Ownership model
Meerkat has one canonical semantic path:- the runtime control plane (
MeerkatMachineinmeerkat-runtime) ownskeep_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 SessionServiceis the substrate lifecycle seam used by those surfacesEphemeralSessionService/build_ephemeral_serviceremain valid for testing, embedded use, and WASM internals, but they are not the primary product path and do not own runtime semantics
- 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::StandaloneEphemeralexplicitly instead of relying on silent fallback
SessionRuntimeBindings is the epoch-local runtime handle for a session. It carries:
session_idepoch_id- the canonical
OpsLifecycleRegistry - shared consumer cursor state used for recovery-safe completion visibility
PersistentRuntimeDriver::recover()owns input/runtime/control recoveryMeerkatMachineowns session entry recovery (ops_lifecycle,epoch_id, cursor state)
- 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 inAgentFactory::build_agent().
- Surfaces pass per-request build data in-band via
CreateSessionRequest.build(SessionBuildOptions). - No out-of-band staging lock is used.
FactoryAgentBuildermapsSessionBuildOptionstoAgentBuildConfig.- For runtime-backed builds,
SessionBuildOptions.runtime_build_modeshould carryRuntimeBuildMode::SessionOwned(bindings)fromprepare_bindings(). - For standalone/testing/embedded builds, prefer
RuntimeBuildMode::StandaloneEphemeralexplicitly. - Session metadata persists realm context and durable session identity:
realm_idinstance_idbackendconfig_generation- durable LLM identity
keep_alive- visible comms identity metadata such as
comms_nameandpeer_meta
Session lifecycle and turn semantics
create_session
create_session(req) builds the agent and optionally runs the first turn.
- Returns
RunResultwithsession_id. InitialTurnPolicy::RunImmediatelyexecutes the first turn inline (default).InitialTurnPolicy::Deferregisters the session without running a turn.
- pre-commit failure: side-effect free; no committed session identity
- post-commit first-turn failure: return session identity and keep the session resumable
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.
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)andlist(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.
offsetandlimitapply 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
SessionDocumentMachineowns thesession_lifecycle_terminalfact for ALL profiles; shells realize its verdict, they never decide it. - Realization order is fail-closed: durable document commit first, runtime retire second.
RuntimeState::Retiredis 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_FOUNDon persistent services; the ephemeral service serves a final in-memory view). - Committed transcript history remains readable via
read_historyon 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_aliveoverride is a session/runtime mutation once validated - invalid
keep_alive+ comms configuration is rejected before any stateful work - once validated, an explicit
keep_alivemutation 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
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 addsproviderandephemeral_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 helpwhose 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
CompactionFailedand 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.
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 deserializedSession cannot authorize a write, migration, or recovery
operation.
The selected store profile issues the physical authority:
- WholeBlob —
session_id, monotonic store revision, and SHA-256 of the exact serialized row. - HeadCanonical —
session_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.
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 explicitRunCheckpointReceipt. 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,
EndTurnterminal, 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.
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.
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 ameerkat_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.
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-onlysession_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.
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_headreturns the persisted head row ONLY when head+rows are the session’s canonical durable representation. Unlike a full materializing load, it answersNonefor absent and non-canonical rows and never reads a WholeBlob body.Somepromises the row is the persisted head itself and thatload_messagesoverhead.strandpage-serves exactly the rows the head covers.load_rewrite_commitsreturns the ADOPTED rewrite commits (idx < head.rewrite_count), oldest first, without materializing retained revision bodies. It must always equalload_rewrites’ commits, in order — including the empty set while a recorded rewrite is not yet adopted.
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/getreturns currentgeneration.config/setandconfig/patchcan specifyexpected_generation.- Mismatches fail deterministically with generation conflict.
Data governance
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.
