Watch
0
0
Fork
You've already forked hyperhive
0

remove hive and agent apis nothing calls: build-log routes, /api/compact, host.sock list, mcp.sock wake

This commit is contained in:
atlas 2026-11-13 02:48:37 +01:00
commit 36c7781245
28 changed files with 115 additions and 801 deletions

View file

@ -152,13 +152,12 @@ Capturing the full stream (rather than a short tail buffer) matters
because real eval errors routinely run long — "tried alternatives" because real eval errors routinely run long — "tried alternatives"
blocks alone are often 30+ lines — so a truncated tail would cut off blocks alone are often 30+ lines — so a truncated tail would cut off
the actual failure and leave only the host journal holding the the actual failure and leave only the host journal holding the
complete output. With this table the dashboard can surface the entire complete output. This table holds the entire log; no HTTP route serves
log. it.
Three indices: Three indices:
- `(agent, started_at)` — backs the per-agent latest-N lookup used - `(agent, started_at)` — per-agent lookup by recency.
by the agent card chip.
- `(status, finished_at)` — backs the retention sweep that runs - `(status, finished_at)` — backs the retention sweep that runs
as part of the existing hourly vacuum. as part of the existing hourly vacuum.
- `(node_id)` — added by a later migration so a build log row can be - `(node_id)` — added by a later migration so a build log row can be

View file

@ -101,10 +101,8 @@ state dirs work as for every other agent.
- **Wire-protocol** — `hive-core-agent-sock`'s unified `Request` enum - **Wire-protocol** — `hive-core-agent-sock`'s unified `Request` enum
is the same on both socket flavours. Container lifecycle is the same on both socket flavours. Container lifecycle
ops (kill/start/restart/rebuild) go through the separate host-admin ops (kill/start/restart/rebuild) go through the separate host-admin
socket `hivectl` speaks. `Wake` (inject a `from: <X>` message straight socket `hivectl` speaks. Matrix, bash and forge wake the agent by pushing a todo on the
into the caller's own inbox) is on this socket too, unprivileged on harness's in-agent socket (see
both flavours; matrix, bash and forge don't call it — their
notifications push a todo on the harness's in-agent socket (see
[`docs/4-implementation/turn-loop/mcp.md`](../turn-loop/mcp.md#waking-the-agent-from-inside-the-container)). [`docs/4-implementation/turn-loop/mcp.md`](../turn-loop/mcp.md#waking-the-agent-from-inside-the-container)).
- **Storage/mounts** — the `manage_root_agent` permission, which the - **Storage/mounts** — the `manage_root_agent` permission, which the
[`ruth` archetype](../../2-handbook/swarm/archetypes.md#the-built-in-archetypes) grants, gets a container [`ruth` archetype](../../2-handbook/swarm/archetypes.md#the-built-in-archetypes) grants, gets a container

View file

@ -8,9 +8,8 @@ Its own long-running per-agent daemon (`hive-forge-notify`, a sibling of
`hive-bash-daemon` / `hive-matrix-daemon`) — it used `hive-bash-daemon` / `hive-matrix-daemon`) — it used
to be a background task inside the `hive-agent` serve loop. Polls to be a background task inside the `hive-agent` serve loop. Polls
`GET /api/v1/notifications?all=false` every 30 seconds (Forgejo's `GET /api/v1/notifications?all=false` every 30 seconds (Forgejo's
unread-only filter), formats each notification as a broker unread-only filter), formats each notification as a todo and pushes it to the agent's own
`Wake { from: "forge" }` message, and delivers it to the agent's own in-agent socket so claude's normal turn loop picks it up.
inbox so claude's normal turn loop picks it up.
The daemon waits about ten minutes for the agent's forge token, then exits 0, The daemon waits about ten minutes for the agent's forge token, then exits 0,
which its `Restart=on-failure` doesn't restart. A path unit which its `Restart=on-failure` doesn't restart. A path unit

View file

@ -276,7 +276,7 @@ Solution: an `nginx http`-context `map $http_accept $matrix_spa_target { ... }`
hive-c0re serves exactly two prefixes, so the dashboard routes by **path** — deterministic, where a content-type split would let one URL resolve differently by the caller's `Accept` header: hive-c0re serves exactly two prefixes, so the dashboard routes by **path** — deterministic, where a content-type split would let one URL resolve differently by the caller's `Accept` header:
- `location /api/` → hive-c0re (`7000`): all dashboard data, actions, and the two SSE streams (`/api/dashboard/stream`, `/api/build-logs/id/{id}/stream`). `proxy_buffering off` and a 1d read timeout keep the streams live. - `location /api/` → hive-c0re (`7000`): all dashboard data, actions, and the SSE stream (`/api/dashboard/stream`). `proxy_buffering off` and a 1d read timeout keep the stream live.
- `location /health/` → hive-c0re: liveness and readiness. - `location /health/` → hive-c0re: liveness and readiness.
- `location /` → the dashboard dist (from the `servedFrontend` nix-store path) with `try_files $uri /index.html`. - `location /` → the dashboard dist (from the `servedFrontend` nix-store path) with `try_files $uri /index.html`.

View file

@ -172,12 +172,12 @@ best-effort — a failed checkpoint or `/compact` never fails the turn that
already succeeded. An ACP agent gets the same policy; how it compacts is in already succeeded. An ACP agent gets the same policy; how it compacts is in
[`hive-runtime/README.md`](../../../hive-runtime/README.md#acp-backend-compaction). [`hive-runtime/README.md`](../../../hive-runtime/README.md#acp-backend-compaction).
The operator can force a compaction any time via `POST /api/compact`. It's The agent can force a compaction via its `compact` MCP tool. It's
**deferred**: the handler sets `Bus::request_compact()` and returns **deferred**: the tool sets `Bus::request_compact()` and returns
immediately; the harness runs the `/compact` at the next turn boundary immediately; the harness runs the `/compact` at the next turn boundary
(end of the in-flight turn in `drive_turn`, or — when the agent is idle — (end of the in-flight turn in `drive_turn`, or — when the agent is idle —
in `turn::run_pending_compact` on the serve loop's next empty poll). This in `turn::run_pending_compact` on the serve loop's next empty poll). This
lets `/api/compact` work mid-turn instead of only when idle, without racing lets the request work mid-turn instead of only when idle, without racing
a live claude process. a live claude process.
To disable proactive compaction for a specific agent, use the nix option: To disable proactive compaction for a specific agent, use the nix option:

View file

@ -102,12 +102,12 @@ hive_name?, swarm_name?, matrix_accounts? }`. `matrix_accounts` is a
`{state_dir}/hyperhive-status`. Pass `""` to clear. Not gated by the `{state_dir}/hyperhive-status`. Pass `""` to clear. Not gated by the
`meta` group — every agent needs to keep its dashboard status chip `meta` group — every agent needs to keep its dashboard status chip
current regardless of which optional groups it holds. current regardless of which optional groups it holds.
- `compact` — agent self-service equivalent of the operator dashboard's - `compact` — queues a compaction of the current session; the only way to
`/compact` button. No args. Gated server-side on the last completed request one. No args. Gated server-side on the last completed
turn's context usage: refused (with an explanation, no side effect) turn's context usage: refused (with an explanation, no side effect)
unless usage is above 66% of the effective context window. On a unless usage is above 66% of the effective context window. On a
pass, queues the same deferred `compact_pending` flag the dashboard pass, queues the deferred `compact_pending` flag — consumed at the end
button sets — consumed at the end of the current turn, so it never of the current turn, so it never
races a live claude process, and the usual pre-compaction races a live claude process, and the usual pre-compaction
notes-checkpoint turn still fires first. Dispatched through the notes-checkpoint turn still fires first. Dispatched through the
in-agent socket (`hive-agent-sock::Request::Compact`), not the in-agent socket (`hive-agent-sock::Request::Compact`), not the
@ -159,15 +159,6 @@ makes the harness signal its own turn loop, with no hive-c0re
round-trip. An external MCP server (or any other in-container process) round-trip. An external MCP server (or any other in-container process)
can push its own todo the same way — `subsystem` is any string. can push its own todo the same way — `subsystem` is any string.
The host-served per-agent socket at `/run/hive/mcp.sock` carries a
lower-level `Wake` request (`hive_core_agent_sock::Request::Wake {
from, body }` — JSON-line, `{"cmd":"wake","from":"matrix","body":"new dm
from @alice"}\n`) that drops the body straight into the broker inbox as
`{from:<label>, to:<agent>, body}`, bypassing the todo layer entirely.
`hive-c0re` handles it; matrix, bash and forge don't call it. Identity = socket: anything that can connect to
`/run/hive/mcp.sock` is implicitly trusted to inject one — the
bind-mount is the agent's own container only.
## Authoritative state ## Authoritative state
`hive-agent`'s `events::Bus` carries the current turn-loop state in `hive-agent`'s `events::Bus` carries the current turn-loop state in
@ -175,7 +166,7 @@ addition to the broadcast channel and the events history. Variants:
- `Idle` — sitting on `Recv` waiting for mail. - `Idle` — sitting on `Recv` waiting for mail.
- `Thinking` — `claude --print` is running for a turn. - `Thinking` — `claude --print` is running for a turn.
- `Compacting` — operator-triggered `/compact` is in flight. - `Compacting` — a `/compact` is in flight.
The harness flips state at the relevant transitions The harness flips state at the relevant transitions
(`set_state(Thinking)` before `drive_turn`, `set_state(Idle)` (`set_state(Thinking)` before `drive_turn`, `set_state(Idle)`

View file

@ -233,20 +233,19 @@ The state badge's menu carries the actions on the agent's claude:
credential files, parks the agent in `needs_login`, and preserves credential files, parks the agent in `needs_login`, and preserves
session history (`~/.claude/projects/`). session history (`~/.claude/projects/`).
`POST /api/compact` sets the deferred `Bus::request_compact()` flag and The agent's `compact` MCP tool sets the deferred `Bus::request_compact()` flag and
returns immediately. The harness runs the `/compact` at the next turn returns immediately. The harness runs the `/compact` at the next turn
boundary (end of the in-flight turn, or `turn::run_pending_compact` when boundary (end of the in-flight turn, or `turn::run_pending_compact` when
idle); output streams into the live panel, which ends with a `/compact done` idle); output streams into the live panel, which ends with a `/compact done`
or `/compact failed: …` note. It works mid-turn, not only when idle. Nothing or `/compact failed: …` note. It works mid-turn, not only when idle.
on this page calls it now.
## Per-agent endpoints ## Per-agent endpoints
Successful POSTs return 200 (no 303 redirects). Error responses Successful POSTs return 200 (no 303 redirects). Error responses
use semantic status codes: **400** for missing/malformed input use semantic status codes: **400** for missing/malformed input
(`/api/todos/mark-done` with no todo ids), **500** only for genuine (`/api/todos/mark-done` with no todo ids), **500** only for genuine
server/transport failures. `/compact` never conflicts: it queues server/transport failures. A queued `/compact` never conflicts: it
and runs at the end of the current turn. The matching runs at the end of the current turn. The matching
mutations fire `LiveEvent` variants on the per-agent bus, so the mutations fire `LiveEvent` variants on the per-agent bus, so the
client doesn't refetch `/api/state` on submit — the SSE stream client doesn't refetch `/api/state` on submit — the SSE stream
delivers the new state faster anyway. delivers the new state faster anyway.
@ -257,11 +256,6 @@ credentials arrive from the swarm secret store.
- `POST /api/cancel` — SIGINT the in-flight claude turn and the - `POST /api/cancel` — SIGINT the in-flight claude turn and the
commands it started. Emits a `LiveEvent::Note`. commands it started. Emits a `LiveEvent::Note`.
- `POST /api/compact` — run `/compact` on the persistent session
(same MCP config + system prompt + allowed tools as a normal
turn — only the stdin payload differs). Flips state to
`Compacting` via `Bus::set_state`, which emits
`TurnStateChanged`.
- `POST /api/new-session` — arm a one-shot for the next turn to - `POST /api/new-session` — arm a one-shot for the next turn to
drop `--continue`. Emits a `LiveEvent::Note`. drop `--continue`. Emits a `LiveEvent::Note`.
- `POST /api/logout` — three-step teardown that re-uses the - `POST /api/logout` — three-step teardown that re-uses the

View file

@ -69,7 +69,7 @@ conflicting agent.
Passive / rare-interaction state. `kept.js` renders it from the Passive / rare-interaction state. `kept.js` renders it from the
`/api/state` snapshot and keeps it live from the `tombstones_changed` `/api/state` snapshot and keeps it live from the `tombstones_changed`
stream event. (The rebuild queue and build logs are dashboard tabs too — stream event. (The rebuild queue is a dashboard tab too —
see below.) see below.)
Per-container CPU, memory and disk load isn't shown in the hive UI. The Per-container CPU, memory and disk load isn't shown in the hive UI. The
@ -433,27 +433,6 @@ below — some endpoints aren't in it yet.
not yet delivered and sets `acked_at = now`. Returns not yet delivered and sets `acked_at = now`. Returns
`{ "marked": N }`. Agent name validated against `{ "marked": N }`. Agent name validated against
`[a-z0-9_-]`, 1-63 chars; 400 on bad input. `[a-z0-9_-]`, 1-63 chars; 400 on bad input.
- `GET /api/build-logs?limit=N` — most-recent build log headers across
all agents (`limit` defaults to 30).
- `GET /api/build-log/{node_id}` and `/api/build-log/{node_id}/raw` — the
build log of one job-queue node, as JSON and as `text/plain`.
- `GET /api/build-logs/{agent}?limit=N` — most-recent build log
headers for one agent, newest first. Returns
`Vec<BuildLogHeader>` (JSON): `id`, `agent`, `kind`, `cmdline`,
`started_at`, `finished_at`, `status` (`"ok"` / `"fail"` /
`null` while in-progress). `limit` defaults to 10, server-side
cap at 50. Agent name validated (`[a-z0-9_-]`, 1-63 chars).
- `GET /api/build-logs/id/{id}` — full build log by id. Returns
`BuildLogFull` (JSON): all header fields plus `stdout` and
`stderr` as plain text (newline-terminated lines, utf-8). HTTP
404 when the row is missing (vacuum-reaped or stale id).
- `GET /api/build-logs/id/{id}/stream` — live SSE stream of
stdout+stderr for an in-progress build (newline-delimited JSON
frames). The
stream closes when the build finishes or errors.
- `GET /api/build-logs/id/{id}/raw` — raw concatenated
stdout+stderr as `text/plain`. Works on both finished and in-progress
build logs.
- `GET /api/dashboard/stream` — unified live event channel: - `GET /api/dashboard/stream` — unified live event channel:
broker `sent` / `delivered`, plus the mutation events listed broker `sent` / `delivered`, plus the mutation events listed

View file

@ -66,24 +66,17 @@ No auth tokens exist on the per-agent unix sockets. The socket
bind-mount." A sub-agent only sees its own `/run/hive/mcp.sock`; bind-mount." A sub-agent only sees its own `/run/hive/mcp.sock`;
hive-c0re owns the host admin socket. hive-c0re owns the host admin socket.
### Wake injection Matrix, bash and forge wake the agent by upserting a todo on the harness's
in-agent socket (`HIVE_AGENT_SOCKET`), which signals the turn loop without a
`hive_core_agent_sock::Request::Wake { from, body }` is the
wake-event-injection surface on the host-served per-agent socket.
Recipient is implicit — the agent the socket belongs to — and `from`
is caller-chosen so the wake prompt can label the source verbatim
(`"matrix: new message in #general"`, `"forge: PR #42 opened"`, etc.). <!-- lint:allow: example strings, not real tags -->
`hive-c0re` handles it; matrix, bash and forge don't call it — their
notifications upsert a todo on the harness's in-agent socket
(`HIVE_AGENT_SOCKET`), which signals the same turn loop without a
hive-c0re round-trip. See hive-c0re round-trip. See
[`docs/4-implementation/turn-loop/mcp.md` § Waking the agent from inside the [`docs/4-implementation/turn-loop/mcp.md` § Waking the agent from inside the
container](../../4-implementation/turn-loop/mcp.md#waking-the-agent-from-inside-the-container). container](../../4-implementation/turn-loop/mcp.md#waking-the-agent-from-inside-the-container).
The host-served `/run/hive/mcp.sock` has no verb that injects a message into
the agent's own inbox.
Identity = socket means anything that can connect to Anything that can connect to `/run/hive/mcp.sock` is implicitly trusted to
`/run/hive/mcp.sock` is implicitly trusted to inject wakes. That's call its verbs. That's fine: the bind-mount only exposes the socket inside the
fine: the bind-mount only exposes the socket inside the agent's own agent's own container, so the trust boundary is the container's process
container, so the trust boundary is the container's process
namespace, not the wire surface. namespace, not the wire surface.
## Recipient sentinels ## Recipient sentinels

View file

@ -59,7 +59,7 @@ const STATE_TOOLTIPS: Record<string, string> = {
offline: "harness unreachable or claude not logged in", offline: "harness unreachable or claude not logged in",
idle: "turn loop running, no claude invocation in flight", idle: "turn loop running, no claude invocation in flight",
thinking: "claude is executing the current turn", thinking: "claude is executing the current turn",
compacting: "operator-triggered /compact running on the persistent session", compacting: "agent-requested /compact running on the persistent session",
}; };
// `total` here matches app.js's `renderOneUsage` exactly: input + both // `total` here matches app.js's `renderOneUsage` exactly: input + both

View file

@ -123,11 +123,11 @@ pub enum Request {
/// `ReminderRollup` against the broker; now served from the local store). /// `ReminderRollup` against the broker; now served from the local store).
ReminderRollup { since_secs: u64 }, ReminderRollup { since_secs: u64 },
/// Agent self-service request to compact the current session, mirroring /// Agent self-service request to compact the current session, mirroring
/// the operator's `/compact` dashboard button. Gated server-side: only /// the `compact` MCP tool. Gated server-side: only
/// honoured when the last completed turn's context usage is above 66% /// honoured when the last completed turn's context usage is above 66%
/// of the effective context window — below that, `Response::Err` /// of the effective context window — below that, `Response::Err`
/// explains why and takes no action. On a pass, queues the same /// explains why and takes no action. On a pass, queues the same
/// deferred `compact_pending` flag the operator's button sets (consumed /// deferred `compact_pending` flag (consumed
/// at the next turn boundary), so it never races a live claude process. /// at the next turn boundary), so it never races a live claude process.
/// `wake_prompt`, when set, is driven as a synthetic follow-up turn once /// `wake_prompt`, when set, is driven as a synthetic follow-up turn once
/// the compaction actually finishes — the dashboard button's own /// the compaction actually finishes — the dashboard button's own

View file

@ -251,8 +251,7 @@ pub enum TurnState {
Idle, Idle,
/// `claude --print` is running for a turn. /// `claude --print` is running for a turn.
Thinking, Thinking,
/// Operator-triggered `/compact` is running on the persistent /// A `/compact` is running on the persistent session.
/// session.
Compacting, Compacting,
} }
@ -316,9 +315,8 @@ pub struct Bus {
/// boundary keeps compaction from racing a live claude process mid-turn. /// boundary keeps compaction from racing a live claude process mid-turn.
/// `Some(request)` when a compact is pending; `request.wake_prompt` is /// `Some(request)` when a compact is pending; `request.wake_prompt` is
/// what to drive as a synthetic follow-up turn once the compaction /// what to drive as a synthetic follow-up turn once the compaction
/// actually completes (`None` = pending but no follow-up wake wanted, /// actually completes (`None` = pending but no follow-up wake wanted). `None` = no
/// e.g. the operator dashboard's `/compact` button). `None` = no compact /// compact pending. Wrapped in [`CompactRequest`] rather than
/// pending. Wrapped in [`CompactRequest`] rather than
/// `Option<Option<String>>` (clippy pedantic's `option_option` lint, /// `Option<Option<String>>` (clippy pedantic's `option_option` lint,
/// and the named field reads clearer at call sites than a bare nested /// and the named field reads clearer at call sites than a bare nested
/// `Option`) so "pending" and "what to wake with" can never desync. /// `Option`) so "pending" and "what to wake with" can never desync.
@ -491,8 +489,7 @@ impl Bus {
/// serviced just overwrites `wake_prompt` with the latest ask. `Some /// serviced just overwrites `wake_prompt` with the latest ask. `Some
/// (wake_prompt)` schedules a synthetic follow-up turn (driven with /// (wake_prompt)` schedules a synthetic follow-up turn (driven with
/// `wake_prompt` as its body) once the compaction actually completes; /// `wake_prompt` as its body) once the compaction actually completes;
/// `None` requests a plain compact with no follow-up wake (the operator /// `None` requests a plain compact with no follow-up wake.
/// dashboard's `/compact` button).
pub fn request_compact(&self, wake_prompt: Option<String>) { pub fn request_compact(&self, wake_prompt: Option<String>) {
*self.compact_pending.lock().unwrap() = Some(CompactRequest { wake_prompt }); *self.compact_pending.lock().unwrap() = Some(CompactRequest { wake_prompt });
} }

View file

@ -11,9 +11,8 @@
//! reminder op then returns `Response::Err`); a reminder *firing* is a //! reminder op then returns `Response::Err`); a reminder *firing* is a
//! separate path (`reminder_timer`), not driven through this socket. //! separate path (`reminder_timer`), not driven through this socket.
//! `Request::Compact` is the odd one out — it doesn't touch any store, just //! `Request::Compact` is the odd one out — it doesn't touch any store, just
//! the harness's [`Bus`] (gate-checked context usage, then the same //! the harness's [`Bus`] (gate-checked context usage, then the deferred
//! deferred `compact_pending` flag the operator dashboard's `/compact` //! `compact_pending` flag). No hive-c0re round-trip, no broker long-poll, no marker
//! button sets). No hive-c0re round-trip, no broker long-poll, no marker
//! files. //! files.
//! //!
//! One request/response line per connection, matching the producers' //! One request/response line per connection, matching the producers'
@ -39,8 +38,7 @@ use crate::todos::{Todo, Todos};
/// Context-usage floor for `Request::Compact`: below this fraction of the /// Context-usage floor for `Request::Compact`: below this fraction of the
/// effective context window, an agent-initiated compact is refused (nothing /// effective context window, an agent-initiated compact is refused (nothing
/// meaningful to reclaim yet, and it'd just burn a compaction pass on a /// meaningful to reclaim yet, and it'd just burn a compaction pass on a
/// near-empty session). Same number a human operator would eyeball off the /// near-empty session).
/// dashboard's `ctx·Nk` badge before hitting the `/compact` button.
const COMPACT_MIN_USAGE_FRACTION: f64 = 0.66; const COMPACT_MIN_USAGE_FRACTION: f64 = 0.66;
/// Pause after a failed `accept`, so a failure that persists (fd exhaustion) /// Pause after a failed `accept`, so a failure that persists (fd exhaustion)
@ -434,13 +432,10 @@ fn mark_todos_done(store: &Todos, ids: &[i64]) -> Response {
} }
} }
/// `Request::Compact` handler: gate on context usage, then queue the same /// `Request::Compact` handler: gate on context usage, then queue the
/// deferred `compact_pending` flag the operator's `/compact` button sets. /// deferred `compact_pending` flag. Refuses below
/// Mirrors `hive-agent::web_ui::actions::post_compact` but reachable from /// [`COMPACT_MIN_USAGE_FRACTION`] because an agent can call this
/// the agent's own MCP tool instead of the dashboard, and refuses below /// speculatively. `wake_prompt`, when set, is
/// [`COMPACT_MIN_USAGE_FRACTION`] instead of always honouring the request —
/// an agent can call this speculatively, a human clicking the dashboard
/// button already made the judgment call. `wake_prompt`, when set, is
/// forwarded to `Bus::request_compact` so the turn loop drives a synthetic /// forwarded to `Bus::request_compact` so the turn loop drives a synthetic
/// follow-up turn once the compaction actually finishes. /// follow-up turn once the compaction actually finishes.
fn compact(bus: &Bus, wake_prompt: Option<String>) -> Response { fn compact(bus: &Bus, wake_prompt: Option<String>) -> Response {

View file

@ -92,7 +92,7 @@ and end the turn.";
/// passed here — they live at `/etc/claude-code/managed-settings.json` /// passed here — they live at `/etc/claude-code/managed-settings.json`
/// and claude auto-discovers them. /// and claude auto-discovers them.
/// Materialised at harness startup; shared between the turn loop and the /// Materialised at harness startup; shared between the turn loop and the
/// operator-driven `/compact` path so both invocations look identical to /// `/compact` path so both invocations look identical to
/// claude (same MCP surface, same allowed tools, same role prompt — only the /// claude (same MCP surface, same allowed tools, same role prompt — only the
/// stdin payload differs). The MCP config is rewritten before each /// stdin payload differs). The MCP config is rewritten before each
/// invocation ([`TurnFiles::refresh_mcp_config`]), since which servers it /// invocation ([`TurnFiles::refresh_mcp_config`]), since which servers it
@ -161,7 +161,7 @@ pub async fn write_system_prompt(socket: &Path, label: &str) -> Result<PathBuf>
/// One claude turn's outcome: `Ok(compacted)` on success, or a [`TurnError`] /// One claude turn's outcome: `Ok(compacted)` on success, or a [`TurnError`]
/// the serve loop must act on. The `compacted` bool is `true` when a /// the serve loop must act on. The `compacted` bool is `true` when a
/// compaction ran this turn (reactively on overflow, or proactively per the /// compaction ran this turn (reactively on overflow, or proactively per the
/// policy — or a successful operator `/compact` at turn end); it's recorded as /// policy — or a successful agent-requested `/compact` at turn end); it's recorded as
/// `result_kind = "compacted"` in turn stats so the stats page can distinguish /// `result_kind = "compacted"` in turn stats so the stats page can distinguish
/// those turns. Both `Ok(true)` and `Ok(false)` are ack'd; the error cases /// those turns. Both `Ok(true)` and `Ok(false)` are ack'd; the error cases
/// each map to a distinct serve-loop action (see [`emit_turn_end`] and the /// each map to a distinct serve-loop action (see [`emit_turn_end`] and the
@ -452,8 +452,7 @@ pub async fn drive_turn(
archive_session(bus, session); archive_session(bus, session);
return Err(TurnError::PromptTooLong); return Err(TurnError::PromptTooLong);
} }
// Operator `/compact` (`POST /api/compact`) or an agent's own `compact` // An agent's own `compact` MCP tool call, deferred to the turn boundary: run it now that the turn
// MCP tool call, deferred to the turn boundary: run it now that the turn
// is done, so it works mid-turn rather than only when the agent is idle. // is done, so it works mid-turn rather than only when the agent is idle.
// Only on a healthy turn — no point spawning a compaction after a // Only on a healthy turn — no point spawning a compaction after a
// rate-limited / auth-failed / crashed one. `is_ok()` first: `take_compact()` // rate-limited / auth-failed / crashed one. `is_ok()` first: `take_compact()`
@ -485,7 +484,7 @@ pub async fn drive_turn(
} }
/// Emit the `/compact done` / `/compact failed: …` note for a finished /// Emit the `/compact done` / `/compact failed: …` note for a finished
/// operator or agent `/compact` and return whether it succeeded. Shared by the /// agent-requested `/compact` and return whether it succeeded. Shared by the
/// turn-end path in [`drive_turn`] and the idle path in [`run_pending_compact`]. /// turn-end path in [`drive_turn`] and the idle path in [`run_pending_compact`].
fn report_compact<E: std::fmt::Display>(bus: &Bus, result: Result<(), E>) -> bool { fn report_compact<E: std::fmt::Display>(bus: &Bus, result: Result<(), E>) -> bool {
match result { match result {
@ -620,7 +619,7 @@ pub fn emit_turn_end(bus: &Bus, outcome: &TurnOutcome) {
} }
} }
/// Service a pending operator `/compact` (`Bus::request_compact`) while the /// Service a pending `/compact` (`Bus::request_compact`) while the
/// agent is idle — the serve loop calls this when a `recv` returns no message, /// agent is idle — the serve loop calls this when a `recv` returns no message,
/// so a queued `/compact` runs even when no turn is driving. (The in-flight /// so a queued `/compact` runs even when no turn is driving. (The in-flight
/// case is handled at the end of [`drive_turn`].) Resume-only via /// case is handled at the end of [`drive_turn`].) Resume-only via

View file

@ -1,4 +1,4 @@
//! Operator action POST handlers (cancel, compact, reset, todos mark-done). //! Operator action POST handlers (cancel, reset, todos mark-done).
use axum::{ use axum::{
Form, Form,
@ -16,22 +16,6 @@ pub(super) async fn post_cancel_turn(State(state): State<AppState>) -> Response
(axum::http::StatusCode::OK, "ok").into_response() (axum::http::StatusCode::OK, "ok").into_response()
} }
/// Operator-initiated `/compact`. Deferred: sets the `compact_pending` flag
/// that `turn::drive_turn` consumes at the end of the current/next turn, so it
/// works while a turn is in flight (a mid-turn compaction would race the live
/// claude process) rather than only when the agent is idle. Returns 200
/// immediately; the compaction stream lands in the live panel when it runs.
pub(super) async fn post_compact(State(state): State<AppState>) -> Response {
// No wake prompt: the operator is watching the dashboard, not waiting on
// an inbox message — `request_compact`'s wake-prompt arg exists for the
// agent's own `compact` MCP tool (`todo_server.rs::compact`).
state.bus.request_compact(None);
state.bus.emit(crate::events::LiveEvent::Note {
text: "operator: /compact queued — runs at the end of the current turn".into(),
});
(axum::http::StatusCode::OK, "ok").into_response()
}
/// Request a session reset. The current session is archived (its backing /// Request a session reset. The current session is archived (its backing
/// `<uuid>.jsonl` renamed out of claude's resolution glob) at the next turn /// `<uuid>.jsonl` renamed out of claude's resolution glob) at the next turn
/// boundary, so the following turn's `--resume` misses and self-heals into a /// boundary, so the following turn's `--resume` misses and self-heals into a

View file

@ -76,7 +76,6 @@ fn routes() -> Router<AppState> {
.route("/events/stream", get(stream::events_stream)) .route("/events/stream", get(stream::events_stream))
.route("/events/history", get(stream::events_history)) .route("/events/history", get(stream::events_history))
.route("/api/cancel", post(actions::post_cancel_turn)) .route("/api/cancel", post(actions::post_cancel_turn))
.route("/api/compact", post(actions::post_compact))
.route("/api/new-session", post(actions::post_new_session)) .route("/api/new-session", post(actions::post_new_session))
.route("/api/logout", post(auth::post_logout)) .route("/api/logout", post(auth::post_logout))
.route("/api/todos", get(stats::api_todos)) .route("/api/todos", get(stats::api_todos))
@ -532,7 +531,7 @@ mod tests {
/// either. /// either.
#[test] #[test]
fn neither_model_nor_effort_has_a_route() { fn neither_model_nor_effort_has_a_route() {
let (model, effort, compact) = crate::paths::with_harness_dir(|| { let (model, effort, existing) = crate::paths::with_harness_dir(|| {
let state = AppState { let state = AppState {
label: "test".to_owned(), label: "test".to_owned(),
login: Arc::new(Mutex::new(LoginState::Online)), login: Arc::new(Mutex::new(LoginState::Online)),
@ -550,17 +549,17 @@ mod tests {
let effort = app.clone().oneshot(form("/api/effort", "effort=high")); let effort = app.clone().oneshot(form("/api/effort", "effort=high"));
// A route the router does serve, so the 404s above are not // A route the router does serve, so the 404s above are not
// the router refusing everything. // the router refusing everything.
let compact = app.oneshot(form("/api/compact", "")); let existing = app.oneshot(form("/api/new-session", ""));
( (
model.await.unwrap(), model.await.unwrap(),
effort.await.unwrap(), effort.await.unwrap(),
compact.await.unwrap(), existing.await.unwrap(),
) )
}) })
}); });
assert_eq!(model.status(), StatusCode::NOT_FOUND); assert_eq!(model.status(), StatusCode::NOT_FOUND);
assert_eq!(effort.status(), StatusCode::NOT_FOUND); assert_eq!(effort.status(), StatusCode::NOT_FOUND);
assert_eq!(compact.status(), StatusCode::OK); assert_eq!(existing.status(), StatusCode::OK);
} }
/// Poll `path` until it holds a complete, newline-terminated pid. /// Poll `path` until it holds a complete, newline-terminated pid.

View file

@ -25,9 +25,7 @@ pub struct Coordinator {
pub broker: Arc<Broker>, pub broker: Arc<Broker>,
/// Full build-log capture. `lifecycle::run` `start()`s a row per attempt, /// Full build-log capture. `lifecycle::run` `start()`s a row per attempt,
/// pipes every stdout/stderr line into it, and `finish()`es it on /// pipes every stdout/stderr line into it, and `finish()`es it on
/// child exit. Dashboard reads it via `list_recent_for_agent` / /// child exit. See `build_logs.rs` for retention.
/// `get_full` for the per-card chip + side-panel viewer. See
/// `build_logs.rs` for retention.
pub build_logs: Arc<crate::build_logs::BuildLogs>, pub build_logs: Arc<crate::build_logs::BuildLogs>,
/// Store path of the hyperhive flake source (no fragment). Used to /// Store path of the hyperhive flake source (no fragment). Used to
/// identify the running revision (`current_flake_rev`). /// identify the running revision (`current_flake_rev`).
@ -1061,7 +1059,7 @@ mod open_tests {
let coord = open(&db); let coord = open(&db);
let row = coord.build_logs.get_full(id).expect("get").expect("Some"); let row = coord.build_logs.get_full(id).expect("get").expect("Some");
assert_eq!(row.header.status.as_deref(), Some("fail")); assert_eq!(row.status.as_deref(), Some("fail"));
assert!(row.header.finished_at.is_some()); assert!(row.finished_at.is_some());
} }
} }

View file

@ -1,365 +0,0 @@
//! Build-log endpoints for the dashboard.
//!
//! Header lists (all-agents + per-agent), the full row by id, a `text/plain`
//! download, and an SSE stream that delivers incremental stdout/stderr while
//! a build runs (closing once it finishes or the row is vacuum-reaped).
use std::convert::Infallible;
use axum::{
extract::{Path as AxumPath, State},
http::StatusCode,
response::{
IntoResponse, Response,
sse::{Event, KeepAlive, Sse},
},
};
use serde::{Deserialize, Serialize};
use tokio_stream::Stream;
use tokio_stream::wrappers::ReceiverStream;
use utoipa::IntoParams;
use super::{AppState, Ident, error_response};
use crate::build_logs::{BuildLogFull, BuildLogHeader};
#[derive(Deserialize, IntoParams)]
pub(super) struct BuildLogsAllQuery {
/// Max rows to return. Capped at 100. Default 30.
#[serde(default)]
limit: Option<usize>,
}
/// Most-recent build log headers across
/// all agents, newest first.
///
/// Same JSON shape as the per-agent endpoint.
#[utoipa::path(
get,
path = "/api/build-logs",
params(BuildLogsAllQuery),
responses(
(status = 200, description = "recent build log headers, newest first", body = Vec<BuildLogHeader>),
(status = 500, description = "sqlite read failed"),
),
tag = "build_logs"
)]
pub(super) async fn get_build_logs_all(
State(state): State<AppState>,
axum::extract::Query(q): axum::extract::Query<BuildLogsAllQuery>,
) -> Response {
let limit = q.limit.unwrap_or(30);
match state.coord.build_logs.list_recent_all(limit) {
Ok(rows) => axum::Json(rows).into_response(),
Err(e) => error_response(&format!("build-logs all: {e:#}")),
}
}
#[derive(Deserialize, IntoParams)]
pub(super) struct BuildLogsQuery {
/// Maximum number of rows to return. Capped server-side at 50
/// (see `build_logs::list_recent_for_agent`). Default 10.
#[serde(default)]
limit: Option<usize>,
}
/// Most-recent build log
/// headers for one agent, newest first.
///
/// Returns `Vec<BuildLogHeader>` (JSON). Backs the per-agent log chip
/// in the agent card and the side-panel header list.
#[utoipa::path(
get,
path = "/api/build-logs/{agent}",
params(
("agent" = String, Path, description = "agent name"),
BuildLogsQuery,
),
responses(
(status = 200, description = "recent build log headers for the agent, newest first", body = Vec<BuildLogHeader>),
(status = 400, description = "bad agent name"),
(status = 500, description = "sqlite read failed"),
),
tag = "build_logs"
)]
pub(super) async fn get_build_logs_agent(
State(state): State<AppState>,
AxumPath(name): AxumPath<String>,
axum::extract::Query(q): axum::extract::Query<BuildLogsQuery>,
) -> Response {
let name = match Ident::parse(&name) {
Ok(n) => n,
Err(reason) => {
return (StatusCode::BAD_REQUEST, format!("bad agent name: {reason}")).into_response();
}
};
let limit = q.limit.unwrap_or(10);
match state
.coord
.build_logs
.list_recent_for_agent(name.as_str(), limit)
{
Ok(rows) => axum::Json(rows).into_response(),
Err(e) => error_response(&format!("build-logs {name}: {e:#}")),
}
}
/// Full build log row (stdout +
/// stderr concatenated) by id.
///
/// Returns `BuildLogFull` (JSON), or HTTP 404 when the id doesn't
/// exist (vacuum-reaped, or the operator passed a stale id from a
/// refresh race).
#[utoipa::path(
get,
path = "/api/build-logs/id/{id}",
params(("id" = i64, Path, description = "build log row id")),
responses(
(status = 200, description = "full build log row", body = BuildLogFull),
(status = 404, description = "no such build log row"),
(status = 500, description = "sqlite read failed"),
),
tag = "build_logs"
)]
pub(super) async fn get_build_log_full(
State(state): State<AppState>,
AxumPath(id): AxumPath<i64>,
) -> Response {
match state.coord.build_logs.get_full(id) {
Ok(Some(log)) => axum::Json(log).into_response(),
Ok(None) => (StatusCode::NOT_FOUND, format!("build log #{id} not found")).into_response(),
Err(e) => error_response(&format!("build-log {id}: {e:#}")),
}
}
/// The build log for a **queue node**,
/// resolved node id → log-row id → full log.
///
/// Same `BuildLogFull` JSON (`stdout` / `stderr` + header) as
/// `get_build_log_full`; HTTP 404 when the node has no linked log (the
/// client gates the request on the wire node's `build_log_id`, but a vacuum
/// race can still 404). This is the on-demand live-log-panel fetch,
/// distinct from the `build_log_id` on the wire — that id is for
/// deep-linking to the BUILD L0GS tab's full history view, not for
/// fetching the log content itself.
#[utoipa::path(
get,
path = "/api/build-log/{node_id}",
params(("node_id" = u64, Path, description = "job-queue node id")),
responses(
(status = 200, description = "full build log row for the node's linked log", body = BuildLogFull),
(status = 404, description = "node has no linked build log, or the log row is gone"),
(status = 500, description = "sqlite read failed"),
),
tag = "build_logs"
)]
pub(super) async fn get_build_log_for_node(
State(state): State<AppState>,
AxumPath(node_id): AxumPath<u64>,
) -> Response {
match state.coord.build_logs.id_for_node(node_id) {
Some(log_id) => get_build_log_full(State(state), AxumPath(log_id)).await,
None => (
StatusCode::NOT_FOUND,
format!("node #{node_id} has no build log"),
)
.into_response(),
}
}
/// The node's build log as `text/plain`
/// for download (delegates to `get_build_log_raw` after resolving the node id).
#[utoipa::path(
get,
path = "/api/build-log/{node_id}/raw",
params(("node_id" = u64, Path, description = "job-queue node id")),
responses(
(status = 200, description = "build log text for download", body = String, content_type = "text/plain"),
(status = 404, description = "node has no linked build log, or the log row is gone"),
(status = 500, description = "sqlite read failed"),
),
tag = "build_logs"
)]
pub(super) async fn get_build_log_raw_for_node(
State(state): State<AppState>,
AxumPath(node_id): AxumPath<u64>,
) -> Response {
match state.coord.build_logs.id_for_node(node_id) {
Some(log_id) => get_build_log_raw(State(state), AxumPath(log_id)).await,
None => (
StatusCode::NOT_FOUND,
format!("node #{node_id} has no build log"),
)
.into_response(),
}
}
/// JSON frame sent on the `/api/build-logs/id/{id}/stream` SSE channel.
/// `stdout_append` / `stderr_append` carry only the new bytes since the
/// last frame; `done = true` means the build finished and the stream
/// will close after this frame.
#[derive(Serialize)]
struct BuildLogFrame {
stdout_append: String,
stderr_append: String,
#[serde(skip_serializing_if = "Option::is_none")]
status: Option<String>,
done: bool,
}
/// SSE stream that delivers
/// incremental stdout/stderr as a build runs.
///
/// The client connects when it opens a running-build panel; the
/// stream closes automatically once the build finishes (or the row
/// disappears due to a vacuum). Each frame is a JSON-serialised
/// `BuildLogFrame`. The first frame always carries the full
/// accumulated log so far (cursors start at 0); subsequent frames
/// carry only new bytes. `done: true` on the final frame signals the
/// browser to close the `EventSource`.
#[utoipa::path(
get,
path = "/api/build-logs/id/{id}/stream",
params(("id" = i64, Path, description = "build log row id")),
responses(
(status = 200, description = "server-sent event stream; each event's \
`data` is a JSON-serialised `BuildLogFrame` \
(stdout_append/stderr_append/status/done)",
body = String, content_type = "text/event-stream"),
),
tag = "build_logs"
)]
pub(super) async fn get_build_log_stream(
State(state): State<AppState>,
AxumPath(id): AxumPath<i64>,
) -> Sse<impl Stream<Item = Result<Event, Infallible>>> {
let (tx, rx) = tokio::sync::mpsc::channel::<Result<Event, Infallible>>(32);
let logs = state.coord.build_logs.clone();
tokio::spawn(async move {
let mut notify_rx = logs.subscribe_notifications();
let mut stdout_cursor = 0usize;
let mut stderr_cursor = 0usize;
// ── initial snapshot ──────────────────────────────────────────
match logs.get_progress(id, stdout_cursor, stderr_cursor) {
Ok(Some(prog)) => {
stdout_cursor += prog.stdout_append.len();
stderr_cursor += prog.stderr_append.len();
let done = prog.finished_at.is_some();
if let Ok(json) = serde_json::to_string(&BuildLogFrame {
stdout_append: prog.stdout_append,
stderr_append: prog.stderr_append,
status: prog.status,
done,
}) {
let _ = tx.send(Ok(Event::default().data(json))).await;
}
if done {
return;
}
}
Ok(None) => {
// Row missing — send a single error event and exit.
let _ = tx
.send(Ok(Event::default()
.event("error")
.data(format!("build log #{id} not found"))))
.await;
return;
}
Err(e) => {
let _ = tx
.send(Ok(Event::default()
.event("error")
.data(format!("build log #{id}: {e:#}"))))
.await;
return;
}
}
// ── live delta loop ───────────────────────────────────────────
loop {
match notify_rx.recv().await {
// Notification for a different build — ignore and wait
// for the next one.
Ok(notif_id) if notif_id != id => {}
Ok(_) => {
match logs.get_progress(id, stdout_cursor, stderr_cursor) {
Ok(Some(prog)) => {
stdout_cursor += prog.stdout_append.len();
stderr_cursor += prog.stderr_append.len();
let done = prog.finished_at.is_some();
if let Ok(json) = serde_json::to_string(&BuildLogFrame {
stdout_append: prog.stdout_append,
stderr_append: prog.stderr_append,
status: prog.status,
done,
}) && tx.send(Ok(Event::default().data(json))).await.is_err()
{
return; // browser disconnected
}
if done {
return;
}
}
Ok(None) | Err(_) => return, // vacuum reaped row / channel closed
}
}
Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => {}
Err(tokio::sync::broadcast::error::RecvError::Closed) => return,
}
}
});
Sse::new(ReceiverStream::new(rx)).keep_alive(KeepAlive::default())
}
/// Full log as `text/plain` for
/// download.
///
/// Stdout and stderr are concatenated with a `--- stderr ---`
/// separator (same layout the JS side-panel renders). The
/// `Content-Disposition` header triggers a browser download with a
/// descriptive filename so the operator can save and share the log.
#[utoipa::path(
get,
path = "/api/build-logs/id/{id}/raw",
params(("id" = i64, Path, description = "build log row id")),
responses(
(status = 200, description = "build log text for download", body = String, content_type = "text/plain"),
(status = 404, description = "no such build log row"),
(status = 500, description = "sqlite read failed"),
),
tag = "build_logs"
)]
pub(super) async fn get_build_log_raw(
State(state): State<AppState>,
AxumPath(id): AxumPath<i64>,
) -> Response {
match state.coord.build_logs.get_full(id) {
Ok(Some(log)) => {
let mut text = log.stdout;
if !log.stderr.is_empty() {
text.push_str("\n--- stderr ---\n");
text.push_str(&log.stderr);
}
(
StatusCode::OK,
[
("content-type", "text/plain; charset=utf-8".to_string()),
(
"content-disposition",
format!(
"attachment; filename=\"build-log-{}-{}.txt\"",
log.header.agent, id
),
),
],
text,
)
.into_response()
}
Ok(None) => (StatusCode::NOT_FOUND, format!("build log #{id} not found")).into_response(),
Err(e) => error_response(&format!("build-log {id}: {e:#}")),
}
}

View file

@ -35,7 +35,6 @@ use crate::lifecycle;
), ),
tags( tags(
(name = "health", description = "hive-wide liveness/readiness probes"), (name = "health", description = "hive-wide liveness/readiness probes"),
(name = "build_logs", description = "build log headers, full rows, and raw text downloads"),
(name = "lifecycle_ops", description = "agent container lifecycle: rebuild/restart/start/stop/pause/limits"), (name = "lifecycle_ops", description = "agent container lifecycle: rebuild/restart/start/stop/pause/limits"),
(name = "misc_api", description = "mark-all-read, compose, spawn-request"), (name = "misc_api", description = "mark-all-read, compose, spawn-request"),
@ -46,7 +45,6 @@ use crate::lifecycle;
)] )]
struct ApiDoc; struct ApiDoc;
mod build_logs;
// The single validated identifier type — homed in `hive-host-sock` (the crate // The single validated identifier type — homed in `hive-host-sock` (the crate
// owning agent-path facts) so every dashboard path-param validates through the // owning agent-path facts) so every dashboard path-param validates through the
// same type used to build agent paths. Re-exported so submodules + the socket // same type used to build agent paths. Re-exported so submodules + the socket
@ -104,12 +102,6 @@ fn app(coord: Arc<Coordinator>) -> axum::Router {
.routes(routes!(health::get_health_ready)) .routes(routes!(health::get_health_ready))
.routes(routes!(state_snapshot::api_state)) .routes(routes!(state_snapshot::api_state))
.routes(routes!(misc_api::post_mark_all_read)) .routes(routes!(misc_api::post_mark_all_read))
.routes(routes!(build_logs::get_build_logs_all))
.routes(routes!(build_logs::get_build_log_for_node))
.routes(routes!(build_logs::get_build_log_raw_for_node))
.routes(routes!(build_logs::get_build_logs_agent))
.routes(routes!(build_logs::get_build_log_full))
.routes(routes!(build_logs::get_build_log_raw))
.routes(routes!(rebuild_queue::post_rebuild_queue_cancel)) .routes(routes!(rebuild_queue::post_rebuild_queue_cancel))
.routes(routes!(lifecycle_ops::post_destroy)) .routes(routes!(lifecycle_ops::post_destroy))
.routes(routes!(lifecycle_ops::post_kill)) .routes(routes!(lifecycle_ops::post_kill))
@ -119,7 +111,6 @@ fn app(coord: Arc<Coordinator>) -> axum::Router {
.routes(routes!(lifecycle_ops::post_pause)) .routes(routes!(lifecycle_ops::post_pause))
.routes(routes!(lifecycle_ops::post_resume)) .routes(routes!(lifecycle_ops::post_resume))
.routes(routes!(tombstones::post_purge_tombstone)) .routes(routes!(tombstones::post_purge_tombstone))
.routes(routes!(build_logs::get_build_log_stream))
.routes(routes!(state_snapshot::dashboard_stream)) .routes(routes!(state_snapshot::dashboard_stream))
.routes(routes!(state_snapshot::jobq_graph)) .routes(routes!(state_snapshot::jobq_graph))
.routes(routes!(state_snapshot::jobq_rollup)) .routes(routes!(state_snapshot::jobq_rollup))
@ -351,12 +342,6 @@ mod router_build_probe {
.routes(routes!(health::get_health_ready)) .routes(routes!(health::get_health_ready))
.routes(routes!(state_snapshot::api_state)) .routes(routes!(state_snapshot::api_state))
.routes(routes!(misc_api::post_mark_all_read)) .routes(routes!(misc_api::post_mark_all_read))
.routes(routes!(build_logs::get_build_logs_all))
.routes(routes!(build_logs::get_build_log_for_node))
.routes(routes!(build_logs::get_build_log_raw_for_node))
.routes(routes!(build_logs::get_build_logs_agent))
.routes(routes!(build_logs::get_build_log_full))
.routes(routes!(build_logs::get_build_log_raw))
.routes(routes!(rebuild_queue::post_rebuild_queue_cancel)) .routes(routes!(rebuild_queue::post_rebuild_queue_cancel))
.routes(routes!(lifecycle_ops::post_destroy)) .routes(routes!(lifecycle_ops::post_destroy))
.routes(routes!(lifecycle_ops::post_kill)) .routes(routes!(lifecycle_ops::post_kill))
@ -366,7 +351,6 @@ mod router_build_probe {
.routes(routes!(lifecycle_ops::post_pause)) .routes(routes!(lifecycle_ops::post_pause))
.routes(routes!(lifecycle_ops::post_resume)) .routes(routes!(lifecycle_ops::post_resume))
.routes(routes!(tombstones::post_purge_tombstone)) .routes(routes!(tombstones::post_purge_tombstone))
.routes(routes!(build_logs::get_build_log_stream))
.routes(routes!(state_snapshot::dashboard_stream)) .routes(routes!(state_snapshot::dashboard_stream))
.routes(routes!(state_snapshot::jobq_graph)) .routes(routes!(state_snapshot::jobq_graph))
.routes(routes!(state_snapshot::jobq_rollup)); .routes(routes!(state_snapshot::jobq_rollup));

View file

@ -882,8 +882,7 @@ async fn priv_run(kind: &str, name: &str) -> Result<()> {
} }
/// Like `priv_run` but stamps `node_id` onto the build-log row it opens, so /// Like `priv_run` but stamps `node_id` onto the build-log row it opens, so
/// the dashboard can find the log from the queue node (and link to /// the log can be found from the queue node.
/// `/api/build-logs/id/{id}/stream`).
/// ///
/// This used to be a `Fn(i64)` callback that handed the row id *back* to the /// This used to be a `Fn(i64)` callback that handed the row id *back* to the
/// queue, which then held it in a side map. The row carries the link itself /// queue, which then held it in a side map. The row carries the link itself

Some files were not shown because too many files have changed in this diff Show more