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

35 KiB


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 * TempTags* (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.