Skip to content

Back off PR refresh cadence for branches with no PR (#3136) - #5125

Open
mverrilli wants to merge 1 commit into
manaflow-ai:mainfrom
mverrilli:fix-pr-poll-cadence-3136
Open

mverrilli wants to merge 1 commit into
manaflow-ai:mainfrom
mverrilli:fix-pr-poll-cadence-3136

Conversation

@mverrilli

@mverrilli mverrilli commented Jun 1, 2026 •

Copy link
Copy Markdown

Summary

  • Branches without a PR were re-polled at the same fast cadence as branches with an open PR (every 10s when focused), hammering the GitHub API. They rarely gain a PR without a local action (push, gh pr create), and those fire event-driven refreshes that bypass the periodic cadence — so back off to 60s for no-PR branches and 5min for non-GitHub repos. Focused open-PR cadence relaxes from 10s to 15s.
  • Classify refresh outcomes in one place (classifyWorkspacePullRequestPollOutcome) rather than splitting the mapping across two early-return guards plus a tail switch. Adding a new Resolution case now touches one site.
  • Reschedule pending polls on selectedTabId change. The focused/unfocused cadence asymmetry took effect only on the next refresh; now focus changes recompute target nextPollAt immediately, so Cmd-Tab into an open-PR workspace honors the 15s focused cadence without waiting out the prior 60s. (Within-tab panel focus changes are still not hooked; noted in the doc comment.)
  • Centralize jitter inside nextWorkspacePullRequestPollAt so all cadence paths spread load, including terminal-sweep transitions (previously un-jittered, prone to thundering-herd on bulk merges).
  • Keep transient-failure retries on a dedicated 10s constant so they don't ride the (now slower) open-PR cadence change.
  • isSelectedFocusedPanel reads selectedWorkspace once instead of twice.

Fixes #3136.

Test plan

  • ./scripts/reload.sh --tag fix-pr-poll-3136 builds cleanly
  • Open workspace with an open PR: badge refreshes ~every 15s when focused, ~60s when unfocused
  • Open workspace with no PR: badge refresh quiets to ~60s regardless of focus
  • Open non-GitHub workspace: PR probe quiets to ~5min
  • Cmd-Tab from an unfocused-scheduled workspace into it: next refresh fires within 15s (focused cadence), not 60s
  • Run gh pr create in the integrated terminal of a no-PR workspace: PR badge appears via the event-driven refresh path (not the 60s periodic)
  • Bulk-merge multiple PRs: re-poll wake-ups are spread across the jitter window rather than firing simultaneously

View with Codesmith Autofix with Codesmith
Need help on this PR? Tag @codesmith with what you need. Autofix is disabled.


Summary by cubic

Backed off PR refresh polling to reduce GitHub API load and make focus changes apply immediately. No-PR branches poll every 60s, non-GitHub repos every 5m, and open PRs every 15s when focused (60s unfocused). Fixes #3136.

  • Bug Fixes

    • Outcome-based intervals: no PR 60s, unsupported repo 5m, open PR 15s focused / 60s unfocused.
    • Recompute next poll on selection change; newly focused panel’s deadline is pulled forward.
    • Add jitter to all paths, including terminal-state sweeps; transient failures retry at 10s (or sweep interval if previously terminal).
  • Refactors

    • Centralize classification and scheduling in classifyWorkspacePullRequestPollOutcome and nextWorkspacePullRequestPollAt; add WorkspacePullRequestPollOutcome.
    • Track last outcome per key for rescheduling and pruning; add selectedFocusedPanel() and invoke reschedule on workspace selection.

Written for commit ce64373. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • Performance Improvements

    • Polling for GitHub PRs is now focus-aware: active/focused PR tabs refresh more responsively while background or unsupported views poll less frequently to save resources.
    • Switching tabs triggers an immediate reschedule so the newly viewed PRs refresh promptly.
  • Bug Fixes

    • Improved cleanup of polling state to avoid stale or duplicate refreshes after panel/workspace changes.

@vercel

vercel Bot commented Jun 1, 2026

Copy link
Copy Markdown

@mverrilli is attempting to deploy a commit to the Manaflow Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Jun 1, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

TabManager.swift updates GitHub PR polling to be responsive to tab focus. Polling intervals are tuned, per-probe outcome tracking is added, poll scheduling is refactored into outcome classification and focus-aware interval computation with jitter, and cleanup paths are extended to maintain the new state.

Changes

Focus-Aware GitHub PR Polling

Layer / File(s) Summary
Polling interval tuning
Sources/TabManager.swift
PR polling intervals are adjusted: selected PR polling increases to 15s, and new fixed cadences are set for no-PR, unsupported-repo, and transient-failure branches.
Focus-driven rescheduling foundation
Sources/TabManager.swift
A rescheduling trigger is added to immediately recompute poll times when selectedTabId changes so focused probes can have earlier deadlines applied.
Per-probe outcome storage
Sources/TabManager.swift
Adds a dictionary that stores the last classified poll outcome per probe key for later focus-aware deadline recomputation.
Poll outcome classification and focus-aware scheduling
Sources/TabManager.swift
Polling logic is refactored into helpers: refresh results are classified into WorkspacePullRequestPollOutcome, nextWorkspacePullRequestPollAt computes next wake times with jitter using outcome and focus, and scheduling now stores per-probe last-outcome and prunes it alongside other tracking maps.
Pruning and cleanup integration
Sources/TabManager.swift
Single-probe and workspace-wide PR tracking cleanup now remove last-outcome entries, and PR refresh-state reset clears the outcome dictionary.
Helper simplification
Sources/TabManager.swift
isSelectedFocusedPanel is simplified to early-return false when selectedWorkspace is missing before comparing workspace and panel ids.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • manaflow-ai/cmux#4820: Adjusts sidebar PR polling logic in Sources/TabManager.swift through timer/probe scheduling and state-clearing/guard paths for when PR polling should run.

Poem

🐰 A rabbit hops through tabs with grace,
No longer polls at frantic pace,
When focus shifts, the timers dance,
Outcomes stored, schedules advance—
Quiet ticks now keep the place.


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 Actor Isolation ❌ Error File-scoped Logger tabManagerLogger (line 15) should be marked nonisolated per swift-actor-isolation.md rule to avoid unnecessary main actor coupling. Add nonisolated keyword: nonisolated private let tabManagerLogger = Logger(subsystem: "com.cmuxterm.app", category: "TabManager")
Cmux Algorithmic Complexity ❌ Error rescheduleWorkspacePullRequestPollsForFocusChange() on hot UI tab-switch path calls updateWorkspacePullRequestPollTimer() which does .values.min() over all probe keys, an O(n) scan. Cache minimum poll deadline separately and update only when keys added/removed, not on every tab switch.
Docstring Coverage ⚠️ Warning Docstring coverage is 16.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (15 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change—backing off PR refresh cadence for branches without a PR—which aligns with the primary focus of the changeset.
Linked Issues check ✅ Passed The code changes comprehensively address issue #3136 by reducing API load through outcome-based polling intervals, adding jitter to avoid thundering herds, preserving event-driven refreshes, and implementing immediate focus-aware rescheduling.
Out of Scope Changes check ✅ Passed All changes in TabManager.swift are directly related to poll scheduling, outcome classification, and focus-aware rescheduling—core to addressing the rate-limiting issue and are within the stated PR objectives.
Cmux Swift Blocking Runtime ✅ Passed PR uses non-blocking DispatchSource timers for polling with async dispatch; Task.sleep only in test scaffolding; no blocking waits or semaphores in polling paths.
Cmux No Hacky Sleeps ✅ Passed PR's primary changes are Swift code (out of scope for this check). TypeScript files use proper cancellation-aware timeout abstractions with AbortSignal, and test scripts are explicitly allowed.
Cmux Swift Concurrency ✅ Passed PR introduces outcome classification enums and pure synchronous helpers with no legacy async patterns: no DispatchQueue.global, Combine, completion-handlers, or fire-and-forget Tasks.
Cmux Swift @Concurrent ✅ Passed Existing nonisolated async functions unchanged; @concurrent correctly applied to resolveWorkspacePullRequestCandidateSeeds and githubRepositorySlugs; no sync functions marked @concurrent.
Cmux Swift File And Package Boundaries ✅ Passed Adds 113 lines to TabManager.swift, under 250-line threshold. Changes focus on PR polling cadence for issue #3136, no new responsibility mixing, clear extraction path preserved.
Cmux Swift Logging ✅ Passed File uses OSLog/Logger properly, declares logger as private file-scoped, no forbidden logging functions, privacy redaction used, no ad hoc file/stdout logging violations.
Cmux User-Facing Error Privacy ✅ Passed WorkspacePullRequestPollOutcome is internal-only, with values only in debug logs (#if DEBUG). No GitHub errors, rate limits, or vendor details exposed to users.
Cmux Full Internationalization ✅ Passed PR contains only internal implementation changes to PR polling logic with no new user-facing text; all 20 existing localized strings remain properly supported across all required locales.
Cmux Swiftui State Layout ✅ Passed PR modifies only TabManager.swift. New private state is not @Published/@observable and mutations occur in async callbacks, not render-time.
Cmux Architecture Rethink ✅ Passed Fixes API rate-limit root cause with outcome-aware backoff polling, centralizes logic, uses DispatchSourceTimer not delays, maintains single owner, avoids timing repair patterns.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed CmuxMainWindow is allowed as main workspace window. SettingsWindowPresenter has stable "cmux.settings" identifier and is registered in cmuxAuxiliaryWindowIdentifiers.
Description check ✅ Passed The PR description is comprehensive and well-structured, covering all major template sections with detailed information about changes, rationale, and testing plans.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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 1, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR addresses a GitHub API hammering issue (#3136) by introducing differentiated poll cadences: no-PR branches back off to 60s, non-GitHub repos to 5min, and open PRs relax from 10s to 15s when focused (60s unfocused). Rescheduling on selectedTabId change makes the faster focused cadence take effect immediately on tab switch rather than waiting out the prior unfocused window.

  • Centralizes outcome classification into a new WorkspacePullRequestPollOutcome enum and the nextWorkspacePullRequestPollAt static function, so adding a new outcome now touches exactly one site.
  • Adds workspacePullRequestLastOutcomeByKey state consistently across all four eviction paths, enabling the focus-change reschedule hook to recompute deadlines without re-running the full classifier.
  • Narrows the focus-change reschedule to only the newly-focused key, only shortens (never extends) the deadline, and skips focus-insensitive outcomes — correctly addressing the previous jitter-over-all-keys concern.

Confidence Score: 5/5

Safe to merge — the focus-change reschedule is narrowly scoped to the newly-focused key and only shortens existing deadlines, jitter is applied uniformly, and the new outcome dictionary is correctly added to all four eviction paths.

The cadence refactor is well-contained: outcome classification is now a single pure function, the focus-change hook avoids the previously-reported thundering-herd and over-reset problems, and all state cleanup sites were updated consistently. The only finding is a one-word stale cadence value in the class-level doc comment — a documentation accuracy issue with no runtime impact.

The class-level doc comment in PullRequestPollService.swift still says "10s selected" and should read "15s selected" after this PR's cadence change.

Important Files Changed

Filename Overview
Packages/CmuxSidebarGit/Sources/CmuxSidebarGit/Service/PullRequestPollService+Apply.swift Refactors scheduling logic into centralized classifyWorkspacePullRequestPollOutcome + nextWorkspacePullRequestPollAt; adds rescheduleWorkspacePullRequestPollsForFocusChange that only touches the newly-focused key and only shortens deadlines. Cleanup of workspacePullRequestLastOutcomeByKey is consistently added to all four eviction sites.
Packages/CmuxSidebarGit/Sources/CmuxSidebarGit/Service/PullRequestPollService.swift Adds new tuning constants and workspacePullRequestLastOutcomeByKey state dict; class-level doc comment still says "10s selected" after the cadence was bumped to 15s in this PR.
Packages/CmuxSidebarGit/Sources/CmuxSidebarGit/Service/WorkspacePullRequestPollOutcome.swift New Sendable, Equatable enum centralizing the five cadence-relevant outcomes; clean and minimal.
Packages/CmuxSidebarGit/Sources/CmuxSidebarGit/Hosting/SidebarGitHosting.swift Adds selectedFocusedPanel() to the protocol with a clear doc comment scoping it to focus-change rescheduling.
Sources/TabManager+SidebarGitHosting.swift Implements selectedFocusedPanel() by reading selectedWorkspace once; also tightens isSelectedFocusedPanel to avoid a double-dereference of the optional.
Sources/TabManager.swift Calls rescheduleWorkspacePullRequestPollsForFocusChange() in selectedTabId.didSet after selectedTabId is committed, so selectedWorkspace correctly reflects the new selection when the reschedule runs.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant TM as TabManager (selectedTabId.didSet)
    participant PPS as PullRequestPollService
    participant Host as SidebarGitHosting
    participant Timer as Poll Timer

    TM->>PPS: rescheduleWorkspacePullRequestPollsForFocusChange()
    PPS->>Host: selectedFocusedPanel()
    Host-->>PPS: (workspaceId, panelId)
    PPS->>PPS: lookup lastOutcomeByKey[key]
    PPS->>PPS: workspacePullRequestPollOutcomeIsFocusSensitive(outcome)?
    alt outcome is focus-sensitive (openPR / transientFailure w/o terminal)
        PPS->>PPS: nextWorkspacePullRequestPollAt(now, outcome, isFocused: true)
        PPS->>PPS: "if target < current deadline → update nextPollAt"
        PPS->>Timer: updateWorkspacePullRequestPollTimer()
    else outcome is focus-insensitive (noPR / terminal / unsupported)
        PPS-->>TM: return (no change)
    end

    Timer->>PPS: refreshTrackedWorkspacePullRequestsIfNeeded(timer)
    PPS->>PPS: applyWorkspacePullRequestRefreshResults(...)
    PPS->>PPS: classifyWorkspacePullRequestPollOutcome(key, resolution)
    PPS->>PPS: nextWorkspacePullRequestPollAt(now, outcome, isFocused)
    PPS->>PPS: "lastOutcomeByKey[key] = outcome"
    PPS->>Timer: updateWorkspacePullRequestPollTimer()
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 TM as TabManager (selectedTabId.didSet)
    participant PPS as PullRequestPollService
    participant Host as SidebarGitHosting
    participant Timer as Poll Timer

    TM->>PPS: rescheduleWorkspacePullRequestPollsForFocusChange()
    PPS->>Host: selectedFocusedPanel()
    Host-->>PPS: (workspaceId, panelId)
    PPS->>PPS: lookup lastOutcomeByKey[key]
    PPS->>PPS: workspacePullRequestPollOutcomeIsFocusSensitive(outcome)?
    alt outcome is focus-sensitive (openPR / transientFailure w/o terminal)
        PPS->>PPS: nextWorkspacePullRequestPollAt(now, outcome, isFocused: true)
        PPS->>PPS: "if target < current deadline → update nextPollAt"
        PPS->>Timer: updateWorkspacePullRequestPollTimer()
    else outcome is focus-insensitive (noPR / terminal / unsupported)
        PPS-->>TM: return (no change)
    end

    Timer->>PPS: refreshTrackedWorkspacePullRequestsIfNeeded(timer)
    PPS->>PPS: applyWorkspacePullRequestRefreshResults(...)
    PPS->>PPS: classifyWorkspacePullRequestPollOutcome(key, resolution)
    PPS->>PPS: nextWorkspacePullRequestPollAt(now, outcome, isFocused)
    PPS->>PPS: "lastOutcomeByKey[key] = outcome"
    PPS->>Timer: updateWorkspacePullRequestPollTimer()
Loading

Reviews (4): Last reviewed commit: "Back off PR refresh cadence for branches..." | Re-trigger Greptile

Comment thread Sources/TabManager.swift Outdated
@mverrilli
mverrilli force-pushed the fix-pr-poll-cadence-3136 branch from 0c2ff1d to dee4bce Compare June 1, 2026 18:09

@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: 2

🤖 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 `@Sources/TabManager.swift`:
- Around line 2276-2283: The loop is recomputing a fresh jittered target for
every key on each tab switch which re-jitters non-focused outcomes; change the
logic in the block iterating workspacePullRequestLastOutcomeByKey (and the
similar block at 2298-2324) to skip recalculating when a non-focused outcome
(e.g., .noPullRequest or .unsupportedRepository) already has an entry in
workspacePullRequestNextPollAtByKey: retrieve the existing target first, and
only call Self.nextWorkspacePullRequestPollAt(from:outcome:isFocused:) to set a
new target when there is no existing target, the outcome has changed, or the
focus state transitioned to focused (isSelectedFocusedPanel returned true);
reference workspacePullRequestNextPollAtByKey,
workspacePullRequestLastOutcomeByKey,
isSelectedFocusedPanel(workspace:panelId:), and
Self.nextWorkspacePullRequestPollAt to locate the code to modify.
- Around line 2310-2315: The transient-failure branch currently sets base to
backgroundPollInterval when unfocused, which removes the dedicated transient
retry cadence; in the case .transientFailure(let hadTerminalState) block, change
the logic so that when hadTerminalState is false you always set base =
workspacePullRequestTransientFailurePollInterval (ignoring isFocused) and only
use workspacePullRequestTerminalStateSweepInterval when hadTerminalState is
true, keeping backgroundPollInterval out of this branch.
🪄 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: 235ef7d4-f3a7-4dad-be77-c4f51ab8049a

📥 Commits

Reviewing files that changed from the base of the PR and between 264455e and 0c2ff1d.

📒 Files selected for processing (1)
  • Sources/TabManager.swift

Comment thread Sources/TabManager.swift Outdated
Comment thread Sources/TabManager.swift Outdated
@mverrilli
mverrilli force-pushed the fix-pr-poll-cadence-3136 branch from dee4bce to 7a43b2d Compare June 1, 2026 18:11

@cubic-dev-ai cubic-dev-ai 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.

1 issue found across 1 file

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread Sources/TabManager.swift Outdated
@mverrilli
mverrilli force-pushed the fix-pr-poll-cadence-3136 branch from 7a43b2d to a4127a8 Compare June 1, 2026 18:23
A branch with no PR was being re-polled at the same fast cadence as an
open PR (every 10s when focused), hammering the GitHub API. Branches
without a PR rarely gain one without a local action (push, gh pr
create), and those fire event-driven refreshes that bypass the periodic
cadence — so back off to 60s for no-PR branches and 5min for non-GitHub
repos. Focused open-PR cadence relaxes from 10s to 15s.

Additional cleanup folded in:

- Classify refresh outcomes in one place (classifyWorkspacePullRequestPollOutcome)
  rather than splitting the mapping across two early-return guards plus a tail
  switch. Adding a new Resolution case now touches one site.
- Reschedule pending polls when the host's selection changes. The focused/unfocused
  cadence asymmetry took effect only on the NEXT refresh; now focus changes
  recompute target nextPollAt immediately. The hook only shortens deadlines and
  only targets the newly-focused key (not focus-insensitive outcomes), so
  no-PR/unsupported/terminal/transient-with-terminal cadences aren't disturbed.
- Centralize jitter inside nextWorkspacePullRequestPollAt so all cadence paths
  spread load, including terminal-sweep transitions (previously un-jittered,
  prone to thundering-herd on bulk merges).
- Keep transient-failure retries on a dedicated 10s constant so they don't
  ride the (now slower) open-PR cadence.
- isSelectedFocusedPanel reads selectedWorkspace once instead of twice.

The PR-polling subsystem lives in Packages/CmuxSidebarGit since the recent
extraction (manaflow-ai#5907); changes target that package plus the host-side wiring and
the SidebarGitHosting/PullRequestProbing protocol seams.
@mverrilli
mverrilli force-pushed the fix-pr-poll-cadence-3136 branch from a4127a8 to ce64373 Compare June 16, 2026 21:35
@teamleaderleo teamleaderleo added area: performance Latency, CPU, memory, launch time S2: major A crash, hang, lost state, broken connection, or a regression on a path people use labels Sep 30, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: performance Latency, CPU, memory, launch time S2: major A crash, hang, lost state, broken connection, or a regression on a path people use

Projects

None yet

Development

Successfully merging this pull request may close these issues.

GitHub API rate limits - per tab

2 participants