@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/cli.js
CHANGED
|
@@ -207,6 +207,18 @@ async function resolveCallbackSecretWithMint(argsSecret, agentOsId, opts) {
|
|
|
207
207
|
"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"
|
|
208
208
|
);
|
|
209
209
|
}
|
|
210
|
+
async function refreshRunnerToken(agentOsId, opts) {
|
|
211
|
+
const apiKey = loadApiKey();
|
|
212
|
+
const baseUrl = resolveConfiguredBaseUrl(opts?.baseUrl);
|
|
213
|
+
if (!apiKey || !agentOsId || !baseUrl) return null;
|
|
214
|
+
try {
|
|
215
|
+
const token = await fetchRunnerCredential(agentOsId, { baseUrl, apiKey });
|
|
216
|
+
saveRunnerToken(agentOsId, token);
|
|
217
|
+
return token;
|
|
218
|
+
} catch {
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
210
222
|
async function fetchRunnerCredential(agentOsId, opts) {
|
|
211
223
|
const apiKey = opts?.apiKey || loadApiKey();
|
|
212
224
|
if (!apiKey) throw new Error("API key required \u2014 run `kynver login` first");
|
|
@@ -380,12 +392,12 @@ var DEFAULT_CRITICAL_FREE_BYTES = 15 * 1024 * 1024 * 1024;
|
|
|
380
392
|
var DEFAULT_MAX_USED_PERCENT = 80;
|
|
381
393
|
var DEFAULT_HARD_MAX_USED_PERCENT = 90;
|
|
382
394
|
function observeRunnerDiskGate(input = {}) {
|
|
383
|
-
const
|
|
395
|
+
const path15 = input.diskPath?.trim() || "/";
|
|
384
396
|
const warnBelowBytes = input.diskFreeWarnBytes ?? DEFAULT_WARN_FREE_BYTES;
|
|
385
397
|
const criticalBelowBytes = input.diskFreeCriticalBytes ?? DEFAULT_CRITICAL_FREE_BYTES;
|
|
386
398
|
const maxUsedPercent = input.diskMaxUsedPercent ?? DEFAULT_MAX_USED_PERCENT;
|
|
387
399
|
const hardMaxUsedPercent = input.diskHardMaxUsedPercent ?? DEFAULT_HARD_MAX_USED_PERCENT;
|
|
388
|
-
const stats = statfsSync(
|
|
400
|
+
const stats = statfsSync(path15);
|
|
389
401
|
const freeBytes = Number(stats.bavail) * Number(stats.bsize);
|
|
390
402
|
const totalBytes = Number(stats.blocks) * Number(stats.bsize);
|
|
391
403
|
const usedPercent = totalBytes > 0 ? (totalBytes - freeBytes) / totalBytes * 100 : 100;
|
|
@@ -405,7 +417,7 @@ function observeRunnerDiskGate(input = {}) {
|
|
|
405
417
|
}
|
|
406
418
|
return {
|
|
407
419
|
ok,
|
|
408
|
-
path:
|
|
420
|
+
path: path15,
|
|
409
421
|
freeBytes,
|
|
410
422
|
totalBytes,
|
|
411
423
|
usedPercent,
|
|
@@ -418,10 +430,12 @@ function observeRunnerDiskGate(input = {}) {
|
|
|
418
430
|
}
|
|
419
431
|
|
|
420
432
|
// src/resource-gate.ts
|
|
433
|
+
import { readFileSync as readFileSync5 } from "node:fs";
|
|
421
434
|
import os from "node:os";
|
|
422
435
|
import path5 from "node:path";
|
|
423
436
|
|
|
424
437
|
// src/run-store.ts
|
|
438
|
+
import { existsSync as existsSync4, readdirSync as readdirSync2 } from "node:fs";
|
|
425
439
|
import path4 from "node:path";
|
|
426
440
|
|
|
427
441
|
// src/paths.ts
|
|
@@ -457,6 +471,20 @@ function loadRun(id) {
|
|
|
457
471
|
const { runsDir } = getPaths();
|
|
458
472
|
return readJson(path4.join(runDir(runsDir, safeSlug(id)), "run.json"));
|
|
459
473
|
}
|
|
474
|
+
function listRunRecords() {
|
|
475
|
+
const { runsDir } = getPaths();
|
|
476
|
+
if (!existsSync4(runsDir)) return [];
|
|
477
|
+
const runs = [];
|
|
478
|
+
for (const entry of readdirSync2(runsDir, { withFileTypes: true })) {
|
|
479
|
+
if (!entry.isDirectory()) continue;
|
|
480
|
+
const run = readJson(
|
|
481
|
+
path4.join(runsDir, entry.name, "run.json"),
|
|
482
|
+
void 0
|
|
483
|
+
);
|
|
484
|
+
if (run?.id) runs.push(run);
|
|
485
|
+
}
|
|
486
|
+
return runs;
|
|
487
|
+
}
|
|
460
488
|
function loadWorker(runId, name) {
|
|
461
489
|
const { runsDir } = getPaths();
|
|
462
490
|
return readJson(
|
|
@@ -477,7 +505,7 @@ function runDirectory(id) {
|
|
|
477
505
|
}
|
|
478
506
|
|
|
479
507
|
// src/heartbeat.ts
|
|
480
|
-
import { existsSync as
|
|
508
|
+
import { existsSync as existsSync5, readFileSync as readFileSync3 } from "node:fs";
|
|
481
509
|
function parseHeartbeat(file) {
|
|
482
510
|
const result = {
|
|
483
511
|
heartbeatCount: 0,
|
|
@@ -486,7 +514,7 @@ function parseHeartbeat(file) {
|
|
|
486
514
|
lastHeartbeatSummary: null,
|
|
487
515
|
heartbeatBlocker: null
|
|
488
516
|
};
|
|
489
|
-
if (!
|
|
517
|
+
if (!existsSync5(file)) return result;
|
|
490
518
|
const lines = readFileSync3(file, "utf8").split("\n").filter(Boolean);
|
|
491
519
|
for (const line of lines) {
|
|
492
520
|
const entry = safeJson(line);
|
|
@@ -502,7 +530,7 @@ function parseHeartbeat(file) {
|
|
|
502
530
|
}
|
|
503
531
|
|
|
504
532
|
// src/stream.ts
|
|
505
|
-
import { existsSync as
|
|
533
|
+
import { existsSync as existsSync6, readFileSync as readFileSync4 } from "node:fs";
|
|
506
534
|
function parseClaudeStream(file) {
|
|
507
535
|
const result = {
|
|
508
536
|
firstEventAt: null,
|
|
@@ -511,7 +539,7 @@ function parseClaudeStream(file) {
|
|
|
511
539
|
finalResult: null,
|
|
512
540
|
error: null
|
|
513
541
|
};
|
|
514
|
-
if (!
|
|
542
|
+
if (!existsSync6(file)) return result;
|
|
515
543
|
const lines = readFileSync4(file, "utf8").split("\n").filter(Boolean);
|
|
516
544
|
for (const line of lines) {
|
|
517
545
|
const event = safeJson(line);
|
|
@@ -804,13 +832,23 @@ function computeAutoMaxWorkers(totalMemBytes, opts = {}) {
|
|
|
804
832
|
const raw = Math.max(1, Math.floor(budgetBytes / perWorkerMemBytes));
|
|
805
833
|
return Math.min(raw, AUTO_MAX_WORKERS_CEILING);
|
|
806
834
|
}
|
|
807
|
-
function
|
|
808
|
-
|
|
835
|
+
function readAvailableMemBytes() {
|
|
836
|
+
if (process.platform === "linux") {
|
|
837
|
+
try {
|
|
838
|
+
const meminfo = readFileSync5("/proc/meminfo", "utf8");
|
|
839
|
+
const match = meminfo.match(/^MemAvailable:\s+(\d+)\s*kB/m);
|
|
840
|
+
if (match) return Number(match[1]) * 1024;
|
|
841
|
+
} catch {
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
return os.freemem();
|
|
845
|
+
}
|
|
846
|
+
function countActiveWorkersForRun(run) {
|
|
809
847
|
let active = 0;
|
|
810
848
|
for (const name of Object.keys(run.workers || {})) {
|
|
811
849
|
const worker = readJson(
|
|
812
850
|
path5.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
813
|
-
|
|
851
|
+
void 0
|
|
814
852
|
);
|
|
815
853
|
if (!worker) continue;
|
|
816
854
|
const status = computeWorkerStatus(worker);
|
|
@@ -820,14 +858,19 @@ function countActiveWorkers(runId) {
|
|
|
820
858
|
}
|
|
821
859
|
return active;
|
|
822
860
|
}
|
|
861
|
+
function countActiveWorkersGlobal() {
|
|
862
|
+
let active = 0;
|
|
863
|
+
for (const run of listRunRecords()) active += countActiveWorkersForRun(run);
|
|
864
|
+
return active;
|
|
865
|
+
}
|
|
823
866
|
function observeRunnerResourceGate(input) {
|
|
824
867
|
const { perWorkerMemBytes, memReserveBytes, memUtilization, configuredMaxWorkers } = resolveResourceConfig(
|
|
825
868
|
input.config,
|
|
826
869
|
input.configuredMaxWorkersOverride
|
|
827
870
|
);
|
|
828
871
|
const totalMemBytes = input.totalMemBytes ?? os.totalmem();
|
|
829
|
-
const freeMemBytes = input.freeMemBytes ??
|
|
830
|
-
const activeWorkers = input.activeWorkers ??
|
|
872
|
+
const freeMemBytes = input.freeMemBytes ?? readAvailableMemBytes();
|
|
873
|
+
const activeWorkers = input.activeWorkers ?? countActiveWorkersGlobal();
|
|
831
874
|
const budgetBytes = Math.max(0, Math.floor(totalMemBytes * memUtilization) - memReserveBytes);
|
|
832
875
|
const capacityFromTotal = Math.max(0, Math.floor(budgetBytes / perWorkerMemBytes));
|
|
833
876
|
const capacityFromFree = Math.max(0, Math.floor(Math.max(0, freeMemBytes - memReserveBytes) / perWorkerMemBytes));
|
|
@@ -835,13 +878,13 @@ function observeRunnerResourceGate(input) {
|
|
|
835
878
|
const targetCap = configuredMaxWorkers ?? autoCap;
|
|
836
879
|
const maxConcurrentWorkers = Math.max(0, Math.min(targetCap, capacityFromTotal));
|
|
837
880
|
const slotsByCapacity = Math.max(0, maxConcurrentWorkers - activeWorkers);
|
|
838
|
-
const slotsByFreeMem =
|
|
881
|
+
const slotsByFreeMem = capacityFromFree;
|
|
839
882
|
const slotsAvailable = Math.min(slotsByCapacity, slotsByFreeMem);
|
|
840
883
|
let reason = null;
|
|
841
884
|
if (slotsAvailable <= 0) {
|
|
842
885
|
if (activeWorkers >= maxConcurrentWorkers) {
|
|
843
886
|
reason = `at worker limit (${activeWorkers}/${maxConcurrentWorkers} running)`;
|
|
844
|
-
} else if (capacityFromFree <=
|
|
887
|
+
} else if (capacityFromFree <= 0) {
|
|
845
888
|
reason = "insufficient free memory \u2014 waiting for workers to finish";
|
|
846
889
|
} else {
|
|
847
890
|
reason = "no worker slots available";
|
|
@@ -864,7 +907,7 @@ function observeRunnerResourceGate(input) {
|
|
|
864
907
|
}
|
|
865
908
|
|
|
866
909
|
// src/supervisor.ts
|
|
867
|
-
import { existsSync as
|
|
910
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync3 } from "node:fs";
|
|
868
911
|
import path7 from "node:path";
|
|
869
912
|
|
|
870
913
|
// src/prompt.ts
|
|
@@ -937,19 +980,19 @@ var claudeProvider = {
|
|
|
937
980
|
};
|
|
938
981
|
|
|
939
982
|
// src/providers/cursor.ts
|
|
940
|
-
import { closeSync as closeSync2, existsSync as
|
|
983
|
+
import { closeSync as closeSync2, existsSync as existsSync7, openSync as openSync2, readdirSync as readdirSync3 } from "node:fs";
|
|
941
984
|
import { spawn as spawn2 } from "node:child_process";
|
|
942
985
|
import path6 from "node:path";
|
|
943
986
|
var DEFAULT_CURSOR_MODEL = "composer-2.5";
|
|
944
987
|
function latestVersionDir(versionsRoot) {
|
|
945
|
-
if (!
|
|
946
|
-
const versions =
|
|
988
|
+
if (!existsSync7(versionsRoot)) return null;
|
|
989
|
+
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));
|
|
947
990
|
return versions[0] ? path6.join(versionsRoot, versions[0]) : null;
|
|
948
991
|
}
|
|
949
992
|
function resolveBundledCursor(versionDir) {
|
|
950
993
|
const nodeExe = path6.join(versionDir, "node.exe");
|
|
951
994
|
const indexJs = path6.join(versionDir, "index.js");
|
|
952
|
-
if (!
|
|
995
|
+
if (!existsSync7(nodeExe) || !existsSync7(indexJs)) return null;
|
|
953
996
|
return { executable: nodeExe, prefixArgs: [indexJs], shell: false, detached: true };
|
|
954
997
|
}
|
|
955
998
|
function resolveWindowsCursorSpawn(agentBin) {
|
|
@@ -972,7 +1015,7 @@ function resolveAgentBin() {
|
|
|
972
1015
|
if (configured) return configured;
|
|
973
1016
|
if (process.platform === "win32") {
|
|
974
1017
|
const localAgent = path6.join(process.env.LOCALAPPDATA || "", "cursor-agent", "agent.cmd");
|
|
975
|
-
if (
|
|
1018
|
+
if (existsSync7(localAgent)) return localAgent;
|
|
976
1019
|
}
|
|
977
1020
|
return "agent";
|
|
978
1021
|
}
|
|
@@ -1041,8 +1084,11 @@ function resolveWorkerProvider(name) {
|
|
|
1041
1084
|
|
|
1042
1085
|
// src/supervisor.ts
|
|
1043
1086
|
function spawnWorkerProcess(run, opts) {
|
|
1044
|
-
const
|
|
1045
|
-
if (!
|
|
1087
|
+
const rawName = typeof opts.name === "string" ? opts.name.trim() : "";
|
|
1088
|
+
if (!rawName || rawName === "undefined" || rawName === "null") {
|
|
1089
|
+
throw new Error(`worker name is required and must be a real identifier (got: ${JSON.stringify(opts.name)})`);
|
|
1090
|
+
}
|
|
1091
|
+
const name = safeSlug(rawName);
|
|
1046
1092
|
if (run.workers?.[name]) throw new Error(`worker already exists in run ${run.id}: ${name}`);
|
|
1047
1093
|
if (!opts.task) throw new Error(`missing task text for worker ${name}`);
|
|
1048
1094
|
const { worktreesDir } = getPaths();
|
|
@@ -1050,7 +1096,7 @@ function spawnWorkerProcess(run, opts) {
|
|
|
1050
1096
|
mkdirSync3(workerDir, { recursive: true });
|
|
1051
1097
|
const worktreePath = path7.join(worktreesDir, run.id, name);
|
|
1052
1098
|
const branch = opts.branch || `agent/${run.id}/${name}`;
|
|
1053
|
-
if (
|
|
1099
|
+
if (existsSync8(worktreePath)) throw new Error(`worktree path already exists: ${worktreePath}`);
|
|
1054
1100
|
git(run.repo, ["fetch", "origin", "--prune"], { allowFailure: true });
|
|
1055
1101
|
git(run.repo, ["worktree", "add", "-b", branch, worktreePath, run.baseCommit], { throwError: true });
|
|
1056
1102
|
const stdoutPath = path7.join(workerDir, "stdout.jsonl");
|
|
@@ -1112,6 +1158,11 @@ function spawnWorkerProcess(run, opts) {
|
|
|
1112
1158
|
}
|
|
1113
1159
|
function startWorker(args) {
|
|
1114
1160
|
const run = loadRun(String(args.run));
|
|
1161
|
+
const name = typeof args.name === "string" ? args.name.trim() : "";
|
|
1162
|
+
if (!name) {
|
|
1163
|
+
console.error("worker start failed: --name is required");
|
|
1164
|
+
process.exit(1);
|
|
1165
|
+
}
|
|
1115
1166
|
const task = args.task ? String(args.task) : readMaybeFile(args.taskFile ? String(args.taskFile) : void 0);
|
|
1116
1167
|
if (!task) {
|
|
1117
1168
|
console.error("missing --task or --task-file");
|
|
@@ -1119,7 +1170,7 @@ function startWorker(args) {
|
|
|
1119
1170
|
}
|
|
1120
1171
|
try {
|
|
1121
1172
|
const worker = spawnWorkerProcess(run, {
|
|
1122
|
-
name
|
|
1173
|
+
name,
|
|
1123
1174
|
task,
|
|
1124
1175
|
ownedPaths: args.owned ? String(args.owned).split(",").map((s) => s.trim()).filter(Boolean) : [],
|
|
1125
1176
|
model: args.model ? String(args.model) : void 0,
|
|
@@ -1316,7 +1367,7 @@ async function sweepRun(args) {
|
|
|
1316
1367
|
for (const name of Object.keys(run.workers || {})) {
|
|
1317
1368
|
const worker = readJson(
|
|
1318
1369
|
path8.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
1319
|
-
|
|
1370
|
+
void 0
|
|
1320
1371
|
);
|
|
1321
1372
|
if (!worker || !worker.dispatched || !worker.taskId) continue;
|
|
1322
1373
|
const status = computeWorkerStatus(worker);
|
|
@@ -1358,7 +1409,7 @@ async function sweepRun(args) {
|
|
|
1358
1409
|
}
|
|
1359
1410
|
|
|
1360
1411
|
// src/worktree.ts
|
|
1361
|
-
import { existsSync as
|
|
1412
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync4 } from "node:fs";
|
|
1362
1413
|
import path10 from "node:path";
|
|
1363
1414
|
|
|
1364
1415
|
// src/validate.ts
|
|
@@ -1381,7 +1432,7 @@ function createRun(args) {
|
|
|
1381
1432
|
ensureGitRepo(repo);
|
|
1382
1433
|
const id = args.id ? validateRunId(String(args.id)) : timestampSlug(String(args.name || "run"));
|
|
1383
1434
|
const dir = runDirectory(id);
|
|
1384
|
-
if (
|
|
1435
|
+
if (existsSync9(dir)) failExists(`run already exists: ${id}`);
|
|
1385
1436
|
mkdirSync4(dir, { recursive: true });
|
|
1386
1437
|
const base = String(args.base || "origin/main");
|
|
1387
1438
|
const baseCommit = git(repo, ["rev-parse", base]).trim();
|
|
@@ -1400,7 +1451,7 @@ function createRun(args) {
|
|
|
1400
1451
|
}
|
|
1401
1452
|
function listRuns() {
|
|
1402
1453
|
const { runsDir } = getPaths();
|
|
1403
|
-
const rows = listRunIds(runsDir).map((id) => readJson(path10.join(runDirectory(id), "run.json"),
|
|
1454
|
+
const rows = listRunIds(runsDir).map((id) => readJson(path10.join(runDirectory(id), "run.json"), void 0)).filter(Boolean).map((run) => ({
|
|
1404
1455
|
id: run.id,
|
|
1405
1456
|
name: run.name,
|
|
1406
1457
|
status: run.status,
|
|
@@ -1416,6 +1467,34 @@ function failExists(message) {
|
|
|
1416
1467
|
|
|
1417
1468
|
// src/worker-ops.ts
|
|
1418
1469
|
import path11 from "node:path";
|
|
1470
|
+
async function postCompletion(url, secret, body) {
|
|
1471
|
+
const res = await fetch(url, {
|
|
1472
|
+
method: "POST",
|
|
1473
|
+
headers: buildHarnessCallbackHeaders(secret),
|
|
1474
|
+
body: JSON.stringify(body)
|
|
1475
|
+
});
|
|
1476
|
+
let parsed = null;
|
|
1477
|
+
try {
|
|
1478
|
+
parsed = await res.json();
|
|
1479
|
+
} catch {
|
|
1480
|
+
parsed = null;
|
|
1481
|
+
}
|
|
1482
|
+
return { ok: res.ok, status: res.status, parsed };
|
|
1483
|
+
}
|
|
1484
|
+
function completionErrorText(parsed) {
|
|
1485
|
+
if (parsed && typeof parsed === "object") {
|
|
1486
|
+
const err = parsed.error;
|
|
1487
|
+
if (typeof err === "string" && err.trim()) return err.trim();
|
|
1488
|
+
}
|
|
1489
|
+
return void 0;
|
|
1490
|
+
}
|
|
1491
|
+
function persistCompletionBlocker(worker, reason) {
|
|
1492
|
+
const current = worker.completionBlocker;
|
|
1493
|
+
if ((current ?? void 0) === (reason ?? void 0)) return;
|
|
1494
|
+
if (reason) worker.completionBlocker = reason;
|
|
1495
|
+
else delete worker.completionBlocker;
|
|
1496
|
+
saveWorker(worker.runId, worker);
|
|
1497
|
+
}
|
|
1419
1498
|
async function tryCompleteWorker(args) {
|
|
1420
1499
|
const worker = loadWorker(String(args.run), String(args.name));
|
|
1421
1500
|
const status = computeWorkerStatus(worker);
|
|
@@ -1428,7 +1507,8 @@ async function tryCompleteWorker(args) {
|
|
|
1428
1507
|
return { ok: true, skipped: true, reason: "worker-not-finished" };
|
|
1429
1508
|
}
|
|
1430
1509
|
const base = resolveBaseUrl(args.baseUrl ? String(args.baseUrl) : void 0);
|
|
1431
|
-
const
|
|
1510
|
+
const explicitSecret = args.secret ? String(args.secret) : void 0;
|
|
1511
|
+
let secret = await resolveCallbackSecretWithMint(explicitSecret, agentOsId, { baseUrl: base });
|
|
1432
1512
|
const url = `${base}/api/agent-os/by-id/${encodeURIComponent(agentOsId)}/harness/completion`;
|
|
1433
1513
|
const body = {
|
|
1434
1514
|
source: "openclaw-harness",
|
|
@@ -1440,18 +1520,23 @@ async function tryCompleteWorker(args) {
|
|
|
1440
1520
|
finishedAt: status.lastActivityAt || (/* @__PURE__ */ new Date()).toISOString(),
|
|
1441
1521
|
status
|
|
1442
1522
|
};
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1523
|
+
let result = await postCompletion(url, secret, body);
|
|
1524
|
+
if ((result.status === 401 || result.status === 403) && !explicitSecret) {
|
|
1525
|
+
const refreshed = await refreshRunnerToken(agentOsId, { baseUrl: base });
|
|
1526
|
+
if (refreshed && refreshed !== secret) {
|
|
1527
|
+
secret = refreshed;
|
|
1528
|
+
result = await postCompletion(url, secret, body);
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
if (result.ok) {
|
|
1532
|
+
persistCompletionBlocker(worker, void 0);
|
|
1533
|
+
return { ok: true, httpStatus: result.status, response: result.parsed };
|
|
1453
1534
|
}
|
|
1454
|
-
|
|
1535
|
+
const authRejected = result.status === 401 || result.status === 403;
|
|
1536
|
+
const detail = completionErrorText(result.parsed) ?? (authRejected ? "runner token unauthorized" : "non-2xx response");
|
|
1537
|
+
const reason = authRejected ? `completion replay rejected (${result.status}): ${detail}` : `completion replay failed (${result.status}): ${detail}`;
|
|
1538
|
+
persistCompletionBlocker(worker, reason);
|
|
1539
|
+
return { ok: false, httpStatus: result.status, response: result.parsed, completionBlocked: true };
|
|
1455
1540
|
}
|
|
1456
1541
|
async function completeWorker(args) {
|
|
1457
1542
|
try {
|
|
@@ -1513,17 +1598,19 @@ function runStatus(args) {
|
|
|
1513
1598
|
const workers = names.map((name) => {
|
|
1514
1599
|
const worker = readJson(
|
|
1515
1600
|
path11.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
1516
|
-
|
|
1601
|
+
void 0
|
|
1517
1602
|
);
|
|
1518
1603
|
if (!worker) {
|
|
1519
1604
|
return { worker: name, status: "missing", attention: "needs_attention", attentionReason: "worker.json not found" };
|
|
1520
1605
|
}
|
|
1521
1606
|
const status = computeWorkerStatus(worker, { base: run.base });
|
|
1607
|
+
const rawBlocker = worker.completionBlocker;
|
|
1608
|
+
const completionBlocker = typeof rawBlocker === "string" && rawBlocker ? rawBlocker : void 0;
|
|
1522
1609
|
return {
|
|
1523
1610
|
worker: status.worker,
|
|
1524
|
-
status: status.status,
|
|
1525
|
-
attention: status.attention.state,
|
|
1526
|
-
attentionReason: status.attention.reason,
|
|
1611
|
+
status: completionBlocker ? "blocked" : status.status,
|
|
1612
|
+
attention: completionBlocker ? "blocked" : status.attention.state,
|
|
1613
|
+
attentionReason: completionBlocker ?? status.attention.reason,
|
|
1527
1614
|
pid: status.pid,
|
|
1528
1615
|
alive: status.alive,
|
|
1529
1616
|
currentTool: status.currentTool,
|
|
@@ -1579,10 +1666,53 @@ function stopWorker(args) {
|
|
|
1579
1666
|
}
|
|
1580
1667
|
|
|
1581
1668
|
// src/pipeline-tick.ts
|
|
1582
|
-
import
|
|
1669
|
+
import path14 from "node:path";
|
|
1583
1670
|
|
|
1584
|
-
// src/
|
|
1671
|
+
// src/finalize.ts
|
|
1585
1672
|
import path12 from "node:path";
|
|
1673
|
+
var ACTIVE_RUN_STATUSES = /* @__PURE__ */ new Set(["running", "dispatching", "pending", "queued"]);
|
|
1674
|
+
function terminalStatusFor(run) {
|
|
1675
|
+
const names = Object.keys(run.workers || {});
|
|
1676
|
+
if (names.length === 0) return "failed";
|
|
1677
|
+
let anyAlive = false;
|
|
1678
|
+
let anyResult = false;
|
|
1679
|
+
let anyCompletionBlocked = false;
|
|
1680
|
+
for (const name of names) {
|
|
1681
|
+
const worker = readJson(
|
|
1682
|
+
path12.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
1683
|
+
void 0
|
|
1684
|
+
);
|
|
1685
|
+
if (!worker) continue;
|
|
1686
|
+
const status = computeWorkerStatus(worker);
|
|
1687
|
+
if (status.alive && !status.finalResult) {
|
|
1688
|
+
anyAlive = true;
|
|
1689
|
+
break;
|
|
1690
|
+
}
|
|
1691
|
+
if (typeof worker.completionBlocker === "string" && worker.completionBlocker) {
|
|
1692
|
+
anyCompletionBlocked = true;
|
|
1693
|
+
}
|
|
1694
|
+
if (status.finalResult) anyResult = true;
|
|
1695
|
+
}
|
|
1696
|
+
if (anyAlive) return null;
|
|
1697
|
+
if (anyCompletionBlocked) return null;
|
|
1698
|
+
return anyResult ? "completed" : "failed";
|
|
1699
|
+
}
|
|
1700
|
+
function finalizeStaleRuns() {
|
|
1701
|
+
const finalized = [];
|
|
1702
|
+
for (const run of listRunRecords()) {
|
|
1703
|
+
if (!ACTIVE_RUN_STATUSES.has(run.status)) continue;
|
|
1704
|
+
const next = terminalStatusFor(run);
|
|
1705
|
+
if (!next || next === run.status) continue;
|
|
1706
|
+
const from = run.status;
|
|
1707
|
+
run.status = next;
|
|
1708
|
+
saveRun(run);
|
|
1709
|
+
finalized.push({ runId: run.id, from, to: next });
|
|
1710
|
+
}
|
|
1711
|
+
return finalized;
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
// src/plan-progress-daemon-sync.ts
|
|
1715
|
+
import path13 from "node:path";
|
|
1586
1716
|
|
|
1587
1717
|
// src/plan-progress-sync.ts
|
|
1588
1718
|
async function syncPlanProgress(args) {
|
|
@@ -1606,8 +1736,8 @@ async function syncActiveWorkerPlanProgress(runId, args) {
|
|
|
1606
1736
|
const outcomes = [];
|
|
1607
1737
|
for (const name of Object.keys(run.workers || {})) {
|
|
1608
1738
|
const worker = readJson(
|
|
1609
|
-
|
|
1610
|
-
|
|
1739
|
+
path13.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
1740
|
+
void 0
|
|
1611
1741
|
);
|
|
1612
1742
|
if (!worker?.dispatched || !worker.taskId) continue;
|
|
1613
1743
|
const status = computeWorkerStatus(worker);
|
|
@@ -1660,13 +1790,13 @@ async function completeFinishedWorkers(runId, args) {
|
|
|
1660
1790
|
const outcomes = [];
|
|
1661
1791
|
for (const name of Object.keys(run.workers || {})) {
|
|
1662
1792
|
const worker = readJson(
|
|
1663
|
-
|
|
1664
|
-
|
|
1793
|
+
path14.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
1794
|
+
void 0
|
|
1665
1795
|
);
|
|
1666
|
-
if (!worker?.
|
|
1796
|
+
if (!worker?.taskId) continue;
|
|
1667
1797
|
const status = computeWorkerStatus(worker);
|
|
1668
1798
|
if (!isFinishedWorkerStatus(status)) continue;
|
|
1669
|
-
if (!status.finalResult) continue;
|
|
1799
|
+
if (!worker.dispatched && !status.finalResult) continue;
|
|
1670
1800
|
const result = await tryCompleteWorker({
|
|
1671
1801
|
run: runId,
|
|
1672
1802
|
name,
|
|
@@ -1695,6 +1825,7 @@ async function runPipelineTick(args) {
|
|
|
1695
1825
|
const execute = args.execute !== false && args.execute !== "false";
|
|
1696
1826
|
runStatus({ run: runId });
|
|
1697
1827
|
const completedWorkers = await completeFinishedWorkers(runId, args);
|
|
1828
|
+
const finalizedStaleRuns = finalizeStaleRuns();
|
|
1698
1829
|
const planProgressSync = await syncActiveWorkerPlanProgress(runId, args);
|
|
1699
1830
|
const workspacePrefs = await fetchWorkspaceRuntimePreferences(agentOsId, args);
|
|
1700
1831
|
const resourceGate = observeRunnerResourceGate({
|
|
@@ -1735,6 +1866,7 @@ async function runPipelineTick(args) {
|
|
|
1735
1866
|
execute,
|
|
1736
1867
|
resourceGate,
|
|
1737
1868
|
completedWorkers,
|
|
1869
|
+
finalizedStaleRuns,
|
|
1738
1870
|
planProgressSync,
|
|
1739
1871
|
operatorTick,
|
|
1740
1872
|
sweep,
|