# src/shell/persist — project ext-state persistence, prune filesystem I/O, usage scan ## Scope The persist seam: project ext-state read/write (`session` / `ext_state_io`), the prune path's filesystem half (`prune_fs`), and the extension-side instance-usage scan (`usage_scan`). Internal helpers shared only within the persist TU family live in `persist_internal.h`. The pure orphan computation is owned elsewhere (`core/reclaim`), the pure usage wire elsewhere again (`core/wire`), and every tracking *decision* by `core/tracking`'s authority — this directory is the REAPER/filesystem-facing half only, and it gathers rather than decides. ## Invariants - **Prune is the single, exclusive file-deletion authority.** No bank op, no capture op, no Design View op deletes a file; if any path other than prune deletes a bank file, reject it in review. The ONE carve-out — a shell removing a file it wrote itself moments earlier that no index ever referenced, wherever it sits — is stated in full at `prune_fs.cpp`'s header and nowhere else. - **Dry-run first, always; no silent deletion.** Prune reports before it deletes (orphan count, reclaimed size, and — for a small set — the files); actual deletion is a confirmed second step. No periodic/background sweep. - **Referenced-set is the union across ALL banks, pool included**, further unioned with every live instance's held paths — supplied by `tracking::pruneProtection`, never assembled here — via `prune_reconcile::mergeReferenced`. A file is an orphan iff no bank AND no live instance references it. - **Safest platform deletion available.** Trash-preferred, unlink fallback — Windows routes through `SHFileOperationW` (`FOF_ALLOWUNDO`, verified against SDK 10.0.26100); macOS/Linux fall back to unlink (no portable SWELL trash surface). `prune_fs` is the only module that calls this. - **Manual, explicit trigger only** — a bindable action + a `bank_panel` button, never a silent background sweep. - **Instance-usage fail-safe:** a capture held by any live ReaSampler 9000 instance can never be deleted by prune. `usage_scan` decodes every `rsusage_*` key, enumerates every ReaSampler 9000 FX instance (all tracks incl. master, normal + record/input chains, containers recursively, take FX), and folds via the pure `sample_usage::foldUsageRecords`. Read-only at prune-scan time: `usage_scan` writes no ext-state. - **A ledger this build cannot read is degraded, never "empty".** `ext_state_io` keeps the `LedgerStatus` alongside the ledger, and `saveToActiveProject` SKIPS the `owned_files` write while `tracking::ledgerDegraded` holds — replacing a blob we could not read would destroy the only record of every file created before it, silently turning them into permanently unreclaimable foreign files. The status is written only by `loadFromProject`, so it is sticky until the project is reloaded: captures made during such a session are recorded in memory but not persisted, and degrade to foreign (untouchable), which is the safe direction. - **`PruneReport` carries `blockedByTracking` + `ledgerUnreadable` / `ledgerFutureVersion` / `unreadableUsageKeys`**; dry-run, orphan-set, and reclaim each independently abort (delete nothing) on a block. `BANK_PRUNE_FOLDER` (in `shell/actions`) halts on the flag and prints whichever blockers fired, with channel-correct recovery lines — a corrupt blob may be cleared, a newer build's must not be. ## Modules - `shell/persist` (`session` / `ext_state_io` / `prune_fs`) — the persist seam, split by responsibility (Q-W5; the former `persist.cpp` god-TU and its `persist.h` compatibility umbrella are both retired — callers include `shell/persist/session.h` / `ext_state_io.h` directly). `session` owns the `ReaSamplerSession` lifecycle: the poll identity-transition detection (load / Save-As / forked sibling / recycled pointer) and the `projectconfig`-driven deferred undo/redo reload. `ext_state_io` owns project ext state (`SetProjExtState`/`GetProjExtState`, namespace `"reasampler"`) ↔ `BankBook` JSON, `ViewModeModel` JSON, `TailSetting` JSON, the tracking ledger JSON, the writing-version stamp, GUID minting, and bank-folder relocation. `session` additionally owns `recordCreated` — **the one writer of a birth record**, called at the same point the `Sample` is added, deriving lineage from that `Sample`'s own provenance. `prune_fs` hosts the prune dry-run / full-set orphan queries (gathering `referencedPaths()` plus `tracking::pruneProtection`'s two inputs for the `prune_reconcile` pure core) — and, beside them, `tiedUsageFor`, the resample's replace-vs-add input, deliberately co-located so "both answers come out of one `TrackingState`" is structural rather than a rule two files must remember. It is also **the single file-deletion authority over user files in the bank folder** (`deleteOrphanFile` via `SHFileOperationW`); nothing else in the system deletes bank-folder bytes. Dry-run / orphan-set / reclaim each independently abort (delete nothing) when the authority reports a block. - `usage_scan` — extension-side prune-scan shell: enumerates every `rsusage_*` ext-state key, decodes each `sample_usage` wire record, enumerates every ReaSampler 9000 FX instance across all tracks + master / normal + record chains / containers (recursive) / take FX, and returns the pure `sample_usage::foldUsageRecords` result verbatim. One of the two inputs `tracking::pruneProtection` reads; it decides nothing itself. Read-only: writes no ext-state. - `persist_internal.h` — internal-only shared helpers for the persist TU family (`session` / `ext_state_io` / `prune_fs`); included only by those three TUs, never a public seam (mirror of the panel's `panel_state.h` / the editor's `editor_internal.h` precedent). Holds the former anonymous-namespace helpers more than one split TU needs (active-project + `.rpp` path lookup, project-dir derivation, growing `GetProjExtState` read, project-GUID minting, bank-folder relocation) — all definitions live in `ext_state_io.cpp`. REAPER-free header: the project handle crosses this seam as the same opaque `void*` the public `session` header already uses. ## Gotchas - The pure orphan computation (`prune_reconcile`, `(owned ∩ present) − referenced`) is documented under `core/reclaim`, not here — do not duplicate its spec in this file. - The pure usage wire (`sample_usage`: `UsageRecord`, `planUsagePublish`, `foldUsageRecords`/`usageHeldPaths`, `identityMatches`) is documented under `core/wire`, and the ledger + the two consumer answers under `core/tracking` — neither belongs in this file. - `persist_internal.h` is an internal seam, not a public header — do not include it outside `session.cpp` / `ext_state_io.cpp` / `prune_fs.cpp`.