fix(rpc): the degraded question asks no trailing "Anything else?" prompt
Part of sisyphuslabs/omo-desktop-app#4042
Problem
A client that does not declare the question capability gets ctx.ui.question through degradeQuestion (connection-handler.ts:530-533). After one select per question it always raised ui.input("Anything else? (optional)") (connection-question-bridge.ts:159). The desktop renders each dialog as its own blocking card, so users saw a new, empty "Anything else? (optional)" question after every ask. That card's only option is the label Answer; sending it returned value: "Answer", which became the comment (comment-submitted), and the model read "The user responded: Answer".
Fix
degradeQuestion settles from the picks alone, through settledQuestionStatus(answers, undefined) like the structured path: answered (resolvedBy rpc_connection) when at least one question was answered, with the rest listed in unanswered (the model reads Unanswered: q2), and cancelled only when none was. The response carries no comment key. "Other (type an answer)" still asks one input for that question. No other path changes. docs/rpc.md, CHANGELOG.md [Unreleased] Fixed and src/modes/rpc/changes.md are updated.
Tests (RED first)
test/suite/rpc-question-bridge.test.ts:
- degraded flow asks one dialog per question and nothing after them (input called once, for "Other")
- picks alone settle
answeredwith no optional-comment prompt - an input answered with the literal
Answercan never become the comment:formatModelAnswertext isq1: A\nq2: B, no user words
RED on origin/main: 3 failed | 5 passed (8). GREEN on this head: 8 passed (8).
Review fold (be7e8dbb07)
- MEDIUM: a partial fallback answer (q1 answered, q2 dismissed) settled
cancelledand the model read only "The user dismissed the question.". It now settlesansweredandformatModelAnswergivesq1: A\nUnanswered: q2; nothing answered stayscancelled. - LOW 1: the
commentkey is left out instead ofcomment: undefined(asserted withtoStrictEqual/"comment" in response). - RED at 6887a9c177: rpc-question-bridge 3 failed | 7 passed (10). GREEN at be7e8dbb07: 10 passed (10).
- Suites at be7e8dbb07 (rpc-question-bridge, all
ask-user-*, rpc-types-question, extension-ui-question-prompt, goal-prompt-question-routing, rpc-extension-ui-response-reply, rpc-worker-routing, regressions 2949/2951/3768/9268/second-session-inherits-capabilities): 46 files, 340 passed (340).bun run checkexit 0, changelog gate PASS, pre-commit hooks passed.
Review fold 2 (d2da60dcbe): an abort is never an answer
- An abort of the question's signal partway through the fallback used to let the remaining selects resolve
undefined, and the partial picks then settledanswered. The fallback now opens no dialog after an abort and settlescancelled(ortimed_outwithautoResolvedAfterMsfor a"timed_out"abort reason), matching the structured bridge, keeping the partial answers inanswersand the rest inunanswered. - RED at be7e8dbb07: rpc-question-bridge 3 failed | 10 passed (13). GREEN at d2da60dcbe: 13 passed (13). Same suite set as fold 1: 46 files, 343 passed (343).
bun run checkexit 0, changelog gate PASS, pre-commit hooks passed.
Known, out of scope (pre-existing)
- LOW 2: dismissing q1 in the fallback still asks q2 (each question is its own
select). Unchanged by this PR.
QA (real CLI, --mode rpc, senpi-qa sandbox, client declares no question)
An extension asks two questions; the driver picks the first option and answers any input card with Answer, like the desktop does.
- before (origin/main): dialogs = select, select, input "Anything else? (optional)"; result
comment-submitted,comment: "Answer"-> FAIL - after (this head): dialogs = select, select; result
answered, answers q1=A q2=C, no comment; real auth unchanged -> PASS
6506d6538aee49b02707b59aa9667e8e21c8a2e37579a840d5aa97d143c4f470 <run>/qa-evidence/qa-4042.json
3952d84c0434c0416b8be5c2dfbad4db37b0c9e401d8f2708365fb1c66c9ec72 <run>/qa-evidence-before/qa-4042.json
Checks
bun run check: exit 0node scripts/check-pr-changelog.mjs --base <merge-base>: PASS- pre-commit hooks: passed
Follow-ups (separate PRs, not in this one)
Why the desktop session took the degraded path although the desktop declares question on its socket (session bindings keep the capabilities they were opened with), and the desktop-side card handling, are tracked on sisyphuslabs/omo-desktop-app#4042.
Summary by cubic
Fixes the degraded RPC question flow so clients without the question capability get no trailing "Anything else?" prompt, a partial answer still reaches the model, and an aborted question settles without opening further dialogs (part of sisyphuslabs/omo-desktop-app#4042).
Previously degradeQuestion always ended with ui.input("Anything else? (optional)"). The desktop renders each dialog as its own blocking card, so users saw a new, empty question after every ask; its only option label Answer came back as the comment, so the model read "The user responded: Answer". Now the flow settles from the picks like the structured settledQuestionStatus path: answered when at least one question was answered (the rest listed in unanswered), cancelled only when none was, and the response carries no comment key. Partial answers no longer dismiss the unanswered questions. "Other (type an answer)" still asks one input for that question's typed answer. An abort stops the flow before its next dialog and settles cancelled (or timed_out when the abort reason is "timed_out"), never answered, keeping the answers given so far.
- Tests cover the dialog count per question, settlement without a comment prompt, that the literal
Answernever becomes the model's comment, partial answers, full cancellation, and aborts (partway, timed out, and already-aborted). docs/rpc.md,CHANGELOG.md, andsrc/modes/rpc/changes.mdare updated.