@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/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 path14 = input.diskPath?.trim() || "/";
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(path14);
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: path14,
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 existsSync4, readFileSync as readFileSync3 } from "node:fs";
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 (!existsSync4(file)) return result;
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 existsSync5, readFileSync as readFileSync4 } from "node:fs";
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 (!existsSync5(file)) return result;
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 countActiveWorkers(runId) {
809
- const run = loadRun(runId);
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
- null
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 ?? os.freemem();
831
- const activeWorkers = input.activeWorkers ?? countActiveWorkers(input.runId);
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 = Math.max(0, capacityFromFree - activeWorkers);
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 <= activeWorkers) {
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 existsSync7, mkdirSync as mkdirSync3 } from "node:fs";
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 existsSync6, openSync as openSync2, readdirSync as readdirSync2 } from "node:fs";
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 (!existsSync6(versionsRoot)) return null;
947
- 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));
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 (!existsSync6(nodeExe) || !existsSync6(indexJs)) return null;
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 (existsSync6(localAgent)) return localAgent;
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 name = safeSlug(opts.name);
1046
- if (!opts.name) throw new Error("worker name is required");
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 (existsSync7(worktreePath)) throw new Error(`worktree path already exists: ${worktreePath}`);
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: String(args.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 existsSync8, mkdirSync as mkdirSync4 } from "node:fs";
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 (existsSync8(dir)) failExists(`run already exists: ${id}`);
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"), null)).filter(Boolean).map((run) => ({
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
- null
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 secret = await resolveCallbackSecretWithMint(args.secret ? String(args.secret) : void 0, agentOsId, { baseUrl: base });
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
- 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;
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
- return { ok: res.ok, httpStatus: res.status, response: parsed };
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
- null
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 path13 from "node:path";
1701
+ import path14 from "node:path";
1615
1702
 
1616
- // src/plan-progress-daemon-sync.ts
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
- path12.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
1642
- null
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
- path13.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
1696
- null
1825
+ path14.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
1826
+ void 0
1697
1827
  );
1698
- if (!worker?.dispatched || !worker.taskId) continue;
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,