claude-code - 💡(How to fix) Fix Channel plugins: support cancelling / superseding an in-flight turn in `claude -p` (stream-json)

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…

We maintain dc-claude-channel, a channel plugin that bridges Delta Chat to Claude Code. Unlike the first-party Telegram, Discord, and iMessage channel plugins, which route every incoming message through a single persistent CLI session, we run one persistent claude -p subagent per chat. Each chat is its own --session-id conversation with its own agent definition, prompt, and tool scope. The dispatcher holds them in an LRU cache and talks to each one over stream-json stdio.

That architecture gives us per-chat isolation and parallelism — a long-running turn in one chat can't block a quick question in another. It also makes the issue below much more visible, because users interact with these subagents the way they'd chat with a human: they fire off a message, have a second thought, and fire off another while the first is still being processed.

Error Message

  • A cancel frame in stream-json stdin. Something like {"type":"cancel_turn"} that the CLI handles by aborting whatever assistant/tool loop is in progress, posting a terminal assistant_message or error with reason: "cancelled", and returning to the ready-for-input state. Process stays alive, session UUID stays valid, prompt cache stays warm.

Root Cause

That architecture gives us per-chat isolation and parallelism — a long-running turn in one chat can't block a quick question in another. It also makes the issue below much more visible, because users interact with these subagents the way they'd chat with a human: they fire off a message, have a second thought, and fire off another while the first is still being processed.

Fix Action

Fix / Workaround

We maintain dc-claude-channel, a channel plugin that bridges Delta Chat to Claude Code. Unlike the first-party Telegram, Discord, and iMessage channel plugins, which route every incoming message through a single persistent CLI session, we run one persistent claude -p subagent per chat. Each chat is its own --session-id conversation with its own agent definition, prompt, and tool scope. The dispatcher holds them in an LRU cache and talks to each one over stream-json stdio.

Today, once a turn has been dispatched to a claude -p subagent on stdin, there is no supported way to:

  1. Text chat follow-ups. User: "Refactor the auth module." Five seconds later: "Wait — leave the OAuth path alone, I need that stable for the release." Today the second message queues and runs as a second full turn after the first. We'd like to supersede turn 1 with turn 1 text + "\n[user added]: " + turn 2 text and start one turn from the merged input.
RAW_BUFFERClick to expand / collapse

Context

We maintain dc-claude-channel, a channel plugin that bridges Delta Chat to Claude Code. Unlike the first-party Telegram, Discord, and iMessage channel plugins, which route every incoming message through a single persistent CLI session, we run one persistent claude -p subagent per chat. Each chat is its own --session-id conversation with its own agent definition, prompt, and tool scope. The dispatcher holds them in an LRU cache and talks to each one over stream-json stdio.

That architecture gives us per-chat isolation and parallelism — a long-running turn in one chat can't block a quick question in another. It also makes the issue below much more visible, because users interact with these subagents the way they'd chat with a human: they fire off a message, have a second thought, and fire off another while the first is still being processed.

The problem

Today, once a turn has been dispatched to a claude -p subagent on stdin, there is no supported way to:

  1. Cancel it and keep the session hot.
  2. Supersede its input with a new, merged prompt.
  3. Have any tool call in flight (Bash, Edit, etc.) cleanly terminate as part of that cancel.

Our only options are:

  • Wait. Queue the second message until the first turn's tool chain completes. On a multi-step task this can be minutes, during which the user's correction is stale.
  • SIGINT the child. Kills the whole process — we lose the in-memory session, have to respawn and --resume <uuid>, which takes ~10 s on our measurements and leaves any in-flight tool subprocess orphaned (e.g., a spawned Bash that was mid-command).

Neither gives us the interaction quality a chat UI wants.

Concrete use cases this blocks

In order of how painful the gap is for us:

  1. Text chat follow-ups. User: "Refactor the auth module." Five seconds later: "Wait — leave the OAuth path alone, I need that stable for the release." Today the second message queues and runs as a second full turn after the first. We'd like to supersede turn 1 with turn 1 text + "\n[user added]: " + turn 2 text and start one turn from the merged input.

  2. Voice messages with preview-before-act. We transcribe voice locally (whisper.cpp) and echo the transcript back to the chat before the subagent acts on it. If the transcript is wrong, the user's natural response is another voice message correcting it — but turn 1 has already been dispatched. An interrupt lets us replace it with the correction.

  3. Halting in-progress actions from WebXDC UIs. Our file reviewer lets users long-press specific lines or paragraphs of a rendered doc and comment. A natural flow is: user sends a file → subagent starts a long refactor based on their first comment → user scrolls down and adds a second comment that changes the intent. Right now the second comment has to wait in line behind the first. An interrupt primitive would let the reviewer-app surface "you're about to supersede an in-flight change, ok?" and merge the new intent.

All three share a shape: the user's latest input is a truer signal than what the subagent is currently processing, but we can't act on it.

What would unblock this

Any of the following, roughly from cleanest to most minimal:

  • A cancel frame in stream-json stdin. Something like {"type":"cancel_turn"} that the CLI handles by aborting whatever assistant/tool loop is in progress, posting a terminal assistant_message or error with reason: "cancelled", and returning to the ready-for-input state. Process stays alive, session UUID stays valid, prompt cache stays warm.

  • A documented SIGINT contract. "SIGINT on a claude -p process aborts the current turn, propagates to spawned tool children (tree-kill), and leaves the process accepting the next stream-json input." Requires the CLI to install its own SIGINT handler rather than exiting.

  • A replacement-turn frame. {"type":"supersede_turn", "content": "..."} that atomically cancels the in-flight turn and starts a new one from the provided content. This is what we'd most want in practice — it matches the "merge msg1 + msg2" user model directly.

Related work

  • There's an existing feature request in our tracker (jhayashi/dc-claude-channel#21) for a !! user command to interrupt the subagent and report its current activity. We've marked it blocked:upstream because the primitive above is the missing piece — the !! command is one UX on top of it, not the hard problem.
  • If the team already has a ServerNotification-shaped way to do this through the channel notification protocol rather than on stdin, we'd happily use that instead; we just haven't found one.

Happy to prototype on our end against any shape you land on.

extent analysis

TL;DR

Implementing a cancel or supersede mechanism in the claude -p subagent, such as a cancel frame in stream-json stdin or a documented SIGINT contract, would allow for interrupting and replacing in-flight turns.

Guidance

  • Investigate adding a {"type":"cancel_turn"} frame to the stream-json stdin protocol to abort the current turn and return to a ready state.
  • Consider implementing a SIGINT handler in the claude -p process to abort the current turn and propagate the signal to spawned tool children.
  • Explore the possibility of a {"type":"supersede_turn", "content": "..."} frame to atomically cancel the in-flight turn and start a new one from the provided content.
  • Review the existing feature request (jhayashi/dc-claude-channel#21) for a !! user command to interrupt the subagent and report its current activity.

Example

No code snippet is provided as the issue is more focused on the design and implementation of a new feature rather than a specific code fix.

Notes

The solution will depend on the specific requirements and constraints of the claude -p subagent and the stream-json stdin protocol. It is essential to consider the potential impact on the existing functionality and the user experience.

Recommendation

Apply a workaround by implementing a cancel or supersede mechanism, such as a cancel frame in stream-json stdin, to allow for interrupting and replacing in-flight turns. This will provide a more responsive and interactive user experience.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

claude-code - 💡(How to fix) Fix Channel plugins: support cancelling / superseding an in-flight turn in `claude -p` (stream-json)