You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Status: Keep open as an architecture epic. Partially stale against main at e5d48e44.
Priority: P3 backlog for now; not a stable blocker unless cross-daemon/remote workflows become common.
Current code notes:
PR fix(runt): open ephemeral notebooks by id #2561 improved runt open <uuid> for ephemeral notebooks, but it does not implement notebook URIs, cross-daemon ownership, or daemon-to-daemon peer routing.
Current ownership remains in-process only through the room/path index; opening still flows through path/string-style APIs rather than a URI transport selector.
Frame references in the original body should be read with the current protocol map: 0x06 is PoolStateSync, 0x07 is SessionControl, and PutBlob is drafted as 0x08.
Summary
A notebook is identified by a URI, not a bare filesystem path. Daemons participate as Automerge peers of each other for any notebook URI; at most one daemon is the owner (runs the kernel, writes the file) and the rest connect as sync peers over whatever transport fits the URI scheme. The same primitive serves three cases that today need three different answers:
Case
Today
With this primitive
Two local daemons opened the same .ipynb
Silent autosave clobber (see linked bug)
Second daemon dials the first; one room, one writer
Notebook lives on a remote box, edit it from a laptop
URI is ssh://host/path/nb.ipynb; local daemon dials remote daemon over SSH
Multiple agents share one notebook
Pile of MCP proxies, all racing to autosave
All speak to the owning daemon, sync converges
The closed predecessors (#832, #1332, #1333) already built the daemon-as-peer model in-process and as a subprocess. This issue is "extend the same model across daemon process boundaries."
URI scheme
Every connect_notebook call accepts a URI. The MCP wire stays string-typed. Bare paths keep working as a shorthand for file://.
Scheme
Owner location
Transport
Notes
file:///abs/path/nb.ipynb
Same machine
Local Unix socket / named pipe
Today's path, with explicit ownership
ssh://[user@]host[:port]/path/nb.ipynb
Remote machine
SSH stdio (ssh host runt sync-serve --notebook /path)
Useful for "attach me to whatever room this daemon already has," no path involved
The URI is the key. file:///nb.ipynb opened by daemon A and daemon B resolves to the same room; one of them owns it, the other is a peer.
Ownership
Exactly one daemon is the owner of a URI at any given moment. The owner:
Reads and writes the .ipynb (or whatever the scheme's storage is).
Runs the kernel for that notebook.
Holds the canonical Automerge doc.
Everyone else is a peer that syncs the doc and drives the UI / MCP / agent locally.
Discovery on the same machine: the owning daemon writes a tiny presence file in a known cache dir (~/.cache/runtimed/owners/{sha256(uri)}.json) with pid, socket_path, channel, heartbeat_at. Other daemons consult the directory before claiming a URI; if they find a live owner, they dial it as a peer instead.
Discovery across machines: the local daemon dials the remote one explicitly via the URI's transport (SSH for ssh://). The remote daemon's "is this URI already owned" check is local on its side.
Ownership transfer: owner is whoever the kernel was last launched from. If the kernel is restarted on a different machine, ownership transfers along with it. This tracks user intent ("compute moved, doc follows"). Open question: how to make transfer atomic without dropping in-flight executions.
Crash recovery: lockfile sidechannels carry a heartbeat. If the heartbeat goes stale and the PID is dead, the next daemon to ask claims it and reads .ipynb from disk.
What stays local on each box
The kernel's Jupyter transport (shell, iopub, control, stdin, heartbeat: ZMQ today, IPC sockets after #1813) is internal to whichever box the kernel runs on. Nothing about it crosses a daemon boundary. The cross-daemon channel only carries:
Kernel control commands (interrupt, restart, shutdown) become writes to RuntimeStateDoc that the owning daemon observes and dispatches to its local kernel. Today's execution is already CRDT-driven (coordinator writes queued, runtime agent transitions to running/done). Pushing interrupt and restart onto the same surface means the entire daemon-to-daemon protocol is "Automerge bytes both ways," with no parallel RPC channel.
Transport-agnostic protocol
The wire format is the existing notebook-sync framing. SSH stdio, Unix sockets, named pipes, and (in principle) HTTPS websockets all carry the same frames. That answers minrk's question on #1334: yes, an HTTP-based transport can carry the protocol; the protocol does not depend on it. The hard question becomes auth, not wire shape, and that is scoped per-transport (SSH config for ssh://, socket access for file://, bearer token + CORS for any future HTTPS variant).
What needs to change
URI parser + scheme dispatch in runt-mcp and the daemon's connect_notebook handler. file:// continues to work as today; new schemes route through a transport-selector.
Ownership table on disk: ~/.cache/runtimed/owners/{hash}.json with heartbeat. Check before creating a room; bail out and dial the owner if it exists.
Owner-side sync server: each daemon already runs notebook-sync over Unix sockets. Generalize so it can accept inbound peer connections from another daemon, not only frontend / MCP clients.
Peer-side dialer: when the local daemon resolves a URI to a remote owner, it spawns the right transport (ssh ... runt sync-serve --notebook /path for SSH; direct socket connect for file:// cross-daemon) and pipes Automerge frames through.
Kernel-control via RuntimeStateDoc: replace the remaining RPC paths (interrupt, restart, shutdown) with CRDT writes the owner observes.
Failover for ownership transfer (out-of-scope for v1, but worth designing space for): when a kernel-restart targets a different machine, transfer ownership cleanly without losing in-flight execution state.
Scope
In scope: the same-machine cross-daemon case (which fixes the autosave-collision bug #2285) and the SSH transport (which is #1334).
Out of scope: HTTP / HTTPS transport (interesting for the JupyterHub case minrk raised on #1334; tracked separately if we ever take it on), publishing notebook URIs as a stable shareable identity (closer to a "join my notebook" share-link feature), GUI for picking remote hosts.
Current triage (2026-05-05)
Status: Keep open as an architecture epic. Partially stale against
mainate5d48e44.Priority: P3 backlog for now; not a stable blocker unless cross-daemon/remote workflows become common.
Current code notes:
runt open <uuid>for ephemeral notebooks, but it does not implement notebook URIs, cross-daemon ownership, or daemon-to-daemon peer routing.0x06isPoolStateSync,0x07isSessionControl, and PutBlob is drafted as0x08.Summary
A notebook is identified by a URI, not a bare filesystem path. Daemons participate as Automerge peers of each other for any notebook URI; at most one daemon is the owner (runs the kernel, writes the file) and the rest connect as sync peers over whatever transport fits the URI scheme. The same primitive serves three cases that today need three different answers:
.ipynbssh://host/path/nb.ipynb; local daemon dials remote daemon over SSHThe closed predecessors (#832, #1332, #1333) already built the daemon-as-peer model in-process and as a subprocess. This issue is "extend the same model across daemon process boundaries."
URI scheme
Every
connect_notebookcall accepts a URI. The MCP wire stays string-typed. Bare paths keep working as a shorthand forfile://.file:///abs/path/nb.ipynbssh://[user@]host[:port]/path/nb.ipynbssh host runt sync-serve --notebook /path)daemon://socket-or-host/uuidThe URI is the key.
file:///nb.ipynbopened by daemon A and daemon B resolves to the same room; one of them owns it, the other is a peer.Ownership
Exactly one daemon is the owner of a URI at any given moment. The owner:
.ipynb(or whatever the scheme's storage is).Everyone else is a peer that syncs the doc and drives the UI / MCP / agent locally.
Discovery on the same machine: the owning daemon writes a tiny presence file in a known cache dir (
~/.cache/runtimed/owners/{sha256(uri)}.json) withpid,socket_path,channel,heartbeat_at. Other daemons consult the directory before claiming a URI; if they find a live owner, they dial it as a peer instead.Discovery across machines: the local daemon dials the remote one explicitly via the URI's transport (SSH for
ssh://). The remote daemon's "is this URI already owned" check is local on its side.Ownership transfer: owner is whoever the kernel was last launched from. If the kernel is restarted on a different machine, ownership transfers along with it. This tracks user intent ("compute moved, doc follows"). Open question: how to make transfer atomic without dropping in-flight executions.
Crash recovery: lockfile sidechannels carry a heartbeat. If the heartbeat goes stale and the PID is dead, the next daemon to ask claims it and reads
.ipynbfrom disk.What stays local on each box
The kernel's Jupyter transport (shell, iopub, control, stdin, heartbeat: ZMQ today, IPC sockets after #1813) is internal to whichever box the kernel runs on. Nothing about it crosses a daemon boundary. The cross-daemon channel only carries:
0x06)Kernel control commands (interrupt, restart, shutdown) become writes to RuntimeStateDoc that the owning daemon observes and dispatches to its local kernel. Today's execution is already CRDT-driven (coordinator writes
queued, runtime agent transitions torunning/done). Pushing interrupt and restart onto the same surface means the entire daemon-to-daemon protocol is "Automerge bytes both ways," with no parallel RPC channel.Transport-agnostic protocol
The wire format is the existing
notebook-syncframing. SSH stdio, Unix sockets, named pipes, and (in principle) HTTPS websockets all carry the same frames. That answers minrk's question on #1334: yes, an HTTP-based transport can carry the protocol; the protocol does not depend on it. The hard question becomes auth, not wire shape, and that is scoped per-transport (SSH config forssh://, socket access forfile://, bearer token + CORS for any future HTTPS variant).What needs to change
runt-mcpand the daemon'sconnect_notebookhandler.file://continues to work as today; new schemes route through a transport-selector.~/.cache/runtimed/owners/{hash}.jsonwith heartbeat. Check before creating a room; bail out and dial the owner if it exists.ssh ... runt sync-serve --notebook /pathfor SSH; direct socket connect forfile://cross-daemon) and pipes Automerge frames through.Scope
In scope: the same-machine cross-daemon case (which fixes the autosave-collision bug #2285) and the SSH transport (which is #1334).
Out of scope: HTTP / HTTPS transport (interesting for the JupyterHub case minrk raised on #1334; tracked separately if we ever take it on), publishing notebook URIs as a stable shareable identity (closer to a "join my notebook" share-link feature), GUI for picking remote hosts.
Related