> ## Documentation Index
> Fetch the complete documentation index at: https://opensandbox-feat-types-open-question-labels.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Session events

> The durable event log every session keeps and every consumer reads

Every session appends what happens to one durable log: the turns it
accepted, the text the model produced, the tools it called, the memory it
saved and the runtime that ran it. The log is what `opencomputer sessions
tail --json` prints, what [`useAgent`](/agents/react) reduces to messages,
what the dashboard's session view shows, and what
[`GET /sessions/<id>/events`](/agents/api#events) returns.

## Shape and ordering

Each event is one JSON object:

| Field | Meaning |
| - | - |
| `seq` | Position in the log, starting at 1 and increasing with every event |
| `id` | A unique event ID |
| `timestamp` | When the event was recorded |
| `sessionId` | The session |
| `turnId` | The turn the event belongs to; absent on session-level events |
| `type` | One of the types below |
| `data` | Fields specific to the type |

`seq` is the cursor. A read with `after=<seq>` returns events with a greater
`seq`, in ascending order, up to 500 at a time; repeat from the last `seq`
you received until a page is empty, and keep polling from there to follow a
live session. Because the log is durable and `seq` only grows, a consumer
that stops can resume from its cursor without missing anything, and a page
that overlaps one already read is harmless: apply events whose `seq` is
greater than what you have applied. The React hook and the CLI work this
way.

Events at or below the cursor never change. New types can appear; treat an
unknown type as informational and keep reading.

## Event types

### Session lifecycle

| Type | When | `data` |
| - | - | - |
| `session.created` | The session exists. Always the first event. | `agentId`, `deploymentId` |
| `session.status_changed` | The session moved between `new`, `connecting`, `idle`, `running`, `stopping`, `waiting_runtime`, `suspending`, `suspended`, `resuming`, `failed` and `ended` | `from`, `to` |
| `session.ended` | The session accepts no more turns; queued and running turns were marked cancelled. Memory revocation and remote cleanup may still be pending. | none |
| `session.failed` | The session cannot continue | `code`, `message`: a [public failure](#public-failures) |

When the session was created with an [external
reference](/agents/api#external-references), every `session.*` event's
`data` also carries it as `externalReference`.

`stopping` follows an [interrupt](/agents/api#end-and-interrupt) or an
interrupt-mode turn: the session stays there until the stopped turn's
commands are confirmed stopped or its computer is terminated, then `idle`
follows and the next turn can start. See the API's compatibility note for
sessions that cancel immediately instead.

`session.ended` is not a command-settlement or memory-revocation receipt.
The [end response](/agents/api#end-and-interrupt) confirms memory revocation;
remote cleanup continues in the background.

### Turns

| Type | When | `data` |
| - | - | - |
| `message.received` | A turn was accepted; its input is recorded before anything runs | `input`: the user text; `mode`: `queue`, `steer` or `interrupt`; `payload`: the structured value the turn was sent with, when there was one |
| `turn.queued` | The turn waits for earlier turns | `mode` |
| `turn.steered` | The input was delivered into the running turn | `activeTurnId` |
| `turn.interrupted` | This turn requested interruption of the running turns | `interruptedTurnIds` |
| `turn.started` | The runtime began the turn | none |
| `turn.completed` | The turn finished and the session is idle again | none, or `outcome: "question"` and `questionId` when the turn ended by [asking](#questions) |
| `turn.failed` | The turn stopped with an error | `code`, `message`, and `model` or `tool` when named: a [public failure](#public-failures) |
| `turn.cancelled` | The turn was cancelled by an interrupt or session end, held behind a question, or stopped from Linear | `reason`: `interrupted`, `session_ended`, `held` or `stopped`; optional `replacementTurnId`, `settledAfterMs`, `operationsSettled`, `computerTerminated`; `questionId` with `held`; `discarded: true` on a queued turn a stop discarded |

For an interrupt that waits for commands, `settledAfterMs` records the wait,
`operationsSettled` counts the settled commands, and `computerTerminated`
says whether stopping required terminating the computer. Cancellation with
`reason: "session_ended"` has no settlement fields: it records the session's
decision to end, before remote cleanup finishes. Immediate-cancellation
sessions also omit these fields; see [End and interrupt](/agents/api#end-and-interrupt).

A turn that was queued behind a turn that asked a question is cancelled
with `reason: "held"` and the `questionId`: its input did not run on its own
and is delivered with the answer as `steering`.

#### Public failures

A failure's `data` is a stable `code`, a fixed `message` for that code, and
at most one parameter. The runtime's own error text is never sent; a failure
no rule recognizes is `agent_failed`.

A model call that fails in flight may be retried inside the turn before
the turn fails. The runtime retries provider errors, rate limits,
incomplete response streams and transport failures that interrupt a
started response on its own schedule, per model call; the host adds one
retry for a transport failure or an invalid response the runtime would
not retry on its own. The agent's own calls are covered; the runtime's
auxiliary calls (compacting the conversation, titling it) are not
retried. Each retry appears in the log as a `runtime.log` milestone with
`phase: "model_retry"`, the attempt and the failure's class (for example
`provider.transport`), never the provider's own text. A rejection is not
retried: credentials, quota, content policy and an invalid request are
`model_rejected` at once. A `model_stream_failed` message names its retry
only when one happened.

| `code` | Meaning | Parameter |
| - | - | - |
| `interrupted` | The turn was stopped before it finished | |
| `session_ended` | The session ended while the turn ran | |
| `runtime_lost` | The runtime stopped responding and the turn was abandoned | |
| `runtime_failed` | The runtime failed before the turn finished | |
| `deployment_invalid` | The runtime could not load the deployment | |
| `model_unavailable` | The requested model is not available to this agent | `model`: the requested model id |
| `model_rejected` | The model provider rejected the request: credentials, rate limit or quota | |
| `context_too_long` | The conversation exceeds the model's context window | |
| `tool_failed` | A tool failed | `tool`: the tool id, when the runtime named it |
| `sandbox_timeout` | A sandbox command did not finish in time | |
| `sandbox_failed` | The sandbox could not run the turn | |
| `model_stream_failed` | The model call failed before it finished, or its response stream broke; when it was retried, the retry failed too | `model`: the model id, when known |
| `agent_failed` | Any other failure | |

### Questions

| Type | When | `data` |
| - | - | - |
| `question.asked` | The agent called [`ask`](/agents/tools#ask-a-question); the asking turn completes with `outcome: "question"` | `questionId`, `text`, `options`: `{ label, value }` choices, empty for free text |
| `question.answered` | A turn answering the question was admitted | `questionId`, `answer`: `{ questionId, text, value? }` |
| `question.closed` | The question closed without an answer | `questionId`, `reason`: `stopped`, `dismissed`, `ended` or `undeliverable` |
| `message.held` | An input sent without `answers` was held behind the open question | `heldId`, `questionId`, `input`, `payload?`, `receivedAt` |
| `message.delivered` | A held input reached the agent | `heldId`, `answerTurnId` (the turn that carried it), `as`: `steering`, `answer` or `turn` |
| `message.discarded` | A held input will not run | `heldId`, `reason`: `stopped` or `ended` |

`turn.completed` with `outcome: "question"` is the turn's terminal event, so a
consumer that knows only completed turns reads an asking turn correctly, and
[outcome subscriptions](/agents/api#event-subscriptions) deliver it as a
completed turn. The open question is also on the session as
[`question`](/agents/sessions#questions), where each closure `reason` is
explained. A held input is recorded once, however often its key is repeated,
so a client can rebuild held messages after a reload.

### Messages

| Type | When | `data` |
| - | - | - |
| `message.delta` | The model produced a fragment of its reply | `text`: the fragment; concatenate deltas of one turn |
| `message.completed` | The reply is complete | `text`: the whole reply |
| `reasoning.delta` | The model produced a fragment of reasoning, when the model exposes it | `text` |
| `reasoning.completed` | The reasoning is complete | `text` |

### Tools

| Type | When | `data` |
| - | - | - |
| `tool.started` | The model called a tool | `tool`: its name; `callId`; `title`; `input`: the arguments as a JSON value |
| `tool.progress` | The tool reported progress | runtime-defined |
| `tool.completed` | The tool returned | `tool`, `callId`, `title`, `output`: what the tool returned as a JSON value; `result: true` when the output became the session's result |
| `tool.failed` | The tool raised an error, or its turn ended before it completed | `tool`, `callId`, `title`, `message`; `settledBy` when the turn's end settled the call: `turn.completed`, `turn.failed` or `turn.cancelled` |

The exact fields come from the runtime's tool record; read them as optional
and key a tool call on `callId` when it is present. The memory tools
(`memory_save`, `memory_read`, `memory_list`) appear here like any other
tool; the save itself is reported separately.

`input` and `output` are JSON values, never JSON text: an object comes as an
object, and a consumer reads it without parsing. Tool events recorded before
these fields were introduced keep the shape they were recorded with, so the
React hook's [`turns`](/agents/react#hook-reference) shows those calls unnamed and
without output; `messages` is unaffected.

A successful call of the agent's [result tool](/agents/tools#the-session-result)
is recorded with `result: true`; its `output` is the validated value, the
same JSON value `GET /sessions/<id>` returns as `result.data`. The session
writes this event when it commits the result, before the model sees the call
succeed, and there is one per call.

Every tool call ends in the log. The runtime reports a `tool.completed` or
`tool.failed` per call; when a turn ends with a call still open, whether
the turn completed, failed or was cancelled, the session records a
`tool.failed` for that call ahead of the terminal turn event, in the same
write, with `settledBy` naming the terminal event (`turn.completed`,
`turn.failed` or `turn.cancelled`) and a `message` saying the turn ended
first. A consumer therefore never sees a settled turn with a running call;
one that keys rows on `callId` needs no rule of its own for the turn's end.
This settles the call in the log; it does not strengthen the command-stop
guarantees of the terminal turn event.

### Memory

| Type | When | `data` |
| - | - | - |
| `memory.saved` | A `memory_save` the session observed succeeding | `resource`, `documentId`, `revision`, `bytes` |

Delivery is best-effort. A save commits independently of the event: a
runtime that loses its connection can commit without ever reporting it, and
the same event can be reported once or twice after a reconnect. Treat the
event as a hint to re-read the document, and also re-read when attaching,
reconnecting and completing work. See
[Document memory](/agents/document-memory#saving).

### Model and usage

| Type | When | `data` |
| - | - | - |
| `model.route_resolved` | A model call is about to be sent | `providerCallId`; `requested` and `effective` as `{ provider, model }`; `runtime`; `access` |
| `model.access_fallback` | A call planned on a connected model account fell back to managed access | `providerCallId`, `requested`, `from`, `reason` |
| `usage.recorded` | A model call finished | `provider`, `model`, `inputTokens`, `outputTokens`, `reasoningTokens`, `cachedTokens`, `cacheWriteTokens`, `costUsd`, `payer` |

### Outbound requests

| Type | When | `data` |
| - | - | - |
| `egress.request` | A [declared connection](/agents/secrets) sent a request | `connectionId`, `method`, `path` |
| `egress.response` | The destination answered | `connectionId`, `method`, `path`, `status`, `durationMs` |
| `egress.failed` | The request did not complete | `connectionId`, `method`, `path`, `message` |

### Runtime

| Type | When | `data` |
| - | - | - |
| `runtime.connected` | An agent runtime attached to the session | none |
| `runtime.disconnected` | The runtime connection closed; running turns return to the queue to await a runtime | none |
| `runtime.suspended` | The runtime was suspended between turns | none |
| `runtime.resumed` | The runtime was resumed | none |
| `runtime.log` | A line of runtime output or a platform milestone | `level`, `stream`, `message` for output; `phase` and `message` for milestones such as the runtime starting |

A runtime disconnect does not prove that a command stopped or that its
effects were undone. Follow the subsequent turn events to learn whether
work continued or failed; see [Durability and recovery](/agents/sessions#durability-and-recovery).

### Agent renders

| Type | When | `data` |
| - | - | - |
| `agent.rendered` | Your agent function was rendered for a model step | `renderId`, `instructions`, `model`, `enabledTools`, `enabledMcpServers`, `requiredConnections`, `input`, `tools`, `renderedAt` |

One turn can carry several renders, one per model step. The debug inspector
in the [playground](/agents/playground) shows the same data.

## Reading the log

From the CLI, with one NDJSON record per event:

```bash theme={null}
opencomputer sessions tail <session-id> --after 0 --json
```

From an application, poll the [events route](/agents/api#events) or attach
[`useAgent`](/agents/react), which turns `message.received`,
`message.delta` and `message.completed` into messages, `turn.*` and
`session.*` into `isRunning` and `ended`, and `memory.saved` into
`memorySaves`; every event, these included, also reaches `onEvent`.


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