hub-runner 0.2.3 → 0.2.4

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 +162 -198
  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
  }
package/main.mjs CHANGED
@@ -496,14 +496,14 @@ var require_graceful_fs = __commonJS({
496
496
  return close;
497
497
  })(fs.close);
498
498
  fs.closeSync = (function(fs$closeSync) {
499
- function closeSync3(fd) {
499
+ function closeSync4(fd) {
500
500
  fs$closeSync.apply(fs, arguments);
501
501
  resetQueue();
502
502
  }
503
- Object.defineProperty(closeSync3, previousSymbol, {
503
+ Object.defineProperty(closeSync4, previousSymbol, {
504
504
  value: fs$closeSync
505
505
  });
506
- return closeSync3;
506
+ return closeSync4;
507
507
  })(fs.closeSync);
508
508
  if (/\bgfs4\b/i.test(process.env.NODE_DEBUG || "")) {
509
509
  process.on("exit", function() {
@@ -1619,7 +1619,7 @@ import { randomUUID } from "node:crypto";
1619
1619
  import { existsSync as existsSync11, mkdirSync as mkdirSync7, readFileSync as readFileSync10, realpathSync as realpathSync4, writeFileSync as writeFileSync7 } from "node:fs";
1620
1620
  import { join as join16 } from "node:path";
1621
1621
 
1622
- // ../../lib/clean-env.mjs
1622
+ // ../../hosted-hub/lib/clean-env.mjs
1623
1623
  var INHERITED_ENV = /^(EVE_|WORKFLOW_)/;
1624
1624
  function cleanEnv(env = process.env) {
1625
1625
  return Object.fromEntries(Object.entries(env).filter(([key]) => !INHERITED_ENV.test(key)));
@@ -1629,12 +1629,12 @@ function cleanGitEnv(env = process.env) {
1629
1629
  return Object.fromEntries(Object.entries(env).filter(([key]) => !GIT_REPO_ENV.test(key)));
1630
1630
  }
1631
1631
 
1632
- // ../../lib/processes.mjs
1633
- import { spawn, execFile, execFileSync } from "node:child_process";
1634
- import { closeSync, existsSync as existsSync2, mkdirSync as mkdirSync2, openSync, readFileSync as readFileSync2, readSync, renameSync, rmSync, readdirSync as readdirSync2, statSync, writeFileSync as writeFileSync2, writeSync } from "node:fs";
1632
+ // ../../hosted-hub/lib/processes.mjs
1633
+ import { spawn } from "node:child_process";
1634
+ import { closeSync as closeSync2, existsSync as existsSync2, mkdirSync as mkdirSync2, openSync as openSync2, readFileSync as readFileSync2, renameSync, rmSync, readdirSync as readdirSync2, statSync as statSync2, writeFileSync as writeFileSync2, writeSync } from "node:fs";
1635
1635
  import { join as join2 } from "node:path";
1636
1636
 
1637
- // ../../lib/fingerprint.mjs
1637
+ // ../../hosted-hub/lib/fingerprint.mjs
1638
1638
  import { createHash, createHmac, randomBytes } from "node:crypto";
1639
1639
  import { existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
1640
1640
  import { join, relative } from "node:path";
@@ -1701,22 +1701,44 @@ function fingerprintDiff(recorded, current) {
1701
1701
  }
1702
1702
  var BUILD_SKIP = /* @__PURE__ */ new Set([...TREE_SKIP, ".eve", ".output"]);
1703
1703
 
1704
- // ../../lib/processes.mjs
1705
- var num = (key, fallback) => {
1706
- const raw = process.env[key];
1707
- return raw === void 0 || raw === "" || !Number.isFinite(Number(raw)) ? fallback : Number(raw);
1708
- };
1704
+ // ../../hosted-hub/lib/log-tail.mjs
1705
+ import { closeSync, openSync, readSync, statSync } from "node:fs";
1706
+ function createLogTail(log2) {
1707
+ const tails = /* @__PURE__ */ new Map();
1708
+ function follow(name, file, offset) {
1709
+ tails.set(name, { file, offset, carry: "", prefix: `[${name}]`.padEnd(20) });
1710
+ }
1711
+ function pump() {
1712
+ for (const tail of tails.values()) {
1713
+ let size;
1714
+ try {
1715
+ size = statSync(tail.file).size;
1716
+ } catch {
1717
+ continue;
1718
+ }
1719
+ if (size < tail.offset) tail.offset = 0;
1720
+ if (size === tail.offset) continue;
1721
+ const length = Math.min(size - tail.offset, 1e6);
1722
+ const buffer = Buffer.alloc(length);
1723
+ const fd = openSync(tail.file, "r");
1724
+ try {
1725
+ readSync(fd, buffer, 0, length, tail.offset);
1726
+ } finally {
1727
+ closeSync(fd);
1728
+ }
1729
+ tail.offset += length;
1730
+ const lines = (tail.carry + buffer.toString("utf8")).split("\n");
1731
+ tail.carry = lines.pop();
1732
+ for (const line of lines) if (line.trim() && !line.startsWith("Migration file of version")) log2(`${tail.prefix} ${line}`);
1733
+ }
1734
+ }
1735
+ return { follow, pump };
1736
+ }
1737
+
1738
+ // ../../hosted-hub/lib/process-tree.mjs
1739
+ import { execFile, execFileSync } from "node:child_process";
1709
1740
  var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
1710
- var CRASH_LIMIT = num("ROSTER_CRASH_LIMIT", 5);
1711
- var CRASH_WINDOW_MS = num("ROSTER_CRASH_WINDOW_MS", 10 * 6e4);
1712
- var BACKOFF_BASE_MS = num("ROSTER_BACKOFF_BASE_MS", 1e3);
1713
- var BACKOFF_MAX_MS = num("ROSTER_BACKOFF_MAX_MS", 6e4);
1714
- var HEALTH_TIMEOUT_MS = num("ROSTER_START_HEALTH_TIMEOUT_MS", 12e4);
1715
- var PORT_RECHECK_MS = num("ROSTER_PORT_RECHECK_MS", 5e3);
1716
1741
  var KILL_GRACE_MS = 3e3;
1717
- var WATCH_MS = 1e3;
1718
- var TREE_MS = 5e3;
1719
- var LOG_ROTATE_BYTES = 2e7;
1720
1742
  function groupAlive(pgid) {
1721
1743
  if (!(pgid > 1)) return false;
1722
1744
  try {
@@ -1739,7 +1761,7 @@ var C = { ...process.env, LC_ALL: "C" };
1739
1761
  var SLOW_MS = 1e3;
1740
1762
  function slow(what, started2) {
1741
1763
  const ms = Date.now() - started2;
1742
- if (ms >= SLOW_MS) console.warn(`[registry] ${what} took ${ms}ms`);
1764
+ if (ms >= SLOW_MS) console.warn(`[runner] ${what} took ${ms}ms`);
1743
1765
  }
1744
1766
  var LSTART = String.raw`(\w{3}\s+\w{3}\s+\d+\s+[\d:]+\s+\d{4})`;
1745
1767
  function table() {
@@ -1802,6 +1824,73 @@ async function healthy(port, ms = 1e3) {
1802
1824
  }
1803
1825
  }
1804
1826
  var merge = (...lists) => [...new Map(lists.flat().map((m) => [m.pid, m])).values()];
1827
+ var treeAlive = (pgid, tree) => groupAlive(pgid) || liveMembers(tree, table()).length > 0;
1828
+ async function untilTreeGone(pgid, tree, ms) {
1829
+ const deadline = Date.now() + ms;
1830
+ while (treeAlive(pgid, tree)) {
1831
+ if (Date.now() >= deadline) return false;
1832
+ await sleep(200);
1833
+ }
1834
+ return true;
1835
+ }
1836
+ function killTree(pgid, tree) {
1837
+ signalGroup(pgid, "SIGKILL");
1838
+ for (const g of new Set(liveMembers(tree, table()).map((m) => m.pgid))) signalGroup(g, "SIGKILL");
1839
+ }
1840
+ async function stopTree(pgid, tree, { name, timeoutMs, warn }) {
1841
+ if (!treeAlive(pgid, tree)) return true;
1842
+ let membersTermed = false;
1843
+ const termMembers = () => {
1844
+ membersTermed = true;
1845
+ for (const g of new Set(liveMembers(tree, table()).map((m) => m.pgid))) if (g !== pgid) signalGroup(g, "SIGTERM");
1846
+ };
1847
+ if (groupAlive(pgid)) signalGroup(pgid, "SIGTERM");
1848
+ else termMembers();
1849
+ const deadline = Date.now() + timeoutMs;
1850
+ let gone = true;
1851
+ while (treeAlive(pgid, tree)) {
1852
+ if (!membersTermed && !groupAlive(pgid)) termMembers();
1853
+ if (Date.now() >= deadline) {
1854
+ gone = false;
1855
+ break;
1856
+ }
1857
+ await sleep(200);
1858
+ }
1859
+ if (gone) return true;
1860
+ warn(`[runner] ${name} didn't stop within ${timeoutMs / 1e3}s of SIGTERM; sending SIGKILL`);
1861
+ killTree(pgid, tree);
1862
+ return untilTreeGone(pgid, tree, KILL_GRACE_MS);
1863
+ }
1864
+ function ownsLeader(record, rows, signature) {
1865
+ if (!(record.pid > 1) || record.pgid !== record.pid) return false;
1866
+ const row = rows.find((r) => r.pid === record.pid);
1867
+ if (!row || row.pgid !== record.pgid) return false;
1868
+ if (!` ${row.command} `.includes(signature)) return false;
1869
+ const member = record.tree?.find((m) => m.pid === record.pid);
1870
+ if (member) return member.startedAt === row.startedAt;
1871
+ const recorded = Date.parse(record.startedAt);
1872
+ return !(Number.isFinite(row.startedAt) && Number.isFinite(recorded) && Math.abs(row.startedAt - recorded) > 5e3);
1873
+ }
1874
+ function ourTree(record, rows, signature) {
1875
+ const leader = ownsLeader(record, rows, signature);
1876
+ return { leader, tree: merge(leader ? treeOf(record.pid, rows) : [], liveMembers(record.tree, rows)) };
1877
+ }
1878
+
1879
+ // ../../hosted-hub/lib/processes.mjs
1880
+ var num = (key, fallback) => {
1881
+ const raw = process.env[key];
1882
+ return raw === void 0 || raw === "" || !Number.isFinite(Number(raw)) ? fallback : Number(raw);
1883
+ };
1884
+ var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
1885
+ var CRASH_LIMIT = num("ROSTER_CRASH_LIMIT", 5);
1886
+ var CRASH_WINDOW_MS = num("ROSTER_CRASH_WINDOW_MS", 10 * 6e4);
1887
+ var BACKOFF_BASE_MS = num("ROSTER_BACKOFF_BASE_MS", 1e3);
1888
+ var BACKOFF_MAX_MS = num("ROSTER_BACKOFF_MAX_MS", 6e4);
1889
+ var HEALTH_TIMEOUT_MS = num("ROSTER_START_HEALTH_TIMEOUT_MS", 12e4);
1890
+ var PORT_RECHECK_MS = num("ROSTER_PORT_RECHECK_MS", 5e3);
1891
+ var WATCH_MS = 1e3;
1892
+ var TREE_MS = 5e3;
1893
+ var LOG_ROTATE_BYTES = 2e7;
1805
1894
  function createSupervisor({
1806
1895
  root,
1807
1896
  // the workspace: eve dev's cwd
@@ -1814,18 +1903,12 @@ function createSupervisor({
1814
1903
  isCurrent = () => true,
1815
1904
  // is this agent object still on the roster?
1816
1905
  // ({ agent, env, argv }) -> what a server spawned now runs with
1817
- // (lib/fingerprint.mjs), or null. A previous registry's server is
1906
+ // (lib/fingerprint.mjs), or null. A previous runner's server is
1818
1907
  // adopted only if its recorded fingerprint matches.
1819
1908
  fingerprint = null,
1820
1909
  onChange = () => {
1821
1910
  },
1822
1911
  // status changed (broadcast)
1823
- onUsage = () => {
1824
- },
1825
- // a running tree's memory changed (status() reports rssKb)
1826
- onRecovered = () => {
1827
- },
1828
- // healthy again after a crash restart
1829
1912
  onUp = () => {
1830
1913
  },
1831
1914
  // a server is serving: adopted, spawned, or respawned ({ startedAt, adopted, recovered })
@@ -1853,10 +1936,7 @@ function createSupervisor({
1853
1936
  status: e.status,
1854
1937
  ...e.tree?.length ? { tree: e.tree } : {},
1855
1938
  ...e.lastExit ? { lastExit: e.lastExit } : {},
1856
- ...e.fingerprint ? { fingerprint: e.fingerprint } : {},
1857
- // The daemon token it was spawned with (HUB_INSTANCE): an adopted bot
1858
- // keeps its spawner's, and lib/daemon.mjs selfStopRefusal checks it here.
1859
- ...e.instance ? { instance: e.instance } : {}
1939
+ ...e.fingerprint ? { fingerprint: e.fingerprint } : {}
1860
1940
  }));
1861
1941
  mkdirSync2(stateDir, { recursive: true });
1862
1942
  const tmp = `${recordsPath}.${process.pid}.tmp`;
@@ -1877,34 +1957,7 @@ function createSupervisor({
1877
1957
  persist();
1878
1958
  onChange(entry.name);
1879
1959
  }
1880
- const tails = /* @__PURE__ */ new Map();
1881
- function follow(name, file, offset) {
1882
- tails.set(name, { file, offset, carry: "", prefix: `[${name}]`.padEnd(20) });
1883
- }
1884
- function pump() {
1885
- for (const tail of tails.values()) {
1886
- let size;
1887
- try {
1888
- size = statSync(tail.file).size;
1889
- } catch {
1890
- continue;
1891
- }
1892
- if (size < tail.offset) tail.offset = 0;
1893
- if (size === tail.offset) continue;
1894
- const length = Math.min(size - tail.offset, 1e6);
1895
- const buffer = Buffer.alloc(length);
1896
- const fd = openSync(tail.file, "r");
1897
- try {
1898
- readSync(fd, buffer, 0, length, tail.offset);
1899
- } finally {
1900
- closeSync(fd);
1901
- }
1902
- tail.offset += length;
1903
- const lines = (tail.carry + buffer.toString("utf8")).split("\n");
1904
- tail.carry = lines.pop();
1905
- for (const line of lines) if (line.trim() && !line.startsWith("Migration file of version")) log2(`${tail.prefix} ${line}`);
1906
- }
1907
- }
1960
+ const { follow, pump } = createLogTail(log2);
1908
1961
  const pumpTimer = setInterval(pump, 250);
1909
1962
  pumpTimer.unref();
1910
1963
  function refreshTree(entry, rows = table()) {
@@ -1912,10 +1965,8 @@ function createSupervisor({
1912
1965
  const before = JSON.stringify(entry.tree ?? []);
1913
1966
  entry.tree = merge(treeOf(entry.pid, rows), liveMembers(entry.tree, rows));
1914
1967
  const own = rows.filter((r) => r.pid === entry.pid || r.ppid === entry.pid);
1915
- const rss = entry.rssKb;
1916
1968
  if (own.length) entry.rssKb = own.reduce((sum, r) => sum + (r.rssKb || 0), 0);
1917
1969
  if (JSON.stringify(entry.tree) !== before) persist();
1918
- if (entry.rssKb !== rss) onUsage(entry.name);
1919
1970
  }
1920
1971
  const treeTimer = setInterval(() => {
1921
1972
  const live = [...entries.values()].filter((e) => e.pid && !e.stopping);
@@ -1924,45 +1975,8 @@ function createSupervisor({
1924
1975
  for (const e of live) refreshTree(e, rows);
1925
1976
  }, TREE_MS);
1926
1977
  treeTimer.unref();
1927
- const treeAlive = (pgid, tree) => groupAlive(pgid) || liveMembers(tree, table()).length > 0;
1928
- async function untilTreeGone(pgid, tree, ms) {
1929
- const deadline = Date.now() + ms;
1930
- while (treeAlive(pgid, tree)) {
1931
- if (Date.now() >= deadline) return false;
1932
- await sleep(200);
1933
- }
1934
- return true;
1935
- }
1936
- function killTree(pgid, tree) {
1937
- signalGroup(pgid, "SIGKILL");
1938
- for (const g of new Set(liveMembers(tree, table()).map((m) => m.pgid))) signalGroup(g, "SIGKILL");
1939
- }
1940
- async function stopTree(pgid, tree, name) {
1941
- if (!treeAlive(pgid, tree)) return true;
1942
- let membersTermed = false;
1943
- const termMembers = () => {
1944
- membersTermed = true;
1945
- for (const g of new Set(liveMembers(tree, table()).map((m) => m.pgid))) if (g !== pgid) signalGroup(g, "SIGTERM");
1946
- };
1947
- if (groupAlive(pgid)) signalGroup(pgid, "SIGTERM");
1948
- else termMembers();
1949
- const deadline = Date.now() + stopTimeoutMs;
1950
- let gone = true;
1951
- while (treeAlive(pgid, tree)) {
1952
- if (!membersTermed && !groupAlive(pgid)) termMembers();
1953
- if (Date.now() >= deadline) {
1954
- gone = false;
1955
- break;
1956
- }
1957
- await sleep(200);
1958
- }
1959
- if (gone) return true;
1960
- warn(`[registry] ${name} didn't stop within ${stopTimeoutMs / 1e3}s of SIGTERM; sending SIGKILL`);
1961
- killTree(pgid, tree);
1962
- return untilTreeGone(pgid, tree, KILL_GRACE_MS);
1963
- }
1964
1978
  const logFile = (name) => join2(logDir, `${name}.log`);
1965
- const sizeOf = (file) => existsSync2(file) ? statSync(file).size : 0;
1979
+ const sizeOf = (file) => existsSync2(file) ? statSync2(file).size : 0;
1966
1980
  function entryFor(agent) {
1967
1981
  let entry = entries.get(agent.name);
1968
1982
  if (!entry) {
@@ -1973,33 +1987,19 @@ function createSupervisor({
1973
1987
  entry.port = agent.port;
1974
1988
  return entry;
1975
1989
  }
1976
- function ownsLeader(record, rows) {
1977
- if (!(record.pid > 1) || record.pgid !== record.pid) return false;
1978
- const row = rows.find((r) => r.pid === record.pid);
1979
- if (!row || row.pgid !== record.pgid) return false;
1980
- if (!` ${row.command} `.includes(signature(record.agent, record.port))) return false;
1981
- const member = record.tree?.find((m) => m.pid === record.pid);
1982
- if (member) return member.startedAt === row.startedAt;
1983
- const recorded = Date.parse(record.startedAt);
1984
- return !(Number.isFinite(row.startedAt) && Number.isFinite(recorded) && Math.abs(row.startedAt - recorded) > 5e3);
1985
- }
1986
- function ourTree(record, rows) {
1987
- const leader = ownsLeader(record, rows);
1988
- return { leader, tree: merge(leader ? treeOf(record.pid, rows) : [], liveMembers(record.tree, rows)) };
1989
- }
1990
1990
  async function killStale(record, why) {
1991
1991
  const rows = table();
1992
- const { leader, tree } = ourTree(record, rows);
1992
+ const { leader, tree } = ourTree(record, rows, signature(record.agent, record.port));
1993
1993
  if (!leader && !tree.length) {
1994
- if (groupAlive(record.pgid)) warn(`[registry] ! ${record.agent}: process group ${record.pgid} from processes.json is no longer its server; leaving it alone`);
1994
+ if (groupAlive(record.pgid)) warn(`[runner] ! ${record.agent}: process group ${record.pgid} from processes.json is no longer its server; leaving it alone`);
1995
1995
  return true;
1996
1996
  }
1997
- warn(`[registry] ${record.agent}: stopping its stale server (pid ${record.pid}): ${why}`);
1998
- return stopTree(leader ? record.pgid : null, tree, record.agent);
1997
+ warn(`[runner] ${record.agent}: stopping its stale server (pid ${record.pid}): ${why}`);
1998
+ return stopTree(leader ? record.pgid : null, tree, { name: record.agent, timeoutMs: stopTimeoutMs, warn });
1999
1999
  }
2000
2000
  async function vetAdoption(agent, record) {
2001
2001
  const rows = table();
2002
- const { leader, tree } = ourTree(record, rows);
2002
+ const { leader, tree } = ourTree(record, rows, signature(record.agent, record.port));
2003
2003
  if (!leader && !tree.length) return { adopted: false };
2004
2004
  if (!leader) return { adopted: false, stale: true, why: "its leader is gone" };
2005
2005
  if (record.port !== agent.port) return { adopted: false, stale: true, why: `its port moved from ${record.port} to ${agent.port}` };
@@ -2031,7 +2031,7 @@ function createSupervisor({
2031
2031
  const { tree } = vetted;
2032
2032
  if (closed) return carry(agent, record, tree);
2033
2033
  const entry = entryFor(agent);
2034
- 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 });
2034
+ 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 });
2035
2035
  const gen = ++entry.gen;
2036
2036
  follow(agent.name, logFile(agent.name), sizeOf(logFile(agent.name)));
2037
2037
  entry.watch = setInterval(() => {
@@ -2042,14 +2042,14 @@ function createSupervisor({
2042
2042
  }
2043
2043
  }, WATCH_MS);
2044
2044
  entry.watch.unref();
2045
- log2(`[registry] adopted ${agent.name} (pid ${record.pid}, port ${agent.port})`);
2045
+ log2(`[runner] adopted ${agent.name} (pid ${record.pid}, port ${agent.port})`);
2046
2046
  changed(entry);
2047
2047
  onUp(agent, { startedAt: entry.startedAt, adopted: true, recovered: false });
2048
2048
  return { adopted: true };
2049
2049
  }
2050
2050
  function carry(agent, record, tree = record.tree ?? []) {
2051
2051
  const entry = entryFor(agent);
2052
- 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 });
2052
+ 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 });
2053
2053
  entry.gen++;
2054
2054
  return { adopted: true, carried: true };
2055
2055
  }
@@ -2075,7 +2075,7 @@ function createSupervisor({
2075
2075
  return spawnAgent(entry, { recovered });
2076
2076
  }
2077
2077
  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.`;
2078
- if (entry.error !== error || entry.status !== "blocked") warn(`[registry] ! ${error}`);
2078
+ if (entry.error !== error || entry.status !== "blocked") warn(`[runner] ! ${error}`);
2079
2079
  Object.assign(entry, { status: "blocked", error, pid: null, pgid: null });
2080
2080
  entry.timer = setTimeout(() => spawnAgent(entry, { recovered }), PORT_RECHECK_MS);
2081
2081
  entry.timer.unref();
@@ -2084,7 +2084,7 @@ function createSupervisor({
2084
2084
  mkdirSync2(logDir, { recursive: true });
2085
2085
  const file = logFile(name);
2086
2086
  if (sizeOf(file) > LOG_ROTATE_BYTES) renameSync(file, `${file}.1`);
2087
- const fd = openSync(file, "a");
2087
+ const fd = openSync2(file, "a");
2088
2088
  writeSync(fd, `
2089
2089
  --- ${(/* @__PURE__ */ new Date()).toISOString()} hub: starting ${name} on port ${port}
2090
2090
  `);
@@ -2094,7 +2094,7 @@ function createSupervisor({
2094
2094
  try {
2095
2095
  fp = fingerprintFor(agent, spawnEnv);
2096
2096
  } catch (error) {
2097
- warn(`[registry] ! ${name}: couldn't fingerprint its inputs (${error.message}); the next registry respawns it instead of adopting it`);
2097
+ warn(`[runner] ! ${name}: couldn't fingerprint its inputs (${error.message}); the next runner respawns it instead of adopting it`);
2098
2098
  }
2099
2099
  let child;
2100
2100
  try {
@@ -2102,17 +2102,17 @@ function createSupervisor({
2102
2102
  cwd: root,
2103
2103
  env: spawnEnv,
2104
2104
  detached: true,
2105
- // own process group; survives the registry
2105
+ // own process group; survives the runner
2106
2106
  stdio: ["ignore", fd, fd]
2107
2107
  });
2108
2108
  } finally {
2109
- closeSync(fd);
2109
+ closeSync2(fd);
2110
2110
  }
2111
2111
  const gen = ++entry.gen;
2112
- 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 });
2112
+ 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 });
2113
2113
  follow(name, file, sizeOf(file));
2114
2114
  child.once("error", (error) => {
2115
- warn(`[registry] ! couldn't start ${name}: ${error.message}`);
2115
+ warn(`[runner] ! couldn't start ${name}: ${error.message}`);
2116
2116
  exited(entry, gen, { code: null, signal: null, error: error.message });
2117
2117
  });
2118
2118
  child.once("exit", (code, signal) => exited(entry, gen, { code, signal }));
@@ -2137,15 +2137,14 @@ function createSupervisor({
2137
2137
  entry.status = "up";
2138
2138
  changed(entry);
2139
2139
  onUp(entry.agent, { startedAt: entry.startedAt, adopted: false, recovered });
2140
- if (recovered) onRecovered(entry.agent);
2141
2140
  return;
2142
2141
  }
2143
2142
  }
2144
2143
  if (Date.now() - started2 > HEALTH_TIMEOUT_MS && !entry.slowWarned) {
2145
- warn(`[registry] ! ${entry.name} isn't healthy ${HEALTH_TIMEOUT_MS / 1e3}s after starting; see ${logFile(entry.name)}`);
2144
+ warn(`[runner] ! ${entry.name} isn't healthy ${HEALTH_TIMEOUT_MS / 1e3}s after starting; see ${logFile(entry.name)}`);
2146
2145
  entry.slowWarned = true;
2147
2146
  }
2148
- await sleep(500);
2147
+ await sleep2(500);
2149
2148
  }
2150
2149
  }
2151
2150
  function exited(entry, gen, { code, signal, error }) {
@@ -2171,13 +2170,13 @@ function createSupervisor({
2171
2170
  if (entry.crashes.length >= CRASH_LIMIT) {
2172
2171
  entry.status = "crashed";
2173
2172
  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)}`;
2174
- warn(`[registry] ! ${entry.name} crashed: ${entry.error}`);
2173
+ warn(`[runner] ! ${entry.name} crashed: ${entry.error}`);
2175
2174
  return changed(entry);
2176
2175
  }
2177
2176
  const delay3 = Math.min(BACKOFF_BASE_MS * 2 ** (entry.crashes.length - 1), BACKOFF_MAX_MS);
2178
2177
  entry.status = "down";
2179
2178
  entry.nextRestartAt = new Date(now + delay3).toISOString();
2180
- warn(`[registry] ${entry.name} exited unexpectedly (${how}); restarting in ${delay3 / 1e3}s`);
2179
+ warn(`[runner] ${entry.name} exited unexpectedly (${how}); restarting in ${delay3 / 1e3}s`);
2181
2180
  entry.timer = setTimeout(async () => {
2182
2181
  await untilTreeGone(pgid, tree, KILL_GRACE_MS);
2183
2182
  if (entry.status !== "down" || entry.stopping) return;
@@ -2189,7 +2188,7 @@ function createSupervisor({
2189
2188
  changed(entry);
2190
2189
  }
2191
2190
  return {
2192
- // Startup: adopt what's still running from a previous registry, start the rest.
2191
+ // Startup: adopt what's still running from a previous runner, start the rest.
2193
2192
  startAll(agents) {
2194
2193
  starting = (async () => {
2195
2194
  const records = readRecords();
@@ -2207,7 +2206,7 @@ function createSupervisor({
2207
2206
  const result = results.get(agent.name);
2208
2207
  if (result.adopted) return;
2209
2208
  if (closed && !result.superseded) {
2210
- if (ourTree(record, table()).tree.length) carry(agent, record);
2209
+ if (ourTree(record, table(), signature(record.agent, record.port)).tree.length) carry(agent, record);
2211
2210
  return;
2212
2211
  }
2213
2212
  await killStale(record, result.why ?? "it isn't serving");
@@ -2251,7 +2250,7 @@ function createSupervisor({
2251
2250
  }
2252
2251
  entry.status = "stopping";
2253
2252
  changed(entry);
2254
- const gone = await stopTree(pgid, tree, agent.name);
2253
+ const gone = await stopTree(pgid, tree, { name: agent.name, timeoutMs: stopTimeoutMs, warn });
2255
2254
  if (gone) {
2256
2255
  if (entry.pgid === pgid) {
2257
2256
  entry.gen++;
@@ -2273,42 +2272,6 @@ function createSupervisor({
2273
2272
  pump();
2274
2273
  persist();
2275
2274
  },
2276
- // Exit without stopping any bot (a `hub restart` that keeps them): nothing
2277
- // new starts, pending (re)starts are cancelled, each tree is re-read and
2278
- // saved, and no signal is sent. The next registry vets every record
2279
- // (vetAdoption) and adopts what still matches. Returns the bots left running.
2280
- async detach() {
2281
- closed = true;
2282
- await starting.catch(() => {
2283
- });
2284
- clearInterval(pumpTimer);
2285
- clearInterval(treeTimer);
2286
- const rows = table();
2287
- for (const e of entries.values()) {
2288
- clearTimeout(e.timer);
2289
- e.timer = void 0;
2290
- e.claim = (e.claim ?? 0) + 1;
2291
- clearInterval(e.watch);
2292
- e.gen++;
2293
- if (e.pid) refreshTree(e, rows);
2294
- }
2295
- pump();
2296
- persist();
2297
- return [...entries.values()].filter((e) => e.pid).map((e) => ({ name: e.name, pid: e.pid }));
2298
- },
2299
- // Drop a removed bot's record (after its server is confirmed down).
2300
- forget(name) {
2301
- const entry = entries.get(name);
2302
- if (entry) {
2303
- clearTimeout(entry.timer);
2304
- clearInterval(entry.watch);
2305
- entry.gen++;
2306
- entry.claim = (entry.claim ?? 0) + 1;
2307
- }
2308
- entries.delete(name);
2309
- tails.delete(name);
2310
- persist();
2311
- },
2312
2275
  // Resolves true once the bot is up (spawned by us and healthy); false
2313
2276
  // once it can't be: it crashed, left the roster, or this supervisor closed.
2314
2277
  async waitUp(agent, timeoutMs) {
@@ -2318,21 +2281,10 @@ function createSupervisor({
2318
2281
  if (closed || !isCurrent(agent)) return false;
2319
2282
  if (entry?.agent === agent && entry.status === "up") return true;
2320
2283
  if (entry?.status === "crashed") return false;
2321
- await sleep(250);
2284
+ await sleep2(250);
2322
2285
  }
2323
2286
  return false;
2324
2287
  },
2325
- // A running bot's memory as of the last tree refresh (eve dev + its
2326
- // server) and its uptime (an adopted one keeps its start). Kept out of
2327
- // status(): that's in every state snapshot, and these change constantly.
2328
- usage(name) {
2329
- const e = entries.get(name);
2330
- if (!e?.pid || e.status !== "up") return void 0;
2331
- return {
2332
- ...e.rssKb ? { rssMb: Math.round(e.rssKb / 1024) } : {},
2333
- ...e.startedAt ? { uptimeS: Math.max(0, Math.round((Date.now() - Date.parse(e.startedAt)) / 1e3)) } : {}
2334
- };
2335
- },
2336
2288
  status(name) {
2337
2289
  const e = entries.get(name);
2338
2290
  if (!e) return null;
@@ -2606,7 +2558,7 @@ var status = (state, extra = {}) => {
2606
2558
  console.log(JSON.stringify({ state, ...extra }));
2607
2559
  };
2608
2560
 
2609
- // ../../lib/v26-facts.mjs
2561
+ // ../../hosted-hub/lib/v26-facts.mjs
2610
2562
  function isV26Boundary(event) {
2611
2563
  const type = event?.type;
2612
2564
  return type === "session.waiting" || type === "session.completed" || type === "session.failed";
@@ -2830,7 +2782,8 @@ function fact(type, data, source) {
2830
2782
  }
2831
2783
  var NO_RUN_SCOPE = /* @__PURE__ */ new Set(["context.started", "context.settled", "interaction.opened", "interaction.settled", "response.submitted", "response.settled"]);
2832
2784
  function content(data, kind, phase, value) {
2833
- return fact("content.completed", { partId: partIdOf(data, kind), runId: runIdOf(data), kind, phase, value }, data);
2785
+ const run4 = typeof data.stepIndex === "number" ? { runId: runIdOf(data) } : {};
2786
+ return fact("content.completed", { partId: partIdOf(data, kind), ...run4, kind, phase, value }, data);
2834
2787
  }
2835
2788
  function turnSettled(data, settled) {
2836
2789
  const turnId = str(data.turnId);
@@ -2940,7 +2893,7 @@ var str = (value) => typeof value === "string" ? value : void 0;
2940
2893
  var num2 = (value) => typeof value === "number" && Number.isFinite(value) ? value : 0;
2941
2894
  var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
2942
2895
 
2943
- // ../../lib/session-stream.mjs
2896
+ // ../../hosted-hub/lib/session-stream.mjs
2944
2897
  async function* readSessionLines(body, startIndex) {
2945
2898
  let line = startIndex;
2946
2899
  let lastAt = null;
@@ -3023,9 +2976,19 @@ async function* streamLines(eve, sessionId, o) {
3023
2976
  }
3024
2977
  const idle = new AbortController(), signal = o.signal ? AbortSignal.any([o.signal, idle.signal]) : idle.signal;
3025
2978
  let timer = setTimeout(() => idle.abort(), idleMs);
2979
+ const opened = Date.now();
2980
+ let firstByte = false, lineAt = opened;
3026
2981
  const quiet = () => {
3027
2982
  clearTimeout(timer);
3028
2983
  timer = setTimeout(() => idle.abort(), idleMs);
2984
+ if (!firstByte) {
2985
+ firstByte = true;
2986
+ console.log(JSON.stringify({ event: "hub.stream_first_byte", sessionId, startIndex: next, ms: Date.now() - opened }));
2987
+ }
2988
+ if (Date.now() - lineAt >= idleMs) {
2989
+ console.log(JSON.stringify({ event: "hub.stream_no_lines", sessionId, next, ms: Date.now() - lineAt }));
2990
+ lineAt = Date.now();
2991
+ }
3029
2992
  };
3030
2993
  let renewed = false;
3031
2994
  try {
@@ -3053,6 +3016,7 @@ async function* streamLines(eve, sessionId, o) {
3053
3016
  break;
3054
3017
  }
3055
3018
  yield record;
3019
+ lineAt = Date.now();
3056
3020
  next = record.line + 1;
3057
3021
  reopens = o.reopens ?? 3;
3058
3022
  }
@@ -3192,7 +3156,7 @@ async function downloadWorkspace(res, dir2) {
3192
3156
  }
3193
3157
 
3194
3158
  // ../../hosted-hub/runner/bot-state.ts
3195
- import { closeSync as closeSync2, openSync as openSync2, readSync as readSync2, statSync as statSync2 } from "node:fs";
3159
+ import { closeSync as closeSync3, openSync as openSync3, readSync as readSync2, statSync as statSync3 } from "node:fs";
3196
3160
  var MARKER = "hub: starting";
3197
3161
  var WINDOW = 16 * 1024;
3198
3162
  var LINES = 5;
@@ -3200,13 +3164,13 @@ var CHARS = 500;
3200
3164
  function latestRun(file) {
3201
3165
  let text;
3202
3166
  try {
3203
- const size = statSync2(file).size, fd = openSync2(file, "r");
3167
+ const size = statSync3(file).size, fd = openSync3(file, "r");
3204
3168
  try {
3205
3169
  const buf = Buffer.alloc(Math.min(size, WINDOW));
3206
3170
  readSync2(fd, buf, 0, buf.length, size - buf.length);
3207
3171
  text = buf.toString("utf8");
3208
3172
  } finally {
3209
- closeSync2(fd);
3173
+ closeSync3(fd);
3210
3174
  }
3211
3175
  } catch {
3212
3176
  return void 0;
@@ -3363,7 +3327,7 @@ async function diskUsage(path) {
3363
3327
 
3364
3328
  // ../../workspaces/snapshot.mjs
3365
3329
  import { randomBytes as randomBytes3 } from "node:crypto";
3366
- import { existsSync as existsSync5, lstatSync as lstatSync2, readdirSync as readdirSync4, readFileSync as readFileSync5, statSync as statSync3 } from "node:fs";
3330
+ import { existsSync as existsSync5, lstatSync as lstatSync2, readdirSync as readdirSync4, readFileSync as readFileSync5, statSync as statSync4 } from "node:fs";
3367
3331
  import { copyFile, mkdir, rm } from "node:fs/promises";
3368
3332
  import { tmpdir } from "node:os";
3369
3333
  import { dirname as dirname2, join as join6, relative as relative2, resolve as resolve2, sep } from "node:path";
@@ -3471,7 +3435,7 @@ async function saveCheckout(info, { threadId, name, snapshotRoot, root, label =
3471
3435
  const target = existsSync5(bundle) ? bundle.replace(/\.bundle$/, `-${sha2.slice(0, 8)}.bundle`) : bundle;
3472
3436
  await git(checkout, ["bundle", "create", "-q", target, tmpRef, "--branches", "--not", "--remotes"]);
3473
3437
  await git(checkout, ["bundle", "verify", "-q", target]);
3474
- return { checkout, kind: "bundle", bundle: target, bytes: statSync3(target).size, sha: sha2, dirty: info.dirty, unsaved: info.unsaved };
3438
+ return { checkout, kind: "bundle", bundle: target, bytes: statSync4(target).size, sha: sha2, dirty: info.dirty, unsaved: info.unsaved };
3475
3439
  } finally {
3476
3440
  await git(checkout, ["update-ref", "-d", tmpRef], { allowFail: true });
3477
3441
  }
@@ -3938,11 +3902,11 @@ function fileRecords(dir2) {
3938
3902
  }
3939
3903
 
3940
3904
  // ../../workspaces/bare-guard.mjs
3941
- import { existsSync as existsSync7, readFileSync as readFileSync6, statSync as statSync4 } from "node:fs";
3905
+ import { existsSync as existsSync7, readFileSync as readFileSync6, statSync as statSync5 } from "node:fs";
3942
3906
  import { join as join9 } from "node:path";
3943
3907
  async function repairBareFlag(repo) {
3944
3908
  const dotgit = join9(repo, ".git");
3945
- if (!existsSync7(dotgit) || !statSync4(dotgit).isDirectory()) return false;
3909
+ if (!existsSync7(dotgit) || !statSync5(dotgit).isDirectory()) return false;
3946
3910
  const file = join9(dotgit, "config");
3947
3911
  try {
3948
3912
  const text = readFileSync6(file, "utf8");
@@ -4711,7 +4675,7 @@ async function run3(config, bots = realBots) {
4711
4675
  const open = work.sessions.filter((s) => s.bot === name);
4712
4676
  for (const s of open) {
4713
4677
  const res = await cancelTurn(net, eve(name, work.human), s.eveSessionId);
4714
- if (!res.ok) throw new Error(`cancel ${s.sessionId}: HTTP ${res.status}`);
4678
+ if (!res.ok && !sessionEnded(res.status, await res.text())) throw new Error(`cancel ${s.sessionId}: HTTP ${res.status}`);
4715
4679
  }
4716
4680
  for (const s of open) await upload(s, name, work.human, s.foldedIndex + 1, { toTail: true });
4717
4681
  const sessionIds = open.map((s) => s.sessionId);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hub-runner",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
4
4
  "license": "Apache-2.0",
5
5
  "description": "Runs a hub workspace's Mac bots on this Mac under launchd, with no app needed.",
6
6
  "type": "module",