svamp-cli 0.2.311 → 0.2.313
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-DINkkchF.mjs} +1 -1
- package/dist/{agentCommands-BefFTl6g.mjs → agentCommands-CqHUKyRM.mjs} +5 -5
- package/dist/{auth-DgGAjihq.mjs → auth-fmYi12gU.mjs} +1 -1
- package/dist/{cli-B0TyKXyU.mjs → cli-C0K7bTFQ.mjs} +70 -70
- package/dist/cli.mjs +2 -2
- package/dist/{commands-CEOhve0Y.mjs → commands-B2KHp8-n.mjs} +2 -2
- package/dist/{commands-CjVl4amM.mjs → commands-BlSsMfW6.mjs} +8 -8
- package/dist/{commands-BoGUxb2o.mjs → commands-BspIWEyi.mjs} +1 -1
- package/dist/{commands-XQ8-fOSu.mjs → commands-CE9x3OVJ.mjs} +1 -1
- package/dist/{commands-CnqL60f7.mjs → commands-DMG7VDBo.mjs} +1 -1
- package/dist/{commands-imLC7CK2.mjs → commands-DxwqcKtb.mjs} +2 -2
- package/dist/{commands-xoalqGUY.mjs → commands-c57jjMV-.mjs} +10 -5
- package/dist/{commands-BCQbBzYU.mjs → commands-lt3_KA5S.mjs} +2 -2
- package/dist/{fleet-BS5eS0Wz.mjs → fleet-DX-cpu52.mjs} +2 -2
- package/dist/{frpc-DvBVvsZC.mjs → frpc-B5LHRKTD.mjs} +1 -1
- package/dist/{headlessCli-C9wTLm1x.mjs → headlessCli-BE4CanO7.mjs} +2 -2
- package/dist/index.mjs +1 -1
- package/dist/{notifyCommands-0IltLO-S.mjs → notifyCommands-Deh2F41t.mjs} +1 -1
- package/dist/{package-CFhmZcVJ.mjs → package-C9HRV5XP.mjs} +1 -1
- package/dist/{pinnedClaudeCode-zDzkOTi2.mjs → pinnedClaudeCode-mM9Q6Q2G.mjs} +1 -1
- package/dist/{rpc-BXpuORiQ.mjs → rpc-BxOm5nyE.mjs} +1 -1
- package/dist/{rpc-BLWr4p8g.mjs → rpc-DOl276lO.mjs} +1 -1
- package/dist/{run-CMoQtnIj.mjs → run-CDZ1g-Fa.mjs} +1 -1
- package/dist/{run-DtVX2pQN.mjs → run-DI8N2DhV.mjs} +364 -75
- package/dist/{scheduler-7aLvf5mn.mjs → scheduler-C0WXBdL_.mjs} +1 -1
- package/dist/{serveCommands-D7BieIJ5.mjs → serveCommands-DChM4ubY.mjs} +5 -5
- package/dist/{sideband-DjjXlVhw.mjs → sideband-BdHdwflN.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-B5LHRKTD.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-B5LHRKTD.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-BdHdwflN.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-BspIWEyi.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,21 @@ 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);
|
|
15645
|
+
}
|
|
15646
|
+
function shouldRearmDormantLoop(s, pendingCount, sessionId) {
|
|
15647
|
+
if (!s || s.phase !== "dormant") return false;
|
|
15648
|
+
if (typeof s.session_id === "string" && sessionId && s.session_id !== sessionId) return false;
|
|
15649
|
+
if (!isBacklogDrainerOracle(typeof s.oracle === "string" ? s.oracle : void 0)) return false;
|
|
15650
|
+
return pendingCount > 0;
|
|
15610
15651
|
}
|
|
15611
15652
|
function issuesClosedSince(projectRoot, sinceMs) {
|
|
15612
15653
|
const all = listIssues(projectRoot, { includeArchived: true, status: "archived" });
|
|
@@ -15831,6 +15872,25 @@ function decideLoopContinuation(input) {
|
|
|
15831
15872
|
const tail = `no progress across ${stuckCheckpoints} checkpoints; ${maxAutoResumes} auto-resume(s) exhausted`;
|
|
15832
15873
|
return { action: "stop", kind: "stuck", reason: input.stuckReason ? `${input.stuckReason} \u2014 ${tail}` : tail };
|
|
15833
15874
|
}
|
|
15875
|
+
function terminalPhaseForDone(durable) {
|
|
15876
|
+
return durable ? "dormant" : "done";
|
|
15877
|
+
}
|
|
15878
|
+
const DEFAULT_LOOP_VERIFY_ERROR_RETRIES = 3;
|
|
15879
|
+
function resolveVerifyErrorRetries(envValue) {
|
|
15880
|
+
if (envValue == null || envValue.trim() === "") return DEFAULT_LOOP_VERIFY_ERROR_RETRIES;
|
|
15881
|
+
const n = Number(envValue);
|
|
15882
|
+
if (Number.isFinite(n) && n >= 0) return Math.floor(n);
|
|
15883
|
+
return DEFAULT_LOOP_VERIFY_ERROR_RETRIES;
|
|
15884
|
+
}
|
|
15885
|
+
function decideVerifyErrorHandling(input) {
|
|
15886
|
+
const maxRetries = typeof input.maxRetries === "number" && input.maxRetries >= 0 ? Math.floor(input.maxRetries) : DEFAULT_LOOP_VERIFY_ERROR_RETRIES;
|
|
15887
|
+
const attempt = (typeof input.priorErrors === "number" && input.priorErrors > 0 ? Math.floor(input.priorErrors) : 0) + 1;
|
|
15888
|
+
const why = input.reason ? String(input.reason).slice(0, 200) : "verification error";
|
|
15889
|
+
if (attempt <= maxRetries) {
|
|
15890
|
+
return { action: "retry", attempt, reason: `${why} \u2014 retry ${attempt}/${maxRetries} (keeping the loop running, not declaring done)` };
|
|
15891
|
+
}
|
|
15892
|
+
return { action: "park", attempt, reason: `${why} \u2014 ${maxRetries} verification retries exhausted; parking safely instead of a false "done"` };
|
|
15893
|
+
}
|
|
15834
15894
|
const DEFAULT_LOOP_EVALUATOR_MODEL = "claude-haiku-4-5-20251001";
|
|
15835
15895
|
function resolveEvaluatorModel(envValue) {
|
|
15836
15896
|
if (envValue === void 0) return DEFAULT_LOOP_EVALUATOR_MODEL;
|
|
@@ -15905,13 +15965,13 @@ Keep working until it is genuinely empty.` };
|
|
|
15905
15965
|
try {
|
|
15906
15966
|
raw = await deps.runEvaluator(prompt);
|
|
15907
15967
|
} catch (e) {
|
|
15908
|
-
log(`[loopVerify] evaluator spawn failed \u2014
|
|
15909
|
-
return { action: "
|
|
15968
|
+
log(`[loopVerify] evaluator spawn failed \u2014 signalling verify_error (fail-closed, will retry): ${e?.message || e}`);
|
|
15969
|
+
return { action: "verify_error", reason: `evaluator spawn failed: ${e?.message || e}` };
|
|
15910
15970
|
}
|
|
15911
15971
|
const verdict = parseEvaluatorVerdict(raw);
|
|
15912
15972
|
if (!verdict) {
|
|
15913
|
-
log(`[loopVerify] could not parse evaluator verdict \u2014
|
|
15914
|
-
return { action: "
|
|
15973
|
+
log(`[loopVerify] could not parse evaluator verdict \u2014 signalling verify_error (fail-closed, will retry)`);
|
|
15974
|
+
return { action: "verify_error", reason: "could not parse evaluator verdict" };
|
|
15915
15975
|
}
|
|
15916
15976
|
if (verdict.verdict === "done") {
|
|
15917
15977
|
log(`[loopVerify] evaluator confirms done${verdict.reason ? `: ${verdict.reason}` : ""}`);
|
|
@@ -16922,18 +16982,27 @@ function readLoopState(directory, sessionId) {
|
|
|
16922
16982
|
return null;
|
|
16923
16983
|
}
|
|
16924
16984
|
}
|
|
16985
|
+
function isTerminalLoopPhase(phase) {
|
|
16986
|
+
return phase === "done" || phase === "gave_up" || phase === "cancelled";
|
|
16987
|
+
}
|
|
16925
16988
|
function isLoopActive(directory, sessionId) {
|
|
16926
16989
|
const s = readLoopState(directory, sessionId);
|
|
16927
|
-
return !!s && s.active !== false && s.phase !== "
|
|
16990
|
+
return !!s && s.active !== false && s.phase !== "dormant" && !isTerminalLoopPhase(s.phase);
|
|
16928
16991
|
}
|
|
16929
16992
|
function loopOwnerSession(directory, sessionId) {
|
|
16930
16993
|
const s = readLoopState(directory, sessionId);
|
|
16931
|
-
if (!s || s.active === false || s.phase === "
|
|
16994
|
+
if (!s || s.active === false || s.phase === "dormant" || isTerminalLoopPhase(s.phase)) return null;
|
|
16932
16995
|
return typeof s.session_id === "string" ? s.session_id : null;
|
|
16933
16996
|
}
|
|
16934
16997
|
function isLoopActiveForSession(directory, sessionId) {
|
|
16935
16998
|
const s = readLoopState(directory, sessionId);
|
|
16936
|
-
if (!s || s.active === false || s.phase === "
|
|
16999
|
+
if (!s || s.active === false || s.phase === "dormant" || isTerminalLoopPhase(s.phase)) return false;
|
|
17000
|
+
if (typeof s.session_id !== "string") return true;
|
|
17001
|
+
return s.session_id === sessionId;
|
|
17002
|
+
}
|
|
17003
|
+
function isLoopArmedForSession(directory, sessionId) {
|
|
17004
|
+
const s = readLoopState(directory, sessionId);
|
|
17005
|
+
if (!s || s.active === false || s.phase !== "dormant") return false;
|
|
16937
17006
|
if (typeof s.session_id !== "string") return true;
|
|
16938
17007
|
return s.session_id === sessionId;
|
|
16939
17008
|
}
|
|
@@ -17002,6 +17071,14 @@ function loopResumeMessage(directory, sessionId) {
|
|
|
17002
17071
|
function stripOldLoopHooks(directory) {
|
|
17003
17072
|
try {
|
|
17004
17073
|
const p = join(directory, ".claude", "settings.json");
|
|
17074
|
+
if (!existsSync$1(p)) return false;
|
|
17075
|
+
return withFileLock(p + ".svamp-lock", () => stripOldLoopHooksLocked(p));
|
|
17076
|
+
} catch {
|
|
17077
|
+
return false;
|
|
17078
|
+
}
|
|
17079
|
+
}
|
|
17080
|
+
function stripOldLoopHooksLocked(p) {
|
|
17081
|
+
try {
|
|
17005
17082
|
if (!existsSync$1(p)) return false;
|
|
17006
17083
|
const cfg = JSON.parse(readFileSync$1(p, "utf-8"));
|
|
17007
17084
|
if (!cfg || typeof cfg.hooks !== "object" || !cfg.hooks) return false;
|
|
@@ -17025,13 +17102,31 @@ function stripOldLoopHooks(directory) {
|
|
|
17025
17102
|
}
|
|
17026
17103
|
if (changed) {
|
|
17027
17104
|
if (Object.keys(cfg.hooks).length === 0) delete cfg.hooks;
|
|
17028
|
-
|
|
17105
|
+
const tmp = `${p}.${process.pid}.tmp`;
|
|
17106
|
+
writeFileSync(tmp, JSON.stringify(cfg, null, 2));
|
|
17107
|
+
renameSync$1(tmp, p);
|
|
17029
17108
|
}
|
|
17030
17109
|
return changed;
|
|
17031
17110
|
} catch {
|
|
17032
17111
|
return false;
|
|
17033
17112
|
}
|
|
17034
17113
|
}
|
|
17114
|
+
const HELPER_KILL_GRACE_MS = 3e3;
|
|
17115
|
+
const BTW_TIMEOUT_MS = Number(process.env.SVAMP_BTW_TIMEOUT_MS) > 0 ? Number(process.env.SVAMP_BTW_TIMEOUT_MS) : 10 * 6e4;
|
|
17116
|
+
function killHelperTree(proc, graceMs = HELPER_KILL_GRACE_MS) {
|
|
17117
|
+
const pid = proc.pid;
|
|
17118
|
+
if (!pid) return;
|
|
17119
|
+
try {
|
|
17120
|
+
process.kill(-pid, "SIGTERM");
|
|
17121
|
+
} catch {
|
|
17122
|
+
}
|
|
17123
|
+
setTimeout(() => {
|
|
17124
|
+
try {
|
|
17125
|
+
process.kill(-pid, "SIGKILL");
|
|
17126
|
+
} catch {
|
|
17127
|
+
}
|
|
17128
|
+
}, graceMs).unref();
|
|
17129
|
+
}
|
|
17035
17130
|
function spawnHeadlessEvaluator(prompt, opts) {
|
|
17036
17131
|
return new Promise((resolve2, reject) => {
|
|
17037
17132
|
const evalArgs = [
|
|
@@ -17062,7 +17157,7 @@ function spawnHeadlessEvaluator(prompt, opts) {
|
|
|
17062
17157
|
delete evalEnv.CLAUDECODE;
|
|
17063
17158
|
let proc;
|
|
17064
17159
|
try {
|
|
17065
|
-
proc = spawn("claude", evalArgs, { cwd: opts.cwd, env: evalEnv, stdio: ["pipe", "pipe", "pipe"] });
|
|
17160
|
+
proc = spawn("claude", evalArgs, { cwd: opts.cwd, env: evalEnv, stdio: ["pipe", "pipe", "pipe"], detached: true });
|
|
17066
17161
|
} catch (e) {
|
|
17067
17162
|
reject(e);
|
|
17068
17163
|
return;
|
|
@@ -17071,10 +17166,7 @@ function spawnHeadlessEvaluator(prompt, opts) {
|
|
|
17071
17166
|
const timer = setTimeout(() => {
|
|
17072
17167
|
if (settled) return;
|
|
17073
17168
|
settled = true;
|
|
17074
|
-
|
|
17075
|
-
proc.kill("SIGTERM");
|
|
17076
|
-
} catch {
|
|
17077
|
-
}
|
|
17169
|
+
killHelperTree(proc);
|
|
17078
17170
|
reject(new Error("evaluator timeout"));
|
|
17079
17171
|
}, opts.timeoutMs && opts.timeoutMs > 0 ? opts.timeoutMs : 3e5);
|
|
17080
17172
|
proc.stderr?.on("data", (c) => {
|
|
@@ -17420,11 +17512,12 @@ function createSvampConfigChecker(directory, sessionId, getMetadata, setMetadata
|
|
|
17420
17512
|
const softTurnCap = computeSoftTurnCap(hardMax, LOOP_CHECKPOINT_CADENCE);
|
|
17421
17513
|
const condition = buildGoalCondition({ task, until, oracle, maxIterations: softTurnCap });
|
|
17422
17514
|
const budget = parseLoopBudget(cfg.budget);
|
|
17515
|
+
const durable = resolveLoopDurable({ durable: cfg.durable, oracle });
|
|
17423
17516
|
try {
|
|
17424
17517
|
stripOldLoopHooks(directory);
|
|
17425
17518
|
} catch {
|
|
17426
17519
|
}
|
|
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 });
|
|
17520
|
+
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
17521
|
const existingQueue = getMetadata().messageQueue || [];
|
|
17429
17522
|
setMetadata((m) => ({ ...m, messageQueue: [...existingQueue, { id: randomUUID$1(), text: `/goal ${condition}`, displayText: `\u{1F3AF} Goal: ${(task || until || "").slice(0, 100)}`, createdAt: Date.now() }] }));
|
|
17430
17523
|
onLoopActivated?.();
|
|
@@ -17877,7 +17970,7 @@ async function startDaemon(options) {
|
|
|
17877
17970
|
try {
|
|
17878
17971
|
const dir = loadSessionIndex()[sessionId]?.directory;
|
|
17879
17972
|
if (!dir) return;
|
|
17880
|
-
const { reconcileServiceLinks } = await import('./agentCommands-
|
|
17973
|
+
const { reconcileServiceLinks } = await import('./agentCommands-CqHUKyRM.mjs');
|
|
17881
17974
|
const configPath = getSvampConfigPath(dir, sessionId);
|
|
17882
17975
|
const config = readSvampConfig(configPath);
|
|
17883
17976
|
const entries = Array.from(urls.entries());
|
|
@@ -17895,7 +17988,7 @@ async function startDaemon(options) {
|
|
|
17895
17988
|
}
|
|
17896
17989
|
}
|
|
17897
17990
|
async function createExposedTunnel(spec) {
|
|
17898
|
-
const { FrpcTunnel } = await import('./frpc-
|
|
17991
|
+
const { FrpcTunnel } = await import('./frpc-B5LHRKTD.mjs');
|
|
17899
17992
|
const tunnel = new FrpcTunnel({
|
|
17900
17993
|
name: spec.name,
|
|
17901
17994
|
ports: spec.ports,
|
|
@@ -17923,7 +18016,7 @@ async function startDaemon(options) {
|
|
|
17923
18016
|
ensureAutoInstalledCommands(logger);
|
|
17924
18017
|
(async () => {
|
|
17925
18018
|
try {
|
|
17926
|
-
const { beginClaudeVersionReconcile } = await import('./pinnedClaudeCode-
|
|
18019
|
+
const { beginClaudeVersionReconcile } = await import('./pinnedClaudeCode-mM9Q6Q2G.mjs');
|
|
17927
18020
|
beginClaudeVersionReconcile((msg) => logger.log(msg));
|
|
17928
18021
|
} catch (e) {
|
|
17929
18022
|
logger.log(`[claude-version] check failed: ${e?.message || e}`);
|
|
@@ -18418,12 +18511,16 @@ ${v.guidance ? v.guidance + "\n" : ""}Criteria: ${v.criteria || "(none)"}
|
|
|
18418
18511
|
"json",
|
|
18419
18512
|
"--max-turns",
|
|
18420
18513
|
"6"
|
|
18421
|
-
|
|
18514
|
+
// #0820: detached → own process group. Node's `timeout` option kills only
|
|
18515
|
+
// the direct child, so without this a tool grandchild survives the 60s cap
|
|
18516
|
+
// and reparents to init. The exit/error handlers below kill the group.
|
|
18517
|
+
], { cwd, timeout: 6e4, stdio: ["ignore", "pipe", "pipe"], env: forkEnv, detached: true });
|
|
18422
18518
|
let stdout = "";
|
|
18423
18519
|
child.stdout?.on("data", (d) => {
|
|
18424
18520
|
stdout += d.toString();
|
|
18425
18521
|
});
|
|
18426
18522
|
child.on("close", () => {
|
|
18523
|
+
killHelperTree(child);
|
|
18427
18524
|
try {
|
|
18428
18525
|
const r = JSON.parse(stdout);
|
|
18429
18526
|
resolve2(r.result || r.text || null);
|
|
@@ -18431,7 +18528,10 @@ ${v.guidance ? v.guidance + "\n" : ""}Criteria: ${v.criteria || "(none)"}
|
|
|
18431
18528
|
resolve2(null);
|
|
18432
18529
|
}
|
|
18433
18530
|
});
|
|
18434
|
-
child.on("error", () =>
|
|
18531
|
+
child.on("error", () => {
|
|
18532
|
+
killHelperTree(child);
|
|
18533
|
+
resolve2(null);
|
|
18534
|
+
});
|
|
18435
18535
|
} catch {
|
|
18436
18536
|
resolve2(null);
|
|
18437
18537
|
}
|
|
@@ -18514,6 +18614,13 @@ ${parts.join("\n")}`);
|
|
|
18514
18614
|
let startupRetryMessage;
|
|
18515
18615
|
let startupNonJsonLines = [];
|
|
18516
18616
|
let errorOutputLines = [];
|
|
18617
|
+
const MAX_ERROR_OUTPUT_LINES = 200;
|
|
18618
|
+
const pushErrorOutputLine = (line) => {
|
|
18619
|
+
errorOutputLines.push(line);
|
|
18620
|
+
if (errorOutputLines.length > MAX_ERROR_OUTPUT_LINES) {
|
|
18621
|
+
errorOutputLines.splice(0, errorOutputLines.length - MAX_ERROR_OUTPUT_LINES);
|
|
18622
|
+
}
|
|
18623
|
+
};
|
|
18517
18624
|
const STUCK_PROCESS_TIMEOUT_MS = 30 * 60 * 1e3;
|
|
18518
18625
|
const STUCK_CHECK_INTERVAL_MS = 2 * 60 * 1e3;
|
|
18519
18626
|
let lastOutputTime = Date.now();
|
|
@@ -18620,6 +18727,7 @@ ${parts.join("\n")}`);
|
|
|
18620
18727
|
const budget = ls.budget;
|
|
18621
18728
|
const progress_history = curLs.progress_history ?? ls.progress_history;
|
|
18622
18729
|
const extensions = curLs.extensions ?? ls.extensions;
|
|
18730
|
+
const durable = resolveLoopDurable({ durable: ls.durable, oracle: oracleCmd });
|
|
18623
18731
|
if (result.action === "rekick") {
|
|
18624
18732
|
const cond = ls.task || "";
|
|
18625
18733
|
writeGoalLoopState(directory, sessionId, {
|
|
@@ -18637,7 +18745,9 @@ ${parts.join("\n")}`);
|
|
|
18637
18745
|
ledger,
|
|
18638
18746
|
progress_history,
|
|
18639
18747
|
auto_resumes: ls.auto_resumes,
|
|
18640
|
-
extensions
|
|
18748
|
+
extensions,
|
|
18749
|
+
durable: ls.durable,
|
|
18750
|
+
verify_errors: 0
|
|
18641
18751
|
});
|
|
18642
18752
|
const guidance = result.guidance.replace(/\s+/g, " ").slice(0, 1200);
|
|
18643
18753
|
sessionService.pushMessage({ type: "message", message: `\u{1F501} Loop not done \u2014 independent review: ${result.reason}`, level: "warning" }, "event");
|
|
@@ -18674,7 +18784,9 @@ ${parts.join("\n")}`);
|
|
|
18674
18784
|
ledger,
|
|
18675
18785
|
progress_history,
|
|
18676
18786
|
auto_resumes: nextAuto,
|
|
18677
|
-
extensions
|
|
18787
|
+
extensions,
|
|
18788
|
+
durable: ls.durable,
|
|
18789
|
+
verify_errors: 0
|
|
18678
18790
|
});
|
|
18679
18791
|
const label = decision.kind === "autoresume" ? "\u{1F501} Auto-resuming stalled loop" : "\u{1F501} Loop still progressing";
|
|
18680
18792
|
sessionService.pushMessage({ type: "message", message: `${label} \u2014 ${decision.reason}` }, "event");
|
|
@@ -18687,15 +18799,75 @@ ${parts.join("\n")}`);
|
|
|
18687
18799
|
logger.log(`[Session ${sessionId}] [loop-persist] stuck-stop: ${decision.reason}`);
|
|
18688
18800
|
checkSvampConfig?.();
|
|
18689
18801
|
}
|
|
18802
|
+
} else if (result.action === "verify_error") {
|
|
18803
|
+
const decision = decideVerifyErrorHandling({
|
|
18804
|
+
priorErrors: ls.verify_errors,
|
|
18805
|
+
maxRetries: resolveVerifyErrorRetries(process.env.SVAMP_LOOP_VERIFY_ERROR_RETRIES),
|
|
18806
|
+
reason: result.reason
|
|
18807
|
+
});
|
|
18808
|
+
if (decision.action === "retry") {
|
|
18809
|
+
const cond = ls.task || "";
|
|
18810
|
+
writeGoalLoopState(directory, sessionId, {
|
|
18811
|
+
active: true,
|
|
18812
|
+
phase: "continue",
|
|
18813
|
+
iteration: ls.iteration || 0,
|
|
18814
|
+
task: ls.task,
|
|
18815
|
+
goal_task: ls.goal_task,
|
|
18816
|
+
until: ls.until,
|
|
18817
|
+
oracle: oracleCmd,
|
|
18818
|
+
max_iterations: ls.max_iterations,
|
|
18819
|
+
budget,
|
|
18820
|
+
started_at: startedAt,
|
|
18821
|
+
holds,
|
|
18822
|
+
ledger,
|
|
18823
|
+
progress_history,
|
|
18824
|
+
auto_resumes: ls.auto_resumes,
|
|
18825
|
+
extensions,
|
|
18826
|
+
durable: ls.durable,
|
|
18827
|
+
verify_errors: decision.attempt
|
|
18828
|
+
});
|
|
18829
|
+
sessionService.pushMessage({ type: "message", message: `\u26A0\uFE0F Loop verification hit a transient error \u2014 ${decision.reason}`, level: "warning" }, "event");
|
|
18830
|
+
logger.log(`[Session ${sessionId}] [loop-verify-error] ${decision.reason}`);
|
|
18831
|
+
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");
|
|
18832
|
+
if (!trackedSession.stopped) setTimeout(() => processMessageQueueRef?.(), 200);
|
|
18833
|
+
} else {
|
|
18834
|
+
const parkPhase = terminalPhaseForDone(durable);
|
|
18835
|
+
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 });
|
|
18836
|
+
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");
|
|
18837
|
+
logger.log(`[Session ${sessionId}] [loop-verify-error] park ${parkPhase}: ${decision.reason}`);
|
|
18838
|
+
checkSvampConfig?.();
|
|
18839
|
+
}
|
|
18690
18840
|
} else {
|
|
18691
|
-
|
|
18841
|
+
const donePhase = terminalPhaseForDone(durable);
|
|
18842
|
+
if (donePhase === "dormant") {
|
|
18843
|
+
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 });
|
|
18844
|
+
sessionService.pushMessage({ type: "message", message: "\u{1F319} Backlog clear \u2014 loop is armed and watching for new issues (auto-resumes when work arrives)." }, "event");
|
|
18845
|
+
logger.log(`[Session ${sessionId}] [loop] backlog drained \u2192 dormant (armed)`);
|
|
18846
|
+
} else {
|
|
18847
|
+
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 });
|
|
18848
|
+
}
|
|
18692
18849
|
checkSvampConfig?.();
|
|
18693
18850
|
}
|
|
18694
18851
|
} catch (e) {
|
|
18695
|
-
logger.log(`[Session ${sessionId}] verifyGoalCompletion
|
|
18852
|
+
logger.log(`[Session ${sessionId}] verifyGoalCompletion threw \u2014 failing closed (retry/park): ${e?.message || e}`);
|
|
18696
18853
|
try {
|
|
18697
|
-
|
|
18698
|
-
|
|
18854
|
+
const durable2 = resolveLoopDurable({ durable: ls.durable, oracle: oracleCmd });
|
|
18855
|
+
const decision = decideVerifyErrorHandling({
|
|
18856
|
+
priorErrors: ls.verify_errors,
|
|
18857
|
+
maxRetries: resolveVerifyErrorRetries(process.env.SVAMP_LOOP_VERIFY_ERROR_RETRIES),
|
|
18858
|
+
reason: `verify threw: ${e?.message || e}`
|
|
18859
|
+
});
|
|
18860
|
+
if (decision.action === "retry") {
|
|
18861
|
+
const cond = ls.task || "";
|
|
18862
|
+
writeGoalLoopState(directory, sessionId, { ...ls, active: true, phase: "continue", started_at: startedAt, verify_errors: decision.attempt });
|
|
18863
|
+
sessionService.pushMessage({ type: "message", message: `\u26A0\uFE0F Loop verification error \u2014 ${decision.reason}`, level: "warning" }, "event");
|
|
18864
|
+
enqueueLoopMessage(`/goal ${cond} \u2014 the completion check could not run; CONTINUE and it will be re-verified. Not done.`, "\u26A0\uFE0F Re-verifying loop");
|
|
18865
|
+
if (!trackedSession.stopped) setTimeout(() => processMessageQueueRef?.(), 200);
|
|
18866
|
+
} else {
|
|
18867
|
+
const parkPhase = terminalPhaseForDone(durable2);
|
|
18868
|
+
writeGoalLoopState(directory, sessionId, { ...ls, active: parkPhase === "dormant", phase: parkPhase, completed_at: Date.now(), verify_errors: 0 });
|
|
18869
|
+
checkSvampConfig?.();
|
|
18870
|
+
}
|
|
18699
18871
|
} catch {
|
|
18700
18872
|
}
|
|
18701
18873
|
} finally {
|
|
@@ -19213,7 +19385,7 @@ ${parts.join("\n")}`);
|
|
|
19213
19385
|
clearInboundContext(sessionId);
|
|
19214
19386
|
try {
|
|
19215
19387
|
const ls = readLoopState(directory, sessionId);
|
|
19216
|
-
if (ls && ls.engine === "goal" && ls.active !== false && ls.phase !== "done" && ls.phase !== "gave_up" && ls.phase !== "cancelled") {
|
|
19388
|
+
if (ls && ls.engine === "goal" && ls.active !== false && ls.phase !== "dormant" && ls.phase !== "done" && ls.phase !== "gave_up" && ls.phase !== "cancelled") {
|
|
19217
19389
|
const now = Date.now();
|
|
19218
19390
|
const ledger = accumulateLedger(ls.ledger, {
|
|
19219
19391
|
turns: Number(msg.num_turns) || 0,
|
|
@@ -19247,7 +19419,8 @@ ${parts.join("\n")}`);
|
|
|
19247
19419
|
sessionService.pushMessage({ type: "message", message: `\u{1F6D1} Loop stopped \u2014 ${detail}`, level: "warning" }, "event");
|
|
19248
19420
|
checkSvampConfig?.();
|
|
19249
19421
|
} else if (process.env.SVAMP_LOOP_VERIFY === "0") {
|
|
19250
|
-
|
|
19422
|
+
const donePhase = terminalPhaseForDone(resolveLoopDurable({ durable: ls.durable, oracle: ls.oracle }));
|
|
19423
|
+
writeGoalLoopState(directory, sessionId, { ...ls, active: donePhase === "dormant", phase: donePhase, completed_at: now, ledger, progress_history: prog.progress_history, auto_resumes: prog.auto_resumes });
|
|
19251
19424
|
} else {
|
|
19252
19425
|
writeGoalLoopState(directory, sessionId, { ...ls, ledger, progress_history: prog.progress_history, auto_resumes: prog.auto_resumes });
|
|
19253
19426
|
void verifyGoalCompletion(ls);
|
|
@@ -19383,7 +19556,7 @@ ${parts.join("\n")}`);
|
|
|
19383
19556
|
startupNonJsonLines.push(line.slice(0, 500));
|
|
19384
19557
|
}
|
|
19385
19558
|
if (looksLikeClaudeError(line)) {
|
|
19386
|
-
|
|
19559
|
+
pushErrorOutputLine(line.slice(0, 500));
|
|
19387
19560
|
}
|
|
19388
19561
|
}
|
|
19389
19562
|
}
|
|
@@ -19401,14 +19574,12 @@ ${parts.join("\n")}`);
|
|
|
19401
19574
|
stdoutBuffer = "";
|
|
19402
19575
|
}
|
|
19403
19576
|
});
|
|
19404
|
-
let stderrBuffer = "";
|
|
19405
19577
|
child.stderr?.on("data", (chunk) => {
|
|
19406
19578
|
const text = chunk.toString();
|
|
19407
19579
|
logger.log(`[Session ${sessionId}] Claude stderr: ${text.trim()}`);
|
|
19408
|
-
stderrBuffer += text;
|
|
19409
19580
|
for (const line of text.split("\n")) {
|
|
19410
19581
|
if (looksLikeClaudeError(line)) {
|
|
19411
|
-
|
|
19582
|
+
pushErrorOutputLine(line.trim().slice(0, 500));
|
|
19412
19583
|
}
|
|
19413
19584
|
}
|
|
19414
19585
|
});
|
|
@@ -19663,12 +19834,28 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
|
|
|
19663
19834
|
const btwProcess = spawn("claude", btwArgs, {
|
|
19664
19835
|
cwd: directory,
|
|
19665
19836
|
env: btwEnv,
|
|
19666
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
19837
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
19838
|
+
detached: true
|
|
19839
|
+
// #0820: own process group so the timeout can kill grandchildren
|
|
19667
19840
|
});
|
|
19668
19841
|
let btwResult = "";
|
|
19669
19842
|
let btwBuffer = "";
|
|
19670
19843
|
let btwStderr = "";
|
|
19671
19844
|
let settled = false;
|
|
19845
|
+
const btwTimer = setTimeout(() => {
|
|
19846
|
+
if (settled) return;
|
|
19847
|
+
settled = true;
|
|
19848
|
+
killHelperTree(btwProcess);
|
|
19849
|
+
const err = new Error(`/btw timed out after ${Math.round(BTW_TIMEOUT_MS / 1e3)}s`);
|
|
19850
|
+
err.retryable = false;
|
|
19851
|
+
reject(err);
|
|
19852
|
+
}, BTW_TIMEOUT_MS);
|
|
19853
|
+
const settleBtw = () => {
|
|
19854
|
+
if (settled) return false;
|
|
19855
|
+
settled = true;
|
|
19856
|
+
clearTimeout(btwTimer);
|
|
19857
|
+
return true;
|
|
19858
|
+
};
|
|
19672
19859
|
btwProcess.stderr?.on("data", (chunk) => {
|
|
19673
19860
|
btwStderr += chunk.toString();
|
|
19674
19861
|
});
|
|
@@ -19688,8 +19875,7 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
|
|
|
19688
19875
|
}
|
|
19689
19876
|
}
|
|
19690
19877
|
} else if (msg.type === "result") {
|
|
19691
|
-
if (
|
|
19692
|
-
settled = true;
|
|
19878
|
+
if (!settleBtw()) continue;
|
|
19693
19879
|
if (msg.is_error) {
|
|
19694
19880
|
const err = new Error(String(msg.result || "error"));
|
|
19695
19881
|
err.retryable = isRetryableRateLimit(String(msg.result || ""), msg.api_error_status);
|
|
@@ -19703,8 +19889,7 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
|
|
|
19703
19889
|
}
|
|
19704
19890
|
});
|
|
19705
19891
|
btwProcess.on("exit", (code) => {
|
|
19706
|
-
if (
|
|
19707
|
-
settled = true;
|
|
19892
|
+
if (!settleBtw()) return;
|
|
19708
19893
|
if (code === 0) {
|
|
19709
19894
|
resolve2(btwResult || "(no response)");
|
|
19710
19895
|
return;
|
|
@@ -19715,8 +19900,7 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
|
|
|
19715
19900
|
reject(err);
|
|
19716
19901
|
});
|
|
19717
19902
|
btwProcess.on("error", (e) => {
|
|
19718
|
-
if (
|
|
19719
|
-
settled = true;
|
|
19903
|
+
if (!settleBtw()) return;
|
|
19720
19904
|
const err = new Error(e.message);
|
|
19721
19905
|
err.retryable = false;
|
|
19722
19906
|
reject(err);
|
|
@@ -20149,11 +20333,11 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
|
|
|
20149
20333
|
});
|
|
20150
20334
|
},
|
|
20151
20335
|
onIssue: async (params) => {
|
|
20152
|
-
const { issueRpc } = await import('./rpc-
|
|
20336
|
+
const { issueRpc } = await import('./rpc-DOl276lO.mjs');
|
|
20153
20337
|
return issueRpc(params?.cwd || directory, params || {}, { notifySession: notifyIssueOwner, rekickLoopOwner });
|
|
20154
20338
|
},
|
|
20155
20339
|
onWorkflow: async (params) => {
|
|
20156
|
-
const { workflowRpc } = await import('./rpc-
|
|
20340
|
+
const { workflowRpc } = await import('./rpc-BxOm5nyE.mjs');
|
|
20157
20341
|
return workflowRpc(params?.cwd || directory, params || {});
|
|
20158
20342
|
},
|
|
20159
20343
|
onRipgrep: async (args, cwd) => {
|
|
@@ -20362,7 +20546,7 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
|
|
|
20362
20546
|
logger.log(`[Session ${sessionId}] re-kick skipped \u2014 loop is resource-limit-stalled; human must run \`loop --max N\``);
|
|
20363
20547
|
return;
|
|
20364
20548
|
}
|
|
20365
|
-
if (s.active === false || s.phase === "done") {
|
|
20549
|
+
if (s.active === false || s.phase === "done" || s.phase === "dormant") {
|
|
20366
20550
|
try {
|
|
20367
20551
|
const lp = join(getLoopDir(directory, sessionId), "loop-state.json");
|
|
20368
20552
|
atomicWriteLoopState(lp, {
|
|
@@ -20372,6 +20556,8 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
|
|
|
20372
20556
|
iteration: 0,
|
|
20373
20557
|
completed_at: void 0,
|
|
20374
20558
|
gave_up_reason: void 0,
|
|
20559
|
+
verify_errors: 0,
|
|
20560
|
+
// fresh verification budget on re-arm
|
|
20375
20561
|
stall_hinted: false,
|
|
20376
20562
|
resumed_at: Date.now()
|
|
20377
20563
|
// #0156: re-arm the stall hint
|
|
@@ -20379,7 +20565,7 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
|
|
|
20379
20565
|
} catch {
|
|
20380
20566
|
}
|
|
20381
20567
|
}
|
|
20382
|
-
logger.log(`[Session ${sessionId}] issue change \u2192 re-kicking loop (budget remaining)`);
|
|
20568
|
+
logger.log(`[Session ${sessionId}] issue change \u2192 re-kicking loop (${s.phase === "dormant" ? "was dormant/armed" : "budget remaining"})`);
|
|
20383
20569
|
enqueueLoopContinue();
|
|
20384
20570
|
processMessageQueueRef?.();
|
|
20385
20571
|
};
|
|
@@ -20428,6 +20614,34 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
|
|
|
20428
20614
|
if (INBOX_RECONCILE_INTERVAL_MS > 0) {
|
|
20429
20615
|
inboxReconcileTimer = setInterval(reconcileIdleInbox, INBOX_RECONCILE_INTERVAL_MS);
|
|
20430
20616
|
}
|
|
20617
|
+
const dormantSweepMs = Number(process.env.SVAMP_LOOP_DORMANT_SWEEP_MS);
|
|
20618
|
+
const DORMANT_SWEEP_INTERVAL_MS = Number.isFinite(dormantSweepMs) && dormantSweepMs >= 0 ? dormantSweepMs : 2e4;
|
|
20619
|
+
let dormantRearmTimer = null;
|
|
20620
|
+
const sweepDormantRearm = () => {
|
|
20621
|
+
if (trackedSession.stopped) {
|
|
20622
|
+
if (dormantRearmTimer) clearInterval(dormantRearmTimer);
|
|
20623
|
+
return;
|
|
20624
|
+
}
|
|
20625
|
+
let s = null;
|
|
20626
|
+
try {
|
|
20627
|
+
s = readLoopState(directory, sessionId);
|
|
20628
|
+
} catch {
|
|
20629
|
+
return;
|
|
20630
|
+
}
|
|
20631
|
+
if (!s || s.phase !== "dormant") return;
|
|
20632
|
+
let pending = [];
|
|
20633
|
+
try {
|
|
20634
|
+
pending = backlogOraclePending(directory, sessionId);
|
|
20635
|
+
} catch {
|
|
20636
|
+
return;
|
|
20637
|
+
}
|
|
20638
|
+
if (!shouldRearmDormantLoop(s, pending.length, sessionId)) return;
|
|
20639
|
+
logger.log(`[Session ${sessionId}] Dormant re-arm sweep: ${pending.length} pending issue(s) on disk (added outside the daemon RPC) \u2014 re-arming loop`);
|
|
20640
|
+
trackedSession.rekickLoop?.();
|
|
20641
|
+
};
|
|
20642
|
+
if (DORMANT_SWEEP_INTERVAL_MS > 0) {
|
|
20643
|
+
dormantRearmTimer = setInterval(sweepDormantRearm, DORMANT_SWEEP_INTERVAL_MS);
|
|
20644
|
+
}
|
|
20431
20645
|
return {
|
|
20432
20646
|
type: "success",
|
|
20433
20647
|
sessionId,
|
|
@@ -20827,11 +21041,11 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
|
|
|
20827
21041
|
});
|
|
20828
21042
|
},
|
|
20829
21043
|
onIssue: async (params) => {
|
|
20830
|
-
const { issueRpc } = await import('./rpc-
|
|
21044
|
+
const { issueRpc } = await import('./rpc-DOl276lO.mjs');
|
|
20831
21045
|
return issueRpc(params?.cwd || directory, params || {}, { notifySession: notifyIssueOwner, rekickLoopOwner });
|
|
20832
21046
|
},
|
|
20833
21047
|
onWorkflow: async (params) => {
|
|
20834
|
-
const { workflowRpc } = await import('./rpc-
|
|
21048
|
+
const { workflowRpc } = await import('./rpc-BxOm5nyE.mjs');
|
|
20835
21049
|
return workflowRpc(params?.cwd || directory, params || {});
|
|
20836
21050
|
},
|
|
20837
21051
|
onRipgrep: async (args, cwd) => {
|
|
@@ -21175,7 +21389,9 @@ ${oracle.output.trim().slice(0, 500)}`, "\u{1F501} Continuing loop");
|
|
|
21175
21389
|
return;
|
|
21176
21390
|
}
|
|
21177
21391
|
if (process.env.SVAMP_LOOP_VERIFY === "0") {
|
|
21178
|
-
|
|
21392
|
+
const killPhase = terminalPhaseForDone(resolveLoopDurable({ durable: ls.durable, oracle: oracleCmd }));
|
|
21393
|
+
writeGoalLoopState(directory, sessionId, { ...ls, active: killPhase === "dormant", phase: killPhase, completed_at: now, ledger, verify_errors: 0 });
|
|
21394
|
+
if (killPhase === "dormant") sessionService.pushMessage({ type: "message", message: "\u{1F319} Backlog clear \u2014 loop is armed and watching for new issues." }, "event");
|
|
21179
21395
|
checkSvampConfig?.();
|
|
21180
21396
|
return;
|
|
21181
21397
|
}
|
|
@@ -21217,15 +21433,48 @@ ${oracle.output.trim().slice(0, 500)}`, "\u{1F501} Continuing loop");
|
|
|
21217
21433
|
logger.log(`[Session ${sessionId}] [loop-persist] stuck-stop: ${decision.reason}`);
|
|
21218
21434
|
checkSvampConfig?.();
|
|
21219
21435
|
}
|
|
21436
|
+
} else if (result.action === "verify_error") {
|
|
21437
|
+
const decision = decideVerifyErrorHandling({
|
|
21438
|
+
priorErrors: ls.verify_errors,
|
|
21439
|
+
maxRetries: resolveVerifyErrorRetries(process.env.SVAMP_LOOP_VERIFY_ERROR_RETRIES),
|
|
21440
|
+
reason: result.reason
|
|
21441
|
+
});
|
|
21442
|
+
if (decision.action === "retry") {
|
|
21443
|
+
writeGoalLoopState(directory, sessionId, { ...ls, active: true, phase: "continue", holds, started_at: startedAt, ledger, verify_errors: decision.attempt });
|
|
21444
|
+
sessionService.pushMessage({ type: "message", message: `\u26A0\uFE0F Loop verification hit a transient error \u2014 ${decision.reason}`, level: "warning" }, "event");
|
|
21445
|
+
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");
|
|
21446
|
+
} else {
|
|
21447
|
+
const parkPhase = terminalPhaseForDone(resolveLoopDurable({ durable: ls.durable, oracle: oracleCmd }));
|
|
21448
|
+
writeGoalLoopState(directory, sessionId, { ...ls, active: parkPhase === "dormant", phase: parkPhase, completed_at: Date.now(), holds, ledger, verify_errors: 0 });
|
|
21449
|
+
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");
|
|
21450
|
+
checkSvampConfig?.();
|
|
21451
|
+
}
|
|
21220
21452
|
} else {
|
|
21221
|
-
|
|
21453
|
+
const donePhase = terminalPhaseForDone(resolveLoopDurable({ durable: ls.durable, oracle: oracleCmd }));
|
|
21454
|
+
if (donePhase === "dormant") {
|
|
21455
|
+
writeGoalLoopState(directory, sessionId, { ...ls, active: true, phase: "dormant", durable: true, completed_at: Date.now(), holds, ledger, verify_errors: 0 });
|
|
21456
|
+
sessionService.pushMessage({ type: "message", message: "\u{1F319} Backlog clear \u2014 loop is armed and watching for new issues." }, "event");
|
|
21457
|
+
} else {
|
|
21458
|
+
writeGoalLoopState(directory, sessionId, { ...ls, active: false, phase: "done", completed_at: Date.now(), holds, ledger, verify_errors: 0 });
|
|
21459
|
+
}
|
|
21222
21460
|
checkSvampConfig?.();
|
|
21223
21461
|
}
|
|
21224
21462
|
} catch (e) {
|
|
21225
|
-
logger.log(`[Session ${sessionId}] verifyAcpLoopCompletion error \u2014 failing
|
|
21463
|
+
logger.log(`[Session ${sessionId}] verifyAcpLoopCompletion error \u2014 failing closed (retry/park): ${e?.message || e}`);
|
|
21226
21464
|
try {
|
|
21227
|
-
|
|
21228
|
-
|
|
21465
|
+
const decision = decideVerifyErrorHandling({
|
|
21466
|
+
priorErrors: ls.verify_errors,
|
|
21467
|
+
maxRetries: resolveVerifyErrorRetries(process.env.SVAMP_LOOP_VERIFY_ERROR_RETRIES),
|
|
21468
|
+
reason: `verify threw: ${e?.message || e}`
|
|
21469
|
+
});
|
|
21470
|
+
if (decision.action === "retry") {
|
|
21471
|
+
writeGoalLoopState(directory, sessionId, { ...ls, active: true, phase: "continue", holds, started_at: startedAt, ledger, verify_errors: decision.attempt });
|
|
21472
|
+
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");
|
|
21473
|
+
} else {
|
|
21474
|
+
const parkPhase = terminalPhaseForDone(resolveLoopDurable({ durable: ls.durable, oracle: oracleCmd }));
|
|
21475
|
+
writeGoalLoopState(directory, sessionId, { ...ls, active: parkPhase === "dormant", phase: parkPhase, completed_at: Date.now(), ledger, verify_errors: 0 });
|
|
21476
|
+
checkSvampConfig?.();
|
|
21477
|
+
}
|
|
21229
21478
|
} catch {
|
|
21230
21479
|
}
|
|
21231
21480
|
} finally {
|
|
@@ -21789,7 +22038,7 @@ ${oracle.output.trim().slice(0, 500)}`, "\u{1F501} Continuing loop");
|
|
|
21789
22038
|
}
|
|
21790
22039
|
if (persistedSessions.length > 0) {
|
|
21791
22040
|
try {
|
|
21792
|
-
const { awaitClaudeVersionReady } = await import('./pinnedClaudeCode-
|
|
22041
|
+
const { awaitClaudeVersionReady } = await import('./pinnedClaudeCode-mM9Q6Q2G.mjs');
|
|
21793
22042
|
await awaitClaudeVersionReady();
|
|
21794
22043
|
} catch {
|
|
21795
22044
|
}
|
|
@@ -21822,12 +22071,26 @@ ${oracle.output.trim().slice(0, 500)}`, "\u{1F501} Continuing loop");
|
|
|
21822
22071
|
break;
|
|
21823
22072
|
}
|
|
21824
22073
|
}
|
|
22074
|
+
const _loopArmed = !isOrphaned && isLoopArmedForSession(persisted.directory, persisted.sessionId);
|
|
21825
22075
|
const _restoreAction = classifyRestoreContinuation({
|
|
21826
22076
|
wasProcessing: !!persisted.wasProcessing,
|
|
21827
22077
|
hasResumeId: !!persisted.claudeResumeId,
|
|
21828
22078
|
isOrphaned: !!isOrphaned,
|
|
21829
|
-
loopActive: isLoopActiveForSession(persisted.directory, persisted.sessionId)
|
|
22079
|
+
loopActive: isLoopActiveForSession(persisted.directory, persisted.sessionId),
|
|
22080
|
+
loopArmed: _loopArmed,
|
|
22081
|
+
armedHasPendingWork: _loopArmed ? safeBacklogPendingCount(persisted.directory, persisted.sessionId) > 0 : false
|
|
21830
22082
|
});
|
|
22083
|
+
if (_loopArmed) {
|
|
22084
|
+
logger.log(`[loop] Restored DORMANT loop for ${persisted.sessionId} \u2192 ${_restoreAction === "loop-resume" ? "re-arming (pending work found)" : "staying dormant/watching (backlog empty)"}`);
|
|
22085
|
+
if (_restoreAction === "loop-resume") {
|
|
22086
|
+
try {
|
|
22087
|
+
const lp = join(getLoopDir(persisted.directory, persisted.sessionId), "loop-state.json");
|
|
22088
|
+
const cur = readLoopState(persisted.directory, persisted.sessionId);
|
|
22089
|
+
if (cur) atomicWriteLoopState(lp, { ...cur, active: true, phase: "continue", completed_at: void 0, verify_errors: 0, resumed_at: Date.now() });
|
|
22090
|
+
} catch {
|
|
22091
|
+
}
|
|
22092
|
+
}
|
|
22093
|
+
}
|
|
21831
22094
|
if (_restoreAction === "auto-continue") {
|
|
21832
22095
|
sessionsToAutoContinue.push(persisted.sessionId);
|
|
21833
22096
|
} else if (_restoreAction === "loop-resume") {
|
|
@@ -21945,18 +22208,44 @@ ${oracle.output.trim().slice(0, 500)}`, "\u{1F501} Continuing loop");
|
|
|
21945
22208
|
logger.log(`Session ${sessionId} RPC handlers not found for loop resume`);
|
|
21946
22209
|
continue;
|
|
21947
22210
|
}
|
|
22211
|
+
const loopTurnFingerprint = () => {
|
|
22212
|
+
try {
|
|
22213
|
+
const s = readLoopState(sessDir, sessionId);
|
|
22214
|
+
return `${s?.phase || ""}|${s?.holds || 0}|${s?.ledger?.turns || 0}`;
|
|
22215
|
+
} catch {
|
|
22216
|
+
return "";
|
|
22217
|
+
}
|
|
22218
|
+
};
|
|
22219
|
+
const sendResume = async (tag) => {
|
|
22220
|
+
const prompt = loopResumeMessage(sessDir, sessionId);
|
|
22221
|
+
await rpc.sendMessage(
|
|
22222
|
+
JSON.stringify({
|
|
22223
|
+
role: "user",
|
|
22224
|
+
content: { type: "text", text: prompt },
|
|
22225
|
+
meta: { sentFrom: "svamp-daemon-loop-resume" }
|
|
22226
|
+
})
|
|
22227
|
+
);
|
|
22228
|
+
logger.log(`Resumed loop for session ${sessionId}${tag}`);
|
|
22229
|
+
};
|
|
21948
22230
|
setTimeout(async () => {
|
|
21949
22231
|
try {
|
|
21950
22232
|
if (!isLoopActiveForSession(sessDir, sessionId)) return;
|
|
21951
|
-
const
|
|
21952
|
-
await
|
|
21953
|
-
|
|
21954
|
-
|
|
21955
|
-
|
|
21956
|
-
|
|
21957
|
-
|
|
21958
|
-
|
|
21959
|
-
|
|
22233
|
+
const fpBefore = loopTurnFingerprint();
|
|
22234
|
+
await sendResume("");
|
|
22235
|
+
const graceMs = Math.max(5e3, Number(process.env.SVAMP_LOOP_RESUME_WATCHDOG_MS) || 45e3);
|
|
22236
|
+
setTimeout(() => {
|
|
22237
|
+
try {
|
|
22238
|
+
if (!isLoopActiveForSession(sessDir, sessionId)) return;
|
|
22239
|
+
const live = Array.from(pidToTrackedSession.values()).find((s) => s.svampSessionId === sessionId && !s.stopped);
|
|
22240
|
+
const lifecycle = live?.hyphaService?.getMetadata()?.lifecycleState;
|
|
22241
|
+
if (lifecycle && lifecycle !== "idle") return;
|
|
22242
|
+
if (loopTurnFingerprint() !== fpBefore) return;
|
|
22243
|
+
logger.log(`[loop] resume watchdog: session ${sessionId} loop still active but never advanced after ${graceMs}ms \u2014 re-issuing /goal once`);
|
|
22244
|
+
void sendResume(" (watchdog re-issue)").catch(() => {
|
|
22245
|
+
});
|
|
22246
|
+
} catch {
|
|
22247
|
+
}
|
|
22248
|
+
}, graceMs);
|
|
21960
22249
|
} catch (err) {
|
|
21961
22250
|
logger.log(`Failed to resume loop for session ${sessionId}: ${err.message}`);
|
|
21962
22251
|
}
|
|
@@ -21994,7 +22283,7 @@ ${oracle.output.trim().slice(0, 500)}`, "\u{1F501} Continuing loop");
|
|
|
21994
22283
|
const PING_TIMEOUT_MS = 15e3;
|
|
21995
22284
|
const POST_RECONNECT_GRACE_MS = 2e4;
|
|
21996
22285
|
const RECONNECT_JITTER_MS = 2500;
|
|
21997
|
-
const { WorkflowScheduler } = await import('./scheduler-
|
|
22286
|
+
const { WorkflowScheduler } = await import('./scheduler-C0WXBdL_.mjs');
|
|
21998
22287
|
const workflowProjectRoots = () => {
|
|
21999
22288
|
const dirs = /* @__PURE__ */ new Set();
|
|
22000
22289
|
for (const s of pidToTrackedSession.values()) {
|