@volter/supercode-teams 0.3.92 → 0.3.94

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)
package/live-sessions.mjs CHANGED
@@ -100,7 +100,7 @@ export class LiveSessions extends EventEmitter {
100
100
  this.homesFile = join(supercodeHome, 'live-claude-homes.json');
101
101
  this.markersDir = join(supercodeHome, 'mail', 'message-mcp');
102
102
  this.codexHooks = join(env.CODEX_HOME || join(homedir(), '.codex'), 'hooks.json');
103
- this.boardOwner = boardOwner; this.log = log;
103
+ this.boardOwner = boardOwner; this.log = log; this.startedAt = Date.now();
104
104
  this.claude = new Map(); // pid → { pid, dir, sessionId, name, status, cwd, tmux, procStart, history: [ids, oldest first] }
105
105
  this.runtimes = new Map(); // receipt id → { pid, harness, sessionId, runtimeSessionId, cwd }
106
106
  this.markers = new Map(); // session pid → server pid
@@ -428,21 +428,41 @@ export class LiveSessions extends EventEmitter {
428
428
 
429
429
  #codexChanged() { this.emit('codex-followed'); }
430
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
+ // what the ended serve said is not kept as what is true now: a Codex session that ended in the gap would be filed for
437
+ // a door it no longer has; until serve answers anew, a Codex session is not known yet (a send to it waits)
438
+ const had = this.codexRunning.size + this.codexPaneOf.size;
439
+ this.codexRunning.clear(); this.codexPaneOf = new Map();
440
+ if (had) this.#changed();
441
+ }
442
+
431
443
  /** The session index's first answer is read: its Codex sessions' activity is followed next (even when it has none). */
432
- codexIndexed() { this.codexIndexRead = true; this.#codexChanged(); }
444
+ codexIndexed() {
445
+ if (!this.codexIndexRead) this.codexIndexedAt = Date.now();
446
+ this.codexIndexRead = true; this.#codexChanged();
447
+ }
433
448
 
434
449
  /** The Codex sessions' activity was followed: after the index's first answer, the Codex sessions are known. */
435
450
  codexFollowedNow() {
436
451
  if (!this.codexIndexRead || this.codexKnown) return;
437
- this.codexKnown = true;
452
+ this.codexKnown = true; this.codexWaitedOut = false;
453
+ // how long a daemon's start took to know its Codex sessions, and in which part (said once)
454
+ const now = Date.now();
455
+ 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)`);
438
456
  for (const resolve of this.codexWaiters.splice(0)) resolve(true);
439
457
  }
440
458
 
441
- /** Resolves true once the Codex sessions are known, or false when `boundMs` passes first. */
459
+ /** Resolves true once the Codex sessions are known, or false when `boundMs` passes first. Once a wait has run out,
460
+ * later ones answer false at once until they are known: a harness serve that does not answer holds no send twice. */
442
461
  whenCodexKnown(boundMs) {
443
462
  if (this.codexKnown) return Promise.resolve(true);
463
+ if (this.codexWaitedOut) return Promise.resolve(false);
444
464
  return new Promise((resolve) => {
445
- const timer = setTimeout(() => { this.codexWaiters = this.codexWaiters.filter((waiter) => waiter !== done); resolve(false); }, boundMs);
465
+ const timer = setTimeout(() => { this.codexWaiters = this.codexWaiters.filter((waiter) => waiter !== done); this.codexWaitedOut = true; resolve(false); }, boundMs);
446
466
  const done = (value) => { clearTimeout(timer); resolve(value); };
447
467
  this.codexWaiters.push(done);
448
468
  });
@@ -21,6 +21,8 @@ const MIN_READ_MS = 2000;
21
21
  export const BEAT_MS = 15_000;
22
22
  export const STALE_MS = 45_000;
23
23
  const backoff = (previous) => Math.min(300_000, Math.max(2000, (previous || 1000) * 2));
24
+ /** How often a machine whose daemon is too old to serve records is asked again (its update is what answers it). */
25
+ const TOO_OLD_RETRY_MS = 600_000;
24
26
 
25
27
  /**
26
28
  * The producer: `readers[section](detail)` reads one section (async), `detail` naming what changed when the event knows
@@ -231,8 +233,10 @@ export class RecordHolder {
231
233
  held.attempt = null;
232
234
  Object.assign(held, { stream: null, state, since: Date.now(), why: String(why ?? 'the stream closed').slice(0, 300) });
233
235
  try { stream?.close?.(); } catch { /* closing */ }
234
- // opened again after a backoff, 2 s doubling to 5 min: an obligation of this machine, never a sweep
235
- held.backoff = backoff(held.backoff);
236
+ // opened again after a backoff, 2 s doubling to 5 min: an obligation of this machine, never a sweep. A daemon too
237
+ // old to serve records is asked again every TOO_OLD_RETRY_MS: only its update answers it (asked every 8-16 s, it
238
+ // filled the log)
239
+ held.backoff = state === 'too-old' ? TOO_OLD_RETRY_MS : backoff(held.backoff);
236
240
  if (this.closed) return;
237
241
  held.retry = setTimeout(() => { held.retry = null; if ((held.state === 'closed' || held.state === 'too-old') && !this.closed) this.subscribe(machine); }, held.backoff);
238
242
  held.retry.unref?.();
package/machine.mjs CHANGED
@@ -611,12 +611,14 @@ export class TeamsMachine extends MachineCore {
611
611
  pending = setTimeout(follow, backoffMs); pending.unref?.();
612
612
  }
613
613
  };
614
- 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?.(); } });
615
617
  this.on('internal-notification', (message) => {
616
618
  const params = message.params ?? {};
617
619
  if (params.subscription && params.subscription === this.liveActivity) this.live.codexActivity(params.activities);
618
620
  });
619
- 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'); });
620
622
  }
621
623
 
622
624
  // ---- this machine's mail (mail/; docs/architecture/overview.md, "The mail operations") ---------------------------
package/mail/send.mjs CHANGED
@@ -189,7 +189,9 @@ export class MailSend {
189
189
  /** What a receiver not found is, while the Codex sessions are not known yet: not known yet, never not running. */
190
190
  #notYetKnown(what) {
191
191
  if (this.live.codexKnown !== false) return null;
192
- return `${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.`;
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.`;
193
195
  }
194
196
 
195
197
  /** The door that reaches `address` here, from one read of the live sessions (mail_route.rs `door_in`): null when no
@@ -208,9 +210,10 @@ export class MailSend {
208
210
 
209
211
  /** Deliver from `caller` to `to` on this machine (mail_send.rs `deliver`). */
210
212
  async #deliver(caller, to, body, { subject, in_reply_to, notify_when_idle, queue, id, notice, thread: carried, chosen }, link) {
211
- await this.#codexKnown();
212
- const sessions = this.live.sessions();
213
- 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); }
214
217
  if (receiver.outcome) return receiver.outcome;
215
218
  const { address, name } = receiver;
216
219
  if (address === caller.address) return outcome(EXIT_REFUSED, 'Not sent: that address is your own session.');
@@ -458,7 +461,8 @@ export class MailSend {
458
461
  const receiver = typeof harness === 'string' && typeof sessionId === 'string' ? makeAddress(this.machine, harness, sessionId) : null;
459
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>`);
460
463
  if (params.voice_for != null && params.voice_for !== receiver) return refused('invalid_sender', 'a voice front must represent the receiving session');
461
- await this.#codexKnown();
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();
462
466
  const sessions = this.live.sessions();
463
467
  // a Claude session's registry name must still name only that session; a holder the mailbox launched may have stopped
464
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.92",
3
+ "version": "0.3.94",
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",