@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/cli.js CHANGED
@@ -380,12 +380,12 @@ var DEFAULT_CRITICAL_FREE_BYTES = 15 * 1024 * 1024 * 1024;
380
380
  var DEFAULT_MAX_USED_PERCENT = 80;
381
381
  var DEFAULT_HARD_MAX_USED_PERCENT = 90;
382
382
  function observeRunnerDiskGate(input = {}) {
383
- const path14 = input.diskPath?.trim() || "/";
383
+ const path15 = input.diskPath?.trim() || "/";
384
384
  const warnBelowBytes = input.diskFreeWarnBytes ?? DEFAULT_WARN_FREE_BYTES;
385
385
  const criticalBelowBytes = input.diskFreeCriticalBytes ?? DEFAULT_CRITICAL_FREE_BYTES;
386
386
  const maxUsedPercent = input.diskMaxUsedPercent ?? DEFAULT_MAX_USED_PERCENT;
387
387
  const hardMaxUsedPercent = input.diskHardMaxUsedPercent ?? DEFAULT_HARD_MAX_USED_PERCENT;
388
- const stats = statfsSync(path14);
388
+ const stats = statfsSync(path15);
389
389
  const freeBytes = Number(stats.bavail) * Number(stats.bsize);
390
390
  const totalBytes = Number(stats.blocks) * Number(stats.bsize);
391
391
  const usedPercent = totalBytes > 0 ? (totalBytes - freeBytes) / totalBytes * 100 : 100;
@@ -405,7 +405,7 @@ function observeRunnerDiskGate(input = {}) {
405
405
  }
406
406
  return {
407
407
  ok,
408
- path: path14,
408
+ path: path15,
409
409
  freeBytes,
410
410
  totalBytes,
411
411
  usedPercent,
@@ -418,10 +418,12 @@ function observeRunnerDiskGate(input = {}) {
418
418
  }
419
419
 
420
420
  // src/resource-gate.ts
421
+ import { readFileSync as readFileSync5 } from "node:fs";
421
422
  import os from "node:os";
422
423
  import path5 from "node:path";
423
424
 
424
425
  // src/run-store.ts
426
+ import { existsSync as existsSync4, readdirSync as readdirSync2 } from "node:fs";
425
427
  import path4 from "node:path";
426
428
 
427
429
  // src/paths.ts
@@ -457,6 +459,20 @@ function loadRun(id) {
457
459
  const { runsDir } = getPaths();
458
460
  return readJson(path4.join(runDir(runsDir, safeSlug(id)), "run.json"));
459
461
  }
462
+ function listRunRecords() {
463
+ const { runsDir } = getPaths();
464
+ if (!existsSync4(runsDir)) return [];
465
+ const runs = [];
466
+ for (const entry of readdirSync2(runsDir, { withFileTypes: true })) {
467
+ if (!entry.isDirectory()) continue;
468
+ const run = readJson(
469
+ path4.join(runsDir, entry.name, "run.json"),
470
+ void 0
471
+ );
472
+ if (run?.id) runs.push(run);
473
+ }
474
+ return runs;
475
+ }
460
476
  function loadWorker(runId, name) {
461
477
  const { runsDir } = getPaths();
462
478
  return readJson(
@@ -477,7 +493,7 @@ function runDirectory(id) {
477
493
  }
478
494
 
479
495
  // src/heartbeat.ts
480
- import { existsSync as existsSync4, readFileSync as readFileSync3 } from "node:fs";
496
+ import { existsSync as existsSync5, readFileSync as readFileSync3 } from "node:fs";
481
497
  function parseHeartbeat(file) {
482
498
  const result = {
483
499
  heartbeatCount: 0,
@@ -486,7 +502,7 @@ function parseHeartbeat(file) {
486
502
  lastHeartbeatSummary: null,
487
503
  heartbeatBlocker: null
488
504
  };
489
- if (!existsSync4(file)) return result;
505
+ if (!existsSync5(file)) return result;
490
506
  const lines = readFileSync3(file, "utf8").split("\n").filter(Boolean);
491
507
  for (const line of lines) {
492
508
  const entry = safeJson(line);
@@ -502,7 +518,7 @@ function parseHeartbeat(file) {
502
518
  }
503
519
 
504
520
  // src/stream.ts
505
- import { existsSync as existsSync5, readFileSync as readFileSync4 } from "node:fs";
521
+ import { existsSync as existsSync6, readFileSync as readFileSync4 } from "node:fs";
506
522
  function parseClaudeStream(file) {
507
523
  const result = {
508
524
  firstEventAt: null,
@@ -511,7 +527,7 @@ function parseClaudeStream(file) {
511
527
  finalResult: null,
512
528
  error: null
513
529
  };
514
- if (!existsSync5(file)) return result;
530
+ if (!existsSync6(file)) return result;
515
531
  const lines = readFileSync4(file, "utf8").split("\n").filter(Boolean);
516
532
  for (const line of lines) {
517
533
  const event = safeJson(line);
@@ -593,6 +609,92 @@ function ensureGitRepo(repo) {
593
609
  function gitStatusShort(worktreePath) {
594
610
  return git(worktreePath, ["status", "--short"], { allowFailure: true }).split("\n").map((line) => line.trim()).filter(Boolean);
595
611
  }
612
+ function gitCapture(cwd, args) {
613
+ try {
614
+ const res = spawnSync("git", args, { cwd, encoding: "utf8" });
615
+ return {
616
+ status: res.status,
617
+ stdout: res.stdout || "",
618
+ stderr: res.stderr || "",
619
+ error: res.error ? res.error.message : null
620
+ };
621
+ } catch (error) {
622
+ return {
623
+ status: null,
624
+ stdout: "",
625
+ stderr: "",
626
+ error: error.message
627
+ };
628
+ }
629
+ }
630
+ function gitIsAncestor(cwd, ancestor, descendant) {
631
+ const res = gitCapture(cwd, ["merge-base", "--is-ancestor", ancestor, descendant]);
632
+ if (res.status === 0) return { isAncestor: true, error: null };
633
+ if (res.status === 1) return { isAncestor: false, error: null };
634
+ return { isAncestor: null, error: res.error || res.stderr || res.stdout || `git exited ${res.status}` };
635
+ }
636
+ function computeGitAncestry(worktreePath, base = "origin/main") {
637
+ if (!worktreePath) {
638
+ return unknownAncestry(base, "missing worktree path");
639
+ }
640
+ const head = gitCapture(worktreePath, ["rev-parse", "HEAD"]);
641
+ if (head.status !== 0) return unknownAncestry(base, head.error || head.stderr || head.stdout || "failed to resolve HEAD");
642
+ const baseHead = gitCapture(worktreePath, ["rev-parse", base]);
643
+ if (baseHead.status !== 0) {
644
+ return unknownAncestry(base, baseHead.error || baseHead.stderr || baseHead.stdout || `failed to resolve ${base}`, head.stdout.trim());
645
+ }
646
+ const headSha = head.stdout.trim();
647
+ const baseSha = baseHead.stdout.trim();
648
+ if (headSha === baseSha) {
649
+ return {
650
+ checked: true,
651
+ base,
652
+ head: headSha,
653
+ baseHead: baseSha,
654
+ baseIsAncestorOfHead: true,
655
+ headIsAncestorOfBase: true,
656
+ relation: "synced"
657
+ };
658
+ }
659
+ const baseIsAncestorOfHead = gitIsAncestor(worktreePath, baseSha, headSha);
660
+ const headIsAncestorOfBase = gitIsAncestor(worktreePath, headSha, baseSha);
661
+ const error = baseIsAncestorOfHead.error || headIsAncestorOfBase.error || void 0;
662
+ if (baseIsAncestorOfHead.isAncestor == null || headIsAncestorOfBase.isAncestor == null) {
663
+ return {
664
+ checked: false,
665
+ base,
666
+ head: headSha,
667
+ baseHead: baseSha,
668
+ baseIsAncestorOfHead: baseIsAncestorOfHead.isAncestor,
669
+ headIsAncestorOfBase: headIsAncestorOfBase.isAncestor,
670
+ relation: "unknown",
671
+ ...error ? { error } : {}
672
+ };
673
+ }
674
+ const relation = baseIsAncestorOfHead.isAncestor ? "ahead" : headIsAncestorOfBase.isAncestor ? "merged" : "diverged";
675
+ return {
676
+ checked: true,
677
+ base,
678
+ head: headSha,
679
+ baseHead: baseSha,
680
+ baseIsAncestorOfHead: baseIsAncestorOfHead.isAncestor,
681
+ headIsAncestorOfBase: headIsAncestorOfBase.isAncestor,
682
+ relation,
683
+ ...error ? { error } : {}
684
+ };
685
+ }
686
+ function unknownAncestry(base, error, head = null) {
687
+ return {
688
+ checked: false,
689
+ base,
690
+ head,
691
+ baseHead: null,
692
+ baseIsAncestorOfHead: null,
693
+ headIsAncestorOfBase: null,
694
+ relation: "unknown",
695
+ error
696
+ };
697
+ }
596
698
  function scrubClaudeEnv(env) {
597
699
  const next = { ...env };
598
700
  delete next.ANTHROPIC_API_KEY;
@@ -619,7 +721,7 @@ function computeAttention(input) {
619
721
  }
620
722
  return { state: "ok", reason: "recent activity" };
621
723
  }
622
- function computeWorkerStatus(worker) {
724
+ function computeWorkerStatus(worker, options = {}) {
623
725
  const parsed = parseClaudeStream(worker.stdoutPath);
624
726
  const heartbeat = parseHeartbeat(worker.heartbeatPath);
625
727
  const alive = isPidAlive(worker.pid);
@@ -627,6 +729,7 @@ function computeWorkerStatus(worker) {
627
729
  const stderrBytes = fileSize(worker.stderrPath);
628
730
  const heartbeatBytes = fileSize(worker.heartbeatPath);
629
731
  const changedFiles = gitStatusShort(worker.worktreePath);
732
+ const gitAncestry = computeGitAncestry(worker.worktreePath, options.base);
630
733
  const lastActivityAt = latestIso([
631
734
  parsed.lastEventAt,
632
735
  heartbeat.lastHeartbeatAt,
@@ -668,7 +771,8 @@ function computeWorkerStatus(worker) {
668
771
  heartbeatBlocker: heartbeat.heartbeatBlocker,
669
772
  finalResult: parsed.finalResult,
670
773
  error: parsed.error || (!alive && !parsed.finalResult ? tailFile(worker.stderrPath, 10).trim() || void 0 : void 0),
671
- changedFiles
774
+ changedFiles,
775
+ gitAncestry
672
776
  };
673
777
  }
674
778
  function isFinishedWorkerStatus(status) {
@@ -716,13 +820,23 @@ function computeAutoMaxWorkers(totalMemBytes, opts = {}) {
716
820
  const raw = Math.max(1, Math.floor(budgetBytes / perWorkerMemBytes));
717
821
  return Math.min(raw, AUTO_MAX_WORKERS_CEILING);
718
822
  }
719
- function countActiveWorkers(runId) {
720
- const run = loadRun(runId);
823
+ function readAvailableMemBytes() {
824
+ if (process.platform === "linux") {
825
+ try {
826
+ const meminfo = readFileSync5("/proc/meminfo", "utf8");
827
+ const match = meminfo.match(/^MemAvailable:\s+(\d+)\s*kB/m);
828
+ if (match) return Number(match[1]) * 1024;
829
+ } catch {
830
+ }
831
+ }
832
+ return os.freemem();
833
+ }
834
+ function countActiveWorkersForRun(run) {
721
835
  let active = 0;
722
836
  for (const name of Object.keys(run.workers || {})) {
723
837
  const worker = readJson(
724
838
  path5.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
725
- null
839
+ void 0
726
840
  );
727
841
  if (!worker) continue;
728
842
  const status = computeWorkerStatus(worker);
@@ -732,14 +846,19 @@ function countActiveWorkers(runId) {
732
846
  }
733
847
  return active;
734
848
  }
849
+ function countActiveWorkersGlobal() {
850
+ let active = 0;
851
+ for (const run of listRunRecords()) active += countActiveWorkersForRun(run);
852
+ return active;
853
+ }
735
854
  function observeRunnerResourceGate(input) {
736
855
  const { perWorkerMemBytes, memReserveBytes, memUtilization, configuredMaxWorkers } = resolveResourceConfig(
737
856
  input.config,
738
857
  input.configuredMaxWorkersOverride
739
858
  );
740
859
  const totalMemBytes = input.totalMemBytes ?? os.totalmem();
741
- const freeMemBytes = input.freeMemBytes ?? os.freemem();
742
- const activeWorkers = input.activeWorkers ?? countActiveWorkers(input.runId);
860
+ const freeMemBytes = input.freeMemBytes ?? readAvailableMemBytes();
861
+ const activeWorkers = input.activeWorkers ?? countActiveWorkersGlobal();
743
862
  const budgetBytes = Math.max(0, Math.floor(totalMemBytes * memUtilization) - memReserveBytes);
744
863
  const capacityFromTotal = Math.max(0, Math.floor(budgetBytes / perWorkerMemBytes));
745
864
  const capacityFromFree = Math.max(0, Math.floor(Math.max(0, freeMemBytes - memReserveBytes) / perWorkerMemBytes));
@@ -747,13 +866,13 @@ function observeRunnerResourceGate(input) {
747
866
  const targetCap = configuredMaxWorkers ?? autoCap;
748
867
  const maxConcurrentWorkers = Math.max(0, Math.min(targetCap, capacityFromTotal));
749
868
  const slotsByCapacity = Math.max(0, maxConcurrentWorkers - activeWorkers);
750
- const slotsByFreeMem = Math.max(0, capacityFromFree - activeWorkers);
869
+ const slotsByFreeMem = capacityFromFree;
751
870
  const slotsAvailable = Math.min(slotsByCapacity, slotsByFreeMem);
752
871
  let reason = null;
753
872
  if (slotsAvailable <= 0) {
754
873
  if (activeWorkers >= maxConcurrentWorkers) {
755
874
  reason = `at worker limit (${activeWorkers}/${maxConcurrentWorkers} running)`;
756
- } else if (capacityFromFree <= activeWorkers) {
875
+ } else if (capacityFromFree <= 0) {
757
876
  reason = "insufficient free memory \u2014 waiting for workers to finish";
758
877
  } else {
759
878
  reason = "no worker slots available";
@@ -776,7 +895,7 @@ function observeRunnerResourceGate(input) {
776
895
  }
777
896
 
778
897
  // src/supervisor.ts
779
- import { existsSync as existsSync7, mkdirSync as mkdirSync3 } from "node:fs";
898
+ import { existsSync as existsSync8, mkdirSync as mkdirSync3 } from "node:fs";
780
899
  import path7 from "node:path";
781
900
 
782
901
  // src/prompt.ts
@@ -849,19 +968,19 @@ var claudeProvider = {
849
968
  };
850
969
 
851
970
  // src/providers/cursor.ts
852
- import { closeSync as closeSync2, existsSync as existsSync6, openSync as openSync2, readdirSync as readdirSync2 } from "node:fs";
971
+ import { closeSync as closeSync2, existsSync as existsSync7, openSync as openSync2, readdirSync as readdirSync3 } from "node:fs";
853
972
  import { spawn as spawn2 } from "node:child_process";
854
973
  import path6 from "node:path";
855
974
  var DEFAULT_CURSOR_MODEL = "composer-2.5";
856
975
  function latestVersionDir(versionsRoot) {
857
- if (!existsSync6(versionsRoot)) return null;
858
- 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));
976
+ if (!existsSync7(versionsRoot)) return null;
977
+ 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));
859
978
  return versions[0] ? path6.join(versionsRoot, versions[0]) : null;
860
979
  }
861
980
  function resolveBundledCursor(versionDir) {
862
981
  const nodeExe = path6.join(versionDir, "node.exe");
863
982
  const indexJs = path6.join(versionDir, "index.js");
864
- if (!existsSync6(nodeExe) || !existsSync6(indexJs)) return null;
983
+ if (!existsSync7(nodeExe) || !existsSync7(indexJs)) return null;
865
984
  return { executable: nodeExe, prefixArgs: [indexJs], shell: false, detached: true };
866
985
  }
867
986
  function resolveWindowsCursorSpawn(agentBin) {
@@ -884,7 +1003,7 @@ function resolveAgentBin() {
884
1003
  if (configured) return configured;
885
1004
  if (process.platform === "win32") {
886
1005
  const localAgent = path6.join(process.env.LOCALAPPDATA || "", "cursor-agent", "agent.cmd");
887
- if (existsSync6(localAgent)) return localAgent;
1006
+ if (existsSync7(localAgent)) return localAgent;
888
1007
  }
889
1008
  return "agent";
890
1009
  }
@@ -953,8 +1072,11 @@ function resolveWorkerProvider(name) {
953
1072
 
954
1073
  // src/supervisor.ts
955
1074
  function spawnWorkerProcess(run, opts) {
956
- const name = safeSlug(opts.name);
957
- if (!opts.name) throw new Error("worker name is required");
1075
+ const rawName = typeof opts.name === "string" ? opts.name.trim() : "";
1076
+ if (!rawName || rawName === "undefined" || rawName === "null") {
1077
+ throw new Error(`worker name is required and must be a real identifier (got: ${JSON.stringify(opts.name)})`);
1078
+ }
1079
+ const name = safeSlug(rawName);
958
1080
  if (run.workers?.[name]) throw new Error(`worker already exists in run ${run.id}: ${name}`);
959
1081
  if (!opts.task) throw new Error(`missing task text for worker ${name}`);
960
1082
  const { worktreesDir } = getPaths();
@@ -962,7 +1084,7 @@ function spawnWorkerProcess(run, opts) {
962
1084
  mkdirSync3(workerDir, { recursive: true });
963
1085
  const worktreePath = path7.join(worktreesDir, run.id, name);
964
1086
  const branch = opts.branch || `agent/${run.id}/${name}`;
965
- if (existsSync7(worktreePath)) throw new Error(`worktree path already exists: ${worktreePath}`);
1087
+ if (existsSync8(worktreePath)) throw new Error(`worktree path already exists: ${worktreePath}`);
966
1088
  git(run.repo, ["fetch", "origin", "--prune"], { allowFailure: true });
967
1089
  git(run.repo, ["worktree", "add", "-b", branch, worktreePath, run.baseCommit], { throwError: true });
968
1090
  const stdoutPath = path7.join(workerDir, "stdout.jsonl");
@@ -1024,6 +1146,11 @@ function spawnWorkerProcess(run, opts) {
1024
1146
  }
1025
1147
  function startWorker(args) {
1026
1148
  const run = loadRun(String(args.run));
1149
+ const name = typeof args.name === "string" ? args.name.trim() : "";
1150
+ if (!name) {
1151
+ console.error("worker start failed: --name is required");
1152
+ process.exit(1);
1153
+ }
1027
1154
  const task = args.task ? String(args.task) : readMaybeFile(args.taskFile ? String(args.taskFile) : void 0);
1028
1155
  if (!task) {
1029
1156
  console.error("missing --task or --task-file");
@@ -1031,7 +1158,7 @@ function startWorker(args) {
1031
1158
  }
1032
1159
  try {
1033
1160
  const worker = spawnWorkerProcess(run, {
1034
- name: String(args.name),
1161
+ name,
1035
1162
  task,
1036
1163
  ownedPaths: args.owned ? String(args.owned).split(",").map((s) => s.trim()).filter(Boolean) : [],
1037
1164
  model: args.model ? String(args.model) : void 0,
@@ -1228,7 +1355,7 @@ async function sweepRun(args) {
1228
1355
  for (const name of Object.keys(run.workers || {})) {
1229
1356
  const worker = readJson(
1230
1357
  path8.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
1231
- null
1358
+ void 0
1232
1359
  );
1233
1360
  if (!worker || !worker.dispatched || !worker.taskId) continue;
1234
1361
  const status = computeWorkerStatus(worker);
@@ -1270,7 +1397,7 @@ async function sweepRun(args) {
1270
1397
  }
1271
1398
 
1272
1399
  // src/worktree.ts
1273
- import { existsSync as existsSync8, mkdirSync as mkdirSync4 } from "node:fs";
1400
+ import { existsSync as existsSync9, mkdirSync as mkdirSync4 } from "node:fs";
1274
1401
  import path10 from "node:path";
1275
1402
 
1276
1403
  // src/validate.ts
@@ -1293,7 +1420,7 @@ function createRun(args) {
1293
1420
  ensureGitRepo(repo);
1294
1421
  const id = args.id ? validateRunId(String(args.id)) : timestampSlug(String(args.name || "run"));
1295
1422
  const dir = runDirectory(id);
1296
- if (existsSync8(dir)) failExists(`run already exists: ${id}`);
1423
+ if (existsSync9(dir)) failExists(`run already exists: ${id}`);
1297
1424
  mkdirSync4(dir, { recursive: true });
1298
1425
  const base = String(args.base || "origin/main");
1299
1426
  const baseCommit = git(repo, ["rev-parse", base]).trim();
@@ -1312,7 +1439,7 @@ function createRun(args) {
1312
1439
  }
1313
1440
  function listRuns() {
1314
1441
  const { runsDir } = getPaths();
1315
- const rows = listRunIds(runsDir).map((id) => readJson(path10.join(runDirectory(id), "run.json"), null)).filter(Boolean).map((run) => ({
1442
+ const rows = listRunIds(runsDir).map((id) => readJson(path10.join(runDirectory(id), "run.json"), void 0)).filter(Boolean).map((run) => ({
1316
1443
  id: run.id,
1317
1444
  name: run.name,
1318
1445
  status: run.status,
@@ -1425,12 +1552,12 @@ function runStatus(args) {
1425
1552
  const workers = names.map((name) => {
1426
1553
  const worker = readJson(
1427
1554
  path11.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
1428
- null
1555
+ void 0
1429
1556
  );
1430
1557
  if (!worker) {
1431
1558
  return { worker: name, status: "missing", attention: "needs_attention", attentionReason: "worker.json not found" };
1432
1559
  }
1433
- const status = computeWorkerStatus(worker);
1560
+ const status = computeWorkerStatus(worker, { base: run.base });
1434
1561
  return {
1435
1562
  worker: status.worker,
1436
1563
  status: status.status,
@@ -1444,7 +1571,9 @@ function runStatus(args) {
1444
1571
  lastHeartbeatSummary: status.lastHeartbeatSummary,
1445
1572
  heartbeatBlocker: status.heartbeatBlocker,
1446
1573
  changedFileCount: status.changedFiles.length,
1447
- branch: status.branch
1574
+ branch: status.branch,
1575
+ ancestry: status.gitAncestry.relation,
1576
+ ancestryChecked: status.gitAncestry.checked
1448
1577
  };
1449
1578
  });
1450
1579
  const board = {
@@ -1489,10 +1618,48 @@ function stopWorker(args) {
1489
1618
  }
1490
1619
 
1491
1620
  // src/pipeline-tick.ts
1492
- import path13 from "node:path";
1621
+ import path14 from "node:path";
1493
1622
 
1494
- // src/plan-progress-daemon-sync.ts
1623
+ // src/finalize.ts
1495
1624
  import path12 from "node:path";
1625
+ var ACTIVE_RUN_STATUSES = /* @__PURE__ */ new Set(["running", "dispatching", "pending", "queued"]);
1626
+ function terminalStatusFor(run) {
1627
+ const names = Object.keys(run.workers || {});
1628
+ if (names.length === 0) return "failed";
1629
+ let anyAlive = false;
1630
+ let anyResult = false;
1631
+ for (const name of names) {
1632
+ const worker = readJson(
1633
+ path12.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
1634
+ void 0
1635
+ );
1636
+ if (!worker) continue;
1637
+ const status = computeWorkerStatus(worker);
1638
+ if (status.alive && !status.finalResult) {
1639
+ anyAlive = true;
1640
+ break;
1641
+ }
1642
+ if (status.finalResult) anyResult = true;
1643
+ }
1644
+ if (anyAlive) return null;
1645
+ return anyResult ? "completed" : "failed";
1646
+ }
1647
+ function finalizeStaleRuns() {
1648
+ const finalized = [];
1649
+ for (const run of listRunRecords()) {
1650
+ if (!ACTIVE_RUN_STATUSES.has(run.status)) continue;
1651
+ const next = terminalStatusFor(run);
1652
+ if (!next || next === run.status) continue;
1653
+ const from = run.status;
1654
+ run.status = next;
1655
+ saveRun(run);
1656
+ finalized.push({ runId: run.id, from, to: next });
1657
+ }
1658
+ return finalized;
1659
+ }
1660
+
1661
+ // src/plan-progress-daemon-sync.ts
1662
+ import path13 from "node:path";
1496
1663
 
1497
1664
  // src/plan-progress-sync.ts
1498
1665
  async function syncPlanProgress(args) {
@@ -1516,8 +1683,8 @@ async function syncActiveWorkerPlanProgress(runId, args) {
1516
1683
  const outcomes = [];
1517
1684
  for (const name of Object.keys(run.workers || {})) {
1518
1685
  const worker = readJson(
1519
- path12.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
1520
- null
1686
+ path13.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
1687
+ void 0
1521
1688
  );
1522
1689
  if (!worker?.dispatched || !worker.taskId) continue;
1523
1690
  const status = computeWorkerStatus(worker);
@@ -1570,13 +1737,12 @@ async function completeFinishedWorkers(runId, args) {
1570
1737
  const outcomes = [];
1571
1738
  for (const name of Object.keys(run.workers || {})) {
1572
1739
  const worker = readJson(
1573
- path13.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
1574
- null
1740
+ path14.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
1741
+ void 0
1575
1742
  );
1576
1743
  if (!worker?.dispatched || !worker.taskId) continue;
1577
1744
  const status = computeWorkerStatus(worker);
1578
1745
  if (!isFinishedWorkerStatus(status)) continue;
1579
- if (!status.finalResult) continue;
1580
1746
  const result = await tryCompleteWorker({
1581
1747
  run: runId,
1582
1748
  name,
@@ -1604,6 +1770,7 @@ async function runPipelineTick(args) {
1604
1770
  const agentOsId = String(required(String(args.agentOsId || ""), "--agent-os-id"));
1605
1771
  const execute = args.execute !== false && args.execute !== "false";
1606
1772
  runStatus({ run: runId });
1773
+ const finalizedStaleRuns = finalizeStaleRuns();
1607
1774
  const completedWorkers = await completeFinishedWorkers(runId, args);
1608
1775
  const planProgressSync = await syncActiveWorkerPlanProgress(runId, args);
1609
1776
  const workspacePrefs = await fetchWorkspaceRuntimePreferences(agentOsId, args);
@@ -1645,6 +1812,7 @@ async function runPipelineTick(args) {
1645
1812
  execute,
1646
1813
  resourceGate,
1647
1814
  completedWorkers,
1815
+ finalizedStaleRuns,
1648
1816
  planProgressSync,
1649
1817
  operatorTick,
1650
1818
  sweep,