@volter/supercode-teams 0.3.89 → 0.3.90

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/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. */
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. */
65
66
  async function answersAs(receipt) {
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(2000) }))?.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. */
@@ -514,7 +516,7 @@ export class LiveSessions extends EventEmitter {
514
516
  if (runtime === true) {
515
517
  // a pid several receipts name is no one runtime: the rest of the claim names it, never a refusal here
516
518
  let own = null;
517
- try { own = await this.#runtimeOfPid(Number(pid)); } catch (error) { if (!(error instanceof CallerRefused)) throw error; }
519
+ try { own = await this.#runtimeOfPid(Number(pid)); } catch (error) { if (!(error instanceof CallerRefused) || error.unconfirmed) throw error; }
518
520
  if (own) return own;
519
521
  }
520
522
  const parent = this.claude.get(Number(ppid));
@@ -542,7 +544,11 @@ export class LiveSessions extends EventEmitter {
542
544
  if (held.length !== 1) return null;
543
545
  // a caller's identity: checked again now, with no process (the runtime asked at its own address), as a runtime that
544
546
  // 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;
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
549
+ // 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 });
551
+ if (!answered) return null;
546
552
  return this.#address(held[0].harness, held[0].sessionId);
547
553
  }
548
554
 
package/mail/relay.mjs CHANGED
@@ -75,11 +75,28 @@ export class Relays {
75
75
  for (const [directory, relay] of kept.slice(RELAYS_RUNNING)) {
76
76
  if (this.sending.has(directory)) continue;
77
77
  this.running.delete(directory);
78
- void this.serve('harness.v1.runtimes.close', { connection: relay.connection, runtime_id: relay.runtime_id }, 15_000)
78
+ // closed under the relay's own lock: a send to it that arrives meanwhile waits for the close, then starts it
79
+ // again, never handing its turn to a runtime that is closing
80
+ void this.#locked(directory, () => this.serve('harness.v1.runtimes.close', { connection: relay.connection, runtime_id: relay.runtime_id }, 15_000))
79
81
  .catch((error) => this.log(`relay ${directory.split(/[\\/]/).pop()} beyond the ${RELAYS_RUNNING} most recent was not closed: ${error.message}`));
80
82
  }
81
83
  }
82
84
 
85
+ /** `work` run under the relay folder's lock: one send, or one close, at a time per relay, in arrival order. */
86
+ async #locked(directory, work) {
87
+ const previous = this.sending.get(directory) ?? Promise.resolve();
88
+ let release;
89
+ const mine = new Promise((resolve) => { release = resolve; });
90
+ const turn = previous.then(() => mine);
91
+ this.sending.set(directory, turn);
92
+ await previous;
93
+ try { return await work(); }
94
+ finally {
95
+ release();
96
+ if (this.sending.get(directory) === turn) this.sending.delete(directory);
97
+ }
98
+ }
99
+
83
100
  /** The relay that speaks for `represented`, known as `representedName`: its name and its files. */
84
101
  spec(represented, representedName) {
85
102
  const name = relayName(String(representedName).split('@')[0], String(represented).split(':')[1]);
@@ -127,8 +144,9 @@ export class Relays {
127
144
 
128
145
  /** The relay's runtime, started in this daemon's harness serve when it is not running (claude_relay.rs
129
146
  * `ensure_relay_runtime`): `{ runtime (its receipt), connection }`. */
130
- async #ensure(spec) {
147
+ async #ensure(spec, step = () => {}) {
131
148
  const endpoint = await this.endpoint();
149
+ step('relay endpoint');
132
150
  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
151
  // written on every send: Claude Code reloads a changed settings file, so a running relay takes the hooks this
134
152
  // supercode installs
@@ -137,8 +155,11 @@ export class Relays {
137
155
  if (record?.runtime_id) {
138
156
  const runtime = this.runtimeOf('claude-code', record.runtime_id);
139
157
  if (runtime && alive(this.#pid(spec, runtime)) && record.connection && (record.endpoint === endpoint || await answers(Number(String(record.endpoint ?? '').split(':').pop())))) {
158
+ step('runtime reused');
140
159
  return { runtime, connection: record.connection };
141
160
  }
161
+ // why it is not reused, said in the timing line: a relay that should have been warm and was not shows here
162
+ step(`runtime not reused (${!runtime ? 'no live receipt' : !alive(this.#pid(spec, runtime)) ? 'its process ended' : !record.connection ? 'no connection' : 'endpoint moved'})`);
142
163
  // closed through the owner's door even after its receipt went
143
164
  try { await this.serve('harness.v1.runtimes.close', { connection: record.connection, runtime_id: record.runtime_id }, 15_000); }
144
165
  catch (error) { if (this.runtimeOf('claude-code', record.runtime_id)) throw new Error(`could not retire the old relay: ${error.message}`); }
@@ -148,7 +169,7 @@ export class Relays {
148
169
  harness: 'claude-code',
149
170
  launch: {
150
171
  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'],
172
+ 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
173
  // its API is the relay endpoint, its key the relay's folder there (which also keeps account connectors out)
153
174
  env: { ANTHROPIC_API_KEY: key, ANTHROPIC_BASE_URL: endpoint, CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: '1', SUPERCODE_CLAUDE_RELAY: '1' },
154
175
  },
@@ -158,18 +179,27 @@ export class Relays {
158
179
  if (typeof runtimeId !== 'string') throw new Error(`the machine daemon did not start the relay: ${JSON.stringify(started)}`);
159
180
  const connection = typeof started.connection === 'string' ? started.connection : '';
160
181
  const pid = Number.isInteger(started?.handle?.endpoint?.pid) ? started.handle.endpoint.pid : null;
182
+ step('runtime started (runtimes.start)');
161
183
  writeFileSync(spec.record, JSON.stringify({ runtime_id: runtimeId, pid, endpoint, connection }));
184
+ // the daemon's from its start: counted by the cap even when the rest of this start fails
185
+ this.running.set(spec.directory, { connection, runtime_id: runtimeId, usedAt: Date.now() });
162
186
  // harness serve writes its receipt before it answers the start; the map learns it from its watch a moment later, so
163
187
  // one it does not hold yet is read by its own name (the receipt the runtime's terminal launch opens), never waited on
164
188
  const known = this.runtimeOf('claude-code', runtimeId);
165
- if (known) return { runtime: known, connection };
189
+ if (known) { step('its receipt held'); return { runtime: known, connection }; }
166
190
  let receiptId = null;
167
191
  try {
168
192
  const launch = (await this.serve('harness.v1.runtimes.terminal_instructions', { connection }, 5000))?.launch;
169
193
  receiptId = (Array.isArray(launch?.arguments) ? launch.arguments : []).find((arg) => typeof arg === 'string' && arg.startsWith('supercode-live://'))?.slice('supercode-live://'.length) ?? null;
170
194
  } catch { /* said below */ }
171
195
  const runtime = receiptId ? this.runtimeNamed(receiptId) : null;
172
- if (!runtime) throw new Error('the relay started but registered no live runtime');
196
+ step('its receipt read');
197
+ if (!runtime) {
198
+ // a runtime no send can use is closed, never left running untracked
199
+ this.running.delete(spec.directory);
200
+ try { await this.serve('harness.v1.runtimes.close', { connection, runtime_id: runtimeId }, 15_000); } catch { /* said below */ }
201
+ throw new Error('the relay started but registered no live runtime; it was closed');
202
+ }
173
203
  return { runtime, connection };
174
204
  }
175
205
 
@@ -199,20 +229,19 @@ export class Relays {
199
229
  async send(sender, senderName, receiver, message, messageId) {
200
230
  const spec = this.spec(sender, senderName);
201
231
  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;
232
+ const asked = performance.now();
233
+ try { return await this.#locked(spec.directory, () => this.#send(spec, receiver, message, messageId, asked)); }
234
+ finally { this.#cap(); }
235
+ }
236
+
237
+ async #send(spec, receiver, message, messageId, asked) {
208
238
  try {
209
239
  const queued = { to: receiver.socket ? `uds:${receiver.socket}` : receiver.name, message };
210
- const steps = [], began = performance.now();
240
+ const steps = [], began = asked;
211
241
  let mark = began;
212
242
  const step = (name) => { const now = performance.now(); steps.push(`${name} ${Math.round(now - mark)} ms`); mark = now; };
213
243
  step('lock');
214
- let { runtime, connection } = await this.#ensure(spec);
215
- step('runtime (started or reused)');
244
+ let { runtime, connection } = await this.#ensure(spec, step);
216
245
  this.#used(spec, connection);
217
246
  rmSync(spec.receipt, { force: true });
218
247
  writeFileSync(spec.queue, JSON.stringify(queued));
@@ -223,7 +252,7 @@ export class Relays {
223
252
  const pid = this.#pid(spec, runtime);
224
253
  // only a relay proven dead is started again; one alive but slow may still deliver
225
254
  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; }
255
+ try { ({ runtime, connection } = await this.#ensure(spec, step)); this.#used(spec, connection); } catch (error) { receipt = { delivered: false, detail: error.message }; break; }
227
256
  rmSync(spec.receipt, { force: true });
228
257
  }
229
258
  rmSync(spec.queue, { force: true });
@@ -238,10 +267,6 @@ export class Relays {
238
267
  } catch (error) {
239
268
  rmSync(spec.queue, { force: true });
240
269
  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
270
  }
246
271
  }
247
272
 
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.90",
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",