Repository navigation
fix(#2229): create-then-point-read of an activity can NotFound-race the router - #2359
Conversation
…he router Item A's audit: three call sites subscribed workspace.GetMeshNodeStream(activityPath) immediately after meshService.CreateNode(activityNode) succeeded, to wait for the just-created progress activity to become live (a cancel watcher in ActivityRunner and ContentIndexingActivity; a routability gate in MarkdownViewLogic's interactive-kernel activation). CreateNode completing proves the node is PERSISTED, not that it is ROUTABLE yet — the read route can answer an authoritative NotFound for a path whose write already landed (#2229 item A: "the create SUCCEEDS and the REPLY is lost"), which TERMINATES the exact-path stream with an error instead of waiting. ActivityRunner's and ContentIndexingActivity's cancel-watch subscribed with no onError handler, so a NotFound there rethrows on whatever thread delivers it. MarkdownViewLogic's Timeout(15s) read handles the error but the interactive kernel then never activates. Fixed all three with the sanctioned composition from CqrsAndContentAccess.md → "An OPTIONAL node": a live children LISTING for existence (empty-on-absent, never errors) gates the point subscribe, which only opens once the index has corroborated the node. Audited ~40 files where an exact-path GetMeshNodeStream/GetMeshNode call co-occurs with an optional-satellite path segment (_Access/_Notification/_Comment/_Approval/_Tracking/_Usage/ _Activity/_Thread) or absence-aware language, out of ~284-298 with-argument call sites across the non-Blazor/non-Portal.Shared surface. Most are already correct (own-node reads, reads of nodes guaranteed to exist by the same reactive chain, or already-fixed listing- gated patterns from earlier #2229 work). See the PR description for the full candidate list, including lower-confidence findings left unfixed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Systematically addresses the #2229 “create-then-point-read” NotFound race for newly-created Activity nodes by gating exact-path stream subscriptions on an existence-confirming query, preventing stream termination/storm-breaker side effects and improving reliability for activity cancellation watchers and interactive markdown kernel startup.
Changes:
- Gate activity cancel watchers in GitSync and content indexing on a query before attaching
GetMeshNodeStream(activityPath), and add anonErrorhandler to avoid unhandledOnErrorrethrows. - Gate markdown-kernel activity routability on a query before opening the activity stream, avoiding the “persisted but not routable yet” window.
- Add a What’s New entry documenting the user-visible reliability fix.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| src/MeshWeaver.Markdown/MarkdownViewLogic.cs | Gates newly-created markdown activity stream subscription on a query-backed existence check before posting kernel submissions. |
| src/MeshWeaver.GitSync/ActivityRunner.cs | Gates the cancel watcher’s activity stream subscription on a query before attaching and logs failures via onError. |
| src/MeshWeaver.ContentCollections.Indexing.Graph/ContentIndexingActivity.cs | Same cancel-watch gating pattern as GitSync for indexing activities, plus onError handling. |
| src/MeshWeaver.Documentation/Data/WhatsNew/2026-08-26-activity-notfound-race-fix.md | Documents the fix as a user-visible reliability improvement. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
|
Holding this on green intentionally — do not rerun or merge past the current CI red; it is not caused by this PR.
All three fail on Root cause: the GUI extraction landed its additive half (plugins #674) while the deletion half This PR's own CI will hit the identical red once it reaches that job — expected, not a regression |
Test Results (shard 3) 9 files 9 suites 4m 43s ⏱️ Results for commit d6d3b82. ♻️ This comment has been updated with latest results. |
Test Results (shard 0)593 tests 589 ✅ 15m 48s ⏱️ Results for commit d6d3b82. ♻️ This comment has been updated with latest results. |
Test Results (shard 5) 9 files 9 suites 7m 0s ⏱️ Results for commit d6d3b82. ♻️ This comment has been updated with latest results. |
Test Results (shard 1)2 046 tests 1 853 ✅ 7m 57s ⏱️ Results for commit d6d3b82. ♻️ This comment has been updated with latest results. |
Test Results (shard 4)2 064 tests 2 061 ✅ 13m 49s ⏱️ Results for commit d6d3b82. ♻️ This comment has been updated with latest results. |
Test Results (shard 2)2 831 tests 2 728 ✅ 9m 25s ⏱️ Results for commit d6d3b82. ♻️ This comment has been updated with latest results. |
Test Results 53 files 53 suites 58m 45s ⏱️ Results for commit d6d3b82. ♻️ This comment has been updated with latest results. |
…th, not a children listing
Copilot's review on all three sites was correct, and for a sharper reason than the O(N)
cost it named: a `scope:children` listing of the whole `_Activity` container can MISS the
target activity, not merely cost more to check. `_Activity` is explicitly un-pruned, so the
listing grows without bound, and once a partition/owner has enough activities the new one
can land off the query's first page — reintroducing the exact false "not there yet" this PR
was written to remove, on a rarer and harder-to-diagnose route.
Switched to an exact-path query in all three: `path:{activityPath} nodeType:Activity` with
no `scope:` qualifier. That's `QueryScope.Exact`, whose documented contract for an absent
path (QueryParser.cs) is "answered ZERO ROWS with no error" — so it keeps the
empty-on-absent property the fix depends on, while being incapable of paging past the one
row it can return. This is the identical shape `PackageInstaller.WaitForGating` already
uses, and its own comment names the same paging hazard.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Summary
Issue #2229 item A stayed open after #2244/#2267 because the fix landed for exactly one caller
(
PackageInstaller.WaitForGating) and the issue explicitly asked for a systematic audit ofevery other exact-path
GetMeshNodeStream(path)read of a node that may not exist yet — nevera mass-conversion, only genuine, evidenced candidates.
This PR is that audit's first pass: it fixes three call sites that share the exact defect
shape described in
Doc/Architecture/CqrsAndContentAccess.md→ "An OPTIONAL node", and reports(without changing) a few lower-confidence findings for anyone continuing the audit.
The defect, in these three call sites
ActivityRunner.RunActivity,ContentIndexingActivity.Run, andMarkdownViewLogic.CreateActivityAndSubmitall follow the same shape:
meshService.CreateNode(activityNode)succeeds, and — in the very samereactive chain — a point subscribe (
workspace.GetMeshNodeStream(activityPath)) opens immediately towatch for cancel (the first two) or to confirm the kernel activity became routable before posting
code submissions (the third).
CreateNodecompleting proves the node is persisted, not that it is routable yet. Per#2229 item A's read-side findings, the read route can answer an authoritative
NotFoundfor a pathwhose write already landed — sometimes for minutes under load. An exact-path
GetMeshNodeStreamhitting that
NotFoundterminates the stream with an error rather than waiting, and (perMeshNodeStreamCache's storm-breaker) that error can suppress subsequent writes to the same pathfor a backoff window — i.e. the activity's own later progress/completion writes.
Concretely:
ActivityRunner's andContentIndexingActivity's cancel-watch subscribed with noonErrorhandler — an unhandled
OnErrorrethrows on whatever thread is delivering it.MarkdownViewLogic'sTimeout(15s)read does handle the error, but the interactive kernel thensimply never activates for that render (already-logged, but silent to the user beyond "Starting
interactive kernel…" never resolving).
The fix
Updated after review (see the resolved threads below): the first version of this PR gated the
point subscribe on a
scope:childrenlisting of the whole_Activitycontainer. Copilot correctlyflagged that as expensive; on reflection it's worse than expensive —
_Activityis explicitlyun-pruned (
StaticRepoImport.md), so a listing anchored at the container can page past the veryactivity it's looking for once the partition has enough of them. That is the identical false
"not there yet" this PR exists to remove, reintroduced by a rarer, harder-to-diagnose route.
All three now use an exact-path query instead —
meshService.Query<MeshNode>("path:{activityPath} nodeType:Activity"), noscope:qualifier and noselect:projection.path:{x}with noscope:is
QueryScope.Exact, documented inQueryParser.csas "answered ZERO ROWS with no error" for anabsent path — empty-on-absent, no routing
NotFound, no storm-breaker window, exactly like alisting, but it can return at most one row so it can never page past the target. Gate on
change.Items.Count > 0, then.SelectManyinto the point subscribe once corroborated. This is theidentical shape
PackageInstaller.WaitForGatingalready uses (and its own comment names the samepaging hazard) — see
src/MeshWeaver.PluginCatalog/PackageInstaller.csaround lines 490-520.Audit scope and confidence
GetMeshNodeStream(/GetMeshNode(call sites acrosssrc/,memex/(excluding
Memex.Portal.Shared), andtools/(excludingMeshWeaver.Blazor*— under concurrentrestructuring per this session's constraints): 299 call sites in
.cs(my own count landed at~284–298 by a slightly different filter — close enough to confirm the two counts agree). The
issue's originally-cited "560 across 216 files" included 261 markdown-prose matches under
src/MeshWeaver.Documentation/Data/**(API docs describing the pattern, not call sites) — 299 isthe correct code-only figure.
(
_Access,_Notification,_Comment,_Approval,_Tracking,_Usage,_Activity,_Thread) or absence-aware language (may not exist,not yet routable,does not exist,absent, …) in the same file — ~37 files — and read each one's surrounding code.ActivityRunner.cs,ContentIndexingActivity.cs,MarkdownViewLogic.cs—see above.
PackageInstaller.cs(fixed in Read an optional node with a listing AND a stream, not one or the other #2244/571707c3c),StaticRepoImporter.cs(EnsureRootalready gates its point read on a query snapshot; themanifest-existence checks already use the non-erroring
hub.GetMeshNodeprimitive),MeshExtensions.cs's partition-bootstrap reads (rawIStorageAdapter.Read+.Catch, notGetMeshNodeStream— never errors),TokenUsage.cs(already migrated off a point read per its owndoc comment),
AiSettingsNodeType.cs,GitHubSyncService.cs,MeshNodeExtensions.cs,AgentChatClient.cs's provider-selection read (already query-based per its own comment),NodeTypeCompilationActivity.cs(already sequences create-before-emit so nothing point-reads thenew activity),
EventSubscriptionOps.cs,BuildProtocolDriver.cs'sOpenChunk/FinishActivity(same activity-create shape, but a real bake runs between create and the later
.Update, so therace window is materially smaller than the three fixed sites' same-tick subscribe — flagging
rather than changing).
StaticRepoImporter.ReadClaimedRootspoint-reads each partition root directly (no listing gate),but wraps the whole thing in
.Catchthat degrades tonull— so a race here costs at most oneextra re-sync pass, not a crash or suppressed write. Lower severity; left as-is.
IssueService.WatchIssue/ (likely) the analogousPullRequestServicemethod: their doc commentspromise "or null when absent", but an exact-path
GetMeshNodeStreamactually errors onabsence rather than emitting null — a doc/behavior mismatch. Grep found no non-test caller in
this repo (test-only), so I did not change it; worth a look if a production caller appears.
Honest exhaustiveness statement: I audited by keyword/pattern co-occurrence, not line-by-line
across all 299 call sites — consistent with the issue's own "this is judgment, not grep" framing. 37
of 299 files were triaged by the satellite-segment/absence-language predicate. I'm confident in the
three fixes and in the "already correct" list I actually read. I cannot claim the audit is
exhaustive; issue #2229 should stay open for whoever picks up the remaining ~262 sites outside this
pattern-driven sample (or narrows the search further).
Verification
dotnet build -c Release -warnaserrorclean onMeshWeaver.GitSync,MeshWeaver.ContentCollections.Indexing.Graph,MeshWeaver.Markdown..trx):GitHubActivityTest(4/4, includingRunActivity_Cancel_TripsCommandTokenAndEndsCancelled)ContentIndexingPipelineTest(3/3)KernelOwnerAnchorIntegrationTest/KernelSubscribeGateTest/KernelSubscribeGateIntegrationTest(6/6)WhatsNewEntryIntegrityTest,DocumentationLinkIntegrityTest(1/1 each)Does not close #2229 — the audit is not exhaustive (see above).
🤖 Generated with Claude Code