Skip to content

Design: a mirror is what it can prove — one validity, outcome and alarm contract for the github-backup estate mirror #3282

Open
5 of 9 issues completed

Description

@dezren39

Design-architect pass 29 (2026-10-31), audit at origin/main 558cc1d2. Full design doc: files/designs/code-github-backup-mirror-contract.md in the keep-the-four-repos-healthy goal workspace. NO user/review — every origin carries explicit acceptance with verified probe evidence, and the judgment calls below are the architect's recorded decisions.

Problem

infra/github-backup/ (umbrella #3215, landed today in PR #3214) mirrors Drew's whole GitHub estate — 818 bare mirrors across 17 owners — with a webhook freshness layer, a daily fetch, and a weekly reconcile it calls its correctness floor. In its first day it produced eight defect filings, and every one is a place where the system's claims about itself are not checked against the thing claimed: valid_mirror tests bareness only, so a zero-ref bare skeleton is recorded already-present and reported mirrored in the DR source of truth (#3238), while a non-bare partial destination wedges the "resumable" clone driver permanently (#3233); a fetch timeout raises out of the daily run unrecorded and unwritten (#3250), while the drainer acks webhook events for fetches that failed or mirrors that are missing, deleting the freshness signal (#3219); the chronic-failure alarm counts failure days since the last success instead of the docstring's "3 distinct days in a row" and latches (#3257), yet can never fire at all for a missing mirror (#3270); every generated-state file is written by truncation and read with a bare json.load, so one torn write wedges fetch, drainer and manifest until manual repair (#3240); and a null repositoryOwner is read as "owner has no repos", silently shrinking plan.json and MANIFEST.json with exit 0 (#3229). The sibling precedent is design #1854 ("a backup that can prove itself") for the laptop file-backup jobs — this is that contract's estate-mirror form, in this system's vocabulary; #1854's system is different and untouched.

The rules (recorded decisions)

  1. Validity is resolvability. One predicate in ghb_common.py for every consumer (clone skip, fetch missing, manifest status, reconcile missing): bare and HEAD resolves (rev-parse --verify -q HEAD — the call manifest.head_sha already makes). A planned repo is never legitimately empty — build_plan excludes zero-commit repos under the live config (skip_zero_commit_repos: true) — so no emptiness exception exists inside the plan; that settles github-backup(valid_mirror): a bare skeleton with zero refs passes as a verified mirror — clone_all records it ok and MANIFEST.json reports it 'mirrored' #3238's open either/or without an emptiness signal.
  2. Destinations have four states, each with a disposition. Absent → clone; valid → skip (never deleted/re-cloned); skeleton (bare, unresolvable HEAD — zero refs, zero objects by construction) → remove and clone (deletion provably loses nothing); debris (exists, not bare — may hold transferred objects) → move aside to <dest>.partial-<timestamp> and clone. Disposition recorded in clone-status.jsonl.
  3. Every attempt ends in a recorded outcome. fetch_repo never raises for a per-repo outcome — a timeout becomes a recorded fail naming the timeout (clone_all's existing precedent, generalised). One recorder path turns an outcome (ok/fail/missing) into state + fetch.log + summary, used by both the daily loop and the drainer.
  4. An event is acked only when its fetch landed. Drainer acks a planned repo's event only on recorded ok; on fail/missing the event stays in KV for the next drain and the drain_last:: stamp advances only on ok. Unplanned repos keep today's ack + note behaviour (reconcile owns creation).
  5. The alarm is a streak of unavailable days. One per-repo day record — fail, timeout-fail and missing all append today; success clears. Chronic = today completes 3 consecutive calendar days (the docstring pinned over github-backup(fetch): fail_days counts every failure day since the last success, not 3 days in a row — intermittent failures weeks apart trip the chronic alarm and latch exit 1 #3257's window alternative; github-backup(fetch): a missing mirror can never trip the chronic alarm — 'missing' returns before any fail_days record, so the daily run exits 0 forever #3270's immediate-vs-3-day choice settled to the same 3-day scale). Legacy fail_days lists are read as this record as-is: non-consecutive legacy days never form a streak, so latched false alarms clear on the first post-upgrade run unless a true streak exists.
  6. State is written atomically, loaded tolerantly, only through ghb_common. Temp file in the same directory + fsync + os.replace; last-good copy kept; a torn load falls back with a loud named warning, never a startup traceback. The two state.json writers (daily fetch, 5-minute drainer) read-modify-write under a shared flock lockfile — github-backup(state): plan.json, state.json and MANIFEST.json are written non-atomically and loaded with bare json.load — one torn write wedges the daily fetch, the drainer and manifest regeneration #3240's lost-update window is closed, not documented.
  7. A failed owner keeps its plan. Null repositoryOwner/repositories in a successful GraphQL response is an owner-level error via inventory()'s existing errors channel (present-but-empty still inventories as zero). Reconcile retains the failed owner's previously planned entries in plan.json, marks them retained in the report, and still exits non-zero — github-backup(inventory): a null repositoryOwner is silently read as 'this owner has no repos' — reconcile drops the owner's mirrors from plan.json and MANIFEST.json and exits 0 #3229's report-only alternative is pinned away: there is no drop to make unmistakable.

Phases (adopt the origins — no parallels)

Alternatives rejected (in the doc)

Fixing the eight independently (the defects are the divergences — that is how the family arose); the window rule for the alarm (the docstring is the operator contract systemd is judged against; the streak is also the less latch-prone reading); deleting debris outright (its objects may be the only copy mid-estate-clone; quarantine is cheap); fetching into the skeleton in place (re-derives clone --mirror's remote/refspec setup in a second place; removal is lossless by the state's definition); a separate watchdog for missing mirrors (the fetch exit code is this system's stated alarm channel — #1854's watchdog belongs to a system without one); report-only plan shrinkage (MANIFEST is regenerated from the plan, so a shrunken plan falsifies the DR record for mirrors that exist).

Risks (in the doc)

Phase 1 recounts skeletons as missing on the first post-landing reconcile/fetch while #3215's initial 818-mirror clone is still in flight — the correction working, but triage should announce the one-time recount on #3215. Ack retention grows the Worker KV queue for persistently failing repos (bounded by coalescing + the rule-5 alarm). Plan retention keeps a vanished owner's repos planned while it errors — loudly (weekly report marks them, exit stays 1), resolved via config exclusions/gaps where owner rulings already live.

Open questions (in the doc, never as issues)

A third MANIFEST status for the skeleton state vs folding into missing; the 3600s fetch timeout against the drainer's 5-minute cadence (decide on Phase 2's timeout records); state.json schema versioning; Worker-side dead-lettering; FAMILIES.md / reconcile-report atomicity (Phase 3's audit dispositions them explicitly).

Acceptance criteria

Activity

  1. added
    priority/normalOrdinary priority (the default)
    epicMeta-issue grouping multiple items (org Project Type=Epic)
    kind/designA design proposal (design-architect): multi-step, phased, decision-bearing
    app/github-backupinfra/github-backup estate mirror system
    on Nov 1, 2026
  2. added sub-issues on Nov 1, 2026
  3. 2 remaining items

  4. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    Agent note (program tending, 2026-11-01): child progress — sub-issues #3229 (github-backup inventory: null repositoryOwner silently misread) CLOSED 2026-11-01T01:59:58Z and #3219 (github-backup drain: webhook events acked on failed fetch) CLOSED 2026-11-01T02:00:05Z (parent link REST-verified this run). This epic's last comment (2026-11-01T01:41Z) predates both closures, so the landings are recorded here. Progress note only — other children remain open; not a completion proposal.

  5. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    Program tending — sub-issue progress (2026-11-02)

    Closed since this thread's last activity: #3257 (closed 2026-11-02T11:10Z), #3233 (closed 2026-11-02T14:04Z), #3250 (closed 2026-11-02T14:24Z).

    Recorded for the parent rollup; this is a progress note, not a completion proposal — the parent stays open on its remaining children.

  6. added
    queue/queuedHas a queued task in the Supabase agent queue
    task/triageQueue packet kind: verify, reproduce, and route this item
    on Nov 3, 2026
  7. developing-today commented on Nov 3, 2026

    @developing-today
    Contributor

    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

  8. 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 3, 2026
  9. dezren39 commented on Nov 3, 2026

    @dezren39
    MemberAuthor

    agent-note:v1
    state: picked_up
    task: task 8225 (triage, repo-health-code-pilot, lane code-l, direct execution lane)
    worker: repo-worker-code
    issue: #3282

    Triaging design #3282 at current main: verifying each phase/origin issue's fresh state, recording blockers and ordering on the issue, and routing the design's remaining work. No hard-stop labels present (no held / user/owned / do-not-merge / do-not-auto-edit).

  10. dezren39 commented on Nov 3, 2026

    @dezren39
    MemberAuthor

    agent-note:v1
    state: triaged
    task: task 8225 (triage, repo-health-code-pilot, lane code-l)
    worker: repo-worker-code
    issue: #3282

    Triage findings — design #3282 at current main (fresh check 2026-11-03)

    Verdict: still live, not closable. 5 of the 8 adopted origin issues are closed with merged fixes; the remaining 3 are already in the queue system with named blockers. No new fix is scoped from this triage — the design's phases and ordering stand as filed.

    Phase status (verified via fresh gh reads this run)

    Ordering / blockers (as recorded)

    1. Unblock github-backup(valid_mirror): a bare skeleton with zero refs passes as a verified mirror — clone_all records it ok and MANIFEST.json reports it 'mirrored' #3238's queue-row reconciliation (task 5684 vs the task 5685 routing) → Phase 1 fix lands → Phase 1 complete.
    2. task 6813 fixes github-backup(fetch): a missing mirror can never trip the chronic alarm — 'missing' returns before any fail_days record, so the daily run exits 0 forever #3270 → Phase 2 complete.
    3. Only then does github-backup(state): plan.json, state.json and MANIFEST.json are written non-atomically and loaded with bare json.load — one torn write wedges the daily fetch, the drainer and manifest regeneration #3240 (task 5683) un-gate, followed by the Phase 3 (audit) of #3282 — closing state-access inventory + contract statement for infra/github-backup #3283 closing audit (helper adoption inventory + contract statement in README/RUNBOOK, evidence on umbrella github-backup: estate mirror system (laptop mirrors + dedicated App + Worker webhooks + weekly sweep) #3215).

    Acceptance boxes on this design stay unchecked until each phase's origins close with their filed acceptance met, per the criteria above. Umbrella #3215 remains open and in progress independently.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

app/github-backupinfra/github-backup estate mirror systemepicMeta-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