Repository navigation
Releases: jmmana/Agent-Viewer
Release list
v0.8.0
[0.8.0] - 2026-10-13
"Auditoria formal" (formal audit): a SHA-256 hash chain over every stored event (#105) with periodic Ed25519-signed
checkpoints (#387, #106), named ingestion keys with submitter identity (#107) and scoped read-only auditor keys
(#108), chain-compatible retention and redaction that tombstones a row without breaking the chain (#109), and an
offline-verifiable audit bundle with a standalone zero-dependency verifier script (#110). This release also ships
the audit guide, key management guide, threat model, migration notes from the single shared token, and a docs
hygiene check wired into CI (#111, split into sub-issues #408 to #411 per .claude/agent-policy.md's size rule).
The guarantee, in one sentence, from docs/threat-model.md: "a hash chain proves that stored history was not
altered after the fact; it does not prove that what an agent reported is true."
Upgrading
Purely additive: no breaking change to the event contract, the HTTP API or the embeddable library's props. The
hash chain, checkpoints, named/scoped keys and the audit bundle are all SQLite-only and opt-in by use; a
deployment running memory storage or the single shared AGENT_VIEWER_API_TOKEN keeps working exactly as before,
with every audit route reporting enabled: false or 409 audit_requires_chain in memory mode. See
docs/migrating-to-0.8.md for the practical runbook from the single shared token to
named keys, and docs/threat-model.md for what the chain proves and what it does not.
Known gaps
GET /api/v1/audit/verify/agent-viewer verifywere not tombstone-aware until the fix below landed in this
same release; any 0.8.0 deployment running an earlier pre-release build of this milestone should re-verify.- Closing an open SSE connection when its key is revoked or expires,
submittedByon SSE reconnect-replay
frames, andsubmittedByas a rollup/export dimension remain deferred (documented on issue #108). - Pricing-version digests in the audit bundle are not yet chain-anchored (documented in
server/audit/bundle.ts
anddocs/audit-bundle.md, deferred to a follow-up of #83). - A full Spanish translation of
docs/audit.md,docs/key-management.md/docs/api-keys.mdand
docs/threat-model.mdis deferred; the Spanish README mirrors only the changed Security table rows.
Fixed
GET /api/v1/audit/verify/agent-viewer verifyare now tombstone-aware (issue #408, v0.8.0 milestone): a row legitimately tombstoned by issue #109's retention/redaction routes rewrites its ownevent_jsonby design, so it never recomputes to its own storedhashagain; before this fixserver/audit/verify.ts's walk had no tombstone handling at all and reportedhash_mismatchat the first such row, indistinguishable from real tampering, contradicting #109's and the 0.8.0 exit criterion's stated goal ("after a retention purge the chain still verifies through tombstones"). The walk now skips the content-hash recompute for a row withtombstoned_at IS NOT NULL(its link to the previous row is still checked normally, since links are never touched by a tombstone) and instead cross-checks it against the chainedaudit.retention.purged/audit.redaction.appliedevent itstombstone_audit_seqnames: that event must exist, be of the right type, and have committed exactly this row's originalhash/content_hash, or the newtombstone_invalidBreakKindis reported (a forged or missing tombstone is not a silent pass). Mirrors the logicscripts/verify-audit-bundle.mjs'sREDACTIONcheck (#401) already implemented andtests/audit-retention.test.mjs'stombstoneAwareWalkhelper already pinned in tests.docs/audit-chain.mdgains a "Tombstones do not break verification" section;docs/retention.md's "Known limits" entry for this gap is removed (closed, not reworded);docs/threat-model.md's "what the chain does not prove" bullet about this gap moves to "what the chain proves", corrected. New tests intests/audit-verify.test.mjs: a clean tombstoned chain, a tombstone pointing nowhere, one pointing at a real but unrelated row, and one whose committing audit event records the wrong original hash.
Added
-
Migration guide from the single shared token (issue #409, sub-issue of #111, docs only): new
docs/migrating-to-0.8.mdcovers the practical runbook for an operator running today's single shared
AGENT_VIEWER_API_TOKEN: what changes (nothing functional) for the bootstrap token, the CLI's generated
session token, the Dockerapi-tokenfile (AGENT_VIEWER_TOKEN_FILE) andAGENT_VIEWER_WEBHOOK_SECRET;
step by step from backing up the SQLite file through creating one named ingest key per client, an auditor
key, switching clients over and retiring routine use of the bootstrap token; and the exact, code-verified
shape in which rows stored before the hash chain existed are reported byGET /api/v1/audit/verify/
agent-viewer verify(chain.unchainedcount plus alegacy_rowswarning, never a separate "pre-chain"
status, never silently counted as individually verified). Linked fromREADME.md's and
docs/README.es.md's Security tables and fromdocs/api-keys.md's bootstrap-token section. New
tests/audit-migration.test.mjs: builds a SQLite database staged at schema version 16 (the "0.7.0 shape",
the same staging patterntests/audit-verify.test.mjs's legacy-rows case and
tests/usage-rollup-migration.test.mjsalready use), appends pre-chain events, starts the real 0.8.0 server
on that file (running migrations 17-22) and asserts both documented claims over real HTTP. No server, CLI,
SDK or library runtime code changed. -
Docs hygiene check in CI: relative links, README linkage, em dash scan (issue #410, sub-issue of #111): new
scripts/check-docs.mjs,npm run check:docs, wired into.github/workflows/ci.ymlright after the existing token/api_key doc-content check. Three checks: (1) every relative markdown link and image inREADME.md, the top-leveldocs/*.mdfiles (includingdocs/README.es.md) and.github/SECURITY.mdresolves to a real file, and a#anchorresolves to a real heading in the target, using a from-scratch reimplementation of GitHub's own heading-slug algorithm (code spans stripped, non letter/digit/space/hyphen/underscore characters dropped, then each remaining whitespace character replaced with a hyphen individually, deliberately without trimming or collapsing runs, which is what makes emoji-prefixed headings slug the way GitHub actually renders them); (2) every 0.8.0 doc this issue names (docs/audit.md,docs/audit-chain.md,docs/audit-bundle.md,docs/api-keys.md,docs/retention.md,docs/threat-model.md) is linked fromREADME.md; (3) no U+2014 in any file changed since thev0.7.0tag (git diff v0.7.0..HEAD, added/copied/modified/renamed only), chosen over a repo-wide retroactive scan because the latter already failed on untouched pre-0.7.0 prose outside this issue's scope. Running this script against the current repo surfaced and fixed real pre-existing bugs it was built to catch: two em dashes indocs/retention.mdand one each indocs/crew/evidence/{ambience-148,lamps-149}/README.md(all introduced sincev0.7.0), four links to a staledocs/otlp.md#otlp-metrics-post-v1metricsanchor missing the heading's own-issue-73suffix (README.md, docs/README.es.md, docs/integration.md, docs/otlp.md's own table of contents), two links to a staledocs/integration.md#reconnect-replay-and-resyncanchor missing-issue-54(docs/library.md, docs/library.es.md), anddocs/audit.mditself, shipped with issue #388, never having been linked fromREADME.mdat all (added to the existingGET /api/v1/audit/verifyrow). Newtests/check-docs.test.mjs: pure-function unit tests for the slug algorithm (including the two real, previously-fragile emoji/ampersand anchors above as fixtures) and link/em-dash checks against synthetic fixture repos, plus CLI invocations against the real repo. The CI workflow needs no extra checkout change: the script resolvesv0.7.0itself, fetching just that one tag shallowly fromoriginif the default shallow checkout does not already have it. -
Threat model for the 0.8.0 formal-audit work (issue #111, first sub-issue, docs only): new
docs/threat-model.mdstates, in one place, what the hash chain (#105), signed checkpoints (#387), chain verification (#106), named and scoped keys (#107/#108), chain-compatible retention/redaction (#109) and the offline-verifiable audit bundle (#110) actually guarantee: the assets at risk, every actor from a compromised agent to an operator holding both the database and the signing key, explicit "what the chain proves" and "what it does not" sections, memory-mode behavior (every audit route refuses rather than returning a degraded result), bundle confidentiality (an exported bundle is not redacted, unlikeGET /api/v1/usage/export), the invoice-cross-check rules (unknown is never zero, estimated is never mixed with reported), residual risks and recommended mitigations, and explicit non-goals (not tamper-proof against a fully privileged operator, not a compliance certification, not provider billing). Cross-linked fromREADME.md/docs/README.es.md's Security tables,.github/SECURITY.md(which also gets a corrected "Data exfiltration surface" paragraph reflecting shipped scoped keys instead of describing them as a future 0.8.0 item),docs/integration.md's audit-trail section,docs/audit-chain.mdanddocs/api-keys.md. No server, CLI, SDK or library runtime code changed. Also documents, rather than silently assumes, thatGET /api/v1/audit/verify/agent-viewer verifyare not yet tombstone-aware and reporthash_mismatchon a legitimately redacted/retained row even though the standalone bundle verifier (#401) already handles this correctly; filed as [issue #408](https://github.com/jmmana/A...
v0.7.0
[0.7.0] - 2026-10-12
"Captura automática e interoperabilidad" (automatic capture and interoperability): an OTLP/HTTP GenAI traces
receiver and file import, packaged LangGraph and OpenAI Agents SDK adapters tested against the real
frameworks in CI, a durable local queue in both SDKs, and a Prometheus /metrics export (core endpoint,
ledger-derived families, in-process ingestion counters, optional OTLP metrics push).
Upgrading
Purely additive: no breaking change to the event contract, the HTTP API, or the embeddable library's props.
Every new setting defaults to off or to its existing behavior, so an existing deployment needs no
configuration change to keep working.
- The SDK durable queue (Python
spool_dir=..., TypeScriptspoolDir/AGENT_VIEWER_SPOOL_DIR) is opt-in:
without it, both SDKs behave exactly as before (an unrecovered transient failure still raises/throws). GET /metricsis opt-in (AGENT_VIEWER_METRICS=1, CLI--metrics); unset, the route answers404
exactly like before this release. See docs/metrics.md for every new environment
variable it reads.- The OTLP traces receiver (
POST /v1/traces) and file import share the existing OTLP auth, rate limit and
size-limit settings already in place for/v1/logs//v1/metrics; no new required variable. examples/openai-agents-adapter/is removed (dead code since issue #361); import
@warlockcode/agent-viewer-openai-agentsinstead if anything in your own tree still referenced that path
(see the Removed section below).
Added
- Durable local queue in the Python SDK (#101, v0.7.0 milestone): optional, bounded, on-disk spool (
spool_dir=..., stdlibsqlite3only, no third-party dependency) so a transient failure (connection down, reset, timeout,408,429, any5xx) after the normal retries never raises into the host agent. Events are written to disk with their originalidandtimestampand replayed in enqueue order once the server returns, throughPOST /api/v1/events/batch, so the server's own idempotency byidkeeps totals honest. NewAgentViewerarguments (spool_dir,spool_max_events,spool_max_bytes,spool_overflow,spool_batch_size,spool_background,spool_probe_interval,spool_max_probe_interval) and methods (flush(),spool_stats(),spool_rejected(),close(), usable as a context manager, plus anatexitfinal flush). A size cap evicts the oldest non-usage events before usage events (drop_oldest, the default) or refuses the new event (drop_newest); every drop and every server-side rejection is counted inSpoolStats, never silently lost, and the spool never sums, prices or defaults usage (a missingcoststays absent, never0). Also fixes transient-failure classification with the spool disabled: a rawOSError/http.client.HTTPExceptionand HTTP408are now retried instead of escaping or failing immediately. Newdocs/sdk-queue.md(shared contract with the TypeScript SDK, #102) andtests/fixtures/sdk-queue-scenarios.json(shared test vectors). Newtests/test_python_sdk_spool.py(34 tests: outages, restart persistence, overflow matrix, poison events,413/429/401handling, corrupt/newer-schema spool files, permissions, fork safety, a seeded randomized concurrency test). CI now runs both Python SDK test files and adds a Python 3.9 job to exercise the SDK's declared minimum version. - SDK: per-event
sessionIdonemit()/emitBatch()anddurationMsontoolCompleted()/toolFailed()(first sub-issue of #99, v0.7.0 milestone): prerequisite for the packaged LangGraph adapter (#99), which serves manythread_ids through one process and builds events through the low-levelemit/emitBatchpath, not through a per-sessionAgentViewerinstance. TypeScript'sEmitEventInput.sessionId?(sdk/typescript/index.ts) now overrides the client's ownsessionIdfor that one event in bothemit()andemitBatch()(previously hardcoded tothis.sessionId, ignoring any such field on the input); Python'sAgentViewer.emit()gains the samesession_id/timestamp_mskeyword argumentsemit_batch()already had per item, so a callback's own fire time can be recorded instead of the time it reaches a background queue.ToolCallFinishOptions(toolCallIdplusdurationMs) is the new option type fortoolCompleted()/toolFailed()(TypeScript) andduration_msthe new keyword fortool_completed()/tool_failed()(Python), matching the contract:durationMsis valid onToolCompletedPayloadSchema/ToolFailedPayloadSchemabut notToolStartedPayloadSchema, sotoolStarted()/tool_started()do not accept it. New tests intests/sdk-typescript.test.mjsandtests/test_python_sdk.pycover the per-event session override and the duration field landing only on completed/failed. Deferred to later sub-issues of #99: thepackages/sdknpm workspace extraction (#343), the TypeScript (#344) and Python (#345) LangGraph adapters and their recorded-run fixtures, and the release/documentation plumbing (#346). packages/sdk: publishable npm workspace extraction of the TypeScript SDK (second sub-issue of #99, v0.7.0 milestone, issue #343): the rootpackage.jsonnow declares"workspaces": ["packages/*"], and the logic that used to live only atsdk/typescript/index.tsmoved to the new workspace package@warlockcode/agent-viewer-sdk(packages/sdk/src/index.ts), with no runtime or type import ofsrc/integrations/*orzod: every structural type it needs (CanonicalEvent,CanonicalEventType,EventSeverity,LlmErrorKind, thellm.usage/llm.failed/tool.*payload shapes,ViewerSnapshot,UsageSummary,CallRecord, the usage rollup types, the budget types,UsageFigures) is copied, not imported, from the root contract and server modules, so the published.d.tsreferences no file outside the package.sdk/typescript/index.tsis now a thin re-export of the package, sotests/sdk-typescript.test.mjsand every example adapter keep working unchanged. A new CI type test (tests/types/sdk-package-canonical-event.ts, checked bynpm run lint) asserts the package'sCanonicalEvent/CanonicalEventType/EventSeverity/LlmErrorKindstay assignable to the root contract's types in both directions.packages/sdk/package.jsonhasexports,files,publishConfig.access: public, is ESM only, has its owntsconfig.jsonemitting declarations, and acheck:packagescript runningpublint --strictandattw --pack .(also runnable at the root asnpm run check:package:sdk). The rootnpm run build:packages(npm run build --workspaces --if-present) builds every workspace package and now runs automatically beforenpm run lintandnpm test(prelint/pretest), since thesdk/typescript/index.tsre-export needspackages/sdk/distto exist to resolve the package by name.docker/Dockerfile'sdepsstage now also copiespackages/sdk/package.jsonbeforenpm ci, so the workspace lockfile resolves inside the image. CI gained three new steps: building the workspace packages,check:package:sdk, and a packed-tarball smoke test (npm packthe package, install the.tgzinto a throwaway ESM project, import it).packages/sdk/README.mddocuments the package and the workspace layout convention (issue #100's OpenAI Agents adapter reuses this same layout). Deferred to later sub-issues of #99: the TypeScript (#344) and Python (#345) LangGraph adapters and their recorded-run fixtures, and the release/documentation plumbing (#346) that will actually publish this package to npm.packages/openai-agents: publishable npm workspace extraction of the OpenAI Agents adapter (second sub-issue of #100, v0.7.0 milestone, issue #361):examples/openai-agents-adapter/processor.ts/index.tsmove, unchanged in behavior, to the new sibling workspace package@warlockcode/agent-viewer-openai-agents(packages/openai-agents/src/), reusingpackages/sdk's exact layout (issue #343) rather than inventing a new convention: sametsconfig.jsonshape, sameexports/files/publishConfigfields, samecheck:package(publint --strict && attw --pack . --profile esm-only), no second"workspaces"field at the root. The processor now importsAgentViewer/EmitEventInputfrom the published@warlockcode/agent-viewer-sdkpackage instead of the repo-relativesdk/typescript/index.tspath;@openai/agentsis apeerDependenciesentry (the host brings its own version). Deviates from issue #361's own text in one place, documented inpackages/openai-agents/README.md: the issue lists@warlockcode/agent-viewer(the main library package) as a peer dependency, but the processor never imports anything from it, only from@warlockcode/agent-viewer-sdk; that is declared as a regulardependenciesentry instead (an internal implementation detail, not a version the host needs to pin like@openai/agents).tests/openai-agents-adapter.test.mjsnow imports the processor from its new path; its test suite, assertions and thetests/examples-adapters.test.mjsdoc comment are otherwise unchanged. Rootbuild:packagesnow buildspackages/sdkbeforepackages/openai-agentsexplicitly (npm run build --workspaceshas no dependency-graph ordering, and this package's.tssources import the sdk package by name, needing itsdist/first); a newnpm run check:package:openai-agentsscript, a CI step for it, and a packed-tarball smoke test (installing both this package's andpackages/sdk's local tarballs together, since neither is on the public npm registry yet, plus the real@openai/agents) are added alongside the existingpackages/sdkones..github/workflows/release.ymlgains arelease-openai-agentsjob (needsrelease) publishing@warlockcode/agent-viewer-openai-agentswith provenance after the main package, same skip-without-NPM_TOKEN/skip-on-prerelease pattern, plus a version-lockstep check against the ...
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/`si...
v0.5.0
[0.5.0] - 2026-10-10
"Spend in the office": per-agent usage badges and a per-call detail panel on the canvas, fed only by host props (#76, #77); the portal reading those same figures from the real usage ledger for canvas badges, the agent inspector and the call-detail modal (#78); cost per meeting and per tool computed on the server and shown in the Meetings panel and tool tooltips (#80, #81); Model Ops's Matrix, Agents and Feed tabs reconciling call by call against the ledger instead of local counters (#79); and the library guide's four privacy rules written out in one place for integrators: the host computes, the component only displays; usage UI stays hidden until the host opts in; a missing figure is always "unknown", never 0; and the call detail panel is metadata only, never prompt, completion or tool-argument text. Full reference: the usage chapter of the library guide (español).
Added
- Crew: its own audio engine, with music in the Lounge room (#150, epic #114): Crew gets a real audio engine (
src/crew/crewAudioEngine.ts), independent of the Cartoon mode sound effects (src/engine/soundEffects.ts); separate code, separate state, separate preference. Every sound is synthesized at runtime with Web Audio API oscillators, so there is no recorded audio file to license: origin and license (CC0, original code, no third-party IP) for each sound are documented insrc/crew/crewAudioAssets.ts(CREW_AUDIO_MANIFEST, the canonical source) and indocs/crew/AUDIO_LICENSES.md(human-readable table and rationale). Music is off by default and never starts on its own:CrewStage's accessibility-and-audio panel gets a Play/Pause button that only a click or keyboard activation can trigger; selecting the Lounge room and pressing Play starts its ambient loop, moving to any other room (Development, QA, the CEO office, etc.) fades it out and requires a fresh Play press to resume, so Lounge music can never play by accident elsewhere.CREW_ROOM_MUSICmaps every room explicitly (null= silence; onlyloungehas a track today), so adding a sound to another room is a one-line, reviewable change. The engine fades in/out on room changes and on manual pause, pauses on window/tab blur andvisibilitychangeand resumes only if it was already authorized to play (never a new autoplay), anddispose()s fully on unmount (every oscillator stopped, every node disconnected, theAudioContextclosed, no listener left behind): switching to Cartoon mode unmountsCrewStageand so cleans up Crew's audio without touching Cartoon's own sound preference.useCrewAudio(src/crew/useCrewAudio.ts) wires the engine to React's lifecycle and already-existing mute/volume preferences (crewPreferences.ts, wired but inert since #157); a browser blocking autoplay (AudioContext.resume()rejecting) is caught and surfaced as its ownblockedstate with a caption, never thrown. New subtitles for every audio state change (playing/paused/blocked) reuse the existing room/camera caption mechanism (subtitlespreference, #157), satisfying the same textual-equivalent requirement for this sound. Newtests/lib/crewAudioEngine.test.ts,tests/lib/useCrewAudio.test.tsx(mocked Web Audio API, autoplay-blocked handling, focus/visibility pause-resume, StrictMode-safe cleanup) andtests/lib/crewAudioAssets.test.ts(license manifest completeness, room isolation). All new English/Spanish UI text followsCrewStage's existing inline-ternary pattern. Deliberately out of this change's scope, per the issue's own split into sub-topics: the phone ring (#145) and the coffee sound (#146) are separate issues and are not wired to any room; the engine already exposes the primitive (playCue) and the per-room mapping those issues need, with no further change expected here. A per-room volume slider was not added:CrewPreferences.volume/muted(#157) stay global, and room isolation is achieved architecturally (each room's track is independent and fades out when left), matching the issue's "no sala de Lounge por accidente" acceptance criterion without inventing a second, undocumented preference axis. - Example: library-host, a worked host app reading the rollup/calls API (#260, v0.5.0 milestone): new
examples/library-host/, the runnable example#82's guide points to for "getting figures from the server".usageFromRollup.tsdeclares the server's rollup/calls response shapes and two pure mappers,rollupToUsageandcallsToDetail, that only copy or null a figure, never compute one (locked by a token-scanner AST-style check intests/lib/libraryHost.test.tsx, since the whole point of this example is showing the one right way to feed the library real spend).HostOffice.tsxfetches both endpoints on mount and every 15 seconds (abortable, never overlapping) through a smallnode:httpproxy (proxy.ts, about 70 lines) that injectsAuthorization: Bearer $AGENT_VIEWER_API_TOKENserver side; the token variable is referenced only there and in the example's ownREADME.md(tests/library-host-proxy.test.mjschecks this statically, plus that the proxy forwards only allowlisted GET paths, strips a client-sentAuthorization, never echoes the token, and refuses to start empty).main.tsx/index.html/vite.config.tsgive it its own dev server, unrelated to the root build.seed.jsonl/seed.shseed five agents covering a known cost, mixed currencies, an explicitly unknown cost and a failed call (agent-viewer sendcannot sendllm.usage, soseed.shposts the batch endpoint directly, taking the server token as an argument rather than by the same environment variable name, so it stays outside that "only two files" rule too).
Reconciled against the real shipped#66, not the issue's illustrative sketch: the shipped rollup response has no singletotalTokensfield, only a per-kind breakdown already summed server-side (tokens.input.sum, ...); computing one frominput.sum + output.sumhere would be exactly the client-side arithmetic this mapper must not do, sototalTokensreads "unknown" on the compact badge in this example until the rollup API grows a combined field, while the per-kind breakdown, cost and the call detail panel all show real figures. Likewise, an agent absent from the rollup gets nobyAgententry, which the already-shipped#76badge and accessible list render as no usage text at all (not a literal "unknown" badge); this example's tests check that real, shipped behavior rather than the issue's pre-#76wording. Seeexamples/library-host/README.mdand the module doc comment inusageFromRollup.tsfor the full reasoning on both. - Docs: reproducible, script-verified role palette measurement for the Crew visual style contract (#116 partial, epic #114): re-audited #116 and its own blocking foundations (CREW-001..008: #172/#173/#166/#168/#171 closed, #167/#170 still open) and confirmed the issue's core remains genuinely blocked, as it requires final art and real walk animation for all six roles that only exists, as a prototype, for the CEO; that is art direction, not something this change can decide. Found one real, bounded gap inside the already-merged contract itself: section 6 of
docs/crew/visual-style-contract.md/.es.mddescribed the five non-CEO roles' outerwear swatches as "measured automatically... 16-level color quantization, alpha >= 200..." but no script in the repository ever implemented that measurement; the documented values (~#005060and similar) were hand-eyeballed approximations, not reproducible ones. Newscripts/crew-palette-measure.mjs(a from-scratch PNG decoder built only onnode:zlib, no new dependency, since the project has no PNG-decoding library) measures each role'sidle-frontprototype PNG with that exact method, explicitly excluding hair/outline and light-skin pixels by hue and lightness so they cannot be mistaken for the outerwear color. The five swatches are now exact and script-verified (#085868,#1848C8,#283838,#482888,#C85838), replacing the old~-prefixed guesses in both documents, and locked by newtests/crew-palette-measure.test.mjs(npm run crew:palette:measurereproduces them against the real bank PNGs) so the table cannot silently drift from the art it describes. This does not ratify any palette: every swatch still readsunratified, and section 13's open follow-up ("art-director ratification... ") is explicitly left standing, since that is a human art-direction call this change does not make. No PNG/SVG/manifest asset was touched. See the#116row indocs/crew/IMPLEMENTATION-STATUS.mdfor the full accounting. - Crew: real desk and plant artwork from the asset bank in the CEO office (#115 partial, epic #114): closes the gap the issue itself flagged as pending after #172/#194 ("this rescue does not yet integrate art into the rooms") for a small, honest slice. The executive desk and the floor plant (
furniture.desk-executive,furniture.plant-floor, both stillstatus: prototype, neverapproved) now render as the real original SVG in the independent Crew renderer, not the usual flat-shaded 2.5D box, only for theceo-desk/ceo-plantfurniture of the CEO office; the other 51 bank assets and the other ten rooms are untouched. Newsrc/crew/crewPropImages.ts(load/dispose, mirroring the existing CEO sprite loader) andsrc/crew/useCrewPropImages.ts(per-room load/cleanup, same lifecycle asuseCrewSprite), withcrewPropsLayer.ts/renderCrewRoom.tsdrawing the resolved image as a flat billboard instead of the polygon box when one is available, falling back to the box (with its own English/Spanish status text,data-testid="crew-prop-art-status") if the image fails to load. Two known, documented limits: the bank has no per-angle verified views for any furniture piece yet, so the same single image is reused across all four cameras rather ...
v0.4.0
[0.4.0] - 2026-10-09
"Usage ledger": the server keeps one durable, append-only row per model call with its own receive time, answers grouped audit questions and paginated call listings over it, exports it as CSV or JSONL with per-currency reconciliation totals, redacts secrets at export, bounds how long it (and raw events) are kept, and no longer accepts a token in a URL or starts open by accident. Also ships the portal loading full history on open and OTLP metrics ingestion. The one breaking change is listed first; see docs/migrating-to-0.4.md for the full upgrade notes, docs/usage-ledger.md, docs/usage-export.md, docs/retention.md and docs/redaction.md for the references themselves.
Breaking changes
- Removed: a
tokenorapi_keyquery parameter never authenticates/api/v1or/v1/logs, in any mode, even when the value is correct (#71), includingPOST /api/v1/webhooks/genericwhen no webhook secret is configured. Such a request now gets401 query_token_not_supported.EventSourceclients (which cannot send anAuthorizationheader) now call the newPOST /api/v1/stream-ticketsfirst and open the stream with the single-use ticket it returns (?ticket=);connectEventStreamin@warlockcode/agent-viewerdoes this automatically whereverfetchcannot stream a response body. Migration notes, before/after for every client: docs/migrating-to-0.4.md. - The server refuses to bind a non-loopback interface with no token (#71).
npm run serverand a directstartServer()call now default to127.0.0.1(AGENT_VIEWER_HOST), not every interface. SettingAGENT_VIEWER_HOSTto anything else (for example0.0.0.0) with noAGENT_VIEWER_API_TOKENconfigured makes the process print one line to stderr and exit with code 1 (OpenApiRefusedError,code: 'open_api_refused'). Set a token, bind loopback, or setAGENT_VIEWER_ALLOW_OPEN=1to accept an open API anyway. Theagent-viewerCLI and the Docker images are unaffected: they always run with a token. - With no token configured and no
AGENT_VIEWER_ALLOW_OPEN=1,/api/v1now rejects requests that do not look local (#71). A request from a non-loopback remote address gets403 open_api_loopback_only; a non-loopbackHostheader gets403 open_api_host_not_allowed; anOriginheader whose host is neither loopback nor listed in an explicitAGENT_VIEWER_CORS_ORIGINgets403 open_api_origin_not_allowed(a bare*does not count as a list). A request with noOriginheader (curl, the SDKs, the CLI) is unaffected, and an HMAC-signed webhook still works regardless of where it comes from. A reverse proxy in front of the server makes every request look local, so a proxied deployment still needs a token.
Added
-
Crew walk frame pipeline (#117): four original CEO prototype atlases with eight illustrated frames per direction, a versioned manifest and safe prototype-only importer, lazy scene/facing loading, camera transition preload, measured foot anchors and cancellation/reduced-motion/fallback handling in the independent renderer. Playback follows reported
isWalkingwithout creating events, usage or navigation. Includes integrity/runtime tests and four-view/mobile/zoom PNG and MP4 evidence; final art and physical paths remain outside this pipeline pilot. -
Crew visual style contract (#116): a documentation-only deliverable formalizing the "Office Beans" chibi style that the approved CEO reference already draws, so a future illustrator or engineer has one contract to target instead of reverse-engineering the CEO prototype. No art was added or changed. See docs/crew/visual-style-contract.md (English) and its Spanish twin docs/crew/visual-style-contract.es.md.
- Names the asset lifecycle the issue asked for (
reference | vector-study | prototype | approved), ties each state to whatasset-manifest.jsonandscripts/crew-assets.mjsalready enforce today, and records thatapprovedand a formalvector-studymanifest entry are not reachable yet (follow-up work, out of scope here). - Documents the measured character canvas contract (256x352 px RGBA, anchor 0.5/0.9375) that the 11 existing character prototypes already share, the six roles' silhouette/palette cards (CEO's palette authored on the reference sheet, the other five roles' measured automatically from their prototype art and flagged as unratified), the clip/animation contract (
ceo.blink.front.v1as the reference implementation), the ADR-001 layer order and zoom range, and the explicit prohibition on faking a missing facing by stretching or mirroring existing art. scripts/crew-assets.mjs:validateCrewAssets()now also enforces the documented character canvas contract (CREW_CHARACTER_CANVAS256x352,CREW_CHARACTER_ANCHOR0.5/0.9375) for everykind: 'character'entry, not just the CEO's four facings. Covered by two new cases intests/crew-assets.test.mjsplus a dedicated assertion across all 11 character prototypes.
- Names the asset lifecycle the issue asked for (
-
Crew renderer split into independent, tested layers (#167):
src/crew/renderCrewRoom.tsused to mix room structure, furniture, actor sprites and the presence badge in one function. It is now the orchestrator only; the drawing itself lives in five new modules, each with its own Vitest suite and no import of the other:crewLightingLayer.ts(the shared shading palette: wall/floor tones and per-prop-type face colors),crewSceneLayer.ts(background, walls, floor grid and door thresholds),crewPropsLayer.ts(furniture boxes),crewSpriteLayer.ts(the actor's pose or blink frame) andcrewHudLayer.ts(the presence number badge, the only canvas-drawn HUD element).crewRenderPrimitives.tsholds the one shared drawing primitive (a filled, stroked polygon).renderCrewRoom.test.tsandcrewViewport.test.tspass unmodified, confirming identical pixel output and draw order after the split.- New
crewSceneLifecycle.ts: a framework-independentmountCrewScene()that schedules a render frame withrequestAnimationFrame, observes canvas size withResizeObserver, and exposesupdate()/unmount(), so a Crew scene's full lifecycle (mount, update, unmount) is unit-testable without React or a real browser (tests/lib/crewSceneLifecycle.test.ts, including an end-to-end test that mounts, switches room and unmounts a realrenderCrewRoomscene). It is not yet wired intoCrewStage.tsx; that integration is left for a follow-up change, out of this issue's scope, after an attempt to wire it surfaced a regression increw-sprites.spec.tsthat this PR reverts before merging.
- New
-
Two-mode architecture contract: Cartoon (legacy) and Crew (#166): a new ADR-002 formalizes the contract that
visualMode: 'cartoon' | 'crew'already implemented across several merged PRs:cartoonstays the default everywhere, switching the mode never recreates the domainOfficeStore, never drops an event and never resets the host's usage/task state, and each renderer's own Crew presentation state (room, per-room camera,reducedMotion) survives the switch whether or not the host controls it. Two new automated checks enforce the contract instead of only documenting it:tests/crew-mode-boundary-166.test.mjsscans the real source tree so Cartoon can never import fromsrc/crew/and Crew can never import Cartoon's renderer, legacy grid or motion modules (and confirmsrenderFurnitureItemstays private);tests/lib/crewModeContract166.test.tsxswitchesvisualModetwenty times (the issue's own acceptance threshold) against a growing event stream and asserts the office timer is never duplicated, the latest event is never lost, and Crew's room/camera/reducedMotionstate (controlled or not, across two independentAgentOfficeinstances) never leaks or resets. See ISSUE-166-ACCEPTANCE.md for the full acceptance matrix. -
Crew spatial model: collisions, functional doors and walkable zones (#170, CREW-005, building on the eleven-room catalog from #168): pure logic over the existing
CrewRoomDefinitiondata, no new art, no new props.src/crew/crewCollision.ts: axis-aligned bounding box (AABB) collision over room data that already existed.propAABB/roomBoundsAABB/agentAABBturn a furniture anchor, a room's floor and an agent's presence radius into boxes;aabbsOverlap,aabbContainsandpointInAABBare the primitive tests;crewAgentCollidesrejects a spot that leaves the room or overlaps any furniture footprint (with anexcludeFurnitureIdescape hatch so an agent can re-check its own current anchor);overlappingFurniturePairsis a data-integrity check confirmed empty for all eleven rooms;doorAABBgives the threshold box of a door on the south wall.src/crew/crewModel.ts: each room'sdoorsnow also carriesconnectsTo(the neighbouring room id, ornull) andedgeId, built from a new ten-edge spanning tree (CREW_ROOM_EDGES) that connects all eleven rooms into one building, rooted at reception. Every room's original door keeps its exact previous id and position; rooms with more than one neighbour (reception, development, lounge, coffee, meeting, research, qa) get additional doors at new, furniture-clear offsets. No room's furniture, dimensions or rendering changed.src/crew/crewRoutes.ts: the reusable connectivity layer.buildCrewRoomGraphturns the door data into a room adjacency graph;defaultCrewDoorStates/setCrewDoorState/isCrewEdgeOpenare a pure, immutable open/locked state per edge (every door starts open);findCrewRoomRouteandreachableCrewRoomsdo a breadth-first search across rooms that skips any locked door, so a locked door can fully separate two rooms, exactly as...
v0.3.0
[0.3.0] - 2026-10-09
"Cifras ciertas" (true figures): the server, the portal, the CLI and the SDKs stop treating an unknown token count, cost or currency as zero, split cache reads from cache writes, add llm.failed, and Claude Code finally reports tokens through install claude-code --telemetry. First release published to npm, GHCR and (pending the owner's PyPI trusted-publisher setup) PyPI. The full semantics, field by field, are written down once in docs/usage-semantics.md (español); every section below links back to it instead of restating the rules. While the package is at 0.x, its API may still change between minor versions.
Breaking changes
-
Snapshot usage block.
GET /api/v1/snapshot(and the newGET /api/v1/usage) no longer carries only a baretotalTokens/totalCost. It now also returnsusage: UsageSummary(server/usageAggregates.ts): per(currency, costSource)cost buckets, asum/unreportedCountpair per token kind, and the same breakdown by agent and by model.totalTokensandtotalCostare kept for one minor as deprecated projections (removed in 1.0):totalCostis a number only when every successful call reported a cost in exactly one(currency, costSource)pair, and isnullotherwise, instead of a sum across currencies.Before (0.2.x):
{ "totalTokens": { "input": 1200, "output": 340, "cached": 0, "reasoning": 0 }, "totalCost": 0.42 }After (0.3.0), alongside the same (now honest) deprecated fields:
{ "totalTokens": { "input": 1200, "output": 340, "cached": 0, "reasoning": 0 }, "totalCost": null, "usage": { "total": { "calls": 2, "tokens": { "input": { "sum": 1200, "unreportedCount": 0 }, "output": { "sum": 340, "unreportedCount": 0 } }, "byCurrency": [{ "currency": "USD", "costSource": "provider-reported", "amount": 0.3, "calls": 1 }], "costUnknownCount": 1 } } }Migration: read
usage.total(orGET /api/v1/usage) instead oftotalTokens/totalCost; see docs/usage-semantics.md section 2 for every field. -
PATCH /api/v1/agents/:agentIdallow-list. Only descriptive profile and status fields are accepted (name,roleTitle,provider,model,status,statusText,workspace).tokensInput,tokensOutput,cachedTokens,reasoningTokens,costand any other unlisted key are rejected with400 validation_failed(issues[].pathnames the field), instead of silently changing spend. Migration: report usage with anllm.usageevent throughPOST /api/v1/events, or the SDKusage()/agent.usage(...)helper. -
Contract nullish fields.
llm.usage(andllm.failed) no longer defaultcachedTokensorreasoningTokensto0: an omitted ornullvalue now staysnull/absent instead of becoming a zero that looked like "the provider reported zero". NewcacheReadTokensandcacheWriteTokensreplace the deprecatedcachedTokens(kept as an alias; a differing value in both is rejected). Newllm.failedevent type (canonical event count: 22 to 23).Before:
{ "inputTokens": 500, "outputTokens": 120 }meant "cached and reasoning tokens are zero".
After: the same payload means "cached and reasoning tokens were not reported"; a provider that truly billed zero cached tokens must send"cacheReadTokens": 0explicitly.Migration: readers of stored or exported events must handle
null/absent as "unknown", not "zero". -
SDK
costSourcedefault. Passing acostwith nocostSourceno longer sends"provider-reported"; it now sends"unknown"and prints one warning per client instance.currencyand a task id are newly accepted. Migration: passcost_source="provider-reported"(Python) orcostSource: 'provider-reported'(TypeScript) when the cost really came from the provider, and passcurrency. -
Totals no longer include
PATCHamounts or unknown-as-zero. A figure the 0.2.x snapshot showed (built fromPATCH-edited numbers and zero-filled unknowns) can differ from the 0.3.0 figure for the exact same stored data; the 0.3.0 number is the correct one. See docs/usage-semantics.md for how to reconcile it against a provider invoice. -
Webhook usage validation is now stricter.
usageblocks without required fields (provider,model,inputTokens,outputTokens) are rejected with400 validation_failed. Empty usage objects ({}) are now rejected, not accepted as zero-filled events. Usage with an unknowncostSourceis accepted as anunknowncost source, not silently mapped to an assumed value. -
Ingestion behavior change: a client that reuses an event id for different content used to get
200 duplicateand now gets409 conflicting_duplicate. The new event is still not stored, so totals do not change; the difference is that the loss is now visible. Batches keep answering202and report the item asstatus: "conflict". -
Raw HTTP clients that retry by rebuilding the body with a fresh
timestampnow get409on the retry, becausetimestampis part of the event. A retry must resend the identical event. The SDKs are not affected: they build each event once and resend the same object. -
Retries across the upgrade: an event stored by 0.2.x with a default the 0.2.x validator filled in (for example
cachedTokens: 0) has a different fingerprint than the same event validated by 0.3.0, so a retry of it after upgrading gets409. This fails closed: the stored row stays and nothing is counted twice. -
The Claude Code hook gives a tool event a stable id but the
timestampof each hook run, so a hook that fires twice (installed in both user and project settings) now reports the second copy asconflictinstead ofduplicate. The hook still exits 0 and the office keeps the first copy; the count shows in/ready. -
A 0.3.0 SDK needs a 0.3.0 server: a 0.2.x server rejects
llm.failedand dropscacheReadTokensandcacheWriteTokens.
Added
- SSE reconnect now replays every missed event or sends an explicit resync, never a silent partial replay (issue #54). New
AGENT_VIEWER_SSE_REPLAY_MAX(default10000,0means any reconnect that missed something always resyncs) bounds how many missed events a reconnect replays in full; past it, or when the cursor is unknown (never stored, evicted in memory mode, or lost after a memory-mode restart), the server sends a namedresyncframe (reason:cursor_unknown,gap_too_largeorbuffer_overflow,missedas a count ornullwhen truly unknown, never0) instead of event frames, and keeps the connection open and streaming live events afterward so older clients keep working. A full replay ends with a namedreplayedframe (replayed,lastEventId).GET /healthgainssse: { replayMax, replaysSinceStart, eventsReplayedSinceStart, resyncsSinceStart }.connectEventStreamgainslastEventId(resume from a cursor, typicallysnapshot.lastEventId),onResyncandonReplayedoptions, a newresyncingRealtimeStatus, andresyncCount()on the returned connection. The demo app rebuilds its live office fromGET /api/v1/snapshoton a resync through the newrebuildFromSnapshothelper (src/integrations/snapshotRebuild.ts), shows the server's own usage totals (never re-added from the snapshot's 100 events) and a dismissible notice in English and Spanish. See docs/integration.md and docs/library.md. - Golden reconciliation suite (
npm run test:golden, issue #62): one hand-checked fixture (tests/fixtures/reconciliation/, mixed currencies, a missing cost, a missing currency, missing cache tokens, a duplicate id, a conflicting duplicate, a repeatedrequestId, a failed call) run through the memory store, the SQLite store across a restart, the portal reducer and the embeddable library'ssummarizeUsage, asserting all four reach the same figures. Runs as its own CI step, right after typecheck, and fails on the field and the path that diverged. Ships with an independent reference reducer that cross-checks the hand-worked expected figures. Deferred to a follow-up: the HTTP routes (including signed webhook retries and thePATCHrejection path), memory eviction, and alibrary-privacyVitest suite; none are blocked on a missing dependency, see the fixture'sREADME.md. agent-viewer install claude-code --telemetrypoints a project's Claude Code at the office's OpenTelemetry logs, so token and cost per model call show up (opt-in, off by default; needs a new Claude Code session). Helper mode (default) writes anotelHeadersHelpercommand that looks the running office's token up on every export; static mode (--telemetry --token <token>) writes a fixedOTEL_EXPORTER_OTLP_LOGS_HEADERSinstead, for an office on another machine or in Docker, with the token always masked as<token hidden>in the diffs the installer prints. The fiveOTEL_LOG_*content flags are always pinned to"0", and no metrics or traces exporter is ever configured.installrefuses outright on a conflicting existing setting (naming the file and the key) and warns, without refusing, on a setting it only overrides by Claude Code's own precedence.--no-telemetryremoves only the telemetry block, keeping the hooks;uninstall claude-coderemoves both, byte for byte, the same way the hooks round-trip today. Newagent-viewer otel-headers --url <office-url>helper command (used byotelHeadersHelper, not meant to be run by people): never touches the network, prints the office's bearer token as JSON only when a running office has that exact origin, always exits 0. Guide: docs/claude-code.md. Refs #60; the server now has thePOST /v1/logsreceiver this telemetry needs (#59), so this ingests f...