Memory
The CMDOP agent has two kinds of memory: working memory (the messages in the current session) and persistent memory (a shared SQLite store the agent can write to across sessions). They serve different purposes and are wired in different places.
Two layers
| Layer | Lifetime | Where it lives |
|---|---|---|
| Working memory | One session | RunContext.Messages — message list rebuilt every turn |
| Persistent memory | Cross-session | memory.db SQLite at utils.DataDir()/memory.db |
Working memory is the conversation history the LLM sees on the current turn. Persistent memory is something the agent explicitly chose to remember by calling a memory tool.
Working memory — the turn cycle
Every turn the runner injects fresh system context (identity, env info, target machine hints) and replays the message history. There are no hidden long-term facts at this layer — what the LLM sees is exactly what is in the message list plus the system prompt.
Working memory carries:
- The user’s prompts.
- The assistant’s previous replies.
- Tool call requests and tool results.
- The current todo list (re-injected each turn so the agent stays oriented).
When the history grows large, the chat layer can call the Jarvis summary agent
(internal/agent/jarvis/) to produce a brief summary that replaces older turns and reduces
token debt.
Persistent memory — the SQLite store
memory.db is a single SQLite file lazy-opened by whichever consumer needs it first. Two
consumers, one file:
| Consumer | Helper | Wiring |
|---|---|---|
| Local chat (CLI / TUI / Desktop) | desktop/services/chat/service.go::getOrCreateMemoryStore | chat.Optional.MemoryStore |
Remote ask_agent (daemon) | network/connection/agent_service.go::getOrCreateMemoryStore | chat.ForDaemonDeps.MemoryStore |
Both helpers are mutex-guarded singletons. On open failure they log a warning and return
nil — the chat continues, just without memory tools.
See internal/agent/memory/CLAUDE.md for the open-once contract.
Memory tools
When the store is wired, four tools register through session_builder.go::tools.PersistentMemory:
| Tool | Purpose |
|---|---|
remember_topic | Store a fact under a topic key. |
recall_memory | Look up facts by topic or free-text query. |
list_memories | Browse stored topics. |
forget_topic | Remove a topic and its associated facts. |
If a session never sees these tools despite a healthy DB, the upstream façade
(chat.ForCLI, ForDaemon, ForDesktop) is missing the MemoryStore field. That is the
canonical debug path.
How the agent decides what to remember
The agent is responsible for calling the memory tools — the system does not auto-summarise into the store. Typical patterns the system prompt encourages:
- Long-lived preferences (“user prefers TypeScript over JavaScript for new components”).
- Project anchors (“the auth service is at
services/auth”). - Cross-session facts the next turn would otherwise have to rediscover.
Things that should not go into persistent memory:
- Secrets or credentials.
- High-churn state (current branch, current todo).
- Arbitrary chat content “just in case”.
Storage and isolation
Persistent memory is per-installation, not per-workspace. The store at
utils.DataDir()/memory.db is shared across all sessions on that machine for the user
that owns the daemon. If you need workspace isolation, use distinct OS users.
Switching workspaces does not switch memory stores. If you store project-specific context, prefix topics with the project name so recall stays clean.
Don’t store secrets in memory. The store is not encrypted and is readable by any process running as the same OS user.
What is not memory
Several adjacent features get confused with memory and are documented elsewhere:
- Sessions — persistent transcripts, see Sessions.
- Todo list — the current plan, replayed each turn but stored in the session, not in
memory.db. Seetodo_writein Tools. - Skill state — skills run as sub-sessions; their internal state is theirs to manage.
Related
TAGS: memory, persistent-memory, working-memory, memory-tools DEPENDS_ON: [agents, agent-loop, tools]