Skip to content
at_.DEVELOPMENT

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.

August 13, 20269 min read
hal0release-engineeringsystemdpodmansecuritycosign
BUILDING HAL0 — PART 2 OF 3
  1. 01Building hal0, part 1: fifteen tags in ten days
  2. 02Building hal0, part 2: the rework release
  3. 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.

MODEL DEFAULTS — POST-R5 SHAPE
01[model]
02id = "qwen3-6-35b-a3b-nsc-ace-saber-mtp-f16-to-rocmfp4-strix-lean"
03
04[model.defaults]
05device = "gpu-rocm"
06extra_args = ["-ub", "1024", "-ngl", "999", "--jinja"]

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.

A tag triggers gen_release_notes.py, which slices the CHANGELOG section into RELEASE_NOTES.md and release.json before the tarball is cosign-signed. hal0 update reads the signed tarball back and renders the callouts before touching anything.

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.

RELEASE.JSON — SHAPE (V1.0.0-RC.1)
01{
02"version": "1.0.0-rc.1",
03"highlights": [
04 "FLAGS-own: launch flags now live on models, not slots",
05 "Memory is Hindsight-only; Honcho removed"
06],
07"breaking": [
08 "SlotConfig.enabled is gone; a bound model is the activation signal"
09],
10"migrations": [
11 "Run the slot-flag fold migrator (dry-run by default)"
12]
13}

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 lrupriority 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.

Alexander Talaat
Washington, D.C.

Full-stack developer and WordPress architect writing about headless CMS, Next.js and AI-assisted builds.

Work with me
Get new posts by email

No spam. One or two emails a month, unsubscribe anytime.