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 <agent@opencode.local>
This commit is contained in:
Sergey 2026-07-27 02:08:35 +03:00 committed by GitHub
parent 9d654a2201
commit 4cf149cb02
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 233 additions and 41 deletions

View file

@ -11,6 +11,7 @@ OPENAI_BASE_URL=https://openrouter.ai/api/v1
OPENAI_API_KEY=your-openrouter-api-key OPENAI_API_KEY=your-openrouter-api-key
OPENAI_EMBEDDING_MODEL=qwen/qwen3-embedding-8b OPENAI_EMBEDDING_MODEL=qwen/qwen3-embedding-8b
OPENAI_EMBEDDING_BATCH_SIZE=50 OPENAI_EMBEDDING_BATCH_SIZE=50
OPENAI_EMBEDDING_BATCH_DELAY=0.5
MEMORY_CHUNK_SIZE=512 MEMORY_CHUNK_SIZE=512
MEMORY_CHUNK_OVERLAP=64 MEMORY_CHUNK_OVERLAP=64

View file

@ -32,7 +32,7 @@ permission:
"gh issue view*": allow "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 `<memory_dir>/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. 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. 1. Get the PR number from the invocation prompt.
2. Find the merged handoff file: `ls docs/handoff/pr-<N>-*` to discover the slug, then `cat docs/handoff/pr-<N>-<slug>.md` to read it (read-only — agent does not check out branches). 2. Find the merged handoff file: `ls docs/handoff/pr-<N>-*` to discover the slug, then `cat docs/handoff/pr-<N>-<slug>.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`). 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) → `<memory_dir>/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/`) → `<memory_dir>/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. 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 ## Distillation
@ -80,14 +80,14 @@ If a fact is already recorded — update the entry (bump `updated` in frontmatte
## Save ## Save
1. After editing the memory file, call `memory_save` to commit + re-index the isolated memory repo. 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. 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 <path>` manually.
## Rules ## 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. 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). 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 `<memory_dir>/repos/{host}/{org}/{repo}.md`.
4. ONLY read files under `docs/handoff/` and `docs/decisions/`. 4. ONLY read files under `docs/handoff/` and `docs/decisions/`.
5. Receipt is mandatory even if no durable records found. 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). 6. If memory file doesn't exist — create it with proper frontmatter (title, tags, summary, created, updated, importance).

View file

@ -2,4 +2,4 @@
description: Run spec — interactive spec generation for new project description: Run spec — interactive spec generation for new project
agent: build 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. 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.

View file

@ -73,7 +73,7 @@ git status # проверь staged set — только SKILL.md, без лиш
``` ```
> `commit` tool НЕ делает `git add` — коммитит только уже staged файлы. Если > `commit` tool НЕ делает `git add` — коммитит только уже staged файлы. Если
> в индексе лишнее (например `memory_save` stage'нул всё через `git add -A`) > в индексе лишнее (например `memory-save` stage'нул всё через `git add -A`)
> — не коммить: сначала `git restore --staged <file>` или не stage'и его > — не коммить: сначала `git restore --staged <file>` или не stage'и его
> изначально. Используй `git add <конкретные-пути>`, НЕ `git add -A`. > изначально. Используй `git add <конкретные-пути>`, НЕ `git add -A`.

View file

@ -22,7 +22,7 @@ description: Use when adding, changing, or removing MCP servers, providers, perm
- Перед каждым `commit``git status` для проверки staged set. `commit` tool - Перед каждым `commit``git status` для проверки staged set. `commit` tool
НЕ делает `git add` — коммитит только уже staged файлы. Используй НЕ делает `git add` — коммитит только уже staged файлы. Используй
`git add <конкретные-пути>`, НЕ `git add -A` (иначе лишние файлы уйдут в `git add <конкретные-пути>`, НЕ `git add -A` (иначе лишние файлы уйдут в
коммит). Если в индексе лишнее (например `memory_save` stage'нул всё через коммит). Если в индексе лишнее (например `memory-save` stage'нул всё через
`git add -A`) — сначала `git restore --staged <file>`, потом коммить. `git add -A`) — сначала `git restore --staged <file>`, потом коммить.
- В клонах: `git pull` + рестарт opencode (MCP-серверы, skills, agents грузятся при старте — см. `add-skill/SKILL.md`). До рестарта правки не видны. - В клонах: `git pull` + рестарт opencode (MCP-серверы, skills, agents грузятся при старте — см. `add-skill/SKILL.md`). До рестарта правки не видны.
- Для Docker-сетапа: `git pull` на хосте + `docker compose restart opencode` (или эквивалент) — MCP/skills/agents грузятся при старте контейнера. - Для Docker-сетапа: `git pull` на хосте + `docker compose restart opencode` (или эквивалент) — MCP/skills/agents грузятся при старте контейнера.

View file

@ -160,6 +160,6 @@ Label выбирай по типу задачи (совпадает с commit `t
4. **Merge or Repeat** — APPROVE → `merge-pr({ pr_number: N })` tool (squash + 4. **Merge or Repeat** — APPROVE → `merge-pr({ pr_number: N })` tool (squash +
delete branch, без `--admin`; НЕ raw `gh pr merge` — заблокирован deny), delete branch, без `--admin`; НЕ raw `gh pr merge` — заблокирован deny),
после CI ✅; замечания → fix subagent → re-review → merge. после 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 в `<memory_dir>/repos/{host}/{org}/{repo}.md`.
См. `AGENTS.md` (Development Workflow) и `run-pipeline` skill — все три документа описывают одну и ту же full-subagent модель делегирования. См. `AGENTS.md` (Development Workflow) и `run-pipeline` skill — все три документа описывают одну и ту же full-subagent модель делегирования.

View file

@ -6,7 +6,15 @@ description: Инструкция по работе с файловой памя
# File Memory (opencode-memory) # File Memory (opencode-memory)
Графовая память (Graphiti/FalkorDB) удалена — была нестабильна и забагована. Графовая память (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-search({ query, category? })` | Гибридный поиск (ripgrep keyword + Python semantic) |
| `memory-list({ category? })` | Список категорий / файлов | | `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-access({ path })` | Отметить файл как прочитанный (bump `last_accessed`/`access_count`) |
| `memory-doctor()` | Read-only диагностика: ripgrep, Python `src.memory`, env vars, index | | `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 {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`), но не оба сразу.
### Формат записей ### Формат записей

View file

@ -45,7 +45,7 @@ git status # проверь staged set — только CHANGELOG.md, без л
``` ```
> `commit` tool НЕ делает `git add` — коммитит только уже staged файлы. Если > `commit` tool НЕ делает `git add` — коммитит только уже staged файлы. Если
> в индексе лишнее (например `memory_save` stage'нул всё через `git add -A`) > в индексе лишнее (например `memory-save` stage'нул всё через `git add -A`)
> — не коммить: сначала `git restore --staged <file>` или не stage'и его > — не коммить: сначала `git restore --staged <file>` или не stage'и его
> изначально. Используй `git add <конкретные-пути>`, НЕ `git add -A`. > изначально. Используй `git add <конкретные-пути>`, НЕ `git add -A`.

View file

@ -42,7 +42,7 @@ git status # проверь staged set — только README.md, без ли
``` ```
> `commit` tool НЕ делает `git add` — коммитит только уже staged файлы. Если > `commit` tool НЕ делает `git add` — коммитит только уже staged файлы. Если
> в индексе лишнее (например `memory_save` stage'нул всё через `git add -A`) > в индексе лишнее (например `memory-save` stage'нул всё через `git add -A`)
> — не коммить: сначала `git restore --staged <file>` или не stage'и его > — не коммить: сначала `git restore --staged <file>` или не stage'и его
> изначально. Используй `git add <конкретные-пути>`, НЕ `git add -A`. > изначально. Используй `git add <конкретные-пути>`, НЕ `git add -A`.

View file

@ -54,7 +54,7 @@ description: Автономный исполнитель PR-пайплайна.
Перед КАЖДЫМ `commit``git status` для проверки staged set. `commit` Перед КАЖДЫМ `commit``git status` для проверки staged set. `commit`
tool НЕ делает `git add` — коммитит только уже staged файлы. Используй tool НЕ делает `git add` — коммитит только уже staged файлы. Используй
`git add <конкретные-пути>`, НЕ `git add -A` (иначе лишние файлы уйдут в `git add <конкретные-пути>`, НЕ `git add -A` (иначе лишние файлы уйдут в
коммит). Если в индексе лишнее (например `memory_save` stage'нул всё коммит). Если в индексе лишнее (например `memory-save` stage'нул всё
через `git add -A`) — не коммить: сначала `git restore --staged <file>`. через `git add -A`) — не коммить: сначала `git restore --staged <file>`.
5. Push ветку (`git push -u origin HEAD`), затем создай PR через tool: 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 })`. `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 <run-id> --log-failed` output:
``` ```
Дистиллируй PR#M в memory file Дистиллируй PR#M в memory file
`app_data/opencode-memory/repos/{host}/{org}/{repo}.md` `<memory_dir>/repos/{host}/{org}/{repo}.md`
(путь относительно корня репо; `{host}/{org}/{repo}` вычисли через (default `~/.local/share/opencode/opencode-memory`, override через `OPENCODE_MEMORY_DIR`;
`{host}/{org}/{repo}` вычисли через
`git remote get-url origin` — см. `memory-syncer.md:38`). `git remote get-url origin` — см. `memory-syncer.md:38`).
1. Прочитай `docs/handoff/pr-M-*.md` и `docs/decisions/*-pr-M-*.md` с master 1. Прочитай `docs/handoff/pr-M-*.md` и `docs/decisions/*-pr-M-*.md` с master
(`git checkout master && git pull`). (`git checkout master && git pull`).
@ -146,8 +147,8 @@ Log: `gh run view <run-id> --log-failed` output:
3. Добавь записи формата `- [YYYY-MM-DD, PR#M] <summary>` в конец файла 3. Добавь записи формата `- [YYYY-MM-DD, PR#M] <summary>` в конец файла
(секция "Handoff digest"). (секция "Handoff digest").
4. Квитанция ВСЕГДА (даже если durable нет): `- [date, PR#M] — (нет durable-записей)`. 4. Квитанция ВСЕГДА (даже если durable нет): `- [date, PR#M] — (нет durable-записей)`.
5. `memory_save` для commit + reindex + push. 5. `memory-save` для commit + reindex + push.
6. Проверь `git status` основного репо — если staged что-то в `app_data/`, 6. Проверь `git status` основного репо — если staged что-то в memory dir,
репорт пользователю (guard от случайного коммита в master). репорт пользователю (guard от случайного коммита в master).
``` ```

View file

@ -26,7 +26,7 @@ truth для порядка и действий — `spec-status` tool. На в
- Импровизировать порядок. Решать сам какую фазу выполнять — читай `NEXT:`. - Импровизировать порядок. Решать сам какую фазу выполнять — читай `NEXT:`.
- Пропускать вызов `spec-status`, даже если «кажется, что фаза уже ✅» — скрипт решает. - Пропускать вызов `spec-status`, даже если «кажется, что фаза уже ✅» — скрипт решает.
- bash-запуск `python3 .opencode/scripts/spec-status.py` — детерминированный deny-rule (см. ADR-019). Только нативный tool `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 ниже. - Формулировать вопросы не из question templates ниже.
- Предлагать стек вне hardcoded default stack по типу проекта. - Предлагать стек вне hardcoded default stack по типу проекта.
- Запускать /run-pipeline (стоп на issues — дальше юзер сам). - Запускать /run-pipeline (стоп на issues — дальше юзер сам).
@ -119,7 +119,7 @@ Prompt template C (см. ниже).
Опиши модули (1 строка на модуль): Опиши модули (1 строка на модуль):
``` ```
Prompt template D (см. ниже, с memory_search). Prompt template D (см. ниже, с memory-search).
### Phase 4: DB_SCHEMA (вопрос юзеру + subagent) ### Phase 4: DB_SCHEMA (вопрос юзеру + subagent)
@ -208,7 +208,7 @@ Spec complete. Issues: #N1, #N2, ...
status: in_progress status: in_progress
--- ---
(создай директорию docs/spec/ через `mkdir -p docs/spec` если не существует) (создай директорию 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: [...]}. 5. Верни: {spec_exists: bool, current_phase: int, references: [...]}.
``` ```
@ -243,12 +243,12 @@ Default stack для типа (хардкод, добавить всегда):
4. Верни: "done: stack.md created, <N>/<M> mandatory items". 4. Верни: "done: stack.md created, <N>/<M> mandatory items".
``` ```
### Template D (modules / Phase 3, с memory_search) ### Template D (modules / Phase 3, с memory-search)
``` ```
Контекст: Phase 3 (modules) для проекта типа <type>. Контекст: Phase 3 (modules) для проекта типа <type>.
Ответ юзера: <answers>. Ответ юзера: <answers>.
1. memory_search("reference repo <module>") — доберёт паттерны из reference repos. 1. memory-search("reference repo <module>") — доберёт паттерны из reference repos.
2. Сформируй ## Модули (bullet list) + ## Структура (дерево) на основе ответа + референсов. 2. Сформируй ## Модули (bullet list) + ## Структура (дерево) на основе ответа + референсов.
3. Создай docs/spec/modules.md с обеими секциями. edit docs/spec/meta.md frontmatter phase=3. 3. Создай docs/spec/modules.md с обеими секциями. edit docs/spec/meta.md frontmatter phase=3.
4. Верни summary (5-10 строк) для показа юзеру. 4. Верни summary (5-10 строк) для показа юзеру.
@ -320,5 +320,5 @@ Default stack для типа (хардкод, добавить всегда):
- Скрипт read-only (только presence check в `docs/spec/*.md`, без мутаций). - Скрипт read-only (только presence check в `docs/spec/*.md`, без мутаций).
- После каждой фазы → 1 строка прогресса юзеру. - После каждой фазы → 1 строка прогресса юзеру.
- Если subagent error → 1 retry, потом STOP + report пользователю. - Если 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. - Стоп на issues — дальше юзер сам /run-pipeline.

View file

@ -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. " + "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. " + "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. " + "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: { args: {
path: tool.schema path: tool.schema
.string() .string()

View file

@ -56,7 +56,7 @@ function checkIndex(memoryDir: string): string[] {
const lines: string[] = [] const lines: string[] = []
lines.push( lines.push(
`- ${mark(fs.existsSync(memoryDir))} OPENCODE_MEMORY_DIR exists: ${memoryDir}${ `- ${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 if (!fs.existsSync(memoryDir)) return lines
@ -64,7 +64,7 @@ function checkIndex(memoryDir: string): string[] {
const gitDir = path.join(memoryDir, ".git") const gitDir = path.join(memoryDir, ".git")
lines.push( lines.push(
`- ${mark(fs.existsSync(gitDir))} memory dir is a git repo: ${ `- ${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") const indexJson = path.join(memoryDir, ".rag", "index.json")
@ -72,7 +72,7 @@ function checkIndex(memoryDir: string): string[] {
`- ${mark(fs.existsSync(indexJson))} RAG index exists: ${ `- ${mark(fs.existsSync(indexJson))} RAG index exists: ${
fs.existsSync(indexJson) fs.existsSync(indexJson)
? path.relative(memoryDir, 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)) { if (fs.existsSync(indexJson)) {
@ -106,7 +106,7 @@ export default tool({
description: description:
"Read-only diagnostic for the memory subsystem. Reports: ripgrep (keyword) availability, Python src.memory importability, " + "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. " + "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: {}, args: {},
async execute(_args, context) { async execute(_args, context) {
const memoryDir = resolveMemoryDir() const memoryDir = resolveMemoryDir()

View file

@ -305,7 +305,7 @@ export default tool({
const rgBin = resolveRgBinary({ allowSystemFallback: true }) const rgBin = resolveRgBinary({ allowSystemFallback: true })
if (!fs.existsSync(memoryDir)) { 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<string, CandInfo>() let resultMap = new Map<string, CandInfo>()

View file

@ -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` | | `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-запуск скрипта | | `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-запуск скрипта | | `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-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-search({ query, category? })` | — | Гибридный поиск (ripgrep keyword + Python semantic) по opencode-memory | Сообщи оркестратору, не raw bash |
| `memory-list({ category? })` | — | Список категорий / файлов opencode-memory | Сообщи оркестратору, не raw bash | | `memory-list({ category? })` | — | Список категорий / файлов opencode-memory | Сообщи оркестратору, не raw bash |

View file

@ -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 корректен.

View file

@ -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``<memory_dir>/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``<memory_dir>/repos/...`
- `.opencode/agents/memory-syncer.md``memory_save``memory-save` (2 места), `app_data/opencode-memory``<memory_dir>` (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` и запустить снова.

View file

@ -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") EMBEDDING_MODEL = os.environ.get("OPENAI_EMBEDDING_MODEL", "gemini-embedding-2-preview")
BATCH_SIZE = int(os.environ.get("OPENAI_EMBEDDING_BATCH_SIZE", "2048")) 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: def _is_retryable(exc: BaseException) -> bool:
@ -40,6 +41,24 @@ def _call_embedding_api(
return [d["embedding"] for d in data["data"]] 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: def embed_texts(texts: list[str]) -> list[list[float]] | None:
if not texts: if not texts:
return [] return []
@ -58,14 +77,6 @@ def embed_texts(texts: list[str]) -> list[list[float]] | None:
if len(texts) <= BATCH_SIZE: if len(texts) <= BATCH_SIZE:
payload: dict[str, str | list[str]] = {"model": EMBEDDING_MODEL, "input": texts} payload: dict[str, str | list[str]] = {"model": EMBEDDING_MODEL, "input": texts}
return _call_embedding_api(client, url, headers, payload) return _call_embedding_api(client, url, headers, payload)
return _embed_in_batches(client, url, headers, texts)
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
except (httpx.HTTPError, RetryError, ValueError, KeyError): except (httpx.HTTPError, RetryError, ValueError, KeyError):
return None return None