@volter/supercode-teams 0.3.151 → 0.3.153

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md 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
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 });
@@ -842,11 +856,12 @@ export class TeamsMachine extends MachineCore {
842
856
  const turnsWaiting = new Map(); // address → timer
843
857
  const typeWaiting = async (address) => {
844
858
  clearTimeout(turnsWaiting.get(address)); turnsWaiting.delete(address);
859
+ if (this.stopping) return;
845
860
  const session = live.sessions().find((row) => row.address === address);
846
861
  const pane = session ? this.mail.paneOf(session) : null;
847
862
  if (!pane || !this.mailStore.userTurns(address).length) return;
848
863
  try { await this.mailSend.typeUserTurns(address, pane); } catch (error) { this.emit('mail-log', `the user's turns for ${address} wait: ${error.message}`); }
849
- 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); }
850
865
  };
851
866
  this.mailStore.on('changed', (address) => { if (address && !turnsWaiting.has(address)) void typeWaiting(address); });
852
867
  // the turns already waiting when this listener came (the store read every mailbox at its start, before it), and every
@@ -854,6 +869,7 @@ export class TeamsMachine extends MachineCore {
854
869
  const everyMailbox = () => { for (const address of this.mailStore.addresses()) if (!turnsWaiting.has(address)) void typeWaiting(address); };
855
870
  everyMailbox();
856
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(); };
857
873
  // an agent's verbs (3b-2): its threads, a delegation, its record
858
874
  this.agentVerbs = new AgentVerbs({ machine, store: this.mailStore, agents: this.agents, live, supercodeBin: this.supercodeBin, env: this.env,
859
875
  paneOf: (session) => this.mail.paneOf(session), mailRoot,
@@ -868,17 +884,19 @@ export class TeamsMachine extends MachineCore {
868
884
  live.on('changed', () => {
869
885
  // while the Codex sessions are not known (a start, a serve restart) a Codex session missing has not ended: the last
870
886
  // known view stands until they are known again
871
- if (!live.codexKnown) return;
887
+ if (this.stopping || !live.codexKnown) return;
872
888
  const now = new Map(live.sessions().map((row) => [row.address, row.name]));
873
889
  // a Codex session that leaves the map has not provably ended (idle over a day, it is no longer followed): kept
874
890
  for (const [address, name] of wasLive) if (!now.has(address) && address.split(':')[2] !== 'codex') void this.relays.retire(address, name).catch(() => {});
875
891
  wasLive = now;
876
892
  });
877
893
  void live.whenCodexKnown(120_000).then((known) => {
894
+ if (this.stopping) return;
878
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; }
879
896
  return this.relays.sweepAtStart((address) => live.sessions().some((row) => row.address === address));
880
897
  }).catch((error) => this.emit('mail-log', `relays: the start sweep failed: ${error.message}`));
881
898
  live.on('changed', () => {
899
+ if (this.stopping) return;
882
900
  if (naming) return;
883
901
  naming = setTimeout(() => { naming = null; try { this.mailSend.rememberNames(live.sessions()); } catch { /* kept at the next change */ } }, 1000);
884
902
  naming.unref?.();
@@ -1132,6 +1150,8 @@ export class TeamsMachine extends MachineCore {
1132
1150
  }
1133
1151
 
1134
1152
  async beforeClose() {
1153
+ this.mailReady = false;
1154
+ this.#stopMailWorkers();
1135
1155
  this.#stopHealth();
1136
1156
  clearInterval(this.terminalMailTimer); await this.terminalMailWork;
1137
1157
  clearInterval(this.processTimer); await this.processWork;
@@ -1164,14 +1184,10 @@ export class TeamsMachine extends MachineCore {
1164
1184
  for (const child of this.voiceProcesses.values()) child.kill('SIGTERM');
1165
1185
  this.voiceProcesses.clear();
1166
1186
  this.jobs?.stopAll();
1167
- void this.relayEndpoint?.then((endpoint) => endpoint?.close()).catch(() => {});
1168
- this.carrier?.stop();
1187
+ await this.relayEndpoint?.then((endpoint) => endpoint?.close()).catch(() => {});
1169
1188
  this.stopLiveFollow?.();
1189
+ this.#stopMailWorkers();
1170
1190
  this.mailStore?.stop();
1171
- clearInterval(this.hookSpoolTimer);
1172
- this.idleNotices?.stop();
1173
- this.agentUpkeep?.stop();
1174
- this.nativeQuestions?.stop();
1175
1191
  clearInterval(this.turnsSweep);
1176
1192
  this.stoppingServe = true;
1177
1193
  clearTimeout(this.serveRestart);
@@ -1181,6 +1197,22 @@ export class TeamsMachine extends MachineCore {
1181
1197
  this.host.dispose();
1182
1198
  }
1183
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
+
1184
1216
  connectionClosed(index) {
1185
1217
  for (const [key, owners] of this.routes) { owners.delete(index); if (owners.size === 0) this.routes.delete(key); }
1186
1218
  }
@@ -1540,6 +1572,9 @@ export class TeamsMachine extends MachineCore {
1540
1572
  }
1541
1573
 
1542
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');
1543
1578
  // One message into a session on this machine, delivered by this daemon's own send (mail/send.mjs) through the same
1544
1579
  // doors a local send uses. The receiver is the
1545
1580
  // target: `mail:*` reaches every session here, `mail:<address>` one of them; a
@@ -2032,9 +2067,8 @@ export class TeamsMachine extends MachineCore {
2032
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 });
2033
2068
  return launchCommand(params.program, args);
2034
2069
  }
2035
- const mailHome = this.env.SUPERCODE_HOME || join(this.env.XDG_CONFIG_HOME || join(homedir(), '.config'), 'supercode');
2070
+ const mailHome = dirname(this.mailStore.root);
2036
2071
  const dir = join(mailHome, 'mail', 'native-launches');
2037
- mkdirSync(dir, { recursive: true, mode: 0o700 });
2038
2072
  // A reused pane key still gets a new receipt baseline. A late hook from
2039
2073
  // its previous process cannot publish evidence for this launch.
2040
2074
  const binding = join(dir, `${pane}-${randomBytes(9).toString('base64url')}.json`);
@@ -2045,14 +2079,14 @@ export class TeamsMachine extends MachineCore {
2045
2079
  !args.some((arg) => arg === '--remote' || arg.startsWith('--remote=')) ? prompt : null;
2046
2080
  const claudeInput = claude && prompt ? { kind: params.kind, text: prompt, startedAt: Date.now(),
2047
2081
  deadlineMs: Date.now() + INITIAL_INPUT_READY_DEADLINE_MS + 10_000, typed: true } : null;
2048
- 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 });
2049
2083
  // The first input is durable before createSession can start the program.
2050
2084
  // Its body stays in the private binding, never in the launch journal.
2051
2085
  if (claudeInput) {
2052
2086
  this.launches.record({ ...this.launches.find(params.key), nativeInputBinding: binding });
2053
2087
  nativeInput({ ...claudeInput, binding });
2054
2088
  }
2055
- 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() });
2056
2090
  if (params.kind === 'claude-code') {
2057
2091
  // Claude's exec-form hook passes each argument unchanged on all platforms.
2058
2092
  // A Windows .cmd shim is resolved to its Node executable and front door.
@@ -2079,7 +2113,7 @@ export class TeamsMachine extends MachineCore {
2079
2113
  // door. Give this pane its own native server, through Codex's --remote
2080
2114
  // Unix endpoint; never attach to another running session by inference.
2081
2115
  if (args.some((arg) => arg === '--remote' || arg.startsWith('--remote='))) return launchCommand(params.program, args);
2082
- 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]);
2083
2117
  }
2084
2118
 
2085
2119
  /**
@@ -2258,10 +2292,11 @@ export class TeamsMachine extends MachineCore {
2258
2292
  * before this was kept carries the same files. A Codex session has no such hook here and no origin: its resumed pane
2259
2293
  * stays its resumer's. */
2260
2294
  #sessionOrigin(session) {
2261
- let lines, names;
2262
- const dir = join(this.env.SUPERCODE_HOME || join(this.env.XDG_CONFIG_HOME || join(homedir(), '.config'), 'supercode'), 'mail', 'native-launches');
2263
- try { lines = readFileSync(this.launches.path, 'utf8').split('\n'); names = readdirSync(dir); } catch { return null; }
2264
- 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;
2265
2300
  const started = (entry) => names.some((name) => {
2266
2301
  if (!name.startsWith(`${entry.pane}-`) || !/^[^.]+\.json$/.test(name)) return false;
2267
2302
  const seed = read(name);
@@ -2430,6 +2465,19 @@ export class TeamsMachine extends MachineCore {
2430
2465
  const launch = this.launches.find(key);
2431
2466
  if (launch?.pane !== contextKey) return;
2432
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
+ }
2433
2481
  };
2434
2482
  const abort = new AbortController();
2435
2483
  const task = (async () => {
@@ -2463,7 +2511,7 @@ export class TeamsMachine extends MachineCore {
2463
2511
  }
2464
2512
  }
2465
2513
  if (nativeInput) {
2466
- const result = await confirmNativeInitialInput(nativeInput, { signal: abort.signal,
2514
+ const result = await confirmNativeInitialInput(nativeInput, { signal: abort.signal, store: this.mailStore,
2467
2515
  deadlineMs: Date.now() + INITIAL_INPUT_READY_DEADLINE_MS, live: async () => Boolean(await this.#paneByContext(contextKey)) });
2468
2516
  recordInput({ key, pane: contextKey, kind, by, ...resumed, ...executable, inputDelivered: result.delivered,
2469
2517
  inputReadyBy: nativeReadyBy, ...(result.delivered ? { inputReceipt: result.receipt } : { inputError: result.reason }),
@@ -2500,7 +2548,7 @@ export class TeamsMachine extends MachineCore {
2500
2548
  let identity;
2501
2549
  while (!signal.aborted && Date.now() < deadline) {
2502
2550
  if ((await this.#paneByContext(contextKey))?.id !== session.id) return { delivered: false, reason: 'pane-gone' };
2503
- const start = readJson(input.binding.replace(/\.json$/, '.session.json'));
2551
+ const start = this.mailStore.record('native_launches', basename(input.binding).replace(/\.json$/, '.session.json'));
2504
2552
  const id = /([0-9a-f]{8}-[0-9a-f-]{27})\.jsonl$/i.exec(String(start?.path ?? ''))?.[1];
2505
2553
  if (id && Number.isSafeInteger(start.offset) && start.offset >= 0) {
2506
2554
  identity = { kind: 'claude-code', path: start.path, sessionId: id, offset: start.offset };
@@ -2528,7 +2576,7 @@ export class TeamsMachine extends MachineCore {
2528
2576
  if ((onlyKey && launch.key !== onlyKey) || launch.kind !== 'claude-code' || !launch.nativeInputBinding ||
2529
2577
  launch.inputDelivered || launch.inputRefused || this.pendingInitialInputs.has(launch.key) ||
2530
2578
  !['issued', 'ready'].includes(launch.status)) continue;
2531
- const seed = readJson(launch.nativeInputBinding);
2579
+ const seed = this.mailStore.record('native_launches', basename(launch.nativeInputBinding));
2532
2580
  if (seed?.key !== launch.key || seed.pane !== launch.pane || seed.by !== launch.by || !seed.claudeInput?.text) continue;
2533
2581
  const session = panes.find(row => row.contextKey === launch.pane && !row.dead);
2534
2582
  if (!session) {
@@ -2544,9 +2592,10 @@ export class TeamsMachine extends MachineCore {
2544
2592
  }
2545
2593
 
2546
2594
  async #drainTerminalMail() {
2595
+ if (!this.mailReady || this.stopping) return;
2547
2596
  // One pane listing per tick, shared by every mailbox: a listing per mailbox each second was most of this
2548
2597
  // daemon's tmux spawns (8 retained mailboxes, about 70 tmux processes a second, t_23e47b73).
2549
- const retained = retainedTerminalMail(this.terminalMailRoot);
2598
+ const retained = retainedTerminalMail(this.mailStore);
2550
2599
  if (this.initialInputsRecovered && !retained.length && !this.terminalMailboxes.size) return;
2551
2600
  const panes = await this.host.listSessions();
2552
2601
  if (!this.initialInputsRecovered) {
@@ -2558,7 +2607,7 @@ export class TeamsMachine extends MachineCore {
2558
2607
  if (this.terminalMailboxes.has(saved.recipient) || !saved.binding) continue;
2559
2608
  const session = paneByContext(saved.binding.context);
2560
2609
  if (!session || session.rootPid !== saved.binding.pid) continue;
2561
- 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),
2562
2611
  (locator, options) => this.#serveRequest('harness.v1.sessions.load', { locator, options: { ...options, include_subagents: false } }, 10000));
2563
2612
  queue.identity = saved.binding.identity; queue.host = this.host; queue.session = session;
2564
2613
  // A tmux host remints its client IDs after restart. Context and the live
@@ -2583,10 +2632,10 @@ export class TeamsMachine extends MachineCore {
2583
2632
  if (!receipt.delivered && !receipt.refused) return;
2584
2633
  for (const id of receipt.message_ids ?? []) {
2585
2634
  if (!/^first-[A-Za-z0-9_-]+$/.test(id)) continue;
2586
- const path = join(this.terminalMailRoot, 'native-launches', `${id.slice(6)}.json`);
2587
- 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);
2588
2637
  if (!launch || seed.pane !== contextKey || launch.pane !== contextKey) continue;
2589
- 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];
2590
2639
  const { t: _previousAt, ...facts } = launch;
2591
2640
  this.launches.record({ ...facts, inputDelivered: receipt.delivered, inputPending: false, inputRefused: receipt.refused === true,
2592
2641
  ...(receipt.delivered ? { inputReceipt: receipt.receipt, inputError: null, ...(native ? { native_session: native } : {}) } : { inputError: receipt.reason }) });
@@ -2594,6 +2643,7 @@ export class TeamsMachine extends MachineCore {
2594
2643
  }
2595
2644
 
2596
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');
2597
2647
  // Mail a queue of this terminal already holds (the watch asking again after the same message ids) is answered from
2598
2648
  // that queue, which types it once the composer frees: nothing is asked of the harness again for it.
2599
2649
  const ids = options.messageIds ?? [];
@@ -2625,7 +2675,7 @@ export class TeamsMachine extends MachineCore {
2625
2675
  }
2626
2676
  if ((!locator && !['codex', 'claude-code'].includes(kind)) || (['codex', 'claude-code'].includes(kind) && !path)) return { delivered: false, reason: 'native-transcript-unresolved' };
2627
2677
  if (!queue) {
2628
- queue = new TerminalMailbox(this.terminalMailRoot, key, receipt => this.#recordTerminalReceipt(contextKey, receipt),
2678
+ queue = new TerminalMailbox(this.mailStore, key, receipt => this.#recordTerminalReceipt(contextKey, receipt),
2629
2679
  (locator, options) => this.#serveRequest('harness.v1.sessions.load', { locator, options: { ...options, include_subagents: false } }, 10000));
2630
2680
  this.terminalMailboxes.set(key, queue);
2631
2681
  }
@@ -2764,6 +2814,7 @@ export class TeamsMachine extends MachineCore {
2764
2814
  async #deliverPaneText(session, contextKey, text, options = {}) {
2765
2815
  const kind = this.#paneKind(contextKey, session);
2766
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');
2767
2818
  if(this.inputRepairs[contextKey])return {delivered:false,reason:'input-cleanup-pending',cleanup_required:true};
2768
2819
  if (['claude-code', 'codex', 'grok', 'gemini', 'goose', 'opencode', 'pi', 'hermes', 'openclaw'].includes(kind)) return this.#terminalMail(session, contextKey, kind, submission(text, false), options);
2769
2820
  if (this.inputInFlight.has(session.id)) return { delivered: false, reason: 'input-in-flight' };
@@ -2838,6 +2889,7 @@ export class TeamsMachine extends MachineCore {
2838
2889
  // still holds refuses the turn at once and the session waits again, for the next turn to try.
2839
2890
  const queued = [...this.terminalMailboxes.values()].some(queue => queue.session?.id === session.id && (queue.state.active || queue.state.pending.length));
2840
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');
2841
2893
  await this.host.sendInput(session.id, '\u001b', { submitMode: 'single', deadlineMs: Date.now() + 5_000 });
2842
2894
  await new Promise(resolve => setTimeout(resolve, COMPOSER_RECHECK_MS));
2843
2895
  }
@@ -3260,6 +3312,7 @@ export class TeamsMachine extends MachineCore {
3260
3312
  }
3261
3313
  entry.admitted = fresh;
3262
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 });
3263
3316
  const { admitted, principal } = entry;
3264
3317
  const operator = admitted.class === 'operator';
3265
3318
  const need = (cap, target) => {
@@ -3433,7 +3486,7 @@ export class TeamsMachine extends MachineCore {
3433
3486
  case 'harness.v1.sessions.people': {
3434
3487
  if (!operator) throw Object.assign(new Error('session people are for operators'), { code: 403 });
3435
3488
  if (!Array.isArray(params.sessions) || params.sessions.length > 200) throw Object.assign(new Error('sessions: up to 200 { harness, path, id }'), { code: 400 });
3436
- 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 }) };
3437
3490
  }
3438
3491
  // A harness's standing instructions, as the person edits them (ChatGPT's custom instructions): the user tier
3439
3492
  // (instruction-doors.mjs: Claude Code's CLAUDE.md, Codex's AGENTS.md) or a project's own file in its directory.
@@ -3630,6 +3683,12 @@ export class TeamsMachine extends MachineCore {
3630
3683
  this.launches.record({ key, pane: contextKey, kind: params.kind ?? null, by, ...resumed, ...executable,
3631
3684
  status: createdExit ? createdExit.failed ? 'exited' : 'finished' : 'ready',
3632
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
+ }
3633
3692
  if (params.kind === 'codex' && (params.arguments ?? []).includes('--dangerously-bypass-hook-trust')) this.#settleCodexStartup(key, contextKey, session, act);
3634
3693
  act('launch', `pane:${contextKey}`, 'ok', params.kind ?? params.program);
3635
3694
  if (typeof params.name === 'string' && params.name.trim()) {
@@ -4183,6 +4242,28 @@ export class TeamsMachine extends MachineCore {
4183
4242
  return { address, messages };
4184
4243
  }
4185
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
+ }
4186
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': {
4187
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 });
4188
4269
  const input = params.input && typeof params.input === 'object' ? params.input : {};
@@ -4287,6 +4368,10 @@ export class TeamsMachine extends MachineCore {
4287
4368
  if (entry.principal !== 'local' || entry.appDoor) throw Object.assign(new Error('mail is read here by this machine\'s own processes'), { code: 403 });
4288
4369
  return { code: 0, rows: method === 'harness.v1.mail.waiting' ? this.carrier.waiting() : this.carrier.expired() };
4289
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
+ }
4290
4375
  // Who a local client is, from its claim (live-sessions.mjs callerOf): what a command acting on another machine sends
4291
4376
  // there as its caller, resolved here, where the map that holds it is. Local only; never the app door's.
4292
4377
  // The pane this daemon launched that a local process runs in, and who started that pane: what a process with no