zudo-text

検索したい単語を入力

いつでも検索バーを開ける

Cross-device divergence contract

This contract describes the implemented canonical note data model and its preserving conflict behavior. It also retains the historical divergence protocol from #6945 and its follow-ups where that explains the remaining file/path lane. Historical numbering, rename cycles and content-match heuristics are not canonical note APIs. See also Sync Architecture.

The identity sections describe implemented data boundaries, not a release or a claim that every planned Note Tray or AI feature is complete. The historical verification map records its original evidence, not verification of this entire identity implementation or native two-device behavior.

Canonical note identity contract

Identity, numbers and enrollment

A canonical note is identified by (workspaceId, noteId), with a client-generated UUID. A path is its current location; a display number is permanent workspace metadata. Neither the filename, a file-row ID, the number nor matching text can stand in for the note UUID. Identical and empty notes remain distinct, including across workspaces. Registered named notes use the same identity model.

NoteNumberState distinguishes pending, assigned and import-conflict. The client creates the UUID and freezes the creation intent before publishing an editable pending buffer. The server assigns a workspace number atomically; numbers are never reused after deletion. Move, archive, reorder and tidy do not renumber notes or fill gaps. Pending and import-conflict states must not be presented as an assigned number inferred from a filename.

Enrollment reads the complete paginated inventory, decrypts and classifies it on the client, and checks captured workspace/key ownership across awaits. The server validates the inventory generation and membership in the registration transaction. Every eligible old numeric candidate raises the high-water mark before any new number is allocated: an imported unique 42.md reserves 42 even if a named note is encountered first. Unique available numbers are retained. Duplicate candidates receive separate UUIDs and an import-conflict state; there is no automatic keeper. Explicit confirmation covers the full conflict membership, chooses the keeper and assigns the others numbers above the current high-water mark. An already assigned reservation cannot be reassigned. A stale proposal conflicts rather than silently changing the user's mapping.

Registration does not rewrite filenames, body text or history. Unresolved number collisions do not block unrelated new-note allocation. Internal namespaces, assets, arbitrary root files, unregistered deleted files and retired-path tombstones are not newly enrolled; existing registered root notes remain notes. The classifier lives in packages/file-utils/src/note-identity.ts; the shared inventory reader is packages/backend-bridge/src/note-enrollment.ts.

The server stores and can observe note membership, opaque UUID/file/path associations, assigned and candidate numbers, import conflicts, high-water allocation, content/location/metadata revisions and operation receipts. These fields reveal allocation and correlation metadata. Paths are encrypted; enrollment and numbering receive no decrypted path, sticker or note body. Bodies, ciphertext, history, mirrors and sealed operations remain in their existing storage systems. Consented AI access is a separate boundary; this registry grants no plaintext access and documents no new AI capability.

After enrollment, canonical mutations require protocol 1 and the note UUID, operation ID, captured base and location revision. The server rejects missing or obsolete protocol authority; it also rejects the old move-header route for canonical mutations. The client exposes held upgrade/inventory status and keeps sealed work. It does not fall back to numeric allocation, renaming or an unconditional path write when enrollment or protocol validation fails.

Captured edit authority and durable operations

WorkspaceNoteSnapshot binds entry, content and authority together. NoteWriteAuthority contains the exact captured fields:

type NoteWriteAuthority = {
  workspaceId: string;
  noteId: string;
  path: string;
  editBase:
    | { kind: "known"; version: number }
    | { kind: "new" }
    | { kind: "unknown" };
  locationRevision: number;
  writerId?: string;
  observedMutation?: number;
};

The shared editor buffer is keyed by workspace and UUID. Dirty text retains its captured authority through saves, dialogs, workspace switches and restarts. An incidental read of a newer head, metadata refresh, identical body or empty body cannot authorize a stale write or replace dirty text. Async continuations check workspace, generation and ownership before applying results. Only proven operations in the same local lineage can advance their dependent authority; a foreign update requires the existing explicit conflict decision.

Own lineage is the heads one logical writer's receipts produced. writerId names that stream: panes sharing one editor buffer keep a single id, while a raw read, another window, or a direct API write mints its own. observedMutation is the local path-mutation token seen with the capture. It is not a cloud revision, so an equal server version or a newer same-device receipt is not authority for an older dirty body. When a receipt is acknowledged, each queued operation of that same writer at that location records the acknowledged head (a queued own move carries it to its destination), and the completed receipt records its own resulting head. An editor that re-read an intermediate own receipt while later own work was still queued therefore keeps proving its authority after that work is acknowledged, because the buffer keeps its writer id. A different writer's queued or acknowledged write does not. A held entry never grants write authority. A checkpoint plan keeps the mutation token of the head it read; restoring or deleting that note requires the same token, and omitting the token is not observation. Deleted heads instead use the UUID, path, positive tombstone version and location revision from the authenticated checkpoint manifest, confirmed absent by the canonical read. They do not invent a live observation token. The restore request retains those exact server preconditions; a changed head or a replacement owner is refused. Dialog closure, workspace/key-generation changes and A→B→A invalidate awaited preview reads. A pulled row has no operation id. It is this journal's echo only when one retained receipt, or one in-flight operation whose captured base is older than the row, matches its path, note, location, origin device and wire content hash together. That echo is not a foreign conflict, and a later save by the same writer is not reverted by it. Skipping a dirty host additionally requires positive renderer proof that every current dirty owner at that note/path retains the proven operation's writer. An unknown, removed, stale or independent owner keeps the normal preserving conflict flow, even when this journal retains the other writer's receipt. Another writer's row still updates a clean host and still conflicts with an independent older dirty edit. Device identity or equal bytes alone are not that match. A dirty buffer never adopts a newer head itself: it keeps its captured authority, and the write guard decides. Save refusals are shown in plain language; the protocol codes (NOTE_LOCATION_CONFLICT, NOTE_SCOPE_CHANGED, NOTE_SAVE_DEFERRED, NOTE_CONTENT_CHANGED, NOTE_AUTHORITY_CONFLICT) are diagnostics only.

The mock persists path observation generations with its model. Reloading that same model retains a valid capture without turning an old read into a new writer. Missing persisted generations start a fresh observation realm, never reset to zero as proof. Raw reads still mint independent writers; only actual operation receipt heads extend the originating writer's lineage. Immutable operation snapshots retain the exact body, identity and receipt capability independently of later raw reads.

The pending editor remains writable while its local journal is being sealed. Remote allocation/replay waits for durable storage. A sealing failure keeps text visible and unsaved, with recovery protection. Ordered sealed operations preserve write → move → write dependencies. Their frozen UUIDs, operation IDs and request payloads make retries idempotent; changing a request under an existing operation ID is rejected. Exact ciphertext is retained before upload so a lost acknowledgment does not require inventing a new operation.

notes.create reporting locally-durable is not cloud acknowledgment. notes.getOperationState distinguishes queued, held, unknown and an acknowledged receipt. notes.readOperationSnapshot returns the original operation's body with that exact receipt, never old text paired with a newly read head. Recovery must keep its intent when that proof is unavailable.

Completed create/restore/history payloads can be released with notes.releaseOperationSnapshot only after their consumer has safely handed off the exact snapshot and durably completed its recovery intent. A checkpoint restore keeps payloads until its whole sealed plan is removed. Release retains request digests, receipts and lineage; pending or held operations are untouched. A later read can fetch the exact immutable version and verify its digest, not substitute the latest head. That fetch can be unavailable offline or after history retention. Failed release leaves a safe retained payload and does not undo an already completed recovery. Receipt retention is not an unlimited local body recovery archive.

Move, reorder and tidy

notes.moveById preserves the UUID, number, body and history through an atomic same-workspace location mutation with captured content and location authority. It is not destination upload followed by source deletion. The old path is retired without lending its tombstone the moved note's identity; a later occupant at that path cannot receive an old editor's save. When the desired basename is occupied, the client can select a UUID filename; it does not overwrite another note. A genuine deleted note remains reserved for same-ID recovery, distinct from a reusable moved-away path.

notes.reorderById writes the UUID order in .zudotext/note-order.json and preserves unrelated IDs when reordering a subset. It does not rename files, change content versions or remap editor ownership. The current canonical tidyUpDrafts records order for the directory through this same API: it does not compact numbered filenames, allocate replacement notes or delete empty notes. Empty-note deletion is a separate explicit action.

History and checkpoint restoration

Version restore carries the selected note's captured authority and a frozen operation ID, and returns the immutable selected body with its exact restore receipt. A checkpoint manifest includes historical identity plus the current path/version needed for authority; the historical path is a desired location, not proof of the current occupant. A moved note's restore-at-A and delete-at-B entries collapse into one same-ID content/location intent. Different IDs at a reused path remain separate and cannot overwrite each other by ordering accident. The confirmed plan and operation IDs are sealed before mutation. Resume uses that plan and its captured provenance, not a newly generated latest-head plan.

Popout handback

Canonical popout payloads carry UUID and captured authority, including metadata only envelopes for oversized text. Supplied inline text must match its captured digest. A saved child snapshot is accepted only with exact authoritative version proof and matching current stable location/head, checked again after download. An arbitrary snapshot or equal body does not provide that proof. A newer or dirty host, changed location, stale workspace or unavailable proof refuses the handback while retaining recoverable child text. After exact authoritative materialization, the host captures and validates its own local observations for the entire batch before adopting editable buffers. A child's local mutation token is not host write authority; private immutable child proofs remain separate from the rebound host capabilities. A same-version body/owner change or a reentrant local write during publication refuses handoff. Raw close recovery reuses the actual captured manifest; it never fabricates a zero digest or adopts whichever note now occupies the path. Unsaved child text is not silently classified as a saved snapshot.

The four owner decisions are fixed

  1. A note removed elsewhere becomes read-only and shows Archived or removed on another device, with Restore to inbox and Keep my text as a new note. A canonical held deletion also offers Accept cloud removal. Preserve unsaved changes.

  2. Server deletes create versioned tombstones. A stale offline edit receives HTTP 409 and enters the same preserving recovery flow.

  3. When replay rule R3 drops an offline delete because another device edited the note, show a toast once.

  4. Follow a proven move of the same stable note, including an archive destination, while preserving local text. A delete without a proven destination enters removal. The earlier path-move protocol from #7105 remains historical context; canonical reorder and tidy are metadata operations, not rename moves.

A remote deletion is positive information about the identified note and its captured location. It is not a failed read. Rule #2551 remains intact: null reads, download errors and read failures never blank editor text and do not, by themselves, mark a path removed.

Explicit Accept cloud removal uses the captured canonical incoming decision, not a new delete or replacement upload. It seals every preserved editor version, the confirmed baseline, and the exact held-operation bodies before settlement. Changed tombstones, identity, queued operations or local recovery invalidate the choice. The same decision ID resumes an interrupted settlement. The accepted marker and renderer intent retirement share one durable seal; only afterward may the canonical journal compact its reviewed payload. Finish cloud removal retries that final cleanup when needed. The removal record remains read-only so an open preserved editor cannot resurrect the path. Saved local copies remain available. This is local acceptance of the observed cloud tombstone, not a new cloud write acknowledgment. Legacy records without canonical authority refuse this route rather than borrowing path-only authority.

One removal record protects every editor of a note

services/removed-notes.ts keys canonical records by (workspaceId, noteId). The path remains a captured location; legacy path-scoped records do not grant canonical write authority. RemovedNoteState retains the UUID/identity, NoteWriteAuthority, tombstone version, confirmed baseline, per-editor preserved text, local revision and any frozen recovery intent. The intent records its selected text, destination, operation ID and exact held operations chosen for resolution. The original source and a new occupant at the same path are distinct.

Unresolved records are sealed in account/workspace/window-scoped local recovery storage, not a synced workspace file. Hydration completes before ordinary saves enable. Workspace and encryption-generation checks fence async sealing and opening; navigation, unmount, refresh and relaunch do not resolve records. The existing sign-out lifecycle remains responsible for account isolation.

preservedEditors uses stable recovery identities for editor instances, so different unsaved text in two panes is never overwritten by whichever subscriber runs last. Include the held outbox text as its own recovery entry when it differs. lastConfirmedContent is the last server-confirmed content, captured before applying the delete; it is never the dirty editor text. Unknown is null, distinct from a confirmed empty string. revision invalidates async saves/recovery continuations; it is local state, not a server version. tombstoneVersion is monotonic. Repeating an event for the same note/version is idempotent. A later deletion updates the version without dropping preserved text.

State transitions are absent → removed → recovering → absent; failed or superseded recovery returns to removed. absent means no removal record, not that a file exists. Recovery only clears the chosen preserved entry after the replacement write is acknowledged and routing is installed. Keep the record while another editor still has unresolved text. Dismissal hides UI; it never resolves or deletes the record. Reopening the note or its recovery entry presents the preserved text. A remote restore/upsert also does not silently clear a record: retain unsaved text and present the existing incoming-edit conflict flow until the user chooses. Stale removal events at or below a successfully resolved version must not reopen the same recovery.

deletedNoteSlots remains a transient, workspace/UUID-scoped list latch that refresh may clear. It is not the durable recovery store. Remote removal must not blank an editor or automatically create a replacement at its old path, including when it had the highest display number.

Delete feeds enter the same state

FeedProducer and consumerRequired behavior
Committed deleteworkspace-core/store.ts through workspace/index subscriptions and the shared removal serviceAccept { action: "delete", origin: "remote" }, capture editor text and confirmed baseline before reconciliation, and enter removal. Local user deletion keeps its existing semantics.
Suppressed deletecloud-sync-bridge.ts:applyRemoteRows through IncomingWorkspaceConflict and external-note-conflict-model.tsaction: "delete" enters the same state even though a queued local upsert prevented a committed workspace mutation. Never forge a committed workspace event for this case.
Rejected upload against a deleted headcloud-sync-bridge.ts upload 409 handler through IncomingWorkspaceConflictEmit action: "delete", uploadRejected: true, version: tombstoneVersion, retain queued text and enter removal even if no editor was mounted when the rejection occurred.

Bridge events carry canonical noteIdentity when they describe registered notes. Consumers retain the original note's authority even when a conflicting server row now describes a different occupant. An optional legacy moveCandidate alone does not prove canonical identity. Register removal before clean-buffer adoption and do not release a deleted note's held upload through ordinary Keep Mine; explicit removed-note recovery owns that transition.

Save guards cover pending and already-started work

On removal, synchronously block saves and advance revision, capture text, cancel debounce and mark caches removed before asynchronous work. All entry points consult the record: autosave, blur/unmount flush, navigation flush, named notes, Note Tray caches and popouts. A cache can retain recovery text but must never return it as an ordinary live writable note. Remote removal is not implemented by clearing text.

A save captures its base provenance and revision when the edit is queued. Recheck before writing and after every awaited step that can enqueue a write. An older continuation must not clear newer dirty/recovery state, update the confirmed baseline, or enqueue another save. An upload already on the wire cannot reliably be canceled: the server checks its captured base and identity/location, and a rejected operation remains durably held. If it committed before the delete, the later delete wins. Never neutralize a raced write by blindly deleting the path again: another device may have restored it in the meantime. Timer cancellation alone is insufficient; the server precondition and durable hold are part of the same guarantee.

Historical file/path move protocol

This section describes the earlier, noncanonical file lane. Its paired uploads, rename cycles and path headers are not substitutes for notes.moveById or notes.reorderById. Canonical notes use the atomic ID/location protocol above; matching bodies never prove identity in either lane.

#7105 replaces #6945's same-batch content-match heuristic. A destination PUT carries X-Moved-From and optionally X-Moved-From-Version; a retired-source DELETE carries X-Moved-To. Pull rows expose movedFrom and movedTo. These are client-supplied path identities, not a server inference from equal text. The bridge decrypts and validates the paths, downloads and verifies destination content, then classifies the normalized pull batch before dispatching committed workspace changes or suppressed incoming conflicts. Empty or duplicate-content notes may follow because content equality is no longer the identity test. Archive destinations are eligible.

The outbox preserves the original source and its known version while composing repeated local moves. For a chain A → B → C, it redirects the queued source delete toward C, and the terminal destination upsert still identifies A. The receiving bridge composes repeated moves within its normalized pull batch: a delete at A can follow the terminal verified upsert at C only when that upsert still proves movedFrom: A; a path merely occupied at C does not prove identity. A counterpart arriving in a later drain, an unreadable destination, a cyclic or unproven chain, or a destination with local queued/dirty work leaves the source in the existing Archived or removed on another device state. Its text stays available, while saves to the old path remain blocked; a later matching upsert can complete a pending move. A successful follow rebinds the open note to the verified destination, retains unsaved text and keeps it editable using the destination's version.

Reorder is a rename cycle: its destination upserts can displace different notes already open or dirty at those slots, even when no path is deleted. The bridge holds a displaced local edit durably before applying the remote slot content and transfers it only to its proven destination; it never uploads that text over the incoming note. The renderer keeps pending and displaced editors under the same preserving banner until a safe route is installed. Each editor and popout keeps its own text. If a destination is still dirty, held or unresolved, the move remains pending rather than overwriting that work.

Cross-workspace move is copy + retire

A transfer to another workspace of the same account remains a copy followed by conditional retirement, not a same-ID move. The destination gets a new client UUID and a new destination-workspace number; source history stays with the source's tombstone. The full captured body is copied without formatting or auto-field changes. Source and destination never share write authority.

  • Separate encryption and ownership. services/other-workspace-files.ts opens a destination session without switching the active workspace. Paths, body and assets are encrypted with destination keys. An unopened workspace uses the shared complete-inventory enrollment helper, not a separate protocol.

  • Durable offline intent. Before a network mutation, the source journal seals the captured body, asset bytes, source authority, new destination UUID, creation intent and copy/retirement operation IDs. Offline or unavailable destinations leave a copy-pending record and the source intact. Destination provisioning, access and unlock checks still apply before delivery.

  • Assets fail closed. References must resolve. Identical asset bytes may be reused; a conflicting name or missing reference refuses the copy without retiring the source. Ciphertext is never reused across workspace keys.

  • Exact retry. Destination allocation uses its own operation ID, distinct from the initial content upload. Prepared ciphertext is sealed before upload. Retry uses the same recorded request and receipt, never hash-only adoption of some matching destination note or allocation of another UUID.

  • Retire last. Only a proven destination acknowledgment permits a conditional notes.deleteById with the captured source authority and expected source text. A changed source remains available alongside the destination. Retirement stays pending until the exact source operation receipt is acknowledged; absence from a path list or outbox is not proof of deletion.

services/cross-workspace-move.ts persists copy-pending → copy-confirmed → retire-pending → completed, with conflicts retained for explicit retry. Completion prunes the record. Workspace/generation checks fence every resumed operation. Shared journal updates use an account/workspace Web Lock, a fresh load and per-record revision checks, preserving unrelated transfers. A platform without safe cross-window locking refuses the mutation and retains source text; native availability of that capability requires separate verification.

lib/execute-note-move.ts routes same-workspace and cross-workspace actions through their respective APIs. A queued copy does not report the source as removed. The module-scoped service is reconciled on mount/resume and disposed on sign-out; those lifecycle events cannot change the recorded source authority.

Tombstone heads preserve the concurrency boundary

The file/path primitive helpers/workspace-tombstone.ts:tombstoneWorkspaceFile is the shared atomic deletion primitive used by the DELETE route in handlers/file-handlers.ts, automation callers, and delete pushes in handlers/sync-handlers.ts. A transition from a live version v to deleted allocates v + 1, updates files.is_deleted, inserts a blobless file_versions row and a matching changes delete row in the same D1 transaction. It must use the database head, not a stale pre-read file.version, and preserve existing push conflict checks inside the atomic mutation. A repeated delete of the current tombstone is an idempotent no-op; deletion of a never-existing path is also a no-op.

Every committed head, live or deleted, has exactly one matching file_versions row. A tombstone row has an explicit delete discriminator, empty content hash, size zero and no R2 blob (r2_key = NULL). Update schema, SQL/types and history readers for this representation. A live version always has a downloadable blob. Upload SQL must key dependent version/change inserts to the mutation from that request; a rejected upload must not manufacture a version row because retention happened to remove one.

handlers/retention-policy.ts and its retention service protect the current version of every file, including tombstones. History pruning and change-log compaction may remove older history subject to checkpoint pins, but never delete a tombstone head or its files identity/version. Exclude null blob keys from R2 reference/reclaim work. History represents tombstones as deletion states, never attempts to download their null blob, and checkpoint/version restore creates a new live head with a version greater than the tombstone. Restoring an old checkpoint does not reset the version counter. No move identity is inferred on the server.

Historical automation move event versions and retired heads are separate

An automation move preserves the moved file identity at the destination and appends the old-path delete followed by the destination upsert at the moved identity's current version n. Its response and both change rows use n. The retired source is a different file identity with a blobless tombstone head at n + 1. That head version is the precondition for a deliberate source recreation; the move event version is not. A client that tries recovery using the event version receives a conflict containing the actual retired head and keeps the edit held for an explicit choice. It never silently rebases the edit.

The retired source's history initially contains only its synthetic tombstone, whose changeCursor is null because the move events belong to the moved identity. A deliberate recreation adds a new source-path version and cursor; the destination keeps the moved identity's prior history. A destination version ID cannot be restored through the retired source path. Checkpoint manifests continue to project the identities recorded by the change log, but an old-path entry that names the moved identity's history is not a promise that that version can be restored through the now-retired path.

Base zero means the path never existed

file-handlers.ts:baseVersionMatches and the atomic SQL gate must implement the same rules:

RequestCurrent headResult
baseVersion: 0No path identity has ever existedCreate version 1.
baseVersion: 0Live or tombstone head409; zero cannot recreate a deleted path.
baseVersion: v, positiveCurrent head version equals vWrite version v + 1; matching a tombstone is the explicit intentional recreation path.
baseVersion: v, positiveHead differs or path is absent409.
Missing base preconditionTombstone head409; legacy/unconditional uploads cannot bypass removal protection.

These base-version rules describe the file/path API. Canonical creation uses a client UUID, permanent number reservation and the sealed create operation; it does not probe numbered slots or fall back to a local numeric allocator on a probe timeout. Canonical writes additionally require the correct note ID and location revision. A known tombstone base alone cannot authorize an unrelated note to overwrite that identity. Lost-ack success is established by the exact operation receipt; matching bytes at a path are insufficient.

The historical file API returns HTTP 409 with the following shape. Canonical note routes can instead return identity/location-specific NOTE_* conflict codes with head metadata; clients must preserve those distinctions:

{
  error: "Base version is stale";
  code: "BASE_VERSION_MISMATCH";
  version: number; // current head, or 0 for a genuinely absent path
  isDeleted: boolean; // true only when the current head is a tombstone
}

Keep version for existing file conflict readers; cloud-sync transport error parsing retains code and isDeleted. A false isDeleted follows the existing edit-conflict flow, never the removal banner. Only authoritative delete rows or isDeleted: true prove removal; a 404, missing snapshot row or network error does not.

Every file DELETE carries the required X-Base-Version captured with the local action. An absent path uses base zero only when the source was deliberately created locally. Repeating a DELETE against a tombstone with its prior version is idempotent; replaying it after the path has been recreated conflicts and leaves the live head untouched.

Deletes keep the authority seen by the local action

Before a local removal changes the workspace model, the bridge captures its write authority in the durable operation. Canonical deletes retain the UUID, path, content base and location revision; canonical same-workspace moves are atomic. The older file/path lane described in the following replay examples has two independent move operations: a destination upsert and a conditional source delete. If A queued a move from source version 1 while offline and B later writes source version 2, A's resume pull applies B's upsert and durably drops only A's stale source delete before replay. The destination can still upload, leaving B's edited source and A's moved copy intact. The lost-delete notification is emitted once after that drop commits.

If the conditional DELETE reaches the server before that pull observes B's write, its atomic base-version check returns 409 BASE_VERSION_MISMATCH without changing the live head, history, blobs, change rows or notifications. The bridge seals the latest verified incoming text with the rejected delete and restores that Review item after reload. Delete Again is the explicit choice to persist one new base from the observed server version and retry; another concurrent write causes a fresh conflict. Restore Incoming first durably removes the queued delete, then keeps the verified server text in the workspace. A storage failure leaves the prior delete and Review state in place. Unknown delete authority is held without sending a DELETE, and a local new path uses base zero only when its creation intent was captured.

A successful in-flight PUT may advance a replacement delete's base only when it is the queued predecessor on the same base lineage. The bridge seals that version before retrying the DELETE. If sealing succeeds but writing the updated queue fails, the old sealed base remains queued and the DELETE retries against it; the next server conflict enters the normal actionable Review flow. A delete queued behind a restored, adopted, moved or held-text transfer upsert does not advance: that predecessor carries no provable lineage, so the delete keeps its captured base. The server's check then rejects it, and it waits for an explicit Delete Again or Restore Incoming choice. It never completes on its own. Cloud sync lists it as a held delete that is waiting for confirmation (#7589).

The same rule covers a replacement upsert (#7539). Text typed while this window's own PUT is on the wire inherits that PUT's reserved base. The PUT's acknowledgment advances it to the version that PUT minted, so the edit is sent instead of being held against the client's own text. The bridge reports that version only for an acknowledgment its current arm recorded. The outbox keeps it per in-memory base reservation (baseLineage). The lineage is minted when a base is chosen, copied when a later edit inherits it, and never persisted. A removal, re-creation, explicit rebase (even to the same version), restart or adoption therefore starts a new lineage that no acknowledgment covers, and an adopted row, rebased or not, never records one. A queued edit on the acknowledged lineage that was captured after the acknowledged edit and still carries its base advances when the acknowledgment lands, when it commits after a seal that straddled the acknowledgment, and again just before it is sent. It must carry no held conflict, recovery operation, transfer or move, and it records the acknowledged text as its confirmed baseline. It never adopts a pulled or probed server head. A lost acknowledgment, a held or versionless result, a different lineage, or a storage failure (which also drops that lineage's record) keeps the captured base, and the server's check then returns the usual conflict with the local text preserved. A revision from another device after the acknowledgment is still a conflict. An edit typed during a move's or held-text transfer's PUT keeps the conservative conflict path: that PUT wrote text the destination's editor did not show, and its move metadata would otherwise be replayed. An advance also yields to a rebase or an in-place move redirect that lands while it seals.

Replay keeps the base that the edit was made against

The following describes the existing file outbox and its reconciliation policy. Canonical note operations additionally persist UUID and location authority in packages/cloud-sync/src/note-outbox.ts; they never reconstruct authority from a path snapshot. The file outbox introduced by #6951 extends packages/cloud-sync/src/outbox.ts:OutboxUploadPayload, persisted rows, SyncOutbox, orphan adoption and bridge enqueue sites with:

type EditBase =
  | { kind: "known"; version: number } // positive server-confirmed version
  | { kind: "new" } // deliberately allocated creation; upload with base 0
  | { kind: "unknown" }; // hold; never upload without a precondition

Use the field name editBase. It belongs to the queued operation, including queued deletes for R3 comparison, and survives encryption/persistence, queue replacement, retry, orphan-window adoption and both boot paths. Coalescing edits preserves the original base until an acknowledged write or an explicit user conflict resolution establishes a new base. Mirror boot and snapshot fallback must not infer new from a queued path missing in the snapshot. The confirmed baseline remains relevant to edit conflict handling; explicit move identity no longer depends on content matching.

cloud-sync-bridge.ts:uploadBaseVersionFor consumes editBase, not whatever version a later pull placed in the workspace store. known uploads its positive version, new uploads zero, and unknown stays held pending a real baseline or explicit recovery choice. Fetching a newer head alone must never rebase an unknown edit onto it. An intentional restore is a user-authorized new operation with the current tombstone version, not a silent mutation of the stale entry's base.

A tombstone 409 leaves the edit persisted and held (OutboxUploadResult.status = "held") and registers an unresolved delete conflict before notifying listeners. Restore of held status on relaunch must occur before timers can flush. A subscriber mounted later can obtain the unresolved removal. Resolution only retires the old held entry after the replacement has been durably queued and successfully acknowledged; retries and double clicks cannot create duplicate recovery notes.

Pull and reconcile complete before replay starts

#6951 adds the exported reconcileBeforeReplay(): Promise<void> orchestration seam in cloud-sync-bridge.ts. It serializes/coalesces resume attempts, pauses normal timer and manual outbox drains (including deletes), performs the authoritative pull and durable reconciliation, registers removals/holds and completes R3, then permits/expedites replay. Boot and reconnect use the same barrier; adding a pull only to a UI resume callback is insufficient. A successful reconciliation means the completed pull reports synced, has no access failure, and has no per-file failures. Durable conflict holds may coexist with that result. An offline or idle result, or a result with per-file failures, keeps replay gated until a later successful pass. Per-file failures advance the cursor by design, so a retry can complete without those failed rows and release replay after one held pass. Respect arm/workspace generation so an old resume cannot release a new workspace's queue. Already-started PUTs retain their original preconditions.

#6955 uses that seam from use-resume-sync.ts:useResumeSync, enables it in app.tsx on web, desktop and mobile, and covers visible visibilitychange, online and supported native resume events through lib/app-resume.ts:subscribeAppResume. Probe the socket, but a healthy socket still requires a pull. packages/cloud-sync/src/ws-manager.ts:WsManager extends its existing resume probe/pong timeout machinery to regular heartbeat pings: missing pong closes/reconnects once, and reconnect invokes the same pull-before-replay barrier. Clear timers on pong, close and dispose; concurrent visibility, online and heartbeat signals share one reconciliation.

R3 retains its drop-before-apply ordering in applyRemoteRows: after dropQueuedTombstone durably removes an offline delete for a remotely edited note, #6951 emits lost-delete with { workspaceId, path, remoteVersion }. #6954 adds addLostDeleteListener and the renderer subscription if not already exported. Emit only after a successful durable drop, once for that discarded operation; duplicate pulls/retries must not toast twice. A failed drop prevents cursor advance and replay. #6954 owns that small follow-on bridge edit after #6951 merges.

Recovery is explicit and keeps every version of the user's text

RemovedNoteBanner and removed-note-recovery.ts act on the explicitly selected preserved text. Repeated activation is blocked while recovery is in progress.

  • Restore to inbox calls notes.restoreById with the original UUID and captured tombstone authority. It retains the permanent number and history. The destination is frozen for retry; an occupied basename may require a UUID filename, never overwriting a different note. A live or changed source does not silently become a tombstone restore or a latest-head overwrite.

  • Keep my text as a new note freezes a new UUID, operation ID and creation intent, leaving the source identity/history intact. Its assigned number comes from the workspace allocator, not a scan of numbered filenames.

  • Both choices retain their durable intent until acknowledgment, exact receipt snapshot adoption and routing are complete. A conflict or failed proof keeps the text and the original intent; a retry cannot allocate a duplicate or silently refresh the captured authority.

Explicit recovery may supersede only the exact held operations selected with that preserved body. Their operation ID, note ID, path, edit base and location revision are captured before recovery and sealed with its intent. A successful same-ID restore, or an acknowledged Keep-as-new followed by explicit settlement, retires those selected records. It never clears all held operations for a note; newer work and distinct other-window copies remain. Failure leaves the original held text recoverable.

The shared canonical buffer, removal store and popout relay all retain the UUID. Removal/recovery messages carry stable identity and version metadata rather than letting a receiver resolve the current occupant of a numbered path. Each window keeps its own captured edit authority and preserved body. A handshake provides unresolved removals to newly mounted popouts. Echo/version suppression and workspace guards still apply. Routing a recovery cannot discard another window's different text, and a disconnected old window cannot write through the server's identity/location precondition.

Wording uses note and device-neutral terms

Reviewed against .claude/skills/l-domain-vocabulary/SKILL.md through ccref. Use note, Note Tray for the frame, and Note Tray toolbar only for its toolbar. Existing draft* code identifiers stay unchanged.

SurfaceExact English text
Removal bannerArchived or removed on another device
Recovery actionsRestore to inbox · Keep my text as a new note · Accept cloud removal (canonical held deletion)
Lost-delete toastYour delete of “{noteTitle}” was skipped because the note was edited on another device.
Existing conflict toast in write-page.tsxThis note changed elsewhere. Review the incoming changes.

Render noteTitle as plain text using the normal note title/fallback number, never raw HTML. “Elsewhere” covers another tab, window or device. The owner-selected banner remains verbatim. #6954 updates assertions for the former “another window/tab” wording; #6956 writes the corresponding Japanese manual and checks vocabulary consistently. UI must not expose tombstones, version numbers or outbox terminology.

Fifteen-scenario verification map

The paths and test titles below identify the historical #6945 deterministic proofs. “Passed” means the focused suite ran on that divergence epic branch; the final full-suite and browser outcomes are recorded after the integration pass. Mock browser results do not count as native two-device evidence. The macOS app/Safari pass remains #6958. The move behavior in D04–D06 below was superseded by #7105; the table preserves what those earlier tests proved, rather than claiming they verify the newer protocol.

IDDevice A / remote operation / surfaceExpected behaviorLanded proof and result
D01Foreground, saved note / archive / main editorPreserve text, banner, no write to source.tauri-app/renderer/hooks/__tests__/use-draft-manager-removed-note.test.tsx — “D01 remote archive preserves the open note and blocks saves” (passed); e2e/cross-device-removal-recovery.spec.ts — “committed removal recovery actions” (mock Chromium and WebKit passed).
D02Foreground, debounce pending or save started / trash / main editor, including highest slotCancel pending save; stale PUT fails; no replacement slot at source.tauri-app/renderer/hooks/__tests__/use-draft-manager-removed-note.test.tsx — “D02 remote trash cancels debounce and fences in-flight saves; highest removed slot is not recreated” and “D02 in-flight switch flush does not enqueue a trailing write after removal” (passed); workers/sync-server/test/files-base-version.test.ts — “stale PUT after tombstone is rejected” (passed).
D03Foreground, dirty queued edit / archive / main editorSuppressed delete enters persistent removal, queue held.tauri-app/renderer/hooks/__tests__/use-draft-manager-removed-note.test.tsx — “D03 suppressed delete preserves dirty text across refresh and navigation” (passed); packages/backend-bridge/src/cloud-sync-bridge.outbox.test.ts — “D03: a suppressed remote delete keeps dirty text and its removal feed across re-arm” (passed); e2e/cross-device-removal-recovery.spec.ts — “queued-save removal conflict” (mock Chromium and WebKit passed).
D04Foreground / archive-tidy renumber / main editorFollow only unique same-batch confirmed-content match; keep dirty text.tauri-app/renderer/hooks/__tests__/use-draft-manager-removed-note.test.tsx — “D04 unique non-archive move rebinds without losing unsaved text” and “D04 suppressed move uses the same verified route after held edit transfer” (passed); packages/backend-bridge/src/cloud-sync-bridge.outbox.test.ts — “D04: a unique non-archive same-batch move follows the confirmed baseline and transfers held text” (passed).
D05Foreground / ambiguous renumber or archive / all editorsEmpty, duplicate, dirty-destination, archive and split-drain candidates show banner.tauri-app/renderer/hooks/__tests__/use-draft-manager-removed-note.test.tsx — “D05 ambiguous and archive moves remain removed” (passed); packages/backend-bridge/src/cloud-sync-bridge.outbox.test.ts — “D05: archive and duplicate-content destinations never receive a move candidate” (passed).
D06Foreground, clean and dirty notes / reorder / Note TrayOrdinary upsert adoption/conflict; never infer delete or move.tauri-app/renderer/hooks/use-notes-tray-store-removed-note.test.tsx — “D06 remote reorder uses upsert conflicts without removal” parameterized clean/dirty cases (passed).
D07Background tab or suspended app / archive or trash / main editorOn resume, pull/mark removed before any queue replay; preserve text.tauri-app/renderer/hooks/use-resume-sync.test.tsx — “pulls and reconciles before probing the socket on resume” (passed); tauri-app/renderer/lib/app-resume.test.ts — “fires on the native activity resume and on reconnecting to the network” (passed); packages/backend-bridge/src/cloud-sync-bridge.outbox.test.ts — “resume cannot replay delete before R3” (passed). Native sleep/wake remains #6958.
D08Socket silently dead / trash / open main editorHeartbeat timeout reconnects once, then pull before replay and banner.packages/cloud-sync/src/ws-manager.test.ts — “reconnects once when a regular ping receives no pong” (passed); packages/backend-bridge/src/ws-bridge.test.ts — “starts the reconciliation barrier immediately on connect” (passed).
D09Offline with queued edit / archive / main editorOriginal base survives, stale write held, removal visible.packages/backend-bridge/src/cloud-sync-bridge.outbox.test.ts — “D09/D10: an offline edit retains its original base and removed conflict across a mirror restart” (passed); workers/sync-server/test/cross-device-divergence-sequence.test.ts — “restart tombstone recovery sequence” (Miniflare/bridge focused pass: 1 test).
D10Offline across relaunch / trash / main editorBoth mirror boot and snapshot fallback retain provenance and text.packages/backend-bridge/src/cloud-sync-bridge.outbox.test.ts — “D09/D10: an offline edit retains its original base and removed conflict across a mirror restart” and “D10: snapshot fallback overlays a missing queued path without changing its known base” (passed).
D11Offline queued delete / remote edit / main editor or Note TrayR3 durably drops delete before replay, retains remote note, toasts once.packages/backend-bridge/src/cloud-sync-bridge.outbox.test.ts — “lost delete emits once after a durable R3 drop, with failed-drop retry and duplicate-pull deduplication” and “resume cannot replay delete before R3” (passed); tauri-app/renderer/components/lost-delete-notification-host.test.tsx — “shows the confirmed note title once for the R3 event” (passed).
D12Foreground / trash / Note Tray, secondary Note Tray, split paneEach instance preserves its text, blocks saves, invalidates writable cache.tauri-app/renderer/hooks/use-notes-tray-store-removed-note.test.tsx — “D12 removed note guards every tray and split cache” parameterized over all three surfaces, plus “D12 keeps different split buffers under their own recovery identities” (passed).
D13Foreground, unsaved named note / archive or trash / named editorImmediately block autosave, preserve text, both recovery choices work.tauri-app/renderer/hooks/__tests__/use-named-file-editor.test.tsx — “named removal preserves text and both recovery choices” parameterized matrix (68-test file passed); tauri-app/renderer/view-providers/inbox-provider.test.tsx — “routes Restore for a removed named inbox note through its stable editor identity” (passed).
D14Popout active or opening / remote archive / popoutRelay/snapshot installs removal; no upload; different pane text survives.tauri-app/renderer/lib/popout-removal-relay.test.ts — “D14 popout removal relay and late snapshot block writes” and late-delete/restore snapshot cases (34-test focused subset passed); tauri-app/renderer/hooks/__tests__/use-draft-manager-removed-note.test.tsx — preexisting popout snapshot capture (passed). Native windows remain #6958.
D15Foreground or reconnect / remote edit, then restore or unarchive / dirty editor and removed editorExisting edit conflict preserves text; remote reappearance never auto-releases stale edits; explicit recovery produces one note.tauri-app/renderer/services/external-note-conflict-model.test.ts — “D15 remote restore never auto-adopts over preserved changes” (passed); tauri-app/renderer/services/removed-note-recovery.test.ts — exact-once Restore, Keep-as-new and retry cases (passed); tauri-app/renderer/hooks/__tests__/use-draft-manager-removed-note.test.tsx — “D15 read failure preserves text without removal” (passed).

The protocol invariants also have landed proofs:

  • workers/sync-server/test/sync.test.ts — “automation and sync deletes advance tombstone versions” (Miniflare suite passed).

  • workers/sync-server/test/files-base-version.test.ts — “stale PUT after tombstone is rejected”, “base zero cannot bypass a retained tombstone; matching base intentionally restores”, “retention preserves tombstone heads”, and “checkpoint restore crosses tombstones monotonically” (Miniflare suite passed).

  • packages/backend-bridge/src/cloud-sync-bridge.outbox.test.ts — “unknown edit bases stay held while deliberate new files send base zero” and “a direct write with no confirmed metadata defaults to unknown, never base zero” (focused durable-outbox suite: 95 passed).

  • workers/sync-server/test/cross-device-divergence-sequence.test.ts — “restart tombstone recovery sequence” (real bridge/Hono/Miniflare focused test: 1 passed).

The final integration pass passed the affected package, Tauri and sync-server suites in CI and both mock Chromium/WebKit browser cases locally. The native macOS two-device pass belongs to #6958; the focused Computer Use recipe is in .agents/skills/l-cu-test/references/walkthrough.md.

Historical conditional-delete confirmation (#7205)

This historical confirmation records the earlier file/path producer and replay proofs. Its rename-based tidy/reorder examples are not the canonical metadata operations described above, and it does not claim current identity or native two-device verification.

SequenceProof
A moves a source at v1 offline; B edits that source to v2; A resumesworkers/sync-server/test/cross-device-divergence-sequence.test.ts — “A resumes a v1 move after B edits the source to v2 and preserves both texts”. It uses the real CloudSyncClient, Hono routes and Miniflare D1/R2, checks pull-before-replay, one lost-delete notification, the unchanged v2 source and the uploaded destination.
B edits after A's resume pull but before A's DELETE reaches the serverThe same file's “a B edit after A's resume pull rejects the source DELETE and preserves both copies” test asserts that A sends base 1, B creates live v2, the real DELETE returns 409 with version: 2, isDeleted: false, and both B's source and A's destination remain. The isolated storage race is also covered by workers/sync-server/test/files-base-version.test.ts — “DELETE rejects a write interposed between its read and atomic batch”.
Archive, tidy-up, reorder, and rapid A→B→CThe same file's archive, tidy-up, identical-content reorder, and offline A→B→C tests assert the ordered delete/upsert rows and resulting note text.
Rebase a stale delete, restart, and preserve the new choicepackages/cloud-sync/src/outbox.test.ts — “clears a rejected delete conflict from its sealed payload before rebase reload”. The bridge suite also covers “holds a stale delete across reload until an explicit delete-again choice” and “a write after Delete Again creates a fresh held conflict”.
Incoming restoration cannot report success before durable delete removalpackages/backend-bridge/src/cloud-sync-bridge.outbox.test.ts — “Restore Incoming stays pending when retiring its stale delete cannot persist”. It injects a storage failure, restarts from the retained row, then proves the incoming copy can be restored after persistence recovers.
Own in-flight PUT acknowledgement cannot strand a replacement deletepackages/cloud-sync/src/outbox.test.ts — “retries the sealed base when persisting own-ack delete authority fails”. The old base is retried and the resulting conflict is persisted as an actionable held entry.
Own in-flight PUT acknowledgement advances a successor edit, and only thatpackages/backend-bridge/src/cloud-sync-bridge.outbox.test.ts — “a successor edit queued behind its own in-flight PUT (#7539)”: sent on base 2 after the own ack, across a restart; a foreign revision after the ack and a lost ack stay held with the text kept, and an ack landing after its arm went stale reports no version. packages/cloud-sync/src/outbox.test.ts covers held, versionless, skipped, lost-ack, rebased (including to the same version), re-created, moved, moved-predecessor, held-upsert-successor, restored, held-delete, straddling-seal, sealing-at-ack, removal-during-advance, rebase-during-pre-send-advance, rebased-adopted, transfer, late-older-seal and persist-failure cases.

The remaining fixed-matrix cases are covered by existing focused tests: conditional DELETE retries after network failure, 404 and source recreation (workers/sync-server/test/files-base-version.test.ts, packages/backend-bridge/src/cloud-sync-bridge.outbox.test.ts); unknown and deliberate-new bases, queued/in-flight upsert replacement and popout adoption (packages/cloud-sync/src/outbox.test.ts, packages/backend-bridge/src/cloud-sync-bridge.outbox.test.ts); and the real local delete producers, including archive, tidy, reorder and ordinary delete (packages/backend-bridge/src/workspace-core/notes.test.ts, packages/backend-bridge/src/cloud-sync-bridge.outbox.test.ts).

Counted conflict review and local settlement

Canonical review snapshots retain the reviewed workspace/UUID and captured write authority. A server occupant at a reused path has a separate identity; it cannot become the owner of the original local text. Back-to-note navigation uses the captured reviewed identity, not a path lookup after the dialog awaits. The attentionPaths naming below also serves non-note file conflicts; it is not permission to treat a path as canonical note identity.

For canonical notes, workspaceFiles.captureIncomingConflict captures the local snapshot, full identity metadata and pending-operation set before the choice. resolveIncomingConflict receives that capture and a stable decision operation ID, and returns the exact adopted or locally durable snapshot. Load Incoming applies the reviewed server version without another upload or version increment. Keep Mine stages the selected bytes against the explicitly reviewed authority; the held originals remain until the replacement receives its real server receipt. A different UUID at the same path cannot substitute for the reviewed note. Settlement returns the validated post-materialization observation and a writer bound to its decision ID, so a subsequent ordinary save can continue. Keep Mine seals that same writer into its reviewed operation; explicit buffer adoption keeps it through edits queued before the acknowledgment. Ordinary clean rereads still retain their existing logical buffer writer. A later same-version local mutation or a superseded async completion cannot lend that observation to different bytes. Replaying a sealed decision compares durable identity and the original/result bytes; its physical-state comparison does not mistake an independent reader ID for changed note ownership.

The existing encrypted note journal retains prepared and materialized review records separately from server receipts. Exact decision bytes and retired held payloads survive a crash or orphan-window adoption; the existing recovery flow discovers them through getPendingIncomingDecisions. Durable renderer completion allows releaseOperationSnapshot to compact the payload, retaining its decision fingerprint. Reusing an operation with different input, newer pending work (including same-body edits), or changed identity metadata refuses stale replay. An old path's higher version alone cannot retire a canonical decision.

An editor decision lease invalidates in-flight reads and pauses index-triggered refreshes. Receipt adoption checks the original buffer, revision, body, authority and observed index version/location. A later edit stays recoverable with its original authority; releasing the lease must not silently rebase and save it.

Cloud sync Review, note-level Review, pending-item links and the existing Show Unresolved Sync Conflicts command open one SyncConflictReviewHost. Its attentionPaths count is authoritative and independent of upload delivery. An explicitly requested missing path is explained, never replaced by a different note. Background arrival does not open a dialog. Closing, Escape and Decide later leave work and saved copies intact.

The shell embeds TypedDiffViewerPane: incoming is the NEW/added role, local is OLD/removed, and swapped columns show This device | Cloud. Inline rendering uses the same model on mobile. No general Diff View file reload or nested diff dialog participates. Ordinary text starts with neither version selected; selection and application are separate. Settings selects a complete version. Local deletions retain Delete Again / Restore Incoming. Remote removal and unknown-base holds retain their existing recovery authorities rather than pretending to be empty text.

compareSyncConflict captures exact editor snapshots and owner identities, or the scoped unowned comparison. executeSyncConflictDecision owns a per-path semaphore and rechecks workspace, encryption generation, full incoming payload (including same-version replacement), the entire owner set and local bytes/revisions. Owners fence and drain ordinary saves before one owner commits the selected text. Buffer adoption follows synchronously after validation. Different local drafts require an explicit selected draft and are preserved before replacement. The guarded workspace write checks its owner predicate before mutation and after asynchronous work.

Incoming decisions seal the local safety copies and continuation before bridge settlement, then materialize and finalize. Local owned decisions persist the exact selected draft before rebasing its held upload. Failures keep their stage and recorded choice; retries cannot choose a competing version. Ordinary writes made while a recorded continuation is pending preserve the new text as scoped local recovery instead of overwriting that continuation. Failed persistence never advertises a newly inserted copy as saved.

The result and content now in use remain in Review. Next conflict advances explicitly; asynchronous completion never closes or advances the dialog. Saved local copies reads the existing account/workspace/window-scoped encrypted records and copies one exact text. It remains reachable from normal Cloud sync and the note at zero conflicts. This replaces actionable conflict toasts, the editor handoff, separate conflict diff and post-resolution recovery popup; unrelated notices remain unchanged.

Local settlement is not cloud acknowledgment. Upload pending/offline state and unrelated failures remain owned by Cloud sync. No review timeout, empty list or bridge callback proves delivery. Native window, IME, live-cloud and broader hidden-sibling recovery evidence remain separate from browser mock coverage.

A held rejected upload keeps one review identity: the newest verified server head. When a pull suppresses a newer foreign upsert behind that hold (another device saved the same file again, typically .zudotext.settings.json), the bridge seals the newer head into the hold and replaces the unresolved conflict before notifying listeners. Without that, the renderer's latest identity and the unresolved set disagree, and every Compare fails as "changed" even after Review again (#7493). The comparison already open becomes stale and one explicit Review again compares the newer head; reload restores the newer head from the sealed hold. Only a still-held entry with a known edit base qualifies: an unknown-base hold was never sent or refused, and an entry an explicit choice released or re-based is left to its own PUT, which surfaces the newer head. The pull reads the sealed record with the queued entry, and one rule (adoptUnresolvedConflict: a newer version wins, an equal version takes the later record's flags) maintains the unresolved registry for the pull, the delete feed and the relaunch restore, so a pull that races the restore cannot pin the review to the older head. A seal refused because a newer local edit replaced the still-held entry is resealed once against that entry's base. If storage keeps refusing, the review still moves to the newer head for the session; relaunch restores the older sealed head, and Keep Mine against it is refused and surfaces the newer head again. A held local delete is different: R3 drops its tombstone in favour of the newer foreign upsert, so the bridge also retires its Delete Again / Restore Incoming review. For non-note files, a saved Load Incoming choice whose path the local model already holds at a newer server version is retired on relaunch and on Review, keeping its preserved local copies, because materializing the older incoming would overwrite newer text.

A newer explicitly reviewed conflict can supersede an interrupted Load Incoming even after its volatile entry was invalidated. Before releasing/rebasing that newer upload or preparing its incoming choice, the coordinator locates the old durable intent and retires only that exact record through the existing sealed write chain. Retirement checks account/workspace/window, encryption generation, canonical path and record identity again after sealing; a replacement at the same path is an error, not absence. The old record stays live until its omission is durably written, so failure needs no rollback and cannot overwrite a replacement. Every safety copy remains. After awaited retirement the coordinator rechecks the review, ownership and local bytes before settlement. Restart before retirement retains the retryable old intent; restart after retirement retains the newer held upload; acknowledgement is still required to establish cloud delivery. A still-current interrupted incoming choice keeps its original retry behavior.