# R-304 — the household's old recovery code and the retained packages: a one-page design (2026-10-08) **Status:** option C's first slice BUILT 2026-10-08 afternoon on the operator's ruling (09:04, `09` §3 decision 183) — see the end. Ships with the next controller + hub releases. Customer data and promises are the operator's: they are the two questions at the end. ## Where it stands (read in source today, not from the row) - **Retention works** and the material opens the old store (drill 2026-08-12, `audits/DRILL-retained-key-2026-08-12.md`). - **R-311 shipped** (hub v0.103.0, agent v0.129.0, controller v0.214.0): after the current package refuses a code, the agent fetches the retained packages (`GET /hosts//escrow/retained`, self-scoped, cap 16) and tries up to 6. If one opens, the screen says „the code is correct, it opens an earlier package; contact support" (HTTP 422). - **R-312 is DECIDED (2026-08-13): no in-product route from the recovery screen to a set-aside store** — „re-evaluate on a real customer request". So retention is an **operator-only** capability today, by decision. - **What was still false until today:** the agent answered „the code did not open the sealed bundle" (400) also when it had NOT tried every earlier package — the hub withheld rows (no key material, over the cap), a package was malformed, the 6-try cap stopped the loop, or the retained list could not be read. **Fixed on main today** (agent 424 `older_unchecked`, controller `RecoveryOlderUnchecked`, Hungarian + English: „we do not know whether your code is wrong … contact support"). Ships with tomorrow's releases. The fix is in the agent and the controller, not the hub: the hub never sees the code (zero-knowledge, `07` §2), so it cannot check a row; it already reports what it withheld (`unopenable_count`, `truncated_count`), and the agent now counts those. ## „Which package?" — the question is smaller than it looked Each escrow ceremony seals with a NEW recovery code (the household is shown it once). A code opens only the package it sealed. So when a household holds several old codes, **each code selects its own package** — no list, no choice screen. The agent already tries newest-superseded first. The only real limits are the two caps (16 served, 6 tried), which today's fix turns from a silent „wrong code" into an honest „not all checked". ## Options for really serving the old copy | | What | Costs | Customer data / promise | |---|---|---|---| | **A** | Keep it operator-only (R-312). The screen's „contact support" is the route. | Nothing more. The operator needs SQLite, `age` and a shell (the drill's §4) — slow, error-prone, undocumented as a runbook. | No change. | | **B** | In-product: when a retained package opens and carries a repository password, the screen offers a READ-ONLY browse of the old store (list + download), never a restore into place. | New surface: the old store's location (moved aside / orphaned, R-241), a second repository password in memory, a second browse path. ~2 sessions + a drill. | Changes a promise (the household can reach old history alone). **Reverses R-312** — operator only. | | **C** | Operator-assisted, first slice: when the agent answers 422 (opens retained) or 424 (not all checked), the controller sends ONE operator event naming the box and the package date; plus a runbook „open a retained package for a household" (the drill's §4 written down, the household types the code on its own box). | Small: one event type (operator-only), one runbook. ~½ session. | No new promise; makes the existing „contact support" true in practice. | **Pick: C now; B only on R-312's own trigger** (a real customer asks). C makes the sentence the screen already says — „contact support" — something the operator can act on the same day, without reversing a decision. **First slice of C:** controller: on `RecoveryCodeOpensRetained` / `RecoveryOlderUnchecked`, send `recovery_retained_needed` (operator-only, warning, once per box per day) with the package date and the class — never the code. Hub: allowlist + `operatorOnlyEvents` in the same commit. Docs: `runbooks/RUNBOOK-open-retained-package.md` from the drill's §4. ## Two questions for the operator 1. **May the product keep promising, in the capability map and the countdown banner, that old backups „stay recoverable"?** Today that is true only with your hands. *If you do nothing:* the promise stays worded as it is, and the honest route is „contact support" (A/C). 2. **Do you want C's first slice built (an operator mail when a household's code opens — or may open — an old package)?** *If you do nothing:* nothing is built; you learn of such a household only when they write to you. ## Built 2026-10-08 (afternoon) — option C, first slice - **Controller** (`a40729a`): on a 422 (opens an older package) or a 424 (may open one), `recovery_older_package` (warning) to the hub — the class and the package's date, never the code; at most once per day per box (`/recovery-older-mail.day`). `TestR304_OlderPackageMail_*`. - **Hub** (same day): the type is allowlisted and operator-only. `TestR304_RecoveryOlderPackageIsAllowlistedAndOperatorOnly`. - **Not built:** the runbook „open a retained package for a household" (the drill's §4 written down) — next slice. - **Question 1 (the promise wording) was not answered:** the wording stays.