# SessionRuntime (/docs/session-runtime)



A `SessionRuntime` owns one user's sessions, the per-turn graph,
the stream pipeline, the `AuditableCall` ledger, and the plugin
set — it's the one class you instantiate from `@pleach/core`.
Everything else is configuration passed to its constructor or
methods called on the instance. See
[Storage](/docs/storage) for adapter choices and
[Stream events](/docs/stream-events) for what `executeMessage`
yields.

`SessionRuntime` is the implementation surface of the
[runtime-lifecycle cluster](/docs/session-lifecycle#the-runtime-lifecycle-cluster) —
it hosts the three lifecycle concepts (session lifecycle, turn
lifecycle, event log) under one constructor.

```typescript
import { SessionRuntime } from "@pleach/core";
```

<SourceMeta
  source="[
  { label: &#x22;src/SessionRuntime.ts&#x22;, href: &#x22;https://github.com/pleachhq/core/blob/main/src/SessionRuntime.ts&#x22; },
  { label: &#x22;src/runtime/&#x22;, href: &#x22;https://github.com/pleachhq/core/tree/main/src/runtime&#x22; },
]"
/>

## Construction [#construction]

```typescript
const runtime = new SessionRuntime(config);
```

All fields are optional. With no config, you get an in-memory
runtime with `userId: "anonymous"` — fine for a tutorial, never
fine for production.

### `SessionRuntimeConfig` fields [#sessionruntimeconfig-fields]

| Field                       | Type                        | Default               | Purpose                                                                                                                                                                                                                                                            |
| --------------------------- | --------------------------- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `storage`                   | `StorageAdapter`            | fresh `MemoryAdapter` | Where sessions persist. See [Storage](/docs/storage).                                                                                                                                                                                                              |
| `checkpointer`              | `Checkpointer`              | none                  | Where checkpoints persist. See [Checkpointing](/docs/checkpointing).                                                                                                                                                                                               |
| `eventLogWriter`            | `HarnessEventWriter`        | none                  | Write side of the durable event log.                                                                                                                                                                                                                               |
| `eventReader`               | `HarnessEventReader`        | none                  | Read side of the durable event log — backs `runtime.events.iterate` / `.fold` and `resumeSession`'s Layer-3 hydration. Store-agnostic; the host casts its store into it at construction. See [Backing the reader](/docs/event-log-projections#backing-the-reader). |
| `cache`                     | `StorageAdapter`            | none                  | Optional hot-data cache adapter                                                                                                                                                                                                                                    |
| `userId`                    | `string`                    | `"anonymous"`         | Session owner. Production deployments must pass this.                                                                                                                                                                                                              |
| `organizationId`            | `string`                    | –                     | Multi-tenant scoping                                                                                                                                                                                                                                               |
| `clientId`                  | `string`                    | generated             | Sync version-vector key                                                                                                                                                                                                                                            |
| `provider`                  | `AgentProvider`             | –                     | LLM provider implementation                                                                                                                                                                                                                                        |
| `plugins`                   | `HarnessPlugin[]`           | `[]`                  | Domain extensions. See [Plugin contract](/docs/plugin-contract).                                                                                                                                                                                                   |
| `metaToolNames`             | `Set<string>`               | empty set             | Meta-tool names the runtime treats as flow-control (e.g. `set_step_complete`, `wait_for_jobs`)                                                                                                                                                                     |
| `enabledSafetyPolicies`     | `readonly SafetyPolicyId[]` | `[]`                  | Operator opt-in for safety policies plugins registered. Canonical read surface is `runtime.safety.{listAvailable, getActive, getRegistry}`.                                                                                                                        |
| `seedCoreDefaults`          | `boolean`                   | `true`                | Auto-seed the 11 `core.*` prompt fragments                                                                                                                                                                                                                         |
| `interrupt`                 | `InterruptConfig`           | –                     | Human-in-the-loop configuration                                                                                                                                                                                                                                    |
| `enableSubagentConcurrency` | `boolean`                   | `false`               | Allow parallel subagents                                                                                                                                                                                                                                           |
| `maxConcurrentSubagents`    | `number`                    | `4`                   | Concurrency cap when enabled                                                                                                                                                                                                                                       |
| `chatId`                    | `string`                    | –                     | Provenance link to an upstream chat row                                                                                                                                                                                                                            |
| `isGuest` / `guestId`       | `boolean` / `string`        | –                     | Guest-mode session ownership                                                                                                                                                                                                                                       |
| `authToken`                 | `string`                    | –                     | Bearer token forwarded to API calls                                                                                                                                                                                                                                |
| `jobDispatchEndpoint`       | `string`                    | –                     | URL the runtime POSTs queued jobs to                                                                                                                                                                                                                               |

`enabledSafetyPolicies` participates in the fingerprint cache key —
toggling a policy invalidates the cache automatically.

`metaToolNames` is load-bearing after R-1.A. Hosts that don't pass
it see a one-shot `[UXParity:metaToolNames-config-missing]` probe
plus a startup warning, and continuation guards silently disable.
The continuation guards are the runtime nodes that decide whether a
named flow-control tool (`set_step_complete`, `wait_for_jobs`)
signals "this step is done" or "this is just another tool call";
when the guards disable, the tool loop keeps iterating on what
should have been a terminal signal, which is the symptom most hosts
notice before they read the probe.

## Strategy injection [#strategy-injection]

`SessionRuntimeConfig` also carries a second set of fields the
substrate calls **strategies** — typed slots the host fills with
domain code, each with one well-defined shape and one consumer.
A strategy that's unset gracefully degrades; the runtime stays
functional and the strategy-backed behavior goes inert.

Representative slots include `summaryExtractor`, `toolCatalog`,
`subagentExecutor`, `fabricationGuardStrategy`,
`hallucinationDetectorFactory`, and the orchestrator-side
provider-fallback registry. The full surface, the wiring patterns,
and the per-turn vs. per-runtime allocation rule live on
[Runtime strategies](/docs/runtime-strategies).

## Facets [#facets]

Each facet returns a typed slice of the runtime — `runtime.sessions`
for session lifecycle, `runtime.events` for the event log,
`runtime.sync` for bidirectional sync, `runtime.dev` for diagnostics.
Reading a facet gets you the methods grouped under that name and the
typed receipts those methods return.

Runtime-level facet inventory:

| Facet                      | Covers                                                                                                                                  |
| -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| `runtime.sessions`         | Session lifecycle — `create`, `resume`, `find`, `save`, `delete`, `updateProviderModel`                                                 |
| `runtime.sync`             | Bidirectional sync — `execute`, `resolveConflict`, `subscribeToStream`. Callable: `runtime.sync(sessionId)` preserves the legacy shape. |
| `runtime.events`           | Event-log iteration + fold + the live event bus (`on`, `once`, `off`)                                                                   |
| `runtime.spans`            | Span `start` / `flush` / `shutdown` plus `inFlightCount`, `isShutdown`, `snapshot` introspection                                        |
| `runtime.tenant`           | Tenant identity — frozen at construction; identity-stable across reads                                                                  |
| `runtime.plugins`          | `PluginManager` access plus the full `collect*` cohort and registry lookups                                                             |
| `runtime.prompts`          | Prompt-contribution introspection (`list`, `get`, `getAll`, `listByOrigin`)                                                             |
| `runtime.safety`           | `listActivePolicies`, `listAvailablePolicies`, `getRegistry`                                                                            |
| `runtime.tools`            | Registered tool catalog introspection                                                                                                   |
| `runtime.observerRouter`   | Late-binding stream-observer registration; receipts on `unregister`                                                                     |
| `runtime.degradation`      | Model degradation reads; `getDegradedModelRecord` returns a receipt                                                                     |
| `runtime.checkpoints`      | `rollback`, `list`                                                                                                                      |
| `runtime.timeTravel`       | Time-travel API accessor                                                                                                                |
| `runtime.async`            | Async-task manager, subagent spawn, result lookup                                                                                       |
| `runtime.interrupts`       | Interrupt manager; `resolve` returns an `InterruptResolveReceipt`                                                                       |
| `runtime.adapter`          | Orchestrator-adapter wiring + capability registry                                                                                       |
| `runtime.diagnostics`      | Boot-time readiness probes for the host-extension bag                                                                                   |
| `runtime.dev`              | Diagnostic sub-API — store, stream manager, repetition-guard wiring                                                                     |
| `runtime.graph.recovery`   | Per-turn recovery hooks                                                                                                                 |
| `runtime.graph.heuristics` | Tunable graph heuristics                                                                                                                |
| `runtime.graph.config`     | Graph-build configuration                                                                                                               |
| `runtime._internal`        | INTERNAL graph-consumption surface — substrate-only, do not call                                                                        |

### Receipt-shaped returns [#receipt-shaped-returns]

Mutating accessors across `sessions`, `sync`, `observerRouter`,
`degradation`, and `interrupts` return typed receipts rather than
booleans or `void`. The pattern is deliberate: every call site that
mutates runtime state echoes a structured outcome callers can branch
on, audit, or log without re-derivation.

Examples:

* `runtime.sessions.delete(sessionId)` → `SessionDeleteReceipt`
  (`deleted`, `sessionId`, `removedFromCache`).
* `runtime.sync.flushOutbox()` → `OutboxFlushReceipt` with a four-verdict
  `outcome` (`"all-flushed" | "partial" | "all-stuck" | "none-pending"`)
  plus `failedEntries`, `flushedAt`, and the pre-widening counts.
* `runtime.sync.resolveConflicts(...)` includes a `conflictedPaths` array
  describing every diverged field (`path`, `localValue`, `remoteValue`,
  `chosenSide`).
* `runtime.sync.resolveConflict(...)` → `SyncConflictResolveReceipt`
  (`resolved`, `sessionId`, `conflictId`, `resolution`, `resolvedAt`).
* `runtime.observerRouter.unregister(reg)` → `{ removed, wasRegistered }`.
* `runtime.degradation.getDegradedModelRecord(modelId)` → receipt with
  the active routing entry or `null` when none is held.
* `runtime.interrupts.resolve(id, decision)` → `InterruptResolveReceipt`.

### Deprecation contract [#deprecation-contract]

Existing flat methods stay around with `@deprecated` JSDoc that
points to the facet. For example, `runtime.getTenantId()` redirects
readers to `runtime.tenant.getTenantId()`, and
`runtime.getSafetyRegistry()` redirects to `runtime.safety.getRegistry()`.
The minor release that adds the facet also marks the flat method
`@deprecated`; the next major removes the flat method. Flat methods
are never removed in a minor bump.

For the full facet inventory, the `TurnOrchestrator.*` facet set,
and the audit gates that enforce coverage, see [Facets](/docs/facets).

## Creating a session [#creating-a-session]

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

`createSession` accepts a `Partial<SessionConfig>`. Defaults:

| Config field               | Default                              |
| -------------------------- | ------------------------------------ |
| `provider`                 | `{ type: "anthropic" }`              |
| `model`                    | `{ id: "claude-sonnet-4-20250514" }` |
| `tools`                    | `{ enabled: [] }`                    |
| `projectId` / `campaignId` | inherited from runtime if set        |

Returns a `Session` instance bound to a generated UUIDv7 id —
`session.id` is what `executeMessage` takes. The session is
written to storage before this resolves.

## Executing a message [#executing-a-message]

`executeMessage` is an async generator. Iterating it streams the
turn; every yielded value is a `StreamEvent`.

```typescript
for await (const event of runtime.executeMessage(session.id, "Hello")) {
  switch (event.type) {
    case "message.delta":
      process.stdout.write(event.delta);
      break;
    case "tool.completed":
      console.log("tool finished:", event.toolCall.name);
      break;
    case "error":
      console.error(event.code, event.error);
      break;
  }
}
```

### Signature [#signature]

```typescript
runtime.executeMessage(
  sessionId: string,
  content: string,
  options?: {
    fileReferences?: unknown[];
    abortSignal?: AbortSignal;
  },
): AsyncGenerator<StreamEvent>;
```

### Aborting a turn [#aborting-a-turn]

Pass an `AbortSignal`. The runtime cancels in-flight provider calls,
unwinds the tool loop cleanly, and flushes a final ledger row with
`outcome.status: "user-aborted"`. The user-aborted row carries the same
`(sessionId, turnId, stageId, seqWithinTurn)` identity tuple as a
successful row, which is what lets a per-turn cost rollup count an
abort against the same `turnId` as the calls that ran before it —
the aborted call still consumed input tokens through the provider's
prefill, and the row exists so that spend isn't invisible.

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

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

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

```typescript
await runtime.deleteSession(session.id);
```

Cascades into the checkpointer when one is wired. Audit-ledger rows
are not deleted — the ledger is append-only by contract, which is
why `ProviderDecisionLedger` has no `update` or `delete` primitive
in any conforming language. The `GDPRSoftDelete` plug-point on the
ledger interface (no-op default; production wiring lands in
`@pleach/compliance@0.1.0` alongside its scrubber cohort
and attestation surface) is the path for subject-key-derived redaction:
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. Wire
your own implementation today via `HarnessPlugin`.

## Flat method surface (legacy) [#flat-method-surface-legacy]

The tables below mirror the facets above as flat methods on the runtime; the methods are `@deprecated` and forward to their facet equivalents — prefer [Facets](#facets) for new code.

### Session lifecycle methods [#session-lifecycle-methods]

| Method                 | Signature                                                                             | Use                                                                                                                  |
| ---------------------- | ------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| `createSession`        | `(config?: Partial<SessionConfig>) => Promise<Session>`                               | Mint a new session, persist to storage                                                                               |
| `resumeSession`        | `(sessionId: string) => Promise<Session>`                                             | Load an existing session from storage                                                                                |
| `saveSession`          | `(state: SessionState) => Promise<void>`                                              | Flush an explicit state mutation to storage                                                                          |
| `deleteSession`        | `(sessionId: string) => Promise<SessionDeleteReceipt>`                                | Remove the session; cascades to checkpointer. Returns a receipt with `deleted`, `sessionId`, and `removedFromCache`. |
| `getActiveSession`     | `() => Session \| null`                                                               | Last session loaded into the runtime                                                                                 |
| `rollbackToCheckpoint` | `(sessionId, checkpointId) => Promise<Session>`                                       | Restore a session from a named checkpoint                                                                            |
| `resolveConflict`      | `(sessionId, conflictId, "local" \| "remote") => Promise<SyncConflictResolveReceipt>` | Resolve a sync conflict surfaced via `sync.conflict`. Returns a per-path receipt.                                    |
| `destroy`              | `() => Promise<void>`                                                                 | Tear down stream + interrupt managers, unsubscribe listeners                                                         |

### Stream subscription [#stream-subscription]

Two ways to consume events. `executeMessage` is the per-turn async
generator above. For cross-cutting subscribers, the runtime also
exposes a mode-filtered subscription via its embedded `StreamManager`.

```typescript
const unsubscribe = runtime.subscribeToStream("messages", (event) => {
  // event is StreamEvent — already filtered to the mode
});
```

| Mode         | Includes                                                                               |
| ------------ | -------------------------------------------------------------------------------------- |
| `"values"`   | `session.created`, `session.resumed`, `checkpoint.created`                             |
| `"updates"`  | `message.added`, `message.complete`, tool lifecycle, job lifecycle, `artifact.created` |
| `"messages"` | `message.delta`, `message.complete`, `thinking.delta`, `thinking.complete`             |
| `"custom"`   | Any event whose `type` starts with `custom.`                                           |
| `"debug"`    | All events — unfiltered                                                                |
| `"all"`      | All events — unfiltered                                                                |

`getStreamManager()` returns the underlying manager when you need to
set a throttle or an event-type allowlist directly:
`manager.setThrottleInterval(ms)`, `manager.setEventFilter(types)`.

### Accessor methods [#accessor-methods]

The runtime exposes its internal coordinators behind named getters.
Returns are `undefined` when the coordinator wasn't wired at
construction.

| Method                           | Returns                                                                                            |
| -------------------------------- | -------------------------------------------------------------------------------------------------- |
| `getInterruptManager()`          | `InterruptManager \| undefined` — present when `interrupt.enabled: true`                           |
| `resolveInterrupt(id, decision)` | `InterruptResolveReceipt` — submit an `ApprovalDecision` and receive a receipt echoing the outcome |
| `getTimeTravelApi()`             | Checkpoint list / restore API (see [Checkpointing](/docs/checkpointing))                           |
| `getAsyncTaskManager()`          | Background job manager (see [Stream events](/docs/stream-events#async-jobs))                       |
| `getPluginManager()`             | Registered plugin set                                                                              |
| `getStreamManager()`             | `StreamManager` — throttle + filter the live stream                                                |
| `getStore()`                     | The storage adapter passed at construction                                                         |
| `getSubagentManager()`           | `SubagentManager \| undefined` when concurrency is enabled                                         |
| `getCapabilityRegistry()`        | Registered capability strategies                                                                   |
| `getSafetyRegistry()`            | `SafetyPolicyRegistry`                                                                             |
| `listActiveSafetyPolicies()`     | `readonly SafetyPolicySummary[]`                                                                   |
| `getDegradedModels()`            | Models the degradation tracker is routing around                                                   |
| `isModelDegraded(modelId)`       | Boolean check for a single model                                                                   |
| `getPlanningContext()`           | Current planning context (advanced)                                                                |

The published `.d.ts` is the canonical signature reference; your IDE
autocompletes from it.

### Lifecycle events [#lifecycle-events]

`SessionRuntime` extends `EventEmitter`. The same shapes that come
out of `executeMessage` also fire as events, so a long-lived
runtime can attach cross-cutting subscribers without owning the
async iterator:

```typescript
runtime.on("checkpoint.created", (event) => {
  metrics.increment("checkpoints", { sessionId: event.checkpoint.sessionId });
});
```

The full event union is the `StreamEvent` type — see
[Stream events](/docs/stream-events) for the catalog.

## Plugin contributions [#plugin-contributions]

`runtime.plugins` is the canonical read surface for everything a
plugin contributed. Beyond `PluginManager` access, the facet exposes
a `collect*` cohort that mirrors `PluginManager.collect*` one-for-one
— roughly two dozen accessors covering retry policy, continuation
policy, fabrication detectors, finalization passes, intent
classifiers, tool-coupling hints, intent-tool maps, sandbox bridge,
interrupt UI handlers, citation rules, manifest providers, fabrication
guard, guest-denied tools, stream observers, hallucinated-tool
detectors, stream-chunk handlers, middleware, and runtime-aware
middleware. The full hook list lives on
[Plugin contract](/docs/plugin-contract).

```typescript
const retryPolicies = runtime.plugins.collectRetryPolicy();
const continuationPolicies = runtime.plugins.collectContinuationPolicy();
const detectors = runtime.plugins.collectFabricationDetectors(sessionId);
```

Two convenience accessors round out the facet:

* `runtime.plugins.listAvailablePromptContributions()` — every
  prompt contribution every plugin registered, as a snapshot array.
* `runtime.plugins.getSafetyRegistry()` — the active
  `SafetyPolicyRegistry`. Equivalent to `runtime.safety.getRegistry()`.

Collection-shaped accessors return `[]` when no plugin contributed;
capability-shaped accessors return `null`. Empty results never throw.

## Event-log projections [#event-log-projections]

`runtime.events.iterate(...)` returns a paginated async-iterable over
`harness_event_log` rows ordered by `(chat_id, sequence_number,
created_at, id)`. The accessor reads the durable Supabase log
server-side; PA-1 Phase A.2 promoted this from a stub to a real
projection surface.

```typescript
for await (const row of runtime.events.iterate({ chatId })) {
  // EventLogRow — id, session_id, event_type, payload, sequence_number, …
}

const result = await runtime.events.fold(myProjection, { chatId });
// result.state, result.rowsProcessed, result.projection
```

`runtime.events.fold(projection, options?)` composes a
`GraphProjection<T>` over the iteration. `options.fromSequenceNumber`
skips at the SQL layer — rows with `sequence_number IS NULL` (pre-PA-1
legacy) are always included for back-compat.

See [Event-log projections](/docs/event-log-projections) for the
projection contract and the canonical bundled projections.

The live event bus (`runtime.events.on`, `once`, `off`) is co-located
on the same facet but is unrelated to the persisted log — ordering
between the two is not guaranteed; treat the bus as a hot stream.

## Span introspection [#span-introspection]

`runtime.spans` exposes the OTel-shaped exporter the runtime threads
through every turn:

```typescript
runtime.spans.start({ name: "host.custom", kind: "host.custom" });
await runtime.spans.flush();    // call from a waitUntil tail
runtime.spans.inFlightCount();  // 0 when the exporter doesn't track
runtime.spans.isShutdown();     // false when the exporter doesn't track
runtime.spans.snapshot();       // { inFlightCount, isShutdown } in one read
```

`inFlightCount`, `isShutdown`, and `snapshot` are introspection
methods backed by optional `getInFlightCount?()` / `isShutdown?()`
hooks on the exporter contract. When a host-supplied exporter doesn't
implement them, the facet returns `0` / `false` — documented as
"unknown — assume zero / false". The default `NoopOtelExporter` and
the reference `CapturingOtelExporter` both track accurately.

## cacheBackend default-policy promotion [#cachebackend-default-policy-promotion]

`memoryCacheBackend` is the substrate default for `cacheBackend`. The
runtime constructor default-constructs one when `config.cacheBackend`
is `undefined`; the seam factories receive the backend automatically
via `createSeam.ts`. Previously the field had to be configured
explicitly; today it is always-on with a default cap of 1000 entries
and 64 MB.

The change is observable in `runtime.cache?.metricsSnapshot()` on day
zero. No consumer action is required.

To override, pass a custom `cacheBackend` field on
`SessionRuntimeConfig`. Set it to `null` to disable caching entirely.

For the contract, the two read modes, and the 4-gap fingerprint
thread, see [Cache](/docs/cache).

## Event-log hash chain [#event-log-hash-chain]

The persistence layer can stamp a per-chat hash chain onto every
`harness_event_log` row. The wire-in lives on `EventLogWriter` and is
gated by the `c9PhaseBEnabled` flag; when on, the writer maintains a
`prevHashCache` keyed by `chat_id`, links each new row to the previous
`row_hash`, and surfaces `verifyChainForChat(...)` and
`generateProof(...)` from `@pleach/core/eventLog` for downstream
audit. Hosts that don't care about the chain leave the flag off and
the writer behaves exactly as before.

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

<Cards>
  <Card title="Stream events" href="/docs/stream-events" description="Every event type executeMessage can yield, with the payload shape." />

  <Card title="Storage" href="/docs/storage" description="MemoryAdapter, IndexedDBAdapter, SupabaseAdapter — pick one per environment." />

  <Card title="Checkpointing" href="/docs/checkpointing" description="Snapshot, restore, time-travel — the per-channel checkpoint API." />

  <Card title="React" href="/docs/react" description="HarnessProvider, useHarness, and the hook surface for browser apps." />
</Cards>
