Repository navigation
Investigate Codex proxy/orchestration OOM from stalled flows and failed prompt-delivery sessions #369
Description
Status (quality-loop audit, 2026-09-25)
Landed: codex-headless 757c539d: failed and incomplete responses mark the flow terminal, late chunks are dropped, secondary flows don't buffer, HTTP-failure bodies are capped, and failed-bootstrap children are closed.
Remaining: No flow-count or buffer-byte counters, and no heavy-run evidence that the heap stays bounded.
Acceptance: The perf heartbeat reports adapter flow count and buffer bytes; a replayed burst of failed or incomplete flows leaves flows.size at 0 (from recorded Codex SSE dumps).
Original report
Problem
Recent hard main-process OOMs happened during long multi-agent/Codex-heavy runs. The terminal breadcrumbs immediately before crashes repeatedly showed CodexResponsesAdapter watchdog releases, orchestration prompt-delivery failures, and debug-retention prune bursts.
This issue tracks the most plausible concrete retained-heap root cause candidates. It is a focused child of #327 and #365.
Evidence From Recent Crashes
The retained incident runs include:
orchestration.prompt_delivery_failedincidents during heavy agent creation.- Terminal-only lines like:
[CodexResponsesAdapter] watchdog releasing proxy-* (attribution=completed silent=...)[debug-retention] pruned ... reason=performance-append ...
- Main-process V8 OOMs with old-space around
2.33-2.73GB.
Primary Suspect: Codex Proxy Flow Retention
packages/codex-headless/src/proxy/CodexResponsesAdapter.ts keeps a flows: Map<string, FlowState>. Flow state can retain:
- decoded
buffer fullTextblocks- tool/reasoning accumulators
- response/request metadata
On every response-chunk, the adapter appends decoded bytes to flow.buffer before deciding whether the flow is active. Secondary flows skip parsing/draining and can retain buffered chunks until response-end or watchdog cleanup.
High-risk behavior found in read-only audit:
response.completedhas cleanup behavior.response.failedandresponse.incompletefinish/publish idle but appear not to clearactiveFlowId, mark completed, or delete the flow in the same way.- That can pin the active slot and demote later real flows, matching the observed pattern of many proxy watchdog releases.
Amplifiers
responsesProxy.tsmirrors proxy events to JSONL byJSON.stringifyand base64-serializing response chunks/request-body evidence. During retry/stall bursts this can create significant transient heap and disk churn.orchestration_create_agentcan create a child session and then fail bootstrap prompt delivery. On failure it recordsorchestration.prompt_delivery_failedand returns an agent withpromptSubmitted:false, but does not obviously close/kill the newly created session. Each orphan can retain PTY, headless process, rollout watcher, proxy server/adapter, and debug buffers.CodexHeadless.freshRolloutCandidatescan read and retain rollout candidates while no active rollout is claimed. This is more suspicious when failed-bootstrap/orphan sessions exist.- Debug retention prune bursts are probably not the primary leak, but can add transient allocations and I/O pressure when proxy/debug dirs are large.
Investigation Targets
In heap snapshots, look specifically for retainers under:
CodexResponsesAdapter.flowsFlowState.bufferFlowState.blocksFlowState.fullTextResponsesProxylisteners / mirrored event stateCodexHeadless.freshRolloutCandidates- live
CodexSession/ PTY / headless objects afterorchestration.prompt_delivery_failed OrchestrationBridgequeues or child-session registries
Proposed Fixes To Evaluate
- Release
activeFlowId, mark terminal state, and delete flow state onresponse.failedandresponse.incomplete, mirroringresponse.completedcleanup semantics. - Do not accumulate full buffers for secondary/ignored flows, or cap them by bytes and drop old data.
- Add byte counters for live proxy flow buffers and expose them in incident/perf heartbeat.
- Cap/TTL
freshRolloutCandidatesand clear on session stop/error. - On failed
create_agentbootstrap prompt delivery, either close the just-created child session or mark it explicitly as failed/orphaned with cleanup policy. - Add focused regression coverage for failed/incomplete proxy responses releasing the active slot and deleting flow state.
Acceptance
- Reproducing a burst of failed/incomplete Codex Responses flows does not grow
CodexResponsesAdapter.flowsor retained buffers unboundedly. - Failed orchestration bootstrap does not leave unmanaged live Codex sessions.
- New counters/events can tell us current flow count, active flow id, secondary-buffer bytes, and failed-bootstrap child lifecycle.
- Heavy multi-agent orchestration no longer shows unbounded main-process heap growth from proxy/orchestration retainers.
Activity
fix(perf): #369 review — pin the heartbeat gauge tick; proxy releases…
Juliusolsson05 commentedon Sep 25, 2026
Quality-loop re-audit (2026-09-25).
codex.proxy.flowsandcodex.proxy.bufferedChars(src/main/index.ts:1064-1065, fromCodexResponsesAdapter.diagnostics()viaSessionManager.codexProxyDiagnostics()).gaugeSources.test.tscovers them.CodexResponsesAdapter.flowRetention.test.ts(pinnedae0e935) replays a recorded session in which 16 of 32 exchanges have noresponse-end. It assertsflowsandbufferedCharsreturn to 0 after each exchange.~/.config/agent-code/proxy/**/proxy-events.jsonl: 34 files and 702,129response-chunkevents, decoding eachchunk.databyte array. None contains an SSEresponse.failedorresponse.incomplete. Those paths are covered only by the syntheticCodexResponsesAdapter.httpFailure.test.ts, so the recorded-dump half of the acceptance can't be met yet.Labelled needs-evidence. The next step is a proxy dump from a real failed or incomplete Codex response (for example a context-length
incomplete, or a server error mid-stream). Replay it and assert the gauges return to 0. The heavy-run heap question stays with #327 and #365.docs(quality-loop): progress 13:15 — #1301 merged, q35 pre-merge disp…
Juliusolsson05 commentedon Sep 27, 2026
Closing: every fix and counter landed (triage 2026-09-27, main
a70403ef).757c539d).src/main/index.ts:1073-1074, tested ingaugeSources.test.ts).Reopen when: a real recording of a failed or incomplete Codex response turns up, so the replay test can be written.