Changelog
All notable changes to prospero are documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning. While the project is pre-1.0, the minor version is bumped for new features and the patch version for fixes.
Unreleased
Fixed
- The published guide no longer ships dead links. An ADR is written to live in
docs/adr/, whereREADME.mdand../superpowers/specs/<spec>.mdboth resolve; copied into the book they did not, becauseREADME.mdandtemplate.mdare deliberately not chapters and nothing ingestsdocs/superpowers/. mdBook rewrote both to pages it never built, so three links in ADR 0001 and theSource:link of seven ADRs (0002–0007 and 0010) served 404s.sync-adrs.shnow rewrites links that leave the book to absolute repository URLs, resolving each target from the ADR's original location rather than its copied one, and fails the build if a target does not exist — a link rewritten to a missing file is still a dead link. (#268)
Internal
-
The guide's links are checked after every docs build, by
docs/guide/check-links.sh. mdBook validates no links whatsoever: it rewrote every one of the dead links above and emitted zero warnings, so "the guide builds clean" was never evidence that it was navigable. The check resolves relative hrefs against the built output and absolute links back into this repository against the working tree — the latter because the rewrite above converts checked relative links into unchecked absolute ones, and a check that skipped them would bless exactly the mistake the rewrite can make. Third-party URLs are not fetched, so the docs build does not depend on anyone else's uptime. (#268) -
The docs site is rebuilt when
CHANGELOG.mdchanges.sync-changelog.shingests it into the guide, butdocs.ymlwas not triggered by it, so a changelog-only commit rebuilt nothing and the published changelog silently fell behind the repository — entries appeared only when some later commit happened to touch a path that did trigger a build. The same guard that coversci.yml's path filter now also fails the build when a path async-*.shscript reads is missing from either ofdocs.yml's triggers, since the next ingest would otherwise repeat this. (#270) -
CI no longer skips the cargo gate on a docs change that is compiled into a test.
ci.ymlskips every cargo step when a diff touches onlydocs/,*.mdorLICENSE, butcrates/api/tests/guide.rsinclude_str!sdocs/guide/src/api.md— so an edit to the guide's routes table was the single change most likely to fail the route-coverage test added in #256, and the one change CI would not run it for. The workflow now declares those paths inTEST_INPUT_DOCSand treats them as code; a genuinely docs-only change still skips the gate. The declaration is held to the source bycrates/api/tests/ci_filter.rs, which fails the build both when a new doc test input is missing from the list and when a stale entry no longer matches, so the filter cannot quietly go out of date. (#264) -
The Rust toolchain is pinned in
rust-toolchain.toml(1.99.0), so a new stable release can no longer break a build whose code never changed — which is what happened to v0.9.0. CI still installs its toolchain withdtolnay/rust-toolchain@stable; rustup honours this file when cargo runs, so the version lives in exactly one place and upgrading it is a reviewable commit. The file also lists thewasm32-unknown-unknowntarget, because thetargets:input inci.ymlapplies to the toolchain the action installs rather than the one this file selects. (#261) -
caliban-contractpinned to=0.15.0, matching caliban 0.15.0 and moving in lockstep with caliban-operator so the two downstream consumers of the contract do not disagree about its version (caliban-ai/caliban-operator#95). Nothing prospero compiles against changed:launch.rsis the only file that differs between the two contract versions, and prospero imports only fromcaliban_contract::wire. Note for anyone else downstream — the fieldCalibandLaunch.router_configkept its type but changed meaning, from inline config JSON to a filesystem path (caliban ADR 0060), so a caller passing JSON still compiles and silently has it read as a path. prospero never sets it.
0.9.0 - 2026-10-03
This release is about letting other programs drive and watch the fleet. One SSE
connection now carries every agent's events with a resumable cursor, /mcp
serves the fleet to agentic tools, and GET /api/openapi.json describes the
whole REST surface so a client can generate its types instead of hand-writing
them from prose. Spawns no longer have to be manual: an automation fires one on
a cron schedule or a signed webhook. In Kubernetes, a per-session permission
posture makes in-cluster agents usable at last, and when the operator refuses a
task prospero reports the reason instead of leaving an unexplained dead agent.
**Companion versions.** The permission posture needs caliban v0.14.0 (for
`SpawnSpec.permission_posture`) and caliban-operator v0.6.0 (which admits or
refuses it and supplies the refusal reason). Automations need an event and
config store shared by every replica, so they are enabled for the local backend
and, under Kubernetes, only when `--database-url` is set.
Added
-
Fleet-wide event stream.
GET /api/fleet/streamcarries every stream's events on one connection, so watching a fleet no longer needs an SSE connection per agent — and a client can finally learn about an agent it has not seen yet. Each event's SSEid:is a fleet cursor: the store's durable insertion order, the only total order a fleet has (per-agentseqcannot order two agents, since both start at 1).?from=<cursor>resumes exclusively after it,?from=nowskips history, and an absentfromreplays from the start. Every event is read from the durable store rather than the in-process bus, so delivery is exactly-once across replicas and a slow client is never skipped past. Two newStoremethods (replay_fleet,latest_fleet_cursor) are held to identical semantics across JSONL, SQLite and Postgres by a shared conformance battery. Requiresread. The dashboard's 5-second poll is gone, replaced by the stream plus a 30-second reconcile so a droppedEventSourcecannot freeze the screen. (#219) (#247) -
The fleet over MCP.
/mcpserves the fleet over the Model Context Protocol (streamable HTTP,rmcp— the same crate caliban drives its own adapter with, so both speak an identical protocol version to a client). Nine tools: list workspaces and agents, spawn, status, events, send and end input, kill, respawn. It is a thin adapter over the sameFleetProviderseam the REST handlers already use, so both the local and Kubernetes backends work through it and a tool cannot do what the HTTP API cannot. Two choices follow from the consumer being a model rather than a person: history is capped (100 default, 500 max) and returnstruncated+next_from, and a bad agent id is a tool error naming the id rather than a protocol error, so the model can correct itself instead of the session failing. Requiresoperate; workspace administration is deliberately not exposed. (#218) (#249) -
Automations — scheduled and webhook-triggered spawns. An automation is a stored spawn template plus what fires it: a 5-field UTC cron schedule, or a signed inbound webhook whose payload fills the template's
{{ dotted.path }}placeholders. Every firing is recorded, successful or not, in a bounded per-automation history. Exactly-once across replicas is a compare-and-set on the claimed tick rather than the lifecycle lease: a lease makes a double fire unlikely but cannot rule it out, because ownership can change hands between checking and spawning. The webhook signing key is returned once at creation and lives only on core's own stored type, so no response, listing or dashboard view can carry it; signatures are verified in constant time over the raw request bytes, and an absent or malformed header is a rejection. (#220) (#250) -
An OpenAPI 3.1 document at
GET /api/openapi.json. Clients — ariel first — had to hand-write every request and response type from the prose guide. The document is open like the guide it mirrors: it describes the API's shape, carries no fleet data, and a client needs it before it has a credential. Schemas come from the sameprospero-typesstructs the handlers serialize, via schemars, behind an off-by-defaultopenapifeature so the wasm dashboard — which shares those types — does not carry a schema generator it never calls. Requests and responses are generated under their own contracts, because a type is described differently when it is read than when it is written: a serde default makes a field optional to a reader and says nothing to a writer, so one schema per type would misdescribe one direction. Each operation records its required scope inx-prospero-scope. The spec cannot quietly fall behind the router — a test readslib.rsfor the paths and verbs it registers, so a new route, a new verb, or a documented route that is not served all fail the build. (#222) (#253) -
Per-session permission posture. In-cluster agents were unusable: no human is attached to a pod, so caliban's non-interactive gate denied every Bash/Write/Edit and the refusal reached nobody.
permission_posture(supervised|unattended, defaulting tosupervisedeverywhere, fail-closed) is now carried on the spawn body, the CLI (--permission-posture) and theCalibanTask.unattendedrequires anadminscope — a per-field rule the route scope table cannot express — and each grant is audited with the token name. Under Kubernetes what prospero sends caliband is the posture the operator admitted (status.permissionPosture), not the one requested, so an operator predating the field cannot be used to bypass the Workspace'sagentPolicy. Every agent reports the posture it runs under, and unattended agents carry abypasstag in the dashboard. Also wiresspec.isolation.worktreesthrough toSpawnSpec.isolation_worktree. (#238, #226) (#240) -
Per-spawn wall-clock limit.
timeout_secson a spawn (prospero spawn --timeout <SECONDS>) caps an agent's wall-clock life. The deadline is a log entry, not a timer — a timer dies with the replica that set it — so prospero writesDeadlineSetto the shared event log, and a replica that never spawned the agent reads it back the first time it polls. Enforcement runs under the lifecycle lease so one replica acts on an expiry rather than all of them racing to kill the same agent, and anAgentTimedOutevent is emitted before the kill, so history says why the agent stopped instead of leaving a barekilledthat reads like an operator did it./api/usagecounts it astimed_outalongsidekilled, and the dashboard gains the facet. Deliberately noAgentStatus::TimedOut: that enum is matched exhaustively by clients and mirrors caliban's own lifecycle vocabulary, which has no such state, so the distinct outcome is the event. (#221) (#248) -
Attribution to a person, not just a token.
FleetEvent.actoris the token name, which is right for attributing a request to a credential and useless for a client acting on behalf of other people — ariel holds oneoperatetoken and serves a whole Discord, so every agent it spawned readactor: "ariel"whoever asked for it. A request may now carryX-Prospero-On-Behalf-Of, and the events it emits record it ason_behalf_ofbeside the unchangedactor. A header rather than a spawn-body field, so it covers kill and respawn too and an older daemon ignores it for free. The value is the client's assertion, not a fact prosperod checked: the token remains the authenticated identity and is always recorded, so a false claim stays attributable to the credential that made it, and the audit line names itasserted_on_behalf_of. Validated rather than sanitized, and bounded — it reaches the event log and the structured logs, where a control character could forge what reads as a separate record — and a malformed value is a 400 rather than a silent drop. Both SQL stores gain the column. (#251) (#252) -
prospero usage.GET /api/usagewas rendered only by the dashboard. The CLI now prints the same report as a table — one row per workspace, a total when there is more than one, the window stated up front — or, with--json, the server's own report.prospero usage [--since <7d|2w|YYYY-MM-DD|RFC-3339>] [--workspace <name>] [--json]7d/2ware resolved on the server's clock, as the dashboard does, so a drifted client clock cannot clip the window; a date is its UTC midnight and a timestamp is converted to UTC. An hour-level window such as24his refused with a reason, because usage is bucketed per UTC day and an hour would promise a precision the report lacks.--workspacefilters client-side on the raw JSON, so--jsonkeeps fields a newer server adds and both output modes show the same rows. (#223) (#254)
Fixed
-
GET /api/usagevalidates and normalizes its window. Three bugs, each reproduced by a test that failed first. A largedayspanicked the handler —?days=200000000was enough, chrono overflowing on the subtraction rather than insideDuration::daysas the ticket guessed.sinceanduntilwere never parsed, so?since=yesterdayreturned 200 and a report for whatever window that string happens to sort between, because the store compares timestamps lexically on a TEXT column. And for that same reason the spelling of a correct bound mattered: events are stamped…00:00:00.5+00:00, and aZbound sorts after the.of a fractional second, so an event half a second into the window fell out of it — the handler already normalized its own defaults but never client-supplied bounds. Both bounds are now parsed (400bad_requeston failure) and re-rendered, anddaysis checked against a shared cap. (#255) (#257) -
One
Drainedagent no longer drops every agent on its caliband.wire.rshand-mirrored caliband's control-plane types, and the mirror had already drifted into a live bug: caliban'sAgentStatusgainedDrainedthat the mirror never learned, and with no catch-all a single drained agent failed deserialization of the entireListedreply, dropping every agent on that caliband from the poll. Those types are now re-exported from caliban's publishedcaliban-contractcrate (pinned=0.14.0) — serde-only, so not the wide coupling ADR-0003 rejected, and exactly what its own "revisit if" clause asked for once the mirror became a drift source.Drainedmaps toDone: terminal, not a failure. (#239) (#245) -
The operator's failure reason reaches the agent. caliban-operator v0.6.0 refuses an
unattendedtask whose Workspace does not allow it — phaseFailed,Ready=False, reasonPostureNotPermitted— and creates no pod, so there is no sandbox, no caliband and no agent-side error anywhere: the operator's condition is the only account of what happened. prospero mapped the phase tofailedand dropped the rest, leaving an unexplained dead agent and someone waiting for a sandbox that was never coming.Agentgains optionalreasonanddetail, omitted when unset, filled from the operator'sReadycondition and only when it is notTrue. Local agents never carry them, since caliband reports their state directly with no admission step above it. (#241) (#246) -
Tool results show in the dashboard inspector. caliban has carried each tool call's result on
ToolCallEndsince caliban#391, but prospero's normalizer read onlytool_use_idandis_error, so the inspector still said the result "needs a caliban-side change".EventKind::ToolFinishedgainsresultandtruncated, both defaulted and skipped when empty, so events stored before this change still deserialize. The normalizer takes caliban's bounded display preview and applies its own 8,192-character cap, matching caliban's, so one call cannot put an unbounded body into the event stores or SSE. (#236) (#237)
Documentation
-
ADR-0011 — ACP is a second drive protocol, not a second lifecycle (status: proposed). Records an
AgentDriverseam for an agent's data path with caliband keeping every lifecycle, the protocol chosen per spawn and defaulting to NDJSON, and the decision not to make prosperod a process supervisor. Amends ADR-0003. (#217) (#244) -
Automations documented, across both the API and CLI guides: the create body and template defaults, cron semantics, webhook signing with a worked
opensslexample, placeholder rules, and run history. The guide's routes table now carries the same build-breaking guard as the OpenAPI document — a test checks it against the paths and verbslib.rsregisters — so a route can no longer ship undocumented. (#256) (#258)
Internal
async-traitbumped to 0.1.92. Up to 0.1.89 the macro put#[must_use]on the method it desugars anasync fninto, and since that method returns a future — already#[must_use]— clippy 1.99 reportsdouble_must_useon macro-generated code, failing the lint gate for all seven#[async_trait]traits inprospero-core. Nothing in the repo changed to cause it: CI resolvesstableat run time, so the build broke when stable moved from 1.97 to 1.99. The upstream bump is the fix rather than anallow.
0.8.1 - 2026-09-16
Fixed
- The
AgentsSettledcondition is written in a real cluster. v0.8.0's condition carriedlastTransitionTime, which theCalibanTaskCRD's condition schema doesn't declare, so server-side apply rejected every write with a 500 ("field not declared in schema"). No condition was ever written, a finished task never reachedCompleted, and prosperod logged the failure on every poll. prospero now sends onlytype,statusandreason, matching caliban-operator'sCondition, and a test pins the apply body to those fields. (#234)
0.8.0 - 2026-09-16
prosperod now authenticates its API, and in Kubernetes it reports agent
lifecycle back to the operator. Named API tokens with one of three scopes guard
every non-probe route; the dashboard signs in with a token and holds a signed
session cookie; and the CLI carries a token. Separately, prosperod writes an
AgentsSettled condition to each CalibanTask's status, which caliban-operator
v0.5.0 turns into a terminal Completed or Failed phase, so a finished task
finally leaves Running.
**Breaking for deployments:** with no tokens configured, prosperod now refuses a
non-loopback `--addr` (such as `0.0.0.0` in a container) unless
`--insecure-no-auth` / `PROSPERO_INSECURE_NO_AUTH=1` is set. Configure
`--api-tokens-file` / `PROSPERO_API_TOKENS_FILE` before upgrading, or opt out
explicitly. See the "Securing the API" guide page.
Added
-
Breaking: API authentication and authorization (ADR-0010). Named tokens (
pspo_…) with scoperead,operateoradmin, stored as SHA-256 hashes in a tokens file; a route-scope table that fails closed toadmin; JSON 401/403; and oneprospero_auditlog line per mutation. With tokens configured, every request needs one, loopback included./healthz,/readyz, the dashboard shell and/api/sessionstay open. The dashboard signs in with a token for a 12-hour HMAC-signedHttpOnly; SameSite=Strictcookie bound to the token's hash, and cookie-authenticated mutations must be same-origin. Clustered prosperod with tokens needs--session-key-fileso replicas share cookie signing. The token name is recorded asactoron local-fleet spawn and remove events and persisted in every store (nullable column migration). CLI:--token/PROSPERO_TOKEN,--token-file/PROSPERO_TOKEN_FILE,prospero token new <name> --scope <scope>andprospero whoami. (#2) (#227) -
AgentsSettledcondition onCalibanTaskstatus. After each poll in which a pod's caliband answers, prosperod server-side applies one condition under field managerprospero:True/Succeededwhen every agent finished,True/Failedif any ended failed or crashed, otherwiseFalse/AgentsActive(an idle interactive task stays unsettled). The apply carries only that condition, never forces, skips tasks whose caliband didn't answer, and runs under the observer lease so clustered replicas don't race. Needs RBACpatchoncalibantasks/status(caliban-ai/helm-charts prospero chart 0.2.9); without it prosperod logs a warning per poll and carries on. (#228) (#232) -
View model thinking in the dashboard. With
PROSPERO_INCLUDE_THINKINGset, reasoning content is kept through normalization and shown as a collapsible "Thinking" segment on the agent timeline, separate from output. Off by default. (#212) (#216)
Fixed
- A re-spawned k8s agent that reuses a task name is attached again. A fast
delete and recreate of a same-named
CalibanTaskleft the predecessor's terminal mark in place, so prospero never attached to the healthy new agent until prosperod restarted. The poll loop now tracks each CR's uid and clears the mark when a known name returns with a new uid. (#213) (#214)
Documentation
- User guide pages, and the README and container docs brought current. (#229)
- Competitor evaluation: re-verified the OpenClaw parity matrix and added OpenHands, Coder, GitHub Agent HQ and Vibe Kanban. (#215)
0.7.0 - 2026-09-13
Follows caliban's removal of its bespoke ollama provider (caliban ADR 0056).
prospero fills the caliban worker's SpawnSpec.provider from a repo's stored
config, so a repo pinned to ollama now spawns a worker that fails at startup —
this release migrates prospero off Ollama and onto the OpenAI-compatible path.
Changed
- Ollama removed; local inference via
openai+base_url(#207): the dropdown, defaults, fixtures, and the dedicatedollamaenv-projection arm are gone.validate_provider_envnow treats a provider with an overriddenbase_urlas keyless — mirroring caliban's keyless local-endpoint change — so a migrated local config (openai+base_url, no key) keeps the keyless UX thatollamahad, instead of being rejected for a missingOPENAI_API_KEY. (#208) - Reworded the remaining rationale comments off the removed provider's name; the history lives in caliban ADR 0056 (#209). (#210)
A repo or workspace still configured with `provider: "ollama"` will spawn a
caliban worker that fails loudly at startup. Migrate it to `openai` with a
`base_url` pointing at a local `/v1` endpoint.
0.6.0 - 2026-09-09
Finishes what v0.5.0 started. That release made the Rust → WASM dashboard the
default and left the old vanilla-JS page at /v1 as a landing spot in case v2
regressed. It didn't, so both the page and the version suffix that existed to
distinguish the two are gone: there is one dashboard, served at /, and its
files live under /assets/.
The rest is the k8s fleet and its test coverage getting more honest. A pod agent
that dies at spawn is a stable state, and prospero now treats it as one instead
of re-dialing it every two seconds for as long as the process lives — a loop
measured running for twenty-four days against a single stuck task. Two long-open
gaps in coverage close alongside it: cross-stream delivery on the clustered bus,
which had been #[ignore]d as too flaky to gate on, and streaming under
fan-out, which had no coverage at all.
Removed
-
Breaking: the v1 dashboard. The hand-written vanilla-JS page that served
/before v0.5.0, deprecated to/v1in #191, is deleted along with its embedded assets and routes./v1and/v1/app.jsnow 404 — deliberately no redirect and no fallback, so a dead surface does not linger in bookmarks and access logs. Removing it also drops ~200KB of embedded assets from every binary and the known defects v2 was built to fix, most visibly #106's permanently-"running" tool calls. (#197) -
Breaking: the
/v2alias, and the version suffix generally. With only one dashboard left there is nothing to disambiguate, so the document is served at/and its files move from/v2/...to/assets/.... A prefix rather than the bare root, so the bundle cannot shadow the API namespace.GET /v2now 404s. Internally the same rename:crates/api/dashboard-v2/iscrates/api/dashboard/, the handler module isdashboard, and the CI job is "dashboard wasm build". (#204)
Added
-
DistributedBus::subscribe_all_ready— an unfiltered subscription whoseLISTENis established before it returns, rather than lazily on the stream's first poll. The lazy form cannot tell a caller when it is safe to publish, and a doorbell rung in that gap is simply not heard. Connection andLISTENfailures now surface as an error instead of silently ending the stream. (#132) -
Load and soak coverage for streaming under pressure. Forty concurrent attach tasks must each stream to completion carrying their own output and monotonic sequence numbers, so a crossed or silently-unattached stream fails loudly rather than going unnoticed. The in-process bus gains bounded-buffer coverage: a slow subscriber is told exactly how much it lost and keeps working, a subscriber that keeps up never lags, sixty-four subscribers lag independently rather than sharing a cursor, and dropping one mid-pressure leaves the rest untouched. Asserted on observable state rather than elapsed time, so they run in the gate instead of becoming the next ignored test. (#8)
Fixed
-
A terminal pod agent is resolved once, not on every poll. Rejecting a terminal record (#168) removed the reconnect budget that had been incidentally throttling the retry, so the ~2s watch loop re-resolved the same dead agent forever — one warning per poll, indefinitely, for a state that cannot resolve on its own. Observed running for 24 days against a single stuck task. The session plane now remembers terminal agent ids, skips them, and logs the cause once per transition. An explicit stop or restart, or the CR going away, clears the mark so a respawn is re-checked. (#170)
-
Cross-stream
subscribe_alldelivery is covered in CI again. The test was#[ignore]d because the lazy subscription gave it no way to know when it was safe to ring the doorbell, so it re-rang up to 440 times over 44s and still starved to zero deliveries on a contended runner. It now establishesLISTENfirst and asserts on delivery rather than on patience — about a second, including under the coverage instrumentation the flake was worst under. (#132)
Security
anyhowbumped to 1.0.104, clearing RUSTSEC-2026-0190 (unsoundness inError::downcast_mut()). Lockfile only; nothing here callsdowncast_mut, but a release should not carry a known-unsound dependency. (#205)
0.5.0 - 2026-08-22
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 in
crates/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 ordinarycargo 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 at
last_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 afterAgentFinishedreads 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's
ToolCallEndcarries 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 underdefault-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 bothSerializeandDeserialize. 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 only
GET /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 persistedstatus_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 and
ProviderInfodid 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 k8s
CalibanTaskname 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).
0.4.0 - 2026-07-27
Makes the PROSPERO_FLEET=k8s fleet interactive. Since 0.3.0 a k8s agent
could be launched from the dashboard but never talked to: the reply box renders
only for an agent that is both interactive and idle, and under the k8s
backend neither could ever be true. Closing that took a field on the
authoritative CRD (caliban-operator#28) plus both halves of the round-trip here,
and turned up a second defect — 0.3.3's agent-id decoupling had quietly broken
reply delivery and the 0.3.2 status overlay. Local behavior is unchanged.
Deploy note: requires the caliban-crds chart at >= 0.2.1. Older CRDs
have no spec.task.interactive, so the API server prunes the field at admission
and the flag never reaches the pod.
Added
- Interactive agents under
PROSPERO_FLEET=k8s. The dashboard'sinteractive: truenow survives the CR round-trip:build_calibantaskwritesspec.task.interactive(the field caliban-operator#28 added to the authoritative CRD) andspawn_spec_from_taskreads it back into theSpawnSpecsent to the pod's caliband. Previously the flag was silently dropped at the CR boundary and the spawn spec hardcodedinteractive: false, so a k8s agent could never await input and the dashboard reply box (interactive && idle) could never appear. Requires thecaliban-crdschart at >= 0.2.1, or the API server prunes the field at admission (#163).
Fixed
- Operator replies and the interactive/idle overlay now target caliband's own
agent id. #159 decoupled the agent id caliband assigns from prospero's CR
name, but
send_inputstill attached by CR name (404, reply lost) andoverlay_pod_statusstill looked records up by id (silent miss, regressing #130's reply box).send_inputnow resolves the pod's agent viaensure_pod_agent, and the overlay matches each pod's record by endpoint rather than by id. The pre-existing tests used one string for both ids, so they passed while the real path was broken; the new tests use distinct ids (#163).
0.3.3 - 2026-07-19
Completes the PROSPERO_FLEET=k8s control plane so a spawned agent actually
runs: the previous two fixes let the caliband pod bind and decoupled spawn from
reconcile, but nothing ever started the agent inside the pod. Local behavior is
unchanged.
Fixed
- k8s agents now actually start (and stream). Under the k8s backend,
spawning created the
CalibanTaskCR and a caliband pod but never started the LLM run — caliband is a passive supervisor that begins an agent only onCtlRequest::Spawn, and prospero's session plane only attached, so the attach looped forever onagent not found. prospero now spawns the agent in the pod's caliband (list-or-spawn, one agent per pod) from theCalibanTaskprompt, then attaches — idempotent across poll cycles and replicas (ownership-lease-gated). Because caliband assigns the agent id itself, the attach id is decoupled from prospero's stream key (the CR name) so output still streams to the dashboard under the identity/streamexpects (#159) (#160).
0.3.2 - 2026-07-18
Two fixes to the PROSPERO_FLEET=k8s control plane found in live-cluster use:
spawning an agent no longer blocks the dashboard on the operator's reconcile,
and an agent's interactive reply box now appears under the k8s backend. Local
behavior is unchanged.
Fixed
- Spawning an agent no longer hangs the dashboard on reconcile. Under the
k8s backend,
K8sFleet::ensure_agentapplied theCalibanTaskCR and then synchronously polled (up to ~30s) forstatus.phase == "Running"before returning, coupling the HTTP response to the fullCR → operator reconcile → pod schedule → Runningchain (and blocking the entire budget when the pod never started). It now returns as soon as the CR is admitted; the background watch loop surfaces the agent and attaches its session when it reachesRunning— the synchronous poll was redundant with that path (#157). - The interactive reply box now appears for k8s agents. The dashboard shows
it only when an agent is
interactiveandidle, and underPROSPERO_FLEET=k8sneither was sourced correctly — the status/interactive fields are now read from the pod's caliband rather than theCalibanTaskCR alone (#130) (#156).
0.3.1 - 2026-07-14
Bug-fix follow-up to the 0.3.0 Kubernetes config plane, closing the four issues
surfaced in k8s smoke testing on a fresh 0.3.0 deploy. The PROSPERO_FLEET=k8s
fleet now stays Ready through a schema-skewed custom resource, surfaces the
registered Workspace CRs it manages (instead of a synthetic phantom), and
rejects an unregisterable workspace up front. Local behavior is unchanged.
Fixed
- A single un-deserializable
CalibanTaskno longer wedges the whole fleet.K8sFleet's watch/readiness path listedCalibanTasks strictly, so one CR that failed to deserialize (e.g. a stale task predating the now-requiredworkspaceReffield) failed the entire poll — the fleet never populated,/readyzstuck at503, and the pod never becameReady. The list is now decoded per-item, skipping and logging the bad CRs (#148) (#152). - The k8s fleet snapshot reconciles with the
Workspaceregistry.GET /api/fleetsynthesized a single phantomk8sworkspace and never read the registeredWorkspaceCRs, so a registered workspace was invisible in the dashboard while the synthetick8sentry reportedworkspace not registered. The snapshot now surfaces the registeredWorkspaceCRs (agents grouped by the workspace they reference), so/api/fleetand/api/workspacesagree and a fresh deploy shows no phantom (#149, #151) (#153). - Add-workspace rejects an invalid workspace as
400, not a raw apiserver422. The dashboard's+ add workspaceposted aWorkspacewith emptyproviders/sources, which the CRD (minItems: 1on both) rejected — so a workspace could never be registered from the dashboard. The config plane now validates at least one well-formed source and provider before apply (add and edit paths), and the form validates the same client-side (#150) (#154).
0.3.0 - 2026-07-12
The Kubernetes config plane: deploying with PROSPERO_FLEET=k8s is now a
real control plane — create and configure workspaces, and launch provider-bound
agents, from the dashboard — instead of a read-only viewer that returned
405 Method Not Allowed on Save. Workspaces are first-class Workspace custom
resources reconciled by caliban-operator, and the dashboard is backend-aware.
Local behavior is unchanged.
Added
- Kubernetes config plane (core + API). Under
PROSPERO_FLEET=k8s,K8sFleetnow wires aFleetAdminover operator-ownedWorkspacecustom resources, soPOST/PUT/DELETEon/api/workspacespersist and manage real configuration — multi-source workspaces, a named-provider list, and per-provider credentials referenced by KubernetesSecretname (prospero never reads the Secret) — instead of returning405. A backend-neutralWorkspaceConfigDTO lets one API serve both backends (local projects its single-provider subset, unchanged);GET /api/workspacesreturns the realWorkspaceCRs with reconciliation status; async workspace writes answer202 Accepted; and a spawned agent binds a named provider viaproviderRef(#142) (#144, #145). - Backend-aware dashboard. The dashboard fetches
GET /api/capabilitiesand adapts. On k8s it renders a workspace editor (git sources + a named-provider list withsecretName/keySecret references and a default marker), reconciliation status pills (pending/reconciling/ready/failedwith the failure message on hover), and a launch-modal provider picker; on local it is byte-for-byte unchanged (#143) (#146). GET /api/capabilities— a backend capability seam the dashboard gates its controls on (#99) (#101).- Frontmatter / agent-template support through spawn — a spawn can forward an
agent-template markdown file to caliband's
SpawnSpec.frontmatter_path(#6) (#102). - Guiding Principles & Invariants guide page synthesizing ADRs 0002–0009 (#74) (#104).
Changed
- The
CalibanTaskCRD mirror moved from an inlineworkspaceto aworkspaceRef(plus an operator-pinnedstatus.resolvedWorkspace), matching caliban-operator's frozenv1alpha1contract. Pre-v1; existing cluster CRs are recreated under the new schema.
0.2.0 - 2026-07-11
Kubernetes high-availability, a reworked dashboard, and a full QA sweep. A
second QA pass over the real prospero/caliband stack filed 23 findings; all
are fixed here, alongside first-class leader election for the k8s fleet backend
and a new agent-timeline dashboard.
Added
- Leader election + attach lifecycle for the
K8sFleetbackend. The session-plane attach — the one path that writes an agent's events to the shared store/bus — is now gated on a per-agent ownership lease, so with 2+prosperodreplicas exactly one replica owns, attaches to, and emits each agent (no more duplicate SSE events or racing per-streamseqallocation). Standalone is unchanged (SelfOwnsAll); a clustered deploy builds aLeasedOwnershiplease plus heartbeat. Attach tasks are now promptly torn down on stop/remove/restart, and any agent observedRunning— including operator- or peer-created ones — is streamed by the lease owner (#108, #112, #113) (#138). - Dashboard agent timeline, tool-call inspector, and run header — a folded event timeline with expandable tool-call segments and a per-run turns/outcome header (#5) (#96).
prospero-typescrate — the normalizedFleetEvent/model DTOs extracted into a small, wasm-compatible serde-only crate the WASM dashboard can share (#98) (#100).
Changed
- Under
PROSPERO_FLEET=k8s,prosperodno longer builds a localFleetManager/poll loop; the k8s backend serves directly over the shared store/bus (#83) (#92). /readyznow reportsworkspaces_total/workspaces_healthy/workspaces_unreachable(wasrepos_*), and user-facing error wording says "workspace" not "repo", matching the vocabulary used everywhere else (#116, #117) (#135).
Fixed
- Dashboard. Terminal-agent SSE streams no longer reconnect-storm into an
unbounded, duplicated timeline with runaway memory
(#105)
(#128); tool calls resolve
ok/failinstead of showing "running" forever (paired bytool_use_id) (#106) (#131); the fleet summary shows the workspace count, the misleading$0.0000cost is gone, and a favicon is served (#115, #109, #119) (#134). - API. Duplicate workspace registration returns
409 Conflict, not a misleading503(#111) (#139); an unknown agent's events endpoint returns404instead of200 [](#118) (#135);api_key_from_envon a keyless provider is rejected at config-set time, andrmno longer races a just-spawned agent or lags the fleet view (#120, #122, #123) (#137). - k8s hardening. The session-plane bearer token is never sent over plaintext
(#107)
(#133); unrecognized
CalibanTaskphases map to a terminal state,calibandEndpointis validated, lock poisoning can't wedge the fleet view, the token compare is constant-time, and--fleet-backend k8son a non-k8s build fails before any side effects (#114, #121, #125, #126, #127) (#136). - Tests. De-flaked the
distributed_busPG suite under parallel shared-DB load (#110) (#129) andcli_drives_the_full_stack(#85) (#94).
0.1.1 - 2026-07-05
Fixed
- The released image now builds
prosperodwith--features k8s, so theK8sFleetbackend is compiled in andPROSPERO_FLEET=k8sworks. Previously the image only ran the local backend, so an in-cluster deploy showed an empty fleet (#90). Unblocks the k8s-fleet-backend support in the prospero Helm chart.
0.1.0 - 2026-07-04
Initial containerized and licensed release of the prospero control plane —
the agent orchestration layer that sits above many caliband supervisors — as
part of the P0 Kubernetes deployment (epic
caliban-ai/caliban#274).
Added
ghcr.io/caliban-ai/prospero:0.1.0— multi-arch (linux/amd64 + linux/arm64), non-root container image runningprosperod(REST + SSE + dashboard on 7878); also tagged:latestand:sha-<commit>.- Helm chart
charts/prosperoin caliban-ai/helm-charts, rendering standalone (SQLite + PVC) or clustered (external Postgres, N replicas) from onetopologyvalue.
Changed
- Repository relicensed to AGPL-3.0-only, matching its sibling projects.