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
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,10 @@ bash brain-bar/build-app.sh # Build, sign, install LaunchAgent

Requires the BrainLayer MCP server. The build script refuses non-canonical checkouts and dirty trees by default ([#265](https://github.com/EtanHey/brainlayer/pull/265)) and stamps each bundle with `GitCommit`, `GitDescribe`, and `BuildTimeUTC` in `Info.plist` ([#264](https://github.com/EtanHey/brainlayer/pull/264)) so a stale install is diagnosable in seconds.

## Writer Arbitration

Background producers run with `BRAINLAYER_ARBITRATED=1` and append writes to `~/.brainlayer/queue/`; `com.brainlayer.drain.plist` drains that queue every 500ms as the single writer. Trigram FTS maintenance is explicit via `brainlayer repair-fts` and the weekly `com.brainlayer.repair-fts.plist`, not synchronous startup work. See [docs/arbitration.md](docs/arbitration.md).

## Recent Hardening (2026-04-15 → 2026-05-02)

Two-week stability sprint behind the next presentation. Every line below traces to a merged PR.
Expand Down
19 changes: 19 additions & 0 deletions docs/arbitration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# BrainLayer Writer Arbitration

BrainLayer uses a single-writer arbitration path for background producers that can otherwise fight over SQLite's write lock.

## Operator Contract

- `BRAINLAYER_ARBITRATED=1` makes producers enqueue writes instead of writing directly to the database. The current launchd templates set this for watch and enrichment.
- `BRAINLAYER_DRAIN_EMBED=0` disables post-drain embedding, mainly for tests or emergency operator debugging. Production should leave it enabled so queued `brain_store` chunks reach semantic search.
- The unified durable queue lives at `~/.brainlayer/queue/`. Each event is one JSONL file named by source, timestamp, and UUID.
- `com.brainlayer.drain.plist` runs `scripts/drain_daemon.py`, which drains the queue every 500ms under `BEGIN IMMEDIATE`.
- The drain daemon opens SQLite with APSW, loads `sqlite-vec`, writes chunks/enrichment updates, and embeds queued `brain_store` chunks into `chunk_vectors`/`chunk_vectors_binary` before removing their queue files.
- Legacy `pending-stores.jsonl` is migrated by `brainlayer flush`; migration assigns stable chunk IDs so rerunning after a crash is `INSERT OR IGNORE` safe.

## FTS Repair

- Startup no longer performs large synchronous trigram repairs.
- Run `brainlayer repair-fts` for an explicit `chunks_fts_trigram` rebuild.
- `scripts/launchd/com.brainlayer.repair-fts.plist` schedules that repair weekly.
- Set `BRAINLAYER_REPAIR=1` only for an operator-controlled process that should run the repair during VectorStore initialization.
17 changes: 17 additions & 0 deletions scripts/drain_daemon.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#!/usr/bin/env python3
"""Launchd wrapper for BrainLayer's single-writer drain daemon."""

from __future__ import annotations

import sys
from pathlib import Path

REPO_ROOT = Path(__file__).resolve().parents[1]
SRC_DIR = REPO_ROOT / "src"
if str(SRC_DIR) not in sys.path:
sys.path.insert(0, str(SRC_DIR))

from brainlayer.drain import drain_once, main, run_daemon # noqa: E402,F401

if __name__ == "__main__":
raise SystemExit(main())
33 changes: 33 additions & 0 deletions scripts/launchd/com.brainlayer.drain.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?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.drain</string>

<key>ProgramArguments</key>
<array>
<string>__PYTHON_BIN__</string>
<string>__REPO_ROOT__/scripts/drain_daemon.py</string>
<string>--interval</string>
<string>0.5</string>
</array>

<key>StandardOutPath</key>
<string>__HOME__/.brainlayer/logs/drain.log</string>
<key>StandardErrorPath</key>
<string>__HOME__/.brainlayer/logs/drain.err</string>

<key>KeepAlive</key>
<true/>
<key>RunAtLoad</key>
<true/>
<key>ThrottleInterval</key>
<integer>5</integer>
<key>Nice</key>
<integer>10</integer>
<key>ProcessType</key>
<string>Background</string>
</dict>
</plist>

2 changes: 2 additions & 0 deletions scripts/launchd/com.brainlayer.enrichment.plist
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@
<string>__BRAINLAYER_DIR__/src</string>
<key>BRAINLAYER_STALL_TIMEOUT</key>
<string>300</string>
<key>BRAINLAYER_ARBITRATED</key>
<string>1</string>
<key>BRAINLAYER_ENRICH_RATE</key>
<string>15</string>
<key>BRAINLAYER_ENRICH_CONCURRENCY</key>
Expand Down
35 changes: 35 additions & 0 deletions scripts/launchd/com.brainlayer.repair-fts.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?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.repair-fts</string>

<key>ProgramArguments</key>
<array>
<string>__BRAINLAYER_BIN__</string>
<string>repair-fts</string>
</array>

<!-- Run weekly on Sunday at 4:30am, after WAL checkpoint. -->
<key>StartCalendarInterval</key>
<dict>
<key>Weekday</key>
<integer>0</integer>
<key>Hour</key>
<integer>4</integer>
<key>Minute</key>
<integer>30</integer>
</dict>

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

<key>Nice</key>
<integer>15</integer>
<key>ProcessType</key>
<string>Background</string>
</dict>
</plist>
48 changes: 48 additions & 0 deletions scripts/launchd/com.brainlayer.watch.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?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.watch</string>

<key>ProgramArguments</key>
<array>
<string>__BRAINLAYER_BIN__</string>
<string>watch</string>
<string>--poll</string>
<string>1.0</string>
<string>--batch-size</string>
<string>10</string>
<string>--flush-ms</string>
<string>500</string>
</array>

<key>EnvironmentVariables</key>
<dict>
<key>PATH</key>
<string>/usr/local/bin:/usr/bin:/bin:__HOME__/.local/bin</string>
<key>PYTHONUNBUFFERED</key>
<string>1</string>
<key>PYTHONPATH</key>
<string>__BRAINLAYER_DIR__/src</string>
<key>BRAINLAYER_ARBITRATED</key>
<string>1</string>
</dict>

<key>StandardOutPath</key>
<string>__HOME__/.local/share/brainlayer/logs/watch.log</string>
<key>StandardErrorPath</key>
<string>__HOME__/.local/share/brainlayer/logs/watch.err</string>

<key>KeepAlive</key>
<true/>
<key>RunAtLoad</key>
<true/>
<key>ThrottleInterval</key>
<integer>5</integer>
<key>Nice</key>
<integer>10</integer>
<key>ProcessType</key>
<string>Background</string>
</dict>
</plist>
30 changes: 29 additions & 1 deletion scripts/launchd/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@
# Usage:
# ./scripts/launchd/install.sh # Install all
# ./scripts/launchd/install.sh index # Install indexing only
# ./scripts/launchd/install.sh watch # Install watcher only
# ./scripts/launchd/install.sh enrich # Install enrichment only
# ./scripts/launchd/install.sh drain # Install queue drain only
# ./scripts/launchd/install.sh decay # Install decay only
# ./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 repair-fts # Install weekly explicit FTS repair
# ./scripts/launchd/install.sh backup # Install daily DB backup only
# ./scripts/launchd/install.sh remove # Unload and remove all
set -euo pipefail
Expand All @@ -20,8 +23,15 @@ 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")}"
PYTHON_BIN="${PYTHON_BIN:-$(command -v python3)}"
GOOGLE_API_KEY="${GOOGLE_API_KEY:-}"

if [ -z "$PYTHON_BIN" ]; then
echo "ERROR: python3 not found in PATH"
echo "Install Python 3 or set PYTHON_BIN=/path/to/python3"
exit 1
fi

if [ ! -x "$BRAINLAYER_BIN" ]; then
echo "ERROR: brainlayer binary not found at $BRAINLAYER_BIN"
echo "Install with: pip install -e . (from brainlayer repo)"
Expand All @@ -30,6 +40,7 @@ if [ ! -x "$BRAINLAYER_BIN" ]; then
fi

mkdir -p "$LAUNCH_DIR" "$LOG_DIR" "$BRAINLAYER_LOG_DIR" "$BRAINLAYER_LIB_DIR"
mkdir -p "$HOME/.brainlayer/logs" "$HOME/.brainlayer/queue"

resolve_google_api_key() {
if [ -n "${GOOGLE_API_KEY:-}" ]; then
Expand Down Expand Up @@ -88,6 +99,8 @@ install_plist() {
-e "s|__HOME__|$HOME|g" \
-e "s|__BRAINLAYER_BIN__|$BRAINLAYER_BIN|g" \
-e "s|__BRAINLAYER_DIR__|$BRAINLAYER_DIR|g" \
-e "s|__PYTHON_BIN__|$PYTHON_BIN|g" \
-e "s|__REPO_ROOT__|$BRAINLAYER_DIR|g" \
-e "s|__GOOGLE_API_KEY__|$google_api_key|g" \
"$src" > "$dst"

Expand Down Expand Up @@ -135,6 +148,9 @@ case "${1:-all}" in
enrichment)
install_plist enrichment
;;
watch)
install_plist watch
;;
decay)
install_plist decay
;;
Expand All @@ -147,15 +163,24 @@ case "${1:-all}" in
checkpoint)
install_plist wal-checkpoint
;;
drain)
install_plist drain
;;
repair-fts)
install_plist repair-fts
;;
backup)
install_backup_script
install_plist backup-daily
;;
all)
install_plist index
install_plist drain
install_plist watch
install_plist enrichment
install_plist decay
install_plist wal-checkpoint
install_plist repair-fts
install_backup_script
install_plist backup-daily
# Remove old enrich plist if present
Expand All @@ -165,13 +190,16 @@ case "${1:-all}" in
remove_plist index
remove_plist enrich 2>/dev/null || true
remove_plist enrichment 2>/dev/null || true
remove_plist watch 2>/dev/null || true
remove_plist decay 2>/dev/null || true
remove_plist drain 2>/dev/null || true
remove_plist wal-checkpoint
remove_plist repair-fts 2>/dev/null || true
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|backup|all|remove]"
echo "Usage: $0 [index|watch|enrich|enrichment|decay|drain|repair-fts|load [name]|unload [name]|checkpoint|backup|all|remove]"
exit 1
;;
esac
Expand Down
Loading
Loading