Sessions & Hydration
A Session groups identity, application context, projections, channels, and related workflow executions. An execution preserves the progress of one workflow run; a Session preserves continuity across application activity.
Sessions are not chat transcripts and do not ask a model to remember application truth.
Choose the Correct Entry Point
Invariant exposes two intentionally different session entry points:
| API | Behavior | Use when |
|---|---|---|
app.sessions.createSession(userId, initialContext?) | Creates a process-local Session with a Host-generated UUID | Tests and short-lived processes |
await app.sessions.createDurableSession(userId, initialContext?) | Creates and persists a new UUID Session | New production continuity |
app.sessions.getOrCreateSession(id, userId, initialContext?) | Synchronous, process-local lookup or creation | Tests, short-lived processes, or context that the application manages itself |
await app.sessions.loadOrCreateSession(id, userId, initialContext?) | Loads stored context and its last registered execution; if absent, runs hydrate() once and persists the result | Durable application continuity |
await app.sessions.restoreSession(id, userId) | Restores only an existing owned durable Session; returns undefined when no stored row exists | Request/SSE recovery after a process restart |
ts
const session = await app.sessions.createDurableSession(
"usr_441",
{ preferredLanguage: "en" },
);
const sessionId = session.sessionId; // UUID; return it to the authorized clientThe async method is the canonical production entry point. It checks RuntimeStore.loadSession() before invoking the hydrator, so an existing Session does not re-fetch external context on every request. Workflows created with app.workflow() are registered automatically. When the stored Session has an activeRunId, the SDK loads that execution and reconstructs its current .wait() boundary from the registered graph.
Identity, Ownership, and Revisions
When Invariant creates a Session, the Host/SDK generates its ID with crypto.randomUUID() before any durable write. Explicit-ID loaders remain for applications that already persisted a UUID, but Session IDs are identifiers, not authentication credentials.
Every Session is owned by one userId. The synchronous getOrCreateSession() rejects an ownership mismatch in its process-local registry. The asynchronous loadOrCreateSession() and restoreSession() additionally compare the requested owner with durable storage before exposing context or a bound run. The application must derive userId from authenticated request context; never trust a user-supplied owner field.
Snapshots expose three independent counters:
| Revision | Advances when | Does not mean |
|---|---|---|
sessionRevision | Context or Session-to-run binding changes | Workflow event progression |
contextRevision | Developer-owned Session context changes | A run advanced |
executionRevision | Durable events advance the current run | Context changed |
This separation prevents updateContext() from creating a gap in event sequence or corrupting execution OCC. Agent repair authority pins the Session/context and execution revisions relevant to the proposal.
Hydrate Authoritative Context
Configure hydrate() to import facts from authoritative application systems:
ts
import { invariant } from "@invariant-tech/sdk";
import { sqlite } from "@invariant-tech/sqlite";
interface SupportContext extends Record<string, unknown> {
customerId: string;
accountTier: "FREE" | "PRO" | "ENTERPRISE";
preferredLanguage: string;
}
export const app = invariant<SupportContext>({
storage: sqlite("./data/invariant.db"),
session: {
hydrate: async ({ userId }) => {
const customer = await crm.getCustomer(userId);
return {
customerId: customer.id,
accountTier: customer.accountTier,
preferredLanguage: customer.preferredLanguage,
};
},
},
});Hydration is a Session lifecycle read. Workflow-side external I/O belongs in .capability() nodes.
Session Context Schema Status
SessionConfig.contextSchema is reserved authoring metadata in the current Beta. Although the public type accepts the field, the Session runtime does not evaluate it at any context boundary:
initialContextpassed to either Session entry point- context returned by
hydrate()orrehydrate() - context loaded from a
RuntimeStore - patches passed to
updateContext()
Do not treat contextSchema as a runtime security or data-integrity boundary. The invariant<TSessionContext>() generic provides compile-time checking for typed application code only. Validate untrusted and persisted data in application code before passing it to Invariant.
Runtime Session schema validation is reserved for a future contract that defines supported schema dialects, validation timing, stored-data migration behavior, and public validation errors.
Update and Rehydrate Context
updateContext() applies an explicit application change and persists it when a store is configured:
ts
await session.updateContext({ preferredLanguage: "es" });rehydrate() deliberately fetches fresh authoritative context through the configured hydrator and persists it:
ts
await session.rehydrate();Use updateContext() when the application knows the new fact. Use rehydrate() when the application must rediscover current facts from an external source.
Session Context vs. Execution State
| Session context | Execution state |
|---|---|
| Shared across related activity | Belongs to one runId |
| Identity, tenant, preferences, permissions | Workflow input and accumulated node outputs |
Loaded with loadOrCreateSession() | Reduced from workflow events |
| Updated or rehydrated explicitly | Advanced only by valid runtime transitions |
Durable Continuity vs. Conversation Transcripts
Doctrine: Session preserves durable continuity. Conversation history is an input to reasoning, not automatically part of durable runtime truth.
Session context is durable application context. Conversation transcripts are not automatically persisted by Invariant Beta.
text
Durable today:
- Session context (identity, profile, preferences, permissions)
- Workflow state (accumulated step data, active boundary, variables)
- Runtime events (immutable audit trail of transitions and choices)
- Outbox commands (transactional integration side-effects)
- Leases (distributed worker concurrency tokens)
Not automatically durable:
- Raw chat transcript / message historyTranscript Persistence Strategies
If your application requires long-term chat transcript history across process restarts:
- Dedicated Database Table or Message Store (Recommended): Persist user and assistant turns in a dedicated relational table (e.g.
session_messages (id, session_id, role, content, created_at)) or an external conversation store. - Session Context Array (
session.context.messages): Technically possible for lightweight prototypes, but not recommended as a general pattern for production, as growing message arrays inflate Session load and hydration payloads.
IMPORTANT
Persisting a transcript does not mean sending the entire transcript back to the model.
Applications should project only the recent or relevant conversational context required for the current reasoning boundary (e.g., via conversation: { recentTurns: 8 } or custom projection models).
Start and Continue an In-Memory Session Workflow
ts
const run = await session.startWorkflow(refundWorkflow, {
orderId: "ord_99",
});
const snapshot = session.snapshot();
if (snapshot.awaitedInput) {
await session.submitInput({ approved: true });
}session.startWorkflow() executes through the kernel and configured store. session.submitInput() validates the payload against the active .wait() schema before committing it.
A Session owns at most one non-terminal run. Starting another run directly while the current run is pending, running, waiting, or cancelling throws ActiveSessionRunError. SQLite and PostgreSQL also compare-and-swap the Session binding in the initial transition, so two processes racing to start different runs cannot both commit; the loser rolls back its execution, events, commands, and binding.
The low-level session.startWorkflow() return value is deliberately an admission receipt in the current Beta:
ts
const receipt = await session.startWorkflow(refundWorkflow, input);
// { runId, status: "started" }The method drains synchronous graph work before resolving, but the literal "started" is not a snapshot of the final execution state. Read session.snapshot() when using the low-level Session API. By contrast, the start_workflow Agent action returns SettledExecution, which describes the exact committed wait or terminal boundary caused by that action.
With a durable store, loadOrCreateSession() and restoreSession() restore the Session's last activeRunId. If the corresponding workflow graph is registered in the new process, a run paused at .wait() can accept its next validated input with the same revision and OCC contract. This is boundary reattachment, not a background recovery worker: interrupted capability dispatch, retries, timers, and compensation still require application orchestration; see the Recovery Contract Matrix.
Projections and Streaming
ts
const view = session.project(customerSupportProjection);
for await (const nextView of session.subscribe(customerSupportProjection)) {
sendToClient(nextView);
}session.snapshot() returns the current context, active workflow metadata, execution revision, projected state, and awaited input boundary. Projections derive consumer-specific read models without mutating runtime truth.
Session API
| Method | Result |
|---|---|
snapshot() | Fresh read-only snapshot with separate Session, context, and execution revisions; nested application values are not deep-frozen |
startWorkflow(workflow, input) | Admission receipt { runId, status: "started" }; synchronous work is drained, but the status is not the settled execution snapshot |
submitInput(payload) | { submitted, activeRunId? } |
cancelWorkflow(reason?) | { cancelled, status? } |
updateContext(patch) | Persisted updated context when storage is configured |
rehydrate() | Fresh hydrated and persisted context |
app.sessions.restoreSession(id, userId) | Existing owned durable Session and its registered active execution, or undefined |
project(projection) | Synchronous derived view |
subscribe(projection) | Async stream of derived views |
subscribeProjection(listener) | Raw projection-feed subscription |
getProjectionHistory(afterCursor?) | In-process projection feed history for reconnection |
Next Steps
- Database Schema & Data Lifecycle — Understand the physical Session row, CAS semantics, migrations, and retention.
- Projections — Derive channel-specific views.
- Agents & Action Authority — Expose bounded Session and runtime context to models.
- State & Event Sourcing — Understand execution truth and persistence.