Follow a run live
After you submit a Turn, open its Session Event stream to watch progress and
receive the terminal outcome. Save each Event’s sequence after your own handler
commits its work. The JSON page remains the recovery path if you need to inspect
or repair history.
Examples use $WAMP_API for the Cloud API origin and $WAMP_TOKEN for a bearer
credential with wamp.cloud.sessions:read.
Opening the stream spends one ordinary rate-limit token. For example:
RateLimit-Policy: "wamp-cloud";q=600;w=60;wamp-burst=120RateLimit: "wamp-cloud";r=117;t=1Open the stream
Section titled “Open the stream”curl -N -sS \ "$WAMP_API/v1/sessions/$SESSION_ID/events/stream?after=0" \ -H "Authorization: Bearer $WAMP_TOKEN" \ -H 'Accept: text/event-stream'The stream first replays retained Events after the exclusive cursor, then waits
for new appends. Each message carries the same SessionEvent object returned by
GET /v1/sessions/{sessionId}/events:
id: 12data: {"id":"d1f0a934-2b57-4c86-9e1d-3a0b7f625c48","type":"wamp.run.started","createdAt":"2026-08-12T09:21:07Z","sequence":12,"subject":{"sessionId":"9f2b7c14-59d3-4f7a-b8e1-2a6c05d4e731","runId":"c47a1e08-3d6b-4a92-9f15-8b70d2e5c6a4"},"data":{"status":"running"}}There is no event: field: handle every message, including types a newer server
adds. : keep-alive comments arrive at least every 15 seconds and are not
Events. The id is the decimal sequence. The stream never skips a sequence;
assert event.sequence === previous + 1 and stop on a gap.
A reconnect sends Last-Event-ID: <last committed sequence>; this header wins
over ?after. On a first connection or a restart without an in-memory cursor,
pass the cursor you persisted. after=0 replays from the beginning. The stream
ends after 5–10 minutes, at credential expiry, or during a Cloud shutdown; open
it again with a fresh credential and your saved cursor. A stream the server
ends names its reconnection time in a final retry: field (milliseconds): at
most a second when its lifetime ends, spread over up to 30 seconds when a Cloud
shutdown ends every stream at once. Wait that long before reconnecting; when a
stream ends without one (the connection was cut), wait a random 0–30 seconds. On 503, also honor
Retry-After. During a release a reconnect can meet 503 service_unavailable,
or an uncoded 502 or 504 from the edge, for several seconds, so keep trying
for at least 20 seconds before you give up. The source App SDK’s
streamSessionEvents() does this for you.
Record an outcome
Section titled “Record an outcome”Four types terminate a Run: wamp.run.completed, wamp.run.failed,
wamp.run.cancelled, and wamp.run.crashed. Match subject.runId to the Run
you submitted. The SDK exports isTerminalRunEvent(event, runId) for that check.
The terminal Event follows the retained timeline and Artifact Events for the
Run, so stopping there does not miss earlier facts.
An Event log can be empty while a Run is queued or dispatching. A dispatch
refusal can append a terminal failed or crashed Event without a preceding
wamp.run.started. Read GET /v1/sessions/{sessionId}/runs/{runId} to show
queued attempts and lastError.code; do not wait for started before accepting
a terminal Event. Active wamp.run.progress Events describe bounded tool
lifecycle facts. Assistant messages and presented files arrive while the Run
works, a message as it completes rather than token by token; whatever the Run
had not delivered by its end arrives at the terminal capture, before the
terminal Event.
Handle a question mid-run
Section titled “Handle a question mid-run”wamp.interaction.opened identifies a question, plan, or live approval. Fetch
GET /v1/sessions/{sessionId}/interactions for currently open requests. Submit
a new Turn with replyTo.interactionId for a question or plan; decide a live
approval with PUT …/interactions/{interactionId}/decision, which resumes the
same Run without a reply Turn. An
Interaction is a durable edge, not a socket prompt. If your process was offline,
read the Event log from its cursor and then read the open Interaction list before
asking a user to answer anything.
Resuming after a disconnect
Section titled “Resuming after a disconnect”Store your cursor in the same transaction as the side effect caused by each
Event. A crash before that transaction commits replays the Event; make the
handler idempotent on the Event id. A crash after it commits resumes at the saved
sequence. If the stream drops, reconnect using Last-Event-ID; the server pages
the same authorized log before waiting for new work.
Use the JSON page for a finite audit or repair:
curl -sS \ "$WAMP_API/v1/sessions/$SESSION_ID/events?after=$LAST_SEQUENCE&limit=100" \ -H "Authorization: Bearer $WAMP_TOKEN"hasMore only says another page is available. An empty page says you reached
the current tail, not that the Run ended. Archive does not erase the log; Events
remain readable after the Session stops accepting work.
The stream requires fresh authorization at open and closes when the local revocation epoch advances. A refused open is JSON, with the usual machine error code. An authority can open at most 64 streams and the process at most 256; a slow reader is closed after more than 1 MiB remains unflushed. Reconnect from your last committed cursor rather than inferring a Run outcome from a closed connection.
Follow many Sessions
Section titled “Follow many Sessions”A backend that follows many Sessions does not need one Event stream per
Session. With an App installation credential, open GET /v1/sessions/stream:
it tells you which of the installation’s Sessions moved and to which sequence,
and you read each moved Session’s Event page from your own cursor. A person’s
credential gets 403 installation_principal_required.
curl -N -sS "$WAMP_API/v1/sessions/stream?after=${WATERMARK:-0}" \ -H "Authorization: Bearer $WAMP_TOKEN" \ -H 'Accept: text/event-stream'import type { CloudSessionEvent, WampCloud } from '@wamp/app-sdk';
export interface FollowerStore { watermark(): Promise<number | undefined>; saveWatermark(watermark: number): Promise<void>; cursor(sessionId: string): Promise<number>; /** Commits the cursor together with whatever `handle` changed. */ saveCursor(sessionId: string, sequence: number): Promise<void>;}
export async function followInstallation( cloud: WampCloud, store: FollowerStore, handle: (event: CloudSessionEvent) => Promise<void>, signal: AbortSignal,): Promise<void> { const catchUp = async (sessionId: string, lastEventSequence: number): Promise<void> => { const cursor = await store.cursor(sessionId); if (cursor >= lastEventSequence) return; for await (const event of cloud.sessionEvents(sessionId, { after: cursor })) { await handle(event); await store.saveCursor(sessionId, event.sequence); } }; // No saved watermark: 0 announces every Session the installation has created. const after = (await store.watermark()) ?? 0; for await (const { change, watermark } of cloud.streamSessionChanges({ after, signal })) { // An item without a change only moves the watermark; the stream opens with one. if (change) await catchUp(change.data.sessionId, change.data.lastEventSequence); await store.saveWatermark(watermark); }}id: 1786526460000
id: 1786526467250data: {"type":"wamp.session.events_appended","timestamp":"2026-08-12T09:21:07.000Z","data":{"sessionId":"9f2b7c14-59d3-4f7a-b8e1-2a6c05d4e731","lastEventSequence":12}}Each data is a notice, not an Event: the Session’s log reached at least
lastEventSequence. Page GET /v1/sessions/{sessionId}/events?after=<cursor>
for that Session until hasMore is false. Appends within 250 ms reach you as
one notice, notices can repeat, and they arrive in no particular order across
Sessions, so a notice at or below your cursor needs no work. The
notice reference lists its fields.
The id is a watermark: Cloud’s clock in milliseconds. Not every message
carries one. When Cloud announces several Sessions at once, only the last
notice of the batch carries the id, and it covers the notices before it.
A message with an id and no data only moves the watermark; the stream
opens with one. Save the newest watermark after you have acted on the notices
before it.
To resume, send it as ?after= (or Last-Event-ID, which wins). The stream first
re-announces every Session whose log may have moved since that watermark,
looking back 120 seconds before it, so expect some Sessions you are already
caught up on, and then goes live. With no saved watermark, pass after=0: the
stream announces every Session the installation has created that has Events,
which is how a first run catches up. Without either, the stream starts live
from now. A watermark from the future counts as now.
The stream follows the same rules as the Session Event stream above. It needs
fresh authorization at open, ends after 5–10 minutes, at credential expiry or at
a Cloud shutdown, and counts against the same 64 streams per authority. Wait
the retry: it ends with before reconnecting (a random 0–30 seconds when it
ends without one), and keep retrying through a release. The source App SDK’s streamSessionChanges() does
this and resends its newest watermark.