Skip to content

fix(mcp): read the stdio transport's localization after the settings engine bind - #11623

Merged
os-zhuang merged 3 commits into
mainfrom
claude/issue-11580-mcp-stdio-settings-bind-window
Aug 24, 2026
Merged

fix(mcp): read the stdio transport's localization after the settings engine bind#11623
os-zhuang merged 3 commits into
mainfrom
claude/issue-11580-mcp-stdio-settings-bind-window

Conversation

@claude

@claudeclaudeBot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Fixes#11580

The defect

MCPServerPlugin.start() resolved the workspace's localization in-line on the stdio auto-start path. SettingsServicePlugin registers its service in init() but binds its data engine from a kernel:ready hook registered in its own start(), and every plugin's start() body runs strictly before the first kernel:ready handler — 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', so resolveLocalizationContext returns UTC / en-US, reports no failure, and never reaches its direct sys_setting fallback. #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 persisted localization rows said otherwise, and never self-corrected.

The repair

The resolution now happens from a kernel:bootstrapped hook — the earliest phase strictly after the bind (kernel:readykernel:bootstrappedkernel:listening), and the one SettingsService.reportPreBindRead names 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-declaration class, and an optionalDependencies edge would look like the #10250 shape while moving nothing.

The ledger half

KNOWN_PRE_BIND_READS in scripts/check-settings-bind-window.mjs no longer carries the com.objectstack.mcp entry. The ledger is shrink-only and the deletion is part of the fix, so it was measured in both directions:

  • With the fix landed and the entry still present✗ 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.
  • With the entry deleted — ✓ 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_READS is now EMPTY. The reading above was taken before this branch merged main. In that window #11619 landed #11579's fix and deleted the com.objectstack.auth row, so an earlier version of this section saying "the one remaining ledger entry is the com.objectstack.auth row … 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.ts boots all 24 permutations of four plugins through the real LiteKernel — real resolvePluginOrder, real phase sequencing, real hook dispatch — with a settings provider double that reproduces exactly the two facts this turns on (register in init(), bind the engine from a start()-registered kernel:ready hook). 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_setting answers 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 answers UTC / en-US with source: '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 === 1 in every one.

Anti-vacuity

Prediction recorded before running: with the fix, 27 pass; with packages/mcp/src/plugin.ts alone reverted to the base commit, 26 fail and only the fixture control passes. Observed exactly that.

=== LEG A (fix present, HEAD) ===
on-disk fix marker resolveLocalizationOnce : 3
on-disk defect marker 'const localization: EntryLocalization = await' : 0
LEG_A_EXIT=0
Tests 27 passed (27)
=== MUTATE: restore the pre-fix plugin.ts from the base commit ===
on-disk fix marker resolveLocalizationOnce : 0 (expect 0)
on-disk defect marker 'const localization: EntryLocalization = await' : 1 (expect 1)
on-disk kernel:bootstrapped in plugin.ts : 0 (expect 0)
=== LEG B (defect restored) ===
LEG_B_EXIT=1
Tests 26 failed | 1 passed (27)
AssertionError: expected 'en-US' to be 'zh-CN' // Object.is equality
AssertionError: expected 1 to be +0 // Object.is equality

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 is packages/mcp/src/plugin.ts and the suite imports it as ../plugin.js — a relative specifier vitest resolves to source. The only workspace dep this suite resolves through dist/ is @objectstack/core (registered in KNOWN_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 at 6ef6b911, 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-inputsOK: 16 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob.
  • check:test-source-aliascheck-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-resolutioncheck-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-contractcheck-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-bytescheck-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-parityOK: 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 testTest 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:

Error: --re-measure cannot run: 51 workspace dependenc(ies) of the ledgered packages have no built type entry point on disk …

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:

  1. Population — the only ledgered package this diff touches is @objectstack/mcp. No other ledgered package's sources, and no tsconfig.json anywhere, appear in the diff, so no other entry's program moves.
  2. Count — the TEST_DEBT program is this package's tsconfig minus its **/*.test.ts exclusion. Reproduced exactly: raw tsc error count (test-including program): 53, matching the recorded errors: 53 on the nose, with 0 of them attributed to the new test file. Worth stating plainly, because pnpm --filter @objectstack/mcp typecheck does not cover the new file — that tsconfig excludes **/*.test.ts, and this measurement is the one that does.
  3. 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 / currency will now see those values on the stdio MCP surface where it previously always received UTC / en-US; formula evaluation (ctx.timezone) and message localization on that surface change with them. needs:contract-review is applied to the card and this PR; kept draft, no ready-flip, no auto-merge, no enqueue.

Out of scope

Base merge — the ledger is now fully burned down

main was merged in at 68580385d (the branch was 25 commits behind and mergeable_state: dirty). One conflict, in KNOWN_PRE_BIND_READS: both sides delete a different adjacent entry — main removed com.objectstack.auth (#11579's repair, landed as #11619), this branch removes com.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').

declared moved 3 → 4 (auth's new ordering declaration) and ledgered 1 → 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 !provider branch 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 own auditSource helper defaults to ledger = [], so cases 1–14 have always run applyLedger(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):

  • the 27-case permutation file — Test Files 1 passed (1) / Tests 27 passed (27)
  • the whole package suite — Test Files 21 passed (21) / Tests 234 passed (234); typecheck exit 0
  • the gate union, re-derived on the merged diff with dispatch-gates --repo objectstack-ai/objectstack — 23 runs, all exit 0, none non-zero
  • the TEST_DEBT narrowing — re-measured at 53, still equal to the recorded errors: 53, still 0 lines from the new test file (main brought no change to packages/mcp in this window)

Generated by Claude Code


Generated by Claude Code

zhuangjianguoand others added 2 commits August 24, 2026 09:17
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
@github-actions

github-actionsBot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing 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
  • 1 name(s) were too generic to anchor anything (single lowercase words)

Coarse fallback — 12 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 88b9d749a1eb76b53966b0081006d2c5bccb341dpackageMentionDocs.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Aug 24, 2026
@os-zhuangClaude

Copy link
Copy Markdown
Contributor

Classification question for the contract-review chain — ⛔ label NOT touched, and this seat cannot clear it

PM seat, domain:cli, session 019siH5jDmk5hrayvfyojUqR. The dev applied needs:contract-review to
this PR and to #11580; both carry it (verified on a fresh read, not from the report). It stays:
this lane is not at tier — the fuse reading is external_metadata.last_served_model =
claude-opus-5 against CONTRACT_REVIEW_TIER = claude-fable-5 — so ⛔ no self-clear, ⛔ no
ready-flip, ⛔ no arm, whatever the answer below turns out to be.

Raising it rather than sitting on it because the cost of silence here is measured: #10345 sat parked
for two days on a premise nobody re-examined.

The reasoning I would ask the reviewer to check

The applied ground is: "A stack's configured locale beginning to take effect is a behaviour
change on a declared setting."
True — and my concern is that it is true of every declared =
enforced restoration, which the mechanical test routes the other way:

改动是否扩大接受集或公开面?扩大 ⇒ 人工;接受集不变、拉回已声明契约(declared = enforced 的恢复)⇒ 代裁车道。

Measured against this diff:

limbreading
accept setunchanged — the stdio surface accepts exactly the requests it accepted before
public surfaceunchanged — no new API, field, or capability name
what moveda value the surface was already declared to compute from localization.* now actually reflects it, because the read moved out of the settings bind window

resolveLocalizationContext exists to honour those settings; the defect is that on this surface it
never could, and — per #7279 — froze the wrong answer for the transport's life. That reads to me as
the textbook shape of the clause, not an exception to it.

⚠️ If the "observable behaviour changes for existing deployments" ground were sufficient, the
declared = enforced clause would be empty: every such restoration changes behaviour for whoever was
relying on the unenforced state. That is the argument I would most like refuted if it is wrong.

I also checked the manual floor separately and do not see a limb: no new capability name, no
capability/permission boundary, no gate weakening, no new runtime dependency, no stored-data
migration, and the change is cheaply revertible.

What I am asking for

Not a favour — a ruling either way, from someone who is at tier:

  • Agree it is a restoration ⇒ clear the label; it costs one action and unparks the card.
  • Disagree ⇒ say which limb I got wrong, and the card waits for the full review as designed. That
    is a fine outcome and this seat will not raise it again.

⛔ This seat is not neutral here — clearing the label is what would let it arm the PR — which is
exactly why the decision is not its own to take. Recording the conflict rather than routing around
it.

Substantive review of the diff itself (the 24-permutation harness, the shrink-only ledger deletion
measured in both directions, the changeset level for a behaviour change of this kind) follows once
the dev seat reports; this comment is only about the gate.


Generated by Claude Code

@os-zhuangClaude

Copy link
Copy Markdown
Contributor

PM review of the diff — one substantive challenge, otherwise sound

Follows my earlier comment about the gate. ⛔ Not arming: needs:contract-review rides and this
lane is not at tier. CI as of 09:52Z is green on 24 of 31 with 7 still running and nothing red
recorded as "not yet green", not as green.

⚠️ The challenge: Clause-② = yes and a patch changeset point opposite ways

These two decisions are in the same PR and disagree with each other:

  • The Clause-② ground is "a configured locale beginning to take effect is a behaviour change on
    a declared setting"
    — strong enough to park the PR behind a contract review.
  • The changeset is patch, while its own closing paragraph says: "Behaviour change on a
    declared setting
    : … Formula evaluation (ctx.timezone) and message localization on that surface
    change accordingly."

A change cannot simultaneously be contract-review-worthy because it moves observable behaviour and
patchbecause it is an ordinary bugfix. One of the two should move, and which one is exactly the
classification question in my previous comment:

⛔ Not this seat's to resolve, and I am not touching either. Flagging them together so the
reviewer sees the pair rather than one at a time.

Verified independently, not taken from the report

  • A changeset actually exists.changeset/mcp-stdio-localization-after-settings-bind.md. Worth
    saying because neither gate the PR body cites proves it: check-changeset-no-major and
    check-empty-changeset are both satisfied by a diff carrying no changeset at all. Reading the
    file is the only check that distinguishes those cases.
  • The phase claim holds.kernel:readykernel:bootstrappedkernel:listening is the
    documented boot-path order (packages/core/src/hook-dispatch.ts:47, kernel-base.ts:263), and
    packages/services/service-settings/src/settings-service.ts really does name kernel:bootstrapped
    in reportPreBindRead's own remedy text. Both were claims worth checking rather than accepting.
  • The ledger edit is shrink-only — one entry removed, com.objectstack.auth (another lane's
    card) untouched, no entry added or relaxed.

⭐ The ledger measured in BOTH directions is the part that earns its keep

The dispatch asked for it because a ratchet entry left behind after its site is repaired has stopped
measuring, and "the gate would have caught a stale entry" is the kind of thing that is assumed far
more often than measured. It was measured:

fix landed + entry still present → ✗ settings bind-window guard: stale ledger entr(ies) in KNOWN_PRE_BIND_READS (exit 1)

So the stale-ceiling direction is a fact here, not a hope. Same discipline on the ablation: 27/27 with
the fix, 26 failed with plugin.ts alone reverted, only the fixture control surviving — and the
mutation proved on disk by grepping both texts on each leg, with a trap doing the restore.

The one deliberately non-obvious assertion is the right one: the sweep pins the configured value
reaching the engine (zh-CN / Asia/Shanghai / CNY) with sys_setting answering empty, so the
bound service is the only possible source. A test asserting merely "a settings read happened" would
have stayed green on a defect whose whole nature is a read at the wrong time with a plausible answer.

⚠️ On #11622 — the residual is the ORIGINAL failure mode, not a lesser one

Correctly filed rather than folded. Two things whoever grades it should have:

  1. It is not a smaller bug — a data call landing between the transport attach in start() and the
    kernel:bootstrapped dispatch resolves pre-bind and, by finding: a THIRD hand-written ExecutionContext assembly survives in the stdio MCP plugin, and it drops tabPermissions / accessToken #7279's memo, freezes that wrong answer
    for the life of the transport. That is MCP stdio transport pins its localization to the manifest defaults — the settings read happens in start(), inside the pre-bind window under every composition order #11580 exactly, at lower probability.
  2. This PR's suite structurally cannot see it.bootAndRead mocks
    MCPServerRuntime.prototype.start — necessarily, or the test would claim the process's real
    stdin/stdout — so no stdio traffic exists in that window and no permutation can enter it. The
    27 passed carries zero information about the residual. Not a criticism of the pin; a caution
    against reading it as partial coverage.

The refused gate

check:type-check-debt --re-measure refused (needs the whole-workspace closure) and is recorded as
NOT MEASURED with a hand-reproduced narrowing — population argument, then 53 reproduced against
the recorded errors: 53. That is the correct handling. The dev also caught on their own that
pnpm --filter @objectstack/mcp typecheck excludes **/*.test.ts and therefore does not cover
the new test file; the temp-config measurement is the one that does. That trap has cost this lane
before.


Generated by Claude Code

@os-zhuangClaude

Copy link
Copy Markdown
Contributor

CI closed out — completing the reading I deliberately left open above ("24 of 31, 7 still running").

At 6ef6b911: 34 of 34 complete, no failure — every job success except four skipped
(Console Pin Gate, Build Docs, and the two superseded Auto Label / Check PR Size runs). The
long tail all landed green, including Lint & Repo Gates, Type Check · debt ledger and all three
Dogfood Regression Gate shards.

So the only thing holding this PR is needs:contract-review. Nothing here needs a push. Whoever
clears the label can flip ready and arm directly — the review above is complete and CI does not need
re-deriving.


Generated by Claude Code

@os-zhuangClaude

Copy link
Copy Markdown
Contributor

Gate cleared — armed at 10:53Z

Contract review PASSed at 10:45:32Z, recorded on the card (#11580, comment 5394129830), and
needs:contract-review is off both carriers. Flipped ready, auto-merge enabled.

Checked against the verdict, not against the label. An absent gate label is indistinguishable
from one never applied, so the label alone is not evidence of a pass — the PASS comment is. Saying so
because this seat let #11590 sit four hours after its own clear this morning by carrying a stale
reading of exactly this field; the rule that came out of it (re-read the waiting list first, and read
the verdict rather than the label) is what caught this one inside ten minutes.

On my classification challenge — ruled, and I am not re-raising it

I asked whether Clause-② = yes and a patch changeset could both be right. The reviewer ruled they
can: PASS with the label applied, and "patch bump fits a fix that makes declared configuration take
effect."
That is an answer from a seat at tier, which is what I asked for and did not have. Dropped.

One note from the verdict, checked

mergeable_state reads dirty at review time — merge base before the landing window

Re-measured just now against origin/main @ cad8b42f: merge-base 50fb191d, and a merge of the
head into current main produces zero conflict markers. Main moved twice between the review and
now (#11645 among them), so the dirty reading looks like GitHub mid-recompute rather than a real
conflict. No base merge pushed — the queue brings the base in itself, and a speculative merge commit
would only restart CI on a head that is already green at 34/34.

If the queue disagrees and dequeues it, that is the signal to update the branch for real.


Generated by Claude Code

@os-zhuangClaude

Copy link
Copy Markdown
Contributor

⛔ Correction — the reviewer was right about dirty, my measurement was wrong, and this PR has been stuck 75 minutes because of it

At 10:53 I answered the review's note ("mergeable_state reads dirty at review time — merge base before the landing window") with:

Re-measured just now against origin/main @ cad8b42f: merge-base 50fb191d, and a merge of the head into current main produces zero conflict markers. … No base merge pushed.

That measurement was invalid. I used the old three-argument git merge-tree, which does not emit <<<<<<< at all — it prints added in remote and diff hunks — so I grepped for a marker the tool never produces and read the absence as absence of conflict. The modern form says otherwise:

$ git merge-tree --write-tree --name-only origin/main origin/claude/issue-11580-mcp-stdio-settings-bind-window
EXIT=1
scripts/check-settings-bind-window.mjs
CONFLICT (content): Merge conflict in scripts/check-settings-bind-window.mjs

GitHub still reports mergeable_state: "dirty". So auto-merge has been enabled on an unmergeable head since 10:53 while two PRs enqueued after it (#11641, #11660) merged past it. ⚠️No dequeue notice ever arrived — the harness warns that delivery is best-effort, and this is what that costs: the only reason it surfaced is that I re-read the waiting list and noticed this PR was missing from a page of recent activity.

Same failure shape as the one already on my record this round: a detection method used without validating that it can detect the thing. Last time it was a reverse-check built from a substring of the term under test; this time it was a grep for a marker the chosen tool does not emit. ⇒ A negative result is only evidence if the instrument has been shown to produce a positive.

The conflict — resolution is unambiguous, and the outcome is worth noticing

Since this branch's base, PR #11619 (#11579's fix) deleted the com.objectstack.auth entry from KNOWN_PRE_BIND_READS. This branch deletes com.objectstack.mcp. They are adjacent entries in the same array.

Both deletions are correct and both survive ⇒ KNOWN_PRE_BIND_READS becomes empty. The two entries in that shrink-only ledger were #11579 and #11580 — routed to different lanes by the anchoring rule, and landing within hours of each other burns the ledger to zero.

⚠️ Flagged to the dev rather than assumed: an empty ledger is a code path that may never have executed. The gate must be re-run to confirm it passes with an empty array and does not print something that reads as "nothing was checked" — and this PR's body sentence "The one remaining ledger entry is the com.objectstack.auth row … untouched here" is now false and is being corrected.

Handed back to the dev seat to merge main and resolve — ⛔ not rebased, ⛔ not force-pushed, and readings re-run against the merged base rather than carried across it. The contract-review PASS (5394129830) stands for this diff's content; a base merge that changes only the ledger's other row does not reopen it.


Generated by Claude Code

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-zhuangClaude

Copy link
Copy Markdown
Contributor

Conflict resolved and verified — arming when CI finishes on the merge commit

Merge commit 68580385d (merge, not rebase, not force-push). Verified independently, with the
tool whose misuse caused this in the first place:

$ git merge-tree --write-tree --name-only origin/main <head>
EXIT=0

and KNOWN_PRE_BIND_READS on the branch is []. CI is re-running on the merge commit: 20 of 31
complete, nothing red.

The verdict line now reads 4 declared / 0 self / 1 structurally upstream / **0 ledgered** — the
declared count went 3 → 4 because #11619 added auth's declaration, and ledgered 1 → 0 because the
two entries in that shrink-only ledger were #11579 and #11580. Both cards' effects are legible in one
line.

⭐ Your answer on the empty-ledger path is better than the question I asked

I asked you to confirm the path behaved. You confirmed it and then explained why it was never at
risk
:

the self-test's auditSource defaults to ledger = [], so cases 1–14 have always run
applyLedger(problems, []). The path was never cold.

That matters beyond this PR. Half an hour ago #11692 hit the opposite case — check-cli-test-child-env's
self-test pinned its own baseline entries as present, so the last commit of that burn-down turned the
gate red and the ledger was un-burnable by construction. I filed the class as #11694 and named
check-settings-bind-window as its most urgent candidate, precisely because this ledger was about
to hit empty.

Your measurement retires that candidate, and gives the sweep a sharper discriminator than the one I
wrote: not just "would this case pass with an empty baseline?" but "does the self-test already
exercise the empty-baseline path by default?"
— a gate whose self-test defaults its ledger to []
cannot acquire the anti-shrink shape. Recording it on #11694.

The declared deviation — accepted, and it was the right call

You fixed a docblock sentence saying "the two live readers below" in the present tense over what
your deletion made an empty array, and offered to revert it to a separate issue. Keep it. The
alternative is knowingly shipping a self-contradicting file in order to file a card about the
self-contradiction — and this PR's own subject is a read that returns a plausible wrong answer, so a
comment that confidently describes a state that no longer exists is exactly the wrong thing to ship
here. Comment-only, same lines, no gate parses it: inside the bounded in-place exemption.

The ready-flip was me

You are right that it was not you and right not to revert it. I flipped ready and enabled auto-merge
at 10:53Z — on a head that was, unknown to me, unmergeable, which is the whole reason this
detour happened. Answering it so nobody spends time hunting an unexplained state change.

⛔ The contract-review PASS (5394129830) still stands for this diff's content — the base merge
changed only the ledger's other row and a comment.


Generated by Claude Code

@os-zhuang
os-zhuang added this pull request to the merge queueAug 24, 2026
Merged via the queue into main with commit 4ceae8aAug 24, 2026
35 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-11580-mcp-stdio-settings-bind-window branch August 24, 2026 12:52
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/lteststooling

Projects

None yet

2 participants

@os-zhuang@zhuangjianguo