svamp-cli 0.2.97 → 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-PROItll1.mjs → agentCommands-muy26BZI.mjs} +2 -2
- package/dist/{auth-LNLCvIUL.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-ClSwaEXa.mjs → commands-ChzeHFd3.mjs} +1 -1
- package/dist/{commands-CFxWo-VJ.mjs → commands-Cu96nDGv.mjs} +2 -2
- package/dist/{commands-x6AC67Cu.mjs → commands-EwE87XNi.mjs} +1 -1
- package/dist/{commands-DlINkyF8.mjs → commands-lSqc48Ib.mjs} +6 -6
- package/dist/{commands-Bns4qGm-.mjs → commands-rSREfaQg.mjs} +34 -42
- package/dist/{fleet-CFRUR0Zf.mjs → fleet-qN96q6Qb.mjs} +1 -1
- package/dist/{frpc-BLM1a3zD.mjs → frpc-CIkmTNdJ.mjs} +2 -15
- package/dist/{headlessCli-DmyX9JHV.mjs → headlessCli-BVcAcLr1.mjs} +2 -2
- package/dist/index.mjs +1 -1
- package/dist/package-B7S5w1VE.mjs +63 -0
- package/dist/{run-W3GQKGcB.mjs → run-CdtYIBbd.mjs} +202 -709
- package/dist/{run-I7IbKfRn.mjs → run-zXRdkYtk.mjs} +1 -1
- package/dist/{serveCommands-B2BdjSVA.mjs → serveCommands-BZd0reEj.mjs} +5 -5
- package/dist/{serveManager-Dc28oGob.mjs → serveManager-lmPtmRnR.mjs} +3 -3
- package/dist/{sideband-DXtnQ9F-.mjs → sideband-JeID_jF-.mjs} +1 -1
- package/package.json +3 -3
- package/dist/package-DG-a1zOR.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';
|
|
@@ -1089,6 +1089,30 @@ async function mintRealtimeEphemeralKey(baseUrl, apiKey, opts) {
|
|
|
1089
1089
|
clearTimeout(timer);
|
|
1090
1090
|
}
|
|
1091
1091
|
}
|
|
1092
|
+
async function mintTranscriptionEphemeralKey(baseUrl, apiKey, opts) {
|
|
1093
|
+
const realtimeBase = baseUrl || "https://api.openai.com";
|
|
1094
|
+
const ctrl = new AbortController();
|
|
1095
|
+
const timer = setTimeout(() => ctrl.abort(), 15e3);
|
|
1096
|
+
try {
|
|
1097
|
+
const response = await fetch(`${realtimeBase}/v1/realtime/client_secrets`, {
|
|
1098
|
+
method: "POST",
|
|
1099
|
+
headers: { "Authorization": `Bearer ${apiKey}`, "Content-Type": "application/json" },
|
|
1100
|
+
body: JSON.stringify({
|
|
1101
|
+
session: {
|
|
1102
|
+
type: "transcription",
|
|
1103
|
+
audio: { input: { transcription: { model: opts.model || "gpt-realtime-whisper", ...opts.language ? { language: opts.language } : {} } } }
|
|
1104
|
+
}
|
|
1105
|
+
}),
|
|
1106
|
+
signal: ctrl.signal
|
|
1107
|
+
});
|
|
1108
|
+
if (!response.ok) throw new Error(`OpenAI client_secrets error: ${response.status}: ${(await response.text().catch(() => "")).slice(0, 200)}`);
|
|
1109
|
+
const result = await response.json();
|
|
1110
|
+
if (!result.value) throw new Error("client_secrets returned no value");
|
|
1111
|
+
return result.value;
|
|
1112
|
+
} finally {
|
|
1113
|
+
clearTimeout(timer);
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1092
1116
|
function loadPersistedMachineMetadata(svampHomeDir) {
|
|
1093
1117
|
try {
|
|
1094
1118
|
const data = readFileSync$1(getMachineMetadataPath(svampHomeDir), "utf-8");
|
|
@@ -2053,7 +2077,7 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
2053
2077
|
const tunnels = handlers.tunnels;
|
|
2054
2078
|
if (!tunnels) throw new Error("Tunnel management not available");
|
|
2055
2079
|
if (tunnels.has(params.name)) throw new Error(`Tunnel '${params.name}' already running`);
|
|
2056
|
-
const { FrpcTunnel } = await import('./frpc-
|
|
2080
|
+
const { FrpcTunnel } = await import('./frpc-CIkmTNdJ.mjs');
|
|
2057
2081
|
const tunnel = new FrpcTunnel({
|
|
2058
2082
|
name: params.name,
|
|
2059
2083
|
ports: params.ports,
|
|
@@ -2263,6 +2287,26 @@ QUESTION: ${params.question || "Summarize this concisely."}` }
|
|
|
2263
2287
|
return { success: false, error: error instanceof Error ? error.message : "Subagent request failed" };
|
|
2264
2288
|
}
|
|
2265
2289
|
},
|
|
2290
|
+
// Mint an ephemeral transcription token for push-to-talk dictation (ThumbDock).
|
|
2291
|
+
// Server-side with the machine's OpenAI key — the browser holds no key after
|
|
2292
|
+
// Option A. The browser uses the returned ephemeral secret for its WebRTC leg.
|
|
2293
|
+
wiseMintTranscriptionToken: async (params, context) => {
|
|
2294
|
+
trackInbound();
|
|
2295
|
+
if (context && (!context.user || context.user.is_anonymous)) {
|
|
2296
|
+
return { success: false, error: "Sign in to use dictation." };
|
|
2297
|
+
}
|
|
2298
|
+
const resolved = resolveModel({ provider: "openai" }, process.env);
|
|
2299
|
+
const misconfig = describeMisconfiguration(resolved);
|
|
2300
|
+
if (misconfig || !resolved.apiKey) {
|
|
2301
|
+
return { success: false, error: misconfig || "Dictation is not configured: no OpenAI API key on this machine." };
|
|
2302
|
+
}
|
|
2303
|
+
try {
|
|
2304
|
+
const clientSecret = await mintTranscriptionEphemeralKey(resolved.baseUrl, resolved.apiKey, { model: params.model, language: params.language });
|
|
2305
|
+
return { success: true, clientSecret };
|
|
2306
|
+
} catch (error) {
|
|
2307
|
+
return { success: false, error: error instanceof Error ? error.message : "Failed to mint transcription token" };
|
|
2308
|
+
}
|
|
2309
|
+
},
|
|
2266
2310
|
// Text WISE turn. GLOBAL (machine-manager) by default; pass sessionId to
|
|
2267
2311
|
// scope to a session. Runs server-side and returns the reply synchronously.
|
|
2268
2312
|
wiseAsk: async (params, context) => {
|
|
@@ -2294,7 +2338,7 @@ QUESTION: ${params.question || "Summarize this concisely."}` }
|
|
|
2294
2338
|
}
|
|
2295
2339
|
const deps = buildSessionDeps(rpc, { cwd, ownerEmail: owner });
|
|
2296
2340
|
const sender = { name: context?.user?.email || context?.user?.id || "user", kind: "user", verified: true };
|
|
2297
|
-
const { toolsForRole } = await import('./sideband-
|
|
2341
|
+
const { toolsForRole } = await import('./sideband-JeID_jF-.mjs');
|
|
2298
2342
|
const r2 = await runWiseAgent({ message: params.message, sender, config: { tools: toolsForRole(role2) }, deps, transport, model: resolved.model });
|
|
2299
2343
|
return fmt(r2);
|
|
2300
2344
|
}
|
|
@@ -3275,7 +3319,7 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
|
|
|
3275
3319
|
},
|
|
3276
3320
|
/**
|
|
3277
3321
|
* Patch the session config file (.svamp/{sessionId}/config.json).
|
|
3278
|
-
* Used by the frontend to set title, session_link,
|
|
3322
|
+
* Used by the frontend to set title, session_link, loop activation, etc.
|
|
3279
3323
|
* Null values remove keys from the config.
|
|
3280
3324
|
*/
|
|
3281
3325
|
updateConfig: async (patch, context) => {
|
|
@@ -9051,6 +9095,20 @@ async function ensureAutoInstalledSkills(logger) {
|
|
|
9051
9095
|
}
|
|
9052
9096
|
},
|
|
9053
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")
|
|
9054
9112
|
}
|
|
9055
9113
|
];
|
|
9056
9114
|
for (const task of tasks) {
|
|
@@ -9180,72 +9238,58 @@ function writeSvampConfig(configPath, config) {
|
|
|
9180
9238
|
renameSync(tmpPath, configPath);
|
|
9181
9239
|
return content;
|
|
9182
9240
|
}
|
|
9183
|
-
function
|
|
9184
|
-
return join(
|
|
9241
|
+
function getLoopDir(directory) {
|
|
9242
|
+
return join(directory, ".claude", "loop");
|
|
9185
9243
|
}
|
|
9186
|
-
function
|
|
9244
|
+
function readLoopState(directory) {
|
|
9187
9245
|
try {
|
|
9188
|
-
|
|
9189
|
-
|
|
9190
|
-
|
|
9191
|
-
if (parts.length < 3) return null;
|
|
9192
|
-
const frontmatter = parts[1];
|
|
9193
|
-
const task = parts.slice(2).join("---").trim();
|
|
9194
|
-
if (!task) return null;
|
|
9195
|
-
const fields = {};
|
|
9196
|
-
for (const line of frontmatter.split("\n")) {
|
|
9197
|
-
const match = line.match(/^(\w+):\s*(.*)$/);
|
|
9198
|
-
if (match) fields[match[1]] = match[2].replace(/^["']|["']$/g, "");
|
|
9199
|
-
}
|
|
9200
|
-
return {
|
|
9201
|
-
iteration: parseInt(fields.iteration || "1", 10) || 1,
|
|
9202
|
-
max_iterations: parseInt(fields.max_iterations || "0", 10) || 0,
|
|
9203
|
-
completion_promise: fields.completion_promise === "none" ? null : fields.completion_promise || "DONE",
|
|
9204
|
-
cooldown_seconds: parseInt(fields.cooldown_seconds || "1", 10) || 1,
|
|
9205
|
-
started_at: fields.started_at || (/* @__PURE__ */ new Date()).toISOString(),
|
|
9206
|
-
last_iteration_at: fields.last_iteration_at || void 0,
|
|
9207
|
-
context_mode: fields.context_mode === "fresh" || fields.context_mode === "continue" ? fields.context_mode : void 0,
|
|
9208
|
-
original_resume_id: fields.original_resume_id || void 0,
|
|
9209
|
-
task
|
|
9210
|
-
};
|
|
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"));
|
|
9211
9249
|
} catch {
|
|
9212
9250
|
return null;
|
|
9213
9251
|
}
|
|
9214
9252
|
}
|
|
9215
|
-
function
|
|
9216
|
-
|
|
9217
|
-
|
|
9218
|
-
|
|
9219
|
-
|
|
9220
|
-
const
|
|
9221
|
-
|
|
9222
|
-
|
|
9223
|
-
|
|
9224
|
-
const
|
|
9225
|
-
|
|
9226
|
-
max_iterations: ${state.max_iterations}
|
|
9227
|
-
completion_promise: ${promiseYaml}
|
|
9228
|
-
cooldown_seconds: ${state.cooldown_seconds}
|
|
9229
|
-
started_at: "${state.started_at}"${lastIterLine}${contextModeLine}${originalResumeLine}
|
|
9230
|
-
---
|
|
9231
|
-
|
|
9232
|
-
${state.task}
|
|
9233
|
-
`;
|
|
9234
|
-
const tmpPath = `${filePath}.tmp`;
|
|
9235
|
-
writeFileSync(tmpPath, content);
|
|
9236
|
-
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";
|
|
9256
|
+
}
|
|
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;
|
|
9237
9264
|
}
|
|
9238
|
-
function
|
|
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) {
|
|
9239
9278
|
try {
|
|
9240
|
-
|
|
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);
|
|
9241
9287
|
} catch {
|
|
9242
9288
|
}
|
|
9243
9289
|
}
|
|
9244
|
-
function createSvampConfigChecker(directory, sessionId, getMetadata, setMetadata, sessionService, logger,
|
|
9290
|
+
function createSvampConfigChecker(directory, sessionId, getMetadata, setMetadata, sessionService, logger, onLoopActivated) {
|
|
9245
9291
|
const configPath = getSvampConfigPath(directory, sessionId);
|
|
9246
|
-
const ralphStatePath = getRalphStateFilePath(directory, sessionId);
|
|
9247
9292
|
let lastConfigContent = "";
|
|
9248
|
-
let lastRalphContent = "";
|
|
9249
9293
|
if (existsSync$1(configPath)) {
|
|
9250
9294
|
try {
|
|
9251
9295
|
lastConfigContent = readFileSync$1(configPath, "utf-8");
|
|
@@ -9256,13 +9300,6 @@ function createSvampConfigChecker(directory, sessionId, getMetadata, setMetadata
|
|
|
9256
9300
|
} catch {
|
|
9257
9301
|
}
|
|
9258
9302
|
}
|
|
9259
|
-
if (existsSync$1(ralphStatePath)) {
|
|
9260
|
-
try {
|
|
9261
|
-
lastRalphContent = readFileSync$1(ralphStatePath, "utf-8");
|
|
9262
|
-
} catch {
|
|
9263
|
-
}
|
|
9264
|
-
}
|
|
9265
|
-
let needsInitialRalphProcess = !!lastRalphContent;
|
|
9266
9303
|
function processConfig(config, meta) {
|
|
9267
9304
|
if (typeof config.title === "string" && config.title.trim()) {
|
|
9268
9305
|
const newTitle = config.title.trim();
|
|
@@ -9307,56 +9344,6 @@ function createSvampConfigChecker(directory, sessionId, getMetadata, setMetadata
|
|
|
9307
9344
|
}
|
|
9308
9345
|
}
|
|
9309
9346
|
}
|
|
9310
|
-
function processRalphState() {
|
|
9311
|
-
const meta = getMetadata();
|
|
9312
|
-
const prevRalph = meta.ralphLoop;
|
|
9313
|
-
const state = readRalphState(ralphStatePath);
|
|
9314
|
-
if (state) {
|
|
9315
|
-
const ralphLoop = {
|
|
9316
|
-
active: true,
|
|
9317
|
-
task: state.task,
|
|
9318
|
-
completionPromise: state.completion_promise ?? "none",
|
|
9319
|
-
maxIterations: state.max_iterations,
|
|
9320
|
-
currentIteration: state.iteration,
|
|
9321
|
-
startedAt: state.started_at,
|
|
9322
|
-
cooldownSeconds: state.cooldown_seconds,
|
|
9323
|
-
contextMode: state.context_mode || "fresh",
|
|
9324
|
-
lastIterationStartedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
9325
|
-
};
|
|
9326
|
-
if (!prevRalph?.active) {
|
|
9327
|
-
const progressRelPath = `.svamp/${sessionId}/ralph-progress.md`;
|
|
9328
|
-
const prompt = buildRalphPrompt(state.task, state);
|
|
9329
|
-
const ralphSysPrompt = buildRalphSystemPrompt(state, progressRelPath);
|
|
9330
|
-
const existingQueue = getMetadata().messageQueue || [];
|
|
9331
|
-
setMetadata((m) => ({
|
|
9332
|
-
...m,
|
|
9333
|
-
ralphLoop,
|
|
9334
|
-
messageQueue: [...existingQueue, {
|
|
9335
|
-
id: randomUUID$1(),
|
|
9336
|
-
text: prompt,
|
|
9337
|
-
displayText: state.task,
|
|
9338
|
-
createdAt: Date.now(),
|
|
9339
|
-
ralphSystemPrompt: ralphSysPrompt
|
|
9340
|
-
}]
|
|
9341
|
-
}));
|
|
9342
|
-
sessionService.pushMessage(
|
|
9343
|
-
{ type: "message", message: buildIterationStatus(state.iteration + 1, state.max_iterations, state.completion_promise) },
|
|
9344
|
-
"event"
|
|
9345
|
-
);
|
|
9346
|
-
logger.log(`[svampConfig] Ralph loop started/resumed at iteration ${state.iteration + 1}: "${state.task.slice(0, 50)}..."`);
|
|
9347
|
-
onRalphLoopActivated?.();
|
|
9348
|
-
} else if (prevRalph.currentIteration !== ralphLoop.currentIteration || prevRalph.task !== ralphLoop.task) {
|
|
9349
|
-
setMetadata((m) => ({ ...m, ralphLoop }));
|
|
9350
|
-
}
|
|
9351
|
-
} else if (prevRalph?.active) {
|
|
9352
|
-
setMetadata((m) => ({ ...m, ralphLoop: { active: false } }));
|
|
9353
|
-
sessionService.pushMessage(
|
|
9354
|
-
{ type: "message", message: `Ralph loop cancelled at iteration ${prevRalph.currentIteration}.` },
|
|
9355
|
-
"event"
|
|
9356
|
-
);
|
|
9357
|
-
logger.log(`[svampConfig] Ralph loop state file removed \u2014 cancelled at iteration ${prevRalph.currentIteration}`);
|
|
9358
|
-
}
|
|
9359
|
-
}
|
|
9360
9347
|
const configChecker = () => {
|
|
9361
9348
|
try {
|
|
9362
9349
|
if (existsSync$1(configPath)) {
|
|
@@ -9370,50 +9357,54 @@ function createSvampConfigChecker(directory, sessionId, getMetadata, setMetadata
|
|
|
9370
9357
|
} catch {
|
|
9371
9358
|
}
|
|
9372
9359
|
};
|
|
9373
|
-
const ralphChecker = () => {
|
|
9374
|
-
try {
|
|
9375
|
-
if (existsSync$1(ralphStatePath)) {
|
|
9376
|
-
const content = readFileSync$1(ralphStatePath, "utf-8");
|
|
9377
|
-
if (content !== lastRalphContent) {
|
|
9378
|
-
lastRalphContent = content;
|
|
9379
|
-
processRalphState();
|
|
9380
|
-
}
|
|
9381
|
-
} else if (lastRalphContent) {
|
|
9382
|
-
lastRalphContent = "";
|
|
9383
|
-
processRalphState();
|
|
9384
|
-
}
|
|
9385
|
-
} catch {
|
|
9386
|
-
}
|
|
9387
|
-
};
|
|
9388
9360
|
const checker = () => {
|
|
9389
9361
|
configChecker();
|
|
9390
|
-
ralphChecker();
|
|
9391
9362
|
};
|
|
9392
9363
|
const writeConfig = (patch) => {
|
|
9393
|
-
if ("
|
|
9394
|
-
const
|
|
9395
|
-
if (
|
|
9396
|
-
const
|
|
9397
|
-
|
|
9398
|
-
|
|
9399
|
-
|
|
9400
|
-
|
|
9401
|
-
|
|
9402
|
-
|
|
9403
|
-
|
|
9404
|
-
|
|
9405
|
-
})(),
|
|
9406
|
-
started_at: rl.started_at || (/* @__PURE__ */ new Date()).toISOString(),
|
|
9407
|
-
context_mode: contextMode,
|
|
9408
|
-
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
|
|
9409
9376
|
});
|
|
9410
|
-
|
|
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
|
+
}
|
|
9411
9402
|
} else {
|
|
9412
|
-
|
|
9413
|
-
|
|
9414
|
-
|
|
9403
|
+
deactivateLoop(directory);
|
|
9404
|
+
sessionService.pushMessage({ type: "message", message: "Loop cancelled." }, "event");
|
|
9405
|
+
logger.log(`[svampConfig] Loop cancelled`);
|
|
9415
9406
|
}
|
|
9416
|
-
const {
|
|
9407
|
+
const { loop: _, ...restPatch } = patch;
|
|
9417
9408
|
patch = restPatch;
|
|
9418
9409
|
}
|
|
9419
9410
|
if (Object.keys(patch).length > 0) {
|
|
@@ -9432,32 +9423,11 @@ function createSvampConfigChecker(directory, sessionId, getMetadata, setMetadata
|
|
|
9432
9423
|
mkdirSync(configDir, { recursive: true });
|
|
9433
9424
|
watcher = watch(configDir, (eventType, filename) => {
|
|
9434
9425
|
if (filename === "config.json") configChecker();
|
|
9435
|
-
if (filename === "ralph-loop.md") ralphChecker();
|
|
9436
9426
|
});
|
|
9437
9427
|
watcher.on("error", () => {
|
|
9438
9428
|
});
|
|
9439
9429
|
} catch {
|
|
9440
9430
|
}
|
|
9441
|
-
if (needsInitialRalphProcess) {
|
|
9442
|
-
const state = readRalphState(ralphStatePath);
|
|
9443
|
-
if (state) {
|
|
9444
|
-
setMetadata((m) => ({
|
|
9445
|
-
...m,
|
|
9446
|
-
ralphLoop: {
|
|
9447
|
-
active: true,
|
|
9448
|
-
task: state.task,
|
|
9449
|
-
completionPromise: state.completion_promise ?? "none",
|
|
9450
|
-
maxIterations: state.max_iterations,
|
|
9451
|
-
currentIteration: state.iteration,
|
|
9452
|
-
startedAt: state.started_at,
|
|
9453
|
-
cooldownSeconds: state.cooldown_seconds,
|
|
9454
|
-
contextMode: state.context_mode || "fresh",
|
|
9455
|
-
lastIterationStartedAt: state.last_iteration_at || (/* @__PURE__ */ new Date()).toISOString()
|
|
9456
|
-
}
|
|
9457
|
-
}));
|
|
9458
|
-
logger.log(`[svampConfig] Ralph loop state restored (iteration ${state.iteration}): "${state.task.slice(0, 50)}..."`);
|
|
9459
|
-
}
|
|
9460
|
-
}
|
|
9461
9431
|
return {
|
|
9462
9432
|
check: checker,
|
|
9463
9433
|
cleanup: () => {
|
|
@@ -9466,133 +9436,6 @@ function createSvampConfigChecker(directory, sessionId, getMetadata, setMetadata
|
|
|
9466
9436
|
writeConfig
|
|
9467
9437
|
};
|
|
9468
9438
|
}
|
|
9469
|
-
function buildIterationStatus(iteration, maxIterations, completionPromise) {
|
|
9470
|
-
const iterStr = maxIterations > 0 ? `${iteration}/${maxIterations}` : `${iteration}`;
|
|
9471
|
-
if (completionPromise) {
|
|
9472
|
-
return `Ralph iteration ${iterStr} | To stop: output <promise>${completionPromise}</promise>`;
|
|
9473
|
-
}
|
|
9474
|
-
return `Ralph iteration ${iterStr} | Manual stop only`;
|
|
9475
|
-
}
|
|
9476
|
-
function buildRalphSystemPrompt(state, progressFilePath) {
|
|
9477
|
-
const isFresh = state.context_mode === "fresh" || !state.context_mode;
|
|
9478
|
-
if (isFresh && progressFilePath) {
|
|
9479
|
-
return [
|
|
9480
|
-
"# Ralph Loop \u2014 Fresh Context Mode",
|
|
9481
|
-
"",
|
|
9482
|
-
"You are an autonomous coding agent in an automated loop.",
|
|
9483
|
-
"Each iteration spawns a fresh process \u2014 you have NO memory of previous iterations.",
|
|
9484
|
-
"",
|
|
9485
|
-
"## Your Workflow",
|
|
9486
|
-
"",
|
|
9487
|
-
`1. Read the progress file at \`${progressFilePath}\` (check Patterns section first)`,
|
|
9488
|
-
"2. Check workspace files and git history (`git log --oneline -10`) to understand current state",
|
|
9489
|
-
"3. Pick the next incomplete task and implement it",
|
|
9490
|
-
"4. Run quality checks (tests, typecheck, build \u2014 whatever the project requires)",
|
|
9491
|
-
"5. Commit your changes with a clear commit message",
|
|
9492
|
-
`6. Update \`${progressFilePath}\` with your progress (ALWAYS append, never replace)`,
|
|
9493
|
-
"",
|
|
9494
|
-
"## Progress Report Format",
|
|
9495
|
-
"",
|
|
9496
|
-
`APPEND to \`${progressFilePath}\`:`,
|
|
9497
|
-
"```",
|
|
9498
|
-
"## [Date/Time] \u2014 What was done",
|
|
9499
|
-
"- What was implemented",
|
|
9500
|
-
"- Files changed",
|
|
9501
|
-
"- **Learnings for future iterations:**",
|
|
9502
|
-
" - Patterns discovered",
|
|
9503
|
-
" - Gotchas encountered",
|
|
9504
|
-
" - Useful context",
|
|
9505
|
-
"---",
|
|
9506
|
-
"```",
|
|
9507
|
-
"",
|
|
9508
|
-
"## Consolidate Patterns",
|
|
9509
|
-
"",
|
|
9510
|
-
`If you discover a reusable pattern, add it to the \`## Patterns\` section at the TOP of \`${progressFilePath}\`.`,
|
|
9511
|
-
"Only add patterns that are general and reusable, not task-specific details.",
|
|
9512
|
-
"",
|
|
9513
|
-
"## Quality Requirements",
|
|
9514
|
-
"",
|
|
9515
|
-
"- Do NOT commit broken code",
|
|
9516
|
-
"- Keep changes focused and minimal",
|
|
9517
|
-
"- Follow existing code patterns",
|
|
9518
|
-
...state.completion_promise ? [
|
|
9519
|
-
"",
|
|
9520
|
-
"## Stop Condition",
|
|
9521
|
-
"",
|
|
9522
|
-
`To signal completion, output: <promise>${state.completion_promise}</promise>`,
|
|
9523
|
-
"",
|
|
9524
|
-
"ONLY output this when the task is FULLY and PERMANENTLY complete.",
|
|
9525
|
-
"Do NOT output it if there is ANY remaining work.",
|
|
9526
|
-
"When in doubt, do NOT output the promise \u2014 the loop will give you another turn.",
|
|
9527
|
-
"",
|
|
9528
|
-
"CRITICAL: Do NOT output a false promise to exit the loop.",
|
|
9529
|
-
"The loop is designed to continue until genuine completion. Trust the process."
|
|
9530
|
-
] : [
|
|
9531
|
-
"",
|
|
9532
|
-
"## Continuous Mode",
|
|
9533
|
-
"",
|
|
9534
|
-
"This is a continuous/infinite loop with no completion signal.",
|
|
9535
|
-
"Just do meaningful work each iteration. The loop will continue until manually cancelled.",
|
|
9536
|
-
"Focus on making progress, documenting what you did, and setting up the next iteration."
|
|
9537
|
-
]
|
|
9538
|
-
].join("\n");
|
|
9539
|
-
}
|
|
9540
|
-
return [
|
|
9541
|
-
"# Ralph Loop \u2014 Continue Mode",
|
|
9542
|
-
"",
|
|
9543
|
-
"You are in an automated loop. After this turn ends, the system will automatically",
|
|
9544
|
-
"start a new turn with the SAME PROMPT \u2014 you do NOT need to finish everything now.",
|
|
9545
|
-
"Your previous work persists in conversation history and files.",
|
|
9546
|
-
"Just do meaningful work this turn and let the loop continue.",
|
|
9547
|
-
"",
|
|
9548
|
-
...state.completion_promise ? [
|
|
9549
|
-
"## Stop Condition",
|
|
9550
|
-
"",
|
|
9551
|
-
`To signal completion, output: <promise>${state.completion_promise}</promise>`,
|
|
9552
|
-
"",
|
|
9553
|
-
"CRITICAL \u2014 Do NOT output the promise if:",
|
|
9554
|
-
'- The task says "continuously", "forever", "keep running", or "until I stop you"',
|
|
9555
|
-
"- There is ANY remaining work, follow-up, or next step you could do",
|
|
9556
|
-
"- You just finished one pass/cycle of a recurring task (the loop handles repetition)",
|
|
9557
|
-
"- You are uncertain whether the task is truly done",
|
|
9558
|
-
"",
|
|
9559
|
-
"CRITICAL: Do NOT output a false promise to exit the loop, even if you think you're",
|
|
9560
|
-
"stuck or should exit for other reasons. The loop is designed to continue until genuine",
|
|
9561
|
-
"completion. Trust the process."
|
|
9562
|
-
] : [
|
|
9563
|
-
"## Continuous Mode",
|
|
9564
|
-
"",
|
|
9565
|
-
"This is a continuous/infinite loop with no completion signal.",
|
|
9566
|
-
"Just do meaningful work this turn and let the loop continue.",
|
|
9567
|
-
"The loop will run until manually cancelled."
|
|
9568
|
-
]
|
|
9569
|
-
].join("\n");
|
|
9570
|
-
}
|
|
9571
|
-
function buildRalphPrompt(task, state) {
|
|
9572
|
-
const isFresh = state.context_mode === "fresh" || !state.context_mode;
|
|
9573
|
-
if (isFresh) {
|
|
9574
|
-
return task;
|
|
9575
|
-
}
|
|
9576
|
-
const iterStr = state.max_iterations > 0 ? `${state.iteration}/${state.max_iterations}` : `${state.iteration}`;
|
|
9577
|
-
const reminderLines = [
|
|
9578
|
-
"<system-reminder>",
|
|
9579
|
-
`Ralph Loop \u2014 Iteration ${iterStr} (Continue Mode)`,
|
|
9580
|
-
"Your conversation history persists. Continue from where you left off."
|
|
9581
|
-
];
|
|
9582
|
-
if (state.completion_promise) {
|
|
9583
|
-
reminderLines.push(`To signal completion, output EXACTLY: <promise>${state.completion_promise}</promise>`);
|
|
9584
|
-
reminderLines.push("Only output the promise when the task is FULLY and PERMANENTLY complete.");
|
|
9585
|
-
reminderLines.push("Do NOT output a false promise to exit the loop.");
|
|
9586
|
-
} else {
|
|
9587
|
-
reminderLines.push("This is a continuous loop \u2014 no completion signal needed. Just do meaningful work.");
|
|
9588
|
-
}
|
|
9589
|
-
reminderLines.push("</system-reminder>");
|
|
9590
|
-
const reminder = reminderLines.join("\n");
|
|
9591
|
-
return task + "\n\n" + reminder;
|
|
9592
|
-
}
|
|
9593
|
-
function getRalphProgressFilePath(directory, sessionId) {
|
|
9594
|
-
return join(getSessionDir(directory, sessionId), "ralph-progress.md");
|
|
9595
|
-
}
|
|
9596
9439
|
function loadSessionIndex() {
|
|
9597
9440
|
if (!existsSync$1(SESSION_INDEX_FILE)) return {};
|
|
9598
9441
|
try {
|
|
@@ -9638,16 +9481,6 @@ function deletePersistedSession(sessionId) {
|
|
|
9638
9481
|
if (existsSync$1(configFile)) unlinkSync$1(configFile);
|
|
9639
9482
|
} catch {
|
|
9640
9483
|
}
|
|
9641
|
-
const ralphStateFile = getRalphStateFilePath(entry.directory, sessionId);
|
|
9642
|
-
try {
|
|
9643
|
-
if (existsSync$1(ralphStateFile)) unlinkSync$1(ralphStateFile);
|
|
9644
|
-
} catch {
|
|
9645
|
-
}
|
|
9646
|
-
const ralphProgressFile = getRalphProgressFilePath(entry.directory, sessionId);
|
|
9647
|
-
try {
|
|
9648
|
-
if (existsSync$1(ralphProgressFile)) unlinkSync$1(ralphProgressFile);
|
|
9649
|
-
} catch {
|
|
9650
|
-
}
|
|
9651
9484
|
const sessionDir = getSessionDir(entry.directory, sessionId);
|
|
9652
9485
|
try {
|
|
9653
9486
|
rmdirSync(sessionDir);
|
|
@@ -10001,7 +9834,7 @@ async function startDaemon(options) {
|
|
|
10001
9834
|
const list = loadExposedTunnels().filter((t) => t.name !== name);
|
|
10002
9835
|
saveExposedTunnels(list);
|
|
10003
9836
|
}
|
|
10004
|
-
const { ServeManager } = await import('./serveManager-
|
|
9837
|
+
const { ServeManager } = await import('./serveManager-lmPtmRnR.mjs');
|
|
10005
9838
|
const serveManager = new ServeManager(SVAMP_HOME, (msg) => logger.log(`[SERVE] ${msg}`), hyphaServerUrl);
|
|
10006
9839
|
ensureAutoInstalledSkills(logger).catch(() => {
|
|
10007
9840
|
});
|
|
@@ -10113,7 +9946,7 @@ async function startDaemon(options) {
|
|
|
10113
9946
|
}
|
|
10114
9947
|
}, shouldAutoAllow2 = function(toolName, toolInput) {
|
|
10115
9948
|
if (toolName === "AskUserQuestion") {
|
|
10116
|
-
return
|
|
9949
|
+
return isLoopActive(directory);
|
|
10117
9950
|
}
|
|
10118
9951
|
if (toolName === "Bash") {
|
|
10119
9952
|
const inputObj = toolInput;
|
|
@@ -10126,7 +9959,7 @@ async function startDaemon(options) {
|
|
|
10126
9959
|
} else if (allowedTools.has(toolName)) {
|
|
10127
9960
|
return true;
|
|
10128
9961
|
}
|
|
10129
|
-
if (
|
|
9962
|
+
if (isLoopActive(directory)) return true;
|
|
10130
9963
|
if (currentPermissionMode === "bypassPermissions" || currentPermissionMode === "yolo") return true;
|
|
10131
9964
|
if ((currentPermissionMode === "acceptEdits" || currentPermissionMode === "safe-yolo") && EDIT_TOOLS.has(toolName)) return true;
|
|
10132
9965
|
return false;
|
|
@@ -10217,10 +10050,7 @@ async function startDaemon(options) {
|
|
|
10217
10050
|
const sessionCreatedAt = persisted?.createdAt || Date.now();
|
|
10218
10051
|
let lastSpawnMeta = persisted?.spawnMeta || {};
|
|
10219
10052
|
let sessionWasProcessing = !!options2.wasProcessing;
|
|
10220
|
-
let lastAssistantText = "";
|
|
10221
10053
|
let lastMainModel;
|
|
10222
|
-
let consecutiveRalphErrors = 0;
|
|
10223
|
-
const MAX_RALPH_ERRORS = 3;
|
|
10224
10054
|
let spawnHasReceivedInit = false;
|
|
10225
10055
|
let startupFailureRetryPending = false;
|
|
10226
10056
|
let startupRetryMessage;
|
|
@@ -10243,23 +10073,23 @@ async function startDaemon(options) {
|
|
|
10243
10073
|
stuckWatchdogTimer = setInterval(() => {
|
|
10244
10074
|
if (!claudeProcess || claudeProcess.exitCode !== null) return;
|
|
10245
10075
|
if (!sessionWasProcessing) return;
|
|
10246
|
-
|
|
10247
|
-
if (!ralphState) return;
|
|
10076
|
+
if (!isLoopActive(directory)) return;
|
|
10248
10077
|
if (claudeProcess.pid && hasActiveChildren(claudeProcess.pid)) {
|
|
10249
10078
|
lastOutputTime = Date.now();
|
|
10250
10079
|
return;
|
|
10251
10080
|
}
|
|
10252
10081
|
const elapsed = Date.now() - lastOutputTime;
|
|
10253
10082
|
if (elapsed > STUCK_PROCESS_TIMEOUT_MS) {
|
|
10254
|
-
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`);
|
|
10255
10084
|
sessionService.pushMessage(
|
|
10256
|
-
{ 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" },
|
|
10257
10086
|
"event"
|
|
10258
10087
|
);
|
|
10259
10088
|
claudeProcess.kill("SIGTERM");
|
|
10260
10089
|
setTimeout(() => {
|
|
10261
|
-
if (!trackedSession.stopped) {
|
|
10262
|
-
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();
|
|
10263
10093
|
processMessageQueueRef?.();
|
|
10264
10094
|
}
|
|
10265
10095
|
}, 3e3);
|
|
@@ -10272,6 +10102,20 @@ async function startDaemon(options) {
|
|
|
10272
10102
|
stuckWatchdogTimer = null;
|
|
10273
10103
|
}
|
|
10274
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
|
+
};
|
|
10275
10119
|
const signalProcessing = (processing) => {
|
|
10276
10120
|
sessionService.sendKeepAlive(processing);
|
|
10277
10121
|
const newState = processing ? "running" : "idle";
|
|
@@ -10351,7 +10195,7 @@ async function startDaemon(options) {
|
|
|
10351
10195
|
if (options2.forceIsolation || sessionMetadata.sharing?.enabled) {
|
|
10352
10196
|
rawPermissionMode = rawPermissionMode === "default" ? "auto-approve-all" : rawPermissionMode;
|
|
10353
10197
|
}
|
|
10354
|
-
if (
|
|
10198
|
+
if (isLoopActive(directory)) {
|
|
10355
10199
|
rawPermissionMode = "bypassPermissions";
|
|
10356
10200
|
}
|
|
10357
10201
|
const permissionMode = toClaudePermissionMode(rawPermissionMode);
|
|
@@ -10578,10 +10422,6 @@ async function startDaemon(options) {
|
|
|
10578
10422
|
logger.log(`[Session ${sessionId}] Background task launched: ${label} (count=${backgroundTaskCount})`);
|
|
10579
10423
|
}
|
|
10580
10424
|
}
|
|
10581
|
-
const textBlocks = assistantContent.filter((b) => b.type === "text").map((b) => b.text);
|
|
10582
|
-
if (textBlocks.length > 0) {
|
|
10583
|
-
lastAssistantText += textBlocks.join("\n");
|
|
10584
|
-
}
|
|
10585
10425
|
}
|
|
10586
10426
|
if (msg.type === "result") {
|
|
10587
10427
|
if (msg.is_error) {
|
|
@@ -10622,8 +10462,8 @@ async function startDaemon(options) {
|
|
|
10622
10462
|
}
|
|
10623
10463
|
}
|
|
10624
10464
|
if (msg.type === "result") {
|
|
10625
|
-
const
|
|
10626
|
-
if (!turnInitiatedByUser && !
|
|
10465
|
+
const loopActive = isLoopActive(directory);
|
|
10466
|
+
if (!turnInitiatedByUser && !loopActive) {
|
|
10627
10467
|
logger.log(`[Session ${sessionId}] Skipping stale result from SDK-initiated turn`);
|
|
10628
10468
|
const hasBackgroundTasks = backgroundTaskCount > 0;
|
|
10629
10469
|
if (hasBackgroundTasks) {
|
|
@@ -10648,8 +10488,8 @@ async function startDaemon(options) {
|
|
|
10648
10488
|
turnInitiatedByUser = true;
|
|
10649
10489
|
continue;
|
|
10650
10490
|
}
|
|
10651
|
-
if (!turnInitiatedByUser &&
|
|
10652
|
-
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`);
|
|
10653
10493
|
turnInitiatedByUser = true;
|
|
10654
10494
|
}
|
|
10655
10495
|
if (msg.session_id) {
|
|
@@ -10699,191 +10539,8 @@ async function startDaemon(options) {
|
|
|
10699
10539
|
sessionService.pushMessage({ type: "session_event", message: taskInfo }, "session");
|
|
10700
10540
|
}
|
|
10701
10541
|
const queueLen = sessionMetadata.messageQueue?.length ?? 0;
|
|
10702
|
-
if (msg.is_error) {
|
|
10703
|
-
const rlStateForError = readRalphState(getRalphStateFilePath(directory, sessionId));
|
|
10704
|
-
if (rlStateForError) {
|
|
10705
|
-
consecutiveRalphErrors++;
|
|
10706
|
-
logger.log(`[Session ${sessionId}] Ralph loop: error result (consecutive=${consecutiveRalphErrors}/${MAX_RALPH_ERRORS})`);
|
|
10707
|
-
if (consecutiveRalphErrors >= MAX_RALPH_ERRORS) {
|
|
10708
|
-
logger.log(`[Session ${sessionId}] Ralph loop: ${MAX_RALPH_ERRORS} consecutive errors \u2014 stopping loop`);
|
|
10709
|
-
removeRalphState(getRalphStateFilePath(directory, sessionId));
|
|
10710
|
-
if (lastSpawnMeta.appendSystemPrompt) {
|
|
10711
|
-
const { appendSystemPrompt: _, ...rest } = lastSpawnMeta;
|
|
10712
|
-
lastSpawnMeta = rest;
|
|
10713
|
-
}
|
|
10714
|
-
sessionMetadata = { ...sessionMetadata, ralphLoop: { active: false }, lifecycleState: "idle" };
|
|
10715
|
-
sessionService.updateMetadata(sessionMetadata);
|
|
10716
|
-
sessionService.pushMessage(
|
|
10717
|
-
{ type: "message", message: `Ralph loop stopped \u2014 ${consecutiveRalphErrors} consecutive errors. Last error: ${msg.result || "unknown"}`, level: "error" },
|
|
10718
|
-
"event"
|
|
10719
|
-
);
|
|
10720
|
-
consecutiveRalphErrors = 0;
|
|
10721
|
-
signalProcessing(false);
|
|
10722
|
-
sessionService.sendSessionEnd();
|
|
10723
|
-
break;
|
|
10724
|
-
}
|
|
10725
|
-
}
|
|
10726
|
-
} else {
|
|
10727
|
-
consecutiveRalphErrors = 0;
|
|
10728
|
-
}
|
|
10729
10542
|
if (queueLen > 0 && claudeResumeId && !trackedSession.stopped) {
|
|
10730
10543
|
setTimeout(() => processMessageQueueRef?.(), 200);
|
|
10731
|
-
} else if (claudeResumeId) {
|
|
10732
|
-
const rlState = readRalphState(getRalphStateFilePath(directory, sessionId));
|
|
10733
|
-
if (rlState) {
|
|
10734
|
-
let promiseFulfilled = false;
|
|
10735
|
-
if (rlState.completion_promise) {
|
|
10736
|
-
const promiseMatch = lastAssistantText.match(/<promise>([\s\S]*?)<\/promise>/);
|
|
10737
|
-
promiseFulfilled = !!(promiseMatch && promiseMatch[1].trim().replace(/\s+/g, " ") === rlState.completion_promise);
|
|
10738
|
-
}
|
|
10739
|
-
const maxReached = rlState.max_iterations > 0 && rlState.iteration >= rlState.max_iterations;
|
|
10740
|
-
if (promiseFulfilled || maxReached) {
|
|
10741
|
-
const isFreshMode = rlState.context_mode === "fresh" || !rlState.context_mode;
|
|
10742
|
-
removeRalphState(getRalphStateFilePath(directory, sessionId));
|
|
10743
|
-
if (lastSpawnMeta.appendSystemPrompt) {
|
|
10744
|
-
const { appendSystemPrompt: _, ...rest } = lastSpawnMeta;
|
|
10745
|
-
lastSpawnMeta = rest;
|
|
10746
|
-
}
|
|
10747
|
-
sessionMetadata = { ...sessionMetadata, ralphLoop: { active: false }, lifecycleState: "idle" };
|
|
10748
|
-
sessionService.updateMetadata(sessionMetadata);
|
|
10749
|
-
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.`;
|
|
10750
|
-
logger.log(`[Session ${sessionId}] ${reason}`);
|
|
10751
|
-
sessionService.pushMessage({ type: "message", message: reason }, "event");
|
|
10752
|
-
if (isFreshMode && rlState.original_resume_id) {
|
|
10753
|
-
claudeResumeId = rlState.original_resume_id;
|
|
10754
|
-
(async () => {
|
|
10755
|
-
try {
|
|
10756
|
-
if (claudeProcess && claudeProcess.exitCode === null) {
|
|
10757
|
-
isKillingClaude = true;
|
|
10758
|
-
await killAndWaitForExit2(claudeProcess);
|
|
10759
|
-
isKillingClaude = false;
|
|
10760
|
-
}
|
|
10761
|
-
if (trackedSession.stopped) return;
|
|
10762
|
-
if (isRestartingClaude || isSwitchingMode) return;
|
|
10763
|
-
const progressPath = getRalphProgressFilePath(directory, sessionId);
|
|
10764
|
-
let resumeMessage;
|
|
10765
|
-
try {
|
|
10766
|
-
if (existsSync$1(progressPath)) {
|
|
10767
|
-
const progressContent = readFileSync$1(progressPath, "utf-8").trim();
|
|
10768
|
-
if (progressContent) {
|
|
10769
|
-
resumeMessage = `<system-reminder>
|
|
10770
|
-
The Ralph Loop has completed (${reason}).
|
|
10771
|
-
Below is the progress log from all iterations:
|
|
10772
|
-
|
|
10773
|
-
${progressContent}
|
|
10774
|
-
</system-reminder>
|
|
10775
|
-
|
|
10776
|
-
The automated loop has finished. Review the progress above and let me know if you need anything else.`;
|
|
10777
|
-
unlinkSync$1(progressPath);
|
|
10778
|
-
logger.log(`[Session ${sessionId}] Injected progress file content and deleted ${progressPath}`);
|
|
10779
|
-
}
|
|
10780
|
-
}
|
|
10781
|
-
} catch (progressErr) {
|
|
10782
|
-
logger.log(`[Session ${sessionId}] Could not read/delete progress file: ${progressErr.message}`);
|
|
10783
|
-
}
|
|
10784
|
-
spawnClaude(resumeMessage);
|
|
10785
|
-
logger.log(`[Session ${sessionId}] Resumed original session ${rlState.original_resume_id}`);
|
|
10786
|
-
} catch (err) {
|
|
10787
|
-
logger.log(`[Session ${sessionId}] Error resuming original session: ${err.message}`);
|
|
10788
|
-
isKillingClaude = false;
|
|
10789
|
-
}
|
|
10790
|
-
})();
|
|
10791
|
-
} else {
|
|
10792
|
-
sessionService.sendSessionEnd();
|
|
10793
|
-
}
|
|
10794
|
-
} else {
|
|
10795
|
-
const nextIteration = rlState.iteration + 1;
|
|
10796
|
-
const iterationTimestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
10797
|
-
const isFreshMode = rlState.context_mode === "fresh" || !rlState.context_mode;
|
|
10798
|
-
const updatedRlState = { ...rlState, iteration: nextIteration, last_iteration_at: iterationTimestamp };
|
|
10799
|
-
if (isFreshMode && !rlState.original_resume_id && claudeResumeId) {
|
|
10800
|
-
updatedRlState.original_resume_id = claudeResumeId;
|
|
10801
|
-
}
|
|
10802
|
-
try {
|
|
10803
|
-
writeRalphState(getRalphStateFilePath(directory, sessionId), updatedRlState);
|
|
10804
|
-
} catch (writeErr) {
|
|
10805
|
-
logger.log(`[Session ${sessionId}] Ralph: failed to persist state (iter ${nextIteration}): ${writeErr.message} \u2014 stopping loop`);
|
|
10806
|
-
sessionService.pushMessage({ type: "message", message: `Ralph loop error: failed to persist iteration state \u2014 loop stopped. (${writeErr.message})` }, "event");
|
|
10807
|
-
removeRalphState(getRalphStateFilePath(directory, sessionId));
|
|
10808
|
-
sessionMetadata = { ...sessionMetadata, ralphLoop: { active: false }, lifecycleState: "idle" };
|
|
10809
|
-
sessionService.updateMetadata(sessionMetadata);
|
|
10810
|
-
break;
|
|
10811
|
-
}
|
|
10812
|
-
const ralphLoop = {
|
|
10813
|
-
active: true,
|
|
10814
|
-
task: rlState.task,
|
|
10815
|
-
completionPromise: rlState.completion_promise ?? "none",
|
|
10816
|
-
maxIterations: rlState.max_iterations,
|
|
10817
|
-
currentIteration: nextIteration,
|
|
10818
|
-
startedAt: rlState.started_at,
|
|
10819
|
-
cooldownSeconds: rlState.cooldown_seconds,
|
|
10820
|
-
contextMode: rlState.context_mode || "fresh",
|
|
10821
|
-
lastIterationStartedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
10822
|
-
};
|
|
10823
|
-
sessionMetadata = { ...sessionMetadata, ralphLoop };
|
|
10824
|
-
sessionService.updateMetadata(sessionMetadata);
|
|
10825
|
-
logger.log(`[Session ${sessionId}] Ralph loop iteration ${nextIteration}${rlState.max_iterations > 0 ? `/${rlState.max_iterations}` : ""}: spawning (${isFreshMode ? "fresh" : "continue"})`);
|
|
10826
|
-
const progressRelPath = `.svamp/${sessionId}/ralph-progress.md`;
|
|
10827
|
-
const prompt = buildRalphPrompt(rlState.task, updatedRlState);
|
|
10828
|
-
const ralphSysPrompt = buildRalphSystemPrompt(updatedRlState, progressRelPath);
|
|
10829
|
-
const cooldownMs = Math.max(200, rlState.cooldown_seconds * 1e3);
|
|
10830
|
-
if (isFreshMode) {
|
|
10831
|
-
isKillingClaude = true;
|
|
10832
|
-
setTimeout(async () => {
|
|
10833
|
-
try {
|
|
10834
|
-
if (claudeProcess && claudeProcess.exitCode === null) {
|
|
10835
|
-
await killAndWaitForExit2(claudeProcess);
|
|
10836
|
-
}
|
|
10837
|
-
isKillingClaude = false;
|
|
10838
|
-
if (trackedSession.stopped) return;
|
|
10839
|
-
if (isRestartingClaude || isSwitchingMode) return;
|
|
10840
|
-
claudeResumeId = void 0;
|
|
10841
|
-
userMessagePending = true;
|
|
10842
|
-
turnInitiatedByUser = true;
|
|
10843
|
-
sessionWasProcessing = true;
|
|
10844
|
-
signalProcessing(true);
|
|
10845
|
-
sessionService.pushMessage({ type: "message", message: buildIterationStatus(nextIteration, rlState.max_iterations, rlState.completion_promise) }, "event");
|
|
10846
|
-
sessionService.pushMessage(rlState.task, "user");
|
|
10847
|
-
spawnClaude(prompt, { appendSystemPrompt: ralphSysPrompt });
|
|
10848
|
-
} catch (err) {
|
|
10849
|
-
logger.log(`[Session ${sessionId}] Error in fresh Ralph iteration: ${err.message}`);
|
|
10850
|
-
isKillingClaude = false;
|
|
10851
|
-
sessionWasProcessing = false;
|
|
10852
|
-
signalProcessing(false);
|
|
10853
|
-
}
|
|
10854
|
-
}, cooldownMs);
|
|
10855
|
-
} else {
|
|
10856
|
-
setTimeout(() => {
|
|
10857
|
-
if (trackedSession.stopped) return;
|
|
10858
|
-
if (isRestartingClaude || isSwitchingMode) return;
|
|
10859
|
-
try {
|
|
10860
|
-
userMessagePending = true;
|
|
10861
|
-
turnInitiatedByUser = true;
|
|
10862
|
-
sessionWasProcessing = true;
|
|
10863
|
-
signalProcessing(true);
|
|
10864
|
-
sessionService.pushMessage({ type: "message", message: buildIterationStatus(nextIteration, rlState.max_iterations, rlState.completion_promise) }, "event");
|
|
10865
|
-
sessionService.pushMessage(rlState.task, "user");
|
|
10866
|
-
if (claudeProcess && claudeProcess.exitCode === null) {
|
|
10867
|
-
const stdinMsg = JSON.stringify({
|
|
10868
|
-
type: "user",
|
|
10869
|
-
message: { role: "user", content: prompt }
|
|
10870
|
-
});
|
|
10871
|
-
claudeProcess.stdin?.write(stdinMsg + "\n");
|
|
10872
|
-
} else {
|
|
10873
|
-
spawnClaude(prompt, { appendSystemPrompt: ralphSysPrompt });
|
|
10874
|
-
}
|
|
10875
|
-
} catch (err) {
|
|
10876
|
-
logger.log(`[Session ${sessionId}] Error in continue Ralph iteration: ${err.message}`);
|
|
10877
|
-
sessionWasProcessing = false;
|
|
10878
|
-
signalProcessing(false);
|
|
10879
|
-
}
|
|
10880
|
-
}, cooldownMs);
|
|
10881
|
-
}
|
|
10882
|
-
}
|
|
10883
|
-
} else {
|
|
10884
|
-
signalProcessing(false);
|
|
10885
|
-
sessionService.sendSessionEnd();
|
|
10886
|
-
}
|
|
10887
10544
|
} else {
|
|
10888
10545
|
signalProcessing(false);
|
|
10889
10546
|
sessionService.sendSessionEnd();
|
|
@@ -10891,7 +10548,6 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
10891
10548
|
}
|
|
10892
10549
|
sessionService.pushMessage(msg, "agent");
|
|
10893
10550
|
} else if (msg.type === "system" && msg.subtype === "init") {
|
|
10894
|
-
lastAssistantText = "";
|
|
10895
10551
|
consecutiveOverloadRetries = 0;
|
|
10896
10552
|
overloadBailedThisTurn = false;
|
|
10897
10553
|
if (!userMessagePending) {
|
|
@@ -11197,10 +10853,6 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
|
|
|
11197
10853
|
signalProcessing(false);
|
|
11198
10854
|
return;
|
|
11199
10855
|
}
|
|
11200
|
-
if (msgMeta?.ralphFreshContext && claudeResumeId) {
|
|
11201
|
-
logger.log(`[Session ${sessionId}] Ralph fresh context: clearing resumeId for fresh spawn`);
|
|
11202
|
-
claudeResumeId = void 0;
|
|
11203
|
-
}
|
|
11204
10856
|
if (msgMeta?.btw && claudeResumeId) {
|
|
11205
10857
|
logger.log(`[Session ${sessionId}] /btw side-channel: "${text.substring(0, 80)}..."`);
|
|
11206
10858
|
sessionService.pushMessage(
|
|
@@ -11495,7 +11147,6 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
|
|
|
11495
11147
|
const newCustomTitle = newMeta.customTitle;
|
|
11496
11148
|
const oldLink = sessionMetadata.sessionLink;
|
|
11497
11149
|
const newLink = newMeta.sessionLink;
|
|
11498
|
-
const prevRalphLoop = sessionMetadata.ralphLoop;
|
|
11499
11150
|
sessionMetadata = {
|
|
11500
11151
|
...newMeta,
|
|
11501
11152
|
// Daemon drives lifecycleState — don't let frontend overwrite with stale value
|
|
@@ -11505,10 +11156,7 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
|
|
|
11505
11156
|
...sessionMetadata.summary && !newMeta.summary ? { summary: sessionMetadata.summary } : {},
|
|
11506
11157
|
...sessionMetadata.sessionLink && !newMeta.sessionLink ? { sessionLink: sessionMetadata.sessionLink } : {},
|
|
11507
11158
|
// Preserve parentSessionId — set at spawn time, frontend may not track it
|
|
11508
|
-
...sessionMetadata.parentSessionId ? { parentSessionId: sessionMetadata.parentSessionId } : {}
|
|
11509
|
-
// Preserve daemon-owned ralphLoop — frontend may send stale snapshot without it,
|
|
11510
|
-
// which would wipe the active loop state and cause the bar to disappear mid-run.
|
|
11511
|
-
...prevRalphLoop ? { ralphLoop: prevRalphLoop } : {}
|
|
11159
|
+
...sessionMetadata.parentSessionId ? { parentSessionId: sessionMetadata.parentSessionId } : {}
|
|
11512
11160
|
};
|
|
11513
11161
|
const cfgPatch = {};
|
|
11514
11162
|
if (newTitle !== oldTitle) cfgPatch.title = newTitle ?? null;
|
|
@@ -11523,9 +11171,6 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
|
|
|
11523
11171
|
logger.log(`[Session ${sessionId}] Failed to persist metadata patch: ${err.message}`);
|
|
11524
11172
|
}
|
|
11525
11173
|
}
|
|
11526
|
-
if (prevRalphLoop && !newMeta.ralphLoop) {
|
|
11527
|
-
sessionService.updateMetadata(sessionMetadata);
|
|
11528
|
-
}
|
|
11529
11174
|
const queue = newMeta.messageQueue;
|
|
11530
11175
|
if (queue && queue.length > 0 && !sessionWasProcessing && !trackedSession.stopped) {
|
|
11531
11176
|
setTimeout(() => {
|
|
@@ -11665,49 +11310,20 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
|
|
|
11665
11310
|
sessionService.pushMessage(next.displayText || next.text, "user");
|
|
11666
11311
|
userMessagePending = true;
|
|
11667
11312
|
turnInitiatedByUser = true;
|
|
11668
|
-
|
|
11669
|
-
|
|
11670
|
-
|
|
11671
|
-
|
|
11672
|
-
|
|
11673
|
-
|
|
11674
|
-
|
|
11675
|
-
|
|
11676
|
-
|
|
11677
|
-
...rlState,
|
|
11678
|
-
original_resume_id: claudeResumeId
|
|
11679
|
-
});
|
|
11680
|
-
}
|
|
11681
|
-
isKillingClaude = true;
|
|
11682
|
-
await killAndWaitForExit2(claudeProcess);
|
|
11683
|
-
isKillingClaude = false;
|
|
11684
|
-
if (trackedSession?.stopped) return;
|
|
11685
|
-
if (isRestartingClaude || isSwitchingMode) return;
|
|
11686
|
-
claudeResumeId = void 0;
|
|
11687
|
-
spawnClaude(next.text, queueMeta);
|
|
11688
|
-
} catch (err) {
|
|
11689
|
-
logger.log(`[Session ${sessionId}] Error in fresh Ralph queue processing: ${err.message}`);
|
|
11690
|
-
isKillingClaude = false;
|
|
11691
|
-
sessionWasProcessing = false;
|
|
11692
|
-
signalProcessing(false);
|
|
11693
|
-
}
|
|
11694
|
-
})();
|
|
11695
|
-
} else {
|
|
11696
|
-
try {
|
|
11697
|
-
if (!claudeProcess || claudeProcess.exitCode !== null) {
|
|
11698
|
-
spawnClaude(next.text, queueMeta);
|
|
11699
|
-
} else {
|
|
11700
|
-
const stdinMsg = JSON.stringify({
|
|
11701
|
-
type: "user",
|
|
11702
|
-
message: { role: "user", content: next.text }
|
|
11703
|
-
});
|
|
11704
|
-
claudeProcess.stdin?.write(stdinMsg + "\n");
|
|
11705
|
-
}
|
|
11706
|
-
} catch (err) {
|
|
11707
|
-
logger.log(`[Session ${sessionId}] Error in processMessageQueue spawn: ${err.message}`);
|
|
11708
|
-
sessionWasProcessing = false;
|
|
11709
|
-
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");
|
|
11710
11322
|
}
|
|
11323
|
+
} catch (err) {
|
|
11324
|
+
logger.log(`[Session ${sessionId}] Error in processMessageQueue spawn: ${err.message}`);
|
|
11325
|
+
sessionWasProcessing = false;
|
|
11326
|
+
signalProcessing(false);
|
|
11711
11327
|
}
|
|
11712
11328
|
}
|
|
11713
11329
|
};
|
|
@@ -11781,7 +11397,7 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
|
|
|
11781
11397
|
} else if (allowedTools.has(toolName)) {
|
|
11782
11398
|
return true;
|
|
11783
11399
|
}
|
|
11784
|
-
if (
|
|
11400
|
+
if (isLoopActive(directory)) return true;
|
|
11785
11401
|
if (currentPermissionMode === "bypassPermissions" || currentPermissionMode === "yolo") return true;
|
|
11786
11402
|
if ((currentPermissionMode === "acceptEdits" || currentPermissionMode === "safe-yolo") && EDIT_TOOLS.has(toolName)) return true;
|
|
11787
11403
|
return false;
|
|
@@ -11991,7 +11607,6 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
|
|
|
11991
11607
|
const newCustomTitleAcp = newMeta.customTitle;
|
|
11992
11608
|
const oldLinkAcp = sessionMetadata.sessionLink;
|
|
11993
11609
|
const newLinkAcp = newMeta.sessionLink;
|
|
11994
|
-
const prevRalphLoop = sessionMetadata.ralphLoop;
|
|
11995
11610
|
sessionMetadata = {
|
|
11996
11611
|
...newMeta,
|
|
11997
11612
|
// Daemon drives lifecycleState — don't let frontend overwrite with stale value
|
|
@@ -11999,10 +11614,7 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
|
|
|
11999
11614
|
...sessionMetadata.summary && !newMeta.summary ? { summary: sessionMetadata.summary } : {},
|
|
12000
11615
|
...sessionMetadata.sessionLink && !newMeta.sessionLink ? { sessionLink: sessionMetadata.sessionLink } : {},
|
|
12001
11616
|
// Preserve parentSessionId — set at spawn time, frontend may not track it
|
|
12002
|
-
...sessionMetadata.parentSessionId ? { parentSessionId: sessionMetadata.parentSessionId } : {}
|
|
12003
|
-
// Preserve daemon-owned ralphLoop — frontend may send stale snapshot without it,
|
|
12004
|
-
// which would wipe the active loop state and cause the bar to disappear mid-run.
|
|
12005
|
-
...prevRalphLoop ? { ralphLoop: prevRalphLoop } : {}
|
|
11617
|
+
...sessionMetadata.parentSessionId ? { parentSessionId: sessionMetadata.parentSessionId } : {}
|
|
12006
11618
|
};
|
|
12007
11619
|
const cfgPatchAcp = {};
|
|
12008
11620
|
if (newTitleAcp !== oldTitleAcp) cfgPatchAcp.title = newTitleAcp ?? null;
|
|
@@ -12017,9 +11629,6 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
|
|
|
12017
11629
|
logger.log(`[Session ${sessionId}] Failed to persist metadata patch: ${err.message}`);
|
|
12018
11630
|
}
|
|
12019
11631
|
}
|
|
12020
|
-
if (prevRalphLoop && !newMeta.ralphLoop) {
|
|
12021
|
-
sessionService.updateMetadata(sessionMetadata);
|
|
12022
|
-
}
|
|
12023
11632
|
if (acpStopped) return;
|
|
12024
11633
|
const queue = newMeta.messageQueue;
|
|
12025
11634
|
if (queue && queue.length > 0 && sessionMetadata.lifecycleState === "idle") {
|
|
@@ -12153,7 +11762,7 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
|
|
|
12153
11762
|
const remaining = queue.slice(1);
|
|
12154
11763
|
sessionMetadata = { ...sessionMetadata, messageQueue: remaining.length > 0 ? remaining : void 0, lifecycleState: "running" };
|
|
12155
11764
|
sessionService.updateMetadata(sessionMetadata);
|
|
12156
|
-
logger.log(`[Session ${sessionId}] Processing queued message (ACP
|
|
11765
|
+
logger.log(`[Session ${sessionId}] Processing queued message (ACP loop activation): "${next.text.slice(0, 50)}..."`);
|
|
12157
11766
|
sessionService.sendKeepAlive(true);
|
|
12158
11767
|
agentBackend.sendPrompt(sessionId, next.text).catch((err) => {
|
|
12159
11768
|
logger.error(`[Session ${sessionId}] Error processing queued message: ${err.message}`);
|
|
@@ -12235,99 +11844,6 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
|
|
|
12235
11844
|
}
|
|
12236
11845
|
} catch {
|
|
12237
11846
|
}
|
|
12238
|
-
const rlState = readRalphState(getRalphStateFilePath(directory, sessionId));
|
|
12239
|
-
if (rlState) {
|
|
12240
|
-
let promiseFulfilled = false;
|
|
12241
|
-
if (rlState.completion_promise) {
|
|
12242
|
-
const promiseMatch = lastAssistantText.match(/<promise>([\s\S]*?)<\/promise>/);
|
|
12243
|
-
promiseFulfilled = !!(promiseMatch && promiseMatch[1].trim().replace(/\s+/g, " ") === rlState.completion_promise);
|
|
12244
|
-
}
|
|
12245
|
-
const maxReached = rlState.max_iterations > 0 && rlState.iteration >= rlState.max_iterations;
|
|
12246
|
-
if (promiseFulfilled || maxReached) {
|
|
12247
|
-
removeRalphState(getRalphStateFilePath(directory, sessionId));
|
|
12248
|
-
sessionMetadata = { ...sessionMetadata, ralphLoop: { active: false }, lifecycleState: "idle" };
|
|
12249
|
-
sessionService.updateMetadata(sessionMetadata);
|
|
12250
|
-
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.`;
|
|
12251
|
-
logger.log(`[${agentName} Session ${sessionId}] ${reason}`);
|
|
12252
|
-
sessionService.pushMessage({ type: "message", message: reason }, "event");
|
|
12253
|
-
} else {
|
|
12254
|
-
const pendingQueue = sessionMetadata.messageQueue;
|
|
12255
|
-
if (pendingQueue && pendingQueue.length > 0) {
|
|
12256
|
-
const next = pendingQueue[0];
|
|
12257
|
-
const remaining = pendingQueue.slice(1);
|
|
12258
|
-
sessionMetadata = { ...sessionMetadata, messageQueue: remaining.length > 0 ? remaining : void 0, lifecycleState: "running" };
|
|
12259
|
-
sessionService.updateMetadata(sessionMetadata);
|
|
12260
|
-
sessionService.sendKeepAlive(true);
|
|
12261
|
-
sessionService.pushMessage(next.displayText || next.text, "user");
|
|
12262
|
-
logger.log(`[${agentName} Session ${sessionId}] Processing queued message (priority over Ralph advance): "${next.text.slice(0, 50)}..."`);
|
|
12263
|
-
agentBackend.sendPrompt(sessionId, next.text).catch((err) => {
|
|
12264
|
-
logger.error(`[${agentName} Session ${sessionId}] Error processing queued message (Ralph): ${err.message}`);
|
|
12265
|
-
if (!acpStopped) {
|
|
12266
|
-
sessionMetadata = { ...sessionMetadata, lifecycleState: "idle" };
|
|
12267
|
-
sessionService.updateMetadata(sessionMetadata);
|
|
12268
|
-
sessionService.sendSessionEnd();
|
|
12269
|
-
}
|
|
12270
|
-
});
|
|
12271
|
-
return;
|
|
12272
|
-
}
|
|
12273
|
-
const nextIteration = rlState.iteration + 1;
|
|
12274
|
-
const iterationTimestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
12275
|
-
try {
|
|
12276
|
-
writeRalphState(getRalphStateFilePath(directory, sessionId), { ...rlState, iteration: nextIteration, last_iteration_at: iterationTimestamp });
|
|
12277
|
-
} catch (writeErr) {
|
|
12278
|
-
logger.log(`[${agentName} Session ${sessionId}] Ralph: failed to persist state (iter ${nextIteration}): ${writeErr.message} \u2014 stopping loop`);
|
|
12279
|
-
sessionService.pushMessage({ type: "message", message: `Ralph loop error: failed to persist iteration state \u2014 loop stopped. (${writeErr.message})` }, "event");
|
|
12280
|
-
removeRalphState(getRalphStateFilePath(directory, sessionId));
|
|
12281
|
-
sessionMetadata = { ...sessionMetadata, ralphLoop: { active: false }, lifecycleState: "idle" };
|
|
12282
|
-
sessionService.updateMetadata(sessionMetadata);
|
|
12283
|
-
return;
|
|
12284
|
-
}
|
|
12285
|
-
const ralphLoop = {
|
|
12286
|
-
active: true,
|
|
12287
|
-
task: rlState.task,
|
|
12288
|
-
completionPromise: rlState.completion_promise ?? "none",
|
|
12289
|
-
maxIterations: rlState.max_iterations,
|
|
12290
|
-
currentIteration: nextIteration,
|
|
12291
|
-
startedAt: rlState.started_at,
|
|
12292
|
-
cooldownSeconds: rlState.cooldown_seconds,
|
|
12293
|
-
contextMode: rlState.context_mode || "fresh",
|
|
12294
|
-
lastIterationStartedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
12295
|
-
};
|
|
12296
|
-
sessionMetadata = { ...sessionMetadata, ralphLoop, lifecycleState: "running" };
|
|
12297
|
-
sessionService.updateMetadata(sessionMetadata);
|
|
12298
|
-
logger.log(`[${agentName} Session ${sessionId}] Ralph loop iteration ${nextIteration}${rlState.max_iterations > 0 ? `/${rlState.max_iterations}` : ""}: spawning`);
|
|
12299
|
-
const updatedState = { ...rlState, iteration: nextIteration, context_mode: "continue" };
|
|
12300
|
-
const prompt = buildRalphPrompt(rlState.task, updatedState);
|
|
12301
|
-
const cooldownMs = Math.max(200, rlState.cooldown_seconds * 1e3);
|
|
12302
|
-
setTimeout(() => {
|
|
12303
|
-
if (acpStopped) return;
|
|
12304
|
-
const liveRlState = readRalphState(getRalphStateFilePath(directory, sessionId));
|
|
12305
|
-
if (!liveRlState) {
|
|
12306
|
-
sessionMetadata = { ...sessionMetadata, lifecycleState: "idle" };
|
|
12307
|
-
sessionService.updateMetadata(sessionMetadata);
|
|
12308
|
-
sessionService.sendKeepAlive(false);
|
|
12309
|
-
sessionService.sendSessionEnd();
|
|
12310
|
-
return;
|
|
12311
|
-
}
|
|
12312
|
-
sessionService.sendKeepAlive(true);
|
|
12313
|
-
sessionMetadata = { ...sessionMetadata, lifecycleState: "running" };
|
|
12314
|
-
sessionService.updateMetadata(sessionMetadata);
|
|
12315
|
-
sessionService.pushMessage({ type: "message", message: buildIterationStatus(nextIteration, rlState.max_iterations, rlState.completion_promise) }, "event");
|
|
12316
|
-
sessionService.pushMessage(rlState.task, "user");
|
|
12317
|
-
agentBackend.sendPrompt(sessionId, prompt).catch((err) => {
|
|
12318
|
-
logger.error(`[${agentName} Session ${sessionId}] Error in Ralph loop: ${err.message}`);
|
|
12319
|
-
if (!acpStopped) {
|
|
12320
|
-
removeRalphState(getRalphStateFilePath(directory, sessionId));
|
|
12321
|
-
sessionMetadata = { ...sessionMetadata, ralphLoop: { active: false }, lifecycleState: "idle" };
|
|
12322
|
-
sessionService.updateMetadata(sessionMetadata);
|
|
12323
|
-
sessionService.sendSessionEnd();
|
|
12324
|
-
sessionService.pushMessage({ type: "message", message: `Ralph loop error: agent failed to start turn \u2014 loop stopped. (${err.message})` }, "event");
|
|
12325
|
-
}
|
|
12326
|
-
});
|
|
12327
|
-
}, cooldownMs);
|
|
12328
|
-
return;
|
|
12329
|
-
}
|
|
12330
|
-
}
|
|
12331
11847
|
const queue = sessionMetadata.messageQueue;
|
|
12332
11848
|
if (queue && queue.length > 0) {
|
|
12333
11849
|
const next = queue[0];
|
|
@@ -12629,7 +12145,7 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
|
|
|
12629
12145
|
const specs = loadExposedTunnels();
|
|
12630
12146
|
if (specs.length === 0) return;
|
|
12631
12147
|
logger.log(`[exposed-tunnels] Restoring ${specs.length} tunnel(s) from ${EXPOSED_TUNNELS_FILE}`);
|
|
12632
|
-
const { FrpcTunnel } = await import('./frpc-
|
|
12148
|
+
const { FrpcTunnel } = await import('./frpc-CIkmTNdJ.mjs');
|
|
12633
12149
|
for (const spec of specs) {
|
|
12634
12150
|
if (tunnels.has(spec.name)) continue;
|
|
12635
12151
|
try {
|
|
@@ -12710,7 +12226,7 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
|
|
|
12710
12226
|
logger.log(`[staged-homes] sweep failed: ${err.message}`);
|
|
12711
12227
|
}
|
|
12712
12228
|
const sessionsToAutoContinue = [];
|
|
12713
|
-
const
|
|
12229
|
+
const sessionsToLoopResume = [];
|
|
12714
12230
|
if (persistedSessions.length > 0) {
|
|
12715
12231
|
logger.log(`Restoring ${persistedSessions.length} persisted session(s)...`);
|
|
12716
12232
|
for (const persisted of persistedSessions) {
|
|
@@ -12755,11 +12271,8 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
|
|
|
12755
12271
|
if (persisted.wasProcessing && persisted.claudeResumeId && !isOrphaned) {
|
|
12756
12272
|
sessionsToAutoContinue.push(persisted.sessionId);
|
|
12757
12273
|
}
|
|
12758
|
-
if (!isOrphaned && !persisted.wasProcessing) {
|
|
12759
|
-
|
|
12760
|
-
if (rlState) {
|
|
12761
|
-
sessionsToRalphResume.push({ sessionId: persisted.sessionId, directory: persisted.directory });
|
|
12762
|
-
}
|
|
12274
|
+
if (!isOrphaned && !persisted.wasProcessing && isLoopActive(persisted.directory)) {
|
|
12275
|
+
sessionsToLoopResume.push({ sessionId: persisted.sessionId, directory: persisted.directory });
|
|
12763
12276
|
}
|
|
12764
12277
|
} else {
|
|
12765
12278
|
logger.log(`Failed to restore session ${persisted.sessionId}: ${result.type}`);
|
|
@@ -12793,54 +12306,34 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
|
|
|
12793
12306
|
} else if (sessionsToAutoContinue.length > 0) {
|
|
12794
12307
|
logger.log(`Skipping auto-continue for ${sessionsToAutoContinue.length} interrupted session(s) (--no-auto-continue)`);
|
|
12795
12308
|
}
|
|
12796
|
-
if (
|
|
12797
|
-
logger.log(`Resuming
|
|
12798
|
-
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) {
|
|
12799
12312
|
try {
|
|
12800
12313
|
const tracked = Array.from(pidToTrackedSession.values()).find((s) => s.svampSessionId === sessionId);
|
|
12801
12314
|
const rpc = tracked?.sessionRPCHandlers;
|
|
12802
12315
|
if (!rpc) {
|
|
12803
|
-
logger.log(`Session ${sessionId} RPC handlers not found for
|
|
12316
|
+
logger.log(`Session ${sessionId} RPC handlers not found for loop resume`);
|
|
12804
12317
|
continue;
|
|
12805
12318
|
}
|
|
12806
|
-
const rlState = readRalphState(getRalphStateFilePath(sessDir, sessionId));
|
|
12807
|
-
if (!rlState) continue;
|
|
12808
|
-
const initDelayMs = 2e3;
|
|
12809
|
-
let resumeDelayMs = initDelayMs;
|
|
12810
|
-
if (rlState.last_iteration_at) {
|
|
12811
|
-
const lastIterTime = new Date(rlState.last_iteration_at).getTime();
|
|
12812
|
-
const cooldownMs = Math.max(200, rlState.cooldown_seconds * 1e3);
|
|
12813
|
-
const elapsedMs = Date.now() - lastIterTime;
|
|
12814
|
-
const remainingCooldownMs = cooldownMs - elapsedMs;
|
|
12815
|
-
resumeDelayMs = Math.max(initDelayMs, remainingCooldownMs + initDelayMs);
|
|
12816
|
-
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`);
|
|
12817
|
-
}
|
|
12818
12319
|
setTimeout(async () => {
|
|
12819
12320
|
try {
|
|
12820
|
-
|
|
12821
|
-
|
|
12822
|
-
const isFreshMode = currentState.context_mode === "fresh" || !currentState.context_mode;
|
|
12823
|
-
const progressRelPath = `.svamp/${sessionId}/ralph-progress.md`;
|
|
12824
|
-
const prompt = buildRalphPrompt(currentState.task, currentState);
|
|
12825
|
-
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.";
|
|
12826
12323
|
await rpc.sendMessage(
|
|
12827
12324
|
JSON.stringify({
|
|
12828
12325
|
role: "user",
|
|
12829
12326
|
content: { type: "text", text: prompt },
|
|
12830
|
-
meta: {
|
|
12831
|
-
sentFrom: "svamp-daemon-ralph-resume",
|
|
12832
|
-
appendSystemPrompt: ralphSysPrompt,
|
|
12833
|
-
...isFreshMode ? { ralphFreshContext: true } : {}
|
|
12834
|
-
}
|
|
12327
|
+
meta: { sentFrom: "svamp-daemon-loop-resume" }
|
|
12835
12328
|
})
|
|
12836
12329
|
);
|
|
12837
|
-
logger.log(`Resumed
|
|
12330
|
+
logger.log(`Resumed loop for session ${sessionId}`);
|
|
12838
12331
|
} catch (err) {
|
|
12839
|
-
logger.log(`Failed to resume
|
|
12332
|
+
logger.log(`Failed to resume loop for session ${sessionId}: ${err.message}`);
|
|
12840
12333
|
}
|
|
12841
|
-
},
|
|
12334
|
+
}, 2e3);
|
|
12842
12335
|
} catch (err) {
|
|
12843
|
-
logger.log(`Failed to find session service for
|
|
12336
|
+
logger.log(`Failed to find session service for loop resume ${sessionId}: ${err.message}`);
|
|
12844
12337
|
}
|
|
12845
12338
|
}
|
|
12846
12339
|
}
|