docs: brief for the decision-close message to the raising role (#1609)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
# 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.resolve` and `decision.withdraw` write the `decision_events`
|
||||
row and, for a human, a `human.input` event `{kind:'answer'}`. They write
|
||||
no message.
|
||||
- `message.receive` reads only `messages` addressed to the role.
|
||||
- `packages/harness/src/runner.mjs` takes a turn only for a message that
|
||||
`message.receive` returns.
|
||||
|
||||
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.mjs`
|
||||
- `packages/bus/tests/` (a new `decision-close.test.mjs`, or changes to
|
||||
`decisions.test.mjs` and `messages.test.mjs`)
|
||||
- `packages/bus/README.md`, the `decision.resolve`, `decision.withdraw` and
|
||||
`message.receive` rows and the decision section
|
||||
- `packages/harness/tests/`, only for the runner case in item 4
|
||||
- `agents/darkwing/work/queue-115/`
|
||||
|
||||
### What ships
|
||||
|
||||
1. `decision.resolve` and `decision.withdraw` send 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_events` row, so
|
||||
a refused or rolled-back close writes no message.
|
||||
- `to_role` is the decision's `raised_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`, `decision` set to the decision id, `in_reply_to`
|
||||
null.
|
||||
- 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 to
|
||||
`from_role` and a refused send leaves the message unread.
|
||||
- Routed with a `routed` delivery like `message.send`'s.
|
||||
- Body, one fixed form the broker writes:
|
||||
`Decision <id> on <action> was resolved by <by>: <choice> (<option text>).`
|
||||
or `Decision <id> on <action> was withdrawn by <by>.`, then
|
||||
`Note: <note>` on its own line when the close had one. `<by>` is what
|
||||
`decision_events.by` holds. Nothing else from the request goes in.
|
||||
2. 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
|
||||
`supersedes` writes.
|
||||
- A within-role decision, which `decision.raise` resolves itself.
|
||||
3. `message.receive` returns `request` for a human close's message: the id
|
||||
of that close's `human.input` answer event. That event keeps its subject
|
||||
(the decision id), so `trail` on the decision is unchanged. This adds no
|
||||
authority. The event is already a human input that `task_create` can
|
||||
cite; the role just learns its id. A role close's message has
|
||||
`request` null.
|
||||
4. The runner needs no change. `turnRequest` already prints `citing
|
||||
decision <id>` and `request <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 from `human` gets a
|
||||
turn whose header cites the decision and the request.
|
||||
5. README: the `decision.resolve` and `decision.withdraw` rows say who gets
|
||||
the message and when none is sent. The `message.receive` row says where
|
||||
a DECISION message's `request` comes from.
|
||||
6. 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.receive` gives `request` equal to
|
||||
the answer event's id.
|
||||
- A cross-role decision resolved by its arbiter role: from that role and
|
||||
run, `request` null.
|
||||
- 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_to` instead of `raised_by_role`; send one on the raising
|
||||
run's own withdraw. Each must fail a test.
|
||||
7. Suites: every package suite (`node --test tests/*.test.mjs` in each
|
||||
`packages/*/`) and every `scripts/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_decision` tool text. It's accurate once this lands.
|
||||
- A push wake for the runner. It polls `message.receive` every
|
||||
`pollInterval` (2 s by default), which is soon enough.
|
||||
- Decision 104, `bus.tester` and 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.
|
||||
Reference in New Issue
Block a user