File size: 5,138 Bytes
94e1b2f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
import { createLogger } from '../../utils/logger'
import type {
  CodeRetryOptions,
  CodeRetryResult,
  RenderResult,
  RetryManagerResult,
  ChatMessage,
  CodeRetryContext,
  RetryCheckpoint
} from './types'
import type { OutputMode, PromptOverrides } from '../../types'
import { extractErrorMessage, getErrorType } from './utils'
import { retryCodeGeneration } from './code-generation'
import { JobCancelledError } from '../../utils/errors'

const logger = createLogger('CodeRetryManager')

const MAX_RETRIES = parseInt(process.env.CODE_RETRY_MAX_RETRIES || '4', 10)

export function createRetryContext(
  concept: string,
  sceneDesign: string,
  promptOverrides?: PromptOverrides,
  outputMode: OutputMode = 'video'
): CodeRetryContext {
  return {
    concept,
    sceneDesign,
    outputMode,
    promptOverrides
  }
}

export { buildRetryFixPrompt } from './prompt-builder'

export async function executeCodeRetry(
  context: CodeRetryContext,
  renderer: (code: string) => Promise<RenderResult>,
  customApiConfig?: any,
  initialCode?: string,
  onRenderFailure?: (event: {
    attempt: number
    code: string
    codeSnippet?: string
    stderr: string
    stdout: string
    peakMemoryMB: number
    exitCode?: number
  }) => Promise<void> | void,
  onCheckpoint?: RetryCheckpoint
): Promise<RetryManagerResult> {
  logger.info('Starting code retry manager', {
    concept: context.concept,
    maxRetries: MAX_RETRIES
  })

  let generationTimeMs = 0
  let currentCode = initialCode?.trim() || ''
  if (!currentCode) {
    throw new Error('Code retry requires existing code; full regeneration mode is disabled')
  }

  if (onCheckpoint) {
    await onCheckpoint()
  }

  let renderResult = await renderer(currentCode)
  let currentCodeSnippet = renderResult.codeSnippet || currentCode

  if (renderResult.success) {
    logger.info('Initial render succeeded')
    return { code: currentCode, success: true, attempts: 1, generationTimeMs }
  }

  if (onRenderFailure) {
    try {
      await onRenderFailure({
        attempt: 1,
        code: currentCode,
        codeSnippet: renderResult.codeSnippet || currentCode,
        stderr: renderResult.stderr,
        stdout: renderResult.stdout,
        peakMemoryMB: renderResult.peakMemoryMB,
        exitCode: renderResult.exitCode
      })
    } catch (error) {
      logger.warn('onRenderFailure callback failed', { attempt: 1, error: String(error) })
    }
  }

  let errorMessage = extractErrorMessage(renderResult.stderr)
  let errorType = getErrorType(renderResult.stderr)
  logger.warn('Initial render failed', { errorType, error: errorMessage })

  for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
    logger.info('Starting retry patch attempt', {
      totalAttempts: attempt + 1,
      errorType,
      error: errorMessage
    })

    try {
      if (onCheckpoint) {
        await onCheckpoint()
      }

      const generationStart = Date.now()
      currentCode = await retryCodeGeneration(
        context,
        errorMessage,
        attempt,
        currentCode,
        currentCodeSnippet,
        customApiConfig
      )
      generationTimeMs += Date.now() - generationStart

      if (onCheckpoint) {
        await onCheckpoint()
      }

      renderResult = await renderer(currentCode)
      currentCodeSnippet = renderResult.codeSnippet || currentCode

      if (renderResult.success) {
        logger.info('Retry render succeeded', { attempt: attempt + 1 })
        return { code: currentCode, success: true, attempts: attempt + 1, generationTimeMs }
      }

      if (onRenderFailure) {
        try {
          await onRenderFailure({
            attempt: attempt + 1,
            code: currentCode,
            codeSnippet: renderResult.codeSnippet || currentCode,
            stderr: renderResult.stderr,
            stdout: renderResult.stdout,
            peakMemoryMB: renderResult.peakMemoryMB,
            exitCode: renderResult.exitCode
          })
        } catch (error) {
          logger.warn('onRenderFailure callback failed', { attempt: attempt + 1, error: String(error) })
        }
      }

      errorMessage = extractErrorMessage(renderResult.stderr)
      errorType = getErrorType(renderResult.stderr)
      logger.warn('Retry render failed', { attempt: attempt + 1, errorType, error: errorMessage })
    } catch (error) {
      if (error instanceof JobCancelledError) {
        logger.warn('Code retry aborted because job was cancelled', {
          attempt: attempt + 1,
          reason: error.details
        })
        throw error
      }

      logger.error('Retry patch process failed', { attempt: attempt + 1, error: String(error) })
    }
  }

  logger.error('All retry patch attempts failed', {
    totalAttempts: MAX_RETRIES + 1,
    finalError: extractErrorMessage(renderResult.stderr)
  })

  return {
    code: currentCode,
    success: false,
    attempts: MAX_RETRIES + 1,
    generationTimeMs,
    lastError: extractErrorMessage(renderResult.stderr)
  }
}

export type {
  CodeRetryOptions,
  CodeRetryResult,
  RenderResult,
  RetryManagerResult,
  ChatMessage,
  CodeRetryContext
} from './types'