Skip to content

Resume interrupted Clips uploads - #2436

Open
3mdistal wants to merge 3 commits into
BuilderIO:mainfrom
3mdistal:codex/clips-upload-recovery
Open

Resume interrupted Clips uploads#2436
3mdistal wants to merge 3 commits into
BuilderIO:mainfrom
3mdistal:codex/clips-upload-recovery

Conversation

@3mdistal

@3mdistal 3mdistal commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Problem

When a Clips desktop upload failed, retry could replay the entire local recording even when the storage provider had already accepted most of it. That made retries take minutes, and some failed recordings could remain in a loading state because interruption cleanup discarded the resumable session before a retry could reclaim it.

Approach

Treat the server and storage provider as the authority for retry progress. Preserve recoverable upload state, ask the server for the last committed byte, and resume only when the local file is the exact same byte stream. Otherwise, take an explicit full-restart path.

The behavior is protected by the default-off uploadRetryResume feature flag so it can be enabled for one production account before broader rollout.

What changed

  • Preserve resumable provider sessions and buffered chunks for retryable desktop interruptions, while keeping explicit cancellation destructive.
  • Add a resume endpoint that atomically claims a retry and returns the provider-confirmed offset.
  • Fence every writer with a stable retry attempt ID and every destructive restart with a fresh upload generation ID.
  • Namespace provider sessions and buffered scratch by generation so delayed responses from an abandoned upload cannot erase or restore its replacement.
  • Claim finalization with a status-and-generation CAS, and carry the generation through background media verification.
  • Resume browser and native uploads from aligned confirmed offsets; explicitly restart when the session is missing, expired, contradictory, or based on a transformed local file.
  • Reconcile uploads whose final response was lost after the server already accepted the media.
  • Report native legacy retry failures even when the feature flag is disabled and no attempt token exists.
  • Add nullable upload_attempt_id and upload_generation_id columns through additive startup migrations.

Safety and operations

  • The feature flag is registered but defaults off. Initial production validation should use Analytics > Feature flags > Resumable upload retry > Enable for me.
  • Disabling the flag is the rollback: new retries use the prior full-restart behavior. Existing generated writers remain fenced until they finish or restart.
  • Missing or unreadable flag state fails closed. Legacy null-generation uploads remain supported, and the first destructive reset upgrades them to a generated namespace.
  • Reset cannot overwrite processing or ready; stale writers can clean only their own generation.
  • The schema changes are additive and nullable; they need no backfill and do not remove stored media or local backups.

Verification

  • Full Clips suite: 156 test files and 856 tests passed.
  • Rust native suite: 178 passed, 1 ignored, with Rust 1.88.
  • TypeScript checks passed for both the hosted Clips app and desktop client.
  • Desktop Vite and hosted production builds passed.
  • Adversarial tests cover reset-vs-finalize CAS loss, stale scratch cleanup, delayed provider expiry, delayed provider success, and background verification re-entry.
  • Independent final review found no remaining actionable issues.
  • A full signed Tauri artifact was not produced locally because this machine has Command Line Tools rather than full Xcode. Exact native production dogfooding remains the first flag-enabled rollout check.

Review focus

  • Are attempt and generation claims fenced at every destructive, provider-session, byte-writing, and finalization boundary without blocking legacy uploads?
  • Do provider offsets resume only the exact local byte stream, with safe reset for transformed or inconsistent media?
  • Does a flag change in either direction leave browser and native retries recoverable?
  • Is the additive schema and mixed-version behavior safe for a default-off deployment?

@netlify

This comment has been minimized.

@netlify

This comment has been minimized.

@netlify

This comment has been minimized.

@github-actions

github-actions Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Visual recap — screenshot failed

A recap was published, but the PR-comment screenshot could not be captured or uploaded. Open the interactive recap directly:

Open the full interactive recap

Diagnostic:

dark: page.screenshot: Timeout 30000ms exceeded. Call log: - taking page screenshot - waiting for fonts to load...

builder-io-integration[bot]

This comment was marked as outdated.

@builder-io-integration builder-io-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Builder reviewed your changes and found 2 potential issues 🔴

Review Details

Incremental Code Review Summary

The latest commit adds an upload-generation fence and propagates it through resumable sessions, buffered scratch, finalization, interruption, and background verification. It also adds status-and-generation CAS protection, generation-scoped cleanup, and native retry generation handling. I verified that all six issues from the previous review are addressed and resolved their existing threads before this review.

New findings

  • 🔴 HIGH: The generation fence is enforced by chunk.post.ts, but the plain browser recorder does not send uploadGenerationId. When uploadRetryResume is enabled, browser reset/compression/retry receives a new generation and all subsequent browser chunks are rejected as stale. This makes the flag unsafe for the browser client unless rollout is desktop-only or the browser recorder forwards the returned generation.
  • 🟡 MEDIUM: Non-retry native reset callers pass no attempt or generation, but reset now creates a generation whenever recovery is enabled. Their subsequent native upload requests still omit that generation and are rejected by the chunk route.

The generation model is otherwise substantially stronger than the previous revision, with generation-scoped cleanup and finalization claims. The PR remains high risk because these paths coordinate client compatibility with server-side fencing.

🧪 Browser testing: Could not verify — browser automation tools were unavailable; the dev server was healthy and all planned cases reported couldnt_verify/env_issue.

// The first renewal admits the request. Later renewals immediately before
// every durable write/finalization boundary close the body-read/provider
// gap where /abort can otherwise commit while this request is in flight.
if ((existing.uploadGenerationId ?? null) !== uploadGenerationId) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Generation fencing breaks plain-browser retries

The chunk route now rejects any request whose uploadGenerationId differs from the row, but the plain browser recorder's resetUploadedChunks and subsequent uploadBlobInSlices/uploadBlobInStreamingChunks calls never send this field. When uploadRetryResume is enabled, reset creates and persists a generation, so every following browser retry/compression chunk is rejected with A newer upload generation is already active. Either propagate the returned generation through the browser upload path or ensure this flag cannot be enabled for browser clients.

Additional Info
Found by browser-test-planner source analysis and confirmed in app/components/recorder/recorder-engine.ts: reset response and chunk requests omit uploadGenerationId.

Fix in Builder

// Fence this reset before deleting any provider or buffered state. A
// retry that lost the token race must not tear down the winner's session.
const now = new Date().toISOString();
const nextGenerationId = recoveryEnabled ? randomUUID() : null;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Generation assignment breaks unfenced native uploads

This assigns a new generation for every recovery-enabled reset, including existing native live-upload callers that pass neither an attempt nor a generation and do not receive/propagate the returned generation. Their subsequent chunk/finalization requests omit it and fail the generation equality check in chunk.post.ts, so multi-segment/live native uploads can no longer complete when the flag is enabled. Only rotate a generation for a claimed retry, or return and propagate it through every unfenced caller.

Additional Info
Found by 1 of 4 incremental review agents and confirmed against native reset_upload_chunks call sites that pass None for both identifiers.

Fix in Builder

@steve8708
steve8708 requested a review from shomix July 27, 2026 22:20
@steve8708

Copy link
Copy Markdown
Contributor

@shomix can you give this a review when you have a sec

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants