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 reduces to messages,
what the dashboard’s session view shows, and what
GET /sessions/<id>/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 |
When the session was created with an external
reference, every session.* event’s
data also carries it as externalReference.
stopping follows an 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 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 |
turn.failed | The turn stopped with an error | code, message, and model or tool when named: a public failure |
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.
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; 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 deliver it as a
completed turn. The open question is also on the session as
question, 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 shows those calls unnamed and
without output; messages is unaffected.
A successful call of the agent’s result tool
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.
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 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.
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 shows the same data.
Reading the log
From the CLI, with one NDJSON record per event:
opencomputer sessions tail <session-id> --after 0 --jsonFrom an application, poll the events route or attach
useAgent, 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.