7.2 KiB
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 asfile.mdandfile (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-Keyor 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/initand snapshot generation - Implement
/api/sync/deltawith 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)