bus, webui, cli: the Console shows the reply to a web send (#1575)

Row 81. The broker gains a `replies` read verb for human and reader
sessions: `{id}`, a message UUID, returns that business's messages whose
in_reply_to is the id, oldest first, at most 50, and writes no
deliveries row, so the reply still reaches the next `mosaic talk`. An
unknown id refuses message-not-found. The Console serves it through
BUS_VERBS with an id check, and the Sent receipt shows the replies to a
web send. The cli's talk timeout message and README now say the reply
may still come and the next `mosaic talk` prints it.

Dewey's round 2, candidate manifest 3ce7527f, base 1e5f5958, a rebase
after row 61. Approved by Darkwing (27783 round 1, 27821 round 2).
Sage's gate green 19:25:18Z to 19:30:39Z. Live at the next bus restart
(the broker verb) and Console restart (the view), bus first.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-12 14:33:11 -05:00
co-authored by Claude Opus 5.5
parent 9438880e67
commit c9a5650ed0
13 changed files with 455 additions and 30 deletions
+19 -1
View File
@@ -40,6 +40,8 @@ const GATED = new Set([
const CLOSED = "('resolved','withdrawn','expired')";
// How many closed decisions `decisions` with list 'recent' returns, newest close first.
export const RECENT = 50;
// How many replies `replies` returns, oldest first.
export const REPLIES = 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([
@@ -492,7 +494,7 @@ export class Broker {
if (!object(args)) fail('invalid-request');
this.#secretCheck(args);
return this.#store.transaction(() => {
if (s.reader && !['inbox', 'agents', 'tasks', 'trail', 'decisions', 'credentials'].includes(request.verb))
if (s.reader && !['inbox', 'agents', 'tasks', 'trail', 'decisions', 'credentials', 'replies'].includes(request.verb))
fail('read-only');
const result = this.#dispatch(s, request.verb, args);
this.#secretCheck(result);
@@ -906,6 +908,22 @@ export class Broker {
};
});
}
// Row 81 (#1575): the replies to a message, for the Console's web-send receipt. Human and
// reader sessions only, like decisions. It writes no delivery or read row, so a reply it
// lists still reaches the next `mosaic talk`. An id naming no message in the business
// refuses message-not-found, as message.send does for in_reply_to; trail would return [].
case 'replies': {
if (!s.human && !s.reader) fail('human-required');
keys(a, ['id'], ['id']);
identifier(a.id);
if (!this.#store.get('SELECT id FROM messages WHERE id=? AND business=?', a.id, s.business))
fail('message-not-found');
return this.#store.all(
`SELECT * FROM messages WHERE business=? AND in_reply_to=? ORDER BY seq LIMIT ${REPLIES}`,
s.business,
a.id,
);
}
default:
fail('unknown-verb');
}
+1
View File
@@ -7,5 +7,6 @@ export function views(client) {
trail: (subject) => client.call('trail', { subject }),
decisions: (list) => client.call('decisions', { list }),
credentials: () => client.call('credentials'),
replies: (id) => client.call('replies', { id }),
});
}