diff --git a/docs/plans/2026-09-27-claude-queue-idle-settlement.md b/docs/plans/2026-09-27-claude-queue-idle-settlement.md
new file mode 100644
index 000000000..c4c259d45
--- /dev/null
+++ b/docs/plans/2026-09-27-claude-queue-idle-settlement.md
@@ -0,0 +1,47 @@
+# Claude queue idle settlement (#677)
+
+## Problem
+`markStaleWhenIdle` is how Claude queue debt settles (`settleDebtByCohort` + `settleRemoveDebtByCohort`) and how unattributed survivors are marked stale. It had one live call site: a semantic event arriving while the session was already idle.
+
+Exact-evidence carriers (a content-bearing `remove`, `popAll(content)`) deliberately leave debt for that settlement. Two orderings therefore stranded a queue chip for an item that had already left:
+
+1. **Live:** the turn's last semantic event lands while `processActive` is still true, and the process then goes idle. The process-state handler never settled.
+2. **Bootstrap:** production replay delivers no semantic events. The recorded `exact-remove-after-open-dequeue-debt` run ends with 3 pending items and `debt.count = 3`.
+
+## Rule
+Every transition INTO idle asks the same question through one helper, `settleClaudeQueueIfIdle` in `useIpcSubscriptions.ts`:
+
+| Site | Idle means |
+|---|---|
+| semantic event (existing) | `!processActive && streamPhase === 'idle' && !awaitingAssistant` |
+| process-state flip | `!active && streamPhase === 'idle'` (the handler clears `awaitingAssistant` itself) |
+| bootstrap-complete | no live signal (`!processActive && streamPhase === 'idle'`). `awaitingAssistant` is excluded because queue-op replay forces it true. |
+
+Each site commits the result to `claudeQueueBySession` after `setRuntimes` returns, the same rule as the other queue commits.
+
+**Not done:** restoring inline settlement in the carriers. That was the #670 bug: it consumed the item the carrier named.
+
+## Tests
+`claudeQueueIdleSettlement.renderer.test.tsx` drives the real subscriptions through the fake feed:
+
+- the issue's exact carrier sequence, settled on the process-idle flip;
+- no settlement while the process is still active;
+- the process-idle settlement is committed (a later `user` entry must not bring the item back);
+- the recorded fixture replay, settled at bootstrap-complete and committed;
+- no bootstrap settlement while the process is live.
+
+**Fail-first:** 2 fail on origin/main.
+
+**Mutations killed:**
+- no process settle;
+- no process commit;
+- no bootstrap settle;
+- no bootstrap commit;
+- bootstrap ignores the live signal;
+- process path ignores `active`.
+
+## Narrowed (manager decision, option B, after review round 2)
+- **Process-state flip:** does NOT settle. A redelivered `dequeue` (queue-operation records carry no uuid) can make `debt >= pending` cover a genuinely queued prompt, and at a live flip that would hide a real queued user prompt.
+- **Bootstrap-complete:** settles only when the open debt covers EVERY pending item, stale ones included (`debtCoversPending`). The recorded fixture proves this case.
+- **Semantic site:** unchanged.
+- **Scope:** the live-idle half of #677 waits on the redelivery follow-up issue, so this PR says `Refs #677`, not `Fixes`.
diff --git a/src/renderer/src/workspace/hook/ipc/claudeQueueIdleSettlement.renderer.test.tsx b/src/renderer/src/workspace/hook/ipc/claudeQueueIdleSettlement.renderer.test.tsx
new file mode 100644
index 000000000..b423f2cde
--- /dev/null
+++ b/src/renderer/src/workspace/hook/ipc/claudeQueueIdleSettlement.renderer.test.tsx
@@ -0,0 +1,249 @@
+import { readFileSync } from 'node:fs'
+import { dirname, join } from 'node:path'
+import { fileURLToPath } from 'node:url'
+import { afterEach, expect, it, vi } from 'vitest'
+import { render } from '@testing-library/react'
+import { act, useRef } from 'react'
+
+import { createFakeSessionFeed } from '@renderer/features/sessionFeed/FakeSessionFeed'
+import { emptyRuntime } from '@renderer/session-runtime/state'
+import type { SessionRuntime } from '@renderer/session-runtime/state'
+import type { SessionId, WorkspaceState } from '@renderer/workspace/types'
+import type { WorkspaceRefs } from '@renderer/workspace/hook/refs'
+
+import { useIpcSubscriptions } from './useIpcSubscriptions'
+import { makeWorkspaceRefsForTest as makeRefs } from './testing/workspaceRefsForTest'
+
+// #677: Claude queue debt settles only in markStaleWhenIdle, and its one live
+// call site was a semantic event that arrives while the session is ALREADY
+// idle. When the turn's last semantic event lands while the process is still
+// active and the process then goes idle, nothing settled: a queue chip for an
+// item that already left stayed on screen until the next turn, or forever for
+// a session with no next turn. These drive the real subscriptions with the
+// issue's exact carrier sequence.
+const originalWindowApi = window.api
+afterEach(() => {
+ vi.useRealTimers()
+ Object.defineProperty(window, 'api', { configurable: true, value: originalWindowApi })
+})
+
+// Read, not imported: the fixture sits outside the web tsconfig's file list.
+const fixture = JSON.parse(readFileSync(
+ join(dirname(fileURLToPath(import.meta.url)), '../../../../../../testing/fixtures/queue-operations/exact-remove-after-open-dequeue-debt.json'),
+ 'utf8',
+)) as { events: Array<{ op: string; content?: string; timestamp: string }> }
+
+const NOTIFICATION = '\nt-1\ncompleted\n'
+const PROMPT = 'queued prompt'
+
+const op = (uuid: string, operation: string, content: string | undefined, second: number) => ({
+ file: '/s/claude.jsonl',
+ entry: {
+ type: 'queue-operation', uuid, operation, ...(content === undefined ? {} : { content }),
+ timestamp: `2026-09-27T00:00:${String(second).padStart(2, '0')}.000Z`,
+ } as never,
+})
+
+let mounts = 0
+function mount(initial: Partial = {}) {
+ vi.useFakeTimers()
+ const fake = createFakeSessionFeed()
+ // A fresh id per test: the reconciler's per-session state is module-level
+ // (claudeQueueBySession), so a shared id would carry one test's queue into
+ // the next.
+ const sessionId = `queue-idle-${++mounts}` as SessionId
+ let workspaceState = { sessions: { [sessionId]: { cwd: '/repo', kind: 'claude' } } } as unknown as WorkspaceState
+ let runtimes: Record = { [sessionId]: { ...emptyRuntime(), ...initial } }
+ let refsForTest!: WorkspaceRefs
+ const commitRuntimes = (updater: Record | ((current: Record) => Record)): void => {
+ runtimes = typeof updater === 'function' ? updater(runtimes) : updater
+ refsForTest.latestRuntimesRef.current = runtimes
+ }
+ function Harness(): React.JSX.Element {
+ const refs = useRef(null)
+ if (refs.current === null) {
+ refs.current = makeRefs(workspaceState)
+ refs.current.latestRuntimesRef.current = runtimes
+ refsForTest = refs.current
+ }
+ useIpcSubscriptions(fake, refs.current, updater => {
+ workspaceState = typeof updater === 'function' ? updater(workspaceState) : updater
+ refs.current!.stateRef.current = workspaceState
+ refs.current!.latestStateRef.current = workspaceState
+ }, commitRuntimes, () => {}, () => {})
+ return
+ }
+ Object.defineProperty(window, 'api', { configurable: true, value: { gitWorktrees: vi.fn(async () => ({ ok: false })) } })
+ render()
+ return { fake, sessionId, runtime: () => runtimes[sessionId]! }
+}
+
+/** Steps 1-3 of the issue: N and P queued, a content-free remove opens
+ * removeDebt, and an exact remove retires P while leaving that debt open. */
+function strandDebt(fake: ReturnType, sessionId: SessionId) {
+ act(() => {
+ fake.emitJsonlEntries({ sessionId, entries: [
+ op('q1', 'enqueue', NOTIFICATION, 1),
+ op('q2', 'enqueue', PROMPT, 2),
+ op('q3', 'remove', undefined, 3),
+ op('q4', 'remove', PROMPT, 4),
+ ] })
+ })
+}
+
+const visible = (runtime: SessionRuntime) =>
+ runtime.queuedMessages.filter(item => !(item as { stale?: boolean }).stale).map(item => item.content)
+
+// Manager decision (option B, #1396 round 2): the process-idle flip does NOT
+// settle. A redelivered `dequeue` can make the debt cover a genuinely queued
+// prompt, and at a live flip that would hide a real queued user prompt. This
+// pins the removed site: with the issue's exact sequence, the chip stays until
+// a semantic idle event or bootstrap-complete settles it.
+it('does not settle at a process-idle flip (removed site)', () => {
+ const { fake, sessionId, runtime } = mount()
+ act(() => { fake.emitProcessState({ sessionId, active: true, status: 'Working' }) })
+ strandDebt(fake, sessionId)
+ act(() => { fake.emitSemantic({ sessionId, event: { type: 'turn_completed', ts: Date.now() } as never }) })
+ act(() => { vi.advanceTimersByTime(50) })
+ act(() => { fake.emitProcessState({ sessionId, active: false }) })
+ expect(visible(runtime())).toEqual([NOTIFICATION])
+})
+
+it('does not settle while the process is still active', () => {
+ const { fake, sessionId, runtime } = mount()
+ act(() => { fake.emitProcessState({ sessionId, active: true, status: 'Working' }) })
+ strandDebt(fake, sessionId)
+ act(() => { fake.emitProcessState({ sessionId, active: true, status: 'Still working' }) })
+ expect(visible(runtime())).toEqual([NOTIFICATION])
+})
+
+// Bootstrap: production replay delivers no semantic events, so the recorded
+// run that ends with 3 pending items and debt.count = 3 (reconcile.test.ts
+// pins those numbers on the reducer) kept all three chips after replay.
+it('settles the recorded open-debt replay at bootstrap-complete', () => {
+ const { fake, sessionId, runtime } = mount({ bootstrapping: true })
+ act(() => {
+ fake.emitJsonlEntries({
+ sessionId,
+ entries: fixture.events.map((event, index) => op(`r${index}`, event.op, event.content, 0)).map((entry, index) => ({
+ ...entry, entry: { ...(entry.entry as object), timestamp: fixture.events[index]!.timestamp } as never,
+ })),
+ })
+ })
+ expect(runtime().queuedMessages).toHaveLength(3)
+ act(() => { vi.advanceTimersByTime(1_000) })
+ expect(runtime().bootstrapping).toBe(false)
+ expect(visible(runtime())).toEqual([])
+
+ // Committed, not only painted (see the process-idle commit test).
+ act(() => {
+ fake.emitJsonlEntries({ sessionId, entries: [{
+ file: '/s/claude.jsonl',
+ entry: { type: 'user', uuid: 'u-after-replay', message: { role: 'user', content: 'a later prompt' }, timestamp: '2026-09-27T00:00:09.000Z' } as never,
+ }] })
+ })
+ expect(visible(runtime())).toEqual([])
+})
+
+it('leaves the queue alone at bootstrap-complete while the process is live', () => {
+ const { fake, sessionId, runtime } = mount({ bootstrapping: true, processActive: true })
+ strandDebt(fake, sessionId)
+ act(() => { vi.advanceTimersByTime(1_000) })
+ expect(visible(runtime())).toEqual([NOTIFICATION])
+})
+
+// #1396 review a: idleness is not proof the queue drained. Between turns the
+// spinner can read idle while Claude still holds N2, and a resumed pane's
+// first replay-quiet tick starts from idle defaults. With no open debt to
+// account for N2, neither the process-idle flip (which no longer settles) nor
+// bootstrap-complete may touch it.
+const FIRST = 'first queued prompt'
+const SECOND = 'second queued prompt'
+function deliverFirstKeepSecond(fake: ReturnType, sessionId: SessionId) {
+ act(() => {
+ fake.emitJsonlEntries({ sessionId, entries: [
+ op('k1', 'enqueue', FIRST, 1),
+ op('k2', 'enqueue', SECOND, 2),
+ op('k3', 'dequeue', undefined, 3),
+ { file: '/s/claude.jsonl', entry: { type: 'user', uuid: 'k-user', message: { role: 'user', content: FIRST }, timestamp: '2026-09-27T00:00:04.000Z' } as never },
+ ] })
+ })
+}
+
+it('leaves a genuinely queued item live when the process goes idle between turns', () => {
+ const { fake, sessionId, runtime } = mount()
+ act(() => { fake.emitProcessState({ sessionId, active: true, status: 'Working' }) })
+ deliverFirstKeepSecond(fake, sessionId)
+ expect(visible(runtime())).toEqual([SECOND])
+ act(() => { fake.emitProcessState({ sessionId, active: false }) })
+ expect(visible(runtime())).toEqual([SECOND])
+})
+
+it('leaves a genuinely queued item live at bootstrap-complete', () => {
+ const { fake, sessionId, runtime } = mount({ bootstrapping: true })
+ deliverFirstKeepSecond(fake, sessionId)
+ act(() => { vi.advanceTimersByTime(1_000) })
+ expect(runtime().bootstrapping).toBe(false)
+ expect(visible(runtime())).toEqual([SECOND])
+})
+
+// The stream-phase half of the bootstrap guard: a quiet replay while the
+// semantic stream is still responding is not idle, even with the debt
+// covering everything.
+it('does not settle at bootstrap-complete while the stream is still responding', () => {
+ const { fake, sessionId, runtime } = mount({ bootstrapping: true, streamPhase: 'responding' })
+ strandDebt(fake, sessionId)
+ act(() => { vi.advanceTimersByTime(1_000) })
+ expect(visible(runtime())).toEqual([NOTIFICATION])
+})
+
+// #1396 review a, round 2: stale items stay inference candidates, so they
+// must count against the debt too. S is stale-marked by the semantic idle
+// site while still queued; Claude then really dequeues S (one debt unit) while
+// N stays queued. Counting only non-stale items saw debt 1 >= live 1 and
+// settled, which consumed S by cohort and stale-marked the live N.
+it('counts stale items when deciding whether the debt covers the queue', () => {
+ const { fake, sessionId, runtime } = mount({ bootstrapping: true })
+ act(() => {
+ fake.emitJsonlEntries({ sessionId, entries: [
+ op('s1', 'enqueue', 'A', 1),
+ op('s2', 'enqueue', 'S', 2),
+ op('s3', 'dequeue', undefined, 3),
+ { file: '/s/claude.jsonl', entry: { type: 'user', uuid: 's-user-a', message: { role: 'user', content: 'A' }, timestamp: '2026-09-27T00:00:04.000Z' } as never },
+ ] })
+ })
+ // The existing semantic idle site marks S stale.
+ act(() => { fake.emitSemantic({ sessionId, event: { type: 'turn_completed', ts: Date.now() } as never }) })
+ act(() => { vi.advanceTimersByTime(50) })
+ expect(runtime().queuedMessages.map(item => [item.content, Boolean((item as { stale?: boolean }).stale)])).toEqual([['S', true]])
+ act(() => {
+ fake.emitJsonlEntries({ sessionId, entries: [op('s4', 'enqueue', 'N', 5), op('s5', 'dequeue', undefined, 6)] })
+ })
+ act(() => { vi.advanceTimersByTime(1_000) })
+ expect(runtime().bootstrapping).toBe(false)
+ expect(visible(runtime())).toContain('N')
+})
+
+// q101 (review a, round 2): the same `dequeue` record delivered twice (queue
+// operations have no uuid, #1402) opens a phantom debt unit, and bootstrap
+// spends it on N2, a prompt Claude still holds. KNOWN FAILURE, pinned with
+// it.fails: nothing observable at bootstrap tells this apart from the
+// recorded fixture (both are `debt >= pending`), so #1396 is parked behind
+// #1402. When #1402 makes departures countable once, flip this to `it`; it
+// must then pass before #1396 can merge.
+it.fails('does not retire a still-queued prompt at bootstrap when a dequeue is redelivered (#1402)', () => {
+ const { fake, sessionId, runtime } = mount({ bootstrapping: true })
+ act(() => {
+ fake.emitJsonlEntries({ sessionId, entries: [
+ op('r1', 'enqueue', FIRST, 1),
+ op('r2', 'enqueue', SECOND, 2),
+ op('r3', 'dequeue', undefined, 3),
+ { file: '/s/claude.jsonl', entry: { type: 'user', uuid: 'r-user', message: { role: 'user', content: FIRST }, timestamp: '2026-09-27T00:00:04.000Z' } as never },
+ ] })
+ })
+ // The redelivery: the identical dequeue record again.
+ act(() => { fake.emitJsonlEntries({ sessionId, entries: [op('r3', 'dequeue', undefined, 3)] }) })
+ act(() => { vi.advanceTimersByTime(1_000) })
+ expect(runtime().bootstrapping).toBe(false)
+ expect(visible(runtime())).toEqual([SECOND])
+})
diff --git a/src/renderer/src/workspace/hook/ipc/useIpcSubscriptions.ts b/src/renderer/src/workspace/hook/ipc/useIpcSubscriptions.ts
index 7c48f18fa..5c23f0dc9 100644
--- a/src/renderer/src/workspace/hook/ipc/useIpcSubscriptions.ts
+++ b/src/renderer/src/workspace/hook/ipc/useIpcSubscriptions.ts
@@ -5,7 +5,7 @@ import {
DEFAULT_PROVIDER,
isAgentProviderKind,
} from '@shared/types/providerKind'
-import type { AgentProviderKind } from '@shared/types/providerKind'
+import type { AgentProviderKind, SessionKind } from '@shared/types/providerKind'
import { useEffect } from 'react'
import type { Entry } from '@shared/types/transcript'
@@ -178,6 +178,88 @@ function claudeUserEntryText(raw: unknown): string | null {
// once per burst.
const claudeQueueBySession = new Map()
+/**
+ * The idle settlement of a Claude session's queue, or null when nothing
+ * changed (#677). Callers commit a non-null result to claudeQueueBySession
+ * AFTER setRuntimes returns (the rule above) and paint `result.pending`.
+ *
+ * WHY one helper with three callers: settlement (settle both debts, then mark
+ * unattributed survivors stale) is how a queue item that already departed
+ * stops being displayed, and exact-evidence carriers (content-bearing
+ * `remove`, `popAll(content)`) deliberately leave their debt for it. It used
+ * to be reachable only from a semantic event that arrived while the session
+ * was already idle. When the turn's last semantic event lands while the
+ * process is still active and the process then goes idle, or a bootstrap
+ * replay ends with debt open (production bootstrap gets no replayed semantic
+ * events), nothing settled and the chip stayed until the next turn, or
+ * forever for a session with no next turn. The semantic event (existing
+ * guard) and bootstrap-complete now ask this; bootstrap also requires
+ * `debt-covers-pending` (see debtCoversPending), because a quiet replay alone
+ * is not proof the queue drained.
+ *
+ * WHY NOT the process-state flip (manager decision, option B, on #1396's
+ * round-2 review): a REDELIVERED `dequeue` makes `debt >= pending` cover a
+ * genuinely queued prompt, and at a live process-idle flip that would hide a
+ * real queued user prompt. We do not trade a hidden user prompt for tidier
+ * state. Bootstrap is kept because the recorded fixture proves that case.
+ * The live-idle half of #677 waits on the redelivery follow-up.
+ *
+ * WHY not settle inline in the carriers instead: that inline settlement was
+ * the bug those carriers were fixed for. It consumed the very item the
+ * carrier named and left an unrelated item pending forever (#670).
+ *
+ * Only an EXISTING reconciler state is settled. A session with no map entry
+ * has no queue, and defaulting to a fresh state compared unequal on every idle
+ * tick (the no-op bail regression noted at the semantic call site).
+ */
+function settleClaudeQueueIfIdle(
+ sessionId: SessionId,
+ sessionKind: SessionKind | undefined,
+ idle: boolean,
+ proof: 'idle' | 'debt-covers-pending',
+): ClaudeQueueState | null {
+ if (sessionKind !== 'claude' || !idle) return null
+ const existing = claudeQueueBySession.get(sessionId)
+ if (existing === undefined) return null
+ if (proof === 'debt-covers-pending' && !debtCoversPending(existing)) return null
+ const marked = markStaleWhenIdle(existing, true)
+ // markStaleWhenIdle is reference-stable, so identity is the change test.
+ return marked === existing ? null : marked
+}
+
+/**
+ * True when the open departure debt accounts for EVERY pending item
+ * (#1396 review a). Each unit of debt is a departure Claude logged (a
+ * `dequeue`, or a content-free `remove`) whose item was not identified. When
+ * those departures number at least the pending items, every one of them has
+ * left, PROVIDED each logged departure is counted once.
+ *
+ * RESIDUAL (review a, round 2): queue-operation records carry no uuid, so a
+ * REDELIVERED `dequeue` (the burst channel can redeliver, see the enqueue
+ * guard in reconcile.ts) is counted twice and can make the debt cover a
+ * genuinely queued item. A key-based guard is not possible: the local Claude
+ * corpus has 99 genuine same-millisecond, same-content `dequeue` repeats
+ * within one file (2,391 dequeues across 606 transcripts), which a
+ * (operation, timestamp) guard would wrongly drop. Redelivery already
+ * over-counts debt for every reducer path; this site inherits it.
+ *
+ * WHY bootstrap-complete needs this proof and the semantic site does not get
+ * it added: the quiet replay timer is not proof that Claude's queue is empty.
+ * A resumed pane's first 150 ms after replay starts from emptyRuntime's idle
+ * values, and settling there without the proof marked a genuinely queued N2
+ * `stale-unattributed`, which nothing un-stales. The semantic site keeps its
+ * existing guard and behaviour; this PR does not widen it.
+ */
+function debtCoversPending(state: ClaudeQueueState): boolean {
+ // EVERY pending item, stale ones included (#1396 review a, round 2): the
+ // reconciler settles debt against all of `pending`, so a stale item is
+ // still a candidate the debt can be spent on. Counting only live items let
+ // one genuine departure of a stale S look like proof that the live N had
+ // left, and settling then stale-marked N.
+ const debt = (state.debt?.count ?? 0) + (state.removeDebt?.count ?? 0)
+ return debt >= state.pending.length
+}
+
const codexCurrentTurnIdBySession = new Map()
const jsonlProviderStreamBySession = new Map()
@@ -1408,18 +1490,14 @@ export function useIpcSubscriptions(
// created state and compared it against `undefined` — which read as
// "changed" on every idle event, wiping queuedMessages to the empty
// pending list and defeating the no-op bail below.
- const existingQueue = claudeQueueBySession.get(sessionId)
- const claudeQueueIdle =
- sessionKind === 'claude' &&
- existingQueue !== undefined &&
- !current.processActive &&
- streamPhase === 'idle' &&
- !nextAwaitingAssistant
- const markedQueue =
- claudeQueueIdle && existingQueue ? markStaleWhenIdle(existingQueue, true) : null
- // markStaleWhenIdle is reference-stable, so identity is the change test.
- const staleChanged = markedQueue !== null && markedQueue !== existingQueue
- if (staleChanged) pendingStaleQueue = markedQueue
+ const markedQueue = settleClaudeQueueIfIdle(
+ sessionId,
+ sessionKind,
+ !current.processActive && streamPhase === 'idle' && !nextAwaitingAssistant,
+ 'idle',
+ )
+ const staleChanged = markedQueue !== null
+ pendingStaleQueue = markedQueue
// A completed turn is the one unambiguous proof that the limit episode
// is over: the provider answered. `turn_stopped` deliberately does NOT
@@ -2664,6 +2742,8 @@ export function useIpcSubscriptions(
if (existing) clearTimeout(existing)
const timer = setTimeout(() => {
refs.bootstrapTimersRef.current.delete(sessionId)
+ // Captured by the updater, committed after setRuntimes returns (#677).
+ let pendingIdleQueue: ClaudeQueueState | null = null
setRuntimes(prev => {
const current = prev[sessionId]
if (!current || !current.bootstrapping) return prev
@@ -2723,6 +2803,21 @@ export function useIpcSubscriptions(
let next = current
const reconciled: string[] = []
+ // #677: replay can end with Claude queue debt open (the recorded
+ // exact-remove-after-open-dequeue-debt sequence finishes with 3
+ // pending and debt.count = 3), and production bootstrap receives no
+ // replayed semantic events to settle it. awaitingAssistant is NOT
+ // part of idleness here: queue-op replay itself forces it true
+ // (case 1 above), so requiring it false would never settle. Runs
+ // before the awaitingAssistant clear so a queue that settlement
+ // empties lets that clear follow.
+ const idleQueue = settleClaudeQueueIfIdle(sessionId, sessionKind, !hasLiveSignal, 'debt-covers-pending')
+ pendingIdleQueue = idleQueue
+ if (idleQueue !== null) {
+ next = { ...next, queuedMessages: idleQueue.pending }
+ reconciled.push('claudeQueue')
+ }
+
if (
!hasLiveSignal &&
next.awaitingAssistant &&
@@ -2816,6 +2911,7 @@ export function useIpcSubscriptions(
),
}
})
+ if (pendingIdleQueue !== null) claudeQueueBySession.set(sessionId, pendingIdleQueue)
}, 150)
refs.bootstrapTimersRef.current.set(sessionId, timer)
closeSpan({