Repository navigation
feat: memory.quota — your memories against your plan cap on REST, MCP and the CLI - #713
Conversation
The three reads were Next.js server actions, which the app router runs one at a time per client and which cannot take an AbortSignal. On /lore they queued (plan usage -> pending invites -> memory list) on every load and every window focus, so the TopBar activity bar stayed on long after the visible panels had loaded. - useMemories calls POST /memories/list from the browser with React Query's signal (body building moved unchanged into the pure lib/memory-list.ts) - useMemoryTotal and usePendingInvitesForMe read new GET route handlers (/api/plan/usage, /api/invites/pending) that wrap the existing server functions Co-authored-by: dash0-dev[bot] <257284812+dash0-dev[bot]@users.noreply.github.com>
…r-action-reads rule - parallel-reads.spec.ts: useMemories, useMemoryTotal and usePendingInvitesForMe issue ordinary requests with React Query's signal and never call a server action (the action modules are mocked to throw) - read-routes.spec.ts: the two GET route handlers keep the wrapped functions' contract (null / [] when signed out) and are never cached - memory-list.spec.ts pins the POST /memories/list body - packages/web/CLAUDE.md, docs/decisions.md, CLAUDE.md: a client read is never a server action; docs/key-files.md and docs/storybook.md updated - LorePage story now renders the populated list (baseline needs refresh) Co-authored-by: dash0-dev[bot] <257284812+dash0-dev[bot]@users.noreply.github.com>
… and the CLI
GET /memories/quota, the memory.quota MCP tool and `lorekit quota` all answer
{ plan, personal: { count, limit, remaining }, org_count, total } from the
actor-guarded lorekit_memory_count RPC. personal is the pair the cap trigger
enforces; org-owned memories count against each org's own cap, so they sit
beside it instead of being subtracted (the RPC's combined count measured
against the personal limit made 'remaining' wrong for both).
The dashboard reads it over REST too: the TopBar badge (total) from the
browser with React Query's signal, Settings > Plan (personal pair) on the
server. The interim /api/plan/usage route and the direct-RPC getPlanUsage
server action are removed.
Co-authored-by: dash0-dev[bot] <257284812+dash0-dev[bot]@users.noreply.github.com>
…uards; cover lorekit quota Co-authored-by: dash0-dev[bot] <257284812+dash0-dev[bot]@users.noreply.github.com>
✅ No issues foundAdds memory.quota on REST, MCP and the CLI with org counts narrowed to a token's tenancy; no code change since f76e8f2, and the edited description's Tenancy section now matches quotaForKey. Progress: open review threads 2 → 1 → 1 → 0 → 0 across the last 5 reviews Review details
Nothing to report — standards (skipped), measurability (skipped), integrations (skipped), severity, 0 files skipped.
|
… plan page Addresses the pr-reviewer comment: #713 (comment) Refs: #713 Co-authored-by: dash0-dev[bot] <257284812+dash0-dev[bot]@users.noreply.github.com>
Addresses the pr-reviewer comment: #713 (comment) Refs: #713 Co-authored-by: dash0-dev[bot] <257284812+dash0-dev[bot]@users.noreply.github.com>
Addresses the pr-reviewer comment: #713 (comment) Refs: #713 Co-authored-by: dash0-dev[bot] <257284812+dash0-dev[bot]@users.noreply.github.com>
…is backlog Addresses the pr-reviewer comment: #712 (comment) Refs: #713 Co-authored-by: dash0-dev[bot] <257284812+dash0-dev[bot]@users.noreply.github.com>
Co-authored-by: dash0-dev[bot] <257284812+dash0-dev[bot]@users.noreply.github.com>
|
The dashboard preview for this PR — redeployed on each push that changes the web app.
Preview always points at this PR's latest commit; Deployment is the immutable build of |
lorekit_memory_count counts every org the token owner belongs to, so a personal-only or selected-orgs key could read how many memories its owner's other orgs hold. GET /memories/quota and memory.quota now pass the calling key to quotaForKey, which recounts org_count over effectiveOrgIds (0 under personal, only org_ids under selected) and recomputes total. The personal pair is unchanged: the cap is account-wide. Refs: #713 (comment) Co-authored-by: dash0-dev[bot] <257284812+dash0-dev[bot]@users.noreply.github.com>
docs/api-tokens.md gains a quota row in "What a scoped token sees". The llms.txt template no longer claims every operation without a scope is refused for a scoped token: only the account-wide sweeps are, and quota is answered with its org half narrowed. The catalog `returns`, the response schema, and the MCP, CLI and limits docs say the same; llms.txt and the edge schema mirror are regenerated. Refs: #713 (comment) Co-authored-by: dash0-dev[bot] <257284812+dash0-dev[bot]@users.noreply.github.com>
…rowing Addresses the pr-reviewer comment: #713 (comment) Refs: #713 Co-authored-by: dash0-dev[bot] <257284812+dash0-dev[bot]@users.noreply.github.com>
Co-authored-by: dash0-dev[bot] <257284812+dash0-dev[bot]@users.noreply.github.com>
Summary
Follows #712 (merged as
ec35d81; this branch now targetsmain, synced with a merge commit). Plan usage moves off the dashboard's private server action into LoreKit's public surface, as one operation on three faces:GET /memories/quotaon REST, thememory.quotaMCP tool, andlorekit quotaon the CLI. All three answer from the existing actor-guardedlorekit_memory_countRPC (00047). No migration.The contract
{ "plan": "free", "personal": { "count": 3318, "limit": 5000, "remaining": 1682 }, "org_count": 982, "total": 4300 }personalis the pair the cap trigger enforces. The RPC'scountadds personal and org-owned memories, but itslimitis the personal cap, while the trigger (00018) counts personal rows against the personal limit and org rows against each org's own limit. Aremainingcomputed from the combined count would therefore be wrong for both. Soorg_countsits beside the personal pair and is never subtracted. The shaping lives once, in the purememoryQuotaFromRow(packages/mcp-core/src/limits/memory-quota.ts, mirrored to_shared/limits/and drift-checked).Changes
GET /memories/quota(memories/handlers/quota.ts), registered before/:id. It's an OpenAPI path with aMemoryQuotaResponseschema, mapped tomemory.quotain bothrest-tool-name.tscopies. Service-role callers get 403, because they have no user and no cap.memory.quotais in the catalog (read,READ_ONLY,token-or-jwt) and handled bytoolQuota. It's inREAD_TOOLSin both permission mirrors and has a check insmoke-mcp-tools.mjs. It islocalMcpExempt, because the offline store has no plan and no cap. It returns counts only, never a scope or key name, so a scope-restricted key may call it (the same reasoning as/usage); a key narrowed topersonalor selected orgs gets its org half narrowed (see Tenancy below).lorekit quotaprints the personal pair, the org count and the total, with a warning at the cap and under 10 % headroom. It is remote-only and exits 1 when the remote can't answer, so a script can gate a bulk write on it. It has help, completions, a smoke case andtest/quota.test.mjs.totalfrom the browser with React Query's signal./api/plan/usageroute from perf(web): run the /lore list, plan-usage and invites reads in parallel #712 and the direct-RPCgetPlanUsageserver action.docs/api-tokens.md(quota in "What a scoped token sees"),docs/limits.md(reading your headroom),docs/cli.md,docs/key-files.md, anddocs/decisions.md, which explains why this isn't a breach of "Dashboard analytics reads stay REST-only": it's the caller's own headroom against the cap that refusesmemory.write, which an agent can act on.remote.mdx(check headroom before a migration).llms.txtregenerated; its template now names only the account-wide sweeps as refused for a scoped token, and says how quota answers one.Review follow-ups in this PR
MemoryExpandButtonno longer claims the badge shows the same figure as Settings → Plan: the badge istotal, the page the personal pair.quotais listed indocs/mcp-tools.md(annotations table + its own section),packages/cli/README.md, thememoriesroute table and the rootCLAUDE.mdcommand list.lorekit.plan.quota_failed) instead of dropping it silently; it still renders the skeleton.app-route.tsno longer says plan usage is migration backlog (carried over from perf(web): run the /lore list, plan-usage and invites reads in parallel #712's review)./memories/quotadescription states the token narrowing.Tenancy
lorekit_memory_countcounts every org the token's owner belongs to, so both surfaces now pass the calling key toquotaForKey(pure, in the mirroredmemory-quota.ts). For a key narrowed topersonalor selected orgs it recountsorg_countovereffectiveOrgIdsand recomputestotal:0underpersonal, only theorg_idsthe owner still belongs to underselected, never more than the owner's own figure. JWT callers and unrestricted keys are unchanged and trigger no extra read.personalis never narrowed, by tenancy or scope allowlist, because the cap is account-wide and a narrowed count would overstateremaining. The org recount iscountActiveOrgMemories(_shared/limits/org-memory-count.ts), one head-only count with the same filters as the RPC. Behaviour is tested inmemory-quota.spec.ts;tenant-scope-usage.spec.tspins both surfaces to the key.Not in this PR
Verification
gen-surfaces.mjs --checkandsync-edge-schemas.mjs --checkare in sync.vitestschemas: 318 pass.quotaForKeycases and 3 tenancy source guards.quota.test.mjs(91 tests) all pass.deno-check-functions.mjsreports 0 errors in every edge function.tsconpackages/webis clean.eslinton the changed files: 0 errors.CI is green at
f76e8f2: Integration smoke, edge typecheck, Typecheck/Test/Lint, Storybook, Playwright e2e and the Vercel preview. The tenancy narrowing itself is not exercised end to end: no smoke lane mints a scoped key.