@volter/supercode-teams 0.3.150 → 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 CHANGED
@@ -14,7 +14,7 @@ evidence without installing Volter Harness on each developer's machine. Start wi
14
14
  [Teams guide](../../docs/guides/teams.md); the design is
15
15
  [Teams server, CLI and API](../../docs/plans/teams-server.md).
16
16
 
17
- The server uses SQLite and requires Node 22.13 or newer. Package exports: `.` (the daemon,
17
+ The server and machine daemon use SQLite and require Node 22.13.0 or newer. Package exports: `.` (the daemon,
18
18
  channel and client), `/machine`, `/client`, `/admission`, `/fleet`, `/server`,
19
19
  `/workspace-client`, `/connector`, `/connector/native-continuation` and `/contracts`. The
20
20
  workspace client is browser-safe and takes explicit credentials. Native capture and execution use
@@ -40,6 +40,19 @@ and [implementation specification](../../docs/plans/teams-server-build/README.md
40
40
  the same daemon enrolled to the selected Teams context, adding catalog sync and the server link.
41
41
  The daemon owns one `supercode harness serve` and the tmux terminal host.
42
42
 
43
+ It alone opens `<supercode home>/mail.sqlite` in WAL mode. A separate SQLite writer lease lasts until shutdown.
44
+ Clients and native pane wrappers ask its socket for mail and launch bindings. At the first start, it imports the
45
+ legacy Maildir from a private snapshot and removes only successfully imported, byte-verified sources; unknown or
46
+ unreadable files remain diagnostics. Mail and launch requests are refused until import completes. A live older Codex
47
+ wrapper that still writes legacy bindings prevents migration. Ordinary reads never reopen the retired folder.
48
+ Settled read bodies expire after 30 days, with explicit placeholders and retained thread headers; pending mail keeps
49
+ its originals. Expired read headers are capped at 1,000,000 and carrier/terminal event log tables at 10,000 rows each. Read the
50
+ [dated qualification scope](../../docs/evidence/sqlite-mail-store-2026-10-08.md) before inferring platform support.
51
+
52
+ `supercode teams rpc mail.statistics` reads local storage counts and import/retention receipts. It is refused remotely
53
+ and through the app door. Writer groups count stored envelope copies, not call attempts or quotas; database page
54
+ counts exclude physical WAL size. Successful imports record their actual source reads, bytes, batch sizes and time.
55
+
43
56
  Two doors reach it:
44
57
 
45
58
  - **Local**: an owner-only Unix socket, `$SUPERCODE_HOME/teams/machine.sock` (or, when that path
@@ -178,6 +191,25 @@ its token or removing an origin closes the channels already admitted; a door lef
178
191
  off. While its port is taken the daemon retries every ten seconds, and `status` shows why it is not
179
192
  listening (app-door.state.json).
180
193
 
194
+ ## Native mail continuation
195
+
196
+ Mail to an old Claude Code address follows only the harness's explicit terminal
197
+ `continued-in` record to a live UUID on the same machine. It never infers a route
198
+ from a name or working directory. The send result and receipt retain
199
+ `continuation: { addressed, recipient, chain }`; send logs retain that metadata.
200
+ The existing plain-text sent markers and native relay behavior are unchanged.
201
+ If the receiver rolls over between an attempt and a same-`--id` repeat, delivery
202
+ to its successor can duplicate the message (separate task t_65153cfb). This
203
+ routing correction does not promise exactly-once delivery across rollover.
204
+ The target daemon rechecks mail grants on all routed addresses. A grant on only
205
+ the old address does not cover its continuation. Caller identity, pane ownership,
206
+ history access and old mailbox contents are unchanged.
207
+
208
+ The named lookup reads at most 64 KiB per transcript tail and follows at most
209
+ 16 links within a shared 4,096-project-entry / 750 ms work budget. Incomplete,
210
+ changed, conflicting or cyclic records are refused. The filesystem reads are
211
+ asynchronous; an in-progress OS read is not cancelled by the elapsed budget.
212
+
181
213
  ## Files (under `$SUPERCODE_HOME/teams`)
182
214
 
183
215
  `machine.key|pub` (the host key; it signs `describe` and nothing else) · `machine.sock` ·
package/bin/teams.mjs CHANGED
@@ -490,12 +490,20 @@ async function main() {
490
490
  // (order 3 PR 3): what `message list`, `show` and the board read instead of asking those machines
491
491
  return withNode(async (rpc) => out(await rpc.call('machine.records', {})));
492
492
  case 'rpc': {
493
+ if (!sub) die('rpc <method> [JSON params] [--machine NAME]');
494
+ const method = sub.startsWith('harness.v1.') ? sub : `harness.v1.${sub}`;
495
+ const params = rest[0] ? JSON.parse(rest[0]) : {};
493
496
  const channel = await machineChannel();
497
+ // Mail operations were ported to the node door. Keep their operator CLI
498
+ // on that door; native sessions.* requests retain the harness transport.
499
+ if (method.startsWith('harness.v1.mail.')) {
500
+ try { out(await (await nodeRpc(channel)).call(method, params)); }
501
+ finally { channel.close(); }
502
+ return;
503
+ }
494
504
  const client = new SupercodeHarnessClient({ transport: harnessTransport(channel) });
495
505
  try {
496
506
  await client.start?.();
497
- const params = rest[0] ? JSON.parse(rest[0]) : {};
498
- const method = sub.startsWith('harness.v1.') ? sub : `harness.v1.${sub}`;
499
507
  out(await client.request(method, params));
500
508
  } finally {
501
509
  await client.close?.();
package/machine.mjs CHANGED
@@ -36,7 +36,7 @@ import { openMachine, normalMachineName } from '@volter/teams/machine-door';
36
36
  import { RecordHolder, RecordProducer, RECENT_PER_MAILBOX, RECORDS_NOT_GRANTED } from './machine-records.mjs';
37
37
  import { LiveSessions, ACTING_VARIABLES } from './live-sessions.mjs';
38
38
  import { checkRemoteClose } from './remote-close.mjs';
39
- import { MailStore } from './mail/store.mjs';
39
+ import { SqliteMailStore } from './mail/sqlite-store.mjs';
40
40
  import { MailReads, panePrompt } from './mail/reads.mjs';
41
41
  import { render } from './mail/envelope.mjs';
42
42
  import { Agents } from './mail/agents.mjs';
@@ -99,6 +99,7 @@ import { DEFAULT_RANK, normalizeRank, judge } from './attention.mjs';
99
99
  import { popupTerminal, openPopup } from './popup-terminals.mjs';
100
100
  import { JobOwner, executionCapabilities, streamProgram, serveWorkspace } from './execution.mjs';
101
101
  import { TerminalMailbox, retainedTerminalMail } from './terminal-mail.mjs';
102
+ import { nativeLaunchKey } from './native-launch-binding.mjs';
102
103
  import { confirmNativeInitialInput } from './native-initial-input.mjs';
103
104
  import { sessionPeople } from './session-people.mjs';
104
105
  import { canonicalCodexFolderTrust, codexProgram, codexStartupArguments, codexUpdatePrompt, codexVersion, codexWindowsPastes, settleCodexHookReview } from './codex-startup.mjs';
@@ -560,7 +561,6 @@ export class TeamsMachine extends MachineCore {
560
561
  // `stop()` can abandon it rather than leave a timer running past the daemon.
561
562
  this.voiceProcesses = new Map(); // session address -> optional voice pack child
562
563
  this.terminalMailboxes = new Map();
563
- this.terminalMailRoot = join(this.env.SUPERCODE_HOME || join(this.env.XDG_CONFIG_HOME || join(homedir(), '.config'), 'supercode'), 'mail');
564
564
  this.terminalMailTimer = setInterval(() => {
565
565
  if (this.terminalMailWork) return;
566
566
  this.terminalMailWork = this.#drainTerminalMail().catch(error => this.emit('mail-watch-stderr', `terminal mailbox: ${error.message}`)).finally(() => { this.terminalMailWork = null; });
@@ -649,20 +649,33 @@ export class TeamsMachine extends MachineCore {
649
649
  // ---- lifecycle ------------------------------------------------------------
650
650
 
651
651
  async beforeListen() {
652
- this.bornAt ??= Date.now();
653
- mkdirSync(this.portsDir, { recursive: true }); // the port table's announce directory exists before any pane launches
654
- this.#startServe();
655
- if (this.#pendingNotices().length) this.#watchEnds();
656
- this.jobs = new JobOwner({ home: this.home, env: this.env, log: (row) => this.log.append({ principal: null, cap: 'exec', target: `job:${row.job}`, outcome: row.outcome, detail: row.reason }) });
657
- this.jobs.reapOrphans();
652
+ try {
653
+ this.bornAt ??= Date.now();
654
+ this.mailStore = new SqliteMailStore({ mailRoot: join(supercodeHome(this.env), 'mail'), machine: machineName(this.env), log: (line) => this.emit('records-log', line) });
655
+ this.mailStore.acquire();
656
+ mkdirSync(this.portsDir, { recursive: true }); // the port table's announce directory exists before any pane launches
657
+ this.#startServe();
658
+ if (this.#pendingNotices().length) this.#watchEnds();
659
+ this.jobs = new JobOwner({ home: this.home, env: this.env, log: (row) => this.log.append({ principal: null, cap: 'exec', target: `job:${row.job}`, outcome: row.outcome, detail: row.reason }) });
660
+ this.jobs.reapOrphans();
661
+ } catch (error) {
662
+ await this.afterStop();
663
+ throw error;
664
+ }
658
665
  }
659
666
 
660
667
  async afterListen() {
668
+ try {
669
+ this.#startLive();
670
+ await this.#startMail();
671
+ this.mailReady = true;
672
+ } catch (error) {
673
+ await this.stop();
674
+ throw error;
675
+ }
661
676
  // Web apps on this machine reach it through the app door when the person has turned it on (app-door.mjs).
662
677
  this.appDoor = serveAppDoor({ home: this.home, open: () => { const [page, daemon] = inProcessPair(); void this.acceptLocal(daemon, { appDoor: true }); return page; }, log: (row) => this.log.append({ principal: 'app-door', cap: 'app-door', target: row.origin ?? '*', outcome: row.outcome, detail: row.detail }) });
663
678
  this.#startHealth();
664
- this.#startLive();
665
- this.#startMail();
666
679
  this.#startRecords();
667
680
  if(Object.keys(this.inputRepairs).length)this.#watchInputRepairs();
668
681
  }
@@ -754,12 +767,12 @@ export class TeamsMachine extends MachineCore {
754
767
  }
755
768
 
756
769
  // ---- this machine's mail (mail/; docs/architecture/overview.md, "The mail operations") ---------------------------
757
- #startMail() {
770
+ async #startMail() {
758
771
  const mailRoot = join(supercodeHome(this.env), 'mail');
759
772
  // the Messages API a Claude relay talks to (step 4a: the daemon's, in process; the native watch served it)
760
- this.relayEndpoint = serveRelayEndpoint(mailRoot, { log: (line) => this.emit('mail-log', line) })
773
+ await this.mailStore.start();
774
+ this.relayEndpoint = serveRelayEndpoint(this.mailStore, { log: (line) => this.emit('mail-log', line) })
761
775
  .catch((error) => { this.emit('mail-log', `relay endpoint: not served (${error.message}); a Claude session cannot be sent mail until the daemon starts again`); return null; });
762
- this.mailStore = new MailStore({ mailRoot, machine: machineName(this.env), log: (line) => this.emit('records-log', line) }).start();
763
776
  // the session index's rows by address (its descriptors, as the records' index subscription hands them over)
764
777
  this.sessionDescriptors = new Map();
765
778
  const live = this.live;
@@ -771,18 +784,18 @@ export class TeamsMachine extends MachineCore {
771
784
  self: machineName(this.env),
772
785
  }),
773
786
  // the transcripts of every Claude home the map follows, and the native-question records beside the mail
774
- homes: { get claudeProjects() { return [...live.claudeDirs.keys()].map((dir) => join(dirname(dir), 'projects')); }, mailRoot },
787
+ homes: { get claudeProjects() { return [...live.claudeDirs.keys()].map((dir) => join(dirname(dir), 'projects')); }, store: this.mailStore },
775
788
  paneQuestion: async (pane) => { const session = await this.#paneByContext(pane); return session ? panePrompt(await this.host.capture(session.id, { lines: 60 })) : null; },
776
789
  askMachine: (machine, request) => this.#askMailDoor(machine, request),
777
790
  log: (line) => this.emit('mail-log', line),
778
791
  });
779
792
  // a send to a session, for every entry (3b-1): its routing on the live map, an agent's plan and its doors
780
793
  const machine = machineName(this.env);
781
- this.agents = new Agents({ mailRoot, machine, supercodeBin: this.supercodeBin, env: this.env,
794
+ this.agents = new Agents({ store: this.mailStore, machine, supercodeBin: this.supercodeBin, env: this.env,
782
795
  discover: (params) => this.#serveRequest('harness.v1.sessions.discover', params, 10_000),
783
796
  recordedConfig: (locator) => this.#serveRequest('harness.v1.sessions.recorded_config', { locator }, 15_000) });
784
797
  // a relay's hooks (`message gate`, `relay-receipt`, `relay-inbound`) run the machine's stable supercode: its front door
785
- this.relays = new Relays({ mailRoot, machine, supercodeBin: this.#stableSupercode(), hooksServed: () => this.#frontDoorServes('message gate'), serve: (method, params, timeoutMs) => this.#serveRequest(method, params, timeoutMs),
798
+ this.relays = new Relays({ mailRoot, store: this.mailStore, machine, supercodeBin: this.#stableSupercode(), hooksServed: () => this.#frontDoorServes('message gate'), serve: (method, params, timeoutMs) => this.#serveRequest(method, params, timeoutMs),
786
799
  runtimeOf: (harness, id) => live.runtimeReceipt(harness, id), runtimeNamed: (receiptId) => live.receiptNamed(receiptId),
787
800
  runtimesIn: (harness, cwd) => live.runtimesIn(harness, cwd),
788
801
  endpointOf: async () => (await this.relayEndpoint)?.url ?? null,
@@ -790,6 +803,7 @@ export class TeamsMachine extends MachineCore {
790
803
  log: (line) => this.emit('mail-log', line) });
791
804
  // a turn or a wake typed into a daemon pane's empty composer, in process (as `panes.turn` types one)
792
805
  const typeTurn = async (pane, harness, text, messageIds) => {
806
+ if (this.stopping) throw new Error('the daemon is stopping; no pane input was submitted');
793
807
  const session = await this.#paneByContext(pane);
794
808
  if (!session) throw new Error(`no pane with context ${pane}`);
795
809
  const result = await this.#paneTurn(session, pane, text, { messageIds, harness });
@@ -797,6 +811,7 @@ export class TeamsMachine extends MachineCore {
797
811
  return result.waits ?? result;
798
812
  };
799
813
  this.mailSend = new MailSend({ machine, mailRoot, live, store: this.mailStore, agents: this.agents, relays: this.relays,
814
+ claudeProjects: () => [...live.claudeDirs.keys()].map((dir) => join(dirname(dir), 'projects')),
800
815
  // a send waits on its receiver's door there (a relay's receipt takes up to 90 s), never past MAIL_SEND_MS
801
816
  askMachine: (to, request) => this.#askMailDoor(to, request, MAIL_SEND_MS),
802
817
  deliverRuntime: deliverToRuntime, codexAnswer: (request) => codexQuestions(request),
@@ -841,11 +856,12 @@ export class TeamsMachine extends MachineCore {
841
856
  const turnsWaiting = new Map(); // address → timer
842
857
  const typeWaiting = async (address) => {
843
858
  clearTimeout(turnsWaiting.get(address)); turnsWaiting.delete(address);
859
+ if (this.stopping) return;
844
860
  const session = live.sessions().find((row) => row.address === address);
845
861
  const pane = session ? this.mail.paneOf(session) : null;
846
862
  if (!pane || !this.mailStore.userTurns(address).length) return;
847
863
  try { await this.mailSend.typeUserTurns(address, pane); } catch (error) { this.emit('mail-log', `the user's turns for ${address} wait: ${error.message}`); }
848
- if (this.mailStore.userTurns(address).length) { const timer = setTimeout(() => void typeWaiting(address), 2000); timer.unref?.(); turnsWaiting.set(address, timer); }
864
+ if (!this.stopping && this.mailStore.userTurns(address).length) { const timer = setTimeout(() => void typeWaiting(address), 2000); timer.unref?.(); turnsWaiting.set(address, timer); }
849
865
  };
850
866
  this.mailStore.on('changed', (address) => { if (address && !turnsWaiting.has(address)) void typeWaiting(address); });
851
867
  // the turns already waiting when this listener came (the store read every mailbox at its start, before it), and every
@@ -853,6 +869,7 @@ export class TeamsMachine extends MachineCore {
853
869
  const everyMailbox = () => { for (const address of this.mailStore.addresses()) if (!turnsWaiting.has(address)) void typeWaiting(address); };
854
870
  everyMailbox();
855
871
  this.turnsSweep = setInterval(everyMailbox, 300_000); this.turnsSweep.unref?.();
872
+ this.stopWaitingTurns = () => { clearInterval(this.turnsSweep); for (const timer of turnsWaiting.values()) clearTimeout(timer); turnsWaiting.clear(); };
856
873
  // an agent's verbs (3b-2): its threads, a delegation, its record
857
874
  this.agentVerbs = new AgentVerbs({ machine, store: this.mailStore, agents: this.agents, live, supercodeBin: this.supercodeBin, env: this.env,
858
875
  paneOf: (session) => this.mail.paneOf(session), mailRoot,
@@ -867,17 +884,19 @@ export class TeamsMachine extends MachineCore {
867
884
  live.on('changed', () => {
868
885
  // while the Codex sessions are not known (a start, a serve restart) a Codex session missing has not ended: the last
869
886
  // known view stands until they are known again
870
- if (!live.codexKnown) return;
887
+ if (this.stopping || !live.codexKnown) return;
871
888
  const now = new Map(live.sessions().map((row) => [row.address, row.name]));
872
889
  // a Codex session that leaves the map has not provably ended (idle over a day, it is no longer followed): kept
873
890
  for (const [address, name] of wasLive) if (!now.has(address) && address.split(':')[2] !== 'codex') void this.relays.retire(address, name).catch(() => {});
874
891
  wasLive = now;
875
892
  });
876
893
  void live.whenCodexKnown(120_000).then((known) => {
894
+ if (this.stopping) return;
877
895
  if (!known) { this.emit('mail-log', 'relays: the live sessions were not known within two minutes of start; relays of ended sessions are retired at their next end'); return; }
878
896
  return this.relays.sweepAtStart((address) => live.sessions().some((row) => row.address === address));
879
897
  }).catch((error) => this.emit('mail-log', `relays: the start sweep failed: ${error.message}`));
880
898
  live.on('changed', () => {
899
+ if (this.stopping) return;
881
900
  if (naming) return;
882
901
  naming = setTimeout(() => { naming = null; try { this.mailSend.rememberNames(live.sessions()); } catch { /* kept at the next change */ } }, 1000);
883
902
  naming.unref?.();
@@ -1131,6 +1150,8 @@ export class TeamsMachine extends MachineCore {
1131
1150
  }
1132
1151
 
1133
1152
  async beforeClose() {
1153
+ this.mailReady = false;
1154
+ this.#stopMailWorkers();
1134
1155
  this.#stopHealth();
1135
1156
  clearInterval(this.terminalMailTimer); await this.terminalMailWork;
1136
1157
  clearInterval(this.processTimer); await this.processWork;
@@ -1163,14 +1184,10 @@ export class TeamsMachine extends MachineCore {
1163
1184
  for (const child of this.voiceProcesses.values()) child.kill('SIGTERM');
1164
1185
  this.voiceProcesses.clear();
1165
1186
  this.jobs?.stopAll();
1166
- void this.relayEndpoint?.then((endpoint) => endpoint?.close()).catch(() => {});
1167
- this.carrier?.stop();
1187
+ await this.relayEndpoint?.then((endpoint) => endpoint?.close()).catch(() => {});
1168
1188
  this.stopLiveFollow?.();
1189
+ this.#stopMailWorkers();
1169
1190
  this.mailStore?.stop();
1170
- clearInterval(this.hookSpoolTimer);
1171
- this.idleNotices?.stop();
1172
- this.agentUpkeep?.stop();
1173
- this.nativeQuestions?.stop();
1174
1191
  clearInterval(this.turnsSweep);
1175
1192
  this.stoppingServe = true;
1176
1193
  clearTimeout(this.serveRestart);
@@ -1180,6 +1197,22 @@ export class TeamsMachine extends MachineCore {
1180
1197
  this.host.dispose();
1181
1198
  }
1182
1199
 
1200
+ #stopMailWorkers() {
1201
+ if (this.mailWorkersStopped) return;
1202
+ this.mailWorkersStopped = true;
1203
+ this.mailSend?.stop();
1204
+ this.agents?.stop();
1205
+ this.agentVerbs?.stop();
1206
+ this.carrier?.stop();
1207
+ clearInterval(this.hookSpoolTimer);
1208
+ this.idleNotices?.stop();
1209
+ this.agentUpkeep?.stop();
1210
+ this.nativeQuestions?.stop();
1211
+ this.mailHooks?.stop();
1212
+ this.relays?.stop();
1213
+ this.stopWaitingTurns?.();
1214
+ }
1215
+
1183
1216
  connectionClosed(index) {
1184
1217
  for (const [key, owners] of this.routes) { owners.delete(index); if (owners.size === 0) this.routes.delete(key); }
1185
1218
  }
@@ -1539,6 +1572,9 @@ export class TeamsMachine extends MachineCore {
1539
1572
  }
1540
1573
 
1541
1574
  #serveMail(entry, stream, accept, refuse,requestOverride=null) {
1575
+ if (!this.mailReady || this.stopping) return refuse(this.stopping
1576
+ ? 'this machine daemon is stopping'
1577
+ : 'this machine daemon is preparing its mail database; retry once migration completes');
1542
1578
  // One message into a session on this machine, delivered by this daemon's own send (mail/send.mjs) through the same
1543
1579
  // doors a local send uses. The receiver is the
1544
1580
  // target: `mail:*` reaches every session here, `mail:<address>` one of them; a
@@ -1598,7 +1634,18 @@ export class TeamsMachine extends MachineCore {
1598
1634
  const link = linkOf(principal, entry.principalName ?? null);
1599
1635
  void (async () => {
1600
1636
  let answer;
1601
- try { answer = request.op === 'file' ? this.#fileHere(request, link) : await this.#sendHere(request, link); }
1637
+ try { answer = request.op === 'file' ? this.#fileHere(request, link) : await this.#sendHere(request, link, async (addresses) => {
1638
+ // Native continuation changes a recipient, not a grant. Re-derive this stream's admission after the named
1639
+ // transcript read, and require mail on every address in its route before the send can file anything.
1640
+ if (stream.closed) return 'the mail stream closed before delivery';
1641
+ const fresh = entry.verify ? await entry.verify() : entry.admitted;
1642
+ if (stream.closed) return 'the mail stream closed before delivery';
1643
+ if (fresh) entry.admitted = fresh;
1644
+ const missing = addresses.find(address => !holds(fresh, 'mail', `mail:${address}`));
1645
+ if (!missing) return null;
1646
+ this.log.append({ principal, cap: 'mail', target: `mail:${missing}`, outcome: 'refused', detail: 'receiver grant' });
1647
+ return `no live mail grant for mail:${missing}`;
1648
+ }); }
1602
1649
  catch (error) { answer = { outcome: 'failed', detail: error.message }; }
1603
1650
  this.log.append({ principal, cap: 'mail', target, outcome: answer.outcome === 'failed' || (answer.code ?? 0) !== 0 ? 'failed' : request.op, detail: answer.outcome === 'failed' ? String(answer.detail).slice(0, 200) : undefined });
1604
1651
  if (stream.closed) return;
@@ -1610,14 +1657,14 @@ export class TeamsMachine extends MachineCore {
1610
1657
  /** A send through the mail door, for a session here: one for another machine is
1611
1658
  * refused when it came through a link (never forwarded on this machine's own credential), and this machine's own
1612
1659
  * daemon sends on through the one way out. */
1613
- async #sendHere(request, link) {
1660
+ async #sendHere(request, link, authorizeRecipients) {
1614
1661
  const from = parseAddress(request.from)?.address;
1615
1662
  if (!from) return { code: 2, text: `\`${request.from}\` is not a session address; addresses look like sc:<machine>:<harness>:<session-id>`, receipt: null };
1616
1663
  const text = (key) => (typeof request[key] === 'string' ? request[key] : null);
1617
1664
  return this.mailSend.send({ address: from, name: text('from_name') ?? '' }, text('to') ?? '', text('body') ?? '', {
1618
1665
  subject: text('subject'), in_reply_to: text('in_reply_to'), notify_when_idle: request.notify_when_idle === true, queue: request.queue === true,
1619
1666
  id: text('id'), notice: request.notice === true, thread: text('thread'),
1620
- }, { link });
1667
+ }, { link, authorizeRecipients });
1621
1668
  }
1622
1669
 
1623
1670
  /** A message another machine's mailbox carried here, filed in a mailbox here: the
@@ -2020,9 +2067,8 @@ export class TeamsMachine extends MachineCore {
2020
2067
  if (claude && params.initialInput) throw Object.assign(new Error('Claude first input requires the native SessionStart receipt hook; this front door does not serve it'), { code: 503 });
2021
2068
  return launchCommand(params.program, args);
2022
2069
  }
2023
- const mailHome = this.env.SUPERCODE_HOME || join(this.env.XDG_CONFIG_HOME || join(homedir(), '.config'), 'supercode');
2070
+ const mailHome = dirname(this.mailStore.root);
2024
2071
  const dir = join(mailHome, 'mail', 'native-launches');
2025
- mkdirSync(dir, { recursive: true, mode: 0o700 });
2026
2072
  // A reused pane key still gets a new receipt baseline. A late hook from
2027
2073
  // its previous process cannot publish evidence for this launch.
2028
2074
  const binding = join(dir, `${pane}-${randomBytes(9).toString('base64url')}.json`);
@@ -2033,14 +2079,14 @@ export class TeamsMachine extends MachineCore {
2033
2079
  !args.some((arg) => arg === '--remote' || arg.startsWith('--remote=')) ? prompt : null;
2034
2080
  const claudeInput = claude && prompt ? { kind: params.kind, text: prompt, startedAt: Date.now(),
2035
2081
  deadlineMs: Date.now() + INITIAL_INPUT_READY_DEADLINE_MS + 10_000, typed: true } : null;
2036
- writeFileSync(binding, JSON.stringify({ key: params.key, kind: params.kind, pane, by, program: params.program, arguments: args, initialInput, claudeInput }), { mode: 0o600 });
2082
+ this.mailStore.putRecord('native_launches', basename(binding), { key: params.key, kind: params.kind, pane, by, program: params.program, arguments: args, initialInput, claudeInput });
2037
2083
  // The first input is durable before createSession can start the program.
2038
2084
  // Its body stays in the private binding, never in the launch journal.
2039
2085
  if (claudeInput) {
2040
2086
  this.launches.record({ ...this.launches.find(params.key), nativeInputBinding: binding });
2041
2087
  nativeInput({ ...claudeInput, binding });
2042
2088
  }
2043
- else if (initialInput) nativeInput({ binding, kind: params.kind, text: initialInput, startedAt: Date.now() });
2089
+ else nativeInput({ binding, kind: params.kind, text: initialInput, startedAt: Date.now() });
2044
2090
  if (params.kind === 'claude-code') {
2045
2091
  // Claude's exec-form hook passes each argument unchanged on all platforms.
2046
2092
  // A Windows .cmd shim is resolved to its Node executable and front door.
@@ -2067,7 +2113,7 @@ export class TeamsMachine extends MachineCore {
2067
2113
  // door. Give this pane its own native server, through Codex's --remote
2068
2114
  // Unix endpoint; never attach to another running session by inference.
2069
2115
  if (args.some((arg) => arg === '--remote' || arg.startsWith('--remote='))) return launchCommand(params.program, args);
2070
- return launchCommand(process.execPath, [fileURLToPath(new URL('./native-codex-pane.mjs', import.meta.url)), binding]);
2116
+ return launchCommand(process.execPath, [fileURLToPath(new URL('./native-codex-pane.mjs', import.meta.url)), binding, this.home]);
2071
2117
  }
2072
2118
 
2073
2119
  /**
@@ -2246,10 +2292,11 @@ export class TeamsMachine extends MachineCore {
2246
2292
  * before this was kept carries the same files. A Codex session has no such hook here and no origin: its resumed pane
2247
2293
  * stays its resumer's. */
2248
2294
  #sessionOrigin(session) {
2249
- let lines, names;
2250
- const dir = join(this.env.SUPERCODE_HOME || join(this.env.XDG_CONFIG_HOME || join(homedir(), '.config'), 'supercode'), 'mail', 'native-launches');
2251
- try { lines = readFileSync(this.launches.path, 'utf8').split('\n'); names = readdirSync(dir); } catch { return null; }
2252
- const read = (name) => { try { return JSON.parse(readFileSync(join(dir, name), 'utf8')); } catch { return null; } };
2295
+ let lines;
2296
+ try { lines = readFileSync(this.launches.path, 'utf8').split('\n'); } catch { return null; }
2297
+ const bindings = new Map(this.mailStore.records('native_launches').map(({ key, value }) => [key, value]));
2298
+ const names = [...bindings.keys()];
2299
+ const read = (name) => bindings.get(name) ?? null;
2253
2300
  const started = (entry) => names.some((name) => {
2254
2301
  if (!name.startsWith(`${entry.pane}-`) || !/^[^.]+\.json$/.test(name)) return false;
2255
2302
  const seed = read(name);
@@ -2418,6 +2465,19 @@ export class TeamsMachine extends MachineCore {
2418
2465
  const launch = this.launches.find(key);
2419
2466
  if (launch?.pane !== contextKey) return;
2420
2467
  this.launches.record({ ...launch, ...facts, status: launch.status });
2468
+ const binding = nativeInput?.binding ?? launch.nativeInputBinding;
2469
+ if (!this.stopping && facts.inputDelivered === true && binding) {
2470
+ try {
2471
+ const bindingKey = basename(binding);
2472
+ const seed = this.mailStore.record('native_launches', bindingKey);
2473
+ if (seed?.key === key && seed.pane === contextKey && seed.by === by && seed.input_settled_at_ms == null) {
2474
+ this.mailStore.putRecord('native_launches', bindingKey, { ...seed, input_settled_at_ms: Date.now() });
2475
+ }
2476
+ } catch (error) {
2477
+ // Retaining extra text never changes the native delivery receipt.
2478
+ this.emit('records-log', `confirmed launch input retained: settlement could not be recorded (${error.message})`);
2479
+ }
2480
+ }
2421
2481
  };
2422
2482
  const abort = new AbortController();
2423
2483
  const task = (async () => {
@@ -2451,7 +2511,7 @@ export class TeamsMachine extends MachineCore {
2451
2511
  }
2452
2512
  }
2453
2513
  if (nativeInput) {
2454
- const result = await confirmNativeInitialInput(nativeInput, { signal: abort.signal,
2514
+ const result = await confirmNativeInitialInput(nativeInput, { signal: abort.signal, store: this.mailStore,
2455
2515
  deadlineMs: Date.now() + INITIAL_INPUT_READY_DEADLINE_MS, live: async () => Boolean(await this.#paneByContext(contextKey)) });
2456
2516
  recordInput({ key, pane: contextKey, kind, by, ...resumed, ...executable, inputDelivered: result.delivered,
2457
2517
  inputReadyBy: nativeReadyBy, ...(result.delivered ? { inputReceipt: result.receipt } : { inputError: result.reason }),
@@ -2488,7 +2548,7 @@ export class TeamsMachine extends MachineCore {
2488
2548
  let identity;
2489
2549
  while (!signal.aborted && Date.now() < deadline) {
2490
2550
  if ((await this.#paneByContext(contextKey))?.id !== session.id) return { delivered: false, reason: 'pane-gone' };
2491
- const start = readJson(input.binding.replace(/\.json$/, '.session.json'));
2551
+ const start = this.mailStore.record('native_launches', basename(input.binding).replace(/\.json$/, '.session.json'));
2492
2552
  const id = /([0-9a-f]{8}-[0-9a-f-]{27})\.jsonl$/i.exec(String(start?.path ?? ''))?.[1];
2493
2553
  if (id && Number.isSafeInteger(start.offset) && start.offset >= 0) {
2494
2554
  identity = { kind: 'claude-code', path: start.path, sessionId: id, offset: start.offset };
@@ -2516,7 +2576,7 @@ export class TeamsMachine extends MachineCore {
2516
2576
  if ((onlyKey && launch.key !== onlyKey) || launch.kind !== 'claude-code' || !launch.nativeInputBinding ||
2517
2577
  launch.inputDelivered || launch.inputRefused || this.pendingInitialInputs.has(launch.key) ||
2518
2578
  !['issued', 'ready'].includes(launch.status)) continue;
2519
- const seed = readJson(launch.nativeInputBinding);
2579
+ const seed = this.mailStore.record('native_launches', basename(launch.nativeInputBinding));
2520
2580
  if (seed?.key !== launch.key || seed.pane !== launch.pane || seed.by !== launch.by || !seed.claudeInput?.text) continue;
2521
2581
  const session = panes.find(row => row.contextKey === launch.pane && !row.dead);
2522
2582
  if (!session) {
@@ -2532,9 +2592,10 @@ export class TeamsMachine extends MachineCore {
2532
2592
  }
2533
2593
 
2534
2594
  async #drainTerminalMail() {
2595
+ if (!this.mailReady || this.stopping) return;
2535
2596
  // One pane listing per tick, shared by every mailbox: a listing per mailbox each second was most of this
2536
2597
  // daemon's tmux spawns (8 retained mailboxes, about 70 tmux processes a second, t_23e47b73).
2537
- const retained = retainedTerminalMail(this.terminalMailRoot);
2598
+ const retained = retainedTerminalMail(this.mailStore);
2538
2599
  if (this.initialInputsRecovered && !retained.length && !this.terminalMailboxes.size) return;
2539
2600
  const panes = await this.host.listSessions();
2540
2601
  if (!this.initialInputsRecovered) {
@@ -2546,7 +2607,7 @@ export class TeamsMachine extends MachineCore {
2546
2607
  if (this.terminalMailboxes.has(saved.recipient) || !saved.binding) continue;
2547
2608
  const session = paneByContext(saved.binding.context);
2548
2609
  if (!session || session.rootPid !== saved.binding.pid) continue;
2549
- const queue = new TerminalMailbox(this.terminalMailRoot, saved.recipient, receipt => this.#recordTerminalReceipt(saved.binding.context, receipt),
2610
+ const queue = new TerminalMailbox(this.mailStore, saved.recipient, receipt => this.#recordTerminalReceipt(saved.binding.context, receipt),
2550
2611
  (locator, options) => this.#serveRequest('harness.v1.sessions.load', { locator, options: { ...options, include_subagents: false } }, 10000));
2551
2612
  queue.identity = saved.binding.identity; queue.host = this.host; queue.session = session;
2552
2613
  // A tmux host remints its client IDs after restart. Context and the live
@@ -2571,10 +2632,10 @@ export class TeamsMachine extends MachineCore {
2571
2632
  if (!receipt.delivered && !receipt.refused) return;
2572
2633
  for (const id of receipt.message_ids ?? []) {
2573
2634
  if (!/^first-[A-Za-z0-9_-]+$/.test(id)) continue;
2574
- const path = join(this.terminalMailRoot, 'native-launches', `${id.slice(6)}.json`);
2575
- const seed = readJson(path), launch = seed?.key && this.launches.find(seed.key);
2635
+ const key = `${id.slice(6)}.json`;
2636
+ const seed = this.mailStore.record('native_launches', key), launch = seed?.key && this.launches.find(seed.key);
2576
2637
  if (!launch || seed.pane !== contextKey || launch.pane !== contextKey) continue;
2577
- const native = /([0-9a-f]{8}-[0-9a-f-]{27})\.jsonl$/i.exec(String(readJson(path.replace(/\.json$/, '.session.json'))?.path ?? ''))?.[1];
2638
+ const native = /([0-9a-f]{8}-[0-9a-f-]{27})\.jsonl$/i.exec(String(this.mailStore.record('native_launches', key.replace(/\.json$/, '.session.json'))?.path ?? ''))?.[1];
2578
2639
  const { t: _previousAt, ...facts } = launch;
2579
2640
  this.launches.record({ ...facts, inputDelivered: receipt.delivered, inputPending: false, inputRefused: receipt.refused === true,
2580
2641
  ...(receipt.delivered ? { inputReceipt: receipt.receipt, inputError: null, ...(native ? { native_session: native } : {}) } : { inputError: receipt.reason }) });
@@ -2582,6 +2643,7 @@ export class TeamsMachine extends MachineCore {
2582
2643
  }
2583
2644
 
2584
2645
  async #terminalMail(session, contextKey, kind, text, options, identity = null) {
2646
+ if (this.stopping) throw new Error('the daemon is stopping; no terminal mail was submitted');
2585
2647
  // Mail a queue of this terminal already holds (the watch asking again after the same message ids) is answered from
2586
2648
  // that queue, which types it once the composer frees: nothing is asked of the harness again for it.
2587
2649
  const ids = options.messageIds ?? [];
@@ -2613,7 +2675,7 @@ export class TeamsMachine extends MachineCore {
2613
2675
  }
2614
2676
  if ((!locator && !['codex', 'claude-code'].includes(kind)) || (['codex', 'claude-code'].includes(kind) && !path)) return { delivered: false, reason: 'native-transcript-unresolved' };
2615
2677
  if (!queue) {
2616
- queue = new TerminalMailbox(this.terminalMailRoot, key, receipt => this.#recordTerminalReceipt(contextKey, receipt),
2678
+ queue = new TerminalMailbox(this.mailStore, key, receipt => this.#recordTerminalReceipt(contextKey, receipt),
2617
2679
  (locator, options) => this.#serveRequest('harness.v1.sessions.load', { locator, options: { ...options, include_subagents: false } }, 10000));
2618
2680
  this.terminalMailboxes.set(key, queue);
2619
2681
  }
@@ -2752,6 +2814,7 @@ export class TeamsMachine extends MachineCore {
2752
2814
  async #deliverPaneText(session, contextKey, text, options = {}) {
2753
2815
  const kind = this.#paneKind(contextKey, session);
2754
2816
  if (kind === 'codex' && codexUpdatePrompt(await this.host.capture(session.id, { lines: 120 }))) return { delivered: false, reason: 'codex-update-prompt: delivery refused; restart Codex with check_for_update_on_startup=false' };
2817
+ if (this.stopping) throw new Error('the daemon is stopping; no pane input was submitted');
2755
2818
  if(this.inputRepairs[contextKey])return {delivered:false,reason:'input-cleanup-pending',cleanup_required:true};
2756
2819
  if (['claude-code', 'codex', 'grok', 'gemini', 'goose', 'opencode', 'pi', 'hermes', 'openclaw'].includes(kind)) return this.#terminalMail(session, contextKey, kind, submission(text, false), options);
2757
2820
  if (this.inputInFlight.has(session.id)) return { delivered: false, reason: 'input-in-flight' };
@@ -2826,6 +2889,7 @@ export class TeamsMachine extends MachineCore {
2826
2889
  // still holds refuses the turn at once and the session waits again, for the next turn to try.
2827
2890
  const queued = [...this.terminalMailboxes.values()].some(queue => queue.session?.id === session.id && (queue.state.active || queue.state.pending.length));
2828
2891
  if (draftKind === 'claude-code' && !this.inputInFlight.has(session.id) && !queued && claudeUsageLimitWait(await this.host.capture(session.id, { lines: 60 }))) {
2892
+ if (this.stopping) throw new Error('the daemon is stopping; no pane input was submitted');
2829
2893
  await this.host.sendInput(session.id, '\u001b', { submitMode: 'single', deadlineMs: Date.now() + 5_000 });
2830
2894
  await new Promise(resolve => setTimeout(resolve, COMPOSER_RECHECK_MS));
2831
2895
  }
@@ -3248,6 +3312,7 @@ export class TeamsMachine extends MachineCore {
3248
3312
  }
3249
3313
  entry.admitted = fresh;
3250
3314
  }
3315
+ if (!this.mailReady || this.stopping) throw Object.assign(new Error(this.stopping ? 'this machine daemon is stopping' : 'this machine daemon is preparing its mail database; retry once migration completes'), { code: 503 });
3251
3316
  const { admitted, principal } = entry;
3252
3317
  const operator = admitted.class === 'operator';
3253
3318
  const need = (cap, target) => {
@@ -3421,7 +3486,7 @@ export class TeamsMachine extends MachineCore {
3421
3486
  case 'harness.v1.sessions.people': {
3422
3487
  if (!operator) throw Object.assign(new Error('session people are for operators'), { code: 403 });
3423
3488
  if (!Array.isArray(params.sessions) || params.sessions.length > 200) throw Object.assign(new Error('sessions: up to 200 { harness, path, id }'), { code: 400 });
3424
- return { people: await sessionPeople(params.sessions, { launchesDir: join(this.terminalMailRoot, 'native-launches'), journalPath: this.p.launches }) };
3489
+ return { people: await sessionPeople(params.sessions, { store: this.mailStore, journalPath: this.p.launches }) };
3425
3490
  }
3426
3491
  // A harness's standing instructions, as the person edits them (ChatGPT's custom instructions): the user tier
3427
3492
  // (instruction-doors.mjs: Claude Code's CLAUDE.md, Codex's AGENTS.md) or a project's own file in its directory.
@@ -3618,6 +3683,12 @@ export class TeamsMachine extends MachineCore {
3618
3683
  this.launches.record({ key, pane: contextKey, kind: params.kind ?? null, by, ...resumed, ...executable,
3619
3684
  status: createdExit ? createdExit.failed ? 'exited' : 'finished' : 'ready',
3620
3685
  ...(createdExit?.failed === false ? { completion: createdExit } : {}), ...(agent ? { agent } : {}) });
3686
+ if (nativeInput?.binding && !createdExit) {
3687
+ const bindingKey = basename(nativeInput.binding);
3688
+ const seed = this.mailStore.record('native_launches', bindingKey);
3689
+ if (seed) this.mailStore.putRecord('native_launches', bindingKey, { ...seed,
3690
+ launchReceipt: { pane: contextKey, terminal: session.id, rootPid: session.rootPid } });
3691
+ }
3621
3692
  if (params.kind === 'codex' && (params.arguments ?? []).includes('--dangerously-bypass-hook-trust')) this.#settleCodexStartup(key, contextKey, session, act);
3622
3693
  act('launch', `pane:${contextKey}`, 'ok', params.kind ?? params.program);
3623
3694
  if (typeof params.name === 'string' && params.name.trim()) {
@@ -4171,6 +4242,28 @@ export class TeamsMachine extends MachineCore {
4171
4242
  return { address, messages };
4172
4243
  }
4173
4244
  // the harnesses' mail hooks (step 1b): the hook's input in, what the hook prints out (`print`, null for nothing)
4245
+ case 'harness.v1.mail.native_launch': {
4246
+ if (entry.principal !== 'local' || entry.appDoor) throw Object.assign(new Error('a native launch binding is read by this machine\'s own processes'), { code: 403 });
4247
+ const key = nativeLaunchKey(params.binding, this.mailStore.root);
4248
+ let seed = this.mailStore.record('native_launches', key);
4249
+ if (!seed) throw Object.assign(new Error('this native launch binding is absent; no native program was started'), { code: 404 });
4250
+ if (params.socket_path == null) return { seed };
4251
+ // The Unix wrapper publishes only its own endpoint. This is a process
4252
+ // reading, not proof of a Codex thread or permission to act as one.
4253
+ const pid = Number(params.claim?.pid);
4254
+ if (process.platform === 'win32' || !entry.socket || !await peerIs(entry.socket, pid)) throw Object.assign(new Error('the native endpoint publisher could not be bound to this connection'), { code: 403 });
4255
+ const pane = await this.#paneByContext(seed.pane);
4256
+ seed = this.mailStore.record('native_launches', key);
4257
+ if (!seed) throw Object.assign(new Error('this native launch binding ended before endpoint publication'), { code: 404 });
4258
+ const receipt = seed.launchReceipt;
4259
+ if (!receipt && this.launches.find(seed.key)?.status === 'issued') throw Object.assign(new Error('this launch\'s creation receipt is not published yet'), { code: 425 });
4260
+ if (!pane || !receipt || receipt.pane !== seed.pane || receipt.terminal !== pane.id || receipt.rootPid !== pid || pane.rootPid !== pid) throw Object.assign(new Error('this native endpoint publisher is not the live launch root'), { code: 403 });
4261
+ if (seed.kind !== 'codex' || typeof params.socket_path !== 'string' || !isAbsolute(params.socket_path) || !statSync(params.socket_path).isSocket()) throw Object.assign(new Error('the native endpoint must be this Codex wrapper\'s Unix socket'), { code: 400 });
4262
+ if (seed.ownerPid != null && (seed.ownerPid !== pid || seed.socketPath !== params.socket_path)) throw Object.assign(new Error('this native launch already recorded another endpoint'), { code: 409 });
4263
+ const recorded = { ...seed, socketPath: params.socket_path, ownerPid: pid };
4264
+ this.mailStore.putRecord('native_launches', key, recorded);
4265
+ return { seed: recorded };
4266
+ }
4174
4267
  case 'harness.v1.mail.hook': case 'harness.v1.mail.relay_gate': case 'harness.v1.mail.relay_receipt': case 'harness.v1.mail.relay_inbound': case 'harness.v1.mail.question_hook': {
4175
4268
  if (entry.principal !== 'local' || entry.appDoor) throw Object.assign(new Error('a mail hook is answered here for this machine\'s own processes'), { code: 403 });
4176
4269
  const input = params.input && typeof params.input === 'object' ? params.input : {};
@@ -4275,6 +4368,10 @@ export class TeamsMachine extends MachineCore {
4275
4368
  if (entry.principal !== 'local' || entry.appDoor) throw Object.assign(new Error('mail is read here by this machine\'s own processes'), { code: 403 });
4276
4369
  return { code: 0, rows: method === 'harness.v1.mail.waiting' ? this.carrier.waiting() : this.carrier.expired() };
4277
4370
  }
4371
+ case 'harness.v1.mail.statistics': {
4372
+ if (entry.principal !== 'local' || entry.appDoor) throw Object.assign(new Error('mail storage diagnostics are read only by this machine\'s own processes'), { code: 403 });
4373
+ return this.mailStore.statistics();
4374
+ }
4278
4375
  // Who a local client is, from its claim (live-sessions.mjs callerOf): what a command acting on another machine sends
4279
4376
  // there as its caller, resolved here, where the map that holds it is. Local only; never the app door's.
4280
4377
  // The pane this daemon launched that a local process runs in, and who started that pane: what a process with no