Vault Sync
From spec-memo Wiki, the living architecture & domain knowledge base
Feature Overview [edit]
spec-memo persists sync state through three cooperating channels: an opt-in private vault-git remote, hybrid HTTP delta sync to a remote SSE daemon, and multi-machine filesystem peer sync (memo sync-vault). memo sync is the explicit operator command; session_end and graceful memo serve shutdown flush automatically.
- vault-git mirrors the vault root (
~/.spec-memo/) to a private git remote. Off by default; requiresvaultGit.enabled: true. In the defaultatomic: false(batched) mode, mutations write markdown only and git commit/pull/push runs on flush. - Hybrid keeps the local vault authoritative and exchanges changesets with a daemon over authenticated HTTP (
/api/sync/pull,/api/sync/push). It pulls onbootstrap, debounces pushes after mutations, and syncs explicitly viamemo sync. - Multi-machine
memo sync-vault <target>exchanges deltas between two filesystem vault locations, optionally two-way. - Conflict reconciliation (
memo reconcile) cleans conflict sidecars, applies semantic auto-merge, and can drive one sync pass with a chosen source of truth.
Business Rules & Logic [edit]
- Vault-git is a no-op when
vaultGit.enabledis omitted/false or mode isremote.initVaultGitis idempotent: it does not re-init an existing repo, sets origin only whenremoteUrlis configured, and appends missing.gitignorelines (never overwrites operator entries). Required ignores:memo.sqlite,memo.sqlite-wal,memo.sqlite-shm,.sync/,error.logs,telemetry/,backups/. atomicdefault isfalse; the unusedautoCommitmaps toatomicwhenatomicis absent; non-booleanatomicis treated asfalse(effective value reported by status/doctor).- Batched (`atomic: false`): a mutation (
upsert,append,forget,gc, import, reset, prompt record/sessionstart, recurrence backfill) creates no commit and runs no pull/push. Flush points are exactly: `memo sync`, a successful MCP/CLI `prompt` `sessionend, and graceful stdio/SSE shutdown. A flush with a clean tree skips the empty commit but still pulls/pushes whenremoteUrlis set. Commit message isvault-git flush <ISO>(plussession <sessionId>on session end). Staged paths stay in the allow-list (projects/,config.json,.gitignore), nevergit add .`. - Atomic (`atomic: true`): after a listed mutation the process commits (
upsert kind:id, etc.) then pulls/pushes fail-open. Rapid atomic mutations single-flight remote sync per vault root (one in-flight plus exactly one trailing). Hybrid debounce (2000 ms) remains independent. - Remote git pull uses
git pull --rebase --autostash origin <branch>; a failed autostash pull aborts the half-rebase and drains every autostash entry by explicit ref (never a barestash pop). Git child processes use a 30 s timeout (SPEC_MEMO_SYNC_TIMEOUT_MS). Every git/FS/network error logs under subsystemvault-gitand marks$SPEC_MEMO_ROOT/.sync/vault-git-state.json{ dirty, lastError, lastSyncAt }(credentials redacted). Errors never crash MCP/SSE/CLI. - Dual mode (
mode: hybrid+vaultGit.enabled):syncDualruns hybrid first, then vault-git sequentially in one run, so the git commit captures views that hybrid just rewrote. Channels are independent: either may fail while the other still reports.DualSyncReport.okis true only when every enabled channel succeeds, hybrid has zero conflicts, and hybrid state is not dirty.flushOnShutdowncaps the wait atSPEC_MEMO_SYNC_TIMEOUT_MSor 8000 ms and never throws. - Hybrid cursors are monotonic per project. Pull writes a cursor from
changeset.generatedAtonly when records were applied; push writes a cursor from the export snapshotgeneratedAt(not push-completion wall clock) so records updated during an in-flight push remain visible. A dirty project pushes withsinceunset, so older offline records survive an empty pull. Push failure setsdirty/lastErrorwithout rolling back local writes; a successful push clears only its own project flag. - Conflict policy (
smart-mergedefault, pluslocal-wins,remote-wins,sidecar): bodies are compared semantically (line endings and trailing whitespace ignored). Identical bodies auto-merge metadata (max hits/occurrences, latest lastHit/lastSeen, union tags/linkedPaths) without a conflict. Diverging bodies:remote-winsoverwrites;local-winskeeps local;sidecarand equal-timestamp smart-merge write one deterministic{slug}.conflict.md; differing timestamps in smart-merge take the newer side.--prefer localflips the push direction to force remote, and--prefer remoteflips to local. applyChangesetis transactional: it pre-validates every record (schema, path containment, secret rules) before writing, journals original content, rolls back all mutated files plus rebuilds indexes on failure, and on success rebuilds compiled views per project with one retry then skip-and-log, then rebuilds FTS.cleanSidecarsremoves conflict sidecars whose body matches the base record.- Issue #55 hardening: sync-context records whose
pathPatternsare fully covered by builtin ignore rules (e.g. absolute cross-host paths) are skipped and logged per record instead of aborting the whole changeset; direct localupsertstill throws the strict capture-ignore error. View/FTS rebuild failures are retried once then skipped and logged, preserving applied counts. memo syncwith neitherhybridnorvaultGit.enabledexits non-zero;memo syncis unavailable in remote mode.--dry-runreports hybrid counts and vault-git porcelain without committing, pulling, or pushing.- Background sync: when
sync.autoSyncIntervalMinutes > 0and mode ishybrid, the daemon runs a non-blocking per-projectsyncDualcycle that skips when the vault lock is busy, never stalling MCP traffic.
Technical Architecture [edit]
- Config:
VaultConfig.vaultGit(enabled,remoteUrl?,atomic?,autoCommit?,branch?) andVaultConfig.sync(conflictStrategy?,cleanSidecars?,autoSyncIntervalMinutes?) insrc/types.ts; git state file helpers insrc/vault-git-state.ts, redaction insrc/vault-git-redact.ts. - Git engine (
src/vault.ts):initVaultGit,commitVaultChange,scheduleVaultGitRemoteSync,flushScheduledVaultGit,parseAutostashRefs,flushVaultGit,syncVault,getGitTimeoutMs,getShutdownFlushMs,resolveVaultGitAtomic,REQUIRED_VAULT_GITIGNORE.flushVaultGitreturnsVaultGitChannelResult({ ok, committed, pulled, pushed, message, error?, skipped?, wouldCommit? }). - Dual orchestrator (
src/dual-sync.ts):syncDual({ vaultRoot?, projectId?, all?, dryRun?, trigger, sessionId?, force?, prefer?, strategy?, cleanSidecars? })andflushOnShutdown. - Hybrid client (
src/hybrid-sync.ts) and state (src/hybrid-state.ts) as documented in Deployment Modes. Daemon routes live insrc/server.ts(/api/sync/pull|push,/api/sync). - Changeset engine (
src/sync.ts):exportChangeset,applyChangeset,syncVaults,areBodiesSemanticallyEqual,mergeRecordMetadata,cleanConflictSidecars, cursor/tombstone helpers.memo sync-vault <target> [--two-way] [--dry-run] [--json]. - CLI:
memo sync [--all] [--dry-run] [--prefer local|remote] [--strategy ...] [--clean-sidecars] [--force] [--json];memo reconcilerunscleanConflictSidecarsthensyncDualwith--prefer,--strategy,--clean-sidecars,--dry-run,--all(interactive source-of-truth prompt only on a TTY). - Provenance:
0018-vault-git.spec.md,0044-multi-machine-sync.spec.md,0033-vault-git-hybrid-sync.spec.md(batched/atomic cadence, dual dispatch),0035-sync-conflict-reconciliation.spec.md(semantic merge, strategies, rollback journal, background worker, sidecar cleanup),0051-us-55.spec.md(autostash recovery, per-record skip-and-log, view-rebuild skip). See Deployment Modes and Backup & Restore.