Memory-Kind Vocabulary (Form 6, issue #759; Pillar 2, issue #1709)
v0.7.x extends the substrate’s MemoryKind enum from the original
three lifecycle variants (Observation / Reflection / Persona)
with the seven-variant Batman taxonomy extension, and v0.8.0
(Pillar 2, #1709) adds the three-variant typed-cognition cluster. The
full set is now 13 kinds:
| variant | purpose |
|---|---|
observation |
direct note from the caller — the default. |
reflection |
curator-synthesised summary over lower-depth peers. |
persona |
curator-generated entity profile (QW-2). |
concept |
abstract definition / vocabulary term. |
entity |
named real-world thing (person, org, product, system). |
claim |
factual assertion the caller is committing to. |
relation |
typed pair / triple anchored in the memory substrate. |
event |
temporally-bounded happening. |
conversation |
captured dialogue turn. |
decision |
choice point with rationale (L1-6 reservation). |
goal |
a desired end-state / objective (Pillar 2 typed-cognition, #1709). |
plan |
an ordered strategy to reach a goal (Pillar 2 typed-cognition, #1709). |
step |
a single executable unit within a plan (Pillar 2 typed-cognition, #1709). |
The first three are the v0.7.0 lifecycle variants and are unchanged.
The next seven (Form 6) give downstream readers a richer
filter-by-kind surface aligned with the Batman framework’s exemplar
(Tolaria’s frontmatter-as-type schema). The final three
(goal / plan / step, v0.8.0 #1709) are the Pillar-2
typed-cognition kinds: a goal names a desired end-state, a plan
is the ordered strategy to reach it, and a step is one executable
unit within that plan.
Schema impact: none
The memories.memory_kind TEXT column has no CHECK constraint on
either the SQLite or Postgres backends, so the new variants land as
new string values on the existing column. No migration required;
schema version stays at v37 / v18 respectively. Backward compat:
- Old rows with no
memory_kindvalue read asObservation(the SQLDEFAULT 'observation'). - Future variants emitted by a newer client to an older binary
read as
Observationvia theunwrap_or_default()fallback inrow_to_memory. - Old binaries reading a new variant from the DB also fall through
to
Observation— the wire shape stays compatible across version drift.
The fallbacks above are read-path only (row_to_memory widening a
stored/unknown value). They do NOT apply to the write path.
Write-path validation (#1467)
On every write surface — CLI store, MCP memory_store, and HTTP
POST /api/v1/memories — a supplied kind is validated before the row
is created:
- Omitting
kind(absent /null) keeps the defaultObservation. - A non-empty
kindMUST be an exact match for one of the canonical lowercase variants in the table above; anything else (unknown token, wrong case, whitespace) is rejected withinvalid kind '<value>' (expected one of: …).
Prior to #1467 the MCP and HTTP surfaces silently coerced an
unknown/invalid kind to observation while the CLI rejected it; all
three surfaces now reject consistently, so a typo’d kind never lands a
misclassified row. The canonical accepted set is MemoryKind::all()
(the error message is generated from it, never a hardcoded list).
Recall filter
The new kinds parameter on memory_recall (MCP), ?kinds=… (HTTP
GET), and kinds: … (HTTP POST body) accepts either:
- a comma-separated string:
"concept,entity,claim" - a JSON array:
["concept", "entity", "claim"] - the literal
"all"(case-insensitive) ⇒ no filter (equivalent to omission)
OR-of-kinds within the param; AND with the other filters (namespace, tags, time-window, visibility). Unknown tokens are silently dropped so a newer client emitting a future variant doesn’t break recall on an older binary.
MCP
{
"tool": "memory_recall",
"args": {
"context": "policy on token rotation",
"kinds": ["claim", "decision"]
}
}
HTTP
GET /api/v1/recall?q=policy+rotation&kinds=claim,decision
POST /api/v1/recall
{
"context": "policy on token rotation",
"kinds": ["claim", "decision"]
}
CLI
ai-memory recall "policy on token rotation" --kind claim,decision
Auto-classify pre-store hook
The substrate ships a namespace-policy-gated pre-store hook
(auto_classify_kind)
that may rewrite a stored memory’s memory_kind from the default
Observation to a more specific Batman-taxonomy variant. Three
policy modes, set on the namespace standard’s metadata.governance
JSON blob under auto_classify_kind:
{
"governance": {
"auto_classify_kind": "off" | "regex_only" | "regex_then_llm"
}
}
off(default). Substrate quiet — caller-supplied (or defaultObservation) kind stands.regex_only. Deterministic regex heuristics. ~tens of microseconds per call; safe to run on every write. Fires only when the content carries a strong signal (e.g.is_a⇒Concept,happened on⇒Event,X says:⇒Conversation,decided to⇒Decision,depends on⇒Relation). Misses keep the row atObservation.regex_then_llm. Regex first; if no heuristic fires, fall through to a single-shot LLM classifier. Opt-in only — the substrate never spawns an LLM round-trip on a namespace whose policy isofforregex_only. The LLM round-trip path is feature-gated onllm.classify_kind; if a runtime doesn’t carry a classifier, the hook degrades toregex_onlysemantics silently (logged at debug).
The caller-supplied kind parameter on memory_store always wins
— the hook only fills in Observation (the default) when no kind
was set. This keeps explicit-typing callers in full control while
giving operators an opt-in path to classify legacy / unstructured
content automatically.
Operator surface
The substrate exposes the recall-filter and auto-classify wiring
under the memory_kind_vocab block of the v3 capabilities
response. Operators can read the live state via:
printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"memory_capabilities","arguments":{"accept":"3"}}}' \
| ai-memory mcp --profile core | jq .
(The v0.7-alpha drafts referenced ai-memory doctor --capabilities=v3;
that flag was not shipped. The MCP memory_capabilities tool is the
canonical inspection surface — it works against any running daemon
regardless of profile because memory_capabilities is on the
ALWAYS_ON_TOOLS allowlist.)
{
"vocabulary": ["observation", "reflection", "persona", "concept",
"entity", "claim", "relation", "event",
"conversation", "decision", "goal", "plan", "step"],
"recall_filter": "implemented",
"cli_filter": "implemented",
"auto_classify": "implemented",
"auto_classify_modes": ["off", "regex_only", "regex_then_llm"]
}
Forward-compat reservations
Decision is the only L1-6 reservation in the v0.7.x set. The
L1-6 work (v0.8.0) will likely add columns for rationale /
alternatives on top of the variant; binaries that ship the
variant now can already type-tag decisions so downstream readers
get a stable filter surface from day one.
Why no schema bump
The original L1-1 work (v0.7.0) landed the memory_kind TEXT NOT
NULL DEFAULT 'observation' column under migration 0025 / 0018
without a CHECK constraint. That was a deliberate forward-compat
choice: new variants land as new column values; no migration is
required to widen the accepted set. The decision is documented in
the L1-1 commit and validated by Form 6’s no-migration ship.