svamp-cli 0.2.311 → 0.2.312
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/{adminCommands-U5D7i8tD.mjs → adminCommands-CRQQhXsw.mjs} +1 -1
- package/dist/{agentCommands-BefFTl6g.mjs → agentCommands-hLTce38n.mjs} +5 -5
- package/dist/{auth-DgGAjihq.mjs → auth-B2ZuhK_S.mjs} +1 -1
- package/dist/{cli-B0TyKXyU.mjs → cli-HI-J8Y7U.mjs} +70 -70
- package/dist/cli.mjs +2 -2
- package/dist/{commands-CnqL60f7.mjs → commands-12pIcOWK.mjs} +1 -1
- package/dist/{commands-BoGUxb2o.mjs → commands-C2zQmf3Y.mjs} +1 -1
- package/dist/{commands-XQ8-fOSu.mjs → commands-CmMh6R6S.mjs} +1 -1
- package/dist/{commands-xoalqGUY.mjs → commands-D6BqvSLe.mjs} +10 -5
- package/dist/{commands-imLC7CK2.mjs → commands-DGUgZoou.mjs} +2 -2
- package/dist/{commands-BCQbBzYU.mjs → commands-DpfYt_SX.mjs} +2 -2
- package/dist/{commands-CEOhve0Y.mjs → commands-IPkpFWbi.mjs} +2 -2
- package/dist/{commands-CjVl4amM.mjs → commands-ND3vwGkQ.mjs} +8 -8
- package/dist/{fleet-BS5eS0Wz.mjs → fleet-Cuv2uu1t.mjs} +2 -2
- package/dist/{frpc-DvBVvsZC.mjs → frpc-DbZDLphz.mjs} +1 -1
- package/dist/{headlessCli-C9wTLm1x.mjs → headlessCli-DakQSPzS.mjs} +2 -2
- package/dist/index.mjs +1 -1
- package/dist/{notifyCommands-0IltLO-S.mjs → notifyCommands-DhRbelQA.mjs} +1 -1
- package/dist/{package-CFhmZcVJ.mjs → package-BKX7lisO.mjs} +1 -1
- package/dist/{pinnedClaudeCode-zDzkOTi2.mjs → pinnedClaudeCode-mM9Q6Q2G.mjs} +1 -1
- package/dist/{rpc-BXpuORiQ.mjs → rpc-C7uDtxFw.mjs} +1 -1
- package/dist/{rpc-BLWr4p8g.mjs → rpc-Q4PWHeNn.mjs} +1 -1
- package/dist/{run-DtVX2pQN.mjs → run-Ccf8_4AF.mjs} +330 -75
- package/dist/{run-CMoQtnIj.mjs → run-DG0yDh6U.mjs} +1 -1
- package/dist/{scheduler-7aLvf5mn.mjs → scheduler-B1xThM3A.mjs} +1 -1
- package/dist/{serveCommands-D7BieIJ5.mjs → serveCommands-DDq9qzoN.mjs} +5 -5
- package/dist/{sideband-DjjXlVhw.mjs → sideband-CDdpZAXO.mjs} +1 -1
- package/package.json +1 -1
|
@@ -1121,7 +1121,13 @@ function buildSessionDeps(rpc, opts = {}) {
|
|
|
1121
1121
|
loop: {
|
|
1122
1122
|
task: opts2?.prompt?.trim() || "Work the ready issues in this session backlog until none remain (`svamp issue list --status ready`).",
|
|
1123
1123
|
oracle: "svamp issue pending",
|
|
1124
|
-
|
|
1124
|
+
// Durable backlog drainer: no forced iteration cap. A finite `?? 20` sent the loop
|
|
1125
|
+
// to a terminal `gave_up` after 20 turns even with issues remaining — a concrete
|
|
1126
|
+
// "loop stops easily" cause. Only an EXPLICIT user-set cap bounds it; otherwise the
|
|
1127
|
+
// loop runs unbounded (real runaway guard = the rolling per-hour token rate cap) and
|
|
1128
|
+
// goes DORMANT (armed) when the backlog clears. `durable:true` marks it re-arming.
|
|
1129
|
+
...opts2?.maxIterations ? { max_iterations: opts2.maxIterations } : {},
|
|
1130
|
+
durable: true,
|
|
1125
1131
|
evaluator: true
|
|
1126
1132
|
}
|
|
1127
1133
|
}, ctx);
|
|
@@ -3147,7 +3153,7 @@ Connection: close\r
|
|
|
3147
3153
|
const mount = this.mounts.get(mountName);
|
|
3148
3154
|
const subdomainOverride = mount?.access === "link" && mount.linkToken ? /* @__PURE__ */ new Map([[this.port, buildLinkSubdomain(subdomainSafe, mount.linkToken)]]) : void 0;
|
|
3149
3155
|
try {
|
|
3150
|
-
const { FrpcTunnel } = await import('./frpc-
|
|
3156
|
+
const { FrpcTunnel } = await import('./frpc-DbZDLphz.mjs');
|
|
3151
3157
|
let tunnel;
|
|
3152
3158
|
tunnel = new FrpcTunnel({
|
|
3153
3159
|
name: tunnelName,
|
|
@@ -6280,7 +6286,7 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
6280
6286
|
const tunnels = handlers.tunnels;
|
|
6281
6287
|
if (!tunnels) throw new Error("Tunnel management not available");
|
|
6282
6288
|
if (tunnels.has(params.name)) throw new Error(`Tunnel '${params.name}' already running`);
|
|
6283
|
-
const { FrpcTunnel } = await import('./frpc-
|
|
6289
|
+
const { FrpcTunnel } = await import('./frpc-DbZDLphz.mjs');
|
|
6284
6290
|
const tunnel = new FrpcTunnel({
|
|
6285
6291
|
name: params.name,
|
|
6286
6292
|
ports: params.ports,
|
|
@@ -6773,7 +6779,7 @@ QUESTION: ${params.question || "Summarize this concisely."}` }
|
|
|
6773
6779
|
}
|
|
6774
6780
|
const deps = buildSessionDeps(rpc, { cwd, ownerEmail: owner });
|
|
6775
6781
|
const sender = { name: context?.user?.email || context?.user?.id || "user", kind: "user", verified: true };
|
|
6776
|
-
const { toolsForRole } = await import('./sideband-
|
|
6782
|
+
const { toolsForRole } = await import('./sideband-CDdpZAXO.mjs');
|
|
6777
6783
|
const r2 = await runWiseAgent({ message: params.message, sender, config: { tools: toolsForRole(role2) }, deps, transport, model: resolved.model });
|
|
6778
6784
|
return fmt(r2);
|
|
6779
6785
|
}
|
|
@@ -6872,13 +6878,13 @@ QUESTION: ${params.question || "Summarize this concisely."}` }
|
|
|
6872
6878
|
hyphaWorkspace: u?.scope?.current_workspace
|
|
6873
6879
|
});
|
|
6874
6880
|
if (r.error || !r.sender) return { error: r.error || "unauthorized" };
|
|
6875
|
-
const callId = "call_" +
|
|
6881
|
+
const callId = "call_" + randomUUID().slice(0, 10);
|
|
6876
6882
|
const senderKey = (r.sender.name || "anonymous").toLowerCase();
|
|
6877
6883
|
if (!statelessLimiter.tryAcquire(senderKey)) {
|
|
6878
6884
|
return { ok: false, call_id: callId, status: "busy", error: "channel is busy (too many concurrent requests) \u2014 retry shortly" };
|
|
6879
6885
|
}
|
|
6880
6886
|
const rendered = renderMessage(c, { sender: r.sender, body: { message: kwargs.message }, callId });
|
|
6881
|
-
const { queryCore } = await import('./commands-
|
|
6887
|
+
const { queryCore } = await import('./commands-C2zQmf3Y.mjs');
|
|
6882
6888
|
const timeout = c.reply?.timeout_sec || 120;
|
|
6883
6889
|
let result;
|
|
6884
6890
|
try {
|
|
@@ -7018,7 +7024,7 @@ ${d?.error || "not found"}`;
|
|
|
7018
7024
|
try {
|
|
7019
7025
|
const saved = writeUpload(dir, v.target, dec.data);
|
|
7020
7026
|
try {
|
|
7021
|
-
new ChannelStore(dir).recordCall(c.id, { sender: r.sender.name, verified: r.sender.verified, callId: "up_" +
|
|
7027
|
+
new ChannelStore(dir).recordCall(c.id, { sender: r.sender.name, verified: r.sender.verified, callId: "up_" + randomUUID().slice(0, 10), outcome: "uploaded" });
|
|
7022
7028
|
} catch {
|
|
7023
7029
|
}
|
|
7024
7030
|
return { ok: true, name: saved.name, path: saved.relPath, bytes: saved.bytes };
|
|
@@ -7670,7 +7676,15 @@ async function _mintPairingCodeLocked(opts, resolver, ttlMs) {
|
|
|
7670
7676
|
}
|
|
7671
7677
|
|
|
7672
7678
|
const OUTPOST_STALE_MS = 9e4;
|
|
7673
|
-
const OUTPOST_EXEC_TIMEOUT_MS =
|
|
7679
|
+
const OUTPOST_EXEC_TIMEOUT_MS = 12 * 6e4;
|
|
7680
|
+
const OUTPOST_MAX_OUTPUT_BYTES = 1 << 20;
|
|
7681
|
+
function capOutpostOutput(s) {
|
|
7682
|
+
if (!s) return "";
|
|
7683
|
+
if (s.length <= OUTPOST_MAX_OUTPUT_BYTES) return s;
|
|
7684
|
+
const dropped = s.length - OUTPOST_MAX_OUTPUT_BYTES;
|
|
7685
|
+
return `[svamp-daemon] output truncated: ${dropped} earlier characters dropped]
|
|
7686
|
+
${s.slice(-OUTPOST_MAX_OUTPUT_BYTES)}`;
|
|
7687
|
+
}
|
|
7674
7688
|
class OutpostCoordinator {
|
|
7675
7689
|
conns = /* @__PURE__ */ new Map();
|
|
7676
7690
|
queue = /* @__PURE__ */ new Map();
|
|
@@ -7816,7 +7830,14 @@ class OutpostCoordinator {
|
|
|
7816
7830
|
const timer = this.setTimer(() => {
|
|
7817
7831
|
if (!this.pending.has(id)) return;
|
|
7818
7832
|
this.pending.delete(id);
|
|
7819
|
-
resolve({
|
|
7833
|
+
resolve({
|
|
7834
|
+
id,
|
|
7835
|
+
stdout: "",
|
|
7836
|
+
stderr: `command timed out after ${Math.round(this.execTimeoutMs / 1e3)}s with no result from the outpost \u2014 it may still be running remotely, so re-running it is NOT necessarily safe`,
|
|
7837
|
+
exitCode: -1,
|
|
7838
|
+
synthetic: true,
|
|
7839
|
+
ts: this.now()
|
|
7840
|
+
});
|
|
7820
7841
|
}, this.execTimeoutMs);
|
|
7821
7842
|
this.pending.set(id, { machine, resolve, timer, enqueuedAt: this.now() });
|
|
7822
7843
|
});
|
|
@@ -7840,8 +7861,11 @@ class OutpostCoordinator {
|
|
|
7840
7861
|
this.touch(p.machine);
|
|
7841
7862
|
p.resolve({
|
|
7842
7863
|
id: result.id,
|
|
7843
|
-
|
|
7844
|
-
|
|
7864
|
+
// #0814: the outpost caps its own output, but clamp again here — a hostile or
|
|
7865
|
+
// out-of-date outpost must not be able to push an unbounded string through the
|
|
7866
|
+
// daemon and out over Hypha RPC (the #0598 oversized-message class).
|
|
7867
|
+
stdout: capOutpostOutput(result.stdout),
|
|
7868
|
+
stderr: capOutpostOutput(result.stderr),
|
|
7845
7869
|
exitCode: typeof result.exitCode === "number" ? result.exitCode : 0,
|
|
7846
7870
|
ts: this.now()
|
|
7847
7871
|
});
|
|
@@ -8273,7 +8297,9 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
|
|
|
8273
8297
|
const who = conn.label || conn.machine;
|
|
8274
8298
|
const plat = [conn.os, conn.arch].filter(Boolean).join("/") || "unknown platform";
|
|
8275
8299
|
const msg = {
|
|
8276
|
-
|
|
8300
|
+
// #0816: CSPRNG — this id routes `svamp session inbox reply`, so a predictable
|
|
8301
|
+
// one lets a third party target another agent's reply thread.
|
|
8302
|
+
messageId: "op_" + randomUUID().slice(0, 10),
|
|
8277
8303
|
body: `A remote machine connected as an outpost: ${xmlEscape(who)} (${xmlEscape(plat)}). You can now run shell commands on it \u2014 its output returns synchronously, like a local command. Run one with: svamp outpost exec "uname -a"`,
|
|
8278
8304
|
timestamp: Date.now(),
|
|
8279
8305
|
read: false,
|
|
@@ -13937,6 +13963,7 @@ function shouldIsolate(input) {
|
|
|
13937
13963
|
function classifyRestoreContinuation(opts) {
|
|
13938
13964
|
if (opts.isOrphaned) return "none";
|
|
13939
13965
|
if (opts.loopActive) return "loop-resume";
|
|
13966
|
+
if (opts.loopArmed) return opts.armedHasPendingWork ? "loop-resume" : "none";
|
|
13940
13967
|
if (opts.wasProcessing && opts.hasResumeId) return "auto-continue";
|
|
13941
13968
|
return "none";
|
|
13942
13969
|
}
|
|
@@ -15048,7 +15075,7 @@ function searchIssues(projectRoot, query) {
|
|
|
15048
15075
|
}
|
|
15049
15076
|
function summarize(issues) {
|
|
15050
15077
|
const by = (s) => issues.filter((i) => i.status === s).length;
|
|
15051
|
-
return { total: issues.length, ready: by("ready"), in_progress: by("in_progress"), archived: by("archived") };
|
|
15078
|
+
return { total: issues.length, ready: by("ready"), in_progress: by("in_progress"), paused: by("paused"), archived: by("archived") };
|
|
15052
15079
|
}
|
|
15053
15080
|
|
|
15054
15081
|
function isWorkflowEnabled(wf) {
|
|
@@ -15606,7 +15633,15 @@ function loopCancelledDuringVerify(current) {
|
|
|
15606
15633
|
}
|
|
15607
15634
|
function backlogOraclePending(projectRoot, sessionId) {
|
|
15608
15635
|
const inScope = (i) => !sessionId || isVisibleTo(i, sessionId);
|
|
15609
|
-
return listIssues(projectRoot).filter(inScope).filter((i) => i.status
|
|
15636
|
+
return listIssues(projectRoot).filter(inScope).filter((i) => i.status !== "archived" && i.status !== "paused").map((i) => ({ id: i.id }));
|
|
15637
|
+
}
|
|
15638
|
+
function isBacklogDrainerOracle(oracle) {
|
|
15639
|
+
if (!oracle || typeof oracle !== "string") return false;
|
|
15640
|
+
return /(^|\s)svamp\s+issue\s+pending(\s|$)/.test(oracle.trim());
|
|
15641
|
+
}
|
|
15642
|
+
function resolveLoopDurable(cfg) {
|
|
15643
|
+
if (typeof cfg.durable === "boolean") return cfg.durable;
|
|
15644
|
+
return isBacklogDrainerOracle(cfg.oracle);
|
|
15610
15645
|
}
|
|
15611
15646
|
function issuesClosedSince(projectRoot, sinceMs) {
|
|
15612
15647
|
const all = listIssues(projectRoot, { includeArchived: true, status: "archived" });
|
|
@@ -15831,6 +15866,25 @@ function decideLoopContinuation(input) {
|
|
|
15831
15866
|
const tail = `no progress across ${stuckCheckpoints} checkpoints; ${maxAutoResumes} auto-resume(s) exhausted`;
|
|
15832
15867
|
return { action: "stop", kind: "stuck", reason: input.stuckReason ? `${input.stuckReason} \u2014 ${tail}` : tail };
|
|
15833
15868
|
}
|
|
15869
|
+
function terminalPhaseForDone(durable) {
|
|
15870
|
+
return durable ? "dormant" : "done";
|
|
15871
|
+
}
|
|
15872
|
+
const DEFAULT_LOOP_VERIFY_ERROR_RETRIES = 3;
|
|
15873
|
+
function resolveVerifyErrorRetries(envValue) {
|
|
15874
|
+
if (envValue == null || envValue.trim() === "") return DEFAULT_LOOP_VERIFY_ERROR_RETRIES;
|
|
15875
|
+
const n = Number(envValue);
|
|
15876
|
+
if (Number.isFinite(n) && n >= 0) return Math.floor(n);
|
|
15877
|
+
return DEFAULT_LOOP_VERIFY_ERROR_RETRIES;
|
|
15878
|
+
}
|
|
15879
|
+
function decideVerifyErrorHandling(input) {
|
|
15880
|
+
const maxRetries = typeof input.maxRetries === "number" && input.maxRetries >= 0 ? Math.floor(input.maxRetries) : DEFAULT_LOOP_VERIFY_ERROR_RETRIES;
|
|
15881
|
+
const attempt = (typeof input.priorErrors === "number" && input.priorErrors > 0 ? Math.floor(input.priorErrors) : 0) + 1;
|
|
15882
|
+
const why = input.reason ? String(input.reason).slice(0, 200) : "verification error";
|
|
15883
|
+
if (attempt <= maxRetries) {
|
|
15884
|
+
return { action: "retry", attempt, reason: `${why} \u2014 retry ${attempt}/${maxRetries} (keeping the loop running, not declaring done)` };
|
|
15885
|
+
}
|
|
15886
|
+
return { action: "park", attempt, reason: `${why} \u2014 ${maxRetries} verification retries exhausted; parking safely instead of a false "done"` };
|
|
15887
|
+
}
|
|
15834
15888
|
const DEFAULT_LOOP_EVALUATOR_MODEL = "claude-haiku-4-5-20251001";
|
|
15835
15889
|
function resolveEvaluatorModel(envValue) {
|
|
15836
15890
|
if (envValue === void 0) return DEFAULT_LOOP_EVALUATOR_MODEL;
|
|
@@ -15905,13 +15959,13 @@ Keep working until it is genuinely empty.` };
|
|
|
15905
15959
|
try {
|
|
15906
15960
|
raw = await deps.runEvaluator(prompt);
|
|
15907
15961
|
} catch (e) {
|
|
15908
|
-
log(`[loopVerify] evaluator spawn failed \u2014
|
|
15909
|
-
return { action: "
|
|
15962
|
+
log(`[loopVerify] evaluator spawn failed \u2014 signalling verify_error (fail-closed, will retry): ${e?.message || e}`);
|
|
15963
|
+
return { action: "verify_error", reason: `evaluator spawn failed: ${e?.message || e}` };
|
|
15910
15964
|
}
|
|
15911
15965
|
const verdict = parseEvaluatorVerdict(raw);
|
|
15912
15966
|
if (!verdict) {
|
|
15913
|
-
log(`[loopVerify] could not parse evaluator verdict \u2014
|
|
15914
|
-
return { action: "
|
|
15967
|
+
log(`[loopVerify] could not parse evaluator verdict \u2014 signalling verify_error (fail-closed, will retry)`);
|
|
15968
|
+
return { action: "verify_error", reason: "could not parse evaluator verdict" };
|
|
15915
15969
|
}
|
|
15916
15970
|
if (verdict.verdict === "done") {
|
|
15917
15971
|
log(`[loopVerify] evaluator confirms done${verdict.reason ? `: ${verdict.reason}` : ""}`);
|
|
@@ -16922,18 +16976,27 @@ function readLoopState(directory, sessionId) {
|
|
|
16922
16976
|
return null;
|
|
16923
16977
|
}
|
|
16924
16978
|
}
|
|
16979
|
+
function isTerminalLoopPhase(phase) {
|
|
16980
|
+
return phase === "done" || phase === "gave_up" || phase === "cancelled";
|
|
16981
|
+
}
|
|
16925
16982
|
function isLoopActive(directory, sessionId) {
|
|
16926
16983
|
const s = readLoopState(directory, sessionId);
|
|
16927
|
-
return !!s && s.active !== false && s.phase !== "
|
|
16984
|
+
return !!s && s.active !== false && s.phase !== "dormant" && !isTerminalLoopPhase(s.phase);
|
|
16928
16985
|
}
|
|
16929
16986
|
function loopOwnerSession(directory, sessionId) {
|
|
16930
16987
|
const s = readLoopState(directory, sessionId);
|
|
16931
|
-
if (!s || s.active === false || s.phase === "
|
|
16988
|
+
if (!s || s.active === false || s.phase === "dormant" || isTerminalLoopPhase(s.phase)) return null;
|
|
16932
16989
|
return typeof s.session_id === "string" ? s.session_id : null;
|
|
16933
16990
|
}
|
|
16934
16991
|
function isLoopActiveForSession(directory, sessionId) {
|
|
16935
16992
|
const s = readLoopState(directory, sessionId);
|
|
16936
|
-
if (!s || s.active === false || s.phase === "
|
|
16993
|
+
if (!s || s.active === false || s.phase === "dormant" || isTerminalLoopPhase(s.phase)) return false;
|
|
16994
|
+
if (typeof s.session_id !== "string") return true;
|
|
16995
|
+
return s.session_id === sessionId;
|
|
16996
|
+
}
|
|
16997
|
+
function isLoopArmedForSession(directory, sessionId) {
|
|
16998
|
+
const s = readLoopState(directory, sessionId);
|
|
16999
|
+
if (!s || s.active === false || s.phase !== "dormant") return false;
|
|
16937
17000
|
if (typeof s.session_id !== "string") return true;
|
|
16938
17001
|
return s.session_id === sessionId;
|
|
16939
17002
|
}
|
|
@@ -17002,6 +17065,14 @@ function loopResumeMessage(directory, sessionId) {
|
|
|
17002
17065
|
function stripOldLoopHooks(directory) {
|
|
17003
17066
|
try {
|
|
17004
17067
|
const p = join(directory, ".claude", "settings.json");
|
|
17068
|
+
if (!existsSync$1(p)) return false;
|
|
17069
|
+
return withFileLock(p + ".svamp-lock", () => stripOldLoopHooksLocked(p));
|
|
17070
|
+
} catch {
|
|
17071
|
+
return false;
|
|
17072
|
+
}
|
|
17073
|
+
}
|
|
17074
|
+
function stripOldLoopHooksLocked(p) {
|
|
17075
|
+
try {
|
|
17005
17076
|
if (!existsSync$1(p)) return false;
|
|
17006
17077
|
const cfg = JSON.parse(readFileSync$1(p, "utf-8"));
|
|
17007
17078
|
if (!cfg || typeof cfg.hooks !== "object" || !cfg.hooks) return false;
|
|
@@ -17025,13 +17096,31 @@ function stripOldLoopHooks(directory) {
|
|
|
17025
17096
|
}
|
|
17026
17097
|
if (changed) {
|
|
17027
17098
|
if (Object.keys(cfg.hooks).length === 0) delete cfg.hooks;
|
|
17028
|
-
|
|
17099
|
+
const tmp = `${p}.${process.pid}.tmp`;
|
|
17100
|
+
writeFileSync(tmp, JSON.stringify(cfg, null, 2));
|
|
17101
|
+
renameSync$1(tmp, p);
|
|
17029
17102
|
}
|
|
17030
17103
|
return changed;
|
|
17031
17104
|
} catch {
|
|
17032
17105
|
return false;
|
|
17033
17106
|
}
|
|
17034
17107
|
}
|
|
17108
|
+
const HELPER_KILL_GRACE_MS = 3e3;
|
|
17109
|
+
const BTW_TIMEOUT_MS = Number(process.env.SVAMP_BTW_TIMEOUT_MS) > 0 ? Number(process.env.SVAMP_BTW_TIMEOUT_MS) : 10 * 6e4;
|
|
17110
|
+
function killHelperTree(proc, graceMs = HELPER_KILL_GRACE_MS) {
|
|
17111
|
+
const pid = proc.pid;
|
|
17112
|
+
if (!pid) return;
|
|
17113
|
+
try {
|
|
17114
|
+
process.kill(-pid, "SIGTERM");
|
|
17115
|
+
} catch {
|
|
17116
|
+
}
|
|
17117
|
+
setTimeout(() => {
|
|
17118
|
+
try {
|
|
17119
|
+
process.kill(-pid, "SIGKILL");
|
|
17120
|
+
} catch {
|
|
17121
|
+
}
|
|
17122
|
+
}, graceMs).unref();
|
|
17123
|
+
}
|
|
17035
17124
|
function spawnHeadlessEvaluator(prompt, opts) {
|
|
17036
17125
|
return new Promise((resolve2, reject) => {
|
|
17037
17126
|
const evalArgs = [
|
|
@@ -17062,7 +17151,7 @@ function spawnHeadlessEvaluator(prompt, opts) {
|
|
|
17062
17151
|
delete evalEnv.CLAUDECODE;
|
|
17063
17152
|
let proc;
|
|
17064
17153
|
try {
|
|
17065
|
-
proc = spawn("claude", evalArgs, { cwd: opts.cwd, env: evalEnv, stdio: ["pipe", "pipe", "pipe"] });
|
|
17154
|
+
proc = spawn("claude", evalArgs, { cwd: opts.cwd, env: evalEnv, stdio: ["pipe", "pipe", "pipe"], detached: true });
|
|
17066
17155
|
} catch (e) {
|
|
17067
17156
|
reject(e);
|
|
17068
17157
|
return;
|
|
@@ -17071,10 +17160,7 @@ function spawnHeadlessEvaluator(prompt, opts) {
|
|
|
17071
17160
|
const timer = setTimeout(() => {
|
|
17072
17161
|
if (settled) return;
|
|
17073
17162
|
settled = true;
|
|
17074
|
-
|
|
17075
|
-
proc.kill("SIGTERM");
|
|
17076
|
-
} catch {
|
|
17077
|
-
}
|
|
17163
|
+
killHelperTree(proc);
|
|
17078
17164
|
reject(new Error("evaluator timeout"));
|
|
17079
17165
|
}, opts.timeoutMs && opts.timeoutMs > 0 ? opts.timeoutMs : 3e5);
|
|
17080
17166
|
proc.stderr?.on("data", (c) => {
|
|
@@ -17420,11 +17506,12 @@ function createSvampConfigChecker(directory, sessionId, getMetadata, setMetadata
|
|
|
17420
17506
|
const softTurnCap = computeSoftTurnCap(hardMax, LOOP_CHECKPOINT_CADENCE);
|
|
17421
17507
|
const condition = buildGoalCondition({ task, until, oracle, maxIterations: softTurnCap });
|
|
17422
17508
|
const budget = parseLoopBudget(cfg.budget);
|
|
17509
|
+
const durable = resolveLoopDurable({ durable: cfg.durable, oracle });
|
|
17423
17510
|
try {
|
|
17424
17511
|
stripOldLoopHooks(directory);
|
|
17425
17512
|
} catch {
|
|
17426
17513
|
}
|
|
17427
|
-
writeGoalLoopState(directory, sessionId, { active: true, phase: "running", iteration: 0, task: condition, goal_task: task, until, oracle, max_iterations: hardMax, budget, started_at: Date.now(), holds: 0, ledger: { turns: 0, tokens: 0, window: [] }, progress_history: [], auto_resumes: 0 });
|
|
17514
|
+
writeGoalLoopState(directory, sessionId, { active: true, phase: "running", durable, iteration: 0, task: condition, goal_task: task, until, oracle, max_iterations: hardMax, budget, started_at: Date.now(), holds: 0, ledger: { turns: 0, tokens: 0, window: [] }, progress_history: [], auto_resumes: 0, verify_errors: 0 });
|
|
17428
17515
|
const existingQueue = getMetadata().messageQueue || [];
|
|
17429
17516
|
setMetadata((m) => ({ ...m, messageQueue: [...existingQueue, { id: randomUUID$1(), text: `/goal ${condition}`, displayText: `\u{1F3AF} Goal: ${(task || until || "").slice(0, 100)}`, createdAt: Date.now() }] }));
|
|
17430
17517
|
onLoopActivated?.();
|
|
@@ -17877,7 +17964,7 @@ async function startDaemon(options) {
|
|
|
17877
17964
|
try {
|
|
17878
17965
|
const dir = loadSessionIndex()[sessionId]?.directory;
|
|
17879
17966
|
if (!dir) return;
|
|
17880
|
-
const { reconcileServiceLinks } = await import('./agentCommands-
|
|
17967
|
+
const { reconcileServiceLinks } = await import('./agentCommands-hLTce38n.mjs');
|
|
17881
17968
|
const configPath = getSvampConfigPath(dir, sessionId);
|
|
17882
17969
|
const config = readSvampConfig(configPath);
|
|
17883
17970
|
const entries = Array.from(urls.entries());
|
|
@@ -17895,7 +17982,7 @@ async function startDaemon(options) {
|
|
|
17895
17982
|
}
|
|
17896
17983
|
}
|
|
17897
17984
|
async function createExposedTunnel(spec) {
|
|
17898
|
-
const { FrpcTunnel } = await import('./frpc-
|
|
17985
|
+
const { FrpcTunnel } = await import('./frpc-DbZDLphz.mjs');
|
|
17899
17986
|
const tunnel = new FrpcTunnel({
|
|
17900
17987
|
name: spec.name,
|
|
17901
17988
|
ports: spec.ports,
|
|
@@ -17923,7 +18010,7 @@ async function startDaemon(options) {
|
|
|
17923
18010
|
ensureAutoInstalledCommands(logger);
|
|
17924
18011
|
(async () => {
|
|
17925
18012
|
try {
|
|
17926
|
-
const { beginClaudeVersionReconcile } = await import('./pinnedClaudeCode-
|
|
18013
|
+
const { beginClaudeVersionReconcile } = await import('./pinnedClaudeCode-mM9Q6Q2G.mjs');
|
|
17927
18014
|
beginClaudeVersionReconcile((msg) => logger.log(msg));
|
|
17928
18015
|
} catch (e) {
|
|
17929
18016
|
logger.log(`[claude-version] check failed: ${e?.message || e}`);
|
|
@@ -18418,12 +18505,16 @@ ${v.guidance ? v.guidance + "\n" : ""}Criteria: ${v.criteria || "(none)"}
|
|
|
18418
18505
|
"json",
|
|
18419
18506
|
"--max-turns",
|
|
18420
18507
|
"6"
|
|
18421
|
-
|
|
18508
|
+
// #0820: detached → own process group. Node's `timeout` option kills only
|
|
18509
|
+
// the direct child, so without this a tool grandchild survives the 60s cap
|
|
18510
|
+
// and reparents to init. The exit/error handlers below kill the group.
|
|
18511
|
+
], { cwd, timeout: 6e4, stdio: ["ignore", "pipe", "pipe"], env: forkEnv, detached: true });
|
|
18422
18512
|
let stdout = "";
|
|
18423
18513
|
child.stdout?.on("data", (d) => {
|
|
18424
18514
|
stdout += d.toString();
|
|
18425
18515
|
});
|
|
18426
18516
|
child.on("close", () => {
|
|
18517
|
+
killHelperTree(child);
|
|
18427
18518
|
try {
|
|
18428
18519
|
const r = JSON.parse(stdout);
|
|
18429
18520
|
resolve2(r.result || r.text || null);
|
|
@@ -18431,7 +18522,10 @@ ${v.guidance ? v.guidance + "\n" : ""}Criteria: ${v.criteria || "(none)"}
|
|
|
18431
18522
|
resolve2(null);
|
|
18432
18523
|
}
|
|
18433
18524
|
});
|
|
18434
|
-
child.on("error", () =>
|
|
18525
|
+
child.on("error", () => {
|
|
18526
|
+
killHelperTree(child);
|
|
18527
|
+
resolve2(null);
|
|
18528
|
+
});
|
|
18435
18529
|
} catch {
|
|
18436
18530
|
resolve2(null);
|
|
18437
18531
|
}
|
|
@@ -18514,6 +18608,13 @@ ${parts.join("\n")}`);
|
|
|
18514
18608
|
let startupRetryMessage;
|
|
18515
18609
|
let startupNonJsonLines = [];
|
|
18516
18610
|
let errorOutputLines = [];
|
|
18611
|
+
const MAX_ERROR_OUTPUT_LINES = 200;
|
|
18612
|
+
const pushErrorOutputLine = (line) => {
|
|
18613
|
+
errorOutputLines.push(line);
|
|
18614
|
+
if (errorOutputLines.length > MAX_ERROR_OUTPUT_LINES) {
|
|
18615
|
+
errorOutputLines.splice(0, errorOutputLines.length - MAX_ERROR_OUTPUT_LINES);
|
|
18616
|
+
}
|
|
18617
|
+
};
|
|
18517
18618
|
const STUCK_PROCESS_TIMEOUT_MS = 30 * 60 * 1e3;
|
|
18518
18619
|
const STUCK_CHECK_INTERVAL_MS = 2 * 60 * 1e3;
|
|
18519
18620
|
let lastOutputTime = Date.now();
|
|
@@ -18620,6 +18721,7 @@ ${parts.join("\n")}`);
|
|
|
18620
18721
|
const budget = ls.budget;
|
|
18621
18722
|
const progress_history = curLs.progress_history ?? ls.progress_history;
|
|
18622
18723
|
const extensions = curLs.extensions ?? ls.extensions;
|
|
18724
|
+
const durable = resolveLoopDurable({ durable: ls.durable, oracle: oracleCmd });
|
|
18623
18725
|
if (result.action === "rekick") {
|
|
18624
18726
|
const cond = ls.task || "";
|
|
18625
18727
|
writeGoalLoopState(directory, sessionId, {
|
|
@@ -18637,7 +18739,9 @@ ${parts.join("\n")}`);
|
|
|
18637
18739
|
ledger,
|
|
18638
18740
|
progress_history,
|
|
18639
18741
|
auto_resumes: ls.auto_resumes,
|
|
18640
|
-
extensions
|
|
18742
|
+
extensions,
|
|
18743
|
+
durable: ls.durable,
|
|
18744
|
+
verify_errors: 0
|
|
18641
18745
|
});
|
|
18642
18746
|
const guidance = result.guidance.replace(/\s+/g, " ").slice(0, 1200);
|
|
18643
18747
|
sessionService.pushMessage({ type: "message", message: `\u{1F501} Loop not done \u2014 independent review: ${result.reason}`, level: "warning" }, "event");
|
|
@@ -18674,7 +18778,9 @@ ${parts.join("\n")}`);
|
|
|
18674
18778
|
ledger,
|
|
18675
18779
|
progress_history,
|
|
18676
18780
|
auto_resumes: nextAuto,
|
|
18677
|
-
extensions
|
|
18781
|
+
extensions,
|
|
18782
|
+
durable: ls.durable,
|
|
18783
|
+
verify_errors: 0
|
|
18678
18784
|
});
|
|
18679
18785
|
const label = decision.kind === "autoresume" ? "\u{1F501} Auto-resuming stalled loop" : "\u{1F501} Loop still progressing";
|
|
18680
18786
|
sessionService.pushMessage({ type: "message", message: `${label} \u2014 ${decision.reason}` }, "event");
|
|
@@ -18687,15 +18793,75 @@ ${parts.join("\n")}`);
|
|
|
18687
18793
|
logger.log(`[Session ${sessionId}] [loop-persist] stuck-stop: ${decision.reason}`);
|
|
18688
18794
|
checkSvampConfig?.();
|
|
18689
18795
|
}
|
|
18796
|
+
} else if (result.action === "verify_error") {
|
|
18797
|
+
const decision = decideVerifyErrorHandling({
|
|
18798
|
+
priorErrors: ls.verify_errors,
|
|
18799
|
+
maxRetries: resolveVerifyErrorRetries(process.env.SVAMP_LOOP_VERIFY_ERROR_RETRIES),
|
|
18800
|
+
reason: result.reason
|
|
18801
|
+
});
|
|
18802
|
+
if (decision.action === "retry") {
|
|
18803
|
+
const cond = ls.task || "";
|
|
18804
|
+
writeGoalLoopState(directory, sessionId, {
|
|
18805
|
+
active: true,
|
|
18806
|
+
phase: "continue",
|
|
18807
|
+
iteration: ls.iteration || 0,
|
|
18808
|
+
task: ls.task,
|
|
18809
|
+
goal_task: ls.goal_task,
|
|
18810
|
+
until: ls.until,
|
|
18811
|
+
oracle: oracleCmd,
|
|
18812
|
+
max_iterations: ls.max_iterations,
|
|
18813
|
+
budget,
|
|
18814
|
+
started_at: startedAt,
|
|
18815
|
+
holds,
|
|
18816
|
+
ledger,
|
|
18817
|
+
progress_history,
|
|
18818
|
+
auto_resumes: ls.auto_resumes,
|
|
18819
|
+
extensions,
|
|
18820
|
+
durable: ls.durable,
|
|
18821
|
+
verify_errors: decision.attempt
|
|
18822
|
+
});
|
|
18823
|
+
sessionService.pushMessage({ type: "message", message: `\u26A0\uFE0F Loop verification hit a transient error \u2014 ${decision.reason}`, level: "warning" }, "event");
|
|
18824
|
+
logger.log(`[Session ${sessionId}] [loop-verify-error] ${decision.reason}`);
|
|
18825
|
+
enqueueLoopMessage(`/goal ${cond} \u2014 the independent completion check could not run (${result.reason}); CONTINUE the remaining work and it will be re-verified. Do NOT treat this as done.`, "\u26A0\uFE0F Re-verifying loop");
|
|
18826
|
+
if (!trackedSession.stopped) setTimeout(() => processMessageQueueRef?.(), 200);
|
|
18827
|
+
} else {
|
|
18828
|
+
const parkPhase = terminalPhaseForDone(durable);
|
|
18829
|
+
writeGoalLoopState(directory, sessionId, { ...ls, active: parkPhase === "dormant", phase: parkPhase, completed_at: Date.now(), holds, ledger, progress_history, auto_resumes: ls.auto_resumes, extensions, durable: ls.durable, verify_errors: 0 });
|
|
18830
|
+
sessionService.pushMessage({ type: "message", message: parkPhase === "dormant" ? `\u{1F319} Loop parked (armed) after repeated verification errors \u2014 it will re-arm when new work arrives. ${decision.reason}` : `\u26A0\uFE0F Loop stopped after repeated verification errors \u2014 ${decision.reason}`, level: "warning" }, "event");
|
|
18831
|
+
logger.log(`[Session ${sessionId}] [loop-verify-error] park ${parkPhase}: ${decision.reason}`);
|
|
18832
|
+
checkSvampConfig?.();
|
|
18833
|
+
}
|
|
18690
18834
|
} else {
|
|
18691
|
-
|
|
18835
|
+
const donePhase = terminalPhaseForDone(durable);
|
|
18836
|
+
if (donePhase === "dormant") {
|
|
18837
|
+
writeGoalLoopState(directory, sessionId, { ...ls, active: true, phase: "dormant", durable: true, completed_at: Date.now(), holds, ledger, progress_history, auto_resumes: ls.auto_resumes, extensions, verify_errors: 0 });
|
|
18838
|
+
sessionService.pushMessage({ type: "message", message: "\u{1F319} Backlog clear \u2014 loop is armed and watching for new issues (auto-resumes when work arrives)." }, "event");
|
|
18839
|
+
logger.log(`[Session ${sessionId}] [loop] backlog drained \u2192 dormant (armed)`);
|
|
18840
|
+
} else {
|
|
18841
|
+
writeGoalLoopState(directory, sessionId, { ...ls, active: false, phase: "done", completed_at: Date.now(), holds, ledger, progress_history, auto_resumes: ls.auto_resumes, extensions, verify_errors: 0 });
|
|
18842
|
+
}
|
|
18692
18843
|
checkSvampConfig?.();
|
|
18693
18844
|
}
|
|
18694
18845
|
} catch (e) {
|
|
18695
|
-
logger.log(`[Session ${sessionId}] verifyGoalCompletion
|
|
18846
|
+
logger.log(`[Session ${sessionId}] verifyGoalCompletion threw \u2014 failing closed (retry/park): ${e?.message || e}`);
|
|
18696
18847
|
try {
|
|
18697
|
-
|
|
18698
|
-
|
|
18848
|
+
const durable2 = resolveLoopDurable({ durable: ls.durable, oracle: oracleCmd });
|
|
18849
|
+
const decision = decideVerifyErrorHandling({
|
|
18850
|
+
priorErrors: ls.verify_errors,
|
|
18851
|
+
maxRetries: resolveVerifyErrorRetries(process.env.SVAMP_LOOP_VERIFY_ERROR_RETRIES),
|
|
18852
|
+
reason: `verify threw: ${e?.message || e}`
|
|
18853
|
+
});
|
|
18854
|
+
if (decision.action === "retry") {
|
|
18855
|
+
const cond = ls.task || "";
|
|
18856
|
+
writeGoalLoopState(directory, sessionId, { ...ls, active: true, phase: "continue", started_at: startedAt, verify_errors: decision.attempt });
|
|
18857
|
+
sessionService.pushMessage({ type: "message", message: `\u26A0\uFE0F Loop verification error \u2014 ${decision.reason}`, level: "warning" }, "event");
|
|
18858
|
+
enqueueLoopMessage(`/goal ${cond} \u2014 the completion check could not run; CONTINUE and it will be re-verified. Not done.`, "\u26A0\uFE0F Re-verifying loop");
|
|
18859
|
+
if (!trackedSession.stopped) setTimeout(() => processMessageQueueRef?.(), 200);
|
|
18860
|
+
} else {
|
|
18861
|
+
const parkPhase = terminalPhaseForDone(durable2);
|
|
18862
|
+
writeGoalLoopState(directory, sessionId, { ...ls, active: parkPhase === "dormant", phase: parkPhase, completed_at: Date.now(), verify_errors: 0 });
|
|
18863
|
+
checkSvampConfig?.();
|
|
18864
|
+
}
|
|
18699
18865
|
} catch {
|
|
18700
18866
|
}
|
|
18701
18867
|
} finally {
|
|
@@ -19213,7 +19379,7 @@ ${parts.join("\n")}`);
|
|
|
19213
19379
|
clearInboundContext(sessionId);
|
|
19214
19380
|
try {
|
|
19215
19381
|
const ls = readLoopState(directory, sessionId);
|
|
19216
|
-
if (ls && ls.engine === "goal" && ls.active !== false && ls.phase !== "done" && ls.phase !== "gave_up" && ls.phase !== "cancelled") {
|
|
19382
|
+
if (ls && ls.engine === "goal" && ls.active !== false && ls.phase !== "dormant" && ls.phase !== "done" && ls.phase !== "gave_up" && ls.phase !== "cancelled") {
|
|
19217
19383
|
const now = Date.now();
|
|
19218
19384
|
const ledger = accumulateLedger(ls.ledger, {
|
|
19219
19385
|
turns: Number(msg.num_turns) || 0,
|
|
@@ -19247,7 +19413,8 @@ ${parts.join("\n")}`);
|
|
|
19247
19413
|
sessionService.pushMessage({ type: "message", message: `\u{1F6D1} Loop stopped \u2014 ${detail}`, level: "warning" }, "event");
|
|
19248
19414
|
checkSvampConfig?.();
|
|
19249
19415
|
} else if (process.env.SVAMP_LOOP_VERIFY === "0") {
|
|
19250
|
-
|
|
19416
|
+
const donePhase = terminalPhaseForDone(resolveLoopDurable({ durable: ls.durable, oracle: ls.oracle }));
|
|
19417
|
+
writeGoalLoopState(directory, sessionId, { ...ls, active: donePhase === "dormant", phase: donePhase, completed_at: now, ledger, progress_history: prog.progress_history, auto_resumes: prog.auto_resumes });
|
|
19251
19418
|
} else {
|
|
19252
19419
|
writeGoalLoopState(directory, sessionId, { ...ls, ledger, progress_history: prog.progress_history, auto_resumes: prog.auto_resumes });
|
|
19253
19420
|
void verifyGoalCompletion(ls);
|
|
@@ -19383,7 +19550,7 @@ ${parts.join("\n")}`);
|
|
|
19383
19550
|
startupNonJsonLines.push(line.slice(0, 500));
|
|
19384
19551
|
}
|
|
19385
19552
|
if (looksLikeClaudeError(line)) {
|
|
19386
|
-
|
|
19553
|
+
pushErrorOutputLine(line.slice(0, 500));
|
|
19387
19554
|
}
|
|
19388
19555
|
}
|
|
19389
19556
|
}
|
|
@@ -19401,14 +19568,12 @@ ${parts.join("\n")}`);
|
|
|
19401
19568
|
stdoutBuffer = "";
|
|
19402
19569
|
}
|
|
19403
19570
|
});
|
|
19404
|
-
let stderrBuffer = "";
|
|
19405
19571
|
child.stderr?.on("data", (chunk) => {
|
|
19406
19572
|
const text = chunk.toString();
|
|
19407
19573
|
logger.log(`[Session ${sessionId}] Claude stderr: ${text.trim()}`);
|
|
19408
|
-
stderrBuffer += text;
|
|
19409
19574
|
for (const line of text.split("\n")) {
|
|
19410
19575
|
if (looksLikeClaudeError(line)) {
|
|
19411
|
-
|
|
19576
|
+
pushErrorOutputLine(line.trim().slice(0, 500));
|
|
19412
19577
|
}
|
|
19413
19578
|
}
|
|
19414
19579
|
});
|
|
@@ -19663,12 +19828,28 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
|
|
|
19663
19828
|
const btwProcess = spawn("claude", btwArgs, {
|
|
19664
19829
|
cwd: directory,
|
|
19665
19830
|
env: btwEnv,
|
|
19666
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
19831
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
19832
|
+
detached: true
|
|
19833
|
+
// #0820: own process group so the timeout can kill grandchildren
|
|
19667
19834
|
});
|
|
19668
19835
|
let btwResult = "";
|
|
19669
19836
|
let btwBuffer = "";
|
|
19670
19837
|
let btwStderr = "";
|
|
19671
19838
|
let settled = false;
|
|
19839
|
+
const btwTimer = setTimeout(() => {
|
|
19840
|
+
if (settled) return;
|
|
19841
|
+
settled = true;
|
|
19842
|
+
killHelperTree(btwProcess);
|
|
19843
|
+
const err = new Error(`/btw timed out after ${Math.round(BTW_TIMEOUT_MS / 1e3)}s`);
|
|
19844
|
+
err.retryable = false;
|
|
19845
|
+
reject(err);
|
|
19846
|
+
}, BTW_TIMEOUT_MS);
|
|
19847
|
+
const settleBtw = () => {
|
|
19848
|
+
if (settled) return false;
|
|
19849
|
+
settled = true;
|
|
19850
|
+
clearTimeout(btwTimer);
|
|
19851
|
+
return true;
|
|
19852
|
+
};
|
|
19672
19853
|
btwProcess.stderr?.on("data", (chunk) => {
|
|
19673
19854
|
btwStderr += chunk.toString();
|
|
19674
19855
|
});
|
|
@@ -19688,8 +19869,7 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
|
|
|
19688
19869
|
}
|
|
19689
19870
|
}
|
|
19690
19871
|
} else if (msg.type === "result") {
|
|
19691
|
-
if (
|
|
19692
|
-
settled = true;
|
|
19872
|
+
if (!settleBtw()) continue;
|
|
19693
19873
|
if (msg.is_error) {
|
|
19694
19874
|
const err = new Error(String(msg.result || "error"));
|
|
19695
19875
|
err.retryable = isRetryableRateLimit(String(msg.result || ""), msg.api_error_status);
|
|
@@ -19703,8 +19883,7 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
|
|
|
19703
19883
|
}
|
|
19704
19884
|
});
|
|
19705
19885
|
btwProcess.on("exit", (code) => {
|
|
19706
|
-
if (
|
|
19707
|
-
settled = true;
|
|
19886
|
+
if (!settleBtw()) return;
|
|
19708
19887
|
if (code === 0) {
|
|
19709
19888
|
resolve2(btwResult || "(no response)");
|
|
19710
19889
|
return;
|
|
@@ -19715,8 +19894,7 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
|
|
|
19715
19894
|
reject(err);
|
|
19716
19895
|
});
|
|
19717
19896
|
btwProcess.on("error", (e) => {
|
|
19718
|
-
if (
|
|
19719
|
-
settled = true;
|
|
19897
|
+
if (!settleBtw()) return;
|
|
19720
19898
|
const err = new Error(e.message);
|
|
19721
19899
|
err.retryable = false;
|
|
19722
19900
|
reject(err);
|
|
@@ -20149,11 +20327,11 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
|
|
|
20149
20327
|
});
|
|
20150
20328
|
},
|
|
20151
20329
|
onIssue: async (params) => {
|
|
20152
|
-
const { issueRpc } = await import('./rpc-
|
|
20330
|
+
const { issueRpc } = await import('./rpc-Q4PWHeNn.mjs');
|
|
20153
20331
|
return issueRpc(params?.cwd || directory, params || {}, { notifySession: notifyIssueOwner, rekickLoopOwner });
|
|
20154
20332
|
},
|
|
20155
20333
|
onWorkflow: async (params) => {
|
|
20156
|
-
const { workflowRpc } = await import('./rpc-
|
|
20334
|
+
const { workflowRpc } = await import('./rpc-C7uDtxFw.mjs');
|
|
20157
20335
|
return workflowRpc(params?.cwd || directory, params || {});
|
|
20158
20336
|
},
|
|
20159
20337
|
onRipgrep: async (args, cwd) => {
|
|
@@ -20362,7 +20540,7 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
|
|
|
20362
20540
|
logger.log(`[Session ${sessionId}] re-kick skipped \u2014 loop is resource-limit-stalled; human must run \`loop --max N\``);
|
|
20363
20541
|
return;
|
|
20364
20542
|
}
|
|
20365
|
-
if (s.active === false || s.phase === "done") {
|
|
20543
|
+
if (s.active === false || s.phase === "done" || s.phase === "dormant") {
|
|
20366
20544
|
try {
|
|
20367
20545
|
const lp = join(getLoopDir(directory, sessionId), "loop-state.json");
|
|
20368
20546
|
atomicWriteLoopState(lp, {
|
|
@@ -20372,6 +20550,8 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
|
|
|
20372
20550
|
iteration: 0,
|
|
20373
20551
|
completed_at: void 0,
|
|
20374
20552
|
gave_up_reason: void 0,
|
|
20553
|
+
verify_errors: 0,
|
|
20554
|
+
// fresh verification budget on re-arm
|
|
20375
20555
|
stall_hinted: false,
|
|
20376
20556
|
resumed_at: Date.now()
|
|
20377
20557
|
// #0156: re-arm the stall hint
|
|
@@ -20379,7 +20559,7 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
|
|
|
20379
20559
|
} catch {
|
|
20380
20560
|
}
|
|
20381
20561
|
}
|
|
20382
|
-
logger.log(`[Session ${sessionId}] issue change \u2192 re-kicking loop (budget remaining)`);
|
|
20562
|
+
logger.log(`[Session ${sessionId}] issue change \u2192 re-kicking loop (${s.phase === "dormant" ? "was dormant/armed" : "budget remaining"})`);
|
|
20383
20563
|
enqueueLoopContinue();
|
|
20384
20564
|
processMessageQueueRef?.();
|
|
20385
20565
|
};
|
|
@@ -20827,11 +21007,11 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
|
|
|
20827
21007
|
});
|
|
20828
21008
|
},
|
|
20829
21009
|
onIssue: async (params) => {
|
|
20830
|
-
const { issueRpc } = await import('./rpc-
|
|
21010
|
+
const { issueRpc } = await import('./rpc-Q4PWHeNn.mjs');
|
|
20831
21011
|
return issueRpc(params?.cwd || directory, params || {}, { notifySession: notifyIssueOwner, rekickLoopOwner });
|
|
20832
21012
|
},
|
|
20833
21013
|
onWorkflow: async (params) => {
|
|
20834
|
-
const { workflowRpc } = await import('./rpc-
|
|
21014
|
+
const { workflowRpc } = await import('./rpc-C7uDtxFw.mjs');
|
|
20835
21015
|
return workflowRpc(params?.cwd || directory, params || {});
|
|
20836
21016
|
},
|
|
20837
21017
|
onRipgrep: async (args, cwd) => {
|
|
@@ -21175,7 +21355,9 @@ ${oracle.output.trim().slice(0, 500)}`, "\u{1F501} Continuing loop");
|
|
|
21175
21355
|
return;
|
|
21176
21356
|
}
|
|
21177
21357
|
if (process.env.SVAMP_LOOP_VERIFY === "0") {
|
|
21178
|
-
|
|
21358
|
+
const killPhase = terminalPhaseForDone(resolveLoopDurable({ durable: ls.durable, oracle: oracleCmd }));
|
|
21359
|
+
writeGoalLoopState(directory, sessionId, { ...ls, active: killPhase === "dormant", phase: killPhase, completed_at: now, ledger, verify_errors: 0 });
|
|
21360
|
+
if (killPhase === "dormant") sessionService.pushMessage({ type: "message", message: "\u{1F319} Backlog clear \u2014 loop is armed and watching for new issues." }, "event");
|
|
21179
21361
|
checkSvampConfig?.();
|
|
21180
21362
|
return;
|
|
21181
21363
|
}
|
|
@@ -21217,15 +21399,48 @@ ${oracle.output.trim().slice(0, 500)}`, "\u{1F501} Continuing loop");
|
|
|
21217
21399
|
logger.log(`[Session ${sessionId}] [loop-persist] stuck-stop: ${decision.reason}`);
|
|
21218
21400
|
checkSvampConfig?.();
|
|
21219
21401
|
}
|
|
21402
|
+
} else if (result.action === "verify_error") {
|
|
21403
|
+
const decision = decideVerifyErrorHandling({
|
|
21404
|
+
priorErrors: ls.verify_errors,
|
|
21405
|
+
maxRetries: resolveVerifyErrorRetries(process.env.SVAMP_LOOP_VERIFY_ERROR_RETRIES),
|
|
21406
|
+
reason: result.reason
|
|
21407
|
+
});
|
|
21408
|
+
if (decision.action === "retry") {
|
|
21409
|
+
writeGoalLoopState(directory, sessionId, { ...ls, active: true, phase: "continue", holds, started_at: startedAt, ledger, verify_errors: decision.attempt });
|
|
21410
|
+
sessionService.pushMessage({ type: "message", message: `\u26A0\uFE0F Loop verification hit a transient error \u2014 ${decision.reason}`, level: "warning" }, "event");
|
|
21411
|
+
dispatchAcpPrompt(`Continue toward the goal: ${ls.goal_task || ls.until || "the task"}. The independent completion check could not run (${result.reason}); keep working \u2014 it will be re-verified. Do NOT treat this as done.`, "\u26A0\uFE0F Re-verifying loop");
|
|
21412
|
+
} else {
|
|
21413
|
+
const parkPhase = terminalPhaseForDone(resolveLoopDurable({ durable: ls.durable, oracle: oracleCmd }));
|
|
21414
|
+
writeGoalLoopState(directory, sessionId, { ...ls, active: parkPhase === "dormant", phase: parkPhase, completed_at: Date.now(), holds, ledger, verify_errors: 0 });
|
|
21415
|
+
sessionService.pushMessage({ type: "message", message: parkPhase === "dormant" ? `\u{1F319} Loop parked (armed) after repeated verification errors \u2014 re-arms on new work. ${decision.reason}` : `\u26A0\uFE0F Loop stopped after repeated verification errors \u2014 ${decision.reason}`, level: "warning" }, "event");
|
|
21416
|
+
checkSvampConfig?.();
|
|
21417
|
+
}
|
|
21220
21418
|
} else {
|
|
21221
|
-
|
|
21419
|
+
const donePhase = terminalPhaseForDone(resolveLoopDurable({ durable: ls.durable, oracle: oracleCmd }));
|
|
21420
|
+
if (donePhase === "dormant") {
|
|
21421
|
+
writeGoalLoopState(directory, sessionId, { ...ls, active: true, phase: "dormant", durable: true, completed_at: Date.now(), holds, ledger, verify_errors: 0 });
|
|
21422
|
+
sessionService.pushMessage({ type: "message", message: "\u{1F319} Backlog clear \u2014 loop is armed and watching for new issues." }, "event");
|
|
21423
|
+
} else {
|
|
21424
|
+
writeGoalLoopState(directory, sessionId, { ...ls, active: false, phase: "done", completed_at: Date.now(), holds, ledger, verify_errors: 0 });
|
|
21425
|
+
}
|
|
21222
21426
|
checkSvampConfig?.();
|
|
21223
21427
|
}
|
|
21224
21428
|
} catch (e) {
|
|
21225
|
-
logger.log(`[Session ${sessionId}] verifyAcpLoopCompletion error \u2014 failing
|
|
21429
|
+
logger.log(`[Session ${sessionId}] verifyAcpLoopCompletion error \u2014 failing closed (retry/park): ${e?.message || e}`);
|
|
21226
21430
|
try {
|
|
21227
|
-
|
|
21228
|
-
|
|
21431
|
+
const decision = decideVerifyErrorHandling({
|
|
21432
|
+
priorErrors: ls.verify_errors,
|
|
21433
|
+
maxRetries: resolveVerifyErrorRetries(process.env.SVAMP_LOOP_VERIFY_ERROR_RETRIES),
|
|
21434
|
+
reason: `verify threw: ${e?.message || e}`
|
|
21435
|
+
});
|
|
21436
|
+
if (decision.action === "retry") {
|
|
21437
|
+
writeGoalLoopState(directory, sessionId, { ...ls, active: true, phase: "continue", holds, started_at: startedAt, ledger, verify_errors: decision.attempt });
|
|
21438
|
+
dispatchAcpPrompt(`Continue toward the goal: ${ls.goal_task || ls.until || "the task"}. The completion check could not run; keep working \u2014 it will be re-verified. Not done.`, "\u26A0\uFE0F Re-verifying loop");
|
|
21439
|
+
} else {
|
|
21440
|
+
const parkPhase = terminalPhaseForDone(resolveLoopDurable({ durable: ls.durable, oracle: oracleCmd }));
|
|
21441
|
+
writeGoalLoopState(directory, sessionId, { ...ls, active: parkPhase === "dormant", phase: parkPhase, completed_at: Date.now(), ledger, verify_errors: 0 });
|
|
21442
|
+
checkSvampConfig?.();
|
|
21443
|
+
}
|
|
21229
21444
|
} catch {
|
|
21230
21445
|
}
|
|
21231
21446
|
} finally {
|
|
@@ -21789,7 +22004,7 @@ ${oracle.output.trim().slice(0, 500)}`, "\u{1F501} Continuing loop");
|
|
|
21789
22004
|
}
|
|
21790
22005
|
if (persistedSessions.length > 0) {
|
|
21791
22006
|
try {
|
|
21792
|
-
const { awaitClaudeVersionReady } = await import('./pinnedClaudeCode-
|
|
22007
|
+
const { awaitClaudeVersionReady } = await import('./pinnedClaudeCode-mM9Q6Q2G.mjs');
|
|
21793
22008
|
await awaitClaudeVersionReady();
|
|
21794
22009
|
} catch {
|
|
21795
22010
|
}
|
|
@@ -21822,12 +22037,26 @@ ${oracle.output.trim().slice(0, 500)}`, "\u{1F501} Continuing loop");
|
|
|
21822
22037
|
break;
|
|
21823
22038
|
}
|
|
21824
22039
|
}
|
|
22040
|
+
const _loopArmed = !isOrphaned && isLoopArmedForSession(persisted.directory, persisted.sessionId);
|
|
21825
22041
|
const _restoreAction = classifyRestoreContinuation({
|
|
21826
22042
|
wasProcessing: !!persisted.wasProcessing,
|
|
21827
22043
|
hasResumeId: !!persisted.claudeResumeId,
|
|
21828
22044
|
isOrphaned: !!isOrphaned,
|
|
21829
|
-
loopActive: isLoopActiveForSession(persisted.directory, persisted.sessionId)
|
|
22045
|
+
loopActive: isLoopActiveForSession(persisted.directory, persisted.sessionId),
|
|
22046
|
+
loopArmed: _loopArmed,
|
|
22047
|
+
armedHasPendingWork: _loopArmed ? safeBacklogPendingCount(persisted.directory, persisted.sessionId) > 0 : false
|
|
21830
22048
|
});
|
|
22049
|
+
if (_loopArmed) {
|
|
22050
|
+
logger.log(`[loop] Restored DORMANT loop for ${persisted.sessionId} \u2192 ${_restoreAction === "loop-resume" ? "re-arming (pending work found)" : "staying dormant/watching (backlog empty)"}`);
|
|
22051
|
+
if (_restoreAction === "loop-resume") {
|
|
22052
|
+
try {
|
|
22053
|
+
const lp = join(getLoopDir(persisted.directory, persisted.sessionId), "loop-state.json");
|
|
22054
|
+
const cur = readLoopState(persisted.directory, persisted.sessionId);
|
|
22055
|
+
if (cur) atomicWriteLoopState(lp, { ...cur, active: true, phase: "continue", completed_at: void 0, verify_errors: 0, resumed_at: Date.now() });
|
|
22056
|
+
} catch {
|
|
22057
|
+
}
|
|
22058
|
+
}
|
|
22059
|
+
}
|
|
21831
22060
|
if (_restoreAction === "auto-continue") {
|
|
21832
22061
|
sessionsToAutoContinue.push(persisted.sessionId);
|
|
21833
22062
|
} else if (_restoreAction === "loop-resume") {
|
|
@@ -21945,18 +22174,44 @@ ${oracle.output.trim().slice(0, 500)}`, "\u{1F501} Continuing loop");
|
|
|
21945
22174
|
logger.log(`Session ${sessionId} RPC handlers not found for loop resume`);
|
|
21946
22175
|
continue;
|
|
21947
22176
|
}
|
|
22177
|
+
const loopTurnFingerprint = () => {
|
|
22178
|
+
try {
|
|
22179
|
+
const s = readLoopState(sessDir, sessionId);
|
|
22180
|
+
return `${s?.phase || ""}|${s?.holds || 0}|${s?.ledger?.turns || 0}`;
|
|
22181
|
+
} catch {
|
|
22182
|
+
return "";
|
|
22183
|
+
}
|
|
22184
|
+
};
|
|
22185
|
+
const sendResume = async (tag) => {
|
|
22186
|
+
const prompt = loopResumeMessage(sessDir, sessionId);
|
|
22187
|
+
await rpc.sendMessage(
|
|
22188
|
+
JSON.stringify({
|
|
22189
|
+
role: "user",
|
|
22190
|
+
content: { type: "text", text: prompt },
|
|
22191
|
+
meta: { sentFrom: "svamp-daemon-loop-resume" }
|
|
22192
|
+
})
|
|
22193
|
+
);
|
|
22194
|
+
logger.log(`Resumed loop for session ${sessionId}${tag}`);
|
|
22195
|
+
};
|
|
21948
22196
|
setTimeout(async () => {
|
|
21949
22197
|
try {
|
|
21950
22198
|
if (!isLoopActiveForSession(sessDir, sessionId)) return;
|
|
21951
|
-
const
|
|
21952
|
-
await
|
|
21953
|
-
|
|
21954
|
-
|
|
21955
|
-
|
|
21956
|
-
|
|
21957
|
-
|
|
21958
|
-
|
|
21959
|
-
|
|
22199
|
+
const fpBefore = loopTurnFingerprint();
|
|
22200
|
+
await sendResume("");
|
|
22201
|
+
const graceMs = Math.max(5e3, Number(process.env.SVAMP_LOOP_RESUME_WATCHDOG_MS) || 45e3);
|
|
22202
|
+
setTimeout(() => {
|
|
22203
|
+
try {
|
|
22204
|
+
if (!isLoopActiveForSession(sessDir, sessionId)) return;
|
|
22205
|
+
const live = Array.from(pidToTrackedSession.values()).find((s) => s.svampSessionId === sessionId && !s.stopped);
|
|
22206
|
+
const lifecycle = live?.hyphaService?.getMetadata()?.lifecycleState;
|
|
22207
|
+
if (lifecycle && lifecycle !== "idle") return;
|
|
22208
|
+
if (loopTurnFingerprint() !== fpBefore) return;
|
|
22209
|
+
logger.log(`[loop] resume watchdog: session ${sessionId} loop still active but never advanced after ${graceMs}ms \u2014 re-issuing /goal once`);
|
|
22210
|
+
void sendResume(" (watchdog re-issue)").catch(() => {
|
|
22211
|
+
});
|
|
22212
|
+
} catch {
|
|
22213
|
+
}
|
|
22214
|
+
}, graceMs);
|
|
21960
22215
|
} catch (err) {
|
|
21961
22216
|
logger.log(`Failed to resume loop for session ${sessionId}: ${err.message}`);
|
|
21962
22217
|
}
|
|
@@ -21994,7 +22249,7 @@ ${oracle.output.trim().slice(0, 500)}`, "\u{1F501} Continuing loop");
|
|
|
21994
22249
|
const PING_TIMEOUT_MS = 15e3;
|
|
21995
22250
|
const POST_RECONNECT_GRACE_MS = 2e4;
|
|
21996
22251
|
const RECONNECT_JITTER_MS = 2500;
|
|
21997
|
-
const { WorkflowScheduler } = await import('./scheduler-
|
|
22252
|
+
const { WorkflowScheduler } = await import('./scheduler-B1xThM3A.mjs');
|
|
21998
22253
|
const workflowProjectRoots = () => {
|
|
21999
22254
|
const dirs = /* @__PURE__ */ new Set();
|
|
22000
22255
|
for (const s of pidToTrackedSession.values()) {
|