Files
cairnquire/.project/sync-plan-native.md

7.2 KiB

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 <OPENCODE_API_KEY>

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=<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: <token> 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

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/<hash> — 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)