Skip to content

Codex resume feed broken: proxy streaming turn dropped + duplicate semantic-history keys #168

Closed

Description

@Juliusolsson05

Summary

After the recent merges (#161/#162/#163 orchestration + MCP, #165 feed semantic-history mitigation, and the codex-headless rollout tail-ownership fix), a resumed Codex pane no longer renders correctly: the user prompt row, live streaming text, and assistant output do not appear while the agent is working.

Two agents independently traced the same debug bundle and found complementary problems. This issue records all of them so the fix covers the whole picture.

Evidence bundle: ~/.config/agent-code/debug-bundles/2026-05-16T17-23-15-956-28a11684
(session 28a11684-f7fc-4399-b14a-e11468a3e4f2, kind codex, resumed).

Finding 1 — Live streaming dropped: proxy Responses turn rejected by the Codex turn-ownership guard

This is the real cause of "not streaming".

Timeline from feed-debug.jsonl / proxy-semantic.json:

  1. turn_started src=rollout opens turn 019e31cf-ea65-7350-8248-a02503cd3418 (the placeholder turn for the just-submitted prompt). It becomes currentTurn.
  2. The proxy then streams the real Responses turn: block_started src=proxy turn=resp_058170eaf07b062b... block 0 and 1, stream_phase responding → idle (~13s of real streaming).
  3. Every proxy event has turnId = resp_0581..., which mismatches the live currentTurn (019e31cf-ea65...). foldSemanticEvent block_started does if (eventTargetsDifferentTurn(ev, currentTurn)) break and drops all of it (src/renderer/src/workspace/semantic/foldEvent.ts:274). Same guard at the other case branches.
  4. Result: the rendered semanticTurnId stays 019e31cf-ea65... the whole response. That rollout turn is archived with text.length === 0 — it never received content. The proxy turn that actually held the streamed text never owned the live slot.

So the live feed shows an empty turn / work indicator for the entire response. The strict "Codex drops mismatched turnIds (flicker defense)" rule has no exception for "current turn is an empty non-proxy placeholder", so a real proxy turn cannot take over.

This guard is pre-existing, but the resume + rollout tail-switch path now reliably opens the rollout placeholder turn before the proxy turn, which exposes it every time on resume.

Finding 2 — Duplicate semantic-history rows / duplicate React keys (regression from #165)

proxy-semantic.json history holds 10 archived turns; 5 of them have the identical turnId rollout-1778952139266 (the Codex bootstrap synthetic turn, re-archived once per replayed bootstrap turn).

Feed.tsx renders each archived turn as:

renderedSemanticHistory.map(turn => (
  <SemanticStreamingTurn key={`semantic-history:${turn.turnId}`} ... />
))

Five siblings get the same React key (semantic-history:rollout-1778952139266), which breaks reconciliation for that subtree (missing/blank rows on update). The archived bootstrap turns also duplicate content already present as committed JSONL entries.

Root cause: semanticHistoryRow archiving in foldEvent.ts appends unconditionally; nothing de-dupes history by turnId, and Codex bootstrap replay keeps the same fallback rollout-<ts> id across fragments. Introduced by #165 (fix/feed-clearing-semantic-history).

Finding 3 — Successful rollout-tail switch surfaces as "transcript unavailable" (related)

Separately observed right after the bundle was captured:

transcript unavailable: Codex resume: switched rollout tail from stale resume file …/rollout-2026-05-16T16-59-48-…jsonl to newer candidate …/rollout-2026-05-16T17-23-38-…jsonl

CodexHeadless.switchTo() emits rollout-error on a successful tail switch (packages/codex-headless/src/CodexHeadless.ts ~line 1076). codexSession.ts:341 forwards rollout-error → jsonl-error; useIpcSubscriptions.ts:536 sets transcriptStatus: 'error' + transcriptError, and TileLeaf.tsx:273 renders "transcript unavailable: …". The transcript is fine — the switch worked. This is a false error banner.

Partly tracked upstream as codex-headless issue #11 (rollout-error used for non-error notices). The fix is to emit a distinct non-error event for a successful switch (or not emit at all).

Suggested fixes

  • Finding 1: allow a proxy-sourced turn to replace the currentTurn when the current turn is an empty, non-proxy, still-live placeholder (no text, no blocks). Keep the strict mismatch guard for turns that already carry live content.
  • Finding 2: de-dupe semantic history by turnId when archiving (replace the older snapshot rather than append); the history buffer is a "latest renderable state per turn" bridge, not an event log.
  • Finding 3: stop switchTo() emitting rollout-error for a successful switch; bump the codex-headless submodule. See codex-headless#11.

Repro

  1. Resume a Codex session that forks its rollout on resume.
  2. Send a prompt; watch the feed during the response.
  3. Observed: no user row / no streaming text while working; assistant text appears only after the turn fully commits to JSONL. Duplicate archived rows under the feed. Possible "transcript unavailable" banner.

Status

Two in-flight branches address parts of this (not merged):

These should be reconciled into a single fix.

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

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions