remove hive and agent apis nothing calls: build-log routes, /api/compact, host.sock list, mcp.sock wake
This commit is contained in:
parent
cbfd7b2174
commit
36c7781245
28 changed files with 115 additions and 801 deletions
|
|
@ -152,13 +152,12 @@ Capturing the full stream (rather than a short tail buffer) matters
|
|||
because real eval errors routinely run long — "tried alternatives"
|
||||
blocks alone are often 30+ lines — so a truncated tail would cut off
|
||||
the actual failure and leave only the host journal holding the
|
||||
complete output. With this table the dashboard can surface the entire
|
||||
log.
|
||||
complete output. This table holds the entire log; no HTTP route serves
|
||||
it.
|
||||
|
||||
Three indices:
|
||||
|
||||
- `(agent, started_at)` — backs the per-agent latest-N lookup used
|
||||
by the agent card chip.
|
||||
- `(agent, started_at)` — per-agent lookup by recency.
|
||||
- `(status, finished_at)` — backs the retention sweep that runs
|
||||
as part of the existing hourly vacuum.
|
||||
- `(node_id)` — added by a later migration so a build log row can be
|
||||
|
|
|
|||
|
|
@ -101,10 +101,8 @@ state dirs work as for every other agent.
|
|||
- **Wire-protocol** — `hive-core-agent-sock`'s unified `Request` enum
|
||||
is the same on both socket flavours. Container lifecycle
|
||||
ops (kill/start/restart/rebuild) go through the separate host-admin
|
||||
socket `hivectl` speaks. `Wake` (inject a `from: <X>` message straight
|
||||
into the caller's own inbox) is on this socket too, unprivileged on
|
||||
both flavours; matrix, bash and forge don't call it — their
|
||||
notifications push a todo on the harness's in-agent socket (see
|
||||
socket `hivectl` speaks. Matrix, bash and forge wake the agent by pushing 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)).
|
||||
- **Storage/mounts** — the `manage_root_agent` permission, which the
|
||||
[`ruth` archetype](../../2-handbook/swarm/archetypes.md#the-built-in-archetypes) grants, gets a container
|
||||
|
|
|
|||
|
|
@ -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
|
||||
to be a background task inside the `hive-agent` serve loop. Polls
|
||||
`GET /api/v1/notifications?all=false` every 30 seconds (Forgejo's
|
||||
unread-only filter), formats each notification as a broker
|
||||
`Wake { from: "forge" }` message, and delivers it to the agent's own
|
||||
inbox so claude's normal turn loop picks it up.
|
||||
unread-only filter), formats each notification as a todo and pushes it to the agent's own
|
||||
in-agent socket so claude's normal turn loop picks it up.
|
||||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
||||
- `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 /` → the dashboard dist (from the `servedFrontend` nix-store path) with `try_files $uri /index.html`.
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
[`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
|
||||
**deferred**: the handler sets `Bus::request_compact()` and returns
|
||||
The agent can force a compaction via its `compact` MCP tool. It's
|
||||
**deferred**: the tool sets `Bus::request_compact()` and returns
|
||||
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 —
|
||||
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.
|
||||
|
||||
To disable proactive compaction for a specific agent, use the nix option:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
`meta` group — every agent needs to keep its dashboard status chip
|
||||
current regardless of which optional groups it holds.
|
||||
- `compact` — agent self-service equivalent of the operator dashboard's
|
||||
`/compact` button. No args. Gated server-side on the last completed
|
||||
- `compact` — queues a compaction of the current session; the only way to
|
||||
request one. No args. Gated server-side on the last completed
|
||||
turn's context usage: refused (with an explanation, no side effect)
|
||||
unless usage is above 66% of the effective context window. On a
|
||||
pass, queues the same deferred `compact_pending` flag the dashboard
|
||||
button sets — consumed at the end of the current turn, so it never
|
||||
pass, queues the deferred `compact_pending` flag — consumed at the end
|
||||
of the current turn, so it never
|
||||
races a live claude process, and the usual pre-compaction
|
||||
notes-checkpoint turn still fires first. Dispatched through 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)
|
||||
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
|
||||
|
||||
`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.
|
||||
- `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
|
||||
(`set_state(Thinking)` before `drive_turn`, `set_state(Idle)`
|
||||
|
|
|
|||
|
|
@ -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
|
||||
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
|
||||
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`
|
||||
or `/compact failed: …` note. It works mid-turn, not only when idle. Nothing
|
||||
on this page calls it now.
|
||||
or `/compact failed: …` note. It works mid-turn, not only when idle.
|
||||
|
||||
## Per-agent endpoints
|
||||
|
||||
Successful POSTs return 200 (no 303 redirects). Error responses
|
||||
use semantic status codes: **400** for missing/malformed input
|
||||
(`/api/todos/mark-done` with no todo ids), **500** only for genuine
|
||||
server/transport failures. `/compact` never conflicts: it queues
|
||||
and runs at the end of the current turn. The matching
|
||||
server/transport failures. A queued `/compact` never conflicts: it
|
||||
runs at the end of the current turn. The matching
|
||||
mutations fire `LiveEvent` variants on the per-agent bus, so the
|
||||
client doesn't refetch `/api/state` on submit — the SSE stream
|
||||
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
|
||||
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
|
||||
drop `--continue`. Emits a `LiveEvent::Note`.
|
||||
- `POST /api/logout` — three-step teardown that re-uses the
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ conflicting agent.
|
|||
|
||||
Passive / rare-interaction state. `kept.js` renders it from the
|
||||
`/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.)
|
||||
|
||||
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
|
||||
`{ "marked": N }`. Agent name validated against
|
||||
`[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:
|
||||
broker `sent` / `delivered`, plus the mutation events listed
|
||||
|
|
|
|||
|
|
@ -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`;
|
||||
hive-c0re owns the host admin socket.
|
||||
|
||||
### Wake injection
|
||||
|
||||
`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
|
||||
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-c0re round-trip. See
|
||||
[`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).
|
||||
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
|
||||
`/run/hive/mcp.sock` is implicitly trusted to inject wakes. That's
|
||||
fine: the bind-mount only exposes the socket inside the agent's own
|
||||
container, so the trust boundary is the container's process
|
||||
Anything that can connect to `/run/hive/mcp.sock` is implicitly trusted to
|
||||
call its verbs. That's fine: the bind-mount only exposes the socket inside the
|
||||
agent's own container, so the trust boundary is the container's process
|
||||
namespace, not the wire surface.
|
||||
|
||||
## Recipient sentinels
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ const STATE_TOOLTIPS: Record<string, string> = {
|
|||
offline: "harness unreachable or claude not logged in",
|
||||
idle: "turn loop running, no claude invocation in flight",
|
||||
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
|
||||
|
|
|
|||
|
|
@ -123,11 +123,11 @@ pub enum Request {
|
|||
/// `ReminderRollup` against the broker; now served from the local store).
|
||||
ReminderRollup { since_secs: u64 },
|
||||
/// 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%
|
||||
/// of the effective context window — below that, `Response::Err`
|
||||
/// 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.
|
||||
/// `wake_prompt`, when set, is driven as a synthetic follow-up turn once
|
||||
/// the compaction actually finishes — the dashboard button's own
|
||||
|
|
|
|||
|
|
@ -251,8 +251,7 @@ pub enum TurnState {
|
|||
Idle,
|
||||
/// `claude --print` is running for a turn.
|
||||
Thinking,
|
||||
/// Operator-triggered `/compact` is running on the persistent
|
||||
/// session.
|
||||
/// A `/compact` is running on the persistent session.
|
||||
Compacting,
|
||||
}
|
||||
|
||||
|
|
@ -316,9 +315,8 @@ pub struct Bus {
|
|||
/// boundary keeps compaction from racing a live claude process mid-turn.
|
||||
/// `Some(request)` when a compact is pending; `request.wake_prompt` is
|
||||
/// what to drive as a synthetic follow-up turn once the compaction
|
||||
/// actually completes (`None` = pending but no follow-up wake wanted,
|
||||
/// e.g. the operator dashboard's `/compact` button). `None` = no compact
|
||||
/// pending. Wrapped in [`CompactRequest`] rather than
|
||||
/// actually completes (`None` = pending but no follow-up wake wanted). `None` = no
|
||||
/// compact pending. Wrapped in [`CompactRequest`] rather than
|
||||
/// `Option<Option<String>>` (clippy pedantic's `option_option` lint,
|
||||
/// and the named field reads clearer at call sites than a bare nested
|
||||
/// `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
|
||||
/// (wake_prompt)` schedules a synthetic follow-up turn (driven with
|
||||
/// `wake_prompt` as its body) once the compaction actually completes;
|
||||
/// `None` requests a plain compact with no follow-up wake (the operator
|
||||
/// dashboard's `/compact` button).
|
||||
/// `None` requests a plain compact with no follow-up wake.
|
||||
pub fn request_compact(&self, wake_prompt: Option<String>) {
|
||||
*self.compact_pending.lock().unwrap() = Some(CompactRequest { wake_prompt });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,9 +11,8 @@
|
|||
//! reminder op then returns `Response::Err`); a reminder *firing* is a
|
||||
//! separate path (`reminder_timer`), not driven through this socket.
|
||||
//! `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
|
||||
//! deferred `compact_pending` flag the operator dashboard's `/compact`
|
||||
//! button sets). No hive-c0re round-trip, no broker long-poll, no marker
|
||||
//! the harness's [`Bus`] (gate-checked context usage, then the deferred
|
||||
//! `compact_pending` flag). No hive-c0re round-trip, no broker long-poll, no marker
|
||||
//! files.
|
||||
//!
|
||||
//! 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
|
||||
/// effective context window, an agent-initiated compact is refused (nothing
|
||||
/// 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
|
||||
/// dashboard's `ctx·Nk` badge before hitting the `/compact` button.
|
||||
/// near-empty session).
|
||||
const COMPACT_MIN_USAGE_FRACTION: f64 = 0.66;
|
||||
|
||||
/// 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
|
||||
/// deferred `compact_pending` flag the operator's `/compact` button sets.
|
||||
/// Mirrors `hive-agent::web_ui::actions::post_compact` but reachable from
|
||||
/// the agent's own MCP tool instead of the dashboard, and refuses below
|
||||
/// [`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
|
||||
/// `Request::Compact` handler: gate on context usage, then queue the
|
||||
/// deferred `compact_pending` flag. Refuses below
|
||||
/// [`COMPACT_MIN_USAGE_FRACTION`] because an agent can call this
|
||||
/// speculatively. `wake_prompt`, when set, is
|
||||
/// forwarded to `Bus::request_compact` so the turn loop drives a synthetic
|
||||
/// follow-up turn once the compaction actually finishes.
|
||||
fn compact(bus: &Bus, wake_prompt: Option<String>) -> Response {
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@ and end the turn.";
|
|||
/// passed here — they live at `/etc/claude-code/managed-settings.json`
|
||||
/// and claude auto-discovers them.
|
||||
/// 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
|
||||
/// stdin payload differs). The MCP config is rewritten before each
|
||||
/// 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`]
|
||||
/// the serve loop must act on. The `compacted` bool is `true` when a
|
||||
/// 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
|
||||
/// 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
|
||||
|
|
@ -452,8 +452,7 @@ pub async fn drive_turn(
|
|||
archive_session(bus, session);
|
||||
return Err(TurnError::PromptTooLong);
|
||||
}
|
||||
// Operator `/compact` (`POST /api/compact`) or an agent's own `compact`
|
||||
// MCP tool call, deferred to the turn boundary: run it now that the turn
|
||||
// An agent's own `compact` 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.
|
||||
// Only on a healthy turn — no point spawning a compaction after a
|
||||
// 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
|
||||
/// 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`].
|
||||
fn report_compact<E: std::fmt::Display>(bus: &Bus, result: Result<(), E>) -> bool {
|
||||
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,
|
||||
/// 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
|
||||
|
|
|
|||
|
|
@ -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::{
|
||||
Form,
|
||||
|
|
@ -16,22 +16,6 @@ pub(super) async fn post_cancel_turn(State(state): State<AppState>) -> 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
|
||||
/// `<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
|
||||
|
|
|
|||
|
|
@ -76,7 +76,6 @@ fn routes() -> Router<AppState> {
|
|||
.route("/events/stream", get(stream::events_stream))
|
||||
.route("/events/history", get(stream::events_history))
|
||||
.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/logout", post(auth::post_logout))
|
||||
.route("/api/todos", get(stats::api_todos))
|
||||
|
|
@ -532,7 +531,7 @@ mod tests {
|
|||
/// either.
|
||||
#[test]
|
||||
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 {
|
||||
label: "test".to_owned(),
|
||||
login: Arc::new(Mutex::new(LoginState::Online)),
|
||||
|
|
@ -550,17 +549,17 @@ mod tests {
|
|||
let effort = app.clone().oneshot(form("/api/effort", "effort=high"));
|
||||
// A route the router does serve, so the 404s above are not
|
||||
// the router refusing everything.
|
||||
let compact = app.oneshot(form("/api/compact", ""));
|
||||
let existing = app.oneshot(form("/api/new-session", ""));
|
||||
(
|
||||
model.await.unwrap(),
|
||||
effort.await.unwrap(),
|
||||
compact.await.unwrap(),
|
||||
existing.await.unwrap(),
|
||||
)
|
||||
})
|
||||
});
|
||||
assert_eq!(model.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.
|
||||
|
|
|
|||
|
|
@ -25,9 +25,7 @@ pub struct Coordinator {
|
|||
pub broker: Arc<Broker>,
|
||||
/// Full build-log capture. `lifecycle::run` `start()`s a row per attempt,
|
||||
/// pipes every stdout/stderr line into it, and `finish()`es it on
|
||||
/// child exit. Dashboard reads it via `list_recent_for_agent` /
|
||||
/// `get_full` for the per-card chip + side-panel viewer. See
|
||||
/// `build_logs.rs` for retention.
|
||||
/// child exit. See `build_logs.rs` for retention.
|
||||
pub build_logs: Arc<crate::build_logs::BuildLogs>,
|
||||
/// Store path of the hyperhive flake source (no fragment). Used to
|
||||
/// identify the running revision (`current_flake_rev`).
|
||||
|
|
@ -1061,7 +1059,7 @@ mod open_tests {
|
|||
|
||||
let coord = open(&db);
|
||||
let row = coord.build_logs.get_full(id).expect("get").expect("Some");
|
||||
assert_eq!(row.header.status.as_deref(), Some("fail"));
|
||||
assert!(row.header.finished_at.is_some());
|
||||
assert_eq!(row.status.as_deref(), Some("fail"));
|
||||
assert!(row.finished_at.is_some());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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:#}")),
|
||||
}
|
||||
}
|
||||
|
|
@ -35,7 +35,6 @@ use crate::lifecycle;
|
|||
),
|
||||
tags(
|
||||
(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 = "misc_api", description = "mark-all-read, compose, spawn-request"),
|
||||
|
|
@ -46,7 +45,6 @@ use crate::lifecycle;
|
|||
)]
|
||||
struct ApiDoc;
|
||||
|
||||
mod build_logs;
|
||||
// The single validated identifier type — homed in `hive-host-sock` (the crate
|
||||
// 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
|
||||
|
|
@ -104,12 +102,6 @@ fn app(coord: Arc<Coordinator>) -> axum::Router {
|
|||
.routes(routes!(health::get_health_ready))
|
||||
.routes(routes!(state_snapshot::api_state))
|
||||
.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!(lifecycle_ops::post_destroy))
|
||||
.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_resume))
|
||||
.routes(routes!(tombstones::post_purge_tombstone))
|
||||
.routes(routes!(build_logs::get_build_log_stream))
|
||||
.routes(routes!(state_snapshot::dashboard_stream))
|
||||
.routes(routes!(state_snapshot::jobq_graph))
|
||||
.routes(routes!(state_snapshot::jobq_rollup))
|
||||
|
|
@ -351,12 +342,6 @@ mod router_build_probe {
|
|||
.routes(routes!(health::get_health_ready))
|
||||
.routes(routes!(state_snapshot::api_state))
|
||||
.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!(lifecycle_ops::post_destroy))
|
||||
.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_resume))
|
||||
.routes(routes!(tombstones::post_purge_tombstone))
|
||||
.routes(routes!(build_logs::get_build_log_stream))
|
||||
.routes(routes!(state_snapshot::dashboard_stream))
|
||||
.routes(routes!(state_snapshot::jobq_graph))
|
||||
.routes(routes!(state_snapshot::jobq_rollup));
|
||||
|
|
|
|||
|
|
@ -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
|
||||
/// the dashboard can find the log from the queue node (and link to
|
||||
/// `/api/build-logs/id/{id}/stream`).
|
||||
/// the log can be found from the queue node.
|
||||
///
|
||||
/// 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
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue