Repository navigation
Description
Symptom
A Codex→Claude provider switch that needs compaction or a portable handoff pins ~354 MiB (80%) of the main-process heap for the whole wait (up to 5 minutes) and stalls the main event loop. Captured on 2026-09-03 with the heap watchdog: main heap climbed 143 MB → 1.94 GB in ~9 minutes, renderer heartbeat froze repeatedly (freeze-diagnostics eventLoopLagMs up to 6.2 s, main eventLoop.meanMs 1591 / maxMs 3934), then the watchdog tripped at 1.97 GB and wrote heap-snapshots/main-…-75074-2f6565.heapsnapshot.
Evidence (dominator tree, scripts/analyze-heapsnapshot.mjs --owners)
354.1 MiB 79.8% object:Timeout [node 2098111] (node:timers/promises setTimeout)
354.1 MiB 79.8% object:Generator waitForPortableCodexSummary
236.0 MiB 53.2% object:Generator compactSourceBeforeSwitch
118.0 MiB 26.6% object:Object {schemaVersion, sourceProvider:'codex', sourceSessionIds, entries: Array(18362)} ×3
Edge walk of the generators' parameters_and_registers:
compactSourceBeforeSwitchregisters holdbeforeandcompacted(same object, 118 MiB) plus a second fullConversationDocument.waitForPortableCodexSummaryregisters holdcurrent(another 118 MiB copy) acrossawait delay(COMPACTION_POLL_MS).
Source session 795127e7-… (Codex rollout, cwd …/bringdown, 18,362 decoded entries).
Root cause
src/main/providerSwitch/compactBeforeSwitch.ts:
before = await source.read(...)decodes the entire rollout into aConversationDocumentand the local stays reachable for the whole function, even though onlydescribeLatestCompaction(before)?.fingerprintandlatestSourceLine(compacted)are ever used from it.waitForNewCompactionandwaitForPortableCodexSummarypoll every 250 ms for up to 300 s, and every poll callssource.read()again — a fullreadFile+decodeJsonl+decodeCodexConversation+ analysis of a 60–150 MB rollout. That is ~4 full decodes per second (hundreds of MB/s of allocation) on the main thread, and each iteration'scurrentis held in the generator across theawait.waitForNewCompaction's return value (compacted) stays alive while the Codex portable-handoff wait runs another ≤300 s.
Net: 2–3 simultaneous full copies retained + continuous re-decode → heap burst, GC pressure, and multi-second event-loop stalls for every open pane while the switch waits.
Expected
The wait should retain only the scalars it needs (compaction fingerprint, baseline line number), re-decode the transcript only when the file has actually grown, and not hold previous snapshots across the poll await.
Impact
- Main-process heap +350 MB to +1.8 GB during a switch; on a 16 GB machine running 20 agents this pushed the whole system into swap.
- Main event loop blocked for seconds at a time → every pane's screen/IPC stalls, renderer heartbeat freeze snapshots.
- Risk of the hard main-process OOM tracked in Heap: main-process OOM under heavy multi-agent orchestration — follow-up to #288 #327.
Environment
Agent Code main @ 6012427, Electron 43, macOS 15 (Darwin 25.2), 8-core M3 / 16 GB. Dev run via npm start.
Refs #327, #365 (this is the "name the retained owner with dominator evidence" acceptance item), #103.
Activity
perf(provider-switch): decode at the located path and finish the last…