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

5.5 KiB
Raw Permalink Blame History

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-brainmemory, перешёл на 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/pythonpython3. 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 операции.