docs(architecture): ADR-012 — pre-decomposition consistency rulings; tree verified single-valued

Full-tree review before task decomposition found five composition
defects (mechanisms specced correctly in isolation, composition
unruled) and a set of caller-facing gaps. ADR-012 rules each:

- Sweeps are never errors: aborts are GcAbortCause report data in
  Ok(SweepReport) (ProtectFailed / SweeperLock / NoLivenessSources);
  GcAborted retired from the error enum; direct-delete refusal is
  the GcRefuse error covering the full protection set
- Pin token gains its wire shape: blobs/put response {token, digest};
  blobs/have's token-renewal form (one digest + token); token
  validity domain = the minting serving node, process-lifetime
  mapping
- Fleet mode is an explicit constructor declaration (fleet: true),
  never inferred from engine choice
- All fleet GC state hosts on the fleet's kv engine (postgres) — one
  arbitration domain; large=pg-lo fleet nodes required onto the same
  pg instance (composite predicate's new clause); large=local fleet
  puts pin-row-first, publish-second
- Engine-state seam reduced: sqlite pin/sweep-lock bodies dropped
  (dead machinery); non-SQL engines stage delete-window candidates
  in-process; one-window-host rule per instance
- Facade clarifications: fall-through for all key-addressed ops,
  kv-only put-time rejection, mem+local dual-tier valid, error-model
  member/return-shape ruling (trait/facade family split), has ->
  bool, PinState variants, fleet liveness-table registration form,
  window executor = the next sweep

Alignment edits across all specs and ADR-005/008/009/010/011
(bracketed corrections per the established pattern); OQ-11 (pg-only-kv
feature graph) added to the parked index for auditability.

Verification: two independent review passes; all findings resolved;
verdict READY for task decomposition.
This commit is contained in:
glm-5.3-flash committed 2026-10-03 07:14:45 +00:00
1 parent abf8c5ae9c
commit 7b9d904b8a
13 files changed
+771 -184

No files matched your search

+26 -5
View File
@@ -1,6 +1,6 @@
--- ---
status: draft status: draft
last_updated: 2026-10-03 (ADR-010/011 — I/O seams + GC-state home; vocabulary pinned, fs → large, mem joins kv) last_updated: 2026-10-03 (ADR-012 — pre-decomposition consistency rulings: sweep-abort shape, token wire shape, fleet activation, GC-state host)
--- ---
# alkblobs — Architecture # alkblobs — Architecture
@@ -88,6 +88,24 @@ there — not inherited as hedges:
`large`** (feature `fs` → `large`) — its routing criterion is `large`** (feature `fs` → `large`) — its routing criterion is
length, not medium — with constructor modes (dual-tier default, length, not medium — with constructor modes (dual-tier default,
kv-only, mem-only) pinned in the same ADR's table. kv-only, mem-only) pinned in the same ADR's table.
- **ADR-012** (2026-10-03) is the pre-decomposition consistency round:
the full-tree review found five composition defects and this ADR
rules each — sweeps are **never errors** (`Ok(SweepReport)` with
`aborted: Option<GcAbortCause>`; `GcAborted` retired from the error
enum, direct-delete refusal renamed `GcRefuse`;
`NoLivenessSources` added as the third cause variant); the pin
token gains its **wire shape** (`blobs/put` response `{token,
digest}`; `blobs/have`'s token-renewal form; token validity domain
= the minting serving node); **fleet mode is an explicit
constructor declaration** (`fleet: true`, never inferred from
engine choice); **all fleet GC state hosts on the fleet's kv engine
(postgres)** — one arbitration domain — with `large=pg-lo` fleet
nodes required onto the same pg instance (the composite predicate's
new clause) and `large=local` fleet puts pinned pin-row-first,
publish-second; and the engine-state seam's sqlite rows are
repaired (pin/sweep-lock bodies dropped; window staging for
non-SQL engines is in-process). The tree is now single-valued on
every implementation-facing signature.
## Architecture Documents ## Architecture Documents
@@ -97,7 +115,7 @@ there — not inherited as hedges:
| [requirements.md](requirements.md) | Consumer requirements REQ-1..4, pinned vocabulary (tier/engine/instance/node/fleet) | draft | | [requirements.md](requirements.md) | Consumer requirements REQ-1..4, pinned vocabulary (tier/engine/instance/node/fleet) | draft |
| [hashing-and-keys.md](hashing-and-keys.md) | Canonical hash, key encoding (the one-way door) | draft | | [hashing-and-keys.md](hashing-and-keys.md) | Canonical hash, key encoding (the one-way door) | draft |
| [store-api.md](store-api.md) | Store facade: put/get/stat/range/pin/batch/sweep surface, errors, invariants | draft | | [store-api.md](store-api.md) | Store facade: put/get/stat/range/pin/batch/sweep surface, errors, invariants | draft |
| [backends-and-dispatch.md](backends-and-dispatch.md) | Backend trait contract, tiers + engines table, size-threshold dispatch, constructor modes | draft | | [backends-and-dispatch.md](backends-and-dispatch.md) | Backend trait contract, tiers + engines table, size-threshold dispatch, constructor modes, fleet-validity | draft |
| [gc-and-namespaces.md](gc-and-namespaces.md) | Pooled CAS, liveness seams, mark-and-sweep, delete windows | draft | | [gc-and-namespaces.md](gc-and-namespaces.md) | Pooled CAS, liveness seams, mark-and-sweep, delete windows | draft |
| [ops-surface.md](ops-surface.md) | alkcall-backed have/need + fetch/put ops, ACL mapping | draft | | [ops-surface.md](ops-surface.md) | alkcall-backed have/need + fetch/put ops, ACL mapping | draft |
| [open-questions.md](open-questions.md) | Centralized OQ tracker (incl. promoted Phase 0 register) | draft | | [open-questions.md](open-questions.md) | Centralized OQ tracker (incl. promoted Phase 0 register) | draft |
@@ -116,14 +134,17 @@ there — not inherited as hedges:
| [008](decisions/008-trait-size-probe-fleet-gc-and-large-engines.md) | Trait `size` probe; fleet GC (DB-backed pins, advisory-locked sweeper); large-tier engines | Accepted | | [008](decisions/008-trait-size-probe-fleet-gc-and-large-engines.md) | Trait `size` probe; fleet GC (DB-backed pins, advisory-locked sweeper); large-tier engines | Accepted |
| [009](decisions/009-pg-lo-large-tier-engine.md) | pg-lo — postgres Large Objects as the large tier's second engine (admitted on POC #7) | Accepted | | [009](decisions/009-pg-lo-large-tier-engine.md) | pg-lo — postgres Large Objects as the large tier's second engine (admitted on POC #7) | Accepted |
| [010](decisions/010-backend-io-seams-and-fleet-state-home.md) | Backend I/O seams (read cursor, staged put); GC state store-core-owned via the engine-state seam; kv fleet-validity; pin-token renewal | Accepted | | [010](decisions/010-backend-io-seams-and-fleet-state-home.md) | Backend I/O seams (read cursor, staged put); GC state store-core-owned via the engine-state seam; kv fleet-validity; pin-token renewal | Accepted |
| [012](decisions/012-pre-decomposition-consistency-rulings.md) | Pre-decomposition rulings: sweep aborts are report data (`GcAborted` retired); pin-token wire shape; fleet-mode constructor declaration; GC state hosts on the kv engine; sqlite seam reduction | Accepted |
| [011](decisions/011-vocabulary-tiers-engines-and-fleet-state.md) | Vocabulary: tier/engine/instance/node/fleet; mem joins kv; large tier renamed `large`; constructor modes | Accepted | | [011](decisions/011-vocabulary-tiers-engines-and-fleet-state.md) | Vocabulary: tier/engine/instance/node/fleet; mem joins kv; large tier renamed `large`; constructor modes | Accepted |
## Open Questions ## Open Questions
Tracked in [open-questions.md](open-questions.md). The Phase 0 register Tracked in [open-questions.md](open-questions.md). The Phase 0 register
(OQ-BL-01..06) is promoted there with its resolutions; two questions (OQ-BL-01..06) is promoted there with its resolutions; three questions
remain parked — OQ-07/OQ-08 externally-owned (alkgit seam mapping, are parked — OQ-07/OQ-08 externally-owned (alkgit seam mapping,
alkfs intake; carried for visibility, gating nothing here). OQ-09 alkfs intake; carried for visibility, gating nothing here) and
OQ-11 `deferred(scope)` (the pg-only-kv feature graph; a named
requirement from outside this crate decides it). OQ-09
(namespace-visibility default) resolved closed-by-default; OQ-10 (namespace-visibility default) resolved closed-by-default; OQ-10
(second kv engine) resolved by ADR-007. The pg-lo large-engine question (second kv engine) resolved by ADR-007. The pg-lo large-engine question
resolved to *sequenced work*, not a parked question: REQ-2/ADR-008 resolved to *sequenced work*, not a parked question: REQ-2/ADR-008
+31 -11
View File
@@ -1,6 +1,6 @@
--- ---
status: draft status: draft
last_updated: 2026-10-03 (ADR-010 I/O seams + ADR-011 rename — fs tier → large; mem is a kv engine) last_updated: 2026-10-03 (ADR-012 — composite predicate gains the pg-lo same-instance clause; kv-only rejection is put-time; GC state hosts on the kv engine; window wording)
--- ---
# Backends, tiers, and dispatch # Backends, tiers, and dispatch
@@ -92,7 +92,12 @@ is ever reachable through that door.
and sweep locks are store-core-owned via a crate-internal and sweep locks are store-core-owned via a crate-internal
engine-state companion seam on the SQL-backed engines (sqlite, engine-state companion seam on the SQL-backed engines (sqlite,
postgres, pg-lo) — invisible at the public trait; `local` and `mem` postgres, pg-lo) — invisible at the public trait; `local` and `mem`
cannot express it and are never fleet engines. cannot express it and are never fleet engines. **All fleet GC state
(pin rows, window rows, the sweep lock) hosts on the fleet's kv-tier
engine — postgres — regardless of the large tier's engine; pg-lo
hosts no fleet state** (ADR-012 §4). Per-engine clause applicability:
ADR-012 §5's table (sqlite and pg-lo carry window-family clauses
only; pin/sweep-lock clauses are the postgres host's).
## Shipped tiers and engines ## Shipped tiers and engines
@@ -216,12 +221,19 @@ load-bearing for fleets):
two sqlite engines are two pools, never one. two sqlite engines are two pools, never one.
- **The composite fleet-validity rule (one sentence, both tiers):** a - **The composite fleet-validity rule (one sentence, both tiers):** a
constructor configuration is fleet-valid iff **kv = postgres AND constructor configuration is fleet-valid iff **kv = postgres AND
(large = pg-lo OR large = `local`-on-declared-shared-media OR (large = pg-lo *on the same pg instance as the kv engine* OR
large = `local`-on-declared-shared-media OR
large = none with the re-routing posture carrying pool large large = none with the re-routing posture carrying pool large
content)**. Any other combination over one shared pool is invalid — content)** — plus an explicit `fleet: true` constructor declaration
(ADR-012 §3: fleet mode is never inferred). Any other combination over one shared pool is invalid —
the constructor requires the declarations that make this predicate the constructor requires the declarations that make this predicate
checkable per node; cross-node truth remains the deployment's checkable per node; cross-node truth remains the deployment's
verified invariant (ADR-008's seam). This predicate is verified invariant (ADR-008's seam). The pg-lo same-instance clause
formalizes what ADR-009's consolidation posture already assumed
(pool content lives in the pg instance the kv tier rides) — it is
what makes the joint entry+pin tx expressible; cross-instance
kv=postgres + large=pg-lo is valid only as a non-fleet
configuration. This predicate is
backends-and-dispatch.md's and ADR-011 §4's table combined; it is backends-and-dispatch.md's and ADR-011 §4's table combined; it is
stated here once so no reader composes it by inference. stated here once so no reader composes it by inference.
@@ -245,18 +257,24 @@ load-bearing for fleets):
### Constructor modes (ADR-011 §4) ### Constructor modes (ADR-011 §4)
- **dual-tier** (the default): kv + large, size-threshold dispatch - **dual-tier** (the default): kv + large, size-threshold dispatch
(ADR-003). (ADR-003). Any engine pair the tables allow — including
`kv = mem` + `large = local`, the dispatch-coverage test shape
(ADR-012 §6.3; both tiers are load-bearing there too).
- **kv-only** (ADR-008's re-routing client posture, now named): no - **kv-only** (ADR-008's re-routing client posture, now named): no
large tier; over-threshold puts are a constructor-time error (the large tier; over-threshold puts are rejected **at put time** —
re-routing posture handles them via the ops surface — the known-length puts reject immediately, unknown-length puts reject at
consumer's composition, not a store mode). mid-stream threshold overflow (ADR-012 §6.2: lengths are not known
at construction, so "constructor-time error" was strictly
impossible). The re-routing posture handles over-threshold content
via the ops surface — the consumer's composition, not a store mode.
- **mem-only:** the mem engine alone, no dispatch threshold in - **mem-only:** the mem engine alone, no dispatch threshold in
effect; a testing/embedder-ephemeral posture, never production. effect; a testing/embedder-ephemeral posture, never production.
- Single-tier SQL modes (postgres-only, pg-lo-only *tiers*) do not - Single-tier SQL modes (postgres-only, pg-lo-only *tiers*) do not
exist: both tiers are load-bearing (ADR-004). "Postgres-only" as in exist: both tiers are load-bearing (ADR-004). "Postgres-only" as in
one SQL *instance* serving both tiers exists and is the ADR-009 one SQL *instance* serving both tiers exists and is the ADR-009
consolidation: dual-tier mode with kv=postgres + large=pg-lo over consolidation: dual-tier mode with kv=postgres + large=pg-lo over
one pool. one pool — under fleet mode, required to be literally the same
instance (ADR-012 §4).
## Where a *new* engine could come from ## Where a *new* engine could come from
@@ -282,6 +300,7 @@ alkfs intake may name needs externally; OQ-08).
| [009](decisions/009-pg-lo-large-tier-engine.md) | pg-lo admitted | postgres Large Objects as the large tier's second engine, on POC #7 | | [009](decisions/009-pg-lo-large-tier-engine.md) | pg-lo admitted | postgres Large Objects as the large tier's second engine, on POC #7 |
| [010](decisions/010-backend-io-seams-and-fleet-state-home.md) | I/O seams + GC-state home | read cursor / staged put; store-core-owned GC state via the engine-state seam; the kv fleet-validity rule | | [010](decisions/010-backend-io-seams-and-fleet-state-home.md) | I/O seams + GC-state home | read cursor / staged put; store-core-owned GC state via the engine-state seam; the kv fleet-validity rule |
| [011](decisions/011-vocabulary-tiers-engines-and-fleet-state.md) | Vocabulary | tier/engine/instance/node/fleet; mem is a kv engine; fs → large; constructor modes | | [011](decisions/011-vocabulary-tiers-engines-and-fleet-state.md) | Vocabulary | tier/engine/instance/node/fleet; mem is a kv engine; fs → large; constructor modes |
| [012](decisions/012-pre-decomposition-consistency-rulings.md) | Fleet activation & GC-state host | `fleet:` constructor declaration; all fleet GC state on the kv engine; pg-lo same-instance clause; kv-only put-time rejection |
## Open Questions ## Open Questions
@@ -305,6 +324,7 @@ alkfs intake may name needs externally; OQ-08).
- rudolfs notes — `list()` anti-lesson, decorator alternative noted and - rudolfs notes — `list()` anti-lesson, decorator alternative noted and
not adopted (threshold dispatch chose the simpler policy; not adopted (threshold dispatch chose the simpler policy;
ADR-003 §Context) ADR-003 §Context)
- ADR-003, ADR-004, ADR-005; [store-api.md](store-api.md) (the invariants - ADR-003, ADR-004, ADR-005; ADR-012 (fleet activation, GC-state
host, the composite predicate's same-instance clause); [store-api.md](store-api.md) (the invariants
engines must satisfy); [requirements.md](requirements.md) (the pinned engines must satisfy); [requirements.md](requirements.md) (the pinned
vocabulary) vocabulary)
@@ -8,7 +8,13 @@ advisory-locked single sweeper, SQL-level delete arbitration under
shared-pool engines (REQ-2) — while keeping this ADR's invariants shared-pool engines (REQ-2) — while keeping this ADR's invariants
and the in-process protocol unchanged everywhere else. The "one pool and the in-process protocol unchanged everywhere else. The "one pool
per node" phrasing below reads as "per deployment" for fleets; the per node" phrasing below reads as "per deployment" for fleets; the
vocabulary is pinned in requirements.md) vocabulary is pinned in requirements.md. ADR-012 rules the abort
*carrier*: sweep aborts — including the protect callback's — are
`GcAbortCause` report data in `Ok(SweepReport)`, never a typed sweep
error (this ADR's "typed `GcAborted`" phrasing reads through that
ruling), and direct `delete()`'s refusal covers the full protection
set (this ADR's "pinned keys" wording reads as ADR-012 §6.4's
"protected keys"))
## Context ## Context
@@ -73,9 +79,11 @@ served by range reads).
observes the replacement (the local analog of the ops-layer pin observes the replacement (the local analog of the ops-layer pin
token contract). token contract).
3. **Protect callback** — consulted pre-sweep; may add known-live keys 3. **Protect callback** — consulted pre-sweep; may add known-live keys
or **abort the run** (typed `GcAborted`, nothing deleted). iroh's or **abort the run** (nothing deleted; the abort is report data —
`ProtectOutcome::Abort` conclusion adopted: a flaky protection `GcAbortCause::ProtectFailed` in `Ok(SweepReport)` per ADR-012
source skips the sweep rather than risk deletion. §1, not a typed error; this ADR's original "typed `GcAborted`"
phrasing is superseded on the carrier only — the semantics iroh's
`ProtectOutcome::Abort` conclusion adopted stand).
### Sweep: mark → delete window → commit ### Sweep: mark → delete window → commit
@@ -133,10 +141,14 @@ served by range reads).
existence flips atomically per key). existence flips atomically per key).
- **Delete-then-recover is a re-put** (validated: byte-identical under - **Delete-then-recover is a re-put** (validated: byte-identical under
the same key); no tombstone layer exists. the same key); no tombstone layer exists.
- **Direct `delete(key)`:** refuses pinned keys with a typed error - **Direct `delete(key)`:** refuses protected keys (pinned,
(same arbitration); permitted for embedder correction flows on registered-source-live, or protect-callback-named — the full
unpinned entries. Outside-of-sweep deletion is unusual by posture — protection set, ADR-012 §6.4; this ADR's original "refuses pinned
most deletion should flow through sweeps. keys" wording is superseded upward; the refusal is the typed
`GcRefuse` error, ADR-012 §1) — same delete-time arbitration;
permitted for embedder correction flows on unprotected entries.
Outside-of-sweep deletion is unusual by posture — most deletion
should flow through sweeps.
### Traversal and scheduling ### Traversal and scheduling
@@ -202,6 +214,6 @@ OQ-07's.
gix-odb (alternates/pool prior art) gix-odb (alternates/pool prior art)
- ADR-002 (one address space), ADR-003 (list contract, pins), ADR-004 - ADR-002 (one address space), ADR-003 (list contract, pins), ADR-004
(manifest layers above), ADR-006 (verification limits inform recover (manifest layers above), ADR-006 (verification limits inform recover
semantics) semantics), ADR-012 (abort carrier + direct-delete protection set)
- [gc-and-namespaces.md](../gc-and-namespaces.md); - [gc-and-namespaces.md](../gc-and-namespaces.md);
[store-api.md](../store-api.md) [store-api.md](../store-api.md)
@@ -8,7 +8,9 @@ passed gate — the two §Consequences passages that said pg-lo "remains
unbuilt" are resolved by that same supersession; the tier this ADR's unbuilt" are resolved by that same supersession; the tier this ADR's
title and text call "fs" is renamed `large` by ADR-011; the fleet GC title and text call "fs" is renamed `large` by ADR-011; the fleet GC
mechanism's state home and the joint-tx ownership are ruled by mechanism's state home and the joint-tx ownership are ruled by
ADR-010; every other part of this ADR stands as written) ADR-010; fleet-mode activation, the GC-state host, the sweep-abort
return shape, and the window executor are ruled by ADR-012; every
other part of this ADR stands as written)
## Context ## Context
@@ -105,13 +107,16 @@ Under a fleet, the mechanism moves into the shared engine:
single-node/topology-local sweeps that cannot express their single-node/topology-local sweeps that cannot express their
protection as rows. protection as rows.
- **One sweeper per pool.** A fleet node acquires a pg advisory lock - **One sweeper per pool.** A fleet node acquires a pg advisory lock
before sweeping and holds it for the whole sweep. Two nodes before sweeping and holds it for the whole sweep. Two nodes
sweeping concurrently is not a state this crate tolerates — the sweeping concurrently is not a state this crate tolerates — the
lock is the admission gate, and a second-node sweep returns the lock is the admission gate, and a second-node sweep returns the
typed `GcAborted` (no deletion) rather than racing. `GcAborted` typed `GcAborted` (no deletion) rather than racing. `GcAborted`
thus carries two cause classes (protection-source failure — thus carries two cause classes (protection-source failure —
ADR-005; sweeper lock contention — this ADR); the typed error's ADR-005; sweeper lock contention — this ADR); the typed error's
fleet variant identifies which. fleet variant identifies which. *(Return carrier corrected by
ADR-012 §1: the abort is `GcAbortCause::SweeperLock` report data —
`Ok(report)` — the typed sweep error is retired; the two cause
classes and the no-deletion semantics stand.)*
- **Delete-time arbitration is SQL-level.** The delete window's - **Delete-time arbitration is SQL-level.** The delete window's
re-check (ADR-005: "arbitrate under the pin/liveness lock at delete re-check (ADR-005: "arbitrate under the pin/liveness lock at delete
time") executes as `SELECT … FOR UPDATE` against pin rows and time") executes as `SELECT … FOR UPDATE` against pin rows and
@@ -120,22 +125,30 @@ Under a fleet, the mechanism moves into the shared engine:
as rows carrying `visible_after = mark_end + window` (the delete as rows carrying `visible_after = mark_end + window` (the delete
window's delay — default 5 min, constructor-tunable per window's delay — default 5 min, constructor-tunable per
deployment); the executor (the sweeper, or the next sweep's owner) deployment); the executor (the sweeper, or the next sweep's owner)
re-arbitrates each staged candidate *at execution time* against re-arbitrates each staged candidate *at execution time* against
pins and liveness tables before deleting. This is the durable pins and liveness tables before deleting. This is the durable
delete window on every engine — the window table is engine rows on delete window on every SQL-backed engine — the window table is
sqlite as well; what differs by topology is only where the liveness engine rows on
state lives (in-process maps and callbacks for single nodes; sqlite as well; what differs by topology is only where the liveness
tables and SQL arbitration for fleets), not whether staging exists. state lives (in-process maps and callbacks for single nodes;
No external queue dependency is required (the pattern is tables and SQL arbitration for fleets), not whether staging exists.
engine-native transactions; consumer-side queues like honker or *(ADR-012 §5/§6.7: "every SQL-backed engine" is the final wording
pg-boss-lineage tools remain consumer-layer options, per ADR-004's — `local`/`mem` stage in-process, window-inline; the executor of
boundary). overdue staged rows is the next sweep.)* No external queue
dependency is required (the pattern is
engine-native transactions; consumer-side queues like honker or
pg-boss-lineage tools remain consumer-layer options, per ADR-004's
boundary).
- **Single-node postures are unchanged.** sqlite and single-instance - **Single-node postures are unchanged.** sqlite and single-instance
postgres keep ADR-005's in-process protocol verbatim: the pin map, postgres keep ADR-005's in-process protocol verbatim: the pin map,
RAII guards, and arbitration lock stay in-process; only the RAII guards, and arbitration lock stay in-process; only the
*persistence* of the delete window (the staged-candidate table) *persistence* of the delete window (the staged-candidate table) is
is shared machinery. The pin-row expiry/ttl machinery exists only shared machinery. The pin-row expiry/ttl machinery exists only
where fleet engines are active. where fleet engines are active. *(Activation is explicit: ADR-012
§3's `fleet: true` constructor declaration turns the fleet
machinery on; it is never inferred from engine choice alone — a
single-instance postgres declared `fleet: false` keeps the
in-process protocol.)*
### 3. The large tier is engine-selectable; fleet topology dictates which engines are permissible ### 3. The large tier is engine-selectable; fleet topology dictates which engines are permissible
@@ -166,7 +179,9 @@ valid:
(verified fetch/put; ADR-001). Client nodes' constructors are (verified fetch/put; ADR-001). Client nodes' constructors are
configured with **kv-only mode** (ADR-011 §4's name for what this configured with **kv-only mode** (ADR-011 §4's name for what this
bullet first called "kv-tier-only dispatch") — no local large tier bullet first called "kv-tier-only dispatch") — no local large tier
over pool content at all; a client node's *node-private, over pool content at all; over-threshold puts reject at put time
(ADR-012 §6.2), and the re-routing posture handles them via the ops
surface; a client node's *node-private,
consumer-owned* large-tier content is outside the pool and outside consumer-owned* large-tier content is outside the pool and outside
this crate's GC entirely: it is the consumer's own storage beside this crate's GC entirely: it is the consumer's own storage beside
the pool per ADR-004, not a constructor mode over pool content, and the pool per ADR-004, not a constructor mode over pool content, and
@@ -176,23 +191,21 @@ valid:
a client node is its own store for sub-threshold content plus its a client node is its own store for sub-threshold content plus its
registered ops-put path for large content — a consumer-side registered ops-put path for large content — a consumer-side
composition, not a store-level mode. composition, not a store-level mode.
- `pg-lo` (admitted by ADR-009; the gate): postgres Large Objects - **`pg-lo` (shipped; admitted by ADR-009):** postgres Large Objects
as the large tier's storage — as the large tier's storage —
the same "engine behind one contract" the same "engine behind one contract"
move as ADR-007, one tier over. Originally named as a candidate, move as ADR-007, one tier over. Originally named here as a candidate
admission gated on measured evidence (POC #7: LO write/read gated on measured evidence, POC #7 ran and passed that gate
curves at the packfile regime, tx-scoped handle cost under (`docs/research/poc-pglo-findings.md`: companion-table authority,
pooling, `pg_largeobject`/vacuum posture under churn, crash-orphan
behavior) and its own engine ADR. NOT shipped speculatively;
REQ-2's immediate fleet answers are shared media or re-routing;
pg-lo is the consolidation option if those are unacceptable. *Update
2026-10-03: POC #7 ran and passed its gate —
`docs/research/poc-pglo-findings.md` (companion-table authority,
`lo_get` window gets, durable put ≈ fs's durable put, cached gets `lo_get` window gets, durable put ≈ fs's durable put, cached gets
behind page-cache fs by a measured 20-50x single-stream, ~700 MB/s behind page-cache fs by a measured 20-50x single-stream, ~700 MB/s
aggregate at 16 readers). Superseded the same day: ADR-009 admitted aggregate at 16 readers), and ADR-009 admitted the engine the same
the engine — this bullet's original "candidate, not shipped" day — this bullet's original "candidate, not shipped / REQ-2's
posture is historical; the engine is shipped, feature `pg-lo`.* immediate answers are shared media or re-routing; pg-lo the
consolidation option" posture is historical; the engine is shipped,
feature `pg-lo`, and it is the fleet consolidation option as a
first-class engine (ADR-012 §4 adds the same-instance requirement
under fleet mode).
- **The no-mixed-large-tiers rule is a deployment invariant, enforced at - **The no-mixed-large-tiers rule is a deployment invariant, enforced at
the enforceable seam.** Cross-node configuration cannot be validated the enforceable seam.** Cross-node configuration cannot be validated
by any one constructor (it sees only its own node). What the by any one constructor (it sees only its own node). What the
@@ -240,8 +253,12 @@ partitioning failure this decision exists to prevent.
handle), advisory-lock coordination, and SQL-level arbitration handle), advisory-lock coordination, and SQL-level arbitration
added on top of the already-intricate delete-window state machine. added on top of the already-intricate delete-window state machine.
- `pg-lo` was built under ADR-009 after POC #7 passed — the - `pg-lo` was built under ADR-009 after POC #7 passed — the
measured, deliberate sequence of the ADR-006 pattern (admission measured, deliberate sequence of the ADR-006 pattern (admission
gate first, engine second), not a hedge; resolved same-day. gate first, engine second), not a hedge; resolved same-day.
- *(ADR-012 §4: fleet GC state hosts on the fleet's kv engine — the
large tier's `local` engine never joins the pin transaction;
large=local fleet puts commit the pin row first, publish
(commit-rename) second.)*
**Neutral** **Neutral**
@@ -7,7 +7,10 @@ Accepted (admission evidence: POC #7, passed 2026-10-03 —
gate. The tier this ADR calls "fs" is renamed `large` by ADR-011); gate. The tier this ADR calls "fs" is renamed `large` by ADR-011);
§2's "put = one transaction" tx-ownership statement is corrected by §2's "put = one transaction" tx-ownership statement is corrected by
ADR-010 §2 (the store core holds the tx; the engine contributes its ADR-010 §2 (the store core holds the tx; the engine contributes its
staged-put mechanics into it — engine content unchanged)) staged-put mechanics into it — engine content unchanged); ADR-012 §4
adds the same-pg-instance requirement for fleet-mode pg-lo nodes
(the consolidation posture this ADR described is the fleet-valid
shape).
## Context ## Context
@@ -56,8 +59,12 @@ consolidation option as a shipped engine.
2. **Put = one transaction**: BEGIN → `lo_create` → chunked `lowrite` 2. **Put = one transaction**: BEGIN → `lo_create` → chunked `lowrite`
(512 KiB statements — the measured knee; statement count, not (512 KiB statements — the measured knee; statement count, not
LOBLKSIZE, is what costs) → companion-row insert → COMMIT. LOBLKSIZE, is what costs) → companion-row insert → COMMIT.
Pin-before-publish (ADR-005/008) rides this shape: entry row + pin *(Ownership corrected by ADR-010 §2: the store core holds this tx;
row commit together with the content. Unknown-length puts stage the engine contributes LO writes + the companion-row insert into
it.)* Pin-before-publish (ADR-005/008) rides this shape: entry row + pin
row commit together with the content — the pin-row insert is the
store's contribution (hosted on the fleet's kv engine under fleet
mode, ADR-012 §4 — same pg instance, one tx). Unknown-length puts stage
into the LO and commit the row on completion; rollback discards into the LO and commit the row on completion; rollback discards
both. both.
3. **Get = `lo_get(oid, off, len)` windows by default** (POC #7 C4): 3. **Get = `lo_get(oid, off, len)` windows by default** (POC #7 C4):
@@ -100,7 +107,7 @@ consolidation option as a shipped engine.
many-user aggregate. A third engine (S3-like) remains the many-user aggregate. A third engine (S3-like) remains the
ADR-003 door; nothing here touches it. ADR-003 door; nothing here touches it.
7. **redb-class guardrail maintained:** this is the second engine the 7. **redb-class guardrail maintained:** this is the second engine the
admission door ships (ADR-007: kv; ADR-009: fs) — both on the same admission door ships (ADR-007: kv; ADR-009: large) — both on the same
measured-evidence shape; no third engine is opened by this one. measured-evidence shape; no third engine is opened by this one.
## Consequences ## Consequences
@@ -2,7 +2,12 @@
## Status ## Status
Accepted Accepted (fleet-state activation, the GC-state host under mixed
engines, the sweep-abort return shape, the pin token's wire shape,
the sqlite pin/sweep-lock clause reductions, and the window staging
for non-SQL engines are ruled by [ADR-012](012-pre-decomposition-consistency-rulings.md);
§2's sqlite rows and the "fleet state is active" predicate read
through that ADR — where noted inline)
## Context ## Context
@@ -139,7 +144,10 @@ The store core reaches the fleet machinery through a **crate-internal
SQL-backed engines (sqlite, postgres, pg-lo), with `local` and SQL-backed engines (sqlite, postgres, pg-lo), with `local` and
`mem` excluded by construction (it cannot be expressed on them and `mem` excluded by construction (it cannot be expressed on them and
they are never fleet engines). ADR-008's mechanism fixes *what*; the they are never fleet engines). ADR-008's mechanism fixes *what*; the
seam is *where* the mechanism's SQL lives. Clauses: seam is *where* the mechanism's SQL lives. Clauses (per-engine
applicability is ruled by [ADR-012](012-pre-decomposition-consistency-rulings.md)
§5's table, which reduces the original sqlite rows; fleet state hosts
on the kv-tier engine when fleet mode is declared — ADR-012 §4):
- `ensure_gc_schema()` — create `gc_pins (key, owner, expiry)` and - `ensure_gc_schema()` — create `gc_pins (key, owner, expiry)` and
`gc_window (key, visible_after)` at store construction (store-core `gc_window (key, visible_after)` at store construction (store-core
@@ -152,15 +160,19 @@ seam is *where* the mechanism's SQL lives. Clauses:
ADR-005's refcount semantics expressed in rows. The embedder's ADR-005's refcount semantics expressed in rows. The embedder's
liveness table is *not* created by this seam — ADR-008: schema liveness table is *not* created by this seam — ADR-008: schema
pinned by the embedder; the sweep reads it and never owns it. pinned by the embedder; the sweep reads it and never owns it.
**Per-engine applicability (the test-gate shape):** sqlite — **Per-engine applicability (the test-gate shape; final per
`ensure_gc_schema` (window table only, since the window staging ADR-012 §5):** sqlite — schema creates **`gc_window` only** +
is shared machinery per ADR-008 §2) + `window_stage`/ `window_stage`/`window_take` (durable single-node staging);
`window_take` + the sweep-lock clause's single-node body; the the originally-specced sqlite pin clauses and sweep-lock body
pin clauses (`pin_commit`/`pin_renew`/`pin_check`) have sqlite are **dropped** by ADR-012 §5 (the pin bodies inserted into a
bodies but are exercised by unit tests only — they are dead table nothing created — dead machinery; sqlite has no advisory
machinery on sqlite because no sqlite fleet is on offer (§3). lock, and ADR-008's unchanged single-node protocol keeps the
postgres and pg-lo — all clause families, contract-tested, sweep single-flighter in-process). postgres — all clause
fleet-active. families, contract-tested, fleet-active (the fleet host). pg-lo —
the window family only (durable window staging where the kv
engine is not SQL-backed — the one-window-host rule, ADR-012 §5;
it hosts no fleet pin/lock state,
ADR-012 §4), contract-tested.
- `pin_commit(joint_tx, entries)` — the store core **holds the - `pin_commit(joint_tx, entries)` — the store core **holds the
transaction**; the engine contributes its put mechanics (row transaction**; the engine contributes its put mechanics (row
shape, CAS clause, LO writes) and the pin-row inserts, and both shape, CAS clause, LO writes) and the pin-row inserts, and both
@@ -168,37 +180,59 @@ seam is *where* the mechanism's SQL lives. Clauses:
ruled: **the store holds the tx; the engine contributes mechanics ruled: **the store holds the tx; the engine contributes mechanics
to it.** The public `put` remains the plain, pin-free path — the to it.** The public `put` remains the plain, pin-free path — the
store's normal put path routes through `pin_commit` wherever the store's normal put path routes through `pin_commit` wherever the
engine is seam-capable and fleet state is active. engine is seam-capable and fleet mode is active (**activation
ruled by ADR-012 §3: fleet mode is an explicit `fleet: true`
constructor declaration, never inferred from the engine choice**;
for large=local fleet puts the joint tx is unachievable across
media — ADR-012 §4 pins pin-row-first, publish-second ordering).
- pg-lo correction: ADR-009 §2's "put = one transaction" is - pg-lo correction: ADR-009 §2's "put = one transaction" is
unchanged in content but its ownership statement is hereby unchanged in content but its ownership statement is hereby
corrected — the tx is held by the store core; the pg-lo engine corrected — the tx is held by the store core; the pg-lo engine
contributes its staged-put mechanics and companion-row insert contributes its staged-put mechanics and companion-row insert
into it. The engine sees pins only as rows; never as semantics. into it. The engine sees pins only as rows; never as semantics.
- `pin_renew(key, owner, ttl)` — the engine executes the expiry - `pin_renew(key, owner)` — the engine executes the expiry update
update. The store's pin guard owns the TTL/3 renewal schedule (renamed and TTL dropped by ADR-012 §2: the TTL lives with the
(ADR-008); the engine owns only the SQL. store's renewal schedule, not in the clause signature; the original
`pin_renew(key, owner, ttl)` read is superseded). The store's pin
guard owns the TTL/3 renewal schedule
(ADR-008); the engine owns only the SQL. *(postgres host only per
ADR-012 §5 — sqlite bodies dropped.)*
- `pin_check(key) → PinState` / `arbitrate(key) → bool` — the engine - `pin_check(key) → PinState` / `arbitrate(key) → bool` — the engine
executes ADR-008's delete-time arbitration (un-expired pin rows + executes ADR-008's delete-time arbitration (un-expired pin rows +
embedded liveness tables, `SELECT … FOR UPDATE` finger-lock, embedded liveness tables, `SELECT … FOR UPDATE` finger-lock,
keyed, at delete time); the store core receives the answer and keyed, at delete time); the store core receives the answer and
applies the decision. If fleet clauses ever grow, they grow here — applies the decision. `PinState` is a data enum with two variants:
`Protected` (an un-expired pin row exists) and `Unprotected` — the
engine never decides deletion, it only classifies; *when the seam
supports SQL-level liveness tables the store consults those through
`arbitrate` instead, which folds the full answer into the bool*. If
fleet clauses ever grow, they grow here —
the seam is the named home, with contract tests per clause. the seam is the named home, with contract tests per clause.
- `window_stage(candidates)` / `window_take(now)` — the staged - `window_stage(candidates)` / `window_take(now)` — the staged
delete-window candidates. Per ADR-008 §2 the window table exists delete-window candidates. Per ADR-008 §2 the durable window table
on **every** engine (sqlite included): staged candidates are exists on **every SQL-backed engine** (final wording per ADR-012
§5: sqlite included; `local` and `mem` are excluded from the seam
here and stage candidates in-process, executing the ADR-005 window
protocol inline in the same sweep; in dual-SQL instances the
non-host engine's window clauses are dormant — the one-window-host
rule, ADR-012 §5): staged candidates are
engine rows; what differs by topology is where *liveness* lives engine rows; what differs by topology is where *liveness* lives
(in-process maps for single-node engines, tables for fleets), not (in-process maps for single-node engines, tables for fleets), not
whether staging exists. whether staging exists among SQL-backed engines.
- `sweep_lock_acquire()/release()` — the sweep single-flighter: the - `sweep_lock_acquire()/release()` — the sweep single-flighter: the
pg advisory lock (ADR-008) for the SQL engines it applies to; the pg advisory lock (ADR-008), applying to the fleet host (postgres)
only — ADR-012 §5 drops the originally-specced sqlite body (no
advisory-lock equivalent; in-process lock suffices and is
ADR-008-verbatim). The
store treats lock contention exactly as ADR-008 says — the second store treats lock contention exactly as ADR-008 says — the second
sweeper's sweep aborts with the typed `GcAborted` (fleet cause), sweeper's sweep aborts with **`GcAbortCause::SweeperLock` as report
nothing deleted. (SQLite today has no fleet story — see §3 — so its data (`Ok(report)` — ADR-012 §1 retired the typed `GcAborted` sweep
clause body exists but is exercised by single-node tests only.) error)**, nothing deleted.
Each clause is contract-tested per its applicability (the Each clause is contract-tested per its applicability (the
per-engine split above): the conformance suite of §1 grows the fleet per-engine split above): the conformance suite of §1 grows the fleet
clauses for postgres and pg-lo; `local` and clauses for postgres (the host); pg-lo runs the window-family
contract tests; `local` and
`mem` are excluded by construction. `mem` are excluded by construction.
### 3. The kv tier's fleet-validity rule (the mirror of ADR-008 §3) ### 3. The kv tier's fleet-validity rule (the mirror of ADR-008 §3)
@@ -222,10 +256,19 @@ engines the put's pin is the expiry-carrying row (ADR-008); the token
identifies the putter as the row's owner. The token itself is an identifies the putter as the row's owner. The token itself is an
**opaque id minted by the ops layer** over the pin row it just **opaque id minted by the ops layer** over the pin row it just
committed — the ops layer (not the store facade) owns the committed — the ops layer (not the store facade) owns the
token↔(key, owner) mapping, in-memory for the handler's lifetime plus token↔(key, owner) mapping, in-process state with **process lifetime**
a lookup by the renewal op; no schema is specced for it beyond that (ADR-012 §2 corrected the original "handler's lifetime" — the handler's
guard drops at response time, but the mapping must outlive it, or
renewal could never arrive); no schema is specced
for it beyond that
(it is a handle, not durable state — the durable state is the pin row (it is a handle, not durable state — the durable state is the pin row
the seam wrote). Concretely: the seam wrote). The token's **validity domain is the minting node**
(ADR-012 §2: renewal probes reach the serving node that minted the
token; fleet peers refuse foreign tokens; a dead minting node is the
ADR-005 crash case with the re-put backstop).
**Wire shapes (ADR-012 §2):** the `blobs/put` response is
`{token, digest}`; renewal rides the token form of `blobs/have`
(one digest, token alongside). Concretely:
- **When the server-side `Pin` guard drops:** at response time. The - **When the server-side `Pin` guard drops:** at response time. The
put lands pinned (the joint tx guarantees the entry and its pin row put lands pinned (the joint tx guarantees the entry and its pin row
@@ -259,7 +302,10 @@ the seam wrote). Concretely:
had it: nothing persists, nothing expires; the process crash is had it: nothing persists, nothing expires; the process crash is
ADR-005's in-flight-put loss case with the same backstop. (The ADR-005's in-flight-put loss case with the same backstop. (The
cross-wire token under `local` is the ops layer's in-memory handle cross-wire token under `local` is the ops layer's in-memory handle
to that guard — same mapping discipline, no expiry.) to that guard — same mapping discipline, no expiry.) ADR-012 §2
generalizes this to every non-fleet mode: the token is the
in-process guard's handle on sqlite, single-instance postgres, and
pg-lo serving nodes too — nothing expires outside fleet mode.
ops-surface.md's hand-over contract gains the renewal line and the ops-surface.md's hand-over contract gains the renewal line and the
token-mapping note; nothing else changes. token-mapping note; nothing else changes.
@@ -284,17 +330,15 @@ token-mapping note; nothing else changes.
**Negative** **Negative**
- A crate-internal companion trait is real machinery: six clause - A crate-internal companion trait is real machinery: the full clause
families on postgres/pg-lo (fleet-active, all contract-tested); family on postgres (the fleet host — pins, window, sweep lock,
sqlite carries only the window + lock clauses as fleet-relevant contract-tested, fleet-active); sqlite and pg-lo carry the window
machinery (its pin clauses have unit-test bodies but no fleet mode family only (ADR-012 §5's reduction — the original draft's sqlite
on offer — §3). The seam bounds where fleet-GC growth lands; it pin/sweep-lock bodies were dead machinery and are dropped). The
does not shrink it. seam bounds where fleet-GC growth lands; it does not shrink it.
- Remote putters gain a real protocol duty (renew past 20 s; - Remote putters gain a real protocol duty (renew past 20 s;
pause-past-TTL ⇒ delete-then-recover) — documented, but a duty. pause-past-TTL ⇒ delete-then-recover; route renewal to the minting
- The sqlite sweep-lock clause body exists but is exercised only by node — ADR-012 §2) — documented, but a duty.
single-node tests until a sqlite-fleet posture is ever
decided (it is not on offer — §3).
**Neutral** **Neutral**
@@ -314,7 +358,10 @@ token-mapping note; nothing else changes.
contract the cursor serves), ADR-008 (the fleet mechanism whose contract the cursor serves), ADR-008 (the fleet mechanism whose
state home this ADR is), ADR-009 (the pg-lo engine; tx-ownership state home this ADR is), ADR-009 (the pg-lo engine; tx-ownership
correction), [ADR-011](011-vocabulary-tiers-engines-and-fleet-state.md) correction), [ADR-011](011-vocabulary-tiers-engines-and-fleet-state.md)
(vocabulary + constructor table) (vocabulary + constructor table), [ADR-012](012-pre-decomposition-consistency-rulings.md)
(fleet activation predicate, GC-state host, sweep-abort shape,
token wire shape, the §5 clause reduction this ADR's seam table
now references)
- `docs/research/poc-trait-dispatch-findings.md` finding 1; - `docs/research/poc-trait-dispatch-findings.md` finding 1;
`docs/research/poc-largeblob-findings.md` findings A1/A5/A6; `docs/research/poc-largeblob-findings.md` findings A1/A5/A6;
`docs/research/poc-pglo-findings.md` (C1-C7 — the handle shape, the `docs/research/poc-pglo-findings.md` (C1-C7 — the handle shape, the
@@ -2,7 +2,12 @@
## Status ## Status
Accepted Accepted (fleet-mode activation, the GC-state host, the sweep-abort
shape, and kv-only's put-time rejection point are ruled by
[ADR-012](012-pre-decomposition-consistency-rulings.md) — this ADR's
constructor table is closed under ADR-012 §4's host rule; §4's
"constructor-time error" wording reads as ADR-012 §6.2's put-time
rejection)
## Context ## Context
@@ -147,12 +152,17 @@ explicit shared-media assertion, ADR-008 §3, is what disambiguates).
Constructor modes (dispatch shapes) — pinned here (implied by Constructor modes (dispatch shapes) — pinned here (implied by
ADR-008 §3's re-routing bullet and the mem tests, but never named): ADR-008 §3's re-routing bullet and the mem tests, but never named):
- **dual-tier** (the default): kv + large, size-threshold dispatch - **dual-tier** (the default): kv + large, size-threshold dispatch
between them (ADR-003). between them (ADR-003). Engine pairs beyond the default
(`kv = mem` + `large = local`) are valid dual-tier constructions —
ADR-012 §6.3.
- **kv-only** (ADR-008's "kv-tier-only dispatch", now named): a - **kv-only** (ADR-008's "kv-tier-only dispatch", now named): a
constructor mode with no large tier; over-threshold puts are a constructor mode with no large tier; over-threshold puts reject at
constructor-time error (the client-node re-routing posture handles **put time** — known-length immediately, unknown-length at
them via the ops surface instead — its composition, not a store threshold overflow (the original "constructor-time error" wording
mode). is corrected by ADR-012 §6.2: lengths are not known at
construction). The client-node re-routing posture handles
over-threshold content via the ops surface instead — its
composition, not a store mode.
- **mem-only** (test/embedder-ephemeral mode): the mem engine alone, - **mem-only** (test/embedder-ephemeral mode): the mem engine alone,
no dispatch threshold in effect (everything is local and small); no dispatch threshold in effect (everything is local and small);
a testing posture, explicitly not a production story. a testing posture, explicitly not a production story.
@@ -161,7 +171,9 @@ ADR-008 §3's re-routing bullet and the mem tests, but never named):
ADR-004's requirement (both tiers are load-bearing by measured ADR-004's requirement (both tiers are load-bearing by measured
economics); "postgres-only" as in *one SQL instance serving both economics); "postgres-only" as in *one SQL instance serving both
tiers* already exists (kv=postgres + large=pg-lo — the ADR-009 tiers* already exists (kv=postgres + large=pg-lo — the ADR-009
consolidation), which is the dual-tier mode over one engine. consolidation), which is the dual-tier mode over one engine —
under fleet mode that one instance is a requirement, not a
coincidence (ADR-012 §4).
### 5. Deferral-policy wording (README alignment) ### 5. Deferral-policy wording (README alignment)
@@ -216,7 +228,10 @@ this ADR's wording here rather than a silent edit).
and the fleet topology; the vocabulary it assumed is now pinned), and the fleet topology; the vocabulary it assumed is now pinned),
ADR-009 (pg-lo admission; the consolidation option whose ADR-009 (pg-lo admission; the consolidation option whose
constructor mode this ADR names dual-tier), [ADR-010](010-backend-io-seams-and-fleet-state-home.md) constructor mode this ADR names dual-tier), [ADR-010](010-backend-io-seams-and-fleet-state-home.md)
(the I/O seams and GC-state home this vocabulary must fit) (the I/O seams and GC-state home this vocabulary must fit),
[ADR-012](012-pre-decomposition-consistency-rulings.md) (fleet-mode
activation + GC-state host + kv-only put-time rejection — the
constructor table's final closure)
- [requirements.md](../requirements.md) (the canonical vocabulary - [requirements.md](../requirements.md) (the canonical vocabulary
copy, REQ-1..4); [backends-and-dispatch.md](../backends-and-dispatch.md) copy, REQ-1..4); [backends-and-dispatch.md](../backends-and-dispatch.md)
(the tier/engine table's spec home) (the tier/engine table's spec home)
@@ -0,0 +1,352 @@
# ADR-012: Pre-decomposition consistency rulings — sweep-abort shape, pin-token wire shape, fleet-mode activation, and the GC-state host
## Status
Accepted
## Context
The ADR-011 vocabulary round was followed by a full-tree consistency
review — the gate before task decomposition. The vocabulary itself
held everywhere; what the review surfaced is a set of **composition
defects**: mechanisms each specced correctly in isolation, whose
interaction was never ruled, plus two caller-facing shapes stated two
ways. All are pre-implementation (zero code exists), all deciding
facts are in hand, and none waits on a consumer:
1. **The sweep-abort return shape is stated two ways.**
store-api.md's §Lifecycle pins an aborted sweep as
`Ok(SweepReport { aborted: Some(GcAbortCause), .. })` ("the report
is data, not an error carrier"), while §Error model of the *same
file* defines `GcAborted` as the sweep-abort error carrying the
cause classes — and ADR-005, ADR-008, ADR-010, and
gc-and-namespaces.md all follow the error reading. An implementer
writing `sweep()` cannot pick a signature.
2. **The pin token has no wire shape.** The renewal contract (ADR-010
§4) has the putter renew by presenting the token "alongside the
digest" via `blobs/have` — but the specced `have` payload is
`{namespace, digests[]}` with no token field, `blobs/put`'s
token-carrying response is never specced, and an unknown-length
putter does not know its digest until the server computes it. The
token's *validity domain* is also unstated (renewal may arrive
after the minting handler's guard dropped, possibly to a different
fleet node).
3. **"Fleet state is active" has no activation predicate.** ADR-010
routes puts through `pin_commit` "wherever the engine is
seam-capable and fleet state is active", and ADR-008 keeps
single-node postures on the in-process protocol — but nothing
defines what activates fleet state. Under ADR-011's vocabulary
fleetness (two instances sharing one pool) is not observable by
any one constructor.
4. **The GC-state home under mixed fleet engines is unnamed.** In the
kv=postgres + large=local-on-shared-media fleet posture (valid per
the composite rule), pin-before-publish for a large entry is a file
rename, not an insert — the joint entry+pin tx is not achievable on
the large tier's medium — and nothing says which engine hosts
`gc_pins`/`gc_window`/the sweep lock, or whether two SQL engines
mean two pin tables and an arbitration union.
5. **The sqlite seam rows are self-contradictory.** ADR-010 §2 rules
sqlite's `ensure_gc_schema` creates the window table *only*, yet
assigns sqlite `pin_commit`/`pin_renew`/`pin_check` "bodies
exercised by unit tests only" — bodies that insert into a table
nothing creates — and gives sqlite a sweep-lock clause body whose
semantics are undefined (sqlite has no advisory locks) and which
ADR-008's "single-node postures unchanged" says never runs.
The review also surfaced smaller caller-facing gaps (fall-through for
non-get ops, the kv-only error point, a missing abort cause, the
fleet liveness-table registration's API home) that need a ruled home
before signatures are written; §6 records them.
## Decision
### 1. Sweeps are never errors — every abort is data in the report
`sweep()` returns `Result<SweepReport, Error>` where the error family
exists for *operational* failure (I/O, engine failure), never for a
deliberate no-op. All abort causes are report data:
- **`GcAborted` is retired from the error enum.** The only abort
vocabulary is `GcAbortCause`, a data enum carried in
`SweepReport.aborted: Option<GcAbortCause>`, with exactly three
variants: `ProtectFailed` (ADR-005's protection-source failure),
`SweeperLock` (ADR-008's fleet sweeper-lock contention),
`NoLivenessSources` (the safe default — a sweep invoked with zero
registered sources; previously this cause existed in prose,
store-api.md's sweep bullet, but had no variant to carry it).
- Direct `delete()` on a protected key keeps a typed refusal error,
named **`GcRefuse`** — a distinct name so no reader composes the
retired `GcAborted` semantics onto it. (This renames the refusal
path; nothing else about it changes — ADR-005's arbitration still
decides at delete time.)
- `GcAbortCause` is matched non-exhaustively by callers; new causes
are new ADRs.
Rationale: fleet lock contention is a *routine, expected* outcome of
scheduled sweeps — the embedder's next run takes the lock — and a
typed error would push the common case onto the exceptional path.
The report is the data embedders schedule on; "sweep counts are
exact" (the test gate) is asserted against report fields.
This ruling picks store-api.md §Lifecycle's shape over the error
shape used by ADR-005/008/010 and the gc spec; those documents'
wording is aligned to it (mechanical edits; no semantic change — all
four said "nothing deleted" identically, they only disagreed on the
carrier).
### 2. The pin token's wire shape and validity domain
**`blobs/put` response: `{token, digest}`.** Both fields always
present: `digest` is the stored entry's canonical digest (hex) — the
unknown-length putter cannot know it beforehand and needs it for
every later op; `token` is the opaque pin-token handle (ADR-010 §4's
minting unchanged). Known-length putters receive the same shape.
**`blobs/have` gains the token as the renewal form.** Payload
`{namespace, digests[], token?}`:
- without `token` — unchanged: pure existence probe over `digests[]`
(ADR-008's `has` discipline preserved; response `present[]`
corresponds to `digests[]` by index);
- with `token` — the **renewal probe**: exactly one digest in
`digests[]`, and the handler (a) verifies the token maps to a pin
row for that digest, (b) calls the crate-internal `pin_renew(key,
owner)` via the ops module's store-core access (ADR-010 §4's
mechanism, unchanged), (c) responds with `present[]` as usual — the
putter's registration-landed signal. One token binds one pin row,
so renewal is one digest per probe; a putter renewing several pins
issues one probe per token. If the row has expired (putter paused
past TTL), the handler finds no live row: `present: false` and no
renewal occurs — the putter's backstop is re-put (ADR-005's
delete-then-recover), making ADR-010 §4's expiry race end-to-end
explicit on the wire.
**Token validity domain: the minting node.** The token↔(key, owner)
mapping is ops-layer, in-memory state in the process that minted it
(process lifetime, not handler lifetime — the handler's guard drops
at response time per ADR-010 §4, but the mapping outlives the
handler); renewal probes must reach the minting serving node, and
fleet peer nodes cannot serve a token they did not mint (`present:
false`-equivalent refusal, not an error). A putter whose minting node
died before renewal is exactly ADR-005's in-flight-put loss case with
the same backstop: re-put. This is a documented duty of the
re-routing/replication embedder (route renewal to the serving node),
not new machinery.
**Non-fleet serving nodes (any engine):** the token is the ops
layer's in-memory handle to the in-process `Pin` guard — ADR-005
posture, nothing expires. This sentence generalizes ADR-010 §4's
`local`-only wording to every non-fleet mode (kv=sqlite, kv=postgres
single-instance, large=pg-lo non-fleet).
### 3. Fleet mode is a constructor declaration
**A store instance is constructed with fleet mode explicitly on or
off; it is never inferred.** Under `fleet: true`: pins are expiry
rows, puts route through `pin_commit`, sweep acquires the SQL sweep
lock, and the composite fleet-validity predicate (with its per-node
declarations — ADR-008 §3's media assertion, kv-only re-routing
declaration) is enforced at construction. Under `fleet: false`: the
ADR-005 in-process protocol verbatim, regardless of engine.
ADR-011's *deployment* vocabulary is unchanged (fleet = two or more
instances sharing one pool); the constructor flag is the per-instance
admission of that posture. Two `fleet: true` instances over one
postgres pool are a fleet; an undeclared instance sharing the pool is
**misconfigured** — the observable failure is ADR-008 §2's exact
data-loss scenario, and it is the same class of deployment-verified
invariant as the shared-media declaration: the constructor cannot
prove cross-node/cross-instance truth, the deployment asserts it, and
the detection symptom (an unrenewed, un-pinned entry swept while in
flight) is documented.
`fleet: true` is a constructor error for every non-fleet-valid
configuration: any kv engine other than postgres, `mem` anywhere,
`local` large tiers without the shared-media declaration, `pg-lo` on
a different pg instance than the kv engine (§4).
### 4. GC state has exactly one home: the fleet's kv engine
**All fleet GC state — `gc_pins`, `gc_window`, the sweep lock — lives
on the kv-tier engine.** Fleet mode requires kv=postgres, so
postgres hosts the tables and the advisory lock for the whole store
instance, large tier included. The per-engine alternative (each SQL
engine hosting pin rows for its own tier, arbitration unioning two
tables) is rejected: two pin tables mean two arbitration domains and
two locks — the partitioning failure ADR-008 §3 exists to prevent,
re-expressed inside one store instance. One host is the whole point
of the seam.
Consequences per fleet large-tier engine:
- **`large = pg-lo` (fleet): the same pg instance as the kv engine is
required** — this is ADR-009's consolidation posture ("pool content
lives in the same pg instance the kv tier rides") now made a
checkable clause of the composite fleet-validity predicate, because
it is what makes the joint entry+pin tx expressible: the store core
holds one transaction over the shared instance; the pg-lo engine
contributes LO writes + companion-row insert; the pin-row insert is
the store's (§2 of ADR-010, ownership unchanged). Cross-instance
kv=postgres + large=pg-lo is valid *only* with `fleet: false`
(in-process protocol).
- **`large = local` on declared shared media (fleet): pin-first,
publish-second.** The joint tx is not achievable across media, so
the ordering is pinned: the pin row commits on the kv engine
*before* the stage file's commit-rename publishes the entry.
- The invariant survives: an entry becomes visible only after its
pin row is durable — sweep arbitration (which queries pin rows)
can never observe a visible-unpinned entry.
- The crash window moves to the harmless side: crash between
pin-commit and rename leaves an orphan pin row, reaped by the
existing expiry machinery (ADR-008's TTL + sweeper maintenance
step). No new recovery mechanism.
- The reverse ordering (rename-then-pin) is rejected: a crash
there reopens ADR-005's original race (visible, unpinned,
sweepable).
**Non-fleet postures are unchanged and single-hosted:** the GC
arbitration domain is the store instance (ADR-005/011); pins and
liveness arbitration are in-process; the durable window table (§5)
may live on a SQL engine but carries no fleet semantics.
### 5. The engine-state seam's final shape (sqlite rows repaired)
Per-engine clause applicability — the correction of ADR-010 §2's
contradictory rows:
| Engine | `ensure_gc_schema` | Window clauses | Pin clauses | Sweep-lock clause |
|---|---|---|---|---|
| postgres | `gc_pins` + `gc_window` | yes (fleet-active when declared) | yes (fleet-active when declared) | yes (pg advisory lock; fleet-active when declared) |
| sqlite | `gc_window` only | yes (durable single-node staging) | **no** (bodies dropped) | **no** (body dropped — single-flighter is the in-process lock) |
| pg-lo | `gc_window` only | conditional — see the one-host rule below | **no** (fleet pins live on the kv host, §4) | no |
| `local`, `mem` | — excluded from the seam by construction (ADR-010 unchanged); their delete-window staging is **in-process** (ADR-005 protocol inline) | | | |
**The one-window-host rule (per store instance):** the durable staged
window lives on **one** SQL-backed engine — the kv engine when it is
SQL-backed (sqlite or postgres), otherwise the large engine (pg-lo,
e.g. the kv=mem + large=pg-lo shape). A dual-SQL instance (kv=sqlite +
large=pg-lo, non-fleet) stages on the kv engine only; pg-lo's
window clauses are dormant there. Never two window tables in one
instance — the candidate set is one set, and a split staging domain
would be a miniature of the two-arbitration-domain failure §4
rejects.
- The sqlite pin-clause bodies are **dropped**: they were dead
machinery inserting into a table nothing created. A sqlite fleet is
not on offer (ADR-010 §3); if one ever is, it is a new ADR that
re-adds the clauses with their schema.
- The sqlite sweep-lock body is **dropped**: sqlite has no advisory
lock, and ADR-008's "single-node postures keep the arbitration lock
in-process" is the actual mechanism. The seam's sweep-lock clause
is the pg advisory lock and applies to the fleet host only.
- pg-lo remains a seam implementer for the window family (it hosts
the durable window on non-fleet topologies where the kv engine is
not SQL-backed, e.g. kv=mem + large=pg-lo test shapes) and
contributes staged-put mechanics into the store-held tx in the
consolidation posture; it hosts no fleet state.
- The durable staged delete window stays a **SQL-backed-engine
mechanism** (sqlite included) per ADR-008 §2 — what this ruling
adds is the non-SQL half: "`local` and `mem` stage candidates
in-process and execute the ADR-005 window protocol inline within
the same sweep." "On every engine" (ADR-008 §2, ADR-010 §2) is
corrected to "on every SQL-backed engine".
### 6. Facade clarifications (ruled, not deferred)
1. **Fall-through applies to every key-addressed op, not just
get/read_range.** `stat`, `has`, and direct `delete` all consult
the kv tier first, then the large tier (a `size` probe for
`stat`/`has`; arbitration-then-delete where the entry lives for
`delete`). POC #1 finding 6's fall-through is the general rule.
2. **kv-only mode's over-threshold rejection is a put-time error.**
Known-length puts (`len: Some`) exceeding the threshold reject
immediately; unknown-length puts reject at threshold overflow
(mid-buffer). The prior "constructor-time error" wording was
impossible — put lengths are not known at construction.
3. **`dual-tier` with `kv = mem` + `large = local` is a valid
construction** (the dispatch-coverage test shape; `mem` is merely
the kv engine choice in dual-tier mode). `mem-only` remains the
no-large-tier ephemeral mode. Both are non-fleet by definition.
4. **Direct `delete()` refuses any protected key** — pinned,
observed-live via a registered source, or named by the protect
callback — not just pinned keys. This is the ADR-005 invariant
("a visible pool entry is never deleted while liveness protects
it") applied to the direct path; ADR-005's "refuses pinned keys"
wording is aligned upward. An embedder's correction flow
deregisters/re-registers around the delete.
5. **Error-model member vs return shape:** `get`/`stat` return
`Option` (a miss is a value, not an error — `Missing` is not
theirs); `read_range` and `delete` return `Err(Missing)` on absent
keys (informative for the embedder flows that use them).
`Io(String)` covers backend media failure including cursor and
staged-put I/O errors; `Verification` covers hash-check failure on
put and get paths alike; `KeyInvalid` malformed keys. `Backend`
trait methods return this error family (crate-internal variants,
thiserror).
6. **Fleet liveness-table registration has an API home:** the fleet
form of `register_liveness_source` registers a *named table
reference* in the shared engine (the embedder's table per ADR-008
§2; the sweep's SQL arbitration consults it). The facade method is
the same verb with a fleet-shaped argument; the store core reads
the table and never owns it (ADR-008's schema-ownership rule).
7. **The staged window's executor is the next sweep** (ADR-008 §2's
"the sweeper, or the next sweep's owner"): a sweep takes overdue
`gc_window` rows (past `visible_after`), re-arbitrates each, and
commits the deletes before/alongside its own marking; non-SQL
engines execute inline in the same sweep (§5). Exact-count sweep
invariants are asserted relative to this schedule.
## Consequences
**Positive**
- Every signature the implementation needs is now single-valued:
`sweep()`'s return shape, put/have payload shapes, the fleet
activation predicate, the GC-state host, and the per-engine seam
table. Task decomposition has no contradictory text to resolve.
- The fleet mechanism gained the missing pieces (activation flag,
state host, large=local ordering) without new machinery — every
piece reuses an existing mechanism (constructor declarations,
expiry reaper, the store-held tx).
- The sqlite seam shrank (dead bodies dropped); the per-engine
applicability table is the contract-test matrix.
**Negative**
- `GcAborted`'s retirement is a renaming cost across four documents
(mechanical; no semantic change — and no code exists yet to break).
- The pin token gains a documented duty (route renewal to the minting
node) and the have/put payload shapes change from the earlier prose
(a token field and a digest return field) — wire-shape decisions
locked before any consumer, per the ADR-002 door discipline.
- Fleet mode is one more constructor parameter with per-node
declaration duties; the deployment-verified invariant class grows
by one row (undeclared-instance sharing).
**Neutral**
- No tier, engine, feature flag, or constructor *mode* changes; the
composite fleet-validity predicate gains one clause (pg-lo
same-instance) it had already assumed in prose (ADR-009).
- SweepReport, GcAbortCause, GcRefuse, and the fall-through/put-time
rules are the only *new* facade vocabulary; everything else in
store-api.md stands.
## References
- ADR-005 (the GC invariant and single-node protocol this ADR extends
and aligns), ADR-008 (fleet GC mechanism — §2's window wording and
§3's declarations feed §4/§5 here), ADR-009 (pg-lo admission —
§4's same-instance clause formalizes its consolidation posture),
ADR-010 (the I/O seams and GC-state home — §1/§2/§4 here repair its
sqlite rows and rule its activation predicate; §2 here completes
its §4 token contract with a wire shape), ADR-011 (the vocabulary
this ADR's constructor flag and host rules compose with)
- [store-api.md](../store-api.md) (sweep shape, error model,
invariants); [ops-surface.md](../ops-surface.md) (the op payloads
§2 pins); [backends-and-dispatch.md](../backends-and-dispatch.md)
(the composite fleet-validity rule §4 amends; constructor modes);
[gc-and-namespaces.md](../gc-and-namespaces.md) (the delete-window
execution model §6.7 names)
- `docs/research/poc-trait-dispatch-findings.md` finding 6
(fall-through, §6.1's basis); `poc-pglo-findings.md` (the
consolidation posture §4 formalizes)
+16 -6
View File
@@ -1,6 +1,6 @@
--- ---
status: draft status: draft
last_updated: 2026-10-03 (ADR-010 state-home rule + ADR-011 instance/fleet vocabulary) last_updated: 2026-10-03 (ADR-012 — aborts are report data (GcAbortCause), never sweep errors; window executor = the next sweep; non-SQL engines stage in-process)
--- ---
# Pooling, namespaces, and GC # Pooling, namespaces, and GC
@@ -69,13 +69,20 @@ semantics, delete-then-recover as byte-identical re-put):
2. put-path **pins** (RAII; batch scope available for multi-put 2. put-path **pins** (RAII; batch scope available for multi-put
writes — manifest writes are exactly this), writes — manifest writes are exactly this),
3. the **protect callback** consulted before each sweep: it may add 3. the **protect callback** consulted before each sweep: it may add
externally-known hashes or **abort the run** (`GcAborted`; a externally-known hashes or **abort the run** (report abort —
flaky protection source skips the sweep rather than risk `GcAbortCause::ProtectFailed`, nothing deleted; ADR-012 §1:
deletion — iroh's `ProtectOutcome::Abort` conclusion, adopted). sweeps are never errors; a flaky protection source skips the
sweep rather than risk deletion — iroh's
`ProtectOutcome::Abort` conclusion, adopted).
- **Sweep:** enumerate the whole pool (backends' complete `list()`, - **Sweep:** enumerate the whole pool (backends' complete `list()`,
ADR-003), compute the live set, batch-delete the dead ADR-003), compute the live set, batch-delete the dead
(batch-sized, ~100/batch, iroh's proven shape). Delete-then-recover (batch-sized, ~100/batch, iroh's proven shape). Delete-then-recover
is a re-put — byte-identical under the same key (validated). is a re-put — byte-identical under the same key (validated).
Overdue delete-window candidates are re-arbitrated and executed by
the sweep that finds them — the executor is the next sweep
(ADR-012 §6.7); SQL-backed engines stage candidates durably
(engine rows), `local`/`mem` stage in-process and execute the
window inline in the same sweep (ADR-012 §5).
- **Traversal ownership: lean.** Liveness computation beyond "these - **Traversal ownership: lean.** Liveness computation beyond "these
roots exist" is the consumer's job, handed over via the seam. The roots exist" is the consumer's job, handed over via the seam. The
store never learns manifest formats. If a real consumer's live-set store never learns manifest formats. If a real consumer's live-set
@@ -100,7 +107,8 @@ window batches the arbitration).
Test-asserted property: **a visible pool entry is never deleted while Test-asserted property: **a visible pool entry is never deleted while
liveness (pin or registered source) protects it, and an in-flight put liveness (pin or registered source) protects it, and an in-flight put
is never deleted by a sweep started before it committed.** Direct is never deleted by a sweep started before it committed.** Direct
`delete(key)` refuses pinned keys (typed error; same arbitration). `delete(key)` refuses protected keys (the typed `GcRefuse` error; the
full protection set per ADR-005's invariant — ADR-012 §6.4).
`has()`/`get()` during a window observe either the old or the new `has()`/`get()` during a window observe either the old or the new
state; there is no torn observation (the pool's entries are immutable; state; there is no torn observation (the pool's entries are immutable;
@@ -131,6 +139,7 @@ existence flips atomically per key).
| [004](decisions/004-two-backends-no-third.md) | Structure-blindness | manifests/refs never sink into the store | | [004](decisions/004-two-backends-no-third.md) | Structure-blindness | manifests/refs never sink into the store |
| [008](decisions/008-trait-size-probe-fleet-gc-and-large-engines.md) | Fleet GC extension | DB-backed pins, advisory-locked single sweeper, SQL delete arbitration — for shared-pool (fleet) topologies only; the in-process protocol is unchanged elsewhere | | [008](decisions/008-trait-size-probe-fleet-gc-and-large-engines.md) | Fleet GC extension | DB-backed pins, advisory-locked single sweeper, SQL delete arbitration — for shared-pool (fleet) topologies only; the in-process protocol is unchanged elsewhere |
| [010](decisions/010-backend-io-seams-and-fleet-state-home.md) | GC-state home | the store core owns all GC state; SQL engines host it via the contract-tested engine-state seam; pin-tx ownership ruled | | [010](decisions/010-backend-io-seams-and-fleet-state-home.md) | GC-state home | the store core owns all GC state; SQL engines host it via the contract-tested engine-state seam; pin-tx ownership ruled |
| [012](decisions/012-pre-decomposition-consistency-rulings.md) | Abort-as-data + window executor | sweep aborts are `GcAbortCause` report data, never errors; the next sweep executes overdue window rows; non-SQL engines stage in-process; GC state hosts on the fleet's kv engine |
## Open Questions ## Open Questions
@@ -152,5 +161,6 @@ a future requirement does not create one retroactively.
(the re-borrowed conclusions) (the re-borrowed conclusions)
- rudolfs — the physical-namespace anti-pattern (inverted) - rudolfs — the physical-namespace anti-pattern (inverted)
- gix-odb — alternates/pool prior art for the p2p case - gix-odb — alternates/pool prior art for the p2p case
- ADR-005; [store-api.md](store-api.md) (Pin semantics, sweep API); - ADR-005; ADR-012 (the abort-as-data ruling, the window executor,
the GC-state host); [store-api.md](store-api.md) (Pin semantics, sweep API);
[ops-surface.md](ops-surface.md) (namespace-as-resource ACL) [ops-surface.md](ops-surface.md) (namespace-as-resource ACL)
+15 -3
View File
@@ -1,6 +1,6 @@
--- ---
status: draft status: draft
last_updated: 2026-10-03 last_updated: 2026-10-03 (ADR-012 round — no new OQs; overview's pg-only-kv feature-graph deferral recorded in the parked index)
--- ---
# Open Questions # Open Questions
@@ -47,8 +47,9 @@ is pending; deferral policy applies to decisions, not to work.
**Index of active OQs:** OQ-07, OQ-08 (externally-owned, carried **Index of active OQs:** OQ-07, OQ-08 (externally-owned, carried
for visibility). OQ-09 and OQ-10 are resolved (recorded below). for visibility). OQ-09 and OQ-10 are resolved (recorded below).
Promoted Phase 0 questions OQ-BL-01..06 are recorded here with their OQ-11 (deferred(scope), parked index) records a feature-graph
resolutions for traceability. deferral. Promoted Phase 0 questions OQ-BL-01..06 are recorded here
with their resolutions for traceability.
--- ---
@@ -236,6 +237,17 @@ resolutions for traceability.
|---|---|---| |---|---|---|
| OQ-07 | externally-owned | alkgit's architecture process (answerable on paper anytime; gates nothing here) | | OQ-07 | externally-owned | alkgit's architecture process (answerable on paper anytime; gates nothing here) |
| OQ-08 | externally-owned | alkfs Phase 0 intake | | OQ-08 | externally-owned | alkfs Phase 0 intake |
| OQ-11 | deferred(scope) | a named deployment requirement for a pg-only kv node (postgres kv engine without rusqlite in the build) — overview.md's feature-graph note |
OQ-11 records [overview.md](overview.md)'s dependency-posture deferral
(a pg-only kv node — the `kv` feature's rusqlite pull decoupled from
default-on feature graphing) for auditability: the deciding fact is a
*deployment requirement that arrives from outside this crate*
(Schrödinger's-code rule compliant — it is not a fact this crate
creates), the accepted v1 shape (base `kv` feature pulls rusqlite,
default-on) stands until then, and the deferral is a feature-graph
work item, not an open architecture decision. No spec or ADR gates on
it.
OQ-09 was `deferred(scope)` on the first embedded ops deployment — a OQ-09 was `deferred(scope)` on the first embedded ops deployment — a
deciding fact that could only exist once the ops module ships, i.e. a deciding fact that could only exist once the ops module ships, i.e. a
+54 -27
View File
@@ -1,6 +1,6 @@
--- ---
status: draft status: draft
last_updated: 2026-10-03 (pin-token renewal contract pinned per ADR-010 §4) last_updated: 2026-10-03 (ADR-012 §2 — put response {token, digest}; have token-renewal form; fetch verification wording; non-fleet token scope)
--- ---
# Ops surface (alkcall-backed network operations) # Ops surface (alkcall-backed network operations)
@@ -55,11 +55,19 @@ JSON control ops (`Visibility` per ADR-001 §Decision):
- **`blobs/stat`** `{namespace, digest}` → `{len}` — probe before - **`blobs/stat`** `{namespace, digest}` → `{len}` — probe before
offering; ACL-gated like fetch (read action on the namespace) offering; ACL-gated like fetch (read action on the namespace)
- **`blobs/have`** `{namespace, digests[]}` → `{present[]}` — the - **`blobs/have`** `{namespace, digests[], token?}` → `{present[]}` —
have half of have/need set diffing (hashes only — content never the have half of have/need set diffing (hashes only — content never
traverses this op); read-gated: an existence probe over arbitrary traverses this op); read-gated: an existence probe over arbitrary
digests is a discovery surface, so it is gated exactly like fetch, digests is a discovery surface, so it is gated exactly like fetch,
not public not public. `present[]` corresponds to `digests[]` by index. The
optional `token` is the **renewal form** (ADR-012 §2): when present,
exactly one digest accompanies it — the handler verifies the token
maps to that digest's pin row, renews it (crate-internal
`pin_renew` via the ops module's store-core access, ADR-010 §4),
and answers with `present[]` as usual; an expired/foreign row
answers `present: false` (the putter's backstop is re-put). Without
`token`, `have` remains a pure existence probe (ADR-008's
discipline).
- **`blobs/delete`** `{namespace, digests[]}` — operator machinery; - **`blobs/delete`** `{namespace, digests[]}` — operator machinery;
`Visibility::Internal`, evaluated under the internal authority `Visibility::Internal`, evaluated under the internal authority
context per alkcall ADR-017 (internal calls switch authority context, context per alkcall ADR-017 (internal calls switch authority context,
@@ -72,8 +80,12 @@ Binary channel ops (registered via the `channel_open` marker):
- **`blobs/fetch`** (`Sub` — the server→client streaming op shape) - **`blobs/fetch`** (`Sub` — the server→client streaming op shape)
— `{namespace, digest, ranges?}` in; — `{namespace, digest, ranges?}` in;
verified bytes out: the consumer hash-checks each received chunk/whole against verified bytes out: the consumer hash-checks the received whole
the carried digest (ADR-006). Broadcast fanout above the store: against the carried digest (ADR-006's whole-blob verification —
there are no per-chunk or per-range checks derivable under the
canonical digest; ranged fetch responses carry the same ADR-006
slice-digest convention `read_range` returns). Broadcast fanout
above the store:
one reader, store arm + subscriber arms (POC #3 finding A3); late one reader, store arm + subscriber arms (POC #3 finding A3); late
joiners degrade to a normal post-commit `get` — identical bytes under joiners degrade to a normal post-commit `get` — identical bytes under
CAS. Slow-subscriber policy (drop-and-late-join) is ops-layer CAS. Slow-subscriber policy (drop-and-late-join) is ops-layer
@@ -81,7 +93,10 @@ Binary channel ops (registered via the `channel_open` marker):
- **`blobs/put`** (`Sink` — the client→server streaming op shape) - **`blobs/put`** (`Sink` — the client→server streaming op shape)
— `{namespace, digest?, len?}` offer + — `{namespace, digest?, len?}` offer +
byte stream in; server verifies against the canonical derivation byte stream in; server verifies against the canonical derivation
before commit. A known-length offer is the encouraged path (one- before commit. **Response: `{token, digest}`** (ADR-012 §2) — the
pin token (§Pin hand-over below) and the stored entry's canonical
digest (hex), which an unknown-length putter needs for every later
op. A known-length offer is the encouraged path (one-
pass); unknown-length rides the store's pre-threshold buffering path pass); unknown-length rides the store's pre-threshold buffering path
(ADR-003). **Need half of have/need**: a fetch miss *is* the need (ADR-003). **Need half of have/need**: a fetch miss *is* the need
announcement — the consumer computes its need set by diffing announcement — the consumer computes its need set by diffing
@@ -96,7 +111,8 @@ the server. The hand-over contract:
1. the put lands pinned (ADR-005) — the entry cannot be swept while 1. the put lands pinned (ADR-005) — the entry cannot be swept while
the handler holds the pin; the handler holds the pin;
2. the `blobs/put` response returns a **pin token** (opaque handle — 2. the `blobs/put` response returns a **pin token** in the specced
response shape `{token, digest}` (ADR-012 §2 — opaque handle
minted by the ops layer over the pin row the put committed; the minted by the ops layer over the pin row the put committed; the
token↔(key, owner) mapping is ops-layer state, not durable state — token↔(key, owner) mapping is ops-layer state, not durable state —
the durable state is the pin row itself, ADR-010 §4); the putter the durable state is the pin row itself, ADR-010 §4); the putter
@@ -115,31 +131,40 @@ the server. The hand-over contract:
conversion on the server side (token → registered liveness source) conversion on the server side (token → registered liveness source)
is the mechanism the embedder plugs its registry into. is the mechanism the embedder plugs its registry into.
**Token renewal (ADR-010 §4 — the fleet-TTL race, resolved): the **Token renewal (ADR-010 §4, wired per ADR-012 §2 — the fleet-TTL
token *is* the pin — there is no second pin state.** The server-side race, resolved): the token *is* the pin — there is no second pin
handler's in-process `Pin` guard drops when the `blobs/put` response state.** The server-side handler's in-process `Pin` guard drops when
completes (if it never dropped, the TTL machinery would be dead on the `blobs/put` response completes (if it never dropped, the TTL
the ops path — the pin row's expiry, not the guard's presence, is the machinery would be dead on the ops path — the pin row's expiry, not
fleet liveness signal). Under fleet the guard's presence, is the fleet liveness signal). Under fleet
engines the put's pin is an expiry-carrying row (ADR-008: TTL 60 s engines the put's pin is an expiry-carrying row (ADR-008: TTL 60 s
default, renewed every TTL/3). The remote putter holding a token is default, renewed every TTL/3). The remote putter holding a token is
the row's owner and renews by the ops it already speaks: a the row's owner and renews via the renewal form of `blobs/have`
`blobs/have` probe **presenting the token alongside the digest** (the (ADR-012 §2: one digest, token alongside — the handler verifies the
handler verifies the token maps to the digest's pin row and calls the token maps to the digest's pin row and calls the crate-internal
crate-internal `pin_renew` via the ops module's store-core access — `pin_renew` via the ops module's store-core access — the ops module
the ops module is in-crate and crosses the seam by construction, the is in-crate and crosses the seam by construction, the same way the
same way the put handler holds the `Pin` guard; `has` itself remains put handler holds the `Pin` guard; `has` itself remains
a pure existence probe per ADR-008 — renewal is a token-authenticated a pure existence probe per ADR-008 — renewal is a token-authenticated
act on the pin row, not a `has` semantic), or a re-put of the same act on the pin row, not a `has` semantic), or a re-put of the same
digest (CAS dedup renews the row). **A putter may pause past the digest (CAS dedup renews the row). **A putter may pause past the
TTL/3 mark (20 s at the default TTL) only if it renews before expiry; TTL/3 mark (20 s at the default TTL) only if it renews before expiry;
a putter pausing past TTL lets the row expire** — the sweeper reaps a putter pausing past TTL lets the row expire** — the renewal probe
it (ADR-008) and delete-then-recover (ADR-005) is the backstop: the answers `present: false`, the sweeper reaps the row (ADR-008), and
delete-then-recover (ADR-005) is the backstop: the
putter re-offers the digest; CAS makes the re-put idempotent. The putter re-offers the digest; CAS makes the re-put idempotent. The
race resolves to a re-put, never to silent loss. For non-fleet race resolves to a re-put, never to silent loss. **Token validity
(`local`-engine) serving nodes the token is the in-process guard — domain: the minting serving node** (ADR-012 §2) — the token↔(key,
nothing expires; the process-crash case is ADR-005's in-flight-put owner) mapping is ops-layer in-memory state in the node that minted
loss with the same backstop. it (process-lifetime, outliving the handler); renewal probes must
reach that node, fleet peers refuse tokens they did not mint (`present:
false`, not an error), and a minting node's death before renewal is
ADR-005's in-flight-put loss case with the same re-put backstop — a
documented embedder duty (route renewals to the serving node).
For non-fleet serving nodes (any engine — sqlite, single-instance
postgres, `local`, pg-lo; ADR-012 §2) the token is the in-process
guard's handle — nothing expires; the process-crash case is ADR-005's
in-flight-put loss with the same backstop.
Placement of the op *registration* (which embedders wire where they Placement of the op *registration* (which embedders wire where they
want them exposed) matches the alkgit ops pattern (its want them exposed) matches the alkgit ops pattern (its
@@ -186,6 +211,7 @@ fleet-specific op exists.
| [006](decisions/006-verification-posture-and-transfer-encoding.md) | Verification | verified-fetch via carried digests; no chunk trees | | [006](decisions/006-verification-posture-and-transfer-encoding.md) | Verification | verified-fetch via carried digests; no chunk trees |
| [008](decisions/008-trait-size-probe-fleet-gc-and-large-engines.md) | Fleet re-routing topology | non-shared-media fleets route large-blob access through a storage node via these ops; no new ops | | [008](decisions/008-trait-size-probe-fleet-gc-and-large-engines.md) | Fleet re-routing topology | non-shared-media fleets route large-blob access through a storage node via these ops; no new ops |
| [010](decisions/010-backend-io-seams-and-fleet-state-home.md) | Pin-token renewal | the token is the pin; renewal rides have/re-put; TTL expiry falls to delete-then-recover | | [010](decisions/010-backend-io-seams-and-fleet-state-home.md) | Pin-token renewal | the token is the pin; renewal rides have/re-put; TTL expiry falls to delete-then-recover |
| [012](decisions/012-pre-decomposition-consistency-rulings.md) | Wire-shape rulings | put response `{token, digest}`; have's token-renewal form; token validity domain = minting node; non-fleet token scope generalized |
| [011](decisions/011-vocabulary-tiers-engines-and-fleet-state.md) | Vocabulary + modes | instance/node/fleet split; kv-only mode named (the re-routing posture's client shape) | | [011](decisions/011-vocabulary-tiers-engines-and-fleet-state.md) | Vocabulary + modes | instance/node/fleet split; kv-only mode named (the re-routing posture's client shape) |
## Open Questions ## Open Questions
@@ -204,4 +230,5 @@ deployment).
machinery this module rides) machinery this module rides)
- `docs/research/poc-largeblob-findings.md` finding A3 (fanout seam) - `docs/research/poc-largeblob-findings.md` finding A3 (fanout seam)
- ADR-001; [store-api.md](store-api.md); [gc-and-namespaces.md](gc-and-namespaces.md) - ADR-001; [store-api.md](store-api.md); [gc-and-namespaces.md](gc-and-namespaces.md)
(namespace-as-resource) (namespace-as-resource); ADR-012 §2 (the wire shapes this doc's
op payloads carry)
+8 -3
View File
@@ -1,6 +1,6 @@
--- ---
status: draft status: draft
last_updated: 2026-10-03 (ADR-010/011 — I/O seams pinned; vocabulary fs→large, mem joins kv) last_updated: 2026-10-03 (ADR-012 — OQ-11 parking reference; ADR-012 row added)
--- ---
# Overview # Overview
@@ -65,7 +65,8 @@ or three times.
│ hashing & keys (ADR-002) · put/get/stat/range/pin seams │ │ hashing & keys (ADR-002) · put/get/stat/range/pin seams │
│ mark-and-sweep GC + liveness registration (ADR-005) │ │ mark-and-sweep GC + liveness registration (ADR-005) │
│ size-threshold dispatch (ADR-003); GC state owned │ │ size-threshold dispatch (ADR-003); GC state owned │
│ here, hosted by SQL engines via the seam (ADR-010) │ │ here; fleet state hosts on the fleet's kv engine │
│ via the seam (ADR-010/012) │
└───────┬───────────────────┬───────────────────────────────┘ └───────┬───────────────────┬───────────────────────────────┘
▼ ▼ ▼ ▼
┌──────────────────────┐ ┌─────────────────────────────────┐ ┌──────────────────────┐ ┌─────────────────────────────────┐
@@ -105,7 +106,8 @@ optimizes for — feature-graph work for it is deferred until a named
requirement exists (the base `kv` feature pulling rusqlite is the requirement exists (the base `kv` feature pulling rusqlite is the
accepted v1 shape; convention 8's lean-base principle applies to accepted v1 shape; convention 8's lean-base principle applies to
*default* builds, which stay lean since `kv` is what the feature model *default* builds, which stay lean since `kv` is what the feature model
controls, not what default-on means downstream). Wasm: the tier controls, not what default-on means downstream; tracked as OQ-11 in
open-questions.md's parked index). Wasm: the tier
engines are not a wasm story; a wasm client is a consumer of the `ops` engines are not a wasm story; a wasm client is a consumer of the `ops`
surface (which rides alkcall, itself wasm-clean), never an in-process surface (which rides alkcall, itself wasm-clean), never an in-process
embedder (ADR-001 §Consequences). embedder (ADR-001 §Consequences).
@@ -139,6 +141,7 @@ embedder (ADR-001 §Consequences).
| [008](decisions/008-trait-size-probe-fleet-gc-and-large-engines.md) | `size` probe + fleet GC + large-tier engines | trait length probe; fleet pins/sweeps engine-backed; the large tier (now `large`) engine-selectable | | [008](decisions/008-trait-size-probe-fleet-gc-and-large-engines.md) | `size` probe + fleet GC + large-tier engines | trait length probe; fleet pins/sweeps engine-backed; the large tier (now `large`) engine-selectable |
| [009](decisions/009-pg-lo-large-tier-engine.md) | pg-lo large engine | postgres Large Objects as the large tier's second engine, admitted on POC #7; a fleet node can run postgres-only | | [009](decisions/009-pg-lo-large-tier-engine.md) | pg-lo large engine | postgres Large Objects as the large tier's second engine, admitted on POC #7; a fleet node can run postgres-only |
| [010](decisions/010-backend-io-seams-and-fleet-state-home.md) | I/O seams + GC-state home | read cursor / staged put; store-core-owned GC state via the engine-state seam; kv fleet-validity rule; pin-token renewal | | [010](decisions/010-backend-io-seams-and-fleet-state-home.md) | I/O seams + GC-state home | read cursor / staged put; store-core-owned GC state via the engine-state seam; kv fleet-validity rule; pin-token renewal |
| [012](decisions/012-pre-decomposition-consistency-rulings.md) | Sweep-abort shape + composition rulings | sweeps never error (`GcAborted` retired, `GcRefuse` for direct-delete); token wire shape; fleet mode = constructor declaration; GC state hosts on the kv engine; sqlite seam reduction |
| [011](decisions/011-vocabulary-tiers-engines-and-fleet-state.md) | Vocabulary + rename | tier/engine/instance/node/fleet pinned; mem joins the kv tier; large tier renamed `large`; constructor modes | | [011](decisions/011-vocabulary-tiers-engines-and-fleet-state.md) | Vocabulary + rename | tier/engine/instance/node/fleet pinned; mem joins the kv tier; large tier renamed `large`; constructor modes |
## Open Questions ## Open Questions
@@ -154,6 +157,8 @@ crate level:
open-read requirement) open-read requirement)
- **OQ-10**: second kv engine — resolved by ADR-007 (sqlite + postgres - **OQ-10**: second kv engine — resolved by ADR-007 (sqlite + postgres
shipped behind one trait; per-node constructor choice) shipped behind one trait; per-node constructor choice)
- **OQ-11**: pg-only kv node feature graph — deferred(scope), parked
(see [open-questions.md](open-questions.md))
- **POC #7** (`docs/research/poc-pglo-findings.md`): pg Large Objects - **POC #7** (`docs/research/poc-pglo-findings.md`): pg Large Objects
as the large tier's `pg-lo` engine — **passed** 2026-10-03; admitted as as the large tier's `pg-lo` engine — **passed** 2026-10-03; admitted as
a shipped engine by ADR-009 (REQ-2's consolidation option) a shipped engine by ADR-009 (REQ-2's consolidation option)
+72 -30
View File
@@ -1,6 +1,6 @@
--- ---
status: draft status: draft
last_updated: 2026-10-03 (ADR-010 I/O seams + ADR-011 rename — large tier → large) last_updated: 2026-10-03 (ADR-012 — sweep-abort = Ok(report) with GcAbortCause; GcAborted retired; fall-through for all key-addressed ops; put-time kv-only rejection; delete/missing/error-model ruling)
--- ---
# Store API # Store API
@@ -61,7 +61,11 @@ mechanism lives in the engine-state seam, ADR-010 §2).
read cursor the store core drives (ADR-010 §1 — engines construct read cursor the store core drives (ADR-010 §1 — engines construct
their own cursor bodies; the facade's shape is uniform). Dispatch their own cursor bodies; the facade's shape is uniform). Dispatch
fall-through: a small-tier miss queries the large tier (POC #1 fall-through: a small-tier miss queries the large tier (POC #1
finding 6). finding 6 — the general rule, ADR-012 §6.1: every key-addressed op
— `get`, `stat`, `has`, `delete`, `read_range` — consults the kv
tier first, then the large tier). A miss on both tiers is a `None`
(get/stat) or `Err(Missing)` (read_range/delete) — see the error
model.
- **`stat(key) → Option<EntryMeta>`** — cheap length/type probe - **`stat(key) → Option<EntryMeta>`** — cheap length/type probe
(phase-0 OQ-BL-04 addendum; gix's header-only read is git's cheapest (phase-0 OQ-BL-04 addendum; gix's header-only read is git's cheapest
primitive and the packfile-serving surface rides this). `EntryMeta` primitive and the packfile-serving surface rides this). `EntryMeta`
@@ -86,36 +90,56 @@ mechanism lives in the engine-state seam, ADR-010 §2).
### Lifecycle ### Lifecycle
- **`has(key)`, `delete(key)`** — deletion participates in ADR-005's - **`has(key) → bool`** — the pure existence probe (ADR-008's
`has` discipline; a `size`-probe under the dispatch general rule —
absent on both tiers is `false`; virgin-store reads are no-ops).
- **`delete(key) → Result<(), Error>`** — falls through tiers per the
dispatch general rule above. Participates in ADR-005's
delete windows; direct deletes are permitted but refuse protected delete windows; direct deletes are permitted but refuse protected
keys — pinned or re-observed live via registered sources (the same keys — pinned, re-observed live via registered sources, or named by
per-key arbitration sweeps apply, typed error) — and are unusual by the protect callback (ADR-012 §6.4: the full protection set, not
posture; most deletion flows through sweeps. just pins — the ADR-005 invariant applied to the direct path) —
with the typed `GcRefuse` error. Direct deletes are unusual by
posture; most deletion flows through sweeps. An embedder's
correction flow deregisters/re-registers around the delete.
- **`list() → stream of keys`** — whole-pool enumeration; complete by - **`list() → stream of keys`** — whole-pool enumeration; complete by
contract (see backends doc for why list correctness is load-bearing). contract (see backends doc for why list correctness is load-bearing).
- **`register_liveness_source(...)`** — the live-shared GC seam - **`register_liveness_source(...)`** — the live-shared GC seam
(ADR-005, §Decision; the POC's "install implies copy" failure (ADR-005, §Decision; the POC's "install implies copy" failure
(finding 7) is why the verb/name is pinned here). (finding 7) is why the verb/name is pinned here). Fleet form (ADR-012
- **`sweep() → SweepReport`** — explicit mark-and-sweep; embedders own §6.6): the same verb registers a **named liveness table reference**
the cadence (ADR-005, no ambient timers). A sweep with no registered in the shared engine (the embedder's own table per ADR-008 §2 — the
liveness sources aborts without deleting — the safe default. store core reads it, never owns it); in-process callback sources
`SweepReport`'s shape (specced here, resolving its earlier remain the single-node form.
name-only status): `candidates_staged`, `deleted`, - **`sweep() → Result<SweepReport, Error>`** — explicit mark-and-sweep; embedders own
`cancelled_by_arbitration` (u64 counts), `aborted: Option<GcAbortCause>` the cadence (ADR-005, no ambient timers). **Sweeps are never
(the typed cause — protection-source failure (ADR-005) or fleet errors: an aborted sweep returns `Ok(SweepReport)` with
sweeper-lock contention (ADR-008) — present iff the sweep deleted `aborted: Some(GcAbortCause)` — there is no sweep-abort error
nothing for an abort reason; named `GcAbortCause` to keep it a data variant** (ADR-012 §1; the previously specced `GcAborted` sweep
enum, distinct from the `GcAborted` error which carries the error is retired — see the error model). A sweep with no registered
direct-delete refusal path), and `started_at`/`finished_at`. The liveness sources aborts without deleting — the safe default —
report is data, not an error carrier: an aborted sweep returns carrying `GcAbortCause::NoLivenessSources`.
`Ok(report)` with `aborted: Some(..)` — the typed error `SweepReport`'s shape: `candidates_staged`, `deleted`,
`GcAborted` is reserved for the direct-delete refusal path. `cancelled_by_arbitration` (u64 counts), `aborted:
Option<GcAbortCause>`, and `started_at`/`finished_at`. `GcAbortCause`
is a data enum with exactly three variants — `ProtectFailed`
(protection-source failure, ADR-005), `SweeperLock` (fleet
sweeper-lock contention, ADR-008), `NoLivenessSources` (the safe
default above); callers match non-exhaustively; new causes are new
ADRs (ADR-012 §1). The direct-delete refusal path is a *different*
type: the `GcRefuse` error (above), not a sweep outcome.
Staged-window candidates overdue at sweep time are re-arbitrated
and executed by that sweep (the executor is the next sweep —
ADR-012 §6.7; non-SQL engines execute their window inline within
the same sweep, ADR-012 §5).
Under a fleet (multiple store instances over one pool — Under a fleet (multiple store instances over one pool —
requirements.md REQ-2), sweep/put/delete semantics are requirements.md REQ-2), sweep/put/delete semantics are
mechanism-extended by ADR-008: pins are DB-backed rows committed mechanism-extended by ADR-008: pins are DB-backed rows committed
atomically with entries (the store core holds the joint tx, atomically with entries (the store core holds the joint tx,
ADR-010 §2), one sweeper holds the advisory lock, and delete ADR-010 §2; all GC state hosts on the fleet's kv engine —
ADR-012 §4), one sweeper holds the advisory lock (contention is a
report abort, not an error — ADR-012 §1), and delete
arbitration is SQL-level — the facade's invariant text is arbitration is SQL-level — the facade's invariant text is
unchanged. unchanged.
@@ -124,14 +148,30 @@ mechanism lives in the engine-state seam, ADR-010 §2).
`thiserror`; no panics in library code; no `unwrap`/`expect` outside `thiserror`; no panics in library code; no `unwrap`/`expect` outside
tests (AGENTS convention 2). Distinguished failure families: tests (AGENTS convention 2). Distinguished failure families:
- `Missing` — key absent (get/stat miss) - `Missing` — key absent on ops that signal absence via `Err`:
`read_range` and `delete` (ADR-012 §6.5). `get` and `stat` signal
absence with `None` — `Missing` is not theirs.
- `Verification` — put/get hash-check failure (content ≠ key) - `Verification` — put/get hash-check failure (content ≠ key)
- `Io(String)` — backend media failure (stringly because - `Io(String)` — backend media failure, covering cursor and staged-put
`std::io::Error` is not stable across versions) I/O errors surface through the same trait error family (stringly
- `GcAborted` — nothing deleted; sweep not run. Two cause classes because `std::io::Error` is not stable across versions)
(ADR-005: a protection source failed; ADR-008: another node holds - `GcRefuse` — direct delete refused: the key is protected (pinned,
the fleet sweeper lock) — the typed error distinguishes them. registered-source-live, or protect-callback-named) — nothing
- `KeyInvalid` — malformed key bytes at the boundary (hashing doc) deleted; the delete-window arbitration decides at delete time
(ADR-005; named `GcRefuse` per ADR-012 §1 — there is no
sweep-abort error).
- `KeyInvalid` — malformed key bytes at the boundary (truncated,
unknown algorithm — rejects, never guesses; ADR-002/hashing doc).
One enum, both altitudes: the **facade's** error enum exposes
`Missing`/`Verification`/`Io`/`GcRefuse`/`KeyInvalid`; the
`Backend` trait's methods return the same family as crate-internal
variants — `KeyInvalid` never fires at the trait boundary (keys
arrive opaque there) and `GcRefuse` never fires below the facade
(arbitration answers flow up as data; the facade decides and owns
the refusal), so the trait's effective members are
`Missing`/`Verification`/`Io`/`KeyInvalid`; the facade re-exposes
them with `GcRefuse` added (ADR-012 §6.5's family statement reads
through this split).
Virgin-store semantics: read paths on a fresh store see absent/empty, Virgin-store semantics: read paths on a fresh store see absent/empty,
never "table does not exist" errors (POC #1 finding 2 — the redb never "table does not exist" errors (POC #1 finding 2 — the redb
@@ -190,6 +230,7 @@ lesson generalizes to any kv engine).
| [006](decisions/006-verification-posture-and-transfer-encoding.md) | Verification | whole-blob checks; slice digests out-of-band | | [006](decisions/006-verification-posture-and-transfer-encoding.md) | Verification | whole-blob checks; slice digests out-of-band |
| [008](decisions/008-trait-size-probe-fleet-gc-and-large-engines.md) | Trait `size` + fleet GC | stat/accounting ride the trait's `size` probe; fleet pins/sweeps are engine-backed | | [008](decisions/008-trait-size-probe-fleet-gc-and-large-engines.md) | Trait `size` + fleet GC | stat/accounting ride the trait's `size` probe; fleet pins/sweeps are engine-backed |
| [010](decisions/010-backend-io-seams-and-fleet-state-home.md) | I/O seams + GC-state home | read cursor / staged put behind the facade; the store core holds the joint entry+pin tx | | [010](decisions/010-backend-io-seams-and-fleet-state-home.md) | I/O seams + GC-state home | read cursor / staged put behind the facade; the store core holds the joint entry+pin tx |
| [012](decisions/012-pre-decomposition-consistency-rulings.md) | Sweep-abort shape + wire/activation rulings | swept = Ok(report) always, `GcRefuse` for direct-delete refusal; fleet mode is a constructor declaration; GC state hosts on the kv engine |
## Open Questions ## Open Questions
@@ -201,7 +242,8 @@ None owned by this document beyond the cross-references above.
benchmark table benchmark table
- `docs/research/poc-trait-dispatch-findings.md` findings 1–3, 7 - `docs/research/poc-trait-dispatch-findings.md` findings 1–3, 7
- ADR-003 (put-path buffering), ADR-005 (pin/Batch/Pin lifecycle), - ADR-003 (put-path buffering), ADR-005 (pin/Batch/Pin lifecycle),
ADR-006 (range-read semantics), ADR-002 (keys) ADR-006 (range-read semantics), ADR-002 (keys), ADR-012 (the
sweep-abort shape, error-model ruling, fall-through general rule)
- [ops-surface.md](ops-surface.md) — the fanout/fetch consumer of the - [ops-surface.md](ops-surface.md) — the fanout/fetch consumer of the
read path read path
- [gc-and-namespaces.md](gc-and-namespaces.md) — the lifecycle seam - [gc-and-namespaces.md](gc-and-namespaces.md) — the lifecycle seam