Files
poimen/tasks/T0.7-blobstore-port-redb-implementation.md

4.0 KiB

T0.7 — BlobStore port + redb implementation

Field Value
Phase P0 — Foundations
Size M — 1 to 3 days
Status Not started
Flags
Spec inlined below
Blocks

Goal

Content-addressed blob storage, deduplicated within a tenant and never across. delete ships now, not later.

Facts (inlined — no spec read needed)

#[async_trait]
pub trait BlobStore: Send + Sync {
    async fn put(&self, tenant: TenantId, content: Bytes) -> Result<BlobRef>;
    async fn get(&self, tenant: TenantId, r: &BlobRef) -> Result<Option<Bytes>>;
    /// Reduction and tenant deletion both require this. A store that cannot
    /// delete cannot honour either, and both are obligations.
    async fn delete(&self, tenant: TenantId, r: &BlobRef) -> Result<()>;
}
  • Blobs are namespaced per tenant even though they are content-addressed. Global dedup of prompt and output blobs is tempting — identical system prompts across tenants are common — and it is a leak: a shared blob makes one tenant's storage accounting depend on another's, and a hash becomes an oracle for "does anyone else have this content".
  • get returning None is a normal outcome, not an error. After reduction (T8.4) the original body is deleted and the log carries a Reduced { original, summary } event explaining what stands in its place.
  • Content addressing means the ref is a hash of the body. A body must never be rewritten under an existing ref — that makes the ref a lie.

Steps

  1. Define BlobRef as a Blake3 hash newtype. Define the trait as above.
  2. redb table keyed on Scoped<BlobRef> — tenant in the key is what enforces per-tenant namespacing structurally rather than by convention.
  3. put: hash the content, insert if absent, return the ref. Insert-if-absent is the dedup, and it is scoped by the key, so no cross-tenant path exists.
  4. get: point lookup, Ok(None) on miss.
  5. delete: remove the key. Deleting a ref one tenant holds must not touch another tenant's identical content — which follows from the key shape, and the test below is what proves it.
  6. Write the conformance suite generic over impl BlobStore so T7.2's object-store backend reuses it unchanged.

Acceptance

  • Identical content put under two tenants produces two independent blobs.
  • Deleting tenant A's blob leaves tenant B's readable.

Verify

Harness: conformance suite generic over impl BlobStore, reused verbatim by T7.2's object store.

Integration testtests/it_blobstore_conformance.rs:

  1. put identical bytes under tenant A and tenant B. Assert the returned BlobRefs are equal (same content hash) and that a raw table scan shows two stored entries.
  2. delete tenant A's ref. Assert get(A, ref) is Ok(None) and get(B, ref) still returns the bytes.
  3. put the same bytes twice under one tenant; assert one stored entry (dedup within the tenant) and a stable ref.
  4. get a ref that was never written: Ok(None), not an error.
  5. delete a ref that does not exist: Ok(()), so reduction can run twice.
  6. Re-hash every stored body and assert it matches its key.

Command: cargo test -p storage --test it_blobstore_conformance

False pass:

  • Step 1 asserting only that both gets return the right bytes. A single shared blob passes that. The raw scan showing two entries is the isolation proof.
  • Step 2 passing because delete is a no-op stub. Assert the None on A explicitly, not just B's survival.

Traps

  • A global content-addressed table with tenant tracked in a side index. It dedups across tenants by construction, which is the leak.
  • Deferring delete because nothing calls it yet. Reduction and tenant deletion both need it, and retrofitting a delete path into a store designed without one is a rewrite.

Background (not required to do this task): rust-agentic-sys.md §3, §7, §8.6 · rust-agentic-task.md