fix(remote): load a remote session's scrollback from the remote tmux

A remote SSH session's local pane runs `ssh -t … tmux new-session -A` (or
`attach`), so the local tmux only sees the remote tmux client's full-screen
repaints and keeps a fraction of the history: measured 48 of 279 lines while
output streamed, none after a reattach. The remote pane, where the shell
runs, keeps all of it.

A full-history load (`GET /api/sessions/:id/terminal?full=1`) of a remote
session now reads the scrollback rows from the remote pane in one ssh round
trip (`#{history_size}` + `capture-pane -S -<N> -E -1`, exact `=name:`
target, newest bytes kept by a remote `tail -c`), and the local capture
reads only its visible frame (`-S 0`), spliced below them
(`PaneCaptureOptions.scrollbackOverride`). The local frame and caret are
kept because the live stream keeps drawing into the local pane.

Fail-soft and bounded: connection options from buildSshConnectionArgs(),
the shared remote-ssh limiter (a slot waited for longer than 1.5 s is given
up), a 5 s timeout, and a 30 s per-host back-off after any failure, which
keeps the local capture as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
timkjrandClaude Opus 5.5 committed 2026-10-10 12:49:09 -05:00
1 parent aa3730aace
commit b33d6da58a
7 files changed
+449 -2

No files matched your search

+14
View File
@@ -163,6 +163,7 @@ import {
toAttachedSessionRemote,
toSessionRemote,
} from '../../remote-hosts.js';
import { fetchRemoteScrollback } from '../../remote-pane-history.js';
import {
checkDockerAvailable,
checkDockerConfigDrift,
@@ -3116,6 +3117,19 @@ export function registerSessionRoutes(
maxCaptureBytes: terminalBufferMaxBytes,
}
: {};
// A remote session's local pane only sees the remote tmux client's repaints,
// so its own scrollback is a fraction of the real one: read that from the
// remote tmux and splice it above the local frame. Null (host unreachable,
// no remote session, back-off) keeps the local capture as before.
if (isFullReload && session.remote && muxName && captureOpts.historyLimitLines !== undefined) {
const remoteScrollback = await fetchRemoteScrollback(
session.remote,
session.id,
captureOpts.historyLimitLines,
terminalBufferMaxBytes
);
if (remoteScrollback !== null) captureOpts.scrollbackOverride = remoteScrollback;
}
const liveMuxBuffer =
muxName && typeof ctx.mux.captureActivePaneBuffer === 'function'
? ctx.mux.captureActivePaneBuffer(muxName, captureOpts)