offshoot roadmap
This is the working roadmap from here to a public launch and a 1.0-worthy tool. It came out of a deliberate gap analysis from two chairs — a staff AI engineer deciding whether to adopt offshoot for an agent platform / eval harness, and an OSS operator auditing what a launch needs. Items are grouped by milestone, each with the user story it unblocks. Non-goals are at the bottom and are as binding as the goals.
Ground rules carried over from the design spec: correctness stays paranoid (CAS everywhere, fail-closed probes, loud failures), honesty is a feature (limits documented as plainly as features), and the storage format carries a layout version so incompatibility is always detected, never guessed.
Releases use the 0.x prerelease series (tags v0.1.0 … v0.2.9); 1.0 is
reserved for the storage-format freeze. 0.2.0 is the copy-on-write release —
a minor (not patch) bump because it changes the storage format
(LayoutVersion 1 → 2; see the copy-on-write milestone below). The 0.2.x
patch line since then is hardening on top of that arc — see the follow-ups
bullet in that milestone and CHANGELOG.md.
Milestone 1 — Installable and trustworthy
Bar: a stranger can install offshoot without a Go toolchain, read the repo without tripping over internal artifacts, and watch CI prove the claims.
- Fix the module-path/org mismatch. Done —
go.modused to declare a module path under the aspirationaloffshoot-dbGitHub org while the repo actually lived atgithub.com/sricola/offshoot; rather than transfer the repo into an org that doesn't exist, the module path and every import/doc/workflow reference were retargeted togithub.com/sricola/offshootto match where the code already lives.go install github.com/sricola/offshoot/cmd/offshoot@latestresolves. PyPI/npm package names are unaffected by this — see "Claim the names" below. - Claim the names.
offshoot-dbon PyPI, the@offshoot-dbnpm scope, brew formula name; collision + trademark check on "offshoot" in dev tools; buy a domain. - Code CI. Linux + macOS matrix (cgo needs real macOS runners):
go test ./... -race,go vet, plus a MinIO service container running the S3 conformance suite on every PR with no secrets. Nightly on main: real AWS conformance + the full kill-9 torture suite, feeding the provider-support table. Badges in the README. - Release engineering. goreleaser with per-OS native build runners (cgo
rules out naive cross-compilation; zig-cc is the fallback), darwin/linux ×
amd64/arm64,
offshoot versionwith embedded version/commit, Homebrew tap, curl-sh installer, Docker image, THIRD_PARTY_LICENSES/NOTICE bundled with binaries. - Community floor. CONTRIBUTING (dev setup, the four test tiers and what
they cost), SECURITY.md with private vulnerability reporting, Contributor
Covenant, DCO (no CLA), issue templates that ask for store type and
offshoot statusoutput, Discussions on. - Docs scrub. Rewrite the README status line in user terms (no internal plan numbers), edit the design spec into a public architecture doc, publish an honest implemented/deferred matrix so adopters can tell which spec guarantees are real today, add a CLI reference covering every command, and write the "why not Litestream / LiteFS / Turso / Dolt / cp" FAQ. Verify and record capture-engine provenance (clean-room vs adapted) so attribution is airtight.
- Site hygiene. The live site must not link to a private repo; teaser mode until the repo flips public.
Milestone 2 — Safe by default for agents
Bar: an unattended agent writing through offshoot cannot silently lose hours of work, leak branches forever, or take the slow path by default.
- Background flush interval. Today durability advances only on explicit
flush; capture is continuous but nothing ships to the store on its own. A daemon that dies four hours after the last flush loses four hours. Addserve -flush-every(per-session override), defaulting on. Delivered as one daemon-wide cadence, not a per-session override — see docs/status.md's "Per-sessionFlushEveryoverride" row for that gap, deferred as YAGNI. Both follow-ups originally noted here have since shipped: every session's mandatory first "settling" flush now skips the upload entirely when the checkoutOpenreceived was already proven unchanged since the branch's current head, and a cleanSession.Closenow refreshes the checkout's.sumsidecar so reopen-after-settling stays flat too — see status.md's "Settling-flush checksum-compare suppression" and "Sidecar refresh on clean Close" rows for exactly what's covered and what still isn't (a dirty/stale checkout, or a session that ever took a mid-session rebase-on-divergence, still pays the old cost once). - MCP forks get TTLs. The MCP
forktool currently cannot set a TTL, so every agent-initiated fork is immortal — the exact orphan-leak class the design calls launch-killing. Add the tool argument plus a server-side default TTL for MCP-created branches. - MCP rides the daemon. (Amended to reflect what this milestone
actually delivers.) MCP's
checkpoint/fork/checkouttools ride an existing daemon session — one already opened by a harness (the SDKs,offshoot session open, or a custom loop) — instead of running at rest, whenever a daemon is up and has a session open on the branch in question;rollback/promote(its target)/destroyrefuse rather than repoint or delete a branch out from under such a session. No MCP tool opens or closes a session itself — that scope (anoffshoot_open/offshoot_closepair or similar) is explicitly deferred, not delivered here, and moves to Milestone 3: an MCP-opened session would have no natural owner responsible for closing it, which would recreate exactly the leak class (leases and branches nobody ever releases) this milestone's TTL and background-flush work exists to kill. (Milestone 3 update: the pytest fixture plugin and its vitest testkit counterpart both shipped and are exactly the always-present lifecycle owner this note anticipated — but for their own harness workload, not as an MCP tool pair. The MCPopen/closegap named here is still open by the same reasoning; see docs/status.md's "MCP session open/close" row for the confirmed-still-deferred status.) MCP-first stacks are the default enterprise path; the existing-session path (a harness opens the session, MCP rides it — see docs/recipes/claude-agent-sdk.md for the concrete wiring) is the good path they get today. - Fork performance, measured then fixed. Fork is currently a local byte copy plus a synchronous fork-point upload — O(size) twice, unmeasured at any size. Ship a benchmark suite (100MB / 1GB / 10GB, local + MinIO), publish the numbers, then implement reflink/clonefile fork with copy fallback and async fork-point upload. Seed-once-fork-many is the headline workload; it must be fast and provably so.
- 3am observability, first half.
statusgains durable-through age, last-flush time, and capture lag; structured logs on every branch state transition with cause. (Full metrics endpoint lands in v0.4.) - Resource behavior documented. No budgets yet (v0.4), but the current per-session disk/FD costs and failure modes go in the docs now.
Milestone 3 — The eval-harness release
Bar: the target persona's first hour is paved end to end: install, seed, fork-per-test, inspect, export, clean up — from their language.
Status: mostly shipped, with two named deferrals (⏸ below). Everything
marked ✅ landed on the eval-harness branch; see
docs/status.md's Integration Surface section for the
shipped-and-tested rows and docs/eval-harness.md
for the tutorial. Two items were consciously pushed out with a stated
reason rather than silently dropped, not shipped and not pretended
otherwise — create --from's daemon/SDK/MCP reach, and the actual
PyPI/npm/registry button-presses — see their own ⏸ bullets below.
-
✅
offshoot.pytestfixture plugin + vitest helper + the serious tutorial: session-scoped daemon, seed fixture, fork-per-test with TTL, worker-parallel branch naming, teardown. Shipped asoffshoot.pytest_plugin(offshoot-db[pytest]) andsdk/typescript/src/testkit.ts; the tutorial is docs/eval-harness.md. This is also where MCP-initiated session open/close was slated to belong, deferred from Milestone 2's "MCP rides the daemon" (see that bullet) — the fixture and testkit are now real, always-present lifecycle owners for the harness workload they were built for, but no MCPopen/closetool pair was built on top of them; that reach stays deferred for the same no-natural-owner reason — see docs/status.md's "MCP session open/close" row. -
✅ Publish the SDKs. PyPI + npm with trusted publishing/provenance; SDK docs rewritten to installed-package form; manifests filled out (urls, classifiers, files whitelist). Publish pipeline (
.github/workflows/publish.yml) is prepared and gated (PUBLISH_ENABLED, default off) — the pipeline itself is done; see the Listings bullet below for what's still user-gated. -
✅ Export. New
offshoot export <db>@<branch>[@checkpoint] out.dbshipped for plain-file egress (backups, handoff) without fork-checkout-copy-destroy, daemon op and SDK parity included — this half of "Import/export everywhere" is fully shipped. -
⏸
create --fromreach (daemon protocol, SDKs, MCP) — deferred, not shipped. The other half of "Import/export everywhere" did not land this milestone. This is a pre-written deferral, not a gap found late: importing an existing file through the daemon needs either an upload channel (stream bytes over the unix socket — a new wire primitive) or a same-host path-trust story likeexport's own (the daemon reads/writes a path the caller names, trusted under the existing same-host/same-user socket trust model) — that design deserves its own pass. CLIoffshoot create --fromremains the only import path; see docs/status.md'screate --fromreach row. -
✅ Read-only and historical checkouts. Materialize a checkpoint for inspection without forking; sanctioned read-only sessions alongside a live writer. Shipped as
ops.Workspace.CheckoutAt/offshoot checkout --at --read-only/ daemoncheckout-atop / SDKcheckout_at(). -
✅ List databases in the protocol and SDKs (cleanup jobs shouldn't shell out to the CLI). Shipped as the daemon
dbsop /offshoot session dbs/ SDKdbs(). -
✅ Checkpoint/branch metadata. Timestamps and txids in
branchesoutput; a small user-metadata map on fork/checkpoint (eval run id, git SHA, agent id); branch-level lineage is the right grain — no row-level provenance. Shipped asRef.Meta/Checkpoint.Meta,--meta k=v,checkpoints_v2/touched_at. MCP tool metadata exposure (offshoot_fork/offshoot_checkpointtaking a caller-suppliedmeta) stayed out of scope for this task — see docs/status.md. -
✅ Branch diff.
offshoot diff a@x b@ywrapping sqldiff over two materializations; the daily "attempt-2 passed, attempt-3 failed, what changed?" loop. Shipped, CLI-only per the task's own scope (no daemon op, no SDK parity) — see docs/diff.md. -
✅ Framework recipes, not adapters. The ThreadForks pattern (thread → branch, checkpoint-id → checkpoint) documented once and applied as short recipes: Claude agent SDK hooks, OpenAI Agents SDK session store, LlamaIndex/CrewAI notes. LangGraph keeps the real companion (
offshoot.langgraph.ThreadForks); everyone else gets a page, not a package. -
⏸ Listings — prepared, submission deliberately deferred (user-gated). MCP registry
server.jsonmanifest authored in-repo but not submitted — the exact registry schema needs to be fetched and validated against at submission time, not assumed from this repo's own docs. LangGraph community-integration PR text draftedbut not submitted — its install command needs real PyPI publication to be true. Both are blocked on the same user action as actual SDK publication: claiming the
offshoot-dbPyPI name and@offshoot-dbnpm scope (see the Launch track's Milestone 1 note and docs/status.md's publish-pipeline row) — everything up to that button-press shipped this milestone.
Milestone 4 — Operable at scale
Bar: a platform running hundreds of agent sessions can see, bound, and automate offshoot.
Status: shipped, with one bullet delivered narrower than originally scoped and named as such below rather than silently dropped. See docs/status.md's Observability/Resource-behavior sections for the shipped-and-tested rows, docs/operations.md for the operator-facing reference this milestone's Task 8 wrote, and docs/status.md for what's left that no further engineering resolves.
- ✅ Prometheus
/metrics: capture lag, durable-through age per branch (open sessions only, by design — see docs/operations.md), GC backlog, checkout cache usage, fork/checkpoint latencies. Sixteenoffshoot_*metric families total, names locked as API as of the [0.1.3] tag (one free rename window, now closed) — see docs/operations.md's full reference table. - ✅ HTTP binding + single-token auth (loopback by default, explicit
opt-in beyond), unlocking sidecars and remote dev; container/k8s recipe
docs. Shipped as
serve -http ADDR(internal/daemon/http.go) plus docs/recipes/kubernetes.md's sidecar manifest — no container image is published yet (see the Standing-nag section linked above), so the recipe builds its own. - ✅ Branch state taxonomy (
active / detached / dirty / error / pending, plus anidlesixth state the design spec's taxonomy didn't anticipate — see docs/operations.md) implemented and reported, not just spec'd. - ✅ Eventing. A
subscribeop (SSE once HTTP exists) for flush / lease-loss / fence / reap events, replacing supervisor polling. Shipped with acheckouts-roeviction event type too, feeding directly off the budget bullet below. - ⚠️ Resource budgets — delivered narrower than scoped. Checkout-cache
(
checkouts-ro) disk budget with LRU eviction shipped (serve -ro-cache-budget); writable leased checkouts are never evicted, structurally. The FD budget with LRU eviction of cold read-only materializations did not ship this milestone — consciously narrowed at Task 8 dispatch time, not discovered as a gap late:internal/dbfile's file descriptors are deliberately unclosable by that package's own design (a stray-close lock hazard the design deliberately avoids), which makes "evict a cold session's FD" a real design problem needing its own pass, not a variant of the ro-cache budget's shape. See docs/status.md's FD-budget row. Follow-up, not yet scoped:destroy/GC never clean up a branch'scheckouts-roentries — they linger until LRU eviction claims them, which never happens at the default-ro-cache-budget 0(unlimited). Safe today (every entry is an immutable checkpoint snapshot, andrm -rf checkouts-roremains sound at any time), but worth a real cleanup path now that Task 5 turned what used to be a manualrm -rfgrace into an institutionalized cache. - ✅ Tuning surface.
SnapshotEveryexposed through the daemon (serve -snapshot-every N); documented guidance in docs/operations.md including the flush-cost/replay-latency trade-off. - ✅ Follow-ups from review: CAS-conditional ref delete (closes
Destroy's read-then-delete window generally — local gets a true
conditional delete, S3 formalizes the claim-marker pattern since
DeleteObjecthas no real preconditions); typed TS response shapes shipped ahead of any 1.0 SDK claim.
Copy-on-write storage — the storage-amplification arc
Bar: N forks of a G-byte database stop costing N×G in the bucket, without giving up bounded reads, GC safety, or destroy-anytime.
Status: shipped, as the [0.2.0] release (see CHANGELOG.md). This closes the "storage amplification" risk the v1 design carried as its open wart (its own spec said "N materialized forks cost up to N×G").
- ✅ Storage amplification, killed for the fork workload. A fork now
shares its parent's durable objects through a base pointer and writes
new objects only as it diverges — N forks of a G-byte database cost
near-zero added bytes (N×G → shared). Bounded reads survive via two
automatic snapshot floors; GC is rewritten as object-granular
reachability over the transitive base closure;
offshoot compactis the manual cord-cutter; the first shared fork bumps the store to LayoutVersion 2 and locks pre-0.2.0 binaries out of the store (deliberately — their lineage-granular GC would sweep shared objects). - ✅ Honesty preserved:
status/branchesreport each branch's cost class (storage=sharedvsstorage=materialized); the fork-shares vs promote/rollback/compact-materialize asymmetry and the destroy-lingers-until-last-child reclaim semantics are documented rather than hidden. - ✅ Post-0.2.0 follow-ups shipped across the 0.2.x patch line (see
CHANGELOG.md for each): the fork-time snapshot floor
tracks the daemon's configured
-snapshot-everycadence, plusoffshoot_fork_mode_total/offshoot_gc_errors_totalobservability (0.2.1); a store-RPC/memory perf pass — streaming chain materialization and snapshot-flush upload, batchedDeleteObjectsGC sweeps, one less List/resolution per fork and GC pass (0.2.2); S3 multipart uploads, so5 GiB snapshot flushes work (0.2.3); concurrent part uploads, multipart server-side copy up to S3's 5 TiB ceiling, and an epoch-aware GC compensating rule closing a bounded space leak (0.2.4); a fencing-vs-resolution dedup fix for a silent-data-loss race (0.2.5); and bounded waits on every S3 call — per-RPC deadlines plus a
GetReaderprogress watchdog — so a stalled backend can't wedge flush or close (0.2.6/0.2.7). - ⏸ Page-level / content-addressed cross-database dedupe — still deferred, explicitly out of scope for this arc. Copy-on-write shares whole objects between a fork and its ancestors only; deduping unrelated databases (or sub-object pages) remains the standing non-goal below, to be revisited only on evidence that per-object fork sharing isn't enough.
- ⏸ Promote/rollback (and compact) on sharing — deferred. All three still materialize a full copy, by design (they abandon or replace a lineage; base-pointing into a lineage meant to die would pin it forever). Rollback-to-a-kept-checkpoint onto base pointers is the named follow-up in the design spec, not scoped here.
Launch track (parallel to v0.1–v0.3)
- Foundations — org + transfer, names claimed, dead links fixed. Exit: install path resolves; no public 404s.
- Trust floor — CI green and public, community files in place, docs scrubbed. Exit: a stranger finds no internal artifacts and can run the tests.
- Quiet public — repo flips public, v0.1 tagged with binaries, SDKs published, FAQ live; 3–5 hand-picked eval-harness/agent-platform engineers invited. Exit: one external person goes install → fork → promote without help.
- Announce — asciinema of parallel-attempts + an MCP-in-Claude-Code demo (the one nobody else can show), Show HN + lobste.rs + LangGraph/MCP communities same day, author on 48-hour triage rotation with pre-written answers for the predictable questions. Only after phase 3's external user succeeded.
Non-goals (v1)
- Multi-node orchestration. Shared-bucket safety is guaranteed by fencing; placement/failover/routing are the v2 arc. We don't use the word "cluster."
- Merge. Forks are for pick-a-winner (
promote), not three-way merge. The escape hatch is application-level reconciliation over two checkouts. - Windows. The capture path and lock probing are POSIX-dependent.
- Page-level dedupe / content-addressed storage. Still a non-goal even after 0.2.0's copy-on-write forks: that arc shares whole objects between a fork and its own ancestors, never sub-object pages and never across unrelated databases. Revisit only on evidence that per-object fork sharing plus TTLs isn't enough.
- Row-level provenance. Branch-level lineage plus checkpoint metadata is the right grain for the attempt workload.