Repository navigation
Description
Problem
The Claude queue reconciler (src/renderer/src/session-runtime/claudeQueue/reconcile.ts) opens one unit of dequeue debt per dequeue record, and one unit of removeDebt per content-free remove. Only enqueue has a redelivery guard (see the comment above it in reconcile.ts). If the live burst channel redelivers a departure record, the debt is counted twice. Later settlement can then spend the phantom unit on an item Claude still holds, and retire a genuinely queued prompt as delivered-inferred.
Found by #1396 review a, round 2. It reproduced with a real-subscription probe: N1 is delivered, N2 is still queued, and the same dequeue is delivered again. debt = 1 then covers N2, and N2 is retired at the next settlement.
Why no key-based guard
Queue-operation records carry no uuid. In a 400-record sample from a local Claude corpus of 606 transcripts, every record had only type, operation, timestamp, sessionId, and sometimes content or reason.
The same corpus has 13,757 queue operations (6,927 enqueue, 4,435 remove, 2,391 dequeue, 4 popAll). It contains 99 genuine repeats of the same (operation, timestamp, content) key within a single file (99 dequeue repeats and 25 remove repeats). So an (operation, timestamp) guard would drop real departures.
Where it bites
- Every settlement path already over-counts under redelivery. That includes the existing semantic idle site and committed-
userinference. - fix(claude-queue): settle proven queue debt at bootstrap-complete (#677) #1396 narrowed bug(claude-queue): idle debt settlement is only reachable from a semantic event #677 to bootstrap-complete for this reason (manager decision). The live process-idle settlement bug(claude-queue): idle debt settlement is only reachable from a semantic event #677 asked for is blocked on this issue: at a live flip, a phantom unit would hide a real queued user prompt.
Directions to evaluate
- Dedupe at the transport. For example, the JSONL tailer could carry a byte offset or line number for each queue-operation record, so a redelivered record can be recognised by position rather than content.
- Measure redelivery first. Is it real in production, and on which path (tailer recovery, bootstrap replay overlapping live bursts)? Use feed-debug bundles before choosing.
Activity
test(claude-queue): pin the redelivered-dequeue bootstrap counterexam…
Juliusolsson05 commentedon Sep 27, 2026
#1396 (the bootstrap half of #677) is parked behind this issue. Its committed
it.failstest, 'does not retire a still-queued prompt at bootstrap when a dequeue is redelivered (#1402)', is the acceptance check: once departures are countable once, it must flip toitand pass.Juliusolsson05 commentedon Sep 27, 2026
Evidence stage (W2): the "redelivery" framing is wrong; a partial bootstrap window is the real mechanism
No current transport path redelivers a queue-operation record.
6b1fd7ccfrom code that ran while main emitted every JSONL entry twice: a singularsession:jsonl-entryIPC plus a coalesced burst. That dual emit was removed (see the "bootstrap-replay cascade" note that6b1fd7cc^carried inuseIpcSubscriptions.ts).packages/claude-code-headless/src/transcript/JsonlTailer.ts) is offset-based and serializes its reads. On a diverged cursor it refuses to continue (onDiscontinuity/ error) instead of re-reading. Relocation reuses the byte cursor only after verifying a 256-byte anchor.loadInitialHistoryForSession, which maps entries for the feed and never feeds them into the queue reconciler.claudeQueueBySession, so a restarted process starts a fresh reconciler.What does reach the reconciler wrongly: a partial bootstrap window. A resumed Claude session bootstraps only the last
RESUME_BOOTSTRAP_TAIL_LINESlines of the file's last 512 KiB (JsonlTailer.bootstrapTail,ClaudeCodeHeadless.ts:1332), so on resume the replay is partial by design:dequeueor content-freeremovein the window whoseenqueuefell before it opens debt whenever any in-window item is pending (applyDequeue), even though its departed item was never pending here.dequeue, but it needs no duplicate.A direction worth checking before any fix. Claude's command queue lives in the CLI process's memory (
messageQueueManager). A freshly RESUMED process therefore starts with an empty queue, and nothing replayed from the file can still be queued in it. If that holds, the resume bootstrap should treat replayed pending items as already gone, rather than inferring departures from a partial window. Needs confirming againstvendor/claude-code-src(queue persistence across--resume) and one recorded resume.Consequence for #1396: it is still correctly held. The blocker is the partial window, not redelivery.
Juliusolsson05 commentedon Sep 27, 2026
Addendum (W2): confirmed in
vendor/claude-code-src/full. Claude's command queue is process-local:utils/messageQueueManager.ts:53isconst commandQueue: QueuedCommand[] = [], andutils/sessionStorage.ts:1218only APPENDSqueue-operationrecords. Nothing reads them back on--resume. So an item whoseenqueuetimestamp is earlier than the current Claude process's start cannot be in that process's queue.Proposed fix (needs manager sign-off because it supersedes #1396): at session start and at bootstrap-complete, retire every pending item enqueued before the current process started, with a new decision reason such as
process-restarted. That is a provable departure, not an inference from debt. It needs no departure counting, so neither a partial bootstrap window nor a duplicated record can make it retire a prompt the live process still holds. It covers the bootstrap half of #677, and the live process-idle half stays out of scope.