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

123 lines
7.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Реализация: 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)
```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) — не трогали