# Реализация: Thread-scoped memory + custom memory prompt **Статус:** Реализовано ✅ ## Изменённые файлы ### 1. `tools/memory_tool.py` — MemoryStore с thread_key **`__init__`** — новый параметр `thread_key: Optional[str] = None`, сохраняется как `self.thread_key`. **`_path_for(target)`** — теперь instance method (был static): - `target == "user"` → всегда `memories/USER.md` - `target == "memory"` и `self.thread_key` задан → `memories/threads//MEMORY.md` - `target == "memory"` без thread_key → `memories/MEMORY.md` (глобальный fallback) **`load_from_disk()`** — использует `self._path_for("memory")` и `self._path_for("user")` вместо хардкода. ### 2. `agent/agent_init.py` — конфиг + проброс Новые поля на агенте: - `_memory_thread_scoped` — читается из `config.yaml: memory.thread_scoped` - `_memory_instruction` — читается из `config.yaml: memory.prompt_path` (.md файл) MemoryStore создаётся с `thread_key=_gateway_session_key` если `thread_scoped: true`. Кастомная memory instruction логируется: `Loaded custom memory instruction from ...`. ### 3. `agent/system_prompt.py` — вставка в system prompt В блок MEMORY добавляется: - Заголовок `MEMORY for thread: ` (вместо `MEMORY (your personal notes)`) когда thread_scoped включён и есть ключ. После memory блока вставляется отдельный блок `MEMORY INSTRUCTION` (с опциональным `for thread: `), содержащий кастомную инструкцию из .md файла. ### 4. `gateway/run.py` — уже пробрасывает `gateway_session_key` уже передаётся в `AIAgent.__init__` на строке ~17828. Никаких изменений не потребовалось. ### 5. `run_agent.py` — уже принимает Параметр `gateway_session_key` уже есть в `AIAgent.__init__`. Пробрасывается в `init_agent()` где записывается как `agent._gateway_session_key`. ## Конфиг (Whale) ```yaml memory: ... thread_scoped: true prompt_path: ~/.hermes/hermes-whale/review/memory_prompt.md ``` ## Файлы - **`~/.hermes/hermes-whale/review/memory_prompt.md`** — инструкция что запоминать (документы, команды, конфиги, статус проекта, решения). - После добавления нового пункта в список patch() не перенумеровывает — нужен второй clean patch. - **2026-06-24:** Добавлен пункт 2 — после загрузки Obsidian docs (skill_view, mcp_obsidian_read_note) извлекать ключевые факты в memory. ## Файловая структура на диске ``` ~/.hermes/hermes-whale/memories/ ├── MEMORY.md # глобальная (fallback для CLI/старых сессий) ├── USER.md # глобальная (всегда) └── threads/ ├── agent:main:webhook:webhook:webhook:whale/ │ └── MEMORY.md # память Whale ├── agent:main:zulip:stream:general:thread:123/ │ └── MEMORY.md # память конкретного треда └── ... ``` ## Коммиты - `hermes-agent`: `98cb69b50` — feat: thread-scoped memory + configurable memory instruction - `hermes-agent`: `4ebad4f69` — test: thread-scoped memory persistence, drift guard, snapshot, sanitization (+9 тестов, 142 строки) - `hermes-whale`: `736f8f3` — whale: enable thread-scoped memory and custom memory instruction ## Тесты 9 тестов в `tests/tools/test_memory_tool.py` (всего 76 в файле, 76/76 passed): **Persistence:** - `test_thread_scoped_memory_writes_separate_file` — global и thread пишутся в разные файлы - `test_user_stays_global_with_thread_key` — USER.md всегда глобальный, не залезает в `threads/` - `test_thread_and_global_are_independent_on_load` — загрузка thread не видит global entries и vice versa - `test_thread_key_none_falls_back_to_global` — backward compat: без thread_key пишет в `memories/MEMORY.md` **Snapshot:** - `test_snapshot_reflects_thread_scoped_path` — `format_for_system_prompt` берёт данные из thread-файла - `test_snapshot_from_thread_and_global_are_independent` — thread snapshot изолирован от global **Drift guard:** - `test_drift_guard_with_thread_key` — `_detect_external_drift` работает на thread-scoped MEMORY.md **Sanitization:** - `test_load_time_sanitization_with_thread_key` — poisoned entry в thread блокируется на уровне snapshot **Pitfalls:** - `pytest-timeout` плагин не установлен, но `pyproject.toml` содержит `addopts = "--timeout=30"`. Запуск падает с `unrecognized arguments`. Используй `-o "addopts="` для override. - Drift guard на thread: нужен блок > `memory_char_limit` (дефолт 2200), иначе `_detect_external_drift` не находит entry-size overflow. В тесте `"x" * 2300`. ## Тесты - **76/76 passed** (из них 9 новых для thread_key, добавлены в `4ebad4f69`) - **9 новых тестов:** - `test_thread_scoped_memory_writes_separate_file` — разные файлы для global/thread - `test_user_stays_global_with_thread_key` — USER.md не уходит в threads/ - `test_thread_and_global_are_independent_on_load` — не пересекаются при чтении - `test_thread_key_none_falls_back_to_global` — backward compat - `test_snapshot_reflects_thread_scoped_path` — форматирует snapshot из thread файла - `test_snapshot_from_thread_and_global_are_independent` — не подхватывает global entry - `test_drift_guard_with_thread_key` — детекция внешней модификации на thread файле - `test_load_time_sanitization_with_thread_key` — poisoned entry блокируется в thread snapshot - `test_already_blocked_entry_passes_through` — no double-wrap (расширен) - Запуск: `cd ~/.hermes/hermes-agent && source venv/bin/activate && python -m pytest tests/tools/test_memory_tool.py -v -o "addopts="` ## Неизменённое - `run_conversation` / `conversation_loop.py` — не трогали - `background_review.py` — наследует `_memory_store` от родителя, thread_key приходит автоматически - `tools/memory_tool.py` schema/MEMORY_SCHEMA — не меняли, кастомная инструкция в system prompt - External memory providers (honcho/mem0) — не трогали