* feat(memory): wire setup-memory.sh to Python CLI + JS wrapper * test(memory): add wrapper generation, idempotency, backup tests * docs(memory): update project map for setup-memory wrapper integration * docs(handoff): add handoff and ADR-033 for plugin wiring * docs(handoff): set PR number * docs(handoff): fix Pending em-dash typo in PR#77 handoff --------- Co-authored-by: opencode-agent <agent@opencode.local>
27 lines
No EOL
5.5 KiB
Markdown
27 lines
No EOL
5.5 KiB
Markdown
# ADR-033: Wire memory CLI into opencode-memory plugin via setup-memory.sh wrapper (PR #77)
|
||
|
||
## Статус
|
||
Accepted (2026-07-26)
|
||
|
||
## Контекст
|
||
PR #75 (merged) отрефакторил Python Memory CLI (`src/memory/`): переименовал `second-brain` → `memory`, перешёл на OpenAI env (`OPENAI_BASE_URL`/`OPENAI_API_KEY`/`OPENAI_EMBEDDING_MODEL`), добавил chunking/batching/dedup. CLI работает локально (`python -m src.memory index/search/download`).
|
||
|
||
Но плагин `@mathew-cf/opencode-memory` (MCP server, загружается opencode) всё ещё вызывал родной Rust `@mathew-cf/rag-cli` через `require.resolve("@mathew-cf/rag-cli/bin/rag.js")` (вендорный `dist/index.js:12703-12708`, НЕ патчим — upstream код). Rust rag-cli: 5-10 мин на 1.2 MB на CPU, 3 зомби-процесса зафиксировано, индекс никогда не записан (`$OPENCODE_MEMORY_DIR/.rag` отсутствовал). Memory plugin semantic search был нерабочим — active только keyword (ripgrep) path.
|
||
|
||
Контракт плагина (4 подкоманды, детерминированный): `rag index <PATH> -o <OUT>`, `rag search "<q>" -i <IDX> -k 15 --json`, `rag download`, `rag info`. Наш Python CLI уже совпадает по контракту (PR #75). Нужен bridge: перехватить `require.resolve` плагина → перенаправить в Python CLI.
|
||
|
||
## Решение
|
||
1. **setup-memory.sh шаг 5 → Python memory CLI.** Замена `command -v rag` → `"$MEMORY_PYTHON" -c "import src.memory"`. Если доступен → `python -m src.memory index "$MEMORY_DIR" -o "$MEMORY_DIR/.rag"`. Если нет → echo skip + continue (best-effort, не exit 1).
|
||
2. **`MEMORY_PYTHON` выбор интерпретатора.** System `python3` не имеет `httpx` (нет venv) — CLI падает на import. Скрипт выбирает: `${OPENCODE_WORKSPACE}/.venv/bin/python` → `.venv/bin/python` → `python3`. Override через `MEMORY_WRAPPER_PYTHON` env (для тестов).
|
||
3. **Шаг 5b — генерация JS wrapper.** Путь: `WRAPPER_PATH` env (default `/usr/local/lib/node_modules/@mathew-cf/opencode-memory/node_modules/@mathew-cf/rag-cli/bin/rag.js`, override через `MEMORY_WRAPPER_PATH`). Содержимое: `spawnSync(py, ["-m", "src.memory", ...process.argv.slice(2)], { cwd: OPENCODE_WORKSPACE })` — делегирует `rag index/search/download` в Python CLI. `py` = `process.env.MEMORY_WRAPPER_PYTHON` (override) или встроенный абсолютный путь к venv-python (default).
|
||
4. **Idempotency через `cmp -s`.** Сравнение `cmp -s "$WRAPPER_PATH" /dev/stdin <<<"$WRAPPER_CONTENT"` — байт-точное. Bash `$(cat)` удаляет trailing newlines → false-negative (см. handoff Watch out). Если контент совпадает → echo "wrapper correct", не трогает.
|
||
5. **Backup `.orig` один раз.** Если wrapper существует и контент отличается → `cp "$WRAPPER_PATH" "${WRAPPER_PATH}.orig"` (только если `.orig` ещё не существует — не перезаписывать backup). Записывает новый wrapper только если контент отличается.
|
||
6. **`MEMORY_WRAPPER_PATH` env override.** Тесты используют `tmp_path/wrapper/rag.js` для изоляции (не пишут в real plugin path). Default = real path для smoke-test.
|
||
|
||
## Альтернативы
|
||
- **`spawnSync("python3", ["-m", "src.memory", ...])` как в спеке** — отклонено: system `python3` не имеет `httpx` (нет venv), CLI падает на `ModuleNotFoundError: No module named 'httpx'`. Нужен venv-python. Реализация выбирает venv автоматически + встраивает путь в wrapper. Runtime override через `MEMORY_WRAPPER_PYTHON` env оставлен как escape hatch.
|
||
- **Wrapper читает Python путь из env в runtime** (`process.env.MEMORY_WRAPPER_PYTHON || "python3"`) — отклонено как default: env может быть не set в контексте плагина (MCP server запускается opencode, env наследуется непредсказуемо). Встраивание абсолютного пути к venv-python надёжнее. Override через env оставлен для тестов/кастомных setups.
|
||
- **Патчить вендорный `dist/index.js` плагина** — отклонено: upstream код, update плагина затрёт патч. Wrapper по `require.resolve` пути — прозрачный bridge, survives plugin updates (если plugin не меняет `require.resolve` логику).
|
||
- **Форкнуть плагин** — отклонено: maintenance burden, sync с upstream. Wrapper = минимальный shim, не требует fork.
|
||
- **`$(cat)` для idempotency check** — отклонено: bash `$()` удаляет trailing newlines → false-negative при сравнении с `$VAR` содержащим `\n`. `cmp -s` с here-string — байт-точное, no surprises.
|
||
- **Отдельный subagent для wrapper generation** — отклонено: setup-memory.sh = deterministic bootstrap script, wrapper gen = ещё один idempotent step. Subagent = orchestrator overhead для trivial операции. |