Skip to main content

Binary Framing

All communication over the Unix socket uses length-prefixed binary framing:
[4 bytes: payload length (big-endian u32)] [payload bytes]

Frame Size Limits

MAX_FRAME_SIZE
number
default:"104857600"
Maximum data frame size: 100 MiB (matches blob size limit)
MAX_CONTROL_FRAME_SIZE
number
default:"65536"
Maximum control/handshake frame size: 64 KiB
The handshake and JSON request/response frames use the 64 KiB limit to prevent oversized frames from forcing large allocations before channel routing.

Handshake

Every connection starts with a JSON handshake as the first frame:
{
  "channel": "pool"
}

Handshake Fields

channel
string
required
Channel type: pool, settings_sync, notebook_sync, or blob
notebook_id
string
Notebook sync only: Notebook file path or UUID (for untitled notebooks)
protocol
string
default:"v1"
Notebook sync only: Protocol version (v1 or v2). Use v2 for typed frames.
working_dir
string
Notebook sync only: Working directory for untitled notebooks (used for project file detection)

Pool Channel

Request Types

All requests are JSON objects with a type field:
{
  "type": "take",
  "env_type": "uv"  // or "conda"
}

Response Types

{
  "type": "env",
  "env": {
    "env_type": "uv",
    "venv_path": "/Users/username/.cache/runt/envs/runtimed-uv-abc123/",
    "python_path": "/Users/username/.cache/runt/envs/runtimed-uv-abc123/bin/python"
  }
}

Pool Status Fields

uv_available
number
Number of prewarmed UV environments ready to claim
uv_warming
number
Number of UV environments currently being created
conda_available
number
Number of prewarmed Conda environments ready to claim
conda_warming
number
Number of Conda environments currently being created
uv_error
object
Error info if UV pool is in error state (e.g., invalid default packages)
conda_error
object
Error info if Conda pool is in error state

Notebook Sync Channel

Protocol v2: Typed Frames

Notebook sync connections using protocol: "v2" send typed frames where the first byte indicates the message type:
ByteTypeContent
0x00Automerge SyncBinary Automerge sync message
0x01RequestJSON NotebookRequest
0x02ResponseJSON NotebookResponse
0x03BroadcastJSON NotebookBroadcast
After the handshake, the server sends a ProtocolCapabilities response indicating the negotiated protocol before starting sync.

Notebook Requests

{
  "action": "launch_kernel",
  "kernel_type": "python",
  "env_source": "uv:inline",
  "notebook_path": "/path/to/notebook.ipynb"
}

Notebook Responses

{
  "result": "kernel_launched",
  "kernel_type": "python",
  "env_source": "uv:inline",
  "launched_config": {
    "uv_deps": ["numpy", "pandas"],
    "conda_deps": null,
    "conda_channels": null,
    "deno_permissions": null
  }
}

Notebook Broadcasts

Broadcasts are sent proactively to all connected peers when events occur:
{
  "event": "kernel_status",
  "status": "busy",
  "cell_id": "550e8400-e29b-41d4-a716-446655440000"
}

Blob Channel

Store Blob Flow

1. Client sends handshake:
   {"channel": "blob"}

2. Client sends request (JSON frame):
   {"action": "store", "media_type": "image/png"}

3. Client sends blob data (raw binary frame):
   [PNG binary bytes]

4. Server responds (JSON frame):
   {"hash": "a1b2c3d4e5f6..."}

Get Blob Port

Query the HTTP server port:
Request
{
  "action": "get_port"
}
Response
{
  "port": 54321
}

Example: Pool Client

use runtimed::client::PoolClient;
use runtimed::EnvType;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    // Connect to daemon (finds socket automatically)
    let client = PoolClient::connect(None).await?;
    
    // Ping to verify connection
    client.ping().await?;
    println!("Daemon is alive");
    
    // Get pool statistics
    let stats = client.status().await?;
    println!("UV available: {}", stats.uv_available);
    println!("Conda available: {}", stats.conda_available);
    
    // Acquire a UV environment
    match client.take(EnvType::Uv).await? {
        Some(env) => {
            println!("Got environment: {:?}", env.python_path);
            
            // Use the environment...
            
            // Return it to the pool when done
            client.return_env(env).await?;
        }
        None => {
            println!("No environments available");
        }
    }
    
    Ok(())
}

Example: Notebook Sync Client

use runtimed::notebook_sync_client::NotebookSyncClient;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    // Connect to notebook room
    let client = NotebookSyncClient::connect(
        "/path/to/notebook.ipynb",
        None,  // daemon socket (auto-detect)
    ).await?;
    
    // Update cell source (syncs to all peers)
    client.update_source(
        "550e8400-e29b-41d4-a716-446655440000",
        "print('Hello from daemon')"
    ).await?;
    
    // Listen for changes from other peers
    tokio::spawn(async move {
        loop {
            if let Some(change) = client.recv_changes().await? {
                println!("Notebook updated: {} cells", change.cells.len());
            }
        }
    });
    
    Ok(())
}

Error Handling

Connection Errors

  • Socket not found: Daemon is not running
  • Connection refused: Socket exists but daemon is not accepting connections (crashed)
  • Timeout: Daemon is unresponsive (hung)

Protocol Errors

  • Frame too large: Oversized payload (check MAX_FRAME_SIZE limits)
  • Invalid JSON: Malformed request/response
  • Unknown channel: Invalid handshake channel
  • Unknown frame type: Invalid typed frame byte (notebook sync v2)

Pool Errors

  • Empty pool: No environments available (check with status)
  • Invalid env type: Unknown environment type (must be uv or conda)
  • Creation failed: Environment creation error (check daemon logs)

Build docs developers (and LLMs) love