Repository navigation
perf(server): Codex item maps no longer grow and copy for the whole session - #13876
Conversation
…ession Item positions move onto the turn context and are mutated in place, so they live only as long as the turn (or its retained settled context). Streamed plan text is kept in a plain map without per-delta copies and dropped when the plan item completes. 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: Approved at Macroscope's review found this PR approvable — This is a contained one-file server performance refactor that preserves item ordinals and streamed plan assembly while scoping temporary maps to their owning turns and releasing completed plan buffers. It adds no user-facing capability, schema change, product-default change, or static-analysis override. You can add or adjust custom eligibility rules. Learn more. |
93dbb55
into
t3code/codex-turn-mapping
…ession (pingdotgg#13876) Adapted: the completed-plan hunk conflicted on a comment from upstream 0481b76, which the fork doesn't have. The resolution keeps the fork's lines and applies upstream's change to them. (cherry picked from commit 93dbb55) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The Codex V2 adapter kept every item position of the session in one
Map. It copied the whole map on each new item (new Map(current)insideRef.update), along with a per-turn counter map that was also copied per item and never pruned. Streamed plan text worked the same way:planDeltasheld the full markdown of every plan item for the whole session and copied the map on every delta. A long session paid O(items) memory and O(items²) copying. 50k items took 8 minutes of adapter time.This is the same fix as #13864 (Claude), #13866 (Cursor) and #13868 (ACP).
What changed
ActiveCodexTurnContext.itemPositions) and are mutated in place. The map's size is the per-turn counter, sonextItemOrdinalsByTurnis gone. The map goes away with the context once the turn settles, or once a settled turn retained for background commands is released.item/completedarrives for that item.Why per-turn is safe
Every
resolveItemPositioncall passes the context that owns the item:resolveItemEventContext.approvalOwnerCodexTurn), which is still active while its subagent runs.registerSubagentThreadsreturns early for an already-registered thread.To confirm this, I instrumented
resolveItemPositionbefore the fix. It shadowed a per-context map next to the session map and flagged two cases: a hit on an entry some other context had allocated, and any ordinal that differed from the per-context result. I ran the Codex replay suite, the orchestrator replay, recovery and contract suites, the provider-switch, fork, merge-back, steering and selection-restart integration suites, and theCodexAdapterV2andCodexDrivertests: 11 files, 328 tests. That covered 260 allocations and 425 lookups, with 0 cross-turn hits and 0 ordinal mismatches. For plans, noitem/plan/deltaarrived after its plan item completed. The one plan completion in the fixtures carried its full text and also had streamed deltas. The instrumentation was not committed.Benchmark
A scratch test (not committed) drove the real adapter through a replay transcript: 10 turns, N/10
webSearchitems each (item/started+item/completed), thenturn/completed. The last row also streams 1,000 plan deltas per turn and completes the plan item with empty text. Each size ran in a fresh process.Heap after
gc()was within noise (e.g. 131.3 vs 130.2 MiB at 50k), because the map's keys are strings the emitted events share. Once all turns settle, the maps retain nothing.Verification
vp test runinapps/serverwith the fix, on the same 11 files:CodexAdapterV2.test.ts,CodexReplayFixtures.integration.test.ts,OrchestratorReplayFixtures.integration.test.ts,OrchestratorReplayRecovery.integration.test.ts,OrchestratorReplayFixtures.contract.test.ts,ProviderSwitch.integration.test.ts,ThreadFork.integration.test.ts,ThreadMergeBack.integration.test.ts,SteeringCompletion.integration.test.ts,SelectionRestart.integration.test.ts,CodexDriver.test.ts. All 328 tests pass, and no tests or fixtures changed.vp exec tsc --noEmit -p .inapps/server: no errors.vp exec knip --workspace apps/server: nothing for the touched file. The only finding is the existing unlistedccbinary in an ACP test.vp linton the touched file: only the existingmakeCodexAppServerClientFactoryCommandLayerandlayerunused-variable warnings.Model: Claude Opus 5.5 (Claude Code)
🤖 Generated with Claude Code