# Follow a run live Follow a Session's durable Event log over Server-Sent Events, handle a Run's outcome, resume from a saved cursor, and follow many Sessions on one connection. Source: https://docs.cloud.vampikez.fun/guides/follow-a-run/ 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: ```text RateLimit-Policy: "wamp-cloud";q=600;w=60;wamp-burst=120 RateLimit: "wamp-cloud";r=117;t=1 ``` ## Open the stream ```bash 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`: ```text id: 12 data: {"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: `; 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 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 `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 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: ```bash 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 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`. ```bash curl -N -sS "$WAMP_API/v1/sessions/stream?after=${WATERMARK:-0}" \ -H "Authorization: Bearer $WAMP_TOKEN" \ -H 'Accept: text/event-stream' ``` ```ts check export interface FollowerStore { watermark(): Promise; saveWatermark(watermark: number): Promise; cursor(sessionId: string): Promise; /** Commits the cursor together with whatever `handle` changed. */ saveCursor(sessionId: string, sequence: number): Promise; } export async function followInstallation( cloud: WampCloud, store: FollowerStore, handle: (event: CloudSessionEvent) => Promise, signal: AbortSignal, ): Promise { const catchUp = async (sessionId: string, lastEventSequence: number): Promise => { 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); } } ``` ```text id: 1786526460000 id: 1786526467250 data: {"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=` 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](/reference/events/#change-notices) 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.