bus: a closed decision sends the raising role a message (#1609)

Row 115. When a decision closes by resolve or withdraw, and the closer is
a human or another run, the broker routes a DECISION message to the
raising role's holder: "Decision <id> on <action> was resolved by <by>:
<choice> (<text>)" or "withdrawn by <by>", plus the note if there is one.
A seen op sends nothing. #request falls back to the human.input that
closed the decision, filtered to resolved and withdrawn.

Candidate manifest 1910eb5a672e670cb7c8a197ee3fdd888412dd803fe52a0a15680103c26c078c
from Darkwing's round 2 packet (40e5917f9), on BASE 94ece491f. Filbert
approved in 28428 and Sage in 28427. Sage reran the full gate at the
landing target with the candidate applied.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-14 12:09:43 -05:00
co-authored by Claude Opus 5.5
parent 2330074bbe
commit d90482030a
5 changed files with 303 additions and 13 deletions
+43 -9
View File
@@ -889,6 +889,30 @@ export class Broker {
{ kind: 'answer', decision: d.id, op, choice: a.choice ?? null },
d.id,
);
// Row 115 (#1609): a close by anyone but the raising run tells the raising role, routed like message.send's.
if (op !== 'seen' && (s.human || s.run !== d.raised_by_run)) {
const outcome =
op === 'resolved'
? `resolved by ${by}: ${a.choice} (${JSON.parse(d.options).find((o) => o.key === a.choice).text})`
: `withdrawn by ${by}`;
const mid = randomUUID();
this.#store.run(
'INSERT INTO messages(id,at,business,from_role,from_run,to_role,class,in_reply_to,decision,corrects,body) VALUES(?,?,?,?,?,?,?,?,?,?,?)',
mid,
this.#now(),
s.business,
s.human ? 'human' : s.role,
s.human ? 'human-cli' : s.run,
d.raised_by_role,
'DECISION',
null,
d.id,
null,
`Decision ${d.id} on ${d.action} was ${outcome}.${a.note === undefined ? '' : `\nNote: ${a.note}`}`,
);
const h = this.#holder(s.business, d.raised_by_role);
this.#delivery(mid, 'routed', h?.op === 'claim' ? h : null);
}
return { id: d.id, op };
}
case 'message.send': {
@@ -978,15 +1002,7 @@ export class Broker {
s.human || s.tester ? null : this.#holder(s.business, s.role),
s.human || s.tester ? 'cli' : 'broker',
);
return rows.map((m) => ({
...m,
request:
this.#store.get(
"SELECT id FROM events WHERE business=? AND kind='human.input' AND subject=? ORDER BY seq LIMIT 1",
s.business,
m.id,
)?.id ?? null,
}));
return rows.map((m) => ({ ...m, request: this.#request(s.business, m) }));
}
case 'message.read': {
keys(a, ['id'], ['id']);
@@ -1292,6 +1308,24 @@ export class Broker {
decision,
);
}
// A message's originating human.input: a human instruction's own, or for the
// message a human decision close sends (row 115, #1609), the close's answer.
// A decision has one close, so the answer is unique.
#request(business, m) {
const own = this.#store.get(
"SELECT id FROM events WHERE business=? AND kind='human.input' AND subject=? ORDER BY seq LIMIT 1",
business,
m.id,
);
if (own || m.class !== 'DECISION' || m.from_role !== 'human' || !m.decision) return own?.id ?? null;
return (
this.#store.get(
"SELECT id FROM events WHERE business=? AND kind='human.input' AND subject=? AND json_extract(body,'$.kind')='answer' AND json_extract(body,'$.op') IN ('resolved','withdrawn') ORDER BY seq LIMIT 1",
business,
m.decision,
)?.id ?? null
);
}
// The message a review verb sends, routed like message.send's.
#reviewMessage(s, mid, { to, cls, in_reply_to = null, decision = null, body }) {
this.#store.run(