Repository navigation
perf(server): Claude item ordinals no longer copy a session-wide map per item - #13864
Conversation
…per item The Claude V2 adapter kept every item ordinal of the session in one Map that it copied on each new item and never pruned, so a long session paid O(items) memory and O(items^2) copying. Ordinals are only ever looked up by the turn that allocated them (a resumed subagent keeps its ordinal on the session subagent registry), so the map now lives on the turn context, is mutated in place, and goes away when the turn settles. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
| providerTurnId, | ||
| providerTurnOrdinal, | ||
| startedAt, | ||
| itemOrdinals: new Map(), |
There was a problem hiding this comment.
This changes ordinal allocation for native item IDs reused across turns: the fresh map now assigns an ordinal in the new turn rather than retaining the previous turn's value. Could you add focused adapter tests covering repeated IDs across turns and resumed subagents, asserting the emitted ordinals and ordering? The existing resume test checks routing but not ordinals.
Posted via Macroscope — Effect Service Conventions
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 focused server-side performance refactor that scopes Claude item ordinal allocation to each provider turn while preserving resumed subagent ordinals. It changes only ordering metadata, introduces no schema, deployment, security, billing, or configuration impact, and the open comment identifies test coverage rather than a concrete defect. You can add or adjust custom eligibility rules. Learn more. |
2e68ee5
into
t3code/codex-turn-mapping
…per item (pingdotgg#13864) Applied with reduced patch context: upstream's claudeSubagentIds helper, from subagent commits the fork doesn't have yet, sits next to the change. The changed lines are upstream's. (cherry picked from commit 2e68ee5) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The Claude V2 adapter kept every item ordinal of the session in one
Mapand copied the whole map on each new item (new Map(current)insideRef.update). Nothing ever pruned it. A long session paid O(items) memory and O(items²) copying: 50k items took 5.5 minutes of adapter time, almost all of it spent copying the map.What changed
The ordinal map now lives on the turn context (
ActiveClaudeTurnContext.itemOrdinals). It is mutated in place, and it goes away when the turn settles and the context is dropped. The per-turn counter map (nextItemOrdinalsByTurn, also copied per item and never pruned) is gone, because the map's size is the counter. OpenCode and Pi already allocate ordinals this way.Why per-turn is safe
Every
resolveItemOrdinalcall passes the active turn's context, and keys are either turn-scoped (terminal-failure:${providerTurnId},usage-limit:${providerTurnId}:…,assistant:${runId}) or native ids first seen in that turn (message uuids, tool_use ids, thinking blocks). The one long-lived reference is a resumed subagent (SendMessage, wake replay, #13668/#13735 restart recovery). It keeps itsturnItemOrdinalon the session subagent registry (existingSubagent?.turnItemOrdinal ?? resolveItemOrdinal(...)) and never looks it up again. Its child items usesubagent.nextChildItemOrdinal, not this map.To confirm, before the fix I instrumented
resolveItemOrdinalto flag any hit on an ordinal a different turn had allocated. Then I ran the full replay suites (all providers: background tasks, wakes, subagent resume, resume after restart, forks, rollback) plus the Claude, Cursor, ACP, Codex, Grok and registry adapter tests: 566 tests, 314 Claude allocations, 0 cross-turn hits. The instrumentation was not committed.Benchmark
A scratch, uncommitted test drove the real adapter through a fake query runner: 10 turns, N/10 root assistant messages each, then a result per turn. Each size ran in a fresh process. Heap is measured after
gc()once all turns settled.Time is the real win. Heap retention is noisy at this scale because the map's keys are strings the emitted events share. Measured alone, a 50k-entry map holds about 1.8 MiB for the life of the session. After the fix, that memory is bounded by the largest single turn.
Verification
vp test runwithClaudeAdapterV2.test.ts,ClaudeReplayFixtures.integration.test.ts,OrchestratorReplayFixtures.integration.test.ts,OrchestratorReplayRecovery.integration.test.tsandOrchestratorReplayFixtures.contract.test.ts: 5 files, 223 tests pass. The replay suites are unchanged.vp exec tsc --noEmit -p .inapps/server: no errors.vp exec knip: nothing new for the touched file.vp linton the touched file: only the existinglayerunused-variable warning.Model: Claude Opus 5.5 (Claude Code)
🤖 Generated with Claude Code