docs(spec): ruflo memory H7 fix + advisory hook — design

Design for 3 deliverables (brainstorming output):
- D1: install standalone claude CLI to unblock hive-mind spawn --claude
- D2: fix H7 memory-persistence bug — patch getBridge() so memory ops
  use the persisting raw sql.js path instead of the non-flushing
  AgentDB-v3 bridge
- D3: UserPromptSubmit advisory hook injecting ruflo memory recall

cspell-words.txt +11 Russian IT-slang inflections used by the spec.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Дмитрий
2026-05-15 14:00:47 +03:00
parent 55696e5b40
commit a6649e4696
2 changed files with 394 additions and 0 deletions
+13
View File
@@ -1191,3 +1191,16 @@ poincaré
наслой
нормативке
рефреш
# ruflo memory H7 fix + advisory hook design (2026-05-15) — Russian IT-slang inflections
персистит
персистящий
персистят
инжектит
инжектить
инжектящий
флашит
незакоммиченная
бинаря
промпты
свежеустановленный
@@ -0,0 +1,381 @@
# Ruflo Memory H7 Fix + Advisory Hook — Design
**Дата:** 2026-05-15
**Статус:** Design (согласован в `superpowers:brainstorming`)
**Связанные документы:** `docs/superpowers/specs/2026-05-15-ruflo-integration-design.md`
(ruflo big-bang integration), memory `project_ruflo_integration.md`.
---
## 1. Цель и контекст
ruflo runtime активирован (daemon под PM2, hive-mind Queen + 9 agents, memory +
локальные embeddings `Xenova/all-MiniLM-L6-v2` 384-dim), но архитектурно остаётся
**parallel subsystem** — не интегрирован в интерактивную Claude Code-сессию: Claude
работает напрямую, ruflo daemon/hive-mind крутятся отдельным процессом рядом.
Цель — три деливерабла, приближающие ruflo к роли advisory-слоя над сессией:
- **D1.** Установить standalone `claude` CLI, чтобы `ruflo hive-mind spawn --claude`
мог запускать реальную агентскую работу on-demand.
- **D2.** Починить баг **H7**`ruflo memory` не персистит данные между
process-invocations.
- **D3.** Создать `UserPromptSubmit` advisory-хук, инжектящий релевантную
ruflo-память в каждый промпт интерактивной сессии.
### 1.1. Решения пользователя (приняты в brainstorming)
- Provider-ключ Anthropic в ruflo **не настраиваем** — непрерывный daemon-расход не
нужен. Реальная работа агентов — через `spawn --claude` on-demand, который
использует auth самого `claude` CLI (существующий Claude-план), а не отдельный
ruflo-ключ и не отдельный $-поток.
- H7 чиним подходом **«патч `getBridge()`»** (см. §4) — выбран из 4 вариантов как
единственный, верифицированный по исходнику.
- Порядок: D1 → D2 → D3.
### 1.2. Не-цели
- Настройка provider-ключа Anthropic в ruflo.
- Исправление самого AgentDB-v3 bridge или пакета `@claude-flow/memory` — H7 чиним
**обходом** bridge'а, не его починкой.
- Установка native `better-sqlite3` (рассмотрена как альтернатива §4.4, отклонена).
- Авто-store хук (PostToolUse / Stop), наполняющий память автоматически — память
наполняется вручную через `ruflo memory store` по уместности (YAGNI).
- Routing-классификация в хуке (`ruflo route` / Q-Learning) — Q-таблица на свежей
установке не обучена, сигнал пустой; хук инжектит только memory-recall.
- Косметический баг namespace `undefined` (`ctx.flags.namespace` приходит
`undefined` несмотря на `default: 'default'` в `commands/memory.js:38`) — отдельный
дефект, вне scope этого дизайна.
- Превращение ruflo в буквальный entry-point/control-flow над Claude — архитектурно
невозможно (`UserPromptSubmit`-хук умеет только инжектить контекст или блокировать
промпт, не передавать управление). Хук даёт «advisory», не «routing».
---
## 2. H7 — корневая причина (верифицировано по исходнику)
Симптом: `ruflo memory store -k K -v V` рапортует `[OK] Data stored successfully`
(384-dim вектор, ID), но `.swarm/memory.db` не меняется (mtime/size константны), а
`memory list`/`retrieve`/`search`/`stats` в последующих вызовах возвращают пусто
(`Total Entries: 0`).
Цепочка (`@claude-flow/cli` в глобальной установке ruflo, `dist/src/`):
1. `ruflo memory store``storeEntry()``memory/memory-initializer.js:1777`.
2. `storeEntry` зовёт `getBridge()``memory-initializer.js:1779`. `getBridge`
(`memory-initializer.js:71-84`) делает `await import('./memory-bridge.js')`
это **локальный sibling-модуль, импорт всегда успешен**`getBridge()`
практически всегда возвращает truthy bridge.
3.`bridge.bridgeStoreEntry(options)``memory-initializer.js:1781`.
4. `bridgeStoreEntry` (`memory/memory-bridge.js:534`) поднимает AgentDB-v3
`ControllerRegistry` через `import('@claude-flow/memory')`
(`memory-bridge.js:86`); на этой машине загрузка успешна → `bridgeAvailable = true`
(`memory-bridge.js:350`).
5. Вставка строки: `ctx.db.prepare(insertSql).run(...)`
`memory-bridge.js:593-594`. Код написан под **`better-sqlite3`** (синхронный
`.run()`, комментарий `memory-bridge.js:570`). Native-бинарь `better-sqlite3` на
этой машине не собран (нет build tools) → AgentDB падает на **sql.js (WASM,
in-RAM)** — отсюда строка «✅ Using sql.js» в каждом выводе `memory`-команд.
6. `bridgeStoreEntry` возвращает `{ success: true, id, ... }`
(`memory-bridge.js:602-610`) — **truthy, но нигде не экспортирует in-RAM
WASM-буфер на диск**: ни `db.export()`, ни `writeFileRestricted` в функции нет.
Контраст — raw-fallback `storeEntry` явно делает `const data = db.export();
writeFileRestricted(...)` на `memory-initializer.js:1858-1859`.
7. Раз `bridgeStoreEntry` вернул truthy — `storeEntry` делает ранний `return
bridgeResult` на `memory-initializer.js:1793`. **Корректный персистящий
raw-sql.js путь (`memory-initializer.js:1796-1875`) недостижим — он «затенён»
bridge'ом.**
8. Процесс CLI завершается → WASM-память освобождается → строка пропадает.
Следующий `ruflo memory <cmd>` — новый процесс, новый пустой in-RAM реестр.
`memory init` работает (создаёт `.swarm/memory.db` 144 KB) потому что
`initializeMemoryDatabase` пишет файл напрямую (`fs.writeFileSync` /
`writeFileRestricted` + `copyFileSync` в `.claude/memory.db`), не через bridge.
**Итог H7:** AgentDB-v3 bridge перехватывает каждую memory-операцию
(`store`/`get`/`list`/`search`/`delete` — все имеют паттерн `getBridge()`-first),
пишет в in-RAM sql.js и никогда не флашит на диск; рабочий персистящий raw-sql.js
путь затенён.
### 2.1. Гипотезы (systematic-debugging) — статус
| Гип. | Формулировка | Статус |
|---|---|---|
| A | `store` пишет в другой файл (stray `.db` / `ruvector.db`) | **Фальсифицирована** — `find` нашёл только `.swarm/memory.db` + `ruvector.db`; `stat` обоих через T0/T1/T2 вокруг `store` — не изменились ни разу |
| B | sql.js не экспортится на диск | **Уточнена и подтверждена** — no-export реален, но в `bridgeStoreEntry`, не в raw-пути (raw-путь экспортит корректно) |
| C | Нужен running memory-сервер | **Фальсифицирована** — `store` делает реальную in-process работу (embedding, ID) |
| D | namespace `undefined` ломает запись | **Фальсифицирована как корень** — отдельный косметический баг; namespace-слепой `list` тоже пуст, т.к. на диск ничего не пишется |
| E | Незакоммиченная транзакция | **Схлопывается в B** — у sql.js «commit» = `db.export()` + запись файла |
---
## 3. D1 — `claude` CLI + spawn-capability
### 3.1. Проблема
`ruflo doctor` сообщает «⚠ Claude Code CLI: Not installed». На машине Claude Code
работает через VSCode-расширение; отдельного бинаря `claude` на PATH нет (`claude
--version` → command not found). `ruflo hive-mind spawn --claude` запускает `claude`
Code CLI как подпроцесс (`--claude Launch Claude Code with hive-mind coordination
prompt`) → без бинаря путь «реальной работы агентов» недоступен.
### 3.2. Решение
1. `npm i -g @anthropic-ai/claude-code` — установить standalone `claude` CLI.
2. Верификация: `claude --version` возвращает версию; `ruflo doctor` больше не
выдаёт «Claude Code CLI: Not installed».
3. Smoke `spawn`: `ruflo hive-mind spawn --claude --no-auto-permissions` на
тривиальной задаче. Флаг `--no-auto-permissions` обязателен — по умолчанию
`spawn --claude` идёт с `--dangerously-skip-permissions` (default true),
spawned-агент пропускает все permission-промпты (автономно правит репозиторий) —
для CRM-проекта это недопустимо без явного решения.
### 3.3. Открытые вопросы D1
- **Auth `claude` CLI** — не верифицировано, подхватит ли свежеустановленный
standalone CLI существующие credentials из `~/.claude/` (общие с VSCode-расширением)
или потребует интерактивный `claude login`. Проверяется в ходе D1; если нужен
`login` — это user-action (интерактив, не автоматизируется).
- Реальный `spawn --claude` потребляет ёмкость существующего Claude-плана
пользователя — smoke делается на минимальной задаче.
---
## 4. D2 — Фикс H7: патч `getBridge()`
### 4.1. Суть
Сделать `getBridge()` в `memory-initializer.js` всегда возвращающим `null`. Тогда
`storeEntry`/`getEntry`/`searchEntries`/`listEntries`/`deleteEntry` пропускают
bridge-ветку и выполняют свои **raw-sql.js fallback-пути**, которые корректно
персистят (`db.export()` + `writeFileRestricted` — например
`memory-initializer.js:1858-1859` для `store`, `:2196-2197` для `get`,
`:2325-2326` для `delete`).
### 4.2. Патч
Файл: `<npm-root-g>/ruflo/node_modules/@claude-flow/cli/dist/src/memory/memory-initializer.js`.
Текущая функция (`:71-84`):
```js
let _bridge;
async function getBridge() {
if (_bridge === null)
return null;
if (_bridge)
return _bridge;
try {
_bridge = await import('./memory-bridge.js');
return _bridge;
}
catch {
_bridge = null;
return null;
}
}
```
Патч — вставка одной строки первой в тело функции:
```js
async function getBridge() {
return null; /* LIDERRA-H7-PATCH: bridge writes to in-RAM sql.js, never flushes — force raw persisting path */
if (_bridge === null)
```
Маркер `LIDERRA-H7-PATCH` служит признаком идемпотентности для re-apply-скрипта.
Остальное тело функции остаётся как мёртвый, но безвредный код (не переписываем —
минимизируем diff и риск рассинхрона с upstream при будущих сверках).
### 4.3. Re-apply-механизм
Патч лежит в `node_modules` **глобальной** установки ruflo — он не под git проекта
и **молча затирается** при `ruflo update` / `npm i -g ruflo`. Поэтому:
- Скрипт `tools/ruflo-h7-patch.mjs` (в репо проекта, под git):
- Резолвит путь к `memory-initializer.js`: `npm root -g` → кандидаты
`<root>/ruflo/node_modules/@claude-flow/cli/dist/src/memory/memory-initializer.js`
и `<root>/@claude-flow/cli/.../memory-initializer.js` (на случай hoist).
- Читает файл; если содержит маркер `LIDERRA-H7-PATCH` — no-op (идемпотентно).
- Если маркера нет — находит якорь `async function getBridge() {` и вставляет
строку патча сразу за ним; пишет файл обратно.
- Если якорь **не найден и маркера нет** — выходит с ненулевым кодом и сообщением
(upstream изменил функцию — патч пересмотреть вручную, не молчать).
- Флаг `--revert` — снимает патч (удаляет строку с маркером).
- Флаг `--check` — только проверка статуса (exit 0 patched / exit 1 unpatched),
для CI/диагностики.
- Запуск — **вручную** после каждого `ruflo update`. Документируется в memory
`project_ruflo_integration.md` (operational note) и, опционально, в
`Tooling_v8_3.md` §4.10.
- `patch-package` не используется — он рассчитан на проектный `./node_modules`, а не
на глобальную установку.
### 4.4. Отклонённые альтернативы
- **better-sqlite3 native** — bridge написан под better-sqlite3 (синхронная запись
на диск); собранный native-бинарь убрал бы H7 без правки исходника. Отклонено:
native-build на Windows + Node 24 невыполнимости не гарантирован (параллель с
сагой sharp/libvips при активации embeddings).
- **MCP-сервер (session-scoped)** — `memory_*` через долгоживущий MCP-процесс.
Отклонено: персист только на время жизни сервера, cross-session нет.
- **Обойти ruflo memory вовсе** — отклонено: пользователь выбрал чинить H7.
### 4.5. Граница scope D2
Патч `getBridge()` в `memory-initializer.js` покрывает **CLI-путь** memory-операций
(`ruflo memory store/get/list/search/delete`), который и использует хук D3.
**Не верифицировано**, разделяют ли MCP-инструменты `memory_*`
(`dist/src/mcp-tools/memory-tools.js`) тот же модуль `memory-initializer.js` или
имеют отдельный bridge-путь. Если в будущем понадобятся MCP memory-инструменты —
проверить и при необходимости расширить патч; в текущем дизайне MCP memory-путь вне
scope.
---
## 5. D3 — Advisory-хук
### 5.1. Назначение
`UserPromptSubmit`-хук, который на каждый промпт интерактивной сессии достаёт из
ruflo-памяти релевантные записи (semantic search) и инжектит их в контекст как
`additionalContext`. Это сдвигает ruflo из «parallel» в «integrated advisory» —
Claude видит припомненный контекст, но решение остаётся за Claude (не routing).
### 5.2. Компоненты
- **`tools/ruflo-recall-hook.mjs`** (в репо, под git) — Node-скрипт хука.
- Регистрация в проектном **`.claude/settings.json`** → `hooks.UserPromptSubmit`
(не глобально — хук специфичен для этого проекта/ruflo-установки).
### 5.3. Data flow
1. Пользователь отправляет промпт → Claude Code запускает `UserPromptSubmit`-хуки.
2. `ruflo-recall-hook.mjs` получает на stdin JSON с полем `prompt`.
3. Хук спавнит `ruflo memory search -q "<prompt>" --format json -l 3` с таймаутом.
4. После фикса D2 `search` читает `.swarm/memory.db` (персистящий raw-путь) и
возвращает топ-хиты JSON-ом.
5. Хук форматирует хиты в короткий текстовый блок.
6. Хук пишет в stdout:
`{"hookSpecificOutput":{"hookEventName":"UserPromptSubmit","additionalContext":"<блок>"}}`,
exit 0.
7. Claude видит припомненный контекст в этом turn.
### 5.4. Обработка ошибок — fail-open (жёсткое требование)
Хук **никогда не ломает сессию**:
- Таймаут вызова `ruflo` (~3000 мс) — kill подпроцесса, инжект пустой.
- `ruflo` вернул ошибку / невалидный JSON / 0 результатов — инжект пустой.
- Любое исключение в скрипте — поймать, exit 0, инжект пустой.
- Хук **не использует** `decision: "block"` — промпт всегда проходит.
«Инжект пустой» = либо `additionalContext: ""`, либо вывод без `hookSpecificOutput`
(хук просто завершается exit 0 без JSON).
### 5.5. Стоимость и ограничения D3
- **Латентность:** per-prompt вызов `ruflo` CLI = Node + sql.js WASM init →
~1-3 с задержки на КАЖДЫЙ промпт. Таймаут ограничивает худший случай.
- **Coexistence:** в глобальном `~/.claude/settings.json` уже есть
`UserPromptSubmit`-хук (economy-mode). Хуки независимы — оба срабатывают, оба
инжектят свой `additionalContext`. Конфликта нет.
- **Ценность растёт с накоплением данных:** на свежей памяти recall пуст; полезным
хук становится по мере наполнения `.swarm/memory.db`.
### 5.6. Write-сторона
Память наполняет Claude вручную — вызовом `ruflo memory store -k <key> --value
<val>` (после D2 он персистит), когда в сессии есть что запомнить. Авто-store хук —
не строим (YAGNI; при потребности — отдельная итерация).
---
## 6. Последовательность и зависимости
```
D1 (claude CLI) ──independent──┐
D2 (H7 patch) ───────────────┤
D3 (advisory hook) — зависит от D2 (хук бесполезен без персистящей памяти)
```
Порядок исполнения: **D1 → D2 → D3**. D1 первым — это «шаг 1» по постановке
пользователя; D2 строго до D3.
---
## 7. Верификация
### D1
- `claude --version` → версия выведена.
- `ruflo doctor` → нет строки «Claude Code CLI: Not installed».
- `ruflo hive-mind spawn --claude --no-auto-permissions` на тривиальной задаче →
spawned-инстанс отработал (smoke).
### D2
- `tools/ruflo-h7-patch.mjs` — round-trip:
- запуск на чистой установке → патч наложен, маркер присутствует;
- повторный запуск → no-op (идемпотентность);
- `--check` → exit 0;
- `--revert` → маркер удалён, `--check` → exit 1.
- Эффект патча — **cross-process round-trip**: `ruflo memory store -k h7-verify -v
"..."` → (новый процесс) `ruflo memory retrieve -k h7-verify` → запись найдена;
`ruflo memory stats` → `Total Entries ≥ 1`; mtime `.swarm/memory.db` изменилась.
- `ruflo memory delete -k h7-verify` — cleanup verify-записи.
### D3
- Submit промпта → в контексте turn появляется блок recall (когда в памяти есть
релевантные записи).
- **Fail-open тест:** временно сломать `ruflo` (переименовать бинарь / недостижимый
PATH) → submit промпта → промпт проходит, сессия не падает, инжект пустой.
- Таймаут-тест: убедиться, что зависший `ruflo` убивается по таймауту и хук
завершается exit 0.
### Регрессия
- ruflo daemon worker-jitter усиливает Pest quirk 72 (memory `feedback_environment.md`
квирк #93) — при прогоне baseline-регрессии `pm2 stop ruflo-daemon`. D1/D2/D3 не
меняют код приложения (`app/`), schema или тесты — отдельный Pest/Vitest-прогон не
требуется; затрагиваются только ruflo-инфраструктура, `.claude/settings.json`,
`tools/`.
---
## 8. Риски и ограничения
- **Фрагильность патча D2** — патч в глобальном `node_modules` теряется при `ruflo
update`. Митигировано re-apply-скриптом + документированием; остаётся ручной шаг.
- **`claude` CLI auth (D1)** — не верифицировано, нужен ли `claude login`
(см. §3.3).
- **Латентность хука (D3)** — ~1-3 с на каждый промпт (см. §5.5).
- **ruflo alpha** — raw-sql.js путь верифицирован чтением исходника (персистит), но
его runtime-корректность подтверждается только тестом round-trip в D2; возможны
иные alpha-баги в raw-пути, не выявленные чтением.
- **MCP memory-путь** — не верифицирован (см. §4.5).
- **Намеренно не чиним** косметический баг namespace `undefined` (§1.2).
---
## 9. Откат
- **D1:** `npm uninstall -g @anthropic-ai/claude-code`.
- **D2:** `node tools/ruflo-h7-patch.mjs --revert`, либо `npm i -g ruflo` (чистая
переустановка перетирает патч).
- **D3:** удалить `UserPromptSubmit`-блок из `.claude/settings.json`; удалить
`tools/ruflo-recall-hook.mjs`.
---
## 10. Артефакты
| Артефакт | Путь | Под git |
|---|---|---|
| Re-apply-скрипт патча H7 | `tools/ruflo-h7-patch.mjs` | да |
| Скрипт advisory-хука | `tools/ruflo-recall-hook.mjs` | да |
| Регистрация хука | `.claude/settings.json` (`hooks.UserPromptSubmit`) | да |
| Патч H7 | `<npm-root-g>/.../memory-initializer.js` | нет (node_modules) |
| Operational-нота | memory `project_ruflo_integration.md` | нет (личная память) |