Skip to content

docs: a dynamic NodeType registers its content type only where an instance activates (Plugins#2178, #2180) - #5079

Merged
meshweaver-cloud[bot] merged 5 commits into
mainfrom
docs/dynamic-content-type-registration
Sep 21, 2026
Merged

meshweaver-cloud[bot] merged 5 commits into
mainfrom
docs/dynamic-content-type-registration

Conversation

@rbuergi

@rbuergi rbuergi commented Sep 21, 2026 •

Copy link
Copy Markdown
Contributor

What this is

Plugins#2178 (CD stalled 24 h) and Plugins#2180 (Hosting/InstanceAction untypeable on one replica) both say a Hosting type is untypeable because its prebuilt bundle was declined. Measured read-only on both live portals today, that is not the mechanism — and the difference decides where the fix goes.

Root cause

A dynamic (in-mesh, Roslyn-compiled) NodeType's content CLR type enters IMeshContentTypeRegistry as a side effect of its hub configuration being built, which happens at a per-node hub's cold activation. A per-node hub is a single activation cluster-wide, and nothing at boot closes the gap:

  • DynamicTypePreWarmer's adopt-only pass "ASKS, NEVER BUILDS" — and it is the default path for every deployment;
  • where the compiling sweep does run, a type the shared assembly store already holds is reported AlreadyBaked and, in the warmer's own words, "never activated" — the normal case for every replica after the first;
  • ContentTypeRegistrationSweep covers static definitions only.

So a dynamic type whose few instances all activate on another replica has a usable assembly and no registration here, and every read of one degrades to a bare JsonElement: pages render empty, ContentAs<T> yields null, and SelfUpdateRouting asking "is there a Hosting/Deployment record" is answered 0 while the rows are in the table.

A declined prebuilt cannot be the cause: the decline path logs "DECLINED … — compiling instead", and a local compile activates the hub and registers the type. The decline was correlated, not causal.

Measurements (2026-09-21, read-only, both portals)

memex.systemorph.com (746b4e48) memex.meshweaver.cloud (026442ff)
bake-report Healthy, 229/230 usable Healthy, all with a verdict usable
types with no usable assembly BinaryClickerV2 3, all in partition MeshWeaver
live record census clean — 0 untyped, 0 foreign clean
content-types Hosting/DeploymentStatus ×30, Store/Plugin ×8 8–11 per replica, different set on each, incl. Hosting/Deployment

🚨 The two sets are DISJOINT — the types with no usable assembly are not the untypeable ones, and vice versa. The discriminator is instance count: Hosting/InstanceAction has ≥200 instances and is untypeable on no replica sampled; Hosting/DeploymentStatus has 5 and Hosting/Deployment has 4, and both are untypeable right now. Store/Plugin reads buildProvenance: AdoptedVerified on the live framework identity — adopted, never compiled, never activated.

Also corrected here: content-types entry presence is a live verdict, not a since-boot residue — ContentTypeHealthCheck calls ContentDegradationRegistry.Unresolved, which re-asks the registry per entry at probe time. Only the ×count is cumulative.

The fix that must NOT ship — measured, with a control on each side

Calling ContentTypeRegistration.ProbeRegister from the degrade seam is the obvious fix. It was built, it works, and it is unsafe: obtaining a dynamic type's configuration goes through EnrichWithNodeType, which triggers the compilation chain and writes the shared NodeType record (the stale-Ok self-heal flips it to Pending). A read seam doing that lets every non-owning replica independently compile and re-stamp one row the whole deployment shares — the cross-stamp NodeTypeLiveRecordCensus exists to DETECT.

🚨 A measurement that looked like proof of this has been WITHDRAWN, and the withdrawal is in the diff. With the seam wired, ANodeTypesSourcesWaitForItsBundleTest failed "Expected AdoptedVerified … but found Compiled" while clean main ran 861/861 — the exact shape the hazard predicts. Re-running the control on the FINAL tree, with the seam removed and a 862-test assembly identical in count to main's, the same test failed again. The diff cannot reach it; one green run of main is not a control. The hazard argument above never needed the test and stands on the code alone; the intermittent failure is filed separately rather than explained away.

The behavioural change is not in this PR. Registration belongs with the component already sanctioned to activate dynamic types on this replica — the pre-warmer's AlreadyBaked branch — as a registration-only pass on a paced background trickle after the sweep settles. That trade (the ~13.5 s of assembly opening #1660 removed from boot) is a maintainer decision, so the page describes the shape rather than shipping it.

What is in the diff

  • A doc page (Doc/Architecture/DynamicContentTypeRegistration): the mechanism, the measurements, how to read content-types, the unsafe fix and why, and the acceptance test any fix must pass. Listed in the Architecture topic map — ArchitectureTopicMapTest caught the omission.
  • ContentTypeRegistrationSweep's stated reason for skipping dynamic types, corrected in place. The old sentence ("a dynamic type with zero instances has no payload … so there is nothing to degrade") reads as "nothing is exposed", and something is. It now also says, at the one place someone would go to write the fix, why the degrade-seam version must not be it.
  • The /health sentence itself — the instrument that misdiagnosed both issues. ContentDegradationRegistry.Describe asserted two causes as fact ("its prebuilt bundle was declined or its compiled assembly is not on this replica") and the commonest one was neither. It now names that one FIRST, keeps the other two, and names what decides between them: if bake-report's no-usable-assembly list does not name a type, that type's assembly is present. Pinned by test.
  • A retraction, committed rather than quietly dropped: the page originally claimed this defect explains SelfUpdate's "lists 0". It does not — that lookup folds over index ROWS and DeploymentContent is a framework type reached by reference, so ContentAs<T> never asks the registry. That zero was core#4958, since fixed. The section now carries the boundary instead, because it is what makes a false attribution easy.

The only runtime change is the text of one /health description string. dotnet build -c Release -warnaserror clean on MeshWeaver.Graph and MeshWeaver.Hosting; MeshWeaver.Documentation.Test 604/604; the ContentDegradation/UntypedContent seam tests 13/13.

What this does not establish

No production verification, and neither issue is closed. The acceptance measurement for any fix is positive, never an absence: on a replica reporting a type under content-types, that entry disappears from the next /health probe while bake-report is unchanged and the record still reads buildProvenance: AdoptedVerified — a fix that heals by compiling has re-stamped the shared record and is the unsafe shape wearing a green tick.

🤖 Generated with Claude Code

…tance activates

Plugins#2178 and #2180 both report a Hosting type that one replica cannot
type. Both attribute it to a declined prebuilt bundle. Measured on two live
portals, that is not what is happening, and the difference decides the fix.

A dynamic NodeType's content CLR type enters IMeshContentTypeRegistry as a
side effect of its hub configuration being built, which happens when a
per-node hub cold-activates. A per-node hub is a single activation
cluster-wide. DynamicTypePreWarmer's adopt-only pass ASKS, NEVER BUILDS (the
default for every deployment), and its compiling sweep reports a type the
shared store already holds as AlreadyBaked and "never activated". So on every
replica after the first, a dynamic type whose few instances activate elsewhere
has a usable assembly and no registration — and every read of one degrades.

ContentTypeRegistrationSweep's own reason for skipping dynamic types carried
the false half of this: "a dynamic type with zero instances has no payload
carrying its discriminator, so there is nothing to degrade". True for zero,
false for few. Corrected in place, with the measurements.

Measured 2026-09-21, read-only:
- memex.systemorph.com: bake-report Healthy, 229/230 usable, live record
  census clean (0 untyped, 0 foreign) — while content-types named
  Hosting/DeploymentStatus and Store/Plugin as untypeable on that replica.
- memex.meshweaver.cloud: 8-11 untypeable types per replica, a DIFFERENT set
  on each, Hosting/Deployment among them; the 3 types with no usable assembly
  are all in partition MeshWeaver, disjoint from every named type.
- The discriminator is instance count, not bundle state: Hosting/InstanceAction
  has >=200 instances and is untypeable nowhere; Hosting/DeploymentStatus has
  5 and Hosting/Deployment has 4, and both are untypeable now.
- Store/Plugin reads buildProvenance: AdoptedVerified on the live framework
  identity — adopted, never compiled, never activated.

Also records the fix that must NOT ship, because it was built and measured:
calling ProbeRegister from the degrade seam obtains the configuration through
EnrichWithNodeType, which triggers the compilation chain and WRITES the shared
NodeType record (the stale-Ok self-heal flips it to Pending). A read seam doing
that lets every non-owning replica compile and re-stamp one row the whole
deployment shares — the cross-stamp NodeTypeLiveRecordCensus exists to detect.
Control on each side, full MeshWeaver.Hosting.Test suite: clean main 861/861
green; with the seam wired, ANodeTypesSourcesWaitForItsBundleTest failed
"Expected AdoptedVerified ... but found Compiled".

No behaviour change here. Registration belongs with the pre-warmer's
AlreadyBaked branch as a post-sweep background trickle; that trade (the ~13.5 s
of assembly opening #1660 removed from boot) is a maintainer decision.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings September 21, 2026 13:32

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

The new doc page’s front-matter description claims it includes “on-demand registration that closes it” while the page content explicitly says no fix ships and that the on-demand degrade-seam fix is unsafe (plus a minor ambiguous issue reference in XML docs).

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 2 Low severity

Open (2)
What changed in this PR

Documents and clarifies a production failure mode where dynamic (runtime-compiled) NodeType content types are only registered on the replica that activates an instance hub, leaving other replicas able to load assemblies yet still unable to type node content.

Changes:

  • Adds a new architecture page explaining the registration mechanism, observed measurements, and why the “register-on-degrade” fix is unsafe.
  • Updates ContentTypeRegistrationSweep XML docs to correct the rationale for excluding dynamic types and to point readers to the new doc page.
File Description
src/​MeshWeaver.Graph/​ContentTypeRegistrationSweep.cs Refines XML documentation around dynamic type registration behavior and documents the unsafe “probe from degrade seam” approach.
src/​MeshWeaver.Documentation/​Data/​Architecture/​DynamicContentTypeRegistration.md New architecture doc capturing the mechanism, diagnostics, and acceptance criteria for any future fix.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

---
Name: Dynamic Content Type Registration
Category: Architecture
Description: A dynamic NodeType's content CLR type registers only as a side effect of one of its instances activating in THIS process — so a type whose few instances live on another replica is untypeable here, with a usable assembly, a clean bake and a clean census. The mechanism, the measurements that separate it from a declined bundle, and the on-demand registration that closes it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct, and it is the worst place for this page to be stale: the Description is what a catalog reader sees INSTEAD of the body. It is left over from an earlier draft that did ship a degrade-seam registrar; the body now records that that fix was built, measured, and must NOT ship (it drives an AdoptedVerified type into a compile and re-stamps a record the whole deployment shares).

Rewritten to say what the page actually delivers: the mechanism, the measurements that separate it from a declined bundle, and why the obvious fix is unsafe — no claim that anything is closed.

Comment thread src/MeshWeaver.Graph/ContentTypeRegistrationSweep.cs Outdated
@github-actions

github-actions Bot commented Sep 21, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 0)

  1 files  ±0    1 suites  ±0   2m 0s ⏱️ ±0s
300 tests ±0  300 ✅ ±0  0 💤 ±0  0 ❌ ±0 
302 runs  ±0  302 ✅ ±0  0 💤 ±0  0 ❌ ±0 

Results for commit afeb162. ± Comparison against base commit c449390.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Sep 21, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 1)

396 tests   - 1 157   396 ✅  - 1 157   55s ⏱️ - 2m 17s
  1 suites  -     1     0 💤 ±    0 
  1 files    -     1     0 ❌ ±    0 

Results for commit afeb162. ± Comparison against base commit c449390.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Sep 21, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 4)

    2 files   -   1      2 suites   - 1   2m 35s ⏱️ - 3m 22s
1 539 tests  - 383  1 539 ✅  - 383  0 💤 ±0  0 ❌ ±0 
1 539 runs   - 384  1 539 ✅  - 384  0 💤 ±0  0 ❌ ±0 

Results for commit afeb162. ± Comparison against base commit c449390.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Sep 21, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 2)

    1 files   -     2      1 suites   - 2   5m 36s ⏱️ +2s
1 899 tests +1 183  1 899 ✅ +1 375  0 💤  - 192  0 ❌ ±0 
1 900 runs  +1 184  1 900 ✅ +1 376  0 💤  - 192  0 ❌ ±0 

Results for commit afeb162. ± Comparison against base commit c449390.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Sep 21, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 3)

862 tests  +419   862 ✅ +419   6m 33s ⏱️ + 5m 38s
  1 suites  -   2     0 💤 ±  0 
  1 files    -   2     0 ❌ ±  0 

Results for commit afeb162. ± Comparison against base commit c449390.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Sep 21, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 5)

    2 files   -     3      2 suites   - 3   7m 5s ⏱️ - 6m 36s
2 421 tests  - 1 404  2 421 ✅  - 1 402  0 💤  - 2  0 ❌ ±0 
2 425 runs   - 1 404  2 425 ✅  - 1 402  0 💤  - 2  0 ❌ ±0 

Results for commit afeb162. ± Comparison against base commit c449390.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Sep 21, 2026 •

Copy link
Copy Markdown
Contributor

Test Results

    8 files   -     9      8 suites   - 9   24m 46s ⏱️ - 6m 36s
7 417 tests  - 1 342  7 417 ✅  - 1 148  0 💤  - 194  0 ❌ ±0 
7 424 runs   - 1 342  7 424 ✅  - 1 148  0 💤  - 194  0 ❌ ±0 

Results for commit afeb162. ± Comparison against base commit c449390.

♻️ This comment has been updated with latest results.

rbuergi and others added 4 commits September 21, 2026 17:30
…n one was neither

Copilot findings, both real:
- the page's front-matter Description still promised "the on-demand registration
  that closes it" from a draft that did ship one; the body says the opposite.
  The Description is what a catalog reader sees INSTEAD of the body, so that is
  the worst place for it to be stale. Rewritten, and it now says NOT yet closed.
- "Systemorph/MeshWeaver.Plugins#2178, #2180" left the second number unqualified.
  Core has a #2180 too, so the bare form does not fail to resolve — it resolves
  to the wrong issue. Both fully qualified.

And the thing those two issues were actually misdiagnosed BY. ContentDegradationRegistry
.Describe told every reader of /health that a type it cannot bind means "the module
that declares the type is not loaded here (its prebuilt bundle was declined or its
compiled assembly is not on this replica)". Measured on two live portals, neither
was happening for any named type, and the sets are disjoint: on memex.systemorph.com
the ONE type with no usable assembly is BinaryClickerV2, and the two untypeable ones
are Hosting/DeploymentStatus and Store/Plugin; on memex.meshweaver.cloud the three
with no usable assembly are all in partition MeshWeaver and none is named. An
instrument that asserts two causes as fact sends its reader past the real one — twice,
at sev:H.

The sentence now names the commonest cause FIRST (a dynamic NodeType this replica
ADOPTED rather than compiled is never activated by the bake, so its content type is
never registered here), keeps the other two, and names the instrument that decides
between them: if bake-report's no-usable-assembly list does not name a type, that
type's assembly is present and the first cause is the answer. Pinned by test.

Also adds the page to Data/Architecture.md — ArchitectureTopicMapTest caught the
omission: a working URL alone does not make a page discoverable.

MeshWeaver.Documentation.Test 604/604; the ContentDegradation/UntypedContent seam
tests 13/13; Release -warnaserror clean on MeshWeaver.Graph and MeshWeaver.Hosting.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
It does not, and the page asserting it would have sent the next reader down a
branch already excluded.

Two independent reasons, both measured after the page was written:
- SelfUpdateRouting's lookup folds over INDEX ROWS — ResolveRecord →
  MatchRecord(rows, reference), path/id only. No content is typed, so a
  degraded content type cannot turn a listed record into a zero.
- DeploymentContent is a FRAMEWORK type (core src/MeshWeaver.Deployment.Contract),
  shipped in the image and reached by reference; Apply reads the record with
  ContentAs<DeploymentContent>, which deserialises to a statically-known T and
  never consults the mesh-wide registry. Hosting/PlatformBuildInbox shares six
  named files from Hosting/Deployment/Source, SelfUpdateRouting among them, and
  no content declaration.

That zero was core#4958 — a zero with two causes treated as one — and the lane
now answers RecordVerdict.Unseen at Error instead of concluding absence
(Plugins, 2026-09-20 15:18Z).

The section is replaced with what the defect DOES reach: every consumer that
reads through MeshNodeStreamCache and then asks for the CLR type. And with the
boundary that matters more, because it is the one that makes a false
attribution easy: ContentAs<T> with a statically-known T is immune, so a silent
empty is not by itself evidence of this defect.

Retracted on the issue as well. Documentation.Test 604/604.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…s do decay

Two corrections to how content-types gets read, both now measured rather than
argued.

The standing belief is that content-types is a cumulative since-boot counter
with no decay, so an entry may be historical. That is half right and the wrong
half is load-bearing. ContentTypeHealthCheck calls Unresolved, which re-asks
the registry per entry at probe time under BOTH routes, and Clear removes an
entry the moment a read of that type succeeds. Measured over 2.7 h on one
deployment: the named set CHANGED and members LEFT it — which a counter that
never decayed could not do.

And the part that is cumulative is an instrument rather than a caveat, if two
probes are compared. The count only rises when a read degrades. Measured on
memex.meshweaver.cloud, Hosting/DeploymentStatus went x260/x257 to x373/x376
across 2.7 h — those reads are degrading now, not at boot. A count that is
static across two probes says the opposite, and is the only case where an entry
may be a residue nothing has since asked the registry to clear.

That matters because every cheap signal near this defect is an absence, and
this is one of the few positive ones available without cluster access.

Documentation.Test 604/604.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ails without it

I published a measurement that did not hold, and caught it only by running the
control again on the final tree.

The claim: with the degrade-seam registrar wired,
ANodeTypesSourcesWaitForItsBundleTest failed "Expected AdoptedVerified ... but
found Compiled" while clean main ran 861/861 green. That is the exact shape the
hazard predicts — an adopted type driven into a compile — so it read as
confirmation and I wrote it into the page, the code comment, the PR body and two
issue comments.

It is not confirmation. The same test failed again on THIS tree, which has the
registrar removed and a 862-test assembly identical in count to main's. The diff
cannot reach that test. One green run of main is not a control, and two failures
against one pass attributes nothing.

What survives is the code: ResolveHubConfiguration -> EnrichWithNodeType is
handed the compilation service and can write the shared NodeType record
(DispatchPending flips a stale-Ok record to Pending to force a recompile) and
arms the rebind watcher. A read seam must not do that on a row the whole
deployment shares. That argument never needed the test and is now stated as what
it is — a reading of the code.

The intermittent failure is filed separately rather than explained away: an
assertion that an ADOPTED type is never driven through a COMPILE, failing
intermittently, is either a test-isolation defect or the race this page is
about, and guessing which is how a wrong root cause gets published.

Documentation.Test 604/604; Release -warnaserror clean on MeshWeaver.Graph.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@meshweaver-cloud
meshweaver-cloud Bot added this pull request to the merge queue Sep 21, 2026
Merged via the queue into main with commit 15bc838 Sep 21, 2026
37 checks passed
@rbuergi
rbuergi deleted the docs/dynamic-content-type-registration branch October 10, 2026 13:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants