hub-runner 0.2.3 → 0.2.5

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.
Files changed (3) hide show
  1. package/check.mjs +151 -187
  2. package/main.mjs +244 -266
  3. package/package.json +1 -1
package/check.mjs CHANGED
@@ -6,18 +6,18 @@ import { tmpdir } from "node:os";
6
6
  import { join as join3, resolve } from "node:path";
7
7
  import { setTimeout as delay } from "node:timers/promises";
8
8
 
9
- // ../../lib/clean-env.mjs
9
+ // ../../hosted-hub/lib/clean-env.mjs
10
10
  var INHERITED_ENV = /^(EVE_|WORKFLOW_)/;
11
11
  function cleanEnv(env = process.env) {
12
12
  return Object.fromEntries(Object.entries(env).filter(([key]) => !INHERITED_ENV.test(key)));
13
13
  }
14
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";
15
+ // ../../hosted-hub/lib/processes.mjs
16
+ import { spawn } from "node:child_process";
17
+ import { closeSync as closeSync2, existsSync, mkdirSync, openSync as openSync2, readFileSync, renameSync, rmSync, readdirSync, statSync as statSync2, writeFileSync, writeSync } from "node:fs";
18
18
  import { join } from "node:path";
19
19
 
20
- // ../../lib/fingerprint.mjs
20
+ // ../../hosted-hub/lib/fingerprint.mjs
21
21
  var TREE_SKIP = /* @__PURE__ */ new Set(["node_modules", ".git", ".DS_Store"]);
22
22
  function fingerprintDiff(recorded, current) {
23
23
  if (!recorded?.hash) return "it was started without a fingerprint (by an older registry)";
@@ -36,22 +36,44 @@ function fingerprintDiff(recorded, current) {
36
36
  }
37
37
  var BUILD_SKIP = /* @__PURE__ */ new Set([...TREE_SKIP, ".eve", ".output"]);
38
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
- };
39
+ // ../../hosted-hub/lib/log-tail.mjs
40
+ import { closeSync, openSync, readSync, statSync } from "node:fs";
41
+ function createLogTail(log) {
42
+ const tails = /* @__PURE__ */ new Map();
43
+ function follow(name, file, offset) {
44
+ tails.set(name, { file, offset, carry: "", prefix: `[${name}]`.padEnd(20) });
45
+ }
46
+ function pump() {
47
+ for (const tail of tails.values()) {
48
+ let size;
49
+ try {
50
+ size = statSync(tail.file).size;
51
+ } catch {
52
+ continue;
53
+ }
54
+ if (size < tail.offset) tail.offset = 0;
55
+ if (size === tail.offset) continue;
56
+ const length = Math.min(size - tail.offset, 1e6);
57
+ const buffer = Buffer.alloc(length);
58
+ const fd = openSync(tail.file, "r");
59
+ try {
60
+ readSync(fd, buffer, 0, length, tail.offset);
61
+ } finally {
62
+ closeSync(fd);
63
+ }
64
+ tail.offset += length;
65
+ const lines = (tail.carry + buffer.toString("utf8")).split("\n");
66
+ tail.carry = lines.pop();
67
+ for (const line of lines) if (line.trim() && !line.startsWith("Migration file of version")) log(`${tail.prefix} ${line}`);
68
+ }
69
+ }
70
+ return { follow, pump };
71
+ }
72
+
73
+ // ../../hosted-hub/lib/process-tree.mjs
74
+ import { execFile, execFileSync } from "node:child_process";
44
75
  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
76
  var KILL_GRACE_MS = 3e3;
52
- var WATCH_MS = 1e3;
53
- var TREE_MS = 5e3;
54
- var LOG_ROTATE_BYTES = 2e7;
55
77
  function groupAlive(pgid) {
56
78
  if (!(pgid > 1)) return false;
57
79
  try {
@@ -74,7 +96,7 @@ var C = { ...process.env, LC_ALL: "C" };
74
96
  var SLOW_MS = 1e3;
75
97
  function slow(what, started) {
76
98
  const ms = Date.now() - started;
77
- if (ms >= SLOW_MS) console.warn(`[registry] ${what} took ${ms}ms`);
99
+ if (ms >= SLOW_MS) console.warn(`[runner] ${what} took ${ms}ms`);
78
100
  }
79
101
  var LSTART = String.raw`(\w{3}\s+\w{3}\s+\d+\s+[\d:]+\s+\d{4})`;
80
102
  function table() {
@@ -137,6 +159,73 @@ async function healthy(port, ms = 1e3) {
137
159
  }
138
160
  }
139
161
  var merge = (...lists) => [...new Map(lists.flat().map((m) => [m.pid, m])).values()];
162
+ var treeAlive = (pgid, tree) => groupAlive(pgid) || liveMembers(tree, table()).length > 0;
163
+ async function untilTreeGone(pgid, tree, ms) {
164
+ const deadline = Date.now() + ms;
165
+ while (treeAlive(pgid, tree)) {
166
+ if (Date.now() >= deadline) return false;
167
+ await sleep(200);
168
+ }
169
+ return true;
170
+ }
171
+ function killTree(pgid, tree) {
172
+ signalGroup(pgid, "SIGKILL");
173
+ for (const g of new Set(liveMembers(tree, table()).map((m) => m.pgid))) signalGroup(g, "SIGKILL");
174
+ }
175
+ async function stopTree(pgid, tree, { name, timeoutMs, warn }) {
176
+ if (!treeAlive(pgid, tree)) return true;
177
+ let membersTermed = false;
178
+ const termMembers = () => {
179
+ membersTermed = true;
180
+ for (const g of new Set(liveMembers(tree, table()).map((m) => m.pgid))) if (g !== pgid) signalGroup(g, "SIGTERM");
181
+ };
182
+ if (groupAlive(pgid)) signalGroup(pgid, "SIGTERM");
183
+ else termMembers();
184
+ const deadline = Date.now() + timeoutMs;
185
+ let gone = true;
186
+ while (treeAlive(pgid, tree)) {
187
+ if (!membersTermed && !groupAlive(pgid)) termMembers();
188
+ if (Date.now() >= deadline) {
189
+ gone = false;
190
+ break;
191
+ }
192
+ await sleep(200);
193
+ }
194
+ if (gone) return true;
195
+ warn(`[runner] ${name} didn't stop within ${timeoutMs / 1e3}s of SIGTERM; sending SIGKILL`);
196
+ killTree(pgid, tree);
197
+ return untilTreeGone(pgid, tree, KILL_GRACE_MS);
198
+ }
199
+ function ownsLeader(record, rows, signature) {
200
+ if (!(record.pid > 1) || record.pgid !== record.pid) return false;
201
+ const row = rows.find((r) => r.pid === record.pid);
202
+ if (!row || row.pgid !== record.pgid) return false;
203
+ if (!` ${row.command} `.includes(signature)) return false;
204
+ const member = record.tree?.find((m) => m.pid === record.pid);
205
+ if (member) return member.startedAt === row.startedAt;
206
+ const recorded = Date.parse(record.startedAt);
207
+ return !(Number.isFinite(row.startedAt) && Number.isFinite(recorded) && Math.abs(row.startedAt - recorded) > 5e3);
208
+ }
209
+ function ourTree(record, rows, signature) {
210
+ const leader = ownsLeader(record, rows, signature);
211
+ return { leader, tree: merge(leader ? treeOf(record.pid, rows) : [], liveMembers(record.tree, rows)) };
212
+ }
213
+
214
+ // ../../hosted-hub/lib/processes.mjs
215
+ var num = (key, fallback) => {
216
+ const raw = process.env[key];
217
+ return raw === void 0 || raw === "" || !Number.isFinite(Number(raw)) ? fallback : Number(raw);
218
+ };
219
+ var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
220
+ var CRASH_LIMIT = num("ROSTER_CRASH_LIMIT", 5);
221
+ var CRASH_WINDOW_MS = num("ROSTER_CRASH_WINDOW_MS", 10 * 6e4);
222
+ var BACKOFF_BASE_MS = num("ROSTER_BACKOFF_BASE_MS", 1e3);
223
+ var BACKOFF_MAX_MS = num("ROSTER_BACKOFF_MAX_MS", 6e4);
224
+ var HEALTH_TIMEOUT_MS = num("ROSTER_START_HEALTH_TIMEOUT_MS", 12e4);
225
+ var PORT_RECHECK_MS = num("ROSTER_PORT_RECHECK_MS", 5e3);
226
+ var WATCH_MS = 1e3;
227
+ var TREE_MS = 5e3;
228
+ var LOG_ROTATE_BYTES = 2e7;
140
229
  function createSupervisor({
141
230
  root,
142
231
  // the workspace: eve dev's cwd
@@ -149,18 +238,12 @@ function createSupervisor({
149
238
  isCurrent = () => true,
150
239
  // is this agent object still on the roster?
151
240
  // ({ agent, env, argv }) -> what a server spawned now runs with
152
- // (lib/fingerprint.mjs), or null. A previous registry's server is
241
+ // (lib/fingerprint.mjs), or null. A previous runner's server is
153
242
  // adopted only if its recorded fingerprint matches.
154
243
  fingerprint = null,
155
244
  onChange = () => {
156
245
  },
157
246
  // 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
247
  onUp = () => {
165
248
  },
166
249
  // a server is serving: adopted, spawned, or respawned ({ startedAt, adopted, recovered })
@@ -188,10 +271,7 @@ function createSupervisor({
188
271
  status: e.status,
189
272
  ...e.tree?.length ? { tree: e.tree } : {},
190
273
  ...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 } : {}
274
+ ...e.fingerprint ? { fingerprint: e.fingerprint } : {}
195
275
  }));
196
276
  mkdirSync(stateDir, { recursive: true });
197
277
  const tmp = `${recordsPath}.${process.pid}.tmp`;
@@ -212,34 +292,7 @@ function createSupervisor({
212
292
  persist();
213
293
  onChange(entry.name);
214
294
  }
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
- }
295
+ const { follow, pump } = createLogTail(log);
243
296
  const pumpTimer = setInterval(pump, 250);
244
297
  pumpTimer.unref();
245
298
  function refreshTree(entry, rows = table()) {
@@ -247,10 +300,8 @@ function createSupervisor({
247
300
  const before = JSON.stringify(entry.tree ?? []);
248
301
  entry.tree = merge(treeOf(entry.pid, rows), liveMembers(entry.tree, rows));
249
302
  const own = rows.filter((r) => r.pid === entry.pid || r.ppid === entry.pid);
250
- const rss = entry.rssKb;
251
303
  if (own.length) entry.rssKb = own.reduce((sum, r) => sum + (r.rssKb || 0), 0);
252
304
  if (JSON.stringify(entry.tree) !== before) persist();
253
- if (entry.rssKb !== rss) onUsage(entry.name);
254
305
  }
255
306
  const treeTimer = setInterval(() => {
256
307
  const live = [...entries.values()].filter((e) => e.pid && !e.stopping);
@@ -259,45 +310,8 @@ function createSupervisor({
259
310
  for (const e of live) refreshTree(e, rows);
260
311
  }, TREE_MS);
261
312
  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
313
  const logFile = (name) => join(logDir, `${name}.log`);
300
- const sizeOf = (file) => existsSync(file) ? statSync(file).size : 0;
314
+ const sizeOf = (file) => existsSync(file) ? statSync2(file).size : 0;
301
315
  function entryFor(agent) {
302
316
  let entry = entries.get(agent.name);
303
317
  if (!entry) {
@@ -308,33 +322,19 @@ function createSupervisor({
308
322
  entry.port = agent.port;
309
323
  return entry;
310
324
  }
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
325
  async function killStale(record, why) {
326
326
  const rows = table();
327
- const { leader, tree } = ourTree(record, rows);
327
+ const { leader, tree } = ourTree(record, rows, signature(record.agent, record.port));
328
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`);
329
+ if (groupAlive(record.pgid)) warn(`[runner] ! ${record.agent}: process group ${record.pgid} from processes.json is no longer its server; leaving it alone`);
330
330
  return true;
331
331
  }
332
- warn(`[registry] ${record.agent}: stopping its stale server (pid ${record.pid}): ${why}`);
333
- return stopTree(leader ? record.pgid : null, tree, record.agent);
332
+ warn(`[runner] ${record.agent}: stopping its stale server (pid ${record.pid}): ${why}`);
333
+ return stopTree(leader ? record.pgid : null, tree, { name: record.agent, timeoutMs: stopTimeoutMs, warn });
334
334
  }
335
335
  async function vetAdoption(agent, record) {
336
336
  const rows = table();
337
- const { leader, tree } = ourTree(record, rows);
337
+ const { leader, tree } = ourTree(record, rows, signature(record.agent, record.port));
338
338
  if (!leader && !tree.length) return { adopted: false };
339
339
  if (!leader) return { adopted: false, stale: true, why: "its leader is gone" };
340
340
  if (record.port !== agent.port) return { adopted: false, stale: true, why: `its port moved from ${record.port} to ${agent.port}` };
@@ -366,7 +366,7 @@ function createSupervisor({
366
366
  const { tree } = vetted;
367
367
  if (closed) return carry(agent, record, tree);
368
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 });
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, rssKb: void 0 });
370
370
  const gen = ++entry.gen;
371
371
  follow(agent.name, logFile(agent.name), sizeOf(logFile(agent.name)));
372
372
  entry.watch = setInterval(() => {
@@ -377,14 +377,14 @@ function createSupervisor({
377
377
  }
378
378
  }, WATCH_MS);
379
379
  entry.watch.unref();
380
- log(`[registry] adopted ${agent.name} (pid ${record.pid}, port ${agent.port})`);
380
+ log(`[runner] adopted ${agent.name} (pid ${record.pid}, port ${agent.port})`);
381
381
  changed(entry);
382
382
  onUp(agent, { startedAt: entry.startedAt, adopted: true, recovered: false });
383
383
  return { adopted: true };
384
384
  }
385
385
  function carry(agent, record, tree = record.tree ?? []) {
386
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 });
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 });
388
388
  entry.gen++;
389
389
  return { adopted: true, carried: true };
390
390
  }
@@ -410,7 +410,7 @@ function createSupervisor({
410
410
  return spawnAgent(entry, { recovered });
411
411
  }
412
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}`);
413
+ if (entry.error !== error || entry.status !== "blocked") warn(`[runner] ! ${error}`);
414
414
  Object.assign(entry, { status: "blocked", error, pid: null, pgid: null });
415
415
  entry.timer = setTimeout(() => spawnAgent(entry, { recovered }), PORT_RECHECK_MS);
416
416
  entry.timer.unref();
@@ -419,7 +419,7 @@ function createSupervisor({
419
419
  mkdirSync(logDir, { recursive: true });
420
420
  const file = logFile(name);
421
421
  if (sizeOf(file) > LOG_ROTATE_BYTES) renameSync(file, `${file}.1`);
422
- const fd = openSync(file, "a");
422
+ const fd = openSync2(file, "a");
423
423
  writeSync(fd, `
424
424
  --- ${(/* @__PURE__ */ new Date()).toISOString()} hub: starting ${name} on port ${port}
425
425
  `);
@@ -429,7 +429,7 @@ function createSupervisor({
429
429
  try {
430
430
  fp = fingerprintFor(agent, spawnEnv);
431
431
  } catch (error) {
432
- warn(`[registry] ! ${name}: couldn't fingerprint its inputs (${error.message}); the next registry respawns it instead of adopting it`);
432
+ warn(`[runner] ! ${name}: couldn't fingerprint its inputs (${error.message}); the next runner respawns it instead of adopting it`);
433
433
  }
434
434
  let child;
435
435
  try {
@@ -437,17 +437,17 @@ function createSupervisor({
437
437
  cwd: root,
438
438
  env: spawnEnv,
439
439
  detached: true,
440
- // own process group; survives the registry
440
+ // own process group; survives the runner
441
441
  stdio: ["ignore", fd, fd]
442
442
  });
443
443
  } finally {
444
- closeSync(fd);
444
+ closeSync2(fd);
445
445
  }
446
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 });
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, rssKb: void 0 });
448
448
  follow(name, file, sizeOf(file));
449
449
  child.once("error", (error) => {
450
- warn(`[registry] ! couldn't start ${name}: ${error.message}`);
450
+ warn(`[runner] ! couldn't start ${name}: ${error.message}`);
451
451
  exited(entry, gen, { code: null, signal: null, error: error.message });
452
452
  });
453
453
  child.once("exit", (code, signal) => exited(entry, gen, { code, signal }));
@@ -472,15 +472,14 @@ function createSupervisor({
472
472
  entry.status = "up";
473
473
  changed(entry);
474
474
  onUp(entry.agent, { startedAt: entry.startedAt, adopted: false, recovered });
475
- if (recovered) onRecovered(entry.agent);
476
475
  return;
477
476
  }
478
477
  }
479
478
  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)}`);
479
+ warn(`[runner] ! ${entry.name} isn't healthy ${HEALTH_TIMEOUT_MS / 1e3}s after starting; see ${logFile(entry.name)}`);
481
480
  entry.slowWarned = true;
482
481
  }
483
- await sleep(500);
482
+ await sleep2(500);
484
483
  }
485
484
  }
486
485
  function exited(entry, gen, { code, signal, error }) {
@@ -506,13 +505,13 @@ function createSupervisor({
506
505
  if (entry.crashes.length >= CRASH_LIMIT) {
507
506
  entry.status = "crashed";
508
507
  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}`);
508
+ warn(`[runner] ! ${entry.name} crashed: ${entry.error}`);
510
509
  return changed(entry);
511
510
  }
512
511
  const delay2 = Math.min(BACKOFF_BASE_MS * 2 ** (entry.crashes.length - 1), BACKOFF_MAX_MS);
513
512
  entry.status = "down";
514
513
  entry.nextRestartAt = new Date(now + delay2).toISOString();
515
- warn(`[registry] ${entry.name} exited unexpectedly (${how}); restarting in ${delay2 / 1e3}s`);
514
+ warn(`[runner] ${entry.name} exited unexpectedly (${how}); restarting in ${delay2 / 1e3}s`);
516
515
  entry.timer = setTimeout(async () => {
517
516
  await untilTreeGone(pgid, tree, KILL_GRACE_MS);
518
517
  if (entry.status !== "down" || entry.stopping) return;
@@ -524,7 +523,7 @@ function createSupervisor({
524
523
  changed(entry);
525
524
  }
526
525
  return {
527
- // Startup: adopt what's still running from a previous registry, start the rest.
526
+ // Startup: adopt what's still running from a previous runner, start the rest.
528
527
  startAll(agents) {
529
528
  starting = (async () => {
530
529
  const records = readRecords();
@@ -542,7 +541,7 @@ function createSupervisor({
542
541
  const result = results.get(agent.name);
543
542
  if (result.adopted) return;
544
543
  if (closed && !result.superseded) {
545
- if (ourTree(record, table()).tree.length) carry(agent, record);
544
+ if (ourTree(record, table(), signature(record.agent, record.port)).tree.length) carry(agent, record);
546
545
  return;
547
546
  }
548
547
  await killStale(record, result.why ?? "it isn't serving");
@@ -586,7 +585,7 @@ function createSupervisor({
586
585
  }
587
586
  entry.status = "stopping";
588
587
  changed(entry);
589
- const gone = await stopTree(pgid, tree, agent.name);
588
+ const gone = await stopTree(pgid, tree, { name: agent.name, timeoutMs: stopTimeoutMs, warn });
590
589
  if (gone) {
591
590
  if (entry.pgid === pgid) {
592
591
  entry.gen++;
@@ -608,42 +607,6 @@ function createSupervisor({
608
607
  pump();
609
608
  persist();
610
609
  },
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
610
  // Resolves true once the bot is up (spawned by us and healthy); false
648
611
  // once it can't be: it crashed, left the roster, or this supervisor closed.
649
612
  async waitUp(agent, timeoutMs) {
@@ -653,21 +616,10 @@ function createSupervisor({
653
616
  if (closed || !isCurrent(agent)) return false;
654
617
  if (entry?.agent === agent && entry.status === "up") return true;
655
618
  if (entry?.status === "crashed") return false;
656
- await sleep(250);
619
+ await sleep2(250);
657
620
  }
658
621
  return false;
659
622
  },
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
623
  status(name) {
672
624
  const e = entries.get(name);
673
625
  if (!e) return null;
@@ -742,7 +694,7 @@ async function createSession(fetchFn, eve, operationId) {
742
694
  return sessionId;
743
695
  }
744
696
 
745
- // ../../lib/v26-facts.mjs
697
+ // ../../hosted-hub/lib/v26-facts.mjs
746
698
  function factsOfV26(event) {
747
699
  if (!event || typeof event.type !== "string") return { facts: [], progress: false };
748
700
  const data = isRecord(event.data) ? event.data : {};
@@ -962,7 +914,8 @@ function fact(type, data, source) {
962
914
  }
963
915
  var NO_RUN_SCOPE = /* @__PURE__ */ new Set(["context.started", "context.settled", "interaction.opened", "interaction.settled", "response.submitted", "response.settled"]);
964
916
  function content(data, kind, phase, value) {
965
- return fact("content.completed", { partId: partIdOf(data, kind), runId: runIdOf(data), kind, phase, value }, data);
917
+ const run = typeof data.stepIndex === "number" ? { runId: runIdOf(data) } : {};
918
+ return fact("content.completed", { partId: partIdOf(data, kind), ...run, kind, phase, value }, data);
966
919
  }
967
920
  function turnSettled(data, settled) {
968
921
  const turnId = str(data.turnId);
@@ -1072,7 +1025,7 @@ var str = (value) => typeof value === "string" ? value : void 0;
1072
1025
  var num2 = (value) => typeof value === "number" && Number.isFinite(value) ? value : 0;
1073
1026
  var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1074
1027
 
1075
- // ../../lib/session-stream.mjs
1028
+ // ../../hosted-hub/lib/session-stream.mjs
1076
1029
  async function* readSessionLines(body, startIndex) {
1077
1030
  let line = startIndex;
1078
1031
  let lastAt = null;
@@ -1155,9 +1108,19 @@ async function* streamLines(eve, sessionId, o) {
1155
1108
  }
1156
1109
  const idle = new AbortController(), signal = o.signal ? AbortSignal.any([o.signal, idle.signal]) : idle.signal;
1157
1110
  let timer = setTimeout(() => idle.abort(), idleMs);
1111
+ const opened = Date.now();
1112
+ let firstByte = false, lineAt = opened;
1158
1113
  const quiet = () => {
1159
1114
  clearTimeout(timer);
1160
1115
  timer = setTimeout(() => idle.abort(), idleMs);
1116
+ if (!firstByte) {
1117
+ firstByte = true;
1118
+ console.log(JSON.stringify({ event: "hub.stream_first_byte", sessionId, startIndex: next, ms: Date.now() - opened }));
1119
+ }
1120
+ if (Date.now() - lineAt >= idleMs) {
1121
+ console.log(JSON.stringify({ event: "hub.stream_no_lines", sessionId, next, ms: Date.now() - lineAt }));
1122
+ lineAt = Date.now();
1123
+ }
1161
1124
  };
1162
1125
  let renewed = false;
1163
1126
  try {
@@ -1185,6 +1148,7 @@ async function* streamLines(eve, sessionId, o) {
1185
1148
  break;
1186
1149
  }
1187
1150
  yield record;
1151
+ lineAt = Date.now();
1188
1152
  next = record.line + 1;
1189
1153
  reopens = o.reopens ?? 3;
1190
1154
  }