7.0 KiB
Реализация: 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.mdtarget == "memory"иself.thread_keyзадан →memories/threads/<key>/MEMORY.mdtarget == "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: <gateway_session_key>(вместоMEMORY (your personal notes)) когда thread_scoped включён и есть ключ.
После memory блока вставляется отдельный блок MEMORY INSTRUCTION (с опциональным for thread: <key>), содержащий кастомную инструкцию из .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)
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 instructionhermes-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 versatest_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/threadtest_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 compattest_snapshot_reflects_thread_scoped_path— форматирует snapshot из thread файлаtest_snapshot_from_thread_and_global_are_independent— не подхватывает global entrytest_drift_guard_with_thread_key— детекция внешней модификации на thread файлеtest_load_time_sanitization_with_thread_key— poisoned entry блокируется в thread snapshottest_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.pyschema/MEMORY_SCHEMA — не меняли, кастомная инструкция в system prompt- External memory providers (honcho/mem0) — не трогали