Skip to content

Investigate Codex proxy/orchestration OOM from stalled flows and failed prompt-delivery sessions #369

Closed

Description

@Juliusolsson05

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_failed incidents 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
  • fullText
  • blocks
  • 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.completed has cleanup behavior.
  • response.failed and response.incomplete finish/publish idle but appear not to clear activeFlowId, 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.ts mirrors proxy events to JSONL by JSON.stringify and base64-serializing response chunks/request-body evidence. During retry/stall bursts this can create significant transient heap and disk churn.
  • orchestration_create_agent can create a child session and then fail bootstrap prompt delivery. On failure it records orchestration.prompt_delivery_failed and returns an agent with promptSubmitted: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.freshRolloutCandidates can 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.flows
  • FlowState.buffer
  • FlowState.blocks
  • FlowState.fullText
  • ResponsesProxy listeners / mirrored event state
  • CodexHeadless.freshRolloutCandidates
  • live CodexSession / PTY / headless objects after orchestration.prompt_delivery_failed
  • OrchestrationBridge queues or child-session registries

Proposed Fixes To Evaluate

  • Release activeFlowId, mark terminal state, and delete flow state on response.failed and response.incomplete, mirroring response.completed cleanup 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 freshRolloutCandidates and clear on session stop/error.
  • On failed create_agent bootstrap 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.flows or 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

  1. added a commit that references this issue on Sep 25, 2026
  2. Juliusolsson05 commented on Sep 25, 2026

    @Juliusolsson05
    OwnerAuthor

    Quality-loop re-audit (2026-09-25).

    • Counters: done. The perf heartbeat samples codex.proxy.flows and codex.proxy.bufferedChars (src/main/index.ts:1064-1065, from CodexResponsesAdapter.diagnostics() via SessionManager.codexProxyDiagnostics()). gaugeSources.test.ts covers them.
    • Replay: done for completed flows. codex-headless CodexResponsesAdapter.flowRetention.test.ts (pinned ae0e935) replays a recorded session in which 16 of 32 exchanges have no response-end. It asserts flows and bufferedChars return to 0 after each exchange.
    • Replay: no recording exists for failed or incomplete flows. I scanned every ~/.config/agent-code/proxy/**/proxy-events.jsonl: 34 files and 702,129 response-chunk events, decoding each chunk.data byte array. None contains an SSE response.failed or response.incomplete. Those paths are covered only by the synthetic CodexResponsesAdapter.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.

  3. added a commit that references this issue on Sep 25, 2026
  4. Juliusolsson05 commented on Sep 27, 2026

    @Juliusolsson05
    OwnerAuthor

    Closing: every fix and counter landed (triage 2026-09-27, main a70403ef).

    Reopen when: a real recording of a failed or incomplete Codex response turns up, so the replay test can be written.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    class:C6-unboundedUnbounded growthneeds-evidenceNeeds a recording/real use to settleprovider:codexAffects Codex integrationsev:P1Core flow broken or badly degradedtype:perfPerformance or resource use

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions