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/main.mjs CHANGED
@@ -127,29 +127,29 @@ var require_polyfills = __commonJS({
127
127
  })(fs.rename);
128
128
  }
129
129
  fs.read = typeof fs.read !== "function" ? fs.read : (function(fs$read) {
130
- function read(fd, buffer, offset, length, position, callback_) {
130
+ function read(fd, buffer, offset, length, position2, callback_) {
131
131
  var callback;
132
132
  if (callback_ && typeof callback_ === "function") {
133
133
  var eagCounter = 0;
134
134
  callback = function(er, _, __) {
135
135
  if (er && er.code === "EAGAIN" && eagCounter < 10) {
136
136
  eagCounter++;
137
- return fs$read.call(fs, fd, buffer, offset, length, position, callback);
137
+ return fs$read.call(fs, fd, buffer, offset, length, position2, callback);
138
138
  }
139
139
  callback_.apply(this, arguments);
140
140
  };
141
141
  }
142
- return fs$read.call(fs, fd, buffer, offset, length, position, callback);
142
+ return fs$read.call(fs, fd, buffer, offset, length, position2, callback);
143
143
  }
144
144
  if (Object.setPrototypeOf) Object.setPrototypeOf(read, fs$read);
145
145
  return read;
146
146
  })(fs.read);
147
147
  fs.readSync = typeof fs.readSync !== "function" ? fs.readSync : /* @__PURE__ */ (function(fs$readSync) {
148
- return function(fd, buffer, offset, length, position) {
148
+ return function(fd, buffer, offset, length, position2) {
149
149
  var eagCounter = 0;
150
150
  while (true) {
151
151
  try {
152
- return fs$readSync.call(fs, fd, buffer, offset, length, position);
152
+ return fs$readSync.call(fs, fd, buffer, offset, length, position2);
153
153
  } catch (er) {
154
154
  if (er.code === "EAGAIN" && eagCounter < 10) {
155
155
  eagCounter++;
@@ -461,10 +461,10 @@ var require_graceful_fs = __commonJS({
461
461
  }
462
462
  function noop() {
463
463
  }
464
- function publishQueue(context, queue2) {
464
+ function publishQueue(context, queue4) {
465
465
  Object.defineProperty(context, gracefulQueue, {
466
466
  get: function() {
467
- return queue2;
467
+ return queue4;
468
468
  }
469
469
  });
470
470
  }
@@ -478,8 +478,8 @@ var require_graceful_fs = __commonJS({
478
478
  console.error(m);
479
479
  };
480
480
  if (!fs[gracefulQueue]) {
481
- queue = global[gracefulQueue] || [];
482
- publishQueue(fs, queue);
481
+ queue3 = global[gracefulQueue] || [];
482
+ publishQueue(fs, queue3);
483
483
  fs.close = (function(fs$close) {
484
484
  function close(fd, cb) {
485
485
  return fs$close.call(fs, fd, function(err) {
@@ -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() {
@@ -512,7 +512,7 @@ var require_graceful_fs = __commonJS({
512
512
  });
513
513
  }
514
514
  }
515
- var queue;
515
+ var queue3;
516
516
  if (!global[gracefulQueue]) {
517
517
  publishQueue(global, fs[gracefulQueue]);
518
518
  }
@@ -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";
@@ -1675,23 +1675,23 @@ function botFingerprint({ stateDir, workspace, name, env, argv, shared }) {
1675
1675
  const envKeys = Object.fromEntries(
1676
1676
  Object.keys(env ?? {}).filter((k) => !VOLATILE_ENV.test(k)).sort().map((k) => [k, short(createHmac("sha256", key).update(`${k}\0${env[k]}`).digest("hex"))])
1677
1677
  );
1678
- const parts = {
1678
+ const parts2 = {
1679
1679
  env: short(sha(JSON.stringify(envKeys))),
1680
1680
  command: short(sha(JSON.stringify(argv))),
1681
1681
  config: configDigest(workspace, name),
1682
1682
  ...shared
1683
1683
  };
1684
- const hash = short(sha(JSON.stringify(Object.keys(parts).sort().map((k) => [k, parts[k]]))));
1685
- return { hash, parts, envKeys };
1684
+ const hash = short(sha(JSON.stringify(Object.keys(parts2).sort().map((k) => [k, parts2[k]]))));
1685
+ return { hash, parts: parts2, envKeys };
1686
1686
  }
1687
1687
  function fingerprintDiff(recorded, current) {
1688
1688
  if (!recorded?.hash) return "it was started without a fingerprint (by an older registry)";
1689
1689
  if (recorded.hash === current.hash) return null;
1690
- const parts = Object.keys({ ...recorded.parts, ...current.parts }).filter((k) => recorded.parts?.[k] !== current.parts[k]);
1690
+ const parts2 = Object.keys({ ...recorded.parts, ...current.parts }).filter((k) => recorded.parts?.[k] !== current.parts[k]);
1691
1691
  const out = [];
1692
- for (const part of parts) {
1693
- if (part !== "env" || !recorded.envKeys) {
1694
- out.push(part);
1692
+ for (const part2 of parts2) {
1693
+ if (part2 !== "env" || !recorded.envKeys) {
1694
+ out.push(part2);
1695
1695
  continue;
1696
1696
  }
1697
1697
  const keys = [.../* @__PURE__ */ new Set([...Object.keys(recorded.envKeys), ...Object.keys(current.envKeys)])].filter((k) => recorded.envKeys[k] !== current.envKeys[k]).sort();
@@ -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() {
@@ -1755,17 +1777,17 @@ function table() {
1755
1777
  }
1756
1778
  function treeOf(pid, rows) {
1757
1779
  const byPid = new Map(rows.map((r) => [r.pid, r]));
1758
- const children = /* @__PURE__ */ new Map();
1759
- for (const r of rows) (children.get(r.ppid) ?? children.set(r.ppid, []).get(r.ppid)).push(r);
1780
+ const children2 = /* @__PURE__ */ new Map();
1781
+ for (const r of rows) (children2.get(r.ppid) ?? children2.set(r.ppid, []).get(r.ppid)).push(r);
1760
1782
  const out = [];
1761
- const queue = byPid.has(pid) ? [byPid.get(pid)] : [];
1783
+ const queue3 = byPid.has(pid) ? [byPid.get(pid)] : [];
1762
1784
  const seen = /* @__PURE__ */ new Set();
1763
- while (queue.length) {
1764
- const r = queue.shift();
1785
+ while (queue3.length) {
1786
+ const r = queue3.shift();
1765
1787
  if (seen.has(r.pid)) continue;
1766
1788
  seen.add(r.pid);
1767
1789
  out.push({ pid: r.pid, pgid: r.pgid, startedAt: r.startedAt });
1768
- queue.push(...children.get(r.pid) ?? []);
1790
+ queue3.push(...children2.get(r.pid) ?? []);
1769
1791
  }
1770
1792
  return out;
1771
1793
  }
@@ -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";
@@ -2663,9 +2615,9 @@ function committedOf(type, data, deliveryIds) {
2663
2615
  case "message.received": {
2664
2616
  const turnId = str(data.turnId);
2665
2617
  if (turnId === void 0) return [];
2666
- const parts = userParts(data);
2618
+ const parts2 = userParts(data);
2667
2619
  const ids = deliveryIds.length ? deliveryIds : [deliveryIdOf(data)];
2668
- return ids.map((deliveryId) => fact("delivery.consumed", { deliveryId, turnId, parts }, data));
2620
+ return ids.map((deliveryId) => fact("delivery.consumed", { deliveryId, turnId, parts: parts2 }, data));
2669
2621
  }
2670
2622
  case "step.started": {
2671
2623
  const runId = runIdOf(data), turnId = str(data.turnId);
@@ -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);
@@ -2849,12 +2802,12 @@ function sessionStarted(data) {
2849
2802
  }
2850
2803
  function userParts(data) {
2851
2804
  if (Array.isArray(data.parts)) {
2852
- return data.parts.flatMap((part) => {
2853
- if (!isRecord(part)) return [];
2854
- if (part.type === "text" && typeof part.text === "string") return [{ kind: "text", text: part.text }];
2855
- if (part.type === "file" && typeof part.mediaType === "string") {
2856
- const url = str(part.url);
2857
- return [{ kind: "file", mediaType: part.mediaType, ...str(part.filename) === void 0 ? {} : { filename: part.filename }, ...typeof part.size === "number" ? { size: part.size } : {}, ...url === void 0 ? { unavailable: true } : { url } }];
2805
+ return data.parts.flatMap((part2) => {
2806
+ if (!isRecord(part2)) return [];
2807
+ if (part2.type === "text" && typeof part2.text === "string") return [{ kind: "text", text: part2.text }];
2808
+ if (part2.type === "file" && typeof part2.mediaType === "string") {
2809
+ const url = str(part2.url);
2810
+ return [{ kind: "file", mediaType: part2.mediaType, ...str(part2.filename) === void 0 ? {} : { filename: part2.filename }, ...typeof part2.size === "number" ? { size: part2.size } : {}, ...url === void 0 ? { unavailable: true } : { url } }];
2858
2811
  }
2859
2812
  return [];
2860
2813
  });
@@ -2923,13 +2876,13 @@ function signInRequest(data) {
2923
2876
  function signInOutcome(outcome) {
2924
2877
  return outcome === "authorized" ? "accepted" : outcome === "declined" ? "declined" : outcome === "timed-out" ? "expired" : "failed";
2925
2878
  }
2926
- function usageOf(usage) {
2879
+ function usageOf(usage2) {
2927
2880
  return {
2928
- inputTokens: num2(usage.inputTokens),
2929
- outputTokens: num2(usage.outputTokens),
2930
- cacheReadTokens: num2(usage.cacheReadTokens),
2931
- cacheWriteTokens: num2(usage.cacheWriteTokens),
2932
- ...typeof usage.costUsd === "number" ? { costUsd: usage.costUsd } : {}
2881
+ inputTokens: num2(usage2.inputTokens),
2882
+ outputTokens: num2(usage2.outputTokens),
2883
+ cacheReadTokens: num2(usage2.cacheReadTokens),
2884
+ cacheWriteTokens: num2(usage2.cacheWriteTokens),
2885
+ ...typeof usage2.costUsd === "number" ? { costUsd: usage2.costUsd } : {}
2933
2886
  };
2934
2887
  }
2935
2888
  function errorOf(data) {
@@ -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;
@@ -3021,16 +2974,26 @@ async function* streamLines(eve, sessionId, o) {
3021
2974
  query.set("follow", "false");
3022
2975
  query.set("includeTailIndex", "1");
3023
2976
  }
3024
- const idle = new AbortController(), signal = o.signal ? AbortSignal.any([o.signal, idle.signal]) : idle.signal;
3025
- let timer = setTimeout(() => idle.abort(), idleMs);
2977
+ const idle3 = new AbortController(), signal = o.signal ? AbortSignal.any([o.signal, idle3.signal]) : idle3.signal;
2978
+ let timer = setTimeout(() => idle3.abort(), idleMs);
2979
+ const opened = Date.now();
2980
+ let firstByte = false, lineAt = opened;
3026
2981
  const quiet = () => {
3027
2982
  clearTimeout(timer);
3028
- timer = setTimeout(() => idle.abort(), idleMs);
2983
+ timer = setTimeout(() => idle3.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 {
3032
2995
  const res = await fetchFn(`${eve.url}/v1/session/${encodeURIComponent(sessionId)}/stream?${query}`, { headers: eve.headers, cache: "no-store", redirect: "error", signal }).catch((error) => {
3033
- if (o.signal?.aborted || idle.signal.aborted) return void 0;
2996
+ if (o.signal?.aborted || idle3.signal.aborted) return void 0;
3034
2997
  throw error;
3035
2998
  });
3036
2999
  if (o.signal?.aborted) return;
@@ -3053,12 +3016,13 @@ 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
  }
3059
3023
  } catch (error) {
3060
3024
  if (o.signal?.aborted) return;
3061
- if (!idle.signal.aborted) throw error;
3025
+ if (!idle3.signal.aborted) throw error;
3062
3026
  }
3063
3027
  }
3064
3028
  } finally {
@@ -3075,7 +3039,16 @@ async function* watched(body, onBytes) {
3075
3039
  }
3076
3040
  }
3077
3041
 
3042
+ // ../../hosted-hub/node_modules/eve/dist/src/protocol/session-events/catalog.js
3043
+ var FACT_CATALOG = { "call.requested": { family: `call`, idField: `callId`, role: `introduces` }, "call.settled": { family: `call`, idField: `callId`, role: `terminal` }, "call.started": { family: `call`, idField: `callId`, role: `updates` }, "child.opened": { family: `child`, idField: `sessionId`, role: `introduces` }, "child.sent": { family: `child`, idField: `sessionId`, role: `record` }, "content.completed": { family: `content`, idField: `partId`, role: `complete` }, "context.settled": { family: `context`, idField: `changeId`, role: `terminal` }, "context.started": { family: `context`, idField: `changeId`, role: `introduces` }, "delivery.admitted": { family: `delivery`, idField: `deliveryId`, role: `introduces` }, "delivery.consumed": { family: `delivery`, idField: `deliveryId`, role: `updates` }, "delivery.settled": { family: `delivery`, idField: `deliveryId`, role: `terminal` }, "interaction.opened": { family: `interaction`, idField: `interactionId`, role: `introduces` }, "interaction.settled": { family: `interaction`, idField: `interactionId`, role: `terminal` }, "model.requested": { family: `model`, idField: `runId`, role: `introduces` }, "model.retried": { family: `model`, idField: `runId`, role: `updates` }, "model.settled": { family: `model`, idField: `runId`, role: `terminal` }, "model.started": { family: `model`, idField: `runId`, role: `updates` }, "response.admitted": { family: `response`, idField: `responseId`, role: `updates` }, "response.settled": { family: `response`, idField: `responseId`, role: `terminal` }, "response.submitted": { family: `response`, idField: `responseId`, role: `introduces` }, "session.ended": { family: `session`, idField: ``, role: `terminal` }, "session.started": { family: `session`, idField: ``, role: `introduces` }, "task.ended": { family: `task`, idField: `taskId`, role: `terminal` }, "task.started": { family: `task`, idField: `taskId`, role: `introduces` }, "turn.paused": { family: `turn`, idField: `turnId`, role: `updates` }, "turn.settled": { family: `turn`, idField: `turnId`, role: `terminal` }, "turn.started": { family: `turn`, idField: `turnId`, role: `introduces` }, "usage.recorded": { family: `usage`, idField: ``, role: `record` } };
3044
+
3045
+ // ../../hosted-hub/node_modules/eve/dist/src/protocol/session-projection/session-view.js
3046
+ var KEY = /* @__PURE__ */ Symbol.for(`eve.session-view-registry.v1`);
3047
+ var snapshots = globalThis[KEY] ??= /* @__PURE__ */ new WeakMap();
3048
+
3078
3049
  // ../../hosted-hub/src/store/facts.ts
3050
+ var PROGRESS_TYPES2 = ["call.input", "call.progress", "content.delta"];
3051
+ var V27_TYPES = /* @__PURE__ */ new Set([...Object.keys(FACT_CATALOG), ...PROGRESS_TYPES2]);
3079
3052
  var TURN_SETTLED = "turn.settled";
3080
3053
  var SESSION_ENDED = "session.ended";
3081
3054
  var TURN_PAUSED = "turn.paused";
@@ -3192,7 +3165,7 @@ async function downloadWorkspace(res, dir2) {
3192
3165
  }
3193
3166
 
3194
3167
  // ../../hosted-hub/runner/bot-state.ts
3195
- import { closeSync as closeSync2, openSync as openSync2, readSync as readSync2, statSync as statSync2 } from "node:fs";
3168
+ import { closeSync as closeSync3, openSync as openSync3, readSync as readSync2, statSync as statSync3 } from "node:fs";
3196
3169
  var MARKER = "hub: starting";
3197
3170
  var WINDOW = 16 * 1024;
3198
3171
  var LINES = 5;
@@ -3200,13 +3173,13 @@ var CHARS = 500;
3200
3173
  function latestRun(file) {
3201
3174
  let text;
3202
3175
  try {
3203
- const size = statSync2(file).size, fd = openSync2(file, "r");
3176
+ const size = statSync3(file).size, fd = openSync3(file, "r");
3204
3177
  try {
3205
3178
  const buf = Buffer.alloc(Math.min(size, WINDOW));
3206
3179
  readSync2(fd, buf, 0, buf.length, size - buf.length);
3207
3180
  text = buf.toString("utf8");
3208
3181
  } finally {
3209
- closeSync2(fd);
3182
+ closeSync3(fd);
3210
3183
  }
3211
3184
  } catch {
3212
3185
  return void 0;
@@ -3363,7 +3336,7 @@ async function diskUsage(path) {
3363
3336
 
3364
3337
  // ../../workspaces/snapshot.mjs
3365
3338
  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";
3339
+ import { existsSync as existsSync5, lstatSync as lstatSync2, readdirSync as readdirSync4, readFileSync as readFileSync5, statSync as statSync4 } from "node:fs";
3367
3340
  import { copyFile, mkdir, rm } from "node:fs/promises";
3368
3341
  import { tmpdir } from "node:os";
3369
3342
  import { dirname as dirname2, join as join6, relative as relative2, resolve as resolve2, sep } from "node:path";
@@ -3471,7 +3444,7 @@ async function saveCheckout(info, { threadId, name, snapshotRoot, root, label =
3471
3444
  const target = existsSync5(bundle) ? bundle.replace(/\.bundle$/, `-${sha2.slice(0, 8)}.bundle`) : bundle;
3472
3445
  await git(checkout, ["bundle", "create", "-q", target, tmpRef, "--branches", "--not", "--remotes"]);
3473
3446
  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 };
3447
+ return { checkout, kind: "bundle", bundle: target, bytes: statSync4(target).size, sha: sha2, dirty: info.dirty, unsaved: info.unsaved };
3475
3448
  } finally {
3476
3449
  await git(checkout, ["update-ref", "-d", tmpRef], { allowFail: true });
3477
3450
  }
@@ -3811,28 +3784,28 @@ function createWorkspaceManager({
3811
3784
  } else if (!snapshotRoot) throw new Error("purge needs a snapshotRoot");
3812
3785
  const bytes = await sizeOf(rec.path) ?? 0;
3813
3786
  if (dryRun) return { purged: true, dryRun, bytes, record: rec };
3814
- let snapshots = [];
3787
+ let snapshots2 = [];
3815
3788
  if (lossless) {
3816
3789
  if (!await stillEligible()) return no("ineligible");
3817
3790
  const r = await repoOp(rec.repo, () => git(rec.repo, ["worktree", "remove", rec.path], { allowFail: true }));
3818
3791
  if (r.code !== 0) return no("remove-failed", { error: r.stderr.trim() });
3819
3792
  } else {
3820
3793
  try {
3821
- snapshots = await saveAll(rec.path, { threadId: rec.threadId, name: rec.agent, snapshotRoot });
3794
+ snapshots2 = await saveAll(rec.path, { threadId: rec.threadId, name: rec.agent, snapshotRoot });
3822
3795
  } catch (error) {
3823
3796
  return no("snapshot-failed", { error: error.message, bytes });
3824
3797
  }
3825
- if (!await stillEligible()) return no("ineligible", { snapshots });
3798
+ if (!await stillEligible()) return no("ineligible", { snapshots: snapshots2 });
3826
3799
  const nested = [...new Set(linkedWorktrees(rec.path).map((w) => w.common).filter((r) => r && normPath(r) !== normPath(rec.repo) && !normPath(r).startsWith(normPath(rec.path) + "/")))];
3827
3800
  await rm2(rec.path, { recursive: true, force: true });
3828
3801
  await repoOp(rec.repo, () => git(rec.repo, ["worktree", "prune"], { allowFail: true }));
3829
3802
  for (const repo of nested) await git(repo, ["worktree", "prune"], { allowFail: true });
3830
- rec.purged = { at: now(), bytes, snapshots: snapshots.map(({ kind, repo, ref, bundle, bytes: b, sha: sha2, dirty, unsaved }) => ({ kind, repo, ref, bundle, bytes: b, sha: sha2, dirty, unsaved })) };
3803
+ rec.purged = { at: now(), bytes, snapshots: snapshots2.map(({ kind, repo, ref, bundle, bytes: b, sha: sha2, dirty, unsaved }) => ({ kind, repo, ref, bundle, bytes: b, sha: sha2, dirty, unsaved })) };
3831
3804
  }
3832
3805
  rec.state = "released";
3833
3806
  rec.headSha = head;
3834
3807
  rec.releasedAt = now();
3835
- return { purged: true, bytes, snapshots, record: await put(rec) };
3808
+ return { purged: true, bytes, snapshots: snapshots2, record: await put(rec) };
3836
3809
  }
3837
3810
  async function purge(ref, opts) {
3838
3811
  const r = await refFrom(ref);
@@ -3938,11 +3911,11 @@ function fileRecords(dir2) {
3938
3911
  }
3939
3912
 
3940
3913
  // ../../workspaces/bare-guard.mjs
3941
- import { existsSync as existsSync7, readFileSync as readFileSync6, statSync as statSync4 } from "node:fs";
3914
+ import { existsSync as existsSync7, readFileSync as readFileSync6, statSync as statSync5 } from "node:fs";
3942
3915
  import { join as join9 } from "node:path";
3943
3916
  async function repairBareFlag(repo) {
3944
3917
  const dotgit = join9(repo, ".git");
3945
- if (!existsSync7(dotgit) || !statSync4(dotgit).isDirectory()) return false;
3918
+ if (!existsSync7(dotgit) || !statSync5(dotgit).isDirectory()) return false;
3946
3919
  const file = join9(dotgit, "config");
3947
3920
  try {
3948
3921
  const text = readFileSync6(file, "utf8");
@@ -4336,7 +4309,7 @@ var MODEL_AUTH = ["project", "user", "codex"];
4336
4309
  var KINDS = ["coordinator", "planner", "builder", "reviewer", "git-ops", "demo"];
4337
4310
  var WorkspaceError = class extends Error {
4338
4311
  };
4339
- var isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
4312
+ var isObject2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
4340
4313
  var fail = (file, message) => {
4341
4314
  throw new WorkspaceError(`${file}: ${message}`);
4342
4315
  };
@@ -4346,13 +4319,13 @@ function agentRunsOn(parsed, name) {
4346
4319
  return agent.runsOn ?? parsed.runsOn ?? "hosted";
4347
4320
  }
4348
4321
  function parseWorkspace(data, file = WORKSPACE_FILE) {
4349
- if (!isObject(data)) fail(file, 'must be a JSON object with an "agents" array');
4322
+ if (!isObject2(data)) fail(file, 'must be a JSON object with an "agents" array');
4350
4323
  if (!Array.isArray(data.agents)) fail(file, '"agents" must be an array');
4351
4324
  const seen = /* @__PURE__ */ new Set();
4352
4325
  const ports = /* @__PURE__ */ new Set();
4353
4326
  const agents = data.agents.map((agent, i) => {
4354
4327
  const at = `agents[${i}]`;
4355
- if (!isObject(agent)) fail(file, `${at} must be an object`);
4328
+ if (!isObject2(agent)) fail(file, `${at} must be an object`);
4356
4329
  if (typeof agent.name !== "string" || !NAME2.test(agent.name)) fail(file, `${at}.name must be a lowercase handle like deep-diver (got ${JSON.stringify(agent.name)})`);
4357
4330
  const label = `${at} (${agent.name})`;
4358
4331
  if (seen.has(agent.name)) fail(file, `${label}: duplicate name`);
@@ -4362,13 +4335,13 @@ function parseWorkspace(data, file = WORKSPACE_FILE) {
4362
4335
  }
4363
4336
  if (agent.color !== void 0 && (typeof agent.color !== "string" || !COLOR.test(agent.color))) fail(file, `${label}.color must be a hex color like #a78bfa`);
4364
4337
  if (agent.subagents !== void 0) {
4365
- if (!isObject(agent.subagents)) fail(file, `${label}.subagents must be an object of { name: { model?, reasoning? } }`);
4338
+ if (!isObject2(agent.subagents)) fail(file, `${label}.subagents must be an object of { name: { model?, reasoning? } }`);
4366
4339
  for (const [sub, spec] of Object.entries(agent.subagents)) {
4367
- if (!isObject(spec)) fail(file, `${label}.subagents.${sub} must be an object`);
4340
+ if (!isObject2(spec)) fail(file, `${label}.subagents.${sub} must be an object`);
4368
4341
  if (spec.when !== void 0 && (typeof spec.when !== "string" || !spec.when.trim() || /\n/.test(spec.when.trim()))) fail(file, `${label}.subagents.${sub}.when must be a one-line string`);
4369
4342
  }
4370
4343
  }
4371
- if (agent.subagentCap !== void 0 && !isObject(agent.subagentCap)) fail(file, `${label}.subagentCap must be an object`);
4344
+ if (agent.subagentCap !== void 0 && !isObject2(agent.subagentCap)) fail(file, `${label}.subagentCap must be an object`);
4372
4345
  if (agent.limits !== void 0 && limitsError(agent.limits)) fail(file, `${label}.limits${limitsError(agent.limits)}`);
4373
4346
  if (agent.port !== void 0) {
4374
4347
  if (!Number.isInteger(agent.port) || agent.port < 1024 || agent.port > 65535) fail(file, `${label}.port must be an integer between 1024 and 65535`);
@@ -4394,14 +4367,14 @@ function parseWorkspace(data, file = WORKSPACE_FILE) {
4394
4367
  if (data.humans !== void 0) {
4395
4368
  if (!Array.isArray(data.humans) || !data.humans.length) fail(file, '"humans" must be a non-empty array');
4396
4369
  out.humans = data.humans.map((h, i) => {
4397
- if (!isObject(h) || typeof h.name !== "string" || !HUMAN_NAME.test(h.name)) fail(file, `humans[${i}].name must be a lowercase handle like alice`);
4370
+ if (!isObject2(h) || typeof h.name !== "string" || !HUMAN_NAME.test(h.name)) fail(file, `humans[${i}].name must be a lowercase handle like alice`);
4398
4371
  if (seen.has(h.name)) fail(file, `humans[${i}] (${h.name}) has the same name as an agent`);
4399
4372
  if (h.color !== void 0 && (typeof h.color !== "string" || !COLOR.test(h.color))) fail(file, `humans[${i}].color must be a hex color`);
4400
4373
  return { ...h };
4401
4374
  });
4402
4375
  }
4403
4376
  if (data.subagentCap !== void 0) {
4404
- if (!isObject(data.subagentCap)) fail(file, '"subagentCap" must be an object');
4377
+ if (!isObject2(data.subagentCap)) fail(file, '"subagentCap" must be an object');
4405
4378
  out.subagentCap = data.subagentCap;
4406
4379
  }
4407
4380
  if (data.limits !== void 0) {
@@ -4561,8 +4534,8 @@ function heldReports(send, failed) {
4561
4534
  if (reason) told.set(bot, reason);
4562
4535
  else told.delete(bot);
4563
4536
  });
4564
- return (waiting, why) => {
4565
- for (const bot of /* @__PURE__ */ new Set([...waiting, ...told.keys()])) {
4537
+ return (waiting3, why) => {
4538
+ for (const bot of /* @__PURE__ */ new Set([...waiting3, ...told.keys()])) {
4566
4539
  const reason = why(bot);
4567
4540
  if (now(bot) !== reason) tell(bot, reason).catch(failed);
4568
4541
  }
@@ -4710,15 +4683,15 @@ async function run3(config, bots = realBots) {
4710
4683
  async function retireDead(name, work) {
4711
4684
  const open = work.sessions.filter((s) => s.bot === name);
4712
4685
  for (const s of open) {
4713
- const res = await cancelTurn(net, eve(name, work.human), s.eveSessionId);
4714
- if (!res.ok) throw new Error(`cancel ${s.sessionId}: HTTP ${res.status}`);
4686
+ const res = await cancelTurn(net, eve(name, work.human), s.eveSessionId), body = res.ok ? "" : await res.text();
4687
+ if (!res.ok && !sessionEnded(res.status, body)) throw new Error(`cancel ${s.sessionId}: HTTP ${res.status} ${body.slice(0, 200)}`);
4715
4688
  }
4716
- for (const s of open) await upload(s, name, work.human, s.foldedIndex + 1, { toTail: true });
4689
+ return retireEnded(open, name, work.human, "started afresh");
4690
+ }
4691
+ async function retireEnded(open, name, human, why) {
4692
+ for (const s of open) await upload(s, name, human, s.foldedIndex + 1, { toTail: true });
4717
4693
  const sessionIds = open.map((s) => s.sessionId);
4718
- if (sessionIds.length) {
4719
- const { retired } = await hubJson("/api/local-runner/lost", { method: "POST", body: JSON.stringify({ sessionIds }) });
4720
- log(name, "started afresh: retired", String(retired), "session(s) its last run left open");
4721
- }
4694
+ if (sessionIds.length) log(name, `${why}: retired`, String((await hubJson("/api/local-runner/lost", { method: "POST", body: JSON.stringify({ sessionIds }) })).retired), "session(s)");
4722
4695
  return sessionIds;
4723
4696
  }
4724
4697
  const worktrees = runnerWorktrees(config.dir, () => workspace);
@@ -4787,7 +4760,7 @@ async function run3(config, bots = realBots) {
4787
4760
  }
4788
4761
  async function upload({ sessionId, eveSessionId }, bot, human, from, { toTail = false, signal } = {}) {
4789
4762
  const { url, headers } = eve(bot, human);
4790
- let pending = [], posting = Promise.resolve(), failure, dropped = false;
4763
+ let pending = [], posting = Promise.resolve(), failure3, dropped = false;
4791
4764
  const halt = new AbortController();
4792
4765
  const post = (expired = false) => posting = posting.then(async () => {
4793
4766
  const lines = pending;
@@ -4809,7 +4782,7 @@ async function run3(config, bots = realBots) {
4809
4782
  pending.push({ line: line.line, at: line.at ?? void 0, events: line.v26 ? [line.v26] : line.facts });
4810
4783
  const flushed = post();
4811
4784
  flushed.catch((e) => {
4812
- failure ??= e;
4785
+ failure3 ??= e;
4813
4786
  halt.abort();
4814
4787
  });
4815
4788
  const end = !toTail && boundaryOf(line.facts, line.v26, 0);
@@ -4820,7 +4793,7 @@ async function run3(config, bots = realBots) {
4820
4793
  } catch (error) {
4821
4794
  await posting.catch(() => {
4822
4795
  });
4823
- if (failure) throw failure;
4796
+ if (failure3) throw failure3;
4824
4797
  if (dropped) return;
4825
4798
  if (toTail && error instanceof StreamError && error.status === 404) return;
4826
4799
  if (!ended(error)) throw error;
@@ -4846,8 +4819,13 @@ async function run3(config, bots = realBots) {
4846
4819
  }
4847
4820
  async function cancel(s, human) {
4848
4821
  for (const turnId of s.cancel) {
4849
- const res = await cancelTurn(net, eve(s.bot, human), s.eveSessionId, turnId);
4850
- if (!res.ok) throw new Error(`cancel ${s.sessionId}: HTTP ${res.status}`);
4822
+ const res = await cancelTurn(net, eve(s.bot, human), s.eveSessionId, turnId), body = res.ok ? "" : await res.text();
4823
+ if (sessionEnded(res.status, body)) {
4824
+ await retireEnded([s], s.bot, human, "stop");
4825
+ wake();
4826
+ return;
4827
+ }
4828
+ if (!res.ok) throw new Error(`cancel ${s.sessionId}: HTTP ${res.status} ${body.slice(0, 200)}`);
4851
4829
  }
4852
4830
  }
4853
4831
  const restarting = /* @__PURE__ */ new Map();