> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orchestrahq.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Steering a session together

> Who is here, who is typing, whether the agent can hear you, and how interrupts work when two people steer one agent

A session is a shared object. Several people can be in one at once, and the
session page shows four things without anyone having to explain them: who else
is there, who is typing, whether the agent can hear a message before you type
it, and whose turn an interrupt would cancel.

This page describes the rules the API enforces. The web app is one client of
them; any client that talks to the API gets the same answers.

## What is logged and what is not

| Fact | Kind | Where it lives |
| - | - | - |
| Someone posted a message, to the agent or to the room | Event | The log, attributed, forever |
| Someone interrupted a turn | Event | The log, naming whose request it cancelled |
| A cancelled request was re-run | Event | The log, citing the original (`rerunOf`) |
| An owner turned teammates' messages on or off | Event | The log, as a history of consent |
| Someone is watching | Ephemeral | API memory, while their stream is open |
| Someone is typing | Ephemeral | API memory, for five seconds after a keystroke |

Watching and typing are never written anywhere durable. A record of who looked
at whose session is surveillance, not memory. They travel over the session's
SSE stream as non-event frames (`event: presence`, `event: typing`) with no
`id:`, so they are outside the reconnect cursor.

## Can the agent hear me?

Every session has a **reach**, and the API publishes it: `sessions.list`,
`sessions.detail`, the terminal's session reads and the `get_session` tool all
return `reach` and `reachReason`. It is the same answer the API checks a
message against, from the same call, so a client that shows it never tells
someone the agent can hear them and then has the message refused. Clients show
it; none works it out for itself.

| Reach | When | What a message to the agent does |
| - | - | - |
| `queue` | A native session with an open run | Enters the running loop's queue, even mid-turn |
| `boundary` | A captured Claude Code session whose owner has messages on, and that has not ended or stopped reporting | Waits in a queue and is handed over at the end of the agent's current turn |
| `none` | Anything else | Nothing will pick it up |

`reachReason` says why, so every surface explains a `none` the same way, and a
refused message names the same reason:

| `reachReason` | Meaning |
| - | - |
| `running` | Native, and a run is open |
| `hooks` | Captured, its owner's hooks are on, and Orchestra heard its process within the last minute |
| `unconfirmed` | Captured with hooks on, heard from before but not in the last minute, and not yet for ten |
| `recent_activity` | Captured with hooks on, never heard from directly, but it wrote something in the last two minutes (a guess) |
| `not_running` | Native, and no run is open |
| `ended` | It recorded an end (a clean exit) and nothing has happened since |
| `process_gone` | Captured, and its process stopped without saying so (a crash, a closed terminal, a killed process) |
| `stopped_reporting` | Captured, and Orchestra has not heard from it for ten minutes (its machine may be asleep or offline) |
| `quiet` | Captured with hooks on, never heard from directly, and nothing written for two minutes (a guess) |
| `no_hooks` | Captured, and its hooks never turned messages on |
| `revoked` | Captured, and its owner turned messages off with `orchestra disable` |

### How Orchestra knows a captured session is running

A captured session runs in its owner's own terminal, so Orchestra cannot ask
it directly. The capture tailer on the owner's machine can see the agent's
process: when a hook registers a session, it records which process is the
agent, and every 15 seconds the tailer reports the ones that are still
running. Whether a session is live comes from that report, never from how
recently it wrote something. An agent thinking, running a long build or
waiting on CI stays live however quiet it is.

When the process stops without Claude Code's SessionEnd hook saying so, the
tailer reports it, and the session records an ending with the reason
`process_gone`. When nothing is heard from a session for ten minutes, the API
records `stopped_reporting`. Both are recorded once, and neither claims the
session exited: they say what Orchestra observed. A session heard from again
afterwards (a laptop waking up, say) is live again in place.

A session that has never been heard from (a client older than this, or a
machine that runs no tailer) is judged by its last activity as before, and its
reason says so: `recent_activity` or `quiet`, never `hooks`.

`steerable` and `openRun` are still returned for older clients. They are
deprecated: read `reach` and `reachReason` instead.

For a native session the fact is an **open run** (a `run_started` with no
`run_ended`), not how recently the session spoke. A loop waiting for input is
reachable however quiet it has been, and a fork nobody has started is not,
however fresh. The page's stream refines this live: its liveness frames carry
the open runs, and a run its supervisor has let go of does not count.

`sessions.postMessage` takes `target: "agent" | "room"`. Without one, it
defaults by session kind: `agent` on native, `room` on captured. A message to
the agent on a session whose reach is `none` is **refused**, with the reason and
what to do instead. It is never accepted as steering that nothing will read.
A message to the room is always accepted. The terminal's `postMessage` and the
`post_session_message` tool are refused in exactly the same cases, with the
same words.

## Turning messages on and off

A teammate's message reaches a captured session only with its owner's consent,
because it runs in their terminal with their credentials and files. Consent is
recorded in the log, and it is a history rather than a switch:

* The first time the hooks `orchestra enable` installs run in a session, they
  record consent for it.
* `orchestra disable` in that repo removes the hooks and records a revocation
  for each of your sessions there. From then on the session's reach is `none`
  (`revoked`): a message to its agent is refused, and messages that were still
  waiting are withdrawn, never handed over later. A hook that keeps running
  from the settings Claude Code read at startup delivers nothing and does not
  turn messages back on.
* `orchestra enable` again records consent again for the sessions you turned
  off. Messages sent from then on are delivered; the withdrawn ones are not.

Only your own sessions are affected, and only in that directory and below.
Offline, the change is queued and recorded as soon as Orchestra is reachable;
until then the session still reads as taking messages, and says so.

## Interrupts

An interrupt cancels the agent's in-flight turn. It never deletes a message.

* **Your own turn** stops on one call.
* **Someone else's turn** costs one confirmation that names them. Until the
  caller confirms, `sessions.interrupt` appends nothing and answers with
  `needsConfirmation`:

```json theme={null}
{
  "eventId": null,
  "needsConfirmation": {
    "turnRequestEventId": "01K7...",
    "requestedBy": { "principalId": "…", "name": "Ana" },
    "occurredAt": "2026-10-13T16:02:11.000Z",
    "text": "migrate the schema"
  }
}
```

Send the call again with `confirmed: true` and that `turnRequestEventId`. The
confirmation counts for that request only: if someone else asked for something
in between, the API asks again, naming them.

Whose turn it is comes from the log, over the whole session: the latest message
to the agent since the last interrupt or end. A room aside is not a request.
The interrupt event records `turnRequestEventId` and, when the request was
someone else's, `turnRequestedBy`.

The same rule holds on every door. The terminal's `interrupt` answers with a
confirmation naming the requester, and the next Esc confirms that request.
The `interrupt_session` tool answers `needs_confirmation` until it is called
again with `confirmed: true` and the `turnRequestEventId` it returned. An agent
going through the tools meets exactly the friction a person in the browser
does.

## Re-running a cancelled request

`sessions.rerun({ sessionId, interruptEventId })` re-queues the cancelled
request's stored words to the agent as a new message from the caller, with
`rerunOf` pointing at the original. The client names only the interrupt; the
words come from the log, so a request far outside the page's window still
re-runs, and an escaped slash is never re-read as a command. Pressing it twice
re-runs once. Like any message to the agent, it is refused when nothing would
hear it. Agents do the same with the `rerun_session_request` tool.

## Two people steering at once

There are no locks and no turn-taking. Both messages land, in the log's total
order, attributed to whoever authenticated. The agent receives both, in that
order, by name, and the timeline shows exactly what it saw.

## Who can see the room

The session's visibility governs everything here. A principal who cannot see a
session cannot get a stream for it, cannot post into it, cannot interrupt it,
and never appears in its watcher list or typing line. The stream checks
visibility when it connects, on every batch of frames, and on every heartbeat,
so a revoked watcher stops receiving frames and drops out of the room's
presence without reconnecting. Granted again, they are back in the room's
presence on the next heartbeat, also without reconnecting.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.