@volter/supercode-teams 0.3.172 → 0.3.174

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/machine.mjs CHANGED
@@ -39,6 +39,7 @@ import { checkRemoteClose } from './remote-close.mjs';
39
39
  import { SqliteMailStore } from './mail/sqlite-store.mjs';
40
40
  import { SessionIndexOwner, requireSessionIndexRuntime } from './session-index-owner.mjs';
41
41
  import { SessionIndexViews, SESSION_INDEX_METHODS, SESSION_INDEX_CAPABILITY } from './session-index-views.mjs';
42
+ import { SessionReadClient, SESSION_READ_METHODS, SESSION_READ_EVENT, SESSION_READ_CAPABILITY, parseSessionWire, stringifySessionWire } from './session-read-client.mjs';
42
43
  import { projectLiveDescriptors, addDiscoveryActivities, displayRecordLive } from './session-index-live.mjs';
43
44
  import { MailReads, panePrompt } from './mail/reads.mjs';
44
45
  import { render } from './mail/envelope.mjs';
@@ -1271,7 +1272,8 @@ export class TeamsMachine extends MachineCore {
1271
1272
  this.#stopMailWorkers();
1272
1273
  const closingViews = this.indexViews?.close();
1273
1274
  const closingIndex = this.sessionIndex?.close();
1274
- await Promise.all([closingViews, closingIndex]);
1275
+ const closingReads = [...(this.sessionReadClients ?? [])].map((client) => client.close());
1276
+ await Promise.all([closingViews, closingIndex, ...closingReads]);
1275
1277
  this.mailStore?.stop();
1276
1278
  this.stoppingServe = true;
1277
1279
  clearTimeout(this.serveRestart);
@@ -1380,6 +1382,15 @@ export class TeamsMachine extends MachineCore {
1380
1382
  /** One `harness.v1` request of the daemon's own, to its `harness serve`: the
1381
1383
  * connection id INTERNAL_CONNECTION marks the answer as ours. */
1382
1384
  #serveRequest(method, params, timeoutMs = 3000) {
1385
+ if (SESSION_READ_METHODS.includes(method)) {
1386
+ if (this.stopping) return Promise.reject(new Error('this machine daemon is stopping'));
1387
+ if (!this.sessionReads) {
1388
+ const client = this.#newSessionReads(); this.sessionReads = client;
1389
+ client.on('notification', (frame) => this.emit('internal-notification', frame));
1390
+ client.on('ended', (error) => { if (this.sessionReads === client) this.sessionReads = null; this.emit('serve-stderr', `session read service ended: ${error.message}\n`); });
1391
+ }
1392
+ return this.sessionReads.call(method, params, { timeoutMs });
1393
+ }
1383
1394
  if (SESSION_INDEX_METHODS.includes(method)) return new Promise((resolve, reject) => {
1384
1395
  let ended = false;
1385
1396
  const timer = setTimeout(() => { ended = true; reject(new Error(`${method} timed out`)); }, timeoutMs);
@@ -1447,9 +1458,9 @@ export class TeamsMachine extends MachineCore {
1447
1458
  const method = entry.harnessRequests?.get(original);
1448
1459
  entry.harnessRequests?.delete(original);
1449
1460
  if (method === 'harness.v1.capabilities' && message.result) message.result = { ...message.result,
1450
- methods: [...new Set([...(message.result.methods ?? []), ...SESSION_INDEX_METHODS])],
1451
- notifications: [...new Set([...(message.result.notifications ?? []), 'harness.v1.sessions.index_event'])],
1452
- session_index: SESSION_INDEX_CAPABILITY };
1461
+ methods: [...new Set([...(message.result.methods ?? []), ...SESSION_INDEX_METHODS, ...SESSION_READ_METHODS])],
1462
+ notifications: [...new Set([...(message.result.notifications ?? []), 'harness.v1.sessions.index_event', SESSION_READ_EVENT])],
1463
+ session_index: SESSION_INDEX_CAPABILITY, session_read: SESSION_READ_CAPABILITY };
1453
1464
  // Learn routing keys from results so later notifications find their owner.
1454
1465
  if (message.result && typeof message.result === 'object') {
1455
1466
  for (const key of ['subscription', 'connection', 'runtime_id', 'subscription_id']) {
@@ -1527,7 +1538,7 @@ export class TeamsMachine extends MachineCore {
1527
1538
  }
1528
1539
  if (stream.closed) return;
1529
1540
  let message;
1530
- try { message = JSON.parse(line); } catch { return; }
1541
+ try { message = parseSessionWire(line); } catch { return; }
1531
1542
  const asked = message.id !== undefined && message.id !== null ? message.id : null;
1532
1543
  if (SESSION_INDEX_METHODS.includes(message.method)) {
1533
1544
  // A cold read on one owner source cannot hold this stream's
@@ -1535,6 +1546,10 @@ export class TeamsMachine extends MachineCore {
1535
1546
  void this.#answerIndex(entry, stream, message);
1536
1547
  return;
1537
1548
  }
1549
+ if (SESSION_READ_METHODS.includes(message.method)) {
1550
+ void this.#answerRead(entry, stream, message);
1551
+ return;
1552
+ }
1538
1553
  if (asked !== null) message.id = joinId(index, message.id);
1539
1554
  if (asked !== null && message.method === 'harness.v1.runtimes.close' && !message.params?.connection && typeof message.params?.runtime_id === 'string' && this.closeOwnedRuntime) {
1540
1555
  try {
@@ -1560,7 +1575,60 @@ export class TeamsMachine extends MachineCore {
1560
1575
  }).catch(() => { if (!stream.closed) stream.close('machine authority could not be verified'); });
1561
1576
  }
1562
1577
  });
1563
- stream.on('close', () => { this.indexViews?.releaseConnection(stream); entry.harness = null; entry.harnessRequests.clear(); entry.harnessNative = false; });
1578
+ stream.on('close', () => {
1579
+ this.indexViews?.releaseConnection(stream);
1580
+ void entry.sessionReads?.close().catch((error) => this.emit('serve-stderr', `${error.message}\n`)); entry.sessionReads = null;
1581
+ entry.harness = null; entry.harnessRequests.clear(); entry.harnessNative = false;
1582
+ });
1583
+ }
1584
+
1585
+ #newSessionReads() {
1586
+ const client = new SessionReadClient({ env: this.env, cwd: process.cwd() });
1587
+ (this.sessionReadClients ??= new Set()).add(client);
1588
+ client.on('closed', () => this.sessionReadClients.delete(client));
1589
+ client.on('shutdown-error', (error) => this.emit('serve-stderr', `${error.message}\n`));
1590
+ return client;
1591
+ }
1592
+ #readOutput(entry, stream, frame) {
1593
+ if (stream.closed || this.stopping) return Promise.resolve(false);
1594
+ const line = stringifySessionWire(frame), bytes = Buffer.byteLength(line);
1595
+ entry.sessionReadBuffered = (entry.sessionReadBuffered ?? 0) + bytes;
1596
+ if (entry.sessionReadBuffered > 16 * 1024 * 1024 && frame.method === SESSION_READ_EVENT) {
1597
+ entry.sessionReadBuffered -= bytes; stream.close('session read notifications exceeded the stream buffer'); return Promise.resolve(false);
1598
+ }
1599
+ entry.sessionReadOutput = (entry.sessionReadOutput ?? Promise.resolve()).then(async () => {
1600
+ if (stream.closed || this.stopping) return false;
1601
+ if (entry.verify) {
1602
+ const fresh = await entry.verify();
1603
+ if (fresh?.class !== 'operator') { stream.close('no live operator grant'); return false; }
1604
+ entry.admitted = fresh;
1605
+ }
1606
+ if (stream.closed || entry.admitted?.class !== 'operator') return false;
1607
+ writeLine(stream, line); return true;
1608
+ }).catch(() => { if (!stream.closed) stream.close('machine authority could not be verified'); return false; })
1609
+ .finally(() => { entry.sessionReadBuffered -= bytes; });
1610
+ return entry.sessionReadOutput;
1611
+ }
1612
+ async #answerRead(entry, stream, message) {
1613
+ const asked = message.id !== undefined && message.id !== null ? message.id : null;
1614
+ if (message.jsonrpc !== '2.0' || asked !== null && typeof asked !== 'string' && !Number.isFinite(asked)) {
1615
+ if (!stream.closed) writeLine(stream, JSON.stringify({ jsonrpc: '2.0', id: null, error: { code: -32600, message: 'invalid JSON-RPC request' } }));
1616
+ return;
1617
+ }
1618
+ try {
1619
+ if (stream.closed || this.stopping) return;
1620
+ if (!entry.sessionReads) {
1621
+ const client = this.#newSessionReads(); entry.sessionReads = client;
1622
+ client.on('notification', (frame) => { void this.#readOutput(entry, stream, frame); });
1623
+ client.on('ended', (error) => { if (!stream.closed) stream.close(`session read service ended: ${error.message}`); });
1624
+ }
1625
+ const client = entry.sessionReads, answer = await client.submit(message);
1626
+ if (asked !== null && await this.#readOutput(entry, stream, answer)
1627
+ && message.method === 'harness.v1.sessions.follow' && answer.result?.subscription) client.activate(answer.result.subscription);
1628
+ } catch (error) {
1629
+ if (asked !== null && !stream.closed) await this.#readOutput(entry, stream, { jsonrpc: '2.0', id: asked,
1630
+ error: { code: error.code ?? -32000, message: error.message } });
1631
+ }
1564
1632
  }
1565
1633
 
1566
1634
  async #indexClaudeName(query, name) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/supercode-teams",
3
- "version": "0.3.172",
3
+ "version": "0.3.174",
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",
@@ -12,6 +12,10 @@
12
12
  "types": "./session-index-client.d.ts",
13
13
  "default": "./session-index-client.mjs"
14
14
  },
15
+ "./session-read-client": {
16
+ "types": "./session-read-client.d.ts",
17
+ "default": "./session-read-client.mjs"
18
+ },
15
19
  "./live-sessions": "./live-sessions.mjs",
16
20
  "./codex-hooks": {
17
21
  "types": "./codex-hooks.d.ts",
@@ -57,6 +61,7 @@
57
61
  "workspace-client.d.ts",
58
62
  "codex-hooks.d.ts",
59
63
  "session-index-client.d.ts",
64
+ "session-read-client.d.ts",
60
65
  "porthole-hook.cjs",
61
66
  "bin",
62
67
  "server",
@@ -79,7 +84,7 @@
79
84
  "@volter/supercode-frontend": "^0.2.2",
80
85
  "@volter/supercode-harness-sdk": "^0.3.58",
81
86
  "@volter/supercode-health": "^0.1.0",
82
- "@volter/supercode-interchange": "0.1.1",
87
+ "@volter/supercode-interchange": "0.1.3",
83
88
  "@volter/supercode-terminal": "^0.2.54",
84
89
  "@volter/supercode-trace": "^0.1.0",
85
90
  "@volter/teams": "0.2.45",
@@ -0,0 +1,11 @@
1
+ import { EventEmitter } from 'node:events';
2
+ import type { IndexRpcFrame } from './session-index-client.js';
3
+ export { parseSessionWire, stringifySessionWire, SESSION_READ_METHODS, SESSION_READ_EVENT, SESSION_READ_CAPABILITY } from '@volter/supercode-interchange/session-read';
4
+ /** Caller-owned worker lanes; never forwards a read to native or to a daemon. */
5
+ export class SessionReadClient extends EventEmitter {
6
+ constructor(options?: { env?: Record<string, string | undefined>; cwd?: string; pollMs?: number | string });
7
+ submit(request: IndexRpcFrame, onAnswer?: ((answer: IndexRpcFrame) => void) | null): Promise<IndexRpcFrame>;
8
+ call(method: string, params?: unknown, options?: { timeoutMs?: number | null }): Promise<unknown>;
9
+ activate(subscription: string): void;
10
+ close(): Promise<void>;
11
+ }
@@ -0,0 +1,109 @@
1
+ import { EventEmitter } from 'node:events';
2
+ import { Worker } from 'node:worker_threads';
3
+ import { parseSessionWire, stringifySessionWire, SESSION_READ_METHODS, SESSION_READ_EVENT, SESSION_READ_CAPABILITY } from '@volter/supercode-interchange/session-read';
4
+ import { SessionRuntimeStates } from './session-runtime-state.mjs';
5
+
6
+ export { parseSessionWire, stringifySessionWire, SESSION_READ_METHODS, SESSION_READ_EVENT, SESSION_READ_CAPABILITY };
7
+ const hasId = (request) => request.id !== undefined && request.id !== null;
8
+
9
+ export class SessionReadClient extends EventEmitter {
10
+ #env; #cwd; #pollMs; #workers = new Map(); #pending = new Map(); #sources = new Map();
11
+ #next = 1; #subscription = 1; #states; #timer; #statePolling = false; #closed = false; #stopping = null;
12
+ constructor({ env = process.env, cwd = process.cwd(), pollMs = 500 } = {}) {
13
+ super(); this.#env = { ...env }; this.#cwd = cwd;
14
+ this.#pollMs = Math.min(0x7fffffff, Math.max(1, Number(pollMs)));
15
+ this.#states = new SessionRuntimeStates(this.#env);
16
+ this.#timer = setInterval(() => { void this.#pollStates(); }, this.#pollMs); this.#timer.unref();
17
+ }
18
+ #worker(lane) {
19
+ if (this.#closed) throw new Error('session-read client is closed');
20
+ let worker = this.#workers.get(lane); if (worker) return worker;
21
+ worker = new Worker(new URL('./session-read-worker.mjs', import.meta.url), { name: `supercode-session-${lane}`, env: this.#env,
22
+ workerData: { lane, env: this.#env, cwd: this.#cwd, pollMs: this.#pollMs } });
23
+ this.#workers.set(lane, worker);
24
+ worker.on('message', (frame) => {
25
+ if (this.#closed) return;
26
+ try {
27
+ if (frame.fatal) throw new Error(frame.fatal);
28
+ if (frame.notification) {
29
+ const notification = parseSessionWire(frame.notification);
30
+ if (this.#sources.get(notification.params?.subscription)?.active) this.emit('notification', notification);
31
+ return;
32
+ }
33
+ const held = this.#pending.get(frame.id); if (!held || held.worker !== worker) return;
34
+ const answer = frame.error ? { jsonrpc: '2.0', id: held.request.id, error: frame.error }
35
+ : { jsonrpc: '2.0', id: held.request.id, result: parseSessionWire(frame.result) };
36
+ this.#pending.delete(frame.id);
37
+ if (answer.result && hasId(held.request) && held.request.method === 'harness.v1.sessions.follow') {
38
+ const locator = held.request.params.locator;
39
+ this.#sources.set(answer.result.subscription, { harness: locator.harness, session_id: locator.session_id, reported: null, active: false });
40
+ }
41
+ if (answer.result?.removed && held.request.method === 'harness.v1.sessions.unfollow') this.#sources.delete(held.request.params.subscription);
42
+ try {
43
+ held.onAnswer?.(answer);
44
+ if (held.onAnswer && held.request.method === 'harness.v1.sessions.follow' && answer.result?.subscription && hasId(held.request)) this.activate(answer.result.subscription);
45
+ held.resolve(answer);
46
+ } catch (error) { held.reject(error); }
47
+ } catch (error) { this.#failed(error); }
48
+ });
49
+ worker.on('error', (error) => this.#failed(error));
50
+ worker.on('exit', (code) => { if (!this.#closed) this.#failed(new Error(`session ${lane} worker exited (${code})`)); });
51
+ return worker;
52
+ }
53
+ activate(subscription) {
54
+ const source = this.#sources.get(subscription); if (!source || this.#closed || source.active) return;
55
+ source.active = true; this.#worker('follow').postMessage({ activate: subscription });
56
+ }
57
+ async call(method, params = {}, { timeoutMs = null } = {}) {
58
+ let timer;
59
+ try {
60
+ const work = this.submit({ jsonrpc: '2.0', id: 1, method, params });
61
+ const answer = timeoutMs == null ? await work : await Promise.race([work, new Promise((_, reject) => {
62
+ timer = setTimeout(() => { this.#failed(new Error(`${method} did not answer within ${timeoutMs} ms`)); reject(new Error(`${method} timed out`)); }, timeoutMs);
63
+ })]);
64
+ if (answer.error) throw Object.assign(new Error(answer.error.message), { code: answer.error.code, answered: true });
65
+ if (method === 'harness.v1.sessions.follow' && answer.result?.subscription) this.activate(answer.result.subscription);
66
+ return answer.result;
67
+ } finally { clearTimeout(timer); }
68
+ }
69
+ #failed(error) { if (this.#closed) return; void this.close().catch((failure) => this.emit('shutdown-error', failure)); this.emit('ended', error); }
70
+ submit(request, onAnswer) {
71
+ if (this.#closed) return Promise.reject(new Error('session-read client is closed'));
72
+ if (!SESSION_READ_METHODS.includes(request.method)) return Promise.reject(new Error(`unknown session-read method: ${request.method}`));
73
+ const worker = this.#worker(request.method === 'harness.v1.sessions.load' ? 'load' : 'follow'), id = this.#next++;
74
+ return new Promise((resolve, reject) => {
75
+ this.#pending.set(id, { worker, request, onAnswer, resolve, reject });
76
+ try { worker.postMessage({ id, method: request.method, params: stringifySessionWire(request.params ?? {}), retain: hasId(request),
77
+ subscription: `read-sub-${this.#subscription++}` }); }
78
+ catch (error) { this.#pending.delete(id); reject(error); }
79
+ });
80
+ }
81
+ async #pollStates() {
82
+ if (this.#closed || this.#statePolling || !this.#sources.size) return;
83
+ this.#statePolling = true;
84
+ try {
85
+ const selected = new Map([...this.#sources].filter(([, source]) => source.active)), states = await this.#states.read([...selected.values()]);
86
+ if (this.#closed) return;
87
+ for (const [subscription, source] of selected) {
88
+ if (this.#sources.get(subscription) !== source) continue;
89
+ const state = states.get(JSON.stringify([source.harness, source.session_id]));
90
+ if (state == null || state === source.reported) continue;
91
+ source.reported = state;
92
+ this.emit('notification', { jsonrpc: '2.0', method: SESSION_READ_EVENT, params: { subscription, event: { type: 'runtime_state', state } } });
93
+ }
94
+ } catch { /* a registry read failure establishes no transition */ }
95
+ finally { this.#statePolling = false; }
96
+ }
97
+ close() {
98
+ if (this.#stopping) return this.#stopping;
99
+ this.#closed = true; clearInterval(this.#timer); this.#states.close(); this.#sources.clear();
100
+ for (const held of this.#pending.values()) held.reject(new Error('session-read client closed'));
101
+ this.#pending.clear();
102
+ this.#stopping = Promise.all([...this.#workers.values()].map(async (worker) => {
103
+ let timer;
104
+ try { await Promise.race([worker.terminate(), new Promise((_, reject) => { timer = setTimeout(() => reject(new Error(`session-read worker ${worker.threadId} did not end after shutdown`)), 1500); })]); }
105
+ finally { clearTimeout(timer); }
106
+ })).then(() => { this.#workers.clear(); this.emit('closed'); });
107
+ return this.#stopping;
108
+ }
109
+ }
@@ -0,0 +1,51 @@
1
+ // One caller-owned worker per read lane; no binary, daemon, or credentials
2
+ // outside that caller's process. A long load cannot hold the stdio router.
3
+ import { parentPort, workerData } from 'node:worker_threads';
4
+ import { loadSessionRead, openSessionReadFollower, parseSessionWire, stringifySessionWire, SESSION_READ_EVENT } from '@volter/supercode-interchange/session-read';
5
+
6
+ const followers = new Map(), context = { env: workerData.env, cwd: workerData.cwd };
7
+ let closing = false, queue = Promise.resolve(), polling = false;
8
+ const post = (frame) => { if (!closing) parentPort.postMessage(frame); };
9
+ const enqueue = (work) => { queue = queue.then(work).catch((error) => post({ fatal: error.message })); };
10
+ const timer = workerData.lane === 'follow' ? setInterval(() => {
11
+ if (closing || polling || !followers.size) return;
12
+ polling = true;
13
+ enqueue(async () => {
14
+ try {
15
+ for (const [subscription, { follower, active }] of followers) {
16
+ if (closing) return;
17
+ if (!active) continue;
18
+ let event;
19
+ try { event = await follower.poll(); }
20
+ catch (error) { event = { type: 'watch_error', recoverable: true, message: error.message }; }
21
+ if (event) post({ notification: stringifySessionWire({ jsonrpc: '2.0', method: SESSION_READ_EVENT, params: { subscription, event } }) });
22
+ }
23
+ } finally { polling = false; }
24
+ });
25
+ }, workerData.pollMs) : null;
26
+
27
+ parentPort.on('message', (request) => {
28
+ if (closing) return;
29
+ enqueue(async () => {
30
+ if (closing) return;
31
+ try {
32
+ if (request.activate) { const held = followers.get(request.activate); if (held) held.active = true; return; }
33
+ const params = parseSessionWire(request.params);
34
+ let result;
35
+ if (request.method === 'harness.v1.sessions.load') result = await loadSessionRead(params, context);
36
+ else if (request.method === 'harness.v1.sessions.follow') {
37
+ const follower = await openSessionReadFollower(params, context);
38
+ if (closing) { follower.close(); return; }
39
+ const initial = await follower.poll();
40
+ if (request.retain) followers.set(request.subscription, { follower, active: false }); else follower.close();
41
+ result = { subscription: request.subscription, initial };
42
+ } else if (request.method === 'harness.v1.sessions.unfollow') {
43
+ if (typeof params?.subscription !== 'string') throw Object.assign(new Error('subscription must be a string'), { rpcCode: -32602 });
44
+ const held = followers.get(params.subscription);
45
+ held?.follower.close(); result = { removed: followers.delete(params.subscription) };
46
+ } else throw Object.assign(new Error(`unknown session-read method: ${request.method}`), { rpcCode: -32601 });
47
+ post({ id: request.id, result: stringifySessionWire(result) });
48
+ } catch (error) { post({ id: request.id, error: { code: error.rpcCode ?? -32000, message: error.message } }); }
49
+ });
50
+ });
51
+ parentPort.on('close', () => { closing = true; clearInterval(timer); for (const { follower } of followers.values()) follower.close(); followers.clear(); });
@@ -0,0 +1,110 @@
1
+ // Read-only source lifecycle projection for sessions.follow. The retained
2
+ // registry still owns receipt retirement; this reader never removes a receipt
3
+ // or touches the runtime's controller, observer lease, or event stream.
4
+ import { readdir, readFile, realpath } from 'node:fs/promises';
5
+ import { userInfo } from 'node:os';
6
+ import { join } from 'node:path';
7
+ import { request, Agent } from 'node:http';
8
+ import { lookup } from 'node:dns';
9
+ import { parseSessionWire } from '@volter/supercode-interchange/session-read';
10
+
11
+ const loopback = (address) => address === '127.0.0.1' || address === '::1';
12
+ function receiptHome(env) {
13
+ if (env.SUPERCODE_HOME) return env.SUPERCODE_HOME;
14
+ if (env.XDG_CONFIG_HOME) return join(env.XDG_CONFIG_HOME, 'supercode');
15
+ let accountHome; try { accountHome = userInfo().homedir; } catch { accountHome = '.'; }
16
+ return join(env.HOME || (process.platform === 'win32' && env.USERPROFILE) || accountHome || '.', '.config', 'supercode');
17
+ }
18
+ const canonical = async (path) => { try { return await realpath(path); } catch { return path; } };
19
+ function live(pid) {
20
+ if (!Number.isInteger(pid) || pid < 1 || pid > 0xffffffff) return false;
21
+ try { process.kill(pid, 0); return true; } catch (error) { return process.platform !== 'win32' && error.code === 'EPERM'; }
22
+ }
23
+ function address(value) {
24
+ let url; try { url = new URL(value); } catch { return null; }
25
+ return url.protocol === 'http:' && !url.username && !url.password && !url.search && !url.hash
26
+ && ['127.0.0.1', 'localhost', '[::1]'].includes(url.hostname) ? url : null;
27
+ }
28
+ function localLookup(host, options, callback) {
29
+ lookup(host, options, (error, resolved, family) => {
30
+ if (error) return callback(error);
31
+ if (options.all) {
32
+ const local = resolved.filter((item) => loopback(item.address));
33
+ return local.length ? callback(null, local) : callback(new Error('runtime receipt hostname did not resolve to loopback'));
34
+ }
35
+ return loopback(resolved) ? callback(null, resolved, family) : callback(new Error('runtime receipt hostname did not resolve to loopback'));
36
+ });
37
+ }
38
+
39
+ export class SessionRuntimeStates {
40
+ #directory; #agent = new Agent({ keepAlive: true, maxSockets: 8, lookup: localLookup });
41
+ #requests = new Set(); #closed = false; #nextId = 1;
42
+ constructor(env) { this.#directory = join(receiptHome(env), 'live-runtimes'); }
43
+ async #receipt(path) {
44
+ const receipt = parseSessionWire(await readFile(path, 'utf8'));
45
+ if (receipt?.schema !== 'supercode.live-runtime.v1' || !address(receipt.base_url) || !live(receipt.pid)
46
+ || !/^[0-9a-f]{32}$/.test(receipt.receipt_id) || typeof receipt.runtime_session_id !== 'string'
47
+ || typeof receipt.token !== 'string' || !receipt.token || typeof receipt.source?.harness !== 'string'
48
+ || typeof receipt.source?.session_id !== 'string' || typeof receipt.source?.workspace !== 'string'
49
+ || !Number.isFinite(receipt.created_at_ms) || receipt.created_at_ms < 0) throw new Error('not a live runtime receipt');
50
+ return receipt;
51
+ }
52
+ async #describe(receipt) {
53
+ if (this.#closed) throw new Error('runtime-state reader closed');
54
+ const url = address(`${receipt.base_url.replace(/\/+$/, '')}/rpc`); if (!url) throw new Error('invalid local runtime endpoint');
55
+ const id = this.#nextId++, body = JSON.stringify({ id, method: 'frontend.v2.describe', params: {} });
56
+ return new Promise((resolveResult, reject) => {
57
+ const req = request(url, { agent: this.#agent, method: 'POST', headers: {
58
+ authorization: `Bearer ${receipt.token}`, 'content-type': 'application/json', 'content-length': Buffer.byteLength(body),
59
+ 'x-supercode-client-id': `registry-${receipt.receipt_id}`, 'x-supercode-permissions': 'observe',
60
+ } }, (res) => {
61
+ // A receipt is authority only for its loopback endpoint. Never follow
62
+ // redirects or give its bearer to a different host.
63
+ if (res.statusCode < 200 || res.statusCode >= 300) { res.resume(); reject(new Error('local runtime refused its state probe')); return; }
64
+ let bytes = 0; const chunks = [];
65
+ res.on('data', (chunk) => { bytes += chunk.length; if (bytes > 1024 * 1024) req.destroy(new Error('runtime descriptor exceeds its read limit')); else chunks.push(chunk); });
66
+ res.on('error', reject);
67
+ res.on('end', () => {
68
+ try {
69
+ const frame = JSON.parse(Buffer.concat(chunks).toString('utf8')), descriptor = frame.result;
70
+ if (frame.id !== id || frame.error || descriptor?.schema_version !== 2 || typeof descriptor.session_id !== 'string'
71
+ || !['connected', 'shutting_down'].includes(descriptor.connection_state)
72
+ || !['idle', 'busy'].includes(descriptor.turn_state)) throw new Error('invalid runtime descriptor');
73
+ resolveResult(descriptor.connection_state === 'shutting_down' ? 'shutting_down' : descriptor.turn_state === 'busy' ? 'busy' : 'idle');
74
+ } catch (error) { reject(error); }
75
+ });
76
+ });
77
+ this.#requests.add(req);
78
+ const timer = setTimeout(() => req.destroy(new Error('local runtime state probe did not answer')), 2000);
79
+ req.once('close', () => { clearTimeout(timer); this.#requests.delete(req); });
80
+ req.once('error', reject); req.end(body);
81
+ });
82
+ }
83
+ /** One receipt scan per poll, probing only followed source identities. */
84
+ async read(sources) {
85
+ if (this.#closed) throw new Error('runtime-state reader closed');
86
+ const keys = new Map(sources.map((source) => [JSON.stringify([source.harness, source.session_id]), 'persisted']));
87
+ if (!keys.size) return keys;
88
+ let names; try { names = await readdir(this.#directory); } catch { names = []; }
89
+ const receipts = [];
90
+ for (const name of names) {
91
+ if (!name.endsWith('.json')) continue;
92
+ try { const receipt = await this.#receipt(join(this.#directory, name)); receipts.push(receipt); } catch { /* native registry discovery skips malformed/stale receipts */ }
93
+ }
94
+ receipts.sort((a, b) => b.created_at_ms - a.created_at_ms || Buffer.compare(Buffer.from(a.runtime_session_id), Buffer.from(b.runtime_session_id)));
95
+ for (const receipt of receipts) {
96
+ if (this.#closed) throw new Error('runtime-state reader closed');
97
+ const key = JSON.stringify([receipt.source.harness, receipt.source.session_id]);
98
+ if (keys.get(key) !== 'persisted') continue;
99
+ try {
100
+ const selected = await this.#receipt(join(this.#directory, `${receipt.receipt_id}.json`));
101
+ if (selected.receipt_id !== receipt.receipt_id || selected.source.harness !== receipt.source.harness
102
+ || selected.source.session_id !== receipt.source.session_id
103
+ || await canonical(selected.source.workspace) !== await canonical(receipt.source.workspace)) continue;
104
+ keys.set(key, await this.#describe(selected));
105
+ } catch { /* a failed probe is not a live runtime; try another matching registration */ }
106
+ }
107
+ return keys;
108
+ }
109
+ close() { this.#closed = true; for (const req of this.#requests) req.destroy(new Error('runtime-state reader closed')); this.#agent.destroy(); }
110
+ }