@volter/supercode-teams 0.3.151 → 0.3.152
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -1
- package/bin/teams.mjs +10 -2
- package/machine.mjs +128 -43
- package/mail/agents.mjs +40 -119
- package/mail/carrier.mjs +72 -111
- package/mail/envelope.mjs +8 -10
- package/mail/hooks.mjs +43 -27
- package/mail/idle.mjs +14 -5
- package/mail/import-maildir.mjs +350 -0
- package/mail/questions.mjs +38 -46
- package/mail/reads.mjs +15 -26
- package/mail/relay-endpoint.mjs +14 -22
- package/mail/relay.mjs +100 -65
- package/mail/retire-maildir.mjs +122 -0
- package/mail/send.mjs +65 -81
- package/mail/sqlite-store.mjs +476 -0
- package/mail/sqlite.mjs +353 -0
- package/mail/transcript.mjs +3 -3
- package/mail/upkeep.mjs +46 -26
- package/mail/verbs.mjs +14 -36
- package/native-codex-pane.mjs +6 -6
- package/native-initial-input.mjs +6 -6
- package/native-launch-binding.mjs +35 -0
- package/package.json +2 -2
- package/session-people.mjs +7 -8
- package/terminal-mail.mjs +8 -19
- package/mail/store.mjs +0 -531
package/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
|
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 });
|
|
@@ -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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
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;
|
|
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 =
|
|
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 =
|
|
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.
|
|
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.
|
|
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
|
|
2587
|
-
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);
|
|
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(
|
|
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.
|
|
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, {
|
|
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
|