Files
obsidian-vault/personal/plans/thread-scoped-memory.md
T

7.0 KiB
Raw Blame History

Реализация: 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/<key>/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: <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 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_pathformat_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) — не трогали