@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/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). 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';
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.#sentIds(callerAddress)) if (id.startsWith(answered) && !found.includes(id)) found.push(id);
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
- let previous = null; try { previous = readFileSync(marker, 'utf8'); } catch { /* not sent before */ }
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 { mkdirSync(join(marker, '..'), { recursive: true }); writeFileSync(marker, text); } catch { /* the marker is best kept */ } }
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 { unlinkSync(filed); } catch { /* gone */ } } throw new Error(relayed.detail); }
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 ? readJson(previous.path) : null;
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 = readJson(found.path);
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
- const after = this.store.wakeState(address, id);
872
- this.store.setWakeState(address, id, { ...after, handover_at_ms: null });
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 dir = join(this.mailRoot, 'native-questions');
881
- const path = (suffix) => join(dir, `${id}.${suffix}`);
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 = readJson(path('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
- try { createPrivate(dir, path('claim'), reply); }
898
- catch (error) {
899
- 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.');
900
- throw error;
901
- }
902
- this.store.deliver(to, reply.envelope);
903
- // published to the hook only once the whole reply is in mail
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 = readJson(join(dir, `binding-${blake3Hex(to)}.json`));
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
- const filed = this.store.findIn(to, reply.envelope.id);
913
- if (filed?.state === 'unread') this.store.markRead(to, filed.path);
914
- try { createPrivate(dir, path('delivered'), Date.now()); } catch (error) { if (error.code !== 'EEXIST') throw error; }
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
- #sentDir(sender) { return join(this.mailRoot, 'sent', blake3Hex(sender).slice(0, 24)); }
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
- /** How deep the reply chain ending at `answered` runs between `a` and `b`, read from both mailboxes' folders. */
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 && links.has(current)) { depth += 1; current = links.get(current); }
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
- let names = [];
984
- try { names = readdirSync(join(this.mailRoot, 'terminal-delivery')); } catch { /* none yet */ }
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 = readJson(join(this.mailRoot, 'names.json'));
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 = readJson(join(this.mailRoot, 'names.json'))?.[name];
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 path = join(this.mailRoot, 'names.json');
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
- let changed = false;
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 = true;
1039
+ changed.push(name);
1048
1040
  }
1049
- if (!changed) return;
1050
- const staged = `${path}.${process.pid}.tmp`;
1051
- try { writeFileSync(staged, JSON.stringify(names)); renameSync(staged, path); } catch { /* kept for the next listing */ }
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
- }