Repository navigation
Description
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:
turn_startedsrc=rolloutopens turn019e31cf-ea65-7350-8248-a02503cd3418(the placeholder turn for the just-submitted prompt). It becomescurrentTurn.- The proxy then streams the real Responses turn:
block_startedsrc=proxyturn=resp_058170eaf07b062b...block 0 and 1,stream_phaseresponding → idle (~13s of real streaming). - Every proxy event has
turnId = resp_0581..., which mismatches the livecurrentTurn(019e31cf-ea65...).foldSemanticEventblock_starteddoesif (eventTargetsDifferentTurn(ev, currentTurn)) breakand drops all of it (src/renderer/src/workspace/semantic/foldEvent.ts:274). Same guard at the othercasebranches. - Result: the rendered
semanticTurnIdstays019e31cf-ea65...the whole response. That rollout turn is archived withtext.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 thecurrentTurnwhen 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
turnIdwhen 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()emittingrollout-errorfor a successful switch; bump the codex-headless submodule. See codex-headless#11.
Repro
- Resume a Codex session that forks its rollout on resume.
- Send a prompt; watch the feed during the response.
- 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):
revert/feed-semantic-history-165— reverts fix(feed): keep semantic history visible while JSONL catches up #165 (clears Finding 2's duplicate-key crash, but does not fix Finding 1).fix/codex-semantic-proxy-precedence— de-dupes history byturnIdand lets a proxy turn replace an empty non-proxy placeholder (addresses Findings 1 and 2).
These should be reconciled into a single fix.
Activity