Roll Selection

🚨 The platform rolls independently of its packages β€” policy platform-rolls-independently-of-packages (Policy Not Prose). The selection takes the newest admitted candidate, always, and asks the package predicate about it exactly once. Whatever that verdict says β€” a missing bake, an unresolvable framework identity, an unloadable module, an unread plugin list β€” is reported on the outcome (Advisories, the summary line) and never moves the choice. Packages catch up through their own update policy. Release & Self-Update Strategy has the measured deadlock this removed.

History. The selection was built (#3479) as "the latest platform version shipping all plugins" and refined by the Module Adoption Policy (#3648–#3651), under which only a module that provably could not load declined a candidate. Under policy platform-rolls-independently-of-packages nothing declines one. The measurements those changes rest on are kept below as evidence; where a paragraph describes the retired behaviour it says so.

Two questions, kept apart

Before policy platform-rolls-independently-of-packages the selection asked one question β€” which is the latest release that ships all of this environment's plugins? β€” and walked past every release that did not:

 a set is published  ──►  which release ships ALL of THIS environment's plugins?
                                     β”‚
                        the latest one that does ──► different from current β‡’ update
                                     β”‚
                        none does ──► stay put, and NAME the plugin

That made the platform wait for its packages, while every package update waits for its platform floor β€” and the two waits met (measured on one deployment; see Release & Self-Update Strategy). The selection now asks two questions and lets only the first decide:

 a set is published  ──►  which is the NEWEST release this environment's policy admits?
                                     β”‚
                          that one ──► different from current β‡’ update
                                     β”‚
                          and, about that one release only:
                          what do THIS environment's packages say?  ──► REPORTED
                              (advisories + the denominator on the outcome; never a decline)

A package that is not available for the selected release does not follow the roll: it keeps its previous generation, or parks, and catches up through its own update policy. The release availability predicate does not go away β€” it still answers the package question truthfully, and the CI build gate and the package surfaces still read it as a yes or no.

The package answer is per environment. It is a statement about that environment's plugin list, so two environments selecting the same release may carry different advisories.

The algorithm

MeshWeaver.PluginCatalog.RollSelection.Select states it once:

1. no candidate was offered        β‡’ NoCandidates   (said, not left as a silence)
2. read this environment's plugin list          ← the DENOMINATOR
   Β· could not read it  β‡’ select the NEWEST candidate; advisory: "the plugin list could not be read …"
   Β· read it, EMPTY     β‡’ select the NEWEST candidate; advisory: "0 plugins required β€” … named none …"
3. otherwise take the NEWEST candidate and ask the SHARED predicate about it β€” once
   Β· the verdict goes through UpdatabilityVerdict.ForPlatformRoll: a refusal becomes advisories,
     led by the reason it would have held on (WaivedHold)
   Β· a content package with no bake for it β‡’ "would recompile at boot: …" (BootCompiles)
4. conclude, from where the selection sits relative to what runs
   Β· selected == current          β‡’ AlreadyCurrent   (said, not left as a silence)
   Β· otherwise                    β‡’ Update

The walk is lazy and now trivially so: it examines one candidate, so a selection costs exactly one verdict. Declined on the outcome is empty.

ReleaseAvailabilityService.SelectRollTarget wraps that with what only the instance knows:

Before the walk Outcome
the deployment consumes no CI bakes (no published root, or a root that serves none) NotEnforced β€” nothing is selected by this selection; the caller takes the newest admitted tag
the published root could not be read, and the caller supplied candidates the newest candidate (RollSelection.Unjudged), advisory: "the published bundle root could not be read …, so no package was checked"
the published root could not be read, and the candidates would have come from it Indeterminate β€” there is nothing to select from
the release markers could not be listed Indeterminate
the selection did not answer within its budget, or faulted Indeterminate

Indeterminate therefore means only "the candidate universe could not be seen, or the selection did not answer" β€” never a statement about a package. It is not a hold on the poller: SelfUpdateHostedService.FirstRollable takes the head of its own candidate list whenever the selection names no release.

What a package verdict becomes

Nothing declines a candidate. Each thing the shared predicate can say about the selected release is carried on the outcome instead:

A package that… The predicate's answer What the outcome says
ships a compiled module the target publishes a build of available nothing
ships a compiled module the target publishes no build of, whose landed generation links against the target's published surface (platform-surface.json, the link gate) available nothing
…whose landed generation does not link β€” it references a type the target does not carry (ModuleUnloadable) refuses Advisories[0]: "the platform rolls independently of packages β€” N package(s) are not available for this release …" with the module and the missing type; the summary: "not following this roll: …"
…whose loadability could not be measured β€” the release published no surface, the bytes are unreadable available, with an advisory Advisories: "could not be determined …"
has no sealed content bake for the target available β€” the instance compiles it at boot BootCompiles / Advisories: "would recompile at boot: …", and the summary line
has no sealed content bake, on an instance with Modules:RequirePrebuilt (the seeder refuses a boot compile and parks the type) refuses advisory + "not following this roll: …"; the package parks until its bake exists
has a bake whose sealed set is torn or inconsistent (MeshWeaver#3175) refuses advisory + "not following this roll: …"
cannot be judged because the release's framework identity is not resolvable (no release marker) indeterminate Advisories[0]: "… package availability for this release could NOT be determined, and that decides nothing about the platform roll: …"

SatisfiedPlugins counts the packages that are available on the selected release, so it can be less than RequiredPlugins β€” that difference is the number of packages that do not follow the roll.

What is shared, and what is new

Piece Where Status
Does release R ship package P? ReleaseAvailability.IsUpdatable shared β€” the predicate #3441/#3443 built and falsified
What is published for R? PublishedBundleCatalogue.Read shared
Which packages must be shipped? ReleaseAvailabilityService.RequiredPackages shared β€” the monotone denominator
Which release should this environment be on? RollSelection.Select the selection β€” the newest admitted candidate
What does a package refusal become on a roll? UpdatabilityVerdict.ForPlatformRoll shared β€” the one waiver every roll path applies
Which releases exist at all? PublishedBundleCatalogue.PublishedReleases new β€” the _releases/ markers
The answer, readable from outside GET /api/plugins/roll-target new

There is deliberately no second copy of the package rule. RollSelection.Select takes the verdict as a function, so what the outcome reports cannot drift from the predicate: the poller, the endpoint and CD all reach the same predicate through the same service method, and the same waiver.

The candidate universe is the release markers

<root>/_releases/<version> records the framework identity a release resolves β€” the only way anything outside the image can learn it, because the identity is a property of the binaries. A version with no marker has no resolvable identity, and every package answers Indeterminate for it. When a caller asks SelectRollTarget without candidates, the markers are the universe. A caller that supplies its own candidates (the poller, from the registry's tags) is taken at its word: a tag with no marker is selected like any other, and the unresolvable identity is the leading advisory.

🚨 GET /api/plugins/roll-target answers with the POLLER's candidates, never the markers' (#4413). A marker is written by main-cd's publish-bake, which needs promote and not arm, so a set the fleet promoted but has not armed carries a marker and no memex-portal-ai:<version> tag. Over the markers the route named such a set (memex answered 3.1.11003 while only mw-plugin-test:3.1.11003 existed), the poller could never take it, and combo verification, which asks this route which image to verify, failed docker pull … manifest unknown. The route now calls SelfUpdateHostedService.SelectRollTarget: the policy read, the registry listing, the channel filter and the walk that FirstRollable shares, so the named candidate has a portal image by construction. A listing that faults is Indeterminate (combo verification goes red on it), and a host with no poller answers Indeterminate too.

Ordering stays with the caller: VersionSelect owns the ordering rules β€” the sealed-publication lineage rather than the version string, see Self-Update Target Selection β€” the tag-shape filters (a bare git sha parses as 6943991.0.0 and sorts above every real release) and the update policy. RollSelection reads "newer" off the candidate list's order, so MeshWeaver.PluginCatalog holds no second opinion about which release is newer.

The denominator

What "this environment's plugins" means

It is the install records β€” Plugins/*, nodeType:Package β€” the same source the deployment gate already reads. That choice is not automatic: an instance's modules also arrive as per-Space _GitSync entries, and InstanceComboReader folds both shapes precisely because a reader of one alone under-reports. Two things decide it here:

  1. The predicate is shared, so the denominator must be too. A selector whose denominator differs from the gate's would report one package answer on the outcome and another on the Updates tab β€” two answers about one environment, which is the drift sharing it exists to prevent.
  2. Only an install record carries the id the bake names its bundle by. A sync entry names a partition; the published root holds <bundle>.zip. Folding sync entries in would add names that can never match a bundle, and every environment would report them as lagging forever.

The known weakness of that source is that it can answer empty β€” measured on memex on 2026-08-10: 42 sync entries, zero install records. That is said on the outcome rather than passed off as completeness (below), which is the whole reason the source is stated on every outcome instead of assumed.

🚨 An empty denominator is said, never dressed as a pass

With an empty denominator every release ships all zero plugins. Before policy platform-rolls-independently-of-packages that was refused (NoPluginsKnown), because a selector that tolerated it degenerated into "take the newest" while claiming completeness. Taking the newest is now the rule, so the refusal is gone β€” and the claim is what must not come back. "No plugins were checked" and "all plugins present" are still never spelled the same way:

The per-version verdict is unchanged, and the roll paths waive it. ReleaseAvailabilityService.IsUpdatable still answers an empty denominator β€” and an unreadable published root β€” as not updatable (UpdatabilityVerdict.Unavailable), because as an answer to the package question that is the truth. Every roll path (the poller, the reboot's image step, the manual button, GET /api/plugins/is-updatable) passes the verdict through ForPlatformRoll, which turns the refusal into the leading advisory and keeps the reason verbatim as WaivedHold.

Every outcome prints the denominator and the cost, whatever it concluded:

[RollSelect] memex: 77 plugin(s) required (from the install records (Plugins/*, nodeType:Package));
             75 of 77 satisfied by 3.0.0-ci.8100 (the newest candidate); 2 package(s) would
             recompile at boot: AgenticPrimer, Crm; the platform rolls independently of packages β€”
             not following this roll: SocialMedia: its landed module MeshWeaver.Social cannot load
             on 3.0.0-ci.8100 … references MeshWeaver.Mesh.CodeOutputCurrency …. Different from
             3.0.0-ci.8009 β‡’ update.

A package answer whose expected count nobody can read is one nobody can tell from a vacuous one. This is the same discipline [ReleaseGate] denominator: … established one level up.

Presence, or presence and adoptability?

The package predicate reads both, and each has discriminated on a different real incident β€” so neither half may be dropped from what the outcome reports:

Incident What the publication looked like Which half saw it What the selection does with it now
the public instance, 3.0.0-rc9.ci.7676 (held 2026-09-03) 38 of 77 installed packages had no sealed bundle under identity sf61a0f5d75… presence named as the boot compile, and the release is selected; under Modules:RequirePrebuilt the packages are named as not following the roll
the public instance, 3.0.0-ci.7621 (#3175) every package present; a bundle's NodeTypes recorded a module MVID the same identity's sealed module set did not carry adoptability the predicate still refuses the torn set; the release is selected and the package is named as not following the roll
the control instance, 3.0.0-ci.7926 (2026-09-06) complete and consistent neither β€” see below unchanged
every production portal, 3.0.0-ci.8009 β†’ all eleven candidates (2026-09-07) complete for the platform's own content; the satellites not yet baked for the new identity; every landed module would have loaded presence held it (behind the floors) β€” wrongly the newest is selected; the unbaked satellites are the named cost

The predicate carries all of it, and RollSelection reports it unchanged.

🚨 The boundary: what happened on 3.0.0-ci.7926

This is the incident that prompted the directive, and it is worth stating exactly, because a reader will otherwise assume this algorithm covers it.

The control instance rolled to 3.0.0-ci.7926 at 19:17Z on 2026-09-06. Its bake gate found Feedback/Feedback regressed and refused readiness β€” correctly. The refused pod nonetheless kept running and kept stamping module generations, two images served one mesh for two hours, and Crm/Offer and Crm/Opportunity went unloadable: every deal and offer page on the client portal was dead from 19:28Z.

A completeness selector would have chosen 7926 anyway β€” and so does today's β€” and the evidence is in the CD run. Run 7926 (actions/runs/34050278127, all 26 jobs green) resolved framework identity sc273ee39fdccbfc088f9aaf1fc548a9a, and its bake-and-seal job logged, on both storage targets:

ok  Feedback/Feedback [5 source(s), 9 dependency record entr(ies)]
bake: Feedback β†’ 1 assembly(ies) + 15 node file(s) β†’ /bake/Feedback.zip
[PASS] Feedback (13 node(s), 1 type(s))   ok  Feedback/Feedback: compile=Ok render=ok tests=ok
published: …/prebuilt-bundles/sc273ee39fdccbfc088f9aaf1fc548a9a/plugins/Feedback.zip
sealed:    …/prebuilt-bundles/sc273ee39fdccbfc088f9aaf1fc548a9a/plugins/_complete (36 bundle(s))

7926 shipped Feedback. What failed was the instance: that portal's own copy of the package is missing one source node β€” Feedback/Feedback/Source/FeedbackContent, absent since 2026-08-26; the NodeType's compile record shows four matched Code nodes and src=4 where the bake compiled five. The shortfall had been masked for eleven days by an AdoptedUnverified prebuilt assembly. Changing the framework identity made that assembly stale, the pre-warmer compiled the live β€” incomplete β€” source set, and 135 diagnostics later the readiness gate refused.

So what the selection can report stops at the publication. Its package answer is "has this release published something every installed package can adopt?", and it cannot answer "will this instance's own content build against it?" β€” the instance's content is not a property of the release. The checks that do see that are instance-side and already exist:

Stretching the package answer to try to cover this would be guesswork. Recording the boundary is not, and AReleaseThatShipsEveryPluginIsSelectedEvenWhenTheInstanceCannotBuildOne pins it as a test.

The selection never goes backwards

Before policy platform-rolls-independently-of-packages the walk could conclude that the latest complete release was behind what the environment runs, and reported that as BehindCurrent without applying it. The walk no longer produces that answer: it takes the head of the caller's newest-first list, and nothing behind the head is examined.

What keeps a roll from moving an environment backwards is therefore the candidate list. The poller β€” the path that applies a selection β€” offers only tags strictly newer than the running build. roll-target asks the poller for the same list and applies nothing: its answer is for a person or a pipeline to read. A rollback is still a separate decision: a schema migration is not reversible and the roll path carries the image and nothing else (The Self-Update Schema Wall). The one deliberate exception β€” recovering an install stranded on a tag the registry no longer lists β€” belongs to VersionSelect, which builds the candidate list, not to this selection (Self-Update Target Selection).

Who asks

All roll paths reach the same method and the same waiver, for the same reason the availability verdict has one implementation β€” a rule only one caller honours is not a rule.

Path How
The self-update poll SelfUpdateHostedService.FirstRollable β†’ SelectRollTarget over the registry's tags. The poller supplies its own candidates (policy-filtered, strictly newer than what it runs) and takes the head whenever the selection names no release. The combo verdict is consulted afterwards, on the selected tag, and recorded as an advisory.
An operator or a pipeline rolling by hand, and combo verification GET /api/plugins/roll-target β€” the poller's own selection (SelfUpdateHostedService.SelectRollTarget), instance-key or build-principal gated, fails closed, answers {environment, kind, current, selected, shouldUpdate, indeterminate, requiredPlugins, satisfiedPlugins, advisories[], bootCompiles[], declined[]} (declined is empty). The running version is read from the instance, never taken from the caller, so nobody can ask for a rollback by naming one.
A version somebody already chose GET /api/plugins/is-updatable?version=… β€” operator and external roll tooling (a manual kubectl set image, see Deployment β€” AKS). It answers the roll question: a package refusal comes back as isUpdatable: true with the reason verbatim in waivedHold, the advisories, and every package's own status in packages[].

🚨 The gap the endpoint closed is not hypothetical. Both memex portals carried policy: None at the time, so the poller β€” the only place a selection existed β€” was not running on either. is-updatable can only answer about a version the caller already named. On 2026-09-06 there was no way, anywhere, to ask "which release should this environment be on".

What it does not see