Skip to content

[Bug] Question tool cancelled mid-propagation surfaces as a fake error to users #553

@Astro-Han

Description

@Astro-Han

What happened?

In an attached session export the assistant called the question tool to ask "export as Word or PDF?", but the user never saw the option buttons. The question dock flashed for 22 ms and collapsed into the small grey "这个问题已取消,尚未收到回答..." fallback copy under a "已中断" divider, so the user reads it as the model erroring out. The model itself did not fail — there is a 1-2 s race window between the server emitting a question tool part and the client receiving the matching sync.question entry, and any POST /sessions/:id/abort that lands inside that window cancels the still-propagating question and writes metadata.interrupted: true, which the UI renders as the cancelled-question fallback hint instead of the card.

Steps to reproduce

  1. Send a prompt that's likely to trigger a clarification question (e.g. "重新整理纪要为正式文件" with no format specified).
  2. While the model is mid-stream, with the composer focused and empty, press Enter — or press Stop within ~1 s after the question tool starts.
  3. Observe: dock briefly transitions to question (Word/PDF buttons) then back to prompt, with a "已中断" divider and "这个问题已取消..." hint, no chance to answer.

Expected

Either the question card stays visible long enough to be answered, or — if a halt really does fire — the fallback copy reads as "the card didn't render in time, just resend" instead of "the model cancelled its question".

Diagnostics

Session export shared out-of-band. Timeline:

  • 10:50:33.621 server: question tool time.start, part status → running
  • 10:50:34.642 server: question state written as error + metadata.interrupted: true
  • 10:50:34.643 server: assistant message MessageAbortedError: Aborted, trace aborted: true
  • 10:50:34.807 client: session.layout.composer_dock flips to dock_kind: question (sync.question finally arrives, composer 120→280)
  • 10:50:34.829 client: flips back to dock_kind: prompt (Rejected event arrives 22 ms later)

Server abort chain (only path that produces MessageAbortedError):

  1. POST /sessions/:id/abortSessionPrompt.cancel (packages/opencode/src/server/instance/session.ts:446)
  2. state.cancelRunner.cancelFiber.interrupt(runner.fiber) (packages/opencode/src/session/run-state.ts:77)
  3. processor Effect.onInterrupt fires → halt(new DOMException("Aborted","AbortError")) (packages/opencode/src/session/processor.ts:959)
  4. Effect.acquireRelease releases the LLM scope → cleanup() calls ctrl.abort() on the shared AbortController (packages/opencode/src/session/llm.ts:438-475)
  5. ctx.abort = request.ctrl.signal and tool/question.ts:38 signal: ctx.abort share the same signal
  6. Question.ask's failFromAbort (packages/opencode/src/question/index.ts:291-320) publishes Event.Rejected({cancelled: true, reason: "cancelled"}) and fails the deferred with RejectedError{cancelled: true}
  7. processor tool-fail handler (packages/opencode/src/session/processor.ts:566-583) sees error.cancelled === true and writes metadata.interrupted: true on the question part
  8. UI (packages/ui/src/components/message-part.tsx:1435) renders ui.messagePart.questions.interrupted instead of the question card

Callers of POST /sessions/:id/abort in the app:

  • Composer Stop button — packages/app/src/components/prompt-input/submit.ts:248
  • Empty-prompt Enter while running — packages/app/src/components/prompt-input/submit.ts:326
  • undo command — packages/app/src/pages/session/use-session-commands.tsx:307
  • Session revert — packages/app/src/pages/session.tsx:134
  • Auto-heal recovery clock (HEAL_DELAY_MS=3000) — packages/app/src/pages/session/blockers/question-recovery-clock.ts:141

For this specific session, auto-heal can't fit (3 s vs 1.02 s) and renderer log has no session.action.submit between 10:50:26 and 10:50:57, so revert/undo/content-submit are ruled out. The remaining paths (Stop button, empty-Enter while working) currently emit no renderer diagnostic, so the trigger can't be confirmed from the export. Most likely: empty-Enter collided with the dock expanding 120→280 px as the question tried to render.

Suggested fixes

  1. Hide the question tool part in the renderer until both part.state.status === "running" and a matching sync.question[messageID:callID] entry exist. Closes the race window for both auto-heal and stray abort calls.
  2. Emit a session.action.abort renderer diagnostic with source: stopButton | emptyEnter | undo | revert | autoHeal from each of the four non-submit abort entry points, so the trigger is recoverable from session exports.
  3. Rewrite ui.messagePart.questions.interrupted — current wording reads as "the model cancelled its question". Suggest something closer to "didn't have time to show the question, please say it in the next prompt".

Environment

  • PawWork version: local build (session export runtime_context.app_version: "local")
  • OS: macOS 15.x (Darwin 25.3.0)
  • Reproducibility: Sometimes (timing race)

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Medium priorityappApplication behavior and product flowsbugSomething isn't workingharnessModel harness, prompts, tool descriptions, and session mechanics

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions