Skip to content

A landed module needs a restart, the mesh knows it, and nobody is told — PendingRestart is surfaced nowhere #1979

Closed

Description

@rbuergi

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

  1. 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.
  2. Surface it to operators — a field on the platform status/health surface, so a fleet with pending activations is visible without opening a portal.
  3. 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

  1. added 2 commits that reference this issue on Aug 21, 2026
  2. rbuergi commented on Aug 22, 2026

    @rbuergi
    ContributorAuthor

    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:

    1. PendingRestart is 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".
    2. It named every Enabled entry, not the pending ones — one landing on a deployment carrying fifteen modules reported "15 module(s) are landed but not yet loaded".
    3. A corrupt sidecar read as Healthy. ModuleActivationSidecar.Read swallows an unparseable file into the empty list and reports it through an onCorrupt callback the check did not pass — so "cannot parse the activation state" rendered as "no pending activation", cheerfully, forever.

    Landed: ModuleActivationStatus derives 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. PendingModuleActivations is the one reader every surface consults, and its report separates a known empty answer from an undetermined one; the health check reports both Degraded (still never Unhealthy).

    Note PendingRestart itself 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: PendingModuleActivations is registered in DI beside ModuleLandingService at the same resolved module root, so a NodeType's layout area can resolve it from hub.ServiceProvider, and IsPendingForPackage(packagePath) answers per package off the entries' PackagePath back-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.

  3. rbuergi commented on Aug 25, 2026

    @rbuergi
    ContributorAuthor

    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 as pending_module_activation at memex/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 in Systemorph/MeshWeaver.Plugins where 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 IsPendingForPackage into the Store package card, so a landed-but-not-yet-active module says so where the installer is looking.

  4. added a commit that references this issue on Aug 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions