Repository navigation
bug(claude-queue): idle debt settlement is only reachable from a semantic event #677
Description
Expected behavior
Open queue debt (debt / removeDebt) should reach settlement whenever a Claude session becomes idle, so a queued item that has already departed upstream stops being displayed.
Actual behavior
markStaleWhenIdle — which settles both debts and marks survivors stale — has exactly one production call site:
src/renderer/src/workspace/hook/ipc/useIpcSubscriptions.ts:1151, insidehandleSemanticEvent.
Its guard requires !current.processActive && streamPhase === 'idle' && !nextAwaitingAssistant, i.e. a semantic event that arrives while the session is already idle.
The process-state handler (feed.onSessionProcessState, ~line 886) never settles. It has an idle-queue path, but that path is shouldClearIdleQueuedMessages, which is capability-gated to optimistic-echo providers and documents that "Claude's provider-owned queue is never touched here" (src/renderer/src/session-runtime/queueInvariants.ts).
So this ordering strands debt:
- Notification
Nand promptPare queued. - A legacy content-free
removeopensremoveDebt = 1. - A carrier that applies exact evidence —
popAll(content=P), or a content-bearingremove— retiresPand deliberately leaves the debt for its own settlement. - The final semantic event of the turn arrives while
processActiveis stilltrue, so the idle guard fails. - Process state flips to inactive. That handler settles nothing.
- With no later semantic or queue event,
Nstays visible with the debt unsettled.
The same gap affects bootstrap: replaying a recorded session leaves pending items and open debt (exact-remove-after-open-dequeue-debt.json finishes replay with 3 pending and debt.count = 3), and production bootstrap receives no replayed semantic events. reconcile.test.ts calls markStaleWhenIdle manually, which is correct for a reducer test but means no test covers the live trigger.
Scope and severity
Not misattribution — no item is removed, and no departure is credited to the wrong item. The failure is a stale queue chip lingering until the next semantic event, typically the next turn. It is only indefinite when the session has no further turn.
The root is pre-existing: exact-evidence carriers that deliberately defer settlement were introduced across PR #670, and the earliest of them (content-bearing remove) shipped in that PR's first reviewed rounds. #670's final commit adds popAll to that set, so it widens an existing gap by one carrier rather than creating it.
Why it is not fixed in #670
The correct fix is at the live boundary — making idle settlement reachable from the process-state transition and from bootstrap — not by restoring inline settlement to the exact-evidence carriers. That inline settlement was the bug those carriers were fixed for: it consumed the very item the carrier named, leaving an unrelated item pending forever.
The process-state handler is one of the documented edge-site clears and is load-bearing for session lifecycle; adding reconciler settlement to it deserves its own change and its own live-boundary test, not a merge-gate patch.
Evidence
useIpcSubscriptions.ts:1151— the solemarkStaleWhenIdlecall site and its idle guard.useIpcSubscriptions.ts:~886— the process-state handler; no queue settlement for Claude.queueInvariants.ts—shouldClearIdleQueuedMessagescapability gate excluding Claude.reconcile.ts—markStaleWhenIdle,settleDebtByCohort,settleRemoveDebtByCohort.
Found by an orchestrated review of PR #670. Two independent reviewers disagreed on whether the backstop is reached; the mechanism above was verified directly against the handlers. Refs #665.
Activity
fix(claude-queue): resolve remove carriers by exact body before task id
fix(claude-queue): type the idle helper on SessionKind; read the fixt…
Juliusolsson05 commentedon Sep 27, 2026
Status (W2): #1396 now settles only at bootstrap-complete, and only when the open departure debt covers every pending item (manager decision, option B). The live process-idle half of this issue is blocked on #1402: a redelivered
dequeuedouble-counts debt, and at a live flip that could hide a genuinely queued user prompt. #1396 therefore saysRefs #677.