Skip to content

fix(mesh): issue the agent surface's reads and the editor's write off the router, and ratchet the RECEIVER (#1140) - #4487

Merged
meshweaver-cloud[bot] merged 6 commits into
mainfrom
fix/1140-router-capable-receiver-origin
Sep 16, 2026
Merged

meshweaver-cloud[bot] merged 6 commits into
mainfrom
fix/1140-router-capable-receiver-origin

Conversation

@rbuergi

@rbuergi rbuergi commented Sep 16, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #1140.

The defect, in one paragraph

MeshWeaver.AI.MeshOperations is built over the DI-injected IMessageHub, which in the mesh's root
container is the router — and that is a measurement, not an inference: production named this
exact class on 2026-09-16 01:10:13Z, ROUTER_TRAFFIC ORIGIN: DisposeRequest was POSTED with the mesh hub as sender … at MeshWeaver.AI.MeshOperations+<>c__DisplayClass95_0.<RecycleCore>b__3 (#4463).
#4477 hopped that one line. The ratchet that holds it keys on the message — a post counts when
the framework registers a lifecycle handler for what it carries — but the seam is a property of
the HUB
, so a message-keyed denominator can only ever see the lifecycle slice of any one receiver.
Five sibling exchanges on the identical hub field kept leaving stamped Sender = mesh/{id} with
their replies addressed straight back at mesh/{id}: the UCR read, three content-collection reads
and the script dispatch. One class over, MeshNodeEditor.Move hopped (a lifecycle verb) while the
DataChangeRequest in MeshNodeEditor.Update, ten lines above it on the same field, did not. That
pair — request out as mesh/{id}, reply back at mesh/{id} — is #1140's receiver-side evidence
verbatim, and it reproduces (control 3 below).

#1140's own hypothesis was wrong ("portal hubs not assigning their own address" was never the
mechanism), and the bulk of its 41,091 lines was two things already fixed: the HOP mis-keying, and
the per-node-hub CRUD whose target fell back to mesh/{id}. What remained when those were removed is
what this PR is about.

The denominator I measured, before changing anything

Over src/, by walking every call's receiver as an expression:

.Post/.Observe call sites in src/ 261
…naming an explicit WithTarget(…) (so the router can be an END) 73
…in the 9 files that declare a router-capable receiver 25
before: still on the bare declared receiver 11
after: in-scope deliveries, all through a seam 19 of 19
excluded: self-directed (#981 + one more) · unrelated receiver 2 · 1

Of the 11: eight hopped, two deleted, one removed entirely.

  • Deleted, not hopped: MeshOperations.UpdateViaDataChange and PatchViaDataRequest both posted
    to a node hub off the same field and were reachable from nowhere in the repo (Patch/Update
    persist through mesh.UpdateNode). Hopping dead code is decoration; leaving a router-issuing shape
    in it is a loaded gun for whoever wires it up next.
  • Removed entirely: MeshNodeEditor.Update now writes through
    workspace.GetMeshNodeStream(path).Update(…) — the stream it already READS from — rather than a
    bespoke DataChangeRequest. That cache hub is off the router by construction, so the exchange
    stops existing rather than moving. (Review finding; see the round below.)

Which seam, and why: ReadIssuingHub() for everything the issuing hub does not execute — the work
runs on the TARGET and this hub is only the reply mailbox under a bounded deadline, and
portal/reads-{meshId} registers no handlers, whereas portal/nodeops- would queue those replies
behind every write in flight (#2901's ~10.3 s-then-503). NodeOperationIssuingHub() for the node
MUTATIONS. Both are the identity function for every hub whose address type is not the mesh type,
so the MCP/session/portal callers that dominate these surfaces are byte-for-byte unaffected.

The half that matters more: a RECEIVER-keyed ratchet

RouterAsRouterCapableReceiverRatchetGuard derives its denominator from the receiver. A file that
writes X.NodeOperationIssuingHub() or X.ReadIssuingHub() has declared, in production code,
that X can be the router — nobody writes that about a reference that cannot be, because both seams
are the identity function everywhere else. So every other targeted post on X in that file must hop
too. This is derived rather than listed for the same reason the lifecycle one is: you cannot adopt
the seam for one message on a hub without making the statement
, and it is self-extending — the
moment a new file's first site is hopped, every sibling post on that reference joins the denominator.

The same structural exclusion applies for the same reason — a post naming no target, or only the
receiver's own address, is out because the remedy would misdeliver it.

Both src/ ratchets now read the tree through one shared matcher (RouterOriginScan), so the
syntax-only evasions closed on #4477's review round are closed here by construction rather than by a
second attempt at the same regexes. The lifecycle ratchet still measures 29 sites / 4 excluded —
byte-identical to #4477, which is the evidence the extraction changed nothing.

What neither guard sees, stated rather than implied: a file that has never hopped anything
declares nothing, so it stays invisible to the receiver guard exactly as a non-lifecycle message is
invisible to the message one. The named residue left open on purpose is
JsonSynchronizationStream's new UnsubscribeRequest(reduced.StreamId) — hopping it is not a
no-op, it would change which hub the unsubscribe originates from (which the owner's per-subscriber
bookkeeping reads, while the SUBSCRIBE that pairs with it came from the same hub), so it is a
correlation question rather than a routing one. It is now #4489 rather than a sentence in a doc
page: that issue states the site, why the mechanical hop is wrong, what "done" looks like either
way, and that NEITHER ratchet can see it.

Controls — each armed, shown RED, disarmed, rebuilt, shown GREEN

  1. Revert MeshNodeEditor.Update → the new ratchet reds:
    NEW SITE src/MeshWeaver.Mesh.Contract/Services/MeshNodeEditor.cs (1) …
    MeshNodeEditor.cs:99 posted on 'hub' → newAddress(CurrentPath).

  2. Break the receiver derivation (as a seam rename would) → the ratchet went GREEN over a tree
    carrying that unguarded site
    , and ONLY the derivation test reddened. That is fix(mesh): issue the recycle teardown off the router, and derive the lifecycle guard's denominator (#4463) #4477's recorded
    lesson reproduced, and it is why they are two tests that fail independently.

  3. RUNTIME, because fix(mesh): issue the recycle teardown off the router, and derive the lifecycle guard's denominator (#4463) #4477's review round found both its fixes pinned only by SOURCE: every other
    test of MeshNodeEditor drives it through a session or client hub where the seam is the identity
    function, so a revert leaves them all green. ANodeEditIssuedFromTheRootMeshHub_… drives it from
    the ROOT hub and reads the detector's own output. Reverted, it emits Portal hubs send messages addressed from/to the mesh router hub instead of using their own hub identity #1140's production pair byte
    for byte:

    IsOrigin=True  DataChangeRequest   sender  mesh/WF84…  →  TestData/RouterTrafficEditorProbe
                                               at MeshWeaver.Mesh.Services.MeshNodeEditor.Update (MeshNodeEditor.cs:99)
    IsOrigin=False RawJson             sender  mesh/WF84…  →  TestData/RouterTrafficEditorProbe
    IsOrigin=False DataChangeResponse  target  TestData/RouterTrafficEditorProbe  →  mesh/WF84…
    

    It asserts a positive anchor first (the edit landing), because Update returns without posting
    anything
    when the editor has no snapshot yet.

  4. The premise itself. "Adopting a seam is never a behaviour change" is what makes this a rule
    rather than a judgement call, it is asserted in the guard, both allow files, the doc page and
    every call site that adopts a seam — and nothing measured it.
    BothSeams_AreTheIdentityFunction_ForAHubThatIsNotTheRouter now does, in both directions in one
    method so it cannot pass on a single-direction bug: off the router both seams return the same
    instance
    (reference identity, not address equality — a second hub at the same address is still
    a different actor with a different action block), and ON the router both must return something
    else or the hop is decoration. It also asserts its own precondition (the fixture's client really
    is a non-mesh hub) first. This is what makes the swaps no end-to-end suite reaches — the script
    dispatch, two of the content-collection reads — safe to have made.

🚨 Control 3's first draft drove MeshOperations.Patch instead, and PASSED with the fix reverted.
That false pass is what found the two dead methods — Patch persists through mesh.UpdateNode and
never reaches PatchViaDataRequest. A control that cannot fail is not a control; it is also, here,
the only reason the dead code was found at all.

Review round (55a9718)

All three findings from the automatic review were valid; two were correctness bugs in this
change
, and each fix has its own control. Details in the thread replies.

  1. The local-vs-remote test was wrong. TryResolveUnifiedPath asked whether an address PART was
    written, not where the read goes — but a UCR may address this very hub explicitly, and on that
    input the old spelling hopped the SENDER while leaving the TARGET on this hub: the router became
    the target of a work delivery and a local self-read turned into a routed request the router
    has to EXECUTE. It now compares the RESOLVED address.
  2. A syntax-only evasion in the new ratchet. The declaration regex grabbed the identifier next
    to the seam while the post scan walks the whole expression, so this.hub declared hub and a
    sibling this.hub.Post(…) fell out of the denominator as an unrelated receiver — qualifying a
    receiver walked a violating post straight past the guard. Both sides now use the same Receiver
    walker, with the shape planted as its own case. Control: on the old regex that case reports 0
    violations instead of 1 while the ratchet stays green.
  3. The editor's write was still bespoke — see above. The runtime pin was written to assert the
    OUTCOME rather than the mechanism, so it survived the mechanism changing under it: the edit now
    emits no router-traffic record at all, and reverted to the bespoke post it still reproduces
    Portal hubs send messages addressed from/to the mesh router hub instead of using their own hub identity #1140's pair from the same frame.

Green

MeshWeaver.Documentation.Test 478/478 · MeshWeaver.Graph.Test 1721/1721 ·
Memex.Portal.Shared.Test 1606/1606 · MeshWeaver.Data.Test 526/526. The first three were
re-run in full after the review round.
dotnet build -c Release -warnaserror clean (0 Error(s)) on MeshWeaver.Mesh.Contract,
MeshWeaver.Mesh.Operations, MeshWeaver.Documentation.Test, MeshWeaver.Graph.Test,
MeshWeaver.Data.Test, Memex.Portal.Shared.Test.

Docs and hand-over

Split out as #4489: the stream-unsubscribe residue, with its design question stated rather than
left as prose.

Doc/Architecture/RouterTrafficDetection gains the receiver-keyed denominator, the shared matcher,
the revised #1140 verdict (its hypothesis was wrong; what its bulk actually was; what reproduces),
the dead-code finding, and the one named residue. What's New entry: Agent and editor writes no
longer compete with the portal's routing
(Category: Fix).


Pairs-with: none — this diff removes no public top-level type and no public member from src/. The two deletions (MeshOperations.UpdateViaDataChange, PatchViaDataRequest) are private, so no code outside this assembly — compiled or in-mesh — could name them; everything else is a changed receiver expression plus comments.
Implementers: none — no member is added to a public interface or abstract class.
Mirror-sync: none — no i18n catalog key or value is touched.

🤖 Generated with Claude Code

… the router, and ratchet the RECEIVER (#1140)

#4477 proved in production that `MeshWeaver.AI.MeshOperations`' `hub` field IS the
router (`ROUTER_TRAFFIC ORIGIN … <RecycleCore>b__3`, 2026-09-16 01:10:13Z) and hopped
the one `DisposeRequest` on it. The ratchet that held that keys on the MESSAGE — a
post counts when the framework registers a lifecycle handler for what it carries —
but the seam is a property of the HUB, so it could only ever see the lifecycle slice
of any one receiver. Five sibling exchanges on the identical field kept leaving
stamped `Sender = mesh/{id}` with their replies addressed back at `mesh/{id}`, which
is #1140's receiver-side pair verbatim. One class over, `MeshNodeEditor.Move` hopped
(a lifecycle verb) while the `DataChangeRequest` ten lines above it did not.

Fixed, all through seams that are the identity function off the router:

- `MeshOperations`: the UCR read, three content-collection reads and the script
  dispatch → `ReadIssuingHub()` (the issuing hub executes none of them; it is the
  reply mailbox, and `portal/nodeops-` would queue those replies behind every write
  in flight — #2901).
- `MeshNodeEditor.Update`, and the create path's confirm fan-out, Argument forward
  and post-creation announcement in `MeshExtensions` → `NodeOperationIssuingHub()`.
- `MeshOperations.UpdateViaDataChange` / `PatchViaDataRequest` are DELETED, not
  hopped: both posted to a node hub off the same field and both were reachable from
  nowhere in the repo (`Patch`/`Update` persist through `mesh.UpdateNode`). Hopping
  dead code is decoration; leaving a router-issuing shape in it is a loaded gun for
  whoever wires it up next.

The guard half: `RouterAsRouterCapableReceiverRatchetGuard` derives its denominator
from the RECEIVER. A file that writes `X.NodeOperationIssuingHub()` /
`X.ReadIssuingHub()` has DECLARED that `X` can be the router — nobody writes that
about a reference that cannot be — so every other targeted post on `X` in that file
must hop too. Derived, not listed: you cannot adopt the seam for one message on a hub
without making the statement, and it self-extends as new files adopt it. Both `src/`
ratchets now read the tree through one shared matcher (`RouterOriginScan`), so the
syntax-only evasions closed on #4477's review round are closed here by construction.
The lifecycle ratchet still measures 29 sites / 4 excluded — byte-identical.

Measured over `src/`: 261 `.Post`/`.Observe` call sites, 73 naming an explicit target,
25 of those in the 9 files that declare a router-capable receiver. Before: 11 still on
the bare declared receiver. After: 20 in-scope deliveries, all 20 through a seam; 2
self-directed and 1 on an unrelated receiver, both excluded.

Controls, each armed, shown RED, disarmed, rebuilt, shown GREEN:
1. Revert `MeshNodeEditor.Update` → the new ratchet reds naming
   `MeshNodeEditor.cs:99 posted on 'hub'`.
2. Break the receiver derivation (as a seam rename would) → the ratchet goes GREEN
   over a tree carrying that unguarded site and ONLY the derivation test reds. That
   is #4477's recorded lesson, reproduced.
3. RUNTIME: `MeshNodeEditor.Update` driven from the ROOT hub with the fix reverted
   emits #1140's production pair byte for byte —
   `RawJson has the mesh hub as sender (sender: mesh/…, target: TestData/…)` plus
   `DataChangeResponse … target: mesh/…` — with the origin line naming
   `MeshNodeEditor.Update (MeshNodeEditor.cs:99)`.

🚨 Control 3's FIRST draft drove `MeshOperations.Patch` and PASSED with the fix
reverted — that false pass is what found the two dead methods.

Green: MeshWeaver.Documentation.Test 478/478, MeshWeaver.Graph.Test 1720/1720,
Memex.Portal.Shared.Test 1606/1606, MeshWeaver.Data.Test 526/526.
`dotnet build -c Release -warnaserror` clean (`0 Error(s)`) on MeshWeaver.Mesh.Contract,
MeshWeaver.Mesh.Operations, MeshWeaver.Documentation.Test, MeshWeaver.Graph.Test,
MeshWeaver.Data.Test and Memex.Portal.Shared.Test.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings September 16, 2026 09:19
rbuergi and others added 2 commits September 16, 2026 11:22
"Hop every targeted post off a router-capable receiver" is a RULE rather than a
judgement call for exactly one reason: both seams are the IDENTITY FUNCTION for any
hub whose address type is not the mesh type, so a site already off the router is
byte-for-byte unaffected and only a site that is not is corrected. That premise is
stated in the new ratchet, in both allow files, in the doc page and at every call
site that adopts a seam — and nothing measured it.

It is also what makes the swaps no runtime test reaches safe to make at all: the
script dispatch and two of the content-collection reads have no end-to-end suite, and
every existing test of them runs through a session or client hub. If the seams are the
identity there, those swaps changed nothing; if they are not, every one of those sites
silently moved action block and the claim in the comments is false.

Both directions in one method, so it cannot pass on a single-direction bug: off the
router both seams must hand back the SAME INSTANCE (reference identity, not address
equality — a second hub at the same address is still a different actor with a different
action block), and ON the router both must hand back something else, or the hop is
decoration. The precondition — that the fixture's client really is a non-mesh hub — is
asserted first, so the test cannot pass for the wrong reason.

MeshWeaver.Graph.Test RouterTrafficOnNodeCreateFromTheRootHubTest 6/6.
`dotnet build -c Release -warnaserror` clean (`0 Error(s)`).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… and spell the denominator label

Three tidy-ups on the same change, none of which moves a measurement (the ratchet
reports the identical 23/20/20/2/1 before and after):

- SitesIn masked each file TWICE — once for the declaration pass and once for the
  call pass. It now masks once and derives the declarations from that.
- "SEVEN sibling exchanges" overstated it: FIVE were live, and the other two were
  the dead private methods this change deletes rather than hops. A count that
  includes code nothing calls is the kind of number a reader later cannot reproduce.
- The scan line said "files that declare a router-capable receiver" while counting
  files that BOTH declare one AND post with a target. It is a denominator statement,
  so it says what it counts.

MeshWeaver.Documentation.Test 478/478. dotnet build -c Release -warnaserror clean.

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

The stream-unsubscribe strand is the one site this change deliberately does not fix,
and it was written down in the doc page and nowhere else. A finding that lives only
in prose is invisible to whoever goes looking for open work — so it is now an issue
(#4489) that states the site, why hopping it is NOT a no-op (its sender is what the
owner's per-subscriber bookkeeping reads, and the SUBSCRIBE that pairs with it came
from the same hub), what 'done' would look like either way, and the fact that NEITHER
src/ ratchet can see it: the file declares no router-capable receiver, and
UnsubscribeRequest is not a lifecycle message.

The doc page and the What's New entry now point at it, and the page description says
there are two derived denominators rather than one.

MeshWeaver.Documentation.Test 478/478 (incl. DocumentationLinkIntegrityTest).

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

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.

🟡 Changes recommended

Unresolved moderate findings affect routing correctness, teardown behavior, context propagation, and regression coverage.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

This PR routes agent reads and editor/node operations away from the mesh router and adds source/runtime safeguards.

Changes:

  • Adds issuing-hub seams and updates affected operations.
  • Removes unreachable mutation helpers.
  • Adds shared scanning, ratchets, regression coverage, and documentation.
File summaries
File Summary
test/RouterCapableReceiverOriginSites.allow Documents receiver-ratchet exclusions. Nit (1 vote): update the stale one-site explanation to name both exclusions.
test/MeshWeaver.Graph.Test/RouterTrafficOnNodeCreateFromTheRootHubTest.cs Adds runtime editor-routing regression coverage.
test/MeshWeaver.Documentation.Test/RouterOriginScan.cs Adds shared origin scanning. Moderate (3 votes): use full receiver derivation; Moderate (1 vote): ensure aliases are entirely seam-derived.
test/MeshWeaver.Documentation.Test/RouterAsRouterCapableReceiverRatchetGuard.cs Adds receiver-based enforcement. Moderate (1 vote): return immutable scan results; Nit (1 vote): use immutable failure accumulation; Nit (1 vote): correct the exclusion count.
test/MeshWeaver.Documentation.Test/RouterAsNodeOperationOriginRatchetGuard.cs Integrates the shared lifecycle scanner.
src/MeshWeaver.Mesh.Operations/MeshOperations.cs Routes reads and removes dead helpers. Moderate (2 votes): handle explicitly addressed current-hub UCRs; Moderate (1 vote): resolve the read hub per request during teardown.
src/MeshWeaver.Mesh.Contract/Services/MeshNodeEditor.cs Routes editor updates. Moderate (2 votes): use the canonical stream/cache mutation path with explicit error handling.
src/MeshWeaver.Mesh.Contract/MeshExtensions.cs Updates node-operation fan-out. Moderate (1 vote): explicitly preserve the intended request/system context in the persistence callback.
src/MeshWeaver.Documentation/Data/WhatsNew/2026-09-16-agent-and-editor-writes-no-longer-compete-with-the-portals-routing.md Adds the user-facing fix entry.
src/MeshWeaver.Documentation/Data/Architecture/RouterTrafficDetection.md Documents the receiver ratchet and routing safeguards.
Review details

Suppressed comments (7)

src/MeshWeaver.Mesh.Contract/MeshExtensions.cs:997

  • This callback runs from the persistence Subscribe, so AccessService.Context may no longer contain the original request identity when Post snapshots its options. The new hop therefore can make the confirmation DataChangeRequest fail its update permission and silently drop the fan-out; the adjacent argument-forwarding path explicitly copies request.AccessContext, but this path does not. Carry the intended request/system context explicitly here as well.
                        hub.NodeOperationIssuingHub().Post(DataChangeRequest.Update([resultNode]),
                            o => o.WithTarget(new Address(resultNode.Path)));
                    }

src/MeshWeaver.Mesh.Operations/MeshOperations.cs:107

  • ReadHub is cached for the lifetime of MeshOperations, but ReadIssuingHub() is teardown-sensitive: MeshReadHub() can return null after DisposeHostedHubs, so a first read during teardown can permanently cache the root router, while a hub cached before teardown can be disposed. Subsequent UCR/content/script reads can then target or execute on the router, or use a dead hub. Resolve hub.ReadIssuingHub() per request instead of caching it.
    private IMessageHub ReadHub => readHub ??= hub.ReadIssuingHub();

test/MeshWeaver.Documentation.Test/RouterAsRouterCapableReceiverRatchetGuard.cs:428

  • The new scanner accumulates matched sites in a mutable List<Site>, contrary to the repository's immutable-collection policy. Build an immutable result (for example with an immutable builder and ToImmutable() at the return) instead of returning a mutable accumulator through the IReadOnlyList interface.
    private static IReadOnlyList<Site> SitesIn(string file, string text)

test/MeshWeaver.Documentation.Test/RouterAsRouterCapableReceiverRatchetGuard.cs:146

  • This new ratchet accumulates failures in a mutable List<string>, contrary to the repository's immutable-collection policy. Use an immutable collection (or an immutable builder with an immutable result) for this diagnostic accumulation rather than introducing another mutable list.
            .GroupBy(s => s.File, StringComparer.Ordinal)

test/MeshWeaver.Documentation.Test/RouterAsRouterCapableReceiverRatchetGuard.cs:67

  • This says the receiver guard excludes exactly one self-directed site, but the guard's own production output/documentation records two receiver-side self-directed sites: the #981 inner create and MeshNodeEditor.Move's issuingHub.Address post. Keeping this count at one makes the guard documentation and its measured output disagree; describe both exclusions here.
/// issue the delivery from a DIFFERENT hub, which for a self-directed message sends it somewhere
/// else entirely. A rule whose remedy is nonsense at a site must not report that site. Today that
/// covers exactly one site — the #981 self-targeted inner create inside the

test/MeshWeaver.Documentation.Test/RouterOriginScan.cs:58

  • The RHS pattern permits ? and :, so the production assignment var issuingHub = !string.IsNullOrEmpty(addressPart) ? hub.ReadIssuingHub() : hub is recorded as a seam alias. On its : hub branch, issuingHub.Observe(...) still posts from the root router, but this classifier marks it OffRouter; a sibling regression using the same alias can therefore pass the ratchet. Alias derivation needs to prove every possible assignment is seam-derived, or model the local branch as an explicit structural exclusion.
    internal static readonly Regex SeamAlias =
        new(@"\b([A-Za-z_][A-Za-z0-9_]*)\s*(?:=>|\?\?=|=)\s*[^;{}()]*?"
            + @"(?:NodeOperationIssuingHub|ReadIssuingHub)\s*\(\s*\)", RegexOptions.Compiled);

test/RouterCapableReceiverOriginSites.allow:33

  • The allow-file header repeats that the structural exclusion covers exactly one site, but the receiver guard reports two: the #981 inner create and MeshNodeEditor.Move's self-targeted post. This stale explanation conflicts with the measured denominator and can mislead maintainers about why the second site is excluded; update the header to name both.
# that covers exactly one site — the #981 self-targeted inner create in MeshExtensions, which
# RouterNodeOperationOriginSites.allow documents as not-debt. Do not "fix" it here either.
  • Files reviewed: 10/10 changed files
  • Comments generated: 3
  • Review effort level: Lite

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines 99 to 100
hub.NodeOperationIssuingHub().Post(new DataChangeRequest { Updates = [updated] },
o => o.WithTarget(new Address(CurrentPath)));

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.

Valid, and the right call — done in 55a9718.

Hopping the bespoke post fixed the sender and left the deeper problem you name: this class READS through workspace.GetMeshNodeStream(CurrentPath) and wrote through a second, hand-built path to the same node. Update now writes through that same stream. That is a stronger fix than re-addressing the post, because MeshNodeStreamHandle.Update routes through IMeshNodeStreamCache, whose cache/{meshId} hub is off the router by construction — the router-ended exchange stops existing rather than moving.

Move keeps NodeOperationIssuingHub(), correctly: MoveNodeRequest is node lifecycle and has no stream equivalent.

On the cold observable: subscribed with an error handler that LOGS. Deliberately not pushed into the editor’s node subject — OnError there would terminate the editor’s own live subscription permanently, turning one refused write into a dead editor. Update returns void, so the caller cannot subscribe for us.

Measured across the change: the runtime pin was written to assert the OUTCOME rather than the mechanism, so it survived the mechanism changing under it. With the stream write the root-hub edit now emits no router-traffic record at all (it previously emitted a correctly-addressed one); reverted to the bespoke post it still reproduces #1140’s pair from this exact frame — DataChangeRequest … sender: mesh/{id} → TestData/RouterTrafficEditorProbe at MeshNodeEditor.Update. Its origin-side assertion is now wholesale on that measurement.

Green after the change: MeshWeaver.Graph.Test 1721/1721, Memex.Portal.Shared.Test 1606/1606, MeshWeaver.Documentation.Test 478/478.

// the receiver of a post is what RouterAsRouterCapableReceiverRatchetGuard reads, and a
// local bound from anything but a seam call would make this site invisible to it. One
// GetMeshHub() walk per UCR resolution is the price of staying measurable.
var issuingHub = !string.IsNullOrEmpty(addressPart) ? hub.ReadIssuingHub() : hub;

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.

Valid, and the sharper of the two correctness findings — fixed in 55a9718.

You are right that a non-empty addressPart proves nothing about where the read goes. On an explicitly self-addressed UCR the old spelling hopped the SENDER to portal/reads-{meshId} while leaving the TARGET on this hub, so the router became the target of a work delivery and a local self-read turned into a routed request the router has to EXECUTE — the opposite of the fix, reached by the input I did not think of.

It now compares the RESOLVED address: address.Equals(hub.Address) ? hub : hub.ReadIssuingHub(). That is the same structural exclusion the lifecycle ratchet makes for a self-directed post, stated on the resolved end rather than on the syntax that produced it.

Comment on lines +46 to +48
internal static readonly Regex SeamCallOnReceiver =
new(@"\b([A-Za-z_][A-Za-z0-9_]*)\s*\.\s*(?:NodeOperationIssuingHub|ReadIssuingHub)\s*\(\s*\)",
RegexOptions.Compiled);

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.

Valid — fixed in 55a9718, and it was a real hole rather than a theoretical one.

The declaration and the post have to agree on what "the same receiver" is, and they did not: SeamCallOnReceiver grabbed the identifier next to the seam while Receiver walks the whole primary expression. So this.hub.NodeOperationIssuingHub() declared hub, a sibling this.hub.Post(...) read as this.hub, the two never matched, and the sibling fell out of the denominator as an unrelated receiver. Qualifying a receiver walked a violating post past the ratchet having changed nothing but spelling — the same class of hole the #4477 review round closed for the sibling guard.

The declaration is now read with RouterOriginScan.ReceiverOfSeamCall, which runs the SAME Receiver walker the post scan uses, and the shape is planted as its own named case.

Control: reverting to the old regex, that planted case reports 0 counted violations instead of 1 while the ratchet itself stays green — the evasion, reproduced. The production measurement is unchanged (23 sites / 20 in the denominator / 20 hopped), because no file in src/ currently spells a qualified seam receiver.

@github-actions

github-actions Bot commented Sep 16, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 0)

263 tests  ±0   263 ✅ ±0   1m 47s ⏱️ -22s
  1 suites ±0     0 💤 ±0 
  1 files   ±0     0 ❌ ±0 

Results for commit d6fe304. ± Comparison against base commit 678d974.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Sep 16, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 1)

1 524 tests  +8   1 524 ✅ +8   2m 50s ⏱️ -4s
    2 suites ±0       0 💤 ±0 
    2 files   ±0       0 ❌ ±0 

Results for commit d6fe304. ± Comparison against base commit 678d974.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Sep 16, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 2)

937 tests  +5   937 ✅ +5   5m 54s ⏱️ +39s
  2 suites ±0     0 💤 ±0 
  2 files   ±0     0 ❌ ±0 

Results for commit d6fe304. ± Comparison against base commit 678d974.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Sep 16, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 4)

    4 files  ± 0      4 suites  ±0   5m 14s ⏱️ ±0s
1 972 tests +13  1 780 ✅ +13  192 💤 ±0  0 ❌ ±0 
1 973 runs  +13  1 781 ✅ +13  192 💤 ±0  0 ❌ ±0 

Results for commit d6fe304. ± Comparison against base commit 678d974.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Sep 16, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 5)

    6 files  ±0      6 suites  ±0   5m 43s ⏱️ -5s
2 239 tests ±0  2 239 ✅ ±0  0 💤 ±0  0 ❌ ±0 
2 243 runs  ±0  2 243 ✅ ±0  0 💤 ±0  0 ❌ ±0 

Results for commit d6fe304. ± Comparison against base commit 678d974.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Sep 16, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 3)

1 146 tests  ±0   1 144 ✅ ±0   6m 14s ⏱️ -9s
    3 suites ±0       2 💤 ±0 
    3 files   ±0       0 ❌ ±0 

Results for commit d6fe304. ± Comparison against base commit 678d974.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Sep 16, 2026 •

Copy link
Copy Markdown
Contributor

Test Results

   18 files  ± 0     18 suites  ±0   27m 45s ⏱️ ±0s
8 081 tests +26  7 887 ✅ +26  194 💤 ±0  0 ❌ ±0 
8 086 runs  +26  7 892 ✅ +26  194 💤 ±0  0 ❌ ±0 

Results for commit d6fe304. ± Comparison against base commit 678d974.

♻️ This comment has been updated with latest results.

@meshweaver-cloud
meshweaver-cloud Bot added this pull request to the merge queue Sep 16, 2026
…ange (#1140)

All valid, all acted on, each with its own control.

1. **The local-vs-remote test was wrong** (MeshOperations.TryResolveUnifiedPath). It asked whether
   an address PART was written, not where the read actually goes — but a UCR may address this very
   hub explicitly (`mesh/{id}/$area/…` parses an addressPart that resolves straight back to
   `hub.Address`). On that input the old spelling hopped the SENDER to portal/reads-{meshId} while
   leaving the TARGET on this hub: the router became the TARGET of a work delivery and a local
   self-read turned into a routed request the ROUTER has to EXECUTE — the opposite of the fix.
   It now compares the RESOLVED address against `hub.Address`.

2. **A syntax-only evasion in the new ratchet.** `SeamCallOnReceiver` captured the identifier
   immediately before the seam, so `this.hub.NodeOperationIssuingHub()` declared `hub` while the
   post walker read a sibling `this.hub.Post(…)` as `this.hub`. The two spellings did not match,
   the sibling fell out of the denominator as an "unrelated receiver", and QUALIFYING A RECEIVER
   walked a violating post straight past the guard. The declaration is now read with the SAME
   `Receiver` walker the post scan uses, and the shape is planted as its own case.
   Control: reverting to the old regex, that case reports 0 violations instead of 1 while the
   ratchet itself stays GREEN — the hole, reproduced.

3. **The editor's write was still bespoke.** Hopping `MeshNodeEditor.Update`'s `DataChangeRequest`
   onto the issuing seam fixed the sender and left the deeper problem: this class READS through
   `workspace.GetMeshNodeStream(CurrentPath)` and wrote through a second, hand-built path to the
   same node. It now writes through that same stream, whose `IMeshNodeStreamCache` hub is off the
   router BY CONSTRUCTION — so the exchange stops existing rather than moving, which is a stronger
   fix than re-addressing it. `Move` keeps the seam, correctly: `MoveNodeRequest` is node lifecycle
   and has no stream equivalent. The stream's `Update` is COLD, so it is subscribed with an error
   handler that LOGS — pushing into the editor's `node` subject would terminate its own live
   subscription and turn one refused write into a dead editor.

The runtime control survived the mechanism changing under it, which is the point of pinning the
OUTCOME rather than the mechanism: reverted to the bespoke post it still reproduces #1140's pair
from this exact frame (`DataChangeRequest … sender: mesh/{id} → TestData/RouterTrafficEditorProbe`
at `MeshNodeEditor.Update`), and with the fix the edit now emits NO router-traffic record at all
rather than a correctly-addressed one. Its origin-side assertion is widened to wholesale on that
measurement; the receiver side stays filtered, because a routed payload arrives as RawJson and
cannot be attributed to a caller.

MeshWeaver.Graph.Test 1721/1721. `dotnet build -c Release -warnaserror` clean (`0 Error(s)`) on
MeshWeaver.Mesh.Contract, MeshWeaver.Mesh.Operations, MeshWeaver.Graph.Test and
MeshWeaver.Documentation.Test.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@rbuergi
rbuergi removed this pull request from the merge queue due to a manual request Sep 16, 2026
@meshweaver-cloud

Copy link
Copy Markdown
Contributor

ℹ️ Merge-queue steward: no action — removed from the queue with reason MANUAL — the steward takes no action for this reason.

… no longer uses

The class remark said MeshNodeEditor 'writes via MeshNodeStreamExtensions.UpdateMeshNode'
— which is `[Obsolete]`, and whose obsolete message points at exactly the handle the
class now uses. Before this change set the remark was simply wrong (the write was a
bespoke DataChangeRequest post, not a stream extension at all); after it, it sent the
next reader to a deprecated method to learn what the class does.

It now names MeshNodeStreamHandle.Update, and says why one path for both directions is
the point rather than a detail: a second path to the same node is how two views of it
drift, and it is why the router was ever an end of an edit (#1140).

dotnet build -c Release -warnaserror clean on MeshWeaver.Mesh.Contract (the cref
resolves; a stale one would not compile).

Co-Authored-By: Claude Opus 5 (1M context) <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.

Portal hubs send messages addressed from/to the mesh router hub instead of using their own hub identity

2 participants