Repository navigation
v0.6.0
[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.alertcanonical event, crossing detection, deduplication and storage (#319, sub-issue of #86, v0.6.0 milestone):src/integrations/canonicalTypes.tsgains'budget.alert'inCANONICAL_EVENT_TYPES(additive, schema stays1.0) andcanonicalContract.tsaBudgetAlertPayloadSchema, server-emitted only. Newserver/budgetAlerts.tsadapts #86's original design (written before #85 shipped, assuming anonBudgetsEvaluated(BudgetEvaluation[])hook with an arbitrarythresholdsarray) to the real, mergedonBudgetTransition(BudgetTransition)hook (#85, sub-issue #305):buildBudgetAlertturns a transition landing onwarning/exceededinto a deterministicevt_budget_id (sha256 ofbudgetId/to/window.start, same construction aswebhookEventId) and a payload named after the realBudgetStatus/BudgetTransitionfields (knownSpent, the fullBudgetCallCounts, apricingVersionsarray), never the guessed names of #86's draft; a transition intookorunknownis never an alert. NewEventStore.recordBudgetAlert()(memory: aSetreserved synchronously before the method's onlyawait; SQLite: one transaction, a newbudget_alertstable withUNIQUE (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/eventsand/events/batchnow reject a client-submittedtype: "budget.alert"or an id startingevt_budget_(including viaIdempotency-Key) with400 reserved_event_type(batch: existing all-or-nothing validation-error shape). Newtests/budget-alerts.test.mjs(purebuildBudgetAlertcases, 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); extendedtests/canonical-contract.test.mjs,tests/event-fingerprint.test.mjsandtests/budgets-sse.test.mjs(the liveevent: budgettransition frame stays unstored, but the newbudget.alertevent 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 itsbudget_alert_deliveriestable (#321),GET /api/v1/budget-alerts(#322), the SDKverifyWebhookSignaturehelpers (#323), and the librarytrackUsageprivacy guard plus the feature's README/docs narrative (#320). -
Budget alerts: library
trackUsageprivacy guard and documentation (#320, sub-issue of #86, v0.6.0 milestone):applyExternalEventnow ignores everybudget.alertevent completely (not applied to any agent, task or meeting, never added toevents) unless the host opts in withtrackUsage: true, the exact same guardllm.usagealready has; the embedded<AgentOffice>(alwaystrackUsage: false) never sees a spend figure from an alert this way. A host that does passtrackUsage: truekeeps the raw event in its own history but the library still renders none of itssummaryorpayload. Documented indocs/library.md/docs/library.es.mdand in the event table ofdocs/integration.md(server-emitted only, reservedevt_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 detectedbudget.alert(except abackfill: trueone, from an already-closed period) is queued for delivery toAGENT_VIEWER_ALERT_WEBHOOK_URLwhen configured; with no URL, the feature is off and every delivery staysskipped. Each attemptPOSTs the alert's exact payload, byte-identical on every retry, withX-Agent-Viewer-Event: budget.alert,X-Agent-Viewer-Delivery(the alert id, stable across retries),X-Agent-Viewer-Attempt, a freshX-Agent-Viewer-Timestampper attempt and, whenAGENT_VIEWER_ALERT_WEBHOOK_SECRETis set,X-Agent-Viewer-Signature(the same${timestamp}.${rawBody}hex HMAC-SHA256 schemeserver/index.tsverifies inbound webhooks with, computed with this separate secret; omitted entirely, not faked, when no secret is set). Any2xxsucceeds; a network error, a timeout,408,429or any5xxretries with backoffmin(5000ms * 2^(attempt-1), 1h)(uniform jitter in[base/2, base], aRetry-Afterheader raising the floor); any other4xxor a redirect fails immediately with no retry; afterAGENT_VIEWER_ALERT_WEBHOOK_MAX_ATTEMPTS(default 6) the delivery isfailedfor good, a dead letter, logged with only the alert id and the last HTTP status. A plainhttp://target is refused at startup unless loopback orAGENT_VIEWER_ALERT_WEBHOOK_ALLOW_HTTP=1; there is no SSRF protection onhttps://targets today, a documented, current limitation, not yet a check. A background worker (startAlertDeliveryWorker) polls due deliveries; in SQLite mode a still-pendingdelivery resumes after a restart, in memory mode it is lost. Newbudget_alert_deliveriestable (migration 16). Newtests/budget-alert-delivery.test.mjs. -
Budget alerts: SDK
verifyWebhookSignaturehelpers (#323, sub-issue of #86, v0.6.0 milestone): a pure, constant-time, receiver-side verification helper for the outboundbudget.alertwebhook (#321), in both SDKs, mirroring the exact HMAC-SHA256 schemeserver/index.tsalready uses to verify an inbound webhook's signature:${timestamp}.${rawBody}, hex digest. Python'sverify_webhook_signature(secret, timestamp, raw_body, signature, tolerance_ms=300000, now_ms=None)(sdk/python/agent_viewer.py) compares withhmac.compare_digest; TypeScript'sverifyWebhookSignature({ secret, timestamp, rawBody, signature, toleranceMs, nowMs })(sdk/typescript/index.ts) hashes both sides with SHA-256 first (mirroring the server's ownsafeEqual) beforecrypto.timingSafeEqual, so a signature of any length never throws and never leaks its length through timing. Both reject atimestampmore thantoleranceMs/tolerance_ms(default 300000, 5 minutes) away fromnowMs/now_msin either direction, return a plain boolean and never raise or throw. Neither SDK gains any way to build or emit abudget.alertevent: that type stays server-reserved (#319); these functions only verify one a receiver already has. New tests intests/test_python_sdk.pyandtests/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.mddocuments the helper with a minimal receiver example. -
Budget alerts:
GET /api/v1/budget-alertsread API (#322, sub-issue of #86, v0.6.0 milestone):GET /api/v1/budget-alerts[?budgetId=&since=&limit=]lists recordedbudget_alertsrows newest first bydetectedAt(sinceis an epoch-ms floor;limitdefaults to 100 and is capped at 500; a non-numericsince/limitis400 validation_failed), each with its realdelivery: { status, attempts, lastStatus, deliveredAt }embedded (read from #321'sbudget_alert_deliveries, migration 16), plus awebhook: { configured, signed }summary derived only from whetherAGENT_VIEWER_ALERT_WEBHOOK_URL/_SECRETare set, never the URL or the secret itself.GET /api/v1/budget-alerts/:alertIdreads one alert, or404 { "error": "not_found" }. Same/api/v1auth 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 itsbudget_alert_deliveriesmigration) would merge first:EventStore.getBudgetAlertDeliveriesstill checks for the table viasqlite_master(never a hardcoded flag) and falls back to the honestPENDING_BUDGET_ALERT_DELIVERYplaceholder (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 realpending/delivered/failed/skippedstatus end to end. Newtests/budget-alerts-api.test.mjs(auth, query validation, the newest-first/since/limitlisting, 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 everyunknownReason, the six CRUD routes with real captured request/response pairs, thesnapshot.budgetsfield, theevent: budgetSSE frame andonBudgetTransition, and both SDKs'budgets()).README.md/docs/README.es.mdgain a short "Budgets" section, three new API table rows and a mention under the "unknown is not zero, anywhere" rule;docs/integration.mdgains 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 theBudgetStateit only ever draws. New opt-in benchmarktests/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 timedGET /api/v1/budgetspasses 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 indocs/budgets.md's own "Performance" section:evaluateBudgetViewcalls the full 8-statementstore.rollup()per budget only to readasOf/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.#85itself 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 forbudget.alertvs #86's draft): writing a price isPOST /api/v1/pricing/changes(not aPUTper model), and a budget's state is embedded inGET /api/v1/budgets'sBudgetView.status(there is no separate/budgets/stateendpoint).Authorization: Bearer <token>only, never a query-string token; every request awaitsgetToken()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 isnullon the wire (spent,ratio, every price field) staysnull, an unrecognizedBudgetStatus.stateis 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, withkindone ofunauthorized | forbidden | not_found | conflict | invalid | network | contract(a bare 5xx folds intonetwork, same UI treatment as an unreachable server).POST/PUTwrites send anIdempotency-Keyheader (auto-generated unless the caller supplies one to keep across a retry). Newsrc/integrations/useServerApi.tsturns the portal's already-resolved connection (the same{ baseUrl, token, tokenResolved }shapeuseModelOpsLedger.ts'sLedgerConnectionestablished for the usage ledger, #79) into aServerApi, returningnullin demo mode (noapiBase) or while the token has not finished resolving.server/index.ts's CORS preflight now allowsPUTandDELETE(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. Newtests/lib/serverApi.test.tsandtests/lib/useServerApi.test.ts(26 cases: auth header/no query token, token-resolution wait, null-stays-null, unknown-state mapping, every error kind,Idempotency-Keyon writes and not on reads, memoization) and a new CORS preflight case intests/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], viaserverApi.tsfrom #332). Mounted asModelOpsModal's new "pricing" tab and insideSettingsModal, replacing the old local, client-only pricing table (App.tsx,AgentDetailModal.tsx,engine/officeModel.tsandtypes/agent.tslose 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 isnumber | null, never a bare0standing in for "unknown" (an emptied field savesnull, a deliberately typed0saves and displays as0, parsed by a strict parser, neverparseFloat(...) || 0). Every row's editable state is a deep copy of the server's own entry, never theGETresponse object itself. Optimistic concurrency: a successful save's ownpricingVersionbecomes theexpectedVersionfor the next save; a409 version_conflictkeeps the operator's typed values untouched and offers a "Reapply" action against the server'scurrentVersion; a403switches the editor read-only without discarding loaded data. Demo mode (serverApi === null) shows an explanation only, with zero editable fields and zero requests. Newtests/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(viaserverApi.ts) on connect, on tab open, after a successful write (the panel's ownrefresh()), on a 30s timer, and on a throttled trailing refresh (at most once per 5s) after a newllm.usageevent; a monotonically increasing request id drops a stale response, and a failed refresh never clears the last good list, only setsstale: trueand an error message. Newsrc/components/modelOps/BudgetsPanel.tsxrenders that list verbatim, including a budget'sspent/ratio/state/evaluatedAt, with no client-sidespent/limitarithmetic or state classification anywhere (statealways drives the chip color, even whenspent/limitwould imply a different one);status === null(disabled or never evaluated) renders a neutral "no state yet" chip, never green, andspent === nullrenders a translated "Unknown", never0. Create, edit and delete forms go throughserverApi'screateBudget/putBudget/deleteBudget, each refreshing the list on success; a create keeps the sameIdempotency-Keyfor as long as the form stays open, including across a failed retry, so a double click cannot create a duplicate budget. The form only offersall/agent/modelscopes; the widersession/user/tagscopes the server accepts are reachable only through a direct API call. Newtests/lib/useBudgetState.test.tsxandtests/lib/budgetsPanel.test.tsx(the contradictory-values case wherestatewins overspent/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 abudgetStateByAgentprop; this feeds it real server data for the first time. Newsrc/integrations/budgetByAgent.ts:reduceBudgetStateByAgent(budgets)reducesuseBudgetState's flat list to one verdict per agent id, counting onlyenabled: truebudgets withscope.type === 'agent'(neverscope.kind, matching the realBudgetViewshape from #332), the most severe state winning byexceeded > warning > unknown > ok; an agent whose most severe state resolves tounknownis 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.tsxadds a dedicateduseServerApi/useBudgetStateinstance (mirroring how every other ledger consumer in the file already makes its own hook call), feedingbudgetByAgentintoOfficeCanvasand per-agentbudgetRowsintoAgentInspector.tsxandAgentDetailModal.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. NewagentPanels.budget.*translation keys (English and Spanish) insrc/content/app/agentPanels.ts. Newtests/lib/budgetByAgent.test.ts(the reduction and row-selection functions, includingexceeded+unknown=>exceeded,ok+unknown=> absent, and budgets scoped toall/model/session/user/tagnever 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.mdgains a new "Pricing and Budgets from the portal" section documentingsrc/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 CORSAccess-Control-Allow-Methodschange (PUT/DELETEadded 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-modelPUT,scope.kind, a separate/budgets/stateendpoint) do not exist in the shipped server or client.docs/library.md/.es.md's existing "Budget state" section (from #87/#330) already documentedbudgetStateByAgentas a host-supplied prop and the library's ownofficeMessageskey 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 inboundPOST /api/v1/webhooks/genericsignature scheme and the outboundbudget.alertdelivery (#86, #321), replacing the partial, duplicated copies inREADME.md,docs/integration.md,docs/README.es.mdand.github/SECURITY.md(each now a two-line summary and a link).docs/pricing.mdgains thecostSourcetable (reported/estimated/unknown), a worked per-token-class estimate formula, a "What is never assumed" list with aTest: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.mdgains 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.mdcorrects a conflated claim that the server writescostSource: "estimated"onto a ledger row (it never does; the server's own estimate is the separateestimatedCost/estimateStatusfamily of fields) and fixes stale "always 0 today" wording indocs/usage-export.mdnow that pricing recompute (#296) can produce a later revision. No code or behavior change.
Changed
- Version bumped to
0.6.0acrosspackage.json,package-lock.json,sdk/python/pyproject.toml,sdk/python/README.md's wheel filename, the README badge and install snippets (English and Spanish), anddocs/library.md/docs/library.es.md's guide version and install URL. server/.env.examplegainsAGENT_VIEWER_HOST,AGENT_VIEWER_ALLOW_OPEN,AGENT_VIEWER_SSE_REPLAY_MAX,AGENT_VIEWER_EXPORT_MAX_ROWS,AGENT_VIEWER_EXPORT_CONCURRENCYandAGENT_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.