My Flight Recorder Crashed Before The Plane Did: Four Bugs From One Dogfood Session
Yesterday I published a post about immutable DAG engines and shipped v0 of my flight recorder for agent runs. Tonight my job-hunting bot attached it for a real scoring run.
The recorder crashed before the bot did. Four separate times.
This is that story — unedited, because proof-of-work only counts if you publish the failures too.
Crash 1: The Extra Parenthesis
First flight attempt, the host program refused to even start:
Unmatched delimiter: )
daglog/core.clj:89:14
careerbot.core -- could not be loaded
One extra closing paren left behind from an edit. The interesting part is not the bug — it is the blast radius. Because careerbot required the library directly through its classpath, a typo in the recorder meant the entire bot failed to boot.
Tight coupling turned a cosmetic syntax slip into a total outage. That is the integration seam doing its job: loud, instant, impossible to ignore.
Crash 2: Blobs Full Of Garbage
Fixed the paren, ran again. This time the tool’s own integrity verification rejected its output:
daglog: verification FAILED: blob mismatch
hash c6879b53... does not match stored blob
Root cause was beautiful. To store payload bytes content-addressed, I wrote them like text:
;; broken — stringifies a byte array into "[B@25f38ecd"
(spit path bytes)
;; fixed — raw bytes to disk
(java.nio.file.Files/write path bytes (make-array java.nio.file.StandardOpenOption 0))
Every “payload” on disk was literally the string
[B@25f38ecd — Java’s toString for a byte array. Ten
megabytes of web page became eleven characters of pointer noise.
Here is the part I love: nobody tested this manually. The SHA-256 chain caught it. A flight recorder whose black-box tape silently corrupted would be worse than no recorder — and the tamper-evidence design I wrote about yesterday flagged its own corruption on first read.
Crashes 3 and 4: Contract Mismatches
Two more followed within minutes:
| # | Crash | Root cause |
|---|---|---|
| 1 | Unmatched delimiter | Extra paren from editing |
| 2 | Blob mismatch | Byte arrays written via a text API |
| 3 | ISeq from Integer |
verify returns a summary (:nodes
= count); replay consumed it as if it were the full
document |
| 4 | Range [0,8) out of bounds |
Span IDs derived by truncating display names like :n1 —
shorter than the slice |
Crashes three and four are the same disease wearing different symptoms: return-shape contracts nobody enforced. The fix for number four was also the principled one — derive span IDs from content hashes, never from human-readable labels.
Why Not Just Write Unit Tests?
Because every one of these lived exactly where unit tests are blind:
| Seam | What tests usually fake | What dogfooding exercised |
|---|---|---|
| Load path | test the namespace in isolation | bb script loading lib across repos via classpath |
| Payload types | pass strings everywhere | real files, real bytes, real browser packets |
| Return values | assert on happy shape | caller consuming the wrong-but-plausible shape |
| ID formats | nice long IDs in fixtures | auto-generated :n1, :n2,
:n47 |
My hypothetical test suite would have passed strings to the writer, used tidy IDs, and consumed correct return shapes — then production would have found all four bugs anyway, with worse manners.
Dogfooding finds what your imagination does not simulate: reality’s types, reality’s lengths, reality’s load order.
The Distribution Angle
I keep a written strategy that says developer tools earn adoption through open proof-of-work, not polished demos. This morning was the cheapest possible proof session: one command, four crash receipts, four fixes, all public within an hour.
A tool that survives its creator’s own first flight is worth trying. A tool whose author publishes exactly how it died and recovered is worth trusting.
Takeaway: ship the tool against your own real workload before you ship the announcement — the first flight is the test suite you cannot fake.