Skip to content

feat(sync): add automatic scheduled sync - #159

Merged
ClaudiaFang merged 1 commit into
claude/mobile-source-control-densityfrom
claude/automatic-sync-141
Sep 21, 2026
Merged

ClaudiaFang merged 1 commit into
claude/mobile-source-control-densityfrom
claude/automatic-sync-141

Conversation

@ClaudiaFang

Copy link
Copy Markdown
Member

Closes #141

Summary

Adds v1 Automatic Sync: an optional, configurable schedule that keeps the vault and remote repository in sync using the same default Sync action routing as the manual Sync button, while leaving conflicts for manual resolution.

Stacked on claude/mobile-source-control-density (PR #156, still open). This PR contains only Automatic Sync changes; retarget to main if #156 merges first.

Behavior

  • Automatic sync is off by default. When enabled, it refreshes authoritative local + remote state at the configured interval and applies the default action per change:
    • local-only / local-modified → push
    • local-deleted → delete remote
    • remote-only / remote-modified → pull
    • moved → push/move through the existing planning path
    • conflict → skipped, left for manual resolution
    • synced → no-op
  • No per-file instant sync: vault create/modify/delete/rename events continue to update status only. Mutation happens only on the interval and optionally once on startup.

Background conflict semantics (critical)

  • PushCoordinator.planSyncBatch() gains a UI-free PushConflictBehavior = 'prompt' | 'skip'.
  • Interactive/manual keeps the existing behavior: prompt for batch conflict resolution, and cancelling aborts the whole batch (unchanged).
  • Background/automatic passes skip: conflicting paths are left out of the plan (skippedConflicts/conflictedPaths) and the batch is never cancelled, so unrelated safe paths continue in the same run.
  • Skipped/conflicted paths are excluded from the commit and pull target sets and reset to idle, so they are never reported as operation success even though they were in the original target set. Regression test: SourceControlActionService.test.ts"never marks a path that became a conflict as operation success…".
  • After the final refresh, conflicting files remain visible as conflicts in Source Control.
  • Pull-side conflicts reuse the existing safe-skip behavior rather than being reimplemented.

Settings and defaults

Setting Default Notes
automaticSyncEnabled false Off by default
automaticSyncIntervalMinutes 5 Minimum 1 minute
automaticSyncOnStartup false Optional one-shot on load
autoRefreshOnStartup (existing) true Kept as a separate concept; semantics unchanged
  • Invalid/older-stored intervals are normalized (normalizeAutomaticSyncIntervalMinutes) so 0, negative, NaN, Infinity, or non-numeric values can never create a zero/tight-loop timer.
  • Settings changes take effect without a plugin reload; unchanged values are a no-op (no timer postponement).
  • Settings UX clearly separates Automatic sync, Sync interval, Sync on startup, and Refresh status on startup.
  • EN / zh-TW / zh-CN strings added.

Startup behavior

  • After onLayoutReady, if automaticSyncEnabled && automaticSyncOnStartup, one background automatic sync runs without opening or focusing Source Control.
  • Otherwise the existing autoRefreshOnStartup behavior is preserved.
  • Startup automatic sync supersedes the legacy startup refresh so boot does not duplicate its own refresh/network fetch.

Concurrency

  • SyncExecutionGuard serializes all provider mutations.
  • Automatic runs use try-acquire: a tick firing mid-run is skipped (silent/debug), never queued as a backlog.
  • Manual work waits for the lock rather than being discarded, and serializes behind an in-flight automatic commit instead of racing provider mutations.
  • No mid-request cancellation/preemption; no general-purpose job framework.

Architecture

Reuses one engine — Source Control application layer → SyncWorkspace → Sync domain → provider:

  • AutomaticSyncService.runOnce(): refresh → ChangeRepository → exclude synced/conflict → default intents via ChangeActionPolicySourceControlActionService.sync(intents, 'background') → refresh.
  • SyncExecutionMode = 'interactive' | 'background' is a per-execution policy (not a loose set of booleans, so invalid combinations can't be expressed).
  • Timer mechanics live in AutomaticSyncScheduler (plugin runtime) and are wired via createSyncRuntime; main.ts only owns lifecycle.
  • No concrete GitHub/GitLab/Gitea service, classification, rename detection, or provider mutation logic is duplicated.

Notifications

  • Manual behavior unchanged.
  • Automatic: silent on success / no changes / skipped conflicts / skipped ticks; transient errors are logged through the normal logger with no repeating periodic toast; final refresh makes unresolved states visible.

What's New — 1.7.0

  • Added src/changelog/1.7.0/index.ts, registered first in src/changelog/index.ts (hand-curated release entry; release-specific localized copy stays in the entry, not the shared UI catalog).
  • Headline, summary, and 3 notable entries in EN / zh-TW / zh-CN.

Test coverage added

  • Settings: defaults OFF / 5 / OFF; older stored settings inherit new defaults; interval normalization rejects invalid input; persistence.
  • Execution policy: interactive prompts; background skips confirmation and never invokes conflict UI; background planning still validates the plan; safe push continues when another path conflicts; skipped conflicts not reported as success; mixed push/pull/delete default routing.
  • AutomaticSyncService: refresh before/after; refreshed snapshot used; excludes synced/conflict; default actions via policy; background mode; no-op on no changes; overlapping runOnce() does not execute concurrently; errors do not permanently lock.
  • Scheduler/lifecycle: disabled → no timer; enabled → scheduled; interval respected; interval change reschedules; old timer no longer fires; invalid interval falls back to default; unchanged re-apply does not postpone; dispose stops execution.
  • Startup: sync-on-startup runs exactly once, does not open Source Control, and suppresses the redundant legacy refresh; fallback preserved.
  • Settings UI / i18n: toggles + interval render and validate; refresh setting stays separate; EN/zh-TW/zh-CN keys resolve.
  • What's New: 1.7.0 registered; headline/summary/notable entries resolve; current-version banner surfaces 1.7.0; dismissal unchanged.

Verification (exact commands)

npx eslint .   → 0 errors, 0 warnings
npm run build  → passed (tsc + Obsidian 1.11.0 compat typecheck + esbuild)
npx vitest run → 80 files / 1011 tests passed

Manual verification

Not performed — no executable Obsidian environment was available. Checklist for a reviewer with Obsidian:

  • Automatic Sync defaults OFF.
  • Enabling it exposes/activates interval behavior correctly.
  • Scheduled local change pushes.
  • Scheduled remote change pulls.
  • A conflict remains unresolved and does not block an unrelated safe change.
  • Startup sync works without opening Source Control.
  • Disabling Automatic Sync stops subsequent runs.
  • Changing the interval does not create duplicate timers.

Release note

Semantic-release performs the actual 1.7.0 version bump. manifest.json, package.json, versions.json, and the generated CHANGELOG.md were intentionally not hand-edited.

Base branch integrity

@ClaudiaFang
ClaudiaFang force-pushed the claude/automatic-sync-141 branch from 0b2061e to bd12bbf Compare September 16, 2026 09:01
@sonarqubecloud

Copy link
Copy Markdown

@ClaudiaFang
ClaudiaFang merged commit bc9caa3 into claude/mobile-source-control-density Sep 21, 2026
50 of 67 checks passed
@ClaudiaFang
ClaudiaFang deleted the claude/automatic-sync-141 branch September 21, 2026 02:19
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