Latest posts

Reversibility is the bottleneck

Aug 10, 2026 · by Marko Bakovic · 6 min read

If your data stack could survive an army of agents curating a data asset for investigations or experiments, this blog post is not for you.
I've watched enough coding agents work to understand why software velocity is exploding: code work is made of small, reversible, compounding steps. Every change is a commit, every commit can be walked back, and nothing is ever truly lost. Data work has none of this. Too much of it happens in large, throw-away iterations: copies, scratch buckets, one-off datasets that never add up to anything.
Here's a morning that will feel familiar. Your training script skips a list of files by name, because those episodes were recorded before the firmware fix — the IMU reported degrees instead of radians. Someone has since reprocessed them, but the fix isn't in your copy of the dataset. You try to migrate. Three hours in, your script fails: it turns out they changed something else too. You tell yourself you'll ask on Slack what dataset-fixed-v0 actually contains, and you restart on the old version.
Later that day you have an idea worth trying — an agent can write the pipeline in minutes. But the dataset doesn't quite fit, and handling the mismatch at runtime would make every run crawl. The bitter part: you had a similar idea last month, and the transform you ran then would have covered this one too, but it lives in a scratch bucket nobody dares reuse. The idea goes into your backlog, a graveyard of missed opportunities, and you go back to watching the current run.
Frontier work is a search process, and the team that runs the most investigations and experiments per week wins. Ideas are cheap and compute is rentable; iteration throughput is what compounds. The way to better iterations is more iterations. In physical AI, a single experiment's input is terabytes of multimodal episodes, camera and lidar in MCAP, labels in Parquet, calibration in JSON, spread across more buckets than anyone will admit. The thing standing between you and the next iteration is almost always data logistics.
And the worst failures aren't the loud ones. Somebody "fixes" bad calibration by rewriting files in place, and every model trained on the old bytes quietly becomes unreproducible. Nothing noticed, because the paths didn't change. These failures are invisible on day one, catastrophic at month eighteen, and unfixable retroactively. You can't add provenance to data you already lost.

Machine scale, everywhere

A robot fleet produces 5TB of MCAP a day without a human touching anything, but deciding whether last Tuesday's episodes are safe to train on is still a person spelunking a bucket. Machines are the primary producers and consumers of the data; humans still run every investigation and every experiment.
If we want investigations and experiments to run at machine scale too, the thing that has to change first is that a change to shared data can't be a one-way door.
Every change to your data would have to be a transaction. Every past state would have to stay addressable, not just the latest one, and with retention that respects time travel. The position your shipped model trained on stays hot for as long as the model matters, while the 30TB from last spring that no run ever pinned quietly ages into cold storage.
The record of those transactions can't be something the system merely writes down. The record has to be the only way changes happen.
There's a name for that: a journal.

The morning, replayed

Here's how that morning goes with a journal.
The teammate who reprocessed the broken episodes didn't make a dataset-fixed-v0. The fix landed as one transaction on the shared history, 312 episodes reprocessed with calibration v2, position 1042, with nothing rewritten in place and nothing for you to migrate to.
Your running experiment doesn't notice. It pinned position 1038 when it started, and nothing anyone does can disturb the position you're standing at. No freeze window, no "don't touch the dataset" Slack message; your teammate didn't need to know your run existed.
Trying the fix is launching your next run at 1042. And the "something else" that killed your migration three hours in? It's in the diff between 1038 and 1042, a transaction sitting right next to the calibration pass. You read what changed before you launch instead of discovering it when your script fails. If v2 makes the model worse, that's a result, not a mess: walking it back is launching at 1038 again, and "did we ever try recalibrating those?" is a query.
And the afternoon idea? The transform you ran last month isn't rotting in a scratch bucket nobody dares reuse. It landed as a transaction, so it's already part of the history your next run starts from. Today's idea runs on top of last month's work in the time it takes to type a position. Every change that lands moves the starting line for everyone, and negative results accumulate alongside the wins. The copies never added up to anything. The history adds up by default.

Haven't we solved this?

Table formats have understood this for a long time; transactions and time travel are why Iceberg and Delta won the warehouse. But time travel over a Parquet table doesn't version the 50TB of MCAP and point-cloud bytes the table points at. The table says episode 4711's camera stream lives at s3://…/cam0.mcap; the calibration "fix" rewrites that file in place; your snapshot now faithfully points at bytes that didn't exist when the snapshot was taken. The moment your rows reference external blobs, a snapshot is a snapshot of pointers to mutable things. And a real robotics dataset isn't a table. It's MCAP episodes plus URDF robot models plus calibration files plus sim scene descriptions; many assets that only mean something together, and no single container format with transactions can hold them all.
Branching bolted onto the bucket attacks the problem from the other side, and misses for the same reason. It diffs object keys, but the unit of meaning is the episode. One episode spans MCAP, video, labels, and calibration files, while one file holds a thousand episodes. The diff is too coarse and too fine at the same time; it answers what bytes changed, never what happened. Unless you can branch your whole data stack and changes land as commits small enough to understand and clean enough to merge, branching just enables more copies. The problem with copies was never the copying; storage is cheap. It's that they never add up to anything. A fork that can't merge is a scratch bucket with a nicer name.
So one approach versions the table but not the bytes, and the other versions the bytes but not the meaning. "Keep everything immutable and manage references carefully" is the C++ of data management.

The part where agents come in, of course

With coding agents, generating experiments is no longer the bottleneck — risk is.
The question that keeps a team slow isn't "can we build it?" but "can we walk it back?" Agents can only be let loose on your data if every action they take is a transaction you can revert. A mutable pile of files is unsafe, but how about immutable? Fifty agents, each forking the dataset, produce fifty scratch buckets; you've automated the graveyard.
What makes a fleet productive is that each agent's work lands as a transaction the others can see, build on, and walk back. One asset that compounds instead of fifty copies that diverge.

Getting the primitives right

Every workaround teams invented to go faster is actually a defense against irreversibility. Copies are defensive. Freeze windows are defensive. The scratch bucket is defensive. Teams aren't slow because they lack tools; they're slow because destructive operations on shared data are scary. Speed is what's left when the fear and the waiting are removed.
We want to get the primitives for machine-scale data work right, and we want adopting them to require no leap of faith. The journal is the first one we're deploying with design partners at Spiral. If you think this will help you move faster, we should talk: hello@spiraldb.com.