Skip to content

fix: reconcile stale session statuses on boot; name the tool blocking a send - #305

Merged
saucam merged 1 commit into
mainfrom
fix/stale-status-and-approval-affordance
Aug 26, 2026
Merged

fix: reconcile stale session statuses on boot; name the tool blocking a send#305
saucam merged 1 commit into
mainfrom
fix/stale-status-and-approval-affordance

Conversation

@saucam

@saucam saucam commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator

Two fixes for the same confusing experience: returning to a session and being told it's busy or blocked, with nothing to act on.

1. Non-terminal statuses survived daemon restarts

thinking / tool_running / waiting_approval all describe work owned by a live process — the provider's turn loop, and for approvals the in-memory #pendingApprovals resolvers. None of it survives a restart, and a resumed Session starts idle (#status is initialised, never restored from the row).

The row was never corrected, so the session list advertised work that no longer existed. Observed on a live daemon that had restarted many times:

waiting_approval  mpc-policy-eval-paper   Aug 9   (16 days)
tool_running      studio                  Aug 6   (19 days)
thinking          shield_and_policy       Aug 14  (11 days)

Store.reconcileStaleSessionStatuses() clears them at boot, before resumeSessions, so rows and live sessions agree from the first broadcast.

error is deliberately preserved — a terminal state a human may still want to see, not an artifact of the process dying.

2. The mid-turn send rejection named nothing

Sending while an approval is pending correctly throws — falling through would close the live turn queue and auto-deny the approval. But the message only said:

A tool approval is pending — approve or deny it before sending

which tells you something is waiting, not what, and gives you nothing to act on. After stepping away from a session it reads as the model having simply gone silent. Now:

The tool Bash is waiting for approval — approve or deny it before sending (this backend can't queue mid-turn). Your message was saved; send again after deciding.

Names come from the same maps the approval bar already uses (#approvalIdToMessageId#toolCallMessages), so there's no new bookkeeping to drift. It falls back to the original phrasing when no name resolves: the status is authoritative, the label is best-effort, and a missing name must never turn a clear refusal into a crash.

Scope note

This does not change when the guard fires — that behaviour is correct and unchanged. It only makes the refusal legible. Whether a pending approval should also be re-surfaced to the sending client (so it can be answered in place rather than requiring the user to find it) is a real follow-up, but it's a protocol/UI change rather than a message fix, so it's deliberately not bundled here.

Verification

lint + typecheck clean, 2382 pass / 0 fail. Four new reconcile tests: all three non-terminal statuses cleared, error preserved, idle rows untouched (reports zero), and idempotence across a second boot. The two existing tests asserting on the rejection text still pass — the fallback keeps them honest.

🤖 Generated with Claude Code

… a send

Two fixes for the same confusing experience — returning to a session and being
told it is busy or blocked, with nothing to act on.

1. Non-terminal statuses survived daemon restarts.

`thinking` / `tool_running` / `waiting_approval` all describe work owned by a
LIVE process: the provider's turn loop, and for approvals the in-memory
`#pendingApprovals` resolvers. None of it survives a restart, and a resumed
Session starts `idle` (`#status` is initialised, never restored from the row).
The row was never corrected, so the session list advertised work that no longer
existed. Observed on a live daemon: rows stuck in `tool_running` and
`waiting_approval` for 11-19 days across many restarts.

`Store.reconcileStaleSessionStatuses()` clears them at boot, before
resumeSessions, so rows and live sessions agree from the first broadcast.
`error` is deliberately preserved — a terminal state a human may still want to
see, not an artifact of the process dying.

2. The mid-turn send rejection named nothing.

Sending while an approval is pending correctly throws (falling through would
close the live turn queue and auto-deny the approval), but the message only
said "A tool approval is pending" — which tells the user something is waiting,
not what, and offers no way to act. After stepping away it reads as the session
having gone silent.

It now names the tool, via the same maps the approval bar already uses
(`#approvalIdToMessageId` -> `#toolCallMessages`), so there is no extra
bookkeeping to keep in sync, and states plainly that the message was saved.
Falls back to the old phrasing when no name resolves: the status is
authoritative, the label is best-effort, and a missing name must never turn a
clear refusal into a crash.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@saucam
saucam merged commit fb60509 into main Aug 26, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants