Skip to content

fix(studio-server): a history restore refuses to remove every tracked file - #6175

Open
miguel-heygen wants to merge 5 commits into
mainfrom
fix/history-restore-refuses-to-empty-project
Open

miguel-heygen wants to merge 5 commits into
mainfrom
fix/history-restore-refuses-to-empty-project

Conversation

@miguel-heygen

@miguel-heygen miguel-heygen commented Oct 24, 2026 •

Copy link
Copy Markdown
Collaborator

What

A project history restore no longer deletes every file in the project. It now refuses, writes nothing, and says why, in two cases:

  • the restore would leave the project with no files at all (for example restoring "the start" of a history that was opened on an empty folder, so every file was added later). History's own hidden records (the media ledger and Studio's manifests) do not count as files kept, so a start that held only imported media is refused too;
  • the restore targets "the start" while large files from the first open are still being copied into history, so the starting snapshot is not complete yet.

A restore that removes some files (the ones added after the chosen point) still works as before. A normal undo of the change that added a project's only file is unchanged (it does not go through restore).

Why

A project lost its index.html, compositions/* and assets/* files; the folders stayed. A restore builds its plan as "every tracked file deleted, then the files at the chosen point laid back on top". When the chosen point is the start and the start snapshot is empty (history opened on an empty folder) or still missing files that are copied in the background, that plan deletes everything. File deletes do not remove folders, which matches what was left behind.

Every path that restores goes through the same function: the restore route and hyperframes history restore, and undo with "back to before" (the undo route, hyperframes history undo --back-to-before, and the Desktop chat's go-back and agent undo, which pass that mode). Going back to before a project's first recorded change is a restore of the start.

Related work

None open for this case. Other open PRs edit the same file (media adoption on close, media not copied into history); this change is a few lines in the restore function and should rebase cleanly on either.

How

  • One guard in the engine's restore function, the single place all restore paths reach. It throws a new HistoryRefusedError (exported next to HistoryClosedError) before anything is written.
  • The HTTP history routes already answer any thrown error as a 409 with its message, so they need no change.
  • The CLI counts HistoryRefusedError as a refusal: one line on stderr (or JSON with --json) and exit code 2, like the other history refusals. Through a running preview server, the 409 already became a refusal.

Test plan

  • Unit tests added/updated
  • Manual testing performed
  • Documentation updated (if applicable)
  • Comments follow CONTRIBUTING.md "Comments"

New tests, each entering through a real caller:

Test Enters through
restore of the start on a history opened empty refuses, files stay (projectHistory.test.ts) history.restore, as the restore route and CLI call it
back-to-before of the first entry on that history refuses, files stay history.undo(id, { mode: "back-to-before" }), as the undo route, CLI and Desktop call it
restore of the start when the start held only the media ledger refuses, files stay history.restore
restore of the start while a large file is still being copied refuses; after the copy finishes it goes through history.restore
restore to an earlier entry still removes the file added after it history.restore
normal undo of the change that added the only file still removes it history.undo
POST /history/restore {point: "start"} answers 409 with the message, files stay (routes/history.test.ts) the restore route
hyperframes history restore start exits 2 with the message, files stay, both in-process and through a preview server (commands/history.test.ts) the CLI command

Failing first: with the source files reset to main and the new tests kept, the four refusal tests in studio-server failed (promise resolved instead of rejecting, expected 200 to be 409) and the CLI test failed in both modes (expected +0 to be 2). The two "still works" tests passed on main, as they should.

With the fix, three runs in a row of each touched file: projectHistory.test.ts + routes/history.test.ts 179 passed, 2 skipped (the same 2 skipped as on main), and commands/history.test.ts 50 passed. The ledger-only test also fails on main and on this PR's first commit (promise resolved instead of rejecting). tsc --noEmit for studio-server and cli, oxlint and oxfmt --check on the changed files: clean.

Not exercised: the Desktop go-back UI against a real project (Desktop lives in another repository; a thrown error from this call reaches it the same way as its existing refusals such as "That change is already undone.", but I did not click through it).

Package size budget

@hyperframes/studio-server packed budget goes from 142100 to 143700 in scripts/package-size-budgets.json. The guard adds real code to the package (+249 B packed, about +1 KB unpacked). The budget was already stale: main packed at 143380 B, 0.9% over the old budget and 141 B from the 1% limit, so any change here failed the merge queue's verify:packed-manifests. The new budget is what ships today, with the usual 1% tolerance on top.

@github-actions

Copy link
Copy Markdown
Contributor

Edit accuracy: accurate 271 (base branch 271), smooth 152 of those

The gate passes.
Smoothness is reported in the artifact, not gated. A case fails only if it fails 2 of 3 runs.

Quarantined, measured but not gated (0)

Unstable (2)

  • seqheldtwice-tlhold-px-r0-root-z100: tracking 120.29, pressJump 312.3, drop 0, reload 0, render 0.02, renderKey -, undo true, teleport false / tracking 0.41, pressJump 0.11, drop 0.35, reload 0.35, render 0.02, renderKey -, undo true, teleport true / tracking 0.41, pressJump 0.11, drop 0.35, reload 0.35, render 0.02, renderKey -, undo true, teleport true
  • seqheldtwice-tlhold-px-r0-nested-z100: tracking 120.29, pressJump 312.3, drop 0.07, reload 0.07, render 0.03, renderKey -, undo true, teleport false / tracking 0.41, pressJump 0.11, drop 0.39, reload 0.39, render 0.02, renderKey -, undo true, teleport true / tracking 0.41, pressJump 0.11, drop 0.39, reload 0.39, render 0.02, renderKey -, undo true, teleport true

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

Verdict: APPROVE · 0 blocker · 0 important · 1 nit · head 01f5a328

The guard sits in restoreNow, and every whole-project restore goes through it: restore (route, CLI, owner) and undo --back-to-before. Plain undo and step only write an entry's own files, so they can't empty a project this way. The error is thrown before writeAs, so nothing is written, and the route already maps it to a 409. CI is green at this head.

  • nit: the empty-project check only fires when no project file would remain. A start that held one stray file (say a README.md) still lets a restore of the start delete everything added later. The PR says this is intended. A threshold or a confirm step could be a follow-up if the incident's project wasn't fully empty at the start.
Scope

Read all 6 files plus the engine's adoption code (firstOpen, adoptMediaInBackground, recordAdopted, start). Files still being adopted aren't in tracked until they're recorded, so a restore never deleted them. The adopting refusal is extra caution and doesn't change behaviour once the copy finishes; the test covers both sides. No prior human reviews.

— Genesis

@miguel-heygen
miguel-heygen added this pull request to the merge queue Oct 24, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Oct 24, 2026
@miguel-heygen
miguel-heygen added this pull request to the merge queue Oct 24, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Oct 24, 2026
@miguel-heygen
miguel-heygen added this pull request to the merge queue Oct 24, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Oct 24, 2026

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

Verdict: APPROVE · 0 blocker · 0 important · 0 nits · 01f5a328 → bb46729e

Only change since my approval is the one-line @hyperframes/studio-server packed budget bump (142100 → 143700) in scripts/package-size-budgets.json. The reason is in the PR body, as verify-packed-manifests.mjs asks. Main's 143380 B plus the 249 B guard comes to about 143629 B. That fits under the new budget and stays well above the 0.85 ratchet floor. CI on this head was still running when I approved.

This branch has not been deployed

No deployments
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