opencode-config/docs/handoff/pr-83-incremental-index-sha256-atomic.md
Sergey 25cf10baa6
feat(memory): incremental index with SHA256 + atomic + versioning + flock (#83)
* feat(memory): add SHA256 incremental index with atomic writes, versioning, flock

* feat(memory): parse Retry-After header and add batch progress logging

* test(memory): add incremental, atomic, versioning, retry, batch tests

* docs(memory): update .env.example with OpenRouter defaults

* docs(handoff): add handoff and ADR-036 for incremental index

* docs(handoff): set PR number

* docs(project-map): update index.py and embedder.py descriptions for PR#83

* fix(ci): skip index rewrite on no-op + versioning first-run

* fix(ci): ruff format index.py

---------

Co-authored-by: opencode-agent <agent@opencode.local>
2026-07-26 19:19:55 +03:00

4.6 KiB
Raw Blame History


pr: 83 title: feat(memory): incremental index with SHA256 + atomic + versioning + flock

Что сделано

  • SHA256 инкрементальный индекс в src/memory/index.py: _content_hash(text), .rag/meta.json ({version, files: {path: {sha256, chunks}}}), embed только изменившихся чанков, merge old+new, prune удалённых файлов.
  • Atomic writes: _atomic_write(path, content) через .tmp + os.replace для index.json и meta.json — защита от битого индекса при сбое.
  • Index versioning: version = f"{EMBEDDING_MODEL}:{chunk_size}:{chunk_overlap}" в meta.json, mismatch → полный reindex.
  • Concurrent safety: fcntl.flock(LOCK_EX) на .rag/.lock во время index операции.
  • Rate-limit-aware batching в src/memory/embedder.py: парсинг Retry-After header при 429 → time.sleep(retry_after) → raise; stop_after_attempt(5), wait_exponential(max=30).
  • Прогресс-лог: index.py печатает Embedding N chunks in M batches... перед embed_texts; embedder.py печатает batch K/N... (только если > 1 batch).
  • 10 новых тестов: 8 в tests/test_index.py (incremental add/edit/delete, no-changes-noop, meta persistence, sha256 content hash, atomic writes, versioning, atomic write unit) + test_retry_after_header в tests/test_embedder.py + test_live_embed_qwen3_batch в tests/test_embedder_live.py (skip без RUN_LIVE).
  • .env.example: блок OpenAI Embeddings обновлён с OpenRouter defaults (Qwen3 8B, batch=50, delay=1).

Почему

PR #75 (refactor) + PR #77 (plugin wrapper) сделали Memory CLI рабочим через OpenRouter Qwen3 8B, но полный reindex при каждом memory_save = 2354 embeddings = ~13 минут + $0.0043/save. Инкрементальный индекс по SHA256 от extracted text = embed только изменившихся чанков (~10 per save), ~1 сек + $0.0000128/save (340× экономия бюджета, 780× ускорение). Production-grade практики (atomic writes, versioning, flock) снимают риски: битый index.json при сбое процесса, несовместимые embeddings при смене модели, race condition при concurrent memory index. Retry-After header + увеличенные retry лимиты = корректная обработка OpenRouter rate limits.

Pending

Watch out

  • EMBEDDING_MODEL и BATCH_SIZE читаются из env на import src.memory.embedder — тесты, проверяющие дефолты, должны monkeypatch.delenv + importlib.reload(embedder_mod). Существующие тесты test_embed_texts_default_model/test_embed_texts_batches обновлены: используют monkeypatch.setattr(embedder_mod, "BATCH_SIZE", 2048) вместо依赖имости от env дефолта (контейнер env задаёт OPENAI_EMBEDDING_BATCH_SIZE=50).
  • .rag/meta.json — новый файл. При первом запуске после этого PR — полный reindex (meta.json не существует → needs_full_reindex=True). При последующих — инкрементальный. Старый .rag/index.json (PR #77) совместим по схеме ({files: [{source, chunk_idx, offset, text, embedding}]}), merge сохраняет существующие записи для unchanged файлов.
  • fcntl.flock — POSIX-only. На Windows не работает, но Memory CLI предназначен для Linux/Docker контейнера (python>=3.12, Operating System :: POSIX :: Linux в pyproject). Search НЕ блокирует (читает index.json без lock — допускает stale reads, это OK для RAG).
  • OPENAI_EMBEDDING_BATCH_DELAY=1 в .env.example — documented, но НЕ используется в embedder.py (нет delay между батчами в текущей реализации). Оставлен для документации/future use. Если добавить delay — нужен time.sleep(BATCH_DELAY) в loop.
  • Progress log идёт в stdout. Plugin парсит только JSON в search, не в index — stdout лог безопасен для index команды. Для search (JSON output) логging НЕ добавлялся.
  • ADR-036 (НЕ PR-number-based): sequential нумерация ADR в этом репо (PR#26 docs-reviewer typo зафиксировал правило — ADR = sequential, НЕ PR number).