Skip to content

Split BrainBar daemon and UI#298

Merged
EtanHey merged 1 commit into
fix/brainbar-hybrid-parityfrom
refactor/daemon-ui-split
May 18, 2026
Merged

Split BrainBar daemon and UI#298
EtanHey merged 1 commit into
fix/brainbar-hybrid-parityfrom
refactor/daemon-ui-split

Conversation

@EtanHey
Copy link
Copy Markdown
Owner

@EtanHey EtanHey commented May 18, 2026

Summary

  • Adds a separate BrainBarDaemon SwiftPM executable that owns the BrainBar MCP socket, brain bus, single-writer server path, and hybrid helper lifecycle.
  • Keeps BrainBar as the LSUIElement UI process with NSStatusItem/NSPopover from item test: QA integration tests + fix FTS5 query escaping #6 and a reconnecting BrainBusClient subscriber from item feat(engine): Think/Recall/Sessions intelligence layer #5.
  • Updates build-app.sh to build both products, embed both binaries into BrainBar.app, and install two ProcessType=Interactive LaunchAgents.
  • Aligns fresh Swift-created DBs with the Phase D Python helper by keeping schema_migrations.details present.
  • Documents the two-process UDS topology in README.md.

30-second lecture demo

bash brain-bar/build-app.sh
launchctl print gui/$(id -u)/com.brainlayer.brainbar-daemon | head -40
launchctl print gui/$(id -u)/com.brainlayer.brainbar | head -40
pgrep -fl 'BrainBar|BrainBarDaemon'
kill -9 $(pgrep -x BrainBar)
python3 - <<'PY'
import json, socket
path = '/tmp/brainbar.sock'

def send(sock, payload):
    body = json.dumps(payload, separators=(',', ':')).encode()
    sock.sendall(f'Content-Length: {len(body)}\r\n\r\n'.encode() + body)

def recv(sock):
    data = b''
    while b'\r\n\r\n' not in data:
        data += sock.recv(1)
    header, body = data.split(b'\r\n\r\n', 1)
    length = int([line.split(':', 1)[1] for line in header.decode().split('\r\n') if line.lower().startswith('content-length:')][0])
    while len(body) < length:
        body += sock.recv(length - len(body))
    return json.loads(body[:length])

with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock:
    sock.connect(path)
    send(sock, {"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"lecture-demo","version":"1"}}})
    print(recv(sock).get('result', {}).get('serverInfo'))
PY
launchctl kickstart -k gui/$(id -u)/com.brainlayer.brainbar
sleep 2
pgrep -fl 'BrainBar|BrainBarDaemon'

Expected: killing BrainBar does not remove /tmp/brainbar.sock; the daemon still answers MCP; kickstarting the UI relaunches the menu-bar process.

Verification

  • TDD red: swift test --package-path brain-bar --filter DatabaseTests/testSchemaMigrationsTableIncludesDetailsForHybridHelperParity failed before the Swift schema migration added details.
  • pytest tests/test_brainbar_build_app_guards.py -q -> 13 passed.
  • swift build --package-path brain-bar --product BrainBar -> passed.
  • swift build --package-path brain-bar --product BrainBarDaemon -> passed.
  • Isolated daemon smoke: launched BrainBarDaemon with temp BRAINBAR_SOCKET_PATH + BRAINBAR_DB_PATH; initialize succeeded and tools/list returned 16 tools over UDS.
  • swift test --package-path brain-bar -> 363 passed.
  • Pre-push gate -> pytest unit suite 2018 passed, 9 skipped, 75 deselected, 1 xfailed; MCP registration 3 passed; isolated eval/hook 32 passed; Bun 1 passed; FTS5 regression passed.

Note

Medium Risk
Splits BrainBar into two cooperating processes and updates launchd packaging/build scripts, which can impact startup/IPC reliability and deployment behavior. Also adjusts SQLite schema migration compatibility, which could affect fresh DB initialization if incorrect.

Overview
BrainBar is split into two SwiftPM executables: BrainBarDaemon now owns the MCP server and /tmp/brainbar.sock, while BrainBar becomes a UI-only menu bar shell that no longer starts/stops the server or requires database readiness for URL handling.

The build and install flow is updated to build/embed both binaries into BrainBar.app and install two ProcessType=Interactive LaunchAgents (com.brainlayer.brainbar + com.brainlayer.brainbar-daemon), with new tests and a new daemon LaunchAgent plist.

SQLite startup migrations are tweaked to ensure schema_migrations.details exists (and is backfilled via ALTER TABLE when missing) to maintain compatibility with the hybrid helper.

Reviewed by Cursor Bugbot for commit 983a1a3. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Split BrainBar into separate UI and headless BrainBarDaemon processes

  • Introduces a new BrainBarDaemon executable (BrainBarDaemonMain.swift) that owns the MCP server, Unix domain socket (/tmp/brainbar.sock), SQLite DB, and helper process lifecycle, running as a persistent launchd agent via com.brainlayer.brainbar-daemon.plist.
  • The BrainBar UI app (BrainBarApp.swift) no longer starts or manages the server, database, or InjectionStore; it assumes the daemon is already running.
  • Package.swift declares both executables; build-app.sh builds, validates, and installs both binaries and their respective LaunchAgents.
  • Shared source files are symlinked into Sources/BrainBarDaemon/ rather than duplicated.
  • BrainDatabase.ensureChunkColumns now adds a details column to schema_migrations if absent.
  • Behavioral Change: quick capture panel invocation in non-menu-bar-window mode now logs and returns instead of opening a panel; isReadyToHandleBrainBarURL always returns true.
📊 Macroscope summarized 983a1a3. 7 files reviewed, 2 issues evaluated, 0 issues filtered, 1 comment posted

🗂️ Filtered Issues

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 18, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 1edb290f-9bd1-4323-a0bb-bd11745fb3e0

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/daemon-ui-split

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.

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.

@EtanHey
Copy link
Copy Markdown
Owner Author

EtanHey commented May 18, 2026

@cursor review

Please review the BrainBar Swift split, especially AppDelegate runtime wiring, NSStatusItem/NSPopover continuity from item #6, and reconnect/no-poll assumptions from item #5.

@EtanHey
Copy link
Copy Markdown
Owner Author

EtanHey commented May 18, 2026

@BugBot review

Please check for launchd lifecycle regressions, socket ownership/races, helper subprocess startup issues, and single-writer violations.

@EtanHey
Copy link
Copy Markdown
Owner Author

EtanHey commented May 18, 2026

@codex review

Focus on the daemon/UI process boundary, launchd plist/install behavior, Phase D helper compatibility, and whether any UI path still starts an in-process server or writes directly.

Comment thread brain-bar/build-app.sh
}

# Install LaunchAgents (expands path to actual APP_DIR)
if [ "$DEV_BUNDLE_BUILD" -eq 0 ]; then
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟢 Low brain-bar/build-app.sh:321

The success message "[build-app] LaunchAgents installed — daemon and UI restart independently" at line 328 prints unconditionally, even when neither $DAEMON_PLIST_SRC nor $UI_PLIST_SRC exists and no LaunchAgent was actually installed. Consider only printing the message when at least one plist file exists and install_launchagent was invoked.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file brain-bar/build-app.sh around line 321:

The success message `"[build-app] LaunchAgents installed — daemon and UI restart independently"` at line 328 prints unconditionally, even when neither `$DAEMON_PLIST_SRC` nor `$UI_PLIST_SRC` exists and no LaunchAgent was actually installed. Consider only printing the message when at least one plist file exists and `install_launchagent` was invoked.

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: 983a1a353a

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +97 to 100
let collector = BrainBarAppSupport.makeUIStatsCollector(
dbPath: dbPath,
targetPID: ProcessInfo.processInfo.processIdentifier,
brainBusEvents: BrainBusClient()
)
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 Stop opening the database from the UI collector

In the split UI process, this still constructs a StatsCollector, whose initializer creates a BrainDatabase(path:); that connection opens READWRITE | CREATE, runs PRAGMAs, and calls ensureMigrations() on startup. So every BrainBar UI launch can still acquire write locks and mutate schema directly instead of only watching the daemon/socket, which violates the new single-writer process boundary and can race the daemon on the canonical WAL DB.

Useful? React with 👍 / 👎.

Comment on lines +102 to +106
runtime.install(
collector: collector,
injectionStore: nil,
database: 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.

P1 Badge Restore a daemon-backed command bar model

With runtime.database permanently installed as nil, BrainBarWindowRootView never creates its QuickCaptureViewModel because commandBarProvider.viewModel(database:) only becomes ready for a non-nil database. In menuBarWindow mode, the Capture/Search menu commands and hotkey now only show the popover with the “Warming memory…” placeholder forever, so the UI quick-capture/search path is disabled unless it is replaced with a socket-backed model.

Useful? React with 👍 / 👎.

@EtanHey EtanHey merged commit fed97bb into fix/brainbar-hybrid-parity May 18, 2026
3 checks passed
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 3 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 983a1a3. Configure here.

targetPID: 0,
brainBusEvents: brainBusEvents
)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

UI dashboard permanently stuck in degraded state

High Severity

makeUIStatsCollector passes targetPID: 0 to DaemonHealthMonitor. Since DaemonHealthMonitor.sample() has a guard targetPID > 0 else { return nil } check, it always returns nil. This causes PipelineState.derive(daemon: nil, ...) to always evaluate as .degraded, and DashboardFlowSummary to always show "Pipeline visibility is degraded" with all pipeline indicators as .unavailable — regardless of actual daemon health.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 983a1a3. Configure here.

server.onDatabaseReady = { [weak self] database in
Task { @MainActor in
guard let self else { return }
self.sharedDatabase = database
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

UI process writes to database violating single-writer design

Medium Severity

The UI process's StatsCollector creates a BrainDatabase(path: dbPath) that calls openAndConfigure()ensureMigrations(), which executes DDL statements (CREATE VIEW IF NOT EXISTS, ALTER TABLE ADD COLUMN, etc.) against the same database the daemon owns. This violates the single-writer architecture the PR establishes, where BrainBarDaemon is documented as owning "the single-writer path."

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 983a1a3. Configure here.

NSLog("[BrainBar] Starting UI shell; daemon owns %@", BrainBarServer.defaultSocketPath())
let collector = BrainBarAppSupport.makeUIStatsCollector(
dbPath: dbPath,
targetPID: ProcessInfo.processInfo.processIdentifier,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale log message claims socket ownership the UI lacks

Low Severity

The log message "Socket ready; database will self-heal" is a leftover from the pre-split code. The UI process no longer owns a socket or database — the daemon does. This makes diagnostic logs actively misleading during incident response.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 983a1a3. Configure here.

EtanHey added a commit that referenced this pull request May 18, 2026
* fix: route BrainBar search through Python hybrid helper

* fix: harden BrainBar hybrid helper startup

* fix: address BrainBar hybrid review followups

* fix: report hybrid helper launch failures

* fix: keep source all unfiltered for entity routing

* fix: bound hybrid helper socket waits

* fix: harden hybrid helper fallback cleanup

* fix: sanitize hybrid helper responses

* feat: stream BrainBar brain bus events

* refactor: use NSStatusItem popover shell

* fix: prevent SIGPIPE from hybrid helper socket writes

* fix: route BrainBar search through Python hybrid helper

* fix: harden BrainBar hybrid helper startup

* fix: address BrainBar hybrid review followups

* fix: report hybrid helper launch failures

* fix: keep source all unfiltered for entity routing

* fix: bound hybrid helper socket waits

* fix: harden hybrid helper fallback cleanup

* fix: sanitize hybrid helper responses

* feat: stream BrainBar brain bus events

* refactor: use NSStatusItem popover shell

* fix: prevent SIGPIPE from hybrid helper socket writes

* fix: serialize BrainBus socket writes

* refactor: split BrainBar daemon and UI (#298)

* fix: refresh BrainBar stats before bus events

* test: wait for BrainBar socket readiness

* test: relax arbitration drain deadline

* fix: open BrainBar UI stats database read-only (#300)
EtanHey added a commit that referenced this pull request May 22, 2026
After PR #312 removed the FastAPI daemon, the UI process must open
SQLite directly. BrainBarApp.applicationDidFinishLaunching was still
calling runtime.install(injectionStore: nil, database: nil) — a holdover
from when the daemon owned the DB and the UI consumed via socket. Since
BrainBarRuntime.database / .injectionStore are @published private(set)
and only mutable via install(), they stayed nil forever, leaving the
command bar stuck on "Warming memory…" and the Injections tab on the
"feed not wired" placeholder.

Extract a testable BrainBarAppSupport.wireRuntime(_:dbPath:collector:)
helper that opens BrainDatabase read-only (so the writer pidfile stays
uncontended with the Python supervisor) and constructs an InjectionStore
(which owns its own writable connection for ack writes). Replace the
nil-hardcoded install call in BrainBarApp.swift with this helper.

Add BrainBarRuntimeWiringTests as a regression guard — if anyone re-
introduces the nil install, the test fails with a comment pointing back
to PR #312 and the gating UI placeholders.

Regression history:
- fed97bb (PR #298): refactor split daemon + UI; UI consumed DB via daemon socket
- 692839c / e609d84 (PR #312): removed FastAPI daemon — UI was left waiting for a daemon that no longer exists

Co-Authored-By: Claude Opus 4.7 (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