Remote (Desktop over SSH): reattach replays a bounded output buffer and silently drops the conversation tail — should reconcile from the on-disk transcript

Open 💬 0 comments Opened Jun 24, 2026 by grepfruit

Preflight: searched existing issues — #50982, #58701, #58797, #49790, #26729 are related but distinct (see "How this differs" below); reproduced on the current version.

What's Wrong?

When Claude Desktop is attached to a Claude Code session running on a remote host over SSH, a transient client disconnect (laptop sleep / lid-close / network blip) correctly does not kill the remote session — the claude-ssh daemon keeps the ccd-cli agent running. But on reattach, the tail of the conversation is frequently missing in the Desktop UI — most importantly the final assistant message(s) produced while the client was disconnected — and the "assistant is responding" indicator is not restored. It is intermittent: short turns survive the reconnect, long ones get truncated.

The conversation is not lost: the on-disk transcript on the remote (~/.claude/projects/<proj>/<session>.jsonl) is complete and current. This is purely a reattach re-sync/replay gap in the daemon→client path.

Mechanism (observed via strings on the claude-ssh server binary, package github.com/anthropics/claude-ssh):

  • The agent's stdout is streamed to the client as frames; the daemon keeps a bounded in-memory ring of them (process.bufferedFrame, process.frameSink).
  • On reconnect, handlers.(*ProcessHandler).reattach replays that bounded buffer. The binary contains the string [frameSink] replay write failed, detaching: %v — i.e. replay is written frame-by-frame and aborts on a write failure, dropping everything after that point.
  • There is no fallback to read the complete transcript on reattach, even though the daemon already exposes handlers.(*FilesHandler).read.

So output produced during the disconnect that (a) overflows the bounded buffer or (b) trips a mid-replay frameSink write failure is never delivered to the reattached client. The final message of a turn is the most likely casualty, leaving the user staring at a stale last message with no way to know the turn completed — or that an autonomous session did substantial work / a background task finished — without manually reading the remote .jsonl.

How this differs from existing issues

  • #50982 (OPEN, Windows — loses history after reboot) and #58701 (CLOSED — "No messages yet" after Desktop restart) are full history loss after a restart. This report is partial tail truncation on a live reattach after a transient disconnect, with the session still running.
  • #58797 (CLOSED — --resume empty after bridge restart) is the resume path, not live reattach.
  • #49790 (session should survive disconnect) — the session does survive here; the bug is that the reattach replay is incomplete.
  • #26729 (CLOSED — streaming resilience / in-flight snapshots) — this narrows the problem to the concrete reattach-replay path plus a lower-risk fix (read the transcript that already exists, rather than snapshot in-flight state).

What Should Happen?

On reattach the client should always show the complete conversation tail and correct in-flight/idle state, regardless of how much output was produced while disconnected. Concretely: reconcile the reattaching client from the on-disk transcript (the daemon already has FilesHandler.read) — e.g. fall back to a transcript read whenever the buffered replay is incomplete or the frameSink write fails — instead of trusting a bounded, abortable in-memory replay. Restoring the "responding" indicator when the agent is mid-turn on reattach would also resolve the "is it still working?" ambiguity.

Error Messages/Logs

String compiled into the claude-ssh server binary (the silent failure path; nothing is surfaced to the user):

[frameSink] replay write failed, detaching: %v

Relevant reattach/buffer symbols in the binary:

github.com/anthropics/claude-ssh/internal/handlers.(*ProcessHandler).reattach
*handlers.ReattachParams   *handlers.ReattachResult
*process.bufferedFrame     *[]process.bufferedFrame     *process.frameSink
github.com/anthropics/claude-ssh/internal/handlers.(*FilesHandler).read   (available, but not used as a reattach fallback)

Steps to Reproduce

  1. Run a Claude Code session on a remote Linux host; attach from the Claude Desktop app over SSH.
  2. Have the agent produce a long final message (or a long run of messages / a background task that completes), so a lot of output is generated.
  3. While that output is being produced (or right after), disconnect the client for a while — close the laptop lid / sleep / drop the network (long enough to exceed the buffer; an overnight disconnect reliably does it).
  4. Reconnect. The Desktop conversation is missing the tail (often the final assistant message) and the busy indicator is not restored. Confirm ~/.claude/projects/<proj>/<session>.jsonl on the remote holds the full, correct conversation — proving the data exists and only the reattach render is incomplete.

Is this a regression?

Not sure — appears pre-existing across recent versions.

Claude Code Version

Remote ccd-cli 2.1.187; claude-ssh server daemon build dated 2026-06-24.

Platform

Claude Desktop — remote session over SSH.

Operating System

Client: macOS (Claude Desktop). Remote: Linux.

Terminal/Shell

N/A (Claude Desktop GUI; remote bash).

Additional Information

The remote transcript is the source of truth and is always current, so a reattach that reconciles from it (or falls back to it whenever the buffered replay is short or aborts) fully resolves this without the in-flight-snapshot machinery proposed in #26729. Happy to provide per-run daemon-log excerpts (~/.claude/remote/run/<id>/remote-server.log).

View original on GitHub ↗