fix: a handoff to a worker reads as done, and work an agent waits on shows an hourglass, not a live turn's spinner
Summary
A session whose agent handed its work to a separate worker ("I've started a worker to fix it, so you can close this session… I turned off its notifications to this session") kept showing as working. Two causes:
- Turn check read "a delegated task" as working even when this agent won't follow it.
- Sidebar and tab drew a turn-check "working" verdict (
agent_state: finished+unfinished) with the same spinner as a live turn. Only the dashboard card said "Waiting on its work".
working: something runs now that the agent waits on or acts on when it reports back:
- the agent itself, a delegated task, a build, …
+ the agent itself, a delegated task it waits on or will act on when it reports back, a build, …
+ A task handed to a separate worker or session that this agent won't follow is a handoff:
+ "you can close this session", "followed elsewhere", "turned off its notifications to this
+ session" → done, even while the worker still runs.
sessionState(s) // unchanged: still "running" for counts, the board, grouping
+onItsWork(s) = finished && unfinished
+allOnItsWork = every "running" session in a row is onItsWork
<TaskGlyph state onWork> // sidebar rows, folded headers and forks, Labs columns
<StateGlyph state session noTip> // tabs, a pane's header, the rail's card
- running → spinner
+ running && onItsWork → hourglass (WorkGlyph), with "Its turn ended while its work runs on…"
+ in its tip, or in the tab's / pane's own tooltip (noTip)
A new turn clears unfinished on the box (turns.go), so the spinner comes back while it runs.
Evidence
Turn check on the real model (TestTurnCheckLive, new, skipped unless BERTH_TURNCHECK_LIVE_KEY_FILE and BERTH_TURNCHECK_LIVE_MODEL are set; CI never calls a provider). Same fixtures, typesafe-ai/jev on AI Gateway:
| Turn | Before (main's prompt), 3 runs | After, 5 runs |
|---|---|---|
| handoff to a worker it won't follow | working 0.76 / 0.77 / 0.80 ❌ | done 0.78–0.85 ✅ |
| delegated task it waits on | working 1.00 | working 0.99–1.00 |
| deploy running | working 1.00 | working 1.00 |
| background re-check | working 0.92–0.95 | working 0.95–0.97 |
| check-in scheduled for tomorrow | parked 0.88–0.92 | parked 0.85–0.90 |
On a real box, the real turn (.claude/skills/verify-berthd/scripts/turn-check-replay.sh, new: a throwaway location, a stand-in claude session, the turn as its transcript, the UserPromptSubmit and Stop hooks through berthd hook). Before, on the installed v0.3.30 build, 2 of 2 runs:
replay-cp9-handoff agent_state=finished unfinished=True parked=False ← shows as working
replay-delegated-waiting agent_state=finished unfinished=True parked=False
The after run on a build with this branch is in a comment below.
App (drive.mjs turn-check-indicator, mock fixtures, no console errors; e2e/sidebar-turn-check.spec.ts asserts the same):
| Sidebar: hourglass and its tip (order-export), spinner for a live turn (qa-deck) | Tab: hourglass, tooltip says why | Live turn's tab keeps the spinner |
|---|---|---|
![]() | ![]() | ![]() |
Before, the sidebar's order-export row and its tab drew the blue spinner.
Checks (local; fork PRs need a maintainer for CI): go vet ./... ✅; go test -race ./... ✅ except internal/leadline/api TestDetach, which failed once under the full suite's load and passes 5/5 alone (a package this PR doesn't touch). pnpm typecheck:plugins, check:titles, check:type, check:grid, check:csp, check:themes, test (incl. new derive.test.ts cases), build, tsc -p e2e ✅. e2e/sidebar-turn-check.spec.ts + e2e/dashboard-turn-check.spec.ts ✅. Docs build ✅. Codex review and adversarial review: no findings.
Merge Danger
Door: two-way
Prompt text and a UI indicator; no data or schema. Revert to undo.
Blast Radius: classification
Every box's turn check uses the new wording, so a borderline delegated turn could now read as done (it then shows as done instead of working; nothing is lost and no prompt is held). The live replay keeps a delegated-and-waiting turn at working 0.99–1.00. In the app, sessionState() is unchanged, so counts, the dashboard's columns and notifications are untouched; only the glyph changes.
🤖 Generated with Claude Code


