Repository navigation
fix(#3472): a process refuses a build keyed to a framework identity that is not its own, and stops reporting Ok for it - #3509
Merged
Conversation
…hat is not its own, and stops reporting Ok for it Closes #3472 The adoption half of the 2026-09-06 outage; #3491 (`MeshPublicationGate`) was the membership half. `Crm/Offer` and `Crm/Opportunity` reported `compilationStatus: Ok` for two and a half hours while every one of their per-instance hubs was dead. `CompilationStatus.Ok` is a claim SCOPED to `CompiledFrameworkVersion`, persisted as though it were absolute. The record has always carried both halves — the verdict and its scope — in two separate fields, and every instrument read the first one. That is why no writer-side rule fixes it alone: the pod that wrote `Ok` could load what it had just built. THE WRITER HALF — the identity travels with the coordinates. `NodeTypeContractHandler.ApplyResolvedSuccess` decided the assembly path and the store key by "did the response carry a reference" and decided `CompiledFrameworkVersion` by a different predicate (`freshCompile`). On the HYDRATE path those disagree, and a hydrate only succeeds when the store RESOLVED bytes — under a key whose framework tag is this process's own. The record then said `Ok`, named loadable bytes, and declared them unloadable; `HasUsableBuild` is false forever, and after `MaxRecompileAttempts` the instance binds the fallback configuration for the grain's whole life ("Area not found", under a green tick). One extracted predicate now decides all four fields. The retained branch still retains — that is the only case where an ABI-staleness marker is real. THE READER HALF — `NodeTypeBuildIdentity`, one pure predicate with two consumers, deliberately the shape `MeshPublicationGate` uses because the defect is a divergence. `RefusalReason` gates the six load paths that never consulted `HasUsableBuild`; each takes exactly the branch a store MISS already takes, so behaviour on a tag-carrying store is unchanged and the operator gains a line naming BOTH identities. `ReportedStatus` folds the pair into `CompilationStatus.Foreign`. `Foreign` is a fourth state because each existing one carries a different, wrong remedy: `Error` says "correct the code" (#641's defect in the other direction), `Unavailable` says "retry or wait" (the state is determined, and waiting does not help), `Ok` is the lie. It is DERIVED and never persisted — writing a reader-relative verdict into a shared record is exactly how #3395's ping-pong was made. Log level: the refusals are `LogError`, matching the sibling `NodeTypeExecutionGate` refusal in `CellSurfaceAssemblyProvider`. This is a verdict about the bytes that only a rebuild or a rebake changes, not an availability problem that resolves itself — the 2026-09-06 cost of it being unsaid was two and a half hours of a client portal, so the Loki volume is bought deliberately. It fires once per refused load site, not per request. Doc: `Doc/Architecture/BuildIdentityAdmission`, cross-linked from `MeshAdmission`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
🟡 Changes recommended
There are two confirmed correctness/i18n issues in the updated code paths (stored as PR comments) that should be addressed before approval.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR tightens NodeType build adoption by enforcing a framework-build-identity match at load time, and by reporting a derived non-persisted CompilationStatus.Foreign when a record says Ok but is not loadable by the current process—addressing the outage class described in #3472.
Changes:
- Introduces
CompilationStatus.Foreign(derived at read time) andNodeTypeBuildIdentityto centralize “refuse vs report” decisions. - Enforces the identity refusal across previously unguarded NodeType assembly load paths (schema probes, pinned releases, cell surface, diagnostics reporting).
- Adds end-to-end + unit tests, localized UI copy for the “foreign framework” overlay, and architecture/What’s New documentation.
File summaries
| File | Description |
|---|---|
| test/MeshWeaver.Compiler.Pipeline.Test/ForeignFrameworkBuildIsRefusedTest.cs | End-to-end mesh test asserting foreign-identity builds are not served and do not report Ok. |
| test/MeshWeaver.Compiler.Pipeline.Test/AdoptTimeFrameworkIdentityTest.cs | Unit tests pinning refusal and reported-status behavior, plus the writer-side hydrate stamping invariant. |
| src/MeshWeaver.Messaging.Hub/Localization/strings.en.json | Adds localized keys for the foreign-framework compile overlay (EN). |
| src/MeshWeaver.Messaging.Hub/Localization/strings.de.json | Adds localized keys for the foreign-framework compile overlay (DE). |
| src/MeshWeaver.Mesh.Operations/MeshOperations.cs | Uses scoped/derived status for diagnostics and refuses foreign builds in schema hub-config recovery. |
| src/MeshWeaver.Mesh.Contract/Services/CompilationStatus.cs | Appends Foreign enum member with documentation stating it is derived and not persisted. |
| src/MeshWeaver.Graph/NodeTypeLayoutAreas.cs | Uses ReportedStatus to avoid redirect loops and renders localized foreign-framework messaging. |
| src/MeshWeaver.Graph/NodeTypeDataModelAreas.cs | Refuses foreign builds when resolving instance hub configuration, falling back to node config. |
| src/MeshWeaver.Graph/MeshDataSource.cs | Uses ContentAs and refuses foreign builds in the NodeType schema probe path. |
| src/MeshWeaver.Graph/Configuration/NodeTypeEnrichmentHelpers.cs | Refuses pinned releases built for a different framework identity, returning the existing “assembly unavailable” overlay path. |
| src/MeshWeaver.Documentation/Data/WhatsNew/2026-09-07-a-page-no-longer-reports-ok-when-it-cannot-open.md | What’s New entry describing the user-visible behavioral change. |
| src/MeshWeaver.Documentation/Data/Architecture/MeshAdmission.md | Cross-links the membership/admission doc to the new adoption/identity doc. |
| src/MeshWeaver.Documentation/Data/Architecture/BuildIdentityAdmission.md | New architecture doc detailing the design, enforcement sites, and scope boundaries. |
| src/MeshWeaver.Compiler.Pipeline/NodeTypeContractHandler.cs | Refuses foreign-build hydration, adds pinned-release framework check, and fixes hydrate stamping so identity follows resolved coordinates. |
| src/MeshWeaver.Compiler.Pipeline/NodeTypeBuildIdentity.cs | New centralized predicate for refusal + derived reported status for foreign-identity Ok. |
| src/MeshWeaver.Compiler.Pipeline/CellSurfaceAssemblyProvider.cs | Refuses joining foreign-identity assemblies into the kernel session cell surface. |
Review details
- Files reviewed: 16/16 changed files
- Comments generated: 2
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Comment on lines
+2821
to
+2825
| // ContentAs, never a cast: the Where above already accepted this node, but the | ||
| // value can still arrive as raw JSON on a hub that did not register the type. | ||
| var def = node.ContentAs<NodeTypeDefinition>(hub.JsonSerializerOptions); | ||
| if (def is null) | ||
| return Observable.Return<NodeCompilationResult?>(null); |
Comment on lines
+473
to
+476
| private static string Short(string? identity) | ||
| => string.IsNullOrEmpty(identity) | ||
| ? "(none)" | ||
| : identity[..Math.Min(8, identity.Length)]; |
Contributor
Contributor
Contributor
Contributor
Contributor
Contributor
Contributor
rbuergi
marked this pull request as draft
September 7, 2026 00:14
This was referenced Sep 7, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #3472
#3491 (
MeshPublicationGate,d54cf3d80) is the membership half of 2026-09-06: a pod itsreadiness gate had refused kept running and kept publishing. This is the adoption half — what a
healthy, admitted process must refuse to load, and what it may honestly report about a build it
cannot load. It composes with that gate rather than duplicating it: that one decides whether
this process may publish; this one adds nothing to publish.
Criterion 2 first, because it is the one that needed a design
🚨
CompilationStatus.Okis a claim SCOPED toCompiledFrameworkVersion, persisted as though itwere absolute. "The last compile succeeded" is true and process-independent. "These bytes can be
loaded" is only ever answerable relative to a process. The record has always carried both halves
— the verdict and its scope — in two separate fields, and every instrument read the first one.
That is why no writer-side rule fixes it alone: the pod that wrote
Okcould load what it hadjust built. It was telling the truth about itself. So the answer is split, and each half is necessary.
Half one — the writer, and a live defect that needs no second image
NodeTypeContractHandler.ResolvedStoreVersionalready argues the rule, for the store-key version:The framework identity is the fourth member of that reference and was left out of it.
ApplyResolvedSuccessdecided the assembly path and the store key by "did the response carry areference" and decided
CompiledFrameworkVersionby a different predicate (freshCompile). On theHYDRATE path those disagree — and a hydrate only succeeds when the assembly store resolved
bytes, under a key whose framework tag is this process's own. The record then reads:
Ok, naming loadable bytes, declaring them unloadable.HasUsableBuildis false forever; everyper-instance activation takes the ABI-stale recompile path; after
MaxRecompileAttemptsthe instancebinds the fallback configuration — and a hub resolves its configuration exactly once, so that is
"Area not found" for the grain's whole lifetime, under a green tick. The handler also races the
activity write-back, so it re-imposes the foreign stamp over each correct one and the type never
converges. One extracted predicate (
StoreVersionFromResponse/ReferenceCameFromResponse) nowdecides all four fields, so they cannot drift. The retained branch still retains — that is the
only case where an ABI-staleness marker is real, and stamping live over retained coordinates would be
the same #1368 defect reached the other way round.
Half two — the reader:
CompilationStatus.Foreign, derived and never persistedForeign-
Okrecords still arrive, and no writer rule can stop them: a peer replica on anotherimage (#3395), a node repo that COMMITS a record (Plugins ships
Store/Catalogwith a July hash), aprebuilt bundle.
NodeTypeBuildIdentity.ReportedStatusfolds the pair.Why a fourth state rather than a redefinition — each candidate carries a different, wrong remedy:
ErrorUnavailableOkForeignsays "recompile here", which is the remedy, and which the compile watcher alreadydrives. 🚨 Nothing writes it. Writing a reader-relative verdict into a shared record is exactly
how #3395's ping-pong was made — two replicas, two identities, each correctly overwriting the other's
answer forever. The record keeps saying what the compiler did; only the report is scoped.
Reported through:
get_diagnostics(status: "Foreign", both identities inerror), thecompile-progress overlay (which used to read the green status and redirect the viewer back to the
page that cannot render, which bounced them back — now it holds and explains, localized), and the
NodeType overview progress line (which showed ✓ Compiled while printing the foreign hash beside
it as decoration).
Criterion 1 — the refusal, at seven load paths instead of one
HasUsableBuildhas carried the framework equality since #464 — on one of the seven paths thatload a NodeType's assembly. The other six gated on the two coordinate fields being populated, or on
CompilationStatus == Okalone: both pinned-release branches (NodeTypeEnrichmentHelpers,NodeTypeContractHandler—NodeTypeRelease.FrameworkVersionexists and nothing read it), thepublished-release hydrate,
MeshDataSource.HandleNodeTypeSchemaRequest,NodeTypeDataModelAreas.ResolveInstanceHubConfig,MeshOperations.ResolveHubConfigForSchema, andCellSurfaceAssemblyProvider(which takes a lifetime lease).🚨 What stood between those and a foreign load was an eight-character substring inside one store
implementation's glob —
FileSystemAssemblyStore'sv{version}-{FrameworkTag}-*.dll. Nothingasserted that property, and the other implementation (
BlobAssemblyStore) ships in a module thisrepository cannot compile. An implicit guard is not a guard.
Refusing is not "the type is dead". Every site takes exactly the branch a store miss already
takes — a local compile, a fallback configuration, or a no-answer — so behaviour on a tag-carrying
store is unchanged, and the operator gains a line naming both identities. Naming only one is how
the bake gate's "regressed on this image" sent three investigations to the wrong repository that
same night.
Falsification — three arms, with numbers
Two pure arms on the functions that decide it, one end-to-end on a real mesh (
MonolithMeshTestBase,a real Roslyn compile, no mocking).
AdoptTimeFrameworkIdentityTest17/17,ForeignFrameworkBuildIsRefusedTest1/1, andHydrateIsNotACompileTest(#2895's no-op pins) 6/6 unchangedCompiledFrameworkVersion = freshCompile ? live : def.…)ResolvedSuccessNeverLeavesOkStandingOverAForeignIdentity: the record keptsc273ee39fdccbfc088f9aaf1fc548a9a, the incident's own identity, where the live one belonged. The other 22, including all six #2895 no-op pins, stayed green — the change does not touch the hydrate no-opReportedStatus => definition?.CompilationStatus)AForeignOkNeverReportsOk. The other 16, including theOk-control and the five pass-through statuses, stayed greenOk; the pack is on the cell surface with an honest build. Without them every assertion would pass against a gate that refused everythingThe mesh test asserts the premise after the measurement — it re-reads the record and requires it
to still be foreign before judging the resolution — so a healed record makes the test say so
instead of passing. Not a retry: the framework-stale kickoff's store probe answers
Skiphere(bytes exist under the live tag), which is exactly why the state is durable.
Local verification (Release,
-warnaserror, one project per invocation):MeshWeaver.Mesh.Contract,MeshWeaver.Compiler.Pipeline,MeshWeaver.Graph,MeshWeaver.Mesh.Operations,MeshWeaver.Messaging.Hub,MeshWeaver.Hosting,MeshWeaver.Documentation,Memex.Portal.Shared—0 warnings, 0 errors each.
MeshWeaver.Compiler.Pipeline.Test673/673,MeshWeaver.Graph.Test1382/1382,
MeshWeaver.Hosting.Test404/404,MeshWeaver.Documentation.Test304/304(link integrity, What's New integrity, every localization guard),
OrleansCompileActivityAccessTest8/8 — including
FrameworkStaleAssembly_SelfHealsOnInstanceActivation, the test theResolveStaleBuildActioninvariant note says goes red when this rule is relaxed.🚨 Two findings that falsify a premise, both measured while building this
#3472's own hypotheses. (a) a pod on a different platform build compiled these types was
confirmed on the issue and is the incident's mechanism. (b) the watcher adopted a prebuilt published
under a foreign identity was not needed to explain anything observed — and this change found the
third route the issue did not name: the writer can mint the foreign-
Okrecord itself, with oneimage, no bundle and no peer, through
ApplyResolvedSuccess's hydrate path.🚨
search 'nodeType:NodeType compilationStatus:Error'— AGENTS.md's stated sweep instrument —cannot see this, and on some hosts cannot see anything. Not changed here (both fixes have blast
radius far beyond this issue), documented instead:
MeshOperations' search envelope projectsPath/Name/NodeType/Version/LastModifiedonly;compilationStatusis a WHERE clause and nothingmore.
QueryEvaluator.GetDirectPropertyValueresolves a selector by reflection againstMeshNode,which has no
compilationStatusproperty and noContentfallback — so the comparison isnull == "Error"and the sweep returnscount: 0for a mesh full of broken types. Thetranslation that makes it work is in
PostgreSqlSqlGenerator, which is not in this repository.A green sweep on a dev Monolith is evidence of nothing. The instrument that is
reader-relative is
get_diagnostics, which now answersForeign.Also filed for the record in the doc page:
MeshDataSource.HandleNodeTypeSchemaRequestandMeshOperations.ResolveHubConfigForSchemawere readingnode.Content is NodeTypeDefinition/(NodeTypeDefinition)node.Content— the forbidden payload cast. Both are nowContentAs.Scope boundaries (deliberate)
selection).
DecideStaleBuildActionstill answersSkipwhen the framework moved and the store holds live-tag bytes; nothing has hashed those bytes,and restamping would assert validity for a build the lane never examined. That boundary is
unchanged — and it is why a record can sit at
Okwith a foreign identity indefinitely, which iswhat makes half two necessary rather than cosmetic.
is still the path that corrects it.
Also in this change
Doc/Architecture/BuildIdentityAdmission— the design, the seven load paths, the fourth-stateargument, and the two sweep findings. Cross-linked from
MeshAdmission, which now names itscounterpart.
Category: Fix).ui.compileForeignFramework,…Body) in both catalogs. 🚨 The Pluginsmirror needs the hand-over after this merges:
npm run sync:i18n -- --ref <merged core sha>. Itreds nothing today (the guard compares against a pinned core commit), which is exactly why it has
to be said here.
No public type or member is removed, and no interface gains a member, so neither cross-repo gate
applies.
🤖 Generated with Claude Code