Sync Obsidian both ways so notes Simba writes reach your devices
CI / backend (pull_request) Successful in 42s
CI / frontend (pull_request) Failing after 6s
CI / backend (push) Successful in 41s
CI / frontend (push) Failing after 7s

The container ran Obsidian Sync in pull-only mode, so notes, tasks and journal
entries the agent wrote stayed in its local copy. OBSIDIAN_SYNC_MODE now
defaults to bidirectional (merge on conflict); pull-only and mirror-remote
remain available.

Blue and green share the vault volume, and two sync clients writing one vault
and its sync state would race during a deploy. Setup and sync now run under a
flock on a file in the vault, so a new container starts syncing only after the
old container's sync process exits. Clearing Obsidian's stale sync lock is safe
inside that lock. Setup runs in the background, so startup no longer waits for
Obsidian login.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-13 08:10:48 -04:00
parent 61acd85954
commit b02bf0a6df
6 changed files with 44 additions and 20 deletions
+4
View File
@@ -195,6 +195,10 @@ OBSIDIAN_E2E_PASSWORD=
OBSIDIAN_DEVICE_NAME=simbarag
# Set to true to run continuous sync in the background
OBSIDIAN_CONTINUOUS_SYNC=false
# bidirectional: notes and journal tasks Simba writes sync back to your devices.
# pull-only: download only, Simba's writes stay in the container's copy.
# mirror-remote: download only and revert local changes.
OBSIDIAN_SYNC_MODE=bidirectional
OBSIDIAN_SYNC_INTERVAL=60
# Local path to Obsidian vault (where files are synced)
OBSIDIAN_VAULT_PATH=/app/data/obsidian
+8
View File
@@ -225,6 +225,14 @@ def validate_configuration(env: Mapping[str, str], *, production: bool = True) -
problem(flag, "signature validation must remain enabled in production")
for name in ("METRICS_TOKEN", "CRON_SECRET", "EMAIL_HMAC_SECRET"):
secret(name, minimum=32)
if env.get("OBSIDIAN_SYNC_MODE", "bidirectional") not in {
"bidirectional",
"pull-only",
"mirror-remote",
}:
problem(
"OBSIDIAN_SYNC_MODE", "must be bidirectional, pull-only or mirror-remote"
)
if env.get("OBSIDIAN_CONTINUOUS_SYNC") == "true":
for name in ("OBSIDIAN_EMAIL", "OBSIDIAN_PASSWORD", "OBSIDIAN_VAULT_ID"):
if not value(name):
+1
View File
@@ -61,6 +61,7 @@ x-raggr-common: &raggr-common
- OBSIDIAN_E2E_PASSWORD=${OBSIDIAN_E2E_PASSWORD}
- OBSIDIAN_DEVICE_NAME=${OBSIDIAN_DEVICE_NAME}
- OBSIDIAN_CONTINUOUS_SYNC=${OBSIDIAN_CONTINUOUS_SYNC:-false}
- OBSIDIAN_SYNC_MODE=${OBSIDIAN_SYNC_MODE:-bidirectional}
- OBSIDIAN_VAULT_PATH=${OBSIDIAN_VAULT_PATH:-/app/data/obsidian}
- OBSIDIAN_VAULT_NAME=${OBSIDIAN_VAULT_NAME:-}
- S3_ENDPOINT_URL=${S3_ENDPOINT_URL}
+1 -1
View File
@@ -39,7 +39,7 @@ make logs
make migrate-history
```
Startup validates configuration, runs `aerich upgrade`, optionally sets up Obsidian pull-only sync, and replaces the shell with the Python process. **A migration failure stops startup; the application is never launched.** Obsidian login/setup failures still log a warning and allow the application to continue.
Startup validates configuration, runs `aerich upgrade`, optionally starts Obsidian sync in the background (`OBSIDIAN_SYNC_MODE`, default `bidirectional` so notes and tasks Simba writes reach your devices; containers take turns via a lock file in the shared vault), and replaces the shell with the Python process. **A migration failure stops startup; the application is never launched.** Obsidian login/setup failures still log a warning and allow the application to continue.
An empty database without Aerich metadata may need explicit initialization. Inspect its state first. Run the migration tool in a one-off container because the app cannot start until migrations pass:
+18 -9
View File
@@ -19,27 +19,36 @@ if [ "${OBSIDIAN_CONTINUOUS_SYNC:-false}" = "true" ]; then
if [ -z "${OBSIDIAN_EMAIL:-}" ] || [ -z "${OBSIDIAN_PASSWORD:-}" ] || [ -z "${OBSIDIAN_VAULT_ID:-}" ]; then
echo "WARNING: OBSIDIAN_EMAIL, OBSIDIAN_PASSWORD, or OBSIDIAN_VAULT_ID not set. Skipping sync."
else
echo "Setting up Obsidian sync..."
VAULT_PATH="${OBSIDIAN_VAULT_PATH:-/app/data/obsidian}"
SYNC_MODE="${OBSIDIAN_SYNC_MODE:-bidirectional}"
mkdir -p "${VAULT_PATH}"
# Login and setup sync (foreground, must complete before sync starts)
# Blue and green share the vault volume. flock on a file in it works
# across containers, so this container only sets up and runs sync once
# the previous container's sync process has exited. Two clients writing
# one vault and its sync state would otherwise race. Setup runs in the
# background so the app starts while it waits.
(
flock 9
echo "Setting up Obsidian sync (${SYNC_MODE})..."
if ob login --email "${OBSIDIAN_EMAIL}" --password "${OBSIDIAN_PASSWORD}" && \
ob sync-setup \
--vault "${OBSIDIAN_VAULT_ID}" \
--path "${VAULT_PATH}" \
--password "${OBSIDIAN_E2E_PASSWORD:-}" \
--device-name "${OBSIDIAN_DEVICE_NAME:-simbarag}"; then
# Remove stale lock from previous container run
# Safe now: the lock means no other container's sync is running.
rm -rf "${VAULT_PATH}/.obsidian/.sync.lock"
# Set sync to pull-only (read-only) mode
ob sync-config --mode pull-only --path "${VAULT_PATH}"
# Start continuous sync in background
echo "Starting Obsidian continuous sync (pull-only)..."
ob sync --continuous --path "${VAULT_PATH}" &
if ob sync-config --mode "${SYNC_MODE}" --conflict-strategy merge \
--path "${VAULT_PATH}"; then
echo "Starting Obsidian continuous sync (${SYNC_MODE})..."
exec ob sync --continuous --path "${VAULT_PATH}"
fi
echo "WARNING: Obsidian sync-config failed. Continuing without sync."
else
echo "WARNING: Obsidian sync setup failed. Continuing without sync."
fi
) 9>"${VAULT_PATH}/.raggr-sync.lock" &
fi
fi
+2
View File
@@ -63,6 +63,7 @@ def test_missing_required_settings(production_env, name):
("CHECKPOINT_DB_PATH", "/data/invalid\x00.db"),
("CONVERSATION_SUMMARY_DB_PATH", ""),
("OBSIDIAN_CONTINUOUS_SYNC", "yes"),
("OBSIDIAN_SYNC_MODE", "push-only"),
],
)
def test_malformed_and_unsafe_settings(production_env, name, value):
@@ -202,6 +203,7 @@ def test_compose_forwards_runtime_settings():
"CHECKPOINT_DB_PATH",
"CONVERSATION_SUMMARY_DB_PATH",
"OBSIDIAN_SYNC_INTERVAL",
"OBSIDIAN_SYNC_MODE",
"OIDC_AUTHORIZATION_ENDPOINT",
"OIDC_TOKEN_ENDPOINT",
"OIDC_USERINFO_ENDPOINT",