Files
alkblobs/docs/research/phase-0.md
T
glm-5.3-flash 493516424a docs(research): POC #1 passed — trait contract + digest + dispatch
POC code: /workspace/alkblobs-trait-poc (17 tests passing, clippy -D
warnings + fmt clean; digests validated byte-exact against the real
git hash-object CLI, both object formats)

Findings (poc-trait-dispatch-findings.md, 8):
- lean Backend trait (opaque byte keys, complete list(), put-path
  pinning) is sufficient; Key typed wrapper lives at the boundary
- redb traps: list() key-vs-value destructure (silent until GC
  catches it — malformed list is as deadly as incomplete), virgin-db
  TableDoesNotExist on first read, no in-memory db (temp file used)
- pinning: Pinned RAII guard + refcount map works; batch-scope pins
  and the sweep-vs-put race are named Phase 1 requirements (iroh
  DeleteSet = prior art)
- git-blob preamble abstraction validated; external-git oids address
  pool entries with zero indirection (the OQ-BL-03 resolution,
  implementation-confirmed)
- tagged key format ([algo byte][digest]) makes git-sha256 + git-sha1
  coexist; sort-stable for sweep diffs
- dual dispatch works as simply as hoped; routing deterministic
- namespace tables: protect callback MUST share live state (the
  snapshot-install bug proves the API lesson: register_liveness_source
  shape, not install_into)
- GC semantics verified: exact deletion counts, abort-callback,
  delete-then-recover byte-identical re-put

phase-0.md: POC #1 marked passed, #2 absorbed (validated), #4 covered
in miniature (concurrency half deferred to Phase 1 impl work);
OQ-BL-02 base settled; OQ-BL-03 implementation-validated; OQ-BL-05
mechanism settled with findings folded in; plan updated (POC #3 is
the last open POC)
2026-10-01 06:51:32 +00:00

625 lines
35 KiB
Markdown

---
status: draft
last_updated: 2026-10-02 (POC #1 executed and passed — see
poc-trait-dispatch-findings.md; POC #2 absorbed earlier; register
updated; earlier rounds: hash simplification, GC/pooling, dedup/p2p,
setup draft)
---
# alkblobs — Phase 0 (Exploration)
This document captures Phase 0 (Exploration) for the `alkblobs` crate:
vision, guiding principles, prior art, and open questions (OQ-BL-01..NN).
Phase 0's objective per `docs/sdd_process.md`: *capture vision and guiding
principles; research options; validate approaches; converge on a
recommended approach.* It is the input to Phase 1 (Architecture), where
the Architect will produce `docs/architecture/` specs, ADRs, and the
open-questions tracker.
Drafted 2026-09-30, emerging from the initial setup discussion. Nothing
is converged yet — this is the working sketch, not a specification.
## Vision and guiding principles
**One sentence:** content-addressed blob storage in the alk* family —
a put/get/verify store keyed under one canonical content hash (the
git-family derivation; legacy SHA-1-tolerant), with pluggable backends
(small blobs in a kv/sqlite-ish store, large blobs on a filesystem
fallback), split from any transport/protocol layer, with auth-gated
network operations riding the alkcall seam when they exist.
**Why this crate exists (three converging consumers):**
1. **alkgit** (planning phase) needs an object backend that is *not*
its proposed default file-based backend ("kind of gross") — blobs
stored under git's own object hashes, but with iroh-blobs-like
handling for large blobs (where git typically delegates to git-lfs;
we could instead follow an iroh-blobs-shaped large-blob path). The
original framing of this as a "hash conflict" dissolved in the
2026-10-01 rounds: git's oid derivation was adopted as the store's
canonical hash (§OQ-BL-03), so git objects land in the pool as
first-class entries with zero indirection — the conflict was an
artifact of inheriting iroh-blobs' BLAKE3-only choice, and
once that inheritance was rejected the conflict went with it.
2. **The alknet rewrite** (the original alk* project, being decomposed
and improved) needs the "appfile" external-store shape: small blobs
in a kv/sqlite store (faster than the filesystem for small items —
true beyond sqlite: it applies to the kv store iroh-blobs uses
too), large blobs on a filesystem fallback, with filename↔hash
mapping. alknet's own research hit the core awkwardness of
dispatching across more than one backend at a time — which is
exactly a store-layer problem this crate should own.
3. **Agent workspaces** — many agents working simultaneously in
workspaces that are mostly identical to each other's, each making
small edits in specific areas. Per-agent stores multiply the near-
identical content; a shared content-addressed store dedups it by
construction, and a path→hash manifest per workspace makes the
per-agent delta just its changed hashes (the alknet-filesystem
probe shows the manifest mapping is straightforward).
**The p2p shape and what it pins (added 2026-10-01, from the dedup
discussion).** Two alkgit use cases exist: self-hosted git (where
cross-repo dedup matters little — you don't fork your own repos) and
**p2p git for OSS projects**, the demanding one. The p2p sketch:
ownership/ACL live in a smart contract on a low-fee network;
replicators are push/clone endpoints that watch the contract and cache
its state; repos sync via iroh-style gossip (hashes, not content) and
pull/sync on demand. Neither alkgit approach described so far takes
advantage of dedup — which is exactly a store-layer property. The
implications for this crate:
- Both hard consumers (p2p replicators, agent swarms) reduce to the
same shape: **a pooled CAS per node**, where repos / workspaces are
*sets of hash references* (trees/manifests), not separate object
stores. Cross-repo dedup is then by construction; git's own
`alternates`/object-pool mechanism is the same idea done awkwardly.
With the canonical-hash resolution (§OQ-BL-03) the dedup strengthens
further: workspaces and git repos collide into one address domain —
the same file content has the same address whether it entered via a
git oid or a workspace manifest.
- Because everything is hash-addressed, the p2p wire story collapses
to "announce hashes, diff hash sets, fetch missing blobs verified" —
which is precisely the ops surface OQ-BL-01 left provisional. The
consumer exists now: if p2p git is a confirmed alkgit direction,
the ops surface upgrades from "leaning store-only" to a confirmed
have/need + fetch op family, auth-gated via the alkcall seam.
**The scope line (current posture, revisit as evidence arrives):**
this crate is the *store*, not the *transport*. iroh-blobs welds store
+ provider protocol + tickets + postcard into one crate; the defining
posture here is the opposite split — put/get/verify against a hash is
its own layer, and any provider/protocol/ops surface lives above it or
in feature-gated modules (the alk* inversion-point pattern). The ops
surface (a channels-native "have/need + fetch blob with this hash" op
family, auth-gated via `AccessControl`) is a Phase 0/1 question — the
p2p-git consumer (§The p2p shape) upgrades it from speculative to
likely, but the crate-boundary decision itself is still open
(OQ-BL-01). What stays *above* the crate regardless: repos/workspaces
as reference sets (trees/manifests), path→hash mapping, git semantics,
the contract/gossip/replicator policy layer.
Guiding principles, inherited from the alk* family:
1. **Substrate-agnostic by construction.** The store must not know
whether bytes arrive over the network, from a local writer, or get
reassembled anywhere particular. Backends behind an injected seam
(alktty `TtyBackend` / alktunnels pump-halves precedent).
2. **One canonical hash; the git-family derivation.** The store keys
entries under a single canonical algorithm — the git-blob derivation
(`blob <len>\0` domain-separated input, SHA-256 for new content;
OQ-BL-03). This is the resolution of the earlier "multi-hash"
framing: BLAKE3's presence was an inherited iroh-blobs choice, not
a requirement, and once rejected the hash-conflict motivation
dissolved with it. Legacy SHA-1 (existing git repos) stays
tolerated via the git-blob prefix; the abstraction is still a small
enum/capability set rather than a hardcoded single algorithm —
SHA-1-vs-SHA-256 is already two, and the key encoding is a one-way
door.
3. **Borrow conclusions, not wire surface.** iroh-blobs' tickets,
postcard serialization, and provider protocol are design-welded
choices we do not inherit. Its *store-shape* lessons (kv + flat
backends, verification flow, chunking) are fair game per prior-art
reading.
4. **Producer/consumer vocabulary** for any network-facing surface;
authorization via alkcall's `AccessControl`/identity seam, never an
in-band invented scheme (convention 6).
5. **Verify where it matters.** iroh-blobs' core virtue is verified
transfer (bao outboard encoding) — but its verification story is
BLAKE3-native, and we rejected the BLAKE3 inheritance. Git objects
self-verify, so the git case needs nothing from us. The remaining
question is the large-blob path: per-blob digest checks (rudolfs'
verify-as-decorator shape), a conditional bao-like chunk tree
(BLAKE3 confined to that encoding layer if ever adopted), or
backend-native — open (OQ-BL-04).
6. **Pooling is the point.** The dedup wins both demanding consumers
want (Forknet-style multi-repo OSS content, agent swarms) require
that repos/workspaces are *sets of hash references* over one pooled
CAS per node, not walled-off per-repo stores. Pooling forces a GC
story (OQ-BL-05) and a multi-backend dispatch story (OQ-BL-02).
7. **Whole-file CAS as the default; chunking earns its keep only
where it must.** For source-code-scale content, whole-file hashing
captures the agent/edit delta perfectly and needs no chunk tree;
fixed-size chunk trees are actually *hostile* to insertion/deletion
edits (byte shifts cascade). Content-defined chunking (CDC —
fastcdc/restic-style, shift-resistant) only pays on large binary
files receiving small edits — the git-lfs weakness. iroh-blobs/bao
use fixed-size trees; do not inherit that as the default. Where
chunking lives (store encoding vs manifest layer) is OQ-BL-04.
## What is already known (settled, thin)
Almost nothing is pinned — deliberately. The postures agreed so far:
- **Not a fork of iroh-blobs.** A downstream store crate inspired by
its `src/store` work, diverging deliberately on hashing and wire
surface (see `iroh-blobs-eval` below when written).
- **Canonical hash = the git-family derivation** (2026-10-01 hash
simplification round; was "multi-hash from day one"). git-blob-SHA-256
as primary, legacy SHA-1 tolerated, BLAKE3 demoted to a conditional
large-blob encoding consideration. Details and the reasoning trail
in OQ-BL-03.
- **Backend pluralism assumed, not designed.** kv/sqlite for small
blobs, filesystem fallback for large — the appfile shape — but
*how* multi-backend dispatch works is exactly what alknet's research
ran into, so it earns research and probably POCs (OQ-BL-02).
- **Pooled CAS, not per-repo stores** (agreed in the 2026-10-01
dedup discussion — the load-bearing scope decision so far): repos
and agent workspaces are sets of hash references over one shared
content-addressed store. This is the property both demanding
consumers actually need; everything else (GC, manifests, git
semantics) lives around it. Details open (OQ-BL-05).
- **Physically flat, logically namespaced** (2026-10-01 GC/pooling
round; the rudolfs inversion): the byte layer is a flat
dedup-by-construction CAS; namespaces are reference tables above it
(GC roots, sweep scoping, ACL boundary). Backends stay
namespace-blind.
- **Whole-file CAS is the default granularity**; chunking is scoped
to the large-binary-blob case (principle 7), not the default path.
## Prior art
### iroh-blobs — the shape inspiration (evaluated, not the base)
`/workspace/iroh-blobs` (fresh upstream checkout; read-only reference).
Specifically `src/store`: the kv backend (small blobs, in-process) and
flat file backend (large blobs) split; bao outboard encoding and
verification flow; chunking. Its BLAKE3-only hashing, tickets, postcard
serialization, and provider protocol are the design-welded choices we
diverge from. **The store eval should be written against the current
checkout** — alknet's older research refers to an older iroh-blobs and
its conclusions must be re-verified rather than inherited.
**GC mechanics — verified against the current checkout (2026-10-01,
`src/store/gc.rs`, `src/util/temp_tag.rs`, `src/store/fs/delete_set.rs`).
This is the load-bearing prior art for OQ-BL-05:**
- **Mark-and-sweep, not refcounting, for persistent liveness.**
`gc_mark_task` collects roots from three sources: persistent *named
tags* (a flat `tags-0` table, name → `HashAndFormat`), in-memory *
`TempTag`s* (RAII-refcounted, `#[must_use]`, `.leak()` for
pin-until-exit-of-process), and an injectable `ProtectCb` — a
callback GC consults before each run that can add externally-known
hashes or **abort the run** (`ProtectOutcome::Abort`; a flaky
protection source skips the sweep rather than risking deletion).
- **Format-aware mark traversal** — a non-raw root (HashSeq/collection)
contributes all reachable children to the live set via the bao hash
stream (gc.rs:63-78). Protecting a collection protects everything
reachable from it.
- **Sweep** lists the whole store and batch-deletes (~100/batch)
anything not live. `list()` being complete is therefore load-bearing
for GC — relevant to the backend trait contract (OQ-BL-02).
- **Write-safety against a concurrent sweep** — the fs backend carries
a separate `DeleteSet` transaction layer (`ProtectHandle` /
mark-for-delete / protect-cancel / commit); `TempTag` holds a refcount
so a blob put but not yet referenced cannot vanish mid-write. Temp
tags are scoped (batch scope / process scope) with per-scope counters.
- **No namespaces in their model** — flat named tags over a flat
pool. Our namespace concept (OQ-BL-05) lands on their machinery as
policy over what counts as a root: `namespace → root manifest →
(mark-walk) → {oids}` is tags plus traversal, not new storage
concepts.
### rudolfs — git-lfs server; the composite-key + decorator-storage prior art
`/workspace/rudolfs` (v0.3.8, MIT, read-only reference; the alknet
research doc is
`/workspace/@alkdev/alknet/docs/research/references/gitlfs/
rudolfs-reference.md`). A git-lfs server whose **storage layer**
(`src/storage/`) is the most direct ancestor of our composite-key +
dispatch shape:
- **`StorageKey = (Namespace, Oid)`** — a composite key over a flat CAS
(`Namespace = (org, project)` strings, `Oid = SHA-256`). All backend
operations take the composite key.
- **`Storage` trait** — `get`/`put`/`size`/`delete`/`list`/`
total_size`/`max_size`/`public_url`/`upload_url`, with
`LFSObject = (len: u64, ByteStream)` — the streaming-first put/get
shape (pinned boxed async byte stream), the natural large-blob API
and likely the channels ops-surface shape.
- **Decorator composition** — `Verify ↔ Encrypted ↔ Cached(LRU →
permanent) ↔ Retrying → s3/disk`. The `Cached` decorator is the
appfile shape in LRU form: "fast store in front of fallback" as a
composable wrapper, an alternative dispatch policy to size
thresholds. `fanout()` duplicates one stream into two lock-step
copies (serve + persist) — precisely "serve the fetch while
persisting on receipt" for networked gets.
- **Verify-as-decorator (OQ-BL-04 input)** — streaming SHA-256 check on
both put and get paths with auto-purge of a corrupted tier.
- **Footnote if at-rest encryption is ever considered** — nonce derived
from the oid: deterministic and correct, but a key rotation
invalidates every object and breaks dedup across keys (the known
encryption-vs-dedup tension).
- **THE ANTI-PATTERN (inverted, not inherited)** — namespaces are
*physical*: `s3://{org}/{project}/{sha256}` — identical content in
two orgs is stored twice. It buys tenant isolation by paying the
cross-tenant dedup that is our whole reason for pooled CAS. The
reconciliation recorded in OQ-BL-05: keep the composite-key API
shape, put the namespace half in metadata (reference tables) above
the byte layer, and keep physical storage flat (`oid → bytes`,
backends namespace-blind).
- **`list()` is load-bearing** — the S3 backend punts on `list()`
(returns an empty stream) and its `delete()` is a no-op, so it can
*never* GC. A lesson for the backend trait contract: list/sweep
support is a real requirement, not an optional extra.
### gix-odb — git's own object database (alkgit's baseline)
`/workspace/git-oxide/gix-odb` (read-only reference). The backend
alkgit currently plans against; the hashing-algorithm baseline git
actually uses (SHA-1/SHA-256), git's loose-object and packfile layout,
and git's already-hash-addressed object model. The alkgit question is
whether this crate can sit *under or beside* gix-odb semantics — git
objects are already content-addressed and verified by git's own model,
so the crate's value there is the large-blob story (git-lfs-shaped)
and a better small-object backend than the proposed default, without
fighting git's hash model. Note for the p2p case: git's `alternates`
/ object-pool mechanism (e.g. GitLab object pools for fork networks)
is the prior art for cross-repo dedup among related repos — the
pooled-CAS posture (OQ-BL-05) generalizes it to unrelated repos and
to p2p replication.
### alknet's appfile external-store probe — the direct ancestor
`/workspace/@alkdev/alknet/docs/research/alknet-filesystem/`
(`alknet-blobs-external-store-probe.md`, `poc-summary.md`) — written
against an older iroh-blobs; conclusions re-verify in this Phase 0.
The load-bearing bits: the appfile shape (small blobs in kv/sqlite,
large on fs fallback), the filename↔hash mapping problem, and the
multi-backend dispatch pain the probe hit (which is this crate's
reason to own that dispatch). Old-data warning: any API or behavior
claims there describe an older upstream; re-check against
`/workspace/iroh-blobs` as checked out today.
### alkcall — the substrate
`/workspace/@alkdev/alkcall` (the call + channels RPC crate). The
authorization seam for network-facing blob ops (`AccessControl`,
producer/consumer vocabulary, alkcall ADR-022/037) and, if blob
transfer rides channels, the established data-path patterns
(`BiStream`, two-pump `pump_bidi` ADR-050). Whether transport even
belongs in this crate is itself open (OQ-BL-01); alkcall is the
substrate it composes with *when it does*.
## Open Questions
Numbering is provisional until the register solidifies; promote the
final set into Phase 1's `docs/architecture/open-questions.md`.
### OQ-BL-01: Crate scope — store-only, or store + ops surface?
The store (put/get/verify) is clearly in. What about the network ops
layer — a channels-native op family on the alkcall substrate,
auth-gated via `AccessControl`? iroh-blobs has it (provider protocol);
our posture is to separate it. Options: (a) store-only crate, ops in a
sibling crate later; (b) store + optional feature-gated ops module
here; (c) undecided pending the first consumer's shape.
Original lean was (a)/(b) on the inversion-point pattern. **Updated
2026-10-01:** the p2p-git consumer (§The p2p shape) makes the ops
surface likely rather than speculative — because everything is
hash-addressed, p2p sync *is* "announce hashes, diff hash sets, fetch
missing blobs verified," which is an op family on this store. Whether
that family lives here (feature-gated) or in the sibling that owns
replicator/gossip policy is the remaining question; the deciding input
is what alkgit's replicator actually needs and whether alknet's
appfile case ever transfers over the network. Even so, the *shape* of
the ops (have/need + verified fetch, ACL-gated) is now firm enough to
plan against.
### OQ-BL-02: Multi-backend dispatch — the appfile problem
**Base settled by POC #1 (2026-10-02, `poc-trait-dispatch-findings.md`
findings 1/2/6):** the lean `Backend` trait contract works — opaque
byte keys (typed `Key` at the store-layer boundary only),
`has`/`get`/`put`/`delete`/`list`/`name`, `list()` complete by
contract (a malformed list is as deadly as an incomplete one — the
reedb trap in finding 2), and size-threshold dual dispatch (kv small /
fs large) is a thin layer with deterministic per-digest routing. The
"virgin store" read path must treat a missing table as absent/empty.
Remaining (above-trait concerns, no further POC gate): migration
between backends policy; whether per-namespace backend config is ever
needed; the sqlite-vs-fs micro-benchmark (POC #3's pull-out) still
anchors the small-blob belief the dispatch leans on; streaming put/
get shape rides POC #3's findings (`Vec<u8>` was the *contract*
question, not the performance question).
### OQ-BL-03: Hash abstraction — RESOLVED: canonical git-family hash
**RESOLVED 2026-10-01 (the hash simplification round).** The original
question — "trait, enum, or per-backend config for tolerating multiple
hash algorithms?" — was based on a premise worth questioning first:
*why did iroh-blobs' BLAKE3 ever enter our requirements?* traced back to
pure inheritance ("iroh-blobs uses it"). With that inheritance
explicitly rejected (convention 7's posture), the reason evaporated, and
the multi-hash framing dissolved with it. It was replaced by asking
which algorithm the *consumers* force:
- **alkgit forces git's derivation** — by protocol definition, not
preference. Unavoidable.
- **The vfs/appfile/workspace case forces nothing** — any cryptographically
sound content hash works; the consumer never sees a hash *algorithm*
requirement, only *addresses*. It can use whatever the store keys.
- **Nothing else forces BLAKE3.** Its real virtues were (i) merkle-
nativeness (chunk-tree verification/range reads — iroh's transfer
story, valuable only if/when the conditional large-blob encoding
adopts chunking, OQ-BL-04) and (ii) raw hash throughput (irrelevant
at our scales: small-file paths are I/O/SQLite-bound; large-file
paths hash at disk speed even with SHA-256's hardware-accelerated
~1-2 GB/s). Neither is a requirement today.
**The resolution, three tiers:**
1. **Canonical: `git-blob-sha-256`** — git's oid derivation
(`"blob <len>\0" + content`, SHA-256, uncompressed content) is the
store's canonical algorithm. Git objects land in the pool as
first-class entries (the odb addresses the pool directly, no
indirection); and because one algorithm domain = one address space,
non-git consumers (workspaces, appfiles) collide into the same
pool *by construction* — the workspace manifest's path→hash entries
point at git oids without any mapping layer. A stronger dedup
posture than the multi-hash design offered: cross-consumer dedup
for free.
2. **Tolerated legacy: `git-blob-sha-1`** — existing git repos are
overwhelmingly SHA-1; the store must accept them (protocol
necessity, git's own hardened-SHA-1 threat model inherited verbatim
— a store entry under `git-sha-1` behaves like any git odb entry).
This is what keeps the abstraction an enum/capability set rather
than a hardcoded single algorithm: SHA-1-vs-SHA-256 is already two,
and the key encoding is a one-way door (a hash's length is baked
into physical keyspace and any wire surface).
3. **Conditional: BLAKE3** — demoted from requirement to encoding
consideration. If the large-blob path ever adopts a bao-like
chunk-tree verification encoding, BLAKE3 lives *there* (confined
to the encoding layer); its trees' verified leaves/wholes would be
registered into the pool as ordinary entries, like any other
content. Not designed for now; recorded so the demotion is
deliberate rather than forgotten.
**Security footnote for the record:** one algorithm across consumers is
safe here — SHA-256 has no practical cross-protocol ambiguity at these
input shapes, and the git-blob prefix domain-separates the derivation.
The SHA-1 caveat is already git's accepted position (collision-hardened
SHA-1); any real git consumer lives with the same.
**Residual (Phase 1, now smaller):** abstraction shape is a small enum
over `{git-sha-256 (canonical), git-sha-1 (tolerated), [blake3
(conditional)]}` with domain-separated input handling (the
`blob <len>\0` preamble is why `fn(content) → digest` alone is
insufficient) — but the *why* of each member is now decided; what
remains is key-encoding mechanics (byte layouts, length prefixes,
wire representation), which is a Phase 1 ADR with the first wire
consumer in sight (OQ-BL-01). The wire-format question reads OQ-BL-03
first — a wire ADR inherits whatever key-encoding mechanics this
settles.
**Implementation-validated by POC #1 (2026-10-02):** the
domain-separated preamble abstraction carried both algorithms;
`git-sha256` and `git-sha1` coexist in one flat pool via the tagged
key encoding (`[algorithm byte][digest]`); external-git oids address
pool entries with no mapping layer (byte-exact vs `git hash-object`,
including the empty blob). Residual Phase 1 notes from the POC: the
algo-discriminant must remain part of the key itself (a per-store
single-algorithm config would not have survived the interop test); a
single leading tag byte is probably needless (currently one key
kind); fixed-length enum-tagged keys sort cleanly for the sweep's
live-set diffs.
### OQ-BL-04: Verification and chunking story
iroh-blobs' verification is bao outboard encoding over fixed-size
BLAKE3 chunk trees (BLAKE3-native, and we rejected the BLAKE3
inheritance — OQ-BL-03); git objects are self-verifying under git's own
model; raw large blobs (git-lfs-shaped, appfile large files) need
*some* verification story from us. Options (per the hash resolution,
none of them BLAKE3-mandatory): per-blob digest checks on put/get
paths (rudolfs' verify-as-decorator, verified prior art), a
bao-like chunk-tree encoding as a *conditional* later addition (BLAKE3
confined to that encoding layer, OQ-BL-03 tier 3), or backend-native
verification.
**Chunking is scoped (2026-10-01, principle 7), not open-ended:** the
default is whole-file CAS — content-defined chunking (fastcdc/
restic-style; shift-resistant, unlike bao's fixed trees) is an
encoding consideration *only* for large binary blobs receiving small
edits, and git objects are unit blobs that never want chunking. The
remaining question is *where* variable chunking lives if adopted: in
the store's encoding layer (a blob is stored as chunk tree + the
store serves ranges) or in the manifest layer above (chunks are
themselves small blobs; the store stays whole-file-only — the dedup-
friendlier option, since chunks are then pooled and cross-file shared
like any other content). Decision input: how alkgit handles
git-lfs-shaped large files vs how agent workspaces transfer large
artifacts, and whether range reads are a store API or a
reassemble-above concern. With the canonical-hash resolution
(OQ-BL-03), a chunk-tree encoding that does get adopted would register
its verified whole-contents as git-blob-sha-256 pool entries (a
`git-blob-sha-256` digest over the reassembled content), so chunked
and whole-file paths dedup against each other in the one pool.
### OQ-BL-05: Pooling and GC — namespaces as reference sets over a flat CAS
**The decisions from the discussions (2026-10-01, principles 6; the
GC/pooling round; the rudolfs collision):**
1. **One pooled CAS per node**; repos/workspaces are sets of hash
references (manifests/trees) rather than isolated stores —
cross-repo dedup by construction.
2. **Physically flat, logically namespaced** (the rudolfs inversion).
Physical layer: `hash → bytes`, flat, backends never see the
namespace (`{hex-prefix}/{hex-prefix}/{hash}` sharding survives —
fan-out, not partitioning). Logical layer: `namespace → {hashes}`
reference tables, which simultaneously give GC roots, per-namespace
sweeps, accounting, and the ACL boundary (alkcall `AccessControl`
gates the namespace for network ops). rudolfs' composite
`StorageKey(ns, oid)` API shape is kept; only its physical
namespacing is inverted.
3. **Tags are the root table; references stay above the crate.**
iroh-blobs' named `Tag` (flat name → hash+format table) is the
"an external consumer cares about this hash" concept — namespaces
map onto it as `namespace → root manifest tag → mark-walk → {oids}`.
Git-flavored: refs (`refs/heads/*`) *are* the names; the consumer
registers them as tags. LFS pointer files are the same shape in
another hat — the pointer (oid + size) in the tree *is* the
reference; no extra tag needed because the consumer's own
structure holds it. **Store stays structure-blind**: it does not
walk consumer-side manifests.
**The GC picture, assembled from verified prior art:**
- **Mechanism: mark-and-sweep from registered roots** (iroh-blobs'
model, verified — see §Prior art). Roots = registered namespace/root
tags + put-path RAII pins (TempTag-shaped) + a protect callback with
abort semantics (`ProtectOutcome` — protection-source errors skip the
sweep rather than risk deletion). The store does sweep; liveness
beyond "these roots exist" is the consumer's job (it walks its own
manifest format) — the callback seam.
- **Traversal ownership is THE open sub-question.** Iroh can walk
children in-mark because HashSeq is store-native; our
manifests/trees live above the crate, so either **(a) generalized
protect callback** — the layer above computes liveness and hands
hash sets to GC (store stays structure-blind; simplest; liveness
computation is the consumer's), **(b) store-native manifest format**
(a hashseq-like encoding so mark can traverse — cheap GC, but drags
structure into the store layer and creates a wire-ish format), or
**(c) reference-tracker seam** — roots registered with a
children-of(hash) callback; the store does mark/sweep through it.
Tentative lean: **(a) + temp-tag pinning**, revisiting (b) only if
traversal-per-sweep proves expensive (a large multi-tenant node
could pass large hash sets each run; incremental sweeps are an
in-(a) option since the callback is injectable). The three options
are a Phase 1 ADR, decided with the first consumer in sight.
- **Temp-tag pinning is needed regardless** of which option wins: a
blob put but not yet referenced by any namespace must survive
mid-write before any sweep sees it (iroh's `TempTag`/batch-scope
pattern).
- **Per-namespace sweeps** become possible with the reference tables
(delete only within a reclamation scope) — the multi-tenant
refinement the flat iroh model can't express; not needed at minimum.
- **Delete-then-recover semantics** — CAS append-only by nature; a
swept-but-somehow-referenced blob is a re-put, not corruption. The
dangerous direction is only deleting liveness (data loss), which the
abort-callback + pinning layers exist to prevent.
**Still open (the residual mechanics):** namespace registry shape
(namespaced root tags vs separate reference tables — the POC built
in-memory reference tables; persistence of the tables is Phase 1
mechanical work), GC scheduling (interval + injectable protect vs
consumer-driven sweeps), **concurrency** — the sweep-vs-put race is a
named Phase 1 requirement with iroh's DeleteSet/ProtectHandle
transactions as verified prior art (POC #1 finding 3; the POC worked
single-threaded), batch-scope pins for multi-put writes (finding 3),
and the callback-must-be-live-shared API lesson (finding 7 — prefer a
`register_liveness_source`-shaped seam over any install-copies-state
verb). Mark-and-sweep-with-protect-callback (lean (a) + pinning) is
now empirically the right mechanism; option (b) (store-native
manifest traversal) stays parked unless a real consumer's live-set
computation proves too heavy.
**The pack tension (alkgit-shaped, constrains the store surface)** —
git packfiles are pack-efficient but blob one object-store-per-pack
(bad for cross-repo dedup); loose-per-object in the pooled store dedups
but is fs-inefficient at git's object volumes. Small git
objects in the kv backend largely dissolve this for the common
case; the residual question is what the store must support for
large/packed content — per-object granularity, range reads into
packed blobs, or both (interacts with OQ-BL-04's range-read
question — likely read them together).
### OQ-BL-06: POC register (draft)
Numbered POCs, opened as research reaches them (findings land in
`docs/research/`; worktree placement per the SDD process). The GC round
reshaped the register — see the discussion at the end for what each POC
is actually deciding:
| # | What | Status | Where |
|---|------|--------|-------|
| 1 | Backend-trait + dispatch shape (kv small / fs large); trait must include `list()` complete by contract (rudolfs anti-lesson) + temp-tag/pinning on the put path; the hash-enum abstraction with git-blob-sha-256's domain-separated preamble + git-sha-1 tolerance case | **Passed 2026-10-02** — `poc-trait-dispatch-findings.md` (8 findings; code: `/workspace/alkblobs-trait-poc`) | findings landed |
| 2 | ~~Multi-hash store~~ **Absorbed into #1** (2026-10-01 hash round): the canonical-hash resolution removed the "two families coexisting" question; what remains (the preamble abstraction + SHA-1 tolerance) is POC #1's trait work | **Absorbed** (and validated by #1) | — |
| 3 | Large-blob path (iroh-blobs store read under current checkout; fs fallback + range reads; streaming `LFSObject`-shaped put/get + `fanout` seam) + the small-object sqlite-vs-fs micro-benchmark (the dual-belief anchor) | **Pending** — reading-and-design POC + benchmark | findings file TBD |
| 4 | Pooled CAS + GC: namespace reference tables over the flat pool; mark-and-sweep with protect-callback + TempTag pinning; delete-then-recover semantics | **Covered in miniature by #1** (2026-10-02): namespace tables + sweep + recover all validated single-threaded; the concurrency half (sweep-vs-put race, batch-scope pins) is Phase 1 implementation work, not a POC gate | `poc-trait-dispatch-findings.md` findings 3/7/8 |
Sequencing note: #1 is done (single opening worktree; multi-hash
subsumed; git-preamble hashing validated byte-exact vs the git CLI);
#3 is a reading-and-design POC against the current iroh-blobs checkout
plus the alknet probe re-verification, with its micro-benchmark
pull-out; #4's single-threaded core is covered by #1 (see the
register row) and the residual is implementation work. The pack
tension (OQ-BL-05) rides #3's reading rather than earning its own POC
yet.
**What each POC is deciding (updated 2026-10-02, post-POC-#1):**
- **POC #1 — DONE.** Decided the backend trait's minimum contract
(opaque byte keys, `list()` complete-by-contract, put-path
pinning) and validated the canonical-hash key encoding (git's
domain-separated preamble for both SHA-256 and SHA-1; external-git
oid interop byte-exact). Findings summary in the table row; full
detail in `poc-trait-dispatch-findings.md` (findings 1-8: opaque-
keys-at-the-boundary, the redb list/read-path traps, the pinning
shape + sweep-race caveat, the CLI-validated digest, the tagged key
format, dispatch simplicity, the callback-must-be-live-shared
lesson, GC semantics).
- **POC #2 is no more** — the "SHA-256 + BLAKE3 coexisting" question
evaporated when BLAKE3's only reason-for-being (iroh-blobs
inheritance) was rejected, and its residual content validated under
#1.
- **POC #3 is the next (and last) POC** — reading-and-design against
the *current* iroh-blobs checkout (the store eval) + re-verifying
alknet's probe + the **micro-benchmark pull-out** (the "kv/sqlite
is faster for small blobs" belief underlies the dual-backend design
and traces to *old* alknet research — anchor it: small objects,
1KB-64KB, both backends, random + sequential). The pack tension
(OQ-BL-05) is analyzed here without its own POC.
- **POC #4's single-threaded core is covered by #1** (namespace
tables + mark-and-sweep + protect-callback abort + delete-then-
recover all validated; `poc-trait-dispatch-findings.md` findings
3/7/8). The concurrent half (sweep-vs-put race, batch-scope pins,
incremental scheduling) is Phase 1 implementation work — the
mechanism choice it was gating is settled, and races are
construction details with iroh's DeleteSet as the named prior art.
POC placement conventions (inherited from alksocks/alktunnels): a POC
that needs code from this repo runs in a worktree/branch
(`.worktrees/research/<task-id>/` per the SDD process); a
self-contained POC runs as a standalone crate in the global workspace
with findings written into `docs/research/` here. Findings always land
in `docs/research/` regardless of where the code lives.
## Phase 0 plan (next steps)
1. ~~Write `iroh-blobs-eval.md`~~ — still open, rides POC #3's
reading against the *current* checkout (kv + flat backends, bao,
chunking; the GC mechanics eval is already done — see
`poc-trait-dispatch-findings.md`'s context and phase-0's verified
GC section).
2. **Re-verify alknet's probe** (`alknet-blobs-external-store-probe.md`,
`poc-summary.md`) against the current upstream; mark what carried
over — rides POC #3 too.
3. **Read gix-odb** (`/workspace/git-oxide/gix-odb`) for the alkgit
baseline: its storage layout, object model, and where a blob-store
crate under/beside it earns its keep.
4. **POC #3** (the last open POC): reading-and-design + the
sqlite-vs-fs micro-benchmark per OQ-BL-06.
5. Converge: recommended approach + final OQ register → Phase 1.