Memory plane
An agent needs to remember things at more than one timescale. Within a single dispatch it holds the conversation it is building. Across a session it accrues what happened. Across many sessions it should draw on knowledge that earlier runs produced, and it should honor the conventions a project has already established. The memory plane splits all of this into distinct tiers — each with its own lifetime, scope, and store surface — persisted through one durable port, and readable under any coordination loop.
What it governs
Section titled “What it governs”- The durable, addressable memory tiers — episodic, semantic, procedural —
each a named
MemoryTierwith its own lifetime, scope, and set of store methods. - Working memory — the in-flight message stream a dispatch builds — and the snapshots that make a session recoverable. Working memory is deliberately not a durable tier (see below); it is per-instance and ephemeral.
- How each tier is scoped and bound: per-agent, per sibling group, per tenant, per project — optionally into a named memory space so recall can outlive a single session.
- Cross-run knowledge — how a later run confers over what earlier runs contributed, through the tenant-scoped semantic tier.
What it deliberately does not govern: the prompt a single model call sees (the context plane), the durability machinery itself (sessions, events & durability), and where a secret lives (the credential plane). Memory never holds a credential.
The tiers
Section titled “The tiers”MemoryTier is a closed enum with three variants — episodic, semantic,
procedural. Working memory is intentionally absent from it: it is the
ephemeral state a dispatch carries, never a durable addressable space. So there
are four kinds of memory in play, but only three tiers.
| Memory | Lifetime | Scope | Store surface |
|---|---|---|---|
| Working | per-dispatch, snapshotted at node completion | per-agent, unconditional read-write | working_memory_read / _write |
| Episodic | session-local, or cross-session in a named space | per-agent or per sibling group | episodic_read / _tail / _write |
| Semantic | cross-session | tenant-wide, or a named space | semantic_search / _write |
| Procedural | cross-session | per project (tenant × project_id) | procedural_load / _save |
Working memory is the message stream — assistant turns and tool results — that an agent accumulates during a dispatch. Every agent gets it read-write, unconditionally; there is no access mode to withhold it. It is snapshotted to the store each time a node completes, where a node is one iteration of the worker’s tool-use loop — so the snapshot cadence is per-turn, not once at end-of-task. That snapshot is also the recovery point: a restarted session rehydrates working memory from the last checkpoint.
Episodic memory is what happened during a session. It is scoped by
EpisodicScope to either a single agent (PerAgent) or a sibling group —
agents spawned from the same parent under the same role, sharing one episodic
record. Sharing is opt-in per role and defaults to per-agent. Reads come in two
shapes: a full read, and a bounded recency tail used at dispatch time.
Semantic memory is the tenant-scoped, cross-session knowledge tier. A write stores content plus metadata and an embedding; a read is a hybrid search — vector similarity blended with a lexical rank (detailed below). This is the tier that carries knowledge forward across runs.
Procedural memory is a project’s operating conventions, authored as an
AGENT.md document and parsed into named sections. It is per (tenant, project_id), versioned with a monotonic revision, and — unlike the other tiers
— eager-loaded: it is prepended as the first system message of a fresh agent, so
the conventions frame every model call from turn zero.
Every tier persists through one StateStore port. Three adapters implement it —
in-memory, SQLite, and Postgres (fronted by a Redis hot cache for working memory
and event-log append) — and the tier semantics above are identical across all
three. The port lives in the substrate types crate; the adapters live beside it.
Scope, access, and memory spaces
Section titled “Scope, access, and memory spaces”An agent’s reach into the durable tiers is a MemoryScope: an AccessMode
(None → Read → ReadWrite) per tier, plus a Locality. Access is granted
per role, so a worker can read semantic memory without being able to write it,
or be denied a tier entirely. Working memory has no entry here — it is always
read-write.
By default each tier resolves to its tenant-default, unnamed space. A start
request can instead bind a tier to a named memory space — a semantic_space
or episodic_space on the request, and a project_id for procedural — which
turns session-local recall into durable, cross-session recall under that name. A
named space is created explicitly (memory_space_create, exposed over gRPC and
the REST memory routes) and listed per tier. Binding happens once, at role-config
resolution when the agent is spawned, and is stamped onto the agent for the life
of the session.
Cross-run knowledge: confer and contribute
Section titled “Cross-run knowledge: confer and contribute”Two verbs move knowledge across the session boundary. Confer reads relevant prior knowledge into a dispatch; contribute writes what a completed session learned back out.
Confer happens at dispatch setup, for the planner, worker, and synthesizer
tiers only — the validator tier is deliberately excluded so a judge’s evaluation
stays sealed from prior-run influence. The orchestrator runs a hybrid
semantic_search over the bound space, filtered by a per-agent relevance floor,
and injects the results as one system message headed “Relevant prior
knowledge:”. That message lands after any procedural AGENT.md block, so the
project conventions stay at index zero. Confer queries the store directly and is
deliberately not billed as a tool call. The episodic tier is conferred the
same way, from its recency tail.
Contribute happens at the end, when a session synthesizes a non-empty result successfully. It is not a raw append: it is a consolidation pipeline — extract candidate facts, retrieve their nearest existing neighbors, classify each as add / update / delete / no-op, then apply. That is what keeps the semantic tier from accumulating near-duplicates run after run. Contribution is gated on write access to the semantic tier and is best-effort — a consolidation failure is logged, never fatal to the session.
Both verbs are inspectable: confer emits a MemoryConferred event carrying each
result’s vector, lexical, and combined scores, and contribute emits
MemoryContributed. The tiers themselves are loop-agnostic substrate — every
coordinator writes through the same port, so every coordination
loop shares one memory model rather than each inventing its
own.
No embedding provider means no semantic tier
Section titled “No embedding provider means no semantic tier”The semantic tier needs an embedding provider, and a deployment may not have one configured. In that case every semantic call is refused with a typed error that names the config key — it does not fall back to a stand-in:
semantic memory is not configured: this deployment resolved no embedding provider, so there is nothing to embed against.
This is deliberate, and the alternative is worse in a way that is easy to miss. A hash-based stand-in returns consistent rankings, so every semantic test passes, the search returns plausible-looking results, and nothing anywhere reports a problem — while the rankings mean nothing. A refusal an operator can see beats a plausible answer they cannot distinguish from a real one.
For a local-dev stack that genuinely wants hash vectors, the relaxation is explicit, named, and logged at boot:
[substrate]embedding_provider_policy = "allow_fake_local_dev"Unset, absent, or unrecognised resolves to the strict posture — forgetting the flag fails safe. All three store adapters behave identically here.
Retrieval, honestly per backend
Section titled “Retrieval, honestly per backend”Semantic search is hybrid: a SemanticQuery carries a vector weight, a lexical
weight, and an optional cosine relevance floor; each result reports a
vector_score, a lexical score, and their weighted combined_score. The vector
half is always cosine similarity over the stored embedding. The two halves are
computed differently per adapter, and the difference is worth knowing:
- In-memory — a full linear scan over the space, cosine similarity plus a
hand-rolled BM25 lexical rank. No index; correct and simple, sized for dev and
test. See
apomesh-substrate-state. - SQLite — a linear-scan cosine plus SQLite’s native FTS5
bm25()ranking over a virtual table, joined so the lexical rank never gates recall. Also inapomesh-substrate-state. - Postgres — pgvector HNSW over a cosine index for approximate-nearest-
neighbor vector search, plus Postgres full-text
ts_rank_cdfor the lexical half. The result field is still named for BM25 for cross-backend uniformity, but the Postgres lexical algorithm ists_rank_cd, not literal BM25. This is the production path — seeapomesh-state-store-postgres.
The query surface is identical across all three; only the index and lexical algorithm differ. Consult the crate pages for the exact index and tuning.
Procedural memory and AGENT.md
Section titled “Procedural memory and AGENT.md”Procedural memory is authored, not learned. An AGENT.md document is parsed on
## headers into named sections, keeping the raw markdown verbatim alongside the
structured map, under a monotonically bumped revision. An agent changes it only
through propose-then-approve: the procedural_memory.propose tool submits a
diff and a reason, and the orchestrator turns that into a human-in-the-loop
pause. It emits a proposal pending event, installs a HITL waiter, and awaits an
operator decision — approve (write the diff), redirect (write the
operator’s edited version), or reject (record the reason, refuse the write).
The write re-parses the sections and bumps the revision; every outcome is a
durable event, so the change history is auditable. Memory never silently
rewrites a project’s operating rules.
Working memory and the context plane
Section titled “Working memory and the context plane”Working memory is what the agent holds; the context plane is what a single
model call sees. The two are distinct, and the seam between them is
compaction. As a dispatch grows, the message stream is trimmed to a token runway
before the context plane composes the actual prompt —
the planner’s history under an operator-selectable CompactionStrategy, the
worker’s conversation under a fixed sliding-window safety net. Compaction mutates
working memory; the context plane then compiles what survives into the call. The
memory plane owns the durable, recoverable stream; the context plane owns the
per-call composition. Keeping them separate is why a session can be replayed:
what the agent remembered and what any one call saw are recorded as distinct
events.
Guarantees
Section titled “Guarantees”- Tiers are scope-isolated. Semantic memory never crosses a tenant boundary; procedural memory never crosses a project; episodic memory never crosses its agent or sibling group. Isolation is enforced at the store boundary, not by convention.
- Working memory is recoverable. It is snapshotted at every node completion, so a restarted session rehydrates from the last checkpoint rather than losing its turn-by-turn state.
- Cross-run recall is inspectable. Confer and contribute emit events carrying the entries and scores involved, so what a run drew on and what it wrote back is auditable on the log.
- Procedural knowledge is never silently rewritten. Every change goes through propose → approve / redirect / reject, each a durable event.
- Consolidation is best-effort. A failed contribution never fails the session; the work already completed still lands.
- One model, every loop. The tiers and the store port are loop-agnostic — the same memory model serves every coordination strategy.
Where this shows up
Section titled “Where this shows up”- Operate: the operator console inspects all four memory kinds per agent — working, episodic, semantic, procedural — with each tier’s access mode and bound space. A procedural proposal surfaces there as an ordinary HITL pause, not a memory-specific queue.
- Build: an agent’s per-tier memory scope is part of its configuration — see the agent-configuration plane.
- Reference:
apomesh-substrate-typesowns the tier vocabulary and theStateStoreport;apomesh-substrate-stateandapomesh-state-store-postgresimplement it;apomesh-orchestratorowns snapshotting, confer/contribute, and the procedural sink.