The arc
| Feb 2026 | v0.1 — internal preview: MCAP parsing, BagFrame prototype |
| Mar 2026 | v0.2.0 — first public release, 164 tests |
| Apr 2026 | v0.2.1 → v0.4.2 — the reliability & honesty arc |
| May 2026 | v0.5 → v0.6.2 — 3D scene, custom types, the e2e harness |
| Jun 2026 | v0.7.x — six "10x" features, two of them demand-validated |
| Jul 2026 | v0.8.x — the notebook UI rebuild |
| Oct 2026 | v0.8.4 → v0.8.7 — four releases in nine days, mostly export correctness |
| Today | 19 releases · 1,799 backend + 101 unit + 63 e2e tests |
Twelve stories
1. Picking a lane meant naming who's better
The first scoping decision was about what not to build. Foxglove is excellent at 3D visualization; PlotJuggler is excellent at fast plotting; competing there would have meant losing slowly. The lane that was actually empty: treating a recorded bag as a queryable dataset — health checks, sync, ML export, search — with no ROS install. Even so, early comparison claims overreached, an internal review flagged them as expectation risk, and the README's comparison table got tightened release by release.
2. The "lazy" that wasn't and the "sha256" that wasn't
Two early names lied. to_lazy_polars() materialized every chunk
before returning — fixed once to be genuinely lazy, then removed entirely in
v0.4.0 when it turned out to leak a temp file per call (the replacement,
materialize_ipc_cache(), has an explicit lifecycle — the
project's only breaking change to date). And _compute_sha256
hashed only the first megabyte: fine as a change-detection fingerprint,
dishonest as a SHA. The rename to fingerprint (with opt-in
--full-hash for a real digest) required renaming a database
column on users' machines — which forced building the forward-only
migration framework the index still runs on today.
3. The wheel that worked on my machine
v0.3.2, the project's first true shipped-broken release: resurrector
demo crashed on every PyPI install with
ModuleNotFoundError: No module named 'tests'. The demo-bag
generator lived under tests/fixtures/, which doesn't ship in
the wheel — so every from-source check passed while every real user's first
command failed. The fix moved the generator into the package; the real fix
was a new CI job that builds the wheel, installs it into a fresh venv, and
runs the smoke suite against that.
4. The flagship feature was broken on every install
v0.4.1: semantic frame search — the headline demo — raised a DuckDB type
error on every query, on every install, because two parameters were
passed in the wrong order. A test existed that would have caught it. It
never ran: it was gated behind importorskip("sentence_transformers")
and CI never installed the [vision] extra (avoiding a 2 GB
model download), so the test silently skipped for weeks. The fix added
SQL-level tests with synthetic embeddings, plus an extras-test
CI matrix that installs every optional extra and runs the suite under each.
5. The OOM reckoning
Real bags are bigger than RAM, and the early code paths quietly assumed they weren't. v0.4.0 was a release with zero new features: health checks, density, plotting, and sync were all rewritten from accumulate-then-compute into running state — Welford accumulators, bin counters, bounded lookahead buffers. The contract ("memory is bounded by chunk size, not bag size") went into the README, and a memory-regression suite now measures peak RSS in CI and fails if any hot path regresses. The two places the rule genuinely can't hold got typed errors that name the limit and the alternative instead of a silent warning nobody reads.
6. Two sync engines, one truth
The streaming rewrite left sync with two implementations behind one API: eager (vectorized, fastest under 1M messages) and streaming (bounded buffers, unbounded bags). Two engines invite silent drift, so the test suite runs both against nine timing-pathology fixture bags and asserts identical output — which surfaced real subtleties, like tie-breaking on equidistant samples. Best gotcha: MCAP readers return messages time-sorted even when written out of order, so the out-of-order policies couldn't be tested through a fixture bag at all — they're tested at the row-iterator level with a fake view that regresses timestamps on purpose.
7. 717 green tests and a broken product
By v0.6.0 the backend suite was over 700 tests and users were still hitting
a toast that said play: [object Object], a playback engine
that sprinted through messages after resume, and an opaque 500 on the
search page. None of those live where backend tests look. v0.6.1 added a
Playwright harness driving a real browser against a hermetic dashboard —
and every e2e spec is annotated with the bug it would have caught. One trap
worth passing on: --update-snapshots only rewrites failing
visual baselines, so a stale-but-within-tolerance baseline survives
"regenerate everything" — delete the file first.
8. The error message is the product
The heavy dependencies — CLIP, TensorFlow, rclpy — live behind pip extras, which means the most common failure mode is "feature not installed." The v0.6.1 answer was a capability registry: one endpoint probes what's importable, and every surface renders the same honest state — available, or an install banner with a copy-paste command. Then reality kept teaching: the printed command failed on zsh (macOS's default shell) until every install string was quoted, and v0.8.3 moved the search cell's check before the input, because typing a query just to learn the extra is missing is its own small betrayal.
9. Strangers set the roadmap
The best features weren't planned. A ROS Discourse question about custom
message types became v0.6's pluggable decoder registry, and a
promise made in that public reply ("if that's a common pattern I'll promote
it to first-class") became concatenate_bags(). Outreach
replies asking "can it hand me an incident report?" and "can it gate CI?"
became v0.7.1's report generator and bag contracts — both labeled
demand-validated in the changelog because they came from users, not the
roadmap.
10. Rebuilding the front door without closing the old one
An internal usability review in April was blunt: capability 8.5/10, UI
maturity 5/10, discoverability 5.5/10. The dashboard was a page per
feature, and features users didn't find might as well not exist. The v0.8
answer was a rebuild around one rule — every capability is a cell in a
notebook feed, sharing a command palette, a linked time cursor, and inline
guides. The rebuild shipped as slices: the classic UI stayed fully
reachable at /classic, cells landed one release at a time,
and the default route flipped only when the notebook covered everything.
The e2e suite tripled in the same release the routes swapped.
11. The demo recording that kept dying at 20 seconds
The v0.8 demo videos are recorded by a Playwright harness driving the real
app. One night every scene started truncating at exactly ~20 seconds —
because macOS's temp cleaner had deleted the demo environment under the
running server, the bag list came back empty, and every scene timed out
on its first selector. The failure was invisible for an hour because the
recorder's output was piped through tail -1. Recording demos
also found real product bugs — v0.8.1 exists because the camera never lies
about a cell silently appearing below the fold.
/tmp, and never
pipe away the stderr of your own tooling.12. Solo, with guardrails
This is a nights-and-weekends project built solo, pairing heavily with AI
coding agents — which changed what discipline means. Branch protection is
turned on against myself; every release runs the same gate (full backend,
unit, and e2e suites, then a clean-venv install smoke); the visual
baselines have a human gate before every tag. The repo's
CLAUDE.md conventions file became the standing brief that
keeps every working session — human or machine — honest about the memory
contract, the migration rules, and what never gets committed. The
deliberately-not-done list is written down too, so deferring stays a
decision instead of a drift.