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.
- package/README.md +7 -5
- package/bin/skills/loop/IMPLEMENTATION_PROGRESS.md +49 -0
- package/bin/skills/loop/SKILL.md +99 -0
- package/bin/skills/loop/bin/channel-core.mjs +161 -0
- package/bin/skills/loop/bin/channel-server.mjs +151 -0
- package/bin/skills/loop/bin/inject-loop.mjs +41 -0
- package/bin/skills/loop/bin/loop-init.mjs +128 -0
- package/bin/skills/loop/bin/loop-status.mjs +38 -0
- package/bin/skills/loop/bin/precompact.mjs +27 -0
- package/bin/skills/loop/bin/routine-cli.mjs +121 -0
- package/bin/skills/loop/bin/routine-core.mjs +126 -0
- package/bin/skills/loop/bin/routine-runner.mjs +125 -0
- package/bin/skills/loop/bin/routine-store.mjs +49 -0
- package/bin/skills/loop/bin/state-fp.mjs +113 -0
- package/bin/skills/loop/bin/stop-gate.mjs +170 -0
- package/bin/skills/loop/routines.process.yaml +20 -0
- package/bin/skills/loop/test/test-channel-core.mjs +86 -0
- package/bin/skills/loop/test/test-loop-gate.mjs +246 -0
- package/bin/skills/loop/test/test-routine-core.mjs +54 -0
- package/bin/skills/loop/test/test-routine-engine.mjs +122 -0
- package/dist/{agentCommands-BULNvfKa.mjs → agentCommands-CAqLhLOH.mjs} +2 -2
- package/dist/{auth-BfDOBBPy.mjs → auth-CYA0e4mT.mjs} +1 -1
- package/dist/{caddy-BMbX-mFX.mjs → caddy-CuTbE3NY.mjs} +1 -14
- package/dist/cli.mjs +76 -77
- package/dist/{commands-C9DbNFz1.mjs → commands-B2uNdsyR.mjs} +2 -2
- package/dist/{commands-h2Dzb5m1.mjs → commands-Bxn_4u7d.mjs} +1 -1
- package/dist/{commands-DIhhodx8.mjs → commands-CdxEOPUt.mjs} +34 -42
- package/dist/{commands-qE4ZGLzB.mjs → commands-D-3h8H0C.mjs} +6 -6
- package/dist/{commands-FhGCsATM.mjs → commands-DRQUzw4j.mjs} +1 -1
- package/dist/{fleet-Cmma7Iu-.mjs → fleet-CNF84yJV.mjs} +1 -1
- package/dist/{frpc-BZ4l4-os.mjs → frpc-WVnBbyjf.mjs} +2 -15
- package/dist/{headlessCli-xRpI9fdk.mjs → headlessCli-DcP8eawK.mjs} +2 -2
- package/dist/index.mjs +1 -1
- package/dist/package-DHxiXJ3N.mjs +63 -0
- package/dist/{run-DTIEcH-W.mjs → run-C7WSV8zx.mjs} +1 -1
- package/dist/{run-DxzG-3JD.mjs → run-CsMTSngP.mjs} +246 -709
- package/dist/{serveCommands-CzllIFB_.mjs → serveCommands-Can8WtLI.mjs} +5 -5
- package/dist/{serveManager-C6_Vloil.mjs → serveManager-DfETVSOb.mjs} +3 -3
- package/dist/{sideband-wPe3a3m1.mjs → sideband-C10Ni7p_.mjs} +1 -1
- package/package.json +3 -3
- package/dist/package-DD227VZO.mjs +0 -63
|
@@ -3,7 +3,7 @@ import fs, { mkdir as mkdir$1, readdir as readdir$1, readFile, writeFile as writ
|
|
|
3
3
|
import { readFileSync as readFileSync$1, mkdirSync, writeFileSync, renameSync, existsSync as existsSync$1, rmSync as rmSync$1, unlinkSync as unlinkSync$1, copyFileSync, watch, rmdirSync, readdirSync as readdirSync$1 } from 'fs';
|
|
4
4
|
import path__default, { join, dirname, basename, resolve } from 'path';
|
|
5
5
|
import { fileURLToPath } from 'url';
|
|
6
|
-
import { execFile, spawn as spawn$1, execSync as execSync$1 } from 'child_process';
|
|
6
|
+
import { execFile, spawn as spawn$1, execSync as execSync$1, spawnSync } from 'child_process';
|
|
7
7
|
import { randomUUID as randomUUID$1 } from 'crypto';
|
|
8
8
|
import { existsSync, readFileSync, mkdirSync as mkdirSync$1, readdirSync, writeFileSync as writeFileSync$1, renameSync as renameSync$1, rmSync, appendFileSync, unlinkSync } from 'node:fs';
|
|
9
9
|
import { exec, spawn, execSync, execFile as execFile$1, execFileSync } from 'node:child_process';
|
|
@@ -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-
|
|
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-
|
|
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,
|
|
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
|
|
9228
|
-
return join(
|
|
9329
|
+
function getLoopDir(directory) {
|
|
9330
|
+
return join(directory, ".claude", "loop");
|
|
9229
9331
|
}
|
|
9230
|
-
function
|
|
9332
|
+
function readLoopState(directory) {
|
|
9231
9333
|
try {
|
|
9232
|
-
|
|
9233
|
-
|
|
9234
|
-
|
|
9235
|
-
if (parts.length < 3) return null;
|
|
9236
|
-
const frontmatter = parts[1];
|
|
9237
|
-
const task = parts.slice(2).join("---").trim();
|
|
9238
|
-
if (!task) return null;
|
|
9239
|
-
const fields = {};
|
|
9240
|
-
for (const line of frontmatter.split("\n")) {
|
|
9241
|
-
const match = line.match(/^(\w+):\s*(.*)$/);
|
|
9242
|
-
if (match) fields[match[1]] = match[2].replace(/^["']|["']$/g, "");
|
|
9243
|
-
}
|
|
9244
|
-
return {
|
|
9245
|
-
iteration: parseInt(fields.iteration || "1", 10) || 1,
|
|
9246
|
-
max_iterations: parseInt(fields.max_iterations || "0", 10) || 0,
|
|
9247
|
-
completion_promise: fields.completion_promise === "none" ? null : fields.completion_promise || "DONE",
|
|
9248
|
-
cooldown_seconds: parseInt(fields.cooldown_seconds || "1", 10) || 1,
|
|
9249
|
-
started_at: fields.started_at || (/* @__PURE__ */ new Date()).toISOString(),
|
|
9250
|
-
last_iteration_at: fields.last_iteration_at || void 0,
|
|
9251
|
-
context_mode: fields.context_mode === "fresh" || fields.context_mode === "continue" ? fields.context_mode : void 0,
|
|
9252
|
-
original_resume_id: fields.original_resume_id || void 0,
|
|
9253
|
-
task
|
|
9254
|
-
};
|
|
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
|
|
9260
|
-
|
|
9261
|
-
|
|
9262
|
-
|
|
9263
|
-
|
|
9264
|
-
const
|
|
9265
|
-
|
|
9266
|
-
|
|
9267
|
-
|
|
9268
|
-
const
|
|
9269
|
-
|
|
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
|
|
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
|
-
|
|
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,
|
|
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 ("
|
|
9438
|
-
const
|
|
9439
|
-
if (
|
|
9440
|
-
const
|
|
9441
|
-
|
|
9442
|
-
|
|
9443
|
-
|
|
9444
|
-
|
|
9445
|
-
|
|
9446
|
-
|
|
9447
|
-
|
|
9448
|
-
|
|
9449
|
-
})(),
|
|
9450
|
-
started_at: rl.started_at || (/* @__PURE__ */ new Date()).toISOString(),
|
|
9451
|
-
context_mode: contextMode,
|
|
9452
|
-
task: rl.task.trim()
|
|
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
|
-
|
|
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
|
-
|
|
9457
|
-
|
|
9458
|
-
|
|
9491
|
+
deactivateLoop(directory);
|
|
9492
|
+
sessionService.pushMessage({ type: "message", message: "Loop cancelled." }, "event");
|
|
9493
|
+
logger.log(`[svampConfig] Loop cancelled`);
|
|
9459
9494
|
}
|
|
9460
|
-
const {
|
|
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-
|
|
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
|
|
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 (
|
|
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
|
-
|
|
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}]
|
|
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: `
|
|
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
|
|
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 (
|
|
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
|
|
10670
|
-
if (!turnInitiatedByUser && !
|
|
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 &&
|
|
10696
|
-
logger.log(`[Session ${sessionId}] SDK-initiated result during active
|
|
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
|
-
|
|
11713
|
-
|
|
11714
|
-
|
|
11715
|
-
|
|
11716
|
-
|
|
11717
|
-
|
|
11718
|
-
|
|
11719
|
-
|
|
11720
|
-
|
|
11721
|
-
...rlState,
|
|
11722
|
-
original_resume_id: claudeResumeId
|
|
11723
|
-
});
|
|
11724
|
-
}
|
|
11725
|
-
isKillingClaude = true;
|
|
11726
|
-
await killAndWaitForExit2(claudeProcess);
|
|
11727
|
-
isKillingClaude = false;
|
|
11728
|
-
if (trackedSession?.stopped) return;
|
|
11729
|
-
if (isRestartingClaude || isSwitchingMode) return;
|
|
11730
|
-
claudeResumeId = void 0;
|
|
11731
|
-
spawnClaude(next.text, queueMeta);
|
|
11732
|
-
} catch (err) {
|
|
11733
|
-
logger.log(`[Session ${sessionId}] Error in fresh Ralph queue processing: ${err.message}`);
|
|
11734
|
-
isKillingClaude = false;
|
|
11735
|
-
sessionWasProcessing = false;
|
|
11736
|
-
signalProcessing(false);
|
|
11737
|
-
}
|
|
11738
|
-
})();
|
|
11739
|
-
} else {
|
|
11740
|
-
try {
|
|
11741
|
-
if (!claudeProcess || claudeProcess.exitCode !== null) {
|
|
11742
|
-
spawnClaude(next.text, queueMeta);
|
|
11743
|
-
} else {
|
|
11744
|
-
const stdinMsg = JSON.stringify({
|
|
11745
|
-
type: "user",
|
|
11746
|
-
message: { role: "user", content: next.text }
|
|
11747
|
-
});
|
|
11748
|
-
claudeProcess.stdin?.write(stdinMsg + "\n");
|
|
11749
|
-
}
|
|
11750
|
-
} catch (err) {
|
|
11751
|
-
logger.log(`[Session ${sessionId}] Error in processMessageQueue spawn: ${err.message}`);
|
|
11752
|
-
sessionWasProcessing = false;
|
|
11753
|
-
signalProcessing(false);
|
|
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 (
|
|
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
|
|
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-
|
|
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
|
|
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
|
-
|
|
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 (
|
|
12841
|
-
logger.log(`Resuming
|
|
12842
|
-
for (const { sessionId, directory: sessDir } of
|
|
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
|
|
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
|
-
|
|
12865
|
-
|
|
12866
|
-
const isFreshMode = currentState.context_mode === "fresh" || !currentState.context_mode;
|
|
12867
|
-
const progressRelPath = `.svamp/${sessionId}/ralph-progress.md`;
|
|
12868
|
-
const prompt = buildRalphPrompt(currentState.task, currentState);
|
|
12869
|
-
const ralphSysPrompt = buildRalphSystemPrompt(currentState, progressRelPath);
|
|
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
|
|
12418
|
+
logger.log(`Resumed loop for session ${sessionId}`);
|
|
12882
12419
|
} catch (err) {
|
|
12883
|
-
logger.log(`Failed to resume
|
|
12420
|
+
logger.log(`Failed to resume loop for session ${sessionId}: ${err.message}`);
|
|
12884
12421
|
}
|
|
12885
|
-
},
|
|
12422
|
+
}, 2e3);
|
|
12886
12423
|
} catch (err) {
|
|
12887
|
-
logger.log(`Failed to find session service for
|
|
12424
|
+
logger.log(`Failed to find session service for loop resume ${sessionId}: ${err.message}`);
|
|
12888
12425
|
}
|
|
12889
12426
|
}
|
|
12890
12427
|
}
|