← Back to PRs

#22454: fix(macos): add re-subscribe loop to gateway stream subscribers

by mandofever78 open 2026-02-21 05:58 View on GitHub →
app: macos size: XS
## Summary Wraps the `GatewayConnection.shared.subscribe()` + `for await` pattern in a `while !Task.isCancelled` loop with a 1-second backoff for three gateway event subscribers: - `ExecApprovalsGatewayPrompter` - `DevicePairingApprovalPrompter` - `NodePairingApprovalPrompter` ## Problem If the underlying `AsyncStream` from `GatewayConnection.subscribe()` ever terminates, these prompters silently stop receiving gateway events and never recover. This can happen if `GatewayConnection.shutdown()` is later changed to properly `finish()` subscriber continuations, or if the stream ends for any other lifecycle reason. Currently the streams never terminate (continuations are not finished in `shutdown()`), so the prompters hang in a zombie state — blocked on `for await` but receiving nothing — rather than crashing. This makes the bug silent and hard to diagnose. ## Fix Add the same defensive re-subscribe loop already used by `VoiceWakeGlobalSettingsSync`: ```swift while !Task.isCancelled { let stream = await GatewayConnection.shared.subscribe(bufferingNewest: 200) for await push in stream { if Task.isCancelled { return } // handle push } if Task.isCancelled { return } try? await Task.sleep(nanoseconds: 1_000_000_000) // 1s backoff } ``` When the stream ends, the subscriber logs, waits 1 second, and re-subscribes. Task cancellation (via each prompter's existing `stop()` method) cleanly breaks the loop. ## Behavioral impact **None under current code.** The `for await` loop blocks indefinitely today because `GatewayConnection.shutdown()` does not finish subscriber continuations. The outer `while` loop never iterates. This change is purely defensive/future-proofing. ## Testing - `swift build` succeeds (265 targets, 43s) - No new test failures introduced (pre-existing test compile failures in `GatewayChannelShutdownTests` and `GatewayChannelRequestTests` are unrelated — `sendPing` protocol conformance was added to `WebSocketTasking` but test fakes were not updated) - Diff is minimal: +26 lines / -9 lines across 3 files - Matches established pattern in `VoiceWakeGlobalSettingsSync` ## Related The approval prompters are high-impact subscribers (exec approvals, device pairing, node pairing). Unlike `CronJobsStore` or `InstancesStore` which have polling fallbacks, these prompters rely entirely on the gateway event stream to trigger native macOS alert prompts. <!-- greptile_comment --> <h3>Greptile Summary</h3> Added defensive re-subscribe loops to three gateway event subscribers (`ExecApprovalsGatewayPrompter`, `DevicePairingApprovalPrompter`, `NodePairingApprovalPrompter`) following the existing pattern from `VoiceWakeGlobalSettingsSync`. **Pattern implemented:** - Wrapped `subscribe()` + `for await` in a `while !Task.isCancelled` loop - Added 1-second backoff between re-subscribe attempts - Task cancellation checks at loop boundaries prevent zombie tasks **Impact:** - Currently zero behavioral impact because `GatewayConnection.shutdown()` does not call `finish()` on subscriber continuations, so streams never terminate naturally - Future-proofs against stream termination if gateway lifecycle changes (e.g., if `shutdown()` is later modified to properly finish continuations) - Prevents silent failure if the underlying `AsyncStream` ends for any reason **Consistency notes:** - `ExecApprovalsGatewayPrompter` adds logging and detailed comments (good practice) - `DevicePairingApprovalPrompter` and `NodePairingApprovalPrompter` lack the logging statement that `ExecApprovalsGatewayPrompter` includes when the stream ends - All three use 1-second backoff, matching the PR description and established pattern - `VoiceWakeGlobalSettingsSync` uses a different backoff (600ms) for its re-subscribe loop, but that's intentional as it handles different event types <h3>Confidence Score: 5/5</h3> - This PR is safe to merge with minimal risk - The changes are minimal, defensive, and follow an established pattern from `VoiceWakeGlobalSettingsSync`. Current behavior is unchanged (streams never terminate because `shutdown()` doesn't finish continuations), so this is purely future-proofing. The pattern correctly handles task cancellation at all loop boundaries, preventing resource leaks. The only minor issue is missing debug logging in two files (non-critical). - No files require special attention <sub>Last reviewed commit: cd68611</sub> <!-- greptile_other_comments_section --> <sub>(2/5) Greptile learns from your feedback when you react with thumbs up/down!</sub> <!-- /greptile_comment -->

Most Similar PRs