fix(server): Codex V2 threads show todo lists again - #13546
Conversation
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>
Thread transfer impact✅ Thread transfer remains within every enforced ceiling.
Baseline: unavailable · PR result: Scenario and decoded snapshot size10 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.
Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed. |
ApprovabilityVerdict: 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>
4000976
into
t3code/codex-turn-mapping
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Codex made its
update_planchecklist tool opt-in in openai/codex#41744, first shipped in rust-v0.152.0.tools.update_plan.enablednow defaults tofalse(resolve_update_plan_enabledincodex-rs/core/src/config/mod.rs), and while it is off Codex also strips theupdate_planguidance 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 Codextodo_listfixture only kept working because the recorder turned the option on for that one scenario (#13533).What changed
Adapter.
codexThreadRuntimeParamsnow always sendsconfig: { "tools.update_plan.enabled": true }, plus thet3-codeMCP server when one is attached. The adapter buildsthread/start,thread/resumeandthread/forkfrom this helper, so all three carry it. The key is dotted: Codex appliesconfigentries as dotted-path overrides, so this sets onlytools.update_plan.enabledand 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
configfrom start, resume and fork into the same override path,ConfigManager::load_with_cli_overridesincodex-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
configin theSessionFlagslayer (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:update_plan, even if the user'sconfig.tomlsetstools.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-managedmanaged_config.tomlstill wins.update_planin 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 mentionupdate_plan.Replay now matches the thread config T3 owns. Replay used to delete
config(pluscwdandmodel) fromthread/start,thread/resumeandthread/forkbefore comparing frames.turn/plan/updatedis inbound and replays either way, so no fixture failed when the adapter stopped sending the flag.packages/effect-codex-app-server/src/replay.tsnow keepsconfigand drops only two parts of it:mcp_serversentry, which carries the host's local MCP URL and a short-livedAuthorizationheader;metadata.recorderThreadConfigKeys. These are settings only the recorder sends, and the adapter never does:skills.include_instructions(subagent) andagents.max_depth(subagent_v2_nested).cwdandmodelare still ignored as before. Resume goes out throughclient.raw.request, which writes to the same replay transport, sothread/resumeconfig is matched too.Recorder.
record-codex-app-server-replay-fixture.tssends the adapter'sCODEX_THREAD_CONFIGon everythread/start,thread/resumeandthread/fork, like the adapter.todo_listno longer sets the option itself. A scenario'sthreadConfigmerges over the shared config, and its keys go in the header asrecorderThreadConfigKeys.Fixtures. I re-recorded
todo_list/codex_transcript.ndjsonlive on Codex 0.156.1 /gpt-6-luna, with the recorder no longer forcing the option. Itsthread/startsends only{"tools.update_plan.enabled":true}. Codex emitted twoturn/plan/updatednotifications, 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, andsubagentandsubagent_v2_nestedgetrecorderThreadConfigKeysin their headers. Nothing else in them changes. Hand-built thread frames inCodexAdapterV2.test.tsnow expectCODEX_THREAD_CONFIG.No unit test. The first revision added a
CodexAdapterV2.test.tscase asserting the params, because replay could not seeconfig. 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 withCodexAppServerReplayFrameMismatchErroronthread/start, e.g.todo_list: expected"config":{"tools.update_plan.enabled":true}, received"config":{}.OrchestratorReplayFixtures -t codex: 19/19 fail, each onthread/start.OrchestratorReplayRecovery(provider_thread_resume),ThreadFork(thread_fork_native,thread_fork_native_prior_turn),ThreadMergeBack(2) andOrchestratorMcpToolkit(delegated_task_status): 6/6 fail, each onthread/start.thread/resumeframe'sconfigremoved fromprovider_thread_resume,OrchestratorReplayRecoveryfails. So resume config is matched too.startingafterensureThreadfails. 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 runonOrchestratorReplayRecovery,CodexReplayFixtures,ThreadFork,ThreadMergeBack,OrchestratorReplayFixtures.contract,OrchestratorMcpToolkit,CodexAdapterV2.test.tsandCodexThreadRevert.test.ts: 137 passed.vp test runinpackages/effect-codex-app-server: 36 passed.vp exec tsc --noEmitinapps/serverand inpackages/effect-codex-app-server: noerror TSorwarning TS.vp run knip:check: clean.vp linton the touched files: only the two existingno-unused-varswarnings inCodexAdapterV2.ts, which are also on the base.subagent_v2_nestedwroterecorderThreadConfigKeys: ["agents.max_depth"]andconfig: {"tools.update_plan.enabled":true,"agents.max_depth":3}, matching the backfilled transcript. I discarded it.todo_listtranscript: no home paths, hostname, tailnet names or IPs, tokens orAuthorizationheaders.installationIdandplanTypeare scrubbed.T3_CODEX_BIN=…/codex-0.156.1/…/codex node ../../apps/server/scripts/record-codex-app-server-replay-fixture.ts --scenario todo_list, run frompackages/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 becausebunisn'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