6.5 KiB
A closed decision reaches the role that raised it (2026-10-14)
Filed by Sage on Mos's report of 2026-10-14. Gitea #1609, Vikunja #205.
A closed decision sends the raising role a message
Problem
Jason resolved decision 03130d08-ddce-480d-a601-e59f64744fc3 (the PM's
prd.approve for Q139/Q140, gated, routed to human) yes from the CLI at
2026-10-14T16:15:35.512Z. The bus db has the decision_events row (op
resolved, by jason, via cli) and event 346, its human.input answer. No
message cites the decision, and the PM had no turn after it: the newest
message was still 75 (pm to human, 16:12:19Z) when Sage read the db at
16:23Z.
The cause is in packages/bus/src/broker.mjs (cc48b124f, live; refactor is
the same):
decision.resolveanddecision.withdrawwrite thedecision_eventsrow and, for a human, ahuman.inputevent{kind:'answer'}. They write no message.message.receivereads onlymessagesaddressed to the role.packages/harness/src/runner.mjstakes a turn only for a message thatmessage.receivereturns.
So a role learns an answer only when some unrelated message gives it a
turn. raise_decision's tool text (packages/harness/src/tools.mjs:135)
already says "The answer arrives as a message." This row makes the broker
keep that promise, for every routed decision.
The fix goes in the broker, not the runner. A message is what every
transport already receives, routes to the role's current holder, and
strands visibly in agents when nobody reads it. A second wake path in the
runner would need its own delivery record.
Owner and reviewer
- Owner: darkwing, after row 113.
- Reviewers: filbert (decision 130, not a sandbox row) and sage.
Files owned
packages/bus/src/broker.mjspackages/bus/tests/(a newdecision-close.test.mjs, or changes todecisions.test.mjsandmessages.test.mjs)packages/bus/README.md, thedecision.resolve,decision.withdrawandmessage.receiverows and the decision sectionpackages/harness/tests/, only for the runner case in item 4agents/darkwing/work/queue-115/
What ships
decision.resolveanddecision.withdrawsend the raising role a message when someone other than the raising run closes the decision:- It's written in the same transaction as the
decision_eventsrow, so a refused or rolled-back close writes no message. to_roleis the decision'sraised_by_role. It's the role, not the run, because runs end. When no run holds the role, the message waits for the next claim like any other.- Class
DECISION,decisionset to the decision id,in_reply_tonull. - Sender: a human close is stored
from_role 'human',from_run 'human-cli'. A role close is stored as that role and run. The sender must be a real address, because the runner sends its answer tofrom_roleand a refused send leaves the message unread. - Routed with a
routeddelivery likemessage.send's. - Body, one fixed form the broker writes:
Decision <id> on <action> was resolved by <by>: <choice> (<option text>).orDecision <id> on <action> was withdrawn by <by>., thenNote: <note>on its own line when the close had one.<by>is whatdecision_events.byholds. Nothing else from the request goes in.
- It's written in the same transaction as the
- No message in these cases:
decision.seen. It's an acknowledgement, not a close.- A withdraw by the raising run itself, including the withdraw that
supersedeswrites. - A within-role decision, which
decision.raiseresolves itself.
message.receivereturnsrequestfor a human close's message: the id of that close'shuman.inputanswer event. That event keeps its subject (the decision id), sotrailon the decision is unchanged. This adds no authority. The event is already a human input thattask_createcan cite; the role just learns its id. A role close's message hasrequestnull.- The runner needs no change.
turnRequestalready printsciting decision <id>andrequest <id>. The role's answer to the DECISION message goes back to its sender as RESULT, so Jason sees what the role did with their answer, and a resolving role gets one RESULT and sends none back. One harness test shows a DECISION message fromhumangets a turn whose header cites the decision and the request. - README: the
decision.resolveanddecision.withdrawrows say who gets the message and when none is sent. Themessage.receiverow says where a DECISION message'srequestcomes from. - Tests, offline as today:
- Human resolve: exactly one message to the raising role, class
DECISION, the decision id, from
human/human-cli, the choice and option text in the body;message.receivegivesrequestequal to the answer event's id. - A cross-role decision resolved by its arbiter role: from that role and
run,
requestnull. - Human withdraw: one message, saying withdrawn, with the note.
- Raising run's withdraw, a supersede, a within-role raise and
decision.seen: no message. - Refused closes (
invalid-choice,decision-closed,not-resolver): no message. - Raising role unheld at close: the message is routed with no holder and
delivered to the next claim's
message.receive. - Mutants, each recorded in the packet: drop the message insert; address
it to
route_toinstead ofraised_by_role; send one on the raising run's own withdraw. Each must fail a test.
- Human resolve: exactly one message to the raising role, class
DECISION, the decision id, from
- Suites: every package suite (
node --test tests/*.test.mjsin eachpackages/*/) and everyscripts/test-*.sh, on the packet gate.
Out of scope
- Backfill. On 2026-10-14 decision 03130d08 was the only closed decision in the live db, and Mos unblocks it by hand. The broker writes messages only for closes after this lands.
- The runner, the notifier, the Console Inbox and the Discord connector.
- The
raise_decisiontool text. It's accurate once this lands. - A push wake for the runner. It polls
message.receiveeverypollInterval(2 s by default), which is soon enough. - Decision 104,
bus.testerand the C-suite rows, held for Q135.
Gate
Filbert and Sage approve on #1609 naming the candidate manifest. Sage
reruns the full gate at the landing target. Mos gets notices before and
after the landing (decision 109). It goes live only through
stack-copy.sh copy, a re-pin and a bus restart on Mos's go. After the
restart, the next human close of a PM decision should show the DECISION
message in messages and a PM turn after it.