Repository navigation
Description
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:
-
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, nodata-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.
- Debug bundle:
-
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:
SemanticStreamingTurnrenders a mutablecurrentTurn; if multiple assistant messages/tool phases happen before durable rows arrive, the UI can look like replacement rather than append/stable provisional rows.
- Debug bundle:
-
Mid-turn/follow-up prompt suspicion / streaming becomes hard to reason about
- Debug bundle:
2026-05-16T18-08-15-424-f4b37c29. Write tests for @filenameappears in the terminal/screen snapshot but not as anoptimistic_user_addor committed user row infeed-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, thenflow_ignored proxy-8/9/10 — concurrent with active flow proxy-7, thenflow_selected proxy-11later. 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.
- Debug bundle:
-
Historical feed clearing / missing user rows during MCP/tool-call turns
- Tracked in Fix feed clearing and missing user rows during MCP/tool-call turns #159 and fixed in layers by fix(codex): correct rollout tail ownership — feed clearing during MCP turns (#159) #160 and fix(feed): keep semantic history visible while JSONL catches up #165.
- Evidence from Fix feed clearing and missing user rows during MCP/tool-call turns #159: 559s session, 67 Responses turns,
SEM/turn_startedandturn_completedhealthy,SEM/block_completedhealthy, butJSONL/jsonl_entriesfired only once at bootstrap (+53). The committed transcript channel was dead after bootstrap. - User input was received (
optimistic_user_addfired), and the agent reacted, but rows did not remain visible because committed entries froze and live semantic currentTurn churned.
-
Resumed Codex pane dropped live streaming and duplicated semantic history keys
- Tracked in Codex resume feed broken: proxy streaming turn dropped + duplicate semantic-history keys #168 and fixed in fix(feed): restore Codex live streaming on resume + de-dupe semantic history #167/chore(codex): bump codex-headless — drop false rollout-error on tail switch #169.
turn_startedfrom rollout opened an empty019e...placeholder. Proxy streamed the realresp_*turn, but strict mismatch guards dropped it becausecurrentTurnwas already owned by the placeholder.- Semantic history also accumulated duplicate
turnIdvalues (semantic-history:rollout-...React key duplicates), breaking reconciliation.
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_addproved 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.
SemanticStreamingTurnstill 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, deletingSemanticStreamingTurn) 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
- Fixed provider/runtime layer for Fix feed clearing and missing user rows during MCP/tool-call turns #159 by making Codex tail follow forked rollout files and releasing proxy active-flow slot on
response.completedso MCP follow-up turns publish. - This addressed dead committed transcript in one class, but did not solve renderer ownership generally.
#165 — semantic history bridge
- Added bounded full semantic history rendered by
Feedwhile JSONL catches up. - This was a necessary mitigation for completed turns leaving
currentTurnbefore 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_itemat a time and several committed rows can sharecodexTurnId.
#168 / #167 / #169 — resume feed broken after semantic-history mitigation
- Codex resume feed broken: proxy streaming turn dropped + duplicate semantic-history keys #168 found two renderer failures and one false transcript error:
- proxy
resp_*turn dropped by empty rollout placeholder strict guard - duplicate semantic-history keys from repeated bootstrap fallback ids
- successful rollout-tail switch emitted as error banner
- proxy
- fix(feed): restore Codex live streaming on resume + de-dupe semantic history #167 allowed proxy to replace an empty non-proxy placeholder and de-duped semantic history by
turnId. - chore(codex): bump codex-headless — drop false rollout-error on tail switch #169 stopped false
rollout-erroron successful tail switch.
#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
WriteRowbehavior. - 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, committedmessage.id/codexTurnId/text hash as fallback - tool call:
toolUseId/callId - user prompt: optimistic uuid -> committed uuid/text reconciliation
- assistant text block: semantic
- current owner:
committedsemantic-currentsemantic-historyghostoptimistic-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.tssrc/renderer/src/workspace/semantic/helpers.tssrc/renderer/src/workspace/ghosts.tssrc/renderer/src/workspace/mergedEntries.tssrc/renderer/src/features/feed/ui/Feed.tsxsrc/renderer/src/features/feed/ui/semantic/StreamingTurn.tsxsrc/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.
-
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.
Feedshould render the ledger, not independently render committed entries plus semantic history plus current turn with ad hoc suppression.
-
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.
-
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, semanticitemId) and only use exact text as a fallback for known Codexresp_*vs rollout id splits.
-
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.
-
Clarify ghost vs semantic responsibilities
- Either complete the original ghost Phase 3 design (ghost/provisional rows are the live render path and
SemanticStreamingTurnis 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.
- Either complete the original ghost Phase 3 design (ghost/provisional rows are the live render path and
-
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_addor 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
- Fix feed clearing and missing user rows during MCP/tool-call turns #159
- fix(codex): correct rollout tail ownership — feed clearing during MCP turns (#159) #160
- fix(feed): keep semantic history visible while JSONL catches up #165
- fix(feed): restore Codex live streaming on resume + de-dupe semantic history #167
- Codex resume feed broken: proxy streaming turn dropped + duplicate semantic-history keys #168
- chore(codex): bump codex-headless — drop false rollout-error on tail switch #169
- fix(feed): suppress committed semantic assistant duplicates #170
- Add rendering trace debug for transcript and semantic channel ownership #171
- Streaming Write tool calls render as raw partial JSON in the live feed #138 / feat(feed): live preview for streaming Write tool calls #134
Activity
Juliusolsson05 commentedon May 16, 2026
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.mdKey reasoning from the doc:
SemanticStreamingTurnfromruntime.semantic.currentTurn, not by ghosts.Important invariant from the doc:
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.tsdocuments the five-rule ghost render predicateSource:
src/renderer/src/workspace/mergedEntries.tsInline comments define ghost rendering as a narrow fallback, not normal live rendering. A ghost is render-eligible only if all of these hold:
SemanticStreamingTurnowns thatlastJsonlEntryAt, meaning proxy state is past the JSONL tailThe comments explicitly explain why rules 4 and 5 both exist:
Important invariant:
selectMergedEntriesmust returnruntime.entriesby 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.tssays ghosts are bookkeeping today, not the primary live UI pathSource:
src/renderer/src/workspace/ghosts.tsThe file-level comments state:
ghostsFromSemanticTurnmints provisional entries from semantic blocks.reconcileUpstreamsupersedes them when JSONL lands.orphanStalemarks them after TTL.SemanticStreamingTurn.The comments also warn that reducers must return
prevunchanged on no-op. This is not described as an optimization; it is described as load-bearing becauseFeedrow 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
setRuntimescascades and busteduseMemo([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.mdpackages/agent-transcript-parser/src/ghost.tsKey reasoning from the library docs/comments:
_atp.origin = 'ghost'.g-<turnId>-<blockIndex>.mergeWithUpstreamis 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.mdThis is the most important historical write-up. It says:
SemanticStreamingTurnstill owns live current-turn rendering.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-taillocal v1: hide all ghosts again. Same failure as10e4fc5: 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.mdThe plan explicitly says timestamp-only is insufficient:
Example from the plan:
ghost.updatedAt=105 > lastJsonlEntryAt=100So the current code keeps both:
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.mdThe plan clustered the failures around two boundaries:
SemanticStreamingTurnand the ghost-merged feedIt recorded concrete defects:
The plan also says the fixes were surgical and did not delete the legacy live-view path. Phase 3 deletion of
SemanticStreamingTurnwas 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.mdCore rule from the plan:
Only one source may publish semantic turn mutations for a given turn lifecycle.
The plan names the source split:
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:
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.tsdocuments reducer ownership and Codex mismatch trapsSource:
src/renderer/src/workspace/semantic/foldEvent.tsImportant inline reasoning:
runtime.semantic, not subscribe independently.resp_*turnImplication for this issue:
The exact
resp_*vs019e...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.tsdocuments why semantic history existsSource:
src/renderer/src/workspace/semantic/helpers.tsImportant inline reasoning:
semanticHistoryRowkeeps 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.appendSemanticHistoryreplaces byturnIdinstead 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.tsxandrenderUnits.tscontain the current local duplicate guardsSources:
src/renderer/src/features/feed/ui/semantic/StreamingTurn.tsxsrc/renderer/src/features/feed/ui/semantic/renderUnits.tsscripts/test-semantic-committed-text.tsImportant inline reasoning:
turn.textas markdown would recreate screen-parsing brittleness.resp_*live ids but committed rollout/task019e...idsImplication 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.tsxcomments warn against reintroducing screen/content double renderingSource:
src/renderer/src/features/feed/ui/Feed.tsxImportant inline reasoning:
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.tssrc/renderer/src/workspace/hook/ipc/useIpcSubscriptions.tsKey reasoning from comments:
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:
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.
Juliusolsson05 commentedon May 16, 2026
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.tsInline reasoning says resumed Claude/Codex bootstrap tails must be delivered as one bulk
session:jsonl-entriesburst, 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.tsImportant inline reasoning:
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.tssrc/renderer/src/workspace/codex/rollout.tsKey inline reasoning:
turn_contextis the clean source of truth, but payload-levelturn_idis load-bearing when chunks start afterturn_context.Entrys with stable synthesized uuids from timestamp plus payload id / call_id / type.turn_idon every item, so Agent Code stampscodexTurnIdlocally for ghost reconciliation.<environment_context>are filtered beforeruntime.entriesso 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.
codexTurnIdand stable synthesized uuids are part of the ownership proof. Missing or drifting stamp logic causes exactly the class fixed narrowly in #170: live proxyresp_*and committed rollout019e...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.tspackages/codex-headless/src/channels/SemanticChannel.tspackages/codex-headless/src/channels/CommittedChannel.tspackages/codex-headless/src/channels/ScreenChannel.tsKey inline reasoning:
proxy,rollout, andscreenare mutually exclusive live owners.SemanticChannelis 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:
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.tsImportant inline reasoning:
requestId, not by path.response.idbecomes semanticturnId.call_idpairs 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.tsImportant inline reasoning:
semanticShadow, not the renderer-facing semantic channel.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.tssrc/renderer/src/workspace/hook/persistence/useAutoSave.tssrc/renderer/src/workspace/hook/persistence/useBootstrap.tsKey inline reasoning:
resumeSessionIdis persisted immediately, before transcript lines round-trip, so the pane can resume later even if no line arrives yet.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.tssrc/renderer/src/workspace/hook/ipc/useIpcSubscriptions.tsInline 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:
9. Feed/debug tooling already encodes the desired forensic workflow
Sources:
src/renderer/src/workspace/runtime/feedDebug.tssrc/main/storage/feedDebugLog.tssrc/renderer/src/features/debug/saveDebugBundle.tssrc/main/storage/debugBundle.tssrc/renderer/src/features/debug/renderTrace.tssrc/renderer/src/features/debug/ui/HtmlDebugPanel.tsxdocs/superpowers/specs/2026-04-21-html-debug-panel-design.mdKey inline/doc reasoning:
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:
10. Rendering harness history already names these bug classes
Sources:
testing/rendering/README.mddocs/superpowers/plans/2026-04-20-claude-dup-text-and-screen-churn.mdtesting/regression-claude-tool-result.tsscripts/test-ghost-fallback.tsscripts/test-semantic-committed-text.tsKnown 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 intoSemanticLiveTurn.tool_resultfailed to release live semantic turn, so JSONL rows and lingering semantic row rendered together.resp_*id require exact committed-text suppression for finalized live assistant text.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:
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:
Juliusolsson05 commentedon May 16, 2026
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.tssrc/renderer/src/workspace/hook/actions/streaming.tssrc/renderer/src/workspace/hook/ipc/useIpcSubscriptions.tsdocs/superpowers/plans/2026-04-21-claude-paste-submit-clean-fix.mdImportant code-comment invariants:
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.tspackages/codex-headless/src/channels/types.tspackages/claude-code-headless/src/channels/SemanticChannel.tspackages/codex-headless/src/channels/SemanticChannel.tsdocs/superpowers/plans/2026-04-18-headless-live-turn-redesign.mdImportant code-comment invariants:
semantic: what the provider/model is producing nowscreen: terminal paint, overlays, approvals, trust dialogs, command UIcommitted: durable transcript/rollout truthSemanticChannelis strict transport, not a healer. Mismatched starts/deltas are dropped with diagnostics instead of silently sealing one turn and opening another.ClaudeCodeHeadless/CodexHeadless, because only the orchestrator sees proxy, rollout, and screen state together.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.tspackages/codex-headless/src/proxy/CodexResponsesAdapter.tsdocs/superpowers/plans/2026-04-17-codex-semantic-flicker-fix.mddocs/superpowers/plans/2026-05-06-claude-title-turn-filter.mdImportant code-comment invariants:
flow_ignoredand must not mutate the visible semantic turn.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_ignoreddecisions alongside row ownership decisions.4. Committed transcript ingestion is bulk and deduped by design
Relevant areas:
src/main/sessions/jsonlCoalescer.tssrc/main/sessions/forwarder.tssrc/renderer/src/workspace/hook/ipc/useIpcSubscriptions.tssrc/renderer/src/workspace/hook/actions/initialHistory.tsdocs/superpowers/plans/2026-04-15-bootstrap-replay-perf.mdImportant code-comment invariants:
seenUuidsRef, tool indices,lastJsonlEntryAt, history markers, and ghost/optimistic reconciliation must be initialized before live replay can duplicate rows.entriesper 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.tssrc/renderer/src/workspace/semantic/helpers.tsdocs/superpowers/plans/2026-04-17-claude-semantic-provider-gating.mddocs/superpowers/plans/2026-04-17-codex-semantic-flicker-fix.mdImportant code-comment invariants:
runtime.semantic; they should not subscribe and fold independently.msg_*turn.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.tsxsrc/renderer/src/features/feed/ui/semantic/StreamingTurn.tsxsrc/renderer/src/features/feed/ui/semantic/renderUnits.tssrc/renderer/src/features/feed/WorkIndicator.tsxsrc/renderer/src/features/feed/ui/rows/*Current render order:
EntryRowrowsWorkIndicatorImportant code-comment invariants:
SemanticStreamingTurn. The old raw TUIStreamingRowwas removed because it double-rendered resume tails and stale previous-turn text.message.idowns the turn.turn.textis cleared because committed rows own that text.WorkIndicatoris the only in-feed working affordance. It is phase-driven, not gated on semantic turn presence, to avoid blind spots during tool execution.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.mdtesting/rendering/renderer/RenderingHarnessApp.tsxsrc/renderer/src/features/debug/ui/FeedDebugPanel.tsxsrc/renderer/src/features/debug/saveDebugBundle.tssrc/renderer/src/features/debug/renderTrace.tssrc/renderer/src/features/debug/ui/HtmlDebugPanel.tsxdocs/superpowers/plans/2026-04-18-feed-debug-stream.mddocs/superpowers/specs/2026-04-21-html-debug-panel-design.mdImportant code-comment/doc invariants:
STATE,JSONL,SEM,RENDER,GHOST.RENDERis the critical layer for this issue because it records visible row decisions, not just raw inputs.outerHTML, not whole-app DOM and not a live observer.REND-1: subscribe-before-spawn for bootstrap JSONLREND-2: resume double-rendered last message because screen and JSONL both painted itREND-3: stale previous assistant text after submit because semantic folding/baseline ownership failedImplication 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
RENDERownership decision with:html-clean.htmlAdditional acceptance criteria for #172
flow_selected/flow_ignored) are visible in the rendering trace when proxy semantics are involved.Regression matrix to keep attached to this issue
Run/extend these when touching the relevant layer:
npm run test:ghost-fallbacknpm run test:semantic-committed-texttesting/regression-claude-tool-result.tsnpm run testing:rendering:buildscripts/proxy-harness-semantic.mts/scripts/proxy-harness-real.mtsIf a new #172 reproduction is captured, add the next
REND-Nentry totesting/rendering/README.mdand put a thick WHY comment at the patch site.Juliusolsson05 commentedon May 16, 2026
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.tssrc/renderer/src/workspace/tile-tree/TileLeaf/claudePaste.tssrc/renderer/src/workspace/hook/actions/streaming.tssrc/renderer/src/workspace/hook/ipc/useIpcSubscriptions.tsImportant comments/invariants:
streamingBaselinefrom 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.setStreamingBaselinealso marksawaitingAssistant, setsstreamPhase: 'submitting', and timestamps before adapter events arrive.totalEntries.[Pasted text #N]placeholder; wall-clock delay is fallback only.CLAUDE_PASTE_THRESHOLD = 100is 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:
Optimistic Codex reconciliation invariants
Relevant files:
src/renderer/src/workspace/hook/actions/streaming.tssrc/renderer/src/workspace/hook/ipc/useIpcSubscriptions.tsImportant comments/invariants:
current.entriesbecause 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-userhandoff, independent of row position.Queued follow-up prompts are intentionally not feed rows
Relevant files:
src/renderer/src/workspace/hook/ipc/useIpcSubscriptions.tssrc/renderer/src/workspace/tile-tree/TileLeaf/QueueStrip.tsxsrc/renderer/src/workspace/tile-tree/TileLeaf.tsxImportant comments/invariants:
runtime.queuedMessages; they are intentionally not pushed intoruntime.entries.QueueStriprenders queued messages between Feed and composer so they are visible but not mixed into the committed transcript feed.Implication:
For the mid-turn/follow-up bug class, the trace must distinguish:
QueueStripPrompt history is downstream of committed/visible entry correctness
Relevant files:
src/renderer/src/workspace/tile-tree/TileLeaf/usePromptHistory.tssrc/renderer/src/features/workspace/lib/latestUserPrompts.tsImportant comments/invariants:
runtime.entries, not backend TUI history.permissionModeand filters meta entries, slash-command errors, local-command caveats, and tool-result-only user-role rows.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
QueueStripand must not be inserted as phantom feed rows.Juliusolsson05 commentedon May 20, 2026
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-7b859c43STILL NOT SHOWING FUCKING USER PROMPT ; THE FUCK ?STATE submitandoptimistic_user_addfired for the real submitted prompt beginningplease read A LOT...RENDER visible_rowsincludes that optimistic user rowSo 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 @filenameline 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.mdsaysderiveFeedRenderModelis the first implementation pass.FeedRenderItem[].docs/codex-rewrite-render/feed-ui-rendering.mdsays current semantic content is appended after committed rows and that this is not a general model.Current code confirms that.
deriveFeedRenderModelreturns buckets:visibleEntriesrenderedSemanticHistoryrenderedSemanticTurnshouldShowWorkIndicatordebugRowsThen
Feed.tsxstill renders separate JSX planes:That means final DOM order is still structurally:
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-modelpassednpm run test:codex-optimistic-submitpassednpm run test:semantic-committed-textpassednpm run test:ghost-fallbackpassednpm run test:semantic-fold-codex-replacepassednpm run test:session-ownershippassednpm run test:codex-semantic-channelpassed after submodules were initializednpm run buildpassed after submodules were initializedI 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:
The diagnostic also verifies #252's submit gate returns
truefor 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:
deriveFeedRenderModelor a successor returns a single orderedvisibleItems: FeedRenderItem[]list.Feed.tsxmaps that list once; it does not separately map committed entries, semantic history, semantic current, and work.committed,semantic-history,semantic-current,optimistic-submit,queue-strip,ghost,work, orempty.This is not a new isolated issue. It is #172 still being unfinished.
Juliusolsson05 commentedon May 20, 2026
Additional diagnostics from isolated simulation and saved-bundle replay:
#252fixes one submit-time path by keeping a mid-turn Codex prompt inQueueStripwhen renderable semantic history/current already exists. That makes the prompt visible at the pane bottom immediately after submit.useIpcSubscriptionsremoves the queued prompt and appends the real user row toentries.Feedstill paintsentries -> semanticHistory -> semanticCurrent -> work, so if semantic history is still renderable, the real user row is again above stale semantic/work rows.shouldQueueOptimisticCodexUserEntryreturns false and the optimistic prompt is appended toentries; 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: promptcan you figure it out for me .is present at row83/86, followed bysemanticHistory=1,semanticCurrent=1,work=1.2026-05-20T15-09-04-906-7b859c43: promptplease read A LOT...is present at row60/76, followed bysemanticHistory=14,semanticCurrent=1,work=1.2026-05-20T15-15-32-562-7b859c43: promptLets reivew it with a codex agent...is present at row62/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:
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.Juliusolsson05 commentedon Jun 22, 2026
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 inqueuedMessages, 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.Likely root: follow-up prompts are intentionally routed into
queuedMessageswhile 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.Juliusolsson05 commentedon Jul 7, 2026
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, andFeedonly paints its output (useLedgerFeedItems). The legacyderiveFeedRenderModelmulti-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).