svamp-cli 0.2.230 → 0.2.231

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 (24) hide show
  1. package/dist/{agentCommands-C__ZDnGy.mjs → agentCommands-V9V2kh0Q.mjs} +5 -5
  2. package/dist/{auth-B0FVB29Q.mjs → auth-DkjnoUxX.mjs} +1 -1
  3. package/dist/cli.mjs +61 -61
  4. package/dist/{commands-DZsbsqkr.mjs → commands-BGRljSMD.mjs} +1 -1
  5. package/dist/{commands-B-kINrK9.mjs → commands-BtaFWs9X.mjs} +2 -2
  6. package/dist/{commands-CSfdrg2h.mjs → commands-Bvdq1b3h.mjs} +10 -7
  7. package/dist/{commands-CvKU6_YX.mjs → commands-CQP12ojC.mjs} +1 -1
  8. package/dist/{commands-o-RWBC8N.mjs → commands-CdpWrKls.mjs} +2 -2
  9. package/dist/{commands-BNsxJYMS.mjs → commands-DBHDbRVm.mjs} +1 -1
  10. package/dist/{commands-C-9UCf3L.mjs → commands-qu4qKm64.mjs} +1 -1
  11. package/dist/{fleet-HW2op7Lu.mjs → fleet-BPNSihXR.mjs} +1 -1
  12. package/dist/{frpc-o7YfuAtf.mjs → frpc-CUBlxR7B.mjs} +1 -1
  13. package/dist/{headlessCli-C0ySAt1z.mjs → headlessCli-DxhjiAyU.mjs} +2 -2
  14. package/dist/index.mjs +1 -1
  15. package/dist/{package-B56skiR7.mjs → package-omNKWcir.mjs} +1 -1
  16. package/dist/{rpc-D_Suxu2Q.mjs → rpc-CPHva9-3.mjs} +1 -1
  17. package/dist/{rpc-RpVqDF8O.mjs → rpc-Cm2Uy3Hf.mjs} +1 -1
  18. package/dist/{run-DVqhiYP9.mjs → run-BzobWMYv.mjs} +335 -122
  19. package/dist/{run-D3SEkDfQ.mjs → run-DS_XkUa2.mjs} +1 -1
  20. package/dist/{scheduler-B2ZHfyig.mjs → scheduler-CwyAdWQa.mjs} +1 -1
  21. package/dist/{serveCommands-DZ3BWJTu.mjs → serveCommands-YMWshAkF.mjs} +5 -5
  22. package/dist/{serveManager-J2VwaLs1.mjs → serveManager-CkfMDcNQ.mjs} +2 -2
  23. package/dist/{sideband-BDwb3wnc.mjs → sideband-DDCUdyBK.mjs} +1 -1
  24. package/package.json +1 -1
@@ -1967,6 +1967,33 @@ function savePersistedMachineMetadata(svampHomeDir, data) {
1967
1967
  console.error("[HYPHA MACHINE] Failed to persist machine metadata:", err);
1968
1968
  }
1969
1969
  }
1970
+ function buildSessionTunnels(handlers, sessionId) {
1971
+ const specs = handlers.listExposedTunnels?.() ?? [];
1972
+ const tunnels = handlers.tunnels;
1973
+ const stateOf = (s) => {
1974
+ if (!s) return "reconnecting";
1975
+ if (s.probe && !s.probe.ok) return "failed";
1976
+ if (s.connected) return "connected";
1977
+ return s.restartAttempts > 0 ? "reconnecting" : "failed";
1978
+ };
1979
+ const rows = [];
1980
+ for (const spec of specs) {
1981
+ if (spec.sessionId !== sessionId) continue;
1982
+ const tunnel = tunnels?.get(spec.name);
1983
+ const status = tunnel ? tunnel.status : null;
1984
+ const urls = {};
1985
+ if (tunnel) for (const [port, url] of tunnel.getUrls()) urls[port] = url;
1986
+ rows.push({
1987
+ name: spec.name,
1988
+ ports: spec.ports,
1989
+ group: spec.group,
1990
+ live: !!tunnel,
1991
+ state: stateOf(status),
1992
+ urls: Object.keys(urls).length ? urls : void 0
1993
+ });
1994
+ }
1995
+ return rows;
1996
+ }
1970
1997
  async function registerMachineService(server, machineId, metadata, daemonState, handlers) {
1971
1998
  let currentMetadata = { ...metadata };
1972
1999
  let currentDaemonState = { ...daemonState };
@@ -2773,30 +2800,27 @@ async function registerMachineService(server, machineId, metadata, daemonState,
2773
2800
  // user can manage their own session's tasks without machine-level
2774
2801
  // access.
2775
2802
  /**
2776
- * Snapshot of everything the session's Tasks panel needs to render:
2803
+ * Snapshot of everything the session's Tasks panel needs to render.
2804
+ * Only svamp-MANAGED processes for this session are surfaced (#0192):
2777
2805
  * - supervised: ProcessSpec/state filtered to this session
2778
- * - detected: descendants of the agent PID that aren't supervised
2806
+ * - tunnels: `svamp service expose` tunnels owned by this session
2807
+ * Unsupervised ps-tree descendants of the agent are no longer shown —
2808
+ * `detected` is kept as an empty array for wire back-compat with older
2809
+ * app clients that read `snap.detected`.
2779
2810
  */
2780
2811
  sessionProcessList: async (params, context) => {
2781
2812
  await authorizeSessionAccess(params.sessionId, "view", context);
2782
2813
  const supervisor = handlers.supervisor;
2783
2814
  const all = supervisor ? supervisor.list() : [];
2784
2815
  const supervised = all.filter((p) => p.spec.sessionId === params.sessionId);
2816
+ const tunnels = buildSessionTunnels(handlers, params.sessionId);
2785
2817
  const agentPid = handlers.getSessionPid?.(params.sessionId);
2786
- let detected = [];
2787
- if (agentPid) {
2788
- const supervisedPids = /* @__PURE__ */ new Set();
2789
- for (const info of all) {
2790
- if (info.state.pid) supervisedPids.add(info.state.pid);
2791
- }
2792
- detected = await detectDescendants(agentPid, supervisedPids);
2793
- }
2794
- return { supervised, detected, agentPid };
2818
+ return { supervised, tunnels, detected: [], agentPid };
2795
2819
  },
2796
2820
  /**
2797
- * Cheap counts for the input-bar badge avoids the cost of a full
2798
- * detection walk when the panel is closed. Counts are still
2799
- * accurate enough to indicate "is anything running for this session".
2821
+ * Cheap counts for the input-bar badge. Counts only svamp-managed
2822
+ * processes for this session (supervised specs + exposed tunnels);
2823
+ * `detected` stays 0 for wire back-compat.
2800
2824
  */
2801
2825
  sessionProcessCounts: async (params, context) => {
2802
2826
  await authorizeSessionAccess(params.sessionId, "view", context);
@@ -2804,15 +2828,9 @@ async function registerMachineService(server, machineId, metadata, daemonState,
2804
2828
  const all = supervisor ? supervisor.list() : [];
2805
2829
  const supervised = all.filter((p) => p.spec.sessionId === params.sessionId);
2806
2830
  const supervisedRunning = supervised.filter((p) => p.state.status === "running" || p.state.status === "starting").length;
2831
+ const tunnels = buildSessionTunnels(handlers, params.sessionId).length;
2807
2832
  const agentPid = handlers.getSessionPid?.(params.sessionId);
2808
- let detected = 0;
2809
- if (agentPid) {
2810
- const supervisedPids = /* @__PURE__ */ new Set();
2811
- for (const info of all) if (info.state.pid) supervisedPids.add(info.state.pid);
2812
- const list = await detectDescendants(agentPid, supervisedPids);
2813
- detected = list.length;
2814
- }
2815
- return { supervised: supervised.length, supervisedRunning, detected, agentPid };
2833
+ return { supervised: supervised.length, supervisedRunning, tunnels, detected: 0, agentPid };
2816
2834
  },
2817
2835
  /** Logs for a supervised process, gated by session ownership. */
2818
2836
  sessionProcessLogs: async (params, context) => {
@@ -2916,7 +2934,7 @@ async function registerMachineService(server, machineId, metadata, daemonState,
2916
2934
  const tunnels = handlers.tunnels;
2917
2935
  if (!tunnels) throw new Error("Tunnel management not available");
2918
2936
  if (tunnels.has(params.name)) throw new Error(`Tunnel '${params.name}' already running`);
2919
- const { FrpcTunnel } = await import('./frpc-o7YfuAtf.mjs');
2937
+ const { FrpcTunnel } = await import('./frpc-CUBlxR7B.mjs');
2920
2938
  const tunnel = new FrpcTunnel({
2921
2939
  name: params.name,
2922
2940
  ports: params.ports,
@@ -2943,6 +2961,7 @@ async function registerMachineService(server, machineId, metadata, daemonState,
2943
2961
  healthCheckType: params.healthCheckType,
2944
2962
  healthCheckPath: params.healthCheckPath,
2945
2963
  healthCheckInterval: params.healthCheckInterval,
2964
+ sessionId: params.sessionId,
2946
2965
  addedAt: Date.now()
2947
2966
  });
2948
2967
  const urls = {};
@@ -3363,7 +3382,7 @@ QUESTION: ${params.question || "Summarize this concisely."}` }
3363
3382
  }
3364
3383
  const deps = buildSessionDeps(rpc, { cwd, ownerEmail: owner });
3365
3384
  const sender = { name: context?.user?.email || context?.user?.id || "user", kind: "user", verified: true };
3366
- const { toolsForRole } = await import('./sideband-BDwb3wnc.mjs');
3385
+ const { toolsForRole } = await import('./sideband-DDCUdyBK.mjs');
3367
3386
  const r2 = await runWiseAgent({ message: params.message, sender, config: { tools: toolsForRole(role2) }, deps, transport, model: resolved.model });
3368
3387
  return fmt(r2);
3369
3388
  }
@@ -3462,7 +3481,7 @@ QUESTION: ${params.question || "Summarize this concisely."}` }
3462
3481
  if (r.error || !r.sender) return { error: r.error || "unauthorized" };
3463
3482
  const callId = "call_" + Math.random().toString(16).slice(2, 12);
3464
3483
  const rendered = renderMessage(c, { sender: r.sender, body: { message: kwargs.message }, callId });
3465
- const { queryCore } = await import('./commands-C-9UCf3L.mjs');
3484
+ const { queryCore } = await import('./commands-qu4qKm64.mjs');
3466
3485
  const timeout = c.reply?.timeout_sec || 120;
3467
3486
  let result;
3468
3487
  try {
@@ -10719,6 +10738,7 @@ ${o.task.trim()}`);
10719
10738
  ${o.until.trim()}`);
10720
10739
  lines.push("");
10721
10740
  lines.push("Use your tools (Read, Bash, Grep, Glob) to inspect the ACTUAL repository state \u2014 do NOT trust the transcript. Check the working tree, git log/diff, build/test output, and whether changes were actually built/published/deployed where the request implied it.");
10741
+ lines.push('IMPORTANT \u2014 the loop oracle only counts OPEN (ready/in_progress) issues, so an issue removed from the queue without genuine resolution is invisible to it. Run `svamp issue list --all` (or read `.svamp/issues/`) and confirm NO issue tied to this goal was sidelined merely to make the queue look empty. A PAUSED issue is FINE if it is genuinely blocked on a user answer OR clearly delegated/handed to another session (crew child or a named peer) \u2014 do NOT penalize those. Reject "done" ONLY if goal-relevant work is neither genuinely complete, nor legitimately blocked on the user, nor handed to another session.');
10722
10742
  if (o.oracle) {
10723
10743
  lines.push("");
10724
10744
  lines.push(`The loop oracle is \`${o.oracle.trim()}\` and it currently PASSES${o.oracleOutput ? ` (output: ${o.oracleOutput.trim().slice(0, 400)})` : ""}. A green oracle here only means "no OPEN issues" \u2014 it does NOT mean each CLOSED issue actually works.`);
@@ -10753,6 +10773,54 @@ function buildGoalCondition(o) {
10753
10773
  }
10754
10774
  return parts.join(" ");
10755
10775
  }
10776
+ const HOUR_MS = 36e5;
10777
+ function sumUsageTokens(usage) {
10778
+ if (!usage || typeof usage !== "object") return 0;
10779
+ const n = (v) => typeof v === "number" && Number.isFinite(v) && v > 0 ? v : 0;
10780
+ return n(usage.input_tokens) + n(usage.output_tokens) + n(usage.cache_creation_input_tokens);
10781
+ }
10782
+ function accumulateLedger(prev, sample) {
10783
+ const base = prev && typeof prev === "object" ? { turns: prev.turns || 0, tokens: prev.tokens || 0, window: Array.isArray(prev.window) ? prev.window : [] } : { turns: 0, tokens: 0, window: [] };
10784
+ const addTurns = sample.turns > 0 ? sample.turns : 0;
10785
+ const addTokens = sample.tokens > 0 ? sample.tokens : 0;
10786
+ const window = [...base.window, [sample.ts, addTokens]].filter(([ts]) => sample.ts - ts < HOUR_MS);
10787
+ return { turns: base.turns + addTurns, tokens: base.tokens + addTokens, window };
10788
+ }
10789
+ function tokensInLastHour(ledger, now) {
10790
+ if (!ledger || !Array.isArray(ledger.window)) return 0;
10791
+ return ledger.window.filter(([ts]) => now - ts < HOUR_MS).reduce((s, [, t]) => s + (t > 0 ? t : 0), 0);
10792
+ }
10793
+ function checkLoopBudget(ledger, limits, now, startedAt) {
10794
+ const L = limits || {};
10795
+ const led = ledger || { turns: 0, tokens: 0, window: [] };
10796
+ const pos = (v) => typeof v === "number" && Number.isFinite(v) && v > 0;
10797
+ if (pos(L.max_iterations) && led.turns >= L.max_iterations) {
10798
+ return { exceeded: true, reason: `iteration budget reached (${led.turns}/${L.max_iterations} turns)` };
10799
+ }
10800
+ if (pos(L.max_runtime_sec) && pos(startedAt)) {
10801
+ const elapsed = (now - startedAt) / 1e3;
10802
+ if (elapsed >= L.max_runtime_sec) {
10803
+ return { exceeded: true, reason: `runtime budget reached (${Math.round(elapsed)}s/${L.max_runtime_sec}s)` };
10804
+ }
10805
+ }
10806
+ if (pos(L.max_tokens) && led.tokens >= L.max_tokens) {
10807
+ return { exceeded: true, reason: `token budget reached (${led.tokens}/${L.max_tokens} tokens)` };
10808
+ }
10809
+ if (pos(L.max_tokens_per_hour)) {
10810
+ const rate = tokensInLastHour(led, now);
10811
+ if (rate >= L.max_tokens_per_hour) {
10812
+ return { exceeded: true, reason: `token-rate budget reached (${rate}/${L.max_tokens_per_hour} tokens in the last hour)` };
10813
+ }
10814
+ }
10815
+ return { exceeded: false };
10816
+ }
10817
+ const DEFAULT_LOOP_EVALUATOR_MODEL = "claude-haiku-4-5-20251001";
10818
+ function resolveEvaluatorModel(envValue) {
10819
+ if (envValue === void 0) return DEFAULT_LOOP_EVALUATOR_MODEL;
10820
+ const v = envValue.trim();
10821
+ if (!v || v.toLowerCase() === "inherit" || v.toLowerCase() === "default") return void 0;
10822
+ return v;
10823
+ }
10756
10824
  function parseEvaluatorVerdict(text) {
10757
10825
  if (!text) return null;
10758
10826
  const candidates = [];
@@ -11636,6 +11704,7 @@ function writeSvampConfig(configPath, config) {
11636
11704
  renameSync$1(tmpPath, configPath);
11637
11705
  return content;
11638
11706
  }
11707
+ const DEFAULT_LOOP_MAX_ITERATIONS = 1e3;
11639
11708
  function getLoopDir(directory, sessionId) {
11640
11709
  if (sessionId) {
11641
11710
  const scoped = join$1(directory, ".svamp", sessionId, "loop");
@@ -11739,6 +11808,91 @@ function stripOldLoopHooks(directory) {
11739
11808
  return false;
11740
11809
  }
11741
11810
  }
11811
+ function spawnHeadlessEvaluator(prompt, opts) {
11812
+ return new Promise((resolve2, reject) => {
11813
+ const evalArgs = [
11814
+ "--print",
11815
+ prompt,
11816
+ // Read-only reviewer (mirrors .claude/agents/loop-evaluator.md): it inspects real repo
11817
+ // state but must not mutate the working tree it's judging. The allowedTools whitelist
11818
+ // already excludes mutation tools; --disallowedTools is belt-and-suspenders. Kept to
11819
+ // Write/Edit only — MultiEdit/NotebookEdit aren't known tool names on some claude builds
11820
+ // and emit a noisy "matches no known tool" warning (#0191 canary).
11821
+ "--allowedTools",
11822
+ "Read",
11823
+ "Bash",
11824
+ "Grep",
11825
+ "Glob",
11826
+ "--disallowedTools",
11827
+ "Write",
11828
+ "Edit",
11829
+ "--permission-mode",
11830
+ "bypassPermissions",
11831
+ "--output-format",
11832
+ "stream-json",
11833
+ "--verbose",
11834
+ ...opts.model ? ["--model", opts.model] : []
11835
+ ];
11836
+ const evalEnv = { ...process.env };
11837
+ applyClaudeProxyEnv(evalEnv);
11838
+ delete evalEnv.CLAUDECODE;
11839
+ let proc;
11840
+ try {
11841
+ proc = spawn$1("claude", evalArgs, { cwd: opts.cwd, env: evalEnv, stdio: ["pipe", "pipe", "pipe"] });
11842
+ } catch (e) {
11843
+ reject(e);
11844
+ return;
11845
+ }
11846
+ let out = "", buf = "", stderr = "", settled = false;
11847
+ const timer = setTimeout(() => {
11848
+ if (settled) return;
11849
+ settled = true;
11850
+ try {
11851
+ proc.kill("SIGTERM");
11852
+ } catch {
11853
+ }
11854
+ reject(new Error("evaluator timeout"));
11855
+ }, opts.timeoutMs && opts.timeoutMs > 0 ? opts.timeoutMs : 3e5);
11856
+ proc.stderr?.on("data", (c) => {
11857
+ stderr += c.toString();
11858
+ });
11859
+ proc.stdout.on("data", (chunk) => {
11860
+ buf += chunk.toString();
11861
+ let nl;
11862
+ while ((nl = buf.indexOf("\n")) !== -1) {
11863
+ const line = buf.slice(0, nl);
11864
+ buf = buf.slice(nl + 1);
11865
+ if (!line.trim()) continue;
11866
+ try {
11867
+ const m = JSON.parse(line);
11868
+ if (m.type === "assistant" && m.message?.content) {
11869
+ for (const b of m.message.content) if (b.type === "text" && b.text) out += b.text;
11870
+ } else if (m.type === "result") {
11871
+ if (settled) continue;
11872
+ settled = true;
11873
+ clearTimeout(timer);
11874
+ if (m.is_error) reject(new Error(String(m.result || "evaluator error")));
11875
+ else resolve2(out || String(m.result || ""));
11876
+ }
11877
+ } catch {
11878
+ }
11879
+ }
11880
+ });
11881
+ proc.on("exit", (code) => {
11882
+ if (settled) return;
11883
+ settled = true;
11884
+ clearTimeout(timer);
11885
+ if (code === 0) resolve2(out);
11886
+ else reject(new Error(stderr.trim() || `evaluator exited with code ${code}`));
11887
+ });
11888
+ proc.on("error", (e) => {
11889
+ if (settled) return;
11890
+ settled = true;
11891
+ clearTimeout(timer);
11892
+ reject(e);
11893
+ });
11894
+ });
11895
+ }
11742
11896
  function deactivateLoop(directory, sessionId) {
11743
11897
  try {
11744
11898
  const p = join$1(getLoopDir(directory, sessionId), "loop-state.json");
@@ -11939,6 +12093,7 @@ function createSvampConfigChecker(directory, sessionId, getMetadata, setMetadata
11939
12093
  } catch {
11940
12094
  }
11941
12095
  const wasStopped = existing.active === false || existing.phase === "done" || existing.phase === "gave_up" || existing.phase === "cancelled";
12096
+ const bumpedBudget = parseLoopBudget(cfg.budget);
11942
12097
  if (wasStopped) {
11943
12098
  try {
11944
12099
  const sp = join$1(getLoopDir(directory, sessionId), "loop-state.json");
@@ -11947,6 +12102,8 @@ function createSvampConfigChecker(directory, sessionId, getMetadata, setMetadata
11947
12102
  active: true,
11948
12103
  phase: "continue",
11949
12104
  iteration: 0,
12105
+ ...newMax != null ? { max_iterations: newMax } : {},
12106
+ ...bumpedBudget ? { budget: { ...existing.budget || {}, ...bumpedBudget } } : {},
11950
12107
  completed_at: void 0,
11951
12108
  gave_up_reason: void 0,
11952
12109
  stall_hinted: false,
@@ -11958,18 +12115,33 @@ function createSvampConfigChecker(directory, sessionId, getMetadata, setMetadata
11958
12115
  setMetadata((m) => ({ ...m, messageQueue: [...eq, { id: randomUUID$1(), text: loopResumeMessage(directory, sessionId), displayText: "\u{1F501} Resuming loop", createdAt: Date.now() }] }));
11959
12116
  onLoopActivated?.();
11960
12117
  }
12118
+ if (!wasStopped && (newMax != null || bumpedBudget)) {
12119
+ try {
12120
+ const cur = readLoopState(directory, sessionId);
12121
+ if (cur) writeGoalLoopState(directory, sessionId, {
12122
+ ...cur,
12123
+ ...newMax != null ? { max_iterations: newMax } : {},
12124
+ ...bumpedBudget ? { budget: { ...cur.budget || {}, ...bumpedBudget } } : {}
12125
+ });
12126
+ } catch {
12127
+ }
12128
+ }
11961
12129
  sessionService.pushMessage({ type: "message", message: `\u{1F501} Loop limit updated${newMax != null ? ` \u2192 max ${newMax} iterations` : ""}${wasStopped ? " \u2014 resuming" : ""}.` }, "event");
11962
12130
  logger.log(`[svampConfig] Loop limit modified (max=${newMax}, resumed=${wasStopped}, restarted=${wasStopped})`);
11963
12131
  }
11964
12132
  } else if (cfg && typeof cfg === "object" && (task || until)) {
11965
12133
  const oracle = typeof cfg.oracle === "string" && cfg.oracle.trim() ? cfg.oracle.trim() : void 0;
11966
- const maxIterations = typeof cfg.max_iterations === "number" ? cfg.max_iterations : typeof cfg.max_rounds === "number" ? cfg.max_rounds : 20;
11967
- const condition = buildGoalCondition({ task, until, oracle, maxIterations });
12134
+ const explicitMax = typeof cfg.max_iterations === "number" ? cfg.max_iterations : typeof cfg.max_rounds === "number" ? cfg.max_rounds : void 0;
12135
+ const hasExplicitMax = typeof explicitMax === "number" && explicitMax > 0;
12136
+ const hardMax = hasExplicitMax ? explicitMax : DEFAULT_LOOP_MAX_ITERATIONS;
12137
+ const softTurnCap = hasExplicitMax ? explicitMax : 20;
12138
+ const condition = buildGoalCondition({ task, until, oracle, maxIterations: softTurnCap });
12139
+ const budget = parseLoopBudget(cfg.budget);
11968
12140
  try {
11969
12141
  stripOldLoopHooks(directory);
11970
12142
  } catch {
11971
12143
  }
11972
- writeGoalLoopState(directory, sessionId, { active: true, phase: "running", iteration: 0, task: condition, goal_task: task, until, oracle, max_iterations: maxIterations, started_at: Date.now(), holds: 0 });
12144
+ 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: [] } });
11973
12145
  const existingQueue = getMetadata().messageQueue || [];
11974
12146
  setMetadata((m) => ({ ...m, messageQueue: [...existingQueue, { id: randomUUID$1(), text: `/goal ${condition}`, displayText: `\u{1F3AF} Goal: ${(task || until || "").slice(0, 100)}`, createdAt: Date.now() }] }));
11975
12147
  onLoopActivated?.();
@@ -12418,7 +12590,7 @@ async function startDaemon(options) {
12418
12590
  saveExposedTunnels(list);
12419
12591
  }
12420
12592
  async function createExposedTunnel(spec) {
12421
- const { FrpcTunnel } = await import('./frpc-o7YfuAtf.mjs');
12593
+ const { FrpcTunnel } = await import('./frpc-CUBlxR7B.mjs');
12422
12594
  const tunnel = new FrpcTunnel({
12423
12595
  name: spec.name,
12424
12596
  ports: spec.ports,
@@ -12438,7 +12610,7 @@ async function startDaemon(options) {
12438
12610
  return tunnel;
12439
12611
  }
12440
12612
  const tunnelRecreateState = /* @__PURE__ */ new Map();
12441
- const { ServeManager } = await import('./serveManager-J2VwaLs1.mjs');
12613
+ const { ServeManager } = await import('./serveManager-CkfMDcNQ.mjs');
12442
12614
  const serveManager = new ServeManager(SVAMP_HOME, (msg) => logger.log(`[SERVE] ${msg}`), hyphaServerUrl);
12443
12615
  ensureAutoInstalledSkills(logger).catch(() => {
12444
12616
  });
@@ -13069,87 +13241,7 @@ ${parts.join("\n")}`);
13069
13241
  };
13070
13242
  sessionService.updateMetadata(sessionMetadata);
13071
13243
  };
13072
- const runHeadlessEvaluator = (prompt) => new Promise((resolve2, reject) => {
13073
- const evalArgs = [
13074
- "--print",
13075
- prompt,
13076
- // Read-only reviewer (mirrors .claude/agents/loop-evaluator.md): it inspects
13077
- // real repo state but must not mutate the working tree it's judging.
13078
- "--allowedTools",
13079
- "Read",
13080
- "Bash",
13081
- "Grep",
13082
- "Glob",
13083
- "--disallowedTools",
13084
- "Write",
13085
- "Edit",
13086
- "MultiEdit",
13087
- "NotebookEdit",
13088
- "--permission-mode",
13089
- "bypassPermissions",
13090
- "--output-format",
13091
- "stream-json",
13092
- "--verbose"
13093
- ];
13094
- const evalEnv = { ...process.env };
13095
- applyClaudeProxyEnv(evalEnv);
13096
- delete evalEnv.CLAUDECODE;
13097
- let proc;
13098
- try {
13099
- proc = spawn$1("claude", evalArgs, { cwd: directory, env: evalEnv, stdio: ["pipe", "pipe", "pipe"] });
13100
- } catch (e) {
13101
- reject(e);
13102
- return;
13103
- }
13104
- let out = "", buf = "", stderr = "", settled = false;
13105
- const timer = setTimeout(() => {
13106
- if (settled) return;
13107
- settled = true;
13108
- try {
13109
- proc.kill("SIGTERM");
13110
- } catch {
13111
- }
13112
- reject(new Error("evaluator timeout"));
13113
- }, 3e5);
13114
- proc.stderr?.on("data", (c) => {
13115
- stderr += c.toString();
13116
- });
13117
- proc.stdout.on("data", (chunk) => {
13118
- buf += chunk.toString();
13119
- let nl;
13120
- while ((nl = buf.indexOf("\n")) !== -1) {
13121
- const line = buf.slice(0, nl);
13122
- buf = buf.slice(nl + 1);
13123
- if (!line.trim()) continue;
13124
- try {
13125
- const m = JSON.parse(line);
13126
- if (m.type === "assistant" && m.message?.content) {
13127
- for (const b of m.message.content) if (b.type === "text" && b.text) out += b.text;
13128
- } else if (m.type === "result") {
13129
- if (settled) continue;
13130
- settled = true;
13131
- clearTimeout(timer);
13132
- if (m.is_error) reject(new Error(String(m.result || "evaluator error")));
13133
- else resolve2(out || String(m.result || ""));
13134
- }
13135
- } catch {
13136
- }
13137
- }
13138
- });
13139
- proc.on("exit", (code) => {
13140
- if (settled) return;
13141
- settled = true;
13142
- clearTimeout(timer);
13143
- if (code === 0) resolve2(out);
13144
- else reject(new Error(stderr.trim() || `evaluator exited with code ${code}`));
13145
- });
13146
- proc.on("error", (e) => {
13147
- if (settled) return;
13148
- settled = true;
13149
- clearTimeout(timer);
13150
- reject(e);
13151
- });
13152
- });
13244
+ const runHeadlessEvaluator = (prompt) => spawnHeadlessEvaluator(prompt, { cwd: directory, model: resolveEvaluatorModel(process.env.SVAMP_LOOP_EVALUATOR_MODEL) });
13153
13245
  let goalVerifyInFlight = false;
13154
13246
  const verifyGoalCompletion = async (ls) => {
13155
13247
  if (goalVerifyInFlight) return;
@@ -13180,6 +13272,8 @@ ${parts.join("\n")}`);
13180
13272
  log: (m) => logger.log(`[Session ${sessionId}] ${m}`)
13181
13273
  }
13182
13274
  );
13275
+ const ledger = ls.ledger;
13276
+ const budget = ls.budget;
13183
13277
  if (result.action === "rekick") {
13184
13278
  const cond = ls.task || "";
13185
13279
  writeGoalLoopState(directory, sessionId, {
@@ -13191,18 +13285,20 @@ ${parts.join("\n")}`);
13191
13285
  until: ls.until,
13192
13286
  oracle: oracleCmd,
13193
13287
  max_iterations: ls.max_iterations,
13288
+ budget,
13194
13289
  started_at: startedAt,
13195
- holds: holds + 1
13290
+ holds: holds + 1,
13291
+ ledger
13196
13292
  });
13197
13293
  const guidance = result.guidance.replace(/\s+/g, " ").slice(0, 1200);
13198
13294
  sessionService.pushMessage({ type: "message", message: `\u{1F501} Loop not done \u2014 independent review: ${result.reason}`, level: "warning" }, "event");
13199
13295
  enqueueLoopMessage(`/goal ${cond} \u2014 INDEPENDENT REVIEW FEEDBACK (resolve before declaring done): ${guidance}`, "\u{1F501} Re-verifying loop");
13200
13296
  if (!trackedSession.stopped) setTimeout(() => processMessageQueueRef?.(), 200);
13201
13297
  } else if (result.action === "gave_up") {
13202
- writeGoalLoopState(directory, sessionId, { active: false, phase: "gave_up", completed_at: Date.now(), gave_up_reason: result.reason, holds });
13298
+ writeGoalLoopState(directory, sessionId, { active: false, phase: "gave_up", completed_at: Date.now(), gave_up_reason: result.reason, holds, ledger });
13203
13299
  checkSvampConfig?.();
13204
13300
  } else {
13205
- writeGoalLoopState(directory, sessionId, { active: false, phase: "done", completed_at: Date.now(), holds });
13301
+ writeGoalLoopState(directory, sessionId, { active: false, phase: "done", completed_at: Date.now(), holds, ledger });
13206
13302
  checkSvampConfig?.();
13207
13303
  }
13208
13304
  } catch (e) {
@@ -13663,9 +13759,30 @@ ${parts.join("\n")}`);
13663
13759
  try {
13664
13760
  const ls = readLoopState(directory, sessionId);
13665
13761
  if (ls && ls.engine === "goal" && ls.active !== false && ls.phase !== "done" && ls.phase !== "gave_up" && ls.phase !== "cancelled") {
13666
- if (process.env.SVAMP_LOOP_VERIFY === "0") {
13667
- writeGoalLoopState(directory, sessionId, { active: false, phase: "done", completed_at: Date.now() });
13762
+ const now = Date.now();
13763
+ const ledger = accumulateLedger(ls.ledger, {
13764
+ turns: Number(msg.num_turns) || 0,
13765
+ tokens: sumUsageTokens(msg.usage),
13766
+ ts: now
13767
+ });
13768
+ ls.ledger = ledger;
13769
+ const startedAt = typeof ls.started_at === "number" ? ls.started_at : typeof ls.resumed_at === "number" ? ls.resumed_at : void 0;
13770
+ const budgetCheck = process.env.SVAMP_LOOP_BUDGET === "0" ? { exceeded: false } : checkLoopBudget(ledger, { max_iterations: ls.max_iterations, ...ls.budget || {} }, now, startedAt);
13771
+ if (budgetCheck.exceeded) {
13772
+ writeGoalLoopState(directory, sessionId, {
13773
+ ...ls,
13774
+ active: false,
13775
+ phase: "gave_up",
13776
+ completed_at: now,
13777
+ gave_up_reason: `resource budget exhausted \u2014 ${budgetCheck.reason}`,
13778
+ ledger
13779
+ });
13780
+ sessionService.pushMessage({ type: "message", message: `\u{1F6D1} Loop stopped \u2014 ${budgetCheck.reason}. Resume & extend with: svamp session loop-resume ${sessionId} --max <N>`, level: "warning" }, "event");
13781
+ checkSvampConfig?.();
13782
+ } else if (process.env.SVAMP_LOOP_VERIFY === "0") {
13783
+ writeGoalLoopState(directory, sessionId, { ...ls, active: false, phase: "done", completed_at: now, ledger });
13668
13784
  } else {
13785
+ writeGoalLoopState(directory, sessionId, { ...ls, ledger });
13669
13786
  void verifyGoalCompletion(ls);
13670
13787
  }
13671
13788
  }
@@ -14502,11 +14619,11 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
14502
14619
  });
14503
14620
  },
14504
14621
  onIssue: async (params) => {
14505
- const { issueRpc } = await import('./rpc-RpVqDF8O.mjs');
14622
+ const { issueRpc } = await import('./rpc-Cm2Uy3Hf.mjs');
14506
14623
  return issueRpc(params?.cwd || directory, params || {}, { notifySession: notifyIssueOwner, rekickLoopOwner });
14507
14624
  },
14508
14625
  onWorkflow: async (params) => {
14509
- const { workflowRpc } = await import('./rpc-D_Suxu2Q.mjs');
14626
+ const { workflowRpc } = await import('./rpc-CPHva9-3.mjs');
14510
14627
  return workflowRpc(params?.cwd || directory, params || {});
14511
14628
  },
14512
14629
  onRipgrep: async (args, cwd) => {
@@ -15039,11 +15156,11 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
15039
15156
  });
15040
15157
  },
15041
15158
  onIssue: async (params) => {
15042
- const { issueRpc } = await import('./rpc-RpVqDF8O.mjs');
15159
+ const { issueRpc } = await import('./rpc-Cm2Uy3Hf.mjs');
15043
15160
  return issueRpc(params?.cwd || directory, params || {}, { notifySession: notifyIssueOwner, rekickLoopOwner });
15044
15161
  },
15045
15162
  onWorkflow: async (params) => {
15046
- const { workflowRpc } = await import('./rpc-D_Suxu2Q.mjs');
15163
+ const { workflowRpc } = await import('./rpc-CPHva9-3.mjs');
15047
15164
  return workflowRpc(params?.cwd || directory, params || {});
15048
15165
  },
15049
15166
  onRipgrep: async (args, cwd) => {
@@ -15211,6 +15328,100 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
15211
15328
  }
15212
15329
  let acpStopped = false;
15213
15330
  let acpBackendReady = false;
15331
+ let acpVerifyInFlight = false;
15332
+ const dispatchAcpPrompt = (text, displayText) => {
15333
+ if (acpStopped) return;
15334
+ sessionMetadata = { ...sessionMetadata, lifecycleState: "running" };
15335
+ sessionService.updateMetadata(sessionMetadata);
15336
+ sessionService.sendKeepAlive(true);
15337
+ sessionService.pushMessage(displayText, "user");
15338
+ agentBackend.sendPrompt(sessionId, text).catch((err) => {
15339
+ logger.error(`[Session ${sessionId}] ACP loop re-kick send failed: ${err.message}`);
15340
+ if (!acpStopped) {
15341
+ sessionMetadata = { ...sessionMetadata, lifecycleState: "idle" };
15342
+ sessionService.updateMetadata(sessionMetadata);
15343
+ sessionService.sendSessionEnd();
15344
+ }
15345
+ });
15346
+ };
15347
+ const verifyAcpLoopCompletion = async () => {
15348
+ if (acpVerifyInFlight || acpStopped) return;
15349
+ const ls = readLoopState(directory, sessionId);
15350
+ if (!ls || ls.engine !== "goal" || ls.active === false || ls.phase === "done" || ls.phase === "gave_up" || ls.phase === "cancelled") return;
15351
+ if (!isLoopActiveForSession(directory, sessionId)) return;
15352
+ acpVerifyInFlight = true;
15353
+ const now = Date.now();
15354
+ const projectRoot = resolveProjectRoot(directory);
15355
+ const startedAt = typeof ls.started_at === "number" ? ls.started_at : typeof ls.resumed_at === "number" ? ls.resumed_at : void 0;
15356
+ const oracleCmd = typeof ls.oracle === "string" && ls.oracle.trim() ? ls.oracle.trim() : void 0;
15357
+ const ledger = accumulateLedger(ls.ledger, { turns: 1, tokens: 0, ts: now });
15358
+ ls.ledger = ledger;
15359
+ const holds = typeof ls.holds === "number" ? ls.holds : 0;
15360
+ const runOracle = async () => {
15361
+ if (oracleCmd) {
15362
+ return await new Promise((resolve2) => {
15363
+ exec$1(oracleCmd, { cwd: directory, timeout: 12e4, maxBuffer: 16 * 1024 * 1024 }, (err, stdout, stderr) => {
15364
+ if (err) resolve2({ ok: false, output: (String(stdout || "") + String(stderr || "")).slice(-500) });
15365
+ else resolve2({ ok: true, output: "" });
15366
+ });
15367
+ });
15368
+ }
15369
+ const pending = backlogOraclePending(projectRoot, sessionId);
15370
+ return { ok: pending.length === 0, output: pending.length ? `${pending.length} pending: ${pending.map((i) => "#" + i.id).join(" ")}` : "No pending issues." };
15371
+ };
15372
+ try {
15373
+ const budgetCheck = process.env.SVAMP_LOOP_BUDGET === "0" ? { exceeded: false } : checkLoopBudget(ledger, { max_iterations: ls.max_iterations, ...ls.budget || {} }, now, startedAt);
15374
+ if (budgetCheck.exceeded) {
15375
+ writeGoalLoopState(directory, sessionId, { ...ls, active: false, phase: "gave_up", completed_at: now, gave_up_reason: `resource budget exhausted \u2014 ${budgetCheck.reason}`, ledger });
15376
+ sessionService.pushMessage({ type: "message", message: `\u{1F6D1} Loop stopped \u2014 ${budgetCheck.reason}. Resume & extend with: svamp session loop-resume ${sessionId} --max <N>`, level: "warning" }, "event");
15377
+ checkSvampConfig?.();
15378
+ return;
15379
+ }
15380
+ const oracle = await runOracle();
15381
+ if (!oracle.ok) {
15382
+ writeGoalLoopState(directory, sessionId, { ...ls, active: true, phase: "continue", ledger });
15383
+ dispatchAcpPrompt(`Continue toward the goal: ${ls.goal_task || ls.until || "the task"}. Not done yet \u2014 the loop oracle still reports pending work:
15384
+ ${oracle.output.trim().slice(0, 500)}`, "\u{1F501} Continuing loop");
15385
+ return;
15386
+ }
15387
+ if (process.env.SVAMP_LOOP_VERIFY === "0") {
15388
+ writeGoalLoopState(directory, sessionId, { ...ls, active: false, phase: "done", completed_at: now, ledger });
15389
+ checkSvampConfig?.();
15390
+ return;
15391
+ }
15392
+ sessionService.pushMessage({ type: "message", message: "\u{1F50D} Verifying loop completion (independent review)\u2026" }, "event");
15393
+ const result = await runLoopVerification(
15394
+ { task: ls.goal_task || ls.task, until: ls.until, oracle: oracleCmd, startedAt, holds, maxHolds: 3 },
15395
+ {
15396
+ projectRoot,
15397
+ runOracle: async () => ({ ok: true, output: oracle.output }),
15398
+ runEvaluator: (prompt) => spawnHeadlessEvaluator(prompt, { cwd: directory, model: resolveEvaluatorModel(process.env.SVAMP_LOOP_EVALUATOR_MODEL) }),
15399
+ log: (m) => logger.log(`[Session ${sessionId}] ${m}`)
15400
+ }
15401
+ );
15402
+ if (result.action === "rekick") {
15403
+ writeGoalLoopState(directory, sessionId, { ...ls, active: true, phase: "continue", holds: holds + 1, started_at: startedAt, ledger });
15404
+ const guidance = result.guidance.replace(/\s+/g, " ").slice(0, 1200);
15405
+ sessionService.pushMessage({ type: "message", message: `\u{1F501} Loop not done \u2014 independent review: ${result.reason}`, level: "warning" }, "event");
15406
+ dispatchAcpPrompt(`Continue toward the goal: ${ls.goal_task || ls.until || "the task"}. INDEPENDENT REVIEW FEEDBACK (resolve before declaring done): ${guidance}`, "\u{1F501} Re-verifying loop");
15407
+ } else if (result.action === "gave_up") {
15408
+ writeGoalLoopState(directory, sessionId, { ...ls, active: false, phase: "gave_up", completed_at: Date.now(), gave_up_reason: result.reason, holds, ledger });
15409
+ checkSvampConfig?.();
15410
+ } else {
15411
+ writeGoalLoopState(directory, sessionId, { ...ls, active: false, phase: "done", completed_at: Date.now(), holds, ledger });
15412
+ checkSvampConfig?.();
15413
+ }
15414
+ } catch (e) {
15415
+ logger.log(`[Session ${sessionId}] verifyAcpLoopCompletion error \u2014 failing open (done): ${e?.message || e}`);
15416
+ try {
15417
+ writeGoalLoopState(directory, sessionId, { ...ls, active: false, phase: "done", completed_at: Date.now(), ledger });
15418
+ checkSvampConfig?.();
15419
+ } catch {
15420
+ }
15421
+ } finally {
15422
+ acpVerifyInFlight = false;
15423
+ }
15424
+ };
15214
15425
  const onTurnEnd = (lastAssistantText) => {
15215
15426
  if (acpStopped) return;
15216
15427
  insideOnTurnEnd = true;
@@ -15246,6 +15457,8 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
15246
15457
  sessionService.sendSessionEnd();
15247
15458
  }
15248
15459
  });
15460
+ } else if (isLoopActiveForSession(directory, sessionId)) {
15461
+ void verifyAcpLoopCompletion();
15249
15462
  }
15250
15463
  } finally {
15251
15464
  insideOnTurnEnd = false;
@@ -15521,7 +15734,7 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
15521
15734
  sharingNotificationSync,
15522
15735
  persistExposedTunnel,
15523
15736
  forgetExposedTunnel,
15524
- listExposedTunnels: () => loadExposedTunnels().map((t) => ({ name: t.name, ports: t.ports, group: t.group, addedAt: t.addedAt }))
15737
+ listExposedTunnels: () => loadExposedTunnels().map((t) => ({ name: t.name, ports: t.ports, group: t.group, sessionId: t.sessionId, addedAt: t.addedAt }))
15525
15738
  });
15526
15739
  const machineService = await registerMachineService(
15527
15740
  server,
@@ -15817,7 +16030,7 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
15817
16030
  const PING_TIMEOUT_MS = 15e3;
15818
16031
  const POST_RECONNECT_GRACE_MS = 2e4;
15819
16032
  const RECONNECT_JITTER_MS = 2500;
15820
- const { WorkflowScheduler } = await import('./scheduler-B2ZHfyig.mjs');
16033
+ const { WorkflowScheduler } = await import('./scheduler-CwyAdWQa.mjs');
15821
16034
  const workflowScheduler = new WorkflowScheduler({
15822
16035
  projectRoots: () => {
15823
16036
  const dirs = /* @__PURE__ */ new Set();