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.
Files changed (28) hide show
  1. package/dist/{adminCommands-U5D7i8tD.mjs → adminCommands-CRQQhXsw.mjs} +1 -1
  2. package/dist/{agentCommands-BefFTl6g.mjs → agentCommands-hLTce38n.mjs} +5 -5
  3. package/dist/{auth-DgGAjihq.mjs → auth-B2ZuhK_S.mjs} +1 -1
  4. package/dist/{cli-B0TyKXyU.mjs → cli-HI-J8Y7U.mjs} +70 -70
  5. package/dist/cli.mjs +2 -2
  6. package/dist/{commands-CnqL60f7.mjs → commands-12pIcOWK.mjs} +1 -1
  7. package/dist/{commands-BoGUxb2o.mjs → commands-C2zQmf3Y.mjs} +1 -1
  8. package/dist/{commands-XQ8-fOSu.mjs → commands-CmMh6R6S.mjs} +1 -1
  9. package/dist/{commands-xoalqGUY.mjs → commands-D6BqvSLe.mjs} +10 -5
  10. package/dist/{commands-imLC7CK2.mjs → commands-DGUgZoou.mjs} +2 -2
  11. package/dist/{commands-BCQbBzYU.mjs → commands-DpfYt_SX.mjs} +2 -2
  12. package/dist/{commands-CEOhve0Y.mjs → commands-IPkpFWbi.mjs} +2 -2
  13. package/dist/{commands-CjVl4amM.mjs → commands-ND3vwGkQ.mjs} +8 -8
  14. package/dist/{fleet-BS5eS0Wz.mjs → fleet-Cuv2uu1t.mjs} +2 -2
  15. package/dist/{frpc-DvBVvsZC.mjs → frpc-DbZDLphz.mjs} +1 -1
  16. package/dist/{headlessCli-C9wTLm1x.mjs → headlessCli-DakQSPzS.mjs} +2 -2
  17. package/dist/index.mjs +1 -1
  18. package/dist/{notifyCommands-0IltLO-S.mjs → notifyCommands-DhRbelQA.mjs} +1 -1
  19. package/dist/{package-CFhmZcVJ.mjs → package-BKX7lisO.mjs} +1 -1
  20. package/dist/{pinnedClaudeCode-zDzkOTi2.mjs → pinnedClaudeCode-mM9Q6Q2G.mjs} +1 -1
  21. package/dist/{rpc-BXpuORiQ.mjs → rpc-C7uDtxFw.mjs} +1 -1
  22. package/dist/{rpc-BLWr4p8g.mjs → rpc-Q4PWHeNn.mjs} +1 -1
  23. package/dist/{run-DtVX2pQN.mjs → run-Ccf8_4AF.mjs} +330 -75
  24. package/dist/{run-CMoQtnIj.mjs → run-DG0yDh6U.mjs} +1 -1
  25. package/dist/{scheduler-7aLvf5mn.mjs → scheduler-B1xThM3A.mjs} +1 -1
  26. package/dist/{serveCommands-D7BieIJ5.mjs → serveCommands-DDq9qzoN.mjs} +5 -5
  27. package/dist/{sideband-DjjXlVhw.mjs → sideband-CDdpZAXO.mjs} +1 -1
  28. 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
- max_iterations: opts2?.maxIterations ?? 20,
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-DvBVvsZC.mjs');
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-DvBVvsZC.mjs');
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-DjjXlVhw.mjs');
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_" + Math.random().toString(16).slice(2, 12);
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-BoGUxb2o.mjs');
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_" + Math.random().toString(16).slice(2, 10), outcome: "uploaded" });
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 = 10 * 6e4;
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({ id, stdout: "", stderr: `command timed out after ${Math.round(this.execTimeoutMs / 1e3)}s`, exitCode: -1, synthetic: true, ts: this.now() });
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
- stdout: result.stdout ?? "",
7844
- stderr: result.stderr ?? "",
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
- messageId: "op_" + Math.random().toString(16).slice(2, 10),
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 === "ready" || i.status === "in_progress").map((i) => ({ id: i.id }));
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 failing OPEN (marking done): ${e?.message || e}`);
15909
- return { action: "done" };
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 failing OPEN (marking done)`);
15914
- return { action: "done" };
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 !== "done" && s.phase !== "gave_up" && s.phase !== "cancelled";
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 === "done" || s.phase === "gave_up" || s.phase === "cancelled") return null;
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 === "done" || s.phase === "gave_up" || s.phase === "cancelled") return false;
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
- writeFileSync(p, JSON.stringify(cfg, null, 2));
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
- try {
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-BefFTl6g.mjs');
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-DvBVvsZC.mjs');
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-zDzkOTi2.mjs');
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
- ], { cwd, timeout: 6e4, stdio: ["ignore", "pipe", "pipe"], env: forkEnv });
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", () => resolve2(null));
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
- writeGoalLoopState(directory, sessionId, { ...ls, active: false, phase: "done", completed_at: Date.now(), holds, ledger, progress_history, auto_resumes: ls.auto_resumes, extensions });
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 error \u2014 failing open (done): ${e?.message || e}`);
18846
+ logger.log(`[Session ${sessionId}] verifyGoalCompletion threw \u2014 failing closed (retry/park): ${e?.message || e}`);
18696
18847
  try {
18697
- writeGoalLoopState(directory, sessionId, { ...ls, active: false, phase: "done", completed_at: Date.now() });
18698
- checkSvampConfig?.();
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
- writeGoalLoopState(directory, sessionId, { ...ls, active: false, phase: "done", completed_at: now, ledger, progress_history: prog.progress_history, auto_resumes: prog.auto_resumes });
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
- errorOutputLines.push(line.slice(0, 500));
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
- errorOutputLines.push(line.trim().slice(0, 500));
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 (settled) continue;
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 (settled) return;
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 (settled) return;
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-BLWr4p8g.mjs');
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-BXpuORiQ.mjs');
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-BLWr4p8g.mjs');
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-BXpuORiQ.mjs');
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
- writeGoalLoopState(directory, sessionId, { ...ls, active: false, phase: "done", completed_at: now, ledger });
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
- writeGoalLoopState(directory, sessionId, { ...ls, active: false, phase: "done", completed_at: Date.now(), holds, ledger });
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 open (done): ${e?.message || e}`);
21429
+ logger.log(`[Session ${sessionId}] verifyAcpLoopCompletion error \u2014 failing closed (retry/park): ${e?.message || e}`);
21226
21430
  try {
21227
- writeGoalLoopState(directory, sessionId, { ...ls, active: false, phase: "done", completed_at: Date.now(), ledger });
21228
- checkSvampConfig?.();
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-zDzkOTi2.mjs');
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 prompt = loopResumeMessage(sessDir, sessionId);
21952
- await rpc.sendMessage(
21953
- JSON.stringify({
21954
- role: "user",
21955
- content: { type: "text", text: prompt },
21956
- meta: { sentFrom: "svamp-daemon-loop-resume" }
21957
- })
21958
- );
21959
- logger.log(`Resumed loop for session ${sessionId}`);
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-7aLvf5mn.mjs');
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()) {