@volter/supercode-teams 0.3.126 → 0.3.128
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/machine.mjs +33 -1
- package/mail/relay.mjs +13 -1
- package/mail/send.mjs +65 -14
- package/package.json +1 -1
package/machine.mjs
CHANGED
|
@@ -1845,10 +1845,24 @@ export class TeamsMachine extends MachineCore {
|
|
|
1845
1845
|
/** Whether the machine's stable supercode serves `command` in TypeScript (its front door's own list), asked once: a dev
|
|
1846
1846
|
* or cargo build's native, run with no front door, serves no hook verb. */
|
|
1847
1847
|
#frontDoorServes(command) {
|
|
1848
|
+
// asked once, kept while it answered; an ask that found nothing is asked again a minute on, never kept for the
|
|
1849
|
+
// daemon's life (one slow or failed start of the front door refused every relay send until a restart)
|
|
1850
|
+
if (this.frontDoorServed !== undefined && !this.frontDoorServed.size && Date.now() - (this.frontDoorAskedAt ?? 0) > 60_000) this.frontDoorServed = undefined;
|
|
1848
1851
|
if (this.frontDoorServed === undefined) {
|
|
1849
1852
|
this.frontDoorServed = new Set();
|
|
1853
|
+
this.frontDoorAskedAt = Date.now();
|
|
1850
1854
|
try {
|
|
1851
|
-
const
|
|
1855
|
+
const stable = this.#stableSupercode();
|
|
1856
|
+
// On Windows the stable supercode is a `.cmd` shim, which Node starts only through a shell (EINVAL otherwise,
|
|
1857
|
+
// since Node 20.12): asked plainly it answered nothing, so every relay was refused as "no front door here" and
|
|
1858
|
+
// no mail reached a Claude session on the machine. The shim is supercode's own one line (claude_relay.rs:
|
|
1859
|
+
// `@"<node>" "<front door>" %*`), so what it runs is run directly: no shell, nothing of its path read by one
|
|
1860
|
+
// (#1270 review: cmd.exe /s cut a home path with a space at the space). A shim of another form goes through
|
|
1861
|
+
// cmd.exe, the whole line in an outer pair of quotes, so /s takes off only those.
|
|
1862
|
+
const direct = /\.(cmd|bat)$/i.test(stable) ? shimTarget(stable) : [stable];
|
|
1863
|
+
const answer = direct
|
|
1864
|
+
? spawnSync(direct[0], [...direct.slice(1), 'front-door', '--json'], { env: this.env, encoding: 'utf8', timeout: 5000, stdio: ['ignore', 'pipe', 'ignore'], windowsHide: true })
|
|
1865
|
+
: spawnSync(this.env.ComSpec || process.env.ComSpec || 'cmd.exe', ['/d', '/s', '/c', `""${stable}" front-door --json"`], { env: this.env, encoding: 'utf8', timeout: 5000, stdio: ['ignore', 'pipe', 'ignore'], windowsHide: true, windowsVerbatimArguments: true });
|
|
1852
1866
|
if (answer.status === 0) for (const row of JSON.parse(answer.stdout).typescript ?? []) this.frontDoorServed.add(row.command);
|
|
1853
1867
|
} catch { /* none served */ }
|
|
1854
1868
|
}
|
|
@@ -1864,6 +1878,14 @@ export class TeamsMachine extends MachineCore {
|
|
|
1864
1878
|
const path = answer.status === 0 ? answer.stdout.trim() : '';
|
|
1865
1879
|
if (path && isAbsolute(path) && existsSync(path)) this.stableSupercodePath = path;
|
|
1866
1880
|
} catch { /* this daemon's own supercode */ }
|
|
1881
|
+
// On Windows the daemon runs this release's per-version copy (service\bin\<version>, so an upgrade can replace the
|
|
1882
|
+
// npm one), and `__stable-program` refuses it as not the machine's own supercode: the daemon fell back to that
|
|
1883
|
+
// native, which serves no front door, so every relay was refused and no mail reached a Claude session here. The
|
|
1884
|
+
// shim the machine's own supercode keeps in <SUPERCODE_HOME>\bin is the machine's stable supercode.
|
|
1885
|
+
if (this.stableSupercodePath === null && process.platform === 'win32') {
|
|
1886
|
+
const shim = join(supercodeHome(this.env), 'bin', 'supercode.cmd');
|
|
1887
|
+
if (existsSync(shim)) this.stableSupercodePath = shim;
|
|
1888
|
+
}
|
|
1867
1889
|
return this.stableSupercodePath ?? this.supercodeBin;
|
|
1868
1890
|
}
|
|
1869
1891
|
|
|
@@ -4329,3 +4351,13 @@ function healthEntry(env) {
|
|
|
4329
4351
|
const sibling = fileURLToPath(new URL('../health/bin/health.mjs', import.meta.url));
|
|
4330
4352
|
return existsSync(sibling) ? sibling : null;
|
|
4331
4353
|
}
|
|
4354
|
+
|
|
4355
|
+
/** What supercode's Windows shim runs, from its one line (`@"<node>" "<front door>" %*`, or `@"<native>" %*`;
|
|
4356
|
+
* claude_relay.rs writes it): `[program, ...args]`, or null for a shim of another form. */
|
|
4357
|
+
function shimTarget(path) {
|
|
4358
|
+
let text;
|
|
4359
|
+
try { text = readFileSync(path, 'utf8'); } catch { return null; }
|
|
4360
|
+
const line = text.split(/\r?\n/).map((row) => row.trim()).find((row) => row.startsWith('@"'));
|
|
4361
|
+
const m = line ? /^@"([^"]+)"(?: "([^"]+)")? %\*$/.exec(line) : null;
|
|
4362
|
+
return m ? [m[1], ...(m[2] ? [m[2]] : [])] : null;
|
|
4363
|
+
}
|
package/mail/relay.mjs
CHANGED
|
@@ -270,7 +270,7 @@ export class Relays {
|
|
|
270
270
|
async #ensure(spec, step = () => {}, { stale = false } = {}) {
|
|
271
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
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,
|
|
273
|
+
if (!this.hooksServed()) throw new Error('relays run their hooks through the machine\'s stable supercode, and none here serves them (no stable supercode was found, or it is a dev or cargo build); install supercode from npm to send to Claude sessions by relay');
|
|
274
274
|
const endpoint = await this.endpoint();
|
|
275
275
|
step('relay endpoint');
|
|
276
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');
|
|
@@ -371,6 +371,18 @@ export class Relays {
|
|
|
371
371
|
* (claude_relay.rs `send_through_relay`): the receiver is named by its own socket (`uds:<path>`), unique to it, never
|
|
372
372
|
* by its name. One send at a time per relay; a relay whose process is proven dead is started again once.
|
|
373
373
|
*/
|
|
374
|
+
/** Whether `sender`'s relay still holds a send queued (its queue file, written within SEND_TIMEOUT_MS twice over): a
|
|
375
|
+
* relay runtime outlives a daemon that ended mid-send and may still make it. */
|
|
376
|
+
sending(sender) {
|
|
377
|
+
try { return Date.now() - statSync(this.spec(sender).queue).mtimeMs < 2 * SEND_TIMEOUT_MS; } catch { return false; }
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/** Whether message `messageId` from `sender` landed in Claude session `sessionId`'s transcript, as a relay send reads
|
|
381
|
+
* an unconfirmed outcome (send.mjs handOver asks it of a hand-over the daemon did not live to settle). */
|
|
382
|
+
async landed(sessionId, messageId, sender) {
|
|
383
|
+
return this.arrived(sessionId, `cross-session-message id=\\"${shortId(messageId)}\\" from=\\"${sender}\\"`);
|
|
384
|
+
}
|
|
385
|
+
|
|
374
386
|
async send(sender, senderName, receiver, message, messageId) {
|
|
375
387
|
const spec = this.spec(sender, senderName);
|
|
376
388
|
const asked = performance.now();
|
package/mail/send.mjs
CHANGED
|
@@ -25,6 +25,12 @@ export function unansweredText(where, detail, id) {
|
|
|
25
25
|
|
|
26
26
|
/** How long a send waits, in a daemon's first moments, for its Codex sessions to be known before it resolves a receiver. */
|
|
27
27
|
const CODEX_KNOWN_WAIT_MS = 20_000;
|
|
28
|
+
// A relayed message's wake is due this long after its relay send begins: a send still running here holds it (handOver),
|
|
29
|
+
// so it fires only for a send the daemon did not live to finish (t_b0f9a2a0)
|
|
30
|
+
const RELAY_GRACE_MS = 30_000;
|
|
31
|
+
/** The kinds of delivery that hand nothing over (#deliverWoken's `how`): the copy waits filed. An operator's mailbox,
|
|
32
|
+
* which its operator reads, and a session's hook, which shows it, are deliveries. */
|
|
33
|
+
const LEFT_FILED = new Set(['queued', 'stored', 'pane_held', 'pane_waits', 'not_running']);
|
|
28
34
|
|
|
29
35
|
/** Replies in one unbroken chain between two sessions beyond which another is refused. */
|
|
30
36
|
const REPLY_CHAIN_LIMIT = 32;
|
|
@@ -53,6 +59,7 @@ export class MailSend {
|
|
|
53
59
|
constructor({ machine, mailRoot, live, store, agents, relays, askMachine, deliverRuntime, codexAnswer, typeTurn, log = () => {} }) {
|
|
54
60
|
Object.assign(this, { machine, mailRoot, live, store, agents, relays, askMachine, deliverRuntime, codexAnswer, typeTurn, log });
|
|
55
61
|
this.handing = new Set(); // `<address>\n<id>` of each hand-over running here (handOver)
|
|
62
|
+
this.relaying = new Set(); // `<address>\n<id>` of each relay send running here (route): its wake waits for it
|
|
56
63
|
}
|
|
57
64
|
|
|
58
65
|
// ---- one send ----------------------------------------------------------------------------------------------------
|
|
@@ -319,11 +326,21 @@ export class MailSend {
|
|
|
319
326
|
const handed = sent.reply_via?.mode === 'command' && door.session.tools ? { ...sent, reply_via: { mode: 'tool' } } : sent;
|
|
320
327
|
const text = render(handed);
|
|
321
328
|
if (Buffer.byteLength(text) > MAX_RELAYED_BYTES) throw new Refused('too_long', Buffer.byteLength(text));
|
|
322
|
-
// filed before it is handed over
|
|
323
|
-
//
|
|
329
|
+
// filed before it is handed over, unread, with a wake due RELAY_GRACE_MS from now: a session that reads its mailbox
|
|
330
|
+
// at its first tool call finds it, and a daemon that ends before the relay answers (an upgrade's restart, a crash)
|
|
331
|
+
// leaves it for the carrier, which hands it over again (t_b0f9a2a0: filed read before, such a send was lost). It is
|
|
332
|
+
// read once the relay has made it; a relay that fails takes back the copy this delivery filed
|
|
324
333
|
const filedHere = this.store.findIn(to, handed.id) == null;
|
|
325
|
-
const filed = this.store.
|
|
326
|
-
|
|
334
|
+
const filed = this.store.deliver(to, handed);
|
|
335
|
+
if (wake && !this.store.hasWake(to, handed.id)) {
|
|
336
|
+
// its hand-over begins now: a daemon that restarts reads the receiver's transcript before sending it again
|
|
337
|
+
try { this.store.requestWake(to, handed.id); this.store.setWakeState(to, handed.id, { attempts: 0, next_at_ms: Date.now() + RELAY_GRACE_MS, handover_at_ms: Date.now() }); } catch { /* the relay's own outcome stands */ }
|
|
338
|
+
}
|
|
339
|
+
const key = `${to}\n${handed.id}`;
|
|
340
|
+
this.relaying.add(key);
|
|
341
|
+
let relayed;
|
|
342
|
+
try { relayed = await this.relays.send(handed.from, handed.from_name, { sessionId: door.session.address.split(':').slice(3).join(':'), name: door.session.name.split('@')[0], socket: door.session.socket }, text, handed.id); }
|
|
343
|
+
finally { this.relaying.delete(key); }
|
|
327
344
|
// a send the relay may have made keeps its copy and is unanswered, never taken back and said not sent; only one the
|
|
328
345
|
// relay is known not to have made (refused, never handed in) takes its copy back. One its receiver read from its
|
|
329
346
|
// mailbox meanwhile (its recipient claim) has reached it. Otherwise the relay can no longer make it (its queue is
|
|
@@ -331,7 +348,8 @@ export class MailSend {
|
|
|
331
348
|
// goes back unread, for the receiver's own mailbox read and the mailbox's wake, which hands it over again
|
|
332
349
|
const received = this.store.claimOf(to, handed.id)?.recipient === true;
|
|
333
350
|
if (!relayed.delivered && relayed.unknown && !received) {
|
|
334
|
-
|
|
351
|
+
// kept unread, its wake due now: the carrier hands it over again after its backoff
|
|
352
|
+
if (filedHere && wake) { try { this.store.requestWake(to, handed.id); } catch { /* the wake filed above stands */ } }
|
|
335
353
|
throw Object.assign(new Error(relayed.detail), { unanswered: true });
|
|
336
354
|
}
|
|
337
355
|
if (!relayed.delivered && !received) { if (filedHere) { try { unlinkSync(filed); } catch { /* gone */ } } throw new Error(relayed.detail); }
|
|
@@ -378,10 +396,19 @@ export class MailSend {
|
|
|
378
396
|
const reached = [], copied = [], failed = [];
|
|
379
397
|
let recorded = false;
|
|
380
398
|
const held = []; // recipients whose own copy is held unread with its wake (an unanswered relay)
|
|
399
|
+
let agentKey = null;
|
|
381
400
|
if (plan.agent) {
|
|
382
|
-
// the agent's own mailbox keeps the record of what was addressed to it
|
|
383
|
-
|
|
401
|
+
// the agent's own mailbox keeps the record of what was addressed to it: unread, with a wake due RELAY_GRACE_MS on,
|
|
402
|
+
// until a recipient has it, so a daemon that ends while the plan runs (a resume, a relay send, another machine
|
|
403
|
+
// asked) leaves it for the carrier to plan again (#1264 review P1). While this plan runs its wake waits for it
|
|
404
|
+
try {
|
|
405
|
+
const fresh = this.store.findIn(plan.agent, sent.id) == null;
|
|
406
|
+
this.store.deliver(plan.agent, sent); recorded = true;
|
|
407
|
+
if (fresh && !this.store.hasWake(plan.agent, sent.id)) { this.store.requestWake(plan.agent, sent.id); this.store.setWakeState(plan.agent, sent.id, { attempts: 0, next_at_ms: Date.now() + RELAY_GRACE_MS }); }
|
|
408
|
+
agentKey = `${plan.agent}\n${sent.id}`; this.relaying.add(agentKey);
|
|
409
|
+
} catch (error) { failed.push(`the agent's mailbox (${error.message})`); }
|
|
384
410
|
}
|
|
411
|
+
try {
|
|
385
412
|
const sessions = this.live.sessions();
|
|
386
413
|
for (const recipient of plan.recipients.filter((item) => item.address !== caller.address)) {
|
|
387
414
|
const label = this.#label(sessions, recipient.address);
|
|
@@ -390,7 +417,16 @@ export class MailSend {
|
|
|
390
417
|
try { this.#fileUnread(recipient.address, sent); copied.push(label); } catch (error) { failed.push(`${label} (${error.message})`); }
|
|
391
418
|
continue;
|
|
392
419
|
}
|
|
393
|
-
try {
|
|
420
|
+
try {
|
|
421
|
+
const { how, text: said } = await this.#deliverWoken(sent, recipient.address, sessions, queue, notify_when_idle);
|
|
422
|
+
// only a hand-over is a reach: a copy left filed with nothing to carry it (queued, stored, held in the daemon's
|
|
423
|
+
// memory for a pane, or for a session not running) is held when its own wake carries it, and otherwise the
|
|
424
|
+
// agent's copy keeps its retry (t_b0f9a2a0: such a send was counted reached, and nothing carried it). One the
|
|
425
|
+
// sender asked to queue (--queue) is held as asked: its receiver reads it at its next turn
|
|
426
|
+
if (!LEFT_FILED.has(how)) reached.push(`${label} (${said})`);
|
|
427
|
+
else if (this.store.hasWake(recipient.address, sent.id) || (queue && how === 'queued')) held.push(`${label} (${said})`);
|
|
428
|
+
else failed.push(`${label} (${said}; handed over again when it can be)`);
|
|
429
|
+
}
|
|
394
430
|
catch (error) {
|
|
395
431
|
// held only when its copy is here, unread: its wake hands it over, or (queued, no wake) the receiver's own read
|
|
396
432
|
// does. An unanswered request to another machine's door holds nothing here, so it is a failure, and the agent's
|
|
@@ -400,6 +436,7 @@ export class MailSend {
|
|
|
400
436
|
else failed.push(error.unanswered ? `${label} (unanswered, whether it arrived is unknown: ${error.message})` : `${label} (${error.message})`);
|
|
401
437
|
}
|
|
402
438
|
}
|
|
439
|
+
} finally { if (agentKey) this.relaying.delete(agentKey); }
|
|
403
440
|
const thread = plan.thread ? ` Thread ${shortId(plan.thread.id)}.` : '';
|
|
404
441
|
// agent mail that reached nobody is never left only as a read record: its copy in the agent's mailbox goes back
|
|
405
442
|
// unread with a wake, and the carrier hands it over again (handOver plans it anew) after its backoff, until a
|
|
@@ -450,18 +487,18 @@ export class MailSend {
|
|
|
450
487
|
if (cameThroughLink(sent)) throw new Error(`not carried on to ${door.machine}: a message that came to this machine through a link is for its own sessions only`);
|
|
451
488
|
const answer = await this.askMachine(door.machine, { op: 'file', to, envelope: sent });
|
|
452
489
|
if (answer?.code !== 0) throw new Error(typeof answer?.text === 'string' ? answer.text : 'the other machine refused the message');
|
|
453
|
-
return 'filed on its machine';
|
|
490
|
+
return { how: 'other_machine', text: 'filed on its machine' };
|
|
454
491
|
}
|
|
455
492
|
if (!door) {
|
|
456
493
|
// a reply to a closed session resumes it; only one the mailbox launched, the way it was launched
|
|
457
494
|
this.store.deliver(to, sent);
|
|
458
|
-
if (!(await this.agents.resumable(to))) return 'stopped; not resumed, since the mailbox did not launch it: the message waits in its mailbox';
|
|
495
|
+
if (!(await this.agents.resumable(to))) return { how: 'not_running', text: 'stopped; not resumed, since the mailbox did not launch it: the message waits in its mailbox' };
|
|
459
496
|
try { await this.agents.resume(to); } catch (error) { throw new Error(`not resumed: ${error.message}`); }
|
|
460
497
|
const started = Date.now();
|
|
461
498
|
for (;;) {
|
|
462
499
|
door = this.#door(to, this.live.sessions());
|
|
463
500
|
if (door) break;
|
|
464
|
-
if (Date.now() - started > RESUME_WAIT_MS) return 'resumed; the message waits in its mailbox';
|
|
501
|
+
if (Date.now() - started > RESUME_WAIT_MS) return { how: 'not_running', text: 'resumed; the message waits in its mailbox' };
|
|
465
502
|
await sleep(1000);
|
|
466
503
|
}
|
|
467
504
|
}
|
|
@@ -472,7 +509,7 @@ export class MailSend {
|
|
|
472
509
|
if (error instanceof Refused) throw new Error(`${error.bytes} bytes; the limit is ${MAX_RELAYED_BYTES}`);
|
|
473
510
|
throw error;
|
|
474
511
|
}
|
|
475
|
-
return { steered: 'steered into its running turn', started: 'started a turn', native_busy: 'read at its next tool call', native_idle: 'starts its next turn', hooked: 'in its mailbox, its hook shows it', queued: 'waits in its mailbox', operator: 'filed', already: 'it already had it', stored: 'stored; it has no delivery door', typed: 'typed into its pane', pane_held: 'held for its pane until its composer is free', pane_waits: 'waits in its mailbox (its composer was not empty)' }[how];
|
|
512
|
+
return { how, text: { steered: 'steered into its running turn', started: 'started a turn', native_busy: 'read at its next tool call', native_idle: 'starts its next turn', hooked: 'in its mailbox, its hook shows it', queued: 'waits in its mailbox', operator: 'filed', already: 'it already had it', stored: 'stored; it has no delivery door', typed: 'typed into its pane', pane_held: 'held for its pane until its composer is free', pane_waits: 'waits in its mailbox (its composer was not empty)' }[how] };
|
|
476
513
|
}
|
|
477
514
|
|
|
478
515
|
// ---- a filing --------------------------------------------------------------------------------------------------
|
|
@@ -724,6 +761,8 @@ export class MailSend {
|
|
|
724
761
|
this.store.markRead(address, found.path);
|
|
725
762
|
return { outcome: 'filed', detail: 'already handed over' };
|
|
726
763
|
}
|
|
764
|
+
// a plan or a relay send still running here holds its wake (#1264 review P1)
|
|
765
|
+
if (this.relaying.has(`${address}\n${id}`)) return { outcome: 'later', detail: 'its delivery is still in progress' };
|
|
727
766
|
const parsed = parseAddress(address);
|
|
728
767
|
if (parsed?.harness === AGENT_HARNESS) {
|
|
729
768
|
const plan = await this.#plan(sent, address, {});
|
|
@@ -743,8 +782,20 @@ export class MailSend {
|
|
|
743
782
|
// one still running here (its carrier gave up on its answer, past its bound) is waited for: the wake stays and is
|
|
744
783
|
// tried again after its backoff, settled by that hand-over's own outcome
|
|
745
784
|
const key = `${address}\n${id}`;
|
|
746
|
-
if (this.handing.has(key)) return { outcome: 'later', detail: 'its hand-over is still in progress' };
|
|
747
|
-
|
|
785
|
+
if (this.handing.has(key) || this.relaying.has(key)) return { outcome: 'later', detail: 'its hand-over is still in progress' };
|
|
786
|
+
// an earlier hand-over never settled (the daemon ended mid-way): one its receiver's transcript holds landed, and is
|
|
787
|
+
// read; any other is handed over again, never given up (t_b0f9a2a0)
|
|
788
|
+
if (state.handover_at_ms != null && door.kind === 'native') {
|
|
789
|
+
// a relay that outlived its daemon may still be making that send (its queue still there, fresh): waited for, never
|
|
790
|
+
// sent beside it (#1264 review P2a)
|
|
791
|
+
if (this.relays.sending?.(sent.from, id)) return { outcome: 'later', detail: 'its relay may still be making it' };
|
|
792
|
+
let landed = false;
|
|
793
|
+
try { landed = await this.relays.landed(door.session.address.split(':').slice(3).join(':'), id, sent.from); } catch { /* unread: hand it over */ }
|
|
794
|
+
if (landed) {
|
|
795
|
+
try { this.store.claimFor(address, id, { pid: process.pid, via: 'door', caller: null, recipient: true, at_ms: Date.now() }); this.store.markRead(address, found.path); } catch { /* moved meanwhile */ }
|
|
796
|
+
return { outcome: 'filed', detail: 'an earlier hand-over landed' };
|
|
797
|
+
}
|
|
798
|
+
}
|
|
748
799
|
this.store.setWakeState(address, id, { ...state, handover_at_ms: Date.now() });
|
|
749
800
|
this.handing.add(key);
|
|
750
801
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/supercode-teams",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.128",
|
|
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",
|