storage-unification branch (phases 0-6). Deltas discovered and decided
during implementation, recorded here so the plan stays honest against the
code: the dead config field is StorageConfig.directory (no
SessionStoreConfig exists); the two find_project_root copies were NOT
byte-identical — the canonical copy adopts the live exists() semantic;
the maintenance fence is per-database-file <file>.mfence OS locks with
per-operation shared guards and holder self-admission (the migrate
process’s own production store paths pass their guards; bulk checkpoint
adoption runs through the CAS-hardened #909/#910 machinery while the fence
is held); mob’s event-route backfill and memory’s NULL-session_id heal
remain open-time heals (old binaries keep writing pre-migration shapes) —
only their schema halves became ledger migrations; the per-mob storage
factory hook is deferred to the mobkit composite provider where it has a
real consumer; external-provider manifests are read-refused now but the
write path ships with the first real external provider; wire ErrorCode
additions for storage refusals are deferred (startup refusals are typed at
the process boundary; no contracts change was needed in this arc);
RealmSelection::Isolated minting was fixed to resolve identity exactly
once; the ambient no-_in realm helpers are deprecated rather than
deleted (published API); doctor/migrate/prune dispatch before realm
resolution so they can run against the split-brain states the resolver
refuses; per-mob mobs/*.db files are report-only in migrate v1. A sixth
busy-timeout definition beyond the plan’s count (mob’s realm-profile
store) was found and folded in.
Revision 4. Revision 2 incorporated three reviews of the original draft:
the ob3 validator team (remote/BigQuery downstream on ephemeral disk), the
HomeCore operators (tuple-world state-generation deployment on local
SQLite), and an independent static review that corrected two stale inventory
claims and identified safety defects in the original migration design.
Revision 3 incorporates the second review round on both this document and
the mobkit companion: per-operation fence enforcement, the
user_home_root/comms-identity layout corrections, the pinned migration
transaction protocol, method-level retryability, and the joint
divergent-bytes acceptance case for the hotfix pair. Revision 4 rebases
the hotfix section on PR #909, which implements it as machine-owned lazy
auto-migration (a conscious, recorded override of the quiescent-step
requirement) and exports the stamping helper. A disposition ledger is at
the end.
Context
A survey of the workspace (2026-07) found the storage layer fragmented along these axes:- Surfaces disagree on storage defaults — root and identity. The CLI
defaults to workspace-derived realm ids under a project-local
<context-root>/.rkat/realms(meerkat-cli/src/main.rs,default_cli_state_root). The RPC, REST, and MCP servers default to isolated (freshly generated) realm ids under the user-globaldirs::data_dir()/meerkat/realms(RealmSelection::Isolatedin each servermain.rs;default_state_root()inmeerkat-core/src/runtime_bootstrap.rs). By default the surfaces therefore do not collide — they quietly do completely unrelated things. The split-brain hazard (the same realm id materialized under two roots) arises as soon as a realm id is shared deliberately: an explicit--realmgiven to surfaces with different root defaults, or workspace-derived selection configured on a server. Fleets that have done so may already hold divergent twins. - Six independently authored SQLite connection setups. The PRAGMA policy
(WAL +
synchronous=FULL+ busy_timeout) is copy-pasted across meerkat-store, its JSONL index, meerkat-mob, meerkat-workgraph, meerkat-memory, and meerkat-tools, withSQLITE_BUSY_TIMEOUT_MSredefined six times (5s vs 60s). No migration framework exists anywhere — every store relies onCREATE TABLE IF NOT EXISTSplus bespoke legacy-upgrade functions. Correction from review: the runtime store is not one of the offenders on main —meerkat-runtime/src/store/sqlite.rsalready usesmeerkat_store::sqlite_store::{open_connection, begin_immediate_transaction}for all production paths; itsbusy_timeout(Duration::ZERO)survives only in the deliberate legacy-migration maintenance opener. The consolidation work is real, but it is harmonization of defaults, not a contention-bug fix. Deployments on older release lines should verify which release picked up the runtime-store opener unification. - A shadowed
dirsmodule in core.meerkat-core/src/config.rsdefines a localpub mod dirswhosehome_dir()reads only$HOME, shadowing the platform-awaredirscrate within that file.Config::global_config_path()anddata_dir()are HOME-only while realm, auth, and REST paths use the real crate — two notions of “home” inside one crate. - Duplicated resolution logic.
find_project_rootexists twice byte-for-byte (meerkat-core/src/config.rs,meerkat-tools/src/builtin/project.rs);default_state_root()/default_realms_root()are identical duplicates in core and store;home_dirstubs exist in bothconfig.rsandmcp_config.rs. - Dead config trap.
SessionStoreConfig.directorydefaults to asessions/directory that no live surface code consumes. - Three unrelated path regimes for user-scoped state: realm data under
XDG
data_dir/meerkat/realms, config/project state under.rkat/and~/.rkat/, credentials under XDGconfig_dir/meerkat/credentials, plus a fourth platform-state-dir location for comms identity keys (meerkat/src/sdk.rs).
sessions.sqlite3 busy-timeout mismatch is already fixed on main, and the
surfaces’ default divergence is root and identity, which changes both the
severity story and the remedy.
Downstream constraints
Ephemeral disk / remote stores (ob3 validator). Cloud Run / GKE deployments run with no durable disk. ob3 replaces meerkat’s stores with hand-rolled BigQuery implementations ofSessionStore, mobkit’s
ContinuityStore, and EventLogStore — but has no public seam for a
RuntimeStore or ScheduleStore, so it runs a shadow scheduler. Its session
store invented transcript chunking to survive whole-blob saves and drove the
IncrementalSessionStore contract now in
meerkat-core/src/session_store.rs. BigQuery cannot do cheap in-place CAS;
revision guards are emulated with windowed reads, and a store-contract
ambiguity for append-only media has already produced a production incident
(orphan high-version sibling rows making every subsequent save permanently
stale). A mobkit blob store silently defaulting to an in-memory implementation
on GKE cost a user a month-broken agent — silent fallback for durable slots is
a proven production hazard, not a hypothetical.
Immutable state generations (HomeCore). Full deploys byte-clone the state
directory and boot a candidate against the clone; rollback keeps the previous
generation untouched. Migration-at-open inside the candidate boot is
compatible with this model — but backup files written by migration land
inside a generation that is re-cloned repeatedly, so backup artifacts must be
registered with retention, and schema-version refusal must surface as typed,
health-visible certification failure rather than a boot crash-loop. Multiple
byte-identical copies of the same realm id on one disk are by design in
this world; split-brain detection must scope strictly to the resolved roots.
Operators also read the platform SQLite directly in sanctioned diagnostic
paths — table renames/moves must be changelog-flagged the way binary renames
are.
Design principles
- One path authority, one composition seam. All durable storage flows
through a single
StorageLayout(paths) and a single provider seam (store construction). No crate resolves ambient roots on its own. - Durability classes are explicit and fail-closed. Every piece of state
is declared
Durable(sessions, runtime checkpoints, schedules, workgraph, mob events, continuity, memory text),RebuildableCache(HNSW index, skill git cache, session projections), orScratch. A missing durable slot is a startup error unless the realm explicitly selects ephemeral semantics — never a silent in-memory fallback. The classes are machine-readable so deployment tooling can, e.g., cloneDurableonly. - Backends are pluggable at the bundle level. One provider supplies all durable stores; capability differences (incremental persistence, guarded projections, append-only media) are typed capabilities with conformance profiles, not silent degradations.
- Schema evolution is a framework. Every SQLite file carries a migration ledger; remote backends get the same hook in their own medium.
- Migrations are offline, resumable, and fail-closed. No semantic merge is ever synthesized automatically; conflicts are preserved and reported.
Immediate hotfix (0.8.x, independent of everything below)
Status: landing as PR #909 (“machine-owned auto-migration of pre-typed session checkpoints”), with one conscious design override of this section recorded below. External acceptance pending: the HomeCore dump-driven restore harness against the branch.Legacy checkpoint-evidence adoption. Sessions written by 0.7.x carry
SessionCheckpointState::LegacyUnverified metadata, and resume on 0.8.x hits
the hard error in load_committed_checkpoint_authority
(meerkat-session/src/persistent.rs). This is a live fleet blocker for at
least two downstreams today (ob3: 32/32 identities broken on a real 0.7.9
dataset; HomeCore: roster bricked on upgrade). It must not wait for the plan’s
arc:
- Ship an explicit, quiescent adoption migration on the 0.8.x line, built on
the runtime store’s existing legacy-evidence machinery (the path that
verifies the session-store row is byte-identical to its runtime checkpoint
before upgrading,
meerkat-runtime/src/store/sqlite.rs). The original draft citedmigrate_legacy_blob_in_txnhere; that is the wrong primitive — it lays out blob sessions as strand/head rows and neither verifies nor mints checkpoint authority. - Export the stamping helper — landed as
meerkat_core::adopt_legacy_session(source_blob, generation, revision)plusmeerkat_core::legacy_session_transcript_relation(the per-message canonical-JSON prefix comparison between unstamped copies). The ordering subtleties every remote backend used to re-derive are now owned in one place: the stamp digest binds to the final bytes after any metadata repair (e.g. comms_name rewrites at restore), observed generation/revision come from the continuity row, and typed documents are refused. The resolver’s auto-migration is one caller; ob3’s shim and mobkit’s continuity adoption (companion H3) are the others. - Adoption mechanism — conscious override, decided in PR #909. This plan
originally required adoption as an explicit migration step, never an eager
side effect of ordinary open. The shipped design is instead lazy,
machine-owned, per-session adoption at first committed-authority touch
(a
SessionDocumentMachineinput with typed dispositions:MigrateCanonicalSnapshot,AdoptProjectionExtension,MigrateStoreProjection, and fail-closedRefuseDivergent), an operator-confirmed trade: zero-ceremony fleet upgrades outweigh the maintenance-window framing. What survives of the original requirement: the cost is observable rather than inferred (per-migrationtracingwith session, disposition, and source bytes — it lands inside resume/certification windows by design), adoption is idempotent and exactly-once per document, and the Phase 6rkat storage migrateverb remains as the bulk, fenced form of the same machinery for operators who want the work out of their resume paths. - Documented residual (INITIAL-cursor stickiness). The lazy auto path
seeds
INITIALgeneration/revision cursors — correct for lineages that never minted authority (both known real datasets are generation 0). A fleet whose continuity rows record a nonzero generation floor must adopt through the exported helper with the observed cursor (the mobkit companion’s H3) before the lazy path touches those sessions: a verified document never re-migrates, so a prematurely stamped lower generation is sticky. - Joint acceptance case with the mobkit companion’s H3 (from HomeCore’s
real dump): the byte-divergent canonical/projection pair. A canonical
runtime snapshot and a continuity projection can legitimately differ in
bytes at the same generation/checkpoint (observed: 82,261,276 B vs
82,262,809 B at checkpoint 859). PR #909’s machine now defines the
authority rule when both copies are visible to the resolver, superseding
this plan’s earlier “canonical wins; projection re-anchors” sketch: a
projection that provably extends the canonical (per-message
canonical-JSON prefix relation) is adopted so no trailing turn is lost;
a stale-prefix projection is rebuilt from canonical; genuinely unrelated
transcripts refuse fail-closed (
RefuseDivergent) with the divergence named for the operator tool — no synthesis, matching principle 5. HomeCore’s dump pair is expected to classify as an extension. What remains a joint acceptance case with mobkit H3 is the independently-adopted variant: when the continuity copy lives in a store the meerkat resolver never sees (identity-first gateways), H3’s adoption and meerkat’s adoption happen independently and the pair must still converge afterwards rather than landing in an ambiguous-checkpoint terminal state. HomeCore’s harness validates both shapes against the real dump; this is an acceptance gate for the hotfix pair.
Phase 0 — Storage conformance harness
Prerequisite that protects every later phase. A published crate (meerkat-store-conformance) so downstream backends run the identical suite.
Structure: per-trait capability profiles, not one universal matrix. The
JSONL realm composes a JSONL session store with SQLite runtime/workgraph,
disabled scheduling, and filesystem blobs — a single backend matrix misfits
reality. Profiles per trait (baseline / incremental / guarded-projection),
plus a whole-bundle PersistentSessionService integration suite.
Coverage, by chapter:
- Core contracts: save/load round-trips, CAS/revision-guard semantics, checkpoint-stamp preservation across save/load, concurrent-writer contention, large-payload behavior.
- Capability discovery:
as_incrementalforwarding through delegating wrappers and trait erasure — its default returnsNoneand the runtime silently degrades to whole-blob persistence when a wrapper swallows the capability. A conformance test makes that silent downgrade loud. - Append-only media (new, from ob3’s zombie incident): pin what a revision guard means for backends that emulate CAS with windowed reads; who owns deduplication of superseded sibling rows; that checkpoint monotonicity survives generation rebinds. Also pin the non-atomic projection-vs-authority recovery protocol (quarantine → Broken → repair) as a tested contract — disk backends with transactional co-commit must not let everyone forget that remote backends never have it.
- Legacy data (new, from HomeCore): “open a store written by version N−1” is a first-class axis, seeded with real dumps (HomeCore has offered a 371-message / 82 MB session corpus with genuine version scar tissue) — every release-day incident so far has been a legacy-data-shape issue, none were fresh-store bugs.
- Blobs (new): a session-referenced blob survives provider round-trip/restart; references never dangle silently.
Phase 1 — Read-only rkat storage doctor
Land the diagnostic surface before anything mutates:
- Read-only diagnosis path that is safe against a live realm (the first thing an operator does at 2 AM is run doctor against the wedged production store); lease-aware; JSON output.
- Reports: per-root realm inventory (both candidate roots), manifest state, schema-ledger state per database, dual-root twins for the realm being resolved, checkpoint-evidence census (verified vs legacy rows), dangling session→blob references, orphaned index/lease files.
- Calls the provider’s
StorageMigrator::diagnosehook so remote deployments get doctor output too, not just disk realms. - Repair verbs stay in Phase 6; doctor gains a sanctioned strip-to-placeholder repair for dangling blob refs there (today the only remedy is hand-editing production session JSON).
Phase 2 — Path authority
One module,meerkat_core::storage_layout, producing an immutable
StorageLayout at bootstrap:
-
Naming preserves existing parameter semantics. Today’s
user_config_rootparameters are home-like: the CLI resolvescli.user_config_root.or_else(dirs::home_dir)and helpers append.rkatthemselves (meerkat-cli/src/main.rs,meerkat/src/sdk.rs,mcp_config.rs). Substituting a “means~/.rkat” field for those parameters would produce~/.rkat/.rkat. The layout therefore carries bothuser_home_root(the compatibility boundary existing parameters map onto) and the deriveduser_rkat_root; existinguser_config_rootparameters are deprecated ontouser_home_rootwith unchanged meaning. -
Session-comms identity is an explicit slot. Its current
platform-specific default (
canonical_session_comms_identity_rootinmeerkat/src/sdk.rs) is durable key material; if the layout omitted it, the Phase 5 anti-ambient-resolution gate would either exempt it forever or a naive port would silently relocate — and thereby rotate — identity keys. The slot preserves the existing resolution exactly. -
Invocation context and project root are distinct fields. MCP config
discovery deliberately checks only the exact directory and forbids walk-up
for security (
meerkat-core/src/mcp_config.rs,find_project_mcp). That trust boundary is preserved: MCP keys offinvocation_context; storage and project config may key off the walked-upproject_root. -
Realm-id-first root resolution. Resolve the
RealmIdbefore choosing a root, then probe that specific realm under both candidate roots (project-local.rkat/realms, user-global data dir): explicit--state-root/config wins → else the single root where the realm already exists → both = typed split-brain error pointing at doctor → neither = the surface’s documented default root. Probing the parent directory’s mere existence (the original draft’s rule) could route a realm that exists only globally into a local root and create an empty twin — the exact disease being cured. - Split-brain detection scopes strictly to the two resolved roots. Byte-identical copies of a realm elsewhere on disk (tuple-world state generations) are none of its business.
- Identity defaults are not silently changed. Servers defaulting to isolated realms vs the CLI’s workspace-derived realms is an isolation decision, not a path bug; unifying it would change server multi-tenancy semantics. This plan documents the divergence and adds doctor visibility; any default change is a separately reviewed decision.
-
Delete the shadowed
pub mod dirsinconfig.rsand thehome_dirstub inmcp_config.rs; use the realdirscrate, with testability via an injected-roots constructor onStorageLayout. -
Delete the duplicates:
default_realms_root()delegates to the one function in core; the secondfind_project_rootis replaced by a passed-inStorageLayout. -
Retire
SessionStoreConfig.directory: deprecate with a load-time warning in 0.9, remove in 0.10.
Phase 3 — Shared SQLite mechanics
A new leaf cratemeerkat-sqlite (rusqlite + serde only — meerkat-schedule
and meerkat-workgraph sit below meerkat-store in the dependency order, so
the shared machinery cannot live there):
- DDL-free connection helper with named policy profiles:
Primary(WAL,synchronous=FULL, the one shared busy timeout),ReadOnly, andMaintenance(fail-fast zero timeout, exclusive). The runtime store’s existing deliberate policies (read-only observation, zero-timeout maintenance opens, nonblocking fence probes) are preserved as named profiles, not erased. Port the five remaining bespoke openers (JSONL index, mob, workgraph, memory, tools). - The
json_columncodec moves here so mob (and any future store) stops re-solving TEXT-vs-BLOB reads. - Migration ledger: per-file
meerkat_schema(domain TEXT, version INTEGER); each store registers ordered idempotent migrations; existing DDL becomes migration 0001 per domain. Opening a database whose ledger is ahead of the binary fails with a typedSchemaFromTheFutureerror that is health-visible — surfaces report it as a refusal (a rollback candidate fails certification cleanly) rather than crash-looping the process manager. Idempotent migration functions alone do not make concurrent opens safe, so the runner pins a minimal transaction protocol: exactly one ledger row per domain;BEGIN IMMEDIATE; re-read the version inside that transaction; execute the pending migrations and the ledger update atomically in it; and reject a future version before any mutation. MobKit consumes this protocol unchanged (its M3). - Store error taxonomy (from ob3’s incident review): classify store errors at the boundary as transient / stale / corrupt, so callers can retry transient failures instead of terminalizing every store error into executor-stop + quarantine. The error class alone does not authorize a retry, though: a transient failure can land after a write committed but before success became observable, and blind retry duplicates non-idempotent effects. Retryability is therefore a method-level contract — automatic retry only for idempotent or CAS-keyed operations; an indeterminate non-idempotent write requires outcome reconciliation (read back, then decide) before any retry. The same rule applies to mobkit’s continuity and event-log loops.
realm.sqlite3 for cross-domain atomicity.
Review showed (a) session, schedule, and runtime tables already co-tenant
sessions.sqlite3 in the sqlite realm backend, and (b) runtime authority and
session projection are deliberately committed through separate trait calls
with distinct failure handling (meerkat-session/src/persistent.rs) — file
co-location cannot make separate trait calls transactional, and remote
backends will never have cross-domain transactions anyway (the recovery
protocol is the contract, per Phase 0). Revisit only if a concrete
cross-domain invariant justifies designing a narrow composite-commit seam;
any table move that does happen gets changelog treatment equivalent to a
binary rename, because operators read these files directly.
Phase 4 — The provider seam
PersistenceBundle already accepts injected session/runtime/schedule/
workgraph/blob/artifact stores; what is missing is standardized selection and
bootstrap. So the seam is a store-only provider:
- The provider returns stores and durability declarations; the facade
composes
PersistenceBundleandMeerkatMachine. Mob storage stays mob-owned behind a per-mob storage factory the provider can supply — putting mob stores in the bundle directly would create the dependency cyclemeerkat → meerkat-mob → meerkat(mob depends on the facade), andMobEventStoreis sealed by design. - Fail-closed durability: a durable slot the provider does not supply is a startup error unless the realm manifest explicitly declares ephemeral semantics for that domain. (This rule, applied at mobkit’s blob store, would have prevented the month-long silent-in-memory outage.)
- Backend selection: the built-in
DiskStorageProvidercovers sqlite/jsonl/memory as today. Manifest v2 carries an explicitmanifest_formatversion and a provider discriminator that old readers reject rather than ignore —RealmBackend::External { name }is not wire-compatible with the current fieldless string codec, and additive fields are silently dropped by old binaries, which could then reopen a renamed-away SQLite path and create an empty database. Per-domain schema versions live solely in the backend’s own ledger, not duplicated in the manifest. - Manifest on ephemeral disk: in no-durable-disk mode the manifest cannot live on scratch or it resets every boot — the provider stores it in its own durable medium or derives it from deployment config; the disk file is a cache.
- Remote-friendliness requirements: remote providers implement
IncrementalSessionStore(O(delta) turns); blob/artifact stores get an object-store-shaped contract so transcripts stop being forced through query-sized requests; the Phase 0 append-only chapter is their conformance gate. - Scheduler injection is a foundation, not a drop-in replacement for downstream shadow schedulers: existing ones carry app semantics (multi-replica claim via conditional update, timezone cron, jitter, delivery-policy handoffs). Expect a feature-parity audit before any deletion.
- Surfaces converge on one bootstrap:
RuntimeBootstrap → StorageLayout → provider(manifest) → facade composition.
ContinuityStore, EventLogStore, the session
bridge, AgentMemoryProvider), and mobkit’s judgment plane (taint firewall,
Distiller, Steward) is currently welded to a concrete SQLite memory store
rather than the trait. mobkit must adopt the same rules in the same arc —
one remote bundle, declared (never silent) fallbacks, the migration-ledger
discipline, and legacy checkpoint adoption running for continuity snapshots
too (that is the store the resume bridge actually reads). Otherwise
downstreams keep two integration seams and two heads of session authority,
and the projection-drift incident class survives everything this plan fixes.
The mobkit work is tracked in its own repo as a companion plan
(docs/plans/storage-unification-plan.md in meerkat-mobkit); this plan owns
the meerkat-side contracts it needs. Three of those contracts carry explicit
obligations the companion’s dependency table relies on:
- The stamping helper ships on 0.8.x, not just the 0.9 arc — mobkit is
pinned to
=0.8.2and its continuity-snapshot adoption hotfix (H3) blocks on the export. StorageMigrator::diagnoseis shape-stable at Phase 1, even though the full provider trait lands in Phase 4 — mobkit’s doctor phase (M1) consumes the hook shape early, so it is defined (as a small standalone trait) when doctor lands, not retrofitted later.- The Phase 6 maintenance fence is a reusable library primitive over an
arbitrary state directory, not welded to rkat-resolved realm roots —
standalone mobkit gateways with no
rkatCLI on the box run their migrations under the same fence via a gateway-native maintenance command.
Phase 5 — Surface cleanup and the anti-regression gate
- Delete
default_cli_state_rootand per-surface locator defaults in favor of the shared resolver; route the REST task-store fallback through the layout. - Re-point docs (
docs/reference/capability-matrix.mdx,docs/reference/session-contracts.mdx). - CI gate, scoped correctly: ban ambient root resolution
(
$HOMEreads,dirs::*calls) outside the bootstrap/layout modules. Feature-owned relative paths (blob dirs, projection files, per-mob databases) stay with their owners — banning all path literals would create a storage god-module, which is its own fragmentation.
Phase 6 — Migration framework
rkat storage migrate [--apply], dry-run by default, offline, resumable,
fail-closed.
Fencing is a first-class design problem. The existing manifest lock and
blocks_destructive_prune lease check cannot fence migration: the lease
check is a point-in-time predicate (a new writer can open immediately after
it passes), and the manifest lock goes stale after 30 seconds, can be removed
by another process, and its guard deletes the replacement lock unconditionally
on drop (meerkat-store/src/realm.rs). Phase 6 introduces a dedicated
exclusive maintenance fence: an OS-level lock plus ownership token with
heartbeat, spanning both candidate roots, held through quiescence → WAL
checkpoint → copy → validate → publish. Migration refuses to start without
it — and honoring it at realm open is not sufficient: the SQLite stores
deliberately hold no long-lived connection (SqliteSessionStore is “one
connection per operation” by design), so a process that opened its realm
before the fence was acquired would keep writing straight through it. The
fence is therefore enforced per operation, in the Phase 3 connection helper:
every store operation takes a shared guard, migration acquires the exclusive
side and waits for outstanding guards and in-flight operations to drain
before checkpointing. Stores built on the shared helper get this for free;
that is another reason no store may roll its own opener.
Migration cases:
- Ledger baseline (auto-safe). A legacy SQLite file without
meerkat_schemais structurally verified and baseline-stamped under the fence. - State-root adoption (auto-safe). A realm existing under exactly one root is used where it lies (the Phase 2 resolver makes this the steady state). No data moves.
- Split-brain reconciliation (manual, fail-closed). For a realm id present under both roots: deduplicate exact-equality rows only; for anything divergent, adopt one root as authority and archive the other read-only, with a full per-domain divergence report. No newest-revision synthesis: workgraph’s rebuild authority is its locally sequenced event stream (per-database autoincrement — merging streams corrupts replay), and schedule heads/occurrences/claims/receipts are transactional aggregates, not independently revisioned rows. No “fork with a suffix” either — persisted session ids are UUIDs; preserved divergent sessions are archived under the non-authoritative root and surfaced in the report.
- Checkpoint-evidence adoption. The PR #909 machinery — the
SessionDocumentMachinedisposition plusadopt_legacy_session— invoked in bulk under the fence for any rows the lazy resolver path has not yet touched (including via the exported helper for remote backends and mobkit continuity snapshots, with observed cursors where continuity records a nonzero generation floor). - Deprecated leftovers (report-only). Legacy
~/.rkat/sessions-style directories, orphanedsession_index.sqlite3, stale lease files. Credentials do not move.
*.pre-<version>-<timestamp>), never deletes — and those artifacts are
registered: doctor lists them, rkat storage prune owns their lifecycle,
and their naming is documented so external retention tooling
(state-generation cloning, HomeCore-style prune jobs) can recognize them
instead of treating them as unknown files that bloat every clone.
Downstream migration. The StorageMigrator hook gives remote backends
the same lifecycle: a version ledger in their own medium (for BigQuery, a
meerkat_schema table) and ordered migrations under a provider-supplied
lock. Meerkat ships the framework and the disk implementation.
Sequencing, risk, and gates
- Hotfix ships first, on 0.8.x, independent of the arc.
- Phase order 0 → 1 → 2 → 3 → 4 → 5 → 6; Phases 2 and 3 are independent and can land in parallel; Phase 4 depends on 2; Phase 6 depends on 1 and 3.
- Each phase gates on the conformance suite plus
e2e-system. The Phase 2 resolver change additionally gates on a dual-root fixture matrix (realm in local only / global only / both / neither, per surface). - Riskiest items: the Phase 2 resolver semantics (it must never create a twin — the realm-id-first probe is the invariant) and Phase 6 fencing. Both get explicit changelog entries and doctor checks. Concurrency-heavy deployments (16-way member restore) should re-run their boot benchmarks when the opener-profile port lands, since contention texture may shift.
- Contract changes (manifest v2, provider discriminator) require the
make regen-schemascycle and SDK codegen per the standard CI gates. - Target: hotfix on 0.8.x now; the arc lands across 0.9; deprecations removed in 0.10.
Review disposition ledger (v1 → v2)
Accepted from the independent static review: migration fencing redesign (P0); split-brain merge rewritten to exact-dedup + adopt/archive, no synthesis, no session forks (P0); hotfix re-based on the runtime-store evidence path instead ofmigrate_legacy_blob_in_txn (P1); realm-id-first dual-root
probing (P1); invocation-context vs project-root split preserving the MCP
no-walk-up boundary (P1); runtime-store opener finding corrected as already
fixed on main (P1); file convergence deferred — co-location ≠ trait-call
atomicity (P1); mob stores stay mob-owned, provider returns store-only
result (P1); fail-closed durable slots (P1); manifest v2 old-reader rejection
(P1); CI gate rescoped to ambient-root resolution (P2); conformance
capability profiles + as_incremental discovery test (P2).
Accepted from ob3: hotfix pulled forward with exported stamping helper;
append-only conformance chapter (revision-guard semantics, sibling dedup
ownership, checkpoint monotonicity, recovery protocol as tested contract);
mobkit companion arc; scheduler feature-parity audit framing; manifest
placement on ephemeral disk; blob conformance + doctor dangling-ref repair;
store error taxonomy; provider diagnose hook in doctor.
Accepted from HomeCore: hotfix priority; legacy-data conformance axis with
real-dump fixtures; typed health-visible SchemaFromTheFuture (refuse at
certification, no crash-loop); split-brain detection scoped to resolved
roots; machine-readable durability classes (durable-only clones); registered
backup/retention discipline; doctor read-only-safe on live realms; boot-time
expectations documented for adoption migration; changelog policy for any
table moves. Note: HomeCore’s endorsement of the busy-timeout fix as “Bug
K’s grandfather” rested on the v1 claim that review disproved — the defect is
already fixed on main; their observed texture likely predates that or stems
from the remaining divergent openers.
Not adopted: unifying server realm-identity defaults with the CLI (isolation
decision, kept separate and documented); eager per-row stamp backfill at
ordinary open (replaced by explicit quiescent migration); automatic semantic
merge of divergent realms (replaced by adopt/archive + report).
Round 2 (v2 → v3). Accepted from the static review: per-operation fence
guards in the shared connection helper — fence-at-open cannot quiesce
connection-per-operation stores (P1); user_home_root/user_rkat_root
split preserving today’s home-like user_config_root parameter semantics,
plus an explicit comms-identity slot so the anti-ambient gate neither
exempts nor rotates durable keys (P1); the MobKit provider-layering
correction (P1, applied in the companion: a MobKit-owned composite provider
wraps RealmStorageProvider; MobStorageFactory reserved for genuinely
per-mob storage); the pinned ledger transaction protocol (P2); method-level
retryability with outcome reconciliation for indeterminate writes (P2); and
the filename-ownership boundary — layout owns roots and canonical top-level
locators, feature crates own relative filenames (P2, applied in both gates).
Accepted from HomeCore: the independently-adopted byte-divergent
canonical/projection pair as a named joint acceptance case for the hotfix
pair, validated against their real dump; HomeCore also retracted the “Bug
K’s grandfather” attribution after independently confirming v0.7.31 already
used the shared opener. Accepted from ob3: the lazy-at-restore adoption
variant as a sanctioned H3 mechanism for always-on single-replica
deployments (applied in the companion); the disposition-ledger pattern
propagated to the companion doc. Out of scope by agreement: ob3’s
“explosion #3” (run_flow → identity-first dispatch producing zero turns) is
a 0.8 runtime/flow defect, not storage — it needs its own issue and must not
hide under this arc.
Round 3 (rebase on PR #909). The hotfix landed as machine-owned lazy
auto-migration at the committed-authority resolver, with one operator-
confirmed override of this plan recorded in place: lazy per-session
adoption at first authority touch instead of an explicit quiescent step
(observability, idempotency, and the Phase 6 bulk/fenced verb preserved
from the original requirement — and the lazy shape is the one ob3’s review
had argued for). The exported helpers are meerkat_core::adopt_legacy_session
and legacy_session_transcript_relation. The divergent-pair authority rule
is superseded by the machine’s typed dispositions (extension adopted, stale
prefix rebuilt from canonical, unrelated transcripts refuse fail-closed);
the joint acceptance case narrows to the independently-adopted variant on
identity-first gateways. New documented residual: the lazy path seeds
INITIAL cursors, so nonzero-generation fleets must adopt via the helper
with observed cursors before the lazy path touches those sessions
(stickiness), which reorders mobkit H3 ahead of first resume on such
fleets.