@volter/supercode-teams 0.3.151 → 0.3.152
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 +65 -81
- 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;
|
|
@@ -81,6 +89,7 @@ export class MailSend {
|
|
|
81
89
|
const refused = await this.#authorizeReceiver(context, receiver.continuation?.chain ?? [receiver.address]);
|
|
82
90
|
if (refused) return refused;
|
|
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.
|
|
@@ -140,6 +149,7 @@ export class MailSend {
|
|
|
140
149
|
async #authorizeReceiver(context, addresses) {
|
|
141
150
|
if (!context.authorizeRecipients) return null;
|
|
142
151
|
const reason = await context.authorizeRecipients(addresses);
|
|
152
|
+
this.#active();
|
|
143
153
|
return reason ? outcome(EXIT_REFUSED, `Not sent: ${reason}. Nothing was filed or delivered by this request.`) : null;
|
|
144
154
|
}
|
|
145
155
|
|
|
@@ -174,7 +184,7 @@ export class MailSend {
|
|
|
174
184
|
#answeredId(callerAddress, answered) {
|
|
175
185
|
if (typeof answered !== 'string' || answered.length >= FULL_ID_LEN) return { id: answered ?? null };
|
|
176
186
|
const found = this.store.findPrefixIn(callerAddress, answered).map((item) => item.envelope.id);
|
|
177
|
-
for (const id of this
|
|
187
|
+
for (const id of this.store.sentIds(callerAddress, answered)) if (!found.includes(id)) found.push(id);
|
|
178
188
|
if (found.length === 1) return { id: found[0] };
|
|
179
189
|
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
190
|
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 +285,8 @@ export class MailSend {
|
|
|
275
285
|
if (address === caller.address) return outcome(EXIT_REFUSED, 'Not sent: that address is your own session.');
|
|
276
286
|
const door = parseAddress(address).harness === AGENT_HARNESS ? null : this.#door(address, sessions);
|
|
277
287
|
const messageId = id == null && chosen ? chosen : messageIdFor(caller.address, id);
|
|
278
|
-
const marker = this.#sentMarker(caller.address, messageId);
|
|
279
288
|
const terminal = door?.kind === 'hook' && door.pane != null;
|
|
280
|
-
|
|
289
|
+
const previous = this.store.sentMarker(caller.address, messageId);
|
|
281
290
|
if (previous !== null) {
|
|
282
291
|
if (terminal && !queue) this.store.requestWake(address, messageId);
|
|
283
292
|
return outcome(0, `Already sent with --id ${id ?? ''}: ${previous} Nothing was sent again.`, this.#deliveryReceipt(address, messageId, terminal));
|
|
@@ -339,7 +348,7 @@ export class MailSend {
|
|
|
339
348
|
const text = code === 0
|
|
340
349
|
? `sent to ${name} (${harness}, ${door.kind}): ${what}. Message id ${shortId(messageId)}.${receiver.alias ?? ''}${idleNote} Don't poll; carry on.`
|
|
341
350
|
: `${name} (${harness}): ${what}. Message id ${shortId(messageId)}.${idleNote}`;
|
|
342
|
-
if (code === 0 && id != null) { try {
|
|
351
|
+
if (code === 0 && id != null) { try { this.store.setSentMarker(caller.address, messageId, text); } catch { /* the marker is best kept */ } }
|
|
343
352
|
return outcome(code, text, code === 0 ? this.#deliveryReceipt(address, messageId, terminal) : null);
|
|
344
353
|
}
|
|
345
354
|
|
|
@@ -358,6 +367,7 @@ export class MailSend {
|
|
|
358
367
|
* refused before anything was sent, and an Error with what failed.
|
|
359
368
|
*/
|
|
360
369
|
async route(sent, to, door, wake, notifyWhenIdle) {
|
|
370
|
+
this.#active();
|
|
361
371
|
this.store.open(to);
|
|
362
372
|
let finalReply = false;
|
|
363
373
|
let delivered;
|
|
@@ -421,7 +431,7 @@ export class MailSend {
|
|
|
421
431
|
if (filedHere && wake) { try { this.store.requestWake(to, handed.id); } catch { /* the wake filed above stands */ } }
|
|
422
432
|
throw Object.assign(new Error(relayed.detail), { unanswered: true });
|
|
423
433
|
}
|
|
424
|
-
if (!relayed.delivered && !received) { if (filedHere) { try {
|
|
434
|
+
if (!relayed.delivered && !received) { if (filedHere) { try { this.store.remove(to, filed); } catch { /* gone */ } } throw new Error(relayed.detail); }
|
|
425
435
|
// relayed: a copy filed unread before this delivery (an earlier unanswered or queued one, held for the receiver's
|
|
426
436
|
// own read) is moved to cur/, so that read never hands it over a second time
|
|
427
437
|
if (relayed.delivered && !received) {
|
|
@@ -565,6 +575,7 @@ export class MailSend {
|
|
|
565
575
|
try { await this.agents.resume(to); } catch (error) { throw new Error(`not resumed: ${error.message}`); }
|
|
566
576
|
const started = Date.now();
|
|
567
577
|
for (;;) {
|
|
578
|
+
this.#active();
|
|
568
579
|
door = this.#door(to, this.live.sessions());
|
|
569
580
|
if (door) break;
|
|
570
581
|
if (Date.now() - started > RESUME_WAIT_MS) return { how: 'not_running', text: 'resumed; the message waits in its mailbox' };
|
|
@@ -650,7 +661,7 @@ export class MailSend {
|
|
|
650
661
|
if (typeof key !== 'string' || !key || Buffer.byteLength(key) > 256) return refused('invalid_key', 'idempotency_key needs 1–256 bytes');
|
|
651
662
|
id = `m-${blake3Hex(JSON.stringify([sender, receiver, key]))}`.slice(0, 26);
|
|
652
663
|
const previous = this.store.findIn(receiver, id);
|
|
653
|
-
const saved = previous ?
|
|
664
|
+
const saved = previous ? this.store.body(previous) : null;
|
|
654
665
|
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
666
|
return refused('idempotency_conflict', 'this key already names different mail');
|
|
656
667
|
}
|
|
@@ -785,9 +796,11 @@ export class MailSend {
|
|
|
785
796
|
* (mail_route.rs `type_user_turns`).
|
|
786
797
|
*/
|
|
787
798
|
async typeUserTurns(address, pane) {
|
|
799
|
+
this.#active();
|
|
788
800
|
const typed = [];
|
|
789
801
|
const harness = parseAddress(address).harness;
|
|
790
802
|
for (const waiting of this.store.userTurns(address)) {
|
|
803
|
+
this.#active();
|
|
791
804
|
const claimed = this.store.claimUserTurn(address, waiting);
|
|
792
805
|
if (!claimed) break;
|
|
793
806
|
let result;
|
|
@@ -817,9 +830,10 @@ export class MailSend {
|
|
|
817
830
|
* `{ outcome: 'delivered' | 'filed' | 'later', detail }`.
|
|
818
831
|
*/
|
|
819
832
|
async handOver(address, id, { wake = false } = {}) {
|
|
833
|
+
this.#active();
|
|
820
834
|
const found = this.store.findIn(address, id);
|
|
821
835
|
if (!found) return { outcome: 'filed', detail: 'not in its mailbox' };
|
|
822
|
-
const sent =
|
|
836
|
+
const sent = this.store.body(found);
|
|
823
837
|
if (!sent) return { outcome: 'later', detail: 'its envelope could not be read' };
|
|
824
838
|
if (found.state !== 'unread') return { outcome: 'filed', detail: 'already read' };
|
|
825
839
|
// 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 +882,10 @@ export class MailSend {
|
|
|
868
882
|
return error instanceof Refused ? { outcome: 'filed', detail: error.kind } : { outcome: 'later', detail: error.message };
|
|
869
883
|
} finally {
|
|
870
884
|
this.handing.delete(key);
|
|
871
|
-
|
|
872
|
-
|
|
885
|
+
if (!this.stopped) {
|
|
886
|
+
const after = this.store.wakeState(address, id);
|
|
887
|
+
this.store.setWakeState(address, id, { ...after, handover_at_ms: null });
|
|
888
|
+
}
|
|
873
889
|
}
|
|
874
890
|
}
|
|
875
891
|
|
|
@@ -877,13 +893,13 @@ export class MailSend {
|
|
|
877
893
|
|
|
878
894
|
/** Answer the native question `id` asked in `to` (mail_question.rs `reply`): only its recorded creator answers, once. */
|
|
879
895
|
async #questionReply(caller, to, id, body, link) {
|
|
880
|
-
const
|
|
881
|
-
const
|
|
882
|
-
const question = readJson(path('json'));
|
|
896
|
+
const key = (suffix) => `${id}.${suffix}`;
|
|
897
|
+
const question = this.store.record('native_questions', key('json'));
|
|
883
898
|
if (!question) return outcome(EXIT_FAILED, `Not answered: question ${id} could not be read.`);
|
|
884
899
|
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.');
|
|
900
|
+
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
901
|
if (parseAddress(to).harness === 'claude-code') {
|
|
886
|
-
const waiting =
|
|
902
|
+
const waiting = this.store.record('native_questions', key('waiting'));
|
|
887
903
|
if (!waiting) return outcome(EXIT_REFUSED, 'Not answered: the native question hook is no longer waiting.');
|
|
888
904
|
let alive = false; try { process.kill(waiting.pid, 0); alive = true; } catch (error) { alive = error.code === 'EPERM'; }
|
|
889
905
|
if (!Number.isInteger(waiting.pid) || !alive) return outcome(EXIT_REFUSED, 'Not answered: the native question hook has ended.');
|
|
@@ -894,24 +910,25 @@ export class MailSend {
|
|
|
894
910
|
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
911
|
answers,
|
|
896
912
|
};
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
linkSync(path('claim'), path('reply'));
|
|
913
|
+
const claimed = this.store.transaction(() => {
|
|
914
|
+
if (!this.store.putRecord('native_questions', key('claim'), reply, { create: true })) return false;
|
|
915
|
+
this.store.deliver(to, reply.envelope);
|
|
916
|
+
this.store.putRecord('native_questions', key('reply'), reply, { create: true });
|
|
917
|
+
return true;
|
|
918
|
+
});
|
|
919
|
+
if (!claimed) return outcome(EXIT_REFUSED, 'An answer is already recorded for this question. Nothing was submitted again; inspect its thread for delivery.');
|
|
905
920
|
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 =
|
|
921
|
+
const binding = this.store.record('native_questions', `binding-${blake3Hex(to)}.json`);
|
|
907
922
|
if (!binding?.socket_path) return outcome(EXIT_STORED, 'Answer recorded but NOT delivered: this launch has no native question endpoint. Nothing was typed.');
|
|
908
923
|
let result;
|
|
909
924
|
try { result = await this.codexAnswer({ socketPath: binding.socket_path, threadId: parseAddress(to).session, answer: { question: question.native, answers: reply.answers } }); }
|
|
910
925
|
catch (error) { result = { delivered: false, reason: error.code ?? 'native-question-error' }; }
|
|
911
926
|
if (result?.delivered === true) {
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
927
|
+
this.store.transaction(() => {
|
|
928
|
+
const filed = this.store.findIn(to, reply.envelope.id);
|
|
929
|
+
if (filed?.state === 'unread') this.store.markRead(to, filed.path);
|
|
930
|
+
this.store.putRecord('native_questions', key('delivered'), Date.now(), { create: true });
|
|
931
|
+
});
|
|
915
932
|
return outcome(0, `Answer ${shortId(reply.envelope.id)} delivered to native question ${id}.`);
|
|
916
933
|
}
|
|
917
934
|
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 +936,10 @@ export class MailSend {
|
|
|
919
936
|
|
|
920
937
|
// ---- the send log and its guards ---------------------------------------------------------------------------------
|
|
921
938
|
|
|
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; }
|
|
939
|
+
#sentThread(sender, id) { return this.store.sentThread(sender, id); }
|
|
933
940
|
|
|
934
941
|
/** 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
|
-
}
|
|
942
|
+
#sentThreadHere(id) { return this.store.sentThreadHere(id); }
|
|
945
943
|
|
|
946
944
|
/** The thread `parent` is filed in on this machine (mailbox.rs `filed_thread`). */
|
|
947
945
|
#filedThread(parent) {
|
|
@@ -959,19 +957,19 @@ export class MailSend {
|
|
|
959
957
|
return parent;
|
|
960
958
|
}
|
|
961
959
|
|
|
962
|
-
/**
|
|
960
|
+
/** The bounded reply chain between `a` and `b`, from exact message keys. */
|
|
963
961
|
#replyChainDepth(a, b, answered) {
|
|
964
|
-
const links = new Map([...this.store.links(a), ...this.store.links(b)]);
|
|
965
962
|
let depth = 0, current = answered;
|
|
966
|
-
while (current != null && depth <= REPLY_CHAIN_LIMIT
|
|
963
|
+
while (current != null && depth <= REPLY_CHAIN_LIMIT) {
|
|
964
|
+
const parent = this.store.replyParent(a, b, current);
|
|
965
|
+
if (parent === undefined) break;
|
|
966
|
+
depth += 1; current = parent;
|
|
967
|
+
}
|
|
967
968
|
return depth;
|
|
968
969
|
}
|
|
969
970
|
|
|
970
971
|
#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 */ }
|
|
972
|
+
try { this.store.recordSend(sender, to, id, thread, continuation); } catch { /* the log is best kept */ }
|
|
975
973
|
}
|
|
976
974
|
|
|
977
975
|
/** A terminal's acknowledgement from the native watch's retained receipts, never from a filing (mail_send.rs
|
|
@@ -980,11 +978,8 @@ export class MailSend {
|
|
|
980
978
|
if (!terminal) return { delivered: true, message_ids: [id], receipt: 'native-mail' };
|
|
981
979
|
const parsed = parseAddress(address);
|
|
982
980
|
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;
|
|
981
|
+
const state = this.store.terminalState(recipient);
|
|
982
|
+
if (state) {
|
|
988
983
|
const receipt = (Array.isArray(state.receipts) ? state.receipts : []).find((item) => Array.isArray(item?.message_ids) && item.message_ids.includes(id));
|
|
989
984
|
if (receipt) return receipt;
|
|
990
985
|
return { delivered: false, queued: true, message_ids: [id], reason: state.active?.error ?? null };
|
|
@@ -1003,7 +998,7 @@ export class MailSend {
|
|
|
1003
998
|
|
|
1004
999
|
/** rememberedName for each of `addresses`, the ledger read once. */
|
|
1005
1000
|
rememberedNames(addresses) {
|
|
1006
|
-
const names =
|
|
1001
|
+
const names = Object.fromEntries(this.store.records('names').map(({ key, value }) => [key, value]));
|
|
1007
1002
|
const wanted = new Set(addresses), best = new Map();
|
|
1008
1003
|
if (names && typeof names === 'object') {
|
|
1009
1004
|
for (const [name, record] of Object.entries(names)) {
|
|
@@ -1016,7 +1011,7 @@ export class MailSend {
|
|
|
1016
1011
|
}
|
|
1017
1012
|
|
|
1018
1013
|
#rememberedAddress(name) {
|
|
1019
|
-
const record =
|
|
1014
|
+
const record = this.store.record('names', name);
|
|
1020
1015
|
if (!record || Date.now() - Math.max(record.seen_ms ?? 0, record.since_ms ?? 0) > NAME_KEPT_MS) return null;
|
|
1021
1016
|
return parseAddress(record.address)?.address ?? null;
|
|
1022
1017
|
}
|
|
@@ -1024,10 +1019,7 @@ export class MailSend {
|
|
|
1024
1019
|
/** Each running session's name recorded (mail_route.rs `remember_names`): a name two sessions carry is left as it was;
|
|
1025
1020
|
* written only when a name is new, names another session, or its last listing is an hour old. */
|
|
1026
1021
|
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;
|
|
1022
|
+
const names = Object.fromEntries(this.store.records('names').map(({ key, value }) => [key, value]));
|
|
1031
1023
|
const carried = new Map();
|
|
1032
1024
|
for (const row of sessions) {
|
|
1033
1025
|
const name = row.name.split('@')[0];
|
|
@@ -1035,7 +1027,7 @@ export class MailSend {
|
|
|
1035
1027
|
}
|
|
1036
1028
|
const now = Date.now();
|
|
1037
1029
|
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
|
-
|
|
1030
|
+
const changed = [];
|
|
1039
1031
|
for (const [name, addresses] of carried) {
|
|
1040
1032
|
if (addresses.length !== 1) continue;
|
|
1041
1033
|
const [address] = addresses;
|
|
@@ -1044,11 +1036,11 @@ export class MailSend {
|
|
|
1044
1036
|
if (current && latest(address) === name) continue;
|
|
1045
1037
|
const renewed = record?.address === address && latest(address) === name;
|
|
1046
1038
|
names[name] = { address, since_ms: renewed ? record.since_ms : now, seen_ms: now };
|
|
1047
|
-
changed
|
|
1039
|
+
changed.push(name);
|
|
1048
1040
|
}
|
|
1049
|
-
if (!changed) return;
|
|
1050
|
-
const
|
|
1051
|
-
|
|
1041
|
+
if (!changed.length) return;
|
|
1042
|
+
try { this.store.transaction(() => { for (const name of changed) this.store.putRecord('names', name, names[name]); }); }
|
|
1043
|
+
catch { /* kept for the next listing */ }
|
|
1052
1044
|
}
|
|
1053
1045
|
}
|
|
1054
1046
|
|
|
@@ -1070,11 +1062,3 @@ function questionAnswers(question, body) {
|
|
|
1070
1062
|
if (!values || typeof values !== 'object' || Object.keys(values).length !== Object.keys(out).length) throw new Error('answer only the question ids shown');
|
|
1071
1063
|
return out;
|
|
1072
1064
|
}
|
|
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
|
-
}
|