Skip to content

ADE-69: PR/GitHub subsystem: freshness/ETag correctness, merge-rail bugs, switchBranch stale cleanup, polling cursors, index.lock, decomposition & PR-detail polish#498

Merged
arul28 merged 1 commit into
mainfrom
ade-69-pr-github-subsystem-freshness-etag-correctness-merge-rail-bugs-switchbranch-stale-cleanup-polling-cursors-index-lock-decomposition-pr-detail-polish
Jun 1, 2026
Merged

ADE-69: PR/GitHub subsystem: freshness/ETag correctness, merge-rail bugs, switchBranch stale cleanup, polling cursors, index.lock, decomposition & PR-detail polish#498
arul28 merged 1 commit into
mainfrom
ade-69-pr-github-subsystem-freshness-etag-correctness-merge-rail-bugs-switchbranch-stale-cleanup-polling-cursors-index-lock-decomposition-pr-detail-polish

Conversation

@arul28
Copy link
Copy Markdown
Owner

@arul28 arul28 commented Jun 1, 2026

Fixes ADE-69

Summary

Describe the change.

What Changed

Key files and behaviors.

Validation

How you tested.

Risks

Anything to watch.

Linked Linear issues

ADE   Open in ADE  ·  ade-69-pr-github-subsystem-freshness-etag-correctness-merge-rail-bugs-switchbranch-stale-cleanup-polling-cursors-index-lock-decomposition-pr-detail-polish branch  ·  PR #498

Greptile Summary

This PR addresses a broad set of correctness issues in the PR/GitHub subsystem: ETag cache eviction safety, polling cursor freshness after downtime, stale conflict predictions, index.lock recovery hardening, and PR merge-rail UX polish. It also extracts PR row deletion into a shared module and fixes several silent-swallow error patterns across the rebase banner and tab.

  • ETag cache: Adds inFlightConditionalGetKeys to protect cache entries from eviction while a conditional GET is in flight; the eviction loop and eviction helper are updated to respect this set.
  • Polling cursors: First-seen comments and reviews are now emitted when a durable since cursor is available (post-restart), fixing missed events after downtime; pollReviews gains a since parameter for filtering old reviews on restart.
  • Staleness & merge correctness: Conflict-matrix staleness now checks lane HEAD SHA changes, behindBaseBy becomes number | null throughout so compare failures don't silently masquerade as "zero commits behind", and the land path adds a pre-merge draft/conflict check before calling GitHub's merge API.

Confidence Score: 3/5

Safe to merge for the majority of users; one narrow but real scenario in the ETag cache can cause polling errors under high concurrency when the cache is at capacity.

The concurrent-conditional-GET race in githubService.ts is the main concern: two requests for the same URL both add the protection key, but the first to complete removes it, leaving the second unprotected. If an eviction fires in that window, the 304 handler finds no cached data and throws, surfacing as a polling failure. This is hard to hit in practice (requires ≥200 cached URLs and two overlapping requests to the same endpoint) but the failure mode is a thrown error rather than a graceful degradation. Everything else in the PR — polling cursor fixes, SHA-based staleness, merge-rail guards, error surfacing, PR row cleanup refactor — looks correct and well-tested.

apps/desktop/src/main/services/github/githubService.ts (ETag cache race), apps/desktop/src/main/services/git/git.ts (synchronous lsof call), apps/desktop/src/main/services/ai/tools/ctoOperatorTools.ts (outdated-thread state inconsistency)

Important Files Changed

Filename Overview
apps/desktop/src/main/services/github/githubService.ts Adds in-flight ETag cache protection via inFlightConditionalGetKeys; has a concurrent-request race where two GETs for the same URL can leave the key unprotected, causing a 304-with-empty-cache error at capacity.
apps/desktop/src/main/services/git/git.ts Raises stale-lock threshold to 2 min and adds lsof-based live-process check; the execFileSync call blocks the event loop up to 2 s on each recovery attempt.
apps/desktop/src/main/services/prs/pullRequestRowCleanup.ts New shared module for PR row deletion; correctly handles pr_group_members, cascade deletes, and empty-group pruning for both ID-based and lane-based deletions.
apps/desktop/src/main/services/prs/prService.ts Adds draft/conflict pre-check before GitHub merge API call; consolidates error-swallowing with bestEffort; changes compare-failure fallback to null instead of 0.
apps/desktop/src/renderer/components/prs/shared/prMergeRailUtils.ts Fixes draft-PR merge guard and NULL-safe behindBaseBy handling; null compare status silently maps to 0 in deriveMergeBlockers, hiding unknown state from the user.
apps/desktop/src/main/services/automations/githubPollingService.ts Fixes first-seen comment/review emission after downtime by using the durable repo cursor; adds since parameter to pollReviews for cursor-based filtering on restart.
apps/desktop/src/main/services/prs/pathToMergeOrchestrator.ts Treats null behindBaseBy conservatively — runs base-advance sync when the compare result is unknown, preventing premature merge attempts on stale state.
apps/desktop/src/main/services/ai/tools/ctoOperatorTools.ts Removes isOutdated from fixed-state condition; fallback path now returns "new" for outdated+unresolved threads, diverging from the issueInventoryService behavior that skips them.

Sequence Diagram

sequenceDiagram
    participant Poller as githubPollingService
    participant GH as githubService.apiRequest
    participant ECache as etagCache (Map)
    participant Fly as inFlightConditionalGetKeys (Set)

    Poller->>GH: GET /repos/... (cached ETag present)
    GH->>Fly: add(urlKey)
    GH->>GH: fetch (await - yields event loop)
    Note over GH,Fly: concurrent eviction window

    alt Single in-flight (fixed)
        GH-->>GH: 304 received
        GH->>Fly: delete(urlKey) [finally]
        GH->>ECache: get(urlKey) → cached ✓
        GH-->>Poller: return cached data
    else Two concurrent GETs for same key (race)
        GH-->>GH: Request 1 completes
        GH->>Fly: delete(urlKey) [finally] — key now unprotected
        Note over Fly,ECache: eviction can now remove urlKey entry
        GH-->>GH: Request 2 gets 304
        GH->>Fly: delete(urlKey) [no-op]
        GH->>ECache: get(urlKey) → undefined ✗
        GH-->>Poller: throws "GitHub API request failed (HTTP 304)"
    end
Loading

Comments Outside Diff (2)

  1. apps/desktop/src/main/services/github/githubService.ts, line 633-645 (link)

    P1 304 cache-miss when two concurrent conditional GETs share the same URL key

    inFlightConditionalGetKeys uses a plain Set, so two concurrent requests for the same urlKey both add the key, but the first to complete removes it in finally, leaving the second request unprotected. If the cache is at capacity and another apiRequest fires a new URL between those two completions, evictOldestEtagCacheEntry will evict the shared key. The second request then receives a 304 and reaches line 641 where etagCache.get(urlKey) returns undefined; the code falls through to response.text(), parses an empty 304 body as {}, then throws on !response.ok (304 is not ok): GitHub API request failed (HTTP 304). This would surface as a polling error when the GitHub API cache is at capacity and two overlapping polls for the same resource are in flight.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: apps/desktop/src/main/services/github/githubService.ts
    Line: 633-645
    
    Comment:
    **304 cache-miss when two concurrent conditional GETs share the same URL key**
    
    `inFlightConditionalGetKeys` uses a plain `Set`, so two concurrent requests for the same `urlKey` both add the key, but the first to complete removes it in `finally`, leaving the second request unprotected. If the cache is at capacity and another `apiRequest` fires a new URL between those two completions, `evictOldestEtagCacheEntry` will evict the shared key. The second request then receives a 304 and reaches line 641 where `etagCache.get(urlKey)` returns `undefined`; the code falls through to `response.text()`, parses an empty 304 body as `{}`, then throws on `!response.ok` (`304` is not `ok`): `GitHub API request failed (HTTP 304)`. This would surface as a polling error when the GitHub API cache is at capacity and two overlapping polls for the same resource are in flight.
    
    How can I resolve this? If you propose a fix, please make it concise.

    Fix in Claude Code

  2. apps/desktop/src/main/services/ai/tools/ctoOperatorTools.ts, line 883-885 (link)

    P2 Fallback inventory treats outdated threads as "new", diverging from issueInventoryService

    issueInventoryService.ts was updated to skip brand-new outdated threads (thread.isOutdated && !existing) rather than marking them "fixed". The fallback path here still classifies any non-resolved thread (including outdated ones) as "new". When the full DB sync hasn't run yet, the CTO operator may receive an outdated review thread as an open action item and attempt to address it, even though the main sync would have dropped it entirely.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: apps/desktop/src/main/services/ai/tools/ctoOperatorTools.ts
    Line: 883-885
    
    Comment:
    **Fallback inventory treats outdated threads as `"new"`, diverging from `issueInventoryService`**
    
    `issueInventoryService.ts` was updated to skip brand-new outdated threads (`thread.isOutdated && !existing`) rather than marking them `"fixed"`. The fallback path here still classifies any non-resolved thread (including outdated ones) as `"new"`. When the full DB sync hasn't run yet, the CTO operator may receive an outdated review thread as an open action item and attempt to address it, even though the main sync would have dropped it entirely.
    
    How can I resolve this? If you propose a fix, please make it concise.

    Fix in Claude Code

Fix All in Claude Code

Prompt To Fix All With AI
Fix the following 4 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 4
apps/desktop/src/main/services/github/githubService.ts:633-645
**304 cache-miss when two concurrent conditional GETs share the same URL key**

`inFlightConditionalGetKeys` uses a plain `Set`, so two concurrent requests for the same `urlKey` both add the key, but the first to complete removes it in `finally`, leaving the second request unprotected. If the cache is at capacity and another `apiRequest` fires a new URL between those two completions, `evictOldestEtagCacheEntry` will evict the shared key. The second request then receives a 304 and reaches line 641 where `etagCache.get(urlKey)` returns `undefined`; the code falls through to `response.text()`, parses an empty 304 body as `{}`, then throws on `!response.ok` (`304` is not `ok`): `GitHub API request failed (HTTP 304)`. This would surface as a polling error when the GitHub API cache is at capacity and two overlapping polls for the same resource are in flight.

### Issue 2 of 4
apps/desktop/src/main/services/git/git.ts:112-125
**`execFileSync('lsof', …)` blocks the event loop for up to 2 seconds**

`recoverStaleIndexLock` is called synchronously inside the git error-recovery path. Introducing a `timeout: 2_000` blocking `execFileSync` here will stall every other pending I/O — IPC messages from the renderer, timers, in-flight promises — for up to 2 seconds whenever the 2-minute age threshold is crossed and `activeGitPids` is empty.

### Issue 3 of 4
apps/desktop/src/renderer/components/prs/shared/prMergeRailUtils.ts:70-76
**Null `behindBaseBy` silently collapses to 0, hiding unknown compare status**

When the GitHub compare API fails, `behindBaseBy` is now `null` (the `bestEffort` fallback in `prService.ts`). This coercion maps `null → 0`, so `behindBaseBy > 0` is false and no "behind" blocker is shown — effectively displaying "not behind" when the status is actually unknown.

```suggestion
  const behindBaseBy = status?.behindBaseBy;
  if (behindBaseBy != null && behindBaseBy > 0) {
    blockers.push({
      id: "behind",
      label: `The head branch is ${behindBaseBy} commit${behindBaseBy === 1 ? "" : "s"} behind the base branch.`,
    });
  }
```

### Issue 4 of 4
apps/desktop/src/main/services/ai/tools/ctoOperatorTools.ts:883-885
**Fallback inventory treats outdated threads as `"new"`, diverging from `issueInventoryService`**

`issueInventoryService.ts` was updated to skip brand-new outdated threads (`thread.isOutdated && !existing`) rather than marking them `"fixed"`. The fallback path here still classifies any non-resolved thread (including outdated ones) as `"new"`. When the full DB sync hasn't run yet, the CTO operator may receive an outdated review thread as an open action item and attempt to address it, even though the main sync would have dropped it entirely.

Reviews (1): Last reviewed commit: "Fix ADE-69 Linear links and PR freshness" | Re-trigger Greptile

Greptile also left 2 inline comments on this PR.

@linear-code
Copy link
Copy Markdown

linear-code Bot commented Jun 1, 2026

ADE-69

@vercel
Copy link
Copy Markdown

vercel Bot commented Jun 1, 2026

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

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
ade Ignored Ignored Preview Jun 1, 2026 12:21am

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 1, 2026

Warning

Review limit reached

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

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

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ 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.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: fe50fa4e-2ea4-4e0b-9b5f-034d4f8f4f3f

📥 Commits

Reviewing files that changed from the base of the PR and between 8fdf95a and a3dd3eb.

📒 Files selected for processing (32)
  • apps/ade-cli/src/adeRpcServer.test.ts
  • apps/ade-cli/src/adeRpcServer.ts
  • apps/ade-cli/src/cli.test.ts
  • apps/ade-cli/src/cli.ts
  • apps/desktop/src/main/services/ai/tools/ctoOperatorTools.ts
  • apps/desktop/src/main/services/automations/githubPollingService.test.ts
  • apps/desktop/src/main/services/automations/githubPollingService.ts
  • apps/desktop/src/main/services/conflicts/conflictService.test.ts
  • apps/desktop/src/main/services/conflicts/conflictService.ts
  • apps/desktop/src/main/services/git/git.test.ts
  • apps/desktop/src/main/services/git/git.ts
  • apps/desktop/src/main/services/github/githubService.test.ts
  • apps/desktop/src/main/services/github/githubService.ts
  • apps/desktop/src/main/services/lanes/laneService.test.ts
  • apps/desktop/src/main/services/lanes/laneService.ts
  • apps/desktop/src/main/services/prs/issueInventoryService.ts
  • apps/desktop/src/main/services/prs/pathToMergeOrchestrator.ts
  • apps/desktop/src/main/services/prs/prIssueResolution.test.ts
  • apps/desktop/src/main/services/prs/prService.test.ts
  • apps/desktop/src/main/services/prs/prService.ts
  • apps/desktop/src/main/services/prs/pullRequestRowCleanup.ts
  • apps/desktop/src/renderer/components/prs/PrRebaseBanner.test.tsx
  • apps/desktop/src/renderer/components/prs/PrRebaseBanner.tsx
  • apps/desktop/src/renderer/components/prs/detail/PrDetailPane.tsx
  • apps/desktop/src/renderer/components/prs/shared/PrDetailMergeRail.test.tsx
  • apps/desktop/src/renderer/components/prs/shared/PrDetailMergeRail.tsx
  • apps/desktop/src/renderer/components/prs/shared/prMergeRailUtils.test.ts
  • apps/desktop/src/renderer/components/prs/shared/prMergeRailUtils.ts
  • apps/desktop/src/renderer/components/prs/tabs/IntegrationTab.tsx
  • apps/desktop/src/renderer/components/prs/tabs/RebaseTab.tsx
  • apps/desktop/src/renderer/components/review/ReviewPage.tsx
  • apps/desktop/src/shared/types/prs.ts
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ade-69-pr-github-subsystem-freshness-etag-correctness-merge-rail-bugs-switchbranch-stale-cleanup-polling-cursors-index-lock-decomposition-pr-detail-polish

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.

@arul28 arul28 force-pushed the ade-69-pr-github-subsystem-freshness-etag-correctness-merge-rail-bugs-switchbranch-stale-cleanup-polling-cursors-index-lock-decomposition-pr-detail-polish branch from 14da1d3 to 43d8978 Compare June 1, 2026 00:18
@arul28
Copy link
Copy Markdown
Owner Author

arul28 commented Jun 1, 2026

@copilot review but do not make fixes

@arul28 arul28 force-pushed the ade-69-pr-github-subsystem-freshness-etag-correctness-merge-rail-bugs-switchbranch-stale-cleanup-polling-cursors-index-lock-decomposition-pr-detail-polish branch from 43d8978 to a3dd3eb Compare June 1, 2026 00:21
Comment on lines +112 to +125
function isIndexLockHeldByProcess(lockPath: string): boolean {
if (activeGitPids.size > 0) return true;
if (process.platform === "win32") return false;
try {
const out = execFileSync("lsof", [lockPath], {
encoding: "utf8",
timeout: 2_000,
stdio: ["ignore", "pipe", "ignore"],
});
return out.trim().split(/\r?\n/).length > 1;
} catch {
return false;
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 execFileSync('lsof', …) blocks the event loop for up to 2 seconds

recoverStaleIndexLock is called synchronously inside the git error-recovery path. Introducing a timeout: 2_000 blocking execFileSync here will stall every other pending I/O — IPC messages from the renderer, timers, in-flight promises — for up to 2 seconds whenever the 2-minute age threshold is crossed and activeGitPids is empty.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src/main/services/git/git.ts
Line: 112-125

Comment:
**`execFileSync('lsof', …)` blocks the event loop for up to 2 seconds**

`recoverStaleIndexLock` is called synchronously inside the git error-recovery path. Introducing a `timeout: 2_000` blocking `execFileSync` here will stall every other pending I/O — IPC messages from the renderer, timers, in-flight promises — for up to 2 seconds whenever the 2-minute age threshold is crossed and `activeGitPids` is empty.

How can I resolve this? If you propose a fix, please make it concise.

Fix in Claude Code

Comment on lines +70 to 76
const behindBaseBy = status?.behindBaseBy ?? 0;
if (behindBaseBy > 0) {
blockers.push({
id: "behind",
label: `The head branch is ${status.behindBaseBy} commit${status.behindBaseBy === 1 ? "" : "s"} behind the base branch.`,
label: `The head branch is ${behindBaseBy} commit${behindBaseBy === 1 ? "" : "s"} behind the base branch.`,
});
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Null behindBaseBy silently collapses to 0, hiding unknown compare status

When the GitHub compare API fails, behindBaseBy is now null (the bestEffort fallback in prService.ts). This coercion maps null → 0, so behindBaseBy > 0 is false and no "behind" blocker is shown — effectively displaying "not behind" when the status is actually unknown.

Suggested change
const behindBaseBy = status?.behindBaseBy ?? 0;
if (behindBaseBy > 0) {
blockers.push({
id: "behind",
label: `The head branch is ${status.behindBaseBy} commit${status.behindBaseBy === 1 ? "" : "s"} behind the base branch.`,
label: `The head branch is ${behindBaseBy} commit${behindBaseBy === 1 ? "" : "s"} behind the base branch.`,
});
}
const behindBaseBy = status?.behindBaseBy;
if (behindBaseBy != null && behindBaseBy > 0) {
blockers.push({
id: "behind",
label: `The head branch is ${behindBaseBy} commit${behindBaseBy === 1 ? "" : "s"} behind the base branch.`,
});
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src/renderer/components/prs/shared/prMergeRailUtils.ts
Line: 70-76

Comment:
**Null `behindBaseBy` silently collapses to 0, hiding unknown compare status**

When the GitHub compare API fails, `behindBaseBy` is now `null` (the `bestEffort` fallback in `prService.ts`). This coercion maps `null → 0`, so `behindBaseBy > 0` is false and no "behind" blocker is shown — effectively displaying "not behind" when the status is actually unknown.

```suggestion
  const behindBaseBy = status?.behindBaseBy;
  if (behindBaseBy != null && behindBaseBy > 0) {
    blockers.push({
      id: "behind",
      label: `The head branch is ${behindBaseBy} commit${behindBaseBy === 1 ? "" : "s"} behind the base branch.`,
    });
  }
```

How can I resolve this? If you propose a fix, please make it concise.

Fix in Claude Code

@arul28 arul28 merged commit 13972d3 into main Jun 1, 2026
27 checks passed
@arul28 arul28 deleted the ade-69-pr-github-subsystem-freshness-etag-correctness-merge-rail-bugs-switchbranch-stale-cleanup-polling-cursors-index-lock-decomposition-pr-detail-polish branch June 1, 2026 00:35
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