Publish an agent session live
How runners stream an agent session to orboto through a harness adapter, how a headless login is handed to an operator, and the relay contract for your own adapter.
Added in v0.210.0 A runner that starts an agent also publishes the agent's session to orboto, so operators can watch and steer it from the browser, the phone or the terminal. This page is for whoever runs agents: which runners publish, how each agent tool (the harness: Claude Code, Codex, Pi, or any terminal program) is adapted, how a login reaches an operator, and the contract a custom adapter speaks.
What publishes on its own
Every session a runner starts registers itself and streams its transcript:
orboto pi-runner, local lanes and--remotelanes (see Run an agent fleet),orboto agent-runnerwith pi, Codex, Claude Code and protocol lanes,orboto codex-runner.
Nothing to configure: the runner uses the credential it already has and connects
out over the agent WebSocket, so the machine opens no port. Set
ORBOTO_LIVE_SESSION=off to switch the stream off for one runner. An instance
that does not offer live sessions refuses the registration; the runner logs this
once and keeps working without a stream.
orboto claude, the interactive Claude Code session a person starts, is not
runner-owned and publishes nothing by itself. Run it inside tmux and publish
the pane with orboto live-session pty (below).
Publish a session yourself
For a session no runner owns, start the stream with orboto live-session:
orboto live-session run --harness claude_code -- claude --name "review" # Claude Code through its stream-json mode
orboto live-session run --harness codex # codex app-server over stdio
orboto live-session run --harness pi # pi --mode rpc
orboto live-session pty --tmux agent # any program in a tmux pane
orboto live-session login --harness claude_code # only a headless login, handed to an operatorrun starts the harness and streams it through its adapter. Without a command
after --, it starts claude, codex app-server --listen stdio:// or
pi --mode rpc; for Claude Code it adds the stream-json flags the adapter needs
when your command lacks them. The session's name is the --name of the command,
else --label, else the harness and host.
pty streams a tmux pane: its output as text, operator messages and keystrokes
typed into it. It pipes the pane into a file under ~/.orboto/live/ unless
--log names a file the pane already pipes into (an existing pipe stays). The
session ends when the pane is gone.
Common flags for all three:
| Flag | Effect |
|---|---|
--label <text> | How operators see the session. |
--project <KEY> | The project the session works in; decides who may watch it. |
--ticket <KEY> | The ticket the session works on; sets the project too. |
--external-id <id> | A stable id: a restarted stream resumes the same session and its transcript. |
How each harness is adapted
| Harness | Transcript | Questions | Operator messages | Login |
|---|---|---|---|---|
| Claude Code | --output-format stream-json | tool approvals (--permission-prompt-tool stdio), AskUserQuestion, usage limits | --input-format stream-json; in a lane that runs one turn at a time, a message waits for the next turn | headless browser login, see below |
| Codex | app-server notifications | command and file approvals, request_user_input, usage limits | steered into a running turn, otherwise the next turn | device code |
| Pi | --mode rpc events | extension select, confirm and input prompts, provider limits | a prompt, or a steering message during a turn | none in RPC mode |
| Any other program | the tmux pane | numbered menus after a question line, limit notices, Claude Code and Codex login prompts | typed into the pane; raw keystrokes | the login URL printed in the pane |
What the Remote console shows for each harness today is listed there.
A session that no adapter covers, such as the Gemini CLI, runs through the
terminal fallback (live-session pty). Added in v0.213.0 orboto codex and orboto gemini stream their terminal the same way when you are logged in to orboto, with no transcript; Added in v0.226.2 orboto kimi and orboto agy stream the terminal and a structured transcript; what each launcher provides is on Launchers and the uniform flags.
The login question
An agent on a headless machine cannot open a browser to sign in. The adapter starts the harness's login so that it opens nothing locally, captures the login link, and sends it to the operators as a login question. The operator signs in on their own device and sends back what the harness needs:
- Claude Code:
/logintyped into the session (ororboto live-session login --harness claude_code) runsclaude auth loginwithBROWSER=false. Claude Code then prints a login URL that redirects to Anthropic's code page, not tolocalhost, so it works from a phone. The operator signs in, copies the code the page shows and sends it; the adapter types it into Claude Code'sPaste code here if promptedprompt. A login URL that points anywhere else (localhost, an IP address, plain http) is refused. - Codex:
/logininside an app-server session starts the device-code login (standalone:orboto live-session login --harness codex, which runscodex login --device-auth). The operator gets the URL, the one-time code and its expiry, enters the code on that page and confirms. - Pi: no login command in RPC mode;
/loginis refused with the reason. Give the lane an API key, or sign in inside pi's terminal published withlive-session pty. - Terminal fallback: a Claude Code or Codex login printed in the pane becomes the same question, and the code is typed into the pane.
A session has at most one open login question: a new login prompt withdraws the older one. The code is masked in every later line.
What the runner masks before sending
Before a line leaves the machine, the runner replaces what it knows is secret:
the CLI's own token, environment values whose name reads like a credential
(TOKEN, SECRET, PASSWORD, API_KEY and similar, at least eight
characters), and every login code an operator sent, also when a terminal wrapped
the value across two lines. Every value a tool call types into a web page through
the orboto browser (orboto browser type and select,
browser_type, browser_fill_form, browser_select_option, orboto_act)
becomes ***** in that call and in every later line. The runner cannot know at
that moment whether the field was a password, so it masks every typed value.
The instance then runs its own secret detector over each stored line.
Delivery guarantees
- An input the relay delivers twice after a reconnect reaches the harness once; the second delivery gets the first answer.
- A Pi prompt is confirmed only after Pi accepted it; otherwise it is reported as refused, with Pi's reason or after 30 seconds.
- Output the runner could not keep up with, or lines lost while the connection was away, show as a status line with the count, and the next line is marked truncated.
- Switching the agent to another login profile is accepted only by a runner that has login profiles configured, and refused with the reason otherwise.
The relay contract
Write your own adapter against the same contract the built-in ones use. All
routes use the agent's normal API key; discover their request and response
schemas with orboto api-search "live session" (see the
API overview).
Runner side:
POST /agents/live-sessionsregisters the session withharness(claude_code,codex,pi,gemini,ptyorother),label,hostand optionallyprojectKeyorticketKey,laneId,model,externalId(the same id and account resume the session and its numbering) andinputKinds(what the adapter accepts:text,answer,keystroke,switch_account, Added in v0.230.0set_limit_policy). Send thex-orboto-agent-sessionheader of your process. The answer carries the session id and, once, a relay token.- Get a socket ticket with
POST /auth/ws-ticket, open/agent/ws?ticket=...and sendlive_attach {sessionId, token}. orboto answerslive_attached {lastSeq}, the current takeover, and every input still waiting. - Send one
live_event {sessionId, seq, event}per transcript line,seqcounting up from 1; after a reconnect resend everything afterlastSeq(repeated numbers are ignored). Event kinds:text,tool_call,tool_result,question,permission_prompt,limit_prompt,login(an httpsurl, optionaldeviceCodeandexpiresAt) andstatus. A line may mark secret ranges of its text withsecretSpans. One frame stays under 60 KiB. - Answer every
live_input {inputId, input, actor}withlive_input_ack {inputId, status}(delivered,rejectedorunsupported, optionalreason). Report a question answered at the terminal or withdrawn withlive_question_resolved. PATCH /agents/live-sessions/:id/heartbeatevery 30 seconds (two minutes without one read as lost) andPATCH /agents/live-sessions/:id/endedat the end: the takeover ends, open questions are withdrawn and waiting inputs expire.
Added in v0.230.0 The socket ticket keeps the scopes of the API key that
requested it, so a runner on a scoped key follows tickets, comments and live
sessions inside those scopes. When the key is revoked, rotated, rescoped or
given a new expiry date, when it expires, or when its account is deactivated or
deleted, the socket closes with code 4401. Request a new ticket with the key,
attach again with live_attach and resend everything after lastSeq. A
401 on the ticket request means the key or the account is gone: stop and
mint a new key. See Live connections.
Operator side:
| Route | Purpose |
|---|---|
GET /agents/live-sessions | Sessions you may see, newest first, cursor-paginated (state, projectId, laneId filters) |
GET /agents/live-sessions/:id | One session with its open questions and takeover |
GET /agents/live-sessions/:id/transcript | Transcript pages (cursor, order, afterSeq) |
POST /agents/live-sessions/:id/questions/:questionId/answer | Answer with optionIds and/or text |
POST /agents/live-sessions/:id/input | A message (text), keystrokes (keystroke), switch_account or Added in v0.230.0 set_limit_policy |
POST /agents/live-sessions/:id/lease | Take over, or renew your takeover |
POST /agents/live-sessions/:id/lease/request | Ask the holder to hand over |
DELETE /agents/live-sessions/:id/lease | Give back |
POST /agents/live-sessions/:id/lease/revoke | End someone else's takeover (admin:agents:write) |
Added in v0.230.0 A runner that rotates login profiles can publish what it does at a usage limit: limitPolicy on the registration and on a heartbeat (mode off, ask, stop or switch, everyProfile, profiles, threshold, strategy, resume, active, source, changedAt, changedBy), which GET /agents/live-sessions/:id returns as the session's current policy. It lists set_limit_policy in inputKinds and takes {"kind":"set_limit_policy","policy":{"mode":"switch","profiles":["work","second"],"threshold":85}} to change the policy without a restart; a switch order may name only profiles the session published (400 otherwise), threshold is 1 to 100, and resume belongs to ask and stop. The input follows the permission rules and the takeover of switch_account, is audited as agent.live.input with the new and the previous policy, and the runner answers it with live_input_ack as it does every input. The full description is in Switching on and off.
A second operator who tries to take over gets 409 naming the holder. A
terminal client can follow a session by subscribing to
orboto://live-session/<id> on the agent WebSocket; it is woken on every change
and reads the new lines with afterSeq. There are no named MCP tools for live
sessions; MCP clients reach every route through orboto_api_search and
orboto_api_call.
Build capacity
Claim, queue, hold and book shared build hosts, CI runners and Unity machines through orboto instead of asking in a message thread: resources, claims, windows, reservations, the CI step, the admin page and the metrics.
Update Claude Code, Codex and the other harnesses
Update the coding harness your agents run with one command, one tap in the Live panel or by itself on your runners, without losing the running session.