Skip to content

fix: Codex セッションのモデルを一覧に表示する - #121

Merged
takecchi merged 4 commits into
mainfrom
codiva/codex
Aug 11, 2026
Merged

fix: Codex セッションのモデルを一覧に表示する#121
takecchi merged 4 commits into
mainfrom
codiva/codex

Conversation

@takecchi

Copy link
Copy Markdown
Owner

概要

Codex でセッションを開始すると、セッション一覧のモデル欄が空のままになる問題を修正しました。

--model を明示したときは #109 の対応で表示されるので、埋まらないのは CLI の既定に任せているとき/model で何も選んでいない・/agent で Codex に切り替えた直後)だけでした。既定で使うのが普通なので、実質ほぼ常に空欄に見えていました。

原因(実バイナリ codex-cli 0.147.0 で確認)

codex exec --json は Claude の system/init に当たるもの、つまり解決済みモデル名を一切運びません。どこから取れるかを実測で当たった結果は次のとおりです。

調べた先 結果
codex exec --json の stdout モデル名なしthread.startedthread_id だけ / turn.started は空 / turn.completed は usage だけ)
codex debug models 既定を示す印が無いpriority はあるが「CLI の既定」とは別物)
codex doctor --json config.load.details.model"<default>" としか答えない
rollout の session_meta model_provider"openai")だけで slug は無い
rollout の turn_context model: "gpt-5.6-sol" ← ここだけが解決済み slug を持つ

-m を渡さない実行でも turn_context には解決済みの slug が記録されることを確認しました。

変更内容

src/core/codex-rollout.ts(新規・純粋)

rollout ファイル名の突き合わせ(isCodexRolloutFile)と turn_context.model の抽出(codexRolloutModel / codexRolloutModelFromText)。ファイル名の末尾が thread.startedthread_id なので、時刻部分(ローカル時刻)は当てにせず id で突き合わせます。codex exec resume は同じファイルへ追記していくので、最後の turn_context を採ります。

src/utils/codex.ts(唯一の I/O)

resolveCodexRolloutModel を追加。$CODEX_HOME(既定 ~/.codex)の sessions/<年>/<月>/<日>/新しい日付から 8 ディレクトリまで探し、先頭 512KB だけ読みます(1 ファイルは実測平均 1MB 超。session_metabase_instructions 全文を 1 行で運ぶため)。turn_context はターン開始時に書かれるので数回だけ間を置いて読み直し、それでも読めなければ黙って諦めます(throw しない = モデル欄が空のままになるだけ)。リトライのタイマーは TUI の終了を引き止めないよう unref 済み。

src/core/agent-events.ts

中立イベント model_resolved を追加(status を触らずモデル欄だけを更新)。

session_started / assistant_message にも model は載りますが、あれは「ターンが動いている」区切りでもあるため statusrunning に戻します。問い合わせの答えはターンが終わったあとに届くことがあるので、相乗りさせると完了したセッションが running に巻き戻り、auto-PR まで走りかねません。

src/core/codex-adapter.ts

thread.started で問い合わせを開始し(resolveModel は DI)、ストリームは止めずにイベントの合間へ流します。短いターンで取りこぼさないよう、終端イベントを流したあとにも回収します。--model を明示しているときは問い合わせません(Session が既に表示済み)。/model で既定へ戻したときだけ引き直します。

採らなかった案

カタログ先頭(priority 1)を既定とみなす方法は採っていません。実測では確かに priority 1 = gpt-5.6-sol = 実際の既定でしたが、~/.codex/config.tomlmodel を設定しているユーザーに嘘のモデル名を出すことになるためです。取れないときは空欄のままにする方針(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)

  • Codex で新規セッションを開始し、一覧にモデル名(例: gpt-5.6-sol)が出ること
  • /model で明示選択したときも従来どおり表示されること
  • /agent で Claude → Codex に切り替えたあと、次のターンでモデル欄が Codex のものに入れ替わること
  • codex 未導入・未ログインの環境でセッションが壊れないこと(モデル欄が空のままなら期待どおり)

`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 件とも修正前のコードで落ちることを確認した回帰テスト付き。
@takecchi

Copy link
Copy Markdown
Owner Author

セルフレビューで見つかった 3 点を 84e5ed9 で修正しました。いずれも問い合わせの答えが遅れて届くことに起因していて、放置すると間違ったモデル名が一覧に出たまま state.json に焼き付きます(Codex は二度とモデルを報告しないので、一度ずれると自然回復しません)。

  1. 明示選択を上書きしていた — 「明示指定が勝つ」判定が問い合わせの .then の中だけにあり、答えを保留したあとに /model で選び直されても捨てていなかった。yield する直前に判定し直すようにした(takeResolvedModel)。
    • 再現順序: 問い合わせが解決(保留)→ ユーザーが /model で選択 → 次のイベントで flush。次のターンは選んだモデルで動くのに、一覧は既定のモデル名のまま。
  2. 中断・エージェント切替の後にも流していた — 終端イベント側にはある interrupted のガードが無かった。/agent で Claude へ切り替えると run.interrupt()agent_switchedmodel をクリアするが、その直後に遅れて届いた Codex の slug が Claude のセッションに出てしまう。interrupted / aborted のときは捨て、待ちもしない(Ctrl+C の直後を表示用の問い合わせで引き止めない)。
  3. 空振りを記憶して二度と引き直さなかったturn_context はターン開始時に書かれるので初回が早すぎると空振りするが、probedThread を結果が出る前に latch していたため、次のターンなら即座に読めるのに諦めたままになっていた。空振りは記憶せず、代わりに試行回数の上限(3 回)で「そもそも取れない環境」を打ち切る。

あわせて codexRolloutModelFromText のコメントを訂正しました(「最後の turn_context が勝つ」のは呼び出し側が渡す先頭 512KB の範囲での話。モデルが途中で変わるのは明示選択したときだけで、そのときはそもそも問い合わせません)。

4 件とも修正前のコードで落ちることを確認した回帰テスト付きです(テスト総数 2658 → 2662)。

@takecchi takecchi left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 を読めない環境で毎ターン探し回らない」ための
上限であって、ユーザーが明示的に既定へ戻す操作まで縛るものではない。予算はユーザーの
操作回数で自然に頭打ちになるので、既定へ戻したときにリセットする。

回帰テスト付き(修正前のコードで落ちることを確認)。
@takecchi

Copy link
Copy Markdown
Owner Author

ご指摘ありがとうございます。再現手順どおりの問題でした。6f42202 で修正しています。

setModel(undefined)probedThread に加えて probeAttempts もリセットするようにしました。MAX_MODEL_PROBES は「そもそも rollout を読めない環境で毎ターン探し回らない」ための上限であって、ユーザーが明示的に既定へ戻す操作まで縛るためのものではない、という整理です。予算はユーザーの操作回数で自然に頭打ちになるので、リセットしても探索が発散することはありません。

回帰テストもいただいたケースそのままで追加しました(resolveModel は最初の 3 回 undefined / 4 回目に gpt-5.6-sol、3 ターン空振り後に setModel("gpt-5.4-mini")setModel(undefined) → 次のターン)。予算リセットを外すとこのテストだけが落ちることも確認済みです(asked が 3 のまま止まり model_resolved が出ない)。

ローカルで lint / typecheck / test(2663 件)/ build 緑です。

@takecchi takecchi left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

再レビューで、前回指摘した 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/0208/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` に
置いて解決できること + 年月をまたぐケース。旧上限のコードで落ちることを確認)。
@takecchi

Copy link
Copy Markdown
Owner Author

ご指摘ありがとうございます。おっしゃるとおりで、3ee8f29 で修正しました。

ROLLOUT_MAX_DAY_DIRS = 8 は「当日に無ければ日付跨ぎを疑う程度」のつもりで置いた上限でしたが、resume が開始日の rollout へ追記し続けるという前提と噛み合っていませんでした。codiva のセッションは state.json に無期限で残るので、探索範囲を日数で切ってよい根拠がそもそも無い、という整理です(しかも暦日ではなく「存在する日付ディレクトリ数」なので、指摘のとおり使うほど早く再現します)。

変更

  • 日数の上限を撤廃し、新しい日付から見て最初に当たった時点で止める。当日始まったスレッドなら readdir 数回で終わり、全日付を舐めるのは「そのスレッドの rollout が本当に無い」ときだけになります。
  • 一度見つけたパスを使い回すようにしました。rollout は追記されるだけで移動しないので、リトライは同じ 1 ファイルの読み直しで済み、走査を待ち時間のたびに繰り返しません(元の実装は毎リトライで探索し直していたので、上限を外すと素直に悪化する箇所でした)。
  • 「本当に無い」ときの走査の繰り返しは MAX_ROLLOUT_SEARCHES(3 回)で抑え、それ以降は待たずに諦める。ファイルはスレッド開始時(session_meta)に作られるため、待っても現れないからです。

テスト

いただいた再現ケースをそのまま追加しました(対象 thread を sessions/2026/08/01、別 thread を 08/0208/10 に置いて解決できること)。加えて年月をまたぐケースも入れています。旧上限のコードでは前者が落ちることを確認済みです。

docs/TECH_NOTES.md にも「探索を日数で打ち切らない」理由を追記しました。

ローカルで lint / typecheck / test(2665 件)/ build 緑です。

@takecchi takecchi left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

再レビュー完了です。追加の修正指摘はありません。

前回指摘した「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の問題とは判断していません。前回までの探索予算リセットを含め、今回の変更範囲に追加の問題は見つかりませんでした。

@takecchi
takecchi merged commit 81d249a into main Aug 11, 2026
1 check passed
@takecchi
takecchi deleted the codiva/codex branch August 11, 2026 22:25
takecchi added a commit that referenced this pull request Aug 12, 2026
## 概要

`/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` が届く
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant