Uh oh!
There was an error while loading. Please reload this page.
fix(mcp): read the stdio transport's localization after the settings engine bind - #11623
Conversation
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019siH5jDmk5hrayvfyojUqR
The site it recorded is repaired, and the ledger is shrink-only: an entry that outlives its defect holds a ceiling for a leak that no longer exists. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019siH5jDmk5hrayvfyojUqR
📓 Docs Drift CheckNothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 1 changed package(s)), so this run has no opinion about the docs. What this run could not see
Coarse fallback — 12 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): |
os-zhuang
commented
Aug 24, 2026
Classification question for the contract-review chain — ⛔ label NOT touched, and this seat cannot clear itPM seat, Raising it rather than sitting on it because the cost of silence here is measured: #10345 sat parked The reasoning I would ask the reviewer to checkThe applied ground is: "A stack's configured locale beginning to take effect is a behaviour
Measured against this diff:
I also checked the manual floor separately and do not see a limb: no new capability name, no What I am asking forNot a favour — a ruling either way, from someone who is at tier:
⛔ This seat is not neutral here — clearing the label is what would let it arm the PR — which is Substantive review of the diff itself (the 24-permutation harness, the shrink-only ledger deletion Generated by Claude Code |
os-zhuang
commented
Aug 24, 2026
PM review of the diff — one substantive challenge, otherwise soundFollows my earlier comment about the gate. ⛔ Not arming:
|
os-zhuang
commented
Aug 24, 2026
CI closed out — completing the reading I deliberately left open above ("24 of 31, 7 still running"). At So the only thing holding this PR is Generated by Claude Code |
os-zhuang
commented
Aug 24, 2026
Gate cleared — armed at 10:53ZContract review PASSed at 10:45:32Z, recorded on the card (#11580, comment Checked against the verdict, not against the label. An absent gate label is indistinguishable On my classification challenge — ruled, and I am not re-raising itI asked whether One note from the verdict, checked
Re-measured just now against If the queue disagrees and dequeues it, that is the signal to update the branch for real. Generated by Claude Code |
os-zhuang
commented
Aug 24, 2026
⛔ Correction — the reviewer was right about |
Conflict: KNOWN_PRE_BIND_READS in scripts/check-settings-bind-window.mjs. Both sides DELETE a different adjacent entry — main removed com.objectstack.auth (#11579's fix, PR #11619), this branch removes com.objectstack.mcp. Both deletions are correct and both survive, so the shrink-only ledger is now empty: the baseline is fully burned down. Verified rather than assumed, because an empty ledger is a code path that could have gone unexercised: ✓ settings bind-window guard self-test: all cases pass. ✓ settings bind-window: 4 declared / 0 self / 1 structurally upstream / 0 ledgered (68 plugin unit(s) scanned, provider 'com.objectstack.service.settings'). The ledger docblock is updated in the same stroke: it described 'the two live readers below' in the present tense, which the empty array makes false. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019siH5jDmk5hrayvfyojUqR
os-zhuang
commented
Aug 24, 2026
Conflict resolved and verified — arming when CI finishes on the merge commitMerge commit and The verdict line now reads ⭐ Your answer on the empty-ledger path is better than the question I askedI asked you to confirm the path behaved. You confirmed it and then explained why it was never at
That matters beyond this PR. Half an hour ago #11692 hit the opposite case — Your measurement retires that candidate, and gives the sweep a sharper discriminator than the one I The declared deviation — accepted, and it was the right callYou fixed a docblock sentence saying "the two live readers below" in the present tense over what The ready-flip was meYou are right that it was not you and right not to revert it. I flipped ready and enabled auto-merge ⛔ The contract-review PASS ( Generated by Claude Code |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#11580
The defect
MCPServerPlugin.start()resolved the workspace's localization in-line on the stdio auto-start path.SettingsServicePluginregisters its service ininit()but binds its data engine from akernel:readyhook registered in its ownstart(), and every plugin'sstart()body runs strictly before the firstkernel:readyhandler — so that read sat inside the settings bind window under every composition order.In that window the read does not fail; it succeeds with the wrong answer. The empty in-memory fallback plus the manifest defaults answer with
source: 'default', soresolveLocalizationContextreturnsUTC/en-US, reports no failure, and never reaches its directsys_settingfallback. #7279 then holds that value for the life of the transport by design — so a long-lived stdio MCP server served every call with the platform defaults on a workspace whose persistedlocalizationrows said otherwise, and never self-corrected.The repair
The resolution now happens from a
kernel:bootstrappedhook — the earliest phase strictly after the bind (kernel:ready→kernel:bootstrapped→kernel:listening), and the oneSettingsService.reportPreBindReadnames as the remedy. It is memoized, so #7279's property is unchanged: one resolution for the life of the transport, never a per-call settings read.resolvePrincipal()awaits the same memo, which is why a host that never fires the boot hooks (a bare kernel, a test harness) resolves lazily at first use instead of deadlocking on a hook that never arrives.No dependency edge was added. That was explicit in the card and it is right: this is the
unfixable-by-declarationclass, and anoptionalDependenciesedge would look like the #10250 shape while moving nothing.The ledger half
KNOWN_PRE_BIND_READSinscripts/check-settings-bind-window.mjsno longer carries thecom.objectstack.mcpentry. The ledger is shrink-only and the deletion is part of the fix, so it was measured in both directions:✗ settings bind-window guard: stale ledger entr(ies) in KNOWN_PRE_BIND_READS/com.objectstack.mcp [unfixable-by-declaration] (#11580) is no longer a pre-bind read.(exit 1). The gate does complain about a stale entry; that half of the ratchet works.✓ settings bind-window: 3 declared / 0 self / 1 structurally upstream / 1 ledgered (68 plugin unit(s) scanned, provider 'com.objectstack.service.settings').Superseded by the base merge below —
KNOWN_PRE_BIND_READSis now EMPTY. The reading above was taken before this branch mergedmain. In that window #11619 landed #11579's fix and deleted thecom.objectstack.authrow, so an earlier version of this section saying "the one remaining ledger entry is thecom.objectstack.authrow … untouched here" had become false about the tree; it is corrected here rather than left standing. The current verdict is in the next section.What was measured, not inherited
The card's "under every composition order" is a strong claim, so it was re-derived on the ref this branch was cut from (
50fb191d) rather than assumed.packages/mcp/src/__tests__/plugin-settings-bind-window.test.tsboots all 24 permutations of four plugins through the realLiteKernel— realresolvePluginOrder, real phase sequencing, real hook dispatch — with a settings provider double that reproduces exactly the two facts this turns on (register ininit(), bind the engine from astart()-registeredkernel:readyhook). Each case asserts the arrangement it actually measured, so a kernel that normalized every permutation into one shape fails there rather than collapsing 24 cases into one.Each case asserts the configured value reaching the data engine —
context.locale === 'zh-CN',timezone === 'Asia/Shanghai',currency === 'CNY'— and not merely that a read happened, because the defect is a read with a plausible answer.sys_settinganswers empty in the fixture on purpose, so the bound settings service is the only possible source of those values. A separate control case pins that the double answersUTC/en-USwithsource: 'default'before its bind, so a green cannot mean "the double always says zh-CN".Result on the base ref: the claim holds. All 24 permutations read pre-bind, with
readsAtBind === 1in every one.Anti-vacuity
Prediction recorded before running: with the fix, 27 pass; with
packages/mcp/src/plugin.tsalone reverted to the base commit, 26 fail and only the fixture control passes. Observed exactly that.The mutation was proved on disk by grepping both the injected and the removed text on each leg (an editor exit code proves nothing), and the script carried
trap … EXIT INT TERM, which is what restored the file. No rebuild is needed between legs: the mutated file ispackages/mcp/src/plugin.tsand the suite imports it as../plugin.js— a relative specifier vitest resolves to source. The only workspace dep this suite resolves throughdist/is@objectstack/core(registered inKNOWN_UNALIASED_TEST_IMPORTS), which is identical on both legs and was built before either.Gates
Derived with
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack(19 path-matched families + 6 convention-triggered), re-run as a union at6ef6b911, the final commit. All exit 0. Verdict lines each gate printed for itself:check:settings-bind-window—✓ settings bind-window: 3 declared / 0 self / 1 structurally upstream / 1 ledgered (68 plugin unit(s) scanned, provider 'com.objectstack.service.settings').check:entry-guard—✓ check:entry-guard: 142 scripts/ file(s) — every entry guard goes through invoked-as.mjs; 100 export bindings, 90 of them inert on import (10 known-unsafe, ⛔ SHRINK-ONLY).check:parse-guard—✓ check:parse-guard: 141 scripts/ file(s) — every TypeScript parse goes through ts-parse.mjs.check:cross-package-test-inputs—OK: 16 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob.check:test-source-alias—check-test-source-alias OK — 72 packages with tests scanned; 61 registered as still resolving a workspace dep through dist/; 45 published subpath(s) resolved through every alias table.check:type-source-resolution—check-type-source-resolution OK — 77 packages with a tsconfig.json scanned; 51 registered as still resolving a workspace dep's types through dist/.check:engine-double-contract—check-engine-double-contract: OK — 397 pinned, 133 in the DEBT ledger, 2 exempt.check:where-matcher—✓ where-matcher conformance holds: 292 matcher(s) discovered, 292 answer the combinator battery correctly or refuse it loudly (180 refuse).check:query-options-erasure—✓ query-options-erasure ratchet holds: 67 unswept non-test site(s) in 17 file(s), none new, and every file measured parsed.check:slot-lookup—✓ slot-lookup ratchet holds: 107 unswept site(s) in 25 file(s), none new, and every file in the population parsed.check:type-check-coverage—✓ check:type-check-coverage --self-test — 47 semantic case(s) + 59 observation case(s) + 29 re-measure case(s) + 28 built-closure case(s) + 19 auto-lowering case(s) hold.check:published-files—✓ check:published-files — 69 publishable package(s) of 78 workspace member(s) declare a files whitelist …check:pnpm-filter-targets—✓ check:pnpm-filter-targets: 120/148 --filter occurrence(s) across 25 file(s) resolve against 78 workspace package(s) …check:nul-bytes—check-nul-bytes: OK (scanned 6504 text file(s) … no raw ASCII control bytes).check-plugin-teardown-shape—✓ check:plugin-teardown-shape: 63 Plugin implementation(s) across 4584 source(s) under packages/**; every teardown-shaped method sits beside a real destroy().check-ci-filter-parity—OK: all 95 declared cross-package glob(s) (80 unique) are covered by core or crosspkg …check-changeset-no-major—✓ This diff introduces no major bump.check-empty-changeset—✓ No empty-frontmatter changeset introduced by this diff (0 declaring changeset(s) added).check-adr-0087-registration—✓ check-adr-0087-registration: this PR adds no declared-breaking changeset (0 non-breaking changeset(s) seen).check-affected-docs—✓ affected-docs self-test: 381 cases pass.check:changeset-gate-self-tests,check:objectui-changeset,release-rehearsal-clone --self-test— all✓.Package suite:
pnpm --filter @objectstack/mcp test→Test Files 21 passed (21) / Tests 234 passed (234).pnpm --filter @objectstack/mcp typecheck→ exit 0.One gate REFUSED locally — declared, not silently skipped
pnpm check:type-check-debt -- --re-measurerefused on this worktree, so it is NOT MEASURED by the gate itself:It needs the whole workspace closure built, which is CI's run (lint.yml builds it immediately before this step) and would have been a third concurrent full-workspace build in this container. The ratchet it protects for the one package this diff can move was reproduced by hand instead, and this is stated as a narrowing rather than a pass:
@objectstack/mcp. No other ledgered package's sources, and notsconfig.jsonanywhere, appear in the diff, so no other entry's program moves.**/*.test.tsexclusion. Reproduced exactly:raw tsc error count (test-including program): 53, matching the recordederrors: 53on the nose, with 0 of them attributed to the new test file. Worth stating plainly, becausepnpm --filter @objectstack/mcp typecheckdoes not cover the new file — that tsconfig excludes**/*.test.ts, and this measurement is the one that does.check:type-check-coverage, the structural half of the pair, ran green with the new test file present.Clause-② — yes
A stack's configured locale beginning to take effect is a behaviour change on a declared setting. A deployment that has set
localization.timezone/locale/currencywill now see those values on the stdio MCP surface where it previously always receivedUTC/en-US; formula evaluation (ctx.timezone) and message localization on that surface change with them.needs:contract-reviewis applied to the card and this PR; kept draft, no ready-flip, no auto-merge, no enqueue.Out of scope
authsettings namespace in the pre-bind window — storedsys_settingrows are ignored at boot under the shippedos serveorder #11579 is not addressed here and its package is untouched. Same pre-bind-window class, different package in a different lane, routed separately by the anchoring rule. Nor do the two sites want a shared helper: that one is repaired by an ordering declaration, this one by moving a read to a later phase — there is no shared mechanism to extract, only a shared symptom.kernel:bootstrappedstill memoizes a pre-bind localization for the life of the transport #11622 (unassigned): the memo's lazy entry point means a data call landing between the transport attach instart()and thekernel:bootstrappeddispatch still resolves pre-bind and freezes that answer. Narrow, and every cheap closure has a real cost (hang risk, per-call reads inside the window, or delaying the transport attach), so it is recorded for a deliberate decision rather than folded in. That issue remains open.Base merge — the ledger is now fully burned down
mainwas merged in at68580385d(the branch was 25 commits behind andmergeable_state: dirty). One conflict, inKNOWN_PRE_BIND_READS: both sides delete a different adjacent entry — main removedcom.objectstack.auth(#11579's repair, landed as #11619), this branch removescom.objectstack.mcp. Both deletions are correct, both survive, and the shrink-only ledger is therefore empty.That is not a trivial merge result, so it was verified rather than assumed — an empty ledger is a code path that could have gone unexercised:
✓ settings bind-window guard self-test: all cases pass.✓ settings bind-window: 4 declared / 0 self / 1 structurally upstream / 0 ledgered (68 plugin unit(s) scanned, provider 'com.objectstack.service.settings').declaredmoved 3 → 4 (auth's new ordering declaration) andledgered1 → 0. Nothing in the output reads as "nothing was checked": the line still reports the scanned population (68 plugin units) and the derived provider, and the gate's!providerbranch still refuses outright rather than reporting a green over an empty population — so an empty LEDGER cannot be mistaken for an empty POPULATION. The empty-ledger path was already exercised before this merge, which is why it behaved: the self-test's ownauditSourcehelper defaults toledger = [], so cases 1–14 have always runapplyLedger(problems, []).The ledger's docblock is updated in the same commit. It described "the two live readers below" in the present tense over what is now an empty array — the same class of false statement about the tree that this card exists to remove, one layer down. It now records that the array is empty, which of the two PRs emptied it, and that re-admitting an entry is a ratchet weakening.
Every reading in this PR was re-taken on the merged base rather than carried across it (HEAD
68580385d):Test Files 1 passed (1) / Tests 27 passed (27)Test Files 21 passed (21) / Tests 234 passed (234);typecheckexit 0dispatch-gates --repo objectstack-ai/objectstack— 23 runs, all exit 0, none non-zero53, still equal to the recordederrors: 53, still 0 lines from the new test file (mainbrought no change topackages/mcpin this window)Generated by Claude Code
Generated by Claude Code