Session Management

Focus

Full Session Lifecycle & State Tracking

The session management subsystem controls the entire lifecycle of gateway sessions — from creation through active use to termination. It tracks 32+ fields per session including token usage, cost estimation, runtime metrics, and message previews. Sessions transition through a well-defined state machine and every lifecycle event is persisted for observability.

Core Type GatewaySessionRow with 32+ fields
Run Statuses "running" | "done" | "failed" | "killed" | "timeout"
Session Kinds "direct" | "group" | "global" | "unknown"
Persistence persistGatewaySessionLifecycleEvent()

Table of Contents

Overview

Every interaction between a client and the Gateway occurs within the context of a session. Sessions are the fundamental unit of conversation state — they track which model is being used, how many tokens have been consumed, what the estimated cost is, and what the current run status is.

Sessions are not ephemeral WebSocket connections. A single session can span multiple reconnections, and its state is persisted across Gateway restarts. The GatewaySessionRow type captures all the metadata needed to resume, display, and bill for a session.

📌 Design principle: Sessions are append-only from a lifecycle perspective. Once a session reaches a terminal state (done, failed, killed, timeout), it is never modified again. This makes the session log a reliable audit trail.

GatewaySessionRow

The primary data structure representing a session. Each field is tracked and persisted throughout the session lifecycle.

Identity & Classification Fields

FieldTypeDescription
keystringUnique session identifier (UUID v4)
kindSessionKindSession classification: "direct", "group", "global", or "unknown"
statusSessionRunStatusCurrent run state of the session
spawnedBystringUser or agent that created this session
displayNamestringHuman-readable session name
derivedTitlestring | nullAuto-generated title from first message
lastMessagePreviewstring | nullTruncated preview of the last message
channelstringCommunication channel (e.g., "cli", "web", "api")

Token & Cost Metrics

FieldTypeDescription
inputTokensnumberTotal input tokens consumed
outputTokensnumberTotal output tokens generated
estimatedCostUsdnumberRunning cost estimate in USD
modelProviderstringProvider name (e.g., "anthropic", "openai")
modelstringModel identifier (e.g., "claude-sonnet-4-20250514")
contextTokensnumberContext window size for the model

Timing Fields

FieldTypeDescription
startedAtstringISO 8601 timestamp of session creation
endedAtstring | nullISO 8601 timestamp of session termination
runtimeMsnumberTotal wall-clock runtime in milliseconds
lastActivityAtstringTimestamp of last message or event

SessionRunStatus

Sessions transition through a well-defined set of statuses. The status field is the primary indicator of what a session is currently doing.

/**
 * The run status of a gateway session.
 * Terminal states: done, failed, killed, timeout
 * Active state: running
 */
type SessionRunStatus =
  | 'running'   // Session is actively processing
  | 'done'      // Session completed successfully
  | 'failed'    // Session encountered an unrecoverable error
  | 'killed'    // Session was manually terminated by user or admin
  | 'timeout';  // Session exceeded the maximum runtime limit

type SessionKind =
  | 'direct'    // One-on-one conversation with a single assistant
  | 'group'     // Multi-agent session with multiple assistants
  | 'global'    // System-wide broadcast session
  | 'unknown';  // Kind not yet determined (initial state)

Session State Machine

The following diagram shows all valid state transitions. Note that all paths eventually lead to a terminal state, and no transitions out of terminal states are allowed.

stateDiagram-v2
    [*] --> Created: Client requests new session
    Created --> Running: First message received
    Running --> Running: Additional messages processed
    Running --> Done: Session completed normally
    Running --> Failed: Unrecoverable error
    Running --> Killed: User or admin terminates
    Running --> Timeout: Max runtime exceeded
    Created --> Killed: Terminated before first message
    Created --> Timeout: Idle timeout before activation
    Done --> [*]
    Failed --> [*]
    Killed --> [*]
    Timeout --> [*]
⚠ Immutability rule: Once a session enters a terminal state (done, failed, killed, timeout), its GatewaySessionRow is frozen. Any subsequent writes are rejected. This guarantees that billing data and audit logs are immutable.

GatewaySessionsDefaults

When creating a new session, missing fields are filled from the defaults configuration.

interface GatewaySessionsDefaults {
  /** Default model provider (e.g., "anthropic") */
  modelProvider: string;

  /** Default model identifier (e.g., "claude-sonnet-4-20250514") */
  model: string;

  /** Default context window size in tokens */
  contextTokens: number;

  /** Default session kind when not specified */
  kind: SessionKind;

  /** Maximum runtime in milliseconds before timeout */
  maxRuntimeMs: number;

  /** Maximum idle time before auto-termination */
  idleTimeoutMs: number;
}

Session Lifecycle

Session Creation

A new session is created when a client sends a session.create request. The Gateway merges the provided fields with GatewaySessionsDefaults and persists the initial row.

function createSession(
  params: SessionCreateParams,
  defaults: GatewaySessionsDefaults
): GatewaySessionRow {
  const now = new Date().toISOString();
  return {
    key: generateSessionKey(),
    kind: params.kind ?? defaults.kind,
    status: 'running',
    spawnedBy: params.userId,
    displayName: params.displayName ?? 'Untitled Session',
    derivedTitle: null,
    lastMessagePreview: null,
    channel: params.channel ?? 'cli',
    inputTokens: 0,
    outputTokens: 0,
    estimatedCostUsd: 0,
    modelProvider: params.modelProvider ?? defaults.modelProvider,
    model: params.model ?? defaults.model,
    contextTokens: params.contextTokens ?? defaults.contextTokens,
    startedAt: now,
    endedAt: null,
    runtimeMs: 0,
    lastActivityAt: now,
  };
}

Lifecycle Event Persistence

Every state transition is recorded via persistGatewaySessionLifecycleEvent(). This function writes an immutable event record and updates the session row atomically.

async function persistGatewaySessionLifecycleEvent(
  sessionKey: string,
  event: SessionLifecycleEvent
): Promise<void> {
  const row = await loadSessionRow(sessionKey);
  if (isTerminalStatus(row.status)) {
    throw new Error(`Session ${sessionKey} is in terminal state: ${row.status}`);
  }

  const updatedRow: GatewaySessionRow = {
    ...row,
    status: event.newStatus ?? row.status,
    endedAt: event.newStatus && isTerminalStatus(event.newStatus)
      ? new Date().toISOString()
      : row.endedAt,
    runtimeMs: Date.now() - new Date(row.startedAt).getTime(),
    lastActivityAt: new Date().toISOString(),
  };

  await writeSessionRow(updatedRow);
  await appendLifecycleEvent(sessionKey, event);
}

Snapshot Derivation

The deriveGatewaySessionLifecycleSnapshot() function computes a read-only snapshot of the session at a point in time. Snapshots are used for UI display and API responses.

function deriveGatewaySessionLifecycleSnapshot(
  row: GatewaySessionRow
): GatewaySessionLifecycleSnapshot {
  return {
    key: row.key,
    kind: row.kind,
    status: row.status,
    displayName: row.derivedTitle ?? row.displayName,
    tokenUsage: {
      input: row.inputTokens,
      output: row.outputTokens,
      total: row.inputTokens + row.outputTokens,
    },
    cost: { estimatedUsd: row.estimatedCostUsd },
    timing: {
      startedAt: row.startedAt,
      endedAt: row.endedAt,
      runtimeMs: row.runtimeMs,
    },
    preview: row.lastMessagePreview,
    isTerminal: isTerminalStatus(row.status),
  };
}

Session Listing & Preview

Clients can list sessions and retrieve previews. The SessionsListResult and SessionsPreviewResult types support pagination and filtering.

interface SessionsListResult {
  sessions: GatewaySessionRow[];
  total: number;
  hasMore: boolean;
  cursor: string | null;
}

interface SessionPreviewItem {
  key: string;
  displayName: string;
  status: SessionRunStatus;
  lastMessagePreview: string | null;
  updatedAt: string;
}

interface SessionsPreviewEntry {
  item: SessionPreviewItem;
  snapshot: GatewaySessionLifecycleSnapshot;
}

interface SessionsPreviewResult {
  entries: SessionsPreviewEntry[];
  total: number;
  hasMore: boolean;
}

Lifecycle Sequence

The following diagram shows the complete lifecycle of a typical session from creation through completion.

sequenceDiagram
    participant C as Client
    participant GW as Gateway
    participant SM as Session Manager
    participant DB as Persistence Layer
    participant SN as Snapshot Engine

    C->>GW: session.create(params)
    GW->>SM: createSession(params, defaults)
    SM->>DB: writeSessionRow(newRow)
    DB-->>SM: ok
    SM->>DB: appendLifecycleEvent("created")
    SM-->>GW: GatewaySessionRow
    GW-->>C: {ok: true, session: row}

    C->>GW: message.send(sessionKey, content)
    GW->>SM: updateTokenUsage(sessionKey, tokens)
    SM->>DB: persistGatewaySessionLifecycleEvent(event)
    SM->>SN: deriveGatewaySessionLifecycleSnapshot(row)
    SN-->>SM: snapshot

    C->>GW: session.list(filters)
    GW->>SM: listSessions(filters)
    SM->>DB: querySessions(filters)
    DB-->>SM: SessionsListResult

    C->>GW: session.patch(sessionKey, {status: "killed"})
    GW->>SM: terminateSession(sessionKey, "killed")
    SM->>DB: persistGatewaySessionLifecycleEvent({newStatus: "killed"})
    SM->>DB: freezeSessionRow(sessionKey)
    DB-->>SM: ok
    SM-->>GW: SessionsPatchResult
    GW-->>C: {ok: true, patched: true}

SessionsPatchResult

Session updates use a patch model. Only mutable fields can be updated, and patches to terminal sessions are rejected.

Patchable FieldConstraints
displayNameMax 200 characters, non-empty
statusOnly valid forward transitions (e.g., running → killed)
modelMust be a supported model identifier
contextTokensMust be within provider limits
kindCan only be set once from "unknown"
✅ Idempotency: Patching a session to its current status is a no-op and returns {ok: true, patched: false, reason: "no-change"}. This makes it safe for clients to retry patch operations without side effects.

GatewaySessionLifecycleSnapshot

A read-only, computed view of a session used in API responses and UI rendering.

interface GatewaySessionLifecycleSnapshot {
  key: string;
  kind: SessionKind;
  status: SessionRunStatus;
  displayName: string;
  tokenUsage: {
    input: number;
    output: number;
    total: number;
  };
  cost: {
    estimatedUsd: number;
  };
  timing: {
    startedAt: string;
    endedAt: string | null;
    runtimeMs: number;
  };
  preview: string | null;
  isTerminal: boolean;
}

Edge Cases & Error Handling

ScenarioBehaviorResolution
Patch on terminal sessionReturn {ok: false, reason: "session-frozen"}Client must create a new session
Duplicate session keyReturn {ok: false, reason: "key-conflict"}Retry with new UUID
Gateway restart mid-sessionSessions in running state are recoveredPersistence layer replays events
Token count overflowCapped at Number.MAX_SAFE_INTEGERExtremely unlikely in practice
Cost estimation driftRecalculated on each snapshotFinal cost computed at termination
Concurrent patchesLast-write-wins with optimistic lockingRetry on version conflict
🚨 Data integrity: The persistence layer uses write-ahead logging to ensure that lifecycle events are never lost, even during crashes. If the Gateway restarts, sessions in running state are recovered and their timers resumed.

Directory Map

Source files for this topic
src/gateway/
├── sessions/
│   ├── index.ts               ← GatewaySessionRow, SessionRunStatus, SessionKind
│   ├── defaults.ts            ← GatewaySessionsDefaults
│   ├── lifecycle.ts           ← persistGatewaySessionLifecycleEvent()
│   ├── snapshot.ts            ← deriveGatewaySessionLifecycleSnapshot()
│   ├── list.ts                ← SessionsListResult, SessionsPreviewResult
│   └── patch.ts               ← SessionsPatchResult, patchSession()
├── protocol/
│   └── session-types.ts       ← SessionPreviewItem, SessionsPreviewEntry
└── persistence/
    └── session-store.ts       ← writeSessionRow(), loadSessionRow()