Skip to content

fix(studio): harden draft durability and recovery - #14

Merged
aetosdios27 merged 5 commits into
mainfrom
aetosdios/aet-15-harden-studio-durability
Jul 25, 2026
Merged

aetosdios27 merged 5 commits into
mainfrom
aetosdios/aet-15-harden-studio-durability

Conversation

@aetosdios27

@aetosdios27 aetosdios27 commented Jul 25, 2026 •

Copy link
Copy Markdown
Owner

Linear

Refs AET-15

Summary

  • durably journal acknowledged drafts and immediately preserve unsent browser and Rich Text edits in IndexedDB
  • serialize revisioned mutations behind a single-writer lease and reject stale, cross-origin, or conflicting writes
  • save through conflict-aware, symlink-safe atomic replacement while preserving UTF-8 BOMs, line endings, and file permissions
  • isolate MDX compilation in a restartable worker and replace polling/full preview reloads with SSE and Vite HMR
  • cover recovery, corruption, worker failure, external edits, deletion, symlink swaps, multi-tab behavior, and preview continuity

Verification

  • Focused tests pass — 54 Studio tests
  • Type checking passes
  • Meaningful consumer-facing changes include a Changeset
  • No documentation change is required; behavior is internal hardening with existing user-facing recovery diagnostics
  • Existing coverage was not weakened or skipped
  • CLI production build passes
  • Focused Chromium Studio flow passes
  • git diff --check passes

Visual evidence

No intentional visual change. The focused Chromium Studio flow verifies that draft updates reach the production preview through HMR without replacing the iframe document.

Summary by CodeRabbit

  • New Features

    • Added durable draft recovery across browser and application restarts.
    • Added writer coordination so concurrent edits are serialized and stale changes are detected.
    • Added safer saving that preserves symlinks, permissions, BOMs, and line endings while preventing overwrites after external edits.
    • Improved live preview updates without unnecessarily reloading the preview document.
    • Added clearer read-only, reconnecting, conflict, and unsaved-change states.
    • Added background MDX validation with resilient error handling.
  • Bug Fixes

    • Improved protection against invalid, corrupted, cross-origin, and conflicting Studio mutations.

@linear-code

linear-code Bot commented Jul 25, 2026

Copy link
Copy Markdown

AET-15

@coderabbitai

coderabbitai Bot commented Jul 25, 2026 •

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

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

Next review available in: 5 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 Plus

Run ID: 93a15988-bafe-4436-a528-ee69e5b97e82

📥 Commits

Reviewing files that changed from the base of the PR and between 8ca9ede and b882719.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (27)
  • .github/workflows/ci.yml
  • README.md
  • RELEASING.md
  • package.json
  • packages/cli/README.md
  • packages/cli/src/init.test.ts
  • packages/cli/src/studio-compiler.test.ts
  • packages/cli/src/studio-files.ts
  • packages/cli/src/studio-fs.ts
  • packages/cli/src/studio-recovery.test.ts
  • packages/cli/src/studio-recovery.ts
  • packages/cli/src/studio-transactions.test.ts
  • packages/cli/src/studio-transactions.ts
  • packages/cli/src/studio-ui.test.ts
  • packages/cli/src/studio-ui.ts
  • packages/cli/src/studio.test.ts
  • packages/cli/src/studio.ts
  • packages/mdx/README.md
  • packages/react/README.md
  • packages/styles/README.md
  • scripts/audit-public-packages.mjs
  • scripts/test-packed-consumers.mjs
  • tests/integration/next-css-modules/package.json
  • tests/integration/next-remote/package.json
  • tests/integration/next/package.json
  • tests/release/ci.test.ts
  • tests/release/documentation.test.ts
📝 Walkthrough

Walkthrough

Studio gains durable draft recovery, atomic conflict-aware file writes, serialized mutations, secure writer leases, isolated MDX compilation, server recovery APIs, browser IndexedDB recovery, and preview updates that preserve the existing preview document.

Changes

Studio recovery and mutation flow

Layer / File(s) Summary
Durable files and recovery records
packages/cli/src/studio-files.ts, packages/cli/src/studio-recovery.ts, packages/cli/src/*-files.test.ts, packages/cli/src/*-recovery.test.ts
Source snapshots, atomic writes, line-ending preservation, conflict detection, checksummed recovery records, archival, quarantine, and retention are implemented and tested.
Coordination, security, and compilation primitives
packages/cli/src/studio-transactions.ts, packages/cli/src/studio-lease.ts, packages/cli/src/studio-security.ts, packages/cli/src/studio-events.ts, packages/cli/src/studio-compiler.ts, packages/cli/src/rich-preservation.ts, packages/cli/src/*test.ts
Mutation serialization, idempotency, writer leases, capability authorization, revision events, worker-based compilation, and the rich-candidate compiler contract are added with tests.
Studio server integration
packages/cli/src/studio.ts, packages/cli/src/studio.test.ts, .changeset/durable-studios-recover.md
Studio startup, workspace validation, external watching, HTTP mutation routes, recovery persistence, durable saves, preview HMR, diagnostics, shutdown, and release metadata are updated.
Browser recovery and editor integration
packages/cli/src/studio-ui.ts, packages/cli/src/studio-ui.test.ts, tests/studio-browser/studio.spec.ts, playwright.studio.config.ts
The client adds session-aware mutations, writer-aware editing, IndexedDB draft recovery, rich candidate preservation, conflict handling, isolated test state, and preview iframe preservation checks.

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

Possibly related PRs

  • aetosdios27/scribe#9: Related conflict-aware save, reload-before-save, external-change, and line-ending handling in Studio.
🚥 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
Title check ✅ Passed The title is concise and accurately summarizes the PR’s main focus on Studio durability and recovery hardening.
Description check ✅ Passed The description covers the required Linear, Summary, Verification, and Visual evidence sections with relevant details.
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 aetosdios/aet-15-harden-studio-durability

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.

@aetosdios27
aetosdios27 marked this pull request as ready for review July 25, 2026 00:56

@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: 9

🧹 Nitpick comments (3)
packages/cli/src/studio-ui.ts (2)

371-377: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

revision in the body is unused by the server.

The coordinator reads baseRevision from the envelope that request() injects; this extra revision field is silently ignored and invites confusion about which value gates staleness.

🤖 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 `@packages/cli/src/studio-ui.ts` around lines 371 - 377, Remove the unused
revision field from the JSON body built in the request flow around
revisionRef.current, leaving baseSource and baseDiskVersion unchanged. Preserve
the coordinator’s use of the injected baseRevision envelope field as the sole
staleness-gating value.

209-217: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Add onabort so an aborted delete cannot leave the promise pending.

writeBrowserRecovery handles onabort; clearBrowserRecovery does not, so a quota/abort event never settles this promise.

♻️ Suggested tweak
     transaction.oncomplete = () => resolve();
     transaction.onerror = () => reject(transaction.error);
+    transaction.onabort = () => reject(transaction.error);
   });
🤖 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 `@packages/cli/src/studio-ui.ts` around lines 209 - 217, Update
clearBrowserRecovery’s delete transaction promise to register an onabort handler
that rejects with the transaction error, matching writeBrowserRecovery and
ensuring aborted deletes always settle the promise.
packages/cli/src/studio.test.ts (1)

141-146: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

This assertion cannot distinguish cross-origin rejection from missing-token rejection.

authorizeStudioMutation checks the session token first, so the request is rejected before the origin is ever inspected. Removing the cross-site/origin checks entirely would keep this test green. Add a case that supplies a valid sessionToken with a foreign origin (and/or sec-fetch-site: cross-site).

🧪 Suggested addition
   expect(unauthenticated.status).toBe(403);
+
+  const crossOrigin = await fetch(`${handle.origin}/__scribe/api/discard`, {
+    method: "POST",
+    headers: {
+      "content-type": "application/json",
+      "x-scribe-studio-session": handle.sessionToken,
+      origin: "https://attacker.invalid",
+      "sec-fetch-site": "cross-site"
+    },
+    body: "{}"
+  });
+  expect(crossOrigin.status).toBe(403);
🤖 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 `@packages/cli/src/studio.test.ts` around lines 141 - 146, Add a separate test
case in the studio mutation authorization tests that sends a valid sessionToken
while using a foreign origin and/or sec-fetch-site: cross-site, then asserts
rejection. Keep the existing unauthenticated assertion, and ensure the new case
exercises cross-origin validation after token authentication rather than
missing-token handling.
🤖 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 `@packages/cli/src/studio-compiler.test.ts`:
- Around line 12-45: Replace all six hardcoded “/tmp/...” compile identifiers in
the StudioCompiler tests, including the calls in the validation, event-loop,
worker-restart, and timeout cases, with a portable relative-style placeholder
while keeping the test behavior unchanged.

In `@packages/cli/src/studio-files.ts`:
- Around line 129-139: Update packages/cli/src/studio-files.ts lines 129-139 by
moving the Windows EINVAL/ENOTSUP/EPERM tolerance to cover the open call itself,
then extract syncDirectory into a shared module. In
packages/cli/src/studio-recovery.ts lines 188-198, remove the duplicate
syncDirectory implementation and import the shared helper so both callers use
identical handling.

In `@packages/cli/src/studio-recovery.ts`:
- Around line 188-198: Remove the duplicate syncDirectory implementation from
the current module and reuse a single shared helper with the existing
Windows-specific error tolerance. Update both callers/modules, including the
syncDirectory definition in studio-files.ts, to import or otherwise reference
that shared helper so platform handling is maintained in one place.
- Around line 105-111: Update `#trimArchives` to exclude in-flight temporary
archive files ending in .tmp from the entries eligible for deletion, while
preserving the existing active-record and .corrupt exclusions; if practical,
restrict matching to the recognized archive reason suffixes so unrelated files
are not trimmed.
- Around line 35-43: Update loadDraft so `#quarantineCorruptRecord` is invoked
only when reading succeeds but JSON parsing or validateRecord fails; propagate
non-ENOENT read errors such as EACCES or EIO to the caller instead of returning
undefined. Preserve the existing undefined result for ENOENT and ensure
startStudio handles surfaced read failures as diagnostics.

In `@packages/cli/src/studio-transactions.ts`:
- Around line 31-53: Update StudioTransactions.mutate around the enqueued result
and `#operations` cache so failed mutations evict their deduplication entry,
allowing a later call with the same clientId and operationId to rerun
operation(). Preserve caching for successful, stale, and rejected decision
results, and ensure thrown operation failures remove only their corresponding
key.

In `@packages/cli/src/studio-ui.ts`:
- Around line 861-870: Update discard() to check the discard request response
status before calling apply(body, true). For unsuccessful 423/409 responses,
avoid updating editor state, preserve the current session, and surface the
returned error through the existing UI error handling; only apply the body and
reset author mode/session state for successful responses containing the expected
source data.
- Around line 497-498: Update the writer state in the studio UI component to use
a nullable value, initializing it as unknown rather than false. Treat writer ===
null as writable while the lease request is pending, keeping the textarea
editable and hiding read-only status/affordances; render those restrictions only
after the lease explicitly resolves to false.

In `@packages/cli/src/studio.ts`:
- Around line 318-360: Update the error handling in handleExternalChange so
transient ENOENT failures from readStudioFile are ignored, leaving the current
editor state and diagnostics unchanged while waiting for the follow-up
filesystem event. For other errors, preserve the existing conflict state,
watcher diagnostic, and event publication behavior in the catch handler.

---

Nitpick comments:
In `@packages/cli/src/studio-ui.ts`:
- Around line 371-377: Remove the unused revision field from the JSON body built
in the request flow around revisionRef.current, leaving baseSource and
baseDiskVersion unchanged. Preserve the coordinator’s use of the injected
baseRevision envelope field as the sole staleness-gating value.
- Around line 209-217: Update clearBrowserRecovery’s delete transaction promise
to register an onabort handler that rejects with the transaction error, matching
writeBrowserRecovery and ensuring aborted deletes always settle the promise.

In `@packages/cli/src/studio.test.ts`:
- Around line 141-146: Add a separate test case in the studio mutation
authorization tests that sends a valid sessionToken while using a foreign origin
and/or sec-fetch-site: cross-site, then asserts rejection. Keep the existing
unauthenticated assertion, and ensure the new case exercises cross-origin
validation after token authentication rather than missing-token handling.
🪄 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 Plus

Run ID: c7a498e9-0be1-4726-bd90-d6c7963c10e5

📥 Commits

Reviewing files that changed from the base of the PR and between 9ab5ccf and 8ca9ede.

📒 Files selected for processing (22)
  • .changeset/durable-studios-recover.md
  • packages/cli/src/rich-preservation.ts
  • packages/cli/src/studio-compiler.test.ts
  • packages/cli/src/studio-compiler.ts
  • packages/cli/src/studio-events.test.ts
  • packages/cli/src/studio-events.ts
  • packages/cli/src/studio-files.test.ts
  • packages/cli/src/studio-files.ts
  • packages/cli/src/studio-lease.test.ts
  • packages/cli/src/studio-lease.ts
  • packages/cli/src/studio-recovery.test.ts
  • packages/cli/src/studio-recovery.ts
  • packages/cli/src/studio-security.test.ts
  • packages/cli/src/studio-security.ts
  • packages/cli/src/studio-transactions.test.ts
  • packages/cli/src/studio-transactions.ts
  • packages/cli/src/studio-ui.test.ts
  • packages/cli/src/studio-ui.ts
  • packages/cli/src/studio.test.ts
  • packages/cli/src/studio.ts
  • playwright.studio.config.ts
  • tests/studio-browser/studio.spec.ts

Comment thread packages/cli/src/studio-files.ts Resolved
Comment thread packages/cli/src/studio-recovery.ts Resolved
Comment thread packages/cli/src/studio-recovery.ts Resolved
Comment thread packages/cli/src/studio-recovery.ts Resolved
Comment thread packages/cli/src/studio-transactions.ts Resolved
Comment thread packages/cli/src/studio-ui.ts Resolved
Comment thread packages/cli/src/studio-ui.ts Resolved
Comment thread packages/cli/src/studio.ts Resolved
@aetosdios27
aetosdios27 merged commit b4730dd into main Jul 25, 2026
7 checks passed
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