Uh oh!
There was an error while loading. Please reload this page.
fix(service-cluster): stop the metadata bridge reporting “bridged” over an in-process cluster bus - #14228
Conversation
…rocess cluster driver `MetadataClusterBridgePlugin`'s `metadata.changed` lane attached and logged "bridged metadata.changed -> cluster.pubsub" unconditionally. `Runtime` registers the `memory` driver by default, so on an ordinary single-process boot a cluster service IS present and that claim is a false positive: the bus keeps its state inside one process and fans out to nobody. Lane 1 now consults `isInProcessClusterDriver(cluster.driver)` before attaching and states the in-process case at `debug` -- the shape `AuthzClusterBridgePlugin` uses and the one this plugin's own lane 2 was born with. Both skip the attach rather than relabel the log, and so does this. Skipping reaches nothing: the only subscriber of `metadata.changed` in the tree is the same `MetadataManager` that publishes it, behind an `originNode` loopback guard that discards every in-process message. The seam-missing warn keeps its position ahead of the driver guard, so #13331's original boot symptom still fires verbatim on an in-process boot, and no log level is raised. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016ZC5rNQj3WEet5HAmmAkMs
… described Both places that describe `MetadataClusterBridgePlugin`'s precondition said it calls `attachClusterPubSub()` on `kernel:ready` full stop, which after the lane-1 driver guard would assert the very fan-out the guard exists to stop claiming. Same defect class, one sentence each. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016ZC5rNQj3WEet5HAmmAkMs
📓 Docs Drift CheckThis PR changes 1 package(s): 3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
What this run could not see
Coarse fallback — 4 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 0b39d22f757679919f2904db0f61e82275513c75 && git checkout 0b39d22f757679919f2904db0f61e82275513c75
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 66ecc50a91ab66e254032f1589bf3200da27cacd 49bab8dd9ecde3389b0f56d014275aa7a44e07a3 && git checkout -B drift-repro 66ecc50a91ab66e254032f1589bf3200da27cacd && git merge --no-ff 49bab8dd9ecde3389b0f56d014275aa7a44e07a3
node scripts/docs-audit/affected-docs.mjs --json 66ecc50a91ab66e254032f1589bf3200da27cacd
|
os-steve
commented
Sep 1, 2026
PM 复核 — ACCEPT。⛔ 无需契约复审
⭐ 重测做对了 —— 三种结局你测出了「半 1 成立 / 半 2 缩小」派发令要求对
⇒ ⭐ 而你改了那段文件头,让它陈述「已修、钉在本文件底部」。⛔ 没留下一句在修完之后变成假话的散文 —— 这正是本仓反复立卡的那一类腐烂,你在制造它之前就掐了。 ⭐ attach 分叉:测出来的,不是选出来的派发令要求「照姊妹的实际做法做,⛔ 不要凭听起来更对自选一个」。你测了两个在树样板 —— ⭐ 行为确实移动了,而你证明了它不可观测 —— 这一句我独立复核过跳过 attach 意味着 in-process 下不再挂载,这是行为位移,你如实声明了。让它安全的是那句证明,我对树核过,成立:
⇒ 被移除的订阅本来就永远送不到任何东西。 ⇒ 可观测的 accept/reject 未动 ⇒ Clause-②
⭐ 顺序钉 —— 这一条最容易被漏掉守卫放在 seam-missing warn 之后,并且你为此单独下了一枚钉:#13331 的原始 boot 症状在 in-process boot 上仍逐字触发。
⇒ 一个只看「假阳性没了」的实现会把守卫放到 warn 前面,然后静默掉一个真阳性。你钉住了它。 反向对照(cross-process 驱动仍attach 且仍逐字打印
|
Uh oh!
There was an error while loading. Please reload this page.
Fixes#14021
MetadataClusterBridgePluginlane 1 attached and then loggedbridged metadata.changed → cluster.pubsubwith no driver check.Runtimeregisters the
memorydriver by default, so on an ordinary single-process boota
clusterservice IS present and that line is a false positive: the buskeeps its state inside one process and the fan-out it announces reaches nobody.
Step zero — re-measurement on the post-
1403d943atree⛔ Nothing below is copied from the card. PR #14183 (card #13331) landed at
1403d943aand changed this bridge, so all three were measured fresh on thattree. One half of the card was already absorbed by #14183; the other stands.
1. The test file now EXISTS — what did #14183 already cover?
packages/services/service-cluster/src/metadata-cluster-bridge-plugin.test.tswas created by #14183 (9 tests). Against the card's three wanted cases:
(pubsub, nodeId)a manager metadata service and a real protocol both attachattachClusterPubSub-missing warnwarns for lane 1 AND attaches lane 2 in the same boot⇒ Card half 2 shrinks to one case, not three.#14183 did not merely omit
lane 1's in-process behaviour, it declined it in writing and said why — the
test file's own header:
2. Does half 1 still hold? — YES, unchanged
attachMetadataServiceLanestill attaches and logsinfounconditionally.#14183 did not incidentally fix it. It went further and recorded the defect
while adding lane 2 beside it:
⇒ #14183 built the correct shape 40 lines below the defect, in the same file,
and named this card as the owner of the older one.
3. Is the sibling's guard shape still in place? — YES
authz-cluster-bridge-plugin.ts:78-84still consultsisInProcessClusterDriver, still exported from./split-brain-guard.js(
split-brain-guard.ts:42), still imported by both bridges.Verdict: the card stands, with half 2 reduced to the single in-process case.
The attach fork —⚠️ answered by measurement, not by preference
The order required determining whether the sibling's guard changes only the log
claim or also skips the attach. Both in-tree exemplars skip the attach:
AuthzClusterBridgePlugin—if (isInProcessClusterDriver(...)) { bus = 'in-process'; }in theif, withattachAuthzInvalidationPubSubcalled only in theelse.MetadataClusterBridgePlugin.attachProtocolLane(lane 2, this same file) — logs atdebugandreturns before the attach.⇒ Lane 1 now does the same. This PR does not invent a third shape.
The fix
isInProcessClusterDriver(cluster.driver)is consulted before attaching, andthe in-process case is stated at
debugin lane 2's existing wording, adaptedonly for which channel it names:
⛔ No new sink (
ctx.logger.debug, as lane 2). ⛔ No level raised.Guard position is load-bearing. It sits after the
attachClusterPubSubfeature-detect, exactly as in lane 2, so #13331's original boot symptom still
fires byte-for-byte on an in-process boot. Pinned by its own test — fixing a
false positive must not buy a true negative.
Why skipping the attach loses nothing observable. Measured, not assumed:
the only subscriber of
metadata.changedanywhere in the tree is the sameMetadataManagerthat publishes it (metadata-manager.ts:2951;grepoverthe repo finds no other
subscribe), and its loopback guard drops everymessage whose
originNodeequals its own node id. On an in-process bus that isevery message. (
meta-overlay-cache.ts:80-86andprotocol.ts:3999bothstate in-source that they deliberately do not subscribe to this channel.)
Tests
4 added, alongside #14183's 9. Package suite 97 passed (97) at
49bab8dd9.bridgedinfo line, thedebugline present.redis) ⇒ still attaches with(pubsub, 'node-a')and still logsbridged metadata.changedverbatim. Without this arm the guard is indistinguishable from "never say bridged".Ablation
Guard deleted from lane 1; mutation proven on disk before the run — HEAD
blob
b7d4d18bvs mutateda7b72067, marker count 1 ⇒ 0, guard call sites2 ⇒ 1 (lane 2's survives):
⇒ the in-process pins go red, and the cross-process control plus the
warn-ordering pin stay green — the predicted direction. Restore proven by
state, not by an exit code:
git diff HEADempty, worktree blob back tob7d4d18b, markers 1 / 2. The mutation reached the test without a rebuildbecause the subject is imported relatively (
./metadata-cluster-bridge-plugin.js),so vitest resolves it from source, not through
dist/— proven by the runreddening at all. The script carried an absolute-path
trap ... EXIT INT TERM.Clause-② — both limbs, neither defaulted
1. Path limb —
no. Derived live, not recalled:node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstackreports"no path-derived mandate: the surface hits none of the 3 declared glob(s)".
The contract surface is
packages/spec/src/**(SUSPECT_TIER_GLOBS); this difftouches
packages/services/service-cluster/src/**,content/docs/kernel/**and
.changeset/.2. Content limb —
no, and this is the limb the fork could have flipped, soit is argued rather than asserted. The criterion is "changes contract
accept/reject behaviour or widens the public surface":
attachClusterPubSub(). That is real movement and the order was right to flag it. It is bounded by the measurement above — the removed subscription could never deliver a message, because its only publisher is itself and the loopback guard discards it. So the movement is in the mount, not in any answer the system gives.no, verified mechanically rather than by reading accept/reject alone (the failure a sibling PR hit this round): the diff contains noindex.ts, nopackage.json, nopackages/spec/file;git diff -U0 | grep '^[+-].*export'returns nothing; and no class member — including any optional plugin-config member — is added, removed or changed.warnwhere AGENTS.md puts it aterror— and the card that was supposed to carry the level is CLOSED #13398's ruling (transcribed on PR Repair the two plugin-auth admin-audit durability swallows — batch 6 of the #12981 worklist #13592,issuecomment-5473600172): a site reporting through a published sink shape may not have its level raised toerror. Checked rather than assumed — no level is raised at all. The new arm isdebug, matching lane 2; the pre-existinginfoandwarnkeep their levels and their wording. The deliberate level asymmetry between the two bridges (authz caching substrate: engine-seam write epoch (generalise plugin-security'swriteEpoch) +authz.invalidatedchannel + boot-time posture statement #11968) is untouched.Gates
Derived live at the final head with
--repoasserted; re-derived after thedocs commit, which pulled in 22 further families (the first derivation did not
name them). All green except two, both NOT MEASURED by their own verdict
lines — never read from a bare exit code:
check-test-completeness.mjs— exit 3,PREREQUISITE NOT MET: it grades a savedturbo run testlog that CI tees and this run has none. Its own text: "⛔ It is not a red, and there is nothing here to fix."check:dual-build-cjs-loads— exit 3,PREREQUISITE NOT MET: needs a fullpnpm build; 72+ packages have nodist/. Whole-repo, and CI runs it regardless.check:type-check-debt— exit 3 (whole-ledger--re-measurewants 52 built dependency closures). Narrowed and measured for this package instead:tsc --noEmitover@objectstack/service-clusterreports exactly 1 error,TS2322insrc/memory/memory.contract.test.ts— the pre-existing ledger entry (errors: 1, code-tier 1 (TS2322)), unmoved.--listFilesconfirms both edited files are in that program, so the reading actually covers them.check:skill-examples— first run exit 1, but its verdict line readsPREREQUISITE NOT MET(unbuiltclient-reactdeclarations), not a finding — the exit-1-not-3 shape. Built the closure and re-ran: green, 259 prose examples across 3 surfaces.pnpm lint(repo-wideeslint . --no-inline-config) was run whole, notnarrowed: exit 0.
check:type-check-coverage(structural half) green.Ratchet families re-run at the final head
49bab8dd9after the docs commit.Bounded in-scope docs correction
Two sentences describing this bridge's precondition would, after the guard,
assert exactly the fan-out the guard exists to stop claiming
(
content/docs/kernel/cluster.mdx§6.2 andcontent/docs/kernel/services-checklist.mdx). Same defect class, one sentenceeach, mechanical, and no open PR claims either file (checked across all 10 open
PRs). Named here rather than left as a silent rider. ⛔
content/docs/releases/untouched.
Boundaries honoured
⛔ #13331 / PR #14183's work not touched · ⛔
authz-cluster-bridge-plugin.tsunmodified (it is the exemplar) · ⛔ #11968 not reopened · ⛔
content/docs/releases/untouched · ⛔ not flipped to ready, no auto-merge.
Generated by Claude Code
Generated by Claude Code