# Row 185: every activate backs up the stopped store Brief: `docs/plans/2026-10-20_activate-always-backs-up.md` (blob 2877ca401c2f, 52ce57dfe). Gitea #1683, P19 task 403. Owner Darkwing, reviewer Filbert, gate owner Sage. It is the precondition for mos-0928 Part A (`agents/sage/work/window-plan/repin-rows-177-185.md`). ## Files | File | What | |---|---| | `build.patch` | the change, `git diff 52ce57dfe` over the paths below | | `candidate-manifest.sha256` | one line per changed path | - Base: 52ce57dfe. Line numbers are at the base unless marked as the candidate's. `scripts/stack-release.sh` has not changed since 9f5153deb, so the brief's numbers match. - Changed: `scripts/stack-release.sh`, `packages/cli/tests/stack-release.test.mjs` and `docs/TOOLS.md`. Nothing outside the brief's list. ## Design ### The run (change 1) The migrate branch of `run_steps` (:835-842) now reads: ```bash if [ "$STORE_SCHEMA" != "$T_SCHEMA" ]; then in_chain "$STORE_SCHEMA" || rollback_in_run store-newer [ "$MIGRATE" = 1 ] || rollback_in_run migration-needs-flag fi backup_store || rollback_in_run backup if [ "$STORE_SCHEMA" != "$T_SCHEMA" ]; then TAKEN="$ACT" node .../migrate-schema.mjs ... || rollback_in_run migrate-failed say "migrated the store to $T_SCHEMA" fi ``` It sits inside `if [ -e "$STORE" ]`, after the `store_unclean` and schema reads (:831-833), so: - a store with a `-wal` or `-shm` still rolls back `store-unclean` with no backup; - a missing store takes no backup; - a failed backup rolls back with `backup` as before. TAKEN is still empty then, so nothing is restored. `store-newer` and `migration-needs-flag` still roll back before the backup. Those runs never touch the store, so a backup there would only add a directory, and T6 and T9 keep asserting that no `backups/stack` appears. I chose this over "backup first, then the checks" because it keeps the backup tied to a run that goes on to install the target. The cost is one more `if` on the schema test. ### TAKEN and BACKUP (change 2) Before this row `backup_store` set both (:367), because the only backup was a migration's. They now mean two things: - `BACKUP` is the backup the run names. `backup_store` sets it on every backup, so `log_line` (:52) writes it into every line after the backup, and `from_log backup` (:146) finds it on the ok line that made the copy live. `rollback_failed` (:571) names it too. - `TAKEN` is the backup `rollback_in_run` (:543) restores. Only the migrate branch sets it, right before the migration. After a gate failure on a same-schema activate the store is the one the run started from, plus whatever the target's bus wrote while it ran. Restoring would throw those writes away for no reason, and every restore moves the live store to a post-attempt directory. So that rollback puts back the units and the wrapper and leaves the store. The two comments at `backup_store` and `rollback_in_run` say this. ### `--plan` (change 3) `plan` prints one line after the migration or restore block, for activate only: - with a store file: `backup: the run backs up the stopped store to /backups/stack// before any migration`, with the real data root; - with no store file: `backup: none; there is no store file`. The activation id is made by the run, so the plan prints `` as written. The migration lines are unchanged. They already say "after a backup of the stopped store". The line is printed on the `S_CHANGED` path too (the bus stopped during the read), since a store file is there. For `store-newer` and `migration-needs-flag` the plan already says the run would roll back. I did not add a third form for those. ### Rollback and pm-on (change 4) Unchanged. `do_rollback` already lets a same-schema rollback take `--restore` (:891-922): it checks the backup's SCHEMA against the target's, its DATA_ROOT and STAMP, and the restore guard. A same-schema activate's backup has SCHEMA equal to the store's, so test 3 needed no script change. pm-on does not call `run_steps migrate`. ### docs/TOOLS.md (change 5) "Pinned stack update" steps 5 to 7: - step 5: every activate takes the backup before any migration, the log line names it, a missing store takes none, and `store-newer` or a migration without `--migrate` rolls back first; - step 6: the backup is taken on every activate and restored only when a migration ran; after a same-schema activate the store stays as the failed run left it; - step 7: a same-schema rollback can take `--restore` with the activate's backup. Step 3 lists what the plan names and does not mention the new line. The brief assigns steps 5 to 7 only, so I left step 3 for Sage. ## Existing tests that change | Test | Why | |---|---| | T5 | asserted no `backups/stack` after the same-schema B to C1 activate. Now asserts the directory holds exactly the activation's backup (brief test 1 says to update it). | | T11 | the `rollback-failed` message now ends `, backup ` after a same-schema activate. Also asserts the log's backup and that no post-attempt store appears. | | T26 | exact plan output for B to C: the backup line after the `needs:` lines. The same-schema `includes` check has the line between `migration: none` and `commits:`. | | T29 | exact plan output for the stopped-during-read case: the backup line after `target schema: 3h`. | T6 and T9 are unchanged (above). T7, T8, T13, T14, T14b, T43 and T45 do migrating activates and still pass as written: T8's migration case still restores. T23, T27, T36 and T44 read plan lines by offset from a line the backup line comes after, or from the end. T43's `[c, c-1]` listing has only migrating activates, since rollbacks take no backup. The fixture gets one fault, `start-writes-row-`: the copy's bus start writes one event before the holder opens the store. T48 uses it so the store after the failed gate differs from the backup, which makes a restore visible by sha. ## New tests (candidate lines, after T46) 1. T47: a same-schema B to C1 activate leaves `backups/stack//` with SHA256SUMS, DATA_ROOT equal to the data root, SCHEMA equal to the store's (3g) and a STAMP. The backup's sum is the store's sha before the run. The run prints `backup: `, and the ok log line's `backup` is the activation. 2. T48: a gate failure (`healthz-C1`) on the same activate, with the target's start writing one row. It rolls back `healthz`. The store sha after the rollback equals the store at the stop after the failed gate (the fixture's stop snapshot), that sha differs from the backup's, and no post-attempt store appears. Units, wrapper and pointer are back. 3. T49: a same-schema `rollback --restore ` with no rows after STAMP restores and goes live on B at 3g. The store is the backup and the C1 store is kept as the post-attempt store. With one `instance_names` row after STAMP it refuses `post-backup-events` with `0 event(s), 1 instance_names row(s)` and changes nothing, and `--events 1` restores. 4. T50: `activate C1 --plan` prints exactly one `backup:` line, the store-file form, and writes nothing (units, wrapper, pointer, store, the data tree and the log). A rollback plan has no backup line. 5. T51: with the bus stopped and the store files removed, the plan prints `backup: none; there is no store file`, and the activate passes with no `backups` directory, no `backup:` line and a null `backup` in the log. ## Mutants `run185.sh` runs `stack-release.test.mjs` whole, then each mutant against T5, T11 and T47 to T51 (`--test-name-pattern='^(T5|T11) |row 185'`). Each edits the worktree script after a checked backup and puts it back by the backup's sha. | Mutant | Expected killers | |---|---| | m1: the backup only when the schemas differ (today's code) | T5, T47, T49, T11 | | m2: TAKEN set by every backup | T48, T11 | | m3: BACKUP left empty on a same-schema activate | T47, T48, T11 | ## Runs Slot from sage-1233, 2026-10-20T06:46:32.784Z to 06:51:52.217Z. TMPDIR `~/darkwing-scratch/r185/tmp`. | Run | Result | Failing tests | |---|---|---| | `stack-release.test.mjs` | 125 tests, 125 pass, 0 fail | none | | m1: the backup only when the schemas differ | killed | T5, T11, T47, T48, T49 (both subtests) | | m2: TAKEN set by every backup | killed | T11, T48 | | m3: BACKUP empty on a same-schema activate | killed | T11, T47, T48 | T48 also fails under m1, at its log assertion: the line's `backup` is null. `stack-release.sh` was back at its candidate sha after the mutants. ## Readers and hash pins `git grep` over `packages/*/tests/**` and `scripts/test-*.sh` for `stack-release` and `TOOLS.md`, full output: the only reader of `scripts/stack-release.sh` is `stack-release.test.mjs` itself (`SR`, :273, and the copied script at :838). Nothing reads `docs/TOOLS.md`, and no test pins a sha256 of any of the three paths. ## Candidate - Base 52ce57dfe. `52ce57dfe..origin/refactor` (b5d662cab) touches only `agents/sage/work/window-plan/repin-rows-177-185.md`. `build.patch` applies cleanly to both (`git apply --cached --check` on a scratch index). - `build.patch`: sha256 8f2293de151c1c3b4d96ac6d494f7d9f67e849d8f51b804c6cdad799c4269ae6, patch-id (stable) 774cd23186d54eda08be028f7c39b5212cd1ed75, 3 files, 121 insertions, 16 deletions. - `candidate-manifest.sha256`: sha256 bf87dbba645b5c4443670e4a81f44a99afedad6ab16ae0f125d6ab8de27bce3b. | Path | sha256 | |---|---| | `scripts/stack-release.sh` | dccb4a10d5b9a6f680a5bdc06277abf39f7d438c0500cb44d5b1c45012de21bc | | `packages/cli/tests/stack-release.test.mjs` | 94b398246cd87cec282e13147b244cdfa85ea5c98a7bf06a468e1b387692df08 | | `docs/TOOLS.md` | a0ea0f560cd2683c2dd38bdeb88fd4bf339be771e0436e623ba554fcb34d6d54 |