docs: fix round 1 — corrected #250 reproduction, restored two rollout minors
Review round 1 (spec ✅, quality 4 Important): rollout item 11's #250 reproduction was the ledger's pre-correction phrasing (-grant/-backing bare); rewritten to the actual reproduction (public-addr instance route via -backing unit then -grant; any everywhere-class row incl. static via -tunnel, which has no kind rail at all). Restored two rollout-relevant minors dropped from the original sweep: Task 2's corrupt-mesh-key operator-delete-only recovery path (beside item 2), and Task 3's silent staleHopRefusal (beside item 14).
1 file changed,
+43
−9
docs/superpowers/plans/2026-08-22-multi-region-backhaul.md+43 −9
diff --git a/docs/superpowers/plans/2026-08-22-multi-region-backhaul.md b/docs/superpowers/plans/2026-08-22-multi-region-backhaul.md| index 44ff9d203..20c1ef82d 100644 |
| --- a/docs/superpowers/plans/2026-08-22-multi-region-backhaul.md |
| +++ b/docs/superpowers/plans/2026-08-22-multi-region-backhaul.md |
| @@ -518,7 +518,14 @@ plan's own spec Part 4 describes. |
| cadence) per Task 2's ledgered honest-header fix (`0c07490`) — during |
| that window some edges verify hops under the old key, some under |
| `.next`; dual-accept on both is what makes that safe, not a race to |
| - avoid. |
| + avoid. **A corrupt key is operator-delete only.** `mintMeshKey` is |
| + create-only (Task 2) — it never overwrites an existing |
| + `control/edge-mesh-key`, so a key that gets corrupted in the bucket |
| + (a bad manual edit, a truncated write) is never self-repaired by any |
| + box's `mintMeshKeyLoop`. Recovery is manual: delete the object; the |
| + next box to hold the housekeeper lease re-mints on its next tick. |
| + Worth knowing before touching that key by hand during an arming |
| + window, not just at a real rotation. |
| 3. **Exercise the backhaul path on dev/Kass before flagship AU.** |
| Flagship AU is the spec's designated first target because a steering |
| mistake there "burns nobody's ticket sales" (spec Part 1 decisions) — |
| @@ -674,13 +681,29 @@ plan's own spec Part 4 describes. |
| materialized on every platform box, and the removal phase reads the |
| class off the ROW (`everywhereRoute`), which deliberately answers |
| "home-boxed" for any row carrying `Backing`, `Tunnel` or `Caps` — a |
| - conservative choice that also means: mint a static record on a far |
| - box, `carlos route -grant`/`-backing`/`-tunnel` that far copy, delete |
| - the record — the home box retires its route, the far box keeps |
| - serving the retired site forever with its grants intact and no record |
| - anywhere to explain why. The gate stays conservative on purpose (the |
| - alternative hands an unattended bucket-record deletion the |
| - capability-grant purge's human-gated contract), so the fix is |
| + conservative choice, but it means such a row is never retired once |
| + touched. **Corrected reproduction** (the plain `-grant`/`-backing` |
| + phrasing this note originally carried does not hold: both rails |
| + refuse non-instance rows — `grantRail`, `cli.go:1197`, refuses |
| + anything but a unit-backed instance route; `backingRail`, `cli.go:1370`, |
| + refuses any non-instance kind outright — so neither reaches a static |
| + row directly): |
| + - a public-addr **instance** route (the Everywhere-probed class) on a |
| + far box: `carlos route -backing unit` succeeds (kind `instance`, |
| + not Platform), then `-grant <token>` succeeds (kind `instance`, |
| + `Backing == unit`) — both rails clear because the row really is an |
| + instance row, just a far-box copy of one; |
| + - **or any everywhere-class row at all, static included**, touched |
| + with `-tunnel <name>`: there is no `tunnelRail` — the tunnel axis |
| + (`cli.go:951-974`) checks only `platformCanaryOwnedRow`, no kind |
| + check exists, so a far box's static copy takes a tunnel stamp as |
| + readily as an instance row. |
| + Either path leaves `Backing`/`Tunnel` non-empty on the far copy, and |
| + deleting the record then retires the home box's route while the far |
| + box keeps serving the retired site forever, grants intact, with no |
| + record left anywhere to explain why. The gate stays conservative on |
| + purpose (the alternative hands an unattended bucket-record deletion |
| + the capability-grant purge's human-gated contract), so the fix is |
| visibility + a supported cleanup verb, not a gate change. Relevant to |
| this rollout because everywhere-class adoption (this plan) is what |
| makes far-box copies routine for the first time — #250 was inert |
| @@ -717,7 +740,18 @@ plan's own spec Part 4 describes. |
| dead box will produce one line per failed request rather than a |
| throttled cadence — expected noise during the arming window, not a |
| defect, but flagged so it isn't mistaken for something worse the |
| - first time it's seen live. |
| + first time it's seen live. **`staleHopRefusal` (`internal/edge/homemap.go:551`) |
| + is silent in the same neighbourhood, the other direction** — a |
| + sender stuck dialing a home box whose map has moved on gets its 503 |
| + answered correctly but nothing is LOGGED on either end, so a sender |
| + looping into that refusal (map propagation stuck, a box that never |
| + converged the move) produces zero signal that anything is wrong. |
| + Task 3's review named this the "converge is not healing this" case |
| + and wanted the same first-and-every-tenth cadence the mirror's |
| + partial-poll logging already uses elsewhere in this branch — not |
| + fixed here, but worth doing the next time anyone is in that file |
| + rather than left to be rediscovered blind during a real stuck-move |
| + incident. |
| **Full gate** (env `GOCACHE=/tmp/claude-1001/gocache GOFLAGS=-mod=mod |
| GOPROXY=off`): `go build ./...`, `go vet ./...`, `GOOS=darwin go build |