File size: 6,382 Bytes
b1df986
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
import { performance } from 'node:perf_hooks'
import { createLogger } from '../../shared/logging/logger'
import type {
  CodeRetryContext,
  RenderResult,
  RetryManagerResult,
  RetryCheckpoint
} from './types'
import type { OutputMode, PromptOverrides, ProblemFramingPlan, ReferenceImage } from '../../types'
import { extractErrorContext, extractErrorMessage, getErrorType } from './utils'
import { retryCodeGeneration } from './code-generation'
import { JobCancelledError } from '../../shared/errors/errors'
import { classifyRenderFailure } from './render-failure-policy'

const logger = createLogger('CodeRetryManager')

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

export function createRetryContext(
  concept: string,
  sceneDesign: string,
  promptOverrides?: PromptOverrides,
  outputMode: OutputMode = 'video',
  problemPlan?: ProblemFramingPlan,
  referenceImages?: ReferenceImage[]
): CodeRetryContext {
  return {
    referenceImages,
    problemPlan,
    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,
  timings?: Record<string, number>,
  onRetryStart?: () => Promise<void> | void
): 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

  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,
        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 = extractErrorContext(renderResult.stderr)
  let errorType = getErrorType(renderResult.stderr)
  const failureDecision = classifyRenderFailure(renderResult.stderr)
  logger.warn('Initial render failed', { errorType, error: errorMessage })

  if (!failureDecision.allowAiRepair) {
    logger.warn('Skipping AI code retry for non-repairable render failure', {
      category: failureDecision.category,
      reason: failureDecision.reason
    })
    return {
      code: currentCode,
      success: false,
      attempts: 1,
      generationTimeMs,
      lastError: `${failureDecision.reason}: ${extractErrorMessage(renderResult.stderr)}`,
      failureCategory: failureDecision.category,
      retrySkipped: true
    }
  }

  if (onRetryStart) {
    await onRetryStart()
    if (timings) timings.retry = generationTimeMs
  }

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

    const generationStart = performance.now()
    let generationRecorded = false
    try {
      if (onCheckpoint) {
        await onCheckpoint()
      }

      currentCode = await retryCodeGeneration(
        context,
        errorMessage,
        attempt,
        currentCode,
        currentCodeSnippet,
        customApiConfig,
        onCheckpoint
      )
      generationTimeMs += performance.now() - generationStart
      generationRecorded = true
      if (timings) timings.retry = generationTimeMs

      if (onCheckpoint) {
        await onCheckpoint()
      }

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

      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 = extractErrorContext(renderResult.stderr)
      errorType = getErrorType(renderResult.stderr)
      logger.warn('Retry render failed', { attempt: attempt + 1, errorType, error: errorMessage })
    } catch (error) {
      if (!generationRecorded) {
        generationTimeMs += performance.now() - generationStart
        if (timings) timings.retry = generationTimeMs
      }

      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'