ScallopBot memory architecture: sleep consolidation and self-reflection
ScallopBot keeps memory in one SQLite file and maintains it on three clocks. Every minute it decays. Every 72 minutes it summarises and forgets. Once a night, in the small hours, it consolidates related memories, looks for links it missed, and reflects on the day. This page describes what the code does, with the numbers it ships with, and how that differs from Letta and Mem0.
One file, three kinds of state
Everything lives in memories.db. That makes a backup one file copy, and it lets other processes (skills, the MCP server) open the same store in WAL mode.
- Memories: a fact with a category (preference, fact, event, relationship, insight), a type (static or dynamic profile, regular, derived, superseded), importance, confidence, an event date, an embedding and a prominence score between 0 and 1.
- Relations: a graph between memories.
UPDATESmeans a newer fact replaced an older one,EXTENDSmeans it adds to it,DERIVESmeans a summary was built from it. - Sessions: raw transcripts and their summaries. A transcript is only pruned 30 days after its session is archived, and only if a verified summary exists.
How a fact gets in
After each user message, an LLM fact extractor runs in the background so it never delays the reply. It pulls out durable facts and ignores chatter. Each candidate is embedded and checked against existing memories: at cosine similarity 0.93 or higher on the same event day it counts as the user repeating themselves, and the existing memory is reinforced instead of duplicated.
New facts are then classified against related ones as NEW, UPDATES or EXTENDS. An update only replaces the old fact if the classifier is at least 0.8 confident, the subject and category match, and a deterministic check agrees the two actually contradict. If any of that fails, both facts are kept. A wrong overwrite loses information for good. A kept duplicate costs a little retrieval noise, and the night shift can merge it later.
Three clocks
A background gardener runs on a one-minute timer. The deep and sleep ticks are time-based and their last-run timestamps are stored in SQLite, so a restart neither skips nor repeats them. All intervals can be overridden with environment variables.
Source: memory/memory.ts, config/config.ts (GARDENER_LIGHT_INTERVAL_MS, GARDENER_DEEP_INTERVAL_MS, GARDENER_SLEEP_INTERVAL_MS, GARDENER_QUIET_HOURS_START/END).
Sleep-style consolidation
The sleep tick waits for two conditions: at least 20 hours since the last one, and the user’s local time between 2 and 5 AM. The 20-hour interval gives slack so it still lands in the window every night. Then it runs a dream cycle in two phases, in the same order sleep does: consolidation first, loose association second.
Figure 1. The nightly dream cycle. Solid boxes with a bright border are where the database is written.
NREM: fuse what belongs together
Candidates are the latest memories with prominence from 0.05 up to 0.8. That covers fading memories and fresh ones, but leaves out the strongest, which need no help. Clusters are found by walking the relation graph. If no edges connect anything, the fallback groups memories by embedding similarity (cosine 0.6 or higher). A cluster needs at least three members, may span categories, and at most ten are processed a night. A source set that was fused on an earlier night is skipped.
Each cluster goes to the LLM together with the relations between its members, so the model knows why the memories connect, not only that they are similar. The summary is rejected unless it is shorter than the text it replaces. It is stored as a derived memory with a DERIVES edge to each source. It takes the highest importance and the lowest confidence of its sources, and starts at a prominence capped at 0.6. The sources are kept. Consolidation adds a summary, it does not delete evidence.
REM: look for links nobody asked for
REM picks six seed memories, weighted by importance × prominence with Gaussian noise, and no more than two per category so one topic cannot dominate. From each seed it runs spreading activation over the relation graph with deliberately high noise (σ 0.6, up to four hops) and filters out pairs that are already linked. Up to eight candidates per seed go to an LLM judge, which scores novelty, plausibility and usefulness from 1 to 5. A pair averaging 3 or more gets an EXTENDS edge. REM writes no new memory text, only edges. Those edges are what spreading activation follows at recall time, and what the next night’s NREM pass clusters on.
Source: gardener-sleep-steps.ts, dream.ts, nrem-consolidation.ts, fusion.ts, rem-exploration.ts.
Decay and forgetting, between the nights
Prominence is recomputed from age (weight 0.55), how often and how recently the user confirmed the fact (0.20 and 0.10), importance (0.10) and confidence (0.05). A memory is exempt for its first day. Decay rates differ by category: relationships fade slowest (about a 346-day half-life), then preferences (about 138 days), facts (about 69), insights (about 23) and events (about 14). Static profile facts never decay. Above 0.5 a memory is active, between 0.1 and 0.5 it is dormant, and below 0.1 it is archived.
Every deep tick, active memories that have never been retrieved, or not for a long time, lose 5% of their prominence. Memories older than 14 days whose utility (prominence × log(2 + confirmations)) falls under 0.1 are archived, at most 50 per run. Only superseded memories that have faded below 0.01 are deleted.
Self-reflection
ScallopBot reflects in two places, and they are kept apart on purpose. The first learns from the day and writes it down. The second can change what the assistant does, so it has to prove the change before it goes live and can be rolled back.
Figure 2. Loop A runs every night. Loop B only runs with EVOLUTION_ENABLED=true.
Loop A: reflect on the day
On the deep tick, sessions idle for two hours or more are summarised (up to 20 per tick). On the sleep tick, the summaries written in the last 24 hours go through one composite reflection prompt, a format from Renze & Guven. It asks what went well and badly, which do’s and don’ts recur, which step-by-step procedures emerged, and what to do differently next time.
The insights are stored as insight memories with source = assistant, type derived and importance 7, with the session ids that produced them. They are the assistant’s notes about itself, so they are filtered out of user recall and profile inference. They cannot come back later as something the user supposedly said.
The reflection module can also rewrite the assistant’s SOUL.md guidelines, but the runtime runs it with that step turned off. An unreviewed nightly rewrite of the system prompt is the kind of change that should not happen silently.
Loop B: evolve behaviour, with a way back
Changes to behaviour go through the evolution pipeline. At the end of every turn a cheap recorder writes sanitised signals (no LLM call). Once a day on the sleep tick the optimizer harvests the signals since its last run, clusters them, reflects on each cluster, stages a skill change, and runs automatic verification. A change that passes is promoted live and the previous version is snapshotted. A watchdog on every deep tick rolls a regression back. The pipeline is off by default, never patches protected targets, and skips runs that would need session content without consent.
Source: reflection.ts, gardener-deep-steps.ts, evolution/optimizer.ts, evolution/watchdog.ts.
How memory comes back out
A search takes BM25’s top matches and the embedding search’s top matches as two independent candidate sets and merges them. Each candidate scores 0.3 × its BM25 rank score + 0.7 × its semantic similarity. Very recent memories get a boost (3-hour half-life, up to +0.3) and an exact phrase match is multiplied by 1.5. Anything scoring under 0.35 is dropped rather than padded in. That gate is why the assistant declines on questions it has no memory for, instead of making something up. An LLM reranker (up to 20 candidates) and MMR diversity (λ 0.7) are optional. Each result then brings related memories found by spreading activation over the relation graph.
Time-scoped questions (“last month”, “before the review”) are detected and routed through time-aware retrieval. Measured results for all of this are on the OpenClaw memory benchmark page: LoCoMo F1 0.48 against OpenClaw’s 0.38, same models and scoring.
Source: scallop-store.ts, bm25.ts, relations.ts.
How it differs from Letta and Mem0
The three projects solve different problems, so this is a comparison of design choices, not a ranking. Letta is a platform for building stateful agents that manage their own memory. Mem0 is a memory layer you add to your own app. ScallopBot is a finished assistant with its memory built in. The Letta and Mem0 columns come from their public docs and README as of September 2026.
| ScallopBot | Letta | Mem0 | |
|---|---|---|---|
| Who writes memory | The runtime. A background extractor reads each user message and stores durable facts; the agent can also search memory with a skill. | The agent, through memory tools that edit in-context memory blocks and insert into archival memory. | Your app calls add(); an LLM extracts facts from the messages you pass in. |
| When a fact changes | The new fact supersedes the old one (UPDATES edge, old row marked not-latest) only if a classifier is ≥0.8 confident, subject and category match, and a deterministic check confirms a contradiction. Otherwise both are kept. | The agent rewrites the block text itself; archival passages are hard for the agent to modify (developers can via the SDK). | ADD-only: one LLM pass, no UPDATE or DELETE. Old facts stay; retrieval is expected to rank the current one first. |
| Background consolidation | Nightly, in quiet hours: clusters of related memories are fused into derived summaries (sources kept), then a noisy association pass links distant memories. | Optional sleep-time agents (“dreaming”) review recent conversations and update memory, triggered after a number of agent steps or on context compaction. | None documented. Memory changes happen when add() is called. |
| Forgetting | Every memory carries a prominence score that decays by category and type. Low-utility memories are archived; superseded ones are deleted once they fade below 0.01. | No decay. Blocks have a character limit, so the agent chooses what to keep in context; everything else stays in the database. | No decay documented; memories accumulate. |
| Retrieval | BM25 and embedding candidates unioned and scored (0.3 / 0.7), recency boost, score gate, optional LLM rerank and MMR, then relation-graph expansion. | Core blocks are always in context. Archival memory is semantic search the agent calls; old messages are searchable too. | Semantic, BM25 and entity matching scored in parallel and fused, with time-aware ranking. |
| Shape of the product | A complete self-hosted assistant with memory built in. One SQLite file. Memory also served over MCP. MIT. | A platform for building stateful agents: API, SDKs, self-hostable server. Apache-2.0. | A memory layer for your own app: library, self-hosted server, or hosted platform. Apache-2.0. |
The real differences
Who does the upkeep. In Letta, memory is something the agent does: it spends tool calls deciding what goes in its blocks, and optional sleep-time agents do more of that between turns. In ScallopBot, the main agent does none of it. Extraction, supersession, decay, consolidation and association all run in the runtime, on a schedule, on the cheaper cognition model.
What happens to old facts. Mem0 now appends only and leaves it to retrieval to pick the current fact. Letta leaves it to the agent’s own edits. ScallopBot records an explicit supersession edge, but only after a strict check, and otherwise keeps both. It also lets unused memories fade by category, which neither of the others documents.
Where the others are ahead. Letta gives an agent explicit, inspectable control of its context, and lets several agents share memory blocks. Mem0 is built to drop into any app, with SDKs, a hosted platform and entity linking. ScallopBot is aimed at one person running their own assistant. It shares memory with other tools over MCP, but it is not a general memory API for multi-tenant products.
References: Letta memory, Letta archival memory, Mem0 add memory, Mem0 README. Spotted something out of date? Open an issue on GitHub.
Common questions
Is “sleep-style consolidation” a metaphor or a schedule?
Both, but the schedule is literal. The sleep tick only fires when at least 20 hours have passed since the last one and the user’s local clock reads between 2 and 5 AM. The timestamp is stored in SQLite, so a restart does not reset it. Both numbers are configurable (GARDENER_SLEEP_INTERVAL_MS, GARDENER_QUIET_HOURS_START / _END).
The NREM-then-REM order follows the sleep literature: consolidation first, loose association second. The code does not claim to simulate a brain; it borrows the order and the idea that maintenance should happen while nobody is waiting on a reply.
Does self-reflection rewrite the assistant’s personality file?
No. The reflection module can produce a rewritten SOUL.md, but the runtime calls it with that step switched off. Reflection output is stored as insight memories marked as the assistant’s own, which are kept out of user recall and profile inference.
Changes to behaviour go through a separate evolution pipeline instead. It is off by default. When enabled, a proposed skill change has to pass automatic verification before it is promoted, the previous version is snapshotted, and a watchdog on the deep tick rolls it back if it regresses.
Does looking a memory up keep it alive?
No, on purpose. Retrieval is logged but does not reinforce a memory, because the software fetching something says nothing about whether it is still true. A memory is reinforced when the user states the same fact again, which the ingestion de-duplicator detects (cosine similarity 0.93 or higher on the same event day).
Can I use this memory from another agent?
Yes. The bundled MCP server exposes memory_store, memory_recall and memory_temporal over the same memories.db, so Claude Code or any MCP client reads and writes what the assistant remembers. Setup is on the OpenClaw memory page.
→ Memory benchmark vs OpenClaw · What it costs to run · Source on GitHub