← the Atlas

Remote terminals — the finale (R9–R10)

budding·proposed·

R9 (kolu dials remotes) and R10 (canvas) decomposed into landable PRs, now that R8 shipped the awareness bisection. R9a localhost dedup · R9.1 the TerminalEndpoint resolver · R9.2 ssh kaval driver · R9.3 remote awareness via the surface mirror · R9.4 persisted location + reconnect + adoption · R9.5 the Code-tab fs/git rewrite (procedure + pulse) · R10 canvas multiplex. Each phase carries its own needs, done-criterion and test; every reused seam is grounded in code, and the three open decisions are flagged.

The last leg of #951 — kolu dials remote hosts — broken into landable PRs. R9 was one paragraph hiding ~12 workstreams; now that R8 shipped (#1594 — the awareness bisection, the reader-side join, and the serveTerminalWorkspace factory), the remote work reduces to injecting a mirrored awareness backing behind shipped seams plus an ssh driver. This note decomposes R9 the way pty-daemon (R2) and pulam (R4) decomposed theirs; the parent remote-terminals keeps the one-line roadmap rows. Full model: the terminal model.

Roadmap

FINALE — R9 (decomposed) · R10✓ shipped · ▶ do next · ◐ build-clean · ○ todo · → drill in
R9a · localhost dedup — one sensor set (the first PR)R8 ✓ · startable now
R9.1 · the TerminalEndpoint resolver (local-only retrofit)◐ #1603
R9.2 · ssh kaval driver → a remote PTY tileneeds R9.1
R9.3 · remote awareness (mirror the surface)needs R9.2 · R7
R9.4 · persisted location + reconnect + adoptionneeds R9.2
R9.5 · Code-tab fs/git → procedure + pulseneeds R-pulamweb-4 · R9.3
R10 · canvas multiplex + validateneeds R9

Only R9.5 is gated on R-pulamweb-4 (the live git drill-in that proves the procedure + pulse remote leg). R9a needs only the shipped R8, so it is the first landable PR.

What R9 reuses — and what it must build

Shipped seams (reuse as-is):

seam where leg it serves
frontDaemonOverStdio @kolu/surface-daemon dial the remote kaval over ssh stdio — the ssh <host> kaval --stdio relay kaval-tui --host already uses → the PTY
getHostSession @kolu/surface-nix-host a pooled, long-lived session to dial the remote pulam → the mirror
mirrorRemoteSurface @kolu/surface the total dual ({ procedures, done }, R7) — mirror the whole terminal-workspace surface into one local handle
HostLocation remote arm @kolu/common { kind:"remote", hostId } (note: hostId, not host)
persisted location ServerPersistedTerminalFieldsSchema location already round-trips to disk
serveTerminalWorkspace @kolu/terminal-workspace the awareness-backing injection point (R8)

Must build (not in code today):

gap today the phase that builds it
a HostLocation-keyed endpoint resolver localTerminalEndpoint is a singleton imported directly by router.ts/surface.ts/terminals.ts; no location.kind switch exists anywhere R9.1 (a retrofit — route the imports through the resolver)
the ssh driver only the {kind:"local"} arm exists R9.2
live local activity kolu serves quietActivity — no raw byte tap open decision (below)

The sub-phases

R9a — localhost dedup (one sensor set) · the first PR

R9a· localhost dedup — one sensor set○ todo
needs ← R8

The desync users actually see. Under PULAM_WEB_HOSTS=localhost, pulam-web spawns its own pulam, so two sensor sets observe the same terminals and never reconcile — working in pulam-web vs idle in the Dock. Fix: don’t spawn a second pulam; point pulam-web at kolu’s served terminalWorkspace.awareness (cross-process as of R8) instead of its own sensor-fed cache. One sensor, two readers.

R9.1 — the TerminalEndpoint resolver (local-only retrofit)

R9.1· HostLocation-keyed endpoint resolver◐ build-clean
needs ← R8

Today localTerminalEndpoint is a singleton imported directly at router.ts / surface.ts / terminals.ts; there is no location.kind switch. Introduce one resolver keyed on HostLocation — the sole place a terminal maps to its kaval endpoint — and route every direct import through it. Local-only and behavior-preserving (the {kind:"local"} arm is the only arm). This is the retrofit that makes the remote arm purely additive; there is no RemoteTerminalEndpoint.

R9.2 — ssh kaval driver → a remote PTY tile

R9.2· ssh kaval driver — remote PTY○ todo
needs ← R9.1

Add the {kind:"remote",hostId} arm to the resolver: dial the remote host’s kaval over frontDaemonOverStdio (the same ssh <host> kaval --stdio relay kaval-tui --host uses) → a remote PTY on the canvas, PTY only (no awareness badge yet). Persist location = {kind:"remote",hostId}. Adoption reuses the local boot’s adoptOrEnsure spine + the pid-gate’s atomic link(2) (a dial joins a host’s kaval, never races to kill it).

R9.3 — remote awareness (mirror the surface)

R9.3· remote awareness via the mirror○ todo
needs ← R9.2 · R7

Dial the host’s pulam over a long-lived getHostSession, mirror the whole terminal-workspace surface (R7’s total dual) into one local handle, and make kolu-server’s awareness backing resolve remote terminals from that mirror (local ones stay the registry projection) — plus a live activity source off the mirror’s tracker. The remote tile gains the git/agent/PR badge + green dot.

R9.4 — persisted location, reconnect, adoption

R9.4· persisted location + reconnect + adoption○ todo
needs ← R9.2

Harden the remote lifecycle: re-validate the persisted location on reconnect, and cover the adoption/reconcile cases.

the remote kaval is… kolu does
absent provision + spawn fresh (adoptOrEnsure → ensure)
live · wire-compatible adopt — connect, never kill; its PTYs reconcile in
live · a build behind adopt + an “update pending” nudge
live · contract-skewed recycle (kill → respawn) — only on a typed DaemonContractSkewError
unreachable bounded retry, then degraded — never killed

R9.5 — the Code-tab fs/git rewrite (procedure + pulse)

R9.5· Code-tab fs/git → procedure + pulse○ todo
needs ← R-pulamweb-4 · R9.3

The one large client change, and the only finale phase R-pulamweb-4 gates. The Code tab reads koluSurface’s value-bearing streams today (CodeTab.tsxapp.streams.gitStatus/fsListAll/gitDiff.use). Rewrite it to the shared surface’s procedure + {seq} pulse: call the procedure once, re-query on each pulse (the two wire shapes are in surface live data). This deletes the last koluSurface fs/git reads and works for local and remote tiles alike.

R10 — canvas multiplex + validate

R10· canvas multiplex + validate○ todo
needs ← R9.5

The UI: an active-kaval strip (local always present), a per-tile host hint, an ssh-config host picker — kinda like tmux sessions. Then validate the feel on real use: does N-kavals-at-once beat tmux-style one-at-a-time switching? Fall back to switch if multiplex doesn’t earn its keep. The granularity call lands here, validated, not up front.

Open decisions

Resolve each before the phase that needs it — so the implementer isn’t choosing mid-flight.

decision needed by options
rename the join TerminalMetadata → Terminal cleanest before R9.1 (a mechanical pass, ~63 refs) ride it into R9.1, or defer out of the finale (recommended in the terminal model)
where local live activity comes from R9.3 (remote gets it via the mirror; local does not) build kaval’s raw byte tap, or scope local “green dot” out of R9
reconnect re-validation semantics R9.4 what’s compared (location vs the remote kaval’s live cwd) + the fail-fast behavior