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
- Send a prompt that's likely to trigger a clarification question (e.g. "重新整理纪要为正式文件" with no format specified).
- 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.
- 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):
POST /sessions/:id/abort → SessionPrompt.cancel (packages/opencode/src/server/instance/session.ts:446)
state.cancel → Runner.cancel → Fiber.interrupt(runner.fiber) (packages/opencode/src/session/run-state.ts:77)
- processor
Effect.onInterrupt fires → halt(new DOMException("Aborted","AbortError")) (packages/opencode/src/session/processor.ts:959)
Effect.acquireRelease releases the LLM scope → cleanup() calls ctrl.abort() on the shared AbortController (packages/opencode/src/session/llm.ts:438-475)
ctx.abort = request.ctrl.signal and tool/question.ts:38 signal: ctx.abort share the same signal
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}
- 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
- 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
- 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.
- 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.
- 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)
What happened?
In an attached session export the assistant called the
questiontool 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 aquestiontool part and the client receiving the matchingsync.questionentry, and anyPOST /sessions/:id/abortthat lands inside that window cancels the still-propagating question and writesmetadata.interrupted: true, which the UI renders as the cancelled-question fallback hint instead of the card.Steps to reproduce
question(Word/PDF buttons) then back toprompt, 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:
time.start, part status →runningerror + metadata.interrupted: trueMessageAbortedError: Aborted, traceaborted: truesession.layout.composer_dockflips todock_kind: question(sync.question finally arrives, composer 120→280)dock_kind: prompt(Rejected event arrives 22 ms later)Server abort chain (only path that produces
MessageAbortedError):POST /sessions/:id/abort→SessionPrompt.cancel(packages/opencode/src/server/instance/session.ts:446)state.cancel→Runner.cancel→Fiber.interrupt(runner.fiber)(packages/opencode/src/session/run-state.ts:77)Effect.onInterruptfires →halt(new DOMException("Aborted","AbortError"))(packages/opencode/src/session/processor.ts:959)Effect.acquireReleasereleases the LLM scope →cleanup()callsctrl.abort()on the sharedAbortController(packages/opencode/src/session/llm.ts:438-475)ctx.abort = request.ctrl.signalandtool/question.ts:38signal: ctx.abortshare the same signalQuestion.ask'sfailFromAbort(packages/opencode/src/question/index.ts:291-320) publishesEvent.Rejected({cancelled: true, reason: "cancelled"})and fails the deferred withRejectedError{cancelled: true}packages/opencode/src/session/processor.ts:566-583) seeserror.cancelled === trueand writesmetadata.interrupted: trueon the question partpackages/ui/src/components/message-part.tsx:1435) rendersui.messagePart.questions.interruptedinstead of the question cardCallers of
POST /sessions/:id/abortin the app:packages/app/src/components/prompt-input/submit.ts:248packages/app/src/components/prompt-input/submit.ts:326undocommand —packages/app/src/pages/session/use-session-commands.tsx:307packages/app/src/pages/session.tsx:134packages/app/src/pages/session/blockers/question-recovery-clock.ts:141For this specific session, auto-heal can't fit (3 s vs 1.02 s) and renderer log has no
session.action.submitbetween 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
part.state.status === "running"and a matchingsync.question[messageID:callID]entry exist. Closes the race window for both auto-heal and stray abort calls.session.action.abortrenderer diagnostic withsource: stopButton | emptyEnter | undo | revert | autoHealfrom each of the four non-submit abort entry points, so the trigger is recoverable from session exports.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
runtime_context.app_version: "local")