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.