Skip to content

fix: async init + DB lock prevention for BrainBar launch#160

Merged
EtanHey merged 1 commit into
mainfrom
fix/async-init-db-lock
Mar 31, 2026
Merged

fix: async init + DB lock prevention for BrainBar launch#160
EtanHey merged 1 commit into
mainfrom
fix/async-init-db-lock

Conversation

@EtanHey
Copy link
Copy Markdown
Owner

@EtanHey EtanHey commented Mar 31, 2026

Summary

  • Status item shows instantly (239ms boot vs infinite hang)
  • DB init moved to background queue — no more blocking on watch agent lock
  • brainbar:// URL scheme added to bundle plist

Root Cause

PRAGMA journal_mode = WAL requires a write lock even when WAL is already set. Watch agent holds this lock during enrichment batches → BrainBar init blocked forever → no menu item.

Test plan

  • swift test --package-path brain-bar — 205 tests pass
  • bash brain-bar/build-app.sh — builds and signs
  • Manual: menu item appears instantly, F4 works, store works, search works

Requesting review from @coderabbitai

Note

Fix async DB init and lock contention at BrainBar launch

  • Moves database, server, collector, and injection store initialization to a background queue in BrainBarApp.swift, so the status bar item appears immediately at launch with a loading tooltip instead of blocking the main thread.
  • Clicking the status item before initialization now shows a BrainBar loading database... popover; once ready, the existing status item is upgraded in-place with live sparkline and a BrainBar — connected tooltip.
  • In BrainDatabase.swift, busy_timeout is raised from 5s to 30s, journal_mode=WAL is only issued when not already set, and ensureSchema is skipped if the chunks table already exists — all reducing write-lock contention on startup.
  • Registers the brainbar:// URL scheme in Info.plist.
📊 Macroscope summarized 24439a3. 3 files reviewed, 2 issues evaluated, 0 issues filtered, 1 comment posted

🗂️ Filtered Issues

Summary by CodeRabbit

  • New Features

    • Added support for custom URL scheme handling.
  • Bug Fixes & Improvements

    • Status bar now appears immediately on launch, with a loading indicator during database initialization.
    • Optimized database startup by moving heavy initialization to the background.

Root cause: BrainDatabase init blocked indefinitely on SQLite write lock
held by the watch agent. PRAGMA journal_mode=WAL requires a write lock
even when WAL is already set. This prevented the status item from ever
appearing.

Fixes:
- Status item created IMMEDIATELY in applicationDidFinishLaunching (no DB)
- All DB/server/collector init moved to background DispatchQueue
- Skip PRAGMA journal_mode=WAL if already WAL (avoids write lock)
- Skip ensureSchema if chunks table exists (avoids RESERVED lock)
- busy_timeout increased to 30s for enrichment batch lock contention
- Loading popover shown on click before DB is ready
- brainbar:// URL scheme added to bundle Info.plist (persists across rebuilds)

Boot time: infinite → 239ms

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

@greptile-apps greptile-apps Bot left a comment

Choose a reason for hiding this comment

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

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 31, 2026

Caution

Review failed

Pull request was closed or merged during review

📝 Walkthrough

Walkthrough

The changes implement early UI initialization with deferred backend setup, add database connection lifecycle logging with conditional schema creation, and register a custom URL scheme for the application.

Changes

Cohort / File(s) Summary
App Startup and UI Initialization
brain-bar/Sources/BrainBar/BrainBarApp.swift
Moved status bar creation to launch, made database/server initialization asynchronous with background dispatch, refactored configureStatusItem to upgradeStatusItem to update UI once backend is ready, and updated togglePopover to show a temporary loading state before main popover is available.
Database Connection and Lifecycle
brain-bar/Sources/BrainBar/BrainDatabase.swift
Added connection lifecycle logging, made schema creation conditional by checking table existence via new queryPragma() helper, reordered pragmas with busy_timeout set before journal configuration, and changed journal_mode to conditionally apply WAL mode only when not already active.
URL Scheme Registration
brain-bar/bundle/Info.plist
Added CFBundleURLTypes configuration to register custom brainbar URL scheme with bundle identifier com.brainlayer.brainbar.

Sequence Diagram(s)

sequenceDiagram
    participant App as BrainBarApp
    participant UI as Status Bar UI
    participant BG as Background Queue
    participant DB as BrainDatabase
    participant Server as BrainBarServer
    participant Collector as StatsCollector

    App->>UI: Create NSStatusItem immediately
    App->>UI: Show temporary "loading" state
    App->>BG: Dispatch async backend initialization
    
    BG->>DB: Open and configure
    DB->>DB: Check if schema exists
    alt Schema doesn't exist
        DB->>DB: Create schema
    end
    DB-->>BG: Ready
    
    BG->>Server: Construct BrainBarServer
    Server-->>BG: Ready
    
    BG->>Collector: Create StatsCollector
    Collector-->>BG: Ready
    
    BG->>App: Switch to main thread
    App->>App: Assign shared instances
    App->>Server: Start server
    App->>Collector: Start collector
    App->>UI: Upgrade status item with real popover
    UI-->>UI: Replace loading popover with operational UI
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 Hop! The status bar appears,
While databases wake from their digital sleep,
No more waiting—the UI dons its coat,
Backend tasks dispatch with a wink and a note,
The popover dances when ready at last!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title concisely and accurately captures the two main fixes: async initialization to prevent blocking on launch and database lock prevention—both core issues resolved in the changeset.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/async-init-db-lock

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.

Comment on lines +79 to +86
let collector = StatsCollector(
dbPath: dbPath,
daemonMonitor: DaemonHealthMonitor(targetPID: ProcessInfo.processInfo.processIdentifier)
)

let injStore = try? InjectionStore(databasePath: dbPath)
NSLog("[BrainBar] Backend loaded — injectionStore=%@", injStore != nil ? "OK" : "nil")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Medium BrainBar/BrainBarApp.swift:79

StatsCollector is marked @MainActor but is synchronously instantiated on a background thread via DispatchQueue.global(qos: .userInitiated).async. In Swift 6 this violates actor isolation and will cause a runtime crash or data race. Move the instantiation to the main actor (e.g., wrap in Task { @MainActor in … } or instantiate on DispatchQueue.main).

-            let collector = StatsCollector(
-                daemonMonitor: DaemonHealthMonitor(targetPID: ProcessInfo.processInfo.processIdentifier)
-            )
+            let collector = await MainActor.run {
+                StatsCollector(
+                    dbPath: dbPath,
+                    daemonMonitor: DaemonHealthMonitor(targetPID: ProcessInfo.processInfo.processIdentifier)
+                )
+            }
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file brain-bar/Sources/BrainBar/BrainBarApp.swift around lines 79-86:

`StatsCollector` is marked `@MainActor` but is synchronously instantiated on a background thread via `DispatchQueue.global(qos: .userInitiated).async`. In Swift 6 this violates actor isolation and will cause a runtime crash or data race. Move the instantiation to the main actor (e.g., wrap in `Task { @MainActor in … }` or instantiate on `DispatchQueue.main`).

Evidence trail:
1. `brain-bar/Sources/BrainBar/Dashboard/StatsCollector.swift` lines 18-19: `@MainActor final class StatsCollector: ObservableObject {`
2. `brain-bar/Sources/BrainBar/Dashboard/StatsCollector.swift` lines 30-45: The `init(dbPath:daemonMonitor:)` is not marked `nonisolated`, so it inherits `@MainActor` isolation.
3. `brain-bar/Sources/BrainBar/BrainBarApp.swift` line 66: `DispatchQueue.global(qos: .userInitiated).async { [weak self] in`
4. `brain-bar/Sources/BrainBar/BrainBarApp.swift` lines 79-82: `let collector = StatsCollector(dbPath: dbPath, daemonMonitor: DaemonHealthMonitor(...))` is called synchronously inside the background queue closure.

@EtanHey EtanHey merged commit 0fe5403 into main Mar 31, 2026
2 of 6 checks passed
EtanHey added a commit that referenced this pull request Mar 31, 2026
…er size

Five QA fixes from user testing after overnight sprint PRs #155-#160:

(1) Store freezes UI: Added storeAsync() — runs DB write on background
    DispatchQueue via withCheckedThrowingContinuation. QuickCapture
    submitCapture now uses async path, UI stays responsive.

(2) Search results missing dates: SearchQueryCandidate now includes
    date, project, importance fields. searchCandidates SQL extended to
    SELECT created_at, project, importance. SearchViewModel passes
    these through to SearchResult for display.

(3) Enter in search goes to capture: Changed applySelectedSearchResult()
    to copy result to clipboard instead of switching to capture mode.
    Also auto-selects first result when selectedResultIndex is nil.

(4) Popover oversized: Reduced contentSize from 420x620 to 360x320.

(5) VoiceBar duplicate struct: Merged voicelayer PR #109.

Also: Updated busy_timeout test to match PR #160's 30s value.

212 tests, 0 failures. Build and sign pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
EtanHey added a commit that referenced this pull request Mar 31, 2026
…er size (#161)

Five QA fixes from user testing after overnight sprint PRs #155-#160:

(1) Store freezes UI: Added storeAsync() — runs DB write on background
    DispatchQueue via withCheckedThrowingContinuation. QuickCapture
    submitCapture now uses async path, UI stays responsive.

(2) Search results missing dates: SearchQueryCandidate now includes
    date, project, importance fields. searchCandidates SQL extended to
    SELECT created_at, project, importance. SearchViewModel passes
    these through to SearchResult for display.

(3) Enter in search goes to capture: Changed applySelectedSearchResult()
    to copy result to clipboard instead of switching to capture mode.
    Also auto-selects first result when selectedResultIndex is nil.

(4) Popover oversized: Reduced contentSize from 420x620 to 360x320.

(5) VoiceBar duplicate struct: Merged voicelayer PR #109.

Also: Updated busy_timeout test to match PR #160's 30s value.

212 tests, 0 failures. Build and sign pass.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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