hub-runner 0.2.4 → 0.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/launchd.mjs CHANGED
@@ -1,9 +1,9 @@
1
1
  // The runner's launchd job: a plist in the user's LaunchAgents, loaded in the GUI domain, so it starts at login and
2
2
  // outlives every app. `RunAtLoad` without `KeepAlive`: a crash waits for `hub-runner start`.
3
3
  import { execFileSync, spawnSync } from 'node:child_process';
4
- import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
5
- import { dirname } from 'node:path';
6
- import { LABEL, credentialsFile, logFile, mainFile, plistFile, runnerDir, version } from './paths.mjs';
4
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
5
+ import { dirname, join } from 'node:path';
6
+ import { LABEL, appHome, credentialsFile, logFile, mainFile, plistFile, runnerDir, version } from './paths.mjs';
7
7
  import { deviceName } from './device.mjs';
8
8
 
9
9
  const xml = (s) => String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
@@ -31,9 +31,19 @@ function bootout() {
31
31
  }
32
32
  }
33
33
 
34
- export function plist({ takeOver }) {
34
+ /** What the job runs: the app's install through its `current` symlink, or this npm install with the shell's pnpm. */
35
+ function program(home) {
36
+ if (home) {
37
+ const bin = join(home, 'bin');
38
+ return { node: join(bin, 'node'), main: join(home, 'hub-runner', 'main.mjs'), pnpm: join(bin, 'pnpm'), path: `${bin}:${process.env.PATH}`, build: readFileSync(join(home, 'build'), 'utf8').trim() };
39
+ }
35
40
  const pnpm = execFileSync('/bin/sh', ['-c', 'command -v pnpm'], { encoding: 'utf8' }).trim();
36
- const env = { HUB_PNPM: pnpm, PATH: process.env.PATH, HUB_RUNNER_DIR: runnerDir(), HUB_RUNNER_BUILD: version(), HUB_DEVICE_NAME: deviceName(), ...(takeOver ? { HUB_RUNNER_TAKE_OVER: '1' } : {}) };
41
+ return { node: process.execPath, main: mainFile(), pnpm, path: process.env.PATH, build: version() };
42
+ }
43
+
44
+ export function plist({ takeOver }) {
45
+ const run = program(appHome());
46
+ const env = { HUB_PNPM: run.pnpm, PATH: run.path, HUB_RUNNER_DIR: runnerDir(), HUB_RUNNER_BUILD: run.build, HUB_DEVICE_NAME: deviceName(), ...(takeOver ? { HUB_RUNNER_TAKE_OVER: '1' } : {}) };
37
47
  return `<?xml version="1.0" encoding="UTF-8"?>
38
48
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
39
49
  <plist version="1.0">
@@ -42,8 +52,8 @@ export function plist({ takeOver }) {
42
52
  <string>${LABEL}</string>
43
53
  <key>ProgramArguments</key>
44
54
  <array>
45
- <string>${xml(process.execPath)}</string>
46
- <string>${xml(mainFile())}</string>
55
+ <string>${xml(run.node)}</string>
56
+ <string>${xml(run.main)}</string>
47
57
  </array>
48
58
  <key>WorkingDirectory</key>
49
59
  <string>${xml(runnerDir())}</string>
@@ -77,9 +87,11 @@ export function stop() {
77
87
  if (loaded()) bootout();
78
88
  }
79
89
 
80
- /** Starts the loaded job again, or loads it from the plist the last `start` wrote. */
90
+ /** Starts the loaded job again, or loads it from the plist the last `start` wrote. The app's install writes the plist
91
+ * again first, because its `HUB_RUNNER_BUILD` names the version `current` points at now. */
81
92
  export function kickstart() {
82
- if (loaded()) launchctl(['kickstart', '-k', `${domain()}/${LABEL}`]);
93
+ if (appHome()) start();
94
+ else if (loaded()) launchctl(['kickstart', '-k', `${domain()}/${LABEL}`]);
83
95
  else if (existsSync(plistFile())) launchctl(['bootstrap', domain(), plistFile()]);
84
96
  else throw new Error('not started yet: run `hub-runner start`');
85
97
  }
package/lib/paths.mjs CHANGED
@@ -10,3 +10,6 @@ export const logFile = () => join(runnerDir(), 'runner.log');
10
10
  export const plistFile = () => join(homedir(), 'Library', 'LaunchAgents', `${LABEL}.plist`);
11
11
  export const mainFile = () => new URL('../main.mjs', import.meta.url).pathname;
12
12
  export const version = () => JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
13
+ // The Hub app's own install (native/Sources/HubCore/RunnerInstall.swift): Node, pnpm and this package under the
14
+ // `current` symlink the app moves on each update, so the launchd job always runs the version the app installed last.
15
+ export const appHome = () => process.env.HUB_RUNNER_HOME || null;
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) {
@@ -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
  }
@@ -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();
@@ -1777,17 +1777,17 @@ function table() {
1777
1777
  }
1778
1778
  function treeOf(pid, rows) {
1779
1779
  const byPid = new Map(rows.map((r) => [r.pid, r]));
1780
- const children = /* @__PURE__ */ new Map();
1781
- 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);
1782
1782
  const out = [];
1783
- const queue = byPid.has(pid) ? [byPid.get(pid)] : [];
1783
+ const queue3 = byPid.has(pid) ? [byPid.get(pid)] : [];
1784
1784
  const seen = /* @__PURE__ */ new Set();
1785
- while (queue.length) {
1786
- const r = queue.shift();
1785
+ while (queue3.length) {
1786
+ const r = queue3.shift();
1787
1787
  if (seen.has(r.pid)) continue;
1788
1788
  seen.add(r.pid);
1789
1789
  out.push({ pid: r.pid, pgid: r.pgid, startedAt: r.startedAt });
1790
- queue.push(...children.get(r.pid) ?? []);
1790
+ queue3.push(...children2.get(r.pid) ?? []);
1791
1791
  }
1792
1792
  return out;
1793
1793
  }
@@ -2615,9 +2615,9 @@ function committedOf(type, data, deliveryIds) {
2615
2615
  case "message.received": {
2616
2616
  const turnId = str(data.turnId);
2617
2617
  if (turnId === void 0) return [];
2618
- const parts = userParts(data);
2618
+ const parts2 = userParts(data);
2619
2619
  const ids = deliveryIds.length ? deliveryIds : [deliveryIdOf(data)];
2620
- return ids.map((deliveryId) => fact("delivery.consumed", { deliveryId, turnId, parts }, data));
2620
+ return ids.map((deliveryId) => fact("delivery.consumed", { deliveryId, turnId, parts: parts2 }, data));
2621
2621
  }
2622
2622
  case "step.started": {
2623
2623
  const runId = runIdOf(data), turnId = str(data.turnId);
@@ -2802,12 +2802,12 @@ function sessionStarted(data) {
2802
2802
  }
2803
2803
  function userParts(data) {
2804
2804
  if (Array.isArray(data.parts)) {
2805
- return data.parts.flatMap((part) => {
2806
- if (!isRecord(part)) return [];
2807
- if (part.type === "text" && typeof part.text === "string") return [{ kind: "text", text: part.text }];
2808
- if (part.type === "file" && typeof part.mediaType === "string") {
2809
- const url = str(part.url);
2810
- 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 } }];
2811
2811
  }
2812
2812
  return [];
2813
2813
  });
@@ -2876,13 +2876,13 @@ function signInRequest(data) {
2876
2876
  function signInOutcome(outcome) {
2877
2877
  return outcome === "authorized" ? "accepted" : outcome === "declined" ? "declined" : outcome === "timed-out" ? "expired" : "failed";
2878
2878
  }
2879
- function usageOf(usage) {
2879
+ function usageOf(usage2) {
2880
2880
  return {
2881
- inputTokens: num2(usage.inputTokens),
2882
- outputTokens: num2(usage.outputTokens),
2883
- cacheReadTokens: num2(usage.cacheReadTokens),
2884
- cacheWriteTokens: num2(usage.cacheWriteTokens),
2885
- ...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 } : {}
2886
2886
  };
2887
2887
  }
2888
2888
  function errorOf(data) {
@@ -2974,13 +2974,13 @@ async function* streamLines(eve, sessionId, o) {
2974
2974
  query.set("follow", "false");
2975
2975
  query.set("includeTailIndex", "1");
2976
2976
  }
2977
- const idle = new AbortController(), signal = o.signal ? AbortSignal.any([o.signal, idle.signal]) : idle.signal;
2978
- 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
2979
  const opened = Date.now();
2980
2980
  let firstByte = false, lineAt = opened;
2981
2981
  const quiet = () => {
2982
2982
  clearTimeout(timer);
2983
- timer = setTimeout(() => idle.abort(), idleMs);
2983
+ timer = setTimeout(() => idle3.abort(), idleMs);
2984
2984
  if (!firstByte) {
2985
2985
  firstByte = true;
2986
2986
  console.log(JSON.stringify({ event: "hub.stream_first_byte", sessionId, startIndex: next, ms: Date.now() - opened }));
@@ -2993,7 +2993,7 @@ async function* streamLines(eve, sessionId, o) {
2993
2993
  let renewed = false;
2994
2994
  try {
2995
2995
  const res = await fetchFn(`${eve.url}/v1/session/${encodeURIComponent(sessionId)}/stream?${query}`, { headers: eve.headers, cache: "no-store", redirect: "error", signal }).catch((error) => {
2996
- if (o.signal?.aborted || idle.signal.aborted) return void 0;
2996
+ if (o.signal?.aborted || idle3.signal.aborted) return void 0;
2997
2997
  throw error;
2998
2998
  });
2999
2999
  if (o.signal?.aborted) return;
@@ -3022,7 +3022,7 @@ async function* streamLines(eve, sessionId, o) {
3022
3022
  }
3023
3023
  } catch (error) {
3024
3024
  if (o.signal?.aborted) return;
3025
- if (!idle.signal.aborted) throw error;
3025
+ if (!idle3.signal.aborted) throw error;
3026
3026
  }
3027
3027
  }
3028
3028
  } finally {
@@ -3039,7 +3039,16 @@ async function* watched(body, onBytes) {
3039
3039
  }
3040
3040
  }
3041
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
+
3042
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]);
3043
3052
  var TURN_SETTLED = "turn.settled";
3044
3053
  var SESSION_ENDED = "session.ended";
3045
3054
  var TURN_PAUSED = "turn.paused";
@@ -3775,28 +3784,28 @@ function createWorkspaceManager({
3775
3784
  } else if (!snapshotRoot) throw new Error("purge needs a snapshotRoot");
3776
3785
  const bytes = await sizeOf(rec.path) ?? 0;
3777
3786
  if (dryRun) return { purged: true, dryRun, bytes, record: rec };
3778
- let snapshots = [];
3787
+ let snapshots2 = [];
3779
3788
  if (lossless) {
3780
3789
  if (!await stillEligible()) return no("ineligible");
3781
3790
  const r = await repoOp(rec.repo, () => git(rec.repo, ["worktree", "remove", rec.path], { allowFail: true }));
3782
3791
  if (r.code !== 0) return no("remove-failed", { error: r.stderr.trim() });
3783
3792
  } else {
3784
3793
  try {
3785
- 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 });
3786
3795
  } catch (error) {
3787
3796
  return no("snapshot-failed", { error: error.message, bytes });
3788
3797
  }
3789
- if (!await stillEligible()) return no("ineligible", { snapshots });
3798
+ if (!await stillEligible()) return no("ineligible", { snapshots: snapshots2 });
3790
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) + "/")))];
3791
3800
  await rm2(rec.path, { recursive: true, force: true });
3792
3801
  await repoOp(rec.repo, () => git(rec.repo, ["worktree", "prune"], { allowFail: true }));
3793
3802
  for (const repo of nested) await git(repo, ["worktree", "prune"], { allowFail: true });
3794
- 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 })) };
3795
3804
  }
3796
3805
  rec.state = "released";
3797
3806
  rec.headSha = head;
3798
3807
  rec.releasedAt = now();
3799
- return { purged: true, bytes, snapshots, record: await put(rec) };
3808
+ return { purged: true, bytes, snapshots: snapshots2, record: await put(rec) };
3800
3809
  }
3801
3810
  async function purge(ref, opts) {
3802
3811
  const r = await refFrom(ref);
@@ -4300,7 +4309,7 @@ var MODEL_AUTH = ["project", "user", "codex"];
4300
4309
  var KINDS = ["coordinator", "planner", "builder", "reviewer", "git-ops", "demo"];
4301
4310
  var WorkspaceError = class extends Error {
4302
4311
  };
4303
- var isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
4312
+ var isObject2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
4304
4313
  var fail = (file, message) => {
4305
4314
  throw new WorkspaceError(`${file}: ${message}`);
4306
4315
  };
@@ -4310,13 +4319,13 @@ function agentRunsOn(parsed, name) {
4310
4319
  return agent.runsOn ?? parsed.runsOn ?? "hosted";
4311
4320
  }
4312
4321
  function parseWorkspace(data, file = WORKSPACE_FILE) {
4313
- 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');
4314
4323
  if (!Array.isArray(data.agents)) fail(file, '"agents" must be an array');
4315
4324
  const seen = /* @__PURE__ */ new Set();
4316
4325
  const ports = /* @__PURE__ */ new Set();
4317
4326
  const agents = data.agents.map((agent, i) => {
4318
4327
  const at = `agents[${i}]`;
4319
- if (!isObject(agent)) fail(file, `${at} must be an object`);
4328
+ if (!isObject2(agent)) fail(file, `${at} must be an object`);
4320
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)})`);
4321
4330
  const label = `${at} (${agent.name})`;
4322
4331
  if (seen.has(agent.name)) fail(file, `${label}: duplicate name`);
@@ -4326,13 +4335,13 @@ function parseWorkspace(data, file = WORKSPACE_FILE) {
4326
4335
  }
4327
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`);
4328
4337
  if (agent.subagents !== void 0) {
4329
- 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? } }`);
4330
4339
  for (const [sub, spec] of Object.entries(agent.subagents)) {
4331
- 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`);
4332
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`);
4333
4342
  }
4334
4343
  }
4335
- 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`);
4336
4345
  if (agent.limits !== void 0 && limitsError(agent.limits)) fail(file, `${label}.limits${limitsError(agent.limits)}`);
4337
4346
  if (agent.port !== void 0) {
4338
4347
  if (!Number.isInteger(agent.port) || agent.port < 1024 || agent.port > 65535) fail(file, `${label}.port must be an integer between 1024 and 65535`);
@@ -4358,14 +4367,14 @@ function parseWorkspace(data, file = WORKSPACE_FILE) {
4358
4367
  if (data.humans !== void 0) {
4359
4368
  if (!Array.isArray(data.humans) || !data.humans.length) fail(file, '"humans" must be a non-empty array');
4360
4369
  out.humans = data.humans.map((h, i) => {
4361
- 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`);
4362
4371
  if (seen.has(h.name)) fail(file, `humans[${i}] (${h.name}) has the same name as an agent`);
4363
4372
  if (h.color !== void 0 && (typeof h.color !== "string" || !COLOR.test(h.color))) fail(file, `humans[${i}].color must be a hex color`);
4364
4373
  return { ...h };
4365
4374
  });
4366
4375
  }
4367
4376
  if (data.subagentCap !== void 0) {
4368
- if (!isObject(data.subagentCap)) fail(file, '"subagentCap" must be an object');
4377
+ if (!isObject2(data.subagentCap)) fail(file, '"subagentCap" must be an object');
4369
4378
  out.subagentCap = data.subagentCap;
4370
4379
  }
4371
4380
  if (data.limits !== void 0) {
@@ -4525,8 +4534,8 @@ function heldReports(send, failed) {
4525
4534
  if (reason) told.set(bot, reason);
4526
4535
  else told.delete(bot);
4527
4536
  });
4528
- return (waiting, why) => {
4529
- 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()])) {
4530
4539
  const reason = why(bot);
4531
4540
  if (now(bot) !== reason) tell(bot, reason).catch(failed);
4532
4541
  }
@@ -4674,15 +4683,15 @@ async function run3(config, bots = realBots) {
4674
4683
  async function retireDead(name, work) {
4675
4684
  const open = work.sessions.filter((s) => s.bot === name);
4676
4685
  for (const s of open) {
4677
- const res = await cancelTurn(net, eve(name, work.human), s.eveSessionId);
4678
- if (!res.ok && !sessionEnded(res.status, await res.text())) 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)}`);
4679
4688
  }
4680
- 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 });
4681
4693
  const sessionIds = open.map((s) => s.sessionId);
4682
- if (sessionIds.length) {
4683
- const { retired } = await hubJson("/api/local-runner/lost", { method: "POST", body: JSON.stringify({ sessionIds }) });
4684
- log(name, "started afresh: retired", String(retired), "session(s) its last run left open");
4685
- }
4694
+ if (sessionIds.length) log(name, `${why}: retired`, String((await hubJson("/api/local-runner/lost", { method: "POST", body: JSON.stringify({ sessionIds }) })).retired), "session(s)");
4686
4695
  return sessionIds;
4687
4696
  }
4688
4697
  const worktrees = runnerWorktrees(config.dir, () => workspace);
@@ -4751,7 +4760,7 @@ async function run3(config, bots = realBots) {
4751
4760
  }
4752
4761
  async function upload({ sessionId, eveSessionId }, bot, human, from, { toTail = false, signal } = {}) {
4753
4762
  const { url, headers } = eve(bot, human);
4754
- let pending = [], posting = Promise.resolve(), failure, dropped = false;
4763
+ let pending = [], posting = Promise.resolve(), failure3, dropped = false;
4755
4764
  const halt = new AbortController();
4756
4765
  const post = (expired = false) => posting = posting.then(async () => {
4757
4766
  const lines = pending;
@@ -4773,7 +4782,7 @@ async function run3(config, bots = realBots) {
4773
4782
  pending.push({ line: line.line, at: line.at ?? void 0, events: line.v26 ? [line.v26] : line.facts });
4774
4783
  const flushed = post();
4775
4784
  flushed.catch((e) => {
4776
- failure ??= e;
4785
+ failure3 ??= e;
4777
4786
  halt.abort();
4778
4787
  });
4779
4788
  const end = !toTail && boundaryOf(line.facts, line.v26, 0);
@@ -4784,7 +4793,7 @@ async function run3(config, bots = realBots) {
4784
4793
  } catch (error) {
4785
4794
  await posting.catch(() => {
4786
4795
  });
4787
- if (failure) throw failure;
4796
+ if (failure3) throw failure3;
4788
4797
  if (dropped) return;
4789
4798
  if (toTail && error instanceof StreamError && error.status === 404) return;
4790
4799
  if (!ended(error)) throw error;
@@ -4810,8 +4819,13 @@ async function run3(config, bots = realBots) {
4810
4819
  }
4811
4820
  async function cancel(s, human) {
4812
4821
  for (const turnId of s.cancel) {
4813
- const res = await cancelTurn(net, eve(s.bot, human), s.eveSessionId, turnId);
4814
- 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)}`);
4815
4829
  }
4816
4830
  }
4817
4831
  const restarting = /* @__PURE__ */ new Map();
@@ -4991,6 +5005,7 @@ var deviceFile = join16(dir, "device-id");
4991
5005
  if (!existsSync11(deviceFile)) writeFileSync7(deviceFile, randomUUID());
4992
5006
  var device = readFileSync10(deviceFile, "utf8").trim();
4993
5007
  var credentials = process.env.HUB_URL || process.env.HUB_SESSION ? { hubUrl: need("HUB_URL"), session: need("HUB_SESSION"), bypass: process.env.HUB_PROTECTION_BYPASS || void 0 } : JSON.parse(readFileSync10(join16(dir, "credentials.json"), "utf8"));
5008
+ if ("workspace" in credentials && credentials.workspace) status("starting", { workspace: credentials.workspace });
4994
5009
  var deviceName = process.env.HUB_DEVICE_NAME || device;
4995
5010
  var started = await run3({
4996
5011
  hubUrl: credentials.hubUrl.replace(/\/$/, ""),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hub-runner",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
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",