← the Atlas

Remote terminals over SSH

Features·budding·accepted·

The portal for kolu#951 — remote terminals over SSH. This is the root of the whole plan tree — every inner note (the padi plan of record, the surface consolidation plan, the framework primers, the shipped branches' build logs, the design records, the future menu) is reachable from the map here. Status — R1–R8 shipped; the R9/R10 leg was re-architected from the ground up as padi (one workspace daemon per host owns the complete terminal state; the canvas single-host per view), and on the padi side W0–W4 and W6–W8 are shipped — kolu on master runs the multi-host switch today, with an honest connect overlay — with W5 (cross-host attention) next and the surface consolidation running in parallel. The superseded pre-padi R9/R10 decomposition (the finale) is recorded in a section here.

The portal for #951 — remote terminals over SSH. Every host — this machine and each ssh remote — runs the same daemon stack, and you switch hosts from a picker, kinda like tmux sessions. R1–R8 are shipped — the foundation, the local kaval survivor, the remote-ssh spike, the standalone pulam awareness daemon, the overflow-recovery loop, @kolu/terminal-workspace, the total-dual surface mirror, and kolu serving the shared awareness raw. The R9/R10 leg was re-architected as padi — one workspace daemon per host, the canvas single-host per view — and that plan is now mostly shipped: W0–W4 and W6–W8 are live (kolu on master runs the multi-host switch today, honest connect overlay included — #1730), W5 (cross-host attention) is next, and the surface consolidation runs in parallel. This note is the root of the plan tree: the map below reaches every inner note, and the finale record holds the superseded pre-padi R9/R10 decomposition.

The map — navigating this tree

Every plan note in the remote-terminals tree, reachable from this root:

Current plan

Framework primers

Build logs of shipped branches

Design records

Future / demand-driven

User-facing description

kolu
hostlocalnix@prodstaging+ host⌥H switch · one host per canvas
nix@prodssh
kolu · feat/dial-ssh #1412 ✓
claudeawaiting you
nix@prodssh
infra · deploy
working
nix@prodssh
kolu · master
codexworking
nix@prodssh
notes · main
claudeidle

You switch hosts from a pickerkinda like tmux sessions — and the canvas shows one host’s terminals at a time (the shipped shape; the mockup above predates padi’s single-host ruling). A remote terminal carries the same live awareness a local one does — git branch/dirty, PR status, agent state, live activity — and it survives a network blip or a kolu-server deploy with full scrollback. One local host is always present; remotes are added on demand from your ssh config.

Architecture-level changes

The rest of this section records the pre-padi architecture (the R1–R8 era) — kept as the historical record; the current architecture is padi.

Remote terminals — the multi-host shape one kaval daemon per host · kolu-server keys every endpoint by hostId · colour = shipped vs remaining shipped — R1 · R2 · R6 remaining — R5 · R8 · R9 browser — SolidJS client ChromeBar host switcher WebSocket · oRPC kolu-server — restarts every deploy TerminalEndpoint seam · keyed by hostId · awareness sensors (fresh each deploy) R2 ✓ local driver spawn · supervise · adopt localDriver.ts R9 ssh driver reach · provision · multiplex surface-nix-host R9 the mirror mirror pulam's surface awareness + fs/git HOSTS — same hashed kaval daemon on every one this machine kaval ✓ durable PTY · holds the fds + screen mirror survives a kolu-server deploy awareness + fs/git: in-process here (the same terminal-workspace surface pulam hosts) remote ssh host ×N kaval same daemon PTYs survive blips + deploys pulam awareness + fs/git (R6) served / ssh unix socket ssh stdio ssh · mirror
The pre-padi multi-host shape (historical). Every host runs the same hashed kaval daemon; kolu-server keys its terminal endpoint by hostId behind one TerminalEndpoint seam. The local driver (R2, shipped) spawns + supervises a kaval over a unix socket; the ssh driver (R9) reaches + provisions the same kaval closure over ssh stdio; the remote mirror (R9) reaches a host-side pulam's awareness (kolu serves the shared awareness collection raw as of R8, joined with the authored half at the reader) + fs/git (read in R9). Local awareness runs in-process; only remote crosses a process boundary.

One endpoint, bound to a dialed kaval. A kaval is a daemon you dial — local over a unix socket, remote over ssh — so remote is not a second backend. There is one TerminalEndpoint per terminal, resolved by HostLocation ({kind:"local"} today, {kind:"remote",hostId} in R9). That resolver is meant to be the sole place a tile maps to its kaval; everything downstream talks to the backend and never asks “which kind?”. No RemoteTerminalEndpoint — and the resolver shipped in R9.1 (resolveTerminalEndpoint, #1603): per-terminal ops already resolve off entry.meta.location and never ask “which kind?”, with the {kind:"remote"} arm failing loud until R9.2 adds the ssh driver as an additive sibling of R2’s local driver.

Two volatility axes stay open (R1/R2 closed the rest):

Axis — changes for its own reason Encapsulated by
Where a terminal’s state lives — this machine vs an ssh host the HostLocation-resolved kaval endpoint — one impl + a discriminator, not a second backend
How the backend reaches its agent — transport · framing · reconnect HostSession + @kolu/surface/links/stdio — unix socket today, ssh in R9

Zero domain knowledge crosses the transport (Hickey’s cut): the agent runs the unmodified sensors and the boundary ships their values. An earlier per-domain design (RemoteGitInfoProvider, …) was transport adapters wearing domain costumes; the single-endpoint seam dissolved them.

Already paid for, so R5–R10 are narrow. @kolu/surface-nix-host ships HostSession, provisionAgent (ships the derivation — a darwin parent drives a linux remote with no cross-builder), and resolveSystem; @kolu/surface ships mirrorRemoteSurface (now a total dual). R2 already shipped the host-count-agnostic spine: @kolu/surface-daemon-supervisor and the per-connect system.version handshake. So R9 is an ssh driver behind shipped seams; R8 made kolu serve the shared awareness (#1594); R7 completed the surface mirror (#1505); R6 grew pulam’s fs/git (#1506); R5 added the one wire signal that distinguishes a slow-subscriber drop from a PTY exit (#1591).

Implementation details

The whole epic as one tree — bird’s-eye, with each row’s right-hand link a drill-down to where that phase lives (the child notes for shipped branches, the ### sections below for the rest). R1–R8 (incl. pulam-web’s framework) are shipped; the R9/R10 leg re-architected as padi — W0–W2.2 shipped there.

ROADMAP — remote terminals over ssh (#951)✓ shipped · ▶ do next · ◐ build-clean · ○ todo · → drill in
R1 · Foundation — seam · framework · engine#981·984·1004
R2 · kaval — local PTY survival→ pty-daemon
R2.1–R2.5 · inversion → door → survival → inventory#1292…1458
R3 · remote-ssh spike→ kaval-sessions
R3.1–R3.6 · kaval-tui dials local + ssh#1364…1378
R4 · pulam — the workspace daemon→ pulam
R4.1–R4.5 · sensors → daemon → --host → fleet#1413…1497
R4.6 · rename arivu → pulam#1512
R4.7 · live git status in pulam-tui#1519
R4.8 · pulam-web — the browser twin (retired at padi W0)→ pulam-web
R-pulamweb-1 · drishti reactive consumerdrishti#72
R-pulamweb-2 · framework — provision · fan-out · list#1524
R-pulamweb-3 · agent dashboard — agents by state#1535
R-pulamweb-4 · git status drill-in — never proceeded (pulam-web retired)superseded by padi
R5 · Overflow-recovery loop — typed overflow frame, web tier re-attaches#1591
R-dock-unify · kolu's Dock reads the shared agentProjection (consistency)#1541
R-pip-unify · one status pip — Dock + dashboard render the same icon#1551
R-activity-merge · merge the activity dot into the state pip, one shared indicator#1555
R6 · terminal-workspace — one fs/git impl, two homes#1506
R7 · mirrorRemoteSurface → total dual#1505
R8 · kolu serves awareness raw; client joins authored ⋈ awareness#1594
S1 · S2 · R9.0 · R9.1 · the awareness foundation + the endpoint resolver#1626 · #1603
R9 · R10 → re-architected as padi — W0–W4 · W6–W8 SHIPPED (the multi-host switch is live); W5 next→ padi

Everything marked ✓ is shipped (PRs linked). R8 (#1594) ended kolu’s private awareness copy: a single-writer store served raw, kolu keeping only kolu.authored, the client joining the halves at the reader. The S1/S2 awareness foundation + the endpoint resolver shipped next (#1626, #1603) — and the rest of the R9/R10 leg was re-architected wholesale as padi: its W1 rewired the client onto one complete surface, its W2 made padi the per-host process that owns the domain, its W3/W4 landed the remote binding and the host switch. The web-UI indicator thread — R-dock-unify (#1541) · R-pip-unify (#1551) · R-activity-merge (#1555) — single-sourced the per-row indicators, so the Dock and the dashboard render one indicator from one projection. Each ✓ row drills into its detail — the child notes for shipped branches, the ### sections below for the rest.

R5 — overflow-recovery loop

R5· overflow-recovery loop✓ shipped
links #1591

Independent — needed nothing, blocked nothing; landed on its own.

Shipped (#1591). kaval sheds a slow attach subscriber by ending its iterator, which was indistinguishable on the wire from a PTY exit — so the client treated the drop as terminal and froze scrollback. The attach contract (ptyHostSurface, bumped to 5.0 — a new emitted union variant is breaking for an older client, which the version predicate would otherwise wave through) now carries a typed overflow control frame, emitted as the stream’s last frame when the host drops a lagging subscriber, distinct from a PTY exit. The web tier (terminalEndpoint/local.ts) reads it and re-attaches for a fresh snapshot — reset-then-snapshot so the repaint replaces stale rows rather than double-painting — instead of freezing; kaval-tui’s existing re-attach loop reads it to skip writing a dataless frame; pulam ignores it (activity is best-effort). The drop is reproducible on the local socket, so it is CI-tested today (inProcessPtyHost.test.ts, reattachingDeltas.test.ts), and the hardened contract is a clean prerequisite for the remote reconnect R9 leans on. herdr vs kolu flagged the same gap.

R-dock-unify — kolu’s Dock reads the shared agentProjection

R-dock-unify· Dock + dashboard, one source✓ shipped
needs ← R-pulamweb-3links born in #1535 · #1541

Shipped (#1541) — kolu’s Dock became the third consumer of the shared @kolu/terminal-workspace/agentProjection, so the Dock, pulam-tui, and pulam-web rank/paint agents from one source; a differential test pins them equal.

R-pip-unify — one status pip across the web surfaces

R-pip-unify· Dock + dashboard render the same icon✓ shipped
needs ← R-dock-unifylinks #1551

Shipped (#1551) — the Dock’s StatePip lifted into @kolu/solid-statepip + a shared @kolu/theme, so the Dock and pulam-web render the identical status pip from one component.

R-activity-merge — merge the activity dot into the state pip

R-activity-merge· merge the activity dot into the state pip✓ shipped
needs ← R-pip-unifylinks #1555

Shipped (#1555) — the live-activity dot and the unread alert merged into that same StatePip (a sweeping green ring + an amber corner badge), so each row renders one indicator from one leaf.

R6 — @kolu/terminal-workspace: one library, one fs/git impl, two homes

R6· terminal-workspace — one fs/git impl, two homes✓ shipped
needs ← R4blocks → R8links #1506

Shipped (#1506) — @kolu/terminal-workspace: one fs/git impl (createTerminalWorkspaceEndpoint), run in-process by kolu and hosted remotely by pulam. The shared impl, not yet the shared surface — the shared awareness surface is R8; the fs/git surface is R9.

R7 — mirrorRemoteSurface is now a total dual — shipped #1505

R7· mirrorRemoteSurface → total dual✓ shipped
blocks → R9links #1505

Shipped (#1505) — mirrorRemoteSurface became a total dual returning { procedures, done } (every procedure a forwarding stub), so serve ∘ mirror ≈ identity; proven by drishti’s forwarded “Kill process” action under the surface.md gate.

R8 — kolu serves awareness raw; the client joins it — shipped #1594

R8· kolu serves awareness raw; client joins authored ⋈ awareness✓ shipped
needs ← R6blocks → R9 · R9alinks #1594

Shipped (#1594). kolu used to fuse awareness into its own private terminalMetadata record. R8 bisected it: the sensor fields live in a single-writer awareness store served raw on terminalWorkspaceSurface.awareness (AwarenessValue); kolu keeps only what it authors on kolu.authored (AuthoredTerminal — location · chrome · the active|sleeping discriminant). Neither side fuses themsurfaceCtx.collections.terminalMetadata is a compile error. The client joins the two halves at the reader (useTerminalMetadatacomposeTerminalMetadata), and the same join authors the on-disk SavedTerminal, so disk and the read can’t diverge. One writer per fact, and the bisection reaches the consumer. Full model: the terminal model. (The awareness half was later reshaped by the awareness-derive-store cutover: the single-writer store became kolu’s fold over a memoryless producer’s observation stream, and the collection value AwarenessValue became TerminalSnapshot — the reader-join itself is unchanged.)

One surface, two homes. kolu-server and the pulam daemon both serve terminalWorkspaceSurface, but assemble it via one factory@kolu/terminal-workspace/serveTerminalWorkspace, the volatility-boundary twin of serveFsGit: the version cell + fs/git procedures/streams live there once, and each home injects only its awareness backing (kolu projects off its registry; pulam reads its own store) and its activity source (quiet for kolu, live for pulam). So R8 unblocks R9a — kolu now serves an awareness a second process can read — and makes remote awareness in R9 a backing injection, not a rewrite.

fs/git is not part of R8 — it rides R9. terminalWorkspaceSurface deliberately serves fs/git as procedures + pulse, not value-bearing streams: fs.listAll/git.getStatus are request→response procedures, and subscribeRepoChange/subscribeFileChange are payload-free {seq} pulses you re-query on (surface.ts:89-109“re-queries procedures rather than streaming full diffs over the wire”). kolu’s Code tab today reads koluSurface’s value-bearing streams (app.streams.gitStatus/fsListAll/gitDiff.use, CodeTab.tsx:314-373). Making kolu read the shared surface’s fs/git means rewriting the Code tab from value-bearing streams to procedure + pulse-then-requery — a large client change the surface ties to mirroring the surface whole. So it lands with R9, where kolu mirrors the whole surface anyway.

R9–R10 — kolu dials remotes, then the canvas

R9· kolu dials remotes → re-architected as padi○ todo
needs ← R7 · R8

Superseded — this leg became the padi architecture, after the ground-up re-evaluation recorded in History: “which host” is a property of the connection, not of each terminal, so instead of threading a per-terminal HostLocation through kolu-server, one workspace daemon per host (padi) owns the complete terminal state and the canvas shows one host at a time. What the old decomposition planned maps as: R9.2 (ssh driver) + R9.3 (remote awareness) + R9.4 (reconnect/adoption) → padi W3.1 (the remote binding, shipped); R9.5 (Code-tab rewrite) → shipped inside padi W1 (un-gated from R-pulamweb-4); R10 (canvas) → padi W4/W5 (the host switch — shipped — + cross-host attention, single-host per view; the multiplex-vs-switch question was settled by a persona UX study, switch won). Shipped on the padi side: W0–W4 and W6–W8 — kolu on master runs the multi-host switch today; W5 next. The finale record below holds the superseded R9/R10 decomposition; archived notes holds the consolidation plan (shipped).

The finale record (R9 converged · R10) — superseded

The pre-padi plan of record for this leg. Its foundations shipped and live on: S1 + S2 + R9.0 + R9.1 (#1626, #1603) — the memoryless awareness producer, kolu’s fold, the in-process local awareness, and the HostLocation endpoint resolver — all reused inside padi. The awareness types and API are recorded in awareness-derive-store.

The re-sequenced remainder — three parallel local-preserving prep PRs (PR-1 lifecycle · PR-2 fs/git · PR-3 awareness) → F-REMOTE (the one complete remote tile) → R10 (canvas + host picker) — never proceeded: the whole decomposition was superseded by padi on 2026-07-01 (its per-terminal HostLocation threading was N-site handling of a connection-level volatility), and its prep PRs #1637–#1640 were closed. The W-phase mapping is in the section above; the detailed sub-phase specs live in git history.

History