@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 +33 -1
- package/bin/teams.mjs +10 -2
- package/machine.mjs +143 -46
- package/mail/agents.mjs +40 -119
- package/mail/carrier.mjs +72 -111
- package/mail/continuation.mjs +101 -0
- package/mail/envelope.mjs +8 -10
- package/mail/hooks.mjs +43 -27
- package/mail/idle.mjs +14 -5
- package/mail/import-maildir.mjs +350 -0
- package/mail/questions.mjs +38 -46
- package/mail/reads.mjs +15 -26
- package/mail/relay-endpoint.mjs +14 -22
- package/mail/relay.mjs +100 -65
- package/mail/retire-maildir.mjs +122 -0
- package/mail/send.mjs +141 -104
- package/mail/sqlite-store.mjs +476 -0
- package/mail/sqlite.mjs +353 -0
- package/mail/transcript.mjs +3 -3
- package/mail/upkeep.mjs +46 -26
- package/mail/verbs.mjs +14 -36
- package/native-codex-pane.mjs +6 -6
- package/native-initial-input.mjs +6 -6
- package/native-launch-binding.mjs +35 -0
- package/package.json +2 -2
- package/session-people.mjs +7 -8
- package/terminal-mail.mjs +8 -19
- package/mail/store.mjs +0 -531
package/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
|
|
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 {
|
|
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
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
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
|
-
|
|
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')); },
|
|
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({
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
const
|
|
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 =
|
|
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 =
|
|
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.
|
|
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.
|
|
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
|
|
2575
|
-
const seed =
|
|
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(
|
|
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.
|
|
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, {
|
|
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
|