fix(tui): try_lock shell manager in async UI refresh paths (#3804)#3808
Conversation
The async UI loop took a blocking std::sync::Mutex lock on the shell manager in two render-only refresh paths (refresh_active_task_panel, refresh_shell_exec_live_output). If a background shell path held that lock, the UI thread could stall with no async yield — an amplifier of the #3800 fanout freeze. Use try_lock for these render-only reads: - refresh_active_task_panel: on contention, retain the previous frame's background shell entries (last-known snapshot) instead of dropping running shells from the Work panel. - refresh_shell_exec_live_output: on contention, skip this frame's live-output update (the next refresh picks it up). Shell ownership, cancellation, approval state, and output capture do not depend on these UI refreshes, so try_lock + snapshot/skip is safe and does not weaken enforcement (per the #3804 guardrails). Adds a contention regression test that holds the lock and asserts the live-output refresh returns immediately rather than deadlocking (the old blocking lock() would deadlock on the same thread). Parent: #3800 Fixes #3804
|
Claude finished @Hmbown's task in 1m 54s —— View job PR Review: fix(tui): try_lock shell manager in async UI refresh paths
SummaryThe core idea is correct and well-motivated: blocking Correctness Bug —
|
Closes #3804 (child of #3800).
Problem
The async UI loop took a blocking
std::sync::Mutex::lock()on the shell manager in two render-only refresh paths (refresh_active_task_panelui.rs:1432,refresh_shell_exec_live_outputui.rs:1461). A contended lock could stall render/input — an amplifier of the fanout freeze.Change
Both paths now use
try_lock():refresh_active_task_panel: on contention, retain the previous frame's background shell entries (last-known snapshot) so running shells don't flicker out.refresh_shell_exec_live_output: on contention, skip this frame's live-output update (next refresh picks it up).Guardrails (per the issue)
Shell ownership, cancellation, approval state, and output capture do not depend on these UI refreshes — they're render-only reads — so
try_lock+ snapshot/skip cannot weaken enforcement or hide a running shell beyond one frame.Test
shell_live_output_refresh_does_not_block_on_contended_lock: holds the lock and asserts the refresh returns immediately (the old blockinglock()would deadlock on the same thread, so reaching the assert proves non-blocking). Build green; no regressions.Acceptance
std::sync::MutexNote: this is the
[safe-now]child #2 from the #3800 audit; it's isolated from the in-flight backpressure/parallel-dispatch branches.