Agent tools issue their requests off the mesh router

The hub an agent plugin is handed is not necessarily a hub it may post work from. ChatClientAgentFactory builds its built-in plugins — ContentCollectionPlugin, CollaborationPlugin, MeshPlugin, VersionPlugin, … — with its own DI-injected IMessageHub. On a portal that is the root mesh hub, which is the mesh's ROUTER. A tool that calls hub.Observe(request, o => o.WithTarget(owner)) on it makes the router an END of the delivery in both directions:

The second line names the innocent answering half. Nothing there changes: moving the requester removes both lines.

The rule

A plugin method that issues request/response work issues it on hub.NodeOperationIssuingHub() (core, MeshExtensions), never on the bare injected hub. The seam is the identity function for every hub that is not the router, so a plugin built on a session or portal hub keeps its identity byte for byte; on the router it returns the mesh's off-router execution hub, which is routing-registered (replies land on it cross-silo) and shares the mesh's type registry, so a response type registered there by AddAITypes — such as SaveContentResponse — is read exactly as before.

Reads that go through ChunkNavigation or MeshOperations already pick their own seam; the rule is about a plugin that posts a request itself.

Where it was met

ContentCollectionPlugin.UploadContent posted its SaveContentRequest on the injected hub, and production showed the pair — request with the router as sender, SaveContentResponse from AIExtensions.HandleSaveContent with the router as target (Systemorph/MeshWeaver#6647, #6646). CollaborationPlugin.PostAndReport (comment and suggested-edit requests) had the identical shape and moved with it.

The detector prints its line once per (role, message type) per hub lifetime, so one production sighting says nothing about how often a tool posted this way.

The test

MeshWeaver.AI.Test.ContentUploadIssuedOffTheRouterTest builds the plugin on Mesh — the factory's portal shape — runs UploadContent, requires the owner's own verdict to come back (so the exchange really happened), and asserts the origin detector recorded neither half. Its positive control observes the same SaveContentRequest on the root hub and requires the capture to record it; without that control, "no record" and "the capture never saw the detector" read the same. With the fix reverted, the measurement fails on the SaveContentRequest … sender record.