memory

Durable memory across conversations: who the user is, what you made for them, and what worked.

Source: packages/oracle-runtime-workers/src/plugins/memory/

AttributeValue
Visibilityalways
Stabilitystable
Categorymemory
Default stateAuto-detect (env: MEMORY_MCP_URL)
Depends on—

Summary

Durable memory across conversations — who the user is, what you have made for them, and what worked. Surfaces the upstream Memory Engine MCP tools verbatim (memory-engine__search_memory_engine, memory-engine__add_memory, …). Tool list is resolved per-request because MCP headers depend on the in-flight user's UCAN delegation.

Environment variables

VarRequiredDescription
MEMORY_MCP_URLyesMemory MCP HTTP(S) URL. Also triggers auto-detect.
MEMORY_ENGINE_URLyesMemory Engine HTTP(S) URL.
MEMORY_DOCUMENT_CHUNK_BYTESnoLargest part of an oracle document in UTF-8 bytes. Default 12000, 1024 to 16384.
MEMORY_DOCUMENT_MAX_CHUNKSnoMost parts one add_oracle_document call writes. Default 64, 1 to 64.
MEMORY_DOCUMENT_MAX_CHARSnoMost characters read from one file. Default MEMORY_DOCUMENT_CHUNK_BYTES × MEMORY_DOCUMENT_MAX_CHUNKS (768,000). A longer file is refused unless the call passes allow_truncation.
ORACLE_ENTITY_DIDyes (@ixo/oracle-runtime-workers 0.24.0 and later)The oracle's domain entity, did:ixo:entity: followed by 32 lower-case hex characters. Sent as x-oracle-entity-did on every Memory Engine request; every memory space is keyed by it. Without a well-formed value the plugin fails at boot. See Domain entity and runtime compatibility.

Domain entity and runtime compatibility

The Memory Engine keys memory by the oracle's domain entity (ORACLE_ENTITY_DID), not by the oracle's agent account (ORACLE_DID). A user's personal memory belongs to the pair (user, entity); the shared knowledge spaces (public and private) belong to the entity. Several oracles serving one entity therefore share its knowledge, and rotating an oracle's agent key keeps its users' memory.

Every request names the entity in the x-oracle-entity-did header, and the entity must vouch for the oracle: its on-chain document lists the oracle's agent account (ORACLE_DID) as a linkedEntity with service: matrix and relationship: admin. qiforge-cli create-entity writes that link on the entity it creates; an entity created another way needs one of its controllers to add it.

AnswerCodeMeaning
400ENTITY_HEADER_MISSINGNo x-oracle-entity-did header: the runtime predates 0.24.0.
400ENTITY_HEADER_INVALIDThe header is not did:ixo:entity: followed by 32 lower-case hex characters.
403ENTITY_NOT_VOUCHEDThe entity does not list the oracle's agent account as a matrix / admin linked entity. Add the link.
403ENTITY_UNVERIFIEDThe chain could not be read and nothing usable was cached (retryable: true); retry shortly.
404ENTITY_NOT_FOUNDOnly for a user who signs the request alone, with no oracle: the entity is not on chain.
503SPACE_NOT_MIGRATEDThe engine still holds this user's memory under its old, agent-keyed id; the operator has to run the engine's re-keying migration.

A user can also reach their own memory with an entity directly, without any oracle, by signing the UCAN invocation alone; the entity must then exist on chain, and the user can do exactly what an oracle acting for them could.

What it contributes

  • Tools (default selection):
    • memory-engine__search_memory_engine — recall stored facts.
    • memory-engine__add_memory — write a new memory.
    • memory-engine__add_oracle_knowledge — write knowledge shared with every user of the oracle (@ixo/oracle-runtime-workers 0.21.0 and later; the Memory Engine refuses the call unless the user is a domain member of the oracle entity, see Adding global oracle knowledge).
    • memory-engine__delete_episode — delete a memory.
    • memory-engine__clear — clear (destructive; main-agent only).
  • Native tools (@ixo/oracle-runtime-workers 0.26.0 and later, listed only next to the tools above; see Oracle documents):
    • add_oracle_document — store a chat attachment or a Library file as domain knowledge, grouped as one document or as loose facts.
    • list_oracle_documents — list the grouped documents of a knowledge space with their status.
    • delete_oracle_document — delete a grouped document and everything it stored.
  • Sub-agents: none.
  • Middleware: none directly contributed by the plugin (enrichment of state.userContext is handled upstream of the agent).
  • HTTP routes: none.
  • Shared state: userProfile — other plugins read it via rtCtx.shared.userProfile.

Tools available beyond the default selection (e.g. memory-engine__delete_edge) can be enabled by passing selectedTools to new MemoryPlugin({ selectedTools: [...] }); the same option, passed without memory-engine__add_oracle_knowledge, hides the oracle-knowledge write from the agent.

On the Cloudflare Workers runtime, the manifest requires memory/* on ixo:memory. A user whose delegation doesn't grant it can't load the plugin or run its tools, and the agent is told what's missing.

From @ixo/oracle-runtime-workers 0.39.0 the plugin stays always, but only the tools of ordinary conversation are bound on every turn: memory-engine__search_memory_engine, memory-engine__add_memory and list_oracle_documents. The curating and deleting tools (memory-engine__add_oracle_knowledge, memory-engine__delete_episode, memory-engine__delete_edge, memory-engine__clear, add_oracle_document, delete_oracle_document) are in onDemandTools: the model loads them with load_capability(['memory']), from the catalogue line "add or delete oracle documents, knowledge; delete memories". The manifest also sets routable: false, so the capability router never preloads them. To bind them all again:

manifestOverrides: { memory: { onDemandTools: [] } },

Search results and deleting a memory

From Memory Engine 2.1.0, memory-engine__search_memory_engine says which stored episodes are behind what it found:

  • facts[].episodes lists the UUIDs of the episodes behind each fact, newest first, at most 20. Episodes written together in one burst are attributed together, so a fact can name several episodes and not every one of them is about it. The list is empty when the fact's source is not recorded.
  • episodes[] lists matching episodes with uuid, name, content (cut at 600 characters), created_at and knowledge_scope. The tool's max_episodes input sets how many (0 to 20, default 3).

The surest handle for memory-engine__delete_episode is episodes[].uuid, because it comes with the episode's text. A delete answers as soon as the episode itself is gone. Repairing the memories that shared an entity with it, and re-ingesting the other messages of its burst, run in the background once the deletes have been quiet for 5 seconds (at most 60 seconds after the first of a run). Until then, facts from those neighbouring episodes can be missing from search for seconds to minutes.

Oracle documents

From @ixo/oracle-runtime-workers 0.26.0, with Memory Engine 2.3.0 or later (the /knowledge/documents routes; live on devnet, testnet and mainnet), a domain member can hand the oracle a file and ask it to remember it as domain knowledge. The file is a chat attachment (a PDF, a text or Markdown file, a scan or an image) or a file from the user's Library. The oracle reads it (a PDF with a text layer is read page by page in the oracle; a scanned file or an image goes to a helper model, up to 10 MB), cuts it into parts and sends them to the engine in one call.

add_oracle_document takes group_as_document, with no default:

  • true keeps the file as one document under a document_id: a board resolution, a contract, a report. list_oracle_documents shows it with its status (ingesting, ready, partial, deleting, delete_failed). Adding the same document_id again replaces it, and delete_oracle_document removes everything it stored. A replace or a delete is answered at once and finishes in the background.
  • false stores each part as loose knowledge, for a file where someone collected many unrelated facts. The parts are found by search like any other knowledge and removed one at a time with memory-engine__delete_episode.

The agent is told to ask the person when it cannot tell which one a file is, and to confirm the public or private space before writing. The engine applies the same domain-member rule as for memory-engine__add_oracle_knowledge. A file attached in the chat always goes to add_oracle_document, never to memory-engine__add_oracle_knowledge. Limits: about 750 KB of text per document with the defaults, text files and text PDFs up to 25 MB, scanned files and images up to 10 MB. A PDF that mixes text pages and scanned pages keeps only its text pages. Pass new MemoryPlugin({ documentTools: false }) to leave the three tools out.

How memory reaches the prompt

Memory context does not arrive in the prompt because the agent calls a tool to fetch it. Instead, a UserContextFetcher runs before the main agent is compiled — eagerly loading all six context slots in a single pass, before any message is processed.

What the fetcher loads: six slots, in this order:

  1. identity — who the user is (name, role, background)
  2. work — ongoing projects and responsibilities
  3. goals — stated objectives and priorities
  4. interests — topics and domains the user cares about
  5. relationships — people and organizations mentioned in past conversations
  6. recent — notable things from the last few sessions

When it runs: at agent-compile time, before turn 1. The fetcher runs once per session and caches the result for 5 minutes (keyed by sessionId). Subsequent turns within the same session reuse the cached context — the Memory Engine is not called again unless the cache expires.

What appears in the prompt: if at least one slot is non-empty, the runtime inserts a ## What you know about the user block into the system prompt containing all populated slots. If every slot is empty (no prior memory for this user), the block is omitted entirely.

Implication for oracle authors: you do not need to instruct the agent to "look up the user's context" or "recall memory before responding" in config.prompt.opening or anywhere else. The context is already in the system prompt when the agent sees the user's first message. Adding such instructions is redundant and wastes tokens.

Adding global oracle knowledge

Global knowledge is content the oracle should know on every turn, for every user — product docs, brand voice, FAQs, reference material. It's stored on the Memory Engine and indexed under the oracle's entity DID, not the user's DID.

Confirm you are a domain member of the oracle entity

The Memory Engine decides on chain who may write global knowledge, with the rule the Matrix rooms appservice applies to a domain's rooms. From Memory Engine 2.0.0 it reads the oracle's domain entity itself, the one in ORACLE_ENTITY_DID (created via qiforge-cli create-entity). A domain member of an oracle is any of these on that entity:

  • a DID in the entity's controller list, or its owner;
  • the id of a linkedEntity entry with service: matrix and relationship: admin (a support relationship does not count);
  • a member with positive voting power of any DAO group (did:ixo:wasm:<address>) that appears among those controllers or admin links.

Legacy did:x:zQ3… key DIDs are recognised as the did:ixo:<address> account they belong to. Everyone else is refused. The engine caches the member set for five minutes, so a newly added controller, admin or DAO member is recognised within that time. Memory Engine 1.5.x applied the same rule to every entity whose linkedEntity list contained the oracle DID; on 1.4.x and earlier only controllers and the owner pass.

Check the entity with qiforge-cli update-entity or by inspecting it on chain. If you are not a domain member, ask a controller of the entity to add your DID (as a controller or as a matrix / admin linked entity) first.

Check the runtime version

On @ixo/oracle-runtime-workers 0.21.0 and later, memory-engine__add_oracle_knowledge is part of the memory plugin's default tool selection: there is nothing to enable, every oracle built on the runtime has it, and the Memory Engine decides per call whether the user may write.

On an older runtime (or on the deprecated Node runtime, which never added it to the defaults), instantiate MemoryPlugin explicitly and extend the default selection:

// src/index.ts
import {
  createOracleWorker,
  BUNDLED_WORKERS_PLUGINS,
  MemoryPlugin,
  DEFAULT_MEMORY_TOOLS,
  MEMORY_ADD_ORACLE_KNOWLEDGE_MCP_NAME,
} from '@ixo/oracle-runtime-workers';

export default createOracleWorker({
  config,
  plugins: [
    ...BUNDLED_WORKERS_PLUGINS.filter((plugin) => plugin.name !== 'memory'),
    new MemoryPlugin({
      selectedTools: [
        ...DEFAULT_MEMORY_TOOLS,
        MEMORY_ADD_ORACLE_KNOWLEDGE_MCP_NAME,
      ],
    }),
  ],
});

The loader keeps one plugin per name and the last one listed wins, so the bundled memory plugin is filtered out (or listed before the custom instance); listing the custom instance first would silently hand the default back.

Boot the oracle

pnpm dev

The boot log should list memory as loaded. The agent now lists memory-engine__add_oracle_knowledge among its tools.

Open the Portal and connect to your oracle

Pick the environment matching the network your oracle is registered on:

NetworkPortal URL
devnethttps://dev.portal.qi.space
testnethttps://test.portal.qi.space
mainnethttps://portal.qi.space

Navigate directly to your oracle's connect page (replace <ENTITY_DID> with the value of ORACLE_ENTITY_DID from your .env):

https://dev.portal.qi.space/domain/<ENTITY_DID>/connect

Sign in as the entity owner/controller. On the connect page, click the highlighted "Connect" action — the Portal then opens a chat session bound to the oracle.

Drop in content and ask the oracle to save it as global knowledge

In the chat, paste raw text, or drag and drop files (PDFs, markdown, text) — anything you want the oracle to know going forward. Then tell the oracle in plain language:

Save this into the global oracle knowledge.

The agent first confirms the scope with you — public knowledge is read by every user of the oracle, private knowledge only by the oracle's domain members — and then calls memory-engine__add_oracle_knowledge with typed text, or add_oracle_document with a file (0.26.0 and later; it also asks whether the file is one document or a set of loose facts, see Oracle documents). The Memory Engine accepts the write because your delegation chain proves which account you are and that account is a domain member of the oracle entity; any other user who asks for the same thing is refused with a Forbidden: … reply naming the rule.

Wait ~5 minutes for indexing

Newly written knowledge takes about five minutes to index before it appears in memory-engine__search_memory_engine results. After indexing, every user's session will be able to recall it through the normal memory search path.

Opt out / Opt in

const app = await createOracleApp({
  config,
  features: { memory: false },   // never load
  // features: { memory: true },  // force load (will fail env validation if vars missing)
  // features: { memory: 'auto' }, // run autoDetect (default)
});

When to use it

  • First contact (no prior context loaded): greet, ask the user's name and what they need help with, save the answer.
  • You learn something durable about the user — name, role, ongoing project, a constraint, a relationship.
  • You produce an artifact (file, document, edit, generated content) — record what it is, what it is for, the structural choices.
  • The user expresses satisfaction or dissatisfaction with something you produced — capture what worked or did not.
  • The user references something they told you before, or something you made before.
  • A controller or owner of the oracle types something every user of the oracle should know (a policy, a procedure, reference material): tell them it will be shared with every user of the oracle (or, for the private space, with the domain's members only), get their confirmation, then store it as oracle knowledge rather than as a memory about them.
  • A controller or owner hands you a file (an attachment or a Library file) to store as oracle knowledge: use add_oracle_document, grouped for one coherent record, loose for a file of unrelated facts; ask when it is not obvious, and confirm the space first.

When NOT to use it

  • Ephemeral conversation-only state — use the current message thread.
  • Behavioural preferences about how to respond — use user-preferences.
  • Public web facts not specific to this user — use firecrawl.
  • Anything the user asked you to forget or framed as temporary.
  • Oracle knowledge for something about one user: the shared knowledge is only for what every user of this oracle should know.
  • Storing a file the person did not ask you to store, or choosing between a grouped document and loose facts without asking when the file could be either.