Skip to content

bug(claude-queue): idle debt settlement is only reachable from a semantic event #677

Open

Description

@Juliusolsson05

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, inside handleSemanticEvent.

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:

  1. Notification N and prompt P are queued.
  2. A legacy content-free remove opens removeDebt = 1.
  3. A carrier that applies exact evidence — popAll(content=P), or a content-bearing remove — retires P and deliberately leaves the debt for its own settlement.
  4. The final semantic event of the turn arrives while processActive is still true, so the idle guard fails.
  5. Process state flips to inactive. That handler settles nothing.
  6. With no later semantic or queue event, N stays 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 sole markStaleWhenIdle call site and its idle guard.
  • useIpcSubscriptions.ts:~886 — the process-state handler; no queue settlement for Claude.
  • queueInvariants.ts — shouldClearIdleQueuedMessages capability 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

  1. Juliusolsson05 commented on Sep 27, 2026

    @Juliusolsson05
    OwnerAuthor

    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 dequeue double-counts debt, and at a live flip that could hide a genuinely queued user prompt. #1396 therefore says Refs #677.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions