From b02bf0a6df17aed45e034ae61f2181985f77a35c Mon Sep 17 00:00:00 2001 From: Ryan Chen Date: Tue, 13 Oct 2026 08:10:48 -0400 Subject: [PATCH] Sync Obsidian both ways so notes Simba writes reach your devices 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 --- .env.example | 4 +++ config/validation.py | 8 ++++++ docker-compose.yml | 1 + docs/deployment.md | 2 +- startup.sh | 47 +++++++++++++++++++------------- tests/unit/test_configuration.py | 2 ++ 6 files changed, 44 insertions(+), 20 deletions(-) diff --git a/.env.example b/.env.example index 39e53a6..acf6bcb 100644 --- a/.env.example +++ b/.env.example @@ -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 diff --git a/config/validation.py b/config/validation.py index 281c77a..94d02c4 100644 --- a/config/validation.py +++ b/config/validation.py @@ -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): diff --git a/docker-compose.yml b/docker-compose.yml index fdca63c..0ceccac 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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} diff --git a/docs/deployment.md b/docs/deployment.md index 768b51c..ef9471d 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -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: diff --git a/startup.sh b/startup.sh index c681354..86d3783 100644 --- a/startup.sh +++ b/startup.sh @@ -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) - 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 - 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}" & - else - echo "WARNING: Obsidian sync setup failed. Continuing without sync." - fi + # 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 + # Safe now: the lock means no other container's sync is running. + rm -rf "${VAULT_PATH}/.obsidian/.sync.lock" + 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 diff --git a/tests/unit/test_configuration.py b/tests/unit/test_configuration.py index 8eb633e..3354260 100644 --- a/tests/unit/test_configuration.py +++ b/tests/unit/test_configuration.py @@ -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", -- 2.52.0