Skip to content

feat(workspaces): move file storage and extraction to R2#643

Merged
urjitc merged 7 commits into
mainfrom
codex/r2-workspace-file-architecture
Jul 15, 2026
Merged

feat(workspaces): move file storage and extraction to R2#643
urjitc merged 7 commits into
mainfrom
codex/r2-workspace-file-architecture

Conversation

@urjitc

@urjitc urjitc commented Jul 14, 2026

Copy link
Copy Markdown
Member

Summary

  • move workspace file uploads, source binaries, previews, and extracted pages to R2
  • let browsers upload files directly to R2 with signed, single-use completion claims and a 200 MB limit
  • stream existing LiteParse extraction into per-page R2 objects so AI reads stay bounded and fast
  • hard-remove shell-backed storage, compatibility parsing, and temporary migration machinery

Why

Large file bodies and extraction artifacts were flowing through workspace Durable Object memory and container shell storage. That amplified memory pressure and coupled durable state to ephemeral filesystem paths.

The Durable Object now owns metadata and coordination while R2 owns large immutable bytes. AI reads fetch only selected page objects rather than loading an entire PDF extraction.

Changes

  • add direct R2 upload sessions with signed PUT URLs, expiry, atomic completion ownership, and replay protection
  • use a dedicated completion-token HMAC secret instead of reusing R2 credentials
  • store source files, previews, page manifests, and individual Markdown pages under canonical R2 keys
  • preserve streaming through multipart conversion and LiteParse NDJSON, including downstream backpressure
  • bound page selection, page bytes, and individual NDJSON records
  • reject empty conversion output and preserve processor 413/5xx semantics
  • apply timeouts to processor requests and restore workflow retries for preview generation
  • verify source ETags before publishing ready preview or page projections
  • make page projection publication clean up R2 artifacts if the kernel commit fails
  • remove all lazy migration fallbacks, shell paths, legacy extraction parsing, and the temporary migration endpoint

The one-time production migration already ran on July 14, 2026. All 961 D1 workspaces were audited with zero migration failures. Seven unrecoverable file records whose source binaries were already absent were removed. Production bucket CORS is configured, and the temporary migration credential was revoked.

The staging and production R2 buckets now expire abandoned workspace_file_uploads/ objects after one day. Both Workers have independent WORKSPACE_UPLOAD_TOKEN_SECRET values configured.

Testing

  • pnpm check
  • pnpm test — 15 files, 51 tests
  • pnpm exec wrangler types --check
  • pnpm knip
  • git diff --check
  • standalone LiteParse Docker PDF/NDJSON smoke test
  • production migration verification across all 961 workspaces

Review Notes

Start with the direct-upload route, workspace-kernel-file-commands.ts, and workspace-page-projection.ts.

Direct R2 PUT cannot reject an oversized body before storage. Completion validates the actual R2 object size against the signed claim before processing, and the one-day lifecycle bounds abandoned-object retention.

Before rollout, replace any temporary R2_ACCESS_KEY_ID and R2_SECRET_ACCESS_KEY values with real bucket-scoped Object Read & Write credentials. They are deployment configuration and are not committed here.

Summary by CodeRabbit

  • New Features
    • Added direct workspace uploads with resumable browser-to-storage transfers and secure completion handling.
    • Added file previews for PDFs and images, plus improved document extraction and per-page results.
    • Added page-range selection with limits for workspace content reads.
  • Bug Fixes
    • Improved handling of invalid, password-protected, oversized, and incomplete files.
    • Prevented duplicate upload completion and avoided unnecessary reprocessing.
  • Documentation
    • Added deployment guidance for upload storage, security settings, CORS, and cleanup.

@cursor

cursor Bot commented Jul 14, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@capy-ai

capy-ai Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Capy auto-review is paused for this organization because the usage-cycle auto-review limit has been reached. Increase the limit or turn it off in billing settings to resume automatic reviews.

@github-actions

github-actions Bot commented Jul 14, 2026

Copy link
Copy Markdown

React Doctor found 2 new issues in 2 files · 2 warnings · score 89 / 100 (Great) · 4 fixed · vs main

2 warnings

containers/liteparse/server.mjs

  • ⚠️ L75 await inside a loop async-await-in-loop

src/features/workspaces/extraction/workspace-page-projection.ts

  • ⚠️ L69 await inside a loop async-await-in-loop

Reviewed by React Doctor for commit ebe8bc7. See inline comments for fixes.

@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@urjitc, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 38 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 135f91da-5564-4ae0-bb0e-305ce161ab93

📥 Commits

Reviewing files that changed from the base of the PR and between b42820c and ebe8bc7.

📒 Files selected for processing (10)
  • src/features/workspaces/extraction/request-workspace-file-extraction.ts
  • src/features/workspaces/extraction/workspace-file-preview-projection.ts
  • src/features/workspaces/files/workspace-file-object-keys.ts
  • src/features/workspaces/kernel/workspace-kernel-file-commands.ts
  • src/features/workspaces/kernel/workspace-kernel-item-commands.ts
  • src/features/workspaces/kernel/workspace-kernel-types.ts
  • src/features/workspaces/operations/read-items.ts
  • src/features/workspaces/upload/workspace-file-direct-upload.test.ts
  • src/features/workspaces/upload/workspace-file-direct-upload.ts
  • src/routes/api/v1/workspaces.$workspaceId.file-upload.ts
📝 Walkthrough

Walkthrough

This PR introduces direct-to-R2 workspace uploads, streamed conversion and extraction, R2-backed kernel files and projections, LiteParse validation/previews/NDJSON responses, and updated deployment bindings, CORS policies, credentials, and lifecycle instructions.

Changes

Workspace file pipeline

Layer / File(s) Summary
Upload contracts and direct-upload flow
src/features/workspaces/upload/*, src/routes/api/v1/workspaces.$workspaceId.file-upload.ts, config/r2/*
Uploads use signed R2 sessions, completion tokens, conditional claims, staged-object validation, JSON initiate/complete requests, and separate file, selection, and document-import limits.
R2 object storage and kernel contracts
src/features/workspaces/kernel/*, src/features/workspaces/files/workspace-file-object-keys.ts, src/lib/r2.ts, src/routes/api/v1/workspaces.$workspaceId.files.*
Kernel files and projections persist R2 object keys, validate stored objects and etags, serve content and previews from R2, and clean up object prefixes.
Streamed conversion and processor services
containers/liteparse/*, src/features/workspaces/files/*, src/features/workspaces/conversion/*, src/lib/http/streaming-multipart.ts, src/features/workspaces/extraction/providers/*
File conversion, previews, PDF validation, provider uploads, and LiteParse parsing use streamed requests, multipart bodies, processor endpoints, and per-page NDJSON responses.
Page projections and extraction workflow
src/features/workspaces/extraction/*, src/features/workspaces/operations/*
Extraction output is stored as validated page objects with manifests, bounded page selections are parsed and read from R2, and workflows publish preview and extraction projections.
Deployment and operational wiring
wrangler.jsonc, worker-configuration.d.ts, docs/configuration/deployments.mdx, containers/liteparse/Dockerfile, package.json, .dev.vars.example
R2 bindings, credentials, CORS policies, processor durable-object names, migration settings, native tools, dependencies, and deployment instructions are updated.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant UploadRoute
  participant R2
  participant WorkspaceKernel
  participant WorkspaceFileProcessor
  participant ExtractionWorkflow
  Client->>UploadRoute: initiate upload
  UploadRoute->>R2: create signed PUT session
  R2-->>Client: upload URL and completion token
  Client->>R2: upload file bytes
  Client->>UploadRoute: complete upload
  UploadRoute->>WorkspaceKernel: create file item
  UploadRoute->>ExtractionWorkflow: enqueue extraction
  ExtractionWorkflow->>WorkspaceFileProcessor: stream validation or parsing request
  WorkspaceFileProcessor-->>ExtractionWorkflow: validation result or NDJSON pages
  ExtractionWorkflow->>R2: write page objects and manifest
Loading

Possibly related PRs

Suggested labels: capy

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: moving workspace file storage and extraction to R2.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/r2-workspace-file-architecture

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@mintlify

mintlify Bot commented Jul 14, 2026

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
thinkex 🟢 Ready View Preview Jul 14, 2026, 7:31 PM

@greptile-apps

greptile-apps Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR moves workspace file upload and extraction storage into R2. The main changes are:

  • Direct browser uploads through signed R2 PUT URLs and signed completion tokens.
  • R2-backed source files, previews, page manifests, and per-page Markdown objects.
  • Streamed LiteParse NDJSON extraction output instead of buffered extraction payloads.
  • Bounded AI page reads with page-count and byte-size limits.
  • Removal of shell-backed storage, legacy extraction parsing, and migration fallback paths.

Confidence Score: 5/5

This PR appears safe to merge.

The reviewed upload, kernel metadata, R2 projection, extraction, preview, and read paths include ownership checks, size validation, source-hash validation, cleanup on failures, and bounded reads. No blocking correctness or security issues were identified.

No files require special attention.

T-Rex T-Rex Logs

What T-Rex did

  • I ran the project's test suite with pnpm test, and Vitest reported 14 passed test files and 44 passed tests, with exit code 0.
  • I performed a repository diff check with git diff --check and it exited cleanly with no issues.
  • I attempted to run pnpm check to validate dependencies, but it exited with code 1 due to formatting issues in trex-upload-slots.json and trex-setup.md.
  • I attempted to run pnpm knip for static analysis, but it failed with RangeError: Array buffer allocation failed inside oxc-parser/knip.
  • I verified Docker availability and found Docker is not installed or accessible in the environment; the pr-test-plan-summary.txt documents sandbox blockers and next steps.

View all artifacts

T-Rex Ran code and verified through T-Rex

Important Files Changed

Filename Overview
src/routes/api/v1/workspaces.$workspaceId.file-upload.ts Adds the initiate and complete direct-upload API with auth, signed completion claims, staging-object verification, storage, cleanup, and extraction enqueueing.
src/features/workspaces/upload/workspace-file-direct-upload.ts Implements signed R2 PUT URLs and HMAC completion tokens with expiry and single-completion claim ownership.
src/features/workspaces/kernel/workspace-kernel-file-commands.ts Moves file metadata and projection persistence to R2 object keys and validates ready page projections against the current source object.
src/features/workspaces/extraction/workspace-page-projection.ts Introduces R2-backed page manifests and per-page Markdown object reads and writes with page count and byte limits.
src/features/workspaces/upload/workspace-file-upload-storage.ts Streams uploaded objects through optional conversion into R2 and validates stored PDFs before creating workspace file records.
containers/liteparse/server.mjs Expands the processor container to validate PDFs, generate previews, and stream LiteParse page output as NDJSON.
src/features/workspaces/extraction/workspace-file-extraction-workflow.ts Reworks extraction workflow to read source objects from R2, write page projections to R2, and publish previews during extraction.
src/features/workspaces/operations/read-items.ts Reads extracted file content from R2 page projections and maps page-selection errors into tool failure codes.

Reviews (1): Last reviewed commit: "refactor(workspaces): remove legacy file..." | Re-trigger Greptile

@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: 1e54e0e9ab

ℹ️ 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/features/workspaces/kernel/workspace-kernel-schema.ts
@mintlify

mintlify Bot commented Jul 14, 2026

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
thinkex 🟡 Building Jul 14, 2026, 7:30 PM

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

Actionable comments posted: 12

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/features/workspaces/conversion/container-file-conversion.ts (2)

1-1: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Streaming conversions lost the "empty result" guard that the buffered path still has, and no consumer compensates for it (except PDFs, incidentally). convertFileStreamWithContainer only checks response.ok/response.body-truthiness, not actual byte length, unlike convertFileWithContainer's bytes.byteLength === 0 check; the affected consumers convert HEIC images and generate AI markdown from images without any fallback.

  • src/features/workspaces/conversion/container-file-conversion.ts#L67-73: add a byte-length guard for the streamed response, or clearly document that callers must self-validate.
  • src/features/workspaces/upload/workspace-file-upload-storage.ts#L76-92: add if (conversion && stored.size === 0) throw ... after the existing size checks, since stored.size is already available post-put.
  • src/features/workspaces/extraction/providers/workers-ai-to-markdown.ts#L12-23: check bytes.byteLength === 0 before constructing the Blob passed to env.AI.toMarkdown.
🤖 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/features/workspaces/conversion/container-file-conversion.ts` at line 1,
Preserve the buffered conversion’s empty-result handling across streaming
consumers: update convertFileStreamWithContainer to validate streamed byte
length, add a stored.size === 0 rejection after the existing checks in workspace
file upload storage, and validate bytes.byteLength before constructing the Blob
for env.AI.toMarkdown in the workers AI markdown flow.

1-1: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Await multipart.done alongside fetchconvertFileStreamWithContainer only observes multipart.done after fetch() resolves, so a rejected fetch() can leave the multipart pipe promise rejected and unhandled. Use Promise.all([input.container.fetch(...), multipart.done]) or attach a catch handler when creating the stream.

🤖 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/features/workspaces/conversion/container-file-conversion.ts` at line 1,
Update convertFileStreamWithContainer so multipart.done is observed concurrently
with input.container.fetch(...), using Promise.all or an equivalent catch
handler when creating the stream; ensure rejected fetches do not leave the
multipart pipe promise unhandled.
🧹 Nitpick comments (3)
src/features/workspaces/kernel/workspace-kernel-file-commands.ts (1)

329-353: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Only the first R2 cleanup failure is logged; no retry for the rest.

Promise.allSettled runs deleteR2Prefix per file item, but results.find((r) => r.status === "rejected") only surfaces one rejection even when multiple items fail, and there's no retry/backoff — any failed prefix delete is silently abandoned, leaking orphaned R2 objects with no path to reconciliation.

🔧 Suggested fix: log every failure
-		const failure = results.find((result) => result.status === "rejected");
-
-		if (failure?.status === "rejected") {
-			recordOperationalOutcome({
-				error: failure.reason,
-				event: "workspace_file_object_cleanup",
-				fields: {
-					item_count: fileItemIds.length,
-					workspace_id: this.workspaceId(),
-				},
-			});
-		}
+		for (const result of results) {
+			if (result.status === "rejected") {
+				recordOperationalOutcome({
+					error: result.reason,
+					event: "workspace_file_object_cleanup",
+					fields: {
+						item_count: fileItemIds.length,
+						workspace_id: this.workspaceId(),
+					},
+				});
+			}
+		}
🤖 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/features/workspaces/kernel/workspace-kernel-file-commands.ts` around
lines 329 - 353, Update deleteObjects to record an operational outcome for every
rejected deleteR2Prefix result rather than selecting only the first failure.
Preserve the existing workspace and item-count context, and include each
failure’s reason so all failed cleanup attempts remain visible for
reconciliation.
src/features/workspaces/kernel/workspace-kernel.ts (1)

267-276: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Hardcoded R2 prefixes risk silent drift and orphaned data on workspace deletion.

Four of the five prefixes purged here are raw string literals duplicating namespace conventions defined elsewhere (e.g. the "workspace_kernel_files" namespace already configured on ShellWorkspace above, and the workspace_file_objects/workspace_file_uploads schemes presumably owned by the file-object-key helpers). If any of those definitions changes independently, this purge routine will silently miss data on workspace deletion, leaving orphaned R2 objects indefinitely — a retention concern since this runs specifically on workspace deletion.

Consider deriving these prefixes from the same shared helper functions used to build the actual object keys, rather than re-typing the literals here.

♻️ Illustrative direction
-			deleteR2Prefix(this.env.WORKSPACE_KERNEL_FILES, `workspace_kernel_files/${workspaceId}/`),
-			deleteR2Prefix(this.env.WORKSPACE_KERNEL_FILES, `workspace_file_objects/${workspaceId}/`),
-			deleteR2Prefix(this.env.WORKSPACE_KERNEL_FILES, `workspace_file_uploads/${workspaceId}/`),
+			deleteR2Prefix(this.env.WORKSPACE_KERNEL_FILES, getWorkspaceKernelFilesPrefix(workspaceId)),
+			deleteR2Prefix(this.env.WORKSPACE_KERNEL_FILES, getWorkspaceFileObjectPrefix(workspaceId)),
+			deleteR2Prefix(this.env.WORKSPACE_KERNEL_FILES, getWorkspaceFileUploadPrefix(workspaceId)),
🤖 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/features/workspaces/kernel/workspace-kernel.ts` around lines 267 - 276,
Update the workspace deletion cleanup in the surrounding workspace-removal
method to derive all R2 prefixes from the shared namespace constants or
key-building helpers used by ShellWorkspace and the workspace file-object/upload
implementations, instead of hardcoded literals. Preserve the existing five
deletion targets, including the chat attachment prefix from
getChatAttachmentWorkspacePrefix, so cleanup remains aligned with the keys
created elsewhere.
src/lib/http/streaming-multipart.ts (1)

28-59: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Reader is never cancelled on the error path.

On error, writer.abort(error) is called but reader.cancel(error) is not — only releaseLock() happens in finally. This can leave the source stream's underlying resource open longer than necessary during failures.

♻️ Proposed fix
 	} catch (error) {
 		await writer.abort(error).catch(() => undefined);
+		await reader.cancel(error).catch(() => undefined);
 		throw error;
 	} finally {
🤖 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/lib/http/streaming-multipart.ts` around lines 28 - 59, Update the error
handling in pipeMultipartBody to cancel the source reader with the caught error
before releasing its lock. Preserve the existing writer.abort behavior and
rethrow the original error, ensuring reader cancellation is attempted without
masking that error.
🤖 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 `@containers/liteparse/server.mjs`:
- Around line 179-207: Update withRequestFile so an upload-limit rejection does
not let pipeline destroy the IncomingMessage request stream before the 413
response is sent. Ensure the request body is drained or otherwise consumed after
the limiter detects an oversized upload, while preserving the existing 413
validation error and normal file-processing behavior.

In `@src/features/workspaces/conversion/container-file-conversion.ts`:
- Around line 67-73: Update the streaming conversion path around the response
body handling to detect and reject zero-byte response streams, not just a
missing response.body. Preserve the existing conversion-error handling and use
input.emptyMessage via input.error for empty successful responses, ensuring
callers never receive an empty stream.
- Around line 57-65: Update the response handling in the container conversion
flow around input.container.fetch to reject successful responses whose
response.body stream contains zero bytes, not merely when the stream is absent.
Buffer or otherwise track the streamed output before continuing, and apply the
same empty-output error behavior as the existing buffered helper while
preserving handling for non-empty responses.

In `@src/features/workspaces/extraction/providers/workers-ai-to-markdown.ts`:
- Around line 12-23: In the conversion flow before calling env.AI.toMarkdown,
validate that the converted image ArrayBuffer from conversion.arrayBuffer()
contains at least one byte. Reject or propagate an error for an empty result,
matching the existing zero-length handling used by the buffered conversion path,
and only construct the JPEG Blob and invoke toMarkdown for non-empty output.

In `@src/features/workspaces/extraction/workspace-file-preview-projection.ts`:
- Around line 19-24: Update the workspace file preview flow around
generateWorkspaceFilePreview and step.do so failures are rethrown after
recording the failed projection, allowing step.do to observe and retry them.
Increase the retries.limit from 1 to 2 to provide one retry attempt while
preserving the existing delay, backoff, and timeout settings.

In `@src/features/workspaces/files/workspace-file-processor.ts`:
- Around line 23-44: Add request cancellation to the processor.fetch call in the
workspace file processing flow by supplying an AbortSignal with a finite timeout
for the streaming POST. Preserve the existing headers, body, and processor
startup behavior while ensuring stalled uploads terminate rather than hanging
indefinitely.

In `@src/features/workspaces/kernel/workspace-kernel-file-commands.ts`:
- Around line 216-230: Extend the ready-state source staleness validation in the
upsert flow to apply to both page and preview projections, using the existing
sourceHash and row.object_key checks. Keep the manifest existence check
restricted to format === "pages", while ensuring preview upserts also compare
the current source etag before publishing.

In `@src/features/workspaces/upload/pdf-upload-validation.ts`:
- Around line 15-34: Update the non-success handling around
readValidationFailure so only expected PDF validation responses are mapped to
PASSWORD_PROTECTED_PDF or INVALID_PDF; detect processor 5xx or other
non-validation statuses and propagate them as retryable/server errors instead of
classifying them as INVALID_PDF. Preserve the existing password-protected
response behavior.

In `@src/features/workspaces/upload/workspace-file-direct-upload.ts`:
- Around line 119-134: Introduce a dedicated secret for workspace direct-upload
completion tokens and use it throughout createSigningKey, signUploadClaims, and
verifyWorkspaceDirectUploadToken instead of env.R2_SECRET_ACCESS_KEY. Add the
corresponding environment/configuration lookup and ensure token signing and
verification share this independent secret while AwsClient continues using the
R2 credential.

In `@src/features/workspaces/upload/workspace-file-upload-storage.ts`:
- Around line 76-92: In the converted-output validation flow around
assertReadablePdfUpload, reject any converted non-PDF result whose stored size
is zero before persistence. Preserve the existing size-consistency check for
non-converted uploads, the max-size check, and PDF validation behavior; ensure
heic_to_jpeg and other conversions cannot succeed with an empty output.

In `@src/lib/http/streaming-multipart.ts`:
- Around line 1-26: Update the caller of createStreamingMultipartFile,
specifically convertFileStreamWithContainer(), to attach multipart.done to the
request lifecycle before fetch can reject. Await it alongside the fetch promise
using the existing Promise.all pattern, ensuring the multipart pipe rejection is
observed even when the request fails.

In `@src/routes/api/v1/workspaces`.$workspaceId.file-upload.ts:
- Around line 138-141: Update the completion flow around completionClaimKey and
its finally cleanup so the claim is released only when upload completion fails;
preserve the claim after successful completion. Ensure retries with the same
valid completion token cannot re-claim and proceed to the deleted staging
object, while existing failure cleanup remains intact.

---

Outside diff comments:
In `@src/features/workspaces/conversion/container-file-conversion.ts`:
- Line 1: Preserve the buffered conversion’s empty-result handling across
streaming consumers: update convertFileStreamWithContainer to validate streamed
byte length, add a stored.size === 0 rejection after the existing checks in
workspace file upload storage, and validate bytes.byteLength before constructing
the Blob for env.AI.toMarkdown in the workers AI markdown flow.
- Line 1: Update convertFileStreamWithContainer so multipart.done is observed
concurrently with input.container.fetch(...), using Promise.all or an equivalent
catch handler when creating the stream; ensure rejected fetches do not leave the
multipart pipe promise unhandled.

---

Nitpick comments:
In `@src/features/workspaces/kernel/workspace-kernel-file-commands.ts`:
- Around line 329-353: Update deleteObjects to record an operational outcome for
every rejected deleteR2Prefix result rather than selecting only the first
failure. Preserve the existing workspace and item-count context, and include
each failure’s reason so all failed cleanup attempts remain visible for
reconciliation.

In `@src/features/workspaces/kernel/workspace-kernel.ts`:
- Around line 267-276: Update the workspace deletion cleanup in the surrounding
workspace-removal method to derive all R2 prefixes from the shared namespace
constants or key-building helpers used by ShellWorkspace and the workspace
file-object/upload implementations, instead of hardcoded literals. Preserve the
existing five deletion targets, including the chat attachment prefix from
getChatAttachmentWorkspacePrefix, so cleanup remains aligned with the keys
created elsewhere.

In `@src/lib/http/streaming-multipart.ts`:
- Around line 28-59: Update the error handling in pipeMultipartBody to cancel
the source reader with the caught error before releasing its lock. Preserve the
existing writer.abort behavior and rethrow the original error, ensuring reader
cancellation is attempted without masking that error.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9f9ae256-8ba3-404c-9139-fdb5db64c764

📥 Commits

Reviewing files that changed from the base of the PR and between 8cc5b38 and 1e54e0e.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (65)
  • config/r2/cors.production.json
  • config/r2/cors.staging.json
  • containers/liteparse/Dockerfile
  • containers/liteparse/server.mjs
  • docs/configuration/deployments.mdx
  • package.json
  • src/features/workspaces/conversion/container-file-conversion.ts
  • src/features/workspaces/conversion/image-file-converter.ts
  • src/features/workspaces/conversion/office-pdf-converter.ts
  • src/features/workspaces/extraction/liteparse-projection.ts
  • src/features/workspaces/extraction/page-markdown-projection.ts
  • src/features/workspaces/extraction/providers/firecrawl.ts
  • src/features/workspaces/extraction/providers/liteparse-response.ts
  • src/features/workspaces/extraction/providers/liteparse.test.ts
  • src/features/workspaces/extraction/providers/liteparse.ts
  • src/features/workspaces/extraction/providers/llama-parse.ts
  • src/features/workspaces/extraction/providers/workers-ai-to-markdown.ts
  • src/features/workspaces/extraction/request-workspace-file-extraction.ts
  • src/features/workspaces/extraction/types.ts
  • src/features/workspaces/extraction/workspace-file-extraction-workflow.ts
  • src/features/workspaces/extraction/workspace-file-preview-projection.ts
  • src/features/workspaces/extraction/workspace-file-source.ts
  • src/features/workspaces/extraction/workspace-page-projection.test.ts
  • src/features/workspaces/extraction/workspace-page-projection.ts
  • src/features/workspaces/files/pdfium-server.ts
  • src/features/workspaces/files/workspace-file-object-keys.ts
  • src/features/workspaces/files/workspace-file-preview.constants.ts
  • src/features/workspaces/files/workspace-file-preview.ts
  • src/features/workspaces/files/workspace-file-processor.ts
  • src/features/workspaces/files/workspace-file-upload.ts
  • src/features/workspaces/kernel/workspace-kernel-access.ts
  • src/features/workspaces/kernel/workspace-kernel-file-commands.ts
  • src/features/workspaces/kernel/workspace-kernel-files.ts
  • src/features/workspaces/kernel/workspace-kernel-item-commands.ts
  • src/features/workspaces/kernel/workspace-kernel-rows.ts
  • src/features/workspaces/kernel/workspace-kernel-schema.ts
  • src/features/workspaces/kernel/workspace-kernel-types.ts
  • src/features/workspaces/kernel/workspace-kernel.ts
  • src/features/workspaces/model/workspace-file/limits.ts
  • src/features/workspaces/operations/read-items.ts
  • src/features/workspaces/operations/read-page-selection.test.ts
  • src/features/workspaces/operations/read-page-selection.ts
  • src/features/workspaces/operations/workspace-tool-definitions.ts
  • src/features/workspaces/operations/workspace-tool-schemas.ts
  • src/features/workspaces/read-page-selection.ts
  • src/features/workspaces/upload/pdf-upload-validation.test.ts
  • src/features/workspaces/upload/pdf-upload-validation.ts
  • src/features/workspaces/upload/workspace-file-direct-upload.test.ts
  • src/features/workspaces/upload/workspace-file-direct-upload.ts
  • src/features/workspaces/upload/workspace-file-upload-normalization.test.ts
  • src/features/workspaces/upload/workspace-file-upload-normalization.ts
  • src/features/workspaces/upload/workspace-file-upload-protocol.ts
  • src/features/workspaces/upload/workspace-file-upload-storage.test.ts
  • src/features/workspaces/upload/workspace-file-upload-storage.ts
  • src/features/workspaces/upload/workspace-upload-intake.test.ts
  • src/features/workspaces/upload/workspace-upload-intake.ts
  • src/lib/http/streaming-multipart.ts
  • src/lib/r2.test.ts
  • src/lib/r2.ts
  • src/routes/api/v1/workspaces.$workspaceId.file-upload.ts
  • src/routes/api/v1/workspaces.$workspaceId.files.$itemId.content.ts
  • src/routes/api/v1/workspaces.$workspaceId.files.$itemId.preview.ts
  • src/server.ts
  • worker-configuration.d.ts
  • wrangler.jsonc
💤 Files with no reviewable changes (7)
  • src/features/workspaces/files/workspace-file-preview.constants.ts
  • src/features/workspaces/files/pdfium-server.ts
  • src/features/workspaces/upload/workspace-file-upload-normalization.test.ts
  • src/features/workspaces/operations/read-page-selection.ts
  • src/features/workspaces/kernel/workspace-kernel-files.ts
  • src/features/workspaces/extraction/page-markdown-projection.ts
  • src/features/workspaces/upload/workspace-file-upload-normalization.ts

Comment thread containers/liteparse/server.mjs
Comment thread src/features/workspaces/conversion/container-file-conversion.ts Outdated
Comment thread src/features/workspaces/conversion/container-file-conversion.ts
Comment thread src/features/workspaces/extraction/workspace-file-preview-projection.ts Outdated
Comment thread src/features/workspaces/upload/pdf-upload-validation.ts
Comment thread src/features/workspaces/upload/workspace-file-direct-upload.ts
Comment thread src/features/workspaces/upload/workspace-file-upload-storage.ts
Comment thread src/lib/http/streaming-multipart.ts
Comment thread src/routes/api/v1/workspaces.$workspaceId.file-upload.ts Outdated

@cubic-dev-ai cubic-dev-ai 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.

All reported issues were addressed across 66 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread src/features/workspaces/kernel/workspace-kernel-schema.ts
Comment thread src/features/workspaces/read-page-selection.ts
Comment thread src/routes/api/v1/workspaces.$workspaceId.file-upload.ts Outdated
Comment thread src/features/workspaces/upload/workspace-file-direct-upload.ts
Comment thread src/features/workspaces/files/workspace-file-processor.ts
Comment thread containers/liteparse/server.mjs
Comment thread containers/liteparse/server.mjs Outdated
Comment thread src/features/workspaces/kernel/workspace-kernel.ts
Comment thread src/features/workspaces/operations/read-page-selection.test.ts Outdated
Close review-found failures in upload replay, empty output, and processor errors.

Make projection publication atomic and bound streaming reads.

Tests: pnpm check; pnpm test; pnpm exec wrangler types --check
Skip processor error-body parsing for 5xx responses.

Acquire the workspace kernel while document conversion runs.
Comment thread containers/liteparse/server.mjs
Comment thread src/features/workspaces/extraction/workspace-page-projection.ts

@cubic-dev-ai cubic-dev-ai 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.

1 issue found across 26 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/features/workspaces/conversion/container-file-conversion.test.ts">

<violation number="1" location="src/features/workspaces/conversion/container-file-conversion.test.ts:59">
P3: The `startAndWaitForPorts` mock accepts any arguments silently; the test only checks call count. Adding a `toHaveBeenCalledWith` assertion for the config payload (`{cancellationOptions: {portReadyTimeoutMS: 60000}}`) would catch accidental misconfiguration.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread src/features/workspaces/extraction/liteparse-projection.ts Outdated
Comment thread src/features/workspaces/extraction/workspace-page-projection.ts Outdated
await request.arrayBuffer();
return new Response(output.slice().buffer);
}),
startAndWaitForPorts: vi.fn(async () => undefined),

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.

P3: The startAndWaitForPorts mock accepts any arguments silently; the test only checks call count. Adding a toHaveBeenCalledWith assertion for the config payload ({cancellationOptions: {portReadyTimeoutMS: 60000}}) would catch accidental misconfiguration.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/workspaces/conversion/container-file-conversion.test.ts, line 59:

<comment>The `startAndWaitForPorts` mock accepts any arguments silently; the test only checks call count. Adding a `toHaveBeenCalledWith` assertion for the config payload (`{cancellationOptions: {portReadyTimeoutMS: 60000}}`) would catch accidental misconfiguration.</comment>

<file context>
@@ -0,0 +1,69 @@
+			await request.arrayBuffer();
+			return new Response(output.slice().buffer);
+		}),
+		startAndWaitForPorts: vi.fn(async () => undefined),
+	};
+}
</file context>

urjitc added 2 commits July 15, 2026 10:30
Preserve immutable R2 artifacts across ambiguous Durable Object failures.

Recover retried item creation from the kernel event log and avoid duplicate extraction queues.

Tests: pnpm check; pnpm test; pnpm knip

@cubic-dev-ai cubic-dev-ai 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.

3 issues found across 18 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/features/workspaces/extraction/liteparse-projection.ts">

<violation number="1" location="src/features/workspaces/extraction/liteparse-projection.ts:54">
P2: A failed LiteParse metadata commit now leaks the already-written page objects and manifest in R2 because this direct upsert is outside the writer's cleanup scope. Retain the cleanup-aware commit helper (or delete the projection prefix when this upsert fails) so retries and source-change failures do not accumulate orphaned extraction artifacts.</violation>
</file>

<file name="src/routes/api/v1/workspaces.$workspaceId.file-upload.ts">

<violation number="1" location="src/routes/api/v1/workspaces.$workspaceId.file-upload.ts:136">
P1: Concurrent or replayed completion requests can now both process the same staging object because this path no longer acquires an atomic completion claim. Idempotency is checked only after conversion/R2 storage and extraction is queued on each request, duplicating work and potentially racing the extraction workflow; retaining an atomic completion gate or equivalent one-time state would preserve replay protection.</violation>

<violation number="2" location="src/routes/api/v1/workspaces.$workspaceId.file-upload.ts:156">
P2: A source object is orphaned when kernel metadata creation fails after `storeWorkspaceFileUpload` succeeds: the object has already been written, but this path no longer deletes it unless the command completes. Restoring failure cleanup around `createWorkspaceFileFromUpload` would remove the object on pre-commit errors while retaining it after a successful commit.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic


const formData = await request.formData();
const file = formData.get(fileFormKey);
const stagingObjectKey = getWorkspaceFileUploadObjectKey(claims);

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.

P1: Concurrent or replayed completion requests can now both process the same staging object because this path no longer acquires an atomic completion claim. Idempotency is checked only after conversion/R2 storage and extraction is queued on each request, duplicating work and potentially racing the extraction workflow; retaining an atomic completion gate or equivalent one-time state would preserve replay protection.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/routes/api/v1/workspaces.$workspaceId.file-upload.ts, line 136:

<comment>Concurrent or replayed completion requests can now both process the same staging object because this path no longer acquires an atomic completion claim. Idempotency is checked only after conversion/R2 storage and extraction is queued on each request, duplicating work and potentially racing the extraction workflow; retaining an atomic completion gate or equivalent one-time state would preserve replay protection.</comment>

<file context>
@@ -136,22 +130,20 @@ async function finalizeWorkspaceFileUpload(
 		observation.plan = validation.plan.kind;
-		stagingObjectKey = getWorkspaceFileUploadObjectKey(claims);
+
+		const stagingObjectKey = getWorkspaceFileUploadObjectKey(claims);
 		const stagingObject = await env.WORKSPACE_KERNEL_FILES.get(stagingObjectKey);
 
</file context>

Comment thread src/features/workspaces/kernel/workspace-kernel-types.ts Outdated
const projectionPages = parseMarkdownPagesProjection(content);
const markdownLength = joinMarkdownProjectionPages(projectionPages).length;

await kernel.upsertFileProjection({

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.

P2: A failed LiteParse metadata commit now leaks the already-written page objects and manifest in R2 because this direct upsert is outside the writer's cleanup scope. Retain the cleanup-aware commit helper (or delete the projection prefix when this upsert fails) so retries and source-change failures do not accumulate orphaned extraction artifacts.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/workspaces/extraction/liteparse-projection.ts, line 54:

<comment>A failed LiteParse metadata commit now leaks the already-written page objects and manifest in R2 because this direct upsert is outside the writer's cleanup scope. Retain the cleanup-aware commit helper (or delete the projection prefix when this upsert fails) so retries and source-change failures do not accumulate orphaned extraction artifacts.</comment>

<file context>
@@ -54,23 +51,20 @@ export async function publishLiteParseProjection(
-							provisional: true,
-						},
-						actorUserId: params.actorUserId,
+				await kernel.upsertFileProjection({
+					itemId: params.itemId,
+					format: "pages",
</file context>

Comment thread src/features/workspaces/kernel/workspace-kernel-file-commands.ts Outdated
observation.outputBytes = upload.fileSize;
observation.outputBytes = claims.fileSize;
} else {
const finalObjectKey = getWorkspaceFileSourceObjectKey(claims);

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.

P2: A source object is orphaned when kernel metadata creation fails after storeWorkspaceFileUpload succeeds: the object has already been written, but this path no longer deletes it unless the command completes. Restoring failure cleanup around createWorkspaceFileFromUpload would remove the object on pre-commit errors while retaining it after a successful commit.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/routes/api/v1/workspaces.$workspaceId.file-upload.ts, line 156:

<comment>A source object is orphaned when kernel metadata creation fails after `storeWorkspaceFileUpload` succeeds: the object has already been written, but this path no longer deletes it unless the command completes. Restoring failure cleanup around `createWorkspaceFileFromUpload` would remove the object on pre-commit errors while retaining it after a successful commit.</comment>

<file context>
@@ -160,62 +152,50 @@ async function finalizeWorkspaceFileUpload(
-		observation.conversion = upload.source?.conversion;
-		observation.outputBytes = upload.fileSize;
+		} else {
+			const finalObjectKey = getWorkspaceFileSourceObjectKey(claims);
+			const upload = await storeWorkspaceFileUpload({
+				body: stagingObject.body,
</file context>

Comment thread src/features/workspaces/extraction/workspace-file-preview-projection.ts Outdated
`${JSON.stringify({ markdown: page.markdown, pageNumber: page.pageNum })}\n`,
)
) {
await once(response, "drain");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

React Doctor · react-doctor/async-await-in-loop (warning)

This makes the for…of loop slow because each await runs one after another, so collect the independent calls & run them together with await Promise.all(items.map(...))

Fix → Collect the items, then use await Promise.all(items.map(...)) so independent work runs at the same time

Docs

}

for (let pageNumber = lastPageNumber + 1; pageNumber < page.pageNumber; pageNumber += 1) {
await schedulePageWrite(input.bucket, writes, prefix, pageNumber, "");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

React Doctor · react-doctor/async-await-in-loop (warning)

This makes the for-loop slow because each await runs one after another, so collect the independent calls & run them together with await Promise.all(items.map(...))

Fix → Collect the items, then use await Promise.all(items.map(...)) so independent work runs at the same time

Docs

@cubic-dev-ai cubic-dev-ai 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.

1 issue found across 5 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/features/workspaces/kernel/workspace-kernel-file-commands.ts">

<violation number="1" location="src/features/workspaces/kernel/workspace-kernel-file-commands.ts:59">
P3: The upload path now duplicates the created-item idempotency helper already implemented in `workspace-kernel-item-commands.ts`. Keeping two copies means future changes to client-mutation recovery can diverge between item and file creation; a shared helper would keep these paths consistent.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

input: CreateWorkspaceKernelFileFromUploadArgs,
): Promise<WorkspaceCommandResult<WorkspaceItemSummary>> {
const parentId = input.parentId ?? null;
const getPriorResult = () => {

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.

P3: The upload path now duplicates the created-item idempotency helper already implemented in workspace-kernel-item-commands.ts. Keeping two copies means future changes to client-mutation recovery can diverge between item and file creation; a shared helper would keep these paths consistent.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/workspaces/kernel/workspace-kernel-file-commands.ts, line 59:

<comment>The upload path now duplicates the created-item idempotency helper already implemented in `workspace-kernel-item-commands.ts`. Keeping two copies means future changes to client-mutation recovery can diverge between item and file creation; a shared helper would keep these paths consistent.</comment>

<file context>
@@ -56,15 +56,20 @@ export class WorkspaceKernelFileCommands {
-
-		if (priorEvent) {
-			return { event: priorEvent, result: this.store.requireItem(input.id) };
+		const getPriorResult = () => {
+			const event = input.clientMutationId
+				? this.events.getCreatedItemEvent({
</file context>

@urjitc
urjitc merged commit 31e5e0f into main Jul 15, 2026
9 checks passed
@urjitc
urjitc deleted the codex/r2-workspace-file-architecture branch July 15, 2026 14:56
@github-project-automation github-project-automation Bot moved this from Backlog to Done in Dev Board Jul 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant