@@ -518,88 +518,84 @@ export class Conductor {
518518 // 3. Run after_create hook (restore context)
519519 await this . runHook ( 'after-create' , workspacePath , issue ) ;
520520
521- // 4. Spawn agent
522- run . status = 'running' ;
523- await this . runAgent ( issue , run ) ;
524-
525- // 5. Success path
526- run . status = 'completed' ;
527- this . completeCount ++ ;
528-
529- // Run after_run hook (capture context)
530- await this . runHook ( 'after-run' , workspacePath , issue , run . attempt ) ;
531-
532- // Take snapshot for session continuity
533- this . takeSnapshot ( workspacePath , issue ) ;
534-
535- // Move to In Review
536- await this . transitionIssue ( issue , this . config . inReviewState ) ;
537-
538- console . log ( `[${ issue . identifier } ] Completed successfully` ) ;
521+ // 4. Attempt agent run (with retries)
522+ await this . attemptRun ( issue , run ) ;
539523 } catch ( err ) {
540- run . status = 'failed' ;
541- run . error = ( err as Error ) . message ;
524+ this . failCount ++ ;
525+ console . log ( `[${ issue . identifier } ] Failed: ${ ( err as Error ) . message } ` ) ;
526+ } finally {
527+ this . running . delete ( issueId ) ;
528+ // Keep claimed so we don't re-dispatch within this session
529+ }
530+ }
542531
543- logger . error ( 'Issue dispatch failed' , {
544- identifier : issue . identifier ,
545- error : run . error ,
546- attempt : run . attempt ,
547- } ) ;
532+ /**
533+ * Run the agent with retry logic. Throws on final failure.
534+ */
535+ private async attemptRun (
536+ issue : LinearIssue ,
537+ run : RunningIssue
538+ ) : Promise < void > {
539+ const maxAttempts = this . config . maxRetries + 1 ;
540+
541+ while ( run . attempt <= maxAttempts ) {
542+ try {
543+ run . status = 'running' ;
544+ await this . runAgent ( issue , run ) ;
548545
549- // Run after_run hook even on failure
550- if ( run . workspacePath ) {
546+ // Success
547+ run . status = 'completed' ;
548+ this . completeCount ++ ;
551549 await this . runHook (
552550 'after-run' ,
553551 run . workspacePath ,
554552 issue ,
555553 run . attempt
556554 ) . catch ( ( ) => { } ) ;
557- }
558-
559- // Retry logic
560- if ( run . attempt < this . config . maxRetries + 1 ) {
555+ this . takeSnapshot ( run . workspacePath , issue ) ;
556+ await this . transitionIssue ( issue , this . config . inReviewState ) ;
561557 console . log (
562- `[${ issue . identifier } ] Failed (attempt ${ run . attempt } ), retrying...`
558+ run . attempt === 1
559+ ? `[${ issue . identifier } ] Completed successfully`
560+ : `[${ issue . identifier } ] Completed on retry ${ run . attempt } `
563561 ) ;
564- run . attempt ++ ;
565- this . totalAttempts ++ ;
562+ return ;
563+ } catch ( err ) {
564+ run . status = 'failed' ;
565+ run . error = ( err as Error ) . message ;
566566
567- // Exponential backoff
568- const backoffMs = Math . min ( 1000 * Math . pow ( 2 , run . attempt - 1 ) , 300000 ) ;
569- await new Promise ( ( r ) => setTimeout ( r , backoffMs ) ) ;
567+ logger . error ( 'Agent run failed' , {
568+ identifier : issue . identifier ,
569+ error : run . error ,
570+ attempt : run . attempt ,
571+ } ) ;
570572
571- if ( ! this . stopping ) {
572- try {
573- run . status = 'running' ;
574- await this . runAgent ( issue , run ) ;
575- run . status = 'completed' ;
576- this . completeCount ++ ;
577- await this . runHook (
578- 'after-run' ,
579- run . workspacePath ,
580- issue ,
581- run . attempt
582- ) . catch ( ( ) => { } ) ;
583- await this . transitionIssue ( issue , this . config . inReviewState ) ;
584- console . log (
585- `[ ${ issue . identifier } ] Completed on retry ${ run . attempt } `
586- ) ;
587- } catch ( retryErr ) {
588- run . status = 'failed' ;
589- run . error = ( retryErr as Error ) . message ;
590- this . failCount ++ ;
591- console . log (
592- `[ ${ issue . identifier } ] Failed after ${ run . attempt } attempts: ${ run . error } `
593- ) ;
594- }
573+ // Run after_run hook even on failure
574+ if ( run . workspacePath ) {
575+ await this . runHook (
576+ 'after- run' ,
577+ run . workspacePath ,
578+ issue ,
579+ run . attempt
580+ ) . catch ( ( ) => { } ) ;
581+ }
582+
583+ // If more attempts remain, retry with backoff
584+ if ( run . attempt < maxAttempts && ! this . stopping ) {
585+ console . log (
586+ `[ ${ issue . identifier } ] Failed (attempt ${ run . attempt } ), retrying...`
587+ ) ;
588+ run . attempt ++ ;
589+ this . totalAttempts ++ ;
590+ const backoffMs = Math . min (
591+ 1000 * Math . pow ( 2 , run . attempt - 1 ) ,
592+ 300000
593+ ) ;
594+ await new Promise ( ( r ) => setTimeout ( r , backoffMs ) ) ;
595+ } else {
596+ throw err ;
595597 }
596- } else {
597- this . failCount ++ ;
598- console . log ( `[${ issue . identifier } ] Failed: ${ run . error } ` ) ;
599598 }
600- } finally {
601- this . running . delete ( issueId ) ;
602- // Keep claimed so we don't re-dispatch within this session
603599 }
604600 }
605601
0 commit comments