Skip to content

hal0 v1.1.0

Choose a tag to compare

@github-actions github-actions released this 31 Aug 18:23
· 53 commits to main since this release
89eb965

Highlights

  • The pi coding 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 of hal0 update, and
    a status/retry surface across hal0 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 new hal0 runner-images CLI (#2134, #2135, #2137).

  • CPU-only installs actually work — the cpu runner 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.toml with a [model] table naming nothing, and ended
    at Verify 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 ran hal0 setup --auto
    before its curated seed loop, so every stable box carries the generic
    scaffold's enabled = false beside a model-less [model] table; install.sh
    bound the freshly pulled default into it, and the SlotConfig.enabled sweep
    — which ran after the brain step — read enabled = false next to a bound
    model and cleared it, exactly as that migration is designed to. The same
    sweep runs at every hal0-api boot, 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 = false with 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_model suppresses
    every exception so a config rewrite can never abort a pull), so the installer
    printed brain model ready: … bound to the 'brain' slot and exited 0 whether
    or not the write landed. hal0.install.brain_model now 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].default is 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, while hal0 slot list showed warming (#2126). The
    cpu runner carried FALLBACK_VULKAN_IMAGE, the GPU toolbox, so a
    correctly derived device = "cpu" slot launched a GPU llama-server build.

    The cpu runner now resolves ghcr.io/hal0ai/hal0-toolbox-cpu:v1 — a real
    CPU-only llama.cpp build (GGML_VULKAN/CUDA/HIP=OFF, and GGML_NATIVE=OFF
    so the binary is portable rather than tuned to the build machine) — with a
    cpu entry in manifest.json's toolbox_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 resolve rocmfpx, 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 names 132, 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_reason now reads ExecMainStatus, so hal0 status and the
      dashboard say "SIGILL — the image's CPU/ISA baseline does not match this
      host's CPU" instead of a bare result=exit-code;
    • the installer verifies the cpu runner 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,
      naming HAL0_TOOLBOX_IMAGE_CPU as the way through, rather than repeating
      the "To install CPU-only anyway, re-run with HAL0_ALLOW_CPU_ONLY=1"
      remedy that produced this report.

    Existing broken boxes heal on hal0 update. A CPU slot installed before
    this fix carries image = "…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_images re-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 cpu lane still uses the GPU image, because
    it needs llama-bench, which hal0-toolbox-cpu does not build. And
    cpu.Dockerfile still builds llama.cpp from master rather than a pinned
    ref — the manifest digest is what holds the shipped surface steady today.

  • A freshly booted box could finish the installer with Hermes silently
    unprovisioned.
    If unattended-upgrades or apt-daily still 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. Every apt-get in the
    preflight library now waits for the lock
    (-o DPkg::Lock::Timeout=120), the convention install.sh already
    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.sh installed uv with
    pipx and then looked for it on PATH; in a non-login exec context
    (pct exec, lxc-attach, docker exec, cloud-init) PATH can 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-PATH shape sat one step downstream in the provisioner,
    where the root privilege drop through runuser/setpriv/sudo can
    replace PATH outright; 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 update no longer re-breaks hal0-gpu-perms.service on
    custom-prefix boxes.
    The updater copied the bundled unit verbatim on
    every activate, reverting the ExecStart interpreter path install.sh
    rewrites for a non-default HAL0_PREFIX; the unit then died 203/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.py now 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 the hal0 service user (useradd --system doesn'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 migrate on 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 carrying binary = "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
    rocmfpx selection, 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 pull behind
    (#2140). The teardown now escalates
    SIGTERM → SIGKILL after a 10-second grace, and podman rmi return codes
    are reported faithfully instead of collapsed into one bucket.

Added

  • pi is installable as a cli-kind bundled agent — coexists with Hermes.
    hal0 agent install pi provisions a minimal profile: the hal0 theme, 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 ComponentDef catalog covers
    every companion hal0 ships — OpenWebUI, runner images, Hermes, the
    Hindsight memory engine — with release-carried pins and auto-converge as
    the final pass of hal0 update (boot stays diagnose-only). New surfaces:
    hal0 update status and hal0 update component <id>, a component summary
    and extended exit-2 contract on bare hal0 update,
    GET /api/updates/components + job-based per-component converge retry,
    components_pending on /check, version cells and failure retry on the
    dashboard services page, and component_status/component_converge on
    the admin MCP (converge owner-approval gated; self-update deliberately
    not exposed). OpenWebUI moves from floating upstream :main to a
    release-carried digest pin — hal0 update owui --tag is removed,
    --target/--clear-override added.
  • 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 and hal0 update never 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.new aside 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 declarative SPECIALTY_KINDS registry
    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
    promptforge runner. 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
    families payload 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 new hal0-podman-rw write
    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 on hal0 update.
  • Per-image llama.cpp build provenance (#2144). Catalogue rows and
    /api/system-info backends now carry provenance (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) vs upstream — llama.cpp @c841aee in the backend picker.
  • Upstream llama.cpp runner-image variant (#2118, #2122).
    hal0-combined-upstream — pristine ggml-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.json carries 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-bootstrap splits its signed-
    manifest gate from the publish, so a refusal is a skipped job
    conclusion instead of a ::warning:: on a green run, and gains
    gate_channel/dry_run inputs so the publish path can be rehearsed
    before a GA cut. bootstrap-parity opens a single tracking issue after
    three consecutive daily reds and closes it when parity returns. A new
    daily stable-pointer-watch workflow fails loudly and files an issue
    when a GA tag is older than its grace window and the live stable.json
    /stable.json.bundle pair still does not deliver it. CONTRIBUTING.md
    gains the post-tag "Release delivery" runbook.

Changed

  • Removed the vulkanfpx runner key — there was never a vulkanFPX
    binary; the runner is ROCmFPX and its image serves both GPU backends.
    rocmfpx is the single key (supported_backends: rocm, vulkan; the
    slot's device picks the lane). Persisted binary = "vulkanfpx",
    [slots].default_images keys, and HAL0_TOOLBOX_IMAGE_VULKANFPX are
    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). The agents and project:<id> namespaces are
    retired: shared is the default for every write, private:<agent>
    stays behind the private-mode toggle, and scoping within shared is
    tags (agent identity cards keep their agent-identity tag and move to
    shared; project provenance is a project:<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 in coding-agent::<repo> engine
    banks.

Migrations

  • Deployments with data still in an agents memory bank must
    document-transfer it to shared by hand — nothing recreates or reads
    that bank after the namespace-grammar cut (#2161).
  • The first hal0 update on a provisioned box stamps the Hermes venv pin
    (one-time; the provision-checkpoint fallback prevents a gratuitous
    rebuild), and the Hindsight engine's .pg0 is migrated one-way to
    0.9.2 — hal0 update --rollback deliberately leaves the engine on the
    newer, wire-compatible version (#2146, #2150).