From ccc7a2a271d5b2facd3af8fc4dc08f7e6eafd7a6 Mon Sep 17 00:00:00 2001 From: Tanner Linsley Date: Fri, 11 Sep 2026 13:24:30 -0600 Subject: [PATCH 1/2] Preserve validated Builder progress and distinguish incomplete runs --- .../builder/BuilderAssistant.client.tsx | 91 +++++++++++++- src/utils/builder-ai-checkpoints.client.ts | 14 ++- src/utils/builder-ai-run-outcome.ts | 41 +++++++ src/utils/builder-ai-stream.client.ts | 42 ++++--- src/utils/builder-ai.ts | 40 +++--- tests/builder-ai-checkpoints.test.ts | 33 +++++ tests/builder-ai-run-outcome.test.ts | 80 ++++++++++++ tests/builder-ai-stream.test.ts | 114 ++++++++++++++++++ 8 files changed, 415 insertions(+), 40 deletions(-) create mode 100644 src/utils/builder-ai-run-outcome.ts create mode 100644 tests/builder-ai-run-outcome.test.ts diff --git a/src/components/builder/BuilderAssistant.client.tsx b/src/components/builder/BuilderAssistant.client.tsx index ede068be2..dfa7c1b53 100644 --- a/src/components/builder/BuilderAssistant.client.tsx +++ b/src/components/builder/BuilderAssistant.client.tsx @@ -166,11 +166,13 @@ type TranscriptRow = message: string } | { id: 'error'; kind: 'error'; message: string } - | { id: 'checkpoint'; kind: 'checkpoint' } + | { id: 'checkpoint'; kind: 'checkpoint'; validated: boolean } type RollbackCheckpoint = { id: string + kind?: 'validated' execution: BuilderAiExecution + expectedExecution?: BuilderAiExecution persisted?: BuilderAiCheckpoint } @@ -544,7 +546,11 @@ export const BuilderAssistant = React.forwardRef< ) if (error) rows.push({ id: 'error', kind: 'error', message: error }) if (rollbackCheckpoint) { - rows.push({ id: 'checkpoint', kind: 'checkpoint' }) + rows.push({ + id: 'checkpoint', + kind: 'checkpoint', + validated: rollbackCheckpoint.kind === 'validated', + }) } return rows }, [ @@ -617,6 +623,7 @@ export const BuilderAssistant = React.forwardRef< return { checkpoint: { id: snapshot.checkpoint.id, + kind: snapshot.checkpoint.kind, execution: snapshot.execution, persisted: snapshot.checkpoint, }, @@ -1304,8 +1311,19 @@ export const BuilderAssistant = React.forwardRef< async function restoreRollbackCheckpoint() { const checkpoint = rollbackCheckpointRef.current if (!checkpoint) return + const generation = checkpointHydrationGenerationRef.current + const expectedCurrentExecution = serializeBuilderAiExecution(getExecution()) try { + if ( + checkpoint.expectedExecution && + serializeBuilderAiExecution(checkpoint.expectedExecution) !== + serializeBuilderAiExecution(getExecution()) + ) { + await dismissRollbackCheckpoint() + setError('The project changed after this checkpoint was created.') + return + } if ( checkpoint.persisted && !(await builderAiCheckpointMatchesExecution( @@ -1317,6 +1335,18 @@ export const BuilderAssistant = React.forwardRef< setError('The project changed after this checkpoint was created.') return } + if ( + generation !== checkpointHydrationGenerationRef.current || + rollbackCheckpointRef.current !== checkpoint + ) + return + if ( + serializeBuilderAiExecution(getExecution()) !== expectedCurrentExecution + ) { + await dismissRollbackCheckpoint() + setError('The project changed while the checkpoint was being checked.') + return + } await onRestore(cloneBuilderAiExecution(checkpoint.execution), 'manual') await dismissRollbackCheckpoint() setError('') @@ -1942,6 +1972,7 @@ export const BuilderAssistant = React.forwardRef< let checkpoint: RollbackCheckpoint | undefined let didStageExecution = false let lastStagedExecution: BuilderAiExecution | undefined + let validatedCheckpoint: RollbackCheckpoint | undefined let preserveStoppedExecution = false const activityId = pendingPrompt?.runId ?? crypto.randomUUID() let currentActivity: BuilderAiActivity | undefined @@ -2074,6 +2105,7 @@ export const BuilderAssistant = React.forwardRef< setError(withTerminalSyncFailure(message, syncFailure)) return 'error' } + await discardValidatedCheckpoint() stopActivity() const syncFailure = await finishDurableRun({ status: 'cancelled' }) if (syncFailure) { @@ -2109,6 +2141,7 @@ export const BuilderAssistant = React.forwardRef< } if (response.changedFiles.length === 0 && !response.runtimeChanged) { + await discardValidatedCheckpoint() completeActivity() const assistantMessage = createTranscriptMessage( 'assistant', @@ -2159,6 +2192,7 @@ export const BuilderAssistant = React.forwardRef< return 'error' } completeActivity() + await discardValidatedCheckpoint() const assistantMessage = createTranscriptMessage( 'assistant', response.message, @@ -2222,6 +2256,7 @@ export const BuilderAssistant = React.forwardRef< setError(withTerminalSyncFailure(message, syncFailure)) return 'error' } + await discardValidatedCheckpoint() stopActivity() const syncFailure = await finishDurableRun({ status: 'cancelled' }) if (syncFailure) { @@ -2233,7 +2268,17 @@ export const BuilderAssistant = React.forwardRef< } else { const message = formatError(cause) if (preserveStoppedExecution) await discardCheckpoint() - else await rollbackStagedExecution() + else { + await rollbackStagedExecution() + if ( + validatedCheckpoint && + serializeBuilderAiExecution(getExecution()) === + serializeBuilderAiExecution(checkpointExecution) + ) { + rollbackCheckpointRef.current = validatedCheckpoint + setRollbackCheckpoint(validatedCheckpoint) + } + } failActivity(message) const syncFailure = await finishDurableRun({ status: 'failed', @@ -2454,6 +2499,7 @@ export const BuilderAssistant = React.forwardRef< timestamp: Date.now(), }) const runResult = await onApply(state.execution, abortController.signal) + abortController.signal.throwIfAborted() updateActivity({ type: 'item-completed', runId: activityId, @@ -2471,6 +2517,31 @@ export const BuilderAssistant = React.forwardRef< }) const completion = validateBuilderAiCompletion(environmentSnapshot) if (completion.status === 'complete') { + if ( + serializeBuilderAiExecution(state.execution) !== + serializeBuilderAiExecution(checkpointExecution) + ) { + validatedCheckpoint = { + id: `${activityId}:validated`, + kind: 'validated', + execution: cloneBuilderAiExecution(state.execution), + expectedExecution: checkpointExecution, + } + if (storageScope) { + validatedCheckpoint.persisted = await createBuilderAiCheckpoint( + storageScope, + validatedCheckpoint.id, + validatedCheckpoint.execution, + { kind: 'validated', expectedExecution: checkpointExecution }, + ) + } + if (abortController.signal.aborted) { + await discardValidatedCheckpoint() + abortController.signal.throwIfAborted() + } + } else { + await discardValidatedCheckpoint() + } updateActivity({ type: 'item-completed', runId: activityId, @@ -2582,6 +2653,13 @@ export const BuilderAssistant = React.forwardRef< setRollbackCheckpoint(checkpoint) } + async function discardValidatedCheckpoint() { + if (!validatedCheckpoint) return + if (storageScope) + await removeBuilderAiCheckpoint(storageScope, validatedCheckpoint.id) + validatedCheckpoint = undefined + } + async function rollbackStagedExecution() { if (!checkpoint || !didStageExecution) return if ( @@ -3352,6 +3430,7 @@ function TranscriptRowView({ /> ) : row.kind === 'checkpoint' ? ( @@ -3367,16 +3446,20 @@ function TranscriptRowView({ } function CheckpointNotice({ + validated, onDismiss, onRestore, }: { + validated: boolean onDismiss: () => void onRestore: () => void }) { return (

- The pre-run builder is available as a checkpoint. + {validated + ? 'Validated changes are available to restore.' + : 'The pre-run builder is available as a checkpoint.'}