GatewaySessionRow with 32+ fieldsSession Management
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.
"running" | "done" | "failed" | "killed" | "timeout""direct" | "group" | "global" | "unknown"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.
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
| Field | Type | Description |
|---|---|---|
key | string | Unique session identifier (UUID v4) |
kind | SessionKind | Session classification: "direct", "group", "global", or "unknown" |
status | SessionRunStatus | Current run state of the session |
spawnedBy | string | User or agent that created this session |
displayName | string | Human-readable session name |
derivedTitle | string | null | Auto-generated title from first message |
lastMessagePreview | string | null | Truncated preview of the last message |
channel | string | Communication channel (e.g., "cli", "web", "api") |
Token & Cost Metrics
| Field | Type | Description |
|---|---|---|
inputTokens | number | Total input tokens consumed |
outputTokens | number | Total output tokens generated |
estimatedCostUsd | number | Running cost estimate in USD |
modelProvider | string | Provider name (e.g., "anthropic", "openai") |
model | string | Model identifier (e.g., "claude-sonnet-4-20250514") |
contextTokens | number | Context window size for the model |
Timing Fields
| Field | Type | Description |
|---|---|---|
startedAt | string | ISO 8601 timestamp of session creation |
endedAt | string | null | ISO 8601 timestamp of session termination |
runtimeMs | number | Total wall-clock runtime in milliseconds |
lastActivityAt | string | Timestamp 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 --> [*]
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 Field | Constraints |
|---|---|
displayName | Max 200 characters, non-empty |
status | Only valid forward transitions (e.g., running → killed) |
model | Must be a supported model identifier |
contextTokens | Must be within provider limits |
kind | Can only be set once from "unknown" |
{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
| Scenario | Behavior | Resolution |
|---|---|---|
| Patch on terminal session | Return {ok: false, reason: "session-frozen"} | Client must create a new session |
| Duplicate session key | Return {ok: false, reason: "key-conflict"} | Retry with new UUID |
| Gateway restart mid-session | Sessions in running state are recovered | Persistence layer replays events |
| Token count overflow | Capped at Number.MAX_SAFE_INTEGER | Extremely unlikely in practice |
| Cost estimation drift | Recalculated on each snapshot | Final cost computed at termination |
| Concurrent patches | Last-write-wins with optimistic locking | Retry on version conflict |
running state are recovered and their timers resumed.
Related Topics
- Routing Overview — full routing subsystem
- Message Dispatch — how RPC requests reach session handlers
- Assistant Identity — assistant resolution within sessions
- Auth — authentication precedes session creation
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()