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:- Three copies of path derivation, with divergent file names. The
library builder (
unified_runtime/builder.rs),mobkit_gateway(src/bin/mobkit_gateway.rs), andrpc_gateway(src/bin/rpc_gateway.rs) each derive the per-database paths independently. They disagree: sessions aresessions.dbin rpc_gateway (rpc_gateway.rs:4471) butsessions.sqlitein mobkit_gateway (mobkit_gateway.rs:390); continuity iscontinuity.dbin both gateways (mobkit_gateway.rs:945,rpc_gateway.rs:4418) butidentity_continuity.sqlitein 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. - 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 usesmeerkat_store::sqlite_store::open_connection. - No schema versioning anywhere. Every store is
CREATE TABLE IF NOT EXISTS; the only evolution mechanism is the memory store’s ad-hocensure_columnprobes with backfill (sqlite_store.rs:357,381). No ledger, nouser_version, no typed refusal when a newer binary’s schema is opened by an older binary. - 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_innerlogs a warning and constructsObjectStoreBlobStore::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:
InMemoryConsoleLogStorewheneverpersistent_state_pathis unset (unified_runtime/builder.rs:701-714, default atunified_runtime/mod.rs:283). - Event log:
EventLogConfig::defaultinstallsNullEventLogStore(unified_runtime/event_log.rs:131-140) — worse than in-memory, it silently drops every event. - Runtime metadata:
InMemoryMetadataStorewheneverpersistent_state_pathis 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.
- Blobs: when no custom blob store is supplied and the local blob
directory fails to open,
ContinuityStoreandLeaseProviderhave no injection seam. They are raw struct fields onIdentityRuntimeConfig(identity_first/runtime.rs:326-345), hardwired toLocalContinuityStore/LocalLeaseProvideringateway_wiring.rs.UnifiedRuntimeBuilderhas 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.- A confirmed capability swallow in the identity-first session path.
ContinuitySessionStoreAdapter(identity_first/adapters.rs:677) implements Meerkat’sSessionStoreover theContinuityStorebut does not overrideas_incremental, so it inherits theNonedefault — despite Meerkat’s trait doc saying delegating wrappers MUST forward it.PersistentSessionServiceprobes 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 underlyingContinuityStorecontract has no incremental channel — see Phase M4.) - The judgment plane is welded to concrete SQLite. The Meerkat plan’s
claim is confirmed, with useful nuance:
- Steward:
StewardEngine.storeisArc<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_resolverare inherent methods on the concrete store (memory_wiring.rs:114,118,177); the tracker itself is abstract. - Hygienist:
StoreSpanReferenceSource.storeis 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
SqliteAgentMemoryStoreend to end (unified_runtime/mod.rs:793,http_console.rs), wired through anas_sqlite_store()downcast baked into theAgentMemoryProvidertrait 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.
- Steward:
- No legacy-evidence channel in the continuity contract.
SessionSnapshotis opaque bytes (identity_first/types.rs:929-970); checkpoint metadata lives onContinuityRecord, and nothing inidentity_first/knows about Meerkat’sSessionCheckpointState::LegacyUnverified. On identity-first gateways the continuity store is the session authority (the adapter is installed as the Meerkat session store viaMobSessionBridge::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.
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). ImplementsContinuityStore 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:- 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.
- 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 atmob_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.
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
ContinuitySessionStoreAdapterand any store wrapped inmeerkat_store::StoreAdapter. Theas_incrementaldiscovery test makes item 6 permanently loud. - MobKit trait profiles:
ContinuityStore: fencing-token CAS semantics (stale token rejected, monotonic issuance),CheckpointVersionmonotonicity per(identity, generation)across generation rebinds, snapshot save/load/delete-if-current round-trips,rollback_continuity_recordsemantics including the non-atomic default-impl path,max_fencing_tokenfloor 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_batchidempotency under redelivery (the documented requirement), flush-failure retry, and theEVENT_LOG_RETRY_BUFFER_CAPoldest-dropped behavior pinned as an explicit, observable contract rather than a surprise.ConsoleLogStore:append_if_absentidempotency, 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: everysupports_*()capability flag backed by a behavioral test — a provider advertising a capability must pass its chapter, one refusing must return the typedUnsupportederror.
- Legacy-data axis, seeded with real dumps: a HomeCore
continuity.db+ memory-realm corpus with genuineensure_columnscar 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’sStorageMigrator 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.dbandsessions.sqlitepresent;continuity.dbandidentity_continuity.sqlitepresent — 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, andrpc_gateway.rsall 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
*.sqlite3convention (continuity.sqlite3,mobkit_metadata.sqlite3,mobkit_console.sqlite3, …). - Delete ambient resolution outside the module: the manual
$HOME/XDG_STATE_HOMEreads, thetemp_dir()continuity fallback inrpc_gateway.rs:4417-4421(that becomes a declared-ephemeral choice or an error), andresolve_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’smeerkat-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’squery_only=ONconnections →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’sensure_columnprobes (ever_quarantined,proposals.taint) become migrations 0002+ and the probe code is deleted.SchemaFromTheFuturesurfaces 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
ContinuityStoreandEventLogStoreerrors 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_batchqualifies 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-sqliteconnection 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’sRealmStorageProvider
(Meerkat Phase 4) and applies fail-closed durability to every MobKit slot.
- Two provider levels, dependency-correct. Meerkat’s
RealmStorageProviderreturns an upstreamRealmStoreSetand cannot name MobKit-owned types without reversing the crate dependency. MobKit therefore defines its own composite seam,MobKitStorageProvider, which wraps or references aRealmStorageProvider(for the Meerkat-shared stores) and additionally opens a realm-wideMobKitRealmStoreSet— 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 oneMobKitStorageProviderand 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.
UnifiedRuntimeBuildergainscontinuity_store(...),lease_provider(...),schedule_store(...),blob_store(...)(the existingcustom_blob_storeparameter is promoted to a first-class builder method), alongside the existingset_console_log_store,persistent_metadata, andstart_event_log.gateway_wiring.rsstops hardwiringLocalContinuityStoreconstruction; 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;
NullEventLogStoreand 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
ContinuityStorecontract with an optional incremental-persistence capability (a session-delta channel compatible with Meerkat’sIncrementalSessionStoreshape) soContinuitySessionStoreAdaptercan genuinely forwardas_incrementalinstead 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:
- Promote the concrete-only surface onto traits: the five Steward
methods (
pending_promotions,scope_overview,scope_floors,recent_records,discard_stage) ontoStagedMemoryStoreor a newStewardStorecapability trait; the firewall control surface (set_llm_write_gate,set_event_sink,set_evidence_resolver) onto aTaintableStorecapability;StoreSpanReferenceSource’s concrete field replaced by the trait the engine already accepts. - Give the console memory panel a trait-based read API and delete the
as_sqlite_store()downcast fromAgentMemoryProvider— the trait stops naming its own implementation. - Generalize the assembly:
AgentMemoryStackandattach_memory_enginesaccept the capability traits; the SQLite-only branches inrpc_gateway.rs:5120andbuilder.rs:623become 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 aSelectedRecordFetchprovider”. Distiller and Coordinator are the pattern to copy; they need no changes. The HNSW discard source stays classifiedRebuildableCache.
- Promote the concrete-only surface onto traits: the five Steward
methods (
- 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_dirextension-sniffing; both gateways and the builder compose exclusively throughMobKitStorageLayout+ the provider/factory seam. - SDK surface: the Python/TS
session_storeandmemoryconfig modules gain the durability vocabulary — explicit ephemeral declaration where the runtime now requires it, and (new) blob-store and event-log configuration blocks soruntime_optionscan 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 (MobKitErrorsubclass), not a transport failure. - CI gate, scoped like Meerkat’s: ban ambient root resolution
(
env::var("HOME"),XDG_*reads) and duplicate canonical-locator construction outsidestorage_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 ofStorageMigrator, 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:
- Ledger baseline (auto-safe). Each legacy MobKit SQLite file
without
meerkat_schemais structurally verified and baseline-stamped. - 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. - 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.
- 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. - Deprecated leftovers (report-only). Orphaned blob files under the
legacy FS layout, stale admission-sidecar locks, dead
tux-runtimes.jsonregistry entries.
*.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.2today); 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:
- 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).
- The console store gaining WAL in M3 — a behavior shift on the highest-write-rate database; benchmark before/after.
- 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.
- 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 likemobkit/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-ownedMobKitStorageProvider 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.