Repository navigation
feat: memory map, usage metering and value metering — GET /memories/field, /spend and /value - #2071
Merged
Merged
Conversation
erni-a
force-pushed
the
CAURA-000-value-metering
branch
from
October 13, 2026 10:11
15d7b67 to
3ad1578
Compare
This was referenced Oct 13, 2026
Contributor
|
Claude Code Review — skipped: PR author 'erni-a' is not a public member of the 'caura-ai' org |
1 similar comment
Contributor
|
Claude Code Review — skipped: PR author 'erni-a' is not a public member of the 'caura-ai' org |
…ories Adds a read that lays out the memories a caller can list on a 2D map, computed from their embeddings with numpy only: PCA to <=50 dims, k-means (k = clamp(round(sqrt(n/2)), 2, 24), fixed seed), centroids placed by a PCA of the centroids plus repulsion, members placed by each cluster's local 2D PCA at a radius proportional to sqrt(size), normalised to [0, 1] with 6% padding. Deterministic for the same inputs. Memories with no embedding yet appear with cluster -1 near the centre. Clusters carry a short label from their most distinctive linked entities (tf-idf style), falling back to memory type plus a frequent title word. Same tenant check, fleet-read gate and visibility scoping as GET /memories; superseded and archived rows are included, deleted and held rows are not. Results are cached in-process for 10 minutes per caller scope and storage signature (count, embedded count, newest write/status change). Persisting the fitted model is a later step. core-storage-api gains POST /memories/field-signature and POST /memories/field-page (keyset pages of up to 2000 compact rows with base64 float32 embeddings, linked entity ids, 240-char preview and content length). _numpy_kmeans moves to services/kmeans.py, shared with the insights discover mode. Signed-off-by: erni <erni@caura.ai>
One far-off tiny cluster squeezed every other cluster into a corner, because the final normalisation used min/max and the centroid PCA let one outlying centroid set the scale. Now: - each axis is normalised by the 3rd-97th percentile of the point coordinates to [0.06, 0.94], with outliers clamped into [0.02, 0.98]; - centroid anchors are centred on their size-weighted mean and compressed per axis (signed square root) before repulsion; - repulsion never moves an anchor more than 0.5 from its PCA position. Also registers the field reads' fleet predicate in the C27 strict fleet scoping inventory (strict, org-blind, as memory_list_by_filters). Signed-off-by: erni <erni@caura.ai>
erni-a
force-pushed
the
CAURA-000-value-metering
branch
from
October 13, 2026 10:17
799b13c to
c0122b8
Compare
Contributor
|
Claude Code Review — skipped: PR author 'erni-a' is not a public member of the 'caura-ai' org |
…peration Every LLM and embedding call the server makes is now counted with the provider's own token figures (input, output, reasoning) and attributed to the tenant, agent, fleet and operation it was made for. - common/usage: a usage scope (ContextVar, plus a @Scoped decorator for entry points), record_model_call / record_value, and a per-minute in-memory aggregator flushed every 15 s. - Providers report usage: OpenAI-compatible chat and embeddings (the counts were already read, then only logged), Vertex and Gemini (usage_metadata was discarded). The retry layer counts failed attempts separately, so a billed-then-rejected response is never two calls. - Attribution at the entry points: writes, search, recall, enrichment, entity extraction, contradiction, crystallization, forge, ingest, evolve, interview, digests, queue consumers and lifecycle actions; tracked background tasks inherit their tenant. Request paths fall back to the signed-in tenant and the MCP agent. A call with no tenant is recorded as _unattributed rather than dropped. - Storage (migration 066): llm_usage_minutes and value_usage_minutes as additive per-minute upserts, and usage_flush_batches so a re-sent flush is counted once. POST /usage/flush (cross-tenant by design, like the capability and tenant usage flushes) and POST /usage/query, bound to one tenant. Both tables join the tenant and fleet purges. - observability.usage_metering_enabled (default on) turns it off per workspace; storage drops that tenant's rows at flush time, for core-api and core-worker alike. Counts only, never content, kept in the deployment's own database. Signed-off-by: erni <erni@caura.ai>
Only measures with both sides known exactly: - Recall (RecordRecallValue, the search pipeline's last step, and the legacy path so the hotfix lever keeps it): recalls, results, tokens returned, results written by another agent, and for results that are a piece of a larger memory (auto-chunks and atomic facts, which name their parent through the reserved parent_memory_id) the tokens returned and the full size of the distinct parents. A whole memory returned as itself claims no saving; a deleted parent claims nothing; diagnostic searches are not recalls. Runs after the response, one storage read for the parent sizes. - Duplicate merges: crystallization records the memories and tokens it retired and the tokens it wrote back, once the sources are archived; the write-path near-duplicate merge records the row it retired (the agent's own new write is not offset against it). Not recorded: savings against sending every memory, or anything not observed. Imported documents are left out: the server keeps no verifiable size for the source, and a client-supplied one would be forgeable. Tokens are tiktoken cl100k_base, which core-api already ships. Signed-off-by: erni <erni@caura.ai>
GET /api/v1/spend returns one workspace's model calls (provider-reported tokens in, out and reasoning, failed attempts) and recall savings over a window, grouped by day, agent, fleet, operation, kind, provider, model or metric, with a summary: total tokens, context saved (parent tokens less returned), cross-agent results and duplicates retired. Spend per agent is an admin-plane report, gated like tenant settings: tenant and admin credentials read it; agent keys and org members get 403; a tenant outside the caller's readable set is refused. docs/usage-metering.md says what is recorded and what is deliberately not, how it is stored and counted, and how a workspace turns it off. Signed-off-by: erni <erni@caura.ai>
…1/value Value metering: what the memory does for a customer's agents, counted per session (docs/plans/value-metering.md). - Sessions. MCP stays stateless but now issues a session label (Mcp-Session-Id) on the first response and reads it back; REST and SDKs send X-Caura-Session-Id; the broker's metadata.session_id wins on its writes. A per-process tracker counts calls per session and flushes per tenant. - Origin. After a write, the memory's session work is recorded once: minutes since the session started or last wrote, calls to Caura in that stretch (a lower bound on turns), and tokens/turns when a hook, plugin or SDK reported them in the new `origin` field. The MCP write tool does not take it: a model cannot observe its own token use. - Reuse. After a recall, each memory from another session is recorded the first time this session recalls it; the primary key makes "once per session" structural, and a session never reuses its own memory. - Storage (migration 067): agent_sessions, memory_origins, memory_reuse, every route bound to one tenant; origins and reuse cascade with their memory and are listed in the tenant purge. - GET /api/v1/value: sessions helped, work reused (minutes, calls, reported tokens with their coverage), knowledge shared with the agents others learn from most and who learns most, mistakes prevented, and Caura's own cost as one line. Tenant and admin credentials only. Signed-off-by: erni <erni@caura.ai>
…d contradictions Sessions helped now needs a recalled memory with a recorded origin; memories older than value metering still count as reuse, reported separately. Mistakes prevented counts contradictions a person resolved (dismissed ones were false alarms), and reports the ones still waiting for review instead of counting them. Signed-off-by: erni <erni@caura.ai>
… and the SDKs - OpenClaw plugin: each afterTurn adds the provider-reported usage of the assistant messages not counted before (by timestamp, so a history handed over again is not counted twice), per OpenClaw session. The plugin's next write in that session sends it as `origin` (source plugin) with the turn count, then starts again. Its writes and recall searches carry the session as X-Caura-Session-Id. Turns without reported usage send turns only, never a guess. - Python and TypeScript clients: `session_id` / `sessionId` and `set_session()` / `setSession()` put calls in a session; `write()` takes `origin` (tokens in, out, turns from the caller's own provider usage), sent as SDK-reported. Signed-off-by: erni <erni@caura.ai>
…'s path The broker writes through /memories/bulk with its session on each item's metadata, and bulk writes recorded no origin. Each item this attempt created now gets one, a microsecond after the previous item so a batch is one stretch of work carried by its first item rather than repeated for every item. Signed-off-by: erni <erni@caura.ai>
An Evolve report names the memories the outcome relied on. When the reporting session had reused one of them from another session, that reuse is marked with the outcome (latest report wins); naming a memory the session never reused marks nothing. GET /api/v1/value adds work_reused.confirmed: the reuses, minutes and reported tokens behind reported successes, and the reuses that preceded a failure. Signed-off-by: erni <erni@caura.ai>
Signed-off-by: erni <erni@caura.ai>
erni-a
force-pushed
the
CAURA-000-value-metering
branch
from
October 13, 2026 10:27
c0122b8 to
67cf25e
Compare
Contributor
|
Claude Code Review — skipped: PR author 'erni-a' is not a public member of the 'caura-ai' org |
Contributor
Author
|
@ran-taig Please approve |
Contributor
Author
|
@arkash20 please approve |
arkash20
approved these changes
Oct 13, 2026
Merged
erni-a
pushed a commit
that referenced
this pull request
Oct 13, 2026
🤖 I have created a release *beep* *boop* --- <details><summary>backend: 3.33.0</summary> ## [3.33.0](backend-v3.32.0...backend-v3.33.0) (2026-10-13) ### Features * memory map, usage metering and value metering — GET /memories/field, /spend and /value ([#2071](#2071)) ([da9fc57](da9fc57)) ### Documentation * **skill:** three lessons from real agents — a miss isn't proof, don't save found-nothing, leave memory_type to the server ([#2068](#2068)) ([f6e7578](f6e7578)) </details> <details><summary>plugin: 2.25.0</summary> ## [2.25.0](plugin-v2.24.7...plugin-v2.25.0) (2026-10-13) ### Features * memory map, usage metering and value metering — GET /memories/field, /spend and /value ([#2071](#2071)) ([da9fc57](da9fc57)) ### Documentation * **skill:** three lessons from real agents — a miss isn't proof, don't save found-nothing, leave memory_type to the server ([#2068](#2068)) ([f6e7578](f6e7578)) </details> --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). Signed-off-by: release-please[bot] <release-please[bot]@users.noreply.github.com> Co-authored-by: caura-deploy-bot[bot] <265395343+caura-deploy-bot[bot]@users.noreply.github.com>
1 task
eldad-caura-ai
added a commit
that referenced
this pull request
Oct 15, 2026
## Summary Bumps both SDK clients to **1.1.0** so the next publish carries what has merged since the last releases (O14). Neither client has been published since `caura-client` 1.0.3 on PyPI (tag `caura-client-v1.0.3`) and `@caura/client` 1.0.2 on npm (tag `caura-client-ts-v1.0.2`). Versions only: `clients/python/pyproject.toml` and `caura_client/_version.py`; `clients/typescript/package.json`, `package-lock.json` (its own two entries) and `src/version.ts`. ## What 1.1.0 carries Both clients: - #1807: refuse to send the API key over plain HTTP to another host - #2010: parse HTTP-date `Retry-After` values - #2012: `search()` keeps the response envelope (`SearchResult`, still a list or array), `recall()` sends `items_alias: false`, and `caller_agent_id` / `callerAgentId` lets a tenant-key client read as an agent (opt-in) - #2035: `stats()` - #2071: the memory map, spend and value methods Python: #2013 (`CauraAPIError` for a non-JSON 2xx body), plus #1775 and #1909's client parts. TypeScript: #2014 (`CauraApiError` for a non-JSON 2xx body), #1667 (typing and response timeouts). **Why minor:** new methods and fields. `search()` still returns a list subclass (an array in TypeScript), so existing callers keep working. One visible change: `recall()`'s `raw` no longer contains `items` unless `items_alias` is passed. **Server versions:** `stats()` needs a backend with #2035 (3.32.0 or later) and the memory map, spend and value methods need #2071 (3.33.0 or later). Against an older server those calls get a 404; everything else works as before. ## Release, after merge Each tag runs its publish workflow: - `caura-client-v1.1.0` → `publish-python-client.yml` (PyPI `caura-client`) - `caura-client-ts-v1.1.0` → `publish-npm-client.yml` (npm `@caura/client`) The `caura`, `caura-sdk` (PyPI) and unscoped `caura-client` (npm) packages depend on `caura-client>=1.0.0` / `@caura/client ^1.0.1`, so they pick up 1.1.0 without a release of their own. ## Test plan - [ ] CI: the Python client's version test (`__version__` agrees with `pyproject.toml`) and the TypeScript client's build and tests pass 🤖 Generated with [Claude Code](https://claude.com/claude-code) Signed-off-by: eldad-caura <eldad@caura.ai> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
eldad-caura-ai
added a commit
that referenced
this pull request
Oct 20, 2026
## Why From a recheck of what `main` gained since the audit's last reviewed head (`0ea70e7f`, 2026-10-08). #2071 added `GET /api/v1/memories/field`, and enterprise Home now calls it on every load. - **M-125 (Medium, availability):** the route has no rate limit (limits are attached only to specific write and search routes), takes no per-tenant slot, and doesn't share builds. - **Cost per build:** an uncached request pages up to 50,000 memories with their 1024-d embeddings from the read replica, about 11 MB of JSON per 2,000 rows, decoded on the event loop. PCA and k-means then run on the shared thread pool. - **An easy-to-bypass cache:** its key includes `limit` (1–50,000) and `fleet_id`, and it holds 32 entries. - **Impact:** any credential of any tenant can start any number of builds at once, slowing or exhausting a shared core-api instance for every tenant. For an active tenant, every write also changes the signature, so normal Home traffic rebuilds the map too. #2071's own description says the route "needs a bound on concurrent builds" before broad rollout. - **L-236 (Low):** the cache's change detector (`count`, `embedded`, newest `created_at`/`status_changed_at`) doesn't move on an edit. An edited or redacted memory keeps its old 240-character preview, title and visibility on the map for up to the 10-minute TTL. ## What - **Shared builds:** requests for the same map (same cache key) wait on one build. - **Per-process caps:** at most `memory_field_max_concurrent_builds` builds (default 2), and one per tenant. Anything beyond gets **429** with `Retry-After`. Home already treats a failed map request quietly, and the Map page retries. - **Shared signature reads** (from the Claude review): concurrent requests for one visibility scope share a single signature scan, and nothing is kept once it finishes. The signature doesn't depend on `limit`, so varying `limit` no longer multiplies scans either. - **Edit detection:** the storage signature adds a hash over each visible row's `content_hash`, title, visibility, type, weight, fleet, agent, status and `supersedes_id`, so an edit invalidates the cache. ## Verification Staged in CI. The tests-only commit ([run 39328014463](https://github.com/caura-ai/caura/actions/runs/39328014463)) failed exactly its four new cases, each for its reason, with the other 10,269 tests passing: - **`test_an_edit_misses_the_cache`:** `computed_at` was unchanged after the edit, so the stale cache was served. - **`test_requests_for_one_map_share_its_build`:** the two requests got different `computed_at` values, so two builds ran. - **`test_a_tenant_builds_one_map_at_a_time`:** `TimeoutError`; the second map started a build instead of answering 429. - **`test_builds_are_capped_per_process`:** `TimeoutError`; the third tenant built instead of answering 429. The review fix was staged the same way. `test_concurrent_requests_share_one_signature_scan` was pushed without the sharing code ([run 39330490412](https://github.com/caura-ai/caura/actions/runs/39330490412)) and failed with three scans for three requests (`assert 3 == 1`); the other 10,273 tests passed, including the four above. The test harness wraps the storage client: each build's first page fetch counts the build and then holds on an `asyncio.Event`, so the tests need no threads and no polling. Earlier pushes of the tests-only stage stopped at ruff (ASYNC109, ASYNC110, then blank-line formatting) before any test ran. The final commit is `12f71f9a`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Signed-off-by: eldad-caura <eldad@caura.ai> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The memory map and metering for the enterprise Prism › Map, Home and Insights › Value, in one PR to save serial CI cycles (it replaces #2069 and #2070). Eleven commits in three parts, each readable on its own:
1.
GET /api/v1/memories/field— the memory map (2 commits)field-page/field-signaturereads; the layout is cached in-process for 10 minutes, keyed by that signature.docs/api-reference.mddocuments the route.2. Usage metering — migration 066,
GET /api/v1/spend(3 commits)common/usage: a scope (@scoped) and an aggregator; provider hooks read usage from OpenAI, Vertex/Gemini (usage_metadata) and the rest.llm_usage_minutes,value_usage_minutes,usage_flush_batches.POST /usage/flush?tenant_id=andPOST /usage/query?tenant_id=, both bound to one tenant. A writer sends one flush per tenant (common.usage.per_tenant); each tenant's batch id is auuid5of the batch's, so a re-sent batch, even one that half landed, is counted once.GET /api/v1/spend(tenant or admin credentials only). Namedspendbecause the enterprise gateway already owns/api/v1/usagefor plan counters.observability.usage_metering_enabledgates it;docs/usage-metering.mddocuments what is and isn't metered.3. Value metering — migration 067,
GET /api/v1/value(6 commits)Mcp-Session-Idlabel, REST takesX-Caura-Session-Id, and a broker'smetadata.session_idwins.agent_sessions,memory_origins,memory_reuse(tenant-bound routes, no allowlist growth).MemoryCreate.origin).memory_reuse.outcome).GET /api/v1/value, read by the enterprise Insights › Value page and Home.X-Caura-Session-Idand per-turn origin; the Python and TypeScript SDKs takesession_id/origin.Type of Change
How Has This Been Tested?
All of CI's static checks pass locally on this branch, the tenant-scope gate included (lock files, broker OpenAPI, every ruff and format scope, the MCP guards, mypy ×6, the legacy-name ratchet, do-not-touch, the evidence registry). Each part's tests passed on its own branch: root suite 10,087 / 10,094 / 10,110, core-api + core-storage 1,035 / 1,044, migrations 065 → 066 → 067 on a fresh database, plugin 574/576 (the two resolveAgentId fallback warn tests fail the same way on main on the test machine), Python client 229, TypeScript client 68. The three parts combined without a conflict.
Additional Notes
No tenant-scope allowlist growth. Every new storage path is bound to one tenant: the flush takes a required
tenant_idand writes every row under it, so a flush can't reach another tenant's totals (test_a_flush_writes_only_the_tenant_it_names).features.prism_field, off by default./spendbecause the enterprise gateway already owns/api/v1/usage. caura-ops' Model Spend page reads these tables once 066 is in production.plugin/src/origin-usage.tswasn't in the plugin install lists (_plugin_files, the install script'sSRC_FILES,FALLBACK_SRC_FILES), so fresh installs would fail with TS2307;test_plugin_source_manifest.pycovers it.originonBulkMemoryItem, part of the frozen broker contract.