* 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>
4.6 KiB
4.6 KiB
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-Afterheader при 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 на importsrc.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).