svamp-cli 0.2.247 → 0.2.248

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 (26) hide show
  1. package/dist/{agentCommands-DOzMhE_G.mjs → agentCommands-B_ozIVKT.mjs} +28 -11
  2. package/dist/{auth-B_ZnoKX0.mjs → auth-BLDmT7XN.mjs} +2 -1
  3. package/dist/cli.mjs +62 -61
  4. package/dist/{commands-vCeWXvYJ.mjs → commands-C3TQOYUo.mjs} +2 -1
  5. package/dist/{commands-Bm5q9G35.mjs → commands-CL8YI8k5.mjs} +2 -1
  6. package/dist/{commands-_rMwFmFH.mjs → commands-CRMQUQZG.mjs} +2 -1
  7. package/dist/{commands-F6j9TF4Q.mjs → commands-Cs2fNd-_.mjs} +7 -7
  8. package/dist/{commands-CxOPcoWa.mjs → commands-DS4i8ohA.mjs} +10 -6
  9. package/dist/{commands-BENx03Oz.mjs → commands-Djvi1cZ2.mjs} +3 -2
  10. package/dist/{commands-C2u9U3y6.mjs → commands-DwhA-iV3.mjs} +3 -2
  11. package/dist/{fleet-pMXCRilx.mjs → fleet-CJZOVjOY.mjs} +2 -1
  12. package/dist/{frpc-Di-y4cWK.mjs → frpc-CsjEXBuS.mjs} +2 -1
  13. package/dist/{headlessCli-D6k9Pnum.mjs → headlessCli-qBontdlf.mjs} +3 -2
  14. package/dist/index.mjs +2 -1
  15. package/dist/{package-BJYmQjS2.mjs → package-B7z6GsNX.mjs} +2 -2
  16. package/dist/{rpc-DbY7NUhj.mjs → rpc-BGFgegj5.mjs} +1 -3
  17. package/dist/{rpc-BncvIQMO.mjs → rpc-C1DI7HfZ.mjs} +2 -1
  18. package/dist/{run-CYx_Rhiu.mjs → run-C61rsl7r.mjs} +2 -1
  19. package/dist/{run-1BBi0_ol.mjs → run-CHIppB2r.mjs} +655 -35
  20. package/dist/{scheduler-BkXOKjHn.mjs → scheduler-Csp21zzg.mjs} +1 -3
  21. package/dist/{serveCommands-CESRU6-1.mjs → serveCommands-BYHk72ZO.mjs} +5 -5
  22. package/dist/{serveManager-BZtpClpC.mjs → serveManager-Di3xrvG2.mjs} +3 -2
  23. package/dist/{sideband-BqD4-WWR.mjs → sideband-Cs-kNLye.mjs} +2 -1
  24. package/package.json +2 -2
  25. package/dist/runStore-C5IyfbON.mjs +0 -239
  26. package/dist/store-BTs0H_y0.mjs +0 -148
@@ -19,6 +19,7 @@ import { ElicitRequestSchema } from '@modelcontextprotocol/sdk/types.js';
19
19
  import { z } from 'zod';
20
20
  import { mkdir, rm, chmod, access, mkdtemp, copyFile, writeFile, readdir, stat, readFile as readFile$1 } from 'node:fs/promises';
21
21
  import { promisify as promisify$1 } from 'node:util';
22
+ import { parse, stringify } from 'yaml';
22
23
 
23
24
  let connectToServerFn = null;
24
25
  async function getConnectToServer() {
@@ -2984,6 +2985,7 @@ async function registerMachineService(server, machineId, metadata, daemonState,
2984
2985
  if (!client) throw new Error(`Tunnel '${params.name}' not found`);
2985
2986
  client.destroy();
2986
2987
  tunnels.delete(params.name);
2988
+ handlers.forgetExposedTunnel?.(params.name);
2987
2989
  return { name: params.name, stopped: true };
2988
2990
  },
2989
2991
  /**
@@ -2996,7 +2998,7 @@ async function registerMachineService(server, machineId, metadata, daemonState,
2996
2998
  const tunnels = handlers.tunnels;
2997
2999
  if (!tunnels) throw new Error("Tunnel management not available");
2998
3000
  if (tunnels.has(params.name)) throw new Error(`Tunnel '${params.name}' already running`);
2999
- const { FrpcTunnel } = await import('./frpc-Di-y4cWK.mjs');
3001
+ const { FrpcTunnel } = await import('./frpc-CsjEXBuS.mjs');
3000
3002
  const tunnel = new FrpcTunnel({
3001
3003
  name: params.name,
3002
3004
  ports: params.ports,
@@ -3447,7 +3449,7 @@ QUESTION: ${params.question || "Summarize this concisely."}` }
3447
3449
  }
3448
3450
  const deps = buildSessionDeps(rpc, { cwd, ownerEmail: owner });
3449
3451
  const sender = { name: context?.user?.email || context?.user?.id || "user", kind: "user", verified: true };
3450
- const { toolsForRole } = await import('./sideband-BqD4-WWR.mjs');
3452
+ const { toolsForRole } = await import('./sideband-Cs-kNLye.mjs');
3451
3453
  const r2 = await runWiseAgent({ message: params.message, sender, config: { tools: toolsForRole(role2) }, deps, transport, model: resolved.model });
3452
3454
  return fmt(r2);
3453
3455
  }
@@ -3546,7 +3548,7 @@ QUESTION: ${params.question || "Summarize this concisely."}` }
3546
3548
  if (r.error || !r.sender) return { error: r.error || "unauthorized" };
3547
3549
  const callId = "call_" + Math.random().toString(16).slice(2, 12);
3548
3550
  const rendered = renderMessage(c, { sender: r.sender, body: { message: kwargs.message }, callId });
3549
- const { queryCore } = await import('./commands-vCeWXvYJ.mjs');
3551
+ const { queryCore } = await import('./commands-C3TQOYUo.mjs');
3550
3552
  const timeout = c.reply?.timeout_sec || 120;
3551
3553
  let result;
3552
3554
  try {
@@ -10920,6 +10922,438 @@ function summarize(issues) {
10920
10922
  return { total: issues.length, ready: by("ready"), in_progress: by("in_progress"), archived: by("archived") };
10921
10923
  }
10922
10924
 
10925
+ function isWorkflowEnabled(wf) {
10926
+ return wf.enabled !== false;
10927
+ }
10928
+ function workflowSteps(wf) {
10929
+ return Object.values(wf.jobs || {}).flatMap((j) => j?.steps || []);
10930
+ }
10931
+ function workflowCrons(wf) {
10932
+ return (wf.on?.schedule || []).map((s) => s?.cron).filter((c) => !!c);
10933
+ }
10934
+ function workflowsDir(projectRoot) {
10935
+ return join(projectRoot, ".svamp", "workflows");
10936
+ }
10937
+ function workflowPath(projectRoot, name) {
10938
+ return join(workflowsDir(projectRoot), `${name}.yaml`);
10939
+ }
10940
+ function normalizeOn(on) {
10941
+ if (!on || typeof on !== "object") {
10942
+ return void 0;
10943
+ }
10944
+ const out = {};
10945
+ if (Array.isArray(on.schedule)) {
10946
+ const sched = on.schedule.map((s) => ({ cron: String(s?.cron ?? s ?? "") })).filter((s) => s.cron);
10947
+ if (sched.length) out.schedule = sched;
10948
+ } else if (typeof on.schedule === "string" && on.schedule.trim()) {
10949
+ out.schedule = [{ cron: on.schedule.trim() }];
10950
+ }
10951
+ if ("workflow_dispatch" in on || on.dispatch === true) out.workflow_dispatch = true;
10952
+ if (typeof on.channel === "string" && on.channel) out.channel = on.channel;
10953
+ if (Array.isArray(on.issue) && on.issue.length) out.issue = on.issue.map(String);
10954
+ if ("idle" in on && on.idle !== false || on.idle === true) out.idle = true;
10955
+ return Object.keys(out).length ? out : void 0;
10956
+ }
10957
+ function normalizeJobs(jobs) {
10958
+ if (Array.isArray(jobs)) {
10959
+ const steps = jobs.map((j) => ({ run: String(j?.run ?? "") })).filter((s) => s.run);
10960
+ return steps.length ? { run: { steps } } : {};
10961
+ }
10962
+ if (jobs && typeof jobs === "object") {
10963
+ const out = {};
10964
+ for (const [id, job] of Object.entries(jobs)) {
10965
+ const rawSteps = Array.isArray(job?.steps) ? job.steps : job?.run ? [{ run: job.run }] : [];
10966
+ const steps = rawSteps.map((s) => ({ run: String(s?.run ?? ""), ...s?.name ? { name: String(s.name) } : {} })).filter((s) => s.run);
10967
+ if (steps.length) out[String(id)] = { steps };
10968
+ }
10969
+ return out;
10970
+ }
10971
+ return {};
10972
+ }
10973
+ function parseWorkflow(content) {
10974
+ try {
10975
+ const o = parse(content);
10976
+ if (!o || typeof o !== "object" || !o.name) return null;
10977
+ return {
10978
+ name: String(o.name),
10979
+ on: normalizeOn(o.on),
10980
+ jobs: normalizeJobs(o.jobs),
10981
+ session: o.session ?? null,
10982
+ // Only `enabled: false` disables; anything else (absent/true) is active.
10983
+ ...o.enabled === false ? { enabled: false } : {}
10984
+ };
10985
+ } catch {
10986
+ return null;
10987
+ }
10988
+ }
10989
+ function serializeWorkflow(wf) {
10990
+ const clean = { name: wf.name };
10991
+ if (wf.session) clean.session = wf.session;
10992
+ if (wf.enabled === false) clean.enabled = false;
10993
+ const on = {};
10994
+ if (wf.on?.schedule?.length) on.schedule = wf.on.schedule.map((s) => ({ cron: s.cron }));
10995
+ if (wf.on?.workflow_dispatch) on.workflow_dispatch = {};
10996
+ if (wf.on?.channel) on.channel = wf.on.channel;
10997
+ if (wf.on?.issue?.length) on.issue = wf.on.issue;
10998
+ if (wf.on?.idle) on.idle = {};
10999
+ if (Object.keys(on).length) clean.on = on;
11000
+ const jobs = {};
11001
+ for (const [id, job] of Object.entries(wf.jobs || {})) {
11002
+ jobs[id] = { steps: (job.steps || []).map((s) => s.name ? { name: s.name, run: s.run } : { run: s.run }) };
11003
+ }
11004
+ clean.jobs = Object.keys(jobs).length ? jobs : { run: { steps: [] } };
11005
+ return stringify(clean);
11006
+ }
11007
+ function listWorkflows(projectRoot) {
11008
+ const dir = workflowsDir(projectRoot);
11009
+ if (!existsSync(dir)) return [];
11010
+ const out = [];
11011
+ for (const name of readdirSync(dir)) {
11012
+ if (!name.endsWith(".yaml") && !name.endsWith(".yml")) continue;
11013
+ const p = join(dir, name);
11014
+ try {
11015
+ const wf = parseWorkflow(readFileSync(p, "utf-8"));
11016
+ if (wf) {
11017
+ try {
11018
+ const st = statSync(p);
11019
+ wf.created = new Date(st.birthtimeMs || st.mtimeMs).toISOString();
11020
+ } catch {
11021
+ }
11022
+ out.push(wf);
11023
+ }
11024
+ } catch {
11025
+ }
11026
+ }
11027
+ return out.sort((a, b) => a.name.localeCompare(b.name));
11028
+ }
11029
+ function getWorkflow(projectRoot, name) {
11030
+ const p = workflowPath(projectRoot, name);
11031
+ if (!existsSync(p)) return null;
11032
+ try {
11033
+ return parseWorkflow(readFileSync(p, "utf-8"));
11034
+ } catch {
11035
+ return null;
11036
+ }
11037
+ }
11038
+ function rawWorkflow(projectRoot, name) {
11039
+ const p = workflowPath(projectRoot, name);
11040
+ return existsSync(p) ? readFileSync(p, "utf-8") : null;
11041
+ }
11042
+ function saveWorkflow(projectRoot, wf) {
11043
+ const dir = workflowsDir(projectRoot);
11044
+ mkdirSync(dir, { recursive: true });
11045
+ const path = workflowPath(projectRoot, wf.name);
11046
+ const tmp = `${path}.tmp-${process.pid}`;
11047
+ writeFileSync(tmp, serializeWorkflow(wf));
11048
+ renameSync(tmp, path);
11049
+ }
11050
+ function setWorkflowEnabled(projectRoot, name, enabled) {
11051
+ const wf = getWorkflow(projectRoot, name);
11052
+ if (!wf) return null;
11053
+ const next = { ...wf };
11054
+ if (enabled) delete next.enabled;
11055
+ else next.enabled = false;
11056
+ saveWorkflow(projectRoot, next);
11057
+ return next;
11058
+ }
11059
+ function removeWorkflow(projectRoot, name) {
11060
+ const p = workflowPath(projectRoot, name);
11061
+ if (!existsSync(p)) return false;
11062
+ try {
11063
+ unlinkSync(p);
11064
+ return true;
11065
+ } catch {
11066
+ return false;
11067
+ }
11068
+ }
11069
+
11070
+ const MAX_RUNS_PER_WORKFLOW = 50;
11071
+ function maxRunsPerWorkflow() {
11072
+ const raw = Number(process.env.SVAMP_MAX_RUNS_PER_WORKFLOW);
11073
+ if (Number.isFinite(raw) && raw > 0) return Math.min(Math.floor(raw), 5e3);
11074
+ return MAX_RUNS_PER_WORKFLOW;
11075
+ }
11076
+ const MAX_STREAM_CHARS = 8e3;
11077
+ const STEP_TIMEOUT_MS = 12e4;
11078
+ function capStream(s, max = MAX_STREAM_CHARS) {
11079
+ if (s.length <= max) return s;
11080
+ const head = Math.floor(max * 0.6);
11081
+ const tail = max - head;
11082
+ const omitted = s.length - max;
11083
+ return `${s.slice(0, head)}
11084
+ \u2026 [${omitted} chars omitted] \u2026
11085
+ ${s.slice(s.length - tail)}`;
11086
+ }
11087
+ function runsDir(projectRoot) {
11088
+ return join(workflowsDir(projectRoot), ".runs");
11089
+ }
11090
+ function runsFile(projectRoot, workflow) {
11091
+ const safe = workflow.replace(/[^\w.-]+/g, "_");
11092
+ return join(runsDir(projectRoot), `${safe}.jsonl`);
11093
+ }
11094
+ function shortRunId(seed) {
11095
+ let h = 2166136261;
11096
+ for (let i = 0; i < seed.length; i++) {
11097
+ h ^= seed.charCodeAt(i);
11098
+ h = Math.imul(h, 16777619);
11099
+ }
11100
+ return (h >>> 0).toString(36).padStart(7, "0").slice(0, 7);
11101
+ }
11102
+ function recordRun(projectRoot, run) {
11103
+ try {
11104
+ const dir = runsDir(projectRoot);
11105
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
11106
+ const file = runsFile(projectRoot, run.workflow);
11107
+ const existing = loadRuns(projectRoot, run.workflow);
11108
+ const idx = existing.findIndex((r) => r.id === run.id);
11109
+ if (idx >= 0) existing[idx] = run;
11110
+ else existing.push(run);
11111
+ const kept = existing.slice(-maxRunsPerWorkflow());
11112
+ const tmp = `${file}.tmp-${process.pid}-${randomBytes(4).toString("hex")}`;
11113
+ writeFileSync(tmp, kept.map((r) => JSON.stringify(r)).join("\n") + "\n");
11114
+ renameSync(tmp, file);
11115
+ } catch {
11116
+ }
11117
+ }
11118
+ function loadRuns(projectRoot, workflow) {
11119
+ try {
11120
+ const file = runsFile(projectRoot, workflow);
11121
+ if (!existsSync(file)) return [];
11122
+ return readFileSync(file, "utf-8").split("\n").filter(Boolean).map((l) => {
11123
+ try {
11124
+ return JSON.parse(l);
11125
+ } catch {
11126
+ return null;
11127
+ }
11128
+ }).filter((r) => !!r);
11129
+ } catch {
11130
+ return [];
11131
+ }
11132
+ }
11133
+ function listRuns(projectRoot, workflow, opts = {}) {
11134
+ const o = typeof opts === "number" ? { limit: opts } : opts;
11135
+ const all = loadRuns(projectRoot, workflow);
11136
+ const total = all.length;
11137
+ const newestFirst = all.slice().reverse();
11138
+ const offset = Math.max(0, Math.floor(o.offset ?? 0));
11139
+ const limit = o.limit != null && o.limit > 0 ? Math.floor(o.limit) : total;
11140
+ const page = newestFirst.slice(offset, offset + limit).map((r) => {
11141
+ const failedStep = r.steps.findIndex((s) => s.exitCode !== 0 && s.exitCode !== null || s.timedOut);
11142
+ const { steps, ...rest } = r;
11143
+ return { ...rest, stepCount: steps.length, failedStep: failedStep >= 0 ? failedStep : void 0 };
11144
+ });
11145
+ return { runs: page, total, offset, hasMore: offset + page.length < total };
11146
+ }
11147
+ function getRun(projectRoot, workflow, runId) {
11148
+ return loadRuns(projectRoot, workflow).find((r) => r.id === runId) || null;
11149
+ }
11150
+ function defaultExecStep(root, cmd, env) {
11151
+ return new Promise((resolve) => {
11152
+ let stdout = "", stderr = "", timedOut = false, settled = false;
11153
+ const finish = (exitCode) => {
11154
+ if (settled) return;
11155
+ settled = true;
11156
+ resolve({ exitCode, stdout, stderr, timedOut });
11157
+ };
11158
+ try {
11159
+ const child = spawn("sh", ["-c", cmd], { cwd: root, env, stdio: ["ignore", "pipe", "pipe"] });
11160
+ const timer = setTimeout(() => {
11161
+ timedOut = true;
11162
+ try {
11163
+ child.kill("SIGKILL");
11164
+ } catch {
11165
+ }
11166
+ }, STEP_TIMEOUT_MS);
11167
+ child.stdout?.on("data", (d) => {
11168
+ stdout += d.toString();
11169
+ if (stdout.length > MAX_STREAM_CHARS * 4) stdout = capStream(stdout);
11170
+ });
11171
+ child.stderr?.on("data", (d) => {
11172
+ stderr += d.toString();
11173
+ if (stderr.length > MAX_STREAM_CHARS * 4) stderr = capStream(stderr);
11174
+ });
11175
+ child.on("error", (e) => {
11176
+ clearTimeout(timer);
11177
+ stderr += `
11178
+ [spawn error] ${e.message}`;
11179
+ finish(null);
11180
+ });
11181
+ child.on("close", (code) => {
11182
+ clearTimeout(timer);
11183
+ finish(code);
11184
+ });
11185
+ } catch (e) {
11186
+ stderr += `[spawn threw] ${e?.message || e}`;
11187
+ finish(null);
11188
+ }
11189
+ });
11190
+ }
11191
+ function escalateWorkflowFailure(projectRoot, wf, run) {
11192
+ try {
11193
+ if (process.env.SVAMP_WORKFLOW_ESCALATE === "0") return void 0;
11194
+ const failLabel = `wf-fail:${wf.name}`;
11195
+ const open = listIssues(projectRoot, { label: failLabel }).filter((i) => i.status !== "archived");
11196
+ if (open.length > 0) return void 0;
11197
+ const failed = run.steps.find((s) => s.timedOut || s.exitCode !== 0 && s.exitCode !== null) || run.steps[run.steps.length - 1];
11198
+ const failCause = run.error ? `runner error: ${run.error}` : failed?.timedOut ? `step timed out after ${STEP_TIMEOUT_MS / 1e3}s` : `step exited with code ${failed?.exitCode ?? "null (killed/signal)"}`;
11199
+ const defLines = workflowSteps(wf).map((s, i) => ` ${i}. ${s.run}`).join("\n") || " (no steps)";
11200
+ const stderrTail = failed?.stderr ? capStream(failed.stderr, 3e3) : "(none)";
11201
+ const stdoutTail = failed?.stdout ? capStream(failed.stdout, 1500) : "(none)";
11202
+ const body = [
11203
+ `Workflow **${wf.name}** failed on its last run (${run.trigger} trigger${run.triggerContext ? ` \xB7 ${run.triggerContext}` : ""}). Diagnose the failure and **correct the workflow definition** so future runs succeed \u2014 edit it with \`svamp workflow\` (see \`svamp workflow --help\`), or pause/remove it if it's obsolete.`,
11204
+ "",
11205
+ "## Failure",
11206
+ `- Cause: ${failCause}`,
11207
+ `- Failed step: ${failed ? `#${failed.index} \u2014 \`${failed.cmd}\`` : "(unknown)"}`,
11208
+ `- Run id: ${run.id} \xB7 finished: ${run.finishedAt || "?"}`,
11209
+ "",
11210
+ "## Workflow definition",
11211
+ "```",
11212
+ `name: ${wf.name}`,
11213
+ `steps:`,
11214
+ defLines,
11215
+ "```",
11216
+ "",
11217
+ "## Failed step stderr (tail)",
11218
+ "```",
11219
+ stderrTail,
11220
+ "```",
11221
+ "",
11222
+ "## Failed step stdout (tail)",
11223
+ "```",
11224
+ stdoutTail,
11225
+ "```"
11226
+ ].join("\n");
11227
+ const issue = addIssue(projectRoot, {
11228
+ title: `Fix failing workflow "${wf.name}" (${failCause})`,
11229
+ labels: ["workflow", "automation", "auto-escalated", failLabel],
11230
+ verify: { type: "agent", text: `The workflow "${wf.name}" is corrected (or removed/paused) so it no longer fails with: ${failCause}` },
11231
+ scope: wf.session ? "session" : "project",
11232
+ session: wf.session ?? null,
11233
+ triaged: true,
11234
+ body
11235
+ });
11236
+ return issue.id;
11237
+ } catch {
11238
+ return void 0;
11239
+ }
11240
+ }
11241
+ async function runWorkflow(projectRoot, wf, opts) {
11242
+ const now = opts.now || (() => {
11243
+ const d = /* @__PURE__ */ new Date();
11244
+ return { iso: d.toISOString(), ms: d.getTime() };
11245
+ });
11246
+ const exec = opts.execStep || defaultExecStep;
11247
+ const start = now();
11248
+ const id = shortRunId(`${wf.name}:${start.iso}:${start.ms}`);
11249
+ const env = wf.session ? { ...process.env, SVAMP_SESSION_ID: wf.session } : process.env;
11250
+ const run = {
11251
+ id,
11252
+ workflow: wf.name,
11253
+ trigger: opts.trigger,
11254
+ triggerContext: opts.triggerContext,
11255
+ actor: opts.actor ?? wf.session ?? null,
11256
+ startedAt: start.iso,
11257
+ status: "running",
11258
+ steps: []
11259
+ };
11260
+ recordRun(projectRoot, run);
11261
+ const steps = workflowSteps(wf);
11262
+ try {
11263
+ for (let i = 0; i < steps.length; i++) {
11264
+ const s0 = now();
11265
+ const r = await exec(projectRoot, steps[i].run, env);
11266
+ const s1 = now();
11267
+ run.steps.push({
11268
+ index: i,
11269
+ cmd: steps[i].run,
11270
+ exitCode: r.exitCode,
11271
+ stdout: capStream(r.stdout),
11272
+ stderr: capStream(r.stderr),
11273
+ durationMs: s1.ms - s0.ms,
11274
+ timedOut: r.timedOut || void 0
11275
+ });
11276
+ if (r.timedOut || r.exitCode !== 0) {
11277
+ run.status = "failed";
11278
+ break;
11279
+ }
11280
+ }
11281
+ if (run.status === "running") run.status = "success";
11282
+ } catch (e) {
11283
+ run.status = "failed";
11284
+ run.error = e?.message || String(e);
11285
+ }
11286
+ const end = now();
11287
+ run.finishedAt = end.iso;
11288
+ run.durationMs = end.ms - start.ms;
11289
+ if (run.status === "failed") {
11290
+ const escalated = escalateWorkflowFailure(projectRoot, wf, run);
11291
+ if (escalated) {
11292
+ run.escalatedIssue = escalated;
11293
+ opts.log?.(`[workflow] run ${id} "${wf.name}" failed \u2192 escalated to issue #${escalated}`);
11294
+ }
11295
+ }
11296
+ recordRun(projectRoot, run);
11297
+ opts.log?.(`[workflow] run ${id} "${wf.name}" \u2192 ${run.status} (${run.steps.length} step${run.steps.length === 1 ? "" : "s"})`);
11298
+ return run;
11299
+ }
11300
+
11301
+ const DEFAULT_IDLE_COOLDOWN_MS = 15e3;
11302
+ function idleCooldownMs() {
11303
+ const raw = Number(process.env.SVAMP_WORKFLOW_IDLE_COOLDOWN_MS);
11304
+ if (Number.isFinite(raw) && raw >= 0) return Math.floor(raw);
11305
+ return DEFAULT_IDLE_COOLDOWN_MS;
11306
+ }
11307
+ function shouldFireIdle(prevActive, event, lastFireMs, now, cooldownMs) {
11308
+ if (prevActive !== true) return false;
11309
+ if (event.active !== false) return false;
11310
+ if (lastFireMs != null && now - lastFireMs < cooldownMs) return false;
11311
+ return true;
11312
+ }
11313
+ class IdleTriggerTracker {
11314
+ constructor(cooldownMs = idleCooldownMs()) {
11315
+ this.cooldownMs = cooldownMs;
11316
+ }
11317
+ prevActive = /* @__PURE__ */ new Map();
11318
+ lastFire = /* @__PURE__ */ new Map();
11319
+ observe(event, now) {
11320
+ if (!event || event.type !== "activity" || !event.sessionId || typeof event.active !== "boolean") return false;
11321
+ const sid = event.sessionId;
11322
+ const fire = shouldFireIdle(this.prevActive.get(sid), event, this.lastFire.get(sid), now, this.cooldownMs);
11323
+ this.prevActive.set(sid, event.active);
11324
+ if (fire) this.lastFire.set(sid, now);
11325
+ return fire;
11326
+ }
11327
+ /** Drop tracked state for a session (e.g. when it is archived/removed) so its memory is reclaimed. */
11328
+ forget(sessionId) {
11329
+ this.prevActive.delete(sessionId);
11330
+ this.lastFire.delete(sessionId);
11331
+ }
11332
+ }
11333
+ function fireIdleWorkflows(root, sessionId, deps = {}) {
11334
+ let workflows;
11335
+ try {
11336
+ workflows = listWorkflows(root);
11337
+ } catch {
11338
+ return [];
11339
+ }
11340
+ const matches = workflows.filter((wf) => wf.on?.idle === true && isWorkflowEnabled(wf));
11341
+ return matches.map((wf) => {
11342
+ deps.log?.(`[workflow] idle fired "${wf.name}" in ${root} for session ${sessionId}`);
11343
+ return runWorkflow(root, wf, {
11344
+ trigger: "idle",
11345
+ triggerContext: sessionId,
11346
+ actor: wf.session || null,
11347
+ execStep: deps.execStep,
11348
+ now: deps.now,
11349
+ log: deps.log
11350
+ }).catch((e) => {
11351
+ deps.log?.(`[workflow] idle run threw for "${wf.name}": ${e?.message || e}`);
11352
+ return void 0;
11353
+ });
11354
+ });
11355
+ }
11356
+
10923
11357
  const UNBOUNDED_LOOP_ITERATIONS = 1e6;
10924
11358
  function backlogOraclePending(projectRoot, sessionId) {
10925
11359
  const inScope = (i) => !sessionId || isVisibleTo(i, sessionId);
@@ -11010,30 +11444,84 @@ function tokensInLastHour(ledger, now) {
11010
11444
  if (!ledger || !Array.isArray(ledger.window)) return 0;
11011
11445
  return ledger.window.filter(([ts]) => now - ts < HOUR_MS).reduce((s, [, t]) => s + (t > 0 ? t : 0), 0);
11012
11446
  }
11447
+ function isCostCeiling(kind) {
11448
+ return !!kind && kind !== "iteration";
11449
+ }
11013
11450
  function checkLoopBudget(ledger, limits, now, startedAt) {
11014
11451
  const L = limits || {};
11015
11452
  const led = ledger || { turns: 0, tokens: 0, window: [] };
11016
11453
  const pos = (v) => typeof v === "number" && Number.isFinite(v) && v > 0;
11017
11454
  if (pos(L.max_iterations) && L.max_iterations < UNBOUNDED_LOOP_ITERATIONS && led.turns >= L.max_iterations) {
11018
- return { exceeded: true, reason: `iteration budget reached (${led.turns}/${L.max_iterations} turns)` };
11455
+ return { exceeded: true, reason: `iteration budget reached (${led.turns}/${L.max_iterations} turns)`, kind: "iteration" };
11019
11456
  }
11020
11457
  if (pos(L.max_runtime_sec) && pos(startedAt)) {
11021
11458
  const elapsed = (now - startedAt) / 1e3;
11022
11459
  if (elapsed >= L.max_runtime_sec) {
11023
- return { exceeded: true, reason: `runtime budget reached (${Math.round(elapsed)}s/${L.max_runtime_sec}s)` };
11460
+ return { exceeded: true, reason: `runtime budget reached (${Math.round(elapsed)}s/${L.max_runtime_sec}s)`, kind: "runtime" };
11024
11461
  }
11025
11462
  }
11026
11463
  if (pos(L.max_tokens) && led.tokens >= L.max_tokens) {
11027
- return { exceeded: true, reason: `token budget reached (${led.tokens}/${L.max_tokens} tokens)` };
11464
+ return { exceeded: true, reason: `token budget reached (${led.tokens}/${L.max_tokens} tokens)`, kind: "tokens" };
11028
11465
  }
11029
11466
  if (pos(L.max_tokens_per_hour)) {
11030
11467
  const rate = tokensInLastHour(led, now);
11031
11468
  if (rate >= L.max_tokens_per_hour) {
11032
- return { exceeded: true, reason: `token-rate budget reached (${rate}/${L.max_tokens_per_hour} tokens in the last hour)` };
11469
+ return { exceeded: true, reason: `token-rate budget reached (${rate}/${L.max_tokens_per_hour} tokens in the last hour)`, kind: "tokens_per_hour" };
11033
11470
  }
11034
11471
  }
11035
11472
  return { exceeded: false };
11036
11473
  }
11474
+ const DEFAULT_LOOP_STUCK_CHECKPOINTS = 3;
11475
+ const DEFAULT_LOOP_MAX_AUTORESUMES = 3;
11476
+ const PROGRESS_HISTORY_CAP = 12;
11477
+ function resolveStuckLimit(envValue) {
11478
+ const n = Number(envValue);
11479
+ if (Number.isFinite(n) && n >= 1) return Math.floor(n);
11480
+ return DEFAULT_LOOP_STUCK_CHECKPOINTS;
11481
+ }
11482
+ function resolveMaxAutoResumes(envValue) {
11483
+ const n = Number(envValue);
11484
+ if (Number.isFinite(n) && n >= 0) return Math.floor(n);
11485
+ return DEFAULT_LOOP_MAX_AUTORESUMES;
11486
+ }
11487
+ function recordProgressMarker(history, marker, cap = PROGRESS_HISTORY_CAP) {
11488
+ const base = Array.isArray(history) ? history : [];
11489
+ const next = [...base, marker];
11490
+ return next.length > cap ? next.slice(next.length - cap) : next;
11491
+ }
11492
+ function assessLoopProgress(history, opts) {
11493
+ const stuckLimit = opts && typeof opts.stuckLimit === "number" && opts.stuckLimit >= 1 ? Math.floor(opts.stuckLimit) : DEFAULT_LOOP_STUCK_CHECKPOINTS;
11494
+ const h = (Array.isArray(history) ? history : []).filter((m) => m && typeof m.pending === "number" && Number.isFinite(m.pending));
11495
+ if (h.length === 0) return { progressing: true, stuckCheckpoints: 0 };
11496
+ let best = Infinity;
11497
+ let stuck = 0;
11498
+ for (const m of h) {
11499
+ if (m.pending < best) {
11500
+ best = m.pending;
11501
+ stuck = 0;
11502
+ } else {
11503
+ stuck++;
11504
+ }
11505
+ }
11506
+ return { progressing: stuck < stuckLimit, stuckCheckpoints: stuck };
11507
+ }
11508
+ function decideLoopContinuation(input) {
11509
+ if (input.costCeilingBreached) {
11510
+ return { action: "stop", kind: "cost", reason: input.costReason || "cost budget exhausted" };
11511
+ }
11512
+ const stuckLimit = typeof input.stuckLimit === "number" && input.stuckLimit >= 1 ? Math.floor(input.stuckLimit) : DEFAULT_LOOP_STUCK_CHECKPOINTS;
11513
+ const maxAutoResumes = typeof input.maxAutoResumes === "number" && input.maxAutoResumes >= 0 ? Math.floor(input.maxAutoResumes) : DEFAULT_LOOP_MAX_AUTORESUMES;
11514
+ const autoResumes = typeof input.autoResumes === "number" && input.autoResumes > 0 ? Math.floor(input.autoResumes) : 0;
11515
+ const { progressing, stuckCheckpoints } = assessLoopProgress(input.progressHistory, { stuckLimit });
11516
+ if (progressing) {
11517
+ return { action: "continue", kind: "progress", consumeAutoResume: false, reason: `still making progress (${stuckCheckpoints}/${stuckLimit} stuck checkpoints)` };
11518
+ }
11519
+ if (autoResumes < maxAutoResumes) {
11520
+ return { action: "continue", kind: "autoresume", consumeAutoResume: true, reason: `no progress across ${stuckCheckpoints} checkpoints \u2014 auto-resume ${autoResumes + 1}/${maxAutoResumes}` };
11521
+ }
11522
+ const tail = `no progress across ${stuckCheckpoints} checkpoints; ${maxAutoResumes} auto-resume(s) exhausted`;
11523
+ return { action: "stop", kind: "stuck", reason: input.stuckReason ? `${input.stuckReason} \u2014 ${tail}` : tail };
11524
+ }
11037
11525
  const DEFAULT_LOOP_EVALUATOR_MODEL = "claude-haiku-4-5-20251001";
11038
11526
  function resolveEvaluatorModel(envValue) {
11039
11527
  if (envValue === void 0) return DEFAULT_LOOP_EVALUATOR_MODEL;
@@ -12012,6 +12500,19 @@ function writeGoalLoopState(directory, sessionId, state) {
12012
12500
  } catch {
12013
12501
  }
12014
12502
  }
12503
+ function safeBacklogPendingCount(projectRoot, sessionId) {
12504
+ try {
12505
+ return backlogOraclePending(projectRoot, sessionId).length;
12506
+ } catch {
12507
+ return 0;
12508
+ }
12509
+ }
12510
+ function updateLoopProgress(ls, pending, now) {
12511
+ const progress_history = recordProgressMarker(ls?.progress_history, { ts: now, pending });
12512
+ const madeProgress = assessLoopProgress(progress_history).stuckCheckpoints === 0;
12513
+ const prev = typeof ls?.auto_resumes === "number" && ls.auto_resumes > 0 ? ls.auto_resumes : 0;
12514
+ return { progress_history, auto_resumes: madeProgress ? 0 : prev };
12515
+ }
12015
12516
  function loopResumeMessage(directory, sessionId) {
12016
12517
  try {
12017
12518
  const ls = readLoopState(directory, sessionId);
@@ -12218,7 +12719,14 @@ function createSvampConfigChecker(directory, sessionId, getMetadata, setMetadata
12218
12719
  }
12219
12720
  }
12220
12721
  if (Array.isArray(config.session_links)) {
12221
- const links = config.session_links.filter((l) => l && typeof l.url === "string" && l.url.trim()).map((l) => ({ id: String(l.id || ""), label: String(l.label || "View"), url: String(l.url).trim(), ...l.icon ? { icon: String(l.icon) } : {} }));
12722
+ const links = config.session_links.filter((l) => l && typeof l.url === "string" && l.url.trim()).map((l) => ({
12723
+ id: String(l.id || ""),
12724
+ label: String(l.label || "View"),
12725
+ url: String(l.url).trim(),
12726
+ ...l.icon ? { icon: String(l.icon) } : {},
12727
+ // #0266: carry the service backing ({kind,name}) so the Launch Pad can decommission it.
12728
+ ...l.service && typeof l.service.name === "string" && (l.service.kind === "tunnel" || l.service.kind === "process") ? { service: { kind: l.service.kind, name: String(l.service.name) } } : {}
12729
+ }));
12222
12730
  const cur = meta.sessionLinks;
12223
12731
  if (JSON.stringify(cur || []) !== JSON.stringify(links)) {
12224
12732
  setMetadata((m) => ({ ...m, sessionLinks: links }));
@@ -12270,7 +12778,7 @@ function createSvampConfigChecker(directory, sessionId, getMetadata, setMetadata
12270
12778
  const gaveUpCfg = readLoopJson(directory, sessionId, "loop.config.json") || {};
12271
12779
  const curMax = typeof gaveUpCfg.max_iterations === "number" ? gaveUpCfg.max_iterations : iter;
12272
12780
  const nextMax = Math.max(curMax * 2, iter + 10);
12273
- const msg = s.phase === "done" ? `\u{1F501} Loop complete \u2705 \u2014 finished after ${iter} iteration${plural}.` : `\u{1F501} Loop paused \u{1F6D1} \u2014 hit its iteration limit (${iter}/${curMax}) and did NOT finish${s.gave_up_reason ? ` (${s.gave_up_reason})` : ""}. Resume & extend with: svamp session loop-resume ${sessionId} --max ${nextMax}`;
12781
+ const msg = s.phase === "done" ? `\u{1F501} Loop complete \u2705 \u2014 finished after ${iter} iteration${plural}.` : `\u{1F501} Loop paused \u{1F6D1} \u2014 did NOT finish after ${iter} iteration${plural}${s.gave_up_reason ? ` (${s.gave_up_reason})` : ""}. Resume & extend with: svamp session loop-resume ${sessionId} --max ${nextMax}`;
12274
12782
  sessionService.pushMessage(
12275
12783
  s.phase === "gave_up" ? { type: "message", message: msg, level: "warning" } : { type: "message", message: msg },
12276
12784
  "event"
@@ -12403,7 +12911,7 @@ function createSvampConfigChecker(directory, sessionId, getMetadata, setMetadata
12403
12911
  stripOldLoopHooks(directory);
12404
12912
  } catch {
12405
12913
  }
12406
- 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: [] } });
12914
+ 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 });
12407
12915
  const existingQueue = getMetadata().messageQueue || [];
12408
12916
  setMetadata((m) => ({ ...m, messageQueue: [...existingQueue, { id: randomUUID$1(), text: `/goal ${condition}`, displayText: `\u{1F3AF} Goal: ${(task || until || "").slice(0, 100)}`, createdAt: Date.now() }] }));
12409
12917
  onLoopActivated?.();
@@ -12851,8 +13359,30 @@ async function startDaemon(options) {
12851
13359
  const list = loadExposedTunnels().filter((t) => t.name !== name);
12852
13360
  saveExposedTunnels(list);
12853
13361
  }
13362
+ async function reconcileTunnelSessionLinks(sessionId, tunnelName, urls) {
13363
+ if (!sessionId || urls.size === 0) return;
13364
+ try {
13365
+ const dir = loadSessionIndex()[sessionId]?.directory;
13366
+ if (!dir) return;
13367
+ const { reconcileServiceLinks } = await import('./agentCommands-B_ozIVKT.mjs');
13368
+ const configPath = getSvampConfigPath(dir, sessionId);
13369
+ const config = readSvampConfig(configPath);
13370
+ const entries = Array.from(urls.entries());
13371
+ const incoming = entries.map(([port, url]) => ({
13372
+ url: String(url),
13373
+ label: entries.length > 1 ? `${tunnelName}:${port}` : tunnelName,
13374
+ service: { kind: "tunnel", name: tunnelName }
13375
+ }));
13376
+ if (reconcileServiceLinks(config, incoming)) {
13377
+ writeSvampConfig(configPath, config);
13378
+ logger.log(`[exposed-tunnels] Reconciled Launch Pad link(s) for ${tunnelName} \u2192 session ${sessionId}`);
13379
+ }
13380
+ } catch (err) {
13381
+ logger.log(`[exposed-tunnels] Link reconcile failed for ${tunnelName}: ${err?.message || err}`);
13382
+ }
13383
+ }
12854
13384
  async function createExposedTunnel(spec) {
12855
- const { FrpcTunnel } = await import('./frpc-Di-y4cWK.mjs');
13385
+ const { FrpcTunnel } = await import('./frpc-CsjEXBuS.mjs');
12856
13386
  const tunnel = new FrpcTunnel({
12857
13387
  name: spec.name,
12858
13388
  ports: spec.ports,
@@ -12872,7 +13402,7 @@ async function startDaemon(options) {
12872
13402
  return tunnel;
12873
13403
  }
12874
13404
  const tunnelRecreateState = /* @__PURE__ */ new Map();
12875
- const { ServeManager } = await import('./serveManager-BZtpClpC.mjs');
13405
+ const { ServeManager } = await import('./serveManager-Di3xrvG2.mjs');
12876
13406
  const serveManager = new ServeManager(SVAMP_HOME, (msg) => logger.log(`[SERVE] ${msg}`), hyphaServerUrl);
12877
13407
  ensureAutoInstalledSkills(logger).catch(() => {
12878
13408
  });
@@ -13077,6 +13607,22 @@ ${v.guidance ? v.guidance + "\n" : ""}Criteria: ${v.criteria || "(none)"}
13077
13607
  return taken;
13078
13608
  };
13079
13609
  let machineServiceRef = null;
13610
+ const idleTriggerTracker = new IdleTriggerTracker();
13611
+ const routeSessionEventForIdle = (update, dir) => {
13612
+ try {
13613
+ if (!update || update.type !== "activity") return;
13614
+ if (!idleTriggerTracker.observe(update, Date.now())) return;
13615
+ let root;
13616
+ try {
13617
+ root = resolveProjectRoot(dir);
13618
+ } catch {
13619
+ return;
13620
+ }
13621
+ fireIdleWorkflows(root, String(update.sessionId), { log: (m) => logger.log(m) });
13622
+ } catch (e) {
13623
+ logger.log(`[workflow] idle trigger error: ${e?.message || e}`);
13624
+ }
13625
+ };
13080
13626
  const spawnSession = async (options2) => {
13081
13627
  logger.log("Spawning session:", JSON.stringify(options2));
13082
13628
  const { directory, approvedNewDirectoryCreation = true, resumeSessionId } = options2;
@@ -13536,6 +14082,7 @@ ${parts.join("\n")}`);
13536
14082
  );
13537
14083
  const ledger = ls.ledger;
13538
14084
  const budget = ls.budget;
14085
+ const progress_history = ls.progress_history;
13539
14086
  if (result.action === "rekick") {
13540
14087
  const cond = ls.task || "";
13541
14088
  writeGoalLoopState(directory, sessionId, {
@@ -13550,17 +14097,55 @@ ${parts.join("\n")}`);
13550
14097
  budget,
13551
14098
  started_at: startedAt,
13552
14099
  holds: holds + 1,
13553
- ledger
14100
+ ledger,
14101
+ progress_history,
14102
+ auto_resumes: ls.auto_resumes
13554
14103
  });
13555
14104
  const guidance = result.guidance.replace(/\s+/g, " ").slice(0, 1200);
13556
14105
  sessionService.pushMessage({ type: "message", message: `\u{1F501} Loop not done \u2014 independent review: ${result.reason}`, level: "warning" }, "event");
13557
14106
  enqueueLoopMessage(`/goal ${cond} \u2014 INDEPENDENT REVIEW FEEDBACK (resolve before declaring done): ${guidance}`, "\u{1F501} Re-verifying loop");
13558
14107
  if (!trackedSession.stopped) setTimeout(() => processMessageQueueRef?.(), 200);
13559
14108
  } else if (result.action === "gave_up") {
13560
- writeGoalLoopState(directory, sessionId, { active: false, phase: "gave_up", completed_at: Date.now(), gave_up_reason: result.reason, holds, ledger });
13561
- checkSvampConfig?.();
14109
+ const decision = decideLoopContinuation({
14110
+ costCeilingBreached: false,
14111
+ progressHistory: progress_history || [],
14112
+ stuckLimit: resolveStuckLimit(process.env.SVAMP_LOOP_STUCK_CHECKPOINTS),
14113
+ autoResumes: typeof ls.auto_resumes === "number" ? ls.auto_resumes : 0,
14114
+ maxAutoResumes: resolveMaxAutoResumes(process.env.SVAMP_LOOP_MAX_AUTORESUMES),
14115
+ stuckReason: result.reason
14116
+ });
14117
+ if (decision.action === "continue") {
14118
+ const cond = ls.task || "";
14119
+ const nextAuto = (typeof ls.auto_resumes === "number" ? ls.auto_resumes : 0) + (decision.consumeAutoResume ? 1 : 0);
14120
+ writeGoalLoopState(directory, sessionId, {
14121
+ active: true,
14122
+ phase: "continue",
14123
+ iteration: ls.iteration || 0,
14124
+ task: ls.task,
14125
+ goal_task: ls.goal_task,
14126
+ until: ls.until,
14127
+ oracle: oracleCmd,
14128
+ max_iterations: ls.max_iterations,
14129
+ budget,
14130
+ started_at: startedAt,
14131
+ holds,
14132
+ ledger,
14133
+ progress_history,
14134
+ auto_resumes: nextAuto
14135
+ });
14136
+ const label = decision.kind === "autoresume" ? "\u{1F501} Auto-resuming stalled loop" : "\u{1F501} Loop still progressing";
14137
+ sessionService.pushMessage({ type: "message", message: `${label} \u2014 ${decision.reason}` }, "event");
14138
+ logger.log(`[Session ${sessionId}] [loop-persist] ${decision.kind}: ${decision.reason}`);
14139
+ const push = decision.kind === "autoresume" ? `/goal ${cond} \u2014 AUTO-RESUME after an apparent stall (${decision.reason}). Re-verify the remaining work and PUSH IT TO COMPLETION; do not stop early. Prior reviewer note: ${result.reason}` : `/goal ${cond} \u2014 still making progress; KEEP GOING until genuinely done. Prior reviewer note: ${result.reason}`;
14140
+ enqueueLoopMessage(push, label);
14141
+ if (!trackedSession.stopped) setTimeout(() => processMessageQueueRef?.(), 200);
14142
+ } else {
14143
+ writeGoalLoopState(directory, sessionId, { active: false, phase: "gave_up", completed_at: Date.now(), gave_up_reason: decision.reason, holds, ledger, progress_history, auto_resumes: ls.auto_resumes });
14144
+ logger.log(`[Session ${sessionId}] [loop-persist] stuck-stop: ${decision.reason}`);
14145
+ checkSvampConfig?.();
14146
+ }
13562
14147
  } else {
13563
- writeGoalLoopState(directory, sessionId, { active: false, phase: "done", completed_at: Date.now(), holds, ledger });
14148
+ writeGoalLoopState(directory, sessionId, { active: false, phase: "done", completed_at: Date.now(), holds, ledger, progress_history, auto_resumes: ls.auto_resumes });
13564
14149
  checkSvampConfig?.();
13565
14150
  }
13566
14151
  } catch (e) {
@@ -14029,22 +14614,28 @@ ${parts.join("\n")}`);
14029
14614
  });
14030
14615
  ls.ledger = ledger;
14031
14616
  const startedAt = typeof ls.started_at === "number" ? ls.started_at : typeof ls.resumed_at === "number" ? ls.resumed_at : void 0;
14032
- const budgetCheck = process.env.SVAMP_LOOP_BUDGET === "0" ? { exceeded: false } : checkLoopBudget(ledger, { max_iterations: ls.max_iterations, ...ls.budget || {} }, now, startedAt);
14033
- if (budgetCheck.exceeded) {
14617
+ const budgetCheck = process.env.SVAMP_LOOP_BUDGET === "0" ? { exceeded: false, kind: void 0 } : checkLoopBudget(ledger, { max_iterations: ls.max_iterations, ...ls.budget || {} }, now, startedAt);
14618
+ const pending = safeBacklogPendingCount(resolveProjectRoot(directory), sessionId);
14619
+ const prog = updateLoopProgress(ls, pending, now);
14620
+ ls.progress_history = prog.progress_history;
14621
+ ls.auto_resumes = prog.auto_resumes;
14622
+ if (budgetCheck.exceeded && isCostCeiling(budgetCheck.kind)) {
14034
14623
  writeGoalLoopState(directory, sessionId, {
14035
14624
  ...ls,
14036
14625
  active: false,
14037
14626
  phase: "gave_up",
14038
14627
  completed_at: now,
14039
14628
  gave_up_reason: `resource budget exhausted \u2014 ${budgetCheck.reason}`,
14040
- ledger
14629
+ ledger,
14630
+ progress_history: prog.progress_history,
14631
+ auto_resumes: prog.auto_resumes
14041
14632
  });
14042
- 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");
14633
+ sessionService.pushMessage({ type: "message", message: `\u{1F6D1} Loop stopped \u2014 ${budgetCheck.reason} (hard cost ceiling \u2014 will not auto-resume). Resume & extend with: svamp session loop-resume ${sessionId} --max <N>`, level: "warning" }, "event");
14043
14634
  checkSvampConfig?.();
14044
14635
  } else if (process.env.SVAMP_LOOP_VERIFY === "0") {
14045
- writeGoalLoopState(directory, sessionId, { ...ls, active: false, phase: "done", completed_at: now, ledger });
14636
+ writeGoalLoopState(directory, sessionId, { ...ls, active: false, phase: "done", completed_at: now, ledger, progress_history: prog.progress_history, auto_resumes: prog.auto_resumes });
14046
14637
  } else {
14047
- writeGoalLoopState(directory, sessionId, { ...ls, ledger });
14638
+ writeGoalLoopState(directory, sessionId, { ...ls, ledger, progress_history: prog.progress_history, auto_resumes: prog.auto_resumes });
14048
14639
  void verifyGoalCompletion(ls);
14049
14640
  }
14050
14641
  }
@@ -14897,11 +15488,11 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
14897
15488
  });
14898
15489
  },
14899
15490
  onIssue: async (params) => {
14900
- const { issueRpc } = await import('./rpc-BncvIQMO.mjs');
15491
+ const { issueRpc } = await import('./rpc-C1DI7HfZ.mjs');
14901
15492
  return issueRpc(params?.cwd || directory, params || {}, { notifySession: notifyIssueOwner, rekickLoopOwner });
14902
15493
  },
14903
15494
  onWorkflow: async (params) => {
14904
- const { workflowRpc } = await import('./rpc-DbY7NUhj.mjs');
15495
+ const { workflowRpc } = await import('./rpc-BGFgegj5.mjs');
14905
15496
  return workflowRpc(params?.cwd || directory, params || {});
14906
15497
  },
14907
15498
  onRipgrep: async (args, cwd) => {
@@ -14980,7 +15571,10 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
14980
15571
  return { success: !!tree, tree };
14981
15572
  }
14982
15573
  },
14983
- { messagesDir: getSessionDir(directory, sessionId), onSessionEvent: (update) => machineServiceRef?.notifySessionEvent(update) }
15574
+ { messagesDir: getSessionDir(directory, sessionId), onSessionEvent: (update) => {
15575
+ machineServiceRef?.notifySessionEvent(update);
15576
+ routeSessionEventForIdle(update, directory);
15577
+ } }
14984
15578
  );
14985
15579
  const svampConfig = createSvampConfigChecker(
14986
15580
  directory,
@@ -15434,11 +16028,11 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
15434
16028
  });
15435
16029
  },
15436
16030
  onIssue: async (params) => {
15437
- const { issueRpc } = await import('./rpc-BncvIQMO.mjs');
16031
+ const { issueRpc } = await import('./rpc-C1DI7HfZ.mjs');
15438
16032
  return issueRpc(params?.cwd || directory, params || {}, { notifySession: notifyIssueOwner, rekickLoopOwner });
15439
16033
  },
15440
16034
  onWorkflow: async (params) => {
15441
- const { workflowRpc } = await import('./rpc-DbY7NUhj.mjs');
16035
+ const { workflowRpc } = await import('./rpc-BGFgegj5.mjs');
15442
16036
  return workflowRpc(params?.cwd || directory, params || {});
15443
16037
  },
15444
16038
  onRipgrep: async (args, cwd) => {
@@ -15517,7 +16111,10 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
15517
16111
  return { success: !!tree, tree };
15518
16112
  }
15519
16113
  },
15520
- { messagesDir: getSessionDir(directory, sessionId), onSessionEvent: (update) => machineServiceRef?.notifySessionEvent(update) }
16114
+ { messagesDir: getSessionDir(directory, sessionId), onSessionEvent: (update) => {
16115
+ machineServiceRef?.notifySessionEvent(update);
16116
+ routeSessionEventForIdle(update, directory);
16117
+ } }
15521
16118
  );
15522
16119
  let insideOnTurnEnd = false;
15523
16120
  const svampConfigChecker = createSvampConfigChecker(
@@ -15648,10 +16245,14 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
15648
16245
  return { ok: pending.length === 0, output: pending.length ? `${pending.length} pending: ${pending.map((i) => "#" + i.id).join(" ")}` : "No pending issues." };
15649
16246
  };
15650
16247
  try {
15651
- const budgetCheck = process.env.SVAMP_LOOP_BUDGET === "0" ? { exceeded: false } : checkLoopBudget(ledger, { max_iterations: ls.max_iterations, ...ls.budget || {} }, now, startedAt);
15652
- if (budgetCheck.exceeded) {
16248
+ const budgetCheck = process.env.SVAMP_LOOP_BUDGET === "0" ? { exceeded: false, kind: void 0 } : checkLoopBudget(ledger, { max_iterations: ls.max_iterations, ...ls.budget || {} }, now, startedAt);
16249
+ const acpPending = safeBacklogPendingCount(projectRoot, sessionId);
16250
+ const acpProg = updateLoopProgress(ls, acpPending, now);
16251
+ ls.progress_history = acpProg.progress_history;
16252
+ ls.auto_resumes = acpProg.auto_resumes;
16253
+ if (budgetCheck.exceeded && isCostCeiling(budgetCheck.kind)) {
15653
16254
  writeGoalLoopState(directory, sessionId, { ...ls, active: false, phase: "gave_up", completed_at: now, gave_up_reason: `resource budget exhausted \u2014 ${budgetCheck.reason}`, ledger });
15654
- 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");
16255
+ sessionService.pushMessage({ type: "message", message: `\u{1F6D1} Loop stopped \u2014 ${budgetCheck.reason} (hard cost ceiling \u2014 will not auto-resume). Resume & extend with: svamp session loop-resume ${sessionId} --max <N>`, level: "warning" }, "event");
15655
16256
  checkSvampConfig?.();
15656
16257
  return;
15657
16258
  }
@@ -15683,8 +16284,26 @@ ${oracle.output.trim().slice(0, 500)}`, "\u{1F501} Continuing loop");
15683
16284
  sessionService.pushMessage({ type: "message", message: `\u{1F501} Loop not done \u2014 independent review: ${result.reason}`, level: "warning" }, "event");
15684
16285
  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");
15685
16286
  } else if (result.action === "gave_up") {
15686
- writeGoalLoopState(directory, sessionId, { ...ls, active: false, phase: "gave_up", completed_at: Date.now(), gave_up_reason: result.reason, holds, ledger });
15687
- checkSvampConfig?.();
16287
+ const decision = decideLoopContinuation({
16288
+ costCeilingBreached: false,
16289
+ progressHistory: ls.progress_history || [],
16290
+ stuckLimit: resolveStuckLimit(process.env.SVAMP_LOOP_STUCK_CHECKPOINTS),
16291
+ autoResumes: typeof ls.auto_resumes === "number" ? ls.auto_resumes : 0,
16292
+ maxAutoResumes: resolveMaxAutoResumes(process.env.SVAMP_LOOP_MAX_AUTORESUMES),
16293
+ stuckReason: result.reason
16294
+ });
16295
+ if (decision.action === "continue") {
16296
+ const nextAuto = (typeof ls.auto_resumes === "number" ? ls.auto_resumes : 0) + (decision.consumeAutoResume ? 1 : 0);
16297
+ writeGoalLoopState(directory, sessionId, { ...ls, active: true, phase: "continue", holds, started_at: startedAt, ledger, auto_resumes: nextAuto });
16298
+ const label = decision.kind === "autoresume" ? "\u{1F501} Auto-resuming stalled loop" : "\u{1F501} Loop still progressing";
16299
+ sessionService.pushMessage({ type: "message", message: `${label} \u2014 ${decision.reason}` }, "event");
16300
+ logger.log(`[Session ${sessionId}] [loop-persist] ${decision.kind}: ${decision.reason}`);
16301
+ dispatchAcpPrompt(`Continue toward the goal: ${ls.goal_task || ls.until || "the task"}. ${decision.kind === "autoresume" ? `AUTO-RESUME after an apparent stall (${decision.reason}) \u2014 re-verify the remaining work and PUSH IT TO COMPLETION.` : "Still making progress \u2014 KEEP GOING until genuinely done."} Prior reviewer note: ${result.reason}`, label);
16302
+ } else {
16303
+ writeGoalLoopState(directory, sessionId, { ...ls, active: false, phase: "gave_up", completed_at: Date.now(), gave_up_reason: decision.reason, holds, ledger });
16304
+ logger.log(`[Session ${sessionId}] [loop-persist] stuck-stop: ${decision.reason}`);
16305
+ checkSvampConfig?.();
16306
+ }
15688
16307
  } else {
15689
16308
  writeGoalLoopState(directory, sessionId, { ...ls, active: false, phase: "done", completed_at: Date.now(), holds, ledger });
15690
16309
  checkSvampConfig?.();
@@ -16082,6 +16701,7 @@ ${oracle.output.trim().slice(0, 500)}`, "\u{1F501} Continuing loop");
16082
16701
  const tunnel = await createExposedTunnel(spec);
16083
16702
  tunnels.set(spec.name, tunnel);
16084
16703
  logger.log(`[exposed-tunnels] Restored: ${spec.name} \u2192 ports ${spec.ports.join(",")}`);
16704
+ if (spec.sessionId) await reconcileTunnelSessionLinks(spec.sessionId, spec.name, tunnel.getUrls());
16085
16705
  } catch (err) {
16086
16706
  logger.log(`[exposed-tunnels] Failed to restore ${spec.name}: ${err.message}`);
16087
16707
  }
@@ -16370,7 +16990,7 @@ ${oracle.output.trim().slice(0, 500)}`, "\u{1F501} Continuing loop");
16370
16990
  const PING_TIMEOUT_MS = 15e3;
16371
16991
  const POST_RECONNECT_GRACE_MS = 2e4;
16372
16992
  const RECONNECT_JITTER_MS = 2500;
16373
- const { WorkflowScheduler } = await import('./scheduler-BkXOKjHn.mjs');
16993
+ const { WorkflowScheduler } = await import('./scheduler-Csp21zzg.mjs');
16374
16994
  const workflowScheduler = new WorkflowScheduler({
16375
16995
  projectRoots: () => {
16376
16996
  const dirs = /* @__PURE__ */ new Set();
@@ -16954,4 +17574,4 @@ var run = /*#__PURE__*/Object.freeze({
16954
17574
  writeStopMarker: writeStopMarker
16955
17575
  });
16956
17576
 
16957
- export { buildMachineShareUrl as $, shortId as A, loadMachineContext as B, buildMachineInstructions as C, machineToolsForRole as D, buildMachineTools as E, parseFrontmatter as F, getSkillsServer as G, getSkillsWorkspaceName as H, getSkillsCollectionName as I, fetchWithTimeout as J, searchSkills as K, SKILLS_DIR as L, getSkillInfo as M, downloadSkillFile as N, listSkillFiles as O, resolveModel as P, formatHandle as Q, READ_ONLY_TOOLS as R, ServeAuth as S, normalizeAllowedUser as T, loadSecurityContextConfig as U, resolveSecurityContext as V, buildSecurityContextFromFlags as W, mergeSecurityContexts as X, buildSessionShareUrl as Y, computeOutboundHop as Z, registerAwaitingReply as _, createSessionStore as a, parseHandle as a0, handleMatchesMetadata as a1, describeMisconfiguration as a2, buildMachineDeps as a3, applyClaudeProxyEnv as a4, composeSessionId as a5, generateFriendlyName as a6, generateHookSettings as a7, claudeAuth as a8, projectInfo as a9, DefaultTransport$1 as aa, acpBackend as ab, acpAgentConfig as ac, codexMcpBackend as ad, GeminiTransport$1 as ae, instanceConfig as af, api as ag, run as ah, stopDaemon as b, connectToHypha as c, daemonStatus as d, clearStopMarker as e, stopMarkerExists as f, getHyphaServerUrl$1 as g, getFrpsSubdomainHost as h, getFrpsServerPort as i, getFrpsServerAddr as j, getHyphaServerUrl as k, hasCookieToken as l, resolveProjectRoot as m, getIssue as n, resumeIssue as o, pauseIssue as p, addComment as q, registerMachineService as r, startDaemon as s, addIssue as t, updateIssue as u, listIssues as v, searchIssues as w, isVisibleTo as x, cronMatches as y, summarize as z };
17577
+ export { downloadSkillFile as $, listRuns as A, getWorkflow as B, runWorkflow as C, setWorkflowEnabled as D, removeWorkflow as E, saveWorkflow as F, rawWorkflow as G, listWorkflows as H, isWorkflowEnabled as I, workflowCrons as J, cronMatches as K, summarize as L, workflowSteps as M, loadMachineContext as N, buildMachineInstructions as O, machineToolsForRole as P, buildMachineTools as Q, READ_ONLY_TOOLS as R, ServeAuth as S, parseFrontmatter as T, getSkillsServer as U, getSkillsWorkspaceName as V, getSkillsCollectionName as W, fetchWithTimeout as X, searchSkills as Y, SKILLS_DIR as Z, getSkillInfo as _, createSessionStore as a, listSkillFiles as a0, resolveModel as a1, formatHandle as a2, normalizeAllowedUser as a3, loadSecurityContextConfig as a4, resolveSecurityContext as a5, buildSecurityContextFromFlags as a6, mergeSecurityContexts as a7, buildSessionShareUrl as a8, computeOutboundHop as a9, registerAwaitingReply as aa, buildMachineShareUrl as ab, parseHandle as ac, handleMatchesMetadata as ad, describeMisconfiguration as ae, buildMachineDeps as af, applyClaudeProxyEnv as ag, composeSessionId as ah, generateFriendlyName as ai, generateHookSettings as aj, claudeAuth as ak, projectInfo as al, DefaultTransport$1 as am, acpBackend as an, acpAgentConfig as ao, codexMcpBackend as ap, GeminiTransport$1 as aq, instanceConfig as ar, api as as, run as at, stopDaemon as b, connectToHypha as c, daemonStatus as d, clearStopMarker as e, stopMarkerExists as f, getHyphaServerUrl$1 as g, getFrpsSubdomainHost as h, getFrpsServerPort as i, getFrpsServerAddr as j, shortId as k, getHyphaServerUrl as l, hasCookieToken as m, resolveProjectRoot as n, getIssue as o, resumeIssue as p, pauseIssue as q, registerMachineService as r, startDaemon as s, addComment as t, updateIssue as u, addIssue as v, listIssues as w, searchIssues as x, isVisibleTo as y, getRun as z };