Skip to content

feat(staged): centralize PR polling with tiered intervals and PR recovery#650

Open
matt2e wants to merge 7 commits intomainfrom
no-pr-updates
Open

feat(staged): centralize PR polling with tiered intervals and PR recovery#650
matt2e wants to merge 7 commits intomainfrom
no-pr-updates

Conversation

@matt2e
Copy link
Copy Markdown
Contributor

@matt2e matt2e commented Apr 22, 2026

Summary

  • Replace per-component PR polling in BranchCardPrButton with a centralized prPollingService that polls all projects app-wide with tiered intervals (15s for pending CI, 60s for selected project, 5min for background projects)
  • Add recover_branch_pr backend command to look up existing open PRs on GitHub for branches missing PR numbers, with frontend coordination to prevent concurrent gh pr view calls
  • Fix race condition where session completes before frontend callback fires by emitting "running" event atomically from backend create_pr and push_branch commands

Test plan

  • Verify PR status updates correctly for the selected project at ~60s intervals
  • Verify projects with pending CI checks poll at ~15s intervals
  • Verify background (non-selected) projects poll at ~5min intervals
  • Verify polling pauses when window loses focus and resumes on focus
  • Verify stale indicator appears after 3 consecutive polling failures
  • Verify PR recovery works for branches that were pushed but lost their PR number
  • Verify no duplicate gh pr view calls when multiple branch cards mount simultaneously

🤖 Generated with Claude Code

matt2e and others added 7 commits April 22, 2026 17:05
Each BranchCardPrButton ran its own setInterval for PR status polling,
leading to N concurrent gh CLI processes, stuck refreshing guards, and
leaked event listeners. This replaces that with a single PrPollingService
that coordinates all PR status refreshes via the existing backend
refreshAllPrStatuses command.

Key changes:
- New PrPollingService with single timer, adaptive intervals, window
  focus awareness, concurrency protection, and stale-data tracking
- Fix event listener race condition by awaiting listen() promises in
  cleanup instead of fire-and-forget .then()
- Remove prStatusRefreshing guard, per-component setInterval, and
  window focus/blur handlers from BranchCardPrButton
- Add stale-data indicator when polling fails repeatedly

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…component

Add [PrPolling] prefixed console.info logs to help diagnose PR status
update failures. Logs cover track/untrack, poll start/skip/end,
scheduleNext intervals, refreshNow accept/drop, window focus/blur,
pr-status-changed events, and $effect lifecycle in the component.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The create_pr and push_branch commands now emit a "running" session
event (with branch_id, project_id, and session_type) before returning
the session ID. This ensures the global sessionStatusListener registers
the session before any completion event can arrive, fixing the race
where fast-completing sessions hit "unknown session ID" warnings and
PR numbers are never persisted.

Remove the now-redundant sessionRegistry.register() and
projectStateStore.addRunningSession() calls from the component's
.then() callbacks.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When a branch has been pushed but has no prNumber (e.g. due to a race
condition during PR creation), the BranchCardPrButton now checks GitHub
for an existing open PR on mount via `gh pr view <branch>`. This runs
async so the frontend isn't blocked. If a PR is found, the number is
persisted and polling starts immediately.

New backend command `recover_branch_pr` wraps the existing
`get_pr_for_branch` GitHub CLI helper and updates the branch record.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… component

The info-level console.info logs were added to diagnose PR status update
failures and are no longer needed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The polling service now refreshes all projects, not just the one being
viewed. The selected project polls at the existing 60s interval, while
background projects poll every 5 minutes. Projects with pending CI
checks still use the faster 15s interval regardless of selection.

Key changes:
- Replace branch-level track/untrack API with project-level setProjects
  and setSelectedProject, driven by reactive $effects in App.svelte
- Per-project lastPolledAt tracking so each project polls independently
  at its own interval via a single adaptive timer
- Remove component-driven track/untrack from BranchCardPrButton; stale
  notifications now keyed by projectId instead of branchId

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…anup

Resolve code review feedback on the centralized PR polling service:

- poll(): remove scheduleNext() from the early-return path when
  refreshInFlight is true — the in-flight operation's finally block
  already reschedules, avoiding a potential busy-wait cycle
- setProjects(): short-circuit when the set of project IDs hasn't
  changed, preventing unnecessary poll triggers from Svelte $effect
  re-fires
- refreshNow(): queue the projectId when a refresh is already in-flight
  instead of silently dropping it, so post-PR-creation and post-push
  refreshes are never lost
- Stale notification: use exact equality (=== MAX_CONSECUTIVE_FAILURES)
  so the callback fires only on the transition to stale, not every
  subsequent failure cycle
- Move unsubStale from onMount/onDestroy to $effect with cleanup,
  making it immune to double-mount (HMR, keyed re-render) and
  automatically reactive to branch.projectId changes
- Add shouldAttemptRecovery() guard in the polling service to prevent
  N concurrent `gh pr view` CLI calls when many BranchCardPrButton
  components mount simultaneously for branches without PR numbers
@matt2e matt2e requested review from baxen and wesbillman as code owners April 22, 2026 07:20
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c7abde0b8d

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +159 to +165
session_runner::emit_session_running(
&app_handle,
&session.id,
&branch_id,
&branch.project_id,
"pr",
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Emit running status only after session start succeeds

This emits a running session event before start_session(...) can fail, so if startup fails (for example provider/driver initialization errors), the command returns an error but the frontend has already registered the session as running via the global listener and may never receive a terminal event to clean it up. That leaves project/session UI stuck in a running state for PR creation (and the same ordering is repeated in the push path below).

Useful? React with 👍 / 👎.

Comment on lines +123 to +124
refreshInFlight = false;
scheduleNext();
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Drain queued immediate refreshes after scheduled polls

refreshNow() queues pendingRefreshProjectId whenever refreshInFlight is true, but this path only gets drained in refreshNow(...).finally. When the in-flight operation is poll(), execution reaches this block and schedules the next timer without consuming the queued project, so immediate refresh requests that happen during a scheduled poll are silently deferred until the normal interval.

Useful? React with 👍 / 👎.

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.

1 participant