Chat: the Interrupt button does nothing #38
Labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Cloonar/coding-lab#38
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
In the embedded chat (
/runs/:id), while the agent is working the Send buttonmorphs into the pulsing Interrupt button. Clicking it has no visible effect
— the agent keeps going and nothing appears to happen.
Reproduction
/runs/:id.Expected: the current turn is interrupted (the agent stops).
Notes for triage
The interrupt call path is fully wired and covered by passing tests, so this is
likely a runtime / version-coupling problem rather than a broken path:
web/src/routes/RunChat.tsx:901-923(onClick :913),action
createInterruptatRunChat.tsx:754-769; the Interrupt-vs-Send gateis
working = () => state === 'working'atRunChat.tsx:793.interruptRun→POST /runs/:id/interrupt(web/src/api.ts:534-536).handleRunInterrupt(internal/httpapi/chat.go:198-209) →chat.Interrupt(internal/chat/chat.go:324-333) →Provider.Interrupt(
internal/provider/claudecode/interact.go:62-67) →SendNamedKeys(…, "Escape")→
tmux send-keys -t <session> -- Escape(internal/tmuxx/tmuxx.go:294-303).returns 204 but the Escape has no downstream effect.
Leading suspects to confirm:
not test-covered (
internal/compat/compat.md:346-357). If the installedClaude Code changed its interrupt gesture (e.g. double-Escape, or Escape now
only clears the composer line), the lone
Escapeatinteract.go:63is ano-op.
workinggate — the working state comes from the transcript-tailderiveStateon a ~1s poll; a stale tail can keep the button pulsing afterthe agent has already idled, so the Escape lands on an idle prompt.
the claude pane isn't the active pane, it is silently dropped.
The
working/deriveStatecoupling is shared with the separate report that arunning Claude Code workflow is not recognized as "working".
Root cause confirmed: suspect #2 (stale/false
working), triggered by a/clearcommand echoReproduced and live-verified against real transcripts on the host. This is your suspect #2 — a false
workingstate where Escape lands on an idle prompt — with a concrete, deterministic trigger. Suspect #1 (the single-Escape gesture) is not at fault in this case: the interrupt POST works, there is simply nothing to interrupt.Trigger
Type
/clear(or/rewind) in the composer. After issue #34 made lab follow the transcript rotation, the chat correctly empties — but the composer then gets stuck showing the pulsing Interrupt with no Send, and tapping it does nothing (stays Interrupt). It generalizes to any trailing local command echo with no following model turn.Mechanism (evidence chain)
/clearcallssetConversationId(newUUID)→ a fresh transcript file B (compat.md §5). lab now follows the rotation, so it reads B.assistantline at all (the model is never invoked). From a real post-/clearsession on the host (acc89838…), B's line types aremode, user, user, system, user, deferred_tools_delta, attachment, ….userlines is the/clearecho, and itsisMetais absent → false: So theisMetaskip atinternal/provider/claudecode/chat.go:191does not catch it — it is emitted as auser:text, and it is the last content-bearing event (only skipped deltas/attachments follow).deriveStatemaps a trailinguser:text→StateWorking(internal/provider/claudecode/chat.go:238-242). Already codified by the existing testinternal/provider/claudecode/chat_test.go:85— the case literally named"user waits"assertsStateWorking.svc.Read()(internal/chat/chat.go:200-227) therefore returnsworkingon every read of B (the live-signal overlay only ever forcesquestion/needs_input, neverworking).Why both symptoms follow
state === 'working'(web/src/routes/RunChat.tsx:1158, morph button:1278-1300), so a falseworkingshows the pulsing Interrupt and hides Send./clearuser line — so the nextderiveStateisworkingagain and the button never flips back. This is exactly "Escape lands on an idle prompt" from suspect #2, and it's why a refetch never self-corrects.Proposed fix
In the claudecode parser, treat a slash-command echo (user text wrapped in
<command-name>/<command-message>, plus the<local-command-stdout>output wrapper) as non-conversational — skip it exactly likeisMetaatchat.go:191. Then B has no trailing user turn →deriveState→StateIdle→ composer shows Send, and Interrupt is correctly absent. Bonus: it stops the raw<command-name>/clear…>tag from rendering as a user bubble. Rotation detection stays "by effect" (compat §5) — this only reclassifies a command breadcrumb, it does not parse the command to detect the rotation.Regression seam: add a case to
TestParseTranscript_stateEdges(chat_test.go:79) — a transcript ending in a/clearecho → expectStateIdle. Fails today (working), passes after.Relationship to #39
Same
deriveStatecoupling, opposite direction: #39 is a false idle (real background work misread as needs-input/idle); this is a false working (a command breadcrumb misread as working). Both are "state derived from only the last transcript block."Analysis by an AI agent; not yet implemented.
Ready for Human — re-scoped
The false-
workingcase that/cleartriggers has been split into #45 (agent-ready, root cause confirmed). With that removed, this issue is now specifically about interrupt failing while the agent is genuinely working — the original repro here (the turn continues after clicking Interrupt).Category: bug
Summary: Clicking Interrupt while an agent is genuinely mid-turn does not stop the turn.
Current behavior:
With a genuinely-working agent, the composer correctly shows the Interrupt morph. Clicking it POSTs the interrupt, which sends the interrupt gesture to the agent's terminal — a single
Escape, pinned to Claude Code 2.1.198 and explicitly not test-covered (compat §5). The turn continues; nothing visible happens. (Note: many "interrupt does nothing" observations are actually the #45 false-workingstate, where Escape lands on an idle prompt. This issue is the residue: the genuine mid-turn case.)Desired behavior:
Interrupting a genuinely-working agent stops the current turn.
Why human (not agent-delegable):
The fix hinges on the correct interrupt gesture for the installed Claude Code, which can only be established by driving a live, genuinely-working agent session and observing what actually stops it — single vs double
Escape, timing between keystrokes, and whether the interrupt targets the agent's active pane. This is interactive, environment-coupled verification an AFK agent cannot do headlessly. Splitting off #45 is what leaves the gesture and pane-targeting suspects as the remaining explanation for the genuine case.What to verify:
/clearstuck state — that's #45).Escapegesture interrupts the installed Claude Code version; if not, find the gesture that does.Acceptance criteria:
Out of scope:
/clear/ command-echo false-workingcase → #45.Root fix designed: #62 now carries the full decision record (2026-07-10 grill) — liveness edges make the false-
workinglockout impossible by construction (a Stop edge ends the turn regardless of the transcript tail; state table row 3), and the honest state also fixes the Interrupt affordance showing when nothing is interruptible. This issue stays open as the symptom tracker / prose regression test; #62's implementation closes it. Prerequisites filed: #92 (seam refactor), #93 (dead-session sweep — covers the crashed-session variant of the lockout).