| 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'
|
|
|