What happened?
A user hit a stuck question dock while answering a 4-question question tool prompt. The message timeline already showed 问题已忽略, but the bottom question dock still displayed question 4/4 with 忽略, 返回, and 提交 controls. The user reported that neither 忽略 nor 提交 worked anymore, and only 返回 still changed the visible question.
This is a stale pending-question state bug, not simply a missing-answer validation issue. The backend had already ended the tool call with The user dismissed this question, while the frontend still believed the same request was pending and kept blocking the composer.
Which area seems affected?
Model harness, prompts, tools, or session mechanics; UI question dock state.
How much does this affect you?
Blocks me from using PawWork.
Steps to reproduce
- Start a session where the assistant calls the
question tool with multiple questions. 2. Navigate to the last question and choose an answer. 3. Submit while at least one earlier question has no answer, or otherwise trigger the backend Question.reply empty-answer validation path. 4. Observe that the message part can show the question as dismissed while the dock remains visible. 5. Click 忽略 or 提交 again. The dock remains stuck because the backend request has already been removed, while the frontend still has the old request in sync.data.question.
Expected interaction design
The user must not be forced to answer every question. Each question should have an explicit per-question state: answered, skipped, or still pending. 跳过当前题 should mean the user intentionally does not want to answer this question, but still wants to continue the group. This should not be treated as a tool error. 提交 should finish successfully only when every question has a clear state: answered or skipped. The model-facing result should preserve that distinction, for example "端头标签是否打印GTIN条形码?"="Skipped by user", rather than collapsing it into an ambiguous empty array. A separate group-level escape can still exist, but it should be clearly labeled as skipping or dismissing the whole group, not confused with skipping the current question.
Agreed product contract for v1: per-question skip is allowed; the bottom 忽略 action should skip the current question; final submit should require every question to be either answered or skipped; skipped questions should be returned to the model as explicit skipped semantics; only a true whole-group cancellation should reject the question tool call.
Root cause diagnosis
packages/opencode/src/question/index.ts currently treats an empty answer array inside Question.reply as a rejected question. In that branch it deletes the pending request and fails the deferred with RejectedError, but it does not publish question.rejected. The normal Question.reject path does publish question.rejected. The frontend removes the dock through packages/app/src/context/global-sync/event-reducer.ts, which only deletes pending questions on question.replied or question.rejected. Therefore the empty-answer failure path can remove the backend pending request without notifying the frontend. The frontend then keeps showing a stale dock. Further reply or reject calls hit an unknown request and currently return successfully without clearing the stale client state.
Relevant code paths: packages/opencode/src/question/index.ts reply() empty-answer branch deletes pending and fails without event; packages/opencode/src/question/index.ts reject() deletes pending and publishes question.rejected; packages/app/src/context/global-sync/event-reducer.ts removes dock entries only on question.replied and question.rejected; packages/app/src/pages/session/composer/session-question-dock.tsx sends reply and reject but does not locally remove the dock.
Proposed design
Represent question responses as a clear per-question outcome instead of overloading empty arrays. The smallest protocol-compatible option is to keep answers: string[][] for now and use a reserved sentinel string for skipped questions in the tool result, but this is fragile because labels are user/model-controlled strings. The cleaner design is to introduce an answer outcome shape such as { status: "answered", answers: string[] } | { status: "skipped" }, then have the tool result render skipped questions explicitly for the model. If backward compatibility must be preserved in the API, the backend can normalize legacy [] to skipped for now and expose a clearer shape later.
The UI should make the current-question action explicit. Rename or reinterpret the bottom 忽略 as 跳过此题 for question prompts. When clicked, mark only the current tab skipped and move to the next unanswered or unskipped tab. On the last tab, if all questions are answered or skipped, 提交 sends the group as a successful reply. If there are still untouched questions, 提交 jumps to the first untouched question instead of sending an invalid request.
A whole-group cancellation, if retained, should be separate from current-question skip. It can be secondary and clearly labeled, for example 跳过全部 or hidden behind a small menu if the UI should stay minimal. This action should call Question.reject, publish question.rejected, clear the dock, and render the message part as group dismissed.
Acceptance criteria
- A user can skip exactly one question in a multi-question prompt and still answer the remaining questions. - Skipping one question does not mark the entire
question tool call as error. - Final submit succeeds when every question is answered or skipped. - The model-facing output explicitly tells the model which questions were skipped. - The dock never remains visible after the backend has resolved or rejected the request. - Unknown or stale reply / reject attempts do not leave the user blocked. - The message timeline distinguishes skipped answers from group-level dismissal. - Focus and keyboard behavior remain usable for back, next, submit, and skip. - Regression coverage includes the exported-session shape where the message shows dismissed while the dock stays visible.
Non-goals
Do not redesign the whole question tool UI. Do not add long-form survey features, nested questions, validation rules, or required-question semantics unless a later issue explicitly asks for them. Do not force users to answer every question.
PawWork version
local build from exported session context.
OS version
macOS Darwin 25.3.0 from exported session context.
Can you reproduce it again?
Only once so far from the provided user report and session export.
Diagnostics
Evidence came from a screenshot showing 问题已忽略 in the message timeline while the bottom dock still showed question 4/4, plus the exported session /Users/yuhan/Downloads/pawwork-session-shiny-squid-2026-05-01-00-36-33.json. In that export, the final question tool part is status: error, error: The user dismissed this question, with 4 questions and no successful answers persisted for the GTIN question.
What happened?
A user hit a stuck question dock while answering a 4-question
questiontool prompt. The message timeline already showed问题已忽略, but the bottom question dock still displayed question 4/4 with忽略,返回, and提交controls. The user reported that neither忽略nor提交worked anymore, and only返回still changed the visible question.This is a stale pending-question state bug, not simply a missing-answer validation issue. The backend had already ended the tool call with
The user dismissed this question, while the frontend still believed the same request was pending and kept blocking the composer.Which area seems affected?
Model harness, prompts, tools, or session mechanics; UI question dock state.
How much does this affect you?
Blocks me from using PawWork.
Steps to reproduce
questiontool with multiple questions. 2. Navigate to the last question and choose an answer. 3. Submit while at least one earlier question has no answer, or otherwise trigger the backendQuestion.replyempty-answer validation path. 4. Observe that the message part can show the question as dismissed while the dock remains visible. 5. Click忽略or提交again. The dock remains stuck because the backend request has already been removed, while the frontend still has the old request insync.data.question.Expected interaction design
The user must not be forced to answer every question. Each question should have an explicit per-question state: answered, skipped, or still pending.
跳过当前题should mean the user intentionally does not want to answer this question, but still wants to continue the group. This should not be treated as a tool error.提交should finish successfully only when every question has a clear state: answered or skipped. The model-facing result should preserve that distinction, for example"端头标签是否打印GTIN条形码?"="Skipped by user", rather than collapsing it into an ambiguous empty array. A separate group-level escape can still exist, but it should be clearly labeled as skipping or dismissing the whole group, not confused with skipping the current question.Agreed product contract for v1: per-question skip is allowed; the bottom
忽略action should skip the current question; final submit should require every question to be either answered or skipped; skipped questions should be returned to the model as explicit skipped semantics; only a true whole-group cancellation should reject thequestiontool call.Root cause diagnosis
packages/opencode/src/question/index.tscurrently treats an empty answer array insideQuestion.replyas a rejected question. In that branch it deletes the pending request and fails the deferred withRejectedError, but it does not publishquestion.rejected. The normalQuestion.rejectpath does publishquestion.rejected. The frontend removes the dock throughpackages/app/src/context/global-sync/event-reducer.ts, which only deletes pending questions onquestion.repliedorquestion.rejected. Therefore the empty-answer failure path can remove the backend pending request without notifying the frontend. The frontend then keeps showing a stale dock. Furtherreplyorrejectcalls hit an unknown request and currently return successfully without clearing the stale client state.Relevant code paths:
packages/opencode/src/question/index.tsreply()empty-answer branch deletes pending and fails without event;packages/opencode/src/question/index.tsreject()deletes pending and publishesquestion.rejected;packages/app/src/context/global-sync/event-reducer.tsremoves dock entries only onquestion.repliedandquestion.rejected;packages/app/src/pages/session/composer/session-question-dock.tsxsendsreplyandrejectbut does not locally remove the dock.Proposed design
Represent question responses as a clear per-question outcome instead of overloading empty arrays. The smallest protocol-compatible option is to keep
answers: string[][]for now and use a reserved sentinel string for skipped questions in the tool result, but this is fragile because labels are user/model-controlled strings. The cleaner design is to introduce an answer outcome shape such as{ status: "answered", answers: string[] } | { status: "skipped" }, then have the tool result render skipped questions explicitly for the model. If backward compatibility must be preserved in the API, the backend can normalize legacy[]to skipped for now and expose a clearer shape later.The UI should make the current-question action explicit. Rename or reinterpret the bottom
忽略as跳过此题for question prompts. When clicked, mark only the current tab skipped and move to the next unanswered or unskipped tab. On the last tab, if all questions are answered or skipped,提交sends the group as a successful reply. If there are still untouched questions,提交jumps to the first untouched question instead of sending an invalid request.A whole-group cancellation, if retained, should be separate from current-question skip. It can be secondary and clearly labeled, for example
跳过全部or hidden behind a small menu if the UI should stay minimal. This action should callQuestion.reject, publishquestion.rejected, clear the dock, and render the message part as group dismissed.Acceptance criteria
questiontool call aserror. - Final submit succeeds when every question is answered or skipped. - The model-facing output explicitly tells the model which questions were skipped. - The dock never remains visible after the backend has resolved or rejected the request. - Unknown or stalereply/rejectattempts do not leave the user blocked. - The message timeline distinguishes skipped answers from group-level dismissal. - Focus and keyboard behavior remain usable for back, next, submit, and skip. - Regression coverage includes the exported-session shape where the message shows dismissed while the dock stays visible.Non-goals
Do not redesign the whole question tool UI. Do not add long-form survey features, nested questions, validation rules, or required-question semantics unless a later issue explicitly asks for them. Do not force users to answer every question.
PawWork version
local build from exported session context.
OS version
macOS Darwin 25.3.0 from exported session context.
Can you reproduce it again?
Only once so far from the provided user report and session export.
Diagnostics
Evidence came from a screenshot showing
问题已忽略in the message timeline while the bottom dock still showed question 4/4, plus the exported session/Users/yuhan/Downloads/pawwork-session-shiny-squid-2026-05-01-00-36-33.json. In that export, the final question tool part isstatus: error,error: The user dismissed this question, with 4 questions and no successful answers persisted for the GTIN question.