diff --git a/docs/plans/2026-10-14_decision-close-message.md b/docs/plans/2026-10-14_decision-close-message.md new file mode 100644 index 000000000..1dedf1fb7 --- /dev/null +++ b/docs/plans/2026-10-14_decision-close-message.md @@ -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 on was resolved by : (