Problem
A Claude session can have a persistent headless agent (claude -p + stdin FIFO, CCC-spawned) AND an interactive terminal (claude --resume) live at once. A live claude process runs off in-memory state loaded at resume and never re-reads the transcript from disk (verified empirically). So:
- The two processes silently diverge — each replies from its own memory, blind to the other's turns (all of which still land, append-only, in the shared
.jsonl).
- CCC routes injection to the terminal while a TTY exists, but the moment the terminal closes, routing falls back to the stale headless FIFO → it answers from frozen (possibly day-old) memory → the session effectively rolls back. This is the likely mechanism behind the reported "history rolled back ~a day" incident (the transcript itself is NOT corrupted — append-only holds; the live process is stale).
Background: scratch/session-states-and-control.md (states C1–C4, T1).
Scope: Claude only
Only Claude uses the persistent stdin-FIFO model (_make_stdin_fifo, O_RDWR keep-alive). Codex/Cursor/Gemini/Antigravity CLI are one-shot (fresh read of disk each turn); the Codex app-server divergence is a separate issue (T10).
Mechanisms (employ 4 + 2 + 5)
- (4) Use-time staleness check [correctness backbone]. Before CCC writes to a headless FIFO, compare what that headless last produced against the transcript's current tail. If the
.jsonl advanced via a non-headless writer → the headless is stale → retire it (_retire_unresponsive_spawn_entry) and spawn a fresh claude --resume (reads current disk) instead. Runs on the inject path, independent of any open dashboard — this is the only mechanism that catches a terminal CCC never witnessed (opened/closed between polls or while CCC was down).
- (2) Retire on "Launch terminal". When CCC opens a terminal for a session that has a headless spawn, SIGTERM the idle headless right there (immediate, common path).
- (5) Always-on server-side watcher (~5–10s daemon thread). Periodically: any session with a live headless + live terminal where the headless is idle → retire the headless. Cleanup so the stale process doesn't linger (~500MB) when no dashboard is open. NOTE: the existing liveStatus poll is frontend-driven (5s, only while dashboard open), so a dedicated server-side thread is needed.
Skip: (1) frontend poll (dashboard-only; (5) supersedes), (3) send+TTY (subsumed by (4)).
Hard safety rule
Never retire a BUSY headless (mid-turn / actively working) — only idle ones. Killing a working agent interrupts real work.
Acceptance
- After a terminal drives a session, the idle headless is retired (within seconds via (5)/(2), or at next inject via (4)).
- Closing the terminal then sending from CCC produces a response with current context (fresh resume reads disk), not a rollback.
- A busy headless is never retired.
- Injection path is unbroken when there's no concurrency (no regression).
🤖 Generated with Claude Code
Problem
A Claude session can have a persistent headless agent (
claude -p+ stdin FIFO, CCC-spawned) AND an interactive terminal (claude --resume) live at once. A liveclaudeprocess runs off in-memory state loaded at resume and never re-reads the transcript from disk (verified empirically). So:.jsonl).Background:
scratch/session-states-and-control.md(states C1–C4, T1).Scope: Claude only
Only Claude uses the persistent stdin-FIFO model (
_make_stdin_fifo, O_RDWR keep-alive). Codex/Cursor/Gemini/Antigravity CLI are one-shot (fresh read of disk each turn); the Codex app-server divergence is a separate issue (T10).Mechanisms (employ 4 + 2 + 5)
.jsonladvanced via a non-headless writer → the headless is stale → retire it (_retire_unresponsive_spawn_entry) and spawn a freshclaude --resume(reads current disk) instead. Runs on the inject path, independent of any open dashboard — this is the only mechanism that catches a terminal CCC never witnessed (opened/closed between polls or while CCC was down).Skip: (1) frontend poll (dashboard-only; (5) supersedes), (3) send+TTY (subsumed by (4)).
Hard safety rule
Never retire a BUSY headless (mid-turn / actively working) — only idle ones. Killing a working agent interrupts real work.
Acceptance
🤖 Generated with Claude Code