fix(rpc): the degraded question asks no trailing "Anything else?" prompt

Merged
CO
2d ago
+149-11

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 answered with no optional-comment prompt
  • an input answered with the literal Answer can never become the comment: formatModelAnswer text is q1: 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 cancelled and the model read only "The user dismissed the question.". It now settles answered and formatModelAnswer gives q1: A\nUnanswered: q2; nothing answered stays cancelled.
  • LOW 1: the comment key is left out instead of comment: undefined (asserted with toStrictEqual / "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 check exit 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 settled answered. The fallback now opens no dialog after an abort and settles cancelled (or timed_out with autoResolvedAfterMs for a "timed_out" abort reason), matching the structured bridge, keeping the partial answers in answers and the rest in unanswered.
  • 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 check exit 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 0
  • node 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 Answer never becomes the model's comment, partial answers, full cancellation, and aborts (partway, timed out, and already-aborted).
  • docs/rpc.md, CHANGELOG.md, and src/modes/rpc/changes.md are updated.