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. UPDATES means a newer fact replaced an older one, EXTENDS means it adds to it, DERIVES means 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.

Light tick
every 1 min
Incremental decay on recently changed memories, expiry of old scheduled items, a health ping. No LLM calls.
Deep tick
every 72 min
Full decay scan and utility archival, summaries of sessions idle for 2+ hours, the forgetting audit, behavioural inference, proactive follow-up evaluation.
Sleep tick
20 h+ since last, 2–5 AM only
The dream cycle (NREM consolidation, then REM association), self-reflection, board review, and the evolution optimizer when it is switched on.

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.

Sleep-style consolidation loopThe sleep tick fires at most once per 20 hours and only between 2 and 5 AM local time. It selects latest memories with prominence between 0.05 and 0.8 from memories.db, runs NREM consolidation (cluster, fuse with an LLM, store a derived memory with DERIVES edges while keeping the sources) and then REM exploration (sample seeds, noisy spreading activation, LLM judge, store EXTENDS edges). Both write back to memories.db, which the next night reads.> SLEEP TICK — src/memory/memory.ts, gardener-sleep-steps.ts, dream.tsGate≥ 20 h since last sleep tickAND local hour in 2–5 AMtimestamp kept in SQLitememories.db (SQLite)memories + embeddingsrelation graph:UPDATES · EXTENDS · DERIVESSelect candidatesis_latest = trueprominence in [0.05, 0.8)not profile, not derivedNREM · consolidate1. ClusterBFS over relation edgesfallback: embedding cos ≥ 0.6≥ 3 memories, ≤ 10 clustersskip source sets fused before2. Fuse with an LLMprompt carries the relationsbetween the memoriesreject if summary is notshorter than its sources3. Store derived memoryimportance = max of sourcesconfidence = min of sourcesDERIVES edge to each sourcesources are kept, not replacedREM · associate1. Sample seeds6 seeds, ≤ 2 per categoryweight = importance× prominence × noise2. Wanderspreading activationnoise σ 0.6, 4 hops≤ 8 candidates per seedknown links filtered out3. LLM judgenovelty, plausibility,usefulness scored 1–5accept if average ≥ 34. Store edgeEXTENDS relationno new memory textNREM runs firstthen REM (a failure in one does not stop the other)both phases write back to memories.db (dashed line); tomorrow night starts from thereRuns on the cognition model (fast tier by default). Numbers are the shipped defaults.

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.

Self-reflection loopsConversations become session summaries on the deep tick once a session is idle for two hours. On the sleep tick, the last 24 hours of summaries go through a composite reflection prompt and come out as insight memories marked as the assistant's own, kept out of user recall. The live SOUL.md is not rewritten. Separately, and off by default, each turn records evolution signals; the nightly optimizer harvests, clusters, reflects, stages, verifies and promotes skill changes, and a watchdog on the deep tick rolls back regressions.> LOOP A — memory reflection (always on) · src/memory/reflection.tsConversationsTelegram, Discord,web UI, APISession summarydeep tick, once asession is idle ≥ 2 hComposite reflectionsleep tick, last 24 hexplain · principles ·procedures · adviceInsight memoriessource = assistanttype = derived, imp. 7What reflection does not dono SOUL.md rewrite (distillSoul: false)never enters user recall or profile inference> LOOP B — behaviour evolution (EVOLUTION_ENABLED, off by default) · src/evolution/Per-turn signalsend of every turncheap thresholds,no LLM, sanitisedNightly optimizerharvest → cluster → reflect→ stage → verify → promoteonly signals newer thanthe last run (watermark)Skill promoted liveprior version snapshottedwatchdog on every deep tickrolls a regression backchanged skills are what the next conversation runsThe optimizer also refuses to patch protected targets and skips runs whose evidence would need session content without consent.

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.

ScallopBotLettaMem0
Who writes memoryThe 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 changesThe 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 consolidationNightly, 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.
ForgettingEvery 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.
RetrievalBM25 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 productA 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