| ... | @@ -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 |
| 5 | shipping Android. Nothing in this document implements or enables a new storage |
| 6 | mode. Existing local/native and direct SMB publication retain their existing |
| 7 | OneNote-compatible exclusion. The wire examples and ownership policies below |
| 8 | are proposals for review, not frozen public APIs. |
| 9 | |
| 10 | Clover's direction is a notebook's `.snowbound/` directory carrying coordination |
| 11 | between Snowbound clients, with one owner process publishing the real `.one` |
| 12 | files. Other builds exchange separate files instead of racing on those files. |
| 13 | Generic cloud mounts cannot be assumed to implement reliable distributed locks. |
| 14 | This should cover Android/Linux provider-backed notebooks and improve iCloud |
| 15 | coordination. Practical tests must determine the tradeoffs before selecting an |
| 16 | ownership/recovery policy. |
| 17 | |
| 18 | ## The contract under investigation |
| 19 | |
| 20 | ```text |
| 21 | Android / 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 | ▼ |
| 27 | owner existing merge and revision append |
| 28 | ├── immutable decision/checkpoint evidence ──► other Snowbound replicas |
| 29 | └── ordinary .one / .onetoc2 files ─────────► eventual provider projection |
| 30 | ``` |
| 31 | |
| 32 | A single application publisher reduces ordinary write/write contention. It |
| 33 | cannot prevent a provider from delivering an old queued upload after ownership |
| 34 | has moved. A successful local flush, a successful handoff, and a unique current |
| 35 | logical owner are three different facts. The design must retain enough immutable |
| 36 | evidence to repair the mutable file after a rollback even if every client has |
| 37 | already received a historical publication receipt. |
| 38 | |
| 39 | Proposed 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 | |
| 62 | These requirements depend on at least one retained complete copy of each |
| 63 | necessary object eventually being delivered. No file protocol preserves data |
| 64 | when every durable copy is destroyed, or proves delivery when the provider |
| 65 | permanently withholds it. Eventual projection repair additionally requires that |
| 66 | old uploads eventually stop, required objects are eventually delivered, a |
| 67 | capable reconciler keeps running, and necessary keys/versions remain available. |
| 68 | An unbounded stream of stale overwrites can keep the canonical path regressing |
| 69 | forever. Retained history preserves recoverability; it does not make that path |
| 70 | monotonic under an adversarial delivery schedule. |
| 71 | |
| 72 | ## Storage model and observations |
| 73 | |
| 74 | Assume independent client views of one provider-backed directory tree. Reads and |
| 75 | listings may be stale; files may become visible before their bytes are complete; |
| 76 | notifications and uploads may reorder; a complete old `.one` image may replace a |
| 77 | newer one. A provider may omit known children, return unknown metadata, rename a |
| 78 | file while changing its opaque handle, or reject a nominally supported operation. |
| 79 | Clients can remain offline indefinitely. Process clocks are unrelated. A process |
| 80 | can stop at any instruction and later resume with old state. |
| 81 | |
| 82 | There is no assumed atomic directory rename, distributed exclusive create, |
| 83 | compare-and-swap, reliable distributed advisory lock, complete listing, or |
| 84 | multi-file transaction. Independent objects with unguessably unique identities |
| 85 | reduce collisions; they do not turn a provider's filename API into exclusive |
| 86 | creation. Object integrity is checked after reading complete bytes. Local |
| 87 | app-private SQLite ownership is a different boundary and can still serialize |
| 88 | processes sharing that same local replica. |
| 89 | |
| 90 | Android is one transport, not the definition of the protocol. SAF `rw` opens |
| 91 | provide seeking when supported; that says nothing about remote exclusion or |
| 92 | upload ordering. Provider handles/IDs remain opaque, and returned rename handles |
| 93 | must be retained. iCloud coordination/versions remain useful host capabilities, |
| 94 | but 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 | |
| 98 | A provider that cannot retain separately named byte objects, enumerate them |
| 99 | again, or represent the selected notebook subtree cannot carry this transport |
| 100 | without another adapter. Unsupported capabilities must be observed in disposable |
| 101 | provider 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 | |
| 114 | No recommendation here may describe a wall-clock lease as sufficient fencing. |
| 115 | Heartbeat objects are discovery/liveness hints only. A process that sees no other |
| 116 | owner cannot distinguish an absent owner from a partition or stale listing. |
| 117 | |
| 118 | Majority agreement is a real alternative, not impossible consensus: fixed |
| 119 | acceptors can persist promises, choose one logical history, and recover after |
| 120 | restart. Its availability cost matters for a phone and one laptop. With two |
| 121 | members, a majority requires both. Membership changes and acceptor-state loss |
| 122 | need 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 | |
| 125 | Fencing must be checked by the resource receiving the write. A local owner epoch |
| 126 | inside `.snowbound` cannot force a generic mount to reject a queued older `.one` |
| 127 | upload; the provider does not interpret that epoch. Chubby's sequencers address |
| 128 | precisely 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 |
| 132 | publisher, explicit handoff, and immutable recoverable inputs/outcomes. Other |
| 133 | clients keep editing from retained local replicas while that publisher is absent; |
| 134 | publication waits. Preserve branches as the recovery fallback, and treat automatic |
| 135 | takeover as separate policy work. Timeout takeover is not part of the strict |
| 136 | owner contract. Majority agreement remains an alternative with its stated |
| 137 | availability cost. This makes the sidecar necessary recovery authority for |
| 138 | accepted history when the mutable projection regresses; that authority/backup |
| 139 | contract needs owner review before production implementation. The recorded |
| 140 | experiments below support the recovery requirement, not a finished owner protocol. |
| 141 | |
| 142 | ## Identities and duplicate processes |
| 143 | |
| 144 | Notebook identity must be independent of mount path, provider document ID, |
| 145 | display name, host registration, and filename case normalization. Existing |
| 146 | OneNote file/root identities remain the content identities. A new coordination |
| 147 | notebook identity distinguishes separate imported copies and ownership histories; |
| 148 | how deliberate notebook copies inherit or reset this identity remains a |
| 149 | migration decision, not an inference from their path. |
| 150 | |
| 151 | Use 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 | |
| 163 | Previously sealed/outstanding records keep their original request, edit and |
| 164 | segment identities after every restart. A fresh process incarnation names only |
| 165 | new records. Retrying an old outbox may write its original namespace/bytes; it |
| 166 | does not rekey old edits into the new process namespace. Otherwise deduplication |
| 167 | would turn an ordinary restart into a second user action. |
| 168 | |
| 169 | Two processes on one installation must not both publish because they share a |
| 170 | device ID. A locally owned publisher database/socket may serialize processes |
| 171 | sharing reliable private storage. A second process with a copied private profile |
| 172 | on another device is a different failure: distinguish incarnations, preserve |
| 173 | both claims, and stop automatic publication when contradictory ownership becomes |
| 174 | visible. Absence of a contradiction in a stale view is not a uniqueness proof. |
| 175 | A backup of owner credentials is not permission to run both copies. |
| 176 | |
| 177 | For fixed ownership, restart uses the retained durable owner state and a fresh |
| 178 | process incarnation. For explicit handoff, the old state records retirement |
| 179 | before a successor becomes eligible. Losing the old durable state requires |
| 180 | recovery; creating a fresh claim is not a transparent restart. |
| 181 | |
| 182 | ## Proposed object namespace |
| 183 | |
| 184 | Names illustrate roles; this is not a frozen layout or extension: |
| 185 | |
| 186 | ```text |
| 187 | Notebook/ |
| 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 | |
| 200 | Every published object is immutable. A transport retry keeps its identity and |
| 201 | bytes. Correcting an object creates a distinct object referencing what it |
| 202 | supersedes. Mutable convenience indexes or heartbeats may accelerate discovery |
| 203 | but cannot authorize publication, retirement, or data loss. No implementation |
| 204 | may overwrite one shared `owner.json` or `inbox.json` as the coordination truth. |
| 205 | |
| 206 | Per-process directories avoid ordinary client/client name collisions. Unique |
| 207 | logical object IDs also appear inside the envelope. Two provider documents with |
| 208 | one display name are not automatically one object; the same object under two |
| 209 | names/handles is one logical object only after matching its validated identity |
| 210 | and bytes. Different bodies claiming one immutable identity are a contradiction |
| 211 | and are preserved/quarantined. This cannot retroactively undo side effects that |
| 212 | already occurred before the contradiction became visible. |
| 213 | |
| 214 | No one file per keystroke: transport sealed batches from the existing queue into |
| 215 | segments bounded by encoded bytes and editing bursts. The durable local receipt |
| 216 | remains immediate; the provider upload can coalesce multiple edits. Individual |
| 217 | edit IDs remain present for receipts/dedup. Attachments and large bodies are |
| 218 | separate immutable blobs; transport does not repeatedly embed them in every |
| 219 | segment. Exact segmentation thresholds require measurements, not a public |
| 220 | extension promise. |
| 221 | |
| 222 | ## Framing, validation, and wire examples |
| 223 | |
| 224 | Before accepting an object, check magic/version, bounded envelope/body lengths, |
| 225 | notebook identity, declared kind, complete record digest, sender/object identity, |
| 226 | and all required dependencies. Integrity covers the entire encoded envelope and |
| 227 | body, including identities, owner epoch, predecessor/base/dependency references, |
| 228 | kind and version. A body-only digest permits metadata mixups and is insufficient. |
| 229 | A trailing digest or excluded self-digest field must have an unambiguous encoding |
| 230 | rule. Digests prove byte consistency, not permission to edit. If access/authentication is added, it covers the envelope and dependencies |
| 231 | as well as payload bytes. Provider ACLs and section passwords remain separate. |
| 232 | |
| 233 | Do not trust size/mtime metadata as completeness. A parseable JSON prefix or an |
| 234 | apparently complete `.one` header is not enough; an object references its exact |
| 235 | complete body length/digest. A readiness marker may arrive before the body and |
| 236 | merely adds a dependency to wait for. Atomic rename is unnecessary for acceptance |
| 237 | only if incomplete objects are rejected and complete objects stay immutable. |
| 238 | |
| 239 | Illustrative 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 | |
| 257 | Illustrative 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 | |
| 291 | The examples omit separate payload bytes. `recordDigest` is symbolic: the codec |
| 292 | must define exact bytes and self-field exclusion before implementation. Every |
| 293 | object kind requires the same metadata integrity, not only edit segments. |
| 294 | |
| 295 | Version 1 is illustrative. The actual serialization must reuse existing |
| 296 | `onestore::op::Edit`/`Op` semantics, with an explicit compatibility boundary |
| 297 | around them. Existing serde representation is not independently promised stable |
| 298 | forever. Do not invent a second page-diff language or CRDT. Live Share's bounded |
| 299 | op batch plus named revisions demonstrates an existing reusable approach; its |
| 300 | network membership/timeout assumptions do not transfer automatically to cloud |
| 301 | files. |
| 302 | |
| 303 | ## Causal bases, merges, and replay identity |
| 304 | |
| 305 | A segment names its exact input base and prior writer segment. A client editing |
| 306 | without the owner continues its own local causal chain. The owner waits for a |
| 307 | complete base/checkpoint, referenced blobs, and required predecessor decisions |
| 308 | before interpreting positional text ops. An old base is not permission to apply |
| 309 | UTF-16 offsets directly to a changed page. |
| 310 | |
| 311 | Reuse the current queue/merge machinery: `PendingEdit` carries author and ops; |
| 312 | `Replica` keeps its base, seals batches, rebases against changed remote state, |
| 313 | and makes conflict pages for incompatible operations. Existing |
| 314 | `resolve::merge` discovers common revision ancestry and deterministic merged |
| 315 | revision identities for retained competing section versions. Where no shared |
| 316 | valid ancestry exists, preserve the foreign/unreadable branch for recovery. |
| 317 | |
| 318 | Deduplication is two related checks: |
| 319 | |
| 320 | 1. Request identity and encoded content digest identify a transport retry. |
| 321 | 2. Stored named revision identities/outcomes identify work already incorporated |
| 322 | into a section/checkpoint, including after owner restart or request replay. |
| 323 | |
| 324 | A numeric edit ID alone is local to one replica. A file stamp alone does not |
| 325 | prove every request's effect is present. A page's visible text matching expected |
| 326 | text also does not prove correct history, attachments, or membership outcome. |
| 327 | |
| 328 | An unmergeable request or section branch remains retained with its original |
| 329 | request/base/blobs and exact error. Custody acknowledgement of |
| 330 | `retained-unresolved` is distinct from semantic acceptance and from file |
| 331 | publication. It cannot appear in an accepted edit list as though the note had |
| 332 | merged. A recoverable conflict outcome names the actual stored conflict page; |
| 333 | a retained foreign/unreadable branch names recovery evidence instead. |
| 334 | |
| 335 | Owner decisions impose a durable selected order under the chosen ownership |
| 336 | policy. Concurrent branches remain explicit inputs to the existing merge; do |
| 337 | not choose the newest wall clock or discard the branch with the smaller owner |
| 338 | ID. An automatic recovery policy needs a stable, documented reconciliation of |
| 339 | multiple accepted decision histories. Deterministic sorting for processing is |
| 340 | not a consensus or permission rule. |
| 341 | |
| 342 | ## Owner state and write/ack ordering |
| 343 | |
| 344 | Proposed durable states, persisted before their corresponding externally |
| 345 | observable 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 | |
| 359 | The exact linearization between durable prepared decision, file append, and |
| 360 | immutable result publication needs implementation tests. The invariant is |
| 361 | stronger than a particular file ordering: no externally acknowledged request |
| 362 | may depend solely on mutable `.one` bytes or a volatile owner map. A crash at any |
| 363 | point must leave either an unacknowledged retained request or a recoverable |
| 364 | recorded outcome. Checkpoints are raw section bytes/named append artifacts, |
| 365 | not projected page models. |
| 366 | |
| 367 | An owner can prepare and retain a reproducible named transaction before an |
| 368 | append; after an ambiguous append it uses revision evidence and existing |
| 369 | `CommitState`/confirmation logic. It does not regenerate new revision IDs and |
| 370 | retry the user action. Appending successfully and crashing before writing the |
| 371 | receipt must converge to the same outcome when the original request returns. |
| 372 | |
| 373 | If the provider exposes the receipt before its referenced checkpoint/decision, |
| 374 | the reader waits. If the `.one` file is behind the receipt, the reader obtains |
| 375 | or reconstructs the referenced section from its recovery closure; it does not |
| 376 | claim the mutable provider path already holds it. If the file is ahead, it can |
| 377 | read validated revisions while separately resolving incomplete decision history. |
| 378 | |
| 379 | Once a client has validated a selected decision/checkpoint, a stale bare `.one` |
| 380 | observation must not downgrade its known accepted history. Keep that history |
| 381 | and recovery dependencies locally while repairing the projection. An explicit |
| 382 | intentional restore is a new recorded operation, not a stale-file inference. |
| 383 | |
| 384 | An owner-unavailable client still edits from its private replica. Requests wait |
| 385 | for owner processing without blocking local read/navigation/Undo. Publishing |
| 386 | status cannot report owner custody merely because local creation of a provider |
| 387 | object succeeded. |
| 388 | |
| 389 | ## Explicit handoff, recovery takeover, and late uploads |
| 390 | |
| 391 | Proposed explicit handoff sequence: |
| 392 | |
| 393 | 1. 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. |
| 395 | 2. 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. |
| 398 | 3. Successor validates the chain, retrieves every required dependency, resolves |
| 399 | uncertain attempts, and records acceptance in its own durable state. |
| 400 | 4. Successor publishes only under the selected handoff policy. Old owner never |
| 401 | resumes that epoch after a restart; a failed handoff remains recoverable. |
| 402 | 5. Both retain historical recovery evidence, because old provider uploads can |
| 403 | arrive after all application steps have completed. |
| 404 | |
| 405 | A receipt from the successor is useful evidence for operator recovery; it does |
| 406 | not prove every cloud cache drained. Waiting an arbitrary duration is not a |
| 407 | fence. A fixed owner with no ownership transitions avoids one source of late |
| 408 | writes but still needs rollback repair for reordered uploads from its own run. |
| 409 | |
| 410 | Automatic recovery experimentation must keep each candidate's decisions in its |
| 411 | own immutable branch. A candidate cannot overwrite another owner's mailbox or |
| 412 | checkpoint, and claims no exclusive owner authority based on silence. The |
| 413 | mutable `.one` projection may temporarily regress or produce provider conflict |
| 414 | versions. A later reconciler imports all valid branches, preserves incompatible |
| 415 | results as conflict pages/recovery data, and republishes normal revisions. |
| 416 | This is a weaker, explicit safety/availability trade than a strict global |
| 417 | single publisher, and remains unselected pending the experiments. |
| 418 | |
| 419 | A quorum alternative can choose an ownership/decision chain if acceptor |
| 420 | membership and durable promises are implemented. It still needs immutable |
| 421 | recovery and projection repair unless the actual provider enforces epochs on |
| 422 | writes. Do not add a quorum merely to hide this physical fencing limitation. |
| 423 | |
| 424 | ## Authority and disaster recovery |
| 425 | |
| 426 | The `.one` files remain the interoperable materialized section format. The |
| 427 | sidecar must preserve accepted history beyond those mutable files if it is to |
| 428 | repair demonstrated rollbacks. This is a proposed recovery requirement, not a |
| 429 | silent declaration that an arbitrary `.snowbound` folder always overrides every |
| 430 | `.one` file found beside it. |
| 431 | |
| 432 | Distinguish 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 | |
| 440 | Durable custody is always qualified by its adapter: a private SQLite transaction |
| 441 | is locally retained; a provider close is not a cloud durability receipt. A client |
| 442 | must not evict its only outbox because the owner merely closed a provider file. |
| 443 | Cross-device verified copies or an explicit retained owner-private closure have |
| 444 | their own failure domains, which the recovery contract must name. |
| 445 | |
| 446 | A portable backup of a coordinated notebook needs the section bytes plus every |
| 447 | retained immutable object required by accepted decisions and pending requests. |
| 448 | Copying only `.one` is a valid interoperable snapshot, but not proof that |
| 449 | already receipted cloud edits are recoverable. Copying only `.snowbound` may lack |
| 450 | the seed images or external payloads needed for reconstruction. |
| 451 | |
| 452 | The mode must define when a deliberately restored older `.one` is an intentional |
| 453 | user restore versus provider rollback. Timestamps cannot distinguish them. |
| 454 | A deliberate restore needs a new explicit operation/epoch/disposition preserving |
| 455 | the previous accepted branch, rather than deleting its evidence. Exact copy and |
| 456 | restore UI semantics remain owner-facing design work after evidence; no current |
| 457 | build is changed by this draft. |
| 458 | |
| 459 | ## External writers, replacement, and repair |
| 460 | |
| 461 | On each projection change validate store/header/checksums, section root/file |
| 462 | identity, and revision ancestry. The owner compares the actual base stamp at the |
| 463 | moment of normal publication using available adapter guards; coordination mode |
| 464 | never falls back by silently dropping an existing lock. |
| 465 | |
| 466 | A matching identity with extra external revisions is an import/merge candidate. |
| 467 | The existing tolerant reader and retained-version merge can preserve content. |
| 468 | Record the imported exact base/version so later requests have stable ancestry. |
| 469 | A different section at the same filename is foreign content, not a new base for |
| 470 | old queued ops. Preserve it and stop automatic destructive reconciliation. |
| 471 | Unreadable/partially repaired images do not replace the last validated base. |
| 472 | A repair must record the chosen input and retained rejected alternatives. |
| 473 | |
| 474 | Concurrent OneNote edits on a generic cloud mount bypass the coordination |
| 475 | protocol. This mode cannot guarantee safe live interchange with such writers. |
| 476 | Interoperable native filesystem/direct SMB mode remains the route for OneNote |
| 477 | 2010 side-by-side editing. A user opening a resulting `.one` offline still gets |
| 478 | a normal OneNote file; that does not make its cloud upload fenced. |
| 479 | |
| 480 | On iCloud, continue reading unresolved `NSFileVersion` conflicts as recovery |
| 481 | inputs. Multi-file coordination can reduce ordinary competing application |
| 482 | writes and give them retained request history; it cannot promise the operating |
| 483 | system will create zero conflict versions, nor retire those versions before |
| 484 | complete recoverability is established. |
| 485 | |
| 486 | ## Namespace, TOC, moves, deletes, and Undo |
| 487 | |
| 488 | A notebook has multiple files: section groups, `.onetoc2`, recycle-bin sections, |
| 489 | external payload folders, and section files. A single-section protocol is not |
| 490 | a notebook protocol. Namespace operations are owner-processed intentions named |
| 491 | by stable section/page identities and expected causal state; paths are results. |
| 492 | Only the publisher changes shared canonical namespace/TOC in this mode. |
| 493 | |
| 494 | Create/rename/move/delete outcomes reference all files and object identities |
| 495 | they changed. Provider creation and rename return actual handles; do not infer |
| 496 | success from a requested filename or overwrite an unrelated same-name item. |
| 497 | A provider may expose TOC before a new section or source deletion before a |
| 498 | moved section arrives. Consumers wait for the decision's full closure and keep |
| 499 | last valid local catalog state. |
| 500 | |
| 501 | Multi-file changes use resumable intent records, not fictitious atomicity. |
| 502 | For page/section moves, establish destination durability/recoverability before |
| 503 | retiring the source, validate expected source stamp and queued generation under |
| 504 | actual writer coordination, and make retries identity-stable. Without physical |
| 505 | transaction/fencing, prefer a preserved duplicate to loss. Do not mix this |
| 506 | unsettled transport implementation with the separately investigated |
| 507 | cross-notebook move work. |
| 508 | |
| 509 | Delete is a durable tombstone/outcome, never merely disappearance in a listing. |
| 510 | Retain the historical deleted branch or recycle-bin closure required by Undo |
| 511 | and offline rejoin. An old already-accepted create must not recreate a deleted |
| 512 | object when replayed. A genuinely unseen edit based before deletion receives an |
| 513 | explicit merge/conflict/recovery disposition; its content is never dropped by a |
| 514 | membership timeout. Undo is a new conditional intention against actual latest |
| 515 | content, not blindly replaying an old whole-image inverse over later edits. |
| 516 | |
| 517 | ## Protected content and blobs |
| 518 | |
| 519 | The existing protected replica queue seals edits/payloads under keys derived |
| 520 | from the section key; payload names use keyed digests. Reuse those semantics |
| 521 | when transporting protected data, with fresh nonces and authenticated envelope |
| 522 | context as appropriate. A plain SHA-256 of private attachment bytes can leak |
| 523 | cross-notebook equality; choose protected blob identifiers consistently with |
| 524 | the existing keyed queue. No cleartext ops, attachment previews, page titles, or |
| 525 | raw protected payloads may be written into `.snowbound` as a transport shortcut. |
| 526 | |
| 527 | An owner cannot semantically merge protected requests without the required |
| 528 | section key. Proposed behavior is retain encrypted requests and wait for an |
| 529 | unlocked capable publisher; an encrypted raw-transaction path must preserve |
| 530 | existing validation and cannot pretend it can rebase unknown plaintext. Whether |
| 531 | an always-on publisher may retain unlocked keys and for how long is a policy |
| 532 | question for later design, not an implicit background service requirement. |
| 533 | Password changes/rekeying need explicit key-epoch dependencies, retained old |
| 534 | keys/requests policy, and safe receipts before old material is retired. |
| 535 | |
| 536 | Unprotected pictures/attachments/video remain byte payloads associated with |
| 537 | existing ops. Blobs arrive before or after requests; missing blob means wait. |
| 538 | Validate exact length/digest, reject oversized declarations before allocation, |
| 539 | bound concurrent reads, and stream where the existing storage APIs permit. |
| 540 | Preserve external `_onefiles` identities and original bytes. A raw section |
| 541 | checkpoint does not automatically include external payload files. |
| 542 | |
| 543 | ## Retention, compaction, and device rejoin |
| 544 | |
| 545 | The first safe implementation can retain immutable logs/checkpoints without |
| 546 | automatic deletion. Storage use is real, so measure it and design compaction; |
| 547 | finite retention must not be disguised as a correctness proof. |
| 548 | |
| 549 | No “all devices have seen it” rule can be inferred from current listings or |
| 550 | heartbeats. An offline device may hold unpublished edits indefinitely. Deleting |
| 551 | an accepted request after observing `.one` alone is unsafe because that file |
| 552 | can later roll back. Receipt existence alone is also insufficient if the |
| 553 | receipt's reconstructible dependencies are removed. |
| 554 | |
| 555 | A compacted checkpoint needs complete section/catalog/blob state, accepted |
| 556 | request identity/outcome coverage, selected history and retained branch/tombstone |
| 557 | coverage, plus its exact predecessor closure or an explicit permanent cutoff. |
| 558 | Only a verified replacement recovery closure can justify retiring old objects. |
| 559 | Keep dedup/tombstone evidence needed to recognize old retries after compaction. |
| 560 | |
| 561 | For an offline client older than retained ancestry, retain its private queue and |
| 562 | import a fresh base without discarding it. If exact rebase is unavailable, preserve |
| 563 | its old base/ops as a recovery branch and require an explicit content outcome. |
| 564 | A destructive administrative reset establishes a new notebook lineage and keeps |
| 565 | an export of displaced data; it does not make stale requests silently valid. |
| 566 | An app uninstall destroying the sole private outbox remains an actual loss |
| 567 | boundary unless that outbox reached retained custody elsewhere. |
| 568 | |
| 569 | ## Scale and discovery cost |
| 570 | |
| 571 | A long notebook cannot relist a flat object directory on every keypress. Segment |
| 572 | existing sealed batches, partition by writer/owner and bounded buckets, cache |
| 573 | validated handles/objects locally, follow known dependency/predecessor references, |
| 574 | and use notifications as hints plus periodic reconciliation. Mutable indexes |
| 575 | may reduce scans but cannot establish absence or completeness. |
| 576 | |
| 577 | SAF opaque document IDs make filename-derived constant-time lookup unavailable |
| 578 | in general. Resolve actual handles from queried parents and cache them by |
| 579 | validated object identity. Bucket size and deep directory cost need real provider |
| 580 | measurements; a huge hierarchy can cost more than a flat list. Unknown new writer |
| 581 | discovery still needs enumeration, so “bounded lookup” cannot mean a complete |
| 582 | new-client discovery guarantee without scanning any provider state. |
| 583 | |
| 584 | Measure at least: edits per sealed segment; object and request count; bytes |
| 585 | including retained bases/blobs; cold-open queries; warm sync queries; maximum |
| 586 | listing size; incomplete dependency backlog; latency until local durability, |
| 587 | owner custody and visible remote result; and time to rebuild after projection |
| 588 | rollback. Record provider/API/build identity. A host memory microbenchmark is |
| 589 | not a Google Drive/iCloud listing benchmark. |
| 590 | |
| 591 | ## Status and receipt meanings |
| 592 | |
| 593 | These 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 | |
| 605 | Current `EditStatus::Published` is historical by design. Adding transport states |
| 606 | must preserve that meaning and the existing local receipt boundary. Current |
| 607 | mobile `sb_section_poll` coarse saved/saving/offline values cannot silently |
| 608 | encode these extra guarantees; UI flow/copy changes require a separate approved |
| 609 | mapping after protocol behavior exists. |
| 610 | |
| 611 | ## Fault matrix and practical investigation |
| 612 | |
| 613 | Lab evidence lives outside source under |
| 614 | `target/multi-file-sync-spec-20261011/`. It is a bounded investigation, not a |
| 615 | production backend or commercial-provider certification. The Rust lab uses the |
| 616 | existing `onestore::Transaction::commit`, `notebook::Replica`, receipt logic, |
| 617 | retained-version merge, and cold parser. Provider delivery is a deterministic |
| 618 | simulation; logical drop/reopen is not an OS-kill durability test. The separate |
| 619 | Python model explores object framing/dependencies and ownership rules only. |
| 620 | |
| 621 | ### Recorded results |
| 622 | |
| 623 | The following are observed experiment outcomes. The requirements and future |
| 624 | implementation gates elsewhere in this draft are not proven by these tests. |
| 625 | Native-corpus Rust run 7 completed with exit 0, and transport-model run 3 |
| 626 | completed with exit 0. Reports and raw `.one` outputs remain in the ignored lab |
| 627 | folder; 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 | |
| 645 | The generated-seed diagnostic is specifically the current `create_section` |
| 646 | helper's output and those exact edits. Current-image parsing does not certify |
| 647 | all historical ancestry candidates or OneNote conformance. The existing sync |
| 648 | fallback keeps an unmergeable version whole; this result is not evidence that |
| 649 | all supported production inputs fail to merge. It does mean that a protocol |
| 650 | must represent retained-unresolved custody separately from accepted content. |
| 651 | The native-corpus success demonstrates existing section-version merge/dedup, |
| 652 | not a new request/owner-decision/tombstone implementation. |
| 653 | |
| 654 | Failed 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 |
| 656 | looked only at ordinary page-list content, excluding the conflict page). Final |
| 657 | run 7 asserts actual conflict-page text through the existing section conflict |
| 658 | reader and preserves the earlier evidence. No failed run is a product gate. |
| 659 | |
| 660 | Rerun from the repository root on a clean **new output directory**, using the |
| 661 | shared target and one build slot; never reuse a run folder or point the lab at an original notebook: |
| 662 | |
| 663 | ```sh |
| 664 | nice -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" |
| 667 | python3 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 | |
| 672 | The ignored standalone manifest currently has absolute path dependencies into |
| 673 | `workspaces/android-host-plan`; the verification manifest records them. The lab |
| 674 | links the repository's existing test remote and private merge sources directly, |
| 675 | without copying or changing them. The fixture is a disposable read of |
| 676 | `corpus/conflict-page/native/initial/notebook/synthetic.one`; its input hash is |
| 677 | recorded. 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 | |
| 701 | The Android host/provider plan and its two DocumentsProvider fixtures are at |
| 702 | `target/android-wave-20261011/host/`. They are specified, not yet implemented or |
| 703 | run. They test the real platform boundary separately from these simulations. |
| 704 | Commercial provider observations, native OneNote cold-open of new protocol |
| 705 | outputs, large-notebook scale, protected transport, namespace recovery, and actual |
| 706 | process-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. |
| 711 | The data-layer protocol belongs toward `notebook`, reusing queue, merge, |
| 712 | recovery and normal append operations. Platform hosts adapt document/file APIs, |
| 713 | retained local directories and actual I/O/error callbacks. Android rendering and |
| 714 | native IME/accessibility can ship independently of this protocol. |
| 715 | |
| 716 | Before 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 | |
| 725 | Unknown protocol/required-op versions must leave remote data untouched and keep |
| 726 | local edits recoverable. Enabling coordination requires an explicit migration |
| 727 | record and closure; old builds that ignore `.snowbound` can still edit `.one` |
| 728 | uncoordinated. A sidecar marker cannot stop those binaries. Rollout therefore |
| 729 | needs an honest compatibility boundary and an opt-in/import path, not a claim |
| 730 | that an unsupported build will obey a new marker. |
| 731 | |
| 732 | The next concrete deliverable after the draft is a narrow transport/owner lab |
| 733 | against real provider fixtures, then one selected policy implemented behind an |
| 734 | explicit storage mode and tested with actual notes. No timeout election or |
| 735 | full-image save shortcut should land as an Android dependency. |