Repository navigation
fix(mesh): issue the agent surface's reads and the editor's write off the router, and ratchet the RECEIVER (#1140) - #4487
Conversation
… 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>
"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>
There was a problem hiding this comment.
🟡 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, soAccessService.Contextmay no longer contain the original request identity whenPostsnapshots its options. The new hop therefore can make the confirmationDataChangeRequestfail its update permission and silently drop the fan-out; the adjacent argument-forwarding path explicitly copiesrequest.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
ReadHubis cached for the lifetime ofMeshOperations, butReadIssuingHub()is teardown-sensitive:MeshReadHub()can return null afterDisposeHostedHubs, 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. Resolvehub.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 andToImmutable()at the return) instead of returning a mutable accumulator through theIReadOnlyListinterface.
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'sissuingHub.Addresspost. 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 assignmentvar issuingHub = !string.IsNullOrEmpty(addressPart) ? hub.ReadIssuingHub() : hubis recorded as a seam alias. On its: hubbranch,issuingHub.Observe(...)still posts from the root router, but this classifier marks itOffRouter; 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.
| hub.NodeOperationIssuingHub().Post(new DataChangeRequest { Updates = [updated] }, | ||
| o => o.WithTarget(new Address(CurrentPath))); |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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.
| internal static readonly Regex SeamCallOnReceiver = | ||
| new(@"\b([A-Za-z_][A-Za-z0-9_]*)\s*\.\s*(?:NodeOperationIssuingHub|ReadIssuingHub)\s*\(\s*\)", | ||
| RegexOptions.Compiled); |
There was a problem hiding this comment.
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.
…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>
|
ℹ️ Merge-queue steward: no action — removed from the queue with 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>
Fixes #1140.
The defect, in one paragraph
MeshWeaver.AI.MeshOperationsis built over the DI-injectedIMessageHub, which in the mesh's rootcontainer 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
hubfield kept leaving stampedSender = mesh/{id}withtheir replies addressed straight back at
mesh/{id}: the UCR read, three content-collection readsand the script dispatch. One class over,
MeshNodeEditor.Movehopped (a lifecycle verb) while theDataChangeRequestinMeshNodeEditor.Update, ten lines above it on the same field, did not. Thatpair — request out as
mesh/{id}, reply back atmesh/{id}— is #1140's receiver-side evidenceverbatim, 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 iswhat this PR is about.
The denominator I measured, before changing anything
Over
src/, by walking every call's receiver as an expression:.Post/.Observecall sites insrc/WithTarget(…)(so the router can be an END)Of the 11: eight hopped, two deleted, one removed entirely.
MeshOperations.UpdateViaDataChangeandPatchViaDataRequestboth postedto a node hub off the same field and were reachable from nowhere in the repo (
Patch/Updatepersist through
mesh.UpdateNode). Hopping dead code is decoration; leaving a router-issuing shapein it is a loaded gun for whoever wires it up next.
MeshNodeEditor.Updatenow writes throughworkspace.GetMeshNodeStream(path).Update(…)— the stream it already READS from — rather than abespoke
DataChangeRequest. That cache hub is off the router by construction, so the exchangestops existing rather than moving. (Review finding; see the round below.)
Which seam, and why:
ReadIssuingHub()for everything the issuing hub does not execute — the workruns on the TARGET and this hub is only the reply mailbox under a bounded deadline, and
portal/reads-{meshId}registers no handlers, whereasportal/nodeops-would queue those repliesbehind every write in flight (#2901's ~10.3 s-then-503).
NodeOperationIssuingHub()for the nodeMUTATIONS. 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
RouterAsRouterCapableReceiverRatchetGuardderives its denominator from the receiver. A file thatwrites
X.NodeOperationIssuingHub()orX.ReadIssuingHub()has declared, in production code,that
Xcan be the router — nobody writes that about a reference that cannot be, because both seamsare the identity function everywhere else. So every other targeted post on
Xin that file must hoptoo. 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 thesyntax-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'snew UnsubscribeRequest(reduced.StreamId)— hopping it is not ano-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
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).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.
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
MeshNodeEditordrives it through a session or client hub where the seam is the identityfunction, so a revert leaves them all green.
ANodeEditIssuedFromTheRootMeshHub_…drives it fromthe 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:
It asserts a positive anchor first (the edit landing), because
Updatereturns without postinganything when the editor has no snapshot yet.
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_ForAHubThatIsNotTheRouternow does, in both directions in onemethod 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.Patchinstead, and PASSED with the fix reverted.That false pass is what found the two dead methods —
Patchpersists throughmesh.UpdateNodeandnever 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.
TryResolveUnifiedPathasked whether an address PART waswritten, 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.
to the seam while the post scan walks the whole expression, so
this.hubdeclaredhuband asibling
this.hub.Post(…)fell out of the denominator as an unrelated receiver — qualifying areceiver walked a violating post straight past the guard. Both sides now use the same
Receiverwalker, 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.
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.Test478/478 ·MeshWeaver.Graph.Test1721/1721 ·Memex.Portal.Shared.Test1606/1606 ·MeshWeaver.Data.Test526/526. The first three werere-run in full after the review round.
dotnet build -c Release -warnaserrorclean (0 Error(s)) onMeshWeaver.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/RouterTrafficDetectiongains 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) areprivate, 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