Skip to content

Withdraw the parent's linked question once courier_answer answers it - #55

Merged
ivopogace merged 6 commits into
mainfrom
issue-54-withdraw-linked-question
Oct 6, 2026
Merged

ivopogace merged 6 commits into
mainfrom
issue-54-withdraw-linked-question

Conversation

@ivopogace

@ivopogace ivopogace commented Oct 6, 2026 •

Copy link
Copy Markdown
Owner

Closes #54

A parent that asks the person a child's question with its question tool and calls courier_answer in the same step (two parallel tool calls, a model slip) left its own question open, and the person's pick there was dropped with a note that said only "nothing was passed on". This implements the decision on the issue: option 1 plus option 2's message.

What changed

  • Any answer claimed for a question withdraws the top session's call linked to it. Done in passOn, right after the answer is claimed, so it covers both paths: the child's waiting call (ended with { by: "top" }) and the cut-off call, whose answer goes out as a message. In the message path the link goes only once the message is out, so a failed send still leaves the parent's question open and the question answerable again.
  • The withdrawn result names the answers: Session … no longer waits on this question: it was already answered with courier_answer (User has answered your questions: …). There is nothing to pass on; tell the person it is settled. It returns those answers as output.answers with metadata.withdrawn: true, like the other withdrawn results.
  • Option 2's message, for the race withdrawing cannot close (a pick that lands just as courier_answer goes through): passedNote(undefined) now says … it had already been answered or settled, so these answers were not passed on; tell the person so. instead of nothing was passed on.
  • The dropped dismissal is logged: linkedCallFailed → deliver returning undefined now logs courier question: the dismissal of … was not passed on: it had already been answered or settled.
  • Out of scope, as decided: naming both answers when they differ, and refusing courier_answer while a linked call is open.
  • Also in this PR, at the owner's request in the session: .claude/settings.json enables the code-review plugin at project scope, copied from riviera-sunbed-booking, so the review gate has it in later sessions. Unrelated to A courier_answer leaves the parent's linked question open, and the person's pick is dropped #54.

Decisions on the "your call" points

  • link gets a wider type of its own, Withdrawal = Elsewhere | { by: "top"; outcome: Outcome }, rather than a new Elsewhere variant: settledHow/settledNotice never see the new case, as the research suggested.
  • The top variant carries the whole Outcome, not just answers. A dismissal can reach passOn with a link set too: the person dismisses the parent's linked question, the dismissal is held for the two-second grace, the parent asks again within it (linked anew), and the held dismissal then ends the child's call. That second question is withdrawn as well (… it was already dismissed in your session, so it carries on without the answers), instead of staying open for a pick that would be dropped. A unit test and a live-test scenario cover it, and docs/reference.md has a clause on it.
  • withdrawnResult is split into withdrawnHow (the wording and the answers that count, by variant) and the result itself, to keep each small.

Tests

  • test/question.test.ts: the test from Work down the SonarCloud findings in src/question.ts #51 that pinned the old behaviour is rewritten to expect the withdrawn result naming the answer, both forms cancelled, nothing stored, and a second courier_answer returning answered: false. New tests: a cut-off child with a linked parent plus courier_answer (the parent's call withdrawn, the child gets exactly one message); a pick that lands while courier_answer's answer is still going out (the new note, the pick not passed on); a dismissal that lands after courier_answer (dropped and logged); the dismissal-during-grace relink above.
  • e2e/mock-model.mjs can now stream several tool calls in one step (calls), and COURIER-QUESTION-BOTH makes the parent emit question and courier_answer together. The new scenario in e2e/run.sh checks that the parent was told, its question result carries the withdrawn text with the answers, courier_answer passed them on, the parent's form is gone unanswered, and the child carried on with the courier_answer pick. A second scenario dismisses the parent's question and prompts it over the API to ask again (COURIER-ASK-AGAIN) inside the two seconds the dismissal is held: the new question is withdrawn, and the child reports that it was dismissed.
  • docs/reference.md gets the bullet in the "whichever answer comes first" list and the clause on the held dismissal; README's relay summary says whichever answer reaches the child first counts, by courier_answer too.
  • Real-model smoke test, COURIER_SCENARIO=question on OpenCode Zen (longcat-2.5-preview-free, opencode v2.0.23): all six checks passed, linked; recorded in docs/real-model.md. The reworded results are rare-path ones the scenario does not reach.

Verification on the head: bun run typecheck, bun test (240 pass), bun run build, e2e/run.sh on @opencode/cli@2.0.23: all 149 checks pass.

Review round

The code-review plugin's procedure was run on the PR (five independent reviewers, then confidence scoring; the plugin itself could not be loaded into the running session, so its command was followed by hand). No defect in the change; four low-confidence findings (50 to 75, under the plugin's reporting bar), all addressed: the "not passed on" note and the dismissal log say "answered or settled", since deliver also returns nothing for a question settled some other way; the held-dismissal withdrawal is documented and has a live-test scenario; the real-model run is recorded; the settings file is intentional (above).

🤖 Generated with Claude Code

https://claude.ai/code/session_01QbKM8QcDsNCdyc8m3XvxtK

claude added 6 commits October 6, 2026 20:52
A parent that asks the person a child's question and calls courier_answer in
the same step left its own question open, and the person's pick there was
dropped without a word. Now any answer claimed for a question withdraws the
top session's call linked to it, whether the child's call waits or was cut
off and gets the answer as a message. The withdrawn result names the answers
that went out and returns them as output.answers, with metadata.withdrawn.

For the one race withdrawing cannot close, a pick that lands just as the
answer goes through, the parent's tool result now says the question had
already been answered and the pick was not passed on, and a dismissal that
lands then is logged instead of dropped silently.

The mock model of the live test can stream two tool calls in one step, and
COURIER-QUESTION-BOTH makes the parent slip that way.

Closes #54

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QbKM8QcDsNCdyc8m3XvxtK
The child's form goes within a moment of the notice, so polling for it raced.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QbKM8QcDsNCdyc8m3XvxtK
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QbKM8QcDsNCdyc8m3XvxtK
…ment the held dismissal

From the review: deliver also returns nothing when the question was settled
some other way, and the second question a held dismissal withdraws was not
in docs/reference.md.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QbKM8QcDsNCdyc8m3XvxtK
Copied from riviera-sunbed-booking's .claude/settings.json: the review gate
runs the plugin on the PR number, so a session in this repository has it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QbKM8QcDsNCdyc8m3XvxtK
COURIER-ASK-AGAIN makes the mock parent ask the last question it was told of
once more; prompted over the API within the two seconds a dismissal is held,
its new question is linked, then withdrawn when the dismissal goes through.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QbKM8QcDsNCdyc8m3XvxtK

Copy link
Copy Markdown
Owner Author

Code review

No issues found. Checked for bugs and CLAUDE.md compliance.

Procedure of the code-review plugin, run by hand on head e2183e3: five independent reviewers (CLAUDE.md adherence, a shallow bug scan, git history, earlier pull requests' comments, code comments), then a confidence score per finding. Four findings, all below the reporting bar (50 to 75), were addressed anyway on this head: the note for a pick that was not passed on now says "answered or settled" (

if (passed === "message")
return `These answers were passed on to session ${linked.sessionID} as a message, since its question had been cut off; it carries on with them. Do not call courier_answer for ${linked.requestID}. ${END_TURN}`
if (passed === undefined)
return `Session ${linked.sessionID} no longer waits on this question: it had already been answered or settled, so these answers were not passed on; tell the person so.`
return `Passing these answers on to session ${linked.sessionID} failed (${passed.slice(7)}); call courier_answer with requestID "${linked.requestID}" to pass them on.`
), the second question a held dismissal withdraws is documented (
- answered in the parent's session: the child's question disappears;
- dismissed in the parent's session: the parent's turn ends, as OpenCode's tool does, and the
child's call returns that you dismissed the question, so the child carries on without the answer
and can report. Should the parent ask you again within the two seconds the dismissal is held
(below), that question is withdrawn too, saying the child carries on without the answers;
) and has a live-test scenario (

opencode-courier/e2e/run.sh

Lines 293 to 303 in e2183e3

ask_question COURIER-QUESTION
check "dismissed in the parent's session" "$([ "$(dismiss_form "$parent" "$parent_form")" = 204 ] && echo true || echo false)"
# Within the two seconds the dismissal is held, over the API as a prompt from the TUI would come
# (opencode run cancels the question form it opens).
check "the parent is prompted to ask again" "$([[ $(person "session/$parent/prompt" '{"text":"COURIER-ASK-AGAIN"}') == 20* ]] && echo true || echo false)"
check "the parent's new question was withdrawn, saying the child carries on without the answers" \
"$(has_text "$parent" "already dismissed in your session, so it carries on without the answers" 45)"
check "and no longer shown" "$([ "$(forms_of "$parent")" = 0 ] && echo true || echo false)"
check "the child was told, carried on and reported" "$(has_text "$parent" "CHILD DISMISSED" 45)"
# A model slip: the parent asks the person and calls courier_answer in the same step, so its
), the real-model question run after the result wording changed is recorded (
## On OpenCode 2.0.23, after #55
On 2026-10-06, with `opencode v2.0.23` and `longcat-2.5-preview-free`, after the change that
withdraws the parent's linked question once `courier_answer` answers it (#55), which reworded the
results of a withdrawn question and of a pick that was not passed on: the question relay passed
all six checks, linked (the parent's form held the child's three greetings), 5 seconds from the
person's answer to the child's report. The parent asked by form and did not call `courier_answer`,
so the new results were not reached; the linked path's result, unchanged, was.
), and the project settings file enabling the plugin is in the PR on purpose.

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.


Generated by Claude Code

@sonarqubecloud

sonarqubecloud Bot commented Oct 6, 2026

Copy link
Copy Markdown

@ivopogace
ivopogace merged commit 97a0b6e into main Oct 6, 2026
7 checks passed
@ivopogace
ivopogace deleted the issue-54-withdraw-linked-question branch October 6, 2026 21:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A courier_answer leaves the parent's linked question open, and the person's pick is dropped

2 participants