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

10 KiB
Raw Permalink Blame History


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.35termCoverage), 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 платформенный binaryrequire("@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).