Skip to content

perf(core,metadata-protocol): an authenticated data request pays 6 warm serial rounds instead of 8, and 3 with the landed opt-ins - #24533

Merged
objectstack-fleet[bot] merged 10 commits into
mainfrom
claude/issue-10757-request-rounds
Oct 19, 2026
Merged

objectstack-fleet[bot] merged 10 commits into
mainfrom
claude/issue-10757-request-rounds

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #10757. This PR carries the card's engine legs (leg 4, leg 5, the cold double resolution and leg 6) under claim 6264940865; leg A is ruled out at 6265874149 (#24515 closed not_planned), so every item is landed or ruled out once this lands.

Clause-②: no (no accept set, served body or authz answer moves: fewer queries and rounds for the same answer; owes the seat's review of record before the queue, at CONTRACT_REVIEW_TIER by the seat's choice, since it changes when authz and localization caches are trusted)

What changed

An authenticated GET /api/v1/data/OBJECT?$top=1 on the stock showcase (local PostgreSQL 16) now pays fewer rounds:

  • warm admin: 11 queries / 6 rounds, was 12 / 8;
  • warm member: 10 / 6, was 11 / 7;
  • with the landed opt-ins on (grants cache TTL above 0, $count=false): 3 / 3, was 4 / 4.

The served bodies and the resolved authz envelopes are unchanged.

  • Leg 4, landed (packages/core/src/security/resolve-authz-context.ts). The Default Organization read (sys_organization by slug) and the activation-ledger read (sys_metadata_activation) are now one wave. Before, they were awaited one after the other.

    • While the organization read is open, the ledger is asked about every permission-set name that may apply.
    • One recorded widening: an organization-owned admin_full_access grant that turns out not to anchor platform standing adds admin_full_access to the ledger's name IN list. Nobody reads that verdict, and the envelope does not move.
    • ⛔ No predicate is edited. The organization read is still { slug: 'default' } with limit 1. grantAppliesInTenant and grantAnchorsPlatformStanding (the NULL-organization readings that the C8 lane holds, 6264887348) are called as before and not edited. A pin asserts the organization read byte-for-byte.
  • Leg 4, the duplicate sys_user read: ruled out here. The two reads are better-auth's session-to-user read (plugin-auth's adapter) and getUserRow inside the customSession callback's resolveUserAuthzGrants call. getUserRow is already memoized within one resolution.

  • Leg 5, landed (the same file, localizationCache). A success entry is now retired only by:

    • a write that can change its answer: a write to sys_setting or sys_platform_setting, the two tables SettingsService.getMany and the direct fallback read, observed after it settles by an unkeyed engine middleware;
    • a non-write epoch reason (remote, metadata, manual);
    • the localization settings change seam;
    • the TTL.

    Before, any write to any object retired it. On an idle boot, the outbox pollers' claim updates were such writes.

  • Cold, ruled out with its reading (request-grants-memo.ts, the attachedHere guard in commit). The guard is load-bearing:

    • A one-shot reading (scratch test, not committed) on the memo suite's own doubles: a revocation that started before the observer existed and landed between the first request's two resolutions. With the guard, the request is authorised without the revoked auditor position. With the guard removed, it is served auditor.
    • A dist-only experiment with the guard removed measured the first request at 16 / 8 instead of 23 / 10, so the guard is the whole cause of the double resolution.
    • The only change that keeps the memo's argument whole is to register the observer before the first request: a boot-time seam, outside this card's file fence. It would save 7 queries and 2 rounds once per process. It is not built here (four-axis reasoning in the report).
  • Leg 6, landed in packages/metadata-protocol (this lane). Nothing changes in @objectstack/rest, so nothing is declared to domain:cli.

  • Docs: the OS_LOCALIZATION_CACHE_TTL_MS and OS_METADATA_OVERLAY_CACHE_TTL_MS rows (content/docs/deployment/environment-variables.mdx) and the overlay-cache note (content/docs/concepts/metadata-lifecycle.mdx) now state the new retirement rules.

Reading: local PostgreSQL 16.15, method of 6255731843

  • Setup: the stock showcase, pnpm dev -- --fresh -p PORT --database postgres://…, a fresh database per boot. Each request is GET /api/v1/data/OBJECT?$top=1 with X-OS-Debug-Timing: json, 2.5 s apart. Per persona (admin, and auditor.demo@example.com), three objects (sys_user_preference, showcase_task, sys_api_key), four requests each.
  • Instrument: a temporary per-query log in SqlDriver.installQueryTiming, applied to the source, built into dist/ and then reverted. The source is blob-equal to HEAD and the dists were rebuilt clean afterwards. The admin's logged count equals Server-Timing's N queries on every request.
  • Rounds: a query that starts before the latest end of the current round joins it; otherwise it opens a new round.
  • Background write: before each object's third request, one untimed PATCH of a showcase_task row. That object is unrelated to localization and to sys_metadata.
  • Runs: L = behind a +20 ms RTT proxy (switched on after boot), R = direct.
  • Trees: main = 913fd6df8a. After = this branch merged with origin/main f4755a68a, head 4746b38c1.
case main L1 / R1 after L1 / R1
warm admin 12 / 8 · 12 / 8 11 / 6 · 11 / 6
warm member 11 / 7 · 11 / 7 10 / 6 · 10 / 6
first request after boot (admin) 23 / 12 · 23 / 12 23 / 10 · 23 / 10
warm, after a write to an unrelated object 14 / 9 (admin), 13 / 8 (member): 6 of 6 retired localization 11 / 6 and 10 / 6: 0 of 6 retired anything
opt-ins on (grants TTL 10000, $count=false), warm (L2c / R2c) 4 / 4 · n/a 3 / 3 · 3 / 3

The warm admin request after, per query (final-L1, sys_user_preference #2):

# round query
1 1 sys_session by token (better-auth)
2 2 sys_user by id (better-auth's fallback join, leg A)
3–7 3 wave 1: sys_user, sys_member ×2, sys_user_permission_set, sys_user_position
8–9 4 sys_organization by slug and sys_metadata_activation (was rounds 4 and 5)
10 5 the data SELECT
11 6 the COUNT
  • Gone from the warm request: sys_metadata by name (was round 6). The localization pair is gone too, except at its 30 s TTL (the two remaining misses in each run land 30.4 s and 30.1 s after the key's first read).
  • The member's request: the same, without sys_organization.
  • Opt-ins on: sys_session, sys_user, the SELECT.
  • Wall time, illustrative only: a synthetic RTT, not deployment evidence, and ⛔ no claim about cloud#1521's gate or cloud#1518's placement. With the proxy, the warm admin DB span is 207 ms on main and 169 ms after. With the opt-ins on it is 111 ms and 79 ms.

Controls (byte identity, normalized for per-boot random ids and timestamps):

  • The resolved authz envelope (positions, permissions, org_user_ids, accessible_org_ids, posture, tab permissions) is one distinct value per persona, and it is identical on main and after, in every pair compared (main-R1 against final-R1 and final-L1; main-L2c against final-L2c and final-R2c).
  • Served bodies: 24 / 24 identical for main-R1 against final-R1, and against the intermediate leg run.
  • Across other boot pairs, showcase_task picks a different seeded row under order by id asc limit 1, because seeded ids are random per boot. The titles differ; the keys and totals do not. sys_user_preference and sys_api_key are identical in every pair.

Pins and ablations

Each ablation mutates only this package's source, and the subject is imported by a relative specifier, so no rebuild leg applies. Each restore was checked blob-equal to HEAD.

  • Leg 4: resolve-authz-context.request-rounds.test.ts (new, 5 cases). The envelope written out, the reads issued, and 2 legs.

    • Ablation: the base resolver restored. 4 of 5 red. Three of them red on the leg count alone (expected 3 to be 2), with the envelope and reads asserted green before it. The fourth is the recorded widening, red on the ledger read.
  • Leg 5: resolve-localization-cache.test.ts. Its doubles now carry the engine's middleware seam. New pins:

    • writes to sys_http_delivery, sys_notification_delivery, showcase_task and sys_session do not retire the entry, with zero reads;
    • a sys_platform_setting write retires it;
    • the retirement lands after the write settles;
    • metadata and remote bumps retire it;
    • no middleware seam means no cache;
    • TTL 0 registers nothing;
    • the failure memo survives a watched write.

    Ablation: the leg-4 version of the file restored. 9 red, the in-flight case among them.

  • Leg 6: protocol.item-read-overlay-cache.test.ts (new, 7 cases) and a new meta-overlay-cache.test.ts §10.

    • Ablation 1: servedRead set to false. 4 red; the authoring-read, no-seam and TTL-0 controls stay green.
    • Ablation 2: the epoch-keyed meta-overlay-cache.ts and protocol.ts restored. 10 red.
    • The prolongation suite's double gained the middleware seam so that its arms still run against a live cache. Ablation: applyRemoteMetadataMutation's bumpWriteEpoch removed. 3 red.
  • Pin sweep: request-grants-memo* does not move. The resolve-authz-context.batch-equivalence goldens do not move (no fixture holds an organization-owned admin_full_access). rest-data-read-one-overlay-read.test.ts does not move: its engine has no write-epoch seam, so the cache declines there, as before. The rest/hono localization pins pass unchanged.

    • Moved: the doubles in resolve-localization-cache.test.ts, meta-overlay-cache.test.ts and protocol.datasource-delete-prolongation.test.ts, each to carry the seam a real engine has.
    • scripts/engine-double-contract.pinned.json learned the new suite's findOne double (--write).

Verification (head 4746b38c1)

  • pnpm --filter @objectstack/core typecheck: exit 0. pnpm --filter @objectstack/metadata-protocol typecheck: exit 0. --listFiles counts the new and edited test files in both programs.
  • pnpm --filter @objectstack/core test: exit 0, 106 files / 2580 tests. pnpm --filter @objectstack/core test:repo: exit 0, 6 / 82.
  • metadata-protocol vitest: exit 0, 248 files passed and 3 skipped, 20723 tests.
  • The rest one-overlay-read and business-timezone pins: 46 / 46. The plugin-hono-server localization pin: 13 / 13.
  • turbo run build (all 76 packages) after merging origin/main: exit 0.
  • node scripts/pm/dispatch-gates.mjs --commands (no paths) derived 109 families. Every one was run with its exit code captured before any pipe, and all exited 0 (check:engine-double-contract after the ledger write). --ran: "109 derived famil(ies) accounted for — 109 run, 0 NOT-MEASURED (a DERIVED zero)".
  • Fix round 1 (head 084e79e16), for Test Core (5/6): environment-overlay-five-types.test.ts stored its overlay row with driver.create after a control read that now fills the by-name overlay cache, so no engine middleware saw the write. The row now goes through engine.insert under a system context, as every production writer does. No cache was disabled, no TTL changed and no assertion was weakened. Before: exit 1, 10 failed. After: 26 / 26. A sweep of the tests that write sys_metadata, sys_setting or sys_platform_setting straight into a driver found no other one that reads before it writes. Full suites at 084e79e16, each exit 0: objectql 446 / 8392, runtime 422 files, rest 300 files, plugin-security 188 files, service-settings 51 / 881, plugin-hono-server 34 / 422, mcp 40 / 479. objectql and rest typecheck exit 0. --ran over both rounds: "111 derived famil(ies) accounted for — 111 run, 0 NOT-MEASURED (a DERIVED zero)".

Acceptance notes

  • Still paid, for the card's remaining lane: leg A's second round, ruled out under the current engine contract (6265874149); the duplicate sys_user read (needs a plugin-auth seed; carrier: none); the cold double resolution (a boot-time observer registration); and the COUNT unless $count=false.
  • Grants cache default unchanged: with its default of 0 (Design: cross-request caching for the authenticated request path (tranche 2 of #10757) — write-invalidation-first, short-TTL fallback, configurable staleness window #11633 Fork 4, the maintainer's), leg B still costs 2 rounds per warm request. This PR does not move it.
  • What the after-reading shows: with the landed opt-ins on, the warm request is already at 3 rounds before leg A lands. With leg A it would be 2.
  • The ⚠️ pre-registration write: the localization and overlay observers register on the first read that names an engine. A write that started before that registration never passes them. The epoch reading they replace was blind to it as well, and the TTL bounds it. Stated in both docblocks.
  • The transaction window: the localization and overlay caches are retired when a watched write's statement completes. Inside an engine transaction that comes before the commit, so a read landing between the two can keep the pre-commit rows until the next watched write or the TTL. The epoch reading this replaced had the same window. Stated in metadata-lifecycle.mdx and environment-variables.mdx.

🤖 Generated with Claude Code

https://claude.ai/code/session_011Xuc8buBLe9f15TPKBjPKS


Generated by Claude Code

…ead are one wave

Part of the #10757 engine legs (leg 4). resolveUserAuthzGrants awaited the
sys_organization by-slug read, then the sys_metadata_activation read; both
depend on wave 1 only. The ledger is now asked about every name that may
apply while the anchor is open, so the two reads run together. Envelopes and
read predicates are unchanged; the organization-less readings are called, not
edited.

Claude-Session: https://claude.ai/code/session_011Xuc8buBLe9f15TPKBjPKS
Co-authored-by: Claude <noreply@anthropic.com>
…rom retires its cache

Part of the #10757 engine legs (leg 5). The localization success cache was
retired by the engine write epoch, which every write to any object advances,
so the two idle outbox pollers made warm requests pay the settings read again.
It now registers the leg-B shape of seam: an unkeyed engine middleware that
retires the bucket after a write to sys_setting or sys_platform_setting
settles, plus every non-write epoch reason. The settings change seam, the TTL
and the TTL-only failure memo are unchanged.

Claude-Session: https://claude.ai/code/session_011Xuc8buBLe9f15TPKBjPKS
Co-authored-by: Claude <noreply@anthropic.com>
…te verb, as the engine does

Claude-Session: https://claude.ai/code/session_011Xuc8buBLe9f15TPKBjPKS
Co-authored-by: Claude <noreply@anthropic.com>
…s through the overlay cache

Part of the #10757 engine legs (leg 6). The REST API-exposure gate reads the
object by name on every /data request, and getMetaItem's active-row read was
the one sys_metadata round trip #11967's overlay cache did not cover. The
served face now reads those rows through the same cache (pre-read epoch, TTL,
clone both ways) under a key naming the item; the protocol's own authoring
reads keep reading the store.

Claude-Session: https://claude.ai/code/session_011Xuc8buBLe9f15TPKBjPKS
Co-authored-by: Claude <noreply@anthropic.com>
…y cache

Part of the #10757 engine legs (leg 6). The overlay rows (list read and the
served by-name read) are read from sys_metadata alone, but the cache was
retired by the engine write epoch, which every write to any object advances.
It now uses the leg-B seam: an unkeyed engine middleware that retires it after
a sys_metadata write settles, plus every non-write epoch reason (the metadata
bridge's 'remote' bump included). The doubles carry the middleware seam a real
engine has, so the prolongation arms keep exercising a live cache.

Claude-Session: https://claude.ai/code/session_011Xuc8buBLe9f15TPKBjPKS
Co-authored-by: Claude <noreply@anthropic.com>
…by-name cache suite's findOne double

Claude-Session: https://claude.ai/code/session_011Xuc8buBLe9f15TPKBjPKS
Co-authored-by: Claude <noreply@anthropic.com>
…e tables they read

Claude-Session: https://claude.ai/code/session_011Xuc8buBLe9f15TPKBjPKS
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/core, @objectstack/metadata-protocol, touching 33 documentable anchor(s).

31 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json ba4afcc2391922a86678bf5e47b1a0e4d2c87ff1.

⛔ 11 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 4 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 33 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json ba4afcc2391922a86678bf5e47b1a0e4d2c87ff1 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 4ba5a2eed74f36111e6dc64494e0eeffbef6aafa — the merge of head 084e79e1659a99bb77bfe015c654c0b57f9ebfb5 into base ba4afcc2391922a86678bf5e47b1a0e4d2c87ff1, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 4ba5a2eed74f36111e6dc64494e0eeffbef6aafa && git checkout 4ba5a2eed74f36111e6dc64494e0eeffbef6aafa
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin ba4afcc2391922a86678bf5e47b1a0e4d2c87ff1 084e79e1659a99bb77bfe015c654c0b57f9ebfb5 && git checkout -B drift-repro ba4afcc2391922a86678bf5e47b1a0e4d2c87ff1 && git merge --no-ff 084e79e1659a99bb77bfe015c654c0b57f9ebfb5

node scripts/docs-audit/affected-docs.mjs --json ba4afcc2391922a86678bf5e47b1a0e4d2c87ff1

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs ba4afcc2391922a86678bf5e47b1a0e4d2c87ff1 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 4746b38c1052cd0451448c07cc6df9bf69bbfa58
Local-runs: none
Scope: .changeset/10757-request-rounds-core.md, .changeset/10757-request-rounds-metadata-protocol.md, content/docs/concepts/metadata-lifecycle.mdx, content/docs/deployment/environment-variables.mdx, packages/core/src/security/resolve-authz-context.request-rounds.test.ts, packages/core/src/security/resolve-authz-context.ts, packages/core/src/security/resolve-localization-cache.test.ts, packages/metadata-protocol/src/meta-overlay-cache.test.ts, packages/metadata-protocol/src/meta-overlay-cache.ts, packages/metadata-protocol/src/protocol.datasource-delete-prolongation.test.ts, packages/metadata-protocol/src/protocol.item-read-overlay-cache.test.ts, packages/metadata-protocol/src/protocol.ts, scripts/engine-double-contract.pinned.json + packages/objectql/src/write-epoch.ts, packages/objectql/src/engine.ts (executeWithMiddleware, registerMiddleware, hasObjectMiddleware, transaction, insertMany, the bound-object delegates), packages/core/src/security/resolve-user-grants-cache.ts, packages/core/src/security/request-grants-memo.ts, packages/core/src/security/index.ts, packages/services/service-settings/src/settings-service.ts, packages/services/service-settings/src/settings-service-plugin.ts, packages/metadata-protocol/src/sys-metadata-repository.ts, packages/metadata-protocol/src/seed-loader.ts, packages/metadata-protocol/src/migrations/overlay-index.ts, packages/metadata-protocol/src/migrations/recorded-by-sentinel.ts, packages/metadata-protocol/src/index.ts, packages/metadata-protocol/package.json, packages/rest/src/meta-item-read-gate.ts, packages/rest/src/rest-server.ts (enforceApiAccess), packages/plugins/plugin-auth/src/auth-manager.ts (the customSession call, the ai_access note), packages/cli/src/utils/secret-reference-union.ts, packages/cli/src/commands/secret/orphans.ts, scripts/check-dev-prereqs.mjs

Inputs: card #10757 (body and all 17 comments, the ruling 6250819728, the reading 6255731843, the release 6255757782, the retriage 6256237052, the claim 6264940865, the dispatch 6264971259, the leg-A note 6265900988, the os-dev-report 6267599450 read as claims), #11633 (the design 5394942824 §4 leg C/D and Forks 3–4, the acceptance 5404681591), #11967 (the leg-D report 5440580595 and its ACCEPT), #15194's territory declaration 6264887348 and the two rulings after it (6266750393, 6267391172 — neither answers the declaration), #24515 (closed not_planned at 6265874149), PR #24533's body, 13-file list and the net diff against merge base f4755a68a7 (+1218/−211), and the head's check-runs read twice (12:57Z and 13:06Z).

① Derived judgments

Leg 5 — the localization cache's retirement set is exactly the set of writes that can change its answer. Right.

  • What the answer is read from: SettingsService.loadRows reads the global rung from sys_platform_setting (settings-service.ts:81, PLATFORM_SETTING_OBJECT) and the tenant and user rungs from sys_setting (:58, DEFAULT_OBJECT); the resolver's direct fallback reads sys_setting through ql.find. The manifest defaults and OS_LOCALIZATION_* are process state. So LOCALIZATION_CACHE_WATCHED_OBJECTS = {sys_setting, sys_platform_setting} is complete.
  • Every in-process writer of those two tables goes through the engine: the settings service writes with this.engine.update(object, …) / this.engine.insert(object, …) (settings-service.ts:2957, :2981) and emits emitChange (:773, :2223, the primary seam, unchanged); seeds write through this.engine.insert/update (seed-loader.ts:1149–1181, :2630–2675). A repo-wide search of non-test code finds no raw SQL or knex write to either table; the one raw handle on sys_setting is packages/cli/src/commands/secret/orphans.ts:313, a find, in a CLI process this cache cannot see anyway (the peer-write case, TTL-bounded, unchanged from before).
  • The seam fires for every engine write verb: insert, update, delete each run executeWithMiddleware (engine.ts:15469, :16873, :19697); insertMany is this.insert(object, rows, …) (:16576); the bound-object API delegates to engine.insert/update/delete (:21616–21660); there is no upsert. An unkeyed registration is in applicable for every object (:6627–6629) and is not counted by hasObjectMiddleware (:6602), as the new pin asserts. Any non-read verb on a watched object, including one the list has never heard of, retires (the read set is find/findOne/count/aggregate), the safe direction. The non-write reasons (remote, metadata, manual) retire wholesale through writeEpoch.subscribe — the same two-seam shape resolve-user-grants-cache.ts:196–236 landed for leg B.
  • The failure entry keeps its TTL-only rule (localizationEntryIsLive returns true for kind === 'failure'), pinned.
  • Timing: the generation is read before the resolve and stored with the entry; the middleware bumps in finally after next() settles, so an in-flight resolve across a write stamps the pre-write generation and is dead on arrival (pinned, "the retirement lands AFTER the write settles").

Leg 6 — the overlay cache's retirement set. Right, with one residual window named below (TTL-bounded, not a bypass, and narrower than before).

  • The cached value is the raw sys_metadata row set and nothing else, for the list read and the by-name read alike. Every writer of sys_metadata in server code goes through the engine: sys-metadata-repository.ts:1168 (update), :1177 (insert), :1323 (delete), protocol.ts:31072 (delete), the seed loader's this.engine.insert/update. The two migrations/* files are DDL (overlay-index) or a sys_metadata_history update (recorded-by-sentinel). The only raw handle, packages/cli/src/utils/secret-reference-union.ts:559, is a driver.find in a CLI process. No raw SQL writer exists.
  • Writes whose visible effect comes from another source: a package's disabled flag and the artifact table are registry/in-memory state consulted on every call (getMetaItem runs isObjectOfDisabledPackage before readMetaItem; readMetaItem merges layers per call); sys_metadata_activation feeds the grants envelope (leg B's watched set), not this row set. Package install/uninstall/publish change sys_metadata rows through the repository, so they retire. A peer's write arrives as applyRemoteMetadataMutation → bumpWriteEpoch(this.engine, 'remote') (protocol.ts:8143) → the subscribe listener bumps the generation; pinned by the prolongation suite's ablation. metadata.changed still does not retire it — unchanged since authz caching leg D: getMetaItems overlay cache keyed on the registry epoch — resolve the SchemaRegistry-hydration trap explicitly #11967 (5440580595 refuted that channel), and the docs page now says so correctly.
  • Residual window, stated exactly: SysMetadataRepository.put and delete run inside withTxn → engine.transaction (sys-metadata-repository.ts:685–693, :1069, :1285), writing the sys_metadata row and then the sys_metadata_history row in one transaction. The middleware bumps the generation when the sys_metadata operation settles — before the history insert and before commit. A /data request whose by-name read starts in that window reads the pre-commit row on another connection and stamps the new generation, so enforceApiAccess can hold a pre-publish enable for that one object until the TTL (30 s) or the next sys_metadata write. This is not a missed write path: the write passes the seam; the seam fires before commit. The same window existed for the list read since authz caching leg D: getMetaItems overlay cache keyed on the registry epoch — resolve the SchemaRegistry-hydration trap explicitly #11967, wider (the epoch bumped at operation START, engine.ts:6623), so for the list read this PR narrows it; for the by-name read it is new only in that the read is cached at all, which the retriage ordered, and the design's rule (Design: cross-request caching for the authenticated request path (tranche 2 of #10757) — write-invalidation-first, short-TTL fallback, configurable staleness window #11633 §0: seam plus TTL bound) covers it. The window is one history-insert round trip wide. Judged acceptable; the docs prose should say "commits" where it says "settles" (see ②).
  • Pre-registration write in flight: stated in both docblocks and the PR body; the epoch reading it replaced was blind to it too; TTL-bounded.

Leg 6's by-name coverage — byte-identical for every caller. Right. readItemActiveRows(type, name, cached = false) is uncached for every caller except getMetaItem → readMetaItem(request, { servedRead: true }) → readItemActiveRows(…, true), and only when readState === 'active' (protocol.ts:11309, :11444); draft and previewDrafts reads are untouched. The hit is a structuredClone of rows cloned on store, upstream of the parse, the package-less binding, the served-row selection and the lock, which all run per call. The authoring read is pinned to keep reading the store. An engine without both seams, or TTL 0, declines (pinned; rest-data-read-one-overlay-read.test.ts's epoch-less engine keeps its multiset). The key spells the by-name entry ['item', type, packageId, name] so it never collides with the list entry. The cache is keyed per engine; findEnvironmentRows adds no environment column (the environment is the engine's database, ADR-0131 D6), so the per-engine key is sound.

Leg 4 — the merged wave. Right; the envelope and the C8 regions are byte-identical.

  • The old ledger question was the names of applyingGrants (grantAppliesInTenant(org, tenant) || anchorsPlatformStanding(r)); the new one is the names where mayApply (grantAppliesInTenant || (anchorReadsDefaultOrganization && name === admin_full_access && org !== null)). Proof the new set contains the old: grantAnchorsPlatformStanding (:912–922) is true only when not walled and either org === null — then grantAppliesInTenant(null, …) is true (:861: !organizationId || …) — or org === defaultOrganizationId !== undefined, which implies anchorReadsDefaultOrganization and org !== null. So no name the old code asked is dropped, and the only addition is admin_full_access for an organization-owned grant that turns out not to anchor.
  • Nobody reads the extra verdict: disabled.permissions is consumed only as .has(name) for names in applyingGrants and boundSetNames (:1610–1640), disabled.positions only filters grants.positions; no later reader exists (checked past :1700). hasPlatformAdminGrant derives from anchorsPlatformStanding(grant) && active as before; the deactivated-anchor case is pinned.
  • The widening is a predicate move on the activation-ledger read in that one case: name IN gains one name and limit (names.length * 2, :1000) grows by 2. It is disclosed in the PR body, the changeset, the deviations and the pin (ledgerReadOf([... ADMIN_FULL_ACCESS])). The PR body's "the filter shape is byte-identical" is loose; the same sentence states the widening, so the record is honest.
  • The C8 regions named in 6264887348 are untouched: the diff's hunks in resolve-authz-context.ts are :1495–1600 and :1913 onward; grantAppliesInTenant (:861), grantOrganizationOf (:890), grantAnchorsPlatformStanding (:912) and readDefaultOrganizationId (:934, { slug: 'default' } limit 1) are outside them, and the organization read is pinned byte-for-byte in all five cases. Error direction: Promise.all rejects if either read throws; the sequential version threw too (fail closed, unchanged).

The cold leg — the attachedHere guard is load-bearing. Right, and rightly ruled out. withRequestGrantsMemo registers the observer at scope open (request-grants-memo.ts:235–236) and commit stores nothing for an engine this scope attached (:318). The engine snapshots applicable when an operation starts (engine.ts:6627), so a write in flight at registration never passes the observer, and its epoch bump already happened at its start (:6623), so epochAtOpen === epochNow and started === completed would both hold across it: without the guard a revocation landing between the first request's two resolutions is served stale. The dev's one-shot reading (guard removed → auditor served) matches this reading of the code. The only whole fix is registering the observer before the first request — a boot seam outside the fence; the dev's option A (leave it, once per process, 7 queries / 2 rounds) is the sound one.

The duplicate sys_user read, ruled out to plugin-auth. Right. Row 2 is better-auth's handleFallbackJoin through plugin-auth's adapter; row 3 is getUserRow (resolve-authz-context.ts:1318, memoized within one resolution) inside resolveUserAuthzGrants as called from the customSession callback (auth-manager.ts:4515). better-auth's user deliberately omits ai_access (auth-manager.ts:1747), which getUserRow is read for (:1723), so one read can serve both only through a plugin-auth seed. Both reads sit in wave 1, so removing one saves a query and no round — no measured pull for a core option nothing passes.

The re-reading against the retriage's target. Done-when met; Fixes #10757 is right. Retriage 6256237052's done-when: each item landed or ruled out with its reading, plus one local re-reading by 6255731843's method posted on the card. Leg A: ruled out with its reading (6265874149, carried at 6265900988, which corrects the expected floor to count leg A as 2 rounds). Leg 4: the wave landed and pinned; the duplicate read ruled out with its reading. Leg 5 and leg 6: landed and pinned. Cold: ruled out with its reading. The re-reading is posted at 6267599450 (method, trees, controls, per-query table): opt-ins on 3 / 3, the corrected floor; stock 11 / 6 admin and 10 / 6 member, from 12 / 8 and 11 / 7; after an unrelated write 0 of 6 retirements, from 6 of 6. The dispatch's "Part of" was conditioned on leg A landing; leg A was ruled out instead, which the done-when accepts, so the seat's patch to Fixes is right. The ruling's bounds hold: no claim about cloud#1521 or cloud#1518 is made; the stock default still pays 6 rounds and the PR says so.

② Semver level

@objectstack/core patch and @objectstack/metadata-protocol patch, Clause-②: no on the PR body and both changesets — right.

  • No accept set, served body or authz answer moves: the resolved envelope and the served bodies are unchanged by construction (above) and by the dev's controls.
  • No public surface moves. meta-overlay-cache.ts is not re-exported from packages/metadata-protocol/src/index.ts and the package's exports map has only ".", so readOverlayCacheGeneration, META_OVERLAY_CACHE_WATCHED_OBJECTS and the new name key field are package-internal. In core, LOCALIZATION_CACHE_WATCHED_OBJECTS, localizationWriteState and LocalizationWriteState are module-private; resolveLocalizationContext (exported at security/index.ts:213) and resolveUserAuthzGrants keep their signatures. readItemActiveRows and readMetaItem are private and gained defaulted parameters. The two env vars keep their names, defaults and parsing (malformed → off).
  • The internal WriteEpochSeam now requires subscribe; a real engine's WriteEpoch has it, and a double without it declines rather than caching — the stricter direction, pinned.
  • Docs prose against code: environment-variables.mdx and metadata-lifecycle.mdx state the new rule (watched-table write, non-write bump, TTL; a write to any other object does not retire) and match the code. One looseness to carry: "retires the entry once the write settles and before the next read" / "read-your-writes is exact on the node that made the write" is exact for a read that starts after the transaction commits; a concurrent request's read in the settle-to-commit window of a transactional publish can cache the pre-commit row for the TTL (①, leg 6). Acceptance-note material, not a changeset error.

③ Boundary flags

  • Fence widened to the list read's retirement, the two docs pages and the ledger. Accepted. (a) Narrowing the list read's trigger is the same module as the by-name coverage, is the trigger Design: cross-request caching for the authenticated request path (tranche 2 of #10757) — write-invalidation-first, short-TTL fallback, configurable staleness window #11633 §4 leg D names ("any engine-seam write to sys_metadata"), and without it the measured by-name cache was retired by every data write (leg456 run: 12 / 7 after each forced write). (b) Two hand-written docs pages stated the old rule; correcting them is owed, and neither is release-owned. (c) scripts/engine-double-contract.pinned.json was written by the gate's own --write for a double the new suite exercises; the head's "Lint · CI, release & engine gates" check is green.
  • The admin_full_access name. The dispatch's "no predicate change" is read against the territory declaration's named regions: grantAppliesInTenant, grantAnchorsPlatformStanding and the organization read's filter are byte-identical and pinned. The moved predicate is the activation-ledger read's name IN list and derived limit, in one case, read by no verdict, disclosed four ways. Answered as a declared deviation, not a breach of the C8 territory. Open: the epic seat has not answered 6264887348 (its two later comments on [epic] ADR-0131 — total organization ownership: no NULL organization_id (v18 line) #15194 are 18.x-line rulings). The dispatch owes "an objection-free read of this thread" before the queue; that is the dispatching seat's act, not this record's.
  • Six pushes. All to the one branch; AGENTS.md requires a WIP push before every minutes-long step, and the claim set no push budget. No flag.
  • Dist-only instrumentation and the check-dev-prereqs hand-stamp. A measurement deviation, disclosed: sources edited, built, reverted blob-equal, dists rebuilt clean; scripts/check-dev-prereqs.mjs:153–157 names the hand-run --stamp as the class its artifact-side check cannot see. The diff carries no instrumentation (13 files, none under packages/drivers), and no gate verdict rests on the instrumented dist. The readings are the dev's self-report; the pins and the head's check-runs are the verdicts this record rests on.
  • The first 70 families at the merge commit bc343e9b9, not the head. The head differs from it by the ledger JSON and two docs pages; the gate verdicts are the head's own check-runs: at 13:06Z, 36 success, 7 skipped, 4 still running — Test Core (1/6), Test Core (3/6), Test Core (4/6), Test Core (5/6). Type Check · workspace, Test Core (2/6) and Test Core (6/6) completed green between the two reads. The queue needs every check green; this record does not stand in for the four running ones.
  • open_questions answered. Cold prime: A (leave it) — a boot-time cross-package call for a once-per-process saving earns no seam; a forgotten prime degrades to today's safe path. Duplicate sys_user carrier: the reading is right that it is plugin-auth's; it saves a query and no round, so no card is owed until a measured pull exists — carrier none stands. List read's retirement: A (keep) — the accepted design's trigger; the remote bump is pinned.
  • out_of_scope_findings judged. (1) The duplicate sys_user read: no filing gate met (one query, zero rounds, carrier closed). (2) The cold double resolution: once per process, ruled load-bearing here; no filing gate met. (3) rest-data-read-one-overlay-read.test.ts's header "nothing is kept between requests" now describes only its seam-less double: a stale test comment, acceptance-note material, no card.
  • Noted for the acceptance notes, not raised by the dev: the settle-to-commit window on a transactional sys_metadata publish (①, leg 6) — bounded by the TTL, narrower than the epoch design it replaces, and the one place the docs' "settles" should read "commits".

Implemented-by: claude/issue-10757-request-rounds
Reviewed-by: session_01JtSQQ3EWsbNPzyYMug1tXy

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

CI note · domain:engine seat 1 · session_01JtSQQ3EWsbNPzyYMug1tXy · 2026-10-19T13:14Z

  • Red on 4746b38c1: Test Core (5/6) (run 39254699009). @objectstack/objectql's environment-overlay-five-types.test.ts, 10 cases: "a stored environment-wide overlay of the packaged %s is what its read serves", expected 'Packaged' to be 'Environment overlay' at :356.
  • This PR's, not main's: the leg's control read now fills the by-name overlay cache this PR adds. The test then stores the overlay with driver.create('sys_metadata', …), straight into the driver, past the engine's write seam that retires the cache.
    • Production writers all go through the engine (record 6268067889 ①, leg 6), so the fix is the test's write path, not the cache.
  • Fix round 1 is with the dev: the row goes through the engine, a sweep covers the same premise elsewhere, the dependent packages' suites run in full, and the record's two notes are carried. A delta note follows the new head; the record's verdict on the code stands meanwhile.

Generated by Claude Code

…ine, as every production writer does

The served by-name read is cached and retired by the engine's sys_metadata
write seam; a row put straight into the driver after the control read was a
write no production path makes. The docs name the statement-to-commit window
of that retirement, and the rest overlay-read pin's header says why nothing
is kept on its seam-less engine.

Claude-Session: https://claude.ai/code/session_011Xuc8buBLe9f15TPKBjPKS
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Delta note on record 6268067889

Head: 084e79e1659a99bb77bfe015c654c0b57f9ebfb5. Written in-seat; local runs: none.

This note covers one commit on top of the record's head (084e79e16, parent 4746b38c1), with no merge of main. It touches four files, +29/−13: two test files and two docs pages. No source file moved, so every judgment of record 6268067889 ①–③ carries over unchanged, and so does its PASS.

Inputs:

  • the fix-round report 6269271222, read as claims;
  • the commit's diff, read with git diff on fetched refs.

The red, Test Core (5/6) (note 6268090803): fixed at the cause.

  • storeEnvironmentRow in environment-overlay-five-types.test.ts now writes its row through engine.insert(…, { context: { isSystem: true } }), not driver.create. It writes the same row shape as before: no organization, no package, the body as JSON, active, version 1.
  • There is still no metadata write door in front of the row, and the control read still comes first. So the leg still asks the read alone. Its write now passes the sys_metadata seam that every production writer passes.
  • No cache was disabled, no TTL changed and no assertion was weakened. The dev's before/after: exit 1 with 10 failed, then 26 / 26.

The sweep: the dev covered tests that write sys_metadata, sys_setting or sys_platform_setting straight into a driver. Each one named is either seeded before the stack boots or before its first served read, or it reads through a loader the new caches do not serve. Accepted as the premise sweep the fix round asked for.

The record's two notes:

  • The rest pin's header now says why "nothing is kept between requests" holds only on its seam-less engine, and names where the real engine's retirement is pinned. Done.
  • "Settles": the record asked for "commits". The dev wrote "when the write's statement completes", and named the window inside an engine transaction, before the commit, in both metadata-lifecycle.mdx and environment-variables.mdx.
    • Accepted, and it is more exact than the note: "commits" would have stated a guarantee the middleware does not give.
    • The window is record ①'s settle-to-commit window. The epoch reading it replaced had the same one.

PR body: the seat patches in a fix-round bullet under Verification and the transaction-window bullet under Acceptance notes, using the dev's wording from 6269271222.

Full suites at this head, from the dev, each exit 0:

Package Result
objectql 446 / 8392
runtime 422 files
rest 300 files
plugin-security 188 files
service-settings 51 / 881
plugin-hono-server 34 / 422
mcp 40 / 479

The --ran reconciliation over both rounds gives 111 of 111. The ACCEPT waits on CI on 084e79e16.

Reviewed-by: session_01JtSQQ3EWsbNPzyYMug1tXy

VERDICT: PASS (record 6268067889 carries)


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 · domain:engine seat 1 · session_01JtSQQ3EWsbNPzyYMug1tXy · 2026-10-19T14:46Z · ACCEPT

Head: 084e79e165. The commits, in order: the reading on main, one commit per leg with its pin and its re-reading (leg 4, leg 5, leg 6), the cold leg's ruling-out, the ledger row, the docs, the changesets, a merge of main, and fix round 1 (the overlay test writes through the engine). Card #10757 (p2), claim 6264940865, dispatch 6264971259. Dev reports: 6267599450, and fix round 1 6269271222.

Item Reading
Shape Draft PR → main. The body opens Fixes #10757; leg A was ruled out at 6265874149 (#24515 closed not_planned). 15 files, +1243/−220. Not governed: check-governed-merges on the final file list matched 0 of 15 paths.
Scope Leg 4: the activation-ledger read joins the first wave, one round fewer per authenticated request. Leg 5: the localization cache is retired only by writes to sys_setting or sys_platform_setting, plus every non-write epoch bump. The failure entry keeps its TTL-only rule. Leg 6: the served by-name getMetaItem read, which the REST exposure gate makes on every /data request, takes its stored rows from the overlay cache. Only sys_metadata writes retire that cache. The cold double resolution: ruled out; the attachedHere guard is load-bearing. The duplicate sys_user read: ruled out to plugin-auth; it saves a query, not a round.
Reading By 6255731843's method on local PostgreSQL: with the landed opt-ins on, 3 rounds for both personas; stock, 11 queries / 6 rounds admin and 10 / 6 member. The served bodies and the resolved authz envelopes are byte-identical before and after.
Review CONTRACT_REVIEW_TIER record 6268067889 PASS on 4746b38c1. CI note 6268090803 (Test Core (5/6)). Delta note 6269310900 on this head: the record carries.
Declaration Clause-②: no on the claim, the PR body and both changesets (@objectstack/core patch, @objectstack/metadata-protocol patch).
Cross-lane epic:#15194: territory declaration 6264887348. The C8 regions are byte-identical and pinned (record ①). The thread has two later rulings and no objection, which meets the declaration's own condition. domain:devx: 6267689991 and 6267701042. domain:spec: 6267709075 and 6267716787. domain:cli: 6269996668 and 6270008721, for the fix round's one comment-only edit to a packages/rest test header. No objection on any of them, or on the card.
Gates Dev: 111 derived families over both rounds, 111 run, every exit 0. Core test:repo 82/82. Full suites at this head, each exit 0: objectql, runtime, rest, plugin-security, service-settings, plugin-hono-server and mcp. Core and metadata-protocol stand from the first round.
CI At this read on 084e79e165: ci-failure GREEN, all 41 check-runs completed, none failed. Mergeable, clean.

Disposition:

  • open_questions: all accepted, per record 6268067889 ③:
    • the cold prime is left as it is;
    • the duplicate sys_user read has no carrier;
    • the list read keeps its narrowed retirement.
  • deviations: all accepted (record 6268067889 ③ and delta 6269310900):
    • the fence widened to the list read's retirement, the two docs pages and the ledger row;
    • the measurement-only dist instrumentation, reverted blob-equal;
    • the first families measured at the merge commit;
    • "statement completes" in place of the record's "commits", which is the more exact statement.
  • out_of_scope_findings: none filed; none meets a filing gate (record ③). The transaction window before a sys_metadata publish commits is stated in both docs pages and the PR's acceptance notes. It is bounded by the TTL and narrower than the epoch design it replaces.
  • Downstream:

Landing: ordinary queue (pr_ready + automerge_enable through the relay). Fixes #10757 then closes the card.


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 19, 2026 14:48
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 19, 2026 14:48
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 19, 2026
Merged via the queue into main with commit 5a871e4 Oct 19, 2026
49 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-10757-request-rounds branch October 19, 2026 15:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/xl tests tooling

Projects

None yet

2 participants