Sessions
The log, turns, recovery, and reading events.
A session is a conversation plus everything Brain did on its behalf, recorded as an ordered log.
Writes land before effects
Before Brain calls a model, activates the agent loop, or dispatches a tool call, it writes the intent to the log. Before the loop sees a result, that result is written too.
The log is written behind the turn — no fsync on the hot path — so a crash may lose its tail. On
restart Brain rebuilds every session from the records that reached the disk: a session whose last
turn finished comes back idle with its history readable, and one that was mid-turn comes back with
a turn_interrupted event and returns to idle. Whether the in-flight call reached the model or a
tool is not knowable from here, so Brain records exactly that and lets the client resume or
abandon on its own terms.
A session also does not have to be resumed in place. Pass the events you already received back as
history when creating a session, and Brain writes them as the new session's opening records and
tells the agent loop what it is continuing — which is how a conversation moves across processes or
machines.
Turns
A turn starts when you send a message and ends when the agent loop decides it is finished. In between, Brain and the loop alternate: the loop returns a decision, Brain performs it, the loop sees the result and decides again.
BRAIN_MAX_DECISIONS bounds how many decisions one turn may take. The default is 128.
Reading events
Events are the log, exposed. Read from a cursor and you get everything committed since:
let cursor = 0;
for await (const event of session.events(cursor)) {
await handle(event);
cursor = event.sequence;
await saveCursor(cursor);
}This reads the durable log, so it is replayable and gap-free. The live stream on top of it is bounded and best-effort: under sustained pressure it drops rather than blocking a turn or growing without limit. If you need at-least-once delivery into your own systems, own the cursor and do the forwarding yourself — Brain is not a queue.
The live stream is the same endpoint asked for a different media type:
GET /v1/sessions/{id}/events?after={cursor}
Accept: text/event-streamIt begins with the page after names and then carries records as they are appended, so opening it
before sending a message is how you see that turn's first output. While a turn is running it
also carries the model's output as it arrives — assistant_delta and tool_call_delta events.
Those are never written to the log and carry no id: the id is the resume cursor, and a client
that reconnects gets the completed message from the log rather than the tokens that built it. The
stream ends when the session ends, and it ends if you fall too far behind — reconnect with the
last id you saw and the log hands back exactly what you missed. Accept: application/json
returns one page and is what session.events() uses.
Lifecycle
cancel stops work in flight. end closes the session to new messages. delete removes it and its
log. They are separate on purpose: ending a session keeps its history readable.