fix(tui): marshal off-loop Channels label refresh onto the loop (#1426)#1439
Merged
Aaronontheweb merged 2 commits intoJun 18, 2026
Conversation
…law-dev#1426) The background channel-label refresh reconciled in its probe continuation — mutating _channelAudiences, the Step channel ids, Status/IsSaved, and the persisted config — on a thread-pool thread, racing the loop thread that reads that state for render and mutates it in the audience/remove handlers. The audience is a security-relevant ACL trust tier, so the race could tear the Dictionary or lose a lowering edit. Bump Termina 0.12.1 -> 0.14.0-beta.1 to use the new ReactiveViewModel loop marshal primitive. The background path now runs only the pure probe off-loop and fire-and-forgets the reconcile onto the loop via InvokeAsync, serialized with render/input. This replaces the hand-rolled publish-cell + drain + page-chokepoint approach. The marshal is fire-and-forget (not awaited) on purpose: awaiting it would tie the tracked task's completion to a loop turn, so CancelAndAwaitLabelRefreshAsync (which SaveAsync awaits at its top) would defer the save's own disk write behind a loop turn — a save-then-immediate-quit would drop the write. Cancelling neuters any queued stale apply via the ct re-check. Also update the termina-tui-patterns skill to document the new marshal primitive and mark ChannelsConfigViewModel as the reference, and drive the InvokeAsync path in tests via a probe-entered/release handshake (no Thread.Sleep/Task.Delay).
Aaronontheweb
enabled auto-merge (squash)
June 18, 2026 20:20
…floop-renderframeprovider
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.
Summary
The background channel-label refresh in
ChannelsConfigViewModelreconciled in its probe continuation — mutating_channelAudiences, the Step channel IDs,Status/IsSaved, and the persisted config — on a thread-pool thread, racing the Termina loop thread that reads that state for render and mutates it in the audience/remove handlers. Audiences are security-relevant ACL trust tiers, so the race could tear theDictionaryor lose an audience-lowering edit (fail open).Fix
0.12.1→0.14.0-beta.1to use the newReactiveViewModelloop-marshal primitive (Post/InvokeAsync).InvokeAsync, serialized with render/input. This replaces the hand-rolled publish-cell + drain + page-chokepoint approach.CancelAndAwaitLabelRefreshAsync(whichSaveAsyncawaits at its top) would defer the save's own disk write behind a loop turn — a save-then-immediate-quit would drop the write. Cancelling neuters any queued stale apply via thectre-check.termina-tui-patternsskill to document the new marshal primitive and markChannelsConfigViewModelas the reference.This introduces a dependency on a prerelease Termina (
0.14.0-beta.1).Validation
Netclaw.Cli.Tests: 1096/1096config-channelstape (real Termina loop) ✓dotnet slopwatch analyze(no new violations) + copyright headers ✓scripts/docker/build-image.sh) + in-container CLI smoke (test-nonroot-cli.sh) + interactiveconfigsmoke (added channel persists with correct canonical ID + audience) ✓Follow-on
#1418 (retry-with-feedback paradigm) is sequenced to build on this marshal foundation.
Closes #1426