svamp-cli 0.2.98 → 0.2.101

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-BULNvfKa.mjs → agentCommands-CAqLhLOH.mjs} +2 -2
  22. package/dist/{auth-BfDOBBPy.mjs → auth-CYA0e4mT.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-C9DbNFz1.mjs → commands-B2uNdsyR.mjs} +2 -2
  26. package/dist/{commands-h2Dzb5m1.mjs → commands-Bxn_4u7d.mjs} +1 -1
  27. package/dist/{commands-DIhhodx8.mjs → commands-CdxEOPUt.mjs} +34 -42
  28. package/dist/{commands-qE4ZGLzB.mjs → commands-D-3h8H0C.mjs} +6 -6
  29. package/dist/{commands-FhGCsATM.mjs → commands-DRQUzw4j.mjs} +1 -1
  30. package/dist/{fleet-Cmma7Iu-.mjs → fleet-CNF84yJV.mjs} +1 -1
  31. package/dist/{frpc-BZ4l4-os.mjs → frpc-WVnBbyjf.mjs} +2 -15
  32. package/dist/{headlessCli-xRpI9fdk.mjs → headlessCli-DcP8eawK.mjs} +2 -2
  33. package/dist/index.mjs +1 -1
  34. package/dist/package-DHxiXJ3N.mjs +63 -0
  35. package/dist/{run-DTIEcH-W.mjs → run-C7WSV8zx.mjs} +1 -1
  36. package/dist/{run-DxzG-3JD.mjs → run-CsMTSngP.mjs} +246 -709
  37. package/dist/{serveCommands-CzllIFB_.mjs → serveCommands-Can8WtLI.mjs} +5 -5
  38. package/dist/{serveManager-C6_Vloil.mjs → serveManager-DfETVSOb.mjs} +3 -3
  39. package/dist/{sideband-wPe3a3m1.mjs → sideband-C10Ni7p_.mjs} +1 -1
  40. package/package.json +3 -3
  41. package/dist/package-DD227VZO.mjs +0 -63
@@ -3,7 +3,7 @@ import fs, { mkdir as mkdir$1, readdir as readdir$1, readFile, writeFile as writ
3
3
  import { readFileSync as readFileSync$1, mkdirSync, writeFileSync, renameSync, existsSync as existsSync$1, rmSync as rmSync$1, unlinkSync as unlinkSync$1, copyFileSync, watch, rmdirSync, readdirSync as readdirSync$1 } from 'fs';
4
4
  import path__default, { join, dirname, basename, resolve } from 'path';
5
5
  import { fileURLToPath } from 'url';
6
- import { execFile, spawn as spawn$1, execSync as execSync$1 } from 'child_process';
6
+ import { execFile, spawn as spawn$1, execSync as execSync$1, spawnSync } from 'child_process';
7
7
  import { randomUUID as randomUUID$1 } from 'crypto';
8
8
  import { existsSync, readFileSync, mkdirSync as mkdirSync$1, readdirSync, writeFileSync as writeFileSync$1, renameSync as renameSync$1, rmSync, appendFileSync, unlinkSync } from 'node:fs';
9
9
  import { exec, spawn, execSync, execFile as execFile$1, execFileSync } from 'node:child_process';
@@ -433,6 +433,75 @@ function buildTools(deps, skills) {
433
433
  }
434
434
  return "(sent to the coding agent)";
435
435
  }
436
+ },
437
+ // Create routine / loop / channel. ONLY call after the caller confirmed the
438
+ // proposed config (see the propose-then-confirm instruction in context.ts).
439
+ {
440
+ name: "create_routine",
441
+ readOnly: false,
442
+ description: "Create a session-scoped routine (cron schedule or webhook \u2192 message or loop). ONLY after the caller confirmed the proposal.",
443
+ parameters: { type: "object", properties: {
444
+ name: { type: "string", description: "Short human name." },
445
+ cron: { type: "string", description: 'Cron for a schedule trigger, e.g. "0 9 * * 1-5". Omit for a webhook.' },
446
+ tz: { type: "string", description: "IANA timezone for the schedule." },
447
+ action_kind: { type: "string", enum: ["message", "loop"], description: '"message" sends text to this session; "loop" starts a self-verifying loop.' },
448
+ message: { type: "string", description: "For message action: the text/template to deliver." },
449
+ task: { type: "string", description: "For loop action: the loop task." },
450
+ oracle: { type: "string", description: "For loop action: optional pass/fail command." }
451
+ }, required: ["name", "action_kind"], additionalProperties: false },
452
+ run: async (a) => {
453
+ const cron = a?.cron ? str$1(a.cron) : "";
454
+ const routine = {
455
+ name: str$1(a?.name),
456
+ enabled: true,
457
+ trigger: cron ? { type: "schedule", cron, ...a?.tz ? { tz: str$1(a.tz) } : {} } : { type: "webhook" },
458
+ action: str$1(a?.action_kind) === "loop" ? { kind: "loop", task_template: str$1(a?.task), loop: { task: str$1(a?.task), ...a?.oracle ? { oracle: str$1(a.oracle) } : {} } } : { kind: "message", template: str$1(a?.message) }
459
+ };
460
+ const r = await deps.saveRoutine(routine);
461
+ return r.success ? `Created routine "${str$1(a?.name)}" (${r.routine?.id || "saved"}).` : `Could not create routine: ${r.error || "unknown error"}.`;
462
+ }
463
+ },
464
+ {
465
+ name: "create_loop",
466
+ readOnly: false,
467
+ description: "Start a self-verifying loop in the bound session (iterates until an evaluator + optional oracle confirm done). ONLY after the caller confirmed the proposal.",
468
+ parameters: { type: "object", properties: {
469
+ task: { type: "string", description: "What the loop should accomplish." },
470
+ criteria: { type: "string", description: "How we know it is genuinely done." },
471
+ oracle: { type: "string", description: 'Optional pass/fail command (e.g. "npm test").' },
472
+ max_iterations: { type: "number", description: "Iteration ceiling (default 20)." }
473
+ }, required: ["task"], additionalProperties: false },
474
+ run: async (a) => {
475
+ await deps.startLoop({
476
+ task: str$1(a?.task),
477
+ criteria: a?.criteria ? str$1(a.criteria) : void 0,
478
+ oracle: a?.oracle ? str$1(a.oracle) : void 0,
479
+ maxIterations: typeof a?.max_iterations === "number" ? a.max_iterations : void 0
480
+ });
481
+ return `Started a loop: "${str$1(a?.task).slice(0, 80)}".`;
482
+ }
483
+ },
484
+ {
485
+ name: "create_channel",
486
+ readOnly: false,
487
+ description: "Create an inbound channel so other users/agents can message this session (identity-tagged). ONLY after the caller confirmed the proposal.",
488
+ parameters: { type: "object", properties: {
489
+ name: { type: "string", description: "Short channel name." },
490
+ description: { type: "string", description: "What this channel is for." },
491
+ identity_mode: { type: "string", enum: ["per-key", "caller-supplied", "fixed"], description: 'How callers are identified (default "per-key").' }
492
+ }, required: ["name"], additionalProperties: false },
493
+ run: async (a) => {
494
+ const mode = a?.identity_mode ? str$1(a.identity_mode) : "per-key";
495
+ const channel = {
496
+ name: str$1(a?.name),
497
+ ...a?.description ? { description: str$1(a.description) } : {},
498
+ enabled: true,
499
+ identity: mode === "fixed" ? { mode, fixed: { name: "caller", kind: "agent" } } : { mode },
500
+ action: { kind: "message" }
501
+ };
502
+ const r = await deps.saveChannel(channel);
503
+ return r.success ? `Created channel "${str$1(a?.name)}" (${r.channel?.id || "saved"}).` : `Could not create channel: ${r.error || "unknown error"}.`;
504
+ }
436
505
  }
437
506
  ];
438
507
  }
@@ -560,10 +629,12 @@ You are WISE Agent, a fast, text-mode companion to the deep coding agent (Claude
560
629
  - use_skill \u2014 load a project skill's full steps by name, then carry them out with run_bash.
561
630
  - run_bash \u2014 run a shell command on the session's machine (when granted).
562
631
  - send_to_session \u2014 hand a clear, reformulated instruction to the deep coding agent (when granted); pass wait=true to block for its reply.
632
+ - create_routine / create_loop / create_channel \u2014 set up a scheduled/triggered routine, a self-verifying loop, or an inbound channel for this session (when granted). ALWAYS propose first and confirm before calling these (see below).
563
633
 
564
634
  # Instructions
565
635
  - Answer general questions and questions about yourself directly. Use tools only to act on the machine/session.
566
636
  - Take the cheap path: read state directly; delegate anything LONG to summarize_session \u2014 keep your own context small.
637
+ - To create a routine, loop, or channel: first restate the resolved config in one line and ask the caller to reply "confirm" to proceed. Only call create_routine / create_loop / create_channel after they confirm in a follow-up message. Never create without confirmation.
567
638
  - For destructive actions (deleting, stopping, killing), require a verified caller and confirm intent; for safe reads, just do it.
568
639
  - If a tool fails or returns nothing useful, say so plainly \u2014 never fabricate a result.
569
640
  - Report the outcome in one line.`;
@@ -936,6 +1007,23 @@ function buildSessionDeps(rpc, opts = {}) {
936
1007
  recentMessageCount: messages.length,
937
1008
  latestMessage: latestText
938
1009
  };
1010
+ },
1011
+ async saveRoutine(routine) {
1012
+ return await rpc.saveRoutine(routine, ctx);
1013
+ },
1014
+ async startLoop(cfg) {
1015
+ await rpc.updateConfig({
1016
+ loop: {
1017
+ task: cfg.task,
1018
+ ...cfg.criteria ? { criteria: cfg.criteria } : {},
1019
+ ...cfg.oracle ? { oracle: cfg.oracle } : {},
1020
+ max_iterations: cfg.maxIterations ?? 20,
1021
+ evaluator: true
1022
+ }
1023
+ }, ctx);
1024
+ },
1025
+ async saveChannel(channel) {
1026
+ return await rpc.saveChannel(channel, ctx);
939
1027
  }
940
1028
  };
941
1029
  }
@@ -2077,7 +2165,7 @@ async function registerMachineService(server, machineId, metadata, daemonState,
2077
2165
  const tunnels = handlers.tunnels;
2078
2166
  if (!tunnels) throw new Error("Tunnel management not available");
2079
2167
  if (tunnels.has(params.name)) throw new Error(`Tunnel '${params.name}' already running`);
2080
- const { FrpcTunnel } = await import('./frpc-BZ4l4-os.mjs');
2168
+ const { FrpcTunnel } = await import('./frpc-WVnBbyjf.mjs');
2081
2169
  const tunnel = new FrpcTunnel({
2082
2170
  name: params.name,
2083
2171
  ports: params.ports,
@@ -2338,7 +2426,7 @@ QUESTION: ${params.question || "Summarize this concisely."}` }
2338
2426
  }
2339
2427
  const deps = buildSessionDeps(rpc, { cwd, ownerEmail: owner });
2340
2428
  const sender = { name: context?.user?.email || context?.user?.id || "user", kind: "user", verified: true };
2341
- const { toolsForRole } = await import('./sideband-wPe3a3m1.mjs');
2429
+ const { toolsForRole } = await import('./sideband-C10Ni7p_.mjs');
2342
2430
  const r2 = await runWiseAgent({ message: params.message, sender, config: { tools: toolsForRole(role2) }, deps, transport, model: resolved.model });
2343
2431
  return fmt(r2);
2344
2432
  }
@@ -3319,7 +3407,7 @@ function createSessionStore(server, sessionId, initialMetadata, initialAgentStat
3319
3407
  },
3320
3408
  /**
3321
3409
  * Patch the session config file (.svamp/{sessionId}/config.json).
3322
- * Used by the frontend to set title, session_link, ralph_loop, etc.
3410
+ * Used by the frontend to set title, session_link, loop activation, etc.
3323
3411
  * Null values remove keys from the config.
3324
3412
  */
3325
3413
  updateConfig: async (patch, context) => {
@@ -9095,6 +9183,20 @@ async function ensureAutoInstalledSkills(logger) {
9095
9183
  }
9096
9184
  },
9097
9185
  marketplaceVersion: async () => readBundledSkillVersion("artifact")
9186
+ },
9187
+ {
9188
+ // The self-verifying `loop` skill drives loop mode (Stop-hook gate +
9189
+ // LOOP.md injection). Bundled in the npm package (bin/skills/loop/) so the
9190
+ // daemon can run loop-init.mjs even offline / before marketplace publish.
9191
+ name: "loop",
9192
+ install: async () => {
9193
+ try {
9194
+ installBundledSkill("loop");
9195
+ } catch {
9196
+ await installSkillFromMarketplace("loop");
9197
+ }
9198
+ },
9199
+ marketplaceVersion: async () => readBundledSkillVersion("loop")
9098
9200
  }
9099
9201
  ];
9100
9202
  for (const task of tasks) {
@@ -9224,72 +9326,58 @@ function writeSvampConfig(configPath, config) {
9224
9326
  renameSync(tmpPath, configPath);
9225
9327
  return content;
9226
9328
  }
9227
- function getRalphStateFilePath(directory, sessionId) {
9228
- return join(getSessionDir(directory, sessionId), "ralph-loop.md");
9329
+ function getLoopDir(directory) {
9330
+ return join(directory, ".claude", "loop");
9229
9331
  }
9230
- function readRalphState(filePath) {
9332
+ function readLoopState(directory) {
9231
9333
  try {
9232
- if (!existsSync$1(filePath)) return null;
9233
- const content = readFileSync$1(filePath, "utf-8");
9234
- const parts = content.split(/^---$/m);
9235
- if (parts.length < 3) return null;
9236
- const frontmatter = parts[1];
9237
- const task = parts.slice(2).join("---").trim();
9238
- if (!task) return null;
9239
- const fields = {};
9240
- for (const line of frontmatter.split("\n")) {
9241
- const match = line.match(/^(\w+):\s*(.*)$/);
9242
- if (match) fields[match[1]] = match[2].replace(/^["']|["']$/g, "");
9243
- }
9244
- return {
9245
- iteration: parseInt(fields.iteration || "1", 10) || 1,
9246
- max_iterations: parseInt(fields.max_iterations || "0", 10) || 0,
9247
- completion_promise: fields.completion_promise === "none" ? null : fields.completion_promise || "DONE",
9248
- cooldown_seconds: parseInt(fields.cooldown_seconds || "1", 10) || 1,
9249
- started_at: fields.started_at || (/* @__PURE__ */ new Date()).toISOString(),
9250
- last_iteration_at: fields.last_iteration_at || void 0,
9251
- context_mode: fields.context_mode === "fresh" || fields.context_mode === "continue" ? fields.context_mode : void 0,
9252
- original_resume_id: fields.original_resume_id || void 0,
9253
- task
9254
- };
9334
+ const p = join(getLoopDir(directory), "loop-state.json");
9335
+ if (!existsSync$1(p)) return null;
9336
+ return JSON.parse(readFileSync$1(p, "utf-8"));
9255
9337
  } catch {
9256
9338
  return null;
9257
9339
  }
9258
9340
  }
9259
- function writeRalphState(filePath, state) {
9260
- mkdirSync(dirname(filePath), { recursive: true });
9261
- const promiseYaml = state.completion_promise === null ? "none" : state.completion_promise.includes(":") || state.completion_promise.includes('"') ? `"${state.completion_promise.replace(/"/g, '\\"')}"` : `"${state.completion_promise}"`;
9262
- const lastIterLine = state.last_iteration_at ? `
9263
- last_iteration_at: "${state.last_iteration_at}"` : "";
9264
- const contextModeLine = state.context_mode ? `
9265
- context_mode: "${state.context_mode}"` : "";
9266
- const originalResumeLine = state.original_resume_id ? `
9267
- original_resume_id: "${state.original_resume_id}"` : "";
9268
- const content = `---
9269
- iteration: ${state.iteration}
9270
- max_iterations: ${state.max_iterations}
9271
- completion_promise: ${promiseYaml}
9272
- cooldown_seconds: ${state.cooldown_seconds}
9273
- started_at: "${state.started_at}"${lastIterLine}${contextModeLine}${originalResumeLine}
9274
- ---
9275
-
9276
- ${state.task}
9277
- `;
9278
- const tmpPath = `${filePath}.tmp`;
9279
- writeFileSync(tmpPath, content);
9280
- renameSync(tmpPath, filePath);
9341
+ function isLoopActive(directory) {
9342
+ const s = readLoopState(directory);
9343
+ return !!s && s.active !== false && s.phase !== "done" && s.phase !== "gave_up" && s.phase !== "cancelled";
9344
+ }
9345
+ function resolveLoopInit() {
9346
+ const candidates = [
9347
+ join(CLAUDE_SKILLS_DIR, "loop", "bin", "loop-init.mjs"),
9348
+ ...getBundledSkillsDir() ? [join(getBundledSkillsDir(), "loop", "bin", "loop-init.mjs")] : []
9349
+ ];
9350
+ for (const c of candidates) if (existsSync$1(c)) return c;
9351
+ return null;
9281
9352
  }
9282
- function removeRalphState(filePath) {
9353
+ function initLoop(directory, cfg) {
9354
+ const initScript = resolveLoopInit();
9355
+ if (!initScript) return false;
9356
+ const args = [initScript, directory, "--task", cfg.task];
9357
+ if (cfg.criteria) args.push("--criteria", cfg.criteria);
9358
+ if (cfg.oracle) args.push("--oracle", cfg.oracle);
9359
+ if (typeof cfg.maxIterations === "number") args.push("--max", String(cfg.maxIterations));
9360
+ args.push("--evaluator", cfg.evaluator === false ? "off" : "on");
9361
+ if (cfg.model) args.push("--model", cfg.model);
9362
+ const res = spawnSync(process.execPath, args, { encoding: "utf-8", timeout: 3e4 });
9363
+ return res.status === 0;
9364
+ }
9365
+ function deactivateLoop(directory) {
9283
9366
  try {
9284
- unlinkSync$1(filePath);
9367
+ const p = join(getLoopDir(directory), "loop-state.json");
9368
+ if (!existsSync$1(p)) return;
9369
+ const s = JSON.parse(readFileSync$1(p, "utf-8"));
9370
+ s.active = false;
9371
+ s.phase = "cancelled";
9372
+ const tmp = p + ".tmp";
9373
+ writeFileSync(tmp, JSON.stringify(s, null, 2));
9374
+ renameSync(tmp, p);
9285
9375
  } catch {
9286
9376
  }
9287
9377
  }
9288
- function createSvampConfigChecker(directory, sessionId, getMetadata, setMetadata, sessionService, logger, onRalphLoopActivated) {
9378
+ function createSvampConfigChecker(directory, sessionId, getMetadata, setMetadata, sessionService, logger, onLoopActivated) {
9289
9379
  const configPath = getSvampConfigPath(directory, sessionId);
9290
- const ralphStatePath = getRalphStateFilePath(directory, sessionId);
9291
9380
  let lastConfigContent = "";
9292
- let lastRalphContent = "";
9293
9381
  if (existsSync$1(configPath)) {
9294
9382
  try {
9295
9383
  lastConfigContent = readFileSync$1(configPath, "utf-8");
@@ -9300,13 +9388,6 @@ function createSvampConfigChecker(directory, sessionId, getMetadata, setMetadata
9300
9388
  } catch {
9301
9389
  }
9302
9390
  }
9303
- if (existsSync$1(ralphStatePath)) {
9304
- try {
9305
- lastRalphContent = readFileSync$1(ralphStatePath, "utf-8");
9306
- } catch {
9307
- }
9308
- }
9309
- let needsInitialRalphProcess = !!lastRalphContent;
9310
9391
  function processConfig(config, meta) {
9311
9392
  if (typeof config.title === "string" && config.title.trim()) {
9312
9393
  const newTitle = config.title.trim();
@@ -9351,56 +9432,6 @@ function createSvampConfigChecker(directory, sessionId, getMetadata, setMetadata
9351
9432
  }
9352
9433
  }
9353
9434
  }
9354
- function processRalphState() {
9355
- const meta = getMetadata();
9356
- const prevRalph = meta.ralphLoop;
9357
- const state = readRalphState(ralphStatePath);
9358
- if (state) {
9359
- const ralphLoop = {
9360
- active: true,
9361
- task: state.task,
9362
- completionPromise: state.completion_promise ?? "none",
9363
- maxIterations: state.max_iterations,
9364
- currentIteration: state.iteration,
9365
- startedAt: state.started_at,
9366
- cooldownSeconds: state.cooldown_seconds,
9367
- contextMode: state.context_mode || "fresh",
9368
- lastIterationStartedAt: (/* @__PURE__ */ new Date()).toISOString()
9369
- };
9370
- if (!prevRalph?.active) {
9371
- const progressRelPath = `.svamp/${sessionId}/ralph-progress.md`;
9372
- const prompt = buildRalphPrompt(state.task, state);
9373
- const ralphSysPrompt = buildRalphSystemPrompt(state, progressRelPath);
9374
- const existingQueue = getMetadata().messageQueue || [];
9375
- setMetadata((m) => ({
9376
- ...m,
9377
- ralphLoop,
9378
- messageQueue: [...existingQueue, {
9379
- id: randomUUID$1(),
9380
- text: prompt,
9381
- displayText: state.task,
9382
- createdAt: Date.now(),
9383
- ralphSystemPrompt: ralphSysPrompt
9384
- }]
9385
- }));
9386
- sessionService.pushMessage(
9387
- { type: "message", message: buildIterationStatus(state.iteration + 1, state.max_iterations, state.completion_promise) },
9388
- "event"
9389
- );
9390
- logger.log(`[svampConfig] Ralph loop started/resumed at iteration ${state.iteration + 1}: "${state.task.slice(0, 50)}..."`);
9391
- onRalphLoopActivated?.();
9392
- } else if (prevRalph.currentIteration !== ralphLoop.currentIteration || prevRalph.task !== ralphLoop.task) {
9393
- setMetadata((m) => ({ ...m, ralphLoop }));
9394
- }
9395
- } else if (prevRalph?.active) {
9396
- setMetadata((m) => ({ ...m, ralphLoop: { active: false } }));
9397
- sessionService.pushMessage(
9398
- { type: "message", message: `Ralph loop cancelled at iteration ${prevRalph.currentIteration}.` },
9399
- "event"
9400
- );
9401
- logger.log(`[svampConfig] Ralph loop state file removed \u2014 cancelled at iteration ${prevRalph.currentIteration}`);
9402
- }
9403
- }
9404
9435
  const configChecker = () => {
9405
9436
  try {
9406
9437
  if (existsSync$1(configPath)) {
@@ -9414,50 +9445,54 @@ function createSvampConfigChecker(directory, sessionId, getMetadata, setMetadata
9414
9445
  } catch {
9415
9446
  }
9416
9447
  };
9417
- const ralphChecker = () => {
9418
- try {
9419
- if (existsSync$1(ralphStatePath)) {
9420
- const content = readFileSync$1(ralphStatePath, "utf-8");
9421
- if (content !== lastRalphContent) {
9422
- lastRalphContent = content;
9423
- processRalphState();
9424
- }
9425
- } else if (lastRalphContent) {
9426
- lastRalphContent = "";
9427
- processRalphState();
9428
- }
9429
- } catch {
9430
- }
9431
- };
9432
9448
  const checker = () => {
9433
9449
  configChecker();
9434
- ralphChecker();
9435
9450
  };
9436
9451
  const writeConfig = (patch) => {
9437
- if ("ralph_loop" in patch) {
9438
- const rl = patch.ralph_loop;
9439
- if (rl && typeof rl === "object" && typeof rl.task === "string") {
9440
- const contextMode = rl.context_mode === "fresh" || rl.context_mode === "continue" ? rl.context_mode : void 0;
9441
- writeRalphState(ralphStatePath, {
9442
- iteration: typeof rl.current_iteration === "number" ? Math.max(0, rl.current_iteration - 1) : 0,
9443
- max_iterations: typeof rl.max_iterations === "number" ? rl.max_iterations : 0,
9444
- completion_promise: rl.completion_promise === "none" || rl.completion_promise === null ? null : rl.completion_promise || "DONE",
9445
- cooldown_seconds: (() => {
9446
- const cd = typeof rl.cooldown_seconds === "number" ? rl.cooldown_seconds : 1;
9447
- const isInfinite = !rl.completion_promise || rl.completion_promise === "none";
9448
- return isInfinite ? Math.max(1, cd) : cd;
9449
- })(),
9450
- started_at: rl.started_at || (/* @__PURE__ */ new Date()).toISOString(),
9451
- context_mode: contextMode,
9452
- task: rl.task.trim()
9452
+ if ("loop" in patch) {
9453
+ const lp = patch.loop;
9454
+ if (lp && typeof lp === "object" && typeof lp.task === "string" && lp.task.trim()) {
9455
+ const oracle = typeof lp.oracle === "string" && lp.oracle.trim() ? lp.oracle.trim() : void 0;
9456
+ const maxIterations = typeof lp.max_iterations === "number" ? lp.max_iterations : 20;
9457
+ const evaluator = lp.evaluator !== false;
9458
+ const ok = initLoop(directory, {
9459
+ task: lp.task.trim(),
9460
+ criteria: typeof lp.criteria === "string" && lp.criteria.trim() ? lp.criteria.trim() : void 0,
9461
+ oracle,
9462
+ maxIterations,
9463
+ evaluator
9453
9464
  });
9454
- ralphChecker();
9465
+ if (ok) {
9466
+ const existingQueue = getMetadata().messageQueue || [];
9467
+ 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.";
9468
+ setMetadata((m) => ({
9469
+ ...m,
9470
+ messageQueue: [...existingQueue, {
9471
+ id: randomUUID$1(),
9472
+ text: kickoff,
9473
+ displayText: `\u{1F501} Loop started: ${lp.task.trim().slice(0, 100)}`,
9474
+ createdAt: Date.now()
9475
+ }]
9476
+ }));
9477
+ sessionService.pushMessage(
9478
+ { type: "message", message: `\u{1F501} Loop started \u2014 iterating until done (oracle: ${oracle || "none"}, evaluator ${evaluator ? "on" : "off"}, max ${maxIterations}).` },
9479
+ "event"
9480
+ );
9481
+ logger.log(`[svampConfig] Loop started: "${lp.task.trim().slice(0, 50)}..."`);
9482
+ onLoopActivated?.();
9483
+ } else {
9484
+ sessionService.pushMessage(
9485
+ { type: "message", message: "Failed to start loop \u2014 the loop skill could not be located. Reinstall with: svamp skills install loop --force", level: "error" },
9486
+ "event"
9487
+ );
9488
+ logger.log(`[svampConfig] Loop init failed \u2014 loop-init.mjs not found`);
9489
+ }
9455
9490
  } else {
9456
- removeRalphState(ralphStatePath);
9457
- lastRalphContent = "";
9458
- processRalphState();
9491
+ deactivateLoop(directory);
9492
+ sessionService.pushMessage({ type: "message", message: "Loop cancelled." }, "event");
9493
+ logger.log(`[svampConfig] Loop cancelled`);
9459
9494
  }
9460
- const { ralph_loop: _, ...restPatch } = patch;
9495
+ const { loop: _, ...restPatch } = patch;
9461
9496
  patch = restPatch;
9462
9497
  }
9463
9498
  if (Object.keys(patch).length > 0) {
@@ -9476,32 +9511,11 @@ function createSvampConfigChecker(directory, sessionId, getMetadata, setMetadata
9476
9511
  mkdirSync(configDir, { recursive: true });
9477
9512
  watcher = watch(configDir, (eventType, filename) => {
9478
9513
  if (filename === "config.json") configChecker();
9479
- if (filename === "ralph-loop.md") ralphChecker();
9480
9514
  });
9481
9515
  watcher.on("error", () => {
9482
9516
  });
9483
9517
  } catch {
9484
9518
  }
9485
- if (needsInitialRalphProcess) {
9486
- const state = readRalphState(ralphStatePath);
9487
- if (state) {
9488
- setMetadata((m) => ({
9489
- ...m,
9490
- ralphLoop: {
9491
- active: true,
9492
- task: state.task,
9493
- completionPromise: state.completion_promise ?? "none",
9494
- maxIterations: state.max_iterations,
9495
- currentIteration: state.iteration,
9496
- startedAt: state.started_at,
9497
- cooldownSeconds: state.cooldown_seconds,
9498
- contextMode: state.context_mode || "fresh",
9499
- lastIterationStartedAt: state.last_iteration_at || (/* @__PURE__ */ new Date()).toISOString()
9500
- }
9501
- }));
9502
- logger.log(`[svampConfig] Ralph loop state restored (iteration ${state.iteration}): "${state.task.slice(0, 50)}..."`);
9503
- }
9504
- }
9505
9519
  return {
9506
9520
  check: checker,
9507
9521
  cleanup: () => {
@@ -9510,133 +9524,6 @@ function createSvampConfigChecker(directory, sessionId, getMetadata, setMetadata
9510
9524
  writeConfig
9511
9525
  };
9512
9526
  }
9513
- function buildIterationStatus(iteration, maxIterations, completionPromise) {
9514
- const iterStr = maxIterations > 0 ? `${iteration}/${maxIterations}` : `${iteration}`;
9515
- if (completionPromise) {
9516
- return `Ralph iteration ${iterStr} | To stop: output <promise>${completionPromise}</promise>`;
9517
- }
9518
- return `Ralph iteration ${iterStr} | Manual stop only`;
9519
- }
9520
- function buildRalphSystemPrompt(state, progressFilePath) {
9521
- const isFresh = state.context_mode === "fresh" || !state.context_mode;
9522
- if (isFresh && progressFilePath) {
9523
- return [
9524
- "# Ralph Loop \u2014 Fresh Context Mode",
9525
- "",
9526
- "You are an autonomous coding agent in an automated loop.",
9527
- "Each iteration spawns a fresh process \u2014 you have NO memory of previous iterations.",
9528
- "",
9529
- "## Your Workflow",
9530
- "",
9531
- `1. Read the progress file at \`${progressFilePath}\` (check Patterns section first)`,
9532
- "2. Check workspace files and git history (`git log --oneline -10`) to understand current state",
9533
- "3. Pick the next incomplete task and implement it",
9534
- "4. Run quality checks (tests, typecheck, build \u2014 whatever the project requires)",
9535
- "5. Commit your changes with a clear commit message",
9536
- `6. Update \`${progressFilePath}\` with your progress (ALWAYS append, never replace)`,
9537
- "",
9538
- "## Progress Report Format",
9539
- "",
9540
- `APPEND to \`${progressFilePath}\`:`,
9541
- "```",
9542
- "## [Date/Time] \u2014 What was done",
9543
- "- What was implemented",
9544
- "- Files changed",
9545
- "- **Learnings for future iterations:**",
9546
- " - Patterns discovered",
9547
- " - Gotchas encountered",
9548
- " - Useful context",
9549
- "---",
9550
- "```",
9551
- "",
9552
- "## Consolidate Patterns",
9553
- "",
9554
- `If you discover a reusable pattern, add it to the \`## Patterns\` section at the TOP of \`${progressFilePath}\`.`,
9555
- "Only add patterns that are general and reusable, not task-specific details.",
9556
- "",
9557
- "## Quality Requirements",
9558
- "",
9559
- "- Do NOT commit broken code",
9560
- "- Keep changes focused and minimal",
9561
- "- Follow existing code patterns",
9562
- ...state.completion_promise ? [
9563
- "",
9564
- "## Stop Condition",
9565
- "",
9566
- `To signal completion, output: <promise>${state.completion_promise}</promise>`,
9567
- "",
9568
- "ONLY output this when the task is FULLY and PERMANENTLY complete.",
9569
- "Do NOT output it if there is ANY remaining work.",
9570
- "When in doubt, do NOT output the promise \u2014 the loop will give you another turn.",
9571
- "",
9572
- "CRITICAL: Do NOT output a false promise to exit the loop.",
9573
- "The loop is designed to continue until genuine completion. Trust the process."
9574
- ] : [
9575
- "",
9576
- "## Continuous Mode",
9577
- "",
9578
- "This is a continuous/infinite loop with no completion signal.",
9579
- "Just do meaningful work each iteration. The loop will continue until manually cancelled.",
9580
- "Focus on making progress, documenting what you did, and setting up the next iteration."
9581
- ]
9582
- ].join("\n");
9583
- }
9584
- return [
9585
- "# Ralph Loop \u2014 Continue Mode",
9586
- "",
9587
- "You are in an automated loop. After this turn ends, the system will automatically",
9588
- "start a new turn with the SAME PROMPT \u2014 you do NOT need to finish everything now.",
9589
- "Your previous work persists in conversation history and files.",
9590
- "Just do meaningful work this turn and let the loop continue.",
9591
- "",
9592
- ...state.completion_promise ? [
9593
- "## Stop Condition",
9594
- "",
9595
- `To signal completion, output: <promise>${state.completion_promise}</promise>`,
9596
- "",
9597
- "CRITICAL \u2014 Do NOT output the promise if:",
9598
- '- The task says "continuously", "forever", "keep running", or "until I stop you"',
9599
- "- There is ANY remaining work, follow-up, or next step you could do",
9600
- "- You just finished one pass/cycle of a recurring task (the loop handles repetition)",
9601
- "- You are uncertain whether the task is truly done",
9602
- "",
9603
- "CRITICAL: Do NOT output a false promise to exit the loop, even if you think you're",
9604
- "stuck or should exit for other reasons. The loop is designed to continue until genuine",
9605
- "completion. Trust the process."
9606
- ] : [
9607
- "## Continuous Mode",
9608
- "",
9609
- "This is a continuous/infinite loop with no completion signal.",
9610
- "Just do meaningful work this turn and let the loop continue.",
9611
- "The loop will run until manually cancelled."
9612
- ]
9613
- ].join("\n");
9614
- }
9615
- function buildRalphPrompt(task, state) {
9616
- const isFresh = state.context_mode === "fresh" || !state.context_mode;
9617
- if (isFresh) {
9618
- return task;
9619
- }
9620
- const iterStr = state.max_iterations > 0 ? `${state.iteration}/${state.max_iterations}` : `${state.iteration}`;
9621
- const reminderLines = [
9622
- "<system-reminder>",
9623
- `Ralph Loop \u2014 Iteration ${iterStr} (Continue Mode)`,
9624
- "Your conversation history persists. Continue from where you left off."
9625
- ];
9626
- if (state.completion_promise) {
9627
- reminderLines.push(`To signal completion, output EXACTLY: <promise>${state.completion_promise}</promise>`);
9628
- reminderLines.push("Only output the promise when the task is FULLY and PERMANENTLY complete.");
9629
- reminderLines.push("Do NOT output a false promise to exit the loop.");
9630
- } else {
9631
- reminderLines.push("This is a continuous loop \u2014 no completion signal needed. Just do meaningful work.");
9632
- }
9633
- reminderLines.push("</system-reminder>");
9634
- const reminder = reminderLines.join("\n");
9635
- return task + "\n\n" + reminder;
9636
- }
9637
- function getRalphProgressFilePath(directory, sessionId) {
9638
- return join(getSessionDir(directory, sessionId), "ralph-progress.md");
9639
- }
9640
9527
  function loadSessionIndex() {
9641
9528
  if (!existsSync$1(SESSION_INDEX_FILE)) return {};
9642
9529
  try {
@@ -9682,16 +9569,6 @@ function deletePersistedSession(sessionId) {
9682
9569
  if (existsSync$1(configFile)) unlinkSync$1(configFile);
9683
9570
  } catch {
9684
9571
  }
9685
- const ralphStateFile = getRalphStateFilePath(entry.directory, sessionId);
9686
- try {
9687
- if (existsSync$1(ralphStateFile)) unlinkSync$1(ralphStateFile);
9688
- } catch {
9689
- }
9690
- const ralphProgressFile = getRalphProgressFilePath(entry.directory, sessionId);
9691
- try {
9692
- if (existsSync$1(ralphProgressFile)) unlinkSync$1(ralphProgressFile);
9693
- } catch {
9694
- }
9695
9572
  const sessionDir = getSessionDir(entry.directory, sessionId);
9696
9573
  try {
9697
9574
  rmdirSync(sessionDir);
@@ -10045,7 +9922,7 @@ async function startDaemon(options) {
10045
9922
  const list = loadExposedTunnels().filter((t) => t.name !== name);
10046
9923
  saveExposedTunnels(list);
10047
9924
  }
10048
- const { ServeManager } = await import('./serveManager-C6_Vloil.mjs');
9925
+ const { ServeManager } = await import('./serveManager-DfETVSOb.mjs');
10049
9926
  const serveManager = new ServeManager(SVAMP_HOME, (msg) => logger.log(`[SERVE] ${msg}`), hyphaServerUrl);
10050
9927
  ensureAutoInstalledSkills(logger).catch(() => {
10051
9928
  });
@@ -10157,7 +10034,7 @@ async function startDaemon(options) {
10157
10034
  }
10158
10035
  }, shouldAutoAllow2 = function(toolName, toolInput) {
10159
10036
  if (toolName === "AskUserQuestion") {
10160
- return sessionMetadata.ralphLoop?.active === true;
10037
+ return isLoopActive(directory);
10161
10038
  }
10162
10039
  if (toolName === "Bash") {
10163
10040
  const inputObj = toolInput;
@@ -10170,7 +10047,7 @@ async function startDaemon(options) {
10170
10047
  } else if (allowedTools.has(toolName)) {
10171
10048
  return true;
10172
10049
  }
10173
- if (sessionMetadata.ralphLoop?.active) return true;
10050
+ if (isLoopActive(directory)) return true;
10174
10051
  if (currentPermissionMode === "bypassPermissions" || currentPermissionMode === "yolo") return true;
10175
10052
  if ((currentPermissionMode === "acceptEdits" || currentPermissionMode === "safe-yolo") && EDIT_TOOLS.has(toolName)) return true;
10176
10053
  return false;
@@ -10261,10 +10138,7 @@ async function startDaemon(options) {
10261
10138
  const sessionCreatedAt = persisted?.createdAt || Date.now();
10262
10139
  let lastSpawnMeta = persisted?.spawnMeta || {};
10263
10140
  let sessionWasProcessing = !!options2.wasProcessing;
10264
- let lastAssistantText = "";
10265
10141
  let lastMainModel;
10266
- let consecutiveRalphErrors = 0;
10267
- const MAX_RALPH_ERRORS = 3;
10268
10142
  let spawnHasReceivedInit = false;
10269
10143
  let startupFailureRetryPending = false;
10270
10144
  let startupRetryMessage;
@@ -10287,23 +10161,23 @@ async function startDaemon(options) {
10287
10161
  stuckWatchdogTimer = setInterval(() => {
10288
10162
  if (!claudeProcess || claudeProcess.exitCode !== null) return;
10289
10163
  if (!sessionWasProcessing) return;
10290
- const ralphState = readRalphState(getRalphStateFilePath(directory, sessionId));
10291
- if (!ralphState) return;
10164
+ if (!isLoopActive(directory)) return;
10292
10165
  if (claudeProcess.pid && hasActiveChildren(claudeProcess.pid)) {
10293
10166
  lastOutputTime = Date.now();
10294
10167
  return;
10295
10168
  }
10296
10169
  const elapsed = Date.now() - lastOutputTime;
10297
10170
  if (elapsed > STUCK_PROCESS_TIMEOUT_MS) {
10298
- 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`);
10171
+ logger.log(`[Session ${sessionId}] Loop stuck: mid-turn, no output for ${Math.round(elapsed / 1e3)}s, no child processes \u2014 killing to resume the loop`);
10299
10172
  sessionService.pushMessage(
10300
- { 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" },
10173
+ { type: "message", message: `Loop appears stuck (no output for ${Math.round(elapsed / 6e4)} minutes, no active tools). Restarting to continue...`, level: "warning" },
10301
10174
  "event"
10302
10175
  );
10303
10176
  claudeProcess.kill("SIGTERM");
10304
10177
  setTimeout(() => {
10305
- if (!trackedSession.stopped) {
10306
- logger.log(`[Session ${sessionId}] Stuck watchdog: nudging Ralph loop to resume`);
10178
+ if (!trackedSession.stopped && isLoopActive(directory)) {
10179
+ logger.log(`[Session ${sessionId}] Stuck watchdog: nudging loop to resume`);
10180
+ enqueueLoopContinue();
10307
10181
  processMessageQueueRef?.();
10308
10182
  }
10309
10183
  }, 3e3);
@@ -10316,6 +10190,20 @@ async function startDaemon(options) {
10316
10190
  stuckWatchdogTimer = null;
10317
10191
  }
10318
10192
  };
10193
+ const enqueueLoopContinue = () => {
10194
+ const existingQueue = sessionMetadata.messageQueue || [];
10195
+ const text = "Continue the loop. Read LOOP.md and keep working toward the exit conditions until the Stop gate confirms completion.";
10196
+ sessionMetadata = {
10197
+ ...sessionMetadata,
10198
+ messageQueue: [...existingQueue, {
10199
+ id: randomUUID$1(),
10200
+ text,
10201
+ displayText: "\u{1F501} Resuming loop",
10202
+ createdAt: Date.now()
10203
+ }]
10204
+ };
10205
+ sessionService.updateMetadata(sessionMetadata);
10206
+ };
10319
10207
  const signalProcessing = (processing) => {
10320
10208
  sessionService.sendKeepAlive(processing);
10321
10209
  const newState = processing ? "running" : "idle";
@@ -10395,7 +10283,7 @@ async function startDaemon(options) {
10395
10283
  if (options2.forceIsolation || sessionMetadata.sharing?.enabled) {
10396
10284
  rawPermissionMode = rawPermissionMode === "default" ? "auto-approve-all" : rawPermissionMode;
10397
10285
  }
10398
- if (sessionMetadata.ralphLoop?.active) {
10286
+ if (isLoopActive(directory)) {
10399
10287
  rawPermissionMode = "bypassPermissions";
10400
10288
  }
10401
10289
  const permissionMode = toClaudePermissionMode(rawPermissionMode);
@@ -10622,10 +10510,6 @@ async function startDaemon(options) {
10622
10510
  logger.log(`[Session ${sessionId}] Background task launched: ${label} (count=${backgroundTaskCount})`);
10623
10511
  }
10624
10512
  }
10625
- const textBlocks = assistantContent.filter((b) => b.type === "text").map((b) => b.text);
10626
- if (textBlocks.length > 0) {
10627
- lastAssistantText += textBlocks.join("\n");
10628
- }
10629
10513
  }
10630
10514
  if (msg.type === "result") {
10631
10515
  if (msg.is_error) {
@@ -10666,8 +10550,8 @@ async function startDaemon(options) {
10666
10550
  }
10667
10551
  }
10668
10552
  if (msg.type === "result") {
10669
- const ralphActive = !!readRalphState(getRalphStateFilePath(directory, sessionId));
10670
- if (!turnInitiatedByUser && !ralphActive) {
10553
+ const loopActive = isLoopActive(directory);
10554
+ if (!turnInitiatedByUser && !loopActive) {
10671
10555
  logger.log(`[Session ${sessionId}] Skipping stale result from SDK-initiated turn`);
10672
10556
  const hasBackgroundTasks = backgroundTaskCount > 0;
10673
10557
  if (hasBackgroundTasks) {
@@ -10692,8 +10576,8 @@ async function startDaemon(options) {
10692
10576
  turnInitiatedByUser = true;
10693
10577
  continue;
10694
10578
  }
10695
- if (!turnInitiatedByUser && ralphActive) {
10696
- logger.log(`[Session ${sessionId}] SDK-initiated result during active Ralph loop \u2014 processing anyway to avoid stalling`);
10579
+ if (!turnInitiatedByUser && loopActive) {
10580
+ logger.log(`[Session ${sessionId}] SDK-initiated result during active loop \u2014 processing anyway to avoid stalling`);
10697
10581
  turnInitiatedByUser = true;
10698
10582
  }
10699
10583
  if (msg.session_id) {
@@ -10743,191 +10627,8 @@ async function startDaemon(options) {
10743
10627
  sessionService.pushMessage({ type: "session_event", message: taskInfo }, "session");
10744
10628
  }
10745
10629
  const queueLen = sessionMetadata.messageQueue?.length ?? 0;
10746
- if (msg.is_error) {
10747
- const rlStateForError = readRalphState(getRalphStateFilePath(directory, sessionId));
10748
- if (rlStateForError) {
10749
- consecutiveRalphErrors++;
10750
- logger.log(`[Session ${sessionId}] Ralph loop: error result (consecutive=${consecutiveRalphErrors}/${MAX_RALPH_ERRORS})`);
10751
- if (consecutiveRalphErrors >= MAX_RALPH_ERRORS) {
10752
- logger.log(`[Session ${sessionId}] Ralph loop: ${MAX_RALPH_ERRORS} consecutive errors \u2014 stopping loop`);
10753
- removeRalphState(getRalphStateFilePath(directory, sessionId));
10754
- if (lastSpawnMeta.appendSystemPrompt) {
10755
- const { appendSystemPrompt: _, ...rest } = lastSpawnMeta;
10756
- lastSpawnMeta = rest;
10757
- }
10758
- sessionMetadata = { ...sessionMetadata, ralphLoop: { active: false }, lifecycleState: "idle" };
10759
- sessionService.updateMetadata(sessionMetadata);
10760
- sessionService.pushMessage(
10761
- { type: "message", message: `Ralph loop stopped \u2014 ${consecutiveRalphErrors} consecutive errors. Last error: ${msg.result || "unknown"}`, level: "error" },
10762
- "event"
10763
- );
10764
- consecutiveRalphErrors = 0;
10765
- signalProcessing(false);
10766
- sessionService.sendSessionEnd();
10767
- break;
10768
- }
10769
- }
10770
- } else {
10771
- consecutiveRalphErrors = 0;
10772
- }
10773
10630
  if (queueLen > 0 && claudeResumeId && !trackedSession.stopped) {
10774
10631
  setTimeout(() => processMessageQueueRef?.(), 200);
10775
- } else if (claudeResumeId) {
10776
- const rlState = readRalphState(getRalphStateFilePath(directory, sessionId));
10777
- if (rlState) {
10778
- let promiseFulfilled = false;
10779
- if (rlState.completion_promise) {
10780
- const promiseMatch = lastAssistantText.match(/<promise>([\s\S]*?)<\/promise>/);
10781
- promiseFulfilled = !!(promiseMatch && promiseMatch[1].trim().replace(/\s+/g, " ") === rlState.completion_promise);
10782
- }
10783
- const maxReached = rlState.max_iterations > 0 && rlState.iteration >= rlState.max_iterations;
10784
- if (promiseFulfilled || maxReached) {
10785
- const isFreshMode = rlState.context_mode === "fresh" || !rlState.context_mode;
10786
- removeRalphState(getRalphStateFilePath(directory, sessionId));
10787
- if (lastSpawnMeta.appendSystemPrompt) {
10788
- const { appendSystemPrompt: _, ...rest } = lastSpawnMeta;
10789
- lastSpawnMeta = rest;
10790
- }
10791
- sessionMetadata = { ...sessionMetadata, ralphLoop: { active: false }, lifecycleState: "idle" };
10792
- sessionService.updateMetadata(sessionMetadata);
10793
- const reason = promiseFulfilled ? `Ralph loop completed at iteration ${rlState.iteration} \u2014 promise "${rlState.completion_promise}" fulfilled.` : `Ralph loop stopped \u2014 max iterations (${rlState.max_iterations}) reached.`;
10794
- logger.log(`[Session ${sessionId}] ${reason}`);
10795
- sessionService.pushMessage({ type: "message", message: reason }, "event");
10796
- if (isFreshMode && rlState.original_resume_id) {
10797
- claudeResumeId = rlState.original_resume_id;
10798
- (async () => {
10799
- try {
10800
- if (claudeProcess && claudeProcess.exitCode === null) {
10801
- isKillingClaude = true;
10802
- await killAndWaitForExit2(claudeProcess);
10803
- isKillingClaude = false;
10804
- }
10805
- if (trackedSession.stopped) return;
10806
- if (isRestartingClaude || isSwitchingMode) return;
10807
- const progressPath = getRalphProgressFilePath(directory, sessionId);
10808
- let resumeMessage;
10809
- try {
10810
- if (existsSync$1(progressPath)) {
10811
- const progressContent = readFileSync$1(progressPath, "utf-8").trim();
10812
- if (progressContent) {
10813
- resumeMessage = `<system-reminder>
10814
- The Ralph Loop has completed (${reason}).
10815
- Below is the progress log from all iterations:
10816
-
10817
- ${progressContent}
10818
- </system-reminder>
10819
-
10820
- The automated loop has finished. Review the progress above and let me know if you need anything else.`;
10821
- unlinkSync$1(progressPath);
10822
- logger.log(`[Session ${sessionId}] Injected progress file content and deleted ${progressPath}`);
10823
- }
10824
- }
10825
- } catch (progressErr) {
10826
- logger.log(`[Session ${sessionId}] Could not read/delete progress file: ${progressErr.message}`);
10827
- }
10828
- spawnClaude(resumeMessage);
10829
- logger.log(`[Session ${sessionId}] Resumed original session ${rlState.original_resume_id}`);
10830
- } catch (err) {
10831
- logger.log(`[Session ${sessionId}] Error resuming original session: ${err.message}`);
10832
- isKillingClaude = false;
10833
- }
10834
- })();
10835
- } else {
10836
- sessionService.sendSessionEnd();
10837
- }
10838
- } else {
10839
- const nextIteration = rlState.iteration + 1;
10840
- const iterationTimestamp = (/* @__PURE__ */ new Date()).toISOString();
10841
- const isFreshMode = rlState.context_mode === "fresh" || !rlState.context_mode;
10842
- const updatedRlState = { ...rlState, iteration: nextIteration, last_iteration_at: iterationTimestamp };
10843
- if (isFreshMode && !rlState.original_resume_id && claudeResumeId) {
10844
- updatedRlState.original_resume_id = claudeResumeId;
10845
- }
10846
- try {
10847
- writeRalphState(getRalphStateFilePath(directory, sessionId), updatedRlState);
10848
- } catch (writeErr) {
10849
- logger.log(`[Session ${sessionId}] Ralph: failed to persist state (iter ${nextIteration}): ${writeErr.message} \u2014 stopping loop`);
10850
- sessionService.pushMessage({ type: "message", message: `Ralph loop error: failed to persist iteration state \u2014 loop stopped. (${writeErr.message})` }, "event");
10851
- removeRalphState(getRalphStateFilePath(directory, sessionId));
10852
- sessionMetadata = { ...sessionMetadata, ralphLoop: { active: false }, lifecycleState: "idle" };
10853
- sessionService.updateMetadata(sessionMetadata);
10854
- break;
10855
- }
10856
- const ralphLoop = {
10857
- active: true,
10858
- task: rlState.task,
10859
- completionPromise: rlState.completion_promise ?? "none",
10860
- maxIterations: rlState.max_iterations,
10861
- currentIteration: nextIteration,
10862
- startedAt: rlState.started_at,
10863
- cooldownSeconds: rlState.cooldown_seconds,
10864
- contextMode: rlState.context_mode || "fresh",
10865
- lastIterationStartedAt: (/* @__PURE__ */ new Date()).toISOString()
10866
- };
10867
- sessionMetadata = { ...sessionMetadata, ralphLoop };
10868
- sessionService.updateMetadata(sessionMetadata);
10869
- logger.log(`[Session ${sessionId}] Ralph loop iteration ${nextIteration}${rlState.max_iterations > 0 ? `/${rlState.max_iterations}` : ""}: spawning (${isFreshMode ? "fresh" : "continue"})`);
10870
- const progressRelPath = `.svamp/${sessionId}/ralph-progress.md`;
10871
- const prompt = buildRalphPrompt(rlState.task, updatedRlState);
10872
- const ralphSysPrompt = buildRalphSystemPrompt(updatedRlState, progressRelPath);
10873
- const cooldownMs = Math.max(200, rlState.cooldown_seconds * 1e3);
10874
- if (isFreshMode) {
10875
- isKillingClaude = true;
10876
- setTimeout(async () => {
10877
- try {
10878
- if (claudeProcess && claudeProcess.exitCode === null) {
10879
- await killAndWaitForExit2(claudeProcess);
10880
- }
10881
- isKillingClaude = false;
10882
- if (trackedSession.stopped) return;
10883
- if (isRestartingClaude || isSwitchingMode) return;
10884
- claudeResumeId = void 0;
10885
- userMessagePending = true;
10886
- turnInitiatedByUser = true;
10887
- sessionWasProcessing = true;
10888
- signalProcessing(true);
10889
- sessionService.pushMessage({ type: "message", message: buildIterationStatus(nextIteration, rlState.max_iterations, rlState.completion_promise) }, "event");
10890
- sessionService.pushMessage(rlState.task, "user");
10891
- spawnClaude(prompt, { appendSystemPrompt: ralphSysPrompt });
10892
- } catch (err) {
10893
- logger.log(`[Session ${sessionId}] Error in fresh Ralph iteration: ${err.message}`);
10894
- isKillingClaude = false;
10895
- sessionWasProcessing = false;
10896
- signalProcessing(false);
10897
- }
10898
- }, cooldownMs);
10899
- } else {
10900
- setTimeout(() => {
10901
- if (trackedSession.stopped) return;
10902
- if (isRestartingClaude || isSwitchingMode) return;
10903
- try {
10904
- userMessagePending = true;
10905
- turnInitiatedByUser = true;
10906
- sessionWasProcessing = true;
10907
- signalProcessing(true);
10908
- sessionService.pushMessage({ type: "message", message: buildIterationStatus(nextIteration, rlState.max_iterations, rlState.completion_promise) }, "event");
10909
- sessionService.pushMessage(rlState.task, "user");
10910
- if (claudeProcess && claudeProcess.exitCode === null) {
10911
- const stdinMsg = JSON.stringify({
10912
- type: "user",
10913
- message: { role: "user", content: prompt }
10914
- });
10915
- claudeProcess.stdin?.write(stdinMsg + "\n");
10916
- } else {
10917
- spawnClaude(prompt, { appendSystemPrompt: ralphSysPrompt });
10918
- }
10919
- } catch (err) {
10920
- logger.log(`[Session ${sessionId}] Error in continue Ralph iteration: ${err.message}`);
10921
- sessionWasProcessing = false;
10922
- signalProcessing(false);
10923
- }
10924
- }, cooldownMs);
10925
- }
10926
- }
10927
- } else {
10928
- signalProcessing(false);
10929
- sessionService.sendSessionEnd();
10930
- }
10931
10632
  } else {
10932
10633
  signalProcessing(false);
10933
10634
  sessionService.sendSessionEnd();
@@ -10935,7 +10636,6 @@ The automated loop has finished. Review the progress above and let me know if yo
10935
10636
  }
10936
10637
  sessionService.pushMessage(msg, "agent");
10937
10638
  } else if (msg.type === "system" && msg.subtype === "init") {
10938
- lastAssistantText = "";
10939
10639
  consecutiveOverloadRetries = 0;
10940
10640
  overloadBailedThisTurn = false;
10941
10641
  if (!userMessagePending) {
@@ -11241,10 +10941,6 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
11241
10941
  signalProcessing(false);
11242
10942
  return;
11243
10943
  }
11244
- if (msgMeta?.ralphFreshContext && claudeResumeId) {
11245
- logger.log(`[Session ${sessionId}] Ralph fresh context: clearing resumeId for fresh spawn`);
11246
- claudeResumeId = void 0;
11247
- }
11248
10944
  if (msgMeta?.btw && claudeResumeId) {
11249
10945
  logger.log(`[Session ${sessionId}] /btw side-channel: "${text.substring(0, 80)}..."`);
11250
10946
  sessionService.pushMessage(
@@ -11539,7 +11235,6 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
11539
11235
  const newCustomTitle = newMeta.customTitle;
11540
11236
  const oldLink = sessionMetadata.sessionLink;
11541
11237
  const newLink = newMeta.sessionLink;
11542
- const prevRalphLoop = sessionMetadata.ralphLoop;
11543
11238
  sessionMetadata = {
11544
11239
  ...newMeta,
11545
11240
  // Daemon drives lifecycleState — don't let frontend overwrite with stale value
@@ -11549,10 +11244,7 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
11549
11244
  ...sessionMetadata.summary && !newMeta.summary ? { summary: sessionMetadata.summary } : {},
11550
11245
  ...sessionMetadata.sessionLink && !newMeta.sessionLink ? { sessionLink: sessionMetadata.sessionLink } : {},
11551
11246
  // Preserve parentSessionId — set at spawn time, frontend may not track it
11552
- ...sessionMetadata.parentSessionId ? { parentSessionId: sessionMetadata.parentSessionId } : {},
11553
- // Preserve daemon-owned ralphLoop — frontend may send stale snapshot without it,
11554
- // which would wipe the active loop state and cause the bar to disappear mid-run.
11555
- ...prevRalphLoop ? { ralphLoop: prevRalphLoop } : {}
11247
+ ...sessionMetadata.parentSessionId ? { parentSessionId: sessionMetadata.parentSessionId } : {}
11556
11248
  };
11557
11249
  const cfgPatch = {};
11558
11250
  if (newTitle !== oldTitle) cfgPatch.title = newTitle ?? null;
@@ -11567,9 +11259,6 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
11567
11259
  logger.log(`[Session ${sessionId}] Failed to persist metadata patch: ${err.message}`);
11568
11260
  }
11569
11261
  }
11570
- if (prevRalphLoop && !newMeta.ralphLoop) {
11571
- sessionService.updateMetadata(sessionMetadata);
11572
- }
11573
11262
  const queue = newMeta.messageQueue;
11574
11263
  if (queue && queue.length > 0 && !sessionWasProcessing && !trackedSession.stopped) {
11575
11264
  setTimeout(() => {
@@ -11709,49 +11398,20 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
11709
11398
  sessionService.pushMessage(next.displayText || next.text, "user");
11710
11399
  userMessagePending = true;
11711
11400
  turnInitiatedByUser = true;
11712
- const queueMeta = next.ralphSystemPrompt ? { appendSystemPrompt: next.ralphSystemPrompt } : void 0;
11713
- const rlState = readRalphState(getRalphStateFilePath(directory, sessionId));
11714
- const isRalphFresh = rlState && (rlState.context_mode === "fresh" || !rlState.context_mode);
11715
- if (isRalphFresh && claudeProcess && claudeProcess.exitCode === null) {
11716
- logger.log(`[Session ${sessionId}] Ralph fresh context: killing process for fresh spawn`);
11717
- (async () => {
11718
- try {
11719
- if (!rlState.original_resume_id && claudeResumeId) {
11720
- writeRalphState(getRalphStateFilePath(directory, sessionId), {
11721
- ...rlState,
11722
- original_resume_id: claudeResumeId
11723
- });
11724
- }
11725
- isKillingClaude = true;
11726
- await killAndWaitForExit2(claudeProcess);
11727
- isKillingClaude = false;
11728
- if (trackedSession?.stopped) return;
11729
- if (isRestartingClaude || isSwitchingMode) return;
11730
- claudeResumeId = void 0;
11731
- spawnClaude(next.text, queueMeta);
11732
- } catch (err) {
11733
- logger.log(`[Session ${sessionId}] Error in fresh Ralph queue processing: ${err.message}`);
11734
- isKillingClaude = false;
11735
- sessionWasProcessing = false;
11736
- signalProcessing(false);
11737
- }
11738
- })();
11739
- } else {
11740
- try {
11741
- if (!claudeProcess || claudeProcess.exitCode !== null) {
11742
- spawnClaude(next.text, queueMeta);
11743
- } else {
11744
- const stdinMsg = JSON.stringify({
11745
- type: "user",
11746
- message: { role: "user", content: next.text }
11747
- });
11748
- claudeProcess.stdin?.write(stdinMsg + "\n");
11749
- }
11750
- } catch (err) {
11751
- logger.log(`[Session ${sessionId}] Error in processMessageQueue spawn: ${err.message}`);
11752
- sessionWasProcessing = false;
11753
- signalProcessing(false);
11401
+ try {
11402
+ if (!claudeProcess || claudeProcess.exitCode !== null) {
11403
+ spawnClaude(next.text);
11404
+ } else {
11405
+ const stdinMsg = JSON.stringify({
11406
+ type: "user",
11407
+ message: { role: "user", content: next.text }
11408
+ });
11409
+ claudeProcess.stdin?.write(stdinMsg + "\n");
11754
11410
  }
11411
+ } catch (err) {
11412
+ logger.log(`[Session ${sessionId}] Error in processMessageQueue spawn: ${err.message}`);
11413
+ sessionWasProcessing = false;
11414
+ signalProcessing(false);
11755
11415
  }
11756
11416
  }
11757
11417
  };
@@ -11825,7 +11485,7 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
11825
11485
  } else if (allowedTools.has(toolName)) {
11826
11486
  return true;
11827
11487
  }
11828
- if (sessionMetadata.ralphLoop?.active) return true;
11488
+ if (isLoopActive(directory)) return true;
11829
11489
  if (currentPermissionMode === "bypassPermissions" || currentPermissionMode === "yolo") return true;
11830
11490
  if ((currentPermissionMode === "acceptEdits" || currentPermissionMode === "safe-yolo") && EDIT_TOOLS.has(toolName)) return true;
11831
11491
  return false;
@@ -12035,7 +11695,6 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
12035
11695
  const newCustomTitleAcp = newMeta.customTitle;
12036
11696
  const oldLinkAcp = sessionMetadata.sessionLink;
12037
11697
  const newLinkAcp = newMeta.sessionLink;
12038
- const prevRalphLoop = sessionMetadata.ralphLoop;
12039
11698
  sessionMetadata = {
12040
11699
  ...newMeta,
12041
11700
  // Daemon drives lifecycleState — don't let frontend overwrite with stale value
@@ -12043,10 +11702,7 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
12043
11702
  ...sessionMetadata.summary && !newMeta.summary ? { summary: sessionMetadata.summary } : {},
12044
11703
  ...sessionMetadata.sessionLink && !newMeta.sessionLink ? { sessionLink: sessionMetadata.sessionLink } : {},
12045
11704
  // Preserve parentSessionId — set at spawn time, frontend may not track it
12046
- ...sessionMetadata.parentSessionId ? { parentSessionId: sessionMetadata.parentSessionId } : {},
12047
- // Preserve daemon-owned ralphLoop — frontend may send stale snapshot without it,
12048
- // which would wipe the active loop state and cause the bar to disappear mid-run.
12049
- ...prevRalphLoop ? { ralphLoop: prevRalphLoop } : {}
11705
+ ...sessionMetadata.parentSessionId ? { parentSessionId: sessionMetadata.parentSessionId } : {}
12050
11706
  };
12051
11707
  const cfgPatchAcp = {};
12052
11708
  if (newTitleAcp !== oldTitleAcp) cfgPatchAcp.title = newTitleAcp ?? null;
@@ -12061,9 +11717,6 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
12061
11717
  logger.log(`[Session ${sessionId}] Failed to persist metadata patch: ${err.message}`);
12062
11718
  }
12063
11719
  }
12064
- if (prevRalphLoop && !newMeta.ralphLoop) {
12065
- sessionService.updateMetadata(sessionMetadata);
12066
- }
12067
11720
  if (acpStopped) return;
12068
11721
  const queue = newMeta.messageQueue;
12069
11722
  if (queue && queue.length > 0 && sessionMetadata.lifecycleState === "idle") {
@@ -12197,7 +11850,7 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
12197
11850
  const remaining = queue.slice(1);
12198
11851
  sessionMetadata = { ...sessionMetadata, messageQueue: remaining.length > 0 ? remaining : void 0, lifecycleState: "running" };
12199
11852
  sessionService.updateMetadata(sessionMetadata);
12200
- logger.log(`[Session ${sessionId}] Processing queued message (ACP ralph activation): "${next.text.slice(0, 50)}..."`);
11853
+ logger.log(`[Session ${sessionId}] Processing queued message (ACP loop activation): "${next.text.slice(0, 50)}..."`);
12201
11854
  sessionService.sendKeepAlive(true);
12202
11855
  agentBackend.sendPrompt(sessionId, next.text).catch((err) => {
12203
11856
  logger.error(`[Session ${sessionId}] Error processing queued message: ${err.message}`);
@@ -12279,99 +11932,6 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
12279
11932
  }
12280
11933
  } catch {
12281
11934
  }
12282
- const rlState = readRalphState(getRalphStateFilePath(directory, sessionId));
12283
- if (rlState) {
12284
- let promiseFulfilled = false;
12285
- if (rlState.completion_promise) {
12286
- const promiseMatch = lastAssistantText.match(/<promise>([\s\S]*?)<\/promise>/);
12287
- promiseFulfilled = !!(promiseMatch && promiseMatch[1].trim().replace(/\s+/g, " ") === rlState.completion_promise);
12288
- }
12289
- const maxReached = rlState.max_iterations > 0 && rlState.iteration >= rlState.max_iterations;
12290
- if (promiseFulfilled || maxReached) {
12291
- removeRalphState(getRalphStateFilePath(directory, sessionId));
12292
- sessionMetadata = { ...sessionMetadata, ralphLoop: { active: false }, lifecycleState: "idle" };
12293
- sessionService.updateMetadata(sessionMetadata);
12294
- const reason = promiseFulfilled ? `Ralph loop completed at iteration ${rlState.iteration} \u2014 promise "${rlState.completion_promise}" fulfilled.` : `Ralph loop stopped \u2014 max iterations (${rlState.max_iterations}) reached.`;
12295
- logger.log(`[${agentName} Session ${sessionId}] ${reason}`);
12296
- sessionService.pushMessage({ type: "message", message: reason }, "event");
12297
- } else {
12298
- const pendingQueue = sessionMetadata.messageQueue;
12299
- if (pendingQueue && pendingQueue.length > 0) {
12300
- const next = pendingQueue[0];
12301
- const remaining = pendingQueue.slice(1);
12302
- sessionMetadata = { ...sessionMetadata, messageQueue: remaining.length > 0 ? remaining : void 0, lifecycleState: "running" };
12303
- sessionService.updateMetadata(sessionMetadata);
12304
- sessionService.sendKeepAlive(true);
12305
- sessionService.pushMessage(next.displayText || next.text, "user");
12306
- logger.log(`[${agentName} Session ${sessionId}] Processing queued message (priority over Ralph advance): "${next.text.slice(0, 50)}..."`);
12307
- agentBackend.sendPrompt(sessionId, next.text).catch((err) => {
12308
- logger.error(`[${agentName} Session ${sessionId}] Error processing queued message (Ralph): ${err.message}`);
12309
- if (!acpStopped) {
12310
- sessionMetadata = { ...sessionMetadata, lifecycleState: "idle" };
12311
- sessionService.updateMetadata(sessionMetadata);
12312
- sessionService.sendSessionEnd();
12313
- }
12314
- });
12315
- return;
12316
- }
12317
- const nextIteration = rlState.iteration + 1;
12318
- const iterationTimestamp = (/* @__PURE__ */ new Date()).toISOString();
12319
- try {
12320
- writeRalphState(getRalphStateFilePath(directory, sessionId), { ...rlState, iteration: nextIteration, last_iteration_at: iterationTimestamp });
12321
- } catch (writeErr) {
12322
- logger.log(`[${agentName} Session ${sessionId}] Ralph: failed to persist state (iter ${nextIteration}): ${writeErr.message} \u2014 stopping loop`);
12323
- sessionService.pushMessage({ type: "message", message: `Ralph loop error: failed to persist iteration state \u2014 loop stopped. (${writeErr.message})` }, "event");
12324
- removeRalphState(getRalphStateFilePath(directory, sessionId));
12325
- sessionMetadata = { ...sessionMetadata, ralphLoop: { active: false }, lifecycleState: "idle" };
12326
- sessionService.updateMetadata(sessionMetadata);
12327
- return;
12328
- }
12329
- const ralphLoop = {
12330
- active: true,
12331
- task: rlState.task,
12332
- completionPromise: rlState.completion_promise ?? "none",
12333
- maxIterations: rlState.max_iterations,
12334
- currentIteration: nextIteration,
12335
- startedAt: rlState.started_at,
12336
- cooldownSeconds: rlState.cooldown_seconds,
12337
- contextMode: rlState.context_mode || "fresh",
12338
- lastIterationStartedAt: (/* @__PURE__ */ new Date()).toISOString()
12339
- };
12340
- sessionMetadata = { ...sessionMetadata, ralphLoop, lifecycleState: "running" };
12341
- sessionService.updateMetadata(sessionMetadata);
12342
- logger.log(`[${agentName} Session ${sessionId}] Ralph loop iteration ${nextIteration}${rlState.max_iterations > 0 ? `/${rlState.max_iterations}` : ""}: spawning`);
12343
- const updatedState = { ...rlState, iteration: nextIteration, context_mode: "continue" };
12344
- const prompt = buildRalphPrompt(rlState.task, updatedState);
12345
- const cooldownMs = Math.max(200, rlState.cooldown_seconds * 1e3);
12346
- setTimeout(() => {
12347
- if (acpStopped) return;
12348
- const liveRlState = readRalphState(getRalphStateFilePath(directory, sessionId));
12349
- if (!liveRlState) {
12350
- sessionMetadata = { ...sessionMetadata, lifecycleState: "idle" };
12351
- sessionService.updateMetadata(sessionMetadata);
12352
- sessionService.sendKeepAlive(false);
12353
- sessionService.sendSessionEnd();
12354
- return;
12355
- }
12356
- sessionService.sendKeepAlive(true);
12357
- sessionMetadata = { ...sessionMetadata, lifecycleState: "running" };
12358
- sessionService.updateMetadata(sessionMetadata);
12359
- sessionService.pushMessage({ type: "message", message: buildIterationStatus(nextIteration, rlState.max_iterations, rlState.completion_promise) }, "event");
12360
- sessionService.pushMessage(rlState.task, "user");
12361
- agentBackend.sendPrompt(sessionId, prompt).catch((err) => {
12362
- logger.error(`[${agentName} Session ${sessionId}] Error in Ralph loop: ${err.message}`);
12363
- if (!acpStopped) {
12364
- removeRalphState(getRalphStateFilePath(directory, sessionId));
12365
- sessionMetadata = { ...sessionMetadata, ralphLoop: { active: false }, lifecycleState: "idle" };
12366
- sessionService.updateMetadata(sessionMetadata);
12367
- sessionService.sendSessionEnd();
12368
- sessionService.pushMessage({ type: "message", message: `Ralph loop error: agent failed to start turn \u2014 loop stopped. (${err.message})` }, "event");
12369
- }
12370
- });
12371
- }, cooldownMs);
12372
- return;
12373
- }
12374
- }
12375
11935
  const queue = sessionMetadata.messageQueue;
12376
11936
  if (queue && queue.length > 0) {
12377
11937
  const next = queue[0];
@@ -12673,7 +12233,7 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
12673
12233
  const specs = loadExposedTunnels();
12674
12234
  if (specs.length === 0) return;
12675
12235
  logger.log(`[exposed-tunnels] Restoring ${specs.length} tunnel(s) from ${EXPOSED_TUNNELS_FILE}`);
12676
- const { FrpcTunnel } = await import('./frpc-BZ4l4-os.mjs');
12236
+ const { FrpcTunnel } = await import('./frpc-WVnBbyjf.mjs');
12677
12237
  for (const spec of specs) {
12678
12238
  if (tunnels.has(spec.name)) continue;
12679
12239
  try {
@@ -12754,7 +12314,7 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
12754
12314
  logger.log(`[staged-homes] sweep failed: ${err.message}`);
12755
12315
  }
12756
12316
  const sessionsToAutoContinue = [];
12757
- const sessionsToRalphResume = [];
12317
+ const sessionsToLoopResume = [];
12758
12318
  if (persistedSessions.length > 0) {
12759
12319
  logger.log(`Restoring ${persistedSessions.length} persisted session(s)...`);
12760
12320
  for (const persisted of persistedSessions) {
@@ -12799,11 +12359,8 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
12799
12359
  if (persisted.wasProcessing && persisted.claudeResumeId && !isOrphaned) {
12800
12360
  sessionsToAutoContinue.push(persisted.sessionId);
12801
12361
  }
12802
- if (!isOrphaned && !persisted.wasProcessing) {
12803
- const rlState = readRalphState(getRalphStateFilePath(persisted.directory, persisted.sessionId));
12804
- if (rlState) {
12805
- sessionsToRalphResume.push({ sessionId: persisted.sessionId, directory: persisted.directory });
12806
- }
12362
+ if (!isOrphaned && !persisted.wasProcessing && isLoopActive(persisted.directory)) {
12363
+ sessionsToLoopResume.push({ sessionId: persisted.sessionId, directory: persisted.directory });
12807
12364
  }
12808
12365
  } else {
12809
12366
  logger.log(`Failed to restore session ${persisted.sessionId}: ${result.type}`);
@@ -12837,54 +12394,34 @@ ${capturedError}${buildClaudeErrorHint(capturedError)}`;
12837
12394
  } else if (sessionsToAutoContinue.length > 0) {
12838
12395
  logger.log(`Skipping auto-continue for ${sessionsToAutoContinue.length} interrupted session(s) (--no-auto-continue)`);
12839
12396
  }
12840
- if (sessionsToRalphResume.length > 0 && !options?.noAutoContinue) {
12841
- logger.log(`Resuming Ralph loop for ${sessionsToRalphResume.length} session(s)...`);
12842
- for (const { sessionId, directory: sessDir } of sessionsToRalphResume) {
12397
+ if (sessionsToLoopResume.length > 0 && !options?.noAutoContinue) {
12398
+ logger.log(`Resuming loop for ${sessionsToLoopResume.length} session(s)...`);
12399
+ for (const { sessionId, directory: sessDir } of sessionsToLoopResume) {
12843
12400
  try {
12844
12401
  const tracked = Array.from(pidToTrackedSession.values()).find((s) => s.svampSessionId === sessionId);
12845
12402
  const rpc = tracked?.sessionRPCHandlers;
12846
12403
  if (!rpc) {
12847
- logger.log(`Session ${sessionId} RPC handlers not found for Ralph resume`);
12404
+ logger.log(`Session ${sessionId} RPC handlers not found for loop resume`);
12848
12405
  continue;
12849
12406
  }
12850
- const rlState = readRalphState(getRalphStateFilePath(sessDir, sessionId));
12851
- if (!rlState) continue;
12852
- const initDelayMs = 2e3;
12853
- let resumeDelayMs = initDelayMs;
12854
- if (rlState.last_iteration_at) {
12855
- const lastIterTime = new Date(rlState.last_iteration_at).getTime();
12856
- const cooldownMs = Math.max(200, rlState.cooldown_seconds * 1e3);
12857
- const elapsedMs = Date.now() - lastIterTime;
12858
- const remainingCooldownMs = cooldownMs - elapsedMs;
12859
- resumeDelayMs = Math.max(initDelayMs, remainingCooldownMs + initDelayMs);
12860
- logger.log(`Ralph loop for session ${sessionId}: cooldown ${rlState.cooldown_seconds}s, elapsed ${Math.round(elapsedMs / 1e3)}s, resuming in ${Math.round(resumeDelayMs / 1e3)}s`);
12861
- }
12862
12407
  setTimeout(async () => {
12863
12408
  try {
12864
- const currentState = readRalphState(getRalphStateFilePath(sessDir, sessionId));
12865
- if (!currentState) return;
12866
- const isFreshMode = currentState.context_mode === "fresh" || !currentState.context_mode;
12867
- const progressRelPath = `.svamp/${sessionId}/ralph-progress.md`;
12868
- const prompt = buildRalphPrompt(currentState.task, currentState);
12869
- const ralphSysPrompt = buildRalphSystemPrompt(currentState, progressRelPath);
12409
+ if (!isLoopActive(sessDir)) return;
12410
+ const prompt = "Continue the loop. Read LOOP.md and keep working toward the exit conditions until the Stop gate confirms completion.";
12870
12411
  await rpc.sendMessage(
12871
12412
  JSON.stringify({
12872
12413
  role: "user",
12873
12414
  content: { type: "text", text: prompt },
12874
- meta: {
12875
- sentFrom: "svamp-daemon-ralph-resume",
12876
- appendSystemPrompt: ralphSysPrompt,
12877
- ...isFreshMode ? { ralphFreshContext: true } : {}
12878
- }
12415
+ meta: { sentFrom: "svamp-daemon-loop-resume" }
12879
12416
  })
12880
12417
  );
12881
- logger.log(`Resumed Ralph loop for session ${sessionId} at iteration ${currentState.iteration} (${isFreshMode ? "fresh" : "continue"})`);
12418
+ logger.log(`Resumed loop for session ${sessionId}`);
12882
12419
  } catch (err) {
12883
- logger.log(`Failed to resume Ralph loop for session ${sessionId}: ${err.message}`);
12420
+ logger.log(`Failed to resume loop for session ${sessionId}: ${err.message}`);
12884
12421
  }
12885
- }, resumeDelayMs);
12422
+ }, 2e3);
12886
12423
  } catch (err) {
12887
- logger.log(`Failed to find session service for Ralph resume ${sessionId}: ${err.message}`);
12424
+ logger.log(`Failed to find session service for loop resume ${sessionId}: ${err.message}`);
12888
12425
  }
12889
12426
  }
12890
12427
  }