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 <noreply@anthropic.com>
This commit is contained in:
@@ -195,6 +195,10 @@ OBSIDIAN_E2E_PASSWORD=
|
|||||||
OBSIDIAN_DEVICE_NAME=simbarag
|
OBSIDIAN_DEVICE_NAME=simbarag
|
||||||
# Set to true to run continuous sync in the background
|
# Set to true to run continuous sync in the background
|
||||||
OBSIDIAN_CONTINUOUS_SYNC=false
|
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
|
OBSIDIAN_SYNC_INTERVAL=60
|
||||||
# Local path to Obsidian vault (where files are synced)
|
# Local path to Obsidian vault (where files are synced)
|
||||||
OBSIDIAN_VAULT_PATH=/app/data/obsidian
|
OBSIDIAN_VAULT_PATH=/app/data/obsidian
|
||||||
|
|||||||
@@ -225,6 +225,14 @@ def validate_configuration(env: Mapping[str, str], *, production: bool = True) -
|
|||||||
problem(flag, "signature validation must remain enabled in production")
|
problem(flag, "signature validation must remain enabled in production")
|
||||||
for name in ("METRICS_TOKEN", "CRON_SECRET", "EMAIL_HMAC_SECRET"):
|
for name in ("METRICS_TOKEN", "CRON_SECRET", "EMAIL_HMAC_SECRET"):
|
||||||
secret(name, minimum=32)
|
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":
|
if env.get("OBSIDIAN_CONTINUOUS_SYNC") == "true":
|
||||||
for name in ("OBSIDIAN_EMAIL", "OBSIDIAN_PASSWORD", "OBSIDIAN_VAULT_ID"):
|
for name in ("OBSIDIAN_EMAIL", "OBSIDIAN_PASSWORD", "OBSIDIAN_VAULT_ID"):
|
||||||
if not value(name):
|
if not value(name):
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ x-raggr-common: &raggr-common
|
|||||||
- OBSIDIAN_E2E_PASSWORD=${OBSIDIAN_E2E_PASSWORD}
|
- OBSIDIAN_E2E_PASSWORD=${OBSIDIAN_E2E_PASSWORD}
|
||||||
- OBSIDIAN_DEVICE_NAME=${OBSIDIAN_DEVICE_NAME}
|
- OBSIDIAN_DEVICE_NAME=${OBSIDIAN_DEVICE_NAME}
|
||||||
- OBSIDIAN_CONTINUOUS_SYNC=${OBSIDIAN_CONTINUOUS_SYNC:-false}
|
- 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_PATH=${OBSIDIAN_VAULT_PATH:-/app/data/obsidian}
|
||||||
- OBSIDIAN_VAULT_NAME=${OBSIDIAN_VAULT_NAME:-}
|
- OBSIDIAN_VAULT_NAME=${OBSIDIAN_VAULT_NAME:-}
|
||||||
- S3_ENDPOINT_URL=${S3_ENDPOINT_URL}
|
- S3_ENDPOINT_URL=${S3_ENDPOINT_URL}
|
||||||
|
|||||||
+1
-1
@@ -39,7 +39,7 @@ make logs
|
|||||||
make migrate-history
|
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:
|
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:
|
||||||
|
|
||||||
|
|||||||
+28
-19
@@ -19,27 +19,36 @@ if [ "${OBSIDIAN_CONTINUOUS_SYNC:-false}" = "true" ]; then
|
|||||||
if [ -z "${OBSIDIAN_EMAIL:-}" ] || [ -z "${OBSIDIAN_PASSWORD:-}" ] || [ -z "${OBSIDIAN_VAULT_ID:-}" ]; 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."
|
echo "WARNING: OBSIDIAN_EMAIL, OBSIDIAN_PASSWORD, or OBSIDIAN_VAULT_ID not set. Skipping sync."
|
||||||
else
|
else
|
||||||
echo "Setting up Obsidian sync..."
|
|
||||||
|
|
||||||
VAULT_PATH="${OBSIDIAN_VAULT_PATH:-/app/data/obsidian}"
|
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
|
||||||
if ob login --email "${OBSIDIAN_EMAIL}" --password "${OBSIDIAN_PASSWORD}" && \
|
# across containers, so this container only sets up and runs sync once
|
||||||
ob sync-setup \
|
# the previous container's sync process has exited. Two clients writing
|
||||||
--vault "${OBSIDIAN_VAULT_ID}" \
|
# one vault and its sync state would otherwise race. Setup runs in the
|
||||||
--path "${VAULT_PATH}" \
|
# background so the app starts while it waits.
|
||||||
--password "${OBSIDIAN_E2E_PASSWORD:-}" \
|
(
|
||||||
--device-name "${OBSIDIAN_DEVICE_NAME:-simbarag}"; then
|
flock 9
|
||||||
# Remove stale lock from previous container run
|
echo "Setting up Obsidian sync (${SYNC_MODE})..."
|
||||||
rm -rf "${VAULT_PATH}/.obsidian/.sync.lock"
|
if ob login --email "${OBSIDIAN_EMAIL}" --password "${OBSIDIAN_PASSWORD}" && \
|
||||||
# Set sync to pull-only (read-only) mode
|
ob sync-setup \
|
||||||
ob sync-config --mode pull-only --path "${VAULT_PATH}"
|
--vault "${OBSIDIAN_VAULT_ID}" \
|
||||||
# Start continuous sync in background
|
--path "${VAULT_PATH}" \
|
||||||
echo "Starting Obsidian continuous sync (pull-only)..."
|
--password "${OBSIDIAN_E2E_PASSWORD:-}" \
|
||||||
ob sync --continuous --path "${VAULT_PATH}" &
|
--device-name "${OBSIDIAN_DEVICE_NAME:-simbarag}"; then
|
||||||
else
|
# Safe now: the lock means no other container's sync is running.
|
||||||
echo "WARNING: Obsidian sync setup failed. Continuing without sync."
|
rm -rf "${VAULT_PATH}/.obsidian/.sync.lock"
|
||||||
fi
|
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
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ def test_missing_required_settings(production_env, name):
|
|||||||
("CHECKPOINT_DB_PATH", "/data/invalid\x00.db"),
|
("CHECKPOINT_DB_PATH", "/data/invalid\x00.db"),
|
||||||
("CONVERSATION_SUMMARY_DB_PATH", ""),
|
("CONVERSATION_SUMMARY_DB_PATH", ""),
|
||||||
("OBSIDIAN_CONTINUOUS_SYNC", "yes"),
|
("OBSIDIAN_CONTINUOUS_SYNC", "yes"),
|
||||||
|
("OBSIDIAN_SYNC_MODE", "push-only"),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_malformed_and_unsafe_settings(production_env, name, value):
|
def test_malformed_and_unsafe_settings(production_env, name, value):
|
||||||
@@ -202,6 +203,7 @@ def test_compose_forwards_runtime_settings():
|
|||||||
"CHECKPOINT_DB_PATH",
|
"CHECKPOINT_DB_PATH",
|
||||||
"CONVERSATION_SUMMARY_DB_PATH",
|
"CONVERSATION_SUMMARY_DB_PATH",
|
||||||
"OBSIDIAN_SYNC_INTERVAL",
|
"OBSIDIAN_SYNC_INTERVAL",
|
||||||
|
"OBSIDIAN_SYNC_MODE",
|
||||||
"OIDC_AUTHORIZATION_ENDPOINT",
|
"OIDC_AUTHORIZATION_ENDPOINT",
|
||||||
"OIDC_TOKEN_ENDPOINT",
|
"OIDC_TOKEN_ENDPOINT",
|
||||||
"OIDC_USERINFO_ENDPOINT",
|
"OIDC_USERINFO_ENDPOINT",
|
||||||
|
|||||||
Reference in New Issue
Block a user