Repository navigation
fix(#2463): a PARTIAL merge refusal is still a refusal — stop acking it Success - #2840
Merged
Merged
Conversation
…it Success `ApplyMeshNodeMerge` returns how many keys the three-way merge refused, and both callers NACK Conflict only when `DeepEquals(preMergeNode, currentNode)` — i.e. only when NOTHING landed. A patch where some fields applied and others were refused falls straight through to the success path. So the caller is told its whole write landed when half of it is on the floor. That is the #648 acked-write-loss in its partial form, and it is the harder one to see: the node really did change, so every "did it commit?" question answers yes. The existing pin only covers the all-refused case precisely because that one leaves the node byte-identical. It has a name in the wild. RolePlay/Scenery compiled fine; the mesh phase patched four compile-outcome fields; MergeGuard refused all four as stale/reordered while another field in the same patch landed; this path acked Success; `IsDirty` never converged; and the gate read a status that had never been written and called it a compile failure — a false RED on the required check, from a write that reported itself applied. The fix NACKs Conflict on any refusal and LETS THE COMMIT PROCEED. AckOnce latches, so the caller learns either way. The fields that did not conflict were written against a current base and are valid — rolling them back would convert a reportable conflict into real data loss. The caller re-reads and re-applies; what already landed re-diffs to a no-op, and Conflict is one of the provably-safe retried codes with a bounded budget, so this cannot spin. Mutation-proven both directions. With the fix reverted behind a runtime-false condition the compiler cannot fold: PartiallyRefusedThreeWayMerge_IsNackedConflict_AndKeepsWhatDidNotConflict [FAIL] Expected value to be false … but found True Restored: green. The test also pins that Description ("landed") survives, so a future fix cannot satisfy the guard by discarding the valid half. Verified: MeshWeaver.Hosting.Test 269/269, the conflict-path suites (SupersededActivationPatchNack, StoreLevelWriteConflict, ActivityWriteSurvivesConflict) 5/5, Release -warnaserror clean. 🚨 No timing claim. The change adds one integer comparison to the common path — `refusedKeys` is already computed and the extra ack only fires when it is non-zero — so a wall-clock A/B would measure noise for a structurally null result. That reasoning, not a measurement, is the cost argument here. Fixes #2463 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014FhP7Jg65q8cAATnFfeaTK
Contributor
There was a problem hiding this comment.
Pull request overview
This PR fixes owner-side ACK semantics for cross-hub MeshNode three-way merges so that any refused keys (including partial refusals) are NACKed as Conflict rather than incorrectly ACKed as Success, preventing “acked-success write loss” where callers stop retrying refused fields.
Changes:
- NACK
Conflictwhen a three-way merge refuses any fields (even if other fields applied), instead of only when the merge resulted in no net change. - Add a deterministic regression test that asserts (a) partial refusal returns
Conflict, and (b) non-conflicting fields still persist.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
src/MeshWeaver.Data/DataExtensions.cs |
Adjusts PatchDataRequest handling to NACK Conflict on partial merge refusal in both relevant code paths. |
test/MeshWeaver.Hosting.Monolith.Test/SupersededActivationPatchNackTest.cs |
Adds a new pin test covering the partial-refusal scenario and asserting “kept non-conflicting fields” behavior. |
Suppressed comments (1)
src/MeshWeaver.Data/DataExtensions.cs:1689
- The Conflict response text asserts that non-conflicting fields were kept, but this AckOnce is emitted before the commit/flush completes; if a later apply/flush error occurs, that claim can be inaccurate. Consider wording that doesn’t imply successful persistence.
$"cross-hub write PARTIALLY refused: {refusedKeys} field(s) changed on "
+ "the owner since the writer's base. What did not conflict was kept — "
+ "re-read and re-apply so the refused field(s) converge."));
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Comment on lines
+1310
to
+1312
| $"cross-hub write PARTIALLY refused: {refusedKeys} field(s) changed on " | ||
| + "the owner since the writer's base. What did not conflict was kept — " | ||
| + "re-read and re-apply so the refused field(s) converge.")); |
Contributor
Contributor
Contributor
Contributor
Contributor
Contributor
Contributor
rbuergi
added this pull request to the merge queue
Aug 30, 2026
github-merge-queue
Bot
removed this pull request from the merge queue due to failed status checks
Aug 30, 2026
rbuergi
added this pull request to the merge queue
Aug 30, 2026
This was referenced Aug 31, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The defect
ApplyMeshNodeMergereturns how many keys the owner's three-way merge refused. Both callers act on it only inside this branch:A patch where some fields applied and others were refused never reaches it — the node changed, so
DeepEqualsis false — and falls straight through to the success path.So the caller is told its whole write landed when half of it is on the floor. That is the #648 acked-write-loss in its partial form, and it is the harder one to see: the node really did change, so every "did it commit?" question answers yes. The existing pin covers only the all-refused case precisely because that one leaves the node byte-identical.
It has a name in the wild
#2463, verbatim from the run:
RolePlay/Scenerycompiled fine (ok … 4 source(s)), the mesh phase patched four compile-outcome fields,MergeGuardrefused all four as stale/reordered while another field in the same patch landed — so this path acked Success,IsDirtynever converged, and the gate read a status that had never been written and called it a compile failure.A false RED on the required check, produced by a write that reported itself applied.
The fix, and the arguable part
NACK
Conflicton any refusal, and let the commit proceed.AckOncelatches onInterlocked.Exchange, so the caller learns either way.Letting the commit stand is deliberate: the fields that did not conflict were written against a current base and are valid. Rolling them back would convert a reportable conflict into real data loss. The caller re-reads and re-applies; what already landed re-diffs to a no-op, and
Conflictis one of the three provably-safe retried codes with a bounded budget, so this cannot spin.The test pins that half explicitly —
Descriptionmust still read"landed"— so a future change cannot satisfy the guard by discarding the valid fields.Mutation-proven, both directions
With the fix reverted behind a runtime-false condition the compiler cannot fold (
&& Environment.TickCount < 0), so--no-buildcannot run stale binaries:The owner really does ack
Success = true. Restored: green.Verification
MeshWeaver.Hosting.Test269/269 (whole project — it ownsConflictRetryPolicyTest, the caller-side path this change routes more traffic into).SupersededActivationPatchNack+StoreLevelWriteConflict+ActivityWriteSurvivesConflict: 5/5.-warnaserrorclean.🚨 No timing claim
A peer rightly asked for a cost measurement, having had two fixes refuted by wall-clock tonight. I am not offering one, because the honest answer is that it would measure noise: the change adds one integer comparison to the common path —
refusedKeysis already computed, and the extra ack only fires when it is non-zero. There is no new work on any path that was previously fast. The structural argument is the cost argument here, and I would rather say that than dress up a contended single-sided run as evidence.Fixes #2463