Skip to content

fix(blazor): cure the render-storm circuit wedge (re-bind guard) + surface bind errors - #151

Merged
rbuergi merged 10 commits into
mainfrom
fix/render-storm-rebind-guard
Jul 2, 2026
Merged

rbuergi merged 10 commits into
mainfrom
fix/render-storm-rebind-guard

Conversation

@rbuergi

@rbuergi rbuergi commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Problem

The recurring "chat freeze"/wedge (#100): the Blazor circuit pegs to multiple cores with no data write in flight — a pure client-side render storm.

Root cause

BlazorView.OnParametersSet re-disposed + re-subscribed all data bindings on every parent render. The sync stream is ReplaySubject-backed, so each re-subscribe replays → StateHasChanged → the parent re-renders → OnParametersSet → … a self-sustaining loop.

Fix (client-side only — does NOT touch the server RenderArea pipeline, unlike the reverted generator-dedup attempt that broke submit)

  1. Guard the re-bind: skip when no binding-relevant parameter actually changed — ViewModel/Stream/Model/Context by reference (immutable records/stable handles), Area/DataContext by value. Identical inputs ⇒ identical bindings, so skipping is correct and cannot stale a binding; it just stops the spurious re-subscribe→replay that sustained the loop.
  2. Surface the swallowed fault: the DataBind catch only logged, freezing the area silently. It now calls SurfaceError (modal + inline) so a wedge shows what broke.

Test

Repro: ChatRepeatedlyNoVanishTest (Playwright, ≥5 chats incl. mid-stream). Deployed to memex.localhost for live CPU verification.

🤖 Generated with Claude Code

…-binds + surface bind errors

BlazorView.OnParametersSet re-disposed and re-subscribed ALL data bindings on EVERY
parent render. The sync stream is ReplaySubject-backed, so each re-subscribe replays
→ StateHasChanged → the parent re-renders → OnParametersSet → … a self-sustaining
client-side render loop that pegs the Blazor circuit (the recurring "chat freeze"/
wedge; CPU to multiple cores, no data write in flight).

Guard it: skip the re-bind when no binding-relevant parameter actually changed
(ViewModel / Stream / Model / Context by reference — immutable records/handles;
Area / DataContext by value). Identical inputs ⇒ identical bindings, so skipping is
correct and cannot stale a binding; it just stops the spurious re-subscribe→replay
that sustained the loop. This is client-side only — it does NOT touch the server
RenderArea pipeline (unlike the earlier generator-dedup attempt that broke submit).

Also surface the swallowed bind fault: the DataBind catch only logged, freezing the
area silently. It now calls SurfaceError (modal + inline) so a wedge shows what broke.

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.

Pull request overview

This PR addresses a recurring Blazor circuit “render-storm” wedge by preventing redundant data-binding teardown/rebuild cycles on no-op parent renders, and by surfacing previously-swallowed binding faults to the UI so failures don’t present as a silent “freeze”.

Changes:

  • Add a parameter snapshot + guard in BlazorView.OnParametersSet to skip re-binding when binding-relevant inputs are unchanged.
  • Surface unexpected DataBind failures via SurfaceError (modal + inline) instead of only logging them.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@github-actions

github-actions Bot commented Jul 1, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 1)

1 391 tests  +116   1 390 ✅ +116   6m 35s ⏱️ + 1m 33s
   14 suites ±  0       1 💤 ±  0 
   14 files   ±  0       0 ❌ ±  0 

Results for commit 5b17d11. ± Comparison against base commit 583764c.

This pull request removes 6 and adds 122 tests. Note that renamed tests count towards both.
MeshWeaver.Todo.Test.TodoDataChangeTest ‑ Diagnostic_TodoApplication_IsProperlyConfigured
MeshWeaver.Todo.Test.TodoDataChangeTest ‑ Step1_SetupDataContext_WithTodoItems
MeshWeaver.Todo.Test.TodoDataChangeTest ‑ Steps2to6_TodoViewsAndDataUpdate_ShouldWork
MeshWeaver.Todo.Test.TodoLayoutAreaInteractionTest ‑ TodoList_ClickIndividualStartButton_ShouldMoveSpecificItemToInProgress
MeshWeaver.Todo.Test.TodoLayoutAreaInteractionTest ‑ TodoList_ClickStartButton_ShouldMoveItemToInProgressAndUpdateActions
MeshWeaver.Todo.Test.TodoLayoutAreaInteractionTest ‑ TodoList_ShouldRenderLayoutGridWithData
MeshWeaver.Threading.Test.AccessContextToolCallTest ‑ AccessContextAIFunction_RestoresIdentity_BeforeToolCall
MeshWeaver.Threading.Test.AccessContextToolCallTest ‑ AccessContextAIFunction_WithoutExecutionContext_DoesNotCrash
MeshWeaver.Threading.Test.AccessContextToolCallTest ‑ DelegationDepthGuard_BlocksExcessiveNesting
MeshWeaver.Threading.Test.AgentMarkdownParsingTest ‑ AgentInstructions_InlineReferences_PointToExistingNodes
MeshWeaver.Threading.Test.AgentMarkdownParsingTest ‑ AgentInstructions_NoLiteralPathPlaceholders
MeshWeaver.Threading.Test.AgentMarkdownParsingTest ‑ BuiltInAgentProvider_LoadsAllNodes
MeshWeaver.Threading.Test.AutoExecuteFlowTest ‑ PortalFlow_CreateThread_CreateCells_Submit_ResponseWritten
MeshWeaver.Threading.Test.AutoExecuteFlowTest ‑ PortalFlow_ResponseCell_GetsUpdatedByExecution
MeshWeaver.Threading.Test.BinaryAttachmentTest ‑ BinaryExtensions_DetectedCorrectly
MeshWeaver.Threading.Test.BinaryAttachmentTest ‑ ClaudeContentBlock_WithSource_SerializesToCorrectJson
…

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Jul 1, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 3)

786 tests  +4   679 ✅ +4   2m 30s ⏱️ -1s
 13 suites +1   107 💤 ±0 
 13 files   +1     0 ❌ ±0 

Results for commit 5b17d11. ± Comparison against base commit 583764c.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Jul 1, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 0)

1 478 tests   - 122   1 478 ✅  - 122   3m 43s ⏱️ - 1m 55s
   12 suites  -   1       0 💤 ±  0 
   12 files    -   1       0 ❌ ±  0 

Results for commit 5b17d11. ± Comparison against base commit 583764c.

This pull request removes 122 tests.
MeshWeaver.Threading.Test.AccessContextToolCallTest ‑ AccessContextAIFunction_RestoresIdentity_BeforeToolCall
MeshWeaver.Threading.Test.AccessContextToolCallTest ‑ AccessContextAIFunction_WithoutExecutionContext_DoesNotCrash
MeshWeaver.Threading.Test.AccessContextToolCallTest ‑ DelegationDepthGuard_BlocksExcessiveNesting
MeshWeaver.Threading.Test.AgentMarkdownParsingTest ‑ AgentInstructions_InlineReferences_PointToExistingNodes
MeshWeaver.Threading.Test.AgentMarkdownParsingTest ‑ AgentInstructions_NoLiteralPathPlaceholders
MeshWeaver.Threading.Test.AgentMarkdownParsingTest ‑ BuiltInAgentProvider_LoadsAllNodes
MeshWeaver.Threading.Test.AutoExecuteFlowTest ‑ PortalFlow_CreateThread_CreateCells_Submit_ResponseWritten
MeshWeaver.Threading.Test.AutoExecuteFlowTest ‑ PortalFlow_ResponseCell_GetsUpdatedByExecution
MeshWeaver.Threading.Test.BinaryAttachmentTest ‑ BinaryExtensions_DetectedCorrectly
MeshWeaver.Threading.Test.BinaryAttachmentTest ‑ ClaudeContentBlock_WithSource_SerializesToCorrectJson
…

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Jul 1, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 2)

   15 files  + 1     15 suites  +1   7m 12s ⏱️ +17s
1 180 tests +19  1 177 ✅ +19  3 💤 ±0  0 ❌ ±0 
1 183 runs  +20  1 180 ✅ +20  3 💤 ±0  0 ❌ ±0 

Results for commit 5b17d11. ± Comparison against base commit 583764c.

This pull request removes 27 and adds 46 tests. Note that renamed tests count towards both.
MeshWeaver.Persistence.Test.FileSystemObservableQueryTests ‑ ObserveQuery_CRUDWithMultipleSubscribers_AllReceiveNotifications
MeshWeaver.Persistence.Test.FileSystemObservableQueryTests ‑ ObserveQuery_CreateMultiple_EmitsBatchedNotification
MeshWeaver.Persistence.Test.FileSystemObservableQueryTests ‑ ObserveQuery_Create_EmitsAddedNotification
MeshWeaver.Persistence.Test.FileSystemObservableQueryTests ‑ ObserveQuery_Delete_EmitsRemovedNotification
MeshWeaver.Persistence.Test.FileSystemObservableQueryTests ‑ ObserveQuery_DisposalStopsNotifications
MeshWeaver.Persistence.Test.FileSystemObservableQueryTests ‑ ObserveQuery_FullCRUDCycle_EmitsCorrectNotifications
MeshWeaver.Persistence.Test.FileSystemObservableQueryTests ‑ ObserveQuery_MoveNode_EmitsDeleteAndCreate
MeshWeaver.Persistence.Test.FileSystemObservableQueryTests ‑ ObserveQuery_Read_EmitsInitialResults
MeshWeaver.Persistence.Test.FileSystemObservableQueryTests ‑ ObserveQuery_ScopeChildren_OnlyNotifiesDirectChildren
MeshWeaver.Persistence.Test.FileSystemObservableQueryTests ‑ ObserveQuery_ScopeExact_OnlyNotifiesExactPath
…
MeshWeaver.Persistence.Test.DocumentationCodeBlockCompilationTest ‑ ExecutedCsharpBlocks_MustCompile(embeddedResourceName: "MeshWeaver.Documentation.Data.AI.ChatCommands.md")
MeshWeaver.Persistence.Test.DocumentationCodeBlockCompilationTest ‑ ExecutedCsharpBlocks_MustCompile(embeddedResourceName: "MeshWeaver.Documentation.Data.Architecture.EmailIn"···)
MeshWeaver.Persistence.Test.DocumentationCodeBlockCompilationTest ‑ ExecutedCsharpBlocks_MustCompile(embeddedResourceName: "MeshWeaver.Documentation.Data.Architecture.HubInit"···)
MeshWeaver.Persistence.Test.DocumentationCodeBlockCompilationTest ‑ ExecutedCsharpBlocks_MustCompile(embeddedResourceName: "MeshWeaver.Documentation.Data.Architecture.ImageCl"···)
MeshWeaver.Persistence.Test.DocumentationCodeBlockCompilationTest ‑ ExecutedCsharpBlocks_MustCompile(embeddedResourceName: "MeshWeaver.Documentation.Data.Architecture.MemexCl"···)
MeshWeaver.Persistence.Test.DocumentationCodeBlockCompilationTest ‑ ExecutedCsharpBlocks_MustCompile(embeddedResourceName: "MeshWeaver.Documentation.Data.Architecture.Notific"···)
MeshWeaver.Persistence.Test.DocumentationCodeBlockCompilationTest ‑ ExecutedCsharpBlocks_MustCompile(embeddedResourceName: "MeshWeaver.Documentation.Data.Architecture.Permiss"···)
MeshWeaver.Persistence.Test.DocumentationCodeBlockCompilationTest ‑ ExecutedCsharpBlocks_MustCompile(embeddedResourceName: "MeshWeaver.Documentation.Data.Architecture.Postmor"···)
MeshWeaver.Persistence.Test.DocumentationCodeBlockCompilationTest ‑ ExecutedCsharpBlocks_MustCompile(embeddedResourceName: "MeshWeaver.Documentation.Data.Architecture.Release"···)
MeshWeaver.Persistence.Test.DocumentationCodeBlockCompilationTest ‑ ExecutedCsharpBlocks_MustCompile(embeddedResourceName: "MeshWeaver.Documentation.Data.Architecture.Sending"···)
…

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Jul 1, 2026 •

Copy link
Copy Markdown
Contributor

Test Results

   54 files  + 1     54 suites  +1   20m 1s ⏱️ -5s
4 835 tests +17  4 724 ✅ +17  111 💤 ±0  0 ❌ ±0 
4 838 runs  +18  4 727 ✅ +18  111 💤 ±0  0 ❌ ±0 

Results for commit 5b17d11. ± Comparison against base commit 583764c.

This pull request removes 27 and adds 44 tests. Note that renamed tests count towards both.
MeshWeaver.Persistence.Test.FileSystemObservableQueryTests ‑ ObserveQuery_CRUDWithMultipleSubscribers_AllReceiveNotifications
MeshWeaver.Persistence.Test.FileSystemObservableQueryTests ‑ ObserveQuery_CreateMultiple_EmitsBatchedNotification
MeshWeaver.Persistence.Test.FileSystemObservableQueryTests ‑ ObserveQuery_Create_EmitsAddedNotification
MeshWeaver.Persistence.Test.FileSystemObservableQueryTests ‑ ObserveQuery_Delete_EmitsRemovedNotification
MeshWeaver.Persistence.Test.FileSystemObservableQueryTests ‑ ObserveQuery_DisposalStopsNotifications
MeshWeaver.Persistence.Test.FileSystemObservableQueryTests ‑ ObserveQuery_FullCRUDCycle_EmitsCorrectNotifications
MeshWeaver.Persistence.Test.FileSystemObservableQueryTests ‑ ObserveQuery_MoveNode_EmitsDeleteAndCreate
MeshWeaver.Persistence.Test.FileSystemObservableQueryTests ‑ ObserveQuery_Read_EmitsInitialResults
MeshWeaver.Persistence.Test.FileSystemObservableQueryTests ‑ ObserveQuery_ScopeChildren_OnlyNotifiesDirectChildren
MeshWeaver.Persistence.Test.FileSystemObservableQueryTests ‑ ObserveQuery_ScopeExact_OnlyNotifiesExactPath
…
MeshWeaver.Persistence.Test.DocumentationCodeBlockCompilationTest ‑ ExecutedCsharpBlocks_MustCompile(embeddedResourceName: "MeshWeaver.Documentation.Data.AI.ChatCommands.md")
MeshWeaver.Persistence.Test.DocumentationCodeBlockCompilationTest ‑ ExecutedCsharpBlocks_MustCompile(embeddedResourceName: "MeshWeaver.Documentation.Data.Architecture.EmailIn"···)
MeshWeaver.Persistence.Test.DocumentationCodeBlockCompilationTest ‑ ExecutedCsharpBlocks_MustCompile(embeddedResourceName: "MeshWeaver.Documentation.Data.Architecture.HubInit"···)
MeshWeaver.Persistence.Test.DocumentationCodeBlockCompilationTest ‑ ExecutedCsharpBlocks_MustCompile(embeddedResourceName: "MeshWeaver.Documentation.Data.Architecture.ImageCl"···)
MeshWeaver.Persistence.Test.DocumentationCodeBlockCompilationTest ‑ ExecutedCsharpBlocks_MustCompile(embeddedResourceName: "MeshWeaver.Documentation.Data.Architecture.MemexCl"···)
MeshWeaver.Persistence.Test.DocumentationCodeBlockCompilationTest ‑ ExecutedCsharpBlocks_MustCompile(embeddedResourceName: "MeshWeaver.Documentation.Data.Architecture.Notific"···)
MeshWeaver.Persistence.Test.DocumentationCodeBlockCompilationTest ‑ ExecutedCsharpBlocks_MustCompile(embeddedResourceName: "MeshWeaver.Documentation.Data.Architecture.Permiss"···)
MeshWeaver.Persistence.Test.DocumentationCodeBlockCompilationTest ‑ ExecutedCsharpBlocks_MustCompile(embeddedResourceName: "MeshWeaver.Documentation.Data.Architecture.Postmor"···)
MeshWeaver.Persistence.Test.DocumentationCodeBlockCompilationTest ‑ ExecutedCsharpBlocks_MustCompile(embeddedResourceName: "MeshWeaver.Documentation.Data.Architecture.Release"···)
MeshWeaver.Persistence.Test.DocumentationCodeBlockCompilationTest ‑ ExecutedCsharpBlocks_MustCompile(embeddedResourceName: "MeshWeaver.Documentation.Data.Architecture.Sending"···)
…

♻️ This comment has been updated with latest results.

rbuergi and others added 9 commits July 1, 2026 17:03
…u + thread lists)

Traced the recurring circuit wedge to two reactive subscriptions that fire
StateHasChanged on structurally-identical re-emissions — the re-bind guard treated
the symptom (child re-binds); these are the roots.

1) Whole-page cascade. The client menu read (MenuStreamExtensions.GetMenu) had NO
   dedup, and NodeMenuItemDefinition compared its Children list by REFERENCE — so the
   layout-area stream's per-render Full re-emission delivered a "changed" menu every
   time and PortalLayoutBase re-rendered the WHOLE page (every area, incl. search-*).
   Fix: NodeMenuItemDefinition value equality compares Children by sequence; GetMenu
   gets DistinctUntilChanged(MenuItemsSequenceComparer). Only a real menu change now
   re-renders the layout.

2) Per-token chat churn. ThreadChatView's my-threads / child-threads / resume
   Hub.GetQuery subscriptions rebuilt the list + StateHasChanged UNCONDITIONALLY, and
   a streaming round re-emits the whole snapshot many times/second. The nav menu shows
   only path+name, so dedup on that visible shape (VisibleThreadListChanged) — a
   content-only churn no longer re-renders. Plus cache GetHeaderCell by threadPath so
   the header area gets a stable ViewModel reference (lets the re-bind guard skip it).

Both fixes sit at the read/subscription layer — NOT the LayoutAreaHost control-generator
input (that earlier attempt broke submit by skipping RenderArea's data push).
…de panel

Selecting a thread previewed it in the (cramped) side panel via SetContentPath. Per
user preference, navigate the main pane to the thread (full width) and collapse the
side panel instead. (Separable from the render-storm fix; bundled for one local roll.)
… correctly

- Navigate to a valid landing + open the chat side panel via the header toggle
  (title="Chat"); the composer is a Monaco editor (was: guessed selector on an
  invalid /User/{id} URL, so it never chatted).
- Reconnect assertion checks VISIBILITY, not presence — #components-reconnect-modal
  is always in the DOM (hidden), so Count>0 was a permanent false positive.
- Container check polls a few seconds (tolerates the momentary re-mount on a rapid
  mid-stream submit; only a PERMANENT teardown = the storm).

Verified against the fix image: passes; 1× re-bind per area (was 37-59×), CPU 300m
peak (was 1.2+ cores).
…per-token re-render

- BuildHarnessCommandCompletions now sources the active CLI harness's OWN slash-commands
  AND skills (probed from the CLI init via IHarnessRuntimeInfo → harnessRuntimes), so
  typing / under Claude Code / Copilot lists ALL its commands + skills; each forwards
  1:1 to the CLI (existing dispatch).
- HeaderView: dedup the aggregated change-set on its visible summary (path/op/version)
  before BuildHeader — CollectUpdatedNodes re-emitted per streamed token, re-rendering
  the Header layout area (~128x in the e2e capture). A residual render-storm loop.
Selecting a thread navigated to /{threadPath} + collapsed the side panel, but a Thread
node has no main-pane renderer — the chat vanished ('entering chat makes it disappear').
Restore the working side-panel open (SetContentPath). A real full-width thread view needs
a dedicated main-pane chat route, not a raw path nav.
Status chips (harness / model / effort / agent) are now <button>s that open the SAME
combobox the matching slash-command uses — reusing HandleSlashCommandAsync → the skill's
Pick action → OpenPicker (no hand-rolled dropdown):
- harness → runtime switcher (all harnesses)
- mesh agent / model → the node-pick combobox
- ClaudeCode/Copilot model + effort → forwarded 1:1 to the CLI (their own, not mesh nodes)

Emoji glyphs replaced with crisp inline SVG (currentColor) per type via ChipSvg(kind).
Button chrome reset in CSS; .is-clickable adds cursor + hover.
…-code

The '/' trigger fired the command completion at line-start OR after any whitespace, so a
'/' inside code — paths (cd /etc), // comments, a / b, http:// — popped the command list.
Harmless when that list was 3 items; now that it lists EVERY CLI command + skill it churned
Monaco → 'stuck when entering code'. Gate '/' to line 1, only optional whitespace before it
(a real command is the first thing typed). '@' references are unchanged.
Resolve the Monaco completion-provider conflict by combining both fixes:
the '/' trigger keeps this branch's composer-start-only rule (line 1,
whitespace-prefix only — no command popup mid-code), the '@' trigger keeps
main's newer lastIndexOf word-boundary implementation (punctuation-safe,
email- and in-path-@ excluded).

Verified: node --check parses; MeshWeaver.Blazor + Blazor.Portal build
Release -warnaserror clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CS8604 under -warnaserror after merging main (main carries the older test
iteration that interpolated BaseUrl). Available (asserted via SkipUnless at
the top) guarantees BaseUrl is non-null.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@rbuergi
rbuergi merged commit 25ed00e into main Jul 2, 2026
11 checks passed
@rbuergi
rbuergi deleted the fix/render-storm-rebind-guard branch August 5, 2026 10:45
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.

2 participants