# Plan A: Native Sync Protocol (macOS + Server) > **AGENT INSTRUCTION — READ FIRST** > > When you complete a phase or are ready for a code review, you **must** notify the opencode session that created this plan. Use the opencode HTTP API to send a message to session `ses_225742b62ffeOSelYyCe4IPwxL`. > > **Message format:** > ``` > Subject: Plan A Review Ready — Phase X Complete > Body: > - Phase completed: [e.g., Phase 1] > - Files touched: [list of files added/modified] > - Summary: [2-3 sentence description of what was implemented] > - Tests: [which tests were added or updated] > - Blockers: [any issues or decisions needed] > ``` > > **OpenCode API Specification:** > ``` > POST https://api.opencode.ai/v1/sessions/ses_225742b62ffeOSelYyCe4IPwxL/messages > Content-Type: application/json > Authorization: Bearer > > Request body: > { > "role": "assistant", > "content": "Subject: Plan A Review Ready — Phase X Complete\n\nBody:\n- Phase completed: ...\n- Files touched: ...\n- Summary: ...\n- Tests: ...\n- Blockers: ..." > } > ``` > > Wait for a response before starting the next phase unless explicitly told otherwise. ## Goal Build a bidirectional filesystem sync protocol that lets a native macOS client keep a local folder in sync with the cairnquire server. The browser client continues to work via the existing WebSocket + REST API. ## Architecture ### Server: Sync Service (`internal/sync/`) New package alongside existing services: ``` internal/sync/ ├── protocol.go # Sync protocol types and state machine ├── service.go # Business logic: resolve conflicts, apply changes ├── handlers.go # HTTP endpoints for sync (wrapped in httpserver) ├── store.go # Abstract over docs + store for sync operations └── watcher.go # Bridge file watcher events into sync protocol ``` ### macOS Client A separate Go binary in `apps/macos-sync/` (or `apps/sync/`): ``` apps/macos-sync/ ├── cmd/md-sync/ # Main entry point ├── internal/ │ ├── client.go # HTTP client for sync protocol │ ├── fs.go # macOS filesystem watcher (fsevents) │ ├── sync.go # Local sync engine │ └── state.go # Local state DB (SQLite or JSON) ``` ## Sync Protocol v1 ### Data Model - **Snapshot**: A point-in-time tree of `{path, hash, size, mtime}` - **Delta**: List of changes since a snapshot: `{type: create|update|delete|rename, path, oldPath?, hash, size}` - **Conflict**: When server and client both changed same file since last common snapshot ### Endpoints ``` POST /api/sync/init Body: {deviceId, rootPath} Response: {snapshotId, serverSnapshot} POST /api/sync/delta Body: {snapshotId, clientDelta} Response: {serverDelta, conflicts[]} POST /api/sync/resolve Body: {snapshotId, resolutions[]} Response: {newSnapshotId} GET /api/sync/content?hash= Response: raw file bytes (content-addressed, immutable) ``` ### Protocol Flow ``` 1. Client calls /sync/init → gets server snapshot 2. Client computes local delta (fsevents + local state) 3. Client POSTs delta to /sync/delta 4. Server responds with: - serverDelta (changes client needs to apply) - conflicts[] (need user resolution or auto-merge) 5. Client resolves conflicts locally or via UI 6. Client POSTs resolutions to /sync/resolve 7. Server returns new snapshotId, both sides updated ``` ### Conflict Resolution Strategies - `last-write-wins` (default for non-conflicting files) - `rename-both` (keep both as `file.md` and `file (conflict).md`) - `manual-merge` (flag for UI) - `server-wins` / `client-wins` (per-file or global preference) ### Authentication - API keys (long-lived, revocable) — simpler for native clients - OAuth2 device flow — better for multi-user - Start with API keys: `X-API-Key: ` header ## macOS Client Design ### File Watcher - Use `github.com/fsnotify/fsevents` (Cgo wrapper around FSEvents) - Debounce rapid changes (500ms window) - Ignore `.git`, `.DS_Store`, temp files ### Local State - SQLite DB at `~/Library/Application Support/md-sync/state.db` - Tables: `snapshots`, `files`, `pending_deltas`, `conflicts` - Or simpler: JSON files + file hashes on disk ### Sync Engine - Runs as daemon / menubar app - Configurable sync interval (default: continuous via fsevents) - Offline queue: store deltas locally, sync when reconnected - Bandwidth-aware: skip large files on cellular ### UI (Optional Phase 2) - Menubar icon: sync status, last sync time, conflicts badge - Settings window: root folder, server URL, API key, exclusions - Conflict resolution dialog ## Server Changes Required ### 1. Sync Service ```go type SyncService struct { docStore *docs.Service fileStore *store.Service watcher *fsnotify.Watcher } func (s *SyncService) InitSync(deviceID string) (*Snapshot, error) func (s *SyncService) ApplyDelta(snapshotID string, delta Delta) (*DeltaResult, error) func (s *SyncService) ResolveConflicts(snapshotID string, resolutions []Resolution) (*Snapshot, error) ``` ### 2. Authentication - Add API key table to database - Middleware to accept `X-API-Key` or session cookie - API keys scoped to user + device ### 3. Content-Addressed Fetch - `GET /api/content/` — immutable, cache-friendly - Uses existing `store.Service` (already content-addressed) ### 4. Push Notifications (Optional) - Server-Sent Events or WebSocket to tell client "server changed" - Avoids client polling ## Implementation Phases ### Phase 1: Server Sync Protocol (2-3 weeks) - [ ] Define protocol types and state machine - [ ] Implement `/api/sync/init` and snapshot generation - [ ] Implement `/api/sync/delta` with conflict detection - [ ] Add content-addressed fetch endpoint - [ ] Write sync protocol tests (table-driven, simulate client/server) - [ ] Document protocol in `docs/specs/sync-protocol-v1.md` ### Phase 2: macOS Sync Engine (2-3 weeks) - [ ] Project scaffold: Go module, build scripts - [ ] FSEvents watcher with debouncing - [ ] Local state SQLite DB - [ ] HTTP client for sync endpoints - [ ] Basic sync loop: init → delta → apply - [ ] Offline queue (store deltas, retry with backoff) - [ ] CLI tool: `md-sync --folder ./docs --server https://...` ### Phase 3: Conflict Resolution + Polish (1-2 weeks) - [ ] Auto-resolve strategies - [ ] Conflict file generation (`file (conflict).md`) - [ ] Menubar app wrapper (Swift or `github.com/progrium/macdriver`) - [ ] Settings/preferences - [ ] Binary signing + notarization for distribution ### Phase 4: Advanced Features (Future) - [ ] Selective sync (ignore patterns) - [ ] Bandwidth limiting - [ ] Version history / time machine from server snapshots - [ ] iOS client (shares core sync engine) ## Pros - Native performance, works with any editor (VS Code, Obsidian, etc.) - Familiar sync model (Dropbox, iCloud Drive) - Content-addressed storage makes deduplication trivial - macOS client opens door to iOS, Windows, Linux ## Cons - New authentication system (API keys) - Complex conflict resolution UX - macOS development overhead (codesign, notarization, FSEvents) - Diverges from browser-first architecture ## Decision Needed - Should we support both browser sync AND native sync, or pick one? - If both: shared sync service, different transport (WebSocket vs HTTP)