Skip to content

Memory

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.
  • Directives: memory_directive_list / _get / _create / _update / _delete — standing instructions injected into prompts.
  • 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_deletememory_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 interface showing stored facts and recall 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.

Memory graph visualization showing relationships between facts Fact relationships in the knowledge graph

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>.