Repository navigation
A landed module needs a restart, the mesh knows it, and nobody is told — PendingRestart is surfaced nowhere #1979
Description
Residual from #1664 step 10 ("Restart semantics"), the one item of that chain that stayed BUILD. Everything else in Wave 7 landed; this did not, and it is the only gap a person actually meets.
The state exists and is correct
ModuleActivationList.PendingRestart (src/MeshWeaver.PluginCatalog/ModuleActivation.cs:88-90) is set whenever a landing or an uninstall changes what will load:
ModuleLandingService.cs:278— a module landed into a fresh generation dir;ModuleLandingService.cs:308— an uninstall cleared the generation pointer.
It is persisted with the activation sidecar (modules/activation.json, atomic temp+rename) and consumed and reset at boot (memex/Memex.Portal.Shared/MemexConfiguration.cs:606-615). All of that is right.
Nothing reads it
grep -rn "PendingRestart" src/ memex/ returns only the two writers and the boot reset. There is:
- no Store UI state — a package installs, reports success, and the module it landed does nothing until someone happens to restart the pod;
- no health/readiness field, so an operator cannot see it either;
- no API surface, so nothing external can.
ModuleRoot.cs:12 even describes the boot reset as "best-effort and documented as cosmetic" — accurate today, precisely because nothing depends on it.
Why it matters more than it looks
Loading is restart-as-activation by design (ModuleActivation.cs:76: "Takes effect at the next restart, like every activation change"; PluginBundleClient.cs:188: "the module LOADS at the next restart"). That is a deliberate, defensible choice — a genuinely dynamic loader collides with the kernel snapshot (#1649). But it makes the restart part of the install, and an install whose last step is invisible reads as a broken install:
buy → Get → "installed" → the feature is not there → nothing anywhere says why.
That is the same shape as the paid-and-never-delivered defect the Store already learned once (#450): a success message that is true about the half that ran.
Shape of the fix
- Surface it where the install happened — the Store cover's install step is the natural home, since every acquisition already funnels through it. A "Restart required to finish activating N" state on the package card, read from the activation list.
- Surface it to operators — a field on the platform status/health surface, so a fleet with pending activations is visible without opening a portal.
- Optionally, act on it — the self-update lane already rolls pods; a pending activation is a legitimate reason to schedule one. Out of scope here; do (1) and (2) first.
🚨 Whatever surfaces it must read the persisted list rather than an in-memory flag: the process that landed the module is not necessarily the process a viewer is talking to, and the whole point is that the state outlives the request that created it.
Test it can actually fail
The sidecar is durable, so the assertion is cheap and needs no restart: land a module, read the persisted activation list, assert PendingRestart is true and that whatever surface is added reports it; then boot and assert it clears. test/MeshWeaver.PluginCatalog.Test/ModuleLandingServiceTest.cs and ModuleActivationBootTest.cs already stage both halves.
🤖 Generated with Claude Code
Activity
Merge pull request #2003 from Systemorph/feat/1979-surface-pending-re…
Merge pull request #2037 from Systemorph/fix/1979-pending-activation-…
rbuergi commentedon Aug 22, 2026
Item 2 shipped in #2003; #2037 (merged 2026-08-22) corrects it and adds the seam item 1 needs. Item 1's UI half and item 3 remain open.
What #2037 found in the shipped health check — three holes, all of which fail quietly:
PendingRestartis one deployment-wide boolean that the next boot clears, and on a multi-replica deployment the pod that clears it is not the pod missing the module. Replica A lands one and sets it; replica B restarts for unrelated reasons, applies the list, resets it; A keeps serving without the module while every surface reads "nothing pending".Enabledentry, not the pending ones — one landing on a deployment carrying fifteen modules reported "15 module(s) are landed but not yet loaded".Healthy.ModuleActivationSidecar.Readswallows an unparseable file into the empty list and reports it through anonCorruptcallback the check did not pass — so "cannot parse the activation state" rendered as "no pending activation", cheerfully, forever.Landed:
ModuleActivationStatusderives the honest, per-process answer — the persisted enabled entries minus what this process actually loaded. True per pod, immune to another replica's boot, names only what is genuinely missing, and needs no extra state.PendingModuleActivationsis the one reader every surface consults, and its report separates a known empty answer from an undetermined one; the health check reports bothDegraded(still neverUnhealthy).Note
PendingRestartitself is unchanged — still written, still consumed at boot. Nothing now depends on it being accurate across replicas, which is the property it never had.Item 1 — half done. The platform seam exists:
PendingModuleActivationsis registered in DI besideModuleLandingServiceat the same resolved module root, so a NodeType's layout area can resolve it fromhub.ServiceProvider, andIsPendingForPackage(packagePath)answers per package off the entries'PackagePathback-pointer (a blank path matches nothing — it is not a wildcard).What remains is a change in
Systemorph/MeshWeaver.Plugins, not here: rendering "Restart required to finish activating N" on the Store cover's install step (Store/Plugin/Source/PluginLayoutAreas.cs,InstallCtaArea). Kept separate because the two repos deploy on different lanes.One deliberate asymmetry to preserve when that lands: an undetermined state must not put a restart prompt on every package card. It is an operator signal, reported where it is actionable — telling a buyer to restart because a file would not parse is noise they cannot act on.
Item 3 (self-update schedules a roll for a pending activation) is still out of scope, as the issue asks.
rbuergi commentedon Aug 25, 2026
Narrowing on measurement (
origin/main@201a16aa7): the operator half shipped; the user-facing half is shipped-but-unused.Item 2 is DONE (
1cdc14b80,f3ff80f79):src/MeshWeaver.PluginCatalog/ModuleActivationStatus.cs+PendingModuleActivations.cs, with the health check registered aspending_module_activationatmemex/aspire/Memex.Portal.Distributed/Program.cs:427. An operator can see it.Item 1 is the classic "extracting is not using" gap.
PendingModuleActivations.IsPendingForPackage(PendingModuleActivations.cs:104) has zero callers — not in this repo, and not inSystemorph/MeshWeaver.Pluginswhere the Store package's in-mesh source lives. The API to answer "does this package need a restart?" exists and nothing asks it.So the issue's actual complaint — "the mesh knows it, and nobody is told" — is still true for the person who installed the module. They see a Store card that looks finished. Only an operator reading a health endpoint knows otherwise.
Narrow to: wire
IsPendingForPackageinto the Store package card, so a landed-but-not-yet-active module says so where the installer is looking.Merge pull request #2437 from Systemorph/fix/1979-2087-restart-badge-…