Skip to main content
This page is the canonical settled-state contract for session and runtime behavior.

Ownership model

Meerkat has one canonical semantic path:
  • the runtime control plane (MeerkatMachine in meerkat-runtime) owns keep_alive, Queue/Steer routing, comms drain lifecycle, external-event admission, and request/turn commit semantics; runtime-backed surfaces reach those semantics only by lowering into it
  • SessionService is the substrate lifecycle seam used by those surfaces
  • EphemeralSessionService / build_ephemeral_service remain valid for testing, embedded use, and WASM internals, but they are not the primary product path and do not own runtime semantics
The runtime-backed path is:
The runtime-backed build contract has an explicit binding seam:
  • runtime-backed surfaces should call MeerkatMachine::prepare_bindings(session_id)
  • those bindings flow into SessionBuildOptions.runtime_build_mode = RuntimeBuildMode::SessionOwned(bindings)
  • standalone / embedded / test-only builds should opt into RuntimeBuildMode::StandaloneEphemeral explicitly instead of relying on silent fallback
SessionRuntimeBindings is the epoch-local runtime handle for a session. It carries:
  • session_id
  • epoch_id
  • the canonical OpsLifecycleRegistry
  • shared consumer cursor state used for recovery-safe completion visibility
This keeps one owner for runtime semantics:
  • PersistentRuntimeDriver::recover() owns input/runtime/control recovery
  • MeerkatMachine owns session entry recovery (ops_lifecycle, epoch_id, cursor state)
Direct substrate usage is intentionally narrower:
  • Queue-only turns
  • no runtime-owned keep_alive
  • no Steer/render-metadata semantics
  • no runtime ingress/admission ownership

Agent construction contract

Agent construction is centralized in AgentFactory::build_agent().
  • Surfaces pass per-request build data in-band via CreateSessionRequest.build (SessionBuildOptions).
  • No out-of-band staging lock is used.
  • FactoryAgentBuilder maps SessionBuildOptions to AgentBuildConfig.
  • For runtime-backed builds, SessionBuildOptions.runtime_build_mode should carry RuntimeBuildMode::SessionOwned(bindings) from prepare_bindings().
  • For standalone/testing/embedded builds, prefer RuntimeBuildMode::StandaloneEphemeral explicitly.
  • Session metadata persists realm context and durable session identity:
    • realm_id
    • instance_id
    • backend
    • config_generation
    • durable LLM identity
    • keep_alive
    • visible comms identity metadata such as comms_name and peer_meta

Session lifecycle and turn semantics

create_session

create_session(req) builds the agent and optionally runs the first turn.
  • Returns RunResult with session_id.
  • InitialTurnPolicy::RunImmediately executes the first turn inline (default).
  • InitialTurnPolicy::Defer registers the session without running a turn.
Create has a commit boundary:
  • pre-commit failure: side-effect free; no committed session identity
  • post-commit first-turn failure: return session identity and keep the session resumable
Committed create failure must not be silently rewritten to “cancelled” or cleaned up as unpublished work.

start_turn

start_turn(id, req) executes a new turn on an existing session.
  • At most one in-flight turn per session.
  • Concurrent attempts return SESSION_BUSY.
  • Committed success must not be rewritten to cancellation.
  • Runtime-backed surfaces may hot-swap supported live settings on an existing session where the surface contract says that is allowed.
The runtime may coalesce multiple queued Steer inputs into one agent turn. Admission remains durable per input, but there is no promise of one model call per input. Applications that require one side effect per admitted input must iterate the durable input records deterministically in a tool or host-owned worker. Do not rely on the model to notice and act once for every item in a batched prompt.

Ordered System messages

system_prompt on StartTurnRequest appends one ordinary System message at that turn boundary. System messages are repeatable and may appear anywhere in the transcript; an update never replaces row zero or rewrites an earlier message. There is no separate prompt identity or host-injection ledger. Materialization and resume preserve the transcript byte-for-byte and never infer a System message from current host configuration. A host that wants to add an instruction supplies it on the admitted turn, where it becomes one new ordered System event. Compaction retains every System message in relative order. Provider adapters preserve each System at its exact position where the wire accepts ordinary role=system messages. A provider with only a top-level instruction field cannot silently hoist or merge later System rows: its request projection returns a typed error while the ordered row remains valid durable history. Provider capability never changes, rejects, or rewrites the durable Session itself.

interrupt

interrupt(id) cancels an in-flight turn.
  • If no turn is running: SESSION_NOT_RUNNING.

read and list

  • read(id) and list(query) are non-blocking with respect to in-flight turns.
  • Persistent services can include durable sessions from the realm backend.
  • Presence in list() is not the same as “live session exists”; runtime-owned seams must answer liveness.

read_history

read_history(id, query) returns the last committed transcript snapshot for a session.
  • Messages are returned oldest-to-newest.
  • offset and limit apply from the start of the full transcript.
  • Active sessions do not expose in-flight partial output through history reads.
  • Archived sessions remain readable on persistent backends; the ephemeral service rejects archived history reads with SESSION_PERSISTENCE_DISABLED.

archive

  • The canonical SessionDocumentMachine owns the session_lifecycle_terminal fact for ALL profiles; shells realize its verdict, they never decide it.
  • Realization order is fail-closed: durable document commit first, runtime retire second. RuntimeState::Retired is the runtime realization of the same verdict, so the resurrection window is unrepresentable.
  • If runtime retirement fails after the document commits Archived, the durable terminal remains authoritative: reads, resumes, and turns stay rejected while a repeated archive command follows the generated convergence transition and retires the residual runtime.
  • Archived sessions are excluded from list() and rejected for further reads/turns (SESSION_NOT_FOUND on persistent services; the ephemeral service serves a final in-memory view).
  • Committed transcript history remains readable via read_history on persistent backends.

Keep-alive contract

keep_alive is a runtime/session concept, not an old “host mode” execution path. Rules:
  • create / run omitted => default false
  • continue / resume omitted => inherit persisted session intent
  • explicit keep_alive override is a session/runtime mutation once validated
  • invalid keep_alive + comms configuration is rejected before any stateful work
  • once validated, an explicit keep_alive mutation may commit independently of turn success
keep_alive=true requires usable comms identity/config on the surfaces that expose it.

External events

External events are queue-only runtime-backed inputs.
  • they are admitted into runtime ingress
  • they do not invent a second direct execution path
  • “turn-boundary inbox draining” is not the primary mental model for the current runtime-backed design

Explicit override semantics

Where the surface supports omission vs explicit override, these are distinct facts:
  • omit / inherit
  • disable / false
  • set / concrete value
When all three meanings matter, the wire/API must preserve that distinction. Typed optional fields and override masks are preferred over default-value folklore. Concretely: SystemPromptOverride (Inherit / Set / Disable) is the canonical wire+persisted carrier for the per-request system-prompt fact, and per-turn provider parameters travel as the typed ProviderParamsOverride, not a raw JSON bag.

Realm contract

Sessions are realm-scoped.
  • Same realm_id: shared visibility and config context.
  • Different realm_id: strict isolation.
  • Backend is pinned per realm via realm_manifest.json (manifest format 2 adds provider and ephemeral_domains; readers refuse formats newer than they support instead of silently ignoring fields, and realms pinned to an external storage provider refuse built-in disk opens typed).
  • State roots resolve realm-id-first across the project-local and user-global candidates; a realm materialized under both is a typed split-brain refusal (see rkat storage doctor).
  • First materialization of a fresh realm reserves across every candidate root before writing the manifest, so surfaces with different default roots racing the same realm converge on one copy instead of minting split-brain twins.
  • A fresh prompt-first CLI run or rkat help whose workspace-derived realm cannot open may isolate storage into a newly generated durable SQLite realm under the same resolved state root. No historical session from the failed realm is loaded into the fresh run, and the compatibility bridge is never invoked automatically. An ordinary supported initialization or migration may have completed before a later store refused. The original workspace config and auth/tool policy still govern the run. Explicit realms, isolated runs, resumes, and session commands never take this fallback. Historical session access requires the explicit fenced pre-0.8.10 bridge; there is no in-memory compatibility fallback.

Compaction contract

Compaction is optional and non-fatal.
  • Triggered by token thresholds and turn guards.
  • On failure, emits CompactionFailed and continues with uncompacted history.
  • Compaction usage counts toward run budgets.

Durability

Ephemeral mode

No durability across process restart.

Persistent mode

Durability follows the active persistent realm backend (sqlite or jsonl). Both persistent backends mount a SQLite runtime companion for runtime authority: sqlite realms keep the runtime_* tables inside sessions.sqlite3, and jsonl realms carry a dedicated runtime.sqlite3 alongside the JSONL session documents. Queued inputs, run-boundary receipts, ops snapshots, and auth-lease/OAuth-flow authority are durable under both backends. The memory backend is explicit ephemeral storage and does not survive process restart.
  • Completed turns are persisted.
  • Crash during an in-flight turn loses at most the work the intra-turn persistence hook had not yet written; any durable tail it did write is preserved and recovered (see below), never discarded.
  • SQLite-backed realms are the default persistent mode and support normal same-realm multi-process workflows.
Persistent session metadata is the source of truth for resumed durable session behavior unless an interactive surface supplies an explicit typed override.

Domain document and store authority

The current Session envelope is version 3 and contains domain state only. It does not authenticate itself and does not carry a persistence token. A deserialized Session cannot authorize a write, migration, or recovery operation. The selected store profile issues the physical authority:
  • WholeBlobsession_id, monotonic store revision, and SHA-256 of the exact serialized row.
  • HeadCanonicalsession_id, monotonic store revision, the committed boundary head, and its exact head token. The boundary head binds the message-row, rewrite, graph, component, and metadata prefixes.
RuntimeStore also maintains a small catalog entry in the same atomic boundary as physical authority. The catalog can answer listing and lifecycle discovery, but cannot carry a transcript, graph, component body, or serialized Session. It is not a second session document. Meerkat 0.8.11 accepts durable state from 0.8.10 only through the one-time released-state importer. An exact released v2 document with the historical embedded proof is checked once by the frozen 0.8.10 verifier. An unstamped released document is accepted only when the same store transaction proves the released physical schema and exact source row or blob identity. Either path strips the historical proof fields and installs current store-issued authority; ordinary v3 reads never enter the importer, and no current write emits the old fields. State older than 0.8.10 in a SQLite realm is admitted only through the explicit offline command rkat ... storage migrate --apply --bridge-pre-0-8-10. JSONL and memory realms are rejected before any database is mutated. This is a frozen, fail-closed pre-floor importer: under the realm maintenance fence it authenticates supported historical schema and row shapes before installing current representation and authority. Ordinary store opens and v3 reads remain strict and never enter the bridge. The maintenance transaction preserves queued and nonterminal input rows without scheduling or replaying them. A later session activation follows normal recovery.

Durable-tail recovery

The intra-turn persistence hook writes a provisional physical successor outside the runtime boundary transaction. Every successful write returns an explicit RunCheckpointReceipt. Receipts are scoped to one session, committed base, and run; candidate_sequence starts at one and advances contiguously. An exact retry may return the same receipt, while the next candidate must present and succeed the preceding receipt. A stale, divergent, or out-of-sequence candidate fails closed. WholeBlob encodes, hashes, and writes each candidate once. HeadCanonical applies one sealed prepared delta and returns the exact resulting physical revision and head token. The actor retains only the latest successful receipt; an uncertain outcome or failed acknowledgement terminates the actor and requires durable reload. At the final boundary the store promotes that exact candidate. It does not construct a second document or replay the delta. A crash or shutdown race can still leave a durable transcript tail — up to and including a fully completed turn — whose runtime boundary commit never landed. Durable is not the same as runtime-committed, and neither view is silently preferred: a cold read drives a typed, machine-owned read-source decision (serve the committed runtime snapshot, serve the committed store head, recovery required, or quarantine) instead of a shell comparison. The recovery rule is never-discard: every store-proven durable descendant of the committed authority is preserved.
  • A completed tail (single run, EndTurn terminal, structurally coherent) is committed as a recovered run boundary in one atomic store transaction: recovered snapshot, boundary receipt, and input terminalization land together, so exactly-once holds and the delivery layer never re-runs a recovered turn.
  • An interrupted tail is closed as interrupted: content preserved, a typed recovery notice appended, the original run terminalized — never requeued.
  • Anything else — including any tail carrying a dangling tool_use — is held intact for reconciliation. A dangling call proves intent, not execution; its external side effect may already have fired, so recovery never closes it with synthetic results.
A committed strict descendant wins over a stale live runtime snapshot regardless of local actor liveness, and projection convergence requires intra-turn row provenance, so two committed sibling documents can never overwrite each other. Read-triggered recovery runs under an exclusive per-session fence, re-observes the head under it, and converges idempotently when a competing process wins the commit; a recovery that already landed is refused rather than committed twice. While a session is held or quarantined, its content is retained intact and resume fails typed rather than with internal-error prose:
  • SESSION_DURABLE_TAIL_HELD_FOR_RECOVERY — the durable tail exists but machine-authorized recovery has not promoted it yet; it awaits reconciliation.
  • SESSION_DURABLE_TAIL_RECOVERY_REFUSED — machine-authorized recovery was refused by conflicting persisted runtime facts (another live runtime, or boundary receipts that already cover or contradict the tail); retry after the conflicting runtime quiesces.
  • SESSION_DURABLE_EVIDENCE_QUARANTINED — the durable evidence is forked or unverifiable; the session is quarantined from resume.
All three carry the typed durable_resume_hold structured payload so callers can distinguish the hold class without parsing messages. Ownership of the pipeline (SessionDocumentMachine classifies, MeerkatMachine authorizes, RuntimeStore realizes) is documented in Machine Authority.

Storage mechanics (schema ledger, fence, durability classes)

Every SQLite file in a realm carries a meerkat_schema(domain, version) migration ledger; stores bring their domain up to date at open under a pinned concurrent-open protocol, and a file whose ledger is ahead of the binary refuses typed (SchemaFromTheFuture) — a rollback candidate fails certification cleanly instead of crash-looping. Each database also has a sibling <file>.mfence lock file: store operations hold a shared per-operation guard, and offline maintenance (rkat storage migrate) takes the exclusive side after in-flight operations drain. Backup artifacts written by maintenance use the registered *.pre-<version>-<timestamp> naming that rkat storage doctor inventories and rkat storage prune owns.
Recent session-envelope upgrades are one-way once a newer binary writes the session. Before upgrading a durable realm, snapshot every durable store and keep that snapshot with the prior binary. Rolling the binary back after new writes can leave those sessions unreadable to the older release; a code-only rollback is not a supported recovery plan.
Every store slot a realm composes carries a machine-readable durability class (durable / rebuildable_cache / scratch) and its resolution. Declarations are complete by construction: a storage provider must declare exactly one class for each of the seven domains (sessions, runtime, schedule, workgraph, jobs, blobs, artifacts), so no slot dodges the rule by omission. A durable slot resolving to a non-persistent store without the realm manifest declaring that domain ephemeral is a startup error — never a silent in-memory fallback. Deployment tooling can rely on the classes (for example, cloning only durable domains between state generations).

Session storage layout (0.7.25+): head-canonical strands

Since 0.7.25 the SQLite session store persists sessions incrementally: the transcript lives in append-only session_strand_messages rows, rewrite history in session_rewrites, and a single session_heads row per session carries the canonical metadata, message count, and CAS token. Saving a long session appends O(delta) rows instead of rewriting a monolithic document. The canonical-representation rule, per session: if a session_heads row exists, the head representation is canonical. A released 0.8.10 blob, if present, is consumed only by the one-time activation importer and is never a fallback authority for a current head. Sessions created in HeadCanonical mode may have no meaningful sessions blob row at all.
Downstream tooling that reads sessions.session_json via direct SQL sees empty or stale content for sessions written by 0.7.25+ binaries — silently, because the column still exists. Raw table reads were never a supported surface; use one of the typed read paths instead:
  • SessionStore::load — full canonical session document.
  • SessionStore::load_meta — metadata-only projection (no transcript deserialization); list for filtered metadata.
  • The wire surfaces (session/read, session/list, REST equivalents).
For forensic tooling that must read the raw database, the canonical shape is: resolve the session_heads row for the session, then read session_strand_messages rows for (session_id, head.strand) ordered by seq over 0..head.message_count. Anything else is internal representation and may change without notice.

Range-read capability probes (load_canonical_head / load_rewrite_commits)

IncrementalSessionStore carries two additive read verbs that expose the head-canonical representation to future O(page) history reads:
  • load_canonical_head returns the persisted head row ONLY when head+rows are the session’s canonical durable representation. Unlike a full materializing load, it answers None for absent and non-canonical rows and never reads a WholeBlob body. Some promises the row is the persisted head itself and that load_messages over head.strand page-serves exactly the rows the head covers.
  • load_rewrite_commits returns the ADOPTED rewrite commits (idx < head.rewrite_count), oldest first, without materializing retained revision bodies. It must always equal load_rewrites’ commits, in order — including the empty set while a recorded rewrite is not yet adopted.
Both verbs have conservative defaults (None; derive from load_rewrites), so every existing store — including delegating wrappers that do not forward them — degrades to the whole-load path, never to a refusal. The conformance suite pins the contract conditionally: a store answering None for every canonical-head probe is fully conformant; a store answering Some must serve the persisted row and page-exact strand reads, and delegating wrappers must forward both verbs (capability_discovery chapter). Service reads trust only the store-selected profile and its issued authority. read_history, read_transcript_revision, and list_transcript_revisions may materialize the required domain view from the canonical rows, but a materialized Session never becomes write authority.

Durable runtime lifecycle vocabulary (runtime_states)

The runtime companion persists one lifecycle row per runtime. The full runtime_state vocabulary, with polling guidance: Busy-detection loops must treat stopped and destroyed as not-busy: stopped means nothing is executing and nothing will execute without an explicit resume, so polling it as “busy” hangs until an external timeout. Prefer the typed status surfaces over raw row reads: since 0.7.25, session/input_status answers from durable state without requiring a live runtime registration, so a fresh host process reports truthful terminal outcomes (Ok/NotFound) for work finished before a restart.

Config concurrency contract

Config runtime uses generation CAS.
  • config/get returns current generation.
  • config/set and config/patch can specify expected_generation.
  • Mismatches fail deterministically with generation conflict.

Data governance

No automatic sensitive-data redaction is applied to session content or tool payloads. Encrypt storage at rest externally if required by your environment.

Request-only turn context

transient_turn_context is non-empty runtime-input text, not Session state. Whitespace is significant and its exact text bytes remain on a pending input so cold recovery can retry the same turn, then retire with that terminal input payload. During the active logical turn, Meerkat projects the caller context first and any runtime-admitted steers after it, each as a distinct temporary user-channel injected-context message immediately before the exact admitted conversational user message in each foreground provider request. They are never System messages, transcript appends, rewrite records, compaction input, or extraction input. Deferred creation cannot carry this context because no runtime input exists yet; attach it to the eventual turn instead. The browser/WASM standalone live surface does not expose this durable runtime-input feature.

See also