Skip to content

Commit abb17d1

Browse files
author
Developer
committed
Add structured logging, failure tracking, and ralph:logs command
1 parent d481060 commit abb17d1

6 files changed

Lines changed: 369 additions & 47 deletions

File tree

config/ralph.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
'permission_mode' => env('RALPH_PERMISSION_MODE', 'acceptEdits'),
1313
'model' => env('RALPH_MODEL'),
1414
'completion_marker' => '<promise>COMPLETE</promise>',
15+
'max_consecutive_failures' => (int) env('RALPH_MAX_CONSECUTIVE_FAILURES', 3),
1516
],
1617

1718
/*
@@ -56,6 +57,7 @@
5657

5758
'logging' => [
5859
'directory' => storage_path('ralph-logs'),
60+
'non_json_warn_threshold' => 50,
5961
],
6062

6163
/*

scripts/ralph-loop.cjs

Lines changed: 121 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,15 @@ function parseArgs() {
4141
sessionId: null,
4242
budget: null,
4343
fresh: false,
44+
logPath: null,
45+
maxConsecutiveFailures: parseInt(
46+
process.env.AGENT_MAX_CONSECUTIVE_FAILURES || "3",
47+
10,
48+
),
49+
nonJsonWarnThreshold: parseInt(
50+
process.env.AGENT_NON_JSON_WARN_THRESHOLD || "50",
51+
10,
52+
),
4453
};
4554

4655
for (let i = 0; i < args.length; i++) {
@@ -69,6 +78,9 @@ function parseArgs() {
6978
case "--fresh":
7079
parsed.fresh = true;
7180
break;
81+
case "--log-path":
82+
parsed.logPath = args[++i];
83+
break;
7284
}
7385
}
7486

@@ -91,31 +103,70 @@ const color = {
91103

92104
// ── Logging ───────────────────────────────────────────────────────
93105

94-
function createLogger(name) {
95-
const logDir =
96-
process.env.AGENT_LOG_DIR || path.join("storage", "ralph-logs");
97-
const agentLogDir = path.join(logDir, name);
106+
function createLogger(name, logPath) {
107+
let resolvedPath = logPath;
108+
109+
if (!resolvedPath) {
110+
const logDir =
111+
process.env.AGENT_LOG_DIR || path.join("storage", "ralph-logs");
112+
const agentLogDir = path.join(logDir, name);
113+
fs.mkdirSync(agentLogDir, { recursive: true });
114+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
115+
resolvedPath = path.join(agentLogDir, `${timestamp}.log`);
116+
} else {
117+
fs.mkdirSync(path.dirname(resolvedPath), { recursive: true });
118+
}
98119

99-
fs.mkdirSync(agentLogDir, { recursive: true });
120+
const stream = fs.createWriteStream(resolvedPath, { flags: "a" });
100121

101-
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
102-
const logPath = path.join(agentLogDir, `${timestamp}.log`);
103-
const stream = fs.createWriteStream(logPath, { flags: "a" });
122+
function formatLevel(level, message) {
123+
return `[${new Date().toISOString()}] [${level}] ${message}\n`;
124+
}
104125

105126
return {
106-
path: logPath,
127+
path: resolvedPath,
107128
write(text) {
108129
stream.write(text);
109130
},
110131
writeLine(text) {
111132
stream.write(text + "\n");
112133
},
134+
debug(message) {
135+
stream.write(formatLevel("DEBUG", message));
136+
},
137+
info(message) {
138+
stream.write(formatLevel("INFO", message));
139+
},
140+
warn(message) {
141+
stream.write(formatLevel("WARN", message));
142+
},
143+
error(message) {
144+
stream.write(formatLevel("ERROR", message));
145+
},
113146
close() {
114147
stream.end();
115148
},
116149
};
117150
}
118151

152+
// ── Session summary ──────────────────────────────────────────────
153+
154+
function logSummary(logger, reason, iteration, totalIterations, consecutiveFailures) {
155+
const lines = [
156+
"",
157+
"=== Session Summary ===",
158+
`Reason: ${reason}`,
159+
`Iterations: ${iteration}/${totalIterations}`,
160+
`Consecutive failures: ${consecutiveFailures}`,
161+
`Timestamp: ${new Date().toISOString()}`,
162+
"========================",
163+
"",
164+
];
165+
for (const line of lines) {
166+
logger.info(line);
167+
}
168+
}
169+
119170
// ── Prompt resolution ─────────────────────────────────────────────
120171

121172
function resolvePrompt(promptArg) {
@@ -172,7 +223,7 @@ function truncate(str, maxLen) {
172223

173224
// ── Run single Claude iteration ───────────────────────────────────
174225

175-
function runClaude(claudeArgs, logger) {
226+
function runClaude(claudeArgs, logger, nonJsonWarnThreshold) {
176227
return new Promise((resolve, reject) => {
177228
// Remove CLAUDECODE to prevent "nested session" detection when Ralph
178229
// is started from within a Claude Code terminal
@@ -186,6 +237,7 @@ function runClaude(claudeArgs, logger) {
186237

187238
let output = "";
188239
let completionDetected = false;
240+
let nonJsonLineCount = 0;
189241
const completionMarker =
190242
process.env.AGENT_COMPLETION_MARKER || "<promise>COMPLETE</promise>";
191243

@@ -215,6 +267,7 @@ function runClaude(claudeArgs, logger) {
215267
output += event.result;
216268
}
217269
} catch {
270+
nonJsonLineCount++;
218271
// Non-JSON line, just log it
219272
if (line.trim()) {
220273
console.log(`${color.dim}${line}${color.reset}`);
@@ -231,6 +284,16 @@ function runClaude(claudeArgs, logger) {
231284
proc.on("close", (code) => {
232285
completionDetected = output.includes(completionMarker);
233286

287+
logger.debug(`Stream stats: ${nonJsonLineCount} non-JSON lines`);
288+
if (nonJsonLineCount > nonJsonWarnThreshold) {
289+
logger.warn(
290+
`High non-JSON line count: ${nonJsonLineCount} (threshold: ${nonJsonWarnThreshold})`,
291+
);
292+
console.warn(
293+
`${color.yellow}Warning: ${nonJsonLineCount} non-JSON lines in stream (threshold: ${nonJsonWarnThreshold})${color.reset}`,
294+
);
295+
}
296+
234297
resolve({
235298
exitCode: code,
236299
completionDetected,
@@ -293,7 +356,7 @@ async function main() {
293356
const promptSuffix = process.env.AGENT_PROMPT_SUFFIX || "";
294357
const continuationPrompt = process.env.AGENT_CONTINUATION_PROMPT || "Continue working on the task.";
295358
const basePrompt = resolvePrompt(config.prompt);
296-
const logger = createLogger(config.name);
359+
const logger = createLogger(config.name, config.logPath);
297360

298361
const fullPrompt = promptSuffix
299362
? `${basePrompt}\n\n${promptSuffix}`
@@ -328,99 +391,115 @@ async function main() {
328391
);
329392
console.log();
330393

331-
logger.writeLine(
332-
`[${new Date().toISOString()}] Starting ralph loop: ${config.name}`,
333-
);
334-
logger.writeLine(`Iterations: ${config.iterations}`);
335-
logger.writeLine(`Permission mode: ${config.permissionMode}`);
336-
logger.writeLine(`Model: ${config.model || "default"}`);
337-
logger.writeLine(`Session ID: ${config.sessionId || "none"}`);
338-
logger.writeLine(`Resume: ${resumeMode ? "enabled" : "disabled"}`);
339-
logger.writeLine(`Prompt: ${basePrompt.slice(0, 200)}...`);
394+
logger.info(`Starting ralph loop: ${config.name}`);
395+
logger.info(`Iterations: ${config.iterations}`);
396+
logger.info(`Permission mode: ${config.permissionMode}`);
397+
logger.info(`Model: ${config.model || "default"}`);
398+
logger.info(`Session ID: ${config.sessionId || "none"}`);
399+
logger.info(`Resume: ${resumeMode ? "enabled" : "disabled"}`);
400+
logger.info(`Max consecutive failures: ${config.maxConsecutiveFailures}`);
401+
logger.debug(`Prompt: ${basePrompt.slice(0, 200)}...`);
340402
logger.writeLine("---");
341403

404+
let consecutiveFailures = 0;
405+
342406
for (let i = 1; i <= config.iterations; i++) {
343407
console.log(
344408
`\n${color.bold}${color.yellow}── Iteration ${i}/${config.iterations} ──${color.reset}\n`,
345409
);
346-
logger.writeLine(
347-
`\n[${new Date().toISOString()}] === Iteration ${i}/${config.iterations} ===`,
348-
);
410+
logger.info(`=== Iteration ${i}/${config.iterations} ===`);
349411

350412
// Use full prompt for iteration 1 (or all iterations in fresh mode),
351413
// continuation prompt for subsequent iterations in resume mode
352414
const prompt = (i === 1 || config.fresh) ? fullPrompt : continuePrompt;
353415
const claudeArgs = buildClaudeArgs(config, prompt, i);
354416

355-
logger.writeLine(`Claude args: ${JSON.stringify(claudeArgs)}`);
417+
logger.debug(`Claude args: ${JSON.stringify(claudeArgs)}`);
356418

357419
try {
358-
const result = await runClaude(claudeArgs, logger);
420+
const result = await runClaude(claudeArgs, logger, config.nonJsonWarnThreshold);
359421

360422
if (result.completionDetected) {
361423
console.log(
362424
`\n${color.bold}${color.green}✓ Completion marker detected on iteration ${i}. Done!${color.reset}`,
363425
);
364-
logger.writeLine(
365-
`[${new Date().toISOString()}] Completion detected on iteration ${i}`,
366-
);
426+
logger.info(`Completion detected on iteration ${i}`);
427+
logSummary(logger, "completion_marker_detected", i, config.iterations, consecutiveFailures);
367428
logger.close();
368429
process.exit(0);
369430
}
370431

432+
if (result.exitCode === 0) {
433+
consecutiveFailures = 0;
434+
}
435+
371436
if (result.exitCode !== 0) {
437+
consecutiveFailures++;
438+
logger.warn(`Claude exited with code ${result.exitCode} (consecutive failures: ${consecutiveFailures})`);
372439
console.log(
373-
`\n${color.yellow}Claude exited with code ${result.exitCode}${color.reset}`,
374-
);
375-
logger.writeLine(
376-
`[${new Date().toISOString()}] Claude exited with code ${result.exitCode}`,
440+
`\n${color.yellow}Claude exited with code ${result.exitCode} (failures: ${consecutiveFailures}/${config.maxConsecutiveFailures})${color.reset}`,
377441
);
378442

379443
// If resume failed on iteration 2+, fall back to fresh
380444
if (i > 1 && resumeMode && result.exitCode !== 0) {
381445
console.log(
382446
`${color.yellow}Resume may have failed, retrying as fresh...${color.reset}`,
383447
);
384-
logger.writeLine("Retrying iteration as fresh invocation");
448+
logger.info("Retrying iteration as fresh invocation");
385449

386450
const freshArgs = ["-p", fullPrompt, "--verbose", "--output-format", "stream-json", "--permission-mode", config.permissionMode];
387451
if (config.model) freshArgs.push("--model", config.model);
388452
if (config.budget) freshArgs.push("--max-budget-usd", config.budget);
389453

390-
const retryResult = await runClaude(freshArgs, logger);
454+
const retryResult = await runClaude(freshArgs, logger, config.nonJsonWarnThreshold);
391455

392456
if (retryResult.completionDetected) {
393457
console.log(
394458
`\n${color.bold}${color.green}✓ Completion marker detected on retry. Done!${color.reset}`,
395459
);
460+
logSummary(logger, "completion_marker_detected", i, config.iterations, consecutiveFailures);
396461
logger.close();
397462
process.exit(0);
398463
}
464+
465+
// Failed retry within same iteration doesn't increment counter again
466+
}
467+
468+
if (consecutiveFailures >= config.maxConsecutiveFailures) {
469+
console.error(
470+
`\n${color.red}Consecutive failure threshold (${config.maxConsecutiveFailures}) reached. Stopping.${color.reset}`,
471+
);
472+
logSummary(logger, "consecutive_failures_exceeded", i, config.iterations, consecutiveFailures);
473+
logger.close();
474+
process.exit(1);
399475
}
400476
}
401477
} catch (err) {
478+
consecutiveFailures++;
479+
logger.error(`Exception on iteration ${i}: ${err.message}`);
402480
console.error(
403481
`\n${color.red}Error on iteration ${i}: ${err.message}${color.reset}`,
404482
);
405-
logger.writeLine(
406-
`[${new Date().toISOString()}] Error: ${err.message}`,
407-
);
408-
logger.close();
409-
process.exit(1);
483+
484+
if (consecutiveFailures >= config.maxConsecutiveFailures) {
485+
logSummary(logger, "consecutive_exceptions_exceeded", i, config.iterations, consecutiveFailures);
486+
logger.close();
487+
process.exit(1);
488+
}
489+
// Otherwise: continue to next iteration
410490
}
411491
}
412492

413493
console.log(
414494
`\n${color.bold}${color.yellow}Max iterations (${config.iterations}) reached.${color.reset}`,
415495
);
416-
logger.writeLine(
417-
`[${new Date().toISOString()}] Max iterations reached`,
418-
);
496+
logSummary(logger, "max_iterations_reached", config.iterations, config.iterations, consecutiveFailures);
419497
logger.close();
420498
process.exit(2);
421499
}
422500

423501
main().catch((err) => {
424502
console.error(`${color.red}Fatal error: ${err.message}${color.reset}`);
503+
// Cannot log summary here — logger may not exist yet
425504
process.exit(1);
426505
});

0 commit comments

Comments
 (0)