Durable Instruction Activation
Status: Accepted for implementation, pending integrated review and CI Date: 2026-08-25 Scope: persistent Meerkat sessions and model-visible textual instructionsNeed
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:Sessionowns the ordered transcript.Message::Systemis 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.
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 orderedMessage::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
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:
[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:
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 namespaceInstructionKeyInstructionRevisionIdInstructionContentDigestInstructionActivationIdsupersedes: Option<InstructionActivationId>origin_session_id, the MeerkatSessionIdthat minted the rowrender_version
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
UnsupportedCurrentLoweringand appends nothing - a detached, dormant, staged, unknown, or otherwise unmaterialized target
returns
TargetNotMaterializedand appends nothing in v1 - an open live channel returns
LiveChannelOpenand appends nothing; v1 does not claim a close/reopen handoff it has not completed - a staged session returns
TargetNotMaterialized - an ephemeral service returns
DurabilityUnavailable
CAS, idempotency, and rollback
InstructionActivationExpectation is a closed enum:
AbsentEffective(InstructionActivationId)
- 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
EffectiveActivationConflictand appends nothing. - A repeated revision ID with another body or digest returns
ImmutableRevisionConflict.
Persistent and cancellation-safe mutation
OnlyPersistentSessionService 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
supersedeswhile its CAS expectation remainsAbsentfor 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, causepolicy_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_atandsession/fork_replace, whose child is not materialized).refuserefuses every fork whose child would inherit a row.reactivate_inheritedmakes 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,supersedesnaming the head, expectationAbsent. The rows follow the prefix in (namespace, key) order, carry the timestamp of the last copied message, and take the idfork-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.
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_requestdigest_mismatcheffective_activation_conflictactivation_identity_conflictimmutable_revision_conflictrecorded_but_not_effectiveinherited_activation_requires_explicit_activationmalformed_activation_history
InstructionActivationAdmissionErrorCode variants are:
target_not_materializedunsupported_current_loweringlive_channel_opendurability_unavailablesession_busyexternal_write_fence_conflictexternal_write_fence_backoff
InstructionNamespace,InstructionKey,InstructionRevisionIdInstructionContentDigest,InstructionRevisionRefInstructionActivationId,InstructionActivationIdentityInstructionActivationExpectation,InstructionActivationRequestInstructionActivationDisposition,InstructionActivationReceiptInstructionActivationRecordInstructionActivationErrorwith stable error codes for every refusal above
MeerkatSessionRuntime::activate_instructionMeerkatSessionRuntime::activate_instruction_with_write_fenceMeerkatSessionRuntime::read_instruction_activationsSessionServiceHistoryExt::read_instruction_activation_recordsas the persistence-only internal/read-model seam
- JSON-RPC
session/activate_instructionandsession/instruction_activations - generated Rust wire and JSON Schema contracts
- model catalog capability
supports_mid_conversation_system_messages
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 publicoai-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.
