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