Skip to content

Fix feed render ownership across committed transcript, semantic live/history, ghosts, and optimistic input #172

Closed

Description

@Juliusolsson05

Problem

The feed renderer still does not have one coherent ownership model for user-visible chat content. We have repeatedly fixed one symptom, then reintroduced another, because the same assistant/user/tool output can be owned by multiple channels at different times:

  • committed transcript / JSONL entries (runtime.entries)
  • live semantic/proxy current turn (runtime.semantic.currentTurn)
  • archived semantic history (runtime.semantic.history)
  • ghost/provisional records
  • optimistic user rows / queued prompt state
  • screen fallback

The current bugs are not isolated UI polish bugs. They are failures of the invariant: a user-visible chat item must have exactly one stable visual owner at a time, and ownership transfer must be explicit when JSONL catches up or semantic moves to the next turn.

Known Symptoms

Recent concrete symptoms:

  1. Duplicate assistant response

    • Debug bundle: 2026-05-16T17-51-43-433-f2395303.
    • Raw HTML had a committed assistant row plus a live semantic assistant row with the same paragraph.
    • Committed row: data-entry-uuid="2026-05-16T17:51:37.605Z:message".
    • Live row: semantic resp_009dff... below committed tool results, no data-entry-uuid.
    • Cause class: committed transcript id and live proxy resp_* id did not match, so turn-scoped suppression missed that both rows were the same visible assistant text.
    • Narrow mitigation merged in fix(feed): suppress committed semantic assistant duplicates #170: suppress finalized live semantic assistant text when exact same assistant text is already committed.
  2. Live assistant commentary appears to overwrite itself / prior line disappears

    • Debug bundle: 2026-05-16T18-05-46-189-f4b37c29.
    • User-visible behavior: while the agent was working, only one live line appeared to exist; each new assistant progress/commentary line appeared to replace the previous one until committed JSONL later caught up.
    • In the bundle, semantic/proxy had live text for the merge message, and JSONL eventually had committed rows, but the live surface before commit behaved like a mutable single-row owner.
    • Cause class: SemanticStreamingTurn renders a mutable currentTurn; if multiple assistant messages/tool phases happen before durable rows arrive, the UI can look like replacement rather than append/stable provisional rows.
  3. Mid-turn/follow-up prompt suspicion / streaming becomes hard to reason about

    • Debug bundle: 2026-05-16T18-08-15-424-f4b37c29.
    • Write tests for @filename appears in the terminal/screen snapshot but not as an optimistic_user_add or committed user row in feed-debug.jsonl; likely typed while the prior turn was still active / not submitted, but current instrumentation makes this hard to prove.
    • Same bundle shows proxy flow complexity: flow_selected proxy-7, then flow_ignored proxy-8/9/10 — concurrent with active flow proxy-7, then flow_selected proxy-11 later. This class can break streaming if the wrong flow owns the semantic channel or if the renderer has no stable handoff for overlapping/follow-up flows.
  4. Historical feed clearing / missing user rows during MCP/tool-call turns

  5. Resumed Codex pane dropped live streaming and duplicated semantic history keys

Relevant Issue / PR History

#159 — feed clearing and missing user rows during MCP/tool-call turns

Important findings from comments:

  • MCP itself was not the first failure. MCP/tool calls exposed the bug by multiplying short Responses turns and tool handoffs.
  • optimistic_user_add proved input reception was alive. If the agent reacts while the user row is not visible, the row is lost/hidden after runtime insertion or during render ownership handoff.
  • Root cause at that time: Codex committed transcript tailer pinned to a dead rollout file after resume/fork. Proxy semantic stream was alive, committed JSONL channel was dead.
  • Ghost system did not save the feed because shipped ghosts are not the only live render path. SemanticStreamingTurn still owns current live turns, ghost fallback has a 30s orphan TTL, and short assistant text ghosts can be filtered as sidecars.
  • Comment correction from reading docs/design/ghost-system.md: ghost was designed for stuck JSONL, but Phase 3 (ghost as only live render path, deleting SemanticStreamingTurn) never shipped. The current hybrid model is the problem.
  • Fix direction recorded there: layer 1 must keep committed rollout tail authoritative and alive; layer 3 should render completed semantic turns from full history while JSONL catches up.

#160 — rollout tail ownership fix

#165 — semantic history bridge

  • Added bounded full semantic history rendered by Feed while JSONL catches up.
  • This was a necessary mitigation for completed turns leaving currentTurn before committed transcript rows arrived.
  • It also increased the number of render owners: committed entries + current semantic turn + archived semantic history + ghosts.
  • Codex duplicate suppression had to be per block/item, not whole-turn, because Codex commits one response_item at a time and several committed rows can share codexTurnId.

#168 / #167 / #169 — resume feed broken after semantic-history mitigation

#170 — committed/live duplicate assistant text mitigation

  • Fixed the latest duplicate row where committed JSONL had one id and live semantic resp_* had another id but the visible assistant text was identical.
  • This is still a narrow suppression guard, not a complete ownership model.

#138 / #134 — streaming Write raw JSON

  • Separate but relevant precedent: live semantic tool rows and committed transcript rows can have different renderers and a visual jump at commit boundary. feat(feed): live preview for streaming Write tool calls #134 made live Write preview closer to committed WriteRow behavior.
  • The same principle applies more broadly: live/provisional rows should converge structurally with committed rows and transfer ownership cleanly.

Current Mental Model

We need to stop treating these as isolated conditions. The renderer should model ownership explicitly.

For every user-visible item, track something like:

  • stable logical item identity
    • assistant text block: semantic turnId + blockIndex/itemId, committed message.id/codexTurnId/text hash as fallback
    • tool call: toolUseId / callId
    • user prompt: optimistic uuid -> committed uuid/text reconciliation
  • current owner:
    • committed
    • semantic-current
    • semantic-history
    • ghost
    • optimistic-user
  • lifecycle state:
    • streaming
    • finalized live but not committed
    • committed
    • superseded by committed
    • orphan/fallback
  • render action:
    • paint
    • suppress because committed owns it
    • keep provisional row until committed arrives
    • replace provisional row with committed row without visual duplicate

Today this ownership is scattered across:

  • src/renderer/src/workspace/semantic/foldEvent.ts
  • src/renderer/src/workspace/semantic/helpers.ts
  • src/renderer/src/workspace/ghosts.ts
  • src/renderer/src/workspace/mergedEntries.ts
  • src/renderer/src/features/feed/ui/Feed.tsx
  • src/renderer/src/features/feed/ui/semantic/StreamingTurn.tsx
  • src/renderer/src/features/feed/ui/semantic/renderUnits.ts
  • Codex rollout/entry stamping paths

That makes regressions likely: a fix in one layer changes timing or ids in another layer and the renderer falls back to duplicate/suppression/replacement behavior.

Proposed Direction

This should be a rendering architecture fix, not another one-off guard.

  1. Define a feed ownership ledger / selector

    • Build a single derived structure from committed entries, semantic current turn, semantic history, ghosts, and optimistic rows.
    • The ledger decides ownership before React rendering.
    • Feed should render the ledger, not independently render committed entries plus semantic history plus current turn with ad hoc suppression.
  2. Stable provisional rows

    • Finalized semantic message/tool blocks should remain as stable provisional rows until committed entries supersede them.
    • New assistant commentary should append as a new provisional row/block, not overwrite prior finalized live content.
  3. Explicit committed supersedure

    • When JSONL commits a row, it should claim ownership of the matching semantic/ghost/optimistic item.
    • Matching should prefer ids (message.id, codexTurnId, toolUseId, callId, semantic itemId) and only use exact text as a fallback for known Codex resp_* vs rollout id splits.
  4. Do not whole-turn suppress Codex by codexTurnId

    • Codex commits one response item at a time.
    • A turn can have committed tool/text blocks and still have live uncommitted blocks.
    • Suppression must remain per item/block.
  5. Clarify ghost vs semantic responsibilities

    • Either complete the original ghost Phase 3 design (ghost/provisional rows are the live render path and SemanticStreamingTurn is removed), or explicitly document and implement the hybrid model.
    • The current half-ghost/half-semantic model is what creates no-owner and double-owner windows.
  6. Handle mid-turn/follow-up prompts explicitly

    • If a user submits while a turn is active, the feed should show either a queued optimistic user row or a clear queued state.
    • If the input is only typed but not submitted, debug/trace should show that distinctly.
    • Proxy flow selection/ignored-flow events must not silently starve the render owner for the submitted turn.

Acceptance Criteria

  • No duplicate assistant text when committed transcript catches up to live semantic text.
  • No blank/missing user prompt row after optimistic_user_add or committed user entry exists.
  • Live assistant messages/tool phases append or remain stable; they do not visually replace previous finalized live commentary while JSONL catches up.
  • Completed semantic turns stay visible immediately until committed rows supersede them; no 30s ghost-only gap for normal chat content.
  • Codex resumed sessions stream live text even when rollout opens an empty placeholder first.
  • Semantic history never renders duplicate React keys and does not duplicate committed transcript rows.
  • Mid-turn submitted prompts have an explicit visible optimistic/queued row or a clear queued indicator.
  • Debug bundles plus Add rendering trace debug for transcript and semantic channel ownership #171 rendering trace can show exactly which owner rendered or suppressed each visible item.

Related

Activity

  1. Juliusolsson05 commented on May 16, 2026

    @Juliusolsson05
    OwnerAuthor

    Codebase comment extraction: ghost/render ownership history

    This comment is sourced from inline code comments and repo-local docs/plans, not just GitHub issue memory. The codebase already contains a lot of the reasoning we need; the rendering bug should be treated as violating these documented invariants.

    1. The canonical ghost doc says the current architecture has two live owners

    Source: docs/design/ghost-system.md

    Key reasoning from the doc:

    • Agent Code watches two streams for one conversation:
      • committed transcript / rollout JSONL: durable, authoritative, structurally delayed
      • proxy semantic stream: real-time, non-durable
    • The naive alternatives all fail:
      • JSONL only: visible delay or blank feed while model is speaking
      • proxy only: no durable resume/crash record
      • both unconditionally: duplicate assistant rows during the lag window
    • Ghost was intended to bridge that gap: render a provisional row, then supersede it when committed JSONL arrives.
    • But the doc explicitly says the original Phase 3 never shipped. Today the live current turn is still rendered by SemanticStreamingTurn from runtime.semantic.currentTurn, not by ghosts.
    • Ghosts are now parallel bookkeeping plus fallback rendering for JSONL-stalled-past-proxy cases.

    Important invariant from the doc:

    The live current turn is rendered by SemanticStreamingTurn, not from ghosts. Ghosts exist in parallel. The render predicate excludes ghosts whose turnId === currentTurn.turnId because SemanticStreamingTurn owns that live turn.

    Implication for this issue:

    The architecture is knowingly hybrid. That is why regressions keep appearing as either duplicate rows, missing rows, or mutable/replacing live rows. The codebase does not have a single feed ownership selector; it has committed rows, semantic current turn, semantic history, ghost fallback, and optimistic rows coordinated by several local predicates.

    2. mergedEntries.ts documents the five-rule ghost render predicate

    Source: src/renderer/src/workspace/mergedEntries.ts

    Inline comments define ghost rendering as a narrow fallback, not normal live rendering. A ghost is render-eligible only if all of these hold:

    1. not superseded
    2. orphaned after TTL
    3. not the current live turn, because SemanticStreamingTurn owns that
    4. newer than lastJsonlEntryAt, meaning proxy state is past the JSONL tail
    5. not sidecar-shaped

    The comments explicitly explain why rules 4 and 5 both exist:

    • Timestamp alone catches stale old ghosts below newer JSONL, but cannot distinguish a legitimate JSONL-stalled tail from a final sidecar leak after the last real commit.
    • Sidecar shape catches title-gen / predict-next-prompt / branch-name style auxiliary responses that Claude/Codex may never commit to JSONL.

    Important invariant:

    selectMergedEntries must return runtime.entries by identity when no ghost survives. This is load-bearing for Feed memoization. Returning a fresh array on no-op busts memoized rows and makes markdown reparse/re-render churn.

    Implication for this issue:

    If we add a feed ownership ledger, it must preserve reference stability. A correct ownership model that allocates new arrays/maps on every semantic tick will reintroduce performance/render churn bugs.

    3. ghosts.ts says ghosts are bookkeeping today, not the primary live UI path

    Source: src/renderer/src/workspace/ghosts.ts

    The file-level comments state:

    • Ghost is a disk-backed ledger of semantic events.
    • ghostsFromSemanticTurn mints provisional entries from semantic blocks.
    • reconcileUpstream supersedes them when JSONL lands.
    • orphanStale marks them after TTL.
    • Most ticks produce no rendered ghost output because the live current turn renders through SemanticStreamingTurn.

    The comments also warn that reducers must return prev unchanged on no-op. This is not described as an optimization; it is described as load-bearing because Feed row memoization depends on object identity.

    Important invariant:

    A semantic tick should not create render work unless it actually changes the ghost map or visible ownership. The previous always-clone behavior forced setRuntimes cascades and busted useMemo([entries]) in Feed.

    Implication for this issue:

    A new ownership selector must be pure and identity-stable. It should be possible to trace why a visible row changed: new committed row, semantic block mutation, ownership transfer, orphan fallback, or optimistic reconciliation. No invisible churn.

    4. ATP ghost docs define ghost as a general two-producer durability-gap primitive

    Sources:

    • packages/agent-transcript-parser/docs/ghost.md
    • packages/agent-transcript-parser/src/ghost.ts

    Key reasoning from the library docs/comments:

    • Ghost is a valid transcript record tagged as non-authoritative via _atp.origin = 'ghost'.
    • Ghosts are append-only JSONL with deterministic uuids: g-<turnId>-<blockIndex>.
    • Deterministic uuids are required because last-write-wins reduction only works if repeated snapshots for the same block share a key.
    • Converters skip ghosts because they are runtime artifacts, not durable transcript history.
    • Reconciliation is consumer-driven. ATP does not know app-specific equality rules.
    • mergeWithUpstream is only a reference merger; UI ownership semantics belong in Agent Code.

    Important invariant:

    Ghost is the right primitive for “record exists before the authoritative producer catches up,” but Agent Code must decide matching, supersedure, and visual ownership. The library deliberately does not solve our feed-rendering policy.

    Implication for this issue:

    Do not push app-specific duplicate suppression into ATP. The bug belongs in Agent Code’s selector/renderer ownership layer.

    5. The 2026-05-07 ghost findings doc records why previous fixes regressed

    Source: docs/superpowers/plans/2026-05-07-ghost-system-findings.md

    This is the most important historical write-up. It says:

    • Ghost was designed to become the single live render path.
    • That Phase 3 never landed.
    • SemanticStreamingTurn still owns live current-turn rendering.
    • Ghost became a parallel ledger that only renders in rare fallback states.
    • Several prior attempts regressed because they treated TTL orphaning as enough information.

    Recorded failed attempts:

    • 10e4fc5: hide non-current ghosts. Fixed stale-bottom-row ordering but killed legitimate orphan fallback.
    • 686b94e: render orphaned ghosts after TTL. Reintroduced sidecar leaks because title-gen / predict-next-prompt also orphan.
    • 2a83978: suppress short sidecar-shaped orphan ghosts. Helped one bundle, but longer sidecar variants can defeat it; genuine short assistant replies can also be hidden.
    • fix/hide-orphan-ghost-tail local v1: hide all ghosts again. Same failure as 10e4fc5: loses the real JSONL-stalled-mid-turn recovery case.

    The doc’s corrected signal:

    A ghost should render only when semantic/proxy state is newer than the newest JSONL timestamp the session has observed. That distinguishes “JSONL stalled past proxy” from “sidecar leak JSONL intentionally never wrote.”

    Implication for this issue:

    Any new fix that says “just hide ghosts,” “just render orphans,” or “just use TTL” is repeating a documented failed design. The issue should require preserving both sides: no sidecar clutter and no loss of real stalled-turn recovery.

    6. The 2026-05-07 implementation plan explains why the predicate is layered

    Source: docs/superpowers/plans/2026-05-07-ghost-rendering-predicate.md

    The plan explicitly says timestamp-only is insufficient:

    Example from the plan:

    • real assistant JSONL committed at t=100
    • predict-next-prompt sidecar streams at t=105
    • no further JSONL writes because user walks away
    • ghost.updatedAt=105 > lastJsonlEntryAt=100
    • timestamp-only would render the sidecar at the tail

    So the current code keeps both:

    • timestamp gate: ghost newer than JSONL tail
    • sidecar-shape gate: suppress known auxiliary-call shapes

    Implication for this issue:

    The future ownership ledger must keep these distinctions explicit. “Newest thing wins” is not enough because sidecars can be newest and still not be user-visible conversation.

    7. The 2026-04-20 rendering-fixes plan documents the earlier render failure cluster

    Source: docs/superpowers/plans/2026-04-20-rendering-fixes.md

    The plan clustered the failures around two boundaries:

    1. the ghost bridge between live semantic state and JSONL transcript
    2. the gating between SemanticStreamingTurn and the ghost-merged feed

    It recorded concrete defects:

    • bootstrap ghost flash
    • live-turn null flip
    • duplicate Codex rendering
    • historical tool_result flood at resume
    • render-path reference churn busting Feed memos

    The plan also says the fixes were surgical and did not delete the legacy live-view path. Phase 3 deletion of SemanticStreamingTurn was still deferred.

    Implication for this issue:

    The current bug is not surprising: the old plan fixed symptoms while preserving the hybrid model. That was pragmatic then, but the repeated regressions now point at needing the architecture that the docs already describe: one resolved visible-feed owner per item.

    8. The 2026-04-18 live-turn redesign says the correct upstream model is one live owner

    Source: docs/superpowers/plans/2026-04-18-headless-live-turn-redesign.md

    Core rule from the plan:

    Only one source may publish semantic turn mutations for a given turn lifecycle.

    The plan names the source split:

    • semantic: what the model is saying now
    • screen: what terminal UI is painting now
    • committed: what transcript/rollout has durably written

    And it says the bug class comes from letting proxy, screen, rollout, and committed sources co-own the same live assistant turn.

    Design rules from the plan:

    • one live owner per turn
    • ownership assignment must be explicit
    • secondary sources are reconciliation-only
    • screen is overlay/status, not assistant truth
    • SemanticChannel should be strict transport, not a healer that auto-repairs mismatched lifecycle

    Implication for this issue:

    The renderer should not be forced to merge already-merged truth. The headless/channel layer should expose one live semantic timeline, and the renderer should then perform one explicit handoff from live/provisional owner to committed owner.

    9. foldEvent.ts documents reducer ownership and Codex mismatch traps

    Source: src/renderer/src/workspace/semantic/foldEvent.ts

    Important inline reasoning:

    • There must be one semantic reducer per session. UI surfaces should select from runtime.semantic, not subscribe independently.
    • Explicit wrong-turn events are rejected to avoid mixing old proxy blocks into the active turn.
    • Codex has a strict live mismatch rule to stop concurrent proxy/rollout/screen flows from thrashing the single live-turn slot.
    • Codex also has narrow exceptions:
      • an already-ended pending-tool turn can be replaced by the follow-up turn
      • a proxy turn can replace an empty non-proxy rollout/screen shell, because rollout may open a placeholder before proxy emits the real resp_* turn

    Implication for this issue:

    The exact resp_* vs 019e... split from the latest duplicate bug is a known family of mismatches. Fixes must distinguish empty placeholder replacement from live-turn thrash. A renderer-only fix cannot safely ignore reducer ownership state.

    10. semantic/helpers.ts documents why semantic history exists

    Source: src/renderer/src/workspace/semantic/helpers.ts

    Important inline reasoning:

    semanticHistoryRow keeps the full block map, not a tiny summary, because the semantic stream knows immediately that a Codex/MCP round completed while durable JSONL may lag by one or more Responses turns. If only { turnId, text } is archived, the renderer has nothing to paint during that gap and the visible conversation appears to clear after each MCP call.

    appendSemanticHistory replaces by turnId instead of blindly appending because repeated fallback ids can create duplicate React keys and repeated rows.

    Implication for this issue:

    Semantic history is already a second fallback bridge besides ghosts. The future ownership ledger must include it explicitly, otherwise we keep patching duplicates between committed entries, current semantic turn, and semantic history.

    11. StreamingTurn.tsx and renderUnits.ts contain the current local duplicate guards

    Sources:

    • src/renderer/src/features/feed/ui/semantic/StreamingTurn.tsx
    • src/renderer/src/features/feed/ui/semantic/renderUnits.ts
    • scripts/test-semantic-committed-text.ts

    Important inline reasoning:

    • Semantic turns render block-by-block because the proxy stream already has structure. Dumping turn.text as markdown would recreate screen-parsing brittleness.
    • Committed tool ids suppress live tool blocks because the committed transcript already owns those rows.
    • Committed assistant text suppression has two layers:
      • turn-scoped key when ids agree
      • exact text fallback when Codex exposes proxy resp_* live ids but committed rollout/task 019e... ids
    • The exact-text fallback is intentionally committed-only, exact-string-only, and limited to finalized/completed live text so active streaming text is not hidden by prefix overlap.

    Implication for this issue:

    PR #170 is a narrow, justified guard for one proven duplicate class. It should not be mistaken for the final architecture. The broader issue remains that suppression is scattered inside row rendering instead of decided by one feed ownership selector.

    12. Feed.tsx comments warn against reintroducing screen/content double rendering

    Source: src/renderer/src/features/feed/ui/Feed.tsx

    Important inline reasoning:

    • The old raw TUI streaming row was removed because it was a second producer for the same live assistant slot.
    • Screen fallback must arrive as semantic events with baseline gating, not as a separate raw renderer.
    • Feed also documents why whole-turn Codex suppression is unsafe: Codex commits response items incrementally, so a turn may have committed items and still have uncommitted live items.

    Implication for this issue:

    The solution cannot be “hide the whole live Codex turn once anything from that turn commits.” Suppression/ownership must be per item/block.

    13. Optimistic user row comments are part of the same ownership bug class

    Sources:

    • src/renderer/src/workspace/hook/actions/streaming.ts
    • src/renderer/src/workspace/hook/ipc/useIpcSubscriptions.ts

    Key reasoning from comments:

    • Optimistic Codex user rows exist because rollout JSON arrives later than the TUI submit. The row makes submit visible immediately.
    • Reconciliation cannot depend only on tail position because Codex can commit tool-result user rows before the real prompt. The code searches both current entries and appended burst entries for optimistic rows with matching text.

    Implication for this issue:

    Missing or duplicated user prompts are the user-side equivalent of assistant duplicate/suppression bugs. The ownership ledger must include optimistic user rows and their committed supersedure, not just assistant/ghost rows.

    Issue impact

    The codebase’s own comments point to the same root cause:

    There is no single explicit visual ownership model for feed items. Instead, ownership is distributed across:

    • committed JSONL entries
    • semantic current turn
    • semantic history
    • ghost fallback
    • optimistic user rows
    • screen/overlay fallback
    • local duplicate guards in semantic render units

    Each layer has defensible comments in isolation. The repeated regressions happen when timing changes make two layers believe they own the same visible item, or when no layer keeps a finalized provisional item visible until committed JSONL catches up.

    The fix should be evaluated against the code-comment invariants above, not against one symptom bundle only.

  2. Juliusolsson05 commented on May 16, 2026

    @Juliusolsson05
    OwnerAuthor

    Additional code-comment extraction: JSONL, Codex rollout, channel ownership, and debug tooling

    This comment adds the second pass from inline comments/tests/docs. It focuses on the parts not covered enough in the ghost appendix: JSONL ingestion, Codex id stamping, headless channel ownership, optimistic user reconciliation, and debug bundle tooling.

    1. JSONL replay must be bulk-owned, not event-spammed

    Source: src/main/sessions/jsonlCoalescer.ts

    Inline reasoning says resumed Claude/Codex bootstrap tails must be delivered as one bulk session:jsonl-entries burst, not hundreds of singular IPC events.

    Implication for this issue:

    If bootstrap replay is treated like live incremental render input, the feed can scroll/pin/lazy-mount through history and produce render cascades that look like the feed is replaying or replacing content. The ownership ledger should distinguish bootstrap transcript hydration from live committed ownership changes.

    2. Only the bulk JSONL handler should own committed-side effects

    Source: src/renderer/src/workspace/hook/ipc/useIpcSubscriptions.ts

    Important inline reasoning:

    • Codex rolling turn id is ingestion bookkeeping outside React runtime state.
    • Terminal/exit clear it so a new task cannot inherit an old turn id.
    • The bulk JSONL handler owns JSONL side effects now.
    • Optimistic user reconciliation removes Codex prompts by identity/text, not tail position.

    Implication for this issue:

    Committed ownership should enter the feed through one path. If committed entries can be applied through multiple handlers, or if Codex rolling turn ids leak between tasks, duplicate suppression and semantic/ghost supersedure will be unreliable.

    3. Codex rollout mapping has intentionally synthesized stable ids

    Sources:

    • src/renderer/src/workspace/codex/eventCursor.ts
    • src/renderer/src/workspace/codex/rollout.ts

    Key inline reasoning:

    • turn_context is the clean source of truth, but payload-level turn_id is load-bearing when chunks start after turn_context.
    • Codex rollout lines are mapped into Claude-shaped Entrys with stable synthesized uuids from timestamp plus payload id / call_id / type.
    • Rollout does not put turn_id on every item, so Agent Code stamps codexTurnId locally for ghost reconciliation.
    • Bootstrap shim messages like AGENTS.md and <environment_context> are filtered before runtime.entries so they do not render as user chat.

    Implication for this issue:

    The renderer cannot assume every committed Codex row has the same ids as the live proxy stream. codexTurnId and stable synthesized uuids are part of the ownership proof. Missing or drifting stamp logic causes exactly the class fixed narrowly in #170: live proxy resp_* and committed rollout 019e... rows both appear to be different owners for the same visible assistant text.

    4. Codex headless channel comments define three separate truths

    Sources:

    • packages/codex-headless/src/channels/types.ts
    • packages/codex-headless/src/channels/SemanticChannel.ts
    • packages/codex-headless/src/channels/CommittedChannel.ts
    • packages/codex-headless/src/channels/ScreenChannel.ts

    Key inline reasoning:

    • Semantic channel is “now” / live provisional model structure.
    • Screen channel is terminal paint and overlays, not model truth.
    • Committed channel is durable rollout truth.
    • proxy, rollout, and screen are mutually exclusive live owners.
    • SemanticChannel is strict transport: mismatched starts/deltas are dropped with diagnostics rather than auto-healed.

    Implication for this issue:

    Rendering bugs often happen when these channel roles are crossed:

    • screen chrome/content treated as assistant semantic truth
    • proxy and rollout both mutating the live turn
    • committed history leaking back into semantic current turn
    • semantic live content and committed rows both painting the same item

    The feed ownership model should preserve this three-channel separation and make handoff explicit.

    5. Codex proxy flow ownership is request-based and single-active

    Source: packages/codex-headless/src/proxy/CodexResponsesAdapter.ts

    Important inline reasoning:

    • Proxy flows are keyed by proxy requestId, not by path.
    • Only one active flow may publish visible semantic events.
    • response.id becomes semantic turnId.
    • call_id pairs tool blocks/results.

    Implication for this issue:

    The mid-turn/follow-up suspicion belongs partly here: if multiple proxy flows race, retries/warmups/follow-ups must not all mutate the visible current turn. The trace/debug command from #171 should expose active/ignored flow decisions next to render ownership decisions.

    6. Codex screen fallback is shadow, not renderer-facing truth

    Source: packages/codex-headless/src/CodexHeadless.ts

    Important inline reasoning:

    • Screen fallback publishes to semanticShadow, not the renderer-facing semantic channel.
    • Screen, rollout, and proxy transitions are explicit.
    • Codex resume can append to the original rollout or fork a new one.
    • A bounded watcher switches rollout tails only when same cwd plus lineage overlap prove ownership.

    Implication for this issue:

    A stale rollout tail creates live proxy content with no committed rows forever. Blindly switching by cwd can paint a sibling agent’s transcript into the pane. So committed-channel ownership is not “latest rollout file in cwd”; it requires lineage proof.

    This is directly relevant to #159 and to any renderer fix that assumes JSONL will always catch up quickly.

    7. Resume/session comments show why ordering matters

    Sources:

    • src/renderer/src/workspace/hook/actions/session.ts
    • src/renderer/src/workspace/hook/persistence/useAutoSave.ts
    • src/renderer/src/workspace/hook/persistence/useBootstrap.ts

    Key inline reasoning:

    • Ghost bootstrap is deferred one tick so it merges onto the fresh runtime.
    • After reading disk ghosts, the code reconciles against already-loaded JSONL entries and persists supersedes.
    • Requested resumeSessionId is persisted immediately, before transcript lines round-trip, so the pane can resume later even if no line arrives yet.
    • Autosave is disabled until bootstrap completes.
    • Partial restore and persisted fallback protect disk state from being overwritten by incomplete startup state.

    Implication for this issue:

    Rendering ownership is tied to persistence ordering. If autosave or resume metadata runs too early, stale/unowned sessions can become durable, old ghosts can resurrect, or panes can lose the provider transcript id needed to find committed rows later.

    8. Optimistic user rows are an explicit committed-handoff problem too

    Sources:

    • src/renderer/src/workspace/hook/actions/streaming.ts
    • src/renderer/src/workspace/hook/ipc/useIpcSubscriptions.ts

    Inline reasoning says optimistic Codex user rows exist because rollout JSON arrives later than TUI submit. Reconciliation must not depend on tail position because Codex can commit tool-result user rows before the real prompt.

    Implication for this issue:

    The ownership model must include user prompts:

    • optimistic user row owns visibility immediately after submit
    • committed user entry supersedes it when matching text/id appears
    • tool-result user entries must not accidentally supersede or duplicate the prompt
    • if a follow-up prompt is queued mid-turn, the UI needs an explicit queued/optimistic owner or an explicit “typed but not submitted” distinction in trace output

    9. Feed/debug tooling already encodes the desired forensic workflow

    Sources:

    • src/renderer/src/workspace/runtime/feedDebug.ts
    • src/main/storage/feedDebugLog.ts
    • src/renderer/src/features/debug/saveDebugBundle.ts
    • src/main/storage/debugBundle.ts
    • src/renderer/src/features/debug/renderTrace.ts
    • src/renderer/src/features/debug/ui/HtmlDebugPanel.tsx
    • docs/superpowers/specs/2026-04-21-html-debug-panel-design.md

    Key inline/doc reasoning:

    • Feed debug is per-session and persisted to disk for later debug bundles.
    • Feed debug disk writes are serialized per session and idempotent by monotonic entry id.
    • Renderer assembles debug bundles because it owns state, feed-debug, semantic runtime, DOM HTML, and sanitizer output.
    • Main only writes files.
    • Debug bundles include state snapshot, feed-debug JSONL, proxy semantic data, raw/clean HTML, render traces, and performance artifacts.
    • Render traces store sanitized HTML commit chains with periodic checkpoints.
    • HTML debug snapshots intentionally capture the focused pane, not the whole app, and are a rendering debug aid, not export.

    Implication for this issue:

    #171 should be built on top of existing bundle/trace concepts. #172 acceptance should require the trace to print owner decisions for each visible item across:

    • committed transcript rows
    • semantic current turn
    • semantic history
    • ghost fallback
    • optimistic user row
    • screen/HTML snapshot presence
    • suppress/render/replace decisions

    10. Rendering harness history already names these bug classes

    Sources:

    • testing/rendering/README.md
    • docs/superpowers/plans/2026-04-20-claude-dup-text-and-screen-churn.md
    • testing/regression-claude-tool-result.ts
    • scripts/test-ghost-fallback.ts
    • scripts/test-semantic-committed-text.ts

    Known regression classes recorded in local docs/tests:

    • REND-1: subscribe-after-spawn lost bootstrap JSONL burst.
    • REND-2: resume double-rendered last message because screen still showed text while JSONL rendered committed entry.
    • REND-3: stale previous assistant text after submit due to missing streaming baseline and semantic events not folding into SemanticLiveTurn.
    • Claude duplicate text: committed tool_result failed to release live semantic turn, so JSONL rows and lingering semantic row rendered together.
    • Screen churn: debug logs exploded from structurally identical screen updates.
    • Codex id split: committed rollout id and proxy resp_* id require exact committed-text suppression for finalized live assistant text.
    • Ghost predicate matrix: superseded/current/older-than-JSONL-tail/sidecar ghosts hidden; substantive/tool-use orphan newer than JSONL tail rendered.

    Implication for this issue:

    The new work should not create only a single reproducer for today’s duplicate. It should preserve these regression tests and ideally add a higher-level ownership-selector test that covers the known matrix:

    • committed catches live semantic text by same id
    • committed catches live semantic text by Codex exact text fallback
    • committed tool id suppresses live tool block
    • semantic history bridges JSONL lag without duplicate keys
    • current-turn ghosts never render
    • stale/sidecar ghosts never render
    • real orphan after JSONL stall renders
    • optimistic user prompt is visible until committed prompt supersedes it
    • bootstrap replay does not create live-render churn

    Bottom line from the second pass

    The repo’s comments consistently say this is an ownership system, not a styling bug. The actual render fix should be designed around explicit item ownership and handoff:

    • one committed ingestion path
    • one semantic reducer
    • one active proxy/rollout/screen live owner
    • stable ids and fallback identity where Codex lacks matching ids
    • committed supersedure per item/block, not per whole turn
    • ghost fallback only for JSONL-stalled-past-proxy cases
    • optimistic user rows reconciled independently of tail order
    • debug traces showing every owner decision
  3. Juliusolsson05 commented on May 16, 2026

    @Juliusolsson05
    OwnerAuthor

    Full rendering pipeline map from code comments

    This is the non-ghost-centered rendering map. The bug class spans the whole path from submit to DOM, so the fix should be evaluated across all of these stages.

    1. Submit/composer starts the visible row lifecycle

    Relevant areas:

    • src/renderer/src/workspace/tile-tree/TileLeaf/useComposerKeybinds.ts
    • src/renderer/src/workspace/hook/actions/streaming.ts
    • src/renderer/src/workspace/hook/ipc/useIpcSubscriptions.ts
    • docs/superpowers/plans/2026-04-21-claude-paste-submit-clean-fix.md

    Important code-comment invariants:

    • Submit must make the user action visible before durable Codex rollout JSON arrives. That is why optimistic Codex user rows exist.
    • Optimistic user reconciliation cannot depend on tail position. Codex can commit tool-result user rows before the real prompt, so the committed prompt may not be adjacent to the optimistic row.
    • Streaming baseline capture exists to prevent the previous assistant answer still visible in the terminal screen from being treated as the first live text of the next turn.
    • Paste/submit handling should be event/state driven, not timer driven. The paste plan says the right wait is “until Claude visibly absorbed the paste,” not “sleep N ms.”

    Implication for #172:

    The ownership model must include user prompts and queued/follow-up prompts, not only assistant text. A submitted prompt needs a stable optimistic/queued owner until committed JSONL supersedes it. A typed-but-not-submitted prompt needs to be distinguishable in debug traces from a submitted prompt that failed to render.

    2. Headless channels define the source roles

    Relevant areas:

    • packages/claude-code-headless/src/channels/types.ts
    • packages/codex-headless/src/channels/types.ts
    • packages/claude-code-headless/src/channels/SemanticChannel.ts
    • packages/codex-headless/src/channels/SemanticChannel.ts
    • docs/superpowers/plans/2026-04-18-headless-live-turn-redesign.md

    Important code-comment invariants:

    • There are three different truths:
      • semantic: what the provider/model is producing now
      • screen: terminal paint, overlays, approvals, trust dialogs, command UI
      • committed: durable transcript/rollout truth
    • SemanticChannel is strict transport, not a healer. Mismatched starts/deltas are dropped with diagnostics instead of silently sealing one turn and opening another.
    • Ownership policy belongs in ClaudeCodeHeadless / CodexHeadless, because only the orchestrator sees proxy, rollout, and screen state together.
    • Screen fallback should not be a content source for renderer-visible assistant prose in the normal path. In current headless packages, screen fallback routes to semanticShadow, while overlays/activity still publish on the screen channel.

    Implication for #172:

    The renderer should not merge already-merged truth. If proxy, rollout, screen, and committed sources race, the headless/orchestrator layer must expose one live semantic owner. The feed then only solves item-level handoff from live/provisional owner to committed owner.

    3. Proxy/live flow selection is part of rendering correctness

    Relevant areas:

    • packages/claude-code-headless/src/proxy/ClaudeProxyAdapter.ts
    • packages/codex-headless/src/proxy/CodexResponsesAdapter.ts
    • docs/superpowers/plans/2026-04-17-codex-semantic-flicker-fix.md
    • docs/superpowers/plans/2026-05-06-claude-title-turn-filter.md

    Important code-comment invariants:

    • Proxy flows are keyed by request/flow id.
    • First streaming chunk wins active ownership.
    • Concurrent flows become flow_ignored and must not mutate the visible semantic turn.
    • Sidecar/title/predict-next-prompt style auxiliary calls must be demoted before they open visible turns.
    • Flow decisions must be logged because a rendering symptom can be caused upstream by the wrong flow publishing.

    Implication for #172:

    A “rendering” fix that only touches React can still leave the root bug alive if wrong proxy flows are allowed to publish. The rendering trace from #171 must print flow_selected / flow_ignored decisions alongside row ownership decisions.

    4. Committed transcript ingestion is bulk and deduped by design

    Relevant areas:

    • src/main/sessions/jsonlCoalescer.ts
    • src/main/sessions/forwarder.ts
    • src/renderer/src/workspace/hook/ipc/useIpcSubscriptions.ts
    • src/renderer/src/workspace/hook/actions/initialHistory.ts
    • docs/superpowers/plans/2026-04-15-bootstrap-replay-perf.md

    Important code-comment invariants:

    • JSONL replay must be bulk-only. Reintroducing singular per-entry IPC for bootstrap revives the “scrolling through the whole conversation” cascade.
    • Bootstrap is a render phase, not just data loading. During bootstrap, Feed suspends per-append autoscroll and lazy mounting, then pins once after the quiet window.
    • Initial history and live replay must share dedupe/index contracts: seenUuidsRef, tool indices, lastJsonlEntryAt, history markers, and ghost/optimistic reconciliation must be initialized before live replay can duplicate rows.
    • Tool indices are maintained incrementally on runtime. Rebuilding from entries per render was an O(N²) bootstrap path.

    Implication for #172:

    Committed ownership cannot be modeled as “append one row, render one row” during resume. Bootstrap/live committed ingestion must be represented in the ownership model without causing per-entry ownership churn or scroll/lazy-mount cascades.

    5. Semantic reducer is the one renderer-side semantic truth

    Relevant areas:

    • src/renderer/src/workspace/semantic/foldEvent.ts
    • src/renderer/src/workspace/semantic/helpers.ts
    • docs/superpowers/plans/2026-04-17-claude-semantic-provider-gating.md
    • docs/superpowers/plans/2026-04-17-codex-semantic-flicker-fix.md

    Important code-comment invariants:

    • One session has one semantic reducer. Feed, Reader, debug, and any other UI should read runtime.semantic; they should not subscribe and fold independently.
    • Codex and Claude need different mismatch policies:
      • Codex must be strict while live, because proxy/rollout/screen races caused 0/1/0/1 block flicker.
      • Claude must be able to replace/archive in certain cases, because it can pin a turn across pending cross-turn tool results and then receive a new msg_* turn.
    • Codex has narrow exceptions: already-ended pending-tool turns can be replaced by valid follow-up turns; proxy can replace an empty non-proxy shell turn.
    • Semantic history keeps full block maps, not tiny summaries, to bridge the gap where semantic knows a turn finished but durable JSONL has not caught up.

    Implication for #172:

    The ownership selector must consume semantic current turn and semantic history explicitly. If it hides semantic history too aggressively, the feed clears during JSONL lag. If it keeps it too long, committed rows duplicate it.

    6. Feed render ordering and ownership today

    Relevant areas:

    • src/renderer/src/features/feed/ui/Feed.tsx
    • src/renderer/src/features/feed/ui/semantic/StreamingTurn.tsx
    • src/renderer/src/features/feed/ui/semantic/renderUnits.ts
    • src/renderer/src/features/feed/WorkIndicator.tsx
    • src/renderer/src/features/feed/ui/rows/*

    Current render order:

    1. committed/merged EntryRow rows
    2. semantic history rows
    3. semantic current turn row
    4. WorkIndicator
    5. end sentinel for scrolling

    Important code-comment invariants:

    • Live assistant text has one owner: SemanticStreamingTurn. The old raw TUI StreamingRow was removed because it double-rendered resume tails and stale previous-turn text.
    • Claude archived semantic turns can be suppressed at whole-turn level when committed message.id owns the turn.
    • Codex cannot use whole-turn suppression because rollout commits item-by-item. Codex suppression is per tool id / block / exact finalized text.
    • Empty Codex semantic turns must render null, not an empty assistant marker. There are legitimate gaps between response items after turn.text is cleared because committed rows own that text.
    • WorkIndicator is the only in-feed working affordance. It is phase-driven, not gated on semantic turn presence, to avoid blind spots during tool execution.
    • Lazy rows mount once and never unmount; bootstrap suspends observer attachment.
    • Streaming markdown and committed markdown have separate plugin sets to reduce visual jump when live text becomes committed text.

    Implication for #172:

    The desired fix is not a raw DOM patch. The feed needs a derived render model that says why each candidate row renders or does not render before React paints it.

    7. Debugging evidence is part of the contract

    Relevant areas:

    • testing/rendering/README.md
    • testing/rendering/renderer/RenderingHarnessApp.tsx
    • src/renderer/src/features/debug/ui/FeedDebugPanel.tsx
    • src/renderer/src/features/debug/saveDebugBundle.ts
    • src/renderer/src/features/debug/renderTrace.ts
    • src/renderer/src/features/debug/ui/HtmlDebugPanel.tsx
    • docs/superpowers/plans/2026-04-18-feed-debug-stream.md
    • docs/superpowers/specs/2026-04-21-html-debug-panel-design.md

    Important code-comment/doc invariants:

    • Full rendering debugging is layered: PTY/screen -> JSONL -> mapper -> semantic/proxy -> Feed render -> DOM HTML.
    • Feed-debug layers must preserve ordered evidence: STATE, JSONL, SEM, RENDER, GHOST.
    • RENDER is the critical layer for this issue because it records visible row decisions, not just raw inputs.
    • Debug bundles are coherent per-invocation snapshots. Renderer assembles them because it owns runtime state, semantic state, DOM, sanitizer output, and trace state. Main only writes files.
    • HTML debug is focused-pane outerHTML, not whole-app DOM and not a live observer.
    • Render trace stores sanitized HTML commit chains and screen tail samples with caps/deduping.
    • The rendering harness registry already records bug classes:
      • REND-1: subscribe-before-spawn for bootstrap JSONL
      • REND-2: resume double-rendered last message because screen and JSONL both painted it
      • REND-3: stale previous assistant text after submit because semantic folding/baseline ownership failed

    Implication for #172:

    The fix must preserve and extend the forensic pipeline. After the architecture change, a debug bundle should explain a visible row from source event to DOM row.

    Required ownership/debug output for the future fix

    For every user-visible item, the future selector/ledger should be able to emit a RENDER ownership decision with:

    • stable logical item id
    • item kind: user prompt, assistant text, tool use, tool result, reasoning/thinking, work indicator, system/compact marker
    • candidate owners: committed, semantic-current, semantic-history, ghost, optimistic-user, screen/overlay
    • selected owner
    • suppressed owners and exact reasons
    • handoff/supersedure target when committed JSONL catches up
    • row key used by React
    • whether the selected owner exists in the current DOM snapshot / html-clean.html

    Additional acceptance criteria for #172

    • A submitted user prompt has an explicit optimistic/queued owner until committed JSONL supersedes it.
    • A typed but not submitted follow-up prompt is distinguishable from a submitted prompt in trace/debug output.
    • JSONL bootstrap remains bulk and produces one render phase, not per-entry row churn.
    • Headless flow ownership decisions (flow_selected / flow_ignored) are visible in the rendering trace when proxy semantics are involved.
    • Semantic history bridges JSONL lag without duplicate React keys or committed-row duplicates.
    • WorkIndicator remains phase-driven and does not disappear merely because semantic rows mount/unmount.
    • Feed debug and debug bundles can correlate raw screen, JSONL, semantic events, render decisions, and final DOM HTML for every duplicate/missing/replaced row report.

    Regression matrix to keep attached to this issue

    Run/extend these when touching the relevant layer:

    • npm run test:ghost-fallback
    • npm run test:semantic-committed-text
    • Claude tool-result semantic regression: testing/regression-claude-tool-result.ts
    • rendering harness build/smoke: npm run testing:rendering:build
    • proxy semantic smoke when touching proxy ownership: scripts/proxy-harness-semantic.mts / scripts/proxy-harness-real.mts

    If a new #172 reproduction is captured, add the next REND-N entry to testing/rendering/README.md and put a thick WHY comment at the patch site.

  4. Juliusolsson05 commented on May 16, 2026

    @Juliusolsson05
    OwnerAuthor

    Submit, queued follow-up, and user-row ownership details

    Adding the input/submit pass because #172 must cover user rows and mid-turn follow-ups, not just assistant rendering.

    Submit path invariants

    Relevant files:

    • src/renderer/src/workspace/tile-tree/TileLeaf/useComposerKeybinds.ts
    • src/renderer/src/workspace/tile-tree/TileLeaf/claudePaste.ts
    • src/renderer/src/workspace/hook/actions/streaming.ts
    • src/renderer/src/workspace/hook/ipc/useIpcSubscriptions.ts

    Important comments/invariants:

    • Submit captures streamingBaseline from the freshest screen before sending input. This exists so any still-visible previous assistant text in the terminal is not treated as live text for the next turn.
    • setStreamingBaseline also marks awaitingAssistant, sets streamPhase: 'submitting', and timestamps before adapter events arrive.
    • Codex gets an immediate optimistic user row because rollout JSON can lag. That optimistic row is a transient UI placeholder and is not counted in totalEntries.
    • If send fails, the Codex optimistic row is removed and the draft remains visible.
    • Claude paste-like submit must not send paste bytes and Enter in the same PTY write. The primary path waits for Claude’s visible [Pasted text #N] placeholder; wall-clock delay is fallback only.
    • CLAUDE_PASTE_THRESHOLD = 100 is intentionally lower than Claude’s visible paste-collapse threshold because Claude’s paste accumulator can swallow Enter well below the visible placeholder threshold.

    Implication:

    If a submitted prompt disappears or does not show, the trace needs to answer which stage failed:

    • baseline captured?
    • optimistic user row added?
    • send failed and optimistic row removed?
    • committed user row arrived and superseded optimistic row?
    • committed row got filtered as meta/tool-result-only/system context?
    • queued rather than submitted?

    Optimistic Codex reconciliation invariants

    Relevant files:

    • src/renderer/src/workspace/hook/actions/streaming.ts
    • src/renderer/src/workspace/hook/ipc/useIpcSubscriptions.ts

    Important comments/invariants:

    • Reconciliation matches optimistic Codex rows by optimistic marker plus text, not by feed tail position.
    • Tail-position reconciliation was wrong because Codex can commit tool-result user rows before the real user prompt.
    • The code checks both the appended burst and pre-existing current.entries because the optimistic row may no longer be adjacent to the committed prompt by the time JSONL catches up.

    Implication:

    A future ownership ledger needs an explicit optimistic-user -> committed-user handoff, independent of row position.

    Queued follow-up prompts are intentionally not feed rows

    Relevant files:

    • src/renderer/src/workspace/hook/ipc/useIpcSubscriptions.ts
    • src/renderer/src/workspace/tile-tree/TileLeaf/QueueStrip.tsx
    • src/renderer/src/workspace/tile-tree/TileLeaf.tsx

    Important comments/invariants:

    • Queue-operation entries update runtime.queuedMessages; they are intentionally not pushed into runtime.entries.
    • QueueStrip renders queued messages between Feed and composer so they are visible but not mixed into the committed transcript feed.
    • This avoids “phantom future user rows” that would later duplicate when real transcript entries arrive.

    Implication:

    For the mid-turn/follow-up bug class, the trace must distinguish:

    • text typed in composer but not submitted
    • submitted prompt accepted as queued message
    • queued message rendered in QueueStrip
    • queued message later materialized as committed transcript entry
    • submitted prompt that should have rendered optimistically but did not

    Prompt history is downstream of committed/visible entry correctness

    Relevant files:

    • src/renderer/src/workspace/tile-tree/TileLeaf/usePromptHistory.ts
    • src/renderer/src/features/workspace/lib/latestUserPrompts.ts

    Important comments/invariants:

    • Prompt history derives from runtime.entries, not backend TUI history.
    • Claude history requires positive signal such as permissionMode and filters meta entries, slash-command errors, local-command caveats, and tool-result-only user-role rows.
    • Adjacent duplicates collapse, distant duplicates remain.

    Implication:

    Prompt history bugs can be a secondary symptom of user-row ownership bugs. If optimistic rows are not reconciled or committed user rows are filtered incorrectly, history will reflect that drift.

    Additional acceptance for #172

    • A submitted prompt must have exactly one visible owner: optimistic user, queued message, or committed user entry.
    • Queued follow-up prompts must be visible in QueueStrip and must not be inserted as phantom feed rows.
    • The rendering trace must label queued vs optimistic vs committed user ownership separately.
    • A committed tool-result user entry must not supersede or duplicate the real user prompt.
    • Paste-submit failures must be visible in debug as send failure / paste timeout / optimistic rollback, not as a silent missing prompt.
  5. Juliusolsson05 commented on May 20, 2026

    @Juliusolsson05
    OwnerAuthor

    Reopened: #172 was closed after a first-pass selector, not the full ownership fix

    We are still seeing the same bug family in saved debug bundles: user prompts can appear to be missing even when the data is not lost. The current evidence says this is still the feed ownership/order problem described here, not a brand-new prompt-submit failure.

    What we found now

    Latest manual bundle investigated:

    • ~/.config/agent-code/debug-bundles/manual/2026-05-20T15-09-04-906-7b859c43
    • note: STILL NOT SHOWING FUCKING USER PROMPT ; THE FUCK ?
    • STATE submit and optimistic_user_add fired for the real submitted prompt beginning please read A LOT...
    • RENDER visible_rows includes that optimistic user row
    • but the row is ordered before a stack of semantic-history rows and the work row:
    entry user: please read A LOT ...
    semantic history resp_033ca5d
    semantic history resp_0620432
    ...
    work responding
    

    So the row exists and is technically rendered, but it is not the visual tail/latest action. This matches the repeated user-facing symptom: "the prompt is not showing".

    The Improve documentation in @filename line in the same bundle is a separate Codex TUI placeholder/suggestion. It has no submit/paste-debug event and should not be treated as a submitted user prompt. The real issue is the submitted prompt being present-but-buried or routed to a separate queue surface.

    Why the previous closure was too early

    PR #184 closed this issue, but the docs added in that PR explicitly say it was only the first implementation pass:

    • docs/codex-rewrite-render/README.md says deriveFeedRenderModel is the first implementation pass.
    • The same doc lists remaining work: move more semantic ownership into the selector so Feed can eventually render a single typed FeedRenderItem[].
    • docs/codex-rewrite-render/feed-ui-rendering.md says current semantic content is appended after committed rows and that this is not a general model.

    Current code confirms that. deriveFeedRenderModel returns buckets:

    • visibleEntries
    • renderedSemanticHistory
    • renderedSemanticTurn
    • shouldShowWorkIndicator
    • debugRows

    Then Feed.tsx still renders separate JSX planes:

    {visible.map(...EntryRow...)}
    {renderedSemanticHistory.map(...SemanticStreamingTurn...)}
    {renderedSemanticTurn && <SemanticStreamingTurn ... />}
    <WorkIndicator ... />

    That means final DOM order is still structurally:

    entries first
    semantic history second
    semantic current third
    work last
    

    What #252 did and did not do

    #252 fixed #239 by changing Codex optimistic submit ownership: if rendered semantic history still owns the feed tail, the prompt is queued instead of appended as a normal optimistic entry. That is valuable, but it is a workaround around the existing plane order. It does not make Feed render one ordered ownership plan.

    I verified this in an isolated temp worktree at the merged #252 commit b0050cdec5d213cb82d05f153c51be13e1c95afb:

    • npm run test:feed-render-model passed
    • npm run test:codex-optimistic-submit passed
    • npm run test:semantic-committed-text passed
    • npm run test:ghost-fallback passed
    • npm run test:semantic-fold-codex-replace passed
    • npm run test:session-ownership passed
    • npm run test:codex-semantic-channel passed after submodules were initialized
    • npm run build passed after submodules were initialized

    I also added a temporary local diagnostic script in the isolated worktree only. It constructs exactly the dangerous shape: optimistic user entry + renderable semantic history + submitting work phase. The model reports:

    render model slots with optimistic entry + semantic history: entry -> semantic -> work
    - entry: user: new submitted prompt that should not be visually buried
    - semantic: semantic history resp_history · proxy
    - work: work submitting
    

    The diagnostic also verifies #252's submit gate returns true for queueing in this situation. So #252 prevents one path into the bad order; it does not remove the bad order from Feed.

    Updated diagnosis

    The bug loop is happening because we keep landing targeted mitigations for symptoms of the same incomplete architecture:

    But Feed still does not render from a single ordered FeedRenderItem[] ownership plan. It selects some owners centrally, then paints them in separate planes. That is the structural reason the same class keeps reappearing as missing rows, buried prompts, stale semantic tails, or confusing debug evidence.

    Required next fix

    This issue should remain open until Feed renders from one ordered plan, not separate buckets.

    Proposed acceptance criteria:

    1. deriveFeedRenderModel or a successor returns a single ordered visibleItems: FeedRenderItem[] list.
    2. Feed.tsx maps that list once; it does not separately map committed entries, semantic history, semantic current, and work.
    3. Each item carries owner metadata: committed, semantic-history, semantic-current, optimistic-submit, queue-strip, ghost, work, or empty.
    4. Render debug logs include final DOM order and owner/suppression reasons for every candidate.
    5. Regression tests assert final order, not only row existence. Specifically: a submitted user prompt must not be followed by older semantic-history/work rows in a way that makes it cease to be the latest user-authored action.
    6. Fix Codex prompt queue rendering ownership #252's queue behavior can remain as a tactical guard, but the final renderer must not rely on queueing to compensate for non-chronological planes.

    This is not a new isolated issue. It is #172 still being unfinished.

  6. Juliusolsson05 commented on May 20, 2026

    @Juliusolsson05
    OwnerAuthor

    Additional diagnostics from isolated simulation and saved-bundle replay:

    • #252 fixes one submit-time path by keeping a mid-turn Codex prompt in QueueStrip when renderable semantic history/current already exists. That makes the prompt visible at the pane bottom immediately after submit.
    • The same ownership problem can reappear at queue handoff: when the authoritative rollout user row arrives, useIpcSubscriptions removes the queued prompt and appends the real user row to entries. Feed still paints entries -> semanticHistory -> semanticCurrent -> work, so if semantic history is still renderable, the real user row is again above stale semantic/work rows.
    • There is also a timing race: if no semantic history is renderable at submit time, shouldQueueOptimisticCodexUserEntry returns false and the optimistic prompt is appended to entries; if older semantic history becomes renderable one frame later, the prompt is again above semantic/work rows.

    Saved bundle replay:

    • 2026-05-20T14-00-04-079-b53fc4fe: prompt can you figure it out for me . is present at row 83/86, followed by semanticHistory=1, semanticCurrent=1, work=1.
    • 2026-05-20T15-09-04-906-7b859c43: prompt please read A LOT... is present at row 60/76, followed by semanticHistory=14, semanticCurrent=1, work=1.
    • 2026-05-20T15-15-32-562-7b859c43: prompt Lets reivew it with a codex agent... is present at row 62/110; this one also has later committed assistant/tool entries, so it is not the exact same "row absent" shape, but it still has a large stale semantic-history tail below committed rows.

    Temp strategy simulation says a single ordered render plan can distinguish the cases:

    • stale semantic history ended before a newer user prompt should render before that user prompt, not after all entries;
    • current/live semantic that starts after the prompt should still render after the prompt.

    So the remaining fix should not be another point workaround in submit/queue reconciliation. The issue is still the multi-plane renderer. We need the feed selector to produce one ordered FeedRenderItem[] containing committed entries, semantic history, semantic current, work, and queued prompt state, with explicit ownership/time ordering.

  7. Juliusolsson05 commented on Jun 22, 2026

    @Juliusolsson05
    OwnerAuthor

    Adding saved-bundle diagnosis link. I opened #339 for the specific prompt-ownership failure mode:

    #339: Submitted prompts can stay queued or screen-only while the agent is already responding

    Key evidence:

    • 2026-06-22T11-53-14-054-3398cdb4: prompt is in queuedMessages, session is active/thinking, and feed only shows queued-message UI.
    • 2026-06-18T11-37-47-852-e5236cbe: same stuck queued-prompt shape while active/thinking.
    • 2026-06-22T12-34-16-431-2db014bc: queuedMessages=[], but submitted prompt appears in screen tail and not as a feed row.
    • Several May 20 saved notes show the same recurrence around prompt rendering, queue state, and MCP-heavy turns.

    Likely root: follow-up prompts are intentionally routed into queuedMessages while semantic history/current is renderable, but if the committed rollout/transcript user row is missing, delayed, filtered, or screen-only, the prompt never gets a durable feed owner while the provider is active.

  8. Juliusolsson05 commented on Jul 7, 2026

    @Juliusolsson05
    OwnerAuthor

    Resolved by the ownership-ledger rewrite (PR #442, cutover #483).

    The ledger (src/renderer/src/rendering/model/ledger.ts + ownership.ts) is now the sole pre-render decision core: it assigns a single owner per feed item across the committed / semantic-current / semantic-history / ghost / optimistic-submit planes, and Feed only paints its output (useLedgerFeedItems). The legacy deriveFeedRenderModel multi-owner path is deleted. This is exactly the "feed ownership ledger / selector" proposed in this issue.

    Closing as delivered. Individual child symptoms tracked separately where a non-rendering component remains (e.g. #340 Codex subagent state, #342 Task* UI).

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