@dispatch-foo/protocol 0.0.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2490 @@
1
+ import { c as MAX_ADDRESS_BYTES, d as SYSTEM_ADDRESS, f as gateTypeOf, h as raiserOf, l as MAX_SEGMENT_BYTES, m as isDecidingAuthor, n as BUILT_IN_KINDS, o as GATE_TYPES, p as hasGateData, r as DELIVERY_STATES, s as MARKERS, u as REF_TYPES } from "./constants-CRXF4f7g.js";
2
+ import { ActorRefError, TASK_ID_PATTERN, dbVersion, openSqliteDb, parseActorRef, queryAll, queryOne } from "@dispatch-foo/core";
3
+
4
+ //#region src/errors.ts
5
+ var MessagingError = class extends Error {
6
+ constructor(code, message, field) {
7
+ super(message);
8
+ this.code = code;
9
+ this.field = field;
10
+ this.name = "MessagingError";
11
+ }
12
+ };
13
+
14
+ //#endregion
15
+ //#region src/address.ts
16
+ /** An outbound A2A peer's alias: the handle grammar, at most 40 characters. */
17
+ const PEER_ALIAS_PATTERN = /^[a-z0-9][a-z0-9._-]{0,39}$/;
18
+ const RUN_ID = /^r-[0-9a-f]{6,12}$/;
19
+ const CHANNEL_NAME = /^[a-z0-9][a-z0-9._-]*(\/[a-z0-9][a-z0-9._-]*)*$/;
20
+ const utf8Length = (s) => new TextEncoder().encode(s).byteLength;
21
+ function parseAddress(raw, field = "to") {
22
+ if (typeof raw !== "string") throw new MessagingError("invalid", "expected an address string", field);
23
+ const bad = (why) => {
24
+ throw new MessagingError("invalid", `invalid address ${JSON.stringify(raw.slice(0, 80))}: ${why}`, field);
25
+ };
26
+ if (utf8Length(raw) > MAX_ADDRESS_BYTES) return bad(`at most ${MAX_ADDRESS_BYTES} bytes`);
27
+ const segment = (value, what) => {
28
+ if (utf8Length(value) > MAX_SEGMENT_BYTES) bad(`${what} is over ${MAX_SEGMENT_BYTES} bytes`);
29
+ };
30
+ const colon = raw.indexOf(":");
31
+ if (colon <= 0) return bad("expected <kind>:<id>");
32
+ const scheme = raw.slice(0, colon);
33
+ const rest = raw.slice(colon + 1);
34
+ switch (scheme) {
35
+ case "human":
36
+ case "agent": {
37
+ let ref;
38
+ try {
39
+ ref = parseActorRef(raw);
40
+ } catch (err) {
41
+ if (err instanceof ActorRefError) return bad(err.message);
42
+ throw err;
43
+ }
44
+ if (ref === null || ref.handle === null) return bad("actor needs a handle");
45
+ segment(ref.handle, "handle");
46
+ if (ref.operator !== null) segment(ref.operator, "operator");
47
+ return ref.kind === "human" ? {
48
+ kind: "human",
49
+ handle: ref.handle,
50
+ address: raw
51
+ } : {
52
+ kind: "agent",
53
+ handle: ref.handle,
54
+ operator: ref.operator,
55
+ address: raw
56
+ };
57
+ }
58
+ case "task":
59
+ segment(rest, "id");
60
+ return TASK_ID_PATTERN.test(rest) ? {
61
+ kind: "task",
62
+ id: rest,
63
+ address: raw
64
+ } : bad("not a task id");
65
+ case "run":
66
+ segment(rest, "id");
67
+ return RUN_ID.test(rest) ? {
68
+ kind: "run",
69
+ id: rest,
70
+ address: raw
71
+ } : bad("not a run id");
72
+ case "channel":
73
+ for (const part of rest.split("/")) segment(part, "channel segment");
74
+ return CHANNEL_NAME.test(rest) ? {
75
+ kind: "channel",
76
+ name: rest,
77
+ address: raw
78
+ } : bad("not a channel name");
79
+ case "a2a":
80
+ segment(rest, "alias");
81
+ return PEER_ALIAS_PATTERN.test(rest) ? {
82
+ kind: "a2a",
83
+ alias: rest,
84
+ address: raw
85
+ } : bad("not an A2A peer alias (a-z, 0-9, \".\", \"_\", \"-\"; at most 40)");
86
+ default: return bad(`unknown kind ${JSON.stringify(scheme)}`);
87
+ }
88
+ }
89
+ /** An outbound A2A peer (`a2a:<alias>`); every message it sends is recorded by the daemon. */
90
+ function isPeerAddress(address) {
91
+ return address.startsWith("a2a:");
92
+ }
93
+ /** True for senders the ping-pong breaker counts: runs, agents and A2A peers, not the system. */
94
+ function isAgentAuthored(address) {
95
+ if (address === SYSTEM_ADDRESS) return false;
96
+ return address.startsWith("run:") || address.startsWith("agent:") || isPeerAddress(address);
97
+ }
98
+
99
+ //#endregion
100
+ //#region src/lines.ts
101
+ const LINE_BREAK = /\r\n|[\n\v\f\r\u0085\u2028\u2029]/;
102
+
103
+ //#endregion
104
+ //#region src/envelope.ts
105
+ const MEMORY_GATE_KINDS = [
106
+ "preference",
107
+ "convention",
108
+ "constraint",
109
+ "hazard",
110
+ "decision",
111
+ "fact",
112
+ "reference"
113
+ ];
114
+ const MEMORY_GATE_ACTIONS = [
115
+ "add",
116
+ "supersede",
117
+ "retire"
118
+ ];
119
+ const PROPOSAL_ID = /^mp-[0-9A-HJKMNP-TV-Z]{26}$/;
120
+ const PACKAGE_GATE_TYPES = new Set(GATE_TYPES);
121
+ const X_KIND = /^x-[a-z0-9][a-z0-9-]*$/;
122
+ const IDENTIFIER = /^[a-z0-9][a-z0-9._-]*$/;
123
+ const ASKING_KINDS = new Set(["question", "handoff"]);
124
+ const MAX_BODY_BYTES = 64 * 1024;
125
+ const MAX_DATA_BYTES = 64 * 1024;
126
+ const MAX_RECIPIENTS = 50;
127
+ const MAX_REFS = 50;
128
+ const MAX_CHOICES = 20;
129
+ const MAX_REF_BYTES = 512;
130
+ const MAX_LABEL_BYTES = 200;
131
+ /** The gate payload a message carries, or null when `data` is not a gate. */
132
+ function gateOf(message) {
133
+ const data = message.data;
134
+ if (data === null || typeof data !== "object" || Array.isArray(data)) return null;
135
+ const type = data["type"];
136
+ return typeof type === "string" && GATE_TYPES.includes(type) ? data : null;
137
+ }
138
+ function isSystemMarker(message, type) {
139
+ const data = message.data;
140
+ return message.from === SYSTEM_ADDRESS && message.origin === void 0 && typeof data === "object" && data !== null && !Array.isArray(data) && data["type"] === type;
141
+ }
142
+ function invalid(field, why) {
143
+ throw new MessagingError("invalid", `${field}: ${why}`, field);
144
+ }
145
+ /** §1.4's identifier: lowercase, no line breaks, at most one segment's bytes. */
146
+ function isIdentifier(value) {
147
+ return typeof value === "string" && IDENTIFIER.test(value) && value.length <= MAX_SEGMENT_BYTES;
148
+ }
149
+ function overBytes(text, max) {
150
+ if (text.length > max) return true;
151
+ if (text.length * 3 <= max) return false;
152
+ return new TextEncoder().encode(text).byteLength > max;
153
+ }
154
+ function singleLine(value, field, maxBytes) {
155
+ if (typeof value !== "string") return;
156
+ if (LINE_BREAK.test(value)) invalid(field, "must not contain line breaks");
157
+ if (overBytes(value, maxBytes)) invalid(field, `at most ${maxBytes} bytes (UTF-8)`);
158
+ }
159
+ function checkIdempotencyKey(key) {
160
+ if (key === "") invalid("idempotencyKey", "must not be empty");
161
+ singleLine(key, "idempotencyKey", MAX_LABEL_BYTES);
162
+ }
163
+ function validateGate(input, sender, canDecide, known) {
164
+ const type = input.data.type;
165
+ if (!ASKING_KINDS.has(input.kind)) invalid("data.type", "gate data travels only on questions and handoffs; private payloads use an x- type");
166
+ if (!known.has(type)) invalid("data.type", `unregistered or unimplemented gate type ${type}; private payloads use an x- type`);
167
+ if (type === "memory") validateMemoryShape(input);
168
+ if (type === "doc") validateDocShape(input);
169
+ const raiser = raiserOf(type);
170
+ if (raiser === "session" && !sender.startsWith("run:")) throw new MessagingError("forbidden", `only runs may request ${type}`, "data");
171
+ if (raiser === "system" && sender !== SYSTEM_ADDRESS) throw new MessagingError("forbidden", `only Dispatch may raise ${type} gates`, "data");
172
+ if (raiser === "system-or-decider" && sender !== SYSTEM_ADDRESS && !(canDecide && sender.startsWith("human:"))) throw new MessagingError("forbidden", `only Dispatch may raise ${type} gates, or a deciding human`, "data");
173
+ if (type === "scope") validateScopeShape(input);
174
+ }
175
+ function validateScopeShape(input) {
176
+ const gate = input.data;
177
+ if (!Array.isArray(gate.paths) || gate.paths.length === 0 || !gate.paths.every((p) => typeof p === "string" && p !== "")) invalid("data.paths", "expected a non-empty list of paths");
178
+ if (typeof gate.reason !== "string" || gate.reason.trim() === "") invalid("data.reason", "required");
179
+ if (input.kind !== "question" || input.blocking !== true || JSON.stringify(input.choices) !== "[\"grant\",\"deny\"]") invalid("data", "a scope request is { kind: \"question\", blocking: true, choices: [\"grant\", \"deny\"], data: { type: \"scope\", paths, reason } }");
180
+ }
181
+ function validateMemoryShape(input) {
182
+ const gate = input.data;
183
+ if (typeof gate.proposalId !== "string" || !PROPOSAL_ID.test(gate.proposalId)) invalid("data.proposalId", "expected a proposal id like mp-01K…");
184
+ if (!MEMORY_GATE_ACTIONS.includes(gate.action)) invalid("data.action", `expected ${MEMORY_GATE_ACTIONS.join("|")}`);
185
+ if (gate.scope !== "project" && gate.scope !== "team") invalid("data.scope", "expected project|team");
186
+ if (!MEMORY_GATE_KINDS.includes(gate.kind)) invalid("data.kind", `expected ${MEMORY_GATE_KINDS.join("|")}`);
187
+ if (input.kind !== "question" || input.blocking !== true || JSON.stringify(input.choices) !== "[\"approve\",\"reject\"]") invalid("data", "a memory gate is { kind: \"question\", blocking: true, choices: [\"approve\", \"reject\"], data: { type: \"memory\", proposalId, action, scope, kind } }");
188
+ }
189
+ function validateDocShape(input) {
190
+ const gate = input.data;
191
+ if (typeof gate.doc !== "string" || !gate.doc.startsWith("doc-")) invalid("data.doc", "expected a doc- id");
192
+ if (typeof gate.proposal !== "string" || !gate.proposal.startsWith("rev-")) invalid("data.proposal", "expected a rev- id");
193
+ if (input.kind !== "question" || input.blocking !== true || JSON.stringify(input.choices) !== "[\"approve\",\"reject\"]") invalid("data", "a doc gate is { kind: \"question\", blocking: true, choices: [\"approve\", \"reject\"], data: { type: \"doc\", doc, proposal } }");
194
+ }
195
+ function validateSendInput(input, sender, canDecide, replyTarget, options = {}) {
196
+ const known = options.gateTypes ?? PACKAGE_GATE_TYPES;
197
+ if (typeof input !== "object" || input === null || Array.isArray(input)) invalid("input", "expected an object");
198
+ if (input.refs !== void 0 && !Array.isArray(input.refs)) invalid("refs", "expected a list");
199
+ if (input.choices !== void 0 && (!Array.isArray(input.choices) || input.choices.some((c) => typeof c !== "string"))) invalid("choices", "expected a list of strings");
200
+ if (input.replyTo !== void 0 && input.replyTo !== null && typeof input.replyTo !== "string") invalid("replyTo", "expected a message id or null");
201
+ if (!Array.isArray(input.to) || input.to.length === 0) invalid("to", "at least one recipient");
202
+ if (input.to.length > MAX_RECIPIENTS) invalid("to", `at most ${MAX_RECIPIENTS} recipients`);
203
+ input.to.forEach((addr, i) => parseAddress(addr, `to[${i}]`));
204
+ const kind = input.kind;
205
+ if (typeof kind !== "string") invalid("kind", "expected a string");
206
+ if (!BUILT_IN_KINDS.includes(kind) && !X_KIND.test(kind)) invalid("kind", `unknown kind ${JSON.stringify(kind)}`);
207
+ const hasChoice = typeof input.choice === "string";
208
+ if (typeof input.body !== "string" || input.body.trim() === "" && !(kind === "answer" && hasChoice)) invalid("body", "required");
209
+ if (overBytes(input.body, MAX_BODY_BYTES)) invalid("body", `at most ${MAX_BODY_BYTES} bytes (UTF-8)`);
210
+ if (input.data !== void 0 && overBytes(JSON.stringify(input.data), MAX_DATA_BYTES)) invalid("data", `at most ${MAX_DATA_BYTES} bytes as JSON`);
211
+ singleLine(input.session, "session", MAX_LABEL_BYTES);
212
+ if (input.idempotencyKey !== void 0) checkIdempotencyKey(input.idempotencyKey);
213
+ const refs = input.refs ?? [];
214
+ if (refs.length > MAX_REFS) invalid("refs", `at most ${MAX_REFS} refs`);
215
+ refs.forEach((ref, i) => {
216
+ if (typeof ref !== "object" || ref === null) invalid(`refs[${i}]`, "expected { type, id }");
217
+ const registered = REF_TYPES.includes(ref.type);
218
+ const receivedOk = options.origin === "received" && isIdentifier(ref.type);
219
+ if (!registered && !receivedOk) invalid(`refs[${i}].type`, "unknown ref type");
220
+ if (typeof ref.id !== "string" || ref.id === "") invalid(`refs[${i}].id`, "required");
221
+ singleLine(ref.id, `refs[${i}].id`, MAX_REF_BYTES);
222
+ singleLine(ref.at, `refs[${i}].at`, MAX_REF_BYTES);
223
+ });
224
+ const asking = ASKING_KINDS.has(kind);
225
+ if (input.blocking === true && !asking) invalid("blocking", "only questions and handoffs block");
226
+ if (input.choices !== void 0) {
227
+ if (!asking) invalid("choices", "only questions and handoffs carry choices");
228
+ const choices = input.choices;
229
+ if (choices.length > MAX_CHOICES) invalid("choices", `at most ${MAX_CHOICES} choices`);
230
+ choices.forEach((c, i) => singleLine(c, `choices[${i}]`, MAX_LABEL_BYTES));
231
+ if (choices.length === 0 || new Set(choices).size !== choices.length || choices.some((c) => c.trim() === "")) invalid("choices", "expected distinct, non-empty choices");
232
+ }
233
+ if (hasChoice && kind !== "answer") invalid("choice", "only answers carry a choice");
234
+ singleLine(input.choice, "choice", MAX_LABEL_BYTES);
235
+ const replyTo = input.replyTo ?? null;
236
+ if (kind === "answer" && replyTo === null) invalid("replyTo", "an answer needs the question id");
237
+ if (replyTo !== null) {
238
+ if (replyTarget === null) {
239
+ if (options.parentOptional !== true) throw new MessagingError("not-found", `no message ${replyTo}`, "replyTo");
240
+ } else if (kind === "answer") {
241
+ if (!ASKING_KINDS.has(replyTarget.kind)) invalid("replyTo", "only questions and handoffs take answers");
242
+ const isGate = gateTypeOf(replyTarget, known) !== null;
243
+ if (isGate && !canDecide) throw new MessagingError("forbidden", "answering this gate needs the decide tier", "replyTo");
244
+ const choices = replyTarget.choices ?? [];
245
+ if ((isGate || replyTarget.kind === "handoff") && choices.length > 0 && !hasChoice) invalid("choice", `choose one of ${choices.join(", ")}`);
246
+ if (hasChoice && !choices.includes(input.choice)) invalid("choice", `choose one of ${choices.join(", ")}`);
247
+ }
248
+ }
249
+ if (hasGateData(input)) validateGate(input, sender, canDecide, known);
250
+ }
251
+
252
+ //#endregion
253
+ //#region src/localOnly.ts
254
+ /** Markers that never leave the machine: the registry's plus the policy engine's. */
255
+ const LOCAL_ONLY_MARKERS = [...new Set([
256
+ ...MARKERS,
257
+ "x-policy",
258
+ "x-expired"
259
+ ])];
260
+ const OVERSEER = /^agent:[^/]+\/overseer$/;
261
+ const A2A_AGENT = /^agent:[^/]+\/a2a\./;
262
+ /** Overseer and A2A identities: never federated, never in channel or agent ops. */
263
+ function isFederationLocalAddress(address) {
264
+ return address.startsWith("a2a:") || OVERSEER.test(address) || A2A_AGENT.test(address);
265
+ }
266
+ function dataType(message) {
267
+ const data = message.data;
268
+ if (data === null || data === void 0 || typeof data !== "object" || Array.isArray(data)) return null;
269
+ const type = data.type;
270
+ return typeof type === "string" ? type : null;
271
+ }
272
+ function localOnlyReason(message, replyTarget, root) {
273
+ if (hasGateData(message) || replyTarget !== null && hasGateData(replyTarget)) return "gate";
274
+ const type = dataType(message);
275
+ if (type !== null && LOCAL_ONLY_MARKERS.includes(type)) return "marker";
276
+ if ([message.from, ...message.to].some(isFederationLocalAddress)) return "participant";
277
+ if (root !== null && root.id !== message.id && localOnlyReason(root, null, null) !== null) return "root";
278
+ return null;
279
+ }
280
+
281
+ //#endregion
282
+ //#region src/render.ts
283
+ const DIGEST_WIDTH = 80;
284
+ function refText(m) {
285
+ if (m.refs.length === 0) return null;
286
+ return `refs: ${m.refs.map((r) => `${r.type}:${r.id}${r.at ? `@${r.at}` : ""}`).join(", ")}`;
287
+ }
288
+ function senderLabel(m, external, remote) {
289
+ if (remote !== void 0) return `${m.from} (remote: ${remote})`;
290
+ return external ? `${m.from} (external)` : m.from;
291
+ }
292
+ function renderForAgent(m, external = false, remote) {
293
+ const quote = (line) => `│ ${line}`;
294
+ const tags = [
295
+ m.kind,
296
+ ...m.urgent ? ["urgent"] : [],
297
+ m.id
298
+ ].join(" · ");
299
+ const lines = [`[message from ${senderLabel(m, external, remote)} · ${tags}]`, ...m.body.split(LINE_BREAK).map(quote)];
300
+ if (m.replyTo !== null) lines.push(`(in reply to ${m.replyTo})`);
301
+ const carried = [];
302
+ if (m.choices !== void 0) carried.push(`choices: ${m.choices.join(" | ")}`);
303
+ if (m.choice !== void 0) carried.push(`choice: ${m.choice}`);
304
+ const refs = refText(m);
305
+ if (refs !== null) carried.push(refs);
306
+ const quoteCarried = external || remote !== void 0;
307
+ lines.push(...quoteCarried ? carried.map(quote) : carried);
308
+ if (m.blocking) lines.push(`The sender is waiting. Answer with msg_reply(messageId: "${m.id}").`);
309
+ return lines.join("\n");
310
+ }
311
+ function firstLine(body) {
312
+ const first = body.split(LINE_BREAK, 1)[0] ?? "";
313
+ const chars = Array.from(first);
314
+ return chars.length > DIGEST_WIDTH ? `${chars.slice(0, DIGEST_WIDTH - 1).join("")}…` : first;
315
+ }
316
+ function renderDigestLine(m, external = false, remote) {
317
+ const channel = m.to.find((a) => a.startsWith("channel:"));
318
+ return `📬${channel === void 0 ? "" : ` #${channel.slice(8)} ·`} ${m.kind} from ${senderLabel(m, external, remote)}: ${firstLine(m.body)} (${m.id})`;
319
+ }
320
+
321
+ //#endregion
322
+ //#region src/ulid.ts
323
+ const ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
324
+ const MAX_TIME = 2 ** 48 - 1;
325
+ function defaultRandom(n) {
326
+ return crypto.getRandomValues(new Uint8Array(n));
327
+ }
328
+ function encodeTime(ms) {
329
+ let out = "";
330
+ let t = ms;
331
+ for (let i = 0; i < 10; i++) {
332
+ out = ALPHABET[t % 32] + out;
333
+ t = Math.floor(t / 32);
334
+ }
335
+ return out;
336
+ }
337
+ function increment(digits) {
338
+ for (let i = digits.length - 1; i >= 0; i--) {
339
+ if (digits[i] < 31) {
340
+ digits[i] += 1;
341
+ return;
342
+ }
343
+ digits[i] = 0;
344
+ }
345
+ throw new Error("ulid random component overflowed within one millisecond");
346
+ }
347
+ function createUlidFactory(randomBytes = defaultRandom) {
348
+ let lastTime = -1;
349
+ let lastRandom = [];
350
+ return (nowMs) => {
351
+ if (!Number.isInteger(nowMs) || nowMs < 0 || nowMs > MAX_TIME) throw new RangeError(`ulid time out of range: ${nowMs}`);
352
+ if (nowMs <= lastTime) increment(lastRandom);
353
+ else {
354
+ lastTime = nowMs;
355
+ lastRandom = Array.from(randomBytes(16), (b) => b % 32);
356
+ }
357
+ return encodeTime(lastTime) + lastRandom.map((d) => ALPHABET[d]).join("");
358
+ };
359
+ }
360
+
361
+ //#endregion
362
+ //#region src/engine.ts
363
+ const DEFAULT_LIMITS = {
364
+ urgentPerHour: 10,
365
+ agentTurnsPerThreadPerHour: 20,
366
+ agentThreadsPerHour: Number.POSITIVE_INFINITY,
367
+ openWakeGatesPerTarget: Number.POSITIVE_INFINITY
368
+ };
369
+ const HOUR_MS = 3600 * 1e3;
370
+ /** Superseded-answer notices one question sends at most (FW-R33). */
371
+ const MAX_SUPERSEDED_NOTICES = 5;
372
+ const SYSTEM_SENDER = {
373
+ address: SYSTEM_ADDRESS,
374
+ canDecide: true
375
+ };
376
+ const LOCAL = { kind: "local" };
377
+ const LOCAL_ONLY_TEXT = {
378
+ gate: "gates never leave the machine whose runs they guard",
379
+ marker: "system markers never travel between machines",
380
+ participant: "overseer and A2A conversations stay on their machine",
381
+ root: "this thread stays on the machine it started on"
382
+ };
383
+ const REMOTE_RANK = {
384
+ refused: -1,
385
+ forwarded: 0,
386
+ held: 1,
387
+ pushed: 2,
388
+ notified: 2,
389
+ read: 3,
390
+ answered: 4
391
+ };
392
+ const LOCAL_RANK = {
393
+ held: 0,
394
+ sending: 1,
395
+ pushed: 2,
396
+ notified: 2,
397
+ read: 3,
398
+ answered: 4
399
+ };
400
+ var DeliveryEngine = class {
401
+ store;
402
+ host;
403
+ limits;
404
+ ulid;
405
+ gateTypes;
406
+ listeners = /* @__PURE__ */ new Set();
407
+ constructor(opts) {
408
+ this.store = opts.store;
409
+ this.host = opts.host;
410
+ this.limits = {
411
+ ...DEFAULT_LIMITS,
412
+ ...opts.limits
413
+ };
414
+ this.ulid = opts.newUlid ?? createUlidFactory();
415
+ this.gateTypes = new Set(opts.gateTypes ?? ["wake"]);
416
+ }
417
+ subscribe(listener) {
418
+ this.listeners.add(listener);
419
+ return () => this.listeners.delete(listener);
420
+ }
421
+ /** How many listeners are subscribed right now, so a test can check that a
422
+ * subscriber's cleanup unsubscribed it. */
423
+ get listenerCount() {
424
+ return this.listeners.size;
425
+ }
426
+ emit(e) {
427
+ for (const l of this.listeners) try {
428
+ l(e);
429
+ } catch (err) {
430
+ console.error("messaging listener failed", err);
431
+ }
432
+ }
433
+ id(prefix) {
434
+ return `${prefix}-${this.ulid(this.host.now().getTime()).toLowerCase()}`;
435
+ }
436
+ nowIso() {
437
+ return this.host.now().toISOString();
438
+ }
439
+ hourAgoIso() {
440
+ return new Date(this.host.now().getTime() - HOUR_MS).toISOString();
441
+ }
442
+ authorize(sender) {
443
+ if (sender.canDecide && sender.address !== SYSTEM_ADDRESS && !sender.address.startsWith("human:")) throw new MessagingError("forbidden", `${sender.address} cannot decide; only humans and the system do`, "from");
444
+ if (parseAddress(sender.address, "from").kind !== "agent" || sender.address === SYSTEM_ADDRESS) return { muted: false };
445
+ const agent = this.store.getAgent(sender.address);
446
+ if (agent === null || agent.status !== "approved") throw new MessagingError("forbidden", `${sender.address} is not an approved agent`, "from");
447
+ return { muted: agent.muted };
448
+ }
449
+ resolveTargets(to, sender, fields) {
450
+ const senderTask = sender.startsWith("run:") ? this.host.taskOfRun(sender.slice(4)) : null;
451
+ const isSelf = (addr) => addr === sender || senderTask !== null && addr === `task:${senderTask}`;
452
+ const byRecipient = /* @__PURE__ */ new Map();
453
+ const add = (recipient, via) => {
454
+ if (isSelf(recipient) || recipient === SYSTEM_ADDRESS) return;
455
+ const existing = byRecipient.get(recipient);
456
+ if (existing === void 0 || existing.via === "channel" && via === "direct") byRecipient.set(recipient, {
457
+ recipient,
458
+ via
459
+ });
460
+ };
461
+ for (const addr of to) {
462
+ const field = fields.get(addr) ?? "to";
463
+ const parsed = parseAddress(addr, field);
464
+ if (parsed.kind !== "channel") {
465
+ add(addr, "direct");
466
+ continue;
467
+ }
468
+ const explicit = this.store.members(parsed.name);
469
+ const implicit = this.host.implicitMembers(parsed.name);
470
+ if (!this.store.channels().some((c) => c.name === parsed.name) && implicit.length === 0) throw new MessagingError("not-found", `no channel ${parsed.name}`, field);
471
+ for (const member of [...explicit, ...implicit]) if (!member.startsWith("channel:")) add(member, "channel");
472
+ }
473
+ return [...byRecipient.values()];
474
+ }
475
+ plan(target, muted, field, heldIfEnded) {
476
+ const base = {
477
+ id: this.id("d"),
478
+ messageId: "",
479
+ recipient: target.recipient,
480
+ via: target.via,
481
+ updatedAt: this.nowIso()
482
+ };
483
+ if (muted) return {
484
+ ...base,
485
+ runId: null,
486
+ state: "read"
487
+ };
488
+ const parsed = parseAddress(target.recipient, field);
489
+ switch (parsed.kind) {
490
+ case "human": return {
491
+ ...base,
492
+ runId: null,
493
+ state: "notified"
494
+ };
495
+ case "agent":
496
+ case "channel":
497
+ case "a2a": return {
498
+ ...base,
499
+ runId: null,
500
+ state: "held"
501
+ };
502
+ case "task": {
503
+ const run = this.host.liveRunFor(parsed.id);
504
+ return run === null ? {
505
+ ...base,
506
+ runId: null,
507
+ state: "held"
508
+ } : {
509
+ ...base,
510
+ runId: run,
511
+ state: "sending"
512
+ };
513
+ }
514
+ case "run":
515
+ if (!this.host.isLiveRun(parsed.id)) {
516
+ if (target.via === "channel") return null;
517
+ if (heldIfEnded) return {
518
+ ...base,
519
+ runId: null,
520
+ state: "held"
521
+ };
522
+ throw new MessagingError("invalid", `run ${parsed.id} is not live`, field);
523
+ }
524
+ return {
525
+ ...base,
526
+ runId: parsed.id,
527
+ state: "sending"
528
+ };
529
+ }
530
+ }
531
+ async send(input, sender, options = {}) {
532
+ const { muted } = this.authorize(sender);
533
+ const key = input.idempotencyKey;
534
+ if (key !== void 0) {
535
+ checkIdempotencyKey(key);
536
+ const prior = this.replay(sender.address, key);
537
+ if (prior !== null) return prior;
538
+ }
539
+ const replyTo = input.replyTo ?? null;
540
+ const replyTarget = replyTo === null ? null : this.store.getMessage(replyTo);
541
+ if (replyTo !== null && (replyTarget === null || !this.participates(replyTarget, sender))) throw new MessagingError("not-found", `no message ${replyTo}`, "replyTo");
542
+ validateSendInput(input, sender.address, sender.canDecide, replyTarget, {
543
+ gateTypes: this.gateTypes,
544
+ origin: options.origin ?? "local"
545
+ });
546
+ await this.checkBreaker(replyTarget, sender);
547
+ this.checkNewThreads(input, replyTarget, sender);
548
+ const raced = key === void 0 ? null : this.replay(sender.address, key);
549
+ if (raced !== null) return raced;
550
+ if (input.kind === "answer" && replyTarget !== null && this.store.answersTo(replyTarget.id).length > 0) throw alreadyAnswered(replyTarget.id);
551
+ let urgent = input.urgent === true;
552
+ let downgraded = false;
553
+ if (urgent && !sender.address.startsWith("human:") && sender.address !== SYSTEM_ADDRESS) {
554
+ const since = this.hourAgoIso();
555
+ if ((this.host.quotaGroup?.(sender.address) ?? [sender.address]).reduce((n, address) => n + this.store.countFrom(address, since, true), 0) >= this.limits.urgentPerHour) {
556
+ urgent = false;
557
+ downgraded = true;
558
+ }
559
+ }
560
+ const id = this.id("m");
561
+ const message = {
562
+ id,
563
+ thread: replyTarget?.thread ?? id,
564
+ replyTo: input.replyTo ?? null,
565
+ from: sender.address,
566
+ to: this.replyRecipients(input.to, replyTarget),
567
+ kind: input.kind,
568
+ body: input.body,
569
+ refs: input.refs ?? [],
570
+ urgent,
571
+ blocking: input.blocking === true,
572
+ wake: input.wake ?? "none",
573
+ createdAt: this.nowIso()
574
+ };
575
+ if (input.session !== void 0) message.session = input.session;
576
+ if (input.data !== void 0) message.data = input.data;
577
+ if (input.choices !== void 0) message.choices = input.choices;
578
+ else if (input.kind === "handoff") message.choices = ["accept", "decline"];
579
+ if (input.choice !== void 0) message.choice = input.choice;
580
+ const fields = this.recipientFields(input.to, replyTarget);
581
+ const { targets: placedTargets, placed } = this.place(this.resolveTargets(message.to, sender.address, fields), fields, replyTarget, message);
582
+ const targets = this.admitExternal(placedTargets, fields, sender, replyTarget, message);
583
+ const fed = this.host.federation;
584
+ if (fed !== void 0) message.hlc = fed.hlc();
585
+ const wakesRuns = wakesEndedRuns(message);
586
+ const deliveries = [];
587
+ const remotes = [];
588
+ const skipWake = /* @__PURE__ */ new Set();
589
+ for (const t of targets) {
590
+ const p = placed.get(t.recipient) ?? LOCAL;
591
+ if (p.kind === "remote") {
592
+ remotes.push({
593
+ messageId: id,
594
+ recipient: t.recipient,
595
+ via: t.via,
596
+ state: "forwarded",
597
+ homes: p.homes,
598
+ wakeAt: p.wakeAt ?? null,
599
+ refusedBy: [],
600
+ updatedAt: this.nowIso()
601
+ });
602
+ if (p.wakeAt !== void 0 && fed !== void 0 && p.wakeAt !== fed.replica) skipWake.add(t.recipient);
603
+ if (!p.alsoLocal) continue;
604
+ }
605
+ const planned = this.plan(t, muted, fields.get(t.recipient) ?? "to", t.recipient === replyTarget?.from || wakesRuns && this.hasTask(t.recipient));
606
+ if (planned !== null) deliveries.push({
607
+ ...planned,
608
+ messageId: id
609
+ });
610
+ }
611
+ const question = message.kind === "answer" ? replyTarget : null;
612
+ const settledAs = question === null || fed === void 0 ? null : this.localAnswerSettledAs(question);
613
+ const written = this.store.transaction(() => {
614
+ const prior = key === void 0 ? null : this.replay(sender.address, key);
615
+ if (prior !== null) return prior;
616
+ if (question !== null && this.store.answersTo(question.id).length > 0) throw alreadyAnswered(question.id);
617
+ this.store.insertMessage(message, key, settledAs === null ? void 0 : { settledAs });
618
+ for (const d of deliveries) this.store.insertDelivery(d);
619
+ for (const r of remotes) this.store.insertRemote(r);
620
+ if (question !== null && settledAs === "accepted" && fed !== void 0) this.store.putSettlement({
621
+ questionId: question.id,
622
+ answerId: id,
623
+ closedReason: null,
624
+ settler: fed.replica,
625
+ at: this.nowIso()
626
+ });
627
+ return question === null ? [] : this.markAnswered(question.id);
628
+ });
629
+ if (!Array.isArray(written)) return written;
630
+ const answered = written;
631
+ const gateType = question === null ? null : gateTypeOf(question, this.gateTypes);
632
+ if (question !== null && gateType !== null && this.gateTypes.has(gateType) && !isSystemMarker(message, "x-closed") && isDecidingAuthor(message.from)) await this.applyGate(question, message);
633
+ this.emit({
634
+ type: "message",
635
+ message
636
+ });
637
+ for (const d of answered) this.emit({
638
+ type: "delivery",
639
+ delivery: d
640
+ });
641
+ for (const r of remotes) this.emit({
642
+ type: "remote",
643
+ messageId: id,
644
+ recipient: r.recipient
645
+ });
646
+ const settled = [];
647
+ for (const d of deliveries) settled.push((await this.dispatch(d, message)).delivery);
648
+ if (message.wake === "request") try {
649
+ await this.runWake(message, settled, skipWake);
650
+ } catch (err) {
651
+ console.error("messaging wake failed", err);
652
+ }
653
+ return {
654
+ message,
655
+ deliveries: settled,
656
+ downgraded
657
+ };
658
+ }
659
+ localAnswerSettledAs(question) {
660
+ if (question.origin !== void 0) return "pending";
661
+ return this.store.remoteDeliveries({ messageId: question.id }).length > 0 ? "accepted" : null;
662
+ }
663
+ hasTask(address) {
664
+ return address.startsWith("run:") && this.host.taskOfRun(address.slice(4)) !== null;
665
+ }
666
+ place(targets, fields, replyTarget, message) {
667
+ const fed = this.host.federation;
668
+ const gateData = hasGateData(message) || replyTarget !== null && hasGateData(replyTarget);
669
+ const placed = /* @__PURE__ */ new Map();
670
+ let external = false;
671
+ for (const t of targets) {
672
+ if (this.isExternal(t.recipient)) {
673
+ external = true;
674
+ continue;
675
+ }
676
+ let p = fed === void 0 ? LOCAL : fed.placement(t, message, replyTarget);
677
+ if (gateData && p.kind === "remote") p = p.alsoLocal ? LOCAL : {
678
+ kind: "refuse",
679
+ reason: "local-only"
680
+ };
681
+ placed.set(t.recipient, p);
682
+ }
683
+ const refused = targets.filter((t) => placed.get(t.recipient)?.kind === "refuse");
684
+ if (gateData && (external || refused.length > 0)) throw new MessagingError("forbidden", "gates never leave this machine", "data");
685
+ for (const t of refused) if (t.via === "direct") throw new MessagingError("forbidden", "overseer and A2A conversations stay on this machine", fields.get(t.recipient) ?? "to");
686
+ return {
687
+ targets: targets.filter((t) => placed.get(t.recipient)?.kind !== "refuse"),
688
+ placed
689
+ };
690
+ }
691
+ admitExternal(targets, fields, sender, replyTarget, message) {
692
+ const external = (t) => this.isExternal(t.recipient);
693
+ if (!targets.some(external)) return targets;
694
+ const out = [];
695
+ for (const t of targets) {
696
+ if (!external(t)) {
697
+ out.push(t);
698
+ continue;
699
+ }
700
+ const target = {
701
+ recipient: t.recipient,
702
+ via: t.via,
703
+ field: fields.get(t.recipient) ?? "to"
704
+ };
705
+ try {
706
+ if ((this.host.admitExternal?.(target, sender, replyTarget, message) ?? "deliver") === "deliver") out.push(t);
707
+ } catch (err) {
708
+ if (t.via === "channel" && err instanceof MessagingError) continue;
709
+ throw err;
710
+ }
711
+ }
712
+ return out;
713
+ }
714
+ isExternal(address) {
715
+ return (this.host.external?.(address) ?? null) !== null;
716
+ }
717
+ replay(from, key) {
718
+ const message = this.store.byIdemKey(from, key);
719
+ if (message === null) return null;
720
+ return {
721
+ message,
722
+ deliveries: this.store.deliveries({ messageId: message.id }),
723
+ downgraded: false,
724
+ replayed: true
725
+ };
726
+ }
727
+ rewriteForReply(address, target) {
728
+ return target !== null && (address === target.from || target.to.includes(address)) ? this.deliverableAddress(address) : address;
729
+ }
730
+ replyRecipients(to, target) {
731
+ if (target === null) return [...to];
732
+ return [...new Set(to.map((a) => this.rewriteForReply(a, target)))];
733
+ }
734
+ recipientFields(to, target) {
735
+ const fields = /* @__PURE__ */ new Map();
736
+ to.forEach((address, i) => {
737
+ const resolved = this.rewriteForReply(address, target);
738
+ if (!fields.has(resolved)) fields.set(resolved, `to[${i}]`);
739
+ });
740
+ return fields;
741
+ }
742
+ participates(target, sender) {
743
+ if (decides(sender)) return true;
744
+ const senderRunId = sender.address.startsWith("run:") ? sender.address.slice(4) : null;
745
+ const senderTask = senderRunId !== null ? this.host.taskOfRun(senderRunId) : null;
746
+ const actsFor = (address) => address === sender.address || senderTask !== null && (address === `task:${senderTask}` || address.startsWith("run:") && this.host.taskOfRun(address.slice(4)) === senderTask);
747
+ if (actsFor(target.from)) return true;
748
+ return this.store.deliveries({ messageId: target.id }).some((d) => actsFor(d.recipient) || senderRunId !== null && d.runId === senderRunId);
749
+ }
750
+ canRead(messageId, sender) {
751
+ const message = this.store.getMessage(messageId);
752
+ return message !== null && this.participates(message, sender);
753
+ }
754
+ canReadThread(threadId, sender) {
755
+ return this.store.thread(threadId).some((m) => this.participates(m, sender));
756
+ }
757
+ async applyGate(question, answer) {
758
+ if (!isDecidingAuthor(answer.from)) return false;
759
+ try {
760
+ await this.host.onAnswered(question, answer);
761
+ } catch (err) {
762
+ console.error("messaging hook failed", err);
763
+ return false;
764
+ }
765
+ try {
766
+ this.store.markGateApplied(question.id, this.nowIso());
767
+ } catch (err) {
768
+ console.error("messaging markGateApplied failed", err);
769
+ return false;
770
+ }
771
+ return true;
772
+ }
773
+ async dispatch(d, message, opts = {}) {
774
+ let next = d;
775
+ if (d.state === "sending" && d.runId !== null) {
776
+ const push = d.via === "direct" || (opts.urgent ?? message.urgent);
777
+ const external = this.isExternal(message.from);
778
+ const remote = opts.remote ?? this.remoteLabel(message);
779
+ try {
780
+ if (push) await this.host.push(d.runId, renderForAgent(message, external, remote), message);
781
+ else await this.host.notify(d.runId, renderDigestLine(message, external, remote), message);
782
+ next = {
783
+ ...d,
784
+ state: push ? "pushed" : "notified",
785
+ updatedAt: this.nowIso()
786
+ };
787
+ } catch {
788
+ next = {
789
+ ...d,
790
+ state: "held",
791
+ runId: null,
792
+ updatedAt: this.nowIso()
793
+ };
794
+ }
795
+ if (!this.store.setDelivery(next.id, next.state, next.runId, next.updatedAt, "sending")) return {
796
+ delivery: this.store.getDelivery(d.id) ?? d,
797
+ won: false
798
+ };
799
+ } else if (d.state === "notified") try {
800
+ this.host.notifyHuman(d.recipient, message);
801
+ } catch {}
802
+ this.emit({
803
+ type: "delivery",
804
+ delivery: next
805
+ });
806
+ return {
807
+ delivery: next,
808
+ won: true
809
+ };
810
+ }
811
+ remoteLabel(message) {
812
+ if (message.origin === void 0) return void 0;
813
+ return this.host.federation?.label(message.origin) ?? message.origin;
814
+ }
815
+ async recover() {
816
+ let retried = 0;
817
+ let reverted = 0;
818
+ let replayed = 0;
819
+ let voided = 0;
820
+ for (const d of this.store.deliveries({ states: ["sending"] })) {
821
+ const message = this.store.getMessage(d.messageId);
822
+ if (d.runId !== null && message !== null && this.host.isLiveRun(d.runId)) {
823
+ const { won } = await this.dispatch(d, message);
824
+ if (won) retried++;
825
+ } else if (this.store.setDelivery(d.id, "held", null, this.nowIso(), "sending")) reverted++;
826
+ }
827
+ for (const { question, answer } of this.store.unappliedAnsweredGates()) {
828
+ const type = gateTypeOf(question, this.gateTypes);
829
+ if (type === null || !this.gateTypes.has(type)) continue;
830
+ if (!isDecidingAuthor(answer.from)) {
831
+ if (await this.voidAndReopen(question, answer)) voided++;
832
+ continue;
833
+ }
834
+ if (await this.applyGate(question, answer)) replayed++;
835
+ }
836
+ return {
837
+ retried,
838
+ reverted,
839
+ replayed,
840
+ voided
841
+ };
842
+ }
843
+ async voidAndReopen(question, answer) {
844
+ const muted = this.store.getAgent(question.from)?.muted === true;
845
+ const reopened = this.store.transaction(() => {
846
+ if (!this.store.voidAnswer(answer.id, question.id, this.nowIso())) return null;
847
+ const out = [];
848
+ for (const d of this.store.deliveries({ messageId: question.id })) {
849
+ if (d.state !== "answered") continue;
850
+ const { state, runId } = this.reopenState(d, muted);
851
+ const next = {
852
+ ...d,
853
+ state,
854
+ runId,
855
+ updatedAt: this.nowIso()
856
+ };
857
+ if (this.store.setDelivery(d.id, state, runId, next.updatedAt, "answered")) out.push(next);
858
+ }
859
+ return out;
860
+ });
861
+ if (reopened === null) return false;
862
+ const voidedRow = this.store.getMessage(answer.id);
863
+ if (voidedRow !== null) this.emit({
864
+ type: "message",
865
+ message: voidedRow
866
+ });
867
+ for (const d of reopened) await this.dispatch(d, question);
868
+ try {
869
+ await this.send({
870
+ to: [this.host.owner(answer.from)],
871
+ kind: "notice",
872
+ replyTo: question.id,
873
+ refs: [{
874
+ type: "message",
875
+ id: answer.id
876
+ }],
877
+ body: `An answer from ${answer.from} to gate ${question.id} was set aside; only deciding humans answer gates. The gate is open again.`
878
+ }, SYSTEM_SENDER);
879
+ } catch (err) {
880
+ console.error("messaging void notice failed", err);
881
+ }
882
+ return true;
883
+ }
884
+ reopenState(d, muted) {
885
+ try {
886
+ const planned = this.plan({
887
+ recipient: d.recipient,
888
+ via: d.via
889
+ }, muted, "to", true);
890
+ return planned === null ? {
891
+ state: "held",
892
+ runId: null
893
+ } : {
894
+ state: planned.state,
895
+ runId: planned.runId
896
+ };
897
+ } catch {
898
+ return {
899
+ state: "held",
900
+ runId: null
901
+ };
902
+ }
903
+ }
904
+ getMessage(id) {
905
+ return this.store.getMessage(id);
906
+ }
907
+ /** What `address` sent at or after `sinceIso`, oldest first, optionally only these kinds. */
908
+ messagesFrom(address, sinceIso, kinds) {
909
+ return this.store.messagesFrom(address, sinceIso, kinds);
910
+ }
911
+ answerOf(questionId) {
912
+ return this.store.answersTo(questionId)[0] ?? null;
913
+ }
914
+ openBlocking() {
915
+ return this.store.openBlocking();
916
+ }
917
+ /** One message's local deliveries; recipients homed elsewhere are not among them. */
918
+ deliveriesOf(messageId) {
919
+ return this.store.deliveries({ messageId });
920
+ }
921
+ thread(threadId) {
922
+ const messages = this.store.thread(threadId);
923
+ return {
924
+ messages,
925
+ deliveries: messages.flatMap((m) => this.store.deliveries({ messageId: m.id }))
926
+ };
927
+ }
928
+ inbox(recipient, states) {
929
+ return this.store.deliveries({
930
+ recipient,
931
+ states
932
+ }).flatMap((delivery) => {
933
+ const message = this.store.getMessage(delivery.messageId);
934
+ return message === null ? [] : [{
935
+ delivery,
936
+ message
937
+ }];
938
+ });
939
+ }
940
+ markRead(deliveryId) {
941
+ const d = this.store.getDelivery(deliveryId);
942
+ if (d === null) throw new MessagingError("not-found", `no delivery ${deliveryId}`, "deliveryId");
943
+ if (d.state === "answered" || d.state === "read") return d;
944
+ const next = {
945
+ ...d,
946
+ state: "read",
947
+ updatedAt: this.nowIso()
948
+ };
949
+ if (!this.store.setDelivery(next.id, next.state, next.runId, next.updatedAt, d.state)) return this.store.getDelivery(d.id) ?? d;
950
+ this.emit({
951
+ type: "delivery",
952
+ delivery: next
953
+ });
954
+ return next;
955
+ }
956
+ markRelayed(deliveryId) {
957
+ const d = this.store.getDelivery(deliveryId);
958
+ if (d === null || d.state !== "held" || !isPeerAddress(d.recipient)) return null;
959
+ const next = {
960
+ ...d,
961
+ state: "pushed",
962
+ updatedAt: this.nowIso()
963
+ };
964
+ if (!this.store.setDelivery(next.id, next.state, next.runId, next.updatedAt, "held")) return null;
965
+ this.emit({
966
+ type: "delivery",
967
+ delivery: next
968
+ });
969
+ return next;
970
+ }
971
+ join(channel, member) {
972
+ parseAddress(`channel:${channel}`, "channel");
973
+ const parsed = parseAddress(member, "member");
974
+ if (parsed.kind === "run") throw new MessagingError("invalid", "channels hold tasks and actors, not runs — join as task:<id>", "member");
975
+ if (parsed.kind === "channel") throw new MessagingError("invalid", "channels hold tasks and actors, not channels", "member");
976
+ if (this.store.transaction(() => {
977
+ this.store.ensureChannel(channel, this.nowIso(), false);
978
+ return this.store.addMember(channel, member, this.nowIso());
979
+ })) this.emit({
980
+ type: "membership",
981
+ channel,
982
+ member,
983
+ joined: true
984
+ });
985
+ }
986
+ leave(channel, member) {
987
+ const removed = this.store.removeMember(channel, member);
988
+ if (removed) this.emit({
989
+ type: "membership",
990
+ channel,
991
+ member,
992
+ joined: false
993
+ });
994
+ return removed;
995
+ }
996
+ deliverableAddress(address) {
997
+ if (!address.startsWith("run:")) return address;
998
+ const runId = address.slice(4);
999
+ if (this.host.isLiveRun(runId)) return address;
1000
+ const task = this.host.taskOfRun(runId);
1001
+ if (task !== null) return `task:${task}`;
1002
+ const remote = this.host.federation?.remoteRunTask(runId) ?? null;
1003
+ return remote === null ? address : `task:${remote}`;
1004
+ }
1005
+ async reply(messageId, input, sender) {
1006
+ this.authorize(sender);
1007
+ const target = this.store.getMessage(messageId);
1008
+ if (target === null) throw new MessagingError("not-found", `no message ${messageId}`, "replyTo");
1009
+ const asking = target.kind === "question" || target.kind === "handoff";
1010
+ return this.send({
1011
+ ...input,
1012
+ to: [target.from],
1013
+ kind: asking ? "answer" : "message",
1014
+ replyTo: messageId
1015
+ }, sender);
1016
+ }
1017
+ close(questionId, reason) {
1018
+ const target = this.store.getMessage(questionId);
1019
+ if (target === null) throw new MessagingError("not-found", `no message ${questionId}`, "replyTo");
1020
+ if (target.kind !== "question" && target.kind !== "handoff") throw new MessagingError("invalid", `${questionId} is a ${target.kind}, not a question or handoff`, "replyTo");
1021
+ if (this.answerOf(questionId) !== null) throw alreadyAnswered(questionId);
1022
+ const answer = {
1023
+ id: this.id("m"),
1024
+ thread: target.thread,
1025
+ replyTo: questionId,
1026
+ from: SYSTEM_ADDRESS,
1027
+ to: [target.from],
1028
+ kind: "answer",
1029
+ body: `Closed: ${reason}`,
1030
+ refs: [],
1031
+ data: {
1032
+ type: "x-closed",
1033
+ reason
1034
+ },
1035
+ urgent: false,
1036
+ blocking: false,
1037
+ wake: "none",
1038
+ createdAt: this.nowIso()
1039
+ };
1040
+ const fed = this.host.federation;
1041
+ if (fed !== void 0) answer.hlc = fed.hlc();
1042
+ const answered = this.store.transaction(() => {
1043
+ this.store.insertMessage(answer);
1044
+ return this.markAnswered(questionId);
1045
+ });
1046
+ this.emit({
1047
+ type: "message",
1048
+ message: answer
1049
+ });
1050
+ for (const d of answered) this.emit({
1051
+ type: "delivery",
1052
+ delivery: d
1053
+ });
1054
+ return answer;
1055
+ }
1056
+ markAnswered(questionId) {
1057
+ const moved = [];
1058
+ for (const d of this.store.deliveries({ messageId: questionId })) {
1059
+ if (d.state === "answered") continue;
1060
+ const next = {
1061
+ ...d,
1062
+ state: "answered",
1063
+ updatedAt: this.nowIso()
1064
+ };
1065
+ if (this.store.setDelivery(next.id, next.state, next.runId, next.updatedAt, d.state)) moved.push(next);
1066
+ }
1067
+ return moved;
1068
+ }
1069
+ async deliverHeld(runId, taskId) {
1070
+ const claimed = this.store.transaction(() => {
1071
+ const held = [...this.store.deliveries({
1072
+ recipient: `task:${taskId}`,
1073
+ states: ["held"]
1074
+ }), ...this.store.deliveries({
1075
+ recipientPrefix: "run:",
1076
+ states: ["held"]
1077
+ }).filter((d) => this.host.taskOfRun(d.recipient.slice(4)) === taskId)];
1078
+ const out$1 = [];
1079
+ for (const d of held) {
1080
+ const message = this.store.getMessage(d.messageId);
1081
+ if (message === null) continue;
1082
+ const bound = {
1083
+ ...d,
1084
+ state: "sending",
1085
+ runId,
1086
+ updatedAt: this.nowIso()
1087
+ };
1088
+ if (this.store.setDelivery(bound.id, bound.state, bound.runId, bound.updatedAt, "held")) out$1.push({
1089
+ bound,
1090
+ message
1091
+ });
1092
+ }
1093
+ return out$1;
1094
+ });
1095
+ const out = [];
1096
+ for (const { bound, message } of claimed) out.push((await this.dispatch(bound, message)).delivery);
1097
+ return out;
1098
+ }
1099
+ requireFederation() {
1100
+ const fed = this.host.federation;
1101
+ if (fed === void 0) throw new Error("this host does not federate");
1102
+ return fed;
1103
+ }
1104
+ async receive(message, origin) {
1105
+ const fed = this.requireFederation();
1106
+ checkReceivedEnvelope(message);
1107
+ checkReceivedTargets(origin);
1108
+ const stored = this.store.getMessage(message.id);
1109
+ if (stored !== null) {
1110
+ if (!sameSignedContent(stored, this.store.settledAs(stored.id), message)) throw new MessagingError("conflict", `message id ${message.id} was reused with other content`, "id");
1111
+ return {
1112
+ status: "duplicate",
1113
+ deliveries: await this.applyForward(stored, origin)
1114
+ };
1115
+ }
1116
+ const replyTarget = message.replyTo === null ? null : this.store.getMessage(message.replyTo);
1117
+ const root = message.thread === message.id ? null : this.store.getMessage(message.thread);
1118
+ checkReceivedThread(message, replyTarget, root);
1119
+ const local = localOnlyReason(message, replyTarget, root);
1120
+ if (local !== null) throw new MessagingError("forbidden", LOCAL_ONLY_TEXT[local], local === "gate" ? "data" : "to");
1121
+ const { muted } = this.authorizeRemote(message, origin.replica);
1122
+ const joined = replyTarget ?? root;
1123
+ if (joined !== null && !this.remoteParticipant(joined, message.from, origin.replica, fed)) throw new MessagingError("forbidden", `${message.from} is not a participant of ${joined.id}`, joined === replyTarget ? "replyTo" : "thread");
1124
+ validateSendInput(toSendInput(message), message.from, false, replyTarget, {
1125
+ gateTypes: this.gateTypes,
1126
+ origin: "received",
1127
+ parentOptional: true
1128
+ });
1129
+ const sender = {
1130
+ address: message.from,
1131
+ canDecide: false
1132
+ };
1133
+ await this.checkBreaker(joined, sender, true);
1134
+ const demote = message.urgent && !message.from.startsWith("human:") && this.store.countFrom(message.from, this.hourAgoIso(), true, origin.replica) >= this.limits.urgentPerHour;
1135
+ const answer = this.settleOnArrival(message, replyTarget);
1136
+ const asking = message.kind === "question" || message.kind === "handoff";
1137
+ const early = this.store.earlySettlements(message.id);
1138
+ const valid = asking ? early.find((s) => s.settler === origin.replica) ?? null : null;
1139
+ const refused = early.filter((s) => s !== valid);
1140
+ const closeHlc = valid !== null && valid.closedReason !== null ? fed.hlc() : void 0;
1141
+ const deliveries = [];
1142
+ const remotes = [];
1143
+ const seen = /* @__PURE__ */ new Set();
1144
+ for (const t of origin.targets) {
1145
+ if (t.recipient === SYSTEM_ADDRESS || seen.has(t.recipient)) continue;
1146
+ seen.add(t.recipient);
1147
+ if (!(t.homes.includes(fed.replica) || origin.forwardTarget === t.recipient)) {
1148
+ remotes.push(remoteRow(message.id, t, this.nowIso()));
1149
+ continue;
1150
+ }
1151
+ const planned = this.planRemote(t, muted);
1152
+ if (deliveries.some((d) => d.recipient === planned.recipient)) continue;
1153
+ deliveries.push({
1154
+ ...planned,
1155
+ messageId: message.id
1156
+ });
1157
+ }
1158
+ const storedMessage = {
1159
+ ...message,
1160
+ kind: answer.kind,
1161
+ origin: origin.replica
1162
+ };
1163
+ const changed = [];
1164
+ const answered = this.store.transaction(() => {
1165
+ if (this.store.getMessage(message.id) !== null) return null;
1166
+ if (answer.swapFirst && message.replyTo !== null) changed.push(...this.swapSettled(message.replyTo, message.id));
1167
+ this.store.insertMessage(storedMessage, void 0, {
1168
+ receivedAt: this.nowIso(),
1169
+ ...answer.settledAs === void 0 ? {} : { settledAs: answer.settledAs }
1170
+ });
1171
+ for (const d of deliveries) this.store.insertDelivery(d);
1172
+ for (const r of remotes) this.store.insertRemote(r);
1173
+ if (answer.recordSettlement && message.replyTo !== null) this.store.putSettlement({
1174
+ questionId: message.replyTo,
1175
+ answerId: message.id,
1176
+ closedReason: null,
1177
+ settler: fed.replica,
1178
+ at: this.nowIso()
1179
+ });
1180
+ if (early.length > 0) this.store.clearEarlySettlements(message.id);
1181
+ const honoured = valid === null ? [] : this.honourSettlement(storedMessage, valid, changed, closeHlc);
1182
+ const answeredFirst = asking ? this.answerEarly(storedMessage, fed, changed) : [];
1183
+ return [
1184
+ ...honoured,
1185
+ ...answeredFirst,
1186
+ ...answer.markAnswered === null ? [] : this.markAnswered(answer.markAnswered)
1187
+ ];
1188
+ });
1189
+ if (answered === null) return {
1190
+ status: "duplicate",
1191
+ deliveries: []
1192
+ };
1193
+ this.emit({
1194
+ type: "message",
1195
+ message: storedMessage
1196
+ });
1197
+ this.emitChanged(changed, message.id);
1198
+ for (const d of answered) this.emit({
1199
+ type: "delivery",
1200
+ delivery: d
1201
+ });
1202
+ for (const r of remotes) this.emit({
1203
+ type: "remote",
1204
+ messageId: r.messageId,
1205
+ recipient: r.recipient
1206
+ });
1207
+ const moved = new Map(answered.map((d) => [d.id, d]));
1208
+ const label = fed.label(origin.replica);
1209
+ const settled = [];
1210
+ for (const d of deliveries) {
1211
+ const done = moved.get(d.id);
1212
+ settled.push(done ?? (await this.dispatch(d, storedMessage, {
1213
+ remote: label,
1214
+ urgent: !demote && message.urgent
1215
+ })).delivery);
1216
+ }
1217
+ try {
1218
+ for (const s of refused) fed.problem(`message:${message.id}`, `${s.settler} sent a settle for ${message.id}; only the question's origin settles it`);
1219
+ if (answer.supersededBy !== null && message.replyTo !== null && this.mayNoteSuperseded(message.replyTo, message.id)) await this.noticeTo(message.from, message, `${message.id} was already answered by ${answer.supersededBy}; yours was kept as a reply.`);
1220
+ if (storedMessage.wake === "request") {
1221
+ const wakeHere = new Set(origin.targets.filter((t) => t.wakeAt === fed.replica).map((t) => t.recipient));
1222
+ const skip = new Set(settled.map((d) => d.recipient).filter((r) => !wakeHere.has(r)));
1223
+ await this.runWake(storedMessage, settled, skip);
1224
+ }
1225
+ } catch (err) {
1226
+ console.error("messaging receive follow-up failed", err);
1227
+ }
1228
+ return {
1229
+ status: "applied",
1230
+ deliveries: settled
1231
+ };
1232
+ }
1233
+ async applyForward(stored, origin) {
1234
+ if (origin.forwardTarget === void 0) return [];
1235
+ const target = origin.targets.find((t) => t.recipient === origin.forwardTarget);
1236
+ if (target === void 0) return [];
1237
+ const muted = this.store.getAgent(stored.from)?.muted === true;
1238
+ const planned = {
1239
+ ...this.planRemote(target, muted),
1240
+ messageId: stored.id
1241
+ };
1242
+ if (!this.store.transaction(() => {
1243
+ if (this.store.deliveries({
1244
+ messageId: stored.id,
1245
+ recipient: planned.recipient
1246
+ }).length > 0) return false;
1247
+ if (!this.store.deleteRemote(stored.id, target.recipient)) return false;
1248
+ this.store.insertDelivery(planned);
1249
+ return true;
1250
+ })) return [];
1251
+ this.emit({
1252
+ type: "remote",
1253
+ messageId: stored.id,
1254
+ recipient: target.recipient
1255
+ });
1256
+ return [(await this.dispatch(planned, stored)).delivery];
1257
+ }
1258
+ authorizeRemote(message, replica) {
1259
+ const parsed = parseAddress(message.from, "from");
1260
+ if (message.from === SYSTEM_ADDRESS) {
1261
+ if (!(message.kind === "notice" && message.data === void 0 && message.refs.length > 0 && message.refs.every((r) => r.type === "message" && this.exchangedWith(r.id, replica)))) throw new MessagingError("forbidden", `${SYSTEM_ADDRESS} from another machine may only send notices about messages exchanged with it`, "from");
1262
+ return { muted: false };
1263
+ }
1264
+ if (parsed.kind === "task" || parsed.kind === "channel") throw new MessagingError("forbidden", `${message.from} cannot send`, "from");
1265
+ if (parsed.kind !== "agent") return { muted: false };
1266
+ const agent = this.store.getAgent(message.from);
1267
+ if (agent === null || agent.status !== "approved") throw new MessagingError("forbidden", `${message.from} is not an approved agent`, "from");
1268
+ return { muted: agent.muted };
1269
+ }
1270
+ exchangedWith(messageId, replica) {
1271
+ const m = this.store.getMessage(messageId);
1272
+ if (m === null) return false;
1273
+ return m.origin === replica || this.store.remoteDeliveries({ messageId }).some((r) => r.homes.includes(replica));
1274
+ }
1275
+ remoteParticipant(target, from, replica, fed) {
1276
+ const taskOf = (runId) => this.host.taskOfRun(runId) ?? fed.remoteRunTask(runId);
1277
+ const fromTask = from.startsWith("run:") ? taskOf(from.slice(4)) : null;
1278
+ const actsFor = (address) => {
1279
+ if (address === SYSTEM_ADDRESS) return from === SYSTEM_ADDRESS && target.origin === replica;
1280
+ if (address === from) return true;
1281
+ if (fromTask === null) return false;
1282
+ return address === `task:${fromTask}` || address.startsWith("run:") && taskOf(address.slice(4)) === fromTask;
1283
+ };
1284
+ const remoteRows = this.store.remoteDeliveries({ messageId: target.id });
1285
+ return actsFor(target.from) || target.to.some(actsFor) || this.store.deliveries({ messageId: target.id }).some((d) => actsFor(d.recipient)) || remoteRows.some((r) => actsFor(r.recipient)) || from.startsWith("human:") && remoteRows.some((r) => r.homes.includes(replica));
1286
+ }
1287
+ answerEarly(question, fed, changed) {
1288
+ if (this.store.settlement(question.id) !== null) return this.store.answersTo(question.id).length > 0 ? this.markAnswered(question.id) : [];
1289
+ const early = this.store.answerCandidates(question.id).filter((c) => c.settledAs === "pending" || c.settledAs === "candidate");
1290
+ const first = early.find((c) => this.earlyAnswerHolds(question, c.message, fed)) ?? null;
1291
+ for (const c of early) {
1292
+ if (c === first || c.settledAs === "candidate") continue;
1293
+ this.store.setSettled(c.message.id, "message", "candidate");
1294
+ changed.push(c.message.id);
1295
+ }
1296
+ if (first === null) return [];
1297
+ if (first.settledAs !== "pending") {
1298
+ this.store.setSettled(first.message.id, "answer", "pending");
1299
+ changed.push(first.message.id);
1300
+ }
1301
+ return this.markAnswered(question.id);
1302
+ }
1303
+ earlyAnswerHolds(question, answer, fed) {
1304
+ if (answer.origin === void 0 || !this.remoteParticipant(question, answer.from, answer.origin, fed)) return false;
1305
+ try {
1306
+ validateSendInput(toSendInput({
1307
+ ...answer,
1308
+ kind: "answer"
1309
+ }), answer.from, false, question, {
1310
+ gateTypes: this.gateTypes,
1311
+ origin: "received"
1312
+ });
1313
+ return true;
1314
+ } catch (err) {
1315
+ if (err instanceof MessagingError) return false;
1316
+ throw err;
1317
+ }
1318
+ }
1319
+ planRemote(t, muted) {
1320
+ const recipient = this.deliverableAddress(t.recipient);
1321
+ const held = {
1322
+ id: this.id("d"),
1323
+ messageId: "",
1324
+ recipient,
1325
+ runId: null,
1326
+ via: t.via,
1327
+ state: "held",
1328
+ updatedAt: this.nowIso()
1329
+ };
1330
+ if (muted) return {
1331
+ ...held,
1332
+ state: "read"
1333
+ };
1334
+ if (recipient.startsWith("run:")) {
1335
+ const runId = recipient.slice(4);
1336
+ return this.host.isLiveRun(runId) ? {
1337
+ ...held,
1338
+ runId,
1339
+ state: "sending"
1340
+ } : held;
1341
+ }
1342
+ try {
1343
+ return this.plan({
1344
+ recipient,
1345
+ via: t.via
1346
+ }, false, "to", true) ?? held;
1347
+ } catch {
1348
+ return held;
1349
+ }
1350
+ }
1351
+ settleOnArrival(message, question) {
1352
+ const plain = {
1353
+ kind: message.kind,
1354
+ markAnswered: null,
1355
+ recordSettlement: false,
1356
+ swapFirst: false,
1357
+ supersededBy: null
1358
+ };
1359
+ if (message.kind !== "answer" || message.replyTo === null) return plain;
1360
+ const first = this.store.answersTo(message.replyTo)[0] ?? null;
1361
+ if (question !== null && question.origin === void 0) {
1362
+ if (first === null) return {
1363
+ ...plain,
1364
+ settledAs: "accepted",
1365
+ markAnswered: question.id,
1366
+ recordSettlement: true
1367
+ };
1368
+ return {
1369
+ ...plain,
1370
+ kind: "message",
1371
+ settledAs: "superseded",
1372
+ supersededBy: first.from
1373
+ };
1374
+ }
1375
+ const settlement = question === null ? null : this.store.settlement(question.id);
1376
+ if (question !== null && settlement !== null) {
1377
+ if (settlement.answerId === message.id) return {
1378
+ ...plain,
1379
+ settledAs: "accepted",
1380
+ markAnswered: question.id,
1381
+ swapFirst: true
1382
+ };
1383
+ return {
1384
+ ...plain,
1385
+ kind: "message",
1386
+ settledAs: "superseded"
1387
+ };
1388
+ }
1389
+ if (first === null) return {
1390
+ ...plain,
1391
+ settledAs: "pending",
1392
+ markAnswered: question?.id ?? null
1393
+ };
1394
+ return {
1395
+ ...plain,
1396
+ kind: "message",
1397
+ settledAs: "candidate"
1398
+ };
1399
+ }
1400
+ applyState(entries, publisher) {
1401
+ const fed = this.requireFederation();
1402
+ const events = [];
1403
+ const notices = [];
1404
+ this.store.transaction(() => {
1405
+ for (const e of entries) if (e.t === "delivery") this.mergeDelivery(e, publisher, events);
1406
+ else this.mergeRefused(e, publisher, fed, events, notices);
1407
+ });
1408
+ for (const e of events) this.emit(e);
1409
+ for (const n of notices) this.noticeTo(n.to, n.about, n.body).catch((err) => console.error("messaging refusal notice failed", err));
1410
+ }
1411
+ mergeDelivery(e, publisher, events) {
1412
+ const now = this.nowIso();
1413
+ const where = {
1414
+ messageId: e.message,
1415
+ recipient: e.recipient
1416
+ };
1417
+ for (const row of this.store.remoteDeliveries(where)) {
1418
+ if (!row.homes.includes(publisher)) continue;
1419
+ const state = maxRemote(row.state, e.state);
1420
+ if (state === row.state) continue;
1421
+ const patch = row.state === "refused" ? {
1422
+ state,
1423
+ refusedBy: []
1424
+ } : { state };
1425
+ if (this.store.setRemote(row.messageId, row.recipient, patch, now, row.state)) events.push({
1426
+ type: "remote",
1427
+ ...where
1428
+ });
1429
+ }
1430
+ for (const d of this.store.deliveries(where)) if (d.recipient.startsWith("human:")) {
1431
+ if (e.state !== "read" && e.state !== "answered") continue;
1432
+ if (LOCAL_RANK[e.state] <= LOCAL_RANK[d.state]) continue;
1433
+ if (this.store.setDelivery(d.id, e.state, d.runId, now, d.state)) events.push({
1434
+ type: "delivery",
1435
+ delivery: {
1436
+ ...d,
1437
+ state: e.state,
1438
+ updatedAt: now
1439
+ }
1440
+ });
1441
+ } else if (d.recipient.startsWith("task:") && d.state === "held" && REMOTE_RANK[e.state] >= REMOTE_RANK.pushed) {
1442
+ this.store.deleteDelivery(d.id);
1443
+ this.putRemote(d, [publisher], e.state, now);
1444
+ events.push({
1445
+ type: "remote",
1446
+ ...where
1447
+ });
1448
+ }
1449
+ }
1450
+ mergeRefused(e, publisher, fed, events, notices) {
1451
+ const message = this.store.getMessage(e.message);
1452
+ if (message === null || message.origin !== void 0) return;
1453
+ const now = this.nowIso();
1454
+ let newlyRefused = false;
1455
+ for (const row of this.store.remoteDeliveries({ messageId: message.id })) {
1456
+ if (!row.homes.includes(publisher) || row.refusedBy.includes(publisher)) continue;
1457
+ const refusedBy = [...row.refusedBy, publisher];
1458
+ const all = row.homes.every((h) => refusedBy.includes(h));
1459
+ const state = all ? "refused" : row.state;
1460
+ if (!this.store.setRemote(row.messageId, row.recipient, {
1461
+ state,
1462
+ refusedBy
1463
+ }, now, row.state)) continue;
1464
+ events.push({
1465
+ type: "remote",
1466
+ messageId: row.messageId,
1467
+ recipient: row.recipient
1468
+ });
1469
+ if (all && row.state !== "refused") newlyRefused = true;
1470
+ }
1471
+ if (newlyRefused && message.from !== SYSTEM_ADDRESS) notices.push({
1472
+ to: message.from,
1473
+ about: message,
1474
+ body: `${message.id} was refused by ${fed.label(publisher)}'s machine: ${e.reason}.`
1475
+ });
1476
+ const question = message.replyTo === null ? null : this.store.getMessage(message.replyTo);
1477
+ if (question === null || question.origin !== publisher || this.store.settledAs(message.id) !== "pending") return;
1478
+ this.store.setSettled(message.id, "message", null);
1479
+ events.push({
1480
+ type: "message",
1481
+ message: {
1482
+ ...message,
1483
+ kind: "message"
1484
+ }
1485
+ });
1486
+ for (const d of this.store.deliveries({
1487
+ messageId: question.id,
1488
+ states: ["answered"]
1489
+ })) {
1490
+ const state = d.recipient.startsWith("human:") ? "notified" : "held";
1491
+ if (this.store.setDelivery(d.id, state, null, now, "answered")) events.push({
1492
+ type: "delivery",
1493
+ delivery: {
1494
+ ...d,
1495
+ state,
1496
+ runId: null,
1497
+ updatedAt: now
1498
+ }
1499
+ });
1500
+ }
1501
+ }
1502
+ applySettlement(entry, publisher) {
1503
+ const fed = this.requireFederation();
1504
+ const record = {
1505
+ questionId: entry.question,
1506
+ answerId: entry.answer,
1507
+ closedReason: entry.closed ?? null,
1508
+ settler: publisher,
1509
+ at: entry.at
1510
+ };
1511
+ const q = this.store.getMessage(entry.question);
1512
+ if (q === null) {
1513
+ this.store.putEarlySettlement(record);
1514
+ return;
1515
+ }
1516
+ if ((q.origin ?? fed.replica) !== publisher) throw new MessagingError("forbidden", `${publisher} did not ask ${entry.question}`, "question");
1517
+ const closeHlc = entry.closed === void 0 ? void 0 : fed.hlc();
1518
+ const changed = [];
1519
+ const moved = this.store.transaction(() => this.honourSettlement(q, record, changed, closeHlc));
1520
+ this.emitChanged(changed);
1521
+ for (const d of moved) this.emit({
1522
+ type: "delivery",
1523
+ delivery: d
1524
+ });
1525
+ }
1526
+ honourSettlement(q, s, changed, closeHlc) {
1527
+ const answer = s.answerId === null ? null : this.store.getMessage(s.answerId);
1528
+ if (answer !== null && !this.store.answerCandidates(q.id).some((c) => c.message.id === answer.id)) return [];
1529
+ this.store.putSettlement(s);
1530
+ if (s.answerId === null) return [];
1531
+ if (answer !== null) {
1532
+ changed.push(...this.swapSettled(q.id, s.answerId));
1533
+ return this.markAnswered(q.id);
1534
+ }
1535
+ if (s.closedReason === null) return [];
1536
+ changed.push(...this.demoteOtherAnswers(q.id, s.answerId));
1537
+ this.store.insertMessage(closeCopy(q, s.answerId, s.closedReason, s.settler, this.nowIso(), closeHlc), void 0, {
1538
+ receivedAt: this.nowIso(),
1539
+ settledAs: "accepted"
1540
+ });
1541
+ changed.push(s.answerId);
1542
+ return this.markAnswered(q.id);
1543
+ }
1544
+ swapSettled(questionId, answerId) {
1545
+ const changed = this.demoteOtherAnswers(questionId, answerId);
1546
+ this.store.setSettled(answerId, "answer", "accepted");
1547
+ return [...changed, answerId];
1548
+ }
1549
+ demoteOtherAnswers(questionId, keep) {
1550
+ const changed = [];
1551
+ for (const c of this.store.answerCandidates(questionId)) {
1552
+ if (c.message.id === keep) continue;
1553
+ if (c.message.kind !== "answer" && c.settledAs === "superseded") continue;
1554
+ this.store.setSettled(c.message.id, "message", "superseded");
1555
+ changed.push(c.message.id);
1556
+ }
1557
+ return changed;
1558
+ }
1559
+ emitChanged(ids, skip) {
1560
+ for (const id of new Set(ids)) {
1561
+ if (id === skip) continue;
1562
+ const m = this.store.getMessage(id);
1563
+ if (m !== null) this.emit({
1564
+ type: "message",
1565
+ message: m
1566
+ });
1567
+ }
1568
+ }
1569
+ claimRemote(taskId) {
1570
+ const recipient = `task:${taskId}`;
1571
+ const now = this.nowIso();
1572
+ const retired = [];
1573
+ const claimed = this.store.transaction(() => {
1574
+ const out = [];
1575
+ for (const row of this.store.remoteDeliveries({
1576
+ recipient,
1577
+ states: ["forwarded", "held"]
1578
+ })) {
1579
+ if (this.store.getMessage(row.messageId) === null) continue;
1580
+ if (this.store.deleteRemote(row.messageId, recipient)) retired.push(row.messageId);
1581
+ if (this.store.deliveries({
1582
+ messageId: row.messageId,
1583
+ recipient
1584
+ }).length > 0) continue;
1585
+ const d = {
1586
+ id: this.id("d"),
1587
+ messageId: row.messageId,
1588
+ recipient,
1589
+ runId: null,
1590
+ via: row.via,
1591
+ state: "held",
1592
+ updatedAt: now
1593
+ };
1594
+ this.store.insertDelivery(d);
1595
+ out.push(d);
1596
+ }
1597
+ return out;
1598
+ });
1599
+ for (const messageId of retired) this.emit({
1600
+ type: "remote",
1601
+ messageId,
1602
+ recipient
1603
+ });
1604
+ for (const d of claimed) this.emit({
1605
+ type: "delivery",
1606
+ delivery: d
1607
+ });
1608
+ return claimed;
1609
+ }
1610
+ moveToRemote(messageId, recipient, homes, state = "forwarded") {
1611
+ const now = this.nowIso();
1612
+ if (this.store.transaction(() => {
1613
+ const local = this.store.deliveries({
1614
+ messageId,
1615
+ recipient
1616
+ });
1617
+ const [first] = local;
1618
+ if (first === void 0) return false;
1619
+ for (const d of local) this.store.deleteDelivery(d.id);
1620
+ this.putRemote(first, homes, state, now);
1621
+ return true;
1622
+ })) this.emit({
1623
+ type: "remote",
1624
+ messageId,
1625
+ recipient
1626
+ });
1627
+ }
1628
+ putRemote(d, homes, state, now) {
1629
+ const row = {
1630
+ messageId: d.messageId,
1631
+ recipient: d.recipient,
1632
+ via: d.via,
1633
+ state,
1634
+ homes,
1635
+ wakeAt: null,
1636
+ refusedBy: [],
1637
+ updatedAt: now
1638
+ };
1639
+ if (this.store.insertRemote(row)) return;
1640
+ const [existing] = this.store.remoteDeliveries({
1641
+ messageId: d.messageId,
1642
+ recipient: d.recipient
1643
+ });
1644
+ if (existing === void 0) return;
1645
+ this.store.setRemote(d.messageId, d.recipient, {
1646
+ state: maxRemote(existing.state, state),
1647
+ homes: [...new Set([...existing.homes, ...homes])]
1648
+ }, now);
1649
+ }
1650
+ mayNoteSuperseded(questionId, answerId) {
1651
+ return this.store.answerCandidates(questionId).filter((c) => c.settledAs === "superseded" && c.message.id !== answerId).length < MAX_SUPERSEDED_NOTICES;
1652
+ }
1653
+ async noticeTo(recipient, about, body) {
1654
+ await this.send({
1655
+ to: [this.deliverableAddress(recipient)],
1656
+ kind: "notice",
1657
+ body,
1658
+ refs: [{
1659
+ type: "message",
1660
+ id: about.id
1661
+ }]
1662
+ }, SYSTEM_SENDER);
1663
+ }
1664
+ async runWake(message, settled, skip) {
1665
+ const wakesRuns = wakesEndedRuns(message);
1666
+ for (const d of settled) {
1667
+ if (d.state !== "held" || skip.has(d.recipient)) continue;
1668
+ if (!d.recipient.startsWith("task:") && !(wakesRuns && d.recipient.startsWith("run:"))) continue;
1669
+ const request = {
1670
+ type: "wake",
1671
+ target: d.recipient,
1672
+ message
1673
+ };
1674
+ if (message.origin !== void 0) request.origin = message.origin;
1675
+ const ruling = this.host.decide(request);
1676
+ if (ruling === "allow") {
1677
+ let result;
1678
+ try {
1679
+ result = await this.host.wake(d.recipient, message);
1680
+ } catch (err) {
1681
+ console.error("messaging hook failed", err);
1682
+ result = {
1683
+ ok: false,
1684
+ reason: err instanceof Error ? err.message : String(err)
1685
+ };
1686
+ }
1687
+ if (!result.ok) await this.noticeTo(message.from, message, `Could not wake ${d.recipient}: ${result.reason}. Your message is waiting for it.`);
1688
+ } else if (ruling === "deny") await this.noticeTo(message.from, message, `Waking ${d.recipient} was not allowed. Your message is waiting for it.`);
1689
+ else {
1690
+ if (this.openWakeGates(d.recipient) >= this.limits.openWakeGatesPerTarget) continue;
1691
+ const label = this.remoteLabel(message);
1692
+ const who = label === void 0 ? message.from : `${message.from} (remote: ${label})`;
1693
+ await this.send({
1694
+ to: [this.host.owner(d.recipient, message.from)],
1695
+ kind: "question",
1696
+ blocking: true,
1697
+ choices: ["approve", "deny"],
1698
+ body: `${who} wants to wake ${d.recipient}:\n\n> ${firstLine(message.body)}`,
1699
+ data: {
1700
+ type: "wake",
1701
+ target: d.recipient,
1702
+ message: message.id
1703
+ },
1704
+ refs: [{
1705
+ type: "message",
1706
+ id: message.id
1707
+ }]
1708
+ }, SYSTEM_SENDER);
1709
+ }
1710
+ }
1711
+ }
1712
+ checkNewThreads(input, replyTarget, sender) {
1713
+ if (replyTarget !== null || !isAgentAuthored(sender.address) || !isAgentThread(input.to)) return;
1714
+ const started = this.store.countAgentThreadsFrom(sender.address, this.hourAgoIso());
1715
+ if (started < this.limits.agentThreadsPerHour) return;
1716
+ throw new MessagingError("limited", `${sender.address} started ${started} new threads with other agents this hour; message a human or wait`, "to");
1717
+ }
1718
+ openWakeGates(target) {
1719
+ return this.openBlocking().filter((m) => {
1720
+ const gate = gateOf(m);
1721
+ return gate?.type === "wake" && gate.target === target;
1722
+ }).length;
1723
+ }
1724
+ async checkBreaker(replyTarget, sender, remote = false) {
1725
+ const agentAuthored = isAgentAuthored(sender.address) || remote && sender.address === SYSTEM_ADDRESS;
1726
+ if (replyTarget === null || !agentAuthored) return;
1727
+ const since = this.hourAgoIso();
1728
+ const count = this.store.countAgentAuthored(replyTarget.thread, since, SYSTEM_ADDRESS);
1729
+ if (count < this.limits.agentTurnsPerThreadPerHour) return;
1730
+ if (!this.store.thread(replyTarget.thread).some((m) => isSystemMarker(m, "x-breaker") && m.createdAt >= since)) await this.send({
1731
+ to: [this.host.owner(sender.address)],
1732
+ kind: "notice",
1733
+ replyTo: replyTarget.id,
1734
+ body: `Agents have sent ${count} messages in thread ${replyTarget.thread} this hour; further agent replies are paused.`,
1735
+ data: {
1736
+ type: "x-breaker",
1737
+ thread: replyTarget.thread
1738
+ }
1739
+ }, SYSTEM_SENDER);
1740
+ throw new MessagingError("limited", `thread ${replyTarget.thread} hit the agent turn limit; a human has been asked to step in`, "replyTo");
1741
+ }
1742
+ };
1743
+ function alreadyAnswered(questionId) {
1744
+ return new MessagingError("conflict", `${questionId} is already answered`, "replyTo");
1745
+ }
1746
+ function decides(sender) {
1747
+ return sender.address === SYSTEM_ADDRESS || sender.canDecide && sender.address.startsWith("human:");
1748
+ }
1749
+ function maxRemote(current, reported) {
1750
+ if (current === "refused") return reported;
1751
+ return REMOTE_RANK[reported] > REMOTE_RANK[current] ? reported : current;
1752
+ }
1753
+ function remoteRow(messageId, t, now) {
1754
+ return {
1755
+ messageId,
1756
+ recipient: t.recipient,
1757
+ via: t.via,
1758
+ state: "forwarded",
1759
+ homes: t.homes,
1760
+ wakeAt: t.wakeAt ?? null,
1761
+ refusedBy: [],
1762
+ updatedAt: now
1763
+ };
1764
+ }
1765
+ function closeCopy(q, id, reason, settler, now, hlc) {
1766
+ const close = {
1767
+ id,
1768
+ thread: q.thread,
1769
+ replyTo: q.id,
1770
+ from: SYSTEM_ADDRESS,
1771
+ to: [q.from],
1772
+ kind: "answer",
1773
+ body: `Closed: ${reason}`,
1774
+ refs: [],
1775
+ data: {
1776
+ type: "x-closed",
1777
+ reason
1778
+ },
1779
+ urgent: false,
1780
+ blocking: false,
1781
+ wake: "none",
1782
+ createdAt: now,
1783
+ origin: settler
1784
+ };
1785
+ if (hlc !== void 0) close.hlc = hlc;
1786
+ return close;
1787
+ }
1788
+ function toSendInput(m) {
1789
+ const input = {
1790
+ to: m.to,
1791
+ kind: m.kind,
1792
+ body: m.body,
1793
+ refs: m.refs,
1794
+ urgent: m.urgent,
1795
+ blocking: m.blocking,
1796
+ replyTo: m.replyTo,
1797
+ wake: m.wake
1798
+ };
1799
+ if (m.data !== void 0) input.data = m.data;
1800
+ if (m.choices !== void 0) input.choices = m.choices;
1801
+ if (m.choice !== void 0) input.choice = m.choice;
1802
+ if (m.session !== void 0) input.session = m.session;
1803
+ return input;
1804
+ }
1805
+ function malformed(field, why) {
1806
+ throw new MessagingError("invalid", `${field}: ${why}`, field);
1807
+ }
1808
+ function checkReceivedEnvelope(m) {
1809
+ if (!isIdentifier(m.id)) malformed("id", "expected a message id");
1810
+ if (!isIdentifier(m.thread)) malformed("thread", "expected a message id");
1811
+ if (m.replyTo !== null && !isIdentifier(m.replyTo)) malformed("replyTo", "expected a message id or null");
1812
+ for (const field of [
1813
+ "from",
1814
+ "kind",
1815
+ "body",
1816
+ "createdAt"
1817
+ ]) if (typeof m[field] !== "string") malformed(field, "expected a string");
1818
+ for (const field of [
1819
+ "session",
1820
+ "hlc",
1821
+ "choice"
1822
+ ]) if (m[field] !== void 0 && typeof m[field] !== "string") malformed(field, "expected a string");
1823
+ if (!Array.isArray(m.to)) malformed("to", "expected a list");
1824
+ checkStrings(m.to, "to");
1825
+ if (m.choices !== void 0) {
1826
+ if (!Array.isArray(m.choices)) malformed("choices", "expected a list");
1827
+ checkStrings(m.choices, "choices");
1828
+ }
1829
+ if (!Array.isArray(m.refs) || !m.refs.every((r) => typeof r === "object" && r !== null)) malformed("refs", "expected a list of refs");
1830
+ m.refs.forEach((r, i) => {
1831
+ if (typeof r.type !== "string") malformed(`refs[${i}].type`, "expected a string");
1832
+ if (typeof r.id !== "string") malformed(`refs[${i}].id`, "expected a string");
1833
+ if (r.at !== void 0 && typeof r.at !== "string") malformed(`refs[${i}].at`, "expected a string");
1834
+ });
1835
+ if (typeof m.urgent !== "boolean") malformed("urgent", "expected a boolean");
1836
+ if (typeof m.blocking !== "boolean") malformed("blocking", "expected a boolean");
1837
+ if (m.wake !== "none" && m.wake !== "request") malformed("wake", "expected none or request");
1838
+ }
1839
+ function checkStrings(list, field) {
1840
+ list.forEach((v, i) => {
1841
+ if (typeof v !== "string") malformed(`${field}[${i}]`, "expected a string");
1842
+ });
1843
+ }
1844
+ function checkReceivedTargets(origin) {
1845
+ if (!Array.isArray(origin.targets)) malformed("targets", "expected a list");
1846
+ if (origin.forwardTarget !== void 0 && typeof origin.forwardTarget !== "string") malformed("forwardTarget", "expected an address");
1847
+ origin.targets.forEach((raw, i) => {
1848
+ const at = `targets[${i}]`;
1849
+ const t = typeof raw === "object" && raw !== null ? raw : {};
1850
+ if (typeof t.recipient !== "string") malformed(`${at}.recipient`, "expected an address");
1851
+ if (t.via !== "direct" && t.via !== "channel") malformed(`${at}.via`, "expected direct or channel");
1852
+ if (!Array.isArray(t.homes)) malformed(`${at}.homes`, "expected a list");
1853
+ checkStrings(t.homes, `${at}.homes`);
1854
+ if (t.wakeAt !== void 0 && typeof t.wakeAt !== "string") malformed(`${at}.wakeAt`, "expected a replica id");
1855
+ if (isFederationLocalAddress(t.recipient)) throw new MessagingError("forbidden", LOCAL_ONLY_TEXT.participant, "to");
1856
+ parseAddress(t.recipient, at);
1857
+ });
1858
+ }
1859
+ function checkReceivedThread(m, replyTarget, root) {
1860
+ if (m.replyTo === m.id) malformed("replyTo", "a message cannot reply to itself");
1861
+ if (root !== null && root.thread !== root.id) malformed("thread", `${root.id} is a reply, not a thread's first message`);
1862
+ if (m.replyTo === null && m.thread !== m.id) malformed("thread", "a message that replies to nothing starts its own thread");
1863
+ if (replyTarget !== null && m.thread !== replyTarget.thread) malformed("thread", `expected ${replyTarget.thread}, the thread of ${replyTarget.id}`);
1864
+ }
1865
+ const SIGNED_FIELDS = [
1866
+ "id",
1867
+ "thread",
1868
+ "replyTo",
1869
+ "from",
1870
+ "session",
1871
+ "to",
1872
+ "kind",
1873
+ "body",
1874
+ "refs",
1875
+ "data",
1876
+ "urgent",
1877
+ "blocking",
1878
+ "choices",
1879
+ "choice",
1880
+ "wake",
1881
+ "createdAt",
1882
+ "hlc"
1883
+ ];
1884
+ function sameSignedContent(stored, settledAs, incoming) {
1885
+ const storedKind = settledAs === "superseded" || settledAs === "candidate" ? "answer" : stored.kind;
1886
+ const signed = (m, kind) => canonical(Object.fromEntries(SIGNED_FIELDS.map((f) => [f, f === "kind" ? kind : m[f]])));
1887
+ return signed(stored, storedKind) === signed(incoming, incoming.kind);
1888
+ }
1889
+ function canonical(value) {
1890
+ if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
1891
+ if (value !== null && typeof value === "object") {
1892
+ const record = value;
1893
+ return `{${Object.keys(record).filter((k) => record[k] !== void 0).sort().map((k) => `${JSON.stringify(k)}:${canonical(record[k])}`).join(",")}}`;
1894
+ }
1895
+ return JSON.stringify(value) ?? "null";
1896
+ }
1897
+ function wakesEndedRuns(message) {
1898
+ return message.wake === "request" && message.origin === void 0 && message.from.startsWith("human:");
1899
+ }
1900
+ function isAgentThread(to) {
1901
+ return !to.some((a) => a.startsWith("human:")) && to.some((a) => a.startsWith("agent:") || a.startsWith("run:") || a.startsWith("task:"));
1902
+ }
1903
+
1904
+ //#endregion
1905
+ //#region src/store.ts
1906
+ const REMOTE_STATES = [
1907
+ "forwarded",
1908
+ "held",
1909
+ "pushed",
1910
+ "notified",
1911
+ "read",
1912
+ "answered",
1913
+ "refused"
1914
+ ];
1915
+
1916
+ //#endregion
1917
+ //#region src/sqliteStore.ts
1918
+ const MESSAGES_DB_VERSION = 1;
1919
+ const DDL = `
1920
+ CREATE TABLE IF NOT EXISTS messages (
1921
+ id TEXT PRIMARY KEY, thread TEXT NOT NULL, reply_to TEXT, from_addr TEXT NOT NULL,
1922
+ session TEXT, kind TEXT NOT NULL, body TEXT NOT NULL, refs_json TEXT NOT NULL,
1923
+ data_json TEXT, urgent INTEGER NOT NULL, blocking INTEGER NOT NULL,
1924
+ choices_json TEXT, choice TEXT, wake TEXT NOT NULL, created_at TEXT NOT NULL
1925
+ );
1926
+ CREATE INDEX IF NOT EXISTS messages_thread ON messages (thread, id);
1927
+ CREATE INDEX IF NOT EXISTS messages_reply ON messages (reply_to);
1928
+ CREATE INDEX IF NOT EXISTS messages_from ON messages (from_addr, created_at);
1929
+ CREATE UNIQUE INDEX IF NOT EXISTS messages_one_answer ON messages (reply_to) WHERE kind = 'answer';
1930
+ CREATE TABLE IF NOT EXISTS recipients (
1931
+ message_id TEXT NOT NULL, position INTEGER NOT NULL, addr TEXT NOT NULL,
1932
+ PRIMARY KEY (message_id, position)
1933
+ );
1934
+ CREATE INDEX IF NOT EXISTS recipients_addr ON recipients (addr);
1935
+ CREATE TABLE IF NOT EXISTS deliveries (
1936
+ id TEXT PRIMARY KEY, message_id TEXT NOT NULL, recipient TEXT NOT NULL,
1937
+ run_id TEXT, via TEXT NOT NULL, state TEXT NOT NULL, updated_at TEXT NOT NULL
1938
+ );
1939
+ CREATE INDEX IF NOT EXISTS deliveries_recipient ON deliveries (recipient, state);
1940
+ CREATE INDEX IF NOT EXISTS deliveries_message ON deliveries (message_id);
1941
+ CREATE INDEX IF NOT EXISTS deliveries_run ON deliveries (run_id);
1942
+ CREATE TABLE IF NOT EXISTS channels (
1943
+ name TEXT PRIMARY KEY, created_at TEXT NOT NULL, auto INTEGER NOT NULL
1944
+ );
1945
+ CREATE TABLE IF NOT EXISTS members (
1946
+ channel TEXT NOT NULL, addr TEXT NOT NULL, joined_at TEXT NOT NULL,
1947
+ PRIMARY KEY (channel, addr)
1948
+ );
1949
+ CREATE TABLE IF NOT EXISTS agents (
1950
+ addr TEXT PRIMARY KEY, display_name TEXT NOT NULL, client TEXT NOT NULL,
1951
+ token_hash TEXT NOT NULL UNIQUE, status TEXT NOT NULL, muted INTEGER NOT NULL,
1952
+ approved_by TEXT, created_at TEXT NOT NULL
1953
+ );
1954
+ CREATE TABLE IF NOT EXISTS gate_effects (
1955
+ question_id TEXT PRIMARY KEY, applied_at TEXT NOT NULL
1956
+ );
1957
+ CREATE TABLE IF NOT EXISTS voided_answers (
1958
+ answer_id TEXT PRIMARY KEY, question_id TEXT NOT NULL, at TEXT NOT NULL
1959
+ );
1960
+ `;
1961
+ function openMessagesDb(path) {
1962
+ const db = openSqliteDb(path);
1963
+ const existing = dbVersion(db);
1964
+ if (existing > MESSAGES_DB_VERSION) {
1965
+ db.close();
1966
+ throw new Error(`messages database at ${path} was written by a newer schema (version ${existing}, this build understands ${MESSAGES_DB_VERSION})`);
1967
+ }
1968
+ db.exec(DDL);
1969
+ addIdemKey(db);
1970
+ addFederationSchema(db);
1971
+ addMessageRefs(db);
1972
+ db.exec(`PRAGMA user_version = ${MESSAGES_DB_VERSION}`);
1973
+ return db;
1974
+ }
1975
+ function addIdemKey(db) {
1976
+ if (!hasIdemKey(db)) try {
1977
+ db.exec("ALTER TABLE messages ADD COLUMN idem_key TEXT");
1978
+ } catch (err) {
1979
+ if (!hasIdemKey(db)) throw err;
1980
+ }
1981
+ db.exec("CREATE UNIQUE INDEX IF NOT EXISTS messages_idem ON messages (from_addr, idem_key) WHERE idem_key IS NOT NULL");
1982
+ }
1983
+ function hasIdemKey(db) {
1984
+ return messageColumns(db).has("idem_key");
1985
+ }
1986
+ function messageColumns(db) {
1987
+ return new Set(queryAll(db, "PRAGMA table_info(messages)").map((c) => c.name));
1988
+ }
1989
+ const FEDERATION_DDL = `
1990
+ CREATE TABLE IF NOT EXISTS remote_deliveries (
1991
+ message_id TEXT NOT NULL, recipient TEXT NOT NULL, via TEXT NOT NULL,
1992
+ state TEXT NOT NULL, homes_json TEXT NOT NULL, wake_at TEXT, refused_by TEXT,
1993
+ updated_at TEXT NOT NULL, PRIMARY KEY (message_id, recipient)
1994
+ );
1995
+ CREATE INDEX IF NOT EXISTS remote_recipient ON remote_deliveries (recipient, state);
1996
+ CREATE TABLE IF NOT EXISTS settlements (
1997
+ question_id TEXT PRIMARY KEY, answer_id TEXT, closed_reason TEXT,
1998
+ settler TEXT NOT NULL, at TEXT NOT NULL
1999
+ );
2000
+ CREATE TABLE IF NOT EXISTS early_settlements (
2001
+ question_id TEXT NOT NULL, settler TEXT NOT NULL, answer_id TEXT,
2002
+ closed_reason TEXT, at TEXT NOT NULL, PRIMARY KEY (question_id, settler)
2003
+ );
2004
+ `;
2005
+ const FEDERATION_COLUMNS = [
2006
+ "origin",
2007
+ "hlc",
2008
+ "received_at",
2009
+ "settled_as"
2010
+ ];
2011
+ const SETTLED_AS = [
2012
+ "pending",
2013
+ "accepted",
2014
+ "superseded",
2015
+ "candidate"
2016
+ ];
2017
+ function addFederationSchema(db) {
2018
+ const have = messageColumns(db);
2019
+ for (const column of FEDERATION_COLUMNS) {
2020
+ if (have.has(column)) continue;
2021
+ try {
2022
+ db.exec(`ALTER TABLE messages ADD COLUMN ${column} TEXT`);
2023
+ } catch (err) {
2024
+ if (!messageColumns(db).has(column)) throw err;
2025
+ }
2026
+ }
2027
+ db.exec("CREATE INDEX IF NOT EXISTS messages_thread_hlc ON messages (thread, hlc, id)");
2028
+ db.exec(FEDERATION_DDL);
2029
+ }
2030
+ function addMessageRefs(db) {
2031
+ const existed = queryOne(db, "SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'message_refs'") !== void 0;
2032
+ db.exec(`CREATE TABLE IF NOT EXISTS message_refs (
2033
+ message_id TEXT NOT NULL, type TEXT NOT NULL, ref_id TEXT NOT NULL,
2034
+ PRIMARY KEY (message_id, type, ref_id)
2035
+ );
2036
+ CREATE INDEX IF NOT EXISTS message_refs_target ON message_refs (type, ref_id);`);
2037
+ if (existed) return;
2038
+ db.exec(`INSERT OR IGNORE INTO message_refs (message_id, type, ref_id)
2039
+ SELECT m.id, json_extract(j.value, '$.type'), json_extract(j.value, '$.id')
2040
+ FROM messages m, json_each(m.refs_json) j
2041
+ WHERE json_extract(j.value, '$.type') IS NOT NULL
2042
+ AND json_extract(j.value, '$.id') IS NOT NULL`);
2043
+ }
2044
+ function oneOf(value, allowed, what) {
2045
+ if (!allowed.includes(value)) throw new Error(`messages.db: bad ${what} ${JSON.stringify(value)}`);
2046
+ return value;
2047
+ }
2048
+ var SqliteMessageStore = class {
2049
+ depth = 0;
2050
+ constructor(db) {
2051
+ this.db = db;
2052
+ }
2053
+ transaction(fn) {
2054
+ if (this.depth > 0) return fn();
2055
+ this.db.exec("BEGIN");
2056
+ this.depth++;
2057
+ try {
2058
+ const out = fn();
2059
+ this.db.exec("COMMIT");
2060
+ return out;
2061
+ } catch (err) {
2062
+ this.db.exec("ROLLBACK");
2063
+ throw err;
2064
+ } finally {
2065
+ this.depth--;
2066
+ }
2067
+ }
2068
+ insertMessage(m, idemKey, meta) {
2069
+ this.transaction(() => {
2070
+ this.db.prepare(`INSERT INTO messages (id, thread, reply_to, from_addr, session, kind, body, refs_json, data_json, urgent, blocking, choices_json, choice, wake, created_at, idem_key, origin, hlc, received_at, settled_as) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`).run(m.id, m.thread, m.replyTo, m.from, m.session ?? null, m.kind, m.body, JSON.stringify(m.refs), m.data === void 0 ? null : JSON.stringify(m.data), m.urgent ? 1 : 0, m.blocking ? 1 : 0, m.choices === void 0 ? null : JSON.stringify(m.choices), m.choice ?? null, m.wake, m.createdAt, idemKey ?? null, m.origin ?? null, m.hlc ?? null, meta?.receivedAt ?? null, meta?.settledAs ?? null);
2071
+ const insert = this.db.prepare("INSERT INTO recipients (message_id, position, addr) VALUES (?,?,?)");
2072
+ m.to.forEach((addr, i) => insert.run(m.id, i, addr));
2073
+ const ref = this.db.prepare("INSERT OR IGNORE INTO message_refs (message_id, type, ref_id) VALUES (?,?,?)");
2074
+ for (const r of m.refs) ref.run(m.id, r.type, r.id);
2075
+ });
2076
+ }
2077
+ toMessage(row) {
2078
+ const to = queryAll(this.db, "SELECT addr FROM recipients WHERE message_id = ? ORDER BY position", [row.id]).map((r) => r.addr);
2079
+ const message = {
2080
+ id: row.id,
2081
+ thread: row.thread,
2082
+ replyTo: row.reply_to,
2083
+ from: row.from_addr,
2084
+ to,
2085
+ kind: row.kind,
2086
+ body: row.body,
2087
+ refs: JSON.parse(row.refs_json),
2088
+ urgent: row.urgent === 1,
2089
+ blocking: row.blocking === 1,
2090
+ wake: oneOf(row.wake, ["none", "request"], "wake"),
2091
+ createdAt: row.created_at
2092
+ };
2093
+ if (row.session !== null) message.session = row.session;
2094
+ if (row.data_json !== null) message.data = JSON.parse(row.data_json);
2095
+ if (row.choices_json !== null) message.choices = JSON.parse(row.choices_json);
2096
+ if (row.choice !== null) message.choice = row.choice;
2097
+ if (row.origin !== null) message.origin = row.origin;
2098
+ if (row.hlc !== null) message.hlc = row.hlc;
2099
+ return message;
2100
+ }
2101
+ getMessage(id) {
2102
+ const row = queryOne(this.db, "SELECT * FROM messages WHERE id = ?", [id]);
2103
+ return row === void 0 ? null : this.toMessage(row);
2104
+ }
2105
+ byIdemKey(from, key) {
2106
+ const row = queryOne(this.db, "SELECT * FROM messages WHERE from_addr = ? AND idem_key = ?", [from, key]);
2107
+ return row === void 0 ? null : this.toMessage(row);
2108
+ }
2109
+ idemKeysFor(messageIds) {
2110
+ const out = /* @__PURE__ */ new Map();
2111
+ for (let i = 0; i < messageIds.length; i += 500) {
2112
+ const chunk = messageIds.slice(i, i + 500);
2113
+ const rows = queryAll(this.db, `SELECT id, idem_key FROM messages WHERE idem_key IS NOT NULL AND id IN (${chunk.map(() => "?").join(",")})`, chunk);
2114
+ for (const r of rows) out.set(r.id, r.idem_key);
2115
+ }
2116
+ return out;
2117
+ }
2118
+ messagesFrom(address, sinceIso, kinds) {
2119
+ const byKind = kinds === void 0 ? "" : ` AND kind IN (${kinds.map(() => "?").join(",")})`;
2120
+ return queryAll(this.db, `SELECT * FROM messages WHERE from_addr = ? AND created_at >= ?${byKind} ORDER BY id`, [
2121
+ address,
2122
+ sinceIso,
2123
+ ...kinds ?? []
2124
+ ]).map((r) => this.toMessage(r));
2125
+ }
2126
+ thread(threadId) {
2127
+ return queryAll(this.db, "SELECT * FROM messages WHERE thread = ? ORDER BY (hlc IS NOT NULL), hlc, id", [threadId]).map((r) => this.toMessage(r));
2128
+ }
2129
+ answersTo(messageId) {
2130
+ return queryAll(this.db, "SELECT * FROM messages WHERE reply_to = ? AND kind = 'answer' ORDER BY id", [messageId]).map((r) => this.toMessage(r));
2131
+ }
2132
+ openBlocking() {
2133
+ return queryAll(this.db, "SELECT m.* FROM messages m WHERE m.blocking = 1 AND NOT EXISTS (SELECT 1 FROM messages a WHERE a.reply_to = m.id AND a.kind = 'answer') ORDER BY m.id").map((r) => this.toMessage(r));
2134
+ }
2135
+ insertDelivery(d) {
2136
+ this.db.prepare("INSERT INTO deliveries (id, message_id, recipient, run_id, via, state, updated_at) VALUES (?,?,?,?,?,?,?)").run(d.id, d.messageId, d.recipient, d.runId, d.via, d.state, d.updatedAt);
2137
+ }
2138
+ deleteDelivery(id) {
2139
+ return Number(this.db.prepare("DELETE FROM deliveries WHERE id = ?").run(id).changes) > 0;
2140
+ }
2141
+ toDelivery(r) {
2142
+ return {
2143
+ id: r.id,
2144
+ messageId: r.message_id,
2145
+ recipient: r.recipient,
2146
+ runId: r.run_id,
2147
+ via: oneOf(r.via, ["direct", "channel"], "via"),
2148
+ state: oneOf(r.state, DELIVERY_STATES, "state"),
2149
+ updatedAt: r.updated_at
2150
+ };
2151
+ }
2152
+ getDelivery(id) {
2153
+ const row = queryOne(this.db, "SELECT * FROM deliveries WHERE id = ?", [id]);
2154
+ return row === void 0 ? null : this.toDelivery(row);
2155
+ }
2156
+ deliveries(filter) {
2157
+ const where = [];
2158
+ const params = [];
2159
+ if (filter.recipient !== void 0) {
2160
+ where.push("recipient = ?");
2161
+ params.push(filter.recipient);
2162
+ }
2163
+ if (filter.runId !== void 0) {
2164
+ where.push("run_id = ?");
2165
+ params.push(filter.runId);
2166
+ }
2167
+ if (filter.recipientPrefix !== void 0) {
2168
+ where.push("recipient LIKE ? ESCAPE '\\'");
2169
+ params.push(`${filter.recipientPrefix.replace(/[\\%_]/g, "\\$&")}%`);
2170
+ }
2171
+ if (filter.messageId !== void 0) {
2172
+ where.push("message_id = ?");
2173
+ params.push(filter.messageId);
2174
+ }
2175
+ if (filter.states !== void 0) {
2176
+ const placeholders = filter.states.map(() => "?").join(",");
2177
+ where.push(`state IN (${placeholders.length > 0 ? placeholders : "''"})`);
2178
+ params.push(...filter.states);
2179
+ }
2180
+ const sql = `SELECT * FROM deliveries${where.length > 0 ? ` WHERE ${where.join(" AND ")}` : ""} ORDER BY id`;
2181
+ return queryAll(this.db, sql, params).map((r) => this.toDelivery(r));
2182
+ }
2183
+ setDelivery(id, state, runId, at, expected) {
2184
+ const result = expected === void 0 ? this.db.prepare("UPDATE deliveries SET state = ?, run_id = ?, updated_at = ? WHERE id = ?").run(state, runId, at, id) : this.db.prepare("UPDATE deliveries SET state = ?, run_id = ?, updated_at = ? WHERE id = ? AND state = ?").run(state, runId, at, id, expected);
2185
+ return Number(result.changes) > 0;
2186
+ }
2187
+ markGateApplied(questionId, at) {
2188
+ this.db.prepare("INSERT INTO gate_effects (question_id, applied_at) VALUES (?,?) ON CONFLICT (question_id) DO NOTHING").run(questionId, at);
2189
+ }
2190
+ voidAnswer(answerId, questionId, at) {
2191
+ return this.transaction(() => {
2192
+ const changed = this.db.prepare("UPDATE messages SET kind = 'message' WHERE id = ? AND reply_to = ? AND kind = 'answer'").run(answerId, questionId);
2193
+ if (Number(changed.changes) === 0) return false;
2194
+ this.db.prepare("INSERT INTO voided_answers (answer_id, question_id, at) VALUES (?,?,?) ON CONFLICT (answer_id) DO NOTHING").run(answerId, questionId, at);
2195
+ return true;
2196
+ });
2197
+ }
2198
+ unappliedAnsweredGates() {
2199
+ return queryAll(this.db, `SELECT q.id AS question_id, a.id AS answer_id FROM messages q
2200
+ JOIN messages a ON a.reply_to = q.id AND a.kind = 'answer'
2201
+ WHERE q.data_json LIKE '%"type":%'
2202
+ AND NOT EXISTS (SELECT 1 FROM gate_effects g WHERE g.question_id = q.id)
2203
+ ORDER BY q.id`).flatMap((r) => {
2204
+ const question = this.getMessage(r.question_id);
2205
+ const answer = this.getMessage(r.answer_id);
2206
+ if (question === null || answer === null || !hasGateData(question)) return [];
2207
+ return isSystemMarker(answer, "x-closed") ? [] : [{
2208
+ question,
2209
+ answer
2210
+ }];
2211
+ });
2212
+ }
2213
+ countFrom(from, sinceIso, urgentOnly, origin, untilIso) {
2214
+ const row = queryOne(this.db, `SELECT COUNT(*) AS n FROM messages WHERE from_addr = ? AND COALESCE(received_at, created_at) >= ? AND COALESCE(received_at, created_at) <= ?${urgentOnly ? " AND urgent = 1" : ""}${origin === void 0 ? "" : " AND origin = ?"}`, origin === void 0 ? [
2215
+ from,
2216
+ sinceIso,
2217
+ untilIso ?? "9999"
2218
+ ] : [
2219
+ from,
2220
+ sinceIso,
2221
+ untilIso ?? "9999",
2222
+ origin
2223
+ ]);
2224
+ return row === void 0 ? 0 : Number(row.n);
2225
+ }
2226
+ countDeliveredTo(recipient, sinceIso, untilIso) {
2227
+ const row = queryOne(this.db, "SELECT COUNT(DISTINCT m.id) AS n FROM deliveries d JOIN messages m ON m.id = d.message_id WHERE d.recipient = ? AND m.created_at >= ? AND m.created_at <= ?", [
2228
+ recipient,
2229
+ sinceIso,
2230
+ untilIso ?? "9999"
2231
+ ]);
2232
+ return row === void 0 ? 0 : Number(row.n);
2233
+ }
2234
+ countAgentThreadsFrom(from, sinceIso) {
2235
+ const row = queryOne(this.db, "SELECT COUNT(*) AS n FROM messages m WHERE m.from_addr = ? AND m.thread = m.id AND COALESCE(m.received_at, m.created_at) >= ? AND EXISTS (SELECT 1 FROM recipients r WHERE r.message_id = m.id AND (r.addr LIKE 'agent:%' OR r.addr LIKE 'run:%' OR r.addr LIKE 'task:%')) AND NOT EXISTS (SELECT 1 FROM recipients r WHERE r.message_id = m.id AND r.addr LIKE 'human:%') AND NOT EXISTS (SELECT 1 FROM deliveries d WHERE d.message_id = m.id AND d.recipient LIKE 'human:%')", [from, sinceIso]);
2236
+ return row === void 0 ? 0 : Number(row.n);
2237
+ }
2238
+ countAgentAuthored(threadId, sinceIso, exclude) {
2239
+ const row = queryOne(this.db, "SELECT COUNT(*) AS n FROM messages WHERE thread = ? AND COALESCE(received_at, created_at) >= ? AND (from_addr != ? OR origin IS NOT NULL) AND (from_addr LIKE 'run:%' OR from_addr LIKE 'agent:%' OR from_addr LIKE 'a2a:%')", [
2240
+ threadId,
2241
+ sinceIso,
2242
+ exclude
2243
+ ]);
2244
+ return row === void 0 ? 0 : Number(row.n);
2245
+ }
2246
+ settledAs(messageId) {
2247
+ const row = queryOne(this.db, "SELECT settled_as FROM messages WHERE id = ?", [messageId]);
2248
+ return row === void 0 || row.settled_as === null ? null : oneOf(row.settled_as, SETTLED_AS, "settled_as");
2249
+ }
2250
+ setSettled(messageId, kind, settledAs) {
2251
+ this.db.prepare("UPDATE messages SET kind = ?, settled_as = ? WHERE id = ?").run(kind, settledAs, messageId);
2252
+ }
2253
+ answerCandidates(questionId) {
2254
+ return queryAll(this.db, "SELECT * FROM messages WHERE reply_to = ? AND (kind = 'answer' OR settled_as IS NOT NULL) ORDER BY rowid", [questionId]).map((r) => ({
2255
+ message: this.toMessage(r),
2256
+ settledAs: r.settled_as === null ? null : oneOf(r.settled_as, SETTLED_AS, "settled_as")
2257
+ }));
2258
+ }
2259
+ insertRemote(row) {
2260
+ const result = this.db.prepare("INSERT INTO remote_deliveries (message_id, recipient, via, state, homes_json, wake_at, refused_by, updated_at) VALUES (?,?,?,?,?,?,?,?) ON CONFLICT (message_id, recipient) DO NOTHING").run(row.messageId, row.recipient, row.via, row.state, JSON.stringify(row.homes), row.wakeAt, JSON.stringify(row.refusedBy), row.updatedAt);
2261
+ return Number(result.changes) > 0;
2262
+ }
2263
+ toRemote(r) {
2264
+ return {
2265
+ messageId: r.message_id,
2266
+ recipient: r.recipient,
2267
+ via: oneOf(r.via, ["direct", "channel"], "via"),
2268
+ state: oneOf(r.state, REMOTE_STATES, "remote state"),
2269
+ homes: JSON.parse(r.homes_json),
2270
+ wakeAt: r.wake_at,
2271
+ refusedBy: r.refused_by === null ? [] : JSON.parse(r.refused_by),
2272
+ updatedAt: r.updated_at
2273
+ };
2274
+ }
2275
+ remoteDeliveries(filter) {
2276
+ const where = [];
2277
+ const params = [];
2278
+ if (filter.messageId !== void 0) {
2279
+ where.push("message_id = ?");
2280
+ params.push(filter.messageId);
2281
+ }
2282
+ if (filter.recipient !== void 0) {
2283
+ where.push("recipient = ?");
2284
+ params.push(filter.recipient);
2285
+ }
2286
+ if (filter.states !== void 0) {
2287
+ const placeholders = filter.states.map(() => "?").join(",");
2288
+ where.push(`state IN (${placeholders.length > 0 ? placeholders : "''"})`);
2289
+ params.push(...filter.states);
2290
+ }
2291
+ const sql = `SELECT * FROM remote_deliveries${where.length > 0 ? ` WHERE ${where.join(" AND ")}` : ""} ORDER BY message_id, recipient`;
2292
+ return queryAll(this.db, sql, params).map((r) => this.toRemote(r));
2293
+ }
2294
+ setRemote(messageId, recipient, patch, at, expected) {
2295
+ const sets = ["updated_at = ?"];
2296
+ const params = [at];
2297
+ if (patch.state !== void 0) {
2298
+ sets.push("state = ?");
2299
+ params.push(patch.state);
2300
+ }
2301
+ if (patch.refusedBy !== void 0) {
2302
+ sets.push("refused_by = ?");
2303
+ params.push(JSON.stringify(patch.refusedBy));
2304
+ }
2305
+ if (patch.homes !== void 0) {
2306
+ sets.push("homes_json = ?");
2307
+ params.push(JSON.stringify(patch.homes));
2308
+ }
2309
+ params.push(messageId, recipient);
2310
+ if (expected !== void 0) params.push(expected);
2311
+ const result = this.db.prepare(`UPDATE remote_deliveries SET ${sets.join(", ")} WHERE message_id = ? AND recipient = ?${expected === void 0 ? "" : " AND state = ?"}`).run(...params);
2312
+ return Number(result.changes) > 0;
2313
+ }
2314
+ deleteRemote(messageId, recipient) {
2315
+ return Number(this.db.prepare("DELETE FROM remote_deliveries WHERE message_id = ? AND recipient = ?").run(messageId, recipient).changes) > 0;
2316
+ }
2317
+ settlement(questionId) {
2318
+ const row = queryOne(this.db, "SELECT * FROM settlements WHERE question_id = ?", [questionId]);
2319
+ return row === void 0 ? null : toSettlement(row);
2320
+ }
2321
+ putSettlement(s) {
2322
+ this.db.prepare(`INSERT INTO settlements (question_id, answer_id, closed_reason, settler, at) VALUES (?,?,?,?,?)
2323
+ ON CONFLICT (question_id) DO UPDATE SET answer_id = excluded.answer_id, closed_reason = excluded.closed_reason,
2324
+ settler = excluded.settler, at = excluded.at`).run(s.questionId, s.answerId, s.closedReason, s.settler, s.at);
2325
+ }
2326
+ putEarlySettlement(s) {
2327
+ this.db.prepare(`INSERT INTO early_settlements (question_id, settler, answer_id, closed_reason, at) VALUES (?,?,?,?,?)
2328
+ ON CONFLICT (question_id, settler) DO UPDATE SET answer_id = excluded.answer_id,
2329
+ closed_reason = excluded.closed_reason, at = excluded.at`).run(s.questionId, s.settler, s.answerId, s.closedReason, s.at);
2330
+ }
2331
+ earlySettlements(questionId) {
2332
+ return queryAll(this.db, "SELECT * FROM early_settlements WHERE question_id = ? ORDER BY at, settler", [questionId]).map(toSettlement);
2333
+ }
2334
+ clearEarlySettlements(questionId) {
2335
+ this.db.prepare("DELETE FROM early_settlements WHERE question_id = ?").run(questionId);
2336
+ }
2337
+ messagesAfter(rowid, limit) {
2338
+ return queryAll(this.db, "SELECT rowid AS rid, * FROM messages WHERE rowid > ? AND origin IS NULL ORDER BY rowid LIMIT ?", [rowid, limit]).map((r) => ({
2339
+ rowid: Number(r.rid),
2340
+ message: this.toMessage(r)
2341
+ }));
2342
+ }
2343
+ maxRowid() {
2344
+ const row = queryOne(this.db, "SELECT MAX(rowid) AS n FROM messages");
2345
+ return row === void 0 || row.n === null ? 0 : Number(row.n);
2346
+ }
2347
+ ensureChannel(name, at, auto) {
2348
+ this.db.prepare("INSERT INTO channels (name, created_at, auto) VALUES (?,?,?) ON CONFLICT (name) DO NOTHING").run(name, at, auto ? 1 : 0);
2349
+ }
2350
+ channels() {
2351
+ return queryAll(this.db, "SELECT * FROM channels ORDER BY name").map((r) => ({
2352
+ name: r.name,
2353
+ createdAt: r.created_at,
2354
+ auto: r.auto === 1
2355
+ }));
2356
+ }
2357
+ addMember(channel, member, at) {
2358
+ return Number(this.db.prepare("INSERT INTO members (channel, addr, joined_at) VALUES (?,?,?) ON CONFLICT (channel, addr) DO NOTHING").run(channel, member, at).changes) > 0;
2359
+ }
2360
+ removeMember(channel, member) {
2361
+ return Number(this.db.prepare("DELETE FROM members WHERE channel = ? AND addr = ?").run(channel, member).changes) > 0;
2362
+ }
2363
+ members(channel) {
2364
+ return queryAll(this.db, "SELECT addr FROM members WHERE channel = ? ORDER BY addr", [channel]).map((r) => r.addr);
2365
+ }
2366
+ channelsOf(member) {
2367
+ return queryAll(this.db, "SELECT channel FROM members WHERE addr = ? ORDER BY channel", [member]).map((r) => r.channel);
2368
+ }
2369
+ putAgent(a) {
2370
+ this.db.prepare(`INSERT INTO agents (addr, display_name, client, token_hash, status, muted, approved_by, created_at) VALUES (?,?,?,?,?,?,?,?)
2371
+ ON CONFLICT (addr) DO UPDATE SET display_name = excluded.display_name, client = excluded.client, token_hash = excluded.token_hash,
2372
+ status = excluded.status, muted = excluded.muted, approved_by = excluded.approved_by`).run(a.address, a.displayName, a.client, a.tokenHash, a.status, a.muted ? 1 : 0, a.approvedBy, a.createdAt);
2373
+ }
2374
+ toAgent(r) {
2375
+ return {
2376
+ address: r.addr,
2377
+ displayName: r.display_name,
2378
+ client: r.client,
2379
+ tokenHash: r.token_hash,
2380
+ status: oneOf(r.status, [
2381
+ "pending",
2382
+ "approved",
2383
+ "revoked"
2384
+ ], "agent status"),
2385
+ muted: r.muted === 1,
2386
+ approvedBy: r.approved_by,
2387
+ createdAt: r.created_at
2388
+ };
2389
+ }
2390
+ getAgent(address) {
2391
+ const row = queryOne(this.db, "SELECT * FROM agents WHERE addr = ?", [address]);
2392
+ return row === void 0 ? null : this.toAgent(row);
2393
+ }
2394
+ agentByTokenHash(hash) {
2395
+ const row = queryOne(this.db, "SELECT * FROM agents WHERE token_hash = ?", [hash]);
2396
+ return row === void 0 ? null : this.toAgent(row);
2397
+ }
2398
+ agents() {
2399
+ return queryAll(this.db, "SELECT * FROM agents ORDER BY addr").map((r) => this.toAgent(r));
2400
+ }
2401
+ recentThreads(limit, about) {
2402
+ if (about !== void 0 && about.length === 0) return [];
2403
+ const params = [];
2404
+ let where = "";
2405
+ if (about !== void 0) {
2406
+ const clause = threadsAbout(about);
2407
+ where = `WHERE thread IN (${clause.sql})`;
2408
+ params.push(...clause.params);
2409
+ }
2410
+ params.push(limit);
2411
+ return queryAll(this.db, `SELECT thread, MAX(rowid) AS last_rowid, COUNT(*) AS count
2412
+ FROM messages ${where} GROUP BY thread ORDER BY last_rowid DESC LIMIT ?`, params).flatMap((r) => {
2413
+ const root = this.getMessage(r.thread) ?? this.firstStored(r.thread);
2414
+ const last = this.byRowid(Number(r.last_rowid));
2415
+ return root === null || last === null ? [] : [{
2416
+ thread: r.thread,
2417
+ root,
2418
+ last,
2419
+ count: Number(r.count)
2420
+ }];
2421
+ });
2422
+ }
2423
+ /** Messages a conversation read matches, newest (highest id) first. */
2424
+ conversation(match, page) {
2425
+ const where = [];
2426
+ const params = [];
2427
+ if (match.kind === "pair") {
2428
+ const party = "(m.from_addr = ? OR EXISTS (SELECT 1 FROM recipients r WHERE r.message_id = m.id AND r.addr = ?))";
2429
+ where.push(`${party} AND ${party}`);
2430
+ params.push(match.a, match.a, match.b, match.b);
2431
+ } else if (match.kind === "about") {
2432
+ if (match.addresses.length === 0) return [];
2433
+ const clause = threadsAbout(match.addresses);
2434
+ where.push(`m.thread IN (${clause.sql})`);
2435
+ params.push(...clause.params);
2436
+ } else {
2437
+ where.push(`m.thread IN (SELECT t.thread FROM message_refs x
2438
+ JOIN messages t ON t.id = x.message_id WHERE x.type = ? AND x.ref_id = ?)`);
2439
+ params.push(match.type, match.id);
2440
+ }
2441
+ if (page.before !== void 0) {
2442
+ where.push("m.id < ?");
2443
+ params.push(page.before);
2444
+ }
2445
+ if (page.rootsOnly === true) where.push("m.id = m.thread");
2446
+ params.push(page.limit ?? 50);
2447
+ return queryAll(this.db, `SELECT m.* FROM messages m WHERE ${where.join(" AND ")} ORDER BY m.id DESC LIMIT ?`, params).map((r) => this.toMessage(r));
2448
+ }
2449
+ firstStored(threadId) {
2450
+ const row = queryOne(this.db, "SELECT * FROM messages WHERE thread = ? ORDER BY rowid LIMIT 1", [threadId]);
2451
+ return row === void 0 ? null : this.toMessage(row);
2452
+ }
2453
+ byRowid(rowid) {
2454
+ const row = queryOne(this.db, "SELECT * FROM messages WHERE rowid = ?", [rowid]);
2455
+ return row === void 0 ? null : this.toMessage(row);
2456
+ }
2457
+ };
2458
+ function threadsAbout(about) {
2459
+ const marks = about.map(() => "?").join(", ");
2460
+ const runIds = about.filter((a) => a.startsWith("run:")).map((a) => a.slice(4));
2461
+ return {
2462
+ sql: `SELECT thread FROM messages WHERE from_addr IN (${marks})
2463
+ UNION SELECT m.thread FROM messages m JOIN recipients r ON r.message_id = m.id WHERE r.addr IN (${marks})
2464
+ UNION SELECT m.thread FROM messages m JOIN deliveries d ON d.message_id = m.id
2465
+ WHERE d.recipient IN (${marks})${runIds.length === 0 ? "" : ` OR d.run_id IN (${runIds.map(() => "?").join(", ")})`}`,
2466
+ params: [
2467
+ ...about,
2468
+ ...about,
2469
+ ...about,
2470
+ ...runIds
2471
+ ]
2472
+ };
2473
+ }
2474
+ function toSettlement(r) {
2475
+ return {
2476
+ questionId: r.question_id,
2477
+ answerId: r.answer_id,
2478
+ closedReason: r.closed_reason,
2479
+ settler: r.settler,
2480
+ at: r.at
2481
+ };
2482
+ }
2483
+
2484
+ //#endregion
2485
+ //#region src/version.ts
2486
+ const PROTOCOL_VERSION = "1.0.0-draft.2";
2487
+
2488
+ //#endregion
2489
+ export { parseAddress as C, isPeerAddress as S, isSystemMarker as _, REMOTE_STATES as a, PEER_ALIAS_PATTERN as b, createUlidFactory as c, LOCAL_ONLY_MARKERS as d, isFederationLocalAddress as f, gateOf as g, checkIdempotencyKey as h, openMessagesDb as i, renderDigestLine as l, MEMORY_GATE_KINDS as m, MESSAGES_DB_VERSION as n, DEFAULT_LIMITS as o, localOnlyReason as p, SqliteMessageStore as r, DeliveryEngine as s, PROTOCOL_VERSION as t, renderForAgent as u, validateSendInput as v, MessagingError as w, isAgentAuthored as x, LINE_BREAK as y };
2490
+ //# sourceMappingURL=version-Ds8ZFZLh.js.map