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 Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface 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
This was referenced Oct 26, 2026
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 Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants