Skip to main content

Controlled rolling-context conversation runtime for Claude — transparent turns store, role doc system prompt, and the Claude Agent SDK underneath.

Project description

Mnemara

PyPI Python License: MIT

Note: There is also a Rust project called mnemara by deliberium — a memory engine for embedded/service systems. Different project, parallel naming (both inspired by Mnemosyne). If you arrived looking for that one, head over there.

Stop your Claude agent from running away with your bill.

Mnemara is a Claude conversation runtime where the role doc is re-read on every turn and pinned as the system prompt. That sounds boring until you realize what it lets you do: write rules the agent has to obey on every single API call — not just the first one — and the agent enforces them on itself.

Empirical check (2026-05-08): in a 101-turn coding session run on Mnemara itself, the panel made zero out-of-lane writes and zero protocol violations against its role doc. The rule that fires on turn 1 still fires on turn 101.

The flagship example is examples/roles/sentinel.md. Drop it in, and the agent watches its own execution for the four failure modes that turn agent sessions into expensive accidents:

  • No progress — N+ turns on the same sub-goal with no state change.
  • Polling — same tool, same args, 3+ times in a row.
  • Drift — about to do something the user didn't ask for.
  • Sycophancy — about to reverse a conclusion under tone-only pushback.

When any one fires, the agent halts and asks, instead of burning another N turns of API budget. The role doc is plain Markdown — edit it to match the failure modes you actually see.

Try it in 30 seconds:

pip install mnemara
mnemara init --instance scratch
mnemara role --instance scratch --set-from-url \
  https://raw.githubusercontent.com/mekickdemons-creator/mnemara/main/examples/roles/sentinel.md
mnemara run --instance scratch

Built on the Claude Agent SDK. Mnemara wraps the SDK with a transparent, file-based context layer so you can see and shape exactly what the model sees on every turn.

What's in the box:

  • A role doc re-read on every API call and pinned as the system prompt (the bit that makes Sentinel work).
  • A configurable rolling window of recent turns (FIFO, by row count or token budget).
  • Native tool use — Bash, Read, Edit, Write — plus an in-process WriteMemory tool registered as an SDK MCP server.
  • Optional MCP wire-through: declare stdio MCP servers in config and the Claude Agent SDK exposes them to the model.
  • A Textual TUI (mnemara run) and a bare prompt-toolkit REPL fallback.
  • Per-instance, file-only state under ~/.mnemara/<instance>/ — no daemon, no service, no hidden state.
  • Optional memory/wiki + LanceDB RAG + Kuzu property graph backends, and a mnemara replay consolidation primitive that drafts wiki pages and role-amendment proposals from clustered memory atoms.

Install

pip install mnemara

Or from source if you want to hack on it:

git clone https://github.com/mekickdemons-creator/mnemara.git
cd mnemara
python -m venv .venv
source .venv/bin/activate
pip install -e .

Platform support

Tested on Linux and macOS. Windows works through WSL — the Claude Agent SDK's tool calls use bash, which is not available on native Windows shells (cmd / PowerShell). If you're on Windows, run Mnemara inside a WSL distro and treat that as your "Linux" environment.

Auth

Mnemara runs on the Claude Agent SDK, which talks to the Anthropic API. The easiest way is to set your API key:

export ANTHROPIC_API_KEY=sk-ant-...

Get a key at https://console.anthropic.com/. The SDK also supports the local claude CLI's subscription auth as a fallback if you have Claude Code installed and logged in — but the documented path is the API key.

Quick start

Before your first run: skim the Permissions model section. Mnemara gives an LLM Bash/Read/Write/Edit access to the machine you run it on. It is not a sandbox.

mnemara init --instance scratch
# (prompts for role doc path; you can leave it blank and set it later)
mnemara role --instance scratch --set examples/roles/sentinel.md
mnemara run --instance scratch

The --set argument points at a role doc — a Markdown file that becomes the agent's system prompt. The repo ships with examples/roles/sentinel.md as a starting point; see Role docs below for what to put in your own.

By default mnemara run opens the Textual chat panel (TUI). Pass --no-tui (or set MNEMARA_NO_TUI=1) for the bare prompt-toolkit REPL — useful for scripting or non-TTY contexts.

Chat panel layout

+------------------------------------------------------------+
| mnemara: scratch        model=claude-opus-4-7  role=...    |   header
+------------------------------------------------------------+
|                                                            |
|  you: how do I check the lease timeout?                    |
|  assistant: open server.py and grep for ...                |
|  > tool: Read(file_path=server.py)                         |
|    result: ...                                             |
|                                                            |   chat log
+------------------------------------------------------------+
| turns: 12/100 | tokens: 14K/200K | model: claude-opus-4-7  |   status
+------------------------------------------------------------+
| > _                                                        |   input
+------------------------------------------------------------+

The input area is multi-line. See Slash commands below for the full keybinding table and the slash-command surface (/role_doc, /context, /clear, /evict, /inbox, /models, /swap, /tokens, /export, /import, /compress reads, /skeleton, /name, etc.).

Role docs

The role doc is a plain Markdown file that becomes the agent's system prompt. Mnemara re-reads it on every API call and pins it at slot 0 of the messages — meaning it applies to every turn, not just the opening one, and you can edit the file mid-session and the next turn picks up the changes.

This is the strongest steering signal you have over the agent. Use it.

What to put in a role doc

A good role doc is a short prose document (a few hundred to a few thousand words) that answers, in order:

  1. Who the agent is — its identity and standing instructions in this instance. ("You are a code reviewer for the Acme repo." "You are a research assistant working on tax law.")
  2. What it should and shouldn't do — scope, hard constraints, anti-patterns to avoid.
  3. How it should behave when something goes wrong — when to halt, when to ask for help, when to escalate.

You can include style notes ("be terse, no apologies"), tooling conventions ("always run the tests after writing code"), or domain glossaries. There is no required schema. The only mechanical requirement is that the file exists and is readable.

Solving the looping / drift problem

The most common reason an interactive agent session burns through tokens with nothing to show for it is that the agent gets stuck:

  • It calls the same tool over and over waiting for output to change.
  • It drifts from the user's actual request into adjacent rabbit holes.
  • It reverses a correct conclusion the moment the user pushes back.

These are role-doc-shaped problems. The role doc is where you encode the rules that keep the agent from spiraling. If those rules aren't in the system prompt, they aren't applied consistently — they reappear only when the user remembers to remind the agent.

Example: Sentinel

examples/roles/sentinel.md is a self-monitoring role doc. Drop it in as your instance's role and the agent will watch its own execution for the failure modes above (timeout / no progress, polling, semantic drift, sycophantic reversal) and halt to ask the user rather than spending another N turns on a runaway loop.

mnemara role --instance my-agent --set examples/roles/sentinel.md

Or download it directly from GitHub without cloning the repo:

mnemara role --instance my-agent --set-from-url \
  https://raw.githubusercontent.com/mekickdemons-creator/mnemara/main/examples/roles/sentinel.md

--set-from-url fetches the doc once (https only, 1 MB cap, UTF-8) and saves it into ~/.mnemara/<instance>/role.md. Mnemara never re-fetches the URL at runtime — the saved local copy is what gets re-read each turn. Edit the local copy to customize.

Use it as-is for monitoring-flavored work, or treat it as a template: copy the file, edit the trigger conditions to match the failure modes you care about, and point your instance at the copy.

You can also layer Sentinel-style self-monitoring rules on top of a task-specific role doc — there's nothing special about Sentinel; it's just text in a Markdown file that the agent reads on every turn.

More example role docs

The repo ships three role docs under examples/roles/:

File Use it when
sentinel.md You want the agent to self-monitor for loops, drift, sycophancy and halt to ask the user.
coder.md You want a careful, grounded coding collaborator that reads before it writes and edits narrowly.
researcher.md You want a citation-grounded research assistant that distinguishes observation from inference and refuses to fabricate.

These are starting points. Copy any of them, edit the rules to match your work, and point your instance at the copy.

What a session looks like with a self-monitoring role doc

Concrete example. Your role doc tells the agent to halt if it polls the same tool 3 times. The user asks the agent to wait for a build to finish:

you: run the build and tell me when it's done

agent: > Bash(npm run build)
       (output: building...)
       Build is running. Let me check.
       > Bash(npm run build)
       (output: building...)
       Still building.
       > Bash(npm run build)
       (output: building...)

       [SENTINEL HALT]
       trigger: polling
       turn: 4
       context: I've called `npm run build` three times in a row and
       each call started a new build rather than reporting on the
       running one. I'm not actually monitoring; I'm starting fresh
       builds.
       proposed next step: wait on the existing build process via
       `wait` or check `npm run build:status` if it exists. Which
       would you like?

Without the role doc, the same agent would happily call npm run build twenty times in a row, each one starting a fresh build, before either the user noticed or the API budget ran out. The role doc is what makes the agent notice on turn 4 and check in.

State layout

Everything for an instance lives under ~/.mnemara/<instance>/:

Path Purpose
config.json The config (model, role-doc path, window size, tool policies, MCP servers).
turns.sqlite The rolling-window store. One row per turn.
permissions.json Persisted "always allow" patterns per tool.
memory/YYYY-MM-DD.md Notes the agent or user have written via WriteMemory / /note.
wiki/<slug>.md Topic-keyed wiki pages (slash-allowed slugs).
index/ LanceDB RAG index (embeddings of memory + wiki + manual entries).
graph/ Kuzu property graph (entities, wiki pages, topic tags, edges).
wiki_proposals/<slug>.md Replay-drafted wiki promotions awaiting agent review.
sleep/YYYY-MM-DD.md Sleep digests written by the replay primitive.
memory/archive/ Near-duplicate memory atoms archived (never deleted) by replay.
role_proposals/ Role-amendment proposals — written by propose_role_amendment or replay.
debug.log Append-only JSONL log: errors, tool calls, eviction events.
.prompt_history REPL input history.

Config fields

~/.mnemara/<instance>/config.json:

Core

Field Meaning
role_doc_path Absolute path to the role doc. Re-read on every API call. Pinned as the system prompt.
model Claude model id (e.g. claude-opus-4-7, claude-sonnet-4-6, claude-haiku-4-5).
max_window_turns Rolling-window size (FIFO). Default 20. Counts both user and assistant turns.
max_window_tokens Token-budget cap. The window is FIFO-trimmed once total tokens exceed this.
allowed_tools List of {tool, mode, allowed_patterns} policies. modeallow/ask/deny.
mcp_servers List of stdio MCP servers wired through to the model.
stream If true, render the model's text deltas as they arrive.
bash_timeout_seconds Bash command timeout. Default 60.
file_tool_home_only If true, Read/Write/Edit refuse paths outside $HOME. Default true.
display_name Cosmetic label shown in the TUI chat log instead of assistant. Empty = default. Set via /name <label>.

Context discipline (opt-in compression / eviction)

All default to False (or 0). Turn on per instance.

Field Meaning
auto_evict_after_write After any turn containing Edit/Write/MultiEdit/NotebookEdit blocks, stub the bulky body content of those tool_use specs and prior Read specs for the same file. Audit shell preserved.
compress_repeated_reads After every turn, walk the window for repeated Reads of the same file — keep the latest at full fidelity, stub earlier ones as a unified diff or "unchanged" pointer. v0.6.0 / v0.8.0.
preserve_compressed_reads When set, rows flagged as compression stubs are excluded from cap-FIFO eviction (same soft-protect as pinned rows).
read_skeleton_enabled Registers the read_skeleton tool so the agent can request Python signatures + docstrings only (~90% smaller than a full Read). v0.7.0.
file_stat_manifest_enabled Auto-injects a markdown table at the bottom of system_prompt listing every file Read this session: size, mtime, fresh/STALE/gone vs current disk hash, est tokens. v0.7.0.
runtime_sentinel Wires SDK hook events so a per-session RuntimeSentinel watches PreToolUse events. If the same (tool, args) fires 3+ times in 5 events, injects a synthetic [SENTINEL HALT] and stops the turn. Belt-and-suspenders with sentinel.md.
row_cap_slack_when_token_headroom If > 0, lets n_turns exceed max_window_turns by up to this many rows when token usage is well under cap. Lets the row cap "breathe" with the byte budget. Default 0.

Memory backends

Field Meaning
rag_enabled LanceDB RAG index over memory/ + wiki/. Default True.
rag_embed_url Ollama embeddings endpoint. Default http://localhost:11434/api/embeddings.
rag_embed_model Embedding model. Default nomic-embed-text.
rag_auto_index_memory Re-index memory atoms on each write. Default True.
rag_auto_index_wiki Re-index wiki pages on each write. Default True.
graph_enabled Kuzu property graph for memory_atoms/wiki_pages/entities. Default True. Off-switch if Kuzu is unavailable.
replay_default_days Default lookback for mnemara replay. Default 7.
replay_default_threshold Minimum cluster size to count as a pattern. Default 3.
replay_policy_path Override path for the replay policy doc. Empty = <instance>/wiki/replay_policy.md.

CLI commands

mnemara init --instance <name>            # create ~/.mnemara/<name>/, refuses to overwrite
mnemara run --instance <name>             # open the chat panel (TUI; --no-tui for bare REPL)
mnemara list                              # list instances
mnemara show --instance <name> [-n N]     # print the rolling window (read-only)
mnemara clear --instance <name>           # wipe the rolling window
mnemara delete --instance <name> --force  # nuke ~/.mnemara/<name>/
mnemara role --instance <name> --set PATH                # set role_doc_path (local file)
mnemara role --instance <name> --set-from-url URL        # download once into instance dir
mnemara note --instance <name> TEXT...    # append a memory note from the shell
mnemara replay --instance <name> [--days N] [--threshold N] [--apply]  # consolidation pass
mnemara migrate --all                     # run schema migration on every instance (idempotent)
mnemara migrate --instance <name>         # run schema migration on one instance

Slash commands (REPL and TUI)

Commands marked (TUI) are TUI-only — they open a modal overlay or toggle a TUI-specific feature.

/role <path>         (REPL) swap role doc (also persists to config)
/role_doc            (TUI) open the role-doc editor modal — edit live; Ctrl+S
                     saves; changes take effect on the next turn (the role doc
                     is re-read on every API call). Modal exposes
                     [📋 Paste] / [⎘ Copy] buttons (raw terminal paste keys
                     can be intercepted by the OS terminal before reaching
                     Textual's modal focus — the buttons go through pyperclip
                     directly and are reliable across terminals).
/context             (TUI) open the context viewer — tabbed breakdown of where
                     input tokens are going (tool schemas, role doc, manifest,
                     pinned rows, working window) with browse / evict / pin
                     actions and role filtering. Pinned rows are listed in
                     slot-position order with a numeric badge (`#03`). Detail
                     panel splits into a read-only header (Turn / role / ts /
                     pin) and a body-only editable TextArea; [💾 Save edit]
                     writes body changes back to the row in place. [✏️ Rename]
                     renames just the slug portion of a pin label while
                     preserving its position prefix; [➕ Add slot] inserts a
                     new pinned slot at the next available position;
                     [⇅ Move] reorders pinned slots by changing the prefix.
                     [📋 Paste] / [⎘ Copy] available too.
/show                (REPL) print the rolling window
/clear               comprehensive wipe — strips tool_use blocks, thinking
                     blocks, and user/assistant turns; pinned rows preserved;
                     reports the freed-token count
/models              list available Claude model shortcuts
/swap <model|n>      switch model for this and future sessions
/tokens <N>          set max_window_tokens live (accepts 500k, 1m, 200000)
/note <text>         (REPL) append to today's memory file
/proposals           (REPL) list pending role-amendment proposals
/evict               show eviction stats
/evict tools         strip tool_use blocks from all stored rows
/evict thinking      strip thinking blocks from all stored rows
/evict user          drop all user-turn rows (keep assistant responses)
/evict assistant     drop all assistant-turn rows (keep user inputs)
/evict <N>           drop the N oldest rows from the rolling window
/evict last <N>      drop the N most-recent rows (rollback a bad paste/turn)
/inbox               (TUI) toggle peer-message delivery on/off
/stop                cancel the in-flight turn
/export [N] [path]   round-trip the session (turns + config + role_doc) to markdown
/import <path>       restore a session from a /export markdown file
/compress            compress to 25% of current token count (the default
                     compress-to-tokens operation, also available as the
                     [🗜 Compress] button)
/compress <N>        compress to a specific token target (e.g. /compress 20000,
                     /compress 500k); accepts k/m suffixes
/compress smart      inject a structured audit turn so the agent reviews its
                     own rolling window and decides what to evict — judgment-
                     driven rather than mechanical FIFO
/compress reads      stub repeated Read results with diffs (existing behavior)
/skeleton <path>     manually extract Python signatures from a file (debug)
/name <label>        set display_name; clear with /name (no arg)
/quit, /exit         save state and exit
/help                show this list

Toolbar buttons

The TUI chat panel exposes a row of buttons below the chat log:

Button Action
Send ⌃S Send the current input (same as Ctrl+S).
⚡ Inbox: ON / OFF Toggle peer-message delivery live. Only active when peer-poll is configured (see Peer messaging).
📄 Role Open the role-doc editor modal (same as /role_doc).
💬 Context Open the context viewer (same as /context).
🗜 Compress Compress the rolling window to 25% of current tokens (same as /compress with no argument). Strips thinking blocks, write pairs, and tool_use bodies before falling back to FIFO eviction; pinned rows are never touched.
Quit ⌃C Save state and exit (same as Ctrl+C).

TUI keybindings

The TUI input area is multi-line — Enter inserts a newline.

Key Action
Ctrl+S Send the message
Enter Newline in the input
Escape Clear the input
Ctrl+L Clear the on-screen chat log (does NOT touch turns.sqlite)
PageUp / PageDown Scroll chat
Ctrl+C Quit

Permissions model

Read this section before you run Mnemara. The agent has Bash, Read, Write, and Edit tools. With permissive settings it can run any command on your machine — including destructive ones (rm -rf, git push --force, network calls, file overwrites). Mnemara is not a sandbox. It runs as your user, with your filesystem and network permissions. Treat it like a shell session you've handed to an LLM.

Each tool has a mode:

  • allow — never prompts. Use only for tools you've decided are safe to invoke without review.
  • ask — prompts on first use; user picks yes, no, always, or session.
  • deny — always blocked.

Defaults (deliberately conservative): Bash=ask, Read=allow, Write=ask, Edit=ask, WriteMemory=allow.

Things to know:

  • Setting Bash to allow means the agent can run any shell command without prompting. Don't do this on a machine with credentials, prod access, or unbacked-up data unless you know what you're doing.
  • allow_always (the a answer at a prompt) writes a regex to permissions.json. Review that file — a too-broad regex is a permanent foot-gun.
  • file_tool_home_only (default True) restricts Read/Write/Edit to paths under $HOME. Disabling it lets the agent touch anywhere your user can.
  • The agent can call MCP tools wired through mcp_servers in config.json. Those tools run with your privileges — vet them like you'd vet any third-party binary.
  • If you don't trust a role doc to behave, run it in a throwaway instance (--instance scratch) on a non-sensitive machine, or under a restricted user account / container.

Runtime sentinel (runtime_sentinel: true in config.json) adds a second, belt-and-suspenders layer on top of the role-doc sentinel. When enabled, Mnemara wires the Claude Agent SDK's include_hook_events flag (SDK

= 0.1.74 required) so that tool lifecycle events flow into each turn's processing loop. A per-session RuntimeSentinel watches the last five PreToolUse events: if the same (tool_name, args) pair appears three or more times in that window it injects a synthetic [SENTINEL HALT] notice into the assistant turn and stops consuming the stream, preventing the model from burning further tool calls on an obvious polling loop. This is complementary to the role-doc sentinel (sentinel.md): the role-doc rule relies on the model noticing its own pattern; the runtime rule fires at the protocol level regardless of what the model intended. Both can run simultaneously — enable runtime_sentinel when you want a hard floor under the model's self-monitoring.

When prompted at the REPL:

  • y allow this one invocation
  • n deny this one invocation
  • a always allow this exact target (writes a regex to permissions.json)
  • s allow this tool for the rest of the session (not persisted)

You can pre-seed allowed_patterns in config.json:

{"tool": "Bash", "mode": "ask", "allowed_patterns": ["^git status$", "^ls( |$)"]}

Memory files

Anything that needs to survive rolling-window eviction goes here.

  • The agent calls the WriteMemory tool with text and an optional category.
  • You call /note <text> in the REPL or mnemara note --instance <name> <text> from the shell.

Format: append-only Markdown, one block per note:

## [2026-04-27T18:32:01+00:00] insight

Worth remembering across sessions.

Named slots — persistent state primitive

Some state is neither a memory note (write-once, append-only) nor a turn in the rolling window (FIFO-evicted as the conversation grows). It's a single value that changes in place — current health, current location, current mood, the latest reading from a sensor, the value of a counter that ticks every frame.

Mnemara exposes a upsert_slot MCP tool for exactly that:

upsert_slot(label, role, content) -> row_id
  • If a row with pin_label == label already exists, the row is updated in place — same row_id, content replaced, timestamp refreshed.
  • If no such row exists, a new pinned row is inserted.

Slot rows are pinned. They never get FIFO-evicted from the rolling window, no matter how long the session runs. The context viewer shows them with a 📌 [slot: <label>] badge so you can see the current state of the world at a glance.

Why this matters: without upsert_slot, an agent with mutable state (a game character with HP, a robot reporting battery level, a panel tracking the latest deploy status) has two bad options. Either it append_turn every tick — and the rolling window fills up with stale state-snapshots — or it relies on memory files, which require an extra read step and aren't always in context. Slots solve both: in context every turn, mutable in place, never bloating the window.

Game-character example. A character has health, stamina, and hunger. Each combat tick, the simulator updates the slots:

upsert_slot("health",   "system", "HP: 73 / 100")
upsert_slot("stamina",  "system", "stamina: 41 / 100")
upsert_slot("hunger",   "system", "hunger: 62 / 100  (eat soon)")

The character's panel sees current values at slot 0..N every turn. No state-resolution math, no ECS, no separate game-state DB read into context — the context is the state. Tick the slot, the next agent turn reads the new value.

Combine with role-doc swap for state that changes the agent's identity rather than its inventory: pointing role_doc_path at drunk.md for the duration of an inebriation status doesn't add a debuff, it makes the agent run as a drunk person until the condition lifts.

Context budget — agent-side eviction tools

The rolling window's row + token caps are the floor of context discipline. The agent itself has access to a set of in-process tools that let it compact its own history mid-session — useful for long sessions where most of the context is bulky tool-use audit data the model no longer needs.

Tool What it does
evict_thinking_blocks Strips thinking blocks from selected rows while preserving text + tool_use. Cheap, low-risk.
evict_tool_use_blocks Strips tool_use spec bodies (file paths, command strings, edit before/after content) from rows while preserving the audit shell. Often the highest-impact intervention — tool_use specs frequently dominate stored bytes in long sessions.
evict_write_pairs Stubs the bulky body content of Edit/Write/MultiEdit tool calls and their paired prior Read calls for the same file path. Audit trail intact ("I edited /foo/bar.py"); the kilobytes-per-block strings collapse to {file_path, _evicted: true}.

Concrete: an Edit tool call with old_string + new_string commonly carries 1–5 KB of inline content. A Write call with full file body is often much more. Multiplied across a long session, that becomes the majority of stored bytes. The actual change persists on disk; the in-context audit body doesn't need to.

The agent decides when to call these. The role doc is the right place to encode the policy ("when the rolling window is more than 80% full, call evict_write_pairs on completed edit turns before doing more work").

There is also an opt-in auto-evict-after-write config flag (auto_evict_after_write: true in config.json) that runs evict_write_pairs automatically after any turn that contained an edit/write tool call. Off by default; opt in per instance if you've decided that's the policy you want.

MCP wire-through

When mcp_servers are configured, Mnemara hands each entry to the Claude Agent SDK as a type: "stdio" server. The SDK spawns the subprocess, negotiates the MCP handshake, exposes the available tools to the model, and dispatches calls back to the server — Mnemara's permission policy still gates every tool call before it leaves the process.

The mcp__<server>__<tool> namespacing convention is owned by the Claude CLI that the SDK drives as a subprocess — the CLI exposes registered MCP tools to the model under that prefix and pattern-matches --allowedTools entries in the same shape. The SDK passes allowed_tools through verbatim; Mnemara conforms to the convention rather than defining it, and auto-allows the full mcp__<server>__* namespace for every configured server. You can override that with a more specific entry in allowed_tools if you want to gate individual tools:

"allowed_tools": [
  {"tool": "mcp__fetch__fetch", "mode": "ask", "allowed_patterns": []}
]

Add servers in config.json:

"mcp_servers": [
  {
    "name": "myserver",
    "command": "/usr/local/bin/my-mcp-server",
    "args": [],
    "env": {}
  }
]

Example — add web fetch (no extra install step):

"mcp_servers": [
  {
    "name": "fetch",
    "command": "uvx",
    "args": ["mcp-server-fetch"],
    "env": {}
  }
]

uvx downloads and caches mcp-server-fetch on first use; no separate install. The model can then call mcp__fetch__fetch with {"url": "..."}.

MCP server crashes are logged to debug.log (and the server's own stderr). As a fallback, remove the entry from mcp_servers and restart the session.

Graph backend (Kuzu) + sleep/replay primitive

Two co-evolving features. The graph captures relational structure between memories and entities; replay exploits that structure on each consolidation pass.

Graph backendgraph/ directory holding a Kuzu property graph. Two tables: Node(id, label, properties JSON, created_at) and Edge(FROM Node TO Node, id, relationship, properties JSON, created_at). Six tools registered:

graph_add_node(label, properties_json) -> id
graph_add_edge(from_id, to_id, relationship, properties_json) -> id
graph_query(cypher) -> rows
graph_neighbors(node_id, depth=1) -> adjacent nodes
graph_match(pattern_json) -> nodes matching {label, properties_subset}
graph_shortest_path(from_id, to_id) -> list of node ids

Auto-edge hooks fire on every write_memory (with structured applies_to) and wiki_write (frontmatter tags:). All wrapped in try/except — graph failure never fails the primary write.

Lazy: Kuzu is not opened until the first graph tool call. If Kuzu is absent or the DB is corrupt, every tool returns {"ok": false, "error": "Graph backend unavailable: …"} and the rest of the system keeps working. Off-switch: graph_enabled: false in config.

Sleep / replay primitivemnemara replay --instance <name>. Seven phases:

  1. Load atoms from memory/*.md over the last --days (default 7).
  2. Cluster atoms via RAG similarity. Atoms within distance 0.35 cluster; --threshold (default 3) sets the minimum count to count as a pattern.
  3. Augment patterns with graph structure — frequently-co-occurring entities from applies_to edges; causal phrasing in member text.
  4. For patterns not already covered by an existing wiki page, draft a proposal at wiki_proposals/<slug>.md.
  5. Archive near-duplicate atoms (distance < 0.10) into memory/archive/. Never deletes.
  6. When self_observation atoms cluster, draft a role-amendment proposal at role_proposals/<ts>_replay-<slug>.md.
  7. Write a sleep digest at sleep/YYYY-MM-DD.md with counts and pointers.

Default behavior is dry-run. Pass --apply to actually write proposals, archive duplicates, and emit the digest.

Multi-backend memory (wiki + RAG)

Three memory surfaces write together. The agent picks which surface to read from given the kind of recall it needs.

Memory filememory/YYYY-MM-DD.md. Append-only, chronological.

Wikiwiki/<slug>.md. Slash-allowed slugs (e.g. replay_policy, patterns/loader_traps). Plain markdown, optional frontmatter, no schema.

wiki_read(path)
wiki_write(path, content, mode='replace')  # 'replace'|'append'
wiki_list(prefix='')

RAGindex/ (LanceDB), embeddings via Ollama nomic-embed-text (768-dim).

rag_index(text, kind='manual', source_path='', category='')
rag_query(question, k=5, kind=None)

Write-to-all consolidation: every write_memory call also rag_indexes the content. Every wiki_write also indexes itself. If category starts with wiki/, write_memory ALSO writes the body to wiki/<rest>.md.

Setup for RAG:

ollama pull nomic-embed-text  # one-time; ~270MB
# Ollama must be running on http://localhost:11434

If Ollama is unreachable or LanceDB import fails, RAG tools return "RAG backend unavailable: <reason>" and memory + wiki keep working.

Architecture note

Mnemara is a thin runtime around the Claude Agent SDK. The SDK runs the model and its native tools (Bash/Read/Edit/Write); Mnemara owns:

  • The persistent turn store (turns.sqlite).
  • The role doc, re-read every call as system_prompt.
  • The rolling-window transcript serialized into each turn's prompt (the SDK is stateless per query()).
  • The permission policy (mediated via the SDK's can_use_tool callback).
  • The memory/wiki/RAG/graph backends and the replay consolidation pass.

Scope: single-instance runtime

Mnemara is per-instance: one role doc, one rolling window, one config, one set of files under ~/.mnemara/<instance>/. That's deliberate.

If you want to run multiple Mnemara instances with shared coordination — a producer panel handing tasks to engineer panels, a watchdog instance monitoring others, a researcher and a writer running side-by-side — that's a multi-agent orchestration layer that lives above Mnemara, not inside it. Mnemara is the per-instance runtime each panel runs on; the harness that spawns, coordinates, and arbitrates between panels is a separate concern.

We don't ship that orchestration harness publicly. The reason is design, not omission: a generic multi-agent harness has too many opinions (scheduling? message-passing? leader election? failure recovery?) to be useful as one-size-fits-all. Build your own thin wrapper around the programmatic-use surface — Mnemara is small enough that "spawn N AgentSessions and route messages between them" is real code you can write in an afternoon for the specific shape of orchestration your project needs.

Peer messaging

Mnemara can poll a SQLite peer-message database on a background timer and deliver incoming messages as agent turns — enabling autonomous coordination between multiple running Mnemara panels without human relay.

The feature is disabled by default. Enable it by setting peer_poll_enabled = true in your instance config.

Required schema

Your peer-message SQLite file must expose a table called returns with at least these columns:

Column Type Notes
id INTEGER PK AUTOINCREMENT watermark anchor
agent_role TEXT sender identity string
recipient_role TEXT or NULL NULL = broadcast to all watchers
task_id TEXT or NULL optional topic label
payload_json TEXT JSON-encoded message body
status TEXT 'pending' for new rows, 'done' after ack
submitted_at TEXT ISO timestamp

Mnemara reads pending rows matching the configured sender roles or explicitly addressed to this panel, delivers them as a single batched agent turn, and writes status='done' + completed_at when silently auto-acking protocol-noise messages.

Config example

{
  "peer_poll_enabled": true,
  "peer_db_path": "/path/to/peer_messages.db",
  "peer_poll_roles": "panel-a,panel-b",
  "peer_poll_interval_seconds": 30,
  "peer_poll_ack_tool": "my_server__ack_message",
  "peer_poll_submit_tool": "my_server__send_message"
}

peer_poll_ack_tool and peer_poll_submit_tool are the MCP tool names your peer-message system exposes. When empty (the default), the injected turn instruction uses generic prose describing what the agent should do; set them to use specific tool names so the agent can call them directly.

Behavior

  • Detection (every peer_poll_interval_seconds): pure SQLite read, zero token cost.
  • Processing (when next idle): all pending rows in one batched LLM turn — N messages = 1 API call.
  • peer_poll_batch: false: turn-by-turn mode for small/local models or large-context peer messages.
  • [⚡ Inbox: ON/OFF] button: toggle delivery live from the TUI without restarting the panel.
  • Silent auto-ack: protocol-noise message types (configurable via peer_poll_silent_types) are acknowledged without an LLM turn.

Best practices and pitfalls

This is in-process panel coordination over a shared SQLite file, not a distributed message bus. The mechanics matter, especially for autonomous panels with no human-in-the-loop to catch coordination bugs.

Make a peer-message send the terminal action of an agent turn. Sending is asynchronous — the recipient doesn't see your message until their next poll, which can be tens of seconds later. An agent that sends mid-turn and keeps working is either doing prep that could have happened before the send, or speculating about a reply that hasn't been authored yet. The clean discipline:

Verify, prepare, send, stop. When an agent pings a peer, the send should be the last tool block of the turn. The agent then yields control and waits for the reply to arrive on a future turn.

Encode this rule in the role doc that drives any panel using peer messaging — autonomous loops without it tend to compound work on half-stale state.

Drain inbound messages in batches, not one-at-a-time. When several pending rows are addressed to a panel, ack-all and reply-all in the same terminal block. Splitting an inbox across N turns fragments context and burns tokens for no benefit.

Always filter polls by recipient_role. Without the filter, multiple watchers race for every pending row — including messages intended for other panels. With the filter, each panel reads only what's addressed to it. Race-free.

Crossed messages are normal; design for them. Two panels can author replies that cross in flight. A reply you receive may have been authored without visibility into your most recent message. When a reply looks stale, check timestamps and referenced row IDs before acting on it. Sometimes the right move is to flag the cross and re-synchronize; sometimes it's to drop a confirmation-of-confirmation that carries no new information.

Acknowledge what you process. Don't acknowledge acknowledgments. Unacked rows accumulate and slow the queue. But "I received your acknowledgment" is noise that, if both sides send it, becomes an infinite ping-pong loop. Reply only when the reply carries new content.

Local-only by design. This system has no authentication, no encryption, no rate limiting, and no audit log. The SQLite file should live on the same machine as every panel reading from it. If you need cross-machine messaging, route through a real message bus — peer_poll is not a substitute.

The schema is part of your contract. If you change the returns table shape, every panel reading from it must be updated in lockstep. Treat schema changes the way you'd treat changes to a public API.

Programmatic use

The CLI is the primary surface, but Mnemara is also a regular Python library you can embed in your own code. See examples/programmatic_use.py for a minimal embed: initializes an instance, configures a role doc, drives a turn, and inspects the rolling window — about 60 lines.

ANTHROPIC_API_KEY=sk-ant-... python examples/programmatic_use.py

Troubleshooting

  • Auth errors — confirm ANTHROPIC_API_KEY is set, or that claude CLI is installed and claude login has been run as a fallback.
  • Role doc not loading — Mnemara warns to stderr and uses an empty system prompt; the REPL stays alive. Check debug.log for the path that failed.
  • MCP server crashes — check debug.log and the server's own stderr. As a fallback, remove the entry from mcp_servers.
  • Window eviction surprisesmnemara show --instance <name> prints the current window. The rolling window keeps the last max_window_turns rows; long tool-use turns count as one row but can carry many content blocks.
  • Token errors — if a long role doc + window overruns the model context, drop max_window_turns or split the role doc.

Running the tests

pip install -e ".[dev]"
pytest -q tests/

Tests do not call the network — they cover the store, config, permissions, and the file tools.

Acknowledgments

Built by Michael Anderson with Dave Moore.

License

MIT. See LICENSE.

Written by

Claude Aethon Autopoiesis 1.3.3.7

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

mnemara-0.17.1.tar.gz (222.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

mnemara-0.17.1-py3-none-any.whl (127.2 kB view details)

Uploaded Python 3

File details

Details for the file mnemara-0.17.1.tar.gz.

File metadata

  • Download URL: mnemara-0.17.1.tar.gz
  • Upload date:
  • Size: 222.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for mnemara-0.17.1.tar.gz
Algorithm Hash digest
SHA256 b8eb125c42759f0baccb23f06a71b7011ce76901913721f08b1654d5ff673154
MD5 756187eac96d30e8573d43e9cfd0e164
BLAKE2b-256 8c030ba32430327705d378f651f959e313dc788c74ae32d22d5bb6e301044915

See more details on using hashes here.

Provenance

The following attestation bundles were made for mnemara-0.17.1.tar.gz:

Publisher: release.yml on mekickdemons-creator/mnemara

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file mnemara-0.17.1-py3-none-any.whl.

File metadata

  • Download URL: mnemara-0.17.1-py3-none-any.whl
  • Upload date:
  • Size: 127.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for mnemara-0.17.1-py3-none-any.whl
Algorithm Hash digest
SHA256 067168328ea9a2304675423fb45f33f0074d9ea89b9ba79f30467cc0655fe075
MD5 448ccb41b50563acae576f5001f13071
BLAKE2b-256 5b3fc5fcc58c932b5fd54e87c9f2896ab88608befddf9232f06c2c732dbce889

See more details on using hashes here.

Provenance

The following attestation bundles were made for mnemara-0.17.1-py3-none-any.whl:

Publisher: release.yml on mekickdemons-creator/mnemara

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page