# Hermes Memory System Architecture (2026-07-14) ## Overview Hermes has two persistent memory stores injected into the system prompt: - **MEMORY.md** — agent's personal notes (environment facts, infrastructure topology, tool quirks). Default limit: 2,200 chars. - **USER.md** — what the agent knows about the user (preferences, style, communication rules). Default limit: 1,375 chars. Both files live at `~/.hermes/memories/` and use `§` (section sign) as entry delimiter: `\n§\n` between entries. ## Config ```yaml # ~/.hermes/config.yaml memory: memory_enabled: true user_profile_enabled: true memory_char_limit: 2200 user_char_limit: 1375 nudge_interval: 10 ``` Adjust limits: ```bash hermes config set memory.user_char_limit 3000 hermes config set memory.memory_char_limit 4000 # Takes effect on next /reset or gateway restart ``` ## System Prompt Injection Pipeline ### 1. Loading (once per session) `agent_init.py` creates a `MemoryStore` instance and calls `load_from_disk()`. This reads both `.md` files, splits on `§`, deduplicates entries, and creates a **frozen snapshot**. ### 2. Frozen Snapshot (prefix-cache invariant) ```python self._system_prompt_snapshot = { "memory": self._render_block("memory", sanitized_memory), "user": self._render_block("user", sanitized_user), } ``` The snapshot is **never mutated mid-session** — even when `memory()` tool calls write new entries to disk. This keeps the system prompt byte-stable, preserving the LLM prefix cache across all turns. ### 3. Injection Point `system_prompt.py` injects both blocks into the **volatile tier** of the system prompt (alongside date, model, provider): ```python if agent._memory_enabled: mem_block = agent._memory_store.format_for_system_prompt("memory") volatile_parts.append(mem_block) if agent._user_profile_enabled: user_block = agent._memory_store.format_for_system_prompt("user") volatile_parts.append(user_block) ``` ### 4. Rendered Format ``` ════════════════════════════════════════════ MEMORY (your personal notes) [100% — 2,217/2,200 chars] ════════════════════════════════════════════ entry1 § entry2 § ... ════════════════════════════════════════════ USER PROFILE (who the user is) [86% — 1,184/1,375 chars] ════════════════════════════════════════════ entry1 § entry2 ... ``` ### 5. Threat Scanning Each entry is scanned for prompt-injection patterns at load time (`_sanitize_entries_for_snapshot`). Matches are replaced with `[BLOCKED: ...]` in the snapshot, but the original text remains in the live file so the user can inspect and delete it. ## System Prompt Tiers ``` STABLE (byte-stable, cached for entire session) 1. SOUL.md — identity ("You are Hermes Agent...") 2. Tool schemas, platform hints, guardrails CONTEXT (cwd-dependent, changes between sessions) 3. .hermes.md / HERMES.md (walks to git root) 4. AGENTS.md (cwd only) 5. CLAUDE.md (cwd only) 6. .cursorrules (cwd only) (first match wins — only ONE loaded, ~20K char cap) VOLATILE (per-session, not cached) 7. MEMORY.md snapshot (memory_char_limit) 8. USER.md snapshot (user_char_limit) 9. External memory provider (optional, e.g. Hindsight) 10. Date, model, provider, session ID ``` ## Alternative Context Injection Points Beyond MEMORY.md/USER.md, additional context can be injected via: - **SOUL.md** (`~/.hermes/SOUL.md`) — global identity/personality, always loaded, no char limit - **AGENTS.md** (in repo root) — project conventions, ~20K char cap, loaded when cwd is the repo - **CLAUDE.md / .cursorrules** — same as AGENTS.md, first-match-wins - **.hermes.md** — walks up to git root, highest priority among context files ## Live State vs Snapshot | Aspect | Snapshot (System Prompt) | Live State (Tool Calls) | |--------|--------------------------|--------------------------| | When read | Once at session start | Every `memory()` call | | When written | Never (read-only) | Every `memory()` call → disk | | Prefix cache | Byte-stable for session | N/A (not in prompt) | | Changes visible | Next `/reset` or restart | Immediately in tool response | ## Key Insight Memory writes via the `memory()` tool are **not visible in the current session's system prompt** — they persist to disk but the snapshot was already frozen. This is by design: mutating the system prompt mid-session would invalidate the prefix cache and multiply API costs. ## Source Files - `tools/memory_tool.py` — `MemoryStore` class, file I/O, rendering - `agent/agent_init.py` lines 1199-1218 — initialization - `agent/system_prompt.py` lines 426-435 — injection into volatile tier - `agent/prompt_builder.py` lines 1921-1968 — context files (AGENTS.md etc.) - `agent/coding_context.py` — project detection, context file discovery