@kynver-app/runtime 0.1.11 → 0.1.16
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/dist/cli.js +184 -52
- package/dist/cli.js.map +4 -4
- package/dist/index.js +184 -52
- package/dist/index.js.map +4 -4
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -208,6 +208,18 @@ async function resolveCallbackSecretWithMint(argsSecret, agentOsId, opts) {
|
|
|
208
208
|
"requires --secret, KYNVER_RUNNER_TOKEN, a scoped runner token (`kynver runner credential`), ~/.kynver/credentials runnerToken, KYNVER_API_KEY with an API base URL to mint one, or (legacy) KYNVER_RUNTIME_SECRET / OPENCLAW_CRON_SECRET"
|
|
209
209
|
);
|
|
210
210
|
}
|
|
211
|
+
async function refreshRunnerToken(agentOsId, opts) {
|
|
212
|
+
const apiKey = loadApiKey();
|
|
213
|
+
const baseUrl = resolveConfiguredBaseUrl(opts?.baseUrl);
|
|
214
|
+
if (!apiKey || !agentOsId || !baseUrl) return null;
|
|
215
|
+
try {
|
|
216
|
+
const token = await fetchRunnerCredential(agentOsId, { baseUrl, apiKey });
|
|
217
|
+
saveRunnerToken(agentOsId, token);
|
|
218
|
+
return token;
|
|
219
|
+
} catch {
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
211
223
|
async function fetchRunnerCredential(agentOsId, opts) {
|
|
212
224
|
const apiKey = opts?.apiKey || loadApiKey();
|
|
213
225
|
if (!apiKey) throw new Error("API key required \u2014 run `kynver login` first");
|
|
@@ -381,12 +393,12 @@ var DEFAULT_CRITICAL_FREE_BYTES = 15 * 1024 * 1024 * 1024;
|
|
|
381
393
|
var DEFAULT_MAX_USED_PERCENT = 80;
|
|
382
394
|
var DEFAULT_HARD_MAX_USED_PERCENT = 90;
|
|
383
395
|
function observeRunnerDiskGate(input = {}) {
|
|
384
|
-
const
|
|
396
|
+
const path15 = input.diskPath?.trim() || "/";
|
|
385
397
|
const warnBelowBytes = input.diskFreeWarnBytes ?? DEFAULT_WARN_FREE_BYTES;
|
|
386
398
|
const criticalBelowBytes = input.diskFreeCriticalBytes ?? DEFAULT_CRITICAL_FREE_BYTES;
|
|
387
399
|
const maxUsedPercent = input.diskMaxUsedPercent ?? DEFAULT_MAX_USED_PERCENT;
|
|
388
400
|
const hardMaxUsedPercent = input.diskHardMaxUsedPercent ?? DEFAULT_HARD_MAX_USED_PERCENT;
|
|
389
|
-
const stats = statfsSync(
|
|
401
|
+
const stats = statfsSync(path15);
|
|
390
402
|
const freeBytes = Number(stats.bavail) * Number(stats.bsize);
|
|
391
403
|
const totalBytes = Number(stats.blocks) * Number(stats.bsize);
|
|
392
404
|
const usedPercent = totalBytes > 0 ? (totalBytes - freeBytes) / totalBytes * 100 : 100;
|
|
@@ -406,7 +418,7 @@ function observeRunnerDiskGate(input = {}) {
|
|
|
406
418
|
}
|
|
407
419
|
return {
|
|
408
420
|
ok,
|
|
409
|
-
path:
|
|
421
|
+
path: path15,
|
|
410
422
|
freeBytes,
|
|
411
423
|
totalBytes,
|
|
412
424
|
usedPercent,
|
|
@@ -419,10 +431,12 @@ function observeRunnerDiskGate(input = {}) {
|
|
|
419
431
|
}
|
|
420
432
|
|
|
421
433
|
// src/resource-gate.ts
|
|
434
|
+
import { readFileSync as readFileSync5 } from "node:fs";
|
|
422
435
|
import os from "node:os";
|
|
423
436
|
import path5 from "node:path";
|
|
424
437
|
|
|
425
438
|
// src/run-store.ts
|
|
439
|
+
import { existsSync as existsSync4, readdirSync as readdirSync2 } from "node:fs";
|
|
426
440
|
import path4 from "node:path";
|
|
427
441
|
|
|
428
442
|
// src/paths.ts
|
|
@@ -458,6 +472,20 @@ function loadRun(id) {
|
|
|
458
472
|
const { runsDir } = getPaths();
|
|
459
473
|
return readJson(path4.join(runDir(runsDir, safeSlug(id)), "run.json"));
|
|
460
474
|
}
|
|
475
|
+
function listRunRecords() {
|
|
476
|
+
const { runsDir } = getPaths();
|
|
477
|
+
if (!existsSync4(runsDir)) return [];
|
|
478
|
+
const runs = [];
|
|
479
|
+
for (const entry of readdirSync2(runsDir, { withFileTypes: true })) {
|
|
480
|
+
if (!entry.isDirectory()) continue;
|
|
481
|
+
const run = readJson(
|
|
482
|
+
path4.join(runsDir, entry.name, "run.json"),
|
|
483
|
+
void 0
|
|
484
|
+
);
|
|
485
|
+
if (run?.id) runs.push(run);
|
|
486
|
+
}
|
|
487
|
+
return runs;
|
|
488
|
+
}
|
|
461
489
|
function loadWorker(runId, name) {
|
|
462
490
|
const { runsDir } = getPaths();
|
|
463
491
|
return readJson(
|
|
@@ -478,7 +506,7 @@ function runDirectory(id) {
|
|
|
478
506
|
}
|
|
479
507
|
|
|
480
508
|
// src/heartbeat.ts
|
|
481
|
-
import { existsSync as
|
|
509
|
+
import { existsSync as existsSync5, readFileSync as readFileSync3 } from "node:fs";
|
|
482
510
|
function parseHeartbeat(file) {
|
|
483
511
|
const result = {
|
|
484
512
|
heartbeatCount: 0,
|
|
@@ -487,7 +515,7 @@ function parseHeartbeat(file) {
|
|
|
487
515
|
lastHeartbeatSummary: null,
|
|
488
516
|
heartbeatBlocker: null
|
|
489
517
|
};
|
|
490
|
-
if (!
|
|
518
|
+
if (!existsSync5(file)) return result;
|
|
491
519
|
const lines = readFileSync3(file, "utf8").split("\n").filter(Boolean);
|
|
492
520
|
for (const line of lines) {
|
|
493
521
|
const entry = safeJson(line);
|
|
@@ -503,7 +531,7 @@ function parseHeartbeat(file) {
|
|
|
503
531
|
}
|
|
504
532
|
|
|
505
533
|
// src/stream.ts
|
|
506
|
-
import { existsSync as
|
|
534
|
+
import { existsSync as existsSync6, readFileSync as readFileSync4 } from "node:fs";
|
|
507
535
|
function parseClaudeStream(file) {
|
|
508
536
|
const result = {
|
|
509
537
|
firstEventAt: null,
|
|
@@ -512,7 +540,7 @@ function parseClaudeStream(file) {
|
|
|
512
540
|
finalResult: null,
|
|
513
541
|
error: null
|
|
514
542
|
};
|
|
515
|
-
if (!
|
|
543
|
+
if (!existsSync6(file)) return result;
|
|
516
544
|
const lines = readFileSync4(file, "utf8").split("\n").filter(Boolean);
|
|
517
545
|
for (const line of lines) {
|
|
518
546
|
const event = safeJson(line);
|
|
@@ -805,13 +833,23 @@ function computeAutoMaxWorkers(totalMemBytes, opts = {}) {
|
|
|
805
833
|
const raw = Math.max(1, Math.floor(budgetBytes / perWorkerMemBytes));
|
|
806
834
|
return Math.min(raw, AUTO_MAX_WORKERS_CEILING);
|
|
807
835
|
}
|
|
808
|
-
function
|
|
809
|
-
|
|
836
|
+
function readAvailableMemBytes() {
|
|
837
|
+
if (process.platform === "linux") {
|
|
838
|
+
try {
|
|
839
|
+
const meminfo = readFileSync5("/proc/meminfo", "utf8");
|
|
840
|
+
const match = meminfo.match(/^MemAvailable:\s+(\d+)\s*kB/m);
|
|
841
|
+
if (match) return Number(match[1]) * 1024;
|
|
842
|
+
} catch {
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
return os.freemem();
|
|
846
|
+
}
|
|
847
|
+
function countActiveWorkersForRun(run) {
|
|
810
848
|
let active = 0;
|
|
811
849
|
for (const name of Object.keys(run.workers || {})) {
|
|
812
850
|
const worker = readJson(
|
|
813
851
|
path5.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
814
|
-
|
|
852
|
+
void 0
|
|
815
853
|
);
|
|
816
854
|
if (!worker) continue;
|
|
817
855
|
const status = computeWorkerStatus(worker);
|
|
@@ -821,14 +859,19 @@ function countActiveWorkers(runId) {
|
|
|
821
859
|
}
|
|
822
860
|
return active;
|
|
823
861
|
}
|
|
862
|
+
function countActiveWorkersGlobal() {
|
|
863
|
+
let active = 0;
|
|
864
|
+
for (const run of listRunRecords()) active += countActiveWorkersForRun(run);
|
|
865
|
+
return active;
|
|
866
|
+
}
|
|
824
867
|
function observeRunnerResourceGate(input) {
|
|
825
868
|
const { perWorkerMemBytes, memReserveBytes, memUtilization, configuredMaxWorkers } = resolveResourceConfig(
|
|
826
869
|
input.config,
|
|
827
870
|
input.configuredMaxWorkersOverride
|
|
828
871
|
);
|
|
829
872
|
const totalMemBytes = input.totalMemBytes ?? os.totalmem();
|
|
830
|
-
const freeMemBytes = input.freeMemBytes ??
|
|
831
|
-
const activeWorkers = input.activeWorkers ??
|
|
873
|
+
const freeMemBytes = input.freeMemBytes ?? readAvailableMemBytes();
|
|
874
|
+
const activeWorkers = input.activeWorkers ?? countActiveWorkersGlobal();
|
|
832
875
|
const budgetBytes = Math.max(0, Math.floor(totalMemBytes * memUtilization) - memReserveBytes);
|
|
833
876
|
const capacityFromTotal = Math.max(0, Math.floor(budgetBytes / perWorkerMemBytes));
|
|
834
877
|
const capacityFromFree = Math.max(0, Math.floor(Math.max(0, freeMemBytes - memReserveBytes) / perWorkerMemBytes));
|
|
@@ -836,13 +879,13 @@ function observeRunnerResourceGate(input) {
|
|
|
836
879
|
const targetCap = configuredMaxWorkers ?? autoCap;
|
|
837
880
|
const maxConcurrentWorkers = Math.max(0, Math.min(targetCap, capacityFromTotal));
|
|
838
881
|
const slotsByCapacity = Math.max(0, maxConcurrentWorkers - activeWorkers);
|
|
839
|
-
const slotsByFreeMem =
|
|
882
|
+
const slotsByFreeMem = capacityFromFree;
|
|
840
883
|
const slotsAvailable = Math.min(slotsByCapacity, slotsByFreeMem);
|
|
841
884
|
let reason = null;
|
|
842
885
|
if (slotsAvailable <= 0) {
|
|
843
886
|
if (activeWorkers >= maxConcurrentWorkers) {
|
|
844
887
|
reason = `at worker limit (${activeWorkers}/${maxConcurrentWorkers} running)`;
|
|
845
|
-
} else if (capacityFromFree <=
|
|
888
|
+
} else if (capacityFromFree <= 0) {
|
|
846
889
|
reason = "insufficient free memory \u2014 waiting for workers to finish";
|
|
847
890
|
} else {
|
|
848
891
|
reason = "no worker slots available";
|
|
@@ -865,7 +908,7 @@ function observeRunnerResourceGate(input) {
|
|
|
865
908
|
}
|
|
866
909
|
|
|
867
910
|
// src/supervisor.ts
|
|
868
|
-
import { existsSync as
|
|
911
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync3 } from "node:fs";
|
|
869
912
|
import path7 from "node:path";
|
|
870
913
|
|
|
871
914
|
// src/prompt.ts
|
|
@@ -938,19 +981,19 @@ var claudeProvider = {
|
|
|
938
981
|
};
|
|
939
982
|
|
|
940
983
|
// src/providers/cursor.ts
|
|
941
|
-
import { closeSync as closeSync2, existsSync as
|
|
984
|
+
import { closeSync as closeSync2, existsSync as existsSync7, openSync as openSync2, readdirSync as readdirSync3 } from "node:fs";
|
|
942
985
|
import { spawn as spawn2 } from "node:child_process";
|
|
943
986
|
import path6 from "node:path";
|
|
944
987
|
var DEFAULT_CURSOR_MODEL = "composer-2.5";
|
|
945
988
|
function latestVersionDir(versionsRoot) {
|
|
946
|
-
if (!
|
|
947
|
-
const versions =
|
|
989
|
+
if (!existsSync7(versionsRoot)) return null;
|
|
990
|
+
const versions = readdirSync3(versionsRoot, { withFileTypes: true }).filter((entry) => entry.isDirectory() && /^\d{4}\.\d/.test(entry.name)).map((entry) => entry.name).sort((a, b) => b.localeCompare(a));
|
|
948
991
|
return versions[0] ? path6.join(versionsRoot, versions[0]) : null;
|
|
949
992
|
}
|
|
950
993
|
function resolveBundledCursor(versionDir) {
|
|
951
994
|
const nodeExe = path6.join(versionDir, "node.exe");
|
|
952
995
|
const indexJs = path6.join(versionDir, "index.js");
|
|
953
|
-
if (!
|
|
996
|
+
if (!existsSync7(nodeExe) || !existsSync7(indexJs)) return null;
|
|
954
997
|
return { executable: nodeExe, prefixArgs: [indexJs], shell: false, detached: true };
|
|
955
998
|
}
|
|
956
999
|
function resolveWindowsCursorSpawn(agentBin) {
|
|
@@ -973,7 +1016,7 @@ function resolveAgentBin() {
|
|
|
973
1016
|
if (configured) return configured;
|
|
974
1017
|
if (process.platform === "win32") {
|
|
975
1018
|
const localAgent = path6.join(process.env.LOCALAPPDATA || "", "cursor-agent", "agent.cmd");
|
|
976
|
-
if (
|
|
1019
|
+
if (existsSync7(localAgent)) return localAgent;
|
|
977
1020
|
}
|
|
978
1021
|
return "agent";
|
|
979
1022
|
}
|
|
@@ -1042,8 +1085,11 @@ function resolveWorkerProvider(name) {
|
|
|
1042
1085
|
|
|
1043
1086
|
// src/supervisor.ts
|
|
1044
1087
|
function spawnWorkerProcess(run, opts) {
|
|
1045
|
-
const
|
|
1046
|
-
if (!
|
|
1088
|
+
const rawName = typeof opts.name === "string" ? opts.name.trim() : "";
|
|
1089
|
+
if (!rawName || rawName === "undefined" || rawName === "null") {
|
|
1090
|
+
throw new Error(`worker name is required and must be a real identifier (got: ${JSON.stringify(opts.name)})`);
|
|
1091
|
+
}
|
|
1092
|
+
const name = safeSlug(rawName);
|
|
1047
1093
|
if (run.workers?.[name]) throw new Error(`worker already exists in run ${run.id}: ${name}`);
|
|
1048
1094
|
if (!opts.task) throw new Error(`missing task text for worker ${name}`);
|
|
1049
1095
|
const { worktreesDir } = getPaths();
|
|
@@ -1051,7 +1097,7 @@ function spawnWorkerProcess(run, opts) {
|
|
|
1051
1097
|
mkdirSync3(workerDir, { recursive: true });
|
|
1052
1098
|
const worktreePath = path7.join(worktreesDir, run.id, name);
|
|
1053
1099
|
const branch = opts.branch || `agent/${run.id}/${name}`;
|
|
1054
|
-
if (
|
|
1100
|
+
if (existsSync8(worktreePath)) throw new Error(`worktree path already exists: ${worktreePath}`);
|
|
1055
1101
|
git(run.repo, ["fetch", "origin", "--prune"], { allowFailure: true });
|
|
1056
1102
|
git(run.repo, ["worktree", "add", "-b", branch, worktreePath, run.baseCommit], { throwError: true });
|
|
1057
1103
|
const stdoutPath = path7.join(workerDir, "stdout.jsonl");
|
|
@@ -1113,6 +1159,11 @@ function spawnWorkerProcess(run, opts) {
|
|
|
1113
1159
|
}
|
|
1114
1160
|
function startWorker(args) {
|
|
1115
1161
|
const run = loadRun(String(args.run));
|
|
1162
|
+
const name = typeof args.name === "string" ? args.name.trim() : "";
|
|
1163
|
+
if (!name) {
|
|
1164
|
+
console.error("worker start failed: --name is required");
|
|
1165
|
+
process.exit(1);
|
|
1166
|
+
}
|
|
1116
1167
|
const task = args.task ? String(args.task) : readMaybeFile(args.taskFile ? String(args.taskFile) : void 0);
|
|
1117
1168
|
if (!task) {
|
|
1118
1169
|
console.error("missing --task or --task-file");
|
|
@@ -1120,7 +1171,7 @@ function startWorker(args) {
|
|
|
1120
1171
|
}
|
|
1121
1172
|
try {
|
|
1122
1173
|
const worker = spawnWorkerProcess(run, {
|
|
1123
|
-
name
|
|
1174
|
+
name,
|
|
1124
1175
|
task,
|
|
1125
1176
|
ownedPaths: args.owned ? String(args.owned).split(",").map((s) => s.trim()).filter(Boolean) : [],
|
|
1126
1177
|
model: args.model ? String(args.model) : void 0,
|
|
@@ -1347,14 +1398,14 @@ function validateTailLines(lines) {
|
|
|
1347
1398
|
}
|
|
1348
1399
|
|
|
1349
1400
|
// src/worktree.ts
|
|
1350
|
-
import { existsSync as
|
|
1401
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync4 } from "node:fs";
|
|
1351
1402
|
import path9 from "node:path";
|
|
1352
1403
|
function createRun(args) {
|
|
1353
1404
|
const repo = validateRepo(required(String(args.repo || ""), "--repo"));
|
|
1354
1405
|
ensureGitRepo(repo);
|
|
1355
1406
|
const id = args.id ? validateRunId(String(args.id)) : timestampSlug(String(args.name || "run"));
|
|
1356
1407
|
const dir = runDirectory(id);
|
|
1357
|
-
if (
|
|
1408
|
+
if (existsSync9(dir)) failExists(`run already exists: ${id}`);
|
|
1358
1409
|
mkdirSync4(dir, { recursive: true });
|
|
1359
1410
|
const base = String(args.base || "origin/main");
|
|
1360
1411
|
const baseCommit = git(repo, ["rev-parse", base]).trim();
|
|
@@ -1373,7 +1424,7 @@ function createRun(args) {
|
|
|
1373
1424
|
}
|
|
1374
1425
|
function listRuns() {
|
|
1375
1426
|
const { runsDir } = getPaths();
|
|
1376
|
-
const rows = listRunIds(runsDir).map((id) => readJson(path9.join(runDirectory(id), "run.json"),
|
|
1427
|
+
const rows = listRunIds(runsDir).map((id) => readJson(path9.join(runDirectory(id), "run.json"), void 0)).filter(Boolean).map((run) => ({
|
|
1377
1428
|
id: run.id,
|
|
1378
1429
|
name: run.name,
|
|
1379
1430
|
status: run.status,
|
|
@@ -1401,7 +1452,7 @@ async function sweepRun(args) {
|
|
|
1401
1452
|
for (const name of Object.keys(run.workers || {})) {
|
|
1402
1453
|
const worker = readJson(
|
|
1403
1454
|
path10.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
1404
|
-
|
|
1455
|
+
void 0
|
|
1405
1456
|
);
|
|
1406
1457
|
if (!worker || !worker.dispatched || !worker.taskId) continue;
|
|
1407
1458
|
const status = computeWorkerStatus(worker);
|
|
@@ -1444,6 +1495,34 @@ async function sweepRun(args) {
|
|
|
1444
1495
|
|
|
1445
1496
|
// src/worker-ops.ts
|
|
1446
1497
|
import path11 from "node:path";
|
|
1498
|
+
async function postCompletion(url, secret, body) {
|
|
1499
|
+
const res = await fetch(url, {
|
|
1500
|
+
method: "POST",
|
|
1501
|
+
headers: buildHarnessCallbackHeaders(secret),
|
|
1502
|
+
body: JSON.stringify(body)
|
|
1503
|
+
});
|
|
1504
|
+
let parsed = null;
|
|
1505
|
+
try {
|
|
1506
|
+
parsed = await res.json();
|
|
1507
|
+
} catch {
|
|
1508
|
+
parsed = null;
|
|
1509
|
+
}
|
|
1510
|
+
return { ok: res.ok, status: res.status, parsed };
|
|
1511
|
+
}
|
|
1512
|
+
function completionErrorText(parsed) {
|
|
1513
|
+
if (parsed && typeof parsed === "object") {
|
|
1514
|
+
const err = parsed.error;
|
|
1515
|
+
if (typeof err === "string" && err.trim()) return err.trim();
|
|
1516
|
+
}
|
|
1517
|
+
return void 0;
|
|
1518
|
+
}
|
|
1519
|
+
function persistCompletionBlocker(worker, reason) {
|
|
1520
|
+
const current = worker.completionBlocker;
|
|
1521
|
+
if ((current ?? void 0) === (reason ?? void 0)) return;
|
|
1522
|
+
if (reason) worker.completionBlocker = reason;
|
|
1523
|
+
else delete worker.completionBlocker;
|
|
1524
|
+
saveWorker(worker.runId, worker);
|
|
1525
|
+
}
|
|
1447
1526
|
async function tryCompleteWorker(args) {
|
|
1448
1527
|
const worker = loadWorker(String(args.run), String(args.name));
|
|
1449
1528
|
const status = computeWorkerStatus(worker);
|
|
@@ -1456,7 +1535,8 @@ async function tryCompleteWorker(args) {
|
|
|
1456
1535
|
return { ok: true, skipped: true, reason: "worker-not-finished" };
|
|
1457
1536
|
}
|
|
1458
1537
|
const base = resolveBaseUrl(args.baseUrl ? String(args.baseUrl) : void 0);
|
|
1459
|
-
const
|
|
1538
|
+
const explicitSecret = args.secret ? String(args.secret) : void 0;
|
|
1539
|
+
let secret = await resolveCallbackSecretWithMint(explicitSecret, agentOsId, { baseUrl: base });
|
|
1460
1540
|
const url = `${base}/api/agent-os/by-id/${encodeURIComponent(agentOsId)}/harness/completion`;
|
|
1461
1541
|
const body = {
|
|
1462
1542
|
source: "openclaw-harness",
|
|
@@ -1468,18 +1548,23 @@ async function tryCompleteWorker(args) {
|
|
|
1468
1548
|
finishedAt: status.lastActivityAt || (/* @__PURE__ */ new Date()).toISOString(),
|
|
1469
1549
|
status
|
|
1470
1550
|
};
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1551
|
+
let result = await postCompletion(url, secret, body);
|
|
1552
|
+
if ((result.status === 401 || result.status === 403) && !explicitSecret) {
|
|
1553
|
+
const refreshed = await refreshRunnerToken(agentOsId, { baseUrl: base });
|
|
1554
|
+
if (refreshed && refreshed !== secret) {
|
|
1555
|
+
secret = refreshed;
|
|
1556
|
+
result = await postCompletion(url, secret, body);
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
if (result.ok) {
|
|
1560
|
+
persistCompletionBlocker(worker, void 0);
|
|
1561
|
+
return { ok: true, httpStatus: result.status, response: result.parsed };
|
|
1481
1562
|
}
|
|
1482
|
-
|
|
1563
|
+
const authRejected = result.status === 401 || result.status === 403;
|
|
1564
|
+
const detail = completionErrorText(result.parsed) ?? (authRejected ? "runner token unauthorized" : "non-2xx response");
|
|
1565
|
+
const reason = authRejected ? `completion replay rejected (${result.status}): ${detail}` : `completion replay failed (${result.status}): ${detail}`;
|
|
1566
|
+
persistCompletionBlocker(worker, reason);
|
|
1567
|
+
return { ok: false, httpStatus: result.status, response: result.parsed, completionBlocked: true };
|
|
1483
1568
|
}
|
|
1484
1569
|
async function completeWorker(args) {
|
|
1485
1570
|
try {
|
|
@@ -1541,17 +1626,19 @@ function runStatus(args) {
|
|
|
1541
1626
|
const workers = names.map((name) => {
|
|
1542
1627
|
const worker = readJson(
|
|
1543
1628
|
path11.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
1544
|
-
|
|
1629
|
+
void 0
|
|
1545
1630
|
);
|
|
1546
1631
|
if (!worker) {
|
|
1547
1632
|
return { worker: name, status: "missing", attention: "needs_attention", attentionReason: "worker.json not found" };
|
|
1548
1633
|
}
|
|
1549
1634
|
const status = computeWorkerStatus(worker, { base: run.base });
|
|
1635
|
+
const rawBlocker = worker.completionBlocker;
|
|
1636
|
+
const completionBlocker = typeof rawBlocker === "string" && rawBlocker ? rawBlocker : void 0;
|
|
1550
1637
|
return {
|
|
1551
1638
|
worker: status.worker,
|
|
1552
|
-
status: status.status,
|
|
1553
|
-
attention: status.attention.state,
|
|
1554
|
-
attentionReason: status.attention.reason,
|
|
1639
|
+
status: completionBlocker ? "blocked" : status.status,
|
|
1640
|
+
attention: completionBlocker ? "blocked" : status.attention.state,
|
|
1641
|
+
attentionReason: completionBlocker ?? status.attention.reason,
|
|
1555
1642
|
pid: status.pid,
|
|
1556
1643
|
alive: status.alive,
|
|
1557
1644
|
currentTool: status.currentTool,
|
|
@@ -1611,10 +1698,53 @@ import { mkdirSync as mkdirSync5, realpathSync } from "node:fs";
|
|
|
1611
1698
|
import { fileURLToPath } from "node:url";
|
|
1612
1699
|
|
|
1613
1700
|
// src/pipeline-tick.ts
|
|
1614
|
-
import
|
|
1701
|
+
import path14 from "node:path";
|
|
1615
1702
|
|
|
1616
|
-
// src/
|
|
1703
|
+
// src/finalize.ts
|
|
1617
1704
|
import path12 from "node:path";
|
|
1705
|
+
var ACTIVE_RUN_STATUSES = /* @__PURE__ */ new Set(["running", "dispatching", "pending", "queued"]);
|
|
1706
|
+
function terminalStatusFor(run) {
|
|
1707
|
+
const names = Object.keys(run.workers || {});
|
|
1708
|
+
if (names.length === 0) return "failed";
|
|
1709
|
+
let anyAlive = false;
|
|
1710
|
+
let anyResult = false;
|
|
1711
|
+
let anyCompletionBlocked = false;
|
|
1712
|
+
for (const name of names) {
|
|
1713
|
+
const worker = readJson(
|
|
1714
|
+
path12.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
1715
|
+
void 0
|
|
1716
|
+
);
|
|
1717
|
+
if (!worker) continue;
|
|
1718
|
+
const status = computeWorkerStatus(worker);
|
|
1719
|
+
if (status.alive && !status.finalResult) {
|
|
1720
|
+
anyAlive = true;
|
|
1721
|
+
break;
|
|
1722
|
+
}
|
|
1723
|
+
if (typeof worker.completionBlocker === "string" && worker.completionBlocker) {
|
|
1724
|
+
anyCompletionBlocked = true;
|
|
1725
|
+
}
|
|
1726
|
+
if (status.finalResult) anyResult = true;
|
|
1727
|
+
}
|
|
1728
|
+
if (anyAlive) return null;
|
|
1729
|
+
if (anyCompletionBlocked) return null;
|
|
1730
|
+
return anyResult ? "completed" : "failed";
|
|
1731
|
+
}
|
|
1732
|
+
function finalizeStaleRuns() {
|
|
1733
|
+
const finalized = [];
|
|
1734
|
+
for (const run of listRunRecords()) {
|
|
1735
|
+
if (!ACTIVE_RUN_STATUSES.has(run.status)) continue;
|
|
1736
|
+
const next = terminalStatusFor(run);
|
|
1737
|
+
if (!next || next === run.status) continue;
|
|
1738
|
+
const from = run.status;
|
|
1739
|
+
run.status = next;
|
|
1740
|
+
saveRun(run);
|
|
1741
|
+
finalized.push({ runId: run.id, from, to: next });
|
|
1742
|
+
}
|
|
1743
|
+
return finalized;
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
// src/plan-progress-daemon-sync.ts
|
|
1747
|
+
import path13 from "node:path";
|
|
1618
1748
|
|
|
1619
1749
|
// src/plan-progress-sync.ts
|
|
1620
1750
|
async function syncPlanProgress(args) {
|
|
@@ -1638,8 +1768,8 @@ async function syncActiveWorkerPlanProgress(runId, args) {
|
|
|
1638
1768
|
const outcomes = [];
|
|
1639
1769
|
for (const name of Object.keys(run.workers || {})) {
|
|
1640
1770
|
const worker = readJson(
|
|
1641
|
-
|
|
1642
|
-
|
|
1771
|
+
path13.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
1772
|
+
void 0
|
|
1643
1773
|
);
|
|
1644
1774
|
if (!worker?.dispatched || !worker.taskId) continue;
|
|
1645
1775
|
const status = computeWorkerStatus(worker);
|
|
@@ -1692,13 +1822,13 @@ async function completeFinishedWorkers(runId, args) {
|
|
|
1692
1822
|
const outcomes = [];
|
|
1693
1823
|
for (const name of Object.keys(run.workers || {})) {
|
|
1694
1824
|
const worker = readJson(
|
|
1695
|
-
|
|
1696
|
-
|
|
1825
|
+
path14.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
1826
|
+
void 0
|
|
1697
1827
|
);
|
|
1698
|
-
if (!worker?.
|
|
1828
|
+
if (!worker?.taskId) continue;
|
|
1699
1829
|
const status = computeWorkerStatus(worker);
|
|
1700
1830
|
if (!isFinishedWorkerStatus(status)) continue;
|
|
1701
|
-
if (!status.finalResult) continue;
|
|
1831
|
+
if (!worker.dispatched && !status.finalResult) continue;
|
|
1702
1832
|
const result = await tryCompleteWorker({
|
|
1703
1833
|
run: runId,
|
|
1704
1834
|
name,
|
|
@@ -1727,6 +1857,7 @@ async function runPipelineTick(args) {
|
|
|
1727
1857
|
const execute = args.execute !== false && args.execute !== "false";
|
|
1728
1858
|
runStatus({ run: runId });
|
|
1729
1859
|
const completedWorkers = await completeFinishedWorkers(runId, args);
|
|
1860
|
+
const finalizedStaleRuns = finalizeStaleRuns();
|
|
1730
1861
|
const planProgressSync = await syncActiveWorkerPlanProgress(runId, args);
|
|
1731
1862
|
const workspacePrefs = await fetchWorkspaceRuntimePreferences(agentOsId, args);
|
|
1732
1863
|
const resourceGate = observeRunnerResourceGate({
|
|
@@ -1767,6 +1898,7 @@ async function runPipelineTick(args) {
|
|
|
1767
1898
|
execute,
|
|
1768
1899
|
resourceGate,
|
|
1769
1900
|
completedWorkers,
|
|
1901
|
+
finalizedStaleRuns,
|
|
1770
1902
|
planProgressSync,
|
|
1771
1903
|
operatorTick,
|
|
1772
1904
|
sweep,
|