@volter/supercode-teams 0.3.90 → 0.3.91

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.
@@ -19,6 +19,8 @@ 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
24
  const CAPACITY_CODE='SUBSCRIPTION_OPEN_CAPACITY';
23
25
  // 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.
24
26
  const realOr=path=>{try{return realpathSync(path);}catch{return path;}};
@@ -638,8 +640,12 @@ export class TeamsConnector {
638
640
  // the binding is written only while this work is still the session's (checked again after its awaits)
639
641
  const bindingRead=this.clock.nowMs();
640
642
  try{await this.nativeEffects.discover(descriptor,ruleId,()=>this.sessionLive(key,ruleId,epoch)&&!((this.runtimeEnded?.get(key)??-Infinity)>=bindingRead));}catch(error){this.lastError=error;}
641
- // The agent this session's launch named, read when first seen here and whenever it changes, until queued for binding.
642
- if(captured&&!this.state.agent_sessions?.[key]&&(changed||!this.launchChecked?.has(key))){(this.launchChecked??=new Set()).add(key);await this.noteLaunchAgent(locator,()=>this.sessionLive(key,ruleId,epoch));}
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));}
643
649
  // A session seen with its native bundle still owed (from before this start) waits for its file to go quiet; one
644
650
  // captured now has its wait armed by the commit that records it (flushOutbox).
645
651
  if(!changed)this.armNativeIdle(key);
package/live-sessions.mjs CHANGED
@@ -62,11 +62,11 @@ function startedMs(pid) {
62
62
 
63
63
  /** Whether the runtime a receipt names answers at its address as that runtime (its own identity, with the receipt's
64
64
  * token): the proof a start read off the wall clock cannot give, as the clock may have stepped since the receipt.
65
- * true or false, or null when it did not answer within 2 s (the request is aborted then): not known either way. */
66
- async function answersAs(receipt) {
65
+ * true or false, or null when it did not answer within `boundMs` (the request is aborted then): not known either way. */
66
+ async function answersAs(receipt, boundMs = 2000) {
67
67
  try {
68
68
  const client = new FrontendClient({ baseUrl: receipt.base_url, token: receipt.token, clientId: `live-${receipt.receipt_id}`, permissions: ['observe'] });
69
- return (await client.describe({ signal: AbortSignal.timeout(2000) }))?.session_id === receipt.runtime_session_id;
69
+ return (await client.describe({ signal: AbortSignal.timeout(boundMs) }))?.session_id === receipt.runtime_session_id;
70
70
  } catch (error) {
71
71
  if (error?.name === 'TimeoutError' || error?.name === 'AbortError') return null;
72
72
  return false;
@@ -467,6 +467,12 @@ export class LiveSessions extends EventEmitter {
467
467
  * file and checked as live_runtime.rs `resolve_live_runtime` checks it (its schema, a loopback address, its process
468
468
  * alive, the source it names): the bearer token a delivery uses is read here, at the delivery, never kept in the map.
469
469
  */
470
+ /** The live runtimes of `harness` whose workspace is `cwd`, each by the ids it may be closed by (in-memory, no read). */
471
+ runtimesIn(harness, cwd) {
472
+ return [...this.runtimes.values()].filter((runtime) => runtime.harness === harness && runtime.cwd === cwd && alive(runtime.pid))
473
+ .map((runtime) => [...new Set([runtime.sessionId, runtime.runtimeSessionId].filter((id) => typeof id === 'string' && id))]);
474
+ }
475
+
470
476
  runtimeReceipt(harness, id) {
471
477
  for (const [receiptId, runtime] of this.runtimes) {
472
478
  if (runtime.harness !== harness || (runtime.sessionId !== id && runtime.runtimeSessionId !== id) || !alive(runtime.pid)) continue;
@@ -544,10 +550,12 @@ export class LiveSessions extends EventEmitter {
544
550
  if (held.length !== 1) return null;
545
551
  // a caller's identity: checked again now, with no process (the runtime asked at its own address), as a runtime that
546
552
  // ended may have left its pid to another process before the sweep lets its receipt go
547
- const answered = await answersAs(held[0].probe);
548
- // one that did not answer in time is not known either way: refused, never taken for its parent's session or its
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);
556
+ // one that did not answer then is not known either way: refused, never taken for its parent's session or its
549
557
  // environment's (which would name another caller)
550
- if (answered === null) throw Object.assign(new CallerRefused(`process ${pid} is a runtime that did not answer within 2 s, so which session is calling was not confirmed; nothing was done`), { unconfirmed: true });
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 });
551
559
  if (!answered) return null;
552
560
  return this.#address(held[0].harness, held[0].sessionId);
553
561
  }
package/machine.mjs CHANGED
@@ -645,6 +645,7 @@ export class TeamsMachine extends MachineCore {
645
645
  recordedConfig: (locator) => this.#serveRequest('harness.v1.sessions.recorded_config', { locator }, 15_000) });
646
646
  this.relays = new Relays({ mailRoot, machine, supercodeBin: this.supercodeBin, serve: (method, params, timeoutMs) => this.#serveRequest(method, params, timeoutMs),
647
647
  runtimeOf: (harness, id) => live.runtimeReceipt(harness, id), runtimeNamed: (receiptId) => live.receiptNamed(receiptId),
648
+ runtimesIn: (harness, cwd) => live.runtimesIn(harness, cwd),
648
649
  log: (line) => this.emit('mail-log', line) });
649
650
  this.mailSend = new MailSend({ machine, mailRoot, live, store: this.mailStore, agents: this.agents, relays: this.relays,
650
651
  // a send waits on its receiver's door there (a relay's receipt takes up to 90 s), never past MAIL_SEND_MS
package/mail/relay.mjs CHANGED
@@ -25,6 +25,8 @@ export const MAX_RELAYED_BYTES = 100_000;
25
25
  /** How many relays run at once, the most recently used (a relay runtime is some hundreds of MB; 10 held 2.1 GB on one
26
26
  * machine): beyond them a relay's runtime is closed (its folder kept: it starts again when next needed). */
27
27
  export const RELAYS_RUNNING = 4;
28
+ /** How long a relay whose runtime start lost its answer is looked at for a runtime that start left running. */
29
+ const LOST_START_KEPT_MS = 5 * 60_000;
28
30
 
29
31
  const alive = (pid) => { if (!Number.isInteger(pid) || pid <= 0) return false; try { process.kill(pid, 0); return true; } catch (error) { return error.code === 'EPERM'; } };
30
32
  const readJson = (path) => { try { return JSON.parse(readFileSync(path, 'utf8')); } catch { return null; } };
@@ -61,8 +63,9 @@ export class Relays {
61
63
  * id)` the live-runtime receipt the live-session map holds for a runtime id (its file read whole), or null;
62
64
  * `runtimeNamed(receiptId)` one receipt read by its own name.
63
65
  */
64
- constructor({ mailRoot, machine, supercodeBin, serve, runtimeOf, runtimeNamed = () => null, log = () => {} }) {
65
- Object.assign(this, { mailRoot, machine, supercodeBin, serve, runtimeOf, runtimeNamed, log });
66
+ constructor({ mailRoot, machine, supercodeBin, serve, runtimeOf, runtimeNamed = () => null, runtimesIn = () => [], log = () => {} }) {
67
+ Object.assign(this, { mailRoot, machine, supercodeBin, serve, runtimeOf, runtimeNamed, runtimesIn, log });
68
+ this.lost = new Map(); // relay folder → when a start of its runtime lost its answer (the runtime may run, unnamed)
66
69
  this.sending = new Map(); // relay folder → the send in progress through it (one at a time per relay)
67
70
  this.running = new Map(); // relay folder → { connection, runtime_id, usedAt }: the relays this daemon runs
68
71
  }
@@ -78,7 +81,25 @@ export class Relays {
78
81
  // closed under the relay's own lock: a send to it that arrives meanwhile waits for the close, then starts it
79
82
  // again, never handing its turn to a runtime that is closing
80
83
  void this.#locked(directory, () => this.serve('harness.v1.runtimes.close', { connection: relay.connection, runtime_id: relay.runtime_id }, 15_000))
81
- .catch((error) => this.log(`relay ${directory.split(/[\\/]/).pop()} beyond the ${RELAYS_RUNNING} most recent was not closed: ${error.message}`));
84
+ .catch((error) => {
85
+ // still running, so still counted: the next cap closes it again (unless a send started it anew meanwhile)
86
+ if (!this.running.has(directory)) this.running.set(directory, relay);
87
+ this.log(`relay ${directory.split(/[\\/]/).pop()} beyond the ${RELAYS_RUNNING} most recent was not closed: ${error.message}`);
88
+ });
89
+ }
90
+ // a start whose answer was lost may have left a runtime running in its relay's folder that no record names: closed by
91
+ // its id once the live-session map holds it (looked for over five minutes: the start may finish after its answer)
92
+ for (const [directory, lostAt] of this.lost) {
93
+ if (Date.now() - lostAt > LOST_START_KEPT_MS) { this.lost.delete(directory); continue; }
94
+ if (this.sending.has(directory)) continue;
95
+ const named = readJson(join(directory, 'relay.json'))?.runtime_id;
96
+ for (const ids of this.runtimesIn('claude-code', directory)) {
97
+ if (ids.includes(named)) continue;
98
+ void this.#locked(directory, async () => {
99
+ for (const id of ids) { try { await this.serve('harness.v1.runtimes.close', { runtime_id: id }, 15_000); return; } catch { /* the other id */ } }
100
+ this.log(`relay ${directory.split(/[\\/]/).pop()}: a runtime its lost start left running was not closed`);
101
+ });
102
+ }
82
103
  }
83
104
  }
84
105
 
@@ -165,7 +186,7 @@ export class Relays {
165
186
  catch (error) { if (this.runtimeOf('claude-code', record.runtime_id)) throw new Error(`could not retire the old relay: ${error.message}`); }
166
187
  }
167
188
  const key = spec.directory.split(/[\\/]/).pop();
168
- const started = await this.serve('harness.v1.runtimes.start', {
189
+ const started = await this.#start(spec, {
169
190
  harness: 'claude-code',
170
191
  launch: {
171
192
  program: 'claude',
@@ -270,6 +291,13 @@ export class Relays {
270
291
  }
271
292
  }
272
293
 
294
+ /** `runtimes.start` for a relay: a start that fails (its answer lost, or late past its bound) may still have left a
295
+ * runtime running, so its folder is looked at by the next caps (#cap). */
296
+ async #start(spec, params, timeoutMs) {
297
+ try { return await this.serve('harness.v1.runtimes.start', params, timeoutMs); }
298
+ catch (error) { this.lost.set(spec.directory, Date.now()); throw error; }
299
+ }
300
+
273
301
  /** A relay this daemon runs, used now. */
274
302
  #used(spec, connection) {
275
303
  const runtimeId = readJson(spec.record)?.runtime_id;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/supercode-teams",
3
- "version": "0.3.90",
3
+ "version": "0.3.91",
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",