Skip to content

v0.6.0

Choose a tag to compare

@jmmana jmmana released this 12 Oct 03:39
· 44 commits to main since this release
e531019

[0.6.0] - 2026-10-11

"Precios y control" (pricing and control): a versioned, auditable server pricing table (#83) with a per-row,
list-price cost estimate computed at ingestion and recomputable later (#84); budgets per agent, model, session,
user or tag with server-computed ok/warning/exceeded/unknown state (#85); signed outbound alert delivery
when a budget crosses a threshold (#86); a library budget marker that is purely host-decided and hidden by
default (#87); a Model Ops Pricing editor and Budgets panel, and the office canvas wired to real budget state
(#88); and measured latency percentiles and failure rates per model (#89). This release is release-engineering and
documentation only (#90): three new guides (docs/pricing.md, docs/budgets.md, docs/webhooks.md), one
canonical webhook-signing reference replacing three partial copies, and the version bump.

Added

  • Crew CEO phone animation: 32 original RGBA drawings in four directions for pickup and conversation, reverse-frame hangup and return to idle, selected only from PHONE_CALL snapshots. Per-actor playback prevents another CEO from inheriting the shared phone pose; preserves call notices, reduced motion and fallback. Originals, metadata, tests and MP4 evidence retained (#316, Refs #118/#145).

  • Crew CEO think, talk and review: 48 original RGBA frames across four orientations, driven only by reported states with transition/cancellation, reduced-motion and localized fallback support. Preserved originals, measured metadata and runtime video/camera evidence (#280, Refs #118).

  • Budget alerts: the budget.alert canonical event, crossing detection, deduplication and storage (#319, sub-issue of #86, v0.6.0 milestone): src/integrations/canonicalTypes.ts gains 'budget.alert' in CANONICAL_EVENT_TYPES (additive, schema stays 1.0) and canonicalContract.ts a BudgetAlertPayloadSchema, server-emitted only. New server/budgetAlerts.ts adapts #86's original design (written before #85 shipped, assuming an onBudgetsEvaluated(BudgetEvaluation[]) hook with an arbitrary thresholds array) to the real, merged onBudgetTransition(BudgetTransition) hook (#85, sub-issue #305): buildBudgetAlert turns a transition landing on warning/exceeded into a deterministic evt_budget_ id (sha256 of budgetId/to/window.start, same construction as webhookEventId) and a payload named after the real BudgetStatus/BudgetTransition fields (knownSpent, the full BudgetCallCounts, a pricingVersions array), never the guessed names of #86's draft; a transition into ok or unknown is never an alert. New EventStore.recordBudgetAlert() (memory: a Set reserved synchronously before the method's only await; SQLite: one transaction, a new budget_alerts table with UNIQUE (budget_id, period_start, state), migration 15) is the entire deduplication mechanism: a real crossing offered any number of times (two concurrent live checks racing on the same transition, a re-imported history, a restart) is stored and broadcast exactly once. POST /api/v1/events and /events/batch now reject a client-submitted type: "budget.alert" or an id starting evt_budget_ (including via Idempotency-Key) with 400 reserved_event_type (batch: existing all-or-nothing validation-error shape). New tests/budget-alerts.test.mjs (pure buildBudgetAlert cases, a 50-concurrent-transition dedup race, a new-period re-arm case, two end-to-end crossings against the real app with SSE/reconnect replay, and the reserved-type/id guard on both ingest endpoints, run against both memory and SQLite storage); extended tests/canonical-contract.test.mjs, tests/event-fingerprint.test.mjs and tests/budgets-sse.test.mjs (the live event: budget transition frame stays unstored, but the new budget.alert event is a real canonical event and now legitimately appears in a reconnect replay). Deferred to later sub-issues of #86: the signed outbound webhook and its budget_alert_deliveries table (#321), GET /api/v1/budget-alerts (#322), the SDK verifyWebhookSignature helpers (#323), and the library trackUsage privacy guard plus the feature's README/docs narrative (#320).

  • Budget alerts: library trackUsage privacy guard and documentation (#320, sub-issue of #86, v0.6.0 milestone): applyExternalEvent now ignores every budget.alert event completely (not applied to any agent, task or meeting, never added to events) unless the host opts in with trackUsage: true, the exact same guard llm.usage already has; the embedded <AgentOffice> (always trackUsage: false) never sees a spend figure from an alert this way. A host that does pass trackUsage: true keeps the raw event in its own history but the library still renders none of its summary or payload. Documented in docs/library.md/docs/library.es.md and in the event table of docs/integration.md (server-emitted only, reserved evt_budget_ id prefix, rejected from client submission).

  • Budget alerts: signed outbound webhook delivery with retries (#321, sub-issue of #86, v0.6.0 milestone): new server/budgetAlertDelivery.ts. Every detected budget.alert (except a backfill: true one, from an already-closed period) is queued for delivery to AGENT_VIEWER_ALERT_WEBHOOK_URL when configured; with no URL, the feature is off and every delivery stays skipped. Each attempt POSTs the alert's exact payload, byte-identical on every retry, with X-Agent-Viewer-Event: budget.alert, X-Agent-Viewer-Delivery (the alert id, stable across retries), X-Agent-Viewer-Attempt, a fresh X-Agent-Viewer-Timestamp per attempt and, when AGENT_VIEWER_ALERT_WEBHOOK_SECRET is set, X-Agent-Viewer-Signature (the same ${timestamp}.${rawBody} hex HMAC-SHA256 scheme server/index.ts verifies inbound webhooks with, computed with this separate secret; omitted entirely, not faked, when no secret is set). Any 2xx succeeds; a network error, a timeout, 408, 429 or any 5xx retries with backoff min(5000ms * 2^(attempt-1), 1h) (uniform jitter in [base/2, base], a Retry-After header raising the floor); any other 4xx or a redirect fails immediately with no retry; after AGENT_VIEWER_ALERT_WEBHOOK_MAX_ATTEMPTS (default 6) the delivery is failed for good, a dead letter, logged with only the alert id and the last HTTP status. A plain http:// target is refused at startup unless loopback or AGENT_VIEWER_ALERT_WEBHOOK_ALLOW_HTTP=1; there is no SSRF protection on https:// targets today, a documented, current limitation, not yet a check. A background worker (startAlertDeliveryWorker) polls due deliveries; in SQLite mode a still-pending delivery resumes after a restart, in memory mode it is lost. New budget_alert_deliveries table (migration 16). New tests/budget-alert-delivery.test.mjs.

  • Budget alerts: SDK verifyWebhookSignature helpers (#323, sub-issue of #86, v0.6.0 milestone): a pure, constant-time, receiver-side verification helper for the outbound budget.alert webhook (#321), in both SDKs, mirroring the exact HMAC-SHA256 scheme server/index.ts already uses to verify an inbound webhook's signature: ${timestamp}.${rawBody}, hex digest. Python's verify_webhook_signature(secret, timestamp, raw_body, signature, tolerance_ms=300000, now_ms=None) (sdk/python/agent_viewer.py) compares with hmac.compare_digest; TypeScript's verifyWebhookSignature({ secret, timestamp, rawBody, signature, toleranceMs, nowMs }) (sdk/typescript/index.ts) hashes both sides with SHA-256 first (mirroring the server's own safeEqual) before crypto.timingSafeEqual, so a signature of any length never throws and never leaks its length through timing. Both reject a timestamp more than toleranceMs/tolerance_ms (default 300000, 5 minutes) away from nowMs/now_ms in either direction, return a plain boolean and never raise or throw. Neither SDK gains any way to build or emit a budget.alert event: that type stays server-reserved (#319); these functions only verify one a receiver already has. New tests in tests/test_python_sdk.py and tests/sdk-typescript.test.mjs, each building the expected signature independently with the same scheme, covering a good signature, a tampered body, a stale timestamp, a future timestamp beyond tolerance, the exact tolerance boundary, a wrong secret and (TypeScript) a signature of the wrong length. sdk/python/README.md documents the helper with a minimal receiver example.

  • Budget alerts: GET /api/v1/budget-alerts read API (#322, sub-issue of #86, v0.6.0 milestone): GET /api/v1/budget-alerts[?budgetId=&since=&limit=] lists recorded budget_alerts rows newest first by detectedAt (since is an epoch-ms floor; limit defaults to 100 and is capped at 500; a non-numeric since/limit is 400 validation_failed), each with its real delivery: { status, attempts, lastStatus, deliveredAt } embedded (read from #321's budget_alert_deliveries, migration 16), plus a webhook: { configured, signed } summary derived only from whether AGENT_VIEWER_ALERT_WEBHOOK_URL/_SECRET are set, never the URL or the secret itself. GET /api/v1/budget-alerts/:alertId reads one alert, or 404 { "error": "not_found" }. Same /api/v1 auth and rate limiting as every other route; no create/edit/redeliver endpoint, alerts are a pure consequence of spend. Written before it was certain whether #321 (the webhook and its budget_alert_deliveries migration) would merge first: EventStore.getBudgetAlertDeliveries still checks for the table via sqlite_master (never a hardcoded flag) and falls back to the honest PENDING_BUDGET_ALERT_DELIVERY placeholder (status: "pending") for any id it cannot resolve, so the endpoint stays correct regardless of merge order; #321 landed first in practice, so every alert now carries its real pending/delivered/failed/skipped status end to end. New tests/budget-alerts-api.test.mjs (auth, query validation, the newest-first/since/limit listing, 404, the real delivery/webhook shapes, and a store-level test covering the defensive fallback directly).

  • Budgets: documentation and performance benchmark (#307, closes the #85 epic, v0.6.0 milestone): new docs/budgets.md, the full reference for the budgets engine shipped across #302-#306 (data model, scope/period/timezone/basis, the four states and every unknownReason, the six CRUD routes with real captured request/response pairs, the snapshot.budgets field, the event: budget SSE frame and onBudgetTransition, and both SDKs' budgets()). README.md/docs/README.es.md gain a short "Budgets" section, three new API table rows and a mention under the "unknown is not zero, anywhere" rule; docs/integration.md gains a "Budgets" section pointing at the ledger columns it consumes without redefining them; docs/library.md/.es.md's "Budget state" section now names the server engine (#85) and docs/budgets.md that produce the BudgetState it only ever draws. New opt-in benchmark tests/budgets-bench.test.mjs (AGENT_VIEWER_BENCH=1, skipped by default so CI stays stable): 100 budgets across every scope type and period over a 1,000,000-row SQLite ledger inserted directly (bypassing ingestion), and two timed GET /api/v1/budgets passes against the real app. Measured against the code already merged by #302-#306: a cold pass took roughly 9 s and a warm pass roughly 6-7 s, both well past the issue's own 1.5 s/250 ms targets. Root cause, documented in docs/budgets.md's own "Performance" section: evaluateBudgetView calls the full 8-statement store.rollup() per budget only to read asOf/coverage, and the registry's evaluation cache is wired for the live SSE path (#305) but never consulted by the plain read path; closing that gap needs server production-code changes out of this documentation issue's scope, so it is left as a follow-up. #85 itself closes with this issue.

  • Portal: REST client for server-owned pricing and budgets (#332, sub-issue of #88, v0.6.0 milestone): new src/integrations/serverApi.ts (createServerApi), the typed client the Model Ops pricing editor and budgets panel (later sub-issues of #88) will build on. Adapts #88's own illustrative endpoint sketch to the real, shipped server (same kind of correction #319 made for budget.alert vs #86's draft): writing a price is POST /api/v1/pricing/changes (not a PUT per model), and a budget's state is embedded in GET /api/v1/budgets's BudgetView.status (there is no separate /budgets/state endpoint). Authorization: Bearer <token> only, never a query-string token; every request awaits getToken() first, so a caller whose token is still resolving never fires one too early. Every response is validated with zod at the boundary: a numeric field that is null on the wire (spent, ratio, every price field) stays null, an unrecognized BudgetStatus.state is mapped to 'unknown' (logging a contract warning), never to 'ok', and a response whose top-level shape does not parse is a 'contract' error. Every method returns { ok: true, data } | { ok: false, kind, ... }, never throwing for an HTTP or network failure, with kind one of unauthorized | forbidden | not_found | conflict | invalid | network | contract (a bare 5xx folds into network, same UI treatment as an unreachable server). POST/PUT writes send an Idempotency-Key header (auto-generated unless the caller supplies one to keep across a retry). New src/integrations/useServerApi.ts turns the portal's already-resolved connection (the same { baseUrl, token, tokenResolved } shape useModelOpsLedger.ts's LedgerConnection established for the usage ledger, #79) into a ServerApi, returning null in demo mode (no apiBase) or while the token has not finished resolving. server/index.ts's CORS preflight now allows PUT and DELETE (Access-Control-Allow-Methods), needed for the budgets editor's writes from a different dev origin; tests/express5-compat.test.mjs's existing preflight assertion is updated to match. New tests/lib/serverApi.test.ts and tests/lib/useServerApi.test.ts (26 cases: auth header/no query token, token-resolution wait, null-stays-null, unknown-state mapping, every error kind, Idempotency-Key on writes and not on reads, memoization) and a new CORS preflight case in tests/server-hardening.test.mjs. Consuming the client in the pricing editor, the budgets panel and the office's budget visualization are separate, later sub-issues of #88.

  • Portal: server pricing editor (#333, sub-issue of #88, v0.6.0 milestone): new src/components/PricingEditor.tsx, the only place in the portal that reads or writes the server's pricing catalog (GET /api/v1/pricing, POST /api/v1/pricing/changes, GET /api/v1/pricing/versions[/:version/diff], via serverApi.ts from #332). Mounted as ModelOpsModal's new "pricing" tab and inside SettingsModal, replacing the old local, client-only pricing table (App.tsx, AgentDetailModal.tsx, engine/officeModel.ts and types/agent.ts lose that dead state now that pricing lives only on the server). A table of priced models with per-row save, an add-model form and a version-history drawer; a price is number | null, never a bare 0 standing in for "unknown" (an emptied field saves null, a deliberately typed 0 saves and displays as 0, parsed by a strict parser, never parseFloat(...) || 0). Every row's editable state is a deep copy of the server's own entry, never the GET response object itself. Optimistic concurrency: a successful save's own pricingVersion becomes the expectedVersion for the next save; a 409 version_conflict keeps the operator's typed values untouched and offers a "Reapply" action against the server's currentVersion; a 403 switches the editor read-only without discarding loaded data. Demo mode (serverApi === null) shows an explanation only, with zero editable fields and zero requests. New tests/lib/pricingEditor.test.tsx (server rows, null/0 parsing, validation, conflict/reapply, sequential multi-row save version handoff, 403/404 states, add model, history drawer, immutability, demo mode).

  • Portal: Budgets tab in Model Ops (#334, sub-issue of #88, v0.6.0 milestone): new src/integrations/useBudgetState.ts, the hook owning every fetch the tab needs: GET /api/v1/budgets (via serverApi.ts) on connect, on tab open, after a successful write (the panel's own refresh()), on a 30s timer, and on a throttled trailing refresh (at most once per 5s) after a new llm.usage event; a monotonically increasing request id drops a stale response, and a failed refresh never clears the last good list, only sets stale: true and an error message. New src/components/modelOps/BudgetsPanel.tsx renders that list verbatim, including a budget's spent/ratio/state/evaluatedAt, with no client-side spent/limit arithmetic or state classification anywhere (state always drives the chip color, even when spent/limit would imply a different one); status === null (disabled or never evaluated) renders a neutral "no state yet" chip, never green, and spent === null renders a translated "Unknown", never 0. Create, edit and delete forms go through serverApi's createBudget/putBudget/deleteBudget, each refreshing the list on success; a create keeps the same Idempotency-Key for as long as the form stays open, including across a failed retry, so a double click cannot create a duplicate budget. The form only offers all/agent/model scopes; the wider session/user/tag scopes the server accepts are reachable only through a direct API call. New tests/lib/useBudgetState.test.tsx and tests/lib/budgetsPanel.test.tsx (the contradictory-values case where state wins over spent/limit, stale-response ordering, the 30s timer, the event throttle, create/edit/delete, idempotency key reuse).

  • Portal: wire real budget state into the office and agent panels (#335, sub-issue of #88, v0.6.0 milestone): the library side (AgentOffice/OfficeCanvas/canvasRenderer, issue #87) already accepted a budgetStateByAgent prop; this feeds it real server data for the first time. New src/integrations/budgetByAgent.ts: reduceBudgetStateByAgent(budgets) reduces useBudgetState's flat list to one verdict per agent id, counting only enabled: true budgets with scope.type === 'agent' (never scope.kind, matching the real BudgetView shape from #332), the most severe state winning by exceeded > warning > unknown > ok; an agent whose most severe state resolves to unknown is left out of the map entirely, passed through as absent, never as { state: 'ok' } or { state: 'unknown' }. agentBudgetRows(budgets, agentId) instead returns every budget (enabled or not) scoped to that agent id, verbatim, for the inspector panels. src/App.tsx adds a dedicated useServerApi/useBudgetState instance (mirroring how every other ledger consumer in the file already makes its own hook call), feeding budgetByAgent into OfficeCanvas and per-agent budgetRows into AgentInspector.tsx and AgentDetailModal.tsx, which show the selected/focused agent's matching budget row(s) as plain text (name, period, basis, state, limit, spent-or-unknown) with no ratio or percentage computed on the client. New agentPanels.budget.* translation keys (English and Spanish) in src/content/app/agentPanels.ts. New tests/lib/budgetByAgent.test.ts (the reduction and row-selection functions, including exceeded + unknown => exceeded, ok + unknown => absent, and budgets scoped to all/model/session/user/tag never affecting the per-agent map).

  • Docs: the real, shipped Pricing editor, Budgets tab and office budget wiring (#336, sub-issue of #88, v0.6.0 milestone): README.md/docs/README.es.md's Model Ops console bullet and demo app section now describe the Pricing and Budgets tabs added by #333/#334, and the Budgets collapsible section now mentions the portal's own Budgets tab UI. docs/integration.md gains a new "Pricing and Budgets from the portal" section documenting src/integrations/serverApi.ts's real methods (getPricing/putPricingChanges/getPricingVersions/getPricingVersionDiff/getBudgets/getBudget/createBudget/putBudget/deleteBudget/getBudgetRevisions), the header-only Bearer auth, the error taxonomy, and the CORS Access-Control-Allow-Methods change (PUT/DELETE added by #332) the budgets editor's writes depend on. Written against the real merged code rather than issue #88's own illustrative sketch, whose field and method names (a per-model PUT, scope.kind, a separate /budgets/state endpoint) do not exist in the shipped server or client. docs/library.md/.es.md's existing "Budget state" section (from #87/#330) already documented budgetStateByAgent as a host-supplied prop and the library's own officeMessages key count (162) accurately, so neither needed a change. No code change, no new environment variable.

  • Release documentation: pricing, budgets and webhooks guides, and the v0.6.0 release notes (#90): new docs/webhooks.md, the one canonical page for both the inbound POST /api/v1/webhooks/generic signature scheme and the outbound budget.alert delivery (#86, #321), replacing the partial, duplicated copies in README.md, docs/integration.md, docs/README.es.md and .github/SECURITY.md (each now a two-line summary and a link). docs/pricing.md gains the costSource table (reported/estimated/unknown), a worked per-token-class estimate formula, a "What is never assumed" list with a Test: reference per item, a re-estimating/recompute section, an invoice-reconciliation recipe over the CSV export, an embedders note and a 0.5.x upgrade note; corrects a stale "issue #84, not yet implemented" claim now that #84/#295/#296 are merged. docs/budgets.md gains Alerts delivery (#86), Portal (#88) and Latency-and-errors (#89) sections, and a "Limits and honesty notes" section stating plainly that budgets are monitoring, never a gate, since Agent Viewer is not a proxy. docs/integration.md corrects a conflated claim that the server writes costSource: "estimated" onto a ledger row (it never does; the server's own estimate is the separate estimatedCost/estimateStatus family of fields) and fixes stale "always 0 today" wording in docs/usage-export.md now that pricing recompute (#296) can produce a later revision. No code or behavior change.

Changed

  • Version bumped to 0.6.0 across package.json, package-lock.json, sdk/python/pyproject.toml, sdk/python/README.md's wheel filename, the README badge and install snippets (English and Spanish), and docs/library.md/docs/library.es.md's guide version and install URL.
  • server/.env.example gains AGENT_VIEWER_HOST, AGENT_VIEWER_ALLOW_OPEN, AGENT_VIEWER_SSE_REPLAY_MAX, AGENT_VIEWER_EXPORT_MAX_ROWS, AGENT_VIEWER_EXPORT_CONCURRENCY and AGENT_VIEWER_RECOMPUTE_MAX_ROWS, matching the README configuration table.

Behavior you may notice

Estimated cost is new on the usage ledger, calls, rollup and export (estimatedCost/estimateStatus/pricingVersion
and related columns), and it is always labeled and shown separately from a provider-reported cost: nothing that
existed before this release changes meaning, and an estimate never replaces or blends into a reported figure.
Budgets, budget alerts, the Model Ops Pricing and Budgets tabs, and the office's budget marker are all new,
opt-in surfaces with no effect until an operator configures a price table entry, creates a budget or sets the
outbound alert webhook URL.