Memory
This page explains how nanoinfra implements long-term AI agent memory: session history, compressed archives, durable knowledge files, Dream consolidation, and Git-backed memory changes.
nanoinfra's memory is built on a simple belief: memory should feel alive, but it should not feel chaotic.
Good memory is not a pile of notes. It is a quiet system of attention. It notices what is worth keeping, lets go of what no longer needs the spotlight, and turns lived experience into something calm, durable, and useful.
That is the shape of memory in nanoinfra.
The Design
nanoinfra does not treat memory as one giant file.
It separates memory into layers, because different kinds of remembering deserve different tools:
session.messagesholds the living short-term conversation.memory/history.jsonlis the running archive of compressed past turns.SOUL.md,USER.md, andmemory/MEMORY.mdare the durable knowledge files.GitStorerecords how those durable files change over time.
This keeps the system light in the moment, but reflective over time.
The Flow
Memory moves through nanoinfra in two stages.
Stage 1: Consolidator
When a conversation grows large enough to pressure the context window, nanoinfra does not try to carry every old message forever.
Instead, the Consolidator summarizes the oldest safe slice of the conversation and appends that summary to memory/history.jsonl.
This file is:
- append-only
- cursor-based
- optimized for machine consumption first, human inspection second
Each line is a JSON object:
{"cursor": 42, "timestamp": "2026-04-03 00:02", "content": "- User prefers dark mode\n- Decided to use PostgreSQL"}
It is not the final memory. It is the material from which final memory is shaped.
Stage 2: Dream
Dream is the slower, more thoughtful layer. It runs on a cron schedule by default and can also be triggered manually.
Dream reads:
- new entries from
memory/history.jsonl - the current
SOUL.md - the current
USER.md - the current
memory/MEMORY.md
Then it edits the long-term files surgically in a single pass. It does not rewrite everything. It makes the smallest honest change that keeps memory coherent.
This is why nanoinfra's memory is not just archival. It is interpretive.
The Files
In this page, workspace means the configured agent workspace (the default
is ~/.nanoinfra/workspaces/default/, or the path passed with --workspace). Selecting a
different project in the WebUI changes that chat's project context and tool
working directory. It does not relocate the files below.
workspace/
├── SOUL.md # The bot's long-term voice and communication style
├── USER.md # Stable knowledge about the user
├── prompts/
│ ├── README.md # Notes for memory guidance files
│ └── dream.md # Optional instructions for how Dream organizes memory
└── memory/
├── MEMORY.md # Project facts, decisions, and durable context
├── history.jsonl # Append-only history summaries
├── .cursor # Consolidator write cursor
├── .dream_cursor # Dream consumption cursor
├── .history.lock # Held while history.jsonl is appended to or compacted
└── .git/ # Version history for long-term memory files
A selected project may provide its own AGENTS.md, but project-local SOUL.md,
USER.md, and memory/ do not replace the agent-owned files above. This keeps
one agent's profile and memory continuous while it works across projects. Use a
separate configured agent workspace when identity or memory must be isolated.
These files play different roles:
SOUL.mdremembers how nanoinfra should sound.USER.mdremembers who the user is and what they prefer.MEMORY.mdremembers what remains true about the work itself.history.jsonlremembers what happened on the way there.
Why history.jsonl
The old HISTORY.md format was pleasant for casual reading, but it was too fragile as an operational substrate.
history.jsonl gives nanoinfra:
- stable incremental cursors.
- safer machine parsing.
- easier batching.
- cleaner migration and compaction.
- a better boundary between raw history and curated knowledge.
You can still search it with familiar tools:
# grep
grep -i "keyword" memory/history.jsonl
# jq
cat memory/history.jsonl | jq -r 'select(.content | test("keyword"; "i")) | .content' | tail -20
# Python
python -c "import json; [print(json.loads(l).get('content','')) for l in open('memory/history.jsonl','r',encoding='utf-8') if l.strip() and 'keyword' in l.lower()][-20:]"
The difference is philosophical as much as technical:
history.jsonlis for structureSOUL.md,USER.md, andMEMORY.mdare for meaning
Commands
Memory is not hidden behind the curtain. Users can inspect and guide it.
| Command | What it does |
|---|---|
/dream | Run Dream immediately |
/dream-log | Show the latest Dream memory change |
/dream-log <sha> | Show a specific Dream change |
/dream-restore | List recent Dream memory versions |
/dream-restore <sha> | Restore memory to the state before a specific change |
/dream-prompt | Show how Dream is being guided for memory |
/dream-prompt init | Create an editable Dream memory guide at prompts/dream.md |
These commands exist for a reason. Automatic memory writes without being asked, so users should always retain the right to inspect, understand, and restore it.
Versioned Memory
After Dream changes long-term memory files, nanoinfra can record that change with GitStore.
This gives memory a history of its own:
- you can inspect what changed
- you can compare versions
- you can restore a previous state
That turns memory from a silent mutation into an auditable process.
What it versions: SOUL.md, USER.md, memory/MEMORY.md, memory/.dream_cursor, and everything
under skills/. Skills are in that list because a skill whose frontmatter says always: true
reaches every system prompt. That makes one more durable than MEMORY.md rather than less. So a
skill Dream writes is reported, committed and restorable like any other memory change.
/dream-restore <sha> undoes the changes that commit introduced, and only those. A file the commit
never touched is not written. When a file it did touch has moved on since, the restore says so and
names it. Putting that file back throws the later work away.
Guiding Dream
Dream decides what to keep, update, or forget using nanoinfra's built-in memory instructions. Most users can leave this alone.
If one workspace needs a different memory style, create an editable guide:
/dream-prompt init
This creates:
workspace/prompts/dream.md
Edit that file in plain Markdown. When it has content, Dream follows it for this workspace before reading the latest conversation history. You do not need to paste history into the file. Dream adds the current ## Conversation History block automatically.
To return to nanoinfra's default behavior, delete prompts/dream.md or leave it empty.
Each workspace has its own guide. Changing this file does not affect other nanoinfra workspaces.
Configuration
Dream is configured under agents.defaults.dream:
{
"agents": {
"defaults": {
"dream": {
"intervalH": 2,
"modelOverride": null
}
}
}
}
| Field | Meaning |
|---|---|
intervalH | How often Dream runs, in hours |
cron | Cron expression override (takes precedence over intervalH) |
modelOverride | Optional model preset name used for Dream |
In practical terms:
intervalHis the normal way to configure Dream frequency. Internally it runs as aneveryschedule.cronoverridesintervalHwhen set, allowing precise cron expressions (e.g.0 */4 * * *).modelOverrideselects a named entry frommodel_presetsfor Dream. It accepts preset names only. Raw model identifiers are not supported. If omitted, Dream uses the main agent's selected runtime.
In Practice
What this means in daily use is simple:
- conversations can stay fast without carrying infinite context.
- durable facts can become clearer over time instead of noisier.
- the user can inspect and restore memory when needed.
Memory should not feel like a dump. It should feel like continuity.
That is what this design is trying to protect.