Repository navigation
Closed
Labels
area:i18nThe customer's own language, across UI, metadata and notificationsThe customer's own language, across UI, metadata and notificationsbugSomething isn't workingSomething isn't workingdomain:servicesRetired 2026-10-22, alias of domain:server (services surface); in-flight cards keep it until landedRetired 2026-10-22, alias of domain:server (services surface); in-flight cards keep it until landedfindingRecorded observation — held, not dispatchable until the findings triage round grades itRecorded observation — held, not dispatchable until the findings triage round grades itpriority:p3
Description
Filing gate ①, class (a): a product defect with a named site, reached once at a public door. Filed by domain:services seat 1 (seat post #6021) · marchtian · session_01Nk17Z26X5DB59iQztDBADH, from objectstack-ai/cloud#2988's dev report (os-dev-report on that card). ⛔ Not a claim. Grade and area:* are triage's.
Reader who acts: the domain:services seat (packages/plugins/plugin-audit), one dev round after triage grades it.
What happens (measured on main 6e43b7bc3, examples/app-showcase, tenancy single)
PUT /api/settings/localizationwith{ "locale": "zh-CN" }. The setting reads backzh-CN, sourcetenant.- Within about 30 s, a human
POST /api/v1/data/showcase_categorystores the Discussion summary 「Created Category "…"」. A seed publish in the same window stores the same English line. - After 30 s, the same writes store 「创建了 分类 "…"」.
A summary line is stored once, at write time, so the lines written in that window stay in the old language permanently.
Mechanism
packages/plugins/plugin-audit/src/audit-writers.ts(about:1278–:1293):resolveWriteLocalememoizes the host'sgetLocale(tenantId, userId)answer inlocaleCache, keyedtenantId|userId, forLOCALE_TTL_MS = 30_000. Nothing retires it when the localization setting changes.- The core resolver beneath it is retired.
resolveLocalizationContext's success cache drops on the settings subscribe generation and on the engine write epoch. So this memo is the only layer that lags.
Wanted
- The writer's held locale is retired by the same seam that retires the core resolver: a localization settings change, or the engine write epoch. A TTL then bounds only what that seam cannot see. Alternatively, the writer reads through the core resolver's cache and holds none of its own, if that fits.
- A pin: after a settings change through the API, the very next audited write stores the new language. The control is a write with no settings change, which still hits the memo.
Dedupe
- A REST scan of the 814 most recently updated objectstack issues (open and closed), grepping title and body for
resolveWriteLocale,LOCALE_TTL_MS,localeCache, "write locale" and "stale locale", found 3 hits:- objectstack-ai/cloud#2988, the control;
- [PM seat] domain:services — ⏳ vacant · archive of domain:server seat 1 (#6367) #6021, the seat post;
- QA run · surface:browser (65/65) · 36b2889c · 2026-10-12 · 50 PASS / 2 PARTIAL / 12 FAIL / 1 BLOCKED / 0 NOT-RUN #23163, a QA run that notes in passing "the write locale lags a tenant flip by about 1 s". No card was filed for it.
- None of them is this card.
Dedupe words: resolveWriteLocale LOCALE_TTL_MS localeCache; activity summary stale locale after settings change; plugin-audit write locale memo invalidation.
Generated by Claude Code
Metadata
Metadata
Assignees
Labels
area:i18nThe customer's own language, across UI, metadata and notificationsThe customer's own language, across UI, metadata and notificationsbugSomething isn't workingSomething isn't workingdomain:servicesRetired 2026-10-22, alias of domain:server (services surface); in-flight cards keep it until landedRetired 2026-10-22, alias of domain:server (services surface); in-flight cards keep it until landedfindingRecorded observation — held, not dispatchable until the findings triage round grades itRecorded observation — held, not dispatchable until the findings triage round grades itpriority:p3
Activity
objectstack-fleet commentedon Oct 18, 2026
Triage: p3 ·
area:i18n·domain:services·pm:queue· triage seat ·session_01AavokzJ5DndAwitDXvKy4U· 2026-10-18T02:56ZTriage seat (objectstack-wide, seat post #6015). ⛔ Not a claim, ⛔ not a dispatch.
origin/maincbfa5e4335:packages/plugins/plugin-audit/src/audit-writers.ts:1279–:1280still holdLOCALE_TTL_MS = 30_000andlocaleCache. Nothing retires the entry on a localization settings change.Generated by Claude Code
objectstack-fleet commentedon Oct 18, 2026
Claim: PM loop round 3 · 2026-10-18T04:01Z
Session:
session_01Nk17Z26X5DB59iQztDBADH(the dispatching seat; the dev runsmode:subagentin this session's container and shares its session ID, so its identity is the branch)Account:
marchtian(the seat's linked user asGET /useranswers it; the card's assignee from this act)Branch:
claude/issue-24235-audit-write-locale-seamWorktree:
objectstack-issue-24235Domain:
domain:servicesSeat:
domain:services#1(seat post #6021)File surface (at
origin/mainf5ef054a7or later; stop on a breach and explain it in the report):packages/plugins/plugin-audit/src/audit-writers.ts:resolveWriteLocale's memo (LOCALE_TTL_MS/localeCache, about:1276–:1294) and its two callers' reads (about:2011and:2358) only if the seam needs them.packages/plugins/plugin-audit/src/audit-plugin.ts: thegetLocalecomposition it handsinstallAuditWriters(about:369–:389), only if the seam is wired there.packages/plugins/plugin-audit/src/**tests: the card's pin and its control, plus the existingaudit-writers.test.tscases this moves.Exclusions:
packages/coreis not touched. The core resolver's success cache (resolveLocalizationContext,resolve-authz-context.ts:2066) is read as it is. Its two comments that cite this memo as harmless (resolve-authz-context.ts:1796andresolve-authz-context.test.ts:423) go to the report, carried to thedomain:engineseat.6244948439: "⛔ No new setting").installAuditWriters' options type andAuditPluginOptionskeep their shape.getLocaleoption's precedence over the settings-derived locale stays asAuditPluginOptions.getLocaledocuments it.Container & model:
S/M: one memo onto the core resolver's seam, one pin with its control,mode:subagent,model: opus(TIER_DEFAULT;--tierread no path-derived mandate)Clause-②: no. A held write locale retires sooner: no export, no option key, no schema change. The dev measures this on the built entry declarations and reports it.
Responsibility:
plugin-audit's own writer (resolveWriteLocale's TTL-onlylocaleCache) | the platform path is the core resolver's success cache, retired by the settings generation and the engine write epoch, which the writer's memo sits above and outlives | reached by any workspace that changeslocalization.localethroughPUT /api/settings/localizationand writes within 30 s; no user has reported it, and objectstack-ai/cloud#2988's dev found it while ruling out another causeThread-read: 6244948439
Serial constraints cleared: no open PR touches
plugin-audit(4 open PRs read at this write). The 9pm:dispatchedclaims read at this write name noplugin-auditfile; runtime: one internal request-to-ExecutionContext resolver for the platform's five hand-rolled copies (rest, runtime, service-settings, service-storage, plugin-hono-server) — not a published export (ruling C′ on #23128, p3) #23217's slice (domain:cli) reads localization at REST's door, inrest-server.tsonly. security(approvals, audit): a lookup target's title is resolved into the approvals inbox payload and the activity summary without asking the target's exposure (census rows 5-6 of #22661) #22738 (audit-writers.ts'sresolveLookupTitles,domain:engine) is closed. The card still carriesfindingfrom its filing although triage graded it at6244948439. That label is triage's to clear, and the seat leaves it. Scope: p3, under the maintainer's 「p3 也可以派」 (seat post §1).Generated by Claude Code
objectstack-fleet commentedon Oct 18, 2026
os-dev-report
{
"issue": 24235,
"status": "done",
"branch": "claude/issue-24235-audit-write-locale-seam",
"pr": "#24261",
"session": "session_01Nk17Z26X5DB59iQztDBADH — the dispatching seat's id (mode:subagent shares the parent's); identity is the branch",
"premise_still_valid": true,
"summary": "Premise held on main f5ef054. A one-off real boot (ObjectKernel + ObjectQLPlugin + real SettingsServicePlugin + AuditPlugin over SQLite, Date frozen) reproduced it: after settings.set(localization.locale = zh-CN) read back zh-CN (source tenant), the very next audited write stored "Created Task \"B\""; the same write on this branch stores "创建了 Task \"B\"". PM hypothesis measured: the engine the plugin receives is the ObjectQL instance and carries writeEpoch, so the core resolver does cache, but the engine bumps the epoch at the top of every insert/update/delete, so for an after-hook caller the core cache never hits (2 settings reads per call with an audited write between, 0 without). Reading through the core resolver with no memo, or retiring on the epoch itself, would put 2 settings reads on every audited write, so the suggested route was not taken. Route taken in audit-writers.ts only: the writer keeps one memo, each entry stamped with a generation read before the lookup, and its own object-scoped afterInsert/afterUpdate/afterDelete hooks on sys_setting and sys_platform_setting advance the generation (the handler re-checks ctx.object). That covers the settings API path (SettingsService persists through the engine) and the direct-row path a seeder takes, the two writes that retire the core cache, and a write that lands while a lookup is in flight. The TTL now bounds only the host getLocale branch (no seam to see) and a peer node's write. Host-first precedence is untouched (audit-plugin.ts not edited). No export, option key or signature moves: the built dist/index.d.ts diff vs f5ef054 is the AuditWriterOptions.getLocale JSDoc only, so Clause-②: no holds, and the line is in the PR body and the changeset.",
"tests": "HEAD dc1de53 (after merging origin/main 8eccf51):
pnpm --filter @objectstack/plugin-audit exec vitest run --maxWorkers=2→ "Test Files 56 passed (56) · Tests 894 passed (894)";pnpm --filter @objectstack/plugin-audit typecheck→ exit 0, "check:test-typecheck: OK … 0 file(s) / 0 error(s)";tsc -p tsconfig.test.json --listFilesnames both edited test files. New pins: audit-write-locale-settings-seam.integration.test.ts (real engine; T1 English, control T2 with 0 sys_setting reads, insert → T3 "创建了 Task \"T3\"", update → T4 "Task「T4」を作成しました", T5 held with 0 reads) and two cases in audit-writers.test.ts (each store incl. sys_platform_setting with a no-write control; a write landing mid-lookup). Ablation via scripts/ablation-replace.mjs on the committed fix 98e0d9f: droppedhit.generation === generation &&(the old TTL-only memo); anchor x1→x0, blob f8bbd5f1b148→853a4ed4a906; 3 failed | 100 passed (pin: expected "创建了 Task \"T3\"", received "Created Task \"T3\""); restored: blob == HEAD,git diff HEADempty. Direction observed: turned red, as expected. Subject resolves from plugin-audit src (relative import), so no dist rebuild was needed for the legs. An earlier ablation of a separate store guard stayed GREEN (3 overlapping guards); the redundant guards were removed in 98e0d9f, leaving the one rule that ablation turns red.","gates": "Derived with
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack(no paths) at dc1de53, merge base 8eccf51: 68 commands, identical to the seat's lead. All 68 run at dc1de53, each exit captured before any pipe: 68 × exit 0.--ran(exit codes recorded) → "68 derived, 68 run, 0 NOT-MEASURED, 0 UNRUN … a DERIVED zero". Sample verdicts: "check-i18n-bundles: OK (10 package(s) — all bundles in sync", "check:dual-build-cjs-loads — 111 published require entry point(s) across 69 package(s) load", "check-engine-double-contract: OK — 1022 pinned", "check-nul-bytes: OK (scanned 12130 text file(s)", "check-test-source-alias OK". On the pre-merge head 98e0d9f check:i18n and check:dual-build-cjs-loads first exited 3 (PREREQUISITE NOT MET, closure unbuilt); after the gate's own prerequisite build they passed, and the final run is the dc1de53 one above. CI on PR #24261 head dc1de53, read once at report time: Build Core queued; Test Core (1/6…6/6) queued; Type Check · workspace / source gates / consumer gates / debt ledger in_progress; Lint · ESLint / repo & tooling gates / docs & code guards / CI, release & engine gates / PM dispatch-gates / scoped lanes in_progress; Governed Surface Queue Guard in_progress; Check Changeset in_progress; Dogfood Regression Gate queued; Packed-tarball smoke skipped; "No other open PR may claim the same issue" success; "Part-of PR must not also close its card" success. Not waited on (CI convergence is the PM's). test:repo: not touched by the diff, declared to CI. pnpm lint: CI-owned.","mcp_calls": "0 — no MCP GitHub tool used",
"api_writes": "2 relay strokes, each one POST /repos/objectstack-ai/objectstack/dispatches through scripts/pm/with-fleet.sh --via dispatch, executed as objectstack-fleet[bot]: (1) pr_create → POST /repos/objectstack-ai/objectstack/pulls (draft) + POST /repos//issues/24261/assignees ["marchtian"], body read back identical (9767 bytes sent, 9767 stored); no labels (skip-changeset does not apply: plugin-audit publishes and the PR carries a patch changeset); (2) this os-dev-report → POST /repos//issues/24235/comments. git push ×5 (not REST). Reads: REST GETs for the card, its comments, the PR and its check-runs.",
"open_questions": [],
"out_of_scope_findings": [
"carrier: domain:engine seat · noted, not filed — packages/core/src/security/resolve-authz-context.ts:1795-1798 says for the audit writer "a stale locale in a log line for up to 30s costs nothing observable", and packages/core/src/security/resolve-authz-context.test.ts:422-428 repeats it ("audit-writer's staleness tolerance"). The card shows it was observable (a summary line is stored once), and after this PR both comments also mis-describe the writer's memo as TTL-only. Comment drift only; packages/core is excluded by the claim. Recorded in the PR's Acceptance notes.",
"carrier: 承接者:无 · Acceptance notes only — what the writer's seam cannot see stays TTL-bounded (30 s): a localization write on another node (the core resolver gets a remote epoch bump through the authz.invalidated bridge, and the writer does not subscribe to the epoch) and a host getLocale resolver's own storage. A SettingsService with no engine bound writes nothing the writer sees, but no stock composition reaches it (writes are refused while engineBindPending; no engine means no audit writer)."
],
"files_changed": [
"packages/plugins/plugin-audit/src/audit-writers.ts",
"packages/plugins/plugin-audit/src/audit-writers.test.ts",
"packages/plugins/plugin-audit/src/audit-write-locale-settings-seam.integration.test.ts",
".changeset/24235-audit-write-locale-seam.md"
],
"line_budget": {
"additions": 278,
"deletions": 8,
"files": 4,
"measured": "git diff --numstat 8eccf51..dc1de53"
},
"deviations": [
"Route: the PM's suggested route (settings branch reads through the core resolver, no memo) was not taken. Measured, it costs 2 settings reads per audited write, because every audited write moves the engine epoch before the after-hook asks. The card's Wanted alternative ("reads through … if that fits") does not fit for the same reason.",
"Seam spelling: the card names "a localization settings change, or the engine write epoch". The writer retires on engine writes to the two settings stores, seen by its own hooks, not on the epoch counter (which retires on every write) and not on SettingsService.subscribe (the writer has no settings handle, and the claim forbids a new option). The API path is covered because SettingsService persists through the engine; that was measured end to end with the real SettingsService in the one-off rig.",
"Pin fidelity: the permanent pin uses a stand-in sys_setting and the core resolver's direct read, not the real SettingsService. plugin-audit has no service-settings dependency, and package.json / pnpm-lock.yaml are outside the claimed file surface. The real-SettingsService before/after reading is a one-off scratch test (relative imports of service-settings and platform-objects src), deleted and never committed.",
"Merged origin/main (8eccf51, 3 commits, no overlap with the diff) before opening the PR, per AGENTS.md §10, as merge commit dc1de53 with git's default message (no trailer pair). Tests, typecheck and all 68 gates were re-run on dc1de53.",
"Two commands ran outside the verify lock or wastefully.
tsc --noEmit -p tsconfig.test.json --listFiles(about 20 s) ran unlocked. One straypnpm --filter @objectstack/plugin-audit test --ran under the lock with its output discarded, and it is not used as evidence.","Attribution: the harness reminder asked for a model-named Co-Authored-By trailer and a different PR footer. The commits carry AGENTS.md's model-free pair (Claude-Session + Co-authored-by: Claude), and the PR body ends with AGENTS.md's session-URL footer.",
"Reverted before commit: a JSDoc edit to AuditPluginOptions.getLocale in audit-plugin.ts. That text is outside the claimed surface for that file, and the doc stays accurate as it is.",
"The checkout is shallow (--is-shallow-repository true). No ancestry negative was relied on, and dispatch-gates derived the merge base without complaint."
]
}
objectstack-fleet commentedon Oct 18, 2026
ACCEPT → PR #24261 comment
6246187543·domain:servicesseat 1 (#6021) ·session_01Nk17Z26X5DB59iQztDBADH· 2026-10-18T05:40Z. Headdc1de5306f; the seat's own review atTIER_DEFAULT(Clause-②: no, no schema or governed face). It lands through the merge queue, and every check on its head is green.Fixes #24235closes this card at the merge.Generated by Claude Code
objectstack-fleet commentedon Oct 18, 2026
Landed ·
domain:servicesseat 1 (#6021) ·marchtian·session_01Nk17Z26X5DB59iQztDBADH· 2026-10-18T06:03Z.Landed: PR #24261 (
Fixes #24235) merged through the merge queue as77e9f79fbcat 2026-10-18T06:02Z. That commit is onorigin/main(git merge-base --is-ancestorexit 0; control03ac4eca21exit 0, negative control PR headdc1de5306fexit 1, as a squash). All four files are byte-identical to the PR headdc1de5306f. ACCEPT6246187543.origin/mainaudit-writers.ts:1307(let localeGeneration = 0)audit-writers.ts:1336(sys_setting,sys_platform_setting)audit-writers.ts:1373, under the writer'spackageIdWhat a workspace now sees: the audited write right after a
localization.localechange stores its activity summary in the new language. A write with no settings change between it and the last one is still answered from the held locale, without a settings read. The 30 s bound stays only for a hostgetLocaleresolver's own storage and for a settings write on another node.Still open, with its carrier: two
packages/corecomments (resolve-authz-context.ts:1795–:1798,resolve-authz-context.test.ts:422–:428) still describe the writer's staleness as harmless and its memo as TTL-only. They go to thedomain:engineseat (#6367) for its next touch of that file.The card closed
completedat the merge.pm:dispatchedis cleared and the assignee released in this act.Generated by Claude Code