Cortex
Cortex is the per-actor typed memory graph on Pebble with 14 phases of implementation: typed taxonomy, salience-ranked queries, HNSW vector search, Merkle snapshots, EMA weight learning, sub-agent scoping, and the replay invariant.
Cortex is the per-actor typed memory graph. It is authoritative, byte-deterministic, and replay-rebuildable. One Pebble DB per actor; all namespaces are key prefixes. Every store mutation journals atomically; the journal feeds a Merkle accumulator; snapshots seal the state root. Drop the derived indexes, replay the journal, roots match. That is the invariant everything else depends on.
Key surfaces
| Package | Role |
|---|---|
cortex.go | Facade: Write, Resolve, Update, UpdateHead, Tombstone, Find, Context, Compact, Attest, AddEdge, RemoveEdge, Snapshot, OverallRoot, Rebuild |
store/ | Pebble shell + BeginWrite atomic batch + JournalHook |
journal/ | Append-only write log (canonical CBOR entries), 15+ entry kinds |
memory/ | 9-type taxonomy + canonical CBOR + validate + verb/edge types |
keys/ | Key-prefix encoding (memory namespaces) |
query/ | Predicate AST + planner + Find with OrderBy/Form/Budget + graph BFS |
forms/ | Per-type deterministic Render for short/medium/full |
salience/ | 5-factor cold score + EMA weight learner + learned weight persistence |
embed/ | Embedder interface + HashEmbedder stub + Fireworks API client |
vector/ | Pure-Go HNSW + persistence (M=16, efC=200, efS=64) |
snapshot/ | MMR + SMT-256 + SnapshotManifest + OverallRoot + MultiProof |
scope/ | Sub-agent CortexScope (Merkle-proof-bounded reads/writes) |
replay/ | DropDerived + Rebuild + Phase 11 verifier |
embedder.go | Async embedding worker: StartEmbedder/StopEmbedder/DrainEmbedder |
goal_state.go | GoalRuntimeState sidecar (ambient scheduler bookkeeping) |
ratelimit.go | Phase 14 token-bucket DoS guards (scope violations + attests) |
Repository layout
cortex/
├── go.mod
├── cortex.go # top-level facade
├── attest.go # Attest - intent-outcome salience feedback + EMA weight update
├── compact.go # Compact - budget-aware context compaction + checkpoint records
├── context.go # Context - three-tier cold-start bundle composer
├── edges.go # AddEdge / RemoveEdge / GetEdge / IterEdgesOut / IterEdgesIn
├── embedder.go # StartEmbedder / StopEmbedder / DrainEmbedder + HNSW worker
├── goal_state.go # GoalRuntimeState sidecar
├── ratelimit.go # Phase 14 token-bucket DoS guards
├── rebuild.go # Cortex.Rebuild facade
├── scope_enforce.go # VerifyScope / enforceRead / enforceWrite / logScopeViolation
├── update_head.go # UpdateHead - Head-only mutations without Data version bump
├── keys/ # all Pebble key constructors + namespace prefixes
├── journal/ # Entry, Kind, payload types, canonical CBOR, leaf hash
├── memory/ # Type, Head, Version, Forms, Tombstone, data schemas, verb, edge
├── store/ # Open, Get, PrefixIter, JournalHook, NextSeq, WriteBatch
├── salience/ # Score, ColdScore, ColdScoreWith, Weights, EMA helpers
├── forms/ # Render (short + medium), RenderFull, TruncateToTokens
├── query/ # Predicate AST, eval, find planner, graph BFS
├── embed/ # Embedder interface + HashEmbedder + API embedder
├── vector/ # NewIndex, Add, Search, Save, Load, MapStore, VectorStore
├── snapshot/ # State, Snapshot, CurrentRoots, MMR, SMT, MultiProof
├── scope/ # Scope, Selector, Sign, Verify, KeyResolver
├── replay/ # Rebuild, DropDerived, VerifyPreservesRoot
└── cmd/cortex-shell/ # smoke-test CLI
Memory taxonomy (Phase 1-3)
Cortex stores 9 typed memory records, each with deterministic rendering forms (short / medium / full) so the same record always serializes identically:
| Type | Short scaffold |
|---|---|
| Identity | {name} or {name} ({did}) |
| Fact | {predicate}({subject})={statement} |
| Preference | prefers {topic} ({polarity}, strength={s:.2f}) |
| Belief | {stance} {statement} |
| Event | {outcome} {kind} with {counterparty} cost={...} |
| Goal | [{status}] {statement} |
| Constraint | [{strength}] {polarity} {statement} |
| Capability | {subject} can {capability} ({verified?}) |
| Pattern | {statement} (strength={s:.2f}, coverage={c}) |
Each memory carries a Head (ID, type, current version, timestamps, tags, frames, visibility, declared importance) and versioned Version records (Data, Forms, CreatedBy, Confidence, Provenance).
Write API
cortex.Write(h, data, meta) inserts a new memory at version 1. The atomic Pebble batch contains:
- The journal entry (
j/<seq>) meta/journal_headbump- The new
MemoryHead(m/<id>) - The new
MemoryVersion(mv/<id>/v/1) - Predicate index keys (
idx/type,idx/tag,idx/frame,idx/actor_obj) - Salience cache seed
- SMT update for the memories namespace
Either everything commits or nothing does. There is no path that mutates store without a corresponding journal entry (ErrBatchNoJournal enforces it).
Reads: Find and Context
Find
Find(query.Query) runs a typed predicate query through a planner. The predicate AST supports: Eq Ne Gt Gte Lt Lte In HasTag Matches And Or Not.
Planner strategy:
- If the top-level And-conjunction contains
HasTagpredicates, scanidx/tag/<sha256(tag)[:8]>for each, intersect to candidate IDs. - Else scan
idx/type/<t>for eachTypein the query, union to candidate IDs. - Else
ErrTooBroad(full-store scans are refused).
Safety rails:
Findrejects unbounded queries (must passLimitORBudgetTokens).Findrejects too-broad queries (must passTypeORHasTag).- Tombstoned memories excluded by default;
IncludeTombstoned=trueopts in. LateBinding=truejournals aKindFindentry recording the predicate for audit.- Compile-time
Find(LateBinding=false) does not journal.
Find Near / Find NearURI do HNSW vector search. Find From / Find Follow do bounded BFS graph traversal with typed edges.
Context
Cortex.Context(opts) assembles a deterministic three-tier cold-start bundle:
- Pinned - Identity + Constraint{Hard} + Goal{Active}. Tombstone-filtered. Salience-desc. Floor at 0.7.
- Outcomes - Events ordered by
(verb, ref)tuples, created-desc, top N. - FrameRelevant - Memories matching
(verb, kind, ref)tuples, salience-desc.
Dedup priority: Pinned > Outcomes > FrameRelevant. Global salience-asc trim across all tiers until tokens fit budget. API refusal at the type level: no Near/NearVector field exists on ContextOpts (cold-start with vector recall is forbidden).
Salience and learning
Recall ranking combines a 5-factor cold score with per-actor EMA weight learning.
Cold score formula
salience(m) = (0.25*R + 0.15*A + 0.30*C + 0.20*D + 0.10*V) / 1.0
R(m) = exp(-delta_t / 90d) # recency
A(m) = log(1+access)/log(1+1000) # access count
C(m) = log(1+citations)/log(1+1000) # citation count
D(m) = declared_importance / 10 # declared
V(m) = vector similarity (when Near set) # vector weight
Pinned floor 0.7. Tombstoned ranks to 0 (filtered upstream).
EMA weight learning (Phase 12)
When an intent terminates, cortex.Attest emits KindLearnWeights and the EMA (rate = 0.05) pulls the actor's factor weights toward (on success) or away from (on failure with reason in {factual_error, wrong_assumption}) the cited memories' factor profile. Weights are stored at meta/salience_weights (outside OverallRoot). KindLearnWeights journal entries DO contribute to journal_root, so replay reconstructs the same learned weight set deterministically.
Phase 11.5 wires the data: AccessCount bumped by Find (late-binding), Citations bumped by Attest. These are the load-bearing training signal for Phase 12.
Edges and graph traversal (Phase 6)
14 typed edge types: derived_from, supersedes, references, contradicts, corroborates, consents_to, dispatched_to, attested_by, cited_in, tombstones, part_of, instance_of, caused_by, observed_by.
Each AddEdge writes forward (e/from/<src>/<type>/<dst>) and reverse (e/to/<dst>/<type>/<src>) keys with the same canonical CBOR bytes in one atomic batch with the journal entry. RemoveEdge sets Tombstoned=true on both directions (soft delete, audit trail preserved).
Find From / Find Follow runs a hop-bounded BFS. MaxHops capped at 6. Direction modes: DirOut, DirIn, DirBoth. Default ordering: hop-ascending.
Snapshots and proofs (Phase 7)
State is Merkle-anchored:
- MMR over the journal (every
j/<seq>write appends exactly one MMR leaf in the same atomic batch viaJournalHook). - SMT-256 over current heads per namespace (memories + edges). Empty-subtree compression.
- OverallRoot =
sha256(domain || schema_v1 || journal_root || ns_count || lp(name)||state_root for each ns sorted).
Cortex.Snapshot(reason) persists a SnapshotManifest. Cortex.OverallRoot() returns the current root without persisting. Cortex.Proof(uris, manifest) returns MultiProof for the URIs against the manifest's memories root.
Storage rough-back for 100k memories + 100k edges: ~13 MB MMR nodes + ~160 MB SMT nodes + ~10 MB manifests/month. Acceptable.
Compaction (Phase 9)
Cortex.Compact(opts) summarizes non-load-bearing memories into {ref, short_form, salience} stubs while keeping load-bearing and auto-detected pinned items at full fidelity. The algorithm:
- Build effective load-bearing set (caller's
LoadBearingunion auto-detected pinned items). - Partition into Kept vs Compactable.
- Summarize Compactable items to short forms (50 tok, from write-time
Version.Forms.Short). - If post-summarization total still exceeds budget, return
ErrBudgetUnreachable(summarize-and-link, never truncate). - Write
CheckpointRecordtochk/<intent>/<step>+KindCompactjournal entry in one atomic batch.
Checkpoint URI: matrix://journal/logs/<intent>/<step>. Optional filesystem JSON mirror (best-effort).
Sub-agent scoping (Phase 10)
CortexScope issues Merkle-proof-bounded reads to sub-agents so a delegate can be handed a verifiable slice of memory without the whole DB.
Scopecarries a canonical CBORSelector(Types/Tags/IDs/Frame) with optionalFrameFilter(Verb/ObjHashes).Sign(s, priv)/VerifySignature(s, pub)over unsigned CBOR bytes.Verify(s, snapState, resolver, opts)runs: schema -> empty-include reject -> expiry -> signature -> snapshot resolvability -> multi-proof.- Read paths (
Find,Context,ResolveScoped) honorq.Scope. Multi-target reads filter silently; single-target reads journalKindScopeViolationon miss. UpdateHeadrequiresScope.Writable=truefor sub-agents (default deny).
UpdateHead mutates {Tags, Frames, DeclaredImportance, Visibility} without bumping Head.CurrentVersion. Index entries are hard-deleted and re-emitted in one atomic batch.
Replay invariant (Phase 11)
Cortex.Rebuild(opts) implements the Phase 11 replay invariant:
store/ KEEP m/ mv/ e/ j/ tomb/ snap/ chk/
KEEP meta/journal_head meta/snapshot_seq
indexes/ DROP vec/ idx/ salience/ accum/
DROP meta/embed_cursor meta/embed_vertex_next
After drop: walk m/ to re-emit idx/type, idx/tag, idx/frame, idx/actor_obj, and seed salience/<id>; walk e/from/ to stage the edges SMT; walk j/<seq> to replay the journal MMR. vec/* is intentionally NOT rebuilt (re-embedding lives behind the Embedder boundary).
Phase 11.5 extends replay to re-apply BumpForAccess / BumpForCitation / DecrementCitation from KindFind and KindAttest journal entries. Phase 12 extends it to re-apply KindLearnWeights entries.
Verification: RebuildResult.PreOverallRoot == RebuildResult.PostOverallRoot. Run on every PR by the replay-invariant CI job.
Rate limiting (Phase 14)
Token-bucket DoS guards on:
logScopeViolationjournal writes (R5 DoS surface: misbehaving sub-agent spams violations to churn OverallRoot).cortex.Attestentries (R3b DoS surface).
Always non-nil after New; never persisted (runtime policy state, not memory data).
Load-bearing invariants
These are enforced in code and must never be relaxed without a schema bump:
- Byte-sort == numeric-sort (BE uint64 keys).
- One Pebble DB per actor, isolated on disk.
- Journal
seqis monotonic, gap-free, persists across reopen. - Every store mutation journals (
ErrBatchNoJournalenforces it). - Atomic batch: journal + head + version + idx/* + salience commit-or-abort together.
- Canonical CBOR for journal entries (RFC 8949 deterministic).
- Domain-separated leaf hash (
matrix.cortex.journal.v1). - Domain-separated memory hash (
matrix.cortex.memory.v1+ Type byte). #latestis rejected at URI parse time (D13).- Tombstoned blocks
Update; old versions stay resolvable (audit trail). Findrejects unbounded and too-broad queries.Findranks by salience-desc by default.- Auto-rendered forms are deterministic (same inputs, same bytes).
- Skill-supplied oversize forms hard-rejected with
ErrFormTooLong. - BudgetTokens trim drops lowest-salience first; always retains at least 1.
- Phase 13.4: drop derived, walk the journal, expect a byte-identical
OverallRoot.
