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.
Files changed (41) hide show
  1. package/README.md +7 -5
  2. package/bin/skills/loop/IMPLEMENTATION_PROGRESS.md +49 -0
  3. package/bin/skills/loop/SKILL.md +99 -0
  4. package/bin/skills/loop/bin/channel-core.mjs +161 -0
  5. package/bin/skills/loop/bin/channel-server.mjs +151 -0
  6. package/bin/skills/loop/bin/inject-loop.mjs +41 -0
  7. package/bin/skills/loop/bin/loop-init.mjs +128 -0
  8. package/bin/skills/loop/bin/loop-status.mjs +38 -0
  9. package/bin/skills/loop/bin/precompact.mjs +27 -0
  10. package/bin/skills/loop/bin/routine-cli.mjs +121 -0
  11. package/bin/skills/loop/bin/routine-core.mjs +126 -0
  12. package/bin/skills/loop/bin/routine-runner.mjs +125 -0
  13. package/bin/skills/loop/bin/routine-store.mjs +49 -0
  14. package/bin/skills/loop/bin/state-fp.mjs +113 -0
  15. package/bin/skills/loop/bin/stop-gate.mjs +170 -0
  16. package/bin/skills/loop/routines.process.yaml +20 -0
  17. package/bin/skills/loop/test/test-channel-core.mjs +86 -0
  18. package/bin/skills/loop/test/test-loop-gate.mjs +246 -0
  19. package/bin/skills/loop/test/test-routine-core.mjs +54 -0
  20. package/bin/skills/loop/test/test-routine-engine.mjs +122 -0
  21. package/dist/{agentCommands-PROItll1.mjs → agentCommands-muy26BZI.mjs} +2 -2
  22. package/dist/{auth-LNLCvIUL.mjs → auth-RVq9wRhV.mjs} +1 -1
  23. package/dist/{caddy-BMbX-mFX.mjs → caddy-CuTbE3NY.mjs} +1 -14
  24. package/dist/cli.mjs +76 -77
  25. package/dist/{commands-ClSwaEXa.mjs → commands-ChzeHFd3.mjs} +1 -1
  26. package/dist/{commands-CFxWo-VJ.mjs → commands-Cu96nDGv.mjs} +2 -2
  27. package/dist/{commands-x6AC67Cu.mjs → commands-EwE87XNi.mjs} +1 -1
  28. package/dist/{commands-DlINkyF8.mjs → commands-lSqc48Ib.mjs} +6 -6
  29. package/dist/{commands-Bns4qGm-.mjs → commands-rSREfaQg.mjs} +34 -42
  30. package/dist/{fleet-CFRUR0Zf.mjs → fleet-qN96q6Qb.mjs} +1 -1
  31. package/dist/{frpc-BLM1a3zD.mjs → frpc-CIkmTNdJ.mjs} +2 -15
  32. package/dist/{headlessCli-DmyX9JHV.mjs → headlessCli-BVcAcLr1.mjs} +2 -2
  33. package/dist/index.mjs +1 -1
  34. package/dist/package-B7S5w1VE.mjs +63 -0
  35. package/dist/{run-W3GQKGcB.mjs → run-CdtYIBbd.mjs} +202 -709
  36. package/dist/{run-I7IbKfRn.mjs → run-zXRdkYtk.mjs} +1 -1
  37. package/dist/{serveCommands-B2BdjSVA.mjs → serveCommands-BZd0reEj.mjs} +5 -5
  38. package/dist/{serveManager-Dc28oGob.mjs → serveManager-lmPtmRnR.mjs} +3 -3
  39. package/dist/{sideband-DXtnQ9F-.mjs → sideband-JeID_jF-.mjs} +1 -1
  40. package/package.json +3 -3
  41. 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-BLM1a3zD.mjs');
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-DXtnQ9F-.mjs');
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, ralph_loop, etc.
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 getRalphStateFilePath(directory, sessionId) {
9184
- return join(getSessionDir(directory, sessionId), "ralph-loop.md");
9241
+ function getLoopDir(directory) {
9242
+ return join(directory, ".claude", "loop");
9185
9243
  }
9186
- function readRalphState(filePath) {
9244
+ function readLoopState(directory) {
9187
9245
  try {
9188
- if (!existsSync$1(filePath)) return null;
9189
- const content = readFileSync$1(filePath, "utf-8");
9190
- const parts = content.split(/^---$/m);
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 writeRalphState(filePath, state) {
9216
- mkdirSync(dirname(filePath), { recursive: true });
9217
- const promiseYaml = state.completion_promise === null ? "none" : state.completion_promise.includes(":") || state.completion_promise.includes('"') ? `"${state.completion_promise.replace(/"/g, '\\"')}"` : `"${state.completion_promise}"`;
9218
- const lastIterLine = state.last_iteration_at ? `
9219
- last_iteration_at: "${state.last_iteration_at}"` : "";
9220
- const contextModeLine = state.context_mode ? `
9221
- context_mode: "${state.context_mode}"` : "";
9222
- const originalResumeLine = state.original_resume_id ? `
9223
- original_resume_id: "${state.original_resume_id}"` : "";
9224
- const content = `---
9225
- iteration: ${state.iteration}
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 removeRalphState(filePath) {
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
- unlinkSync$1(filePath);
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, onRalphLoopActivated) {
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 ("ralph_loop" in patch) {
9394
- const rl = patch.ralph_loop;
9395
- if (rl && typeof rl === "object" && typeof rl.task === "string") {
9396
- const contextMode = rl.context_mode === "fresh" || rl.context_mode === "continue" ? rl.context_mode : void 0;
9397
- writeRalphState(ralphStatePath, {
9398
- iteration: typeof rl.current_iteration === "number" ? Math.max(0, rl.current_iteration - 1) : 0,
9399
- max_iterations: typeof rl.max_iterations === "number" ? rl.max_iterations : 0,
9400
- completion_promise: rl.completion_promise === "none" || rl.completion_promise === null ? null : rl.completion_promise || "DONE",
9401
- cooldown_seconds: (() => {
9402
- const cd = typeof rl.cooldown_seconds === "number" ? rl.cooldown_seconds : 1;
9403
- const isInfinite = !rl.completion_promise || rl.completion_promise === "none";
9404
- return isInfinite ? Math.max(1, cd) : cd;
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
- ralphChecker();
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
- removeRalphState(ralphStatePath);
9413
- lastRalphContent = "";
9414
- processRalphState();
9403
+ deactivateLoop(directory);
9404
+ sessionService.pushMessage({ type: "message", message: "Loop cancelled." }, "event");
9405
+ logger.log(`[svampConfig] Loop cancelled`);
9415
9406
  }
9416
- const { ralph_loop: _, ...restPatch } = patch;
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-Dc28oGob.mjs');
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 sessionMetadata.ralphLoop?.active === true;
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 (sessionMetadata.ralphLoop?.active) return true;
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
- const ralphState = readRalphState(getRalphStateFilePath(directory, sessionId));
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}] Ralph Loop stuck: mid-turn, no output for ${Math.round(elapsed / 1e3)}s, no child processes \u2014 killing to unblock next iteration`);
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: `Ralph Loop iteration appears stuck (no output for ${Math.round(elapsed / 6e4)} minutes, no active tools). Restarting to continue loop...`, level: "warning" },
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 Ralph loop to resume`);
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 (sessionMetadata.ralphLoop?.active) {
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 ralphActive = !!readRalphState(getRalphStateFilePath(directory, sessionId));
10626
- if (!turnInitiatedByUser && !ralphActive) {
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 && ralphActive) {
10652
- logger.log(`[Session ${sessionId}] SDK-initiated result during active Ralph loop \u2014 processing anyway to avoid stalling`);
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
- const queueMeta = next.ralphSystemPrompt ? { appendSystemPrompt: next.ralphSystemPrompt } : void 0;
11669
- const rlState = readRalphState(getRalphStateFilePath(directory, sessionId));
11670
- const isRalphFresh = rlState && (rlState.context_mode === "fresh" || !rlState.context_mode);
11671
- if (isRalphFresh && claudeProcess && claudeProcess.exitCode === null) {
11672
- logger.log(`[Session ${sessionId}] Ralph fresh context: killing process for fresh spawn`);
11673
- (async () => {
11674
- try {
11675
- if (!rlState.original_resume_id && claudeResumeId) {
11676
- writeRalphState(getRalphStateFilePath(directory, sessionId), {
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 (sessionMetadata.ralphLoop?.active) return true;
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 ralph activation): "${next.text.slice(0, 50)}..."`);
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-BLM1a3zD.mjs');
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 sessionsToRalphResume = [];
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
- const rlState = readRalphState(getRalphStateFilePath(persisted.directory, persisted.sessionId));
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 (sessionsToRalphResume.length > 0 && !options?.noAutoContinue) {
12797
- logger.log(`Resuming Ralph loop for ${sessionsToRalphResume.length} session(s)...`);
12798
- for (const { sessionId, directory: sessDir } of sessionsToRalphResume) {
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 Ralph resume`);
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
- const currentState = readRalphState(getRalphStateFilePath(sessDir, sessionId));
12821
- if (!currentState) return;
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 Ralph loop for session ${sessionId} at iteration ${currentState.iteration} (${isFreshMode ? "fresh" : "continue"})`);
12330
+ logger.log(`Resumed loop for session ${sessionId}`);
12838
12331
  } catch (err) {
12839
- logger.log(`Failed to resume Ralph loop for session ${sessionId}: ${err.message}`);
12332
+ logger.log(`Failed to resume loop for session ${sessionId}: ${err.message}`);
12840
12333
  }
12841
- }, resumeDelayMs);
12334
+ }, 2e3);
12842
12335
  } catch (err) {
12843
- logger.log(`Failed to find session service for Ralph resume ${sessionId}: ${err.message}`);
12336
+ logger.log(`Failed to find session service for loop resume ${sessionId}: ${err.message}`);
12844
12337
  }
12845
12338
  }
12846
12339
  }