Skip to content

Commit fb706db

Browse files
authored
docs: propose ordinary Home persistence pair recovery (#529)
1 parent ed14a77 commit fb706db

2 files changed

Lines changed: 236 additions & 0 deletions

File tree

Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
# ADR 0062: Recover Ordinary Home Persistence as One Durable Pair
2+
3+
- **Status:** Proposed
4+
- **Date:** 2026-09-06
5+
- **Decision owners:** David Irvine / human engineering review pending
6+
- **Reviewers:** Pending; Codex drafted from the #471 source proof
7+
- **Supersedes:** none
8+
- **Superseded by:** none
9+
- **Related:** [#471](https://github.com/saorsa-labs/x0x/issues/471),
10+
[ADR 0028](./0028-authenticated-causal-predecessor-delivery.md),
11+
[ADR 0038](./0038-home-owner-certified-personal-space.md),
12+
[TOOLING](./TOOLING.md)
13+
14+
## Context
15+
16+
At `e16ba97a9d65022251e67ecc17f6e23d4b75699e`, ordinary saves write
17+
`home-suite-groups.json` before `named_groups.json`. The former is the
18+
owner-certified state; the latter contains inert, legacy-decodable placeholders.
19+
A failed second write returns an unchanged/error outcome, permitting memory
20+
rollback while the sidecar retains the rejected Home mutation.
21+
22+
A local deterministic test of the actual saver and merged loader reproduced
23+
this: old memory was `home-x`, but reload returned `x-renamed-by-mutation`.
24+
Exactly one test failed at that assertion under denied outbound networking and
25+
a disposable test home. This is an I/O-fault/loader witness, not a power-loss
26+
or process-crash test. #471 remains open; this ADR is not implementation evidence.
27+
28+
Source anchors at that commit, all in `src/server/routes/named_groups.rs`:
29+
30+
- `persist_named_groups_mutation_unlocked`, lines 3778–3789: failure triggers
31+
per-key compare-and-restore, preserving concurrent writers (#470).
32+
- `save_named_groups_checked_unlocked`, lines 26954–27042: the split write;
33+
`AtomicWriteOutcome`, lines 27062 onward: single-file replacement facts.
34+
- `load_named_groups_merged`, line 24589: sidecar overrides placeholders.
35+
- `TreeKemNamedPersistJournal`, line 22110, and `HomeSuiteJournalTx`, line 23697:
36+
existing group/snapshot-bound journal formats, not generic map transactions.
37+
- `persist_treekem_and_named_groups_atomic_with_info`, line 22241, and
38+
`persist_named_group_info`, line 4044: existing transaction owners.
39+
- `src/server/mod.rs`, lines 612–621: paired TreeKEM recovery, orphan-sidecar
40+
cleanup, then merged load. TreeKEM recovery itself invokes the merged loader.
41+
42+
## Decision Drivers
43+
44+
- A rejected ordinary mutation must not reappear after a recoverable I/O fault.
45+
- Unknown replacement/durability must neither acknowledge success nor trigger
46+
rollback under a false claim that the named destination is unchanged.
47+
- Preserve pending-join exclusion, #470 CAS, owner-certified state, sidecar-first
48+
ordering, existing TreeKEM authority and frozen legacy journal bytes.
49+
- Resolve restart behavior explicitly rather than hiding it in error strings.
50+
51+
## Considered Options
52+
53+
1. Restore the sidecar in process, with a distinct recovery-required result and
54+
retained preimage. Smallest immediate repair; cannot survive process death
55+
during either the original split write or failed restoration. All callers
56+
must still handle uncertainty. This is a bounded interim option, not full #471.
57+
2. **Recommended:** a separate durable undo intent for ordinary paired saves,
58+
exclusive with existing TreeKEM transactions, with recovery before any merged
59+
load and success only after durable intent removal.
60+
3. Reverse the writes, copy best-effort rollback, or reuse `ReplacedNotDurable`
61+
for a partial pair. Rejected: these move the failure window, hide rollback
62+
failure, or contradict callers that rely on the new named file being visible.
63+
4. Extend/fabricate a TreeKEM journal or replace all journal formats. Rejected:
64+
ordinary saves have no honest snapshot envelope; legacy postcard decoding is
65+
frozen. A broader storage migration is unnecessary for this decision.
66+
67+
## Decision
68+
69+
### Ordinary transaction and commit point
70+
71+
Recommend one versioned ordinary-pair intent at a fixed, instance-local path
72+
separate from `.journal`/`.hsjournal`; old journal formats remain byte-identical.
73+
It records transaction identity, exact prior bytes or explicit prior absence of
74+
both files, and digests of prior/candidate bytes for validation and diagnostics.
75+
Bind the intent to the exact serialized candidate and captured preimage bytes,
76+
not a reconstructed current map or a group revision/hash. The mutation snapshot
77+
and save snapshot are separate today; this distinction matters for #470 writers.
78+
Paths are fixed by the store, never supplied by the intent. Checksums detect
79+
corruption, not hostile-owner tampering; ordinary OS file protection still applies.
80+
A missing file is a preimage only on NotFound; other read errors refuse preparation.
81+
82+
Under `named_groups_persistence_lock`, with transaction ownership established:
83+
84+
1. Resolve earlier transactions first; capture the pair preimages and the same
85+
filtered candidate used today (unconfirmed join stubs remain excluded).
86+
2. Durably write the intent before changing either destination. Preparation
87+
failure changes no destination; uncertain intent durability blocks a new
88+
transaction until the unchanged-pair intent is resolved.
89+
3. Replace the sidecar, then the legacy view, using the existing file-fsync,
90+
rename and parent-directory-fsync primitives. Both candidate files must be
91+
durable before the transaction may reach commit cleanup.
92+
4. Remove the intent and fsync its parent. **Successful completion of this fsync
93+
is the acknowledged commit point.** Only then return committed, publish
94+
dependent effects, or clear existing install/recovery markers.
95+
96+
Any ordinary failure before commit cleanup attempts to durably restore BOTH
97+
preimages, including removing files whose preimage was absence and fsyncing the
98+
removal. Keep the intent until restoration and its cleanup are durable.
99+
Recovery must not delete its sole evidence first. Repeating restoration is safe.
100+
101+
Intent removal with failed parent fsync is commit-ambiguous: both new files are
102+
already durable, but a crash can retain or lose the intent. Return recovery
103+
required, withhold all success/effects and block subsequent store transactions.
104+
In the same process, retry commit cleanup/parent fsync while retaining ownership;
105+
never start an unrecorded rollback after the intent has disappeared. After a
106+
crash, a retained valid intent deterministically selects the old pair; absence
107+
selects the completed new pair. Either is allowed for an unacknowledged operation;
108+
never report that ambiguous operation definitively aborted. A committed operation
109+
cannot later be undone by this intent because its removal was acknowledged durable.
110+
111+
### Results and recovery
112+
113+
Keep single-file `AtomicWriteOutcome` unchanged. Ordinary save callers need a
114+
separate result distinguishing **committed**, **aborted with old pair proven
115+
durable** (including failure before any replacement), and **recovery required**
116+
with stage/cause. The last state cannot be flattened into io::Error/NotReplaced
117+
or ReplacedNotDurable. It does not assert either whole pair matches memory.
118+
119+
A startup coordinator checks for an ordinary intent before running ANY existing
120+
journal routine that can load or write this pair. With an ordinary intent alone,
121+
validate it completely, restore both preimages, make both durable, then remove
122+
and durably clear it. Unknown versions, malformed/checksum-invalid data, uncertain
123+
file inspection or failed replay refuse startup with evidence retained. Recovery
124+
must be idempotent, including absence and interrupted cleanup. Only after this
125+
step may legacy paired recovery and orphan cleanup run, then merged state load.
126+
127+
While recovery is required in a running process, fence dependent persistence,
128+
causal replay/publication and mutation success. Resolve the original transaction,
129+
not a new serialization of whatever happens to be in memory. After proven abort,
130+
apply existing per-key CAS to the operation's before/after maps; concurrent updates
131+
must survive, and must receive their own persistence transaction. Reads must not
132+
present uncertain staged state as committed. The exact read/mutation fencing audit
133+
is an implementation prerequisite, not provided by the current durability flag.
134+
135+
### Composition: exactly one recovery owner
136+
137+
Ordinary transactions must not coexist with any pending legacy TreeKEM/HomeSuite
138+
journal that can read or modify this pair, including a retained journal for
139+
another group. Existing journals contain whole-map payloads, but current paired
140+
recovery validates and merges the target group; that safeguard must remain. Under the persistence lock, resolve existing journal work first;
141+
if it cannot be resolved, refuse a new ordinary transaction. Conversely, every
142+
TreeKEM transaction entry must refuse to start while an ordinary intent is pending.
143+
This is instance-wide exclusion, not just group-ID exclusion.
144+
145+
Existing seal/rebind transactions remain the authority for their own sidecar,
146+
named-view and snapshot writes. An internal explicit transaction context must
147+
let those paths use low-level pair writers without creating a nested ordinary
148+
intent. Journal preparation, replay and cleanup belong to that outer owner;
149+
ordinary helpers must not discard, supersede or reinterpret its journal.
150+
Recovery invokes low-level writers, never recursively starts another transaction.
151+
152+
If both journal families are found at startup, fail closed and preserve both;
153+
do not guess an order from timestamps or per-group revisions. This is an invalid
154+
state under the proposed exclusion rule, not permission to guess which recovery
155+
decision wins. Existing paired recovery/tag/fork rules otherwise remain unchanged.
156+
157+
### Human review and implementation gates
158+
159+
This recommendation is **not implementation-ready or Accepted** until review
160+
settles the following explicit boundaries:
161+
162+
- Approve abort-on-retained-intent versus commit-on-intent-absence for an
163+
unacknowledged operation, and how API/causal callers expose recovery-required.
164+
Existing callers at lines 3761/3778, 4032, 4062/4162, 7734, 20189 and 26719,
165+
and server/mod.rs 2776/2889/2960/3168/3259 need explicit handling.
166+
- Audit every pair writer, TreeKEM journal creator, recovery entry and mutation
167+
bypassing the persistence lock. Existing CAS protects against some unlocked
168+
writers; the proposed fence must not silently serialize their updates away.
169+
Mixed-journal states stay fenced until an explicit repair policy is reviewed.
170+
- Decide the exact intent encoding, size cap and version migration. Validate
171+
before mutation and fail closed on unsupported state; no fabricated snapshots.
172+
- Decide downgrade/rollback policy. Old binaries ignore a new intent and cannot
173+
honor its recovery fence. Managed rollback must resolve transactions before
174+
handing the directory to an old binary, or refuse that handoff. Arbitrary manual
175+
old-binary access during recovery is not made transaction-safe by this ADR.
176+
Inert placeholders/legacy decoding alone are not a crash-consistency guarantee.
177+
178+
## Consequences
179+
180+
### Positive
181+
182+
- Ordinary rejection and restart recovery use one explicit authoritative decision.
183+
- Single-file outcomes and existing cryptographic journal formats stay truthful.
184+
185+
### Negative / Trade-offs
186+
187+
- Extra durable writes and full-pair preimages increase I/O and temporary disk use.
188+
- An unresolved transaction fences the store; availability yields to consistency.
189+
- Caller integration and downgrade fencing are required work, not a local catch
190+
block. Existing ambiguous disk states cannot be repaired by inventing intent.
191+
192+
### Neutral / Operational
193+
194+
No membership, placement, encryption, network or Home election policy changes.
195+
No Accepted ADR is edited. #471 stays open until implementation and fault/restart
196+
acceptance; this proposal neither releases v0.41.4 nor implements supervision.
197+
198+
## Validation
199+
200+
Keep the exact baseline regression as a required failing negative control.
201+
After implementation, its old-memory/old-merged-disk/exact-preimage assertions
202+
must pass; a successful Home rename must survive a fresh merged load.
203+
204+
| Injected boundary | Required observation |
205+
|---|---|
206+
| Preimage read / intent create, write, file-fsync, rename, directory-fsync | No candidate destination written before durable intent; uncertain preparation remains fenced. |
207+
| Sidecar or named create/write/file-fsync/rename failure | Old pair durably restored or recovery-required; never false success/unchanged. |
208+
| Either destination directory-fsync failure | No commit acknowledgement; recovery retains a truthful stage. |
209+
| Undo write/rename/fsync; prior-absence remove/fsync | Evidence retained until old pair durable; restart repeats safely. |
210+
| Intent unlink failure | Durable candidate, pending cleanup; no success. |
211+
| Intent unlink succeeded, directory-fsync failed | Both candidate files durable; no success; test retained-intent and absent-intent crash outcomes. |
212+
| Process stops after each stage; recovery interrupted twice | Same selected pair after repeated recovery; no partial merged load. |
213+
| Concurrent same-key/cross-key update, pending join stub | #470 CAS preserves newer updates; no stub leakage or lost independent mutation. |
214+
| Existing rebind/seal journal, orphan sidecar half, mixed families | One owner; valid existing recovery succeeds; invalid coexistence fails before merged load or mutation, preserving evidence and avoiding snapshot/roster drift. |
215+
| Corrupt/unknown/oversize intent; absent preimages; downgrade attempt | Fail closed or exact absence recovery; no undocumented data loss/handoff. |
216+
217+
Exercise a later save after an ambiguous cleanup and restart twice: a stale
218+
preimage must never overwrite a later acknowledged save. Include an unrelated-key
219+
insert and same-key serde-skipped-field update between mutation snapshot, save
220+
snapshot and rollback. A torn ordinary candidate must not be inspected by legacy
221+
recovery and spuriously classify a valid rebind as stale/forked. Mixed-family
222+
crash images must not replay either side before the conflict is resolved.
223+
224+
Verify no success, relay, queue removal or marker clear precedes durable commit.
225+
Tests must call real persistence/recovery entry points with disposable roots and
226+
outbound network denied. Crash simulation and platform power-loss evidence must
227+
be labelled separately. Existing valid TreeKEM recovery, inert old-decoder
228+
placeholders and positive ordinary saves remain required controls.
229+
230+
## Notes for AI-assisted work
231+
232+
AI tools may draft this ADR, but must not mark it Accepted without human review.
233+
Accepted ADRs are immutable. Storage-format implementation waits for review under
234+
TOOLING; the unresolved boundaries above are not discretionary shortcuts.

docs/adr/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@ This directory contains architecture decision records for x0x.
7474

7575
- [ADR 0063: Signed KV legacy gossip compatibility adoption boundary](./0063-signed-kv-legacy-gossip-compatibility-adoption-boundary.md) — draft; V3 pairing preparation only, G0 met and G1–G8 open; disabled pending audited adoption
7676

77+
- [ADR 0062: Recover Ordinary Home Persistence as One Durable Pair](./0062-home-persistence-pair-recovery.md) (proposed 2026-09-06) — #471: ordinary-pair undo intent, truthful recovery-required results and exclusive journal ownership; commit ambiguity, caller fencing and downgrade policy require human design review before implementation.
78+
7779
## Errata (Accepted ADRs are immutable; corrections recorded here)
7880

7981
Documentation-audit corrections, 2026-07-19. The ADR files themselves are

0 commit comments

Comments
 (0)