Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 127 additions & 0 deletions docs/backup-strategy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
# BrainLayer Backup Strategy

Status: implemented for daily database snapshots.

## Decision

Use SQLite's online backup API, gzip the resulting snapshot, and upload it directly to Google Drive using the existing `~/.config/google-drive-mcp` OAuth credentials.

Target folder:

`Brain Drive/06_ARCHIVE/backups/brainlayer-db/YYYY-MM-DD.db.gz`

Encryption posture:

Backups are encrypted in transit by HTTPS and at rest by Google's infrastructure. Google holds the provider-side encryption keys. The database can contain user messages, code snippets, file paths, and agent memory, so client-side encryption should be added before upload if the threat model requires protection from the Drive account/provider layer. Recommended upgrade path: encrypt the gzip with `age` or GPG using a key stored in 1Password, then upload `YYYY-MM-DD.db.gz.age` and document the recovery key location.

Schedule:

Daily at 03:17 local time via `com.brainlayer.backup-daily`.

Retention:

Keep the latest 30 daily snapshots plus the latest snapshot for each of the latest 12 months.

## Why This Approach

The database runs in WAL mode and has active writers from BrainBar, enrichment, watch, and maintenance jobs. Copying `brainlayer.db` directly can miss WAL contents or capture an inconsistent file pair. SQLite's online backup API reads through SQLite itself, so the backup is a consistent snapshot without stopping the live services.

Direct Google Drive API upload is used because the post-repair machine no longer has Google Drive Desktop mounted at the old CloudStorage path. Historical DriveFS logs show the previous path was:

`~/Library/CloudStorage/GoogleDrive-etanface@gmail.com/My Drive/Brain Drive`

That mount is not present after repair, and `/Applications/Google Drive.app` is also absent. The API path avoids depending on that local mount.

## Implementation

Repo files:

- `src/brainlayer/backup_daily.py`: creates the SQLite backup, gzips it, uploads it to Drive, and prunes retention.
- `scripts/launchd/backup-daily.sh`: launchd wrapper installed to `~/.local/lib/brainlayer/backup-daily.sh`.
- `scripts/launchd/com.brainlayer.backup-daily.plist`: LaunchAgent template.
- `scripts/launchd/install.sh backup`: installs the wrapper and LaunchAgent.

Local logs:

- `~/.local/share/brainlayer/logs/backup-daily.log`
- `~/.local/share/brainlayer/logs/backup-daily.err`

Manual run:

```bash
PYTHONPATH=~/Gits/brainlayer/src python3 -m brainlayer.backup_daily
```

## Restore Drill

1. Pick the newest good snapshot from Google Drive:

`Brain Drive/06_ARCHIVE/backups/brainlayer-db/YYYY-MM-DD.db.gz`

2. Download it to a local scratch path, for example:

`/tmp/brainlayer-restore/YYYY-MM-DD.db.gz`

3. Decompress and verify integrity:

```bash
mkdir -p /tmp/brainlayer-restore
gunzip -c /tmp/brainlayer-restore/YYYY-MM-DD.db.gz > /tmp/brainlayer-restore/brainlayer.db
sqlite3 /tmp/brainlayer-restore/brainlayer.db 'PRAGMA integrity_check; SELECT count(*) FROM chunks;'
```

4. Stop writers before replacing the live DB:

```bash
launchctl unload ~/Library/LaunchAgents/com.brainlayer.brainbar.plist 2>/dev/null || true
launchctl unload ~/Library/LaunchAgents/com.brainlayer.enrichment.plist 2>/dev/null || true
launchctl unload ~/Library/LaunchAgents/com.brainlayer.watch.plist 2>/dev/null || true
launchctl unload ~/Library/LaunchAgents/com.brainlayer.decay.plist 2>/dev/null || true
```

5. Preserve the corrupted DB and install the restored copy:

```bash
ts="$(date +%Y%m%d-%H%M%S)"
mkdir -p ~/.local/share/brainlayer/corrupt-$ts
ls -lh ~/.local/share/brainlayer/brainlayer.db ~/.local/share/brainlayer/brainlayer.db-wal ~/.local/share/brainlayer/brainlayer.db-shm 2>/dev/null || true
mv ~/.local/share/brainlayer/brainlayer.db* ~/.local/share/brainlayer/corrupt-$ts/
Comment thread
coderabbitai[bot] marked this conversation as resolved.
ls -lh ~/.local/share/brainlayer/corrupt-$ts/
cp /tmp/brainlayer-restore/brainlayer.db ~/.local/share/brainlayer/brainlayer.db
```

The `brainlayer.db*` move preserves the main database plus SQLite auxiliary files: `brainlayer.db`,
`brainlayer.db-wal`, and `brainlayer.db-shm`. Verify the `ls` output before and after the move; the
wildcard will also move any other similarly named files in that directory.

6. Verify the restored DB before re-enabling services:

```bash
sqlite3 ~/.local/share/brainlayer/brainlayer.db 'PRAGMA integrity_check; SELECT count(*) FROM chunks;'
```

7. Re-enable services:

```bash
launchctl load ~/Library/LaunchAgents/com.brainlayer.brainbar.plist
launchctl load ~/Library/LaunchAgents/com.brainlayer.enrichment.plist
launchctl load ~/Library/LaunchAgents/com.brainlayer.watch.plist
launchctl load ~/Library/LaunchAgents/com.brainlayer.decay.plist
```

8. Run a post-restore WAL checkpoint:

```bash
brainlayer wal-checkpoint --mode TRUNCATE
```

## Monthly Drill

Once per month:

1. Download the newest snapshot from Drive.
2. Restore it into `/tmp/brainlayer-restore`.
3. Run `PRAGMA integrity_check` and `SELECT count(*) FROM chunks`.
4. Record the snapshot date, chunk count, and command output in the maintenance log.

Do not replace the live DB during a drill unless the live DB is actually corrupted.
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ dependencies = [
"scikit-learn>=1.0.0", # K-means for cluster-based sampling
"spacy>=3.7,<4.0", # NER for PII sanitization (en_core_web_sm model)
"requests>=2.28.0", # HTTP calls to Ollama for enrichment
"google-api-python-client>=2.0.0", # Daily DB backup upload to Google Drive
"google-auth>=2.0.0", # OAuth refresh for Google Drive backups
"ranx>=0.3.20", # IR evaluation metrics + significance testing
"abydos>=0.5.0", # Beider-Morse phonetic matching for cross-script entity aliases
]
Expand Down
7 changes: 7 additions & 0 deletions scripts/launchd/backup-daily.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#!/usr/bin/env bash
set -euo pipefail

export PATH="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:$HOME/.local/bin"
export PYTHONUNBUFFERED=1

exec "${BRAINLAYER_PYTHON:-python3}" -m brainlayer.backup_daily
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 Use the installed Python for backups

When BrainLayer is installed in a venv, pipx, or any non-default interpreter, the existing launchd jobs still work because their plists execute __BRAINLAYER_BIN__, but this wrapper ignores that configured executable and falls back to launchd's python3. In that setup the scheduled backup can fail at import time for project or Google Drive dependencies even though scripts/launchd/install.sh successfully found a working BrainLayer install; the backup job should run with the same interpreter/environment as the installed CLI or have the installer populate BRAINLAYER_PYTHON.

Useful? React with 👍 / 👎.

43 changes: 43 additions & 0 deletions scripts/launchd/com.brainlayer.backup-daily.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.brainlayer.backup-daily</string>

<!-- __HOME__ and __BRAINLAYER_DIR__ are expanded by scripts/launchd/install.sh
before this template is copied into ~/Library/LaunchAgents. -->

<key>ProgramArguments</key>
<array>
<string>__HOME__/.local/lib/brainlayer/backup-daily.sh</string>
</array>

<key>StartCalendarInterval</key>
<dict>
<key>Hour</key>
<integer>3</integer>
<key>Minute</key>
<integer>17</integer>
</dict>

<key>StandardOutPath</key>
<string>__HOME__/.local/share/brainlayer/logs/backup-daily.log</string>
<key>StandardErrorPath</key>
<string>__HOME__/.local/share/brainlayer/logs/backup-daily.err</string>

<key>EnvironmentVariables</key>
<dict>
<key>PYTHONPATH</key>
<string>__BRAINLAYER_DIR__/src</string>
<key>BRAINLAYER_BACKUP_DRIVE_FOLDER</key>
<string>Brain Drive/06_ARCHIVE/backups/brainlayer-db</string>
</dict>

<key>Nice</key>
<integer>15</integer>

<key>ProcessType</key>
<string>Background</string>
</dict>
</plist>
28 changes: 26 additions & 2 deletions scripts/launchd/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@
# ./scripts/launchd/install.sh load enrichment
# ./scripts/launchd/install.sh unload enrichment
# ./scripts/launchd/install.sh checkpoint # Install WAL checkpoint only
# ./scripts/launchd/install.sh backup # Install daily DB backup only
# ./scripts/launchd/install.sh remove # Unload and remove all
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
LAUNCH_DIR="$HOME/Library/LaunchAgents"
LOG_DIR="$HOME/Library/Logs"
BRAINLAYER_LOG_DIR="$HOME/.local/share/brainlayer/logs"
BRAINLAYER_LIB_DIR="$HOME/.local/lib/brainlayer"
BRAINLAYER_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"
BRAINLAYER_BIN="${BRAINLAYER_BIN:-$(which brainlayer 2>/dev/null || echo "$HOME/.local/bin/brainlayer")}"
GOOGLE_API_KEY="${GOOGLE_API_KEY:-}"
Expand All @@ -27,7 +29,7 @@ if [ ! -x "$BRAINLAYER_BIN" ]; then
exit 1
fi

mkdir -p "$LAUNCH_DIR" "$LOG_DIR" "$BRAINLAYER_LOG_DIR"
mkdir -p "$LAUNCH_DIR" "$LOG_DIR" "$BRAINLAYER_LOG_DIR" "$BRAINLAYER_LIB_DIR"

resolve_google_api_key() {
if [ -n "${GOOGLE_API_KEY:-}" ]; then
Expand Down Expand Up @@ -95,6 +97,20 @@ install_plist() {
load_plist "$name"
}

install_backup_script() {
local src="$SCRIPT_DIR/backup-daily.sh"
local dst="$BRAINLAYER_LIB_DIR/backup-daily.sh"

if [ ! -f "$src" ]; then
echo "ERROR: $src not found"
return 1
fi

cp "$src" "$dst"
chmod 755 "$dst"
echo "Installed: $dst"
}

remove_plist() {
local name="$1"
local dst="$LAUNCH_DIR/com.brainlayer.${name}.plist"
Expand Down Expand Up @@ -127,11 +143,17 @@ case "${1:-all}" in
checkpoint)
install_plist wal-checkpoint
;;
backup)
install_backup_script
install_plist backup-daily
;;
all)
install_plist index
install_plist enrichment
install_plist decay
install_plist wal-checkpoint
install_backup_script
install_plist backup-daily
# Remove old enrich plist if present
remove_plist enrich 2>/dev/null || true
;;
Expand All @@ -141,9 +163,11 @@ case "${1:-all}" in
remove_plist enrichment 2>/dev/null || true
remove_plist decay 2>/dev/null || true
remove_plist wal-checkpoint
remove_plist backup-daily 2>/dev/null || true
rm -f "$BRAINLAYER_LIB_DIR/backup-daily.sh"
;;
*)
echo "Usage: $0 [index|enrich|enrichment|decay|load [name]|unload [name]|checkpoint|all|remove]"
echo "Usage: $0 [index|enrich|enrichment|decay|load [name]|unload [name]|checkpoint|backup|all|remove]"
exit 1
;;
esac
Expand Down
Loading
Loading