31 lines
2.8 KiB
TypeScript
31 lines
2.8 KiB
TypeScript
import type { DetailJob } from './types'
|
|
import { parseDetailFactsOutput } from './text-contract'
|
|
|
|
export const detailTextRequestId = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i
|
|
export type DetailTextRecovery = { mode: 'query-only'; stage: 'product-analysis' | 'outline'; requestId: string }
|
|
export function detailTextDispatchKey(jobId: string, stage: string, checkpoint: Record<string, unknown>): string {
|
|
const attempt = checkpoint.copyAttempt as { id?: unknown; source?: unknown } | undefined
|
|
return stage === 'outline' && attempt?.source === 'known-failed-copy' && typeof attempt.id === 'string' && detailTextRequestId.test(attempt.id) ? `${jobId}-outline-${attempt.id}` : `${jobId}-${stage}`
|
|
}
|
|
export function readDetailTextRecovery(checkpoint: Record<string, unknown>): DetailTextRecovery | null {
|
|
const value = checkpoint.textRecovery as DetailTextRecovery | null
|
|
if (!value || value.mode !== 'query-only' || !['product-analysis', 'outline'].includes(value.stage) || checkpoint.stage !== value.stage || checkpoint.dispatchPending !== true || typeof value.requestId !== 'string' || !detailTextRequestId.test(value.requestId)) return null
|
|
return { mode: value.mode, stage: value.stage, requestId: value.requestId }
|
|
}
|
|
|
|
export function detailTextRecoveryOptions(job: Pick<DetailJob, 'id' | 'kind' | 'status' | 'checkpoint' | 'requestId'>) {
|
|
const unavailable = { canQuery: false, canContinue: false, needsRequestId: false }
|
|
if (job.kind !== 'analyze') return unavailable
|
|
// A definitely ended copy failure may be retried explicitly without redoing accepted analysis.
|
|
if (job.status === 'failed' && job.checkpoint.stage === 'outline' && job.checkpoint.dispatchPending === false) {
|
|
try { parseDetailFactsOutput(job.checkpoint.facts); return { canQuery: false, canContinue: true, needsRequestId: false } } catch { return unavailable }
|
|
}
|
|
if (job.status !== 'unknown') return unavailable
|
|
const recovered = job.checkpoint.recoveredText as { stage?: unknown; requestId?: unknown; dispatchKey?: unknown } | undefined
|
|
if (recovered?.stage === 'product-analysis' && recovered.dispatchKey === `${job.id}-product-analysis` && typeof recovered.requestId === 'string' && detailTextRequestId.test(recovered.requestId) && job.checkpoint.stage === 'selling-points' && job.checkpoint.dispatchPending === false) {
|
|
try { parseDetailFactsOutput(job.checkpoint.facts); return { canQuery: false, canContinue: true, needsRequestId: false } } catch { return unavailable }
|
|
}
|
|
if (!['product-analysis', 'outline'].includes(String(job.checkpoint.stage)) || job.checkpoint.dispatchPending !== true || job.checkpoint.dispatchKey !== detailTextDispatchKey(job.id, String(job.checkpoint.stage), job.checkpoint)) return unavailable
|
|
return { canQuery: true, canContinue: false, needsRequestId: !job.requestId }
|
|
}
|