Skip to content

fix(runtime): GET /i18n/translations/:locale carries an ETag and Cache-Control: private, no-cache, and a matching If-None-Match is answered 304 (#25785) - #25956

Merged
objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/project-thread-icw4tr
Oct 26, 2026
Merged

objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/project-thread-icw4tr

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Elon · project thread

Part of #25785 (the first step the triage comment 6397341079 names: the validator and cache directive on the bundle). #25785 remains open for its second half, keeping metadataForms out of the boot bundle, which is a wire-shape change for domain:spec.
Clause-②: no
Metadata-protocol: none

The claim is 6407675488 (seat domain:server#2, card thread session_01HFAuCZPty5SsvseoPRgukQ). Declared surface addition: amendment 6408228005 (one scripts/check-route-envelope.mjs row, below).

What changes

GET /i18n/translations/:locale (and its ?locale= spelling) answered every console open with the whole locale bundle and no ETag or Cache-Control, so the browser could not revalidate what it already held. On a hosted tenant that is 80–88 KB compressed per open (the card's cloud#2914 reading).

  1. packages/runtime/src/domains/i18n.ts, the one handler body every transport converges on. A new translationsAnswer builds the translations answer on both of the route's success exits (the direct read and the zh → zh-CN fallback):
    • the 200 is deps.success's envelope, unchanged, plus ETag and Cache-Control: private, no-cache;
    • the ETag is weak, W/"BASE64URL-SHA256" over JSON.stringify of the exact envelope served (locale, requestedLocale and translations), computed on every read. Nothing cheaper (a counter, the bundle object's identity) stands in for the bytes, so a 304 can only ever pin a caller to the bundle it really holds. It is weak because it vouches for the JSON, not for the encoding a compressing layer puts on the wire. This is the invariant getMetaItemCached states for /meta (packages/metadata-protocol/src/protocol.ts), with SHA-256 in place of its 32-bit hash;
    • a request whose If-None-Match names that representation (RFC 9110 §13.1.2: a comma list, weak comparison, or *) is answered { status: 304, headers } with no body. The 304 carries the same two headers (§15.4.5). A malformed or stale tag is served the full bundle, the safe direction;
    • private because the bundle is served only to a signed-in caller and names the application's own objects; no-cache because the bundle changes without notice (an authored translation, a package install), so freshness is the validator's job and never a TTL.
    • The anonymous floor (ADR-0056 D2) is still the handler's first statement: an anonymous caller holding the current validator is refused 401, and no refusal carries either header.
  2. No transport change. dispatcher-plugin.ts's sendResultBase and @objectstack/hono's toResponse already write body: undefined as a body-less response (JSON.stringify(undefined) is undefined, so Hono builds Response(undefined, { status: 304 })). Both are now pinned on that.
  3. Declared surface addition (amendment 6408228005). scripts/check-route-envelope.mjs DISPATCHER_DOMAINS['i18n.ts'] goes 0 → 2 with a note: the two answers are kind 1, the same classification meta.ts took for Vary: Accept-Language (deps.success takes no headers, and a 304 has no envelope a helper could build). No other row and no gate logic moves, and DomainHandlerDeps gains no member.
  4. Changeset: @objectstack/runtime patch.

Not touched: packages/spec, every published response schema (the 200 body is byte-identical and GetTranslationsResponseSchema is unchanged), /i18n/locales, /i18n/labels/*.

The console needs no change: ui/apps/console/src/loadLanguage.ts reads the bundle through createAuthenticatedFetch, a plain fetch(input, { ...init, headers }) with the default cache mode, so the browser sends If-None-Match itself and hands the console the cached body on a 304.

Measured on a real composition

examples/app-crm booted with pnpm dev:crm -- --fresh from this branch (full pnpm build, VERDICT command-exit 0), signed in as the seeded admin, read over loopback with curl:

locale cold read read again with the issued ETag in If-None-Match
zh-CN 200, 220,391 B raw (67,303 B at gzip -9; the dev server does not compress) 304, 0 B
en 200, 213,264 B raw (58,351 B at gzip -9) 304, 0 B
  • Both answers carry Cache-Control: private, no-cache and the same ETag; the 200 keeps Vary: Origin from the CORS layer.
  • 25 cold reads of zh-CN answered one ETag, and 25 conditional reads all answered 304.
  • The ETag recomputed offline from the served zh-CN body equals the header byte for byte (W/"cOAyyUQ7yb5bulH5g7ybiAf8sbJebhwAZur_peIct_E"): the invariant, read on the real bundle.
  • Cost. The extra JSON.stringify plus SHA-256 of the 220 KB envelope is 1.8 ms per read on this 4-core shared box (stringify alone 0.9 ms; mean of 50 after a warm-up). Loopback wall time: 200 median 16.1 ms, 304 median 11.6 ms over 25 reads each. The saving the card targets is the bytes: on the ops seat's measured link (326–464 KB/s) the 80–88 KB compressed hosted bundle is about 0.2–0.27 s per warm open.
  • Before. origin/main cb0a2fcf's handler sets no header on this route (the card's measurement on pin 888d6865 shows none, and the ablation below reds every header assertion on that blob).

Verification

Commits: a373155e (the fix), e8c4b7a7 (runtime tests), 0b2e6592 (adapter test, changeset), 7b2a1a2b (test typing), 773c3f3b (the gate row). Each reading names its head.

  • New tests.
    • packages/runtime/src/domains/i18n-translations-etag.test.ts, 21 tests on the handler body: the 200 headers on both spellings and the fallback arm; the validator recomputed from the served body; stable while unchanged; moves on a one-string change and on an in-place mutation of the same bundle object; 304 for the weak tag, its strong spelling, a list with and without spaces, *, a Fetch Headers object and a repeated header in the record shape, on both spellings; full bundle for a stale, unquoted or empty tag; a validator issued before a change gets the new bundle; one locale's validator does not answer another locale; the anonymous floor answers 401 with no headers and the provider unread.
    • packages/runtime/src/i18n-translations-etag.hono.integration.test.ts, 4 tests over a real socket (LiteKernel + HonoServerPlugin + createDispatcherPlugin): 200 with both headers and the same envelope; 304 with a 0-byte body and both headers; a validator from before a bundle change is served the new bundle; anonymous with the validator is 401, no ETag.
    • packages/adapters/hono/src/hono-not-modified-response.test.ts, 3 tests: a dispatcher { status: 304, headers } leaves toResponse as a 0-byte 304 with both headers; the domain is handed the raw Request whose if-none-match it reads; the 200 control keeps its JSON body. Characterization pins: toResponse is unchanged, and they were green before the domain change.
  • Ablation (fix committed first, i18n.ts replaced on disk by its origin/main cb0a2fcf blob under a trap, restored with git checkout HEAD --): the two runtime files went 23 failed of 25. The 2 that stay green are controls that hold either way (the missing-locale 400 carries no headers; anonymous gets 401). Restored blob bb41d094 equals HEAD's, git diff HEAD empty, git status clean.
  • Package suites. At 0b2e6592 (the later commits touch only the integration test's typing, re-run green at 7b2a1a2b, and the gate row): pnpm --filter @objectstack/runtime test (vitest local project, under the verify lock, VERDICT command-exit 0): 446 files, 7,125 passed, 19 skipped. pnpm --filter @objectstack/hono test: 13 files, 228 passed. typecheck for both packages exits 0 (runtime's includes check:test-typecheck, which first caught two untyped res.json() reads in the integration test, fixed in 7b2a1a2).
  • Derived gates. node scripts/pm/dispatch-gates.mjs --commands at head 773c3f3b derives 87 families on the 6 changed paths, and all 87 exit 0 (each exit code captured before any pipe). node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran: 87 derived, 87 run, 0 NOT-MEASURED, 0 UNRUN, exit 0. The first run, at 7b2a1a2b (67 families), had three that were not green: check-plugin-teardown-shape.mjs --self-test and check:dual-build-cjs-loads answered PREREQUISITE NOT MET (exit 3: the shallow clone lacked the fixture commit 621a4876, and dist/ was unbuilt), and both exit 0 after fetching that commit and a full pnpm build; check:route-envelope was red on i18n.ts handBuilt: found 2, declared 0, which the declared row above classifies. pnpm exec eslint --no-inline-config on the five changed code files exits 0. node scripts/pm/check-governed-merges.mjs --branch HEAD: 0 of 6 paths hit the register, NOT governed, 561 changed lines.

Acceptance notes


Generated by Claude Code

…ol: private, no-cache, and a matching If-None-Match is answered 304

GET /i18n/translations/:locale (and its ?locale= spelling) served the whole
locale bundle on every console open with no validator, so the browser could
not revalidate it. The answer now carries a weak ETag over the bytes it
serves and `Cache-Control: private, no-cache`; a request whose
If-None-Match names that representation is answered 304 with no body.

Claude-Session: https://claude.ai/code/session_01HFAuCZPty5SsvseoPRgukQ
Co-authored-by: Claude <noreply@anthropic.com>
…the 304 over a real Hono socket

Claude-Session: https://claude.ai/code/session_01HFAuCZPty5SsvseoPRgukQ
Co-authored-by: Claude <noreply@anthropic.com>
…toResponse; changeset for the translations validator

Claude-Session: https://claude.ai/code/session_01HFAuCZPty5SsvseoPRgukQ
Co-authored-by: Claude <noreply@anthropic.com>
…-typecheck ratchet

Claude-Session: https://claude.ai/code/session_01HFAuCZPty5SsvseoPRgukQ
Co-authored-by: Claude <noreply@anthropic.com>
…ng answers (kind 1)

The translations 200 carries ETag and Cache-Control, which deps.success cannot
express, and the matching-If-None-Match 304 has no body. Row 0 -> 2 with its
note; no other row and no gate logic moves.

Claude-Session: https://claude.ai/code/session_01HFAuCZPty5SsvseoPRgukQ
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tests tooling labels Oct 26, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/runtime, touching 5 documentable anchor(s).

1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/permissions/system-context.mdx (via handleI18nRequest (symbol, a top-level function))
What this run could not see
  • 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 — 27 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 493a6c4a5f5b83d953f19092279abdf07b6f4017 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 493a6c4a5f5b83d953f19092279abdf07b6f4017

⚠️ 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 493a6c4a5f5b83d953f19092279abdf07b6f4017 → 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

席位核对

ACCEPT · domain:server seat 2 (#20966) · seat session session_01TFnDdepdwyDJeM2kJ4GtMy · 2026-10-26T05:59Z · head 773c3f3bc02786ba72ec530247136b0bda2e82af · card #25785 (claim 6407675488, amendment 6408228005, report 6409288196)

  1. Shape: 6 files, +558/−3. The product file is packages/runtime/src/domains/i18n.ts. The rest are three test files, one patch changeset, and the amendment's one check-route-envelope.mjs row (i18n.ts handBuilt 0 → 2, kind 1, with its note). Clause-②: no holds: no export changes, nothing under packages/spec, and the 200 body stays the same envelope. The diff adds two response headers and a body-less 304 on a matching validator. There is no published schema, no governed path (check-governed-merges --pr 25956: NOT governed), and no narrowing declaration, so no contract-tier review is owed. This is the seat's own TIER_DEFAULT review of record.
  2. Premise, measured: both answers on the translations face go through translationsAnswer. Its weak ETag is a SHA-256 of JSON.stringify over the exact success envelope served, locale and requestedLocale included, computed on every read. A 304 therefore answers only for the bytes the caller holds, whatever changed underneath. If-None-Match is compared weakly over a comma list or *, per RFC 9110 §13.1.2. A malformed member matches nothing, which serves the full bundle: the safe direction. private, no-cache keeps shared caches out and revalidates on every use. The anonymous floor still answers 401 before any validator is read, and refusals carry neither header. The dev measured app-crm zh-CN at 220,391 B → 304 with 0 B, and 23 of 25 ablations turned red.
  3. Gates: check-runs on this head: 46 total, 36 success, 10 skipped, 0 failed, 0 in progress; mergeable clean. Locally the dev ran 87/87 derived gates at exit 0.
  4. Accepted deviations: none beyond the amendment row.
  5. Out-of-scope findings: the metadataForms half has carrier domain:spec, so the card stays open (Part of). The docs-drift bot's flag on system-context.mdx row 52 was re-read: it still holds (anonymous deny), so no doc change.
  6. Landing: Tier S, not over 3000 lines. The maintainer's standing landing word for this seat thread is on record (2026-10-26 05:53Z), so it lands now through the relay.

Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 26, 2026 06:01
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 26, 2026 06:01
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 26, 2026
Merged via the queue into main with commit 69fa23d Oct 26, 2026
47 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-icw4tr branch October 26, 2026 06:42
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/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants