refactor(agent): arbitrate receiver access with leases#302
Conversation
51d2b04 to
1d2455c
Compare
Greptile SummaryThis PR replaces two ad-hoc
Confidence Score: 4/5Safe to merge; the coordination logic is sound for all normal and cancellation paths, and the only gap requires a mutex to be poisoned during a trivially short critical section. The refactoring correctly handles all race paths: the double-check in try_acquire_for_capture, cancellation-safe PairingRequest drop, SessionAdmission rollback, and typed failure propagation all look correct. The one gap is that receiver_lease.lock() in the terminal-event handler silently skips lease release if the mutex is poisoned, keeping pairing_requested stuck at true and blocking all future capture. Recovering from a StdMutex poison with PoisonError::into_inner() would make both paths fully robust. crates/openlogi-agent/src/pairing.rs — specifically the terminal-event handler in translate() and the watcher-shutdown cleanup just below it. Important Files Changed
Sequence Diagram%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant GUI
participant PairingManager
participant ReceiverAccess
participant GestureWatcher
participant PairingWatcher
GUI->>PairingManager: start_pairing()
PairingManager->>ReceiverAccess: "acquire_for_pairing() [sets pairing_requested=true]"
GestureWatcher->>ReceiverAccess: pairing_requested()? → true
GestureWatcher->>GestureWatcher: stop capture session (drops CaptureReceiverLease)
ReceiverAccess-->>PairingManager: PairingReceiverLease granted
PairingManager->>PairingManager: store lease in receiver_lease slot
PairingManager->>PairingWatcher: Control::Start(selector)
PairingWatcher-->>PairingManager: PairingEvent::Paired / Failed
PairingManager->>PairingManager: translate() drops PairingReceiverLease
Note over ReceiverAccess: pairing_requested=false (via Drop)
GestureWatcher->>ReceiverAccess: try_acquire_for_capture() → CaptureReceiverLease
GestureWatcher->>GestureWatcher: resume capture session
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant GUI
participant PairingManager
participant ReceiverAccess
participant GestureWatcher
participant PairingWatcher
GUI->>PairingManager: start_pairing()
PairingManager->>ReceiverAccess: "acquire_for_pairing() [sets pairing_requested=true]"
GestureWatcher->>ReceiverAccess: pairing_requested()? → true
GestureWatcher->>GestureWatcher: stop capture session (drops CaptureReceiverLease)
ReceiverAccess-->>PairingManager: PairingReceiverLease granted
PairingManager->>PairingManager: store lease in receiver_lease slot
PairingManager->>PairingWatcher: Control::Start(selector)
PairingWatcher-->>PairingManager: PairingEvent::Paired / Failed
PairingManager->>PairingManager: translate() drops PairingReceiverLease
Note over ReceiverAccess: pairing_requested=false (via Drop)
GestureWatcher->>ReceiverAccess: try_acquire_for_capture() → CaptureReceiverLease
GestureWatcher->>GestureWatcher: resume capture session
Reviews (1): Last reviewed commit: "refactor(ipc): type pairing failure reas..." | Re-trigger Greptile |
| if sessions.fetch_sub(1, Ordering::Relaxed) == 1 | ||
| && let Ok(mut lease) = receiver_lease.lock() | ||
| { | ||
| *lease = None; | ||
| } |
There was a problem hiding this comment.
Lease not released on poisoned mutex in terminal-event handler
The let-chain short-circuits on Err from receiver_lease.lock(), so if the StdMutex is poisoned the PairingReceiverLease is never dropped, pairing_requested stays true permanently, and all future capture sessions are blocked. While a mutex poison requires a panic in another thread during the critical section (effectively impossible for a bare *slot = None assignment), the consequence — capture deadlocked until the agent restarts — is severe enough to warrant defensive recovery. The watcher-shutdown path just below (if let Ok(mut lease) = receiver_lease.lock()) has the same gap: if the lock is poisoned there, the lease also stays held.
Summary
Validation