Matrix logo

Store & Journal

The per-actor Pebble database, atomic write-batch machinery, key encoding, and the append-only journal that anchors cortex replay.

Package matrix/cortex/store manages the per-actor Pebble database and the atomic write batch machinery. Package matrix/cortex/journal defines the append-only write log entry shape. Package matrix/cortex/keys encodes all Pebble keys. Together they are the foundation every other cortex subsystem builds on.

Source files: cortex/store/store.go, cortex/store/writebatch.go, cortex/keys/keys.go, cortex/journal/journal.go.

Design decisions

One Pebble DB per actor. §11.1 requires that journal + head + version + indexes commit atomically. Pebble batches cannot span databases, so a single Pebble DB per actor is mandatory. All namespaces are key prefixes inside that DB — not separate files or databases.

Every mutation is journaled. WriteBatch.Commit returns ErrBatchNoJournal if the caller never called AppendJournal. This is load-bearing: the journal is the replay anchor, and a write that bypasses it silently breaks the Rebuild invariant.

Journal sequence is per-actor monotonic, gap-free. Allocation happens inside BeginWrite, which holds seqMu until Commit or Abort. Gap-free invariant: if seq=N exists, seq=N-1 must exist too. The embedder and replay harness depend on this.

Store

s, err := store.Open(root, actor, nil)
defer s.Close()

Opens (or creates) the Pebble DB at <root>/<actor>/store/. The actor string must not contain / — enforced by keys.ValidateNoSeparator.

// Point read
value, ok, err := s.Get(key)

// Prefix scan
err = s.PrefixIter(keys.PrefixIdxType, func(k, v []byte) error { ... })

// Journal seq (next-to-allocate)
nextSeq := s.NextSeq()

JournalHook

The snapshot layer installs a hook on the store so every AppendJournal call simultaneously stages an MMR leaf in the same atomic batch:

s.SetJournalHook(c.snap.MMRHook())

The hook fires per AppendJournal call, inside the same WriteBatch. It receives the raw Pebble batch and the leaf hash so it can stage additional keys without a separate commit.

WriteBatch

The atomic write transaction. Every mutating cortex operation (Write, Update, Tombstone, AddEdge, UpdateHead, Compact, Attest) creates one WriteBatch, stages all its keys, appends at least one journal entry, then commits.

wb := s.BeginWrite()
defer wb.Abort()  // no-op if Commit already succeeded

wb.Set(key, value)          // any key: m/, mv/, idx/, salience/, …
wb.Delete(key)              // for UpdateHead idx/tag/idx/frame removals
wb.AppendJournal(entry)     // at least once; may be called twice (Attest)
seq := wb.Seq()             // seq of the most recently appended entry

if err := wb.Commit(); err != nil { ... }

BeginWrite acquires seqMu. Only one WriteBatch may be open per Store at a time. The backing Pebble batch is an indexed batch so that staged writes within one batch are visible to sibling reads in the same batch — required for cortex.Attest's two-entry pattern (KindAttest at seq=N, KindLearnWeights at seq=N+1).

Key encoding

All Pebble keys are defined in cortex/keys. Invariants:

  • All numeric components are big-endian fixed-width so byte-sort == numeric-sort.
  • IDs are 16-byte binary ULIDs (textual only at API boundaries).
  • Versions and seq numbers are uint64, 8 bytes BE.
  • The path separator '/' is forbidden inside path components.

Namespace prefixes

PrefixKey shapePurpose
m/m/<id:16>MemoryHead (canonical, mutable)
mv/mv/<id:16>/v/<version:8>MemoryVersion (canonical, immutable)
e/from/e/from/<src:16>/<type:1>/<dst:16>Forward edge records
e/to/e/to/<dst:16>/<type:1>/<src:16>Reverse edge records
j/j/<seq:8>Journal entries (canonical)
tomb/tomb/<id:16>Tombstone markers
snap/snap/<seq:8>SnapshotManifest records
chk/chk/<intent>/<step>Compact checkpoint records
idx/type/idx/type/<t:1>/<created:8>/<id:16>Type index
idx/tag/idx/tag/<hash:8>/<created:8>/<id:16>Tag index
idx/frame/idx/frame/<verb:1>/<kind:1>/<hash:16>/<id:16>Frame-relevance index
idx/actor_obj/idx/actor_obj/<verb:1>/<hash:16>/<created:8>/<id:16>Outcomes index
idx/smt/idx/smt/<ns>/n/<depth:2>/<path:32>SMT node cache
salience/salience/<id:16>Per-memory salience score cache
vec/meta/vec/meta/<id:16>Vector embedding + HNSW vertex metadata
accum/accum/mmr/…Journal MMR accumulator leaves
meta/meta/<key>Store-level metadata

Important meta/ keys

KeyPurpose
meta/journal_headNext journal seq to allocate (uint64 BE)
meta/salience_weightsPer-actor learned EMA weights (canonical CBOR Weights)
meta/compile_cache/<hex>Compile-result sidecar cache (not in OverallRoot)
meta/goal_state/<id:16>Ambient scheduler runtime state per Goal (not in OverallRoot)
meta/embed_cursorNext journal seq for the embedding worker to process
meta/embed_vertex_nextNext HNSW vertex ID to allocate
meta/embed_modelModel string of the last completed embedding pass

Sidecar posture: meta/salience_weights, meta/compile_cache/*, and meta/goal_state/* are derived / runtime-policy state. They are NOT inputs to OverallRoot. The Rebuild operation drops and re-derives them from the journal.

Journal

The journal is the cortex's write-ahead log and audit ledger. Every WriteBatch.Commit writes at least one entry at j/<seq>.

Entry shape

type Entry struct {
    Seq       uint64   // per-actor monotonic, gap-free
    Kind      Kind     // string: "write" | "update" | "tombstone" | …
    CreatedAt int64    // Unix nanoseconds
    CreatedBy []byte   // agent ref
    Payload   []byte   // canonical CBOR — kind-specific payload
}

Leaf hash

leafHash = sha256("matrix.cortex.journal.v1" || canonical_CBOR_entry)

Domain-separation prefix prevents cross-protocol hash collisions. The leaf hash feeds the journal MMR.

Journal kinds

KindWhen
writecortex.Write
updatecortex.Update
tombstonecortex.Tombstone
add_edgecortex.AddEdge
remove_edgecortex.RemoveEdge
embedAsync embedding worker — after vec/meta + Head.EmbeddingRef are staged
compactcortex.Compact — checkpoint committed
update_headcortex.UpdateHead — Head-only mutation
find_latecortex.Find with LateBinding=true — D13 audit hook
scope_violationSub-agent read/write outside its CortexScope
attestcortex.Attest — intent outcome + cited memory IDs
learn_weightsAlways immediately follows attest — EMA weight delta
gcFuture version GC sweep
migrationFuture schema migration step

Iterating the journal

err = s.IterJournal(fromSeq, func(e *journal.Entry) error {
    // process e
    return nil
})

Used by the embedding worker, replay rebuild, and audit tooling. The gap-free invariant means every seq from 0 to s.NextSeq()-1 has a corresponding entry.

Modifying the store or journal

What to changeWhere
Add a new key namespacekeys/keys.go — new Prefix* var + constructor; replay/drop.go — add to derivedPrefixes if derived
Add a new journal Kindjournal/journal.go — new Kind const; add payload type; update replay/rebuild.go to re-apply on rebuild
Change journal encodingjournal/journal.go — bump LeafDomain version string; all existing journals become unverifiable without migration
Tune Pebble optionsstore/store.goOptions.PebbleOptions