Skip to main content

Durable Instruction Activation

Status: Accepted for implementation, pending integrated review and CI Date: 2026-08-25 Scope: persistent Meerkat sessions and model-visible textual instructions

Need

Long-lived agents need to adopt reviewed instruction revisions without silently changing the meaning of an earlier transcript. The durable change must be reproducible, auditable, exactly-once under retry, safe while a turn is active, and distinguishable from a controller’s desired assignment. The application remains responsible for authoring instruction content, evaluating it, approving it, and choosing a rollout. This primitive does not hot-swap executable skill resources, tool implementations, hooks, provider configuration, or build composition. Those remain with their existing typed factory and runtime owners and may require respawn or a new lineage.

Existing authority is the baseline

Meerkat does not need another chronological delivery mechanism:
  • Session owns the ordered transcript.
  • Message::System is repeatable and position-sensitive.
  • provider lowering consumes the transcript order when the resolved model can represent mid-conversation System rows.
  • the persistent session service already serializes control work with turn finalization and recovery gates.
  • an append admitted while a content turn is active cannot alter that in-flight request.
The missing generic capability is typed artifact and activation identity, CAS, durable receipts, safe persistence, compatibility admission, introspection, and rewrite protection on that existing ordered transcript fact. session/update_system_prompt owns a different semantic. It versions a replaceable prompt slot through transcript rewrite and projects only the latest version. Reusing it would silently reinterpret older conversation under a new prompt, so it is not the instruction-activation authority.

Rejected alternatives

A parallel activation ledger or pending table

Rejected. It would create a second answer to what entered model context and a cross-store commit problem. Desired rollout is application/Meta state; effective durable context is Meerkat transcript state.

Generic append metadata or string conventions

Rejected. source, idempotency strings, or rendered prefixes cannot prove artifact identity, content digest, supersession, rollback, or stale-writer rejection. Typed truth must live on the transcript row.

Keyed prompt replacement

Rejected. Latest-only projection erases the chronological transition from model context and changes the interpretation of prior conversation.

Cumulative raw instruction bodies

Rejected. Two equal-priority bodies that happen to occur in order do not define supersession. Contradictory revisions would both remain instructions.

Fork on every revision

Rejected as the default. A new lineage is valid for destructive reset or a provider that cannot represent the transition, but ordinary compatible agents should retain continuity.

Domain fact and model projection

An activation is an ordinary ordered Message::System row carrying a sealed typed instruction-activation identity. It is not a new queue, message role, or session metadata ledger. The canonical row stores one exact, versioned render envelope. The v1 envelope contains JSON-quoted single-line identity headers, a blank-line delimiter, then the exact approved UTF-8 body without normalization. The typed identity stores render_version = 1 and the explicit predecessor in supersedes. Deserialization re-renders from the typed fields and body and requires byte equality with SystemMessage.content. In the origin session, model materialization preserves the row’s role, position, and already-stored envelope bytes. The origin envelope states:
  • the named revision becomes effective from this transcript boundary
  • it supersedes the earlier activation for the same namespace and key
  • the exact approved instruction body follows
The same render version also permanently defines an inherited historical-only envelope for a child SessionId; it quotes the original envelope as historical context and states that it is not an activation in the child. This explicit versioned rendering, rather than ordering folklore, gives the later body its same-key supersession meaning and makes replay independent of a future renderer upgrade. Earlier rows remain durable evidence for conversation that occurred under them. New render versions are additive and permanently ratcheted; an existing row is never re-rendered under a newer version. The exact v1 origin envelope uses LF separators, headers in the order below, canonical JSON string escaping for string values, lowercase digest hex, decimal body byte length, one empty line, then the body bytes with no added suffix:
A fork child’s model reads a row it inherited through the v2 projection: the first line becomes [meerkat-inherited-instruction-v2], child_session_id=<json-string> follows origin_session_id, status=not_active_in_child precedes body_bytes, and the projection ends after body_bytes=<decimal>, with no body. A body reaches a model only from a row effective in that session: its own activation or re-activation. The projection exists at the model boundary only; the stored row keeps the envelope above, or the retired form below, and a row whose bytes do not match its identity fails the projection and refuses the model request. Releases up to 0.9.4 rendered a v1 projection that kept the body. A compaction retires the body of a superseded row (see “Rewrite, compaction, restore, and fork”). The retired row keeps its typed identity, its timestamp and its place, carries the typed marker retired_instruction_body { render_version, body_bytes }, and stores, and its origin session’s model reads, the retired header: the v1 header lines, status=superseded, and body_bytes, with no blank line and no body:
The identity is unchanged, so its digest still binds the retired body, and the marker keeps the body’s length. The marker is valid only beside an activation identity, its render_version is 1 (additive and permanently ratcheted, like the activation render versions), and deserialization re-renders the header from the identity and the marker and requires byte equality. A fork child reads a copied retired row through the same v2 projection as a live copy, taking body_bytes from the marker, so its requests do not depend on whether the source compacted before the fork. The session document keeps the marker strict; history reads carry it as WireRetiredInstructionBody: Known whenever its fields decode (a later render version with the same two fields included), and Raw as it was sent for one with a field the reader does not know, so a newer member host’s history never fails an older reader’s page. A reader treats either form as a retired body. The sealed identity contains:
  • InstructionNamespace, an application or issuer namespace
  • InstructionKey
  • InstructionRevisionId
  • InstructionContentDigest
  • InstructionActivationId
  • supersedes: Option<InstructionActivationId>
  • origin_session_id, the Meerkat SessionId that minted the row
  • render_version
The digest is canonical lowercase sha256:<64 hex digits> over the exact UTF-8 bytes of the approved body. Requests enforce configured body and identifier bounds. Meerkat verifies the digest before mutation. Within one retained session lineage, one (namespace, key, revision_id) can bind only one digest and body; a row whose body a compaction retired keeps the digest, which binds the body, and the body’s length. Global cross-session artifact immutability remains application-owned; Meerkat is not an artifact registry. The System row’s semantic carriers must be mutually exclusive. An ordinary append identity, replaceable prompt-version identity, and instruction activation identity cannot coexist. All constructors, deserialization paths, head materialization, rewrites, compaction, and activation validate this closed invariant. origin_session_id is the lineage fact. Meerkat does not infer a Meta generation or invent a second lineage identifier. In the origin session, the latest activation for a key is effective. In a fork whose SessionId differs, copied rows are inherited historical context until that child appends an origin-local activation.

Compatibility, machine admission, and effective truth

An activation receipt means the row is committed to the authoritative persistent session. Applied additionally means the already-materialized session projection was converged to that committed transcript before the command completed. No disposition claims that a provider request has already occurred. Repository evidence rules out a separate actor-retirement mechanism for the compatible text path. Every turn rebuilds its model request from Session::messages_for_model_boundary; a compatible provider does not retain a second instruction context inside the actor. The existing runtime-turn finalization boundary already serializes a transcript mutation after any active turn and before a new one. The activation facade therefore holds that boundary, asks the generated runtime owner for the current transcript-edit admission verdict, checks the machine-owned resolved capability surface for the current materialization, rejects an open realtime channel, and only then calls the persistent typed mutation seam. Model reconfiguration threads its already resolved target capability through live apply and rollback; it never re-reads a mutable registry between generated admission and installation. This is intentionally not a new MeerkatMachine transition. The machine already owns the active-work admission fact used here. Adding another generated state plane would duplicate transcript authority without adding a runtime fact. The persistent service remains unable to expose activation directly through the generic control extension; surfaces must use the facade that holds runtime admission and compatibility. Admission outcomes are:
  • a model without mid-conversation System support returns UnsupportedCurrentLowering and appends nothing
  • a detached, dormant, staged, unknown, or otherwise unmaterialized target returns TargetNotMaterialized and appends nothing in v1
  • an open live channel returns LiveChannelOpen and appends nothing; v1 does not claim a close/reopen handoff it has not completed
  • a staged session returns TargetNotMaterialized
  • an ephemeral service returns DurabilityUnavailable
Store-backed reads remain available for detached and dormant sessions. Mutation does not pretend that persisted LLM metadata alone proves a current lowering. Provider adapters remain a final fail-closed guard after the catalog-owned admission check. A later model hot-swap that cannot represent the already recorded ordered System rows must also fail closed before the new identity is installed.

CAS, idempotency, and rollback

InstructionActivationExpectation is a closed enum:
  • Absent
  • Effective(InstructionActivationId)
There is no optional or wildcard predecessor. Activation IDs are scoped by session lineage, namespace, and key and bind an immutable request fingerprint.
  • An exact retry while that activation is still effective returns Duplicate. When the caller supplied an external write fence, the retry still enters the target-locked RuntimeStore boundary and revalidates that fence before the receipt is returned; it appends no second activation row.
  • Reusing an activation ID with another ref or body returns ActivationIdentityConflict.
  • Replaying an exact old activation ID after a later activation returns RecordedButNotEffective; it never proves adoption and never reactivates it.
  • A stale predecessor returns EffectiveActivationConflict and appends nothing.
  • A repeated revision ID with another body or digest returns ImmutableRevisionConflict.
Rollback is a new activation ID naming an earlier immutable revision. It is an append at a later transcript boundary, never erasure or restore. This prevents ABA ambiguity.

Persistent and cancellation-safe mutation

Only PersistentSessionService implements durable activation. The operation uses the existing turn-finalization and recovery gates and the existing actor command path. The session actor derives and appends the exact activation. The persistent owner then commits that actor state through the configured WholeBlob or HeadCanonical boundary. There is no detached candidate document, post-commit actor convergence step, or policy-specific recovery state machine. The command runs in a detached owner task that retains the turn boundary until the actor-owned durable commit completes. Transport or caller cancellation can drop only its waiter, not the commit owner. A persistence failure returns no receipt and discards the mutated actor through the existing cleanup path, so that actor cannot admit a later turn or overwrite canonical state. A lost successful response is recovered by querying canonical activation records before retry; an exact retry returns Duplicate. When live support is enabled, activation first acquires the existing machine-owned live/open lifecycle lease, then the turn-finalization boundary, and rechecks live-channel absence and generated admission while both are held. This prevents a new live/open from crossing the actor/store commit and makes an already-started live/open finish before activation decides admission. An external composition may supply a generic RuntimeStoreWriteFence through the explicit fenced activation verb. Meerkat first owns its existing live-open lease and turn-finalization boundary. The selected RuntimeStore then invokes the fence synchronously from inside its own target write lock or SQLite transaction, with the physical target publication itself as the fenced operation. Applied means that operation ran exactly once. Conflict and Backoff mean it did not run, so neither durable transcript nor live session projection advances. The fence must acquire external lifecycle authority without waiting on anything that can wait for Meerkat, reread its canonical facts, and retain its serialization guard while invoking the target publication. This avoids lock inversion with an external retirement path that already owns lifecycle authority and is waiting for Meerkat’s safe boundary. execute_if_current waits for nothing; a caller that holds an async lock retries its Backoff with the lock released. A caller already on a blocking thread, such as the SQLite RuntimeStore, uses execute_if_current_blocking, where an implementation may wait a bounded time for a lock whose holders never wait for Meerkat (the SQLite identity fence waits up to 2 seconds for its database’s write lock). Meta uses the seam for stable identity, generation, session, and fencing-token continuity. Meerkat does not persist or interpret a transient runtime alias, and the fence remains a precondition on the existing RuntimeStore commit rather than another activation authority. RuntimeStore decorators must opt in to the fenced verb explicitly; the default is a typed unsupported refusal so a decorator cannot silently bypass its projection or recovery behavior. Physical store revisions and fencing tokens remain server-internal. A public command receipt binds session ID, lineage, full activation identity, predecessor, a stable activation ordinal, the current transcript projection witness, and the event’s typed disposition. Persistence introspection derives that record from the RuntimeStore-backed transcript, never from a receipt cache. It does not reconstruct the historical live-projection disposition. Current rollout observation may join that durable record with runtime availability, but it may never replace the record with a cache.

Rewrite, compaction, restore, and fork

Activation rows are immutable transcript-boundary anchors.
  • Ordinary append and prompt-update APIs cannot mint or adopt them.
  • Generic same-session rewrite and prompt update require exact equality of every activation row at its place among the conversation rows: the same rows, byte for byte, in the same order, each with the same number of non-System rows before it. They may add, remove or reorder ordinary System rows around an activation row (a keyed prompt update inserts a later version before one); they cannot mint, alter, move, erase, or restore an activation across conversational content, and cannot retire a body.
  • Same-session restore requires the same of every activation row. It cannot move an activation across conversational content, restore an erased activation boundary, or bring a retired body back.
  • A rewrite record that moves an activation row to another offset is written at session envelope 5: releases before 0.9.9 compared activation rows at their offsets and would refuse the record, so they refuse the document typed at its envelope instead. Records those releases wrote keep validating.
  • Deserialization validates current rows and every retained revision body.
  • Fork-at may copy only exact activation rows from the selected source prefix. Generic fork replacement rejects every activation-bearing replacement, so it cannot forge inherited or effective authority.
  • In a child session, copied rows render with a permanently versioned historical-only envelope. A child with inherited rows cannot start a model turn until it has appended an explicit origin-local activation for every inherited effective key. The child activation may name the inherited row in supersedes while its CAS expectation remains Absent for the child origin.
  • The host’s fork instruction policy (ForkInstructionPolicy) decides who appends that activation, at the durable fork commit, before the child’s one save:
    • require_explicit_activation, the default, leaves it to the host. A fork that leaves the host no window to do so is refused instead, before anything is committed, when the copied prefix carries activation rows (FORK_INSTRUCTION_REFUSED, cause policy_refuses). Those forks start the child’s first turn themselves (fork_off, the bounded and detached member forks, the live delegation’s durable fork, council participants), or commit a child the host cannot activate (session/fork_at and session/fork_replace, whose child is not materialized).
    • refuse refuses every fork whose child would inherit a row.
    • reactivate_inherited makes the fork append one origin-local row per inherited key, through the ordinary activation rules. Each row re-activates the key’s head at the fork boundary: the same revision and body, supersedes naming the head, expectation Absent. The rows follow the prefix in (namespace, key) order, carry the timestamp of the last copied message, and take the id fork-reactivation: plus 32 hex digits of a SHA-256 over the child id and the head’s id, so a child is a function of its prefix and its id. The id prefix is presentation only; a verifier recognizes the rows by reproducing them. A head the source only inherited and never activated is refused (not_effective_in_source), since re-activating it would grant authority the source never ran under. A head whose body a compaction of the source retired is refused (head_body_retired) before anything is committed: a fork whose prefix ends between a retired row and the row that superseded it has no body to carry. A fork at the tail copies, with its body, each key’s newest row the source activated itself (a compaction never retires it); a host can also fork at a later boundary or activate the key in the child.
  • Re-activation adds each effective body to the lineage again, so it counts against the 1 MiB budget of live bodies, and a fork that would cross it is refused (lineage_body_budget_exceeded). The child’s first compaction retires the copied rows its own activations supersede, so nested forks do not carry every superseded copy.
  • A fork does not repeat the lowering check that activation runs at its source. The child’s first model call stays the final fail-closed guard, as it is for the copied historical rows.
Meerkat’s compactors retain System rows, and every activation anchor stays in the live ordered transcript at its place, while ordinary conversation may be summarized around it. Compaction never invents, reorders, moves or deletes an activation. It retires the body of every superseded row: a row that a later activation row of its key follows. The newest row of every key keeps its body, so the session’s effective activation of a key is never retired, and in a fork child the copied rows of a key the child activated itself are retired at the child’s first compaction. A retired row keeps its identity, timestamp, place and activation ordinal, and becomes the retired form above; retirement is absorbing. SessionDocumentMachine decides which rows. Before a compaction the session recovers the machine’s activation-body region from its transcript: per key, how many activation rows there are (every origin), how many of them, oldest first, carry a retired body, and whether the newest row is the session’s own. The machine refuses a torn projection, and the compaction then fails closed and keeps its history; otherwise it retires every superseded row, never lowers a recovered count, and never retires an own newest row. The session maps each key’s retired count onto that key’s oldest rows; the row validator holds that a key’s retired rows precede its live ones, so the mapping is exact. No compactor can retire a body: a compactor’s own rebuild must keep every activation row byte for byte, and only the core fold step that follows puts the authorized rows in their retired form, validated row by row against the source (the exact retired form, never a key’s newest row). A generic rewrite or restore that retires or revives a body is refused. The rewrite graph accepts a record whose activation row is the exact retirement of a live parent row that a later row of its key follows in the revision, and never requires one, so records earlier releases wrote keep validating. The witness is on the row, not in retained history (the graph keeps old bodies for a bounded number of rewrites only): the unchanged identity carries the digest, and the marker carries the body’s length. An exact retry of a retired activation’s id compares the stored header with the retired render of the requested identity and body length, and answers recorded_but_not_effective, as a live superseded row does; any other request with that id answers activation_identity_conflict. A revision binds one digest and body length, so a retired revision id with another body is immutable_revision_conflict. Rollback is unchanged: the caller supplies the body, and the digest proves it. No summarizer, host curator or compactor input reads a body the compaction retires; it reads the retired header, so a summary never restates a superseded body. The 256 KiB per-body limit and the 1 MiB budget bound growth. The budget counts live bodies only: a retired body stops counting at the commit that retires it. Between compactions superseded bodies still count, so a burst of activations can meet the typed refusal before the next compaction; a host can retire them on demand, without waking the session, through the system-row prune’s retire_superseded_activation_bodies (SessionServiceTranscriptEditExt::prune_system_rows), whose one commit takes the same SessionDocumentMachine decision a compaction does and is checked the same way: the rewrite guard compares the activation rows with the source as the authorized retirement leaves it. An exact retry of the current activation remains legal at the budget because it appends nothing. A document that holds a retired body is written at session envelope 5 (the session records its instruction state, as a fork that copies a retired row does), so releases before 0.9.9 refuse it: typed at its envelope where they read a store’s head row first, and as malformed instruction activation history where they decode the whole document, since 0.9.8 checks activation rows before the envelope version there.

Typed public contract

All identifiers are non-empty trimmed UTF-8, at most 128 bytes, and reject control characters. Instruction bodies are at most 256 KiB. Namespace naming does not introduce an authorization system: the application remains responsible for exposing the activation command only through its governed approval workflow, while Meerkat authenticates and authorizes the containing surface according to that surface’s existing rules. InstructionActivationRequest contains:
  • namespace, key, revision ID, declared digest, and exact body
  • activation ID
  • closed CAS expectation Absent | Effective(id)
  • explicit supersedes, which must equal the observed previous same-key activation when present but is not the CAS carrier
InstructionActivationReceipt contains session ID, origin session ID, full activation identity, predecessor activation ID, the stable zero-based ordinal among activation rows, the current projection witness, and Applied | Duplicate. Identity and ordinal are stable across compaction. The projection witness contains the current zero-based message index and current prefix revision, so it can change when compaction summarizes earlier conversation. Applied proves the actor-owned materialized session state was committed by the lifecycle/persistence owner. InstructionActivationRecord is persistence-only and contains the durable identity, origin, predecessor, stable activation ordinal, and current projection witness. InstructionActivationReadQuery accepts optional namespace/key filters and a bounded offset with limit from 1 through 200. InstructionActivationReadPage returns durable records in transcript order and an optional next offset. When the query names one exact namespace and key, it also returns core-derived InstructionActivationKeyState: the effective origin-local record, chronological head, child explicit-activation requirement, and exact expectation/supersedes values for the next request. Meta consumes this state instead of reimplementing transcript or fork semantics. Stable InstructionActivationErrorCode variants are:
  • invalid_request
  • digest_mismatch
  • effective_activation_conflict
  • activation_identity_conflict
  • immutable_revision_conflict
  • recorded_but_not_effective
  • inherited_activation_requires_explicit_activation
  • malformed_activation_history
Stable InstructionActivationAdmissionErrorCode variants are:
  • target_not_materialized
  • unsupported_current_lowering
  • live_channel_open
  • durability_unavailable
  • session_busy
  • external_write_fence_conflict
  • external_write_fence_backoff
JSON-RPC maps admission refusals to stable typed data and preserves domain codes from the persistent seam. No activation refusal is inferred by parsing its message. Core types are:
  • InstructionNamespace, InstructionKey, InstructionRevisionId
  • InstructionContentDigest, InstructionRevisionRef
  • InstructionActivationId, InstructionActivationIdentity
  • InstructionActivationExpectation, InstructionActivationRequest
  • InstructionActivationDisposition, InstructionActivationReceipt
  • InstructionActivationRecord
  • InstructionActivationError with stable error codes for every refusal above
Service/facade methods are:
  • MeerkatSessionRuntime::activate_instruction
  • MeerkatSessionRuntime::activate_instruction_with_write_fence
  • MeerkatSessionRuntime::read_instruction_activations
  • SessionServiceHistoryExt::read_instruction_activation_records as the persistence-only internal/read-model seam
Surfaces:
  • JSON-RPC session/activate_instruction and session/instruction_activations
  • generated Rust wire and JSON Schema contracts
  • model catalog capability supports_mid_conversation_system_messages
Transport handlers only parse, authorize, invoke the service, and lower typed errors. They do not classify state or rebuild receipts.

Ownership and failure table

Release binding and delivery

The reviewed durable activation authority is integrated into the release authority’s exact qualified realtime Meerkat head for the 0.8.30 candidate. Its manifest and lockfile both resolve the public oai-rt-rs 0.4.1 release, tagged from exact commit c051ee75479aaf71a8f1fd5cef2ea2bbe90f15e1 and independently verified from crates.io with SHA-256 fe8d87027059933028b385e8ea9059d452cad805c632531ed3c20314492b6aa8. Until the candidate is merged, tagged, and published, this exact integration commit remains development Git truth rather than a released package. MobKit binds its Meerkat dependency to the exact origin-reachable integration commit, not to a package release or movable branch, and preserves the same locked Realtime release. Cross-repository CI consequently tests the development graph it claims to review. After the integrated gate, the release authority owns BuildBuddy Turbo S e2e-smoke diagnosis, repair, rerun, and green evidence on that exact Meerkat commit. The exact Meerkat/MobKit pair must then pass isolated no-deploy downstream qualification before the Meerkat lead may publish 0.8.30. This feature lane does not independently merge, tag, or publish either repository.

Dogma check

  • The ordered transcript remains the sole model-context authority.
  • No new activation machine, queue, or ledger is introduced; the retirement decision is a region of the existing SessionDocumentMachine.
  • RuntimeStore alone proves physical currentness.
  • Provider capability and live state fail closed before mutation.
  • Typed core code owns semantics; surfaces stay thin.
  • Global policy governance stays outside Meerkat.
  • Durable receipts are unavailable where durability cannot be proved.