@volter/supercode-teams 0.3.85 → 0.3.87

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.
package/mail/send.mjs ADDED
@@ -0,0 +1,723 @@
1
+ // One send for every sender (docs/architecture/overview.md, "The mail operations", 3b-1): `supercode message send` and
2
+ // `reply`, a session's `send_message`, harness serve's `sessions.message`, the daemon's own notices, and a request that
3
+ // came through another machine's link to this machine's mail door. It resolves the receiver on the live-session map,
4
+ // routes to another machine through Teams or to the receiver's door here, guards against loops and repeats, and words
5
+ // the outcome for the sending agent, as crates/harness/src/mail_send.rs and mail_route.rs `deliver` did (their texts
6
+ // kept word for word: an agent reads them). The mail it files is mailbox.rs's, through the store (store.mjs).
7
+ import { appendFileSync, chmodSync, closeSync, fsyncSync, linkSync, mkdirSync, openSync, readFileSync, readdirSync, renameSync, unlinkSync, writeFileSync, writeSync } from 'node:fs';
8
+ import { join } from 'node:path';
9
+ import { blake3Hex, render, shortId } from './envelope.mjs';
10
+ import { AGENT_HARNESS, RESUME_WAIT_MS } from './agents.mjs';
11
+ import { MAX_RELAYED_BYTES, RELAY_NAME_PREFIX } from './relay.mjs';
12
+ import { FULL_ID_LEN, cameThroughLink, envelope as makeEnvelope, makeAddress, messageIdFor, normalMachineName, parseAddress, senderIdentity, subjectLine, threadOf } from './compose.mjs';
13
+
14
+ export const EXIT_REFUSED = 2;
15
+ export const EXIT_UNKNOWN = 3;
16
+ export const EXIT_STORED = 4;
17
+ export const EXIT_FAILED = 5;
18
+ /** Exit code of a send whose answer was lost: neither refused nor failed, its outcome unknown. */
19
+ export const EXIT_UNANSWERED = 1;
20
+
21
+ /** What a sender reads of a send whose answer was lost (docs/architecture/overview.md, 3b-1: unanswered). */
22
+ export function unansweredText(where, detail, id) {
23
+ return `Unanswered: ${where} took the message, but its answer was lost (${detail}), so whether it was delivered is unknown. Message id ${shortId(id)}. Don't send it again as a new message; supercode message show ${shortId(id)} says where it stands.`;
24
+ }
25
+
26
+ /** Replies in one unbroken chain between two sessions beyond which another is refused. */
27
+ const REPLY_CHAIN_LIMIT = 32;
28
+ /** How long a name no running session carries still reaches the session that last carried it. */
29
+ const NAME_KEPT_MS = 7 * 24 * 60 * 60 * 1000;
30
+ /** How often a name's last listing is written again while its session runs. */
31
+ const NAME_SEEN_EVERY_MS = 60 * 60 * 1000;
32
+
33
+ const outcome = (code, text, receipt = null) => ({ code, text, receipt });
34
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
35
+ const readJson = (path) => { try { return JSON.parse(readFileSync(path, 'utf8')); } catch { return null; } };
36
+
37
+ /** A delivery a door refused before anything was sent. */
38
+ class Refused extends Error { constructor(kind, bytes = 0) { super(kind); this.kind = kind; this.bytes = bytes; } }
39
+
40
+ export class MailSend {
41
+ /**
42
+ * `live` the live-session map; `store` the mail store; `agents` an agent's records and plan; `relays` the Claude relay
43
+ * sender; `askMachine(machine, request)` one request to another machine's mail door; `deliverRuntime(receipt, text,
44
+ * wake)` one hosted runtime's input ('steered', 'started' or 'not_woken'); `codexAnswer(request)` a native Codex
45
+ * question's answer through its app-server.
46
+ */
47
+ constructor({ machine, mailRoot, live, store, agents, relays, askMachine, deliverRuntime, codexAnswer, log = () => {} }) {
48
+ Object.assign(this, { machine, mailRoot, live, store, agents, relays, askMachine, deliverRuntime, codexAnswer, log });
49
+ }
50
+
51
+ // ---- one send ----------------------------------------------------------------------------------------------------
52
+
53
+ /**
54
+ * Send `body` from `caller` (`{ address, name }`) to `to` (an address, `name@machine`, `operator:<name>`, or a name
55
+ * unique here): through Teams when `to` is on another machine, else through its door here. `link` is the link a request
56
+ * came through (`{ principal, name }`), or null for this machine's own callers. Answers `{ code, text, receipt }`.
57
+ */
58
+ async send(caller, to, body, options = {}, context = {}) {
59
+ // A send under an --id is held while it runs: the sent marker is written only once it is done, so a second send
60
+ // under the same key meanwhile waits for the first and takes its outcome (sent once), never delivering it again; one
61
+ // after a first that was not sent sends.
62
+ const key = typeof options.id === 'string' ? `${caller.address}\0${options.id}` : null;
63
+ if (key) {
64
+ const running = (this.inFlight ??= new Map()).get(key);
65
+ if (running) {
66
+ const first = await running.catch(() => null);
67
+ if (first?.code === 0) return outcome(0, `Already sent with --id ${options.id}: ${first.text} Nothing was sent again.`, first.receipt ?? null);
68
+ return this.send(caller, to, body, options, context);
69
+ }
70
+ }
71
+ const sending = this.#send(caller, to, body, options, context);
72
+ if (!key) return sending;
73
+ this.inFlight.set(key, sending);
74
+ try { return await sending; } finally { if (this.inFlight.get(key) === sending) this.inFlight.delete(key); }
75
+ }
76
+
77
+ async #send(caller, to, body, { subject = null, in_reply_to = null, notify_when_idle = false, queue = false, id = null, notice = false, thread = null, message_id = null } = {}, { link = null } = {}) {
78
+ // an id its client chose (so a lost answer can name it), never an idempotency key: it marks nothing sent
79
+ const chosen = typeof message_id === 'string' && /^m-[0-9a-f]{24}$/.test(message_id) ? message_id : null;
80
+ const machine = this.#remoteMachine(to);
81
+ if (machine) {
82
+ if (link) return outcome(EXIT_UNKNOWN, `${to} is not on this machine; a send through its mail door is for its own sessions.`);
83
+ const answered = this.#answeredId(caller.address, in_reply_to);
84
+ if (answered.refused) return answered.refused;
85
+ // the sender names the message's id itself (the other door files it under exactly this id), so its own send log
86
+ // holds every message it sent, wherever it went
87
+ const messageId = id == null && chosen ? chosen : messageIdFor(caller.address, id);
88
+ const carried = answered.id ? (this.#filedThread(answered.id) ?? this.#sentThread(caller.address, answered.id)) : null;
89
+ const request = { op: 'send', from: caller.address, from_name: caller.name, to, body, subject, in_reply_to: answered.id, thread: carried, notify_when_idle, queue, id: messageId, notice };
90
+ const sent = await this.#remoteSend(machine, request);
91
+ // unanswered, it may have been delivered: kept in the send log, so a later reply to it resolves here
92
+ if (sent.code === 0 || sent.code === EXIT_STORED || sent.receipt?.unanswered === true) this.#recordSend(caller.address, to, messageId, carried ?? answered.id ?? messageId);
93
+ return sent;
94
+ }
95
+ return this.#deliver(caller, to, body, { subject, in_reply_to, notify_when_idle, queue, id, notice, thread, chosen }, link);
96
+ }
97
+
98
+ /** The machine `to` names when it is not this one, in its normal form. */
99
+ #remoteMachine(to) {
100
+ const parsed = parseAddress(to);
101
+ let machine = parsed?.machine ?? null;
102
+ if (!parsed) { const at = String(to).lastIndexOf('@'); if (at < 0) return null; machine = normalMachineName(String(to).slice(at + 1)); }
103
+ return machine !== this.machine ? machine : null;
104
+ }
105
+
106
+ /** Hand a request to another machine's mail door through Teams (mail_send.rs `remote_send`). */
107
+ async #remoteSend(machine, request) {
108
+ try {
109
+ const answer = await this.askMachine(machine, request);
110
+ const code = Number.isInteger(answer?.code) ? answer.code : EXIT_FAILED;
111
+ const text = typeof answer?.text === 'string' ? answer.text : typeof answer?.detail === 'string' ? answer.detail : 'the other machine answered nothing readable';
112
+ return outcome(code, text, answer?.receipt ?? null);
113
+ } catch (error) {
114
+ const detail = String(error?.message ?? error);
115
+ // the door took it and its answer was lost: unanswered, never refused or failed (what it did there is unknown)
116
+ if (error?.unanswered === true) return outcome(EXIT_UNANSWERED, unansweredText(`machine ${machine}`, detail, request.id), { unanswered: true, message_ids: [request.id] });
117
+ if (detail.includes('no mail grant')) return outcome(EXIT_REFUSED, `Not sent: machine ${machine} does not accept mail from you (no mail grant). Only an operator can grant it; tell your user. Don't retry.`);
118
+ return outcome(EXIT_FAILED, `Not sent to machine ${machine}: ${detail}`);
119
+ }
120
+ }
121
+
122
+ /**
123
+ * The message a reply answers, named as the agent was shown it (mail_send.rs `answered_id`): a prefix names the one
124
+ * message it matches among those the caller received and those it sent, or nothing is sent; a whole id is kept.
125
+ */
126
+ #answeredId(callerAddress, answered) {
127
+ if (typeof answered !== 'string' || answered.length >= FULL_ID_LEN) return { id: answered ?? null };
128
+ const found = this.store.findPrefixIn(callerAddress, answered).map((item) => item.envelope.id);
129
+ for (const id of this.#sentIds(callerAddress)) if (id.startsWith(answered) && !found.includes(id)) found.push(id);
130
+ if (found.length === 1) return { id: found[0] };
131
+ if (!found.length) return { refused: outcome(EXIT_REFUSED, `Not sent: no message you received or sent has an id starting ${answered}. Name the message you answer as its envelope or your send showed it.`) };
132
+ return { refused: outcome(EXIT_REFUSED, `Not sent: ${answered} names ${found.length} messages you received or sent (${found.join(', ')}). Give more of its id.`) };
133
+ }
134
+
135
+ /** The receiver `to` names here: `{ address, name, session (its live row, when it runs) }`, or an outcome. */
136
+ #receiver(to, sessions) {
137
+ const operator = String(to).startsWith('operator:') ? makeAddress(this.machine, 'operator', String(to).slice('operator:'.length)) : null;
138
+ const parsed = parseAddress(operator ?? to);
139
+ // an operator (a board, a script) is not a listed session: its address is taken as given
140
+ if (parsed && (parsed.harness === 'operator' || parsed.harness === 'board')) return { address: parsed.address, name: `${parsed.session}@${parsed.machine}` };
141
+ // an agent's one mailbox: its plan decides which of its sessions receives it
142
+ if (parsed?.harness === AGENT_HARNESS) {
143
+ if (!this.agents.load(parsed.session)) return { outcome: outcome(EXIT_UNKNOWN, `No agent named ${parsed.session} is declared on this machine. Nothing was sent. Run supercode agent show for the declared agents.`) };
144
+ return { address: parsed.address, name: `agent ${parsed.session}@${parsed.machine}` };
145
+ }
146
+ if (parsed) {
147
+ const session = sessions.find((row) => row.address === parsed.address);
148
+ if (!session) return { outcome: outcome(EXIT_UNKNOWN, `${to} is no longer running. Nothing was sent. Run supercode message list for the live sessions.`) };
149
+ return { address: session.address, name: session.name, session };
150
+ }
151
+ // a name: unique among the sessions here, else the session it last named (the names ledger)
152
+ const [wantedRaw, at] = [String(to).split('@')[0], String(to).includes('@') ? String(to).slice(String(to).indexOf('@') + 1) : null];
153
+ if (at !== null && at !== this.machine) return { outcome: outcome(EXIT_UNKNOWN, `Not sent: ${to} is on machine ${at}, not this one. Nothing was sent.`) };
154
+ const wanted = wantedRaw;
155
+ const short = (row) => row.name.split('@')[0];
156
+ const matching = sessions.filter((row) => short(row) === wanted);
157
+ if (matching.length === 1) return { address: matching[0].address, name: matching[0].name, session: matching[0] };
158
+ if (!matching.length) {
159
+ const remembered = this.#rememberedAddress(wanted);
160
+ if (remembered) {
161
+ const session = sessions.find((row) => row.address === remembered);
162
+ if (!session) return { outcome: outcome(EXIT_UNKNOWN, `${wanted} was ${remembered}, which is not running. Nothing was sent. Run supercode message list for the live sessions.`) };
163
+ return { address: session.address, name: session.name, session, alias: ` "${wanted}" is a name it ran under before; it runs as ${session.name} now.` };
164
+ }
165
+ }
166
+ let hint = '';
167
+ if (matching.length > 1) hint = ` ${matching.length} sessions are named ${wanted}; use its address.`;
168
+ else {
169
+ const near = sessions.filter((row) => {
170
+ const name = short(row);
171
+ let common = 0; while (common < name.length && common < wanted.length && name[common] === wanted[common]) common++;
172
+ return name.includes(wanted) || wanted.includes(name) || common >= 4;
173
+ }).slice(0, 3).map((row) => `${row.name} (${row.address.split(':')[2]}, ${row.status})`);
174
+ if (near.length) hint = ` Did you mean: ${near.join(', ')}?`;
175
+ }
176
+ return { outcome: outcome(EXIT_UNKNOWN, `No session named "${wanted}" is reachable.${hint} Run supercode message list. Nothing was sent.`) };
177
+ }
178
+
179
+ /** The door that reaches `address` here, from one read of the live sessions (mail_route.rs `door_in`): null when no
180
+ * running session has it. */
181
+ #door(address, sessions) {
182
+ const parsed = parseAddress(address);
183
+ if (!parsed || parsed.machine !== this.machine) return { kind: 'other_machine', machine: parsed?.machine ?? null };
184
+ if (parsed.harness === 'operator' || parsed.harness === 'board') return { kind: 'operator' };
185
+ const session = sessions.find((row) => row.address === address);
186
+ if (!session) return null;
187
+ if (session.door === 'runtime') return { kind: 'runtime', session };
188
+ if (session.door === 'native') return { kind: 'native', session };
189
+ if (session.door === 'hook') return { kind: 'hook', session, pane: session.pane ?? null, idle: session.status === 'idle' };
190
+ return { kind: 'stored', session };
191
+ }
192
+
193
+ /** Deliver from `caller` to `to` on this machine (mail_send.rs `deliver`). */
194
+ async #deliver(caller, to, body, { subject, in_reply_to, notify_when_idle, queue, id, notice, thread: carried, chosen }, link) {
195
+ const sessions = this.live.sessions();
196
+ const receiver = this.#receiver(to, sessions);
197
+ if (receiver.outcome) return receiver.outcome;
198
+ const { address, name } = receiver;
199
+ if (address === caller.address) return outcome(EXIT_REFUSED, 'Not sent: that address is your own session.');
200
+ const door = parseAddress(address).harness === AGENT_HARNESS ? null : this.#door(address, sessions);
201
+ const messageId = id == null && chosen ? chosen : messageIdFor(caller.address, id);
202
+ const marker = this.#sentMarker(caller.address, messageId);
203
+ const terminal = door?.kind === 'hook' && door.pane != null;
204
+ let previous = null; try { previous = readFileSync(marker, 'utf8'); } catch { /* not sent before */ }
205
+ if (previous !== null) {
206
+ if (terminal && !queue) this.store.requestWake(address, messageId);
207
+ return outcome(0, `Already sent with --id ${id ?? ''}: ${previous} Nothing was sent again.`, this.#deliveryReceipt(address, messageId, terminal));
208
+ }
209
+ const answered = this.#answeredId(caller.address, in_reply_to);
210
+ if (answered.refused) return answered.refused;
211
+ if (answered.id?.startsWith('q-')) return this.#questionReply(caller, address, answered.id, body, link);
212
+ if (answered.id && this.#replyChainDepth(caller.address, address, answered.id) >= REPLY_CHAIN_LIMIT) {
213
+ return outcome(EXIT_REFUSED, `Not sent: you and ${name} have answered each other ${REPLY_CHAIN_LIMIT} times in a row. If you are trading acknowledgements or status, stop; to go on, send a new message (without --re), or tell your user.`);
214
+ }
215
+ const sent = makeEnvelope({
216
+ id: messageId, from: caller.address, from_name: caller.name, sender_identity: senderIdentity(caller.address, link, this.machine),
217
+ kind: notice ? 'notice' : 'peer', reply_via: { mode: notice ? 'none' : 'command' },
218
+ in_reply_to: answered.id, thread: answered.id ? this.#replyThread(caller.address, address, answered.id, carried) : null,
219
+ subject: subjectLine(subject), body,
220
+ });
221
+ const plan = await this.#plan(sent, address, {});
222
+ if (plan.outcome) return plan.outcome;
223
+ if (plan.plan) return this.#deliverPlanned(caller, sent, plan.plan, { queue, notify_when_idle });
224
+ // the thread this message is in, kept in the send log: a later reply to it continues that thread
225
+ const sentIn = threadOf(sent);
226
+ if (!door || door.kind === 'other_machine') return outcome(EXIT_UNKNOWN, `${name} is no longer running. Nothing was sent. Run supercode message list for the live sessions.`);
227
+ const idleNote = notify_when_idle && cameThroughLink(sent)
228
+ ? ' No idle notice: this message came through a link, so its return address is as claimed, and nothing is sent to it on its own (D200); a reply is the receiver\'s own send.'
229
+ : notify_when_idle ? ' Subscribed: one idle notice reaches you when its next turn ends.' : '';
230
+ let delivered;
231
+ try { delivered = await this.route(sent, address, door, !queue, notify_when_idle); }
232
+ catch (error) {
233
+ if (error instanceof Refused && error.kind === 'queue') return outcome(EXIT_REFUSED, `Not sent: ${name} is idle, and a Claude session always starts a turn when a message arrives, so --queue cannot hold it. Send without --queue to wake it.`);
234
+ if (error instanceof Refused) return outcome(EXIT_REFUSED, `Not sent: the message is ${error.bytes} bytes; the limit is ${MAX_RELAYED_BYTES}. Write it to a file and send its path instead.`);
235
+ return outcome(EXIT_FAILED, `Not sent to ${name}: ${error.message}`);
236
+ }
237
+ const [code, what] = {
238
+ steered: [0, 'steered into its running turn'],
239
+ started: [0, 'it was idle, so the message started a turn'],
240
+ native_busy: [0, 'it will read it at its next tool call'],
241
+ native_idle: [0, 'it was idle, so the message starts its next turn'],
242
+ hooked: [0, 'queued in its mailbox; terminal delivery waits for native transcript confirmation'],
243
+ hook_woken: [0, 'it was idle, so its pane was told to read its mailbox; its hook shows it the message in that turn'],
244
+ queued: [0, 'it is idle and --queue leaves it so; the message waits in its mailbox'],
245
+ operator: [0, 'filed in its mailbox'],
246
+ already: [0, 'it already had this message; nothing was sent again'],
247
+ stored: [EXIT_STORED, 'Stored (not failed): it has no delivery door, so it sees this only if it runs supercode message inbox (a Codex session gets one with: supercode message setup codex). Don\'t resend, and don\'t wait for a reply'],
248
+ }[delivered];
249
+ this.#recordSend(caller.address, address, messageId, sentIn);
250
+ const harness = parseAddress(address).harness;
251
+ const text = code === 0
252
+ ? `sent to ${name} (${harness}, ${door.kind}): ${what}. Message id ${shortId(messageId)}.${receiver.alias ?? ''}${idleNote} Don't poll; carry on.`
253
+ : `${name} (${harness}): ${what}. Message id ${shortId(messageId)}.${idleNote}`;
254
+ if (code === 0 && id != null) { try { mkdirSync(join(marker, '..'), { recursive: true }); writeFileSync(marker, text); } catch { /* the marker is best kept */ } }
255
+ return outcome(code, text, code === 0 ? this.#deliveryReceipt(address, messageId, terminal) : null);
256
+ }
257
+
258
+ /** An agent's plan for `sent` (its thread store decides), or an outcome when the plan refused it. */
259
+ async #plan(sent, address, channel) {
260
+ try { return { plan: await this.agents.plan(sent, address, channel) }; }
261
+ catch (error) { return { outcome: outcome(EXIT_FAILED, `Not sent: ${error.message}`) }; }
262
+ }
263
+
264
+ // ---- the doors ---------------------------------------------------------------------------------------------------
265
+
266
+ /**
267
+ * Deliver `sent` to `to` through `door` (mail_route.rs `deliver`); with `wake` false an idle receiver is not started,
268
+ * with `notifyWhenIdle` its sender gets one idle notice after its next turn. Answers how it went ('steered', 'started',
269
+ * 'native_busy', 'native_idle', 'hooked', 'queued', 'stored', 'operator' or 'already'); throws Refused for a delivery
270
+ * refused before anything was sent, and an Error with what failed.
271
+ */
272
+ async route(sent, to, door, wake, notifyWhenIdle) {
273
+ this.store.open(to);
274
+ let finalReply = false;
275
+ let delivered;
276
+ // a message its receiver already has (its own claim, or an earlier hand-over to its door) is never handed over again
277
+ if ((door.kind === 'native' || door.kind === 'runtime') && this.store.findIn(to, sent.id)?.state === 'read' && this.store.claimOf(to, sent.id)?.recipient === true) return 'already';
278
+ if (door.kind === 'runtime') {
279
+ // its final message goes back as the reply only to a sender this machine knows (D200)
280
+ const answers = sent.reply_via?.mode === 'command' && !cameThroughLink(sent);
281
+ const handed = answers ? { ...sent, reply_via: { mode: 'final_message', destination: sent.from_name } } : sent;
282
+ const receipt = this.live.runtimeReceipt(door.session.address.split(':')[2], door.session.address.split(':').slice(3).join(':'));
283
+ if (!receipt) throw new Error('the live runtime\'s receipt is gone');
284
+ const how = await this.deliverRuntime(receipt, render(handed), wake);
285
+ if (how === 'not_woken') { this.store.deliver(to, sent); delivered = 'queued'; }
286
+ else { this.store.deliverRead(to, handed); finalReply = answers; delivered = how === 'steered' ? 'steered' : 'started'; }
287
+ } else if (door.kind === 'native') {
288
+ const busy = door.session.status !== 'idle';
289
+ if (!wake && !busy) throw new Refused('queue');
290
+ // its reply line names the door this session really has: its tools when it runs supercode's MCP server
291
+ const handed = sent.reply_via?.mode === 'command' && door.session.tools ? { ...sent, reply_via: { mode: 'tool' } } : sent;
292
+ const text = render(handed);
293
+ if (Buffer.byteLength(text) > MAX_RELAYED_BYTES) throw new Refused('too_long', Buffer.byteLength(text));
294
+ // filed before it is handed over: a session that reads its mailbox at its first tool call finds it; a relay that
295
+ // fails takes back the copy this delivery filed
296
+ const filedHere = this.store.findIn(to, handed.id) == null;
297
+ const filed = this.store.deliverRead(to, handed);
298
+ const relayed = await this.relays.send(handed.from, handed.from_name, { sessionId: door.session.address.split(':').slice(3).join(':'), name: door.session.name.split('@')[0], socket: door.session.socket }, text, handed.id);
299
+ if (!relayed.delivered) { if (filedHere) { try { unlinkSync(filed); } catch { /* gone */ } } throw new Error(relayed.detail); }
300
+ delivered = busy ? 'native_busy' : 'native_idle';
301
+ } else if (door.kind === 'hook') {
302
+ this.store.deliver(to, sent);
303
+ if (wake) this.store.requestWake(to, sent.id);
304
+ delivered = 'hooked';
305
+ } else if (door.kind === 'stored') { this.store.deliver(to, sent); delivered = 'stored'; }
306
+ else { this.store.deliver(to, sent); delivered = 'operator'; }
307
+ // a hand-over to the recipient's own door is its delivery, recorded as such
308
+ if (['steered', 'started', 'native_busy', 'native_idle'].includes(delivered)) {
309
+ try { this.store.claimFor(to, sent.id, { pid: process.pid, via: 'door', caller: null, recipient: true, at_ms: Date.now() }); } catch { /* best kept */ }
310
+ }
311
+ // nor is an idle notice routed to a claimed address (D200)
312
+ const noticeWanted = notifyWhenIdle && door.kind !== 'operator' && !cameThroughLink(sent);
313
+ if (noticeWanted || finalReply) this.store.subscribeIdle(to, { message_id: sent.id, subscriber: sent.from, notice: noticeWanted, final_reply: finalReply });
314
+ return delivered;
315
+ }
316
+
317
+ /**
318
+ * Deliver `sent` to every receiver `plan` names: a `to` receiver through its door (a stopped holder the mailbox
319
+ * launched is resumed first), a `cc` receiver filed unread and not woken (mail_send.rs `deliver_planned`).
320
+ */
321
+ async deliverPlanned(caller, sent, plan, { queue = false, notify_when_idle = false } = {}) { return this.#deliverPlanned(caller, sent, plan, { queue, notify_when_idle }); }
322
+
323
+ async #deliverPlanned(caller, sent, plan, { queue, notify_when_idle }) {
324
+ const reached = [], copied = [], failed = [];
325
+ if (plan.agent) {
326
+ // the agent's own mailbox keeps the record of what was addressed to it
327
+ try { this.store.deliverRead(plan.agent, sent); } catch (error) { failed.push(`the agent's mailbox (${error.message})`); }
328
+ }
329
+ const sessions = this.live.sessions();
330
+ for (const recipient of plan.recipients.filter((item) => item.address !== caller.address)) {
331
+ const label = this.#label(sessions, recipient.address);
332
+ this.#recordSend(caller.address, recipient.address, sent.id, threadOf(sent));
333
+ if (!recipient.wake) {
334
+ try { this.#fileUnread(recipient.address, sent); copied.push(label); } catch (error) { failed.push(`${label} (${error.message})`); }
335
+ continue;
336
+ }
337
+ try { reached.push(`${label} (${await this.#deliverWoken(sent, recipient.address, sessions, queue, notify_when_idle)})`); }
338
+ catch (error) { failed.push(`${label} (${error.message})`); }
339
+ }
340
+ const thread = plan.thread ? ` Thread ${shortId(plan.thread.id)}.` : '';
341
+ let text;
342
+ if (!reached.length && !copied.length) text = `Not sent: nobody in its thread could be reached.${thread}`;
343
+ else text = `sent to ${reached.length ? reached.join(', ') : 'no one woken'}.${copied.length ? ` CC (filed, not woken): ${copied.join(', ')}.` : ''} Message id ${shortId(sent.id)}.${thread} Don't poll; carry on.`;
344
+ if (failed.length) text += ` Not delivered to: ${failed.join('; ')}.`;
345
+ const code = reached.length || copied.length ? 0 : EXIT_FAILED;
346
+ return outcome(code, text, code === 0 ? { delivered: true, message_ids: [sent.id], receipt: 'agent-mailbox' } : null);
347
+ }
348
+
349
+ /** `sent` filed unread in `address`'s mailbox on this machine's mail, without waking it (mail_agent.rs `file_unread`):
350
+ * mail for another machine's address waits here until the watch carries it; one that came through a link is never
351
+ * filed for another machine (the store refuses it). */
352
+ #fileUnread(address, sent) { this.store.deliver(address, sent); }
353
+
354
+ /** What a receiver is called in an outcome: its session name when it runs here, else its address. */
355
+ #label(sessions, address) {
356
+ const parsed = parseAddress(address);
357
+ if (parsed && (parsed.harness === 'operator' || parsed.harness === 'board')) return `${parsed.session}@${parsed.machine}`;
358
+ return sessions.find((row) => row.address === address)?.name ?? address;
359
+ }
360
+
361
+ /** Deliver `sent` to `to` through its door, waking it: on another machine filed there, a stopped one here resumed
362
+ * first (mail_send.rs `deliver_woken`). Says how it went. */
363
+ async #deliverWoken(sent, to, sessions, queue, notifyWhenIdle) {
364
+ let door = this.#door(to, sessions);
365
+ if (door?.kind === 'other_machine') {
366
+ // the one way out: mail that came to this machine through a link never leaves it again (mailbox.rs `ask_machine`)
367
+ if (cameThroughLink(sent)) throw new Error(`not carried on to ${door.machine}: a message that came to this machine through a link is for its own sessions only`);
368
+ const answer = await this.askMachine(door.machine, { op: 'file', to, envelope: sent });
369
+ if (answer?.code !== 0) throw new Error(typeof answer?.text === 'string' ? answer.text : 'the other machine refused the message');
370
+ return 'filed on its machine';
371
+ }
372
+ if (!door) {
373
+ // a reply to a closed session resumes it; only one the mailbox launched, the way it was launched
374
+ this.store.deliver(to, sent);
375
+ if (!(await this.agents.resumable(to))) return 'stopped; not resumed, since the mailbox did not launch it: the message waits in its mailbox';
376
+ try { await this.agents.resume(to); } catch (error) { throw new Error(`not resumed: ${error.message}`); }
377
+ const started = Date.now();
378
+ for (;;) {
379
+ door = this.#door(to, this.live.sessions());
380
+ if (door) break;
381
+ if (Date.now() - started > RESUME_WAIT_MS) return 'resumed; the message waits in its mailbox';
382
+ await sleep(1000);
383
+ }
384
+ }
385
+ let how;
386
+ try { how = await this.route(sent, to, door, !queue, notifyWhenIdle); }
387
+ catch (error) {
388
+ if (error instanceof Refused && error.kind === 'queue') throw new Error('idle, and --queue cannot hold a Claude session\'s mail');
389
+ if (error instanceof Refused) throw new Error(`${error.bytes} bytes; the limit is ${MAX_RELAYED_BYTES}`);
390
+ throw error;
391
+ }
392
+ return { steered: 'steered into its running turn', started: 'started a turn', native_busy: 'read at its next tool call', native_idle: 'starts its next turn', hooked: 'in its mailbox, its hook shows it', queued: 'waits in its mailbox', operator: 'filed', already: 'it already had it', stored: 'stored; it has no delivery door' }[how];
393
+ }
394
+
395
+ // ---- a filing --------------------------------------------------------------------------------------------------
396
+
397
+ /**
398
+ * File `body` from `from` (`{ address, name }`) in the mailbox of `to`, on this machine's mail whatever machine `to`
399
+ * names (mail for another machine waits here until the watch carries it there), as mail_file.rs `file` did: a key
400
+ * names the same message every time, so a repeat files nothing new; a wake is asked only by the filing that wrote it.
401
+ * A typed line's record (`typed`, D195) is filed read, under its own `i-` id, from the principal that typed it.
402
+ * `sender_identity` is the filer's observation (this daemon's), never the request's own. Answers `{ code, message_id,
403
+ * new, to }`, or `{ code, text }`.
404
+ */
405
+ file(from, to, body, { subject = null, key = null, wake = true, sender_identity = null, typed = false } = {}) {
406
+ const sender = parseAddress(from.address), receiver = parseAddress(to);
407
+ if (!sender) return { code: EXIT_REFUSED, text: `\`${from.address}\` is not a session address; addresses look like sc:<machine>:<harness>:<session-id>` };
408
+ if (!receiver) return { code: EXIT_REFUSED, text: `\`${to}\` is not a session address; addresses look like sc:<machine>:<harness>:<session-id>` };
409
+ let id = messageIdFor(sender.address, key);
410
+ if (typed) id = `i-${id.replace(/^m-/, '')}`;
411
+ const filed = makeEnvelope({ id, from: sender.address, from_name: from.name ?? '', sender_identity, kind: typed ? 'user' : 'peer', reply_via: { mode: typed ? 'none' : 'command' }, subject: subjectLine(subject), body });
412
+ try {
413
+ if (this.store.findIn(receiver.address, id)) return { code: 0, message_id: id, new: false, to: receiver.address };
414
+ // a typed line's record is read already: it reached the session as its composer turn, never as mail
415
+ if (typed) this.store.deliverRead(receiver.address, filed);
416
+ else {
417
+ this.store.deliver(receiver.address, filed);
418
+ if (wake) this.store.requestWake(receiver.address, id);
419
+ }
420
+ return { code: 0, message_id: id, new: true, to: receiver.address };
421
+ } catch (error) { return { code: EXIT_FAILED, text: error.message }; }
422
+ }
423
+
424
+ // ---- harness serve's sessions.message ----------------------------------------------------------------------------
425
+
426
+ /**
427
+ * `harness.v1.sessions.message` without `as_user` (harness_service.rs `message_live_session`): `text` from an operator
428
+ * mailbox (`from_name`, `supercode` by default) into the session or agent `locator` names. A refusal is a result, not
429
+ * an error: `{ delivered_to_bus: false, refusal: { reason, message } }`. harness serve adds its own inbound controls.
430
+ */
431
+ async message(params) {
432
+ const refused = (reason, message) => ({ delivered_to_bus: false, refusal: { reason, message } });
433
+ const text = typeof params?.text === 'string' ? params.text : '';
434
+ if (!text.trim()) return refused('delivery_failed', 'refusing to deliver an empty message');
435
+ const fromName = String(params.from_name ?? 'supercode').trim().replace(/[\s@]/g, '-');
436
+ if (!fromName) return refused('invalid_sender', 'from_name must not be empty');
437
+ const sender = makeAddress(this.machine, 'operator', fromName);
438
+ if (!sender) return refused('invalid_sender', `\`sc:${this.machine}:operator:${fromName}\` is not a session address; addresses look like sc:<machine>:<harness>:<session-id>`);
439
+ const harness = params.locator?.harness, sessionId = params.locator?.session_id;
440
+ const receiver = typeof harness === 'string' && typeof sessionId === 'string' ? makeAddress(this.machine, harness, sessionId) : null;
441
+ if (!receiver) return refused('delivery_failed', `\`sc:${this.machine}:${harness}:${sessionId}\` is not a session address; addresses look like sc:<machine>:<harness>:<session-id>`);
442
+ if (params.voice_for != null && params.voice_for !== receiver) return refused('invalid_sender', 'a voice front must represent the receiving session');
443
+ const sessions = this.live.sessions();
444
+ // a Claude session's registry name must still name only that session; a holder the mailbox launched may have stopped
445
+ if (harness === 'claude-code' && !(await this.agents.resumable(receiver)) && !this.live.runtimeReceipt('claude-code', sessionId)) {
446
+ const target = sessions.find((row) => row.address === receiver && row.door === 'native');
447
+ if (!target) return refused('not_live', `no live Claude Code process is running session \`${sessionId}\`; its transcript is persisted only`);
448
+ const short = target.name.split('@')[0];
449
+ if (sessions.filter((row) => row.door === 'native' && row.name.split('@')[0] === short).length !== 1) return refused('identity_mismatch', `the registry name \`${short}\` no longer resolves to session \`${sessionId}\` alone; refusing rather than delivering into another session`);
450
+ }
451
+ const door = harness === AGENT_HARNESS ? null : this.#door(receiver, sessions);
452
+ const subject = subjectLine(params.subject ?? null);
453
+ let id = null;
454
+ if (params.idempotency_key != null) {
455
+ const key = params.idempotency_key;
456
+ if (typeof key !== 'string' || !key || Buffer.byteLength(key) > 256) return refused('invalid_key', 'idempotency_key needs 1–256 bytes');
457
+ id = `m-${blake3Hex(JSON.stringify([sender, receiver, key]))}`.slice(0, 26);
458
+ const previous = this.store.findIn(receiver, id);
459
+ const saved = previous ? readJson(previous.path) : null;
460
+ if (saved && (saved.body !== text || (saved.subject ?? null) !== subject || saved.kind !== (params.channel ? 'channel' : 'peer') || saved.from_name !== (params.sender_name ?? `${fromName}@${this.machine}`) || (saved.in_reply_to ?? null) !== (params.in_reply_to ?? null) || (saved.voice_for ?? null) !== (params.voice_for ?? null))) {
461
+ return refused('idempotency_conflict', 'this key already names different mail');
462
+ }
463
+ }
464
+ const inReplyTo = typeof params.in_reply_to === 'string' ? params.in_reply_to : null;
465
+ const sent = makeEnvelope({
466
+ id: id ?? messageIdFor(sender, null), from: sender, from_name: params.sender_name ?? `${fromName}@${this.machine}`, sender_identity: senderIdentity(sender, null, this.machine),
467
+ kind: params.channel ? 'channel' : 'peer', reply_via: { mode: 'command' }, in_reply_to: inReplyTo,
468
+ thread: inReplyTo ? (this.#filedThread(inReplyTo) ?? inReplyTo) : null, voice_for: params.voice_for ?? null, subject, body: text,
469
+ });
470
+ const channel = { marker: params.marker ?? null, reply_marker: params.reply_marker ?? null, surface: params.surface ?? null };
471
+ let plan;
472
+ try { plan = await this.agents.plan(sent, receiver, channel); } catch (error) { return refused('delivery_failed', error.message); }
473
+ if (plan) {
474
+ const done = await this.#deliverPlanned({ address: sender, name: sent.from_name }, sent, plan, { queue: false, notify_when_idle: params.notify_when_idle === true });
475
+ return { delivered_to_bus: done.code === 0, message_id: sent.id, reply_to: sender, thread: plan.thread?.id ?? null, target: { harness, session_id: sessionId }, delivery: { door: 'agent', how: done.text } };
476
+ }
477
+ if (!door || door.kind === 'other_machine') return refused('not_live', `no running \`${harness}\` session \`${sessionId}\` is reachable; its transcript is persisted only`);
478
+ let how;
479
+ try { how = await this.route(sent, receiver, door, true, params.notify_when_idle === true); }
480
+ catch (error) {
481
+ if (error instanceof Refused && error.kind === 'too_long') return refused('too_long', `the message is ${error.bytes} bytes; the limit is ${MAX_RELAYED_BYTES}`);
482
+ return refused('delivery_failed', error.message);
483
+ }
484
+ return {
485
+ delivered_to_bus: door.kind !== 'stored', message_id: sent.id, reply_to: sender,
486
+ target: { harness, session_id: sessionId, name: door.kind === 'native' ? door.session.name.split('@')[0] : null },
487
+ delivery: { door: door.kind, how: { steered: 'steered', started: 'started', native_busy: 'next_tool_call', native_idle: 'started', hooked: 'hook', queued: 'queued', stored: 'stored', operator: 'filed', already: 'already' }[how] },
488
+ };
489
+ }
490
+
491
+ // ---- a filed message handed to its door --------------------------------------------------------------------------
492
+
493
+ /**
494
+ * A message filed unread in `address`'s mailbox handed to its session's door, once (mail_watch.rs `carry` and message.rs
495
+ * `push`, and the native watch's notices): only a door that hands it to the session (a relay, a runtime) takes it, a
496
+ * hook's is woken when `wake`, and an agent's mail is planned; `markRead` moves it to `cur/` once handed over. The
497
+ * hand-over is recorded on its wake before it starts, so one interrupted mid-way is never handed over again. Answers
498
+ * `{ outcome: 'delivered' | 'filed' | 'later', detail }`.
499
+ */
500
+ async handOver(address, id, { markRead = false, wake = false } = {}) {
501
+ const found = this.store.findIn(address, id);
502
+ if (!found) return { outcome: 'filed', detail: 'not in its mailbox' };
503
+ const sent = readJson(found.path);
504
+ if (!sent) return { outcome: 'later', detail: 'its envelope could not be read' };
505
+ if (found.state !== 'unread') return { outcome: 'filed', detail: 'already read' };
506
+ const parsed = parseAddress(address);
507
+ if (parsed?.harness === AGENT_HARNESS) {
508
+ const plan = await this.#plan(sent, address, {});
509
+ if (!plan.plan) return { outcome: 'filed', detail: plan.outcome?.text ?? 'no plan' };
510
+ const done = await this.#deliverPlanned({ address: sent.from, name: sent.from_name }, sent, plan.plan, { queue: false, notify_when_idle: false });
511
+ return done.code === 0 ? { outcome: 'delivered', detail: done.text } : { outcome: 'later', detail: done.text };
512
+ }
513
+ const door = this.#door(address, this.live.sessions());
514
+ if (!door) return { outcome: 'later', detail: 'its session is not running' };
515
+ if (door.kind === 'other_machine') return { outcome: 'later', detail: `it is on ${door.machine}` };
516
+ if (door.kind !== 'native' && door.kind !== 'runtime') {
517
+ // a hook's session is pointed at its mailbox by a wake (a native notice filed for it asks one)
518
+ if (door.kind === 'hook' && wake) this.store.requestWake(address, id);
519
+ return { outcome: 'filed', detail: `its door is ${door.kind}` };
520
+ }
521
+ const state = this.store.wakeState(address, id);
522
+ if (state.handover_at_ms != null) return { outcome: 'filed', detail: 'an earlier hand-over never settled' };
523
+ this.store.setWakeState(address, id, { ...state, handover_at_ms: Date.now() });
524
+ try {
525
+ await this.route(sent, address, door, true, false);
526
+ if (markRead) { const now = this.store.findIn(address, id); if (now?.state === 'unread') this.store.markRead(address, now.path); }
527
+ return { outcome: 'delivered', detail: null };
528
+ } catch (error) {
529
+ // a message its door can never take (too long for a relay) stays filed for the session to read
530
+ return error instanceof Refused ? { outcome: 'filed', detail: error.kind } : { outcome: 'later', detail: error.message };
531
+ } finally {
532
+ const after = this.store.wakeState(address, id);
533
+ this.store.setWakeState(address, id, { ...after, handover_at_ms: null });
534
+ }
535
+ }
536
+
537
+ // ---- a native question's answer ----------------------------------------------------------------------------------
538
+
539
+ /** Answer the native question `id` asked in `to` (mail_question.rs `reply`): only its recorded creator answers, once. */
540
+ async #questionReply(caller, to, id, body, link) {
541
+ const dir = join(this.mailRoot, 'native-questions');
542
+ const path = (suffix) => join(dir, `${id}.${suffix}`);
543
+ const question = readJson(path('json'));
544
+ if (!question) return outcome(EXIT_FAILED, `Not answered: question ${id} could not be read.`);
545
+ if (question.binding?.session !== to || question.binding?.creator !== caller.address) return outcome(EXIT_REFUSED, 'Not answered: this question belongs to a different session or recipient.');
546
+ if (parseAddress(to).harness === 'claude-code') {
547
+ const waiting = readJson(path('waiting'));
548
+ if (!waiting) return outcome(EXIT_REFUSED, 'Not answered: the native question hook is no longer waiting.');
549
+ let alive = false; try { process.kill(waiting.pid, 0); alive = true; } catch (error) { alive = error.code === 'EPERM'; }
550
+ if (!Number.isInteger(waiting.pid) || !alive) return outcome(EXIT_REFUSED, 'Not answered: the native question hook has ended.');
551
+ }
552
+ let answers;
553
+ try { answers = questionAnswers(question, body); } catch (error) { return outcome(EXIT_FAILED, `Not answered: ${error.message}`); }
554
+ const reply = {
555
+ envelope: makeEnvelope({ id: `u-${blake3Hex(`${id}\n${caller.address}`).slice(0, 24)}`, from: caller.address, from_name: caller.name, sender_identity: senderIdentity(caller.address, link, this.machine), kind: 'user', reply_via: { mode: 'none' }, in_reply_to: id, thread: id, body }),
556
+ answers,
557
+ };
558
+ try { createPrivate(dir, path('claim'), reply); }
559
+ catch (error) {
560
+ if (error.code === 'EEXIST') return outcome(EXIT_REFUSED, 'An answer is already recorded for this question. Nothing was submitted again; inspect its thread for delivery.');
561
+ throw error;
562
+ }
563
+ this.store.deliver(to, reply.envelope);
564
+ // published to the hook only once the whole reply is in mail
565
+ linkSync(path('claim'), path('reply'));
566
+ if (parseAddress(to).harness === 'claude-code') return outcome(0, `Answer ${shortId(reply.envelope.id)} recorded in reply to ${id}; awaiting confirmation from the native question hook. It is not composer input.`);
567
+ const binding = readJson(join(dir, `binding-${blake3Hex(to)}.json`));
568
+ if (!binding?.socket_path) return outcome(EXIT_STORED, 'Answer recorded but NOT delivered: this launch has no native question endpoint. Nothing was typed.');
569
+ let result;
570
+ try { result = await this.codexAnswer({ socketPath: binding.socket_path, threadId: parseAddress(to).session, answer: { question: question.native, answers: reply.answers } }); }
571
+ catch (error) { result = { delivered: false, reason: error.code ?? 'native-question-error' }; }
572
+ if (result?.delivered === true) {
573
+ const filed = this.store.findIn(to, reply.envelope.id);
574
+ if (filed?.state === 'unread') this.store.markRead(to, filed.path);
575
+ try { createPrivate(dir, path('delivered'), Date.now()); } catch (error) { if (error.code !== 'EEXIST') throw error; }
576
+ return outcome(0, `Answer ${shortId(reply.envelope.id)} delivered to native question ${id}.`);
577
+ }
578
+ return outcome(EXIT_STORED, `Answer recorded but NOT confirmed by the native question: ${JSON.stringify(result?.reason ?? null)}. It will not be typed or automatically resent.`);
579
+ }
580
+
581
+ // ---- the send log and its guards ---------------------------------------------------------------------------------
582
+
583
+ #sentDir(sender) { return join(this.mailRoot, 'sent', blake3Hex(sender).slice(0, 24)); }
584
+ #sentMarker(sender, id) { return join(this.#sentDir(sender), id); }
585
+
586
+ #logOf(sender) {
587
+ let text = '';
588
+ try { text = readFileSync(join(this.#sentDir(sender), 'log.jsonl'), 'utf8'); } catch { return []; }
589
+ return text.split('\n').map((line) => { try { return JSON.parse(line); } catch { return null; } }).filter(Boolean);
590
+ }
591
+
592
+ #sentIds(sender) { return this.#logOf(sender).map((entry) => entry.id).filter((id) => typeof id === 'string'); }
593
+ #sentThread(sender, id) { return this.#logOf(sender).find((entry) => entry.id === id && typeof entry.thread === 'string')?.thread ?? null; }
594
+
595
+ /** The thread any author on this machine sent `id` in, as its send log keeps it. */
596
+ #sentThreadHere(id) {
597
+ let names = [];
598
+ try { names = readdirSync(join(this.mailRoot, 'sent')); } catch { return null; }
599
+ for (const name of names) {
600
+ let text = '';
601
+ try { text = readFileSync(join(this.mailRoot, 'sent', name, 'log.jsonl'), 'utf8'); } catch { continue; }
602
+ for (const line of text.split('\n')) { let entry = null; try { entry = JSON.parse(line); } catch { continue; } if (entry?.id === id && typeof entry.thread === 'string') return entry.thread; }
603
+ }
604
+ return null;
605
+ }
606
+
607
+ /** The thread `parent` is filed in on this machine (mailbox.rs `filed_thread`). */
608
+ #filedThread(parent) {
609
+ const found = this.store.find(parent).find((item) => item.stored.envelope.id === parent);
610
+ return found ? threadOf(found.stored.envelope) : null;
611
+ }
612
+
613
+ /** The thread a reply to `parent` joins, sent by `sender` to `recipient` here (mail_send.rs `reply_thread`). */
614
+ #replyThread(sender, recipient, parent, carried) {
615
+ const parsed = parseAddress(recipient);
616
+ const main = parsed?.harness === AGENT_HARNESS ? this.agents.load(parsed.session)?.main_session ?? null : null;
617
+ const known = this.#filedThread(parent) ?? this.#sentThread(recipient, parent) ?? (main ? this.#sentThread(main, parent) : null) ?? this.#sentThread(sender, parent) ?? this.#sentThreadHere(parent);
618
+ if (known) return known;
619
+ if (typeof carried === 'string' && carried.trim() && !this.#filedThread(carried) && !this.agents.thread(carried)) return carried;
620
+ return parent;
621
+ }
622
+
623
+ /** How deep the reply chain ending at `answered` runs between `a` and `b`, read from both mailboxes' folders. */
624
+ #replyChainDepth(a, b, answered) {
625
+ const links = new Map([...this.store.links(a), ...this.store.links(b)]);
626
+ let depth = 0, current = answered;
627
+ while (current != null && depth <= REPLY_CHAIN_LIMIT && links.has(current)) { depth += 1; current = links.get(current); }
628
+ return depth;
629
+ }
630
+
631
+ #recordSend(sender, to, id, thread) {
632
+ try {
633
+ mkdirSync(this.#sentDir(sender), { recursive: true });
634
+ appendFileSync(join(this.#sentDir(sender), 'log.jsonl'), `${JSON.stringify({ to, at_ms: Date.now(), id, thread })}\n`);
635
+ } catch { /* the log is best kept */ }
636
+ }
637
+
638
+ /** A terminal's acknowledgement from the native watch's retained receipts, never from a filing (mail_send.rs
639
+ * `delivery_receipt`). */
640
+ #deliveryReceipt(address, id, terminal) {
641
+ if (!terminal) return { delivered: true, message_ids: [id], receipt: 'native-mail' };
642
+ const parsed = parseAddress(address);
643
+ const recipient = `${parsed.harness}:${parsed.session}`;
644
+ let names = [];
645
+ try { names = readdirSync(join(this.mailRoot, 'terminal-delivery')); } catch { /* none yet */ }
646
+ for (const name of names) {
647
+ const state = readJson(join(this.mailRoot, 'terminal-delivery', name, 'queue.json'));
648
+ if (state?.recipient !== recipient) continue;
649
+ const receipt = (Array.isArray(state.receipts) ? state.receipts : []).find((item) => Array.isArray(item?.message_ids) && item.message_ids.includes(id));
650
+ if (receipt) return receipt;
651
+ return { delivered: false, queued: true, message_ids: [id], reason: state.active?.error ?? null };
652
+ }
653
+ return { delivered: false, queued: true, message_ids: [id], reason: 'awaiting terminal mailbox' };
654
+ }
655
+
656
+ // ---- the names ledger --------------------------------------------------------------------------------------------
657
+
658
+ /** The session a name last meant here, listed by it within NAME_KEPT_MS (mail_route.rs `remembered_address`). */
659
+ remembered(name) { return this.#rememberedAddress(name); }
660
+
661
+ #rememberedAddress(name) {
662
+ const record = readJson(join(this.mailRoot, 'names.json'))?.[name];
663
+ if (!record || Date.now() - Math.max(record.seen_ms ?? 0, record.since_ms ?? 0) > NAME_KEPT_MS) return null;
664
+ return parseAddress(record.address)?.address ?? null;
665
+ }
666
+
667
+ /** Each running session's name recorded (mail_route.rs `remember_names`): a name two sessions carry is left as it was;
668
+ * written only when a name is new, names another session, or its last listing is an hour old. */
669
+ rememberNames(sessions) {
670
+ const path = join(this.mailRoot, 'names.json');
671
+ let names;
672
+ try { names = JSON.parse(readFileSync(path, 'utf8')); } catch (error) { if (error.code !== 'ENOENT') return; names = {}; }
673
+ if (!names || typeof names !== 'object') return;
674
+ const carried = new Map();
675
+ for (const row of sessions) {
676
+ const name = row.name.split('@')[0];
677
+ if (name && !name.startsWith(RELAY_NAME_PREFIX)) carried.set(name, [...(carried.get(name) ?? []), row.address]);
678
+ }
679
+ const now = Date.now();
680
+ const latest = (address) => Object.entries(names).filter(([, record]) => record.address === address).sort((x, y) => y[1].since_ms - x[1].since_ms)[0]?.[0] ?? null;
681
+ let changed = false;
682
+ for (const [name, addresses] of carried) {
683
+ if (addresses.length !== 1) continue;
684
+ const [address] = addresses;
685
+ const record = names[name];
686
+ const current = record?.address === address && now - Math.max(record.seen_ms ?? 0, record.since_ms ?? 0) < NAME_SEEN_EVERY_MS;
687
+ if (current && latest(address) === name) continue;
688
+ const renewed = record?.address === address && latest(address) === name;
689
+ names[name] = { address, since_ms: renewed ? record.since_ms : now, seen_ms: now };
690
+ changed = true;
691
+ }
692
+ if (!changed) return;
693
+ const staged = `${path}.${process.pid}.tmp`;
694
+ try { writeFileSync(staged, JSON.stringify(names)); renameSync(staged, path); } catch { /* kept for the next listing */ }
695
+ }
696
+ }
697
+
698
+ /** A native question's answer from a reply's body (mail_question.rs `answers`): one question takes plain text, several a
699
+ * JSON object of their ids; every id shown is answered, with non-empty text, and no other. */
700
+ function questionAnswers(question, body) {
701
+ const questions = question?.native?.questions;
702
+ if (!Array.isArray(questions)) throw new Error('question has no native inputs');
703
+ let values;
704
+ if (questions.length === 1 && !body.trimStart().startsWith('{')) values = { [questions[0]?.id ?? '']: body };
705
+ else values = JSON.parse(body);
706
+ const out = {};
707
+ for (const item of questions) {
708
+ if (typeof item?.id !== 'string') throw new Error('native question id missing');
709
+ const value = values?.[item.id];
710
+ if (typeof value !== 'string' || !value.trim()) throw new Error(`answer ${item.id} with nonempty text`);
711
+ out[item.id] = { answers: [value] };
712
+ }
713
+ if (!values || typeof values !== 'object' || Object.keys(values).length !== Object.keys(out).length) throw new Error('answer only the question ids shown');
714
+ return out;
715
+ }
716
+
717
+ /** A file made once, owner-only, written whole and synced (mail_question.rs `create`). */
718
+ function createPrivate(dir, path, value) {
719
+ mkdirSync(dir, { recursive: true });
720
+ try { chmodSync(dir, 0o700); } catch { /* not ours to set */ }
721
+ const fd = openSync(path, 'wx', 0o600);
722
+ try { writeSync(fd, JSON.stringify(value)); fsyncSync(fd); } finally { closeSync(fd); }
723
+ }