* 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>
5.5 KiB
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.
Решение
- 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). MEMORY_PYTHONвыбор интерпретатора. Systempython3не имеетhttpx(нет venv) — CLI падает на import. Скрипт выбирает:${OPENCODE_WORKSPACE}/.venv/bin/python→.venv/bin/python→python3. Override черезMEMORY_WRAPPER_PYTHONenv (для тестов).- Шаг 5b — генерация JS wrapper. Путь:
WRAPPER_PATHenv (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). - Idempotency через
cmp -s. Сравнениеcmp -s "$WRAPPER_PATH" /dev/stdin <<<"$WRAPPER_CONTENT"— байт-точное. Bash$(cat)удаляет trailing newlines → false-negative (см. handoff Watch out). Если контент совпадает → echo "wrapper correct", не трогает. - Backup
.origодин раз. Если wrapper существует и контент отличается →cp "$WRAPPER_PATH" "${WRAPPER_PATH}.orig"(только если.origещё не существует — не перезаписывать backup). Записывает новый wrapper только если контент отличается. MEMORY_WRAPPER_PATHenv override. Тесты используютtmp_path/wrapper/rag.jsдля изоляции (не пишут в real plugin path). Default = real path для smoke-test.
Альтернативы
spawnSync("python3", ["-m", "src.memory", ...])как в спеке — отклонено: systempython3не имеетhttpx(нет venv), CLI падает наModuleNotFoundError: No module named 'httpx'. Нужен venv-python. Реализация выбирает venv автоматически + встраивает путь в wrapper. Runtime override черезMEMORY_WRAPPER_PYTHONenv оставлен как 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 операции.