Chat: /clear leaves the composer stuck in "working" (no Send, Interrupt inert) — command echoes misread by deriveState #45

Closed
opened 2026-07-08 10:18:08 +02:00 by dominik.polakovics · 1 comment

This was generated by AI during triage.

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 as working forever.

Split out of #38: that issue's written repro is about interrupt failing during genuine work; this is the distinct false-working case. Root cause is confirmed and live-verified against real on-host transcripts — see the agent brief comment below.

Reproduction

  1. Open a live run's chat and send /clear from the composer.
  2. Observed: the chat empties, but the composer shows Interrupt (no Send); tapping Interrupt has no effect and stays Interrupt.
  3. Expected: after /clear the 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 user message whose text is a <command-name>/clear</command-name> echo, with isMeta absent (→ treated as false). The transcript parser emits it as a normal user turn, and the state reducer maps a trailing user turn to working — but /clear never invokes the model, so nothing follows it and the run stays working indefinitely. 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.

> *This was generated by AI during triage.* ## 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 as `working` forever. Split out of #38: that issue's written repro is about interrupt failing during *genuine* work; this is the distinct **false-`working`** case. Root cause is confirmed and live-verified against real on-host transcripts — see the agent brief comment below. ## Reproduction 1. Open a live run's chat and send `/clear` from the composer. 2. **Observed:** the chat empties, but the composer shows Interrupt (no Send); tapping Interrupt has no effect and stays Interrupt. 3. **Expected:** after `/clear` the 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 `user` message whose text is a `<command-name>/clear</command-name>` echo, with `isMeta` absent (→ treated as false). The transcript parser emits it as a normal user turn, and the state reducer maps a trailing user turn to `working` — but `/clear` never invokes the model, so nothing follows it and the run stays `working` indefinitely. 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.
Author
Owner

This was generated by AI during triage.

Agent Brief

Category: bug
Summary: A locally-handled slash-command echo (e.g. /clear, /rewind) at the tail of the transcript is misread as working, 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 no isMeta flag. The transcript parser currently skips only isMeta user 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 derives working so the Send→Interrupt morph is preserved.

Key interfaces / contracts:

  • The Claude Code transcript folding step (JSONL → universal messages) already drops isMeta user 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).
  • The conversational-state reducer (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 /clear transcript has no trailing user turn and reduces to the idle default.
  • The state enum and the web composer's working gate are unchanged.

Acceptance criteria:

  • A transcript whose last content-bearing line is a slash-command echo (e.g. <command-name>/clear</command-name>) derives the idle state, not working. 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").
  • A slash-command echo does not render as a user-message bubble (no raw <command-…> tags shown in the chat).
  • A genuine plain-text user reply at the tail still derives working (the Send→Interrupt morph is preserved) — assert this so the fix can't regress the morph.
  • <local-command-stdout> command output is likewise treated as non-conversational.
  • Manually: after /clear in a live run, the composer shows Send and no longer shows a stuck Interrupt.

Out of scope:

  • The genuine mid-turn interrupt no-op (#38) — whether the interrupt gesture actually stops a working agent. Different root cause; needs live verification.
  • Detecting the transcript rotation itself — that stays "by observable effect" (a different located file), never by parsing the command.
  • The workflow / background-subagent false-idle case (#39) — the opposite direction of the same reducer, tracked separately.

Notes for the implementer:

  • Some slash commands (e.g. /triage) do invoke the model. Skipping their echo means the brief window before the agent's first token shows idle/prior-state rather than working; that's acceptable — the agent's own thinking/tool events re-establish working within about one poll interval. Do not special-case which commands invoke the model.
  • The reducer's "trailing user turn → working" rule is intentionally load-bearing for the Send→Interrupt morph (ADR-0022) right after a real reply. Keep it; fix this purely by filtering the echo upstream.
  • Ground truth for the echo shape (verified live, Claude Code 2.1.198): content is 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.
> *This was generated by AI during triage.* ## Agent Brief **Category:** bug **Summary:** A locally-handled slash-command echo (e.g. `/clear`, `/rewind`) at the tail of the transcript is misread as `working`, 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 no `isMeta` flag. The transcript parser currently skips only `isMeta` user 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 derives `working` so the Send→Interrupt morph is preserved. **Key interfaces / contracts:** - The Claude Code transcript folding step (JSONL → universal messages) already drops `isMeta` user 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). - The conversational-state reducer (`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 `/clear` transcript has no trailing user turn and reduces to the idle default. - The state enum and the web composer's `working` gate are unchanged. **Acceptance criteria:** - [ ] A transcript whose last content-bearing line is a slash-command echo (e.g. `<command-name>/clear</command-name>`) derives the **idle** state, not `working`. 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"). - [ ] A slash-command echo does not render as a user-message bubble (no raw `<command-…>` tags shown in the chat). - [ ] A genuine plain-text user reply at the tail still derives **working** (the Send→Interrupt morph is preserved) — assert this so the fix can't regress the morph. - [ ] `<local-command-stdout>` command output is likewise treated as non-conversational. - [ ] Manually: after `/clear` in a live run, the composer shows Send and no longer shows a stuck Interrupt. **Out of scope:** - The genuine mid-turn interrupt no-op (#38) — whether the interrupt *gesture* actually stops a working agent. Different root cause; needs live verification. - Detecting the transcript rotation itself — that stays "by observable effect" (a different located file), never by parsing the command. - The workflow / background-subagent false-*idle* case (#39) — the opposite direction of the same reducer, tracked separately. **Notes for the implementer:** - Some slash commands (e.g. `/triage`) *do* invoke the model. Skipping their echo means the brief window before the agent's first token shows idle/prior-state rather than `working`; that's acceptable — the agent's own thinking/tool events re-establish `working` within about one poll interval. Do not special-case which commands invoke the model. - The reducer's "trailing user turn → working" rule is intentionally load-bearing for the Send→Interrupt morph (ADR-0022) right after a real reply. Keep it; fix this purely by filtering the echo upstream. - Ground truth for the echo shape (verified live, Claude Code 2.1.198): `content` is 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.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Cloonar/coding-lab#45
No description provided.