Skip to content

Fix daily backup via BrainBar VACUUM INTO#294

Merged
EtanHey merged 1 commit into
mainfrom
fix/backup-daily-vacuum-into
May 18, 2026
Merged

Fix daily backup via BrainBar VACUUM INTO#294
EtanHey merged 1 commit into
mainfrom
fix/backup-daily-vacuum-into

Conversation

@EtanHey
Copy link
Copy Markdown
Owner

@EtanHey EtanHey commented May 18, 2026

Summary

  • Route backup_daily snapshot creation through BrainBar's existing Unix socket with a new brain_backup_vacuum_into MCP tool.
  • Run SQLite VACUUM INTO on BrainBar's database connection, preserving the single-writer queue instead of opening a competing sqlite3.backup() reader.
  • Add ExitTimeOut=300 to the daily backup LaunchAgent while retaining the calendar schedule and no KeepAlive.

Fixes the 6h57m runaway via single-writer VACUUM INTO over the existing UDS. launchd swap eliminates self-perpetuation.

Tests

  • PYTHONPATH=src pytest tests/test_backup_daily.py -q
  • swift test --package-path brain-bar --filter SocketIntegrationTests/testBrainBackupVacuumIntoOverSocketCreatesRestorableSnapshot
  • swift test --package-path brain-bar
  • ruff check src/ tests/ && ruff format --check src/ tests/
  • plutil -lint scripts/launchd/com.brainlayer.backup-daily.plist
  • launchctl print gui/$(id -u)/com.brainlayer.backup-daily showed calendar interval trigger and keepalive = 0 for the installed job

Notes

  • Broad PYTHONPATH=src pytest tests -q --ignore=tests/regression/test_drift_detection.py --ignore=tests/test_eval_framework.py is blocked locally by existing live production DB locks plus unrelated eval/hook timing assertions: 2034 passed, 3 failed, 62 errors.
  • CodeRabbit CLI pre-commit review was attempted but rate-limited before findings were produced.

Note

Medium Risk
Changes the production backup path to rely on a new BrainBar MCP tool and SQLite VACUUM INTO, plus adds a hard wall-clock timeout; failures could break scheduled backups if the socket/tool is unavailable or path validation rejects the target.

Overview
Routes daily BrainLayer backup creation through BrainBar’s Unix-socket MCP interface by adding a new brain_backup_vacuum_into tool that runs SQLite VACUUM INTO on BrainBar’s single-writer connection and returns snapshot metadata.

Updates backup_daily.py to request the snapshot over the socket (instead of using sqlite3’s backup API) and adds a configurable wall-clock timeout (BRAINLAYER_BACKUP_TIMEOUT_SECONDS) with a launchd ExitTimeOut=300 default. Tests are expanded to cover the new tool listing, end-to-end socket backup + restore validation, and the Python timeout/socket behavior.

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

Note

Fix daily backup by delegating SQLite snapshot creation to BrainBar via VACUUM INTO

  • Replaces the direct SQLite backup API in backup_daily.py with a Unix socket call to BrainBar's new brain_backup_vacuum_into MCP tool, ensuring the snapshot is created through BrainBar's single-writer connection.
  • Adds BrainDatabase.vacuumInto(targetPath:) in BrainDatabase.swift and the corresponding brain_backup_vacuum_into tool routing in MCPRouter.swift.
  • Adds a configurable wall-clock timeout (BRAINLAYER_BACKUP_TIMEOUT_SECONDS, default 300s) enforced via SIGALRM in main(); exits with code 124 on timeout.
  • Sets ExitTimeOut 300 in the launchd plist and exports the timeout default in the shell wrapper.
  • Behavioral Change: backups now require BrainBar to be running and reachable over its Unix socket; the SQLite online backup API is no longer used directly.
📊 Macroscope summarized 7034ddc. 5 files reviewed, 2 issues evaluated, 0 issues filtered, 2 comments posted

🗂️ Filtered Issues

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 18, 2026

Warning

Rate limit exceeded

@EtanHey has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 55 minutes and 7 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, 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 have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 3276cddf-47b9-4fc8-879f-3319645684fe

📥 Commits

Reviewing files that changed from the base of the PR and between 33db7ff and 7034ddc.

📒 Files selected for processing (8)
  • brain-bar/Sources/BrainBar/BrainDatabase.swift
  • brain-bar/Sources/BrainBar/MCPRouter.swift
  • brain-bar/Tests/BrainBarTests/MCPRouterTests.swift
  • brain-bar/Tests/BrainBarTests/SocketIntegrationTests.swift
  • scripts/launchd/backup-daily.sh
  • scripts/launchd/com.brainlayer.backup-daily.plist
  • src/brainlayer/backup_daily.py
  • tests/test_backup_daily.py
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/backup-daily-vacuum-into

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

@coderabbitai review

@EtanHey
Copy link
Copy Markdown
Owner Author

EtanHey commented May 18, 2026

@codex review

@EtanHey
Copy link
Copy Markdown
Owner Author

EtanHey commented May 18, 2026

@greptileai review

@EtanHey
Copy link
Copy Markdown
Owner Author

EtanHey commented May 18, 2026

@cursor @BugBot review

@cursor
Copy link
Copy Markdown

cursor Bot commented May 18, 2026

You need to increase your spend limit or enable usage-based billing to run background agents. Go to Cursor

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 18, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

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.

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: fc51176ae8

ℹ️ 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 39 to +40

<key>ExitTimeOut</key>
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 Badge Enforce the backup timeout in the job itself

For runs that hang while the process is active, such as during Drive upload after the snapshot is made, this key does not impose a 5-minute runtime limit. I checked the launchd.plist(5) semantics: ExitTimeOut is the grace period between launchd sending SIGTERM and then SIGKILL when it is already stopping a job, not a maximum wall-clock duration. Since this agent only uses StartCalendarInterval and the wrapper has no whole-job watchdog, a stuck backup can still remain alive indefinitely; the timeout needs to be enforced in backup-daily.sh or backup_daily.py if the daily job must be bounded.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed. The launchd wrapper now exports BRAINLAYER_BACKUP_TIMEOUT_SECONDS=300 by default, and backup_daily.main() enforces that wall-clock timeout with SIGALRM and exits 124 on timeout. Added a regression test for the timeout path.

@EtanHey EtanHey force-pushed the fix/backup-daily-vacuum-into branch from fc51176 to 7034ddc Compare May 18, 2026 09:42
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

@codex review

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: 7034ddce0d

ℹ️ 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".

raw_snapshot = Path(tmp) / f"{date_stamp}.db"
source = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True, timeout=60)
target = sqlite3.connect(raw_snapshot)
request_brainbar_vacuum_into(raw_snapshot, socket_path=socket_path)
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 Preserve the selected database when backing up

When db_path is non-canonical or comes from the documented BRAINLAYER_DB override, this now only checks/sizes/reports that path but asks BrainBar to VACUUM INTO whatever database its own process has open (BRAINBAR_DB_PATH or the canonical default). In that configuration the scheduled/manual backup can upload a valid snapshot of the wrong database while reporting the requested db_path, which makes restores lose the intended data; the request needs to verify the BrainBar DB matches db_path or pass the expected source through the tool contract.

Useful? React with 👍 / 👎.

Comment on lines +432 to +435
finally:
if timeout_seconds is not None:
signal.setitimer(signal.ITIMER_REAL, 0)
signal.signal(signal.SIGALRM, previous_alarm_handler)
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 brainlayer/backup_daily.py:432

When signal.getsignal(signal.SIGALRM) returns None (e.g., if the handler was set by C code or a library), passing that None to signal.signal(signal.SIGALRM, previous_alarm_handler) in the finally block raises TypeError: signal handler must be signal.SIG_IGN, signal.SIG_DFL, or a callable. This crashes the cleanup after a successful backup or timeout.

-    finally:
-        if timeout_seconds is not None:
-            signal.setitimer(signal.ITIMER_REAL, 0)
-            signal.signal(signal.SIGALRM, previous_alarm_handler)
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file src/brainlayer/backup_daily.py around lines 432-435:

When `signal.getsignal(signal.SIGALRM)` returns `None` (e.g., if the handler was set by C code or a library), passing that `None` to `signal.signal(signal.SIGALRM, previous_alarm_handler)` in the `finally` block raises `TypeError: signal handler must be signal.SIG_IGN, signal.SIG_DFL, or a callable`. This crashes the cleanup after a successful backup or timeout.

Evidence trail:
src/brainlayer/backup_daily.py lines 410-437 (REVIEWED_COMMIT): line 414 calls `signal.getsignal(signal.SIGALRM)` which can return `None` per Python docs; line 435 in the `finally` block passes that value to `signal.signal(signal.SIGALRM, previous_alarm_handler)` which does not accept `None`. Python docs: https://docs.python.org/3/library/signal.html confirms `getsignal()` may return `None` and `signal.signal()` requires SIG_IGN, SIG_DFL, or a callable.

Comment on lines +479 to +480
let targetURL = URL(fileURLWithPath: trimmedTarget).standardizedFileURL
let sourceURL = URL(fileURLWithPath: path).standardizedFileURL
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/BrainDatabase.swift:479

standardizedFileURL does not resolve symlinks, so the safety check at line 481 can be bypassed when paths differ by symlink. For example, on macOS /tmp is a symlink to /private/tmp, so /tmp/db.sqlite and /private/tmp/db.sqlite compare as different but refer to the same file, allowing VACUUM INTO to overwrite the live database. Consider using resolvingSymlinksInPath() before comparing.

-        let targetURL = URL(fileURLWithPath: trimmedTarget).standardizedFileURL
-        let sourceURL = URL(fileURLWithPath: path).standardizedFileURL
+        let targetURL = URL(fileURLWithPath: trimmedTarget).standardizedFileURL.resolvingSymlinksInPath()
+        let sourceURL = URL(fileURLWithPath: path).standardizedFileURL.resolvingSymlinksInPath()
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file brain-bar/Sources/BrainBar/BrainDatabase.swift around lines 479-480:

`standardizedFileURL` does not resolve symlinks, so the safety check at line 481 can be bypassed when paths differ by symlink. For example, on macOS `/tmp` is a symlink to `/private/tmp`, so `/tmp/db.sqlite` and `/private/tmp/db.sqlite` compare as different but refer to the same file, allowing `VACUUM INTO` to overwrite the live database. Consider using `resolvingSymlinksInPath()` before comparing.

Evidence trail:
brain-bar/Sources/BrainBar/BrainDatabase.swift lines 472-498 (viewed at REVIEWED_COMMIT): `vacuumInto` uses `standardizedFileURL` at lines 479-480, compares paths at line 481. Apple documentation at https://developer.apple.com/documentation/foundation/url/standardizedfileurl confirms standardizedFileURL does not resolve symlinks. Apple documentation at https://developer.apple.com/documentation/foundation/nsurl/resolvingsymlinksinpath confirms resolvingSymlinksInPath does resolve symlinks. macOS `/tmp` → `/private/tmp` is a well-known symlink example.

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 1 potential issue.

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 7034ddc. Configure here.

[
"name": "brain_backup_vacuum_into",
"description": "Create a SQLite backup snapshot using VACUUM INTO on BrainBar's single-writer connection.",
"annotations": MCPRouter.writeIdempotentAnnotations,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Non-idempotent backup tool annotated as idempotent

Low Severity

The brain_backup_vacuum_into tool uses writeIdempotentAnnotations which sets idempotentHint: true, but vacuumInto explicitly rejects repeat calls with the same target_path because it guards against the target file already existing. MCP clients seeing idempotentHint: true may automatically retry failed or timed-out calls, which will always fail on the second attempt with "backup target already exists."

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 7034ddc. Configure here.

@EtanHey EtanHey merged commit 50fd1d7 into main May 18, 2026
7 checks passed
@EtanHey EtanHey deleted the fix/backup-daily-vacuum-into branch May 18, 2026 09:52
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