0.21.0

Итерация 17 плана аудита: память — conflict resolution с историей и MMR.

Added

  • Миграция 023_fact_history (sqlite + pg, up/down):
  • В memory_facts добавлены колонки confidence DOUBLE PRECISION DEFAULT 1.0, superseded_by BIGINT REFERENCES memory_facts(id), valid_from TIMESTAMPTZ, valid_to TIMESTAMPTZ.
  • Новая таблица memory_facts_history(id, fact_id, action, old_content, new_content, old_confidence, new_confidence, reason, created_at) — append-only журнал изменений факта.
  • Индексы idx_memory_facts_superseded, idx_memory_facts_history_fact.
  • MemoryFact расширен полями Confidence, SupersededBy, ValidFrom, ValidTo. Новый тип MemoryFactHistory для записей журнала.
  • Store-методы: UpdateFactConfidence(ctx, factID, confidence) (при <0.2 автоматически деактивирует), SupersedeFact(ctx, factID, supersededByID) (атомарно: superseded_by + valid_to + is_active=false), AddFactHistory(ctx, h), ListFactHistory(ctx, factID, limit). Реализованы для SQLite, Postgres, DualStore.
  • LLM-merge в Consolidator.persistFacts: при cosine ≥ 0.95 вместо простого UpdateFact вызывается askMerge — LLM выдаёт решение KEEP_OLD | UPDATE | MERGE | CONFLICT с предложенным content. При CONFLICT оба факта остаются, у старого confidence понижается на 0.3. Все решения фиксируются в memory_facts_history. При ошибке LLM/невалидном JSON — fallback на UPDATE (не блокирует консолидацию).
  • parseJSONObjectInto в internal/memory/json_parse.go — парсер JSON-объекта из ответа LLM с поддержкой markdown-fence и мусора до/после.
  • Промпт conflictResolutionPrompt для LLM-merge с явным форматом ответа {"action":"...","content":"...","reason":"..."}.
  • Dream.execute для deactivate: вместо моментальной деактивации — понижение confidence на 0.3 (через UpdateFactConfidence); при confidence < 0.2 store-метод сам деактивирует. Это даёт «второй шанс» фактам, которые могут оказаться актуальными. История изменений: confidence_lowerdeactivate. Аналогично merge/update/create теперь пишут в history.
  • Dream.execute для merge: дополнительные факты помечаются superseded_by = главного факта (вместо простого DeactivateFact), чтобы UI смог показать «заменён фактом #N».
  • MMR (Maximal Marginal Relevance) в ContextBuilder.buildMemoryContext (internal/agent/mmr.go): отбор разнообразных результатов поверх вектор-поиска. Формула: score(d) = λ·sim(q, d) − (1−λ)·max sim(d, selected), λ=0.7. Применяется к 30 retrieval-кандидатам перед reranker (15 для facts, 10 для chunks). При ошибке embedder’а — fallback на top-K по релевантности.
  • Retrieval pipeline: SearchFacts/SearchChunks теперь берут 30 кандидатов (было 20) → MMR → reranker → top-K. Это улучшает разнообразие фактов в контексте при сохранении релевантности.
  • Handler GET /api/v1/agents/{id}/memory/facts/{factID}/history — список изменений факта (append-only, anti-IDOR через canAccessFact). Параметр ?limit= (max 500, default 100).
  • UI: на странице /agents/[id]/memory для каждого факта добавлены: индикатор c=N.NN (confidence, цветовая шкала), значок → #N (superseded_by), кнопка «История» с разворачиваемым inline-списком изменений (action badge + старый/новый content в diff-виде + reason от LLM/Dream).
  • TypeScript: типы MemoryFact (поля confidence, superseded_by, valid_from, valid_to), MemoryFactHistory. API-метод api.memory.getFactHistory(agentId, factId, limit?).
  • 30+ юнит-тестов:
  • internal/agent/mmr_test.go (8 тестов): SelectsDiverse (lambda=0.3 выбирает разнообразный C над почти-дубликатом B), LambdaOne_PureRelevance, LambdaZero_PureDiversity, FewerCandidatesThanK, EmbedError_Fallback, LambdaClamping, TopKZero, нормализация и dot-product.
  • internal/memory/iter17_test.go (9 тестов): ConflictResolution_KEEP_OLD/UPDATE/CONFLICT/FallbackOnLLMError, Store_FactHistory_RoundTrip, Store_UpdateFactConfidence_BelowThreshold (граница 0.2), Store_SupersedeFact (superseded_by + valid_to + is_active=false атомарно), Dream_Deactivate_LowersConfidence (3 цикла понижения 1.0→0.7→0.4→0.1, последний цикл деактивирует с записью deactivate в истории), TestParseJSONObjectInto (clean / markdown-fence / garbage / no_json / invalid_json).

Changed

  • Consolidator.persistFacts: при создании нового факта через AddFactWithVector сразу пишется запись create в memory_facts_history с new_content и new_confidence. Это даёт consistent timeline в UI.
  • MemoryFact.Confidence участвует в дедупликации Consolidator-а: при Confidence == 0 в AddFactWithVector сохраняется default 1.0 (обратная совместимость со старым кодом).
  • DualStore.copyAllTables/syncSequences/purge — добавлена таблица memory_facts_history в правильном порядке (после memory_facts).
  • buildMemoryContext теперь срезает top-K (5/3) даже при отсутствии reranker’а — после MMR-фильтрации возвращалось до 15/10 элементов, что было больше документированного поведения.

Notes

  • Внешний API совместим: GET /memory/facts/* возвращает старые поля плюс новые confidence, superseded_by, valid_from, valid_to. Старые SQLite-БД получают default confidence=1.0 через миграцию (NOT NULL DEFAULT 1.0).
  • Dream остаётся в admin-режиме (видит все факты включая приватные); user-scope при merge/dedup отложен — см. план итерации 19.