Why CRDTs?
Traditional approaches to multi-user editing require:- Central server making decisions about conflicts
- Locking preventing concurrent edits
- Last-write-wins losing data on conflicts
- Automatic merge of concurrent edits
- No central authority needed for conflict resolution
- Eventual consistency all replicas converge to the same state
- Offline-first edits work without connectivity
Automerge is a CRDT library that provides JSON-like documents with automatic merge semantics.
Sync Architecture
Two Sync Channels
nteract Desktop maintains two separate Automerge documents:1. Settings Document
A single shared document for user preferences:~/.cache/runt/settings.automerge with JSON mirror at ~/.config/nteract/settings.json
Migration: Backward-compatible migration from flat keys to nested structures
2. Notebook Documents
Each open notebook gets its own Automerge document in a “room”:~/.cache/runt/notebook-docs/{sha256(notebook_id)}.automerge
Room-Based Architecture
The daemon manages notebook sync through rooms:- First window opens → Daemon creates/loads room
- Client handshake →
Handshake::NotebookSync { notebook_id } - Initial sync → Exchange Automerge sync messages until convergence
- Watch loop → Listen for local edits and peer changes
- Peer changes → Apply, persist, broadcast to other peers
- Last window closes → Room evicted, document persisted
Rooms are identified by
notebook_id. Multiple windows opening the same notebook join the same room automatically.Sync Protocol
Wire Format
Length-prefixed binary frames over Unix socket:Sync Flow
Initial Sync
- Server sends first: Daemon initiates with its current state
- Client responds: Window sends its local changes (if any)
- Exchange until convergence: Both sides send sync messages with 100ms timeout
- Sync complete: Both replicas have identical state
Change Propagation
After initial sync, changes propagate immediately:- Local edit: Window modifies Automerge doc
- Generate sync message: Automerge creates minimal change representation
- Send to daemon: Sync message over socket
- Daemon applies: Under write lock, update canonical doc
- Persist: Serialize and write to disk (outside lock)
- Broadcast: Notify all other peers
- Peers apply: Other windows receive and apply change
Latency target: Sub-200ms from edit in Window A to display in Window B for local connections.
Conflict Resolution
Concurrent Edits
Two windows editing the same cell simultaneously: Window A: Typeshello in cell source
Window B: Types world in same cell source
Automerge’s Text CRDT merges these character-level:
- Both edits applied to document
- Automerge determines character insertion order
- Result might be
helloworldorworldhello(deterministic based on Lamport clocks) - Both windows converge to same final text
Deterministic Merge
Automerge uses logical clocks to establish a total order on concurrent operations:Write-Once Data
Outputs are write-once from a single actor (the kernel), so they don’t need CRDT semantics:Text Editing Semantics
Cell source uses Automerge’sText type for proper concurrent editing:
Character-Level Merging
Window A:"hello" → "hello world"
Window B: "hello" → "hello there"
Automerge represents this as:
Update Operation
When you type in a cell, the frontend sends the full new text:- Runs Myers diff algorithm
- Generates minimal character-level patch
- Applies patch operations (insert/delete)
- Broadcasts patch, not full text
Persistence
Automerge Binary Format
Documents are saved as compact binary (not JSON):Persistence Strategy
After every sync message:- Serialize: Inside write lock, call
doc.save() - Write to disk: Outside write lock, async I/O
- Atomic write: Temp file + rename for crash safety
Corrupt Document Recovery
If a persisted.automerge file can’t be loaded:
- Rename to
.automerge.corrupt - Create fresh document
- Log warning
- Continue operation
Settings Sync
JSON Mirror
Settings maintain a JSON mirror for external tool compatibility: Automerge doc:~/.cache/runt/settings.automerge (canonical)
JSON mirror: ~/.config/nteract/settings.json (read-only view)
The daemon watches the JSON file with a debounced file watcher (500ms):
- External tool edits JSON
- Daemon detects change
- Parse JSON, apply to Automerge doc
- Persist Automerge binary (not back to JSON)
- Broadcast to all peers
Migration
Backward-compatible migration from flat keys: Old format:Multi-Window Benefits
Real-Time Collaboration
Multiple windows editing the same notebook see changes in real-time.
Late-Joiner Sync
Open a second window and it catches up instantly with current state.
Output Sharing
Execute in one window, outputs appear in all windows viewing that notebook.
No Save Conflicts
Automatic merge means no “file changed on disk” dialogs.
Performance
Latency Measurements
| Operation | Typical Latency |
|---|---|
| Local edit → sync message | <5ms |
| Sync message round-trip | 1-5ms (local daemon) |
| Daemon apply + persist | 5-20ms |
| Total edit propagation | <50ms |
Optimization: Dual Channel
For execution outputs, nteract uses a dual-channel design: Channel 1 (Automerge): Durable, synced state Channel 2 (Broadcasts): Ephemeral, real-time events When a cell executes:- Kernel outputs → daemon writes to Automerge (persisted)
- Daemon also sends broadcast event to all peers (fast)
- Executing window shows output from broadcast (<50ms)
- Other windows apply Automerge change (synced state)
Batching
Rapid consecutive changes can be batched:Known Limitations
Widget Multi-Window Sync
Widgets only render in the window that created them. Secondary windows show “Loading widget” because they miss the initialcomm_open message.
Root cause: The Jupyter comm protocol establishes widget models via messages. Late joiners don’t receive historical messages.
Workaround: Use single window for widget-heavy notebooks.
Future fix: Sync widget/comm state via Automerge for late-joiner reconstruction.
Output Sync Race
During cell execution, there’s a brief window where daemon sync updates may conflict with local output updates:- Frontend clears outputs, marks cell executing
- Kernel outputs arrive, frontend updates local state
- Frontend calls
sync_append_output(async to daemon) - Daemon may send
notebook:updatedbefore append arrives
parent_header.msg_id in cell metadata to correlate execution requests with outputs.
Troubleshooting
Changes not syncing
Changes not syncing
Check:Look for sync messages and errors.
- Daemon running:
runt daemon status - Same notebook_id: Check daemon logs for room join
- Socket permissions:
ls -l ~/.cache/runt/runtimed.sock
Sync latency high
Sync latency high
Causes:
- Large Automerge document (many historical changes)
- Disk I/O bottleneck (spinning disk)
- Many peers (broadcast overhead)
- Compact Automerge doc (future feature)
- Use SSD for cache directory
- Limit number of concurrent windows
Document corrupted
Document corrupted
Symptoms: Error loading notebook, sync failsCheck:Look for
.automerge.corrupt files.Recovery:- Delete corrupted file
- Reopen notebook (loads from .ipynb)
- Fresh Automerge doc created
Advanced: Inspecting Sync State
List Active Rooms
Inspect Notebook State
- Notebook ID
- Number of active peers
- Kernel status
- Environment source
Daemon Logs
[notebook-sync]Room lifecycle[automerge]Sync protocol messages[persist]Document save operations
Next Steps
Daemon
Learn about room management
Notebooks
Understand notebook format
Architecture
View system overview
Kernels
Explore kernel execution