opencode-config/docs/decisions/033-pr-77-wire-plugin-via-setup-memory.md
Sergey 7c1fa4985e
feat(memory): wire CLI into plugin via setup-memory.sh wrapper (#77)
* 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>
2026-07-26 17:06:45 +03:00

27 lines
No EOL
5.5 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.

# 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 операции.