The session connection
connect() opens a session, keeps its log, and reconnects when the stream drops.
connect gives you a live view of one session. It reads the log, opens the stream, and holds on to both. Whether you are rendering a chat or waiting for a job to finish, everything comes off this object — you do not keep a copy of the messages yourself.
const session = subako.sessions.connect(sessionId, { history: true }); // subako.connect on a session client
await session.ready; // the history is read and the stream is open
What it holds
session.state.status; // "connecting" | "open" | "reconnecting" | "closed" | "failed"
session.state.error; // the failure, set when the status is "failed"
session.state.lastSeq; // number | undefined
session.state.events; // every event seen, in seq order
session.state.transcript; // the messages of the log, each call joined to its result and approval
session.state.approvals; // the approvals no decision has settled
session.state.isRunning; // a run queued or under way
state is immutable. You get a new object on every event and every status change, and the same object for as long as nothing changes. events follows the same rule: a new array when an event arrives, the same one otherwise, and the events inside are never copied. That is what makes it safe to hand straight to a renderer:
const state = useSyncExternalStore(session.subscribe, () => session.state);
state.transcript; // what this render draws
state.events is this client’s copy of the log: raw wire bodies in seq order. The connection does not interpret them. It joins no messages and builds no strings, so what an event means stays your decision. Reading the log covers the three helpers that do the interpreting.
Driving the session
The same object is how you drive the session, so nothing else has to hold state:
await session.send("book me a room"); // posts a user turn
await session.cancel(); // stops the active run at its next checkpoint
await session.resolveApproval(callId, "deny", "not this one"); // "allow" | "deny"
session.transcript; // the same three readings, taken off the connection
session.approvals;
session.isRunning;
Those three appear in two places, and it is one value either way: derived from state.events on first read, and recomputed only when that array changes. Read them off state when you hold a snapshot — inside a render, or in a subscribe listener. Read them off the connection anywhere else, where you want the latest rather than the one that render was woken for.
Iterating and subscribing
const stop = session.subscribe((state) => render(state)); // returns an unsubscribe
for await (const event of session) {
// The events so far, then live ones. `break` stops iterating, not the connection.
}
session.close(); // ends the stream; the clients on it are deleted
session.reconnect(); // only after a failure: opens the stream again on this same connection
Subscribers are called synchronously after every change, in seq order, with the states that follow their subscription. A listener that throws is reported with console.error and delivery carries on.
Options
history?boolean
Reads the log to its end first, oldest first; the stream then starts at the last seq. This is what a chat that draws the conversation so far wants.
booleanafterSeq?number
Otherwise: where the stream starts. Omit for the live head, or pass `0` to replay the log.
numbersignal?AbortSignal
Aborting closes the connection, as `close()` does.
AbortSignalmaxRetries?number
Retries of the request that opens the stream, as for any call.
numbermaxReconnects?number
Consecutive failed reconnects tolerated. Defaults to 5.
numberA connection with neither history nor afterSeq starts at the live head: it carries what happens from now on, and nothing that happened before.
What happens when it drops
The server tails the log forever, so the connection ends only when you end it.
-
Opening the stream is a request like any other. It is retried under
maxRetrieson the same terms, and a refusal such as a404fails the connection right away, withreadyrejecting andstate.errorset. -
Once open, every close is followed by a wait and a fresh connection carrying
Last-Event-ID, whether the server closed cleanly or the connection dropped. The server opens every stream with a cursor frame naming where it starts, so a reconnect resumes from there even when the connection carried no event at all. The wait is the server’s ownretry:value when it sends one, else 3 seconds, doubling up to 30 seconds while reconnects keep failing and resetting as soon as an event arrives. AftermaxReconnectsconsecutive failed reconnects the status isfailedandstate.errorholds the last failure. A reconnect the server refuses outright fails at once: a404, or a401that a freshly resolved token did not settle. -
A connection that failed can be opened again.
reconnect()starts the stream over fromlastSeqon the same connection object — the same subscribers, the same log, the same clients — with the reconnect budget reset andreadya fresh promise that resolves when the stream is open. The tool clients the failure sent away register again once it is, as they would on a connection opened from scratch. In any other status, a closed connection included, it does nothing. Nothing calls it for you: it is what a “reconnect” button in a chat is wired to.
Browser support
The SDK calls AbortSignal.any to join your signal to its own timeout, which puts the browser floor at Safari 17.4, Chrome 116, and Firefox 124.
Aborting with your own AbortSignal rethrows the abort reason, untouched and never wrapped in a SubakoError — whatever you passed to abort(reason), or the AbortError DOMException the platform supplies when you passed nothing. A caller that aborts deliberately can therefore tell its own cancellation from a failure without inspecting a message.