Sessions

Live Subscriptions

Consume the unified SessionMutation stream for streaming text, tools, Turns, and Session changes

Live Subscriptions

session.subscribe() is the only live Session entry point. It broadcasts only changes after subscription, with no cache, replay, or backpressure management.

const unsubscribe = session.subscribe((mutation) => {
  apply_mutation(mutation);
});

// Always release it when it is no longer needed.
unsubscribe();

The callback may be async. A thrown error or rejection from one subscriber does not roll back persisted state or block other subscribers.

Six Mutation variants

Every Mutation has mutation_id, session_id, and created_at.

varianttypePurposeHas Message revision
message`userassistantaction
part`textreasoningtool
delta`textreasoningtool_input`
turn`startfinish`Turn execution lifecycle
compact`startfinish`Explicit Compact Command lifecycle
sessiontitleSession property changeNo

message, part, and delta are published only after the corresponding complete Message or Assistant draft is persisted. The Mutation envelope itself is never written to active.jsonl or a Segment.

Render text with Deltas

session.subscribe((mutation) => {
  if (mutation.variant !== "delta" || mutation.type !== "text") return;

  append_text({
    message_id: mutation.message_id,
    part_id: mutation.part_id,
    delta: mutation.delta,
  });
});

A Delta is an appended fragment, not cumulative text. Do not use it as a complete Part snapshot and do not infer order from text content; use message_id, part_id, and revision. A tool_input Delta also carries the matching tool_call_id.

Correct state with complete snapshots

Network behavior, batched rendering, and UI transitions can leave client-maintained intermediate state incomplete. message and part are complete corrective snapshots:

function apply_mutation(mutation: SessionMutation): void {
  if (mutation.variant === "message") {
    upsert_message(mutation.message, mutation.revision);
    return;
  }

  if (mutation.variant === "part") {
    upsert_part(mutation.message_id, mutation.part, mutation.revision);
    return;
  }

  if (mutation.variant === "delta") {
    if (mutation.type === "tool_input") {
      append_tool_input_delta(
        mutation.message_id,
        mutation.part_id,
        mutation.tool_call_id,
        mutation.delta,
      );
    } else {
      append_text_delta(mutation.message_id, mutation.part_id, mutation.delta);
    }
  }
}

Track the greatest applied revision for each Message. A message or part snapshot at or below that revision can be ignored so an old event cannot overwrite newer state.

Turns, compaction, and title updates

session.subscribe((mutation) => {
  if (mutation.variant === "turn" && mutation.type === "start") {
    set_turn_running(mutation.turn_id);
  }

  if (mutation.variant === "turn" && mutation.type === "finish") {
    set_turn_finished(mutation.turn_id, mutation.status, mutation.error);
  }

  if (mutation.variant === "compact" && mutation.type === "finish") {
    set_compact_finished(mutation.compact_id, mutation.status, mutation.reason);
  }

  if (mutation.variant === "session" && mutation.type === "title") {
    set_session_title(mutation.title);
  }
});

turn.finish has completed, failed, or stopped status. It and turn.finished describe the same final execution: the former is for subscription-driven UI, the latter for control flow.

compact.finish and compact_handle.finished use the same final-result semantics: the Mutation is for UI updates, while the Handle is for application control flow.

Tool Part state machine

The same Tool Part is updated under the same part_id:

input-streaming -> ready -> waiting-user -> running -> completed
                                                   \-> failed
ready ----------------------------------------------> running

input-streaming means the model is still producing tool arguments. During this phase, append tool_input Deltas to the Tool Part's input_text; the ready Part is the corrective snapshot after input completes and carries the parsed input. Key UI elements by tool_call_id or part_id; do not append a new item for every Delta.

See Tool approvals for approval handling.

Lifecycle boundary

unsubscribe() stops only future events. It does not stop a Session, Turn, or tool. Call it on route, component, or Session changes; when returning to the Session, reload snapshots and subscribe again.

Reconnecting requires more than subscribing again. See Reconnect and sync.