An Unloadable Build Is Never A Silent Default
The rule. When an instance activates, its hub binds the configuration its NodeType's recorded
build yields. If that build does not load in this process, the instance serves the
assembly-unavailable diagnosis — after one bounded recompile — and never the mesh default
configuration. A hub resolves its configuration exactly once, so a default binding is not a slower
first paint: it is the whole life of the grain without any of the type's handlers, layout areas or
WithInitialization watchers.
This is the third branch of the same rule. #3006 cured it for a type whose first build had not been kicked off, and The Dependency Record Floor (clause 4, #3934) cured it for a recorded build whose bytes the store could not resolve. The branch where the bytes are resolved and then fail to load kept the old behaviour until #4471.
What was measured — memex.systemorph.com, 2026-09-16
| when (UTC) | what |
|---|---|
| 00:19:40 | last Ops/Status written by the fleet watch, on the pods about to be replaced |
| 00:43 / 00:46 | the two new pods start (3.0.0-ci.8710) |
| 00:49:15.385–.391 | pod …-ztqz8, level Error, MeshNodeCompilationService: Failed to load assembly for Hosting/PlatformBuildInbox — the per-node hub for this NodeType (and every instance of it) cannot activate ×2 — and the same line for Hosting/ModuleReportInbox ×2, Hosting/LogEntry, Hosting/InstanceAction, Hosting/ModuleInventory and Store/Catalog, all inside four seconds |
| 00:49:37 | Hosting/PlatformBuildInbox/_Activity/compile-state re-verified AdoptedVerified, Succeeded — the type itself was fine |
| 01:02 → 21:09 | 125+ WebhookEvents land in Hosting/PlatformBuilds/_Inbox, every one at version: 1; zero [FleetWatch] lines of any kind (#4471's comments) |
| 21:17 | the control instance is restarted (a Restart action, same image) |
| 21:2x | the Inbox area on Hosting/PlatformBuilds reads "Watcher armed … seen 303, processed 303, deferred 0"; _Inbox is empty |
The log lines were read from Ops/Logs entries an earlier Logs action had already landed (selector
{namespace="memex"} |~ "(?i)(…|fail|exception|…)"); nothing was written to take them.
Two facts carry the diagnosis. The hub was activated — it served a 172,350-character read at
08:30 — yet not one of its type's initialisers ran; and the types whose instances were re-activated
later (Hosting/InstanceAction, Hosting/TriageItem, Hosting/LogEntry, Hosting/ModuleInventory)
all healed within the hour, while the one instance the Hosting module's InboxHubAnchor holds
activated for the life of the process never did. An anchored hub is never re-activated, so whatever
it bound at 00:49 was what it served until the restart.
🚨 What is inferred. The default bind itself was not observed — that branch logged nothing, which is half of the defect. What was observed is the load failure, a hub that answered reads while running none of its initialisers, and the recovery on re-activation; the seam below is the only path in the activation code that produces that combination silently, and the regression test reproduces it (including the production log line, word for word) against a real loader.
The seam
NodeTypeEnrichmentHelpers.ApplyStreamResult, usable-build branch (and its pinned-release twin):
HasUsableBuildis true — the record names a build for the live framework.ResolveAssemblyreturns a local path — the bytes are there.GetConfigurationsFromExistingAssembly→CompileResultFromAssembly→LoadNodeAssemblyanswersnull(the file vanished, was deleted as older than the framework, or is a bad image) and the result comes back with noAssemblyLocation, no configurations, and the loader's reason appended to its log as an Error.- The caller took
matching?.HubConfiguration—null— and boundApplyEntry(hubConfig: null), wrapped only in the stale-assembly watcher, which fires when the published build changes. It had not changed and did not change.
No line was logged at the bind. The only trace is step 3's Error, which reads as a compile problem of the type and says nothing about which instance just lost its configuration.
The fix
The extraction's verdict is read before anything is bound. UnloadableBuildDetail answers "did the
recorded build load here?" from the one field every failure branch of CompileResultFromAssembly
leaves empty — AssemblyLocation — and carries the loader's own reason. A build that loaded but
declares no configuration is not refused: a NodeType may legitimately have none, and the default
chain is then correct.
An unloadable build goes through RefuseUnloadableBind, which is exactly the machinery its two
siblings already use, because the state is the same one — a build is recorded and this process
cannot use it:
- within the retry budget,
TriggerRecompileAndRetryflips the typeOk → Pending, waits for the fresh build, and re-enriches against it; - once the budget is spent, the
AssemblyUnavailableoverlay — it names the reason, sets anUnhandledMessageNackso a typed request gets a terminalDeliveryFailurenaming the type instead of being ignored, and self-heals on the next NodeType write.
AnUnloadableBuildIsNeverASilentDefaultTest stores bytes the loader rejects under a record that
reads usable, and asserts the verdict on ApplyStreamResult with the budget already spent: red
before (no configuration at all), green after (the diagnosis overlay). Its control stores loadable
bytes and asserts they are still bound.
Two hypotheses the measurements refuted
Both were written into #4471 before the log lines above were found, and both would have produced a wrong fix.
"MeshWeaver.AI is not loaded, so the watcher's using MeshWeaver.AI; cannot compile." The
Hosting inbox chain does bind MeshWeaver.AI (TriageIntake and the results thread call
hub.StartThread), and /health did list it under pending_module_activation. But
Hosting/TriageItem — whose record carries the same MeshWeaver.AI: min:3.0.0.0 entry — was
re-verified and compiled in this process on the new pods at 01:10:15; the inbox type's own
compile state was re-verified at 00:49:37; and after the restart both fresh pods still listed
MeshWeaver.AI as pending while the watcher drained 303 deliveries. Removing the dependency would
have changed nothing, and would have broken triage threads.
"A restart activates the 8 pending modules." It did not: the pods started by that restart (bake sweeps at 21:21 and 21:24) printed, still at 21:33,
the same eight names. The loader says why, in its own words
([ModuleLoad] STALE PACK: MeshWeaver.Mcp is loading /app/modules/MeshWeaver.Mcp/…): a baseline
image copy is loading because no usable store-installed entry claims the name, and the remedy it
names is re-installing the module, not restarting. So pending_module_activation's "a restart
activates them" is a promise the boot loader does not keep for these entries — a detector
disagreeing with the loader it describes. That is a separate defect and is not changed here.
Design points recorded, not changed
Five capabilities on one hub. The inbox watcher, the build queue, operational-space provisioning,
the fleet watch and the layout are one configuration chain on Hosting/PlatformBuildInbox. That
concentrates the blast radius, but the failure was the activation, not the chain: every
With…-armed capability of any hub dies the same way under a default binding. Splitting the chain
into several anchored NodeTypes would multiply the hubs exposed to it without removing it — the fix
belongs where the binding is decided, which is where it now is.
The watcher's own alarm had no reader. PlatformBuildInboxWatcher.Observe(hub) returns null on a
hub serving the node without an armed watcher, and its comment calls that null the alarm — but it
is read only by the Inbox area, i.e. only while someone looks. The frozen Ops/Status that #4471
was filed on is the same shape one level up: a detector nothing consumes.
See also: The Dependency Record Floor · Node Type Compilation · Webhook Inbox · Operating From The Portal