hub-runner 0.2.1 → 0.2.2

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/check.mjs ADDED
@@ -0,0 +1,1336 @@
1
+ import { createRequire as __hubCreateRequire } from 'node:module'; const require = __hubCreateRequire(import.meta.url);
2
+
3
+ // ../../hosted-hub/runner/check.ts
4
+ import { mkdtempSync, readFileSync as readFileSync3, rmSync as rmSync3 } from "node:fs";
5
+ import { tmpdir } from "node:os";
6
+ import { join as join3, resolve } from "node:path";
7
+ import { setTimeout as delay } from "node:timers/promises";
8
+
9
+ // ../../lib/clean-env.mjs
10
+ var INHERITED_ENV = /^(EVE_|WORKFLOW_)/;
11
+ function cleanEnv(env = process.env) {
12
+ return Object.fromEntries(Object.entries(env).filter(([key]) => !INHERITED_ENV.test(key)));
13
+ }
14
+
15
+ // ../../lib/processes.mjs
16
+ import { spawn, execFile, execFileSync } from "node:child_process";
17
+ import { closeSync, existsSync, mkdirSync, openSync, readFileSync, readSync, renameSync, rmSync, readdirSync, statSync, writeFileSync, writeSync } from "node:fs";
18
+ import { join } from "node:path";
19
+
20
+ // ../../lib/fingerprint.mjs
21
+ var TREE_SKIP = /* @__PURE__ */ new Set(["node_modules", ".git", ".DS_Store"]);
22
+ function fingerprintDiff(recorded, current) {
23
+ if (!recorded?.hash) return "it was started without a fingerprint (by an older registry)";
24
+ if (recorded.hash === current.hash) return null;
25
+ const parts = Object.keys({ ...recorded.parts, ...current.parts }).filter((k) => recorded.parts?.[k] !== current.parts[k]);
26
+ const out = [];
27
+ for (const part of parts) {
28
+ if (part !== "env" || !recorded.envKeys) {
29
+ out.push(part);
30
+ continue;
31
+ }
32
+ const keys = [.../* @__PURE__ */ new Set([...Object.keys(recorded.envKeys), ...Object.keys(current.envKeys)])].filter((k) => recorded.envKeys[k] !== current.envKeys[k]).sort();
33
+ out.push(`env (${keys.slice(0, 8).join(", ")}${keys.length > 8 ? `, +${keys.length - 8} more` : ""})`);
34
+ }
35
+ return `it was started with different ${out.join(", ") || "inputs"}`;
36
+ }
37
+ var BUILD_SKIP = /* @__PURE__ */ new Set([...TREE_SKIP, ".eve", ".output"]);
38
+
39
+ // ../../lib/processes.mjs
40
+ var num = (key, fallback) => {
41
+ const raw = process.env[key];
42
+ return raw === void 0 || raw === "" || !Number.isFinite(Number(raw)) ? fallback : Number(raw);
43
+ };
44
+ var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
45
+ var CRASH_LIMIT = num("ROSTER_CRASH_LIMIT", 5);
46
+ var CRASH_WINDOW_MS = num("ROSTER_CRASH_WINDOW_MS", 10 * 6e4);
47
+ var BACKOFF_BASE_MS = num("ROSTER_BACKOFF_BASE_MS", 1e3);
48
+ var BACKOFF_MAX_MS = num("ROSTER_BACKOFF_MAX_MS", 6e4);
49
+ var HEALTH_TIMEOUT_MS = num("ROSTER_START_HEALTH_TIMEOUT_MS", 12e4);
50
+ var PORT_RECHECK_MS = num("ROSTER_PORT_RECHECK_MS", 5e3);
51
+ var KILL_GRACE_MS = 3e3;
52
+ var WATCH_MS = 1e3;
53
+ var TREE_MS = 5e3;
54
+ var LOG_ROTATE_BYTES = 2e7;
55
+ function groupAlive(pgid) {
56
+ if (!(pgid > 1)) return false;
57
+ try {
58
+ process.kill(-pgid, 0);
59
+ return true;
60
+ } catch (error) {
61
+ return error.code === "EPERM";
62
+ }
63
+ }
64
+ function pidAlive(pid) {
65
+ if (!(pid > 1)) return false;
66
+ try {
67
+ process.kill(pid, 0);
68
+ return true;
69
+ } catch (error) {
70
+ return error.code === "EPERM";
71
+ }
72
+ }
73
+ var C = { ...process.env, LC_ALL: "C" };
74
+ var SLOW_MS = 1e3;
75
+ function slow(what, started) {
76
+ const ms = Date.now() - started;
77
+ if (ms >= SLOW_MS) console.warn(`[registry] ${what} took ${ms}ms`);
78
+ }
79
+ var LSTART = String.raw`(\w{3}\s+\w{3}\s+\d+\s+[\d:]+\s+\d{4})`;
80
+ function table() {
81
+ try {
82
+ const started = Date.now();
83
+ const out = execFileSync("ps", ["-A", "-o", "pid=", "-o", "ppid=", "-o", "pgid=", "-o", "rss=", "-o", "lstart=", "-o", "command="], { encoding: "utf8", env: C, stdio: ["ignore", "pipe", "ignore"], maxBuffer: 64 * 1024 * 1024 });
84
+ slow("ps -A", started);
85
+ const re = new RegExp(String.raw`^(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+${LSTART}\s+(.*)$`);
86
+ return out.split("\n").map((line) => line.trim().match(re)).filter(Boolean).map((m) => ({ pid: Number(m[1]), ppid: Number(m[2]), pgid: Number(m[3]), rssKb: Number(m[4]), startedAt: Date.parse(m[5]), command: m[6] }));
87
+ } catch {
88
+ return [];
89
+ }
90
+ }
91
+ function treeOf(pid, rows) {
92
+ const byPid = new Map(rows.map((r) => [r.pid, r]));
93
+ const children = /* @__PURE__ */ new Map();
94
+ for (const r of rows) (children.get(r.ppid) ?? children.set(r.ppid, []).get(r.ppid)).push(r);
95
+ const out = [];
96
+ const queue = byPid.has(pid) ? [byPid.get(pid)] : [];
97
+ const seen = /* @__PURE__ */ new Set();
98
+ while (queue.length) {
99
+ const r = queue.shift();
100
+ if (seen.has(r.pid)) continue;
101
+ seen.add(r.pid);
102
+ out.push({ pid: r.pid, pgid: r.pgid, startedAt: r.startedAt });
103
+ queue.push(...children.get(r.pid) ?? []);
104
+ }
105
+ return out;
106
+ }
107
+ function liveMembers(tree, rows) {
108
+ const byPid = new Map(rows.map((r) => [r.pid, r]));
109
+ return (tree ?? []).filter((m) => byPid.get(m.pid)?.startedAt === m.startedAt);
110
+ }
111
+ function listeners(port) {
112
+ const started = Date.now();
113
+ return new Promise((resolve2, reject) => {
114
+ execFile("lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-t"], { encoding: "utf8" }, (error, stdout) => {
115
+ slow(`lsof :${port}`, started);
116
+ if (error && error.code !== 1) return reject(error);
117
+ resolve2(error ? [] : stdout.split(/\s+/).filter(Boolean).map(Number));
118
+ });
119
+ });
120
+ }
121
+ function describe(pid, rows) {
122
+ const row = rows.find((r) => r.pid === pid);
123
+ return row ? `pid ${pid}: ${row.command.slice(0, 120)}` : `pid ${pid}`;
124
+ }
125
+ function signalGroup(pgid, signal) {
126
+ if (!(pgid > 1)) return;
127
+ try {
128
+ process.kill(-pgid, signal);
129
+ } catch {
130
+ }
131
+ }
132
+ async function healthy(port, ms = 1e3) {
133
+ try {
134
+ return (await fetch(`http://127.0.0.1:${port}/eve/v1/health`, { signal: AbortSignal.timeout(ms) })).ok;
135
+ } catch {
136
+ return false;
137
+ }
138
+ }
139
+ var merge = (...lists) => [...new Map(lists.flat().map((m) => [m.pid, m])).values()];
140
+ function createSupervisor({
141
+ root,
142
+ // the workspace: eve dev's cwd
143
+ stateDir,
144
+ env,
145
+ // spawn environment (already cleaned), or a function of the agent returning it per spawn
146
+ command,
147
+ // [file, ...leadingArgs]
148
+ stopTimeoutMs = 15e3,
149
+ isCurrent = () => true,
150
+ // is this agent object still on the roster?
151
+ // ({ agent, env, argv }) -> what a server spawned now runs with
152
+ // (lib/fingerprint.mjs), or null. A previous registry's server is
153
+ // adopted only if its recorded fingerprint matches.
154
+ fingerprint = null,
155
+ onChange = () => {
156
+ },
157
+ // status changed (broadcast)
158
+ onUsage = () => {
159
+ },
160
+ // a running tree's memory changed (status() reports rssKb)
161
+ onRecovered = () => {
162
+ },
163
+ // healthy again after a crash restart
164
+ onUp = () => {
165
+ },
166
+ // a server is serving: adopted, spawned, or respawned ({ startedAt, adopted, recovered })
167
+ log = (line) => console.log(line),
168
+ warn = (line) => console.warn(line)
169
+ }) {
170
+ const recordsPath = join(stateDir, "processes.json");
171
+ const logDir = join(stateDir, "logs");
172
+ const entries = /* @__PURE__ */ new Map();
173
+ let closed = false;
174
+ let starting = Promise.resolve();
175
+ const signature = (name, port) => ` dev --agent ${name} --port ${port} `;
176
+ const argvFor = (name, port) => [...command, "dev", "--agent", name, "--port", String(port), "--no-ui", "--no-default-extensions", "--resume"];
177
+ const envFor = (agent) => typeof env === "function" ? env(agent) : env;
178
+ const fingerprintFor = (agent, spawnEnv = envFor(agent)) => fingerprint ? fingerprint({ agent, env: spawnEnv, argv: argvFor(agent.name, agent.port) }) : null;
179
+ function persist() {
180
+ const records = [...entries.values()].filter((e) => e.pid || e.status === "crashed").map((e) => ({
181
+ agent: e.name,
182
+ pid: e.pid,
183
+ pgid: e.pgid,
184
+ port: e.port,
185
+ workspace: root,
186
+ startedAt: e.startedAt,
187
+ restarts: e.restarts,
188
+ status: e.status,
189
+ ...e.tree?.length ? { tree: e.tree } : {},
190
+ ...e.lastExit ? { lastExit: e.lastExit } : {},
191
+ ...e.fingerprint ? { fingerprint: e.fingerprint } : {},
192
+ // The daemon token it was spawned with (HUB_INSTANCE): an adopted bot
193
+ // keeps its spawner's, and lib/daemon.mjs selfStopRefusal checks it here.
194
+ ...e.instance ? { instance: e.instance } : {}
195
+ }));
196
+ mkdirSync(stateDir, { recursive: true });
197
+ const tmp = `${recordsPath}.${process.pid}.tmp`;
198
+ writeFileSync(tmp, `${JSON.stringify(records, null, 2)}
199
+ `);
200
+ renameSync(tmp, recordsPath);
201
+ }
202
+ function readRecords() {
203
+ for (const f of existsSync(stateDir) ? readdirSync(stateDir) : []) if (/^processes\.json\.\d+\.tmp$/.test(f)) rmSync(join(stateDir, f), { force: true });
204
+ try {
205
+ const records = JSON.parse(readFileSync(recordsPath, "utf8"));
206
+ return Array.isArray(records) ? records : [];
207
+ } catch {
208
+ return [];
209
+ }
210
+ }
211
+ function changed(entry) {
212
+ persist();
213
+ onChange(entry.name);
214
+ }
215
+ const tails = /* @__PURE__ */ new Map();
216
+ function follow(name, file, offset) {
217
+ tails.set(name, { file, offset, carry: "", prefix: `[${name}]`.padEnd(20) });
218
+ }
219
+ function pump() {
220
+ for (const tail of tails.values()) {
221
+ let size;
222
+ try {
223
+ size = statSync(tail.file).size;
224
+ } catch {
225
+ continue;
226
+ }
227
+ if (size < tail.offset) tail.offset = 0;
228
+ if (size === tail.offset) continue;
229
+ const length = Math.min(size - tail.offset, 1e6);
230
+ const buffer = Buffer.alloc(length);
231
+ const fd = openSync(tail.file, "r");
232
+ try {
233
+ readSync(fd, buffer, 0, length, tail.offset);
234
+ } finally {
235
+ closeSync(fd);
236
+ }
237
+ tail.offset += length;
238
+ const lines = (tail.carry + buffer.toString("utf8")).split("\n");
239
+ tail.carry = lines.pop();
240
+ for (const line of lines) if (line.trim() && !line.startsWith("Migration file of version")) log(`${tail.prefix} ${line}`);
241
+ }
242
+ }
243
+ const pumpTimer = setInterval(pump, 250);
244
+ pumpTimer.unref();
245
+ function refreshTree(entry, rows = table()) {
246
+ if (!entry.pid) return;
247
+ const before = JSON.stringify(entry.tree ?? []);
248
+ entry.tree = merge(treeOf(entry.pid, rows), liveMembers(entry.tree, rows));
249
+ const own = rows.filter((r) => r.pid === entry.pid || r.ppid === entry.pid);
250
+ const rss = entry.rssKb;
251
+ if (own.length) entry.rssKb = own.reduce((sum, r) => sum + (r.rssKb || 0), 0);
252
+ if (JSON.stringify(entry.tree) !== before) persist();
253
+ if (entry.rssKb !== rss) onUsage(entry.name);
254
+ }
255
+ const treeTimer = setInterval(() => {
256
+ const live = [...entries.values()].filter((e) => e.pid && !e.stopping);
257
+ if (!live.length) return;
258
+ const rows = table();
259
+ for (const e of live) refreshTree(e, rows);
260
+ }, TREE_MS);
261
+ treeTimer.unref();
262
+ const treeAlive = (pgid, tree) => groupAlive(pgid) || liveMembers(tree, table()).length > 0;
263
+ async function untilTreeGone(pgid, tree, ms) {
264
+ const deadline = Date.now() + ms;
265
+ while (treeAlive(pgid, tree)) {
266
+ if (Date.now() >= deadline) return false;
267
+ await sleep(200);
268
+ }
269
+ return true;
270
+ }
271
+ function killTree(pgid, tree) {
272
+ signalGroup(pgid, "SIGKILL");
273
+ for (const g of new Set(liveMembers(tree, table()).map((m) => m.pgid))) signalGroup(g, "SIGKILL");
274
+ }
275
+ async function stopTree(pgid, tree, name) {
276
+ if (!treeAlive(pgid, tree)) return true;
277
+ let membersTermed = false;
278
+ const termMembers = () => {
279
+ membersTermed = true;
280
+ for (const g of new Set(liveMembers(tree, table()).map((m) => m.pgid))) if (g !== pgid) signalGroup(g, "SIGTERM");
281
+ };
282
+ if (groupAlive(pgid)) signalGroup(pgid, "SIGTERM");
283
+ else termMembers();
284
+ const deadline = Date.now() + stopTimeoutMs;
285
+ let gone = true;
286
+ while (treeAlive(pgid, tree)) {
287
+ if (!membersTermed && !groupAlive(pgid)) termMembers();
288
+ if (Date.now() >= deadline) {
289
+ gone = false;
290
+ break;
291
+ }
292
+ await sleep(200);
293
+ }
294
+ if (gone) return true;
295
+ warn(`[registry] ${name} didn't stop within ${stopTimeoutMs / 1e3}s of SIGTERM; sending SIGKILL`);
296
+ killTree(pgid, tree);
297
+ return untilTreeGone(pgid, tree, KILL_GRACE_MS);
298
+ }
299
+ const logFile = (name) => join(logDir, `${name}.log`);
300
+ const sizeOf = (file) => existsSync(file) ? statSync(file).size : 0;
301
+ function entryFor(agent) {
302
+ let entry = entries.get(agent.name);
303
+ if (!entry) {
304
+ entry = { name: agent.name, agent, port: agent.port, pid: null, pgid: null, startedAt: null, restarts: 0, status: "down", crashes: [], gen: 0, tree: [] };
305
+ entries.set(agent.name, entry);
306
+ }
307
+ entry.agent = agent;
308
+ entry.port = agent.port;
309
+ return entry;
310
+ }
311
+ function ownsLeader(record, rows) {
312
+ if (!(record.pid > 1) || record.pgid !== record.pid) return false;
313
+ const row = rows.find((r) => r.pid === record.pid);
314
+ if (!row || row.pgid !== record.pgid) return false;
315
+ if (!` ${row.command} `.includes(signature(record.agent, record.port))) return false;
316
+ const member = record.tree?.find((m) => m.pid === record.pid);
317
+ if (member) return member.startedAt === row.startedAt;
318
+ const recorded = Date.parse(record.startedAt);
319
+ return !(Number.isFinite(row.startedAt) && Number.isFinite(recorded) && Math.abs(row.startedAt - recorded) > 5e3);
320
+ }
321
+ function ourTree(record, rows) {
322
+ const leader = ownsLeader(record, rows);
323
+ return { leader, tree: merge(leader ? treeOf(record.pid, rows) : [], liveMembers(record.tree, rows)) };
324
+ }
325
+ async function killStale(record, why) {
326
+ const rows = table();
327
+ const { leader, tree } = ourTree(record, rows);
328
+ if (!leader && !tree.length) {
329
+ if (groupAlive(record.pgid)) warn(`[registry] ! ${record.agent}: process group ${record.pgid} from processes.json is no longer its server; leaving it alone`);
330
+ return true;
331
+ }
332
+ warn(`[registry] ${record.agent}: stopping its stale server (pid ${record.pid}): ${why}`);
333
+ return stopTree(leader ? record.pgid : null, tree, record.agent);
334
+ }
335
+ async function vetAdoption(agent, record) {
336
+ const rows = table();
337
+ const { leader, tree } = ourTree(record, rows);
338
+ if (!leader && !tree.length) return { adopted: false };
339
+ if (!leader) return { adopted: false, stale: true, why: "its leader is gone" };
340
+ if (record.port !== agent.port) return { adopted: false, stale: true, why: `its port moved from ${record.port} to ${agent.port}` };
341
+ if (record.workspace && record.workspace !== root) return { adopted: false, stale: true, why: `it runs workspace ${record.workspace}, not ${root}` };
342
+ if (fingerprint) {
343
+ const why = fingerprintDiff(record.fingerprint, fingerprintFor(agent));
344
+ if (why) return { adopted: false, stale: true, why };
345
+ }
346
+ let holders;
347
+ try {
348
+ holders = await listeners(agent.port);
349
+ } catch {
350
+ return { adopted: false, stale: true, why: "lsof is unavailable, so the port's listener can't be checked" };
351
+ }
352
+ if (!holders.some((pid) => tree.some((m) => m.pid === pid))) return { adopted: false, stale: true, why: `nothing of it listens on port ${agent.port}` };
353
+ return { candidate: true, tree };
354
+ }
355
+ async function tryAdopt(agent, record, vetted) {
356
+ if (!vetted.candidate) return vetted;
357
+ const before = entries.get(agent.name);
358
+ const mark = before ? { gen: before.gen, claim: before.claim ?? 0 } : null;
359
+ const ok = await healthy(agent.port, 2e3);
360
+ if (!isCurrent(agent)) return { adopted: false, stale: true, why: "its bot left the roster meanwhile" };
361
+ const now = entries.get(agent.name);
362
+ if (now && (now.pid || (mark ? now.gen !== mark.gen || (now.claim ?? 0) !== mark.claim : (now.claim ?? 0) > 0))) {
363
+ return { adopted: false, stale: true, superseded: true, why: "another start of it began meanwhile" };
364
+ }
365
+ if (!ok) return { adopted: false, stale: true, why: "/eve/v1/health doesn't answer" };
366
+ const { tree } = vetted;
367
+ if (closed) return carry(agent, record, tree);
368
+ const entry = entryFor(agent);
369
+ Object.assign(entry, { pid: record.pid, pgid: record.pgid, startedAt: record.startedAt, restarts: record.restarts ?? 0, status: "up", adopted: true, child: null, stopping: false, error: void 0, tree, fingerprint: record.fingerprint, instance: record.instance, rssKb: void 0 });
370
+ const gen = ++entry.gen;
371
+ follow(agent.name, logFile(agent.name), sizeOf(logFile(agent.name)));
372
+ entry.watch = setInterval(() => {
373
+ if (entry.gen !== gen) return clearInterval(entry.watch);
374
+ if (!pidAlive(entry.pid)) {
375
+ clearInterval(entry.watch);
376
+ exited(entry, gen, { code: null, signal: null });
377
+ }
378
+ }, WATCH_MS);
379
+ entry.watch.unref();
380
+ log(`[registry] adopted ${agent.name} (pid ${record.pid}, port ${agent.port})`);
381
+ changed(entry);
382
+ onUp(agent, { startedAt: entry.startedAt, adopted: true, recovered: false });
383
+ return { adopted: true };
384
+ }
385
+ function carry(agent, record, tree = record.tree ?? []) {
386
+ const entry = entryFor(agent);
387
+ Object.assign(entry, { pid: record.pid, pgid: record.pgid, startedAt: record.startedAt, restarts: record.restarts ?? 0, status: record.status ?? "up", adopted: true, child: null, stopping: false, tree, fingerprint: record.fingerprint, instance: record.instance });
388
+ entry.gen++;
389
+ return { adopted: true, carried: true };
390
+ }
391
+ async function spawnAgent(entry, { recovered = false } = {}) {
392
+ clearTimeout(entry.timer);
393
+ entry.timer = void 0;
394
+ const { agent, name, port } = entry;
395
+ if (closed || !isCurrent(agent)) return;
396
+ const claim = entry.claim = (entry.claim ?? 0) + 1;
397
+ let holders = [];
398
+ try {
399
+ holders = await listeners(port);
400
+ } catch {
401
+ }
402
+ if (entry.claim !== claim || closed || !isCurrent(agent)) return;
403
+ if (holders.length) {
404
+ const rows = table();
405
+ const ours = liveMembers(entry.tree, rows);
406
+ if (holders.every((pid) => ours.some((m) => m.pid === pid))) {
407
+ killTree(null, ours);
408
+ await untilTreeGone(null, ours, KILL_GRACE_MS);
409
+ if (!isCurrent(agent) || entry.stopping) return;
410
+ return spawnAgent(entry, { recovered });
411
+ }
412
+ const error = `port ${port} is held by another process (${holders.map((pid) => describe(pid, rows)).join("; ")}), not by ${name}'s server; not touching it. ${name} starts once the port is free.`;
413
+ if (entry.error !== error || entry.status !== "blocked") warn(`[registry] ! ${error}`);
414
+ Object.assign(entry, { status: "blocked", error, pid: null, pgid: null });
415
+ entry.timer = setTimeout(() => spawnAgent(entry, { recovered }), PORT_RECHECK_MS);
416
+ entry.timer.unref();
417
+ return changed(entry);
418
+ }
419
+ mkdirSync(logDir, { recursive: true });
420
+ const file = logFile(name);
421
+ if (sizeOf(file) > LOG_ROTATE_BYTES) renameSync(file, `${file}.1`);
422
+ const fd = openSync(file, "a");
423
+ writeSync(fd, `
424
+ --- ${(/* @__PURE__ */ new Date()).toISOString()} hub: starting ${name} on port ${port}
425
+ `);
426
+ const [bin, ...args] = argvFor(name, port);
427
+ const spawnEnv = envFor(agent);
428
+ let fp = null;
429
+ try {
430
+ fp = fingerprintFor(agent, spawnEnv);
431
+ } catch (error) {
432
+ warn(`[registry] ! ${name}: couldn't fingerprint its inputs (${error.message}); the next registry respawns it instead of adopting it`);
433
+ }
434
+ let child;
435
+ try {
436
+ child = spawn(bin, args, {
437
+ cwd: root,
438
+ env: spawnEnv,
439
+ detached: true,
440
+ // own process group; survives the registry
441
+ stdio: ["ignore", fd, fd]
442
+ });
443
+ } finally {
444
+ closeSync(fd);
445
+ }
446
+ const gen = ++entry.gen;
447
+ Object.assign(entry, { child, pid: child.pid ?? null, pgid: child.pid ?? null, startedAt: (/* @__PURE__ */ new Date()).toISOString(), status: "starting", adopted: false, stopping: false, error: void 0, nextRestartAt: void 0, tree: [], slowWarned: false, fingerprint: fp ?? void 0, instance: spawnEnv?.HUB_INSTANCE || void 0, rssKb: void 0 });
448
+ follow(name, file, sizeOf(file));
449
+ child.once("error", (error) => {
450
+ warn(`[registry] ! couldn't start ${name}: ${error.message}`);
451
+ exited(entry, gen, { code: null, signal: null, error: error.message });
452
+ });
453
+ child.once("exit", (code, signal) => exited(entry, gen, { code, signal }));
454
+ refreshTree(entry);
455
+ changed(entry);
456
+ void waitHealthy(entry, gen, recovered);
457
+ }
458
+ async function waitHealthy(entry, gen, recovered) {
459
+ const started = Date.now();
460
+ while (entry.gen === gen && entry.status === "starting") {
461
+ if (await healthy(entry.port)) {
462
+ let ours = true;
463
+ try {
464
+ const holders = await listeners(entry.port);
465
+ if (entry.gen !== gen || entry.status !== "starting") return;
466
+ refreshTree(entry);
467
+ ours = holders.some((pid) => entry.tree.some((m) => m.pid === pid));
468
+ } catch {
469
+ }
470
+ if (entry.gen !== gen || entry.status !== "starting") return;
471
+ if (ours) {
472
+ entry.status = "up";
473
+ changed(entry);
474
+ onUp(entry.agent, { startedAt: entry.startedAt, adopted: false, recovered });
475
+ if (recovered) onRecovered(entry.agent);
476
+ return;
477
+ }
478
+ }
479
+ if (Date.now() - started > HEALTH_TIMEOUT_MS && !entry.slowWarned) {
480
+ warn(`[registry] ! ${entry.name} isn't healthy ${HEALTH_TIMEOUT_MS / 1e3}s after starting; see ${logFile(entry.name)}`);
481
+ entry.slowWarned = true;
482
+ }
483
+ await sleep(500);
484
+ }
485
+ }
486
+ function exited(entry, gen, { code, signal, error }) {
487
+ if (entry.gen !== gen) return;
488
+ entry.gen++;
489
+ clearInterval(entry.watch);
490
+ const at = (/* @__PURE__ */ new Date()).toISOString();
491
+ entry.lastExit = { code, signal, at, ...error ? { error } : {} };
492
+ const { pgid, tree } = entry;
493
+ Object.assign(entry, { child: null, pid: null, pgid: null });
494
+ if (entry.stopping) {
495
+ entry.status = "down";
496
+ return changed(entry);
497
+ }
498
+ killTree(pgid, tree);
499
+ if (!isCurrent(entry.agent)) {
500
+ entry.status = "down";
501
+ return changed(entry);
502
+ }
503
+ const now = Date.now();
504
+ entry.crashes = [...entry.crashes.filter((t) => now - t < CRASH_WINDOW_MS), now];
505
+ const how = signal ? `signal ${signal}` : code === null ? "unknown status" : `code ${code}`;
506
+ if (entry.crashes.length >= CRASH_LIMIT) {
507
+ entry.status = "crashed";
508
+ entry.error = `exited ${entry.crashes.length} times within ${Math.round(CRASH_WINDOW_MS / 6e4)} min (last: ${how}); not restarting until restart_agent. Log: ${logFile(entry.name)}`;
509
+ warn(`[registry] ! ${entry.name} crashed: ${entry.error}`);
510
+ return changed(entry);
511
+ }
512
+ const delay2 = Math.min(BACKOFF_BASE_MS * 2 ** (entry.crashes.length - 1), BACKOFF_MAX_MS);
513
+ entry.status = "down";
514
+ entry.nextRestartAt = new Date(now + delay2).toISOString();
515
+ warn(`[registry] ${entry.name} exited unexpectedly (${how}); restarting in ${delay2 / 1e3}s`);
516
+ entry.timer = setTimeout(async () => {
517
+ await untilTreeGone(pgid, tree, KILL_GRACE_MS);
518
+ if (entry.status !== "down" || entry.stopping) return;
519
+ entry.restarts++;
520
+ changed(entry);
521
+ void spawnAgent(entry, { recovered: true });
522
+ }, delay2);
523
+ entry.timer.unref();
524
+ changed(entry);
525
+ }
526
+ return {
527
+ // Startup: adopt what's still running from a previous registry, start the rest.
528
+ startAll(agents) {
529
+ starting = (async () => {
530
+ const records = readRecords();
531
+ const byName = new Map(records.map((r) => [r.agent, r]));
532
+ for (const record of records) {
533
+ if (agents.some((a) => a.name === record.agent)) continue;
534
+ await killStale(record, "its bot is no longer on the roster");
535
+ }
536
+ const vetted = new Map(await Promise.all(agents.filter((a) => byName.has(a.name)).map(async (a) => [a.name, closed ? { adopted: false } : await vetAdoption(a, byName.get(a.name))])));
537
+ const results = new Map(await Promise.all([...vetted].map(async ([name, v]) => [name, await tryAdopt(agents.find((a) => a.name === name), byName.get(name), v)])));
538
+ await Promise.all(
539
+ agents.map(async (agent) => {
540
+ const record = byName.get(agent.name);
541
+ if (record) {
542
+ const result = results.get(agent.name);
543
+ if (result.adopted) return;
544
+ if (closed && !result.superseded) {
545
+ if (ourTree(record, table()).tree.length) carry(agent, record);
546
+ return;
547
+ }
548
+ await killStale(record, result.why ?? "it isn't serving");
549
+ if (result.superseded) return;
550
+ }
551
+ await spawnAgent(entryFor(agent));
552
+ })
553
+ );
554
+ persist();
555
+ })();
556
+ return starting;
557
+ },
558
+ // An explicit start (create_agent, restart_agent): clears crash history.
559
+ start(agent) {
560
+ if (!isCurrent(agent)) return;
561
+ const entry = entryFor(agent);
562
+ if (entry.pid && pidAlive(entry.pid) && !entry.stopping) return;
563
+ entry.crashes = [];
564
+ void spawnAgent(entry);
565
+ },
566
+ // SIGTERM, then SIGKILL after stopTimeoutMs. True once nothing of it is left.
567
+ async stop(agent) {
568
+ const entry = entries.get(agent.name);
569
+ if (!entry) return true;
570
+ clearTimeout(entry.timer);
571
+ entry.timer = void 0;
572
+ entry.nextRestartAt = void 0;
573
+ entry.claim = (entry.claim ?? 0) + 1;
574
+ entry.stopping = true;
575
+ const { pgid } = entry;
576
+ if (entry.pid) refreshTree(entry);
577
+ const tree = entry.tree;
578
+ if (!treeAlive(pgid, tree)) {
579
+ if (entry.pgid === pgid) {
580
+ entry.gen++;
581
+ clearInterval(entry.watch);
582
+ Object.assign(entry, { status: "down", pid: null, pgid: null, child: null });
583
+ }
584
+ changed(entry);
585
+ return true;
586
+ }
587
+ entry.status = "stopping";
588
+ changed(entry);
589
+ const gone = await stopTree(pgid, tree, agent.name);
590
+ if (gone) {
591
+ if (entry.pgid === pgid) {
592
+ entry.gen++;
593
+ clearInterval(entry.watch);
594
+ Object.assign(entry, { status: "down", pid: null, pgid: null, child: null });
595
+ }
596
+ entry.tree = [];
597
+ changed(entry);
598
+ }
599
+ return gone;
600
+ },
601
+ async stopAll() {
602
+ closed = true;
603
+ await starting.catch(() => {
604
+ });
605
+ clearInterval(pumpTimer);
606
+ clearInterval(treeTimer);
607
+ await Promise.all([...entries.values()].map((e) => this.stop(e.agent)));
608
+ pump();
609
+ persist();
610
+ },
611
+ // Exit without stopping any bot (a `hub restart` that keeps them): nothing
612
+ // new starts, pending (re)starts are cancelled, each tree is re-read and
613
+ // saved, and no signal is sent. The next registry vets every record
614
+ // (vetAdoption) and adopts what still matches. Returns the bots left running.
615
+ async detach() {
616
+ closed = true;
617
+ await starting.catch(() => {
618
+ });
619
+ clearInterval(pumpTimer);
620
+ clearInterval(treeTimer);
621
+ const rows = table();
622
+ for (const e of entries.values()) {
623
+ clearTimeout(e.timer);
624
+ e.timer = void 0;
625
+ e.claim = (e.claim ?? 0) + 1;
626
+ clearInterval(e.watch);
627
+ e.gen++;
628
+ if (e.pid) refreshTree(e, rows);
629
+ }
630
+ pump();
631
+ persist();
632
+ return [...entries.values()].filter((e) => e.pid).map((e) => ({ name: e.name, pid: e.pid }));
633
+ },
634
+ // Drop a removed bot's record (after its server is confirmed down).
635
+ forget(name) {
636
+ const entry = entries.get(name);
637
+ if (entry) {
638
+ clearTimeout(entry.timer);
639
+ clearInterval(entry.watch);
640
+ entry.gen++;
641
+ entry.claim = (entry.claim ?? 0) + 1;
642
+ }
643
+ entries.delete(name);
644
+ tails.delete(name);
645
+ persist();
646
+ },
647
+ // Resolves true once the bot is up (spawned by us and healthy); false
648
+ // once it can't be: it crashed, left the roster, or this supervisor closed.
649
+ async waitUp(agent, timeoutMs) {
650
+ const deadline = Date.now() + timeoutMs;
651
+ while (Date.now() < deadline) {
652
+ const entry = entries.get(agent.name);
653
+ if (closed || !isCurrent(agent)) return false;
654
+ if (entry?.agent === agent && entry.status === "up") return true;
655
+ if (entry?.status === "crashed") return false;
656
+ await sleep(250);
657
+ }
658
+ return false;
659
+ },
660
+ // A running bot's memory as of the last tree refresh (eve dev + its
661
+ // server) and its uptime (an adopted one keeps its start). Kept out of
662
+ // status(): that's in every state snapshot, and these change constantly.
663
+ usage(name) {
664
+ const e = entries.get(name);
665
+ if (!e?.pid || e.status !== "up") return void 0;
666
+ return {
667
+ ...e.rssKb ? { rssMb: Math.round(e.rssKb / 1024) } : {},
668
+ ...e.startedAt ? { uptimeS: Math.max(0, Math.round((Date.now() - Date.parse(e.startedAt)) / 1e3)) } : {}
669
+ };
670
+ },
671
+ status(name) {
672
+ const e = entries.get(name);
673
+ if (!e) return null;
674
+ return {
675
+ status: e.status,
676
+ pid: e.pid,
677
+ port: e.port,
678
+ restarts: e.restarts,
679
+ startedAt: e.startedAt,
680
+ ...e.rssKb ? { rssKb: e.rssKb } : {},
681
+ ...e.adopted ? { adopted: true } : {},
682
+ ...e.lastExit ? { lastExit: e.lastExit } : {},
683
+ ...e.error ? { error: e.error } : {},
684
+ ...e.status === "down" && e.nextRestartAt ? { nextRestartAt: e.nextRestartAt } : {},
685
+ log: logFile(name)
686
+ };
687
+ }
688
+ };
689
+ }
690
+
691
+ // ../../hosted-hub/src/dispatch/tags.ts
692
+ var TAGS = [
693
+ "identity",
694
+ "team",
695
+ "execution",
696
+ "engineering",
697
+ "what_reaches_you",
698
+ "reaching_others",
699
+ "writing",
700
+ "role",
701
+ "execution_context",
702
+ "tools_note",
703
+ "skills",
704
+ "standing_decisions",
705
+ "decision",
706
+ "about_the_human",
707
+ "relevant_decisions",
708
+ "team_architecture",
709
+ "thread_catchup",
710
+ "summary",
711
+ "latest_status",
712
+ "related_threads",
713
+ "new_posts",
714
+ "post",
715
+ "event",
716
+ "thread_changes",
717
+ "team_status",
718
+ "bot",
719
+ "message_to_you",
720
+ "omitted"
721
+ ];
722
+ var OPENING = new RegExp(`<(/?)(${TAGS.join("|")})\\b`, "g");
723
+
724
+ // ../../hosted-hub/src/dispatch/bot.ts
725
+ var ATTEMPT_MS = 15e3;
726
+ var RETRYABLE_4XX = /* @__PURE__ */ new Set([408, 425, 429]);
727
+ var definitive = (status) => status !== null && status >= 400 && status < 500 && !RETRYABLE_4XX.has(status);
728
+ var CreateRefused = class extends Error {
729
+ };
730
+ async function createSession(fetchFn, eve, operationId) {
731
+ const response = await fetchFn(`${eve.url}/v1/session`, { method: "POST", headers: eve.headers, signal: AbortSignal.timeout(ATTEMPT_MS), body: JSON.stringify({ operationId }) });
732
+ const text = await response.text();
733
+ const body = (() => {
734
+ try {
735
+ return JSON.parse(text);
736
+ } catch {
737
+ return {};
738
+ }
739
+ })(), { sessionId } = body;
740
+ if (definitive(response.status)) throw new CreateRefused(`${response.status} ${body.error ?? body.code ?? text.slice(0, 200)}`);
741
+ if (!response.ok || !sessionId) throw new Error(`create ${operationId}: HTTP ${response.status} ${text.slice(0, 200)}`);
742
+ return sessionId;
743
+ }
744
+
745
+ // ../../lib/v26-facts.mjs
746
+ function factsOfV26(event) {
747
+ if (!event || typeof event.type !== "string") return { facts: [], progress: false };
748
+ const data = isRecord(event.data) ? event.data : {};
749
+ const deliveryIds = Array.isArray(event.meta?.deliveryIds) ? event.meta.deliveryIds.filter((id) => typeof id === "string") : [];
750
+ const progress = PROGRESS_TYPES.has(event.type);
751
+ const facts = progress ? progressOf(event.type, data) : committedOf(event.type, data, deliveryIds);
752
+ return { facts, progress };
753
+ }
754
+ var PROGRESS_TYPES = /* @__PURE__ */ new Set(["message.appended", "reasoning.appended", "action.input.appended", "action.partial"]);
755
+ var TASK_RECEIPT = /^(Started|Sent to) task \S+/;
756
+ var runIdOf = (data) => `v26:run:${str(data.turnId) ?? ""}:${String(data.stepIndex ?? "")}`;
757
+ var partIdOf = (data, kind) => `${runIdOf(data)}:${kind}`;
758
+ var compactionIdOf = (data) => `v26:compaction:${str(data.turnId) ?? ""}:${String(data.stepIndex ?? "")}`;
759
+ var clearIdOf = (data) => `v26:clear:${str(data.turnId) ?? ""}:${String(data.sequence ?? "")}`;
760
+ var signInIdOf = (data) => str(data.attemptId) ?? `v26:sign-in:${str(data.name) ?? "connection"}`;
761
+ var deliveryIdOf = (data) => `v26:delivery:${str(data.turnId) ?? ""}:${String(data.sequence ?? "")}`;
762
+ function progressOf(type, data) {
763
+ switch (type) {
764
+ case "message.appended":
765
+ return [fact("content.delta", { partId: partIdOf(data, "text"), kind: "text", delta: str(data.messageDelta) ?? str(data.delta) ?? "" }, data)];
766
+ case "reasoning.appended":
767
+ return [fact("content.delta", { partId: partIdOf(data, "reasoning"), kind: "reasoning", delta: str(data.reasoningDelta) ?? str(data.delta) ?? "" }, data)];
768
+ case "action.input.appended": {
769
+ const callId = str(data.callId);
770
+ return callId === void 0 ? [] : [fact("call.input", { callId, name: str(data.toolName), kind: "tool", delta: str(data.inputTextDelta) ?? "" }, data)];
771
+ }
772
+ case "action.partial": {
773
+ const result = isRecord(data.result) ? data.result : {};
774
+ const callId = str(result.callId);
775
+ if (callId === void 0) return [];
776
+ const label = labelOf(data.presentation, callId);
777
+ return [fact("call.progress", { callId, output: result.output ?? null, ...label === void 0 ? {} : { label } }, data)];
778
+ }
779
+ default:
780
+ return [];
781
+ }
782
+ }
783
+ function committedOf(type, data, deliveryIds) {
784
+ switch (type) {
785
+ case "session.started":
786
+ return [fact("session.started", sessionStarted(data))];
787
+ case "turn.started": {
788
+ const turnId = str(data.turnId);
789
+ if (turnId === void 0) return [];
790
+ return [
791
+ ...deliveryIds.map((deliveryId) => fact("delivery.admitted", { deliveryId })),
792
+ fact("turn.started", { turnId, cause: deliveryIds.length ? { deliveryId: deliveryIds[0] } : { policy: "v26" }, follows: null }, data)
793
+ ];
794
+ }
795
+ case "message.received": {
796
+ const turnId = str(data.turnId);
797
+ if (turnId === void 0) return [];
798
+ const parts = userParts(data);
799
+ const ids = deliveryIds.length ? deliveryIds : [deliveryIdOf(data)];
800
+ return ids.map((deliveryId) => fact("delivery.consumed", { deliveryId, turnId, parts }, data));
801
+ }
802
+ case "step.started": {
803
+ const runId = runIdOf(data), turnId = str(data.turnId);
804
+ if (turnId === void 0) return [];
805
+ return [
806
+ fact("model.requested", { runId, owner: { turnId } }, data),
807
+ fact("model.started", { runId, modelId: str(data.modelId) ?? "unknown", attempt: 0 }, data)
808
+ ];
809
+ }
810
+ case "step.completed": {
811
+ const runId = runIdOf(data);
812
+ const facts = [fact("model.settled", { runId, outcome: "completed", ...typeof data.finishReason === "string" ? { finishReason: data.finishReason } : {} }, data)];
813
+ if (isRecord(data.usage)) facts.push(fact("usage.recorded", { owner: { runId }, kind: "model", usage: usageOf(data.usage) }, data));
814
+ return facts;
815
+ }
816
+ case "step.failed":
817
+ return [fact("model.settled", { runId: runIdOf(data), outcome: "failed", error: errorOf(data) }, data)];
818
+ case "message.completed": {
819
+ const value = str(data.message);
820
+ if (value === void 0) return [];
821
+ return [content(data, "text", data.finishReason === "tool-calls" ? "narration" : "reply", value)];
822
+ }
823
+ case "reasoning.completed": {
824
+ const value = str(data.reasoning);
825
+ return value === void 0 ? [] : [content(data, "reasoning", "narration", value)];
826
+ }
827
+ case "result.completed":
828
+ return data.result === void 0 ? [] : [content(data, "result", "reply", data.result)];
829
+ case "actions.requested": {
830
+ if (!Array.isArray(data.actions)) return [];
831
+ const runId = runIdOf(data);
832
+ return data.actions.flatMap((action) => {
833
+ if (!isRecord(action) || typeof action.callId !== "string") return [];
834
+ return [fact("call.requested", { callId: action.callId, owner: { runId }, capability: capabilityOf(action, labelOf(data.presentation, action.callId)), ...action.input === void 0 ? {} : { input: action.input } }, data)];
835
+ });
836
+ }
837
+ case "action.result": {
838
+ const result = isRecord(data.result) ? data.result : {};
839
+ const callId = str(result.callId);
840
+ if (callId === void 0) return [];
841
+ if (typeof result.output === "string" && TASK_RECEIPT.test(result.output)) return [];
842
+ const error = isRecord(data.error) ? errorOf(data.error) : void 0;
843
+ const output = result.output === void 0 ? {} : { output: result.output };
844
+ const label = labelOf(data.presentation, callId);
845
+ const labelled = label === void 0 ? {} : { label };
846
+ if (data.status === "rejected") return [fact("call.settled", { callId, outcome: "rejected", ...output, ...error ? { error } : {}, ...labelled }, data)];
847
+ const outcome = data.status === "failed" ? "failed" : "completed";
848
+ return [
849
+ fact("call.started", { callId }, data),
850
+ fact("call.settled", { callId, outcome, ...output, ...outcome === "failed" && error ? { error } : {}, ...labelled }, data)
851
+ ];
852
+ }
853
+ case "task.started": {
854
+ const taskId = str(data.taskId), callId = str(data.callId);
855
+ if (taskId === void 0 || callId === void 0) return [];
856
+ return [
857
+ fact("task.started", { taskId, startedBy: { callId }, kind: str(data.kind) ?? "tool", name: str(data.name) ?? "tool" }, data),
858
+ fact("call.started", { callId, taskId }, data)
859
+ ];
860
+ }
861
+ case "task.settled": {
862
+ const taskId = str(data.taskId), callId = str(data.callId);
863
+ if (taskId === void 0 || callId === void 0) return [];
864
+ const outcome = data.status === "completed" ? "completed" : data.status === "failed" ? "failed" : "cancelled";
865
+ const reason = isRecord(data.cancel) && typeof data.cancel.reason === "string" ? { reason: data.cancel.reason.replaceAll("_", "-") } : {};
866
+ const error = isRecord(data.error) && typeof data.error.message === "string" ? { error: { code: "task_failed", message: data.error.message } } : {};
867
+ return [
868
+ fact("task.ended", { taskId, outcome, ...reason, ...error }, data),
869
+ fact("call.settled", { callId, outcome: outcome === "cancelled" ? "interrupted" : outcome, ...data.output === void 0 ? {} : { output: data.output }, ...reason, ...error }, data)
870
+ ];
871
+ }
872
+ case "agent.started": {
873
+ const sessionId = str(data.sessionId), callId = str(data.callId);
874
+ if (sessionId === void 0 || callId === void 0) return [];
875
+ return [fact("child.opened", { sessionId, owner: { callId }, name: str(data.name) ?? "agent", stream: str(data.streamPath) ?? `/eve/v1/session/${sessionId}/stream` }, data)];
876
+ }
877
+ case "input.requested": {
878
+ if (!Array.isArray(data.requests)) return [];
879
+ const turnId = str(data.turnId), taskId = str(data.taskId);
880
+ return data.requests.flatMap((request) => {
881
+ if (!isRecord(request) || typeof request.requestId !== "string") return [];
882
+ const action = isRecord(request.action) ? request.action : void 0;
883
+ const callId = str(data.callId) ?? (action ? str(action.callId) : void 0);
884
+ if (taskId !== void 0) {
885
+ const origin = action && str(action.callId) !== void 0 ? { origin: { sessionId: taskId, interactionId: request.requestId, call: { callId: action.callId, name: capabilityOf(action).name, ...action.input === void 0 ? {} : { input: action.input } } } } : {};
886
+ return [fact("interaction.opened", { interactionId: request.requestId, subject: { taskId }, request: interactionRequest(request), ...origin }, data)];
887
+ }
888
+ const subject = callId !== void 0 ? { callId } : turnId !== void 0 ? { turnId } : void 0;
889
+ if (subject === void 0) return [];
890
+ return [fact("interaction.opened", { interactionId: request.requestId, subject, request: interactionRequest(request) }, data)];
891
+ });
892
+ }
893
+ case "input.resolved": {
894
+ if (!Array.isArray(data.resolutions)) return [];
895
+ return data.resolutions.flatMap((resolution) => {
896
+ if (!isRecord(resolution) || typeof resolution.requestId !== "string") return [];
897
+ const response = isRecord(resolution.response) ? resolution.response : {};
898
+ const value = { ...typeof response.optionId === "string" ? { optionId: response.optionId } : {}, ...typeof response.text === "string" ? { text: response.text } : {} };
899
+ return [fact("interaction.settled", { interactionId: resolution.requestId, outcome: interactionOutcome(resolution.outcome), ...Object.keys(value).length ? { response: value } : {} }, data)];
900
+ });
901
+ }
902
+ case "approval.settled": {
903
+ const interactionId = str(data.requestId);
904
+ if (interactionId === void 0) return [];
905
+ return [fact("interaction.settled", { interactionId, outcome: data.outcome === "approved" ? "accepted" : "withdrawn" }, data)];
906
+ }
907
+ case "approval.candidate": {
908
+ const responseId = str(data.candidateId), interactionId = str(data.requestId);
909
+ if (responseId === void 0 || interactionId === void 0) return [];
910
+ if (data.outcome === "pending") return [fact("response.submitted", { responseId, interactionId, deliveryId: `v26:response:${responseId}` }, data)];
911
+ const outcome = data.outcome === "rejected" ? "refused" : data.outcome === "failed" ? "failed" : data.outcome === "timed-out" ? "expired" : "withdrawn";
912
+ return [fact("response.settled", { responseId, outcome, ...typeof data.reason === "string" ? { reason: data.reason } : {} }, data)];
913
+ }
914
+ case "authorization.required": {
915
+ const turnId = str(data.turnId), taskId = str(data.taskId);
916
+ const subject = taskId !== void 0 ? { taskId } : turnId !== void 0 ? { turnId } : void 0;
917
+ if (subject === void 0) return [];
918
+ const principalId = str(data.principalId);
919
+ return [fact("interaction.opened", { interactionId: signInIdOf(data), subject, request: signInRequest(data), ...principalId === void 0 ? {} : { audience: { principalIds: [principalId] } } }, data)];
920
+ }
921
+ case "authorization.completed":
922
+ return [fact("interaction.settled", { interactionId: signInIdOf(data), outcome: signInOutcome(data.outcome), ...typeof data.reason === "string" ? { reason: data.reason } : {} }, data)];
923
+ case "turn.waiting": {
924
+ const turnId = str(data.turnId);
925
+ if (turnId === void 0 || data.on !== "input") return [];
926
+ return [fact("turn.paused", { turnId, awaiting: [] }, data)];
927
+ }
928
+ case "turn.completed":
929
+ return turnSettled(data, { outcome: "completed" });
930
+ case "turn.cancelled":
931
+ return turnSettled(data, { outcome: "cancelled" });
932
+ case "turn.failed":
933
+ return turnSettled(data, { outcome: "failed", error: errorOf(data) });
934
+ case "context.cleared": {
935
+ const changeId = clearIdOf(data);
936
+ return [fact("context.started", { changeId, kind: "clear" }, data), fact("context.settled", { changeId, kind: "clear", outcome: "completed", selects: null }, data)];
937
+ }
938
+ case "compaction.requested": {
939
+ const turnId = str(data.turnId);
940
+ const trigger = typeof data.usageInputTokens === "number" ? { trigger: { inputTokens: data.usageInputTokens } } : {};
941
+ return [fact("context.started", { changeId: compactionIdOf(data), kind: "compaction", ...turnId === void 0 ? {} : { turnId }, ...trigger }, data)];
942
+ }
943
+ case "compaction.completed":
944
+ return [fact("context.settled", { changeId: compactionIdOf(data), kind: "compaction", outcome: "completed" }, data)];
945
+ case "compaction.failed":
946
+ return [fact("context.settled", { changeId: compactionIdOf(data), kind: "compaction", outcome: "failed", error: errorOf(data) }, data)];
947
+ case "session.completed":
948
+ return [fact("session.ended", { outcome: "completed" })];
949
+ case "session.failed":
950
+ return [fact("session.ended", { outcome: "failed", error: errorOf(data) })];
951
+ // session.waiting (idle), history.imported (folded into the next delivery by a stateful reader) and unknown types have no fact.
952
+ default:
953
+ return [];
954
+ }
955
+ }
956
+ function fact(type, data, source) {
957
+ const scope = {};
958
+ if (source && typeof source.turnId === "string") scope.turnId = source.turnId;
959
+ if (source && typeof source.taskId === "string") scope.taskId = source.taskId;
960
+ if (source && typeof source.turnId === "string" && typeof source.stepIndex === "number" && !NO_RUN_SCOPE.has(type)) scope.runId = runIdOf(source);
961
+ return Object.keys(scope).length ? { type, data, scope } : { type, data };
962
+ }
963
+ var NO_RUN_SCOPE = /* @__PURE__ */ new Set(["context.started", "context.settled", "interaction.opened", "interaction.settled", "response.submitted", "response.settled"]);
964
+ function content(data, kind, phase, value) {
965
+ return fact("content.completed", { partId: partIdOf(data, kind), runId: runIdOf(data), kind, phase, value }, data);
966
+ }
967
+ function turnSettled(data, settled) {
968
+ const turnId = str(data.turnId);
969
+ return turnId === void 0 ? [] : [fact("turn.settled", { turnId, ...settled }, data)];
970
+ }
971
+ function sessionStarted(data) {
972
+ const invocation = isRecord(data.invocation) ? data.invocation : {};
973
+ const parentSessionId = str(invocation.parentSessionId), parentCallId = str(invocation.parentCallId);
974
+ const predecessor = isRecord(data.predecessor) ? str(data.predecessor.sessionId) : void 0;
975
+ return {
976
+ ...parentSessionId !== void 0 && parentCallId !== void 0 ? { parent: { sessionId: parentSessionId, callId: parentCallId } } : {},
977
+ ...predecessor === void 0 ? {} : { predecessor: { sessionId: predecessor } },
978
+ ...isRecord(data.runtime) && typeof data.runtime.agentId === "string" ? { runtime: data.runtime } : {},
979
+ ...isRecord(data.trace) ? { trace: data.trace } : {}
980
+ };
981
+ }
982
+ function userParts(data) {
983
+ if (Array.isArray(data.parts)) {
984
+ return data.parts.flatMap((part) => {
985
+ if (!isRecord(part)) return [];
986
+ if (part.type === "text" && typeof part.text === "string") return [{ kind: "text", text: part.text }];
987
+ if (part.type === "file" && typeof part.mediaType === "string") {
988
+ const url = str(part.url);
989
+ return [{ kind: "file", mediaType: part.mediaType, ...str(part.filename) === void 0 ? {} : { filename: part.filename }, ...typeof part.size === "number" ? { size: part.size } : {}, ...url === void 0 ? { unavailable: true } : { url } }];
990
+ }
991
+ return [];
992
+ });
993
+ }
994
+ return typeof data.message === "string" ? [{ kind: "text", text: data.message }] : [];
995
+ }
996
+ function capabilityOf(action, label) {
997
+ const labelled = label === void 0 ? {} : { label };
998
+ switch (action.kind) {
999
+ case "subagent-call":
1000
+ return { kind: "agent", name: str(action.name) ?? str(action.subagentName) ?? "agent", ...labelled };
1001
+ case "remote-agent-call":
1002
+ return { kind: "agent", name: str(action.name) ?? str(action.remoteAgentName) ?? "agent", remote: true, ...labelled };
1003
+ case "load-skill":
1004
+ return { kind: "skill", name: str(action.name) ?? "skill", ...labelled };
1005
+ default: {
1006
+ const name = str(action.toolName) ?? str(action.name) ?? "tool";
1007
+ return { kind: name.startsWith("eve__task_") ? "control" : "tool", name, ...labelled };
1008
+ }
1009
+ }
1010
+ }
1011
+ function labelOf(presentation, callId) {
1012
+ const entry = isRecord(presentation) && isRecord(presentation[callId]) ? presentation[callId] : {};
1013
+ return str(entry.label);
1014
+ }
1015
+ function interactionRequest(request) {
1016
+ const kind = request.kind === "tool-approval" ? "approval" : request.kind === "session-limit" ? "budget" : "question";
1017
+ const options = Array.isArray(request.options) ? request.options.flatMap((option) => isRecord(option) && typeof option.id === "string" && typeof option.label === "string" ? [{ id: option.id, label: option.label, ...str(option.description) === void 0 ? {} : { description: option.description }, ...option.style === "primary" || option.style === "danger" || option.style === "default" ? { style: option.style } : {} }] : []) : void 0;
1018
+ const display = request.display;
1019
+ return {
1020
+ kind,
1021
+ prompt: str(request.prompt) ?? "",
1022
+ ...options === void 0 ? {} : { options },
1023
+ ...typeof request.allowFreeform === "boolean" ? { allowFreeform: request.allowFreeform } : {},
1024
+ ...display === "confirmation" || display === "select" || display === "text" ? { display } : {}
1025
+ };
1026
+ }
1027
+ function interactionOutcome(outcome) {
1028
+ switch (outcome) {
1029
+ case "answered":
1030
+ case "approved":
1031
+ return "accepted";
1032
+ case "denied":
1033
+ return "declined";
1034
+ case "invalid":
1035
+ return "invalid";
1036
+ // v26's cancelled is the turn's cancel settling the request; ignored is the one eve dropped for a message (v27 withdraws it).
1037
+ case "cancelled":
1038
+ return "interrupted";
1039
+ default:
1040
+ return "withdrawn";
1041
+ }
1042
+ }
1043
+ function signInRequest(data) {
1044
+ const auth = isRecord(data.authorization) ? data.authorization : {};
1045
+ const name = str(data.name) ?? "sign-in";
1046
+ const url = str(auth.url);
1047
+ const principal = isRecord(data.principal) && typeof data.principal.type === "string" ? { type: data.principal.type, ...str(data.principal.id) === void 0 ? {} : { id: data.principal.id }, ...str(data.principal.issuer) === void 0 ? {} : { issuer: data.principal.issuer } } : void 0;
1048
+ return {
1049
+ kind: "sign-in",
1050
+ prompt: str(data.description) ?? `Sign in to ${name}.`,
1051
+ ...url === void 0 ? {} : { link: { url } },
1052
+ signIn: compact({ name, displayName: str(auth.displayName), url, userCode: str(auth.userCode), expiresAt: str(auth.expiresAt), instructions: str(auth.instructions), callbackUrl: str(data.webhookUrl), grant: str(data.grant), principal })
1053
+ };
1054
+ }
1055
+ function signInOutcome(outcome) {
1056
+ return outcome === "authorized" ? "accepted" : outcome === "declined" ? "declined" : outcome === "timed-out" ? "expired" : "failed";
1057
+ }
1058
+ function usageOf(usage) {
1059
+ return {
1060
+ inputTokens: num2(usage.inputTokens),
1061
+ outputTokens: num2(usage.outputTokens),
1062
+ cacheReadTokens: num2(usage.cacheReadTokens),
1063
+ cacheWriteTokens: num2(usage.cacheWriteTokens),
1064
+ ...typeof usage.costUsd === "number" ? { costUsd: usage.costUsd } : {}
1065
+ };
1066
+ }
1067
+ function errorOf(data) {
1068
+ return { code: str(data.code) ?? "unknown", message: str(data.message) ?? "Unknown error.", ...str(data.name) === void 0 ? {} : { name: data.name } };
1069
+ }
1070
+ var compact = (object) => Object.fromEntries(Object.entries(object).filter(([, value]) => value !== void 0));
1071
+ var str = (value) => typeof value === "string" ? value : void 0;
1072
+ var num2 = (value) => typeof value === "number" && Number.isFinite(value) ? value : 0;
1073
+ var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1074
+
1075
+ // ../../lib/session-stream.mjs
1076
+ async function* readSessionLines(body, startIndex) {
1077
+ let line = startIndex;
1078
+ let lastAt = null;
1079
+ const decoder = new TextDecoder();
1080
+ let buffer = "";
1081
+ for await (const chunk of body) {
1082
+ buffer += decoder.decode(chunk, { stream: true });
1083
+ let nl;
1084
+ while ((nl = buffer.indexOf("\n")) >= 0) {
1085
+ const text = buffer.slice(0, nl).trim();
1086
+ buffer = buffer.slice(nl + 1);
1087
+ if (!text) continue;
1088
+ let record;
1089
+ try {
1090
+ record = JSON.parse(text);
1091
+ } catch {
1092
+ continue;
1093
+ }
1094
+ const read = lineOf(record, lastAt);
1095
+ if (read.kind === "skip") continue;
1096
+ if (read.kind === "position") {
1097
+ line = read.next;
1098
+ continue;
1099
+ }
1100
+ if (read.kind === "signal") {
1101
+ yield { signal: read.signal, line };
1102
+ if (read.signal === "ended") return;
1103
+ continue;
1104
+ }
1105
+ if (read.at) lastAt = read.at;
1106
+ yield { line, at: read.at ?? lastAt, facts: read.facts, progress: read.progress, v26: read.v26 };
1107
+ line++;
1108
+ }
1109
+ }
1110
+ }
1111
+ function lineOf(record, lastAt = null) {
1112
+ if (!record || typeof record !== "object") return { kind: "skip" };
1113
+ if (typeof record.$eve === "string") {
1114
+ if (record.$eve === "position" && Number.isInteger(record.next)) return { kind: "position", next: record.next };
1115
+ if (record.$eve === "stream.lease-ended") return { kind: "signal", signal: "lease-ended" };
1116
+ if (record.$eve === "stream.ended") return { kind: "signal", signal: "ended" };
1117
+ return { kind: "skip" };
1118
+ }
1119
+ if (Array.isArray(record.facts)) {
1120
+ return { kind: "line", at: typeof record.at === "string" ? record.at : lastAt, facts: record.facts.filter(isFact), progress: false };
1121
+ }
1122
+ if (record.progress && isFact(record.progress)) {
1123
+ return { kind: "line", at: lastAt, facts: [record.progress], progress: true };
1124
+ }
1125
+ if (typeof record.type === "string") {
1126
+ const translated = factsOfV26(record);
1127
+ return { kind: "line", at: typeof record.meta?.at === "string" ? record.meta.at : lastAt, facts: translated.facts, progress: translated.progress, v26: record };
1128
+ }
1129
+ return { kind: "skip" };
1130
+ }
1131
+ function isFact(value) {
1132
+ return !!value && typeof value === "object" && typeof value.type === "string";
1133
+ }
1134
+
1135
+ // ../../hosted-hub/src/dispatch/stream.ts
1136
+ var StreamError = class extends Error {
1137
+ status;
1138
+ code;
1139
+ constructor(status, code, message) {
1140
+ super(message);
1141
+ this.status = status;
1142
+ this.code = code;
1143
+ }
1144
+ };
1145
+ var IDLE_MS = 15e3;
1146
+ async function* streamLines(eve, sessionId, o) {
1147
+ const fetchFn = o.fetch ?? fetch, idleMs = o.idleMs ?? IDLE_MS;
1148
+ let next = o.startIndex, reopens = o.reopens ?? 3;
1149
+ for (; ; ) {
1150
+ const query = new URLSearchParams({ startIndex: String(next) });
1151
+ if (o.follow) query.set("streamControlVersion", "1");
1152
+ else {
1153
+ query.set("follow", "false");
1154
+ query.set("includeTailIndex", "1");
1155
+ }
1156
+ const idle = new AbortController(), signal = o.signal ? AbortSignal.any([o.signal, idle.signal]) : idle.signal;
1157
+ let timer = setTimeout(() => idle.abort(), idleMs);
1158
+ const quiet = () => {
1159
+ clearTimeout(timer);
1160
+ timer = setTimeout(() => idle.abort(), idleMs);
1161
+ };
1162
+ let renewed = false;
1163
+ try {
1164
+ const res = await fetchFn(`${eve.url}/v1/session/${encodeURIComponent(sessionId)}/stream?${query}`, { headers: eve.headers, cache: "no-store", redirect: "error", signal }).catch((error) => {
1165
+ if (o.signal?.aborted || idle.signal.aborted) return void 0;
1166
+ throw error;
1167
+ });
1168
+ if (o.signal?.aborted) return;
1169
+ if (res && (!res.ok || !res.body)) {
1170
+ const text = await res.text().catch(() => "");
1171
+ let code;
1172
+ try {
1173
+ code = JSON.parse(text).error?.code ?? JSON.parse(text).code;
1174
+ } catch {
1175
+ }
1176
+ throw new StreamError(res.status, code, `eve answered ${res.status} to the stream of ${sessionId}${code ? ` (${code})` : ""}`);
1177
+ }
1178
+ if (res) {
1179
+ try {
1180
+ for await (const record of readSessionLines(watched(res.body, quiet), next)) {
1181
+ if ("signal" in record) {
1182
+ if (record.signal === "ended") return;
1183
+ next = record.line;
1184
+ renewed = true;
1185
+ break;
1186
+ }
1187
+ yield record;
1188
+ next = record.line + 1;
1189
+ reopens = o.reopens ?? 3;
1190
+ }
1191
+ } catch (error) {
1192
+ if (o.signal?.aborted) return;
1193
+ if (!idle.signal.aborted) throw error;
1194
+ }
1195
+ }
1196
+ } finally {
1197
+ clearTimeout(timer);
1198
+ }
1199
+ if (renewed) continue;
1200
+ if (!o.follow || reopens-- <= 0) return;
1201
+ }
1202
+ }
1203
+ async function* watched(body, onBytes) {
1204
+ for await (const chunk of body) {
1205
+ onBytes();
1206
+ yield chunk;
1207
+ }
1208
+ }
1209
+
1210
+ // ../../hosted-hub/runner/stage.ts
1211
+ import { cpSync, existsSync as existsSync2, lstatSync, mkdirSync as mkdirSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, rmSync as rmSync2, symlinkSync, writeFileSync as writeFileSync2 } from "node:fs";
1212
+ import { spawn as spawn2 } from "node:child_process";
1213
+ import { join as join2 } from "node:path";
1214
+
1215
+ // ../../hosted-hub/src/plain.ts
1216
+ var Plain = class extends Error {
1217
+ plain = true;
1218
+ };
1219
+
1220
+ // ../../hosted-hub/runner/stage.ts
1221
+ var SetupFailure = class extends Plain {
1222
+ };
1223
+ function install(pnpm, cwd) {
1224
+ return new Promise((resolve2, reject) => {
1225
+ const child = spawn2(pnpm, ["install", "--no-frozen-lockfile"], { cwd, stdio: ["ignore", "pipe", "pipe"] });
1226
+ const keep = (chunk) => process.stderr.write(chunk);
1227
+ child.stdout.on("data", keep);
1228
+ child.stderr.on("data", keep);
1229
+ child.once("error", (error) => {
1230
+ console.error("pnpm install could not start:", error.message);
1231
+ reject(new SetupFailure("The bots' dependencies could not be installed: pnpm didn't start. Check that pnpm is installed, then retry.", { cause: error }));
1232
+ });
1233
+ child.once("exit", (code, signal) => {
1234
+ if (code === 0) return resolve2();
1235
+ console.error(`pnpm install failed (${signal ? `signal ${signal}` : `exit ${code}`})`);
1236
+ reject(new SetupFailure("The bots' dependencies could not be installed. The log has pnpm's output."));
1237
+ });
1238
+ });
1239
+ }
1240
+ var SKIP = /* @__PURE__ */ new Set(["node_modules", ".eve", ".env", ".git"]);
1241
+ var WORKSPACE_YAML = "packageExtensions:\n nitro:\n dependencies:\n pathe: ^2.0.3\nallowBuilds:\n tree-sitter-bash: false\n";
1242
+ async function stageLocal(workspace, dir, pnpm) {
1243
+ mkdirSync2(dir, { recursive: true });
1244
+ for (const bot of readdirSync2(join2(workspace, "agents"))) {
1245
+ const staged = join2(dir, "agents", bot);
1246
+ if (!existsSync2(staged)) continue;
1247
+ for (const entry of readdirSync2(staged)) if (!SKIP.has(entry)) rmSync2(join2(staged, entry), { recursive: true, force: true });
1248
+ }
1249
+ cpSync(join2(workspace, "agents"), join2(dir, "agents"), { recursive: true, force: true, filter: (p) => !SKIP.has(p.split("/").at(-1)) });
1250
+ rmSync2(join2(dir, "lib"), { recursive: true, force: true });
1251
+ if (existsSync2(join2(workspace, "lib"))) cpSync(join2(workspace, "lib"), join2(dir, "lib"), { recursive: true, filter: (p) => !SKIP.has(p.split("/").at(-1)) });
1252
+ const config = join2(dir, "hub.workspace.json");
1253
+ if (existsSync2(config) || (() => {
1254
+ try {
1255
+ return lstatSync(config).isSymbolicLink();
1256
+ } catch {
1257
+ return false;
1258
+ }
1259
+ })()) rmSync2(config);
1260
+ symlinkSync(join2(workspace, "hub.workspace.json"), config);
1261
+ const source = JSON.parse(readFileSync2(join2(workspace, "package.json"), "utf8"));
1262
+ const pkg = { name: "hub-local-bots", private: true, type: "module", dependencies: source.dependencies };
1263
+ const manifest = `${JSON.stringify(pkg, null, 2)}
1264
+ `;
1265
+ const want = `${manifest}${WORKSPACE_YAML}`;
1266
+ const installed = join2(dir, "node_modules", ".hub-local-package.json");
1267
+ writeFileSync2(join2(dir, "package.json"), manifest);
1268
+ writeFileSync2(join2(dir, "pnpm-workspace.yaml"), WORKSPACE_YAML);
1269
+ writeFileSync2(join2(dir, ".npmrc"), "registry=https://registry.npmjs.org/\n");
1270
+ if (!existsSync2(installed) || readFileSync2(installed, "utf8") !== want) {
1271
+ for (const lock of ["pnpm-lock.yaml", "node_modules/.pnpm/lock.yaml"]) rmSync2(join2(dir, lock), { force: true });
1272
+ await install(pnpm, dir);
1273
+ writeFileSync2(installed, want);
1274
+ }
1275
+ }
1276
+
1277
+ // ../../hosted-hub/runner/check.ts
1278
+ var QUERY = "reply with the single word: pong";
1279
+ async function checkLocalBots(checkout, { pnpm = "pnpm", node = process.execPath, portBase = 4700, timeoutMs = 18e4 } = {}) {
1280
+ const workspace = resolve(checkout);
1281
+ const { agents = [] } = JSON.parse(readFileSync3(join3(workspace, "hub.workspace.json"), "utf8"));
1282
+ const local = agents.filter((a) => a.runsOn === "local").map((a, i) => ({ name: a.name, port: portBase + i }));
1283
+ const dir = mkdtempSync(join3(tmpdir(), "hub-runner-check-"));
1284
+ const logs = [];
1285
+ const supervisor = createSupervisor({
1286
+ root: dir,
1287
+ stateDir: join3(dir, ".runner"),
1288
+ command: [node, join3(dir, "node_modules", "eve", "bin", "eve.js")],
1289
+ env: (agent) => ({ ...cleanEnv(process.env), HUB_AGENT: agent.name, HUB_WORKSPACE: workspace, HUB_SANDBOX_SERVICES: "none", ROSTER_MODELS: "mock" }),
1290
+ log: (line) => logs.push(line),
1291
+ warn: (line) => logs.push(line)
1292
+ });
1293
+ const failed = (what) => new Error(`${what}
1294
+ ${logs.join("\n")}`);
1295
+ try {
1296
+ await stageLocal(workspace, dir, pnpm);
1297
+ await supervisor.startAll(local);
1298
+ const answered = [];
1299
+ for (const agent of local) {
1300
+ if (!await supervisor.waitUp(agent, timeoutMs)) throw failed(`${agent.name} didn't come up`);
1301
+ answered.push({ bot: agent.name, reply: await ask({ url: `http://127.0.0.1:${agent.port}/eve`, headers: { "content-type": "application/json", "x-roster-user": "check" } }, timeoutMs).catch((e) => {
1302
+ throw failed(`${agent.name} didn't answer: ${e.message}`);
1303
+ }) });
1304
+ }
1305
+ return answered;
1306
+ } finally {
1307
+ await supervisor.stopAll();
1308
+ rmSync3(dir, { recursive: true, force: true });
1309
+ }
1310
+ }
1311
+ async function ask(eve, timeoutMs) {
1312
+ const sessionId = await createSession(fetch, eve, "check");
1313
+ const signal = AbortSignal.timeout(timeoutMs);
1314
+ for (; ; ) {
1315
+ const res = await fetch(`${eve.url}/v1/session/${encodeURIComponent(sessionId)}`, { method: "POST", headers: eve.headers, signal, body: JSON.stringify({ message: [{ type: "text", text: QUERY }], idempotencyKey: "check", turnPolicy: "steer" }) });
1316
+ if (res.ok) break;
1317
+ const body = await res.text();
1318
+ if (res.status !== 409 || !body.includes("session_not_ready")) throw new Error(`send: HTTP ${res.status} ${body.slice(0, 300)}`);
1319
+ await delay(250, void 0, { signal });
1320
+ }
1321
+ let reply = "";
1322
+ for await (const { facts } of streamLines(eve, sessionId, { startIndex: 0, follow: true, signal })) {
1323
+ for (const fact2 of facts) {
1324
+ if (fact2.type === "content.completed" && fact2.data?.phase === "reply") reply += fact2.data.value ?? "";
1325
+ if (fact2.type === "turn.settled") {
1326
+ if (fact2.data?.outcome !== "completed" || !reply) throw new Error(`the turn ended ${fact2.data?.outcome} with reply ${JSON.stringify(reply)}`);
1327
+ return reply;
1328
+ }
1329
+ }
1330
+ }
1331
+ throw new Error("the stream ended before the turn settled");
1332
+ }
1333
+ export {
1334
+ QUERY,
1335
+ checkLocalBots
1336
+ };