> ## Documentation Index
> Fetch the complete documentation index at: https://docs.rkat.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# MobKit Storage Unification Plan

> Companion arc to Meerkat's storage unification: one path authority for MobKit's nine databases, shared SQLite mechanics, fail-closed durability for every MobKit store slot, a single remote bundle over the Meerkat provider seam, and de-welding the judgment plane from concrete SQLite.

**Companion document.** Meerkat's storage unification plan
(`docs/plans/storage-unification-plan.md` in the meerkat repo) names a
"companion arc: mobkit adoption" in its Phase 4: half of downstream storage
pain lives in MobKit-layer contracts, and without a matching arc downstreams
keep two integration seams and two heads of session authority. This document
is that arc. Meerkat owns the contracts MobKit consumes (the `StorageLayout`
path authority, the `meerkat-sqlite` mechanics crate, the
`RealmStorageProvider` seam, the durability vocabulary, the conformance
profiles, the exported legacy-checkpoint stamping helper, the maintenance
fence). MobKit owns everything below: its own store traits, its own database
files, its own silent-fallback hazards, and the judgment plane.

Evidence in this document is from a survey of this repo (2026-07, MobKit
workspace version **0.8.1** — release tags `v0.8.x` — on Meerkat `=0.8.2`).
An earlier draft said "MobKit 0.6.55 on the 0.6.x line"; that was wrong, and
worth correcting loudly because "which line do I pin" is exactly the
ambiguity this plan exists to kill: MobKit's crate version and release tags
are both 0.8.x, and the hotfixes below ship on the **0.8.x** line.

**Revision 4 (implementation).** The arc is implemented on the
`storage-unification` branch (hotfixes H1-H3 plus phases M0-M6, on Meerkat
`=0.8.3` carrying the upstream storage arc). Deltas discovered and decided
during implementation, recorded here so the plan stays honest against the
code: the canonical database spellings are the `*.sqlite3` forms with
canonical-name-first probing and legacy names accepted as read twins
(`mobkit doctor` reports both; migrate renames under the fence with a
registered rename marker); the meerkat-sqlite ledger domains are
`mobkit-memory`, `mobkit-continuity`, `mobkit-metadata`, `mobkit-console`,
and the memory admission sidecar is deliberately **ledger-exempt** (it is
derived state, rebuilt rather than migrated); H3 shipped as batch
adoption via direct SQL under the maintenance fence plus the sanctioned
lazy-at-restore variant in the continuity adapter, both driving observed
cursors; the judgment-plane de-weld landed as capability traits
(`StewardStore`, `TaintableStore`, `MemoryPanelStore`) and
`as_sqlite_store` is deleted; the M4 composite removed the builder's fifth
storage fallback, and **incremental local continuity now ships** — the
deferral is closed: head+rows are the canonical durable session
representation in `continuity.sqlite3`, so an identity-member turn appends
O(delta) instead of rewriting the whole document, every whole-snapshot verb
is representation-aware (a head row means the blob row is a frozen archive,
never read or written again), and the one-way `mobkit-continuity` v2 ledger
bump is committed inside the transaction of a delta write that actually
creates head state (a refused write rolls the DDL and the stamp back with
it; an ACCEPTED write that adopts nothing commits its rows and leaves the
file at v1) or by `storage-migrate --apply`, never as a side effect of a
gateway launch, and the bump is one-way — there is no in-place path back;
recovery from a bad upgrade is restoring the state directory from a
consistent backup taken before the upgrade, accepting the loss of the
turns taken since; a `ContinuityStore` default-method rollback defect found
during M4b is fixed rather than worked around; the SDK durability
vocabulary ships exactly what the wire supports (blob ephemerality is
Rust-builder-only and census-visible, not wire-configurable), storage
refusals in the gateway init path get typed code `-32014` (refusals inside
`UnifiedRuntime::bootstrap` still surface as `-32603` with typed message
text — recorded limitation), and both SDK bootstraps had to check for an
init `RpcError` before their process-died rewrap for the typed error to be
reachable at all; the M5 gate bans ambient root resolution and locator
literals with an explicit allowlist (the layout authority itself, mobpack's
config-discovery HOME uses, wiring-const definitions, and the
doctor/migrate census files); prune only ever deletes **registered**
artifact names — a strict `<original>.pre-<dotted version>-<timestamp>`
/ `.corrupt-<timestamp>` parse, never a substring match; and
post-review hardening requires exactly one durability declaration per slot
in `REQUIRED_MOBKIT_DURABILITY_DOMAINS`, so a provider cannot dodge the
fail-closed rule by omission.

**Revision 3.** Revision 2 incorporated the second review round (HomeCore,
ob3, and an independent static review): the provider layering is split into
a MobKit-owned composite over Meerkat's seam, the filename-ownership
boundary is resolved, H3 gains the lazy-at-restore variant and the
deploy-transition composition plus the joint divergent-bytes acceptance
case, and the version line above is corrected. **Revision 3 rebases H3 on
meerkat PR #909**: the exported helper has landed, meerkat now lazily
auto-migrates its own session documents, H3 narrows to continuity
snapshots, and the INITIAL-cursor ordering constraint is recorded. A
disposition ledger is at the end.

## Context

The survey found MobKit's storage layer fragmented along axes that rhyme
with Meerkat's, plus three failure classes Meerkat does not have:

1. **Three copies of path derivation, with divergent file names.** The
   library builder (`unified_runtime/builder.rs`), `mobkit_gateway`
   (`src/bin/mobkit_gateway.rs`), and `rpc_gateway`
   (`src/bin/rpc_gateway.rs`) each derive the per-database paths
   independently. They disagree: sessions are `sessions.db` in rpc\_gateway
   (`rpc_gateway.rs:4471`) but `sessions.sqlite` in mobkit\_gateway
   (`mobkit_gateway.rs:390`); continuity is `continuity.db` in both gateways
   (`mobkit_gateway.rs:945`, `rpc_gateway.rs:4418`) but
   `identity_continuity.sqlite` in the builder (`builder.rs:792`). Two
   surfaces pointed at the same state directory silently operate on
   unrelated files — MobKit's rendition of Meerkat's dual-root split-brain,
   one directory level down. Only three of the nine database file names are
   shared constants; the rest are inline string literals.
2. **Five independently hand-rolled SQLite openers.** Agent memory
   (`memory/sqlite_store.rs:347`), continuity writer and read pool
   (`identity_first/local_store.rs:172,183`), runtime metadata
   (`runtime/metadata.rs:312`), the console aggregator
   (`console_aggregator/store.rs:458`), and the workgraph admission sidecar
   (`workgraph_admission.rs:163`). The WAL + busy-timeout policy is spelled
   three different ways; only the memory store names a constant
   (`SQLITE_BUSY_TIMEOUT_MS = 5_000`); the admission sidecar uses a 30 s
   outlier; and the console store sets **no PRAGMAs at all** — no WAL, no
   busy timeout, on the one database every console interaction writes.
   Nothing in the crate uses `meerkat_store::sqlite_store::open_connection`.
3. **No schema versioning anywhere.** Every store is
   `CREATE TABLE IF NOT EXISTS`; the only evolution mechanism is the memory
   store's ad-hoc `ensure_column` probes with backfill
   (`sqlite_store.rs:357,381`). No ledger, no `user_version`, no typed
   refusal when a newer binary's schema is opened by an older binary.
4. **Silent in-memory/null defaults for durable state.** Four confirmed,
   one of them the production incident the Meerkat plan cites:
   * **Blobs**: when no custom blob store is supplied and the local blob
     directory fails to open, `persistent_inner` logs a warning and
     constructs `ObjectStoreBlobStore::memory()`
     (`mob_handle_runtime.rs:3732-3746`; same pattern at `:3864-3871`).
     This is the exact code path behind the month-long silently-broken
     agent on GKE.
   * **Console timeline**: `InMemoryConsoleLogStore` whenever
     `persistent_state_path` is unset (`unified_runtime/builder.rs:701-714`,
     default at `unified_runtime/mod.rs:283`).
   * **Event log**: `EventLogConfig::default` installs `NullEventLogStore`
     (`unified_runtime/event_log.rs:131-140`) — worse than in-memory, it
     silently drops every event.
   * **Runtime metadata**: `InMemoryMetadataStore` whenever
     `persistent_state_path` is unset (`builder.rs:687-700`).
     Adjacent but distinct: schedule and workgraph take a "boot-without"
     posture — on open failure the tools are disabled with a warning
     (`schedule_wiring.rs:654-661`, `workgraph_wiring.rs:59-64`). That is
     better than a silent in-memory twin, but it is still a silent
     *capability* degradation invisible to health checks. Gating audit,
     delivery history, and routing resolutions are in-memory ring buffers
     with no store seam at all (`runtime.rs:1095-1118`) — implicitly Scratch,
     declared nowhere.
5. **`ContinuityStore` and `LeaseProvider` have no injection seam.** They
   are raw struct fields on `IdentityRuntimeConfig`
   (`identity_first/runtime.rs:326-345`), hardwired to
   `LocalContinuityStore` / `LocalLeaseProvider` in `gateway_wiring.rs`.
   `UnifiedRuntimeBuilder` has no method for either. A downstream with a
   BigQuery continuity store (ob3) must bypass the builder and hand-rebuild
   the identity substrate — one of the two "no public seam" gaps that force
   shadow infrastructure downstream.
6. **A confirmed capability swallow in the identity-first session path.**
   `ContinuitySessionStoreAdapter` (`identity_first/adapters.rs:677`)
   implements Meerkat's `SessionStore` over the `ContinuityStore` but does
   not override `as_incremental`, so it inherits the `None` default —
   despite Meerkat's trait doc saying delegating wrappers MUST forward it.
   `PersistentSessionService` probes that capability and silently degrades
   to whole-blob persistence when it is absent. Every identity-first
   deployment therefore persists O(session) instead of O(delta) on every
   turn, silently. This is precisely the failure Meerkat's Phase 0
   capability-discovery conformance chapter exists to make loud. (Note the
   adapter *cannot* simply forward today: the underlying `ContinuityStore`
   contract has no incremental channel — see Phase M4.)
7. **The judgment plane is welded to concrete SQLite.** The Meerkat plan's
   claim is confirmed, with useful nuance:
   * **Steward**: `StewardEngine.store` is `Arc<SqliteAgentMemoryStore>`
     (`memory/steward.rs:934`), needed for five inherent methods with no
     trait counterpart (`pending_promotions`, `scope_overview`,
     `scope_floors`, `recent_records`, `discard_stage`).
   * **Taint firewall control surface**: `set_llm_write_gate`,
     `set_event_sink`, `set_evidence_resolver` are inherent methods on the
     concrete store (`memory_wiring.rs:114,118,177`); the tracker itself is
     abstract.
   * **Hygienist**: `StoreSpanReferenceSource.store` is concrete
     (`memory/hygienist.rs:552`).
   * **Selector**: the fetch field is a trait object, but gateway wiring
     hard-fails for anything but SQLite (`rpc_gateway.rs:5043`).
   * **Console memory panel**: typed `SqliteAgentMemoryStore` end to end
     (`unified_runtime/mod.rs:793`, `http_console.rs`), wired through an
     `as_sqlite_store()` downcast baked into the `AgentMemoryProvider`
     trait itself (`identity_first/agent_memory.rs:438`).
   * The entire judgment plane is instantiated through the SQLite provider.
     SQLite is the sole bundled live agent-memory backend; the former Markdown
     provider is retired, and Markdown is accepted only as one-shot migration
     input when a realm is first accessed and its SQLite connection opens.
   * **Already clean**: Distiller and Coordinator consume
     `Arc<dyn AgentMemoryProvider>` plus small source traits
     (`distiller.rs:1131-1134`, `coordinator.rs:329`) — they are the
     template for the rest.
8. **No legacy-evidence channel in the continuity contract.**
   `SessionSnapshot` is opaque bytes (`identity_first/types.rs:929-970`);
   checkpoint metadata lives on `ContinuityRecord`, and nothing in
   `identity_first/` knows about Meerkat's
   `SessionCheckpointState::LegacyUnverified`. On identity-first gateways
   the continuity store **is** the session authority (the adapter is
   installed as the Meerkat session store via
   `MobSessionBridge::with_continuity_session_store`, `bridge.rs:633`), so
   0.7.x-era session bytes stored inside continuity snapshots hit the same
   0.8.x resume wall the Meerkat hotfix exists for — and the hotfix
   machinery must run here too.

What is **not** broken and must be preserved: the identity substrate fails
loudly rather than degrading (`gateway_wiring.rs` returns `Err`); the
SQLite console/metadata opens fail loudly *when a path is set*; the resume
bridge refuses fresh-spawn fallback after a rejected resume
(`bridge.rs:1408-1424`) — the fail-closed postures already in the codebase
are the model, not the exception.

### Downstream constraints

The same two downstreams that shaped the Meerkat plan shape this one, and
their MobKit-specific asks are sharper:

**ob3 (ephemeral disk, BigQuery).** Implements `ContinuityStore` and
`EventLogStore` remotely and a BigQuery session store
(`runtime/session_store.rs` already carries a native
`BigQuerySessionStoreAdapter`). Has no seam for continuity/lease injection
through the builder (item 5), no schedule-store seam
(`schedule_wiring.rs` hardwires SQLite), and runs a shadow scheduler. The
zombie-sibling-row incident that drove Meerkat's append-only conformance
chapter happened in a store implementing *MobKit's* contracts. Wants: one
provider bundle, one conformance suite, fail-closed blob slots.

**HomeCore (state-generation deploys, local SQLite).** Byte-clones the
state directory per generation; reads MobKit's SQLite files directly in
sanctioned diagnostics. Wants: machine-readable durability classes so
clones can be Durable-only (the console/metadata/memory HNSW split
matters), schema-version refusal as health-visible certification failure
(not a crash-looping gateway), registered backup artifacts, and changelog
treatment for any file rename or table move. The file-name divergences in
item 1 are a live hazard for them: a tooling switch from `mobkit_gateway`
to embedded builder would silently orphan `continuity.db`.

## Design principles

MobKit inherits Meerkat's five principles (one path authority, explicit
fail-closed durability classes, bundle-level pluggability, schema evolution
as a framework, offline fail-closed migrations) and adds two of its own:

6. **One remote bundle, one seam.** A downstream backend implements one
   provider surface that covers Meerkat's stores *and* MobKit's
   (continuity, event log, console timeline, metadata, blobs, agent
   memory). Never again two integration seams and two heads of session
   authority.
7. **In-memory is a configuration, never a fallback.** Every durable slot
   resolves to exactly one of: a configured backend, an explicitly declared
   ephemeral choice, or a startup error. `Null`/in-memory implementations
   survive as legitimate declared choices (tests, demos) — they are removed
   as *defaults* and as *error-path fallbacks*.

## Immediate hotfixes (0.8.x line, independent of the arc)

**H1 — Fail-closed blob store.** Delete the silent in-memory fallback at
`mob_handle_runtime.rs:3732-3746` and `:3864-3871`. A failed open of the
local blob directory is a startup error; ephemeral blobs require an
explicit builder/gateway opt-in (`ephemeral_blobs(true)` or equivalent),
which is also surfaced in health output. This is the proven
month-of-silent-data-loss hazard and needs no framework to fix. The
`is_persistent()` flag already exists (`blob_store.rs:29-34`) — assert on
it at composition time.

**H2 — Loud whole-blob degradation.** `ContinuitySessionStoreAdapter`
cannot forward `as_incremental` today (its substrate has no incremental
channel), so the honest hotfix is visibility, not forwarding: when
`PersistentSessionService` composition resolves a session store whose
incremental capability is absent, MobKit logs at startup and exposes it in
the gateway health/capabilities surface (`mobkit/status` / console health).
The structural fix is Phase M4. Do not paper over this by pretending the
adapter can delegate.

**H3 — Continuity snapshot checkpoint adoption.** The Meerkat dependency
has landed: meerkat PR #909 exports
`meerkat_core::adopt_legacy_session(source_blob, generation, revision)`
(plus `legacy_session_transcript_relation`) and additionally ships
machine-owned **lazy auto-migration** of meerkat-side session documents at
the committed-authority resolver. That auto path heals the meerkat store
and runtime-snapshot shapes; H3 remains required for what meerkat never
sees — the session bytes inside **continuity snapshots**, the store that is
the session authority on identity-first gateways. MobKit ships an explicit
migration that walks those snapshots, applies the helper to the bytes
inside each, and rewrites the snapshot under the store's own CAS
(fencing-token) discipline. Observed generation/revision come from the
`ContinuityRecord`, matching the helper's contract. It is exposed as a
maintenance command (`mobkit_gateway` subcommand and a library entry
point), never as an eager per-row side effect of open.

**Ordering constraint (from #909's documented residual):** meerkat's lazy
auto path seeds `INITIAL` cursors, and a verified document never
re-migrates — a prematurely stamped lower generation is sticky. On any
fleet whose continuity rows record a **nonzero generation floor**, H3 (or
the bridge invoking the helper with the observed cursor) must run **before**
meerkat's lazy path first touches those sessions. Generation-0 fleets (both
known real datasets) are unaffected.

Two sanctioned invocation shapes, matching the two deployment worlds:

* **Batch, in a maintenance/candidate window** (HomeCore). Because the
  entry point is a library call, it composes directly with
  state-generation deploy machinery: clone the generation → run H3 against
  the clone as a deploy data transition → boot the candidate → the
  materialization gate demands every member resumes → flip only on proof,
  with rollback a pointer flip to the untouched previous generation. That
  composition turns the scariest migration class into a certified,
  reversible deploy step with zero new machinery.
* **Lazy, at restore** (ob3). Single-replica always-on deployments have no
  natural window — the pod restart *is* the window. Meerkat PR #909
  upstreamed exactly this shape for meerkat-side documents, so the
  meerkat-facing half of ob3's shipped lazy shim is superseded by the
  resolver's auto-migration; the continuity-side half maps onto H3's lazy
  mode, adopting each snapshot at first restore with the same helper and
  the same CAS discipline. Retirement chain: meerkat #909 released → H3 →
  ob3 shim removal.

**Joint acceptance case (with the meerkat hotfix): the byte-divergent
pair.** A canonical runtime snapshot and a continuity projection can
legitimately differ in bytes at the same generation/checkpoint (HomeCore's
real dump: 82,261,276 B vs 82,262,809 B at checkpoint 859). Where meerkat's
resolver sees both copies, PR #909's machine defines the rule: a projection
that provably extends the canonical is adopted (no trailing turn lost), a
stale prefix is rebuilt from canonical, and unrelated transcripts refuse
fail-closed — HomeCore's dump pair is expected to classify as an extension.
What remains joint is the **independently-adopted** variant: on
identity-first gateways the continuity copy lives where meerkat never sees
it, so H3's adoption and meerkat's adoption happen independently — two
valid stamps over different bytes for one session, which the existing
recoverable-divergence rule (byte-exact stamp match) does not accept. The
hotfix pair must make that pair converge afterwards rather than landing in
an ambiguous-checkpoint terminal state. Both shapes are acceptance gates,
validated against the real dump.

## Phase M0 — Conformance adoption

Runs against Meerkat Phase 0 (`meerkat-store-conformance`) and extends it
with MobKit-owned suites, published as `mobkit-store-conformance` so ob3
runs the identical suite against its BigQuery implementations.

* **Consume the Meerkat profiles** for the session-store surface MobKit
  composes: run the baseline profile plus the capability-discovery chapter
  against `ContinuitySessionStoreAdapter` and any store wrapped in
  `meerkat_store::StoreAdapter`. The `as_incremental` discovery test makes
  item 6 permanently loud.
* **MobKit trait profiles:**
  * `ContinuityStore`: fencing-token CAS semantics (stale token rejected,
    monotonic issuance), `CheckpointVersion` monotonicity per
    `(identity, generation)` across generation rebinds, snapshot
    save/load/delete-if-current round-trips,
    `rollback_continuity_record` semantics including the non-atomic
    default-impl path, `max_fencing_token` floor survival across reopen.
    Emulated-CAS backends (windowed reads) get the append-only chapter
    treatment: who deduplicates superseded sibling rows is a pinned
    contract, not an implementation accident.
  * `EventLogStore`: `append_batch` idempotency under redelivery (the
    documented requirement), flush-failure retry, and the
    `EVENT_LOG_RETRY_BUFFER_CAP` oldest-dropped behavior pinned as an
    explicit, observable contract rather than a surprise.
  * `ConsoleLogStore`: `append_if_absent` idempotency, watermark
    round-trip, windowed-query ordering under concurrent append.
  * `BinaryBlobStore`: content-address round-trip, `is_persistent()`
    honesty (a store claiming persistence must survive reopen), legacy
    FS-layout read fallback, dangling-reference behavior.
  * `AgentMemoryProvider` / `StagedMemoryStore`: every `supports_*()`
    capability flag backed by a behavioral test — a provider advertising a
    capability must pass its chapter, one refusing must return the typed
    `Unsupported` error.
* **Legacy-data axis**, seeded with real dumps: a HomeCore
  `continuity.db` + memory-realm corpus with genuine `ensure_column` scar
  tissue, and ob3-shaped fixtures for the emulated-CAS chapters. As with
  Meerkat: release-day incidents have been legacy-shape issues, not
  fresh-store bugs.

## Phase M1 — Doctor

MobKit implements the diagnosis half of Meerkat's `StorageMigrator` hook so
`rkat storage doctor` covers MobKit stores in mixed deployments — and adds
a gateway-native surface (`mobkit/storage/doctor` RPC + a console health
panel section) because many MobKit deployments have no `rkat` CLI on the
box. Read-only, safe against a live gateway, lease/fence-aware, JSON.

Reports:

* Per-state-directory inventory of all nine MobKit databases plus the blob
  root and peer-key file; which exist, which are empty shells.
* **File-name twins**: `sessions.db` *and* `sessions.sqlite` present;
  `continuity.db` *and* `identity_continuity.sqlite` present — the
  MobKit split-brain census (scoped strictly to the resolved state
  directory, per the HomeCore rule; byte-identical generation clones
  elsewhere are none of its business).
* Schema-ledger state per database (post-M3: version per domain;
  pre-M3: "no ledger" is itself a finding).
* **Durability-resolution census**: for each durable slot, what it
  resolved to at last boot — configured / declared-ephemeral /
  silent-fallback (the latter only exists until M4 removes it, but doctor
  must see it while it does). This check, existing earlier, would have
  found the GKE blob outage in one command.
* Continuity checkpoint-evidence census (legacy vs adopted snapshot rows)
  for H3.
* Dangling console-frame → blob references; orphaned blobs.
* Stale workgraph admission sidecar locks; schedule/workgraph
  "boot-without" state.

## Phase M2 — Path authority

One module, `mobkit::storage_layout`, producing an immutable
`MobKitStorageLayout` at bootstrap. When embedded in a Meerkat realm it is
*derived from* Meerkat's `StorageLayout` (Meerkat Phase 2) — MobKit never
re-resolves ambient roots; standalone gateways construct it from the
explicit state directory plus the XDG gateway-home rules that today live
inline in `mobkit_gateway.rs:104-116`.

* **Named accessors for roots and canonical top-level database locators**:
  sessions, runtime, schedule, workgraph, continuity, metadata, console,
  agent-memory realm directory, blob root, event-log locator, peer-key
  file, registry file. These canonical locators become constants in one
  place; `builder.rs`, `mobkit_gateway.rs`, and `rpc_gateway.rs` all
  consume the layout, and the three-way derivation duplication is deleted.
  The boundary (aligned with Meerkat Phase 5 and the M5 gate): the layout
  owns *roots and canonical top-level locators*; feature crates own
  *relative* filenames beneath them — the admission sidecar's lock name,
  the blob dir's internal sharding, per-realm memory files stay
  feature-owned.
* **Canonical-name-first probing** (the analog of Meerkat's
  realm-id-first rule): for each database, resolve the canonical name,
  then probe for the known legacy spellings in the same directory.
  Exactly one exists → use it where it lies (no rename at open). Both
  exist → typed split-brain error pointing at doctor. Neither → create
  under the canonical name. The invariant, exactly as in Meerkat Phase 2:
  the resolver must never *create* a twin. Physical renames to canonical
  names happen only in Phase M6 under the fence, with changelog entries
  (HomeCore reads these files by name).
* Canonical spellings are decided once, in this phase, and documented:
  stores shared with Meerkat (sessions, runtime, schedule, workgraph)
  adopt whatever Meerkat's layout names them; MobKit-owned files converge
  on one `*.sqlite3` convention (`continuity.sqlite3`,
  `mobkit_metadata.sqlite3`, `mobkit_console.sqlite3`, …).
* Delete ambient resolution outside the module: the manual
  `$HOME`/`XDG_STATE_HOME` reads, the `temp_dir()` continuity fallback in
  `rpc_gateway.rs:4417-4421` (that becomes a declared-ephemeral choice or
  an error), and `resolve_store_dir`'s extension-sniffing
  (`mobkit_gateway.rs:379-393`) folds into layout construction.
  Testability via an injected-roots constructor, mirroring Meerkat's.

## Phase M3 — Shared SQLite mechanics

Adopt Meerkat's `meerkat-sqlite` crate (Meerkat Phase 3) for all five
in-crate openers. MobKit adds no second mechanics layer.

* **Port the openers to named profiles**: memory, continuity writer →
  `Primary`; the continuity read pool's `query_only=ON` connections →
  `ReadOnly`; the admission sidecar's fail-fast exclusive lock →
  `Maintenance`-family, with its 30 s intent preserved as a named policy
  decision rather than an inline literal; the console store gets WAL and
  the shared busy timeout **for the first time** — an explicit changelog
  entry, and concurrency-heavy deployments should re-run console-load
  benchmarks, since a store that today blocks on nothing will start
  participating in WAL semantics.
* **Migration ledger everywhere**: per-file `meerkat_schema(domain,
  version)` with MobKit domain names (`mobkit-memory`,
  `mobkit-continuity`, `mobkit-metadata`, `mobkit-console`,
  `mobkit-workgraph-admission`), consuming Meerkat's pinned ledger
  transaction protocol unchanged (one row per domain, `BEGIN IMMEDIATE`,
  version re-read inside the transaction, migration + ledger update
  atomic, future versions rejected before any mutation). Existing DDL
  becomes migration 0001 per domain; the memory store's `ensure_column`
  probes (`ever_quarantined`, `proposals.taint`) become migrations 0002+
  and the probe code is deleted. `SchemaFromTheFuture` surfaces through
  gateway health as a typed refusal — a HomeCore rollback candidate fails
  certification cleanly instead of crash-looping under a process manager.
* **Store error taxonomy at MobKit boundaries**: classify
  `ContinuityStore` and `EventLogStore` errors transient / stale / corrupt
  so identity reconcile can retry transient failures instead of
  terminalizing an embodiment, and the event-log flusher can distinguish
  retry-worthy from poison batches before the retry-cap starts dropping
  the oldest events. Per the Meerkat rule, the class alone does not
  authorize retry: automatic retry only for idempotent or CAS-keyed
  operations (continuity writes are fencing-token CAS and qualify;
  `append_batch` qualifies via its documented idempotency-under-redelivery
  requirement); an indeterminate non-idempotent write requires outcome
  reconciliation before retry.
* **The maintenance fence rides the shared helper.** Meerkat's Phase 6
  fence is enforced per operation inside the `meerkat-sqlite` connection
  helper (MobKit's stores hold no long-lived connections either). Adopting
  the helper is what makes MobKit stores fence-aware; that is a second
  reason M3 forbids keeping any bespoke opener.

## Phase M4 — Provider seam adoption

The core of the arc: MobKit plugs into Meerkat's `RealmStorageProvider`
(Meerkat Phase 4) and applies fail-closed durability to every MobKit slot.

* **Two provider levels, dependency-correct.** Meerkat's
  `RealmStorageProvider` returns an upstream `RealmStoreSet` and cannot
  name MobKit-owned types without reversing the crate dependency. MobKit
  therefore defines its own composite seam, `MobKitStorageProvider`, which
  *wraps or references* a `RealmStorageProvider` (for the Meerkat-shared
  stores) and additionally opens a **realm-wide** `MobKitRealmStoreSet` —
  continuity, lease-fencing floor, event log, console timeline, metadata,
  blobs, agent-memory provider, schedule store — each paired with a
  durability declaration (`Durable` / `RebuildableCache` / `Scratch`).
  These are runtime/realm-wide stores opened once at bootstrap, **not**
  per-mob objects. The per-mob storage *factory* from the Meerkat plan is
  reserved for genuinely per-mob state (the per-mob databases meerkat-mob
  owns); it is not the vehicle for realm-wide MobKit stores. A downstream
  implements one `MobKitStorageProvider` and covers both levels — that is
  the "one remote bundle". The built-in disk implementation reproduces
  today's SQLite/JSON layout via the M2 layout and M3 openers.
* **Builder seams for everything that lacks one.**
  `UnifiedRuntimeBuilder` gains `continuity_store(...)`,
  `lease_provider(...)`, `schedule_store(...)`, `blob_store(...)` (the
  existing `custom_blob_store` parameter is promoted to a first-class
  builder method), alongside the existing `set_console_log_store`,
  `persistent_metadata`, and `start_event_log`. `gateway_wiring.rs` stops
  hardwiring `LocalContinuityStore` construction; it becomes the disk
  factory's implementation detail. The schedule path gets the public
  trait seam ob3 lacks — with the Meerkat plan's caveat repeated
  verbatim: injection is a *foundation*, and any shadow-scheduler
  deletion downstream requires a feature-parity audit first
  (multi-replica claims, timezone cron, jitter, delivery-policy
  handoffs).
* **Fail-closed composition.** At bootstrap, MobKit enumerates its
  durable slots — sessions, runtime, schedule, workgraph, continuity,
  event log, console timeline, metadata, blobs, agent memory — and each
  resolves to provider-supplied, built-in disk, or *explicitly declared*
  ephemeral; anything else is a startup error. Concretely: the four
  silent defaults from Context item 4 are deleted; `NullEventLogStore`
  and the in-memory stores remain constructible but only by declaration;
  the schedule/workgraph boot-without posture becomes a declared,
  health-visible degradation instead of a warn line. Gating audit,
  delivery history, and routing resolutions are explicitly classified
  (today: `Scratch`) so their non-durability is a documented decision;
  giving gating audit an optional durable slot (it is an *audit* surface)
  is flagged as a candidate follow-up, not smuggled into this arc.
* **Incremental continuity.** Extend the `ContinuityStore` contract with
  an optional incremental-persistence capability (a session-delta channel
  compatible with Meerkat's `IncrementalSessionStore` shape) so
  `ContinuitySessionStoreAdapter` can genuinely forward
  `as_incremental` instead of the H2 warning. Remote backends want this
  as badly as disk does — O(session) per turn over BigQuery is the reason
  ob3 invented transcript chunking. Conformance-gated via the M0
  continuity profile; the capability is typed and discoverable, never
  assumed.
* **De-weld the judgment plane.** Target state: the full judgment plane
  (taint firewall, Distiller, Steward, Hygienist, Selector, console
  panel) runs against any provider that advertises the required
  capabilities. Work, in dependency order:
  1. Promote the concrete-only surface onto traits: the five Steward
     methods (`pending_promotions`, `scope_overview`, `scope_floors`,
     `recent_records`, `discard_stage`) onto `StagedMemoryStore` or a new
     `StewardStore` capability trait; the firewall control surface
     (`set_llm_write_gate`, `set_event_sink`, `set_evidence_resolver`)
     onto a `TaintableStore` capability; `StoreSpanReferenceSource`'s
     concrete field replaced by the trait the engine already accepts.
  2. Give the console memory panel a trait-based read API and delete the
     `as_sqlite_store()` downcast from `AgentMemoryProvider` — the trait
     stops naming its own implementation.
  3. Generalize the assembly: `AgentMemoryStack` and
     `attach_memory_engines` accept the capability traits; the
     SQLite-only branches in `rpc_gateway.rs:5120` and `builder.rs:623`
     become capability checks. No bundled Markdown provider remains:
     Markdown is import-only, and selecting it as a live gateway store is
     refused. The Selector's "requires the sqlite store" hard-fail becomes
     "requires a `SelectedRecordFetch` provider".
     Distiller and Coordinator are the pattern to copy; they need no
     changes. The HNSW discard source stays classified `RebuildableCache`.
* **One remote bundle, demonstrated.** Acceptance for this phase includes
  a reference non-disk factory (in-memory-declared or a thin
  object-store-backed one) passing the full M0 suite through the single
  seam — proof that a downstream can implement one provider and get
  sessions, continuity, events, blobs, and memory without touching
  MobKit internals.

## Phase M5 — Surface cleanup and the anti-regression gate

* Delete the now-dead per-binary path derivation and the
  `resolve_store_dir` extension-sniffing; both gateways and the builder
  compose exclusively through `MobKitStorageLayout` + the provider/factory
  seam.
* **SDK surface**: the Python/TS `session_store` and `memory` config
  modules gain the durability vocabulary — explicit ephemeral declaration
  where the runtime now requires it, and (new) blob-store and event-log
  configuration blocks so `runtime_options` can express what today only
  Rust embedders can. No SDK config path may produce a silent in-memory
  slot; the gateway rejects undeclared gaps at startup, and the SDKs
  surface that as a typed error (`MobKitError` subclass), not a transport
  failure.
* **CI gate, scoped like Meerkat's**: ban ambient root resolution
  (`env::var("HOME")`, `XDG_*` reads) and *duplicate canonical-locator
  construction* outside `storage_layout` — not every filename literal.
  Feature-owned *relative* paths (the blob dir's internal sharding,
  per-realm memory files, the admission sidecar's name) stay with their
  owners, matching the M2 boundary — same rationale as Meerkat Phase 5: a
  storage god-module is its own fragmentation.
* Re-point docs: `concepts/sessions`, `concepts/memory`,
  `reference/configuration`, and the SDK pages describe the layout, the
  durability classes, and the declaration requirement. Changelog policy:
  any file rename or table move gets the binary-rename treatment, because
  HomeCore-class operators read these files directly.

## Phase M6 — Migration participation

MobKit implements the mutation half of `StorageMigrator`, running under
Meerkat's exclusive maintenance fence (Meerkat Phase 6) — MobKit builds no
second fence. `rkat storage migrate` (and the gateway-native maintenance
subcommand for CLI-less deployments) then covers MobKit stores.

Migration cases:

1. **Ledger baseline (auto-safe).** Each legacy MobKit SQLite file
   without `meerkat_schema` is structurally verified and baseline-stamped.
2. **File-name unification (auto-safe, rename-only).** Legacy spellings
   (`sessions.db`, `continuity.db`, `identity_continuity.sqlite`) are
   renamed to the M2 canonical names under the fence, with registered
   backups. No content changes.
3. **File-name twin reconciliation (manual, fail-closed).** Where doctor
   found both spellings populated: exact-equality dedup only; otherwise
   adopt one file as authority and archive the other read-only with a
   per-domain divergence report. No synthesis — continuity fencing tokens
   and console autoincrement cursors are per-database sequences, and
   merging them corrupts CAS and cursor replay exactly the way merging
   workgraph event streams would in Meerkat.
4. **Continuity checkpoint-evidence adoption.** The H3 machinery, invoked
   under the fence for remaining legacy snapshot rows, via the exported
   Meerkat stamping helper — including for remote backends through their
   own `StorageMigrator`.
5. **Deprecated leftovers (report-only).** Orphaned blob files under the
   legacy FS layout, stale admission-sidecar locks, dead
   `tux-runtimes.json` registry entries.

Backup and retention follow the Meerkat discipline verbatim:
`*.pre-<version>-<timestamp>` renames, never deletes, registered so doctor
lists them and prune owns their lifecycle — HomeCore's generation-cloning
must be able to recognize them.

## Sequencing, risk, and gates

Dependencies on the Meerkat arc, by phase:

| MobKit phase | Needs from Meerkat                                 | Can start before it lands?       |
| ------------ | -------------------------------------------------- | -------------------------------- |
| H1, H2       | nothing                                            | yes — ship now on 0.8.x          |
| H3           | `adopt_legacy_session` (landed in meerkat PR #909) | ship once a release carries #909 |
| M0           | Phase 0 conformance crate + profiles               | MobKit-trait suites: yes         |
| M1           | Phase 1 doctor + diagnose hook shape               | gateway-native surface: yes      |
| M2           | Phase 2 `StorageLayout` (embedded path)            | standalone layout: yes           |
| M3           | Phase 3 `meerkat-sqlite`                           | no (do not fork the mechanics)   |
| M4           | Phase 4 provider trait + durability vocabulary     | seam design + de-welding: yes    |
| M6           | Phase 6 fence + `StorageMigrator`                  | no                               |

* Hotfixes H1/H2 ship first on the 0.8.x line; H3 ships as soon as MobKit
  consumes a Meerkat release carrying PR #909's exported helper — on
  nonzero-generation fleets it must be sequenced *before* first resume
  under that release (the INITIAL-cursor ordering constraint above).
* Phase order M0 → M1 → M2 → M3 → M4 → M5 → M6, with the same parallelism
  as upstream: M2 and M3 are independent; the judgment-plane de-welding
  inside M4 is independent of the provider seam and can start immediately
  (it is pure trait refactoring with the Distiller as template).
* The arc rides the Meerkat 0.9 dependency upgrade (MobKit is on `=0.8.2`
  today); deprecated spellings and the removed silent fallbacks follow
  MobKit's normal deprecation cadence, aligned with Meerkat's
  0.9-arc / 0.10-removal targets.
* **Riskiest items**, each with explicit changelog entries and doctor
  checks:
  1. The M2 canonical-name probe — like Meerkat's resolver, its invariant
     is that it never creates a twin; it gates on a dual-name fixture
     matrix (canonical only / legacy only / both / neither, per binary ×
     builder).
  2. The console store gaining WAL in M3 — a behavior shift on the
     highest-write-rate database; benchmark before/after.
  3. Judgment-plane trait promotion in M4 — a wide surface; land it as
     mechanical per-engine PRs (Steward, firewall controls, Hygienist,
     Selector, panel) each gated on the M0 memory profile, never as one
     big-bang rewrite.
  4. H1 will *surface* latent misconfigurations (deployments silently on
     in-memory blobs today will now fail to boot) — that is the point,
     but the release notes must say so loudly and doctor must name the
     remediation.
* Every phase gates on the M0 suites plus the existing integration
  batteries (`identity_first_*.rs`, `gateway_memory_config.rs`,
  `e2e_target_contracts.rs`, `e2e_sdk_wire.rs`, SDK test suites).
  Contract-visible changes (SDK config blocks, RPC additions like
  `mobkit/storage/doctor`) follow the standard schema-regen and SDK
  codegen gates.

## Review disposition ledger (v1 → v2)

Accepted from the independent static review: provider layering split — a
MobKit-owned `MobKitStorageProvider` composite wraps `RealmStorageProvider`
(upstream cannot name MobKit types without reversing the crate dependency),
and the per-mob factory is reserved for genuinely per-mob state while
continuity/console/metadata/blobs/memory/schedule are realm-wide bundle
slots (P1); filename-ownership boundary resolved — the layout owns roots and
canonical top-level locators, feature crates own relative filenames, and the
M5 gate bans ambient root resolution and duplicate locator construction
rather than filename literals (P2, applied consistently in M2 and M5);
retryability as a method-level contract layered onto the error taxonomy
(P2, inherited from the Meerkat rule).

Accepted from HomeCore: the version-line correction (this repo is 0.8.1,
tags `v0.8.x`; the draft's "0.6.55 / 0.6.x line" was wrong — hotfixes ship
on 0.8.x); H3 as a deploy data transition composed with state-generation
cloning and the materialization gate; the joint divergent-bytes acceptance
case for the H3 + meerkat-hotfix pair, validated against their real dump;
their `continuity.db` + memory-realm fixture corpus contribution to M0.

Accepted from ob3: the lazy-at-restore adoption variant as a sanctioned H3
mechanism for always-on single-replica deployments, with the shipped shim
as prototype and the retirement chain meerkat hotfix → H3 → shim removal;
this disposition-ledger section itself. Noted, out of scope: "explosion #3"
(run\_flow → identity-first dispatch producing zero turns) is a 0.8
runtime/flow defect tracked separately, not part of this arc.

**Round 3 (rebase on meerkat PR #909).** The upstream dependency landed as
machine-owned lazy auto-migration plus the exported
`meerkat_core::adopt_legacy_session` / `legacy_session_transcript_relation`.
Consequences applied here: H3 narrows to continuity snapshots (meerkat's
resolver heals its own store and runtime-snapshot shapes); the
meerkat-facing half of ob3's lazy shim is superseded upstream while the
continuity half maps onto H3's lazy mode; the joint acceptance case narrows
to the independently-adopted variant (where meerkat sees both copies, the
machine's extension/rebuild/refuse dispositions own the rule); and the
INITIAL-cursor stickiness residual becomes an explicit ordering
constraint — on nonzero-generation fleets, H3 runs before first resume
under a #909-carrying release.
