Skip to content

GH-4825: construct the internal agent messages' routers directly for Native AOT - #4842

Merged
jeremydmiller merged 4 commits into
mainfrom
gh-4825-root-internal-agent-message-routers
Oct 6, 2026
Merged

jeremydmiller merged 4 commits into
mainfrom
gh-4825-root-internal-agent-message-routers

Conversation

@jeremydmiller

Copy link
Copy Markdown
Member

The one part of @chrisbbe's #4827 that main was still missing after #4828 merged, plus six more types their list did not reach. Credited as co-author on the commit.

The bug

PrepopulateRoutingCache walks every message type the handler graph discovered, and RoutingFor's cache-miss path closes MessageRouter<T> / EmptyMessageRouter<T> over each one reflectively.

An application with a durable message store has the framework's own agent messages in that set — and they are internal, so HandlerRegistryCodeFile.onlyPublic() drops them and the emitted [DynamicDependency] rooting block cannot name them: generated code cannot put a non-public type inside a typeof(). In a native image ILC has therefore trimmed exactly those instantiations, and a Balanced-mode durable app dies at startup.

Direct construction in _frameworkRouterFactories is the only fix available — which is what #4287 already did for IAgentCommand itself.

Worth noting why the Marten AOT lane stays green without this: it runs DurabilityMode.Solo, where agent messaging is not used. A clustered durable app is where this bites, and no lane in CI is one.

Coverage, not a list — and that is not a formality

#4827 named eight types. I wrote the test as coverage of every internal concrete IAgentCommand / IDeferredAgentWork in the assembly instead, and it immediately failed on six more:

AssignAgent, AssignAgents, ReassignAgent, ReassignAgents, StopRemoteAgent, StopRemoteAgents

Those are the Balanced-mode leader's own assignment messages, built in NodeAgentController.EvaluateAssignments and AssignmentGrid.Agent — precisely what a clustered durable app routes. The eight in #4827 were the ones that application's lane happened to reach.

All fourteen are covered now, and adding a new agent message fails the test until its factory exists. The test carries its own vacuity guards: the scan must find something, and every type it finds must really be non-public — which is the reason none of them can be rooted from generated code.

On #4827

That PR overlapped #4828 almost entirely, and I merged #4828 without seeing it — my mistake, explained on the PR. It now conflicts with main in seven files, so this lands the missing part rather than asking @chrisbbe for a wholesale rebase. The two remaining places where their version is better than what shipped are tracked as #4840 (the side-effect interface filter, and an IL2037 hazard the current emitter can still produce) and #4841 (the EndpointMetadataProviderTypes ordering assumption, which I filed against my own merged choice).

Gates

  • wolverine.slnx Release -f net9.0: 0 errors, 0 warnings
  • CoreTests 3289 green, including the new framework_router_factories_cover_every_agent_message

🤖 Generated with Claude Code

https://claude.ai/code/session_01VDUrBeB4tTnKj4AExCS1nj

…Native AOT

PrepopulateRoutingCache walks every message type the handler graph discovered, and
RoutingFor's cache-miss path closes MessageRouter<T> / EmptyMessageRouter<T> over
each one reflectively. An application with a durable message store has the
framework's own agent messages in that set -- and they are internal, so
HandlerRegistryCodeFile.onlyPublic() drops them and the emitted [DynamicDependency]
rooting block CANNOT name them: generated code cannot put a non-public type inside a
typeof(). In a native image ILC has therefore trimmed exactly those instantiations,
and a Balanced-mode durable app dies at startup.

Direct construction in _frameworkRouterFactories is the only fix available, which is
what GH-4287 already did for IAgentCommand itself.

Reported and diagnosed by @chrisbbe in #4827, which is credited as co-author here.
That PR overlapped #4828 almost entirely and conflicts with main in seven files
after it merged, so this lands the one part main was still missing rather than
asking for a wholesale rebase. #4840 and #4841 track the two remaining places where
#4827's version is the better one.

Held to COVERAGE of the real set rather than to a list, and that is not a
formality: #4827 named eight types -- the ones its lane happened to reach -- and the
coverage test immediately found SIX MORE with the identical defect (AssignAgent,
AssignAgents, ReassignAgent, ReassignAgents, StopRemoteAgent, StopRemoteAgents), the
Balanced-mode leader's own assignment messages built in
NodeAgentController.EvaluateAssignments and AssignmentGrid. All fourteen are covered
now, and adding a new agent message fails the test until its factory exists.

The test carries its own vacuity guard: the scan must find something, and every type
it finds must really be non-public, which is the reason none of them can be rooted
from generated code.

Gates: wolverine.slnx Release -f net9.0 0 errors / 0 warnings; CoreTests 3289 green.

Co-Authored-By: chrisbbe <chrisbbe@users.noreply.github.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VDUrBeB4tTnKj4AExCS1nj
jeremydmiller and others added 3 commits October 5, 2026 18:51
…merged

Two lines from @chrisbbe's #4827 that main lost:

 - The header still said "This project carries NO hand-written roots, so the lane
   fails until the library itself is fixed." That was true when #4826 landed the lane
   red; the library was fixed in #4828 and the lane passes, so the sentence now reads
   as a standing excuse for a red lane.
 - It enumerated three startup failures. There were four.

The fourth note also records what the lane does NOT cover, which is the more useful
half: this lane runs Solo, where agent messaging is never used, so it cannot
reproduce the failure the rest of this branch fixes. GH-4843 tracks that gap.

Co-Authored-By: chrisbbe <chrisbbe@users.noreply.github.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VDUrBeB4tTnKj4AExCS1nj
My previous commit put a literal "--" in the csproj's comment block, which makes
MSBuild refuse to load the project at all (MSB4025). Every project referencing it
fails, which is to say the whole solution.

I pushed it because the verification command was `dotnet build ... | grep -E "error|
Build succeeded" && git commit`, and grep SUCCEEDS when it finds the word "error" --
so the && ran on a failed build. Check the build's own exit code, not whether a grep
matched something.

Gates, properly this time: the project loads, and wolverine.slnx Release -f net9.0
is 0 errors / 0 warnings.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VDUrBeB4tTnKj4AExCS1nj
@jeremydmiller
jeremydmiller merged commit 72d86fa into main Oct 6, 2026
45 checks passed
jeremydmiller added a commit that referenced this pull request Oct 6, 2026
…he HTTP rooting order, run a Balanced native lane (#4847)

GH-4840: SideEffectAotRoots roots the interfaces that DECLARE Execute/ExecuteAsync,
which is the test findMethod itself applies, instead of the castability proxy. Two
shapes where the answers differ are now tests. The IL2037 question is measured: the
Fisher lane returns a member-less marker side-effect interface and publishes with zero
ILC warnings, so no member-less filter is needed.

That measurement found a real bug: HandlerChain.PublishedTypes() listed side-effect
return types, so PrepopulateRoutingCache closed EmptyMessageRouter<T> over an
application-declared side-effect interface at startup and the native image died.
A side effect is never routed; PublishedTypes() now skips INotToBeRouted types.

GH-4841: the recorded EndpointMetadataProviderTypes form is kept, and its ordering
assumption is loud: under codegen write, HttpGraph.BuildFiles refuses by chain name
while any endpoint is unbuilt. Static attach is unaffected.

GH-4843: the Marten native lane runs DurabilityMode.Balanced and waits for leadership
and the durability agent assignment. Measured limit: with the fourteen #4842 router
factories stripped, the single node still boots on net9.0 and net10.0, so the lane
covers the clustered path without reproducing that failure.


Claude-Session: https://claude.ai/code/session_017DbTkhs3JxSMCWasTzPdRq

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
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.

1 participant