Repository navigation
Description
Context
MCP/tool-call turns are exposing a long-running feed rendering failure. During active agent work, the visible conversation can appear to clear after a couple of messages, and injected user messages are received by the agent but sometimes never render in the feed.
This appears related to the same family of issues that led to ghost records, but the current evidence points at semantic/feed ownership during client-executed tool turns rather than the MCP server itself.
Observed symptoms
- When an agent calls MCP or other client-side tools, the feed may stop growing and look like it is replacing/clearing rows.
- User messages are accepted by the runtime and the agent reacts to them, but the user row may not appear in the visible conversation.
- Codex output can look empty after tool-call handoffs.
- The issue is suspected to affect both Codex and Claude paths, though the latest reproduction is Codex + MCP.
Current evidence
Latest inspected feed-debug session:
~/.config/agent-code/feed-debug/3c8ada8f-c6fe-45b4-b7e1-bd49f72c29c3.jsonl
Notable trace points:
optimistic_user_addfires for injected user messages, so input reception is not the first failure.- MCP/function-call turns produce
block_started,block_completed,turn_completed, andstream_phase: awaiting-tool. - Follow-up Responses turns then continue after tool output. This stresses the handoff between semantic current turn, semantic history, JSONL commits, and ghost rows.
- Existing render-row logging was gated behind the Feed Debug panel, which meant saved traces often missed the final render breakpoint.
Working hypothesis
There are two related ownership gaps:
- Completed semantic turns can leave
currentTurnbefore JSONL entries have caught up. If history stores only summary text or Feed does not render semantic history, the conversation visually shrinks during MCP/tool-call gaps. - Optimistic user entries are added, but the final React-visible row list must be logged and verified to determine whether they are lost in runtime reconciliation or filtered/collapsed during feed rendering.
Codex needs special care: one logical Responses turn can commit multiple response_item entries that share a codexTurnId, so whole-turn duplicate suppression by codexTurnId can incorrectly hide still-uncommitted assistant/tool blocks.
Investigation / implementation checklist
- Persist render-row debug logs for focused panes even when the debug panel is closed.
- Verify whether
optimistic_user_addincreasesentriesand appears invisible_rows. - Keep a bounded, renderable semantic history bridge for completed turns until JSONL supersedes them.
- Ensure Codex duplicate suppression is per block/item, not whole turn by shared
codexTurnId. - Audit Claude semantic-history/tool-result path for the same lifecycle gap.
- Add regression coverage around MCP/client-tool turn handoff and optimistic user row visibility.
Acceptance criteria
- User messages render immediately and remain visible while the agent is working.
- MCP/function-call turns append/grow the feed instead of visually clearing it.
- Debug traces show a complete chain from input, semantic events, JSONL/ghost reconciliation, to final
visible_rows. - Fixes preserve the existing anti-flicker protections that motivated strict Codex turn ownership.
Activity
Juliusolsson05 commentedon May 16, 2026
Investigation update from the current debugging pass.
Findings so far:
orchestration_list_agentsshows up as a normal Responsesfunction_call, thenturn_completed, thenstream_phase: awaiting-tool, then the follow-up Responses turn.optimistic_user_addfor the user messages, so the agent reacting while the user row is not visible means the row is lost or hidden after runtime insertion.currentTurnbefore JSONL has durable rows. The old semantic history shape was summary-only and Feed did not render it, so MCP/tool-call gaps could make the feed visually shrink/clear.codexTurnId; Codex commits oneresponse_itemat a time, so duplicate suppression must be per block/item.Local changes made while investigating:
tool_completedwhen no pending tools remain.optimistic_user_addand JSONL optimistic reconciliation.visible_rowsrender logging include entry-count/visible-count changes, not only row-key changes.Current caveat:
The running app still has not emitted
RENDER visible_rowsafter the latest TileLeaf logging change, which strongly suggests the active renderer did not pick up that source change via HMR. A restart from this working tree should be the next validation step so the trace includes the final render breakpoint.Validation run locally:
npm run test:session-ownershippassed.npm run test:ghost-fallbackpassed.npm --prefix packages/codex-headless run buildpassed.git diff --checkpassed.Juliusolsson05 commentedon May 16, 2026
Findings — root cause traced end to end
Investigated against the live feed-debug log for session
3c8ada8f(559s span, 67 Responses turns).What the debug log proves
SEM/turn_started/turn_completedSEM/block_completedJSONL/jsonl_entries+53)STATE/optimistic_user_add/removeOver 9 minutes / 67 turns,
runtime.entrieswas frozen at the 53-entry bootstrap snapshot. Nothing committed.Breakpoint chain
Root cause — the Codex rollout-file tailer goes silent after bootstrap.
CodexHeadless.start()attachestailFile()to one rollout JSONL file. This session was resumed (53 entries of prior history) → it took thefindRolloutByThreadId→tailFile(rolloutPath)branch. The tailer read the 53-linebootstrapTail, then received nothing — it is pinned to a file Codex is no longer writing. On-disk evidence:~/.codex/sessions/2026/05/16/contains both resumed files (filename-ts ≪ mtime → Codex appended) and rollout files created 3-within-10s (the orchestration multi-Codex burst). Codex's resume behavior is inconsistent: sometimes appends, sometimes forks a new rollout file. When it forks,tailFileis stuck on the dead old path. The fallback-to-new-file-watcher only fires whenfindRolloutByThreadIdmisses — not when it hits but Codex forked. That gap is the bug.Store layer — committed channel dead ⇒
runtime.entriesnever grows. The 7 optimistic user rows are appended and never removed, but the path that grows the feed (session:jsonl-entries→ Pass B) never fires.Renderer layer — with no committed entries, the only live source is the single
currentTurnslot infoldEvent.ts. Everyturn_completedarchived it tohistory, which (pre-patch) was a content-less summaryFeednever rendered. MCP/exec turns produce many short Responses turns ⇒ the single slot churns every few seconds ⇒ "feed clears after ~2 messages, conversation never grows." The user's own message appears to vanish — it is stuck in the frozen 53-entry block while the live turn below it keeps resetting.MCP is not the cause. It just multiplies turn count so the single-slot churn and the history cap get hit fast.
Why the ghost system does NOT save us here
The ghost system is, by its own design (
mergedEntries.ts,docs/design/ghost-system.md), a fallback for a ~100ms JSONL-stall — not a renderer of record. It cannot cover a permanent committed-channel outage:GHOST_ORPHAN_TTL_MS = 30000). A ghost is render-ineligible until 30s after its last update (predicate rule 2). The system assumes JSONL catches up in ~100ms. With JSONL permanently dead, every completed turn is invisible for its first 30s — and by then 5-10 more turns have churned past it.turnId === currentTurnId(deferring toSemanticStreamingTurn). So a just-completed turn is owned by nobody for ~30s:SemanticStreamingTurndropped it (currentTurn moved on), the ghost predicate hasn't adopted it yet (not orphaned).So: ghosts can't fix this because they were built to bridge 100ms, not to be the session's renderer.
Fix direction
CodexHeadlessresume must not assume thefindRolloutByThreadIdfile stays live. Run the new-file watcher concurrently on the resume path, or watch the session dir for a forked rollout, or detect "tailer silent N s while proxy is active" and re-resolve. The orchestration burst (3 rollout files in 10s) also makestailNewRolloutFile's "first new file wins" unsafe for fresh child agents — it can latch a sibling agent's file.historybuffer rendered byFeed) instead of relying on the 30s-late, sidecar-filtered ghost path.Juliusolsson05 commentedon May 16, 2026
Correction — re-read
docs/design/ghost-system.mdMy earlier comment was too dismissive of the ghost system ("outside its design envelope"). After re-reading the design doc, the precise picture:
The ghost system WAS designed for this failure mode. Doc fallback case #1, verbatim: "JSONL stuck mid-turn. The agent process gets wedged or its writer backlogs while the proxy keeps emitting events... The ghost on disk is the only record of the turn." That is exactly this bug.
But the doc also states the design was never finished, and that's why it doesn't save us:
Phase 3 was never built. Doc: "The original design intent was for ghost to be the only live render path.
SemanticStreamingTurnwould be deleted... That deletion never happened." What ships is the bookkeeping half (mint/reconcile/persist) + a narrow fallback render.SemanticStreamingTurnstill owns the live turn; predicate rule 3 deliberately excludes ghosts for it.The 30s TTL is a "wait and see," not a paint speed. Doc: "The TTL is not 'how fast we paint fallback' — it's 'how long we wait before concluding JSONL isn't coming.'" Healthy sessions never hit it (JSONL arrives in ~100ms). Here JSONL is dead for all 67 turns, so every turn eats the full 30s — the ghost feed is permanently 30s-lagged and gappy, not empty.
Rule 5 silently drops short messages. Any assistant ghost that is a single text block ≤200 chars is classified as title-gen sidecar and hidden forever, even when orphaned. Short Codex commentary turns match that shape. The doc calls this a knowingly-accepted trade-off.
The decisive point: the ghost system's entire correctness model is "JSONL is the source of truth; the ghost bridges until it catches up" — reconciliation,
lastJsonlEntryAt, supersedure, the predicate all assume JSONL eventually arrives. This bug's root cause (the dead Codex rollout tailer, see previous comment) means JSONL arrives never. The ghost system has nothing to reconcile against, so it degrades to a 30s-lagged, short-message-filtered, half-empty feed — exactly the reported symptom.Conclusion: the ghost system is not the fix and was never going to be, because the cable it bridges is severed upstream of it. Fix priority order stands:
Finishing Phase 3 would not have prevented this — it would still need JSONL to reconcile against.
Juliusolsson05 commentedon May 16, 2026
Debug update: Codex source + local logs point to a committed-rollout ownership bug, not an MCP tool execution bug.
What Codex upstream does:
codex resume <id>is routed by the CLI into interactive TUI resume (vendor/codex-src/codex-rs/cli/src/main.rs:765,:1388).app_server.resume_thread(... target_session.thread_id)(vendor/codex-src/codex-rs/tui/src/app.rs:3724).InitialHistory::Resumed(vendor/codex-src/codex-rs/core/src/thread_manager.rs:502).Codex::newusesRolloutRecorderParams::resume(resumed_history.rollout_path)for resumed sessions (vendor/codex-src/codex-rs/core/src/codex.rs:1636).RolloutRecorderParams::Resumeopens that exact path with append mode and does not create a new session_meta (vendor/codex-src/codex-rs/rollout/src/recorder.rs:500).So a true Codex resume should keep appending the original rollout. If Agent Code sees a fresh rollout with a new
session_metaand copied prior history, that is a fork/reconstruction path, not the durable file that a resumed pane should blindly treat as the same provider session.Live evidence from the affected pane:
~/.config/agent-code/feed-debug/3c8ada8f-c6fe-45b4-b7e1-bd49f72c29c3.jsonl.JSONL/jsonl_entries: 1 total, the bootstrap burst only:entries +53at2026-05-16T15:29:08.981Z.SEM/turn_started: 112.SEM/block_completed: 374.STATE/submit: 9.STATE/optimistic_user_add: 9.Rollout evidence:
3c8ada8f-c6fe-45b4-b7e1-bd49f72c29c3to provider session019e310d-ce49-7061-a13f-456d7dd609cd.rollout-2026-05-16T15-50-48-019e310d-ce49-7061-a13f-456d7dd609cd.jsonlis actively growing. It contains the same current assistant/tool work that the proxy semantic stream reports, including the Codex-source investigation messages after line 3048.rollout-2026-05-16T15-36-50-c0d44dcb-3972-4c55-9a31-a242bb482e2a.jsonl. Its first line is a newsession_metaforc0d44dcb..., followed by copied history from019e310d..., and it stops around2026-05-16T15:36:50Z. This file shape matches a fork/reconstructed history file, not true resume append.Root cause hypothesis now:
Agent Code's Codex headless layer treats rollout identity too weakly. It can bootstrap from a plausible same-cwd/session-like file, while the live Codex process/proxy is actually producing the durable transcript in a different rollout. Once that happens, the renderer keeps receiving semantic/proxy events, but durable
jsonl-entryevents never land for the visible pane. The user sees exactly the haunted behavior: optimistic user rows appear logically, assistant reacts, then the visible feed never grows because committed entries are frozen.Suggested fix:
session_meta.idto match. Do not accept a same-cwd copied-history fork as equivalent.packages/codex-headless/src/CodexHeadless.ts, but it should be tightened to use provider id when available, not just cwd.session_meta.cwd, capture the resulting provider id, persist it to workspace state, and log ambiguity if multiple new same-cwd rollouts appear during startup.This also explains why MCP exposed the issue without necessarily causing it. MCP creates longer client-tool gaps where semantic/proxy output can keep moving while committed JSONL is the thing that proves whether the transcript owner is correct.
Juliusolsson05 commentedon May 16, 2026
Fix PRs open
Layer-1 fix (the root cause — dead Codex rollout tailer):
fix/codex-rollout-tail-ownershipresponse.completedso MCP follow-up turns publish.fix/codex-feed-clearing-mcp, bumps the submodule pointer to Track primary and active worktree context #9.Verified by
tscbuild only. Not yet exercised against a live Codex resume/fork — a live smoke test (resume a Codex pane, run an MCP-heavy turn, confirm the committed feed grows) is recommended before merge.The renderer-side mitigation (bounded semantic-history bridge) remains separate and is not in these PRs.
[-]Debug feed clearing and missing user rows during MCP/tool-call turns[/-][+]Fix feed clearing and missing user rows during MCP/tool-call turns[/+]fix(codex): address review — close fork-watcher race, harden lineage
fix(codex): bump codex-headless to fix rollout tail ownership (#159)
Merge pull request #160 from Juliusolsson05/fix/codex-feed-clearing-mcp
fix(codex): bump codex-headless to fix rollout tail ownership (#159)