@@ -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
121172function 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
423501main ( ) . 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