- Reframe storage identity: git-compatible sha1/sha256 (algorithm-tagged), not BLAKE3 — the ancestor POCs' BLAKE3 was an iroh-blobs artifact, and consuming iroh-blobs (OQ-FS-03 option (a)) dissolves with it; findings transfer (inline/outboard hybrid, GC shape, write-ordering analysis), code doesn't - Chunk-level dedup adopted as working goal for large content, with the honest calibration recorded: git dedups at object granularity (objects are effectively single-chunk); chunk manifests are the generalization that pays on workspace-scale files - Two-crate working direction: alkblobs (bucket-style CAS blob store, appfile layout) beneath alkfs (path tree + branches + serving protocol); durability contract now crosses the put/get seam - New OQ-FS-16: remote-mount anchor workload — today's sshfs/vanilla-SFTP mount of this dev server's workspace, with an honest decomposition of why it's slow and what an indexed VFS fixes server-side - POC register: #3 rewritten as alkblobs sha1/sha256 store skeleton, #4 reframed (git objects ride whole-file), new #8 mount-workload baseline (sshfs latency measurement, runnable today) - AGENTS.md: two-crate direction in the crate description + convention 10 (identity, chunk-granularity note, seam durability contract) + a Phase-0 working-direction quick-reference section (leanings, not decisions) Verification: docs-only repo — all 16 OQ ids contiguous, every referenced workspace path exists, BLAKE3 references re-checked as historical context
52 KiB
status: draft last_updated: 2026-09-23 (second pass, same day — storage identity reframed to git-compatible sha1/sha256: the ancestor POCs' BLAKE3 was an iroh-blobs artifact, not a choice; chunk-level dedup adopted as the working goal; alkblobs/alkfs two-crate split added as the working direction; the remote-mount SFTP use case recorded as the anchor workload. Still unconverged — every OQ is open, the POC register holds proposals not results)
alkfs — Phase 0 (Exploration)
This document captures Phase 0 (Exploration) for the alkfs crate: vision,
guiding principles, prior art, open questions (OQ-FS-01..NN), and the
candidate POC register. 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-23 from the initial setup discussion, revised the same day as the storage-identity reframe landed (§Working direction). The crate is the storage-engine member of the alk* family: alkcall (call + channels — the substrate), alktty/alktunnels/alksocks (the protocol-crate siblings), alkgit (the first in-family storage consumer, currently paused on exactly the problem this crate exists to solve).
Honest framing: this Phase 0 is earlier in its lifecycle than alksocks' was at the equivalent commit. There are more unknowns here than in any sibling so far — a filesystem is both a storage engine (hashing, chunking, durability, GC) and a serving protocol (verbs, sessions, wire framing), and almost none of those decisions have empirical ground yet. The POC register below is proposals, not results. Expect this document to be rewritten several times before convergence.
Vision and guiding principles
One sentence: a content-addressed, branch-aware virtual filesystem for the alk family — a local storage engine (path-tree metadata + content-addressed blobs) with a producer/consumer serving protocol on alkcall channels, so that alkgit's object storage, the coming alksftp door, and the alknet filesystem vision all compose on the same substrate — and it never touches the host filesystem unless explicitly configured to.
Working direction (2026-09-23, from the setup discussion — leanings to converge, not decisions):
- Git-compatible identity, not BLAKE3. The ancestor POCs used BLAKE3 because iroh-blobs does; flipping the causality, the blob layer should carry the appfile concept with the hashes git uses (sha1/sha256). This dissolves the "BLAKE3 must hash the whole file before the address exists" problem into a chosen design (OQ-FS-04), and makes git object identity native rather than mapped.
- Chunk-level dedup is the working goal for large content (workspace-scale files, datasets, logs). Whole-file hashing dedups only identical files; git itself dedups at object granularity (packs delta-compress within a pack, but the address space is per-object); chunking is the storage-layer generalization (OQ-FS-04 — measure whether the workloads pay before committing).
- The two-crate split:
alkblobs+alkfs. A bucket-style content-addressed blob store (the S3 shape) beneath the path-tree mapping (the alknet-filesystem POC's layer 2). alkgit can consume the blob store without a filesystem; the FS layer keeps its own crate (OQ-FS-01). - The anchor workload is remote mounting. The concrete use case:
the dev-server workspace mounted on a local Linux machine under
remote/— today over vanilla SFTP (sshfs), which is slow in known ways (a round trip per syscall, stat storms, full inode work per stat). An appfile-indexed VFS fixes the server side; caching and pipelining close the rest (OQ-FS-16).
Why this crate exists. The alknet mono-repo's filesystem research (three POC iterations, 24 passing tests) proved a viable three-layer architecture but lived inside a project with excessive scope. The alk* family has since been decomposed and published (alkcall, alktty, alktunnels, alkvault; alksocks and alkgit in flight). alkgit paused because its storage problem — content-addressed blob storage with branch/CAS semantics — is not a git-specific problem, and solving it inside alkgit would leave alksftp and the alknet filesystem rewrite to solve it again. The same basic "shape" recurs across the family: git + git-lfs is small-object CAS metadata over big-blob content storage; iroh-blobs' FsStore is inline-small/outboard-large; the alknet-filesystem POC is SQLite path edges over a blob store. alkfs is that shape, made a crate.
Scope sketch (unconverged; restructured 2026-09-23 around the two-crate working direction — Phase 0 must confirm or cut, OQ-FS-01):
- alkblobs (candidate companion crate) — bucket-style content-addressed blob storage (the S3/rudolfs shape): put/get/has/ delete by hash, tags/pins for liveness, GC, the appfile layout (one transactional store for blob metadata + inline small content; file-backed spill-over for large content), git-compatible sha1/ sha256 identity, chunk-level dedup for large content. Fully local, embeddable in-process, no channels required; engine-only in v1 (no serving ALPN of its own yet — OQ-FS-09).
- alkfs (this crate) — the path-tree mapping on top (the POC's
layer 2): paths, branches, snapshots, tombstones, cached sizes,
write sessions (branch-on-write/merge-on-close); plus the
producer/consumer serving protocol on alkcall channels — a producer
registers openable filesystem channels
(
ChannelCore::register_openable); a consumer opens them and speaks the file protocol inside. Thealk/fs-shaped ALPN resource, the "ALPN as a service" shape of alktty/alktunnels/alksocks. Depends on alkblobs, or a narrow blob seam if the split puts a trait there (OQ-FS-01). - Doors stay out — alksftp (SFTP door), FUSE/mount adapters, sync tools are separate crates/features that consume alkfs, per the family rule (alkgit vision: doors are family infrastructure, not crate contents).
Guiding principles, inherited from the alk* family plus the filesystem research:
- No forced host-FS access — the defining requirement. The analogue of alksocks' "never binds a port": the engine manages its own content store and never reads, writes, or mounts the host filesystem unless explicitly configured to. Every bridge to the real world (a blob-file store, an SFTP door, a mount, a sync tool) is an explicit, optional, feature-gated assembly shape. The engine itself is storage-agnostic about what backs its blob layer (filesystem files, SQLite, remote).
- Metadata/content split is load-bearing. Small structured state (path edges, branch pointers, tombstones, sizes) lives in a transactional store; content bytes live under their hash in the blob layer. Path operations are O(path edges), never O(bytes) — rename is O(1) regardless of file size; identical content dedups across branches and paths by construction (and, with chunking, dedups at chunk granularity). This is the alknet-filesystem POC's core conclusion and the same shape as git + git-lfs and iroh-blobs' inline/outboard split — with git-compatible identity instead of BLAKE3.
- Content addressing with a durability contract. Content is
identified by its hash, not its path — with the hashes git uses
(sha1/sha256), so git object identity is native to the store, not
mapped. The invariant that must survive every design choice: content
is durable before any metadata row naming it commits; a crash
mid-write leaves the old version intact and visible; no torn version
is ever readable; no metadata commit orphans its content. With the
two-crate direction the contract crosses the put/get seam (alkfs
must be able to trust that an alkblobs
putreturning means durable, or get an explicit durability level back) — the seam contract is an OQ. Exact hashing/chunking is an OQ; the invariant is not. - Branch-aware from day one. Fossil-style branches (a branch is a parent chain + per-branch overrides + tombstones), branch-on-write/ merge-on-close for the write path. This is what gives free multi-agent/multi-fork content sharing and the POSIX concurrent-reader-sees-old-version property — and it is the property git's object model needs (content shared across refs by hash).
- "ALPN as a service," not "a server." The filesystem is a produced, ACL-scoped resource on a channels connection. The protocol never opens host files or binds anything; door-shaped conveniences (local loopback front door, in-process consumer handle) are optional assembly-layer shapes on either side. Producer/consumer vocabulary, never server/client.
- Substrate-agnostic protocol, native engine. The file-protocol
state machine is generic over
T: AsyncRead + AsyncWrite + Unpin(fast-socks5/alksocks genericity precedent); the storage engine is native-only behind features (real FS I/O, SQLite). Whether the protocol layer itself stays wasm-clean is an OQ, not an assumption. - Multi-tenancy is a where-clause, not an afterthought. The POC's
bucketcolumn is the isolation unit; identity → bucket mapping is an assembly-layer auth question; per-path/per-bucket policy is a Phase 1 design question. A filesystem is arbitrary read/write by nature — the open gate and path-scope policy are the security boundary. - Bounded resources on every serving path. alkgit's ADR-009 posture (wall-clock, size, round limits) is the consumer shape this crate must serve; the serving protocol inherits alkcall's backpressure and channel-cap invariants rather than re-deriving them.
What is already settled
The foundation is POC-validated and (upstream) ADR-pinned; this crate does not start from zero. It inherits:
- The channels substrate — demux→Connection→handler→mux, the
establishment story (
register_openable_with_establisher, alkcall ADR-049; refused sessions are typedchannel:open_failedcall errors, never phantom channels), the identity seam (CF-005/CF-006: per-call opener identity on the open-op hooks), ACL-for-free viaregister_openable, the two-pump data plane (channels::pump_bidi, ADR-050 — use it, do not hand-roll), hub relay (ChannelRelay/HubLegTemplate, ADR-051). All production-validated by alktty/alktunnels/alksocks. - The metadata/content split, POC-proven. alknet-filesystem POC iteration 1: SQLite path tree over a blob store, 8 tests — branch inheritance, tombstones, O(1) rename, bucket isolation, honker notify-atomic-with-mutation.
- The write path, POC-proven. Iteration 2: branch-on-write/ merge-on-close reconciles "BLAKE3 must hash the complete file" with "writes arrive in chunks, possibly out of order" — concurrent readers see the old version until close() commits atomically; crash/abort leaves the old version intact. 7 tests.
- The distributed-sync shape, POC-proven but scope-uncommitted. Iteration 3: path tree as an automerge CRDT synced over QUIC; 9 tests; metadata and content sync separately; concurrent-root-map initialization is a real constraint (root structures must be created by one node and synced before others write). Whether any of this is alkfs v1 scope is OQ-FS-14.
- The iroh-blobs store-seam findings (the external-store probe). The
alknet-blobs probe traced iroh-blobs 0.103's full
Commandactor surface: there is noStoretrait —Storeis a concrete struct over anirpc::Client;MemStore/FsStoreare actors; a third store is a third actor speaking the pubCommandenum. Blockers are a ~4-line visibility PR (Store::from_sender,ref_from_sender,ApiClient, theScopefield); GC (run_gc) is pub and reusable; ~120-160 lines of bao-tree glue must be re-implemented per store. The probe's cautionary half:TempTags/TempTagScopeare sealedpub(crate)— sealed surface is exactly what turns "external store" into "fork" (AGENTS.md convention 13's cautionary example). - The consumer shapes this crate must serve. alkgit's backend trait
family (
docs/architecture/backend.mdthere:GitRefs,GitPackGen,GitPackIngest— streaming pack generation/ingestion, CAS ref transactions, budgeted resources) and the russh-sftpHandlertrait (the POC's near-1:1 SFTP↔PathTree mapping table). "Optimize for git and sftp-like" is the working posture. - The SQLite-as-application-file-format insight. BLOBs < ~100KB are faster inline in SQLite than as filesystem files; atomic transactions over path-tree metadata; the schema is the documentation. iroh-blobs' FsStore discovered the same shape independently (redb inline tables + filesystem files). The inline-small/outboard-large hybrid is the metadata/content split at storage granularity.
- The family conventions — AGENTS.md in this repo (error handling, tokio subset, feature-gating, upstream posture, one-way doors) and the alkcall ADR set listed there.
Prior art
alknet-filesystem POCs — the direct ancestor
/workspace/@alkdev/alknet/docs/research/alknet-filesystem/
(poc-summary.md, alknet-blobs-external-store-probe.md), POC crates at
/workspace/alknet-filesystem-poc and /workspace/alknet-fs-sync-poc.
The three-layer conclusion (SQLite path tree + iroh-blobs content +
honker coordination) and the write-path resolution
(branch-on-write/merge-on-close) are this crate's inheritance, not prior
art to re-litigate — but every dependency choice in those POCs is open
again:
- SQLite path tree (
rusqlite0.39, bundled):buckets/branches/paths/tombstones+write_sessions/write_chunks; recursive-CTE chain walk resolves(bucket, branch, path)→ content link in sub-millisecond time; only deltas are stored per branch. - iroh-blobs
MemStoreas the content layer (deliberately notFsStore— no redb, no fsync rabbit hole in the POC). - honker-core for notify-in-transaction (the transactional-outbox
pattern built in):
SELECT notify(...)inside the same transaction as a path-tree mutation; watchers wake on commit, not poll. - What did NOT transfer and why — the POC's remaining unknowns (FsStore/redb coexistence, SFTP wiring, GC/tags, chain-depth perf, snapshot semantics) are this Phase 0's OQ list; the probe resolved the two-database question by reframing it (external actor, own SQLite file), and the redb question dissolves entirely if alkfs owns its blob metadata in its own store.
iroh-blobs — the nearest CAS store (evaluated; NOT adopted, and the BLAKE3 causality)
/workspace/iroh-blobs (v0.100 checkout) + published 0.103. DESIGN.md
is the best available treatment of the blob-store trade space (files are
hard: the bitfield/data/outboard write-ordering problem). What it gives:
content addressing, bao-tree verified streaming (per-chunk integrity with
an outboard), dedup by construction, partial-range reads, tags +
mark-sweep GC, a hybrid store (small inline in redb, large as filesystem
files) — the same metadata/content split as principle 2.
The causality correction (2026-09-23): the ancestor POCs' BLAKE3
choice was an iroh-blobs artifact — BLAKE3 is load-bearing across the
whole iroh-blobs surface (Hash type, bao outboards, the Command
actor's payload types), so consuming iroh-blobs means inheriting BLAKE3
and its whole-file addressing. Flipping the causality — we choose the
identity — dissolves the "must have the full file for blake3" problem
into a design question (OQ-FS-04) and dissolves the "consume iroh-blobs
as a store" option (OQ-FS-03's option (a)) entirely: a sha1/sha256
identity layer over iroh-blobs' Command surface would be a mapping
shim on top of a foreign hash, not a store. What remains worth taking
from iroh-blobs is findings, not code: the inline/outboard hybrid
layout, the write-ordering analysis, the mark-sweep GC shape, and the
probe's actor-seam map (what a store API needs to expose to stay
consumable).
The other frictions (record, for the research they hold):
- The transfer protocol is welded to iroh/QUIC. iroh-blobs' network
story is an iroh
ProtocolHandlerspeaking its own ALPN over QUIC. For alkfs this is irrelevant-by-design — content transfer rides alkcall channels — but it means iroh-blobs is at most a local store dependency, never the transfer layer. (The store side itself is transport-independent — that is what the Command-actor probe proved.) - Whole-file naming vs incremental writes. BLAKE3/bao names a blob
by the root hash of its complete byte sequence; you cannot compute
the address until all bytes exist. Partial data can live under temp
tags (
BlobStatus::Partial) but cannot be pinned by its final hash. The POC's branch-on-write/merge-on-close is one reconciliation; chunk lists / Merkle-DAG naming is the other (OQ-FS-04). git escapes the problem structurally: objects are small and independently hashed; packs are the exception, and pack generation streams to a consumer that hashes on arrival. - Two-database coexistence (redb for blob metadata + SQLite for the path tree): two WALs, two fsync paths, two crash-recovery stories — the POC summary's unknown #1, reframed by the probe ("neither fork nor coexist: a third store actor with its own single SQLite file"), and dissolvable entirely if alkfs owns blob metadata in its own store (OQ-FS-02/03).
- Sealed internals (
TempTags, theBaoTreeSender) — the probe's fork-vs-PR calculus. Third-party upstream, not ours, never will be (AGENTS.md convention 17).
git + git-lfs — the shape to optimize for
The user-level framing this crate must honor: "the same basic shape exists with git + gitlfs as with how iroh's blobs handled the mix of small and large blobs." git itself is the small-object CAS: every object individually hashed (sha1 today, sha256 in the object-format transition), refs are names over hashes, content shared across refs/branches for free, rename is metadata-only. git-lfs is the large-blob sidecar: content- addressed pointer rows in the tree, bytes in a separate content store (rudolfs is the family's reference server for that half — namespace/ bucket isolation, LRU cache decorator, fanout streaming). The alkfs analogue: the metadata layer is git's object/ref shape; the blob layer is the LFS store; small content can inline into the metadata store exactly as iroh-blobs inlines small blobs into redb. With sha1/sha256 identity (the 2026-09-23 reframe), git objects are natively addressable in the blob layer — no oids→hash mapping layer, no dedup seam between git's CAS and the store's CAS.
One honest calibration: git dedups at object granularity, not chunk granularity. Packs delta-compress similar objects within a pack, but the address space is per-object. Chunk-level dedup (the working goal, §Working direction) is the storage-layer generalization that pays on workspace-scale files (datasets, logs, agent workspaces with large append-only artifacts) — not on git objects, which are effectively single-chunk. If alkblobs chunks large content, git's pack/idx regime still rides above it as object-granular rows; the two granularities compose (OQ-FS-04, OQ-FS-12). Whether alkgit keeps gix-odb native or serves its object storage from alkfs/alkblobs is OQ-FS-12 — the highest-stakes scope question in this document.
alkgit — the first consumer
/workspace/@alkdev/alkgit — paused on the storage problem. Its backend
doc (docs/architecture/backend.md) pins the seam: five traits
(GitRegistry, GitRegistryStore, GitRefs, GitPackGen,
GitPackIngest), kept small/orthogonal/substrate-blind after the
alknet-blobs sealed-surface lesson. The properties alkfs must serve
efficiently: streaming pack generation (O(counts) memory, blocking-thread
friendly), streaming pack ingestion (budgeted, fsck/connectivity report),
CAS ref transactions, registry records with grants. Its research
(docs/research/gitoxide.md, poc2-findings.md) carries the gix-odb
API contract notes — including the durability shape (pack bytes durable
before the ref transaction that names them commits) that mirrors
principle 3 exactly.
russh-sftp / alksftp — the door shape
/workspace/russh-sftp (read-only reference): the Handler trait is a
near-1:1 mirror of POSIX syscalls as SFTP packets, and the POC mapped
every op onto the PathTree + WriteSession API (open→resolve or
WriteSession::open; write→write_chunk with offset as key; close→hash+
merge+notify; readdir/list_dir; stat→indexed row — cheaper than a real
FS fstat; rename→O(1) edge move). The client's File already implements
AsyncRead + AsyncSeek + AsyncWrite with pipelined writes. alksftp (the
coming family crate) would speak this verb set; whether alkfs's serving
protocol adopts SFTP-shaped verbs natively or defines its own framing
that alksftp translates is OQ-FS-08. The alknet-tty async-IO adapter
patterns (AsyncWrite-over-mpsc, kill-on-drop) are reusable for the write
bridge.
SQLite (application file format) + honker
https://sqlite.org/appfileformat.html — the insight that anchored the
POC. honker (/workspace/honker, honker-core 0.2.4) supplies
notify/locks/queues as SQL functions on your connection — the
transactional-outbox property. Caveats: honker is third-party (not ours,
convention 17), single-machine by explicit design ("two servers writing
the same .db over NFS is not a Honker deployment strategy"), and its
scope in alkfs is an OQ (notify-on-commit may be the only load-bearing
piece — locks/queues/scheduler may be excess).
rudolfs — the git-lfs server reference
/workspace/@alkdev/alknet/docs/research/references/gitlfs/ rudolfs-reference.md: StorageKey = (Namespace, Oid) tenant isolation;
the decorator composition Verify ↔ Encrypted ↔ Cached ↔ Retrying(Disk → S3); LRU cache in front of permanent storage; fanout() streaming to
client and cache simultaneously. The cache/permanent split and the
fanout pattern are the production-shape references for alkfs's blob
layer and its serving read path.
Anti-prior-art (what NOT to carry over)
- The alknet mono-repo's scope. The filesystem research is inherited as findings, not as a code path; the crate stays small per the decomposition discipline (alksocks' "socks + channels, nothing else" resolution is the template — alkfs's version is OQ-FS-01).
- iroh-blobs' transfer protocol as the content path. Content transfer rides alkcall channels, full stop; an iroh/QUIC side-protocol would fork the family's transport story.
- iroh-blobs' FsStore partial-file lifecycle as a requirement. Its
749-line
bao_file.rspartial-blob-on-filesystem machinery exists because redb/filesystem is its substrate; a SQLite-inline store (POC write_chunks) or an alkfs-owned blob store has no need for it. - gix-odb's layout as alkfs's layout. Loose-objects + packfile directory layout is git's on-disk contract; alkfs is a general engine. If alkgit keeps gix-odb native, alkfs serves the general shape and git storage stays in alkgit (OQ-FS-12).
- NFS-style shared-file multi-node assumptions. honker's own warning applies to the whole engine: single-machine local state first; distribution is an explicit layer (OQ-FS-14), not an ambient property.
Open Questions
These are the design questions Phase 0 must resolve (or explicitly
defer) before the architecture spec. Numbered OQ-FS-01.. so they can be
referenced, tracked, and promoted into docs/architecture/ open-questions.md in Phase 1. Status is open unless stated; the point
of this document is to hold the reasoning without forcing premature
decisions. Grouped by theme; numbering is stable across reorganizations
(do not renumber — findings files and commits reference these ids).
Core architecture
OQ-FS-01: Crate scope — one crate or the alkblobs/alkfs split?
Working direction (2026-09-23): the two-crate split — alkblobs (the
bucket-style CAS blob store) + alkfs (the path-tree mapping + serving
protocol). Rationale: a git-like blob store is a complete, useful
thing without a filesystem (alkgit could consume it directly; S3-bucket-
shaped usage exists on its own), the FS layer is a separate concern with
its own consumers, and the POC's layer separation (blob bridge as an
explicit module) already drew the seam. Open sub-questions:
- One repo or two? The family has both shapes (alkcall single-crate;
alknet's
crates/workspace). Two crates in one repo (a workspace) keeps the seam cheap to iterate until first publish; two repos match the family's one-crate-per-repo posture but tax early co-development. Working lean: one repo, two crates, until the seam stabilizes. - The seam shape: does alkfs consume a concrete
alkblobs(the POC's blob_bridge pattern), or does a small blob trait live in alkfs (or a tinyalkblobs-typesleaf) so an embedder can substitute its own store? The sealed-surface lesson (alknet-blobs-external-store-probe.md) says decide before consumers exist; the alkgit trait-family precedent (docs/architecture/backend.mdthere) is the template for keeping it small. The put/get durability contract (principle 3) rides this seam regardless. - What is explicitly out: doors (alksftp, FUSE, mount), sync tooling, UI/registry applications, the alknet rewrite's client stack.
OQ-FS-02: Metadata store substrate — and is blob metadata in the same file?
The POC used rusqlite (bundled) for the path tree and it worked well
(schema-as-documentation, recursive CTEs, BLOB columns at chunk
granularity). Open sub-questions:
- SQLite/rusqlite vs redb vs something else for the path tree. The POC evidence favors SQLite (honker integration, transactional outbox, sub-ms resolves); redb buys a lighter footprint but loses SQL and the honker seam.
- The one-file-vs-two question: if alkfs owns the blob layer too, blob metadata (hash index, sizes, refcounts/liveness) can live in the same SQLite file as the path tree — one WAL, one commit boundary, and the durability ordering of principle 3 becomes a single transaction instead of a cross-store fsync dance. The probe's external-actor shape (iroh-blobs with its own store) vs an alkfs-owned store decides whether this is even on the table. Small content can then inline as BLOBs (the appfileformat sweet spot) — possibly making the "blob store" a layer inside one database with file-backed spill-over for large content (the iroh-blobs FsStore hybrid shape).
- Concurrency model: single writer connection + reader pool? WAL
settings (
synchronous=NORMALvsFULL) as a durability-vs-throughput knob? What is the honest fsync contract per operation class?
OQ-FS-03: Blob substrate — own the store (the direction) — what does alkblobs take from where?
Reframed 2026-09-23: option (a) "consume iroh-blobs as a store" is
dissolved — BLAKE3 is load-bearing across its entire surface (Hash
type, bao outboards, Command payload types), and the identity decision
has flipped to git-compatible sha1/sha256 (§Working direction). A
sha1/sha256 layer over iroh-blobs' Command surface would be a mapping
shim on a foreign hash, paying its dependency weight and its sealed-
surface risks to inherit a hash we don't want. The working direction
is option (b): own the store (alkblobs). What that leaves open:
- What to take as findings vs build fresh: iroh-blobs' inline/
outboard hybrid layout, write-ordering analysis, and mark-sweep GC
shape transfer as findings (§Prior art); the
bao_treecrate (BLAKE3-keyed) does not transfer for verified streaming — if per-chunk integrity is wanted under sha1/sha256 identity, the chunk index itself becomes the verification structure (each chunk row carries its own hash; reads re-verify per chunk), which is a simpler design than outboard files and falls out of the chunk manifest (OQ-FS-04) for free. - The appfile layout (OQ-FS-02's one-file question, now scoped to alkblobs): one transactional store for blob metadata + inline small content, file-backed spill-over for large content, single commit boundary for the durability contract.
- Bucket semantics: the S3/rudolfs shape — namespace/bucket as the isolation + naming prefix. How buckets relate to the alkfs layer (alkfs buckets = alkblobs buckets? a mapping table? one alkblobs store per alkfs bucket?) is an OQ-FS-11 sub-question; multi-tenancy at the blob layer is where-clause isolation per the POC.
- sha1 vs sha256 vs both: git's dual-object-format reality (sha1 repos today, sha256 transition) says the store should carry algorithm-tagged identities (a small enum per hash) rather than hard-coding either — git object ids are natively addressable then (OQ-FS-12's coexistence sub-question mostly dissolves into this).
- Versioned-hash hazard worth recording: sha1/sha256 are not length-extended-safe for streaming concatenation the way tree hashes are — any chunk-manifest scheme must be its own construction (hash list / Merkle over chunk hashes, e.g. the git tree-object shape itself), not a naive linear hash chain. This is a design constraint, not a blocker.
Write path, branching, GC
OQ-FS-04: Hashing and chunking — object-granular identity over a chunk manifest
Reframed 2026-09-23. The identity half is decided in direction: sha1/sha256, algorithm-tagged (OQ-FS-03) — git-compatible, native to alkgit. What remains open is the dedup granularity and the manifest shape — the load-bearing storage-format one-way door once content exists in the wild:
- Whole-file sha256 (the git shape). Objects are addressed whole; identical files dedup, similar-but-not-identical do not. Simplest, matches git objects exactly, and matches the POC's tested write path verbatim. For large workspace content the dedup miss is real (two 1GB files sharing a 500MB prefix dedup zero bytes).
- Chunk manifest (the working goal for large content). A file is a root manifest over chunk hashes (a hash list — the git tree-object shape generalized, per OQ-FS-03's versioned-hash constraint); chunks dedup across files, partial availability and resumable writes come along, and per-chunk re-verification on read falls out of the manifest (each row carries its own hash — the bao-outboard role, without bao). Chunker choice (CDC vs fixed-size) and boundary-shift sensitivity are the design cost.
- Hybrid (the probable landing): whole-file identity under a size threshold (git objects, source files — effectively single-chunk anyway); chunk manifests for large content (datasets, logs, pack- scale blobs). The threshold is a knob; the manifest format for the chunked side is the one-way door.
Sub-questions: CDC (rollsum/buzhash) vs fixed-size vs git-style delta-encoding as adjacent dedup (packs delta-compress similar objects — a chunk-manifest store could optionally delta-compress chunk rows, but that is a compression layer, not identity); where chunk indices live (alkblobs manifest tables); whether cross-bucket chunk dedup is ever allowed (working answer: never — tenants are isolated, OQ-FS-11); interaction with OQ-FS-05 (a chunk-manifest write can commit incrementally, weakening the branch-on-write staging argument for chunked files — the session model may collapse to "extend the manifest" for large content).
OQ-FS-05: Write path semantics beyond the POC
Branch-on-write/merge-on-close is POC-proven for the happy path. What Phase 0 must still answer:
- Durability details — per-chunk transaction commit (POC shape) vs
batched WAL;
synchronouslevel; whether fsync of spilled large content must precede the path-row commit (principle 3) and how that is ordered when content lives outside the DB file; crash-mid-close behavior. - Concurrency — the POC left honker named locks unwired; concurrent writers to the same path need a real policy (last-close-wins is session-safe but not POSIX; O_EXCL-ish create semantics; advisory byte-range locks for the sftp door?).
- Streaming large writes — pack generation and 1GB+ uploads: the POC's per-chunk SQLite rows are proven at ~32KB×1MB; the pack-size regime needs a spill story (OQ-FS-02's file-backed overflow) and a backpressure story inherited from alkcall channels.
- Sparse/out-of-order/ftruncate — SFTP pipelines out-of-order (proven); sparse files and explicit truncation/extend are unaddressed.
OQ-FS-06: Branch/snapshot/commit model
The POC has Fossil-style branches (name → parent chain + deltas) but no explicit commit/snapshot op — writes to a branch are immediately visible on that branch. Open questions:
- Is a snapshot a new branch (Fossil's model — maps naturally to the chain walk) or a recorded point-in-time within a branch (git's model)? What do alkgit (refs + CAS) and the sftp door each need?
- Refs as a first-class concept: alkfs stores
refsrows naming commits (branch heads), enabling CAS ref transactions (alkgit'sGitRefs::applyshape) over the same metadata store. Is the ref namespace git-shaped from day one, or a later addition? - Merge semantics across branches: the POC has none (branch-on-write collapses trivially at close). Real merges (three-way? automerge-CRDT for the metadata layer per POC 3?) are presumably out of v1 — say so explicitly rather than leaving it implied.
- Chain-depth performance: the recursive CTE degrades on deep chains (POC unknown #6); resolve-cache/materialized-view shape if needed — perf probe before spec, or accept and document.
OQ-FS-07: GC and liveness
Content is shared across branches/paths by hash, so naive per-path tags (Poc unknown #5's iroh-tags shape) break sharing — deleting a path must not collect content another path/branch still names. The candidate shape: liveness = reachability walk (mark from all live branch heads
- refs + write sessions; sweep unreachable) — a metadata-store query over path rows, no separate tag table needed if blob liveness rows are refcounted or mark-computed at GC time. Sub-questions: incremental vs stop-the-world GC; tombstone retention (content behind tombstones is unreachable-but-recent — grace windows); orphaned write-session chunk reaping (crash cleanup, the POC's known orphans); cross-bucket sharing (never — buckets are tenants).
Serving protocol
OQ-FS-08: The file protocol over channels — verbs, sessions, framing
The biggest unspecced surface, and the crate's first real wire format (one-way door — ADR before the first consumer):
- Verb set. SFTP-shaped (the russh-sftp Handler mapping is proven
1:1 against the engine) vs a leaner custom set (open/read/write/
close/stat/readdir/rename/remove + extended ops). Working posture:
SFTP-shaped verbs, SFTP-versioned semantics where it matters
(handles, pipelined writes,
SeekFrom::Endcheapness). - Session model. One channel per file session (alksocks' one-channel- per-CONNECT shape) vs one control channel multiplexing many file handles (the alktty demux shape). A directory traversal or a git clone opens many files; per-file channels spend channel IDs and opens (the 256-cap arithmetic, OQ-SK-09's twin); a multiplexed control channel reintroduces the demux trade alksocks resolved for SOCKS5 (whose RFC structure genuinely has one command per connection — a file protocol has no such constraint). This trade has different physics here than it had there; no inherited answer.
- Framing on the data path. If sessions are multiplexed, requests need IDs and payloads need framing (length-prefix + request-id — the one-way door). If per-session, the payload may be near-pass-through. Chunk sizes, backpressure (bounded buffers are inherited), and flow-control for pipelined writes all live here.
- Read-path streaming. Range reads (offset+len), EOF semantics, verified-streaming vs trust-the-store; zero-copy ambitions explicitly out (channels are the substrate, not splice(2)).
- Establishment.
register_openable_with_establisherwith a scope gate (OQ-FS-10); refused opens are typed errors with per-verb error mapping (SFTP status codes at the door, faithful engine errors in protocol — convention 2).
OQ-FS-09: ALPN, params, and the resource model
Provisional alk/fs (final naming per alkcall ADR-004/006 convention).
Open sub-questions (mostly Phase 1 ADRs with Phase 0 ground):
- The params-object shape (ADR-039 precedent: ALPN-specific, interpreted by the open handler) — what identifies a produced filesystem resource? (bucket? branch? scope string? limits?)
- In-process and loopback consumers: the engine half must be directly
embeddable (no channels) — the protocol layer is one substrate among
several, generic over
T(principle 6). The in-process shape is also the test seam, the alksocks POC pattern. - Hub relay story: a filesystem resource traverses relays terminate-and-re-produce like any produced resource; nothing alkfs-specific expected — verify during POC rather than assume.
OQ-FS-10: Doors — alksftp, mounts, sync (out of crate, but shapes the API)
Family rule: doors are separate crates/features. Phase 0's job is to
keep the protocol layer door-ready: the verb set (OQ-FS-08) must map
onto russh-sftp's Handler without translation loss; a FUSE/mount door
needs POSIX-ish error mapping and inode-ish handle semantics — do not
design for FUSE in v1, but record what it would demand so the protocol
doesn't preclude it. Directory-sync tooling (alkfs ↔ host dir) is the
explicit, feature-gated host-FS bridge if/when wanted — never ambient.
OQ-FS-11: Multi-tenancy, identity, and path-scope policy
Buckets are the isolation unit (POC-proven free). Open questions: the
ALKFS_OPEN_SCOPE-shaped scope gate (alktty/alksocks precedent) — does
scope name buckets, path prefixes, or both; identity → bucket mapping
at the assembly layer (vault references, never plaintext secrets —
family rule); per-path/per-bucket policy as a producer-side policy
callback vs declarative ACL (alksocks OQ-SK-05's dialer-refuses
resolution is the evidence-backed template); write-vs-read grant
separation (a read-only bucket mount is the obvious first split).
Consumer fit
OQ-FS-12: alkgit fit — the question that un-pauses alkgit
The first consumer and the reason this crate moved ahead of alkgit. The decision tree:
- (a) alkgit keeps gix-odb native; alkfs serves the general FS. Lowest integration risk; but then alkfs doesn't solve the problem that paused alkgit, and the family carries two storage stories.
- (b) alkgit's backend traits get an alkfs-backed implementation.
GitPackGenstreams a pack out of alkfs content (pack assembly is alkgit-side; the blob bytes come from alkfs reads);GitPackIngeststreams a pack into alkfs (each unpacked object lands as content; refs land as metadata rows);GitRefsbecomes CAS transactions over alkfs refs. gix-odb's on-disk layout disappears; git semantics ride alkfs's content addressing. This is the "optimize for git" path and the probable answer — but it makes alkgit's workload (pack-scale blobs, O(counts) memory, streaming) the benchmark OQ-FS-13 must meet. - (c) gix-odb on top of alkfs (an odb backend where loose objects/packs are alkfs files) — keeps gix-odb's pack machinery but treats alkfs as the disk. Probably the worst of both: gix-odb assumes filesystem semantics alkfs won't natively provide (mmap, file locks).
Sub-questions: does git's object-id namespace (sha1/sha256) coexist with alkfs's content addresses — mostly dissolved by the identity reframe (2026-09-23): git oids are the blob layer's addresses now; what remains is whether git objects ride as whole-file identities (the natural fit — git objects are small) while alkfs path rows may point at chunk-manifest roots for large content (OQ-FS-04's hybrid), and how pack files relate (a pack is just a large blob to alkblobs; unpacked object rows are the useful representation — the ingestion mapping is the question). CAS ref transaction shape; whether pack ingestion budget enforcement (alkgit ADR-009) maps onto alkfs write sessions.
OQ-FS-13: Performance shape and budgets
No numbers exist yet. What Phase 0 should collect (empirically, via the POC register) so Phase 1 can pin budgets: path-resolve cost at realistic branch depths (POC: sub-ms in-memory; chains of 10+?); small-file throughput (the inline-BLOB sweet spot claim vs reality at 4KB metadata ops); large-file write throughput through chunked sessions; pack gen/ingest streaming rates over a channel vs native gix-odb; memory bounds under the alkcall channel caps. The alkgit ADR-009 limits shape is the consumer contract to serve.
Posture and deferred
OQ-FS-14: Distributed sync / multi-node — in scope at all?
POC 3 proved automerge-CRDT sync of the path tree (9 tests, LWW conflicts, the concurrent-root-init constraint) with content fetched separately. But sync is a product-layer concern that may not belong in the engine crate: the family pattern is composition (alkcall relay for transport, automerge as a third-party sync layer, honker queues as the local outbox). Working posture: deferred — out of v1 scope, recorded so the metadata schema doesn't preclude it (branch/paths rows are already CRDT-encodable per POC 3's document structure). Revisit when a consumer needs multi-node.
OQ-FS-15: WASM posture
The expected shape (convention 4): wasm-clean protocol/ops layer; native
storage engine behind features (real FS/SQLite never compiles to
wasm32-unknown-unknown). Open: does any consumer actually want the
protocol layer in wasm (a sandboxed alkfs consumer, mirroring alksocks'
sandboxed-SOCKS-consumer story)? If none is in sight, the honest answer
may be alkhttp-style native-only (the "wasm-clean is preferred, not
mandatory" precedent) — but the protocol layer being wasm-clean by
construction costs little if the engine split exists anyway. Decide
deliberately once the split (OQ-FS-01) exists; verify with
cargo check --target wasm32-unknown-unknown.
OQ-FS-16: The remote-mount workload — the anchor use case
Added 2026-09-23. The concrete deployment this crate exists for (the
user's own daily workflow): a dev-server workspace mounted on a local
Linux machine under remote/ — today via sshfs/vanilla SFTP, with
known slowness:
- Why vanilla SFTP is slow (the honest decomposition): a network
round trip per syscall (stat storms during directory walks; sshfs
even translates
SeekFrom::Endinto a realfstatround trip); attribute-invalidation chatter; no server-side index (the SFTP door shells to real FS syscalls, each paying host-FS costs); attr-cache and cache-timeout knobs that trade staleness for speed. The POC's mapping table already notedstat→ single indexed lookup is cheaper than a real FS fstat — an appfile-indexed VFS fixes the server-side half structurally. - What the alkfs shape adds: server-side indexed metadata (stat/
readdir from index rows, no syscall per attribute); pipelined writes
and batched reads native to the protocol (the russh-sftp client's
write_nowait+ ack window rides the chunked write session); optional client-side caching/prefetch as door-local policy (a mount door can cache content chunks locally — content-addressed means cache coherency is a branch-head-invalidation problem, not a byte-consistency problem); directory-listing snapshots. - What this anchors: the verb set (OQ-FS-08) must make the readdir/stat/read/write path fast, not merely correct; the session model (OQ-FS-08's per-file-channel vs multiplexed question) matters most for directory-walk fan-out; OQ-FS-13's budget table should measure against sshfs-over-localhost baselines.
- alksftp vs a mount protocol: vanilla SFTP clients (sshfs) work against any SFTP door with zero client install — that compatibility is worth keeping as the floor. A leaner alkfs-native mount protocol (OQ-FS-08's framing question) is the speed tier; whether it warrants its own FUSE-facing door early or later is a sequencing question deliberately parked until the protocol shape exists.
This OQ is the consumer pull behind the whole crate — if the mount use case stops mattering, OQ-FS-08's urgency drops with it.
Candidate POC register (proposals — none run yet)
POC placement conventions (inherited from alktunnels/alksocks): a POC
needing this repo's code runs in a worktree (.worktrees/research/ <task-id>/ per docs/sdd_process.md); 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/.
| # | Candidate | Hypothesis to validate | Feeds |
|---|---|---|---|
| 1 | Engine skeleton: path tree + inline-blob store in one SQLite file | One-file metadata+blob-metadata (OQ-FS-02) holds up under the POC's 15-test suite ported over; single-transaction durability ordering (principle 3) is expressible | OQ-FS-02, OQ-FS-05 |
| 2 | Write path at pack scale | Chunked write sessions handle 100MB+ streams (spill-to-file regime) with correct durability ordering and crash-abort semantics | OQ-FS-05, OQ-FS-13 |
| 3 | alkblobs store skeleton (sha1/sha256 identity) | Own-store direction (OQ-FS-03): algorithm-tagged hash ids, bucket isolation, inline-vs-spill layout, manifest-of-chunks for large content — measure against the probe's store-seam map as the API checklist | OQ-FS-03, OQ-FS-04 |
| 4 | Chunking probe (paper + micro-bench) | Whole-file vs chunk manifest: dedup gains on realistic workloads (agent workspaces, datasets, logs — git objects are single-chunk and ride whole-file), write-path impact, metadata size — enough to decide the format one-way door or at least stage it | OQ-FS-04 |
| 5 | Serving protocol spike | SFTP-verb protocol over channels: per-file-channel vs multiplexed-handles session model, open cost at directory-walk and git-clone fan-out, framing sketch (not a wire ADR) | OQ-FS-08, OQ-FS-09 |
| 6 | alkgit seam spike | GitPackGen/GitPackIngest shaped against an alkblobs-backed engine — does the trait family survive contact; git oids as native addresses |
OQ-FS-12 |
| 7 | GC walk prototype | Reachability-mark GC over path rows + refs: correctness on shared content across branches, cost model, orphaned-session reaping | OQ-FS-07 |
| 8 | Mount-workload baseline | Measure the current pain (sshfs against this dev server): op latencies for stat/readdir/read/write at realistic workspace scale — the baseline OQ-FS-16 improvements must beat | OQ-FS-16, OQ-FS-13 |
Order is indicative, not committed: #1/#3 de-risk the engine cores (alkfs metadata, alkblobs store); #2 stress-tests the write path; #4 resolves the storage-format one-way door; #5/#6 are only worth running once #1/#3 exist (the protocol needs an engine to serve); #8 can run anytime (it measures today's sshfs, no alkfs code needed) and its baseline ages well if taken early. The register exists so POC work has home ids; expect entries to be split, merged, or dropped as OQs sharpen.
Survey / prior-art list
Read/verified (cited above):
- alknet-filesystem POCs —
/workspace/@alkdev/alknet/docs/research/alknet-filesystem/ poc-summary.md,alknet-blobs-external-store-probe.md; POC crates/workspace/alknet-filesystem-poc,/workspace/alknet-fs-sync-poc. - iroh-blobs —
/workspace/iroh-blobs(v0.100 checkout +DESIGN.md), published 0.103 in cargo cache (probe source); the family's reference notes at/workspace/@alkdev/alknet/docs/research/references/iroh/ iroh-blobs/(overview, key types, transfer protocol, storage). - alkgit —
docs/architecture/backend.md(the trait seam),docs/research/{vision,gitoxide,git-protocol,poc2-findings}.md. - russh-sftp —
/workspace/russh-sftp(server/handler.rs, clientFilepipelining). - SQLite appfileformat — https://sqlite.org/appfileformat.html; honker
—
/workspace/honker(honker-core0.2.4). - rudolfs (git-lfs server) —
/workspace/@alkdev/alknet/docs/research/references/gitlfs/ rudolfs-reference.md. - alkcall — ADRs 034/035/036/037/039/042/049/050/051, ledger
CF-005/CF-006 (
docs/architecture/decisions/); alktty/alktunnels/ alksocks Phase 0 docs as the format/template precedents.
To evaluate (research-specialist queue, feeding the OQs named):
- Chunking/CDC prior art — FastCDC, rollsum (bup), rsync-style
boundaries; git tree-object hashing as the manifest shape (the
versioned-hash constraint's precedent, OQ-FS-03/04);
bao_tree's chunker as a chunk-boundary algorithm candidate (decoupled from its BLAKE3 identity). - CAS filesystems — Plan 9 fossil+venti (the branch/CAS ancestry the POC name-checks), IPFS unixfs/iros DAG shapes, OST/lessfs-style inline-vs-outboard hybrids (OQ-FS-04, OQ-FS-06).
- git storage internals — gix-odb pack/idx layout and
gix-pack::data::inputstreaming (alkgit research has the survey; re-read for OQ-FS-12's object/pack mapping questions). - SQLite durability posture — WAL +
synchronous=NORMALvsFULLunder the commit-boundary contract; SQLite-as-blob-store limits (OQ-FS-02, OQ-FS-05). - FUSE/mount doors — rust bindings shape, POSIX semantics alkfs would need to fake; sshfs latency characteristics (the OQ-FS-16 baseline's "why it's slow" decomposition, verified against real measurements from POC #8 rather than folklore) (OQ-FS-10, OQ-FS-16; defer design, record constraints).
Convergence checklist (what Phase 0 must produce)
- Vision + guiding principles captured (this doc, §Vision — the scope sketch needs confirmation or cutting, OQ-FS-01)
- Prior-art pass complete: alknet-filesystem POCs re-read and mapped (done); iroh-blobs surface re-verified against the probe's 0.103 findings; git/git-lfs shape pinned against alkgit's backend contract; CDC/chunking survey (OQ-FS-04 input); CAS-filesystem survey
- OQ-FS-01 (crate scope) — converged: the alkblobs/alkfs split confirmed or cut; repo shape (workspace vs two repos); trait seam at the blob boundary or concrete dependency; explicit non-goals
- OQ-FS-02 (metadata store) — converged: substrate, one-file-vs-two (blob metadata placement, now scoped to alkblobs), durability knob
- OQ-FS-03 (blob substrate) — converged: the own-store direction pinned down (what transfers as findings, what's built fresh) (POC #3)
- OQ-FS-04 (hashing/chunking) — converged: identity settled in direction (sha1/sha256); dedup granularity + manifest format are the one-way door (POC #4)
- OQ-FS-05 (write path) — durability/concurrency/spill semantics pinned (POC #2)
- OQ-FS-06 (branch/snapshot/refs) — model chosen; ref namespace decided; merge explicitly in-or-out of v1
- OQ-FS-07 (GC) — liveness shape chosen; retention/reaping policy pinned (POC #7)
- OQ-FS-08 (file protocol) — verbs, session model, framing sketch; the wire ADR itself is Phase 1's (one-way door, first consumer in sight) (POC #5)
- OQ-FS-09 (ALPN/params) — naming + params ground collected; ADR in Phase 1
- OQ-FS-10 (doors) — protocol kept door-ready; nothing designed in
- OQ-FS-11 (tenancy/policy) — scope-string + policy shape settled; bucket semantics across the alkblobs/alkfs seam
- OQ-FS-12 (alkgit fit) — the (a)/(b)/(c) decision made; alkgit un-pause plan written (POC #6)
- OQ-FS-13 (budgets) — numbers collected (incl. the sshfs baseline, POC #8); budget table drafted
- OQ-FS-14 (sync) — explicitly deferred or scoped with evidence
- OQ-FS-15 (wasm) — decided once the engine/protocol split exists
- OQ-FS-16 (remote mount) — the anchor workload's requirements written down (verb-set implications, caching shape); sequencing (SFTP floor first vs native protocol tier) decided
- Targeted POCs run + findings in
docs/research/ - Converge: recommended approach written up, ready to hand to the Architect for Phase 1