Repository navigation
Remove, delete, prune and compact: branch.remove, project.delete, branch.prune, store.compact #511
Description
Answers #411 and is #446 A8. Owner direction (2026-10-30): no automatic compaction. Space comes back only through explicit SDK calls.
The full spec is core/docs/issues/511/README.md on the PR #507 branch (issue506-store-backends-remote). It builds on the #506 Store model, where runtime.project.remove was renamed runtime.project.delete on 2026-10-30.
The four calls
| Call | What it does | Frees space |
|---|---|---|
branch.remove(name, { confirm }) |
Hides a Branch in one row update. It leaves every listing at once; fork, merge, publish and mount refuse it. Refused for main (ReservedName) and while a Workspace is mounted on it (MountsActive). |
no, waits for compact |
project.delete(name, { confirm, store, stopSandboxes, … }) |
A project alone in its Store: deleted with its volume, as today. In a shared Store: proposed to hide in one row update (D2), instead of today's history-sized delete inside the writer. | alone: yes; shared: at compact |
branch.prune(name, { confirm }) |
Keeps every Commit's record and ID, but gives up the file trees of the Branch's own intermediate Commits. Its head, Layer sources, fork points (merge bases) and merge parents stay whole. Reading a pruned Commit's files fails Pruned. |
no, waits for compact |
store.compact(name, { confirm, dryRun, stopSandboxes }) |
With the Store closed, marks what visible Branches, Layers and unpruned Commits use, delta bases and pooled inode rows included. Copies only that into a new file, verifies it, and swaps it in as pull does. dryRun reports exact savings. SQLite only for now. |
yes, the only one |
Each call is one attempt, with no retry. The CLI gets the same verbs.
Why removal can't simply delete
- Stored data is shared and can't be changed or deleted in place.
- Packs are trigger-immutable, and
read_immutabledepends on that. - Each pack is written by one Save, then reused by later Commits of any project through dedup and delta bases. Delta bases are referenced only by ID, possibly across projects.
- Packs are trigger-immutable, and
- History is shared too. Forks share Commits; merges and Layers point into other Branches.
- Branch names can't be reused safely as-is, because a Branch's ID is derived from its name (
BranchId::named). See D1. - No Store table records mounts, and some roots live outside the Store: mounted bases, in-flight Saves and reads, macOS NFS exports (raised by jiangkoumo on layerfs rm/compact: retire Branches and reclaim unreachable Store history #411). Compact therefore requires the Store closed, which covers all of them.
Findings (diagnostic, one read-only sample, 2026-10-30)
r2-examples-clone: 251.2 MB file, 818 Commits, 410 Branches |
Frees up to |
|---|---|
| Remove the 384 Branches with no Commits of their own, then compact | 5.2 MB |
Remove depot_2.implementation (4 own Commits), then compact |
94.6 MB |
| Remove all 410 Branches (main kept), then compact | 105.1 MB |
| Delete the project, then compact | 237.4 MB |
| Prune all 548 intermediate Commits, then compact | 176.4 MB (loose bound) |
- Dedup is already strong: 260.5 GB of files across all Commits become 688 MB after dedup and 237.4 MB on disk.
- History rows cost about 0.7 KB per Commit (0.55 MB in all). The space is in file data, much of it in a few Commits and in intermediate trees.
- Bounds: these are upper bounds from per-Commit "stored first" attribution. Compact's dry run gives the exact figure.
Builds on
-
A1's reachability walk (SnapshotClosure, proposed by SSK015 on layerfs rm/compact: retire Branches and reclaim unreachable Store history #411). Compact reuses it, plus physical dependencies (delta bases, pooled ordinals).
-
A1's backing question: the visited set is the new file's own indexed, on-disk
object_locationtable. -
cadorn's requirements (layerfs rm/compact: retire Branches and reclaim unreachable Store history #411):
- typed refusal while mounted:
MountsActive; - Branch gone from listings at once;
- collection the caller triggers and observes:
store.compactwith its report and dry run; - an SDK call, named
branch.removehere.
The one difference: removing a Branch that another Branch forked from is allowed (D7), since the fork keeps what it uses.
- typed refusal while mounted:
Owner decisions
- D1 · Reuse of a removed Branch's name. Recommended: new Branches get a generation in their ID (format change). Alternative: refuse reuse while the tombstone exists.
- D2 ·
project.deletein a shared Store hides instead of deleting rows (recommended). - D3 · Writers check at admission that their open file is still the Store's (inode), else
Replaced. Recommended; it also closes pull's opener race. - D4 · PostgreSQL compact later. Remove, delete and prune work on PostgreSQL now.
- D5 · A pruned Commit keeps change counts and bytes, not a full path list (recommended).
- D6 · Schema version bump (
removed_ms,deleted_ms,pruned_ms, partial unique indexes, change summary) with a one-time upgrade on open. - D7 · Allow removing a Branch that another Branch forked from (recommended).
Done when
- the four calls, CLI verbs and Connect actions (Remove…, Delete…, Prune history…, Compact… with a dry run) are built;
- on a fixture Store (cross-project dedup, a cross-project delta base, a fork sharing Commits, a merge, Layers, pruned Commits, pooled leaves), every kept Commit and Layer reads identically after compact, merge bases are unchanged, and the dry run equals the real run;
- remove, delete and prune each take one short write;
- measured on copies of
r2-examples-cloneandservicesunder the agent measurement policy: exact bytes freed against the bounds above, and compact's wall time.
Out of scope: automatic collection, in-place sweep, PostgreSQL compaction, remote-side compaction (push the compacted file), and pruning main's Layers.
Activity
[-]Delete, prune and compact: branch.delete, project.delete, branch.prune, store.compact[/-][+]Remove, delete, prune and compact: branch.remove, project.delete, branch.prune, store.compact[/+]refactor(runtime): rename project.remove to project.delete (#511)
Merge pull request #526 from jiangkoumo/vm515-v2 into layerfs_v2_0_0
Mola-maker commentedon Nov 1, 2026
感谢提出这个 issue,也感谢 AI 协作工具在整个排查过程中做的分析 🙏
对 AI agent 的使用来说,这个问题的解决会直接改善 agent 清理分支和回收存储的完整生命周期管理。
两点建议: