Appearance
Agent Memory
Uranus uses a four-tier memory model that combines transient working memory, episodic message history, semantic recall over a vector index, and reflective learnings produced by self-review.
Why four tiers
A single flat memory blob can't simultaneously:
- Hold the current turn's tool results without bloating long-term storage
- Replay recent messages efficiently
- Answer "do I know anything relevant to this user message" via similarity
- Carry forward distilled lessons across context-window compactions
Each tier solves one of those.
| Tier | What it is | Where it lives | Lifetime |
|---|---|---|---|
| L1 Working | Current turn's tool results, turn checkpoints, per-conversation notes | Durable Object RAM + DO storage (turn:*) | one turn / one conversation |
| L2 Episodic | Message-level history + range-bound compaction overlays | chat_messages + conversation_overlays (+ legacy conversation_summary:*) | weeks–months |
| L3 Semantic | Extracted facts with embeddings and a keyword index | agent_state (memory:*) + Vectorize + memory_fts + memory_embeddings registry | indefinite, with consolidation |
| L4 Reflective | Reflections, goals, learnings | agent_reflections + agent_goals + memory:reflection:* keys | indefinite |
L3 — Semantic memory
The most consequential tier. Two write paths and two recall paths.
Write paths
Per-turn extraction. After every chat turn, extractAndPersistMemories runs via ctx.waitUntil (DO-eviction-safe). It asks an LLM to lift durable facts from the exchange (preferences, identities, recurring patterns) and writes them to agent_state keys:
memory:{key}— agent-widememory:contact:{contact_id}:{key}— scoped to one contact
Each write also embeds the value into Vectorize via embedAndUpsertMemory and records the linkage in memory_embeddings.
Pre-compaction flush. When the chat loop's paginated history fetch hits the token budget, summarizeAndStoreHistory runs in parallel with the summary generation. It extracts up to 10 durable facts from the about-to-be-trimmed window. Same write path, but tagged source='pre-compaction'.
Reflection. Reflective memories from self-review write to memory:reflection:{date}:{topic} with confidence=0.85. These outrank per-turn facts (0.5) and pre-compaction facts (0.6) in recall.
Memory kinds
Every memory carries a fact type (preference, identity, event, instruction, task, workflow-outcome, …) that kindOf() in memory.ts folds into one of four kinds — the same four Cloudflare Agent Memory uses:
| Kind | Fact types | Indexed how |
|---|---|---|
fact | preference, identity, relationship, decision, reflection (default) | vector + keyword |
event | event, workflow-outcome, workflow-failure | vector + keyword |
instruction | instruction, skill | vector + keyword |
task | task | keyword only — never vectorised |
Task memories are deliberately kept out of the vector index: a stale "I was booking a table" must not surface semantically weeks later.
Write-time verification
Before a new memory is stored, verifyAndSupersede (on by default; settings.memory.verify = false disables it) finds up to three existing memories with cosine ≥ 0.6 and asks the Jev decision model two yes/no questions per pair: does the new memory replace the existing one? and do they contradict? A replaces probability ≥ 0.75 marks the older memory superseded_by the new key, so recall stops returning it; contradictions that are not replacements are only logged. Task memories skip the verifier. See Typed Decisions for the model and the data-flow policy that gates it.
Recall paths
fusedRecallMemories(query, topK) runs three channels in parallel and merges them with reciprocal rank fusion (score += weight / (60 + rank)):
| Channel | Source | Weight |
|---|---|---|
| Vector | semanticRecallMemories — embeds the message, queries Vectorize filtered to { type: "memory", agentId } | 1 |
| Keyword | memory_fts (FTS5, porter + unicode61), BM25-ordered, top 15 | 1 |
| Exact key | agent_state keys whose segment equals a query token | 1.5 |
Superseded memories are dropped from every channel. The caller resolves the fused keys back to values from agent_state. A SQL relevance pass (0.7 × recency_decay + 0.3 × log(access_count + 1), 30-day decay) still fills the remainder of the prompt budget up to 20 memories.
Memory providers
getMemoryProvider(env, settings) picks the backend behind remember / recall and the pre-compaction flush:
settings.memory.provider | Implementation |
|---|---|
local (default) | LocalMemoryProvider — agent_state + Vectorize + memory_fts, with the Jev verifier |
cloudflare | CloudflareMemoryProvider — the Cloudflare Agent Memory binding (AGENT_MEMORY, private beta), one profile per agent. Falls back to local when the binding is absent. |
The Cloudflare provider is a partner pipeline, so it is only used where the data-flow policy permits partner processing. Settings → Runtime in the dashboard switches the backend and the verifier.
Contact scoping
Memory is contact-scoped by default. The same person reaching the agent via Telegram on Monday and email on Tuesday hits the same memory bucket — memory:contact:{contact_id}:* keys are loaded for any conversation linked to that contact.
Agent-wide memories (memory:* without the contact: infix) exist for facts not tied to a specific person — e.g. business hours, operating preferences, identity of the agent itself.
Embedding details
| Setting | Value |
|---|---|
| Model | @cf/baai/bge-base-en-v1.5 |
| Vector index | shared agents-dashboard-embeddings (filtered by metadata.type = "memory") |
| Vector ID format | mem:{agentId}:{memory_key} |
| Confidence floor | 0.1 (after decay), default 0.5 |
| Re-embed trigger | every write; idempotent on identical content |
L4 — Reflective memory
Reflections write a row to agent_reflections and additionally produce reflective_memories — short distilled facts the agent wants to carry forward. These flow through the same L3 path but at higher confidence.
See Goals & Reflection for the full pipeline.
Consolidation
A daily cron at 04:00 UTC (one hour after reflection) runs consolidateOnePass:
- Decay. Memories not accessed in 30+ days have their confidence multiplied by 0.8 (floor 0.1).
- Sample. Up to 30 most-recent active memories.
- Cluster. For each, query Vectorize at cosine ≥ 0.85 to find near-duplicates.
- Merge. Clusters of size ≥ 2 go to glm-4.7-flash with a "merge these into one canonical statement" prompt.
- Supersede. Members are marked
superseded_bythe new canonical key. Old rows are kept for audit; they're filtered out of recall paths. - Cap. At most 5 clusters merged per pass to keep the cron cheap.
Result: 10 turns about "user lives in Singapore" become one memory instead of ten.
Tools
| Tool | Effect |
|---|---|
remember(key, value, category?) | Explicit write. Contact-scoped if a contact is in the conversation context; agent-wide otherwise. Confidence 0.9 (user-explicit). |
recall(key?) | Read by key (or list all). Tries contact-scoped first, then agent-wide. Bumps access_count. |
recall_conversation(query, mode?, limit?, role?, scope?) | Semantic or keyword search over chat_messages. Scope: contact (default, all gateways for that contact), conversation, or all. |
set_context(notes, mode?) | Per-conversation working notes (replace / append / clear), shown back to the model at the start of every turn. Not a memory — see Chat. |
set_ongoing_task / get_ongoing_task / clear_ongoing_task | Multi-turn task tracking, scoped to the conversation. See Tasks. |
L2 — Episodic memory
Plain chat_messages rows. Messages are never deleted or rewritten; what changes is how much of them the model sees.
Compaction overlays
compaction.ts splits the history token budget three ways:
| Share | Content |
|---|---|
| 70 % | Tail — the most recent messages, verbatim |
| 10 % | Head — the conversation's first two messages, verbatim (a row that does not fit is cut, not dropped) |
| 20 % | Overlays — summaries bound to a message range (conversation_overlays: from_created_at … to_created_at, message_count, summary, tokens), rendered in order between head and tail |
When a stretch between head and tail is not yet covered by an overlay, the oldest uncovered range (3–60 messages) is summarised in the background and stored as a new overlay. The pre-compaction memory flush (above) runs on the same event. The older single-blob summary (conversation_summary:{id}, archived to conversation_summary_history:{id}:{timestamp} on each rewrite) is kept as a fallback.
History is still fetched in pages of 10 rows, newest first, stopping when estimateTokens exceeds the budget (hard cap 200 rows scanned).
L1 — Working memory
In-DO RAM plus two small persisted helpers:
- Turn checkpoints (
turn-checkpoint.ts). After every tool round, both chat loops write a trimmed record of the turn — tool calls, tool-result previews (3 000 chars each, at most 40 rows), browser session ids — to the Durable Object's own storage underturn:{turnId}. It is deleted when the assistant message is saved. If the object is evicted or a deploy lands mid-turn, the next request in that conversation finds a turn untouched for more than 45 s and prepends its completed results to the model input instead of re-running the tools (and tells the model to reuse the open browser sessions). Checkpoints expire after 24 h. - Working notes (
set_context). Per-conversation scratchpad text the model keeps for itself; see Chat.
Everything else — tool results in flight, transient state — is lost on DO eviction by design.
Token estimation
estimateTokens is content-aware (see index.ts for the heuristic):
| Content type | Detection | Ratio |
|---|---|---|
| CJK-heavy | >30% Han/Hiragana/Katakana/Hangul chars | 1.5 chars/token |
| JSON / code-dense | >8% { } [ ] : " chars | 3.0 chars/token |
| English prose (default) | everything else | 3.5 chars/token |
This drives both history trimming (so the budget is realistic) and trackTokenUsage (so billing matches the provider invoice).
Schema
See migrations:
060_memory_embeddings.sql— registry table bridging keys to Vectorize IDs075_conversation_overlays.sql— range-bound compaction summaries076_memory_fts.sql— FTS5 keyword index overmemory:*values (backfilled on apply)
Related
- Goals & Reflection — produces reflective memories
- Tasks — the task kind, kept out of the vector index
- Typed Decisions — the Jev verifier
- Vector Search — the underlying Vectorize binding
- Scheduling — the consolidation cron
