Skip to content

fix(feed): suppress orphan ghosts with title-gen / predict-next-prompt shape - #54

Merged
Juliusolsson05 merged 1 commit into
mainfrom
fix/orphan-sidecar-ghosts
May 7, 2026
Merged

Juliusolsson05 merged 1 commit into
mainfrom
fix/orphan-sidecar-ghosts

Conversation

@Juliusolsson05

Copy link
Copy Markdown
Owner

Summary

Stops Claude Code's auxiliary API calls (title-gen, branch-name-gen, "predict the user's next prompt") from leaking 2-10-word fragments to the bottom of the feed where they accumulate over the whole session.

Forensic root cause (debug bundle 2026-05-07T08-26-35-212-5d948ab5)

  • 7 visible fragments at the bottom of the HTML render, all data-entry-uuid=\"g-msg_*-0\" (orphan ghosts).
  • Each ghost UUID maps 1:1 to a sub-second proxy turn in proxy-semantic.json history: 188-602ms, 12-41 chars (vs real turns 808ms-37s, 76-6579 chars).
  • 0 of 23 proxy flows had attribution: 'ignored' — the existing sidecar predicate (c8c2623) never demoted any of these calls.

The body-shape predicate misses the predict-next-prompt feature because those calls include the full conversation history (messageCount > 3 → compound predicate fails) and use a system prompt not in the 4-entry SIDECAR_SYSTEM_PROMPT_PREFIXES list. Fixing it properly needs the actual request body to extend the prefix list — which we don't have.

Fix

Add ghostHasSidecarShape(ghost) to selectMergedEntries. Filter out orphan ghosts that look like title-gen / predict-next-prompt output:

  • assistant role
  • single content block
  • text type
  • ≤ 200 chars

The bundle's longest sidecar text was 41 chars; shortest real assistant turn was 76 chars — so 200 is a comfortable margin.

Trade-off

A genuinely brief assistant orphan (e.g. "Done.") whose JSONL never committed would also be suppressed. Accepted because orphans are already an error path (JSONL not landing means upstream Claude Code crashed), and permanent visual clutter from 6+ stale title fragments is a daily UX harm whereas a hidden 4-char crash response is rare and unrecoverable either way.

Future work

When we capture a live request body for the predict-next-prompt feature, extend SIDECAR_SYSTEM_PROMPT_PREFIXES and the body-shape predicate in claude-code-headless. At that point this renderer-side filter becomes redundant and can be deleted.

Verification

  • npm run build:app ✅
  • Will need user to restart their app and confirm the bottom-of-feed clutter clears.

🤖 Generated with Claude Code

…t shape

Diagnosed via debug bundle 2026-05-07T08-26-35-212-5d948ab5 that 7 stale
2-10-word fragments were parked permanently at the bottom of a Claude
session's feed:

  - Each fragment was a g-msg_*-0 ghost rendered by selectMergedEntries.
  - proxy-semantic.json history showed each one as a sub-second turn
    (188-602ms, 12-41 chars). The same bundle's real assistant turns
    were 808ms-37s, 76-6579 chars.
  - 0 of 23 flows had attribution='ignored' — the existing sidecar
    predicate in ClaudeProxyAdapter (c8c2623, body-shape based) never
    fired for these calls.

Root cause: Claude Code routes title-gen, branch-name-gen, and the new
"predict the user's next prompt" feature through the same /v1/messages
endpoint as real turns, but does NOT write them to its JSONL rollout.
ClaudeProxyAdapter sees them as live flows and emits turn_started /
text_delta / turn_completed semantic events; ghostsFromSemanticTurn
mints a ghost; reconcileUpstream never matches because there's no JSONL
entry to match against; the orphan timeout fires; mergeWithUpstream
appends the ghost to the tail of the feed forever.

The body-shape predicate misses the predict-next-prompt feature because
those calls include the full conversation history (messageCount > 3 so
the compound budget signal fails) and use a system prompt not in the
4-entry SIDECAR_SYSTEM_PROMPT_PREFIXES list.

Fixing the predicate properly requires capturing a live request body
to extend the prefix list, which we don't have here. This commit
addresses the symptom by detecting the SHAPE of leaked sidecar turns
(assistant role, single text block, ≤200 chars) at render time and
dropping them before mergeWithUpstream sees them.

Trade-off: a genuine but very brief assistant orphan (e.g. "Done.")
whose JSONL never committed would also be suppressed. Accepted because
orphans are already an error path and the user-visible permanent
clutter is the bigger UX harm.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant