@kynver-app/runtime 0.1.0
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/README.md +35 -0
- package/dist/cli.js +1467 -0
- package/dist/cli.js.map +7 -0
- package/dist/index.js +1529 -0
- package/dist/index.js.map +7 -0
- package/package.json +44 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1529 @@
|
|
|
1
|
+
// src/config.ts
|
|
2
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import path2 from "node:path";
|
|
5
|
+
|
|
6
|
+
// src/util.ts
|
|
7
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
function fail(message) {
|
|
10
|
+
console.error(message);
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
|
13
|
+
function required(value, name) {
|
|
14
|
+
if (!value) fail(`missing ${name}`);
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
17
|
+
function safeJson(line) {
|
|
18
|
+
try {
|
|
19
|
+
return JSON.parse(line);
|
|
20
|
+
} catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function readJson(file, fallback) {
|
|
25
|
+
try {
|
|
26
|
+
return JSON.parse(readFileSync(file, "utf8"));
|
|
27
|
+
} catch (error) {
|
|
28
|
+
if (arguments.length > 1) return fallback;
|
|
29
|
+
fail(`failed to read ${file}: ${error.message}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function writeJson(file, value) {
|
|
33
|
+
mkdirSync(path.dirname(file), { recursive: true });
|
|
34
|
+
writeFileSync(file, `${JSON.stringify(value, null, 2)}
|
|
35
|
+
`);
|
|
36
|
+
}
|
|
37
|
+
function safeSlug(value) {
|
|
38
|
+
return String(value || "").toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "worker";
|
|
39
|
+
}
|
|
40
|
+
function timestampSlug(name) {
|
|
41
|
+
return safeSlug(`${(/* @__PURE__ */ new Date()).toISOString().replace(/[-:]/g, "").replace(/\..+/, "Z")}-${name}`);
|
|
42
|
+
}
|
|
43
|
+
function trimTrailingSlash(url) {
|
|
44
|
+
return String(url).replace(/\/+$/, "");
|
|
45
|
+
}
|
|
46
|
+
function oneLine(value) {
|
|
47
|
+
return String(value || "").replace(/\s+/g, " ").trim();
|
|
48
|
+
}
|
|
49
|
+
function fileSize(file) {
|
|
50
|
+
try {
|
|
51
|
+
return statSync(file).size;
|
|
52
|
+
} catch {
|
|
53
|
+
return 0;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function fileMtime(file) {
|
|
57
|
+
try {
|
|
58
|
+
return statSync(file).mtime.toISOString();
|
|
59
|
+
} catch {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function tailFile(file, lines) {
|
|
64
|
+
if (!existsSync(file)) return "";
|
|
65
|
+
const data = readFileSync(file, "utf8");
|
|
66
|
+
return data.split("\n").slice(-lines).join("\n");
|
|
67
|
+
}
|
|
68
|
+
function readMaybeFile(file) {
|
|
69
|
+
return file ? readFileSync(path.resolve(file), "utf8") : "";
|
|
70
|
+
}
|
|
71
|
+
function listRunIds(runsDir) {
|
|
72
|
+
if (!existsSync(runsDir)) return [];
|
|
73
|
+
return readdirSync(runsDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
74
|
+
}
|
|
75
|
+
function sleepMs(ms) {
|
|
76
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
77
|
+
}
|
|
78
|
+
function isPidAlive(pid) {
|
|
79
|
+
if (!pid) return false;
|
|
80
|
+
try {
|
|
81
|
+
process.kill(pid, 0);
|
|
82
|
+
return true;
|
|
83
|
+
} catch {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function killWorkerProcess(pid, signal) {
|
|
88
|
+
try {
|
|
89
|
+
process.kill(-pid, signal);
|
|
90
|
+
} catch {
|
|
91
|
+
process.kill(pid, signal);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function latestIso(values) {
|
|
95
|
+
let best = null;
|
|
96
|
+
let bestMs = -Infinity;
|
|
97
|
+
for (const value of values) {
|
|
98
|
+
if (!value) continue;
|
|
99
|
+
const ms = Date.parse(value);
|
|
100
|
+
if (Number.isFinite(ms) && ms > bestMs) {
|
|
101
|
+
bestMs = ms;
|
|
102
|
+
best = value;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return best;
|
|
106
|
+
}
|
|
107
|
+
function secsAgo(ms) {
|
|
108
|
+
return Math.max(0, Math.round((Date.now() - ms) / 1e3));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// src/config.ts
|
|
112
|
+
var CONFIG_DIR = path2.join(homedir(), ".kynver");
|
|
113
|
+
var CONFIG_FILE = path2.join(CONFIG_DIR, "config.json");
|
|
114
|
+
var CREDENTIALS_FILE = path2.join(CONFIG_DIR, "credentials");
|
|
115
|
+
function loadUserConfig() {
|
|
116
|
+
if (!existsSync2(CONFIG_FILE)) return {};
|
|
117
|
+
try {
|
|
118
|
+
return JSON.parse(readFileSync2(CONFIG_FILE, "utf8"));
|
|
119
|
+
} catch {
|
|
120
|
+
return {};
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function saveUserConfig(config) {
|
|
124
|
+
mkdirSync2(CONFIG_DIR, { recursive: true });
|
|
125
|
+
writeFileSync2(CONFIG_FILE, `${JSON.stringify(config, null, 2)}
|
|
126
|
+
`, { mode: 384 });
|
|
127
|
+
}
|
|
128
|
+
function saveApiKey(apiKey) {
|
|
129
|
+
mkdirSync2(CONFIG_DIR, { recursive: true });
|
|
130
|
+
writeFileSync2(CREDENTIALS_FILE, `${JSON.stringify({ apiKey }, null, 2)}
|
|
131
|
+
`, { mode: 384 });
|
|
132
|
+
}
|
|
133
|
+
function resolveBaseUrl(argsBaseUrl) {
|
|
134
|
+
const baseUrl = argsBaseUrl || process.env.KYNVER_API_URL || process.env.OPENCLAW_CRON_FIRE_BASE_URL || loadUserConfig().apiBaseUrl;
|
|
135
|
+
if (!baseUrl) failConfig("requires --base-url, KYNVER_API_URL, OPENCLAW_CRON_FIRE_BASE_URL, or ~/.kynver/config.json apiBaseUrl");
|
|
136
|
+
return trimTrailingSlash(String(baseUrl));
|
|
137
|
+
}
|
|
138
|
+
function resolveCallbackSecret(argsSecret) {
|
|
139
|
+
const secret = argsSecret || process.env.KYNVER_RUNTIME_SECRET || process.env.OPENCLAW_CRON_SECRET;
|
|
140
|
+
if (!secret) failConfig("requires --secret, KYNVER_RUNTIME_SECRET, or OPENCLAW_CRON_SECRET");
|
|
141
|
+
return String(secret);
|
|
142
|
+
}
|
|
143
|
+
function failConfig(message) {
|
|
144
|
+
console.error(message);
|
|
145
|
+
process.exit(1);
|
|
146
|
+
}
|
|
147
|
+
function parseArgs(argv) {
|
|
148
|
+
const args = {};
|
|
149
|
+
for (let i = 0; i < argv.length; i++) {
|
|
150
|
+
const item = argv[i];
|
|
151
|
+
if (!item.startsWith("--")) continue;
|
|
152
|
+
const key = item.slice(2).replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
153
|
+
const next = argv[i + 1];
|
|
154
|
+
if (!next || next.startsWith("--")) args[key] = true;
|
|
155
|
+
else {
|
|
156
|
+
args[key] = next;
|
|
157
|
+
i++;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return args;
|
|
161
|
+
}
|
|
162
|
+
async function runSetup(args) {
|
|
163
|
+
const existing = loadUserConfig();
|
|
164
|
+
const maxWorkersRaw = typeof args.maxWorkers === "string" ? args.maxWorkers : typeof args.maxConcurrentWorkers === "string" ? args.maxConcurrentWorkers : void 0;
|
|
165
|
+
const config = {
|
|
166
|
+
...existing,
|
|
167
|
+
...typeof args.apiBaseUrl === "string" ? { apiBaseUrl: args.apiBaseUrl } : {},
|
|
168
|
+
...typeof args.agentOsSlug === "string" ? { agentOsSlug: args.agentOsSlug } : {},
|
|
169
|
+
...typeof args.agentOsId === "string" ? { agentOsId: args.agentOsId } : {},
|
|
170
|
+
...typeof args.repo === "string" ? { defaultRepo: args.repo } : {},
|
|
171
|
+
...typeof args.harnessRoot === "string" ? { harnessRoot: args.harnessRoot } : {},
|
|
172
|
+
...maxWorkersRaw ? { maxConcurrentWorkers: Math.max(1, Math.floor(Number(maxWorkersRaw))) } : {},
|
|
173
|
+
workerProvider: typeof args.provider === "string" ? args.provider : existing.workerProvider || "claude"
|
|
174
|
+
};
|
|
175
|
+
saveUserConfig(config);
|
|
176
|
+
console.log(
|
|
177
|
+
JSON.stringify(
|
|
178
|
+
{
|
|
179
|
+
ok: true,
|
|
180
|
+
configPath: CONFIG_FILE,
|
|
181
|
+
config,
|
|
182
|
+
note: "Set worker limit once with --max-workers N (or omit to auto-size from RAM). Advanced RAM tuning stays internal."
|
|
183
|
+
},
|
|
184
|
+
null,
|
|
185
|
+
2
|
|
186
|
+
)
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
async function runLogin(args) {
|
|
190
|
+
const apiKey = typeof args.apiKey === "string" ? args.apiKey : process.env.KYNVER_API_KEY;
|
|
191
|
+
if (!apiKey) failConfig("kynver login requires --api-key or KYNVER_API_KEY");
|
|
192
|
+
saveApiKey(apiKey);
|
|
193
|
+
console.log(JSON.stringify({ ok: true, credentialsPath: CREDENTIALS_FILE }, null, 2));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// src/callbacks.ts
|
|
197
|
+
async function postJson(url, secret, body) {
|
|
198
|
+
const res = await fetch(url, {
|
|
199
|
+
method: "POST",
|
|
200
|
+
headers: {
|
|
201
|
+
"Content-Type": "application/json",
|
|
202
|
+
"X-OpenClaw-Cron-Secret": String(secret),
|
|
203
|
+
"X-Kynver-Runtime-Secret": String(secret)
|
|
204
|
+
},
|
|
205
|
+
body: JSON.stringify(body)
|
|
206
|
+
});
|
|
207
|
+
let response = null;
|
|
208
|
+
try {
|
|
209
|
+
response = await res.json();
|
|
210
|
+
} catch {
|
|
211
|
+
response = null;
|
|
212
|
+
}
|
|
213
|
+
return { ok: res.ok, status: res.status, response };
|
|
214
|
+
}
|
|
215
|
+
async function getJson(url, secret) {
|
|
216
|
+
const res = await fetch(url, {
|
|
217
|
+
method: "GET",
|
|
218
|
+
headers: {
|
|
219
|
+
"X-OpenClaw-Cron-Secret": String(secret),
|
|
220
|
+
"X-Kynver-Runtime-Secret": String(secret)
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
let response = null;
|
|
224
|
+
try {
|
|
225
|
+
response = await res.json();
|
|
226
|
+
} catch {
|
|
227
|
+
response = null;
|
|
228
|
+
}
|
|
229
|
+
return { ok: res.ok, status: res.status, response };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// src/disk-gate.ts
|
|
233
|
+
import { statfsSync } from "node:fs";
|
|
234
|
+
var DEFAULT_WARN_FREE_BYTES = 30 * 1024 * 1024 * 1024;
|
|
235
|
+
var DEFAULT_CRITICAL_FREE_BYTES = 15 * 1024 * 1024 * 1024;
|
|
236
|
+
var DEFAULT_MAX_USED_PERCENT = 80;
|
|
237
|
+
var DEFAULT_HARD_MAX_USED_PERCENT = 90;
|
|
238
|
+
function observeRunnerDiskGate(input = {}) {
|
|
239
|
+
const path13 = input.diskPath?.trim() || "/";
|
|
240
|
+
const warnBelowBytes = input.diskFreeWarnBytes ?? DEFAULT_WARN_FREE_BYTES;
|
|
241
|
+
const criticalBelowBytes = input.diskFreeCriticalBytes ?? DEFAULT_CRITICAL_FREE_BYTES;
|
|
242
|
+
const maxUsedPercent = input.diskMaxUsedPercent ?? DEFAULT_MAX_USED_PERCENT;
|
|
243
|
+
const hardMaxUsedPercent = input.diskHardMaxUsedPercent ?? DEFAULT_HARD_MAX_USED_PERCENT;
|
|
244
|
+
const stats = statfsSync(path13);
|
|
245
|
+
const freeBytes = Number(stats.bavail) * Number(stats.bsize);
|
|
246
|
+
const totalBytes = Number(stats.blocks) * Number(stats.bsize);
|
|
247
|
+
const usedPercent = totalBytes > 0 ? (totalBytes - freeBytes) / totalBytes * 100 : 100;
|
|
248
|
+
const lowFree = freeBytes < warnBelowBytes;
|
|
249
|
+
const criticalFree = freeBytes < criticalBelowBytes;
|
|
250
|
+
const highUse = usedPercent > maxUsedPercent;
|
|
251
|
+
const hardHighUse = usedPercent > hardMaxUsedPercent;
|
|
252
|
+
const ok = !lowFree && !criticalFree && !highUse && !hardHighUse;
|
|
253
|
+
let reason = null;
|
|
254
|
+
if (!ok) {
|
|
255
|
+
reason = [
|
|
256
|
+
criticalFree ? `free space below critical ${criticalBelowBytes} bytes` : null,
|
|
257
|
+
lowFree ? `free space below warning ${warnBelowBytes} bytes` : null,
|
|
258
|
+
hardHighUse ? `used percent above hard cap ${hardMaxUsedPercent}%` : null,
|
|
259
|
+
highUse ? `used percent above cap ${maxUsedPercent}%` : null
|
|
260
|
+
].filter(Boolean).join("; ");
|
|
261
|
+
}
|
|
262
|
+
return {
|
|
263
|
+
ok,
|
|
264
|
+
path: path13,
|
|
265
|
+
freeBytes,
|
|
266
|
+
totalBytes,
|
|
267
|
+
usedPercent,
|
|
268
|
+
warnBelowBytes,
|
|
269
|
+
criticalBelowBytes,
|
|
270
|
+
maxUsedPercent,
|
|
271
|
+
hardMaxUsedPercent,
|
|
272
|
+
reason
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// src/resource-gate.ts
|
|
277
|
+
import os from "node:os";
|
|
278
|
+
import path5 from "node:path";
|
|
279
|
+
|
|
280
|
+
// src/run-store.ts
|
|
281
|
+
import path4 from "node:path";
|
|
282
|
+
|
|
283
|
+
// src/paths.ts
|
|
284
|
+
import { existsSync as existsSync3 } from "node:fs";
|
|
285
|
+
import { homedir as homedir2 } from "node:os";
|
|
286
|
+
import path3 from "node:path";
|
|
287
|
+
var LEGACY_ROOT = path3.join(homedir2(), ".openclaw", "harness");
|
|
288
|
+
function resolveHarnessRoot() {
|
|
289
|
+
const env = process.env.KYNVER_HARNESS_ROOT || process.env.OPUS_HARNESS_ROOT;
|
|
290
|
+
if (env) return path3.resolve(env);
|
|
291
|
+
const kynverRoot = path3.join(homedir2(), ".kynver", "harness");
|
|
292
|
+
if (existsSync3(kynverRoot)) return kynverRoot;
|
|
293
|
+
if (existsSync3(LEGACY_ROOT)) return LEGACY_ROOT;
|
|
294
|
+
return kynverRoot;
|
|
295
|
+
}
|
|
296
|
+
function getHarnessPaths() {
|
|
297
|
+
const harnessRoot = resolveHarnessRoot();
|
|
298
|
+
return {
|
|
299
|
+
harnessRoot,
|
|
300
|
+
runsDir: path3.join(harnessRoot, "runs"),
|
|
301
|
+
worktreesDir: path3.join(harnessRoot, "worktrees")
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
function runDir(runsDir, id) {
|
|
305
|
+
return path3.join(runsDir, safeSlug(id));
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// src/run-store.ts
|
|
309
|
+
function getPaths() {
|
|
310
|
+
return getHarnessPaths();
|
|
311
|
+
}
|
|
312
|
+
function loadRun(id) {
|
|
313
|
+
const { runsDir } = getPaths();
|
|
314
|
+
return readJson(path4.join(runDir(runsDir, safeSlug(id)), "run.json"));
|
|
315
|
+
}
|
|
316
|
+
function loadWorker(runId, name) {
|
|
317
|
+
const { runsDir } = getPaths();
|
|
318
|
+
return readJson(
|
|
319
|
+
path4.join(runDir(runsDir, safeSlug(runId)), "workers", safeSlug(name), "worker.json")
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
function saveRun(run) {
|
|
323
|
+
const { runsDir } = getPaths();
|
|
324
|
+
writeJson(path4.join(runDir(runsDir, run.id), "run.json"), run);
|
|
325
|
+
}
|
|
326
|
+
function saveWorker(runId, worker) {
|
|
327
|
+
const { runsDir } = getPaths();
|
|
328
|
+
writeJson(path4.join(runDir(runsDir, runId), "workers", worker.name, "worker.json"), worker);
|
|
329
|
+
}
|
|
330
|
+
function runDirectory(id) {
|
|
331
|
+
const { runsDir } = getPaths();
|
|
332
|
+
return runDir(runsDir, safeSlug(id));
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// src/heartbeat.ts
|
|
336
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3 } from "node:fs";
|
|
337
|
+
function parseHeartbeat(file) {
|
|
338
|
+
const result = {
|
|
339
|
+
heartbeatCount: 0,
|
|
340
|
+
lastHeartbeatAt: null,
|
|
341
|
+
lastHeartbeatPhase: null,
|
|
342
|
+
lastHeartbeatSummary: null,
|
|
343
|
+
heartbeatBlocker: null
|
|
344
|
+
};
|
|
345
|
+
if (!existsSync4(file)) return result;
|
|
346
|
+
const lines = readFileSync3(file, "utf8").split("\n").filter(Boolean);
|
|
347
|
+
for (const line of lines) {
|
|
348
|
+
const entry = safeJson(line);
|
|
349
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) continue;
|
|
350
|
+
const row = entry;
|
|
351
|
+
result.heartbeatCount++;
|
|
352
|
+
if (row.ts) result.lastHeartbeatAt = String(row.ts);
|
|
353
|
+
if (row.phase !== void 0 && row.phase !== null) result.lastHeartbeatPhase = String(row.phase);
|
|
354
|
+
if (row.summary !== void 0 && row.summary !== null) result.lastHeartbeatSummary = String(row.summary);
|
|
355
|
+
result.heartbeatBlocker = row.blocker ? String(row.blocker) : null;
|
|
356
|
+
}
|
|
357
|
+
return result;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// src/stream.ts
|
|
361
|
+
import { existsSync as existsSync5, readFileSync as readFileSync4 } from "node:fs";
|
|
362
|
+
function parseClaudeStream(file) {
|
|
363
|
+
const result = {
|
|
364
|
+
firstEventAt: null,
|
|
365
|
+
lastEventAt: null,
|
|
366
|
+
currentTool: null,
|
|
367
|
+
finalResult: null,
|
|
368
|
+
error: null
|
|
369
|
+
};
|
|
370
|
+
if (!existsSync5(file)) return result;
|
|
371
|
+
const lines = readFileSync4(file, "utf8").split("\n").filter(Boolean);
|
|
372
|
+
for (const line of lines) {
|
|
373
|
+
const event = safeJson(line);
|
|
374
|
+
if (!event) continue;
|
|
375
|
+
const ts = event.timestamp || event.ts;
|
|
376
|
+
if (ts) {
|
|
377
|
+
result.firstEventAt ||= ts;
|
|
378
|
+
result.lastEventAt = ts;
|
|
379
|
+
}
|
|
380
|
+
if (event.type === "stream_event" && event.event && typeof event.event === "object" && event.event.type === "content_block_start") {
|
|
381
|
+
const block = event.event.content_block;
|
|
382
|
+
if (block?.type === "tool_use") result.currentTool = String(block.name || "tool");
|
|
383
|
+
}
|
|
384
|
+
if (event.type === "assistant" && event.message && typeof event.message === "object") {
|
|
385
|
+
const content = event.message.content;
|
|
386
|
+
if (Array.isArray(content)) {
|
|
387
|
+
const tool = content.find((item) => item?.type === "tool_use");
|
|
388
|
+
if (tool) result.currentTool = String(tool.name || result.currentTool);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
if (event.type === "result") {
|
|
392
|
+
result.finalResult = event.result || event.subtype || event.terminal_reason || "completed";
|
|
393
|
+
if (event.is_error) result.error = String(event.result || event.api_error_status || "Claude result error");
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
return result;
|
|
397
|
+
}
|
|
398
|
+
function summarizeEvent(event) {
|
|
399
|
+
if (event.type === "system" && event.subtype) {
|
|
400
|
+
return `[system:${event.subtype}] ${String(event.status || event.cwd || "")}`.trim();
|
|
401
|
+
}
|
|
402
|
+
if (event.type === "stream_event" && event.event && typeof event.event === "object") {
|
|
403
|
+
const type = event.event.type;
|
|
404
|
+
if (type === "content_block_start") {
|
|
405
|
+
const block = event.event.content_block;
|
|
406
|
+
if (block?.type === "tool_use") return `[tool:start] ${block.name}`;
|
|
407
|
+
}
|
|
408
|
+
if (type === "content_block_delta") {
|
|
409
|
+
const delta = event.event.delta;
|
|
410
|
+
if (delta?.partial_json) return `[tool:input] ${delta.partial_json}`;
|
|
411
|
+
}
|
|
412
|
+
if (type === "message_stop") return "[message:stop]";
|
|
413
|
+
return type ? `[stream:${type}]` : void 0;
|
|
414
|
+
}
|
|
415
|
+
if (event.type === "assistant" && event.message && typeof event.message === "object") {
|
|
416
|
+
const content = event.message.content;
|
|
417
|
+
if (Array.isArray(content)) {
|
|
418
|
+
const text = content.find((item) => item?.type === "text");
|
|
419
|
+
if (text) return `[assistant] ${oneLine(String(text.text || ""))}`;
|
|
420
|
+
const tool = content.find((item) => item?.type === "tool_use");
|
|
421
|
+
if (tool) return `[tool] ${tool.name} ${JSON.stringify(tool.input || {})}`;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
if (event.type === "user" && event.tool_use_result) {
|
|
425
|
+
const result = event.tool_use_result;
|
|
426
|
+
return `[tool:result] stdout=${JSON.stringify(result.stdout || "")} stderr=${JSON.stringify(result.stderr || "")}`;
|
|
427
|
+
}
|
|
428
|
+
if (event.type === "result") {
|
|
429
|
+
return `[result] ${event.subtype || ""} ${oneLine(String(event.result || ""))}`.trim();
|
|
430
|
+
}
|
|
431
|
+
return void 0;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// src/git.ts
|
|
435
|
+
import { spawnSync } from "node:child_process";
|
|
436
|
+
function git(cwd, args, options = {}) {
|
|
437
|
+
const res = spawnSync("git", args, { cwd, encoding: "utf8" });
|
|
438
|
+
if (res.status !== 0 && !options.allowFailure) {
|
|
439
|
+
const message = `git ${args.join(" ")} failed: ${res.stderr || res.stdout}`;
|
|
440
|
+
if (options.throwError) throw new Error(message);
|
|
441
|
+
fail(message);
|
|
442
|
+
}
|
|
443
|
+
return res.stdout || "";
|
|
444
|
+
}
|
|
445
|
+
function ensureGitRepo(repo) {
|
|
446
|
+
git(repo, ["rev-parse", "--show-toplevel"]);
|
|
447
|
+
}
|
|
448
|
+
function gitStatusShort(worktreePath) {
|
|
449
|
+
return git(worktreePath, ["status", "--short"], { allowFailure: true }).split("\n").map((line) => line.trim()).filter(Boolean);
|
|
450
|
+
}
|
|
451
|
+
function scrubClaudeEnv(env) {
|
|
452
|
+
const next = { ...env };
|
|
453
|
+
delete next.ANTHROPIC_API_KEY;
|
|
454
|
+
return next;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// src/status.ts
|
|
458
|
+
var NO_START_MS = 18e4;
|
|
459
|
+
var STALE_MS = 6e5;
|
|
460
|
+
function computeAttention(input) {
|
|
461
|
+
const now = Date.now();
|
|
462
|
+
if (input.finalResult) return { state: "done", reason: "final result recorded" };
|
|
463
|
+
if (!input.alive) return { state: "needs_attention", reason: "process exited without a final result" };
|
|
464
|
+
if (input.heartbeatBlocker) {
|
|
465
|
+
return { state: "blocked", reason: `worker heartbeat reported blocker: ${input.heartbeatBlocker}` };
|
|
466
|
+
}
|
|
467
|
+
const startMs = input.startedAt ? Date.parse(input.startedAt) : NaN;
|
|
468
|
+
if (!input.firstEventAt && input.stdoutBytes === 0 && input.heartbeatBytes === 0 && Number.isFinite(startMs) && now - startMs > NO_START_MS) {
|
|
469
|
+
return { state: "needs_attention", reason: `no first stream event ${secsAgo(startMs)}s after start` };
|
|
470
|
+
}
|
|
471
|
+
const actMs = input.lastActivityAt ? Date.parse(input.lastActivityAt) : NaN;
|
|
472
|
+
if (Number.isFinite(actMs) && now - actMs > STALE_MS) {
|
|
473
|
+
return { state: "stale", reason: `no log/event/heartbeat activity for ${secsAgo(actMs)}s` };
|
|
474
|
+
}
|
|
475
|
+
return { state: "ok", reason: "recent activity" };
|
|
476
|
+
}
|
|
477
|
+
function computeWorkerStatus(worker) {
|
|
478
|
+
const parsed = parseClaudeStream(worker.stdoutPath);
|
|
479
|
+
const heartbeat = parseHeartbeat(worker.heartbeatPath);
|
|
480
|
+
const alive = isPidAlive(worker.pid);
|
|
481
|
+
const stdoutBytes = fileSize(worker.stdoutPath);
|
|
482
|
+
const stderrBytes = fileSize(worker.stderrPath);
|
|
483
|
+
const heartbeatBytes = fileSize(worker.heartbeatPath);
|
|
484
|
+
const changedFiles = gitStatusShort(worker.worktreePath);
|
|
485
|
+
const lastActivityAt = latestIso([
|
|
486
|
+
parsed.lastEventAt,
|
|
487
|
+
heartbeat.lastHeartbeatAt,
|
|
488
|
+
fileMtime(worker.stdoutPath),
|
|
489
|
+
fileMtime(worker.stderrPath),
|
|
490
|
+
fileMtime(worker.heartbeatPath)
|
|
491
|
+
]);
|
|
492
|
+
const attention = computeAttention({
|
|
493
|
+
alive,
|
|
494
|
+
finalResult: parsed.finalResult,
|
|
495
|
+
firstEventAt: parsed.firstEventAt,
|
|
496
|
+
stdoutBytes,
|
|
497
|
+
heartbeatBytes,
|
|
498
|
+
lastActivityAt,
|
|
499
|
+
heartbeatBlocker: heartbeat.heartbeatBlocker,
|
|
500
|
+
startedAt: worker.startedAt
|
|
501
|
+
});
|
|
502
|
+
return {
|
|
503
|
+
runId: worker.runId,
|
|
504
|
+
worker: worker.name,
|
|
505
|
+
pid: worker.pid,
|
|
506
|
+
alive,
|
|
507
|
+
status: parsed.finalResult ? "done" : alive ? "running" : "exited",
|
|
508
|
+
attention,
|
|
509
|
+
branch: worker.branch,
|
|
510
|
+
worktreePath: worker.worktreePath,
|
|
511
|
+
ownedPaths: worker.ownedPaths,
|
|
512
|
+
stdoutBytes,
|
|
513
|
+
stderrBytes,
|
|
514
|
+
heartbeatBytes,
|
|
515
|
+
firstEventAt: parsed.firstEventAt,
|
|
516
|
+
lastEventAt: parsed.lastEventAt,
|
|
517
|
+
lastActivityAt,
|
|
518
|
+
currentTool: parsed.currentTool,
|
|
519
|
+
heartbeatCount: heartbeat.heartbeatCount,
|
|
520
|
+
lastHeartbeatAt: heartbeat.lastHeartbeatAt,
|
|
521
|
+
lastHeartbeatPhase: heartbeat.lastHeartbeatPhase,
|
|
522
|
+
lastHeartbeatSummary: heartbeat.lastHeartbeatSummary,
|
|
523
|
+
heartbeatBlocker: heartbeat.heartbeatBlocker,
|
|
524
|
+
finalResult: parsed.finalResult,
|
|
525
|
+
error: parsed.error || (!alive && !parsed.finalResult ? tailFile(worker.stderrPath, 10).trim() || void 0 : void 0),
|
|
526
|
+
changedFiles
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
function isFinishedWorkerStatus(status) {
|
|
530
|
+
if (status.finalResult) return true;
|
|
531
|
+
if (status.alive === false) return true;
|
|
532
|
+
if (status.status === "exited" || status.status === "done") return true;
|
|
533
|
+
return false;
|
|
534
|
+
}
|
|
535
|
+
function deriveRunStatus(fallback, workers) {
|
|
536
|
+
if (workers.length === 0) return fallback;
|
|
537
|
+
if (workers.some((w) => w.attention === "needs_attention" || w.attention === "stale" || w.attention === "blocked")) {
|
|
538
|
+
return "needs_attention";
|
|
539
|
+
}
|
|
540
|
+
if (workers.every((w) => w.status === "done")) return "done";
|
|
541
|
+
if (workers.some((w) => w.status === "running")) return "running";
|
|
542
|
+
return fallback;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// src/resource-gate.ts
|
|
546
|
+
var DEFAULT_PER_WORKER_MEM_BYTES = 500 * 1024 * 1024;
|
|
547
|
+
var DEFAULT_MEM_RESERVE_BYTES = 4 * 1024 * 1024 * 1024;
|
|
548
|
+
var DEFAULT_MEM_UTILIZATION = 0.85;
|
|
549
|
+
var AUTO_MAX_WORKERS_CEILING = 64;
|
|
550
|
+
function positiveInt(value, fallback) {
|
|
551
|
+
const n = Number(value);
|
|
552
|
+
if (!Number.isFinite(n) || n <= 0) return fallback;
|
|
553
|
+
return Math.floor(n);
|
|
554
|
+
}
|
|
555
|
+
function resolveResourceConfig(config = loadUserConfig(), configuredMaxWorkersOverride) {
|
|
556
|
+
const perWorkerMemBytes = positiveInt(config.perWorkerMemBytes, DEFAULT_PER_WORKER_MEM_BYTES);
|
|
557
|
+
const memReserveBytes = positiveInt(config.memReserveBytes, DEFAULT_MEM_RESERVE_BYTES);
|
|
558
|
+
const memUtilization = Math.min(
|
|
559
|
+
1,
|
|
560
|
+
Math.max(0.1, Number(config.memUtilization) > 0 ? Number(config.memUtilization) : DEFAULT_MEM_UTILIZATION)
|
|
561
|
+
);
|
|
562
|
+
const envCap = process.env.KYNVER_MAX_WORKERS ? positiveInt(process.env.KYNVER_MAX_WORKERS, 0) || null : null;
|
|
563
|
+
const configuredMaxWorkers = configuredMaxWorkersOverride !== void 0 ? configuredMaxWorkersOverride : envCap ?? (config.maxConcurrentWorkers !== void 0 && config.maxConcurrentWorkers !== null ? positiveInt(config.maxConcurrentWorkers, 0) || null : null);
|
|
564
|
+
return { perWorkerMemBytes, memReserveBytes, memUtilization, configuredMaxWorkers };
|
|
565
|
+
}
|
|
566
|
+
function computeAutoMaxWorkers(totalMemBytes, opts = {}) {
|
|
567
|
+
const perWorkerMemBytes = opts.perWorkerMemBytes ?? DEFAULT_PER_WORKER_MEM_BYTES;
|
|
568
|
+
const memReserveBytes = opts.memReserveBytes ?? DEFAULT_MEM_RESERVE_BYTES;
|
|
569
|
+
const memUtilization = opts.memUtilization ?? DEFAULT_MEM_UTILIZATION;
|
|
570
|
+
const budgetBytes = Math.max(0, Math.floor(totalMemBytes * memUtilization) - memReserveBytes);
|
|
571
|
+
const raw = Math.max(1, Math.floor(budgetBytes / perWorkerMemBytes));
|
|
572
|
+
return Math.min(raw, AUTO_MAX_WORKERS_CEILING);
|
|
573
|
+
}
|
|
574
|
+
function countActiveWorkers(runId) {
|
|
575
|
+
const run = loadRun(runId);
|
|
576
|
+
let active = 0;
|
|
577
|
+
for (const name of Object.keys(run.workers || {})) {
|
|
578
|
+
const worker = readJson(
|
|
579
|
+
path5.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
580
|
+
null
|
|
581
|
+
);
|
|
582
|
+
if (!worker) continue;
|
|
583
|
+
const status = computeWorkerStatus(worker);
|
|
584
|
+
if (status.alive && !status.finalResult && status.attention.state !== "done") {
|
|
585
|
+
active++;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
return active;
|
|
589
|
+
}
|
|
590
|
+
function observeRunnerResourceGate(input) {
|
|
591
|
+
const { perWorkerMemBytes, memReserveBytes, memUtilization, configuredMaxWorkers } = resolveResourceConfig(
|
|
592
|
+
input.config,
|
|
593
|
+
input.configuredMaxWorkersOverride
|
|
594
|
+
);
|
|
595
|
+
const totalMemBytes = input.totalMemBytes ?? os.totalmem();
|
|
596
|
+
const freeMemBytes = input.freeMemBytes ?? os.freemem();
|
|
597
|
+
const activeWorkers = input.activeWorkers ?? countActiveWorkers(input.runId);
|
|
598
|
+
const budgetBytes = Math.max(0, Math.floor(totalMemBytes * memUtilization) - memReserveBytes);
|
|
599
|
+
const capacityFromTotal = Math.max(0, Math.floor(budgetBytes / perWorkerMemBytes));
|
|
600
|
+
const capacityFromFree = Math.max(0, Math.floor(Math.max(0, freeMemBytes - memReserveBytes) / perWorkerMemBytes));
|
|
601
|
+
const autoCap = computeAutoMaxWorkers(totalMemBytes, { perWorkerMemBytes, memReserveBytes, memUtilization });
|
|
602
|
+
const targetCap = configuredMaxWorkers ?? autoCap;
|
|
603
|
+
const maxConcurrentWorkers = Math.max(0, Math.min(targetCap, capacityFromTotal));
|
|
604
|
+
const slotsByCapacity = Math.max(0, maxConcurrentWorkers - activeWorkers);
|
|
605
|
+
const slotsByFreeMem = Math.max(0, capacityFromFree - activeWorkers);
|
|
606
|
+
const slotsAvailable = Math.min(slotsByCapacity, slotsByFreeMem);
|
|
607
|
+
let reason = null;
|
|
608
|
+
if (slotsAvailable <= 0) {
|
|
609
|
+
if (activeWorkers >= maxConcurrentWorkers) {
|
|
610
|
+
reason = `at worker limit (${activeWorkers}/${maxConcurrentWorkers} running)`;
|
|
611
|
+
} else if (capacityFromFree <= activeWorkers) {
|
|
612
|
+
reason = "insufficient free memory \u2014 waiting for workers to finish";
|
|
613
|
+
} else {
|
|
614
|
+
reason = "no worker slots available";
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
return {
|
|
618
|
+
ok: slotsAvailable > 0,
|
|
619
|
+
totalMemBytes,
|
|
620
|
+
freeMemBytes,
|
|
621
|
+
memReserveBytes,
|
|
622
|
+
perWorkerMemBytes,
|
|
623
|
+
configuredMaxWorkers,
|
|
624
|
+
autoCap,
|
|
625
|
+
capacityWorkers: capacityFromTotal,
|
|
626
|
+
maxConcurrentWorkers,
|
|
627
|
+
activeWorkers,
|
|
628
|
+
slotsAvailable,
|
|
629
|
+
reason
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// src/supervisor.ts
|
|
634
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync3 } from "node:fs";
|
|
635
|
+
import path6 from "node:path";
|
|
636
|
+
|
|
637
|
+
// src/prompt.ts
|
|
638
|
+
function buildPrompt(input) {
|
|
639
|
+
const ownership = input.ownedPaths.length ? `Owned paths: ${input.ownedPaths.join(", ")}. Do not edit outside these paths without stopping and reporting why.` : "Owned paths: unrestricted for this worker, but keep edits tightly scoped.";
|
|
640
|
+
return [
|
|
641
|
+
"You are running under the Kynver AgentOS runtime.",
|
|
642
|
+
"Immediately state your plan before editing.",
|
|
643
|
+
ownership,
|
|
644
|
+
`Worktree: ${input.worktreePath}`,
|
|
645
|
+
`Progress heartbeat file: ${input.heartbeatPath}`,
|
|
646
|
+
"After each major step, append one JSON line to the heartbeat file with fields: ts, phase, summary, changedFiles, blocker.",
|
|
647
|
+
"Final response must include files changed, verification commands, and unresolved risks.",
|
|
648
|
+
"",
|
|
649
|
+
"Task:",
|
|
650
|
+
input.task
|
|
651
|
+
].join("\n");
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// src/providers/claude.ts
|
|
655
|
+
import { closeSync, openSync } from "node:fs";
|
|
656
|
+
import { spawn } from "node:child_process";
|
|
657
|
+
var claudeProvider = {
|
|
658
|
+
name: "claude",
|
|
659
|
+
start(opts) {
|
|
660
|
+
const model = opts.model || "claude-opus-4-7";
|
|
661
|
+
const stdoutFd = openSync(opts.stdoutPath, "a");
|
|
662
|
+
const stderrFd = openSync(opts.stderrPath, "a");
|
|
663
|
+
const child = spawn(
|
|
664
|
+
"claude",
|
|
665
|
+
[
|
|
666
|
+
"--model",
|
|
667
|
+
model,
|
|
668
|
+
"-p",
|
|
669
|
+
"--verbose",
|
|
670
|
+
"--permission-mode",
|
|
671
|
+
"bypassPermissions",
|
|
672
|
+
"--output-format",
|
|
673
|
+
"stream-json",
|
|
674
|
+
"--include-partial-messages",
|
|
675
|
+
opts.prompt
|
|
676
|
+
],
|
|
677
|
+
{
|
|
678
|
+
cwd: opts.worktreePath,
|
|
679
|
+
detached: true,
|
|
680
|
+
stdio: ["ignore", stdoutFd, stderrFd],
|
|
681
|
+
env: scrubClaudeEnv(process.env)
|
|
682
|
+
}
|
|
683
|
+
);
|
|
684
|
+
closeSync(stdoutFd);
|
|
685
|
+
closeSync(stderrFd);
|
|
686
|
+
if (!child.pid) {
|
|
687
|
+
throw new Error("failed to spawn claude worker process (is the `claude` CLI on PATH?)");
|
|
688
|
+
}
|
|
689
|
+
child.unref();
|
|
690
|
+
return { pid: child.pid, model };
|
|
691
|
+
}
|
|
692
|
+
};
|
|
693
|
+
|
|
694
|
+
// src/providers/registry.ts
|
|
695
|
+
var BUILTIN = {
|
|
696
|
+
claude: claudeProvider
|
|
697
|
+
};
|
|
698
|
+
var overrideProvider = null;
|
|
699
|
+
function resolveWorkerProvider(name) {
|
|
700
|
+
if (overrideProvider) return overrideProvider;
|
|
701
|
+
const configured = (name || loadUserConfig().workerProvider || "claude").trim();
|
|
702
|
+
const provider = BUILTIN[configured];
|
|
703
|
+
if (!provider) {
|
|
704
|
+
throw new Error(`unknown worker provider "${configured}" \u2014 supported: ${Object.keys(BUILTIN).join(", ")}`);
|
|
705
|
+
}
|
|
706
|
+
return provider;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
// src/supervisor.ts
|
|
710
|
+
function spawnWorkerProcess(run, opts) {
|
|
711
|
+
const name = safeSlug(opts.name);
|
|
712
|
+
if (!opts.name) throw new Error("worker name is required");
|
|
713
|
+
if (run.workers?.[name]) throw new Error(`worker already exists in run ${run.id}: ${name}`);
|
|
714
|
+
if (!opts.task) throw new Error(`missing task text for worker ${name}`);
|
|
715
|
+
const { worktreesDir } = getPaths();
|
|
716
|
+
const workerDir = path6.join(runDirectory(run.id), "workers", name);
|
|
717
|
+
mkdirSync3(workerDir, { recursive: true });
|
|
718
|
+
const worktreePath = path6.join(worktreesDir, run.id, name);
|
|
719
|
+
const branch = opts.branch || `agent/${run.id}/${name}`;
|
|
720
|
+
if (existsSync6(worktreePath)) throw new Error(`worktree path already exists: ${worktreePath}`);
|
|
721
|
+
git(run.repo, ["fetch", "origin", "--prune"], { allowFailure: true });
|
|
722
|
+
git(run.repo, ["worktree", "add", "-b", branch, worktreePath, run.baseCommit], { throwError: true });
|
|
723
|
+
const stdoutPath = path6.join(workerDir, "stdout.jsonl");
|
|
724
|
+
const stderrPath = path6.join(workerDir, "stderr.log");
|
|
725
|
+
const heartbeatPath = path6.join(workerDir, "heartbeat.jsonl");
|
|
726
|
+
const prompt = buildPrompt({
|
|
727
|
+
task: opts.task,
|
|
728
|
+
ownedPaths: opts.ownedPaths || [],
|
|
729
|
+
worktreePath,
|
|
730
|
+
heartbeatPath
|
|
731
|
+
});
|
|
732
|
+
const provider = resolveWorkerProvider(opts.provider);
|
|
733
|
+
let started;
|
|
734
|
+
try {
|
|
735
|
+
started = provider.start({
|
|
736
|
+
name,
|
|
737
|
+
task: opts.task,
|
|
738
|
+
ownedPaths: opts.ownedPaths,
|
|
739
|
+
model: opts.model,
|
|
740
|
+
branch,
|
|
741
|
+
worktreePath,
|
|
742
|
+
workerDir,
|
|
743
|
+
stdoutPath,
|
|
744
|
+
stderrPath,
|
|
745
|
+
heartbeatPath,
|
|
746
|
+
prompt
|
|
747
|
+
});
|
|
748
|
+
} catch (error) {
|
|
749
|
+
git(run.repo, ["worktree", "remove", "--force", worktreePath], { allowFailure: true });
|
|
750
|
+
git(run.repo, ["branch", "-D", branch], { allowFailure: true });
|
|
751
|
+
throw error;
|
|
752
|
+
}
|
|
753
|
+
const model = started.model || opts.model || "claude-opus-4-7";
|
|
754
|
+
const worker = {
|
|
755
|
+
name,
|
|
756
|
+
runId: run.id,
|
|
757
|
+
status: "running",
|
|
758
|
+
pid: started.pid,
|
|
759
|
+
model,
|
|
760
|
+
branch,
|
|
761
|
+
worktreePath,
|
|
762
|
+
workerDir,
|
|
763
|
+
stdoutPath,
|
|
764
|
+
stderrPath,
|
|
765
|
+
heartbeatPath,
|
|
766
|
+
ownedPaths: opts.ownedPaths,
|
|
767
|
+
...opts.agentOsId ? { agentOsId: String(opts.agentOsId) } : {},
|
|
768
|
+
...opts.taskId ? { taskId: String(opts.taskId) } : {},
|
|
769
|
+
...opts.leaseOwner ? { leaseOwner: String(opts.leaseOwner) } : {},
|
|
770
|
+
...opts.dispatched ? { dispatched: true } : {},
|
|
771
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
772
|
+
};
|
|
773
|
+
saveWorker(run.id, worker);
|
|
774
|
+
run.workers = { ...run.workers || {}, [name]: { workerDir, statusPath: path6.join(workerDir, "worker.json") } };
|
|
775
|
+
run.status = "running";
|
|
776
|
+
saveRun(run);
|
|
777
|
+
return worker;
|
|
778
|
+
}
|
|
779
|
+
function startWorker(args) {
|
|
780
|
+
const run = loadRun(String(args.run));
|
|
781
|
+
const task = args.task ? String(args.task) : readMaybeFile(args.taskFile ? String(args.taskFile) : void 0);
|
|
782
|
+
if (!task) {
|
|
783
|
+
console.error("missing --task or --task-file");
|
|
784
|
+
process.exit(1);
|
|
785
|
+
}
|
|
786
|
+
try {
|
|
787
|
+
const worker = spawnWorkerProcess(run, {
|
|
788
|
+
name: String(args.name),
|
|
789
|
+
task,
|
|
790
|
+
ownedPaths: args.owned ? String(args.owned).split(",").map((s) => s.trim()).filter(Boolean) : [],
|
|
791
|
+
model: args.model ? String(args.model) : void 0,
|
|
792
|
+
branch: args.branch ? String(args.branch) : void 0,
|
|
793
|
+
agentOsId: args.agentOsId ? String(args.agentOsId) : void 0,
|
|
794
|
+
taskId: args.taskId ? String(args.taskId) : void 0,
|
|
795
|
+
provider: args.provider ? String(args.provider) : void 0
|
|
796
|
+
});
|
|
797
|
+
console.log(
|
|
798
|
+
JSON.stringify(
|
|
799
|
+
{
|
|
800
|
+
runId: run.id,
|
|
801
|
+
worker: worker.name,
|
|
802
|
+
pid: worker.pid,
|
|
803
|
+
branch: worker.branch,
|
|
804
|
+
worktreePath: worker.worktreePath,
|
|
805
|
+
workerDir: worker.workerDir
|
|
806
|
+
},
|
|
807
|
+
null,
|
|
808
|
+
2
|
|
809
|
+
)
|
|
810
|
+
);
|
|
811
|
+
} catch (error) {
|
|
812
|
+
console.error(`worker start failed: ${error.message}`);
|
|
813
|
+
process.exit(1);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
// src/dispatch.ts
|
|
818
|
+
var DEFAULT_DISPATCH_LEASE_MS = 60 * 60 * 1e3;
|
|
819
|
+
function buildDispatchTaskText(task, agentOsId) {
|
|
820
|
+
return [
|
|
821
|
+
`[AgentOS task ${task.id}] ${task.title}`,
|
|
822
|
+
"",
|
|
823
|
+
task.description ? String(task.description) : "(no description on the board task)",
|
|
824
|
+
"",
|
|
825
|
+
`Board linkage: agentOsId=${agentOsId}, taskId=${task.id}, attempt=${task.attempt}, executor=${task.executor}${task.executorRef ? `, executorRef=${task.executorRef}` : ""}.`,
|
|
826
|
+
"This worker was dispatched from the AgentOS board. The harness reports your completion back to the board when you finish."
|
|
827
|
+
].join("\n");
|
|
828
|
+
}
|
|
829
|
+
async function dispatchRun(args) {
|
|
830
|
+
const pipeline = args.pipeline === true || args.pipeline === "true";
|
|
831
|
+
try {
|
|
832
|
+
const run = loadRun(String(required(String(args.run || ""), "--run")));
|
|
833
|
+
const agentOsId = String(required(String(args.agentOsId || ""), "--agent-os-id"));
|
|
834
|
+
const base = resolveBaseUrl(args.baseUrl ? String(args.baseUrl) : void 0);
|
|
835
|
+
const secret = resolveCallbackSecret(args.secret ? String(args.secret) : void 0);
|
|
836
|
+
const execute = args.execute === true || args.execute === "true";
|
|
837
|
+
const dryRun = !execute;
|
|
838
|
+
const leaseOwner = `openclaw-harness:${run.id}`;
|
|
839
|
+
const runnerDiskGate = args.diskPath ? observeRunnerDiskGate({ diskPath: String(args.diskPath) }) : observeRunnerDiskGate({ diskPath: run.repo });
|
|
840
|
+
const runnerResourceGate = observeRunnerResourceGate({ runId: run.id });
|
|
841
|
+
const requestedStarts = Number(args.maxStarts) > 0 ? Math.floor(Number(args.maxStarts)) : 1;
|
|
842
|
+
const cappedStarts = dryRun ? requestedStarts : Math.min(requestedStarts, runnerResourceGate.slotsAvailable);
|
|
843
|
+
const dispatchUrl = `${base}/api/agent-os/by-id/${encodeURIComponent(agentOsId)}/tasks/dispatch-next`;
|
|
844
|
+
const body = {
|
|
845
|
+
agentOsId,
|
|
846
|
+
dryRun,
|
|
847
|
+
maxStarts: cappedStarts,
|
|
848
|
+
leaseOwner,
|
|
849
|
+
leaseDurationMs: Number(args.leaseMs) > 0 ? Math.floor(Number(args.leaseMs)) : DEFAULT_DISPATCH_LEASE_MS,
|
|
850
|
+
runnerDiskGate,
|
|
851
|
+
runnerResourceGate,
|
|
852
|
+
...args.lane ? { lane: String(args.lane) } : {},
|
|
853
|
+
...args.diskPath ? { diskPath: String(args.diskPath) } : {}
|
|
854
|
+
};
|
|
855
|
+
const dispatch = await postJson(dispatchUrl, secret, body);
|
|
856
|
+
const responseBody = dispatch.response;
|
|
857
|
+
if (!dispatch.ok || !responseBody?.result) {
|
|
858
|
+
const failure = {
|
|
859
|
+
runId: run.id,
|
|
860
|
+
agentOsId,
|
|
861
|
+
action: "dispatch",
|
|
862
|
+
httpStatus: dispatch.status,
|
|
863
|
+
response: dispatch.response
|
|
864
|
+
};
|
|
865
|
+
if (pipeline) return { ok: false, ...failure };
|
|
866
|
+
console.log(JSON.stringify(failure, null, 2));
|
|
867
|
+
process.exit(1);
|
|
868
|
+
}
|
|
869
|
+
const result = responseBody.result;
|
|
870
|
+
if (dryRun) {
|
|
871
|
+
const summary2 = {
|
|
872
|
+
runId: run.id,
|
|
873
|
+
agentOsId,
|
|
874
|
+
dryRun: true,
|
|
875
|
+
wouldStart: result.started.map((d) => ({
|
|
876
|
+
taskId: d.task.id,
|
|
877
|
+
title: d.task.title,
|
|
878
|
+
reason: d.reason
|
|
879
|
+
})),
|
|
880
|
+
skipped: result.skipped.map(
|
|
881
|
+
(d) => ({ taskId: d.task.id, skipReason: d.skipReason, reason: d.reason })
|
|
882
|
+
),
|
|
883
|
+
diskGate: result.diskGate,
|
|
884
|
+
resourceGate: result.resourceGate,
|
|
885
|
+
inspected: result.inspected
|
|
886
|
+
};
|
|
887
|
+
if (pipeline) return { ok: true, ...summary2 };
|
|
888
|
+
console.log(JSON.stringify(summary2, null, 2));
|
|
889
|
+
return;
|
|
890
|
+
}
|
|
891
|
+
if (!dryRun && cappedStarts <= 0) {
|
|
892
|
+
const summary2 = {
|
|
893
|
+
runId: run.id,
|
|
894
|
+
agentOsId,
|
|
895
|
+
dryRun: false,
|
|
896
|
+
skipped: true,
|
|
897
|
+
reason: runnerResourceGate.reason ?? "no resource slots",
|
|
898
|
+
resourceGate: runnerResourceGate
|
|
899
|
+
};
|
|
900
|
+
if (pipeline) return { ok: true, ...summary2 };
|
|
901
|
+
console.log(JSON.stringify(summary2, null, 2));
|
|
902
|
+
return;
|
|
903
|
+
}
|
|
904
|
+
const outcomes = [];
|
|
905
|
+
for (const decision of result.started) {
|
|
906
|
+
const task = decision.task;
|
|
907
|
+
const name = safeSlug(`t-${task.id}-a${task.attempt}`);
|
|
908
|
+
try {
|
|
909
|
+
const worker = spawnWorkerProcess(run, {
|
|
910
|
+
name,
|
|
911
|
+
task: buildDispatchTaskText(task, agentOsId),
|
|
912
|
+
ownedPaths: args.owned ? String(args.owned).split(",").map((s) => s.trim()).filter(Boolean) : [],
|
|
913
|
+
model: args.model ? String(args.model) : void 0,
|
|
914
|
+
agentOsId,
|
|
915
|
+
taskId: String(task.id),
|
|
916
|
+
leaseOwner,
|
|
917
|
+
dispatched: true
|
|
918
|
+
});
|
|
919
|
+
outcomes.push({
|
|
920
|
+
taskId: task.id,
|
|
921
|
+
started: true,
|
|
922
|
+
worker: worker.name,
|
|
923
|
+
pid: worker.pid,
|
|
924
|
+
branch: worker.branch
|
|
925
|
+
});
|
|
926
|
+
} catch (error) {
|
|
927
|
+
const releaseUrl = `${base}/api/agent-os/by-id/${encodeURIComponent(agentOsId)}/tasks/${encodeURIComponent(String(task.id))}/release`;
|
|
928
|
+
let release;
|
|
929
|
+
try {
|
|
930
|
+
release = await postJson(releaseUrl, secret, { agentOsId, leaseOwner });
|
|
931
|
+
} catch (relErr) {
|
|
932
|
+
release = { ok: false, error: relErr.message };
|
|
933
|
+
}
|
|
934
|
+
outcomes.push({
|
|
935
|
+
taskId: task.id,
|
|
936
|
+
started: false,
|
|
937
|
+
error: error.message,
|
|
938
|
+
released: release.ok === true || release.response?.ok === true,
|
|
939
|
+
releaseResponse: release.response ?? release
|
|
940
|
+
});
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
const summary = {
|
|
944
|
+
runId: run.id,
|
|
945
|
+
agentOsId,
|
|
946
|
+
dryRun: false,
|
|
947
|
+
leaseOwner,
|
|
948
|
+
startedCount: outcomes.filter((o) => o.started).length,
|
|
949
|
+
outcomes,
|
|
950
|
+
skipped: result.skipped.map((d) => ({
|
|
951
|
+
taskId: d.task.id,
|
|
952
|
+
skipReason: d.skipReason
|
|
953
|
+
})),
|
|
954
|
+
diskGate: result.diskGate,
|
|
955
|
+
resourceGate: result.resourceGate
|
|
956
|
+
};
|
|
957
|
+
if (pipeline) {
|
|
958
|
+
return { ok: !outcomes.some((o) => !o.started), ...summary };
|
|
959
|
+
}
|
|
960
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
961
|
+
if (outcomes.some((o) => !o.started)) process.exit(1);
|
|
962
|
+
} catch (error) {
|
|
963
|
+
if (pipeline) return { ok: false, error: error.message };
|
|
964
|
+
console.error(`run dispatch failed: ${error.message}`);
|
|
965
|
+
process.exit(1);
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
// src/redact.ts
|
|
970
|
+
function redactHarness(text, secret) {
|
|
971
|
+
let out = text;
|
|
972
|
+
if (secret) out = out.split(secret).join("[REDACTED_SECRET]");
|
|
973
|
+
out = out.replace(/kynver_[a-z0-9_]+/gi, "[REDACTED_KYNVER_TOKEN]");
|
|
974
|
+
out = out.replace(/sk-[a-zA-Z0-9_-]+/g, "[REDACTED_API_KEY]");
|
|
975
|
+
return out;
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
// src/validate.ts
|
|
979
|
+
import path7 from "node:path";
|
|
980
|
+
var RUN_ID_RE = /^[a-z0-9][a-z0-9._-]{0,127}$/i;
|
|
981
|
+
var WORKER_NAME_RE = /^[a-z0-9][a-z0-9._-]{0,63}$/i;
|
|
982
|
+
function validateRunId(runId) {
|
|
983
|
+
const trimmed = runId.trim();
|
|
984
|
+
if (!RUN_ID_RE.test(trimmed)) throw new Error(`invalid run id: ${runId}`);
|
|
985
|
+
return trimmed;
|
|
986
|
+
}
|
|
987
|
+
function validateWorkerName(name) {
|
|
988
|
+
const trimmed = name.trim();
|
|
989
|
+
if (!WORKER_NAME_RE.test(trimmed)) throw new Error(`invalid worker name: ${name}`);
|
|
990
|
+
return trimmed;
|
|
991
|
+
}
|
|
992
|
+
function validateRepo(repo) {
|
|
993
|
+
const resolved = path7.resolve(repo);
|
|
994
|
+
if (resolved.includes("..")) throw new Error("repo path must not contain .. segments");
|
|
995
|
+
return resolved;
|
|
996
|
+
}
|
|
997
|
+
function validateOwnedPaths(repoRoot, ownedPaths) {
|
|
998
|
+
return ownedPaths.map((owned) => {
|
|
999
|
+
const resolved = path7.resolve(repoRoot, owned);
|
|
1000
|
+
const rel = path7.relative(repoRoot, resolved);
|
|
1001
|
+
if (rel.startsWith("..") || path7.isAbsolute(rel)) {
|
|
1002
|
+
throw new Error(`owned path escapes repo: ${owned}`);
|
|
1003
|
+
}
|
|
1004
|
+
return resolved;
|
|
1005
|
+
});
|
|
1006
|
+
}
|
|
1007
|
+
function validateTailLines(lines) {
|
|
1008
|
+
if (!Number.isFinite(lines) || lines <= 0 || lines > 500) return 40;
|
|
1009
|
+
return Math.floor(lines);
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
// src/worktree.ts
|
|
1013
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync4 } from "node:fs";
|
|
1014
|
+
import path8 from "node:path";
|
|
1015
|
+
function createRun(args) {
|
|
1016
|
+
const repo = validateRepo(required(String(args.repo || ""), "--repo"));
|
|
1017
|
+
ensureGitRepo(repo);
|
|
1018
|
+
const id = args.id ? validateRunId(String(args.id)) : timestampSlug(String(args.name || "run"));
|
|
1019
|
+
const dir = runDirectory(id);
|
|
1020
|
+
if (existsSync7(dir)) failExists(`run already exists: ${id}`);
|
|
1021
|
+
mkdirSync4(dir, { recursive: true });
|
|
1022
|
+
const base = String(args.base || "origin/main");
|
|
1023
|
+
const baseCommit = git(repo, ["rev-parse", base]).trim();
|
|
1024
|
+
const run = {
|
|
1025
|
+
id,
|
|
1026
|
+
name: String(args.name || id),
|
|
1027
|
+
repo,
|
|
1028
|
+
base,
|
|
1029
|
+
baseCommit,
|
|
1030
|
+
status: "created",
|
|
1031
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1032
|
+
workers: {}
|
|
1033
|
+
};
|
|
1034
|
+
writeJson(path8.join(dir, "run.json"), run);
|
|
1035
|
+
console.log(JSON.stringify({ runId: id, runDir: dir, repo, base, baseCommit }, null, 2));
|
|
1036
|
+
}
|
|
1037
|
+
function listRuns() {
|
|
1038
|
+
const { runsDir } = getPaths();
|
|
1039
|
+
const rows = listRunIds(runsDir).map((id) => readJson(path8.join(runDirectory(id), "run.json"), null)).filter(Boolean).map((run) => ({
|
|
1040
|
+
id: run.id,
|
|
1041
|
+
name: run.name,
|
|
1042
|
+
status: run.status,
|
|
1043
|
+
repo: run.repo,
|
|
1044
|
+
createdAt: run.createdAt
|
|
1045
|
+
}));
|
|
1046
|
+
console.log(JSON.stringify(rows, null, 2));
|
|
1047
|
+
}
|
|
1048
|
+
function failExists(message) {
|
|
1049
|
+
console.error(message);
|
|
1050
|
+
process.exit(1);
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
// src/sweep.ts
|
|
1054
|
+
import path9 from "node:path";
|
|
1055
|
+
async function sweepRun(args) {
|
|
1056
|
+
const pipeline = args.pipeline === true || args.pipeline === "true";
|
|
1057
|
+
try {
|
|
1058
|
+
const run = loadRun(String(required(String(args.run || ""), "--run")));
|
|
1059
|
+
const agentOsId = String(required(String(args.agentOsId || ""), "--agent-os-id"));
|
|
1060
|
+
const base = resolveBaseUrl(args.baseUrl ? String(args.baseUrl) : void 0);
|
|
1061
|
+
const secret = resolveCallbackSecret(args.secret ? String(args.secret) : void 0);
|
|
1062
|
+
const leaseOwner = `openclaw-harness:${run.id}`;
|
|
1063
|
+
const releasedLocalOrphans = [];
|
|
1064
|
+
for (const name of Object.keys(run.workers || {})) {
|
|
1065
|
+
const worker = readJson(
|
|
1066
|
+
path9.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
1067
|
+
null
|
|
1068
|
+
);
|
|
1069
|
+
if (!worker || !worker.dispatched || !worker.taskId) continue;
|
|
1070
|
+
const status = computeWorkerStatus(worker);
|
|
1071
|
+
if (status.alive) continue;
|
|
1072
|
+
if (status.finalResult) continue;
|
|
1073
|
+
const releaseUrl = `${base}/api/agent-os/by-id/${encodeURIComponent(agentOsId)}/tasks/${encodeURIComponent(String(worker.taskId))}/release`;
|
|
1074
|
+
let release;
|
|
1075
|
+
try {
|
|
1076
|
+
release = await postJson(releaseUrl, secret, { agentOsId, leaseOwner });
|
|
1077
|
+
} catch (relErr) {
|
|
1078
|
+
release = { ok: false, error: relErr.message };
|
|
1079
|
+
}
|
|
1080
|
+
releasedLocalOrphans.push({
|
|
1081
|
+
worker: name,
|
|
1082
|
+
taskId: worker.taskId,
|
|
1083
|
+
pid: worker.pid,
|
|
1084
|
+
released: release.ok === true || release.response?.ok === true,
|
|
1085
|
+
response: release.response ?? release
|
|
1086
|
+
});
|
|
1087
|
+
}
|
|
1088
|
+
const reapUrl = `${base}/api/agent-os/by-id/${encodeURIComponent(agentOsId)}/tasks/reap`;
|
|
1089
|
+
let reap;
|
|
1090
|
+
try {
|
|
1091
|
+
reap = await postJson(reapUrl, secret, {
|
|
1092
|
+
agentOsId,
|
|
1093
|
+
...Number(args.graceMs) >= 0 && args.graceMs !== void 0 && args.graceMs !== true ? { graceMs: Math.floor(Number(args.graceMs)) } : {}
|
|
1094
|
+
});
|
|
1095
|
+
} catch (reapErr) {
|
|
1096
|
+
reap = { ok: false, error: reapErr.message };
|
|
1097
|
+
}
|
|
1098
|
+
const summary = { runId: run.id, agentOsId, leaseOwner, releasedLocalOrphans, reap: reap.response ?? reap };
|
|
1099
|
+
if (pipeline) return { ok: true, ...summary };
|
|
1100
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
1101
|
+
} catch (error) {
|
|
1102
|
+
if (pipeline) return { ok: false, error: error.message };
|
|
1103
|
+
console.error(`run sweep failed: ${error.message}`);
|
|
1104
|
+
process.exit(1);
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
// src/worker-ops.ts
|
|
1109
|
+
import path10 from "node:path";
|
|
1110
|
+
async function tryCompleteWorker(args) {
|
|
1111
|
+
const worker = loadWorker(String(args.run), String(args.name));
|
|
1112
|
+
const status = computeWorkerStatus(worker);
|
|
1113
|
+
const agentOsId = (args.agentOsId ? String(args.agentOsId) : worker.agentOsId) || "";
|
|
1114
|
+
const taskId = (args.taskId ? String(args.taskId) : worker.taskId) || null;
|
|
1115
|
+
if (!agentOsId) {
|
|
1116
|
+
return { ok: false, reason: "missing agentOsId" };
|
|
1117
|
+
}
|
|
1118
|
+
if (!isFinishedWorkerStatus(status)) {
|
|
1119
|
+
return { ok: true, skipped: true, reason: "worker-not-finished" };
|
|
1120
|
+
}
|
|
1121
|
+
const base = resolveBaseUrl(args.baseUrl ? String(args.baseUrl) : void 0);
|
|
1122
|
+
const secret = resolveCallbackSecret(args.secret ? String(args.secret) : void 0);
|
|
1123
|
+
const url = `${base}/api/agent-os/by-id/${encodeURIComponent(agentOsId)}/harness/completion`;
|
|
1124
|
+
const body = {
|
|
1125
|
+
source: "openclaw-harness",
|
|
1126
|
+
agentOsId,
|
|
1127
|
+
runId: worker.runId,
|
|
1128
|
+
workerName: worker.name,
|
|
1129
|
+
taskId,
|
|
1130
|
+
startedAt: worker.startedAt,
|
|
1131
|
+
finishedAt: status.lastActivityAt || (/* @__PURE__ */ new Date()).toISOString(),
|
|
1132
|
+
status
|
|
1133
|
+
};
|
|
1134
|
+
const res = await fetch(url, {
|
|
1135
|
+
method: "POST",
|
|
1136
|
+
headers: {
|
|
1137
|
+
"Content-Type": "application/json",
|
|
1138
|
+
"X-OpenClaw-Cron-Secret": secret,
|
|
1139
|
+
"X-Kynver-Runtime-Secret": secret
|
|
1140
|
+
},
|
|
1141
|
+
body: JSON.stringify(body)
|
|
1142
|
+
});
|
|
1143
|
+
let parsed = null;
|
|
1144
|
+
try {
|
|
1145
|
+
parsed = await res.json();
|
|
1146
|
+
} catch {
|
|
1147
|
+
parsed = null;
|
|
1148
|
+
}
|
|
1149
|
+
return { ok: res.ok, httpStatus: res.status, response: parsed };
|
|
1150
|
+
}
|
|
1151
|
+
async function completeWorker(args) {
|
|
1152
|
+
try {
|
|
1153
|
+
const worker = loadWorker(String(args.run), String(args.name));
|
|
1154
|
+
const status = computeWorkerStatus(worker);
|
|
1155
|
+
const agentOsId = (args.agentOsId ? String(args.agentOsId) : worker.agentOsId) || "";
|
|
1156
|
+
const taskId = (args.taskId ? String(args.taskId) : worker.taskId) || null;
|
|
1157
|
+
if (!agentOsId) {
|
|
1158
|
+
console.error("worker complete requires --agent-os-id (or an agentOsId persisted at worker start)");
|
|
1159
|
+
process.exit(1);
|
|
1160
|
+
}
|
|
1161
|
+
if (!isFinishedWorkerStatus(status)) {
|
|
1162
|
+
console.log(
|
|
1163
|
+
JSON.stringify(
|
|
1164
|
+
{
|
|
1165
|
+
worker: worker.name,
|
|
1166
|
+
runId: worker.runId,
|
|
1167
|
+
status: "skipped",
|
|
1168
|
+
reason: "worker-not-finished",
|
|
1169
|
+
workerStatus: status.status,
|
|
1170
|
+
alive: status.alive
|
|
1171
|
+
},
|
|
1172
|
+
null,
|
|
1173
|
+
2
|
|
1174
|
+
)
|
|
1175
|
+
);
|
|
1176
|
+
return;
|
|
1177
|
+
}
|
|
1178
|
+
const result = await tryCompleteWorker(args);
|
|
1179
|
+
console.log(
|
|
1180
|
+
JSON.stringify(
|
|
1181
|
+
{
|
|
1182
|
+
worker: worker.name,
|
|
1183
|
+
runId: worker.runId,
|
|
1184
|
+
agentOsId,
|
|
1185
|
+
taskId,
|
|
1186
|
+
httpStatus: result.httpStatus,
|
|
1187
|
+
response: result.response
|
|
1188
|
+
},
|
|
1189
|
+
null,
|
|
1190
|
+
2
|
|
1191
|
+
)
|
|
1192
|
+
);
|
|
1193
|
+
if (!result.ok) process.exit(1);
|
|
1194
|
+
} catch (error) {
|
|
1195
|
+
console.error(`worker complete failed: ${error.message}`);
|
|
1196
|
+
process.exit(1);
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
function workerStatus(args) {
|
|
1200
|
+
const worker = loadWorker(String(args.run), String(args.name));
|
|
1201
|
+
const status = computeWorkerStatus(worker);
|
|
1202
|
+
writeJson(path10.join(worker.workerDir, "last-status.json"), status);
|
|
1203
|
+
console.log(JSON.stringify(status, null, 2));
|
|
1204
|
+
}
|
|
1205
|
+
function runStatus(args) {
|
|
1206
|
+
const run = loadRun(String(args.run));
|
|
1207
|
+
const names = Object.keys(run.workers || {});
|
|
1208
|
+
const workers = names.map((name) => {
|
|
1209
|
+
const worker = readJson(
|
|
1210
|
+
path10.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
1211
|
+
null
|
|
1212
|
+
);
|
|
1213
|
+
if (!worker) {
|
|
1214
|
+
return { worker: name, status: "missing", attention: "needs_attention", attentionReason: "worker.json not found" };
|
|
1215
|
+
}
|
|
1216
|
+
const status = computeWorkerStatus(worker);
|
|
1217
|
+
return {
|
|
1218
|
+
worker: status.worker,
|
|
1219
|
+
status: status.status,
|
|
1220
|
+
attention: status.attention.state,
|
|
1221
|
+
attentionReason: status.attention.reason,
|
|
1222
|
+
pid: status.pid,
|
|
1223
|
+
alive: status.alive,
|
|
1224
|
+
currentTool: status.currentTool,
|
|
1225
|
+
lastActivityAt: status.lastActivityAt,
|
|
1226
|
+
lastHeartbeatPhase: status.lastHeartbeatPhase,
|
|
1227
|
+
lastHeartbeatSummary: status.lastHeartbeatSummary,
|
|
1228
|
+
heartbeatBlocker: status.heartbeatBlocker,
|
|
1229
|
+
changedFileCount: status.changedFiles.length,
|
|
1230
|
+
branch: status.branch
|
|
1231
|
+
};
|
|
1232
|
+
});
|
|
1233
|
+
const board = {
|
|
1234
|
+
runId: run.id,
|
|
1235
|
+
name: run.name,
|
|
1236
|
+
status: deriveRunStatus(run.status, workers),
|
|
1237
|
+
repo: run.repo,
|
|
1238
|
+
workerCount: workers.length,
|
|
1239
|
+
needsAttention: workers.filter((w) => w.attention && w.attention !== "ok" && w.attention !== "done").map((w) => w.worker),
|
|
1240
|
+
workers
|
|
1241
|
+
};
|
|
1242
|
+
writeJson(path10.join(runDirectory(run.id), "last-board.json"), board);
|
|
1243
|
+
console.log(JSON.stringify(board, null, 2));
|
|
1244
|
+
}
|
|
1245
|
+
function tailWorker(args) {
|
|
1246
|
+
const worker = loadWorker(String(args.run), String(args.name));
|
|
1247
|
+
const raw = tailFile(worker.stdoutPath, Number(args.lines || 40));
|
|
1248
|
+
if (args.raw === true || args.raw === "true") {
|
|
1249
|
+
process.stdout.write(raw);
|
|
1250
|
+
return;
|
|
1251
|
+
}
|
|
1252
|
+
for (const line of raw.split("\n").filter(Boolean)) {
|
|
1253
|
+
const event = safeJson(line);
|
|
1254
|
+
const summary = event ? summarizeEvent(event) : line;
|
|
1255
|
+
if (summary) console.log(summary);
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
function stopWorker(args) {
|
|
1259
|
+
const worker = loadWorker(String(args.run), String(args.name));
|
|
1260
|
+
if (!isPidAlive(worker.pid)) {
|
|
1261
|
+
console.log(JSON.stringify({ worker: worker.name, pid: worker.pid, status: "not_running" }, null, 2));
|
|
1262
|
+
return;
|
|
1263
|
+
}
|
|
1264
|
+
killWorkerProcess(worker.pid, "SIGTERM");
|
|
1265
|
+
sleepMs(1500);
|
|
1266
|
+
if (isPidAlive(worker.pid)) {
|
|
1267
|
+
killWorkerProcess(worker.pid, "SIGKILL");
|
|
1268
|
+
console.log(JSON.stringify({ worker: worker.name, pid: worker.pid, status: "sigkill_sent" }, null, 2));
|
|
1269
|
+
return;
|
|
1270
|
+
}
|
|
1271
|
+
console.log(JSON.stringify({ worker: worker.name, pid: worker.pid, status: "stopped" }, null, 2));
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
// src/cli.ts
|
|
1275
|
+
import { mkdirSync as mkdirSync5 } from "node:fs";
|
|
1276
|
+
import path12 from "node:path";
|
|
1277
|
+
import { fileURLToPath } from "node:url";
|
|
1278
|
+
|
|
1279
|
+
// src/pipeline-tick.ts
|
|
1280
|
+
import path11 from "node:path";
|
|
1281
|
+
|
|
1282
|
+
// src/workspace-runtime-config.ts
|
|
1283
|
+
async function fetchWorkspaceRuntimePreferences(agentOsId, args) {
|
|
1284
|
+
const base = resolveBaseUrl(args.baseUrl ? String(args.baseUrl) : void 0);
|
|
1285
|
+
const secret = resolveCallbackSecret(args.secret ? String(args.secret) : void 0);
|
|
1286
|
+
const url = `${base}/api/agent-os/by-id/${encodeURIComponent(agentOsId)}/runtime`;
|
|
1287
|
+
try {
|
|
1288
|
+
const res = await getJson(url, secret);
|
|
1289
|
+
if (!res.ok) return null;
|
|
1290
|
+
const body = res.response;
|
|
1291
|
+
const raw = body?.preferences?.maxConcurrentWorkers;
|
|
1292
|
+
if (raw === null || raw === void 0) {
|
|
1293
|
+
return { maxConcurrentWorkers: null };
|
|
1294
|
+
}
|
|
1295
|
+
const n = Number(raw);
|
|
1296
|
+
if (!Number.isFinite(n) || n <= 0) return { maxConcurrentWorkers: null };
|
|
1297
|
+
return { maxConcurrentWorkers: Math.floor(n) };
|
|
1298
|
+
} catch {
|
|
1299
|
+
return null;
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
// src/pipeline-tick.ts
|
|
1304
|
+
async function completeFinishedWorkers(runId, args) {
|
|
1305
|
+
const run = loadRun(runId);
|
|
1306
|
+
const outcomes = [];
|
|
1307
|
+
for (const name of Object.keys(run.workers || {})) {
|
|
1308
|
+
const worker = readJson(
|
|
1309
|
+
path11.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
1310
|
+
null
|
|
1311
|
+
);
|
|
1312
|
+
if (!worker?.dispatched || !worker.taskId) continue;
|
|
1313
|
+
const status = computeWorkerStatus(worker);
|
|
1314
|
+
if (!isFinishedWorkerStatus(status)) continue;
|
|
1315
|
+
if (!status.finalResult) continue;
|
|
1316
|
+
const result = await tryCompleteWorker({
|
|
1317
|
+
run: runId,
|
|
1318
|
+
name,
|
|
1319
|
+
agentOsId: String(args.agentOsId || worker.agentOsId || ""),
|
|
1320
|
+
...args
|
|
1321
|
+
});
|
|
1322
|
+
outcomes.push({ worker: name, ok: result.ok, taskId: worker.taskId ?? null });
|
|
1323
|
+
}
|
|
1324
|
+
return outcomes;
|
|
1325
|
+
}
|
|
1326
|
+
async function postOperatorTick(agentOsId, runId, resourceGate, args) {
|
|
1327
|
+
const base = resolveBaseUrl(args.baseUrl ? String(args.baseUrl) : void 0);
|
|
1328
|
+
const secret = resolveCallbackSecret(args.secret ? String(args.secret) : void 0);
|
|
1329
|
+
const url = `${base}/api/agent-os/by-id/${encodeURIComponent(agentOsId)}/operator/tick`;
|
|
1330
|
+
const res = await postJson(url, secret, {
|
|
1331
|
+
agentOsId,
|
|
1332
|
+
runId,
|
|
1333
|
+
ingestHarness: true,
|
|
1334
|
+
resourceGate
|
|
1335
|
+
});
|
|
1336
|
+
return { ok: res.ok, httpStatus: res.status, response: res.response };
|
|
1337
|
+
}
|
|
1338
|
+
async function runPipelineTick(args) {
|
|
1339
|
+
const runId = String(required(String(args.run || ""), "--run"));
|
|
1340
|
+
const agentOsId = String(required(String(args.agentOsId || ""), "--agent-os-id"));
|
|
1341
|
+
const execute = args.execute !== false && args.execute !== "false";
|
|
1342
|
+
runStatus({ run: runId });
|
|
1343
|
+
const completedWorkers = await completeFinishedWorkers(runId, args);
|
|
1344
|
+
const workspacePrefs = await fetchWorkspaceRuntimePreferences(agentOsId, args);
|
|
1345
|
+
const resourceGate = observeRunnerResourceGate({
|
|
1346
|
+
runId,
|
|
1347
|
+
configuredMaxWorkersOverride: workspacePrefs?.maxConcurrentWorkers
|
|
1348
|
+
});
|
|
1349
|
+
const operatorTick = await postOperatorTick(agentOsId, runId, resourceGate, args);
|
|
1350
|
+
let maxStarts = resourceGate.slotsAvailable;
|
|
1351
|
+
const tickBody = operatorTick;
|
|
1352
|
+
const advised = tickBody.response?.dispatch?.recommendedMaxStarts;
|
|
1353
|
+
if (typeof advised === "number") {
|
|
1354
|
+
maxStarts = Math.min(maxStarts, Math.max(0, advised));
|
|
1355
|
+
}
|
|
1356
|
+
const sweep = await sweepRun({ run: runId, agentOsId, pipeline: true, ...args });
|
|
1357
|
+
let dispatch = null;
|
|
1358
|
+
if (execute && maxStarts > 0) {
|
|
1359
|
+
dispatch = await dispatchRun({
|
|
1360
|
+
run: runId,
|
|
1361
|
+
agentOsId,
|
|
1362
|
+
execute: true,
|
|
1363
|
+
maxStarts: String(maxStarts),
|
|
1364
|
+
pipeline: true,
|
|
1365
|
+
...args
|
|
1366
|
+
});
|
|
1367
|
+
} else {
|
|
1368
|
+
dispatch = {
|
|
1369
|
+
ok: true,
|
|
1370
|
+
skipped: true,
|
|
1371
|
+
reason: execute ? resourceGate.reason ?? "no slots or queued work" : "execute disabled",
|
|
1372
|
+
maxStarts: 0
|
|
1373
|
+
};
|
|
1374
|
+
}
|
|
1375
|
+
const startedCount = dispatch?.startedCount ?? 0;
|
|
1376
|
+
const idle = maxStarts === 0 && completedWorkers.length === 0 && startedCount === 0;
|
|
1377
|
+
return {
|
|
1378
|
+
runId,
|
|
1379
|
+
agentOsId,
|
|
1380
|
+
execute,
|
|
1381
|
+
resourceGate,
|
|
1382
|
+
completedWorkers,
|
|
1383
|
+
operatorTick,
|
|
1384
|
+
sweep,
|
|
1385
|
+
dispatch,
|
|
1386
|
+
idle
|
|
1387
|
+
};
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
// src/daemon.ts
|
|
1391
|
+
var DEFAULT_INTERVAL_MS = 6e4;
|
|
1392
|
+
var IDLE_INTERVAL_MS = 5 * 6e4;
|
|
1393
|
+
var MAX_IDLE_STREAK = 10;
|
|
1394
|
+
async function runDaemon(args) {
|
|
1395
|
+
const runId = String(required(String(args.run || ""), "--run"));
|
|
1396
|
+
const agentOsId = String(required(String(args.agentOsId || loadUserConfig().agentOsId || ""), "--agent-os-id"));
|
|
1397
|
+
const execute = args.execute !== false && args.execute !== "false";
|
|
1398
|
+
const intervalMs = Number(args.intervalMs) > 0 ? Math.floor(Number(args.intervalMs)) : DEFAULT_INTERVAL_MS;
|
|
1399
|
+
let stopping = false;
|
|
1400
|
+
let idleStreak = 0;
|
|
1401
|
+
process.on("SIGINT", () => {
|
|
1402
|
+
stopping = true;
|
|
1403
|
+
});
|
|
1404
|
+
process.on("SIGTERM", () => {
|
|
1405
|
+
stopping = true;
|
|
1406
|
+
});
|
|
1407
|
+
console.error(JSON.stringify({ event: "daemon_start", runId, agentOsId, execute, intervalMs }));
|
|
1408
|
+
while (!stopping) {
|
|
1409
|
+
try {
|
|
1410
|
+
const tick = await runPipelineTick({ run: runId, agentOsId, execute, ...args });
|
|
1411
|
+
console.error(JSON.stringify({ event: "daemon_tick", ...tick }));
|
|
1412
|
+
if (tick.idle) {
|
|
1413
|
+
idleStreak++;
|
|
1414
|
+
} else {
|
|
1415
|
+
idleStreak = 0;
|
|
1416
|
+
}
|
|
1417
|
+
const backoff = idleStreak >= MAX_IDLE_STREAK ? IDLE_INTERVAL_MS : intervalMs;
|
|
1418
|
+
sleepMs(backoff);
|
|
1419
|
+
} catch (error) {
|
|
1420
|
+
console.error(JSON.stringify({ event: "daemon_tick_error", error: error.message }));
|
|
1421
|
+
sleepMs(intervalMs);
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
console.error(JSON.stringify({ event: "daemon_stop", runId, agentOsId }));
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
// src/cli.ts
|
|
1428
|
+
function isHelpFlag(arg) {
|
|
1429
|
+
return arg === "help" || arg === "--help" || arg === "-h";
|
|
1430
|
+
}
|
|
1431
|
+
function unknownCommand(scope, action) {
|
|
1432
|
+
const cmd = [scope, action].filter(Boolean).join(" ");
|
|
1433
|
+
console.error(`unknown command: ${cmd || "(none)"}`);
|
|
1434
|
+
usage(1);
|
|
1435
|
+
}
|
|
1436
|
+
function usage(code = 0) {
|
|
1437
|
+
const out = code === 0 ? console.log : console.error;
|
|
1438
|
+
out(
|
|
1439
|
+
[
|
|
1440
|
+
"Usage:",
|
|
1441
|
+
" kynver login --api-key KEY",
|
|
1442
|
+
" kynver setup [--api-base-url URL] [--agent-os-id ID] [--agent-os-slug SLUG] [--repo PATH] [--max-workers N]",
|
|
1443
|
+
" kynver daemon --run RUN_ID --agent-os-id AOS_ID [--execute] [--interval-ms MS]",
|
|
1444
|
+
" kynver run create --repo /path/repo [--name name] [--base origin/main]",
|
|
1445
|
+
" kynver run list",
|
|
1446
|
+
" kynver run status --run RUN_ID",
|
|
1447
|
+
" kynver run dispatch --run RUN_ID --agent-os-id AOS_ID [--base-url URL] [--secret SECRET] [--execute] [--lane any|implementation|review|landing] [--max-starts 1] [--lease-ms MS] [--owned path[,path]] [--model claude-opus-4-7] [--disk-path /]",
|
|
1448
|
+
" kynver run sweep --run RUN_ID --agent-os-id AOS_ID [--base-url URL] [--secret SECRET] [--grace-ms MS]",
|
|
1449
|
+
' kynver worker start --run RUN_ID --name worker --task "..." [--owned path[,path]] [--model claude-opus-4-7] [--agent-os-id AOS_ID] [--task-id TASK_ID]',
|
|
1450
|
+
" kynver worker status --run RUN_ID --name worker",
|
|
1451
|
+
" kynver worker tail --run RUN_ID --name worker [--lines 40] [--raw]",
|
|
1452
|
+
" kynver worker stop --run RUN_ID --name worker",
|
|
1453
|
+
" kynver worker complete --run RUN_ID --name worker [--agent-os-id AOS_ID] [--task-id TASK_ID] [--base-url URL] [--secret SECRET]"
|
|
1454
|
+
].join("\n")
|
|
1455
|
+
);
|
|
1456
|
+
process.exit(code);
|
|
1457
|
+
}
|
|
1458
|
+
async function main(argv = process.argv.slice(2)) {
|
|
1459
|
+
if (argv.length === 0 || isHelpFlag(argv[0])) return usage(0);
|
|
1460
|
+
const scope = argv.shift();
|
|
1461
|
+
const action = argv.shift();
|
|
1462
|
+
const args = parseArgs(argv);
|
|
1463
|
+
const { runsDir, worktreesDir } = getPaths();
|
|
1464
|
+
mkdirSync5(runsDir, { recursive: true });
|
|
1465
|
+
mkdirSync5(worktreesDir, { recursive: true });
|
|
1466
|
+
if (scope === "login") return void await runLogin(args);
|
|
1467
|
+
if (scope === "setup") return void await runSetup(args);
|
|
1468
|
+
if (scope === "daemon") return void await runDaemon(args);
|
|
1469
|
+
if (scope === "run" && action === "create") return createRun(args);
|
|
1470
|
+
if (scope === "run" && action === "list") return listRuns();
|
|
1471
|
+
if (scope === "run" && action === "status") return runStatus(args);
|
|
1472
|
+
if (scope === "run" && action === "dispatch") return void await dispatchRun(args);
|
|
1473
|
+
if (scope === "run" && action === "sweep") return void await sweepRun(args);
|
|
1474
|
+
if (scope === "worker" && action === "start") return startWorker(args);
|
|
1475
|
+
if (scope === "worker" && action === "status") return workerStatus(args);
|
|
1476
|
+
if (scope === "worker" && action === "tail") return tailWorker(args);
|
|
1477
|
+
if (scope === "worker" && action === "stop") return stopWorker(args);
|
|
1478
|
+
if (scope === "worker" && action === "complete") return void await completeWorker(args);
|
|
1479
|
+
unknownCommand(scope, action);
|
|
1480
|
+
}
|
|
1481
|
+
var isCliEntry = process.argv[1] && path12.resolve(process.argv[1]) === path12.resolve(fileURLToPath(import.meta.url));
|
|
1482
|
+
if (isCliEntry) {
|
|
1483
|
+
void main().catch((error) => {
|
|
1484
|
+
console.error(error);
|
|
1485
|
+
process.exit(1);
|
|
1486
|
+
});
|
|
1487
|
+
}
|
|
1488
|
+
export {
|
|
1489
|
+
DEFAULT_DISPATCH_LEASE_MS,
|
|
1490
|
+
buildDispatchTaskText,
|
|
1491
|
+
buildPrompt,
|
|
1492
|
+
completeWorker,
|
|
1493
|
+
computeAttention,
|
|
1494
|
+
computeWorkerStatus,
|
|
1495
|
+
createRun,
|
|
1496
|
+
deriveRunStatus,
|
|
1497
|
+
dispatchRun,
|
|
1498
|
+
getHarnessPaths,
|
|
1499
|
+
isFinishedWorkerStatus,
|
|
1500
|
+
listRuns,
|
|
1501
|
+
loadUserConfig,
|
|
1502
|
+
main,
|
|
1503
|
+
observeRunnerDiskGate,
|
|
1504
|
+
parseArgs,
|
|
1505
|
+
parseClaudeStream,
|
|
1506
|
+
parseHeartbeat,
|
|
1507
|
+
postJson,
|
|
1508
|
+
redactHarness,
|
|
1509
|
+
resolveBaseUrl,
|
|
1510
|
+
resolveCallbackSecret,
|
|
1511
|
+
resolveHarnessRoot,
|
|
1512
|
+
runDaemon,
|
|
1513
|
+
runStatus,
|
|
1514
|
+
saveUserConfig,
|
|
1515
|
+
spawnWorkerProcess,
|
|
1516
|
+
startWorker,
|
|
1517
|
+
stopWorker,
|
|
1518
|
+
summarizeEvent,
|
|
1519
|
+
sweepRun,
|
|
1520
|
+
tailWorker,
|
|
1521
|
+
usage,
|
|
1522
|
+
validateOwnedPaths,
|
|
1523
|
+
validateRepo,
|
|
1524
|
+
validateRunId,
|
|
1525
|
+
validateTailLines,
|
|
1526
|
+
validateWorkerName,
|
|
1527
|
+
workerStatus
|
|
1528
|
+
};
|
|
1529
|
+
//# sourceMappingURL=index.js.map
|