Skip to content

feat(rest,plugin-hono-server,cloud-connection): ETag + 304 on metadata lists, uncached item reads, runtime/config and localization; db/meta Server-Timing on every timed response - #28320

Merged
objectstack-fleet[bot] merged 8 commits into
mainfrom
claude/project-thread-dd5lh4
Oct 31, 2026
Merged

objectstack-fleet[bot] merged 8 commits into
mainfrom
claude/project-thread-dd5lh4

Conversation

@objectstack-fleet

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

Copy link
Copy Markdown
Contributor

domain:spec#1 · os-tesla · session_01L9qibwb6hBqszPTYJobitm

Requested by Tesla · project thread

Part of #27627 (Target 4: metadata caching headers + Server-Timing coverage)

Clause-②: no
Metadata-protocol: none

No contract review is owed: packages/spec/src has no non-test change in this PR.

维护者速读

  • 改了什么:Console 每次加载最先发出的几类读取(/runtime/config、/auth/me/localization、元数据类型列表、每个 /meta/:type 列表、以及 app/dashboard/doc/book 等不走缓存的单项)现在都带 ETag + Cache-Control: private, no-cache,内容没变时回 304;开了耗时头时每个响应都有 db、meta、total 三项。
  • 为什么改:这些读取原来没有任何校验头,每次加载都整包重下;耗时头缺 meta、没跑 SQL 时缺 db,看不出时间花在哪。
  • 风险与回滚:ETag 对“实际发出的内容”做哈希,权限不同的两个人拿到不同的 ETag,一个人的 ETag 不可能换到另一个人的内容;全部是 private,共享缓存不存。metadata.enableCache: false 照旧关闭 /meta 校验头。回滚即还原本 PR。
  • 先看:packages/types/src/representation-validator.ts(规则本身)和 packages/rest/src/meta-item-read-gate.ts 两条链的最后一步。
  • 两分钟验证:pnpm --filter @objectstack/rest exec vitest run src/meta-representation-etag.test.ts;或对任一 /meta/:type 带上返回的 ETag 作 If-None-Match 再请求一次,得到 304。
  • 需要你做的:无,席位落地。

What changes

On main the Console's first reads sent no validator at all, so every load downloaded them in full. Each of these now answers a weak ETag, Cache-Control: private, no-cache, and a bodiless 304 when If-None-Match names the current tag:

route owner
GET /meta, GET /meta/types (type list) packages/rest/src/meta-collection-routes.ts
GET /meta/:type (every list) meta-collection-routes.ts via the shared list chain createMetaListAnswer
GET /meta/:type/:name on the uncached exit (app, dashboard, doc, book, drafts, ?package=, member-scoped reads) meta-item-read-routes.ts via the shared item chain createMetaItemAnswer
GET /auth/me/localization packages/plugins/plugin-hono-server/src/current-user-endpoints.ts
GET /runtime/config packages/cloud-connection/src/runtime-config-plugin.ts

The validator hashes the body actually served, after every per-caller step (read gates, the app filter, the field mask, translation, the host's environment). That is the invariant getMetaItemCached already states for the cached item door. It means no per-caller fold is needed: two callers whose gates differ are served different bytes and so hold different tags, and one caller's tag can never answer 304 for another caller's body. The rule lives once in @objectstack/types (representationEtag, ifNoneMatchHits, REVALIDATE_PRIVATE_CACHE_CONTROL), the only package the three owners share. The tag is two FNV-1a/32 lanes (16 hex chars) written W/"…"; If-None-Match is compared weakly per RFC 9110 §13.1.2 (lists, *, repeated headers).

  • ⛔ No public anywhere: every body here is per caller, per host, or per locale.
  • A schema served under an undetermined field visibility (ADR-0106 D6 tier 2) keeps private, no-store and gets no tag.
  • metadata.enableCache: false (declared "Enable HTTP cache headers (ETag, Last-Modified)") switches the /meta validators off; the tier-2 no-store is still sent.
  • /runtime/config stays anonymous; its payload never depends on who asked.

Server-Timing (gate unchanged: OS_SERVER_TIMING=true, or an admin's X-OS-Debug-Timing: 1):

  • Every timed response carries db and meta beside total. A request that ran no SQL or read no metadata says so with a zero member (db;dur=0;desc="0 queries"), so "nothing spent" no longer reads like "not measured".
  • The /meta routes count each metadata read into meta (meta;dur=…;desc="N reads").
  • GET /runtime/config opens the per-request gate for an admin who sends X-OS-Debug-Timing. It resolves the caller only when that header is sent, and a resolution failure leaves the gate closed and the payload unchanged.

The bundle route is untouched (sibling PR #28287).

Step 1 reading (main 69ce01d, HotCRM, seeded admin, 58 routes)

class ETag Cache-Control 304
GET /runtime/config no none no
GET /auth/me/localization no none no
GET /meta, GET /meta/types no none no
GET /meta/:type lists, all 33 types no none no
GET /meta/:type/:name cached items (object, view, page, action, report, …) yes private, no-cache yes
GET /meta/flow/:name yes private, no-cache 200 by design (engine registration read live beside the document)
GET /meta/app/:name, /dashboard/:name, /doc/:name, /book/:name no none no

Correction to PR 1's claim: the app, dashboard, doc and book item doors carried no ETag on main. They bypass the shared item cache by design (per-caller and per-deployment gates) and took an exit that set nothing. This PR gives that exit its own body-hash validator.

Server-Timing on main: meta on 0 of 58 routes; db only when SQL ran; /runtime/config emitted nothing for an admin because nothing resolved the caller.

Branch re-measure (same HotCRM, branch head before rebase): 58 of 58 routes carry a weak ETag and private, no-cache; 57 of 58 answer 304 on a resend (the flow item answers 200, unchanged and by design); db, meta and total on all 58 for the admin header, /runtime/config included.

Tests

  • packages/types/src/representation-validator.test.ts (10): tag shape and stability, weak comparison, lists, *, arrays.
  • packages/rest/src/meta-representation-etag.test.ts (13): per list and item door, a tag is present, an unchanged body answers 304, two callers with different gates (studio.access vs none, over an unpublished app) get different tags, same-gate callers share one, a change moves it; a member presenting a builder's tag is still refused 404; enableCache: false; the tier-2 posture; both chains hash what they answer; meta marks.
  • packages/plugins/plugin-hono-server/src/server-timing-seeded-marks.test.ts: zero members on an idle request, measured members never doubled, present for an admin, absent for an unproven caller and without the gate.
  • packages/plugins/plugin-hono-server/src/current-user-endpoints-localization.test.ts: a tag with Vary: Accept-Language, 304, two users never cross-revalidate, a language change moves the tag, the signed-out answer carries none.
  • packages/cloud-connection/src/runtime-config-etag.test.ts (7): tag, 304, two hosts never cross-revalidate, no caller resolved without the timing header, gate opens for an admin only, a failed resolution leaves it closed.
  • Three existing pins changed meaning and were rewritten, not removed: the "gated reads carry no ETag" pin (meta-audience-plural.test.ts), the dashboard uncached pin (rest.test.ts), and the parity reference's non-holder posture (meta-list-projection-parity.test.ts).

Ablation: mutating the list chain to hash the pre-gate list (metaRepresentationCaching(raw, cacheControl)) turns exactly "two callers whose gate differs get different tags" red (1 failed / 12 passed); restored, blob identical to HEAD.

Verification

Final head a3d481eb (seat note): on top of 6d31fbb1 it adds only the two seat-approved doc edits (docs/OBSERVABILITY.md Server-Timing members, content/docs/api/metadata-api.mdx list validators) and a changeset round trip (types minor → back to patch, per ruling A: Metadata-protocol: none puts every package at patch). All 124 derived gates green on it; every check-run success or skipped. The readings below are on head 6d31fbb1 (main 53749673 merged in; 23 files, +991 / -34).

  • Derived gates: dispatch-gates --commands → 94 commands, every one run with </dev/null, exit codes recorded first; dispatch-gates --ran → 94 run, 0 NOT-MEASURED, 0 UNRUN, all exit 0. The first sweep caught two, both fixed in 6d31fbb1: check:route-envelope (the new send helper declared {}, and the localization / runtime-config bodies sent as call-site literals again so their bare-body counts stay where the ruled exemption pins them) and check:test-source-alias (cloud-connection aliases @objectstack/observability to source).
  • check:dual-build-cjs-loads, check:lean-entry-closure and check:dts-closure green on a full dts build (turbo run build --filter='./packages/**' --filter='@objectstack/dogfood^...') with the new @objectstack/types module in it.
  • Type check: dts build of types, rest, plugin-hono-server, cloud-connection, runtime and their closure, 35/35 tasks; tsc --noEmit on cloud-connection and plugin-hono-server after the fix.
  • Tests on the final head: rest 360 files / 7127 passed (351 skipped), cloud-connection 62 / 765, plugin-hono-server 36 / 457, types 35 / 846. runtime meta/timing suites (incl. the parity test) 31 files / 1451 before the gate fix, which touched no runtime or rest logic.
  • check-governed-merges --branch HEAD: 0 of 23 paths governed, 1025 changed lines.
  • Live: HotCRM on the workspace CLI, 58 routes, before (main) and after (branch), table above.

Acceptance notes

  • Runtime dispatcher /meta twin (packages/runtime/src/domains/meta.ts): it shares both chains, so it now answers Cache-Control: private, no-cache with no runtime code change. It sends no ETag and no 304; the parity test pins the Cache-Control agreement between the two transports.
  • Layered views (/layers paths) are untouched.
  • metadata.enableCache: false is honored for the new validators.
  • The PR 1 claim about the app/dashboard/doc/book doors is corrected above.
  • @objectstack/types gains a new export, so the changeset bumps it alongside rest, plugin-hono-server and cloud-connection.

🤖 Generated with Claude Code

https://claude.ai/code/session_01L9qibz5qPfjVU7Mns3iE1H

Generated by Claude Code

…hed item doors, runtime/config and localization answer ETag + 304 under private, no-cache; meta/db Server-Timing marks on every timed response

WIP: tests and changeset follow.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L9qibz5qPfjVU7Mns3iE1H
…llow the uncached exit's own validator

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L9qibz5qPfjVU7Mns3iE1H
…uncached item exit, localization and runtime/config; seeded db/meta timing marks; runtime/config admin timing gate

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L9qibz5qPfjVU7Mns3iE1H
…bare bodies counted at their call sites; alias observability in cloud-connection tests

The localization and runtime/config bodies are hashed before they are sent,
so they are sent as call-site literals again: the route-envelope counters
read the literal at the call site and the bodies are still built here.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L9qibz5qPfjVU7Mns3iE1H
@github-actions github-actions Bot added size/xl dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation tests tooling labels Oct 31, 2026
@github-actions

github-actions Bot commented Oct 31, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 4 package(s): @objectstack/cloud-connection, @objectstack/plugin-hono-server, @objectstack/rest, @objectstack/types, touching 24 documentable anchor(s). ⚠️ 3 changed file(s) yielded no anchor (packages/cloud-connection/package.json, packages/cloud-connection/vitest.config.ts, packages/types/src/index.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

47 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 b33946512c9f968432cb8b094961424220da4bc6.

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

What this run could not see
  • 3 changed file(s) yielded no anchor (packages/cloud-connection/package.json, packages/cloud-connection/vitest.config.ts, packages/types/src/index.ts) — pages documenting those are invisible to this run
  • 5 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 59 of 211 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 — 32 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 b33946512c9f968432cb8b094961424220da4bc6 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 1664389afc14cfe320e970b20e8fcdcce1f00333 — the merge of head a3d481eb245456bdfafe9b8f77278a66290f36d5 into base b33946512c9f968432cb8b094961424220da4bc6, 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 1664389afc14cfe320e970b20e8fcdcce1f00333 && git checkout 1664389afc14cfe320e970b20e8fcdcce1f00333
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin b33946512c9f968432cb8b094961424220da4bc6 a3d481eb245456bdfafe9b8f77278a66290f36d5 && git checkout -B drift-repro b33946512c9f968432cb8b094961424220da4bc6 && git merge --no-ff a3d481eb245456bdfafe9b8f77278a66290f36d5

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

⚠️ 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 b33946512c9f968432cb8b094961424220da4bc6 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

…; @objectstack/types minor

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L9qibz5qPfjVU7Mns3iE1H
… A: minor is earned by a metadata-protocol change alone)

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L9qibz5qPfjVU7Mns3iE1H
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#1 · os-tesla · session_01L9qibwb6hBqszPTYJobitm

维护者速读

  • 改了什么: 元数据列表接口(GET /meta、/meta/types、所有 /meta/:type)、app/dashboard/doc/book 这四类未缓存的单项读取、/runtime/config 和 /auth/me/localization 现在都会返回 ETag 和 Cache-Control: private, no-cache,请求带 If-None-Match 且内容没变时回 304,不再每次整包下载。Server-Timing 在开关打开时总是带上 db、meta、total 三项。
  • 为什么改: Seam: one request opens a record — a record-page payload (record + related counts and first rows + field access + lock) and an app-scoped metadata bundle, so a record open is 2–3 requests instead of 23–45 serialized in three stages #27627 第 4 项。云端测量里这些最先发出的请求都没有缓存头,每次都要整包下载。
  • 风险与回滚: ETag 按实际发给这个调用者的内容计算(权限、掩码、翻译之后),所以不同权限的人拿不到彼此的 304。不允许共享缓存(private),字段可见性不确定时仍是 no-store、不发 ETag。metadata.enableCache: false 可以整体关掉。Server-Timing 的开关没有改。回滚即 revert。
  • 先看: packages/types/src/representation-validator.ts(唯一一份规则)和 packages/rest/src/meta-representation-validator.ts。
  • 两分钟验证: pnpm --filter @objectstack/rest exec vitest run src/meta-representation-etag.test.ts。
  • 需要你做的: 无。不属于受管面,也没有改动 spec 源码,所以不需要契约复审,由席位核对后落地。
  • 席位意见: ACCEPT,可落地。

席位核对 · ACCEPT · domain:spec seat 1 (#6017) · 2026-10-31T05:06Z

Head a3d481eb245456bdfafe9b8f77278a66290f36d5. Card #27627 (Target 4, Part of). Claim 6538102504, corrections 6538175648, 6538251138 and 6539077865, os-dev-report 6539142744. The maintainer's rulings, verbatim: 「按 pm-dispatch 审过的pr你可以自主落地」 and 「27627 插队」.

  • Shape: draft to main, made by the relay, with Part of #27627, Clause-②: no and Metadata-protocol: none. The changeset is patch for rest, plugin-hono-server, cloud-connection and types, which ruling A requires (CI Check Changeset refused a types minor).
  • Contract review: not owed. No packages/spec/src/** non-test path moves, and Check Changeset is green.
  • Seat diff read:
    • The validator hashes the served body after every per-caller step (createMetaListAnswer step 10, createMetaItemAnswer step 6), which is the getMetaItemCached invariant.
    • D6 tier-2 keeps private, no-store with no validator.
    • enableCache: false sends none.
    • There is one rule copy, in @objectstack/types, shared by three transports.
    • An ablation (hashing the pre-gate list) reds the cross-caller test.
  • Premise: the dev's 58-route table on main 69ce01d (/mnt/project-files/spec-seat-1/r2026-10-31/out/27627-timing-step1.md). It also corrects PR 1's claim: the app, dashboard, doc and book doors had no validator.
  • Known residue, in the body: the runtime dispatcher's /meta twin sends private, no-cache with no ETag or 304, because packages/runtime is untouched. The console reads through rest, so nothing is filed.
  • Gates: every check-run on this head is success or skipped (66). mergeable_state clean. check-governed-merges --pr 28320: NOT governed, +1007/−34.
  • Landing: pr_ready + automerge_enable in this act.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 31, 2026 05:07
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 31, 2026 05:07
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 31, 2026
Merged via the queue into main with commit db44327 Oct 31, 2026
71 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-dd5lh4 branch October 31, 2026 05:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation size/xl tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant