# Hermes-Agent 上下文压缩与记忆机制参考

> 项目位置：`backend/hermes-agent/`（第三方仓库 `NousResearch/hermes-agent` 的本地克隆，已通过父项目 `.gitignore` 排除）
> 文档目的：记录 Hermes-Agent 内置的上下文压缩（context compression）与记忆（memory）两大子系统的实现机制、配置项、与 Bridge 模式的实际关系，便于后续评估适配成本、规划启用方案。
> 评估时点：2026-08-11（HEAD `f4604f89`）

---

## 0. 一句话定位

Hermes-Agent 的上下文管理由**两条独立子系统**组成：

- **上下文压缩**——会话内消息历史的 token 治理。当 prompt token 超过阈值时，启动一个 5 阶段流水线（裁剪旧工具结果 → 保护头尾 → LLM 总结中段），把摘要写回 messages 列表替换原中段。
- **记忆机制**——跨会话的持久化。包括四层：会话内 messages、跨会话 Markdown 文件（`MEMORY.md` / `USER.md`）、Skills 库、外部 Memory Provider 插件（Honcho / Mem0 / RetainDB 等）。

**与 Bridge 模式的关键事实**：本项目通过 `backend/hermes-bridge/hermes_bridge.py:228` 显式传入 `skip_memory=True`，**完全禁用了所有跨会话记忆机制**；上下文压缩则保持默认启用状态。

---

## 第一部分：上下文压缩机制

### 1.1 触发条件

**判断入口**：`ContextCompressor.should_compress_info()`

> `agent/context_compressor.py:2554-2585`

```python
def should_compress_info(self, prompt_tokens: int = None) -> "tuple[bool, str | None]":
    tokens = prompt_tokens if prompt_tokens is not None else self.last_prompt_tokens
    if tokens < self.threshold_tokens:
        return False, None
    if self._automatic_compression_blocked():
        return False, self._compression_block_reason() or "blocked"
    return True, None
```

**触发依据：基于 token 阈值**，不是消息条数，不是 LLM 自主决定。

- 比较对象：`prompt_tokens` 与 `threshold_tokens`
- `prompt_tokens` 两个来源：
  - 粗估（请求前）：`estimate_messages_tokens_rough(messages)`，字符数 / 4
  - 精确（响应后）：API 返回的 `usage.prompt_tokens` 写入 `last_prompt_tokens`（`conversation_loop.py:3272`）

### 1.2 阈值与默认值

| 参数 | 默认值 | 含义 | 源码位置 |
|---|---|---|---|
| `threshold_percent` | `0.50` | 触发比例（占上下文窗口） | `context_compressor.py:2208` |
| `_MIN_CTX_TRIGGER_RATIO` | `0.85` | 触发比例硬上限 | `context_compressor.py:2090` |
| `_SMALL_CTX_THRESHOLD_PERCENT` | `0.75` | 小上下文地板（窗口 < 512K 时强制 ≥75%） | `context_compressor.py:664` |
| `_SMALL_CTX_CONTEXT_FLOOR` | `512K` | "小上下文"的判定门槛 | `context_compressor.py` |
| `threshold_tokens_cap` | `None` | 绝对 token 上限（设置后取 min） | `agent_init.py:1985-1992` |

**`threshold_tokens` 计算公式**：`context_length × threshold_percent - max_tokens`（保证不超过 85% 窗口，并预留回答空间），见 `context_compressor.py:2166-2204`。

### 1.3 Anti-thrash 阻断（防止反复无效压缩）

> `agent/context_compressor.py:2647-2726`

```python
def _automatic_compression_blocked_locally(self) -> bool:
    # 1. Summary LLM 失败冷却期内：不触发
    if self._summary_failure_cooldown_until - time.monotonic() > 0:
        return True
    # 2. 连续 2 次压缩无效（节省 <10%）或连续 2 次使用 fallback：不触发
    if (self._ineffective_compression_count >= 2
            or self._fallback_compression_streak >= 2):
        # 300 秒后允许一次"试探性"压缩，失败则再次阻断
        return True
    return False
```

阻断时通过 `agent._warn_context_overflow_blocked(...)` 向用户告警（`conversation_loop.py:2077-2087`）。

### 1.4 压缩流水线（5 阶段）

> `agent/context_compressor.py:5942-5977`（算法注释）

```
Algorithm:
  1. Prune old tool results (cheap pre-pass, no LLM call)
  2. Protect head messages (system prompt + first exchange)
  3. Find tail boundary by token budget (~20K tokens of recent context)
  4. Summarize middle turns with structured LLM prompt
  5. On re-compression, iteratively update the previous summary
```

#### 阶段 1：旧工具结果裁剪（无 LLM 调用）

> `context_compressor.py:2732-3005`，四遍扫描：

- **Pass 1**：相同内容去重，旧副本替换为 `"[Duplicate tool output — same content as a more recent call]"`（`context_compressor.py:2819-2843`）
- **Pass 2**：超过 `min_prune_chars`（默认 200）的旧 tool result 替换为一行摘要，如 `[terminal] ran \`npm test\` -> exit 0, 47 lines output`（`_summarize_tool_result`，`context_compressor.py:1128-1187`）
- **Pass 3**：截断超过 500 字符的 `tool_call.arguments` JSON（`context_compressor.py:2906-2924`）
- **Pass 4**：尾部仍超 `protect_tail_tokens × 1.5` 时，对尾部内的 bulky tool 输出降级（`context_compressor.py:2941-3005`）

#### 阶段 2/3：边界确定

> `context_compressor.py:6054-6059`

```python
compress_start = self._protect_head_size(messages)         # 头部边界
compress_start = self._align_boundary_forward(messages, compress_start)
compress_end = self._find_tail_cut_by_tokens(messages, compress_start)  # 尾部边界
```

- **头部**：`_protect_head_size`（`context_compressor.py:4682-4705`）= system prompt（若有）+ `protect_first_n` 条非 system 消息
- **尾部**：`_find_tail_cut_by_tokens`（`context_compressor.py:5026-5100`）从末尾倒推累计 token，直至达到 `tail_token_budget`，消息数下限 `min(protect_last_n, _MAX_TAIL_MESSAGE_FLOOR=8)`

#### 阶段 4：中段 LLM 总结

> `context_compressor.py:3469-3498`、`3765-3879`

- 通过 `auxiliary_client.call_llm`（`context_compressor.py:3813`）调用独立"摘要模型"（可配置 `auxiliary.compression.provider/model`）
- **结构化模板**：`Goal / Active Task / In Progress / Decisions / Files / Completed Actions / Pending / Remaining Work` 等字段（`_summarizer_preamble`，`context_compressor.py:3637-3648`）
- **迭代更新**：已存在 `_previous_summary` 时改用增量更新提示词（`context_compressor.py:3479-3480`）
- **输入上限**：`_SUMMARY_INPUT_MAX_CHARS = 160_000`（约 40K token），超出则头尾保留、中段省略（`context_compressor.py:395`）
- **输出无 max_tokens 上限**：wire 上显式不加 `max_tokens`（`context_compressor.py:3776-3785`），`max_summary_tokens` 仅用于内部预算（上限 `_SUMMARY_TOKENS_CEILING = 10_000`）
- **剥离 think 块**：摘要 LLM 输出会经过 `strip_think_blocks`，避免思考模型（MiniMax / DeepSeek / QwQ）的 `<think>...</think>` 污染摘要（`context_compressor.py:3854-3863`）

### 1.5 被压缩的内容

| 消息范围 | 处理方式 |
|---|---|
| **system prompt** | 永不压缩（始终在头部保护） |
| **头部 `protect_first_n` 条**（默认 3） | 永不压缩；首次压缩后衰减为 0，避免早期消息"化石"（`context_compressor.py:4693-4700`） |
| **尾部 `protect_last_n` 条**（默认 20，实际由 token 预算主导，下限 8） | 永不压缩 |
| **中段所有消息**（user / assistant / tool） | 进入 LLM 总结输入，最终被摘要消息替换 |

**工具调用结果**是压缩的重点对象，三层处理：
1. 预压缩裁剪（阶段 1）
2. LLM 总结输入：`_serialize_for_summary` 把 tool 消息序列化为文本
3. 图片剥离：`_strip_historical_media`（`context_compressor.py:6686-6692`）把最新图片之前的所有图片 part 替换为文本占位符

### 1.6 压缩结果如何使用

**完全替换原消息**，不附加。`conversation_compression.py:2782` 在调用前用 `copy.deepcopy` 保存原消息：

```python
messages_before_compression = copy.deepcopy(messages)
...
compressed = compress_fn(messages, **compress_kwargs)
```

中段 N 条 → 1 条（或合并入尾部首条）摘要消息。

#### 摘要消息的格式

> `context_compressor.py:6564-6614`

**角色由交替规则动态决定**（user / assistant），保证 OpenAI 兼容后端不会因角色连续而 400。强制规则：若 session 中没有任何真实 user 消息，摘要必须用 `role="user"`。

消息额外带进程内元数据（不上 wire）：

```python
{
    "role": summary_role,
    "content": summary,
    COMPRESSED_SUMMARY_METADATA_KEY: True,                          # "_compressed_summary"
    COMPRESSED_SUMMARY_HAS_USER_TURN_KEY: bool(...),
}
```

摘要文本末尾强制追加：

```
--- END OF CONTEXT SUMMARY — respond to the message below, not the summary above ---
```

system prompt 也会被追加一段说明：`[Note: Some earlier conversation turns have been compacted into a handoff summary...]`（`context_compressor.py:6419`）。

### 1.7 原始消息的保留 / 回滚

| 阶段 | 保留情况 |
|---|---|
| 压缩过程中 | `messages_before_compression` 用于异常回滚（`conversation_compression.py:2858-2905`） |
| 提交后 `in_place=true`（默认） | 同一 `session_id`，直接覆盖，**原始中段消息从内存与 DB 中消失** |
| 提交后 `in_place=false` | 创建子 session，父 session 保留原始消息作为审计轨迹 |

### 1.8 失败降级（4 层）

| 层级 | 触发场景 | 行为 | 源码 |
|---|---|---|---|
| **1. 模型回退** | aux 模型未找到（404/503）/ 超时 / JSON 解析失败 / 流式中断 | 切到主模型重试 | `context_compressor.py:3965-3984` |
| **2. 冷却期** | 任意 LLM 失败 | 600 秒（无 provider 配置）或 60→300→900 秒阶梯（超时），期间 `should_compress` 返回 False | `context_compressor.py:627、1935-1951、4025-4028` |
| **3. 静态 fallback** | LLM 总结失败但未 abort | 从被丢弃的中段本地提取连续性锚点（用户问句、assistant 动作、工具名、文件路径、错误文本），组装成结构化文本（上限 8000 字符） | `context_compressor.py:6429-6454、3203-3332` |
| **4. 整体 abort** | 鉴权失败（401/403）/ 网络断连 / `abort_on_summary_failure=true` | 保留原消息不变，向用户告警 | `context_compressor.py:6354-6402、conversation_compression.py:2947-2956` |

### 1.9 与"思考过程"的关系

**压缩过程不产生 `thinking_callback` 输出**。

证据：在 `conversation_compression.py` 中 grep `thinking|on_reasoning|stream_callback|reasoning_callback` **无任何匹配**。压缩的状态通知走独立通道 `agent._emit_status`（用于 UI "压缩中"提示），与 `thinking_callback` 是两个不同的回调。

前端能看到：
- 压缩开始 / 完成的状态通知（走 `status_callback` → SSE）
- 压缩后插入对话的摘要消息本身（带 `_compressed_summary` 元数据，前端可据此特殊渲染）

前端看不到：
- 压缩 LLM 的思考过程（`<think>` 块已被剥离）

---

## 第二部分：记忆机制

### 2.1 四层记忆结构

| 层级 | 类型 | 存储 | 生命周期 |
|---|---|---|---|
| 1 | 短期（会话内） | `agent.messages` 内存 + SQLite `state.db` | 会话内累加；超限时由压缩摘要后切到新 session_id |
| 2 | 中期（跨会话内置） | `$HERMES_HOME/memories/MEMORY.md` 与 `USER.md`（Markdown） | 跨会话持久，写入即时落盘 |
| 3 | 长期（外部 provider 插件） | Honcho / Mem0 / RetainDB / Supermemory 等后端 | 由插件决定，按 user_id / chat_id 隔离 |
| 4 | 技能库（Skills） | `$HERMES_HOME/skills/<name>/`（每个技能一个目录） | 跨会话持久，由后台 review 沉淀 |

### 2.2 短期记忆（会话 DB）

- **存储位置**：内存中的 `agent.messages` 列表 + 磁盘 SQLite SessionDB
- **默认路径**：`hermes_state.py:239`
  ```python
  DEFAULT_DB_PATH = get_hermes_home() / "state.db"
  ```
- **生命周期**：会话内累加；上下文超限时由 `conversation_compression.py` 摘要后切到新 `session_id`（rotate）

### 2.3 中期记忆（MEMORY.md / USER.md）

> `tools/memory_tool.py:1-24`（模块 docstring）

```
Provides bounded, file-backed memory that persists across sessions. Two stores:
  - MEMORY.md: agent's personal notes and observations (environment facts, project
    conventions, tool quirks, things learned)
  - USER.md: what the agent knows about the user (preferences, communication style,
    expectations, workflow habits)

Both are injected into the system prompt as a frozen snapshot at session start.
Mid-session writes update files on disk immediately (durable) but do NOT change
the system prompt -- this preserves the prefix cache for the entire session.
The snapshot refreshes on the next session start.
```

**关键设计**：
- 会话开始时把 MEMORY/USER 快照注入 system prompt
- 会话内的写入立即落盘，但**不更新 system prompt**——保 prefix cache
- 下一会话开始时刷新快照

**字符上限**（默认值）：

> `tools/memory_tool.py:165-169`

```python
def __init__(self, memory_char_limit: int = 2200, user_char_limit: int = 1375):
```

- MEMORY: 2200 字符（≈800 token）
- USER: 1375 字符（≈500 token）

**存储路径**：

> `tools/memory_tool.py:53-67`

```python
def get_memory_dir() -> Path:
    """Return the profile-scoped memories directory."""
    return get_hermes_home() / "memories"

MEMORY_BLOCK_HEADERS = {
    "memory": "MEMORY (your personal notes)",
    "user":   "USER PROFILE (who the user is)",
}

ENTRY_DELIMITER = "\n§\n"
```

**写入用原子 temp-file + rename**（`tools/memory_tool.py:863-876`），并用 `fcntl` / `msvcrt` 做跨进程文件锁（`_file_lock`，`tools/memory_tool.py:278-320`）。

### 2.4 长期记忆（外部 Memory Provider 插件）

> `agent/memory_provider.py:1-31`

```python
"""Abstract base class for pluggable memory providers.

Memory providers give the agent persistent recall across sessions.
The MemoryManager enforces a one-external-provider limit to prevent
tool schema bloat and conflicting memory backends.
"""
```

**只允许一个外部 provider 同时运行**。已内置的插件位于 `plugins/memory/`：

| 插件 | 类型 |
|---|---|
| `honcho/` | 云端用户建模 |
| `hindsight/` | — |
| `mem0/` | — |
| `retaindb/` | SQLite write-behind 队列 + 语义检索 |
| `supermemory/` | — |
| `holographic/`、`byterover/`、`openviking/` | — |

**编排**：`agent/memory_manager.py` 的 `MemoryManager` 负责 prefetch / sync / shutdown。

### 2.5 技能库（Skills）

- 存放在 `$HERMES_HOME/skills/` 下
- 每个技能是一个目录，含 `SKILL.md` + 可选 `references/`、`templates/`、`scripts/`
- 由后台 review（`background_review.py`）把"用户纠正 / 教训"沉淀到 skill 中
- 触发条件基于工具迭代计数 `_iters_since_skill`

### 2.6 后台 Review（background_review.py）

> `agent/background_review.py:1-17`

```python
"""Background memory/skill review — fork the agent to evaluate the turn.

After every turn, ``AIAgent.run_conversation`` may call
:func:`spawn_background_review` to fire off a daemon thread that replays
the conversation snapshot in a forked :class:`AIAgent` and asks itself
"should any skill/memory be saved or updated?".  Writes go straight to
the memory + skill stores.  Main conversation and prompt cache are never
touched.
```

#### 触发方式：**事件触发**（不是定时任务）

> `agent/turn_context.py:582-590`

```python
# Track memory nudge trigger (turn-based, checked here).
should_review_memory = False
if (agent._memory_nudge_interval > 0
        and "memory" in agent.valid_tool_names
        and agent._memory_store):
    agent._turns_since_memory += 1
    if agent._turns_since_memory >= agent._memory_nudge_interval:
        should_review_memory = True
        agent._turns_since_memory = 0
```

> `agent/turn_finalizer.py:698-724`

```python
# Check skill trigger NOW — based on how many tool iterations THIS turn used.
_should_review_skills = False
if (agent._skill_nudge_interval > 0
        and agent._iters_since_skill >= agent._skill_nudge_interval
        and "skill_manage" in agent.valid_tool_names):
    _should_review_skills = True
    agent._iters_since_skill = 0

# External memory provider: sync the completed turn + queue next prefetch.
agent._sync_external_memory_for_turn(...)

# Background memory/skill review — runs AFTER the response is delivered
if final_response and not interrupted and (_should_review_memory or _should_review_skills):
    try:
        agent._spawn_background_review(
            messages_snapshot=list(messages),
            review_memory=_should_review_memory,
            review_skills=_should_review_skills,
        )
    except Exception:
        pass  # Background review is best-effort
```

**默认间隔**：
- Memory：每 10 轮用户消息触发（`nudge_interval: 10`）
- Skill：每 15 次工具迭代触发（`skills.creation_nudge_interval: 15`）

#### Fork 的安全隔离

Review fork 共享父 agent 的 `_memory_store`，但**显式 `skip_memory=True`** 避免触碰外部 provider，并 `_persist_disabled=True` 防止把审查 prompt 写入用户的 `state.db`：

> `agent/background_review.py:716-830`

```python
review_agent._memory_store = agent._memory_store
review_agent._memory_enabled = agent._memory_enabled
review_agent._user_profile_enabled = agent._user_profile_enabled
review_agent._memory_nudge_interval = 0
review_agent._skill_nudge_interval = 0

review_agent = AIAgent(..., skip_memory=True, ...)
review_agent._persist_disabled = True
review_agent._session_db = None
review_agent._session_json_enabled = False
```

工具白名单仅 `memory` / `skill_manage`（`background_review.py:893-909`），其他工具被拒绝。

### 2.7 context_references.py（与记忆无关）

`@file:` / `@folder:` / `@diff` / `@staged` / `@git` / `@url:` 的**用户消息内联展开机制**——不是 RAG 检索，是用户显式 @ 引用的精确解析。

> `agent/context_references.py:18-21`

```python
REFERENCE_PATTERN = re.compile(
    rf"(?<![\w/])@(?:(?P<simple>diff|staged)\b|(?P<kind>file|folder|git|url):(?P<value>...))"
)
```

- 直接读工作区文件 / 跑 git 命令 / 抓 URL
- 注入位置：附加到当前用户消息尾部（`--- Attached Context ---`）
- Token 保护：`hard_limit = context_length × 0.50`，`soft_limit = context_length × 0.25`
- 路径白名单 / 敏感文件黑名单（`_SENSITIVE_HOME_FILES`、`_SENSITIVE_HOME_DIRS`）

### 2.8 跨会话状态汇总

| 类型 | 保留内容 | 存储位置 |
|---|---|---|
| **MEMORY.md** | agent 个人笔记：环境事实、约定、工具怪癖、学到的教训 | `$HERMES_HOME/memories/MEMORY.md` |
| **USER.md** | 用户画像：偏好、沟通风格、期望、工作习惯 | `$HERMES_HOME/memories/USER.md` |
| **Skills** | "如何做这类任务"的类级技能库 | `$HERMES_HOME/skills/<name>/` |
| **state.db** | 完整会话历史（SQLite，可通过 `session_search` 工具检索） | `$HERMES_HOME/state.db` |
| **外部 provider** | Honcho / Mem0 / RetainDB 等后端自己的存储 | 各自后端 |

**写入方式**：
- MEMORY / USER：LLM 通过 `memory` 工具自主决策调用
- 后台 review：每 N 轮自动触发
- `session_search`：LLM 显式调用工具检索历史
- 外部 provider 的 `sync_turn`：每轮后台自动执行

### 2.9 隐私 / 安全机制

| 机制 | 实现 | 源码 |
|---|---|---|
| **写入前注入 / 渗出扫描** | `first_threat_message(content, scope="strict")` | `tools/memory_tool.py:83-88` |
| **加载时再扫** | 命中则替换为占位符（保留原文本到 live state 让用户能看到并删除） | `tools/memory_tool.py:242-276` |
| **写入审批门**（write_approval） | `stage`（暂存待批）/ `block` / `allow` 三档 | `tools/memory_tool.py:911-965` |
| **Drift 检测** | 检测外部 patch / shell append 污染，命中则备份 | `tools/memory_tool.py:807-861` |
| **CLI `/reset`** | `hermes memory` 子命令重置 | `hermes_cli/subcommands/memory.py`、`hermes_cli/memory_reset.py` |

---

## 第三部分：配置项汇总

### 3.1 上下文压缩（`compression.*`）

> `cli-config.yaml.example:425-556`、`agent/agent_init.py:1860-2009、2455-2499`

| YAML 字段 | 默认值 | 作用 |
|---|---|---|
| `enabled` | `true` | 总开关，`false` 完全关闭自动压缩 |
| `threshold` | `0.50` | 触发阈值占上下文比例 |
| `model_thresholds` | `{}` | 按模型子串匹配覆盖阈值，如 `"glm-5.2": 0.40` |
| `threshold_tokens` | `null` | 绝对 token 上限，与比例阈值取 min |
| `target_ratio` | `0.20` | 尾部保留比例 = threshold × ratio × context_length |
| `protect_first_n` | `3` | 头部额外保护消息数（首次压缩后衰减为 0） |
| `protect_last_n` | `20` | 尾部最小保护消息数（实际受 token 预算与 floor=8 约束） |
| `min_tail_user_messages` | `1` | 尾部至少保留的真实 user 消息数 |
| `max_attempts` | `3`（硬上限 10） | 单轮内压缩重试次数 |
| `in_place` | `true` | true=同 session 覆盖，false=分裂子 session |
| `abort_on_summary_failure` | `false` | true=LLM 总结失败时整体放弃压缩 |
| `proactive_prune_tokens` | `0`（禁用） | 独立的旧工具结果裁剪触发阈值 |
| `proactive_prune_min_result_chars` | `8000` | 工具结果裁剪的最小字符门槛 |
| `proactive_prune_min_reclaim_tokens` | `4096` | 裁剪必须至少回收这么多 token 才提交 |
| `progress_notices` | `false` | 是否在聊天平台显示压缩进度 |
| `idle_compact_after_seconds` | `0`（禁用） | 闲置 N 秒后下次回复前主动压缩 |
| `micro_compact` | `false`（opt-in） | 每轮滚动微压缩（实验性，默认关） |

**摘要模型独立配置** 在 `auxiliary.compression.provider / model`（默认 `"auto"`：OpenRouter → Nous Portal → 主模型）。

**是否可完全关闭**：可以。`compression.enabled: false`。运行时所有触发点都先检查 `agent.compression_enabled`（`conversation_loop.py:1966、2063、6360、6396`）。

### 3.2 记忆（`memory.*`）

> `cli-config.yaml.example:665-683`

| YAML 字段 | 默认值 | 作用 |
|---|---|---|
| `memory_enabled` | `true` | 关闭则不加载 MEMORY.md |
| `user_profile_enabled` | `true` | 关闭则不加载 USER.md |
| `memory_char_limit` | `2200` | MEMORY 字符上限（≈800 token） |
| `user_char_limit` | `1375` | USER 字符上限（≈500 token） |
| `nudge_interval` | `10` | 0=禁用后台 memory review 触发 |
| `flush_min_turns` | `6` | 0=禁用退出前 flush |
| `provider` | `""`（空字符串=禁用外部 provider） | 选哪个插件：`honcho` / `mem0` / `retaindb` / `supermemory` 等 |

**Skills**（同位置 yaml 第 785 行）：

| YAML 字段 | 默认值 | 作用 |
|---|---|---|
| `skills.creation_nudge_interval` | `15` | 0=禁用后台 skill review |

### 3.3 启用条件（双层 gate）

> `agent/agent_init.py:1655-1669`

```python
_memory_toolset_requested = "memory" in (agent.enabled_toolsets or [])
if not skip_memory or _memory_toolset_requested:
    mem_config = _agent_cfg.get("memory", {})
    agent._memory_enabled = mem_config.get("memory_enabled", False)
    agent._user_profile_enabled = mem_config.get("user_profile_enabled", False)
    agent._memory_nudge_interval = int(mem_config.get("nudge_interval", 10))
    if agent._memory_enabled or agent._user_profile_enabled:
        from tools.memory_tool import MemoryStore
        agent._memory_store = MemoryStore(
            memory_char_limit=mem_config.get("memory_char_limit", 2200),
            user_char_limit=mem_config.get("user_char_limit", 1375),
        )
        agent._memory_store.load_from_disk()
```

---

## 第四部分：Bridge 模式下的实际状态

### 4.1 关键事实：Bridge 显式禁用所有 Memory 机制

> `backend/hermes-bridge/hermes_bridge.py:219-231`

```python
kwargs = dict(
    base_url=effective_base_url or None,
    api_key=effective_api_key,
    model=effective_model or None,
    max_iterations=max_iterations,
    quiet_mode=True,
    tool_progress_mode="off",
    skip_context_files=True,
    load_soul_identity=False,
    skip_memory=True,           # ← 关键
)

agent = AIAgent(**kwargs)
```

`skip_memory=True` 的影响，对照 `agent_init.py:1673-1739`：

| 机制 | Bridge 模式下是否生效 | 原因 |
|---|---|---|
| **外部 Memory Provider** | **完全禁用** | `_memory_manager = None`（整段被 `if not skip_memory` 包裹） |
| **后台 memory review 触发** | **不触发** | `agent._memory_store` 默认值 `None` + `valid_tool_names` 无 `memory` + 三个 AND 条件全失效 |
| **后台 skill review 触发** | **不触发** | 同上 |
| **sync_all / queue_prefetch_all** | **不执行** | `_memory_manager` 为 None，`_sync_external_memory_for_turn` 早 return |
| **MEMORY.md / USER.md 加载** | **看 `_memory_enabled` 配置** | 若 `enabled_toolsets` 含 `"memory"`（默认 toolset 解析后含 memory），仍会建 store |
| **MEMORY/USER 的 LLM 工具调用** | **取决于 `memory_enabled` 默认值** | 默认 `False`（`mem_config.get("memory_enabled", False)`） |

### 4.2 Bridge 上下文压缩的实际状态

Bridge 没有传任何 `compression.*` 参数，走 AIAgent 默认：

- `compression.enabled = true`（启用）
- `compression.threshold = 0.50`（50% 上下文窗口）
- `compression.target_ratio = 0.20`
- 其他配置看 `$HERMES_HOME/config.yaml` 是否存在

**实际行为**：会话内消息累积到约一半上下文窗口时，自动触发压缩流水线。

### 4.3 启用完整 Memory 的步骤

1. **去掉 `skip_memory=True`**（或改为 `False`）
2. **在 `$HERMES_HOME/config.yaml` 配置**：
   ```yaml
   memory:
     memory_enabled: true
     user_profile_enabled: true
     memory_char_limit: 2200
     user_char_limit: 1375
     nudge_interval: 10
   ```
3. **外部 provider**（可选）：在 config.yaml 设 `memory.provider: honcho` + 配套 env（如 `HONCHO_API_KEY`）
4. **per-user 隔离**（可选）：传 `user_id` / `chat_id` / `gateway_session_key`，参考 `agent_init.py:1707-1723`

### 4.4 启用的潜在影响

| 影响项 | 评估 |
|---|---|
| **system prompt 长度** | MEMORY 2200 字符 + USER 1375 字符 ≈ 1300 token 永久占用上下文 |
| **prefix cache** | 会话内不变（设计上保证 cache 命中） |
| **后台 LLM 调用成本** | 每 10 轮触发一次 background review fork（额外 LLM 调用） |
| **磁盘空间** | `MEMORY.md` / `USER.md` / `skills/` 都很小（KB 级） |
| **隐私** | 用户对话内容会被沉淀到文件（已有注入扫描、写入审批、drift 检测三重防护） |
| **Bridge session 隔离** | Bridge 按 `session_id::hermes_home::model_id` 缓存 agent（最多 8 个 LRU），不同 session_id 不共享 memory store |

---

## 第五部分：核心文件清单

### 上下文压缩

| 文件 | 作用 |
|---|---|
| `agent/context_compressor.py` | 主实现：阈值判断、5 阶段流水线、4 层失败降级 |
| `agent/context_engine.py` | 基类，token 估算 |
| `agent/context_breakdown.py` | 上下文构成分析（用于诊断） |
| `agent/conversation_compression.py` | 压缩调用入口、回滚、用户告警 |
| `agent/conversation_loop.py:1972, 3272, 6360` | 触发点（请求前预检 + 响应后更新） |
| `agent/agent_init.py:1860-2009` | 配置加载与 `ContextCompressor` 实例化 |
| `agent/auxiliary_client.py` | 独立的摘要模型客户端 |
| `cli-config.yaml.example:425-556` | 配置项示例 |

### 记忆机制

| 文件 | 作用 |
|---|---|
| `tools/memory_tool.py` | 内置 `MEMORY.md` / `USER.md` 存储 + `memory` 工具 |
| `agent/memory_provider.py` | `MemoryProvider` ABC 接口 |
| `agent/memory_manager.py` | `MemoryManager`（编排 + prefetch / sync / shutdown） |
| `agent/background_review.py` | 后台 fork 自评 |
| `agent/turn_context.py:582-590` | Memory nudge 触发条件 |
| `agent/turn_finalizer.py:698-724` | Review spawn / sync_all 调用点 |
| `agent/agent_init.py:1655-1739` | Memory 初始化（双层 gate） |
| `agent/context_references.py` | `@file:` / `@diff` 等内联引用展开（与 memory 无关） |
| `plugins/memory/` | 外部 provider 插件目录 |
| `hermes_state.py:239` | `state.db` 默认路径 |
| `cli-config.yaml.example:659-683` | Memory 配置示例 |

### Bridge 模式

| 文件 | 作用 |
|---|---|
| `backend/hermes-bridge/hermes_bridge.py:228` | Bridge 显式 `skip_memory=True` |
| `backend/hermes-bridge/hermes_bridge.py:219-231` | agent 创建参数 |
