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 +21 -9
- package/lib/paths.mjs +3 -0
- package/main.mjs +84 -69
- package/package.json +1 -1
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, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
@@ -31,9 +31,19 @@ function bootout() {
|
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
|
|
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
|
-
|
|
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(
|
|
46
|
-
<string>${xml(
|
|
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 (
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
464
|
+
function publishQueue(context, queue4) {
|
|
465
465
|
Object.defineProperty(context, gracefulQueue, {
|
|
466
466
|
get: function() {
|
|
467
|
-
return
|
|
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
|
-
|
|
482
|
-
publishQueue(fs,
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
1690
|
+
const parts2 = Object.keys({ ...recorded.parts, ...current.parts }).filter((k) => recorded.parts?.[k] !== current.parts[k]);
|
|
1691
1691
|
const out = [];
|
|
1692
|
-
for (const
|
|
1693
|
-
if (
|
|
1694
|
-
out.push(
|
|
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
|
|
1781
|
-
for (const r of rows) (
|
|
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
|
|
1783
|
+
const queue3 = byPid.has(pid) ? [byPid.get(pid)] : [];
|
|
1784
1784
|
const seen = /* @__PURE__ */ new Set();
|
|
1785
|
-
while (
|
|
1786
|
-
const r =
|
|
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
|
-
|
|
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
|
|
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((
|
|
2806
|
-
if (!isRecord(
|
|
2807
|
-
if (
|
|
2808
|
-
if (
|
|
2809
|
-
const url = str(
|
|
2810
|
-
return [{ kind: "file", mediaType:
|
|
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(
|
|
2879
|
+
function usageOf(usage2) {
|
|
2880
2880
|
return {
|
|
2881
|
-
inputTokens: num2(
|
|
2882
|
-
outputTokens: num2(
|
|
2883
|
-
cacheReadTokens: num2(
|
|
2884
|
-
cacheWriteTokens: num2(
|
|
2885
|
-
...typeof
|
|
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
|
|
2978
|
-
let timer = setTimeout(() =>
|
|
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(() =>
|
|
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 ||
|
|
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 (!
|
|
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
|
|
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
|
-
|
|
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:
|
|
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
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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 && !
|
|
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 (!
|
|
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 (!
|
|
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 (
|
|
4529
|
-
for (const bot of /* @__PURE__ */ new Set([...
|
|
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,
|
|
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
|
-
|
|
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(),
|
|
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
|
-
|
|
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 (
|
|
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 (
|
|
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(/\/$/, ""),
|