Skip to content

Gate idle pollers to active workspace - #6583

Merged
austinywang merged 16 commits into
mainfrom
issue-6545-poller-throttle
Jun 22, 2026
Merged

austinywang merged 16 commits into
mainfrom
issue-6545-poller-throttle

Conversation

@austinywang

@austinywang austinywang commented Jun 22, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Closes #6545.

This is a focused slice of the broader poller/resource tracker in #5731 and the SwiftUI AttributeGraph saturation symptoms in #5570. The patch confirms the documented mechanism in source and reduces the timer-driven poller -> Workspace-level publish -> graph-wide invalidation chain without attempting the full #5731 rewrite.

Changes:

  • Pause/cancel the 2s tracked-agent PortScanner timer while cmux is inactive, then restart it on activation if tracked workspaces remain.
  • Keep tracked-agent polling across active background workspaces so background agent port discovery/clearing still works.
  • Diff agent-owned port results before delivering them to Workspace state, including empty-result dedupe after a clear.
  • Force explicit agent-port refreshes to deliver once, so Workspace state cleared during refresh can still be restored/cleared even when the live port set did not change.
  • Evict the PortScanner agent-port cache when tracked workspaces become inactive, keeping the cache bounded.
  • Leave the runaway pane memory guardrail running in the background for safety; this PR does not weaken its leak/OOM protection.

Mechanism confirmed

Read the issue body/comment and verified the cited source paths:

Tests and limits

Added behavioral coverage for guardrail descriptors covering registered window managers and keeping background workspaces represented. Kept the touched PortScanner FD-count regression on XCTest because it samples process-global /dev/fd state and should not move into Swift Testing's default parallel execution model.

Not unit-testable in this PR: the full five-day soak profile, SwiftUI AttributeGraph transaction cost, and the private PortScanner agent-port diff path without adding a production-only test seam. The proof here is source-level mechanism confirmation plus focused behavior tests where there is an existing runtime seam.

Local validation run:

  • git diff --check
  • ./scripts/lint-pbxproj-test-wiring.sh
  • python3 scripts/swift_file_length_budget.py

Per task instruction, I did not run a local app build, reload, or bare xcodebuild; CI is the compile/test gate before any dev build is launched.

@vercel

vercel Bot commented Jun 22, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment Jun 22, 2026 11:01am
cmux-staging Building Building Preview, Comment Jun 22, 2026 11:01am

@coderabbitai

coderabbitai Bot commented Jun 22, 2026 •

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@austinywang, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 3 minutes and 12 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan refill rate.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, the refill rate gradually slows as usage increases. The highest same-day bursts are limited more strictly.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 7edf3225-114f-48eb-ac37-bf225fa2c689

📥 Commits

Reviewing files that changed from the base of the PR and between b185e99 and f6ca98c.

⛔ Files ignored due to path filters (1)
  • .github/swift-file-length-budget.tsv is excluded by !**/*.tsv
📒 Files selected for processing (2)
  • Sources/PortScanner.swift
  • Sources/TerminalController.swift
📝 Walkthrough

Walkthrough

PortScanner gains per-workspace agent port caching and a trackedAgentScanningPaused flag that cancels the periodic scan timer when set. AppDelegate toggles this flag on app active/resign transitions; TerminalController sets the initial state when the socket listener starts. PortScannerTests is migrated from XCTest to Swift Testing, and a new guardrail descriptor test is added.

Changes

Agent Scanning Pause/Resume and Port Deduplication

Layer / File(s) Summary
PortScanner: pause/resume state and port deduplication
Sources/PortScanner.swift
Adds lastAgentPortsByWorkspace, forceAgentResultWorkspaces, and trackedAgentScanningPaused private state. Introduces setTrackedAgentScanningPaused(_:) to update the flag and call updateAgentScanTimerLocked(). The timer guard now cancels when paused. refreshAgentPortsLocked and finishTrackedAgentScan mark workspaces in forceAgentResultWorkspaces on PID-tracking transitions. validatedAgentResults compares against cached ports, suppresses unchanged deliveries, and maintains the per-workspace cache.
Lifecycle wiring
Sources/TerminalController.swift, Sources/AppDelegate.swift
TerminalController.socketListenerDidStart sets the initial pause state from NSApplication.shared.isActive. AppDelegate calls setTrackedAgentScanningPaused(false) on applicationDidBecomeActive and setTrackedAgentScanningPaused(true) on applicationWillResignActive.
Tests: PortScannerTests migration and PaneMemoryGuardrailTests
cmuxTests/PortScannerTests.swift, cmuxTests/PaneMemoryGuardrailTests.swift
PortScannerProcessCaptureTests and ProcessTerminationGateTests are rewritten from XCTest to Swift Testing (@Suite, @Test, #expect, #require). A new @MainActor test verifies paneMemoryGuardrailDescriptors() includes descriptors for both selected and background workspaces.

Sequence Diagram(s)

sequenceDiagram
  participant AppDelegate
  participant TerminalController
  participant PortScanner
  participant AgentScanTimer

  TerminalController->>PortScanner: setTrackedAgentScanningPaused(!isActive)
  AppDelegate->>PortScanner: setTrackedAgentScanningPaused(false) on app active
  PortScanner->>AgentScanTimer: updateAgentScanTimerLocked, schedule timer
  AgentScanTimer->>PortScanner: runTrackedAgentScan fires
  PortScanner->>PortScanner: refreshAgentPortsLocked, mark forceAgentResultWorkspaces on PID change
  PortScanner->>PortScanner: validatedAgentResults, diff lastAgentPortsByWorkspace
  alt ports changed or forced
    PortScanner-->>PortScanner: deliver results, update cache
  else unchanged and not forced
    PortScanner-->>PortScanner: suppress delivery
  end
  AppDelegate->>PortScanner: setTrackedAgentScanningPaused(true) on app resigned
  PortScanner->>AgentScanTimer: updateAgentScanTimerLocked, cancel timer
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Suggested reviewers

  • lawrencecchen

Poem

🐇 The timer was racing, the main thread aghast,
So the rabbit said "pause!" when the window slipped past.
With ports now remembered and changes compared,
No redundant scan dares leave the main thread impaired.
Resume on active, halt when away —
A quieter SwiftUI graph brightens the day! 🌿


Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (2 errors, 1 warning)

Check name Status Explanation Resolution
Cmux Swift Concurrency ❌ Error The PR introduces setTrackedAgentScanningPaused(_ paused: Bool) in PortScanner which uses queue.async (a DispatchQueue) for ordinary state updates when async/await could model this work. Replace queue.async { [self] in ... } with modern async/await patterns, or document why existing DispatchQueue pattern is required if unable to refactor.
Cmux Architecture Rethink ❌ Error PR violates architectural rethink rule: pause state controlled via multiple surfaces. AppDelegate sets PortScanner.trackedAgentScanningPaused via explicit lifecycle methods, while TerminalControlle... Consolidate pause-state management to AppDelegate as the single lifecycle owner. TerminalController should receive pause state via callback rather than independently controlling it.
Docstring Coverage ⚠️ Warning Docstring coverage is 12.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (20 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Gate idle pollers to active workspace' directly addresses the core change of the PR: pausing/gating pollers when the app is inactive, which is the primary solution to the responsiveness issue.
Linked Issues check ✅ Passed The PR successfully implements the core suggestions from #6545: gates the 2s PortScanner timer to app-active state, maintains background workspace polling, diffs agent-port results to reduce graph invalidations, and includes behavioral tests validating the mechanism.
Out of Scope Changes check ✅ Passed All changes align with the linked issue scope: PortScanner timer gating, agent-port result diffing, background workspace support, cache eviction, guardrail gating, and test migration. The PR explicitly notes it does not attempt the broader #5731 rewrite.
Cmux Swift Actor Isolation ✅ Passed PortScanner.setTrackedAgentScanningPaused() safely dispatches to private queue; all new state is queue-guarded. AppDelegate/TerminalController (@MainActor) safely call it. No actor isolation issues...
Cmux Swift Blocking Runtime ✅ Passed PR introduces non-blocking patterns: queue.async dispatch and DispatchSourceTimer gating without semaphores, sleep, or manual locks. No blocking or timing-based synchronization added.
Cmux Expensive Synchronous Load ✅ Passed The PR adds no expensive synchronous agent-history loads onto the main actor or interactive paths. All changes use async queue dispatch to gate timer polling, with only simple data structure update...
Cmux Cache Substitution Correctness ✅ Passed The new lastAgentPortsByWorkspace cache in PortScanner is used only for transient UI deduplication, not substituting fresh authoritative reads in persistence/history/undo/snapshot paths. The fres...
Cmux No Hacky Sleeps ✅ Passed PR changes are all Swift files (AppDelegate, PortScanner, TerminalController, tests). The rule "runtime-no-hacky-sleeps.md" explicitly excludes Swift: "Scope: TypeScript, JavaScript, shell, and non...
Cmux Algorithmic Complexity ✅ Passed PR introduces agent port diffing (validatedAgentResults), caching (lastAgentPortsByWorkspace), and pause/resume logic. No nested full-collection scans detected. Nested iteration in scanAgentPorts h...
Cmux Swift @Concurrent ✅ Passed All Swift concurrency rules followed: setTrackedAgentScanningPaused is synchronous with proper queue dispatching; called from @MainActor contexts correctly; no invalid @concurrent annotations.
Cmux Swift File And Package Boundaries ✅ Passed All Swift file changes comply with package boundaries: PortScanner adds 24 net lines while maintaining single port-scanning responsibility (638 lines, <800 threshold); AppDelegate and TerminalContr...
Cmux Swiftpm Lockfiles ✅ Passed The PR changes only app-level Swift source files (AppDelegate, PortScanner, TerminalController, tests). No Package.swift, Package.resolved, .gitignore, Xcode project, workflow, or dependency resolu...
Cmux Swift Logging ✅ Passed PR adds no print/debugPrint/dump/NSLog statements; all changes to Sources/* use method calls only (setTrackedAgentScanningPaused). Complies with swift-logging.md rules.
Cmux User-Facing Error Privacy ✅ Passed PR contains no user-facing error messages, alerts, credentials, vendor names, env vars, or sensitive data. All changes are internal API calls and implementation code conforming to user-facing-error...
Cmux Full Internationalization ✅ Passed PR contains only internal code changes (timer gating, state management) with no user-facing strings, string catalogs, Info.plist changes, or web UI modifications; test files are excepted from i18n...
Cmux Swiftui State Layout ✅ Passed No new SwiftUI ObservableObject/@Published/@observable state declarations. All new state is private to PortScanner. Workspace state updates occur via callbacks, not render-time. Complies with swift...
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PR adds timer gating and port tracking code without creating or materially changing any standalone cmux-owned windows (NSWindow, NSPanel, NSWindowController, SwiftUI Window, or WindowGroup). Change...
Cmux Source Artifacts ✅ Passed All five changed files are intentional hand-written Swift source code and tests in proper directories (Sources/ and cmuxTests/), with no artifacts, generated files, caches, build output, or temp fo...
Cmux No Test Or Debug Seam In Production Source ✅ Passed PR introduces no test/debug seams in production code; new setTrackedAgentScanningPaused() method implements real product behavior for issue #6545, is called from production code, lacks test-seam na...
Description check ✅ Passed The PR description comprehensively covers all required template sections: detailed summary of changes with context from linked issues, testing approach with local validation steps, mechanism confirmation with source-level verification, and behavioral test coverage added.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue-6545-poller-throttle

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps

greptile-apps Bot commented Jun 22, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR gates the 2-second DispatchSourceTimer-driven tracked-agent port polling to the app's active state, replacing a previous filter-based approach with a trackedAgentScanningPaused flag toggled in applicationWillResignActive / applicationDidBecomeActive. It also adds a per-workspace port diff cache (lastAgentPortsByWorkspace) and a force-delivery set (forceAgentResultWorkspaces) to deduplicate timer-driven onAgentPortsUpdated callbacks while still guaranteeing delivery on agent start/stop transitions.

  • Timer pause: setTrackedAgentScanningPaused(true/false) dispatches to the scanner queue and calls updateAgentScanTimerLocked(), which cancels or creates the 2-second DispatchSourceTimer. TerminalController.start() primes the state from NSApplication.shared.isActive so it is correct even when the first window opens while the app is already backgrounded.
  • Diff + force delivery: validatedAgentResults skips a result if the sorted port list is unchanged and no force flag is set; forceAgentResultWorkspaces is set on every refreshAgentPortsLocked call (agent start/stop) and on every workspace finishTrackedAgentScan marks as inactive, guaranteeing one delivery of [] when an agent stops.
  • Revision-gated cache acknowledgment: acknowledgeAgentResults checks the current revision before committing lastAgentPortsByWorkspace updates, so concurrent in-flight scans with stale revisions are silently dropped without corrupting the cache.

Confidence Score: 5/5

Safe to merge; the timer pause/resume wiring and port-diff cache are logically correct, and the two previously identified delivery-ordering issues are addressed in the current implementation.

The two prior blocking findings are resolved: lastAgentPortsByWorkspace is no longer evicted before scanAgentPorts — eviction is deferred to acknowledgeAgentResults after confirmed delivery — and the trackedAgentScanWorkspaceFilter early-return path that could leave agentScanTimer live is gone, replaced by setTrackedAgentScanningPaused which directly cancels the timer. The revision-gated acknowledgment correctly handles concurrent in-flight scans. Explicit refreshAgentPortsLocked calls still deliver immediately regardless of the pause flag, preserving agent start/stop correctness while backgrounded.

Sources/PortScanner.swift — complex multi-state interaction between forceAgentResultWorkspaces, lastAgentPortsByWorkspace, and the revision guard; warrants careful reading but no current defects found.

Important Files Changed

Filename Overview
Sources/PortScanner.swift Core change: adds trackedAgentScanningPaused flag, lastAgentPortsByWorkspace diff cache, and forceAgentResultWorkspaces set; previous P1s (cache-eviction-before-delivery, timer-live-after-filter-early-return) are addressed in current code; no new P0/P1 issues found
Sources/AppDelegate.swift Adds pause/unpause calls in applicationWillResignActive / applicationDidBecomeActive; correctly dispatches to the scanner queue from the MainActor delegate callbacks
Sources/TerminalController.swift Updates onAgentPortsUpdated to return Bool (workspace-found signal), adds initial pause-state sync after socket start; semantics are consistent with acknowledgeAgentResults expectations
cmuxTests/PaneMemoryGuardrailTests.swift New behavioral test for pane-memory guardrail coverage of background workspaces; test-only change using existing seams, no production-source impact
.github/swift-file-length-budget.tsv Budget entries updated to reflect net line-count changes across touched files; entry for PortScanner.swift correctly moved to its new (higher) bound

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant App as AppDelegate (MainActor)
    participant PS as PortScanner (queue)
    participant MA as MainActor
    participant WS as Workspace state

    App->>PS: setTrackedAgentScanningPaused(true)
    PS->>PS: cancel agentScanTimer
    App->>PS: setTrackedAgentScanningPaused(false)
    PS->>PS: create agentScanTimer (2s)
    loop Every 2s while active
        PS->>PS: runTrackedAgentScan()
        PS->>MA: agentPIDsProvider(workspaceIds)
        MA-->>PS: "UUID to Set<Int> map"
        PS->>PS: finishTrackedAgentScan()
        PS->>PS: "scanAgentPorts() -> validatedAgentResults()"
        PS->>MA: "onAgentPortsUpdated(workspaceId, ports) -> Bool"
        MA->>WS: "agentListeningPorts = ports"
        MA-->>PS: true or false
        PS->>PS: acknowledgeAgentResults()
    end
Loading
%%{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 App as AppDelegate (MainActor)
    participant PS as PortScanner (queue)
    participant MA as MainActor
    participant WS as Workspace state

    App->>PS: setTrackedAgentScanningPaused(true)
    PS->>PS: cancel agentScanTimer
    App->>PS: setTrackedAgentScanningPaused(false)
    PS->>PS: create agentScanTimer (2s)
    loop Every 2s while active
        PS->>PS: runTrackedAgentScan()
        PS->>MA: agentPIDsProvider(workspaceIds)
        MA-->>PS: "UUID to Set<Int> map"
        PS->>PS: finishTrackedAgentScan()
        PS->>PS: "scanAgentPorts() -> validatedAgentResults()"
        PS->>MA: "onAgentPortsUpdated(workspaceId, ports) -> Bool"
        MA->>WS: "agentListeningPorts = ports"
        MA-->>PS: true or false
        PS->>PS: acknowledgeAgentResults()
    end
Loading

Reviews (9): Last reviewed commit: "fix: purge stale agent port scan state" | Re-trigger Greptile

Comment thread Sources/PortScanner.swift Outdated
Comment thread Sources/PortScanner.swift Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cmuxTests/PortScannerTests.swift`:
- Around line 10-36: The PortScannerProcessCaptureTests struct declares `@Suite`
without the .serialized trait, but the test reads process-global file descriptor
counts and performs baseline and delta assertions that can become
non-deterministic when tests run concurrently. Modify the `@Suite` annotation on
the PortScannerProcessCaptureTests struct to include the .serialized trait by
changing `@Suite` to `@Suite`(.serialized), which ensures tests in this suite
execute serially and prevents concurrent file descriptor changes from affecting
the measurements in captureStandardOutputDoesNotLeakPipeFDs.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: aa5b0431-bd14-4177-b259-3905a3346665

📥 Commits

Reviewing files that changed from the base of the PR and between 9abb4e8 and b185e99.

⛔ Files ignored due to path filters (1)
  • .github/swift-file-length-budget.tsv is excluded by !**/*.tsv
📒 Files selected for processing (5)
  • Sources/AppDelegate.swift
  • Sources/PortScanner.swift
  • Sources/TerminalController.swift
  • cmuxTests/PaneMemoryGuardrailTests.swift
  • cmuxTests/PortScannerTests.swift

Comment thread cmuxTests/PortScannerTests.swift Outdated
@austinywang
austinywang merged commit 1a91035 into main Jun 22, 2026
29 of 30 checks passed
azooz2003-bit added a commit that referenced this pull request Jul 3, 2026
…e silently dropped — #6518 (terminal input after window key restore), #6508 (defer restored WebViews until visible), #6559 (avoid DevTools teardown on redock), #6583 (gate idle port scanning to active workspace), #6528 (canvas scroll-hint debug menu), #6580 (drop re-introduced sideDock promote block). #6582/#6517 deferred (see merge-deferred-gaps)

This branch was successfully deployed

1 active deployment
Preview – cmux — f6ca98c4 Deployed Jun 22, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

UI becomes unresponsive: main thread saturated by timer-driven pollers invalidating SwiftUI graph

1 participant