hal0 v1.1.0
Highlights
-
The
picoding agent is installable alongside Hermes — a cli-kind
bundled agent with hal0 slot models and memory wired in (#2163). -
Everything hal0 ships now updates as one system. A component catalog
(OpenWebUI, runner images, Hermes, the Hindsight memory engine) with
release-carried pins, auto-converge as the final pass ofhal0 update, and
a status/retry surface acrosshal0 update status, the dashboard services
page, and the admin MCP (#2150). -
The bundled Hindsight memory engine upgrades to 0.9.2 on every upgrade
path, with build-aside/swap/verify/rollback safety (#2146). -
Specialty model distributions — a declarative registry for models that
ship more than a GGUF; first consumer is the PromptForge distribution with
its gate-validated runner pin (#2129, #2132, #2133). -
Runner-image catalogue v3: rows tell the truth about what is on the box
and what a slot would launch, and pull/delete lifecycle verbs are wired end
to end, including a newhal0 runner-imagesCLI (#2134, #2135, #2137). -
CPU-only installs actually work — the
cpurunner now launches a real
CPU-only llama.cpp build instead of crash-looping the GPU one (#2126).
Fixed
-
The 0.9.8 → 1.0.0 upgrade no longer blanks the brain model it just bound
(#2131). The documented upgrade path downloaded the default brain model, left
/etc/hal0/slots/brain.tomlwith a[model]table naming nothing, and ended
atVerify FAILED: structured-output probe failed— with no hint that one
hal0 slot edit brain --model <id>recovered the box. The binding was not
missing, it was reverted: v0.9.8's installer ranhal0 setup --auto
before its curated seed loop, so every stable box carries the generic
scaffold'senabled = falsebeside a model-less[model]table; install.sh
bound the freshly pulled default into it, and theSlotConfig.enabledsweep
— which ran after the brain step — readenabled = falsenext to a bound
model and cleared it, exactly as that migration is designed to. The same
sweep runs at everyhal0-apiboot, so even a hand-repaired box lost the
binding again on the next restart. install.sh now runs that sweep before
the brain model step, which hits the migration's own "no model bound, just
drop the stale key" branch; the binding then survives every later boot sweep.
A slot an operator deliberately disabled (enabled = falsewith a model
bound) still has its model cleared, as before. -
The brain binding is now verified rather than assumed (#2131). The
activation write is best-effort by design (_activate_slot_modelsuppresses
every exception so a config rewrite can never abort a pull), so the installer
printedbrain model ready: … bound to the 'brain' slotand exited 0 whether
or not the write landed.hal0.install.brain_modelnow reads the slot back:
an existing-but-UNBOUND[model]table is the shipped seed state and gets
the default bound into it, a NON-EMPTY[model].defaultis an operator pick
and is never touched (a re-run can no longer revert one), and a binding that
cannot be made is reported with the exact remediation command instead of
reported as success. Still never fatal — the install continues. A failed
structured-output probe now also names an unbound brain slot when that is the
actual shape on disk. -
CPU-only installs work.
HAL0_ALLOW_CPU_ONLY=1— the installer's own
printed remedy for a box with no GPU — produced an install that completed,
reported itself ready, and crash-looped the brain slot with SIGILL
(status=132) forever, whilehal0 slot listshowedwarming(#2126). The
cpurunner carriedFALLBACK_VULKAN_IMAGE, the GPU toolbox, so a
correctly deriveddevice = "cpu"slot launched a GPU llama-server build.The
cpurunner now resolvesghcr.io/hal0ai/hal0-toolbox-cpu:v1— a real
CPU-only llama.cpp build (GGML_VULKAN/CUDA/HIP=OFF, andGGML_NATIVE=OFF
so the binary is portable rather than tuned to the build machine) — with a
cpuentry inmanifest.json'stoolbox_images, so it takes a digest pin
like every other runner. The image was already built and published by
.github/workflows/toolbox.yml; it had simply never been wired to the
runner. GPU boxes are untouched: they resolverocmfpx, a different image
lineage.Shipped alongside it, as hardening that stands whatever a slot's image turns
out to be:- the slot unit's
RestartPreventExitStatus=also names132, so a SIGILL
parks the unit instead of burning the restart ramp. SIGILL only: a restart
can never fix an instruction this CPU cannot execute, while
SIGSEGV/SIGABRT/SIGKILL can be transient after load and keep their runway; - both runner entrypoints translate a load-phase SIGILL/SIGABRT/SIGSEGV into
the existing exit 64 (#2037) with a diagnostic naming the signal, and
unit_failure_reasonnow readsExecMainStatus, sohal0 statusand the
dashboard say "SIGILL — the image's CPU/ISA baseline does not match this
host's CPU" instead of a bareresult=exit-code; - the installer verifies the
cpurunner actually has an image before it
announces that a CPU-only install is proceeding. It passes on a shipped
build; if that runner ever loses its image again the install refuses,
namingHAL0_TOOLBOX_IMAGE_CPUas the way through, rather than repeating
the "To install CPU-only anyway, re-run withHAL0_ALLOW_CPU_ONLY=1"
remedy that produced this report.
Existing broken boxes heal on
hal0 update. A CPU slot installed before
this fix carriesimage = "…amd-strix-halo-toolboxes:vulkan-radv-server"in
its TOML. That ref is already a known former default, so the updater's
retag_stale_slot_imagesre-resolves it through the hardware gate — which
used to land back on the same GPU image (a no-op that left the box
crash-looping) and now lands on the CPU toolbox. No TOML editing required.Not covered: the bench harness's
cpulane still uses the GPU image, because
it needsllama-bench, whichhal0-toolbox-cpudoes not build. And
cpu.Dockerfilestill builds llama.cpp frommasterrather than a pinned
ref — the manifest digest is what holds the shipped surface steady today. - the slot unit's
-
A freshly booted box could finish the installer with Hermes silently
unprovisioned. Ifunattended-upgradesorapt-dailystill held the
dpkg lock — routine in the first minutes after boot — the preflight
install of git failed immediately rather than waiting, and Hermes
provisioning was skipped without a hard error. Everyapt-getin the
preflight library now waits for the lock
(-o DPkg::Lock::Timeout=120), the conventioninstall.shalready
applied to its own calls. The git install was the reported case, but all
six package installs preflight performs — the Python floor, the venv,
podman, git and Node — had the same gap and all six are fixed (#2125). -
The installer could abort with "uv could not be installed" on a box
where uv had installed perfectly.hermes-prereqs.shinstalled uv with
pipx and then looked for it onPATH; in a non-login exec context
(pct exec,lxc-attach,docker exec, cloud-init)PATHcan lack
/usr/local/bin, so a successful install read as a failure. The script
now checks the location it just wrote to. The same install-here,
verify-through-PATHshape sat one step downstream in the provisioner,
where the root privilege drop throughrunuser/setpriv/sudocan
replacePATHoutright; that path falls back to the fixed location too,
so a box that got past the script no longer fails a step later with a
more confusing error (#2124). -
A post-install seam check could report a working permission grant as
broken. The verifier probed each grant once, so a probe that lost a
race with the grant becoming live was reported as a permanent fault —
and named a cause it had never actually observed, because it discarded
the probe's own stderr. The probe now retries (three attempts, a second
apart, each bounded by a 20-second timeout), says so plainly when a grant
only came up on a later attempt instead of passing silently, and on a
genuine failure reports the command it actually ran, its exit code, the
attempt count and the probe's last stderr line rather than asserting a
diagnosis (#2084). -
hal0 updateno longer re-breakshal0-gpu-perms.serviceon
custom-prefix boxes. The updater copied the bundled unit verbatim on
every activate, reverting theExecStartinterpreter pathinstall.sh
rewrites for a non-defaultHAL0_PREFIX; the unit then died203/EXEC
on every boot until the next hand-fix, silently disabling the boot-time
GPU device-permission convergence. The updater now applies the same
venv-path rewrite the installer does, using the venv the updater itself
runs from, so the unit survives every subsequent update (#1982). -
scripts/set-version.pynow rewrites the README status line. The
front-page version blockquote was the one version-bearing file the
release script skipped, so it shipped stale on every cut and was
hand-fixed twice. It is now part of the same validated, rollback-capable
transaction as the other five files, and the script fails loudly if the
blockquote pattern is ever missing instead of silently skipping it
(#1992). -
Rootless image pulls work on fresh installs (#2119, #2121). The
installer's system-user step never allocated a subordinate uid/gid range
for thehal0service user (useradd --systemdoesn't), so every
dashboard runner-image pull failed at layer unpack with podman exit 125.
The installer now allocates a 65536-wide range past every existing claim
in/etc/subuid//etc/subgid, idempotently, and runs a one-time
podman system migrateon existing installs so a store initialized
single-uid converges on the next upgrade. -
The runner-image tag picker no longer floods with CI debris, and the
"newer" chip no longer fires on mutable tags (#2123). Cosign
signature/attestation objects (sha256-….sig/.att) and per-commit CI
tags are filtered at the single GHCR fetch point so they never enter the
catalogue, and the newer-build chip skips mutable pointers (main,
latest,nightly, …) instead of comparing the headline against
whatever the registry listed first. -
The slot drawer heals superseded runner-key spellings (#2141, #2142).
A slot TOML carryingbinary = "vulkanfpx"launched correctly through
the permanent alias but the drawer showed the raw spelling as
out-of-vocab with a warning; the view now folds it to the canonical
rocmfpxselection, warns once per key in the journal, and still
surfaces genuinely unknown keys verbatim. -
A cancelled rootful image pull can no longer leave a wedged
podman pullbehind (#2140). The teardown now escalates
SIGTERM → SIGKILL after a 10-second grace, andpodman rmireturn codes
are reported faithfully instead of collapsed into one bucket.
Added
piis installable as a cli-kind bundled agent — coexists with Hermes.
hal0 agent install piprovisions a minimal profile: thehal0theme, a
hal0 slot model provider, and memory wiring (the memory MCP server plus
hindsight coding-agent memory). Being cli-kind rather than daemon-kind, it
sits outside single-pick, so it can be installed alongside Hermes.- Component update system (#2150). One
ComponentDefcatalog covers
every companion hal0 ships — OpenWebUI, runner images, Hermes, the
Hindsight memory engine — with release-carried pins and auto-converge as
the final pass ofhal0 update(boot stays diagnose-only). New surfaces:
hal0 update statusandhal0 update component <id>, a component summary
and extended exit-2 contract on barehal0 update,
GET /api/updates/components+ job-based per-component converge retry,
components_pendingon/check, version cells and failure retry on the
dashboard services page, andcomponent_status/component_convergeon
the admin MCP (converge owner-approval gated; self-update deliberately
not exposed). OpenWebUI moves from floating upstream:mainto a
release-carried digest pin —hal0 update owui --tagis removed,
--target/--clear-overrideadded. - The bundled Hindsight memory engine converges to 0.9.2 on every upgrade
path (#2146). The engine venv was pinned 0.8.4 and upgrade-blind —
install.sh only built a missing venv andhal0 updatenever touched
the engine tree, so every box stayed on 0.8.4 forever, without the
/knowledge-base/*API 0.9.x carries. A new convergence pass builds
.venv.newaside while the old engine keeps serving, snapshots.pg0
before the new engine's one-way alembic migration runs, requires
/health+/version== pin after the swap, and restores both venv and
data dir on any postcheck failure. Staleness at boot is logged with the
remedy instead of pip-ing behind the operator's back; a new doctor row
and an install.sh smoke probe watch the pin.hal0 update --rollback
intentionally leaves the engine on the newer version (wire-compatible;
reverting a one-way DB migration silently would be worse). - Specialty model distributions (#1946, #2129). Some distributions ship
more than a GGUF — companion files, required env vars, an argv envelope,
a runner built a specific way. A declarativeSPECIALTY_KINDSregistry
now carries that: pulls classify and SHA-verify companion sidecars,
launches are accelerated only on a runner that declares the specialty
with all companions present and otherwise run loud degraded GGUF-only
(stamped on slot detail,slot status, and the launch log — never
silent). First consumer: the PromptForge distribution, with a HIP-only
promptforgerunner. After the on-silicon validation gate passed, the
manifest digest pin (#2132) and the four card-verbatim
PROMPTFORGE_*mode envs (#2133) shipped; promptforge remains an
optional runner — defaults are untouched. - Runner-image catalogue v3 (#2106, #2134, #2135, #2137). Rows now
compute store state from the image store slots actually launch from
(digest-first, so retagged images still show present) instead of a
marker file only the dashboard's own pulls wrote; a digest is resolved
per tag so "newer" is a digest fact, not a tag-name heuristic; and a new
familiespayload shows, per runner family, the effective ref from the
real resolve chain with its source and store state. Lifecycle verbs are
wired end to end: per-tag pull, honest failed-pull surfacing (#2120),
restart-affected-slots, and — through the newhal0-podman-rwwrite
seam (exactly two verbs, same doctrine as-ro) — rootful pulls into
the store slots launch from plus a guarded per-tag delete with
real disk reclaim (never forced; refuses tags in use, naming the slots).
New CLI:hal0 runner-images ls|sync|pull|rm. A v1.0.0 box gets the
write seam automatically onhal0 update. - Per-image llama.cpp build provenance (#2144). Catalogue rows and
/api/system-infobackends now carryprovenance(source repo,
revision, patch count) read from OCI labels, so an operator can tell the
upstream image's build apart from the ROCmFPX one — e.g.
rocmfpx — ROCmFPX @0a59add (+4 patches)vsupstream — llama.cpp @c841aeein the backend picker. - Upstream llama.cpp runner-image variant (#2118, #2122).
hal0-combined-upstream— pristineggml-org/llama.cpp(carrying
qwen4exp) on the byte-identical base digest, cmake flags, and shared
entrypoint of the default rocmfpx recipe. Reached only through a slot's
image_pin; the default image and every existing slot are untouched. - Machine-readable runner-image pin export (#2139).
exports/runner-image-pins.jsoncarries every ghcr ref shipped hal0
code still pulls, for the runner-images repo's retention sweep to fetch;
an equality test recomputes it from the schema constants so a pin bump
fails CI until the export is regenerated. - The two release-delivery failure modes that stayed invisible for weeks
now have owners (#2057, #2101).mirror-bootstrapsplits its signed-
manifest gate from the publish, so a refusal is askippedjob
conclusion instead of a::warning::on a green run, and gains
gate_channel/dry_runinputs so the publish path can be rehearsed
before a GA cut.bootstrap-parityopens a single tracking issue after
three consecutive daily reds and closes it when parity returns. A new
dailystable-pointer-watchworkflow fails loudly and files an issue
when a GA tag is older than its grace window and the livestable.json
/stable.json.bundlepair still does not deliver it. CONTRIBUTING.md
gains the post-tag "Release delivery" runbook.
Changed
- Removed the
vulkanfpxrunner key — there was never a vulkanFPX
binary; the runner is ROCmFPX and its image serves both GPU backends.
rocmfpxis the single key (supported_backends: rocm, vulkan; the
slot'sdevicepicks the lane). Persistedbinary = "vulkanfpx",
[slots].default_imageskeys, andHAL0_TOOLBOX_IMAGE_VULKANFPXare
honored forever via a permanent alias (warned at load; TOML is never
rewritten in place). - The memory-namespace grammar shrinks to
shared|private:<client_id>
(ADR-0004, #2161). Theagentsandproject:<id>namespaces are
retired:sharedis the default for every write,private:<agent>
stays behind the private-mode toggle, and scoping within shared is
tags (agent identity cards keep theiragent-identitytag and move to
shared; project provenance is aproject:<id>tag). Writes naming a
retired namespace get a 400 with a pointed remedy; reads keep the
established contract (unknown entries dropped, all-unknown lists fail
closed); a single-id delete aimed at a retired namespace now gates for
operator approval like any foreign namespace. Per-repo coding memory
already lives outside this grammar incoding-agent::<repo>engine
banks.
Migrations
- Deployments with data still in an
agentsmemory bank must
document-transfer it tosharedby hand — nothing recreates or reads
that bank after the namespace-grammar cut (#2161). - The first
hal0 updateon a provisioned box stamps the Hermes venv pin
(one-time; the provision-checkpoint fallback prevents a gratuitous
rebuild), and the Hindsight engine's.pg0is migrated one-way to
0.9.2 —hal0 update --rollbackdeliberately leaves the engine on the
newer, wire-compatible version (#2146, #2150).