Chat: /clear leaves the composer stuck in "working" (no Send, Interrupt inert) — command echoes misread by deriveState #45
Labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Cloonar/coding-lab#45
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
After a
/clear(or/rewind) in the embedded chat, the composer gets stuck showing the pulsing Interrupt button with no Send, and tapping Interrupt does nothing (it stays Interrupt). The chat does correctly empty — issue #34's transcript-rotation following works — but the run then reads asworkingforever.Split out of #38: that issue's written repro is about interrupt failing during genuine work; this is the distinct false-
workingcase. Root cause is confirmed and live-verified against real on-host transcripts — see the agent brief comment below.Reproduction
/clearfrom the composer./clearthe run reads as idle and the composer shows Send.Root cause (confirmed)
Claude Code writes a local slash command to the fresh transcript as a
usermessage whose text is a<command-name>/clear</command-name>echo, withisMetaabsent (→ treated as false). The transcript parser emits it as a normal user turn, and the state reducer maps a trailing user turn toworking— but/clearnever invokes the model, so nothing follows it and the run staysworkingindefinitely. The web composer gates its Send↔Interrupt morph purely on that state, so Send is hidden; the interrupt POST fires against an idle prompt, so the state never flips back.Full evidence chain and proposed fix are in the agent brief below.
Agent Brief
Category: bug
Summary: A locally-handled slash-command echo (e.g.
/clear,/rewind) at the tail of the transcript is misread asworking, locking the chat composer into the Interrupt morph with no Send.Current behavior:
Claude Code records a local slash command as a
user-role transcript event whose text content is wrapped in<command-name>…</command-name>/<command-message>…</command-message>tags (and command output as<local-command-stdout>…). These are UI breadcrumbs, not conversational turns, and they carry noisMetaflag. The transcript parser currently skips onlyisMetauser text, so it emits the command echo as an ordinary user message. The conversational-state reducer maps a trailing user message to the "agent is about to work" state. Because/clear(and/rewind) rotate to a fresh transcript whose only tail is that echo — with no following assistant turn, ever — the run is reported as working indefinitely. The web composer gates its Send↔Interrupt morph purely on that state, so it shows a pulsing Interrupt with no Send, and the interrupt POST fires against an idle prompt (nothing to interrupt), so the state never flips back.Desired behavior:
A slash-command echo (and local-command output) is non-conversational: it emits no chat message and does not drive conversational state. After
/clear//rewind, the freshly-rotated transcript therefore derives the idle state and the composer shows Send. A genuine plain-text user reply is unchanged — it still derivesworkingso the Send→Interrupt morph is preserved.Key interfaces / contracts:
isMetauser text; extend that same non-conversational filter to recognize a slash-command echo — user text whose trimmed content begins with<command-name>or<command-message>— and local-command output (<local-command-stdout>). This is a Claude-Code-specific coupling and belongs in the provider layer alongside the existing transcript grammar; document/pin it in the provider compat notes (§5).deriveState, mapping the last content-bearing event to a conversational state) needs no rule change once the echo is filtered upstream: with the echo skipped, a/cleartranscript has no trailing user turn and reduces to the idle default.workinggate are unchanged.Acceptance criteria:
<command-name>/clear</command-name>) derives the idle state, notworking. Add a case at the transcript-parse state-edge test seam (the existing table that already covers "trailing user text → working" / "user then assistant → needs_input").<command-…>tags shown in the chat).<local-command-stdout>command output is likewise treated as non-conversational./clearin a live run, the composer shows Send and no longer shows a stuck Interrupt.Out of scope:
Notes for the implementer:
/triage) do invoke the model. Skipping their echo means the brief window before the agent's first token shows idle/prior-state rather thanworking; that's acceptable — the agent's own thinking/tool events re-establishworkingwithin about one poll interval. Do not special-case which commands invoke the model.contentis a plain string like"<command-name>/clear</command-name>\n <command-message>clear</command-message>\n <command-args></command-args>"; tag order varies and there is leading whitespace, so match on a trimmed prefix, not an exact string.