Skip to content

fix(server): Codex V2 threads show todo lists again - #13546

Merged
juliusmarminge merged 2 commits into
t3code/codex-turn-mappingfrom
v2/codex-update-plan
Sep 25, 2026
Merged

juliusmarminge merged 2 commits into
t3code/codex-turn-mappingfrom
v2/codex-update-plan

Conversation

@juliusmarminge

@juliusmarminge juliusmarminge commented Sep 25, 2026 •

Copy link
Copy Markdown
Member

Codex made its update_plan checklist tool opt-in in openai/codex#41744, first shipped in rust-v0.152.0. tools.update_plan.enabled now defaults to false (resolve_update_plan_enabled in codex-rs/core/src/config/mod.rs), and while it is off Codex also strips the update_plan guidance from its bundled prompts. The V2 Codex adapter never turned it on, so on Codex 0.152 and later (T3 requires 0.156) Codex threads produce no todo lists. The Codex todo_list fixture only kept working because the recorder turned the option on for that one scenario (#13533).

What changed

Adapter. codexThreadRuntimeParams now always sends config: { "tools.update_plan.enabled": true }, plus the t3-code MCP server when one is attached. The adapter builds thread/start, thread/resume and thread/fork from this helper, so all three carry it. The key is dotted: Codex applies config entries as dotted-path overrides, so this sets only tools.update_plan.enabled and leaves the user's other [tools] settings alone.

Why all three requests: Codex builds a thread's config when it loads the thread, not once per process. Its app server passes config from start, resume and fork into the same override path, ConfigManager::load_with_cli_overrides in codex-rs/app-server/src/config_manager.rs. A resumed thread that is not loaded therefore gets its config rebuilt from that resume request. If the thread is already loaded, Codex ignores the request's config and logs "config overrides were provided and ignored while running". It keeps the config the thread already has, which T3 set when it started the thread. Subagents copy their parent turn's config, so they inherit the tool too.

Interaction with a user's own Codex config. Codex puts request config in the SessionFlags layer (precedence 30). That layer sits above system (10), user ~/.codex/config.toml (20, or 21 with a profile) and project .codex/config.toml (25). The only layers above it are legacy managed config (40/50). So in T3:

  • Codex threads always have update_plan, even if the user's config.toml sets tools.update_plan.enabled = false. The old behavior matched Codex's default and was not a choice anyone made; the maintainers decided to enable it. Admin-managed managed_config.toml still wins.
  • There is no new T3 setting. Standalone Codex (CLI/TUI) is unaffected.
  • Plan Mode is unchanged. Codex rejects update_plan in Plan mode ("update_plan is a TODO/checklist tool and is not allowed in Plan mode"). T3's Plan Mode developer instructions (CodexDeveloperInstructions.ts) already tell the model exactly that, and they now describe a tool the model actually has, so they need no change. The Default-mode instructions don't mention update_plan.

Replay now matches the thread config T3 owns. Replay used to delete config (plus cwd and model) from thread/start, thread/resume and thread/fork before comparing frames. turn/plan/updated is inbound and replays either way, so no fixture failed when the adapter stopped sending the flag. packages/effect-codex-app-server/src/replay.ts now keeps config and drops only two parts of it:

  • the mcp_servers entry, which carries the host's local MCP URL and a short-lived Authorization header;
  • keys the transcript header lists in metadata.recorderThreadConfigKeys. These are settings only the recorder sends, and the adapter never does: skills.include_instructions (subagent) and agents.max_depth (subagent_v2_nested).

cwd and model are still ignored as before. Resume goes out through client.raw.request, which writes to the same replay transport, so thread/resume config is matched too.

Recorder. record-codex-app-server-replay-fixture.ts sends the adapter's CODEX_THREAD_CONFIG on every thread/start, thread/resume and thread/fork, like the adapter. todo_list no longer sets the option itself. A scenario's threadConfig merges over the shared config, and its keys go in the header as recorderThreadConfigKeys.

Fixtures. I re-recorded todo_list/codex_transcript.ndjson live on Codex 0.156.1 / gpt-6-luna, with the recorder no longer forcing the option. Its thread/start sends only {"tools.update_plan.enabled":true}. Codex emitted two turn/plan/updated notifications, and the last one has all three steps completed. The other 24 Codex transcripts are not re-recorded. They get the adapter's config added to their 36 outbound thread frames, and subagent and subagent_v2_nested get recorderThreadConfigKeys in their headers. Nothing else in them changes. Hand-built thread frames in CodexAdapterV2.test.ts now expect CODEX_THREAD_CONFIG.

No unit test. The first revision added a CodexAdapterV2.test.ts case asserting the params, because replay could not see config. Replay now catches this, so I removed that test and the harness hook it needed.

Verification

With the flag removed (CODEX_THREAD_CONFIG = {}), every Codex replay fails with CodexAppServerReplayFrameMismatchError on thread/start, e.g. todo_list: expected "config":{"tools.update_plan.enabled":true}, received "config":{}.

  • OrchestratorReplayFixtures -t codex: 19/19 fail, each on thread/start.
  • OrchestratorReplayRecovery (provider_thread_resume), ThreadFork (thread_fork_native, thread_fork_native_prior_turn), ThreadMergeBack (2) and OrchestratorMcpToolkit (delegated_task_status): 6/6 fail, each on thread/start.
  • With the flag restored and only the thread/resume frame's config removed from provider_thread_resume, OrchestratorReplayRecovery fails. So resume config is matched too.
  • The failures surface as the scenario's 60s wait timing out: the run stays starting after ensureThread fails. They are not an immediate frame-mismatch assertion. I got the mismatch text by logging replay failures in a local, uncommitted probe.

With the fix:

  • vp test run src/orchestration-v2/testkit/OrchestratorReplayFixtures.integration.test.ts -t codex: 19 passed.
  • vp test run on OrchestratorReplayRecovery, CodexReplayFixtures, ThreadFork, ThreadMergeBack, OrchestratorReplayFixtures.contract, OrchestratorMcpToolkit, CodexAdapterV2.test.ts and CodexThreadRevert.test.ts: 137 passed.
  • vp test run in packages/effect-codex-app-server: 36 passed.
  • vp exec tsc --noEmit in apps/server and in packages/effect-codex-app-server: no error TS or warning TS.
  • vp run knip:check: clean.
  • vp lint on the touched files: only the two existing no-unused-vars warnings in CodexAdapterV2.ts, which are also on the base.
  • A scratch recording of subagent_v2_nested wrote recorderThreadConfigKeys: ["agents.max_depth"] and config: {"tools.update_plan.enabled":true,"agents.max_depth":3}, matching the backfilled transcript. I discarded it.
  • Leak check on the new todo_list transcript: no home paths, hostname, tailnet names or IPs, tokens or Authorization headers. installationId and planType are scrubbed.
  • Live recording: T3_CODEX_BIN=…/codex-0.156.1/…/codex node ../../apps/server/scripts/record-codex-app-server-replay-fixture.ts --scenario todo_list, run from packages/effect-codex-app-server, the same cwd as the test(server): record Codex replay fixtures on gpt-6-luna and Codex 0.156.1 #13533 recordings. I used Node because bun isn't installed on the recording machine.

Not run: repo-wide checks, and no manual check in a running T3 client.

Model: Claude Opus 5.5 (Claude Code)

🤖 Generated with Claude Code

Codex 0.152 made the update_plan checklist tool opt-in
(tools.update_plan.enabled defaults to false), so Codex threads stopped
producing todo lists. The V2 adapter now sends
tools.update_plan.enabled = true in the config of every thread/start,
thread/resume and thread/fork. The recorder sends the same config for
every scenario instead of only todo_list, and the Codex todo_list
fixture is re-recorded live on Codex 0.156.1 with only that config.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:L 100-499 changed lines (additions + deletions). labels Sep 25, 2026
@github-actions

github-actions Bot commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

ℹ️ No successful main baseline artifact is available yet. This run establishes the initial measurement.

Provider Metric Main baseline This PR Impact PR ceiling
Codex Total thread wire — 4.9 KiB — 6.8 KiB ✅
Codex Thread snapshot wire — 3.7 KiB — 4.9 KiB ✅
Codex Live turn WebSocket wire — 1.2 KiB — 2.0 KiB ✅
Codex Live turn WebSocket decoded — 20.4 KiB — 29.3 KiB ✅
Codex Live turn messages — 2 — 8 ✅
Claude Total thread wire — 4.9 KiB — 6.8 KiB ✅
Claude Thread snapshot wire — 3.7 KiB — 4.9 KiB ✅
Claude Live turn WebSocket wire — 1.2 KiB — 2.0 KiB ✅
Claude Live turn WebSocket decoded — 20.7 KiB — 29.3 KiB ✅
Claude Live turn messages — 1 — 8 ✅

Baseline: unavailable · PR result: 00fa290 · Source CI: failure

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 106.1 KiB
  • Claude decoded thread snapshot: 106.4 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

@macroscopeapp

macroscopeapp Bot commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — This change unconditionally enables Codex's update-plan tool across all V2 thread start, resume, and fork paths, restoring visible todo lists but also changing the product's default behavior and configuration precedence. The production-wide default change warrants human review.

You can add or adjust custom eligibility rules. Learn more.

Replay used to drop `config` from thread/start, thread/resume and
thread/fork, so no fixture noticed when the adapter stopped sending
tools.update_plan.enabled. Replay now keeps `config` and drops only the
host-supplied `mcp_servers` entry and the recorder-only keys a transcript
lists in `metadata.recorderThreadConfigKeys`. The recorder writes that
list, and the committed Codex transcripts get the adapter's config
backfilled. The unit test that stood in for this coverage is removed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@juliusmarminge
juliusmarminge merged commit 4000976 into t3code/codex-turn-mapping Sep 25, 2026
24 of 25 checks passed
@juliusmarminge
juliusmarminge deleted the v2/codex-update-plan branch September 25, 2026 05:07
juliusmarminge added a commit that referenced this pull request Sep 25, 2026
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L 100-499 changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant