@@ -64,17 +64,19 @@ export function createRalphEventHandler(
6464 logger . error ( `Ralph: failed to commit changes in worktree ${ state . worktreeDir } ` , err )
6565 }
6666
67- try {
68- const gitCommonDir = execSync ( 'git rev-parse --git-common-dir' , { cwd : state . worktreeDir , encoding : 'utf-8' } ) . trim ( )
69- const gitRoot = resolve ( state . worktreeDir , gitCommonDir , '..' )
70- const removeResult = spawnSync ( 'git' , [ 'worktree' , 'remove' , '-f' , state . worktreeDir ] , { cwd : gitRoot , encoding : 'utf-8' } )
71- if ( removeResult . status !== 0 ) {
72- throw new Error ( removeResult . stderr || 'git worktree remove failed' )
67+ if ( state . worktreeDir && state . worktreeBranch ) {
68+ try {
69+ const gitCommonDir = execSync ( 'git rev-parse --git-common-dir' , { cwd : state . worktreeDir , encoding : 'utf-8' } ) . trim ( )
70+ const gitRoot = resolve ( state . worktreeDir , gitCommonDir , '..' )
71+ const removeResult = spawnSync ( 'git' , [ 'worktree' , 'remove' , '-f' , state . worktreeDir ] , { cwd : gitRoot , encoding : 'utf-8' } )
72+ if ( removeResult . status !== 0 ) {
73+ throw new Error ( removeResult . stderr || 'git worktree remove failed' )
74+ }
75+ cleaned = true
76+ logger . log ( `Ralph: removed worktree ${ state . worktreeDir } , branch ${ state . worktreeBranch } preserved` )
77+ } catch ( err ) {
78+ logger . error ( `Ralph: failed to remove worktree ${ state . worktreeDir } ` , err )
7379 }
74- cleaned = true
75- logger . log ( `Ralph: removed worktree ${ state . worktreeDir } , branch ${ state . worktreeBranch } preserved` )
76- } catch ( err ) {
77- logger . error ( `Ralph: failed to remove worktree ${ state . worktreeDir } ` , err )
7880 }
7981
8082 return { committed, cleaned }
@@ -188,7 +190,7 @@ export function createRalphEventHandler(
188190 logger . log ( `Ralph loop terminated: reason="${ reason } ", worktree="${ state . worktreeName } ", iteration=${ state . iteration } ` )
189191
190192 let commitResult : { committed : boolean ; cleaned : boolean } | undefined
191- if ( reason === 'completed' ) {
193+ if ( reason === 'completed' || reason === 'cancelled' ) {
192194 commitResult = await commitAndCleanupWorktree ( state )
193195 }
194196 }
@@ -227,7 +229,7 @@ export function createRalphEventHandler(
227229 }
228230 }
229231
230- async function getLastAssistantText ( sessionId : string , worktreeDir : string ) : Promise < string | null > {
232+ async function getLastAssistantInfo ( sessionId : string , worktreeDir : string ) : Promise < { text : string | null ; error : string | null } > {
231233 try {
232234 const messagesResult = await v2Client . session . messages ( {
233235 sessionID : sessionId ,
@@ -236,21 +238,25 @@ export function createRalphEventHandler(
236238 } )
237239
238240 const messages = ( messagesResult . data ?? [ ] ) as Array < {
239- info : { role : string }
241+ info : { role : string ; error ?: { name ?: string ; data ?: { message ?: string } } }
240242 parts : Array < { type : string ; text ?: string } >
241243 } >
242244
243245 const lastAssistant = [ ...messages ] . reverse ( ) . find ( ( m ) => m . info . role === 'assistant' )
244246
245- if ( ! lastAssistant ) return null
247+ if ( ! lastAssistant ) return { text : null , error : null }
246248
247- return lastAssistant . parts
249+ const text = lastAssistant . parts
248250 . filter ( ( p ) => p . type === 'text' && typeof p . text === 'string' )
249251 . map ( ( p ) => p . text as string )
250- . join ( '\n' )
252+ . join ( '\n' ) || null
253+
254+ const error = lastAssistant . info . error ?. data ?. message ?? lastAssistant . info . error ?. name ?? null
255+
256+ return { text, error }
251257 } catch ( err ) {
252258 logger . error ( `Ralph: could not read session messages` , err )
253- return null
259+ return { text : null , error : null }
254260 }
255261 }
256262
@@ -292,9 +298,31 @@ export function createRalphEventHandler(
292298 return
293299 }
294300
301+ if ( ! currentState . worktreeDir ) {
302+ logger . error ( `Ralph: loop ${ sessionId } missing worktreeDir in coding phase, terminating` )
303+ await terminateLoop ( sessionId , currentState , 'missing_worktree_dir' )
304+ return
305+ }
306+
307+ let assistantErrorDetected = false
295308 if ( currentState . completionPromise ) {
296- const textContent = await getLastAssistantText ( sessionId , currentState . worktreeDir )
297- if ( textContent && ralphService . checkCompletionPromise ( textContent , currentState . completionPromise ) ) {
309+ const { text : textContent , error : assistantError } = await getLastAssistantInfo ( sessionId , currentState . worktreeDir )
310+ if ( assistantError ) {
311+ assistantErrorDetected = true
312+ logger . error ( `Ralph: assistant error detected in coding phase: ${ assistantError } ` )
313+ const isModelError = / p r o v i d e r | a u t h | m o d e l | a p i \s * e r r o r / i. test ( assistantError )
314+ if ( isModelError ) {
315+ const nextErrorCount = ( currentState . errorCount ?? 0 ) + 1
316+ if ( nextErrorCount >= MAX_RETRIES ) {
317+ await terminateLoop ( sessionId , currentState , `error_max_retries: assistant error: ${ assistantError } ` )
318+ return
319+ }
320+ ralphService . setState ( sessionId , { ...currentState , modelFailed : true , errorCount : nextErrorCount } )
321+ logger . log ( `Ralph: marking model as failed, will fall back to default model (error ${ nextErrorCount } /${ MAX_RETRIES } )` )
322+ currentState = ralphService . getActiveState ( sessionId ) !
323+ }
324+ }
325+ if ( textContent && currentState . completionPromise && ralphService . checkCompletionPromise ( textContent , currentState . completionPromise ) ) {
298326 const currentAuditCount = currentState . auditCount ?? 0
299327 if ( ! currentState . audit || currentAuditCount >= minAudits ) {
300328 await terminateLoop ( sessionId , currentState , 'completed' )
@@ -305,14 +333,20 @@ export function createRalphEventHandler(
305333 }
306334 }
307335
308- if ( currentState . maxIterations > 0 && currentState . iteration >= currentState . maxIterations ) {
336+ if ( ! assistantErrorDetected && currentState . errorCount && currentState . errorCount > 0 ) {
337+ ralphService . setState ( sessionId , { ...currentState , errorCount : 0 } )
338+ logger . log ( `Ralph: resetting error count after successful retry in coding phase` )
339+ currentState = ralphService . getActiveState ( sessionId ) !
340+ }
341+
342+ if ( ( currentState . maxIterations ?? 0 ) > 0 && ( currentState . iteration ?? 0 ) >= ( currentState . maxIterations ?? 0 ) ) {
309343 await terminateLoop ( sessionId , currentState , 'max_iterations' )
310344 return
311345 }
312346
313347 if ( currentState . audit ) {
314348 ralphService . setState ( sessionId , { ...currentState , phase : 'auditing' , errorCount : 0 } )
315- logger . log ( `Ralph iteration ${ currentState . iteration } complete, running auditor for session ${ sessionId } ` )
349+ logger . log ( `Ralph iteration ${ currentState . iteration ?? 0 } complete, running auditor for session ${ sessionId } ` )
316350
317351 const auditPrompt = {
318352 sessionID : sessionId ,
@@ -353,19 +387,26 @@ export function createRalphEventHandler(
353387 logger . error ( `Ralph: session rotation failed, continuing with existing session` , err )
354388 }
355389
356- const nextIteration = currentState . iteration + 1
390+ const nextIteration = ( currentState . iteration ?? 0 ) + 1
357391 ralphService . setState ( activeSessionId , {
358392 ...currentState ,
359393 sessionId : activeSessionId ,
360394 iteration : nextIteration ,
361- errorCount : 0 ,
395+ errorCount : assistantErrorDetected ? currentState . errorCount : 0 ,
362396 } )
363397
364398 const continuationPrompt = ralphService . buildContinuationPrompt ( { ...currentState , iteration : nextIteration } )
365399 logger . log ( `Ralph iteration ${ nextIteration } for session ${ activeSessionId } ` )
366400
367401 const currentConfig = getConfig ( )
368- const ralphModel = parseModelString ( currentConfig . ralph ?. model ) ?? parseModelString ( currentConfig . executionModel )
402+ const freshStateForModel = ralphService . getActiveState ( activeSessionId )
403+ const ralphModel = freshStateForModel ?. modelFailed
404+ ? undefined
405+ : ( parseModelString ( currentConfig . ralph ?. model ) ?? parseModelString ( currentConfig . executionModel ) )
406+
407+ if ( freshStateForModel ?. modelFailed ) {
408+ logger . log ( `Ralph: configured model previously failed, using default model` )
409+ }
369410
370411 const sendContinuationPromptWithModel = async ( ) => {
371412 const freshState = ralphService . getActiveState ( activeSessionId )
@@ -376,7 +417,7 @@ export function createRalphEventHandler(
376417 sessionID : activeSessionId ,
377418 directory : freshState . worktreeDir ,
378419 parts : [ { type : 'text' as const , text : continuationPrompt } ] ,
379- model : ralphModel ! ,
420+ model : ralphModel ,
380421 } )
381422 return { data : result . data , error : result . error }
382423 }
@@ -405,7 +446,8 @@ export function createRalphEventHandler(
405446 const retryFn = async ( ) => {
406447 const result = await sendContinuationPromptWithoutModel ( )
407448 if ( result . error ) {
408- throw result . error
449+ await handlePromptError ( activeSessionId , currentState , 'retry failed' , result . error )
450+ return
409451 }
410452 }
411453 await handlePromptError ( activeSessionId , currentState , 'failed to send continuation prompt' , promptResult . error , retryFn )
@@ -423,17 +465,46 @@ export function createRalphEventHandler(
423465
424466 async function handleAuditingPhase ( sessionId : string , state : RalphState ) : Promise < void > {
425467 // Re-fetch and validate state to catch aborts that happened during idle event processing
426- const currentState = ralphService . getActiveState ( sessionId )
468+ let currentState = ralphService . getActiveState ( sessionId )
427469 if ( ! currentState ?. active ) {
428470 logger . log ( `Ralph: loop ${ sessionId } no longer active, skipping auditing phase` )
429471 return
430472 }
431473
432- const auditText = await getLastAssistantText ( sessionId , currentState . worktreeDir )
474+ if ( ! currentState . worktreeDir ) {
475+ logger . error ( `Ralph: loop ${ sessionId } missing worktreeDir in auditing phase, terminating` )
476+ await terminateLoop ( sessionId , currentState , 'missing_worktree_dir' )
477+ return
478+ }
479+
480+ const { text : auditText , error : assistantError } = await getLastAssistantInfo ( sessionId , currentState . worktreeDir )
481+
482+ let assistantErrorDetected = false
483+ if ( assistantError ) {
484+ assistantErrorDetected = true
485+ logger . error ( `Ralph: assistant error detected in auditing phase: ${ assistantError } ` )
486+ const isModelError = / p r o v i d e r | a u t h | m o d e l | a p i \s * e r r o r / i. test ( assistantError )
487+ if ( isModelError ) {
488+ const nextErrorCount = ( currentState . errorCount ?? 0 ) + 1
489+ if ( nextErrorCount >= MAX_RETRIES ) {
490+ await terminateLoop ( sessionId , currentState , `error_max_retries: assistant error: ${ assistantError } ` )
491+ return
492+ }
493+ ralphService . setState ( sessionId , { ...currentState , modelFailed : true , errorCount : nextErrorCount } )
494+ logger . log ( `Ralph: marking model as failed, will fall back to default model (error ${ nextErrorCount } /${ MAX_RETRIES } )` )
495+ currentState = ralphService . getActiveState ( sessionId ) !
496+ }
497+ }
433498
434- const nextIteration = currentState . iteration + 1
499+ if ( ! assistantErrorDetected && currentState . errorCount && currentState . errorCount > 0 ) {
500+ ralphService . setState ( sessionId , { ...currentState , errorCount : 0 } )
501+ logger . log ( `Ralph: resetting error count after successful retry in auditing phase` )
502+ currentState = ralphService . getActiveState ( sessionId ) !
503+ }
504+
505+ const nextIteration = ( currentState . iteration ?? 0 ) + 1
435506 const newAuditCount = ( currentState . auditCount ?? 0 ) + 1
436- logger . log ( `Ralph audit ${ newAuditCount } at iteration ${ currentState . iteration } ` )
507+ logger . log ( `Ralph audit ${ newAuditCount } at iteration ${ currentState . iteration ?? 0 } ` )
437508
438509 // Always pass the full audit response to the code agent
439510 const auditFindings = auditText ?? undefined
@@ -450,7 +521,7 @@ export function createRalphEventHandler(
450521 }
451522 }
452523
453- if ( currentState . maxIterations > 0 && nextIteration > currentState . maxIterations ) {
524+ if ( ( currentState . maxIterations ?? 0 ) > 0 && nextIteration > ( currentState . maxIterations ?? 0 ) ) {
454525 await terminateLoop ( sessionId , currentState , 'max_iterations' )
455526 return
456527 }
@@ -469,7 +540,7 @@ export function createRalphEventHandler(
469540 phase : 'coding' ,
470541 lastAuditResult : auditFindings ,
471542 auditCount : newAuditCount ,
472- errorCount : 0 ,
543+ errorCount : assistantErrorDetected ? currentState . errorCount : 0 ,
473544 } )
474545
475546 const continuationPrompt = ralphService . buildContinuationPrompt (
@@ -479,7 +550,14 @@ export function createRalphEventHandler(
479550 logger . log ( `Ralph iteration ${ nextIteration } for session ${ activeSessionId } ` )
480551
481552 const currentConfig = getConfig ( )
482- const ralphModel = parseModelString ( currentConfig . ralph ?. model ) ?? parseModelString ( currentConfig . executionModel )
553+ const freshStateForModel = ralphService . getActiveState ( activeSessionId )
554+ const ralphModel = freshStateForModel ?. modelFailed
555+ ? undefined
556+ : ( parseModelString ( currentConfig . ralph ?. model ) ?? parseModelString ( currentConfig . executionModel ) )
557+
558+ if ( freshStateForModel ?. modelFailed ) {
559+ logger . log ( `Ralph: configured model previously failed, using default model` )
560+ }
483561
484562 const sendContinuationPromptWithModel = async ( ) => {
485563 const freshState = ralphService . getActiveState ( activeSessionId )
@@ -490,7 +568,7 @@ export function createRalphEventHandler(
490568 sessionID : activeSessionId ,
491569 directory : freshState . worktreeDir ,
492570 parts : [ { type : 'text' as const , text : continuationPrompt } ] ,
493- model : ralphModel ! ,
571+ model : ralphModel ,
494572 } )
495573 return { data : result . data , error : result . error }
496574 }
@@ -523,7 +601,8 @@ export function createRalphEventHandler(
523601 }
524602 const result = await sendContinuationPromptWithoutModel ( )
525603 if ( result . error ) {
526- throw result . error
604+ await handlePromptError ( activeSessionId , currentState , 'retry failed after audit' , result . error )
605+ return
527606 }
528607 }
529608 await handlePromptError ( activeSessionId , currentState , 'failed to send continuation prompt after audit' , promptResult . error , retryFn )
@@ -558,17 +637,31 @@ export function createRalphEventHandler(
558637 }
559638
560639 if ( event . type === 'session.error' ) {
561- const errorProps = event . properties as { sessionID ?: string ; error ?: { name ?: string } }
640+ const errorProps = event . properties as { sessionID ?: string ; error ?: { name ?: string ; data ?: { message ?: string } } }
562641 const eventSessionId = errorProps ?. sessionID
563642 const errorName = errorProps ?. error ?. name
564643 const isAbort = errorName === 'MessageAbortedError' || errorName === 'AbortError'
565644
566- if ( ! eventSessionId || ! isAbort ) return
645+ if ( ! eventSessionId ) return
646+
647+ if ( isAbort ) {
648+ const state = ralphService . getActiveState ( eventSessionId )
649+ if ( state ?. active ) {
650+ logger . log ( `Ralph: session ${ eventSessionId } aborted, terminating loop` )
651+ await terminateLoop ( eventSessionId , state , 'user_aborted' )
652+ }
653+ return
654+ }
567655
568656 const state = ralphService . getActiveState ( eventSessionId )
569657 if ( state ?. active ) {
570- logger . log ( `Ralph: session ${ eventSessionId } aborted, terminating loop` )
571- await terminateLoop ( eventSessionId , state , 'user_aborted' )
658+ const errorMessage = errorProps ?. error ?. data ?. message ?? errorName ?? 'unknown error'
659+ logger . error ( `Ralph: session error for ${ eventSessionId } : ${ errorMessage } ` )
660+ const isModelError = / p r o v i d e r | a u t h | m o d e l | a p i \s * e r r o r / i. test ( errorMessage )
661+ if ( isModelError && ! state . modelFailed ) {
662+ logger . log ( `Ralph: marking model as failed, will fall back to default on next iteration` )
663+ ralphService . setState ( eventSessionId , { ...state , modelFailed : true } )
664+ }
572665 }
573666 return
574667 }
0 commit comments