Durable Sessions
Exact session, getSession, handle, subscriptions, streams, lifecycle, inspection, stats, and Runtime read model contracts.
Durable Sessions are keyed, restart-safe owners for one Agent or one exported Flow. Import from the package root or the focused subpath:
import {
session,
getSession,
type Session,
type SessionFor,
type SessionForTarget,
type SessionOptions,
type FlowSessionOptions,
type SessionTurnHandle,
type SessionStatus,
type SessionInspection,
type SessionSubscription,
type SessionEvent,
type SessionStreamOptions,
type SessionRuntimeReadModel,
SessionIdentityConflictError,
SessionNotFoundError,
SessionInputError,
SessionCapabilityError,
SessionClosedError,
SessionDeletedError,
SessionLifecycleError,
SessionNotClosedError,
SessionTombstonedError,
GenerationModelBindingError,
GenerationModelNotStaticError,
GenerationModelCapabilityError,
} from "@use-crux/core";
// equivalent focused surface
import { session, getSession } from "@use-crux/core/session";session(target, options)
// Agent
function session<TAgent extends AnyAgent, TModel extends GenerationModel | undefined>(
target: TAgent,
options: SessionOptions<TAgent, TModel> & SessionModelGuard<TAgent, TModel>,
): Promise<SessionForTarget<TAgent>>;
// Flow
function session<TFlow extends AnyFlowTarget>(
target: TFlow,
options: FlowSessionOptions,
): Promise<SessionForTarget<TFlow>>;Create or reopen one inert keyed Session for target.
| Field | Required | Meaning |
|---|---|---|
options.key | yes | Non-empty caller key bound to one target within the active Runtime namespace |
options.model | Agent only, when Agent has no bound model | Immutable Session-level GenerationModel override |
Promise timing: resolves after durable preparation and Thread-owner registration complete. It does not execute the target.
Throws before Session / Work / Thread mutation (Agent):
| Error | Code |
|---|---|
SessionIdentityConflictError | SESSION_IDENTITY_CONFLICT |
SessionCapabilityError | SESSION_UNSUPPORTED |
SessionTombstonedError | SESSION_TOMBSTONED |
GenerationModelBindingError | GENERATION_MODEL_BINDING_MISSING |
GenerationModelNotStaticError | GENERATION_MODEL_NOT_STATIC |
GenerationModelCapabilityError | GENERATION_CAPABILITY_MISSING |
TypeError | empty key |
Flow Sessions also reject when the Flow is not exported by the bound
Runtime program (TARGET_NOT_EXPORTED).
Requires an active Work host (createWorkHost(...).run(...)) and Runtime store
with Sessions support, plus a configured linearizable Thread RecordStore.
getSession(target, key)
function getSession<TTarget extends SessionTarget>(
target: TTarget,
key: string,
): Promise<SessionForTarget<TTarget>>;Retrieve an existing Session without creating one. Repairs interrupted owner preparation when a compatible record exists.
Types
SessionOptions / FlowSessionOptions / SessionModelGuard
type SessionOptions<A extends AnyAgent, M extends GenerationModel | undefined> = {
readonly key: string;
} & (
| { readonly model?: M } // when Agent already has a GenerationModel
| { readonly model: M } // when Agent has no GenerationModel
);
type FlowSessionOptions = {
readonly key: string;
};SessionModelGuard is a compile-time Agent model compatibility check. Exact
missing language facets become a false type; broad capability evidence remains
accepted for runtime preflight.
Session<TInput, TOutput> / SessionForTarget
interface Session<TInput, TOutput = unknown> {
readonly id: string;
readonly thread: SessionThreadView;
readonly forkedFrom?: SessionForkLineage;
send(input: TInput): Promise<SessionTurnHandle<TOutput>>;
sendMany(
inputs: readonly TInput[],
): Promise<readonly SessionTurnHandle<TOutput>[]>;
status(): Promise<SessionStatus>;
inspect(): Promise<SessionInspection>;
stats(): Promise<ExecutionStats>;
stream(options?: SessionStreamOptions): AsyncIterable<SessionEvent>;
close(): Promise<void>;
kill(): Promise<void>;
delete(): Promise<void>;
fork(): Promise<this>;
clone(): Promise<this>;
forks(): Promise<readonly SessionForkSummary[]>;
}
// Agent and Flow handles also carry:
// targetKind: "agent" | "flow"
// subscribe / subscriptions (Signal surface)SessionForTarget<T> retains exact input/output inference for Agent Prompt
schemas and Flow definition types. Prefer it over SessionFor when the target
may be a Flow.
SessionThreadView
interface SessionThreadView {
readonly id: string;
read(options?: ThreadReadOptions): Promise<ThreadSnapshot>;
}Read-only owner-scoped Thread view. Reads only finalized Session heads. There
is no public append/edit/delete surface on this view. After Session delete
unregisters the owner, read() returns an empty owner path without resurrection.
Input and turn handles
interface SessionInputHandle {
readonly id: string;
readonly cursor: string;
readonly acceptedAt: Date;
}
interface SessionTurnHandle<TOutput> extends SessionInputHandle {
work(): Promise<WorkHandle<TOutput>>;
result(): Promise<TOutput>;
}| Method | Promise timing |
|---|---|
send / sendMany | Durable acceptance only |
work() | After an activation opportunity links this input to Work |
result() | After the linked Work publishes its exact terminal output |
sendMany([]) resolves to a frozen empty array without allocating cursors.
Invalid members fail the entire batch with SESSION_INPUT_INVALID before any
cursor advances.
Signal subscriptions
interface SessionSubscription {
readonly id: string;
readonly signalId: string;
readonly matchKey: string;
readonly state: "active" | "unsubscribed";
unsubscribe(): Promise<void>;
}
// on Session handles that include the subscription surface:
subscribe(source: Signal | MatchFilter): Promise<SessionSubscription>;
subscriptions(): Promise<readonly SessionSubscription[]>;Idempotent by Session + signal id + canonical match key. Predicate Signal views
are rejected; use bare Signals or signal.when({ ...match }).
Streams
interface SessionStreamOptions {
readonly after?: string;
}
type SessionEvent =
| { type: "session.snapshot"; reason: "initial" | "cursor-expired"; status: SessionStatus; /* … */ }
| { type: "session.status"; status: SessionStatus; /* … */ }
| { type: "ingress.accepted"; ingress: SessionIngressSummary; /* … */ }
| { type: "ingress.delivered"; ingress: SessionIngressSummary; stepIndex: number; workId?: string; /* … */ };Without after, emits session.snapshot (initial) then retained history.
A valid after resumes strictly after that cursor. An expired/unknown after
emits session.snapshot (cursor-expired) then continues from the earliest
retained event. Closed Sessions end after the terminal session.status event.
Lifecycle
| Method | Semantics |
|---|---|
close() | Joinable ordered barrier; seals send/subscribe; deactivates subscriptions; drains represented pending obligations |
kill() | Fenced fast terminalization; revokes commit authority; projects as closed |
delete() | After closed/killed only; tombstones key; unregisters Thread owner |
fork() / clone() | New owner/head with immutable lineage; never aliases mutable head |
forks() | Direct children created by fork/clone |
SessionStatus
interface SessionStatus {
readonly state: "parked" | "running" | "blocked" | "closing" | "closed";
readonly acceptedCursor?: string;
readonly processedCursor?: string;
readonly pendingInputs: number;
readonly pendingWork: number;
}Killed Sessions project as closed. Deleted Sessions reject status reads.
SessionInspection
interface SessionInspection {
readonly id: string;
readonly targetId: string;
readonly threadId: string;
readonly wakePending: boolean;
readonly inputs: readonly SessionInputInspection[];
readonly checkpoint?: SessionCheckpointInspection;
readonly recovery?: SessionRecoveryDiagnostic;
readonly coverage: { readonly inputs: "complete" | "truncated"; readonly limit: 64 };
}Public handle inspection may list sealed request identities for operator
correlation. Runtime Bridge serialization replaces that list with a bounded
requestCount and never exposes request payloads.
stats()
Returns the shared ExecutionStats /
ScopeStats aggregate for the complete addressed Session lifetime, including
exact ingress totals under inputs
(accepted / deduplicated / delivered / resumed / dropped) with
first-64 identity attribution.
GenerationModel precedence (Agent only)
- Optional immutable Session
modeloverride supplied tosession(). - Otherwise the Agent's bound
model.
The selected model must be declared on the active
RuntimeProgram.generationModels
(or generated equivalent). Durable state stores only
{ definitionId, fingerprint }. See
GenerationModel.
SessionRuntimeReadModel
Closed JSON-safe projection used by the Runtime Bridge and session.turn
observability. Schema version is 1.
interface SessionRuntimeReadModel {
readonly schema: 1;
readonly identity: {
readonly sessionId: string;
readonly keyHash: string;
readonly targetId: string;
readonly targetKind: "agent" | "flow";
readonly threadId: string;
};
readonly status: SessionStatus;
readonly wakePending: boolean;
readonly activation?: { readonly inputId: string; readonly workId: string };
readonly forkedFrom?: SessionForkLineage;
readonly thread: { readonly revision: string };
readonly subscriptions: readonly SessionRuntimeSubscription[];
readonly inputs: readonly SessionRuntimeInput[];
readonly checkpoint?: SessionRuntimeCheckpoint;
readonly recovery?: SessionRecoveryDiagnostic;
readonly coverage: { readonly inputs: "complete" | "truncated"; readonly limit: 64 };
readonly stats: ExecutionStats;
}Dates serialize as ISO strings. Checkpoints expose requestCount, not sealed
request ids. Subscriptions, lineage, and statistics come from the same Session
ports and ledger the Runtime already persists — not a second source of truth.
The projection never includes prompts, inputs, outputs, reasoning, Tool
arguments, credentials, or provider-native objects.
Capability and store requirements
| Requirement | Failure |
|---|---|
| Runtime store with Sessions port | SESSION_UNSUPPORTED |
| Active Work host / Runtime binding | host/runtime errors from Work admission |
Linearizable Thread RecordStore | Thread owner registration / commit failures |
| Declared generation models (Agent) | GENERATION_MODEL_* codes above |
| Exported Flow target (Flow) | TARGET_NOT_EXPORTED |
Subscription port when using subscribe | SESSION_UNSUPPORTED / capability error |
PostgreSQL deployments must initialize Runtime storage and
postgresRecordStore against the same database. Convex deployments use the
component Runtime and RecordStore together inside the host boundary.
runSessionConformanceTests
import { runSessionConformanceTests } from "@use-crux/core/runtime/testing";Provider-neutral factory used by memory, PostgreSQL, and Convex adapters. The harness injects only setup, reconstruction, fault, attempt, and observation seams; every law calls public Session, Thread, and Work APIs. Laws cover identity, ordering, subscriptions, lifecycle, fork lineage, stream cursors, and truthful capability reporting.
Errors
| Code | Class / surface |
|---|---|
SESSION_IDENTITY_CONFLICT | SessionIdentityConflictError |
SESSION_NOT_FOUND | SessionNotFoundError |
SESSION_INPUT_INVALID | SessionInputError |
SESSION_UNSUPPORTED | SessionCapabilityError |
SESSION_TOMBSTONED | SessionTombstonedError |
SESSION_CLOSED / lifecycle | SessionClosedError, SessionLifecycleError, SessionNotClosedError |
SESSION_DELETED | SessionDeletedError |
GENERATION_MODEL_BINDING_MISSING | GenerationModelBindingError |
GENERATION_MODEL_NOT_STATIC | GenerationModelNotStaticError |
GENERATION_CAPABILITY_MISSING | GenerationModelCapabilityError |
SESSION_TURN_RESULT_ARTIFACT_UNAVAILABLE | recovery diagnostic / Runtime error |