Skip to content

Authenticated data requests issue ~20 DB queries in ~17 sequential legs — per-request auth/session/localization/metadata resolution has no cross-request caching, costing ~1.5s/request on remote Postgres #10757

Description

@os-zhuang

⚠️ 2026-09-01:重述停放理由。 本卡此前挂 pm:blocked,卡体里没有任何 Blocked-by: 行 —— 停放理由只活在一条评论里(Blocked-by: #11633),而解锁扫描只读 body。本次把理由写进 body,并把标签改为 pm:on-hold,因为那个 blocker 的活已经干完了,剩下的是一次测量,不是一个依赖。

Ruled: 6250819728 · 维护者「10757 在本地测试就可以。」 · 2026-10-18 — 原 Restart-when(远程 Postgres 重测)改为本地重测,判据换算见该评论(recorded by the triage seat)
Restart-touch: packages/core/src/security/resolve-authz-context.ts, packages/rest/src/rest-server.ts, packages/plugins/plugin-auth/src/auth-manager.ts

⛔ 原 Blocked-by: #11633 已不成立。 直接读 #11633 的当前状态(⛔ 不取本卡对它的转述):设计 2026-08-24 交付,维护者 2026-08-25 ACCEPT 并裁完四个分叉,卡本身已从 needs-user-decision 转为 tracking 协调父卡;sub_issues_summary = 5 / 5 完成 —— #11966(leg C)、#11967(leg D)、#11968(substrate)、#11969(ADR 转换)、#11971(leg B,授权缓存,即「设计真正要做的那条腿」,2026-08-30T14:44Z 关闭)全部关闭。

⇒ 本卡 2026-08-28 那次巡检问对了问题(「clearing that blocker does not hand this card work — it hands #11633 work」),只是当时最后一个子卡还没关。现在关完了,所以本卡欠的不是等待,是一次读数。

⚠️ NOT MEASURED,且本仓测不了:cloud#1521 的 ≤200ms 门今天是否已经通过、以及放置问题(cloud#1518)。仓内没有任何命令能到达部署数据。能测的人:repo:cloud 席,或握有该部署的运维。⛔ 在读数回来之前,不得据仓内任何数字宣称本卡已解决。


Found while root-causing objectstack-ai/cloud#1518 (control plane pays ~1.5s of server time on EVERY authenticated DB-touching request on prod, ~6s on staging, independent of query shape and response size). Traced at cloud's pin 0c24898c. The mechanism is entirely in framework packages, so per cloud's contract-first rule it is filed here rather than worked around in the consumer.

The measurement

Staging (OS_SERVER_TIMING=true) for GET /api/v1/data/sys_user_preference?$top=1:

wall=6674ms server=[parse=0.05ms, db=6240.57ms, serialize=0.06ms, handler=5812.91ms, total=5813.63ms]

handler ≈ total and db ≈ handler — the request is essentially all DB time. db > total is explained by the db mark being a per-query SUM (PerfTiming.count, packages/drivers/driver-sql/src/sql-driver.ts installQueryTiming) over queries that partly overlap (see the parallel settings reads below).

Where the queries come from — one authenticated GET /data/:object?$top=1 issues ~20 queries in ~17 sequential legs

Every leg is a Neon round trip. At the ~50–90ms/query this deployment measures, ~17 sequential legs are the ~1.5s.

A. better-auth getSession — no caching anywhere (5 queries):

  1. sys_session + 2. sys_user (better-auth core; cookieCache is not configured — zero grep hits in plugin-auth);
  2. sys_user_permission_set, 4. sys_permission_set (FULL list, limit 50), 5. sys_member — the customSession plugin re-derives platform-admin/org roles on EVERY getSession call (packages/plugins/plugin-auth/src/auth-manager.ts ~2898–2960).

B. resolveAuthzContext / resolveUserAuthzGrants (8 queries, sequential) (packages/core/src/security/resolve-authz-context.ts ~330–560):
6. sys_member {user_id}; 7. sys_user_position; 8. sys_member {organization_id} (fellow-org, limit 1000); 9. sys_user_permission_set (duplicate of A3); 10. sys_position {name $in}; 11. sys_position_permission_set; 12. sys_permission_set {id $in}; 13. sys_user {id} for ai_seat (duplicate of A2).

C. Localization (3 queries, parallel): resolveLocalizationContext → 3× settings.get('localization', …) → loadRows is uncached by design (#10221 caches only FAILED reads), so 3 sys_setting reads per request.

packages/rest/src/rest-server.ts ~800 documents this whole block as "~16 sequential queries" and memoizes it per request (execCtxMemo) — but nothing caches it ACROSS requests, so every request pays it once.

D. Route handler (5–6 more):

  • enforceApiAccess → loadObjectItems → getMetaItems('object') ALWAYS queries sys_metadata (1–2: the empty result triggers the alt-type retry) — packages/metadata-protocol/src/protocol.ts ~5700;
  • plugin-security's engine middleware runs resolvePermissionSetsForContext → dbLoader (sys_permission_set {name $in}) with no cache, and it runs TWICE — once for the find, once for the COUNT (security-plugin.ts ~1351);
  • the data SELECT itself;
  • findData runs engine.count() whenever a limit is present (protocol.ts ~8858) — a second data query even for $top=1.

E. Zero-hint observation: the SqlDriver's tenancy auto-scoping itself issues no extra queries — but its INPUTS (accessible_org_ids, org_user_ids) are what B pays for, including the limit-1000 fellow-org read.

Consequences on a real deployment (cloud prod/staging)

  • ~1.5s fixed server time per authenticated data request at ZERO concurrency (prod; ~6s staging where per-query RTT is ~300ms).
  • Requests being ~100% DB-bound makes the pg pool cap the request-concurrency cap: cloud measured a hard knee exactly past 10 concurrent (= its pool max), median doubling at 20. The Console home fires 27 API calls in one cold load.

Fix directions (each independently valuable, roughly by leverage)

⚠️ This list predates #11633's five legs landing. Re-derive which entries survive before acting on any of them.

  1. Session caching: enable better-auth cookieCache (signed cookie, short TTL) or an in-process TTL cache keyed by session token — removes A entirely on warm requests.
  2. customSession is redundant work on the data path: B re-derives everything A3–A5 derive, more completely. Consider gating the customSession enrichment to the endpoints that need it, or a short per-user TTL cache.
  3. Cross-request TTL cache for resolveUserAuthzGrants keyed (userId, tenantId) with write-invalidation or a short TTL. ⚠️ This is authz caching leg B: grants cache — coarse invalidation, default TTL=0 (off), expiry-boundary expiry, bypass list #11971's face and it has LANDED — do not rebuild it; measure it.
  4. Batch B's reads: steps 6–13 are ~8 sequential round trips that could be 2–3 batched/joined queries; the duplicates (A2/13, A3/9) are free wins.
  5. Cache successful localization reads with write-invalidation (the 新环境日志被 sys_setting 'no such table' ERROR 刷屏:本地化读取先于建表,真错误被噪音淹没 #10221 cache deliberately holds failures only; the dogfood analytics-timezone test pins read-after-write — invalidation-on-write satisfies it).
  6. Cache getMetaItems' sys_metadata read (short TTL / registry-epoch key), and skip the alt-type retry when the first read returned an empty-but-healthy result set.
  7. Reuse the find's permission-set resolution for its count (same request, same context — the middleware resolves twice today).
  8. $count=false fast path: honor the existing $count parameter to skip the COUNT for callers that don't need total (objectui's cold-load probes mostly don't).

Per-query evidence is directly obtainable on any deployment of current main by an admin: X-OS-Debug-Timing: json returns Server-Timing (db;dur=…;desc="N queries") plus X-OS-Debug-Timing-Detail (slowest parametrized statements) — rest-server.ts:1937 opens the disclosure gate for admin principals.

Generated by Claude Code


Generated by Claude Code

Activity

  1. added theissue type on Aug 21, 2026
  2. os-zhuang commented on Aug 21, 2026

    @os-zhuang
    ContributorAuthor

    Triage: routed to the decision inbox (needs-user-decision, type Task). This is a multi-week performance program, not one fix: 8 independent directions, and the highest-leverage ones (session cookie cache, grant-resolution TTL cache) trade permission-change latency against request latency — a staleness window on authz grants is a security-posture call, which sits on the manual floor. Landing spans plugin-auth, core, rest, metadata-protocol and driver-sql, so the post-decision split will be contract-first sub-issues per lane. Chinese four-facet analysis below.

    os-decision-facets(四棱卡面块;标记按字面文本写入)

    一句话问题:客户每点一次列表,平台先花 1.5 秒(staging 6 秒)把「你是谁、你能看什么、你说什么语言」从头到尾问一遍数据库 —— 二十来次往返,问完才开始干正事;并发一超过连接池就排队,控制台首屏一次要问 27 次。

    选项与真实代价:

    • A. 全量缓存计划(会话、权限、本地化、元数据四层都上短 TTL 缓存 + 批量合并重复读):业务上 = 请求从 1.5s 降到百毫秒级,像成熟 SaaS 的手感。真实代价:管理员改了权限后,最长一个 TTL 窗口内旧权限仍在生效 —— 撤某人权限后的几十秒他还能看到数据;每层缓存都是一个失效逻辑的永久维护义务。
    • B. 只做无语义变化的批量与去重(合并 B 段 8 次串行为 2–3 次批查、复用 find 的权限解析给 count、跳过多余的 alt-type 重试、$count=false 快路径):业务上 = 大约砍一半时延,零语义风险 —— 任何时刻权限都是现读的。真实代价:到不了百毫秒级;剩下一半时延仍在。
    • C. B 先行,A 分层逐个裁(先合并去重落地,再按「会话缓存 → 本地化缓存 → 权限缓存」逐层单独裁,每层带自己的失效实测):业务上 = 先拿下无风险的一半,带安全语义的每一层单独见你一次。真实代价:多轮裁决与多张卡的协调成本。
    • D. 不动:业务上 = cloud 生产每请求继续付 1.5s,Console 冷启动继续 27 连击,客户感知「平台慢」。

    四棱:

    • ① 项目长远合理性:请求路径的固定开销是平台横向底盘,拖得越久,上面长出的功能越多、返工面越大;C 的分层裁决给每层缓存留下明确的失效契约,优于一次性大改。
    • ② 实际业务拉动:cloud#1518 生产实测拉动,不是投机优化;objectui#5544(冷启动重复请求)是同一根因的前端面。
    • ③ 防 AI 犯错:缓存失效是 AI 最容易写错且错了静默的地方(旧权限静默放行,没人看见)—— 所以带语义的每一层必须有「改权限后立即生效」的实测用例作响亮门,B 类纯去重则结构上无此风险,可放心机械化。
    • ④ 创业阶段不扩散:不加新能力面,只还底盘债;C 把投入切成可叫停的片段,最合创业节奏。

    推荐:C(回退:B —— 若近期不想碰任何带语义的层)。回一个字母即可;「C,但权限层缓存不做」= C 去掉第三层。

    置信缺口(本分析看不见什么):staging 的 ~300ms/查询 RTT 是否代表目标部署形态(同 region 部署下 B 单独就可能够用);better-auth cookieCache 的签名 cookie 在现有多实例部署下的失效行为未实测。裁后执行:按裁定拆 contract-first 子单入各车道队列(engine/services/cli),每子单带各自门禁与实测判据,本卡转协调父单不派发。


    Generated by Claude Code

  3. os-zhuang commented on Aug 21, 2026

    @os-zhuang
    ContributorAuthor

    Maintainer decision — this card is released for development, and split in two

    Authorization, 2026-08-21, verbatim (quoted, not translated):

    「相关的任务你都安排开发。包括 https://github.com/objectstack-ai/objectstack/issues/10757」

    That resolves the needs-user-decision state, which was holding on how far to go across the 8 fix directions. Label removed. Routed under the maintainer-direct channel from the cloud-side performance epic (objectstack-ai/cloud#1521), whose PM session is 30b1d4ba-aaec-4c03-a7b4-d0d9c746cb8a.

    PM split, and the reasoning

    The 8 directions are not one PR, and they do not carry the same risk. They divide cleanly:

    Tranche 1 — deterministic de-duplication. No caching, no staleness, no security surface.

    These remove queries by not asking the same question twice. Nothing observes stale data afterwards that could not observe it before. Dispatched now.

    Tranche 2 — caching. Where most of the value is, and all of the risk.

    Tranche 2 is the larger win by far — roughly 13 of ~20 queries against tranche 1's ~4. It is deliberately not dispatched with tranche 1, for two reasons:

    1. File overlap. Both tranches touch resolve-authz-context.ts, and Add metamodel interfaces for ObjectQL/ObjectUI contract #1/✨ Set up Copilot instructions #2 touch auth-manager.ts alongside it. Same-file work must serialise; running them together produces a conflict, not parallelism.
    2. Caching session and authorization state is a security change wearing a performance costume. A stale grants cache is a privilege-escalation window: a permission revoked at T is still honoured until the TTL expires. That needs an invalidation design agreed before code, not discovered during review. It gets its own card, with that as the primary requirement rather than an afterthought.

    Honest scoping note: tranche 1 alone will not clear cloud#1521's ≤200 ms gate — it removes roughly a quarter of the queries. It is worth landing on its own merits (free wins, no risk), but the gate needs tranche 2, or placement, or both. Nobody should read a green tranche-1 PR as "the performance problem is fixed".

    Fresh reference numbers since this card was filed

    The cloud side has since measured per-query cost directly, which sharpens every projection here:

    per DB query
    prod control plane ~34 ms
    staging control plane ~178 ms
    ideal intra-region us-east-1 ~1–5 ms

    Both Neon projects are confirmed in us-east-1, so neither container is co-located with its database — the deployed cost is N × RTT with both factors wrong. This card owns N. Placement is being pursued separately on cloud#1518.

    A count of N measured directly (rather than the ~20-traced / ~35-implied range this card cites) is in flight as cloud#1539; when it lands it should be treated as authoritative over both estimates here.

  4. self-assigned this
    on Aug 21, 2026
  5. os-zhuang commented on Aug 21, 2026

    @os-zhuang
    ContributorAuthor
    {
      "issue": 10757,
      "status": "done",
      "branch": "claude/issue-10757-dedupe-per-request-queries",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/10824",
      "premise_still_valid": true,
      "duplicates_confirmed_on_main": "Confirmed as SQL, rejected as a tranche-1 fix. The card's whole trace reproduces on main byte-for-byte: I captured every statement of one authenticated GET /data/crm_account?$top=1 with DEBUG=knex:query on a live `pnpm dev:crm --fresh` rig, isolating the request by log byte-offset. 24 queries, matching the card's legs in order, including q3 sys_user (better-auth) / q15 sys_user (resolveUserAuthzGrants step 7) and q4 sys_user_permission_set (customSession) / q11 sys_user_permission_set (resolveUserAuthzGrants section 6). All four are `select *`, so at the SQL level each pair really is one question asked twice. EQUIVALENCE FAILS on the sys_user pair and OWNERSHIP fails on the other. (a) sys_user: better-auth's session payload does NOT carry `ai_access` — verified live against /api/v1/auth/get-session, which returns name/email/role/banned/positions/isPlatformAdmin and no extension columns — because auth-manager.ts:1046 deliberately refuses to declare it as a better-auth additionalField ('better-auth SELECTs explicit columns, so declaring it here would make getSession query a column that may not exist on every env yet -> broken auth'). So the framework's read cannot be seeded from the session without reversing that decision, and removing better-auth's own read is direction 1 (session caching, tranche 2). Note the email half of that read is ALREADY deduped on main via `seedEmail`, and resolve-authz-context.ts already carries an in-function getUserRow memo — main has moved since the card was traced, in the card's favour. (b) sys_user_permission_set: q11 is the authoritative resolution and cannot go; q4 is the customSession enrichment, which the data path never reads back (resolveAuthzContext consumes only user.id/email and session.activeOrganizationId, never positions/isPlatformAdmin) — removing it IS direction 2, in auth-manager.ts, the file the PM split tranche 2 out to serialise on. I did not attempt either: both are authorization-shaped changes wearing a de-duplication costume.",
      "queries_before_after": "24 before, 23 after, 22 after with ?$count=false. Measured, not reasoned: `X-OS-Debug-Timing: json` on an admin principal returns `Server-Timing: db;dur=...;desc=\"N queries\"` (rest-server.ts opens the gate via isPerfDisclosurePrincipal; the counter is driver-sql's installQueryTiming, ALS-attributed per request). Same rig, same seed, same warm-request protocol before and after; cross-checked against the full knex statement log, which shows the second `select * from sys_permission_set where name in (?, ?, ?)` gone at 23 and the `select count(*) from crm_account` gone at 22. Honest ceiling: that is ~1/12 of a request's queries. The remaining ~22 are session, grants, localization and metadata reads that repeat every request — tranche 2 and placement (cloud#1518), neither addressed here.",
      "count_param_existed": "It existed and needed NO contract change. `$count` is declared in packages/spec/src/api/odata.zod.ts (ODataQuerySchema.$count), aliased on the wire by WIRE_DOLLAR_ALIASES ($count -> count), reserved out of the implicit-field-filter bucket by RESERVED_LIST_QUERY_PARAMS, arity-checked (protocol.query-param-arity.test.ts already pins 'neither true nor false'), and boolean-coerced in findData — and then deleted unread by the protocol-key strip. Omitting `total` is equally contract-clean: FindDataResponseSchema already declares `total: z.number().optional()` with the description 'Total number of records matching the filter (if requested)'. packages/spec was not touched.",
      "summary": "Two of tranche 1's three items landed; the third is reported rather than implemented, with the measurement that rules it out. Shipped: (7) the security middleware now memoizes a permission-set resolution per execution-context object and retires it on ANY engine write — the epoch bump sits ahead of the isSystem bypass so seeds/publishes/auto-grants invalidate too — so a find and its COUNT resolve once instead of twice; (8) findData honours an explicit $count=false by skipping engine.count() and omitting `total` (absent and $count=true are unchanged, so no existing caller moves). Not shipped: (4), because each 'duplicate' pair is one better-auth read plus one framework read and the removable half is directions 1 and 2 — tranche 2, in auth-manager.ts. No cache survives a request, a write, or a context; packages/spec untouched; no pin touched.",
      "tests": "All exit codes captured before any pipe. HEAD at the union run: fe87edaac. NOTE ON THE VERIFY LOCK: `bash scripts/pm/os-verify-lock.sh --status` prints 'host: CANNOT OPERATE THIS LOCK ... a stock macOS does not ship flock' — the entry point disqualifies itself on this host, so heavy commands ran directly rather than hand-rolling a lock. (1) `pnpm --filter @objectstack/plugin-security --filter @objectstack/metadata-protocol test` EXIT=0 — 'Test Files 69 passed (69) / Tests 1348 passed (1348)' and 'Test Files 128 passed | 2 skipped (130) / Tests 1747 passed | 10 skipped (1757)'. (2) downstream consumers `--filter @objectstack/objectql --filter @objectstack/rest --filter @objectstack/client --filter @objectstack/runtime test` EXIT=0 — 226 + 133 + 23 + 179 test files. (3) `pnpm --filter @objectstack/plugin-security typecheck` EXIT=0 ('typecheck: Done'); metadata-protocol has NO typecheck script (DEBT-ledger package) so its type coverage comes from the tsup dts build, which DID red on my first attempt ('src/protocol.ts(8977,57): error TS18048: total is possibly undefined') and is green now — `pnpm build` EXIT=0, 'Tasks: 71 successful, 71 total'. (4) `pnpm lint` (repo-wide `eslint . --no-inline-config`, no narrowing) EXIT=0. (5) every gate `node scripts/pm/dispatch-gates.mjs` derived (14 path-matched + 6 convention-triggered) run individually, all EXIT=0: check:changeset-gate-self-tests, check:cross-package-test-inputs, check:durability-log-level, check:filter-alias-parity, check:objectui-changeset, check:slot-lookup, check:test-source-alias, check:type-source-resolution, check:nul-bytes, check-adr-0087-registration.mjs, check-changeset-no-major.mjs, check-cross-package-test-inputs.mjs, check-empty-changeset.mjs, check-plugin-teardown-shape.mjs, check:query-options-erasure, check:type-check-coverage, check:engine-double-contract, check:where-matcher, check:i18n, docs-audit/check-affected-docs.mjs, and check:type-check-debt --re-measure whose verdict line reads 'check-type-check-coverage --re-measure: OK -- 33 ledger entr(ies) re-measured in 134.2s, 1912 raw tsc error(s) total, none above its recorded number'. That ratchet CAUGHT my new test file (+1 raw tsc error in plugin-security's TEST_DEBT: 'Tuple type [object: string] of length 1 has no element at index 1'); fixed in the test, not by raising the ledger. EXISTING COVERAGE OF THE CHANGED AUTHORIZATION PATHS: for the memo, none — resolve-permission-sets-for-context.pin.test.ts pins reachability through the service locator, and read-scope-provenance-mark.test.ts / security-plugin.test.ts drive the middleware but never twice under one context, so nothing observed the duplicate or would have observed a stale reuse. For $count, none — protocol-data.test.ts pins total/hasMore across limit/offset/search and protocol.query-param-arity.test.ts pins $count's arity, but nothing asserted the flag's EFFECT (it had none). Added: packages/plugins/plugin-security/src/permission-set-resolution-memo.test.ts (6 tests: the dedupe plus four invalidation guards — system write, write by another context, a different context object, grants rewritten in place) and packages/metadata-protocol/src/protocol.count-opt-out.test.ts (9 tests: both wire spellings and the boolean form opt out, hasMore from a full/short page, `count` stripped off the engine bag, and absent/$count=true/no-limit unchanged). ABLATIONS — three legs, each via a script carrying `trap restore EXIT INT TERM` (restore = `git checkout HEAD -- <paths>` off the committed fix), each mutation confirmed on disk by grepping BOTH the injected marker and the deleted text, never by an editor exit code. No dist rebuild is involved or claimed: both test files import their subject relatively (./security-plugin.js, ./protocol.js), so vitest resolves source. (A) memo disabled -> 2 red / 4 green: the two dedupe assertions fell ('expected 2 to be 1'), the four invalidation guards held, which is the documented direction — they are guards, not evidence. (B) write-epoch bump removed -> 2 red / 4 green: exactly the two write-invalidation tests fell ('expected 1 to be 2'); the deleted-text count read 1 rather than 0 because `this.writeEpoch++` also occurs at the metadata-watch site, a different statement the mutation left alone. (C) countOptOut forced to false -> 4 red / 5 green: the opt-out block fell, the 'unchanged' block held. `git status --porcelain` clean after each leg. RIG TEARDOWN: dev servers on port 39147 were located by lsof, their cwd verified as this worktree, and killed by PID (never by process name).",
      "open_questions": [
        {
          "question": "Where does the card's direction 4 go, now that both of its 'free win' pairs are measured to be one better-auth read plus one framework read, with the removable half owned by directions 1 and 2?",
          "options": [
            "A — fold direction 4 into tranche 2 and drop it as a separate item: direction 1 removes the sys_user duplicate as a side effect of session caching, and direction 2 removes the sys_user_permission_set duplicate as a side effect of gating customSession. Nothing is left of 4 that 1 and 2 do not already carry.",
            "B — keep direction 4 alive as its own card scoped to BATCHING legs 6-13 (the half of the card's direction 4 the PM did not dispatch): 8 sequential framework round trips that could be 2-3, no caching, no staleness. That is genuinely tranche-1-shaped work and is untouched by tranche 2.",
            "C — reopen the ai_access decision: declare it as a better-auth additionalField so getSession's row serves both readers. Removes one query and reverses a documented refusal."
          ],
          "recommendation": "A for the de-duplication framing, and B as a separate card that is worth dispatching on its own. Real business need: cloud#1521 measures the fixed cost per request, and legs 6-13 are 8 of the ~22 that remain — the largest no-risk block left. Long-term soundness: batching keeps every read live, so it needs no invalidation contract and cannot drift from tranche 2's caching design. Hard to get wrong: a batched read returns the same rows or fails loudly; a cache returns stale grants silently. Startup scope: B adds no capability surface, only removes round trips. C is refused on all four axes — it trades a documented auth-breaking risk for one query, on the exact path where a wrong answer is a 500 on every request."
        },
        {
          "question": "resolveLocalizationContext issues THREE identical `select * from sys_setting where namespace = ?` per request (measured as queries 16-18 of 24). The function already batches its own direct fallback into one $in, but the settings-service path it prefers calls SettingsService.get() three times and each call runs loadRows(namespace, ...) over the whole namespace. This is three reads of ONE question inside one request — tranche-1 shaped, no cache needed — but the card files localization under direction 5 (caching, tranche 2). I did not touch it: the tranche boundary is the PM's, and the dispatch said tranche 1 is exactly three items.",
          "options": [
            "A — dispatch it as tranche 1b: replace the three settings.get() calls with one namespace read the three keys are picked out of. No caching, no staleness, ~2 queries saved.",
            "B — leave it inside direction 5 and let the localization cache subsume it."
          ],
          "recommendation": "A. It is the same class as the two fixes in this PR (one question, asked repeatedly, inside one request) and it is INDEPENDENT of the caching decision — batching the three reads is correct whether or not direction 5 ever ships, and it shrinks what direction 5 has to cache. Filing it as an issue myself would have pre-empted a split the PM owns, which is why it is reported here instead."
        }
      ],
      "out_of_scope_findings": []
    }
  6. os-project-manager commented on Aug 22, 2026

    @os-project-manager
    Collaborator

    H8 配对写入:pm:dispatched → pm:queue

    交付 PR #10824 已于 2026-08-21T14:00:28Z 合并(分支 claude/issue-10757-dedupe-per-request-queries),本卡仍挂 pm:dispatched。

    ⚠️ 这张卡不只是没人回标 —— 它是 H8 的一次真漏报

    #10824 的正文首行写的是 Refs #10757,而 h8MergedPrStillDispatched(scripts/pm/check-half-states.mjs:1158)只认 Part of #N 与 closing keyword 两种拼法,不读分支名。所以巡查 2026-08-22T07:49Z 那轮对本卡只报了 H2(有 assignee 无认领评论),H8 静默 —— 同一轮对另外 6 张卡的 H8 都正常报出,可见谓词跑了,只有这一笔交付对它不可见。

    已另立 finding:#11036。

    剩余范围(按 #10824 正文自述)

    本次只做了 tranche 1 —— 每请求内的确定性去重,24 → 22 queries,明确写着「Do not read this as the performance problem being solved」。未做:

    • tranche 2 跨请求缓存(session / grant / localization / metadata 解析,约 22 条 query 的大头);
    • objectstack-ai/cloud#1518 的放置工作;
    • 现场发现:resolveLocalizationContext 每请求发三条完全相同的 select * from sys_setting where namespace = ?(q16–18),tranche-1 形状、不需要缓存,被留给拆分的所有者放置。

    ⇒ 剩余物可派发,故 pm:queue。

    ⛔ 另一半状态,我不越权处理

    本卡没有任何 domain:* 标签(domain:* 只由分诊席产出,我不代产)。它现在是「有 pm 状态、无域标签」,而 H13 抓的是反过来的那一半 —— 于是没有任何谓词会报它,它对每个车道的候选查询都不可见。请分诊补一个域标签,否则这张卡刚从一种隐身状态换到另一种。

    本笔是维护者指令下的 H8 清账(2026-08-22,session session_014kugUSM5M5fJBsk1f8KdtN):只做合并的配对写入,⛔ 未改卡面范围、未动 assignee。


    Generated by Claude Code

  7. huangyiirene commented on Aug 22, 2026

    @huangyiirene
    Collaborator

    Triage (half-annotation repair): routed domain:services — largest share of the eight fix directions sits on the auth/session/grants path (plugin-auth, resolve-authz-context), with rest/protocol legs as coordinated followers. No re-grade otherwise: the card is queued, typed, and carried by its assignee. Note for the record: this reads as a campaign parent (eight independently-valuable directions across three lanes) — if it is worked as such, per-direction sub-cards with Blocked-by:/Part of links beat one mega-PR, and directions 1–3 (session/grants caching) are the measured highest-leverage start.


    Generated by Claude Code

  8. 30 remaining items

  9. objectstack-fleet commented on Oct 19, 2026

    @objectstack-fleet
    Contributor

    Claim: PM loop round 12 · 2026-10-19T10:18Z · after retriage 6256237052 (p2, domain:engine, re-scoped to the five legs the local reading 6255731843 found still paid), on the maintainer's ruling 6250819728 「10757 在本地测试就可以。」. This seat takes the engine legs. Leg A is cut to domain:services as #24515, as the retriage directs. The epic:#15194 territory is declared at 6264887348.
    Session: session_01JtSQQ3EWsbNPzyYMug1tXy (the dispatching seat; the dev runs as its own cloud session, named in the dispatch comment)
    Account: os-project-manager (the seat's linked user as GET /user answers it; always the card's assignee)
    Branch: claude/issue-10757-request-rounds
    Worktree: objectstack-issue-10757
    Domain: domain:engine
    Seat: domain:engine#1 (seat post #6367)
    File surface:


    Generated by Claude Code

  10. objectstack-fleet commented on Oct 19, 2026

    @objectstack-fleet
    Contributor

    Dispatch · domain:engine seat 1 · session_01JtSQQ3EWsbNPzyYMug1tXy · 2026-10-19T10:19Z: mode:cloud. The dev session is session_011Xuc8buBLe9f15TPKBjPKS. It builds at the default tier on branch claude/issue-10757-request-rounds, under claim 6264940865.


    Generated by Claude Code

  11. objectstack-fleet commented on Oct 19, 2026

    @objectstack-fleet
    Contributor

    Leg A reading · from domain:services seat 2 (seat post #21118) · os-warren · session_01LR1cD9JqgkeqY7k9rrZH8s · 2026-10-19T11:14Z. ⛔ Not a claim, ⛔ no label change. This is for the claimant of this card's engine legs and for the re-reading.

    • Leg A (item 1 of retriage 6256237052) is ruled out with its reading. plugin-auth: an authenticated request reads sys_session then sys_user in two sequential rounds, because better-auth's advanced.database.joins is left unset and its fallback join runs (leg A of #10757) #24515 is closed not_planned with the reading and the seat's review on that card.
      • On SQLite, PostgreSQL 16.15 and driver-memory, findSession costs 2 reads in 2 rounds with or without advanced.database.joins, because objectql-adapter.ts drops the join (:897, :960).
      • No shipped driver or ObjectQL read carries a one-round join: query.joins and DriverCapabilities.joins are retired under ADR-0049.
    • So the retriage's expected floor needs correcting. "With the opt-ins on, the expected floor is 3 rounds: 4, less leg A's second round" does not hold. On the dev's reading, the 4→3 step is reachable only through leg 6, the by-name sys_metadata read (item 4 here).
    • The one re-reading this card's done-when asks for should count leg A as 2 rounds under the current engine contract.

    Generated by Claude Code

  12. objectstack-fleet commented on Oct 19, 2026

    @objectstack-fleet
    Contributor

    os-dev-report

    {
      "issue": 10757,
      "status": "done",
      "branch": "claude/issue-10757-request-rounds",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/24533",
      "session": "session_011Xuc8buBLe9f15TPKBjPKS — this dev's own cloud session (named in dispatch 6264971259); identity = the branch named in claim 6264940865, verified as the newest Claim: on the card",
      "premise_still_valid": true,
      "summary": "Engine legs of the re-scoped card, measured on main and after. Landed: leg 4's sys_organization-by-slug and sys_metadata_activation reads as one wave; leg 5's localization success cache retired only by sys_setting / sys_platform_setting writes (engine middleware, post-settle) plus non-write epoch reasons, the TTL-only failure memo unchanged; leg 6 in metadata-protocol — the served getMetaItem reads its active sys_metadata rows through #11967's overlay cache, and that cache (list read too) is retired only by a sys_metadata write plus non-write epoch reasons. Ruled out with readings: leg 4's duplicate sys_user read (both readers sit behind plugin-auth's customSession call and better-auth's user carries no ai_access, so one read serving both needs a plugin-auth change — #24515's lane), and the cold double resolution (the memo's attachedHere guard is load-bearing: removing it serves a revoked position on the first request; the only whole fix is a boot-time observer registration outside the fence). Local PostgreSQL 16 re-reading by 6255731843's method: warm admin 12/8 -> 11/6, warm member 11/7 -> 10/6, cold 23/12 -> 23/10, after an unrelated write 14/9 -> 11/6 (0 of 6 retirements, was 6 of 6), landed opt-ins on 4/4 -> 3/3 (direct and +20 ms proxy agree). Served bodies and resolved authz envelopes unchanged for both personas. Card stays open: PR says Part of #10757 (leg A, #24515).",
      "reading": {
        "tree_before": "objectstack-ai/objectstack 913fd6df8a (origin/main at dispatch)",
        "tree_after": "branch head 4746b38c1 = legs on 913fd6df8a merged with origin/main f4755a68a",
        "method": "6255731843's: stock showcase, pnpm dev -- --fresh -p PORT --database postgres://… on a private PostgreSQL 16.15 cluster (fresh database per boot); GET /api/v1/data/OBJECT?$top=1 with X-OS-Debug-Timing: json, 2.5 s apart, admin and auditor.demo@example.com, three objects (sys_user_preference, showcase_task, sys_api_key) x 4; per-query list from a temporary SqlDriver.installQueryTiming log (built into dist only, source reverted blob-equal, dists rebuilt clean); admin logged count = Server-Timing N queries on every request; rounds = maximal chains of overlapping execution windows; L = behind a +20 ms RTT proxy switched on after boot, R = direct; background write = one untimed PATCH of a showcase_task row before each object's third request",
        "main": {
          "warm_admin": "12 / 8 (L1, R1)",
          "warm_member": "11 / 7 (L1, R1)",
          "cold_admin": "23 / 12 (L1, R1)",
          "after_unrelated_write": "14 / 9 admin, 13 / 8 member — 6 of 6 forced writes retired localization (R1)",
          "opt_ins_grants_ttl_10000_count_false": "4 / 4 warm (L2c)"
        },
        "after": {
          "warm_admin": "11 / 6 (final-L1, final-R1)",
          "warm_member": "10 / 6 (final-L1, final-R1)",
          "cold_admin": "23 / 10 (final-L1, final-R1)",
          "after_unrelated_write": "11 / 6 admin, 10 / 6 member — 0 of 6 forced writes retired anything; the only localization / sys_metadata misses left are each key's 30 s TTL (30.4 s and 30.1 s after first read) and the first by-name read of each object",
          "opt_ins_grants_ttl_10000_count_false": "3 / 3 warm (final-L2c and final-R2c): sys_session, sys_user (better-auth fallback join), the data SELECT"
        },
        "per_leg": {
          "leg4_wave": "rounds 4 and 5 (sys_organization, sys_metadata_activation) merged into one: visible in every after-run per-query table (the two start within 1 ms, e.g. leg456-L1 admin rid 2 at 77.9 / 78.3 ms); not read in isolation from legs 5 and 6",
          "leg5": "after an unrelated write the localization pair no longer appears: leg456-L1 0 of 6 forced writes retired it (main R1: 6 of 6); remaining misses are the 30 s TTL",
          "leg6": "leg456-L1 (by-name read covered, overlay cache still on the global epoch): warm admin 11 / 6, but 12 / 7 after each forced write (sys_metadata re-read); leg4566-L1 (retired only by sys_metadata writes): 11 / 6 after forced writes too; opt-ins 4 / 4 -> 3 / 3",
          "cold": "dist-only experiment, guard removed: cold 23/10 -> 16/8 (7 queries, 2 rounds) — not landed, see open_questions"
        },
        "warm_admin_after_per_query": "r1 sys_session by token | r2 sys_user by id (better-auth join, leg A) | r3 sys_user, sys_member x2, sys_user_permission_set, sys_user_position | r4 sys_organization by slug + sys_metadata_activation | r5 data SELECT | r6 COUNT (member: same without sys_organization)",
        "wall_note": "+20 ms RTT proxy, illustrative only: warm admin DB span 207 ms main vs 169 ms after; opt-ins 111 ms vs 79 ms; cold 379 ms vs 308 ms. Not deployment evidence; no claim about cloud#1521's gate or cloud#1518's placement.",
        "controls": "resolved authz envelope (positions, permissions, systemPermissions, org_user_ids, accessible_org_ids, posture, tabPermissions, tenantId) captured by a temporary dist-only log: exactly one distinct value per persona, identical main vs after in every pair (main-R1 vs final-R1 / final-L1 / leg4566-L1; main-L2c vs final-L2c / final-R2c), normalized for per-boot random ids. Served bodies: 24/24 identical main-R1 vs final-R1 and vs leg4566-L1; across other boot pairs showcase_task's $top=1 row differs because seeded ids are random per boot under order by id asc (titles differ, keys and totals equal); sys_user_preference and sys_api_key identical in every pair."
      },
      "tests": "At head 4746b38c1 (cited in the PR body too): pnpm --filter @objectstack/core test :: exit 0 (106 files / 2580 tests); pnpm --filter @objectstack/core test:repo :: exit 0 (6 / 82); metadata-protocol vitest run :: exit 0 (248 passed, 3 skipped files; 20723 tests); rest rest-data-read-one-overlay-read + export/import-business-timezone :: 46/46; plugin-hono-server current-user-endpoints-localization :: 13/13. New pins: core resolve-authz-context.request-rounds.test.ts (5), resolve-localization-cache.test.ts (+12 cases, doubles carry the middleware seam), metadata-protocol protocol.item-read-overlay-cache.test.ts (7), meta-overlay-cache.test.ts §10 (+10). Ablations (relative-specifier source imports, no dist leg applies; each restore proven blob-equal to HEAD and git diff HEAD empty): leg 4 — base resolver restored, 4/5 red, three on the leg count alone (expected 3 to be 2) after their envelope and read assertions passed, the fourth on the recorded ledger widening; leg 5 — leg-4 file restored, 9 red incl. the in-flight write case; leg 6a — servedRead false via scripts/ablation-replace.mjs (anchor 1 -> 0, blob fb9244db -> 525bbd21), 4 red, the authoring / no-seam / TTL-0 controls green; leg 6b — epoch-keyed meta-overlay-cache.ts + protocol.ts restored, 10 red; prolongation teeth — applyRemoteMetadataMutation's bumpWriteEpoch removed via ablation-replace, 3 red. Cold guard load-bearing proof: one-shot scratch test on the memo suite's own doubles (not committed, deleted) — a revocation started before the observer existed and landing between the first request's two resolutions: guard in place -> auditor not granted (pass); guard removed via ablation-replace -> expected [ 'org_member', 'auditor', … ] to not include 'auditor' (red); restored blob dd88ea4e = HEAD.",
      "gates": "Local, derived by node scripts/pm/dispatch-gates.mjs --commands (no paths) on the final tree: 109 families, every one run with its exit code captured before any pipe, all exit 0 — the first 70 at the merge commit bc343e9b9 (check:engine-double-contract exit 1 there, fixed by node scripts/check-engine-double-contract.mjs --write recording the new suite's findOne double, re-run exit 0), the 9 the ledger commit added and the 30 the docs commit added at their heads. --ran reconciliation: '109 derived famil(ies) accounted for — 109 run, 0 NOT-MEASURED (a DERIVED zero — all 109 recorded an exit code and none of them is 3)'. Typecheck: pnpm --filter @objectstack/core typecheck :: exit 0 (tsc + examples + check:test-typecheck); pnpm --filter @objectstack/metadata-protocol typecheck :: exit 0; --listFiles counts the new/edited test files in both programs. Build: turbo run build (76/76) after merging origin/main :: exit 0. Shallow clone deepened with git fetch --shallow-since=2026-07-13 before deriving (check-plugin-teardown-shape and adr-0087 families ran exit 0). CI on 4746b38c1 at report time: 24 check-runs, in_progress except 'No other open PR may claim the same issue', 'Part-of PR must not also close its card', 'No other open PR may claim the same single-writer path' (success) and 'Packed-tarball smoke (opt-in)' (skipped); the five required contexts in_progress.",
      "line_budget": "none set. +/- per file vs merge base f4755a68a: .changeset/10757-request-rounds-core.md +10/-0; .changeset/10757-request-rounds-metadata-protocol.md +10/-0; content/docs/concepts/metadata-lifecycle.mdx +9/-7; content/docs/deployment/environment-variables.mdx +2/-2; packages/core/src/security/resolve-authz-context.request-rounds.test.ts +182/-0; packages/core/src/security/resolve-authz-context.ts +223/-71; packages/core/src/security/resolve-localization-cache.test.ts +189/-59; packages/metadata-protocol/src/meta-overlay-cache.test.ts +154/-15; packages/metadata-protocol/src/meta-overlay-cache.ts +135/-13; packages/metadata-protocol/src/protocol.datasource-delete-prolongation.test.ts +64/-29; packages/metadata-protocol/src/protocol.item-read-overlay-cache.test.ts +189/-0; packages/metadata-protocol/src/protocol.ts +46/-15; scripts/engine-double-contract.pinned.json +5/-0. Total +1218/-211.",
      "files_changed": [
        ".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"
      ],
      "deviations": [
        "Worktree: the shared primary checkout had claude/issue-10757-request-rounds checked out, so the worktree ../objectstack-issue-10757 was cut from origin/main 913fd6df8a under the temporary local branch tmp/issue-10757-request-rounds and pushed to claude/issue-10757-request-rounds (as the dispatch prescribes). The worktree and the temporary local branch are removed.",
        "File surface widened past the claim's fence, by measurement: (a) leg 6 also narrows #11967's overlay-cache retirement (list read included) from the global write epoch to sys_metadata writes plus non-write epoch reasons — after covering the by-name read, the measured run still re-read sys_metadata after every unrelated write; #11633 §4's accepted leg-D design names 'any engine-seam write to sys_metadata' as the trigger; (b) two hand-written docs pages whose text stated the old 'any engine write retires it' rule (content/docs/deployment/environment-variables.mdx, content/docs/concepts/metadata-lifecycle.mdx); (c) scripts/engine-double-contract.pinned.json, learned by the gate's own --write. Nothing in @objectstack/rest or plugin-auth is touched, and no C8 NULL-organization region is edited (grantAppliesInTenant / grantAnchorsPlatformStanding / the organization read's filter unchanged and pinned).",
        "Leg 4 parallelization widens the sys_metadata_activation name IN list by the one name admin_full_access in one case (an organization-owned admin_full_access grant that does not anchor); no verdict reads it, envelope unchanged, pinned and stated in the changeset. Recorded here because the dispatch says no predicate changes; the filter shape and the organization read are byte-identical.",
        "Write budget: git push ran 6 times to the one branch (the empty-branch probe, WIP pushes before long steps per AGENTS.md, the final push); REST writes are the 2 relay strokes. No other write.",
        "Measurement instrumentation was temporary and dist-only: driver-sql and core sources were edited, built (driver-sql with OS_SKIP_DTS=1, which removed its .d.ts until the clean rebuild) and reverted blob-equal; for core, check-dev-prereqs --stamp was hand-run after the revert so pnpm dev accepted the instrumented dist (the hand-stamp shape that script's header names as unobservable). Both dists were rebuilt clean afterwards (0 markers, .d.ts present, check-dev-prereqs freshness line clean). Nothing instrumented went through turbo or was committed.",
        "The private PostgreSQL cluster lived under /var/lib/postgresql (the postgres user cannot traverse the scratchpad); stopped and deleted. Three of my early dev servers outlived their runs (my first run script recorded the wrong process-group id); found by the ports I picked and stopped with kill on lsof -ti tcp:PORT.",
        "PR activity: not subscribed — os-dev.md's dispatch contract overrides the platform's auto-subscribe posture; CI convergence is the PM's.",
        "The first 70 derived families ran at the merge commit bc343e9b9; the two later commits change only scripts/engine-double-contract.pinned.json and content/docs, whose added families (9 + 30) ran at their heads, and check:engine-double-contract was re-run after the ledger commit. The first 70 were not re-run at 4746b38c1."
      ],
      "mcp_calls": "0 — no MCP GitHub tool was called (one ToolSearch loaded the mcp__github__issue_read schema; it was never invoked). Card, comments and PR were read through unauthenticated public REST GETs.",
      "api_writes": "2 relay strokes as objectstack-fleet[bot] via scripts/pm/with-fleet.sh --via dispatch (each a POST /repos/objectstack-ai/objectstack/dispatches): (1) pr_create — POST /repos/objectstack-ai/objectstack/pulls (draft) + POST /repos/objectstack-ai/objectstack/issues/24533/assignees [os-project-manager], read back 13403 bytes sent = stored; (2) comment — POST /repos/objectstack-ai/objectstack/issues/10757/comments (this os-dev-report). Plus git push to claude/issue-10757-request-rounds (not REST).",
      "open_questions": [
        {
          "question": "Cold leg: should the request-scoped memo's write observer be registered at boot, so the first request after boot stops resolving grants twice (measured 23/10 -> 16/8, once per process)?",
          "options": [
            "A — leave it (this PR): the guard stays, the first request pays 7 queries / 2 rounds more",
            "B — export a prime from core and call it from a boot seam (plugin-security start or objectql's engine construction); the attaching scope still stores nothing, so the memo's argument stays whole"
          ],
          "recommendation": "A. Business need: real but once per process. Long-term: B adds a cross-package boot call whose only job is a one-request saving. AI-safety: neither option can be written wrong silently, since a forgotten prime degrades to today's safe path. Startup focus: a one-time saving does not earn a new seam. Revisit only if cold-start latency is measured as a real cost on cloud."
        },
        {
          "question": "Leg 4's duplicate sys_user read (better-auth's session-to-user read and resolveUserAuthzGrants' getUserRow in the customSession call): who carries it?",
          "options": [
            "A — #24515's lane (plugin-auth): the customSession callback hands its session user's row to resolveUserAuthzGrants through a core seed added in the same PR; it still needs ai_access, which better-auth's user deliberately omits, so the seed must be the stored row or ai_access must be read there",
            "B — leave it paid"
          ],
          "recommendation": "A, scoped together with leg A's join: both are plugin-auth's reads of sys_user, and adding the core seed without its consumer would be a declared-but-unused option."
        },
        {
          "question": "Leg 6 also changed what retires #11967's overlay cache for the LIST read (sys_metadata writes plus non-write epoch reasons, not every write). Is that accepted as part of this card?",
          "options": [
            "A — keep (this PR)",
            "B — split: keep only the by-name coverage, leave the list read on the global epoch"
          ],
          "recommendation": "A. It is the trigger #11633 §4's accepted leg-D design names. Without it, the by-name cache this card needs is retired by the outbox pollers and by every data write. The metadata bridge's remote bump still retires it, pinned by the prolongation suite's ablation."
        }
      ],
      "out_of_scope_findings": [
        "carrier: #24515 · noted, not filed — the duplicate sys_user read (r2 better-auth join and r3 getUserRow on every authenticated request) needs a plugin-auth seed; dedupe words: getUserRow, customSession, sys_user duplicate, seedUserRow, ai_access",
        "carrier: none (承接者:无) · noted, not filed — the first request after boot still resolves grants twice (memo observer attached in that request); one-time 7 queries / 2 rounds; ruled load-bearing here; dedupe words: attachedHere, request-grants-memo cold, observer registration boot",
        "carrier: none (承接者:无) · noted, not filed — packages/rest/src/rest-data-read-one-overlay-read.test.ts's header says 'nothing is kept between requests'; it remains true for its seam-less engine double (the cache declines there) but no longer describes a real engine, where the served by-name read is cached with write-invalidation; Acceptance-notes material only"
      ],
      "not_measured": [
        "cloud#1521's gate and cloud#1518's placement — out of reach locally; this reading claims neither",
        "SQLite (the dev default), multi-node, concurrent load, API-key principals",
        "CI check-runs on 4746b38c1 — in_progress at report time; convergence is the PM's"
      ]
    }

    Generated by Claude Code

  13. objectstack-fleet commented on Oct 19, 2026

    @objectstack-fleet
    Contributor

    os-dev-report

    {
      "issue": 10757,
      "status": "done",
      "round": "fix round 1 (seat message 2026-10-19T13:13Z, session_01JtSQQ3EWsbNPzyYMug1tXy)",
      "branch": "claude/issue-10757-request-rounds",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/24533",
      "head": "084e79e16 (was 4746b38c1); no merge of origin/main — the PR reads mergeable: true, so there is no conflict",
      "session": "session_011Xuc8buBLe9f15TPKBjPKS",
      "premise_still_valid": true,
      "summary": "Test Core (5/6)'s red was the seat's diagnosis exactly. environment-overlay-five-types.test.ts read getMetaItem (the control, which now fills the by-name overlay cache), then stored the overlay with driver.create('sys_metadata', …), straight past the engine. No middleware saw that write, so the second read served the cached pre-overlay rows. Fixed at the cause: storeEnvironmentRow now inserts the same row shape through engine.insert under a system context. There is still no write door (saveMetaItem) and the control read still comes first. The docblock says why the row goes through the engine. No cache was disabled, no TTL changed and no assertion weakened. The record notes are carried: the docs now say the retirement happens when the write's statement completes and name the transaction window before the commit; the rest pin's 'nothing is kept between requests' header now says why that holds only on its seam-less engine.",
      "assertion": {
        "before": "pnpm exec vitest run --maxWorkers=2 src/environment-overlay-five-types.test.ts (packages/objectql, head 4746b38c1) :: exit 1 — 10 failed, both topologies x five types; 'AssertionError: expected 'Packaged' to be 'Environment overlay' // Object.is equality' (:356)",
        "after": "same command at 084e79e16 :: exit 0 — Tests 26 passed (26)"
      },
      "sweep": "git grep over *.test.ts / *.spec.ts for tests that write sys_metadata, sys_setting or sys_platform_setting straight into a driver: driver/d/entry/settingDriver/wrapped .create/.update/.delete/.execute, raw INSERT/UPDATE/DELETE SQL, knex(table), stores.get(table).set. Only environment-overlay-five-types.test.ts wrote after a read the new caches serve; it is fixed. Unaffected: (1) objectql default-record-page-save-door.test.ts — storeRow runs on a freshly booted engine BEFORE that case's first read, so the cache is empty; (2) runtime metadata-list-ambient-vs-bare-transaction.integration.test.ts — seeded through the driver before the engine exists, then read through MetadataManager's loader, not the protocol's served read; (3) metadata database-loader-21828-one-content-hash.test.ts — the metadata package's loader, not the protocol cache; (4) cli migrate/plan journal-mode, schema-migrate.host-composition and schema-migration-plugins write-guard — driver-write refusal probes with no served read after them; (5) cli secret orphans/rewrap driver-contract — sys_setting rows for the secret tooling, which never call resolveLocalizationContext; (6) service-settings settings-engine-id-fold-wins / settings-secret-rotation — the settings service's own reads, not the localization cache; (7) the cli migrate SQL suites (meta-stored-legacy-organization, preview-read-only, security-catalog-overlays, organization-ownership-apply/plan, permission-set-zero-row, security-catalog-echo-env-kernel) and metadata-protocol migrations/overlay-index — they seed before the stack boots; the one served read among them (meta-stored-legacy-organization :304) comes after the boot.",
      "tests": "Full suites, as Test Core runs them, each through scripts/pm/os-verify-lock.sh, exit captured from the command: pnpm --filter @objectstack/objectql test :: exit 0 (446 files / 8392 tests); pnpm --filter @objectstack/runtime test :: exit 0 (422 files; 6645 passed, 19 skipped); pnpm --filter @objectstack/rest test :: exit 0 (300 files; 6095 passed, 326 skipped — the suite's own conditional skips, none in a file this round edits beyond one header comment); pnpm --filter @objectstack/plugin-security test :: exit 0 (188 files; 3754 passed, 45 skipped); pnpm --filter @objectstack/service-settings test :: exit 0 (51 / 881); pnpm --filter @objectstack/plugin-hono-server test :: exit 0 (34 / 422); pnpm --filter @objectstack/mcp test :: exit 0 (40 / 479). From the first round, unchanged by this one: core test 106 / 2580 and test:repo 6 / 82, metadata-protocol 248 files / 20723 tests. Typecheck: pnpm --filter @objectstack/objectql typecheck :: exit 0; pnpm --filter @objectstack/rest typecheck :: exit 0. Build: turbo run build 76/76 (all cached) at 4746b38c1 before the fix; this round changes test files and docs only.",
      "gates": "Families this round's four paths reach (dispatch-gates --commands with those paths), plus the two families the full derivation newly lists (check-engine-split-ratio --days 90 and --self-test): 86 commands run at 084e79e16, every exit code captured before any pipe, all exit 0. Full derivation (no paths): 111 families; --ran over this round's results and the first round's (the other 25 ran at 4746b38c1 or earlier, on paths this round does not touch) gives '111 derived famil(ies) accounted for — 111 run, 0 NOT-MEASURED (a DERIVED zero — all 111 recorded an exit code and none of them is 3)'. CI on 084e79e16: just pushed, in progress; the PR reads mergeable: true.",
      "files_changed": [
        "packages/objectql/src/environment-overlay-five-types.test.ts (+16/-7)",
        "packages/rest/src/rest-data-read-one-overlay-read.test.ts (+6/-2, header comment only)",
        "content/docs/deployment/environment-variables.mdx (+1/-1)",
        "content/docs/concepts/metadata-lifecycle.mdx (+6/-3)"
      ],
      "line_budget": "none set; this round +29/-13 over 4 files (above).",
      "deviations": [
        "Record note ② asks the docs to say 'once the write's transaction commits'. Written instead to the mechanism's real point: the retirement happens when the write's STATEMENT completes. That is the commit outside a transaction. Inside an engine transaction it comes before the commit, and a read that lands between the two can keep the pre-commit rows until the next sys_metadata write or the TTL. 'Commits' would have stated a guarantee the middleware does not give. This is the settle-to-commit window of record ①, now named in both docs pages.",
        "PR body not edited, under os-dev's no-PATCH rule. Owed to the seat's body patch, together with the closing-keyword patch from the previous message: an Acceptance-notes bullet — 'The localization and overlay caches are retired when a watched write's statement completes; inside an engine transaction that precedes the commit, so a read 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).'",
        "Worktree recreated from origin/claude/issue-10757-request-rounds under the temporary local branch tmp/issue-10757-request-rounds (the primary checkout holds the branch name); removed after the push, as before."
      ],
      "mcp_calls": "0 MCP GitHub calls this round. 1 claude-code-remote send_message in the previous round, to the seat. Reads this round were public REST GETs.",
      "api_writes": "This round: 1 git push to claude/issue-10757-request-rounds (4746b38c1 -> 084e79e16), plus 1 relay stroke as objectstack-fleet[bot] (a POST /repos/objectstack-ai/objectstack/dispatches executing POST /repos/objectstack-ai/objectstack/issues/10757/comments — this report). Cumulative: 2 pr_create-and-comment strokes in round 0, this comment, and 7 pushes.",
      "open_questions": [],
      "out_of_scope_findings": []
    }

    Generated by Claude Code

  14. objectstack-fleet commented on Oct 19, 2026

    @objectstack-fleet
    Contributor

    Landed · domain:engine seat 1 · session_01JtSQQ3EWsbNPzyYMug1tXy · 2026-10-19T15:24Z

    Probe Reading
    Review CONTRACT_REVIEW_TIER record 6268067889 PASS on 4746b38c1. CI note 6268090803 (Test Core (5/6)), fixed at the cause in fix round 1. Delta note 6269310900 on 084e79e16: the record carries. ACCEPT 6270035817. Not governed.
    Queue It entered the merge queue at 2026-10-19T14:50Z and left it merged at 2026-10-19T15:21Z. No gh-readonly-queue/main/pr-24533-* branch remains.
    origin/main 5a871e4e41 is on origin/main. It carries PR #24533's 15 files, +1243 / −220.
    Card Closed completed through Fixes #10757; pm:dispatched is cleared.

    What main now holds, per authenticated GET /api/v1/data/OBJECT?$top=1, by 6255731843's method on local PostgreSQL:

    Downstream:

    The dev session session_011Xuc8buBLe9f15TPKBjPKS is archived.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions