Channel plugins: support cancelling / superseding an in-flight turn in `claude -p` (stream-json)

Resolved 💬 1 comment Opened Apr 20, 2026 by jhayashi Closed May 28, 2026

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.
  1. 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.
  1. 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.

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗