- mouse_click: error on unrecognised button name instead of silently
treating it as left click; "left" now explicit in match arm
- rfb_handshake: cap ServerInit name-length at 256 bytes to prevent
a multi-GB allocation from an aberrant server response
The display size is not always 1280x720 — it changes when the operator
clicks 'match size' in the web UI. Replace the fixed mention with a
recommendation to use screenshot first to check the current resolution.
Implements mouse input by speaking the RFB protocol directly to Weston's
neatvnc server (localhost:HIVE_GUI_VNC_PORT, default 5900) — the VNC
backend's native remote-input path. No /dev/uinput, no kernel bypass;
the compositor mediates all input just as it does for the browser VNC viewer.
Changes:
- rfb_handshake(): RFB 3.8 handshake with security type None (auth-method=none
in weston.ini); shared-session ClientInit keeps the browser viewer connected
- rfb_pointer_event(): encodes a 6-byte RFB PointerEvent (type=5, button-mask,
x/y big-endian)
- rfb_send_pointer_events(): connects, handshakes, sends an event slice,
flushes — all in one TCP connection
- mouse_move(x, y): sends a single PointerEvent(mask=0, x, y)
- mouse_click(x, y, button): sends move → button-down → button-up sequence
(left/middle/right via RFB button-mask bits 0/1/2)
- vnc_port(): reads HIVE_GUI_VNC_PORT from env, falls back to 5900
No new packages or nix options — HIVE_GUI_VNC_PORT is already set by the
harness when gui.enable = true; grim/wtype are the only runtime deps.
Closes#2618.
- run_cmd now returns Result<String, String> — callers pattern-match
instead of comparing against an "ok" sentinel string
- Add cmd_result() helper to format run_cmd results as tool strings
- mouse_click: collapse nested if-let into let-chain (clippy collapsible_if)
- nix fmt: reformat screen.nix package list
New crate hive-screen-mcp: a stdio MCP bridge activated automatically
when an agent has hyperhive.gui.enable = true. Provides five tools:
- screenshot — grim → saves PNG, returns path for Read tool
- type_text — wtype → Unicode text input (no daemon)
- key_press — ydotool key → combos like ctrl+c, super+l
- mouse_move — ydotool mousemove --absolute
- mouse_click — ydotool click, optionally with prior move
New nix/agent-modules/screen.nix: wires the MCP bridge into
extraMcpServers.screen; adds grim + wtype to systemPackages. Adds
hyperhive.gui.screenInput option (default false) which enables the
ydotoold daemon + ydotool for mouse/keyboard injection via /dev/uinput.
screenshot and type_text work without screenInput. key_press,
mouse_move, and mouse_click return a ydotool error until ydotoold is
running and /dev/uinput is accessible in the container.
Rework the crate's scheduling model onto an explicit parent (grouping)
axis, separate from the dep (ordering) axis.
- Node gains a structural `parent: Option<NodeId>`, set by the caller
independent of its `Dep::Node` edges. Grouping is not ordering. A
`Dep::Node` edge must stay inside the depender's own parent group
(validated) — never crossing to another group or onto the parent.
- Resource holding walks the parent tree: acquire fresh when no ancestor
holds it (the acquirer owns it, held for its whole subtree); borrow an
ancestor's grant (one branch at a time; nodes inside are covered); take
an extra unit when the grant is lent to a sibling branch, else wait. A
grant releases only once the owner and its whole subtree are terminal.
- Completion rolls up the parent tree: a node's sub-nodes run after its
own logic, and it is not terminal until they finish — it parks in
`State::Finishing`, rolling up to Done (every child Done) or Failed
(any child Failed/Cancelled). A child is gated on its parent reaching
Finishing; a downstream dep on a node therefore waits for that node's
dynamically-appended children with no explicit edge. A failed node
cancels its pending sub-nodes.
Deletes the SharedResources/ResourceGuard layer (guard.rs) and the
add_dep graph-growth hook (no longer needed). The scheduler stays
single-threaded, owning the ResourceTable directly. Early release of a
grant once no subtree node still needs it is a deferred optimization
(unsafe under dynamically-appended subnodes, #2611).
Base for the hive-c0re job_queue port (#2605), split out so that PR can
rebase onto it.
- asyncBtn now returns fn().finally(...) so callers can await/chain it
- Move re-fetch calls inside try/catch in core.js and permissions.js so
network errors from fetchAndRenderStalePerms / fetchAndRender* are
caught instead of escaping as unhandled rejections
- clearStaleAgent returns the asyncBtn promise so the function is
properly awaitable when a button is present
- Update asyncBtn doc comment to reflect the return-value contract
Wrap the fetch() call in a try/catch so network errors (offline, DNS
failure, CORS) surface via themedToast instead of becoming unhandled
promise rejections. The asyncBtn finally() still restores the button
either way — the catch just adds the missing operator feedback.
add `asyncBtn(btn, fn)` to `@hive/shared/forms.js` as the single
reusable component for async button actions:
1. double-click guard: returns immediately if btn is already disabled
2. saves btn.innerHTML, replaces with spinner while in-flight
3. restores btn on resolve or reject via finally
wire it into all ad-hoc disable/spinner/restore patterns:
- common.js: bindAsyncForms uses asyncBtn internally
- core.js: 'clear perms' button
- permissions.js: clearStaleAgent
- schedules.js: saveSchedule submit, editSchedule submit
- app.js: buildAnswerForm, buildInboxMarkAllRow
fireScheduleNow in schedules.js is left with its existing childNode
save/restore because it shows a custom result flash on the button
content after a successful fire-now (the auto-restore of asyncBtn
would overwrite it); the surrounding themedConfirm dialog already
acts as a natural double-click barrier before the fetch.
saveAll in permissions.js is also left as-is: it uses a custom
'queued ✓' success label + a 900ms delay before re-fetch; the
btn.dataset.busy flag is its own double-submit guard.
add a clippy-discipline note to docs/conventions.md under 'Building &
local checks' and a short pointer bullet in CLAUDE.md so the rule is
visible at first read. covers the three most common patterns that surfaced
in practice (too_many_lines → extract helper, doc_markdown → backticks,
must_use → handle or discard) and gives a concrete worked example
(TurnAccum extraction in stats.rs).
The Reminder loose-end variant's due_at field is serialized as an ISO 8601
string (DateTime<Utc> on the wire), but the JS was doing:
const dueIn = (t.due_at || 0) - now;
A string minus a number is NaN in JS, so fmtAge(NaN) returned 'NaNd',
producing the 'due NaNd overdue' label seen in the screenshot.
Fix: parse the ISO string to unix seconds with new Date(...).getTime() / 1000
before the subtraction.
Adds a 'sessions' chip to the per-agent stats summary panel showing how
many fresh claude sessions started within the selected time window.
Backend (hive-agent/src/stats.rs):
- New optional field `session_count: Option<u64>` on `Snapshot`
(skip_serializing_if = None — inert-until-data, same pattern as
first_turn_ctx). Counts rows in the `sessions` table whose
started_at falls within the window; returns None when the table
doesn't exist on an older db.
- New `read_session_count(conn, from)` helper (rusqlite::Result so the
caller maps Err to None).
- Extracted per-row accumulation loop into `TurnAccum` struct +
`push()` method to keep `snapshot()` under the too_many_lines limit.
Frontend (frontend/packages/agent/src/stats.js):
- New 'sessions' chip added to renderSummary, guarded by
`typeof s.session_count === 'number'`, placed before the
existing first-turn-ctx chip.
The three message-bearing hyperhive tools (send, ask, answer) previously
had named JS branches in renderRichToolUse that each:
- computed a summary string (recipient / line count)
- rendered the body text via detailsOpenMd (marked + DOMPurify)
This commit moves the body text and summary string to the backend,
reducing the JS dispatch table to a single generic markdown path.
Backend (stream_enrich.rs):
- rich_tool_body: new 'markdown' body_type for send/ask/answer —
stamps _body with input.body / input.question / input.answer
- fmt_hyperhive_message_tool: new helper formats _summary as
'send* → to' / 'ask* → to' / 'answer* #id' with ' · NL' when
the body spans multiple lines; extracted out of fmt_hyperhive_tool
to keep it under the too_many_lines limit
- doc: updated rich_tool_body docstring to list the new 'markdown' type
Frontend (app.js):
- Remove the three named branches (send/ask/answer) from renderRichToolUse
- Extend the generic _body path: 'markdown' type calls detailsOpenMd
- The ask-form slot logic (operator inline-answer binding) is preserved
within the markdown branch, now reading the question from c._body
instead of input.question — DOM mounting remains client-side
CI's nix flake check ran in hive-ci's OWN in-container nix-daemon, which
offloads to the pc2 remote builder and HARD-FAILS when pc2 is
unreachable (Connection-reset) — reddening every PR's queue hive-wide.
The host daemon builds fine in the same situation (buildMachines +
max-jobs>=1 + fallback → local build when pc2 is down), and the agent
containers already route through it.
Give hive-ci the same wiring: bind-mount the host nix-daemon socket dir
into the container, set NIX_REMOTE=daemon, and disable the container's
own nix-daemon service + socket. Now CI builds through the host daemon —
pc2 offload when it's up, graceful local fallback when it's down. Drops
the now-moot in-container wait-nix-daemon precond. Needs an operator
rebuild to apply.
The boot provisioning pass (orgs, repos, teams, CI-runner token) all hits
the Forgejo API, but ensure_all only checked the container is *present*,
not that its HTTP is *listening*. A nixos-rebuild that restarts hive-forge
and hive-c0re together races: every ensure_* fired at a refused socket and
left a stale 'provisioning failed' banner that never cleared, since the
pass is one-shot. Poll GET /api/v1/version (unauthenticated) until it
answers, bounded at 1 minute, before provisioning; on timeout proceed
anyway so a genuinely-down forge still surfaces its real errors. Mirrors
the readiness-retry the gateway-nginx path already has.
- fmt_builtin_tool: Edit gets its own arm computing `-N +M` line counts
in _summary (was shared with Read/Write as bare file-path).
- is_rich_tool: drop Write (content is huge/one-sided; flat _summary row
is correct); Edit stays rich since it has an old/new diff.
- rich_tool_body: now returns Option<(String, &'static str)> where the
second field is the body type ('diff' or 'plain'). Edit arm builds the
'-'/'+ ' prefixed diff body; mcp__bash__run gets type 'plain'.
- enrich_tool_use_entry: stamps both _body and _body_type.
Frontend (app.js):
- Remove the Write/Edit branch from renderRichToolUse (~25 lines).
- Generic _body path now dispatches on _body_type: 'diff' ->
api.detailsDiff (colour-coded spans), default -> api.details.
No tool-specific JS remains for file diff rendering.
Backend now stamps `_body` (full `$ <cmd>`) alongside `_summary` (first
line) and `_category: "rich"` for mcp__bash__run tool_use entries. The
frontend drops the mcp__bash__run-specific branch in renderRichToolUse
and uses a generic `if (c._body)` path instead — api.details() with the
backend-computed summary and body, no JS knowledge of the tool name.
Phase 2 of hyperhive#2196. The backend now pre-computes enrichment fields
on every SSE event (stream_enrich.rs); the frontend reads them directly
instead of running its own dispatch logic.
Removed (~330 lines of JS):
- fmtArgsGeneric / TOOL_ICONS / toolIcon / fmtRoom / fmtUser / fmtToolUse
renderStream changes:
- system events: dispatch on v._category (drop/thinking_tok/note/details)
+ v._summary / v._body instead of per-subtype if-chains; status tick
still overrides label client-side when stateName === 'compacting' since
elapsed time is a wall-clock value the backend cannot know at emit time
- tool_use: use c._category === 'rich' for rich-renderer routing,
c._icon / c._summary for flat rows
renderRichToolUse: toolIcon(name) -> c._icon (from backend enrichment)
stream_enrich.rs: also stamp _category: 'drop' on top-level
type=result / type=rate_limit_event so the frontend can use a single
_category check instead of separate type-based early returns
Move per-message rendering logic from frontend JS to backend Rust.
A new stream_enrich::enrich() function stamps display fields onto
LiveEvent::Stream payloads at SSE-emit time (both live tail and history
replay), so the frontend can consume pre-computed fields instead of
re-implementing the dispatch logic in JavaScript.
Phase 1: backend stamps _icon/_summary/_category; client falls back to
its own JS tables when absent. Zero user-visible change.
- system events: _category (drop/thinking_tok/note/details) + _summary
and optional _body (commands_changed expands to a slash-cmd list)
- assistant tool_use entries: _icon + _summary per tool; rich tools
(Write, Edit, send, ask, answer) get _category: 'rich'
- enrichment applied in web_ui/stream.rs at emit time; DB stores raw
events so no migration is needed when enrichment logic changes
- idempotent: existing _-prefixed fields are left unchanged
During a compaction pass, bare `status` ticks from claude are the only
live signal (no dedicated progress event exists in the headless stream-json).
Previously these rendered as a coalescing `⚙ status` row — uninformative
while waiting for a long compact to finish.
The harness emits `TurnStateChanged { state: Compacting, since_unix }`
immediately before invoking `session.compact()` for both the turn-end and
idle-session manual-compact paths. The frontend already tracks this in
`stateName` / `stateSince` for the state badge (`📦 compacting · Xs`);
now the terminal's `status` tick renderer reads the same two variables so
consecutive ticks show `⚙ compact · Xs…` (elapsed, in-place updating via
the existing coalescer) instead of the unhelpful `⚙ status`.
Result: what used to look like
⚙ status
⚙ compact · manual · 37k→2k tokens · 40.1s
now looks like
⚙ compact · 3s… (updating in place)
⚙ compact · manual · 37k→2k tokens · 40.1s
Auto-compact (triggered by hive-claude's InfiniteSession policy internally,
no TurnState::Compacting set by the harness) continues to show `⚙ status`
as before.
Addresses #2276 (follow-up to status-tick coalescing in #2335).
When every resource dep of a node re-enters an ancestor's lock, `owned_reqs`
is empty; the old code still called `acquire(vec![])` and stored a no-op
empty guard in `owned`. Gate the acquire + guard insertion on
`!owned_reqs.is_empty()` — one fewer `borrow_mut` + `HashMap` entry per
fully-re-entrant node in the settle loop. `node_owns` already treats a
missing `owned` entry as non-owning, so behaviour is unchanged.
Make the resource lock unmisusable from outside the crate: the public
surface is now purely declarative (build a Graph with Dep::Resource edges,
configure capacities, run the Scheduler), and the scheduler owns every
acquire/release — a consumer never holds a guard, so it cannot hold the
lock wrong.
- `guard` module + `ResourceTable::try_acquire_all`/`release_all` +
`Graph::set_state` are now `pub(crate)`.
- Remove the dead borrowed-guard layer (`ResourceGuard::borrowed`,
`Acq::Borrowed`, `is_owning`): the scheduler tracks re-entrancy via its
own single borrow slot per (holder, resource) and never constructs a
borrowed guard, so re-entrancy lives in exactly one place. `Acq`
collapses into the owning `ResourceGuard` struct.
- `#[must_use]` on `Scheduler::settle` — ignoring its ids silently drops
runnable work.
- `SharedResources::with` (test-only table observability) is `#[cfg(test)]`.
- Drop the moot borrowed-guard tests; retained owning tests are black-box,
and the redundant `set_state` test helper is gone.
Replace the concrete ResourceName(String) with a type parameter
R: Clone + Eq + Hash threaded end-to-end (Dep<R>, Node<N,R>, Graph<N,R>,
ResourceTable<R>, ResourceGuard<R>/SharedResources<R>, Scheduler<N,R>).
The crate no longer hard-codes the resource identity; the consumer picks
the concrete type (a String, or an enum like BuildSlot/Agent(name)) at
the port. Tests use String as the concrete R. Pure type-parameter
thread-through, no logic change. 25 tests green, clippy pedantic clean.
sync_agent() now returns bool (false if any step fails). ensure_all()
collects the names of agents whose sync failed and raises a single
set_boot_warning with the aggregated list:
forge: per-agent sync failed for: alice, bob (see journal for per-step
detail)
The static_kind() leak is already used for per-org boot warnings in the
same file — the leak is bounded (one per hive-c0re boot, not per request)
so reusing it here is appropriate.
The rebuild call site in job_queue/exec.rs discards the bool return and
keeps its existing tracing::warn! lines, which is the right separation:
rebuilds are their own retry loop and don't need to post a persistent boot
warning.
Extends GET /api/journal/{name} to also accept the four hive infra
container names (hive-ci, hive-forge, hive-gateway, hive-matrix —
hive_priv_sock::InfraContainer is the allowlist), reusing the same
journalctl -M / hive-priv delegation path already used for agent
containers. Infra containers don't run the per-agent hive daemons, so
the unit filter is skipped for them — always the full machine journal.
Frontend: the AGENT tab's agent selector now lists infra containers
in a separate optgroup (sourced from /api/state's existing
infra_containers field), and disables the unit-filter select when one
is chosen.
Both warn!-and-forget sites in ensure_ci_runner_registered() now also
call set_boot_warning():
- fetch_registration_token failure → crit banner (forge unreachable or
API error; runner stays with stale/absent creds)
- hive-priv register_ci_runner failure → crit banner (EROFS or priv
socket error; runner token not written)
Both are only ever invoked from ensure_all() at hive-c0re startup (no
periodic retry), so set_boot_warning() is the right API: the banner
persists until the next c0re restart that re-runs the step, which is
exactly when a config/environment fix (e.g. the ReadWritePaths EROFS
fix from 64075107) would take effect.
Journal warn! lines are kept alongside the banner (belt-and-suspenders).
Remaining in scope for #2289: sync_agent() warn! sites (called from
both startup and rebuild paths — needs set_warning RAII or explicit
return value to enable later success to clear the banner; left for a
follow-up).
Forgejo returns team-already-exists as HTTP 422 ValidationFailed, not
409 Conflict, so the 409-only guard in ensure_operators_team missed it
and logged a spurious warning every boot (and skipped the settings
reconcile). Add a lenient discriminator that also treats a 422 whose
message says already-exists as benign.