Interactive Sessions
Launch a live, attachable terminal session via a template, drive it from web or iOS, and reconnect without losing history.
The PTY session host and the wire protocol that carries keystrokes and screen updates are OSS - documented at donmai.dev/docs/sessions. This page covers the platform's session lifecycle, quotas, and multi-viewer model built on top of that protocol.
Known limitation, as of 2026-08-08. The agent's access to platform MCP tools (memory, knowledge graph, A2A) inside the session runs on a bearer that expires after about an hour and is not refreshed - see MCP Session Tools § known limitation. The PTY/terminal connection itself (what this page documents) is unaffected; only the agent's own platform-tool calls stop working once the bearer expires.
An interactive session is a live, attachable terminal - not a headless agent run you read back afterward. A real command runs under a real pseudo-terminal (PTY) on a worker, and any number of people can watch it live from the web or the iOS app; one of them can drive it at a time. This is the primary way to pair with a running agent, babysit a long build, or hand a terminal to a teammate without a screen-share.
Launching a session
There are two ways to start one:
From the start-session screen (recommended). /{org}/{project}/interactive-sessions/new presents typed selectors for the execution axes, then a Prompt and limits… panel for the free-text fields (initial prompt, max duration). Submitting it calls POST /api/interactive-sessions, which starts the session immediately and hands back a sessionId synchronously.
From a workflow. The interactive-session-authored template wraps the same launch in a manual trigger plus a gate.human_query approval step, so someone signs off on cost and scope before a worker spins up. The interactive.session.launch node behind it takes the same typed selection. A workflow trigger is fire-and-forget and can't return an id inline, which is why the web screen uses the direct route instead.
Choosing the execution axes
Every launch goes through typed selectors, not free text. The screen first calls GET /api/projects/{projectId}/interactive-session-launch-options, which returns the choices you are actually authorized to use on this project: Agent Card, model profile, harness, capacity pool, and repository. Your project and organization defaults arrive pre-selected, so the common case is still one click.
What the typed path buys you:
- Every option is checked before you can pick it. Each choice comes back marked
available,incompatible, orunavailable. Anything not available is shown but not selectable, with the reason attached - "This pool cannot run the selected Agent Card", or "The model profile saved with this Agent Card is no longer available for this project". You find out at selection time instead of after a worker has spun up. - Choices constrain each other as you make them. Harnesses are filtered to those the selected model profile can run, and picking a profile derives its harness for you. Selecting an Agent Card that declares runtime requirements marks every pool that cannot satisfy them as incompatible.
- Every choice is revision-pinned. A selection is a
{ id, revision }reference, not a bare id. If the underlying pool, profile, harness, repository, or Agent Card changes between loading the form and submitting it, the launch fails closed with 409 rather than silently running against something you did not choose. Reload the options and pick again. - Raw pins are rejected. Submitting a bare
poolId,harness,modelProfileId,repositoryId, orsandboxProviderto the launch API returns 400 and asks you to refresh the launch options and submit pinnedexecutionSelectionreferences instead.
Agent Cards are optional, and a definition-only card does not restrict your pools. Many Agent Cards are a role and a set of instructions with no runtime requirements of their own. Selecting one adds its role and prompt to the session and leaves the model profile, harness, and pool as the authoritative execution cell - exactly as they are for ordinary agent dispatch. Only a card that actually declares runtime requirements narrows the pool list.
Legacy API clients may omit executionSelection entirely and fall back to the project and organization default cascade. That path is unpinned, so it cannot give you the staleness guarantee above.
The initial prompt
The initial prompt is delivered. Whatever you type in Prompt and limits… rides the launch through to the worker, and the runner hands it to the agent as the session's first input - you do not have to attach and re-type it.
- Blank is the same as omitted. An empty or whitespace-only prompt normalizes to "no prompt" and launches a plain session. It does not fail the launch, and from a workflow it no longer produces a failed run.
- There is a size limit and it does not truncate. The prompt is trimmed and then measured at 1,023 UTF-8 bytes. Over that, the launch is rejected with the measured size so you can shorten it yourself - the platform will not quietly cut your prompt in half. Note this is a byte limit, so accented characters and emoji cost more than one character each.
- Old workers degrade cleanly. A worker on a runner build that predates prompt delivery ignores the field and starts an ordinary session; nothing errors.
Launches are gated by two feature flags (interactive_sessions entitlement + a route flag), both default-deny per organization. If you don't see interactive sessions in your project, ask an admin to enable the feature - see Quotas & Plans for what plans include it.
Attaching
Open a session from the session list or from a direct link:
- Web -
/{org}/{project}/interactive-sessions/[id]renders the terminal with xterm.js and a WebGL renderer. It shows a presence strip, a quota clock, and a stop button alongside the terminal. - iOS - the iOS app attaches with a native Metal-rendered terminal and Network.framework transport.
Both clients authenticate with a short-lived per-session token (minted via POST /api/interactive-sessions/{id}/tokens) and speak the same wire protocol, so a web viewer and an iOS viewer see byte-identical output.
Presence and who drives
Everyone attached sees the live terminal. Exactly one person - the driver - can type into it. The rest are viewers.
- Any user with a driver-capable grant can take the pen at any time: no approval round-trip, just a grab. The person who was driving loses the pen, sees a toast, and drops to viewer.
- Taking the pen is audit-logged and every input event is stamped with the user who sent it - if two people are debugging together, the activity trail shows who typed what.
- A short cooldown after a grab prevents a rapid tug-of-war between two people reaching for the pen at once.
- After a reconnect, stale input from a superseded connection is dropped server-side (an internal generation counter on the pen grant), so a flaky network doesn't let an old, disconnected client's buffered keystrokes land after someone else has taken over.
The presence strip on both web and iOS shows who is watching and who is driving.
Stopping a session is broader than driving it: any project member with access to the session can stop it, not only the current driver. If you can see a session, you can stop it.
Reconnecting
Interactive sessions are designed to survive flaky networks without losing the session:
- The web client dials a WebSocket first. On repeated dial failures (three failed attempts within 30 seconds) or a failed subprotocol handshake, it automatically falls back to a degraded transport (SSE for output, plain POST for input) rather than disconnecting entirely. There's no automatic upgrade back to the WebSocket once degraded in this version - a manual reload retries the fast path.
- The iOS client uses a dead-peer watchdog: it pings every 10 seconds and tears down and reconnects if it hears nothing back for 20. A network path change (Wi-Fi to cellular, for example) also triggers an immediate reconnect.
- On reconnect, the client either replays the missed frames from the server's ring buffer (if the gap is small) or gets a fresh full-screen snapshot repainted (if it isn't, or if it just missed too much). Either way you land back on the correct, current screen state - not a stale one.
- Backgrounding the iOS app does not keep a socket open. After roughly 15 seconds of background grace, the app cleanly detaches both legs of the connection; the session itself keeps running on the server. Reopening the app re-attaches - minting a fresh token only if the old one expired - and replays or repaints to catch up. This is deliberate: iOS does not grant this class of app a persistent background socket.
Ending a session
A session ends when the command exits, when someone stops it (from the terminal's stop button, rensei tooling, or the admin console), or when it hits its duration budget - see Quotas & Plans for the warning/grace/stop sequence. The terminal shows the final screen and an exit chip once the session ends, so you can read the last output before navigating away.
Recordings
An interactive session's PTY stream can be recorded as an asciinema v2 cast. Whether a given session is recorded follows its recording_policy (org_default | opt_out | retained, stored per session): opt_out never records, retained always records, and org_default - what every session launches with - follows the org's interactive_recording entitlement flag, which is off unless an org has turned it on. opt_out/retained are set later, by a relay callback; there's no launch-time or admin control to pick a policy. The relay writes the finished cast to private blob storage when the session's room tears down, so nothing exists to watch until the session ends.
Watching a recording
Once a session ends, the live page (/{org}/{project}/interactive-sessions/{id}) redirects to /{org}/{project}/interactive-sessions/{id}/recording for any terminal session that has a recording. That page replays the cast into the same xterm renderer the live view uses, with a transport strip underneath it: play/pause (it relabels to Replay once the cast ends), a mm:ss / mm:ss playhead, a seek slider, and rate buttons for 0.5x, 1x, 1.5x, 2x, and 4x. A side panel repeats the session's status, started/ended times, pool, harness, model, sandbox, repo, and session id, plus an "Open activity inspector" link into the Session Inspector and a "Back to sessions" link.
Before the cast is ready, the page shows a banner instead of the player: loading, still-live ("the recording is written when it ends"), none (with a distinct "Recording was turned off for this session" message when the policy was opt_out), or a retryable error. GET /api/interactive-sessions/{id}/recording streams the cast to back the player - it proxies the bytes through the platform and never hands the client the private blob's own URL - authorized the same way as the session-detail route (a real signed-in user, not an API key or OAuth-agent token). It answers 409 while the session is still live and 404 once the session is terminal with nothing recorded.
From the CLI
The CLI does not play the cast back. rensei session tail <session-id> prints a rendered text projection of the terminal's last N lines (--lines); while the session is live it reads the relay's retained snapshot and frames, and once the session has ended it reads the same recording. --raw (the exact encoded frames, unrendered) only works while the session is live. rensei session replay <session-id> is a different command over a different artifact: it replays the platform's session-ledger evidence channel (dispatch/watch/cancel lifecycle events), not the terminal recording. Run against an interactive session, it answers 404 with reason recording_not_replayable and points you at session tail instead.
Policy and retention
Operators can see a session's recording policy and its recording pointer, if one exists, on the session's detail view in the admin console. Recordings are retained under a per-plan, org-overridable window (recordingRetentionDays); an org that hasn't set one falls back to a 365-day system default, and a scheduled job deletes the cast and clears its pointer once a recording ages past that window.
A durable evidence store that checks recordings in for long-term retention and lets you learn over them, beyond played-back review, is on the roadmap and not shipped yet.
Using it from a workflow
Beyond the seeded template, three workflow nodes let you compose interactive sessions into your own automation:
| Node | Does |
|---|---|
interactive.session.launch | Starts a session from a workflow. Its config panel shows the same typed selectors as the start-session screen, but pinning is optional here: an axis you leave alone stays marked inherited and resolves from project defaults at run time, so one workflow works across projects |
interactive.session.inject | Sends text into a running session's input stream (the same chokepoint the terminal itself uses) |
interactive.session.under_quota | A condition node - branch on whether the org still has room under its interactive-session quota before launching |
A common pattern: an event trigger (a Linear comment, a GitHub issue, a Sentry alert) runs into a gate.human_query, and approving the gate launches a live session with the approver as the initial driver - "escalate this to a live session" as a one-click action from wherever the event already lives.
Manual triggers you build by hand in the visual editor can declare their own typed parameters. Select the trigger node and add parameter rows (name, type, required, description, allowed values, and optionally "use project default" for the capacity-pool, model-profile, and repository axes); the editor serializes them to the trigger's parameter schema and it persists with the workflow. At fire time the trigger route validates the caller's params against that schema and rejects a bad payload with 422 and per-field errors. Two caveats: a trigger with no parameter rows keeps the old behavior and accepts arbitrary params, and so does a workflow where the firing trigger is ambiguous because more than one could have fired.
Related Pages
- Notification Preferences - get pinged when a session ends or nears its duration limit
- Quotas & Plans - concurrency, duration, and monthly-hour limits by plan
- Interactive Sessions Admin - operator view: force-stop, grants, relay health
- iOS App - attach and drive from your phone or iPad
- Session Inspector - full tool-call history once a session has agent activity to inspect