Skip to content

design(backups): a backup that can prove itself — an expected-set completion contract for the laptop backup jobs (#976) #1854

Closed

Description

@dezren39

Relocated from dezren39/darwin-nix#1024 on 2026-10-28 (placement audit): backup design epic for the same jobs as #976/#1026; follows their home. Original body below; issue references inside it are historical (dezren39/nix numbering).

Summary

modules/laptop-backups.nix is the laptop's backup system of record (its header, citing #976 and #1516): two daily jobs, pm-dashboard-backup (local tar.zst + R2 + Drive, each leg sha256-verified) and repo-archive-backup (restic). The machinery works — but a run's success signal does not mean a complete, verified archive exists, and the fleet has now found that six times in the nine days since the dashboard script landed (PR #977):

Three structural causes, all on current main 23551fd:

  1. The manifest is descriptive, not normative — modules/pm-dashboard-backup.sh:172-193 builds it by walking the staging tree; verification (:196-219) checks the archive against that same list. Nothing states what a complete backup contains.
  2. Failure accounting is per-step and ad hoc — some steps || fail, some tolerate deliberately (journal placeholder, optional-file WARN), some swallow by accident (|| true at :242, unchecked cp -a at :147,150,152,156). Every new step re-decides whether its failure counts.
  3. Success leaves no durable record — stdout/journal lines and the journal-window cursor are all a run leaves. fix(cold-store-reaper): curl fallback for S3 downloads urllib cannot deliver #976 (2026-10-27) verified neither timer had ever fired and asked for a freshness check; there is still no artefact such a check could read, and no restore of either job has ever been recorded.

This design is the contract those six issues point at, in five rules:

  1. Expected-set manifest — each job declares its expected members (required vs optional) in one place; the manifest records expected vs present vs missing per member; verification compares the archive against the expected set, so a missing required member fails the run before upload, and a missing optional member is recorded in the manifest, not just a WARN.
  2. One failure ledger per run — every step's outcome lands in a single ledger; exit status and the success line derive from it at the end. A step is non-fatal only when named as tolerated; an undeclared swallow is a defect.
  3. A durable run record — one JSON record per run at a fixed path outside every prune scope: job, date, result, failing steps, archive sha256, per-destination results. Written on complete and failed exits.
  4. A freshness watchdog — a third unit beside the jobs reads the run records + both timers' last-trigger times and exits non-zero naming the job when the last complete run is older than a threshold option. It fixes nothing; it makes a stopped backup named, per fix(cold-store-reaper): curl fallback for S3 downloads urllib cannot deliver #976's acceptance.
  5. The contract is the adoption unit for fix(cold-store-reaper): curl fallback for S3 downloads urllib cannot deliver #976 coverage — new datasets join by declaring expected members + destinations under this contract, never as another script with a private notion of success.

Phased breakdown

  1. Phase 1 — the open instances land → the existing opencode: pool pass 2 — contributor seats, variant remaps, speed pool #1023 and fix(id-web): degrade unsupported table-cell content instead of panicking on save (#1017) #1021 (linked as sub-issues below; fix(id-web): degrade unsupported table-cell content instead of panicking on save (#1017) #1021 already in progress, opencode: pool pass 2 — contributor seats, variant remaps, speed pool #1023 already enqueued). Their regression suites are the seed of the contract's test coverage. No new issue for this phase.
  2. Phase 2 — expected-set manifest for pm-dashboard-backup → new sub-issue filed with this design (enqueued): declared required/optional member list; manifest gains expected-vs-present; verify fails on a missing required member; stubbed-harness regression cases for deleted-required (run fails, nothing uploaded) and missing-optional (run completes, manifest records it).
  3. Phase 3 — run record + freshness watchdog → new sub-issue filed with this design (enqueued): rule-3 record on every exit path; laptop-backups-watchdog service+timer + threshold option in modules/laptop-backups.nix; watchdog test suite in the repo's established style wired as a flake check. The repo-archive side is covered via timer-firing only — its script and restic credentials live outside this repo (stated honestly in the design doc).

Alternatives considered and why they lose, the full risk analysis (expected-set drift, watchdog false alarms vs Persistent = true catch-up, run-record placement per #982's lesson), and the open questions (scheduled restore drills; whether the repo-archive script is vendored into the repo; eventual restic migration; off-machine alerting; the missing ROADMAP backup row) are in the design doc — they stay questions, not issues, until answered.

Links

🤖 Agent note · state: filed

Acceptance criteria

Activity

  1. developing-today commented on Oct 28, 2026

    @developing-today
    Contributor
  2. added
    priority/normalOrdinary priority (the default)
    area/infraInfrastructure, deployment, and operations
    epicMeta-issue grouping multiple items (org Project Type=Epic)
    effort/lLarge: multi-day or open-ended; expect staged check-ins
    kind/designA design proposal (design-architect): multi-step, phased, decision-bearing
    queue/queuedHas a queued task in the Supabase agent queue
    on Oct 28, 2026
  3. dezren39 commented on Oct 28, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · state: handed off

    • Agent: fleet monitor (repo-fleet-monitor)
    • Session / ID: repo-fleet-monitor run 2026-10-28 18:19 CDT; queue task 2915
    • Found via: monitor reconciliation (Duty 1 safety net)
    • Task: This issue carried queue/queued with no live queue task and no consumed triage dedupe key (verified by direct dedupe-key query). Enqueued a triage repo-task/v1 packet as task 2915 in repo-health-code-pilot; the queue worker will triage and route it.
    • Expectation: effort/l design item; triage verdict on current main, then a scoped follow-up packet if confirmed.
  4. added
    work/assignedQueue worker lifecycle state
    queue/claimedQueue worker lifecycle state
    and removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Nov 1, 2026
  5. 12 remaining items

  6. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up

  7. added a commit that references this issue on Nov 1, 2026
  8. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · done (scoped slice)

  9. added
    queue/queuedHas a queued task in the Supabase agent queue
    task/triageQueue packet kind: verify, reproduce, and route this item
    and removed
    queue/claimedQueue worker lifecycle state
    work/assignedQueue worker lifecycle state
    on Nov 1, 2026
  10. dezren39 commented on Nov 3, 2026

    @dezren39
    MemberAuthor

    agent-note:v1
    state: triaged (coverage pass)
    Coverage pass 2026-11-03: reviewed in the fleet-wide per-issue pass (drift / overlap / already-handled / stale / blockers / ordering). Verdict: still live; queued in repo-health-code-pilot as triage at priority 40 (priority/normal). If this is already handled at current main, the triage closes it with evidence; if it needs splitting, the triage files the split.
    Found via: coverage pass 2026-11-03

  11. dezren39 commented on Nov 3, 2026

    @dezren39
    MemberAuthor

    Triage verdict: complete — closing with evidence (current main d3476bce7, 2026-11-03)

    Every phase of this contract has landed and every sub-issue is closed, so the epic's own closing rule is met. Verified fresh on the laptop clone (fast-forwarded to origin/main), not from the earlier packet snapshots.

    Phase Vehicle State Evidence at current main
    1 — the two open instances dezren39/nix #1023, #1021 closed 2026-10-28 nixos/home/pm-dashboard-backup.sh — unchecked cp -a copies now fail the run, prune_remote fails the run on a bad listing
    2 — expected-set manifest dezren39/nix #1025 (design #1024) closed 2026-10-28 EXPECTED_SET declared once at :307-330; verify compares the archive against it and a missing required member fails before upload
    3 — run record + freshness watchdog dezren39/nix #1026 closed 2026-10-28 .last-run.json on every exit path (:38, write_record :75-160); nixos/home/laptop-backups-watchdog.sh + laptop-backups.nix watchdog.* options/timer
    rule 2 — one failure ledger per run queue task 5072, PR #4058 (merged c3fda1c) done 2026-11-01 ordered per-step steps ledger; result/failing_step derived from required-step outcomes

    Tests re-run today on current main (all pass, the same suites the flake exposes as checks via lib/default.nix:1106-1123):

    • bash nixos/home/pm-dashboard-backup-record.test.sh → all passed
    • bash nixos/home/pm-dashboard-backup-manifest.test.sh → all passed
    • bash nixos/home/laptop-backups-watchdog.test.sh → all passed

    Acceptance criteria: phase 1 ✅, phase 2 ✅, phase 3 ✅, rule-4 (no undeclared required-step success) ✅ for the dashboard job — the result is now derived from the ledger and the expected set rather than from step-local || true decisions.

    One honest scope caveat, recorded rather than left implicit: repo-archive-backup is a restic job whose script (and credentials) live outside this repo at ~/tools/repo-archive-backup.sh; the watchdog covers it via timer last-trigger only, exactly as the design body and laptop-backups.nix:67 state. If Drew wants that script vendored under the same contract, that is one of the design doc's open questions — it needs its own issue, not a reopened epic. Nothing else under this epic is unfinished.

    🤖 Agent note · state: done (closed)

  12. removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Nov 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area/infraInfrastructure, deployment, and operationseffort/lLarge: multi-day or open-ended; expect staged check-insepicMeta-issue grouping multiple items (org Project Type=Epic)kind/designA design proposal (design-architect): multi-step, phased, decision-bearingpriority/normalOrdinary priority (the default)source/design-architectFiled by the repo design architect jobtask/triageQueue packet kind: verify, reproduce, and route this item

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions