# Session lifecycle (/docs/session-lifecycle)



A session is the unit of persistence and identity. It carries the
locked provider and model, the message history, the channel state,
and the checkpoint chain. The runtime is per-process; the session
is per-conversation. Every turn runs against one session id, and
every audit row joins back to it.

This page walks the session through its lifecycle. Per-turn
execution flow — the stream events, the tool loop, the synthesis
stage — lives on [Turn lifecycle](/docs/turn-lifecycle).

## The runtime-lifecycle cluster [#the-runtime-lifecycle-cluster]

Session lifecycle is one of three concepts paired with
[Turn lifecycle](/docs/turn-lifecycle) (the per-message arc) and
[Event log](/docs/event-log) (the append-only stream both arcs
write into). Sessions outlive turns, turns outlive their stream
frames, and the event log outlives both. The full triplet
framing lives at
[Concept clusters → Runtime-lifecycle](/docs/concept-clusters#runtime-lifecycle-cluster);
the rest of this page is the deep dive on the session arc itself.

## Minting a session [#minting-a-session]

```typescript
const session = await runtime.createSession({
  provider: { type: "anthropic" },
  model:    { id: "claude-sonnet-4-20250514" },
  tools:    { enabled: ["search", "calculator"] },
})
```

`createSession` takes a `Partial<SessionConfig>` and writes the new
session to the storage adapter before resolving. The returned
`Session` is your handle — `session.id` is a UUIDv7 the runtime
generates, and it's what every subsequent call takes.

Three things land on storage during `createSession`:

* The session row itself (id, owner, createdAt, locked
  provider+model).
* An initial version-vector entry keyed on the runtime's
  `clientId`.
* An empty channel set ready for the first turn's writes.

Provider and model are locked at this point. A session keeps the
same `(provider, model)` pair for its lifetime — switching models
mid-conversation means a new session, not a mutation. See
[SessionRuntime](/docs/session-runtime#creating-a-session) for the
full config table and
[`POST /api/harness/sessions`](/docs/api-routes) for the HTTP wire
shape.

## Resuming a session [#resuming-a-session]

```typescript
const session = await runtime.resumeSession(sessionId)
```

Resume rebuilds a session into the runtime from three layers:

1. **The storage row.** The adapter reads the session record —
   owner, locked provider/model, version vector, last-active
   timestamp, and the durable `SessionState` (messages, pending tool
   calls, artifacts, jobs). This is the base state.
2. **The latest checkpoint.** When a `checkpointer` is wired and
   holds a snapshot **at least as recent as the storage row**, the
   runtime overlays it — the recovery path for a crash that landed a
   checkpoint but lost the row write. The version guard means an
   older checkpoint never rolls back a fresher row; the checkpoint
   snapshot is authoritative when it wins. (Making the event log the
   canonical *message/state* reader is a separate forward-looking
   direction — that projection currently runs as a behavior-free
   shadow, gated off by default.)
3. **The event log (server only).** When the runtime can read the
   persisted log (a service-role server adapter), `hydrateFromEvents`
   rebuilds the **ephemeral card-lifecycle state** the `SessionState`
   row does not carry — resolved interrupts, subagents, sandbox
   exports, and user cards. Because that state is *derived* from the
   durable log, it rides the returned Session's transient
   `hydratedHarnessState` field and is **not** written back to the
   row. The browser / in-memory path skips this layer. See
   [Event log](/docs/event-log#hydratefromevents) for the walk.

Resume reconstructs only what storage durably held. The default
in-memory adapter keeps everything in this process's heap — restart
the process (or land on a fresh serverless instance) and there is
nothing to resume. Durability is the host's storage-adapter choice,
not an automatic guarantee; wire a durable adapter
(Supabase / Postgres / Redis) for resume to survive a restart. See
[Storage backends](#storage-backends) below.

Resume is idempotent. Calling it twice with the same id returns
two `Session` instances pointing at the same persisted state — the
later call wins as the runtime's `getActiveSession()`.

A resumed session keeps its original `id`, its `clientId` lineage,
and every prior audit row. The audit ledger doesn't fork on
resume; it appends.

## Aborting an in-flight turn [#aborting-an-in-flight-turn]

```typescript
const ctrl = new AbortController()
setTimeout(() => ctrl.abort(), 30_000)

for await (const event of runtime.executeMessage(
  session.id,
  prompt,
  { abortSignal: ctrl.signal },
)) {
  // ...
}
```

The `AbortController` propagates into the per-call provider stream
and into every active subagent under the turn. The runtime cancels
in-flight network calls, unwinds the tool loop, and flushes a
final audit row with `outcome.status: "user-aborted"` — recorded
distinctly from `provider-error` at both the tool-loop decision call
and the synthesis terminal, so cost rollups and compliance can tell
caller cancellation apart from real provider failure.

Aborting does not delete the session. The session row stays; the
partial turn lands an audit row carrying the same
`(sessionId, turnId, stageId, seqWithinTurn)` identity tuple as a
successful row — so per-turn cost rollups still count the prefill
tokens the provider already consumed. The next `executeMessage` on
the same `sessionId` continues from the post-abort state.

See [SessionRuntime · Aborting a turn](/docs/session-runtime#aborting-a-turn)
for the code path inside the runtime.

## Time-travel and rollback [#time-travel-and-rollback]

The rollback API itself and the rollback-vs-fork choice live on
[Checkpointing](/docs/checkpointing#rolling-back-to-a-checkpoint).
Two session-level invariants matter here:

* **Audit rows are never rolled back.** The ledger is append-only
  by contract — `ProviderDecisionLedger` has no `update` or
  `delete` primitive. A rolled-back turn's calls stay in the
  ledger; the rollback is a new event downstream of them.
* **Subagent provenance is preserved.** Audit rows for subagents
  nested under the rolled-back point keep their `parentTurnId` and
  `subagentDepth`, so a rollback can be replayed and diffed against
  the original branch.

Fork is the non-destructive sibling — it mints a new `sessionId`
pointing at the same checkpoint, so the original transcript stays
intact. See [Checkpointing](/docs/checkpointing) for the rollback
and fork APIs, and [Time travel](/docs/time-travel) for the eval
and replay use cases.

## Conflict resolution (multi-client) [#conflict-resolution-multi-client]

Two clients writing the same session bump their respective
version-vector entries. When one pushes to the server, the
coordinator compares vectors — a `concurrent` outcome is settled by
`SyncCoordinator.resolveConflicts(local, remote)` using
last-writer-wins on `updatedAt`, rather than clobbering by arrival
order. This is the self-host resolution path in open `@pleach/core`.

Interactive conflict resolution is enterprise-tier / planned, not
shipped in the open package:

```typescript
// Enterprise-tier / planned — NOT functional in open @pleach/core.
await runtime.resolveConflict(sessionId, conflictId, "local")
// or "remote"
```

In `@pleach/core`, `runtime.resolveConflict` is a stub: it logs and
returns a receipt without merging or persisting, and no `sync.conflict`
stream event is emitted to hand it a `conflictId`. CRDT merge and the
interactive conflict surface are the enterprise upgrade. For the
working self-host path, call
`SyncCoordinator.resolveConflicts(local, remote)` on your push
handler. See [Sync](/docs/sync) for the version-vector math, the
durable outbox, and the 3xxx error code range.

## Deleting a session [#deleting-a-session]

```typescript
await runtime.deleteSession(sessionId)
```

Delete cascades into the checkpointer when one is wired — every
checkpoint for the session drops. The session row is removed from
storage and `getActiveSession()` returns `null` if it pointed at
the deleted id.

The audit ledger is the exception. Audit rows survive delete
because the ledger is append-only by contract; that's what makes
`ProviderDecisionLedger` replayable and what gives finance and
compliance a row set that can't be silently retroactively edited.

Subject-key-derived deletion of audit data goes through the
`GDPRSoftDelete` plug-point. It leaves the `recordId` in place (so
the hash chain holds) and clears the identifying fields under the
subject key — satisfying a deletion request without breaking the
append-only invariant. See
[Audit ledger · The three compliance plug-points](/docs/audit-ledger#the-three-compliance-plug-points).

## Storage backends [#storage-backends]

The storage adapter is what `createSession` writes to and what
`resumeSession` reads from. Pick by environment.

| Adapter            | Environment   | Use for                                                     |
| ------------------ | ------------- | ----------------------------------------------------------- |
| `MemoryAdapter`    | Any           | Tests, local dev, ephemeral demos                           |
| `IndexedDBAdapter` | Browser       | Offline-first apps, browser extensions, multi-device drafts |
| `SupabaseAdapter`  | Server (Node) | Persistent storage, multi-tenant RLS, production            |

`MemoryAdapter` is the zero-config default — and it is
**non-durable**: session state (messages, checkpoints, event log)
lives in the process heap, is lost on restart, and is not shared
across instances. On serverless, each cold start or new instance
begins from empty state. The quickstart route
(`createPleachRoute`) emits a one-time startup warning when it
falls back to this adapter. Production hosts should supply a
durable adapter — durability is the host's storage-adapter choice,
not something the runtime provides automatically.

All three implement the same `StorageAdapter` interface, so
swapping is a one-line constructor change. See
[Storage](/docs/storage) for the per-adapter config and the RLS
template that `SupabaseAdapter` parameterizes.

## Lifecycle method index [#lifecycle-method-index]

| Method                 | Signature                                                    | When to reach for it                                                                                                                                     |
| ---------------------- | ------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `createSession`        | `(config?: Partial<SessionConfig>) => Promise<Session>`      | Starting a new conversation — locks provider and model                                                                                                   |
| `resumeSession`        | `(sessionId: string) => Promise<Session>`                    | Loading a session from a prior process or another tab                                                                                                    |
| `saveSession`          | `(state: SessionState) => Promise<void>`                     | Flushing an explicit state mutation outside the normal turn write path                                                                                   |
| `deleteSession`        | `(sessionId: string) => Promise<void>`                       | Removing the session and its checkpoints (audit rows stay)                                                                                               |
| `getActiveSession`     | `() => Session \| null`                                      | Reading the last session the runtime loaded                                                                                                              |
| `rollbackToCheckpoint` | `(sessionId, checkpointId) => Promise<Session>`              | Rewinding a session in place (deprecated alias for `runtime.checkpoints.rollback`)                                                                       |
| `resolveConflict`      | `(sessionId, conflictId, "local" \| "remote") => Promise<…>` | Enterprise-tier / planned — a stub in open `@pleach/core` (logs, no merge). Use `SyncCoordinator.resolveConflicts` for the working last-writer-wins path |
| `destroy`              | `() => Promise<void>`                                        | Tearing the runtime down — stream manager, interrupt manager, listeners                                                                                  |

The same flat-method-to-facet deprecation contract applies here:
`rollbackToCheckpoint` redirects to `runtime.checkpoints.rollback`,
and flat methods stay callable through one minor cycle before the
next major drops them. See
[SessionRuntime · Session lifecycle methods](/docs/session-runtime#session-lifecycle-methods)
for the canonical signature reference.

## Where to go next [#where-to-go-next]

<Cards>
  <Card title="Turn lifecycle" href="/docs/turn-lifecycle" description="What happens inside one executeMessage call — stages, stream events, the tool loop." />

  <Card title="SessionRuntime" href="/docs/session-runtime" description="The constructor config, every accessor method, and the full strategy slot table." />

  <Card title="API routes" href="/docs/api-routes" description="The HTTP + SSE wire shapes for create / get / put / delete / sync." />

  <Card title="Checkpointing" href="/docs/checkpointing" description="Snapshot writes, the rollback and fork APIs, and the Checkpointer contract." />

  <Card title="Sync" href="/docs/sync" description="Version vectors, the durable outbox, and the 3xxx error code range." />
</Cards>
