hal0 can give your platform a persistent memory — a store of facts that both
the operator surfaces and the bundled agent can write to and recall from. It’s a
deliberate, gated subsystem behind a single config flag: powerful when it’s on, and
completely absent when it’s off. A fresh install turns it on for you; it stays
a conscious, revertible choice rather than something that silently accumulates
data on an existing box.
The memory subsystem is an engine-neutral layer behind two surfaces: the
/api/memory/* REST routes (which the dashboard’s Memory view uses) and the
/mcp/memory MCP server, which agents and external tools use. Both speak to
the same provider, so a fact written by an agent over MCP is the same fact
the dashboard shows.
The MCP surface covers the full read/write/curate/synthesize lifecycle, not
just add-and-search:
Core: memory_add, memory_search, memory_list, memory_delete,
memory_recall — the token-budgeted, observation-aware retrieval path
(preferred over memory_search), with native per-result relevance scores
and optional entity/chunk/source-fact enrichment.
Reflect: memory_reflect — ask a question and get an LLM-written
answer grounded in recalled facts, instead of a raw fact list.
Curation: memory_curate edits a fact or soft-invalidates/reverts it
(reversible) — the non-destructive “this is wrong” correction path, as
distinct from memory_delete. memory_history shows a fact’s revision
trail.
Mental models: memory_mental_model_list / _get / _create /
_update / _delete / _refresh — standing questions (“what does the
user prefer for X”) whose answer is kept refreshed from memory.
Async operations: memory_operation_list / _get / _cancel /
_retry — retain is async by default, so these are the poll target for
memory_add’s operation_id and for mental-model refreshes.
Bank introspection: memory_tags_list, memory_bank_stats,
memory_bank_consolidate — read-only counts plus an early-trigger for
consolidation. No destructive bank operations (delete/clear a whole bank)
are exposed over MCP; those stay operator-only under /api/memory/*.
Every write/destructive tool goes through the same namespace ACL and
approval-gate pattern as memory_add/memory_delete — memory_curate’s
state toggle is reversible so it stays autonomous, while
memory_mental_model_delete and memory_directive_delete gate for operator
approval the same way a bulk memory_delete does.
Memory view in the hal0 dashboard
The engine is Hindsight — a memory service that powers the shared operator
and agent “brain.” (Cognee, the prior engine, was fully removed; hal0 memory migrate remains only for boxes still carrying a Cognee-era store.) When
configured, hal0 builds a Hindsight client and wraps it with a reranker so
recall surfaces the most relevant facts. If the Hindsight daemon is
unavailable at boot, hal0 degrades to an in-memory PgVector fallback rather
than failing — the subsystem stays usable, but that fallback is volatile
(writes don’t survive a restart) and exists to keep the tools answering, not
as a durable substitute.
This is the most important thing to know: hal0 only constructs a memory
provider when [memory].enabled is set in hal0.toml. This replaces the
older HAL0_MEMORY_ENABLED environment variable — the toggle is now a
persisted config value, editable from the dashboard settings without an env
edit. When it’s off:
No memory provider is built.
Every downstream caller degrades to a no-op or a clean error — the
/api/memory/* routes, the /mcp/memory server, the agent’s memory provider,
and the per-agent memory stats all handle the absent provider gracefully.
The dashboard hides the Memory navigation.
Flipping the flag is the entire toggle — no code change required either way.
A fresh install turns memory on for you and stands up the local Hindsight
daemon automatically. An upgrade never rewrites an existing config, so a box
that was installed before memory shipped on by default, or that had it turned
off by hand, stays off until you set it yourself. Either way it’s a conscious,
revertible choice, not something that quietly starts accumulating data you
didn’t ask for.
Every memory operation is scoped to a namespace, and the grammar is a closed
set: shared, agents, project:<id>, or the caller’s own
private:<client_id>. A write with no explicit namespace defaults to
shared. Passing --private promotes the write to private:<client_id> and
wins over an explicit dataset field in the request body — so a
private-mode client can’t smuggle data into shared by naming it directly in
the payload. An unknown namespace on write raises an error; on read it
silently drops (fail-open-empty), so a typoed namespace can’t leak data but
also won’t crash a caller mid-conversation.
Callers identify themselves via the X-hal0-Agent header, and the same
identity resolves the same memory namespace whether it arrives over REST or
MCP.
When enabled, memory becomes the shared brain that ties the platform together:
Hermes's durable memory, by default
Provisioning a fresh bundled agent wires it straight to this subsystem
with no manual config: a private:hermes bank for its own facts and a
shared bank every agent on the host can read, both backed by Hindsight
via the hal0-api REST front door. Reads union both banks; recalled context
is injected automatically every turn, and the agent can also call the MCP
tools directly.
Operator memory
The dashboard Memory view and the /api/memory/* routes let you add,
search, browse the graph, and prune facts directly — across banks,
documents, mental models, and directives.
Namespaced writes
Callers identify themselves (via the X-hal0-Agent header), so private
writes land in the right per-agent namespace rather than a shared pool.
Destructive ops are audited
Bank deletes and every memories / config / document / directive /
operation / mental-model delete record a durable audit row — actor,
target, and outcome — so a wipe is attributable after the fact. Deleting
an entire bank additionally requires the caller to echo the bank id back
via ?confirm=<bank_id>.