Skip to Content
ConnectRemote sessions

Persistent Remote Sessions

A persistent session is a long-lived shell-like context on a remote machine that survives across many tool calls within one chat turn (and beyond). It is the right model when “one shot exec” is too coarse — when you need to send a sequence of related commands, watch their output as it trickles in, and only close the session when the work is done.

The implementation lives in internal/connect/sessionmgr/ and is exposed to agents as the connect_session tool.

When to use it

The decision tree:

  • One command, finishes in seconds. Use cmdop connect exec.
  • Interactive shell, human at the keyboard. Use interactive-attach.
  • Many commands stitched together by an agent over many turns. Use a persistent session.

Concrete use cases:

  • An agent doing a multi-step build/deploy that wants to read intermediate output before deciding the next step.
  • Tailing a slow log while running diagnostic commands in the same context.
  • Keeping a long-running process (compile, container build, migration) alive across multiple read polls without re-establishing auth.

A persistent session is a sessionmgr object. It is not the same as a terminal session opened by cmdop connect. The former is many commands on one ringbuf; the latter is one PTY the user is interacting with. See ../concepts/sessions for both kinds.

The session model in brief

A persistent session walks a small state machine (opening → ready → busy → closing → closed), captures output in a 1 MiB ring buffer so callers poll instead of stream, and is closed by an idle reaper after 30 minutes of inactivity (64 sessions per process). The formal object model — state transitions, the ring buffer contract, idle TTL, and driver injection — lives on the concept page: Remote sessions. This page stays on the task: when to open one and how to drive it.

The agent tool surface

connect_session exposes five operations on a session:

OperationEffect
openDial a new session on a target machine. Returns a session_id.
sendSend a command. Output goes into the ring buffer.
readPoll output since a given offset. Returns (data, next_cursor, truncated).
listList the caller’s open sessions.
closeClose a session and free its ring buffer.

A typical agent sequence:

{ "tool": "connect_session", "operation": "open", "hostname": "vps-bmw", "idle_ttl_seconds": 0 } // → { "session_id": "sess_3a2b...", "state": "ready" } { "tool": "connect_session", "operation": "send", "session_id": "sess_3a2b...", "command": "make build 2>&1" } { "tool": "connect_session", "operation": "read", "session_id": "sess_3a2b...", "offset": 0 } // → { "data": "...", "next_cursor": 4096, "truncated": false } { "tool": "connect_session", "operation": "read", "session_id": "sess_3a2b...", "offset": 4096 } // → { "data": "...", "next_cursor": 8192, "truncated": false, "exit_code": 0 } { "tool": "connect_session", "operation": "close", "session_id": "sess_3a2b..." }

Reading output without losing data

Output lands in a per-session ring buffer; you read it by offset. Each read returns (data, next_cursor, truncated). A truncated=true means the buffer wrapped and you missed bytes between this read and the last — poll more often, or surface a warning. The polling pattern:

loop: data, cursor, truncated = read(offset=cursor) if truncated: surface a warning ("dropped X bytes") consume(data) offset = cursor

The ring buffer contract (size, monotonic offsets, drop-oldest semantics) is described on the Remote sessions concept page.

Keeping a long task alive

A reaper closes sessions after 30 minutes idle. For tasks that outlive that — long builds, log tails, big migrations — pass idle_ttl_seconds: 0 on open to disable auto-close, then close the session yourself when done:

{ "tool": "connect_session", "operation": "open", "hostname": "prod-api-1", "idle_ttl_seconds": 0 }

Persistent sessions reserve memory (1 MiB ring buffer each by default). The cap is 64 per process; if you fan out heavily across board automations, watch the count and close sessions you no longer need.

Session IDs and concurrency

Session IDs come in two flavors — daemon-issued (host-uuid:slotN) and desktop-minted (machine_<uuid>_<hex>, used by the desktop inspector). A session ID is valid only for the lifetime of the daemon process that holds it; sessions do not survive a daemon restart. Multiple readers can read one session concurrently (each tracks its own offset), but concurrent send is serialized — a send that arrives while the session is busy queues.

Failure modes

SymptomCauseFix
session not found after a long pause.Reaper closed it on idle TTL.Re-open; pass idle_ttl_seconds: 0 next time.
truncated=true and you missed output.Ring buffer wrapped between polls.Poll more often, or accept the gap.
manager full (64 sessions)Too many open sessions.Close idle ones via connect_session.close.
Long build hangs forever.The remote process is genuinely stuck — sessionmgr does not enforce a per-command timeout.Use the agent’s overall turn timeout, or close the session.
Last updated on