svamp-cli 0.2.246 → 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.
- package/dist/{agentCommands-wOdV4Do1.mjs → agentCommands-B_ozIVKT.mjs} +28 -11
- package/dist/{auth-Bg0Z_bP-.mjs → auth-BLDmT7XN.mjs} +2 -1
- package/dist/cli.mjs +83 -68
- package/dist/{commands-BhLGBhBS.mjs → commands-C3TQOYUo.mjs} +2 -1
- package/dist/{commands-Z8Ki8iA9.mjs → commands-CL8YI8k5.mjs} +2 -1
- package/dist/{commands-Bi_LZXPe.mjs → commands-CRMQUQZG.mjs} +2 -1
- package/dist/{commands-CudvGpEz.mjs → commands-Cs2fNd-_.mjs} +7 -7
- package/dist/{commands-CniwP5Tm.mjs → commands-DS4i8ohA.mjs} +10 -6
- package/dist/{commands-CkduYG4a.mjs → commands-Djvi1cZ2.mjs} +3 -2
- package/dist/{commands-D9khoYtI.mjs → commands-DwhA-iV3.mjs} +3 -2
- package/dist/{fleet-DCMPBC7b.mjs → fleet-CJZOVjOY.mjs} +13 -4
- package/dist/{frpc-nvGz8fWs.mjs → frpc-CsjEXBuS.mjs} +2 -1
- package/dist/{headlessCli-d6XjdG6n.mjs → headlessCli-qBontdlf.mjs} +3 -2
- package/dist/index.mjs +2 -1
- package/dist/{package-CUTX-CB-.mjs → package-B7z6GsNX.mjs} +2 -2
- package/dist/{rpc-CDI7i151.mjs → rpc-BGFgegj5.mjs} +1 -3
- package/dist/{rpc-s_Hibfpl.mjs → rpc-C1DI7HfZ.mjs} +2 -1
- package/dist/{run-DRR6psuh.mjs → run-C61rsl7r.mjs} +2 -1
- package/dist/{run-BPZ_1RCI.mjs → run-CHIppB2r.mjs} +739 -35
- package/dist/{scheduler-E1Bw4HUY.mjs → scheduler-Csp21zzg.mjs} +1 -3
- package/dist/{serveCommands-Af_7qqzA.mjs → serveCommands-BYHk72ZO.mjs} +5 -5
- package/dist/{serveManager-DGRpRAFM.mjs → serveManager-Di3xrvG2.mjs} +3 -2
- package/dist/{sideband-C-0cCNeL.mjs → sideband-Cs-kNLye.mjs} +2 -1
- package/package.json +2 -2
- package/dist/runStore-DKC_bMSi.mjs +0 -239
- 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-
|
|
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-
|
|
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-
|
|
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;
|
|
@@ -11852,6 +12340,30 @@ const SVAMP_HOME = process.env.SVAMP_HOME || join$1(os$1.homedir(), ".svamp");
|
|
|
11852
12340
|
const DAEMON_STATE_FILE = join$1(SVAMP_HOME, "daemon.state.json");
|
|
11853
12341
|
const DAEMON_LOCK_FILE = join$1(SVAMP_HOME, "daemon.lock");
|
|
11854
12342
|
const DAEMON_STOP_MARKER_FILE = join$1(SVAMP_HOME, "daemon.stop");
|
|
12343
|
+
const PENDING_RESTART_FILE = join$1(SVAMP_HOME, "pending-restart.json");
|
|
12344
|
+
function writePendingRestart(p) {
|
|
12345
|
+
try {
|
|
12346
|
+
if (!existsSync$1(SVAMP_HOME)) mkdirSync$1(SVAMP_HOME, { recursive: true });
|
|
12347
|
+
const rec = { scheduledAt: Date.now(), ...p };
|
|
12348
|
+
writeFileSync$1(PENDING_RESTART_FILE, JSON.stringify(rec), "utf-8");
|
|
12349
|
+
} catch {
|
|
12350
|
+
}
|
|
12351
|
+
}
|
|
12352
|
+
function readPendingRestart() {
|
|
12353
|
+
try {
|
|
12354
|
+
if (!existsSync$1(PENDING_RESTART_FILE)) return null;
|
|
12355
|
+
const rec = JSON.parse(readFileSync$1(PENDING_RESTART_FILE, "utf-8"));
|
|
12356
|
+
return rec && typeof rec.scheduledAt === "number" ? rec : null;
|
|
12357
|
+
} catch {
|
|
12358
|
+
return null;
|
|
12359
|
+
}
|
|
12360
|
+
}
|
|
12361
|
+
function clearPendingRestart() {
|
|
12362
|
+
try {
|
|
12363
|
+
if (existsSync$1(PENDING_RESTART_FILE)) unlinkSync$1(PENDING_RESTART_FILE);
|
|
12364
|
+
} catch {
|
|
12365
|
+
}
|
|
12366
|
+
}
|
|
11855
12367
|
function writeStopMarker(reason) {
|
|
11856
12368
|
try {
|
|
11857
12369
|
writeFileSync$1(DAEMON_STOP_MARKER_FILE, `${(/* @__PURE__ */ new Date()).toISOString()} ${reason}
|
|
@@ -11988,6 +12500,19 @@ function writeGoalLoopState(directory, sessionId, state) {
|
|
|
11988
12500
|
} catch {
|
|
11989
12501
|
}
|
|
11990
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
|
+
}
|
|
11991
12516
|
function loopResumeMessage(directory, sessionId) {
|
|
11992
12517
|
try {
|
|
11993
12518
|
const ls = readLoopState(directory, sessionId);
|
|
@@ -12194,7 +12719,14 @@ function createSvampConfigChecker(directory, sessionId, getMetadata, setMetadata
|
|
|
12194
12719
|
}
|
|
12195
12720
|
}
|
|
12196
12721
|
if (Array.isArray(config.session_links)) {
|
|
12197
|
-
const links = config.session_links.filter((l) => l && typeof l.url === "string" && l.url.trim()).map((l) => ({
|
|
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
|
+
}));
|
|
12198
12730
|
const cur = meta.sessionLinks;
|
|
12199
12731
|
if (JSON.stringify(cur || []) !== JSON.stringify(links)) {
|
|
12200
12732
|
setMetadata((m) => ({ ...m, sessionLinks: links }));
|
|
@@ -12246,7 +12778,7 @@ function createSvampConfigChecker(directory, sessionId, getMetadata, setMetadata
|
|
|
12246
12778
|
const gaveUpCfg = readLoopJson(directory, sessionId, "loop.config.json") || {};
|
|
12247
12779
|
const curMax = typeof gaveUpCfg.max_iterations === "number" ? gaveUpCfg.max_iterations : iter;
|
|
12248
12780
|
const nextMax = Math.max(curMax * 2, iter + 10);
|
|
12249
|
-
const msg = s.phase === "done" ? `\u{1F501} Loop complete \u2705 \u2014 finished after ${iter} iteration${plural}.` : `\u{1F501} Loop paused \u{1F6D1} \u2014
|
|
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}`;
|
|
12250
12782
|
sessionService.pushMessage(
|
|
12251
12783
|
s.phase === "gave_up" ? { type: "message", message: msg, level: "warning" } : { type: "message", message: msg },
|
|
12252
12784
|
"event"
|
|
@@ -12379,7 +12911,7 @@ function createSvampConfigChecker(directory, sessionId, getMetadata, setMetadata
|
|
|
12379
12911
|
stripOldLoopHooks(directory);
|
|
12380
12912
|
} catch {
|
|
12381
12913
|
}
|
|
12382
|
-
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 });
|
|
12383
12915
|
const existingQueue = getMetadata().messageQueue || [];
|
|
12384
12916
|
setMetadata((m) => ({ ...m, messageQueue: [...existingQueue, { id: randomUUID$1(), text: `/goal ${condition}`, displayText: `\u{1F3AF} Goal: ${(task || until || "").slice(0, 100)}`, createdAt: Date.now() }] }));
|
|
12385
12917
|
onLoopActivated?.();
|
|
@@ -12827,8 +13359,30 @@ async function startDaemon(options) {
|
|
|
12827
13359
|
const list = loadExposedTunnels().filter((t) => t.name !== name);
|
|
12828
13360
|
saveExposedTunnels(list);
|
|
12829
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
|
+
}
|
|
12830
13384
|
async function createExposedTunnel(spec) {
|
|
12831
|
-
const { FrpcTunnel } = await import('./frpc-
|
|
13385
|
+
const { FrpcTunnel } = await import('./frpc-CsjEXBuS.mjs');
|
|
12832
13386
|
const tunnel = new FrpcTunnel({
|
|
12833
13387
|
name: spec.name,
|
|
12834
13388
|
ports: spec.ports,
|
|
@@ -12848,7 +13402,7 @@ async function startDaemon(options) {
|
|
|
12848
13402
|
return tunnel;
|
|
12849
13403
|
}
|
|
12850
13404
|
const tunnelRecreateState = /* @__PURE__ */ new Map();
|
|
12851
|
-
const { ServeManager } = await import('./serveManager-
|
|
13405
|
+
const { ServeManager } = await import('./serveManager-Di3xrvG2.mjs');
|
|
12852
13406
|
const serveManager = new ServeManager(SVAMP_HOME, (msg) => logger.log(`[SERVE] ${msg}`), hyphaServerUrl);
|
|
12853
13407
|
ensureAutoInstalledSkills(logger).catch(() => {
|
|
12854
13408
|
});
|
|
@@ -13053,6 +13607,22 @@ ${v.guidance ? v.guidance + "\n" : ""}Criteria: ${v.criteria || "(none)"}
|
|
|
13053
13607
|
return taken;
|
|
13054
13608
|
};
|
|
13055
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
|
+
};
|
|
13056
13626
|
const spawnSession = async (options2) => {
|
|
13057
13627
|
logger.log("Spawning session:", JSON.stringify(options2));
|
|
13058
13628
|
const { directory, approvedNewDirectoryCreation = true, resumeSessionId } = options2;
|
|
@@ -13512,6 +14082,7 @@ ${parts.join("\n")}`);
|
|
|
13512
14082
|
);
|
|
13513
14083
|
const ledger = ls.ledger;
|
|
13514
14084
|
const budget = ls.budget;
|
|
14085
|
+
const progress_history = ls.progress_history;
|
|
13515
14086
|
if (result.action === "rekick") {
|
|
13516
14087
|
const cond = ls.task || "";
|
|
13517
14088
|
writeGoalLoopState(directory, sessionId, {
|
|
@@ -13526,17 +14097,55 @@ ${parts.join("\n")}`);
|
|
|
13526
14097
|
budget,
|
|
13527
14098
|
started_at: startedAt,
|
|
13528
14099
|
holds: holds + 1,
|
|
13529
|
-
ledger
|
|
14100
|
+
ledger,
|
|
14101
|
+
progress_history,
|
|
14102
|
+
auto_resumes: ls.auto_resumes
|
|
13530
14103
|
});
|
|
13531
14104
|
const guidance = result.guidance.replace(/\s+/g, " ").slice(0, 1200);
|
|
13532
14105
|
sessionService.pushMessage({ type: "message", message: `\u{1F501} Loop not done \u2014 independent review: ${result.reason}`, level: "warning" }, "event");
|
|
13533
14106
|
enqueueLoopMessage(`/goal ${cond} \u2014 INDEPENDENT REVIEW FEEDBACK (resolve before declaring done): ${guidance}`, "\u{1F501} Re-verifying loop");
|
|
13534
14107
|
if (!trackedSession.stopped) setTimeout(() => processMessageQueueRef?.(), 200);
|
|
13535
14108
|
} else if (result.action === "gave_up") {
|
|
13536
|
-
|
|
13537
|
-
|
|
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
|
+
}
|
|
13538
14147
|
} else {
|
|
13539
|
-
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 });
|
|
13540
14149
|
checkSvampConfig?.();
|
|
13541
14150
|
}
|
|
13542
14151
|
} catch (e) {
|
|
@@ -14005,22 +14614,28 @@ ${parts.join("\n")}`);
|
|
|
14005
14614
|
});
|
|
14006
14615
|
ls.ledger = ledger;
|
|
14007
14616
|
const startedAt = typeof ls.started_at === "number" ? ls.started_at : typeof ls.resumed_at === "number" ? ls.resumed_at : void 0;
|
|
14008
|
-
const budgetCheck = process.env.SVAMP_LOOP_BUDGET === "0" ? { exceeded: false } : checkLoopBudget(ledger, { max_iterations: ls.max_iterations, ...ls.budget || {} }, now, startedAt);
|
|
14009
|
-
|
|
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)) {
|
|
14010
14623
|
writeGoalLoopState(directory, sessionId, {
|
|
14011
14624
|
...ls,
|
|
14012
14625
|
active: false,
|
|
14013
14626
|
phase: "gave_up",
|
|
14014
14627
|
completed_at: now,
|
|
14015
14628
|
gave_up_reason: `resource budget exhausted \u2014 ${budgetCheck.reason}`,
|
|
14016
|
-
ledger
|
|
14629
|
+
ledger,
|
|
14630
|
+
progress_history: prog.progress_history,
|
|
14631
|
+
auto_resumes: prog.auto_resumes
|
|
14017
14632
|
});
|
|
14018
|
-
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");
|
|
14019
14634
|
checkSvampConfig?.();
|
|
14020
14635
|
} else if (process.env.SVAMP_LOOP_VERIFY === "0") {
|
|
14021
|
-
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 });
|
|
14022
14637
|
} else {
|
|
14023
|
-
writeGoalLoopState(directory, sessionId, { ...ls, ledger });
|
|
14638
|
+
writeGoalLoopState(directory, sessionId, { ...ls, ledger, progress_history: prog.progress_history, auto_resumes: prog.auto_resumes });
|
|
14024
14639
|
void verifyGoalCompletion(ls);
|
|
14025
14640
|
}
|
|
14026
14641
|
}
|
|
@@ -14873,11 +15488,11 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
|
|
|
14873
15488
|
});
|
|
14874
15489
|
},
|
|
14875
15490
|
onIssue: async (params) => {
|
|
14876
|
-
const { issueRpc } = await import('./rpc-
|
|
15491
|
+
const { issueRpc } = await import('./rpc-C1DI7HfZ.mjs');
|
|
14877
15492
|
return issueRpc(params?.cwd || directory, params || {}, { notifySession: notifyIssueOwner, rekickLoopOwner });
|
|
14878
15493
|
},
|
|
14879
15494
|
onWorkflow: async (params) => {
|
|
14880
|
-
const { workflowRpc } = await import('./rpc-
|
|
15495
|
+
const { workflowRpc } = await import('./rpc-BGFgegj5.mjs');
|
|
14881
15496
|
return workflowRpc(params?.cwd || directory, params || {});
|
|
14882
15497
|
},
|
|
14883
15498
|
onRipgrep: async (args, cwd) => {
|
|
@@ -14956,7 +15571,10 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
|
|
|
14956
15571
|
return { success: !!tree, tree };
|
|
14957
15572
|
}
|
|
14958
15573
|
},
|
|
14959
|
-
{ messagesDir: getSessionDir(directory, sessionId), onSessionEvent: (update) =>
|
|
15574
|
+
{ messagesDir: getSessionDir(directory, sessionId), onSessionEvent: (update) => {
|
|
15575
|
+
machineServiceRef?.notifySessionEvent(update);
|
|
15576
|
+
routeSessionEventForIdle(update, directory);
|
|
15577
|
+
} }
|
|
14960
15578
|
);
|
|
14961
15579
|
const svampConfig = createSvampConfigChecker(
|
|
14962
15580
|
directory,
|
|
@@ -15410,11 +16028,11 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
|
|
|
15410
16028
|
});
|
|
15411
16029
|
},
|
|
15412
16030
|
onIssue: async (params) => {
|
|
15413
|
-
const { issueRpc } = await import('./rpc-
|
|
16031
|
+
const { issueRpc } = await import('./rpc-C1DI7HfZ.mjs');
|
|
15414
16032
|
return issueRpc(params?.cwd || directory, params || {}, { notifySession: notifyIssueOwner, rekickLoopOwner });
|
|
15415
16033
|
},
|
|
15416
16034
|
onWorkflow: async (params) => {
|
|
15417
|
-
const { workflowRpc } = await import('./rpc-
|
|
16035
|
+
const { workflowRpc } = await import('./rpc-BGFgegj5.mjs');
|
|
15418
16036
|
return workflowRpc(params?.cwd || directory, params || {});
|
|
15419
16037
|
},
|
|
15420
16038
|
onRipgrep: async (args, cwd) => {
|
|
@@ -15493,7 +16111,10 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
|
|
|
15493
16111
|
return { success: !!tree, tree };
|
|
15494
16112
|
}
|
|
15495
16113
|
},
|
|
15496
|
-
{ messagesDir: getSessionDir(directory, sessionId), onSessionEvent: (update) =>
|
|
16114
|
+
{ messagesDir: getSessionDir(directory, sessionId), onSessionEvent: (update) => {
|
|
16115
|
+
machineServiceRef?.notifySessionEvent(update);
|
|
16116
|
+
routeSessionEventForIdle(update, directory);
|
|
16117
|
+
} }
|
|
15497
16118
|
);
|
|
15498
16119
|
let insideOnTurnEnd = false;
|
|
15499
16120
|
const svampConfigChecker = createSvampConfigChecker(
|
|
@@ -15624,10 +16245,14 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
|
|
|
15624
16245
|
return { ok: pending.length === 0, output: pending.length ? `${pending.length} pending: ${pending.map((i) => "#" + i.id).join(" ")}` : "No pending issues." };
|
|
15625
16246
|
};
|
|
15626
16247
|
try {
|
|
15627
|
-
const budgetCheck = process.env.SVAMP_LOOP_BUDGET === "0" ? { exceeded: false } : checkLoopBudget(ledger, { max_iterations: ls.max_iterations, ...ls.budget || {} }, now, startedAt);
|
|
15628
|
-
|
|
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)) {
|
|
15629
16254
|
writeGoalLoopState(directory, sessionId, { ...ls, active: false, phase: "gave_up", completed_at: now, gave_up_reason: `resource budget exhausted \u2014 ${budgetCheck.reason}`, ledger });
|
|
15630
|
-
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");
|
|
15631
16256
|
checkSvampConfig?.();
|
|
15632
16257
|
return;
|
|
15633
16258
|
}
|
|
@@ -15659,8 +16284,26 @@ ${oracle.output.trim().slice(0, 500)}`, "\u{1F501} Continuing loop");
|
|
|
15659
16284
|
sessionService.pushMessage({ type: "message", message: `\u{1F501} Loop not done \u2014 independent review: ${result.reason}`, level: "warning" }, "event");
|
|
15660
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");
|
|
15661
16286
|
} else if (result.action === "gave_up") {
|
|
15662
|
-
|
|
15663
|
-
|
|
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
|
+
}
|
|
15664
16307
|
} else {
|
|
15665
16308
|
writeGoalLoopState(directory, sessionId, { ...ls, active: false, phase: "done", completed_at: Date.now(), holds, ledger });
|
|
15666
16309
|
checkSvampConfig?.();
|
|
@@ -16058,6 +16701,7 @@ ${oracle.output.trim().slice(0, 500)}`, "\u{1F501} Continuing loop");
|
|
|
16058
16701
|
const tunnel = await createExposedTunnel(spec);
|
|
16059
16702
|
tunnels.set(spec.name, tunnel);
|
|
16060
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());
|
|
16061
16705
|
} catch (err) {
|
|
16062
16706
|
logger.log(`[exposed-tunnels] Failed to restore ${spec.name}: ${err.message}`);
|
|
16063
16707
|
}
|
|
@@ -16208,6 +16852,61 @@ ${oracle.output.trim().slice(0, 500)}`, "\u{1F501} Continuing loop");
|
|
|
16208
16852
|
);
|
|
16209
16853
|
logger.log(`Restored ${persistedSessions.length} session(s) in ${Date.now() - t0}ms (concurrency ${restoreConcurrency})`);
|
|
16210
16854
|
}
|
|
16855
|
+
{
|
|
16856
|
+
const anySessionBusy = () => {
|
|
16857
|
+
let n = 0;
|
|
16858
|
+
for (const s of pidToTrackedSession.values()) {
|
|
16859
|
+
if (s.stopped || !s.hyphaService) continue;
|
|
16860
|
+
try {
|
|
16861
|
+
if (s.hyphaService.getMetadata()?.lifecycleState === "running") n++;
|
|
16862
|
+
} catch {
|
|
16863
|
+
}
|
|
16864
|
+
}
|
|
16865
|
+
return { busy: n > 0, n };
|
|
16866
|
+
};
|
|
16867
|
+
const triggerSelfRestart = (why) => {
|
|
16868
|
+
logger.log(`[graceful-restart] ${why} \u2014 restarting daemon now`);
|
|
16869
|
+
clearPendingRestart();
|
|
16870
|
+
try {
|
|
16871
|
+
const supPidFile = join$1(SVAMP_HOME, "supervisor.pid");
|
|
16872
|
+
const supPid = existsSync$1(supPidFile) ? parseInt(readFileSync$1(supPidFile, "utf-8").trim(), 10) : NaN;
|
|
16873
|
+
if (supPid && !isNaN(supPid)) {
|
|
16874
|
+
process.kill(supPid, "SIGUSR2");
|
|
16875
|
+
return;
|
|
16876
|
+
}
|
|
16877
|
+
} catch {
|
|
16878
|
+
}
|
|
16879
|
+
logger.log("[graceful-restart] no supervisor to signal \u2014 exiting for KeepAlive respawn");
|
|
16880
|
+
process.exit(0);
|
|
16881
|
+
};
|
|
16882
|
+
let lastDeferLog = 0;
|
|
16883
|
+
const GRACEFUL_RESTART_POLL_MS = 15e3;
|
|
16884
|
+
const DEFAULT_MAX_WAIT_MS = 60 * 60 * 1e3;
|
|
16885
|
+
const gracefulTimer = setInterval(() => {
|
|
16886
|
+
const pending = readPendingRestart();
|
|
16887
|
+
if (!pending) return;
|
|
16888
|
+
if (pending.force) {
|
|
16889
|
+
triggerSelfRestart("force requested");
|
|
16890
|
+
return;
|
|
16891
|
+
}
|
|
16892
|
+
const { busy, n } = anySessionBusy();
|
|
16893
|
+
if (!busy) {
|
|
16894
|
+
triggerSelfRestart("all sessions idle");
|
|
16895
|
+
return;
|
|
16896
|
+
}
|
|
16897
|
+
const maxWait = typeof pending.maxWaitMs === "number" && pending.maxWaitMs > 0 ? pending.maxWaitMs : DEFAULT_MAX_WAIT_MS;
|
|
16898
|
+
if (Date.now() - pending.scheduledAt > maxWait) {
|
|
16899
|
+
triggerSelfRestart(`max-wait ${Math.round(maxWait / 6e4)}m elapsed (${n} still busy)`);
|
|
16900
|
+
return;
|
|
16901
|
+
}
|
|
16902
|
+
const now = Date.now();
|
|
16903
|
+
if (now - lastDeferLog > 6e4) {
|
|
16904
|
+
lastDeferLog = now;
|
|
16905
|
+
logger.log(`[graceful-restart] deferring \u2014 ${n} session(s) actively thinking`);
|
|
16906
|
+
}
|
|
16907
|
+
}, GRACEFUL_RESTART_POLL_MS);
|
|
16908
|
+
gracefulTimer.unref?.();
|
|
16909
|
+
}
|
|
16211
16910
|
if (sessionsToAutoContinue.length > 0 && !options?.noAutoContinue) {
|
|
16212
16911
|
logger.log(`Auto-continuing ${sessionsToAutoContinue.length} interrupted session(s)...`);
|
|
16213
16912
|
for (const sessionId of sessionsToAutoContinue) {
|
|
@@ -16291,7 +16990,7 @@ ${oracle.output.trim().slice(0, 500)}`, "\u{1F501} Continuing loop");
|
|
|
16291
16990
|
const PING_TIMEOUT_MS = 15e3;
|
|
16292
16991
|
const POST_RECONNECT_GRACE_MS = 2e4;
|
|
16293
16992
|
const RECONNECT_JITTER_MS = 2500;
|
|
16294
|
-
const { WorkflowScheduler } = await import('./scheduler-
|
|
16993
|
+
const { WorkflowScheduler } = await import('./scheduler-Csp21zzg.mjs');
|
|
16295
16994
|
const workflowScheduler = new WorkflowScheduler({
|
|
16296
16995
|
projectRoots: () => {
|
|
16297
16996
|
const dirs = /* @__PURE__ */ new Set();
|
|
@@ -16861,13 +17560,18 @@ function daemonStatus() {
|
|
|
16861
17560
|
var run = /*#__PURE__*/Object.freeze({
|
|
16862
17561
|
__proto__: null,
|
|
16863
17562
|
DAEMON_STOP_MARKER_FILE: DAEMON_STOP_MARKER_FILE,
|
|
17563
|
+
PENDING_RESTART_FILE: PENDING_RESTART_FILE,
|
|
17564
|
+
clearPendingRestart: clearPendingRestart,
|
|
16864
17565
|
clearStopMarker: clearStopMarker,
|
|
16865
17566
|
daemonStatus: daemonStatus,
|
|
17567
|
+
isDaemonAlive: isDaemonAlive,
|
|
17568
|
+
readPendingRestart: readPendingRestart,
|
|
16866
17569
|
restartDaemon: restartDaemon,
|
|
16867
17570
|
startDaemon: startDaemon,
|
|
16868
17571
|
stopDaemon: stopDaemon,
|
|
16869
17572
|
stopMarkerExists: stopMarkerExists,
|
|
17573
|
+
writePendingRestart: writePendingRestart,
|
|
16870
17574
|
writeStopMarker: writeStopMarker
|
|
16871
17575
|
});
|
|
16872
17576
|
|
|
16873
|
-
export {
|
|
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 };
|