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 surfacesEphemeralSessionServicesupplies in-memory session storage and execution for browser sessions behindMeerkatMachine; direct use throughbuild_ephemeral_serviceremains available for explicit standalone tests and embeddings
- 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
CloseStandaloneTurn releases the epochless, unplaced session’s
lifecycle while preserving its terminal evidence and model/tool-routing state.
It does not mint a durable commit receipt or close a runtime-owned session;
SessionOwned terminal publication remains with the runtime commit owner.
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, unkeyed System
message at that turn boundary. System messages are repeatable and may appear
anywhere in the transcript; an append never replaces row zero or rewrites an
earlier message.
Keyed system-prompt updates instead append rows carrying
SystemPromptVersionIdentity: a SystemPromptKey and SystemPromptVersion.
The active model view selects the latest version per key; unkeyed rows are
never superseded. Older versions remain historical rows reachable through
retained transcript revisions, not a promise to retain every version in the
active model view.
There is no host-configuration injection ledger. Materialization and resume
never infer a System message from current host configuration. A host that
wants to add an instruction supplies it through the admitted update path,
where it becomes a new ordered System row rather than an overwrite.
Compaction retains active keyed and unkeyed System rows verbatim in relative
order, while discarding superseded keyed versions from the compacted active
head. This does not rewrite their retained revision history.
Provider adapters lower the active instruction view without rewriting durable
history. Wires accepting ordered role=system messages normally preserve their
positions. Anthropic’s model-gated lowering can instead place turn-scoped
instructions after their governing User and before the Assistant, subject to
its legal predecessor constraints; this is a wire-only placement adjustment
(see System-message projection).
Providers with only a top-level instruction field reject later System rows
with a typed projection error; unsupported placements on other wires also fail
typed rather than silently hoisting or merging canonical rows. 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.
Cancelling is not terminal completion.
For surfaces using SurfaceRequestExecutor, upgrading a cancellation action
during initialization is serialized with request cancellation. Cancellation
either receives the new action or the installer replays it for an already
cancelled request. Actions run outside the owner locks and may re-enter the
executor; this does not promise rollback of effects already executed.
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
Event delivery and durable projection
Live delivery and durable evidence use separate lanes:- Ordinary session subscribers read a 256-entry broadcast. If a subscriber
falls behind, it receives a synthetic
StreamTruncatedevent withreason.kind = "stream_lagged"and the exactreason.droppedcount, then continues with retained events. The marker reports a gap in that subscriber’s live view; it is not a canonical durable-log event. - A persistent session whose host installs event projection has one internal dedicated projector stream backed by an unbounded MPSC queue. Built-in realm-backed persistence installs it; custom hosts must call the projection composition seam explicitly. Event publication shares the same envelope with the live lane, but projector latency does not block live subscribers or depend on broadcast retention. The runtime emits a rate-limited degraded warning when the queue reaches 1,024 entries and records its high-water mark.
- The projector asynchronously appends each envelope to the durable event audit
log before updating derived
.rkat/materialized files. UI-ring lag does not drop projector input, but EventStore projection is still best-effort derived state. TheRuntimeStore/backend carrier remains session authority;SessionStorerows and EventStore are component/content and projection seams.
EventProjectionHalted fault, records a durable projection-halt marker when
possible, stops before appending later events across the sequence hole, and
makes replay and later resume fail closed. It does not roll back or fail the
already-committed turn. Clients that see live
stream_truncated reconcile from events/list_since, a current snapshot, or
ATIF export.
Every committed close of one of the session’s live channels publishes
live_channel_closed on the session event stream, after the close commits:
session_id, channel_id, a typed reason (LiveChannelCloseReason:
client_requested, client_disconnected, provider_closed, error,
media_fault, replaced or open_abandoned) and reopen_recommended, true
only for a media_fault while the session’s one reopen is still available.
Observers learn the cause from the stream instead of polling live/status.
Assistant message identity
Every assistant message the agent loop commits carries a session-scopedassistant_message_id (0.8.45+). The same value is on the committed
block_assistant history row and on every live event for that message, so a
client pairs live rows with canonical history by id alone. It never has to
compare text or rank, and identical answers stay distinct.
Assignment. The id is minted in the provider-neutral agent loop when a
provider turn starts, before hooks run, before the request is sent and before
any delta. It is opaque: never derived from content, and not a timestamp
(native builds mint UUIDv7, browser builds UUIDv4). Assignment happens for all
providers; the id never enters the provider request.
Events that carry it. turn_started, reasoning_delta,
reasoning_complete, text_delta, text_complete, server_tool_content,
assistant_image_appended, turn_completed and retrying name the message
they stream or close. run_completed.assistant_message_id names the committed
message whose text result repeats. That may be a message from an earlier run
(for example when a run ends on budget exhaustion before committing new text).
Rust consumers can read any of these through AgentEvent::assistant_message_id().
Commit and failure.
- Commit is a history fact: a message is committed exactly when a
block_assistanthistory row carries its id. turn_completedwith the id is only the live signal that the provider turn finished. It proves neither commit nor durability, and its absence proves nothing either:- It can come BEFORE work that can still fail the run: the boundary work
after a tool-use turn (turn-boundary hooks, the comms drain), the
run-completed hooks of a run with an output schema, and the session save.
A live display-text drain on a persistent session publishes its
turn_completedbefore the session is persisted; if persisting fails, the id never reaches history. - It can be ABSENT for a committed row: a run that fails after the row was
pushed but before
turn_completedwas published (for example a terminal-turn boundary hook or a run-completed hook denies) keeps the row and its id, and noturn_completedfollows.
- It can come BEFORE work that can still fail the run: the boundary work
after a tool-use turn (turn-boundary hooks, the comms drain), the
run-completed hooks of a run with an output schema, and the session save.
A live display-text drain on a persistent session publishes its
- So when a run ends (
run_completed,run_failed, a truncated stream or a reconnect), read history for every id you saw: a row with that id means the message was committed, no such row means it was discarded. History read after the run ends is the only authority. - An id is on at most one committed row. A failed provider turn (the request
fails, retries are exhausted,
max_tokensis hit while thinking, or a pre-request hook denies the call) never appends a row, so its id is never committed. - Retries of the same provider turn reuse its id: same-model, empty-output,
stall and timeout retries, and model fallback.
retryingcarries the id; discard the live buffer held for it and render the retry’s deltas in its place. - A re-poll after compaction re-emits
turn_startedwith the same id. A repeatedturn_startedfor an id that is still open restarts that message’s live buffer.
- Each provider turn’s assistant message, including tool-use turns and
structured-output extraction turns. Extraction turns emit no
turn_started,text_completeorturn_completed; their deltas and row still carry the id. - An assistant message appended by a tool effect gets its own fresh id, with
no
turn_started. Its images are published asassistant_image_appendedunder that id. The id of a message appended after a callback tool result is fixed when the batch is applied, so replays and redeliveries return the same id. - A row committed by a live channel’s display-text drain (host-produced
assistant output appended through
append_external_assistant_output) gets a fresh id that core mints when it commits the row. Thetext_completeandturn_completedpublished for that commit carry it. There is noturn_startedfor it, and its streamed text arrives as realtime transcript deltas, which carry noassistant_message_id. A drain that commits no row (no display text) publishesturn_completedwithout an id. - No id: compaction summaries (user rows), rows the realtime transcript materializer commits for a provider-managed realtime turn, and any row written before 0.8.45. Old rows and events are never backfilled.
- Realtime rows pair by provider item id instead. The
text_completeandturn_completedpublished when the materializer commits a realtime turn carry noassistant_message_id, so aturn_completedwithout an id does NOT mean no row was committed. The history row carriesrealtime_origin(on the wire since 0.8.45, the sameRealtimeMessageOriginshape run events carry inidentity), and the live transport’s realtime observations carry the same provider item ids:provider_item_idonassistant_text_delta,assistant_transcript_delta,assistant_transcript_finalandassistant_transcript_truncated, anditem_idonassistant_audio_chunkand insiderealtime_transcriptevents. A console that rendered live text for a provider item replaces it with the row whoserealtime_origin.provider_item_idscontains that item id. One row can cover several items (a barge-in that re-keys speech into a second item). The id-lesstext_completeandturn_completedcarry no provider item id either, so pair realtime rows with the live transport’s observations, not with session events. The origin is set when the live host hands the session an assistant event together with the channel it streamed on, which the built-in RPC and facade projection sinks do. A realtime row committed by an earlier release, or from events a customLiveProjectionSinkapplied withoutLiveTranscriptIdentity::channel_id, has norealtime_origin; render it from history only. Core does not mint anassistant_message_idfor these rows: their live output reaches the client straight from the live transport, never through the agent loop, so no core-minted id could be stamped on it before the row exists, and the provider item id already is the stable key both sides share. - Deltas with no id are output that is not a transcript assistant message,
such as the compaction summary call. They also come from a legacy custom
client, or a client decorator that does not forward
AgentLlmClient::prepare_request_attempt. A decorator MUST forwardprepare_request_attemptto the client it wraps (passing its transformed messages); otherwise the loop falls back to the legacy streaming path and the live deltas behind it carry no id. The loop still stampsturn_started,text_complete,turn_completed,retryingandassistant_image_appendedin those cases.
- Paging: the id is on each row, so any
offset/limitpage carries it. It implies nothing about position, and history can be read before the final live chunks of the same message arrive. - Digests: the id is construction bookkeeping. Transcript revisions, rewrite span digests, continuity guards and the provider-cache prefix identity are identical with and without it.
- Compaction: retained rows keep their ids byte-for-byte (core validation
already requires retained rows to be unchanged). Discarded ids leave the
active transcript, are never minted again or reused, and stay readable
through
read_transcript_revision(parent). The summary row has no id. Reloading a session (whole-blob or head-canonical storage, crash-recovery tail) round-trips rows unchanged. - Generic rewrite: replacement rows never carry an id. Core clears any supplied value, and wire rewrite input does not accept one (an echoed history key is ignored). Rows outside the selection keep their ids. A replaced or removed id is gone from the active transcript but stays readable in the parent revision.
- Restore:
session/restore_transcript_revisionre-installs rows core reads from the session’s own retained revision, not caller-authored rows, so every restored assistant row keeps its id verbatim. Restoring the revision a rewrite or compaction replaced brings its ids back into the active transcript. - Forks:
fork,fork_atandfork_at_complete_boundarykeep row ids (they are the same occurrences; the child session never re-emits them).fork_replacingkeeps the inherited prefix’s ids and clears the id of the edited row, whether it replaced a whole message or one assistant block.
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_idplus the same physical provider/root: 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 the candidate set resolved by that invocation before writing the manifest. Processes using the same multi-root set converge; surfaces probing different sets do not. Use one explicit root or matching server context for cross-surface starts.
- 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.
Mob-member role continuity
A durable mob member normally resumes only when its stored and requested role match. Mob ID, memberAgentIdentity, bridge session_id, and transcript are
fixed identity facts; a profile edit alone cannot reinterpret the stored
member as a different role.
Trusted in-process hosts have one explicit migration seam. An exact
MemberLaunchMode::Resume may carry resume_from_role naming the stored
predecessor role for that request. Admission proves that the stored and current
comms_name and MobMemberBinding refer to the same mob and member. An absent
declaration returns MemberRoleMigrationRequired; the wrong predecessor or a
contradictory identity returns MemberRoleMigrationRejected.
Role migration is cold and one-shot. The exact session must have no live actor,
the declaration is consumed rather than persisted as standing permission, and
successful materialization restamps the current role into durable comms
metadata while preserving the same session and transcript. There is no
implicit rollback; a later role change requires a new exact cold migration.
This seam is deliberately private. Public CLI, REST, JSON-RPC, MCP, generated
SDK spawn requests, helpers, and standing profiles cannot set
resume_from_role. The private member-host materialization protocol carries it
only as part of the trusted remote-host resume path.
Member creation provenance and host activation
The canonicalMemberSpawnedEvent.creation record carries a runtime-issued
MemberCreationId and typed provenance. A fresh, unrelated member receives a
new token even if it reuses a retired member’s display identity. Cold restore
preserves the token. An explicit runtime-authorized successor preserves the
original token and birth cursor and records its exact predecessor binding.
SpawnMemberSpec::new defaults to Unproven. Trusted host ingress must use
SpawnMemberSpec::host_root explicitly. Agent-lane handles downgrade that
claim; authenticated source witnesses establish delegated ancestry instead.
Policy auto-spawn remains Unproven, because a host-configured policy does not
prove that the resulting creation was independent of member activity.
RPC and REST spawn ingress also remains Unproven until it carries an
authenticated originating principal. Transport choice alone is insufficient;
that authority work is tracked in #1646.
Historical creation facts remain in the existing journal-backed roster
projection after retirement. MobHandle::member_creation_for_session reads
the facts for an exact session, and member_creation_journal_cursor exposes
the committed journal position. These reads do not authenticate a current
caller. Hosts must independently validate its current session/member binding.
Conflicting history is an error, not evidence that an ancestor is absent.
MobHandle::capture_member_creation_source issues a sealed process-local
MemberCreationSourceWitness after checking the current and persisted typed
binding. Agent arguments, transcript text and comms labels cannot
construct that witness. Unsupported source authority remains unknown. A
delegation edge records ancestry only; it never grants execution rights or
resource access and carries no serialized tool policy. Current tool execution
policy and host access policy still apply to every call. Optional consumers
that need a source policy ceiling must read it through the native session API;
missing proof cannot be interpreted as unrestricted access.
MobSessionService::load_retained_session_metadata is the supported
exact-session read for ancestry and policy-ceiling observations. Its
PersistedSessionMetadataView contains the latest committed metadata of the
exact retained session, including archived sessions. Persistent services read
through the authoritative native metadata owner. An absent session returns
None; corrupt metadata returns an error. Unsupported backends return a typed
Unsupported error. The method never substitutes a successor, revives a
session, permits a write, or changes ordinary
load_persisted_session_metadata visibility. It does not provide a historical
revision, lease or currentness guarantee. A valid present metadata view whose
resolved tool policy is None has the native meaning of unrestricted; missing
metadata or an unresolved policy must not be interpreted that way.
An optional resource service can intersect its current ACL with the current
policies of the exact source and executing sessions. In this dynamic model,
an authorized host policy change can widen an existing resource-access route;
the native policy owner records that change. An ancestry record is never a
frozen policy ceiling. Native tool execution still makes its own decision.
These observations identify an executing agent, not the original human
requester, and are not an execution-time currentness fence. Native originating
authority and governed execution work continue separately in
#1646 and
#1730.
Trusted hosts can install MobBuilder::before_activation to bind optional
services before fresh or restored members execute. The MobBeforeActivation
callback receives a MobReadHandle backed by the same roster and lifecycle
watch as the eventual public handle. Only direct read operations are exposed;
the type cannot submit actor commands while the actor has not started.
A callback error aborts bootstrap. Hosts must discard provisional service
bindings if a later bootstrap step fails. With no callback configured, no host
hook runs.
MobMcpState::set_before_activation carries that hook into delegated child
mobs. set_additional_child_tool_bundles carries the matching optional tool
factories, so children can use a configured host service. Each factory uses
the actual executing caller’s resolver; registration conveys no inherited
parent authority and exposes no tools unless the host selects their bundle.
Compaction contract
Compaction is optional. Ordinary recoverable failures preserve uncompacted history and normally reportCompactionFailed, allowing the run to continue.
- Triggered by token thresholds and turn guards.
- A typed provider
PolicyStopfrom the summary call terminates the run. - Unresolved durable compaction-stage authority or cleanup errors, such as
SessionDurableProjectionAuthorityUnknown, also terminate fail-closed. - Preserving history does not guarantee the next request fits the provider’s capacity; an unrepaired oversized request can still fail.
- Compaction usage counts toward run budgets.
EventStore audit append failure does
not undo an already-committed turn; that guarantee does not permit ignoring
uncertain compaction-stage authority.
Durability
Ephemeral mode
No durability across process restart.Persistent mode
Durability follows the selected realm storage provider and its declarations for the seven owned domains. The built-in persistent realm backends aresqlite and jsonl; an external RealmStorageProvider may supply other
persistent stores, but it must declare every domain and may not silently
substitute memory for one declared durable. Both built-in 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 before installing current
representation and authority. Ordinary store opens and v3 reads remain strict
and never enter the bridge. Catalog authentication is per domain and row
admission is per record: a row the current typed contract cannot represent
without loss is left byte-identical on disk and reported, rather than costing
the realm every other row. The maintenance transaction preserves input rows -
including their ingress payloads, whatever lifecycle state they reached -
without scheduling or replaying them. A later session activation follows normal
recovery, which applies this binary’s ordinary terminal-payload retirement to
rows it writes itself.
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. A provider’s
typed DeclaredEphemeral resolution is already an explicit declaration and
does not need a duplicate manifest entry. Neither path permits 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 appended at the current foreground
model-request tail at the authorized request boundary. No conversational-row
anchor is required. They are never System messages, transcript appends, rewrite
records, compaction input, or extraction input. This is separate from durable
injected_context, whose rows are appended before the turn’s conversational
user message.
Deferred creation cannot carry this context because no runtime input exists
yet; attach it to the eventual turn instead. Browser/WASM sessions carry it
through the same runtime-input path, with in-memory retention for the page
lifetime.
Request-only and durable steer
Asteer input admitted while its session’s turn is running reaches that
turn at the next cooperative model boundary: the point after the model returns
tool calls and before the next model request. MeerkatMachine classifies every
steer admission into one delivery class from the input’s typed structure, never
from its content:
A synthetic refresh-projection notice is one the next boundary’s notice
refresh replaces:
background_job and mcp_pending notices whose blocks are
not persisted (for example shell job progress), and auth_reauth_required
notices. Written into a running turn it would vanish from later requests, so it
takes the follow-up turn. A background_job notice whose block is persisted
(the durable completion record of a detached fork_off or council job) is not
a refresh projection: it is durable history and eligible for in-turn
delivery.
Turn metadata that only a fresh turn applies is everything else a runtime turn
start honours: System prompts, skill references, a tool overlay, additional
instructions, request-local context, model, provider or auth routing,
keep-alive, render metadata, a terminal peer-response intent and directed
interaction IDs.
A durable in-turn append joins a run that has already started: the run’s
run-start hooks ran when it began and do not run again for the late rows (the
same as for tool results). Incoming comms blocks among the appends publish the
same peer_content_ingested events the follow-up path publishes.
A durable steer that arrives while the model is streaming waits for the next
boundary of the same run. If the run ends first (the model answered without
another tool call), or the boundary refuses Session writes (structured-output
extraction), the input takes the ordinary follow-up turn, which writes the same
transcript rows. It keeps its admitted steer lane, so the runtime serves it
first after the run, ahead of queued work. Exactly one of the two paths writes
them:
- The input joins the running run as a late contributor before its payload reaches the runner. Persistent runtimes make that binding durable first.
- At the run terminal the runtime reads the typed delivery witness. An append that is in the session image the run commits or keeps is consumed with the run. An append the runner never wrote, or one whose image is discarded, returns the input to the head of its lane for one follow-up turn. A failed or cancelled run on a persistent session discards its image (the next turn resyncs from the store); an ephemeral session keeps it; a compaction rollback discards every append written after the rollback point.
boundary_append_appliedis emitted when the runner writes the rows. It identifies the run and input, carries the model-projected content and append count, and can include the exact canonical notice rows and the first appended row’s transcript offset at application time.- Reconcile applications using
boundary_appends_discardedand canonical transcript history. The discard event identifies the exact session, run, and input IDs whose applications were discarded, and is published after any required requeue persistence succeeds. A run terminal alone is not proof of discard. This event does not terminalize the input or invalidate a later application in another run; canonical retained history remains authoritative.
Stopping one run
MeerkatMachine::stop_run(session_id, expected_run_id, reason) stops exactly
one run and every input already bound to it. The exact-input
cancel_input_if_present and the exact-run hard_cancel_run_if_current only
interrupt the run, so a durable steer that joined it still follows the rules
above and can take a follow-up turn.
- Under the session mutation gate, the machine compares its current run with
expected_run_idand records the stop (StopCurrentRunForRun). It then dispatches the exact-run hard interrupt. - From that point the run admits no durable join. A steer that reaches its boundary later is not a contributor of the stopped run. It stays queued and runs afterwards like any other queued input.
- At the run terminal, a joined durable steer whose append is not in the
surviving image is abandoned as
Cancelledinstead of returning to its lane. A retained append is consumed with the run. A failed attempt never replays its staged batch. - The call returns
RunStopReceipt::Stopped { run_id, contributors }once every contributor staged at the stop has reached its canonical terminal. Each entry carries the delivered completion and the committed input terminal. A cancelled join’s completion isRuntimeTerminated, while its committed terminal isAbandoned { reason: Cancelled }, so read the terminal for the lifecycle fact. - Once the stop is committed, an interrupt dispatch that fails, times out, or finds the executor not yet inside the run does not fail the call. The run still ends as a stopped run, and the call returns at its terminal.
- A stop whose run is no longer current returns
RunStopReceipt::NotCurrent { run_id, current_run_id }and touches nothing. Queued input and newer runs are never interrupted. - If a runtime stop or teardown has taken the still-bound run between the
compare and the stop, the call returns
RunStopReceipt::NotStoppable { run_id, state }and stages nothing.
