@kynver-app/runtime 0.1.10 → 0.1.13

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
@@ -381,12 +381,12 @@ var DEFAULT_CRITICAL_FREE_BYTES = 15 * 1024 * 1024 * 1024;
381
381
  var DEFAULT_MAX_USED_PERCENT = 80;
382
382
  var DEFAULT_HARD_MAX_USED_PERCENT = 90;
383
383
  function observeRunnerDiskGate(input = {}) {
384
- const path14 = input.diskPath?.trim() || "/";
384
+ const path15 = input.diskPath?.trim() || "/";
385
385
  const warnBelowBytes = input.diskFreeWarnBytes ?? DEFAULT_WARN_FREE_BYTES;
386
386
  const criticalBelowBytes = input.diskFreeCriticalBytes ?? DEFAULT_CRITICAL_FREE_BYTES;
387
387
  const maxUsedPercent = input.diskMaxUsedPercent ?? DEFAULT_MAX_USED_PERCENT;
388
388
  const hardMaxUsedPercent = input.diskHardMaxUsedPercent ?? DEFAULT_HARD_MAX_USED_PERCENT;
389
- const stats = statfsSync(path14);
389
+ const stats = statfsSync(path15);
390
390
  const freeBytes = Number(stats.bavail) * Number(stats.bsize);
391
391
  const totalBytes = Number(stats.blocks) * Number(stats.bsize);
392
392
  const usedPercent = totalBytes > 0 ? (totalBytes - freeBytes) / totalBytes * 100 : 100;
@@ -406,7 +406,7 @@ function observeRunnerDiskGate(input = {}) {
406
406
  }
407
407
  return {
408
408
  ok,
409
- path: path14,
409
+ path: path15,
410
410
  freeBytes,
411
411
  totalBytes,
412
412
  usedPercent,
@@ -419,10 +419,12 @@ function observeRunnerDiskGate(input = {}) {
419
419
  }
420
420
 
421
421
  // src/resource-gate.ts
422
+ import { readFileSync as readFileSync5 } from "node:fs";
422
423
  import os from "node:os";
423
424
  import path5 from "node:path";
424
425
 
425
426
  // src/run-store.ts
427
+ import { existsSync as existsSync4, readdirSync as readdirSync2 } from "node:fs";
426
428
  import path4 from "node:path";
427
429
 
428
430
  // src/paths.ts
@@ -458,6 +460,20 @@ function loadRun(id) {
458
460
  const { runsDir } = getPaths();
459
461
  return readJson(path4.join(runDir(runsDir, safeSlug(id)), "run.json"));
460
462
  }
463
+ function listRunRecords() {
464
+ const { runsDir } = getPaths();
465
+ if (!existsSync4(runsDir)) return [];
466
+ const runs = [];
467
+ for (const entry of readdirSync2(runsDir, { withFileTypes: true })) {
468
+ if (!entry.isDirectory()) continue;
469
+ const run = readJson(
470
+ path4.join(runsDir, entry.name, "run.json"),
471
+ void 0
472
+ );
473
+ if (run?.id) runs.push(run);
474
+ }
475
+ return runs;
476
+ }
461
477
  function loadWorker(runId, name) {
462
478
  const { runsDir } = getPaths();
463
479
  return readJson(
@@ -478,7 +494,7 @@ function runDirectory(id) {
478
494
  }
479
495
 
480
496
  // src/heartbeat.ts
481
- import { existsSync as existsSync4, readFileSync as readFileSync3 } from "node:fs";
497
+ import { existsSync as existsSync5, readFileSync as readFileSync3 } from "node:fs";
482
498
  function parseHeartbeat(file) {
483
499
  const result = {
484
500
  heartbeatCount: 0,
@@ -487,7 +503,7 @@ function parseHeartbeat(file) {
487
503
  lastHeartbeatSummary: null,
488
504
  heartbeatBlocker: null
489
505
  };
490
- if (!existsSync4(file)) return result;
506
+ if (!existsSync5(file)) return result;
491
507
  const lines = readFileSync3(file, "utf8").split("\n").filter(Boolean);
492
508
  for (const line of lines) {
493
509
  const entry = safeJson(line);
@@ -503,7 +519,7 @@ function parseHeartbeat(file) {
503
519
  }
504
520
 
505
521
  // src/stream.ts
506
- import { existsSync as existsSync5, readFileSync as readFileSync4 } from "node:fs";
522
+ import { existsSync as existsSync6, readFileSync as readFileSync4 } from "node:fs";
507
523
  function parseClaudeStream(file) {
508
524
  const result = {
509
525
  firstEventAt: null,
@@ -512,7 +528,7 @@ function parseClaudeStream(file) {
512
528
  finalResult: null,
513
529
  error: null
514
530
  };
515
- if (!existsSync5(file)) return result;
531
+ if (!existsSync6(file)) return result;
516
532
  const lines = readFileSync4(file, "utf8").split("\n").filter(Boolean);
517
533
  for (const line of lines) {
518
534
  const event = safeJson(line);
@@ -594,6 +610,92 @@ function ensureGitRepo(repo) {
594
610
  function gitStatusShort(worktreePath) {
595
611
  return git(worktreePath, ["status", "--short"], { allowFailure: true }).split("\n").map((line) => line.trim()).filter(Boolean);
596
612
  }
613
+ function gitCapture(cwd, args) {
614
+ try {
615
+ const res = spawnSync("git", args, { cwd, encoding: "utf8" });
616
+ return {
617
+ status: res.status,
618
+ stdout: res.stdout || "",
619
+ stderr: res.stderr || "",
620
+ error: res.error ? res.error.message : null
621
+ };
622
+ } catch (error) {
623
+ return {
624
+ status: null,
625
+ stdout: "",
626
+ stderr: "",
627
+ error: error.message
628
+ };
629
+ }
630
+ }
631
+ function gitIsAncestor(cwd, ancestor, descendant) {
632
+ const res = gitCapture(cwd, ["merge-base", "--is-ancestor", ancestor, descendant]);
633
+ if (res.status === 0) return { isAncestor: true, error: null };
634
+ if (res.status === 1) return { isAncestor: false, error: null };
635
+ return { isAncestor: null, error: res.error || res.stderr || res.stdout || `git exited ${res.status}` };
636
+ }
637
+ function computeGitAncestry(worktreePath, base = "origin/main") {
638
+ if (!worktreePath) {
639
+ return unknownAncestry(base, "missing worktree path");
640
+ }
641
+ const head = gitCapture(worktreePath, ["rev-parse", "HEAD"]);
642
+ if (head.status !== 0) return unknownAncestry(base, head.error || head.stderr || head.stdout || "failed to resolve HEAD");
643
+ const baseHead = gitCapture(worktreePath, ["rev-parse", base]);
644
+ if (baseHead.status !== 0) {
645
+ return unknownAncestry(base, baseHead.error || baseHead.stderr || baseHead.stdout || `failed to resolve ${base}`, head.stdout.trim());
646
+ }
647
+ const headSha = head.stdout.trim();
648
+ const baseSha = baseHead.stdout.trim();
649
+ if (headSha === baseSha) {
650
+ return {
651
+ checked: true,
652
+ base,
653
+ head: headSha,
654
+ baseHead: baseSha,
655
+ baseIsAncestorOfHead: true,
656
+ headIsAncestorOfBase: true,
657
+ relation: "synced"
658
+ };
659
+ }
660
+ const baseIsAncestorOfHead = gitIsAncestor(worktreePath, baseSha, headSha);
661
+ const headIsAncestorOfBase = gitIsAncestor(worktreePath, headSha, baseSha);
662
+ const error = baseIsAncestorOfHead.error || headIsAncestorOfBase.error || void 0;
663
+ if (baseIsAncestorOfHead.isAncestor == null || headIsAncestorOfBase.isAncestor == null) {
664
+ return {
665
+ checked: false,
666
+ base,
667
+ head: headSha,
668
+ baseHead: baseSha,
669
+ baseIsAncestorOfHead: baseIsAncestorOfHead.isAncestor,
670
+ headIsAncestorOfBase: headIsAncestorOfBase.isAncestor,
671
+ relation: "unknown",
672
+ ...error ? { error } : {}
673
+ };
674
+ }
675
+ const relation = baseIsAncestorOfHead.isAncestor ? "ahead" : headIsAncestorOfBase.isAncestor ? "merged" : "diverged";
676
+ return {
677
+ checked: true,
678
+ base,
679
+ head: headSha,
680
+ baseHead: baseSha,
681
+ baseIsAncestorOfHead: baseIsAncestorOfHead.isAncestor,
682
+ headIsAncestorOfBase: headIsAncestorOfBase.isAncestor,
683
+ relation,
684
+ ...error ? { error } : {}
685
+ };
686
+ }
687
+ function unknownAncestry(base, error, head = null) {
688
+ return {
689
+ checked: false,
690
+ base,
691
+ head,
692
+ baseHead: null,
693
+ baseIsAncestorOfHead: null,
694
+ headIsAncestorOfBase: null,
695
+ relation: "unknown",
696
+ error
697
+ };
698
+ }
597
699
  function scrubClaudeEnv(env) {
598
700
  const next = { ...env };
599
701
  delete next.ANTHROPIC_API_KEY;
@@ -620,7 +722,7 @@ function computeAttention(input) {
620
722
  }
621
723
  return { state: "ok", reason: "recent activity" };
622
724
  }
623
- function computeWorkerStatus(worker) {
725
+ function computeWorkerStatus(worker, options = {}) {
624
726
  const parsed = parseClaudeStream(worker.stdoutPath);
625
727
  const heartbeat = parseHeartbeat(worker.heartbeatPath);
626
728
  const alive = isPidAlive(worker.pid);
@@ -628,6 +730,7 @@ function computeWorkerStatus(worker) {
628
730
  const stderrBytes = fileSize(worker.stderrPath);
629
731
  const heartbeatBytes = fileSize(worker.heartbeatPath);
630
732
  const changedFiles = gitStatusShort(worker.worktreePath);
733
+ const gitAncestry = computeGitAncestry(worker.worktreePath, options.base);
631
734
  const lastActivityAt = latestIso([
632
735
  parsed.lastEventAt,
633
736
  heartbeat.lastHeartbeatAt,
@@ -669,7 +772,8 @@ function computeWorkerStatus(worker) {
669
772
  heartbeatBlocker: heartbeat.heartbeatBlocker,
670
773
  finalResult: parsed.finalResult,
671
774
  error: parsed.error || (!alive && !parsed.finalResult ? tailFile(worker.stderrPath, 10).trim() || void 0 : void 0),
672
- changedFiles
775
+ changedFiles,
776
+ gitAncestry
673
777
  };
674
778
  }
675
779
  function isFinishedWorkerStatus(status) {
@@ -717,13 +821,23 @@ function computeAutoMaxWorkers(totalMemBytes, opts = {}) {
717
821
  const raw = Math.max(1, Math.floor(budgetBytes / perWorkerMemBytes));
718
822
  return Math.min(raw, AUTO_MAX_WORKERS_CEILING);
719
823
  }
720
- function countActiveWorkers(runId) {
721
- const run = loadRun(runId);
824
+ function readAvailableMemBytes() {
825
+ if (process.platform === "linux") {
826
+ try {
827
+ const meminfo = readFileSync5("/proc/meminfo", "utf8");
828
+ const match = meminfo.match(/^MemAvailable:\s+(\d+)\s*kB/m);
829
+ if (match) return Number(match[1]) * 1024;
830
+ } catch {
831
+ }
832
+ }
833
+ return os.freemem();
834
+ }
835
+ function countActiveWorkersForRun(run) {
722
836
  let active = 0;
723
837
  for (const name of Object.keys(run.workers || {})) {
724
838
  const worker = readJson(
725
839
  path5.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
726
- null
840
+ void 0
727
841
  );
728
842
  if (!worker) continue;
729
843
  const status = computeWorkerStatus(worker);
@@ -733,14 +847,19 @@ function countActiveWorkers(runId) {
733
847
  }
734
848
  return active;
735
849
  }
850
+ function countActiveWorkersGlobal() {
851
+ let active = 0;
852
+ for (const run of listRunRecords()) active += countActiveWorkersForRun(run);
853
+ return active;
854
+ }
736
855
  function observeRunnerResourceGate(input) {
737
856
  const { perWorkerMemBytes, memReserveBytes, memUtilization, configuredMaxWorkers } = resolveResourceConfig(
738
857
  input.config,
739
858
  input.configuredMaxWorkersOverride
740
859
  );
741
860
  const totalMemBytes = input.totalMemBytes ?? os.totalmem();
742
- const freeMemBytes = input.freeMemBytes ?? os.freemem();
743
- const activeWorkers = input.activeWorkers ?? countActiveWorkers(input.runId);
861
+ const freeMemBytes = input.freeMemBytes ?? readAvailableMemBytes();
862
+ const activeWorkers = input.activeWorkers ?? countActiveWorkersGlobal();
744
863
  const budgetBytes = Math.max(0, Math.floor(totalMemBytes * memUtilization) - memReserveBytes);
745
864
  const capacityFromTotal = Math.max(0, Math.floor(budgetBytes / perWorkerMemBytes));
746
865
  const capacityFromFree = Math.max(0, Math.floor(Math.max(0, freeMemBytes - memReserveBytes) / perWorkerMemBytes));
@@ -748,13 +867,13 @@ function observeRunnerResourceGate(input) {
748
867
  const targetCap = configuredMaxWorkers ?? autoCap;
749
868
  const maxConcurrentWorkers = Math.max(0, Math.min(targetCap, capacityFromTotal));
750
869
  const slotsByCapacity = Math.max(0, maxConcurrentWorkers - activeWorkers);
751
- const slotsByFreeMem = Math.max(0, capacityFromFree - activeWorkers);
870
+ const slotsByFreeMem = capacityFromFree;
752
871
  const slotsAvailable = Math.min(slotsByCapacity, slotsByFreeMem);
753
872
  let reason = null;
754
873
  if (slotsAvailable <= 0) {
755
874
  if (activeWorkers >= maxConcurrentWorkers) {
756
875
  reason = `at worker limit (${activeWorkers}/${maxConcurrentWorkers} running)`;
757
- } else if (capacityFromFree <= activeWorkers) {
876
+ } else if (capacityFromFree <= 0) {
758
877
  reason = "insufficient free memory \u2014 waiting for workers to finish";
759
878
  } else {
760
879
  reason = "no worker slots available";
@@ -777,7 +896,7 @@ function observeRunnerResourceGate(input) {
777
896
  }
778
897
 
779
898
  // src/supervisor.ts
780
- import { existsSync as existsSync7, mkdirSync as mkdirSync3 } from "node:fs";
899
+ import { existsSync as existsSync8, mkdirSync as mkdirSync3 } from "node:fs";
781
900
  import path7 from "node:path";
782
901
 
783
902
  // src/prompt.ts
@@ -850,19 +969,19 @@ var claudeProvider = {
850
969
  };
851
970
 
852
971
  // src/providers/cursor.ts
853
- import { closeSync as closeSync2, existsSync as existsSync6, openSync as openSync2, readdirSync as readdirSync2 } from "node:fs";
972
+ import { closeSync as closeSync2, existsSync as existsSync7, openSync as openSync2, readdirSync as readdirSync3 } from "node:fs";
854
973
  import { spawn as spawn2 } from "node:child_process";
855
974
  import path6 from "node:path";
856
975
  var DEFAULT_CURSOR_MODEL = "composer-2.5";
857
976
  function latestVersionDir(versionsRoot) {
858
- if (!existsSync6(versionsRoot)) return null;
859
- 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));
977
+ if (!existsSync7(versionsRoot)) return null;
978
+ 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));
860
979
  return versions[0] ? path6.join(versionsRoot, versions[0]) : null;
861
980
  }
862
981
  function resolveBundledCursor(versionDir) {
863
982
  const nodeExe = path6.join(versionDir, "node.exe");
864
983
  const indexJs = path6.join(versionDir, "index.js");
865
- if (!existsSync6(nodeExe) || !existsSync6(indexJs)) return null;
984
+ if (!existsSync7(nodeExe) || !existsSync7(indexJs)) return null;
866
985
  return { executable: nodeExe, prefixArgs: [indexJs], shell: false, detached: true };
867
986
  }
868
987
  function resolveWindowsCursorSpawn(agentBin) {
@@ -885,7 +1004,7 @@ function resolveAgentBin() {
885
1004
  if (configured) return configured;
886
1005
  if (process.platform === "win32") {
887
1006
  const localAgent = path6.join(process.env.LOCALAPPDATA || "", "cursor-agent", "agent.cmd");
888
- if (existsSync6(localAgent)) return localAgent;
1007
+ if (existsSync7(localAgent)) return localAgent;
889
1008
  }
890
1009
  return "agent";
891
1010
  }
@@ -954,8 +1073,11 @@ function resolveWorkerProvider(name) {
954
1073
 
955
1074
  // src/supervisor.ts
956
1075
  function spawnWorkerProcess(run, opts) {
957
- const name = safeSlug(opts.name);
958
- if (!opts.name) throw new Error("worker name is required");
1076
+ const rawName = typeof opts.name === "string" ? opts.name.trim() : "";
1077
+ if (!rawName || rawName === "undefined" || rawName === "null") {
1078
+ throw new Error(`worker name is required and must be a real identifier (got: ${JSON.stringify(opts.name)})`);
1079
+ }
1080
+ const name = safeSlug(rawName);
959
1081
  if (run.workers?.[name]) throw new Error(`worker already exists in run ${run.id}: ${name}`);
960
1082
  if (!opts.task) throw new Error(`missing task text for worker ${name}`);
961
1083
  const { worktreesDir } = getPaths();
@@ -963,7 +1085,7 @@ function spawnWorkerProcess(run, opts) {
963
1085
  mkdirSync3(workerDir, { recursive: true });
964
1086
  const worktreePath = path7.join(worktreesDir, run.id, name);
965
1087
  const branch = opts.branch || `agent/${run.id}/${name}`;
966
- if (existsSync7(worktreePath)) throw new Error(`worktree path already exists: ${worktreePath}`);
1088
+ if (existsSync8(worktreePath)) throw new Error(`worktree path already exists: ${worktreePath}`);
967
1089
  git(run.repo, ["fetch", "origin", "--prune"], { allowFailure: true });
968
1090
  git(run.repo, ["worktree", "add", "-b", branch, worktreePath, run.baseCommit], { throwError: true });
969
1091
  const stdoutPath = path7.join(workerDir, "stdout.jsonl");
@@ -1025,6 +1147,11 @@ function spawnWorkerProcess(run, opts) {
1025
1147
  }
1026
1148
  function startWorker(args) {
1027
1149
  const run = loadRun(String(args.run));
1150
+ const name = typeof args.name === "string" ? args.name.trim() : "";
1151
+ if (!name) {
1152
+ console.error("worker start failed: --name is required");
1153
+ process.exit(1);
1154
+ }
1028
1155
  const task = args.task ? String(args.task) : readMaybeFile(args.taskFile ? String(args.taskFile) : void 0);
1029
1156
  if (!task) {
1030
1157
  console.error("missing --task or --task-file");
@@ -1032,7 +1159,7 @@ function startWorker(args) {
1032
1159
  }
1033
1160
  try {
1034
1161
  const worker = spawnWorkerProcess(run, {
1035
- name: String(args.name),
1162
+ name,
1036
1163
  task,
1037
1164
  ownedPaths: args.owned ? String(args.owned).split(",").map((s) => s.trim()).filter(Boolean) : [],
1038
1165
  model: args.model ? String(args.model) : void 0,
@@ -1259,14 +1386,14 @@ function validateTailLines(lines) {
1259
1386
  }
1260
1387
 
1261
1388
  // src/worktree.ts
1262
- import { existsSync as existsSync8, mkdirSync as mkdirSync4 } from "node:fs";
1389
+ import { existsSync as existsSync9, mkdirSync as mkdirSync4 } from "node:fs";
1263
1390
  import path9 from "node:path";
1264
1391
  function createRun(args) {
1265
1392
  const repo = validateRepo(required(String(args.repo || ""), "--repo"));
1266
1393
  ensureGitRepo(repo);
1267
1394
  const id = args.id ? validateRunId(String(args.id)) : timestampSlug(String(args.name || "run"));
1268
1395
  const dir = runDirectory(id);
1269
- if (existsSync8(dir)) failExists(`run already exists: ${id}`);
1396
+ if (existsSync9(dir)) failExists(`run already exists: ${id}`);
1270
1397
  mkdirSync4(dir, { recursive: true });
1271
1398
  const base = String(args.base || "origin/main");
1272
1399
  const baseCommit = git(repo, ["rev-parse", base]).trim();
@@ -1285,7 +1412,7 @@ function createRun(args) {
1285
1412
  }
1286
1413
  function listRuns() {
1287
1414
  const { runsDir } = getPaths();
1288
- const rows = listRunIds(runsDir).map((id) => readJson(path9.join(runDirectory(id), "run.json"), null)).filter(Boolean).map((run) => ({
1415
+ const rows = listRunIds(runsDir).map((id) => readJson(path9.join(runDirectory(id), "run.json"), void 0)).filter(Boolean).map((run) => ({
1289
1416
  id: run.id,
1290
1417
  name: run.name,
1291
1418
  status: run.status,
@@ -1313,7 +1440,7 @@ async function sweepRun(args) {
1313
1440
  for (const name of Object.keys(run.workers || {})) {
1314
1441
  const worker = readJson(
1315
1442
  path10.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
1316
- null
1443
+ void 0
1317
1444
  );
1318
1445
  if (!worker || !worker.dispatched || !worker.taskId) continue;
1319
1446
  const status = computeWorkerStatus(worker);
@@ -1453,12 +1580,12 @@ function runStatus(args) {
1453
1580
  const workers = names.map((name) => {
1454
1581
  const worker = readJson(
1455
1582
  path11.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
1456
- null
1583
+ void 0
1457
1584
  );
1458
1585
  if (!worker) {
1459
1586
  return { worker: name, status: "missing", attention: "needs_attention", attentionReason: "worker.json not found" };
1460
1587
  }
1461
- const status = computeWorkerStatus(worker);
1588
+ const status = computeWorkerStatus(worker, { base: run.base });
1462
1589
  return {
1463
1590
  worker: status.worker,
1464
1591
  status: status.status,
@@ -1472,7 +1599,9 @@ function runStatus(args) {
1472
1599
  lastHeartbeatSummary: status.lastHeartbeatSummary,
1473
1600
  heartbeatBlocker: status.heartbeatBlocker,
1474
1601
  changedFileCount: status.changedFiles.length,
1475
- branch: status.branch
1602
+ branch: status.branch,
1603
+ ancestry: status.gitAncestry.relation,
1604
+ ancestryChecked: status.gitAncestry.checked
1476
1605
  };
1477
1606
  });
1478
1607
  const board = {
@@ -1521,10 +1650,48 @@ import { mkdirSync as mkdirSync5, realpathSync } from "node:fs";
1521
1650
  import { fileURLToPath } from "node:url";
1522
1651
 
1523
1652
  // src/pipeline-tick.ts
1524
- import path13 from "node:path";
1653
+ import path14 from "node:path";
1525
1654
 
1526
- // src/plan-progress-daemon-sync.ts
1655
+ // src/finalize.ts
1527
1656
  import path12 from "node:path";
1657
+ var ACTIVE_RUN_STATUSES = /* @__PURE__ */ new Set(["running", "dispatching", "pending", "queued"]);
1658
+ function terminalStatusFor(run) {
1659
+ const names = Object.keys(run.workers || {});
1660
+ if (names.length === 0) return "failed";
1661
+ let anyAlive = false;
1662
+ let anyResult = false;
1663
+ for (const name of names) {
1664
+ const worker = readJson(
1665
+ path12.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
1666
+ void 0
1667
+ );
1668
+ if (!worker) continue;
1669
+ const status = computeWorkerStatus(worker);
1670
+ if (status.alive && !status.finalResult) {
1671
+ anyAlive = true;
1672
+ break;
1673
+ }
1674
+ if (status.finalResult) anyResult = true;
1675
+ }
1676
+ if (anyAlive) return null;
1677
+ return anyResult ? "completed" : "failed";
1678
+ }
1679
+ function finalizeStaleRuns() {
1680
+ const finalized = [];
1681
+ for (const run of listRunRecords()) {
1682
+ if (!ACTIVE_RUN_STATUSES.has(run.status)) continue;
1683
+ const next = terminalStatusFor(run);
1684
+ if (!next || next === run.status) continue;
1685
+ const from = run.status;
1686
+ run.status = next;
1687
+ saveRun(run);
1688
+ finalized.push({ runId: run.id, from, to: next });
1689
+ }
1690
+ return finalized;
1691
+ }
1692
+
1693
+ // src/plan-progress-daemon-sync.ts
1694
+ import path13 from "node:path";
1528
1695
 
1529
1696
  // src/plan-progress-sync.ts
1530
1697
  async function syncPlanProgress(args) {
@@ -1548,8 +1715,8 @@ async function syncActiveWorkerPlanProgress(runId, args) {
1548
1715
  const outcomes = [];
1549
1716
  for (const name of Object.keys(run.workers || {})) {
1550
1717
  const worker = readJson(
1551
- path12.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
1552
- null
1718
+ path13.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
1719
+ void 0
1553
1720
  );
1554
1721
  if (!worker?.dispatched || !worker.taskId) continue;
1555
1722
  const status = computeWorkerStatus(worker);
@@ -1602,13 +1769,12 @@ async function completeFinishedWorkers(runId, args) {
1602
1769
  const outcomes = [];
1603
1770
  for (const name of Object.keys(run.workers || {})) {
1604
1771
  const worker = readJson(
1605
- path13.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
1606
- null
1772
+ path14.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
1773
+ void 0
1607
1774
  );
1608
1775
  if (!worker?.dispatched || !worker.taskId) continue;
1609
1776
  const status = computeWorkerStatus(worker);
1610
1777
  if (!isFinishedWorkerStatus(status)) continue;
1611
- if (!status.finalResult) continue;
1612
1778
  const result = await tryCompleteWorker({
1613
1779
  run: runId,
1614
1780
  name,
@@ -1636,6 +1802,7 @@ async function runPipelineTick(args) {
1636
1802
  const agentOsId = String(required(String(args.agentOsId || ""), "--agent-os-id"));
1637
1803
  const execute = args.execute !== false && args.execute !== "false";
1638
1804
  runStatus({ run: runId });
1805
+ const finalizedStaleRuns = finalizeStaleRuns();
1639
1806
  const completedWorkers = await completeFinishedWorkers(runId, args);
1640
1807
  const planProgressSync = await syncActiveWorkerPlanProgress(runId, args);
1641
1808
  const workspacePrefs = await fetchWorkspaceRuntimePreferences(agentOsId, args);
@@ -1677,6 +1844,7 @@ async function runPipelineTick(args) {
1677
1844
  execute,
1678
1845
  resourceGate,
1679
1846
  completedWorkers,
1847
+ finalizedStaleRuns,
1680
1848
  planProgressSync,
1681
1849
  operatorTick,
1682
1850
  sweep,