Daemon Development
Quick Reference
| Task | Command |
|---|---|
| Start full dev loop | cargo xtask dev |
| Report app + daemon identity | cargo xtask dev status |
| Focus exact macOS dev app | cargo xtask dev focus |
| Restart app + daemon cleanly | cargo xtask dev --fresh |
| Start dev daemon | cargo xtask dev-daemon |
| Full build | cargo xtask build |
| Rust-only rebuild | cargo xtask build --rust-only |
| Fast sidecar check | cargo xtask build --rust-only --skip-tauri |
| Run bundled binary | cargo xtask run |
| WASM rebuild | cargo xtask wasm |
| Install nightly (Linux) | ./scripts/install-nightly |
| Daemon status | ./target/debug/runt daemon status |
| Daemon status (JSON) | ./target/debug/runt daemon status --json |
| Tail logs | ./target/debug/runt daemon logs -f |
| List kernels | ./target/debug/runt ps |
| List notebooks | ./target/debug/runt notebooks |
| Flush pool | ./target/debug/runt daemon flush |
| Stop daemon | ./target/debug/runt daemon stop |
| Run daemon tests | cargo test -p runtimed |
| Lint | cargo xtask lint --fix |
Architecture
The daemon (runtimed) is a singleton coordinating notebook windows over a Unix socket. Without it: race conditions on prewarmed environments, wasted resources, slow cold starts.
Components: Unix socket (IPC), lock file (singleton guarantee), UV/Conda pools (prewarmed envs), blob store (content-addressed outputs), notebook-docs (persisted Automerge documents).
Protocol: Length-prefixed typed frames over Unix socket. Preamble → JSON handshake → Automerge sync → steady state. Frame types: AutomergeSync (0x00), Request (0x01), Response (0x02), Broadcast (0x03), Presence (0x04), RuntimeStateSync (0x05), PoolStateSync (0x06), SessionControl (0x07), PutBlob (0x08), CommsDocSync (0x09).
CRDT ownership: Frontend WASM writes cell source, position, type, metadata. Daemon writes RuntimeStateDoc for outputs, execution counts, comm topology, trust, env progress, and project context. Mutable widget state lives in CommsDoc and is gated by RuntimeStateDoc topology. Write to a CRDT document only from an authority allowed for that document.
Build System
How cargo xtask build works (4 phases)
- Artifact guard: Verify gitignored WASM, renderer-plugin, and MCP widget outputs. Build/dev commands fingerprint workspace inputs and skip wasm-pack when outputs are current; rebuild only when outputs are missing, invalid, or stale.
- Single Rust compilation:
cargo build -p runtimed -p runt -p nteract-mcp -p mcp-supervisor. Sidecars copied tocrates/notebook/binaries/. Thenotebookcrate is intentionally excluded until the Tauri link phase after frontend assets exist. - Frontend build:
pnpm build(TypeScript + Vite).--rust-onlyskips this. - Tauri link:
cargo tauri build --debug --no-bundlewith embedded frontend assets. Use--skip-taurionly for fast edit checks where updated sidecar binaries are enough; run a normal build before launching the bundled app.
All Rust targets build in one cargo build call to avoid feature-unification recompilation. WASM outputs are gitignored; runtimed's build.rs panics if missing. nteract-mcp embeds the MCP widget HTML; prepare it with cargo xtask artifacts ensure mcp-widget.
WASM rebuild
Run after changing crates/runtimed-wasm/, crates/sift-wasm/, crates/notebook-doc/, crates/notebook-wire/, or scripts/build-renderer-plugins.ts:
cargo xtask wasm # all (runtimed-wasm + sift-wasm + renderer plugins)
cargo xtask wasm runtimed # only runtimed-wasm
cargo xtask wasm sift # only sift-wasm
Development Workflow
Fast iteration
cargo xtask dev
This is the normal desktop workflow: it starts the per-worktree daemon, waits for readiness, launches the hot-reload app, and stops the daemon it started when the app exits. On macOS the app has a distinct per-worktree display name, bundle ID, and process record. Inspect or focus that exact process with:
cargo xtask dev status
cargo xtask dev focus
After Tauri bootstrap or listener changes, use cargo xtask dev --fresh to reset
only this worktree's app and daemon. Use the split workflow when the daemon must
remain independently managed:
# Terminal 1
cargo xtask dev-daemon
# Terminal 2
cargo xtask notebook
Testing
cargo test -p runtimed # All tests
cargo test -p runtimed --test integration # Integration tests only
cargo test -p runtimed test_daemon_ping_pong # Specific test
Per-worktree isolation
cargo xtask dev, cargo xtask dev-daemon, cargo xtask notebook, and cargo xtask run-mcp derive the git worktree automatically. State lives at <cache>/runt-nightly/worktrees/{hash}/. Set RUNTIMED_DEV=1 and RUNTIMED_WORKSPACE_PATH="$(pwd)" only for raw ./target/debug/runt commands outside xtask.
Python Bindings
Two venvs
| Venv | Path | Purpose |
|---|---|---|
| Workspace | .venv (repo root) |
Day-to-day dev, MCP server |
| Test | python/runtimed/.venv |
Isolated pytest runs |
Installation
# Into workspace venv (most common; what `up rebuild=true` does)
cd crates/runtimed-py
VIRTUAL_ENV=../../.venv uv run --directory ../../python/runtimed maturin develop
# Into test venv (for pytest)
VIRTUAL_ENV=../../python/runtimed/.venv uv run --directory ../../python/runtimed maturin develop
Always set VIRTUAL_ENV explicitly. Without it, the .so installs into whichever venv uv run resolves.
Basic usage
import asyncio, runtimed
async def main():
client = runtimed.Client()
async with await client.create_notebook() as notebook:
cell = await notebook.cells.create("print('hello')")
result = await cell.run()
print(result.stdout) # "hello\n"
# Granular execution control
execution = await cell.execute()
print(execution.status) # "queued" | "running" | "done" | "error"
result = await execution.result()
asyncio.run(main())
Output.data typing
| MIME category | Python type | Notes |
|---|---|---|
Binary image (image/png) |
bytes |
Raw binary |
JSON (application/json) |
dict |
Parsed |
Text (text/plain, text/html) |
str |
UTF-8 |
LLM hint (text/llm+plain) |
str |
Synthesized blob URL |
Integration tests
RUNTIMED_SOCKET_PATH="$(./target/debug/runt daemon status --json | python3 -c 'import sys,json; print(json.load(sys.stdin)["socket_path"])')" \
python/runtimed/.venv/bin/python -m pytest python/runtimed/tests/test_daemon_integration.py -v
MCP Server
Use nteract mcp for the supervised MCP server. runt mcp remains available to run the worker directly. Run via cargo xtask run-mcp for development.
Advertised tools (all_tools()): list_active_notebooks, list_notebooks, connect_notebook, create_notebook, save_notebook, show_notebook, disconnect_notebook, create_cell, set_cell, delete_cell, move_cell, execute_cell, run_all_cells, get_results, interrupt_kernel, restart_kernel, manage_dependencies, replace_match, replace_regex.
Hidden/callable read tools (hidden_tools()): get_cell, get_all_cells. These are callable but not advertised in list_tools(). They are dispatch-only read paths for clients that prefer direct tool calls over resource URIs.
Legacy dependency and cell-metadata tool names still dispatch for compatibility, but new workflows should use manage_dependencies for dependency inspection/edits and get_results for execution output lookup by execution_id.
RuntimeStateDoc
Daemon-authoritative Automerge document synced via frame 0x05. Frontend reads only (useRuntimeState()); Python reads via notebook.runtime.
Schema: kernel/{status, starting_phase, name, language, env_source}, queue/{executing, queued}, executions/{id → cell_id, status, execution_count, success}, env/{in_sync, added, removed}, trust/{status, needs_approval}, last_saved, plus comm topology/routing. Mutable widget values live in CommsDoc, not RuntimeStateDoc.
Execution lifecycle: Client sends ExecuteCell → daemon generates execution_id → writes to queue → status progresses through running/done/error → Python Execution handle polls for updates.
Async CRDT Mutations
Any daemon code that reads doc state, does async work, then writes back must
reconcile against the captured baseline heads. Prefer document-owned
transact_at_heads_recovering(...) for ordinary async writes. Use
fork_with_actor(...) + merge_recovering(...) only when the async worker
genuinely needs an editable fork across the await. Direct mutation after an
async gap overwrites concurrent edits.
let baseline_heads = {
let mut doc = room.doc.write().await;
doc.get_heads()
};
let result = do_async_work().await;
let mut doc = room.doc.write().await;
doc.transact_at_heads_recovering(&baseline_heads, Some("runtimed:formatter"), "tx", |doc| {
doc.update_source(&cell_id, &result)?;
Ok(())
}).ok();
For synchronous blocks: doc.fork_and_merge(|fork| { ... }).
Notebook Room Lifecycle
Each open notebook has a room (NotebookRoom), keyed by UUID. A PathIndex maps canonical paths to room UUIDs.
- Autosave: 2s quiet, 10s max interval. Skips untitled/mid-load notebooks.
- Multi-window: Multiple windows join the same room as Automerge peers.
- Eviction: All peers disconnect → delayed eviction (default 30s) → kernel shuts down, room removed.
- Crash recovery: Untitled notebooks persist to
notebook-docs/{hash}.automerge; snapshots innotebook-docs/snapshots/.
Key Invariants
is_binary_mime()has one canonical Rust implementation innotebook-doc::mime, shared across all crates.- Iframe sandbox:
allow-same-originis forbidden. - Per-cell O(1) accessors must stay in sync across WASM, Rust, and Python.
- Hold tokio mutex guards only within synchronous blocks (use block scoping, verify with
cargo test -p runtimed --test tokio_mutex_lint). - Cell list renders in stable DOM order (sorted by ID) with CSS
orderfor visual positioning.
Troubleshooting
Daemon lock held: Inspect the selected endpoint with nteract daemon status and check its daemon.lock with lsof. The OS releases the advisory lock when its owner exits; an existing file is not proof of a live owner. Do not delete the lock file to bypass contention, since replacing its inode can allow two owners. Filesystem failures are separate errors, and live socket metadata is the discovery source.
Pool not replenishing: Verify uv --version and check ~/.cache/runt/envs/.
Wrong daemon (Python): If outputs return parse errors with hashes, the bindings connect to the wrong daemon (blob store is per-daemon). Set RUNTIMED_SOCKET_PATH.
Build not reflected (Python): Rebuild into the correct venv with explicit VIRTUAL_ENV. Or use up rebuild=true.
Shipped App / System Daemon
Production daemon installs as a system service (macOS: launchd, Linux: systemd user). Manage with nteract daemon {status,stop,start,logs,uninstall}.
Key paths (macOS): Binary at ~/Library/Application Support/runt/bin/runtimed, socket at ~/Library/Caches/runt/runtimed.sock, logs at ~/Library/Caches/runt/runtimed.log.
Use ./target/debug/runt daemon stop to stop only your worktree's daemon. Avoid pkill/killall because they affect every worktree.