Skip to content

Extension State

Z-M-Huang edited this page Apr 27, 2026 · 3 revisions

Extension State

contractVersion: 1.0.0

Per-extension state persisted by the active Session Store alongside the session manifest. The manifest records only slot metadata (see Session Manifest § Shape); the payload itself lives in a separate persistence surface owned by the store. Declared in the contract; serialized by core; scoped strictly to the declaring extension.

Meta-contract guidance. This page describes the state-slot mechanism shared by every category contract. It is not itself a per-category contract and carries no independent contractVersion at the meta-shape level. The contractVersion: 1.0.0 stamped here reflects the first stable version of the slot-payload shape and the verification surface specified below. Breaking changes to the state-slot mechanism are recorded in the Changelog below; individual category contracts own their own slotVersion for their specific payload shapes.


The state slot

An extension's state slot is a blob of typed state identified by the extension's ID. Core owns:

  • Storage — the payload lives in a persistence surface owned by the active Session Store. The slim session manifest does not carry per-extension slot metadata; the active store maintains its own slot-summary index out of band. The owning extension's contractVersion is captured at write time by the store, not by the manifest.
  • Serialization and deserialization.
  • Drift handling on resume.
  • Access control (only the owning extension reads or writes its slot).

The extension owns:

  • The shape of the data.
  • When to update it.
  • Migration of its own prior-version data.
classDiagram
    class Manifest {
        extensions : List
        stateSlots : Map~ExtId, SlotMeta~
    }
    class SlotMeta {
        extId : ExtId
        slotVersion : SemVer
        reference : OpaqueRef
    }
    class Slot {
        extId : ExtId
        payload : Opaque
    }
    Manifest --> SlotMeta : "records metadata"
    SlotMeta ..> Slot : "references payload<br/>(separate persistence surface)"
Loading

Declaration

Every contract category may declare a state-slot shape. A category without a state slot is stateless — its extensions cannot persist anything across resumes.

When declared, the contract specifies:

  • Shape — a type for the payload (JSON-compatible in v1).
  • Slot version — semver independent of contractVersion. Bumped when the extension changes its own state shape.
  • Size class — approximate — "small", "medium" (up to ~1MB), "large" (streamed). Large slots require streaming APIs on the Session Store.
  • Sensitivity — whether the slot may contain references to secrets. Slots may never contain resolved secrets. See Secrets Hygiene.

Access

Extensions reach their slot through the Host API:

host.session.stateSlot(extId) // read | write | swap

An extension cannot read another extension's slot. There is no bulk enumeration of slots. Commands that want to clear a slot do so by invoking the owner (through an event the owner listens for, or a dedicated command the owner registered).


Drift

A resume may find that the extension set has changed: an extension was removed, its slotVersion was bumped, or its contract version was bumped.

flowchart TD
    Start[resume] --> Owner{owner<br/>extension loaded?}
    Owner -->|no| Dropped["record 'orphaned slot';<br/>keep in manifest;<br/>do not pass to any extension"]
    Owner -->|yes| Version{manifest.slotVersion<br/>== loaded.slotVersion?}
    Version -->|yes| Deliver[deliver payload to extension]
    Version -->|no| Migrate[call extension.migrate<br/>oldVersion, newVersion, payload]
    Migrate -->|success| Deliver
    Migrate -->|fail| Fail[drift diagnostic;<br/>severity per contract]
Loading
  • Orphan slots are preserved for inspection but never delivered. A later session that re-loads the original extension (on the same Session Store) sees its slot again.
  • Migrations are the extension's responsibility. The contract exposes a migrate(from, to, payload) extension point; if the extension does not implement it, any version bump is treated as a drift failure.
  • Drift severity is set by the contract's validationSeverity for critical-category extensions (hard-fail on unmigrated drift) and by the extension's config for optional categories.

See also Session Resume flow.


Guarantees

  • Atomicity. Slot writes are all-or-nothing per session snapshot. An interrupted write leaves the previous snapshot intact.
  • Consistency. A slot read during a turn sees a consistent view; writes are visible to subsequent reads from the same writer within the same turn.
  • Isolation. Other extensions never observe a half-written state.
  • Durability. After the active Session Store acknowledges persistence, the slot survives crashes.

Visibility inside a parallel fan-out (compound turn)

Parallel SM stage fan-out runs as one compound session turn (Stage Executions — Turn boundary and persistence). The consistency rule above applies per-writer only; across siblings the rule is stricter:

  • Every sibling reads the state slot as it stood at the start of the compound turn. Mid-flight writes from peer siblings are not visible to other siblings.
  • Each sibling's writes buffer in-memory and land atomically at the compound-turn snapshot alongside any join stage's write.
  • Commit order is deterministic. Core applies the buffered sibling writes in NextResult.stages[] index order, then — if declared — the join stage's write last. Each write replaces the entire slot payload (see Serialization above); the final write in the sequence wins.
  • Two siblings writing the same slot therefore mutually clobber under last-writer-wins by index. The canonical way to combine sibling outputs is to declare a join: its body reads the reduced ctx.upstream and writes the single authoritative payload. See Concurrency and Cancellation — Safe envelope for parallel fan-out.

A sibling cannot observe another sibling's state-slot write during the compound turn. Two mechanisms exist for combining sibling outputs, and they are distinct:

  • Reduced sibling outputs. A join stage reads each sibling's reduced StageResult via ctx.upstream. This is StageResult propagation — not a state-slot read; the join does not see siblings' state-slot writes either.
  • Committed state-slot writes. The earliest reader of a sibling's (or the join's) committed state-slot write is a later sequential stage that runs after the compound-turn snapshot has committed.

These guarantees come from the Session Store. Different stores implement them differently (the filesystem store uses a journal-plus-snapshot pattern); the guarantees are contractual, the mechanism is not.


Serialization

v1 stores slots as JSON. Contracts that need binary blobs declare a base64-encoded field. Large-slot categories (potentially streaming) are a future extension point; v1 does not support streaming state-slot writes.

A slot's payload must be deterministic in its own schema — i.e., two writes with equal data produce equal bytes. This lets the Session Store de-duplicate snapshots and lets replay tooling compare slots across runs.


Not in the state slot

  • Transient turn state. Use in-memory state; it resets on each turn.
  • Resolved secrets. Never. See Secrets Hygiene and LLM Context Isolation.
  • Large blobs without a streaming opt-in. Blocks the snapshot.
  • References to another extension's state. Cross-extension communication rides events/commands/registries, not slot references.

Examples by category (sketch)

Category Typical slot contents
State Machines Current state ID, per-state counters, workflow progress, last user decision
Session Stores Bookkeeping index (slot summaries, write log) — self-referential, so the store writes this first
Loggers Rotation counters (for file loggers)
Providers Auth refresh tokens by reference (the env provider holds the actual value)
Context Providers Cached summaries, last-computed signatures
Tools Retry counters for idempotent tools, cached expensive-to-recompute data
Hooks Debounce counters, window aggregates
Commands Per-command usage counters (non-sensitive)
UI Per-UI preferences (pane sizes, themes) — may also live in config

These are typical, not normative. Any specific extension's slot shape is defined by that extension's contract and its own versioning.


Drift policies

Every StateSlotShape declaration carries a driftPolicy field. Core consults this policy whenever the stored slotVersion differs from the loaded extension's declared slotVersion.

Policy What core does when versions differ
reject Refuse to deliver the slot. Return Session/SlotDriftRejected. Resume continues; the extension receives no state and must start fresh (or fail, if critical).
warn Deliver the stored payload unchanged. Core emits a Deprecation-style warning event on the event bus. The verifier does not emit the event — the caller does.
migrate Call shape.migrate(storedPayload, storedVersion) and deliver the returned payload. A throwing migrator surfaces Session/SlotMigrationFailed.

Unversioned slots

A stored blob that carries no slotVersion field is treated as Validation/SlotVersionMissing. This is a hard failure regardless of drift policy — an unversioned blob cannot be safely compared or migrated.

Error codes

Code Class When
SlotVersionMissing Validation The stored blob has no slotVersion field.
SlotDriftRejected Session Version mismatch under driftPolicy: 'reject'.
SlotMigrationFailed Session The migrate function threw; cause carries the original error.

Slim-manifest scope (Q-2)

The session manifest (written by the active Session Store) stores only:

  • Message history.
  • Attached State Machine state reference (smExtId, smSlotVersion, smStateRef).
  • Security mode and project root.

Non-SM extension state slots are not persisted in the slim manifest. Extensions that wish to persist state independently of the SM must implement their own persistence via the Session Store adapter. Core resume must not fail due to non-SM extension drift — missing or drifting non-SM extensions are silently absent on resume.

"Critical-severity drift fails resume" applies only to the attached SM and the Session Store itself. No other extension category can block resume.


Related pages


Changelog

1.0.0 — initial

  • Declared StateSlotShape with extId, slotVersion, schema, driftPolicy, and optional migrate.
  • Defined three drift policies: migrate, warn, reject.
  • Defined verifyStateSlot function with verdict types StateSlotVerdict and StateSlotFailure.
  • Defined error codes SlotVersionMissing, SlotDriftRejected, SlotMigrationFailed.
  • Exported stateSlotShapeSchema (AJV-compilable JSON Schema for StateSlotShape).
  • Documented Q-2 slim-manifest scope: only SM state is in the manifest; non-SM drift never fails resume.

Introduction

Reading

Core runtime

Contracts

Category contracts

Context

Security

Runtime behavior

Operations

Providers (bundled)

Integrations

Reference extensions

Tools

UI

Session Stores

Loggers

Providers

Hooks

Context Providers

Commands

Case studies

Flows

Maintainers

Clone this wiki locally