opencode-config/docs/handoff/pr-101-ts-memory-tools.md
Sergey 45bbdc50f3
feat(memory): 5 TS tools with auto-setup and fallback (#101)
* 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>
2026-07-27 01:12:51 +03:00

37 lines
No EOL
10 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.

---
pr: 101
title: feat(memory): 5 TS tools with auto-setup and fallback
---
## Что сделано
5 TS tools в `.opencode/tools/` заменяющие плагин `@mathew-cf/opencode-memory`. Прогрессивная модель: keyword (ripgrep) работает всегда, semantic (Python+OpenRouter) как enhancement с fallback. Zero-config: первый `memory-save` авто-инициализирует память.
- `memory-save.ts` (~120 LOC) — auto-setup + git commit + async reindex. Определяет `memoryDir` (env или `~/.local/share/opencode/opencode-memory`). При первом вызове: `mkdir -p`, `git init` (или `git clone` если `OPENCODE_MEMORY_REMOTE` set и dir пуста), установка post-commit hook (`git push origin master 2>/dev/null || true`) при наличии remote, создание 7 категорий. Затем `git add -A` + `git diff --cached --name-only` → если пусто, return "No changes to sync". `git commit -m "memory: sync <files>"`. Async reindex: `spawn("python3", ["-m","src.memory","index",memoryDir,"-o",.rag], {cwd: context.worktree, stdio:"ignore", detached:true}) + child.unref()` — fire-and-forget, НЕ блокирует ответ. Лог в `.rag/reindex.log` (Python из PR#100). Skip semantic если `OPENAI_BASE_URL` не set.
- `memory-search.ts` (~270 LOC) — ripgrep + Python semantic + scoring + fallback. `resolveRgBinary()`: `require2("@vscode/ripgrep").rgPath` (если установлен и существует), fallback на системный `rg`. Keyword: `spawnSync(rgBin, [...buildRgArgs(terms), searchDir])``--glob *.md --glob !.git --glob !.rag --glob !**/INDEX.md -e <term>` per term (OR logic). Semantic: если `OPENAI_BASE_URL` set AND `.rag/index.json` существует → `spawnSync("python3", ["-m","src.memory","search",query,"-i",.rag,"-k","15","--json"], {cwd: context.worktree})`. Если Python упал (status≠0) / вернул `[]` / env не set → skip semantic (fallback keyword-only). Scoring — порт `scoreCandidate()` из плагина `dist/index.js:12822-12852`: rgMatch (+0.15+0.35*termCoverage), ragScore×1.4, rgMatch+ragScore bonus +0.10, tags overlap +0.20*(tagHits/terms), path hits +0.15, importance (high +0.15, low -0.10), accessCount (≥5 +0.10, ≥2 +0.05). Cross-category fallback если category filter дал 0 hits. Output: markdown с top-7 результатами + Related files.
- `memory-list.ts` (~100 LOC) — pure TS, без subprocess. Без `category` → список 7 категорий с count .md файлов (`walkMd` рекурсивно, skip `.git`/`.rag`/`INDEX.md`). С `category` → список файлов (`{path, title, summary, importance, updated}` из frontmatter), отсортированы по `updated` descending.
- `memory-access.ts` (~70 LOC) — pure TS, bump frontmatter. Читает файл, парсит frontmatter, обновляет `last_accessed: <today YYYY-MM-DD>` и `access_count: <current+1>` (regex replace, порт `bumpAccessFields` из плагина `dist/index.js:12677-12694`). Atomic write: `writeFileSync(tmp) + renameSync(tmp,filePath)`. НЕ коммитит — следующий `memory-save` синхронизирует.
- `memory-doctor.ts` (~85 LOC) — read-only диагностика. Проверяет: `rg` (через `@vscode/ripgrep` + fallback system rg + `spawnSync --version`), Python `src.memory` importable (`python3 -c "import src.memory"`, cwd=worktree), env vars (`OPENAI_BASE_URL`, `OPENAI_API_KEY`, `OPENCODE_MEMORY_REMOTE`), `OPENCODE_MEMORY_DIR` exists, memory dir is git repo, `.rag/index.json` exists (size+mtime), count .md files. Возвращает markdown с ✅/❌ по каждому пункту + summary (all-green / keyword-only / rg missing).
- `.opencode/package.json` — добавлены deps: `@vscode/ripgrep ^1.18.0` (keyword search, платформенный binary `@vscode/ripgrep-linux-x64/bin/rg`), devDeps: `@types/node ^26.1.1` (typecheck) + `typescript ^7.0.2` (typecheck).
- ADR-043 + этот handoff.
Scoring logic портирован 1-в-1 из плагина (rgMatch 0.15+0.35*termCoverage, ragScore×1.4, +0.10 за совпадение обоих, tags +0.20, path +0.15, importance, accessCount). Тестирование: typecheck (`tsc --noEmit --target es2022 --moduleResolution bundler --types node`) — clean. Smoke-tests всех 5 tools на реальной памяти (84 .md файла, 210 MB index.json): memory-search (6 keyword matches по "embedder"), memory-list (preferences 0 / repos 19 / technical 63 / workflows 2), memory-access (3→4 count, atomic), memory-doctor (all green), memory-save (auto-setup на пустой /tmp dir с git init + hook + remote + commit).
## Почему
Плагин `@mathew-cf/opencode-memory` не вызывается из-за бага wrapper-пути MCP server (`MEMORY_WRAPPER_PATH` указывает на `@mathew-cf/rag-cli/bin/rag.js`, который не существует/несовместим — см. ADR-030, PR#57, PR#71). TS tools вызывают Python через `spawnSync` (паттерн `pipeline-status.ts`, `commit.ts`, `memory-setup.ts`) — нативный, без wrapper-hop. Это Фаза 2 миграции к единой облачной памяти (issue #94, ADR-030): Фаза 1 (PR#100) сделала embedder lazy-init (returns None без env), Фаза 2 (этот PR) — TS tools, Фаза 3 — удалить плагин + `memory-setup.ts` + `setup-memory.sh`.
Прогрессивная модель (progressive enhancement): keyword (ripgrep) работает ВСЕГДА, semantic (Python+OpenRouter) как enhancement с fallback на keyword-only. Zero-config: первый `memory-save` авто-инициализирует память (mkdir + git init/clone + hook + 7 категорий) — не требует отдельного `memory-setup` вызова. Это упрощает onboarding и убирает зависимость от `OPENCODE_MEMORY_REMOTE` (хотя hook auto-push ставится только если remote set).
## Pending
- Фаза 3 (отдельный PR): удалить `.opencode/tools/memory-setup.ts` + `.opencode/scripts/setup-memory.sh` + плагин `@mathew-cf/opencode-memory` из `opencode.json` `plugin` block + обновить AGENTS.md Tool Usage Policy таблицу (10 tools → 14 tools: добавить `memory-save`, `memory-search`, `memory-list`, `memory-access`, `memory-doctor`; убрать `memory-setup`).
## Watch out
- **`@vscode/ripgrep` платформенный binary** — `require("@vscode/ripgrep").rgPath` возвращает путь к `@vscode/ripgrep-<platform>/bin/rg` (не к `@vscode/ripgrep/bin/rg`). На linux-x64: `@vscode/ripgrep-linux-x64/bin/rg`. Проверяется через `fs.existsSync(mod.rgPath)`. Если платформенный пакет не установлен (unsupported platform) → fallback на системный `rg` через `spawnSync("rg", ["--version"])`. Если оба недоступны → memory-search возвращает "No memories found" (keyword не работает, semantic может). memory-doctor показывает ❌.
- **`OPENCODE_MEMORY_REMOTE` опционален для memory-save** (в отличие от `setup-memory.sh`, который exit 1 без remote). memory-save авто-инициализирует git repo (`git init`) даже без remote — hook auto-push не ставится. Это делает zero-config возможным (новый пользователь без remote может начать писать память, push настроит позже через `OPENCODE_MEMORY_REMOTE`). Hook ставится ТОЛЬКО если remote set AND hook отсутствует/несовпадает.
- **`spawn(... detached: true) + child.unref()`** для async reindex — fire-and-forget, НЕ блокирует return memory-save. Если процесс opencode умрёт до завершения reindex, orphaned python процесс может остаться (но `.rag/.lock` через `fcntl.flock(LOCK_EX)` в `index.py` предотвращает concurrent reindex corruption). Reindex логирует прогресс в `.rag/reindex.log` (из PR#100).
- **Scoring weights портированы 1-в-1** из плагина `scoreCandidate` (`dist/index.js:12822-12852`). НЕ изменяй веса без ADR — это нарушит совместимость результатов поиска между плагином (если останется) и TS tools. Path hits +0.15 — это было в плагине (`input.path.toLowerCase().replace(/[-_/.]/g, " ")`), сохранено.
- **Typecheck требует `@types/node` + `typescript`** — добавлены как devDeps в `.opencode/package.json`. CI runner должен делать `npm install` в `.opencode/` перед typecheck. Runtime (opencode plugin loader) использует Bun/Node native TS, эти devDeps не нужны в production.
- **Frontmatter parsing упрощён** (regex `^(\w+):\s*(.+)$` per line, не полноценный YAML parser) — покрывает наш формат (плоские ключи + `[a, b]` для tags/related). Многострочные значения НЕ поддерживаются. Это тот же парсер что в плагине (`parseFrontmatter` `dist/index.js:12639-12676`).
- **memory-search cross-category fallback** — если `args.category` указан и дал 0 hits, search повторяет по всему `memoryDir` (без category filter). Это поведение плагина (`crossCategoryFallback`, `dist/index.js:12999-13022`). Сохранено для UX.
- **`spawnSync("python3", ["-c", "import src.memory"], {cwd: context.worktree})`** в memory-doctor — требует что `context.worktree` имеет `src/memory/` пакет importable (`.venv/bin/python` или system python с `src.memory` установлен). Если worktree не настроен (нет `.venv`, нет `pip install -e .`) → ❌, но это диагностический сигнал, не блокер.
- **`git -C memoryDir commit`** через `spawnSync`НЕ через `commit` tool (тот для main repo, с conventional format validation). Memory repo commit message: `memory: sync <files>` (derived, не conventional format). Это тот же паттерн что в плагине `runSave` (`dist/index.js:13259-13260`).