fix: Codex セッションのモデルを一覧に表示する - #121
Conversation
`codex exec --json` は解決済みモデルを一切運ばない(実測 codex-cli 0.147.0)ため、 `--model` を明示していないセッション(= CLI の既定に任せている)のモデル欄が一覧で 空のままだった。`--model` を明示した場合は #109 で表示されるので、埋まらないのは 既定で動いているときだけ。 `codex debug models` に既定を示す印は無く、`codex doctor --json` も `"<default>"` と しか答えない。CLI が解決結果を書き残すのは rollout の `turn_context` だけなので、 そこを読む: - `core/codex-rollout.ts`(新規・純粋): rollout ファイル名の突き合わせと `turn_context.model` の抽出 - `utils/codex.ts` の `resolveCodexRolloutModel`: `$CODEX_HOME/sessions/**` の探索と 先頭 512KB の読み出し(1 ファイルは平均 1MB 超)。`turn_context` はターン開始時に 書かれるので数回だけリトライし、読めなければ黙って諦める - `core/agent-events.ts` に `model_resolved` を追加: 答えはターンが終わったあとに 届くことがあり、`session_started` / `assistant_message` に相乗りさせると `status` が `running` へ巻き戻るため、モデル欄だけを触る中立イベントにした - `core/codex-adapter.ts`: `thread.started` で問い合わせを始め、ストリームは止めずに イベントの合間へ流す(短いターンで取りこぼさないよう終端イベントの後でも回収する) カタログ先頭(priority 1)を既定とみなす当て推量は採らない。実測では一致したが、 `~/.codex/config.toml` で `model` を設定しているユーザーに嘘のモデル名を出すため。
セルフレビューで見つかった 3 点。いずれも「問い合わせの答えが遅れて届く」ことに 起因していて、放置すると**間違ったモデル名が一覧に出たまま state.json に焼き付く**。 1. 明示選択を上書きしていた: 「明示指定が勝つ」判定が問い合わせの `.then` の中だけに あり、保留したあとに `/model` で選び直されても捨てていなかった。Codex は二度と モデルを報告しないので、一度ずれると永久にずれたままになる。yield する直前に 判定し直す(`takeResolvedModel`)。 2. 中断・エージェント切替の後にも流していた: 終端イベント側にはある `interrupted` の ガードが無く、`/agent` で Claude へ切り替えた直後に遅れて届いた Codex の slug が Claude のセッションに出てしまう(`agent_switched` が model をクリアした直後に上書き されるため)。`interrupted` / `aborted` のときは捨て、待ちもしない。 3. 空振りを記憶して二度と引き直さなかった: `turn_context` はターン開始時に書かれる ので初回が早すぎると空振りするが、`probedThread` を先に latch していたため、次の ターンなら即座に読めるのに諦めたままになっていた。空振りは記憶せず、代わりに 試行回数の上限(3 回)で「そもそも取れない環境」を打ち切る。 `codexRolloutModelFromText` のコメントも修正(「最後の turn_context が勝つ」のは 呼び出し側が渡す**先頭 512KB の範囲**での話。モデルが途中で変わるのは明示選択した ときだけで、そのときはそもそも問い合わせない)。 4 件とも修正前のコードで落ちることを確認した回帰テスト付き。
|
セルフレビューで見つかった 3 点を
あわせて 4 件とも修正前のコードで落ちることを確認した回帰テスト付きです(テスト総数 2658 → 2662)。 |
takecchi
left a comment
There was a problem hiding this comment.
1件、修正が必要な問題があります。
[Medium] 探索上限に達した後は、/model で既定へ戻してもモデルを再解決できません。
setModel(undefined) は probedThread だけをクリアしますが、probeAttempts は 3 のままです。そのため rollout が一時的に読めず最初の3ターンが空振りしたセッションでは、その後に「明示モデルを選択 → CLI の既定へ戻す」と操作しても、probeModel() が probeAttempts >= MAX_MODEL_PROBES で即 return します。コメントにある「既定に戻したら次のターンで引き直す」が成立せず、モデル欄は明示モデルの表示から更新されないまま永続化されます。
PR head に次の回帰ケースを追加して再現しました。
resolveModelを最初の3回はundefined、4回目はgpt-5.6-solにする- 3回空振り後に
setModel("gpt-5.4-mini")→setModel(undefined) - 次のターンを実行
結果は asked が期待値4に対して3のままで、追加テストが失敗しました(既存の codex-adapter.spec.ts 33件は成功)。setModel(undefined) で探索予算もリセットするか、予算を「既定へ戻した世代」ごとに持つ必要があります。あわせて、上限到達後のリセットを固定するテストを追加すると安全です。
GitHub CI の既存 check は成功していることも確認しました。
レビュー指摘の修正。`setModel(undefined)` は `probedThread` だけをクリアしていて `probeAttempts` を戻していなかったため、探索上限(3 回)に達したあとは「既定に戻したら 次のターンで引き直す」が成立していなかった。 再現: rollout が一時的に読めず最初の 3 ターンが空振りしたセッションで、そのあと 「明示モデルを選択 → CLI の既定へ戻す」と操作しても `probeModel()` が `probeAttempts >= MAX_MODEL_PROBES` で即 return し、モデル欄は明示モデルの表示から 更新されないまま永続化される。 `MAX_MODEL_PROBES` は「そもそも rollout を読めない環境で毎ターン探し回らない」ための 上限であって、ユーザーが明示的に既定へ戻す操作まで縛るものではない。予算はユーザーの 操作回数で自然に頭打ちになるので、既定へ戻したときにリセットする。 回帰テスト付き(修正前のコードで落ちることを確認)。
|
ご指摘ありがとうございます。再現手順どおりの問題でした。
回帰テストもいただいたケースそのままで追加しました( ローカルで lint / typecheck / test(2663 件)/ build 緑です。 |
takecchi
left a comment
There was a problem hiding this comment.
再レビューで、前回指摘した setModel(undefined) 時の探索予算リセットは修正されていることを確認しました。追加された回帰テストも含め、関連4ファイルの既存テスト100件は成功しています。
そのうえで、別の修正が必要な問題が1件あります。
[Medium] 8個より古い日付ディレクトリにある resume セッションのモデルは永久に解決できません。
resolveCodexRolloutModel() は ROLLOUT_MAX_DAY_DIRS = 8 により、新しい日付ディレクトリから8個しか探索しません。一方、このPR自身の説明どおり codex exec resume はスレッド開始時の同じ rollout ファイルへ追記します。したがって、スレッド開始後に9日分以上の別セッションが作られた状態で古いセッションを復元・resumeしても、対象ファイルは古い日付ディレクトリに残ったままで探索範囲に入りません。リトライしても探索範囲は同じなので、モデル欄は空のままです。長期保存されたセッションや、しばらく経ってから /agent で古い Codex conversation に戻る経路で発生します。
PR head に次の回帰ケースを追加して再現しました。
sessions/2026/08/01に対象 thread の rollout を作る08/02〜08/10に別 thread の rollout を置く- 対象 thread を
resolveCodexRolloutModel()で解決する
期待した gpt-5.6-sol に対して undefined となり、追加テストが失敗しました(同ファイルの既存22件は成功)。
対象 thread id で見つかるまで日付ディレクトリを探索するか、少なくとも永続化/resume可能なセッション寿命と整合する探索方法にする必要があります。単なる暦日ではなく「存在する日付ディレクトリ8個」の上限なので、利用頻度が高いほど短期間で再現する点にも注意が必要です。
GitHub CIは最新commitで成功しています。
レビュー指摘の修正。`ROLLOUT_MAX_DAY_DIRS = 8` により新しい日付ディレクトリを 8 個しか 見ていなかったため、**古いセッションを復元・resume したときにモデルを永久に解決できない**。 `codex exec resume` はスレッドを開始した日に作られた同じ rollout へ追記し続ける一方、 codiva のセッションは `.codiva/state.json` に無期限で残って何日も後に復元される。 上限は暦日ではなく「存在する日付ディレクトリ数」なので、間に別セッションを 9 日ぶん 作っただけで対象が探索範囲から外れる(利用頻度が高いほど早く再現する)。リトライしても 探索範囲は同じなのでモデル欄は空のまま。 - 日数の上限を撤廃し、**新しい順に見て最初に当たった時点で止める**(当日始まった スレッドなら readdir 数回。全日付を舐めるのは「本当に無い」ときだけ) - 一度見つけたパスは使い回す。rollout は追記されるだけで移動しないので、リトライは 同じ 1 ファイルの読み直しで済み、走査を待ち時間のたびに繰り返さない - 「本当に無い」ときの走査の繰り返しは `MAX_ROLLOUT_SEARCHES`(3 回)で抑え、 それ以降は待たずに諦める(ファイルはスレッド開始時に作られるので、待っても現れない) 回帰テスト付き(指摘どおりの再現ケース: 対象を `08/01`、別スレッドを `08/02`〜`08/10` に 置いて解決できること + 年月をまたぐケース。旧上限のコードで落ちることを確認)。
|
ご指摘ありがとうございます。おっしゃるとおりで、
変更
テストいただいた再現ケースをそのまま追加しました(対象 thread を
ローカルで lint / typecheck / test(2665 件)/ build 緑です。 |
takecchi
left a comment
There was a problem hiding this comment.
再レビュー完了です。追加の修正指摘はありません。
前回指摘した「8個より古い日付ディレクトリのresumeセッションを探索できない」問題は、次の変更で解消されていることを確認しました。
- 日付ディレクトリ数による打ち切りを撤廃し、対象threadが見つかるまで新しい順に探索
- 一度見つけたrolloutパスをリトライ間で再利用
- ファイル自体が見つからない場合の全走査は
MAX_ROLLOUT_SEARCHESで制限 - 9日分より古いケースと年月を跨ぐケースの回帰テストを追加
確認結果:
- 関連4ファイル: 102 tests passed
npm run typecheck: 成功- GitHub CI check: 成功
ローカルの npm run lint は共有 node_modules に @biomejs/cli-darwin-arm64/biome が無く起動できませんでしたが、最新GitHub CIのlintを含むcheckが成功しているため、PRの問題とは判断していません。前回までの探索予算リセットを含め、今回の変更範囲に追加の問題は見つかりませんでした。
## 概要 `/agent` で Codex に切り替えて始めたセッションの**モデル欄が一覧で空のまま**になる問題を直します。 #121 で「rollout の `turn_context` を読む」仕組みを入れましたが、実バイナリで計測すると **一度も答えを拾えていませんでした**(問い合わせが早すぎて必ず空振りする)。 ## 原因(実測 codex-cli 0.147.0) 実際に `codex exec --json` を走らせて計測した順序: | 時刻(`thread.started` 基準) | 起きること | |---|---| | +0.0s | stdout に `thread.started` | | ~+0.2s | rollout ファイルが作られる(`session_meta` が先) | | **~+3.0s** | **`turn_context`(モデル slug)が書かれる**(入力 `response_item` と `world_state` のあと) | | +7.2s | `turn.completed` | つまり `turn_context` は「ターン開始時」ではなく**約 3 秒遅れて**書かれます。 一方 codiva 側は `thread.started` を見た直後に問い合わせ、**200ms × 6 回 = 最長 1.2 秒** (さらにファイル探索の上限 `MAX_ROLLOUT_SEARCHES` で実質 0.6 秒)で諦めていました。 空振りを記憶しない作りにはなっていましたが、**引き直す契機が次の `thread.started` しか無い**ため、 1 ターンで終わるセッション(一度指示して結果を待つ = いちばん普通の使い方)では モデル欄が永久に空のままでした。 ## 修正 - **ターンが終わったあとに引き直す(本命)**。その時点なら `turn_context` は必ず書かれているので当たります。 猶予は短く(1.5 秒)— ここだけは `await` するため、取れない環境でターンの合間を引き止めません。 - **ターン中の問い合わせは猶予を 20 秒に伸ばし、指数バックオフで読み直す**。誰も待たないので長くても害が無く、 長いターンでは走行中にモデル欄が埋まります。 - **ファイル探索の回数上限を外す**(猶予そのものが上限)。rollout ファイル自体も `thread.started` の あとに作られるため、「数回探して無ければ諦める」だと出現を待てずに空振りしていました。 - 空振りの予算は 4 回に(1 ターンにつき最大 2 回消費するため)。 ## テスト - ローカルで `lint` / `typecheck` / `test` / `build` を通しています(CI でも確認)。 - 回帰テストを追加: - `codex-adapter.spec.ts`「re-asks at the end of the turn when the in-turn lookup was too early」 = 今回の本命(1 ターンで終わるセッションでもモデルが届く) - `codex.spec.ts`「keeps looking for the rollout file until the deadline」= 探索上限の撤去 - `codex.spec.ts`「gives up within the patience window it was given」= 猶予は必ず守る - **実バイナリでの前後比較**(`/tmp` の使い捨てリポジトリで 1 ターン): - 修正前: `session_started` → `assistant_text` → `turn_completed` … `model_resolved` が**一切来ない** - 修正後: `3.45s model_resolved gpt-5.6-sol` が届く
概要
Codex でセッションを開始すると、セッション一覧のモデル欄が空のままになる問題を修正しました。
--modelを明示したときは #109 の対応で表示されるので、埋まらないのは CLI の既定に任せているとき(/modelで何も選んでいない・/agentで Codex に切り替えた直後)だけでした。既定で使うのが普通なので、実質ほぼ常に空欄に見えていました。原因(実バイナリ codex-cli 0.147.0 で確認)
codex exec --jsonは Claude のsystem/initに当たるもの、つまり解決済みモデル名を一切運びません。どこから取れるかを実測で当たった結果は次のとおりです。codex exec --jsonの stdoutthread.startedはthread_idだけ /turn.startedは空 /turn.completedは usage だけ)codex debug modelspriorityはあるが「CLI の既定」とは別物)codex doctor --jsonconfig.load.details.modelは"<default>"としか答えないsession_metamodel_provider("openai")だけで slug は無いturn_contextmodel: "gpt-5.6-sol"← ここだけが解決済み slug を持つ-mを渡さない実行でもturn_contextには解決済みの slug が記録されることを確認しました。変更内容
src/core/codex-rollout.ts(新規・純粋)rollout ファイル名の突き合わせ(
isCodexRolloutFile)とturn_context.modelの抽出(codexRolloutModel/codexRolloutModelFromText)。ファイル名の末尾がthread.startedのthread_idなので、時刻部分(ローカル時刻)は当てにせず id で突き合わせます。codex exec resumeは同じファイルへ追記していくので、最後のturn_contextを採ります。src/utils/codex.ts(唯一の I/O)resolveCodexRolloutModelを追加。$CODEX_HOME(既定~/.codex)のsessions/<年>/<月>/<日>/を新しい日付から 8 ディレクトリまで探し、先頭 512KB だけ読みます(1 ファイルは実測平均 1MB 超。session_metaがbase_instructions全文を 1 行で運ぶため)。turn_contextはターン開始時に書かれるので数回だけ間を置いて読み直し、それでも読めなければ黙って諦めます(throw しない = モデル欄が空のままになるだけ)。リトライのタイマーは TUI の終了を引き止めないよう unref 済み。src/core/agent-events.ts中立イベント
model_resolvedを追加(statusを触らずモデル欄だけを更新)。session_started/assistant_messageにもmodelは載りますが、あれは「ターンが動いている」区切りでもあるためstatusをrunningに戻します。問い合わせの答えはターンが終わったあとに届くことがあるので、相乗りさせると完了したセッションがrunningに巻き戻り、auto-PR まで走りかねません。src/core/codex-adapter.tsthread.startedで問い合わせを開始し(resolveModelは DI)、ストリームは止めずにイベントの合間へ流します。短いターンで取りこぼさないよう、終端イベントを流したあとにも回収します。--modelを明示しているときは問い合わせません(Sessionが既に表示済み)。/modelで既定へ戻したときだけ引き直します。採らなかった案
カタログ先頭(priority 1)を既定とみなす方法は採っていません。実測では確かに priority 1 =
gpt-5.6-sol= 実際の既定でしたが、~/.codex/config.tomlでmodelを設定しているユーザーに嘘のモデル名を出すことになるためです。取れないときは空欄のままにする方針(permissions: falseと同じで、無いものは無いと出す)にしました。テスト
src/core/codex-rollout.spec.ts(新規): ファイル名の突き合わせ・turn_contextの抽出・resume で追記された複数ターン・読み込み上限で切れた最終行src/utils/codex.spec.ts:mkdtempで$CODEX_HOMEを組み立て、レイアウトどおりの rollout から解決 / 別スレッドを拾わない / 無ければ諦める /turn_contextが後から書かれてもリトライで拾うsrc/core/codex-adapter.spec.ts: 既定セッションで報告する(終端イベントの後に流れることも固定)/ 明示指定時は問い合わせない / スレッドごとに 1 回・既定へ戻したら引き直す / 問い合わせが失敗してもターンは壊れないsrc/core/agent-events.spec.ts:model_resolvedが完了済みセッションのstatusを巻き戻さない・同値なら同一参照を返すローカルで lint / typecheck / test(2658 件)/ build すべて緑。加えて実
~/.codexに対して動作確認済みです(既定セッション →gpt-5.6-solを 4ms で解決 / 明示指定 → その slug / 未知スレッド →undefined)。確認してほしいこと(TODO)
gpt-5.6-sol)が出ること/modelで明示選択したときも従来どおり表示されること/agentで Claude → Codex に切り替えたあと、次のターンでモデル欄が Codex のものに入れ替わることcodex未導入・未ログインの環境でセッションが壊れないこと(モデル欄が空のままなら期待どおり)