feat(sync): git sync engine in the daemon - origin push/pull (M4)#220
Merged
Conversation
Contributor
|
🎉 PR Validation ✅ PASSED Commit: Checks:
Ready to merge! ✨ 🔗 View workflow run |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
M4 - git sync
The local daemon now acts on each vault's
origin[]: a debounced commit + push / pull-rebasecycle 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 smallgit-exec wrapper (spawnsgit, cwd = vault path; memory-core keeps itsown
createGitprivate) + a per-vault cycle: commit-if-dirty -> ensure remote -> fetch ->pull-rebase (keeping both sides on conflict) -> push. Every step is tolerant.
origin[].intervalon boot and onvaults.jsonchange; sync state exposed at
GET /api/sync/status, forced runs atPOST /api/sync/run.daemon statusprints a one-liner per synced vault.agentage vault sync [name]- forces a sync now (all origin-carrying vaults, or one). Uses arunning 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-syncedvault is usable end to end.
Key decisions
intervalas a bare non-negative integerwith 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 syncablevia
vault sync)..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 nocommitted
.gitignorein the user's vault. Absentignore-> defaults['.obsidian/', 'data.json']; a set value REPLACES the defaults;[]= sync everything.(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, soremote'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.unreachable | lock | conflict | other). A heldindex.lock(a concurrent engine mutation) is a clean skip that the next cyclecatches 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).
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 writeserroring on "nothing to commit". No other lockfile churn; TypeScript stays pinned at 5.9.3.
Tests
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).
@p0(e2e/sync.test.ts, fully self-contained - localgit init --bare, isolated configdir, ephemeral daemon port; never the real
~/.agentageor:4243): (a) daemon auto-loop pushesa write to the bare remote within one interval; (b) divergence ->
<file>.conflict.md+ bothcontents preserved + push succeeds; (c) unreachable remote -> write still succeeds, error
recorded, no crash.
npm run verifygreen (203 unit tests). Full e2e green (23 tests) against dev, including the newsync tier run 3x locally with no flakes.
Guards
Zero new dependencies (spawns
gitdirectly); the R6 package-guard allowlist is unchanged.