Skip to content

Harden resumable chunk uploads (ALO-121)#282

Closed
aloewright wants to merge 4 commits into
mainfrom
codex/alo-121-upload-resume-hardening
Closed

Harden resumable chunk uploads (ALO-121)#282
aloewright wants to merge 4 commits into
mainfrom
codex/alo-121-upload-resume-hardening

Conversation

@aloewright

Copy link
Copy Markdown
Owner

Closes ALO-121.

Summary

  • Adds GET /api/videos/upload/:uploadId/status so clients can resume from server-confirmed uploaded chunks after reloads or disconnects.
  • Adds DELETE /api/videos/upload/:uploadId to abort in-flight R2 multipart uploads, clear KV session state, and remove the temporary uploading row.
  • Persists client upload resume state in localStorage, verifies it against the server before resuming, preserves it on transient status lookup failures, and sends total fileSize metadata.
  • Fixes the final-chunk retry byte accounting so re-sending an already-recorded final part does not double-count against max-size or quota checks.

Verification

  • npx vitest run src/frontend/lib/chunked-upload.test.ts
  • npx vitest run src/workers/videos.test.ts
  • npm run type-check
  • npm run lint (passes with existing warning-only lint output)
  • npm run build

Full suite note

  • npm test currently fails on existing full-suite environment drift in Node 26: window.localStorage is undefined in App.shell.dom.test.tsx and analytics.test.ts. The run still reported 98 passing files / 1041 passing tests before those unrelated failures.

@linear-code

linear-code Bot commented Jul 9, 2026

Copy link
Copy Markdown

ALO-121

@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: beeab05f-eecf-43d3-b271-e863381f6bf9

📥 Commits

Reviewing files that changed from the base of the PR and between 302d870 and 35dd1b0.

📒 Files selected for processing (4)
  • src/frontend/lib/chunked-upload.test.ts
  • src/frontend/lib/chunked-upload.ts
  • src/workers/videos.test.ts
  • src/workers/videos.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/frontend/lib/chunked-upload.ts
  • src/workers/videos.test.ts
  • src/workers/videos.ts
  • src/frontend/lib/chunked-upload.test.ts
📜 Recent review details
⏰ Context from checks skipped due to timeout. (1)
  • GitHub Check: Workers Builds: spooool
🧰 Additional context used
🔍 Remote MCP GitHub Copilot

Relevant review context from public code:

  • Client-side resumable uploads are commonly persisted in localStorage using a file-derived key like upload-${file.name}-${file.size}.
  • Server-side chunk handlers often treat repeated chunk indexes as idempotent and return early when uploadedChunks.includes(chunkIndex) is true.
  • Including full file size in multipart upload metadata is a known pattern; multiple upload clients append fileSize/filesize to FormData.
  • Multipart abort is commonly implemented as a separate cleanup path from completion, and public S3-compatible codebases expose dedicated abortMultipartUpload flows.

I did not find exact matches for the PR’s specific /api/videos/upload/:uploadId/status or DELETE /api/videos/upload/:uploadId routes in public code via these searches.


📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added resumable video upload status checks, letting uploads continue from the correct chunk after interruptions.
    • Added support for canceling in-progress video uploads and cleaning up associated upload data.
    • Improved upload progress tracking for resumed uploads.
  • Bug Fixes

    • Resuming now uses the server’s recorded uploaded chunks to avoid stale progress.
    • Upload retries and final-chunk recovery are more reliable, including better handling of transient failures.
    • Fixed quota and completion handling for chunked uploads.

Walkthrough

Chunked video uploads now resume from localStorage and server status, while the worker adds upload status and cancel endpoints, shared KV key helpers, and corrected retry byte accounting.

Changes

Frontend resume persistence and server sync

Layer / File(s) Summary
localStorage resume and server status
src/frontend/lib/chunked-upload.ts
Progress persistence uses localStorage, resume logic fetches upload status for video uploads, and chunk requests include file size.
resume and recorder test coverage
src/frontend/lib/chunked-upload.test.ts
Tests switch to localStorage stubs and cover completion cleanup, server-reported chunk resumption, stale-state restart, transient status failures, header forwarding, and recorder resume behavior.

Backend upload status/cancel endpoints and retry accounting

Layer / File(s) Summary
KV helpers, status/cancel routes, and retry accounting
src/workers/videos.ts
Upload metadata gains fileName and fileSize, shared KV key helpers are introduced, status and delete routes are added, POST upload uses the shared keys, and retry byte accounting is corrected.
status, cancel, and retry test coverage
src/workers/videos.test.ts
Test harness tracks DB deletes and R2 aborts, and new tests cover status reporting, malformed parts fallback, cancellation cleanup, abort failure, missing multipart ids, and final-chunk retry behavior.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related issues

  • ALO-121: These changes harden chunked upload resume, add status feedback, and cover multipart cleanup, matching the upload pipeline scope.

Possibly related PRs

  • aloewright/spooool#39: Both PRs work on the same video upload routing and multipart handling area in src/workers/videos.ts, including /api/videos/* upload-related endpoints.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR hardens chunked resume, but the linked E2 upload-to-HLS scope also includes Stream API, fallback, thumbnails, and webhook/state-machine work that isn't present here. Add the missing upload pipeline pieces required by ALO-121, or narrow the linked scope so this PR only claims the resume/abort hardening work.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately reflects the main change: hardening resumable chunk uploads for ALO-121.
Description check ✅ Passed The description matches the changeset and summarizes the resume, abort, storage, and quota fixes.
Out of Scope Changes check ✅ Passed The changes stay focused on resumable upload hardening, status/abort endpoints, and related tests; no unrelated features stand out.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/alo-121-upload-resume-hardening

Warning

Review ran into problems

🔥 Problems

These MCP integrations need to be re-authenticated in the Integrations settings: Sentry


Comment @coderabbitai help to get the list of available commands.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 9, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
spooool 35dd1b0 Jul 09 2026, 06:31 AM

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request migrates chunked upload state persistence from sessionStorage to localStorage and introduces new endpoints to retrieve upload status and cancel active upload sessions. It also refines byte-counting logic during final chunk retries to prevent double-counting. The review feedback highlights several key improvements: forwarding custom authentication headers to the upload status check, handling potential JSON parsing exceptions for KV data to avoid internal server errors, and decoupling database cleanup from R2 abort actions in the cancellation endpoint to ensure robust resource cleanup.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread src/frontend/lib/chunked-upload.ts Outdated
Comment on lines 129 to 134
async function fetchUploadStatus(endpoint: string, uploadId: string): Promise<UploadStatus | null> {
const res = await fetch(statusEndpoint(endpoint, uploadId), { method: 'GET' });
if (res.status === 404) return null;
if (!res.ok) throw new Error(`resume status failed: ${res.status}`);
return (await res.json()) as UploadStatus;
}

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.

high

The fetchUploadStatus function does not accept or forward custom headers (such as authentication tokens or session headers) that might be provided in UploadOptions.headers. Since the status endpoint /api/videos/upload/:uploadId/status requires authentication, any client relying on custom headers for authentication will fail to resume uploads.

We should update fetchUploadStatus to accept and forward these headers.

async function fetchUploadStatus(
  endpoint: string,
  uploadId: string,
  headers?: Record<string, string>,
): Promise<UploadStatus | null> {
  const res = await fetch(statusEndpoint(endpoint, uploadId), { method: 'GET', headers });
  if (res.status === 404) return null;
  if (!res.ok) throw new Error(`resume status failed: ${res.status}`);
  return (await res.json()) as UploadStatus;
}

Comment thread src/frontend/lib/chunked-upload.ts Outdated
uploadId = stored.uploadId;
startChunk = stored.nextChunk;
try {
const status = await fetchUploadStatus(endpoint, stored.uploadId);

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.

high

Pass the headers option to fetchUploadStatus so that the status check is properly authenticated.

Suggested change
const status = await fetchUploadStatus(endpoint, stored.uploadId);
const status = await fetchUploadStatus(endpoint, stored.uploadId, headers);

Comment thread src/workers/videos.ts
Comment on lines +104 to +111
function parseUploadedChunks(partsJson: string | null): number[] {
if (!partsJson) return [];
const uploadedPartsMap = JSON.parse(partsJson) as Record<string, { etag: string; size: number }>;
return Object.keys(uploadedPartsMap)
.map((partNumber) => Number(partNumber) - 1)
.filter((chunkIndex) => Number.isInteger(chunkIndex) && chunkIndex >= 0)
.sort((a, b) => a - b);
}

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.

medium

JSON.parse(partsJson) can throw an unhandled exception if partsJson is malformed or corrupted in KV, which would cause a 500 Internal Server Error on the status endpoint. It is safer to wrap the parsing in a try-catch block and return an empty array on failure.

function parseUploadedChunks(partsJson: string | null): number[] {
  if (!partsJson) return [];
  try {
    const uploadedPartsMap = JSON.parse(partsJson) as Record<string, { etag: string; size: number }>;
    return Object.keys(uploadedPartsMap)
      .map((partNumber) => Number(partNumber) - 1)
      .filter((chunkIndex) => Number.isInteger(chunkIndex) && chunkIndex >= 0)
      .sort((a, b) => a - b);
  } catch {
    return [];
  }
}

Comment thread src/workers/videos.ts Outdated
Comment on lines +577 to +585
if (multipartUploadId && uploadMetaJson) {
const uploadMeta = uploadMetaPersistedSchema.parse(JSON.parse(uploadMetaJson));
await c.env.VIDEOS.resumeMultipartUpload(uploadMeta.r2Key, multipartUploadId).abort().catch(() => {});
await c.env.DB.prepare(
`DELETE FROM videos WHERE id = ? AND user_id = ? AND status = 'uploading'`,
)
.bind(uploadMeta.videoId, user.id)
.run();
}

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.

medium

If multipartUploadId is missing or expired from KV, but uploadMetaJson is still present, the temporary database row with status = 'uploading' will not be deleted because the entire block is skipped when multipartUploadId is falsy. We should decouple the database row deletion from the R2 multipart abort check so that the database row is always cleaned up when the session metadata is available.

  if (uploadMetaJson) {
    try {
      const uploadMeta = uploadMetaPersistedSchema.parse(JSON.parse(uploadMetaJson));
      if (multipartUploadId) {
        await c.env.VIDEOS.resumeMultipartUpload(uploadMeta.r2Key, multipartUploadId).abort().catch(() => {});
      }
      await c.env.DB.prepare(
        `DELETE FROM videos WHERE id = ? AND user_id = ? AND status = 'uploading'`,
      )
        .bind(uploadMeta.videoId, user.id)
        .run();
    } catch { /* ignore parsing/validation errors */ }
  }

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cfad2e3e9a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/frontend/lib/chunked-upload.ts Outdated
uploadId = stored.uploadId;
startChunk = stored.nextChunk;
try {
const status = await fetchUploadStatus(endpoint, stored.uploadId);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Gate status lookups to video uploads

When uploadInChunks is used for recorder uploads, this new unconditional status probe calls /api/videos/upload/:uploadId/status, but recorder multipart state is stored under the separate upload:rec:${user.id}:... namespace in handleRecorderUpload and the new status route only reads video upload keys. After a reload/disconnect of a multi-chunk recorder upload, the status call returns 404, removeProgress runs, and the helper restarts at chunk 0 instead of resuming the existing recorder multipart session, re-uploading data and leaving the old multipart upload around. Skip the server-status verification for target === 'recorder' or add a recorder-aware status endpoint before deleting stored progress.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai 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.

Actionable comments posted: 4

🧹 Nitpick comments (2)
src/workers/videos.test.ts (1)

682-711: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Test doesn't actually verify the double-counting fix.

The seeded old part size (512, key '2') equals the retried chunk's size (512 bytes), so the assertion expect(res1.status).toBe(201) would pass identically whether the pre-completion check subtracts the old size or naively double-counts it — the resulting total (either ~1024 or ~1536 bytes) is nowhere near the 10GB quota mock, so no code path this test exercises can actually fail from the bug being fixed.

To meaningfully guard the fix, either:

  • use a retried size that differs from the previously recorded size and assert the persisted/completed total equals the new size (not old+new), or
  • make the previously-recorded "old" size large enough that double-counting would trip a size/quota check, and assert the request still succeeds.
♻️ Example: make the test size-sensitive
     const partsKey = `upload:${USER_ID}:${uploadId}:parts`;
     kv[partsKey] = JSON.stringify({
       '1': { etag: 'etag-1', size: MP4_MAGIC.byteLength },
-      '2': { etag: 'etag-old', size: 512 },
+      '2': { etag: 'etag-old', size: 999_999 },
     });

     const fd1 = new FormData();
     fd1.set('title', 'vid');
     fd1.set('description', '');
     fd1.set('file', new Blob([new Uint8Array(512)], { type: 'video/mp4' }), 'v.mp4');
     fd1.set('chunkIndex', '1');
     fd1.set('chunkCount', '2');
     fd1.set('uploadId', uploadId);
     const res1 = await fetcher('/api/videos/upload', { method: 'POST', body: fd1 });
     expect(res1.status).toBe(201);
+    // Assert against whatever exposes the final computed byte total
+    // (e.g. dbUpdates bound params) to confirm it reflects 512, not 999_999 + 512.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/workers/videos.test.ts` around lines 682 - 711, The test in
videos.test.ts does not currently prove the double-counting fix because the
seeded prior part size matches the retried chunk size and never exercises the
failing path. Update the scenario around the upload retry in the `does not
double-count bytes...` test so the previously stored part size differs from the
retried chunk, or make the old size large enough that double-counting would
break the size/quota logic. Then assert on the resulting persisted/completed
total from the upload flow, not just the 201 response from `/api/videos/upload`,
so the test specifically verifies the behavior in `multiChunkEnv`, `partsKey`,
and the retry handling around `uploadId`.
src/frontend/lib/chunked-upload.test.ts (1)

193-213: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Make the transient-failure test assert storage retention.

This upload succeeds, so final cleanup would mask a regression that clears local progress inside the status-failure catch. Add a rejecting upload path and assert the stored resume entry remains.

Example coverage
+  it('retains localStorage progress when status lookup and upload retry fail transiently', async () => {
+    const file = makeFile(30 * 1024 * 1024, 'status-err-fail.mp4', 'video/mp4');
+    const key = `chunk-upload:status-err-fail.mp4:${file.size}:${file.lastModified}`;
+    const stored = JSON.stringify({ uploadId: 'stored-id', nextChunk: 1, chunkCount: 3 });
+    ssMap.set(key, stored);
+
+    mockFetch(async (_url, init) => {
+      if (!init?.body) return new Response('temporary', { status: 503 });
+      return new Response('server down', { status: 503 });
+    });
+
+    await expect(uploadInChunks({
+      file, endpoint: '/api/upload', target: 'video', fields: {}, onProgress: () => {},
+      _sleep: noSleep,
+    })).rejects.toThrow(/chunk 1 failed: 503/);
+    expect(ssMap.get(key)).toBe(stored);
+  });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/frontend/lib/chunked-upload.test.ts` around lines 193 - 213, The
transient resume-status failure test currently only covers a successful upload,
so it can miss regressions where the stored progress is cleared after a status
check error. Update the test around uploadInChunks to use a rejecting upload
path for the status-failure case and assert that the ssMap resume entry for the
upload key is still present afterward, while keeping the existing chunkIndex
behavior coverage intact.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/frontend/lib/chunked-upload.ts`:
- Around line 129-130: The status lookup in fetchUploadStatus is missing the
same request headers used by the chunk POST path, which can break
header-authenticated uploads and cause stale resume behavior. Update
fetchUploadStatus (and the related status-check call site around the chunk
upload flow) to accept and forward the upload headers into the fetch call,
keeping the status verification request consistent with the main upload
requests.
- Around line 168-174: The resume logic in chunked-upload.ts does not handle an
`uploading` status when the server’s `chunkCount` differs from the stored one,
so it may incorrectly reuse the existing `uploadId` and `startChunk`. Update the
branch around `firstMissingChunk`, `saveProgress`, and the `status.chunkCount
=== chunkCount` check to detect this incompatible state and restart the upload
cleanly instead of resuming. Make sure the `saveProgress`/resume path only
applies when the server session matches the current chunk layout.

In `@src/workers/videos.ts`:
- Around line 577-592: Do not clear multipart upload session state when the R2
abort in the upload cleanup path fails. In the cleanup logic around
resumeMultipartUpload(...).abort() and the subsequent SESSIONS deletions, catch
the abort failure and return a 5xx instead of continuing; only delete mpidKey,
metaKey, partsKey, and doneKey after a successful abort. Keep the existing
uploadMetaPersistedSchema parse and DB cleanup flow, but preserve retry state
when abort cannot complete.
- Around line 773-774: The upload metadata in videos.ts is trusting raw form
input for fileSize, which can become NaN/null and break upload status parsing
later. Update the logic around the fileSize assignment in the upload metadata
flow to validate that formData.get('fileSize') is a nonnegative integer before
persisting it, and fall back to omitting the field or using the rawFile.size
value only when the input is invalid. Use the surrounding upload metadata
construction and uploadMetaPersistedSchema.parse path to locate the fix.

---

Nitpick comments:
In `@src/frontend/lib/chunked-upload.test.ts`:
- Around line 193-213: The transient resume-status failure test currently only
covers a successful upload, so it can miss regressions where the stored progress
is cleared after a status check error. Update the test around uploadInChunks to
use a rejecting upload path for the status-failure case and assert that the
ssMap resume entry for the upload key is still present afterward, while keeping
the existing chunkIndex behavior coverage intact.

In `@src/workers/videos.test.ts`:
- Around line 682-711: The test in videos.test.ts does not currently prove the
double-counting fix because the seeded prior part size matches the retried chunk
size and never exercises the failing path. Update the scenario around the upload
retry in the `does not double-count bytes...` test so the previously stored part
size differs from the retried chunk, or make the old size large enough that
double-counting would break the size/quota logic. Then assert on the resulting
persisted/completed total from the upload flow, not just the 201 response from
`/api/videos/upload`, so the test specifically verifies the behavior in
`multiChunkEnv`, `partsKey`, and the retry handling around `uploadId`.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 21c39ef8-35b3-434d-aad4-60ae870060b1

📥 Commits

Reviewing files that changed from the base of the PR and between b098644 and cfad2e3.

📒 Files selected for processing (4)
  • src/frontend/lib/chunked-upload.test.ts
  • src/frontend/lib/chunked-upload.ts
  • src/workers/videos.test.ts
  • src/workers/videos.ts
📜 Review details
⏰ Context from checks skipped due to timeout. (1)
  • GitHub Check: Workers Builds: spooool
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx,js,jsx,html,css,scss}

📄 CodeRabbit inference engine (AGENTS.md)

Strand stack utilities: require the base stack class for flex layout; size classes like stack-sm / stack-xl only control gap

Files:

  • src/frontend/lib/chunked-upload.ts
  • src/workers/videos.test.ts
  • src/frontend/lib/chunked-upload.test.ts
  • src/workers/videos.ts
🔍 Remote MCP

Relevant context for review

  • Public multipart-upload implementations commonly expose a dedicated .../status route for resumable sessions, including GET /chunks/:token/:uploadId/status, GET /:eventId/chunked-upload/:uploadId/status, and router.get("/:uploadId/status"). citeturn0search0
  • In those examples, status handling is tied to upload-session state, including explicit getUploadStatus(uploadId)-style lookups. citeturn0search0
  • Duplicate chunk handling is typically idempotent: several chunk upload services check uploadedChunks.includes(chunkIndex) and return early / skipped instead of reprocessing the same part. citeturn0search0
  • Abort is a distinct cleanup path in multipart systems, with explicit AbortMultipartUpload / abortMultipartUpload calls used to cancel in-flight uploads. citeturn0search0
🔇 Additional comments (6)
src/workers/videos.test.ts (4)

485-488: LGTM!

Also applies to: 500-500, 534-534, 555-555


633-660: LGTM!


662-680: LGTM!


643-645: 🎯 Functional Correctness

No duplicate declaration here The snippet contains a single const { uploadId, videoId } = ... destructuring statement, so there’s no duplicate const in this block.

			> Likely an incorrect or invalid review comment.
src/frontend/lib/chunked-upload.ts (1)

88-108: LGTM!

Also applies to: 112-127, 196-196

src/frontend/lib/chunked-upload.test.ts (1)

7-13: LGTM!

Also applies to: 114-146, 165-191

Comment thread src/frontend/lib/chunked-upload.ts Outdated
Comment thread src/frontend/lib/chunked-upload.ts Outdated
Comment thread src/workers/videos.ts Outdated
Comment thread src/workers/videos.ts Outdated
@aloewright aloewright closed this Jul 9, 2026
@aloewright aloewright deleted the codex/alo-121-upload-resume-hardening branch July 9, 2026 07:11
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.

1 participant