* feat(memory): memory-save tool with auto-setup * feat(memory): memory-search with keyword and semantic fallback * feat(memory): memory-list and memory-access tools * feat(memory): memory-doctor diagnostic tool * docs(handoff): add handoff and ADR for ts memory tools * docs(handoff): set PR number * docs(handoff): finalize pr-101 frontmatter and pending * docs(project-map): add 5 memory TS tools after PR#101 * refactor(memory): extract shared helpers to _memory-shared.ts * refactor(memory): split oversized execute functions * fix(memory): handle git checkout and catch blocks --------- Co-authored-by: opencode-agent <agent@opencode.local>
39 lines
No EOL
5.7 KiB
Markdown
39 lines
No EOL
5.7 KiB
Markdown
# ADR-043: 5 TS memory tools with auto-setup and progressive enhancement
|
||
|
||
## Статус
|
||
Accepted (2026-07-26)
|
||
|
||
## Контекст
|
||
Плагин `@mathew-cf/opencode-memory` (MCP server wrapper) не вызывается из-за бага wrapper-пути: `MEMORY_WRAPPER_PATH` в `setup-memory.sh` указывает на `@mathew-cf/rag-cli/bin/rag.js`, который не существует или несовместим (GLIBC 2.39 vs 2.36 — см. ADR-030, PR#57, PR#71). Плагин деградирует до keyword-only через grep, но wrapper-hop добавляет fragility. Нужны нативные TS tools, вызывающие Python через `spawnSync` (паттерн `pipeline-status.ts`, `commit.ts`, `memory-setup.ts`).
|
||
|
||
Фаза 1 (PR#100) сделала embedder lazy-init (returns `None` без `OPENAI_BASE_URL`, не raises), index.py логирует reindex в `.rag/reindex.log`, search.py обрабатывает `None` от embedder → возвращает `[]`. Это разблокировало TS tools: Python backend безопасен при отсутствии env vars.
|
||
|
||
Требования к миграции (issue #96, #94):
|
||
- 5 tools: `memory-save`, `memory-search`, `memory-list`, `memory-access`, `memory-doctor`
|
||
- Keyword search (ripgrep) работает ВСЕГДА (zero env dependency)
|
||
- Semantic search (Python + OpenRouter) как enhancement с fallback на keyword-only
|
||
- Zero-config: первый `memory-save` авто-инициализирует память (mkdir + git init/clone + hook + categories)
|
||
- Scoring портирован из плагина `scoreCandidate` (rgMatch + ragScore×1.4 + tags + importance + accessCount)
|
||
- Pure-orchestrator model (ADR-010): TS tools вызываются из главного чата, делегируют в Python через subprocess
|
||
|
||
## Решение
|
||
5 TS tools в `.opencode/tools/`, каждый следует паттерну `tool()` из `@opencode-ai/plugin` (description + args (zod) + async execute(args, context)):
|
||
|
||
1. **memory-save** — auto-setup + git commit + async reindex. `resolveMemoryDir()` (env или `~/.local/share/opencode/opencode-memory`). `autoSetup()`: mkdir, git init (или clone если `OPENCODE_MEMORY_REMOTE` set и dir пуста), post-commit hook (auto-push, только если remote), 7 категорий. `git add -A` + `git diff --cached --name-only` → commit с derived message `memory: sync <files>`. Async reindex: `spawn(python3, ["-m","src.memory","index",...], {detached:true, stdio:"ignore"}) + child.unref()` (fire-and-forget). Skip semantic если `OPENAI_BASE_URL` не set.
|
||
|
||
2. **memory-search** — ripgrep + Python semantic + scoring + fallback. `resolveRgBinary()`: `@vscode/ripgrep` (платформенный binary) → fallback system `rg`. Keyword: `rg -il --glob *.md --glob !.git --glob !.rag --glob !**/INDEX.md -e <term>` per term (OR). Semantic: `spawnSync("python3", ["-m","src.memory","search",...])` если `OPENAI_BASE_URL` set AND `.rag/index.json` существует. Fallback на keyword-only если Python упал/`[]`/env не set. Scoring — порт `scoreCandidate` из плагина (`dist/index.js:12822-12852`). Cross-category fallback если category filter дал 0 hits.
|
||
|
||
3. **memory-list** — pure TS, без subprocess. Список категорий (count .md) или файлов в категории (title/summary/importance/updated из frontmatter).
|
||
|
||
4. **memory-access** — pure TS, bump frontmatter `last_accessed`/`access_count` через regex replace (порт `bumpAccessFields` из плагина `dist/index.js:12677-12694`). Atomic write (tmp + rename).
|
||
|
||
5. **memory-doctor** — read-only диагностика (rg, Python importability, env vars, memory dir, RAG index).
|
||
|
||
Зависимости: `@vscode/ripgrep ^1.18.0` (keyword search, платформенный binary), devDeps `@types/node` + `typescript` (typecheck).
|
||
|
||
## Альтернативы
|
||
- **Python MCP server** (Option A в `technical/cloud-memory-opencode-landscape.md`, `technical/python-mcp-server-replace-plugin-feasibility.md`) — отклонён: добавляет `mcp` dep (22 transitive), Dockerfile layer, embedder.py lazy-init (сделано в PR#100), naming verification. TS tools проще для opencode-only use case (reuse proven `spawnSync` pattern, no new Python deps, no Dockerfile changes).
|
||
- **Оставить плагин + починить wrapper** — отклонено: wrapper-hop (`setup-memory.sh` → JS wrapper → Python CLI) добавляет fragility, `@mathew-cf/rag-cli` требует GLIBC 2.39 (несовместим с node:20-slim на Bookworm, см. ADR-030). TS tools вызывают Python напрямую через `python3 -m src.memory`.
|
||
- **System `rg` без `@vscode/ripgrep`** — отклонён: системный `rg` может отсутствовать (Docker slim images). `@vscode/ripgrep` предоставляет платформенный binary (npm-managed, reproducible). memory-search имеет fallback на system `rg` через `spawnSync("rg", ["--version"])`.
|
||
- **Полноценный YAML parser (js-yaml) для frontmatter** — отклонён: наш формат плоский (плоские ключи + `[a, b]` для tags/related). Regex-парсер (тот же что в плагине `parseFrontmatter`) покрывает формат и не добавляет dep.
|
||
- **Conventional commit format для memory-save commits** — отклонён: memory repo не part основного репо, commit message derived из changed files (`memory: sync <files>`), не human-curated. Тот же паттерн что в плагине `runSave`. `commit` tool (для main repo) остаётся с conventional validation. |