Memory API
An agent has a memory of its own. Its genes use that memory to keep what they learn between runs: a user’s preferences, patterns that keep coming up, knowledge about a task, and results worth caching.
The memory belongs to the agent, not to any gene. When you replace or update a gene in the agent’s genome, its memory stays as it is.
Memory categories
Section titled “Memory categories”Every entry has one of four categories. The category decides how long it is likely to stay and what happens to it when the agent is moved.
| Category | What it holds | Cleared automatically when memory is full? | Kept when the agent moves to another binding? |
|---|---|---|---|
PREFERENCE | What the agent has learned about a user’s habits and preferences | No, never | Yes |
PATTERN | Patterns the agent has noticed across repeated runs | Yes, oldest first, after CACHE | Yes |
CONTEXT | Knowledge built up about a particular task or domain | Yes, oldest first, after PATTERN | Yes |
CACHE | Intermediate results that can be computed again | Yes, oldest first, before anything else | May be dropped |
Category names are uppercase everywhere: in TypeScript, in JSON, and in the WASM host calls.
Entries
Section titled “Entries”Each memory entry carries:
| Field | Meaning |
|---|---|
entryId | Stable id of the entry: the SHA-256 of the agent id and the key. Updating the entry keeps the same id. |
category | PREFERENCE, PATTERN, CONTEXT or CACHE |
key | The name genes use to read and write the entry |
value | The stored bytes. In JSON they are base64 encoded. |
createdBy | v2 id of the gene that created the entry (v2: followed by 64 hex digits; rotifer compile and rotifer wrap print it as Gene ID (v2)) |
lastAccessedBy | v2 id of the gene that last read or wrote it. Queries do not change it. On a new entry it is the same as createdBy. |
createdAt, updatedAt | When it was created and last changed (RFC 3339, UTC) |
accessPolicy | Who may read, write and share it (below) |
retentionPolicy | How long it is kept: exactly { maxRetentionDays, autoDeleteOnTermination, anonymizationSupported }, or null to follow the agent’s data policy |
Access scopes
Section titled “Access scopes”Every entry has an access policy with three parts: who may read it (readScope), who may change or delete it (writeScope), and whether it may leave the agent (sharePolicy).
readScope and writeScope each take one of these:
| Scope | Who has access |
|---|---|
CREATOR_GENE_ONLY | Only the gene that created the entry |
SAME_DOMAIN | Genes in the same functional domain as the creator |
ALL_GENES | Every gene in the agent |
DEPLOYER_CONFIGURED | Whatever the agent’s data policy says. The data policy has no setting for this yet, so in this release access is always denied, and a write that sets it is refused. |
sharePolicy takes one of these:
| Share policy | Meaning |
|---|---|
PRIVATE | Stays inside this agent |
ANONYMIZED_SHARE | May be shared once everything traceable to a user has been removed |
KNOWLEDGE_GENE | May be turned into a knowledge gene and shared that way |
If you write an entry without an access policy, it gets readScope: SAME_DOMAIN, writeScope: SAME_DOMAIN, sharePolicy: PRIVATE.
A write must also fit the agent’s data policy. The policy an agent gets when you configure none does not support anonymization, so writes with ANONYMIZED_SHARE or KNOWLEDGE_GENE are refused until the deployer configures one that does. A retentionPolicy that keeps an entry longer than the agent’s policy allows is refused too.
The check happens before every read, write and delete. A gene that is not allowed gets MEMORY_ACCESS_DENIED, and the attempt is recorded.
Quota and cleanup
Section titled “Quota and cleanup”Each agent has a memory quota. It counts the UTF-8 bytes of every key and value. The default is 10 MB; set a different one when you create the agent:
rotifer agent create my-agent --genes my-gene --memory-quota=50MB--memory-quota takes a number with KB, MB or GB, in either case (1 KB = 1024 bytes), for example 512KB or 1.5GB. A size without a unit, zero, a negative size or one that is too large is rejected, and the agent is not created.
When a write would go over the quota, the agent makes room in this order, starting with the entries updated longest ago (an entry rewritten recently counts as new):
CACHEPATTERNCONTEXT
PREFERENCE entries are never removed this way. If there is still not enough room after that, the write fails with MEMORY_QUOTA_EXCEEDED and nothing is written.
Size limits
Section titled “Size limits”- A single
valuecan be at most 1 MiB (1,048,576 bytes). - A query returns at most
limitentries (1000 at most) and at most 4 MiB in total. If the matching entries would be larger, the query fails withMEMORY_INVALID_INPUT: lowerlimitor narrow the filter. You never get a silently cut-off result.
How long entries are kept
Section titled “How long entries are kept”An entry’s maxRetentionDays is enforced. The entry expires at its updatedAt plus that many days, to the second, so rewriting an entry starts the count again. An entry whose own policy sets maxRetentionDays: null never expires. An entry with retentionPolicy: null follows the agent’s data policy instead; the default data policy sets no limit, so by default such an entry never expires either.
An expired entry is gone: a read returns null, a query leaves it out, it no longer counts toward the quota, and writing its key again creates a new entry. Expired entries are removed the next time the agent’s memory is read, queried, written or deleted from, or listed with rotifer agent memory list, and each removal adds a line to the audit log.
autoDeleteOnTermination is stored and checked, but in this release nothing acts on it: there is no command yet that terminates or deletes an agent.
Which genes can use memory
Section titled “Which genes can use memory”- TypeScript genes that run on Node.js (Wrapped or Hybrid, or not compiled) get the four calls as
ctx.memory, the second argument ofexpress(), as in the example below. This holds in every composition. When aPar,CondorTrycomposition has a gene that is not compiled,rotifer agent runruns all of its genes one after another, so each of them can read and write memory. - Genes that bring their own WASM (Rust, AssemblyScript, hand-written WAT) call the host functions described in From a WASM module.
rotifer agent runconnects them to their agent’s memory whether they run alone, in a sequence, or in aPar,CondorTrycomposition whose genes are all compiled. Each gene acts under its own v2 id; a gene that has none gets-2on every call. - TypeScript genes compiled to WASM cannot use memory. The compiled gene’s
express()is called with the input only, so there is noctx.memory. If a TypeScript gene needs memory, run it on Node.js.
A gene needs a v2 id to use memory; a gene without one has every call refused with MEMORY_ACCESS_DENIED.
Memory calls only work when the gene runs as part of an agent. Run a gene on its own with rotifer run, and every memory call fails with MEMORY_ACCESS_DENIED (-2 from WASM).
To try out a gene that uses memory, run rotifer test. It gives the gene an empty agent memory of its own for the length of the test and deletes it afterwards, so your agents’ memory is not touched. If the gene has a test suite, all its cases share that one memory, so a case can read what an earlier case wrote.
The four calls
Section titled “The four calls”readMemory(key: string, category?: MemoryCategory): MemoryEntry | null
writeMemory( key: string, value: Uint8Array, category: MemoryCategory, accessPolicy?: MemoryAccessPolicy, retentionPolicy?: RetentionPolicy | null,): MemoryEntry
queryMemory(filter?: MemoryFilter): MemoryEntry[]
deleteMemory(key: string): void
interface MemoryFilter { category?: MemoryCategory; keyPrefix?: string; // plain prefix, not a pattern since?: string; // RFC 3339, inclusive, compared with updatedAt until?: string; // RFC 3339, exclusive, compared with updatedAt limit?: number; // default 100, at most 1000}
interface RetentionPolicy { maxRetentionDays: number | null; // whole days, 1 to 2147483647; null for no limit autoDeleteOnTermination: boolean; anonymizationSupported: boolean;}The four calls are synchronous: each returns its result directly, so express() does not need to be async.
readMemoryreturns the entry forkey, ornullif there is none. Passcategoryto only match an entry of that category. If the entry exists but the gene may not read it, the call fails withMEMORY_ACCESS_DENIED; it does not pretend the entry is missing.writeMemorycreates the entry, or replaces its value if the key already exists, and returns the stored entry. Leave outaccessPolicyto get the defaults above, andretentionPolicyto follow the agent’s data policy. Replacing an existing entry needs that entry’s write scope, and keeps its original creator and access policy; only the gene that created an entry can change its access or retention policy. Any other gene that passes a policy different from the stored one getsMEMORY_ACCESS_DENIEDand nothing is written; leaving the policies out, or passing the same ones, is fine. PassingretentionPolicy: nullresets the entry to follow the agent’s data policy, which also only the creator can do.queryMemoryreturns the entries the gene is allowed to read that match all the filter fields you give, most recently updated first, up tolimit. With no filter it returns everything the gene may read, up tolimit. Entries the gene may not read are left out, not reported. A field the filter does not define is rejected withMEMORY_INVALID_INPUT.deleteMemoryremoves the entry. Only a gene allowed to write the entry may delete it.
Errors
Section titled “Errors”| Code | When |
|---|---|
MEMORY_NOT_FOUND | The entry does not exist (for example, deleting a key that was never written) |
MEMORY_ACCESS_DENIED | The entry’s access policy does not allow this gene to read, write or delete it; a gene other than the creator tried to change an entry’s policies; or the write does not fit the agent’s data policy |
MEMORY_QUOTA_EXCEEDED | The write does not fit even after cleanup; nothing was written |
MEMORY_INVALID_INPUT | A bad key (empty, containing a NUL character, or longer than 1024 bytes), an unknown category or scope, a value that is not bytes or is larger than 1 MiB, a retention policy that is not exactly those three fields with the right types (and not null) or whose maxRetentionDays is not a whole number from 1 to 2147483647, an unknown filter field, or a query result larger than 4 MiB |
MEMORY_STORAGE_ERROR | The memory store failed, or the CLI’s native binding has no memory support (“native binding unavailable”). Memory never falls back to another store. |
In TypeScript, a failed call throws a MemoryError whose code is one of these. Inside the WASM sandbox, they come back through the same error channel that other host calls use.
From a WASM module
Section titled “From a WASM module”A gene with its own WASM imports readMemory, writeMemory, queryMemory and deleteMemory from the rotifer module. Each has the same shape as the other rotifer host functions:
(req_ptr: i32, req_len: i32, out_ptr: i32, out_cap: i32) -> i32The request and the response are JSON, with the field names and uppercase values shown on this page; value is base64. A result of 0 or more is the number of response bytes written to out. A negative result is an error:
| Result | Meaning |
|---|---|
-1 | out is too small for the response |
-2 | MEMORY_ACCESS_DENIED |
-8 | MEMORY_QUOTA_EXCEEDED |
-10 | MEMORY_INVALID_INPUT |
-12 | MEMORY_NOT_FOUND |
-13 | MEMORY_STORAGE_ERROR |
-4 | The run’s deadline or the call’s budget is used up |
Except for -4, when there is room, out also carries {"__rotifer_error":{"code":"MEMORY_…","message":"…"}}. A gene that returns that object as its own output reports the input as rejected. readMemory on a missing key returns JSON null, not an error.
Example: a gene that remembers a preference
Section titled “Example: a gene that remembers a preference”This formatter remembers the indent width a user asked for last time and uses it when the next request does not say.
import type { GeneMemoryAccess } from "@rotifer/playground/memory";
// reindent() is the gene's own formatting logic, not shown here.const KEY = "formatter/indent";
export function express( input: { code: string; indent?: number }, ctx: { memory: GeneMemoryAccess },): { formatted: string; indent: number } { let indent = input.indent;
if (indent === undefined) { const saved = ctx.memory.readMemory(KEY, "PREFERENCE"); indent = saved ? Number(new TextDecoder().decode(saved.value)) : 2; } else { ctx.memory.writeMemory( KEY, new TextEncoder().encode(String(indent)), "PREFERENCE", ); }
return { formatted: reindent(input.code, indent), indent };}The entry is written with the default access policy, so other formatting genes in the same domain can read the preference too. If you swap this gene for a better formatter in the same domain, the new one finds the saved indent where the old one left it.
What gets recorded
Section titled “What gets recorded”Every change to memory adds one line to the agent’s audit log at .rotifer/audit.jsonl: writes, updates, deletes, entries removed to make room, entries that expired, and writes refused for lack of space. The line is written before the change is saved; if it cannot be written, the change is not made. Plain reads are not logged. A refused access also adds a line there, and an event to .rotifer/telemetry.jsonl unless you set ROTIFER_TELEMETRY=0 or DO_NOT_TRACK; the audit line is written either way. Both files stay on your machine; nothing is sent over the network.
Untrusted input and memory poisoning
Section titled “Untrusted input and memory poisoning”If a gene writes content from outside the agent into memory (messages, issues, web pages, documents), whoever wrote that content can influence what is stored. With a language model in the loop, prompt injection can make a gene store entries an attacker chose, and later runs read them like anything else the agent learned.
PREFERENCE entries are never cleared to make room. A poisoned one stays until it is deleted, by the deployer with rotifer agent memory delete or by a gene allowed to change it, or until its maxRetentionDays runs out.
- Write
PREFERENCEentries only from trusted sources. - Put untrusted input in
CACHE, or give it amaxRetentionDaysso it expires.
Things to know before you deploy
Section titled “Things to know before you deploy”- Upgrading a gene changes its id. Entries the old version wrote with
CREATOR_GENE_ONLYcannot be read, changed or deleted by the new version. Keep data that should outlive a version atSAME_DOMAIN, the default; clear what is left behind withrotifer agent memory delete(below). PREFERENCEentries are never cleared for space. A gene that keeps writing them can fill the quota, after which writes fail withMEMORY_QUOTA_EXCEEDED. Give the agent a quota that fits, and delete preferences you no longer need withrotifer agent memory delete.- Cleanup does not look at who wrote an entry. When one gene’s write needs room, the oldest
CACHE,PATTERNandCONTEXTentries go first, whichever gene created them.
Viewing and clearing an agent’s memory
Section titled “Viewing and clearing an agent’s memory”rotifer agent memory list <agent> [--category <category>]rotifer agent memory delete <agent> (--key <key> | --category <category>)listshows each entry’s key, category, size, creator and last update. It does not print values.deleteremoves one entry by key, or every entry of a category. Give exactly one of--keyor--category; it prints how many entries it removed. Deleting a key that does not exist fails withMEMORY_NOT_FOUND. You own the agent, so it works whatever the entries’ access policies say; that is also how you clear entries a gene can no longer reach after an upgrade. Each deletion goes into the audit log as done by the deployer.- There is no command to edit an entry, and no cloud equivalent yet.
Moving an agent
Section titled “Moving an agent”When an agent moves to another binding, its PREFERENCE, PATTERN and CONTEXT entries go with it, with their access policies, retention policies and createdBy unchanged. CACHE entries may be left behind and rebuilt.
That is how moving works by the protocol. In this release there is no command that moves an agent or its memory to another binding, and memory cannot be uploaded to Rotifer Cloud.