Repository navigation
fix(ask): a prompt typed into an Ask-this-session turn takes it over on every host - #1703
Merged
Merged
Conversation
…on every host
When the person typed into the session while it answered a Plannotator
"Ask this session" question, the reply to THEIR prompt kept streaming into
the Ask AI panel as the answer, and a reviewer's Stop (or "Interrupt and ask
now") aborted the turn that now carried the person's own work.
One rule, per host: once a prompt Plannotator did not send enters the turn
answering the question, stop streaming at once, keep what was sent, settle
the question with the new bridge error code `taken_over` (provider:
`session_taken_over` + "You typed into this session while it was answering,
so the rest of the reply went to your prompt."), and never abort that turn
from Plannotator again.
- Claude Code mod: prompt.submit's turnId names the question's turn and the
origin is not task-notification. Output of the step in flight (requested
before their prompt) is held and released when the question settles: at the
turn's next step as taken_over, or at turn.complete as done when no step
followed. Another plugin's framed prompt ("The <name> plugin sent a
message:") now matches the foreign guard, so it is never claimed.
- Pi: a user message, or a custom message delivered right after a
turn_start (steer / follow-up), in the run answering our question. A
display-only, non-triggering custom message (appended at turn_end) is not.
- OpenCode 2: session.inbox.delivered for another row after ours was
delivered and the model began answering; a notice promoted in the same
batch is not a take-over.
- UI: session_taken_over is a note under the partial answer
(response.notice / SessionAskNote), not an error replacing it.
- Pull bridge accepts taken_over; hosts send the text with it so an older
server (unknown code reads as failed) still says why.
Co-Authored-By: Claude <noreply@anthropic.com>
…eutral note, finished answers stay done, older-server fallback - OpenCode 2: a delivery takes the run over only when the row is a USER row, read per inboxID from session.inbox.enqueued (item.type); synthetic, compaction, move and unseen rows never do. - Claude Code mod: an allowlist of origins the model must now answer (composer, bridge, slack-ping, channel, peer); task-notification, peer-send-message, scheduled-trigger, observer*, coordinator, projects-relay, auto-continuation, unclassified and future kinds never take over. Streaming holds the moment the prompt reaches our prompt.submit hook, before next(e); confirmed when it entered, released when a hook beneath dropped it. A Stop while it is on its way closes only the question. - Note wording: neutral "Another message entered this session…" by default; "You typed…" only where the mod knows the person typed (composer, bridge). Interrupt refusal text is neutral and shared. - A message arriving after the answer finished (last response stopped with no tool call: Claude end_turn, Pi stop, OpenCode finish stop) settles done, e.g. Plannotator's own decision follow-up on Pi. - Pi: the taken-over flag holds until agent_settled (idle on older Pi), so an error retry of the person's run cannot be interrupted. - Older CLI servers: the pull server advertises `features: ["taken_over"]` on every poll answer; a pull host (mod bridge.ts, runPullSessionBridgeClient) talking to a server without it settles a take-over as done with the partial answer plus the note (takenOverFallback), so an older UI never replaces the partial answer with an error. Co-Authored-By: Claude <noreply@anthropic.com>
…peer is not a take-over - OpenCode: a step that started a tool call (session.tool.input.started) is never a finished answer, even when its finish reads "stop" (some OpenAI-compatible providers report that on tool-calling steps). - Claude Code mod: the end_turn path settles done only when some answer text was shown; a thinking-only final response settles as taken_over, as on Pi and OpenCode. - Claude Code mod: `peer` leaves the take-over allowlist (owner's call: a lost answer is worse than a peer's reply streaming into the panel). Co-Authored-By: Claude <noreply@anthropic.com>
1 task
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Target: 0.28.3.
Bug
When the person typed into the session while it was answering a Plannotator "Ask this session" question, two things went wrong on all three hosts:
The rule (same on every host)
Once another message enters the turn that is answering the question (the person typing, another extension's steer):
Streaming stops at once, and what was already sent stays.
The question settles with a new bridge error code,
taken_over. The provider maps it tosession_taken_overwith a note.If the answer had already finished before the other message entered, the question settles as
doneinstead. "Finished" means the turn's last model response stopped with no tool call and some answer text was shown:end_turn. A thinking-only final response with no text settles astaken_over, not as an emptydone.stop.finish: "stop"and nosession.tool.input.startedin that step. Some OpenAI-compatible providers report "stop" on a step that called tools.This keeps a follow-up, such as Plannotator's own decision on Pi, from turning a complete answer into a cut one.
Plannotator never aborts that turn again. Stop only closes the question, and
interrupt()refuses withSESSION_ASK_TAKEN_OVER_INTERRUPT_TEXTfor as long as the taken-over run lasts.Note wording
SESSION_ASK_TAKEN_OVER_TEXT): "Another message entered this session while it was answering, so the rest of the reply went to that message."composer,bridge), it usesSESSION_ASK_TAKEN_OVER_BY_PERSON_TEXT: "You typed into this session while it was answering, so the rest of the reply went to your prompt."Per host, before → after
Claude Code mod
Before:
prompt.submitignorede.turnIdand only recorded the prompt text. The turn kept streaming untilturn.complete, and cancel or interrupt called$.turn.abort.After:
turnIdand come from an allowlisted origin:composer,bridge,slack-ping,channel.peeris deliberately excluded (owner's call): a peer session's reply streaming into the panel is better than losing the reviewer's answer.peer,task-notification,peer-send-message(notifications framed for the agent),scheduled-trigger,observer*,coordinator,projects-relay,auto-continuation,unclassified(engine idle notices and delivery receipts), and any origin added later.prompt.submithook, beforenext(e)runs the hooks beneath it, so a slow hook cannot let the next step stream. The take-over is confirmed whennext(e)resolves with the prompt. If a hook beneath drops the prompt, the held output is released and streaming resumes.turn.step, which carries the person's prompt:doneif the previous response stopped withend_turnand some text was shown, otherwisetaken_over.turn.complete, if no step followed:donewith the turn's answer. The person's prompt then runs as its own turn, which is not claimed.turn.startwrapped in "The plugin sent a message:", so the text guard never matched it.takeForeignnow also compares with that frame removed, so another plugin's turn is never claimed.Pi
Before: a
message_startwith roleuserwas ignored. Streaming ran untilagent_end, and Stop calledctx.abort()on the person's steered work.After:
usermessage, or a custom message other than ours that is delivered right after aturn_start(a steer or follow-up).plannotator-handoff, is appended by Pi atturn_end, outside that window, so it is not a take-over.agent_settled, or until the session is idle on older Pi, so an error retry of the person's run cannot be interrupted either.OpenCode 2
Before:
session.inbox.deliveredfor another id was ignored. Deltas streamed until the run ended, andsession.interruptstopped everything.After:
userrow takes over. The delivered event carries only the row id, so each row's kind is read fromsession.inbox.enqueued(item.type). Synthetic, compaction, move, and unseen rows never take over.UI
useAIChattreatssession_taken_overasresponse.notice, not an error, so the partial answer stays visible.SessionAskNoterenders the note under it in the document chat panel and the review AI tab.Back-compat with older CLI servers
The plugin and the binary update separately. An older server reads
taken_overasfailed, and its UI would then replace the partial answer with the error. To handle that:features: ["taken_over"](SESSION_BRIDGE_POLL_FEATURES) on every poll answer.bridge.ts, orrunPullSessionBridgeClientfor OpenCode) talks to a server without that feature, it settles a take-over asdone. The answer is the partial text plus the note as its last paragraph (takenOverFallback).Tests
turns.test.ts,bridge.test.ts):end_turnbefore the next step settles asdoneend_turnsettles astaken_over, not an emptydonedoneagent_settledinterrupt()rejectsfinish: "stop"settles asdonefinish: "stop"is not a finished answerpackages/ai:taken_overfeaturespackages/ui(DOM): the note is kept under the streamed text.Commands run
bun test packages/ai apps/hook apps/pi-extension apps/opencode-plugin: 1312 pass, 0 fail.DOM_TESTS=1 bun teston the two changed UI test files: 9 pass.bun run typecheck, including the mod tsconfig: clean.CLAUDE_CONFIG_DIR=$(mktemp -d) sh scripts/test-claude-code-mod.sh: 14 pass, 0 fail.claude plugin validate apps/hook: passed.🤖 Generated with Claude Code