← the Atlas

Kaval sessions — dial daemons, multiplex the canvas

Features·budding·accepted·

The R3 branch of remote-terminals — remote-over-ssh. A kaval is a daemon you dial (local unix socket, remote ssh via surface-nix-host), so remote is never a second backend. The spike shipped (R3.1–R3.6): kaval-tui create + --host reach/provision, the warm path deduped and connection-multiplexed. The forward work — composing one surface, dialing remotes, the canvas — now lives in the parent remote-terminals R5–R10 roadmap.

The R3 branch of remote-terminals — remote over ssh. A kaval is a daemon you dial — local over a unix socket, remote over ssh — so remote is not a second backend. One backend, bound to an endpoint; location never complected in. Put a tile on any kaval. Whether the canvas switches one kaval at a time (tmux-style) or multiplexes several side by side is a UI choice on the same substrate — and because kolu is a canvas, it leans multiplex.

kolulocalprodssh+ hostfrom ssh config
build · ~/applocal
▸ vite build — watching…
deploy · prodssh
$ kubectl rollout status…
↳ one canvas, tiles on different kavals — local always present, remotes added on demand

One backend, bound to a dialed endpoint

kolu-server keys its pty-host endpoint by hostId (one key today — local). R3 just instantiates more keys: a registry of kaval endpoints, each reached by a driver — unix socket (local, shipped) or ssh stdio (HostSession, the same closure @kolu/surface-nix-host ships). One backend means one shape, not one host: three surfaces bound to an endpoint — PTY · fs · git — local vs remote only the transport each is dialed over.

At the endpoint local remote — same HostSession link
PTY — kaval (fds + OSC taps) unix socket ssh stdio
awareness — provider DAG + git watcher in kolu-server (in-process) in pulam → mirrored, kolu reads (R8)
Code-tab fs — browse · read · watch node fs (native) a thin remote fs/git surface · snapshot-then-delta

Awareness providers (cwd · title · foreground · command · agent-detection) are tap-fed, so transport-free. kaval stays the durable survivor; the provider DAG is pulam — the host-side awareness daemon, already shipped standalone and decoupled from kolu — dialed and read in by R8–R9. pulam is ephemeral and re-provisioned each dial, so its sensors are always re-derived from the current build (the #1031 “never serve stale detection” lesson, met by re-provisioning, not a kolu-coupled process). kaval itself is dialed directly over ssh via the shipped frontDaemonOverStdio (R3.4) — nothing fronts it. So R3.1’s TerminalEndpoint rename anticipated a second implementation the kaval model dissolves: not two backends, one whose surfaces bind to an endpoint. Adding a kaval is adoption you already shippedadoptOrEnsure adopts a live survivor’s PTYs (else provisions + spawns), now run per endpoint — and multiple local kavals fall out free (kaval is namespaced per kolu-server port, kaval-<port>/, #1313).

Switch or multiplex is a later, reversible UI call the spike doesn’t depend on. The lean is multiplex: switch is a terminal-multiplexer metaphor, but kolu is a canvas — “watch a local build beside a prod-ssh tile” is the point, not a corner case. The cost over switch is modest and clean — per-tile location returns, but additively (no RemoteTerminalBackend, no screen mirror — just which endpoint holds this tile’s PTY), and the ChromeBar shows only the kavals this canvas actually has tiles on.

The spike — kaval-tui dials local and ssh (R3.1–R3.6, shipped)

kaval-tui is the minimal client — it proves the transport before any kolu UI, and because it and kolu-server dial the same socket through the same client, the spike is the production driver. Both halves landed.

shipped The full warm path, dialing a remote kaval:

$ kaval-tui list --host nix@prod
  ↳ ssh nix@prod · nix copy --derivation → realise … kaval up
  ↳ dialing the remote kaval over ssh stdio … connected · 0 terminals
$ kaval-tui create --host nix@prod      → spawned a8f1…  plain shell on prod
$ kaval-tui attach a8f1… --host nix@prod
  ↳ ~/app on prod — remote PTY survives · detach/reattach anytime
Phase What shipped
R3.1 · backend cleanup #1364 collapsed the speculative TerminalBackend/getTerminalBackendFor(location) dispatch into one TerminalEndpoint (renamed, byte-identical) — remote becomes a transport, not a second backend. Its doc-comment is the seam the parent’s R9 fills
R3.2 · kaval-tui create #1370 spawns a plain $SHELL (or create -- htop -d 5) and prints the id — a fresh daemon needs something to attach to. Thin over terminal.spawn (spawn is fully-specified; the caller composes {argv, env, initFiles}, no kolu policy). --json for scripts
R3.3 · kaval-tui --host #1373 reach + provision in one PR (ssh). One getHostSession both provisions (nix copy --derivation → realise → pin) and dials over ssh stdioLink, handing back the same ptyHostSurface client the unix socket speaks. The one daemon-side addition — kaval --stdiofronts the durable daemon (adopt-or-start the host’s kaval, raw-byte-relay the link to its unix socket), so a remote PTY survives the link. ssh-config auto-detect deferred
R3.4 · upstream frontDaemonOverStdio #1374 homed R3.3’s durable-fronting bridge in @kolu/surface-daemon as frontDaemonOverStdio — the durable counterpart to serveOverStdio (“dtach/abduco for any surface daemon, over ssh”): a contract-agnostic relay + adopt-or-spawn, parameterized by socket path + spawn command. kaval’s --stdio became a thin call into it
R3.5 · skip redundant provisioning #1377 a warm host re-ran the full nix copy → realise → pin (copy reports “copying 0 paths”). provisionAgent now tries the cheap realise+pin first (instant when the closure is present, doubles as the presence check), falling through to the full copy only on a miss — keyed on store-realisability, not the dangle-prone GC root. Internal to the shared ensure verb, so drishti gets the warm-skip free
R3.6 · multiplex ssh connections #1378 the warm path still opened 3 separate ssh handshakes (arch probe · provision check · agent dial). One shared connection via ControlMaster=auto/ControlPersist, added as -o flags to the existing SSH_OPT_PAIRS (no ~/.ssh/config touch; a kolu-private %C ControlPath), plus a per-host arch cache. Warm dial → one handshake + near-instant channels

Measured warm path (real ssh, median of 7, the exact opts the code emits). Three round-trips; multiplexing turns 2 of the 3 handshakes into channel reuse, so the saving scales with distance:

host fresh handshake multiplexed channel warm-path ssh: beforeafter
linux pu box (kolu-ci-1, Tailscale) 5.42 s 0.70 s 16.3 s → 6.8 s (2.4×)
darwin (rasam, Tailscale WAN) 2.89 s 0.48 s 8.7 s → 3.9 s (2.25×)

What’s next — in the parent roadmap

Dialing remotes, composing awareness + fs/git, and the multiplex canvas now live as the linear R5–R10 roadmap in the parent remote-terminals note — including pulam growing an fs/git surface (R6). This note is the shipped spike (R3.1–R3.6).

History