omnigraph/docs/rfcs/rfc-022-unified-write-path.md
Andrew Altshuler 58defb4c51
Some checks failed
CI / Classify Changes (push) Has been cancelled
CI / Check AGENTS.md Links (push) Has been cancelled
CI / Container Entrypoint (push) Has been cancelled
Release Edge / Prepare edge release (push) Has been cancelled
CI / Test Workspace (push) Has been cancelled
CI / Test omnigraph-server --features aws (push) Has been cancelled
CI / RustFS S3 Integration (cli) (push) Has been cancelled
CI / RustFS S3 Integration (cluster) (push) Has been cancelled
CI / RustFS S3 Integration (engine) (push) Has been cancelled
CI / RustFS S3 Integration (failpoints) (push) Has been cancelled
CI / RustFS S3 Integration (server) (push) Has been cancelled
Release Edge / Build edge omnigraph-linux-arm64 (push) Has been cancelled
Release Edge / Build edge omnigraph-linux-x86_64 (push) Has been cancelled
Release Edge / Build edge omnigraph-macos-arm64 (push) Has been cancelled
Release Edge / Build edge omnigraph-windows-x86_64 (push) Has been cancelled
Release Edge / Smoke Windows installer (push) Has been cancelled
Fence Optimize against late recovery intents (#347)
* Fence optimize against late recovery intents

* Preserve manifest compaction after table errors
2026-07-12 03:28:27 +03:00

926 lines
49 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
type: spec
title: "RFC-022 — Unified graph-write protocol"
description: One correctness protocol for graph-visible writes, with synchronous recovery, complete read-set arbitration, writer-specific physical-effect adapters, and explicit control-plane exceptions.
status: draft
tags: [eng, rfc, write-path, manifest, recovery, concurrency, lance, omnigraph]
timestamp: 2026-07-10
owner:
---
# RFC-022: Unified graph-write protocol
**Status:** Draft / for team review
**Date:** 2026-07-10
**Surveyed:** OmniGraph 0.8.1 (`main`); Lance 9.0.0-beta.15, git rev `f24e42c1`
**Audience:** engine and storage maintainers
**Open architecture review:** [RFC-022027 review ledger](../dev/rfc-022-027-architecture-review.md).
Findings marked **BLOCKER** must be dispositioned before acceptance.
---
## 0. Summary
OmniGraph will have one **correctness protocol** for every operation that changes
manifest-resolved graph state. The protocol does not require every writer to use
the same Lance primitive. A mutation can commit one staged transaction per table,
a branch merge can make several commits to a table, and optimize can use Lance
operations that have no staged API. Writer-specific **effect adapters** describe
those physical operations; one coordinator enforces the common safety rules.
A graph-visible write follows this state machine:
```text
recovery barrier
→ prepare pinned base + complete ReadSet + Effects
→ acquire ordered process-local gates
→ revalidate the complete ReadSet, or restart
→ durably arm recovery
→ apply writer-specific physical effects
→ publish exactly one graph-visible __manifest CAS
→ finalize derived state
```
The protocol has three hard boundaries:
1. Recovery is a synchronous pre-write safety barrier. It may also run in the
background, but a writer never proceeds merely because recovery was scheduled.
2. Validation and merge classification are valid only for their complete read set.
A changed probed table causes revalidation or a full restart; the writer never
refreshes expected versions underneath a plan computed from an older base.
3. “One `__manifest` CAS” applies only to graph-visible commits. Native graph-branch
ref creation/deletion and physical-only maintenance have explicit, smaller
control protocols and do not manufacture graph commits.
This RFC deliberately does not combine key fencing, durable table heads,
checkpoint retention, MemWAL ingest, or lineage-based merge-delta discovery into
one format and rollout. They are focused follow-ups:
- [RFC-023 — Key-conflict fencing](rfc-023-key-conflict-fencing.md)
- [RFC-024 — Durable table heads](rfc-024-durable-table-heads.md)
- [RFC-025 — Checkpoint retention](rfc-025-checkpoint-retention.md)
- [RFC-026 — MemWAL streaming ingest](rfc-026-memwal-streaming-ingest.md)
- [RFC-027 — Lineage merge deltas](rfc-027-lineage-merge-deltas.md)
## 1. Scope and authority
### 1.1 Graph-visible writes
A **graph-visible write** changes state resolved through a graph manifest snapshot:
- a node or edge table version;
- a registered or tombstoned table;
- accepted schema identity or schema-visible table metadata;
- a graph commit or graph head;
- any future logical marker that changes query or time-travel semantics.
For such an operation, the only visibility point is one successful `__manifest`
commit containing the entire graph delta. Per-table Lance commits before that point
are physical effects covered by recovery; they are not independently graph-visible.
### 1.2 Control operations
Two classes are not graph-visible commits:
1. Native graph-branch ref create/delete. Lance stores these refs outside the
dataset-version chain and creates no new `__manifest` version for either action.
2. Physical-only maintenance whose result is content-equivalent and is not selected
through a data-table pointer in `__manifest`, such as compacting `__manifest`
itself or reclaiming unreachable files.
Sections 7 and 8 define their control protocols. Branch **merge** and a data-table
optimize whose new version must be published are graph-visible writes and remain in
the main protocol.
### 1.3 What “unified” means
Unified means one set of safety obligations, one coordinator state machine, and one
closed registry of effect adapters. It does **not** mean:
- one storage trait method for every Lance operation;
- exactly one physical commit per table;
- pretending native refs are manifest rows;
- treating process-local queues as a distributed transaction manager;
- moving commit recovery out of the correctness path.
## 2. Protocol objects
The names below are conceptual. Implementations may choose different Rust names,
but they must preserve the represented information and transitions.
```rust
struct PreparedWrite {
operation_id: OperationId,
writer_kind: WriterKind,
target: BranchTarget,
base: BaseView,
read_set: ReadSet,
effects: Effects,
manifest_delta: ManifestDelta,
lineage_intent: Option<LineageIntent>,
recovery: RecoveryPlan,
}
```
### 2.1 `BaseView`
`BaseView` is the immutable state against which the operation was computed. It
contains at least:
- target manifest branch and its incarnation/freshness token;
- pinned manifest version;
- accepted schema identity;
- pinned table entries used by the operation;
- graph head when parentage, merge base, or branch semantics depend on it.
The base is captured only after the recovery barrier completes. A recovery pass may
advance the manifest or promote schema state, so a base captured before recovery is
not valid write input.
### 2.2 `ReadSet`
`ReadSet` is every authority value whose stability is required for the prepared
result to remain correct. It includes, as applicable:
- target branch incarnation;
- accepted schema identity;
- graph head;
- the table head for every table written;
- the table head for every table probed by uniqueness, referential-integrity,
cardinality, policy-independent structural validation, or merge classification;
- writer-specific authority, such as an enrolled stream's configuration and merge
generation.
A table belongs in the read set because its value affected the decision, not because
the writer happens to update it. Read-only dependencies are load-bearing.
The publisher must arbitrate every read-set member atomically with the manifest
commit. A fresh pre-check followed by an unconditional write is not a CAS. The
implementation must ensure that a concurrent change after the check contends on a
stable authority row or equivalent substrate token. If the current representation
cannot arbitrate a read-only dependency, that writer has not completed this RFC and
must not ship the outside-gate validation optimization.
### 2.3 `Effects`
`Effects` is an adapter-owned physical plan. Each effect declares:
- physical dataset and Lance branch/ref it targets;
- expected pre-state;
- whether it is staged, inline, ref-only, or zero-commit;
- the possible post-state shape: exact version, bounded range, or adapter-confirmed
version;
- whether rollback is safe;
- how recovery distinguishes no movement, partial movement, completed movement,
and an already-published result.
The generic coordinator never assumes `expected_version + 1`. That assumption is
valid for some mutation/load effects and false for branch merge, compaction, index
work, schema metadata changes, and no-op plans.
### 2.4 `ManifestDelta`
`ManifestDelta` is the complete logical result to publish. It contains table-version
journal entries, registrations/tombstones, mutable logical heads when present, schema
identity, and lineage rows as required by the operation. Its logical contents are
immutable for one prepared attempt. Physical version fields may be declared output
slots that the adapter binds to an allowed, confirmed effect result; binding such a
slot cannot widen or otherwise change the logical plan. A revalidation mismatch
discards the delta and restarts preparation.
### 2.5 `RecoveryPlan`
`RecoveryPlan` supplies the writer-specific classifier and compensation/roll-forward
rules. It must be serializable into a recovery sidecar before the first independently
durable physical effect. It is part of the commit protocol, not a best-effort
maintenance hint.
## 3. Normative invariants
1. **Recovery before base capture.** Every graph-visible writer runs and awaits the
recovery barrier before pinning `BaseView`.
2. **No durable effect before durable intent.** On an existing physical ref,
reclaimable uncommitted files may be staged first. A first-touch named-table
transaction is different: Lance writes its uncommitted files into the opened
branch tree, so Prepare retains the logical batch/predicate and pre-mints its
transaction identity; after revalidation the recovery sidecar becomes durable,
then the writer creates the target ref and stages those branch-local files under
the held gates. In every case the sidecar precedes the first independently
persistent physical effect, including a native table ref, data-table HEAD
advance, or native tag/index mutation needed by the graph-visible result.
3. **Complete read-set validation.** Physical effects may start only while the fresh
authority state equals the complete `ReadSet` used to prepare the operation.
4. **Recompute, do not patch.** On a pre-effect mismatch, discard the prepared plan
and rerun validation/classification. Never refresh expected versions beneath an
old plan.
5. **One graph visibility point.** A graph-visible write publishes one manifest CAS;
no subset of its table effects becomes graph-visible independently.
6. **Adapters own effect truth.** Every effect-producing writer uses a registered
adapter. There is no generic fallback that guesses version movement or recovery
safety.
7. **Queues are local optimization.** Ordered process-local gates prevent avoidable
in-process races and deadlocks. Cross-process correctness comes from Lance
conflicts, manifest arbitration, and recovery.
8. **Recovery safety is synchronous.** Background sweeping is permitted, but a later
overlapping writer waits for, performs, or fails on unresolved recovery before it
can advance state.
9. **Derived work follows visibility.** Expensive index reconciliation, cache warming,
and orphan reclaim may run asynchronously only when logical correctness does not
depend on their completion.
These refine, and do not weaken, invariants 2, 3, 4, 5, 7, 9, 13, and 15 in
[`docs/dev/invariants.md`](../dev/invariants.md).
## 4. The graph-write state machine
### 4.1 Stage A — recovery barrier
Before accepting a base, the coordinator discovers pending recovery intents that can
overlap this operation. It must reach one of three outcomes:
1. all relevant intents are fully resolved;
2. each remaining intent is proven already satisfied and can be finalized safely;
3. the write fails with a typed recovery-required or live-writer-contention error.
“Spawn recovery and continue” is not an outcome. Listing sidecars may run concurrently
with non-authoritative I/O, but preparation cannot accept its base until recovery has
completed and the post-recovery schema/manifest state is available.
Recovery must not destructively act on a sidecar that may belong to a live writer
without ownership/fencing proof. It waits, returns contention, or uses a protocol that
proves the prior owner cannot continue.
Recovery classifiers use exact effect identity or confirmed post-state. A numeric
test such as `manifest_version >= observed_lance_head` is sufficient only when the
adapter independently proves lineage containment; version ordering alone is not that
proof. Exact identity cuts both ways: a roll-forward that loses its manifest CAS to
a concurrent writer which already published this intent's exact goal state is
**convergence, not failure** — the barrier records the audit outcome and removes the
intent rather than failing the write (preserving the concurrent-advance convergence
behavior fixed in #296). "Exact" forbids adopting an unrelated newer version; it does
not forbid recognizing one's own goal already achieved.
The barrier is a deliberate availability trade, stated plainly: an unresolvable
overlapping recovery intent — including a live foreign writer's sidecar that cannot
be fenced — fails every overlapping write with a typed error until resolved. Safety
outranks availability here by design; operators observe the condition through the
typed error and recovery telemetry rather than through silently degraded writes.
### 4.2 Stage B — prepare
Prepare runs without writer gates. It may:
- capture `BaseView`;
- evaluate the complete constraint set;
- classify a merge;
- compute embeddings or other deterministic payloads;
- build staged Lance transactions and reclaimable uncommitted objects for targets
whose physical refs already exist;
- for a first-touch named-table target, retain the complete logical stage input and
pre-mint its recovery transaction identity without creating the ref;
- construct the complete `ReadSet`, `Effects`, `ManifestDelta`, and `RecoveryPlan`.
Prepare must not advance a Lance HEAD or the graph's native branch refs. Staged files
that are not referenced by a committed Lance manifest are permitted and are
reclaimable if the attempt restarts.
After Stage E arms recovery, a first-touch adapter may create its declared target ref
under the held gates, stage branch-local files on that ref, bind the resulting Lance
transaction to the pre-minted identity, and only then advance HEAD. The ref is itself
an independently durable effect covered by the sidecar; recovery must classify both
sidecar-before-ref and ref-before-HEAD crash states.
Every validator registers its actual committed-state probes in `ReadSet`. Key fences
are an additional same-key conflict signal; they do not replace read-set arbitration
for non-key uniqueness, RI, cardinality, or merge-target stability.
### 4.3 Stage C — acquire ordered gates
The prepared effects declare their gate keys. One attempt acquires the complete set in
this total order:
1. durable global operation claims (migration, retention, or future claims) by
deterministic claim key;
2. graph/schema-control key, if required;
3. target branch-control key, if required;
4. `(physical_branch, table_key)` keys in deterministic sorted order.
The gates are held through revalidation, recovery arming, physical effects, and the
manifest visibility decision. They may be released after a successful manifest CAS or
after a failed post-effect attempt has safely left its sidecar for recovery.
Acquiring a global claim is coordination, not a graph effect for §3's sidecar-order
rule: the claim record must itself contain an owner/fencing token and an explicit
crash-release/takeover contract. No data HEAD, tag, index, or logical authority may
move merely because the claim was acquired.
The merge-exclusive mutex may protect a coordinator swap, but it is not a semantic
cross-process lock and must not substitute for a target read set.
### 4.4 Stage D — revalidate or restart
With all gates held, the coordinator loads fresh authority state and compares every
member of `ReadSet`.
Revalidation also proves the physical pre-state that the new recovery intent would
claim. For every existing table ref in the effect envelope, live Lance `HEAD` must
equal the prepared manifest pin. `HEAD < pin` is an internal invariant failure;
`HEAD > pin` belongs either to an already-durable recovery intent or to uncovered
drift that requires explicit repair. The attempt must reject that state **before**
writing its own sidecar; a new sidecar may never retroactively claim a pre-existing
physical effect. A first-touch ref is the deliberate exception: it does not exist at
this point and follows the sidecar-before-ref protocol in §4.3 instead.
- If all members match, the attempt may arm recovery.
- If any member differs, no physical effect may run. The attempt releases its gates,
discards staged state, and restarts from Prepare.
- A strict API may return a typed conflict instead of retrying, but it may not publish
the stale plan.
Retries are bounded and observable. Retrying a merge means recomputing the merge base
and reclassifying against the new target. Retrying a validation-sensitive mutation
means rerunning the validators, including probes of tables the mutation does not
write.
### 4.5 Stage E — arm recovery
After successful revalidation, write the recovery sidecar durably before the first
independently durable physical effect. An effect-free or authority-first workflow
described in §4.9 may omit the sidecar. Otherwise the sidecar contains at least:
- operation id, writer kind, actor, and target branch/incarnation;
- pinned schema identity and complete read set;
- every physical target and expected pre-state;
- adapter recovery strategy;
- intended manifest delta and lineage intent, or a durable reference to them;
- confirmed post-state once the adapter reaches its all-effects-complete boundary.
A multi-step adapter first records its pre-state plan. After all its physical effects
finish, it durably records the exact confirmed post-state before manifest publish.
Until that confirmation exists, recovery treats the effect set as possibly partial.
### 4.6 Stage F — apply effects
The adapter applies its declared physical effects. A failure after recovery is armed
leaves the sidecar intact. The request must not delete it, silently adopt live HEAD, or
start a fresh plan around it.
The adapter returns exact achieved state to bind the physical output slots declared by
`ManifestDelta`. If achieved state differs from the prepared effect envelope, the
operation fails into recovery rather than widening its plan in place.
### 4.7 Stage G — manifest CAS
A graph-visible operation performs exactly one `__manifest` CAS carrying its complete
logical delta and lineage when the plan has `LineageIntent`. Metadata-only plans carry
their explicit authority/operation rows without manufacturing graph lineage. On every
CAS attempt, the commit authority re-reads and arbitrates the complete `ReadSet`.
The following cases are distinct:
- **Conflict before any physical effect:** safe bounded restart from Prepare.
- **Conflict after physical effects:** recovery case. Keep the sidecar; do not simply
re-stage or point the manifest at whatever HEAD is now live.
- **CAS success:** the graph commit is visible atomically.
When durable table heads land under RFC-024, tombstoning a table must update its mutable
head to an explicit deleted state in this same CAS. A stale live head plus an immutable
tombstone history is not a valid O(tables) current-state representation.
### 4.8 Stage H — finalize
After CAS success:
- delete the sidecar best-effort when the workflow has one;
- refresh/invalidate process-local views;
- enqueue derived index reconciliation and orphan reclaim;
- record recovery/audit completion when applicable.
Sidecar deletion failure does not turn a durable successful graph commit into a user
error. The next recovery barrier proves the exact intent satisfied and removes the
artifact.
### 4.9 Authority-first control workflows
Some metadata workflows have no independently durable physical effect before their
manifest CAS. They may use an **authority-first** subtype:
1. run the recovery barrier, prepare, acquire gates/claims, and revalidate exactly as
above;
2. publish the metadata transition as the first durable effect;
3. perform only idempotent work whose desired target is fully encoded by that
transition and whose interruption cannot expose incorrect graph data.
No generic sidecar is required before step 2 because there is no pre-authority effect
to recover. The authority row itself is the durable recovery cursor. Checkpoint
deletion, a GC-boundary publish, and `OPEN -> DRAINING` stream intent are candidate
examples; each follow-up RFC must prove its post-CAS work is convergent and safe.
A long control workflow is not one giant RFC-022 attempt. Each lifecycle transition,
fold, schema/branch operation, and resume transition is a separate prepared write or
native-ref control step with its own read set and visibility point. If a later phase
needs a non-idempotent or independently visible effect not fully described by the
authority row, that phase uses a normal sidecar before the effect.
## 5. Crash contract
| Crash point | Required result |
|---|---|
| Before sidecar | No independently durable effect occurred; uncommitted objects are reclaimable. |
| Sidecar durable, no effect | Recovery aborts/finalizes the empty intent. |
| Some effects applied, not confirmed | The adapter rolls back, completes, or refuses safely according to its declared strategy. |
| All effects confirmed, before manifest CAS | Recovery rolls forward the exact confirmed manifest delta, or applies the adapter's explicit all-or-nothing rule. |
| Manifest CAS succeeded, sidecar remains | Recovery proves the exact intent visible, audits it, and removes the sidecar. |
Rollback is not assumed safe. Lance `Restore`, schema-file promotion, native refs, and
content-replacing operations have different concurrency properties; each adapter must
state which recovery direction is legal and under what fencing.
## 6. Writer-effect adapters
The adapter registry is closed by default: adding a graph-visible writer requires a
new adapter or an explicit use of an existing adapter. Code review and tests must be
able to enumerate every adapter and every entry point that invokes it.
### 6.1 Mutation and load
- Construct one staged effect per touched table where the Lance API permits it.
Existing-table effects may stage in Prepare; first-touch named-table effects use
the sidecar → target ref → branch-local stage ordering above.
- Put every uniqueness, RI, and cardinality probe in `ReadSet`.
- Revalidate or restart when a probed-but-untouched table changes.
- Preserve strict replacement semantics for overwrite/delete.
- Treat key-conflict fencing and strict keyed Append semantics as RFC-023 concerns;
no fence is credited as protection until that RFC's rollout gates pass.
### 6.2 Branch merge
- Capture the exact source graph commit/snapshot and compute row classification
against that immutable input outside the effect gates. The source commit is an
effect precondition, not a member of the target publisher's atomic `ReadSet`:
a target CAS cannot arbitrate a foreign source-branch row.
- Revalidate that the captured source incarnation and commit still identify the
prepared input before effects. A later source-head advance is intentionally
harmless: merge means "merge this captured source commit," not "whatever is
latest when the target publishes." A future latest-at-publish contract would
require a real source-branch fence held through target CAS.
- Include the target graph head and every target table used by classification or
validation in `ReadSet`.
- Any target change before effects forces a complete reclassification; publishing a
result computed against an old target and parenting it to a new live head is
forbidden.
- The adapter supports zero, one, or several physical commits per table and records
exact confirmed post-state before manifest publish.
- Lineage-based candidate discovery may replace the classifier only under RFC-027;
this protocol does not assume it is O(delta).
### 6.3 Schema apply and storage migration
- Acquire the schema-control gate before effect application.
- Include accepted schema identity and every affected table in `ReadSet`.
- Cover schema staging-file promotion, data-table schema/field-metadata commits,
registrations, tombstones, and final schema identity with one recovery intent.
- A non-noop schema change still needs that recovery intent when it has no table
effects: schema-contract staging and promotion are independently durable state,
so an empty table-pin set means “metadata-only SchemaApply,” not “effect-free.”
- Until SchemaApply receives the full exact adapter, its schema-v5 bridge records
the target schema identity at arm time and durably confirms Phase C before final
schema-file promotion. Recovery may classify an empty-pin Phase-D residue as
completed only when both that confirmation and the live target identity match;
numeric manifest movement alone is not an outcome discriminator because a
rollback recovery commit also advances the manifest.
- Write the sidecar before the first table HEAD advance, including unenforced-PK
metadata backfill or other inline metadata commits.
- A branch-wide or graph-wide migration must enumerate every physical manifest/data
branch it changes; updating main does not implicitly migrate older branch manifests.
### 6.4 Data-table optimize and index work
- The adapter may describe zero or multiple inline, content-preserving Lance commits.
- It records the exact achieved version rather than assuming one version of movement.
- If the new data-table version is selected through `__manifest`, publishing that
pointer is a graph-visible commit and uses this protocol.
- Until EnsureIndices receives its full exact effect adapter, its schema-v6 bridge
retains legacy loose table classification but pre-mints a stable rollback commit
id. Recovery persists the observed rollback audit plan before the first restore;
a retry after rollback publish therefore finalizes the same `RolledBack` outcome
instead of inferring direction from aligned numeric pins.
- Schema-v6 does not prove ownership of an index commit and does not add a fixed
forward outcome. Exact transaction chains, fixed original lineage, and exact
first-touch ref identity remain requirements of the full adapter.
- Optimize retains its legacy loose effect adapter. Its synchronous overlap
barrier is two-stage: a broad entry probe, then a relist under main's
process-local branch-writer gate that rejects every main-target sidecar plus
graph-global SchemaApply before HEAD inspection, drift classification, or its
own recovery arm. The gate remains held through internally parallel table
effects/publishes because every published table pointer advances shared
`graph_head:main`, and through final physical `__manifest` compaction so a new
main intent cannot arm before raw manifest movement finishes. This coarse
legacy-adapter safety cost makes same-process sidecar-enrolled main writers
wait for the entire graph-wide Optimize, while Optimize's own table tasks stay
parallel. This is not the future exact Optimize adapter, a universal mutex for
every main publisher, or a distributed fence.
- Logical operations never fail because a derived index is absent or behind.
- Physical-only internal-table maintenance remains the exception in Section 8.
### 6.5 MemWAL fold
RFC-026 owns enrollment, acknowledgement, quiescence, fresh-read semantics, and the
public ingest surface. Any fold that becomes graph-visible is an adapter here:
- fold-time validation contributes its complete read set;
- Lance `merged_generations` changes atomically with the base-table data commit;
- the sidecar covers the data-commit-to-manifest gap;
- one successful manifest CAS makes the folded graph state visible.
## 7. Native graph-branch ref control protocol
Creating or deleting a graph branch mutates a native Lance branch ref for
`__manifest`. Lance specifies that these operations do not generate a dataset version.
There is no target branch on which to publish before create, and no target remains on
which to publish after delete. They therefore cannot truthfully be instances of the
graph-visible manifest-CAS protocol.
The native control is not one physical mutation. At the pinned Lance revision:
- create first commits a shallow-cloned branch dataset under `tree/{branch}` and
then writes `BranchContents`; the two phases are non-atomic and
`BranchContents` is the sole logical authority;
- delete removes `BranchContents` first and then reclaims the branch directory.
Accordingly, a clone without `BranchContents` is unreachable physical garbage,
while an absent `BranchContents` after delete means the logical delete succeeded
even if directory cleanup returned an error.
Lance maps slash-separated branch names into nested physical directories and will
not reclaim an ancestor directory while a live descendant name exists. Live graph
branch names are therefore path-prefix-disjoint: `review/2026` is valid, but it
cannot coexist with a live `review` branch. This is checked before native create,
and legacy overlapping names must be deleted leaf-first. Without this namespace
invariant, `force_delete_branch` may return success while deliberately leaving an
ancestor clone-only dataset in place.
Their control protocol is:
1. run and await the recovery barrier;
2. quiesce enrolled streams as required by RFC-026;
3. acquire any active global claim, such as RFC-025's retention claim, and then
the graph/branch-control gate in §4.3 order;
4. freshly capture source ref/version/incarnation and target absence (or the
delete target's exact `BranchIdentifier`);
5. validate a create name and the path-prefix-disjoint namespace before Lance's
shallow-clone phase;
6. execute the bounded native classifier below; and
7. reclaim owned per-table forks best-effort while the complete control-gate set
remains held, then release the gates. A reclaim failure is logged for the
cleanup reconciler; this implementation does not schedule asynchronous reclaim.
| Operation | Fresh physical state | Classifier action |
|---|---|---|
| create | no ref, no clone | idempotent force-reclaim (no-op), then create |
| create | clone only | force-reclaim the unreachable clone, then create |
| create | matching ref + openable clone observed after this invocation's ambiguous native result | complete; accept a lost acknowledgement |
| create | mismatching/broken ref | conflict; never adopt or delete it |
| delete | captured identifier still present | not deleted; return the native error |
| delete | ref absent, directory remains | logically deleted; retry reclaim best-effort |
| delete | ref and directory absent | complete |
| delete | different identifier present | delete/recreate ABA; conflict, never delete it |
Create retries the native call at most once after reclaiming an absent-ref tree.
It never adopts a matching ref that was already present before the invocation;
that remains ordinary `AlreadyExists` because there is no operation marker.
Matching completion requires the expected parent branch and version plus a target
identifier whose mapping is exactly the captured parent identifier followed by one
new `(parent_version, uuid)` element; the target dataset must also open. Lance does
not let the caller pre-mint that UUID, so the proof is intentionally scoped to the
supported single-writer-process boundary.
No separate graph-branch sidecar is written. Under the held target gate,
path-prefix-disjoint namespace, and supported topology, absent `BranchContents`
means there is no logical branch and any same-name tree is safe derived garbage;
the next same-name create is the targeted, idempotent reconciler. A recovery
sidecar would introduce a second authority and would incorrectly pull native
controls into graph lineage. First-touch **data-table** forks are different: their
mutation/load sidecar is already durable before branch creation. Full recovery
force-reclaims an absent-ref target as either a no-op (crash-before-clone) or that
intent's clone-only zombie before retiring the intent.
One compatibility case deliberately defers that cleanup. A legacy graph may
already contain path-prefix-overlapping live names. If an unresolved first-touch
intent targets an ancestor table tree while a live path-child still exists, Full
recovery leaves the sidecar durable instead of recursively deleting the child's
storage. Read-write open may complete for leaf-first remediation only when the
intent owns no physical table effect. If a multi-table attempt owns any effect
while another untouched fork is blocked by the child, rollback is
complete-or-error: open fails closed, the sidecar remains authoritative, and an
existing handle or offline Lance-level branch tool must remove the descendant
before the next Full recovery reclaims the untouched fork, compensates the owned
effect, and retires the sidecar. This distinction prevents legacy writers outside
the v3 barrier from preparing against an unresolved partial rollback.
Delete has one recovery disposition that create does not: after the complete
schema/target-branch/all-table gate set has waited out any live in-process owner,
an unresolved sidecar scoped to the branch being removed may be rendered
unreachable by the native ref deletion. A later heal records the orphan-discard
audit and retires it. Graph-global schema recovery still blocks the control, and
create/merge may not adopt this exception.
The native ref operation itself should enforce the freshly checked precondition or
surface concurrent ref mutation as a conflict — but at the pinned Lance revision it
does not: branch-ref creation is an existence check followed by an unconditional
put, and delete is not compare-and-delete by `BranchIdentifier` (the same substrate
gap for which RFC-025 §2.3 rejects a branch ref as a claim mechanism). Until Lance
ships conditional/CAS ref mutation, graph-branch create/delete therefore inherit the
documented single-writer-process support boundary — the same disposition RFC-023
§10 applies to recovery ownership — and multi-process branch operations are not
advertised. The upstream ask for a conditional ref primitive is filed alongside
this RFC; a process-local branch gate remains a local optimization, not the missing
cross-process guarantee.
These operations do not emit a synthetic graph commit. If a future product contract
requires a native ref mutation and manifest/audit rows to become atomic together, it
needs a separate multi-authority recovery protocol; this RFC does not claim an
atomicity the substrate does not provide.
This exception applies only to graph-level create/delete. Branch merge is a
graph-visible write. Lazy per-table forks created while preparing a branch write are
declared physical effects of that writer and remain subject to its recovery/reclaim
contract.
## 8. Physical-maintenance control protocol
Physical work that does not change manifest-resolved logical graph state does not
create graph lineage merely to fit the main protocol. Examples include:
- compacting `__manifest`, which is itself the authority and is read at its Lance HEAD;
- deleting versions/files already proven unreachable under the active retention
contract;
- reclaiming orphaned branch refs or uncommitted objects;
- rebuilding derived physical state when no graph-visible data-table pointer changes.
Such work must still be idempotent, bounded, observable, and safe under concurrent
native Lance commits. It uses substrate conflict/retry semantics appropriate to the
operation. It must never expose partial logical graph state.
The exception is from graph-lineage publication, not from recovery safety. Maintenance
must run the recovery barrier or refuse before it can replace or delete an artifact
named by an unresolved sidecar. It must also acquire any relevant process-local gates;
as elsewhere, those gates are an optimization rather than cross-process authority.
Data-table compaction/index work that advances a version which graph reads must select
through `__manifest` is not exempt; Section 6.4 applies. Checkpoint reachability and
the mapping from graph checkpoint rows to Lance-native GC pins belong to RFC-025.
## 9. Concurrency and retry semantics
Process-local gates reduce same-process races and establish one deadlock-free order.
They do not coordinate two servers or CLIs. A conforming implementation remains safe
if every process has its own gate manager.
Cross-process safety comes from:
- complete read-set arbitration at the manifest authority;
- Lance transaction conflicts for physical table effects;
- durable recovery before physical HEAD movement;
- refusal to continue past unresolved overlapping recovery.
Retry rules are phase-specific:
- before effects, a read-set mismatch discards and recomputes the whole attempt;
- while applying an effect, the adapter may retry only when Lance guarantees the
operation can be safely replanned from fresh physical state;
- after any effect, manifest contention is resolved through the armed recovery intent,
not by silently rebasing the logical plan;
- retry exhaustion returns a typed, observable conflict.
## 10. Rollout and compatibility
This RFC authorizes a protocol refactor, not a manifest v5 format moment. RFC-023
through RFC-027 own their respective format and public-surface changes.
It also does not require a mutable-tip `GraphState` singleton. Three measured,
local latency fixes can land independently of the adapter conversion:
1. make the graph's shared Lance `Session` a required parameter of every
manifest open/publisher path, so remote opens do not rebuild clients and cold
metadata state;
2. capture one immutable operation-local manifest/lineage view and pass it down
the call stack instead of reopening the same state repeatedly; and
3. remove the verified-redundant branch-idle refresh and the back-to-back second
`branch_delete_as` refresh once their existing coverage asserts unchanged
behavior.
These are narrow access-shape fixes, not a second commit-input authority. They
must preserve snapshot pinning and still cross the recovery/read-set barriers
defined above.
Implementation proceeds in this order:
1. Introduce `PreparedWrite`, `ReadSet`, effect-adapter, and recovery-plan concepts
while preserving existing behavior.
2. Ship conservative branch-wide arbitration first. Mutation/load captures
`(Lance BranchIdentifier, exact optional graph_head, accepted schema identity)`;
every publisher retry compares that token instead of reparenting. Because every
supported graph-content and schema apply advances `graph_head:<branch>` before
schema promotion, the shared head row atomically arbitrates probed-but-untouched
same-branch dependencies. The native branch identifier detects delete/recreate
ABA under the documented single-writer-process branch-control boundary. RFC-024
later narrows false contention with table heads; it is not a correctness
prerequisite for this coarse step. Existing live committed-state validation
probes remain until the narrowed read set replaces them.
> **Implementation note (2026-07-11):** mutation/load now use this coarse
> token, schema-v3 exact-effect sidecars, fixed lineage/rollback outcome ids,
> zero transparent Lance commit retries, and bounded full reprepare before
> effects. Branch merge now captures an immutable source commit/snapshot and
> the target coarse token, computes its merge base from those captured ids,
> and revalidates target authority plus source incarnation under the ordered
> gates. Its schema-v4 recovery envelope distinguishes multi-commit HEAD
> effects from first-touch ref-only forks, persists an ordered pre-minted
> Lance transaction chain for every logical data effect plus fixed
> merge/rollback lineage, and confirms the complete manifest delta (including
> pointer-only slots) before publishing with `ExactGraphHead`. Those data
> transactions commit with zero transparent conflict retries; recovery accepts
> only their contiguous exact prefix (plus a derived `CreateIndex` tail after
> the complete chain) and fails closed on foreign movement. A pre-effect target change
> returns `ReadSetChanged`; any post-arm failure returns `RecoveryRequired`
> and recovery never re-parents onto a target winner. Schema apply,
> optimize/index, and MemWAL fold remain on their writer-specific paths until
> their adapter slices land.
> **Branch-control/cleanup slice (2026-07-11):** native graph-branch create
> and delete now use the §7 authority classifier, including pre-clone name
> validation, prefix-disjoint live names, bounded clone-only recovery,
> lost-acknowledgement classification, exact delete-incarnation fencing, and
> no synthetic graph lineage. First-touch Full recovery also reclaims an
> absent-ref clone-only table fork. Cleanup now protects every exact lazy
> branch pin, computes `keep` from Lance's actual version list, refuses
> uncovered main HEAD drift, and completes its live-root preflight before the
> first table GC.
3. Convert mutation/load, branch merge, schema apply/migration, data-table optimize,
and graph-visible index work one adapter at a time.
4. Add static or runtime enumeration proving no graph-visible entry point bypasses the
coordinator.
5. Delete superseded writer-specific orchestration only after its crash and
concurrency cells pass through the adapter.
6. Optimize background recovery latency only after the synchronous barrier and all
recovery classifications remain intact.
Mixed writer binaries are not made safe by process-local gates. A deployment may
enable the new protocol only when every writer that can reach the graph obeys the same
sidecar and manifest-arbitration contract, or when a compatibility gate rejects older
writers.
## 11. Required tests and cost gates
### 11.1 Protocol conformance
- Enumerate every graph-visible entry point and its adapter.
- Assert no sidecar-backed adapter can create an independently durable physical
effect before its sidecar is durable.
- Enumerate authority-first workflows and assert their CAS is the first durable
effect and every post-CAS action is idempotently derivable from its authority row.
- Assert one graph-visible operation produces exactly one manifest visibility commit.
- Assert branch create/delete and physical-maintenance exceptions produce no synthetic
graph lineage.
### 11.2 Read-set races
- Two distinct ids racing on the same non-key `@unique` value cannot both publish.
- An edge insert racing deletion of an endpoint must revalidate or conflict.
- Cardinality probes of an untouched table participate in arbitration.
- A target advance after merge classification forces complete reclassification.
- A source branch advancing after capture does not silently substitute its new
head: the merge either uses the captured source commit or fails its pre-effect
source-incarnation/commit check. A latest-at-target-publish variant requires a
real source fence and is not inferred from the target `ReadSet`.
- Run the same cells with separate `Omnigraph` handles sharing one root-scoped
process-local gate manager, then with separate processes that do not share it.
### 11.3 Recovery
- Fail before sidecar, after sidecar, after each physical effect, after confirmation,
after manifest CAS, and during sidecar deletion for every adapter.
- A later overlapping writer blocks, heals, or returns recovery-required; it never
advances around the sidecar.
- If Optimize's entry probe passes and a SchemaApply or table-disjoint main intent
arms before its branch gate is acquired, Optimize returns recovery-required with
that intent's id; it does not create an Optimize sidecar, move another table,
advance `graph_head:main`, or compact `__manifest` afterward.
- After Optimize's under-gate relist passes, a table-disjoint main writer started
while productive table compaction is paused remains queued until Optimize
releases its branch-wide effect envelope, then completes normally.
- A live foreign writer's sidecar is not destructively recovered without fencing.
- Recovery proves exact effect identity; a numerically newer unrelated version is not
accepted as proof of ancestry.
### 11.4 Adapter-specific truth tables
- Mutation/load: zero/one table, multi-table, strict, non-strict, and
probed-but-untouched dependency cases.
- Merge: adopt, rewrite, multi-commit, no-op, target advance, and partial-Phase-B crash.
- Schema: staging files, registration-only, metadata HEAD advance, partial multi-table
migration, and branch-local state.
- Optimize/index: zero, one, and several Lance commits, including monotonic publish and
retryable physical contention.
- MemWAL fold, when RFC-026 lands: merged-generation conflict and every fold crash
boundary.
- Native graph branch control: invalid name before clone; live path-prefix
collision before clone; clone-only create recovery on main and a named source;
pre-existing matching ref remains `AlreadyExists`; current-call lost create
acknowledgement is accepted only with exact parent/incarnation metadata and an
openable target; delete with absent ref plus remaining tree is success and
reclaims best-effort; same identifier preserves the native error; a recreated
identifier is a typed conflict and survives; neither direction advances a
manifest version or emits graph lineage.
- First-touch recovery with legacy path overlap: a proven no-effect intent may
defer clone-only ancestor cleanup and return an open handle for leaf-first
remediation; a mixed multi-table intent with one owned effect and one blocked
untouched fork must fail read-write open, retain the sidecar without restoring
or publishing, and converge after offline Lance-level leaf cleanup.
### 11.5 Cost gates
The protocol must not move validation, classification, embedding computation, or
staged-file construction under process-local gates. Measure gate hold time separately
from Prepare. Correctness gates precede latency optimization: a cost regression can
delay rollout, but it cannot justify touched-table-only validation or asynchronous
recovery safety.
## 12. Invariants and deny-list check
This design reinforces the existing architecture:
- Lance and `__manifest` remain the sources of truth; `PreparedWrite` is immutable
attempt-local state, not a shadow mutable tip.
- Graph visibility remains manifest-atomic.
- Recovery remains part of the commit protocol.
- Logical constraints fail loudly and are revalidated when their inputs change.
- Derived indexes and reclaim work converge without becoming logical commit points.
- No custom WAL, transaction manager, buffer pool, or distributed lock is introduced.
The design rejects two tempting deny-list violations: treating process-local queues as
distributed correctness, and treating recovery as derivable background work that a
new writer may outrun.
Acceptance should also add one clarification to invariant 15: a view of immutable,
version-pinned state may be cached, while an in-memory view of the mutable tip is only
a hint. Every use of mutable-tip state as write input must be re-arbitrated by the
commit authority. Durable heads under RFC-024 are one possible authoritative
representation; this protocol does not require or bless a warm parallel truth path.
## 13. Drawbacks and rejected alternatives
### 13.1 One generic staged-storage method
Rejected. Lance does not expose staged forms for every operation, and existing writers
have materially different version movement and rollback safety. A generic method would
either lie about those differences or accumulate writer-kind conditionals. One
protocol plus explicit adapters has lower long-run liability.
### 13.2 Asynchronous heal with optimistic continuation
Rejected. Current recovery classification relies on later writers not advancing past
an unresolved sidecar. Scheduling a sweep does not establish that fact. Recovery may
be proactively asynchronous, but the next writer still crosses a synchronous barrier.
### 13.3 Touched-table-only CAS
Rejected. Non-key uniqueness, RI, cardinality, and merge classification read tables
the operation may not write. Ignoring those dependencies admits commits that were never
valid against one serial graph state.
### 13.4 Treat every state change as a manifest commit
Rejected. Native branch refs and physical maintenance have different substrate
visibility points. Manufacturing manifest commits would add coordination without
making the native mutation atomic with them.
## 14. Reversibility
The in-memory coordinator and adapter refactor is reversible. Recovery-sidecar schema
changes must be versioned and backward-compatible during rollout. This RFC alone does
not authorize a `__manifest` schema-stamp bump, a public wire change, or a new storage
substrate.
The correctness contract is intentionally difficult to reverse: after writers rely on
complete read-set arbitration and recovery-before-write, weakening either would
reintroduce silent integrity or recovery races. Focused irreversible changes are
reviewed in RFC-023 through RFC-027.
## 15. Follow-up RFC boundaries
- **RFC-023** owns PK annotation, fenced merge routing, strict keyed Append behavior,
mixed fenced/unfenced rollout, conflict mapping, and both commit-order tests.
- **RFC-024** owns mutable table-head rows, explicit live/tombstoned head state,
current-state read shape, migration, and cost gates.
- **RFC-025** owns checkpoint rows, Lance-native physical pins, cleanup reachability,
pruned-through semantics, and checkpoint/cleanup crash ordering.
- **RFC-026** owns MemWAL enrollment, durable acknowledgements, fold/dead-letter
behavior, stream quiescence, fresh reads, public surfaces, and upgrade fencing.
- **RFC-027** owns candidate discovery from row lineage, deletion discovery, fallback
semantics, and evidence for any O(delta) merge claim.
Those RFCs call this protocol when they produce a graph-visible write. None may weaken
the recovery barrier, omit read dependencies from `ReadSet`, or create a second graph
visibility point.