# 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
