Skip to main content
Session streams are long-lived and delivery is at-least-once. A correct consumer needs three things: a durable cursor, deduplication by event ID, and settlement read from the log rather than from the connection. This page is about reconnecting an event stream. It is not native resume of a harness capsule.
Plain HTTP against the Checkfu API needs no private package. The Checkfu CLI and TypeScript SDK are optional private-alpha artifacts; the HTTP requests work without them. Part of this flow is behind a rollout fence; a deployment that has not enabled it refuses the request rather than running it. Confirm capability status before making availability part of your application’s contract.

The three rules

Order by seq

seq is the per-Session sequence. It is the cursor you persist and the order you process in.

Skip what you have

Delivery is at-least-once. In one Session stream the cursor filters replays; across unordered consumers, deduplicate on the globally unique id.

Settle on events

A closed connection is not a finished workflow. Read both the status event and its stop_reason before deciding to stop following.

Connecting and resuming

CHECKFU_API_KEY and CHECKFU_WORKSPACE_ID come from Get access; CHECKFU_SESSION_ID is the sess_… you are following. The TypeScript tab uses the TypeScript SDK with this client:
With no cursor, the stream starts from the beginning of retained history, then stays live. On reconnect, send the last sequence you fully processed in Last-Event-ID. A disconnect is not a reason to resend the input or its wait response: reconnecting observes the original work; it does not restart it.
Each SSE frame carries seq in id:, the event type in event:, and the full envelope in data:.

Process each event exactly once

Delivery is at-least-once, so an event can reach your code again after a reconnect or crash. Commit the event’s local changes and cursor in the same database transaction. This prevents duplicate local application of that event; it does not guarantee exactly-once effects in another service. The sample follows one sequential turn through its waits. For overlapping Runs, follow each Run’s recorded outcome instead of using Session idle as a completion boundary for all work. Use the TypeScript SDK’s stream rather than writing your own SSE parser. It resumes from Last-Event-ID, honors Retry-After, bounds its reconnects, and cleans up the response body. The follower below keeps no cursor in memory: it reads the durable cursor, and each event is applied inside the same database transaction that advances it.
path=follow-session.ts
Call followSession with your SDK client, your database, the Session ID, and an apply function that writes the event’s local effect through tx. Keep the cursor and applied state together across restarts. Before following again, check that stored state for an already-recorded settlement. An absent cursor starts this sample at zero and is appropriate for a new local projection. If an existing projection loses its cursor, recover the matching state and cursor or rebuild a fresh projection; do not replay external effects from the beginning. How the pieces behave under failure: Your database adapter must serialize cursor updates for each Session, for example by creating the cursor row and locking it with SELECT ... FOR UPDATE in loadCursor. A transaction alone does not stop two followers from reading the same cursor and both applying the effect. Every follower must use the same locking rule.
  • Crash after the effect, before the cursor. Both are in one transaction, so neither committed. The event replays and is applied once.
  • The cursor write fails. The transaction rolls back the effect with it, and the error reaches your caller. Following again replays the same event.
  • Duplicate delivery. The transaction re-reads the durable cursor, so an event at or below it is skipped without calling apply.
  • Your own handler or database fails. The original error propagates without advancing the cursor, and followSession stops. Even a retryable CheckfuError raised by your application is not classified as a stream reconnect failure. Handle that failure at its application boundary.
  • The stream fails or ends early. When isRetryableStreamFailure(error) is true, the SDK has already used its reconnect budget or the stream closed without a settlement event; wait, then call followSession again. followSession resolves only after a settlement event, never because a connection closed. It resumes from the durable cursor. A non-retryable CheckfuError (an invalid key, a missing Session) needs a fix, not a retry.
  • Leaving early. Returning from the loop, or an exception inside it, closes the SDK iterator, which cancels the underlying request. A cleanup failure propagates when it is the only failure; if application or stream work already failed, that original failure takes precedence.
  • requires_action. An idle boundary whose stop_reason is requires_action is intermediate, so the follower keeps going through the continuation.

Effects outside your database

A database transaction cannot include a call to another service, so a local receipt written after a remote call is not atomic with it. For a remote effect, pick one:
  • The provider supports idempotency keys. Send a key derived from the event, such as its id. A replay repeats the call, and the provider returns the first result instead of acting twice.
  • It does not. In apply, record the intent (event id, target, request) in the same transaction as the cursor, and dispatch it from a separate worker after commit. That worker marks the intent done on success, and after a crash reconciles against the provider’s own state before it sends anything again.

Without the SDK

If you consume the stream directly, keep the same transaction rule and also handle what the SDK does for you: check the HTTP status before reading the body, retry only on 429, 503, and other 5xx responses, honor and bound Retry-After, and resume with the last committed seq in Last-Event-ID. Use an SSE parser that follows the event-stream format: lines can end in CRLF, LF, or a lone CR, and a frame can be split across network reads, so splitting on "\n\n" is not enough. Cancel the response body when you stop reading.

Live previews do not move the cursor

The stream is durable-only unless you repeat the event_deltas query parameter. An opted-in stream may also receive event_start and event_delta frames for live agent.message output, plus event_start for agent.thinking. These frames have no SSE id: line and no seq, are never replayed, and may be dropped. Do not save a cursor for them. If the corresponding observation commits, the final persisted agent.message or agent.thinking event uses the preview’s event ID. Reconcile the temporary rendering to that event, perform durable side effects from the final envelope, and advance Last-Event-ID only from its numeric seq. A failed, interrupted, or lease-lost turn can leave no matching final event, so discard unmatched previews when the turn settles. A non-SDK client may also clear them at an observable HTTP reconnect; the SDK reconnects transparently, so recreating its iterator is the observable reconnect boundary. The TypeScript SDK enforces cursor neutrality automatically when you pass eventDeltas to events.stream.

Settlement

Use both the event type and stop_reason to decide whether your workflow follower can return: These stop rules apply to the sequential follower above. An idle boundary does not end the Session or prove that every overlapping Run has finished. Match run.completed, run.failed, or run.canceled to the Run you are tracking. requires_action and session.status_waiting are intermediate waits. A matching approval decision, question answer, or tool result continues the same original Run; it does not admit a replacement Run. Approval still requires current authority, and an application-executed tool still needs its result. See Handle an approval.
Do not infer settlement from a closed HTTP request or a dropped SSE connection. A connection can close for reasons that have nothing to do with the Run, and the Run keeps going. Read settlement from the log.

Backpressure

A stalled consumer does not lose events. Checkfu keeps at most one internal page in flight and lets the rest wait in the durable log, so a slow reader applies backpressure rather than dropping data. Reconnecting from Last-Event-ID resumes from that log. This means a consumer that falls far behind is fine. It also means the stream is not a substitute for a queue: if your processing is genuinely slow, read with the list endpoint on your own schedule instead.

Catching up without streaming

GET /v1/sessions/{id}/events pages the same log. limit defaults to 100 and caps at 1000. The list supports two mutually exclusive entry points. Pass a decimal event seq as after to begin immediately after that durable position—the same coordinate carried by SSE Last-Event-ID. Or start without after and continue an ordinary paginated walk by echoing each opaque next_page value as page. Never send after and page together, and never try to construct an opaque page token from a sequence. Use it for backfill, reconciliation after downtime, or rebuilding a projection from scratch. The stream and the list return the same persisted envelopes.

Next steps

Events

The envelope, the full event catalog, and the drive events you can post.

Fold a session into named views

Fold the stream into named views: messages, tool calls, ActionApprovals, the subagent tree, and the context ledger.