Skip to content

Fix duplicate review sessions on in-flight crow:auto kickoff#407

Merged
dgershman merged 1 commit into
mainfrom
feature/crow-406-duplicate-review-sessions
Jun 2, 2026
Merged

Fix duplicate review sessions on in-flight crow:auto kickoff#407
dgershman merged 1 commit into
mainfrom
feature/crow-406-duplicate-review-sessions

Conversation

@dgershman
Copy link
Copy Markdown
Collaborator

Summary

  • Stop trusting the lagging ReviewRequest.reviewSessionID cross-ref and consult appState.sessions directly via a new AppState.existingReviewSession(forPRURL:) helper.
  • Layer the check at three sites: auto-review watcher guard, createReviewSession early-return backstop, and both Start Review buttons in ReviewBoardView.
  • Extract the PR-URL parser into Session.parseReviewPR(url:) so all sites share one parser.

Why

After #405 moved createReviewSession's git work into Task.detached, the main actor stays responsive during the ~10s clone window. The IssueTracker watcher can now tick during that window — and because request.reviewSessionID is populated lazily on the next refresh, the watcher's existing guard sees stale nil, enqueues a second kickoff, and createReviewSession (with no dedup of its own) creates a duplicate session.

The same lag was also why the per-row Start Review button stayed clickable for several seconds after a session had been created.

Test plan

  • arch -arm64 swift test --package-path Packages/CrowCore — 205 tests pass (includes the new existingReviewSession and parseReviewPR test files)
  • arch -arm64 swift test (root + every package) — all suites pass except a pre-existing flaky retryReadinessEmitsTimedOutWhenSentinelMissing tmux timing test in CrowTerminal that's unrelated to this change
  • swift build --arch arm64 clean
  • Manual smoke (reviewer): with autoReviewRepos configured, open a crow:auto PR, confirm exactly one review-<repo>-<num> session appears even when the IssueTracker ticks multiple times during the clone; confirm the Start Review button flips to "Go to Session" on the same tick the session appears; force-push the PR to confirm the SHA-advanced re-kick path still works.

Closes #406

🤖 Generated with Claude Code

After #405 made `createReviewSession` non-blocking on the main actor, the
IssueTracker watcher can tick during the ~10s clone window and enqueue a
second kickoff for the same PR — the cross-reference field it gates on
(`ReviewRequest.reviewSessionID`) is populated lazily on the next refresh,
so it stays `nil` until after the session has already been appended.

Stop trusting the lagging cross-reference. Consult the authoritative
`appState.sessions` (filtered to active reviews) via a single shared
`existingReviewSession(forPRURL:)` helper. Layered checks:

- Watcher guard in AppDelegate skips kickoff when a session for the PR
  URL already exists in `appState`, regardless of the `request` cross-ref.
- Universal backstop in `SessionService.createReviewSession` early-returns
  the existing session ID. Safe because `enqueueReviewKickoff` is already
  serialized — Task B observes Task A's appended row.
- Single + batch Start Review buttons in `ReviewBoardView` use the same
  helper, so the UI flips to "Go to Session" the instant the row appears
  instead of lagging by an IssueTracker tick.

Also extract the PR-URL parser inlined in `createReviewSession` into
`Session.parseReviewPR(url:)` so all sites share one implementation.

Closes #406

🐦‍⬛ Generated with Claude Code, orchestrated by Crow

Co-Authored-By: Claude <noreply@anthropic.com>
Crow-Session: 8B3BF03E-DEA0-4C05-B65F-D83BC2CF60CB
@dgershman dgershman requested a review from dhilgaertner as a code owner June 2, 2026 20:29
@dgershman dgershman added the crow:merge Crow auto-merge on green label Jun 2, 2026
Copy link
Copy Markdown
Contributor

@dhilgaertner dhilgaertner left a comment

Choose a reason for hiding this comment

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

Code & Security Review

Critical Issues

None.

Security Review

Strengths:

  • Session.parseReviewPR(url:) is pure string parsing with no injection surface; it preserves the exact behavior of the inlined parser it replaces (split(separator: "/"), count >= 5, trailing-segment Int cast).
  • No new untrusted input paths — PR URLs originate from the GitHub-backed ReviewRequest/SessionLink data that already flows through the app.
  • The new existingReviewSession(forPRURL:) is correctly @MainActor-isolated (matches AppState), and every caller (watcher, review-board buttons, SessionService) is already on the main actor, so there's no data-race introduced.

Concerns:

  • None.

Code Quality

  • Dedup is genuinely authoritative, not racy. The serial reviewKickoffTail queue (AppDelegate.swift:173-187) awaits the previous tail, so the second createReviewSession only runs after the first has fully returned — and the session + .pr link are appended synchronously on the main actor at SessionService.swift:1337-1340 after the detached clone await. The backstop at SessionService.swift:1255 therefore always sees the prior session. Sound.
  • Watcher guard change is the right shape. guard (request.reviewSessionID == nil && existingByPR == nil) || shaAdvanced is strictly more restrictive only on the fresh-kickoff path and leaves the shaAdvanced re-kick path (force-push / round-2) untouched, including the stale-session teardown at AppDelegate.swift:637. The A-path completion case still kicks off correctly since existingReviewSession excludes completed/archived sessions (reviewSessions filter at AppState.swift:380-382).
  • Refactor removes real duplication by sharing one parser across SessionService and the dedup helpers, and the .pr-linkType + exact-URL match is consistent across all three call sites (the link is stored from the same prURL the watcher/buttons pass in).
  • Test coverage is thorough — the new suites cover nil/match/completed/archived/non-PR-linkType/duplicate cases for the lookup and owner/repo/number/malformed cases for the parser. All 10 new tests pass locally (swift test --package-path Packages/CrowCore).

Minor, non-blocking observations (no action needed): existingReviewSession is O(sessions × links) and recomputed each watcher iteration, but both sets are tiny; and exact-string URL matching is fine here because the same URL source feeds both the stored link and every lookup.

Summary Table

Color Meaning Verdict effect
Red Must fix Request changes
Yellow Should fix Request changes
Green Consider Approve allowed

Recommendation: Approve — driven by [0 Red, 0 Yellow, 0 Green] findings. Correct, well-isolated, well-tested fix for the in-flight kickoff race.

🐦‍⬛ Reviewed by Crow via Claude Code

@dgershman dgershman merged commit aab2864 into main Jun 2, 2026
3 checks passed
@dgershman dgershman deleted the feature/crow-406-duplicate-review-sessions branch June 2, 2026 20:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

crow:auto crow:merge Crow auto-merge on green

Projects

None yet

Development

Successfully merging this pull request may close these issues.

crow:auto creates duplicate review sessions during in-flight kickoff (regression from #405)

2 participants