Skip to main content

Memory System

Nanobot implements a two-layer memory system that balances context efficiency with long-term knowledge retention.

Architecture Overview

The memory system consists of two complementary layers:
┌─────────────────────────────────────────────────────────────┐
│                    Session History                          │
│  Recent conversation messages (rolling window)              │
│  Loaded into context for every interaction                  │
└────────────────┬────────────────────────────────────────────┘

                 │ Consolidation (automatic)

┌─────────────────────────────────────────────────────────────┐
│              MEMORY.md + HISTORY.md                         │
│                                                              │
│  MEMORY.md: Long-term facts (updated incrementally)         │
│  HISTORY.md: Searchable event log (append-only)             │
└─────────────────────────────────────────────────────────────┘
This design keeps active context small while preserving important information indefinitely.

Session History

Session Manager

Sessions are managed by the SessionManager class in nanobot/session/manager.py:
class Session:
    """Represents a conversation session."""
    
    key: str                    # "channel:chat_id" identifier
    messages: list[dict]        # Full message history
    last_consolidated: int      # Index of last consolidated message
    created_at: datetime
    updated_at: datetime
    
    def get_history(self, max_messages: int = 50) -> list[dict]:
        """Get recent messages for context building."""
        return self.messages[-max_messages:]

Message Structure

Session messages follow the standard chat format:
{
    "role": "user" | "assistant" | "tool",
    "content": "message text",
    "timestamp": "2026-03-06T10:30:00",
    "tool_calls": [...],        # Optional: for assistant messages
    "tool_call_id": "...",      # Optional: for tool messages
    "name": "tool_name",        # Optional: for tool messages
}

Rolling Window

The agent loop maintains a rolling window of recent messages:
history = session.get_history(max_messages=self.memory_window)
initial_messages = self.context.build_messages(
    history=history,
    current_message=msg.content,
    channel=msg.channel, 
    chat_id=msg.chat_id,
)
See nanobot/agent/loop.py:428-434.
Default memory_window is 100 messages — configurable via agent initialization.

Memory Store

Two-Layer Storage

The MemoryStore class in nanobot/agent/memory.py manages two files:

MEMORY.md

Long-term facts stored as markdown
  • Updated incrementally by consolidation
  • Contains important facts, preferences, context
  • Loaded into system prompt for every message

HISTORY.md

Searchable event log (append-only)
  • Timestamped summaries of past conversations
  • Grep-searchable for specific events/topics
  • Not loaded by default (agent can search it)

Implementation

class MemoryStore:
    """Two-layer memory: MEMORY.md (long-term facts) + HISTORY.md (grep-searchable log)."""

    def __init__(self, workspace: Path):
        self.memory_dir = ensure_dir(workspace / "memory")
        self.memory_file = self.memory_dir / "MEMORY.md"
        self.history_file = self.memory_dir / "HISTORY.md"

    def read_long_term(self) -> str:
        if self.memory_file.exists():
            return self.memory_file.read_text(encoding="utf-8")
        return ""

    def write_long_term(self, content: str) -> None:
        self.memory_file.write_text(content, encoding="utf-8")

    def append_history(self, entry: str) -> None:
        with open(self.history_file, "a", encoding="utf-8") as f:
            f.write(entry.rstrip() + "\n\n")

    def get_memory_context(self) -> str:
        long_term = self.read_long_term()
        return f"## Long-term Memory\n{long_term}" if long_term else ""
See nanobot/agent/memory.py:45-67.

Consolidation Process

Automatic Triggering

Consolidation is triggered automatically when unconsolidated messages exceed the memory window:
unconsolidated = len(session.messages) - session.last_consolidated
if (unconsolidated >= self.memory_window and 
    session.key not in self._consolidating):
    self._consolidating.add(session.key)
    lock = self._consolidation_locks.setdefault(session.key, asyncio.Lock())

    async def _consolidate_and_unlock():
        try:
            async with lock:
                await self._consolidate_memory(session)
        finally:
            self._consolidating.discard(session.key)

    _task = asyncio.create_task(_consolidate_and_unlock())
    self._consolidation_tasks.add(_task)
See nanobot/agent/loop.py:405-421.
Consolidation runs asynchronously without blocking message processing.

LLM-Powered Consolidation

The consolidation process uses the LLM to intelligently extract key information:
1

Select Messages to Consolidate

Grab unconsolidated messages (or specific range):
if archive_all:
    old_messages = session.messages
    keep_count = 0
else:
    keep_count = memory_window // 2
    old_messages = session.messages[session.last_consolidated:-keep_count]
2

Build Consolidation Prompt

Format messages and current memory:
lines = []
for m in old_messages:
    if not m.get("content"):
        continue
    tools = f" [tools: {', '.join(m['tools_used'])}]" if m.get('tools_used') else ""
    lines.append(f"[{m.get('timestamp', '?')[:16]}] {m['role'].upper()}{tools}: {m['content']}")

current_memory = self.read_long_term()
prompt = f"""Process this conversation and call the save_memory tool with your consolidation.

## Current Long-term Memory
{current_memory or "(empty)"}

## Conversation to Process
{chr(10).join(lines)}"""
3

Call LLM with save_memory Tool

The LLM calls a special tool to save the consolidated memory:
_SAVE_MEMORY_TOOL = [
    {
        "type": "function",
        "function": {
            "name": "save_memory",
            "description": "Save the memory consolidation result to persistent storage.",
            "parameters": {
                "type": "object",
                "properties": {
                    "history_entry": {
                        "type": "string",
                        "description": "A paragraph (2-5 sentences) summarizing key events/decisions/topics. Start with [YYYY-MM-DD HH:MM]. Include detail useful for grep search.",
                    },
                    "memory_update": {
                        "type": "string",
                        "description": "Full updated long-term memory as markdown. Include all existing facts plus new ones. Return unchanged if nothing new.",
                    },
                },
                "required": ["history_entry", "memory_update"],
            },
        },
    }
]
4

Save Consolidated Memory

Extract and save both memory layers:
args = response.tool_calls[0].arguments

if entry := args.get("history_entry"):
    self.append_history(entry)

if update := args.get("memory_update"):
    if update != current_memory:
        self.write_long_term(update)

session.last_consolidated = 0 if archive_all else len(session.messages) - keep_count
See nanobot/agent/memory.py:69-150.

Consolidation Example

Input (conversation):
[2026-03-06 10:00] USER: What's my favorite color?
[2026-03-06 10:01] ASSISTANT: I don't have that information yet.
[2026-03-06 10:02] USER: It's blue. Remember that.
[2026-03-06 10:03] ASSISTANT: Got it, I'll remember that your favorite color is blue.
[2026-03-06 10:05] USER: What programming languages do I know?
[2026-03-06 10:06] ASSISTANT: Could you tell me?
[2026-03-06 10:07] USER: Python, JavaScript, and Go.
[2026-03-06 10:08] ASSISTANT: Thanks, I've noted that you know Python, JavaScript, and Go.
Output (MEMORY.md):
# User Preferences

- Favorite color: blue

# Skills

- Programming languages: Python, JavaScript, Go
Output (HISTORY.md):
[2026-03-06 10:00] User shared personal preferences and technical background. 
Confirmed favorite color is blue. Discussed programming experience with Python, 
JavaScript, and Go. No specific projects or tasks initiated during this session.

Manual Memory Control

/new Command

Users can manually trigger consolidation and start fresh:
if cmd == "/new":
    # Archive entire session to memory
    snapshot = session.messages[session.last_consolidated:]
    if snapshot:
        temp = Session(key=session.key)
        temp.messages = list(snapshot)
        if not await self._consolidate_memory(temp, archive_all=True):
            return OutboundMessage(
                channel=msg.channel, chat_id=msg.chat_id,
                content="Memory archival failed, session not cleared. Please try again.",
            )
    
    # Clear session history
    session.clear()
    self.sessions.save(session)
    self.sessions.invalidate(session.key)
    return OutboundMessage(
        channel=msg.channel, chat_id=msg.chat_id,
        content="New session started."
    )
See nanobot/agent/loop.py:373-400.
/new is useful when context switches or when starting an unrelated conversation.

Context Integration

Memory is loaded into the system prompt by ContextBuilder:
def build_system_prompt(self, skill_names: list[str] | None = None) -> str:
    """Build the system prompt from identity, bootstrap files, memory, and skills."""
    parts = [self._get_identity()]

    bootstrap = self._load_bootstrap_files()
    if bootstrap:
        parts.append(bootstrap)

    memory = self.memory.get_memory_context()
    if memory:
        parts.append(f"# Memory\n\n{memory}")

    # ... skills, etc ...
    
    return "\n\n---\n\n".join(parts)
See nanobot/agent/context.py:26-52.
Only MEMORY.md is loaded automatically. HISTORY.md can be searched by the agent using file tools.

Session Persistence

Sessions are persisted to disk as JSON files: Location: {workspace}/sessions/{channel}_{chat_id}.json Format:
{
  "key": "telegram:123456789",
  "messages": [
    {
      "role": "user",
      "content": "Hello",
      "timestamp": "2026-03-06T10:00:00"
    },
    {
      "role": "assistant",
      "content": "Hi! How can I help?",
      "timestamp": "2026-03-06T10:00:01"
    }
  ],
  "last_consolidated": 0,
  "created_at": "2026-03-06T10:00:00",
  "updated_at": "2026-03-06T10:00:01"
}
Sessions are loaded lazily and cached in memory for performance.

Memory Best Practices

Let Consolidation Work

Don’t manually clear sessions frequently — let automatic consolidation manage context.

Use /new Sparingly

Only use /new for true context switches (new project, unrelated topic, etc.).

Explicit Important Info

Tell the agent explicitly to remember important facts: “Remember that I…”

Search History

The agent can search HISTORY.md using exec + grep for past events.

Troubleshooting

Cause: Session history may have been cleared or consolidated prematurely.Solution: Check {workspace}/memory/MEMORY.md — the information should be there. If not, it wasn’t properly consolidated. You can tell the agent the information again.
Cause: LLM may have refused to call the save_memory tool or returned invalid data.Solution: The agent loop logs consolidation failures. Check logs with:
grep "Memory consolidation" ~/.nanobot/nanobot.log
You can retry by using /new to force consolidation.
Cause: Session history + memory + system prompt exceeds model context limit.Solution:
  • Reduce memory_window in agent config (default: 100)
  • Use /new to consolidate and start fresh
  • Keep MEMORY.md concise (agent should do this automatically)

Implementation Reference

Key files:
FilePurposeKey Classes/Methods
nanobot/agent/memory.pyMemory store implementationMemoryStore, consolidate()
nanobot/session/manager.pySession managementSession, SessionManager
nanobot/agent/loop.pyConsolidation trigger_consolidate_memory(), /new command
nanobot/agent/context.pyMemory integrationbuild_system_prompt()

Agent Loop

How consolidation is triggered

Architecture

Where memory fits in the system

Tools

How the agent searches memory

Build docs developers (and LLMs) love