@kynver-app/runtime 0.1.117 → 0.1.119
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/chat/anthropic-credentials.d.ts +9 -0
- package/dist/chat/anthropic-stream.d.ts +43 -0
- package/dist/chat/chat-claim-loop.d.ts +25 -0
- package/dist/chat/delta-batcher.d.ts +13 -0
- package/dist/cli.js +8147 -6990
- package/dist/cli.js.map +4 -4
- package/dist/config.d.ts +2 -0
- package/dist/cron/cron-env-file.d.ts +15 -0
- package/dist/cron/cron-id.d.ts +3 -0
- package/dist/cron/cron-install-api.d.ts +27 -0
- package/dist/cron/cron-install-cli.d.ts +2 -0
- package/dist/cron/cron-install-plan.d.ts +32 -0
- package/dist/cron/cron-install-secrets.d.ts +6 -0
- package/dist/cron/cron-install-systemd.d.ts +18 -0
- package/dist/cron/cron-install-verify.d.ts +15 -0
- package/dist/cron/cron-install.d.ts +51 -0
- package/dist/cron/cron-store.d.ts +5 -0
- package/dist/index.js +1353 -198
- package/dist/index.js.map +4 -4
- package/dist/server/cleanup.js +4 -1
- package/dist/server/cleanup.js.map +2 -2
- package/dist/server/default-repo.js.map +2 -2
- package/dist/server/memory-cost-enforce.js.map +1 -1
- package/dist/server/monitor.js.map +2 -2
- package/dist/server/worker-policy.js.map +2 -2
- package/dist/start.d.ts +7 -0
- package/dist/worktree.d.ts +8 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -532,23 +532,23 @@ function isWslHost() {
|
|
|
532
532
|
function observeWslHostDisk(options = {}) {
|
|
533
533
|
const wsl = options.forceWsl === void 0 ? isWslHost() : options.forceWsl;
|
|
534
534
|
if (!wsl) return null;
|
|
535
|
-
const
|
|
535
|
+
const path79 = options.wslHostMount?.trim() || process.env.KYNVER_WSL_HOST_MOUNT?.trim() || DEFAULT_WSL_HOST_MOUNT;
|
|
536
536
|
const warnBelowBytes = options.wslHostFreeWarnBytes ?? DEFAULT_WSL_HOST_WARN_FREE_BYTES;
|
|
537
537
|
const criticalBelowBytes = options.wslHostFreeCriticalBytes ?? DEFAULT_WSL_HOST_CRITICAL_FREE_BYTES;
|
|
538
538
|
const statfs = options.statfs ?? statfsSync;
|
|
539
539
|
let stats;
|
|
540
540
|
try {
|
|
541
|
-
stats = statfs(
|
|
541
|
+
stats = statfs(path79);
|
|
542
542
|
} catch (error) {
|
|
543
543
|
return {
|
|
544
544
|
ok: false,
|
|
545
|
-
path:
|
|
545
|
+
path: path79,
|
|
546
546
|
freeBytes: 0,
|
|
547
547
|
totalBytes: 0,
|
|
548
548
|
usedPercent: 100,
|
|
549
549
|
warnBelowBytes,
|
|
550
550
|
criticalBelowBytes,
|
|
551
|
-
reason: `Windows host disk probe failed at ${
|
|
551
|
+
reason: `Windows host disk probe failed at ${path79}: ${error.message}`,
|
|
552
552
|
probeError: error.message
|
|
553
553
|
};
|
|
554
554
|
}
|
|
@@ -562,11 +562,11 @@ function observeWslHostDisk(options = {}) {
|
|
|
562
562
|
let reason = null;
|
|
563
563
|
if (!ok) {
|
|
564
564
|
const tag = criticalFree ? "critical" : "warning";
|
|
565
|
-
reason = `Windows host disk ${
|
|
565
|
+
reason = `Windows host disk ${path79} at ${tag}: ${freeGiB} GiB free (<${(criticalFree ? criticalBelowBytes : warnBelowBytes) / 1024 / 1024 / 1024} GiB); WSL VHDX cannot grow safely. ${summarizeWslRecoverySteps()}`;
|
|
566
566
|
}
|
|
567
567
|
return {
|
|
568
568
|
ok,
|
|
569
|
-
path:
|
|
569
|
+
path: path79,
|
|
570
570
|
freeBytes,
|
|
571
571
|
totalBytes,
|
|
572
572
|
usedPercent,
|
|
@@ -592,12 +592,12 @@ var init_wsl_host = __esm({
|
|
|
592
592
|
// src/disk-gate.ts
|
|
593
593
|
import { statfsSync as statfsSync2 } from "node:fs";
|
|
594
594
|
function observeRunnerDiskGate(input = {}) {
|
|
595
|
-
const
|
|
595
|
+
const path79 = input.diskPath?.trim() || "/";
|
|
596
596
|
const warnBelowBytes = input.diskFreeWarnBytes ?? DEFAULT_WARN_FREE_BYTES;
|
|
597
597
|
const criticalBelowBytes = input.diskFreeCriticalBytes ?? DEFAULT_CRITICAL_FREE_BYTES;
|
|
598
598
|
const maxUsedPercent = input.diskMaxUsedPercent ?? DEFAULT_MAX_USED_PERCENT;
|
|
599
599
|
const hardMaxUsedPercent = input.diskHardMaxUsedPercent ?? DEFAULT_HARD_MAX_USED_PERCENT;
|
|
600
|
-
const stats = statfsSync2(
|
|
600
|
+
const stats = statfsSync2(path79);
|
|
601
601
|
const freeBytes = Number(stats.bavail) * Number(stats.bsize);
|
|
602
602
|
const totalBytes = Number(stats.blocks) * Number(stats.bsize);
|
|
603
603
|
const usedPercent = totalBytes > 0 ? (totalBytes - freeBytes) / totalBytes * 100 : 100;
|
|
@@ -620,7 +620,7 @@ function observeRunnerDiskGate(input = {}) {
|
|
|
620
620
|
}
|
|
621
621
|
return {
|
|
622
622
|
ok,
|
|
623
|
-
path:
|
|
623
|
+
path: path79,
|
|
624
624
|
freeBytes,
|
|
625
625
|
totalBytes,
|
|
626
626
|
usedPercent,
|
|
@@ -3165,6 +3165,7 @@ async function enforceMemoryCostPackageGuardAtStartup(input = {}) {
|
|
|
3165
3165
|
function shouldEnforceMemoryCostPackageGuardCli(scope, action) {
|
|
3166
3166
|
if (!scope) return false;
|
|
3167
3167
|
if (scope === "daemon") return true;
|
|
3168
|
+
if (scope === "start") return true;
|
|
3168
3169
|
if (scope === "worker") return true;
|
|
3169
3170
|
if (scope === "monitor") return true;
|
|
3170
3171
|
if (scope === "run" && (action === "dispatch" || action === "sweep" || action === "reconcile" || action === "unblock")) {
|
|
@@ -5857,8 +5858,8 @@ function dirtyPathsCoveredByDisposableRemoval(changedFiles, removed) {
|
|
|
5857
5858
|
if (removed.length === 0) return false;
|
|
5858
5859
|
const removedSet = new Set(removed.map((p) => normalizeRelativePath(p)));
|
|
5859
5860
|
return material.every((line) => {
|
|
5860
|
-
const
|
|
5861
|
-
return removedSet.has(
|
|
5861
|
+
const path79 = normalizeRelativePath(pathFromGitStatusLine(line));
|
|
5862
|
+
return removedSet.has(path79);
|
|
5862
5863
|
});
|
|
5863
5864
|
}
|
|
5864
5865
|
|
|
@@ -8424,7 +8425,10 @@ async function dispatchRun(args) {
|
|
|
8424
8425
|
} : {}
|
|
8425
8426
|
});
|
|
8426
8427
|
const requestDispatch = async (maxStarts) => {
|
|
8427
|
-
const dispatch = await postJsonWithCredentialRefresh(dispatchUrl, secret, buildBody(maxStarts), {
|
|
8428
|
+
const dispatch = await postJsonWithCredentialRefresh(dispatchUrl, secret, buildBody(maxStarts), {
|
|
8429
|
+
agentOsId,
|
|
8430
|
+
baseUrl: base
|
|
8431
|
+
}) ?? { ok: false, status: 0, response: null };
|
|
8428
8432
|
const responseBody = dispatch.response;
|
|
8429
8433
|
return { dispatch, result: responseBody?.result };
|
|
8430
8434
|
};
|
|
@@ -8525,6 +8529,13 @@ async function dispatchRun(args) {
|
|
|
8525
8529
|
task,
|
|
8526
8530
|
report
|
|
8527
8531
|
});
|
|
8532
|
+
if (!post.ok) {
|
|
8533
|
+
return abortClaimedSpawn(
|
|
8534
|
+
task,
|
|
8535
|
+
`land_pr completion POST failed (HTTP ${post.status})`,
|
|
8536
|
+
{ landPr: true, outcome: report.outcome }
|
|
8537
|
+
);
|
|
8538
|
+
}
|
|
8528
8539
|
outcomes.push({
|
|
8529
8540
|
taskId,
|
|
8530
8541
|
started: true,
|
|
@@ -8532,12 +8543,6 @@ async function dispatchRun(args) {
|
|
|
8532
8543
|
outcome: report.outcome,
|
|
8533
8544
|
completionStatus: post.status
|
|
8534
8545
|
});
|
|
8535
|
-
if (!post.ok) {
|
|
8536
|
-
return abortClaimedSpawn(
|
|
8537
|
-
task,
|
|
8538
|
-
`land_pr completion POST failed (HTTP ${post.status})`
|
|
8539
|
-
);
|
|
8540
|
-
}
|
|
8541
8546
|
return true;
|
|
8542
8547
|
} catch (error) {
|
|
8543
8548
|
return abortClaimedSpawn(task, error.message);
|
|
@@ -8676,10 +8681,22 @@ async function dispatchRun(args) {
|
|
|
8676
8681
|
return abortClaimedSpawn(task, error.message);
|
|
8677
8682
|
}
|
|
8678
8683
|
}
|
|
8679
|
-
|
|
8680
|
-
|
|
8684
|
+
const failedStartTaskIds = /* @__PURE__ */ new Set();
|
|
8685
|
+
async function admitClaimedDecision(decision) {
|
|
8686
|
+
const task = decision.task;
|
|
8687
|
+
const taskId = String(task.id);
|
|
8688
|
+
if (failedStartTaskIds.has(taskId)) {
|
|
8689
|
+
return abortClaimedSpawn(
|
|
8690
|
+
task,
|
|
8691
|
+
"dispatch_retry_loop_prevented: task already failed to start this tick"
|
|
8692
|
+
);
|
|
8693
|
+
}
|
|
8681
8694
|
const admitted = isLandPrDecision2(decision) ? await runLandPrClaimed(decision) : await spawnClaimed(decision);
|
|
8682
|
-
|
|
8695
|
+
if (!admitted) failedStartTaskIds.add(taskId);
|
|
8696
|
+
return admitted;
|
|
8697
|
+
}
|
|
8698
|
+
for (const decision of result.started) {
|
|
8699
|
+
await admitClaimedDecision(decision);
|
|
8683
8700
|
}
|
|
8684
8701
|
skipped.push(
|
|
8685
8702
|
...result.skipped ?? []
|
|
@@ -8698,9 +8715,7 @@ async function dispatchRun(args) {
|
|
|
8698
8715
|
});
|
|
8699
8716
|
}
|
|
8700
8717
|
}
|
|
8701
|
-
|
|
8702
|
-
while (shouldContinueDispatch && outcomes.length < cappedStarts) {
|
|
8703
|
-
if (exactTargetMode) break;
|
|
8718
|
+
while (!exactTargetMode && outcomes.length < cappedStarts) {
|
|
8704
8719
|
const next = await requestDispatch(1);
|
|
8705
8720
|
if (!next.dispatch.ok || !next.result) {
|
|
8706
8721
|
outcomes.push({
|
|
@@ -8717,9 +8732,7 @@ async function dispatchRun(args) {
|
|
|
8717
8732
|
if (started.length === 0) break;
|
|
8718
8733
|
for (const decision of started) {
|
|
8719
8734
|
if (outcomes.length >= cappedStarts) break;
|
|
8720
|
-
|
|
8721
|
-
shouldContinueDispatch = admitted && shouldContinueDispatch;
|
|
8722
|
-
if (!shouldContinueDispatch) break;
|
|
8735
|
+
await admitClaimedDecision(decision);
|
|
8723
8736
|
}
|
|
8724
8737
|
}
|
|
8725
8738
|
const startedCount = outcomes.filter((o) => o.started).length;
|
|
@@ -8947,8 +8960,8 @@ function resolveOpenAiCodexRetryBudget(input) {
|
|
|
8947
8960
|
const env = input.env ?? process.env;
|
|
8948
8961
|
const base = Math.max(1, input.defaultRetries ?? 3);
|
|
8949
8962
|
const provider = String(input.provider ?? "").toLowerCase();
|
|
8950
|
-
const
|
|
8951
|
-
if (provider !== OPENAI_CODEX_PROVIDER ||
|
|
8963
|
+
const platform2 = String(input.platform ?? "").toLowerCase();
|
|
8964
|
+
if (provider !== OPENAI_CODEX_PROVIDER || platform2 !== "cron") {
|
|
8952
8965
|
return base;
|
|
8953
8966
|
}
|
|
8954
8967
|
const raw = env.HERMES_CODEX_CRON_API_MAX_RETRIES?.trim();
|
|
@@ -10489,7 +10502,9 @@ function createRun(args) {
|
|
|
10489
10502
|
workers: {}
|
|
10490
10503
|
};
|
|
10491
10504
|
writeJson(path38.join(dir, "run.json"), run);
|
|
10492
|
-
|
|
10505
|
+
const info = { runId: id, runDir: dir, repo, base, baseCommit };
|
|
10506
|
+
console.log(JSON.stringify(info, null, 2));
|
|
10507
|
+
return info;
|
|
10493
10508
|
}
|
|
10494
10509
|
function listRuns() {
|
|
10495
10510
|
listRunsCli();
|
|
@@ -11027,11 +11042,11 @@ var LIVE_SKIP_REASONS = /* @__PURE__ */ new Set([
|
|
|
11027
11042
|
function collectPreservedLivePaths(actions, skips) {
|
|
11028
11043
|
const out = [];
|
|
11029
11044
|
const seen = /* @__PURE__ */ new Set();
|
|
11030
|
-
const push = (
|
|
11031
|
-
const key = `${
|
|
11045
|
+
const push = (path79, reason, detail) => {
|
|
11046
|
+
const key = `${path79}\0${reason}`;
|
|
11032
11047
|
if (seen.has(key) || out.length >= MAX_PRESERVED_LIVE_PATH_SAMPLES) return;
|
|
11033
11048
|
seen.add(key);
|
|
11034
|
-
out.push({ path:
|
|
11049
|
+
out.push({ path: path79, reason, ...detail ? { detail } : {} });
|
|
11035
11050
|
};
|
|
11036
11051
|
for (const skip2 of skips) {
|
|
11037
11052
|
if (!LIVE_SKIP_REASONS.has(skip2.reason)) continue;
|
|
@@ -11967,7 +11982,10 @@ function observeCleanupDiskPressure(input = {}) {
|
|
|
11967
11982
|
diskPath,
|
|
11968
11983
|
diskMaxUsedPercent: input.diskMaxUsedPercent ?? maxUsedPercent
|
|
11969
11984
|
});
|
|
11970
|
-
|
|
11985
|
+
let pressured = !diskGate.ok || diskGate.usedPercent >= maxUsedPercent;
|
|
11986
|
+
const force = process.env.KYNVER_CLEANUP_DISK_PRESSURE_FORCE?.trim().toLowerCase();
|
|
11987
|
+
if (force === "none" || force === "off" || force === "0") pressured = false;
|
|
11988
|
+
else if (force === "pressured" || force === "on" || force === "1") pressured = true;
|
|
11971
11989
|
return { diskGate, pressured, maxUsedPercent };
|
|
11972
11990
|
}
|
|
11973
11991
|
function applyDiskPressureToRetention(retention, pressure) {
|
|
@@ -12448,7 +12466,7 @@ function isPipelineCleanupEnabled() {
|
|
|
12448
12466
|
|
|
12449
12467
|
// src/cli.ts
|
|
12450
12468
|
init_config();
|
|
12451
|
-
import { mkdirSync as
|
|
12469
|
+
import { mkdirSync as mkdirSync13, realpathSync } from "node:fs";
|
|
12452
12470
|
import { fileURLToPath as fileURLToPath5 } from "node:url";
|
|
12453
12471
|
|
|
12454
12472
|
// src/bootstrap.ts
|
|
@@ -12510,68 +12528,13 @@ async function runBootstrap(args) {
|
|
|
12510
12528
|
await runSetup(setupArgs);
|
|
12511
12529
|
console.log("");
|
|
12512
12530
|
console.log(` Bootstrap complete \u2014 ${os9.hostname()} is linked to workspace "${primary.slug}".`);
|
|
12513
|
-
console.log(" Next:
|
|
12514
|
-
console.log(" (
|
|
12531
|
+
console.log(" Next: bring your agent online with `kynver start`.");
|
|
12532
|
+
console.log(" (Advanced: `kynver run create --repo /path/to/repo` + `kynver daemon --run <RUN_ID> --agent-os-id <AOS_ID> --execute`.)");
|
|
12515
12533
|
}
|
|
12516
12534
|
|
|
12517
|
-
// src/
|
|
12518
|
-
|
|
12519
|
-
|
|
12520
|
-
// src/discard-disposable.ts
|
|
12521
|
-
init_run_store();
|
|
12522
|
-
init_status();
|
|
12523
|
-
import { existsSync as existsSync41, rmSync as rmSync4 } from "node:fs";
|
|
12524
|
-
import path54 from "node:path";
|
|
12525
|
-
function normalizeRelativePath2(value) {
|
|
12526
|
-
const normalized = value.replace(/\\/g, "/").replace(/^\.\//, "").trim();
|
|
12527
|
-
if (!normalized || normalized.startsWith("/") || normalized.includes("..")) {
|
|
12528
|
-
throw new Error(`unsafe path: ${value}`);
|
|
12529
|
-
}
|
|
12530
|
-
return normalized;
|
|
12531
|
-
}
|
|
12532
|
-
function parsePathsArg(raw) {
|
|
12533
|
-
if (typeof raw !== "string" || !raw.trim()) return [];
|
|
12534
|
-
return raw.split(",").map((p) => p.trim()).filter(Boolean);
|
|
12535
|
-
}
|
|
12536
|
-
function discardDisposableArtifacts(args) {
|
|
12537
|
-
const { runId, workerName } = resolveWorkerTargetArgs(args);
|
|
12538
|
-
const worker = loadWorker(runId, workerName);
|
|
12539
|
-
const paths = [
|
|
12540
|
-
...parsePathsArg(args.path),
|
|
12541
|
-
...Array.isArray(args.paths) ? args.paths : []
|
|
12542
|
-
];
|
|
12543
|
-
if (paths.length === 0) {
|
|
12544
|
-
return { ok: false, removed: [], reason: "requires at least one --path" };
|
|
12545
|
-
}
|
|
12546
|
-
const worktreeRoot = path54.resolve(worker.worktreePath);
|
|
12547
|
-
const removed = [];
|
|
12548
|
-
for (const raw of paths) {
|
|
12549
|
-
const rel = normalizeRelativePath2(raw);
|
|
12550
|
-
const abs = path54.resolve(worktreeRoot, rel);
|
|
12551
|
-
if (!abs.startsWith(worktreeRoot + path54.sep) && abs !== worktreeRoot) {
|
|
12552
|
-
return { ok: false, removed, reason: `path escapes worktree: ${raw}` };
|
|
12553
|
-
}
|
|
12554
|
-
if (!existsSync41(abs)) {
|
|
12555
|
-
return { ok: false, removed, reason: `path not found: ${raw}` };
|
|
12556
|
-
}
|
|
12557
|
-
rmSync4(abs, { recursive: true, force: true });
|
|
12558
|
-
removed.push(rel);
|
|
12559
|
-
}
|
|
12560
|
-
const prior = Array.isArray(worker.disposableArtifactsRemoved) ? worker.disposableArtifactsRemoved.filter((p) => typeof p === "string") : [];
|
|
12561
|
-
worker.disposableArtifactsRemoved = [.../* @__PURE__ */ new Set([...prior, ...removed])];
|
|
12562
|
-
saveWorker(worker.runId, worker);
|
|
12563
|
-
const status = computeWorkerStatus(worker);
|
|
12564
|
-
return {
|
|
12565
|
-
ok: true,
|
|
12566
|
-
removed,
|
|
12567
|
-
...status.changedFiles.length ? { reason: "worktree still has other changes" } : {}
|
|
12568
|
-
};
|
|
12569
|
-
}
|
|
12570
|
-
function discardDisposableCli(args) {
|
|
12571
|
-
const result = discardDisposableArtifacts(args);
|
|
12572
|
-
console.log(JSON.stringify(result, null, 2));
|
|
12573
|
-
if (!result.ok) process.exit(1);
|
|
12574
|
-
}
|
|
12535
|
+
// src/start.ts
|
|
12536
|
+
init_config();
|
|
12537
|
+
import os12 from "node:os";
|
|
12575
12538
|
|
|
12576
12539
|
// src/daemon.ts
|
|
12577
12540
|
init_config();
|
|
@@ -12621,15 +12584,15 @@ function validateDaemonInstallIdentity(config = loadUserConfig(), env = process.
|
|
|
12621
12584
|
// src/daemon-heartbeat.ts
|
|
12622
12585
|
import { mkdirSync as mkdirSync7, readFileSync as readFileSync16, renameSync as renameSync3, writeFileSync as writeFileSync4 } from "node:fs";
|
|
12623
12586
|
import { homedir as homedir14 } from "node:os";
|
|
12624
|
-
import
|
|
12587
|
+
import path54 from "node:path";
|
|
12625
12588
|
function daemonHeartbeatPath(agentOsId) {
|
|
12626
12589
|
const safe = agentOsId.replace(/[^A-Za-z0-9_-]/g, "_");
|
|
12627
|
-
return
|
|
12590
|
+
return path54.join(homedir14(), ".kynver", `daemon-heartbeat-${safe}.json`);
|
|
12628
12591
|
}
|
|
12629
12592
|
function writeDaemonHeartbeat(input) {
|
|
12630
12593
|
try {
|
|
12631
12594
|
const file = daemonHeartbeatPath(input.agentOsId);
|
|
12632
|
-
mkdirSync7(
|
|
12595
|
+
mkdirSync7(path54.dirname(file), { recursive: true });
|
|
12633
12596
|
const beat = {
|
|
12634
12597
|
observedAt: (input.now ?? /* @__PURE__ */ new Date()).toISOString(),
|
|
12635
12598
|
pid: process.pid,
|
|
@@ -12679,9 +12642,9 @@ function assertNativeDaemonAllowed() {
|
|
|
12679
12642
|
|
|
12680
12643
|
// src/cron/cron-env.ts
|
|
12681
12644
|
init_config();
|
|
12682
|
-
import { existsSync as
|
|
12645
|
+
import { existsSync as existsSync41 } from "node:fs";
|
|
12683
12646
|
import { homedir as homedir15 } from "node:os";
|
|
12684
|
-
import
|
|
12647
|
+
import path55 from "node:path";
|
|
12685
12648
|
function envFlag4(name, defaultValue) {
|
|
12686
12649
|
const raw = process.env[name]?.trim().toLowerCase();
|
|
12687
12650
|
if (!raw) return defaultValue;
|
|
@@ -12697,7 +12660,7 @@ function envInt(name, fallback, min = 1) {
|
|
|
12697
12660
|
function defaultKynverCronStorePath() {
|
|
12698
12661
|
const explicit = process.env.KYNVER_CRON_STORE_PATH?.trim() || process.env.OPENCLAW_CRON_STORE_PATH?.trim();
|
|
12699
12662
|
if (explicit) return explicit;
|
|
12700
|
-
return
|
|
12663
|
+
return path55.join(homedir15(), ".kynver", "agent-os-cron.json");
|
|
12701
12664
|
}
|
|
12702
12665
|
function defaultKynverCronStatePath(storePath = defaultKynverCronStorePath()) {
|
|
12703
12666
|
const explicit = process.env.KYNVER_CRON_TICK_STATE_PATH?.trim();
|
|
@@ -12717,7 +12680,7 @@ function resolveKynverCronEnv() {
|
|
|
12717
12680
|
const fireBaseUrl = resolveKynverCronFireBaseUrl();
|
|
12718
12681
|
const secret = resolveKynverCronSecret();
|
|
12719
12682
|
const credsReady = Boolean(fireBaseUrl && secret);
|
|
12720
|
-
const storeExists =
|
|
12683
|
+
const storeExists = existsSync41(storePath);
|
|
12721
12684
|
const defaultEnabled = credsReady && (storeExists || envFlag4("KYNVER_CRON_TICK_FORCE", false));
|
|
12722
12685
|
return {
|
|
12723
12686
|
storePath,
|
|
@@ -12771,10 +12734,10 @@ async function fireKynverCronJob(input) {
|
|
|
12771
12734
|
|
|
12772
12735
|
// src/cron/cron-lock.ts
|
|
12773
12736
|
init_util();
|
|
12774
|
-
import { closeSync as closeSync6, existsSync as
|
|
12737
|
+
import { closeSync as closeSync6, existsSync as existsSync42, openSync as openSync6, readFileSync as readFileSync17, unlinkSync as unlinkSync3, writeFileSync as writeFileSync5 } from "node:fs";
|
|
12775
12738
|
var STALE_LOCK_MS = 10 * 6e4;
|
|
12776
12739
|
function readLockInfo(lockPath) {
|
|
12777
|
-
if (!
|
|
12740
|
+
if (!existsSync42(lockPath)) return null;
|
|
12778
12741
|
try {
|
|
12779
12742
|
const parsed = JSON.parse(readFileSync17(lockPath, "utf8"));
|
|
12780
12743
|
if (typeof parsed.pid === "number" && typeof parsed.at === "string") return parsed;
|
|
@@ -12792,14 +12755,14 @@ function lockIsStale(lockPath) {
|
|
|
12792
12755
|
return Date.now() - atMs > STALE_LOCK_MS;
|
|
12793
12756
|
}
|
|
12794
12757
|
function tryAcquireCronTickLock(lockPath) {
|
|
12795
|
-
if (
|
|
12758
|
+
if (existsSync42(lockPath) && !lockIsStale(lockPath)) {
|
|
12796
12759
|
const info = readLockInfo(lockPath);
|
|
12797
12760
|
return {
|
|
12798
12761
|
acquired: false,
|
|
12799
12762
|
reason: info ? `held by pid ${info.pid}` : "held by another process"
|
|
12800
12763
|
};
|
|
12801
12764
|
}
|
|
12802
|
-
if (
|
|
12765
|
+
if (existsSync42(lockPath)) {
|
|
12803
12766
|
try {
|
|
12804
12767
|
unlinkSync3(lockPath);
|
|
12805
12768
|
} catch {
|
|
@@ -12936,11 +12899,34 @@ async function loadCronJobs(storePath = defaultKynverCronStorePath()) {
|
|
|
12936
12899
|
const raw = await readFileIfExists(storePath);
|
|
12937
12900
|
return parseCronStore(raw);
|
|
12938
12901
|
}
|
|
12902
|
+
async function writeCronStore(storePath, entries) {
|
|
12903
|
+
const { mkdir, writeFile, rename } = await import("node:fs/promises");
|
|
12904
|
+
const { randomBytes: randomBytes3 } = await import("node:crypto");
|
|
12905
|
+
const { dirname: dirname3 } = await import("node:path");
|
|
12906
|
+
await mkdir(dirname3(storePath), { recursive: true });
|
|
12907
|
+
const tmp = `${storePath}.${randomBytes3(4).toString("hex")}.tmp`;
|
|
12908
|
+
const body = JSON.stringify({ entries }, null, 2);
|
|
12909
|
+
await writeFile(tmp, body, "utf8");
|
|
12910
|
+
await rename(tmp, storePath);
|
|
12911
|
+
}
|
|
12912
|
+
async function saveCronJob(entry, storePath = defaultKynverCronStorePath()) {
|
|
12913
|
+
const entries = await loadCronJobs(storePath);
|
|
12914
|
+
const idx = entries.findIndex((e) => e.providerScheduleId === entry.providerScheduleId);
|
|
12915
|
+
if (idx >= 0) entries[idx] = entry;
|
|
12916
|
+
else entries.push(entry);
|
|
12917
|
+
await writeCronStore(storePath, entries);
|
|
12918
|
+
}
|
|
12919
|
+
async function ensureCronStoreInitialized(storePath = defaultKynverCronStorePath()) {
|
|
12920
|
+
const raw = await readFileIfExists(storePath);
|
|
12921
|
+
if (raw !== null) return { created: false };
|
|
12922
|
+
await writeCronStore(storePath, []);
|
|
12923
|
+
return { created: true };
|
|
12924
|
+
}
|
|
12939
12925
|
|
|
12940
12926
|
// src/cron/cron-tick-state.ts
|
|
12941
12927
|
import { randomBytes } from "node:crypto";
|
|
12942
12928
|
import { promises as fs4 } from "node:fs";
|
|
12943
|
-
import
|
|
12929
|
+
import path56 from "node:path";
|
|
12944
12930
|
var EMPTY = { version: 1, jobs: {} };
|
|
12945
12931
|
async function readFileIfExists2(filePath) {
|
|
12946
12932
|
try {
|
|
@@ -12967,7 +12953,7 @@ async function loadCronTickState(statePath) {
|
|
|
12967
12953
|
return parseCronTickState(raw);
|
|
12968
12954
|
}
|
|
12969
12955
|
async function writeStateAtomic(statePath, state) {
|
|
12970
|
-
await fs4.mkdir(
|
|
12956
|
+
await fs4.mkdir(path56.dirname(statePath), { recursive: true });
|
|
12971
12957
|
const suffix = randomBytes(6).toString("hex");
|
|
12972
12958
|
const tmp = `${statePath}.tmp-${process.pid}-${Date.now()}-${suffix}`;
|
|
12973
12959
|
await fs4.writeFile(tmp, `${JSON.stringify(state, null, 2)}
|
|
@@ -13147,7 +13133,7 @@ async function runKynverCronTick(opts = {}) {
|
|
|
13147
13133
|
init_util();
|
|
13148
13134
|
|
|
13149
13135
|
// src/pipeline-tick.ts
|
|
13150
|
-
import
|
|
13136
|
+
import path59 from "node:path";
|
|
13151
13137
|
init_config();
|
|
13152
13138
|
|
|
13153
13139
|
// src/pipeline-dispatch.ts
|
|
@@ -13289,7 +13275,7 @@ init_util();
|
|
|
13289
13275
|
init_status();
|
|
13290
13276
|
init_run_store();
|
|
13291
13277
|
init_util();
|
|
13292
|
-
import
|
|
13278
|
+
import path57 from "node:path";
|
|
13293
13279
|
|
|
13294
13280
|
// src/plan-progress-sync.ts
|
|
13295
13281
|
init_config();
|
|
@@ -13314,7 +13300,7 @@ async function syncActiveWorkerPlanProgress(runId, args) {
|
|
|
13314
13300
|
const outcomes = [];
|
|
13315
13301
|
for (const name of Object.keys(run.workers || {})) {
|
|
13316
13302
|
const worker = readJson(
|
|
13317
|
-
|
|
13303
|
+
path57.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
13318
13304
|
void 0
|
|
13319
13305
|
);
|
|
13320
13306
|
if (!worker?.dispatched || !worker.taskId) continue;
|
|
@@ -13682,10 +13668,10 @@ function collectProviderEvidence(wanted, opts = {}) {
|
|
|
13682
13668
|
// src/provider-evidence/wanted-store.ts
|
|
13683
13669
|
init_run_store();
|
|
13684
13670
|
init_util();
|
|
13685
|
-
import
|
|
13671
|
+
import path58 from "node:path";
|
|
13686
13672
|
var WANTED_FILE = "provider-evidence-wanted.json";
|
|
13687
13673
|
function wantedFilePath(runId) {
|
|
13688
|
-
return
|
|
13674
|
+
return path58.join(runDirectory(runId), WANTED_FILE);
|
|
13689
13675
|
}
|
|
13690
13676
|
function parseWantedItems(value) {
|
|
13691
13677
|
if (!Array.isArray(value)) return [];
|
|
@@ -13724,7 +13710,7 @@ async function completeFinishedWorkers(runId, args) {
|
|
|
13724
13710
|
const outcomes = [];
|
|
13725
13711
|
for (const name of Object.keys(run.workers || {})) {
|
|
13726
13712
|
const worker = readJson(
|
|
13727
|
-
|
|
13713
|
+
path59.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
13728
13714
|
void 0
|
|
13729
13715
|
);
|
|
13730
13716
|
if (!worker?.taskId || worker.localOnly) continue;
|
|
@@ -13892,6 +13878,389 @@ async function runPipelineTick(args) {
|
|
|
13892
13878
|
};
|
|
13893
13879
|
}
|
|
13894
13880
|
|
|
13881
|
+
// src/chat/chat-claim-loop.ts
|
|
13882
|
+
init_config();
|
|
13883
|
+
import os11 from "node:os";
|
|
13884
|
+
|
|
13885
|
+
// src/chat/anthropic-credentials.ts
|
|
13886
|
+
import { readFileSync as readFileSync18 } from "node:fs";
|
|
13887
|
+
import { homedir as homedir16, platform } from "node:os";
|
|
13888
|
+
import path60 from "node:path";
|
|
13889
|
+
import { execFileSync as execFileSync4 } from "node:child_process";
|
|
13890
|
+
function parseClaudeCredentials(raw, now = Date.now()) {
|
|
13891
|
+
try {
|
|
13892
|
+
const blob = JSON.parse(raw);
|
|
13893
|
+
const token = blob.claudeAiOauth?.accessToken;
|
|
13894
|
+
const expiresAt = blob.claudeAiOauth?.expiresAt;
|
|
13895
|
+
if (typeof token !== "string" || !token) return null;
|
|
13896
|
+
if (typeof expiresAt === "number" && expiresAt - now < 6e4) return null;
|
|
13897
|
+
return token;
|
|
13898
|
+
} catch {
|
|
13899
|
+
return null;
|
|
13900
|
+
}
|
|
13901
|
+
}
|
|
13902
|
+
function readClaudeCliToken() {
|
|
13903
|
+
if (platform() === "darwin") {
|
|
13904
|
+
try {
|
|
13905
|
+
const raw = execFileSync4(
|
|
13906
|
+
"security",
|
|
13907
|
+
["find-generic-password", "-s", "Claude Code-credentials", "-w"],
|
|
13908
|
+
{ encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }
|
|
13909
|
+
);
|
|
13910
|
+
return parseClaudeCredentials(raw.trim());
|
|
13911
|
+
} catch {
|
|
13912
|
+
return null;
|
|
13913
|
+
}
|
|
13914
|
+
}
|
|
13915
|
+
try {
|
|
13916
|
+
const raw = readFileSync18(path60.join(homedir16(), ".claude", ".credentials.json"), "utf8");
|
|
13917
|
+
return parseClaudeCredentials(raw);
|
|
13918
|
+
} catch {
|
|
13919
|
+
return null;
|
|
13920
|
+
}
|
|
13921
|
+
}
|
|
13922
|
+
function resolveLocalAnthropicCredentials(env = process.env) {
|
|
13923
|
+
const apiKey = env.ANTHROPIC_API_KEY?.trim();
|
|
13924
|
+
if (apiKey) return { kind: "api_key", key: apiKey };
|
|
13925
|
+
const optIn = env.KYNVER_CHAT_USE_CLAUDE_OAUTH;
|
|
13926
|
+
if (optIn === "1" || optIn === "true" || optIn === "yes") {
|
|
13927
|
+
const token = readClaudeCliToken();
|
|
13928
|
+
if (token) return { kind: "oauth", token };
|
|
13929
|
+
}
|
|
13930
|
+
return null;
|
|
13931
|
+
}
|
|
13932
|
+
|
|
13933
|
+
// src/chat/anthropic-stream.ts
|
|
13934
|
+
var CLAUDE_CODE_IDENTITY = "You are Claude Code, Anthropic's official CLI for Claude.";
|
|
13935
|
+
function createAnthropicAccumulator() {
|
|
13936
|
+
return { blocks: [], stopReason: null, inputTokens: 0, outputTokens: 0 };
|
|
13937
|
+
}
|
|
13938
|
+
function foldAnthropicEvent(acc, event) {
|
|
13939
|
+
const e = event;
|
|
13940
|
+
switch (e.type) {
|
|
13941
|
+
case "message_start":
|
|
13942
|
+
acc.inputTokens = e.message?.usage?.input_tokens ?? 0;
|
|
13943
|
+
return "";
|
|
13944
|
+
case "content_block_start": {
|
|
13945
|
+
const idx = e.index ?? acc.blocks.length;
|
|
13946
|
+
acc.blocks[idx] = {
|
|
13947
|
+
type: e.content_block?.type === "tool_use" ? "tool_use" : "text",
|
|
13948
|
+
text: "",
|
|
13949
|
+
id: e.content_block?.id,
|
|
13950
|
+
name: e.content_block?.name,
|
|
13951
|
+
inputJson: ""
|
|
13952
|
+
};
|
|
13953
|
+
return "";
|
|
13954
|
+
}
|
|
13955
|
+
case "content_block_delta": {
|
|
13956
|
+
const block = acc.blocks[e.index ?? acc.blocks.length - 1];
|
|
13957
|
+
if (!block) return "";
|
|
13958
|
+
if (e.delta?.type === "text_delta" && e.delta.text) {
|
|
13959
|
+
block.text += e.delta.text;
|
|
13960
|
+
return e.delta.text;
|
|
13961
|
+
}
|
|
13962
|
+
if (e.delta?.type === "input_json_delta" && e.delta.partial_json) {
|
|
13963
|
+
block.inputJson += e.delta.partial_json;
|
|
13964
|
+
}
|
|
13965
|
+
return "";
|
|
13966
|
+
}
|
|
13967
|
+
case "message_delta":
|
|
13968
|
+
if (e.delta?.stop_reason) acc.stopReason = e.delta.stop_reason;
|
|
13969
|
+
if (typeof e.usage?.output_tokens === "number") acc.outputTokens = e.usage.output_tokens;
|
|
13970
|
+
return "";
|
|
13971
|
+
default:
|
|
13972
|
+
return "";
|
|
13973
|
+
}
|
|
13974
|
+
}
|
|
13975
|
+
function toLocalTurnMessage(acc) {
|
|
13976
|
+
const content = [];
|
|
13977
|
+
for (const block of acc.blocks) {
|
|
13978
|
+
if (!block) continue;
|
|
13979
|
+
if (block.type === "tool_use") {
|
|
13980
|
+
let input = {};
|
|
13981
|
+
try {
|
|
13982
|
+
input = block.inputJson ? JSON.parse(block.inputJson) : {};
|
|
13983
|
+
} catch {
|
|
13984
|
+
input = {};
|
|
13985
|
+
}
|
|
13986
|
+
content.push({ type: "tool_use", id: block.id ?? "", name: block.name ?? "", input });
|
|
13987
|
+
} else if (block.text) {
|
|
13988
|
+
content.push({ type: "text", text: block.text });
|
|
13989
|
+
}
|
|
13990
|
+
}
|
|
13991
|
+
return {
|
|
13992
|
+
content,
|
|
13993
|
+
stop_reason: acc.stopReason ?? "end_turn",
|
|
13994
|
+
usage: { input_tokens: acc.inputTokens, output_tokens: acc.outputTokens }
|
|
13995
|
+
};
|
|
13996
|
+
}
|
|
13997
|
+
async function* parseSseLines(body) {
|
|
13998
|
+
const reader = body.getReader();
|
|
13999
|
+
const decoder = new TextDecoder();
|
|
14000
|
+
let buffer = "";
|
|
14001
|
+
try {
|
|
14002
|
+
for (; ; ) {
|
|
14003
|
+
const { done, value } = await reader.read();
|
|
14004
|
+
if (done) break;
|
|
14005
|
+
buffer += decoder.decode(value, { stream: true });
|
|
14006
|
+
let idx;
|
|
14007
|
+
while ((idx = buffer.indexOf("\n")) !== -1) {
|
|
14008
|
+
const line = buffer.slice(0, idx).trim();
|
|
14009
|
+
buffer = buffer.slice(idx + 1);
|
|
14010
|
+
if (!line.startsWith("data:")) continue;
|
|
14011
|
+
const payload = line.slice(5).trim();
|
|
14012
|
+
if (!payload || payload === "[DONE]") continue;
|
|
14013
|
+
try {
|
|
14014
|
+
yield JSON.parse(payload);
|
|
14015
|
+
} catch {
|
|
14016
|
+
}
|
|
14017
|
+
}
|
|
14018
|
+
}
|
|
14019
|
+
} finally {
|
|
14020
|
+
reader.releaseLock();
|
|
14021
|
+
}
|
|
14022
|
+
}
|
|
14023
|
+
function getAnthropicBaseUrl(env = process.env) {
|
|
14024
|
+
return (env.ANTHROPIC_BASE_URL?.trim() || "https://api.anthropic.com").replace(/\/$/, "");
|
|
14025
|
+
}
|
|
14026
|
+
async function streamLocalAnthropicTurn(input) {
|
|
14027
|
+
const { creds, model, payload } = input;
|
|
14028
|
+
const headers = {
|
|
14029
|
+
"Content-Type": "application/json",
|
|
14030
|
+
"anthropic-version": "2023-06-01"
|
|
14031
|
+
};
|
|
14032
|
+
let system = payload.system;
|
|
14033
|
+
if (creds.kind === "api_key") {
|
|
14034
|
+
headers["x-api-key"] = creds.key;
|
|
14035
|
+
} else {
|
|
14036
|
+
headers.Authorization = `Bearer ${creds.token}`;
|
|
14037
|
+
headers["anthropic-beta"] = "oauth-2025-04-20";
|
|
14038
|
+
system = [
|
|
14039
|
+
{ type: "text", text: CLAUDE_CODE_IDENTITY },
|
|
14040
|
+
{ type: "text", text: payload.system }
|
|
14041
|
+
];
|
|
14042
|
+
}
|
|
14043
|
+
const res = await fetch(`${getAnthropicBaseUrl()}/v1/messages`, {
|
|
14044
|
+
method: "POST",
|
|
14045
|
+
headers,
|
|
14046
|
+
signal: input.signal,
|
|
14047
|
+
body: JSON.stringify({
|
|
14048
|
+
model,
|
|
14049
|
+
max_tokens: payload.maxTokens,
|
|
14050
|
+
system,
|
|
14051
|
+
messages: payload.messages,
|
|
14052
|
+
...Array.isArray(payload.tools) && payload.tools.length > 0 ? { tools: payload.tools } : {},
|
|
14053
|
+
...payload.temperature !== void 0 ? { temperature: payload.temperature } : {},
|
|
14054
|
+
stream: true
|
|
14055
|
+
})
|
|
14056
|
+
});
|
|
14057
|
+
if (!res.ok || !res.body) {
|
|
14058
|
+
const detail = await res.text().catch(() => "");
|
|
14059
|
+
throw new Error(`Anthropic stream failed: HTTP ${res.status}${detail ? ` \u2014 ${detail.slice(0, 300)}` : ""}`);
|
|
14060
|
+
}
|
|
14061
|
+
const acc = createAnthropicAccumulator();
|
|
14062
|
+
for await (const event of parseSseLines(res.body)) {
|
|
14063
|
+
const delta = foldAnthropicEvent(acc, event);
|
|
14064
|
+
if (delta) input.onDelta(delta);
|
|
14065
|
+
}
|
|
14066
|
+
return toLocalTurnMessage(acc);
|
|
14067
|
+
}
|
|
14068
|
+
|
|
14069
|
+
// src/chat/delta-batcher.ts
|
|
14070
|
+
var DELTA_FLUSH_FLOOR_MS = 150;
|
|
14071
|
+
var DeltaBatcher = class {
|
|
14072
|
+
constructor(flushFn, floorMs = DELTA_FLUSH_FLOOR_MS) {
|
|
14073
|
+
this.flushFn = flushFn;
|
|
14074
|
+
this.floorMs = floorMs;
|
|
14075
|
+
}
|
|
14076
|
+
buffer = "";
|
|
14077
|
+
timer = null;
|
|
14078
|
+
closed = false;
|
|
14079
|
+
push(text) {
|
|
14080
|
+
if (this.closed || !text) return;
|
|
14081
|
+
this.buffer += text;
|
|
14082
|
+
if (!this.timer) {
|
|
14083
|
+
this.timer = setTimeout(() => {
|
|
14084
|
+
this.timer = null;
|
|
14085
|
+
this.flushNow();
|
|
14086
|
+
}, this.floorMs);
|
|
14087
|
+
}
|
|
14088
|
+
}
|
|
14089
|
+
flushNow() {
|
|
14090
|
+
if (!this.buffer) return;
|
|
14091
|
+
const text = this.buffer;
|
|
14092
|
+
this.buffer = "";
|
|
14093
|
+
this.flushFn(text);
|
|
14094
|
+
}
|
|
14095
|
+
/** Flush any remainder and stop the timer. Idempotent. */
|
|
14096
|
+
close() {
|
|
14097
|
+
if (this.closed) return;
|
|
14098
|
+
this.closed = true;
|
|
14099
|
+
if (this.timer) {
|
|
14100
|
+
clearTimeout(this.timer);
|
|
14101
|
+
this.timer = null;
|
|
14102
|
+
}
|
|
14103
|
+
this.flushNow();
|
|
14104
|
+
}
|
|
14105
|
+
};
|
|
14106
|
+
|
|
14107
|
+
// src/chat/chat-claim-loop.ts
|
|
14108
|
+
var CLAIM_WAIT_MS = 25e3;
|
|
14109
|
+
var CLAIM_FETCH_TIMEOUT_MS = 32e3;
|
|
14110
|
+
var ERROR_BACKOFF_MS = 5e3;
|
|
14111
|
+
var AUTH_BACKOFF_MS = 6e4;
|
|
14112
|
+
var DEFAULT_CHAT_MODEL = "claude-sonnet-4-6";
|
|
14113
|
+
function resolveChatBridgeTarget(env = process.env) {
|
|
14114
|
+
const bridgeUrl = env.KYNVER_RUNTIME_CHAT_BRIDGE_URL?.trim();
|
|
14115
|
+
if (!bridgeUrl) return null;
|
|
14116
|
+
const config = loadUserConfig();
|
|
14117
|
+
const agentOsId = config.agentOsId?.trim();
|
|
14118
|
+
const apiKey = loadApiKey();
|
|
14119
|
+
if (!agentOsId || !apiKey) return null;
|
|
14120
|
+
return {
|
|
14121
|
+
bridgeUrl: bridgeUrl.replace(/\/$/, ""),
|
|
14122
|
+
agentOsId,
|
|
14123
|
+
apiKey,
|
|
14124
|
+
boxId: os11.hostname(),
|
|
14125
|
+
model: env.KYNVER_CHAT_MODEL?.trim() || config.defaultModel?.trim() || DEFAULT_CHAT_MODEL
|
|
14126
|
+
};
|
|
14127
|
+
}
|
|
14128
|
+
async function claimOnce(target, shouldStop) {
|
|
14129
|
+
const controller = new AbortController();
|
|
14130
|
+
const timer = setTimeout(() => controller.abort(), CLAIM_FETCH_TIMEOUT_MS);
|
|
14131
|
+
const stopPoll = setInterval(() => {
|
|
14132
|
+
if (shouldStop()) controller.abort();
|
|
14133
|
+
}, 500);
|
|
14134
|
+
try {
|
|
14135
|
+
const res = await fetch(`${target.bridgeUrl}/runtime-chat/claim`, {
|
|
14136
|
+
method: "POST",
|
|
14137
|
+
headers: {
|
|
14138
|
+
"Content-Type": "application/json",
|
|
14139
|
+
Authorization: `Bearer ${target.apiKey}`
|
|
14140
|
+
},
|
|
14141
|
+
body: JSON.stringify({
|
|
14142
|
+
agentOsId: target.agentOsId,
|
|
14143
|
+
boxId: target.boxId,
|
|
14144
|
+
waitMs: CLAIM_WAIT_MS
|
|
14145
|
+
}),
|
|
14146
|
+
signal: controller.signal
|
|
14147
|
+
});
|
|
14148
|
+
if (res.status === 204) return null;
|
|
14149
|
+
if (res.status === 401 || res.status === 403) return "auth_error";
|
|
14150
|
+
if (!res.ok) throw new Error(`claim failed: HTTP ${res.status}`);
|
|
14151
|
+
const body = await res.json().catch(() => null);
|
|
14152
|
+
return body?.turn ?? null;
|
|
14153
|
+
} catch (err) {
|
|
14154
|
+
if (shouldStop()) return null;
|
|
14155
|
+
throw err;
|
|
14156
|
+
} finally {
|
|
14157
|
+
clearTimeout(timer);
|
|
14158
|
+
clearInterval(stopPoll);
|
|
14159
|
+
}
|
|
14160
|
+
}
|
|
14161
|
+
async function pushEvents(target, turnId, events) {
|
|
14162
|
+
const res = await fetch(`${target.bridgeUrl}/runtime-chat/turns/${encodeURIComponent(turnId)}/events`, {
|
|
14163
|
+
method: "POST",
|
|
14164
|
+
headers: {
|
|
14165
|
+
"Content-Type": "application/json",
|
|
14166
|
+
Authorization: `Bearer ${target.apiKey}`
|
|
14167
|
+
},
|
|
14168
|
+
body: JSON.stringify({ agentOsId: target.agentOsId, events })
|
|
14169
|
+
});
|
|
14170
|
+
return res.ok;
|
|
14171
|
+
}
|
|
14172
|
+
async function executeChatTurn(target, turn) {
|
|
14173
|
+
let seq = 0;
|
|
14174
|
+
let turnClosed = false;
|
|
14175
|
+
const abort = new AbortController();
|
|
14176
|
+
const sendBatch = (text) => {
|
|
14177
|
+
if (turnClosed) return;
|
|
14178
|
+
void pushEvents(target, turn.turnId, [{ seq: seq++, kind: "delta", text }]).then((ok) => {
|
|
14179
|
+
if (!ok) {
|
|
14180
|
+
turnClosed = true;
|
|
14181
|
+
abort.abort();
|
|
14182
|
+
}
|
|
14183
|
+
});
|
|
14184
|
+
};
|
|
14185
|
+
const batcher = new DeltaBatcher(sendBatch);
|
|
14186
|
+
const creds = resolveLocalAnthropicCredentials();
|
|
14187
|
+
if (!creds) {
|
|
14188
|
+
await pushEvents(target, turn.turnId, [
|
|
14189
|
+
{ seq: 0, kind: "error", error: "no_local_credentials" }
|
|
14190
|
+
]).catch(() => {
|
|
14191
|
+
});
|
|
14192
|
+
return;
|
|
14193
|
+
}
|
|
14194
|
+
try {
|
|
14195
|
+
const message = await streamLocalAnthropicTurn({
|
|
14196
|
+
creds,
|
|
14197
|
+
model: target.model,
|
|
14198
|
+
payload: turn.payload,
|
|
14199
|
+
onDelta: (text) => batcher.push(text),
|
|
14200
|
+
signal: abort.signal
|
|
14201
|
+
});
|
|
14202
|
+
batcher.close();
|
|
14203
|
+
if (!turnClosed) {
|
|
14204
|
+
await pushEvents(target, turn.turnId, [{ seq: seq++, kind: "final", message }]);
|
|
14205
|
+
}
|
|
14206
|
+
console.error(JSON.stringify({ event: "chat_turn_done", turnId: turn.turnId, model: target.model }));
|
|
14207
|
+
} catch (err) {
|
|
14208
|
+
batcher.close();
|
|
14209
|
+
if (!turnClosed) {
|
|
14210
|
+
await pushEvents(target, turn.turnId, [
|
|
14211
|
+
{ seq: seq++, kind: "error", error: err instanceof Error ? err.message : String(err) }
|
|
14212
|
+
]).catch(() => {
|
|
14213
|
+
});
|
|
14214
|
+
}
|
|
14215
|
+
console.error(
|
|
14216
|
+
JSON.stringify({
|
|
14217
|
+
event: "chat_turn_error",
|
|
14218
|
+
turnId: turn.turnId,
|
|
14219
|
+
error: err instanceof Error ? err.message : String(err)
|
|
14220
|
+
})
|
|
14221
|
+
);
|
|
14222
|
+
}
|
|
14223
|
+
}
|
|
14224
|
+
async function sleep2(ms, shouldStop) {
|
|
14225
|
+
const step = 500;
|
|
14226
|
+
for (let waited = 0; waited < ms && !shouldStop(); waited += step) {
|
|
14227
|
+
await new Promise((resolve2) => setTimeout(resolve2, Math.min(step, ms - waited)));
|
|
14228
|
+
}
|
|
14229
|
+
}
|
|
14230
|
+
async function runChatClaimLoop(opts) {
|
|
14231
|
+
const target = resolveChatBridgeTarget();
|
|
14232
|
+
if (!target) return;
|
|
14233
|
+
console.error(
|
|
14234
|
+
JSON.stringify({
|
|
14235
|
+
event: "chat_claim_loop_start",
|
|
14236
|
+
bridgeUrl: target.bridgeUrl,
|
|
14237
|
+
agentOsId: target.agentOsId,
|
|
14238
|
+
boxId: target.boxId,
|
|
14239
|
+
model: target.model
|
|
14240
|
+
})
|
|
14241
|
+
);
|
|
14242
|
+
while (!opts.shouldStop()) {
|
|
14243
|
+
try {
|
|
14244
|
+
const claimed = await claimOnce(target, opts.shouldStop);
|
|
14245
|
+
if (claimed === "auth_error") {
|
|
14246
|
+
console.error(JSON.stringify({ event: "chat_claim_auth_error" }));
|
|
14247
|
+
await sleep2(AUTH_BACKOFF_MS, opts.shouldStop);
|
|
14248
|
+
continue;
|
|
14249
|
+
}
|
|
14250
|
+
if (claimed) await executeChatTurn(target, claimed);
|
|
14251
|
+
} catch (err) {
|
|
14252
|
+
console.error(
|
|
14253
|
+
JSON.stringify({
|
|
14254
|
+
event: "chat_claim_error",
|
|
14255
|
+
error: err instanceof Error ? err.message : String(err)
|
|
14256
|
+
})
|
|
14257
|
+
);
|
|
14258
|
+
await sleep2(ERROR_BACKOFF_MS, opts.shouldStop);
|
|
14259
|
+
}
|
|
14260
|
+
}
|
|
14261
|
+
console.error(JSON.stringify({ event: "chat_claim_loop_stop" }));
|
|
14262
|
+
}
|
|
14263
|
+
|
|
13895
14264
|
// src/daemon.ts
|
|
13896
14265
|
var DEFAULT_INTERVAL_MS = 6e4;
|
|
13897
14266
|
var IDLE_INTERVAL_MS = 5 * 6e4;
|
|
@@ -13944,6 +14313,14 @@ async function runDaemon(args) {
|
|
|
13944
14313
|
})
|
|
13945
14314
|
);
|
|
13946
14315
|
const cronEnv = resolveKynverCronEnv();
|
|
14316
|
+
const chatLoop = runChatClaimLoop({ shouldStop: () => stopping }).catch((err) => {
|
|
14317
|
+
console.error(
|
|
14318
|
+
JSON.stringify({
|
|
14319
|
+
event: "chat_claim_loop_crashed",
|
|
14320
|
+
error: err instanceof Error ? err.message : String(err)
|
|
14321
|
+
})
|
|
14322
|
+
);
|
|
14323
|
+
});
|
|
13947
14324
|
while (!stopping) {
|
|
13948
14325
|
try {
|
|
13949
14326
|
writeDaemonHeartbeat({ agentOsId, runId });
|
|
@@ -13970,40 +14347,143 @@ async function runDaemon(args) {
|
|
|
13970
14347
|
await awaitDaemonBackoff(intervalMs, () => stopping);
|
|
13971
14348
|
}
|
|
13972
14349
|
}
|
|
14350
|
+
await chatLoop;
|
|
13973
14351
|
console.error(JSON.stringify({ event: "daemon_stop", runId, agentOsId }));
|
|
13974
14352
|
}
|
|
13975
14353
|
|
|
13976
|
-
// src/
|
|
13977
|
-
|
|
13978
|
-
|
|
13979
|
-
|
|
13980
|
-
|
|
13981
|
-
var STARTUP_GRACE_MS = 2 * 6e4;
|
|
13982
|
-
var KILL_GRACE_MS = 1e4;
|
|
13983
|
-
var BACKOFF_BASE_MS = 5e3;
|
|
13984
|
-
var BACKOFF_CAP_MS = 5 * 6e4;
|
|
13985
|
-
var HEALTHY_RESET_MS = 30 * 6e4;
|
|
13986
|
-
var POLL_MS = 5e3;
|
|
13987
|
-
function resolveKeeperStallMs(flag, env = process.env) {
|
|
13988
|
-
const fromFlag = typeof flag === "string" ? Number.parseInt(flag, 10) : NaN;
|
|
13989
|
-
if (Number.isFinite(fromFlag) && fromFlag > 0) return fromFlag;
|
|
13990
|
-
const fromEnv = Number.parseInt(env.KYNVER_DAEMON_STALL_MS ?? "", 10);
|
|
13991
|
-
if (Number.isFinite(fromEnv) && fromEnv > 0) return fromEnv;
|
|
13992
|
-
return DEFAULT_STALL_MS;
|
|
14354
|
+
// src/start.ts
|
|
14355
|
+
init_run_store();
|
|
14356
|
+
function resolveStartRunId(runs, repo) {
|
|
14357
|
+
const candidates = runs.filter((r) => r.repo === repo && !TERMINAL_RUN_STATUSES.has(r.status)).sort((a, b) => (b.createdAt ?? "").localeCompare(a.createdAt ?? ""));
|
|
14358
|
+
return candidates[0]?.id ?? null;
|
|
13993
14359
|
}
|
|
13994
|
-
function
|
|
13995
|
-
|
|
13996
|
-
|
|
13997
|
-
if (
|
|
13998
|
-
|
|
13999
|
-
|
|
14000
|
-
|
|
14360
|
+
async function runStart(args) {
|
|
14361
|
+
assertNativeDaemonAllowed();
|
|
14362
|
+
let config = loadUserConfig();
|
|
14363
|
+
if (!loadApiKey() || !config.agentOsId?.trim()) {
|
|
14364
|
+
console.log(" This machine isn't linked yet \u2014 running bootstrap first.");
|
|
14365
|
+
await runBootstrap(args);
|
|
14366
|
+
config = loadUserConfig();
|
|
14001
14367
|
}
|
|
14002
|
-
|
|
14368
|
+
const agentOsId = (typeof args.agentOsId === "string" ? args.agentOsId.trim() : "") || config.agentOsId?.trim() || "";
|
|
14369
|
+
if (!agentOsId) {
|
|
14370
|
+
console.error("No AgentOS workspace configured \u2014 run `kynver bootstrap` (or pass --agent-os-id).");
|
|
14371
|
+
process.exit(1);
|
|
14372
|
+
}
|
|
14373
|
+
const repo = (typeof args.repo === "string" ? args.repo.trim() : "") || config.defaultRepo?.trim() || resolveDefaultRepo()?.repo || "";
|
|
14374
|
+
if (!repo) {
|
|
14375
|
+
console.error("No repo configured \u2014 pass --repo /path/to/repo or run `kynver setup --discover-repo`.");
|
|
14376
|
+
process.exit(1);
|
|
14377
|
+
}
|
|
14378
|
+
let runId = typeof args.run === "string" && args.run.trim() ? args.run.trim() : "";
|
|
14379
|
+
if (!runId) {
|
|
14380
|
+
runId = resolveStartRunId(listRunRecords(), repo) ?? "";
|
|
14381
|
+
if (runId) {
|
|
14382
|
+
console.log(` Reusing run ${runId} for ${repo}.`);
|
|
14383
|
+
} else {
|
|
14384
|
+
runId = createRun({ ...args, repo, name: "agent" }).runId;
|
|
14385
|
+
}
|
|
14386
|
+
}
|
|
14387
|
+
console.log("");
|
|
14388
|
+
console.log(` ${os12.hostname()} \u2014 agent coming online`);
|
|
14389
|
+
console.log(` workspace: ${agentOsId}`);
|
|
14390
|
+
console.log(` repo: ${repo}`);
|
|
14391
|
+
console.log(` run: ${runId}`);
|
|
14392
|
+
console.log(" Ctrl-C stops the agent. (Advanced control: `kynver daemon --help`.)");
|
|
14393
|
+
console.log("");
|
|
14394
|
+
await runDaemon({ ...args, run: runId, agentOsId });
|
|
14003
14395
|
}
|
|
14004
|
-
|
|
14005
|
-
|
|
14006
|
-
|
|
14396
|
+
|
|
14397
|
+
// src/cli.ts
|
|
14398
|
+
init_run_store();
|
|
14399
|
+
|
|
14400
|
+
// src/discard-disposable.ts
|
|
14401
|
+
init_run_store();
|
|
14402
|
+
init_status();
|
|
14403
|
+
import { existsSync as existsSync43, rmSync as rmSync4 } from "node:fs";
|
|
14404
|
+
import path61 from "node:path";
|
|
14405
|
+
function normalizeRelativePath2(value) {
|
|
14406
|
+
const normalized = value.replace(/\\/g, "/").replace(/^\.\//, "").trim();
|
|
14407
|
+
if (!normalized || normalized.startsWith("/") || normalized.includes("..")) {
|
|
14408
|
+
throw new Error(`unsafe path: ${value}`);
|
|
14409
|
+
}
|
|
14410
|
+
return normalized;
|
|
14411
|
+
}
|
|
14412
|
+
function parsePathsArg(raw) {
|
|
14413
|
+
if (typeof raw !== "string" || !raw.trim()) return [];
|
|
14414
|
+
return raw.split(",").map((p) => p.trim()).filter(Boolean);
|
|
14415
|
+
}
|
|
14416
|
+
function discardDisposableArtifacts(args) {
|
|
14417
|
+
const { runId, workerName } = resolveWorkerTargetArgs(args);
|
|
14418
|
+
const worker = loadWorker(runId, workerName);
|
|
14419
|
+
const paths = [
|
|
14420
|
+
...parsePathsArg(args.path),
|
|
14421
|
+
...Array.isArray(args.paths) ? args.paths : []
|
|
14422
|
+
];
|
|
14423
|
+
if (paths.length === 0) {
|
|
14424
|
+
return { ok: false, removed: [], reason: "requires at least one --path" };
|
|
14425
|
+
}
|
|
14426
|
+
const worktreeRoot = path61.resolve(worker.worktreePath);
|
|
14427
|
+
const removed = [];
|
|
14428
|
+
for (const raw of paths) {
|
|
14429
|
+
const rel = normalizeRelativePath2(raw);
|
|
14430
|
+
const abs = path61.resolve(worktreeRoot, rel);
|
|
14431
|
+
if (!abs.startsWith(worktreeRoot + path61.sep) && abs !== worktreeRoot) {
|
|
14432
|
+
return { ok: false, removed, reason: `path escapes worktree: ${raw}` };
|
|
14433
|
+
}
|
|
14434
|
+
if (!existsSync43(abs)) {
|
|
14435
|
+
return { ok: false, removed, reason: `path not found: ${raw}` };
|
|
14436
|
+
}
|
|
14437
|
+
rmSync4(abs, { recursive: true, force: true });
|
|
14438
|
+
removed.push(rel);
|
|
14439
|
+
}
|
|
14440
|
+
const prior = Array.isArray(worker.disposableArtifactsRemoved) ? worker.disposableArtifactsRemoved.filter((p) => typeof p === "string") : [];
|
|
14441
|
+
worker.disposableArtifactsRemoved = [.../* @__PURE__ */ new Set([...prior, ...removed])];
|
|
14442
|
+
saveWorker(worker.runId, worker);
|
|
14443
|
+
const status = computeWorkerStatus(worker);
|
|
14444
|
+
return {
|
|
14445
|
+
ok: true,
|
|
14446
|
+
removed,
|
|
14447
|
+
...status.changedFiles.length ? { reason: "worktree still has other changes" } : {}
|
|
14448
|
+
};
|
|
14449
|
+
}
|
|
14450
|
+
function discardDisposableCli(args) {
|
|
14451
|
+
const result = discardDisposableArtifacts(args);
|
|
14452
|
+
console.log(JSON.stringify(result, null, 2));
|
|
14453
|
+
if (!result.ok) process.exit(1);
|
|
14454
|
+
}
|
|
14455
|
+
|
|
14456
|
+
// src/daemon-keeper.ts
|
|
14457
|
+
init_config();
|
|
14458
|
+
import { spawn as spawn6 } from "node:child_process";
|
|
14459
|
+
init_util();
|
|
14460
|
+
var DEFAULT_STALL_MS = 15 * 6e4;
|
|
14461
|
+
var STARTUP_GRACE_MS = 2 * 6e4;
|
|
14462
|
+
var KILL_GRACE_MS = 1e4;
|
|
14463
|
+
var BACKOFF_BASE_MS = 5e3;
|
|
14464
|
+
var BACKOFF_CAP_MS = 5 * 6e4;
|
|
14465
|
+
var HEALTHY_RESET_MS = 30 * 6e4;
|
|
14466
|
+
var POLL_MS = 5e3;
|
|
14467
|
+
function resolveKeeperStallMs(flag, env = process.env) {
|
|
14468
|
+
const fromFlag = typeof flag === "string" ? Number.parseInt(flag, 10) : NaN;
|
|
14469
|
+
if (Number.isFinite(fromFlag) && fromFlag > 0) return fromFlag;
|
|
14470
|
+
const fromEnv = Number.parseInt(env.KYNVER_DAEMON_STALL_MS ?? "", 10);
|
|
14471
|
+
if (Number.isFinite(fromEnv) && fromEnv > 0) return fromEnv;
|
|
14472
|
+
return DEFAULT_STALL_MS;
|
|
14473
|
+
}
|
|
14474
|
+
function shouldRunDaemonKeeper(args, env = process.env) {
|
|
14475
|
+
if (args.keeperChild === true || args.keeperChild === "true") return false;
|
|
14476
|
+
if (args.noSupervise === true || args.noSupervise === "true") return false;
|
|
14477
|
+
if (args.supervised === "false") return false;
|
|
14478
|
+
const envFlag5 = (env.KYNVER_DAEMON_SUPERVISED ?? "").trim().toLowerCase();
|
|
14479
|
+
if (envFlag5 === "0" || envFlag5 === "false" || envFlag5 === "no" || envFlag5 === "off") {
|
|
14480
|
+
return false;
|
|
14481
|
+
}
|
|
14482
|
+
return true;
|
|
14483
|
+
}
|
|
14484
|
+
function nextKeeperBackoffMs(consecutiveFailures, base = BACKOFF_BASE_MS, cap = BACKOFF_CAP_MS) {
|
|
14485
|
+
const exp = Math.min(Math.max(consecutiveFailures, 1) - 1, 10);
|
|
14486
|
+
return Math.min(base * 2 ** exp, cap);
|
|
14007
14487
|
}
|
|
14008
14488
|
function keeperRunWasHealthy(startedAtMs, endedAtMs, healthyMs = HEALTHY_RESET_MS) {
|
|
14009
14489
|
return endedAtMs - startedAtMs >= healthyMs;
|
|
@@ -14106,7 +14586,7 @@ async function runDaemonKeeper(args, rawArgv = process.argv.slice(2)) {
|
|
|
14106
14586
|
|
|
14107
14587
|
// src/plan-progress.ts
|
|
14108
14588
|
init_config();
|
|
14109
|
-
import
|
|
14589
|
+
import path65 from "node:path";
|
|
14110
14590
|
|
|
14111
14591
|
// src/bounded-build/constants.ts
|
|
14112
14592
|
var DEFAULT_BUILD_MEM_BUDGET_BYTES = 1536 * 1024 * 1024;
|
|
@@ -14251,20 +14731,20 @@ import {
|
|
|
14251
14731
|
mkdirSync as mkdirSync9,
|
|
14252
14732
|
openSync as openSync7,
|
|
14253
14733
|
readdirSync as readdirSync15,
|
|
14254
|
-
readFileSync as
|
|
14734
|
+
readFileSync as readFileSync19,
|
|
14255
14735
|
unlinkSync as unlinkSync4,
|
|
14256
14736
|
writeFileSync as writeFileSync6
|
|
14257
14737
|
} from "node:fs";
|
|
14258
|
-
import
|
|
14738
|
+
import path63 from "node:path";
|
|
14259
14739
|
|
|
14260
14740
|
// src/heavy-verification/paths.ts
|
|
14261
14741
|
import { mkdirSync as mkdirSync8 } from "node:fs";
|
|
14262
|
-
import
|
|
14742
|
+
import path62 from "node:path";
|
|
14263
14743
|
function resolveHeavyVerificationRoot() {
|
|
14264
|
-
return
|
|
14744
|
+
return path62.join(resolveKynverStateRoot(), "heavy-verification");
|
|
14265
14745
|
}
|
|
14266
14746
|
function heavyVerificationSlotsDir() {
|
|
14267
|
-
return
|
|
14747
|
+
return path62.join(resolveHeavyVerificationRoot(), "slots");
|
|
14268
14748
|
}
|
|
14269
14749
|
function ensureHeavyVerificationDirs() {
|
|
14270
14750
|
const dir = heavyVerificationSlotsDir();
|
|
@@ -14293,12 +14773,12 @@ function indexedSlotId(index) {
|
|
|
14293
14773
|
return `slot-${index}`;
|
|
14294
14774
|
}
|
|
14295
14775
|
function slotFilePath(slotId, slotsDir = heavyVerificationSlotsDir()) {
|
|
14296
|
-
return
|
|
14776
|
+
return path63.join(slotsDir, `${slotId}.json`);
|
|
14297
14777
|
}
|
|
14298
14778
|
function readSlotRecord(filePath) {
|
|
14299
14779
|
if (!existsSync44(filePath)) return null;
|
|
14300
14780
|
try {
|
|
14301
|
-
const parsed = JSON.parse(
|
|
14781
|
+
const parsed = JSON.parse(readFileSync19(filePath, "utf8"));
|
|
14302
14782
|
if (typeof parsed.slotId === "string" && typeof parsed.pid === "number" && typeof parsed.acquiredAt === "string" && typeof parsed.command === "string") {
|
|
14303
14783
|
return parsed;
|
|
14304
14784
|
}
|
|
@@ -14332,7 +14812,7 @@ function reclaimStaleHeavyVerificationSlots(opts = {}) {
|
|
|
14332
14812
|
let reclaimed = 0;
|
|
14333
14813
|
for (const name of readdirSync15(slotsDir)) {
|
|
14334
14814
|
if (!name.endsWith(".json")) continue;
|
|
14335
|
-
const filePath =
|
|
14815
|
+
const filePath = path63.join(slotsDir, name);
|
|
14336
14816
|
const before = existsSync44(filePath);
|
|
14337
14817
|
reclaimStaleSlot(filePath, staleMs);
|
|
14338
14818
|
if (before && !existsSync44(filePath)) reclaimed += 1;
|
|
@@ -14346,7 +14826,7 @@ function listActiveHeavyVerificationSlots(opts = {}) {
|
|
|
14346
14826
|
const active = [];
|
|
14347
14827
|
for (const name of readdirSync15(slotsDir)) {
|
|
14348
14828
|
if (!name.endsWith(".json")) continue;
|
|
14349
|
-
const record3 = readSlotRecord(
|
|
14829
|
+
const record3 = readSlotRecord(path63.join(slotsDir, name));
|
|
14350
14830
|
if (record3 && !slotIsStale(record3, staleMs)) active.push(record3);
|
|
14351
14831
|
}
|
|
14352
14832
|
return active;
|
|
@@ -14467,11 +14947,11 @@ function waitForHeavyVerificationSlot(command, timeoutMs, pollMs = 2e3, opts = {
|
|
|
14467
14947
|
|
|
14468
14948
|
// src/harness-worktree-build-guard.ts
|
|
14469
14949
|
init_paths();
|
|
14470
|
-
import
|
|
14950
|
+
import path64 from "node:path";
|
|
14471
14951
|
function isPathUnderHarnessWorktree(cwd) {
|
|
14472
14952
|
const worktreesDir = harnessWorktreesDir(resolveHarnessRoot());
|
|
14473
|
-
const rel =
|
|
14474
|
-
return rel.length > 0 && !rel.startsWith("..") && !
|
|
14953
|
+
const rel = path64.relative(worktreesDir, path64.resolve(cwd));
|
|
14954
|
+
return rel.length > 0 && !rel.startsWith("..") && !path64.isAbsolute(rel);
|
|
14475
14955
|
}
|
|
14476
14956
|
function assessHarnessWorktreeBuildGuard(cwd) {
|
|
14477
14957
|
if (!isPathUnderHarnessWorktree(cwd)) return { ok: true };
|
|
@@ -14684,7 +15164,7 @@ async function emitPlanProgress(args) {
|
|
|
14684
15164
|
}
|
|
14685
15165
|
function verifyPlanLocal(args) {
|
|
14686
15166
|
const worktree = required(args.worktree ? String(args.worktree) : void 0, "worktree");
|
|
14687
|
-
const cwd =
|
|
15167
|
+
const cwd = path65.resolve(worktree);
|
|
14688
15168
|
const summary = runHarnessVerifyCommands(cwd);
|
|
14689
15169
|
const emitJson = args.json === true || args.json === "true";
|
|
14690
15170
|
const payload = { passed: summary.passed, worktree: cwd, steps: summary.steps };
|
|
@@ -14733,10 +15213,10 @@ async function verifyPlan(args) {
|
|
|
14733
15213
|
}
|
|
14734
15214
|
|
|
14735
15215
|
// src/harness-verify-cli.ts
|
|
14736
|
-
import
|
|
15216
|
+
import path66 from "node:path";
|
|
14737
15217
|
init_util();
|
|
14738
15218
|
function runHarnessVerifyCli(args) {
|
|
14739
|
-
const cwd =
|
|
15219
|
+
const cwd = path66.resolve(required(args.worktree ? String(args.worktree) : void 0, "worktree"));
|
|
14740
15220
|
const emitJson = args.json === true || args.json === "true" || args.emitJson === true || args.emitJson === "true";
|
|
14741
15221
|
const commands = [];
|
|
14742
15222
|
const rawCmd = args.command;
|
|
@@ -14782,7 +15262,7 @@ function runHarnessVerifyCli(args) {
|
|
|
14782
15262
|
|
|
14783
15263
|
// src/plan-persist-cli.ts
|
|
14784
15264
|
init_config();
|
|
14785
|
-
import { readFileSync as
|
|
15265
|
+
import { readFileSync as readFileSync20 } from "node:fs";
|
|
14786
15266
|
init_util();
|
|
14787
15267
|
var OPERATIONS = ["create", "add_version", "update_metadata"];
|
|
14788
15268
|
var FAILURE_KINDS = [
|
|
@@ -14795,7 +15275,7 @@ var FAILURE_KINDS = [
|
|
|
14795
15275
|
function readBodyArg(args) {
|
|
14796
15276
|
const bodyFile = args.bodyFile ? String(args.bodyFile) : void 0;
|
|
14797
15277
|
if (bodyFile) {
|
|
14798
|
-
return { body:
|
|
15278
|
+
return { body: readFileSync20(bodyFile, "utf8"), bodyPathHint: bodyFile };
|
|
14799
15279
|
}
|
|
14800
15280
|
const inline = args.body ? String(args.body) : void 0;
|
|
14801
15281
|
if (inline) return { body: inline };
|
|
@@ -15175,7 +15655,7 @@ ${text.slice(0, 800)}`,
|
|
|
15175
15655
|
}
|
|
15176
15656
|
|
|
15177
15657
|
// src/monitor/monitor.service.ts
|
|
15178
|
-
import
|
|
15658
|
+
import path68 from "node:path";
|
|
15179
15659
|
init_run_store();
|
|
15180
15660
|
init_status();
|
|
15181
15661
|
init_util();
|
|
@@ -15235,10 +15715,10 @@ function classifyWorkerHealth(input) {
|
|
|
15235
15715
|
init_paths();
|
|
15236
15716
|
init_util();
|
|
15237
15717
|
import { existsSync as existsSync45, mkdirSync as mkdirSync10, readdirSync as readdirSync16, unlinkSync as unlinkSync5 } from "node:fs";
|
|
15238
|
-
import
|
|
15718
|
+
import path67 from "node:path";
|
|
15239
15719
|
function monitorsDir() {
|
|
15240
15720
|
const { harnessRoot } = getHarnessPaths();
|
|
15241
|
-
const dir =
|
|
15721
|
+
const dir = path67.join(harnessRoot, "monitors");
|
|
15242
15722
|
mkdirSync10(dir, { recursive: true });
|
|
15243
15723
|
return dir;
|
|
15244
15724
|
}
|
|
@@ -15246,7 +15726,7 @@ function monitorIdFor(runId, workerName) {
|
|
|
15246
15726
|
return workerName ? `${safeSlug(runId)}--${safeSlug(workerName)}` : safeSlug(runId);
|
|
15247
15727
|
}
|
|
15248
15728
|
function monitorPath(monitorId) {
|
|
15249
|
-
return
|
|
15729
|
+
return path67.join(monitorsDir(), `${monitorId}.json`);
|
|
15250
15730
|
}
|
|
15251
15731
|
function loadMonitorSession(monitorId) {
|
|
15252
15732
|
return readJson(monitorPath(monitorId), void 0);
|
|
@@ -15267,7 +15747,7 @@ function listMonitorSessions() {
|
|
|
15267
15747
|
for (const name of readdirSync16(dir)) {
|
|
15268
15748
|
if (!name.endsWith(".json")) continue;
|
|
15269
15749
|
const session = readJson(
|
|
15270
|
-
|
|
15750
|
+
path67.join(dir, name),
|
|
15271
15751
|
void 0
|
|
15272
15752
|
);
|
|
15273
15753
|
if (!session?.monitorId) continue;
|
|
@@ -15360,7 +15840,7 @@ async function fetchTaskLeasesForWorkers(input) {
|
|
|
15360
15840
|
// src/monitor/monitor.service.ts
|
|
15361
15841
|
function workerRecord2(runId, name) {
|
|
15362
15842
|
return readJson(
|
|
15363
|
-
|
|
15843
|
+
path68.join(runDirectory(runId), "workers", safeSlug(name), "worker.json"),
|
|
15364
15844
|
void 0
|
|
15365
15845
|
);
|
|
15366
15846
|
}
|
|
@@ -15570,17 +16050,17 @@ init_util();
|
|
|
15570
16050
|
init_paths();
|
|
15571
16051
|
import { spawn as spawn7 } from "node:child_process";
|
|
15572
16052
|
import { closeSync as closeSync8, existsSync as existsSync46, openSync as openSync8 } from "node:fs";
|
|
15573
|
-
import
|
|
16053
|
+
import path69 from "node:path";
|
|
15574
16054
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
15575
16055
|
function resolveDefaultCliPath2() {
|
|
15576
|
-
return
|
|
16056
|
+
return path69.join(fileURLToPath4(new URL(".", import.meta.url)), "cli.js");
|
|
15577
16057
|
}
|
|
15578
16058
|
function spawnMonitorSidecar(opts) {
|
|
15579
16059
|
const cliPath = opts.cliPath ?? resolveDefaultCliPath2();
|
|
15580
16060
|
if (!existsSync46(cliPath)) return void 0;
|
|
15581
16061
|
const monitorId = monitorIdFor(opts.runId, opts.workerName);
|
|
15582
16062
|
const { harnessRoot } = getHarnessPaths();
|
|
15583
|
-
const logPath =
|
|
16063
|
+
const logPath = path69.join(harnessRoot, "monitors", `${monitorId}.log`);
|
|
15584
16064
|
let logFd;
|
|
15585
16065
|
try {
|
|
15586
16066
|
logFd = openSync8(logPath, "a");
|
|
@@ -15705,7 +16185,7 @@ init_run_store();
|
|
|
15705
16185
|
init_status();
|
|
15706
16186
|
init_util();
|
|
15707
16187
|
init_config();
|
|
15708
|
-
import
|
|
16188
|
+
import path70 from "node:path";
|
|
15709
16189
|
function skip(runId, worker, taskId, agentOsId, leaseOwner, reason) {
|
|
15710
16190
|
return { runId, worker, taskId, agentOsId, leaseOwner, action: "skipped", reason };
|
|
15711
16191
|
}
|
|
@@ -15718,7 +16198,7 @@ async function postRestartUnblock(args) {
|
|
|
15718
16198
|
const errors = [];
|
|
15719
16199
|
for (const run of listRunRecords()) {
|
|
15720
16200
|
for (const name of Object.keys(run.workers ?? {})) {
|
|
15721
|
-
const workerPath =
|
|
16201
|
+
const workerPath = path70.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json");
|
|
15722
16202
|
const worker = readJson(workerPath, void 0);
|
|
15723
16203
|
if (!worker) {
|
|
15724
16204
|
skipped.push(skip(run.id, name, "", "", "", "worker.json missing"));
|
|
@@ -15832,9 +16312,9 @@ async function postRestartUnblockCli(args) {
|
|
|
15832
16312
|
// src/default-repo-cli.ts
|
|
15833
16313
|
init_path_values();
|
|
15834
16314
|
init_config();
|
|
15835
|
-
import
|
|
15836
|
-
import { homedir as
|
|
15837
|
-
var CONFIG_FILE2 =
|
|
16315
|
+
import path71 from "node:path";
|
|
16316
|
+
import { homedir as homedir17 } from "node:os";
|
|
16317
|
+
var CONFIG_FILE2 = path71.join(homedir17(), ".kynver", "config.json");
|
|
15838
16318
|
function ensureDefaultRepo(opts) {
|
|
15839
16319
|
const existing = loadUserConfig();
|
|
15840
16320
|
const resolved = resolveDefaultRepo({ ...opts, config: existing });
|
|
@@ -15915,14 +16395,14 @@ function summarizeResolvedDefaultRepo(resolved) {
|
|
|
15915
16395
|
}
|
|
15916
16396
|
|
|
15917
16397
|
// src/doctor/runtime-takeover.ts
|
|
15918
|
-
import
|
|
16398
|
+
import path73 from "node:path";
|
|
15919
16399
|
init_path_values();
|
|
15920
16400
|
|
|
15921
16401
|
// src/doctor/runtime-takeover.probes.ts
|
|
15922
16402
|
init_config();
|
|
15923
|
-
import { accessSync, constants, existsSync as existsSync47, readFileSync as
|
|
15924
|
-
import { homedir as
|
|
15925
|
-
import
|
|
16403
|
+
import { accessSync, constants, existsSync as existsSync47, readFileSync as readFileSync21 } from "node:fs";
|
|
16404
|
+
import { homedir as homedir18 } from "node:os";
|
|
16405
|
+
import path72 from "node:path";
|
|
15926
16406
|
import { spawnSync as spawnSync13 } from "node:child_process";
|
|
15927
16407
|
init_paths();
|
|
15928
16408
|
function captureCommand(bin, args) {
|
|
@@ -15965,15 +16445,15 @@ var defaultRuntimeTakeoverProbes = {
|
|
|
15965
16445
|
commandOnPath: (bin) => captureCommand(process.platform === "win32" ? "where" : "which", [bin]),
|
|
15966
16446
|
kynverVersion: (bin) => captureCommand(bin, ["--version"]),
|
|
15967
16447
|
loadConfig: () => loadUserConfig(),
|
|
15968
|
-
configFilePath: () =>
|
|
15969
|
-
credentialsFilePath: () =>
|
|
16448
|
+
configFilePath: () => path72.join(homedir18(), ".kynver", "config.json"),
|
|
16449
|
+
credentialsFilePath: () => path72.join(homedir18(), ".kynver", "credentials"),
|
|
15970
16450
|
readCredentials: () => {
|
|
15971
|
-
const credPath =
|
|
16451
|
+
const credPath = path72.join(homedir18(), ".kynver", "credentials");
|
|
15972
16452
|
if (!existsSync47(credPath)) {
|
|
15973
16453
|
return { hasApiKey: false };
|
|
15974
16454
|
}
|
|
15975
16455
|
try {
|
|
15976
|
-
const parsed = JSON.parse(
|
|
16456
|
+
const parsed = JSON.parse(readFileSync21(credPath, "utf8"));
|
|
15977
16457
|
return {
|
|
15978
16458
|
hasApiKey: Boolean(parsed.apiKey?.trim()),
|
|
15979
16459
|
runnerTokenPrefix: tokenPrefix(parsed.runnerToken),
|
|
@@ -16003,7 +16483,7 @@ var defaultRuntimeTakeoverProbes = {
|
|
|
16003
16483
|
})()
|
|
16004
16484
|
}),
|
|
16005
16485
|
harnessRoot: () => resolveHarnessRoot(),
|
|
16006
|
-
legacyOpenclawHarnessRoot: () =>
|
|
16486
|
+
legacyOpenclawHarnessRoot: () => path72.join(homedir18(), ".openclaw", "harness"),
|
|
16007
16487
|
pathExists: (target) => existsSync47(target),
|
|
16008
16488
|
pathWritable: (target) => isWritable(target)
|
|
16009
16489
|
};
|
|
@@ -16410,8 +16890,8 @@ function assessVercelDeployEvidence(probes) {
|
|
|
16410
16890
|
}
|
|
16411
16891
|
function assessHarnessDirs(probes) {
|
|
16412
16892
|
const harnessRoot = probes.harnessRoot();
|
|
16413
|
-
const runsDir =
|
|
16414
|
-
const worktreesDir =
|
|
16893
|
+
const runsDir = path73.join(harnessRoot, "runs");
|
|
16894
|
+
const worktreesDir = path73.join(harnessRoot, "worktrees");
|
|
16415
16895
|
const displayHarnessRoot = redactHomePath(harnessRoot);
|
|
16416
16896
|
const displayRunsDir = redactHomePath(runsDir);
|
|
16417
16897
|
const displayWorktreesDir = redactHomePath(worktreesDir);
|
|
@@ -16680,9 +17160,9 @@ function applySchedulerCutoverAttestation(config) {
|
|
|
16680
17160
|
|
|
16681
17161
|
// src/scheduler-cutover-cli.ts
|
|
16682
17162
|
init_config();
|
|
16683
|
-
import
|
|
16684
|
-
import { homedir as
|
|
16685
|
-
var CONFIG_FILE3 =
|
|
17163
|
+
import path74 from "node:path";
|
|
17164
|
+
import { homedir as homedir19 } from "node:os";
|
|
17165
|
+
var CONFIG_FILE3 = path74.join(homedir19(), ".kynver", "config.json");
|
|
16686
17166
|
function runSchedulerCutoverCheckCli(json = false) {
|
|
16687
17167
|
const config = loadUserConfig();
|
|
16688
17168
|
const report = assessSchedulerCutover(config);
|
|
@@ -16819,9 +17299,674 @@ async function runCronTickCli(args) {
|
|
|
16819
17299
|
);
|
|
16820
17300
|
}
|
|
16821
17301
|
|
|
17302
|
+
// src/cron/cron-install-cli.ts
|
|
17303
|
+
init_config();
|
|
17304
|
+
|
|
17305
|
+
// src/cron/cron-install.ts
|
|
17306
|
+
init_config();
|
|
17307
|
+
init_path_values();
|
|
17308
|
+
init_util();
|
|
17309
|
+
import { existsSync as existsSync51 } from "node:fs";
|
|
17310
|
+
|
|
17311
|
+
// src/cron/cron-id.ts
|
|
17312
|
+
import { createHash as createHash5 } from "node:crypto";
|
|
17313
|
+
function deterministicCronProviderId(spec) {
|
|
17314
|
+
const seed = JSON.stringify({
|
|
17315
|
+
cb: spec.callbackPath,
|
|
17316
|
+
cron: spec.cron ?? null,
|
|
17317
|
+
runAt: spec.runAt ?? null,
|
|
17318
|
+
kind: spec.kind,
|
|
17319
|
+
target: spec.target,
|
|
17320
|
+
dedupeKey: spec.dedupeKey ?? null
|
|
17321
|
+
});
|
|
17322
|
+
const sha = createHash5("sha1").update(seed).digest("hex").slice(0, 16);
|
|
17323
|
+
return `kc-cron:${sha}`;
|
|
17324
|
+
}
|
|
17325
|
+
|
|
17326
|
+
// src/cron/cron-install-plan.ts
|
|
17327
|
+
import { homedir as homedir21 } from "node:os";
|
|
17328
|
+
import path76 from "node:path";
|
|
17329
|
+
|
|
17330
|
+
// src/cron/cron-env-file.ts
|
|
17331
|
+
import { existsSync as existsSync48, mkdirSync as mkdirSync11, readFileSync as readFileSync22, writeFileSync as writeFileSync7 } from "node:fs";
|
|
17332
|
+
import { homedir as homedir20 } from "node:os";
|
|
17333
|
+
import path75 from "node:path";
|
|
17334
|
+
var DEFAULT_KYNVER_ENV_FILE = path75.join(homedir20(), ".kynver", ".env");
|
|
17335
|
+
function parseEnvFile(content) {
|
|
17336
|
+
const map = /* @__PURE__ */ new Map();
|
|
17337
|
+
for (const line of content.split(/\r?\n/)) {
|
|
17338
|
+
const trimmed = line.trim();
|
|
17339
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
17340
|
+
const eq = trimmed.indexOf("=");
|
|
17341
|
+
if (eq <= 0) continue;
|
|
17342
|
+
const key = trimmed.slice(0, eq).trim();
|
|
17343
|
+
let value = trimmed.slice(eq + 1).trim();
|
|
17344
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
17345
|
+
value = value.slice(1, -1);
|
|
17346
|
+
}
|
|
17347
|
+
map.set(key, value);
|
|
17348
|
+
}
|
|
17349
|
+
return map;
|
|
17350
|
+
}
|
|
17351
|
+
function serializeEnvFile(values, header = "# Managed by kynver cron install \u2014 safe to edit; re-run install to merge keys.\n") {
|
|
17352
|
+
const lines = [header.trimEnd(), ""];
|
|
17353
|
+
for (const [key, value] of [...values.entries()].sort(([a], [b]) => a.localeCompare(b))) {
|
|
17354
|
+
const escaped = value.includes(" ") || value.includes("#") || value.includes('"') ? `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"` : value;
|
|
17355
|
+
lines.push(`${key}=${escaped}`);
|
|
17356
|
+
}
|
|
17357
|
+
lines.push("");
|
|
17358
|
+
return lines.join("\n");
|
|
17359
|
+
}
|
|
17360
|
+
function readEnvFile(filePath = DEFAULT_KYNVER_ENV_FILE) {
|
|
17361
|
+
if (!existsSync48(filePath)) return /* @__PURE__ */ new Map();
|
|
17362
|
+
return parseEnvFile(readFileSync22(filePath, "utf8"));
|
|
17363
|
+
}
|
|
17364
|
+
function mergeEnvFile(updates, options = {}) {
|
|
17365
|
+
const filePath = options.filePath ?? DEFAULT_KYNVER_ENV_FILE;
|
|
17366
|
+
const existing = existsSync48(filePath) ? readFileSync22(filePath, "utf8") : "";
|
|
17367
|
+
const map = parseEnvFile(existing);
|
|
17368
|
+
const keysWritten = [];
|
|
17369
|
+
const keysRemoved = [];
|
|
17370
|
+
for (const key of options.removeKeys ?? []) {
|
|
17371
|
+
if (map.delete(key)) keysRemoved.push(key);
|
|
17372
|
+
}
|
|
17373
|
+
for (const [key, value] of Object.entries(updates)) {
|
|
17374
|
+
if (value === void 0) continue;
|
|
17375
|
+
const prev = map.get(key);
|
|
17376
|
+
map.set(key, value);
|
|
17377
|
+
if (prev !== value) keysWritten.push(key);
|
|
17378
|
+
}
|
|
17379
|
+
const prevMap = parseEnvFile(existing);
|
|
17380
|
+
let changed = prevMap.size !== map.size;
|
|
17381
|
+
if (!changed) {
|
|
17382
|
+
for (const [key, value] of map) {
|
|
17383
|
+
if (prevMap.get(key) !== value) {
|
|
17384
|
+
changed = true;
|
|
17385
|
+
break;
|
|
17386
|
+
}
|
|
17387
|
+
}
|
|
17388
|
+
}
|
|
17389
|
+
const nextContent = serializeEnvFile(map);
|
|
17390
|
+
if (changed) {
|
|
17391
|
+
mkdirSync11(path75.dirname(filePath), { recursive: true });
|
|
17392
|
+
writeFileSync7(filePath, nextContent, { mode: 384 });
|
|
17393
|
+
}
|
|
17394
|
+
return { path: filePath, changed, keysWritten, keysRemoved };
|
|
17395
|
+
}
|
|
17396
|
+
|
|
17397
|
+
// src/cron/cron-install-plan.ts
|
|
17398
|
+
var WATCHDOG_DEDUPE_KEY = "watchdog:board-sweep";
|
|
17399
|
+
var DEFAULT_WATCHDOG_CRON = "*/5 * * * *";
|
|
17400
|
+
var VERCEL_KYNVER_CRON_CUTOVER_STEPS = [
|
|
17401
|
+
"Set KYNVER_SCHEDULER_PROVIDER=kynver-cron on the hosted Kynver deployment (Vercel).",
|
|
17402
|
+
"Set KYNVER_CRON_SECRET to the same value written to ~/.kynver/.env by this installer.",
|
|
17403
|
+
"Unset KYNVER_CRON_STORE_PATH on Vercel \u2014 the connected box owns the local store.",
|
|
17404
|
+
"Keep QSTASH_TOKEN only if other non-watchdog schedules still use QStash (analyst/market jobs)."
|
|
17405
|
+
];
|
|
17406
|
+
function buildCronInstallPlan(input) {
|
|
17407
|
+
const storePath = input.storePath?.trim() || defaultKynverCronStorePath();
|
|
17408
|
+
const envFilePath = input.envFilePath?.trim() || DEFAULT_KYNVER_ENV_FILE;
|
|
17409
|
+
const configPath = path76.join(homedir21(), ".kynver", "config.json");
|
|
17410
|
+
const callbackPath = `/api/agent-os/by-id/${input.agentOsId}/scheduler/fire`;
|
|
17411
|
+
const prerequisites = [];
|
|
17412
|
+
if (!input.apiBaseUrl?.trim()) prerequisites.push("apiBaseUrl \u2014 run `kynver setup --api-base-url \u2026`");
|
|
17413
|
+
if (!input.agentOsId?.trim()) prerequisites.push("agentOsId \u2014 run `kynver setup --agent-os-id \u2026`");
|
|
17414
|
+
const envUpdates = {
|
|
17415
|
+
KYNVER_API_URL: input.apiBaseUrl,
|
|
17416
|
+
KYNVER_CRON_SECRET: input.cronSecret,
|
|
17417
|
+
KYNVER_CRON_STORE_PATH: storePath,
|
|
17418
|
+
KYNVER_CRON_TICK_ENABLED: "1"
|
|
17419
|
+
};
|
|
17420
|
+
const envRemovals = ["OPENCLAW_CRON_STORE_PATH", "OPENCLAW_CRON_SECRET", "OPENCLAW_CRON_FIRE_BASE_URL"];
|
|
17421
|
+
const configUpdates = {
|
|
17422
|
+
deploymentSchedulerProvider: "kynver-cron",
|
|
17423
|
+
apiBaseUrl: input.apiBaseUrl,
|
|
17424
|
+
agentOsId: input.agentOsId,
|
|
17425
|
+
...input.defaultDaemonRunId ? { defaultDaemonRunId: input.defaultDaemonRunId } : {}
|
|
17426
|
+
};
|
|
17427
|
+
return {
|
|
17428
|
+
envFilePath,
|
|
17429
|
+
configPath,
|
|
17430
|
+
storePath,
|
|
17431
|
+
envUpdates,
|
|
17432
|
+
envRemovals,
|
|
17433
|
+
configUpdates,
|
|
17434
|
+
deploymentSteps: VERCEL_KYNVER_CRON_CUTOVER_STEPS,
|
|
17435
|
+
prerequisites,
|
|
17436
|
+
systemdSupported: process.platform === "linux",
|
|
17437
|
+
watchdogSpec: {
|
|
17438
|
+
kind: "watchdog",
|
|
17439
|
+
cron: DEFAULT_WATCHDOG_CRON,
|
|
17440
|
+
dedupeKey: WATCHDOG_DEDUPE_KEY,
|
|
17441
|
+
callbackPath
|
|
17442
|
+
}
|
|
17443
|
+
};
|
|
17444
|
+
}
|
|
17445
|
+
|
|
17446
|
+
// src/cron/cron-install-api.ts
|
|
17447
|
+
async function apiFetch(url, apiKey, init = {}) {
|
|
17448
|
+
return fetch(url, {
|
|
17449
|
+
...init,
|
|
17450
|
+
headers: {
|
|
17451
|
+
"Content-Type": "application/json",
|
|
17452
|
+
Authorization: `Bearer ${apiKey}`,
|
|
17453
|
+
...init.headers
|
|
17454
|
+
}
|
|
17455
|
+
});
|
|
17456
|
+
}
|
|
17457
|
+
async function listSchedulerJobs(baseUrl, agentOsId, apiKey, query = {}) {
|
|
17458
|
+
const sp = new URLSearchParams(query);
|
|
17459
|
+
const url = `${baseUrl}/api/agent-os/by-id/${encodeURIComponent(agentOsId)}/scheduler/jobs?${sp}`;
|
|
17460
|
+
const res = await apiFetch(url, apiKey);
|
|
17461
|
+
const text = await res.text();
|
|
17462
|
+
let parsed = null;
|
|
17463
|
+
try {
|
|
17464
|
+
parsed = JSON.parse(text);
|
|
17465
|
+
} catch {
|
|
17466
|
+
parsed = null;
|
|
17467
|
+
}
|
|
17468
|
+
if (!res.ok) {
|
|
17469
|
+
throw new Error(
|
|
17470
|
+
`list scheduler jobs failed (${res.status}): ${parsed?.error ?? text.slice(0, 200)}`
|
|
17471
|
+
);
|
|
17472
|
+
}
|
|
17473
|
+
return parsed?.items ?? [];
|
|
17474
|
+
}
|
|
17475
|
+
async function ensureWatchdogScheduleRemote(baseUrl, agentOsId, apiKey, cron, options = {}) {
|
|
17476
|
+
const url = `${baseUrl}/api/agent-os/by-id/${encodeURIComponent(agentOsId)}/scheduler/watchdog/ensure`;
|
|
17477
|
+
const body = {};
|
|
17478
|
+
if (cron) body.cron = cron;
|
|
17479
|
+
if (options.requireProvider) body.requireProvider = options.requireProvider;
|
|
17480
|
+
const res = await apiFetch(url, apiKey, {
|
|
17481
|
+
method: "POST",
|
|
17482
|
+
body: JSON.stringify(body)
|
|
17483
|
+
});
|
|
17484
|
+
const text = await res.text();
|
|
17485
|
+
let parsed = null;
|
|
17486
|
+
try {
|
|
17487
|
+
parsed = JSON.parse(text);
|
|
17488
|
+
} catch {
|
|
17489
|
+
parsed = null;
|
|
17490
|
+
}
|
|
17491
|
+
if (!res.ok || !parsed?.job) {
|
|
17492
|
+
throw new Error(
|
|
17493
|
+
`ensure watchdog failed (${res.status}): ${parsed?.error ?? text.slice(0, 200)}`
|
|
17494
|
+
);
|
|
17495
|
+
}
|
|
17496
|
+
return {
|
|
17497
|
+
job: parsed.job,
|
|
17498
|
+
route: parsed.route ?? `/api/agent-os/by-id/${agentOsId}/scheduler/fire`,
|
|
17499
|
+
dedupeKey: parsed.dedupeKey ?? WATCHDOG_DEDUPE_KEY,
|
|
17500
|
+
requestedCron: parsed.requestedCron ?? cron ?? "*/5 * * * *",
|
|
17501
|
+
selectedProvider: parsed.selectedProvider ?? parsed.job.provider ?? null
|
|
17502
|
+
};
|
|
17503
|
+
}
|
|
17504
|
+
async function cancelSchedulerJob(baseUrl, agentOsId, jobId, apiKey) {
|
|
17505
|
+
const url = `${baseUrl}/api/agent-os/by-id/${encodeURIComponent(agentOsId)}/scheduler/jobs/${encodeURIComponent(jobId)}/cancel`;
|
|
17506
|
+
const res = await apiFetch(url, apiKey, { method: "POST", body: "{}" });
|
|
17507
|
+
if (!res.ok) {
|
|
17508
|
+
const text = await res.text();
|
|
17509
|
+
throw new Error(`cancel job ${jobId} failed (${res.status}): ${text.slice(0, 200)}`);
|
|
17510
|
+
}
|
|
17511
|
+
}
|
|
17512
|
+
function buildWatchdogCronSpec(agentOsId, cron, dedupeKey = WATCHDOG_DEDUPE_KEY) {
|
|
17513
|
+
return {
|
|
17514
|
+
kind: "watchdog",
|
|
17515
|
+
scheduleKind: "cron",
|
|
17516
|
+
cron,
|
|
17517
|
+
callbackPath: `/api/agent-os/by-id/${agentOsId}/scheduler/fire`,
|
|
17518
|
+
payload: { source: "agent-os.watchdog-schedule", agentOsId },
|
|
17519
|
+
target: { agentOsId },
|
|
17520
|
+
description: "Watchdog board sweep (repair loop)",
|
|
17521
|
+
dedupeKey
|
|
17522
|
+
};
|
|
17523
|
+
}
|
|
17524
|
+
function findQstashWatchdogLeftovers(jobs) {
|
|
17525
|
+
return jobs.filter(
|
|
17526
|
+
(job) => job.kind === "watchdog" && job.dedupeKey === WATCHDOG_DEDUPE_KEY && job.provider === "qstash" && job.status !== "cancelled"
|
|
17527
|
+
);
|
|
17528
|
+
}
|
|
17529
|
+
function findKynverCronWatchdog(jobs) {
|
|
17530
|
+
return jobs.find(
|
|
17531
|
+
(job) => job.kind === "watchdog" && job.dedupeKey === WATCHDOG_DEDUPE_KEY && job.provider === "kynver-cron" && job.status !== "cancelled"
|
|
17532
|
+
);
|
|
17533
|
+
}
|
|
17534
|
+
function canCancelQstashWatchdogLeftovers(jobs) {
|
|
17535
|
+
if (findKynverCronWatchdog(jobs)) return { allowed: true };
|
|
17536
|
+
return {
|
|
17537
|
+
allowed: false,
|
|
17538
|
+
reason: "Cannot cancel QStash watchdog until hosted KYNVER_SCHEDULER_PROVIDER=kynver-cron is live and a kynver-cron watchdog row exists. Set Vercel env, redeploy, re-run `kynver cron install`, then `--confirm-qstash-removal`."
|
|
17539
|
+
};
|
|
17540
|
+
}
|
|
17541
|
+
|
|
17542
|
+
// src/cron/cron-install-secrets.ts
|
|
17543
|
+
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
17544
|
+
function generateCronSecret() {
|
|
17545
|
+
return randomBytes2(32).toString("base64url");
|
|
17546
|
+
}
|
|
17547
|
+
function resolveCronSecretForInstall(envFilePath = DEFAULT_KYNVER_ENV_FILE) {
|
|
17548
|
+
const fromProcess = resolveKynverCronSecret();
|
|
17549
|
+
if (fromProcess) {
|
|
17550
|
+
return { secret: fromProcess, generated: false, source: "env" };
|
|
17551
|
+
}
|
|
17552
|
+
const file = readEnvFile(envFilePath);
|
|
17553
|
+
const fromFile = file.get("KYNVER_CRON_SECRET")?.trim();
|
|
17554
|
+
if (fromFile) {
|
|
17555
|
+
return { secret: fromFile, generated: false, source: "env-file" };
|
|
17556
|
+
}
|
|
17557
|
+
return { secret: generateCronSecret(), generated: true, source: "generated" };
|
|
17558
|
+
}
|
|
17559
|
+
|
|
17560
|
+
// src/cron/cron-install-systemd.ts
|
|
17561
|
+
import { existsSync as existsSync49, mkdirSync as mkdirSync12, writeFileSync as writeFileSync8 } from "node:fs";
|
|
17562
|
+
import { homedir as homedir22 } from "node:os";
|
|
17563
|
+
import path77 from "node:path";
|
|
17564
|
+
import { spawnSync as spawnSync14 } from "node:child_process";
|
|
17565
|
+
var KYNVER_CRON_DAEMON_UNIT = "kynver-cron-daemon.service";
|
|
17566
|
+
function defaultSystemdUserUnitDir() {
|
|
17567
|
+
return path77.join(homedir22(), ".config", "systemd", "user");
|
|
17568
|
+
}
|
|
17569
|
+
function renderKynverCronDaemonService(input) {
|
|
17570
|
+
const kynverBin = input.kynverBin?.trim() || "kynver";
|
|
17571
|
+
return [
|
|
17572
|
+
"[Unit]",
|
|
17573
|
+
"Description=Kynver AgentOS daemon (pipeline + cron tick)",
|
|
17574
|
+
"After=network-online.target",
|
|
17575
|
+
"",
|
|
17576
|
+
"[Service]",
|
|
17577
|
+
"Type=simple",
|
|
17578
|
+
`EnvironmentFile=${input.envFilePath}`,
|
|
17579
|
+
`ExecStart=${kynverBin} daemon --run ${input.runId} --agent-os-id ${input.agentOsId} --execute`,
|
|
17580
|
+
"Restart=on-failure",
|
|
17581
|
+
"RestartSec=10",
|
|
17582
|
+
"",
|
|
17583
|
+
"[Install]",
|
|
17584
|
+
"WantedBy=default.target",
|
|
17585
|
+
""
|
|
17586
|
+
].join("\n");
|
|
17587
|
+
}
|
|
17588
|
+
function installSystemdUserDaemon(input, execute) {
|
|
17589
|
+
if (!isSystemdRunAvailable()) {
|
|
17590
|
+
return {
|
|
17591
|
+
supported: false,
|
|
17592
|
+
unitPath: null,
|
|
17593
|
+
written: false,
|
|
17594
|
+
enabled: false,
|
|
17595
|
+
started: false,
|
|
17596
|
+
note: process.platform === "linux" ? "systemd-run not available \u2014 install user service manually or run `kynver daemon` under your supervisor." : "systemd user units are only supported on Linux; use `kynver daemon` manually on macOS/Windows."
|
|
17597
|
+
};
|
|
17598
|
+
}
|
|
17599
|
+
const unitDir = defaultSystemdUserUnitDir();
|
|
17600
|
+
const unitPath = path77.join(unitDir, KYNVER_CRON_DAEMON_UNIT);
|
|
17601
|
+
const content = renderKynverCronDaemonService(input);
|
|
17602
|
+
if (!execute) {
|
|
17603
|
+
return {
|
|
17604
|
+
supported: true,
|
|
17605
|
+
unitPath,
|
|
17606
|
+
written: false,
|
|
17607
|
+
enabled: false,
|
|
17608
|
+
started: false,
|
|
17609
|
+
note: "Dry-run \u2014 pass --execute to write and enable the user unit."
|
|
17610
|
+
};
|
|
17611
|
+
}
|
|
17612
|
+
mkdirSync12(unitDir, { recursive: true });
|
|
17613
|
+
const existed = existsSync49(unitPath);
|
|
17614
|
+
writeFileSync8(unitPath, content, "utf8");
|
|
17615
|
+
const reload = spawnSync14("systemctl", ["--user", "daemon-reload"], { encoding: "utf8" });
|
|
17616
|
+
if (reload.status !== 0) {
|
|
17617
|
+
return {
|
|
17618
|
+
supported: true,
|
|
17619
|
+
unitPath,
|
|
17620
|
+
written: true,
|
|
17621
|
+
enabled: false,
|
|
17622
|
+
started: false,
|
|
17623
|
+
note: `Wrote ${unitPath} but systemctl --user daemon-reload failed: ${reload.stderr || reload.stdout}`
|
|
17624
|
+
};
|
|
17625
|
+
}
|
|
17626
|
+
const enable = spawnSync14("systemctl", ["--user", "enable", "--now", KYNVER_CRON_DAEMON_UNIT], {
|
|
17627
|
+
encoding: "utf8"
|
|
17628
|
+
});
|
|
17629
|
+
return {
|
|
17630
|
+
supported: true,
|
|
17631
|
+
unitPath,
|
|
17632
|
+
written: !existed || true,
|
|
17633
|
+
enabled: enable.status === 0,
|
|
17634
|
+
started: enable.status === 0,
|
|
17635
|
+
note: enable.status === 0 ? `Enabled and started ${KYNVER_CRON_DAEMON_UNIT}` : `Wrote ${unitPath} but enable failed: ${enable.stderr || enable.stdout}`
|
|
17636
|
+
};
|
|
17637
|
+
}
|
|
17638
|
+
|
|
17639
|
+
// src/cron/cron-install-verify.ts
|
|
17640
|
+
import { existsSync as existsSync50 } from "node:fs";
|
|
17641
|
+
async function verifyCronInstall(input) {
|
|
17642
|
+
const envFilePath = input.envFilePath ?? DEFAULT_KYNVER_ENV_FILE;
|
|
17643
|
+
const checks = [];
|
|
17644
|
+
const env = resolveKynverCronEnv();
|
|
17645
|
+
const status = await buildKynverCronStatusReport(env);
|
|
17646
|
+
const fileEnv = readEnvFile(envFilePath);
|
|
17647
|
+
checks.push({
|
|
17648
|
+
id: "config_agent_os",
|
|
17649
|
+
ok: Boolean(input.config.agentOsId?.trim()),
|
|
17650
|
+
summary: input.config.agentOsId ? `agentOsId configured (${input.config.agentOsId})` : "agentOsId missing in ~/.kynver/config.json",
|
|
17651
|
+
remediation: "Run `kynver setup --agent-os-id <id>`."
|
|
17652
|
+
});
|
|
17653
|
+
checks.push({
|
|
17654
|
+
id: "config_api_base",
|
|
17655
|
+
ok: Boolean(input.config.apiBaseUrl?.trim() || env.fireBaseUrl),
|
|
17656
|
+
summary: env.fireBaseUrl ? `fire base URL resolved (${env.fireBaseUrl})` : "KYNVER_API_URL / apiBaseUrl not configured",
|
|
17657
|
+
remediation: "Run `kynver setup --api-base-url https://\u2026`."
|
|
17658
|
+
});
|
|
17659
|
+
checks.push({
|
|
17660
|
+
id: "deployment_provider",
|
|
17661
|
+
ok: input.config.deploymentSchedulerProvider === "kynver-cron",
|
|
17662
|
+
summary: `deploymentSchedulerProvider=${input.config.deploymentSchedulerProvider ?? "(unset)"}`,
|
|
17663
|
+
remediation: 'Run `kynver cron install` or set deploymentSchedulerProvider to "kynver-cron".'
|
|
17664
|
+
});
|
|
17665
|
+
checks.push({
|
|
17666
|
+
id: "cron_secret",
|
|
17667
|
+
ok: Boolean(env.secret),
|
|
17668
|
+
summary: env.secret ? "KYNVER_CRON_SECRET present" : "KYNVER_CRON_SECRET missing",
|
|
17669
|
+
remediation: "Run `kynver cron install` to generate and persist the shared secret."
|
|
17670
|
+
});
|
|
17671
|
+
checks.push({
|
|
17672
|
+
id: "env_file",
|
|
17673
|
+
ok: existsSync50(envFilePath) && Boolean(fileEnv.get("KYNVER_CRON_SECRET")),
|
|
17674
|
+
summary: existsSync50(envFilePath) ? `~/.kynver/.env present (${fileEnv.size} keys)` : "~/.kynver/.env missing",
|
|
17675
|
+
remediation: "Run `kynver cron install` to write ~/.kynver/.env."
|
|
17676
|
+
});
|
|
17677
|
+
checks.push({
|
|
17678
|
+
id: "cron_store",
|
|
17679
|
+
ok: existsSync50(env.storePath),
|
|
17680
|
+
summary: existsSync50(env.storePath) ? `cron store present (${env.storePath})` : `cron store missing (${env.storePath})`,
|
|
17681
|
+
remediation: "Run `kynver cron install` to initialize the local store."
|
|
17682
|
+
});
|
|
17683
|
+
const jobs = await loadCronJobs(env.storePath).catch(() => []);
|
|
17684
|
+
const watchdog = jobs.find((j) => j.spec.dedupeKey === WATCHDOG_DEDUPE_KEY);
|
|
17685
|
+
checks.push({
|
|
17686
|
+
id: "watchdog_local",
|
|
17687
|
+
ok: Boolean(watchdog),
|
|
17688
|
+
summary: watchdog ? `watchdog entry in local store (${watchdog.providerScheduleId})` : "watchdog entry not mirrored in local cron store",
|
|
17689
|
+
remediation: "Run `kynver cron install` (with API access) to register and mirror watchdog."
|
|
17690
|
+
});
|
|
17691
|
+
checks.push({
|
|
17692
|
+
id: "daemon_primary",
|
|
17693
|
+
ok: status.daemonPrimary,
|
|
17694
|
+
summary: status.daemonPrimary ? "cron tick enabled with credentials (daemon-primary)" : `cron primary=${status.primary}; tickEnabled=${status.env.tickEnabled}`,
|
|
17695
|
+
remediation: "Ensure KYNVER_CRON_TICK_ENABLED=1 and run `kynver daemon` (or enable the systemd user unit)."
|
|
17696
|
+
});
|
|
17697
|
+
const legacyOpenclaw = fileEnv.has("OPENCLAW_CRON_STORE_PATH") || fileEnv.has("OPENCLAW_CRON_SECRET") || fileEnv.has("OPENCLAW_CRON_FIRE_BASE_URL");
|
|
17698
|
+
checks.push({
|
|
17699
|
+
id: "legacy_openclaw_env",
|
|
17700
|
+
ok: !legacyOpenclaw,
|
|
17701
|
+
summary: legacyOpenclaw ? "legacy OPENCLAW_CRON_* keys still present in ~/.kynver/.env" : "no legacy OPENCLAW_CRON_* keys in env file",
|
|
17702
|
+
remediation: "Re-run `kynver cron install` to retire legacy aliases."
|
|
17703
|
+
});
|
|
17704
|
+
const ok = checks.every((c) => c.ok);
|
|
17705
|
+
return { ok, checks };
|
|
17706
|
+
}
|
|
17707
|
+
|
|
17708
|
+
// src/cron/cron-install.ts
|
|
17709
|
+
init_run_store();
|
|
17710
|
+
function resolveDaemonRunId(config, explicit) {
|
|
17711
|
+
if (explicit?.trim()) return explicit.trim();
|
|
17712
|
+
if (config.defaultDaemonRunId?.trim()) return config.defaultDaemonRunId.trim();
|
|
17713
|
+
const runsDir = getPaths().runsDir;
|
|
17714
|
+
if (!existsSync51(runsDir)) return null;
|
|
17715
|
+
const runs = listRunRecords().sort(
|
|
17716
|
+
(a, b) => Date.parse(b.createdAt) - Date.parse(a.createdAt)
|
|
17717
|
+
);
|
|
17718
|
+
return runs[0]?.id ?? null;
|
|
17719
|
+
}
|
|
17720
|
+
async function runCronInstall(opts = {}) {
|
|
17721
|
+
const execute = opts.execute !== false;
|
|
17722
|
+
const existing = loadUserConfig();
|
|
17723
|
+
const apiBaseUrl = trimTrailingSlash(
|
|
17724
|
+
opts.apiBaseUrl?.trim() || existing.apiBaseUrl?.trim() || process.env.KYNVER_API_URL?.trim() || ""
|
|
17725
|
+
);
|
|
17726
|
+
const agentOsId = opts.agentOsId?.trim() || existing.agentOsId?.trim() || process.env.KYNVER_AGENT_OS_ID?.trim() || "";
|
|
17727
|
+
const secretResolution = resolveCronSecretForInstall();
|
|
17728
|
+
const plan = buildCronInstallPlan({
|
|
17729
|
+
config: existing,
|
|
17730
|
+
apiBaseUrl,
|
|
17731
|
+
agentOsId,
|
|
17732
|
+
cronSecret: secretResolution.secret,
|
|
17733
|
+
defaultDaemonRunId: opts.runId?.trim() || existing.defaultDaemonRunId,
|
|
17734
|
+
installSystemd: opts.installSystemd === true
|
|
17735
|
+
});
|
|
17736
|
+
const blockers = [...plan.prerequisites];
|
|
17737
|
+
const result = {
|
|
17738
|
+
ok: false,
|
|
17739
|
+
dryRun: !execute,
|
|
17740
|
+
plan,
|
|
17741
|
+
secretGenerated: secretResolution.generated,
|
|
17742
|
+
configPath: displayUserPath(plan.configPath),
|
|
17743
|
+
storeInitialized: false,
|
|
17744
|
+
blockers
|
|
17745
|
+
};
|
|
17746
|
+
if (blockers.length) return result;
|
|
17747
|
+
if (execute) {
|
|
17748
|
+
const envMerge = mergeEnvFile(plan.envUpdates, {
|
|
17749
|
+
filePath: plan.envFilePath,
|
|
17750
|
+
removeKeys: plan.envRemovals
|
|
17751
|
+
});
|
|
17752
|
+
result.envFile = {
|
|
17753
|
+
path: displayUserPath(envMerge.path),
|
|
17754
|
+
changed: envMerge.changed,
|
|
17755
|
+
keysWritten: envMerge.keysWritten
|
|
17756
|
+
};
|
|
17757
|
+
const nextConfig = { ...existing, ...plan.configUpdates };
|
|
17758
|
+
const runId = resolveDaemonRunId(nextConfig, opts.runId);
|
|
17759
|
+
if (runId) nextConfig.defaultDaemonRunId = runId;
|
|
17760
|
+
saveUserConfig(nextConfig);
|
|
17761
|
+
process.env.KYNVER_API_URL = apiBaseUrl;
|
|
17762
|
+
process.env.KYNVER_CRON_SECRET = secretResolution.secret;
|
|
17763
|
+
process.env.KYNVER_CRON_STORE_PATH = plan.storePath;
|
|
17764
|
+
process.env.KYNVER_CRON_TICK_ENABLED = "1";
|
|
17765
|
+
const storeInit = await ensureCronStoreInitialized(plan.storePath);
|
|
17766
|
+
result.storeInitialized = storeInit.created || existsSync51(plan.storePath);
|
|
17767
|
+
const apiKey = loadApiKey();
|
|
17768
|
+
if (apiKey) {
|
|
17769
|
+
try {
|
|
17770
|
+
const token = await fetchRunnerCredential(agentOsId, { baseUrl: apiBaseUrl, apiKey });
|
|
17771
|
+
saveRunnerToken(agentOsId, token);
|
|
17772
|
+
} catch {
|
|
17773
|
+
}
|
|
17774
|
+
}
|
|
17775
|
+
if (!opts.skipWatchdog) {
|
|
17776
|
+
if (!apiKey) {
|
|
17777
|
+
result.watchdog = { error: "KYNVER_API_KEY required to register watchdog on server" };
|
|
17778
|
+
blockers.push("login \u2014 run `kynver login --api-key \u2026` to register watchdog remotely");
|
|
17779
|
+
} else {
|
|
17780
|
+
try {
|
|
17781
|
+
const remote = await ensureWatchdogScheduleRemote(apiBaseUrl, agentOsId, apiKey, void 0, {
|
|
17782
|
+
requireProvider: "kynver-cron"
|
|
17783
|
+
});
|
|
17784
|
+
const spec = buildWatchdogCronSpec(agentOsId, remote.requestedCron, remote.dedupeKey);
|
|
17785
|
+
const entry = {
|
|
17786
|
+
providerScheduleId: deterministicCronProviderId(spec),
|
|
17787
|
+
spec,
|
|
17788
|
+
registeredAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
17789
|
+
paused: false
|
|
17790
|
+
};
|
|
17791
|
+
await saveCronJob(entry, plan.storePath);
|
|
17792
|
+
result.watchdog = {
|
|
17793
|
+
remoteJobId: remote.job.id,
|
|
17794
|
+
provider: remote.selectedProvider ?? remote.job.provider,
|
|
17795
|
+
localProviderScheduleId: entry.providerScheduleId
|
|
17796
|
+
};
|
|
17797
|
+
const allJobs = await listSchedulerJobs(apiBaseUrl, agentOsId, apiKey);
|
|
17798
|
+
const leftovers = findQstashWatchdogLeftovers(allJobs);
|
|
17799
|
+
const manualSteps = [
|
|
17800
|
+
"Phase 1 (local): `kynver cron install` writes box env/store and may fail remote watchdog until Vercel cutover.",
|
|
17801
|
+
"Phase 2 (hosted): set KYNVER_SCHEDULER_PROVIDER=kynver-cron + KYNVER_CRON_SECRET on Vercel, redeploy, re-run install.",
|
|
17802
|
+
"Phase 3 (cleanup): `kynver cron install --confirm-qstash-removal` after a kynver-cron watchdog row exists.",
|
|
17803
|
+
"Analyst/market QStash schedules are never touched by this installer."
|
|
17804
|
+
];
|
|
17805
|
+
const removed = [];
|
|
17806
|
+
if (leftovers.length) {
|
|
17807
|
+
if (opts.confirmQstashRemoval) {
|
|
17808
|
+
const cancelGuard = canCancelQstashWatchdogLeftovers(allJobs);
|
|
17809
|
+
if (!cancelGuard.allowed) {
|
|
17810
|
+
blockers.push(cancelGuard.reason);
|
|
17811
|
+
} else {
|
|
17812
|
+
for (const job of leftovers) {
|
|
17813
|
+
await cancelSchedulerJob(apiBaseUrl, agentOsId, job.id, apiKey);
|
|
17814
|
+
removed.push(job.id);
|
|
17815
|
+
}
|
|
17816
|
+
}
|
|
17817
|
+
} else {
|
|
17818
|
+
blockers.push(
|
|
17819
|
+
`${leftovers.length} QStash watchdog job(s) still active \u2014 complete Vercel cutover (KYNVER_SCHEDULER_PROVIDER=kynver-cron + KYNVER_CRON_SECRET), re-run install, then --confirm-qstash-removal`
|
|
17820
|
+
);
|
|
17821
|
+
}
|
|
17822
|
+
}
|
|
17823
|
+
result.qstashWatchdog = {
|
|
17824
|
+
found: leftovers.map((j) => ({ id: j.id, provider: j.provider, status: j.status })),
|
|
17825
|
+
removed,
|
|
17826
|
+
manualSteps
|
|
17827
|
+
};
|
|
17828
|
+
} catch (err) {
|
|
17829
|
+
result.watchdog = { error: err.message };
|
|
17830
|
+
blockers.push(`watchdog registration failed: ${err.message}`);
|
|
17831
|
+
}
|
|
17832
|
+
}
|
|
17833
|
+
}
|
|
17834
|
+
if (opts.installSystemd) {
|
|
17835
|
+
const runId2 = resolveDaemonRunId({ ...existing, ...plan.configUpdates }, opts.runId);
|
|
17836
|
+
if (!runId2) {
|
|
17837
|
+
result.systemd = {
|
|
17838
|
+
supported: plan.systemdSupported,
|
|
17839
|
+
unitPath: null,
|
|
17840
|
+
written: false,
|
|
17841
|
+
enabled: false,
|
|
17842
|
+
started: false,
|
|
17843
|
+
note: "No harness run found \u2014 run `kynver run create` first or pass --run <runId>."
|
|
17844
|
+
};
|
|
17845
|
+
blockers.push("harness run \u2014 create a run before installing systemd unit");
|
|
17846
|
+
} else {
|
|
17847
|
+
try {
|
|
17848
|
+
loadRun(runId2);
|
|
17849
|
+
} catch {
|
|
17850
|
+
blockers.push(`harness run ${runId2} not found`);
|
|
17851
|
+
}
|
|
17852
|
+
result.systemd = installSystemdUserDaemon(
|
|
17853
|
+
{ envFilePath: plan.envFilePath, agentOsId, runId: runId2 },
|
|
17854
|
+
execute
|
|
17855
|
+
);
|
|
17856
|
+
}
|
|
17857
|
+
}
|
|
17858
|
+
if (!opts.skipTestFire && blockers.length === 0) {
|
|
17859
|
+
const tick = await runKynverCronTick({ agentOsIdFilter: agentOsId });
|
|
17860
|
+
result.testFire = { fired: tick.fired, errors: tick.errors };
|
|
17861
|
+
if (tick.errors > 0) {
|
|
17862
|
+
blockers.push(`test fire reported ${tick.errors} error(s) \u2014 check server KYNVER_CRON_SECRET matches`);
|
|
17863
|
+
}
|
|
17864
|
+
} else if (opts.skipTestFire) {
|
|
17865
|
+
result.testFire = { fired: 0, errors: 0, skipped: true };
|
|
17866
|
+
}
|
|
17867
|
+
result.verify = await verifyCronInstall({
|
|
17868
|
+
config: loadUserConfig(),
|
|
17869
|
+
envFilePath: plan.envFilePath
|
|
17870
|
+
});
|
|
17871
|
+
result.blockers = blockers;
|
|
17872
|
+
result.ok = blockers.length === 0 && (result.verify?.ok ?? false);
|
|
17873
|
+
} else {
|
|
17874
|
+
result.ok = blockers.length === 0;
|
|
17875
|
+
}
|
|
17876
|
+
return result;
|
|
17877
|
+
}
|
|
17878
|
+
|
|
17879
|
+
// src/cron/cron-install-cli.ts
|
|
17880
|
+
function parseBoolArg(value, defaultValue) {
|
|
17881
|
+
if (value === void 0) return defaultValue;
|
|
17882
|
+
if (value === true) return true;
|
|
17883
|
+
if (value === false) return false;
|
|
17884
|
+
const v = String(value).trim().toLowerCase();
|
|
17885
|
+
if (v === "0" || v === "false" || v === "no" || v === "off") return false;
|
|
17886
|
+
if (v === "1" || v === "true" || v === "yes" || v === "on") return true;
|
|
17887
|
+
return defaultValue;
|
|
17888
|
+
}
|
|
17889
|
+
async function runCronInstallCli(args) {
|
|
17890
|
+
const dryRun = args.dryRun === true || args["dry-run"] === true;
|
|
17891
|
+
const result = await runCronInstall({
|
|
17892
|
+
execute: dryRun ? false : parseBoolArg(args.execute, true),
|
|
17893
|
+
json: args.json === true,
|
|
17894
|
+
installSystemd: args.installSystemd === true || args["install-systemd"] === true,
|
|
17895
|
+
confirmQstashRemoval: args.confirmQstashRemoval === true || args["confirm-qstash-removal"] === true,
|
|
17896
|
+
skipWatchdog: args.skipWatchdog === true || args["skip-watchdog"] === true,
|
|
17897
|
+
skipTestFire: args.skipTestFire === true || args["skip-test-fire"] === true,
|
|
17898
|
+
agentOsId: typeof args.agentOsId === "string" ? args.agentOsId : void 0,
|
|
17899
|
+
apiBaseUrl: typeof args.apiBaseUrl === "string" ? args.apiBaseUrl : void 0,
|
|
17900
|
+
runId: typeof args.run === "string" ? args.run : void 0
|
|
17901
|
+
});
|
|
17902
|
+
if (args.json === true) {
|
|
17903
|
+
console.log(JSON.stringify(result, null, 2));
|
|
17904
|
+
if (!result.ok) process.exitCode = 1;
|
|
17905
|
+
return;
|
|
17906
|
+
}
|
|
17907
|
+
console.log(result.dryRun ? "Kynver Cron install (dry-run)\n" : "Kynver Cron install\n");
|
|
17908
|
+
console.log(` config: ${result.configPath}`);
|
|
17909
|
+
console.log(` env file: ${result.plan.envFilePath}`);
|
|
17910
|
+
console.log(` store: ${result.plan.storePath}`);
|
|
17911
|
+
if (result.secretGenerated) console.log(" generated new KYNVER_CRON_SECRET");
|
|
17912
|
+
if (result.envFile) {
|
|
17913
|
+
console.log(
|
|
17914
|
+
` env merge: ${result.envFile.changed ? "updated" : "unchanged"} (${result.envFile.keysWritten.join(", ") || "no key changes"})`
|
|
17915
|
+
);
|
|
17916
|
+
}
|
|
17917
|
+
if (result.watchdog?.remoteJobId) {
|
|
17918
|
+
console.log(
|
|
17919
|
+
` watchdog: job ${result.watchdog.remoteJobId} provider=${result.watchdog.provider} local=${result.watchdog.localProviderScheduleId}`
|
|
17920
|
+
);
|
|
17921
|
+
} else if (result.watchdog?.error) {
|
|
17922
|
+
console.log(` watchdog: ERROR ${result.watchdog.error}`);
|
|
17923
|
+
}
|
|
17924
|
+
if (result.qstashWatchdog?.found.length) {
|
|
17925
|
+
console.log(` qstash watchdog leftovers: ${result.qstashWatchdog.found.length}`);
|
|
17926
|
+
for (const j of result.qstashWatchdog.found) {
|
|
17927
|
+
console.log(` - ${j.id} (${j.status})`);
|
|
17928
|
+
}
|
|
17929
|
+
if (result.qstashWatchdog.removed.length) {
|
|
17930
|
+
console.log(` removed: ${result.qstashWatchdog.removed.join(", ")}`);
|
|
17931
|
+
}
|
|
17932
|
+
}
|
|
17933
|
+
if (result.systemd) {
|
|
17934
|
+
console.log(` systemd: ${result.systemd.note ?? (result.systemd.started ? "running" : "not started")}`);
|
|
17935
|
+
}
|
|
17936
|
+
if (result.testFire && !result.testFire.skipped) {
|
|
17937
|
+
console.log(` test fire: fired=${result.testFire.fired} errors=${result.testFire.errors}`);
|
|
17938
|
+
}
|
|
17939
|
+
console.log("\nHosted deployment (manual):");
|
|
17940
|
+
for (const step of result.plan.deploymentSteps) console.log(` - ${step}`);
|
|
17941
|
+
if (result.blockers.length) {
|
|
17942
|
+
console.log("\nBlockers:");
|
|
17943
|
+
for (const b of result.blockers) console.log(` ! ${b}`);
|
|
17944
|
+
process.exitCode = 1;
|
|
17945
|
+
return;
|
|
17946
|
+
}
|
|
17947
|
+
console.log("\nInstall complete \u2014 run `kynver cron verify` to re-check.");
|
|
17948
|
+
}
|
|
17949
|
+
async function runCronVerifyCli(args) {
|
|
17950
|
+
const config = loadUserConfig();
|
|
17951
|
+
const report = await verifyCronInstall({ config });
|
|
17952
|
+
if (args.json === true) {
|
|
17953
|
+
console.log(JSON.stringify(report, null, 2));
|
|
17954
|
+
if (!report.ok) process.exitCode = 1;
|
|
17955
|
+
return;
|
|
17956
|
+
}
|
|
17957
|
+
console.log(`Kynver Cron verify: ${report.ok ? "PASS" : "FAIL"}
|
|
17958
|
+
`);
|
|
17959
|
+
for (const check3 of report.checks) {
|
|
17960
|
+
const mark = check3.ok ? "ok" : "FAIL";
|
|
17961
|
+
console.log(` [${mark}] ${check3.id}: ${check3.summary}`);
|
|
17962
|
+
if (!check3.ok && check3.remediation) console.log(` \u2192 ${check3.remediation}`);
|
|
17963
|
+
}
|
|
17964
|
+
if (!report.ok) process.exitCode = 1;
|
|
17965
|
+
}
|
|
17966
|
+
|
|
16822
17967
|
// src/lane/landing-maintainer-tick.ts
|
|
16823
17968
|
init_config();
|
|
16824
|
-
import
|
|
17969
|
+
import os13 from "node:os";
|
|
16825
17970
|
init_config();
|
|
16826
17971
|
init_box_identity();
|
|
16827
17972
|
init_resource_gate();
|
|
@@ -16837,10 +17982,10 @@ var LANDING_MAINTAINER_LANE_SPEC = {
|
|
|
16837
17982
|
};
|
|
16838
17983
|
|
|
16839
17984
|
// src/lane/landing-maintainer-local.ts
|
|
16840
|
-
import { spawnSync as
|
|
16841
|
-
import
|
|
17985
|
+
import { spawnSync as spawnSync15 } from "node:child_process";
|
|
17986
|
+
import path78 from "node:path";
|
|
16842
17987
|
function runLandingWrapper(prNumber, repoRoot, execute) {
|
|
16843
|
-
const script =
|
|
17988
|
+
const script = path78.join(repoRoot, LANDING_MAINTAINER_LANE_SPEC.landScript);
|
|
16844
17989
|
const args = [script, String(prNumber), ...LANDING_MAINTAINER_LANE_SPEC.landScriptArgs];
|
|
16845
17990
|
if (!execute) {
|
|
16846
17991
|
return {
|
|
@@ -16851,7 +17996,7 @@ function runLandingWrapper(prNumber, repoRoot, execute) {
|
|
|
16851
17996
|
stderr: ""
|
|
16852
17997
|
};
|
|
16853
17998
|
}
|
|
16854
|
-
const result =
|
|
17999
|
+
const result = spawnSync15("node", args, {
|
|
16855
18000
|
cwd: repoRoot,
|
|
16856
18001
|
encoding: "utf8",
|
|
16857
18002
|
timeout: 10 * 60 * 1e3
|
|
@@ -16866,7 +18011,7 @@ function runLandingWrapper(prNumber, repoRoot, execute) {
|
|
|
16866
18011
|
}
|
|
16867
18012
|
function resolveLandingMaintainerRepoRoot(args) {
|
|
16868
18013
|
const explicit = args.repoPath ? String(args.repoPath).trim() : "";
|
|
16869
|
-
if (explicit) return
|
|
18014
|
+
if (explicit) return path78.resolve(explicit);
|
|
16870
18015
|
const resolved = resolveDefaultRepo();
|
|
16871
18016
|
return resolved?.repo ?? process.cwd();
|
|
16872
18017
|
}
|
|
@@ -16885,7 +18030,7 @@ async function runLandingMaintainerLaneTick(args) {
|
|
|
16885
18030
|
...buildBoxResourceSnapshotFromGate(resourceGate, {
|
|
16886
18031
|
harnessRunId: runId,
|
|
16887
18032
|
boxKind: resolveBoxKindFromConfig(loadUserConfig()),
|
|
16888
|
-
hostLabel:
|
|
18033
|
+
hostLabel: os13.hostname()
|
|
16889
18034
|
}),
|
|
16890
18035
|
providerHealthy: resourceGate.ok,
|
|
16891
18036
|
authorizedForRepair: resourceGate.ok,
|
|
@@ -16991,6 +18136,7 @@ function usage(code = 0) {
|
|
|
16991
18136
|
"Usage:",
|
|
16992
18137
|
" kynver login [--api-key KEY] [--api-base-url URL] (omit --api-key to authorize in the browser)",
|
|
16993
18138
|
" kynver bootstrap [--api-base-url URL] [--api-key KEY] [--repo PATH] (login + setup + runner credential in one shot)",
|
|
18139
|
+
" kynver start [--repo PATH] [--api-base-url URL] [--run RUN_ID] [--interval-ms MS] (bring your agent online: bootstrap if needed + run + daemon)",
|
|
16994
18140
|
" kynver runner credential [--agent-os-id ID] [--base-url URL]",
|
|
16995
18141
|
" kynver setup [--api-base-url URL] [--agent-os-id ID] [--agent-os-slug SLUG] [--box-kind forge|ghost] [--repo PATH] [--discover-repo] [--max-workers N] [--provider claude|cursor]",
|
|
16996
18142
|
" kynver daemon --run RUN_ID --agent-os-id AOS_ID [--execute] [--interval-ms MS] [--stall-ms MS] [--no-supervise]",
|
|
@@ -17031,6 +18177,8 @@ function usage(code = 0) {
|
|
|
17031
18177
|
" kynver doctor runtime-takeover [--remediate-default-repo]",
|
|
17032
18178
|
" kynver scheduler cutover-check [--json]",
|
|
17033
18179
|
" kynver scheduler attest-cutover [--json]",
|
|
18180
|
+
" kynver cron install [--dry-run] [--json] [--install-systemd] [--confirm-qstash-removal] [--skip-watchdog] [--skip-test-fire] [--agent-os-id ID] [--api-base-url URL] [--run RUN_ID]",
|
|
18181
|
+
" kynver cron verify [--json]",
|
|
17034
18182
|
" kynver cron status [--json]",
|
|
17035
18183
|
" kynver cron tick [--agent-os-id AOS_ID] [--json]",
|
|
17036
18184
|
" kynver lane tick landing-maintainer [--fleet] [--repo OWNER/NAME] [--agent-os-id AOS_ID] [--execute] [--json]",
|
|
@@ -17054,8 +18202,8 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
17054
18202
|
if (action && isHelpFlag(action) || rest.some(isHelpFlag)) return usage(0);
|
|
17055
18203
|
const args = parseArgs(rest);
|
|
17056
18204
|
const { runsDir, worktreesDir } = getPaths();
|
|
17057
|
-
|
|
17058
|
-
|
|
18205
|
+
mkdirSync13(runsDir, { recursive: true });
|
|
18206
|
+
mkdirSync13(worktreesDir, { recursive: true });
|
|
17059
18207
|
if (scope === "daemon") {
|
|
17060
18208
|
assertNativeDaemonAllowed();
|
|
17061
18209
|
}
|
|
@@ -17076,6 +18224,7 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
17076
18224
|
}
|
|
17077
18225
|
if (scope === "login") return void await runLogin(args);
|
|
17078
18226
|
if (scope === "bootstrap") return void await runBootstrap(args);
|
|
18227
|
+
if (scope === "start") return void await runStart(args);
|
|
17079
18228
|
if (scope === "status") return runStatus(args);
|
|
17080
18229
|
if (scope === "runner" && action === "credential") return void await mintRunnerCredential(args);
|
|
17081
18230
|
if (scope === "setup") return void await runSetup(args);
|
|
@@ -17106,6 +18255,12 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
17106
18255
|
if (scope === "scheduler" && action === "attest-cutover") {
|
|
17107
18256
|
return runSchedulerAttestCutoverCli(args.json === true);
|
|
17108
18257
|
}
|
|
18258
|
+
if (scope === "cron" && action === "install") {
|
|
18259
|
+
return void await runCronInstallCli(args);
|
|
18260
|
+
}
|
|
18261
|
+
if (scope === "cron" && action === "verify") {
|
|
18262
|
+
return void await runCronVerifyCli(args);
|
|
18263
|
+
}
|
|
17109
18264
|
if (scope === "cron" && action === "status") {
|
|
17110
18265
|
return void await runCronStatusCli(args.json === true);
|
|
17111
18266
|
}
|
|
@@ -17119,7 +18274,7 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
17119
18274
|
if (scope === "board" && action === "contract") {
|
|
17120
18275
|
return void await runCommandCenterContractCli(args);
|
|
17121
18276
|
}
|
|
17122
|
-
if (scope === "run" && action === "create") return createRun(args);
|
|
18277
|
+
if (scope === "run" && action === "create") return void createRun(args);
|
|
17123
18278
|
if (scope === "run" && action === "list") return listRuns();
|
|
17124
18279
|
if (scope === "run" && action === "resolve") return resolveHarnessRunCli(args);
|
|
17125
18280
|
if (scope === "run" && action === "status") return runStatus(args);
|