Skip to content

Memory

Maven's memory has two layers — long-term (always in context) and episodic (retrieved on demand) — and one background pass that promotes facts from one to the other.

Layers

Layer Backed by When Tool to write Tool to read
Long-term memory/MEMORY.md Curated; rarely changed. Direct file edit by the agent (via SDK file tools). Always injected after the system prompt.
Episodic memory/YYYY-MM-DD.md One file per UTC date. remember(content) memory_search(query), memory_get(date)

The split keeps the system prompt small while making yesterday's notes addressable.

Long-term: MEMORY.md

Read at every gateway Apply (start, reload). Whatever is in the file is appended to the system prompt under the heading # Long-term Memory. The agent edits the file directly using SDK file tools (no special protocol).

The plugin in internal/plugins/memory/file declares itself primary, so it is the only memory writer in the registry. The kernel/memory.Registry enforces "exactly one primary" at construction time.

A truncation cap (memoryMdMaxChars = 12288) protects the system prompt from runaway memory files; over-cap content is suffixed with .

Episodic: daily journals

memory/2026-05-27.md is plain markdown — one entry per line is conventional but not required. The remember tool appends; nothing else is touched.

Path discipline:

  • Filenames must be YYYY-MM-DD.md. Anything else is ignored.
  • MEMORY.md is never treated as a journal.
  • Sort order is reverse-chronological (newest first) for search and listing.

Tools

remember(content)

{ "name": "remember", "input": { "content": "User prefers Postgres on Hetzner, not RDS." } }

Appends a line to today's journal (UTC). Creates the journal file (and memory/ directory) on first write.

memory_search(query, limit?)

{ "name": "memory_search", "input": { "query": "postgres", "limit": 5 } }

Case-insensitive substring match across journal files. Returns up to limit (default 5) matching entries, newest first, with snippets truncated to 500 characters.

memory_get(date)

{ "name": "memory_get", "input": { "date": "today" } }

Accepts today, yesterday, or YYYY-MM-DD. Returns the file content with a heading or "No journal entry for {date}." when absent.

Shadow journaler

After every real conversation turn (any channel, sync or streaming), Maven runs a lightweight shadow pass in a separate goroutine. It is not visible to the user and does not delay the reply.

The shadow pass receives the user message and assistant response and runs an isolated LLM call with only two tools exposed — memory_search and remember. It uses memory_search to check whether the fact is already recorded today, then calls remember only for net-new information. Greetings, clarifications, and routine replies produce no journal entry.

user message
main runtime → reply delivered to user
    ↓ goroutine
shadow LLM call
    ↓ memory_search (check today's journal)
    ↓ remember (only if net-new)
    ↓ done — no user-visible output

The shadow runtime is stateless: each pass gets a fresh ephemeral session (UUID) with no prior history and no disk persistence. It uses the same provider configuration as the main runtime but is a separate, isolated api.Runtime instance.

When it fires: only on SessionModeCurrent turns (real user conversations). Cron jobs, heartbeat ticks, and memory consolidation passes do not trigger the journaler.

Configuration

"shadowJournal": {
  "enabled": false,
  "model": ""
}
Field Default Description
enabled false Master toggle. When false, no post-turn shadow pass runs.
model "" Override model for shadow turns. Empty inherits agent.model. Use a cheaper model (e.g. xai/grok-4-1-fast-non-reasoning) to cut cost while keeping the main agent on a stronger model. The provider type, key, and base URL are inherited from provider.*.

Hot reload re-reads both fields on Apply.

Background consolidation

When enabled, memconsolidate reviews recent journals and asks the model to promote worth-keeping facts to MEMORY.md.

{
  "memConsolidate": {
    "enabled": true,
    "intervalHours": 24
  }
}
Field Default Description
enabled false Master toggle.
intervalHours 24 Wall-clock interval between passes.

What it does

On each tick:

  1. Lists journal files from the last 7 days (excluding MEMORY.md).
  2. Concatenates them (truncated at 4000 chars per file) into a single review prompt.
  3. Runs one isolated agent turn (isolated:mem-consolidate:{nanos} session) with the long-term memory already in the system prompt.
  4. The model decides whether to rewrite memory/MEMORY.md via the SDK file tools. The instruction is explicit: "only include what clearly should persist indefinitely. If nothing new warrants promotion, leave the file unchanged."

The consolidation pass uses the same Apply-built runtime and obeys the same admission lane (try-once weight-1 semaphore) as heartbeat.

Skipped ticks

  • No journal files in the last 7 days → no-op.
  • Previous tick still running → skipped with a debug log.

File layout

workspace/memory/
  MEMORY.md            # long-term, curated
  2026-05-25.md        # journal
  2026-05-26.md
  2026-05-27.md

Privacy

Memory files are plain markdown. No encryption, no secrets handling beyond the host filesystem. If you put credentials in a journal, the next consolidation pass might promote them to MEMORY.md. Don't.