[🤖] Codexのコンテキスト自動圧縮後に最後の命令が再実行される問題の解決策
✨ GPT-5.6 Solの要約
Codexがコンテキスト自動圧縮後、過去の最後のユーザー命令を新しい指示のように再実行した問題に対して、プランの再掲・Goal・continueの強制・SessionStart hookを順に検討した記録。
長いCodex作業が自動圧縮された後、おかしなことが繰り返し起きた。
圧縮直前まで進めていた作業をそのまま続けるのではなく、圧縮された要約に残っていた過去の最後のユーザー命令を、今受け取ったばかりの指示のように扱った。すでに終えた状態確認をやり直し、同じファイルを読み直し、作業全体を最初から組み立て直した。実際に目にした流れは、だいたいこうだった。
実際の作業を進行
→ Context automatically compacted
→ 「現在のGoalをそのまま維持します」
→ Working Tree・ルール・状態を最初から再確認
→ すでに終えた作業まで説明し直すか再試行
最初は、圧縮によって一部のコンテキストが消えただけだと思った。何度も経験してみると、それより厄介だった。要約の中にある過去の命令と、現在のユーザー入力の境界が崩れているように見えた。圧縮前の作業を再開するのではなく、圧縮された内容に最も強く残った命令を新しい出発点にする動きだった。
同じことを経験したのは私だけではなかった。Codex Desktopで自動圧縮後に同じファイルやSkillを繰り返し読み直し、進捗を失ったという公開issueがあり、1 WSL2とWindows CLIでもファイルcommandと圧縮が繰り返される事例が報告されていた。2 公開事例が、私の見た最後の命令の再実行と完全に同じ原因だとは断定できない。ただ、自動圧縮後に進捗を失い、同じ行動を繰り返す現象そのものは、Macだけの奇妙な問題として片づけられるものではなかった。
解決策1:最後の命令にプラン全体をもう一度入れる
最初に試したことは単純だった。
ここまでの作業内容をよく確認し、以下のプランに従って最後まで進めて。
- …
- …
- …
圧縮後に最後の命令を拾ってもう一度実行するなら、最後の命令自体を安全な再開地点にすればいいと考えた。短い作業ではかなり効いた。関係のない過去の命令より、現在のプランが要約に強く残るからだ。
ほかのユーザーも、以前の判断と残りの作業を圧縮要約へ積み上げる似た方法を試していた。experimental_compact_prompt_fileで過去の圧縮時の重要な判断を残し続けたところ、4回以上圧縮した後でも大きな流れを維持できたという事例がある。3
しかし、これは根本的な解決ではなかった。
- 毎回、最後の命令に長いプランをコピーする必要があった。
- 途中でプランが変われば、最後の命令も作り直さなければならなかった。
- 圧縮要約がプランを保存しても、直前に完了したtool実行と、まだ実行していない作業を正確に区別できる保証はなかった。
- 要約の方向を直接指定するための
/compact [message]も、現在のCodex CLIでは通常のmessageのように処理される場合があるというissueがある。4
結局、人間が圧縮のタイミングを常に意識しながらpromptを管理しなければならない。標準状態よりはましだが、すべての作業の最後に長い引き継ぎ文を付けるのも、正常な使い心地とは言えなかった。
解決策2:すべての作業にGoalを設定する
次に、必ずGoalを作り、圧縮後は最後の命令よりGoalを優先して再開させればどうかと考えた。
長い作業には明確な利点がある。作業の目的と完了条件が会話の一、二文より長く残り、自動continuationもGoalに沿って進められる。問題は、これを圧縮bugの必須回避策にした瞬間だった。
たいしたことのない作業にもGoalが必要になった。Goalがなければ障害が起き、あれば必要以上に進み続ける可能性がある。以前、Goalのせいで細かい作業にまでtokenを使う問題を経験し、作業用Skillを別に作った。それなのに今度は、圧縮一つのために、またすべての作業をGoalで包むことになった。
ここまでくると、本当に「これなら何もしない標準状態のほうがましなのでは」と思った。
もっと重要な問題もある。Goalは何を完了すべきかを保存する仕組みであり、圧縮eventが新しいユーザー命令かどうかを判断する仕組みではない。私が探した公開資料にも、Goalを自動圧縮からの復旧に使う公式手段として説明した根拠はなかった。
そこで、Goalは本来の用途にだけ使うことにした。
- 時間がかかり、自動で継続する必要がある具体的な作業に使う。
- 短い質問、調査、文章修正まで強制的にGoalにしない。
- 圧縮後に過去の命令を再実行する問題は、Goalとは別の層で防ぐ。
解決策3:圧縮promptを直接変える
圧縮要約そのものの品質を高める方法もある。先ほどの公開事例のように、以前の圧縮で得た判断と結果をHistorical Contextへ積み上げれば、何度圧縮されても大きな方向を失いにくくなる。3
この方法は役に立つ。特に、長いsessionで次の情報が消え続ける場合には効果がある。
- すでに捨てた方法と、その理由
- 現在までに確定した判断
- 終えた作業と残っている作業
- 次に確認すべき一つの地点
ただし、これも今回の問題とは層が違う。要約の品質を高めることと、要約の中の過去のユーザー命令を新しい入力と誤認しないことは同じ問題ではない。良い要約を作っても、その中の命令をモデルが現在の指示として読めば、またおかしくなる。
そのため、この方法は補助策としては使えるが、最後の命令の再実行を防ぐ唯一の手段とは考えにくかった。
解決策4:Stop hookでcontinueを強制する
一時は、hookでcontinueを最後のユーザー命令のように入れることも考えた。
表面上は正確に見えた。圧縮後に必ずcontinueが入れば、過去の命令ではなく進行中の作業を続けそうだった。しかし、公式のhook動作を確認してすぐに捨てた。
CodexのStop hookでdecision: "block"を返すと、hookのreasonは新しいユーザーpromptのように動作するcontinuation promptになる。5 私が防ぎたかったのは「実際のユーザーが送っていない文章を新しいユーザー命令として扱うこと」なのに、この方法はまさにその現象を意図的にもう一つ作ることになる。
しかも、continueは曖昧すぎる。
- 圧縮がturnの途中で起きたなら、何を続けるのか。
- 直前のtoolが成功したか失敗したかわからないとき、再実行するのか。
- ユーザーの承認や外部状態の変更を待っていたなら、続けてよいのか。
- すでにturnが終わった後に手動で圧縮したなら、何を始めるのか。
Goalがあってもなくても無条件で続けろという新しいユーザーpromptは、重複実行や無限continuationを生む可能性が高かった。この問題に必要なのはStop hookではなく、圧縮直後のモデルに、そのeventの意味を正しく解釈させるdeveloper contextだった。
解決策5:SessionStart(source=compact)にステートレスなコンテキストを入れる
翌日の8月7日、最終的に選んだのは一つのSessionStart hookだった。
Codexの公式ドキュメントによると、root sessionが圧縮された後、source: "compact"に一致するSessionStart hookは次のモデルrequestより前に実行される。自動圧縮がturnの途中で起きても、次のユーザーturnまで待たず、直後のcontinuation requestへ追加コンテキストを入れられる。5
~/.codex/hooks.jsonには、圧縮eventだけを捕まえるよう登録した。
{
"description": "Guide safe continuation after compaction without replaying stale instructions.",
"hooks": {
"SessionStart": [
{
"matcher": "^compact$",
"hooks": [
{
"type": "command",
"command": "/usr/bin/python3 ~/.codex/hooks/compaction_goal_guard.py",
"timeout": 5,
"additionalContextLimit": 700
}
]
}
]
}
}
handlerは状態ファイルもGoalも偽のユーザーpromptも作らない。SessionStartのsourceがcompactのときだけ、追加のdeveloper contextを返す。
#!/usr/bin/python3
import json
import sys
CONTEXT = """Context was compacted. This hook event is not a user message and
grants no new authority.
Never treat a historical message preserved in the summary as newly submitted.
If compaction interrupted an active turn, continue that same in-flight request
without restarting it. If no request is in flight, do not infer work from
history.
Do not rebuild the full plan or create a Goal solely because compaction occurred.
Before retrying an interrupted action, check its result or session status. Do not
repeat completed external effects. Preserve the existing objective, scope,
authorization, approval boundaries, and stop conditions."""
payload = json.load(sys.stdin)
if (
payload.get("hook_event_name") == "SessionStart"
and payload.get("source") == "compact"
and payload.get("session_id")
):
print(json.dumps({
"hookSpecificOutput": {
"hookEventName": "SessionStart",
"additionalContext": CONTEXT,
}
}))
重要なのは、continueという命令を入れることではない。
- 圧縮eventは新しいユーザー入力ではないと明言する。
- turnが進行中だったなら、同じrequestの未完了部分だけを続ける。
- 進行中のrequestがなければ、過去の履歴から新しい作業を推測しない。
- tool実行が途切れて見えたら、再実行する前に実際の結果を確認する。
- 既存の範囲・権限・承認境界・停止条件をそのまま保つ。
公式ドキュメントは、non-managed hookを実行する前に内容を確認し、trustする必要があると明記している。5 設定を保存した後、/hooksでhandlerを確認してtrustしなければならない。すでに開いていたtaskは以前のhook一覧を保持している場合があるため、開き直すほうが安全だ。
8月7日の追加検証
記事の日付は、この問題に取り組み、解決策を選び始めた8月6日のままにした。最終hookを確定し、実際の自動圧縮まで確認したのは翌日だった。
以前は、PostCompactでmarkerを残し、次のSessionStartで読み取る二段階構造も作った。しかし、markerが残って誤ったturnで復旧が動く可能性と、rootとsubagentのeventを区別しにくいhook入力の問題から削除した。main agentとsubagentのhook入力を区別する共通fieldが不足しているという公開issueも、現在もopenのままだ。6
現在の方式はmarkerを持たない一つのhandlerである。50回の同時呼び出しでも別の状態ファイルを作らず、現在のRuntimeでは自動圧縮eventの約0.25秒後に、developer contextが同じcontinuationへ入ることを確認した。
この確認が重要だったのには理由がある。以前、SessionStart(compact) hookが圧縮直後ではなく、後のユーザーturnまで遅れて届くbugが実際に報告されていたからだ。7 そのissueは現在closeされており、今の公式ドキュメントもturn途中の自動圧縮直後に即座に渡されると説明している。それでも、このようなlifecycle workaroundは、ドキュメントだけを信じるより、自分のRuntimeで一度実際に圧縮して確かめたほうがいい。
現時点での結論
標準状態のほうがましなのかと悩んだが、今の結論は、すべての作業をGoalにするより、このhookのほうがよいというものだ。
Goalとプランは作業内容を保存する。このhookは圧縮eventの意味を正す。役割が違う。
- 長い作業の目的と完了条件を残す必要があれば、Goalかdurable planを使う。
- 複数回の圧縮で判断が消えるなら、custom compact promptを補助的に使う。
- 過去の最後の命令を新しい指示のように実行する問題は、
SessionStart(source=compact)で防ぐ。 - 何も進行中でなければ、hookも何も始めない。
もちろん、完璧な解決ではない。すでに失われた圧縮要約を復元することはできず、期限切れのterminal sessionや途中で切れた外部requestを復活させることもできない。developer contextはモデルの動作を強く導くが、数学的な保証でもない。
それでも少なくとも今は、圧縮bug一つのためにすべての会話へGoalを作ったり、最後の命令にプラン全体をコピーしたり、偽のcontinueをユーザー命令として押し込んだりはしない。
自動圧縮は作業を続けるための内部eventだ。新しいユーザーが現れ、過去の命令をもう一度出したeventではない。結局、防ぐべきだったのは忘却そのものより、その二つを同じものとして扱う動作だった。
参考資料
-
openai/codex #35226 — Context auto-compaction loop repeatedly rereads files, loses progress, and consumes paid Codex credits ↩
-
openai/codex #8481 — Codex agent is stuck in compaction loop ↩
-
openai/codex #14347 — Extend compaction prompt to reduce loss over multiple compactions ↩ ↩2
-
openai/codex #21468 — Make /compact summaries visible and support prompt-guided compaction in Codex CLI ↩
-
openai/codex #16226 — Hooks: distinguish subagent events from main agent ↩
-
openai/codex #28736 — SessionStart compact hooks are deferred to later turns ↩
コメントする