@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 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 path14 = input.diskPath?.trim() || "/";
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(path14);
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: path14,
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 existsSync4, readFileSync as readFileSync3 } from "node:fs";
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 (!existsSync4(file)) return result;
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 existsSync5, readFileSync as readFileSync4 } from "node:fs";
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 (!existsSync5(file)) return result;
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 countActiveWorkers(runId) {
808
- const run = loadRun(runId);
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
- null
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 ?? os.freemem();
830
- const activeWorkers = input.activeWorkers ?? countActiveWorkers(input.runId);
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 = Math.max(0, capacityFromFree - activeWorkers);
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 <= activeWorkers) {
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 existsSync7, mkdirSync as mkdirSync3 } from "node:fs";
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 existsSync6, openSync as openSync2, readdirSync as readdirSync2 } from "node:fs";
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 (!existsSync6(versionsRoot)) return null;
946
- const versions = readdirSync2(versionsRoot, { withFileTypes: true }).filter((entry) => entry.isDirectory() && /^\d{4}\.\d/.test(entry.name)).map((entry) => entry.name).sort((a, b) => b.localeCompare(a));
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 (!existsSync6(nodeExe) || !existsSync6(indexJs)) return null;
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 (existsSync6(localAgent)) return localAgent;
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 name = safeSlug(opts.name);
1045
- if (!opts.name) throw new Error("worker name is required");
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 (existsSync7(worktreePath)) throw new Error(`worktree path already exists: ${worktreePath}`);
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: String(args.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
- null
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 existsSync8, mkdirSync as mkdirSync4 } from "node:fs";
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 (existsSync8(dir)) failExists(`run already exists: ${id}`);
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"), null)).filter(Boolean).map((run) => ({
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 secret = await resolveCallbackSecretWithMint(args.secret ? String(args.secret) : void 0, agentOsId, { baseUrl: base });
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
- const res = await fetch(url, {
1444
- method: "POST",
1445
- headers: buildHarnessCallbackHeaders(secret),
1446
- body: JSON.stringify(body)
1447
- });
1448
- let parsed = null;
1449
- try {
1450
- parsed = await res.json();
1451
- } catch {
1452
- parsed = null;
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
- return { ok: res.ok, httpStatus: res.status, response: parsed };
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
- null
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 path13 from "node:path";
1669
+ import path14 from "node:path";
1583
1670
 
1584
- // src/plan-progress-daemon-sync.ts
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
- path12.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
1610
- null
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
- path13.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
1664
- null
1793
+ path14.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
1794
+ void 0
1665
1795
  );
1666
- if (!worker?.dispatched || !worker.taskId) continue;
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,