feat(bus): decisions routed to roles and recent decisions (#1546, row 57)
The broker gains the reader verb `decisions` with `{list: 'roles' |
'recent'}`: the open decisions routed to each role, and the last 50
decisions (RECENT). views.mjs exposes it in the Q1 client module, and
webui serves `GET /api/bus/decisions?list=roles|recent`. Any other list
is 400 and never reaches the bus. No Inbox tab uses the route yet; the
tabs are the design rows' work. The bus and webui READMEs list the verb,
the route and who may call it.
Filbert built it (agents/filbert/work/queue-57). Round 1 approve from
Darkwing (27290) and Dewey (27300) on manifest af3b03cf, 8 paths. Their
non-blocking notes (no seen-then-resolve test, no webui test of ?list=
before the no-bus 503, test 4 compares fewer fields, roles has no LIMIT,
3 of 14 mutants survived) go to a follow-up row.
Sage's gate on 3c79809c, sequential and teed: node suites bus 80 and
webui 40, 0 failed; test-auth 15, test-conductor 17, test-config 24,
test-discord 66, test-extension-package 18, test-foundation 44,
test-queue 27, test-release 14, test-task 98, all 0 failed.
958668a8..3c79809c touches none of the 8 paths.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -38,6 +38,8 @@ const GATED = new Set([
|
||||
'role.revoke',
|
||||
]);
|
||||
const CLOSED = "('resolved','withdrawn','expired')";
|
||||
// How many closed decisions `decisions` with list 'recent' returns, newest close first.
|
||||
export const RECENT = 50;
|
||||
export const TASK_VERBS = Object.freeze(ACTIONS.filter((a) => a.startsWith('task.')));
|
||||
const SELF_KINDS = new Set(['task.created', 'task.assigned', 'task.state', 'task.closed', 'task.conflict']);
|
||||
const POLL_KINDS = new Set([
|
||||
@@ -466,7 +468,8 @@ export class Broker {
|
||||
if (!object(args)) fail('invalid-request');
|
||||
this.#secretCheck(args);
|
||||
return this.#store.transaction(() => {
|
||||
if (s.reader && !['inbox', 'agents', 'tasks', 'trail'].includes(request.verb)) fail('read-only');
|
||||
if (s.reader && !['inbox', 'agents', 'tasks', 'trail', 'decisions'].includes(request.verb))
|
||||
fail('read-only');
|
||||
const result = this.#dispatch(s, request.verb, args);
|
||||
this.#secretCheck(result);
|
||||
return result;
|
||||
@@ -833,6 +836,27 @@ export class Broker {
|
||||
.map((r) => ({ ...r, fields: JSON.parse(r.fields) }));
|
||||
return this.#trail(s, identifier(a.subject));
|
||||
}
|
||||
// Row 57 (#1546): the Inbox's "Routed to roles" and "Recent" lists, for human and
|
||||
// reader sessions only. Each row is what trail gives the same session for that id.
|
||||
case 'decisions': {
|
||||
if (!s.human && !s.reader) fail('human-required');
|
||||
keys(a, ['list'], ['list']);
|
||||
if (a.list === 'roles')
|
||||
return this.#store
|
||||
.all(
|
||||
`SELECT d.* FROM decisions d WHERE business=? AND route_to<>'human' AND NOT EXISTS(SELECT 1 FROM decision_events x WHERE x.decision=d.id AND x.op IN ${CLOSED}) ORDER BY seq`,
|
||||
s.business,
|
||||
)
|
||||
.map((d) => this.#decisionView(d));
|
||||
if (a.list === 'recent')
|
||||
return this.#store
|
||||
.all(
|
||||
`SELECT d.* FROM decisions d JOIN decision_events x ON x.decision=d.id AND x.op IN ${CLOSED} WHERE d.business=? ORDER BY x.seq DESC LIMIT ${RECENT}`,
|
||||
s.business,
|
||||
)
|
||||
.map((d) => ({ ...this.#decisionView(d), closed: this.#closed(d.id) }));
|
||||
fail('invalid-request');
|
||||
}
|
||||
default:
|
||||
fail('unknown-verb');
|
||||
}
|
||||
|
||||
@@ -5,5 +5,6 @@ export function views(client) {
|
||||
tasks: () => client.call('tasks'),
|
||||
agents: () => client.call('agents'),
|
||||
trail: (subject) => client.call('trail', { subject }),
|
||||
decisions: (list) => client.call('decisions', { list }),
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user