@volter/supercode-teams 0.3.120 → 0.3.122
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/bin/teams.mjs +6 -1
- package/bin/workspace.mjs +17 -2
- package/live-sessions.mjs +36 -21
- package/machine.mjs +92 -10
- package/mail/hooks.mjs +29 -3
- package/mail/relay.mjs +5 -2
- package/mail/send.mjs +18 -0
- package/package.json +1 -1
package/bin/teams.mjs
CHANGED
|
@@ -187,7 +187,12 @@ async function main() {
|
|
|
187
187
|
process.on('SIGTERM', stop);
|
|
188
188
|
return;
|
|
189
189
|
}
|
|
190
|
-
if (sub === 'status') {
|
|
190
|
+
if (sub === 'status') {
|
|
191
|
+
// and every supercode process here still running a release an install replaced (t_8e109edc)
|
|
192
|
+
let stale_releases = null;
|
|
193
|
+
try { stale_releases = await (await import('@volter/supercode-health/drift')).staleSupercode(); } catch { /* not read */ }
|
|
194
|
+
out({ ...readJson(p.machine, { running: false }), stale_releases }); return;
|
|
195
|
+
}
|
|
191
196
|
// `machine restart [--machine M] [--reason text]`: the daemon (this one, or M's through the server) restarts under its service.
|
|
192
197
|
if (sub === 'restart') return withNode(async (rpc) => out(await rpc.call('machine.restart', { reason: flags.reason ? String(flags.reason) : null })));
|
|
193
198
|
die('machine start|status|restart');
|
package/bin/workspace.mjs
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
import { StringDecoder } from 'node:string_decoder';
|
|
3
3
|
import { hostname } from 'node:os';
|
|
4
4
|
import { agentSessionHint } from './workspace/agent-session.mjs';
|
|
5
|
-
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
6
6
|
import { existsSync, readFileSync, realpathSync, rmSync } from 'node:fs';
|
|
7
|
-
import { basename, join, resolve } from 'node:path';
|
|
7
|
+
import { basename, dirname, join, resolve } from 'node:path';
|
|
8
8
|
import { execFileSync, spawn } from 'node:child_process';
|
|
9
9
|
import { createHash } from 'node:crypto';
|
|
10
10
|
import { SupercodeHarnessClient } from '@volter/supercode-harness-sdk';
|
|
@@ -1119,6 +1119,7 @@ async function connectorCommand(verb, { flags, rest, output, client, context, te
|
|
|
1119
1119
|
if (!flags.json) {
|
|
1120
1120
|
process.stdout.write(`Connected ${saved.machine.name} to team ${context.team_name ?? context.team_id}; it stays connected (a ${service.kind ?? 'background'} service).\n`);
|
|
1121
1121
|
for (const line of service.message_tools ?? []) process.stdout.write(` ${line}\n`);
|
|
1122
|
+
for (const line of await releaseSkills()) process.stdout.write(` ${line}\n`);
|
|
1122
1123
|
return;
|
|
1123
1124
|
}
|
|
1124
1125
|
return output({ version: 1, context: scope(context), data: { machine: saved.machine, source: saved.source, service, ...(addedRule ? { rule: addedRule } : {}) } });
|
|
@@ -1486,3 +1487,17 @@ function stampStderr() {
|
|
|
1486
1487
|
};
|
|
1487
1488
|
process.stderr.__stamped = true;
|
|
1488
1489
|
}
|
|
1490
|
+
|
|
1491
|
+
/**
|
|
1492
|
+
* The skills of the release this Teams package belongs to (`@volter/supercode`'s lib/skills.mjs, the package that
|
|
1493
|
+
* holds it), put into every harness here: a machine connected to a team has agents that know supercode
|
|
1494
|
+
* (t_266912ff). A Teams package run from a checkout or installed alone has no release beside it, and installs none.
|
|
1495
|
+
*/
|
|
1496
|
+
async function releaseSkills() {
|
|
1497
|
+
const release = join(dirname(fileURLToPath(import.meta.url)), '..', '..', '..', '..');
|
|
1498
|
+
try {
|
|
1499
|
+
if (JSON.parse(readFileSync(join(release, 'package.json'), 'utf8')).name !== '@volter/supercode') return [];
|
|
1500
|
+
const { installSkills } = await import(pathToFileURL(join(release, 'lib', 'skills.mjs')).href);
|
|
1501
|
+
return installSkills();
|
|
1502
|
+
} catch { return []; }
|
|
1503
|
+
}
|
package/live-sessions.mjs
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
// - Messaging tools: the marker a `message mcp` server writes for its session process, with the server alive.
|
|
15
15
|
import { EventEmitter } from 'node:events';
|
|
16
16
|
import { execFile } from 'node:child_process';
|
|
17
|
-
import { mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, unlinkSync, watch, writeFileSync } from 'node:fs';
|
|
17
|
+
import { mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, unlinkSync, watch, writeFileSync } from 'node:fs';
|
|
18
18
|
import { homedir } from 'node:os';
|
|
19
19
|
import { dirname, join } from 'node:path';
|
|
20
20
|
import { FrontendClient } from '@volter/supercode-frontend';
|
|
@@ -128,7 +128,10 @@ export class LiveSessions extends EventEmitter {
|
|
|
128
128
|
this.baseClaudeDirs = [...new Set([join(homedir(), '.claude', 'sessions'), ...(env.CLAUDE_CONFIG_DIR ? [join(env.CLAUDE_CONFIG_DIR, 'sessions')] : [])])];
|
|
129
129
|
this.receiptsDir = join(supercodeHome, 'live-runtimes');
|
|
130
130
|
this.homesFile = join(supercodeHome, 'live-claude-homes.json');
|
|
131
|
-
|
|
131
|
+
// which session has the messaging tools: told here by each `message mcp` server as it starts (mail step 1c), kept in
|
|
132
|
+
// this daemon's own state, outside the mail folder; the markers servers from before it wrote are taken in once
|
|
133
|
+
this.toolsFile = join(supercodeHome, 'message-tools.json');
|
|
134
|
+
this.legacyMarkersDir = join(supercodeHome, 'mail', 'message-mcp');
|
|
132
135
|
this.codexHooks = join(env.CODEX_HOME || join(homedir(), '.codex'), 'hooks.json');
|
|
133
136
|
this.boardOwner = boardOwner; this.log = log; this.startedAt = Date.now();
|
|
134
137
|
this.claude = new Map(); // pid → { pid, dir, sessionId, name, status, cwd, tmux, procStart, history: [ids, oldest first] }
|
|
@@ -171,7 +174,7 @@ export class LiveSessions extends EventEmitter {
|
|
|
171
174
|
// Grok's registry, followed where Grok is installed (its home is never made here; a symlinked home is followed at its
|
|
172
175
|
// target, where its file events are)
|
|
173
176
|
this.#followGrok();
|
|
174
|
-
this.#
|
|
177
|
+
this.#loadTools();
|
|
175
178
|
this.#follow('codex_hooks', dirname(this.codexHooks), (name) => { if (!name || name === 'hooks.json') this.#readUserHook(); }, () => this.#readUserHook());
|
|
176
179
|
return this;
|
|
177
180
|
}
|
|
@@ -258,7 +261,7 @@ export class LiveSessions extends EventEmitter {
|
|
|
258
261
|
this.claude.delete(pid);
|
|
259
262
|
// its messaging tools' marker goes with it when it has ended (step 4a: by its end, never a minute's sweep); an entry
|
|
260
263
|
// dropped for another reason (its home no longer declared, a parked process) keeps its marker for its return
|
|
261
|
-
if (this.markers.has(pid) && !alive(pid)) { this.markers.delete(pid);
|
|
264
|
+
if (this.markers.has(pid) && !alive(pid)) { this.markers.delete(pid); this.#saveTools(); }
|
|
262
265
|
}
|
|
263
266
|
|
|
264
267
|
/** Every held process asked whether it is alive (signal 0): a dead one's entry goes. */
|
|
@@ -476,25 +479,37 @@ export class LiveSessions extends EventEmitter {
|
|
|
476
479
|
if (JSON.stringify([...this.grok].map(([id, entry]) => [id, entry.pane])) !== before) this.#changed();
|
|
477
480
|
}
|
|
478
481
|
|
|
479
|
-
/**
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
this
|
|
490
|
-
|
|
491
|
-
|
|
482
|
+
/** The messaging tools' markers kept here, and those servers from before wrote into the mail folder, taken in once
|
|
483
|
+
* (that folder removed): each checked as a new one is. */
|
|
484
|
+
#loadTools() {
|
|
485
|
+
let kept = {};
|
|
486
|
+
try { kept = JSON.parse(readFileSync(this.toolsFile, 'utf8')) ?? {}; } catch { /* none yet */ }
|
|
487
|
+
try {
|
|
488
|
+
for (const name of readdirSync(this.legacyMarkersDir)) { const server = Number(readFileSync(join(this.legacyMarkersDir, name), 'utf8').trim()); if (Number.isInteger(Number(name))) kept[name] = server; }
|
|
489
|
+
rmSync(this.legacyMarkersDir, { recursive: true, force: true });
|
|
490
|
+
} catch { /* none written before */ }
|
|
491
|
+
for (const [session, server] of Object.entries(kept)) this.registerTools(Number(session), Number(server), { save: false });
|
|
492
|
+
this.#saveTools();
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
#saveTools() {
|
|
496
|
+
try { writeFileSync(`${this.toolsFile}.tmp`, JSON.stringify(Object.fromEntries(this.markers))); renameSync(`${this.toolsFile}.tmp`, this.toolsFile); }
|
|
497
|
+
catch (error) { this.log(`live sessions: the messaging tools' markers were not kept: ${error.message}`); }
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/** A `message mcp` server says the session `session` has the messaging tools while it, `server`, runs: taken when the
|
|
501
|
+
* server lives and is the session's own child (a harness starts its MCP servers so; a server pid used again by
|
|
502
|
+
* another process names no tools, as mail_route.rs `has_message_tools` read it). Answers whether it was taken. */
|
|
503
|
+
registerTools(session, server, { save = true } = {}) {
|
|
504
|
+
if (!Number.isInteger(session) || session <= 0 || !Number.isInteger(server) || server <= 0 || !alive(server)) return false;
|
|
505
|
+
this.markers.set(session, server);
|
|
506
|
+
if (save) this.#saveTools();
|
|
507
|
+
this.#changed();
|
|
492
508
|
void parentOf(server).then((parent) => {
|
|
493
|
-
if (parent == null || parent ===
|
|
494
|
-
this.markers.delete(
|
|
495
|
-
try { unlinkSync(join(this.markersDir, name)); } catch { /* gone */ }
|
|
496
|
-
this.#changed();
|
|
509
|
+
if (parent == null || parent === session || this.markers.get(session) !== server) return;
|
|
510
|
+
this.markers.delete(session); this.#saveTools(); this.#changed();
|
|
497
511
|
});
|
|
512
|
+
return true;
|
|
498
513
|
}
|
|
499
514
|
|
|
500
515
|
|
package/machine.mjs
CHANGED
|
@@ -771,7 +771,7 @@ export class TeamsMachine extends MachineCore {
|
|
|
771
771
|
discover: (params) => this.#serveRequest('harness.v1.sessions.discover', params, 10_000),
|
|
772
772
|
recordedConfig: (locator) => this.#serveRequest('harness.v1.sessions.recorded_config', { locator }, 15_000) });
|
|
773
773
|
// a relay's hooks (`message gate`, `relay-receipt`, `relay-inbound`) run the machine's stable supercode: its front door
|
|
774
|
-
this.relays = new Relays({ mailRoot, machine, supercodeBin: this.#stableSupercode(), serve: (method, params, timeoutMs) => this.#serveRequest(method, params, timeoutMs),
|
|
774
|
+
this.relays = new Relays({ mailRoot, machine, supercodeBin: this.#stableSupercode(), hooksServed: () => this.#frontDoorServes('message gate'), serve: (method, params, timeoutMs) => this.#serveRequest(method, params, timeoutMs),
|
|
775
775
|
runtimeOf: (harness, id) => live.runtimeReceipt(harness, id), runtimeNamed: (receiptId) => live.receiptNamed(receiptId),
|
|
776
776
|
runtimesIn: (harness, cwd) => live.runtimesIn(harness, cwd),
|
|
777
777
|
endpointOf: async () => (await this.relayEndpoint)?.url ?? null,
|
|
@@ -800,7 +800,7 @@ export class TeamsMachine extends MachineCore {
|
|
|
800
800
|
handOver: (address, id) => this.mailSend.handOver(address, id),
|
|
801
801
|
// the thread a reply to `id` joins: its filed parent's thread, or the parent itself (mailbox.rs `thread_of_reply`)
|
|
802
802
|
threadOf: (id) => { const found = this.mailStore.find(id).find((item) => item.stored.envelope.id === id); return found?.stored.envelope.thread ?? id; },
|
|
803
|
-
log: (line) => this.emit('mail-log', line) });
|
|
803
|
+
stateDir: supercodeHome(this.env), log: (line) => this.emit('mail-log', line) });
|
|
804
804
|
// a relay's inbound mail its hook kept while this daemon did not answer (the front door's spool): filed now, each
|
|
805
805
|
// minute, and before each relay's inbound call
|
|
806
806
|
this.hookSpool = join(supercodeHome(this.env), 'hook-spool', 'relay-inbound');
|
|
@@ -1830,6 +1830,19 @@ export class TeamsMachine extends MachineCore {
|
|
|
1830
1830
|
* this daemon's native once per daemon (`__stable-program`), for a command a pane records; this daemon's own
|
|
1831
1831
|
* supercode when its native has no such verb or the shim cannot be written.
|
|
1832
1832
|
*/
|
|
1833
|
+
/** Whether the machine's stable supercode serves `command` in TypeScript (its front door's own list), asked once: a dev
|
|
1834
|
+
* or cargo build's native, run with no front door, serves no hook verb. */
|
|
1835
|
+
#frontDoorServes(command) {
|
|
1836
|
+
if (this.frontDoorServed === undefined) {
|
|
1837
|
+
this.frontDoorServed = new Set();
|
|
1838
|
+
try {
|
|
1839
|
+
const answer = spawnSync(this.#stableSupercode(), ['front-door', '--json'], { env: this.env, encoding: 'utf8', timeout: 5000, stdio: ['ignore', 'pipe', 'ignore'] });
|
|
1840
|
+
if (answer.status === 0) for (const row of JSON.parse(answer.stdout).typescript ?? []) this.frontDoorServed.add(row.command);
|
|
1841
|
+
} catch { /* none served */ }
|
|
1842
|
+
}
|
|
1843
|
+
return this.frontDoorServed.has(command);
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1833
1846
|
#stableSupercode() {
|
|
1834
1847
|
if (this.stableSupercodePath !== undefined) return this.stableSupercodePath ?? this.supercodeBin;
|
|
1835
1848
|
this.stableSupercodePath = null;
|
|
@@ -1922,14 +1935,7 @@ export class TeamsMachine extends MachineCore {
|
|
|
1922
1935
|
if (process.platform === 'win32' || !((params.kind === 'codex' && name === 'codex') ||
|
|
1923
1936
|
(params.kind === 'claude-code' && name === 'claude'))) return launchCommand(params.program, args);
|
|
1924
1937
|
// the hook is the front door's (mail step 1b): taken when the machine's stable supercode serves it
|
|
1925
|
-
|
|
1926
|
-
let served = false;
|
|
1927
|
-
try {
|
|
1928
|
-
const answer = spawnSync(this.#stableSupercode(), ['front-door', '--json'], { env: this.env, encoding: 'utf8', timeout: 5000, stdio: ['ignore', 'pipe', 'ignore'] });
|
|
1929
|
-
served = answer.status === 0 && (JSON.parse(answer.stdout).typescript ?? []).some((row) => row.command === 'message native-question-hook');
|
|
1930
|
-
} catch { /* not served: the launch runs without it */ }
|
|
1931
|
-
this.nativeQuestionHooksSupported = served;
|
|
1932
|
-
}
|
|
1938
|
+
this.nativeQuestionHooksSupported ??= this.#frontDoorServes('message native-question-hook');
|
|
1933
1939
|
if (!this.nativeQuestionHooksSupported) return launchCommand(params.program, args);
|
|
1934
1940
|
const mailHome = this.env.SUPERCODE_HOME || join(this.env.XDG_CONFIG_HOME || join(homedir(), '.config'), 'supercode');
|
|
1935
1941
|
const dir = join(mailHome, 'mail', 'native-launches');
|
|
@@ -3866,6 +3872,82 @@ export class TeamsMachine extends MachineCore {
|
|
|
3866
3872
|
if (method === 'harness.v1.mail.relay_inbound') { this.mailHooks.drainSpool(this.hookSpool); return { print: this.mailHooks.inbound(params.represented, params.directory, input, params.call) }; }
|
|
3867
3873
|
return { print: await this.mailHooks.questionHook(params.binding, input, params.claim, this.nativeQuestions) };
|
|
3868
3874
|
}
|
|
3875
|
+
// a `message mcp` server saying its session has the messaging tools while it runs (mail step 1c): the server is the
|
|
3876
|
+
// caller's own process (its claim), or on Windows the native that started it (`server`), the session's child either way
|
|
3877
|
+
case 'harness.v1.mail.tools': {
|
|
3878
|
+
if (entry.principal !== 'local' || entry.appDoor) throw Object.assign(new Error('the messaging tools are said here by this machine\'s own processes'), { code: 403 });
|
|
3879
|
+
const server = Number.isInteger(params.server_pid) ? params.server_pid : Number(params.claim?.pid);
|
|
3880
|
+
return { taken: this.live.registerTools(Number(params.session_pid), server) };
|
|
3881
|
+
}
|
|
3882
|
+
// a board's own mailbox (mail step 1c): its owner's round claims its unread mail (board_inbox), and says which it
|
|
3883
|
+
// handled (board_ack: those read, the rest given back unread); a dispatcher waits on it (board_wait) instead of
|
|
3884
|
+
// watching the mail folder
|
|
3885
|
+
case 'harness.v1.mail.board_inbox': case 'harness.v1.mail.board_ack': case 'harness.v1.mail.board_wait': {
|
|
3886
|
+
if (entry.principal !== 'local' || entry.appDoor) throw Object.assign(new Error('a board\'s mail is read here by this machine\'s own processes'), { code: 403 });
|
|
3887
|
+
const board = new RegExp(`^sc:${machineName(this.env).replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}:board:[^:\\s]+$`);
|
|
3888
|
+
if (method === 'harness.v1.mail.board_wait') {
|
|
3889
|
+
const boards = (Array.isArray(params.boards) ? params.boards : []).filter((address) => typeof address === 'string' && board.test(address)).slice(0, 256);
|
|
3890
|
+
const seconds = Math.min(120, Math.max(0, Number.isFinite(Number(params.timeout)) ? Math.floor(Number(params.timeout)) : 60));
|
|
3891
|
+
const waiting = () => boards.filter((address) => this.mailStore.hasClaimable(address));
|
|
3892
|
+
let woken = waiting();
|
|
3893
|
+
if (!woken.length && boards.length) {
|
|
3894
|
+
woken = await new Promise((resolve) => {
|
|
3895
|
+
const changed = (address) => { if (boards.includes(address)) { const now = waiting(); if (now.length) done(now); } };
|
|
3896
|
+
const done = (value) => { clearTimeout(timer); this.mailStore.off('changed', changed); resolve(value); };
|
|
3897
|
+
const timer = setTimeout(() => done([]), seconds * 1000);
|
|
3898
|
+
this.mailStore.on('changed', changed);
|
|
3899
|
+
});
|
|
3900
|
+
}
|
|
3901
|
+
return { woken };
|
|
3902
|
+
}
|
|
3903
|
+
let caller = null;
|
|
3904
|
+
try { caller = await this.#callerOf(entry, params); } catch { /* not a registered owner's: the lock below may say */ }
|
|
3905
|
+
if (typeof caller !== 'string' || !board.test(caller)) {
|
|
3906
|
+
// a board on a machine that serves no team has no registered owner (registration is its connector's): its round
|
|
3907
|
+
// names its home and holds that home's board lock, and its address is one of that home's boards
|
|
3908
|
+
const named = params.claim?.named, root = typeof params.root === 'string' && params.root ? params.root : null;
|
|
3909
|
+
caller = null;
|
|
3910
|
+
if (root && typeof named === 'string' && board.test(named) && await this.#holdsBoardLock(root, Number(params.claim?.pid))) {
|
|
3911
|
+
let slugs = ['default'];
|
|
3912
|
+
try { slugs = [...slugs, ...readdirSync(join(root, 'kanban', 'boards'))]; } catch { /* the default board only */ }
|
|
3913
|
+
if (slugs.some((slug) => `sc:${machineName(this.env)}:board:${boardName(boardLabel(root, slug))}` === named)) caller = named;
|
|
3914
|
+
}
|
|
3915
|
+
}
|
|
3916
|
+
if (!caller) return { code: 2, text: 'a board\'s mailbox is read by that board\'s own owner only (its registered owner, or the process holding its home\'s board lock)' };
|
|
3917
|
+
this.boardClaims ??= new Map();
|
|
3918
|
+
if (method === 'harness.v1.mail.board_ack') {
|
|
3919
|
+
const pending = this.boardClaims.get(params.token);
|
|
3920
|
+
if (!pending || pending.address !== caller) return { code: 0, acknowledged: 0 };
|
|
3921
|
+
this.boardClaims.delete(params.token);
|
|
3922
|
+
const handled = new Set(Array.isArray(params.ids) ? params.ids : []);
|
|
3923
|
+
let acknowledged = 0;
|
|
3924
|
+
for (const item of pending.claimed) {
|
|
3925
|
+
try { if (handled.has(item.envelope.id)) { this.mailStore.acknowledge(caller, item); acknowledged++; } else this.mailStore.release(caller, item); }
|
|
3926
|
+
catch { /* moved meanwhile: its reader's recovery gives it back */ }
|
|
3927
|
+
}
|
|
3928
|
+
return { code: 0, acknowledged };
|
|
3929
|
+
}
|
|
3930
|
+
const claimed = this.mailStore.claimUnread(caller, 'board', caller, Number(params.claim?.pid) || process.pid);
|
|
3931
|
+
if (!claimed.length) return { code: 0, token: null, envelopes: [] };
|
|
3932
|
+
const token = randomBytes(12).toString('hex');
|
|
3933
|
+
for (const [old] of [...this.boardClaims].slice(0, Math.max(0, this.boardClaims.size - 63))) this.boardClaims.delete(old);
|
|
3934
|
+
this.boardClaims.set(token, { address: caller, claimed });
|
|
3935
|
+
return { code: 0, token, envelopes: claimed.map((item) => this.mailStore.body(item) ?? item.envelope) };
|
|
3936
|
+
}
|
|
3937
|
+
// the Codex hooks' records (Windows), for serve's Codex listing (mail step 1c)
|
|
3938
|
+
case 'harness.v1.mail.codex_hooks': {
|
|
3939
|
+
if (entry.principal !== 'local' || entry.appDoor) throw Object.assign(new Error('the Codex hooks\' records are read here by this machine\'s own processes'), { code: 403 });
|
|
3940
|
+
return { records: this.mailHooks.codexHookRecords() };
|
|
3941
|
+
}
|
|
3942
|
+
// the names ledger, kept here from the live map (mail step 1c): the session each name last meant, and the name each
|
|
3943
|
+
// session was last listed by, for the native readers (`message send` by a remembered name, `open`, a session tree)
|
|
3944
|
+
case 'harness.v1.mail.names': {
|
|
3945
|
+
if (entry.principal !== 'local' || entry.appDoor) throw Object.assign(new Error('the names ledger is read here by this machine\'s own processes'), { code: 403 });
|
|
3946
|
+
const list = (value) => (Array.isArray(value) ? value.filter((item) => typeof item === 'string').slice(0, 4096) : []);
|
|
3947
|
+
const addresses = {};
|
|
3948
|
+
for (const name of list(params.names)) { const address = this.mailSend.remembered(name); if (address) addresses[name] = address; }
|
|
3949
|
+
return { addresses, names: this.mailSend.rememberedNames(list(params.addresses)) };
|
|
3950
|
+
}
|
|
3869
3951
|
// `message waiting` and `message expired` (step 1): the carrier's own records
|
|
3870
3952
|
case 'harness.v1.mail.waiting': case 'harness.v1.mail.expired': {
|
|
3871
3953
|
if (entry.principal !== 'local' || entry.appDoor) throw Object.assign(new Error('mail is read here by this machine\'s own processes'), { code: 403 });
|
package/mail/hooks.mjs
CHANGED
|
@@ -32,8 +32,13 @@ const printable = (name) => Array.from(String(name ?? '')).filter((c) => /[A-Za-
|
|
|
32
32
|
|
|
33
33
|
export class MailHooks {
|
|
34
34
|
/** `handOver(address, id)` a filed message to its session's door; `threadOf(id)` the thread a reply to `id` joins. */
|
|
35
|
-
constructor({ machine, mailRoot, store, live, handOver, threadOf, log = () => {} }) {
|
|
35
|
+
constructor({ machine, mailRoot, store, live, handOver, threadOf, stateDir = null, log = () => {} }) {
|
|
36
36
|
Object.assign(this, { machine, mailRoot, store, live, handOver, threadOf, log });
|
|
37
|
+
// Windows: each Codex session's last hook (the process it ran under, the rollout it named, when), which serve's
|
|
38
|
+
// Codex listing reads through this daemon (codex_peer.rs `hook_recorded_rollouts`); kept beside this daemon's state,
|
|
39
|
+
// never in the mail folder (mail step 1c)
|
|
40
|
+
this.codexHooksFile = stateDir ? join(stateDir, 'codex-hooks.json') : null;
|
|
41
|
+
this.codexHooks = new Map(Object.entries((this.codexHooksFile && readJson(this.codexHooksFile)) || {}));
|
|
37
42
|
this.relaysRoot = resolve(mailRoot, 'relays');
|
|
38
43
|
this.questions = join(mailRoot, 'native-questions');
|
|
39
44
|
}
|
|
@@ -49,7 +54,8 @@ export class MailHooks {
|
|
|
49
54
|
// Windows: where the listing cannot see a Codex's open rollout, the session's own hook says it runs (the process
|
|
50
55
|
// that ran it, and the rollout it names), until the live map knows Codex there (step 1c)
|
|
51
56
|
if (process.platform === 'win32' && harness === 'codex' && Number.isInteger(claim?.ppid)) {
|
|
52
|
-
|
|
57
|
+
this.codexHooks.set(address, { pid: claim.ppid, rollout: typeof input.transcript_path === 'string' ? input.transcript_path : '', at_ms: Date.now() });
|
|
58
|
+
this.#saveCodexHooks();
|
|
53
59
|
}
|
|
54
60
|
const event = typeof input.hook_event_name === 'string' ? input.hook_event_name : '';
|
|
55
61
|
// the person's own prompt: filed as theirs, since the harness's transcript does not say who wrote it; no pointer
|
|
@@ -68,6 +74,21 @@ export class MailHooks {
|
|
|
68
74
|
: { decision: 'block', reason: `${pointer} Read them before you finish.` };
|
|
69
75
|
}
|
|
70
76
|
|
|
77
|
+
/** The Codex hooks' records (Windows), for serve's listing: `{address, pid, rollout, at_ms}` each; one whose process is
|
|
78
|
+
* gone is let go. */
|
|
79
|
+
codexHookRecords() {
|
|
80
|
+
let dropped = false;
|
|
81
|
+
for (const [address, record] of this.codexHooks) if (!alive(record.pid)) { this.codexHooks.delete(address); dropped = true; }
|
|
82
|
+
if (dropped) this.#saveCodexHooks();
|
|
83
|
+
return [...this.codexHooks].map(([address, record]) => ({ address, ...record }));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
#saveCodexHooks() {
|
|
87
|
+
if (!this.codexHooksFile) return;
|
|
88
|
+
try { writeFileSync(`${this.codexHooksFile}.tmp`, JSON.stringify(Object.fromEntries(this.codexHooks))); renameSync(`${this.codexHooksFile}.tmp`, this.codexHooksFile); }
|
|
89
|
+
catch (error) { this.log(`codex hooks: not kept: ${error.message}`); }
|
|
90
|
+
}
|
|
91
|
+
|
|
71
92
|
/** How many unread messages, from whom, and the one command that reads them (message.rs `mail_pointer`). */
|
|
72
93
|
#pointer(unread) {
|
|
73
94
|
const senders = [];
|
|
@@ -174,7 +195,12 @@ export class MailHooks {
|
|
|
174
195
|
// drain (its id names its mail, so a filing that half happened is not made twice)
|
|
175
196
|
if (spooled?.input) {
|
|
176
197
|
try { this.inbound(spooled.represented, spooled.directory, spooled.input, spooled.call); filed++; }
|
|
177
|
-
catch (error) {
|
|
198
|
+
catch (error) {
|
|
199
|
+
// one this daemon refuses (its folder no longer a relay's, its address not one: a 400) never will be filed: let
|
|
200
|
+
// go, said; one that failed on its way to the store (an I/O failure) is kept for the next drain
|
|
201
|
+
if (error.code === 400) this.log(`relay inbound: kept mail ${name} refused (${error.message}); let go`);
|
|
202
|
+
else { this.log(`relay inbound: kept mail ${name} not filed yet (${error.message}); kept for the next drain`); continue; }
|
|
203
|
+
}
|
|
178
204
|
}
|
|
179
205
|
rmSync(path, { force: true });
|
|
180
206
|
}
|
package/mail/relay.mjs
CHANGED
|
@@ -91,8 +91,8 @@ export class Relays {
|
|
|
91
91
|
* id)` the live-runtime receipt the live-session map holds for a runtime id (its file read whole), or null;
|
|
92
92
|
* `runtimeNamed(receiptId)` one receipt read by its own name.
|
|
93
93
|
*/
|
|
94
|
-
constructor({ mailRoot, machine, supercodeBin, serve, runtimeOf, runtimeNamed = () => null, runtimesIn = () => [], endpointOf = async () => null, arrived = async () => false, log = () => {} }) {
|
|
95
|
-
Object.assign(this, { mailRoot, machine, supercodeBin, serve, runtimeOf, runtimeNamed, runtimesIn, endpointOf, arrived, log });
|
|
94
|
+
constructor({ mailRoot, machine, supercodeBin, serve, runtimeOf, runtimeNamed = () => null, runtimesIn = () => [], endpointOf = async () => null, arrived = async () => false, hooksServed = () => true, log = () => {} }) {
|
|
95
|
+
Object.assign(this, { mailRoot, machine, supercodeBin, serve, runtimeOf, runtimeNamed, runtimesIn, endpointOf, arrived, hooksServed, log });
|
|
96
96
|
this.lost = new Map(); // relay folder → when a start of its runtime lost its answer (the runtime may run, unnamed)
|
|
97
97
|
this.remoteKept = new Map(); // relay folder of a sender on another machine → the timer that retires it
|
|
98
98
|
this.sending = new Map(); // relay folder → the send in progress through it (one at a time per relay)
|
|
@@ -268,6 +268,9 @@ export class Relays {
|
|
|
268
268
|
/** The relay's runtime, started in this daemon's harness serve when it is not running (claude_relay.rs
|
|
269
269
|
* `ensure_relay_runtime`): `{ runtime (its receipt), connection }`. */
|
|
270
270
|
async #ensure(spec, step = () => {}, { stale = false } = {}) {
|
|
271
|
+
// a relay's hooks are the front door's verbs (mail step 1b): a stable supercode with no front door (a dev or cargo
|
|
272
|
+
// build's native) would answer none of them, and every mail the relay is handed would be blocked unfiled
|
|
273
|
+
if (!this.hooksServed()) throw new Error('relays run their hooks through the machine\'s stable supercode, which has no front door here (a dev or cargo build); install supercode from npm to send to Claude sessions by relay');
|
|
271
274
|
const endpoint = await this.endpoint();
|
|
272
275
|
step('relay endpoint');
|
|
273
276
|
if (!endpoint) throw new Error('the relay endpoint is not served by this machine\'s daemon (its log says why); a Claude session can be sent mail once the daemon starts again');
|
package/mail/send.mjs
CHANGED
|
@@ -890,6 +890,24 @@ export class MailSend {
|
|
|
890
890
|
/** The session a name last meant here, listed by it within NAME_KEPT_MS (mail_route.rs `remembered_address`). */
|
|
891
891
|
remembered(name) { return this.#rememberedAddress(name); }
|
|
892
892
|
|
|
893
|
+
/** The name `address` was last listed by here (the newest it began to carry), or null (mail_route.rs
|
|
894
|
+
* `remembered_name`). */
|
|
895
|
+
rememberedName(address) { return this.rememberedNames([address])[address] ?? null; }
|
|
896
|
+
|
|
897
|
+
/** rememberedName for each of `addresses`, the ledger read once. */
|
|
898
|
+
rememberedNames(addresses) {
|
|
899
|
+
const names = readJson(join(this.mailRoot, 'names.json'));
|
|
900
|
+
const wanted = new Set(addresses), best = new Map();
|
|
901
|
+
if (names && typeof names === 'object') {
|
|
902
|
+
for (const [name, record] of Object.entries(names)) {
|
|
903
|
+
if (!wanted.has(record?.address)) continue;
|
|
904
|
+
const held = best.get(record.address);
|
|
905
|
+
if (!held || (record.since_ms ?? 0) > (held.since_ms ?? 0)) best.set(record.address, { name, since_ms: record.since_ms ?? 0 });
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
return Object.fromEntries([...best].map(([address, { name }]) => [address, name]));
|
|
909
|
+
}
|
|
910
|
+
|
|
893
911
|
#rememberedAddress(name) {
|
|
894
912
|
const record = readJson(join(this.mailRoot, 'names.json'))?.[name];
|
|
895
913
|
if (!record || Date.now() - Math.max(record.seen_ms ?? 0, record.since_ms ?? 0) > NAME_KEPT_MS) return null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/supercode-teams",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.122",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "A machine daemon serving this machine's doors, and a local or remote Teams server for shared session discovery, OpenTelemetry ingestion and access to enrolled machines.",
|
|
6
6
|
"license": "MIT",
|