recovery: destination reset + machine-replacement runbook (F20, F28)#176
Merged
mbertschler merged 6 commits intoJul 24, 2026
Merged
Conversation
Add ResetDestination: an operator-driven verb that forgets a destination's recorded remote state (remote_objects, remote_packs, the durability vector, and push-freshness) so the next sync treats it as fresh and re-uploads. It is the supported recovery for a wrecked or repointed destination, replacing hand SQL or a cross-machine rename. The reset is audit-preserving: the runs table and the append-only destination_run_ids_history advance log are untouched, and the content/files rows are never modified. The reset itself is recorded as a kind='audit' run (reusing the destination-scoped audit shape, so no schema change) with a new free-text 'reset-destination' runs_audit transition carrying the cleared counts. Everything runs in one transaction — a reset is all-or-nothing. CountDestinationRecordedState backs the CLI's dry-run preview. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014Skm2Qm6wWUB7dD4CpPTz7
The packed and content-addressed layout guards keyed on run history by destination name: a recorded success whose manifest segment / pack map is absent was refused outright, so following the guard's own advice (a fresh root) still refused. Now, when the marker is missing, the guard probes the configured root; an empty root — a wiped or repointed destination, or one cleared by `squirrel destination reset` — is treated as a fresh start and re-uploaded. A non-empty root, or any error probing it, keeps the refusal (fail-closed). The refusal message now also points at `destination reset`. Adds Rclone.remoteRootEmpty (recursive lsf) and a shared freshStartOnEmptyRoot helper. The fake-rclone test shim learns `lsf`; the mirror-era guard tests now seed a remote file so they exercise a genuine layout conflict, and new tests cover the empty-root fresh start on both layouts. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014Skm2Qm6wWUB7dD4CpPTz7
`squirrel destination reset <name>` is a weighty change command: it prints
what it will clear, refuses without --yes, and offers --dry-run — mirroring
ux-principle 2. Parent `destination` command and the `reset` subcommand live
in separate files. A configured-but-unrecorded destination reports "nothing
to reset" rather than minting an empty run.
restore's node dead-end ("restore from node destinations is not supported")
now points at the machine-replacement runbook — rebuilding an edge machine
from its hub is a reverse peer push driven from the hub, not a restore.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Skm2Qm6wWUB7dD4CpPTz7
New "Recovery & disaster runbooks" guide: resetting a wrecked destination, rebuilding a dead machine from its hub via a reverse peer push (re-pairing with `node pair`), and the manual DR that already works and stays — mirror restore, ride-along index-snapshot recovery, and packed/content-addressed recovery. Adds `squirrel destination reset` to the CLI reference and the guide to the sidebar. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014Skm2Qm6wWUB7dD4CpPTz7
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014Skm2Qm6wWUB7dD4CpPTz7
There was a problem hiding this comment.
Pull request overview
Adds explicit, audit-preserving recovery workflows to Squirrel: a new operator-driven destination reset command (to clear only derived remote state and force a re-upload), plus “empty remote root ⇒ fresh start” recognition for content-addressed/packed layout guards, and a machine-replacement runbook with improved restore signposting.
Changes:
- Add
squirrel destination reset <destination> [--yes] [--dry-run]and store support to clear derived destination state while recording an audit run. - Teach packed and content-addressed layout watermark guards to treat an empty configured remote root as a fresh start when markers are missing.
- Add recovery documentation/runbooks and update CLI/docs navigation and restore messaging to point users at the supported machine-replacement path.
Reviewed changes
Copilot reviewed 18 out of 18 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| sync/sync.go | Adds freshStartOnEmptyRoot helper to gate “missing marker” cases. |
| sync/rclone.go | Adds remoteRootEmpty probe used by layout guards. |
| sync/packed.go | Uses empty-root probe to allow fresh-start instead of refusing on missing placement map. |
| sync/packed_test.go | Adds/adjusts tests for mirror-era refusal and fresh-start-on-empty-root. |
| sync/content_addressed.go | Uses empty-root probe to allow fresh-start instead of refusing on missing manifest segment. |
| sync/content_addressed_test.go | Extends fake-rclone shim for lsf; adds mirror-era refusal and fresh-start tests. |
| store/runs_audit.go | Adds reset-destination transition constant for audit trail. |
| store/destination_reset.go | Implements destination derived-state counting + transactional reset with audit run. |
| store/destination_reset_test.go | Verifies reset clears only derived state, preserves audit history, and records audit run. |
| cmd/squirrel/root.go | Registers new destination command group. |
| cmd/squirrel/destination.go | Adds squirrel destination parent command. |
| cmd/squirrel/destination_reset.go | Implements CLI UX for reset (preview/refuse unless confirmed, dry-run). |
| cmd/squirrel/destination_reset_test.go | Adds CLI tests for unknown/nothing-to-reset/dry-run/confirmation/confirmed behaviors. |
| cmd/squirrel/restore.go | Improves error message when sync_to names a peer node (signposts runbook). |
| docs/src/content/docs/reference/cli.md | Documents squirrel destination reset and links to recovery guide. |
| docs/src/content/docs/guides/recovery.md | Adds recovery & disaster runbooks (destination reset + machine replacement + manual DR links). |
| docs/astro.config.mjs | Adds recovery guide to sidebar navigation. |
| design/friction-log.md | Updates F20/F28 entries to reference the new supported paths. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
FIX 1 (fail-closed): remoteRootEmpty no longer reads any lsf error with empty stdout as an empty root — runPlain carries stderr only in the error, so an auth/network failure was being mistaken for a wiped destination and letting the layout guard proceed. Now only rclone's canonical "directory not found" (or "object not found") counts as a fresh root; every other error surfaces so the guard fails closed. New test injects an auth-style lsf error (guard refuses) vs a not-found (fresh root). FIX 2: the restore node-restore signpost pointed at a repo source path (docs/guides/recovery) that doesn't exist for an installed binary; it now names the published docs route (guides/recovery). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014Skm2Qm6wWUB7dD4CpPTz7
mbertschler
merged commit Jul 24, 2026
8d1c4bb
into
claude/issue-162-bootstrap-helpers
3 checks passed
3 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The two recovery scenarios the reference-setup walk dead-ended on (friction log F20 + F28). Both are now explicit, human-driven, audit-preserving acts — the agent never runs them on a cadence.
Changes
squirrel destination reset <name>(F20) — forgets a destination's recorded remote state (remote_objects/remote_packs, the durability vector, push-freshness) so the next sync re-uploads. A change command per ux-principle 2: prints what it will clear, refuses without--yes, offers--dry-run, reports loudly with the run id. A configured-but-unrecorded destination reports "nothing to reset" rather than minting an empty run.rootstill refused. Now, when the layout marker is missing, the guard probes the configured root: an empty root (wiped/repointed, or cleared bydestination reset) is a fresh start; a non-empty root or any probe error keeps the refusal (fail-closed). The refusal message points atdestination reset.docs/guides/recovery.mddocuments rebuilding a dead edge machine from its hub via a reverse peer push (the same mechanismnas→htpcuses daily), re-pairing with cli: day-1 bootstrap helpers — config check, agent cert, node pairing #162'snode pair.restore's node dead-end error now signposts it instead of just refusing.Audit-preserving guarantee: the reset never touches the
runstable, the append-onlydestination_run_ids_historyadvance log, or the content/files rows. Only derived state is cleared, and the reset is itself recorded as a run.runs.kindhas a CHECK constraint, so a new run kind would need a migration — the parallel #157/#158 chain owns v25/v26, so I did not add one. Instead the reset reuses the existingauditrun kind (the destination-scoped, volume/destination-NULL shapeBeginRemoteVerifyRunalready uses) and records its detail via a new free-textruns_audittransitionreset-destination(that column has no CHECK — new transitions need no migration).SchemaVersionis unchanged;TestSchemaSnapshotstays green.1. Destination-reset command shape.
squirrel destination reset <name>with--yes(confirm) +--dry-run. Bare (no--yes) prints the preview and refuses. It clears derived state only and leaves the remote bytes untouched — the operator wipes/repoints the remote separately, after which the empty-root guard does the rest. Alternative shapes not taken: an interactive y/n prompt (the codebase's idiom is explicit flags like--force/--allow-rewind), or also deleting the remote bytes (kept out — reset is about squirrel's record, and deleting remote data is a heavier, separate act).2. Machine-replacement approach: reverse peer push (runbook), not
restore --from <node>. I chose to reuse the existing peer-sync push driven from the hub rather than build restore-from-node. Rationale: the machinery already exists and runs daily (nas→htpc), it is content-verified end-to-end (peer-blake3) and records correlated runs on both sides, and re-pairing reuses #162's helper — zero new transfer code, lowest risk.restore --from <node>would mean pulling a volume down over a protocol path that doesn't exist today. The only code change on this front is turning therestoredead-end into a signpost. Please confirm you'd rather have the documented reverse-push than a newrestore --from <node>verb.Testing
go vet ./...,go test ./...— all pass.golangci-lint run(v2.12.2, rebuilt with the module's Go 1.26.1 toolchain to clear a packaged-binary version skew) — 0 issues.lsf.Scope
Closes #163. Recovery paths only; the manual DR paths are cross-referenced, not reimplemented (packed-native restore remains tracked by #142/PR #169).🤖 Generated with Claude Code
https://claude.ai/code/session_014Skm2Qm6wWUB7dD4CpPTz7
Generated by Claude Code