fix: eliminate null transient state in DeleteGroup concurrent reads#290
fix: eliminate null transient state in DeleteGroup concurrent reads#290
Conversation
Organization.Groups.RemoveAll() nulls out trailing array elements before decrementing _size (via Array.Clear), creating a window where concurrent readers (test polling thread, render thread) observe a null SessionGroup and throw NullReferenceException. Replace with an atomic list-reference swap using Where().ToList(). Readers see either the old complete list or the new list without the deleted group, but never a null-containing partial state. Fixes CI failure: WsBridgeIntegrationTests.Organization_DeleteGroup_RemovesFromServer Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
PR Review — 5-Model Consensus AnalysisCI Status: ✅ Passing (2026/2039, 13 pre-existing failures) The Fix (What's Correct)The root-cause diagnosis is accurate. 🔴 CRITICAL — Lost-Update Race Introduced by This Fix (3/5 models)
The
The original in-place Request: Marshal 🟡 MODERATE — Incomplete Fix: Two Other
|
Root cause: WsBridgeServer called DeleteGroup and other Organization- mutating methods directly from the WebSocket receive thread (ThreadPool), violating the UI-thread-only invariant for Organization.Groups mutations. List<T>.RemoveAll() nulls trailing elements before decrementing _size, so concurrent UI-thread readers saw null SessionGroup entries. Fix: convert HandleOrganizationCommand to HandleOrganizationCommandAsync and wrap every _copilot.* organization mutation in InvokeOnUIAsync() so they execute on the UI thread. Same fix applied to the RemoveRepo handler. Also revert the previous Where().ToList() workaround in DeleteGroup -- that approach introduced a lost-update race where a concurrent CreateGroup on the UI thread could be silently overwritten by the assignment. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Refactor ShowSkillsPopup, ShowAgentsPopup, ShowPromptsPopup, and ShowLogPopup in ExpandedSessionView.razor to use the existing CSS class system (skills-popup-*, skills-popup-log-*) instead of inline styles with hardcoded Catppuccin dark-theme hex colors. Changes per method: - Overlay: className='skills-popup-overlay' replaces inline style.cssText - Popup: className='skills-popup' (or with --wide modifier), only bottom/left set inline for computed positioning - Rows: CSS classes for layout (skills-popup-row, -title, -name, -source, -desc, -log-row, -log-ts, -log-detail, -empty) - Log event type colors: CSS variables (--accent-primary/success/ warning/error) instead of hardcoded hex - Form inputs in ShowPromptsPopup: CSS variables (--bg-tertiary, --control-border, --text-bright, --text-muted, --accent-primary) All 2039 tests pass (including all 23 PopupThemeTests). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Round 2 Update — All review findings addressed ✅Previous review checklist status
Additional: All 13 pre-existing PopupThemeTests fixedRefactored Test results: ✅ 2039/2039 passing (0 failures) |
Problem
CI failure:
WsBridgeIntegrationTests.Organization_DeleteGroup_RemovesFromServerRoot cause:
List<T>.RemoveAll()in .NET nulls out trailing array elements viaArray.Clearbefore decrementing_size. This creates a transient window where:DeleteGroup → RemoveAllOrganization.GroupsSessionGroupat an index that's still within the old_sizeg.IdthrowsNullReferenceExceptionThis only manifests on CI because thread scheduling variance is higher in the GitHub Actions runner environment. It passes locally where the polling loop rarely lands in the narrow null-transient window.
Fix
Replace in-place
RemoveAllwith an atomic list-reference replacement:Assigning a new
List<T>reference is atomic on 64-bit .NET (object reference assignment). Readers that already hold a reference to the old list continue to see all elements. Readers that pick up the new reference see the post-delete state. No reader ever sees a null element.Testing
Organization_DeleteGroup_RemovesFromServerpassesWsBridgeIntegrationTestspassPopupThemeTestsfailures unrelated to this change)Fixes: https://github.com/PureWeen/PolyPilot/actions/runs/22735831218/job/65936842712