Building hal0, part 2: the rework release
R5 tore up how flags, memory and releases worked. Then 1.0.0 shipped and needed a fix the next morning — which is the honest ending.
- 01Building hal0, part 1: fifteen tags in ten days
- 02Building hal0, part 2: the rework release
- 03Building hal0, part 3: where the runners come from
1.0.0-rc.1 shipped August 1st under the internal name “R5 · the rework release.” I want to sit
on that subtitle for a second, because it tells you something the version number doesn’t: this
wasn’t a features release. It was the release where I went back through everything that had
accreted since v0.9.0 and asked, slot by slot, config key by config key, whether it would
survive being run on a box that wasn’t mine.
A lot of it wouldn’t have. Part 1 of this series ended on that note deliberately — by 0.9.8 the
platform worked, in the sense that I could point it at my own Strix Halo box and get an honest
/v1/chat/completions response out of it. Whether it worked on your box was a much less
certain question, and R5 is where I stopped assuming the answer was yes.
1. Flags belong to models, not slots
The single biggest structural change in R5 sounds small until you sit with it: launch flags,
device selection and chat-template now live on the model, not the slot. A slot became
just (id, name, model, port, state) — nothing else. Profiles turned into copy-on-stamp
templates instead of live config; the argv resolver stopped reading profile or slot overrides at
launch time entirely, and model.defaults became the single place a materialized tune actually
lived.
Why does this matter more than it sounds like it should? Because before R5, the same model could
be running with two different flag sets depending on which slot happened to load it — a bench-tuned
-ub 1024 on one slot, the profile default on another, both technically “correct” and both
silently different. That’s not a bug you can point at; it’s an ambiguity that becomes a bug the
first time you’re debugging a performance regression and can’t tell which config actually shipped
the tokens you’re looking at.
The migration off the old shape wasn’t automatic, and I made a point of not making it automatic. A fold migrator moves slot-level tunes into model defaults, dry-run by default, and it flatly refuses the whole run — no partial write — if two slots share a model with divergent tunes. You have to resolve the conflict yourself first. I’d rather a migration stop and make you look at the ambiguity than silently pick a winner on your behalf.
2. Memory stops being a choice
R5 also removed Honcho as a memory engine outright. Memory is Hindsight-only now — hal0_memory_*
tools got renamed to hindsight_* (the old names stuck around as aliases for one release, out of
mercy), and there is deliberately no data carry-over path from Honcho. If you’d been running
Honcho, Hindsight starts fresh. That’s a harder line than I like taking, but running two
memory-engine code paths meant every fix to the fail-closed ACL logic — the kind of bug the last
post ended on — had to be verified twice, and I was already not verifying it enough times.
By the time 1.0.0 itself shipped on August 7th, the memory MCP surface had grown to 26 tools at
parity with the live Hindsight release: reflect (LLM-backed synthesis over memory), a
non-destructive curate/history correction path for “this fact is wrong” without deleting it
outright, mental-model and directive CRUD, async-operation polling. The admin catalog alongside
it went from 92 tools to 180 — services, ComfyUI, the updater, hardware telemetry, the long tail
of slots and models. The pitch for all of that is simple: if the dashboard can do it, the agent
running on the box should be able to do it too, under the same approval gates a human would hit.
3. What a release actually is now
The other half of R5 was less about the runtime and more about what happens the moment I tag a
version. This had been informal for too long. Now: a tagged release bundles its matching
## [<version>] CHANGELOG section into the release tarball as RELEASE_NOTES.md, and
scripts/gen_release_notes.py extracts the ### Highlights, ### Breaking and ### Migrations
subsections out of that into a machine-readable release.json.
hal0 update reads that back. Before it applies anything, it downloads the tarball, verifies it
against a cosign signature, and shows you the breaking changes and migrations as actual callouts —
not a changelog link you’re trusting to still be accurate, but the exact text that shipped with
the bytes you’re about to run. hal0 update --rollback exists for the case where you read those
callouts and change your mind after the fact, or where something goes sideways anyway.
I’m not going to pretend the cosign story was clean the first time around, either — the
signature-verification bug that had been silently failing every curl | bash install since
v0.9.2 (part 1 covered it) is exactly the failure mode this pipeline exists to make impossible
going forward. A release pipeline you don’t verify is a release pipeline you’re guessing about.
4. 1.0.0, and rc.2 the next morning
1.0.0 shipped August 7th. It expanded the MCP catalog further, gave slots an explicit
autoload flag so binding a model no longer implicitly meant “start at boot,” replaced the
long-inert lru = true opt-in with a real priority field so memory-pressure eviction actually
does something on a stock box, and reinstated Moonshine as the CPU speech-to-text engine after a
justification for retiring it that — on inspection — had never actually held, since the thing it
was supposedly superseded by had never shipped as a standalone CPU service.
1.0.0-rc.2 shipped August 8th. One day later.
I could frame that as an embarrassment. I’d rather frame it honestly: “1.0” on a release train that’s nightly tags plus release candidates doesn’t mean “finished.” It means “the rework held up well enough to promise semver going forward” — and semver is a promise about the shape of future changes, not a claim that this exact tag is bug-free.
rc.2 wasn’t a panic response to something broken in GA. It was — by its own changelog entry —
“the second and intended final release candidate on the road to 1.0.0,” a preview-channel snapshot
of the same 1.0.0 content for operators validating ahead of the stable promotion, plus the two
breaking changes that had landed since rc.1: the retirement of an experimental, never-actually-
mounted standalone browser MCP server, and that lru → priority eviction change. Nothing in it
contradicts what shipped as 1.0.0 a day earlier. But if you’re the kind of person who reads a
1.0.0 tag as “the version numbers stop moving now,” a release candidate appearing the next
morning is going to read as a red flag whether or not it deserves to.
I’ve decided it doesn’t deserve to, and I think that’s the actual lesson of this release cycle.
Semantic versioning is a contract about compatibility, not a claim about calendar stability. A
project that ships nightly and cuts release candidates continuously is being more honest about
its own churn than one that sits on a 1.0.0 tag for six months while the actual code moves
underneath it in patch releases nobody reads the diff on. The version number is not where the
trust has to come from. The cosign signature and the migration callouts are.
That’s where it should come from, anyway. Two weeks later I ran the validation sweep that should have caught the next round of problems before a release candidate did — and it found eighteen of them. That’s the next post.
Full-stack developer and WordPress architect writing about headless CMS, Next.js and AI-assisted builds.
No spam. One or two emails a month, unsubscribe anytime.