Skip to content

fix(ghosts): supersede Codex ghosts by provider item id - #1383

Merged
Juliusolsson05 merged 4 commits into
mainfrom
fix/codex-ghost-response-identity
Sep 27, 2026
Merged

Juliusolsson05 merged 4 commits into
mainfrom
fix/codex-ghost-response-identity

Conversation

@Juliusolsson05

@Juliusolsson05 Juliusolsson05 commented Sep 27, 2026 •

Copy link
Copy Markdown
Owner

Fixes #1231.

Plan: docs/plans/2026-09-27-codex-ghost-item-identity.md (first commit).

Problem

Codex text ghosts were never superseded.

  • Ghost side. A Codex ghost is keyed by the proxy response id (resp_…).
  • Committed side. The committed rollout entry carries only codexTurnId, the rollout's turn_context UUID. reconcileUpstream compared the two, which can never be equal.
  • No fallback. Text blocks have no tool id to pair on.

So every Codex text ghost orphaned after 30 s. It stayed hidden only by render rules 3 and 4 in selectMergedEntries, as a latent identity weakness (P3 after #1235).

Root cause and evidence

The only id the live stream and the rollout share is the provider item id (msg_…, rs_…, fc_…, ctc_…):

  • the codex proxy adapter already carries it on each live block (SemanticLiveBlock.itemId);
  • the rollout has it on every response_item.

On six real Agent Code codex-cli 0.157.1 sessions (proxy dumps against their rollouts, 2026-09-27), all 45 msg_… ids seen on the proxy stream were present verbatim in their rollouts.

Change

  • Mapper. mapCodexRolloutToFeedEntries stamps codexItemId (the item's payload.id) on every entry it maps from a response_item (stampCodexItemId).
  • Ghost. ghostContextForBlock records the block's itemId in the ghost's context, atp's free-form slot. No package change.
  • Match. reconcileUpstream supersedes a ghost when context.itemId === entry.codexItemId.
  • Dead branch removed. The codexTurnId === ghost.turnId branch compared a rollout UUID with a response id and could never be true. The stale comments claiming the mapper stamped the response id are corrected in ghosts.ts, rollout.ts and Feed.tsx.
  • Kept. codexTurnId stamping stays: the feed's committed-text keys (renderUnits.ts) use it.

Tests

ghosts.codexItemIdentity.test.ts uses one real 0.157.1 assistant message seen from both sides (testing/fixtures/ghosts/codex/message-item-identity.json): the proxy SSE frames and the rollout record for the same msg_…, verbatim. The text is a one-line reviewer commentary.

The chain, all real code:

  1. The frames drive the real CodexResponsesAdapter (codex-headless adapter harness) and the real foldSemanticEvent.
  2. The ghost is minted by ghostsFromSemanticTurn.
  3. The rollout record goes through the real mapper and reconcileUpstream.

The ghost must be superseded by that entry. A message with a different id must not supersede it.

Fail-first. Red on main, where the ghost is not superseded. Each half of the fix is pinned by mutation: dropping the ghost's itemId, or the mapper stamp, turns it red.

Verification

  • npx tsc -b clean (testing/fixtures/ghosts/**/*.json added to tsconfig.web.json).
  • src/renderer/src/session-runtime, src/renderer/src/features/feed, src/providers/codex and src/renderer/src/workspace: 235 files, 1,721 tests.

Out of scope

  • The token_usage_record route ({turn_id, response_id} after each response). The item id is exact per block and needs no lookahead; parser#38 tracks the record type itself.
  • Tool ghosts (fc_…, ctc_…) get the same item-id match, and still also pair by tool_use_id / call_id.
  • Reasoning ghosts are unchanged (residual, review a). A reasoning response_item maps to no feed entry, so a reasoning ghost (rs_…) has nothing to be superseded by. It still orphans after 30 s and stays hidden by the same render rules as before.
  • Ordering corner (residual, review c). Reconcile runs once per appended entry. If a rollout line reaches the renderer before the proxy finishes the block that mints its ghost, that ghost is never matched. It was the behaviour for every Codex ghost on main and is now only this corner. The follow-up is to re-reconcile recent entries when minting.
  • Blocks that mint no ghost (web search, image generation, local shell, tool search) are unaffected.
  • Review a's P3: three stale WHY comments about the old turn-id match are corrected in 2609c3ac.

🤖 Generated with Claude Code

Juliusolsson05 and others added 2 commits September 27, 2026 01:03
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A Codex ghost is keyed by the proxy response id (resp_...), while the
committed rollout entry only carried the rollout turn UUID (codexTurnId).
reconcileUpstream compared the two, which can never match. Text blocks
have no tool id to fall back on, so every Codex text ghost orphaned
after 30 s and stayed hidden only by render rules 3 and 4.

Both sides do carry the provider item id (msg_/rs_/fc_/ctc_): the live
block has it from the proxy adapter, and the rollout has it on every
response_item. On six real 0.157.1 sessions, all 45 msg_ ids seen on the
proxy were present verbatim in their rollouts. The mapper now stamps
codexItemId, the ghost records its block's itemId in its context, and
reconcileUpstream matches on it. The dead turn-id branch and the stale
comments claiming the mapper stamped the response id are gone.

Fail-first: a real recorded message (proxy SSE frames plus the rollout
record) driven through the real adapter, fold, mapper and reconciler.
Red on main; each half of the fix is pinned by mutation.

Fixes #1231

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Juliusolsson05 Juliusolsson05 added provider:codex Affects Codex integration type:bug Something works wrong class:C8-rendering Rendering order/duplicates sev:P3 Minor labels Sep 27, 2026
Juliusolsson05 and others added 2 commits September 27, 2026 01:49
… match

Review a of #1383: rollout.ts, ghosts.ts and useIpcSubscriptions.ts
still said Codex ghosts reconcile by (turnId, blockIndex) or by turn id.
They reconcile by provider item id now, and codexTurnId's remaining
consumer is renderUnits.ts.

Refs #1231

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ot matched

Review c of #1383: the header listed rs_ ids as matched, but reasoning
items map to no feed entry, so reasoning ghosts still orphan as before.
The identity test now maps through createCodexTranscriptEntryMapper, the
wrapper all three production ingest surfaces use.

Refs #1231

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Juliusolsson05

Copy link
Copy Markdown
Owner Author

Disposition (W1). Three reviews: codex a at f8279536, codex b at f8279536, pi c at 2609c3ac. All three: MERGE-READY.

Finding Source Disposition
Three WHY comments still described the removed turn-id match a (P3) Fixed (2609c3ac).
Reasoning ghosts (rs_…) are not superseded: reasoning items map to no feed entry a (coverage), c (low) Residual, stated in the body. Unchanged from main. The ghosts.ts header no longer lists rs_… as matched (b26cc02e).
A rollout line that reaches the renderer before its ghost is minted leaves that ghost unmatched (reconcile runs per appended entry) c (low, pre-existing) Residual. It was the behaviour for every Codex ghost on main, and this PR shrinks it to that ordering corner. The follow-up hardening is to re-run reconcile over recent entries when minting a ghost.
The test called the bare mapper, not the stateful production wrapper c (nit) Fixed (b26cc02e). It maps through createCodexTranscriptEntryMapper.

The commits after the reviewed heads change comments and a test only. Exact-head CI is pending at b26cc02e.

@Juliusolsson05

Copy link
Copy Markdown
Owner Author

READY (W1). All three reviews MERGE-READY at b26cc02e, exact-head CI green, and the disposition is posted. merge-gate.sh --member: GATE PASS. The PR is mergeable and has no conflict with main, so there was no main merge, per the batch rule.

@Juliusolsson05
Juliusolsson05 merged commit b986aa0 into main Sep 27, 2026
2 checks passed
@Juliusolsson05
Juliusolsson05 deleted the fix/codex-ghost-response-identity branch September 27, 2026 11:49
Juliusolsson05 added a commit that referenced this pull request Sep 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

class:C8-rendering Rendering order/duplicates provider:codex Affects Codex integration sev:P3 Minor type:bug Something works wrong

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug(ghosts): Codex text ghosts are never superseded (response id vs rollout turn id)

1 participant