@kynver-app/runtime 0.1.11 → 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);
@@ -804,13 +820,23 @@ function computeAutoMaxWorkers(totalMemBytes, opts = {}) {
804
820
  const raw = Math.max(1, Math.floor(budgetBytes / perWorkerMemBytes));
805
821
  return Math.min(raw, AUTO_MAX_WORKERS_CEILING);
806
822
  }
807
- function countActiveWorkers(runId) {
808
- 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) {
809
835
  let active = 0;
810
836
  for (const name of Object.keys(run.workers || {})) {
811
837
  const worker = readJson(
812
838
  path5.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
813
- null
839
+ void 0
814
840
  );
815
841
  if (!worker) continue;
816
842
  const status = computeWorkerStatus(worker);
@@ -820,14 +846,19 @@ function countActiveWorkers(runId) {
820
846
  }
821
847
  return active;
822
848
  }
849
+ function countActiveWorkersGlobal() {
850
+ let active = 0;
851
+ for (const run of listRunRecords()) active += countActiveWorkersForRun(run);
852
+ return active;
853
+ }
823
854
  function observeRunnerResourceGate(input) {
824
855
  const { perWorkerMemBytes, memReserveBytes, memUtilization, configuredMaxWorkers } = resolveResourceConfig(
825
856
  input.config,
826
857
  input.configuredMaxWorkersOverride
827
858
  );
828
859
  const totalMemBytes = input.totalMemBytes ?? os.totalmem();
829
- const freeMemBytes = input.freeMemBytes ?? os.freemem();
830
- const activeWorkers = input.activeWorkers ?? countActiveWorkers(input.runId);
860
+ const freeMemBytes = input.freeMemBytes ?? readAvailableMemBytes();
861
+ const activeWorkers = input.activeWorkers ?? countActiveWorkersGlobal();
831
862
  const budgetBytes = Math.max(0, Math.floor(totalMemBytes * memUtilization) - memReserveBytes);
832
863
  const capacityFromTotal = Math.max(0, Math.floor(budgetBytes / perWorkerMemBytes));
833
864
  const capacityFromFree = Math.max(0, Math.floor(Math.max(0, freeMemBytes - memReserveBytes) / perWorkerMemBytes));
@@ -835,13 +866,13 @@ function observeRunnerResourceGate(input) {
835
866
  const targetCap = configuredMaxWorkers ?? autoCap;
836
867
  const maxConcurrentWorkers = Math.max(0, Math.min(targetCap, capacityFromTotal));
837
868
  const slotsByCapacity = Math.max(0, maxConcurrentWorkers - activeWorkers);
838
- const slotsByFreeMem = Math.max(0, capacityFromFree - activeWorkers);
869
+ const slotsByFreeMem = capacityFromFree;
839
870
  const slotsAvailable = Math.min(slotsByCapacity, slotsByFreeMem);
840
871
  let reason = null;
841
872
  if (slotsAvailable <= 0) {
842
873
  if (activeWorkers >= maxConcurrentWorkers) {
843
874
  reason = `at worker limit (${activeWorkers}/${maxConcurrentWorkers} running)`;
844
- } else if (capacityFromFree <= activeWorkers) {
875
+ } else if (capacityFromFree <= 0) {
845
876
  reason = "insufficient free memory \u2014 waiting for workers to finish";
846
877
  } else {
847
878
  reason = "no worker slots available";
@@ -864,7 +895,7 @@ function observeRunnerResourceGate(input) {
864
895
  }
865
896
 
866
897
  // src/supervisor.ts
867
- import { existsSync as existsSync7, mkdirSync as mkdirSync3 } from "node:fs";
898
+ import { existsSync as existsSync8, mkdirSync as mkdirSync3 } from "node:fs";
868
899
  import path7 from "node:path";
869
900
 
870
901
  // src/prompt.ts
@@ -937,19 +968,19 @@ var claudeProvider = {
937
968
  };
938
969
 
939
970
  // src/providers/cursor.ts
940
- 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";
941
972
  import { spawn as spawn2 } from "node:child_process";
942
973
  import path6 from "node:path";
943
974
  var DEFAULT_CURSOR_MODEL = "composer-2.5";
944
975
  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));
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));
947
978
  return versions[0] ? path6.join(versionsRoot, versions[0]) : null;
948
979
  }
949
980
  function resolveBundledCursor(versionDir) {
950
981
  const nodeExe = path6.join(versionDir, "node.exe");
951
982
  const indexJs = path6.join(versionDir, "index.js");
952
- if (!existsSync6(nodeExe) || !existsSync6(indexJs)) return null;
983
+ if (!existsSync7(nodeExe) || !existsSync7(indexJs)) return null;
953
984
  return { executable: nodeExe, prefixArgs: [indexJs], shell: false, detached: true };
954
985
  }
955
986
  function resolveWindowsCursorSpawn(agentBin) {
@@ -972,7 +1003,7 @@ function resolveAgentBin() {
972
1003
  if (configured) return configured;
973
1004
  if (process.platform === "win32") {
974
1005
  const localAgent = path6.join(process.env.LOCALAPPDATA || "", "cursor-agent", "agent.cmd");
975
- if (existsSync6(localAgent)) return localAgent;
1006
+ if (existsSync7(localAgent)) return localAgent;
976
1007
  }
977
1008
  return "agent";
978
1009
  }
@@ -1041,8 +1072,11 @@ function resolveWorkerProvider(name) {
1041
1072
 
1042
1073
  // src/supervisor.ts
1043
1074
  function spawnWorkerProcess(run, opts) {
1044
- const name = safeSlug(opts.name);
1045
- 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);
1046
1080
  if (run.workers?.[name]) throw new Error(`worker already exists in run ${run.id}: ${name}`);
1047
1081
  if (!opts.task) throw new Error(`missing task text for worker ${name}`);
1048
1082
  const { worktreesDir } = getPaths();
@@ -1050,7 +1084,7 @@ function spawnWorkerProcess(run, opts) {
1050
1084
  mkdirSync3(workerDir, { recursive: true });
1051
1085
  const worktreePath = path7.join(worktreesDir, run.id, name);
1052
1086
  const branch = opts.branch || `agent/${run.id}/${name}`;
1053
- if (existsSync7(worktreePath)) throw new Error(`worktree path already exists: ${worktreePath}`);
1087
+ if (existsSync8(worktreePath)) throw new Error(`worktree path already exists: ${worktreePath}`);
1054
1088
  git(run.repo, ["fetch", "origin", "--prune"], { allowFailure: true });
1055
1089
  git(run.repo, ["worktree", "add", "-b", branch, worktreePath, run.baseCommit], { throwError: true });
1056
1090
  const stdoutPath = path7.join(workerDir, "stdout.jsonl");
@@ -1112,6 +1146,11 @@ function spawnWorkerProcess(run, opts) {
1112
1146
  }
1113
1147
  function startWorker(args) {
1114
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
+ }
1115
1154
  const task = args.task ? String(args.task) : readMaybeFile(args.taskFile ? String(args.taskFile) : void 0);
1116
1155
  if (!task) {
1117
1156
  console.error("missing --task or --task-file");
@@ -1119,7 +1158,7 @@ function startWorker(args) {
1119
1158
  }
1120
1159
  try {
1121
1160
  const worker = spawnWorkerProcess(run, {
1122
- name: String(args.name),
1161
+ name,
1123
1162
  task,
1124
1163
  ownedPaths: args.owned ? String(args.owned).split(",").map((s) => s.trim()).filter(Boolean) : [],
1125
1164
  model: args.model ? String(args.model) : void 0,
@@ -1316,7 +1355,7 @@ async function sweepRun(args) {
1316
1355
  for (const name of Object.keys(run.workers || {})) {
1317
1356
  const worker = readJson(
1318
1357
  path8.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
1319
- null
1358
+ void 0
1320
1359
  );
1321
1360
  if (!worker || !worker.dispatched || !worker.taskId) continue;
1322
1361
  const status = computeWorkerStatus(worker);
@@ -1358,7 +1397,7 @@ async function sweepRun(args) {
1358
1397
  }
1359
1398
 
1360
1399
  // src/worktree.ts
1361
- import { existsSync as existsSync8, mkdirSync as mkdirSync4 } from "node:fs";
1400
+ import { existsSync as existsSync9, mkdirSync as mkdirSync4 } from "node:fs";
1362
1401
  import path10 from "node:path";
1363
1402
 
1364
1403
  // src/validate.ts
@@ -1381,7 +1420,7 @@ function createRun(args) {
1381
1420
  ensureGitRepo(repo);
1382
1421
  const id = args.id ? validateRunId(String(args.id)) : timestampSlug(String(args.name || "run"));
1383
1422
  const dir = runDirectory(id);
1384
- if (existsSync8(dir)) failExists(`run already exists: ${id}`);
1423
+ if (existsSync9(dir)) failExists(`run already exists: ${id}`);
1385
1424
  mkdirSync4(dir, { recursive: true });
1386
1425
  const base = String(args.base || "origin/main");
1387
1426
  const baseCommit = git(repo, ["rev-parse", base]).trim();
@@ -1400,7 +1439,7 @@ function createRun(args) {
1400
1439
  }
1401
1440
  function listRuns() {
1402
1441
  const { runsDir } = getPaths();
1403
- 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) => ({
1404
1443
  id: run.id,
1405
1444
  name: run.name,
1406
1445
  status: run.status,
@@ -1513,7 +1552,7 @@ function runStatus(args) {
1513
1552
  const workers = names.map((name) => {
1514
1553
  const worker = readJson(
1515
1554
  path11.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
1516
- null
1555
+ void 0
1517
1556
  );
1518
1557
  if (!worker) {
1519
1558
  return { worker: name, status: "missing", attention: "needs_attention", attentionReason: "worker.json not found" };
@@ -1579,10 +1618,48 @@ function stopWorker(args) {
1579
1618
  }
1580
1619
 
1581
1620
  // src/pipeline-tick.ts
1582
- import path13 from "node:path";
1621
+ import path14 from "node:path";
1583
1622
 
1584
- // src/plan-progress-daemon-sync.ts
1623
+ // src/finalize.ts
1585
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";
1586
1663
 
1587
1664
  // src/plan-progress-sync.ts
1588
1665
  async function syncPlanProgress(args) {
@@ -1606,8 +1683,8 @@ async function syncActiveWorkerPlanProgress(runId, args) {
1606
1683
  const outcomes = [];
1607
1684
  for (const name of Object.keys(run.workers || {})) {
1608
1685
  const worker = readJson(
1609
- path12.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
1610
- null
1686
+ path13.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
1687
+ void 0
1611
1688
  );
1612
1689
  if (!worker?.dispatched || !worker.taskId) continue;
1613
1690
  const status = computeWorkerStatus(worker);
@@ -1660,13 +1737,12 @@ async function completeFinishedWorkers(runId, args) {
1660
1737
  const outcomes = [];
1661
1738
  for (const name of Object.keys(run.workers || {})) {
1662
1739
  const worker = readJson(
1663
- path13.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
1664
- null
1740
+ path14.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
1741
+ void 0
1665
1742
  );
1666
1743
  if (!worker?.dispatched || !worker.taskId) continue;
1667
1744
  const status = computeWorkerStatus(worker);
1668
1745
  if (!isFinishedWorkerStatus(status)) continue;
1669
- if (!status.finalResult) continue;
1670
1746
  const result = await tryCompleteWorker({
1671
1747
  run: runId,
1672
1748
  name,
@@ -1694,6 +1770,7 @@ async function runPipelineTick(args) {
1694
1770
  const agentOsId = String(required(String(args.agentOsId || ""), "--agent-os-id"));
1695
1771
  const execute = args.execute !== false && args.execute !== "false";
1696
1772
  runStatus({ run: runId });
1773
+ const finalizedStaleRuns = finalizeStaleRuns();
1697
1774
  const completedWorkers = await completeFinishedWorkers(runId, args);
1698
1775
  const planProgressSync = await syncActiveWorkerPlanProgress(runId, args);
1699
1776
  const workspacePrefs = await fetchWorkspaceRuntimePreferences(agentOsId, args);
@@ -1735,6 +1812,7 @@ async function runPipelineTick(args) {
1735
1812
  execute,
1736
1813
  resourceGate,
1737
1814
  completedWorkers,
1815
+ finalizedStaleRuns,
1738
1816
  planProgressSync,
1739
1817
  operatorTick,
1740
1818
  sweep,