From 4cf149cb021aefe8aa92016a7914ead511291e62 Mon Sep 17 00:00:00 2001 From: Sergey <93754860+slaid098@users.noreply.github.com> Date: Mon, 27 Jul 2026 02:08:35 +0300 Subject: [PATCH] docs(memory): reindex + update SKILL.md (#104) * feat(memory): add OPENAI_EMBEDDING_BATCH_DELAY support * docs(memory): update SKILL.md for progressive enhancement and auto-setup * docs(memory): update AGENTS.md tool usage policy * docs(memory): fix stale snake_case tool references in skills, agents, tools * docs(handoff): add handoff + ADR for reindex-skill-update * docs(handoff): set PR number * refactor(memory): extract _embed_in_batches to satisfy xenon rank A --------- Co-authored-by: opencode-agent --- .env.example | 1 + .opencode/agents/memory-syncer.md | 10 +- .opencode/commands/spec.md | 2 +- .opencode/skills/add-skill/SKILL.md | 2 +- .opencode/skills/configure-opencode/SKILL.md | 2 +- .opencode/skills/issue/SKILL.md | 2 +- .opencode/skills/memory/SKILL.md | 14 ++- .opencode/skills/release/SKILL.md | 2 +- .opencode/skills/repo-init/SKILL.md | 2 +- .opencode/skills/run-pipeline/SKILL.md | 11 +- .opencode/skills/spec/SKILL.md | 12 +- .opencode/tools/memory-access.ts | 2 +- .opencode/tools/memory-doctor.ts | 8 +- .opencode/tools/memory-search.ts | 2 +- AGENTS.md | 2 +- .../046-pr-104-reindex-skill-update.md | 67 +++++++++++ docs/handoff/pr-104-reindex-skill-update.md | 104 ++++++++++++++++++ src/memory/embedder.py | 29 +++-- 18 files changed, 233 insertions(+), 41 deletions(-) create mode 100644 docs/decisions/046-pr-104-reindex-skill-update.md create mode 100644 docs/handoff/pr-104-reindex-skill-update.md diff --git a/.env.example b/.env.example index de93f02..c9c2939 100644 --- a/.env.example +++ b/.env.example @@ -11,6 +11,7 @@ OPENAI_BASE_URL=https://openrouter.ai/api/v1 OPENAI_API_KEY=your-openrouter-api-key OPENAI_EMBEDDING_MODEL=qwen/qwen3-embedding-8b OPENAI_EMBEDDING_BATCH_SIZE=50 +OPENAI_EMBEDDING_BATCH_DELAY=0.5 MEMORY_CHUNK_SIZE=512 MEMORY_CHUNK_OVERLAP=64 diff --git a/.opencode/agents/memory-syncer.md b/.opencode/agents/memory-syncer.md index d21b239..cac3dbf 100644 --- a/.opencode/agents/memory-syncer.md +++ b/.opencode/agents/memory-syncer.md @@ -32,7 +32,7 @@ permission: "gh issue view*": allow --- -You are a memory-syncer agent. Your job: distill durable knowledge from a merged PR handoff into the global memory file at `app_data/opencode-memory/repos/{host}/{org}/{repo}.md`. +You are a memory-syncer agent. Your job: distill durable knowledge from a merged PR handoff into the global memory file at `/repos/{host}/{org}/{repo}.md` (default `~/.local/share/opencode/opencode-memory`, override via `OPENCODE_MEMORY_DIR`). You are **read-only on the repository** and **write-only on memory**. You CANNOT commit, push, or add files to the repo — the permission set physically prevents it (`git push`, `git commit`, `git add` are absent from the allow-list; catch-all `"*": deny` blocks them). This is a deterministic guard against pushing to master, replacing the prompt-level rule that was previously bypassed by invocation prompts. @@ -41,7 +41,7 @@ You are **read-only on the repository** and **write-only on memory**. You CANNOT 1. Get the PR number from the invocation prompt. 2. Find the merged handoff file: `ls docs/handoff/pr--*` to discover the slug, then `cat docs/handoff/pr--.md` to read it (read-only — agent does not check out branches). 3. Determine the repo: `git remote get-url origin` → parse `{host}/{org}/{repo}` (e.g. `github.com/slaid098/opencode-config`). -4. Resolve memory path: read `OPENCODE_MEMORY_DIR` env var (set globally via docker-compose; fallback is `app_data/opencode-memory/` for local dev) → `/repos/{host}/{org}/{repo}.md`. Use `printenv OPENCODE_MEMORY_DIR` to inspect it. +4. Resolve memory path: read `OPENCODE_MEMORY_DIR` env var (set globally via docker-compose; fallback `~/.local/share/opencode/opencode-memory/`) → `/repos/{host}/{org}/{repo}.md`. Use `printenv OPENCODE_MEMORY_DIR` to inspect it. 5. Open the memory file (create if missing) via the `edit`/`write` tool — `edit: allow` permits this. The memory dir is an isolated git repo (post-commit hook auto-pushes), separate from the main repo. ## Distillation @@ -80,14 +80,14 @@ If a fact is already recorded — update the entry (bump `updated` in frontmatte ## Save -1. After editing the memory file, call `memory_save` to commit + re-index the isolated memory repo. -2. **Guard**: run `git status` on the main repo. If anything under `app_data/` is staged (should not happen — `memory_save` commits to the isolated memory repo, not the main repo), report it to the user. **You CANNOT fix this yourself** — `git restore` is not in the allow-list (the agent must not touch the repo). Inform the user so they can run `git restore --staged app_data/` manually. +1. After editing the memory file, call `memory-save` to commit + re-index the isolated memory repo. +2. **Guard**: run `git status` on the main repo. If anything under the memory dir is staged (should not happen — `memory-save` commits to the isolated memory repo, not the main repo), report it to the user. **You CANNOT fix this yourself** — `git restore` is not in the allow-list (the agent must not touch the repo). Inform the user so they can run `git restore --staged ` manually. ## Rules 1. NEVER call `git push`, `git commit`, `git add` — they are not in the allow-list and will be denied by the catch-all rule. 2. NEVER checkout branches or pull — you operate on the current state of the default branch (already merged). -3. ONLY edit files under `app_data/opencode-memory/repos/{host}/{org}/{repo}.md`. +3. ONLY edit files under `/repos/{host}/{org}/{repo}.md`. 4. ONLY read files under `docs/handoff/` and `docs/decisions/`. 5. Receipt is mandatory even if no durable records found. 6. If memory file doesn't exist — create it with proper frontmatter (title, tags, summary, created, updated, importance). diff --git a/.opencode/commands/spec.md b/.opencode/commands/spec.md index 24da4f0..26cdf77 100644 --- a/.opencode/commands/spec.md +++ b/.opencode/commands/spec.md @@ -2,4 +2,4 @@ description: Run spec — interactive spec generation for new project agent: build --- -Load the `spec` skill via `skill({name: "spec"})` and follow its ПРОТОКОЛ strictly. Главный агент — оркестратор: `spec-status` tool (read-only) + вопрос юзеру + task(general) делегирование. Не делает edit/memory_search/gh сам. Каждая фаза = 1 subagent. Стоп на issues — дальше юзер сам /run-pipeline. \ No newline at end of file +Load the `spec` skill via `skill({name: "spec"})` and follow its ПРОТОКОЛ strictly. Главный агент — оркестратор: `spec-status` tool (read-only) + вопрос юзеру + task(general) делегирование. Не делает edit/memory-search/gh сам. Каждая фаза = 1 subagent. Стоп на issues — дальше юзер сам /run-pipeline. \ No newline at end of file diff --git a/.opencode/skills/add-skill/SKILL.md b/.opencode/skills/add-skill/SKILL.md index 5da6299..a9a98b3 100644 --- a/.opencode/skills/add-skill/SKILL.md +++ b/.opencode/skills/add-skill/SKILL.md @@ -73,7 +73,7 @@ git status # проверь staged set — только SKILL.md, без лиш ``` > `commit` tool НЕ делает `git add` — коммитит только уже staged файлы. Если -> в индексе лишнее (например `memory_save` stage'нул всё через `git add -A`) +> в индексе лишнее (например `memory-save` stage'нул всё через `git add -A`) > — не коммить: сначала `git restore --staged ` или не stage'и его > изначально. Используй `git add <конкретные-пути>`, НЕ `git add -A`. diff --git a/.opencode/skills/configure-opencode/SKILL.md b/.opencode/skills/configure-opencode/SKILL.md index a5b9e78..f4d7bb2 100644 --- a/.opencode/skills/configure-opencode/SKILL.md +++ b/.opencode/skills/configure-opencode/SKILL.md @@ -22,7 +22,7 @@ description: Use when adding, changing, or removing MCP servers, providers, perm - Перед каждым `commit` — `git status` для проверки staged set. `commit` tool НЕ делает `git add` — коммитит только уже staged файлы. Используй `git add <конкретные-пути>`, НЕ `git add -A` (иначе лишние файлы уйдут в - коммит). Если в индексе лишнее (например `memory_save` stage'нул всё через + коммит). Если в индексе лишнее (например `memory-save` stage'нул всё через `git add -A`) — сначала `git restore --staged `, потом коммить. - В клонах: `git pull` + рестарт opencode (MCP-серверы, skills, agents грузятся при старте — см. `add-skill/SKILL.md`). До рестарта правки не видны. - Для Docker-сетапа: `git pull` на хосте + `docker compose restart opencode` (или эквивалент) — MCP/skills/agents грузятся при старте контейнера. diff --git a/.opencode/skills/issue/SKILL.md b/.opencode/skills/issue/SKILL.md index 0ccb7f5..05247f5 100644 --- a/.opencode/skills/issue/SKILL.md +++ b/.opencode/skills/issue/SKILL.md @@ -160,6 +160,6 @@ Label выбирай по типу задачи (совпадает с commit `t 4. **Merge or Repeat** — APPROVE → `merge-pr({ pr_number: N })` tool (squash + delete branch, без `--admin`; НЕ raw `gh pr merge` — заблокирован deny), после CI ✅; замечания → fix subagent → re-review → merge. -5. **Memory-sync** — `@memory-syncer` дистиллирует handoff + ADR в `app_data/opencode-memory/repos/{host}/{org}/{repo}.md`. +5. **Memory-sync** — `@memory-syncer` дистиллирует handoff + ADR в `/repos/{host}/{org}/{repo}.md`. См. `AGENTS.md` (Development Workflow) и `run-pipeline` skill — все три документа описывают одну и ту же full-subagent модель делегирования. \ No newline at end of file diff --git a/.opencode/skills/memory/SKILL.md b/.opencode/skills/memory/SKILL.md index c885fd8..e8cba8a 100644 --- a/.opencode/skills/memory/SKILL.md +++ b/.opencode/skills/memory/SKILL.md @@ -6,7 +6,15 @@ description: Инструкция по работе с файловой памя # File Memory (opencode-memory) Графовая память (Graphiti/FalkorDB) удалена — была нестабильна и забагована. -Память работает через файловую систему с keyword + semantic search — 5 TS tools (`.opencode/tools/memory-*.ts`), вызывают `python3 -m src.memory`. Плагин `@mathew-cf/opencode-memory` удалён. +Память работает через файловую систему с keyword + semantic search — 5 TS tools (`.opencode/tools/memory-*.ts`), вызывают `python3 -m src.memory` напрямую (без wrapper-hop, без MCP-плагина). + +## Архитектура (progressive enhancement) + +- **Keyword search** — всегда работает (ripgrep по `.md` файлам). Не требует env vars, не требует индекса. +- **Semantic search** — включается если set `OPENAI_BASE_URL` + `OPENAI_API_KEY`. Embeddings через OpenRouter (`qwen/qwen3-embedding-8b`, 4096-dim), индекс в `.rag/index.json` (Python формат, `meta.json` с `version` key). +- **Fallback**: OpenRouter недоступен / упал → semantic возвращает `[]` → `memory-search` деградирует к keyword-only (работает, но semantic enhancement потерян). Не блокирует работу. +- **Auto-setup**: первый `memory-save` всё создаёт — `mkdir` категорий, `git init` (или `clone` если `OPENCODE_MEMORY_REMOTE` set), `post-commit` hook (auto-push) если remote set. Zero-config: новый пользователь без remote может начать писать память сразу. +- **`memory-doctor`** — read-only диагностика: проверяет ripgrep, Python `src.memory`, env vars, индекс. Не модифицирует ничего. ## Инструменты @@ -14,7 +22,7 @@ description: Инструкция по работе с файловой памя |---|---| | `memory-search({ query, category? })` | Гибридный поиск (ripgrep keyword + Python semantic) | | `memory-list({ category? })` | Список категорий / файлов | -| `memory-save()` | Commit + reindex после записи/редактирования (auto-setup: git init + hook если `OPENCODE_MEMORY_REMOTE` set) | +| `memory-save()` | Commit + reindex после записи/редактирования (auto-setup: git init/clone + hook если `OPENCODE_MEMORY_REMOTE` set) | | `memory-access({ path })` | Отметить файл как прочитанный (bump `last_accessed`/`access_count`) | | `memory-doctor()` | Read-only диагностика: ripgrep, Python `src.memory`, env vars, index | @@ -67,7 +75,7 @@ Default: `/root/.local/share/opencode/opencode-memory` (переопределя {memory-dir}/repos/{host}/{org}/{repo}.md ``` -Используй один путь последовательно во всех примерах — default (`~/opencode-memory`) или override (`app_data/opencode-memory`), но не оба сразу. +Используй один путь последовательно во всех примерах — default (`/root/.local/share/opencode/opencode-memory`) или override (`OPENCODE_MEMORY_DIR`), но не оба сразу. ### Формат записей diff --git a/.opencode/skills/release/SKILL.md b/.opencode/skills/release/SKILL.md index 3c41e91..a289b94 100644 --- a/.opencode/skills/release/SKILL.md +++ b/.opencode/skills/release/SKILL.md @@ -45,7 +45,7 @@ git status # проверь staged set — только CHANGELOG.md, без л ``` > `commit` tool НЕ делает `git add` — коммитит только уже staged файлы. Если -> в индексе лишнее (например `memory_save` stage'нул всё через `git add -A`) +> в индексе лишнее (например `memory-save` stage'нул всё через `git add -A`) > — не коммить: сначала `git restore --staged ` или не stage'и его > изначально. Используй `git add <конкретные-пути>`, НЕ `git add -A`. diff --git a/.opencode/skills/repo-init/SKILL.md b/.opencode/skills/repo-init/SKILL.md index 7cd9a74..d7d0102 100644 --- a/.opencode/skills/repo-init/SKILL.md +++ b/.opencode/skills/repo-init/SKILL.md @@ -42,7 +42,7 @@ git status # проверь staged set — только README.md, без ли ``` > `commit` tool НЕ делает `git add` — коммитит только уже staged файлы. Если -> в индексе лишнее (например `memory_save` stage'нул всё через `git add -A`) +> в индексе лишнее (например `memory-save` stage'нул всё через `git add -A`) > — не коммить: сначала `git restore --staged ` или не stage'и его > изначально. Используй `git add <конкретные-пути>`, НЕ `git add -A`. diff --git a/.opencode/skills/run-pipeline/SKILL.md b/.opencode/skills/run-pipeline/SKILL.md index 5675671..d6100f0 100644 --- a/.opencode/skills/run-pipeline/SKILL.md +++ b/.opencode/skills/run-pipeline/SKILL.md @@ -54,7 +54,7 @@ description: Автономный исполнитель PR-пайплайна. Перед КАЖДЫМ `commit` — `git status` для проверки staged set. `commit` tool НЕ делает `git add` — коммитит только уже staged файлы. Используй `git add <конкретные-пути>`, НЕ `git add -A` (иначе лишние файлы уйдут в - коммит). Если в индексе лишнее (например `memory_save` stage'нул всё + коммит). Если в индексе лишнее (например `memory-save` stage'нул всё через `git add -A`) — не коммить: сначала `git restore --staged `. 5. Push ветку (`git push -u origin HEAD`), затем создай PR через tool: `create-pr({ title: "type(scope): description", body: "## Что сделано\n...\n\n## Почему\n...\n\nCloses #N", issue_number: N })`. @@ -136,8 +136,9 @@ Log: `gh run view --log-failed` output: ``` Дистиллируй PR#M в memory file -`app_data/opencode-memory/repos/{host}/{org}/{repo}.md` -(путь относительно корня репо; `{host}/{org}/{repo}` вычисли через +`/repos/{host}/{org}/{repo}.md` +(default `~/.local/share/opencode/opencode-memory`, override через `OPENCODE_MEMORY_DIR`; +`{host}/{org}/{repo}` вычисли через `git remote get-url origin` — см. `memory-syncer.md:38`). 1. Прочитай `docs/handoff/pr-M-*.md` и `docs/decisions/*-pr-M-*.md` с master (`git checkout master && git pull`). @@ -146,8 +147,8 @@ Log: `gh run view --log-failed` output: 3. Добавь записи формата `- [YYYY-MM-DD, PR#M] ` в конец файла (секция "Handoff digest"). 4. Квитанция ВСЕГДА (даже если durable нет): `- [date, PR#M] — (нет durable-записей)`. -5. `memory_save` для commit + reindex + push. -6. Проверь `git status` основного репо — если staged что-то в `app_data/`, +5. `memory-save` для commit + reindex + push. +6. Проверь `git status` основного репо — если staged что-то в memory dir, репорт пользователю (guard от случайного коммита в master). ``` diff --git a/.opencode/skills/spec/SKILL.md b/.opencode/skills/spec/SKILL.md index a6fb02b..f76a436 100644 --- a/.opencode/skills/spec/SKILL.md +++ b/.opencode/skills/spec/SKILL.md @@ -26,7 +26,7 @@ truth для порядка и действий — `spec-status` tool. На в - Импровизировать порядок. Решать сам какую фазу выполнять — читай `NEXT:`. - Пропускать вызов `spec-status`, даже если «кажется, что фаза уже ✅» — скрипт решает. - bash-запуск `python3 .opencode/scripts/spec-status.py` — детерминированный deny-rule (см. ADR-019). Только нативный tool `spec-status`. -- Главному агенту: edit/write/read файлов (всё через subagent), memory_search (через subagent), gh issue create (через subagent). +- Главному агенту: edit/write/read файлов (всё через subagent), memory-search (через subagent), gh issue create (через subagent). - Формулировать вопросы не из question templates ниже. - Предлагать стек вне hardcoded default stack по типу проекта. - Запускать /run-pipeline (стоп на issues — дальше юзер сам). @@ -119,7 +119,7 @@ Prompt template C (см. ниже). Опиши модули (1 строка на модуль): ``` -Prompt template D (см. ниже, с memory_search). +Prompt template D (см. ниже, с memory-search). ### Phase 4: DB_SCHEMA (вопрос юзеру + subagent) @@ -208,7 +208,7 @@ Spec complete. Issues: #N1, #N2, ... status: in_progress --- (создай директорию docs/spec/ через `mkdir -p docs/spec` если не существует) -4. `memory_search("reference repo")` → верни список релевантных memory paths. +4. `memory-search("reference repo")` → верни список релевантных memory paths. 5. Верни: {spec_exists: bool, current_phase: int, references: [...]}. ``` @@ -243,12 +243,12 @@ Default stack для типа (хардкод, добавить всегда): 4. Верни: "done: stack.md created, / mandatory items". ``` -### Template D (modules / Phase 3, с memory_search) +### Template D (modules / Phase 3, с memory-search) ``` Контекст: Phase 3 (modules) для проекта типа . Ответ юзера: . -1. memory_search("reference repo ") — доберёт паттерны из reference repos. +1. memory-search("reference repo ") — доберёт паттерны из reference repos. 2. Сформируй ## Модули (bullet list) + ## Структура (дерево) на основе ответа + референсов. 3. Создай docs/spec/modules.md с обеими секциями. edit docs/spec/meta.md frontmatter phase=3. 4. Верни summary (5-10 строк) для показа юзеру. @@ -320,5 +320,5 @@ Default stack для типа (хардкод, добавить всегда): - Скрипт read-only (только presence check в `docs/spec/*.md`, без мутаций). - После каждой фазы → 1 строка прогресса юзеру. - Если subagent error → 1 retry, потом STOP + report пользователю. -- Главный агент = оркестратор: `spec-status` tool + вопрос юзеру + task(general) делегирование. Не делает edit/memory_search/gh сам. +- Главный агент = оркестратор: `spec-status` tool + вопрос юзеру + task(general) делегирование. Не делает edit/memory-search/gh сам. - Стоп на issues — дальше юзер сам /run-pipeline. \ No newline at end of file diff --git a/.opencode/tools/memory-access.ts b/.opencode/tools/memory-access.ts index 26a4fb0..5983e16 100644 --- a/.opencode/tools/memory-access.ts +++ b/.opencode/tools/memory-access.ts @@ -30,7 +30,7 @@ export default tool({ "Record that a memory file was accessed (read and used). Updates last_accessed date and increments access_count in frontmatter. " + "Call this AFTER reading a memory file that you actually used to inform your work — not for casual browsing. " + "This helps the memory system track which memories are actively useful vs. stale. " + - "Atomic write (tmp + rename). Does NOT commit — the next memory_save will sync the change.", + "Atomic write (tmp + rename). Does NOT commit — the next memory-save will sync the change.", args: { path: tool.schema .string() diff --git a/.opencode/tools/memory-doctor.ts b/.opencode/tools/memory-doctor.ts index 49f4f31..2e6ff33 100644 --- a/.opencode/tools/memory-doctor.ts +++ b/.opencode/tools/memory-doctor.ts @@ -56,7 +56,7 @@ function checkIndex(memoryDir: string): string[] { const lines: string[] = [] lines.push( `- ${mark(fs.existsSync(memoryDir))} OPENCODE_MEMORY_DIR exists: ${memoryDir}${ - fs.existsSync(memoryDir) ? "" : "— run memory_save to auto-setup" + fs.existsSync(memoryDir) ? "" : "— run memory-save to auto-setup" }` ) if (!fs.existsSync(memoryDir)) return lines @@ -64,7 +64,7 @@ function checkIndex(memoryDir: string): string[] { const gitDir = path.join(memoryDir, ".git") lines.push( `- ${mark(fs.existsSync(gitDir))} memory dir is a git repo: ${ - fs.existsSync(gitDir) ? "yes" : "no — will be init on first memory_save" + fs.existsSync(gitDir) ? "yes" : "no — will be init on first memory-save" }` ) const indexJson = path.join(memoryDir, ".rag", "index.json") @@ -72,7 +72,7 @@ function checkIndex(memoryDir: string): string[] { `- ${mark(fs.existsSync(indexJson))} RAG index exists: ${ fs.existsSync(indexJson) ? path.relative(memoryDir, indexJson) - : "not built — first memory_save with OPENAI_BASE_URL will trigger reindex" + : "not built — first memory-save with OPENAI_BASE_URL will trigger reindex" }` ) if (fs.existsSync(indexJson)) { @@ -106,7 +106,7 @@ export default tool({ description: "Read-only diagnostic for the memory subsystem. Reports: ripgrep (keyword) availability, Python src.memory importability, " + "env vars (OPENAI_BASE_URL, OPENAI_API_KEY, OPENCODE_MEMORY_REMOTE), memory dir existence, RAG index existence. " + - "Does NOT modify anything. Run when memory_search/memory_save misbehave or to verify setup.", + "Does NOT modify anything. Run when memory-search/memory-save misbehave or to verify setup.", args: {}, async execute(_args, context) { const memoryDir = resolveMemoryDir() diff --git a/.opencode/tools/memory-search.ts b/.opencode/tools/memory-search.ts index 1fe648b..8bcd4d6 100644 --- a/.opencode/tools/memory-search.ts +++ b/.opencode/tools/memory-search.ts @@ -305,7 +305,7 @@ export default tool({ const rgBin = resolveRgBinary({ allowSystemFallback: true }) if (!fs.existsSync(memoryDir)) { - return `No memories found for: "${args.query}" (memory directory not initialized — run memory_save to auto-setup)` + return `No memories found for: "${args.query}" (memory directory not initialized — run memory-save to auto-setup)` } let resultMap = new Map() diff --git a/AGENTS.md b/AGENTS.md index b0d13fa..46f7a4f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -35,7 +35,7 @@ fallback на raw bash, НЕ импровизируй обход через `gh | `post-docs-review({ pr_number, verdict, body })` | `gh pr comment` для verdict | Docs review verdict (heading `## Docs Review Summary` + verdict) | Сообщи оркестратору, не `gh pr comment` | | `pipeline-status({ pr_number })` | `python3 .opencode/scripts/pipeline-status.py` | Read-only oracle: статус фаз PR + NEXT action | Сообщи оркестратору, не bash-запуск скрипта | | `spec-status({})` | `python3 .opencode/scripts/spec-status.py` | Read-only oracle: текущая фаза spec + NEXT action | Сообщи оркестратору, не bash-запуск скрипта | -| `memory-doctor()` | — | Read-only диагностика памяти: ripgrep, Python src.memory, env vars, index (замена `memory-setup`) | Сообщи оркестратору, не raw bash | +| `memory-doctor()` | — | Read-only диагностика памяти: ripgrep, Python src.memory, env vars, index | Сообщи оркестратору, не raw bash | | `memory-save()` | — | Commit + reindex opencode-memory после записи/редактирования (auto-setup: git init + hook если remote set) | Сообщи оркестратору, не raw bash | | `memory-search({ query, category? })` | — | Гибридный поиск (ripgrep keyword + Python semantic) по opencode-memory | Сообщи оркестратору, не raw bash | | `memory-list({ category? })` | — | Список категорий / файлов opencode-memory | Сообщи оркестратору, не raw bash | diff --git a/docs/decisions/046-pr-104-reindex-skill-update.md b/docs/decisions/046-pr-104-reindex-skill-update.md new file mode 100644 index 0000000..c56e720 --- /dev/null +++ b/docs/decisions/046-pr-104-reindex-skill-update.md @@ -0,0 +1,67 @@ +# ADR-046: Reindex memory + finalize kebab-case tool names in docs (PR #104) + +## Статус +Accepted (2026-07-26) + +## Контекст + +После серии PR #100–#103 (Python lazy-init, 5 TS tools, E2E tests, remove plugin + Rust) +инфраструктура памяти была готова, но: +1. Индекс `.rag/index.json` существовал (220 MB, 2379 entries), но `meta.json` + отсутствовал → Python incremental reindex не мог определить version mismatch + при будущих запусках (fallback к full reindex или silent skip). +2. Документация (SKILL.md, AGENTS.md, 6 skills, memory-syncer agent, 3 TS tools, + spec command) содержала stale snake_case tool names (`memory_save`, + `memory_search`) — агенты могли вызывать несуществующие tools и получать + "tool not found". +3. `OPENAI_EMBEDDING_BATCH_DELAY` была в issue #99 как требование (throttle + между батчами для OpenRouter rate limits), но `embedder.py` не поддерживал + эту env var — добавление в `.env.example` без поддержки в коде вводило бы + в заблуждение. +4. Memory file `technical/rag-cli-embeddings-model.md` описывал Rust rag-cli + (candle-transformers, all-MiniLM-L6-v2, 384-dim, GLIBC 2.39) как активный — + агенты могли пытаться использовать удалённый путь (PR #103 удалил Rust). + +## Решение + +1. **Переиндекс через Python** — `uv run python -m src.memory index`. Создан + `.rag/meta.json` с `version: "qwen/qwen3-embedding-8b:512:64"` (Python формат, + `version` key — НЕ `model_id` который был Rust). Incremental: 86 изменившихся + файлов, 286s, 2584 entries total (merged со старым index.json). + +2. **`OPENAI_EMBEDDING_BATCH_DELAY` — поддержка в коде + `.env.example`**: + - `embedder.py`: `BATCH_DELAY = float(os.environ.get("OPENAI_EMBEDDING_BATCH_DELAY", "0"))` + - `time.sleep(BATCH_DELAY)` между батчами (только если `> 0` и не последний батч) + - `.env.example`: `OPENAI_EMBEDDING_BATCH_DELAY=0.5` + - Default `0` — backwards compatible, не ломает существующие вызовы. + +3. **Stale references cleanup** — `memory_save`/`memory_search`/`memory_setup` + (snake_case) → `memory-save`/`memory-search`/`memory-doctor` (kebab-case) в + всех активных файлах (`.opencode/skills/`, `.opencode/agents/`, `.opencode/tools/`, + `.opencode/commands/`, `AGENTS.md`). Исторические `docs/decisions/` и + `docs/handoff/` НЕ тронуты (snake_case отражает реальные имена на момент + написания). + +4. **Memory file `rag-cli-embeddings-model.md` переписан** — Rust rag-cli → + Python + OpenRouter как единственный путь semantic. Зафиксировано что удалено. + +5. **SKILL.md (memory)** — добавлена секция "Архитектура (progressive + enhancement)": keyword всегда, semantic если `OPENAI_BASE_URL` set, fallback, + auto-setup, `memory-doctor` как read-only диагностика. Default путь исправлен: + `~/opencode-memory` → `/root/.local/share/opencode/opencode-memory`. + +## Альтернативы + +- **Не добавлять `OPENAI_EMBEDDING_BATCH_DELAY` в код, только в `.env.example`** — + отклонено: env var в `.env.example` без поддержки в коде вводит в заблуждение + (пользователь думает что delay работает, но код его игнорирует). Минимальная + правка embedder.py (3 строки) делает её рабочей. + +- **Трогать исторические `docs/decisions/` и `docs/handoff/`** — отклонено: ADR + и handoffs — исторические записи, snake_case там отражает реальные имена tools + на момент написания (до PR #101 переименования). Правка исказила бы историю. + +- **Полный reindex (удалить старый index.json)** — отклонено: incremental reindex + по SHA256 (ADR-036) достаточно. 86 изменившихся файлов переиндексировано, + остальные merged. Полный reindex = 13 минут + $0.0043, incremental = 286s + + $0.00012. Нет причины делать полный если incremental корректен. \ No newline at end of file diff --git a/docs/handoff/pr-104-reindex-skill-update.md b/docs/handoff/pr-104-reindex-skill-update.md new file mode 100644 index 0000000..c2489be --- /dev/null +++ b/docs/handoff/pr-104-reindex-skill-update.md @@ -0,0 +1,104 @@ +--- +pr: 104 +title: Reindex memory + update SKILL.md/AGENTS.md for kebab-case tools +--- + +## Что сделано + +Финальная фаза починки памяти (issue #99, часть #94). После PR #100–#103 +(Python safe, 5 TS tools, E2E tests, remove plugin + Rust) — переиндекс + docs. + +### 1. Переиндекс памяти через Python + +Запущен `uv run python -m src.memory index` из `/root/workspace/opencode-config`. +Env vars (`OPENAI_BASE_URL`, `OPENAI_API_KEY`, `OPENAI_EMBEDDING_MODEL=qwen/qwen3-embedding-8b`) +наследованы из окружения — OpenRouter ответил. + +Результат: +- `.rag/index.json` — 239 MB (было 220 MB), 2584 entries (было 2379), 4096-dim embeddings +- `.rag/meta.json` — создан, `version: "qwen/qwen3-embedding-8b:512:64"` (Python формат, `version` key — НЕ `model_id` который был Rust) +- `.rag/reindex.log` — финальная строка `done: total=86 took=286s changed=86 unchanged=0 deleted=0` (incremental: 86 изменившихся файлов переиндексировано, остальные merged из старого index.json) +- `.rag/index.bin` — НЕ существует (Rust, удалён в PR #103) +- Время: 2026-07-26 22:55 (свежее) + +### 2. `OPENAI_EMBEDDING_BATCH_DELAY` — добавлена поддержка в код + `.env.example` + +Issue #99 просил добавить `OPENAI_EMBEDDING_BATCH_DELAY=0.5` в `.env.example`, но +embedder.py НЕ поддерживал эту env var. Добавлена: +- `src/memory/embedder.py`: `BATCH_DELAY = float(os.environ.get("OPENAI_EMBEDDING_BATCH_DELAY", "0"))` + `time.sleep(BATCH_DELAY)` между батчами (только если `BATCH_DELAY > 0` и не последний батч) +- `.env.example`: `OPENAI_EMBEDDING_BATCH_DELAY=0.5` с комментарием про OpenRouter rate limits +- Default `0` (без задержки) — backwards compatible, не ломает существующие вызовы + +Проверка: env var уже set в окружении (`=1`), import OK, E2E 4 passed. + +### 3. SKILL.md (memory) — обновлён + +`.opencode/skills/memory/SKILL.md`: +- Добавлена секция "Аритектура (progressive enhancement)": keyword всегда, semantic если `OPENAI_BASE_URL` set, fallback (OpenRouter упал → keyword), auto-setup (первый `memory-save` всё создаёт), `memory-doctor` как read-only диагностика +- Убрано упоминание `@mathew-cf/opencode-memory` plugin (было "Плагин удалён" — стало "без wrapper-hop, без MCP-плагина") +- Убрано упоминание `memory-setup` (удалённый tool) +- Исправлен default путь: `~/opencode-memory` (устаревший) → `/root/.local/share/opencode/opencode-memory` (актуальный, соответствует `_memory-shared.ts:17`) +- Tools уже были kebab-case (`memory-search`, `memory-save`, `memory-list`, `memory-access`, `memory-doctor`) — проверено, не требовало правок + +### 4. AGENTS.md — обновлён + +`AGENTS.md` (root): +- Tool Usage Policy таблица — уже содержала 5 kebab-case tools (`memory-doctor`, `memory-save`, `memory-search`, `memory-list`, `memory-access`) +- Убрано упоминание `memory-setup` из описания `memory-doctor` (было "замена `memory-setup`" — стало чистое описание) + +### 5. Stale references — найдены и обновлены + +Поиск `grep -rnE "memory_(search|save|list|access|setup|doctor)"` по `.opencode/`, `docs/`, `AGENTS.md`: + +**Активные файлы (обновлены):** +- `.opencode/skills/memory/SKILL.md` — `memory-setup` упоминание убрано +- `.opencode/skills/run-pipeline/SKILL.md` — `memory_save` → `memory-save` (2 места), `app_data/opencode-memory` → `/repos/...` +- `.opencode/skills/repo-init/SKILL.md` — `memory_save` → `memory-save` (gotcha-блок) +- `.opencode/skills/release/SKILL.md` — `memory_save` → `memory-save` (gotcha-блок) +- `.opencode/skills/configure-opencode/SKILL.md` — `memory_save` → `memory-save` (gotcha-блок) +- `.opencode/skills/add-skill/SKILL.md` — `memory_save` → `memory-save` (gotcha-блок) +- `.opencode/skills/spec/SKILL.md` — `memory_search` → `memory-search` (6 мест, replaceAll) +- `.opencode/skills/issue/SKILL.md` — `app_data/opencode-memory` → `/repos/...` +- `.opencode/agents/memory-syncer.md` — `memory_save` → `memory-save` (2 места), `app_data/opencode-memory` → `` (3 места) +- `.opencode/commands/spec.md` — `memory_search` → `memory-search` +- `.opencode/tools/memory-access.ts` — `memory_save` → `memory-save` (user-facing message) +- `.opencode/tools/memory-doctor.ts` — `memory_save`/`memory_search` → `memory-save`/`memory-search` (4 user-facing messages) +- `.opencode/tools/memory-search.ts` — `memory_save` → `memory-save` (user-facing message) + +**Исторические файлы (НЕ тронуты — snake_case отражает реальные имена на момент написания):** +- `docs/decisions/*.md` — 20+ ADR (ADR-014, 022, 023, 024, 026, 030, 032, 033, 035, 036, 037, 038, 039, 043, 045) +- `docs/handoff/*.md` — 15+ handoffs (pr-36, 53, 57, 63, 71, 75, 77, 78, 80, 83, 89, 101, 103) + +### 6. Memory file `technical/rag-cli-embeddings-model.md` — переписан + +`~/.local/share/opencode/opencode-memory/technical/rag-cli-embeddings-model.md`: +- Был: описание Rust rag-cli как активного (candle-transformers, all-MiniLM-L6-v2, 384-dim, GLIBC 2.39) +- Стал: описание Python + OpenRouter как единственного пути semantic (qwen/qwen3-embedding-8b, 4096-dim, progressive enhancement, auto-setup) +- Зафиксировано что удалено: `@mathew-cf/opencode-memory` plugin, `@mathew-cf/rag-cli` Rust binary, `setup-memory.sh`, `memory-setup.ts`, `.rag/index.bin`, `meta.json` с `model_id` +- Ссылки на ADR-030, 036, 043, 045 + +### 7. E2E тест + +`RUN_LIVE=1 uv run pytest tests/test_memory_tools_e2e.py -x -q --no-cov` — 4 passed (до и после правок embedder.py). + +## Почему + +PR #100–#103 сделали инфраструктуру (Python safe, 5 TS tools, E2E, remove plugin + Rust), +но: +- Индекс был старый (17:19, без `meta.json` — Python incremental reindex не мог определить version mismatch) +- Документация (SKILL.md, AGENTS.md, skills, agents, TS tools) содержала stale snake_case tool names (`memory_save`, `memory_search`) — agents могли вызывать несуществующие tools +- `OPENAI_EMBEDDING_BATCH_DELAY` была в issue #99 как требование, но не поддерживалась в коде — добавление в `.env.example` без поддержки вводило бы в заблуждение +- Memory file `rag-cli-embeddings-model.md` описывал Rust rag-cli как активный — агенты могли пытаться использовать удалённый путь + +Без этого PR: semantic search работал (index.json был), но docs лгали агентам про tool names → агенты могли вызывать `memory_save` (snake_case) и получать "tool not found". + +## Pending + +— (переиндекс выполнен, env vars были в окружении) + +## Watch out + +- **Reindex требует env vars** (`OPENAI_BASE_URL`, `OPENAI_API_KEY`). Без них `embed_texts` возвращает `None`, индекс не строится. Keyword search продолжает работать. Если env vars отсутствуют при деплое — reindex нужно запустить вручную после настройки env. +- **`OPENAI_EMBEDDING_BATCH_DELAY` default `0`** в коде — backwards compatible. `.env.example` рекомендует `0.5` для OpenRouter. В текущем окружении set `=1` (унаследовано). +- **`docs/decisions/` и `docs/handoff/` содержат snake_case** (`memory_save`, `memory_search`) — это исторические записи, отражают реальные имена tools на момент написания. НЕ трогать. +- **Reindex был incremental** (86 changed файлов, 286s) — не полный. Полный reindex при `meta.json` отсутствии или `version` mismatch. Если нужен полный — удалить `.rag/meta.json` + `.rag/index.json` и запустить снова. \ No newline at end of file diff --git a/src/memory/embedder.py b/src/memory/embedder.py index de9b23e..b3138de 100644 --- a/src/memory/embedder.py +++ b/src/memory/embedder.py @@ -7,6 +7,7 @@ from tenacity import RetryError, retry, retry_if_exception, stop_after_attempt, EMBEDDING_MODEL = os.environ.get("OPENAI_EMBEDDING_MODEL", "gemini-embedding-2-preview") BATCH_SIZE = int(os.environ.get("OPENAI_EMBEDDING_BATCH_SIZE", "2048")) +BATCH_DELAY = float(os.environ.get("OPENAI_EMBEDDING_BATCH_DELAY", "0")) def _is_retryable(exc: BaseException) -> bool: @@ -40,6 +41,24 @@ def _call_embedding_api( return [d["embedding"] for d in data["data"]] +def _embed_in_batches( + client: httpx.Client, + url: str, + headers: dict[str, str], + texts: list[str], +) -> list[list[float]]: + n_batches = (len(texts) + BATCH_SIZE - 1) // BATCH_SIZE + results: list[list[float]] = [] + for i in range(0, len(texts), BATCH_SIZE): + batch = texts[i : i + BATCH_SIZE] + payload: dict[str, str | list[str]] = {"model": EMBEDDING_MODEL, "input": batch} + print(f" batch {i // BATCH_SIZE + 1}/{n_batches}...", flush=True) + results.extend(_call_embedding_api(client, url, headers, payload)) + if BATCH_DELAY > 0 and i + BATCH_SIZE < len(texts): + time.sleep(BATCH_DELAY) + return results + + def embed_texts(texts: list[str]) -> list[list[float]] | None: if not texts: return [] @@ -58,14 +77,6 @@ def embed_texts(texts: list[str]) -> list[list[float]] | None: if len(texts) <= BATCH_SIZE: payload: dict[str, str | list[str]] = {"model": EMBEDDING_MODEL, "input": texts} return _call_embedding_api(client, url, headers, payload) - - n_batches = (len(texts) + BATCH_SIZE - 1) // BATCH_SIZE - results: list[list[float]] = [] - for i in range(0, len(texts), BATCH_SIZE): - batch = texts[i : i + BATCH_SIZE] - payload = {"model": EMBEDDING_MODEL, "input": batch} - print(f" batch {i // BATCH_SIZE + 1}/{n_batches}...", flush=True) - results.extend(_call_embedding_api(client, url, headers, payload)) - return results + return _embed_in_batches(client, url, headers, texts) except (httpx.HTTPError, RetryError, ValueError, KeyError): return None