# src/shell/undo — the one REAPER undo-block opener ## Scope One type, `UndoBlock`, and one query, `undoBlockOpen()`. Every extension-owned undo block in the tree is opened through this type; nothing here decides what a point is called or whether one is worth minting — each call site still owns its own label, mask, and discard-vs-mint condition. The FX reclaim span is NOT this directory's: `shell/view/fx_reclaim` owns it, and this directory only guarantees no block ever nests over it. ## Invariants - **`Undo_BeginBlock2` is not callable outside this directory in every TU but one, and that is enforced by the compiler, not by review.** Only `undo_block.cpp` and `shell/view`'s `fx_reclaim.cpp` (which owns the span's own begin/end) define `REAPERAPI_WANT_Undo_BeginBlock2` / `..._Undo_EndBlock2`; every other TU compiles under `REAPERAPI_MINIMAL`, so a raw `Undo_BeginBlock2` there is an undeclared identifier. **`app/main.cpp` is the one exception** — as the sole `REAPERAPI_IMPLEMENT` TU it never sets `REAPERAPI_MINIMAL` (it must allocate storage for whatever ANY TU wants), so the whole API, `Undo_*` included, compiles clean there too. That TU stays out of the undo business by discipline (`src/app/CLAUDE.md`: pointers + entry + dispatch only), not because the compiler stops it — do not add business logic there that would make this matter. Adding the WANT define back to a third TU is a second, easier way to defeat the enforcement that DOES exist — do not. - **Ending the reclaim span is part of opening, and ONLY the outermost block does it.** At depth 0 the span is the only block that can be open, so ending it mints the pending mode switch's point whole before this block exists. At depth > 0 an enclosing `UndoBlock` already ended it, and a close from the inner one would land at depth 2->1 — minting nothing and hijacking the enclosing block's label. That is the same criterion `applyMode`'s reapply is gated on; it lives in the constructor so no call site has to make the judgment. Full reasoning for the rule itself: `src/shell/view/CLAUDE.md`. - **The constructor's body is one `try`.** `fxReclaimEndUndoSpan` copies a `std::string` and can throw; a throwing constructor runs no destructor, so an uncompensated depth increment would stick forever — `undoBlockOpen()` reads true for the rest of the session, `fxReclaimTick` refuses every tick, and no later `UndoBlock` is ever outermost. Any throw between the increment and the end of the constructor rolls the depth back before propagating. - **Closing is a destructor.** No throw between the open and the label decision can leave a block open in the user's project, which is why several call sites can allocate freely between the two. - **No label recorded means no point.** `UndoBlock` closes on `("", 0)` unless `recordPoint` was called — the discard form, real from a hand-fired action (DAW-measured; `src/shell/view/CLAUDE.md`). A verb that mutated nothing therefore costs no undo entry simply by saying nothing. ## Modules - `undo_block` — `UndoBlock` (RAII open/close plus `recordPoint`) and `undoBlockOpen()`. The latter is read by `fxReclaimTick`, which refuses a tick outright while any block is open: the drain's lazy `openSpan` would otherwise begin at depth 2, and the enclosing block's close would then mint under the span's label and destroy the user's own point. The refusal consumes nothing — no wakeup, no queue, no project pin — so the first tick after the block closes drains as if this one had never been due. ## Gotchas - **`Undo_EndBlock2` is guarded by `ValidatePtr2` only when `project_` is a real, non-null pointer.** `nullptr` means "the active project" — the same convention `Undo_BeginBlock2` was handed — and `ValidatePtr2` does not accept `nullptr` as a valid object of any type, so guarding it unconditionally would silently stop every `UndoBlock undo(nullptr)` site (most of them) from ever closing. A real pointer, by contrast, can go stale between open and close (several call sites allocate freely in between), so only that case is validated — the same asymmetry `fx_reclaim.cpp`'s `closeSpan` already has, for the same reason: its span always holds a real pinned pointer, never `nullptr` for "active". - The depth counter deliberately does NOT count the reclaim span. The span is the thing this stands off, not a peer. - The counter drops AFTER `Undo_EndBlock2`, not before: the close that reaches depth 0 mints a point, and anything re-entered through that must still read a block as open rather than let the drain open a span underneath it.