--- type: spec title: "RFC-018 — Streaming-ingest WAL on Lance MemWAL" description: Adds a durability-first streaming ingest path (ack on WAL durability, asynchronous fold into the graph commit chain) built entirely on Lance's MemWAL primitive; reconciled against Lance v8.0.0 and the v9 beta line; analyzed for composition with the upstream multi-table-commit RFCs. status: draft tags: [eng, rfc, wal, ingest, lance, omnigraph] timestamp: 2026-07-02 owner: --- # RFC-018 — Streaming-ingest WAL on Lance MemWAL **Status:** Draft / for discussion **Date:** 2026-07-02 **Surveyed version:** 0.7.2 (branch `dst-extract-crate`); Lance pinned at 7.0.0 **Upstream surveyed:** Lance v8.0.0 (released; RC votes closed 2026-07-01), v9.0.0-beta.10; MemWAL spec (`lance.org/format/table/mem_wal/`, fetched in full 2026-07-02); discussions #7260, #7264, #7222, #7176 **Audience:** OmniGraph maintainers --- ## 0. TL;DR OmniGraph's write path is optimized for *interactive* graph mutation: every `mutate_as`/`load` pays the full commit chain (staged Lance commit per touched table + one `__manifest` publish CAS) before acknowledging, and per-branch throughput is capped by the `graph_head` row CAS at roughly the object store's conditional-write rate. That is the right trade for its workload and this RFC does not touch it. What OmniGraph lacks is a **streaming ingest** path: high- frequency small writes (event streams, agent telemetry, embedding pipelines) where per-write ack latency matters and per-write graph commits are wasteful. This RFC adds one, with a hard constraint set: 1. **The WAL sits in FRONT of the commit point, never beside it.** The `__manifest` CAS remains the single linearization point for graph visibility and lineage (the RFC-013 Phase 7 conclusion). The WAL is an ingestion buffer whose contents *become* graph state only through the existing publish seam. No second metadata authority is created. 2. **No custom WAL.** Lance ships MemWAL — a complete spec'd LSM layer (shards, epoch-fenced writers, Arrow-IPC WAL entries, flushed memtables as Lance tables with pre-built indexes, a MemWAL system index whose merge progress commits atomically with the merge). Invariant 1 says use it, and §3 inventories every piece of it we consume. 3. **Ack means durable.** A write is acknowledged only after its WAL entry is durably on the object store (Lance durability waiters). The deny-list item "no acks before durable persistence" holds; what moves is *graph visibility*, which arrives at the next fold. Fresh-tier reads that see folded-but-unpublished / unfolded WAL data are **explicit, opt-in, read-only, and documented** (invariant 6's escape hatch, used exactly as written). Target substrate is **Lance v8.0.0** (Phase 0 is the bump): v8 is the MemWAL hardening release (writer-fencing WAL sentinel, durability waiters, LSM read planners). Delete support arrives with **v9** (tombstones); stream-mode deletes are phased on it. The design composes cleanly with — and is partially scaffolding-for — the upstream multi-table commit work (#7260/#7264): §7 analyzes both directions and pins the one design rule (single publish seam) that makes adopting either a publisher swap rather than a redesign. --- ## 1. Motivation Today one graph write costs, serially: per touched table a staged write + `commit_staged` (a Lance commit, itself several object-store round trips), then one `ManifestBatchPublisher::publish` (merge-insert CAS on `__manifest`). Latency is the sum of the chain; throughput per branch is bounded by the shared `graph_head:` row (the deliberate RFC-013 §7.1 contention point) at the object store's conditional-write rate — single-digit commits/sec on S3. `write_cost.rs` keeps this *flat in history*, which is the right guarantee for the interactive path, but nothing can make it *cheap per op*: the floor is object-store round trips. Workloads that don't fit: continuous event/entity streams from agents, sensor-style feeds, bulk embedding backfills that trickle, any producer that needs sub-100ms acks or >tens of writes/sec sustained on one branch. Today such producers must client-side batch into `load` calls, re-implementing an ingestion buffer badly, with no durability until the batch commits. The turbopuffer/SlateDB survey (2026-07, conversation record) located the correct shape: **ack on WAL durability, group-commit, fold asynchronously into the columnar substrate, keep one CAS commit point.** turbopuffer acks at WAL durability and bridges reads with a WAL-tail scan; SlateDB acks on WAL flush and serves cross-node readers only durable published state. Both keep exactly one commit authority. OmniGraph's commit authority is `__manifest`; the WAL therefore buffers *in front of* it. ### 1.1 Relationship to PR #318 (warm publish + pinned opens) PR #318 and this RFC attack different terms of the same budget and compose: #318 removes the *history-dependent* term (warm attempt-0 publish from `known_state` drives the per-write `__manifest` scan to 0; pinned `At(v)` staging opens drop the `_versions/` LIST), making a warm write O(1) in commit depth. This RFC amortizes the *constant* term that remains — the commit-chain round-trip floor and the `graph_head` CAS rate — which #318 cannot touch and which becomes the only wall once #318 lands. Structurally the fold inherits #318 for free: it publishes only through the `ManifestBatchPublisher` seam, whose internals (`PublishPlan`, warm/cold attempts) are exactly what #318 changes, and the fold's long-lived coordinator is the ideal warm-path client. Sequencing notes: (a) §9's concurrency cells should use the `Cohort` multi-coordinator DST harness #318 lands rather than invent one; (b) Phase 0/1 must sequence against #318's U2 follow-up (internal schema v4 → v5, the `table_version` row-accumulation wall) so enrollment's unenforced-PK step lands against v5 rather than straddling the migration; (c) the ~5% cross-process RC-1 uncovered-drift exposure #318 characterizes-but-tolerates is one more argument for MemWAL's epoch fencing: ingest tables become the first write surface with a true cross-process fencing primitive. ## 2. Non-goals and preserved invariants - **Not a replacement for `mutate_as`/`load`.** The per-query-atomic interactive path is unchanged, including its "one query = one graph commit" lineage contract. Stream ingest is a distinct, opt-in surface with a documented coarser lineage granularity (one fold = one commit). - **Not a metadata store.** Lineage, branch heads, table versions stay in `__manifest` rows. Nothing in this RFC writes graph metadata anywhere else. - **Not a custom WAL / transaction manager** (deny-list). Everything below the OmniGraph fold logic is Lance MemWAL, spec'd upstream. - **Not early ack.** `await_durable=false`-style acks (SlateDB's opt-in) are rejected; invariant 6 and the deny-list stand. - **Not cross-query transactions.** Branches remain the multi-query transaction mechanism. ## 3. Substrate inventory — what Lance provides and how we use all of it Per the lance.md protocol the MemWAL spec and adjacent pages were fetched in full (2026-07-02). The spec is **experimental** upstream — risk register in §10. Inventory, mapped to consumption: | Lance tooling | Spec/PR | How this RFC uses it | |---|---|---| | MemWAL shards; one epoch-fenced writer per shard | spec §Shard | One shard per enrolled `(table, branch)` in Phase 1 (OmniGraph is effectively single-writer per graph today); shard count is a later tunable | | Sharding specs + transforms (`bucket(murmur3)`, `identity`, …) | spec §Sharding Spec | Phase 2 scale-out: `bucket(key, N)` on the table's `@key` column so each primary key maps to exactly one shard (the spec's last-write-wins correctness requirement) | | WAL entries: Arrow IPC files, bit-reversed naming (S3 partition spread), strictly sequential positions | spec §WAL | The durable unit. Group commit = batching writes per WAL flush; flush interval/size configurable via `writer_config_defaults` | | Writer epoch fencing + **WAL sentinel on claim** | spec §Writer Fencing; v8 #7110 | Cross-process safety for ingest writers — a fenced zombie cannot ack lost writes. Note #7110 closes the spec's own documented GC-vs-fencing hazard; v8 is therefore the *minimum* substrate for this RFC | | Durability waiters (fenced flush surfaces to waiters, not hangs) | v8 #7132 | **The ack point.** `ingest` resolves the client's request when the waiter resolves | | MemTable + flushed memtables (Lance tables per generation, with pre-built indexes + bloom filter) | spec §MemTable, §Flushed MemTable | The fold input; generation order gives upsert correctness | | `maintained_indexes` (memtable FTS/vector indexes; vector inherits base PQ/SQ params for distance comparability) | spec §MemWAL Index Details | Phase 2 fresh-tier search: FTS + vector queries stay indexed over unfolded data | | MemWAL index: `merged_generations` **updated atomically with the merge-insert data commit**, concurrent-merger conflict resolution | spec §Index Details, Appendix 2 | The fold's exactly-once bookkeeping; two racing folds resolve via the conflicting commit's index, no progress regression | | `index_catchup` progress | spec | Bridges base-table index lag after fold: reads use the flushed memtable's indexes for the gap instead of scanning | | LSM merging read (`_gen`/`_rowaddr` dedupe), shard pruning, fresh-tier as-of cut, LSM FTS planner, LIMIT/OFFSET pushdown, cache prewarm | spec §Reader Expectations; v8 #7215, #7066, #7256, #7284 | Phase 2 fresh-tier read path — consumed, not reimplemented | | GC contract (merged + index caught up + no retained version refs) | spec §Garbage Collector | Wired into `omnigraph cleanup`; the "retained version" clause maps to manifest-pinned versions | | Staged transactions (`execute_uncommitted` for append/delete/merge-insert) | #6781 (merged) | The fold's preferred commit shape (§4.4) | | `CommitBuilder` commit timeout; `skip_auto_cleanup` | v8 #6773; existing | Fold commit hygiene, same as every existing staged path | | Tombstone-preserving point lookup; `delete_no_wait`; `Sealed` shard status (drop-table 2PC) | v9 #7482, #7483, #7361 | Phase 3: stream-mode deletes; clean disable/drop of an enrolled table | | Unenforced primary key (immutable once set) | Lance 7 rule | Enrollment precondition — see §4.1 | Deliberately **not** consumed: Data Overlay files (v9-adjacent, voted experimental 2026-06 — a different fast-write shape targeting in-place overlays; watch-listed, not load-bearing here), MemWAL's multi-shard horizontal scale-out (deferred to Phase 2 with sharding specs). ## 4. Design ### 4.1 Enrollment Stream ingest is **per node/edge type**, declared in the schema: `@stream` annotation on the type. Schema apply records intent (no physical work — same posture as `@index`). Enrollment's one physical precondition runs at the first ingest write, not at apply: - The table's key column must carry Lance's unenforced-PK annotation (MemWAL needs a PK for last-write-wins upsert and PK lookups). Under Lance ≥7 the PK is **immutable once set**, so the enrollment step mirrors `migrate_v1_to_v2`'s guarded shape exactly: key column already the PK → no-op; no PK → set it; a *different* PK → loud refusal. - The MemWAL index (sharding spec, `maintained_indexes`, writer config defaults) is created through the existing index chokepoint discipline: derived state, idempotent, never fails a logical op. ### 4.2 Write path and ack A new explicit surface — `omnigraph ingest --stream` / `POST /graphs/{id}/ingest` (NDJSON stream) — routes rows to a per-`(table, branch)` `ShardWriter` held by the engine (warm, invariant 15). Per row, **before** the WAL append, the synchronously-checkable validations run: type/shape, enum, range/`@check`, required fields, defaults. These fail the row *before* anything is durable — invariant 9 holds for everything checkable without a base-table read. Referential integrity (edge endpoints), cardinality, and cross-row uniqueness need base-table state and are validated **at fold time** (§4.4). A row that fails there cannot be un-acked; it lands in a per-graph dead-letter table (`_ingest_rejects`) with the typed error, surfaced via `ingest status` and the server API — bounded and observable (invariant 13), never silent, and documented as the stream-mode contract difference. This is the standard streaming-system trade, stated honestly rather than hidden. Ack = the Lance durability waiter for the row's WAL entry resolving. Group commit falls out of WAL-flush batching; the flush interval is the latency/throughput knob and lives in `writer_config_defaults` so every writer process shares it. ### 4.3 Visibility tiers - **Default reads: unchanged.** A query reads its manifest snapshot (invariant 3). WAL contents are not graph-visible until folded. Strong consistency at graph-commit granularity, exactly as today. - **Fresh-tier reads: explicit opt-in** (query-level annotation, e.g. `read fresh`). The planner unions base-table-at-snapshot + flushed memtables + (same-process) the live MemTable via Lance's LSM merging read. Same-process this is read-your-writes (the spec's strong-consistency condition: MemTable access + direct shard-manifest reads); cross-process it is flushed-WAL-consistent (bounded lag = the unflushed MemTable). Both are documented per-tier. This is invariant 6's explicit/auditable/non-default clause used precisely as designed — not a silent fallback. ### 4.4 The fold The fold is the MemWAL *merger* (spec §Background Job Expectations) run under OmniGraph's commit discipline: 1. Acquire the per-`(table, branch)` write queue (serializes against interactive writers and other folds in-process). 2. Fold-time validation of pending generations (RI, cardinality, uniqueness via `loader::composite_unique_key`) against the current snapshot + generations-in-order; rejects → dead-letter, never a placeholder node (deny-list). 3. Merge flushed generations in ascending generation order via merge-insert. **Preferred shape:** `execute_uncommitted` (staged) → `commit_staged` → one `ManifestBatchPublisher::publish` carrying the table version *and* the graph-commit lineage rows — i.e. the fold is an ordinary OmniGraph writer. **Open question Q1:** whether the MemWAL index's `merged_generations` update (which the spec requires to ride the data commit atomically) is expressible through the staged path. If not, Phase 1 runs the fold's merge as an inline-commit residual under a `SidecarKind::IngestFold` recovery sidecar — precisely the `optimize_indices` precedent — and migrates to staged when upstream exposes it. 4. One fold = **one graph commit** (actor `omnigraph:ingest`, or the enrolled writer's actor when single). Lineage lands in `__manifest` in the same CAS as always. Fold triggers: generation count, byte size, max lag — plus a synchronous `omnigraph ingest fold` for operators and tests. Crash windows: before WAL flush → row never acked, nothing durable. After ack, before fold → WAL replays (spec §WAL Replay) under the next claimant's epoch; acked data cannot be lost. Mid-fold → either the sidecar rolls the residual forward (Phase 1 inline shape) or the staged state is invisible and the fold re-runs; `merged_generations`' atomic update makes re-merge of an already-merged generation impossible (spec Appendix 2). After fold, before GC → LSM dedupe makes double-reads correct (spec §Reader Consistency). ### 4.5 Branch interactions Phase 1 rule, chosen for smallness: **branch create/merge/delete on an enrolled table first drives its WAL to quiescence** (fold-to-empty under the same write queue). A branch never forks with an un-folded WAL tail, so branch semantics are untouched. Relaxing this (per-branch WAL lanes) is a Phase 3+ question; MemWAL shards are per-dataset-path, and branches are separate version lineages, so the mapping exists — it is deferred, not blocked. ### 4.6 Embeddings (`@embed`) interaction Embedding computation is external and slow; it cannot sit between the client and the WAL ack. Stream-mode `@embed` columns are therefore computed **at fold time** (the fold pipeline calls the embedding client on the folded batch, same code path as `load`). Consequence: memtable-maintained *vector* indexes cannot cover not-yet-embedded rows — fresh-tier vector search over an `@embed`-sourced column sees rows only post-fold (fresh-tier FTS and scans are unaffected). Documented per-column; RFC-015 owns the deeper embedding pipeline. ## 5. Reconciliation with Lance v8.0.0 (Phase 0 — the bump) v8.0.0 released ~2026-07-01 (RC3 vote closed). This RFC's Phase 0 is the 7.0.0 → 8.0.0 bump as its own PR with the full lance.md alignment-audit stanza. Items already identified as load-bearing: - **MemWAL hardening is the reason v8 is the floor:** #7110 (fencing WAL sentinel on claim — closes the spec's documented GC-fencing hazard, which this RFC would otherwise inherit), #7132 (fenced flush surfaces to durability waiters — without it our ack path can hang), #7215/#7066/#7284/ #7256 (fresh-tier read planning this RFC consumes in Phase 2), #7054 (HNSW params for memtable writers). - **Blob compaction fix landed** (#7017: all `BlobKind` in blob-v2 `compact_files`): flip `LANCE_SUPPORTS_BLOB_COMPACTION`, remove the `optimize` skip branch — the `compact_files_still_fails_on_blob_columns` surface guard turns red on the bump by design and forces this. - **Critical merge-insert fix** (#7251: silently dropped matches when a leading payload column is all-null): audit whether any existing OmniGraph merge path could have hit it (all-null embedding columns in `LoadMode:: Merge` are plausible); add a pinned regression either way. - **Breaking index-segment migration** (#6869 bitmap→segment-based, #6997 removed segment builder, #7013 BTree on the segmented framework): OmniGraph builds BTREE/inverted/vector through one chokepoint — expect API churn there; re-run every `lance_surface_guards.rs` guard first, per the standing bump protocol. - **FTS tokenizer default churn** (#6968 ICU default, then #7006 restored simple): verify the effective default matches what OmniGraph's FTS contract shipped (Hyrum's-law item — our search results' tokenization is observable behavior). - **#7158** (fail-fast casting for indexed columns) intersects schema apply; **#6916** (index-accelerated filtered `count_rows`) and **#7129** (no index-file listing after writes) should show up as free improvements in `warm_read_cost.rs` / `write_cost.rs` — re-baseline, don't loosen budgets. - **Namespace feature flags in `__manifest`** (#7191): dir-catalog-only, but re-verify the v7 audit's conclusion that Lance's native `DirectoryNamespace` stays decoupled from OmniGraph's manifest (that decoupling was contingent on the legacy boolean PK key — confirm the contingency still holds under v8's flag writes). - Re-check the two still-open residual issues on the bump: #6666 (vector index two-phase) — **still open**, `create_vector_index` stays inline; #6658 shipped back in 7.0.0 (MR-A unchanged, still pending). ## 6. Reconciliation with the Lance v9 line (beta) v9 is at beta.10; **not a production target until stable**. What it adds that this RFC phases on: - **Stream-mode deletes:** tombstone-preserving point lookup (#7482) and `ShardWriter::delete_no_wait` (#7483) give MemWAL delete semantics. Phase 3 adopts them; until then stream ingest is insert/upsert-only and delete remains an interactive-path operation (the D2 rule's scope is unchanged — note MR-A may retire D2 on the interactive path independently). - **Enrollment teardown:** `Sealed` shard status + `ShardWriter::abort` (#7361, the drop-table 2PC fence — also the subject of the 2026-06 format vote #7418) is the clean disable path for `@stream` removal. - **#7468** (reject `defer_index_remap` with stable row IDs) touches the future stable-row-id traversal plans (invariants.md known gap) — flag for that workstream, not this one. - **Data Overlay files** (#7401, voted experimental #7447): a different fast-targeted-write primitive (masking overlays over base files). If it matures it may fit *small in-place updates* better than WAL-upsert-fold; watch-listed as a possible Phase 4 refinement, not a dependency. - MemWAL fixes keep landing on v9 betas (#7489 cross-generation block-list on in-memory scan arms) — confirming the experimental-spec churn risk (§10) and the value of keeping our exposure transient-state-only. ## 7. Composition with upcoming Lance multi-table commits Upstream state (verified 2026-07-02): issue #6668 ("Multi-dataset atomic commit primitive") is **closed into RFC discussions**, not shipped. Two live proposals: - **#7260 — batch commit record** (v2, supersedes #6775; authored by this team — disclosure as in the upstream thread): stage per-table via `execute_uncommitted` → publish **one immutable record** in a `_txn/` log via put-if-not-exists → idempotent, reader-completable finalize. Namespace- reader atomicity; exclusive commit authority per enrolled table (§3.3a there). - **#7264 — branching MTT** (authored by the Lance maintainer, jackye1995): per-transaction shallow-clone branches, one atomic `__manifest` (catalog) repoint as the commit, leased **barrier commits** to fence the per-table fast path during rebase, riding #7263 (cross-branch rebase) and #7185 (UUID branch paths). Notably, its Alternative A (fast-forward-main instead of pointer-swap) is OmniGraph's current architecture verbatim — N dataset commits + a write-ahead intent record + idempotent roll-forward recovery — so one plausible upstream outcome is Lance adopting the sidecar shape this codebase already runs. Both keep the settled invariants (#7222/#7176): physical files are the eventual source of truth; put-if-not-exists is the primitive; no per-commit work on a catalog table. Neither is in v8 or the v9 betas. **How this RFC composes — the one design rule.** The fold (and every other writer) reaches publication only through the `ManifestBatchPublisher` seam. That is already true and this RFC keeps it true. Consequences: - **Under a #7260-shaped primitive:** the fold's N staged table commits + the manifest publish collapse into operations of one batch record. The residual Lance-HEAD-before-manifest gap — the entire reason recovery sidecars exist (invariant 5) — disappears *by construction*, and the sidecar machinery (including this RFC's possible `SidecarKind::IngestFold`) becomes removable scaffolding. This is exactly the degrade-to-no-op shape invariants.md says to prefer. OmniGraph's `__manifest` remains as the *graph-semantic* layer (lineage rows, branch heads, snapshot pinning) — the batch record is a commit mechanism, not a lineage store; graph_commit rows simply become one more operation in the batch, keeping lineage atomic with visibility as today. Adoption = swapping the publisher's internals; WAL, fold logic, and read paths are untouched. - **Under a #7264-shaped primitive:** OmniGraph's manifest *is* the catalog in that RFC's vocabulary, and OmniGraph already runs the consumer-side pattern (lazy per-write forks ≈ transaction branches; publish CAS ≈ the atomic repoint). The genuinely new upstream piece — the **leased barrier commit** — is independently valuable to OmniGraph: it is a concrete candidate for the cross-process serialization primitive that two documented known gaps explicitly wait on (recovery-sweep-vs-foreign-live-writer; cross-process fork reclaim). One caveat, already raised on the upstream thread (ragnorc, 2026-06-13): a TTL lease alone cannot make "barrier held + tip unmoved + repoint" atomic — if the critical section outruns the lease, a competing writer breaks the barrier and advances the tip, and the coordinator's repoint silently loses that commit — so the barrier needs an epoch/generation, not just a TTL. Any OmniGraph adoption inherits that requirement. If #7264 lands, adopt the (epoch-carrying) barrier for those gaps even if the fold never uses MTT directly. **Update (2026-07-05):** the 2026-07-03 comment on #7264 proposes the unification directly — detached-commit staging + the #7260 record as the intent + fast-forward-main promotion, prototype-validated (24-test probe suite). The barrier is now a slot-occupying content-identical commit (epoch in the record log, enforced in the writer's commit path), answering the TTL-lease caveat above structurally. Measured: ~450–600 ms per transaction, ~6–7 txn/s on S3 with **group commit as the lever** — i.e. the same amortization shape as this RFC's fold, strengthening Phase 4's "publisher swap, not redesign" claim with numbers. The record log also carries a durable pruned-through GC boundary (advance-before-delete, writer re-verifies after put) — see open question 4. - **MemWAL + MTT together:** the fold's merge commit, its `merged_generations` index update, and the manifest publish could all join one batch — closing Q1 (§4.4) from above rather than below. The WAL is upstream of the commit point in every scenario; the commit point's mechanics can change under it freely. That orthogonality is the payoff of constraint 1 (§0) and the reason this RFC refuses any design where WAL state participates in publication authority. ## 8. Invariants and deny-list walk - **1 Respect the substrate:** MemWAL consumed as spec'd; zero WAL code owned (§3). ✅ - **2 Manifest-atomic visibility:** WAL data becomes graph-visible only via the fold's single publish; fresh-tier is explicitly outside graph visibility. ✅ - **3 One snapshot per query:** unchanged; fresh-tier unions the snapshot with WAL tiers *at plan time*, never re-reads head mid-query. ✅ - **4 One publish boundary per mutation:** the fold is one boundary. ✅ - **5 Recovery coverage:** fold Phase 1 residual (if Q1 forces inline) gets `SidecarKind::IngestFold`; WAL-side recovery is Lance's replay + fencing. ✅ - **6 Strong consistency default:** ack = durable; default reads unchanged; fresh-tier is the invariant's own explicit/read-only/auditable/non-default clause. ✅ - **7 Indexes derived:** memtable maintained indexes + `index_catchup` are Lance's implementation of this invariant; `@embed` vector gap documented (§4.6). ✅ - **9 Loud integrity:** sync-checkable validation pre-ack; fold-time RI to a dead-letter table with typed errors — a *documented contract difference*, not a silent weakening; no placeholder nodes ever. ⚠️ documented - **13 Bounded/observable failures:** dead-letter + `ingest status` + fold lag metrics on the observability surface. ✅ - **15 One source of truth, cheaply derived:** ShardWriter handles held warm; shard-manifest `version_hint` is the cheap probe; WAL state is transient (folded + GC'd), so nothing long-lived can drift. ✅ - **Deny-list sweep:** no custom WAL (Lance's); no acks before durability; no per-table publish outside the publisher (fold uses it); no job queue for manifest-derivable state (the fold is a reconciler over MemWAL state, which is NOT manifest-derivable — it is upstream input, the one shape the rule permits); no silent eventual consistency (explicit tier); no raw FS I/O (WAL files are Lance-managed inside the table dir). ✅ ## 9. Testing plan (per testing.md) - **Surface guards** (`lance_surface_guards.rs`): pin ShardWriter claim/ append/flush shapes, durability-waiter semantics, `merged_generations` atomic-update behavior, tombstone APIs when v9 lands. Bump protocol unchanged: guards run first. - **Failpoints:** WAL-append failure → no ack, zero durable state; crash after ack before fold → replay recovers the row (the acked-implies-durable oracle); fold crash windows (each side of `commit_staged`/publish, sidecar lifecycle if inline); two folds racing via `Rendezvous` → exactly-once via `merged_generations`; fencing: zombie writer post-claim cannot ack. - **DST:** concurrency cells ride PR #318's `Cohort` multi- coordinator harness (in-process and two-process tiers) — two coordinators ingesting + folding the same graph must converge to one linear chain; extend the op alphabet with `ingest` + `fold` ops; model gains a "pending" row set; oracles: acked rows survive any crash, fold equivalence (model == graph after fold), fresh-tier read-your-writes in-process; `FaultAdapter` covers WAL object I/O; S3 battery cell for the WAL path. - **Cost budgets** (`helpers::cost`): ack-path object-store ops O(1) and flat in history *and* in WAL depth; fold cost bounded by generations folded (working set), never by table history; extend `write_cost.rs` + `write_cost_s3.rs` (the opener term is S3-only, per the backend-split note). - **CLI parity:** `ingest --stream` in `parity_matrix.rs` (embedded vs remote) and a DST cross-backend cell. ## 10. Risks - **MemWAL is experimental upstream** (spec banner; live format votes — #7418 Status field mid-2026-06). Mitigation is structural: WAL state is *transient* (folded then GC'd), so a format change between Lance versions can be handled by fold-to-quiescent before the bump; no long-lived on-disk state depends on the MemWAL format. This must stay true — resist any future temptation to park durable state in WAL form. - **Q1 (staged fold)** may force the inline-residual shape initially — a known, sidecar-covered, deny-list-documented pattern with two precedents. - **Dead-letter semantics** are a real contract change for stream mode; user-docs must present it before the endpoint ships (maintenance contract rule 1). ## 11. Phasing | Phase | Deliverable | Gate | |---|---|---| | 0 | Lance 7.0.0 → 8.0.0 bump: full alignment audit stanza, guards re-run, blob gate flipped, #7251 audit | v8.0.0 (released) | | 1 | `@stream` enrollment; single-shard ShardWriter per (table, branch); sync validation; ack on durability waiter; fold (staged if Q1 allows, else sidecar-covered inline) publishing one graph commit; manifest-tier reads only; dead-letter + `ingest status`; branch-ops fold-to-quiescent | Phase 0 | | 2 | Fresh-tier reads (explicit opt-in); `maintained_indexes` (FTS + vector, `@embed` caveat); multi-shard via sharding specs; group-commit tuning | Phase 1 | | 3 | Stream-mode deletes (tombstones); `@stream` removal via Sealed/abort; per-branch WAL lanes exploration | Lance v9 stable | | 4 | Adopt the multi-table commit primitive (#7260/#7264 outcome): publisher-seam swap, retire fold sidecars; evaluate barrier commits for the cross-process known gaps | upstream vote/ship | ## 12. Open questions 1. **Q1:** Can `merged_generations` ride `execute_uncommitted` + `commit_staged`, or is the fold's merge inline-only today? (Determines Phase 1 shape; upstream question — possibly a #6781-shaped PR we offer.) 2. Fold-time RI rejects: dead-letter only, or optional strict mode where a fold halts (back-pressuring ingest) on the first reject? 3. Actor granularity in fold lineage: single ingest actor vs. per-WAL-entry actor aggregation into commit metadata. 4. `cleanup` integration ordering with the Q8 cleanup-resurrection watermark (the internal-tables GC gap) — does WAL GC land before or after it? **Note (2026-07-05):** the #7264 record-log design carries exactly the durable GC-boundary primitive `iss-cleanup-boundary-watermark` asks for (GC advances the boundary before deleting; a writer re-verifies `seq > boundary` after a successful put). If that ships, the watermark arrives as substrate rather than omnigraph protocol — factor into the build-vs-wait decision on that P1. 5. Should fresh-tier be exposed over HTTP in Phase 2, or engine/CLI-only until the consistency-tier docs mature?