@volter/supercode-teams 0.3.91 → 0.3.93
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 +1 -0
- package/bin/workspace.mjs +1 -0
- package/connector/index.mjs +24 -13
- package/live-sessions.mjs +60 -11
- package/machine.mjs +7 -3
- package/mail/send.mjs +24 -5
- package/package.json +1 -1
package/bin/teams.mjs
CHANGED
|
@@ -175,6 +175,7 @@ async function main() {
|
|
|
175
175
|
machine.on('door', (d) => { if (flags.verbose) process.stderr.write(`door ${d.method} ${d.outcome} ${d.ms}ms${d.detail ? ` ${d.detail}` : ''}\n`); });
|
|
176
176
|
machine.on('serve-stderr', (t) => process.stderr.write(t));
|
|
177
177
|
machine.on('mail-log', (line) => process.stderr.write(`mail: ${line}\n`));
|
|
178
|
+
machine.on('records-log', (line) => process.stderr.write(`records: ${line}\n`));
|
|
178
179
|
machine.on('serve-exit', (e) => process.stderr.write(`harness serve exited: ${JSON.stringify(e)}\n`));
|
|
179
180
|
machine.on('unrouted-notification', (m) => { if (flags.verbose) process.stderr.write(`unrouted: ${m.method}\n`); });
|
|
180
181
|
const socket = await machine.start();
|
package/bin/workspace.mjs
CHANGED
|
@@ -1136,6 +1136,7 @@ async function connectorCommand(verb, { flags, rest, output, client, context, te
|
|
|
1136
1136
|
machine.on('serve-exit', (exit) => process.stderr.write(`supercode teams: harness serve exited: ${JSON.stringify(exit)}\n`));
|
|
1137
1137
|
machine.on('serve-stderr', (text) => process.stderr.write(`supercode teams: harness serve: ${String(text).trimEnd().slice(-2000)}\n`));
|
|
1138
1138
|
machine.on('mail-log', (line) => process.stderr.write(`supercode teams: mail: ${line}\n`));
|
|
1139
|
+
machine.on('records-log', (line) => process.stderr.write(`supercode teams: records: ${line}\n`));
|
|
1139
1140
|
// its mail goes through the daemon's own send, in this process
|
|
1140
1141
|
machineEvents.send = (request) => machine.sendOwn(request);
|
|
1141
1142
|
// the Claude worker homes the roots declare: their live sessions are this machine's too (the live-session map)
|
package/connector/index.mjs
CHANGED
|
@@ -19,8 +19,22 @@ import { NativeEffects } from './native-effects.mjs';
|
|
|
19
19
|
import { consumeAgentClosures } from './agent-closures.mjs';
|
|
20
20
|
|
|
21
21
|
const SYNC_PROBLEM_LIMIT=20;
|
|
22
|
-
// How
|
|
23
|
-
const
|
|
22
|
+
// How long after a session is first seen a change has its launch agent read again (a launch record written late).
|
|
23
|
+
const LAUNCH_AGENT_WINDOW_MS=120000;
|
|
24
|
+
/**
|
|
25
|
+
* The agent variables a session's launch recorded in its harness home (`<home>/supercode/launches/<session id>.json`,
|
|
26
|
+
* crates/harness/src/launch_agent.rs `read`), read here with no request: a Codex rollout's home is the folder above its
|
|
27
|
+
* `sessions/`, a Claude transcript's the folder above its `projects/`. Null when its launch named no agent.
|
|
28
|
+
*/
|
|
29
|
+
export function readLaunchAgent(locator){
|
|
30
|
+
const id=locator?.session_id,path=locator?.storage?.kind==='file'?locator.storage.path:null;
|
|
31
|
+
const store={codex:'sessions','claude-code':'projects'}[locator?.harness];
|
|
32
|
+
if(typeof path!=='string'||!store||typeof id!=='string'||!/^[A-Za-z0-9_-]+$/.test(id))return null;
|
|
33
|
+
let home=null;for(let dir=dirname(path);dirname(dir)!==dir;dir=dirname(dir))if(basename(dir)===store){home=dirname(dir);break;}
|
|
34
|
+
if(!home)return null;
|
|
35
|
+
try{const environment=JSON.parse(readFileSync(join(home,'supercode','launches',`${id}.json`),'utf8'))?.environment;return environment&&typeof environment==='object'&&!Array.isArray(environment)?environment:null;}
|
|
36
|
+
catch{return null;}
|
|
37
|
+
}
|
|
24
38
|
const CAPACITY_CODE='SUBSCRIPTION_OPEN_CAPACITY';
|
|
25
39
|
// Whether a root covers a session's folder: the root's own, or (unless the root is exact-only) one under it, by real path.
|
|
26
40
|
const realOr=path=>{try{return realpathSync(path);}catch{return path;}};
|
|
@@ -640,12 +654,11 @@ export class TeamsConnector {
|
|
|
640
654
|
// the binding is written only while this work is still the session's (checked again after its awaits)
|
|
641
655
|
const bindingRead=this.clock.nowMs();
|
|
642
656
|
try{await this.nativeEffects.discover(descriptor,ruleId,()=>this.sessionLive(key,ruleId,epoch)&&!((this.runtimeEnded?.get(key)??-Infinity)>=bindingRead));}catch(error){this.lastError=error;}
|
|
643
|
-
// The agent this session's launch named, read when first seen here, and
|
|
644
|
-
//
|
|
645
|
-
//
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
if(captured&&!this.state.agent_sessions?.[key]&&asked<LAUNCH_AGENT_ASKS&&(asked===0||changed)){(this.launchChecked??=new Map()).set(key,asked+1);await this.noteLaunchAgent(locator,()=>this.sessionLive(key,ruleId,epoch));}
|
|
657
|
+
// The agent this session's launch named, read (in this process, one file) when first seen here, and on a change in
|
|
658
|
+
// the LAUNCH_AGENT_WINDOW_MS after (a launch record written late), until queued for binding. A launch's agent is
|
|
659
|
+
// fixed at its launch: read on every change it was a call into harness serve's loop per transcript write.
|
|
660
|
+
const firstSeen=this.launchChecked?.get(key),now=this.clock.nowMs();
|
|
661
|
+
if(captured&&!this.state.agent_sessions?.[key]&&(firstSeen===undefined||(changed&&now-firstSeen<LAUNCH_AGENT_WINDOW_MS))){if(firstSeen===undefined)(this.launchChecked??=new Map()).set(key,now);this.noteLaunchAgent(locator,()=>this.sessionLive(key,ruleId,epoch));}
|
|
649
662
|
// A session seen with its native bundle still owed (from before this start) waits for its file to go quiet; one
|
|
650
663
|
// captured now has its wait armed by the commit that records it (flushOutbox).
|
|
651
664
|
if(!changed)this.armNativeIdle(key);
|
|
@@ -1215,11 +1228,9 @@ export class TeamsConnector {
|
|
|
1215
1228
|
if(error.status!==409&&error.status!==404)throw error;
|
|
1216
1229
|
}
|
|
1217
1230
|
}
|
|
1218
|
-
/** The agent a session's launch named, as its home recorded it (
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
let environment;
|
|
1222
|
-
try{environment=await this.daemon.launchAgent(locator);}catch(error){this.lastError=error;return;}
|
|
1231
|
+
/** The agent a session's launch named, as its home recorded it (read here, readLaunchAgent): queued for binding. */
|
|
1232
|
+
noteLaunchAgent(locator,live=()=>true){
|
|
1233
|
+
const environment=readLaunchAgent(locator);
|
|
1223
1234
|
if(!live())return;
|
|
1224
1235
|
if(environment?.SUPERCODE_AGENT_ID)this.queueAgentSession(`${locator.harness}:${locator.session_id}`,environment);
|
|
1225
1236
|
}
|
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, renameSync, watch, writeFileSync } from 'node:fs';
|
|
17
|
+
import { mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, 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';
|
|
@@ -30,6 +30,9 @@ const HISTORY_KEPT = 64;
|
|
|
30
30
|
/** The argument a Codex hook file carries when supercode's mail hook is installed. */
|
|
31
31
|
const CODEX_HOOK = 'message hook codex';
|
|
32
32
|
|
|
33
|
+
/** How long the runtime probes of one claim may take in all (a host stalled by load answers late, not never). */
|
|
34
|
+
const CLAIM_PROBE_MS = 10_000;
|
|
35
|
+
|
|
33
36
|
const alive = (pid) => {
|
|
34
37
|
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
35
38
|
try { process.kill(pid, 0); return true; } catch (error) { return error.code === 'EPERM'; }
|
|
@@ -50,6 +53,9 @@ function linuxStart(pid) {
|
|
|
50
53
|
} catch { return null; }
|
|
51
54
|
}
|
|
52
55
|
|
|
56
|
+
/** `path` with its symlinks resolved, or null when it cannot be (gone, or not a string). */
|
|
57
|
+
function realPath(path) { try { return typeof path === 'string' ? realpathSync(path) : null; } catch { return null; } }
|
|
58
|
+
|
|
53
59
|
/** When the process on `pid` started, in ms (Linux's /proc, or one `ps` on macOS), or null when it cannot be read. */
|
|
54
60
|
function startedMs(pid) {
|
|
55
61
|
if (process.platform === 'linux') { const started = linuxStart(pid); return Promise.resolve(started ? Date.parse(`${started.date} UTC`) : null); }
|
|
@@ -94,11 +100,14 @@ export class LiveSessions extends EventEmitter {
|
|
|
94
100
|
this.homesFile = join(supercodeHome, 'live-claude-homes.json');
|
|
95
101
|
this.markersDir = join(supercodeHome, 'mail', 'message-mcp');
|
|
96
102
|
this.codexHooks = join(env.CODEX_HOME || join(homedir(), '.codex'), 'hooks.json');
|
|
97
|
-
this.boardOwner = boardOwner; this.log = log;
|
|
103
|
+
this.boardOwner = boardOwner; this.log = log; this.startedAt = Date.now();
|
|
98
104
|
this.claude = new Map(); // pid → { pid, dir, sessionId, name, status, cwd, tmux, procStart, history: [ids, oldest first] }
|
|
99
105
|
this.runtimes = new Map(); // receipt id → { pid, harness, sessionId, runtimeSessionId, cwd }
|
|
100
106
|
this.markers = new Map(); // session pid → server pid
|
|
101
107
|
this.codexRows = new Map(); // session id → { cwd, updatedAt }
|
|
108
|
+
// whether the Codex sessions are known yet: the index's first answer read, then their activity followed once after
|
|
109
|
+
// it (until then a Codex session missing from the map is not yet known, never not running)
|
|
110
|
+
this.codexIndexRead = false; this.codexKnown = false; this.codexWaiters = [];
|
|
102
111
|
this.codexRunning = new Map(); // session id → turn (from activity)
|
|
103
112
|
this.codexPaneOf = new Map(); // session id → pane
|
|
104
113
|
this.projectHooks = new Map(); // directory → whether its .codex/hooks.json carries the hook
|
|
@@ -320,7 +329,8 @@ export class LiveSessions extends EventEmitter {
|
|
|
320
329
|
const source = receipt.source;
|
|
321
330
|
const entry = { pid: receipt.pid, harness: source.harness, sessionId: source.session_id, runtimeSessionId: typeof receipt.runtime_session_id === 'string' ? receipt.runtime_session_id : null, cwd: typeof source.workspace === 'string' ? source.workspace : null,
|
|
322
331
|
// what a later check of the same pid needs: when the receipt was written, and how to ask the runtime itself
|
|
323
|
-
|
|
332
|
+
// its workspace as a real path too, so a relay folder named through a symlinked home still matches it
|
|
333
|
+
realCwd: realPath(source.workspace), created: Number(receipt.created_at_ms), probe: { base_url: receipt.base_url, token: receipt.token, receipt_id: id, runtime_session_id: receipt.runtime_session_id } };
|
|
324
334
|
const known = this.runtimes.get(id);
|
|
325
335
|
if (known && known.pid === entry.pid && known.startChecked) entry.startChecked = true;
|
|
326
336
|
this.runtimes.set(id, entry);
|
|
@@ -418,6 +428,41 @@ export class LiveSessions extends EventEmitter {
|
|
|
418
428
|
|
|
419
429
|
#codexChanged() { this.emit('codex-followed'); }
|
|
420
430
|
|
|
431
|
+
/** harness serve ended: its index and activity subscriptions went with it, so the Codex sessions are not known again
|
|
432
|
+
* until it answers anew (a session missing meanwhile is not known yet, never not running). */
|
|
433
|
+
codexForget() {
|
|
434
|
+
if (!this.codexIndexRead && !this.codexKnown) return;
|
|
435
|
+
this.codexIndexRead = false; this.codexKnown = false; this.codexWaitedOut = false; this.startedAt = Date.now();
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/** The session index's first answer is read: its Codex sessions' activity is followed next (even when it has none). */
|
|
439
|
+
codexIndexed() {
|
|
440
|
+
if (!this.codexIndexRead) this.codexIndexedAt = Date.now();
|
|
441
|
+
this.codexIndexRead = true; this.#codexChanged();
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/** The Codex sessions' activity was followed: after the index's first answer, the Codex sessions are known. */
|
|
445
|
+
codexFollowedNow() {
|
|
446
|
+
if (!this.codexIndexRead || this.codexKnown) return;
|
|
447
|
+
this.codexKnown = true; this.codexWaitedOut = false;
|
|
448
|
+
// how long a daemon's start took to know its Codex sessions, and in which part (said once)
|
|
449
|
+
const now = Date.now();
|
|
450
|
+
this.log(`live sessions: Codex sessions known ${Math.round((now - this.startedAt) / 100) / 10} s after start (the session index answered at ${Math.round((this.codexIndexedAt - this.startedAt) / 100) / 10} s; their activity followed ${Math.round((now - this.codexIndexedAt) / 100) / 10} s later, ${this.codexFollowed().length} followed)`);
|
|
451
|
+
for (const resolve of this.codexWaiters.splice(0)) resolve(true);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/** Resolves true once the Codex sessions are known, or false when `boundMs` passes first. Once a wait has run out,
|
|
455
|
+
* later ones answer false at once until they are known: a harness serve that does not answer holds no send twice. */
|
|
456
|
+
whenCodexKnown(boundMs) {
|
|
457
|
+
if (this.codexKnown) return Promise.resolve(true);
|
|
458
|
+
if (this.codexWaitedOut) return Promise.resolve(false);
|
|
459
|
+
return new Promise((resolve) => {
|
|
460
|
+
const timer = setTimeout(() => { this.codexWaiters = this.codexWaiters.filter((waiter) => waiter !== done); this.codexWaitedOut = true; resolve(false); }, boundMs);
|
|
461
|
+
const done = (value) => { clearTimeout(timer); resolve(value); };
|
|
462
|
+
this.codexWaiters.push(done);
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
|
|
421
466
|
#changed() { this.emit('changed'); }
|
|
422
467
|
|
|
423
468
|
// ---- what it holds ------------------------------------------------------------------------------------------------
|
|
@@ -469,7 +514,8 @@ export class LiveSessions extends EventEmitter {
|
|
|
469
514
|
*/
|
|
470
515
|
/** The live runtimes of `harness` whose workspace is `cwd`, each by the ids it may be closed by (in-memory, no read). */
|
|
471
516
|
runtimesIn(harness, cwd) {
|
|
472
|
-
|
|
517
|
+
const real = realPath(cwd);
|
|
518
|
+
return [...this.runtimes.values()].filter((runtime) => runtime.harness === harness && (runtime.cwd === cwd || (real && runtime.realCwd === real)) && alive(runtime.pid))
|
|
473
519
|
.map((runtime) => [...new Set([runtime.sessionId, runtime.runtimeSessionId].filter((id) => typeof id === 'string' && id))]);
|
|
474
520
|
}
|
|
475
521
|
|
|
@@ -517,12 +563,14 @@ export class LiveSessions extends EventEmitter {
|
|
|
517
563
|
|
|
518
564
|
/** The session a client runs in, from its claim: its parent's current session, else its environment's id through the map. */
|
|
519
565
|
async #sessionOfClaim({ pid, ppid, session, runtime }) {
|
|
566
|
+
// the probes of one claim share one bound (CLAIM_PROBE_MS), however many of its processes are runtimes
|
|
567
|
+
const deadline = Date.now() + CLAIM_PROBE_MS;
|
|
520
568
|
// a runtime supercode hosts (the native agent) says so in its claim: it is named by its own process first, never by
|
|
521
569
|
// the session whose shell started it; one with no receipt is named by the rest of its claim, as any client is
|
|
522
570
|
if (runtime === true) {
|
|
523
571
|
// a pid several receipts name is no one runtime: the rest of the claim names it, never a refusal here
|
|
524
572
|
let own = null;
|
|
525
|
-
try { own = await this.#runtimeOfPid(Number(pid)); } catch (error) { if (!(error instanceof CallerRefused) || error.unconfirmed) throw error; }
|
|
573
|
+
try { own = await this.#runtimeOfPid(Number(pid), deadline); } catch (error) { if (!(error instanceof CallerRefused) || error.unconfirmed) throw error; }
|
|
526
574
|
if (own) return own;
|
|
527
575
|
}
|
|
528
576
|
const parent = this.claude.get(Number(ppid));
|
|
@@ -536,26 +584,27 @@ export class LiveSessions extends EventEmitter {
|
|
|
536
584
|
// only when exactly one live runtime's receipt names that process. A process several receipts name (harness serve
|
|
537
585
|
// records its own pid on each runtime it hosts with no child of its own) is no one runtime, and is refused.
|
|
538
586
|
for (const candidate of [Number(pid), Number(ppid)]) {
|
|
539
|
-
const found = await this.#runtimeOfPid(candidate);
|
|
587
|
+
const found = await this.#runtimeOfPid(candidate, deadline);
|
|
540
588
|
if (found) return found;
|
|
541
589
|
}
|
|
542
590
|
return null;
|
|
543
591
|
}
|
|
544
592
|
|
|
545
593
|
/** The runtime whose live receipt alone names process `pid`, or null; several receipts naming it are no one runtime, refused. */
|
|
546
|
-
async #runtimeOfPid(pid) {
|
|
594
|
+
async #runtimeOfPid(pid, deadline = Date.now() + CLAIM_PROBE_MS) {
|
|
547
595
|
if (!Number.isInteger(pid) || pid <= 0) return null;
|
|
548
596
|
const held = [...this.runtimes.values()].filter((runtime) => runtime.pid === pid && alive(runtime.pid));
|
|
549
597
|
if (held.length > 1) throw new CallerRefused(`process ${pid} hosts ${held.length} runtimes, so which one is calling is not known; nothing was done`);
|
|
550
598
|
if (held.length !== 1) return null;
|
|
551
599
|
// a caller's identity: checked again now, with no process (the runtime asked at its own address), as a runtime that
|
|
552
600
|
// ended may have left its pid to another process before the sweep lets its receipt go
|
|
553
|
-
// a host stalled by load answers late, not never: one that did not answer in 2 s is asked once more, for
|
|
554
|
-
|
|
555
|
-
|
|
601
|
+
// a host stalled by load answers late, not never: one that did not answer in 2 s is asked once more, for what is left
|
|
602
|
+
// of the claim's bound
|
|
603
|
+
let answered = await answersAs(held[0].probe, Math.max(1, Math.min(2000, deadline - Date.now())));
|
|
604
|
+
if (answered === null && deadline - Date.now() > 250) answered = await answersAs(held[0].probe, deadline - Date.now());
|
|
556
605
|
// one that did not answer then is not known either way: refused, never taken for its parent's session or its
|
|
557
606
|
// environment's (which would name another caller)
|
|
558
|
-
if (answered === null) throw Object.assign(new CallerRefused(`process ${pid} is a runtime that did not answer within
|
|
607
|
+
if (answered === null) throw Object.assign(new CallerRefused(`process ${pid} is a runtime that did not answer within ${CLAIM_PROBE_MS / 1000} s, so which session is calling was not confirmed; nothing was done`), { unconfirmed: true });
|
|
559
608
|
if (!answered) return null;
|
|
560
609
|
return this.#address(held[0].harness, held[0].sessionId);
|
|
561
610
|
}
|
package/machine.mjs
CHANGED
|
@@ -594,7 +594,7 @@ export class TeamsMachine extends MachineCore {
|
|
|
594
594
|
const followNow = async () => {
|
|
595
595
|
const locators = this.live.codexFollowed();
|
|
596
596
|
const key = locators.map((locator) => locator.session_id).sort().join('\n');
|
|
597
|
-
if (key === followed) return;
|
|
597
|
+
if (key === followed) { this.live.codexFollowedNow(); return; }
|
|
598
598
|
const previous = this.liveActivity;
|
|
599
599
|
try {
|
|
600
600
|
if (locators.length) {
|
|
@@ -603,6 +603,7 @@ export class TeamsMachine extends MachineCore {
|
|
|
603
603
|
this.live.codexActivity(opened?.initial ?? [], { replace: true });
|
|
604
604
|
} else { this.liveActivity = null; this.live.codexActivity([], { replace: true }); }
|
|
605
605
|
followed = key; backoffMs = 0;
|
|
606
|
+
this.live.codexFollowedNow();
|
|
606
607
|
if (previous) void this.#serveRequest('harness.v1.sessions.activity.unsubscribe', { subscription: previous }, 30_000).catch(() => {});
|
|
607
608
|
} catch (error) {
|
|
608
609
|
backoffMs = Math.min(300_000, Math.max(2000, (backoffMs || 1000) * 2));
|
|
@@ -610,12 +611,14 @@ export class TeamsMachine extends MachineCore {
|
|
|
610
611
|
pending = setTimeout(follow, backoffMs); pending.unref?.();
|
|
611
612
|
}
|
|
612
613
|
};
|
|
613
|
-
|
|
614
|
+
// a change is followed after 2 s (changes come in bursts); the first follow after the index's first answer at once,
|
|
615
|
+
// as sends wait on it (the Codex sessions are not known until it is done)
|
|
616
|
+
this.live.on('codex-followed', () => { if (!pending) { pending = setTimeout(follow, this.live.codexKnown ? 2000 : 0); pending.unref?.(); } });
|
|
614
617
|
this.on('internal-notification', (message) => {
|
|
615
618
|
const params = message.params ?? {};
|
|
616
619
|
if (params.subscription && params.subscription === this.liveActivity) this.live.codexActivity(params.activities);
|
|
617
620
|
});
|
|
618
|
-
this.on('serve-exit', () => { this.liveActivity = null; followed = ''; this.live.emit('codex-followed'); });
|
|
621
|
+
this.on('serve-exit', () => { this.liveActivity = null; followed = ''; this.live.codexForget(); this.live.emit('codex-followed'); });
|
|
619
622
|
}
|
|
620
623
|
|
|
621
624
|
// ---- this machine's mail (mail/; docs/architecture/overview.md, "The mail operations") ---------------------------
|
|
@@ -795,6 +798,7 @@ export class TeamsMachine extends MachineCore {
|
|
|
795
798
|
this.sessionDescriptors?.clear();
|
|
796
799
|
for (const descriptor of answer?.initial ?? []) if (descriptor?.locator) { locators.set(`${descriptor.locator.harness}:${descriptor.locator.session_id}`, descriptor.locator); this.live?.codexIndex({ descriptor }); this.sessionDescriptors?.set(`sc:${machineName(this.env)}:${descriptor.locator.harness}:${descriptor.locator.session_id}`, descriptor); }
|
|
797
800
|
producer.note('sessions_index', null); noteIndexParts(answer?.failed, null); both();
|
|
801
|
+
this.live?.codexIndexed();
|
|
798
802
|
}, (error) => {
|
|
799
803
|
indexBackoff = backoff(indexBackoff);
|
|
800
804
|
const why = `the session index could not be subscribed: ${error.message}; sessions and panes change on pane and mail events only until it is`;
|
package/mail/send.mjs
CHANGED
|
@@ -23,6 +23,9 @@ export function unansweredText(where, detail, id) {
|
|
|
23
23
|
return `Unanswered: ${where} took the message, but its answer was lost (${detail}), so whether it was delivered is unknown. Message id ${shortId(id)}. Don't send it again as a new message; supercode message show ${shortId(id)} says where it stands.`;
|
|
24
24
|
}
|
|
25
25
|
|
|
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
|
+
const CODEX_KNOWN_WAIT_MS = 20_000;
|
|
28
|
+
|
|
26
29
|
/** Replies in one unbroken chain between two sessions beyond which another is refused. */
|
|
27
30
|
const REPLY_CHAIN_LIMIT = 32;
|
|
28
31
|
/** How long a name no running session carries still reaches the session that last carried it. */
|
|
@@ -148,7 +151,7 @@ export class MailSend {
|
|
|
148
151
|
}
|
|
149
152
|
if (parsed) {
|
|
150
153
|
const session = sessions.find((row) => row.address === parsed.address);
|
|
151
|
-
if (!session) return { outcome: outcome(EXIT_UNKNOWN, `${to} is no longer running. Nothing was sent. Run supercode message list for the live sessions.`) };
|
|
154
|
+
if (!session) return { outcome: outcome(EXIT_UNKNOWN, this.#notYetKnown(to) ?? `${to} is no longer running. Nothing was sent. Run supercode message list for the live sessions.`) };
|
|
152
155
|
return { address: session.address, name: session.name, session };
|
|
153
156
|
}
|
|
154
157
|
// a name: unique among the sessions here, else the session it last named (the names ledger)
|
|
@@ -176,7 +179,19 @@ export class MailSend {
|
|
|
176
179
|
}).slice(0, 3).map((row) => `${row.name} (${row.address.split(':')[2]}, ${row.status})`);
|
|
177
180
|
if (near.length) hint = ` Did you mean: ${near.join(', ')}?`;
|
|
178
181
|
}
|
|
179
|
-
return { outcome: outcome(EXIT_UNKNOWN, `No session named "${wanted}" is reachable.${hint} Run supercode message list. Nothing was sent.`) };
|
|
182
|
+
return { outcome: outcome(EXIT_UNKNOWN, this.#notYetKnown(wanted) ?? `No session named "${wanted}" is reachable.${hint} Run supercode message list. Nothing was sent.`) };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** In a daemon's first moments the Codex sessions are not known yet (its session index and their activity still being
|
|
186
|
+
* read): a send waits for them, at most CODEX_KNOWN_WAIT_MS. */
|
|
187
|
+
async #codexKnown() { if (!this.live.codexKnown) await this.live.whenCodexKnown?.(CODEX_KNOWN_WAIT_MS); }
|
|
188
|
+
|
|
189
|
+
/** What a receiver not found is, while the Codex sessions are not known yet: not known yet, never not running. */
|
|
190
|
+
#notYetKnown(what) {
|
|
191
|
+
if (this.live.codexKnown !== false) return null;
|
|
192
|
+
return this.live.codexWaitedOut
|
|
193
|
+
? `${what} is not known: this machine's harness serve has not reported its Codex sessions yet (it may be starting again, or not answering; the daemon's log says which). Nothing was sent.`
|
|
194
|
+
: `${what} is not known yet: this machine's daemon started a moment ago and is still reading its sessions (its Codex sessions among them). Nothing was sent. Send it again in a minute.`;
|
|
180
195
|
}
|
|
181
196
|
|
|
182
197
|
/** The door that reaches `address` here, from one read of the live sessions (mail_route.rs `door_in`): null when no
|
|
@@ -195,8 +210,10 @@ export class MailSend {
|
|
|
195
210
|
|
|
196
211
|
/** Deliver from `caller` to `to` on this machine (mail_send.rs `deliver`). */
|
|
197
212
|
async #deliver(caller, to, body, { subject, in_reply_to, notify_when_idle, queue, id, notice, thread: carried, chosen }, link) {
|
|
198
|
-
|
|
199
|
-
|
|
213
|
+
let sessions = this.live.sessions();
|
|
214
|
+
let receiver = this.#receiver(to, sessions);
|
|
215
|
+
// only a receiver not found waits for the Codex sessions to be known (one the map holds is reached at once)
|
|
216
|
+
if (receiver.outcome && !this.live.codexKnown) { await this.#codexKnown(); sessions = this.live.sessions(); receiver = this.#receiver(to, sessions); }
|
|
200
217
|
if (receiver.outcome) return receiver.outcome;
|
|
201
218
|
const { address, name } = receiver;
|
|
202
219
|
if (address === caller.address) return outcome(EXIT_REFUSED, 'Not sent: that address is your own session.');
|
|
@@ -226,7 +243,7 @@ export class MailSend {
|
|
|
226
243
|
if (plan.plan) return this.#deliverPlanned(caller, sent, plan.plan, { queue, notify_when_idle });
|
|
227
244
|
// the thread this message is in, kept in the send log: a later reply to it continues that thread
|
|
228
245
|
const sentIn = threadOf(sent);
|
|
229
|
-
if (!door || door.kind === 'other_machine') return outcome(EXIT_UNKNOWN, `${name} is no longer running. Nothing was sent. Run supercode message list for the live sessions.`);
|
|
246
|
+
if (!door || door.kind === 'other_machine') return outcome(EXIT_UNKNOWN, this.#notYetKnown(name) ?? `${name} is no longer running. Nothing was sent. Run supercode message list for the live sessions.`);
|
|
230
247
|
const idleNote = notify_when_idle && cameThroughLink(sent)
|
|
231
248
|
? ' No idle notice: this message came through a link, so its return address is as claimed, and nothing is sent to it on its own (D200); a reply is the receiver\'s own send.'
|
|
232
249
|
: notify_when_idle ? ' Subscribed: one idle notice reaches you when its next turn ends.' : '';
|
|
@@ -444,6 +461,8 @@ export class MailSend {
|
|
|
444
461
|
const receiver = typeof harness === 'string' && typeof sessionId === 'string' ? makeAddress(this.machine, harness, sessionId) : null;
|
|
445
462
|
if (!receiver) return refused('delivery_failed', `\`sc:${this.machine}:${harness}:${sessionId}\` is not a session address; addresses look like sc:<machine>:<harness>:<session-id>`);
|
|
446
463
|
if (params.voice_for != null && params.voice_for !== receiver) return refused('invalid_sender', 'a voice front must represent the receiving session');
|
|
464
|
+
// only a receiver the map does not hold waits for the Codex sessions to be known
|
|
465
|
+
if (!this.live.codexKnown && !this.live.sessions().some((row) => row.address === receiver)) await this.#codexKnown();
|
|
447
466
|
const sessions = this.live.sessions();
|
|
448
467
|
// a Claude session's registry name must still name only that session; a holder the mailbox launched may have stopped
|
|
449
468
|
if (harness === 'claude-code' && !(await this.agents.resumable(receiver)) && !this.live.runtimeReceipt('claude-code', sessionId)) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/supercode-teams",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.93",
|
|
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",
|