@volter/supercode-teams 0.3.151 → 0.3.153
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/README.md +14 -1
- package/bin/teams.mjs +10 -2
- package/machine.mjs +128 -43
- package/mail/agents.mjs +40 -119
- package/mail/carrier.mjs +72 -111
- package/mail/envelope.mjs +8 -10
- package/mail/hooks.mjs +43 -27
- package/mail/idle.mjs +14 -5
- package/mail/import-maildir.mjs +350 -0
- package/mail/questions.mjs +38 -46
- package/mail/reads.mjs +15 -26
- package/mail/relay-endpoint.mjs +14 -22
- package/mail/relay.mjs +100 -65
- package/mail/retire-maildir.mjs +122 -0
- package/mail/send.mjs +70 -83
- package/mail/sqlite-store.mjs +476 -0
- package/mail/sqlite.mjs +353 -0
- package/mail/transcript.mjs +3 -3
- package/mail/upkeep.mjs +46 -26
- package/mail/verbs.mjs +14 -36
- package/native-codex-pane.mjs +6 -6
- package/native-initial-input.mjs +6 -6
- package/native-launch-binding.mjs +35 -0
- package/package.json +2 -2
- package/session-people.mjs +7 -8
- package/terminal-mail.mjs +8 -19
- package/mail/store.mjs +0 -531
package/mail/send.mjs
CHANGED
|
@@ -3,9 +3,7 @@
|
|
|
3
3
|
// came through another machine's link to this machine's mail door. It resolves the receiver on the live-session map,
|
|
4
4
|
// routes to another machine through Teams or to the receiver's door here, guards against loops and repeats, and words
|
|
5
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).
|
|
7
|
-
import { appendFileSync, chmodSync, closeSync, fsyncSync, linkSync, mkdirSync, openSync, readFileSync, readdirSync, renameSync, unlinkSync, writeFileSync, writeSync } from 'node:fs';
|
|
8
|
-
import { join } from 'node:path';
|
|
6
|
+
// kept word for word: an agent reads them). Mail is filed through the daemon's SQLite store.
|
|
9
7
|
import { blake3Hex, render, shortId } from './envelope.mjs';
|
|
10
8
|
import { AGENT_HARNESS, RESUME_WAIT_MS } from './agents.mjs';
|
|
11
9
|
import { MAX_RELAYED_BYTES, RELAY_NAME_PREFIX } from './relay.mjs';
|
|
@@ -44,7 +42,6 @@ const outcome = (code, text, receipt = null) => ({ code, text, receipt });
|
|
|
44
42
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
45
43
|
/** The daemon pane a live session runs in, or null (mail_route.rs `daemon_pane`). */
|
|
46
44
|
const paneOf = (session) => { const pane = typeof session.pane === 'string' ? session.pane.split(':')[0] : null; return pane && /^p_[A-Za-z0-9_-]+$/.test(pane) ? pane : null; };
|
|
47
|
-
const readJson = (path) => { try { return JSON.parse(readFileSync(path, 'utf8')); } catch { return null; } };
|
|
48
45
|
|
|
49
46
|
/** A delivery a door refused before anything was sent. */
|
|
50
47
|
class Refused extends Error { constructor(kind, bytes = 0) { super(kind); this.kind = kind; this.bytes = bytes; } }
|
|
@@ -61,6 +58,16 @@ export class MailSend {
|
|
|
61
58
|
Object.assign(this, { machine, mailRoot, live, store, agents, relays, askMachine, deliverRuntime, codexAnswer, typeTurn, claudeProjects, log });
|
|
62
59
|
this.handing = new Set(); // `<address>\n<id>` of each hand-over running here (handOver)
|
|
63
60
|
this.relaying = new Set(); // `<address>\n<id>` of each relay send running here (route): its wake waits for it
|
|
61
|
+
for (const name of ['askMachine', 'deliverRuntime', 'codexAnswer', 'typeTurn']) {
|
|
62
|
+
const operation = this[name];
|
|
63
|
+
this[name] = (...args) => { this.#active(); return operation(...args); };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
stop() { this.stopped = true; }
|
|
68
|
+
|
|
69
|
+
#active() {
|
|
70
|
+
if (this.stopped || this.store.database?.closed) throw Object.assign(new Error('the daemon is stopping; no mail delivery was submitted'), { code: 503, notAsked: true });
|
|
64
71
|
}
|
|
65
72
|
|
|
66
73
|
// ---- one send ----------------------------------------------------------------------------------------------------
|
|
@@ -71,6 +78,7 @@ export class MailSend {
|
|
|
71
78
|
* came through (`{ principal, name }`), or null for this machine's own callers. Answers `{ code, text, receipt }`.
|
|
72
79
|
*/
|
|
73
80
|
async send(caller, to, body, options = {}, context = {}) {
|
|
81
|
+
this.#active();
|
|
74
82
|
if (context.link && this.#remoteMachine(to)) return outcome(EXIT_UNKNOWN, `${to} is not on this machine; a send through its mail door is for its own sessions.`);
|
|
75
83
|
// Every stream earns its current route's grants before it can join the existing same-ID send in flight.
|
|
76
84
|
let receiver = null;
|
|
@@ -79,8 +87,9 @@ export class MailSend {
|
|
|
79
87
|
if (receiver.outcome && !this.live.codexKnown) { await this.#codexKnown(); receiver = await this.#receiver(to, this.live.sessions()); }
|
|
80
88
|
if (receiver.outcome) return receiver.outcome;
|
|
81
89
|
const refused = await this.#authorizeReceiver(context, receiver.continuation?.chain ?? [receiver.address]);
|
|
82
|
-
if (refused) return refused;
|
|
90
|
+
if (refused) return this.#annotateReceiver(refused, receiver);
|
|
83
91
|
}
|
|
92
|
+
this.#active();
|
|
84
93
|
// A send under an --id is held while it runs: the sent marker is written only once it is done, so a second send
|
|
85
94
|
// under the same key meanwhile waits for the first and takes its outcome (sent once), never delivering it again; one
|
|
86
95
|
// after a first that was not sent sends.
|
|
@@ -93,7 +102,10 @@ export class MailSend {
|
|
|
93
102
|
const addresses = [...(first.continuation?.chain ?? []), ...(first.receipt?.recipients ?? []), first.receipt?.recipient].filter(Boolean);
|
|
94
103
|
if (addresses.length) {
|
|
95
104
|
const refused = await this.#authorizeReceiver(context, addresses);
|
|
96
|
-
if (refused) return refused
|
|
105
|
+
if (refused) return first.continuation ? this.#annotateReceiver(refused, {
|
|
106
|
+
address: first.continuation.recipient, continuation: first.continuation,
|
|
107
|
+
alias: ` Addressed to ${first.continuation.addressed}; its native continued-in record routes it to ${first.continuation.recipient}.`,
|
|
108
|
+
}) : refused;
|
|
97
109
|
} else if (context.authorizeRecipients) return outcome(EXIT_REFUSED, 'Not sent: the earlier send has no recipient receipt to authorize on this stream. Nothing was sent again.');
|
|
98
110
|
return { ...outcome(0, `Already sent with --id ${options.id}: ${first.text} Nothing was sent again.`, first.receipt ?? null), ...(first.continuation ? { continuation: first.continuation } : {}) };
|
|
99
111
|
}
|
|
@@ -140,6 +152,7 @@ export class MailSend {
|
|
|
140
152
|
async #authorizeReceiver(context, addresses) {
|
|
141
153
|
if (!context.authorizeRecipients) return null;
|
|
142
154
|
const reason = await context.authorizeRecipients(addresses);
|
|
155
|
+
this.#active();
|
|
143
156
|
return reason ? outcome(EXIT_REFUSED, `Not sent: ${reason}. Nothing was filed or delivered by this request.`) : null;
|
|
144
157
|
}
|
|
145
158
|
|
|
@@ -174,7 +187,7 @@ export class MailSend {
|
|
|
174
187
|
#answeredId(callerAddress, answered) {
|
|
175
188
|
if (typeof answered !== 'string' || answered.length >= FULL_ID_LEN) return { id: answered ?? null };
|
|
176
189
|
const found = this.store.findPrefixIn(callerAddress, answered).map((item) => item.envelope.id);
|
|
177
|
-
for (const id of this
|
|
190
|
+
for (const id of this.store.sentIds(callerAddress, answered)) if (!found.includes(id)) found.push(id);
|
|
178
191
|
if (found.length === 1) return { id: found[0] };
|
|
179
192
|
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.`) };
|
|
180
193
|
return { refused: outcome(EXIT_REFUSED, `Not sent: ${answered} names ${found.length} messages you received or sent (${found.join(', ')}). Give more of its id.`) };
|
|
@@ -275,9 +288,8 @@ export class MailSend {
|
|
|
275
288
|
if (address === caller.address) return outcome(EXIT_REFUSED, 'Not sent: that address is your own session.');
|
|
276
289
|
const door = parseAddress(address).harness === AGENT_HARNESS ? null : this.#door(address, sessions);
|
|
277
290
|
const messageId = id == null && chosen ? chosen : messageIdFor(caller.address, id);
|
|
278
|
-
const marker = this.#sentMarker(caller.address, messageId);
|
|
279
291
|
const terminal = door?.kind === 'hook' && door.pane != null;
|
|
280
|
-
|
|
292
|
+
const previous = this.store.sentMarker(caller.address, messageId);
|
|
281
293
|
if (previous !== null) {
|
|
282
294
|
if (terminal && !queue) this.store.requestWake(address, messageId);
|
|
283
295
|
return outcome(0, `Already sent with --id ${id ?? ''}: ${previous} Nothing was sent again.`, this.#deliveryReceipt(address, messageId, terminal));
|
|
@@ -339,7 +351,7 @@ export class MailSend {
|
|
|
339
351
|
const text = code === 0
|
|
340
352
|
? `sent to ${name} (${harness}, ${door.kind}): ${what}. Message id ${shortId(messageId)}.${receiver.alias ?? ''}${idleNote} Don't poll; carry on.`
|
|
341
353
|
: `${name} (${harness}): ${what}. Message id ${shortId(messageId)}.${idleNote}`;
|
|
342
|
-
if (code === 0 && id != null) { try {
|
|
354
|
+
if (code === 0 && id != null) { try { this.store.setSentMarker(caller.address, messageId, text); } catch { /* the marker is best kept */ } }
|
|
343
355
|
return outcome(code, text, code === 0 ? this.#deliveryReceipt(address, messageId, terminal) : null);
|
|
344
356
|
}
|
|
345
357
|
|
|
@@ -358,6 +370,7 @@ export class MailSend {
|
|
|
358
370
|
* refused before anything was sent, and an Error with what failed.
|
|
359
371
|
*/
|
|
360
372
|
async route(sent, to, door, wake, notifyWhenIdle) {
|
|
373
|
+
this.#active();
|
|
361
374
|
this.store.open(to);
|
|
362
375
|
let finalReply = false;
|
|
363
376
|
let delivered;
|
|
@@ -421,7 +434,7 @@ export class MailSend {
|
|
|
421
434
|
if (filedHere && wake) { try { this.store.requestWake(to, handed.id); } catch { /* the wake filed above stands */ } }
|
|
422
435
|
throw Object.assign(new Error(relayed.detail), { unanswered: true });
|
|
423
436
|
}
|
|
424
|
-
if (!relayed.delivered && !received) { if (filedHere) { try {
|
|
437
|
+
if (!relayed.delivered && !received) { if (filedHere) { try { this.store.remove(to, filed); } catch { /* gone */ } } throw new Error(relayed.detail); }
|
|
425
438
|
// relayed: a copy filed unread before this delivery (an earlier unanswered or queued one, held for the receiver's
|
|
426
439
|
// own read) is moved to cur/, so that read never hands it over a second time
|
|
427
440
|
if (relayed.delivered && !received) {
|
|
@@ -565,6 +578,7 @@ export class MailSend {
|
|
|
565
578
|
try { await this.agents.resume(to); } catch (error) { throw new Error(`not resumed: ${error.message}`); }
|
|
566
579
|
const started = Date.now();
|
|
567
580
|
for (;;) {
|
|
581
|
+
this.#active();
|
|
568
582
|
door = this.#door(to, this.live.sessions());
|
|
569
583
|
if (door) break;
|
|
570
584
|
if (Date.now() - started > RESUME_WAIT_MS) return { how: 'not_running', text: 'resumed; the message waits in its mailbox' };
|
|
@@ -650,7 +664,7 @@ export class MailSend {
|
|
|
650
664
|
if (typeof key !== 'string' || !key || Buffer.byteLength(key) > 256) return refused('invalid_key', 'idempotency_key needs 1–256 bytes');
|
|
651
665
|
id = `m-${blake3Hex(JSON.stringify([sender, receiver, key]))}`.slice(0, 26);
|
|
652
666
|
const previous = this.store.findIn(receiver, id);
|
|
653
|
-
const saved = previous ?
|
|
667
|
+
const saved = previous ? this.store.body(previous) : null;
|
|
654
668
|
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))) {
|
|
655
669
|
return refused('idempotency_conflict', 'this key already names different mail');
|
|
656
670
|
}
|
|
@@ -785,9 +799,11 @@ export class MailSend {
|
|
|
785
799
|
* (mail_route.rs `type_user_turns`).
|
|
786
800
|
*/
|
|
787
801
|
async typeUserTurns(address, pane) {
|
|
802
|
+
this.#active();
|
|
788
803
|
const typed = [];
|
|
789
804
|
const harness = parseAddress(address).harness;
|
|
790
805
|
for (const waiting of this.store.userTurns(address)) {
|
|
806
|
+
this.#active();
|
|
791
807
|
const claimed = this.store.claimUserTurn(address, waiting);
|
|
792
808
|
if (!claimed) break;
|
|
793
809
|
let result;
|
|
@@ -817,9 +833,10 @@ export class MailSend {
|
|
|
817
833
|
* `{ outcome: 'delivered' | 'filed' | 'later', detail }`.
|
|
818
834
|
*/
|
|
819
835
|
async handOver(address, id, { wake = false } = {}) {
|
|
836
|
+
this.#active();
|
|
820
837
|
const found = this.store.findIn(address, id);
|
|
821
838
|
if (!found) return { outcome: 'filed', detail: 'not in its mailbox' };
|
|
822
|
-
const sent =
|
|
839
|
+
const sent = this.store.body(found);
|
|
823
840
|
if (!sent) return { outcome: 'later', detail: 'its envelope could not be read' };
|
|
824
841
|
if (found.state !== 'unread') return { outcome: 'filed', detail: 'already read' };
|
|
825
842
|
// one already handed to its receiver's door (that delivery's claim, the door's or the carrier's: a daemon that ended
|
|
@@ -868,8 +885,10 @@ export class MailSend {
|
|
|
868
885
|
return error instanceof Refused ? { outcome: 'filed', detail: error.kind } : { outcome: 'later', detail: error.message };
|
|
869
886
|
} finally {
|
|
870
887
|
this.handing.delete(key);
|
|
871
|
-
|
|
872
|
-
|
|
888
|
+
if (!this.stopped) {
|
|
889
|
+
const after = this.store.wakeState(address, id);
|
|
890
|
+
this.store.setWakeState(address, id, { ...after, handover_at_ms: null });
|
|
891
|
+
}
|
|
873
892
|
}
|
|
874
893
|
}
|
|
875
894
|
|
|
@@ -877,13 +896,13 @@ export class MailSend {
|
|
|
877
896
|
|
|
878
897
|
/** Answer the native question `id` asked in `to` (mail_question.rs `reply`): only its recorded creator answers, once. */
|
|
879
898
|
async #questionReply(caller, to, id, body, link) {
|
|
880
|
-
const
|
|
881
|
-
const
|
|
882
|
-
const question = readJson(path('json'));
|
|
899
|
+
const key = (suffix) => `${id}.${suffix}`;
|
|
900
|
+
const question = this.store.record('native_questions', key('json'));
|
|
883
901
|
if (!question) return outcome(EXIT_FAILED, `Not answered: question ${id} could not be read.`);
|
|
884
902
|
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.');
|
|
903
|
+
if (this.store.record('native_questions', key('claim'))) return outcome(EXIT_REFUSED, 'An answer is already recorded for this question. Nothing was submitted again; inspect its thread for delivery.');
|
|
885
904
|
if (parseAddress(to).harness === 'claude-code') {
|
|
886
|
-
const waiting =
|
|
905
|
+
const waiting = this.store.record('native_questions', key('waiting'));
|
|
887
906
|
if (!waiting) return outcome(EXIT_REFUSED, 'Not answered: the native question hook is no longer waiting.');
|
|
888
907
|
let alive = false; try { process.kill(waiting.pid, 0); alive = true; } catch (error) { alive = error.code === 'EPERM'; }
|
|
889
908
|
if (!Number.isInteger(waiting.pid) || !alive) return outcome(EXIT_REFUSED, 'Not answered: the native question hook has ended.');
|
|
@@ -894,24 +913,25 @@ export class MailSend {
|
|
|
894
913
|
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 }),
|
|
895
914
|
answers,
|
|
896
915
|
};
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
linkSync(path('claim'), path('reply'));
|
|
916
|
+
const claimed = this.store.transaction(() => {
|
|
917
|
+
if (!this.store.putRecord('native_questions', key('claim'), reply, { create: true })) return false;
|
|
918
|
+
this.store.deliver(to, reply.envelope);
|
|
919
|
+
this.store.putRecord('native_questions', key('reply'), reply, { create: true });
|
|
920
|
+
return true;
|
|
921
|
+
});
|
|
922
|
+
if (!claimed) return outcome(EXIT_REFUSED, 'An answer is already recorded for this question. Nothing was submitted again; inspect its thread for delivery.');
|
|
905
923
|
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.`);
|
|
906
|
-
const binding =
|
|
924
|
+
const binding = this.store.record('native_questions', `binding-${blake3Hex(to)}.json`);
|
|
907
925
|
if (!binding?.socket_path) return outcome(EXIT_STORED, 'Answer recorded but NOT delivered: this launch has no native question endpoint. Nothing was typed.');
|
|
908
926
|
let result;
|
|
909
927
|
try { result = await this.codexAnswer({ socketPath: binding.socket_path, threadId: parseAddress(to).session, answer: { question: question.native, answers: reply.answers } }); }
|
|
910
928
|
catch (error) { result = { delivered: false, reason: error.code ?? 'native-question-error' }; }
|
|
911
929
|
if (result?.delivered === true) {
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
930
|
+
this.store.transaction(() => {
|
|
931
|
+
const filed = this.store.findIn(to, reply.envelope.id);
|
|
932
|
+
if (filed?.state === 'unread') this.store.markRead(to, filed.path);
|
|
933
|
+
this.store.putRecord('native_questions', key('delivered'), Date.now(), { create: true });
|
|
934
|
+
});
|
|
915
935
|
return outcome(0, `Answer ${shortId(reply.envelope.id)} delivered to native question ${id}.`);
|
|
916
936
|
}
|
|
917
937
|
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.`);
|
|
@@ -919,29 +939,10 @@ export class MailSend {
|
|
|
919
939
|
|
|
920
940
|
// ---- the send log and its guards ---------------------------------------------------------------------------------
|
|
921
941
|
|
|
922
|
-
#
|
|
923
|
-
#sentMarker(sender, id) { return join(this.#sentDir(sender), id); }
|
|
924
|
-
|
|
925
|
-
#logOf(sender) {
|
|
926
|
-
let text = '';
|
|
927
|
-
try { text = readFileSync(join(this.#sentDir(sender), 'log.jsonl'), 'utf8'); } catch { return []; }
|
|
928
|
-
return text.split('\n').map((line) => { try { return JSON.parse(line); } catch { return null; } }).filter(Boolean);
|
|
929
|
-
}
|
|
930
|
-
|
|
931
|
-
#sentIds(sender) { return this.#logOf(sender).map((entry) => entry.id).filter((id) => typeof id === 'string'); }
|
|
932
|
-
#sentThread(sender, id) { return this.#logOf(sender).find((entry) => entry.id === id && typeof entry.thread === 'string')?.thread ?? null; }
|
|
942
|
+
#sentThread(sender, id) { return this.store.sentThread(sender, id); }
|
|
933
943
|
|
|
934
944
|
/** The thread any author on this machine sent `id` in, as its send log keeps it. */
|
|
935
|
-
#sentThreadHere(id) {
|
|
936
|
-
let names = [];
|
|
937
|
-
try { names = readdirSync(join(this.mailRoot, 'sent')); } catch { return null; }
|
|
938
|
-
for (const name of names) {
|
|
939
|
-
let text = '';
|
|
940
|
-
try { text = readFileSync(join(this.mailRoot, 'sent', name, 'log.jsonl'), 'utf8'); } catch { continue; }
|
|
941
|
-
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; }
|
|
942
|
-
}
|
|
943
|
-
return null;
|
|
944
|
-
}
|
|
945
|
+
#sentThreadHere(id) { return this.store.sentThreadHere(id); }
|
|
945
946
|
|
|
946
947
|
/** The thread `parent` is filed in on this machine (mailbox.rs `filed_thread`). */
|
|
947
948
|
#filedThread(parent) {
|
|
@@ -959,19 +960,19 @@ export class MailSend {
|
|
|
959
960
|
return parent;
|
|
960
961
|
}
|
|
961
962
|
|
|
962
|
-
/**
|
|
963
|
+
/** The bounded reply chain between `a` and `b`, from exact message keys. */
|
|
963
964
|
#replyChainDepth(a, b, answered) {
|
|
964
|
-
const links = new Map([...this.store.links(a), ...this.store.links(b)]);
|
|
965
965
|
let depth = 0, current = answered;
|
|
966
|
-
while (current != null && depth <= REPLY_CHAIN_LIMIT
|
|
966
|
+
while (current != null && depth <= REPLY_CHAIN_LIMIT) {
|
|
967
|
+
const parent = this.store.replyParent(a, b, current);
|
|
968
|
+
if (parent === undefined) break;
|
|
969
|
+
depth += 1; current = parent;
|
|
970
|
+
}
|
|
967
971
|
return depth;
|
|
968
972
|
}
|
|
969
973
|
|
|
970
974
|
#recordSend(sender, to, id, thread, continuation = null) {
|
|
971
|
-
try {
|
|
972
|
-
mkdirSync(this.#sentDir(sender), { recursive: true });
|
|
973
|
-
appendFileSync(join(this.#sentDir(sender), 'log.jsonl'), `${JSON.stringify({ to, at_ms: Date.now(), id, thread, ...(continuation ? { continuation } : {}) })}\n`);
|
|
974
|
-
} catch { /* the log is best kept */ }
|
|
975
|
+
try { this.store.recordSend(sender, to, id, thread, continuation); } catch { /* the log is best kept */ }
|
|
975
976
|
}
|
|
976
977
|
|
|
977
978
|
/** A terminal's acknowledgement from the native watch's retained receipts, never from a filing (mail_send.rs
|
|
@@ -980,11 +981,8 @@ export class MailSend {
|
|
|
980
981
|
if (!terminal) return { delivered: true, message_ids: [id], receipt: 'native-mail' };
|
|
981
982
|
const parsed = parseAddress(address);
|
|
982
983
|
const recipient = `${parsed.harness}:${parsed.session}`;
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
for (const name of names) {
|
|
986
|
-
const state = readJson(join(this.mailRoot, 'terminal-delivery', name, 'queue.json'));
|
|
987
|
-
if (state?.recipient !== recipient) continue;
|
|
984
|
+
const state = this.store.terminalState(recipient);
|
|
985
|
+
if (state) {
|
|
988
986
|
const receipt = (Array.isArray(state.receipts) ? state.receipts : []).find((item) => Array.isArray(item?.message_ids) && item.message_ids.includes(id));
|
|
989
987
|
if (receipt) return receipt;
|
|
990
988
|
return { delivered: false, queued: true, message_ids: [id], reason: state.active?.error ?? null };
|
|
@@ -1003,7 +1001,7 @@ export class MailSend {
|
|
|
1003
1001
|
|
|
1004
1002
|
/** rememberedName for each of `addresses`, the ledger read once. */
|
|
1005
1003
|
rememberedNames(addresses) {
|
|
1006
|
-
const names =
|
|
1004
|
+
const names = Object.fromEntries(this.store.records('names').map(({ key, value }) => [key, value]));
|
|
1007
1005
|
const wanted = new Set(addresses), best = new Map();
|
|
1008
1006
|
if (names && typeof names === 'object') {
|
|
1009
1007
|
for (const [name, record] of Object.entries(names)) {
|
|
@@ -1016,7 +1014,7 @@ export class MailSend {
|
|
|
1016
1014
|
}
|
|
1017
1015
|
|
|
1018
1016
|
#rememberedAddress(name) {
|
|
1019
|
-
const record =
|
|
1017
|
+
const record = this.store.record('names', name);
|
|
1020
1018
|
if (!record || Date.now() - Math.max(record.seen_ms ?? 0, record.since_ms ?? 0) > NAME_KEPT_MS) return null;
|
|
1021
1019
|
return parseAddress(record.address)?.address ?? null;
|
|
1022
1020
|
}
|
|
@@ -1024,10 +1022,7 @@ export class MailSend {
|
|
|
1024
1022
|
/** Each running session's name recorded (mail_route.rs `remember_names`): a name two sessions carry is left as it was;
|
|
1025
1023
|
* written only when a name is new, names another session, or its last listing is an hour old. */
|
|
1026
1024
|
rememberNames(sessions) {
|
|
1027
|
-
const
|
|
1028
|
-
let names;
|
|
1029
|
-
try { names = JSON.parse(readFileSync(path, 'utf8')); } catch (error) { if (error.code !== 'ENOENT') return; names = {}; }
|
|
1030
|
-
if (!names || typeof names !== 'object') return;
|
|
1025
|
+
const names = Object.fromEntries(this.store.records('names').map(({ key, value }) => [key, value]));
|
|
1031
1026
|
const carried = new Map();
|
|
1032
1027
|
for (const row of sessions) {
|
|
1033
1028
|
const name = row.name.split('@')[0];
|
|
@@ -1035,7 +1030,7 @@ export class MailSend {
|
|
|
1035
1030
|
}
|
|
1036
1031
|
const now = Date.now();
|
|
1037
1032
|
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;
|
|
1038
|
-
|
|
1033
|
+
const changed = [];
|
|
1039
1034
|
for (const [name, addresses] of carried) {
|
|
1040
1035
|
if (addresses.length !== 1) continue;
|
|
1041
1036
|
const [address] = addresses;
|
|
@@ -1044,11 +1039,11 @@ export class MailSend {
|
|
|
1044
1039
|
if (current && latest(address) === name) continue;
|
|
1045
1040
|
const renewed = record?.address === address && latest(address) === name;
|
|
1046
1041
|
names[name] = { address, since_ms: renewed ? record.since_ms : now, seen_ms: now };
|
|
1047
|
-
changed
|
|
1042
|
+
changed.push(name);
|
|
1048
1043
|
}
|
|
1049
|
-
if (!changed) return;
|
|
1050
|
-
const
|
|
1051
|
-
|
|
1044
|
+
if (!changed.length) return;
|
|
1045
|
+
try { this.store.transaction(() => { for (const name of changed) this.store.putRecord('names', name, names[name]); }); }
|
|
1046
|
+
catch { /* kept for the next listing */ }
|
|
1052
1047
|
}
|
|
1053
1048
|
}
|
|
1054
1049
|
|
|
@@ -1070,11 +1065,3 @@ function questionAnswers(question, body) {
|
|
|
1070
1065
|
if (!values || typeof values !== 'object' || Object.keys(values).length !== Object.keys(out).length) throw new Error('answer only the question ids shown');
|
|
1071
1066
|
return out;
|
|
1072
1067
|
}
|
|
1073
|
-
|
|
1074
|
-
/** A file made once, owner-only, written whole and synced (mail_question.rs `create`). */
|
|
1075
|
-
function createPrivate(dir, path, value) {
|
|
1076
|
-
mkdirSync(dir, { recursive: true });
|
|
1077
|
-
try { chmodSync(dir, 0o700); } catch { /* not ours to set */ }
|
|
1078
|
-
const fd = openSync(path, 'wx', 0o600);
|
|
1079
|
-
try { writeSync(fd, JSON.stringify(value)); fsyncSync(fd); } finally { closeSync(fd); }
|
|
1080
|
-
}
|