Skip to content

feat(sync): git sync engine in the daemon - origin push/pull (M4)#220

Merged
vreshch merged 1 commit into
masterfrom
feature/m4-git-sync
Jul 6, 2026
Merged

feat(sync): git sync engine in the daemon - origin push/pull (M4)#220
vreshch merged 1 commit into
masterfrom
feature/m4-git-sync

Conversation

@vreshch

@vreshch vreshch commented Jul 6, 2026

Copy link
Copy Markdown
Member

M4 - git sync

The local daemon now acts on each vault's origin[]: a debounced commit + push / pull-rebase
cycle per interval, against external git remotes only (GitHub or self-hosted bare repos). The
reserved cloud channel is never touched here.

What ships

  • src/sync/ - a small git-exec wrapper (spawns git, cwd = vault path; memory-core keeps its
    own createGit private) + a per-vault cycle: commit-if-dirty -> ensure remote -> fetch ->
    pull-rebase (keeping both sides on conflict) -> push. Every step is tolerant.
  • Daemon loop: per-vault timers scheduled from origin[].interval on boot and on vaults.json
    change; sync state exposed at GET /api/sync/status, forced runs at POST /api/sync/run.
    daemon status prints a one-liner per synced vault.
  • agentage vault sync [name] - forces a sync now (all origin-carrying vaults, or one). Uses a
    running daemon when present, else runs in-process. Respects --no-daemon.
  • vault add --git <remote> now creates a local working copy (path + origin) so a git-synced
    vault is usable end to end.

Key decisions

  • Interval unit = SECONDS. The unified schema stores interval as a bare non-negative integer
    with no minutes marker, so seconds is the least-surprising reading and keeps tests fast. Default
    when absent = 300s. interval: 0 = manual-only (excluded from the daemon loop; still syncable
    via vault sync).
  • Ignore mechanism = .git/info/exclude (not per-command pathspecs). It is the robust,
    idempotent choice: matching untracked files are never staged by add -A, and it leaves no
    committed .gitignore in the user's vault. Absent ignore -> defaults ['.obsidian/', 'data.json']; a set value REPLACES the defaults; [] = sync everything.
  • Conflict semantics = keep both sides, zero lost writes. A rebase probes for a true conflict
    (clean replay stays linear). On conflict: abort the rebase, capture each conflicted file's remote
    side, merge remote in with content conflicts auto-resolved to the local side (-X ours, so
    remote's non-conflicting changes are still merged), then write each remote copy alongside as
    <file>.conflict.md (never clobbered - a numeric suffix is added). Local content stays in
    <file>; the result is a push-able, descendant-of-remote history.
  • Lock-skip strategy. Git failures are classified from stderr (unreachable | lock | conflict | other). A held index.lock (a concurrent engine mutation) is a clean skip that the next cycle
    catches up - never a crash, never a forced unlock. An unreachable remote is likewise recorded and
    retried. Each target is serialised against itself (an in-process flag - no overlapping cycles).
  • CRUD never blocks on sync (V6): writes commit locally and return immediately; the sync loop
    runs on its own timer, so an unreachable remote never blocks a write.

memory-core 0.1.2 ride-along

Bumped @agentage/memory-core ^0.1.1 -> ^0.1.2 (lockfile pins 0.1.2). It fixes no-op writes
erroring on "nothing to commit". No other lockfile churn; TypeScript stays pinned at 5.9.3.

Tests

  • Unit (new): planner (target selection, interval mapping, ignore resolution), conflict-file
    naming (dedup + suffix), git-error classification, the full cycle over real temp git repos
    (commit/push, no-op, ignore honored + [], divergence keeps both sides, clean remote merge,
    unreachable), and the manager (scheduling, interval-0 exclusion, per-target serialisation, state).
  • E2E @p0 (e2e/sync.test.ts, fully self-contained - local git init --bare, isolated config
    dir, ephemeral daemon port; never the real ~/.agentage or :4243): (a) daemon auto-loop pushes
    a write to the bare remote within one interval; (b) divergence -> <file>.conflict.md + both
    contents preserved + push succeeds; (c) unreachable remote -> write still succeeds, error
    recorded, no crash.
  • npm run verify green (203 unit tests). Full e2e green (23 tests) against dev, including the new
    sync tier run 3x locally with no flakes.

Guards

Zero new dependencies (spawns git directly); the R6 package-guard allowlist is unchanged.

@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

🎉 PR Validation ✅ PASSED

Commit: fc967034301398dcfe3a59559ad91ac265cc5dd8
Branch: feature/m4-git-sync

Checks:

  • ✅ Release guard (no version/changelog changes)
  • ✅ Dependencies installed
  • ✅ Type check passed
  • ✅ Linting passed
  • ✅ Format check passed
  • ✅ Tests + coverage passed
  • ✅ Build successful

Ready to merge!


🔗 View workflow run
⏰ Generated at: 2026-07-06T07:52:05.080Z

@vreshch vreshch marked this pull request as ready for review July 6, 2026 07:52
@vreshch vreshch merged commit 83c775d into master Jul 6, 2026
2 checks passed
@vreshch vreshch deleted the feature/m4-git-sync branch July 6, 2026 08:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant