Stop completed reviews from restarting on app relaunch#239
Conversation
dgershman
left a comment
There was a problem hiding this comment.
Code & Security Review
Critical Issues
None.
Security Review
Strengths:
- No secrets, credentials, or user-controlled input flow into the new code path.
- The prompt file is constructed from an internally-generated worktree path, not from user-supplied data. The
$(cat ...)shell expansion reads a local file the app itself wrote — no injection vector. store.mutateis protected byNSLock, so the persistence write is thread-safe.
Concerns:
- None identified.
Code Quality
Strengths:
- Clean, minimal diff — only the two files that need to change.
- Backward-compatible decoding defaults
reviewPromptDispatchedtotruefor pre-existing sessions, preventing the exact bug described in CROW-224 on first upgrade. Newly created sessions default tofalseinSession.init, so the flag correctly gates only the first dispatch. - Dual-write pattern (in-memory
appState+store.mutate) is consistent with existing patterns in the file (e.g.,updateSessionStatus,recoverOrphan). - The
var reviewPromptJustDispatchedflag inside the closure + post-closure write avoids mutating shared state inside the closure, keeping the closure side-effect-free.
Minor observations (non-blocking):
- No unit test explicitly covers
reviewPromptDispatcheddecoding/encoding round-trip or thelaunchClaudebranching logic. The existing 141 CrowCore tests pass, andSessionServiceis@MainActor-bound (hard to unit test without the full app harness), so this is understandable — noting it for awareness only. - The
createReviewSessionmethod (line 1023) createsSession(kind: .review, ...)without explicitly passingreviewPromptDispatched: false, relying on the default. This is correct but implicit — perfectly fine given the parameter has a default value.
Summary Table
| Priority | Issue |
|---|---|
| 🟢 Green | Consider adding a codable round-trip test for reviewPromptDispatched in the Session test suite |
Recommendation: Approve — the fix is clean, correctly handles the upgrade path, and aligns with existing patterns in the codebase. The dual-write to appState and store ensures consistency, and the backward-compat decoding default prevents re-triggering on upgrade.
dgershman
left a comment
There was a problem hiding this comment.
Code & Security Review
Security Review
Strengths:
- No user-facing input handling introduced — the new field is purely internal state.
- No secrets, credentials, or external data flows affected.
promptPathis constructed from trusted internal state (worktree path from the persisted store), not from user input.
Concerns:
- None identified.
Code Quality
The implementation is clean and well-scoped:
-
Backward-compatible decoding (
Session.swift:62): DefaultingreviewPromptDispatchedtotruewhen missing from older JSON is the correct choice — existing sessions that already ran their prompt won't re-trigger. -
Immediately-invoked closure pattern (
SessionService.swift:358-370): Mutating a localvarinside an immediately-invoked closure is valid Swift and avoids the need for a separate helper function. SinceSessionServiceis@MainActor, there are no concurrency concerns. -
Dual state update (
SessionService.swift:382-391): Updating both the in-memoryappState.sessionsarray and the persistedstoreensures consistency across the app lifecycle. The guardif reviewPromptJustDispatched, let sessionIDprevents unnecessary writes. -
Default parameter value (
Session.swift:31):reviewPromptDispatched: Bool = falsemeans new sessions start with the prompt not yet dispatched, which is the correct initial state for review sessions.
Summary Table
| Priority | Issue |
|---|---|
| Green | Consider adding a unit test in CrowCore for decoding a Session JSON blob without reviewPromptDispatched to explicitly verify the ?? true fallback |
Recommendation: Approve — the fix is minimal, correctly scoped, backward-compatible, and all 141 CrowCore tests pass. The architecture (MainActor serialization + JSONStore NSLock) prevents any race condition in the persist path.
Review sessions used to re-`cat` their `.crow-review-prompt.md` into Claude on every app launch because `launchClaude` had no way to tell first-launch from a restart. Persist `Session.reviewPromptDispatched` and gate the prompt branch on it: once the prompt fires, subsequent launches fall through to `claude --continue`. Existing persisted sessions decode the missing field as `true` so the bug does not re-trigger after upgrade. Closes #224 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
d4e3e1b to
947b5dc
Compare
Summary
Session.reviewPromptDispatchedis a new persisted boolean that gates whetherlaunchClaudesends the/crow-review-prprompt or falls through toclaude --continue. Once the prompt fires for the first time, the flag flips totrueand is written to the JSON store so subsequent app launches resume the existing Claude conversation instead of re-running the review.true, so the bug does not re-trigger on the first launch after upgrade.Test plan
swift test --package-path Packages/CrowCore(141 tests pass)swift testat root (46 tests pass)swift buildsucceeds with no new warningsclaude --continuerather than re-executing/crow-review-pr <url>, andcat ~/.local/share/crow/store.json | jq '.sessions[] | select(.kind=="review") | {name, reviewPromptDispatched}'showstruereviewPromptDispatchedbecomestruein store.jsonCloses #224