@kynver-app/runtime 0.1.118 → 0.1.120
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 +8143 -6989
- 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 +1349 -197
- package/dist/index.js.map +4 -4
- 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;
|
|
@@ -12451,7 +12466,7 @@ function isPipelineCleanupEnabled() {
|
|
|
12451
12466
|
|
|
12452
12467
|
// src/cli.ts
|
|
12453
12468
|
init_config();
|
|
12454
|
-
import { mkdirSync as
|
|
12469
|
+
import { mkdirSync as mkdirSync13, realpathSync } from "node:fs";
|
|
12455
12470
|
import { fileURLToPath as fileURLToPath5 } from "node:url";
|
|
12456
12471
|
|
|
12457
12472
|
// src/bootstrap.ts
|
|
@@ -12513,68 +12528,13 @@ async function runBootstrap(args) {
|
|
|
12513
12528
|
await runSetup(setupArgs);
|
|
12514
12529
|
console.log("");
|
|
12515
12530
|
console.log(` Bootstrap complete \u2014 ${os9.hostname()} is linked to workspace "${primary.slug}".`);
|
|
12516
|
-
console.log(" Next:
|
|
12517
|
-
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`.)");
|
|
12518
12533
|
}
|
|
12519
12534
|
|
|
12520
|
-
// src/
|
|
12521
|
-
|
|
12522
|
-
|
|
12523
|
-
// src/discard-disposable.ts
|
|
12524
|
-
init_run_store();
|
|
12525
|
-
init_status();
|
|
12526
|
-
import { existsSync as existsSync41, rmSync as rmSync4 } from "node:fs";
|
|
12527
|
-
import path54 from "node:path";
|
|
12528
|
-
function normalizeRelativePath2(value) {
|
|
12529
|
-
const normalized = value.replace(/\\/g, "/").replace(/^\.\//, "").trim();
|
|
12530
|
-
if (!normalized || normalized.startsWith("/") || normalized.includes("..")) {
|
|
12531
|
-
throw new Error(`unsafe path: ${value}`);
|
|
12532
|
-
}
|
|
12533
|
-
return normalized;
|
|
12534
|
-
}
|
|
12535
|
-
function parsePathsArg(raw) {
|
|
12536
|
-
if (typeof raw !== "string" || !raw.trim()) return [];
|
|
12537
|
-
return raw.split(",").map((p) => p.trim()).filter(Boolean);
|
|
12538
|
-
}
|
|
12539
|
-
function discardDisposableArtifacts(args) {
|
|
12540
|
-
const { runId, workerName } = resolveWorkerTargetArgs(args);
|
|
12541
|
-
const worker = loadWorker(runId, workerName);
|
|
12542
|
-
const paths = [
|
|
12543
|
-
...parsePathsArg(args.path),
|
|
12544
|
-
...Array.isArray(args.paths) ? args.paths : []
|
|
12545
|
-
];
|
|
12546
|
-
if (paths.length === 0) {
|
|
12547
|
-
return { ok: false, removed: [], reason: "requires at least one --path" };
|
|
12548
|
-
}
|
|
12549
|
-
const worktreeRoot = path54.resolve(worker.worktreePath);
|
|
12550
|
-
const removed = [];
|
|
12551
|
-
for (const raw of paths) {
|
|
12552
|
-
const rel = normalizeRelativePath2(raw);
|
|
12553
|
-
const abs = path54.resolve(worktreeRoot, rel);
|
|
12554
|
-
if (!abs.startsWith(worktreeRoot + path54.sep) && abs !== worktreeRoot) {
|
|
12555
|
-
return { ok: false, removed, reason: `path escapes worktree: ${raw}` };
|
|
12556
|
-
}
|
|
12557
|
-
if (!existsSync41(abs)) {
|
|
12558
|
-
return { ok: false, removed, reason: `path not found: ${raw}` };
|
|
12559
|
-
}
|
|
12560
|
-
rmSync4(abs, { recursive: true, force: true });
|
|
12561
|
-
removed.push(rel);
|
|
12562
|
-
}
|
|
12563
|
-
const prior = Array.isArray(worker.disposableArtifactsRemoved) ? worker.disposableArtifactsRemoved.filter((p) => typeof p === "string") : [];
|
|
12564
|
-
worker.disposableArtifactsRemoved = [.../* @__PURE__ */ new Set([...prior, ...removed])];
|
|
12565
|
-
saveWorker(worker.runId, worker);
|
|
12566
|
-
const status = computeWorkerStatus(worker);
|
|
12567
|
-
return {
|
|
12568
|
-
ok: true,
|
|
12569
|
-
removed,
|
|
12570
|
-
...status.changedFiles.length ? { reason: "worktree still has other changes" } : {}
|
|
12571
|
-
};
|
|
12572
|
-
}
|
|
12573
|
-
function discardDisposableCli(args) {
|
|
12574
|
-
const result = discardDisposableArtifacts(args);
|
|
12575
|
-
console.log(JSON.stringify(result, null, 2));
|
|
12576
|
-
if (!result.ok) process.exit(1);
|
|
12577
|
-
}
|
|
12535
|
+
// src/start.ts
|
|
12536
|
+
init_config();
|
|
12537
|
+
import os12 from "node:os";
|
|
12578
12538
|
|
|
12579
12539
|
// src/daemon.ts
|
|
12580
12540
|
init_config();
|
|
@@ -12624,15 +12584,15 @@ function validateDaemonInstallIdentity(config = loadUserConfig(), env = process.
|
|
|
12624
12584
|
// src/daemon-heartbeat.ts
|
|
12625
12585
|
import { mkdirSync as mkdirSync7, readFileSync as readFileSync16, renameSync as renameSync3, writeFileSync as writeFileSync4 } from "node:fs";
|
|
12626
12586
|
import { homedir as homedir14 } from "node:os";
|
|
12627
|
-
import
|
|
12587
|
+
import path54 from "node:path";
|
|
12628
12588
|
function daemonHeartbeatPath(agentOsId) {
|
|
12629
12589
|
const safe = agentOsId.replace(/[^A-Za-z0-9_-]/g, "_");
|
|
12630
|
-
return
|
|
12590
|
+
return path54.join(homedir14(), ".kynver", `daemon-heartbeat-${safe}.json`);
|
|
12631
12591
|
}
|
|
12632
12592
|
function writeDaemonHeartbeat(input) {
|
|
12633
12593
|
try {
|
|
12634
12594
|
const file = daemonHeartbeatPath(input.agentOsId);
|
|
12635
|
-
mkdirSync7(
|
|
12595
|
+
mkdirSync7(path54.dirname(file), { recursive: true });
|
|
12636
12596
|
const beat = {
|
|
12637
12597
|
observedAt: (input.now ?? /* @__PURE__ */ new Date()).toISOString(),
|
|
12638
12598
|
pid: process.pid,
|
|
@@ -12682,9 +12642,9 @@ function assertNativeDaemonAllowed() {
|
|
|
12682
12642
|
|
|
12683
12643
|
// src/cron/cron-env.ts
|
|
12684
12644
|
init_config();
|
|
12685
|
-
import { existsSync as
|
|
12645
|
+
import { existsSync as existsSync41 } from "node:fs";
|
|
12686
12646
|
import { homedir as homedir15 } from "node:os";
|
|
12687
|
-
import
|
|
12647
|
+
import path55 from "node:path";
|
|
12688
12648
|
function envFlag4(name, defaultValue) {
|
|
12689
12649
|
const raw = process.env[name]?.trim().toLowerCase();
|
|
12690
12650
|
if (!raw) return defaultValue;
|
|
@@ -12700,7 +12660,7 @@ function envInt(name, fallback, min = 1) {
|
|
|
12700
12660
|
function defaultKynverCronStorePath() {
|
|
12701
12661
|
const explicit = process.env.KYNVER_CRON_STORE_PATH?.trim() || process.env.OPENCLAW_CRON_STORE_PATH?.trim();
|
|
12702
12662
|
if (explicit) return explicit;
|
|
12703
|
-
return
|
|
12663
|
+
return path55.join(homedir15(), ".kynver", "agent-os-cron.json");
|
|
12704
12664
|
}
|
|
12705
12665
|
function defaultKynverCronStatePath(storePath = defaultKynverCronStorePath()) {
|
|
12706
12666
|
const explicit = process.env.KYNVER_CRON_TICK_STATE_PATH?.trim();
|
|
@@ -12720,7 +12680,7 @@ function resolveKynverCronEnv() {
|
|
|
12720
12680
|
const fireBaseUrl = resolveKynverCronFireBaseUrl();
|
|
12721
12681
|
const secret = resolveKynverCronSecret();
|
|
12722
12682
|
const credsReady = Boolean(fireBaseUrl && secret);
|
|
12723
|
-
const storeExists =
|
|
12683
|
+
const storeExists = existsSync41(storePath);
|
|
12724
12684
|
const defaultEnabled = credsReady && (storeExists || envFlag4("KYNVER_CRON_TICK_FORCE", false));
|
|
12725
12685
|
return {
|
|
12726
12686
|
storePath,
|
|
@@ -12774,10 +12734,10 @@ async function fireKynverCronJob(input) {
|
|
|
12774
12734
|
|
|
12775
12735
|
// src/cron/cron-lock.ts
|
|
12776
12736
|
init_util();
|
|
12777
|
-
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";
|
|
12778
12738
|
var STALE_LOCK_MS = 10 * 6e4;
|
|
12779
12739
|
function readLockInfo(lockPath) {
|
|
12780
|
-
if (!
|
|
12740
|
+
if (!existsSync42(lockPath)) return null;
|
|
12781
12741
|
try {
|
|
12782
12742
|
const parsed = JSON.parse(readFileSync17(lockPath, "utf8"));
|
|
12783
12743
|
if (typeof parsed.pid === "number" && typeof parsed.at === "string") return parsed;
|
|
@@ -12795,14 +12755,14 @@ function lockIsStale(lockPath) {
|
|
|
12795
12755
|
return Date.now() - atMs > STALE_LOCK_MS;
|
|
12796
12756
|
}
|
|
12797
12757
|
function tryAcquireCronTickLock(lockPath) {
|
|
12798
|
-
if (
|
|
12758
|
+
if (existsSync42(lockPath) && !lockIsStale(lockPath)) {
|
|
12799
12759
|
const info = readLockInfo(lockPath);
|
|
12800
12760
|
return {
|
|
12801
12761
|
acquired: false,
|
|
12802
12762
|
reason: info ? `held by pid ${info.pid}` : "held by another process"
|
|
12803
12763
|
};
|
|
12804
12764
|
}
|
|
12805
|
-
if (
|
|
12765
|
+
if (existsSync42(lockPath)) {
|
|
12806
12766
|
try {
|
|
12807
12767
|
unlinkSync3(lockPath);
|
|
12808
12768
|
} catch {
|
|
@@ -12939,11 +12899,34 @@ async function loadCronJobs(storePath = defaultKynverCronStorePath()) {
|
|
|
12939
12899
|
const raw = await readFileIfExists(storePath);
|
|
12940
12900
|
return parseCronStore(raw);
|
|
12941
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
|
+
}
|
|
12942
12925
|
|
|
12943
12926
|
// src/cron/cron-tick-state.ts
|
|
12944
12927
|
import { randomBytes } from "node:crypto";
|
|
12945
12928
|
import { promises as fs4 } from "node:fs";
|
|
12946
|
-
import
|
|
12929
|
+
import path56 from "node:path";
|
|
12947
12930
|
var EMPTY = { version: 1, jobs: {} };
|
|
12948
12931
|
async function readFileIfExists2(filePath) {
|
|
12949
12932
|
try {
|
|
@@ -12970,7 +12953,7 @@ async function loadCronTickState(statePath) {
|
|
|
12970
12953
|
return parseCronTickState(raw);
|
|
12971
12954
|
}
|
|
12972
12955
|
async function writeStateAtomic(statePath, state) {
|
|
12973
|
-
await fs4.mkdir(
|
|
12956
|
+
await fs4.mkdir(path56.dirname(statePath), { recursive: true });
|
|
12974
12957
|
const suffix = randomBytes(6).toString("hex");
|
|
12975
12958
|
const tmp = `${statePath}.tmp-${process.pid}-${Date.now()}-${suffix}`;
|
|
12976
12959
|
await fs4.writeFile(tmp, `${JSON.stringify(state, null, 2)}
|
|
@@ -13150,7 +13133,7 @@ async function runKynverCronTick(opts = {}) {
|
|
|
13150
13133
|
init_util();
|
|
13151
13134
|
|
|
13152
13135
|
// src/pipeline-tick.ts
|
|
13153
|
-
import
|
|
13136
|
+
import path59 from "node:path";
|
|
13154
13137
|
init_config();
|
|
13155
13138
|
|
|
13156
13139
|
// src/pipeline-dispatch.ts
|
|
@@ -13292,7 +13275,7 @@ init_util();
|
|
|
13292
13275
|
init_status();
|
|
13293
13276
|
init_run_store();
|
|
13294
13277
|
init_util();
|
|
13295
|
-
import
|
|
13278
|
+
import path57 from "node:path";
|
|
13296
13279
|
|
|
13297
13280
|
// src/plan-progress-sync.ts
|
|
13298
13281
|
init_config();
|
|
@@ -13317,7 +13300,7 @@ async function syncActiveWorkerPlanProgress(runId, args) {
|
|
|
13317
13300
|
const outcomes = [];
|
|
13318
13301
|
for (const name of Object.keys(run.workers || {})) {
|
|
13319
13302
|
const worker = readJson(
|
|
13320
|
-
|
|
13303
|
+
path57.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
13321
13304
|
void 0
|
|
13322
13305
|
);
|
|
13323
13306
|
if (!worker?.dispatched || !worker.taskId) continue;
|
|
@@ -13685,10 +13668,10 @@ function collectProviderEvidence(wanted, opts = {}) {
|
|
|
13685
13668
|
// src/provider-evidence/wanted-store.ts
|
|
13686
13669
|
init_run_store();
|
|
13687
13670
|
init_util();
|
|
13688
|
-
import
|
|
13671
|
+
import path58 from "node:path";
|
|
13689
13672
|
var WANTED_FILE = "provider-evidence-wanted.json";
|
|
13690
13673
|
function wantedFilePath(runId) {
|
|
13691
|
-
return
|
|
13674
|
+
return path58.join(runDirectory(runId), WANTED_FILE);
|
|
13692
13675
|
}
|
|
13693
13676
|
function parseWantedItems(value) {
|
|
13694
13677
|
if (!Array.isArray(value)) return [];
|
|
@@ -13727,7 +13710,7 @@ async function completeFinishedWorkers(runId, args) {
|
|
|
13727
13710
|
const outcomes = [];
|
|
13728
13711
|
for (const name of Object.keys(run.workers || {})) {
|
|
13729
13712
|
const worker = readJson(
|
|
13730
|
-
|
|
13713
|
+
path59.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
13731
13714
|
void 0
|
|
13732
13715
|
);
|
|
13733
13716
|
if (!worker?.taskId || worker.localOnly) continue;
|
|
@@ -13895,6 +13878,389 @@ async function runPipelineTick(args) {
|
|
|
13895
13878
|
};
|
|
13896
13879
|
}
|
|
13897
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
|
+
|
|
13898
14264
|
// src/daemon.ts
|
|
13899
14265
|
var DEFAULT_INTERVAL_MS = 6e4;
|
|
13900
14266
|
var IDLE_INTERVAL_MS = 5 * 6e4;
|
|
@@ -13947,6 +14313,14 @@ async function runDaemon(args) {
|
|
|
13947
14313
|
})
|
|
13948
14314
|
);
|
|
13949
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
|
+
});
|
|
13950
14324
|
while (!stopping) {
|
|
13951
14325
|
try {
|
|
13952
14326
|
writeDaemonHeartbeat({ agentOsId, runId });
|
|
@@ -13973,40 +14347,143 @@ async function runDaemon(args) {
|
|
|
13973
14347
|
await awaitDaemonBackoff(intervalMs, () => stopping);
|
|
13974
14348
|
}
|
|
13975
14349
|
}
|
|
14350
|
+
await chatLoop;
|
|
13976
14351
|
console.error(JSON.stringify({ event: "daemon_stop", runId, agentOsId }));
|
|
13977
14352
|
}
|
|
13978
14353
|
|
|
13979
|
-
// src/
|
|
13980
|
-
|
|
13981
|
-
|
|
13982
|
-
|
|
13983
|
-
|
|
13984
|
-
var STARTUP_GRACE_MS = 2 * 6e4;
|
|
13985
|
-
var KILL_GRACE_MS = 1e4;
|
|
13986
|
-
var BACKOFF_BASE_MS = 5e3;
|
|
13987
|
-
var BACKOFF_CAP_MS = 5 * 6e4;
|
|
13988
|
-
var HEALTHY_RESET_MS = 30 * 6e4;
|
|
13989
|
-
var POLL_MS = 5e3;
|
|
13990
|
-
function resolveKeeperStallMs(flag, env = process.env) {
|
|
13991
|
-
const fromFlag = typeof flag === "string" ? Number.parseInt(flag, 10) : NaN;
|
|
13992
|
-
if (Number.isFinite(fromFlag) && fromFlag > 0) return fromFlag;
|
|
13993
|
-
const fromEnv = Number.parseInt(env.KYNVER_DAEMON_STALL_MS ?? "", 10);
|
|
13994
|
-
if (Number.isFinite(fromEnv) && fromEnv > 0) return fromEnv;
|
|
13995
|
-
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;
|
|
13996
14359
|
}
|
|
13997
|
-
function
|
|
13998
|
-
|
|
13999
|
-
|
|
14000
|
-
if (
|
|
14001
|
-
|
|
14002
|
-
|
|
14003
|
-
|
|
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();
|
|
14004
14367
|
}
|
|
14005
|
-
|
|
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 });
|
|
14006
14395
|
}
|
|
14007
|
-
|
|
14008
|
-
|
|
14009
|
-
|
|
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);
|
|
14010
14487
|
}
|
|
14011
14488
|
function keeperRunWasHealthy(startedAtMs, endedAtMs, healthyMs = HEALTHY_RESET_MS) {
|
|
14012
14489
|
return endedAtMs - startedAtMs >= healthyMs;
|
|
@@ -14109,7 +14586,7 @@ async function runDaemonKeeper(args, rawArgv = process.argv.slice(2)) {
|
|
|
14109
14586
|
|
|
14110
14587
|
// src/plan-progress.ts
|
|
14111
14588
|
init_config();
|
|
14112
|
-
import
|
|
14589
|
+
import path65 from "node:path";
|
|
14113
14590
|
|
|
14114
14591
|
// src/bounded-build/constants.ts
|
|
14115
14592
|
var DEFAULT_BUILD_MEM_BUDGET_BYTES = 1536 * 1024 * 1024;
|
|
@@ -14254,20 +14731,20 @@ import {
|
|
|
14254
14731
|
mkdirSync as mkdirSync9,
|
|
14255
14732
|
openSync as openSync7,
|
|
14256
14733
|
readdirSync as readdirSync15,
|
|
14257
|
-
readFileSync as
|
|
14734
|
+
readFileSync as readFileSync19,
|
|
14258
14735
|
unlinkSync as unlinkSync4,
|
|
14259
14736
|
writeFileSync as writeFileSync6
|
|
14260
14737
|
} from "node:fs";
|
|
14261
|
-
import
|
|
14738
|
+
import path63 from "node:path";
|
|
14262
14739
|
|
|
14263
14740
|
// src/heavy-verification/paths.ts
|
|
14264
14741
|
import { mkdirSync as mkdirSync8 } from "node:fs";
|
|
14265
|
-
import
|
|
14742
|
+
import path62 from "node:path";
|
|
14266
14743
|
function resolveHeavyVerificationRoot() {
|
|
14267
|
-
return
|
|
14744
|
+
return path62.join(resolveKynverStateRoot(), "heavy-verification");
|
|
14268
14745
|
}
|
|
14269
14746
|
function heavyVerificationSlotsDir() {
|
|
14270
|
-
return
|
|
14747
|
+
return path62.join(resolveHeavyVerificationRoot(), "slots");
|
|
14271
14748
|
}
|
|
14272
14749
|
function ensureHeavyVerificationDirs() {
|
|
14273
14750
|
const dir = heavyVerificationSlotsDir();
|
|
@@ -14296,12 +14773,12 @@ function indexedSlotId(index) {
|
|
|
14296
14773
|
return `slot-${index}`;
|
|
14297
14774
|
}
|
|
14298
14775
|
function slotFilePath(slotId, slotsDir = heavyVerificationSlotsDir()) {
|
|
14299
|
-
return
|
|
14776
|
+
return path63.join(slotsDir, `${slotId}.json`);
|
|
14300
14777
|
}
|
|
14301
14778
|
function readSlotRecord(filePath) {
|
|
14302
14779
|
if (!existsSync44(filePath)) return null;
|
|
14303
14780
|
try {
|
|
14304
|
-
const parsed = JSON.parse(
|
|
14781
|
+
const parsed = JSON.parse(readFileSync19(filePath, "utf8"));
|
|
14305
14782
|
if (typeof parsed.slotId === "string" && typeof parsed.pid === "number" && typeof parsed.acquiredAt === "string" && typeof parsed.command === "string") {
|
|
14306
14783
|
return parsed;
|
|
14307
14784
|
}
|
|
@@ -14335,7 +14812,7 @@ function reclaimStaleHeavyVerificationSlots(opts = {}) {
|
|
|
14335
14812
|
let reclaimed = 0;
|
|
14336
14813
|
for (const name of readdirSync15(slotsDir)) {
|
|
14337
14814
|
if (!name.endsWith(".json")) continue;
|
|
14338
|
-
const filePath =
|
|
14815
|
+
const filePath = path63.join(slotsDir, name);
|
|
14339
14816
|
const before = existsSync44(filePath);
|
|
14340
14817
|
reclaimStaleSlot(filePath, staleMs);
|
|
14341
14818
|
if (before && !existsSync44(filePath)) reclaimed += 1;
|
|
@@ -14349,7 +14826,7 @@ function listActiveHeavyVerificationSlots(opts = {}) {
|
|
|
14349
14826
|
const active = [];
|
|
14350
14827
|
for (const name of readdirSync15(slotsDir)) {
|
|
14351
14828
|
if (!name.endsWith(".json")) continue;
|
|
14352
|
-
const record3 = readSlotRecord(
|
|
14829
|
+
const record3 = readSlotRecord(path63.join(slotsDir, name));
|
|
14353
14830
|
if (record3 && !slotIsStale(record3, staleMs)) active.push(record3);
|
|
14354
14831
|
}
|
|
14355
14832
|
return active;
|
|
@@ -14470,11 +14947,11 @@ function waitForHeavyVerificationSlot(command, timeoutMs, pollMs = 2e3, opts = {
|
|
|
14470
14947
|
|
|
14471
14948
|
// src/harness-worktree-build-guard.ts
|
|
14472
14949
|
init_paths();
|
|
14473
|
-
import
|
|
14950
|
+
import path64 from "node:path";
|
|
14474
14951
|
function isPathUnderHarnessWorktree(cwd) {
|
|
14475
14952
|
const worktreesDir = harnessWorktreesDir(resolveHarnessRoot());
|
|
14476
|
-
const rel =
|
|
14477
|
-
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);
|
|
14478
14955
|
}
|
|
14479
14956
|
function assessHarnessWorktreeBuildGuard(cwd) {
|
|
14480
14957
|
if (!isPathUnderHarnessWorktree(cwd)) return { ok: true };
|
|
@@ -14687,7 +15164,7 @@ async function emitPlanProgress(args) {
|
|
|
14687
15164
|
}
|
|
14688
15165
|
function verifyPlanLocal(args) {
|
|
14689
15166
|
const worktree = required(args.worktree ? String(args.worktree) : void 0, "worktree");
|
|
14690
|
-
const cwd =
|
|
15167
|
+
const cwd = path65.resolve(worktree);
|
|
14691
15168
|
const summary = runHarnessVerifyCommands(cwd);
|
|
14692
15169
|
const emitJson = args.json === true || args.json === "true";
|
|
14693
15170
|
const payload = { passed: summary.passed, worktree: cwd, steps: summary.steps };
|
|
@@ -14736,10 +15213,10 @@ async function verifyPlan(args) {
|
|
|
14736
15213
|
}
|
|
14737
15214
|
|
|
14738
15215
|
// src/harness-verify-cli.ts
|
|
14739
|
-
import
|
|
15216
|
+
import path66 from "node:path";
|
|
14740
15217
|
init_util();
|
|
14741
15218
|
function runHarnessVerifyCli(args) {
|
|
14742
|
-
const cwd =
|
|
15219
|
+
const cwd = path66.resolve(required(args.worktree ? String(args.worktree) : void 0, "worktree"));
|
|
14743
15220
|
const emitJson = args.json === true || args.json === "true" || args.emitJson === true || args.emitJson === "true";
|
|
14744
15221
|
const commands = [];
|
|
14745
15222
|
const rawCmd = args.command;
|
|
@@ -14785,7 +15262,7 @@ function runHarnessVerifyCli(args) {
|
|
|
14785
15262
|
|
|
14786
15263
|
// src/plan-persist-cli.ts
|
|
14787
15264
|
init_config();
|
|
14788
|
-
import { readFileSync as
|
|
15265
|
+
import { readFileSync as readFileSync20 } from "node:fs";
|
|
14789
15266
|
init_util();
|
|
14790
15267
|
var OPERATIONS = ["create", "add_version", "update_metadata"];
|
|
14791
15268
|
var FAILURE_KINDS = [
|
|
@@ -14798,7 +15275,7 @@ var FAILURE_KINDS = [
|
|
|
14798
15275
|
function readBodyArg(args) {
|
|
14799
15276
|
const bodyFile = args.bodyFile ? String(args.bodyFile) : void 0;
|
|
14800
15277
|
if (bodyFile) {
|
|
14801
|
-
return { body:
|
|
15278
|
+
return { body: readFileSync20(bodyFile, "utf8"), bodyPathHint: bodyFile };
|
|
14802
15279
|
}
|
|
14803
15280
|
const inline = args.body ? String(args.body) : void 0;
|
|
14804
15281
|
if (inline) return { body: inline };
|
|
@@ -15178,7 +15655,7 @@ ${text.slice(0, 800)}`,
|
|
|
15178
15655
|
}
|
|
15179
15656
|
|
|
15180
15657
|
// src/monitor/monitor.service.ts
|
|
15181
|
-
import
|
|
15658
|
+
import path68 from "node:path";
|
|
15182
15659
|
init_run_store();
|
|
15183
15660
|
init_status();
|
|
15184
15661
|
init_util();
|
|
@@ -15238,10 +15715,10 @@ function classifyWorkerHealth(input) {
|
|
|
15238
15715
|
init_paths();
|
|
15239
15716
|
init_util();
|
|
15240
15717
|
import { existsSync as existsSync45, mkdirSync as mkdirSync10, readdirSync as readdirSync16, unlinkSync as unlinkSync5 } from "node:fs";
|
|
15241
|
-
import
|
|
15718
|
+
import path67 from "node:path";
|
|
15242
15719
|
function monitorsDir() {
|
|
15243
15720
|
const { harnessRoot } = getHarnessPaths();
|
|
15244
|
-
const dir =
|
|
15721
|
+
const dir = path67.join(harnessRoot, "monitors");
|
|
15245
15722
|
mkdirSync10(dir, { recursive: true });
|
|
15246
15723
|
return dir;
|
|
15247
15724
|
}
|
|
@@ -15249,7 +15726,7 @@ function monitorIdFor(runId, workerName) {
|
|
|
15249
15726
|
return workerName ? `${safeSlug(runId)}--${safeSlug(workerName)}` : safeSlug(runId);
|
|
15250
15727
|
}
|
|
15251
15728
|
function monitorPath(monitorId) {
|
|
15252
|
-
return
|
|
15729
|
+
return path67.join(monitorsDir(), `${monitorId}.json`);
|
|
15253
15730
|
}
|
|
15254
15731
|
function loadMonitorSession(monitorId) {
|
|
15255
15732
|
return readJson(monitorPath(monitorId), void 0);
|
|
@@ -15270,7 +15747,7 @@ function listMonitorSessions() {
|
|
|
15270
15747
|
for (const name of readdirSync16(dir)) {
|
|
15271
15748
|
if (!name.endsWith(".json")) continue;
|
|
15272
15749
|
const session = readJson(
|
|
15273
|
-
|
|
15750
|
+
path67.join(dir, name),
|
|
15274
15751
|
void 0
|
|
15275
15752
|
);
|
|
15276
15753
|
if (!session?.monitorId) continue;
|
|
@@ -15363,7 +15840,7 @@ async function fetchTaskLeasesForWorkers(input) {
|
|
|
15363
15840
|
// src/monitor/monitor.service.ts
|
|
15364
15841
|
function workerRecord2(runId, name) {
|
|
15365
15842
|
return readJson(
|
|
15366
|
-
|
|
15843
|
+
path68.join(runDirectory(runId), "workers", safeSlug(name), "worker.json"),
|
|
15367
15844
|
void 0
|
|
15368
15845
|
);
|
|
15369
15846
|
}
|
|
@@ -15573,17 +16050,17 @@ init_util();
|
|
|
15573
16050
|
init_paths();
|
|
15574
16051
|
import { spawn as spawn7 } from "node:child_process";
|
|
15575
16052
|
import { closeSync as closeSync8, existsSync as existsSync46, openSync as openSync8 } from "node:fs";
|
|
15576
|
-
import
|
|
16053
|
+
import path69 from "node:path";
|
|
15577
16054
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
15578
16055
|
function resolveDefaultCliPath2() {
|
|
15579
|
-
return
|
|
16056
|
+
return path69.join(fileURLToPath4(new URL(".", import.meta.url)), "cli.js");
|
|
15580
16057
|
}
|
|
15581
16058
|
function spawnMonitorSidecar(opts) {
|
|
15582
16059
|
const cliPath = opts.cliPath ?? resolveDefaultCliPath2();
|
|
15583
16060
|
if (!existsSync46(cliPath)) return void 0;
|
|
15584
16061
|
const monitorId = monitorIdFor(opts.runId, opts.workerName);
|
|
15585
16062
|
const { harnessRoot } = getHarnessPaths();
|
|
15586
|
-
const logPath =
|
|
16063
|
+
const logPath = path69.join(harnessRoot, "monitors", `${monitorId}.log`);
|
|
15587
16064
|
let logFd;
|
|
15588
16065
|
try {
|
|
15589
16066
|
logFd = openSync8(logPath, "a");
|
|
@@ -15708,7 +16185,7 @@ init_run_store();
|
|
|
15708
16185
|
init_status();
|
|
15709
16186
|
init_util();
|
|
15710
16187
|
init_config();
|
|
15711
|
-
import
|
|
16188
|
+
import path70 from "node:path";
|
|
15712
16189
|
function skip(runId, worker, taskId, agentOsId, leaseOwner, reason) {
|
|
15713
16190
|
return { runId, worker, taskId, agentOsId, leaseOwner, action: "skipped", reason };
|
|
15714
16191
|
}
|
|
@@ -15721,7 +16198,7 @@ async function postRestartUnblock(args) {
|
|
|
15721
16198
|
const errors = [];
|
|
15722
16199
|
for (const run of listRunRecords()) {
|
|
15723
16200
|
for (const name of Object.keys(run.workers ?? {})) {
|
|
15724
|
-
const workerPath =
|
|
16201
|
+
const workerPath = path70.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json");
|
|
15725
16202
|
const worker = readJson(workerPath, void 0);
|
|
15726
16203
|
if (!worker) {
|
|
15727
16204
|
skipped.push(skip(run.id, name, "", "", "", "worker.json missing"));
|
|
@@ -15835,9 +16312,9 @@ async function postRestartUnblockCli(args) {
|
|
|
15835
16312
|
// src/default-repo-cli.ts
|
|
15836
16313
|
init_path_values();
|
|
15837
16314
|
init_config();
|
|
15838
|
-
import
|
|
15839
|
-
import { homedir as
|
|
15840
|
-
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");
|
|
15841
16318
|
function ensureDefaultRepo(opts) {
|
|
15842
16319
|
const existing = loadUserConfig();
|
|
15843
16320
|
const resolved = resolveDefaultRepo({ ...opts, config: existing });
|
|
@@ -15918,14 +16395,14 @@ function summarizeResolvedDefaultRepo(resolved) {
|
|
|
15918
16395
|
}
|
|
15919
16396
|
|
|
15920
16397
|
// src/doctor/runtime-takeover.ts
|
|
15921
|
-
import
|
|
16398
|
+
import path73 from "node:path";
|
|
15922
16399
|
init_path_values();
|
|
15923
16400
|
|
|
15924
16401
|
// src/doctor/runtime-takeover.probes.ts
|
|
15925
16402
|
init_config();
|
|
15926
|
-
import { accessSync, constants, existsSync as existsSync47, readFileSync as
|
|
15927
|
-
import { homedir as
|
|
15928
|
-
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";
|
|
15929
16406
|
import { spawnSync as spawnSync13 } from "node:child_process";
|
|
15930
16407
|
init_paths();
|
|
15931
16408
|
function captureCommand(bin, args) {
|
|
@@ -15968,15 +16445,15 @@ var defaultRuntimeTakeoverProbes = {
|
|
|
15968
16445
|
commandOnPath: (bin) => captureCommand(process.platform === "win32" ? "where" : "which", [bin]),
|
|
15969
16446
|
kynverVersion: (bin) => captureCommand(bin, ["--version"]),
|
|
15970
16447
|
loadConfig: () => loadUserConfig(),
|
|
15971
|
-
configFilePath: () =>
|
|
15972
|
-
credentialsFilePath: () =>
|
|
16448
|
+
configFilePath: () => path72.join(homedir18(), ".kynver", "config.json"),
|
|
16449
|
+
credentialsFilePath: () => path72.join(homedir18(), ".kynver", "credentials"),
|
|
15973
16450
|
readCredentials: () => {
|
|
15974
|
-
const credPath =
|
|
16451
|
+
const credPath = path72.join(homedir18(), ".kynver", "credentials");
|
|
15975
16452
|
if (!existsSync47(credPath)) {
|
|
15976
16453
|
return { hasApiKey: false };
|
|
15977
16454
|
}
|
|
15978
16455
|
try {
|
|
15979
|
-
const parsed = JSON.parse(
|
|
16456
|
+
const parsed = JSON.parse(readFileSync21(credPath, "utf8"));
|
|
15980
16457
|
return {
|
|
15981
16458
|
hasApiKey: Boolean(parsed.apiKey?.trim()),
|
|
15982
16459
|
runnerTokenPrefix: tokenPrefix(parsed.runnerToken),
|
|
@@ -16006,7 +16483,7 @@ var defaultRuntimeTakeoverProbes = {
|
|
|
16006
16483
|
})()
|
|
16007
16484
|
}),
|
|
16008
16485
|
harnessRoot: () => resolveHarnessRoot(),
|
|
16009
|
-
legacyOpenclawHarnessRoot: () =>
|
|
16486
|
+
legacyOpenclawHarnessRoot: () => path72.join(homedir18(), ".openclaw", "harness"),
|
|
16010
16487
|
pathExists: (target) => existsSync47(target),
|
|
16011
16488
|
pathWritable: (target) => isWritable(target)
|
|
16012
16489
|
};
|
|
@@ -16413,8 +16890,8 @@ function assessVercelDeployEvidence(probes) {
|
|
|
16413
16890
|
}
|
|
16414
16891
|
function assessHarnessDirs(probes) {
|
|
16415
16892
|
const harnessRoot = probes.harnessRoot();
|
|
16416
|
-
const runsDir =
|
|
16417
|
-
const worktreesDir =
|
|
16893
|
+
const runsDir = path73.join(harnessRoot, "runs");
|
|
16894
|
+
const worktreesDir = path73.join(harnessRoot, "worktrees");
|
|
16418
16895
|
const displayHarnessRoot = redactHomePath(harnessRoot);
|
|
16419
16896
|
const displayRunsDir = redactHomePath(runsDir);
|
|
16420
16897
|
const displayWorktreesDir = redactHomePath(worktreesDir);
|
|
@@ -16683,9 +17160,9 @@ function applySchedulerCutoverAttestation(config) {
|
|
|
16683
17160
|
|
|
16684
17161
|
// src/scheduler-cutover-cli.ts
|
|
16685
17162
|
init_config();
|
|
16686
|
-
import
|
|
16687
|
-
import { homedir as
|
|
16688
|
-
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");
|
|
16689
17166
|
function runSchedulerCutoverCheckCli(json = false) {
|
|
16690
17167
|
const config = loadUserConfig();
|
|
16691
17168
|
const report = assessSchedulerCutover(config);
|
|
@@ -16822,9 +17299,674 @@ async function runCronTickCli(args) {
|
|
|
16822
17299
|
);
|
|
16823
17300
|
}
|
|
16824
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
|
+
|
|
16825
17967
|
// src/lane/landing-maintainer-tick.ts
|
|
16826
17968
|
init_config();
|
|
16827
|
-
import
|
|
17969
|
+
import os13 from "node:os";
|
|
16828
17970
|
init_config();
|
|
16829
17971
|
init_box_identity();
|
|
16830
17972
|
init_resource_gate();
|
|
@@ -16840,10 +17982,10 @@ var LANDING_MAINTAINER_LANE_SPEC = {
|
|
|
16840
17982
|
};
|
|
16841
17983
|
|
|
16842
17984
|
// src/lane/landing-maintainer-local.ts
|
|
16843
|
-
import { spawnSync as
|
|
16844
|
-
import
|
|
17985
|
+
import { spawnSync as spawnSync15 } from "node:child_process";
|
|
17986
|
+
import path78 from "node:path";
|
|
16845
17987
|
function runLandingWrapper(prNumber, repoRoot, execute) {
|
|
16846
|
-
const script =
|
|
17988
|
+
const script = path78.join(repoRoot, LANDING_MAINTAINER_LANE_SPEC.landScript);
|
|
16847
17989
|
const args = [script, String(prNumber), ...LANDING_MAINTAINER_LANE_SPEC.landScriptArgs];
|
|
16848
17990
|
if (!execute) {
|
|
16849
17991
|
return {
|
|
@@ -16854,7 +17996,7 @@ function runLandingWrapper(prNumber, repoRoot, execute) {
|
|
|
16854
17996
|
stderr: ""
|
|
16855
17997
|
};
|
|
16856
17998
|
}
|
|
16857
|
-
const result =
|
|
17999
|
+
const result = spawnSync15("node", args, {
|
|
16858
18000
|
cwd: repoRoot,
|
|
16859
18001
|
encoding: "utf8",
|
|
16860
18002
|
timeout: 10 * 60 * 1e3
|
|
@@ -16869,7 +18011,7 @@ function runLandingWrapper(prNumber, repoRoot, execute) {
|
|
|
16869
18011
|
}
|
|
16870
18012
|
function resolveLandingMaintainerRepoRoot(args) {
|
|
16871
18013
|
const explicit = args.repoPath ? String(args.repoPath).trim() : "";
|
|
16872
|
-
if (explicit) return
|
|
18014
|
+
if (explicit) return path78.resolve(explicit);
|
|
16873
18015
|
const resolved = resolveDefaultRepo();
|
|
16874
18016
|
return resolved?.repo ?? process.cwd();
|
|
16875
18017
|
}
|
|
@@ -16888,7 +18030,7 @@ async function runLandingMaintainerLaneTick(args) {
|
|
|
16888
18030
|
...buildBoxResourceSnapshotFromGate(resourceGate, {
|
|
16889
18031
|
harnessRunId: runId,
|
|
16890
18032
|
boxKind: resolveBoxKindFromConfig(loadUserConfig()),
|
|
16891
|
-
hostLabel:
|
|
18033
|
+
hostLabel: os13.hostname()
|
|
16892
18034
|
}),
|
|
16893
18035
|
providerHealthy: resourceGate.ok,
|
|
16894
18036
|
authorizedForRepair: resourceGate.ok,
|
|
@@ -16994,6 +18136,7 @@ function usage(code = 0) {
|
|
|
16994
18136
|
"Usage:",
|
|
16995
18137
|
" kynver login [--api-key KEY] [--api-base-url URL] (omit --api-key to authorize in the browser)",
|
|
16996
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)",
|
|
16997
18140
|
" kynver runner credential [--agent-os-id ID] [--base-url URL]",
|
|
16998
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]",
|
|
16999
18142
|
" kynver daemon --run RUN_ID --agent-os-id AOS_ID [--execute] [--interval-ms MS] [--stall-ms MS] [--no-supervise]",
|
|
@@ -17034,6 +18177,8 @@ function usage(code = 0) {
|
|
|
17034
18177
|
" kynver doctor runtime-takeover [--remediate-default-repo]",
|
|
17035
18178
|
" kynver scheduler cutover-check [--json]",
|
|
17036
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]",
|
|
17037
18182
|
" kynver cron status [--json]",
|
|
17038
18183
|
" kynver cron tick [--agent-os-id AOS_ID] [--json]",
|
|
17039
18184
|
" kynver lane tick landing-maintainer [--fleet] [--repo OWNER/NAME] [--agent-os-id AOS_ID] [--execute] [--json]",
|
|
@@ -17057,8 +18202,8 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
17057
18202
|
if (action && isHelpFlag(action) || rest.some(isHelpFlag)) return usage(0);
|
|
17058
18203
|
const args = parseArgs(rest);
|
|
17059
18204
|
const { runsDir, worktreesDir } = getPaths();
|
|
17060
|
-
|
|
17061
|
-
|
|
18205
|
+
mkdirSync13(runsDir, { recursive: true });
|
|
18206
|
+
mkdirSync13(worktreesDir, { recursive: true });
|
|
17062
18207
|
if (scope === "daemon") {
|
|
17063
18208
|
assertNativeDaemonAllowed();
|
|
17064
18209
|
}
|
|
@@ -17079,6 +18224,7 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
17079
18224
|
}
|
|
17080
18225
|
if (scope === "login") return void await runLogin(args);
|
|
17081
18226
|
if (scope === "bootstrap") return void await runBootstrap(args);
|
|
18227
|
+
if (scope === "start") return void await runStart(args);
|
|
17082
18228
|
if (scope === "status") return runStatus(args);
|
|
17083
18229
|
if (scope === "runner" && action === "credential") return void await mintRunnerCredential(args);
|
|
17084
18230
|
if (scope === "setup") return void await runSetup(args);
|
|
@@ -17109,6 +18255,12 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
17109
18255
|
if (scope === "scheduler" && action === "attest-cutover") {
|
|
17110
18256
|
return runSchedulerAttestCutoverCli(args.json === true);
|
|
17111
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
|
+
}
|
|
17112
18264
|
if (scope === "cron" && action === "status") {
|
|
17113
18265
|
return void await runCronStatusCli(args.json === true);
|
|
17114
18266
|
}
|
|
@@ -17122,7 +18274,7 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
17122
18274
|
if (scope === "board" && action === "contract") {
|
|
17123
18275
|
return void await runCommandCenterContractCli(args);
|
|
17124
18276
|
}
|
|
17125
|
-
if (scope === "run" && action === "create") return createRun(args);
|
|
18277
|
+
if (scope === "run" && action === "create") return void createRun(args);
|
|
17126
18278
|
if (scope === "run" && action === "list") return listRuns();
|
|
17127
18279
|
if (scope === "run" && action === "resolve") return resolveHarnessRunCli(args);
|
|
17128
18280
|
if (scope === "run" && action === "status") return runStatus(args);
|