@kynver-app/runtime 0.1.19 → 0.1.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +436 -79
- package/dist/cli.js.map +4 -4
- package/dist/index.js +438 -79
- package/dist/index.js.map +4 -4
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -396,12 +396,12 @@ var DEFAULT_CRITICAL_FREE_BYTES = 15 * 1024 * 1024 * 1024;
|
|
|
396
396
|
var DEFAULT_MAX_USED_PERCENT = 80;
|
|
397
397
|
var DEFAULT_HARD_MAX_USED_PERCENT = 90;
|
|
398
398
|
function observeRunnerDiskGate(input = {}) {
|
|
399
|
-
const
|
|
399
|
+
const path18 = input.diskPath?.trim() || "/";
|
|
400
400
|
const warnBelowBytes = input.diskFreeWarnBytes ?? DEFAULT_WARN_FREE_BYTES;
|
|
401
401
|
const criticalBelowBytes = input.diskFreeCriticalBytes ?? DEFAULT_CRITICAL_FREE_BYTES;
|
|
402
402
|
const maxUsedPercent = input.diskMaxUsedPercent ?? DEFAULT_MAX_USED_PERCENT;
|
|
403
403
|
const hardMaxUsedPercent = input.diskHardMaxUsedPercent ?? DEFAULT_HARD_MAX_USED_PERCENT;
|
|
404
|
-
const stats = statfsSync(
|
|
404
|
+
const stats = statfsSync(path18);
|
|
405
405
|
const freeBytes = Number(stats.bavail) * Number(stats.bsize);
|
|
406
406
|
const totalBytes = Number(stats.blocks) * Number(stats.bsize);
|
|
407
407
|
const usedPercent = totalBytes > 0 ? (totalBytes - freeBytes) / totalBytes * 100 : 100;
|
|
@@ -421,7 +421,7 @@ function observeRunnerDiskGate(input = {}) {
|
|
|
421
421
|
}
|
|
422
422
|
return {
|
|
423
423
|
ok,
|
|
424
|
-
path:
|
|
424
|
+
path: path18,
|
|
425
425
|
freeBytes,
|
|
426
426
|
totalBytes,
|
|
427
427
|
usedPercent,
|
|
@@ -695,22 +695,36 @@ function gitIsAncestor(cwd, ancestor, descendant) {
|
|
|
695
695
|
if (res.status === 1) return { isAncestor: false, error: null };
|
|
696
696
|
return { isAncestor: null, error: res.error || res.stderr || res.stdout || `git exited ${res.status}` };
|
|
697
697
|
}
|
|
698
|
-
function computeGitAncestry(worktreePath,
|
|
698
|
+
function computeGitAncestry(worktreePath, baseOrOptions = "origin/main") {
|
|
699
|
+
const options = typeof baseOrOptions === "string" ? { base: baseOrOptions } : baseOrOptions;
|
|
700
|
+
const baseLabel = options.baseCommit?.trim() || options.base?.trim() || "origin/main";
|
|
701
|
+
const pinnedBaseCommit = options.baseCommit?.trim() || null;
|
|
699
702
|
if (!worktreePath) {
|
|
700
|
-
return unknownAncestry(
|
|
703
|
+
return unknownAncestry(baseLabel, "missing worktree path");
|
|
701
704
|
}
|
|
702
705
|
const head = gitCapture(worktreePath, ["rev-parse", "HEAD"]);
|
|
703
|
-
if (head.status !== 0)
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
706
|
+
if (head.status !== 0) {
|
|
707
|
+
return unknownAncestry(baseLabel, head.error || head.stderr || head.stdout || "failed to resolve HEAD");
|
|
708
|
+
}
|
|
709
|
+
let baseSha;
|
|
710
|
+
if (pinnedBaseCommit) {
|
|
711
|
+
baseSha = pinnedBaseCommit;
|
|
712
|
+
} else {
|
|
713
|
+
const baseHead = gitCapture(worktreePath, ["rev-parse", baseLabel]);
|
|
714
|
+
if (baseHead.status !== 0) {
|
|
715
|
+
return unknownAncestry(
|
|
716
|
+
baseLabel,
|
|
717
|
+
baseHead.error || baseHead.stderr || baseHead.stdout || `failed to resolve ${baseLabel}`,
|
|
718
|
+
head.stdout.trim()
|
|
719
|
+
);
|
|
720
|
+
}
|
|
721
|
+
baseSha = baseHead.stdout.trim();
|
|
707
722
|
}
|
|
708
723
|
const headSha = head.stdout.trim();
|
|
709
|
-
const baseSha = baseHead.stdout.trim();
|
|
710
724
|
if (headSha === baseSha) {
|
|
711
725
|
return {
|
|
712
726
|
checked: true,
|
|
713
|
-
base,
|
|
727
|
+
base: baseLabel,
|
|
714
728
|
head: headSha,
|
|
715
729
|
baseHead: baseSha,
|
|
716
730
|
baseIsAncestorOfHead: true,
|
|
@@ -724,7 +738,7 @@ function computeGitAncestry(worktreePath, base = "origin/main") {
|
|
|
724
738
|
if (baseIsAncestorOfHead.isAncestor == null || headIsAncestorOfBase.isAncestor == null) {
|
|
725
739
|
return {
|
|
726
740
|
checked: false,
|
|
727
|
-
base,
|
|
741
|
+
base: baseLabel,
|
|
728
742
|
head: headSha,
|
|
729
743
|
baseHead: baseSha,
|
|
730
744
|
baseIsAncestorOfHead: baseIsAncestorOfHead.isAncestor,
|
|
@@ -736,7 +750,7 @@ function computeGitAncestry(worktreePath, base = "origin/main") {
|
|
|
736
750
|
const relation = baseIsAncestorOfHead.isAncestor ? "ahead" : headIsAncestorOfBase.isAncestor ? "merged" : "diverged";
|
|
737
751
|
return {
|
|
738
752
|
checked: true,
|
|
739
|
-
base,
|
|
753
|
+
base: baseLabel,
|
|
740
754
|
head: headSha,
|
|
741
755
|
baseHead: baseSha,
|
|
742
756
|
baseIsAncestorOfHead: baseIsAncestorOfHead.isAncestor,
|
|
@@ -763,12 +777,74 @@ function scrubClaudeEnv(env) {
|
|
|
763
777
|
return next;
|
|
764
778
|
}
|
|
765
779
|
|
|
780
|
+
// src/landing-gate.ts
|
|
781
|
+
function trimOrNull(value) {
|
|
782
|
+
if (typeof value !== "string") return null;
|
|
783
|
+
const trimmed = value.trim();
|
|
784
|
+
return trimmed.length ? trimmed : null;
|
|
785
|
+
}
|
|
786
|
+
function hasFinalResult(value) {
|
|
787
|
+
if (value === void 0 || value === null) return false;
|
|
788
|
+
if (typeof value === "string") return value.trim().length > 0;
|
|
789
|
+
if (typeof value === "boolean") return value;
|
|
790
|
+
if (Array.isArray(value)) return value.length > 0;
|
|
791
|
+
if (typeof value === "object") return Object.keys(value).length > 0;
|
|
792
|
+
return true;
|
|
793
|
+
}
|
|
794
|
+
function hasCommittedLandingRef(snapshot) {
|
|
795
|
+
if (trimOrNull(snapshot.headCommit)) return true;
|
|
796
|
+
if (trimOrNull(snapshot.prUrl)) return true;
|
|
797
|
+
if (trimOrNull(snapshot.artifactBundlePath)) return true;
|
|
798
|
+
if (trimOrNull(snapshot.patchPath)) return true;
|
|
799
|
+
const ancestry = snapshot.gitAncestry;
|
|
800
|
+
if (ancestry?.checked && ancestry.headIsAncestorOfBase === false && trimOrNull(ancestry.head)) {
|
|
801
|
+
return true;
|
|
802
|
+
}
|
|
803
|
+
return false;
|
|
804
|
+
}
|
|
805
|
+
function assessWorkerLanding(snapshot) {
|
|
806
|
+
if (!hasFinalResult(snapshot.finalResult)) return { blocked: false };
|
|
807
|
+
if (snapshot.changedFiles.length === 0) return { blocked: false };
|
|
808
|
+
if (!hasCommittedLandingRef(snapshot)) {
|
|
809
|
+
return {
|
|
810
|
+
blocked: true,
|
|
811
|
+
reason: "dirty_worktree_no_pr",
|
|
812
|
+
detail: `Worktree has ${snapshot.changedFiles.length} uncommitted change(s) with no commit or PR; commit, open a PR, or discard before landing`
|
|
813
|
+
};
|
|
814
|
+
}
|
|
815
|
+
return {
|
|
816
|
+
blocked: true,
|
|
817
|
+
detail: `Worktree has ${snapshot.changedFiles.length} uncommitted change(s); commit or discard before landing`
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
function landingAttentionReason(verdict) {
|
|
821
|
+
if (!verdict.blocked) return void 0;
|
|
822
|
+
return verdict.detail ?? verdict.reason ?? "dirty_worktree_no_pr";
|
|
823
|
+
}
|
|
824
|
+
|
|
766
825
|
// src/status.ts
|
|
767
826
|
var NO_START_MS = 18e4;
|
|
768
827
|
var STALE_MS = 6e5;
|
|
769
828
|
function computeAttention(input) {
|
|
770
829
|
const now = Date.now();
|
|
771
|
-
if (input.
|
|
830
|
+
if (input.completionBlocker) {
|
|
831
|
+
return { state: "blocked", reason: input.completionBlocker };
|
|
832
|
+
}
|
|
833
|
+
if (input.finalResult) {
|
|
834
|
+
const landing = assessWorkerLanding({
|
|
835
|
+
finalResult: input.finalResult,
|
|
836
|
+
changedFiles: input.changedFiles ?? [],
|
|
837
|
+
gitAncestry: input.gitAncestry ?? null
|
|
838
|
+
});
|
|
839
|
+
if (landing.blocked) {
|
|
840
|
+
const detail = landingAttentionReason(landing);
|
|
841
|
+
return {
|
|
842
|
+
state: "needs_attention",
|
|
843
|
+
reason: landing.reason ? `landing blocked (${landing.reason}): ${detail}` : `landing blocked: ${detail}`
|
|
844
|
+
};
|
|
845
|
+
}
|
|
846
|
+
return { state: "done", reason: "final result recorded" };
|
|
847
|
+
}
|
|
772
848
|
if (!input.alive) {
|
|
773
849
|
const classified = classifyExitFailure(input.error);
|
|
774
850
|
if (classified) return { state: "blocked", reason: classified.reason };
|
|
@@ -799,7 +875,10 @@ function computeWorkerStatus(worker, options = {}) {
|
|
|
799
875
|
const stderrBytes = fileSize(worker.stderrPath);
|
|
800
876
|
const heartbeatBytes = fileSize(worker.heartbeatPath);
|
|
801
877
|
const changedFiles = gitStatusShort(worker.worktreePath);
|
|
802
|
-
const gitAncestry = computeGitAncestry(worker.worktreePath,
|
|
878
|
+
const gitAncestry = computeGitAncestry(worker.worktreePath, {
|
|
879
|
+
base: options.base,
|
|
880
|
+
baseCommit: options.baseCommit
|
|
881
|
+
});
|
|
803
882
|
const lastActivityAt = latestIso([
|
|
804
883
|
parsed.lastEventAt,
|
|
805
884
|
heartbeat.lastHeartbeatAt,
|
|
@@ -808,6 +887,7 @@ function computeWorkerStatus(worker, options = {}) {
|
|
|
808
887
|
fileMtime(worker.heartbeatPath)
|
|
809
888
|
]);
|
|
810
889
|
const error = parsed.error || (!alive && !parsed.finalResult ? tailFile(worker.stderrPath, 10).trim() || void 0 : void 0);
|
|
890
|
+
const completionBlocker = typeof worker.completionBlocker === "string" && worker.completionBlocker.trim() ? worker.completionBlocker.trim() : null;
|
|
811
891
|
const attention = computeAttention({
|
|
812
892
|
alive,
|
|
813
893
|
finalResult: parsed.finalResult,
|
|
@@ -817,14 +897,18 @@ function computeWorkerStatus(worker, options = {}) {
|
|
|
817
897
|
lastActivityAt,
|
|
818
898
|
heartbeatBlocker: heartbeat.heartbeatBlocker,
|
|
819
899
|
startedAt: worker.startedAt,
|
|
820
|
-
error
|
|
900
|
+
error,
|
|
901
|
+
changedFiles,
|
|
902
|
+
gitAncestry,
|
|
903
|
+
completionBlocker
|
|
821
904
|
});
|
|
905
|
+
const workerStatusLabel = completionBlocker || attention.state === "blocked" ? "blocked" : attention.state === "done" ? "done" : parsed.finalResult ? "exited" : alive ? "running" : "exited";
|
|
822
906
|
return {
|
|
823
907
|
runId: worker.runId,
|
|
824
908
|
worker: worker.name,
|
|
825
909
|
pid: worker.pid,
|
|
826
910
|
alive,
|
|
827
|
-
status:
|
|
911
|
+
status: workerStatusLabel,
|
|
828
912
|
attention,
|
|
829
913
|
branch: worker.branch,
|
|
830
914
|
worktreePath: worker.worktreePath,
|
|
@@ -853,6 +937,10 @@ function isFinishedWorkerStatus(status) {
|
|
|
853
937
|
if (status.status === "exited" || status.status === "done") return true;
|
|
854
938
|
return false;
|
|
855
939
|
}
|
|
940
|
+
function isLandingBlockedWorkerStatus(status) {
|
|
941
|
+
if (!status.finalResult) return false;
|
|
942
|
+
return status.attention.state === "needs_attention" || status.attention.state === "blocked";
|
|
943
|
+
}
|
|
856
944
|
function deriveRunStatus(fallback, workers) {
|
|
857
945
|
if (workers.length === 0) return fallback;
|
|
858
946
|
if (workers.some((w) => w.attention === "needs_attention" || w.attention === "stale" || w.attention === "blocked")) {
|
|
@@ -903,6 +991,10 @@ function readAvailableMemBytes() {
|
|
|
903
991
|
}
|
|
904
992
|
return os.freemem();
|
|
905
993
|
}
|
|
994
|
+
function isActiveHarnessWorker(worker) {
|
|
995
|
+
const status = computeWorkerStatus(worker);
|
|
996
|
+
return status.alive && !status.finalResult && status.attention.state !== "done";
|
|
997
|
+
}
|
|
906
998
|
function countActiveWorkersForRun(run) {
|
|
907
999
|
let active = 0;
|
|
908
1000
|
for (const name of Object.keys(run.workers || {})) {
|
|
@@ -910,11 +1002,8 @@ function countActiveWorkersForRun(run) {
|
|
|
910
1002
|
path5.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
911
1003
|
void 0
|
|
912
1004
|
);
|
|
913
|
-
if (!worker) continue;
|
|
914
|
-
|
|
915
|
-
if (status.alive && !status.finalResult && status.attention.state !== "done") {
|
|
916
|
-
active++;
|
|
917
|
-
}
|
|
1005
|
+
if (!worker || !isActiveHarnessWorker(worker)) continue;
|
|
1006
|
+
active++;
|
|
918
1007
|
}
|
|
919
1008
|
return active;
|
|
920
1009
|
}
|
|
@@ -939,7 +1028,7 @@ function observeRunnerResourceGate(input) {
|
|
|
939
1028
|
const maxConcurrentWorkers = Math.max(0, Math.min(targetCap, capacityFromTotal));
|
|
940
1029
|
const slotsByCapacity = Math.max(0, maxConcurrentWorkers - activeWorkers);
|
|
941
1030
|
const slotsByFreeMem = capacityFromFree;
|
|
942
|
-
|
|
1031
|
+
let slotsAvailable = Math.min(slotsByCapacity, slotsByFreeMem);
|
|
943
1032
|
let reason = null;
|
|
944
1033
|
if (slotsAvailable <= 0) {
|
|
945
1034
|
if (activeWorkers >= maxConcurrentWorkers) {
|
|
@@ -966,39 +1055,6 @@ function observeRunnerResourceGate(input) {
|
|
|
966
1055
|
};
|
|
967
1056
|
}
|
|
968
1057
|
|
|
969
|
-
// src/supervisor.ts
|
|
970
|
-
import { existsSync as existsSync10, mkdirSync as mkdirSync3 } from "node:fs";
|
|
971
|
-
import path10 from "node:path";
|
|
972
|
-
|
|
973
|
-
// src/prompt.ts
|
|
974
|
-
function buildPrompt(input) {
|
|
975
|
-
const ownership = input.ownedPaths.length ? `Owned paths: ${input.ownedPaths.join(", ")}. Do not edit outside these paths without stopping and reporting why.` : "Owned paths: unrestricted for this worker, but keep edits tightly scoped.";
|
|
976
|
-
const progressLines = [
|
|
977
|
-
"Structured plan progress (required when planId is set):",
|
|
978
|
-
"- Harness checkpoints only: `kynver plan progress --plan <planId> --row <rowKey> --role implementer --status running|partial|blocked` (the by-id harness route rejects `done` and confirm events).",
|
|
979
|
-
"- When a slice is finished, emit `partial` with evidence (`--evidence pr:<url>`, `--evidence path:<file>`, or `--evidence command:<cmd>`). Do not propose or confirm row `done` from the worker CLI.",
|
|
980
|
-
"- Propose/confirm row `done` is MCP/session only: chat agents use `agent_os_plan_progress_event_append` on the slug route (implementer proposes with `proposed: true`; report_reviewer/deep_reviewer confirm with `proposed: false`).",
|
|
981
|
-
"- When blocked on operator/Ghost/runtime review, create a linked review task (MCP `agent_os_plan_review_task_create` or API) and pass `--review-task <taskId>`.",
|
|
982
|
-
"- Before the completion report: mark completion-report rows partial with evidence; do not skip report review.",
|
|
983
|
-
"- After implementation: wait for report_reviewer then deep_reviewer confirmation (via MCP/session agents) before follow-up rows close.",
|
|
984
|
-
input.planId ? `Active planId: ${input.planId}${input.taskId ? ` \xB7 taskId: ${input.taskId}` : ""}` : "No planId on this worker \u2014 still emit progress when you touch plan-scoped work."
|
|
985
|
-
];
|
|
986
|
-
return [
|
|
987
|
-
"You are running under the Kynver AgentOS runtime.",
|
|
988
|
-
"Immediately state your plan before editing.",
|
|
989
|
-
ownership,
|
|
990
|
-
`Worktree: ${input.worktreePath}`,
|
|
991
|
-
`Progress heartbeat file: ${input.heartbeatPath}`,
|
|
992
|
-
"After each major step, append one JSON line to the heartbeat file with fields: ts, phase, summary, changedFiles, blocker.",
|
|
993
|
-
"Final response must include files changed, verification commands, and unresolved risks.",
|
|
994
|
-
"",
|
|
995
|
-
...progressLines,
|
|
996
|
-
"",
|
|
997
|
-
"Task:",
|
|
998
|
-
input.task
|
|
999
|
-
].join("\n");
|
|
1000
|
-
}
|
|
1001
|
-
|
|
1002
1058
|
// src/providers/claude.ts
|
|
1003
1059
|
import { closeSync, openSync } from "node:fs";
|
|
1004
1060
|
import { spawn } from "node:child_process";
|
|
@@ -1057,7 +1113,7 @@ function preflightCursorModel(model, defaultModel) {
|
|
|
1057
1113
|
}
|
|
1058
1114
|
|
|
1059
1115
|
// src/providers/claude.ts
|
|
1060
|
-
var CLAUDE_DEFAULT_MODEL = "claude-
|
|
1116
|
+
var CLAUDE_DEFAULT_MODEL = "claude-sonnet-4-6";
|
|
1061
1117
|
var claudeProvider = {
|
|
1062
1118
|
name: "claude",
|
|
1063
1119
|
defaultModel: CLAUDE_DEFAULT_MODEL,
|
|
@@ -1103,6 +1159,159 @@ var claudeProvider = {
|
|
|
1103
1159
|
}
|
|
1104
1160
|
};
|
|
1105
1161
|
|
|
1162
|
+
// src/model-routing.ts
|
|
1163
|
+
var GLOBAL_DEFAULT_MODEL = "claude-sonnet-4-6";
|
|
1164
|
+
function taskString(task, key) {
|
|
1165
|
+
const v = task[key];
|
|
1166
|
+
return typeof v === "string" ? v.trim() : "";
|
|
1167
|
+
}
|
|
1168
|
+
function normalizeRef(ref) {
|
|
1169
|
+
return ref.toLowerCase();
|
|
1170
|
+
}
|
|
1171
|
+
function resolveGlobalDefaultModel(config = loadUserConfig()) {
|
|
1172
|
+
const fromConfig = config.defaultModel?.trim();
|
|
1173
|
+
if (fromConfig) return fromConfig;
|
|
1174
|
+
const fromEnv = process.env.KYNVER_DEFAULT_MODEL?.trim();
|
|
1175
|
+
if (fromEnv) return fromEnv;
|
|
1176
|
+
return GLOBAL_DEFAULT_MODEL;
|
|
1177
|
+
}
|
|
1178
|
+
function inferProviderFromModel(model) {
|
|
1179
|
+
const m = (model ?? "").toLowerCase();
|
|
1180
|
+
if (!m) return "claude";
|
|
1181
|
+
if (m.includes("composer") || m.includes("cursor") || m.includes("codex") || m.startsWith("gpt-") || m.startsWith("gpt5")) {
|
|
1182
|
+
return "cursor";
|
|
1183
|
+
}
|
|
1184
|
+
return "claude";
|
|
1185
|
+
}
|
|
1186
|
+
function isOpusLane(ref, title) {
|
|
1187
|
+
if (ref.includes("deep") && ref.includes("review")) return true;
|
|
1188
|
+
if (ref.includes("security")) return true;
|
|
1189
|
+
if (ref.includes("plan_author") || ref.includes("plan-author")) return true;
|
|
1190
|
+
if (title.includes("deep review") || title.includes("security review")) return true;
|
|
1191
|
+
if (ref.includes("plan") && !ref.includes("review") && (ref.includes("author") || ref.includes("strategy"))) {
|
|
1192
|
+
return true;
|
|
1193
|
+
}
|
|
1194
|
+
return false;
|
|
1195
|
+
}
|
|
1196
|
+
function inferModelRoutingFromTask(task) {
|
|
1197
|
+
const ref = normalizeRef(taskString(task, "executorRef"));
|
|
1198
|
+
const title = taskString(task, "title").toLowerCase();
|
|
1199
|
+
const priority = taskString(task, "priority") || "normal";
|
|
1200
|
+
const roleLane = normalizeRef(taskString(task, "roleLane"));
|
|
1201
|
+
if (ref.includes("cursor") || ref.includes("codex") || ref.includes("composer") || ref.includes("copilot") || roleLane === "implementer" || roleLane === "repair_implementer") {
|
|
1202
|
+
return { provider: "cursor", rule: "lane:implementation" };
|
|
1203
|
+
}
|
|
1204
|
+
if (ref.includes("landing") || title.startsWith("land:") || title.includes(" merge")) {
|
|
1205
|
+
return {
|
|
1206
|
+
model: "claude-haiku-4-5-20251001",
|
|
1207
|
+
provider: "claude",
|
|
1208
|
+
rule: "lane:landing"
|
|
1209
|
+
};
|
|
1210
|
+
}
|
|
1211
|
+
if (ref.includes("review") || title.startsWith("review ") || roleLane.includes("review")) {
|
|
1212
|
+
if (isOpusLane(ref, title) || roleLane === "deep_reviewer") {
|
|
1213
|
+
return { model: "claude-opus-4-7", provider: "claude", rule: "lane:deep_review" };
|
|
1214
|
+
}
|
|
1215
|
+
return { model: "claude-sonnet-4-6", provider: "claude", rule: "lane:review" };
|
|
1216
|
+
}
|
|
1217
|
+
if (isOpusLane(ref, title) || roleLane === "plan_author") {
|
|
1218
|
+
return { model: "claude-opus-4-7", provider: "claude", rule: "lane:planning" };
|
|
1219
|
+
}
|
|
1220
|
+
if (priority === "critical") {
|
|
1221
|
+
return { model: "claude-opus-4-7", provider: "claude", rule: "priority:critical" };
|
|
1222
|
+
}
|
|
1223
|
+
if (priority === "low") {
|
|
1224
|
+
return {
|
|
1225
|
+
model: "claude-haiku-4-5-20251001",
|
|
1226
|
+
provider: "claude",
|
|
1227
|
+
rule: "priority:low"
|
|
1228
|
+
};
|
|
1229
|
+
}
|
|
1230
|
+
return {
|
|
1231
|
+
model: resolveGlobalDefaultModel(),
|
|
1232
|
+
provider: "claude",
|
|
1233
|
+
rule: "default:sonnet"
|
|
1234
|
+
};
|
|
1235
|
+
}
|
|
1236
|
+
function resolveWorkerLaunch(input) {
|
|
1237
|
+
if (input.explicitModel?.trim()) {
|
|
1238
|
+
const model2 = input.explicitModel.trim();
|
|
1239
|
+
return {
|
|
1240
|
+
model: model2,
|
|
1241
|
+
provider: input.explicitProvider?.trim() || inferProviderFromModel(model2),
|
|
1242
|
+
rule: "explicit:cli",
|
|
1243
|
+
requestedModel: model2
|
|
1244
|
+
};
|
|
1245
|
+
}
|
|
1246
|
+
if (input.task && Object.keys(input.task).length > 0) {
|
|
1247
|
+
const inferred = inferModelRoutingFromTask(input.task);
|
|
1248
|
+
return {
|
|
1249
|
+
...inferred,
|
|
1250
|
+
requestedModel: inferred.model
|
|
1251
|
+
};
|
|
1252
|
+
}
|
|
1253
|
+
const model = resolveGlobalDefaultModel();
|
|
1254
|
+
return {
|
|
1255
|
+
model,
|
|
1256
|
+
provider: input.explicitProvider?.trim() || inferProviderFromModel(model),
|
|
1257
|
+
rule: "default:global",
|
|
1258
|
+
requestedModel: model
|
|
1259
|
+
};
|
|
1260
|
+
}
|
|
1261
|
+
function resolveModelFallback(startedModel, launchModel, providerDefault) {
|
|
1262
|
+
return startedModel || launchModel || providerDefault || resolveGlobalDefaultModel() || CLAUDE_DEFAULT_MODEL;
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
// src/retry-limits.ts
|
|
1266
|
+
function positiveInt2(value, fallback) {
|
|
1267
|
+
const n = Number(value);
|
|
1268
|
+
if (!Number.isFinite(n) || n <= 0) return fallback;
|
|
1269
|
+
return Math.floor(n);
|
|
1270
|
+
}
|
|
1271
|
+
function readHarnessRetryLimits() {
|
|
1272
|
+
return {
|
|
1273
|
+
maxTaskAttempts: positiveInt2(process.env.KYNVER_MAX_TASK_ATTEMPTS, 3),
|
|
1274
|
+
dispatchCooldownMs: positiveInt2(process.env.KYNVER_DISPATCH_COOLDOWN_MS, 5e3)
|
|
1275
|
+
};
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
// src/supervisor.ts
|
|
1279
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync3 } from "node:fs";
|
|
1280
|
+
import path10 from "node:path";
|
|
1281
|
+
|
|
1282
|
+
// src/prompt.ts
|
|
1283
|
+
function buildPrompt(input) {
|
|
1284
|
+
const ownership = input.ownedPaths.length ? `Owned paths: ${input.ownedPaths.join(", ")}. Do not edit outside these paths without stopping and reporting why.` : "Owned paths: unrestricted for this worker, but keep edits tightly scoped.";
|
|
1285
|
+
const compact = Boolean(input.model?.toLowerCase().includes("haiku"));
|
|
1286
|
+
const progressLines = compact ? [
|
|
1287
|
+
"Plan progress: when planId is set, use `kynver plan progress` for running|partial|blocked only; row `done` is MCP/session only.",
|
|
1288
|
+
input.planId ? `Active planId: ${input.planId}` : "No planId on this worker."
|
|
1289
|
+
] : [
|
|
1290
|
+
"Structured plan progress (required when planId is set):",
|
|
1291
|
+
"- Harness checkpoints only: `kynver plan progress --plan <planId> --row <rowKey> --role implementer --status running|partial|blocked` (the by-id harness route rejects `done` and confirm events).",
|
|
1292
|
+
"- When a slice is finished, emit `partial` with evidence (`--evidence pr:<url>`, `--evidence path:<file>`, or `--evidence command:<cmd>`). Do not propose or confirm row `done` from the worker CLI.",
|
|
1293
|
+
"- Propose/confirm row `done` is MCP/session only: chat agents use `agent_os_plan_progress_event_append` on the slug route (implementer proposes with `proposed: true`; report_reviewer/deep_reviewer confirm with `proposed: false`).",
|
|
1294
|
+
"- When blocked on operator/Ghost/runtime review, create a linked review task (MCP `agent_os_plan_review_task_create` or API) and pass `--review-task <taskId>`.",
|
|
1295
|
+
"- Before the completion report: mark completion-report rows partial with evidence; do not skip report review.",
|
|
1296
|
+
"- After implementation: wait for report_reviewer then deep_reviewer confirmation (via MCP/session agents) before follow-up rows close.",
|
|
1297
|
+
input.planId ? `Active planId: ${input.planId}${input.taskId ? ` \xB7 taskId: ${input.taskId}` : ""}` : "No planId on this worker \u2014 still emit progress when you touch plan-scoped work."
|
|
1298
|
+
];
|
|
1299
|
+
return [
|
|
1300
|
+
"You are running under the Kynver AgentOS runtime.",
|
|
1301
|
+
"Immediately state your plan before editing.",
|
|
1302
|
+
ownership,
|
|
1303
|
+
`Worktree: ${input.worktreePath}`,
|
|
1304
|
+
`Progress heartbeat file: ${input.heartbeatPath}`,
|
|
1305
|
+
"After each major step, append one JSON line to the heartbeat file with fields: ts, phase, summary, changedFiles, blocker.",
|
|
1306
|
+
"Final response must include files changed, verification commands, and unresolved risks.",
|
|
1307
|
+
"",
|
|
1308
|
+
...progressLines,
|
|
1309
|
+
"",
|
|
1310
|
+
"Task:",
|
|
1311
|
+
input.task
|
|
1312
|
+
].join("\n");
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1106
1315
|
// src/providers/cursor.ts
|
|
1107
1316
|
import { closeSync as closeSync2, existsSync as existsSync8, openSync as openSync2 } from "node:fs";
|
|
1108
1317
|
import { spawn as spawn2 } from "node:child_process";
|
|
@@ -1306,9 +1515,13 @@ function persistCompletionBlocker(worker, reason) {
|
|
|
1306
1515
|
else delete worker.completionBlocker;
|
|
1307
1516
|
saveWorker(worker.runId, worker);
|
|
1308
1517
|
}
|
|
1518
|
+
function workerStatusOptions(run) {
|
|
1519
|
+
return run ? { base: run.base, baseCommit: run.baseCommit } : {};
|
|
1520
|
+
}
|
|
1309
1521
|
async function tryCompleteWorker(args) {
|
|
1310
1522
|
const worker = loadWorker(String(args.run), String(args.name));
|
|
1311
|
-
const
|
|
1523
|
+
const run = loadRun(worker.runId);
|
|
1524
|
+
const status = computeWorkerStatus(worker, workerStatusOptions(run));
|
|
1312
1525
|
const agentOsId = (args.agentOsId ? String(args.agentOsId) : worker.agentOsId) || "";
|
|
1313
1526
|
const taskId = (args.taskId ? String(args.taskId) : worker.taskId) || null;
|
|
1314
1527
|
if (!agentOsId) {
|
|
@@ -1352,7 +1565,8 @@ async function tryCompleteWorker(args) {
|
|
|
1352
1565
|
async function completeWorker(args) {
|
|
1353
1566
|
try {
|
|
1354
1567
|
const worker = loadWorker(String(args.run), String(args.name));
|
|
1355
|
-
const
|
|
1568
|
+
const run = loadRun(worker.runId);
|
|
1569
|
+
const status = computeWorkerStatus(worker, workerStatusOptions(run));
|
|
1356
1570
|
const agentOsId = (args.agentOsId ? String(args.agentOsId) : worker.agentOsId) || "";
|
|
1357
1571
|
const taskId = (args.taskId ? String(args.taskId) : worker.taskId) || null;
|
|
1358
1572
|
if (!agentOsId) {
|
|
@@ -1399,7 +1613,8 @@ async function completeWorker(args) {
|
|
|
1399
1613
|
}
|
|
1400
1614
|
function workerStatus(args) {
|
|
1401
1615
|
const worker = loadWorker(String(args.run), String(args.name));
|
|
1402
|
-
const
|
|
1616
|
+
const run = loadRun(worker.runId);
|
|
1617
|
+
const status = computeWorkerStatus(worker, workerStatusOptions(run));
|
|
1403
1618
|
writeJson(path8.join(worker.workerDir, "last-status.json"), status);
|
|
1404
1619
|
console.log(JSON.stringify(status, null, 2));
|
|
1405
1620
|
}
|
|
@@ -1414,14 +1629,21 @@ function runStatus(args) {
|
|
|
1414
1629
|
if (!worker) {
|
|
1415
1630
|
return { worker: name, status: "missing", attention: "needs_attention", attentionReason: "worker.json not found" };
|
|
1416
1631
|
}
|
|
1417
|
-
const status = computeWorkerStatus(worker, {
|
|
1632
|
+
const status = computeWorkerStatus(worker, {
|
|
1633
|
+
base: run.base,
|
|
1634
|
+
baseCommit: run.baseCommit
|
|
1635
|
+
});
|
|
1636
|
+
const headCommit = status.gitAncestry.headIsAncestorOfBase === false && status.gitAncestry.head ? status.gitAncestry.head : void 0;
|
|
1418
1637
|
const rawBlocker = worker.completionBlocker;
|
|
1419
1638
|
const completionBlocker = typeof rawBlocker === "string" && rawBlocker ? rawBlocker : void 0;
|
|
1639
|
+
const boardStatus = completionBlocker ? "blocked" : status.status;
|
|
1640
|
+
const boardAttention = completionBlocker ? "blocked" : status.attention.state;
|
|
1420
1641
|
return {
|
|
1421
1642
|
worker: status.worker,
|
|
1422
|
-
status:
|
|
1423
|
-
attention:
|
|
1643
|
+
status: boardStatus,
|
|
1644
|
+
attention: boardAttention,
|
|
1424
1645
|
attentionReason: completionBlocker ?? status.attention.reason,
|
|
1646
|
+
landingBlocked: status.finalResult ? boardAttention === "needs_attention" || boardAttention === "blocked" : false,
|
|
1425
1647
|
pid: status.pid,
|
|
1426
1648
|
alive: status.alive,
|
|
1427
1649
|
currentTool: status.currentTool,
|
|
@@ -1430,7 +1652,14 @@ function runStatus(args) {
|
|
|
1430
1652
|
lastHeartbeatSummary: status.lastHeartbeatSummary,
|
|
1431
1653
|
heartbeatBlocker: status.heartbeatBlocker,
|
|
1432
1654
|
changedFileCount: status.changedFiles.length,
|
|
1655
|
+
changedFiles: status.changedFiles,
|
|
1433
1656
|
branch: status.branch,
|
|
1657
|
+
model: typeof worker.model === "string" ? worker.model : void 0,
|
|
1658
|
+
routingRule: typeof worker.routingRule === "string" ? worker.routingRule : void 0,
|
|
1659
|
+
requestedModel: typeof worker.requestedModel === "string" ? worker.requestedModel : void 0,
|
|
1660
|
+
headCommit,
|
|
1661
|
+
gitAncestry: status.gitAncestry,
|
|
1662
|
+
finalResult: status.finalResult,
|
|
1434
1663
|
ancestry: status.gitAncestry.relation,
|
|
1435
1664
|
ancestryChecked: status.gitAncestry.checked
|
|
1436
1665
|
};
|
|
@@ -1646,8 +1875,17 @@ function spawnWorkerProcess(run, opts) {
|
|
|
1646
1875
|
const name = safeSlug(rawName);
|
|
1647
1876
|
if (run.workers?.[name]) throw new Error(`worker already exists in run ${run.id}: ${name}`);
|
|
1648
1877
|
if (!opts.task) throw new Error(`missing task text for worker ${name}`);
|
|
1649
|
-
const
|
|
1650
|
-
|
|
1878
|
+
const routing = opts.routingRule || opts.requestedModel ? {
|
|
1879
|
+
provider: opts.provider || "claude",
|
|
1880
|
+
model: opts.model,
|
|
1881
|
+
rule: opts.routingRule || "explicit:spawn",
|
|
1882
|
+
requestedModel: opts.requestedModel ?? opts.model
|
|
1883
|
+
} : resolveWorkerLaunch({
|
|
1884
|
+
explicitModel: opts.model,
|
|
1885
|
+
explicitProvider: opts.provider
|
|
1886
|
+
});
|
|
1887
|
+
const provider = resolveWorkerProvider(routing.provider);
|
|
1888
|
+
let launchModel = routing.model;
|
|
1651
1889
|
if (provider.preflightModel) {
|
|
1652
1890
|
const preflight = provider.preflightModel(opts.model);
|
|
1653
1891
|
if (!preflight.ok) {
|
|
@@ -1675,7 +1913,10 @@ function spawnWorkerProcess(run, opts) {
|
|
|
1675
1913
|
task: opts.task,
|
|
1676
1914
|
ownedPaths: opts.ownedPaths || [],
|
|
1677
1915
|
worktreePath,
|
|
1678
|
-
heartbeatPath
|
|
1916
|
+
heartbeatPath,
|
|
1917
|
+
planId: opts.planId,
|
|
1918
|
+
taskId: opts.taskId,
|
|
1919
|
+
model: launchModel
|
|
1679
1920
|
});
|
|
1680
1921
|
let started;
|
|
1681
1922
|
try {
|
|
@@ -1697,7 +1938,7 @@ function spawnWorkerProcess(run, opts) {
|
|
|
1697
1938
|
git(run.repo, ["branch", "-D", branch], { allowFailure: true });
|
|
1698
1939
|
throw error;
|
|
1699
1940
|
}
|
|
1700
|
-
const model = started.model
|
|
1941
|
+
const model = resolveModelFallback(started.model, launchModel, provider.defaultModel);
|
|
1701
1942
|
const worker = {
|
|
1702
1943
|
name,
|
|
1703
1944
|
runId: run.id,
|
|
@@ -1716,6 +1957,8 @@ function spawnWorkerProcess(run, opts) {
|
|
|
1716
1957
|
...opts.planId ? { planId: String(opts.planId) } : {},
|
|
1717
1958
|
...opts.leaseOwner ? { leaseOwner: String(opts.leaseOwner) } : {},
|
|
1718
1959
|
...opts.dispatched ? { dispatched: true } : {},
|
|
1960
|
+
routingRule: routing.rule,
|
|
1961
|
+
...routing.requestedModel ? { requestedModel: routing.requestedModel } : {},
|
|
1719
1962
|
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1720
1963
|
};
|
|
1721
1964
|
saveWorker(run.id, worker);
|
|
@@ -1723,14 +1966,23 @@ function spawnWorkerProcess(run, opts) {
|
|
|
1723
1966
|
run.status = "running";
|
|
1724
1967
|
saveRun(run);
|
|
1725
1968
|
if (worker.agentOsId && worker.taskId) {
|
|
1969
|
+
let sidecarSpawned;
|
|
1726
1970
|
try {
|
|
1727
|
-
spawnCompletionSidecar({
|
|
1971
|
+
sidecarSpawned = spawnCompletionSidecar({
|
|
1728
1972
|
runId: run.id,
|
|
1729
1973
|
workerName: name,
|
|
1730
1974
|
workerDir,
|
|
1731
1975
|
agentOsId: worker.agentOsId
|
|
1732
1976
|
});
|
|
1733
|
-
} catch {
|
|
1977
|
+
} catch (error) {
|
|
1978
|
+
const reason = `completion sidecar failed to spawn: ${error.message}`;
|
|
1979
|
+
worker.completionBlocker = reason;
|
|
1980
|
+
saveWorker(run.id, worker);
|
|
1981
|
+
}
|
|
1982
|
+
if (!sidecarSpawned) {
|
|
1983
|
+
const reason = "completion sidecar failed to spawn (CLI not found or spawn error)";
|
|
1984
|
+
worker.completionBlocker = reason;
|
|
1985
|
+
saveWorker(run.id, worker);
|
|
1734
1986
|
}
|
|
1735
1987
|
}
|
|
1736
1988
|
return worker;
|
|
@@ -1865,17 +2117,34 @@ async function dispatchRun(args) {
|
|
|
1865
2117
|
console.log(JSON.stringify(summary2, null, 2));
|
|
1866
2118
|
return;
|
|
1867
2119
|
}
|
|
2120
|
+
const retryLimits = readHarnessRetryLimits();
|
|
1868
2121
|
const outcomes = [];
|
|
1869
2122
|
for (const decision of result.started) {
|
|
1870
2123
|
const task = decision.task;
|
|
2124
|
+
const attempt = Number(task.attempt) || 1;
|
|
2125
|
+
if (attempt > retryLimits.maxTaskAttempts) {
|
|
2126
|
+
outcomes.push({
|
|
2127
|
+
taskId: task.id,
|
|
2128
|
+
started: false,
|
|
2129
|
+
error: `task attempt ${attempt} exceeds KYNVER_MAX_TASK_ATTEMPTS (${retryLimits.maxTaskAttempts})`
|
|
2130
|
+
});
|
|
2131
|
+
continue;
|
|
2132
|
+
}
|
|
1871
2133
|
const name = safeSlug(`t-${task.id}-a${task.attempt}`);
|
|
2134
|
+
const routing = resolveWorkerLaunch({
|
|
2135
|
+
explicitModel: args.model ? String(args.model) : void 0,
|
|
2136
|
+
task
|
|
2137
|
+
});
|
|
1872
2138
|
try {
|
|
1873
2139
|
const planId = task.planId ? String(task.planId) : void 0;
|
|
1874
2140
|
const worker = spawnWorkerProcess(run, {
|
|
1875
2141
|
name,
|
|
1876
2142
|
task: buildDispatchTaskText(task, agentOsId),
|
|
1877
2143
|
ownedPaths: args.owned ? String(args.owned).split(",").map((s) => s.trim()).filter(Boolean) : [],
|
|
1878
|
-
model:
|
|
2144
|
+
model: routing.model,
|
|
2145
|
+
provider: routing.provider,
|
|
2146
|
+
routingRule: routing.rule,
|
|
2147
|
+
requestedModel: routing.requestedModel,
|
|
1879
2148
|
agentOsId,
|
|
1880
2149
|
taskId: String(task.id),
|
|
1881
2150
|
planId,
|
|
@@ -1887,7 +2156,10 @@ async function dispatchRun(args) {
|
|
|
1887
2156
|
started: true,
|
|
1888
2157
|
worker: worker.name,
|
|
1889
2158
|
pid: worker.pid,
|
|
1890
|
-
branch: worker.branch
|
|
2159
|
+
branch: worker.branch,
|
|
2160
|
+
model: worker.model,
|
|
2161
|
+
provider: routing.provider,
|
|
2162
|
+
routingRule: routing.rule
|
|
1891
2163
|
});
|
|
1892
2164
|
} catch (error) {
|
|
1893
2165
|
const releaseUrl = `${base}/api/agent-os/by-id/${encodeURIComponent(agentOsId)}/tasks/${encodeURIComponent(String(task.id))}/release`;
|
|
@@ -2045,7 +2317,10 @@ function failExists(message) {
|
|
|
2045
2317
|
}
|
|
2046
2318
|
|
|
2047
2319
|
// src/pipeline-tick.ts
|
|
2048
|
-
import
|
|
2320
|
+
import path17 from "node:path";
|
|
2321
|
+
|
|
2322
|
+
// src/stale-reconcile.ts
|
|
2323
|
+
import path15 from "node:path";
|
|
2049
2324
|
|
|
2050
2325
|
// src/finalize.ts
|
|
2051
2326
|
import path14 from "node:path";
|
|
@@ -2056,13 +2331,17 @@ function terminalStatusFor(run) {
|
|
|
2056
2331
|
let anyAlive = false;
|
|
2057
2332
|
let anyResult = false;
|
|
2058
2333
|
let anyCompletionBlocked = false;
|
|
2334
|
+
let anyLandingBlocked = false;
|
|
2059
2335
|
for (const name of names) {
|
|
2060
2336
|
const worker = readJson(
|
|
2061
2337
|
path14.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
2062
2338
|
void 0
|
|
2063
2339
|
);
|
|
2064
2340
|
if (!worker) continue;
|
|
2065
|
-
const status = computeWorkerStatus(worker
|
|
2341
|
+
const status = computeWorkerStatus(worker, {
|
|
2342
|
+
base: run.base,
|
|
2343
|
+
baseCommit: run.baseCommit
|
|
2344
|
+
});
|
|
2066
2345
|
if (status.alive && !status.finalResult) {
|
|
2067
2346
|
anyAlive = true;
|
|
2068
2347
|
break;
|
|
@@ -2070,10 +2349,14 @@ function terminalStatusFor(run) {
|
|
|
2070
2349
|
if (typeof worker.completionBlocker === "string" && worker.completionBlocker) {
|
|
2071
2350
|
anyCompletionBlocked = true;
|
|
2072
2351
|
}
|
|
2073
|
-
if (status
|
|
2352
|
+
if (isLandingBlockedWorkerStatus(status)) {
|
|
2353
|
+
anyLandingBlocked = true;
|
|
2354
|
+
}
|
|
2355
|
+
if (status.finalResult && status.attention.state === "done") anyResult = true;
|
|
2074
2356
|
}
|
|
2075
2357
|
if (anyAlive) return null;
|
|
2076
2358
|
if (anyCompletionBlocked) return null;
|
|
2359
|
+
if (anyLandingBlocked) return null;
|
|
2077
2360
|
return anyResult ? "completed" : "failed";
|
|
2078
2361
|
}
|
|
2079
2362
|
function finalizeStaleRuns() {
|
|
@@ -2090,8 +2373,82 @@ function finalizeStaleRuns() {
|
|
|
2090
2373
|
return finalized;
|
|
2091
2374
|
}
|
|
2092
2375
|
|
|
2376
|
+
// src/stale-reconcile.ts
|
|
2377
|
+
var STALE_RECONCILE_HEARTBEAT_MS = 15 * 60 * 1e3;
|
|
2378
|
+
function staleReconcileDisabled() {
|
|
2379
|
+
return process.env.KYNVER_NO_STALE_CLEANUP === "1";
|
|
2380
|
+
}
|
|
2381
|
+
function reconcileStaleWorkers() {
|
|
2382
|
+
if (staleReconcileDisabled()) {
|
|
2383
|
+
return { workers: [], finalizedRuns: finalizeStaleRuns() };
|
|
2384
|
+
}
|
|
2385
|
+
const outcomes = [];
|
|
2386
|
+
const now = Date.now();
|
|
2387
|
+
for (const run of listRunRecords()) {
|
|
2388
|
+
for (const name of Object.keys(run.workers || {})) {
|
|
2389
|
+
const workerPath = path15.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json");
|
|
2390
|
+
const worker = readJson(workerPath, void 0);
|
|
2391
|
+
if (!worker || worker.status !== "running") {
|
|
2392
|
+
outcomes.push({
|
|
2393
|
+
runId: run.id,
|
|
2394
|
+
worker: name,
|
|
2395
|
+
action: "skipped",
|
|
2396
|
+
reason: worker ? `worker status is ${worker.status}` : "worker.json missing"
|
|
2397
|
+
});
|
|
2398
|
+
continue;
|
|
2399
|
+
}
|
|
2400
|
+
const status = computeWorkerStatus(worker, { base: run.base, baseCommit: run.baseCommit });
|
|
2401
|
+
if (status.finalResult) {
|
|
2402
|
+
outcomes.push({ runId: run.id, worker: name, action: "skipped", reason: "final result present" });
|
|
2403
|
+
continue;
|
|
2404
|
+
}
|
|
2405
|
+
if (!status.alive) {
|
|
2406
|
+
const nextStatus = status.attention.state === "blocked" ? "blocked" : status.status === "done" ? "done" : "exited";
|
|
2407
|
+
worker.status = nextStatus;
|
|
2408
|
+
worker.reconciledAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2409
|
+
worker.reconcileReason = status.attention.reason;
|
|
2410
|
+
saveWorker(run.id, worker);
|
|
2411
|
+
outcomes.push({
|
|
2412
|
+
runId: run.id,
|
|
2413
|
+
worker: name,
|
|
2414
|
+
action: "marked_exited",
|
|
2415
|
+
reason: status.attention.reason
|
|
2416
|
+
});
|
|
2417
|
+
continue;
|
|
2418
|
+
}
|
|
2419
|
+
if (status.attention.state === "stale" && worker.pid && isPidAlive(worker.pid)) {
|
|
2420
|
+
const hbMs = status.lastHeartbeatAt ? Date.parse(status.lastHeartbeatAt) : NaN;
|
|
2421
|
+
const actMs = status.lastActivityAt ? Date.parse(status.lastActivityAt) : NaN;
|
|
2422
|
+
const hbStale = !Number.isFinite(hbMs) || now - hbMs > STALE_RECONCILE_HEARTBEAT_MS;
|
|
2423
|
+
const actStale = Number.isFinite(actMs) && now - actMs > STALE_MS;
|
|
2424
|
+
if (hbStale && actStale) {
|
|
2425
|
+
killWorkerProcess(worker.pid, "SIGTERM");
|
|
2426
|
+
worker.status = "exited";
|
|
2427
|
+
worker.reconciledAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2428
|
+
worker.reconcileReason = `reconciled stale worker: ${status.attention.reason}`;
|
|
2429
|
+
saveWorker(run.id, worker);
|
|
2430
|
+
outcomes.push({
|
|
2431
|
+
runId: run.id,
|
|
2432
|
+
worker: name,
|
|
2433
|
+
action: "killed_stale",
|
|
2434
|
+
reason: status.attention.reason
|
|
2435
|
+
});
|
|
2436
|
+
continue;
|
|
2437
|
+
}
|
|
2438
|
+
}
|
|
2439
|
+
outcomes.push({
|
|
2440
|
+
runId: run.id,
|
|
2441
|
+
worker: name,
|
|
2442
|
+
action: "skipped",
|
|
2443
|
+
reason: status.attention.reason
|
|
2444
|
+
});
|
|
2445
|
+
}
|
|
2446
|
+
}
|
|
2447
|
+
return { workers: outcomes, finalizedRuns: finalizeStaleRuns() };
|
|
2448
|
+
}
|
|
2449
|
+
|
|
2093
2450
|
// src/plan-progress-daemon-sync.ts
|
|
2094
|
-
import
|
|
2451
|
+
import path16 from "node:path";
|
|
2095
2452
|
|
|
2096
2453
|
// src/plan-progress-sync.ts
|
|
2097
2454
|
async function syncPlanProgress(args) {
|
|
@@ -2115,7 +2472,7 @@ async function syncActiveWorkerPlanProgress(runId, args) {
|
|
|
2115
2472
|
const outcomes = [];
|
|
2116
2473
|
for (const name of Object.keys(run.workers || {})) {
|
|
2117
2474
|
const worker = readJson(
|
|
2118
|
-
|
|
2475
|
+
path16.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
2119
2476
|
void 0
|
|
2120
2477
|
);
|
|
2121
2478
|
if (!worker?.dispatched || !worker.taskId) continue;
|
|
@@ -2169,7 +2526,7 @@ async function completeFinishedWorkers(runId, args) {
|
|
|
2169
2526
|
const outcomes = [];
|
|
2170
2527
|
for (const name of Object.keys(run.workers || {})) {
|
|
2171
2528
|
const worker = readJson(
|
|
2172
|
-
|
|
2529
|
+
path17.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
2173
2530
|
void 0
|
|
2174
2531
|
);
|
|
2175
2532
|
if (!worker?.taskId) continue;
|
|
@@ -2204,7 +2561,7 @@ async function runPipelineTick(args) {
|
|
|
2204
2561
|
const execute = args.execute !== false && args.execute !== "false";
|
|
2205
2562
|
runStatus({ run: runId });
|
|
2206
2563
|
const completedWorkers = await completeFinishedWorkers(runId, args);
|
|
2207
|
-
const
|
|
2564
|
+
const staleReconcile = reconcileStaleWorkers();
|
|
2208
2565
|
const planProgressSync = await syncActiveWorkerPlanProgress(runId, args);
|
|
2209
2566
|
const workspacePrefs = await fetchWorkspaceRuntimePreferences(agentOsId, args);
|
|
2210
2567
|
const resourceGate = observeRunnerResourceGate({
|
|
@@ -2245,7 +2602,7 @@ async function runPipelineTick(args) {
|
|
|
2245
2602
|
execute,
|
|
2246
2603
|
resourceGate,
|
|
2247
2604
|
completedWorkers,
|
|
2248
|
-
|
|
2605
|
+
staleReconcile,
|
|
2249
2606
|
planProgressSync,
|
|
2250
2607
|
operatorTick,
|
|
2251
2608
|
sweep,
|