Skip to content

Restore the 8-parameter constructor of LanguageModelCatalogSource - #2333

Merged
rbuergi merged 1 commit into
mainfrom
fix/ai-catalog-source-binary-compat
Aug 26, 2026
Merged

rbuergi merged 1 commit into
mainfrom
fix/ai-catalog-source-binary-compat

Conversation

@rbuergi

@rbuergi rbuergi commented Aug 26, 2026 •

Copy link
Copy Markdown
Contributor

What this fixes

systemorph could not roll for ~2 hours. It did not look like a code problem: the old pods kept serving aade75d00, every health probe returned 200, and helm just sat there. The new ReplicaSet was in CrashLoopBackOff the whole time — 24 restarts — on:

Unhandled exception. System.TypeInitializationException:
  'MeshWeaver.AI.Anthropic.AnthropicExtensions' threw
---> System.MissingMethodException: Method not found:
  'Void MeshWeaver.AI.LanguageModelCatalogSource..ctor(String, String, Int32, String,
   String, ImmutableArray`1<String>, Boolean, MeshWeaver.AI.ProviderKind)'

Cause

987da5f9e added Description as a ninth primary-constructor parameter. It has a default, so it is source-compatible and every gate stayed green — but a record's primary constructor signature is replaced, not extended, and a call site that omits the optional arguments still emits a call to the full constructor as it stood at its own compile time.

The AI provider modules are compiled separately, live in another repository, and land at runtime under /data/modules. Per ModuleLandingService they "bind by simple name, and their contract is API compatibility, not build identity" — build identity is deliberately diagnostic-only there. So all six landed providers went on calling the 8-argument constructor, which no longer existed.

Landed Aug 25 01:17–01:19, all before the break:

module landed
MeshWeaver.AI.Anthropic@c2916b6c Aug 25 01:17
MeshWeaver.AI.AzureFoundry@8f60c8c4 Aug 25 01:17
MeshWeaver.AI.OpenAI@661a3510 Aug 25 01:19
MeshWeaver.AI.WebSearch@8412db3f Aug 25 01:19

Modules whose own source changed were rebuilt and re-landed at Aug 25 20:3x. These four were untouched — only the framework under them moved — so nothing rebuilt them and, by design, nothing rejected them.

The fix is additive — and the CI gate is why

My first cut moved Description to an init property. That restores the 8-parameter form by removing the 9-parameter one: the same mistake pointing the other way, repairing every module compiled before the change and breaking every one compiled after it.

scripts/check-record-signatures.py refused it, exactly as it should:

✗ src/MeshWeaver.AI/BuiltInLanguageModelProvider.cs
    record LanguageModelCatalogSource — arity

That gate landed in f60dc19df at 2026-08-26 00:21 — eleven hours after the break it would have caught. This PR is one of its first real customers, and it caught me committing the inverse of the bug I was fixing.

So: the ninth parameter stays, and the eight-parameter form is restored beside it. Both populations bind. No call site changes; Description keeps working positionally and through with { … }.

The overload has no source-level caller by design — its callers are separately-compiled assemblies that bind by name at runtime. It is therefore pinned by a test rather than by use, and carries a comment saying so, because the obvious next cleanup is to delete it as dead code.

Verification

  • MeshWeaver.AI builds clean, 0 warnings
  • MeshWeaver.AI.Test — 1305 passed, 0 failed
  • check-record-signatures.py --base origin/main — clean (and --self-test passes)
  • All six provider modules in MeshWeaver.Plugins compile against this core
  • LanguageModelCatalogSourceBinaryContractTest verified by mutation in both directions: deleting the 8-param overload turns it red (2 failures); adding a 10th parameter turns it red

The guard

No compile-time gate inside this solution can catch this class of break, because nothing here compiles against the binaries that make the call. The gate above catches it at the diff; this test holds the same line from inside the assembly, where it still applies to a consumer that never sees the diff. It pins both arities, asserts the two forms produce equal values, and carries a negative control so it cannot pass vacuously.

Its doc comment says explicitly: if it goes red, do not update it to match the new signature. That one "fix" converts a caught break into a shipped one.

🤖 Generated with Claude Code

Copilot AI lite review requested due to automatic review settings August 26, 2026 08:21
@rbuergi
rbuergi enabled auto-merge August 26, 2026 08:22

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.

Pull request overview

Restores binary compatibility for LanguageModelCatalogSource so separately-compiled AI provider modules (landed under /data/modules) can continue binding to the shipped 8-parameter primary constructor at runtime, preventing MissingMethodException crash loops during rollout.

Changes:

  • Moves Description off the LanguageModelCatalogSource primary constructor and into an init property to keep the shipped constructor signature stable.
  • Adds a reflection-based contract test that pins the 8-parameter constructor shape (with a negative control) and verifies Description remains settable via init/with.
  • Adds explicit XML documentation warning that the record’s primary constructor is a shipped binary contract.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

File Description
test/MeshWeaver.AI.Test/LanguageModelCatalogSourceBinaryContractTest.cs Adds a reflection-based binary contract test for the shipped constructor and validates Description remains available via init.
src/MeshWeaver.AI/BuiltInLanguageModelProvider.cs Restores the 8-parameter primary constructor and reintroduces Description as an init property with a prominent binary-compatibility warning.

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

`Description` was added as a ninth primary-constructor parameter (987da5f).
It had a default, so every source-level gate stayed green — but a record's
primary constructor signature is REPLACED, not extended, and a call site that
omits optional arguments still emits a call to the full constructor as it stood
at ITS compile time.

The AI provider modules are compiled separately, live in another repository, and
land at runtime under /data/modules, where — per ModuleLandingService — they
"bind by simple name, and their contract is API compatibility, not build
identity". So the six landed providers kept calling the 8-argument constructor
that no longer existed, and the first type initializer to run aborted the host:

  System.TypeInitializationException: 'MeshWeaver.AI.Anthropic.AnthropicExtensions'
  ---> System.MissingMethodException: Method not found:
       'Void MeshWeaver.AI.LanguageModelCatalogSource..ctor(String, String, Int32,
        String, String, ImmutableArray`1<String>, Boolean, ProviderKind)'

That crash-looped systemorph for 100 minutes across 24 restarts on the roll to
rc8.ci.5567. It read as a CD failure the whole way: the old pods kept serving, so
every health probe stayed green, and helm simply waited on a pod that could never
become ready.

The repair is ADDITIVE. My first cut moved Description to an init property, which
restored the 8-parameter form by REMOVING the 9-parameter one — the same mistake
pointing the other way, and check-record-signatures.py (added 11 hours after the
break it would have caught) correctly refused it. So the ninth parameter stays
and the eight-parameter form is restored beside it: modules compiled before the
change and after it both bind.

The overload has no source-level caller by design — its callers are separately
compiled assemblies that bind by name at runtime — so it is pinned by
LanguageModelCatalogSourceBinaryContractTest rather than by use. That test holds
both arities, agreement between them, and a negative control so it cannot pass
vacuously; verified by mutation in both directions.

Verified: MeshWeaver.AI builds with 0 warnings; MeshWeaver.AI.Test 1305/1305;
check-record-signatures.py --base origin/main clean; all six provider modules in
MeshWeaver.Plugins compile against this core.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@rbuergi
rbuergi force-pushed the fix/ai-catalog-source-binary-compat branch from 272504f to 1375b02 Compare August 26, 2026 08:34
@rbuergi rbuergi changed the title Restore the shipped primary-constructor signature of LanguageModelCatalogSource Restore the 8-parameter constructor of LanguageModelCatalogSource Aug 26, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 3)

    9 files  ±  0      9 suites  ±0   5m 35s ⏱️ +17s
1 839 tests +114  1 835 ✅ +116  4 💤  - 2  0 ❌ ±0 
2 387 runs  +114  2 383 ✅ +116  4 💤  - 2  0 ❌ ±0 

Results for commit 1375b02. ± Comparison against base commit 187f910.

This pull request removes 346 and adds 460 tests. Note that renamed tests count towards both.
MeshWeaver.AI.Test.MarkdownReferenceExtractorTest ‑ ExtractReferences_AgentCommand_IsFiltered
MeshWeaver.AI.Test.MarkdownReferenceExtractorTest ‑ ExtractReferences_AtSignAlone_ReturnsEmpty
MeshWeaver.AI.Test.MarkdownReferenceExtractorTest ‑ ExtractReferences_DirectPathWithDashes_ExtractsFull
MeshWeaver.AI.Test.MarkdownReferenceExtractorTest ‑ ExtractReferences_DirectPathWithDots_ExtractsFull
MeshWeaver.AI.Test.MarkdownReferenceExtractorTest ‑ ExtractReferences_DirectPathWithUnderscores_ExtractsFull
MeshWeaver.AI.Test.MarkdownReferenceExtractorTest ‑ ExtractReferences_DirectPath_ExtractsPath
MeshWeaver.AI.Test.MarkdownReferenceExtractorTest ‑ ExtractReferences_Empty_ReturnsEmpty
MeshWeaver.AI.Test.MarkdownReferenceExtractorTest ‑ ExtractReferences_EndIndex_IsExclusive
MeshWeaver.AI.Test.MarkdownReferenceExtractorTest ‑ ExtractReferences_MixedCommandAndPath_ExtractsOnlyPath
MeshWeaver.AI.Test.MarkdownReferenceExtractorTest ‑ ExtractReferences_ModelCommand_IsFiltered
…
MeshWeaver.AI.Test.LanguageModelNodeTypeTest ‑ Constants_NamespaceAndNodeType_AreStable
MeshWeaver.AI.Test.LanguageModelNodeTypeTest ‑ Provider_EmptyCatalog_EmitsOnlyAccessPolicy
MeshWeaver.AI.Test.LanguageModelNodeTypeTest ‑ Provider_MissingSection_NoCrash_NoNodes
MeshWeaver.AI.Test.LanguageModelNodeTypeTest ‑ Provider_ModelDefinition_CarriesIdProviderOrder
MeshWeaver.AI.Test.LanguageModelNodeTypeTest ‑ Provider_OneSection_OneNodePerModel
MeshWeaver.AI.Test.LanguageModelNodeTypeTest ‑ Provider_Policy_IsPublicRead_WithLiftedWriteCaps
MeshWeaver.AI.Test.LanguageModelNodeTypeTest ‑ Provider_SectionWithEmptyOrWhitespaceModels_SkipsThem
MeshWeaver.AI.Test.LanguageModelNodeTypeTest ‑ Provider_TwoSourcesShareModelId_FirstWins
MeshWeaver.AI.Test.McpAccessDeniedSurfacingTest ‑ Create_WithoutPermission_ReturnsError
MeshWeaver.AI.Test.McpFailureSurfacingTest ‑ Create_BareNode_NoTypeNoContent_ReturnsError
…

@rbuergi

rbuergi commented Aug 26, 2026

Copy link
Copy Markdown
Contributor Author

Why the existing gate didn't catch this

Worth recording, because the repo already had a gate aimed squarely at this failure and it passed the break without a murmur.

Landed modules compile against this PR was added in 3a7f8a4e3 on 2026-08-21 — "A platform change can break the sixteen landed modules, and nothing here saw it" — five days before 987da5f9e. It ran on that PR and went green.

It had to. It builds the modules from source against the PR's core, and the change was source-compatible: Description = null has a default, so every provider module compiles against the 9-parameter record without edit. Nothing to report.

But production never runs that compile. It loads MeshWeaver.AI.Anthropic.dll as built days earlier — Aug 25 01:17, against the 8-parameter record — and that binary calls a constructor the new core no longer has.

The gate measures a different artifact than the one that runs.

Both gates are needed and neither subsumes the other:

gate question it answers sees this break?
Landed modules compile against this PR can the module still be built against new core? no — source-compatible
check-record-signatures.py is the shipped signature still there? yes

The natural experiment on the fleet says the same thing from the other side: memex.meshweaver.cloud runs b53aed0aa — the exact commit of the image systemorph cannot boot — 3/3 healthy, because its Anthropic module was re-landed Aug 26 07:13. systemorph's is Aug 25 01:17. Same bytes in the image, opposite outcomes, one variable: when the module binary was built.

Which is also why the repair here is additive rather than a revert. Removing the ninth parameter would fix every module built before the change and break every one built after it — and on a fleet where instances re-land modules at different times, both populations are live simultaneously. memex-cloud is running the new one right now.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 5)

   10 files  ±0     10 suites  ±0   7m 5s ⏱️ -12s
1 310 tests ±0  1 309 ✅ ±0  1 💤 ±0  0 ❌ ±0 
1 311 runs  ±0  1 310 ✅ ±0  1 💤 ±0  0 ❌ ±0 

Results for commit 1375b02. ± Comparison against base commit 187f910.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 0)

1 206 tests  ±0   1 206 ✅ ±0   17m 43s ⏱️ +53s
    9 suites ±0       0 💤 ±0 
    9 files   ±0       0 ❌ ±0 

Results for commit 1375b02. ± Comparison against base commit 187f910.

@github-actions

github-actions Bot commented Aug 26, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 2)

3 442 tests  ±0   3 442 ✅ ±0   9m 21s ⏱️ -16s
    8 suites ±0       0 💤 ±0 
    8 files   ±0       0 ❌ ±0 

Results for commit 1375b02. ± Comparison against base commit 187f910.

♻️ This comment has been updated with latest results.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 4)

2 123 tests   - 109   1 823 ✅  - 111   13m 34s ⏱️ -16s
    9 suites ±  0     300 💤 +  2 
    9 files   ±  0       0 ❌ ±  0 

Results for commit 1375b02. ± Comparison against base commit 187f910.

This pull request removes 460 and adds 351 tests. Note that renamed tests count towards both.
MeshWeaver.AI.Test.LanguageModelNodeTypeTest ‑ Constants_NamespaceAndNodeType_AreStable
MeshWeaver.AI.Test.LanguageModelNodeTypeTest ‑ Provider_EmptyCatalog_EmitsOnlyAccessPolicy
MeshWeaver.AI.Test.LanguageModelNodeTypeTest ‑ Provider_MissingSection_NoCrash_NoNodes
MeshWeaver.AI.Test.LanguageModelNodeTypeTest ‑ Provider_ModelDefinition_CarriesIdProviderOrder
MeshWeaver.AI.Test.LanguageModelNodeTypeTest ‑ Provider_OneSection_OneNodePerModel
MeshWeaver.AI.Test.LanguageModelNodeTypeTest ‑ Provider_Policy_IsPublicRead_WithLiftedWriteCaps
MeshWeaver.AI.Test.LanguageModelNodeTypeTest ‑ Provider_SectionWithEmptyOrWhitespaceModels_SkipsThem
MeshWeaver.AI.Test.LanguageModelNodeTypeTest ‑ Provider_TwoSourcesShareModelId_FirstWins
MeshWeaver.AI.Test.McpAccessDeniedSurfacingTest ‑ Create_WithoutPermission_ReturnsError
MeshWeaver.AI.Test.McpFailureSurfacingTest ‑ Create_BareNode_NoTypeNoContent_ReturnsError
…
MeshWeaver.AI.Test.LanguageModelCatalogSourceBinaryContractTest ‑ A_constructor_shape_that_was_never_shipped_is_absent
MeshWeaver.AI.Test.LanguageModelCatalogSourceBinaryContractTest ‑ Description_is_settable_positionally_and_through_with
MeshWeaver.AI.Test.LanguageModelCatalogSourceBinaryContractTest ‑ The_eight_parameter_constructor_still_exists
MeshWeaver.AI.Test.LanguageModelCatalogSourceBinaryContractTest ‑ The_eight_parameter_form_agrees_with_the_nine_parameter_one
MeshWeaver.AI.Test.LanguageModelCatalogSourceBinaryContractTest ‑ The_nine_parameter_constructor_still_exists
MeshWeaver.AI.Test.MarkdownReferenceExtractorTest ‑ ExtractReferences_AgentCommand_IsFiltered
MeshWeaver.AI.Test.MarkdownReferenceExtractorTest ‑ ExtractReferences_AtSignAlone_ReturnsEmpty
MeshWeaver.AI.Test.MarkdownReferenceExtractorTest ‑ ExtractReferences_DirectPathWithDashes_ExtractsFull
MeshWeaver.AI.Test.MarkdownReferenceExtractorTest ‑ ExtractReferences_DirectPathWithDots_ExtractsFull
MeshWeaver.AI.Test.MarkdownReferenceExtractorTest ‑ ExtractReferences_DirectPathWithUnderscores_ExtractsFull
…

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 1)

1 819 tests  ±0   1 819 ✅ ±0   9m 17s ⏱️ -17s
   10 suites ±0       0 💤 ±0 
   10 files   ±0       0 ❌ ±0 

Results for commit 1375b02. ± Comparison against base commit 187f910.

@github-actions

github-actions Bot commented Aug 26, 2026 •

Copy link
Copy Markdown
Contributor

Test Results

    55 files  ±0      55 suites  ±0   1h 2m 37s ⏱️ +9s
11 739 tests +5  11 434 ✅ +5  305 💤 ±0  0 ❌ ±0 
12 288 runs  +5  11 983 ✅ +5  305 💤 ±0  0 ❌ ±0 

Results for commit 1375b02. ± Comparison against base commit 187f910.

♻️ This comment has been updated with latest results.

@rbuergi
rbuergi merged commit 4929c90 into main Aug 26, 2026
40 of 42 checks passed
@rbuergi
rbuergi deleted the fix/ai-catalog-source-binary-compat branch August 28, 2026 08:56
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