You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
ModuleUpdateDecision compares version alone, so a consumer can skip an update it needs — the consumer half of Plugins#931 #3163
The producer half landed in #3135 (2026-09-02): the module build key and the ledger record carry the platform framework identity, and the registry publish is addressed with it, so two builds of the same content against two platforms are two keys.
The consumer half is untouched, and it is what makes the producer half load-bearing. Verified on main today:
src/MeshWeaver.PluginCatalog/ModuleUpdateDecision.cs:157
? new(ModuleUpdateAction.SkipUpToDate, $"version {bundleVersion} is already landed")
grep -c 'FrameworkMvid\|FrameworkIdentity' over that file: 0.
Why it matters
A module's published version encodes its content only. A module rebuilt against a new platform with unchanged source produces an identical hash → an identical version. The consumer decides on that version alone, before any download:
It holds AI@1.1.0 compiled against rc7; the registry now serves AI@1.1.0 compiled against rc8; the versions match, so it answers SkipUpToDate. The bytes it keeps were compiled against a framework it is no longer running.
That is the FrameworkDeclined / "adopts nothing" family arriving from the other direction. minMeshVersion does not close it: it is a floor (API compatibility), not an identity match — a bundle built against rc7 satisfies a floor that rc8 also satisfies.
Meanwhile the bake half is identity-anchored: FrameworkIdentity is the stamp/MVID of the compile-toolchain assembly, and baked assemblies are stored under v<ver>-s<identity>-<hash>.dll. Those two facts cannot both be right.
What this needs
ModuleUpdateDecision must compare (version, framework identity), not version alone, before answering SkipUpToDate — reading the identity #3135 now publishes alongside the bundle.
Worth deciding explicitly at the same time: what a consumer should do when the identity is absent from a bundle record (a bundle published before #3135). Fail-open (skip) reproduces today's defect for exactly the bundles most likely to be stale; fail-closed (re-fetch) costs one download per module on first upgrade. The second is almost certainly right, but it should be a decision rather than a default that falls out of a null check.
Refs
Systemorph/MeshWeaver.Plugins#931 — the issue this is the second half of, with the full measurement
This is already implemented — in #3154, which is held as a draft awaiting a maintainer decision
#3154 ("already landed" means this content against this FRAMEWORK — Plugins#931 consumer half) is
this exact change: it cites the same site (ModuleUpdateDecision.cs:157), is the same consumer half
of Plugins#931, and carries the same producer-half context from #3135.
It is CLEAN and deliberately not marked ready. Its own body states why:
Opened as a draft on purpose — auto-arm.yml arms every non-draft PR for the merge queue, and
this one is not to be merged without a maintainer look: it changes what the module updater does on
every installation, and the fleet-wide consequence of an identity moving is a re-land of every
affected module.
So this is not un-started work — it is work waiting on a decision.
The open question this issue raises is answered there too. This issue asks what a consumer should
do when the identity is absent from a bundle record (a bundle published before #3135), and reasons
that fail-closed is "almost certainly right". #3154 should be read for the answer it actually
implements rather than that decision being re-litigated here.
Leaving this open rather than closing it: the defect is real and live until #3154 lands. It should be
closed by that merge, not ahead of it.
Closing — the criterion is met, checked against the criterion rather than the PR title
#3154 merged to main at 11:08Z today (d51b2971). I read what it changed rather than trusting the name, and this issue's stated need is met end to end.
1. The comparison this issue asked for
ModuleUpdateDecision must compare (version, framework identity), not version alone, before answering SkipUpToDate
src/MeshWeaver.PluginCatalog/ModuleUpdateDecision.cs on main — the landedComparison == 0 branch no longer ends at the version:
if(servedMvidis not null&&!string.Equals(landedMvid,servedMvid,StringComparison.Ordinal))returnnew(ModuleUpdateAction.Land,$"version {bundleVersion} is landed, but built against framework "+$"{landedMvid??"(unrecorded)"} while the registry serves that same version "+$"built against {servedMvid} — same content, different platform build; ...");
The site this issue quoted — $"version {bundleVersion} is already landed" — is gone. The remaining SkipUpToDate reasons both name the framework, or name the fact that no identity was stated.
2. It is reachable in production, which is the part that was actually missing
This matters more than the comparison itself, because #3135 had not made the identity reachable at all: BundleRef carried none, and the value the consumer recorded at landing was the identity it had requested on the download URL, not what the module was built against. Comparing those two would never have converged. #3154 closed the whole path:
Registry advertises it per bundle — memex/Memex.Portal.Shared/Api/PluginBundleEndpoints.cs emits frameworkMvid = servable?.FrameworkMvid inside the bundles[] array of /api/plugins/bundles/index.json, read off the shelf entry its publisher wrote. Deliberately not the index's top-level identity, which is the registry's own bake.
Consumer deserializes it — PluginBundleClient.BundleRef.FrameworkMvid, an init property (not a sixth ctor parameter, so no binary break), read with JsonSerializerDefaults.Web, so frameworkMvid binds.
The one production caller passes it — PluginBundleClient.AdoptModule → ModuleUpdateDecision.Decide(..., bundle?.FrameworkMvid). I checked the call sites: AdoptModule is reached from CatalogLayoutAreas (explicit install) and RegistryUpdateReconciler (unattended: true, the lane mw-plugin-test: --allow known-debt ratchet for the plugins gate #723 measured going quiet). There is no other production caller of Decide in either repo — the only remaining callers are tests.
Landing writes the compared value back — LandFromBundle(..., advertisedFrameworkMvid) records advertisedFrameworkMvid ?? manifest!.FrameworkMvid on the activation entry, in preference to the archive's requested-lane stamp. That is what makes the two sides converge after one landing instead of re-landing forever.
3. The open question this issue raised was decided, and not the way this issue guessed
This issue asked for the absent-identity case to be "a decision rather than a default that falls out of a null check", and reasoned that fail-closed (re-fetch) was "almost certainly right". #3154 decided it explicitly, and split it — fail-closed on one side, fail-open on the other:
landed
served
verdict
known
known, different
Land — reason names both identities
unknown (pre-field entry)
known
Land, once — then it records the identity and settles
any
unknown
Skip, and the reason says the identity could not be checked
known
known, equal
Skip — the genuine no-op, with the framework named
The asymmetry is argued, not defaulted: landing writes the identity back, so an unknown on the landed side heals after one fetch; it can never heal one on the served side, because a registry that states no identity will state none next time either. Answering Land there would re-download every module on every reconcile, forever, on every deployment pointed at a pre-#931 registry.
So this issue's lean is honoured where it can be (landed-unknown re-fetches, once) and deliberately reversed where it would have bought a permanent loop for a comparison that still has nothing to compare. The verdict there refuses to imply agreement — it says out loud that the registry stated nothing, which is the readable-log half of the original complaint.
Pinned by test/Memex.Portal.Shared.Test/ModuleUpdateIdentityTest.cs (12 facts + a theory), including SameVersion_DifferentIdentity_Lands (the #723 repro), SameVersion_ServedIdentityUnknown_SkipsAndSaysItCouldNotBeChecked, AfterLanding_TheNextReconcileSkips (convergence), and OmittingTheIdentityArgument_IsTheLegacyAnswer_NotASafetyNet — so nobody reads the optional parameter's default as a safety net.
What it does not cover, now tracked
The served-unknown branch is a real blind spot, and #3154 named where it must be closed rather than papering over it at the consumer: a bundle that cannot state what it was built against must become unpublishable. That is still true today — ModulePublish.Accepted.FrameworkMvid is string? and ModulePublish.Validate refuses nothing, so a producer that states no identity shelves a null and every consumer of that bundle sits permanently in skip-and-say-so.
It was recorded only in Doc/Architecture/ModuleBuildArchitecture's roadmap, and Plugins#931 is closed, so it had no open tracker. Filed as #3211.
That is producer-side work and outside what this issue asked for. Closing this one as done.
Activity
rbuergi commentedon Sep 3, 2026
This is already implemented — in #3154, which is held as a draft awaiting a maintainer decision
#3154 ("already landed" means this content against this FRAMEWORK — Plugins#931 consumer half) is
this exact change: it cites the same site (
ModuleUpdateDecision.cs:157), is the same consumer halfof Plugins#931, and carries the same producer-half context from #3135.
It is
CLEANand deliberately not marked ready. Its own body states why:So this is not un-started work — it is work waiting on a decision.
The open question this issue raises is answered there too. This issue asks what a consumer should
do when the identity is absent from a bundle record (a bundle published before #3135), and reasons
that fail-closed is "almost certainly right". #3154 should be read for the answer it actually
implements rather than that decision being re-litigated here.
Leaving this open rather than closing it: the defect is real and live until #3154 lands. It should be
closed by that merge, not ahead of it.
rbuergi commentedon Sep 3, 2026
Closing — the criterion is met, checked against the criterion rather than the PR title
#3154 merged to
mainat 11:08Z today (d51b2971). I read what it changed rather than trusting the name, and this issue's stated need is met end to end.1. The comparison this issue asked for
src/MeshWeaver.PluginCatalog/ModuleUpdateDecision.csonmain— thelandedComparison == 0branch no longer ends at the version:The site this issue quoted —
$"version {bundleVersion} is already landed"— is gone. The remainingSkipUpToDatereasons both name the framework, or name the fact that no identity was stated.2. It is reachable in production, which is the part that was actually missing
This matters more than the comparison itself, because #3135 had not made the identity reachable at all:
BundleRefcarried none, and the value the consumer recorded at landing was the identity it had requested on the download URL, not what the module was built against. Comparing those two would never have converged. #3154 closed the whole path:memex/Memex.Portal.Shared/Api/PluginBundleEndpoints.csemitsframeworkMvid = servable?.FrameworkMvidinside thebundles[]array of/api/plugins/bundles/index.json, read off the shelf entry its publisher wrote. Deliberately not the index's top-level identity, which is the registry's own bake.PluginBundleClient.BundleRef.FrameworkMvid, aninitproperty (not a sixth ctor parameter, so no binary break), read withJsonSerializerDefaults.Web, soframeworkMvidbinds.PluginBundleClient.AdoptModule→ModuleUpdateDecision.Decide(..., bundle?.FrameworkMvid). I checked the call sites:AdoptModuleis reached fromCatalogLayoutAreas(explicit install) andRegistryUpdateReconciler(unattended: true, the lane mw-plugin-test: --allow known-debt ratchet for the plugins gate #723 measured going quiet). There is no other production caller ofDecidein either repo — the only remaining callers are tests.LandFromBundle(..., advertisedFrameworkMvid)recordsadvertisedFrameworkMvid ?? manifest!.FrameworkMvidon the activation entry, in preference to the archive's requested-lane stamp. That is what makes the two sides converge after one landing instead of re-landing forever.3. The open question this issue raised was decided, and not the way this issue guessed
This issue asked for the absent-identity case to be "a decision rather than a default that falls out of a null check", and reasoned that fail-closed (re-fetch) was "almost certainly right". #3154 decided it explicitly, and split it — fail-closed on one side, fail-open on the other:
The asymmetry is argued, not defaulted: landing writes the identity back, so an unknown on the landed side heals after one fetch; it can never heal one on the served side, because a registry that states no identity will state none next time either. Answering
Landthere would re-download every module on every reconcile, forever, on every deployment pointed at a pre-#931 registry.So this issue's lean is honoured where it can be (landed-unknown re-fetches, once) and deliberately reversed where it would have bought a permanent loop for a comparison that still has nothing to compare. The verdict there refuses to imply agreement — it says out loud that the registry stated nothing, which is the readable-log half of the original complaint.
Pinned by
test/Memex.Portal.Shared.Test/ModuleUpdateIdentityTest.cs(12 facts + a theory), includingSameVersion_DifferentIdentity_Lands(the #723 repro),SameVersion_ServedIdentityUnknown_SkipsAndSaysItCouldNotBeChecked,AfterLanding_TheNextReconcileSkips(convergence), andOmittingTheIdentityArgument_IsTheLegacyAnswer_NotASafetyNet— so nobody reads the optional parameter's default as a safety net.What it does not cover, now tracked
The served-unknown branch is a real blind spot, and #3154 named where it must be closed rather than papering over it at the consumer: a bundle that cannot state what it was built against must become unpublishable. That is still true today —
ModulePublish.Accepted.FrameworkMvidisstring?andModulePublish.Validaterefuses nothing, so a producer that states no identity shelves a null and every consumer of that bundle sits permanently in skip-and-say-so.It was recorded only in
Doc/Architecture/ModuleBuildArchitecture's roadmap, and Plugins#931 is closed, so it had no open tracker. Filed as #3211.That is producer-side work and outside what this issue asked for. Closing this one as done.