svamp-cli 0.2.98 → 0.2.100
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/README.md +7 -5
- package/bin/skills/loop/IMPLEMENTATION_PROGRESS.md +49 -0
- package/bin/skills/loop/SKILL.md +99 -0
- package/bin/skills/loop/bin/channel-core.mjs +161 -0
- package/bin/skills/loop/bin/channel-server.mjs +151 -0
- package/bin/skills/loop/bin/inject-loop.mjs +41 -0
- package/bin/skills/loop/bin/loop-init.mjs +128 -0
- package/bin/skills/loop/bin/loop-status.mjs +38 -0
- package/bin/skills/loop/bin/precompact.mjs +27 -0
- package/bin/skills/loop/bin/routine-cli.mjs +121 -0
- package/bin/skills/loop/bin/routine-core.mjs +126 -0
- package/bin/skills/loop/bin/routine-runner.mjs +125 -0
- package/bin/skills/loop/bin/routine-store.mjs +49 -0
- package/bin/skills/loop/bin/state-fp.mjs +113 -0
- package/bin/skills/loop/bin/stop-gate.mjs +170 -0
- package/bin/skills/loop/routines.process.yaml +20 -0
- package/bin/skills/loop/test/test-channel-core.mjs +86 -0
- package/bin/skills/loop/test/test-loop-gate.mjs +246 -0
- package/bin/skills/loop/test/test-routine-core.mjs +54 -0
- package/bin/skills/loop/test/test-routine-engine.mjs +122 -0
- package/dist/{agentCommands-BULNvfKa.mjs → agentCommands-muy26BZI.mjs} +2 -2
- package/dist/{auth-BfDOBBPy.mjs → auth-RVq9wRhV.mjs} +1 -1
- package/dist/{caddy-BMbX-mFX.mjs → caddy-CuTbE3NY.mjs} +1 -14
- package/dist/cli.mjs +76 -77
- package/dist/{commands-h2Dzb5m1.mjs → commands-ChzeHFd3.mjs} +1 -1
- package/dist/{commands-C9DbNFz1.mjs → commands-Cu96nDGv.mjs} +2 -2
- package/dist/{commands-FhGCsATM.mjs → commands-EwE87XNi.mjs} +1 -1
- package/dist/{commands-qE4ZGLzB.mjs → commands-lSqc48Ib.mjs} +6 -6
- package/dist/{commands-DIhhodx8.mjs → commands-rSREfaQg.mjs} +34 -42
- package/dist/{fleet-Cmma7Iu-.mjs → fleet-qN96q6Qb.mjs} +1 -1
- package/dist/{frpc-BZ4l4-os.mjs → frpc-CIkmTNdJ.mjs} +2 -15
- package/dist/{headlessCli-xRpI9fdk.mjs → headlessCli-BVcAcLr1.mjs} +2 -2
- package/dist/index.mjs +1 -1
- package/dist/package-B7S5w1VE.mjs +63 -0
- package/dist/{run-DxzG-3JD.mjs → run-CdtYIBbd.mjs} +158 -709
- package/dist/{run-DTIEcH-W.mjs → run-zXRdkYtk.mjs} +1 -1
- package/dist/{serveCommands-CzllIFB_.mjs → serveCommands-BZd0reEj.mjs} +5 -5
- package/dist/{serveManager-C6_Vloil.mjs → serveManager-lmPtmRnR.mjs} +3 -3
- package/dist/{sideband-wPe3a3m1.mjs → sideband-JeID_jF-.mjs} +1 -1
- package/package.json +3 -3
- package/dist/package-DD227VZO.mjs +0 -63
|
@@ -3,7 +3,7 @@ import fs, { mkdir as mkdir$1, readdir as readdir$1, readFile, writeFile as writ
|
|
|
3
3
|
import { readFileSync as readFileSync$1, mkdirSync, writeFileSync, renameSync, existsSync as existsSync$1, rmSync as rmSync$1, unlinkSync as unlinkSync$1, copyFileSync, watch, rmdirSync, readdirSync as readdirSync$1 } from 'fs';
|
|
4
4
|
import path__default, { join, dirname, basename, resolve } from 'path';
|
|
5
5
|
import { fileURLToPath } from 'url';
|
|
6
|
-
import { execFile, spawn as spawn$1, execSync as execSync$1 } from 'child_process';
|
|
6
|
+
import { execFile, spawn as spawn$1, execSync as execSync$1, spawnSync } from 'child_process';
|
|
7
7
|
import { randomUUID as randomUUID$1 } from 'crypto';
|
|
8
8
|
import { existsSync, readFileSync, mkdirSync as mkdirSync$1, readdirSync, writeFileSync as writeFileSync$1, renameSync as renameSync$1, rmSync, appendFileSync, unlinkSync } from 'node:fs';
|
|
9
9
|
import { exec, spawn, execSync, execFile as execFile$1, execFileSync } from 'node:child_process';
|
|
@@ -2077,7 +2077,7 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
2077
2077
|
const tunnels = handlers.tunnels;
|
|
2078
2078
|
if (!tunnels) throw new Error("Tunnel management not available");
|
|
2079
2079
|
if (tunnels.has(params.name)) throw new Error(`Tunnel '${params.name}' already running`);
|
|
2080
|
-
const { FrpcTunnel } = await import('./frpc-
|
|
2080
|
+
const { FrpcTunnel } = await import('./frpc-CIkmTNdJ.mjs');
|
|
2081
2081
|
const tunnel = new FrpcTunnel({
|
|
2082
2082
|
name: params.name,
|
|
2083
2083
|
ports: params.ports,
|
|
@@ -2338,7 +2338,7 @@ QUESTION: ${params.question || "Summarize this concisely."}` }
|
|
|
2338
2338
|
}
|
|
2339
2339
|
const deps = buildSessionDeps(rpc, { cwd, ownerEmail: owner });
|
|
2340
2340
|
const sender = { name: context?.user?.email || context?.user?.id || "user", kind: "user", verified: true };
|
|
2341
|
-
const { toolsForRole } = await import('./sideband-
|
|
2341
|
+
const { toolsForRole } = await import('./sideband-JeID_jF-.mjs');
|
|
2342
2342
|
const r2 = await runWiseAgent({ message: params.message, sender, config: { tools: toolsForRole(role2) }, deps, transport, model: resolved.model });
|
|
2343
2343
|
return fmt(r2);
|
|
2344
2344
|
}
|
|
@@ -3319,7 +3319,7 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
|
|
|
3319
3319
|
},
|
|
3320
3320
|
/**
|
|
3321
3321
|
* Patch the session config file (.svamp/{sessionId}/config.json).
|
|
3322
|
-
* Used by the frontend to set title, session_link,
|
|
3322
|
+
* Used by the frontend to set title, session_link, loop activation, etc.
|
|
3323
3323
|
* Null values remove keys from the config.
|
|
3324
3324
|
*/
|
|
3325
3325
|
updateConfig: async (patch, context) => {
|
|
@@ -9095,6 +9095,20 @@ async function ensureAutoInstalledSkills(logger) {
|
|
|
9095
9095
|
}
|
|
9096
9096
|
},
|
|
9097
9097
|
marketplaceVersion: async () => readBundledSkillVersion("artifact")
|
|
9098
|
+
},
|
|
9099
|
+
{
|
|
9100
|
+
// The self-verifying `loop` skill drives loop mode (Stop-hook gate +
|
|
9101
|
+
// LOOP.md injection). Bundled in the npm package (bin/skills/loop/) so the
|
|
9102
|
+
// daemon can run loop-init.mjs even offline / before marketplace publish.
|
|
9103
|
+
name: "loop",
|
|
9104
|
+
install: async () => {
|
|
9105
|
+
try {
|
|
9106
|
+
installBundledSkill("loop");
|
|
9107
|
+
} catch {
|
|
9108
|
+
await installSkillFromMarketplace("loop");
|
|
9109
|
+
}
|
|
9110
|
+
},
|
|
9111
|
+
marketplaceVersion: async () => readBundledSkillVersion("loop")
|
|
9098
9112
|
}
|
|
9099
9113
|
];
|
|
9100
9114
|
for (const task of tasks) {
|
|
@@ -9224,72 +9238,58 @@ function writeSvampConfig(configPath, config) {
|
|
|
9224
9238
|
renameSync(tmpPath, configPath);
|
|
9225
9239
|
return content;
|
|
9226
9240
|
}
|
|
9227
|
-
function
|
|
9228
|
-
return join(
|
|
9241
|
+
function getLoopDir(directory) {
|
|
9242
|
+
return join(directory, ".claude", "loop");
|
|
9229
9243
|
}
|
|
9230
|
-
function
|
|
9244
|
+
function readLoopState(directory) {
|
|
9231
9245
|
try {
|
|
9232
|
-
|
|
9233
|
-
|
|
9234
|
-
|
|
9235
|
-
if (parts.length < 3) return null;
|
|
9236
|
-
const frontmatter = parts[1];
|
|
9237
|
-
const task = parts.slice(2).join("---").trim();
|
|
9238
|
-
if (!task) return null;
|
|
9239
|
-
const fields = {};
|
|
9240
|
-
for (const line of frontmatter.split("\n")) {
|
|
9241
|
-
const match = line.match(/^(\w+):\s*(.*)$/);
|
|
9242
|
-
if (match) fields[match[1]] = match[2].replace(/^["']|["']$/g, "");
|
|
9243
|
-
}
|
|
9244
|
-
return {
|
|
9245
|
-
iteration: parseInt(fields.iteration || "1", 10) || 1,
|
|
9246
|
-
max_iterations: parseInt(fields.max_iterations || "0", 10) || 0,
|
|
9247
|
-
completion_promise: fields.completion_promise === "none" ? null : fields.completion_promise || "DONE",
|
|
9248
|
-
cooldown_seconds: parseInt(fields.cooldown_seconds || "1", 10) || 1,
|
|
9249
|
-
started_at: fields.started_at || (/* @__PURE__ */ new Date()).toISOString(),
|
|
9250
|
-
last_iteration_at: fields.last_iteration_at || void 0,
|
|
9251
|
-
context_mode: fields.context_mode === "fresh" || fields.context_mode === "continue" ? fields.context_mode : void 0,
|
|
9252
|
-
original_resume_id: fields.original_resume_id || void 0,
|
|
9253
|
-
task
|
|
9254
|
-
};
|
|
9246
|
+
const p = join(getLoopDir(directory), "loop-state.json");
|
|
9247
|
+
if (!existsSync$1(p)) return null;
|
|
9248
|
+
return JSON.parse(readFileSync$1(p, "utf-8"));
|
|
9255
9249
|
} catch {
|
|
9256
9250
|
return null;
|
|
9257
9251
|
}
|
|
9258
9252
|
}
|
|
9259
|
-
function
|
|
9260
|
-
|
|
9261
|
-
|
|
9262
|
-
const lastIterLine = state.last_iteration_at ? `
|
|
9263
|
-
last_iteration_at: "${state.last_iteration_at}"` : "";
|
|
9264
|
-
const contextModeLine = state.context_mode ? `
|
|
9265
|
-
context_mode: "${state.context_mode}"` : "";
|
|
9266
|
-
const originalResumeLine = state.original_resume_id ? `
|
|
9267
|
-
original_resume_id: "${state.original_resume_id}"` : "";
|
|
9268
|
-
const content = `---
|
|
9269
|
-
iteration: ${state.iteration}
|
|
9270
|
-
max_iterations: ${state.max_iterations}
|
|
9271
|
-
completion_promise: ${promiseYaml}
|
|
9272
|
-
cooldown_seconds: ${state.cooldown_seconds}
|
|
9273
|
-
started_at: "${state.started_at}"${lastIterLine}${contextModeLine}${originalResumeLine}
|
|
9274
|
-
---
|
|
9275
|
-
|
|
9276
|
-
${state.task}
|
|
9277
|
-
`;
|
|
9278
|
-
const tmpPath = `${filePath}.tmp`;
|
|
9279
|
-
writeFileSync(tmpPath, content);
|
|
9280
|
-
renameSync(tmpPath, filePath);
|
|
9253
|
+
function isLoopActive(directory) {
|
|
9254
|
+
const s = readLoopState(directory);
|
|
9255
|
+
return !!s && s.active !== false && s.phase !== "done" && s.phase !== "gave_up" && s.phase !== "cancelled";
|
|
9281
9256
|
}
|
|
9282
|
-
function
|
|
9257
|
+
function resolveLoopInit() {
|
|
9258
|
+
const candidates = [
|
|
9259
|
+
join(CLAUDE_SKILLS_DIR, "loop", "bin", "loop-init.mjs"),
|
|
9260
|
+
...getBundledSkillsDir() ? [join(getBundledSkillsDir(), "loop", "bin", "loop-init.mjs")] : []
|
|
9261
|
+
];
|
|
9262
|
+
for (const c of candidates) if (existsSync$1(c)) return c;
|
|
9263
|
+
return null;
|
|
9264
|
+
}
|
|
9265
|
+
function initLoop(directory, cfg) {
|
|
9266
|
+
const initScript = resolveLoopInit();
|
|
9267
|
+
if (!initScript) return false;
|
|
9268
|
+
const args = [initScript, directory, "--task", cfg.task];
|
|
9269
|
+
if (cfg.criteria) args.push("--criteria", cfg.criteria);
|
|
9270
|
+
if (cfg.oracle) args.push("--oracle", cfg.oracle);
|
|
9271
|
+
if (typeof cfg.maxIterations === "number") args.push("--max", String(cfg.maxIterations));
|
|
9272
|
+
args.push("--evaluator", cfg.evaluator === false ? "off" : "on");
|
|
9273
|
+
if (cfg.model) args.push("--model", cfg.model);
|
|
9274
|
+
const res = spawnSync(process.execPath, args, { encoding: "utf-8", timeout: 3e4 });
|
|
9275
|
+
return res.status === 0;
|
|
9276
|
+
}
|
|
9277
|
+
function deactivateLoop(directory) {
|
|
9283
9278
|
try {
|
|
9284
|
-
|
|
9279
|
+
const p = join(getLoopDir(directory), "loop-state.json");
|
|
9280
|
+
if (!existsSync$1(p)) return;
|
|
9281
|
+
const s = JSON.parse(readFileSync$1(p, "utf-8"));
|
|
9282
|
+
s.active = false;
|
|
9283
|
+
s.phase = "cancelled";
|
|
9284
|
+
const tmp = p + ".tmp";
|
|
9285
|
+
writeFileSync(tmp, JSON.stringify(s, null, 2));
|
|
9286
|
+
renameSync(tmp, p);
|
|
9285
9287
|
} catch {
|
|
9286
9288
|
}
|
|
9287
9289
|
}
|
|
9288
|
-
function createSvampConfigChecker(directory, sessionId, getMetadata, setMetadata, sessionService, logger,
|
|
9290
|
+
function createSvampConfigChecker(directory, sessionId, getMetadata, setMetadata, sessionService, logger, onLoopActivated) {
|
|
9289
9291
|
const configPath = getSvampConfigPath(directory, sessionId);
|
|
9290
|
-
const ralphStatePath = getRalphStateFilePath(directory, sessionId);
|
|
9291
9292
|
let lastConfigContent = "";
|
|
9292
|
-
let lastRalphContent = "";
|
|
9293
9293
|
if (existsSync$1(configPath)) {
|
|
9294
9294
|
try {
|
|
9295
9295
|
lastConfigContent = readFileSync$1(configPath, "utf-8");
|
|
@@ -9300,13 +9300,6 @@ function createSvampConfigChecker(directory, sessionId, getMetadata, setMetadata
|
|
|
9300
9300
|
} catch {
|
|
9301
9301
|
}
|
|
9302
9302
|
}
|
|
9303
|
-
if (existsSync$1(ralphStatePath)) {
|
|
9304
|
-
try {
|
|
9305
|
-
lastRalphContent = readFileSync$1(ralphStatePath, "utf-8");
|
|
9306
|
-
} catch {
|
|
9307
|
-
}
|
|
9308
|
-
}
|
|
9309
|
-
let needsInitialRalphProcess = !!lastRalphContent;
|
|
9310
9303
|
function processConfig(config, meta) {
|
|
9311
9304
|
if (typeof config.title === "string" && config.title.trim()) {
|
|
9312
9305
|
const newTitle = config.title.trim();
|
|
@@ -9351,56 +9344,6 @@ function createSvampConfigChecker(directory, sessionId, getMetadata, setMetadata
|
|
|
9351
9344
|
}
|
|
9352
9345
|
}
|
|
9353
9346
|
}
|
|
9354
|
-
function processRalphState() {
|
|
9355
|
-
const meta = getMetadata();
|
|
9356
|
-
const prevRalph = meta.ralphLoop;
|
|
9357
|
-
const state = readRalphState(ralphStatePath);
|
|
9358
|
-
if (state) {
|
|
9359
|
-
const ralphLoop = {
|
|
9360
|
-
active: true,
|
|
9361
|
-
task: state.task,
|
|
9362
|
-
completionPromise: state.completion_promise ?? "none",
|
|
9363
|
-
maxIterations: state.max_iterations,
|
|
9364
|
-
currentIteration: state.iteration,
|
|
9365
|
-
startedAt: state.started_at,
|
|
9366
|
-
cooldownSeconds: state.cooldown_seconds,
|
|
9367
|
-
contextMode: state.context_mode || "fresh",
|
|
9368
|
-
lastIterationStartedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
9369
|
-
};
|
|
9370
|
-
if (!prevRalph?.active) {
|
|
9371
|
-
const progressRelPath = `.svamp/${sessionId}/ralph-progress.md`;
|
|
9372
|
-
const prompt = buildRalphPrompt(state.task, state);
|
|
9373
|
-
const ralphSysPrompt = buildRalphSystemPrompt(state, progressRelPath);
|
|
9374
|
-
const existingQueue = getMetadata().messageQueue || [];
|
|
9375
|
-
setMetadata((m) => ({
|
|
9376
|
-
...m,
|
|
9377
|
-
ralphLoop,
|
|
9378
|
-
messageQueue: [...existingQueue, {
|
|
9379
|
-
id: randomUUID$1(),
|
|
9380
|
-
text: prompt,
|
|
9381
|
-
displayText: state.task,
|
|
9382
|
-
createdAt: Date.now(),
|
|
9383
|
-
ralphSystemPrompt: ralphSysPrompt
|
|
9384
|
-
}]
|
|
9385
|
-
}));
|
|
9386
|
-
sessionService.pushMessage(
|
|
9387
|
-
{ type: "message", message: buildIterationStatus(state.iteration + 1, state.max_iterations, state.completion_promise) },
|
|
9388
|
-
"event"
|
|
9389
|
-
);
|
|
9390
|
-
logger.log(`[svampConfig] Ralph loop started/resumed at iteration ${state.iteration + 1}: "${state.task.slice(0, 50)}..."`);
|
|
9391
|
-
onRalphLoopActivated?.();
|
|
9392
|
-
} else if (prevRalph.currentIteration !== ralphLoop.currentIteration || prevRalph.task !== ralphLoop.task) {
|
|
9393
|
-
setMetadata((m) => ({ ...m, ralphLoop }));
|
|
9394
|
-
}
|
|
9395
|
-
} else if (prevRalph?.active) {
|
|
9396
|
-
setMetadata((m) => ({ ...m, ralphLoop: { active: false } }));
|
|
9397
|
-
sessionService.pushMessage(
|
|
9398
|
-
{ type: "message", message: `Ralph loop cancelled at iteration ${prevRalph.currentIteration}.` },
|
|
9399
|
-
"event"
|
|
9400
|
-
);
|
|
9401
|
-
logger.log(`[svampConfig] Ralph loop state file removed \u2014 cancelled at iteration ${prevRalph.currentIteration}`);
|
|
9402
|
-
}
|
|
9403
|
-
}
|
|
9404
9347
|
const configChecker = () => {
|
|
9405
9348
|
try {
|
|
9406
9349
|
if (existsSync$1(configPath)) {
|
|
@@ -9414,50 +9357,54 @@ function createSvampConfigChecker(directory, sessionId, getMetadata, setMetadata
|
|
|
9414
9357
|
} catch {
|
|
9415
9358
|
}
|
|
9416
9359
|
};
|
|
9417
|
-
const ralphChecker = () => {
|
|
9418
|
-
try {
|
|
9419
|
-
if (existsSync$1(ralphStatePath)) {
|
|
9420
|
-
const content = readFileSync$1(ralphStatePath, "utf-8");
|
|
9421
|
-
if (content !== lastRalphContent) {
|
|
9422
|
-
lastRalphContent = content;
|
|
9423
|
-
processRalphState();
|
|
9424
|
-
}
|
|
9425
|
-
} else if (lastRalphContent) {
|
|
9426
|
-
lastRalphContent = "";
|
|
9427
|
-
processRalphState();
|
|
9428
|
-
}
|
|
9429
|
-
} catch {
|
|
9430
|
-
}
|
|
9431
|
-
};
|
|
9432
9360
|
const checker = () => {
|
|
9433
9361
|
configChecker();
|
|
9434
|
-
ralphChecker();
|
|
9435
9362
|
};
|
|
9436
9363
|
const writeConfig = (patch) => {
|
|
9437
|
-
if ("
|
|
9438
|
-
const
|
|
9439
|
-
if (
|
|
9440
|
-
const
|
|
9441
|
-
|
|
9442
|
-
|
|
9443
|
-
|
|
9444
|
-
|
|
9445
|
-
|
|
9446
|
-
|
|
9447
|
-
|
|
9448
|
-
|
|
9449
|
-
})(),
|
|
9450
|
-
started_at: rl.started_at || (/* @__PURE__ */ new Date()).toISOString(),
|
|
9451
|
-
context_mode: contextMode,
|
|
9452
|
-
task: rl.task.trim()
|
|
9364
|
+
if ("loop" in patch) {
|
|
9365
|
+
const lp = patch.loop;
|
|
9366
|
+
if (lp && typeof lp === "object" && typeof lp.task === "string" && lp.task.trim()) {
|
|
9367
|
+
const oracle = typeof lp.oracle === "string" && lp.oracle.trim() ? lp.oracle.trim() : void 0;
|
|
9368
|
+
const maxIterations = typeof lp.max_iterations === "number" ? lp.max_iterations : 20;
|
|
9369
|
+
const evaluator = lp.evaluator !== false;
|
|
9370
|
+
const ok = initLoop(directory, {
|
|
9371
|
+
task: lp.task.trim(),
|
|
9372
|
+
criteria: typeof lp.criteria === "string" && lp.criteria.trim() ? lp.criteria.trim() : void 0,
|
|
9373
|
+
oracle,
|
|
9374
|
+
maxIterations,
|
|
9375
|
+
evaluator
|
|
9453
9376
|
});
|
|
9454
|
-
|
|
9377
|
+
if (ok) {
|
|
9378
|
+
const existingQueue = getMetadata().messageQueue || [];
|
|
9379
|
+
const kickoff = "Begin the loop. Read LOOP.md and work on the task until the exit conditions are met. Do not stop early \u2014 an independent Stop gate will re-check before the loop can end.";
|
|
9380
|
+
setMetadata((m) => ({
|
|
9381
|
+
...m,
|
|
9382
|
+
messageQueue: [...existingQueue, {
|
|
9383
|
+
id: randomUUID$1(),
|
|
9384
|
+
text: kickoff,
|
|
9385
|
+
displayText: `\u{1F501} Loop started: ${lp.task.trim().slice(0, 100)}`,
|
|
9386
|
+
createdAt: Date.now()
|
|
9387
|
+
}]
|
|
9388
|
+
}));
|
|
9389
|
+
sessionService.pushMessage(
|
|
9390
|
+
{ type: "message", message: `\u{1F501} Loop started \u2014 iterating until done (oracle: ${oracle || "none"}, evaluator ${evaluator ? "on" : "off"}, max ${maxIterations}).` },
|
|
9391
|
+
"event"
|
|
9392
|
+
);
|
|
9393
|
+
logger.log(`[svampConfig] Loop started: "${lp.task.trim().slice(0, 50)}..."`);
|
|
9394
|
+
onLoopActivated?.();
|
|
9395
|
+
} else {
|
|
9396
|
+
sessionService.pushMessage(
|
|
9397
|
+
{ type: "message", message: "Failed to start loop \u2014 the loop skill could not be located. Reinstall with: svamp skills install loop --force", level: "error" },
|
|
9398
|
+
"event"
|
|
9399
|
+
);
|
|
9400
|
+
logger.log(`[svampConfig] Loop init failed \u2014 loop-init.mjs not found`);
|
|
9401
|
+
}
|
|
9455
9402
|
} else {
|
|
9456
|
-
|
|
9457
|
-
|
|
9458
|
-
|
|
9403
|
+
deactivateLoop(directory);
|
|
9404
|
+
sessionService.pushMessage({ type: "message", message: "Loop cancelled." }, "event");
|
|
9405
|
+
logger.log(`[svampConfig] Loop cancelled`);
|
|
9459
9406
|
}
|
|
9460
|
-
const {
|
|
9407
|
+
const { loop: _, ...restPatch } = patch;
|
|
9461
9408
|
patch = restPatch;
|
|
9462
9409
|
}
|
|
9463
9410
|
if (Object.keys(patch).length > 0) {
|
|
@@ -9476,32 +9423,11 @@ function createSvampConfigChecker(directory, sessionId, getMetadata, setMetadata
|
|
|
9476
9423
|
mkdirSync(configDir, { recursive: true });
|
|
9477
9424
|
watcher = watch(configDir, (eventType, filename) => {
|
|
9478
9425
|
if (filename === "config.json") configChecker();
|
|
9479
|
-
if (filename === "ralph-loop.md") ralphChecker();
|
|
9480
9426
|
});
|
|
9481
9427
|
watcher.on("error", () => {
|
|
9482
9428
|
});
|
|
9483
9429
|
} catch {
|
|
9484
9430
|
}
|
|
9485
|
-
if (needsInitialRalphProcess) {
|
|
9486
|
-
const state = readRalphState(ralphStatePath);
|
|
9487
|
-
if (state) {
|
|
9488
|
-
setMetadata((m) => ({
|
|
9489
|
-
...m,
|
|
9490
|
-
ralphLoop: {
|
|
9491
|
-
active: true,
|
|
9492
|
-
task: state.task,
|
|
9493
|
-
completionPromise: state.completion_promise ?? "none",
|
|
9494
|
-
maxIterations: state.max_iterations,
|
|
9495
|
-
currentIteration: state.iteration,
|
|
9496
|
-
startedAt: state.started_at,
|
|
9497
|
-
cooldownSeconds: state.cooldown_seconds,
|
|
9498
|
-
contextMode: state.context_mode || "fresh",
|
|
9499
|
-
lastIterationStartedAt: state.last_iteration_at || (/* @__PURE__ */ new Date()).toISOString()
|
|
9500
|
-
}
|
|
9501
|
-
}));
|
|
9502
|
-
logger.log(`[svampConfig] Ralph loop state restored (iteration ${state.iteration}): "${state.task.slice(0, 50)}..."`);
|
|
9503
|
-
}
|
|
9504
|
-
}
|
|
9505
9431
|
return {
|
|
9506
9432
|
check: checker,
|
|
9507
9433
|
cleanup: () => {
|
|
@@ -9510,133 +9436,6 @@ function createSvampConfigChecker(directory, sessionId, getMetadata, setMetadata
|
|
|
9510
9436
|
writeConfig
|
|
9511
9437
|
};
|
|
9512
9438
|
}
|
|
9513
|
-
function buildIterationStatus(iteration, maxIterations, completionPromise) {
|
|
9514
|
-
const iterStr = maxIterations > 0 ? `${iteration}/${maxIterations}` : `${iteration}`;
|
|
9515
|
-
if (completionPromise) {
|
|
9516
|
-
return `Ralph iteration ${iterStr} | To stop: output <promise>${completionPromise}</promise>`;
|
|
9517
|
-
}
|
|
9518
|
-
return `Ralph iteration ${iterStr} | Manual stop only`;
|
|
9519
|
-
}
|
|
9520
|
-
function buildRalphSystemPrompt(state, progressFilePath) {
|
|
9521
|
-
const isFresh = state.context_mode === "fresh" || !state.context_mode;
|
|
9522
|
-
if (isFresh && progressFilePath) {
|
|
9523
|
-
return [
|
|
9524
|
-
"# Ralph Loop \u2014 Fresh Context Mode",
|
|
9525
|
-
"",
|
|
9526
|
-
"You are an autonomous coding agent in an automated loop.",
|
|
9527
|
-
"Each iteration spawns a fresh process \u2014 you have NO memory of previous iterations.",
|
|
9528
|
-
"",
|
|
9529
|
-
"## Your Workflow",
|
|
9530
|
-
"",
|
|
9531
|
-
`1. Read the progress file at \`${progressFilePath}\` (check Patterns section first)`,
|
|
9532
|
-
"2. Check workspace files and git history (`git log --oneline -10`) to understand current state",
|
|
9533
|
-
"3. Pick the next incomplete task and implement it",
|
|
9534
|
-
"4. Run quality checks (tests, typecheck, build \u2014 whatever the project requires)",
|
|
9535
|
-
"5. Commit your changes with a clear commit message",
|
|
9536
|
-
`6. Update \`${progressFilePath}\` with your progress (ALWAYS append, never replace)`,
|
|
9537
|
-
"",
|
|
9538
|
-
"## Progress Report Format",
|
|
9539
|
-
"",
|
|
9540
|
-
`APPEND to \`${progressFilePath}\`:`,
|
|
9541
|
-
"```",
|
|
9542
|
-
"## [Date/Time] \u2014 What was done",
|
|
9543
|
-
"- What was implemented",
|
|
9544
|
-
"- Files changed",
|
|
9545
|
-
"- **Learnings for future iterations:**",
|
|
9546
|
-
" - Patterns discovered",
|
|
9547
|
-
" - Gotchas encountered",
|
|
9548
|
-
" - Useful context",
|
|
9549
|
-
"---",
|
|
9550
|
-
"```",
|
|
9551
|
-
"",
|
|
9552
|
-
"## Consolidate Patterns",
|
|
9553
|
-
"",
|
|
9554
|
-
`If you discover a reusable pattern, add it to the \`## Patterns\` section at the TOP of \`${progressFilePath}\`.`,
|
|
9555
|
-
"Only add patterns that are general and reusable, not task-specific details.",
|
|
9556
|
-
"",
|
|
9557
|
-
"## Quality Requirements",
|
|
9558
|
-
"",
|
|
9559
|
-
"- Do NOT commit broken code",
|
|
9560
|
-
"- Keep changes focused and minimal",
|
|
9561
|
-
"- Follow existing code patterns",
|
|
9562
|
-
...state.completion_promise ? [
|
|
9563
|
-
"",
|
|
9564
|
-
"## Stop Condition",
|
|
9565
|
-
"",
|
|
9566
|
-
`To signal completion, output: <promise>${state.completion_promise}</promise>`,
|
|
9567
|
-
"",
|
|
9568
|
-
"ONLY output this when the task is FULLY and PERMANENTLY complete.",
|
|
9569
|
-
"Do NOT output it if there is ANY remaining work.",
|
|
9570
|
-
"When in doubt, do NOT output the promise \u2014 the loop will give you another turn.",
|
|
9571
|
-
"",
|
|
9572
|
-
"CRITICAL: Do NOT output a false promise to exit the loop.",
|
|
9573
|
-
"The loop is designed to continue until genuine completion. Trust the process."
|
|
9574
|
-
] : [
|
|
9575
|
-
"",
|
|
9576
|
-
"## Continuous Mode",
|
|
9577
|
-
"",
|
|
9578
|
-
"This is a continuous/infinite loop with no completion signal.",
|
|
9579
|
-
"Just do meaningful work each iteration. The loop will continue until manually cancelled.",
|
|
9580
|
-
"Focus on making progress, documenting what you did, and setting up the next iteration."
|
|
9581
|
-
]
|
|
9582
|
-
].join("\n");
|
|
9583
|
-
}
|
|
9584
|
-
return [
|
|
9585
|
-
"# Ralph Loop \u2014 Continue Mode",
|
|
9586
|
-
"",
|
|
9587
|
-
"You are in an automated loop. After this turn ends, the system will automatically",
|
|
9588
|
-
"start a new turn with the SAME PROMPT \u2014 you do NOT need to finish everything now.",
|
|
9589
|
-
"Your previous work persists in conversation history and files.",
|
|
9590
|
-
"Just do meaningful work this turn and let the loop continue.",
|
|
9591
|
-
"",
|
|
9592
|
-
...state.completion_promise ? [
|
|
9593
|
-
"## Stop Condition",
|
|
9594
|
-
"",
|
|
9595
|
-
`To signal completion, output: <promise>${state.completion_promise}</promise>`,
|
|
9596
|
-
"",
|
|
9597
|
-
"CRITICAL \u2014 Do NOT output the promise if:",
|
|
9598
|
-
'- The task says "continuously", "forever", "keep running", or "until I stop you"',
|
|
9599
|
-
"- There is ANY remaining work, follow-up, or next step you could do",
|
|
9600
|
-
"- You just finished one pass/cycle of a recurring task (the loop handles repetition)",
|
|
9601
|
-
"- You are uncertain whether the task is truly done",
|
|
9602
|
-
"",
|
|
9603
|
-
"CRITICAL: Do NOT output a false promise to exit the loop, even if you think you're",
|
|
9604
|
-
"stuck or should exit for other reasons. The loop is designed to continue until genuine",
|
|
9605
|
-
"completion. Trust the process."
|
|
9606
|
-
] : [
|
|
9607
|
-
"## Continuous Mode",
|
|
9608
|
-
"",
|
|
9609
|
-
"This is a continuous/infinite loop with no completion signal.",
|
|
9610
|
-
"Just do meaningful work this turn and let the loop continue.",
|
|
9611
|
-
"The loop will run until manually cancelled."
|
|
9612
|
-
]
|
|
9613
|
-
].join("\n");
|
|
9614
|
-
}
|
|
9615
|
-
function buildRalphPrompt(task, state) {
|
|
9616
|
-
const isFresh = state.context_mode === "fresh" || !state.context_mode;
|
|
9617
|
-
if (isFresh) {
|
|
9618
|
-
return task;
|
|
9619
|
-
}
|
|
9620
|
-
const iterStr = state.max_iterations > 0 ? `${state.iteration}/${state.max_iterations}` : `${state.iteration}`;
|
|
9621
|
-
const reminderLines = [
|
|
9622
|
-
"<system-reminder>",
|
|
9623
|
-
`Ralph Loop \u2014 Iteration ${iterStr} (Continue Mode)`,
|
|
9624
|
-
"Your conversation history persists. Continue from where you left off."
|
|
9625
|
-
];
|
|
9626
|
-
if (state.completion_promise) {
|
|
9627
|
-
reminderLines.push(`To signal completion, output EXACTLY: <promise>${state.completion_promise}</promise>`);
|
|
9628
|
-
reminderLines.push("Only output the promise when the task is FULLY and PERMANENTLY complete.");
|
|
9629
|
-
reminderLines.push("Do NOT output a false promise to exit the loop.");
|
|
9630
|
-
} else {
|
|
9631
|
-
reminderLines.push("This is a continuous loop \u2014 no completion signal needed. Just do meaningful work.");
|
|
9632
|
-
}
|
|
9633
|
-
reminderLines.push("</system-reminder>");
|
|
9634
|
-
const reminder = reminderLines.join("\n");
|
|
9635
|
-
return task + "\n\n" + reminder;
|
|
9636
|
-
}
|
|
9637
|
-
function getRalphProgressFilePath(directory, sessionId) {
|
|
9638
|
-
return join(getSessionDir(directory, sessionId), "ralph-progress.md");
|
|
9639
|
-
}
|
|
9640
9439
|
function loadSessionIndex() {
|
|
9641
9440
|
if (!existsSync$1(SESSION_INDEX_FILE)) return {};
|
|
9642
9441
|
try {
|
|
@@ -9682,16 +9481,6 @@ function deletePersistedSession(sessionId) {
|
|
|
9682
9481
|
if (existsSync$1(configFile)) unlinkSync$1(configFile);
|
|
9683
9482
|
} catch {
|
|
9684
9483
|
}
|
|
9685
|
-
const ralphStateFile = getRalphStateFilePath(entry.directory, sessionId);
|
|
9686
|
-
try {
|
|
9687
|
-
if (existsSync$1(ralphStateFile)) unlinkSync$1(ralphStateFile);
|
|
9688
|
-
} catch {
|
|
9689
|
-
}
|
|
9690
|
-
const ralphProgressFile = getRalphProgressFilePath(entry.directory, sessionId);
|
|
9691
|
-
try {
|
|
9692
|
-
if (existsSync$1(ralphProgressFile)) unlinkSync$1(ralphProgressFile);
|
|
9693
|
-
} catch {
|
|
9694
|
-
}
|
|
9695
9484
|
const sessionDir = getSessionDir(entry.directory, sessionId);
|
|
9696
9485
|
try {
|
|
9697
9486
|
rmdirSync(sessionDir);
|
|
@@ -10045,7 +9834,7 @@ async function startDaemon(options) {
|
|
|
10045
9834
|
const list = loadExposedTunnels().filter((t) => t.name !== name);
|
|
10046
9835
|
saveExposedTunnels(list);
|
|
10047
9836
|
}
|
|
10048
|
-
const { ServeManager } = await import('./serveManager-
|
|
9837
|
+
const { ServeManager } = await import('./serveManager-lmPtmRnR.mjs');
|
|
10049
9838
|
const serveManager = new ServeManager(SVAMP_HOME, (msg) => logger.log(`[SERVE] ${msg}`), hyphaServerUrl);
|
|
10050
9839
|
ensureAutoInstalledSkills(logger).catch(() => {
|
|
10051
9840
|
});
|
|
@@ -10157,7 +9946,7 @@ async function startDaemon(options) {
|
|
|
10157
9946
|
}
|
|
10158
9947
|
}, shouldAutoAllow2 = function(toolName, toolInput) {
|
|
10159
9948
|
if (toolName === "AskUserQuestion") {
|
|
10160
|
-
return
|
|
9949
|
+
return isLoopActive(directory);
|
|
10161
9950
|
}
|
|
10162
9951
|
if (toolName === "Bash") {
|
|
10163
9952
|
const inputObj = toolInput;
|
|
@@ -10170,7 +9959,7 @@ async function startDaemon(options) {
|
|
|
10170
9959
|
} else if (allowedTools.has(toolName)) {
|
|
10171
9960
|
return true;
|
|
10172
9961
|
}
|
|
10173
|
-
if (
|
|
9962
|
+
if (isLoopActive(directory)) return true;
|
|
10174
9963
|
if (currentPermissionMode === "bypassPermissions" || currentPermissionMode === "yolo") return true;
|
|
10175
9964
|
if ((currentPermissionMode === "acceptEdits" || currentPermissionMode === "safe-yolo") && EDIT_TOOLS.has(toolName)) return true;
|
|
10176
9965
|
return false;
|
|
@@ -10261,10 +10050,7 @@ async function startDaemon(options) {
|
|
|
10261
10050
|
const sessionCreatedAt = persisted?.createdAt || Date.now();
|
|
10262
10051
|
let lastSpawnMeta = persisted?.spawnMeta || {};
|
|
10263
10052
|
let sessionWasProcessing = !!options2.wasProcessing;
|
|
10264
|
-
let lastAssistantText = "";
|
|
10265
10053
|
let lastMainModel;
|
|
10266
|
-
let consecutiveRalphErrors = 0;
|
|
10267
|
-
const MAX_RALPH_ERRORS = 3;
|
|
10268
10054
|
let spawnHasReceivedInit = false;
|
|
10269
10055
|
let startupFailureRetryPending = false;
|
|
10270
10056
|
let startupRetryMessage;
|
|
@@ -10287,23 +10073,23 @@ async function startDaemon(options) {
|
|
|
10287
10073
|
stuckWatchdogTimer = setInterval(() => {
|
|
10288
10074
|
if (!claudeProcess || claudeProcess.exitCode !== null) return;
|
|
10289
10075
|
if (!sessionWasProcessing) return;
|
|
10290
|
-
|
|
10291
|
-
if (!ralphState) return;
|
|
10076
|
+
if (!isLoopActive(directory)) return;
|
|
10292
10077
|
if (claudeProcess.pid && hasActiveChildren(claudeProcess.pid)) {
|
|
10293
10078
|
lastOutputTime = Date.now();
|
|
10294
10079
|
return;
|
|
10295
10080
|
}
|
|
10296
10081
|
const elapsed = Date.now() - lastOutputTime;
|
|
10297
10082
|
if (elapsed > STUCK_PROCESS_TIMEOUT_MS) {
|
|
10298
|
-
logger.log(`[Session ${sessionId}]
|
|
10083
|
+
logger.log(`[Session ${sessionId}] Loop stuck: mid-turn, no output for ${Math.round(elapsed / 1e3)}s, no child processes \u2014 killing to resume the loop`);
|
|
10299
10084
|
sessionService.pushMessage(
|
|
10300
|
-
{ type: "message", message: `
|
|
10085
|
+
{ type: "message", message: `Loop appears stuck (no output for ${Math.round(elapsed / 6e4)} minutes, no active tools). Restarting to continue...`, level: "warning" },
|
|
10301
10086
|
"event"
|
|
10302
10087
|
);
|
|
10303
10088
|
claudeProcess.kill("SIGTERM");
|
|
10304
10089
|
setTimeout(() => {
|
|
10305
|
-
if (!trackedSession.stopped) {
|
|
10306
|
-
logger.log(`[Session ${sessionId}] Stuck watchdog: nudging
|
|
10090
|
+
if (!trackedSession.stopped && isLoopActive(directory)) {
|
|
10091
|
+
logger.log(`[Session ${sessionId}] Stuck watchdog: nudging loop to resume`);
|
|
10092
|
+
enqueueLoopContinue();
|
|
10307
10093
|
processMessageQueueRef?.();
|
|
10308
10094
|
}
|
|
10309
10095
|
}, 3e3);
|
|
@@ -10316,6 +10102,20 @@ async function startDaemon(options) {
|
|
|
10316
10102
|
stuckWatchdogTimer = null;
|
|
10317
10103
|
}
|
|
10318
10104
|
};
|
|
10105
|
+
const enqueueLoopContinue = () => {
|
|
10106
|
+
const existingQueue = sessionMetadata.messageQueue || [];
|
|
10107
|
+
const text = "Continue the loop. Read LOOP.md and keep working toward the exit conditions until the Stop gate confirms completion.";
|
|
10108
|
+
sessionMetadata = {
|
|
10109
|
+
...sessionMetadata,
|
|
10110
|
+
messageQueue: [...existingQueue, {
|
|
10111
|
+
id: randomUUID$1(),
|
|
10112
|
+
text,
|
|
10113
|
+
displayText: "\u{1F501} Resuming loop",
|
|
10114
|
+
createdAt: Date.now()
|
|
10115
|
+
}]
|
|
10116
|
+
};
|
|
10117
|
+
sessionService.updateMetadata(sessionMetadata);
|
|
10118
|
+
};
|
|
10319
10119
|
const signalProcessing = (processing) => {
|
|
10320
10120
|
sessionService.sendKeepAlive(processing);
|
|
10321
10121
|
const newState = processing ? "running" : "idle";
|
|
@@ -10395,7 +10195,7 @@ async function startDaemon(options) {
|
|
|
10395
10195
|
if (options2.forceIsolation || sessionMetadata.sharing?.enabled) {
|
|
10396
10196
|
rawPermissionMode = rawPermissionMode === "default" ? "auto-approve-all" : rawPermissionMode;
|
|
10397
10197
|
}
|
|
10398
|
-
if (
|
|
10198
|
+
if (isLoopActive(directory)) {
|
|
10399
10199
|
rawPermissionMode = "bypassPermissions";
|
|
10400
10200
|
}
|
|
10401
10201
|
const permissionMode = toClaudePermissionMode(rawPermissionMode);
|
|
@@ -10622,10 +10422,6 @@ async function startDaemon(options) {
|
|
|
10622
10422
|
logger.log(`[Session ${sessionId}] Background task launched: ${label} (count=${backgroundTaskCount})`);
|
|
10623
10423
|
}
|
|
10624
10424
|
}
|
|
10625
|
-
const textBlocks = assistantContent.filter((b) => b.type === "text").map((b) => b.text);
|
|
10626
|
-
if (textBlocks.length > 0) {
|
|
10627
|
-
lastAssistantText += textBlocks.join("\n");
|
|
10628
|
-
}
|
|
10629
10425
|
}
|
|
10630
10426
|
if (msg.type === "result") {
|
|
10631
10427
|
if (msg.is_error) {
|
|
@@ -10666,8 +10462,8 @@ async function startDaemon(options) {
|
|
|
10666
10462
|
}
|
|
10667
10463
|
}
|
|
10668
10464
|
if (msg.type === "result") {
|
|
10669
|
-
const
|
|
10670
|
-
if (!turnInitiatedByUser && !
|
|
10465
|
+
const loopActive = isLoopActive(directory);
|
|
10466
|
+
if (!turnInitiatedByUser && !loopActive) {
|
|
10671
10467
|
logger.log(`[Session ${sessionId}] Skipping stale result from SDK-initiated turn`);
|
|
10672
10468
|
const hasBackgroundTasks = backgroundTaskCount > 0;
|
|
10673
10469
|
if (hasBackgroundTasks) {
|
|
@@ -10692,8 +10488,8 @@ async function startDaemon(options) {
|
|
|
10692
10488
|
turnInitiatedByUser = true;
|
|
10693
10489
|
continue;
|
|
10694
10490
|
}
|
|
10695
|
-
if (!turnInitiatedByUser &&
|
|
10696
|
-
logger.log(`[Session ${sessionId}] SDK-initiated result during active
|
|
10491
|
+
if (!turnInitiatedByUser && loopActive) {
|
|
10492
|
+
logger.log(`[Session ${sessionId}] SDK-initiated result during active loop \u2014 processing anyway to avoid stalling`);
|
|
10697
10493
|
turnInitiatedByUser = true;
|
|
10698
10494
|
}
|
|
10699
10495
|
if (msg.session_id) {
|
|
@@ -10743,191 +10539,8 @@ async function startDaemon(options) {
|
|
|
10743
10539
|
sessionService.pushMessage({ type: "session_event", message: taskInfo }, "session");
|
|
10744
10540
|
}
|
|
10745
10541
|
const queueLen = sessionMetadata.messageQueue?.length ?? 0;
|
|
10746
|
-
if (msg.is_error) {
|
|
10747
|
-
const rlStateForError = readRalphState(getRalphStateFilePath(directory, sessionId));
|
|
10748
|
-
if (rlStateForError) {
|
|
10749
|
-
consecutiveRalphErrors++;
|
|
10750
|
-
logger.log(`[Session ${sessionId}] Ralph loop: error result (consecutive=${consecutiveRalphErrors}/${MAX_RALPH_ERRORS})`);
|
|
10751
|
-
if (consecutiveRalphErrors >= MAX_RALPH_ERRORS) {
|
|
10752
|
-
logger.log(`[Session ${sessionId}] Ralph loop: ${MAX_RALPH_ERRORS} consecutive errors \u2014 stopping loop`);
|
|
10753
|
-
removeRalphState(getRalphStateFilePath(directory, sessionId));
|
|
10754
|
-
if (lastSpawnMeta.appendSystemPrompt) {
|
|
10755
|
-
const { appendSystemPrompt: _, ...rest } = lastSpawnMeta;
|
|
10756
|
-
lastSpawnMeta = rest;
|
|
10757
|
-
}
|
|
10758
|
-
sessionMetadata = { ...sessionMetadata, ralphLoop: { active: false }, lifecycleState: "idle" };
|
|
10759
|
-
sessionService.updateMetadata(sessionMetadata);
|
|
10760
|
-
sessionService.pushMessage(
|
|
10761
|
-
{ type: "message", message: `Ralph loop stopped \u2014 ${consecutiveRalphErrors} consecutive errors. Last error: ${msg.result || "unknown"}`, level: "error" },
|
|
10762
|
-
"event"
|
|
10763
|
-
);
|
|
10764
|
-
consecutiveRalphErrors = 0;
|
|
10765
|
-
signalProcessing(false);
|
|
10766
|
-
sessionService.sendSessionEnd();
|
|
10767
|
-
break;
|
|
10768
|
-
}
|
|
10769
|
-
}
|
|
10770
|
-
} else {
|
|
10771
|
-
consecutiveRalphErrors = 0;
|
|
10772
|
-
}
|
|
10773
10542
|
if (queueLen > 0 && claudeResumeId && !trackedSession.stopped) {
|
|
10774
10543
|
setTimeout(() => processMessageQueueRef?.(), 200);
|
|
10775
|
-
} else if (claudeResumeId) {
|
|
10776
|
-
const rlState = readRalphState(getRalphStateFilePath(directory, sessionId));
|
|
10777
|
-
if (rlState) {
|
|
10778
|
-
let promiseFulfilled = false;
|
|
10779
|
-
if (rlState.completion_promise) {
|
|
10780
|
-
const promiseMatch = lastAssistantText.match(/<promise>([\s\S]*?)<\/promise>/);
|
|
10781
|
-
promiseFulfilled = !!(promiseMatch && promiseMatch[1].trim().replace(/\s+/g, " ") === rlState.completion_promise);
|
|
10782
|
-
}
|
|
10783
|
-
const maxReached = rlState.max_iterations > 0 && rlState.iteration >= rlState.max_iterations;
|
|
10784
|
-
if (promiseFulfilled || maxReached) {
|
|
10785
|
-
const isFreshMode = rlState.context_mode === "fresh" || !rlState.context_mode;
|
|
10786
|
-
removeRalphState(getRalphStateFilePath(directory, sessionId));
|
|
10787
|
-
if (lastSpawnMeta.appendSystemPrompt) {
|
|
10788
|
-
const { appendSystemPrompt: _, ...rest } = lastSpawnMeta;
|
|
10789
|
-
lastSpawnMeta = rest;
|
|
10790
|
-
}
|
|
10791
|
-
sessionMetadata = { ...sessionMetadata, ralphLoop: { active: false }, lifecycleState: "idle" };
|
|
10792
|
-
sessionService.updateMetadata(sessionMetadata);
|
|
10793
|
-
const reason = promiseFulfilled ? `Ralph loop completed at iteration ${rlState.iteration} \u2014 promise "${rlState.completion_promise}" fulfilled.` : `Ralph loop stopped \u2014 max iterations (${rlState.max_iterations}) reached.`;
|
|
10794
|
-
logger.log(`[Session ${sessionId}] ${reason}`);
|
|
10795
|
-
sessionService.pushMessage({ type: "message", message: reason }, "event");
|
|
10796
|
-
if (isFreshMode && rlState.original_resume_id) {
|
|
10797
|
-
claudeResumeId = rlState.original_resume_id;
|
|
10798
|
-
(async () => {
|
|
10799
|
-
try {
|
|
10800
|
-
if (claudeProcess && claudeProcess.exitCode === null) {
|
|
10801
|
-
isKillingClaude = true;
|
|
10802
|
-
await killAndWaitForExit2(claudeProcess);
|
|
10803
|
-
isKillingClaude = false;
|
|
10804
|
-
}
|
|
10805
|
-
if (trackedSession.stopped) return;
|
|
10806
|
-
if (isRestartingClaude || isSwitchingMode) return;
|
|
10807
|
-
const progressPath = getRalphProgressFilePath(directory, sessionId);
|
|
10808
|
-
let resumeMessage;
|
|
10809
|
-
try {
|
|
10810
|
-
if (existsSync$1(progressPath)) {
|
|
10811
|
-
const progressContent = readFileSync$1(progressPath, "utf-8").trim();
|
|
10812
|
-
if (progressContent) {
|
|
10813
|
-
resumeMessage = `<system-reminder>
|
|
10814
|
-
The Ralph Loop has completed (${reason}).
|
|
10815
|
-
Below is the progress log from all iterations:
|
|
10816
|
-
|
|
10817
|
-
${progressContent}
|
|
10818
|
-
</system-reminder>
|
|
10819
|
-
|
|
10820
|
-
The automated loop has finished. Review the progress above and let me know if you need anything else.`;
|
|
10821
|
-
unlinkSync$1(progressPath);
|
|
10822
|
-
logger.log(`[Session ${sessionId}] Injected progress file content and deleted ${progressPath}`);
|
|
10823
|
-
}
|
|
10824
|
-
}
|
|
10825
|
-
} catch (progressErr) {
|
|
10826
|
-
logger.log(`[Session ${sessionId}] Could not read/delete progress file: ${progressErr.message}`);
|
|
10827
|
-
}
|
|
10828
|
-
spawnClaude(resumeMessage);
|
|
10829
|
-
logger.log(`[Session ${sessionId}] Resumed original session ${rlState.original_resume_id}`);
|
|
10830
|
-
} catch (err) {
|
|
10831
|
-
logger.log(`[Session ${sessionId}] Error resuming original session: ${err.message}`);
|
|
10832
|
-
isKillingClaude = false;
|
|
10833
|
-
}
|
|
10834
|
-
})();
|
|
10835
|
-
} else {
|
|
10836
|
-
sessionService.sendSessionEnd();
|
|
10837
|
-
}
|
|
10838
|
-
} else {
|
|
10839
|
-
const nextIteration = rlState.iteration + 1;
|
|
10840
|
-
const iterationTimestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
10841
|
-
const isFreshMode = rlState.context_mode === "fresh" || !rlState.context_mode;
|
|
10842
|
-
const updatedRlState = { ...rlState, iteration: nextIteration, last_iteration_at: iterationTimestamp };
|
|
10843
|
-
if (isFreshMode && !rlState.original_resume_id && claudeResumeId) {
|
|
10844
|
-
updatedRlState.original_resume_id = claudeResumeId;
|
|
10845
|
-
}
|
|
10846
|
-
try {
|
|
10847
|
-
writeRalphState(getRalphStateFilePath(directory, sessionId), updatedRlState);
|
|
10848
|
-
} catch (writeErr) {
|
|
10849
|
-
logger.log(`[Session ${sessionId}] Ralph: failed to persist state (iter ${nextIteration}): ${writeErr.message} \u2014 stopping loop`);
|
|
10850
|
-
sessionService.pushMessage({ type: "message", message: `Ralph loop error: failed to persist iteration state \u2014 loop stopped. (${writeErr.message})` }, "event");
|
|
10851
|
-
removeRalphState(getRalphStateFilePath(directory, sessionId));
|
|
10852
|
-
sessionMetadata = { ...sessionMetadata, ralphLoop: { active: false }, lifecycleState: "idle" };
|
|
10853
|
-
sessionService.updateMetadata(sessionMetadata);
|
|
10854
|
-
break;
|
|
10855
|
-
}
|
|
10856
|
-
const ralphLoop = {
|
|
10857
|
-
active: true,
|
|
10858
|
-
task: rlState.task,
|
|
10859
|
-
completionPromise: rlState.completion_promise ?? "none",
|
|
10860
|
-
maxIterations: rlState.max_iterations,
|
|
10861
|
-
currentIteration: nextIteration,
|
|
10862
|
-
startedAt: rlState.started_at,
|
|
10863
|
-
cooldownSeconds: rlState.cooldown_seconds,
|
|
10864
|
-
contextMode: rlState.context_mode || "fresh",
|
|
10865
|
-
lastIterationStartedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
10866
|
-
};
|
|
10867
|
-
sessionMetadata = { ...sessionMetadata, ralphLoop };
|
|
10868
|
-
sessionService.updateMetadata(sessionMetadata);
|
|
10869
|
-
logger.log(`[Session ${sessionId}] Ralph loop iteration ${nextIteration}${rlState.max_iterations > 0 ? `/${rlState.max_iterations}` : ""}: spawning (${isFreshMode ? "fresh" : "continue"})`);
|
|
10870
|
-
const progressRelPath = `.svamp/${sessionId}/ralph-progress.md`;
|
|
10871
|
-
const prompt = buildRalphPrompt(rlState.task, updatedRlState);
|
|
10872
|
-
const ralphSysPrompt = buildRalphSystemPrompt(updatedRlState, progressRelPath);
|
|
10873
|
-
const cooldownMs = Math.max(200, rlState.cooldown_seconds * 1e3);
|
|
10874
|
-
if (isFreshMode) {
|
|
10875
|
-
isKillingClaude = true;
|
|
10876
|
-
setTimeout(async () => {
|
|
10877
|
-
try {
|
|
10878
|
-
if (claudeProcess && claudeProcess.exitCode === null) {
|
|
10879
|
-
await killAndWaitForExit2(claudeProcess);
|
|
10880
|
-
}
|
|
10881
|
-
isKillingClaude = false;
|
|
10882
|
-
if (trackedSession.stopped) return;
|
|
10883
|
-
if (isRestartingClaude || isSwitchingMode) return;
|
|
10884
|
-
claudeResumeId = void 0;
|
|
10885
|
-
userMessagePending = true;
|
|
10886
|
-
turnInitiatedByUser = true;
|
|
10887
|
-
sessionWasProcessing = true;
|
|
10888
|
-
signalProcessing(true);
|
|
10889
|
-
sessionService.pushMessage({ type: "message", message: buildIterationStatus(nextIteration, rlState.max_iterations, rlState.completion_promise) }, "event");
|
|
10890
|
-
sessionService.pushMessage(rlState.task, "user");
|
|
10891
|
-
spawnClaude(prompt, { appendSystemPrompt: ralphSysPrompt });
|
|
10892
|
-
} catch (err) {
|
|
10893
|
-
logger.log(`[Session ${sessionId}] Error in fresh Ralph iteration: ${err.message}`);
|
|
10894
|
-
isKillingClaude = false;
|
|
10895
|
-
sessionWasProcessing = false;
|
|
10896
|
-
signalProcessing(false);
|
|
10897
|
-
}
|
|
10898
|
-
}, cooldownMs);
|
|
10899
|
-
} else {
|
|
10900
|
-
setTimeout(() => {
|
|
10901
|
-
if (trackedSession.stopped) return;
|
|
10902
|
-
if (isRestartingClaude || isSwitchingMode) return;
|
|
10903
|
-
try {
|
|
10904
|
-
userMessagePending = true;
|
|
10905
|
-
turnInitiatedByUser = true;
|
|
10906
|
-
sessionWasProcessing = true;
|
|
10907
|
-
signalProcessing(true);
|
|
10908
|
-
sessionService.pushMessage({ type: "message", message: buildIterationStatus(nextIteration, rlState.max_iterations, rlState.completion_promise) }, "event");
|
|
10909
|
-
sessionService.pushMessage(rlState.task, "user");
|
|
10910
|
-
if (claudeProcess && claudeProcess.exitCode === null) {
|
|
10911
|
-
const stdinMsg = JSON.stringify({
|
|
10912
|
-
type: "user",
|
|
10913
|
-
message: { role: "user", content: prompt }
|
|
10914
|
-
});
|
|
10915
|
-
claudeProcess.stdin?.write(stdinMsg + "\n");
|
|
10916
|
-
} else {
|
|
10917
|
-
spawnClaude(prompt, { appendSystemPrompt: ralphSysPrompt });
|
|
10918
|
-
}
|
|
10919
|
-
} catch (err) {
|
|
10920
|
-
logger.log(`[Session ${sessionId}] Error in continue Ralph iteration: ${err.message}`);
|
|
10921
|
-
sessionWasProcessing = false;
|
|
10922
|
-
signalProcessing(false);
|
|
10923
|
-
}
|
|
10924
|
-
}, cooldownMs);
|
|
10925
|
-
}
|
|
10926
|
-
}
|
|
10927
|
-
} else {
|
|
10928
|
-
signalProcessing(false);
|
|
10929
|
-
sessionService.sendSessionEnd();
|
|
10930
|
-
}
|
|
10931
10544
|
} else {
|
|
10932
10545
|
signalProcessing(false);
|
|
10933
10546
|
sessionService.sendSessionEnd();
|
|
@@ -10935,7 +10548,6 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
10935
10548
|
}
|
|
10936
10549
|
sessionService.pushMessage(msg, "agent");
|
|
10937
10550
|
} else if (msg.type === "system" && msg.subtype === "init") {
|
|
10938
|
-
lastAssistantText = "";
|
|
10939
10551
|
consecutiveOverloadRetries = 0;
|
|
10940
10552
|
overloadBailedThisTurn = false;
|
|
10941
10553
|
if (!userMessagePending) {
|
|
@@ -11241,10 +10853,6 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
|
|
|
11241
10853
|
signalProcessing(false);
|
|
11242
10854
|
return;
|
|
11243
10855
|
}
|
|
11244
|
-
if (msgMeta?.ralphFreshContext && claudeResumeId) {
|
|
11245
|
-
logger.log(`[Session ${sessionId}] Ralph fresh context: clearing resumeId for fresh spawn`);
|
|
11246
|
-
claudeResumeId = void 0;
|
|
11247
|
-
}
|
|
11248
10856
|
if (msgMeta?.btw && claudeResumeId) {
|
|
11249
10857
|
logger.log(`[Session ${sessionId}] /btw side-channel: "${text.substring(0, 80)}..."`);
|
|
11250
10858
|
sessionService.pushMessage(
|
|
@@ -11539,7 +11147,6 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
|
|
|
11539
11147
|
const newCustomTitle = newMeta.customTitle;
|
|
11540
11148
|
const oldLink = sessionMetadata.sessionLink;
|
|
11541
11149
|
const newLink = newMeta.sessionLink;
|
|
11542
|
-
const prevRalphLoop = sessionMetadata.ralphLoop;
|
|
11543
11150
|
sessionMetadata = {
|
|
11544
11151
|
...newMeta,
|
|
11545
11152
|
// Daemon drives lifecycleState — don't let frontend overwrite with stale value
|
|
@@ -11549,10 +11156,7 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
|
|
|
11549
11156
|
...sessionMetadata.summary && !newMeta.summary ? { summary: sessionMetadata.summary } : {},
|
|
11550
11157
|
...sessionMetadata.sessionLink && !newMeta.sessionLink ? { sessionLink: sessionMetadata.sessionLink } : {},
|
|
11551
11158
|
// Preserve parentSessionId — set at spawn time, frontend may not track it
|
|
11552
|
-
...sessionMetadata.parentSessionId ? { parentSessionId: sessionMetadata.parentSessionId } : {}
|
|
11553
|
-
// Preserve daemon-owned ralphLoop — frontend may send stale snapshot without it,
|
|
11554
|
-
// which would wipe the active loop state and cause the bar to disappear mid-run.
|
|
11555
|
-
...prevRalphLoop ? { ralphLoop: prevRalphLoop } : {}
|
|
11159
|
+
...sessionMetadata.parentSessionId ? { parentSessionId: sessionMetadata.parentSessionId } : {}
|
|
11556
11160
|
};
|
|
11557
11161
|
const cfgPatch = {};
|
|
11558
11162
|
if (newTitle !== oldTitle) cfgPatch.title = newTitle ?? null;
|
|
@@ -11567,9 +11171,6 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
|
|
|
11567
11171
|
logger.log(`[Session ${sessionId}] Failed to persist metadata patch: ${err.message}`);
|
|
11568
11172
|
}
|
|
11569
11173
|
}
|
|
11570
|
-
if (prevRalphLoop && !newMeta.ralphLoop) {
|
|
11571
|
-
sessionService.updateMetadata(sessionMetadata);
|
|
11572
|
-
}
|
|
11573
11174
|
const queue = newMeta.messageQueue;
|
|
11574
11175
|
if (queue && queue.length > 0 && !sessionWasProcessing && !trackedSession.stopped) {
|
|
11575
11176
|
setTimeout(() => {
|
|
@@ -11709,49 +11310,20 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
|
|
|
11709
11310
|
sessionService.pushMessage(next.displayText || next.text, "user");
|
|
11710
11311
|
userMessagePending = true;
|
|
11711
11312
|
turnInitiatedByUser = true;
|
|
11712
|
-
|
|
11713
|
-
|
|
11714
|
-
|
|
11715
|
-
|
|
11716
|
-
|
|
11717
|
-
|
|
11718
|
-
|
|
11719
|
-
|
|
11720
|
-
|
|
11721
|
-
...rlState,
|
|
11722
|
-
original_resume_id: claudeResumeId
|
|
11723
|
-
});
|
|
11724
|
-
}
|
|
11725
|
-
isKillingClaude = true;
|
|
11726
|
-
await killAndWaitForExit2(claudeProcess);
|
|
11727
|
-
isKillingClaude = false;
|
|
11728
|
-
if (trackedSession?.stopped) return;
|
|
11729
|
-
if (isRestartingClaude || isSwitchingMode) return;
|
|
11730
|
-
claudeResumeId = void 0;
|
|
11731
|
-
spawnClaude(next.text, queueMeta);
|
|
11732
|
-
} catch (err) {
|
|
11733
|
-
logger.log(`[Session ${sessionId}] Error in fresh Ralph queue processing: ${err.message}`);
|
|
11734
|
-
isKillingClaude = false;
|
|
11735
|
-
sessionWasProcessing = false;
|
|
11736
|
-
signalProcessing(false);
|
|
11737
|
-
}
|
|
11738
|
-
})();
|
|
11739
|
-
} else {
|
|
11740
|
-
try {
|
|
11741
|
-
if (!claudeProcess || claudeProcess.exitCode !== null) {
|
|
11742
|
-
spawnClaude(next.text, queueMeta);
|
|
11743
|
-
} else {
|
|
11744
|
-
const stdinMsg = JSON.stringify({
|
|
11745
|
-
type: "user",
|
|
11746
|
-
message: { role: "user", content: next.text }
|
|
11747
|
-
});
|
|
11748
|
-
claudeProcess.stdin?.write(stdinMsg + "\n");
|
|
11749
|
-
}
|
|
11750
|
-
} catch (err) {
|
|
11751
|
-
logger.log(`[Session ${sessionId}] Error in processMessageQueue spawn: ${err.message}`);
|
|
11752
|
-
sessionWasProcessing = false;
|
|
11753
|
-
signalProcessing(false);
|
|
11313
|
+
try {
|
|
11314
|
+
if (!claudeProcess || claudeProcess.exitCode !== null) {
|
|
11315
|
+
spawnClaude(next.text);
|
|
11316
|
+
} else {
|
|
11317
|
+
const stdinMsg = JSON.stringify({
|
|
11318
|
+
type: "user",
|
|
11319
|
+
message: { role: "user", content: next.text }
|
|
11320
|
+
});
|
|
11321
|
+
claudeProcess.stdin?.write(stdinMsg + "\n");
|
|
11754
11322
|
}
|
|
11323
|
+
} catch (err) {
|
|
11324
|
+
logger.log(`[Session ${sessionId}] Error in processMessageQueue spawn: ${err.message}`);
|
|
11325
|
+
sessionWasProcessing = false;
|
|
11326
|
+
signalProcessing(false);
|
|
11755
11327
|
}
|
|
11756
11328
|
}
|
|
11757
11329
|
};
|
|
@@ -11825,7 +11397,7 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
|
|
|
11825
11397
|
} else if (allowedTools.has(toolName)) {
|
|
11826
11398
|
return true;
|
|
11827
11399
|
}
|
|
11828
|
-
if (
|
|
11400
|
+
if (isLoopActive(directory)) return true;
|
|
11829
11401
|
if (currentPermissionMode === "bypassPermissions" || currentPermissionMode === "yolo") return true;
|
|
11830
11402
|
if ((currentPermissionMode === "acceptEdits" || currentPermissionMode === "safe-yolo") && EDIT_TOOLS.has(toolName)) return true;
|
|
11831
11403
|
return false;
|
|
@@ -12035,7 +11607,6 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
|
|
|
12035
11607
|
const newCustomTitleAcp = newMeta.customTitle;
|
|
12036
11608
|
const oldLinkAcp = sessionMetadata.sessionLink;
|
|
12037
11609
|
const newLinkAcp = newMeta.sessionLink;
|
|
12038
|
-
const prevRalphLoop = sessionMetadata.ralphLoop;
|
|
12039
11610
|
sessionMetadata = {
|
|
12040
11611
|
...newMeta,
|
|
12041
11612
|
// Daemon drives lifecycleState — don't let frontend overwrite with stale value
|
|
@@ -12043,10 +11614,7 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
|
|
|
12043
11614
|
...sessionMetadata.summary && !newMeta.summary ? { summary: sessionMetadata.summary } : {},
|
|
12044
11615
|
...sessionMetadata.sessionLink && !newMeta.sessionLink ? { sessionLink: sessionMetadata.sessionLink } : {},
|
|
12045
11616
|
// Preserve parentSessionId — set at spawn time, frontend may not track it
|
|
12046
|
-
...sessionMetadata.parentSessionId ? { parentSessionId: sessionMetadata.parentSessionId } : {}
|
|
12047
|
-
// Preserve daemon-owned ralphLoop — frontend may send stale snapshot without it,
|
|
12048
|
-
// which would wipe the active loop state and cause the bar to disappear mid-run.
|
|
12049
|
-
...prevRalphLoop ? { ralphLoop: prevRalphLoop } : {}
|
|
11617
|
+
...sessionMetadata.parentSessionId ? { parentSessionId: sessionMetadata.parentSessionId } : {}
|
|
12050
11618
|
};
|
|
12051
11619
|
const cfgPatchAcp = {};
|
|
12052
11620
|
if (newTitleAcp !== oldTitleAcp) cfgPatchAcp.title = newTitleAcp ?? null;
|
|
@@ -12061,9 +11629,6 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
|
|
|
12061
11629
|
logger.log(`[Session ${sessionId}] Failed to persist metadata patch: ${err.message}`);
|
|
12062
11630
|
}
|
|
12063
11631
|
}
|
|
12064
|
-
if (prevRalphLoop && !newMeta.ralphLoop) {
|
|
12065
|
-
sessionService.updateMetadata(sessionMetadata);
|
|
12066
|
-
}
|
|
12067
11632
|
if (acpStopped) return;
|
|
12068
11633
|
const queue = newMeta.messageQueue;
|
|
12069
11634
|
if (queue && queue.length > 0 && sessionMetadata.lifecycleState === "idle") {
|
|
@@ -12197,7 +11762,7 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
|
|
|
12197
11762
|
const remaining = queue.slice(1);
|
|
12198
11763
|
sessionMetadata = { ...sessionMetadata, messageQueue: remaining.length > 0 ? remaining : void 0, lifecycleState: "running" };
|
|
12199
11764
|
sessionService.updateMetadata(sessionMetadata);
|
|
12200
|
-
logger.log(`[Session ${sessionId}] Processing queued message (ACP
|
|
11765
|
+
logger.log(`[Session ${sessionId}] Processing queued message (ACP loop activation): "${next.text.slice(0, 50)}..."`);
|
|
12201
11766
|
sessionService.sendKeepAlive(true);
|
|
12202
11767
|
agentBackend.sendPrompt(sessionId, next.text).catch((err) => {
|
|
12203
11768
|
logger.error(`[Session ${sessionId}] Error processing queued message: ${err.message}`);
|
|
@@ -12279,99 +11844,6 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
|
|
|
12279
11844
|
}
|
|
12280
11845
|
} catch {
|
|
12281
11846
|
}
|
|
12282
|
-
const rlState = readRalphState(getRalphStateFilePath(directory, sessionId));
|
|
12283
|
-
if (rlState) {
|
|
12284
|
-
let promiseFulfilled = false;
|
|
12285
|
-
if (rlState.completion_promise) {
|
|
12286
|
-
const promiseMatch = lastAssistantText.match(/<promise>([\s\S]*?)<\/promise>/);
|
|
12287
|
-
promiseFulfilled = !!(promiseMatch && promiseMatch[1].trim().replace(/\s+/g, " ") === rlState.completion_promise);
|
|
12288
|
-
}
|
|
12289
|
-
const maxReached = rlState.max_iterations > 0 && rlState.iteration >= rlState.max_iterations;
|
|
12290
|
-
if (promiseFulfilled || maxReached) {
|
|
12291
|
-
removeRalphState(getRalphStateFilePath(directory, sessionId));
|
|
12292
|
-
sessionMetadata = { ...sessionMetadata, ralphLoop: { active: false }, lifecycleState: "idle" };
|
|
12293
|
-
sessionService.updateMetadata(sessionMetadata);
|
|
12294
|
-
const reason = promiseFulfilled ? `Ralph loop completed at iteration ${rlState.iteration} \u2014 promise "${rlState.completion_promise}" fulfilled.` : `Ralph loop stopped \u2014 max iterations (${rlState.max_iterations}) reached.`;
|
|
12295
|
-
logger.log(`[${agentName} Session ${sessionId}] ${reason}`);
|
|
12296
|
-
sessionService.pushMessage({ type: "message", message: reason }, "event");
|
|
12297
|
-
} else {
|
|
12298
|
-
const pendingQueue = sessionMetadata.messageQueue;
|
|
12299
|
-
if (pendingQueue && pendingQueue.length > 0) {
|
|
12300
|
-
const next = pendingQueue[0];
|
|
12301
|
-
const remaining = pendingQueue.slice(1);
|
|
12302
|
-
sessionMetadata = { ...sessionMetadata, messageQueue: remaining.length > 0 ? remaining : void 0, lifecycleState: "running" };
|
|
12303
|
-
sessionService.updateMetadata(sessionMetadata);
|
|
12304
|
-
sessionService.sendKeepAlive(true);
|
|
12305
|
-
sessionService.pushMessage(next.displayText || next.text, "user");
|
|
12306
|
-
logger.log(`[${agentName} Session ${sessionId}] Processing queued message (priority over Ralph advance): "${next.text.slice(0, 50)}..."`);
|
|
12307
|
-
agentBackend.sendPrompt(sessionId, next.text).catch((err) => {
|
|
12308
|
-
logger.error(`[${agentName} Session ${sessionId}] Error processing queued message (Ralph): ${err.message}`);
|
|
12309
|
-
if (!acpStopped) {
|
|
12310
|
-
sessionMetadata = { ...sessionMetadata, lifecycleState: "idle" };
|
|
12311
|
-
sessionService.updateMetadata(sessionMetadata);
|
|
12312
|
-
sessionService.sendSessionEnd();
|
|
12313
|
-
}
|
|
12314
|
-
});
|
|
12315
|
-
return;
|
|
12316
|
-
}
|
|
12317
|
-
const nextIteration = rlState.iteration + 1;
|
|
12318
|
-
const iterationTimestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
12319
|
-
try {
|
|
12320
|
-
writeRalphState(getRalphStateFilePath(directory, sessionId), { ...rlState, iteration: nextIteration, last_iteration_at: iterationTimestamp });
|
|
12321
|
-
} catch (writeErr) {
|
|
12322
|
-
logger.log(`[${agentName} Session ${sessionId}] Ralph: failed to persist state (iter ${nextIteration}): ${writeErr.message} \u2014 stopping loop`);
|
|
12323
|
-
sessionService.pushMessage({ type: "message", message: `Ralph loop error: failed to persist iteration state \u2014 loop stopped. (${writeErr.message})` }, "event");
|
|
12324
|
-
removeRalphState(getRalphStateFilePath(directory, sessionId));
|
|
12325
|
-
sessionMetadata = { ...sessionMetadata, ralphLoop: { active: false }, lifecycleState: "idle" };
|
|
12326
|
-
sessionService.updateMetadata(sessionMetadata);
|
|
12327
|
-
return;
|
|
12328
|
-
}
|
|
12329
|
-
const ralphLoop = {
|
|
12330
|
-
active: true,
|
|
12331
|
-
task: rlState.task,
|
|
12332
|
-
completionPromise: rlState.completion_promise ?? "none",
|
|
12333
|
-
maxIterations: rlState.max_iterations,
|
|
12334
|
-
currentIteration: nextIteration,
|
|
12335
|
-
startedAt: rlState.started_at,
|
|
12336
|
-
cooldownSeconds: rlState.cooldown_seconds,
|
|
12337
|
-
contextMode: rlState.context_mode || "fresh",
|
|
12338
|
-
lastIterationStartedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
12339
|
-
};
|
|
12340
|
-
sessionMetadata = { ...sessionMetadata, ralphLoop, lifecycleState: "running" };
|
|
12341
|
-
sessionService.updateMetadata(sessionMetadata);
|
|
12342
|
-
logger.log(`[${agentName} Session ${sessionId}] Ralph loop iteration ${nextIteration}${rlState.max_iterations > 0 ? `/${rlState.max_iterations}` : ""}: spawning`);
|
|
12343
|
-
const updatedState = { ...rlState, iteration: nextIteration, context_mode: "continue" };
|
|
12344
|
-
const prompt = buildRalphPrompt(rlState.task, updatedState);
|
|
12345
|
-
const cooldownMs = Math.max(200, rlState.cooldown_seconds * 1e3);
|
|
12346
|
-
setTimeout(() => {
|
|
12347
|
-
if (acpStopped) return;
|
|
12348
|
-
const liveRlState = readRalphState(getRalphStateFilePath(directory, sessionId));
|
|
12349
|
-
if (!liveRlState) {
|
|
12350
|
-
sessionMetadata = { ...sessionMetadata, lifecycleState: "idle" };
|
|
12351
|
-
sessionService.updateMetadata(sessionMetadata);
|
|
12352
|
-
sessionService.sendKeepAlive(false);
|
|
12353
|
-
sessionService.sendSessionEnd();
|
|
12354
|
-
return;
|
|
12355
|
-
}
|
|
12356
|
-
sessionService.sendKeepAlive(true);
|
|
12357
|
-
sessionMetadata = { ...sessionMetadata, lifecycleState: "running" };
|
|
12358
|
-
sessionService.updateMetadata(sessionMetadata);
|
|
12359
|
-
sessionService.pushMessage({ type: "message", message: buildIterationStatus(nextIteration, rlState.max_iterations, rlState.completion_promise) }, "event");
|
|
12360
|
-
sessionService.pushMessage(rlState.task, "user");
|
|
12361
|
-
agentBackend.sendPrompt(sessionId, prompt).catch((err) => {
|
|
12362
|
-
logger.error(`[${agentName} Session ${sessionId}] Error in Ralph loop: ${err.message}`);
|
|
12363
|
-
if (!acpStopped) {
|
|
12364
|
-
removeRalphState(getRalphStateFilePath(directory, sessionId));
|
|
12365
|
-
sessionMetadata = { ...sessionMetadata, ralphLoop: { active: false }, lifecycleState: "idle" };
|
|
12366
|
-
sessionService.updateMetadata(sessionMetadata);
|
|
12367
|
-
sessionService.sendSessionEnd();
|
|
12368
|
-
sessionService.pushMessage({ type: "message", message: `Ralph loop error: agent failed to start turn \u2014 loop stopped. (${err.message})` }, "event");
|
|
12369
|
-
}
|
|
12370
|
-
});
|
|
12371
|
-
}, cooldownMs);
|
|
12372
|
-
return;
|
|
12373
|
-
}
|
|
12374
|
-
}
|
|
12375
11847
|
const queue = sessionMetadata.messageQueue;
|
|
12376
11848
|
if (queue && queue.length > 0) {
|
|
12377
11849
|
const next = queue[0];
|
|
@@ -12673,7 +12145,7 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
|
|
|
12673
12145
|
const specs = loadExposedTunnels();
|
|
12674
12146
|
if (specs.length === 0) return;
|
|
12675
12147
|
logger.log(`[exposed-tunnels] Restoring ${specs.length} tunnel(s) from ${EXPOSED_TUNNELS_FILE}`);
|
|
12676
|
-
const { FrpcTunnel } = await import('./frpc-
|
|
12148
|
+
const { FrpcTunnel } = await import('./frpc-CIkmTNdJ.mjs');
|
|
12677
12149
|
for (const spec of specs) {
|
|
12678
12150
|
if (tunnels.has(spec.name)) continue;
|
|
12679
12151
|
try {
|
|
@@ -12754,7 +12226,7 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
|
|
|
12754
12226
|
logger.log(`[staged-homes] sweep failed: ${err.message}`);
|
|
12755
12227
|
}
|
|
12756
12228
|
const sessionsToAutoContinue = [];
|
|
12757
|
-
const
|
|
12229
|
+
const sessionsToLoopResume = [];
|
|
12758
12230
|
if (persistedSessions.length > 0) {
|
|
12759
12231
|
logger.log(`Restoring ${persistedSessions.length} persisted session(s)...`);
|
|
12760
12232
|
for (const persisted of persistedSessions) {
|
|
@@ -12799,11 +12271,8 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
|
|
|
12799
12271
|
if (persisted.wasProcessing && persisted.claudeResumeId && !isOrphaned) {
|
|
12800
12272
|
sessionsToAutoContinue.push(persisted.sessionId);
|
|
12801
12273
|
}
|
|
12802
|
-
if (!isOrphaned && !persisted.wasProcessing) {
|
|
12803
|
-
|
|
12804
|
-
if (rlState) {
|
|
12805
|
-
sessionsToRalphResume.push({ sessionId: persisted.sessionId, directory: persisted.directory });
|
|
12806
|
-
}
|
|
12274
|
+
if (!isOrphaned && !persisted.wasProcessing && isLoopActive(persisted.directory)) {
|
|
12275
|
+
sessionsToLoopResume.push({ sessionId: persisted.sessionId, directory: persisted.directory });
|
|
12807
12276
|
}
|
|
12808
12277
|
} else {
|
|
12809
12278
|
logger.log(`Failed to restore session ${persisted.sessionId}: ${result.type}`);
|
|
@@ -12837,54 +12306,34 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
|
|
|
12837
12306
|
} else if (sessionsToAutoContinue.length > 0) {
|
|
12838
12307
|
logger.log(`Skipping auto-continue for ${sessionsToAutoContinue.length} interrupted session(s) (--no-auto-continue)`);
|
|
12839
12308
|
}
|
|
12840
|
-
if (
|
|
12841
|
-
logger.log(`Resuming
|
|
12842
|
-
for (const { sessionId, directory: sessDir } of
|
|
12309
|
+
if (sessionsToLoopResume.length > 0 && !options?.noAutoContinue) {
|
|
12310
|
+
logger.log(`Resuming loop for ${sessionsToLoopResume.length} session(s)...`);
|
|
12311
|
+
for (const { sessionId, directory: sessDir } of sessionsToLoopResume) {
|
|
12843
12312
|
try {
|
|
12844
12313
|
const tracked = Array.from(pidToTrackedSession.values()).find((s) => s.svampSessionId === sessionId);
|
|
12845
12314
|
const rpc = tracked?.sessionRPCHandlers;
|
|
12846
12315
|
if (!rpc) {
|
|
12847
|
-
logger.log(`Session ${sessionId} RPC handlers not found for
|
|
12316
|
+
logger.log(`Session ${sessionId} RPC handlers not found for loop resume`);
|
|
12848
12317
|
continue;
|
|
12849
12318
|
}
|
|
12850
|
-
const rlState = readRalphState(getRalphStateFilePath(sessDir, sessionId));
|
|
12851
|
-
if (!rlState) continue;
|
|
12852
|
-
const initDelayMs = 2e3;
|
|
12853
|
-
let resumeDelayMs = initDelayMs;
|
|
12854
|
-
if (rlState.last_iteration_at) {
|
|
12855
|
-
const lastIterTime = new Date(rlState.last_iteration_at).getTime();
|
|
12856
|
-
const cooldownMs = Math.max(200, rlState.cooldown_seconds * 1e3);
|
|
12857
|
-
const elapsedMs = Date.now() - lastIterTime;
|
|
12858
|
-
const remainingCooldownMs = cooldownMs - elapsedMs;
|
|
12859
|
-
resumeDelayMs = Math.max(initDelayMs, remainingCooldownMs + initDelayMs);
|
|
12860
|
-
logger.log(`Ralph loop for session ${sessionId}: cooldown ${rlState.cooldown_seconds}s, elapsed ${Math.round(elapsedMs / 1e3)}s, resuming in ${Math.round(resumeDelayMs / 1e3)}s`);
|
|
12861
|
-
}
|
|
12862
12319
|
setTimeout(async () => {
|
|
12863
12320
|
try {
|
|
12864
|
-
|
|
12865
|
-
|
|
12866
|
-
const isFreshMode = currentState.context_mode === "fresh" || !currentState.context_mode;
|
|
12867
|
-
const progressRelPath = `.svamp/${sessionId}/ralph-progress.md`;
|
|
12868
|
-
const prompt = buildRalphPrompt(currentState.task, currentState);
|
|
12869
|
-
const ralphSysPrompt = buildRalphSystemPrompt(currentState, progressRelPath);
|
|
12321
|
+
if (!isLoopActive(sessDir)) return;
|
|
12322
|
+
const prompt = "Continue the loop. Read LOOP.md and keep working toward the exit conditions until the Stop gate confirms completion.";
|
|
12870
12323
|
await rpc.sendMessage(
|
|
12871
12324
|
JSON.stringify({
|
|
12872
12325
|
role: "user",
|
|
12873
12326
|
content: { type: "text", text: prompt },
|
|
12874
|
-
meta: {
|
|
12875
|
-
sentFrom: "svamp-daemon-ralph-resume",
|
|
12876
|
-
appendSystemPrompt: ralphSysPrompt,
|
|
12877
|
-
...isFreshMode ? { ralphFreshContext: true } : {}
|
|
12878
|
-
}
|
|
12327
|
+
meta: { sentFrom: "svamp-daemon-loop-resume" }
|
|
12879
12328
|
})
|
|
12880
12329
|
);
|
|
12881
|
-
logger.log(`Resumed
|
|
12330
|
+
logger.log(`Resumed loop for session ${sessionId}`);
|
|
12882
12331
|
} catch (err) {
|
|
12883
|
-
logger.log(`Failed to resume
|
|
12332
|
+
logger.log(`Failed to resume loop for session ${sessionId}: ${err.message}`);
|
|
12884
12333
|
}
|
|
12885
|
-
},
|
|
12334
|
+
}, 2e3);
|
|
12886
12335
|
} catch (err) {
|
|
12887
|
-
logger.log(`Failed to find session service for
|
|
12336
|
+
logger.log(`Failed to find session service for loop resume ${sessionId}: ${err.message}`);
|
|
12888
12337
|
}
|
|
12889
12338
|
}
|
|
12890
12339
|
}
|