@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/index.js
CHANGED
|
@@ -397,12 +397,12 @@ var DEFAULT_CRITICAL_FREE_BYTES = 15 * 1024 * 1024 * 1024;
|
|
|
397
397
|
var DEFAULT_MAX_USED_PERCENT = 80;
|
|
398
398
|
var DEFAULT_HARD_MAX_USED_PERCENT = 90;
|
|
399
399
|
function observeRunnerDiskGate(input = {}) {
|
|
400
|
-
const
|
|
400
|
+
const path18 = input.diskPath?.trim() || "/";
|
|
401
401
|
const warnBelowBytes = input.diskFreeWarnBytes ?? DEFAULT_WARN_FREE_BYTES;
|
|
402
402
|
const criticalBelowBytes = input.diskFreeCriticalBytes ?? DEFAULT_CRITICAL_FREE_BYTES;
|
|
403
403
|
const maxUsedPercent = input.diskMaxUsedPercent ?? DEFAULT_MAX_USED_PERCENT;
|
|
404
404
|
const hardMaxUsedPercent = input.diskHardMaxUsedPercent ?? DEFAULT_HARD_MAX_USED_PERCENT;
|
|
405
|
-
const stats = statfsSync(
|
|
405
|
+
const stats = statfsSync(path18);
|
|
406
406
|
const freeBytes = Number(stats.bavail) * Number(stats.bsize);
|
|
407
407
|
const totalBytes = Number(stats.blocks) * Number(stats.bsize);
|
|
408
408
|
const usedPercent = totalBytes > 0 ? (totalBytes - freeBytes) / totalBytes * 100 : 100;
|
|
@@ -422,7 +422,7 @@ function observeRunnerDiskGate(input = {}) {
|
|
|
422
422
|
}
|
|
423
423
|
return {
|
|
424
424
|
ok,
|
|
425
|
-
path:
|
|
425
|
+
path: path18,
|
|
426
426
|
freeBytes,
|
|
427
427
|
totalBytes,
|
|
428
428
|
usedPercent,
|
|
@@ -696,22 +696,36 @@ function gitIsAncestor(cwd, ancestor, descendant) {
|
|
|
696
696
|
if (res.status === 1) return { isAncestor: false, error: null };
|
|
697
697
|
return { isAncestor: null, error: res.error || res.stderr || res.stdout || `git exited ${res.status}` };
|
|
698
698
|
}
|
|
699
|
-
function computeGitAncestry(worktreePath,
|
|
699
|
+
function computeGitAncestry(worktreePath, baseOrOptions = "origin/main") {
|
|
700
|
+
const options = typeof baseOrOptions === "string" ? { base: baseOrOptions } : baseOrOptions;
|
|
701
|
+
const baseLabel = options.baseCommit?.trim() || options.base?.trim() || "origin/main";
|
|
702
|
+
const pinnedBaseCommit = options.baseCommit?.trim() || null;
|
|
700
703
|
if (!worktreePath) {
|
|
701
|
-
return unknownAncestry(
|
|
704
|
+
return unknownAncestry(baseLabel, "missing worktree path");
|
|
702
705
|
}
|
|
703
706
|
const head = gitCapture(worktreePath, ["rev-parse", "HEAD"]);
|
|
704
|
-
if (head.status !== 0)
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
707
|
+
if (head.status !== 0) {
|
|
708
|
+
return unknownAncestry(baseLabel, head.error || head.stderr || head.stdout || "failed to resolve HEAD");
|
|
709
|
+
}
|
|
710
|
+
let baseSha;
|
|
711
|
+
if (pinnedBaseCommit) {
|
|
712
|
+
baseSha = pinnedBaseCommit;
|
|
713
|
+
} else {
|
|
714
|
+
const baseHead = gitCapture(worktreePath, ["rev-parse", baseLabel]);
|
|
715
|
+
if (baseHead.status !== 0) {
|
|
716
|
+
return unknownAncestry(
|
|
717
|
+
baseLabel,
|
|
718
|
+
baseHead.error || baseHead.stderr || baseHead.stdout || `failed to resolve ${baseLabel}`,
|
|
719
|
+
head.stdout.trim()
|
|
720
|
+
);
|
|
721
|
+
}
|
|
722
|
+
baseSha = baseHead.stdout.trim();
|
|
708
723
|
}
|
|
709
724
|
const headSha = head.stdout.trim();
|
|
710
|
-
const baseSha = baseHead.stdout.trim();
|
|
711
725
|
if (headSha === baseSha) {
|
|
712
726
|
return {
|
|
713
727
|
checked: true,
|
|
714
|
-
base,
|
|
728
|
+
base: baseLabel,
|
|
715
729
|
head: headSha,
|
|
716
730
|
baseHead: baseSha,
|
|
717
731
|
baseIsAncestorOfHead: true,
|
|
@@ -725,7 +739,7 @@ function computeGitAncestry(worktreePath, base = "origin/main") {
|
|
|
725
739
|
if (baseIsAncestorOfHead.isAncestor == null || headIsAncestorOfBase.isAncestor == null) {
|
|
726
740
|
return {
|
|
727
741
|
checked: false,
|
|
728
|
-
base,
|
|
742
|
+
base: baseLabel,
|
|
729
743
|
head: headSha,
|
|
730
744
|
baseHead: baseSha,
|
|
731
745
|
baseIsAncestorOfHead: baseIsAncestorOfHead.isAncestor,
|
|
@@ -737,7 +751,7 @@ function computeGitAncestry(worktreePath, base = "origin/main") {
|
|
|
737
751
|
const relation = baseIsAncestorOfHead.isAncestor ? "ahead" : headIsAncestorOfBase.isAncestor ? "merged" : "diverged";
|
|
738
752
|
return {
|
|
739
753
|
checked: true,
|
|
740
|
-
base,
|
|
754
|
+
base: baseLabel,
|
|
741
755
|
head: headSha,
|
|
742
756
|
baseHead: baseSha,
|
|
743
757
|
baseIsAncestorOfHead: baseIsAncestorOfHead.isAncestor,
|
|
@@ -764,12 +778,74 @@ function scrubClaudeEnv(env) {
|
|
|
764
778
|
return next;
|
|
765
779
|
}
|
|
766
780
|
|
|
781
|
+
// src/landing-gate.ts
|
|
782
|
+
function trimOrNull(value) {
|
|
783
|
+
if (typeof value !== "string") return null;
|
|
784
|
+
const trimmed = value.trim();
|
|
785
|
+
return trimmed.length ? trimmed : null;
|
|
786
|
+
}
|
|
787
|
+
function hasFinalResult(value) {
|
|
788
|
+
if (value === void 0 || value === null) return false;
|
|
789
|
+
if (typeof value === "string") return value.trim().length > 0;
|
|
790
|
+
if (typeof value === "boolean") return value;
|
|
791
|
+
if (Array.isArray(value)) return value.length > 0;
|
|
792
|
+
if (typeof value === "object") return Object.keys(value).length > 0;
|
|
793
|
+
return true;
|
|
794
|
+
}
|
|
795
|
+
function hasCommittedLandingRef(snapshot) {
|
|
796
|
+
if (trimOrNull(snapshot.headCommit)) return true;
|
|
797
|
+
if (trimOrNull(snapshot.prUrl)) return true;
|
|
798
|
+
if (trimOrNull(snapshot.artifactBundlePath)) return true;
|
|
799
|
+
if (trimOrNull(snapshot.patchPath)) return true;
|
|
800
|
+
const ancestry = snapshot.gitAncestry;
|
|
801
|
+
if (ancestry?.checked && ancestry.headIsAncestorOfBase === false && trimOrNull(ancestry.head)) {
|
|
802
|
+
return true;
|
|
803
|
+
}
|
|
804
|
+
return false;
|
|
805
|
+
}
|
|
806
|
+
function assessWorkerLanding(snapshot) {
|
|
807
|
+
if (!hasFinalResult(snapshot.finalResult)) return { blocked: false };
|
|
808
|
+
if (snapshot.changedFiles.length === 0) return { blocked: false };
|
|
809
|
+
if (!hasCommittedLandingRef(snapshot)) {
|
|
810
|
+
return {
|
|
811
|
+
blocked: true,
|
|
812
|
+
reason: "dirty_worktree_no_pr",
|
|
813
|
+
detail: `Worktree has ${snapshot.changedFiles.length} uncommitted change(s) with no commit or PR; commit, open a PR, or discard before landing`
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
return {
|
|
817
|
+
blocked: true,
|
|
818
|
+
detail: `Worktree has ${snapshot.changedFiles.length} uncommitted change(s); commit or discard before landing`
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
function landingAttentionReason(verdict) {
|
|
822
|
+
if (!verdict.blocked) return void 0;
|
|
823
|
+
return verdict.detail ?? verdict.reason ?? "dirty_worktree_no_pr";
|
|
824
|
+
}
|
|
825
|
+
|
|
767
826
|
// src/status.ts
|
|
768
827
|
var NO_START_MS = 18e4;
|
|
769
828
|
var STALE_MS = 6e5;
|
|
770
829
|
function computeAttention(input) {
|
|
771
830
|
const now = Date.now();
|
|
772
|
-
if (input.
|
|
831
|
+
if (input.completionBlocker) {
|
|
832
|
+
return { state: "blocked", reason: input.completionBlocker };
|
|
833
|
+
}
|
|
834
|
+
if (input.finalResult) {
|
|
835
|
+
const landing = assessWorkerLanding({
|
|
836
|
+
finalResult: input.finalResult,
|
|
837
|
+
changedFiles: input.changedFiles ?? [],
|
|
838
|
+
gitAncestry: input.gitAncestry ?? null
|
|
839
|
+
});
|
|
840
|
+
if (landing.blocked) {
|
|
841
|
+
const detail = landingAttentionReason(landing);
|
|
842
|
+
return {
|
|
843
|
+
state: "needs_attention",
|
|
844
|
+
reason: landing.reason ? `landing blocked (${landing.reason}): ${detail}` : `landing blocked: ${detail}`
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
return { state: "done", reason: "final result recorded" };
|
|
848
|
+
}
|
|
773
849
|
if (!input.alive) {
|
|
774
850
|
const classified = classifyExitFailure(input.error);
|
|
775
851
|
if (classified) return { state: "blocked", reason: classified.reason };
|
|
@@ -800,7 +876,10 @@ function computeWorkerStatus(worker, options = {}) {
|
|
|
800
876
|
const stderrBytes = fileSize(worker.stderrPath);
|
|
801
877
|
const heartbeatBytes = fileSize(worker.heartbeatPath);
|
|
802
878
|
const changedFiles = gitStatusShort(worker.worktreePath);
|
|
803
|
-
const gitAncestry = computeGitAncestry(worker.worktreePath,
|
|
879
|
+
const gitAncestry = computeGitAncestry(worker.worktreePath, {
|
|
880
|
+
base: options.base,
|
|
881
|
+
baseCommit: options.baseCommit
|
|
882
|
+
});
|
|
804
883
|
const lastActivityAt = latestIso([
|
|
805
884
|
parsed.lastEventAt,
|
|
806
885
|
heartbeat.lastHeartbeatAt,
|
|
@@ -809,6 +888,7 @@ function computeWorkerStatus(worker, options = {}) {
|
|
|
809
888
|
fileMtime(worker.heartbeatPath)
|
|
810
889
|
]);
|
|
811
890
|
const error = parsed.error || (!alive && !parsed.finalResult ? tailFile(worker.stderrPath, 10).trim() || void 0 : void 0);
|
|
891
|
+
const completionBlocker = typeof worker.completionBlocker === "string" && worker.completionBlocker.trim() ? worker.completionBlocker.trim() : null;
|
|
812
892
|
const attention = computeAttention({
|
|
813
893
|
alive,
|
|
814
894
|
finalResult: parsed.finalResult,
|
|
@@ -818,14 +898,18 @@ function computeWorkerStatus(worker, options = {}) {
|
|
|
818
898
|
lastActivityAt,
|
|
819
899
|
heartbeatBlocker: heartbeat.heartbeatBlocker,
|
|
820
900
|
startedAt: worker.startedAt,
|
|
821
|
-
error
|
|
901
|
+
error,
|
|
902
|
+
changedFiles,
|
|
903
|
+
gitAncestry,
|
|
904
|
+
completionBlocker
|
|
822
905
|
});
|
|
906
|
+
const workerStatusLabel = completionBlocker || attention.state === "blocked" ? "blocked" : attention.state === "done" ? "done" : parsed.finalResult ? "exited" : alive ? "running" : "exited";
|
|
823
907
|
return {
|
|
824
908
|
runId: worker.runId,
|
|
825
909
|
worker: worker.name,
|
|
826
910
|
pid: worker.pid,
|
|
827
911
|
alive,
|
|
828
|
-
status:
|
|
912
|
+
status: workerStatusLabel,
|
|
829
913
|
attention,
|
|
830
914
|
branch: worker.branch,
|
|
831
915
|
worktreePath: worker.worktreePath,
|
|
@@ -854,6 +938,10 @@ function isFinishedWorkerStatus(status) {
|
|
|
854
938
|
if (status.status === "exited" || status.status === "done") return true;
|
|
855
939
|
return false;
|
|
856
940
|
}
|
|
941
|
+
function isLandingBlockedWorkerStatus(status) {
|
|
942
|
+
if (!status.finalResult) return false;
|
|
943
|
+
return status.attention.state === "needs_attention" || status.attention.state === "blocked";
|
|
944
|
+
}
|
|
857
945
|
function deriveRunStatus(fallback, workers) {
|
|
858
946
|
if (workers.length === 0) return fallback;
|
|
859
947
|
if (workers.some((w) => w.attention === "needs_attention" || w.attention === "stale" || w.attention === "blocked")) {
|
|
@@ -904,6 +992,10 @@ function readAvailableMemBytes() {
|
|
|
904
992
|
}
|
|
905
993
|
return os.freemem();
|
|
906
994
|
}
|
|
995
|
+
function isActiveHarnessWorker(worker) {
|
|
996
|
+
const status = computeWorkerStatus(worker);
|
|
997
|
+
return status.alive && !status.finalResult && status.attention.state !== "done";
|
|
998
|
+
}
|
|
907
999
|
function countActiveWorkersForRun(run) {
|
|
908
1000
|
let active = 0;
|
|
909
1001
|
for (const name of Object.keys(run.workers || {})) {
|
|
@@ -911,11 +1003,8 @@ function countActiveWorkersForRun(run) {
|
|
|
911
1003
|
path5.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
912
1004
|
void 0
|
|
913
1005
|
);
|
|
914
|
-
if (!worker) continue;
|
|
915
|
-
|
|
916
|
-
if (status.alive && !status.finalResult && status.attention.state !== "done") {
|
|
917
|
-
active++;
|
|
918
|
-
}
|
|
1006
|
+
if (!worker || !isActiveHarnessWorker(worker)) continue;
|
|
1007
|
+
active++;
|
|
919
1008
|
}
|
|
920
1009
|
return active;
|
|
921
1010
|
}
|
|
@@ -940,7 +1029,7 @@ function observeRunnerResourceGate(input) {
|
|
|
940
1029
|
const maxConcurrentWorkers = Math.max(0, Math.min(targetCap, capacityFromTotal));
|
|
941
1030
|
const slotsByCapacity = Math.max(0, maxConcurrentWorkers - activeWorkers);
|
|
942
1031
|
const slotsByFreeMem = capacityFromFree;
|
|
943
|
-
|
|
1032
|
+
let slotsAvailable = Math.min(slotsByCapacity, slotsByFreeMem);
|
|
944
1033
|
let reason = null;
|
|
945
1034
|
if (slotsAvailable <= 0) {
|
|
946
1035
|
if (activeWorkers >= maxConcurrentWorkers) {
|
|
@@ -967,39 +1056,6 @@ function observeRunnerResourceGate(input) {
|
|
|
967
1056
|
};
|
|
968
1057
|
}
|
|
969
1058
|
|
|
970
|
-
// src/supervisor.ts
|
|
971
|
-
import { existsSync as existsSync10, mkdirSync as mkdirSync3 } from "node:fs";
|
|
972
|
-
import path10 from "node:path";
|
|
973
|
-
|
|
974
|
-
// src/prompt.ts
|
|
975
|
-
function buildPrompt(input) {
|
|
976
|
-
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.";
|
|
977
|
-
const progressLines = [
|
|
978
|
-
"Structured plan progress (required when planId is set):",
|
|
979
|
-
"- 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).",
|
|
980
|
-
"- 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.",
|
|
981
|
-
"- 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`).",
|
|
982
|
-
"- 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>`.",
|
|
983
|
-
"- Before the completion report: mark completion-report rows partial with evidence; do not skip report review.",
|
|
984
|
-
"- After implementation: wait for report_reviewer then deep_reviewer confirmation (via MCP/session agents) before follow-up rows close.",
|
|
985
|
-
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."
|
|
986
|
-
];
|
|
987
|
-
return [
|
|
988
|
-
"You are running under the Kynver AgentOS runtime.",
|
|
989
|
-
"Immediately state your plan before editing.",
|
|
990
|
-
ownership,
|
|
991
|
-
`Worktree: ${input.worktreePath}`,
|
|
992
|
-
`Progress heartbeat file: ${input.heartbeatPath}`,
|
|
993
|
-
"After each major step, append one JSON line to the heartbeat file with fields: ts, phase, summary, changedFiles, blocker.",
|
|
994
|
-
"Final response must include files changed, verification commands, and unresolved risks.",
|
|
995
|
-
"",
|
|
996
|
-
...progressLines,
|
|
997
|
-
"",
|
|
998
|
-
"Task:",
|
|
999
|
-
input.task
|
|
1000
|
-
].join("\n");
|
|
1001
|
-
}
|
|
1002
|
-
|
|
1003
1059
|
// src/providers/claude.ts
|
|
1004
1060
|
import { closeSync, openSync } from "node:fs";
|
|
1005
1061
|
import { spawn } from "node:child_process";
|
|
@@ -1058,7 +1114,7 @@ function preflightCursorModel(model, defaultModel) {
|
|
|
1058
1114
|
}
|
|
1059
1115
|
|
|
1060
1116
|
// src/providers/claude.ts
|
|
1061
|
-
var CLAUDE_DEFAULT_MODEL = "claude-
|
|
1117
|
+
var CLAUDE_DEFAULT_MODEL = "claude-sonnet-4-6";
|
|
1062
1118
|
var claudeProvider = {
|
|
1063
1119
|
name: "claude",
|
|
1064
1120
|
defaultModel: CLAUDE_DEFAULT_MODEL,
|
|
@@ -1104,6 +1160,159 @@ var claudeProvider = {
|
|
|
1104
1160
|
}
|
|
1105
1161
|
};
|
|
1106
1162
|
|
|
1163
|
+
// src/model-routing.ts
|
|
1164
|
+
var GLOBAL_DEFAULT_MODEL = "claude-sonnet-4-6";
|
|
1165
|
+
function taskString(task, key) {
|
|
1166
|
+
const v = task[key];
|
|
1167
|
+
return typeof v === "string" ? v.trim() : "";
|
|
1168
|
+
}
|
|
1169
|
+
function normalizeRef(ref) {
|
|
1170
|
+
return ref.toLowerCase();
|
|
1171
|
+
}
|
|
1172
|
+
function resolveGlobalDefaultModel(config = loadUserConfig()) {
|
|
1173
|
+
const fromConfig = config.defaultModel?.trim();
|
|
1174
|
+
if (fromConfig) return fromConfig;
|
|
1175
|
+
const fromEnv = process.env.KYNVER_DEFAULT_MODEL?.trim();
|
|
1176
|
+
if (fromEnv) return fromEnv;
|
|
1177
|
+
return GLOBAL_DEFAULT_MODEL;
|
|
1178
|
+
}
|
|
1179
|
+
function inferProviderFromModel(model) {
|
|
1180
|
+
const m = (model ?? "").toLowerCase();
|
|
1181
|
+
if (!m) return "claude";
|
|
1182
|
+
if (m.includes("composer") || m.includes("cursor") || m.includes("codex") || m.startsWith("gpt-") || m.startsWith("gpt5")) {
|
|
1183
|
+
return "cursor";
|
|
1184
|
+
}
|
|
1185
|
+
return "claude";
|
|
1186
|
+
}
|
|
1187
|
+
function isOpusLane(ref, title) {
|
|
1188
|
+
if (ref.includes("deep") && ref.includes("review")) return true;
|
|
1189
|
+
if (ref.includes("security")) return true;
|
|
1190
|
+
if (ref.includes("plan_author") || ref.includes("plan-author")) return true;
|
|
1191
|
+
if (title.includes("deep review") || title.includes("security review")) return true;
|
|
1192
|
+
if (ref.includes("plan") && !ref.includes("review") && (ref.includes("author") || ref.includes("strategy"))) {
|
|
1193
|
+
return true;
|
|
1194
|
+
}
|
|
1195
|
+
return false;
|
|
1196
|
+
}
|
|
1197
|
+
function inferModelRoutingFromTask(task) {
|
|
1198
|
+
const ref = normalizeRef(taskString(task, "executorRef"));
|
|
1199
|
+
const title = taskString(task, "title").toLowerCase();
|
|
1200
|
+
const priority = taskString(task, "priority") || "normal";
|
|
1201
|
+
const roleLane = normalizeRef(taskString(task, "roleLane"));
|
|
1202
|
+
if (ref.includes("cursor") || ref.includes("codex") || ref.includes("composer") || ref.includes("copilot") || roleLane === "implementer" || roleLane === "repair_implementer") {
|
|
1203
|
+
return { provider: "cursor", rule: "lane:implementation" };
|
|
1204
|
+
}
|
|
1205
|
+
if (ref.includes("landing") || title.startsWith("land:") || title.includes(" merge")) {
|
|
1206
|
+
return {
|
|
1207
|
+
model: "claude-haiku-4-5-20251001",
|
|
1208
|
+
provider: "claude",
|
|
1209
|
+
rule: "lane:landing"
|
|
1210
|
+
};
|
|
1211
|
+
}
|
|
1212
|
+
if (ref.includes("review") || title.startsWith("review ") || roleLane.includes("review")) {
|
|
1213
|
+
if (isOpusLane(ref, title) || roleLane === "deep_reviewer") {
|
|
1214
|
+
return { model: "claude-opus-4-7", provider: "claude", rule: "lane:deep_review" };
|
|
1215
|
+
}
|
|
1216
|
+
return { model: "claude-sonnet-4-6", provider: "claude", rule: "lane:review" };
|
|
1217
|
+
}
|
|
1218
|
+
if (isOpusLane(ref, title) || roleLane === "plan_author") {
|
|
1219
|
+
return { model: "claude-opus-4-7", provider: "claude", rule: "lane:planning" };
|
|
1220
|
+
}
|
|
1221
|
+
if (priority === "critical") {
|
|
1222
|
+
return { model: "claude-opus-4-7", provider: "claude", rule: "priority:critical" };
|
|
1223
|
+
}
|
|
1224
|
+
if (priority === "low") {
|
|
1225
|
+
return {
|
|
1226
|
+
model: "claude-haiku-4-5-20251001",
|
|
1227
|
+
provider: "claude",
|
|
1228
|
+
rule: "priority:low"
|
|
1229
|
+
};
|
|
1230
|
+
}
|
|
1231
|
+
return {
|
|
1232
|
+
model: resolveGlobalDefaultModel(),
|
|
1233
|
+
provider: "claude",
|
|
1234
|
+
rule: "default:sonnet"
|
|
1235
|
+
};
|
|
1236
|
+
}
|
|
1237
|
+
function resolveWorkerLaunch(input) {
|
|
1238
|
+
if (input.explicitModel?.trim()) {
|
|
1239
|
+
const model2 = input.explicitModel.trim();
|
|
1240
|
+
return {
|
|
1241
|
+
model: model2,
|
|
1242
|
+
provider: input.explicitProvider?.trim() || inferProviderFromModel(model2),
|
|
1243
|
+
rule: "explicit:cli",
|
|
1244
|
+
requestedModel: model2
|
|
1245
|
+
};
|
|
1246
|
+
}
|
|
1247
|
+
if (input.task && Object.keys(input.task).length > 0) {
|
|
1248
|
+
const inferred = inferModelRoutingFromTask(input.task);
|
|
1249
|
+
return {
|
|
1250
|
+
...inferred,
|
|
1251
|
+
requestedModel: inferred.model
|
|
1252
|
+
};
|
|
1253
|
+
}
|
|
1254
|
+
const model = resolveGlobalDefaultModel();
|
|
1255
|
+
return {
|
|
1256
|
+
model,
|
|
1257
|
+
provider: input.explicitProvider?.trim() || inferProviderFromModel(model),
|
|
1258
|
+
rule: "default:global",
|
|
1259
|
+
requestedModel: model
|
|
1260
|
+
};
|
|
1261
|
+
}
|
|
1262
|
+
function resolveModelFallback(startedModel, launchModel, providerDefault) {
|
|
1263
|
+
return startedModel || launchModel || providerDefault || resolveGlobalDefaultModel() || CLAUDE_DEFAULT_MODEL;
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
// src/retry-limits.ts
|
|
1267
|
+
function positiveInt2(value, fallback) {
|
|
1268
|
+
const n = Number(value);
|
|
1269
|
+
if (!Number.isFinite(n) || n <= 0) return fallback;
|
|
1270
|
+
return Math.floor(n);
|
|
1271
|
+
}
|
|
1272
|
+
function readHarnessRetryLimits() {
|
|
1273
|
+
return {
|
|
1274
|
+
maxTaskAttempts: positiveInt2(process.env.KYNVER_MAX_TASK_ATTEMPTS, 3),
|
|
1275
|
+
dispatchCooldownMs: positiveInt2(process.env.KYNVER_DISPATCH_COOLDOWN_MS, 5e3)
|
|
1276
|
+
};
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
// src/supervisor.ts
|
|
1280
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync3 } from "node:fs";
|
|
1281
|
+
import path10 from "node:path";
|
|
1282
|
+
|
|
1283
|
+
// src/prompt.ts
|
|
1284
|
+
function buildPrompt(input) {
|
|
1285
|
+
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.";
|
|
1286
|
+
const compact = Boolean(input.model?.toLowerCase().includes("haiku"));
|
|
1287
|
+
const progressLines = compact ? [
|
|
1288
|
+
"Plan progress: when planId is set, use `kynver plan progress` for running|partial|blocked only; row `done` is MCP/session only.",
|
|
1289
|
+
input.planId ? `Active planId: ${input.planId}` : "No planId on this worker."
|
|
1290
|
+
] : [
|
|
1291
|
+
"Structured plan progress (required when planId is set):",
|
|
1292
|
+
"- 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).",
|
|
1293
|
+
"- 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.",
|
|
1294
|
+
"- 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`).",
|
|
1295
|
+
"- 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>`.",
|
|
1296
|
+
"- Before the completion report: mark completion-report rows partial with evidence; do not skip report review.",
|
|
1297
|
+
"- After implementation: wait for report_reviewer then deep_reviewer confirmation (via MCP/session agents) before follow-up rows close.",
|
|
1298
|
+
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."
|
|
1299
|
+
];
|
|
1300
|
+
return [
|
|
1301
|
+
"You are running under the Kynver AgentOS runtime.",
|
|
1302
|
+
"Immediately state your plan before editing.",
|
|
1303
|
+
ownership,
|
|
1304
|
+
`Worktree: ${input.worktreePath}`,
|
|
1305
|
+
`Progress heartbeat file: ${input.heartbeatPath}`,
|
|
1306
|
+
"After each major step, append one JSON line to the heartbeat file with fields: ts, phase, summary, changedFiles, blocker.",
|
|
1307
|
+
"Final response must include files changed, verification commands, and unresolved risks.",
|
|
1308
|
+
"",
|
|
1309
|
+
...progressLines,
|
|
1310
|
+
"",
|
|
1311
|
+
"Task:",
|
|
1312
|
+
input.task
|
|
1313
|
+
].join("\n");
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1107
1316
|
// src/providers/cursor.ts
|
|
1108
1317
|
import { closeSync as closeSync2, existsSync as existsSync8, openSync as openSync2 } from "node:fs";
|
|
1109
1318
|
import { spawn as spawn2 } from "node:child_process";
|
|
@@ -1307,9 +1516,13 @@ function persistCompletionBlocker(worker, reason) {
|
|
|
1307
1516
|
else delete worker.completionBlocker;
|
|
1308
1517
|
saveWorker(worker.runId, worker);
|
|
1309
1518
|
}
|
|
1519
|
+
function workerStatusOptions(run) {
|
|
1520
|
+
return run ? { base: run.base, baseCommit: run.baseCommit } : {};
|
|
1521
|
+
}
|
|
1310
1522
|
async function tryCompleteWorker(args) {
|
|
1311
1523
|
const worker = loadWorker(String(args.run), String(args.name));
|
|
1312
|
-
const
|
|
1524
|
+
const run = loadRun(worker.runId);
|
|
1525
|
+
const status = computeWorkerStatus(worker, workerStatusOptions(run));
|
|
1313
1526
|
const agentOsId = (args.agentOsId ? String(args.agentOsId) : worker.agentOsId) || "";
|
|
1314
1527
|
const taskId = (args.taskId ? String(args.taskId) : worker.taskId) || null;
|
|
1315
1528
|
if (!agentOsId) {
|
|
@@ -1353,7 +1566,8 @@ async function tryCompleteWorker(args) {
|
|
|
1353
1566
|
async function completeWorker(args) {
|
|
1354
1567
|
try {
|
|
1355
1568
|
const worker = loadWorker(String(args.run), String(args.name));
|
|
1356
|
-
const
|
|
1569
|
+
const run = loadRun(worker.runId);
|
|
1570
|
+
const status = computeWorkerStatus(worker, workerStatusOptions(run));
|
|
1357
1571
|
const agentOsId = (args.agentOsId ? String(args.agentOsId) : worker.agentOsId) || "";
|
|
1358
1572
|
const taskId = (args.taskId ? String(args.taskId) : worker.taskId) || null;
|
|
1359
1573
|
if (!agentOsId) {
|
|
@@ -1400,7 +1614,8 @@ async function completeWorker(args) {
|
|
|
1400
1614
|
}
|
|
1401
1615
|
function workerStatus(args) {
|
|
1402
1616
|
const worker = loadWorker(String(args.run), String(args.name));
|
|
1403
|
-
const
|
|
1617
|
+
const run = loadRun(worker.runId);
|
|
1618
|
+
const status = computeWorkerStatus(worker, workerStatusOptions(run));
|
|
1404
1619
|
writeJson(path8.join(worker.workerDir, "last-status.json"), status);
|
|
1405
1620
|
console.log(JSON.stringify(status, null, 2));
|
|
1406
1621
|
}
|
|
@@ -1415,14 +1630,21 @@ function runStatus(args) {
|
|
|
1415
1630
|
if (!worker) {
|
|
1416
1631
|
return { worker: name, status: "missing", attention: "needs_attention", attentionReason: "worker.json not found" };
|
|
1417
1632
|
}
|
|
1418
|
-
const status = computeWorkerStatus(worker, {
|
|
1633
|
+
const status = computeWorkerStatus(worker, {
|
|
1634
|
+
base: run.base,
|
|
1635
|
+
baseCommit: run.baseCommit
|
|
1636
|
+
});
|
|
1637
|
+
const headCommit = status.gitAncestry.headIsAncestorOfBase === false && status.gitAncestry.head ? status.gitAncestry.head : void 0;
|
|
1419
1638
|
const rawBlocker = worker.completionBlocker;
|
|
1420
1639
|
const completionBlocker = typeof rawBlocker === "string" && rawBlocker ? rawBlocker : void 0;
|
|
1640
|
+
const boardStatus = completionBlocker ? "blocked" : status.status;
|
|
1641
|
+
const boardAttention = completionBlocker ? "blocked" : status.attention.state;
|
|
1421
1642
|
return {
|
|
1422
1643
|
worker: status.worker,
|
|
1423
|
-
status:
|
|
1424
|
-
attention:
|
|
1644
|
+
status: boardStatus,
|
|
1645
|
+
attention: boardAttention,
|
|
1425
1646
|
attentionReason: completionBlocker ?? status.attention.reason,
|
|
1647
|
+
landingBlocked: status.finalResult ? boardAttention === "needs_attention" || boardAttention === "blocked" : false,
|
|
1426
1648
|
pid: status.pid,
|
|
1427
1649
|
alive: status.alive,
|
|
1428
1650
|
currentTool: status.currentTool,
|
|
@@ -1431,7 +1653,14 @@ function runStatus(args) {
|
|
|
1431
1653
|
lastHeartbeatSummary: status.lastHeartbeatSummary,
|
|
1432
1654
|
heartbeatBlocker: status.heartbeatBlocker,
|
|
1433
1655
|
changedFileCount: status.changedFiles.length,
|
|
1656
|
+
changedFiles: status.changedFiles,
|
|
1434
1657
|
branch: status.branch,
|
|
1658
|
+
model: typeof worker.model === "string" ? worker.model : void 0,
|
|
1659
|
+
routingRule: typeof worker.routingRule === "string" ? worker.routingRule : void 0,
|
|
1660
|
+
requestedModel: typeof worker.requestedModel === "string" ? worker.requestedModel : void 0,
|
|
1661
|
+
headCommit,
|
|
1662
|
+
gitAncestry: status.gitAncestry,
|
|
1663
|
+
finalResult: status.finalResult,
|
|
1435
1664
|
ancestry: status.gitAncestry.relation,
|
|
1436
1665
|
ancestryChecked: status.gitAncestry.checked
|
|
1437
1666
|
};
|
|
@@ -1647,8 +1876,17 @@ function spawnWorkerProcess(run, opts) {
|
|
|
1647
1876
|
const name = safeSlug(rawName);
|
|
1648
1877
|
if (run.workers?.[name]) throw new Error(`worker already exists in run ${run.id}: ${name}`);
|
|
1649
1878
|
if (!opts.task) throw new Error(`missing task text for worker ${name}`);
|
|
1650
|
-
const
|
|
1651
|
-
|
|
1879
|
+
const routing = opts.routingRule || opts.requestedModel ? {
|
|
1880
|
+
provider: opts.provider || "claude",
|
|
1881
|
+
model: opts.model,
|
|
1882
|
+
rule: opts.routingRule || "explicit:spawn",
|
|
1883
|
+
requestedModel: opts.requestedModel ?? opts.model
|
|
1884
|
+
} : resolveWorkerLaunch({
|
|
1885
|
+
explicitModel: opts.model,
|
|
1886
|
+
explicitProvider: opts.provider
|
|
1887
|
+
});
|
|
1888
|
+
const provider = resolveWorkerProvider(routing.provider);
|
|
1889
|
+
let launchModel = routing.model;
|
|
1652
1890
|
if (provider.preflightModel) {
|
|
1653
1891
|
const preflight = provider.preflightModel(opts.model);
|
|
1654
1892
|
if (!preflight.ok) {
|
|
@@ -1676,7 +1914,10 @@ function spawnWorkerProcess(run, opts) {
|
|
|
1676
1914
|
task: opts.task,
|
|
1677
1915
|
ownedPaths: opts.ownedPaths || [],
|
|
1678
1916
|
worktreePath,
|
|
1679
|
-
heartbeatPath
|
|
1917
|
+
heartbeatPath,
|
|
1918
|
+
planId: opts.planId,
|
|
1919
|
+
taskId: opts.taskId,
|
|
1920
|
+
model: launchModel
|
|
1680
1921
|
});
|
|
1681
1922
|
let started;
|
|
1682
1923
|
try {
|
|
@@ -1698,7 +1939,7 @@ function spawnWorkerProcess(run, opts) {
|
|
|
1698
1939
|
git(run.repo, ["branch", "-D", branch], { allowFailure: true });
|
|
1699
1940
|
throw error;
|
|
1700
1941
|
}
|
|
1701
|
-
const model = started.model
|
|
1942
|
+
const model = resolveModelFallback(started.model, launchModel, provider.defaultModel);
|
|
1702
1943
|
const worker = {
|
|
1703
1944
|
name,
|
|
1704
1945
|
runId: run.id,
|
|
@@ -1717,6 +1958,8 @@ function spawnWorkerProcess(run, opts) {
|
|
|
1717
1958
|
...opts.planId ? { planId: String(opts.planId) } : {},
|
|
1718
1959
|
...opts.leaseOwner ? { leaseOwner: String(opts.leaseOwner) } : {},
|
|
1719
1960
|
...opts.dispatched ? { dispatched: true } : {},
|
|
1961
|
+
routingRule: routing.rule,
|
|
1962
|
+
...routing.requestedModel ? { requestedModel: routing.requestedModel } : {},
|
|
1720
1963
|
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1721
1964
|
};
|
|
1722
1965
|
saveWorker(run.id, worker);
|
|
@@ -1724,14 +1967,23 @@ function spawnWorkerProcess(run, opts) {
|
|
|
1724
1967
|
run.status = "running";
|
|
1725
1968
|
saveRun(run);
|
|
1726
1969
|
if (worker.agentOsId && worker.taskId) {
|
|
1970
|
+
let sidecarSpawned;
|
|
1727
1971
|
try {
|
|
1728
|
-
spawnCompletionSidecar({
|
|
1972
|
+
sidecarSpawned = spawnCompletionSidecar({
|
|
1729
1973
|
runId: run.id,
|
|
1730
1974
|
workerName: name,
|
|
1731
1975
|
workerDir,
|
|
1732
1976
|
agentOsId: worker.agentOsId
|
|
1733
1977
|
});
|
|
1734
|
-
} catch {
|
|
1978
|
+
} catch (error) {
|
|
1979
|
+
const reason = `completion sidecar failed to spawn: ${error.message}`;
|
|
1980
|
+
worker.completionBlocker = reason;
|
|
1981
|
+
saveWorker(run.id, worker);
|
|
1982
|
+
}
|
|
1983
|
+
if (!sidecarSpawned) {
|
|
1984
|
+
const reason = "completion sidecar failed to spawn (CLI not found or spawn error)";
|
|
1985
|
+
worker.completionBlocker = reason;
|
|
1986
|
+
saveWorker(run.id, worker);
|
|
1735
1987
|
}
|
|
1736
1988
|
}
|
|
1737
1989
|
return worker;
|
|
@@ -1866,17 +2118,34 @@ async function dispatchRun(args) {
|
|
|
1866
2118
|
console.log(JSON.stringify(summary2, null, 2));
|
|
1867
2119
|
return;
|
|
1868
2120
|
}
|
|
2121
|
+
const retryLimits = readHarnessRetryLimits();
|
|
1869
2122
|
const outcomes = [];
|
|
1870
2123
|
for (const decision of result.started) {
|
|
1871
2124
|
const task = decision.task;
|
|
2125
|
+
const attempt = Number(task.attempt) || 1;
|
|
2126
|
+
if (attempt > retryLimits.maxTaskAttempts) {
|
|
2127
|
+
outcomes.push({
|
|
2128
|
+
taskId: task.id,
|
|
2129
|
+
started: false,
|
|
2130
|
+
error: `task attempt ${attempt} exceeds KYNVER_MAX_TASK_ATTEMPTS (${retryLimits.maxTaskAttempts})`
|
|
2131
|
+
});
|
|
2132
|
+
continue;
|
|
2133
|
+
}
|
|
1872
2134
|
const name = safeSlug(`t-${task.id}-a${task.attempt}`);
|
|
2135
|
+
const routing = resolveWorkerLaunch({
|
|
2136
|
+
explicitModel: args.model ? String(args.model) : void 0,
|
|
2137
|
+
task
|
|
2138
|
+
});
|
|
1873
2139
|
try {
|
|
1874
2140
|
const planId = task.planId ? String(task.planId) : void 0;
|
|
1875
2141
|
const worker = spawnWorkerProcess(run, {
|
|
1876
2142
|
name,
|
|
1877
2143
|
task: buildDispatchTaskText(task, agentOsId),
|
|
1878
2144
|
ownedPaths: args.owned ? String(args.owned).split(",").map((s) => s.trim()).filter(Boolean) : [],
|
|
1879
|
-
model:
|
|
2145
|
+
model: routing.model,
|
|
2146
|
+
provider: routing.provider,
|
|
2147
|
+
routingRule: routing.rule,
|
|
2148
|
+
requestedModel: routing.requestedModel,
|
|
1880
2149
|
agentOsId,
|
|
1881
2150
|
taskId: String(task.id),
|
|
1882
2151
|
planId,
|
|
@@ -1888,7 +2157,10 @@ async function dispatchRun(args) {
|
|
|
1888
2157
|
started: true,
|
|
1889
2158
|
worker: worker.name,
|
|
1890
2159
|
pid: worker.pid,
|
|
1891
|
-
branch: worker.branch
|
|
2160
|
+
branch: worker.branch,
|
|
2161
|
+
model: worker.model,
|
|
2162
|
+
provider: routing.provider,
|
|
2163
|
+
routingRule: routing.rule
|
|
1892
2164
|
});
|
|
1893
2165
|
} catch (error) {
|
|
1894
2166
|
const releaseUrl = `${base}/api/agent-os/by-id/${encodeURIComponent(agentOsId)}/tasks/${encodeURIComponent(String(task.id))}/release`;
|
|
@@ -2077,7 +2349,10 @@ import { mkdirSync as mkdirSync5, realpathSync } from "node:fs";
|
|
|
2077
2349
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
2078
2350
|
|
|
2079
2351
|
// src/pipeline-tick.ts
|
|
2080
|
-
import
|
|
2352
|
+
import path17 from "node:path";
|
|
2353
|
+
|
|
2354
|
+
// src/stale-reconcile.ts
|
|
2355
|
+
import path15 from "node:path";
|
|
2081
2356
|
|
|
2082
2357
|
// src/finalize.ts
|
|
2083
2358
|
import path14 from "node:path";
|
|
@@ -2088,13 +2363,17 @@ function terminalStatusFor(run) {
|
|
|
2088
2363
|
let anyAlive = false;
|
|
2089
2364
|
let anyResult = false;
|
|
2090
2365
|
let anyCompletionBlocked = false;
|
|
2366
|
+
let anyLandingBlocked = false;
|
|
2091
2367
|
for (const name of names) {
|
|
2092
2368
|
const worker = readJson(
|
|
2093
2369
|
path14.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
2094
2370
|
void 0
|
|
2095
2371
|
);
|
|
2096
2372
|
if (!worker) continue;
|
|
2097
|
-
const status = computeWorkerStatus(worker
|
|
2373
|
+
const status = computeWorkerStatus(worker, {
|
|
2374
|
+
base: run.base,
|
|
2375
|
+
baseCommit: run.baseCommit
|
|
2376
|
+
});
|
|
2098
2377
|
if (status.alive && !status.finalResult) {
|
|
2099
2378
|
anyAlive = true;
|
|
2100
2379
|
break;
|
|
@@ -2102,10 +2381,14 @@ function terminalStatusFor(run) {
|
|
|
2102
2381
|
if (typeof worker.completionBlocker === "string" && worker.completionBlocker) {
|
|
2103
2382
|
anyCompletionBlocked = true;
|
|
2104
2383
|
}
|
|
2105
|
-
if (status
|
|
2384
|
+
if (isLandingBlockedWorkerStatus(status)) {
|
|
2385
|
+
anyLandingBlocked = true;
|
|
2386
|
+
}
|
|
2387
|
+
if (status.finalResult && status.attention.state === "done") anyResult = true;
|
|
2106
2388
|
}
|
|
2107
2389
|
if (anyAlive) return null;
|
|
2108
2390
|
if (anyCompletionBlocked) return null;
|
|
2391
|
+
if (anyLandingBlocked) return null;
|
|
2109
2392
|
return anyResult ? "completed" : "failed";
|
|
2110
2393
|
}
|
|
2111
2394
|
function finalizeStaleRuns() {
|
|
@@ -2122,8 +2405,82 @@ function finalizeStaleRuns() {
|
|
|
2122
2405
|
return finalized;
|
|
2123
2406
|
}
|
|
2124
2407
|
|
|
2408
|
+
// src/stale-reconcile.ts
|
|
2409
|
+
var STALE_RECONCILE_HEARTBEAT_MS = 15 * 60 * 1e3;
|
|
2410
|
+
function staleReconcileDisabled() {
|
|
2411
|
+
return process.env.KYNVER_NO_STALE_CLEANUP === "1";
|
|
2412
|
+
}
|
|
2413
|
+
function reconcileStaleWorkers() {
|
|
2414
|
+
if (staleReconcileDisabled()) {
|
|
2415
|
+
return { workers: [], finalizedRuns: finalizeStaleRuns() };
|
|
2416
|
+
}
|
|
2417
|
+
const outcomes = [];
|
|
2418
|
+
const now = Date.now();
|
|
2419
|
+
for (const run of listRunRecords()) {
|
|
2420
|
+
for (const name of Object.keys(run.workers || {})) {
|
|
2421
|
+
const workerPath = path15.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json");
|
|
2422
|
+
const worker = readJson(workerPath, void 0);
|
|
2423
|
+
if (!worker || worker.status !== "running") {
|
|
2424
|
+
outcomes.push({
|
|
2425
|
+
runId: run.id,
|
|
2426
|
+
worker: name,
|
|
2427
|
+
action: "skipped",
|
|
2428
|
+
reason: worker ? `worker status is ${worker.status}` : "worker.json missing"
|
|
2429
|
+
});
|
|
2430
|
+
continue;
|
|
2431
|
+
}
|
|
2432
|
+
const status = computeWorkerStatus(worker, { base: run.base, baseCommit: run.baseCommit });
|
|
2433
|
+
if (status.finalResult) {
|
|
2434
|
+
outcomes.push({ runId: run.id, worker: name, action: "skipped", reason: "final result present" });
|
|
2435
|
+
continue;
|
|
2436
|
+
}
|
|
2437
|
+
if (!status.alive) {
|
|
2438
|
+
const nextStatus = status.attention.state === "blocked" ? "blocked" : status.status === "done" ? "done" : "exited";
|
|
2439
|
+
worker.status = nextStatus;
|
|
2440
|
+
worker.reconciledAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2441
|
+
worker.reconcileReason = status.attention.reason;
|
|
2442
|
+
saveWorker(run.id, worker);
|
|
2443
|
+
outcomes.push({
|
|
2444
|
+
runId: run.id,
|
|
2445
|
+
worker: name,
|
|
2446
|
+
action: "marked_exited",
|
|
2447
|
+
reason: status.attention.reason
|
|
2448
|
+
});
|
|
2449
|
+
continue;
|
|
2450
|
+
}
|
|
2451
|
+
if (status.attention.state === "stale" && worker.pid && isPidAlive(worker.pid)) {
|
|
2452
|
+
const hbMs = status.lastHeartbeatAt ? Date.parse(status.lastHeartbeatAt) : NaN;
|
|
2453
|
+
const actMs = status.lastActivityAt ? Date.parse(status.lastActivityAt) : NaN;
|
|
2454
|
+
const hbStale = !Number.isFinite(hbMs) || now - hbMs > STALE_RECONCILE_HEARTBEAT_MS;
|
|
2455
|
+
const actStale = Number.isFinite(actMs) && now - actMs > STALE_MS;
|
|
2456
|
+
if (hbStale && actStale) {
|
|
2457
|
+
killWorkerProcess(worker.pid, "SIGTERM");
|
|
2458
|
+
worker.status = "exited";
|
|
2459
|
+
worker.reconciledAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2460
|
+
worker.reconcileReason = `reconciled stale worker: ${status.attention.reason}`;
|
|
2461
|
+
saveWorker(run.id, worker);
|
|
2462
|
+
outcomes.push({
|
|
2463
|
+
runId: run.id,
|
|
2464
|
+
worker: name,
|
|
2465
|
+
action: "killed_stale",
|
|
2466
|
+
reason: status.attention.reason
|
|
2467
|
+
});
|
|
2468
|
+
continue;
|
|
2469
|
+
}
|
|
2470
|
+
}
|
|
2471
|
+
outcomes.push({
|
|
2472
|
+
runId: run.id,
|
|
2473
|
+
worker: name,
|
|
2474
|
+
action: "skipped",
|
|
2475
|
+
reason: status.attention.reason
|
|
2476
|
+
});
|
|
2477
|
+
}
|
|
2478
|
+
}
|
|
2479
|
+
return { workers: outcomes, finalizedRuns: finalizeStaleRuns() };
|
|
2480
|
+
}
|
|
2481
|
+
|
|
2125
2482
|
// src/plan-progress-daemon-sync.ts
|
|
2126
|
-
import
|
|
2483
|
+
import path16 from "node:path";
|
|
2127
2484
|
|
|
2128
2485
|
// src/plan-progress-sync.ts
|
|
2129
2486
|
async function syncPlanProgress(args) {
|
|
@@ -2147,7 +2504,7 @@ async function syncActiveWorkerPlanProgress(runId, args) {
|
|
|
2147
2504
|
const outcomes = [];
|
|
2148
2505
|
for (const name of Object.keys(run.workers || {})) {
|
|
2149
2506
|
const worker = readJson(
|
|
2150
|
-
|
|
2507
|
+
path16.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
2151
2508
|
void 0
|
|
2152
2509
|
);
|
|
2153
2510
|
if (!worker?.dispatched || !worker.taskId) continue;
|
|
@@ -2201,7 +2558,7 @@ async function completeFinishedWorkers(runId, args) {
|
|
|
2201
2558
|
const outcomes = [];
|
|
2202
2559
|
for (const name of Object.keys(run.workers || {})) {
|
|
2203
2560
|
const worker = readJson(
|
|
2204
|
-
|
|
2561
|
+
path17.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
2205
2562
|
void 0
|
|
2206
2563
|
);
|
|
2207
2564
|
if (!worker?.taskId) continue;
|
|
@@ -2236,7 +2593,7 @@ async function runPipelineTick(args) {
|
|
|
2236
2593
|
const execute = args.execute !== false && args.execute !== "false";
|
|
2237
2594
|
runStatus({ run: runId });
|
|
2238
2595
|
const completedWorkers = await completeFinishedWorkers(runId, args);
|
|
2239
|
-
const
|
|
2596
|
+
const staleReconcile = reconcileStaleWorkers();
|
|
2240
2597
|
const planProgressSync = await syncActiveWorkerPlanProgress(runId, args);
|
|
2241
2598
|
const workspacePrefs = await fetchWorkspaceRuntimePreferences(agentOsId, args);
|
|
2242
2599
|
const resourceGate = observeRunnerResourceGate({
|
|
@@ -2277,7 +2634,7 @@ async function runPipelineTick(args) {
|
|
|
2277
2634
|
execute,
|
|
2278
2635
|
resourceGate,
|
|
2279
2636
|
completedWorkers,
|
|
2280
|
-
|
|
2637
|
+
staleReconcile,
|
|
2281
2638
|
planProgressSync,
|
|
2282
2639
|
operatorTick,
|
|
2283
2640
|
sweep,
|
|
@@ -2495,6 +2852,7 @@ if (isCliEntry) {
|
|
|
2495
2852
|
}
|
|
2496
2853
|
export {
|
|
2497
2854
|
DEFAULT_DISPATCH_LEASE_MS,
|
|
2855
|
+
assessWorkerLanding,
|
|
2498
2856
|
autoCompleteWorker,
|
|
2499
2857
|
autoCompleteWorkerCli,
|
|
2500
2858
|
buildDispatchTaskText,
|
|
@@ -2507,6 +2865,7 @@ export {
|
|
|
2507
2865
|
dispatchRun,
|
|
2508
2866
|
getHarnessPaths,
|
|
2509
2867
|
isFinishedWorkerStatus,
|
|
2868
|
+
isLandingBlockedWorkerStatus,
|
|
2510
2869
|
listRuns,
|
|
2511
2870
|
loadUserConfig,
|
|
2512
2871
|
main,
|