Replaces the dashboard. The hand-written vanilla-JS page is superseded by a
Rust → WASM single-page app that shares the server's own types, and it now
serves /; the old page remains at /v1, deprecated. Everything an operator
previously needed v1 for — launching, killing, replying to an interactive
agent, editing a workspace's config, watching a live stream — is in v2, along
with things v1 never had: a per-agent timeline with a tool-call inspector, and
fleet-wide spend, turns, and outcome charts over a selectable window.
The other half of this release is the k8s fleet telling the truth about what it
ran. Terminal outcomes were structurally invisible under PROSPERO_FLEET=k8s:
the watch loop derived agent status from the CalibanTask phase, and the
operator never advances that past Running, so the new outcome charts read a
permanent zero — including for runs that had demonstrably failed. Outcomes are
now taken from the pod, recorded once per agent, and survive a restart.
Upgrade note. GET / now serves dashboard v2 rather than v1, and
GET /app.js is gone — v1's script moved to /v1/app.js along with the page.
/v2 remains a permanent alias for the new dashboard, so existing links to it
keep working.
Added
- Dashboard v2 — a Rust → WASM single-page app, and now the dashboard. A
Dioxus SPA incrates/dashboard, compiled towasm32-unknown-unknownand
embedded inprosperod, replacing the hand-written vanilla-JS page. The read
model and the control-plane DTOs are the same Rust types the server uses, so
the client cannot drift from the API the way a hand-maintained JS copy did.
The crate sits outside the cargo workspace on purpose (a wasm-only crate would
break the host-target build and sink the coverage floor) and has its own CI
job; its built bundle is committed andinclude_bytes!'d, so an ordinary
cargo buildstill needs no wasm toolchain and one binary still ships the UI
(#97). - Full operator control from the dashboard. Launch, kill, remove, respawn,
interactive input, end-input, and workspace removal — every action gated on
/api/capabilities, so the UI never offers an operation the active backend
would answer with a 405
(#173). - Workspace registration and configuration from the dashboard, with two form
shapes chosen at runtime: the local single-provider/env form, and the k8s
Workspace-CR editor with sources, named providers, and Secret references,
plus the reconciliation-status pill and a provider picker on launch. This also
fixed a lossy read:GET /api/workspaceswas projecting the CR's source spec
down and dropping the git remote and ref, so editing a k8s workspace meant
retyping every remote from memory
(#175). - Live agent stream viewer — replay history from the store, then tail over
SSE. A reconnect resumes atlast_seq + 1and anything at or below the
high-water mark is dropped, which is the defence against the v1 reconnect
storm that duplicated the timeline unboundedly (#105); a close after
AgentFinishedreads as "finished" rather than an error, so a healthy
completed run does not sit there retrying
(#178). - Per-agent timeline with a tool-call inspector. Tool calls pair start with
finish into a collapsible entry, consecutive output coalesces, and the opening
context and final accounting become a header and a summary. Pairing is on the
tool id and never the name — caliban'sToolCallEndcarries the id but leaves
the name empty, which is what left every tool stuck "running" in v1 (#106)
(#179). GET /api/usage— cost, turns, and terminal outcomes aggregated per
workspace per UTC day over a window (since/until, ordays). Computed
by the store rather than by replaying the log, with identical semantics across
the JSONL, SQLite, and Postgres backends, held there by a shared conformance
suite (#180).- Fleet overview charts for spend, turns, and outcomes over a selectable
24h / 7d / 30d window. Outcomes are faceted rather than stacked: measured
against the real tokens, no ordering of a four-way stack is separable in light
mode, so one single-hue chart per outcome is the fix rather than a mitigation.
Hand-rolled SVG with native<title>tooltips — the page is served under
default-src 'none', so there is no script and nothing for the CSP to refuse
(#181). - An explicit theme setting (System / Light / Dark), persisted in
localStorage, replacing "whateverprefers-color-schemesays". The explicit
choice wins in both directions, and the theme is applied before first paint so
there is no flash of the wrong one
(#183).
Changed
- The control-plane DTOs moved to
prospero-typesand now carry both
SerializeandDeserialize. They previously lived inprospero-api, which
pulls axum and tokio and so compiles for no wasm target, and each carried only
the server's half of the contract — so a WASM client would have had to
hand-duplicate all eight, reintroducing exactly the drift Rust/WASM was chosen
to avoid. They are re-exported from their original paths and the serde output
is unchanged, so this is additive for existing consumers
(#172). - Dashboard v2 is now the default surface.
GET /serves the Dioxus/WASM
dashboard;/v2stays mounted as a permanent alias, since the bundle's own
asset URLs are absolute/v2/...and existing links point there. The scaffold
deliberately parked v2 at/v2so/stayed untouched while epic #95 landed —
that transition is complete
(#191).
Deprecated
- The v1 dashboard has moved to
/v1and is deprecated. It renders a notice
pointing at/, and its script now lives at/v1/app.js(GET /app.jsis
gone). It is kept rather than removed so an operator hitting a v2 regression
has somewhere to land, but it receives no further work and carries defects v2
was built to fix — most visibly #106, where a tool call whose finish frame has
a blank name stays "running" forever. Removal is a follow-up once v2 has a
release of real-world use (#191).
Fixed
-
Restarting prosperod no longer re-counts outcomes it already recorded. A
fresh process starts with an empty view of the fleet and re-derives the
terminal transition it had already written before the restart, so every
outcome facet doubled on each restart — measured on a live cluster as
3 done / 3 failed becoming 6 / 6 with nothing having run. The watch loop now
checks the durable log (which outlives the process) before recording an
agent's outcome, so an agent that finished once is counted once
(#196). -
Terminal outcomes are now derived from the pod, not the CalibanTask phase.
#190 made the watch loop persist the transitions it observed, but it observed
status.phase, and the operator never advances that pastRunning— CRs whose
agents finished a day earlier still readRunning, so no terminal transition
ever occurred and the outcome facets stayed at zero on a real cluster. The loop
now applies the same pod-caliband overlaysnapshot()has always used, so the
component that emits events and the one that renders them finally agree. Pod
dials are bounded and concurrent, and an agent is consulted only until it is
observed terminal (#194). -
An unreachable pod can no longer stall fleet observation. The status
overlay dialled each pod sequentially with no deadline. A pod that black-holes
its SYN (rather than refusing it) blocked until the OS connect timeout, which
was survivable when onlyGET /api/fleetdid this and is not now that the
watch loop depends on it. Dials are concurrent and bounded; a miss retries on
the next poll (#194). -
Terminal outcomes are now recorded under
PROSPERO_FLEET=k8s. The usage
aggregate countsdone/failed/killed/crashedfrom persisted
status_changedevents. The local arm has always emitted them, but the k8s
watch loop computed the identical transition diff and only broadcast it
in-memory — so on k8s every outcome facet in the dashboard read zero,
including for agents that had demonstrably failed. The loop now persists the
transition it observes, elected by a single-writer observer lease so replicas
don't multiply the counts
(#190). -
The dashboard's usage panel refreshes on its own. It fetched once per
window selection and never again, so a finished agent's spend could sit
invisible until the operator toggled the window by hand. It now refetches when
the fleet poll observes an agent appear, change status, or disappear, and on a
one-minute heartbeat — without re-running a 30-day store aggregate on every
five-second poll (#190). -
A k8s workspace's provider base URL survives an edit. The v2 editor had no
base-URL input andProviderInfodid not carry the field, so reopening the
editor showed a blank box and saving wrote it back — a routine model edit
silently unpicked a self-hosted provider, after which agents died instantly
with aProviderErroragainstlocalhost
(#188). -
k8s-spawned agents get the resolved provider and model.
spawn_spec_from_tasksentprovider/modelasNoneon the premise that
pod env would drive selection; the caliban worker selects fromSpawnSpecand
nothing else, so every k8s agent fell back to caliban's default and died at
preflight withANTHROPIC_API_KEY is not set. The operator already pins the
resolved provider intostatus.resolvedWorkspace, so its kind and model are
projected onto the spawn.ensure_pod_agentalso no longer attaches to an
agent already in a terminal state — caliband keeps a failed agent in its
registry with the endpoint it advertised, but the worker died before binding
that port, turning a one-shot failure into a silent reconnect loop
(#169). -
Re-submitting an identical prompt no longer claims a launch that didn't
happen. Spawning is idempotent and the k8sCalibanTaskname is derived
from the spec, so an identical prompt resolves to the run already in flight.
POST /api/workspaces/{name}/agentsnow reportscreated, and the dashboard
says it attached to the existing run instead of "Launched"
(#190).