@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 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)
@@ -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 many times a session's launch agent is asked of harness serve: when first seen, and after a change at most twice more.
23
- const LAUNCH_AGENT_ASKS=3;
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 after a change at most twice more (a launch
644
- // record written just after its session appeared), until queued for binding. Never on every change: a launch's agent
645
- // is fixed at the launch, and asking on every transcript write of every live session kept harness serve's loop
646
- // busy with more than one call a second (yuerans: 1357 calls over a second each in ten minutes).
647
- const asked=this.launchChecked?.get(key)??0;
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 (`harness.v1.sessions.launch_agent`): queued for binding. */
1219
- async noteLaunchAgent(locator,live=()=>true){
1220
- if(typeof this.daemon?.launchAgent!=='function')return;
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
- 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 } };
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
- return [...this.runtimes.values()].filter((runtime) => runtime.harness === harness && runtime.cwd === cwd && alive(runtime.pid))
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 8 s
554
- let answered = await answersAs(held[0].probe);
555
- if (answered === null) answered = await answersAs(held[0].probe, 8000);
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 10 s, so which session is calling was not confirmed; nothing was done`), { unconfirmed: true });
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
- this.live.on('codex-followed', () => { if (!pending) { pending = setTimeout(follow, 2000); pending.unref?.(); } });
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
- const sessions = this.live.sessions();
199
- const receiver = this.#receiver(to, sessions);
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.91",
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",