authorclover caruso <git@paperclover.net> 2026-10-11 12:35:45-07:00
committerclover caruso <git@paperclover.net> 2026-10-11 12:39:01-07:00
log2b4c678b0cf76c937249714ed7a461c613d34dc3
treea6d90ec168c67b576c2c0590ff6e0d7af809c24d
parent2e662d1cf6a7d5f234931627efa27a95cff70bb0
signature Signed by SSH key SHA256:52mNGHRsVFBDED9IAX5pe+LRWUefqTbxEReunq21QvU (~clover)

docs: investigate multi-file cloud coordination

Specify preferred-publisher coordination, immutable recoverable histories, causal requests, receipts, protected payloads and namespace recovery. Record actual store/replica experiments and distinguish logical ownership from physical provider upload fencing. Link the separate protocol from sync notes. Refs #120 Assisted-by: gpt-6 Assisted-by: gpt-6.1-sol

2 files changed, 738 insertions(+), 0 deletions(-)

arc/multi-file-sync.md created+735
...@@ -0,0 +1,735 @@
1# Coordinating cloud edits through separate files
2
3**Draft specification and investigation, 2026-10-11.**
4[#120](https://shale.paperclover.net/snowbound/issues/120). This is separate from
5shipping Android. Nothing in this document implements or enables a new storage
6mode. Existing local/native and direct SMB publication retain their existing
7OneNote-compatible exclusion. The wire examples and ownership policies below
8are proposals for review, not frozen public APIs.
9
10Clover's direction is a notebook's `.snowbound/` directory carrying coordination
11between Snowbound clients, with one owner process publishing the real `.one`
12files. Other builds exchange separate files instead of racing on those files.
13Generic cloud mounts cannot be assumed to implement reliable distributed locks.
14This should cover Android/Linux provider-backed notebooks and improve iCloud
15coordination. Practical tests must determine the tradeoffs before selecting an
16ownership/recovery policy.
17
18## The contract under investigation
19
20```text
21Android / Linux / iOS editor
22 │ existing edits, locally durable SQLite queue
23 ▼
24.snowbound/ unique immutable request segments + blobs
25 │ validated complete bytes and causal dependencies
26 ▼
27owner existing merge and revision append
28 ├── immutable decision/checkpoint evidence ──► other Snowbound replicas
29 └── ordinary .one / .onetoc2 files ─────────► eventual provider projection
30```
31
32A single application publisher reduces ordinary write/write contention. It
33cannot prevent a provider from delivering an old queued upload after ownership
34has moved. A successful local flush, a successful handoff, and a unique current
35logical owner are three different facts. The design must retain enough immutable
36evidence to repair the mutable file after a rollback even if every client has
37already received a historical publication receipt.
38
39Proposed safety requirements:
40
41- An acknowledged local edit remains in retained local storage until its content
42 and outcome are recoverable elsewhere under an explicit retention contract.
43- Seeing a request, seeing a receipt, or seeing a matching file stamp does not
44 authorize deletion of the only recoverable request/base/blob.
45- Missing, stale, incomplete, repeated, or reordered provider observations never
46 silently delete notes, select an owner, retire a branch, or establish a
47 complete list of devices.
48- Every accepted request has a stable identity, complete validated payload,
49 known causal base, and a recorded disposition. Retried transport does not
50 repeat the user edit or create duplicate conflict pages.
51- Normal edits use existing ops and append normal revisions. A projection is
52 never rebuilt by diffing or recreating a page model.
53- A dependency that has not arrived means wait. Concurrent incompatible content
54 means the existing merge produces conflict pages or a preserved recovery
55 branch. A missing dependency is not a content conflict.
56- Locked protected content, unavailable providers, and absent owners retain
57 encrypted pending edits. No network condition turns a protected edit into a
58 cleartext file.
59- Guarantees identify their observation point: local durability, owner custody,
60 file publication, and provider/cloud visibility are distinct.
61
62These requirements depend on at least one retained complete copy of each
63necessary object eventually being delivered. No file protocol preserves data
64when every durable copy is destroyed, or proves delivery when the provider
65permanently withholds it. Eventual projection repair additionally requires that
66old uploads eventually stop, required objects are eventually delivered, a
67capable reconciler keeps running, and necessary keys/versions remain available.
68An unbounded stream of stale overwrites can keep the canonical path regressing
69forever. Retained history preserves recoverability; it does not make that path
70monotonic under an adversarial delivery schedule.
71
72## Storage model and observations
73
74Assume independent client views of one provider-backed directory tree. Reads and
75listings may be stale; files may become visible before their bytes are complete;
76notifications and uploads may reorder; a complete old `.one` image may replace a
77newer one. A provider may omit known children, return unknown metadata, rename a
78file while changing its opaque handle, or reject a nominally supported operation.
79Clients can remain offline indefinitely. Process clocks are unrelated. A process
80can stop at any instruction and later resume with old state.
81
82There is no assumed atomic directory rename, distributed exclusive create,
83compare-and-swap, reliable distributed advisory lock, complete listing, or
84multi-file transaction. Independent objects with unguessably unique identities
85reduce collisions; they do not turn a provider's filename API into exclusive
86creation. Object integrity is checked after reading complete bytes. Local
87app-private SQLite ownership is a different boundary and can still serialize
88processes sharing that same local replica.
89
90Android is one transport, not the definition of the protocol. SAF `rw` opens
91provide seeking when supported; that says nothing about remote exclusion or
92upload ordering. Provider handles/IDs remain opaque, and returned rename handles
93must be retained. iCloud coordination/versions remain useful host capabilities,
94but they do not establish distributed fencing for the generic mode.
95[Android ContentResolver](https://developer.android.com/reference/android/content/ContentResolver),
96[DocumentsContract](https://developer.android.com/reference/android/provider/DocumentsContract).
97
98A provider that cannot retain separately named byte objects, enumerate them
99again, or represent the selected notebook subtree cannot carry this transport
100without another adapter. Unsupported capabilities must be observed in disposable
101provider fixtures; a provider name is not evidence of support.
102
103## Ownership choices and their actual guarantees
104
105| Candidate | Progress while original owner is absent | Logical ownership | Physical `.one` protection |
106| --- | --- | --- | --- |
107| Fixed designated publisher | Other clients edit/queue; publication waits | One configured owner under non-duplicated local state | No cross-provider upload fencing; late old uploads still require repair |
108| Explicit handoff | New owner proceeds after validated handoff dependencies | Old process durably stops admitting/publishing; successor receives state | Provider may still deliver old process's earlier uploads |
109| Timeout/heartbeat takeover | Available to isolated clients | Stale views can authorize two writers | Unsafe as a strict single-writer rule |
110| Fixed-member majority consensus | Majority can proceed; minority queues | Possible with durable acceptor promises and valid membership changes | Consensus does not make a generic provider reject stale uploads |
111| Branch-preserving recovery takeover | Isolated clients can preserve new branches | Cannot promise one global writer/order without additional agreement | Mutable file remains an eventually repairable projection |
112| Fenced server/provider | Depends on service availability | Service validates owner epoch for each accepted write | Can reject stale owner writes when checks cover the actual storage destination |
113
114No recommendation here may describe a wall-clock lease as sufficient fencing.
115Heartbeat objects are discovery/liveness hints only. A process that sees no other
116owner cannot distinguish an absent owner from a partition or stale listing.
117
118Majority agreement is a real alternative, not impossible consensus: fixed
119acceptors can persist promises, choose one logical history, and recover after
120restart. Its availability cost matters for a phone and one laptop. With two
121members, a majority requires both. Membership changes and acceptor-state loss
122need a complete protocol; a directory full of claims or votes is not one.
123[Paxos Made Simple](https://www.microsoft.com/en-us/research/uploads/prod/2016/12/paxos-simple-Copy.pdf).
124
125Fencing must be checked by the resource receiving the write. A local owner epoch
126inside `.snowbound` cannot force a generic mount to reject a queued older `.one`
127upload; the provider does not interpret that epoch. Chubby's sequencers address
128precisely delayed operations by requiring validation at the recipient.
129[The Chubby lock service](https://research.google.com/archive/chubby-osdi06.pdf).
130
131**Investigation recommendation, not yet selected:** start with a fixed preferred
132publisher, explicit handoff, and immutable recoverable inputs/outcomes. Other
133clients keep editing from retained local replicas while that publisher is absent;
134publication waits. Preserve branches as the recovery fallback, and treat automatic
135takeover as separate policy work. Timeout takeover is not part of the strict
136owner contract. Majority agreement remains an alternative with its stated
137availability cost. This makes the sidecar necessary recovery authority for
138accepted history when the mutable projection regresses; that authority/backup
139contract needs owner review before production implementation. The recorded
140experiments below support the recovery requirement, not a finished owner protocol.
141
142## Identities and duplicate processes
143
144Notebook identity must be independent of mount path, provider document ID,
145display name, host registration, and filename case normalization. Existing
146OneNote file/root identities remain the content identities. A new coordination
147notebook identity distinguishes separate imported copies and ownership histories;
148how deliberate notebook copies inherit or reset this identity remains a
149migration decision, not an inference from their path.
150
151Use distinct roles:
152
153| Identity | Lifetime / purpose |
154| --- | --- |
155| Local installation/device identity | Retained private registration and diagnosis; not sufficient owner authorization |
156| Process/writer incarnation | Fresh random identity for each process run; disjoint message namespace |
157| Local edit identity | Existing durable replica edit ID plus its originating replica/writer identity |
158| Request segment identity | Stable once sealed, retries resend identical bytes |
159| Owner epoch identity | Chained ownership decision, not a timestamp or largest visible number |
160| Decision identity | Immutable owner result naming its predecessor and accepted request identities |
161| Blob/checkpoint identity | Validated immutable bytes, content digest and exact byte length |
162
163Previously sealed/outstanding records keep their original request, edit and
164segment identities after every restart. A fresh process incarnation names only
165new records. Retrying an old outbox may write its original namespace/bytes; it
166does not rekey old edits into the new process namespace. Otherwise deduplication
167would turn an ordinary restart into a second user action.
168
169Two processes on one installation must not both publish because they share a
170device ID. A locally owned publisher database/socket may serialize processes
171sharing reliable private storage. A second process with a copied private profile
172on another device is a different failure: distinguish incarnations, preserve
173both claims, and stop automatic publication when contradictory ownership becomes
174visible. Absence of a contradiction in a stale view is not a uniqueness proof.
175A backup of owner credentials is not permission to run both copies.
176
177For fixed ownership, restart uses the retained durable owner state and a fresh
178process incarnation. For explicit handoff, the old state records retirement
179before a successor becomes eligible. Losing the old durable state requires
180recovery; creating a fresh claim is not a transparent restart.
181
182## Proposed object namespace
183
184Names illustrate roles; this is not a frozen layout or extension:
185
186```text
187Notebook/
188 Notes.one
189 Notebook.onetoc2
190 .snowbound/
191 protocol-<notebook-id>-<object-id>
192 writers/<incarnation>/segments/<bucket>/<segment-id>
193 owners/<epoch-id>/decisions/<bucket>/<decision-id>
194 handoffs/<handoff-id>
195 blobs/<digest-prefix>/<blob-id>
196 checkpoints/<checkpoint-id>
197 recovery/<recovery-id>
198```
199
200Every published object is immutable. A transport retry keeps its identity and
201bytes. Correcting an object creates a distinct object referencing what it
202supersedes. Mutable convenience indexes or heartbeats may accelerate discovery
203but cannot authorize publication, retirement, or data loss. No implementation
204may overwrite one shared `owner.json` or `inbox.json` as the coordination truth.
205
206Per-process directories avoid ordinary client/client name collisions. Unique
207logical object IDs also appear inside the envelope. Two provider documents with
208one display name are not automatically one object; the same object under two
209names/handles is one logical object only after matching its validated identity
210and bytes. Different bodies claiming one immutable identity are a contradiction
211and are preserved/quarantined. This cannot retroactively undo side effects that
212already occurred before the contradiction became visible.
213
214No one file per keystroke: transport sealed batches from the existing queue into
215segments bounded by encoded bytes and editing bursts. The durable local receipt
216remains immediate; the provider upload can coalesce multiple edits. Individual
217edit IDs remain present for receipts/dedup. Attachments and large bodies are
218separate immutable blobs; transport does not repeatedly embed them in every
219segment. Exact segmentation thresholds require measurements, not a public
220extension promise.
221
222## Framing, validation, and wire examples
223
224Before accepting an object, check magic/version, bounded envelope/body lengths,
225notebook identity, declared kind, complete record digest, sender/object identity,
226and all required dependencies. Integrity covers the entire encoded envelope and
227body, including identities, owner epoch, predecessor/base/dependency references,
228kind and version. A body-only digest permits metadata mixups and is insufficient.
229A trailing digest or excluded self-digest field must have an unambiguous encoding
230rule. Digests prove byte consistency, not permission to edit. If access/authentication is added, it covers the envelope and dependencies
231as well as payload bytes. Provider ACLs and section passwords remain separate.
232
233Do not trust size/mtime metadata as completeness. A parseable JSON prefix or an
234apparently complete `.one` header is not enough; an object references its exact
235complete body length/digest. A readiness marker may arrive before the body and
236merely adds a dependency to wait for. Atomic rename is unnecessary for acceptance
237only if incomplete objects are rejected and complete objects stay immutable.
238
239Illustrative request envelope (symbolic IDs/digests, not executable wire data):
240
241```json
242{
243 "protocol": 1,
244 "kind": "edit-segment",
245 "notebook": "N",
246 "id": "writer-R/segment-S",
247 "writer": "R",
248 "predecessor": "writer-R/segment-before-S",
249 "base": {"section": "OneNote-file-identity", "checkpoint": "C", "decisions": ["D"]},
250 "edits": [{"replica": "Q", "id": 42, "author": "Alice", "opsObject": "O"}],
251 "dependencies": ["C", "D", "O", "blob-B"],
252 "body": {"length": 1234, "sha256": "complete-body-digest"},
253 "recordDigest": "digest-of-envelope-and-body-excluding-this-field"
254}
255```
256
257Illustrative decision and publication receipt:
258
259```json
260{
261 "protocol": 1,
262 "kind": "owner-decision",
263 "id": "D-next",
264 "ownerEpoch": "E",
265 "predecessor": "D",
266 "accepted": ["writer-R/segment-S"],
267 "dispositions": [{"replica": "Q", "id": 42, "result": "merged-or-conflict-retained"}],
268 "section": "OneNote-file-identity",
269 "baseCheckpoint": "C",
270 "resultCheckpoint": "C-next",
271 "revisions": {"page-space": "sealed-revision-identity"},
272 "dependencies": ["D", "C", "C-next", "writer-R/segment-S"],
273 "recordDigest": "digest-of-envelope-and-body-excluding-this-field"
274}
275```
276
277```json
278{
279 "protocol": 1,
280 "kind": "projection-receipt",
281 "id": "P",
282 "decision": "D-next",
283 "section": "OneNote-file-identity",
284 "observed": {"header": "OneNote-stamp-header", "length": 5678},
285 "checkpoint": "C-next",
286 "guarantee": "owner-local-file-confirmed",
287 "recordDigest": "digest-of-envelope-and-body-excluding-this-field"
288}
289```
290
291The examples omit separate payload bytes. `recordDigest` is symbolic: the codec
292must define exact bytes and self-field exclusion before implementation. Every
293object kind requires the same metadata integrity, not only edit segments.
294
295Version 1 is illustrative. The actual serialization must reuse existing
296`onestore::op::Edit`/`Op` semantics, with an explicit compatibility boundary
297around them. Existing serde representation is not independently promised stable
298forever. Do not invent a second page-diff language or CRDT. Live Share's bounded
299op batch plus named revisions demonstrates an existing reusable approach; its
300network membership/timeout assumptions do not transfer automatically to cloud
301files.
302
303## Causal bases, merges, and replay identity
304
305A segment names its exact input base and prior writer segment. A client editing
306without the owner continues its own local causal chain. The owner waits for a
307complete base/checkpoint, referenced blobs, and required predecessor decisions
308before interpreting positional text ops. An old base is not permission to apply
309UTF-16 offsets directly to a changed page.
310
311Reuse the current queue/merge machinery: `PendingEdit` carries author and ops;
312`Replica` keeps its base, seals batches, rebases against changed remote state,
313and makes conflict pages for incompatible operations. Existing
314`resolve::merge` discovers common revision ancestry and deterministic merged
315revision identities for retained competing section versions. Where no shared
316valid ancestry exists, preserve the foreign/unreadable branch for recovery.
317
318Deduplication is two related checks:
319
3201. Request identity and encoded content digest identify a transport retry.
3212. Stored named revision identities/outcomes identify work already incorporated
322 into a section/checkpoint, including after owner restart or request replay.
323
324A numeric edit ID alone is local to one replica. A file stamp alone does not
325prove every request's effect is present. A page's visible text matching expected
326text also does not prove correct history, attachments, or membership outcome.
327
328An unmergeable request or section branch remains retained with its original
329request/base/blobs and exact error. Custody acknowledgement of
330`retained-unresolved` is distinct from semantic acceptance and from file
331publication. It cannot appear in an accepted edit list as though the note had
332merged. A recoverable conflict outcome names the actual stored conflict page;
333a retained foreign/unreadable branch names recovery evidence instead.
334
335Owner decisions impose a durable selected order under the chosen ownership
336policy. Concurrent branches remain explicit inputs to the existing merge; do
337not choose the newest wall clock or discard the branch with the smaller owner
338ID. An automatic recovery policy needs a stable, documented reconciliation of
339multiple accepted decision histories. Deterministic sorting for processing is
340not a consensus or permission rule.
341
342## Owner state and write/ack ordering
343
344Proposed durable states, persisted before their corresponding externally
345observable promise:
346
347| State | Required evidence | Permitted action |
348| --- | --- | --- |
349| Discovered | Candidate object handles | Read/validate; no ack |
350| Complete | Exact body validated | Wait for dependencies or stage locally |
351| Ready | Causal base/blobs/ownership validated | Prepare existing ops/merge |
352| Retained unresolved | Original request/base/blob closure and exact failure retained | Report custody only; preserve for later merge/review |
353| Prepared | Stable request disposition, named revisions, reconstructible input/base retained | Attempt normal append |
354| Attempt uncertain | Append/header/flush result ambiguous | Inspect/confirm exact evidence; never blindly reapply |
355| File confirmed locally | Named revisions/stamp confirmed at owner's actual adapter boundary | Record historical local publication evidence |
356| Recoverable result | Immutable input/decision/checkpoint closure available under custody contract | Publish decision/result receipt |
357| Receipted | Request identity maps to durable outcome and recovery closure | Client may advance display/status; retention rules still apply |
358
359The exact linearization between durable prepared decision, file append, and
360immutable result publication needs implementation tests. The invariant is
361stronger than a particular file ordering: no externally acknowledged request
362may depend solely on mutable `.one` bytes or a volatile owner map. A crash at any
363point must leave either an unacknowledged retained request or a recoverable
364recorded outcome. Checkpoints are raw section bytes/named append artifacts,
365not projected page models.
366
367An owner can prepare and retain a reproducible named transaction before an
368append; after an ambiguous append it uses revision evidence and existing
369`CommitState`/confirmation logic. It does not regenerate new revision IDs and
370retry the user action. Appending successfully and crashing before writing the
371receipt must converge to the same outcome when the original request returns.
372
373If the provider exposes the receipt before its referenced checkpoint/decision,
374the reader waits. If the `.one` file is behind the receipt, the reader obtains
375or reconstructs the referenced section from its recovery closure; it does not
376claim the mutable provider path already holds it. If the file is ahead, it can
377read validated revisions while separately resolving incomplete decision history.
378
379Once a client has validated a selected decision/checkpoint, a stale bare `.one`
380observation must not downgrade its known accepted history. Keep that history
381and recovery dependencies locally while repairing the projection. An explicit
382intentional restore is a new recorded operation, not a stale-file inference.
383
384An owner-unavailable client still edits from its private replica. Requests wait
385for owner processing without blocking local read/navigation/Undo. Publishing
386status cannot report owner custody merely because local creation of a provider
387object succeeded.
388
389## Explicit handoff, recovery takeover, and late uploads
390
391Proposed explicit handoff sequence:
392
3931. Old owner stops admitting new work for the epoch and persists that stop in
394 its retained local owner state. It retains requests arriving later.
3952. Seal/record admitted decisions and their complete recovery closure; publish
396 an immutable handoff naming successor, last decision, checkpoints and pending
397 uncertainty. No time-based claim of a complete device/request census.
3983. Successor validates the chain, retrieves every required dependency, resolves
399 uncertain attempts, and records acceptance in its own durable state.
4004. Successor publishes only under the selected handoff policy. Old owner never
401 resumes that epoch after a restart; a failed handoff remains recoverable.
4025. Both retain historical recovery evidence, because old provider uploads can
403 arrive after all application steps have completed.
404
405A receipt from the successor is useful evidence for operator recovery; it does
406not prove every cloud cache drained. Waiting an arbitrary duration is not a
407fence. A fixed owner with no ownership transitions avoids one source of late
408writes but still needs rollback repair for reordered uploads from its own run.
409
410Automatic recovery experimentation must keep each candidate's decisions in its
411own immutable branch. A candidate cannot overwrite another owner's mailbox or
412checkpoint, and claims no exclusive owner authority based on silence. The
413mutable `.one` projection may temporarily regress or produce provider conflict
414versions. A later reconciler imports all valid branches, preserves incompatible
415results as conflict pages/recovery data, and republishes normal revisions.
416This is a weaker, explicit safety/availability trade than a strict global
417single publisher, and remains unselected pending the experiments.
418
419A quorum alternative can choose an ownership/decision chain if acceptor
420membership and durable promises are implemented. It still needs immutable
421recovery and projection repair unless the actual provider enforces epochs on
422writes. Do not add a quorum merely to hide this physical fencing limitation.
423
424## Authority and disaster recovery
425
426The `.one` files remain the interoperable materialized section format. The
427sidecar must preserve accepted history beyond those mutable files if it is to
428repair demonstrated rollbacks. This is a proposed recovery requirement, not a
429silent declaration that an arbitrary `.snowbound` folder always overrides every
430`.one` file found beside it.
431
432Distinguish three recovery operations:
433
434| Operation | Meaning |
435| --- | --- |
436| Open a registered coordinated notebook | Reconcile validated selected history/retained branches with its section identities and observed projection |
437| Import a bare `.one`/notebook copy | Treat validated bytes as an import base; no historical receipts or missing sidecar authority inferred |
438| Restore a coordinated notebook backup | Restore a consistent closure or explicitly import/reconcile incomplete histories; never reset edit IDs/epochs silently |
439
440Durable custody is always qualified by its adapter: a private SQLite transaction
441is locally retained; a provider close is not a cloud durability receipt. A client
442must not evict its only outbox because the owner merely closed a provider file.
443Cross-device verified copies or an explicit retained owner-private closure have
444their own failure domains, which the recovery contract must name.
445
446A portable backup of a coordinated notebook needs the section bytes plus every
447retained immutable object required by accepted decisions and pending requests.
448Copying only `.one` is a valid interoperable snapshot, but not proof that
449already receipted cloud edits are recoverable. Copying only `.snowbound` may lack
450the seed images or external payloads needed for reconstruction.
451
452The mode must define when a deliberately restored older `.one` is an intentional
453user restore versus provider rollback. Timestamps cannot distinguish them.
454A deliberate restore needs a new explicit operation/epoch/disposition preserving
455the previous accepted branch, rather than deleting its evidence. Exact copy and
456restore UI semantics remain owner-facing design work after evidence; no current
457build is changed by this draft.
458
459## External writers, replacement, and repair
460
461On each projection change validate store/header/checksums, section root/file
462identity, and revision ancestry. The owner compares the actual base stamp at the
463moment of normal publication using available adapter guards; coordination mode
464never falls back by silently dropping an existing lock.
465
466A matching identity with extra external revisions is an import/merge candidate.
467The existing tolerant reader and retained-version merge can preserve content.
468Record the imported exact base/version so later requests have stable ancestry.
469A different section at the same filename is foreign content, not a new base for
470old queued ops. Preserve it and stop automatic destructive reconciliation.
471Unreadable/partially repaired images do not replace the last validated base.
472A repair must record the chosen input and retained rejected alternatives.
473
474Concurrent OneNote edits on a generic cloud mount bypass the coordination
475protocol. This mode cannot guarantee safe live interchange with such writers.
476Interoperable native filesystem/direct SMB mode remains the route for OneNote
4772010 side-by-side editing. A user opening a resulting `.one` offline still gets
478a normal OneNote file; that does not make its cloud upload fenced.
479
480On iCloud, continue reading unresolved `NSFileVersion` conflicts as recovery
481inputs. Multi-file coordination can reduce ordinary competing application
482writes and give them retained request history; it cannot promise the operating
483system will create zero conflict versions, nor retire those versions before
484complete recoverability is established.
485
486## Namespace, TOC, moves, deletes, and Undo
487
488A notebook has multiple files: section groups, `.onetoc2`, recycle-bin sections,
489external payload folders, and section files. A single-section protocol is not
490a notebook protocol. Namespace operations are owner-processed intentions named
491by stable section/page identities and expected causal state; paths are results.
492Only the publisher changes shared canonical namespace/TOC in this mode.
493
494Create/rename/move/delete outcomes reference all files and object identities
495they changed. Provider creation and rename return actual handles; do not infer
496success from a requested filename or overwrite an unrelated same-name item.
497A provider may expose TOC before a new section or source deletion before a
498moved section arrives. Consumers wait for the decision's full closure and keep
499last valid local catalog state.
500
501Multi-file changes use resumable intent records, not fictitious atomicity.
502For page/section moves, establish destination durability/recoverability before
503retiring the source, validate expected source stamp and queued generation under
504actual writer coordination, and make retries identity-stable. Without physical
505transaction/fencing, prefer a preserved duplicate to loss. Do not mix this
506unsettled transport implementation with the separately investigated
507cross-notebook move work.
508
509Delete is a durable tombstone/outcome, never merely disappearance in a listing.
510Retain the historical deleted branch or recycle-bin closure required by Undo
511and offline rejoin. An old already-accepted create must not recreate a deleted
512object when replayed. A genuinely unseen edit based before deletion receives an
513explicit merge/conflict/recovery disposition; its content is never dropped by a
514membership timeout. Undo is a new conditional intention against actual latest
515content, not blindly replaying an old whole-image inverse over later edits.
516
517## Protected content and blobs
518
519The existing protected replica queue seals edits/payloads under keys derived
520from the section key; payload names use keyed digests. Reuse those semantics
521when transporting protected data, with fresh nonces and authenticated envelope
522context as appropriate. A plain SHA-256 of private attachment bytes can leak
523cross-notebook equality; choose protected blob identifiers consistently with
524the existing keyed queue. No cleartext ops, attachment previews, page titles, or
525raw protected payloads may be written into `.snowbound` as a transport shortcut.
526
527An owner cannot semantically merge protected requests without the required
528section key. Proposed behavior is retain encrypted requests and wait for an
529unlocked capable publisher; an encrypted raw-transaction path must preserve
530existing validation and cannot pretend it can rebase unknown plaintext. Whether
531an always-on publisher may retain unlocked keys and for how long is a policy
532question for later design, not an implicit background service requirement.
533Password changes/rekeying need explicit key-epoch dependencies, retained old
534keys/requests policy, and safe receipts before old material is retired.
535
536Unprotected pictures/attachments/video remain byte payloads associated with
537existing ops. Blobs arrive before or after requests; missing blob means wait.
538Validate exact length/digest, reject oversized declarations before allocation,
539bound concurrent reads, and stream where the existing storage APIs permit.
540Preserve external `_onefiles` identities and original bytes. A raw section
541checkpoint does not automatically include external payload files.
542
543## Retention, compaction, and device rejoin
544
545The first safe implementation can retain immutable logs/checkpoints without
546automatic deletion. Storage use is real, so measure it and design compaction;
547finite retention must not be disguised as a correctness proof.
548
549No “all devices have seen it” rule can be inferred from current listings or
550heartbeats. An offline device may hold unpublished edits indefinitely. Deleting
551an accepted request after observing `.one` alone is unsafe because that file
552can later roll back. Receipt existence alone is also insufficient if the
553receipt's reconstructible dependencies are removed.
554
555A compacted checkpoint needs complete section/catalog/blob state, accepted
556request identity/outcome coverage, selected history and retained branch/tombstone
557coverage, plus its exact predecessor closure or an explicit permanent cutoff.
558Only a verified replacement recovery closure can justify retiring old objects.
559Keep dedup/tombstone evidence needed to recognize old retries after compaction.
560
561For an offline client older than retained ancestry, retain its private queue and
562import a fresh base without discarding it. If exact rebase is unavailable, preserve
563its old base/ops as a recovery branch and require an explicit content outcome.
564A destructive administrative reset establishes a new notebook lineage and keeps
565an export of displaced data; it does not make stale requests silently valid.
566An app uninstall destroying the sole private outbox remains an actual loss
567boundary unless that outbox reached retained custody elsewhere.
568
569## Scale and discovery cost
570
571A long notebook cannot relist a flat object directory on every keypress. Segment
572existing sealed batches, partition by writer/owner and bounded buckets, cache
573validated handles/objects locally, follow known dependency/predecessor references,
574and use notifications as hints plus periodic reconciliation. Mutable indexes
575may reduce scans but cannot establish absence or completeness.
576
577SAF opaque document IDs make filename-derived constant-time lookup unavailable
578in general. Resolve actual handles from queried parents and cache them by
579validated object identity. Bucket size and deep directory cost need real provider
580measurements; a huge hierarchy can cost more than a flat list. Unknown new writer
581discovery still needs enumeration, so “bounded lookup” cannot mean a complete
582new-client discovery guarantee without scanning any provider state.
583
584Measure at least: edits per sealed segment; object and request count; bytes
585including retained bases/blobs; cold-open queries; warm sync queries; maximum
586listing size; incomplete dependency backlog; latency until local durability,
587owner custody and visible remote result; and time to rebuild after projection
588rollback. Record provider/API/build identity. A host memory microbenchmark is
589not a Google Drive/iCloud listing benchmark.
590
591## Status and receipt meanings
592
593These are engineering states, not proposed public UI strings:
594
595| Evidence | What is known | What is not known |
596| --- | --- | --- |
597| Local queue durable | This installation can recover the edit after process death | Provider/owner received it |
598| Provider create/close succeeded | This adapter completed local object I/O | Another device or cloud service has it durably |
599| Owner intake durable | Owner has validated recoverable request/dependencies under stated custody | Canonical `.one` contains it |
600| Owner result recoverable | Accepted outcome can be reconstructed from retained closure | Every client has downloaded it |
601| Owner file locally confirmed | Normal append/confirmation completed at owner adapter | Cloud globally ordered/durable, no later rollback |
602| Other client observed result | That client's validated state includes decision/checkpoint | All devices saw it or GC is safe |
603| Historical published receipt | A publication was confirmed at an earlier point | Its effect remains in today's mutable projection |
604
605Current `EditStatus::Published` is historical by design. Adding transport states
606must preserve that meaning and the existing local receipt boundary. Current
607mobile `sb_section_poll` coarse saved/saving/offline values cannot silently
608encode these extra guarantees; UI flow/copy changes require a separate approved
609mapping after protocol behavior exists.
610
611## Fault matrix and practical investigation
612
613Lab evidence lives outside source under
614`target/multi-file-sync-spec-20261011/`. It is a bounded investigation, not a
615production backend or commercial-provider certification. The Rust lab uses the
616existing `onestore::Transaction::commit`, `notebook::Replica`, receipt logic,
617retained-version merge, and cold parser. Provider delivery is a deterministic
618simulation; logical drop/reopen is not an OS-kill durability test. The separate
619Python model explores object framing/dependencies and ownership rules only.
620
621### Recorded results
622
623The following are observed experiment outcomes. The requirements and future
624implementation gates elsewhere in this draft are not proven by these tests.
625Native-corpus Rust run 7 completed with exit 0, and transport-model run 3
626completed with exit 0. Reports and raw `.one` outputs remain in the ignored lab
627folder; the verification manifest binds their hashes and the exact lab sources.
628
629| Experiment | Observed outcome | Evidence |
630| --- | --- | --- |
631| Heartbeat/lease takeover with stale view | Counterexample found: two independently valid append results; winning snapshot omits ALPHA | `run-7/report.json` → `lease_takeover_split_writer` |
632| Explicit handoff, then old provider upload | Counterexample found: exact ALPHA BETA becomes ALPHA RIGHT after delayed snapshot delivery, despite no concurrent application writers | `clean_handoff_late_old_upload` |
633| Retained independent section branches | Existing merge yields ordinary text exactly ALPHA BETA | `independent_immutable_branches_recover` |
634| Late-upload rollback with retained checkpoint | Existing retained-version merge recovers BETA | `late_upload_recovered_from_immutable_checkpoint` |
635| Concurrent overlapping changes | Main page retains BOB RIGHT; actual conflict page retains ALICE RIGHT | `overlap_retains_actual_conflict_page` |
636| Append succeeds, reply lost, logical close/reopen | One publication total; existing receipt settles without another append | `owner_append_then_crash_before_ack` |
637| Historical Published receipt, then projection rollback | Counterexample found: queue empty and historical receipt remains Published while ALPHA is absent | `historical_receipt_does_not_repair_projection_rollback` |
638| Resumed old-owner branch and new-owner branch | Existing merge retains OLD and NEW; strict owner uniqueness not established | `old_owner_resumes_branch_preservation` |
639| Old offline branch retained through rejoin | Existing merge can import its ALPHA content | `offline_rejoin_request_retention` |
640| Original branch redelivered after merge | For each recovery case: three same-database reopen/retries plus one genuinely fresh replica seeded from merged bytes; zero extra publications, identical output | Recovery case `proof` fields |
641| Generated minimal section seed | Current cold parse succeeds for both same-file/root branches; exact existing resolve returns “Object space has no active default revision” | `run-7/generated-seed-diagnostic.json` |
642| Abstract transport model | 737 truncated prefixes and 737 single-byte mutations rejected; 120 arrival permutations converge once with omission/duplicates; schema/cycle/contradiction checks pass | `visibility-run-3/visibility-report.json` |
643| Abstract deep input | 1,101-object dependency chain traversed iteratively; nested 1,100-level JSON rejected without recursion crash | Same transport report |
644
645The generated-seed diagnostic is specifically the current `create_section`
646helper's output and those exact edits. Current-image parsing does not certify
647all historical ancestry candidates or OneNote conformance. The existing sync
648fallback keeps an unmergeable version whole; this result is not evidence that
649all supported production inputs fail to merge. It does mean that a protocol
650must represent retained-unresolved custody separately from accepted content.
651The native-corpus success demonstrates existing section-version merge/dedup,
652not a new request/owner-decision/tombstone implementation.
653
654Failed investigations remain preserved: run 1 (missing lab dependency), run 3
655(lab type-alias error), runs 2/4/5 (generated-seed refusal), and run 6 (text oracle
656looked only at ordinary page-list content, excluding the conflict page). Final
657run 7 asserts actual conflict-page text through the existing section conflict
658reader and preserves the earlier evidence. No failed run is a product gate.
659
660Rerun from the repository root on a clean **new output directory**, using the
661shared target and one build slot; never reuse a run folder or point the lab at an original notebook:
662
663```sh
664nice -n 10 env CARGO_BUILD_JOBS=2 CARGO_TARGET_DIR="$PWD/target" cargo run --offline \
665 --manifest-path target/multi-file-sync-spec-20261011/lab/Cargo.toml -- \
666 "$PWD/target/multi-file-sync-spec-20261011/run-new"
667python3 target/multi-file-sync-spec-20261011/lab/visibility.py \
668 target/multi-file-sync-spec-20261011/run-new \
669 target/multi-file-sync-spec-20261011/visibility-run-new
670```
671
672The ignored standalone manifest currently has absolute path dependencies into
673`workspaces/android-host-plan`; the verification manifest records them. The lab
674links the repository's existing test remote and private merge sources directly,
675without copying or changing them. The fixture is a disposable read of
676`corpus/conflict-page/native/initial/notebook/synthetic.one`; its input hash is
677recorded. No commercial provider, emulator, or OneNote process ran in this lab.
678
679| Fault | Required result / investigation oracle |
680| --- | --- |
681| Prefix/partial body, invalid length/digest | Reject/wait; last valid state untouched |
682| Reordered/duplicate/omitted listings | Same logical requests applied once; omission never deletion |
683| Missing base/blob/predecessor | Wait for dependency; do not apply positional ops to wrong base |
684| Same immutable ID, different bytes | Preserve contradiction; stop automatic authority selection |
685| Owner unavailable | Local editing/Undo/reopen works; requests retained |
686| Owner crash after append before ack | Original named outcome recognized after reopen; no repeated edit |
687| Lease takeover while old view is stale | Expected counterexample: both writers eligible; never call this fenced |
688| Graceful handoff followed by old queued upload | Expected counterexample: canonical `.one` can regress |
689| Old owner resumes after takeover | Preserve both decision/section branches; merge/recover without silent loss |
690| Published receipt then `.one` rollback | Historical receipt cannot alone restore removed effect |
691| Independent retained branches | Actual merge retains both changes, retry idempotent |
692| Overlapping retained branches | Actual conflict page preserves both alternatives |
693| Retried old branch after owner restart | No duplicate revisions/conflict pages from original immutable input |
694| Offline rejoin after delete/compaction | Dedup/tombstones honored; unseen edits retained for explicit outcome |
695| Invalid/foreign/repaired `.one` | Preserve bytes; never rebase old requests onto a different section |
696| Close/flush/upload ambiguity | Keep attempt evidence and request; do not promise remote durability |
697| Protected request without owner key | Encrypted queue stays recoverable; no plaintext transport fallback |
698| Attachment missing/partial | Wait; no receipt claiming complete recoverability |
699| TOC arrives before section / move halfway | Keep catalog/intent recoverable; prefer duplicate over loss |
700
701The Android host/provider plan and its two DocumentsProvider fixtures are at
702`target/android-wave-20261011/host/`. They are specified, not yet implemented or
703run. They test the real platform boundary separately from these simulations.
704Commercial provider observations, native OneNote cold-open of new protocol
705outputs, large-notebook scale, protected transport, namespace recovery, and actual
706process-kill/crash tests are implementation gates, not claims made by this draft.
707
708## Implementation boundary and review decisions
709
710`onestore` remains unaware of providers, coordination folders and ownership.
711The data-layer protocol belongs toward `notebook`, reusing queue, merge,
712recovery and normal append operations. Platform hosts adapt document/file APIs,
713retained local directories and actual I/O/error callbacks. Android rendering and
714native IME/accessibility can ship independently of this protocol.
715
716Before production protocol code, review practical evidence and select:
717
718- Fixed publisher/explicit handoff versus branch-preserving automatic recovery,
719 and whether majority membership offers enough benefit for its availability cost.
720- The authority/recovery contract for registered notebooks, bare imports,
721 intentional restores, duplicate notebook copies and copied owner credentials.
722- Protected-key custody and unsupported-client migration/escape behavior.
723- Retention/compaction guarantees and a measured provider capability floor.
724
725Unknown protocol/required-op versions must leave remote data untouched and keep
726local edits recoverable. Enabling coordination requires an explicit migration
727record and closure; old builds that ignore `.snowbound` can still edit `.one`
728uncoordinated. A sidecar marker cannot stop those binaries. Rollout therefore
729needs an honest compatibility boundary and an opt-in/import path, not a claim
730that an unsupported build will obey a new marker.
731
732The next concrete deliverable after the draft is a narrow transport/owner lab
733against real provider fixtures, then one selected policy implemented behind an
734explicit storage mode and tested with actual notes. No timeout election or
735full-image save shortcut should land as an Android dependency.
arc/sync.md+3
...@@ -363,3 +363,6 @@ output: every converted page must equal the page the old cache held, or the...@@ -363,3 +363,6 @@ output: every converted page must equal the page the old cache held, or the
363conversion rolls back and names the archive it made first.363conversion rolls back and names the archive it made first.
364364
365The [notebook README](../crates/notebook/README.md) documents the API in detail.365The [notebook README](../crates/notebook/README.md) documents the API in detail.
366
367A separate [draft multi-file coordination protocol](multi-file-sync.md) explores
368generic cloud mounts; it is independent of the Android port and is not enabled.