Skip to content

fix(chat): a turn that ended while its work runs on shows an hourglass and Send, not a live turn - #184

Merged
sean-brydon merged 5 commits into
cosscom:mainfrom
Stener1:chat-turn-ended-state
Oct 24, 2026
Merged

sean-brydon merged 5 commits into
cosscom:mainfrom
Stener1:chat-turn-ended-state

Conversation

@Stener1

@Stener1 Stener1 commented Oct 22, 2026

Copy link
Copy Markdown
Contributor

Summary

A finished turn that turn check read as working kept the chat in live-turn mode: the spinner's fallback word ("Making way… · 49s"), Stop, and queue mode, while the person typed their next message. The reply that triggered it reported on another session and offered a follow-up ("I can nudge it… or tell you when its turn ends").

Diagnosis from the box's journal: agent.finished (seq 82707), then turn check's agent.ready with choice: "working" 2 s later. No tool event came between the Stop and the next prompt, so it was the classifier (A). A stray tool event after Stop (B) is real too, though: the journal holds 559 tool events after a Stop. The ones followed by nothing until the next prompt come from background subagents, whose hooks carry the parent's session_id.

 turn check prompt
+  a status report on another session or worker + an offer to follow it up → done
+  waiting on another session → working only when the agent says it will act when it reports back

 Claude adapter: PostToolUse
+  agent_id set (a subagent's tool) → data.helper = true

 ledger: agent.started{signal: tool}
+  helper && no turn open
+    a permission the helper asked for after Stop → finished again (wait ends)
+    otherwise → ignored, and not published (Redundant)
   the agent's own tool with no turn open → running (unchanged: a prompt
     Claude Code dequeues itself fires no UserPromptSubmit, e.g. seq 19552)

 chat pane (sessionState() unchanged everywhere else)
+  chatState(s): onItsWork(s) ? "finished" : sessionState(s)
+  footer: on-its-work item → ⧗ "Waiting on its work" + ON_ITS_WORK tooltip, no timer
+  reply box: replyMode(state) → Send, no Stop

onItsWork and ON_ITS_WORK match #183's definitions line for line, so whichever PR merges second has an easy conflict. #183 also changes the same turn-check prompt, so whichever lands second needs to merge both prompts and re-record internal/box/testdata/turncheck (the command is below).

Evidence

Turn check, the incident turn replayed on a real box (turn-check-replay.sh from #183, exact prompt and reply):

Recorded AI Gateway answers (TestTurnCheckReplays). Each recording is tied to its exact request by a sha256, so a prompt change fails the test until it is recorded again. CI never calls the gateway.

turn origin/main prompt this prompt
other session's status + offer (the incident, names changed) working 0.94 done 0.68–0.74 over 4 runs
waits on another session, acts when it reports working 0.94 working 0.99
running deploy / background re-check — working 1.00 / 0.95
waits on a reply / scheduled check-in parked 0.68–0.75 parked 0.66–0.78 / 0.92

Recording again: BERTH_TURNCHECK_RECORD_KEY_FILE=<AI Gateway key file> go test ./internal/box -run TestTurnCheckReplays.

Helper tool events (turns_test.go):

  • TestAHelpersToolAfterStopLeavesTheTurnFinished fails on main ("a helper's tool use after the turn ended would be published") and passes here.
  • TestAHelpersPermissionAfterStopEndsWithItsTool: Stop → the helper asks permission (waiting, prompts held) → the helper's tool runs → finished and ready, with no turn made. Turn check's soft wait stays.

Chat, real Chromium on the mock (verify-berthd scenario chat-turn-state, no console errors):

finished + unfinished: ⧗ Waiting on its work, tooltip, Send, no Stop live turn: spinner word + timer, Stop, Queue
on its work live turn

Chat on a real box (scenario chat-turn-state-live: a dev box of this branch, real Claude Code, turn check on). Claude starts sleep 600 in the background and ends its turn; the box lists it finished + unfinished. Then a foreground sleep 45:

real turn, background task still running real live turn
live on its work live turn real

Asserted: the row text Waiting on its work, the tooltip = the dashboard card's, a Send button with no Queue and no Stop …; and in a live turn /…\s*· \d+s/, Stop Claude and Queue. The same is in app/e2e/chat.spec.ts.

Checks run locally (fork PR CI waits for a maintainer):

  • go vet ./... passes.
  • go test -race ./... passes except internal/leadline/api TestDetach, which fails the same way on origin/main (TestServicesAreCounted failed once under load and passed on rerun).
  • typecheck:plugins, check:titles/type/grid/csp/themes, pnpm test (578), and build.
  • Full Playwright suite: 517 of 525 pass. The 8 that fail also fail on origin/main: a11y database-review, database :227, keyboard :173, sidebar-fleet ×2, sidebar-quiet, team-setup :331, and chat-feed :55, which is flaky on both (2/25 on main, 3/25 here).
  • Codex review + adversarial review: one finding (a helper's permission after Stop would stay waiting), fixed in c8b6de0; the re-review was clean.

Merge Danger

Door: two-way

Prompt text, a ledger guard on events marked helper, and chat UI. No data written. Revert to undo.

Blast Radius: small

🤖 Generated with Claude Code

@vercel

vercel Bot commented Oct 22, 2026

Copy link
Copy Markdown

@Stener1 is attempting to deploy a commit to the seanbrydonio's projects Team on Vercel.

A member of the Team first needs to authorize it.

Stener1 and others added 5 commits October 23, 2026 22:50
…s and Send, not a live turn

Turn check read a reply that reports how another session is getting on
and offers to follow it up as working, so a finished session stayed
"running": its chat kept the live turn's spinner word, a ticking timer,
Stop and queue mode while the person typed their next message.

- Turn check's prompt: a status report on another session or worker with
  an offer is done; waiting on another session is working only when the
  agent says it will act when that session reports back. Real turns are
  replayed through recorded AI Gateway answers (testdata/turncheck), each
  tied to its exact request, so a prompt change asks for a new recording.
- Claude Code marks a subagent's tool hooks with agent_id. The adapter
  passes that on, and the ledger ignores a helper's tool use while no
  turn is open, so a helper left running in the background no longer
  puts a finished session back to running. The agent's own tool use with
  no turn open (a prompt Claude Code took from its queue) still starts one.
- The chat draws a session on its work (finished + unfinished) as between
  turns: an hourglass row, "Waiting on its work" with the dashboard
  card's tooltip, Send, no Stop. sessionState() is unchanged elsewhere.
- verify-berthd: chat-turn-state feature and scenario.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ed for

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… notes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… shows the hourglass, a live turn Stop

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Main's handoff wording (cosscom#183) changed the request these six replays were
recorded for. Recorded again against the real provider; every verdict is
the one it was.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Stener1
Stener1 force-pushed the chat-turn-ended-state branch from de46a27 to d30dbd5 Compare October 23, 2026 23:06
@sean-brydon
sean-brydon merged commit 6805e8f into cosscom:main Oct 24, 2026
1 of 3 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.

2 participants