@volter/supercode-teams 0.3.89 → 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
@@ -61,14 +61,16 @@ function startedMs(pid) {
61
61
  }
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
- * token): the proof a start read off the wall clock cannot give, as the clock may have stepped since the receipt. */
65
- async function answersAs(receipt) {
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 `boundMs` (the request is aborted then): not known either way. */
66
+ async function answersAs(receipt, boundMs = 2000) {
66
67
  try {
67
68
  const client = new FrontendClient({ baseUrl: receipt.base_url, token: receipt.token, clientId: `live-${receipt.receipt_id}`, permissions: ['observe'] });
68
- let timer;
69
- const bound = new Promise((resolve) => { timer = setTimeout(() => resolve(null), 2000); });
70
- try { return (await Promise.race([client.describe(), bound]))?.session_id === receipt.runtime_session_id; } finally { clearTimeout(timer); }
71
- } catch { return false; }
69
+ return (await client.describe({ signal: AbortSignal.timeout(boundMs) }))?.session_id === receipt.runtime_session_id;
70
+ } catch (error) {
71
+ if (error?.name === 'TimeoutError' || error?.name === 'AbortError') return null;
72
+ return false;
73
+ }
72
74
  }
73
75
 
74
76
  /** Why a claim names no caller. */
@@ -343,7 +345,7 @@ export class LiveSessions extends EventEmitter {
343
345
  if (!alive(entry.pid)) return false;
344
346
  const started = await startedMs(entry.pid);
345
347
  if (!Number.isFinite(started) || !Number.isFinite(entry.created) || started <= entry.created + 2000) return true;
346
- return answersAs(entry.probe);
348
+ return (await answersAs(entry.probe)) !== false;
347
349
  }
348
350
 
349
351
  /** One messaging-tools marker: the session process named by its file, the server pid in it. */
@@ -465,6 +467,12 @@ export class LiveSessions extends EventEmitter {
465
467
  * file and checked as live_runtime.rs `resolve_live_runtime` checks it (its schema, a loopback address, its process
466
468
  * alive, the source it names): the bearer token a delivery uses is read here, at the delivery, never kept in the map.
467
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
+
468
476
  runtimeReceipt(harness, id) {
469
477
  for (const [receiptId, runtime] of this.runtimes) {
470
478
  if (runtime.harness !== harness || (runtime.sessionId !== id && runtime.runtimeSessionId !== id) || !alive(runtime.pid)) continue;
@@ -514,7 +522,7 @@ export class LiveSessions extends EventEmitter {
514
522
  if (runtime === true) {
515
523
  // a pid several receipts name is no one runtime: the rest of the claim names it, never a refusal here
516
524
  let own = null;
517
- try { own = await this.#runtimeOfPid(Number(pid)); } catch (error) { if (!(error instanceof CallerRefused)) throw error; }
525
+ try { own = await this.#runtimeOfPid(Number(pid)); } catch (error) { if (!(error instanceof CallerRefused) || error.unconfirmed) throw error; }
518
526
  if (own) return own;
519
527
  }
520
528
  const parent = this.claude.get(Number(ppid));
@@ -542,7 +550,13 @@ export class LiveSessions extends EventEmitter {
542
550
  if (held.length !== 1) return null;
543
551
  // a caller's identity: checked again now, with no process (the runtime asked at its own address), as a runtime that
544
552
  // ended may have left its pid to another process before the sweep lets its receipt go
545
- if (!(await answersAs(held[0].probe))) return null;
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
557
+ // 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 });
559
+ if (!answered) return null;
546
560
  return this.#address(held[0].harness, held[0].sessionId);
547
561
  }
548
562
 
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
  }
@@ -75,8 +78,43 @@ export class Relays {
75
78
  for (const [directory, relay] of kept.slice(RELAYS_RUNNING)) {
76
79
  if (this.sending.has(directory)) continue;
77
80
  this.running.delete(directory);
78
- void this.serve('harness.v1.runtimes.close', { connection: relay.connection, runtime_id: relay.runtime_id }, 15_000)
79
- .catch((error) => this.log(`relay ${directory.split(/[\\/]/).pop()} beyond the ${RELAYS_RUNNING} most recent was not closed: ${error.message}`));
81
+ // closed under the relay's own lock: a send to it that arrives meanwhile waits for the close, then starts it
82
+ // again, never handing its turn to a runtime that is closing
83
+ void this.#locked(directory, () => this.serve('harness.v1.runtimes.close', { connection: relay.connection, runtime_id: relay.runtime_id }, 15_000))
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
+ }
103
+ }
104
+ }
105
+
106
+ /** `work` run under the relay folder's lock: one send, or one close, at a time per relay, in arrival order. */
107
+ async #locked(directory, work) {
108
+ const previous = this.sending.get(directory) ?? Promise.resolve();
109
+ let release;
110
+ const mine = new Promise((resolve) => { release = resolve; });
111
+ const turn = previous.then(() => mine);
112
+ this.sending.set(directory, turn);
113
+ await previous;
114
+ try { return await work(); }
115
+ finally {
116
+ release();
117
+ if (this.sending.get(directory) === turn) this.sending.delete(directory);
80
118
  }
81
119
  }
82
120
 
@@ -127,8 +165,9 @@ export class Relays {
127
165
 
128
166
  /** The relay's runtime, started in this daemon's harness serve when it is not running (claude_relay.rs
129
167
  * `ensure_relay_runtime`): `{ runtime (its receipt), connection }`. */
130
- async #ensure(spec) {
168
+ async #ensure(spec, step = () => {}) {
131
169
  const endpoint = await this.endpoint();
170
+ step('relay endpoint');
132
171
  if (!endpoint) throw new Error('the relay endpoint is not answering; the machine daemon\'s `supercode message watch` serves it (see mail/machine-daemon.log)');
133
172
  // written on every send: Claude Code reloads a changed settings file, so a running relay takes the hooks this
134
173
  // supercode installs
@@ -137,18 +176,21 @@ export class Relays {
137
176
  if (record?.runtime_id) {
138
177
  const runtime = this.runtimeOf('claude-code', record.runtime_id);
139
178
  if (runtime && alive(this.#pid(spec, runtime)) && record.connection && (record.endpoint === endpoint || await answers(Number(String(record.endpoint ?? '').split(':').pop())))) {
179
+ step('runtime reused');
140
180
  return { runtime, connection: record.connection };
141
181
  }
182
+ // why it is not reused, said in the timing line: a relay that should have been warm and was not shows here
183
+ step(`runtime not reused (${!runtime ? 'no live receipt' : !alive(this.#pid(spec, runtime)) ? 'its process ended' : !record.connection ? 'no connection' : 'endpoint moved'})`);
142
184
  // closed through the owner's door even after its receipt went
143
185
  try { await this.serve('harness.v1.runtimes.close', { connection: record.connection, runtime_id: record.runtime_id }, 15_000); }
144
186
  catch (error) { if (this.runtimeOf('claude-code', record.runtime_id)) throw new Error(`could not retire the old relay: ${error.message}`); }
145
187
  }
146
188
  const key = spec.directory.split(/[\\/]/).pop();
147
- const started = await this.serve('harness.v1.runtimes.start', {
189
+ const started = await this.#start(spec, {
148
190
  harness: 'claude-code',
149
191
  launch: {
150
192
  program: 'claude',
151
- arguments: ['--print', '--input-format', 'stream-json', '--output-format', 'stream-json', '--verbose', '--permission-prompt-tool', 'stdio', '--model', RELAY_MODEL, '--name', spec.name, '--permission-mode', 'bypassPermissions', '--setting-sources', 'project', '--settings', spec.settings, '--tools', 'ListAgents,SendMessage', '--no-session-persistence'],
193
+ arguments: ['--print', '--input-format', 'stream-json', '--output-format', 'stream-json', '--verbose', '--permission-prompt-tool', 'stdio', '--model', RELAY_MODEL, '--name', spec.name, '--permission-mode', 'bypassPermissions', '--setting-sources', 'project', '--settings', spec.settings, '--tools', 'ListAgents,SendMessage', '--no-session-persistence', '--strict-mcp-config'],
152
194
  // its API is the relay endpoint, its key the relay's folder there (which also keeps account connectors out)
153
195
  env: { ANTHROPIC_API_KEY: key, ANTHROPIC_BASE_URL: endpoint, CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: '1', SUPERCODE_CLAUDE_RELAY: '1' },
154
196
  },
@@ -158,18 +200,27 @@ export class Relays {
158
200
  if (typeof runtimeId !== 'string') throw new Error(`the machine daemon did not start the relay: ${JSON.stringify(started)}`);
159
201
  const connection = typeof started.connection === 'string' ? started.connection : '';
160
202
  const pid = Number.isInteger(started?.handle?.endpoint?.pid) ? started.handle.endpoint.pid : null;
203
+ step('runtime started (runtimes.start)');
161
204
  writeFileSync(spec.record, JSON.stringify({ runtime_id: runtimeId, pid, endpoint, connection }));
205
+ // the daemon's from its start: counted by the cap even when the rest of this start fails
206
+ this.running.set(spec.directory, { connection, runtime_id: runtimeId, usedAt: Date.now() });
162
207
  // harness serve writes its receipt before it answers the start; the map learns it from its watch a moment later, so
163
208
  // one it does not hold yet is read by its own name (the receipt the runtime's terminal launch opens), never waited on
164
209
  const known = this.runtimeOf('claude-code', runtimeId);
165
- if (known) return { runtime: known, connection };
210
+ if (known) { step('its receipt held'); return { runtime: known, connection }; }
166
211
  let receiptId = null;
167
212
  try {
168
213
  const launch = (await this.serve('harness.v1.runtimes.terminal_instructions', { connection }, 5000))?.launch;
169
214
  receiptId = (Array.isArray(launch?.arguments) ? launch.arguments : []).find((arg) => typeof arg === 'string' && arg.startsWith('supercode-live://'))?.slice('supercode-live://'.length) ?? null;
170
215
  } catch { /* said below */ }
171
216
  const runtime = receiptId ? this.runtimeNamed(receiptId) : null;
172
- if (!runtime) throw new Error('the relay started but registered no live runtime');
217
+ step('its receipt read');
218
+ if (!runtime) {
219
+ // a runtime no send can use is closed, never left running untracked
220
+ this.running.delete(spec.directory);
221
+ try { await this.serve('harness.v1.runtimes.close', { connection, runtime_id: runtimeId }, 15_000); } catch { /* said below */ }
222
+ throw new Error('the relay started but registered no live runtime; it was closed');
223
+ }
173
224
  return { runtime, connection };
174
225
  }
175
226
 
@@ -199,20 +250,19 @@ export class Relays {
199
250
  async send(sender, senderName, receiver, message, messageId) {
200
251
  const spec = this.spec(sender, senderName);
201
252
  mkdirSync(spec.directory, { recursive: true });
202
- const previous = this.sending.get(spec.directory) ?? Promise.resolve();
203
- let release;
204
- const mine = new Promise((resolve) => { release = resolve; });
205
- const turn = previous.then(() => mine);
206
- this.sending.set(spec.directory, turn);
207
- await previous;
253
+ const asked = performance.now();
254
+ try { return await this.#locked(spec.directory, () => this.#send(spec, receiver, message, messageId, asked)); }
255
+ finally { this.#cap(); }
256
+ }
257
+
258
+ async #send(spec, receiver, message, messageId, asked) {
208
259
  try {
209
260
  const queued = { to: receiver.socket ? `uds:${receiver.socket}` : receiver.name, message };
210
- const steps = [], began = performance.now();
261
+ const steps = [], began = asked;
211
262
  let mark = began;
212
263
  const step = (name) => { const now = performance.now(); steps.push(`${name} ${Math.round(now - mark)} ms`); mark = now; };
213
264
  step('lock');
214
- let { runtime, connection } = await this.#ensure(spec);
215
- step('runtime (started or reused)');
265
+ let { runtime, connection } = await this.#ensure(spec, step);
216
266
  this.#used(spec, connection);
217
267
  rmSync(spec.receipt, { force: true });
218
268
  writeFileSync(spec.queue, JSON.stringify(queued));
@@ -223,7 +273,7 @@ export class Relays {
223
273
  const pid = this.#pid(spec, runtime);
224
274
  // only a relay proven dead is started again; one alive but slow may still deliver
225
275
  if (receipt.delivered || attempt === 1 || pid == null || alive(pid)) break;
226
- try { ({ runtime, connection } = await this.#ensure(spec)); this.#used(spec, connection); } catch (error) { receipt = { delivered: false, detail: error.message }; break; }
276
+ try { ({ runtime, connection } = await this.#ensure(spec, step)); this.#used(spec, connection); } catch (error) { receipt = { delivered: false, detail: error.message }; break; }
227
277
  rmSync(spec.receipt, { force: true });
228
278
  }
229
279
  rmSync(spec.queue, { force: true });
@@ -238,13 +288,16 @@ export class Relays {
238
288
  } catch (error) {
239
289
  rmSync(spec.queue, { force: true });
240
290
  return { delivered: false, detail: error.message };
241
- } finally {
242
- release();
243
- if (this.sending.get(spec.directory) === turn) this.sending.delete(spec.directory);
244
- this.#cap();
245
291
  }
246
292
  }
247
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
+
248
301
  /** A relay this daemon runs, used now. */
249
302
  #used(spec, connection) {
250
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.89",
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",