Write API
Package matrix/cortex exposes four mutating primitives: Write, Update, Tombstone, and UpdateHead. Every call commits an atomic Pebble batch containing the canonical record chang...
Package matrix/cortex exposes four mutating primitives: Write, Update, Tombstone, and UpdateHead. Every call commits an atomic Pebble batch containing the canonical record changes, all affected index keys, a salience update, and a journal entry. Either everything commits or nothing does.
Source files: cortex/cortex.go, cortex/update_head.go.
Design decisions
Atomicity is non-negotiable. Every write uses a single store.BeginWrite → AppendJournal → Commit batch. There is no path that mutates a store key without a corresponding journal entry; Commit enforces this via ErrBatchNoJournal.
Forms are rendered at write time. forms.Render runs inside Write and Update before the batch commits. Short and medium forms are persisted alongside the data. This pays a one-time cost at write time so all subsequent reads are zero-recompute.
Version bumps are for Data changes only. UpdateHead rewrites mutable Head fields — Tags, Frames, DeclaredImportance, Visibility — without incrementing the version counter. The URI stays matrix://cortex/<type>/<id>#<n> where n is unchanged. A new Data version requires Update.
Write
Inserts a new memory at version 1. Returns matrix://cortex/<type>/<id>#1.
uri, err := c.Write(
memory.Head{
ActorScope: "andrew",
Visibility: memory.VisPrivate,
Tags: []memory.Tag{"onchain", "paxeer"},
DeclaredImportance: 7,
},
memory.FactData{
Subject: "matrix://tool/paxeer-net/chain_info@0.1.0",
Predicate: "latest_block",
Statement: "12345678",
Source: memory.SourceObserved,
},
cortex.WriteMeta{
CreatedBy: "paxeer-assistant",
Confidence: 1.0,
Provenance: memory.Provenance{Kind: "tool_call"},
},
)
What the batch contains
m/<id> ← canonical CBOR Head (v=1, Forms rendered)
mv/<id>/v/1 ← canonical CBOR Version (Data, Hash, Forms)
idx/type/<t>/<ts>/<id> ← type index entry
idx/tag/<h>/<ts>/<id> ← one entry per tag
idx/frame/<…>/<id> ← one entry per FrameRef on the Head
idx/actor_obj/<…>/<id> ← one entry per FrameRef if TypeEvent
salience/<id> ← cold score seeded from DeclaredImportance
j/<seq> ← KindWrite journal entry
accum/mmr/… ← MMR leaf (via JournalHook)
idx/smt/memories/… ← memories SMT update (via StageMemoryUpdate)
URI scheme
matrix://cortex/<Type>/<crockford-ulid>#<version>
ParseURI rejects #latest (D13). All version references must be pinned.
Update
Creates a new Data version on an existing memory. Increments CurrentVersion. Returns matrix://cortex/<type>/<id>#<n+1>.
newURI, err := c.Update(existingURI, newFactData, meta)
- Rejected if the memory is tombstoned (
ErrTombstoned). - Type of
newDatamust match the URI type (ErrTypeDataMismatch). - Old version records remain intact and resolvable forever — audit trail per §6.
- Forms are re-rendered against the new Data.
- Salience
LastUsedis bumped.
The URI passed to Update may reference any version number — only the ID is used to locate the head.
Tombstone
Soft-deletes a memory. Idempotent.
err := c.Tombstone(uri, "superseded by v2", "paxeer-assistant")
The MemoryHead is rewritten with Tombstoned set. Version records are NOT deleted — they remain readable for audit via Resolve(uri). A tomb/<id> marker is written as a fast existence probe.
Salience is collapsed to 0 (factor inputs are preserved so an un-tombstone path could recompute correctly). The memories SMT root advances to reflect the soft-delete.
UpdateHead
Rewrites mutable Head fields without creating a new Data version. Returns the unchanged URI.
tags := []memory.Tag{"audited", "onchain"}
newURI, err := c.UpdateHead(
uri,
cortex.HeadPatch{
Tags: &tags,
DeclaredImportance: &importance,
},
cortex.UpdateHeadMeta{CreatedBy: "auditor"},
)
HeadPatch uses pointer fields — nil means "no change", non-nil means "replace wholesale". To remove all tags, pass &[]memory.Tag{}.
Mutable fields
| Field | Notes |
|---|---|
Tags | Pointer semantics; immutable in old versions |
Frames | Pointer semantics; idx/frame entries updated atomically |
DeclaredImportance | 0..10; affects salience.D factor |
Visibility | Must be a valid Visibility value |
idx-key mechanics
UpdateHead computes the diff of old vs new Tags and Frames and surgically inserts/removes the matching index keys in the same atomic batch:
idx/type/*— never touched (type is immutable)idx/tag/*— scan for old tag keys (shape includes creation timestamp); hard-delete; insert new at currentnowidx/frame/*— direct delete by reconstruction (no timestamp component); insert newidx/actor_obj/*— scan-and-delete for Event-typed memories; insert new
Scope gating
Sub-agent UpdateHead requires Scope.Writable=true (Phase 10 Q7). If the scope is non-nil but not writable, ErrNotWritable is returned and a KindScopeViolation entry is journaled.
URI parsing and construction
uri := cortex.BuildURI(memory.TypeFact, id, version)
// → "matrix://cortex/Fact/01JXXX...#1"
t, id, version, err := cortex.ParseURI(uri)
// #latest → ErrBadURI (D13)
// version 0 → ErrBadURI
// unknown type → ErrBadURI
Error reference
| Error | Cause |
|---|---|
memory.ErrEmptyData | Write/Update called with nil data |
memory.ErrTypeDataMismatch | Update data type doesn't match URI type |
memory.ErrTombstoned | Update or UpdateHead on a tombstoned memory |
memory.ErrNotFound | Resolve can't find the head or version |
memory.ErrBadURI | Malformed URI or #latest (D13) |
ErrNoOp | UpdateHead called with an empty patch |
memory.ErrFormTooLong | WriteMeta.FormsOverride exceeds token budget |
Modifying write behavior
| What to change | Where |
|---|---|
| Auto-form templates | forms/forms.go — per-type render functions |
| Validation rules | memory/validate.go — ValidateMemory |
| WriteMeta shape | cortex/cortex.go — WriteMeta struct |
| HeadPatch mutable fields | cortex/update_head.go — HeadPatch struct + apply logic |
| idx-key construction | cortex/update_head.go — findIdxTagKey, findIdxActorObjKey |
