Skip to content

Resume interrupted Clips uploads - #2436

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

Resume interrupted Clips uploads#2436
3mdistal wants to merge 6 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.
  • Keep mixed-version clients compatible: browser resets opt into and forward generation fences, while legacy native resets remain on the null-generation protocol until upgraded.
  • 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.
  • Make abort and lease cleanup generation-scoped, CAS-protected, retryable after partial cleanup, and fail loudly when session state cannot be read.
  • Isolate six-digit legacy scratch keys from generation-prefixed keys and reject out-of-range chunk indexes.
  • 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 and reset clients remain supported; clients that opt into generation fencing must echo the returned generation.
  • Reset cannot overwrite processing or ready; stale writers can clean only their own generation.
  • Abort updates the recording and associated upload/verification state with identity-aware compare-and-set operations, so a delayed abort cannot erase replacement state.
  • The schema changes are additive and nullable; they need no backfill and do not remove stored media or local backups.

Verification

  • Exact-head GitHub CI passed, including macOS, Windows, and Ubuntu desktop builds.
  • Exact-head upload recovery suite: 8 test files and 86 tests passed.
  • Exact-head hosted Clips typecheck and production build passed.
  • The two unrelated timer-test files that timed out during a heavily contended full-suite rerun passed in isolation: 2 files and 40 tests.
  • A prior full merged-tree Clips run passed 170 test files and 976 tests. The final concurrent rerun passed 168 files and 973 tests, with only four unrelated five-second timer timeouts attributable to workstation contention.
  • Desktop suite: 26 files and 124 tests passed; desktop typecheck and Vite build passed.
  • Rust native suite: 178 passed and 1 ignored with Rust 1.88.
  • Adversarial coverage includes reset-vs-finalize CAS loss, stale scratch cleanup, replacement-state preservation, partial-cleanup retry, unreadable session state, delayed provider expiry/success, and background verification re-entry.
  • The latest Builder review findings were addressed with fail-loud state snapshots, compare-and-set state publication, late-session cleanup, and reset startup compensation; its seven-digit index finding was already covered by route validation and a regression test.
  • 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 abort or lease cleanup preserve a replacement generation and its auxiliary state under delayed requests?
  • 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?

Follow-ups

  • Upgrade Chrome extension restart transport to opt into generation fencing. It remains compatible on the legacy null-generation contract today; this is an optional hardening step.

@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 — generation failed

The visual recap could not be generated for this pull request. This is informational only and does not block the PR.

Diagnostic:

No plan URL: create-visual-recap failed 422 Unprocessable Entity: {"error":"plan.mdx: Expected a closing tag for <Endpoint> (52:1-52:1099)"}

Agent output: Repaired both malformed Screen HTML attribute expressions in recap-source.json and validated the JSON. --- ⠀ 🟢 Visual recap source repaired and ready for the hosted parser retry. stderr: Reading additional input from stdin... 2026-07-30T18:30:46.380737Z ERROR codex_core::tools::router: error=apply_patch verification failed: invalid patch: The last line of the patch must be '*** End Patch'

builder-io-integration[bot]

This comment was marked as outdated.

builder-io-integration[bot]

This comment was marked as outdated.

@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

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 6 potential issues 🔴

Review Details

Incremental Code Review Summary

The latest remote head addresses the five prior cleanup/compatibility comments, so I resolved those existing threads before this review. The new changes improve generation-scoped abort/reaper cleanup and legacy namespace isolation, but the new asynchronous state transitions still have several race and failure-coercion gaps.

New findings

  • 🔴 HIGH: A delayed interruption callback can overwrite a replacement generation's uploading application state after its SQL claim has already become stale.
  • 🔴 HIGH: Reset can race with abort between the row CAS and provider-session persistence, leaving a provider session attached to an already-cancelled generation.
  • 🔴 HIGH: Seven-digit chunk indexes can be accepted while exact scratch-key filters only recognize six-digit indexes, allowing accepted bytes to be omitted from sums/finalization and left behind by cleanup.
  • 🟡 MEDIUM: Resume and interruption convert unreadable application-state reads into empty state and overwrite metadata instead of surfacing the state-store failure.
  • 🟡 MEDIUM: Resume can publish uploading application state after an interrupt wins concurrently, leaving SQL and polling state inconsistent.

The generation CAS structure is substantially improved, but these remaining issues affect correctness under delayed callbacks and partial state-store/provider failures. Risk remains high because this PR coordinates durable upload state with external storage sessions.

🧪 Browser testing: Will run after this review (PR touches UI code).

Comment thread templates/clips/server/routes/api/uploads/[recordingId]/interrupt.post.ts Outdated
Comment thread templates/clips/server/lib/recording-upload-state.ts
Comment thread templates/clips/server/routes/api/uploads/[recordingId]/interrupt.post.ts Outdated
Comment thread templates/clips/server/routes/api/uploads/[recordingId]/resume.get.ts Outdated
Comment thread templates/clips/server/routes/api/uploads/[recordingId]/resume.get.ts Outdated
@3mdistal
3mdistal force-pushed the codex/clips-upload-recovery branch from 0dfbb69 to bbb0d44 Compare July 30, 2026 18:24
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