Uh oh!
There was an error while loading. Please reload this page.
perf(security,protocol): stop asking the same question twice within one request - #10824
Conversation
📓 Docs Drift CheckThis PR changes 2 package(s): 21 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 5 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 19 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin f3d444353442b4a943fbd1ea3f1c26e9594e9778 && git checkout f3d444353442b4a943fbd1ea3f1c26e9594e9778
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin a7ea3289eb605c6681ca6bc904e4ab5f799d6504 fe87edaac0ebcecf7ae3851261022f24d2cf5ed0 && git checkout -B drift-repro a7ea3289eb605c6681ca6bc904e4ab5f799d6504 && git merge --no-ff fe87edaac0ebcecf7ae3851261022f24d2cf5ed0
node scripts/docs-audit/affected-docs.mjs --json a7ea3289eb605c6681ca6bc904e4ab5f799d6504
|
os-zhuang
commented
Aug 21, 2026
PM rulings: Q1 → A and B (B is filed as its own card). Q2 → A, filed. And direction 4's premise was mine, and it was wrong. The correction I owe this PRI dispatched tranche 1 with three items and called #4 "the duplicate reads … same request, same context, same answer. Remove the second read of each." That framing was wrong, and you disproved it with a live statement capture rather than accepting it:
So both "free wins" were one better-auth read plus one framework read, and the removable half belongs to tranche 2. Declining to implement them — "authorization-shaped changes wearing a de-duplication costume" — is precisely what 裁决 #3 asked for, and the one path by which tranche 1 could have shipped a silent security bug behind a green suite. Also noted: Q1 — direction 4: A, and B is filed as cloud-side card #10825A is right: nothing remains of direction 4 that directions 1 and 2 do not already carry. Fold it in and stop tracking it separately. B is more than "also worth doing" — it may be the highest-leverage no-risk work left, and the reason comes from a measurement taken after this card was written. cloud#1539 established causally (latency injection, R²=0.9994) that a request is 23.4 sequential legs, and that:
Legs 6–13 are 8 sequential round trips that could be 2–3. That is ~5–6 legs off 23.4 — roughly a quarter of the whole request — with no cache, no invalidation contract, and no staleness. Your four-axis argument for B holds up under that number: a batched read returns the same rows or fails loudly; a cache returns stale grants silently. Q2 — localization's three identical |
Uh oh!
There was an error while loading. Please reload this page.
Refs #10757 — tranche 1 of the per-request query program (deterministic de-duplication only; no caching, no staleness, no security surface).
Measured, not projected
One authenticated
GET /data/crm_account?$top=1, counted by the platform's own instrument (X-OS-Debug-Timing: json→Server-Timing: db;dur=…;desc="N queries") onpnpm dev:crm --fresh, admin principal, warm request:origin/main(f4e5d91)?$count=falseDo not read this as the performance problem being solved. It removes roughly a twelfth of a request's queries, and the ~22 that remain — session resolution, grant resolution, localization, metadata — are what the card's tranche 2 (cross-request caching) and the placement work on objectstack-ai/cloud#1518 are for. Neither of those is addressed here.
What changed
$count=falsenow skips the COUNT query —@objectstack/metadata-protocol.The parameter was already fully plumbed: declared in the spec (
ODataQuerySchema.$count), aliased on the wire ($count→count), reserved out of the implicit-field-filter bucket, arity-checked, boolean-coerced — and then deleted unread by the protocol-key strip infindData, so every paginated list ranengine.count()whether or not the caller wanted a total. No contract change was needed:FindDataResponse.totalis alreadyoptional()and already documented "if requested".falseopts out. An absent$countstill counts and still reportstotal. OData reads absent as "omit", and taking that reading would silently striptotalfrom every existing caller — none send the parameter, all read the number.totalis omitted, never estimated. A caller who declined the real number should not be handed a plausible-looking guess;hasMoreis still answered from the page (a full page means there may be more), the same page-local rule$searchalready uses.A find and its COUNT resolve permission sets once, not twice —
@objectstack/plugin-security.findDataanswers a paginated list with two engine operations and the security middleware runs on both, soselect * from sys_permission_set where name in (…)went out twice per list request with identical bindings (queries 21 and 23 of the 24 above). The resolution is now memoized per execution context and retired by any write:WeakMap— that object is built once per request (RestServer.execCtxMemokeys it byreq; the runtime/MCP dispatchers assemble one per dispatch) and is collected with it. No TTL, no table keyed by user id, nothing that outlives the caller it was resolved for;insert/update/deletethe middleware sees, placed ahead of theisSystembypass so a seeder, a package publish or the auto-org-admin grant invalidates too. Any write by any context retires every entry;positions,permissions,principalKindand the presence ofuserId);What is left is exactly two reads by one context with no write in between — the find/count pair, and the definition of a duplicate question.
The card's third item is NOT implemented, and the reason is a measurement
The card's direction 4 (
sys_userread twice,sys_user_permission_setread twice) is confirmed as a duplicate at the SQL level and rejected as a tranche-1 fix. Both pairs are one better-auth read and one framework read, and in each pair the removable half belongs to tranche 2:sys_usergetSessionresolveUserAuthzGrantsstep 7, forsys_user.ai_accessai_access— verified against a live/auth/get-sessionresponse.auth-manager.ts:1046refuses to declare it as a better-authadditionalFieldon purpose ("better-auth SELECTs explicit columns … a column that may not exist on every env yet → broken auth"). Seeding the framework read from the session is therefore impossible without reversing that decision; removing better-auth's own read is the card's direction 1 (session caching).sys_user_permission_setcustomSession.isPlatformAdmin(), limit 50resolveUserAuthzGrants§6, limit 100customSessionenrichment, which the data path never reads back — removing it is the card's direction 2 ("customSession is redundant work on the data path"), inauth-manager.ts, the file tranche 2 was split out to serialise on.Attempting either inside this PR would have meant widening better-auth's declared user schema or changing when
customSessionenriches — both authorization-shaped changes wearing a de-duplication costume, which is precisely what the tranche split exists to keep apart.Verification
Full log of commands and exit codes is in the report comment on the card. Headlines, at
fe87edaac:pnpm --filter @objectstack/plugin-security --filter @objectstack/metadata-protocol test→ exit 0; 69 + 128 files, 1348 + 1747 tests.objectql/rest/runtime/client→ exit 0; 561 test files.pnpm lint(repo-wideeslint . --no-inline-config) → exit 0.node scripts/pm/dispatch-gates.mjsderived for this diff → exit 0, includingcheck:type-check-debt --re-measure("33 ledger entr(ies) re-measured … none above its recorded number") andcheck:cross-package-test-inputs.New coverage, because the honest answer to "what already covers this?" was the dedupe: nothing; the invalidation: nothing:
packages/plugins/plugin-security/src/permission-set-resolution-memo.test.ts— the dedupe, plus four invalidation guards (system write, write by another context, different context object, grants rewritten in place).packages/metadata-protocol/src/protocol.count-opt-out.test.ts— both wire spellings and the boolean form opt out; absent andtrueare unchanged.Both suites were ablated to prove they can fail (mutation confirmed on disk by marker counts; both packages' tests import their subject from source, so no
distrebuild is involved):countOptOutforced tofalseFound while measuring, not fixed here
resolveLocalizationContextissues three identicalselect * from sys_setting where namespace = ?per request (queries 16–18). The function batches its own fallback read into one$in, but the settings-service path it prefers callsSettingsService.get()three times and each call runsloadRows(namespace, …)over the whole namespace. That is three reads of one question inside one request — tranche-1 shaped, needing no cache — but the card files localization under its direction 5, so it is left for the split's owner to place rather than smuggled in here.Generated by Claude Code