The success line of both link forms read the live text field, so editing
the name after a link changed the sentence. The result now carries the
name that was sent.
The matrix form held the typed token and password after a failed submit,
and kept them across a switch of credential mode. It now drops them when
the request ends and when the mode changes.
The linked-accounts list and the delete dialog said forgejo where the
link menu and forms say forge; they now say forge.
The running line is a live region whose text changed every second, so a
screen reader announced the counter again each time. The sentence is now
stable and the elapsed seconds sit outside it.
The run and open-a-PR buttons point at the hint that says why they are
off, and add-a-classification says why it is off at the cap.
The dashboard's top-of-page warning banner is gone. The warning registry
stays, because /health/ready, the swarm status view and the OTEL gauges
read it. What it feeds is now alerted on in Grafana:
- hyperhive.hive.warning{kind,level}: one series per raised warning, so an
alert names the cause. Raising a warning also logs one `hive warning
raised` line with the message.
- hyperhive.hive.nix_store.utilization: the used fraction of the nix
store's filesystem, probed in the gauge callback.
- Alerts: hh-hive-degraded on the existing degraded gauge, hh-hive-warning
per kind (disk excluded), hh-host-disk-high at 85% and
hh-host-disk-critical at 95%.
- pending_logins is now a registry warning, refreshed from the container
rescan, so it reaches the per-kind gauge.
Frontend: server_warnings leaves the state snapshot, and the dashboard,
flow and stats pages keep only the sticky chrome wrapper. The shared
hive-warn and WarnBanner components stay, since other views use them.
A module-eval case checks each alert's metric name against the exporter's
source, so a rename on either side fails the build.
The external export is a setting of the swarm collector, so it moves to
services.hyperhive.deploy.swarm-otel.upstream. Renamed:
- otel.endpoint -> upstream.endpoint
- otel.protocol -> upstream.protocol
- otel.headersCredential -> upstream.headersCredential
- otel.collector.upstreamHeaderName -> upstream.headerName
The unreleased otel.upstreamSignals is declared as upstream.signals.
Hosts keep working through the rename warnings. Eval test covers the old
paths reaching the new options. Docs and stale comments point at the
new paths.
Adds services.hyperhive.otel.upstreamSignals, a list of "metrics" and/or
"logs" naming the signals the swarm collector sends to otel.endpoint. The
swarm's own stores always receive everything. The upstream exporter is
only defined when at least one signal uses it, since otelcol refuses an
exporter no pipeline references.
On deploy: nothing changes until the option is set; the default is
[ "metrics" "logs" ], today's behaviour. To stop the 404 flood from an
upstream that has no /v1/logs, set services.hyperhive.otel.upstreamSignals
= [ "metrics" ].
swarm-bao-granter-role exits 4 when the store is unsealed but refuses the
bootstrap token, and Restart=on-failure retried that every 30s for as long
as the token file existed. RestartPreventExitStatus=4 makes that permanent
failure leave the unit failed once. Exit 1 (store sealed or unreachable)
keeps retrying.
On deploy: the unit goes red once instead of looping every 30s. The stale
token on muede-lpt2 still needs the operator step (remove it or mint a
fresh one); this does not clear it.
nixos-rebuild switch no longer hangs when nginx is (re)started in the
same transaction: the cert unit is Before=nginx, so its synchronous
reload and nginx's queued start job waited on each other. The reload is
now queued with --no-block try-reload-or-restart, as the nixpkgs ACME
module does.
The logs/self pipeline shipped the collector's own journal to the upstream
exporter too. When the upstream failed, the collector logged the failure,
journald/self read it, and logs/self sent it upstream again: ~8 lines/s.
logs/self now exports only to otlphttp/victorialogs, and a filter processor
drops the collector's failure lines that name that exporter, so the store
exporter cannot rebuild the loop one hop later. Other collector lines,
including upstream failures, still reach the store.
On deploy, swarm-otel's store rows drop from ~8/s of error spam to its real
output.
The collector's container ran with --link-journal=host and the host's
journal bound over /var/log/journal, so its own journal was never
written where anything read it. It now keeps its journal in the guest,
reads it with a second journald receiver, and gets the host's journal
at /run/host-journal.
On deploy swarm-otel rows start arriving in the store, stamped
service.name swarm-otel.
/messages reads GET /api/messages/stream. A history select picks the window
(live only up to 24 hours) and a filter keeps the messages whose sender or
recipient contains the text. Each message is a row of the terminal list an
agent's terminal uses: one summary line with the time, the two parties and
the start of the body, and the whole body under it. The list follows the
newest message and holds the last 2000.
The feed has no event id, so a reconnect sends the history again. The page
empties its list each time the connection opens and fills it from the
replay, where the terminal stream hook, which keeps its rows across a
reconnect, would list every message twice.
The stream URL, filter, cap and row logic is in its own module with tests,
and docs/swarm/ui.md describes the page.
The refusal sat inside the collapsed section, and the general error panel
hides while a refusal is set, so a refused submit with the section closed
showed nothing. The refusal now renders under the section, outside the
details element.
The form gets a collapsed "instructions" section with a plain text box. Its
text goes in the optional agents_md field of the create request, and is left
out while the box is empty or only whitespace. The controller's size refusal
shows under the box instead of in the form's general error panel. The text
box empties after a successful create.
The send and refusal logic sits in its own module with tests, and
docs/swarm/ui.md describes the field.
The page opens on the firing ones. The button lists every instance again,
with the state filter and the tones back, so the table's filtering is
visible. The saved-filters key goes back to its original name, since the
state column is a filter again.
The page listed every non-normal instance. It now keeps the ones in the
Alerting state and links to Grafana's alert-groups view for the pending,
no-data and error ones.
The instructions row of the agent detail panel gets an edit button that
opens the agent's AGENTS.md, as the harness last published it, in a text
box. Save sends a unified diff against the revision that was loaded; when
the agent changed the file meanwhile the editor keeps the operator's text,
shows the agent's version and offers to save over it or take it. An unsaved
edit survives closing the dialog.
The diff is built in the browser (LCS over lines, three lines of context,
git's no-newline marker) and was checked against git apply on 600 random
before/after pairs.
docs/swarm/README.md gets the routes, the revision, the apply semantics
and the stopped-agent behaviour; the persistence doc points there from
the AGENTS.md entry. The swarm-controller, swarm-controller-api and
swarmctl READMEs and the repo map in AGENTS.md list the new bodies and
routes. docs/tools/swarmctl-cli.md is regenerated for
`swarmctl agent agents-md`, whose help text loses two passive phrasings
vale flags.
A subagent's terminal opened directly showed nothing until the subagent
published again: the relay was a core subscription, though its rows sit
in term-sub-<agent> for 24 hours. It now goes through the agent route's
replay-then-tail, so it opens with the subagent's newest 100 rows and
resumes after Last-Event-ID.
- term_stream.rs: the replay takes a stream name and a filter subject
instead of an agent, and `replay_or_live` (replay, else the live core
subscription) is the one fn both routes call. When the filter is one of
several subjects in the stream, a fresh reader's consumer starts at the
oldest row and drops all but the newest REPLAY_ROWS it counts as
pending, since the stream's newest sequences may belong to sibling
subagents. A single-subject stream (term-main-<agent>) picks its start
with replay_start exactly as before. Tests for both subagent cases.
- subagent_term.rs: the relay calls replay_or_live on the subagent's
subject in term-sub-<agent> and reads Last-Event-ID; module doc and
OpenAPI text no longer say "live only".
- main.rs: the handler test passes the new headers argument.
- swarm-queue-client agent_term.rs: replay_consumer_config takes the
filter subject instead of the agent.
- swarm-queue-client subagent_term.rs: consumer_create_grant, the
extended CONSUMER.CREATE form with a subagent subject as filter.
- swarm-nats-auth policy.rs: the controller holds that grant; the agent
grant's `$SWARM.term.*` filter is two tokens short of a subagent
subject, and the test that refused it now admits it.
- docs/swarm/ui.md, README.md: a subagent terminal opens with its newest
rows.
Fixes#5527
Pick agents from a grid of their icons (running or all), type the text, send.
The page posts the names and the text to the controller and shows what
happened to each agent, with a retry that selects just the ones that failed.
The selection rules and the controller's answer are read by plain functions
with tests.
The swarm UI's broadcast page (#5524) needs the controller to do the
fan-out, so POST /api/agents/broadcast takes {agents, body} and answers
200 with {results: [{agent, sent, reason?}]} in request order.
- agent_input.rs: the single-agent send is now send_one, which both
routes call; the single route maps its outcome to the same statuses
and texts as before. The broadcast validates the whole request first
(sender, empty body, empty or duplicated agents, more than 64, a name
outside the roster), then sends up to 16 at once with the single
route's no-responders window.
- main.rs: wires the route.
- docs/swarm/ui.md: describes the route next to the single-agent one.
An N running subagents badge opens a list of the agent's subagents, each a
link to its terminal, so you can switch without going back to the agents page.
A subagent counts as running when it published a row in the last 15 minutes;
the quiet ones are listed below it, dimmed. The list under the preview and the
menu now share one fetch hook and one definition of running.
/builds lists the builder's recent builds newest first, one card each with how
it ended, and opens a failed build's nix tail. /builds?run=<id> opens that
build's card and scrolls to it. The swarm-builder commit status now links
there instead of to the raw JSON record.
After a failed or timed-out store read the responder denies for LOOKUP_FAILURE_WINDOW (3 s) without a new lookup, so a burst of requests during an outage costs one lookup instead of one per request.
Three edits besides the code in main.rs belong to this change:
- swarm-metrics-auth/Cargo.toml gains tokio's test-util dev-dependency because the burst test runs on paused time (start_paused) to let a hanging store sleep past LOOKUP_TIMEOUT without a real 5 s wait.
- docs/swarm/services.md gains the sentence that a failed or timed-out read is denied for three seconds without a retry, which documents the window this change adds.
- docs/swarm/credentials.md gains the clause "every three seconds while the store fails" on the Grafana bearer row, which documents the same window in the secrets table.
A request that waits behind another build, e.g. one a controller
restart orphaned, could start a full build after most of the
controller's 61-minute reply deadline was gone. The builder now waits
at most REPLY_DEADLINE - BUILD_DEADLINE - 30 s for the slot and
answers Failed, without building, past that. The 30 s covers the
forge token read, now bounded to 20 s, and the reply's trip back.
Fixes#5437
The poller's forge, authelia and collector calls go to leaves issued by
the swarm's own CA, and its reqwest/rustls roots come from SSL_CERT_FILE,
which the unit never set. Every pass since it was deployed fails
"invalid peer certificate: UnknownIssuer" before pushing, so the CI runs
dashboard has no hyperhive.ci.run.* samples (#5438).
- nix/host-modules/swarm-ci-metrics.nix: import lib/hive-ca-trust.nix and
register a host trustBundle for the unit, as swarm-controller does; it
adds swarm-ci-metrics-ca-bundle.service and sets SSL_CERT_FILE.
- nix/module-eval/ci-metrics.nix: a case asserting the unit's
SSL_CERT_FILE is its bundle path when self-signed, with the bundle
unit's ordering, and absent where the poller is off.
- docs/swarm/ci-metrics.md: how the poller trusts the swarm's CA.
The forge and matrix are swarm services; per mara's ruling on #5475
(shape a) a hive no longer starts, stops or restarts them.
- hive-priv-sock: InfraContainer keeps only Gateway; InfraTarget goes,
service_unit maps the gateway to nginx.service directly. A
ControlInfraContainer naming Forge/Matrix fails to decode.
- hive-host-sock: LifecycleScope drops forge/matrix and refuses unknown
fields, so an older hivectl's --forge is an error rather than an
all-false scope meaning everything.
- hive-c0re: scoped_infra selects only the gateway.
- hivectl: --forge/--matrix removed from stop/start/restart.
- docs: services.md says how the forge and matrix are managed;
security.md, coordinator.md and hivectl-cli.md follow.
- hive-host-sock, hivectl/power.rs: Stop and RestartScoped doc comments name the gateway (host nginx.service), not infra containers.
The user JWT the callout issues carried no exp, so nats-server armed no
expiry timer and a connection kept its grant until the client dropped it:
a deleted or re-minted agent queue secret, a removed OIDC client or an
expired access token never reached a live connection.
Every issued user JWT now expires at the presented OIDC token's exp
(read from the introspection answer) or an hour after issue, whichever
is sooner; the agent queue-secret path has no exp of its own and gets
the hour. Every queue client reconnects through an auth callback that
re-reads or re-mints its credential, so the forced reconnect presents a
current one.
Fixes#5432
The operator-boundary sentence about config PRs moves into the config:repo
block, the extra MCP servers bullet no longer names agent.nix, and agent
creation is described without a config repo. config_bind_source's doc
covers the empty mount of an archetype agent. The archetype doc says to
deploy swarm-nats-auth before the hives.
Refs #5386
The hive reads the agent-archetype key when it provisions and rebuilds an
agent and keeps it in swarm-agent-archetypes.json. For an archetype agent
the meta flake has no agent-<name> input and mkAgent imports
hyperhive.hyperhiveArchetypes.<name>.module; provisioning, rebuilds, the
meta-input cascade, a deploy's rev and the startup migration skip the
config repos. The harness gets HIVE_ARCHETYPE and the system prompt says
the agent has no config repo. The create form sends the picked archetype.
Adds the android-dev built-in.
Refs #5386
These three are per-host choices that hive-c0re reads and renders into
its agents, so they belong under the hive controller's deploy namespace
rather than swarm-wide:
- c0re.dashboardPort -> deploy.hive-controller.dashboardPort
- c0re.claudeCodePackage -> deploy.hive-controller.claudeCodePackage
(the claude-login and review-classifier containers on the same host
keep following it)
- otel.extraResourceAttributes -> deploy.hive-controller.otel.extraResourceAttributes
The old paths stay as mkRenamedOptionModule aliases, now in deploy.nix
with the namespace's other renames. github.enable and the collector port
stay swarm-wide.