Skip to main content
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:
  1. 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.
  2. 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:
  • 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.