Skip to content

feat: Phases 2-5 + 8a/8b/8c/8d — dashboard, self-learning, consistency, documents#59

Merged
thebtf merged 7 commits into
mainfrom
feat/self-learning-completion
Mar 24, 2026
Merged

feat: Phases 2-5 + 8a/8b/8c/8d — dashboard, self-learning, consistency, documents#59
thebtf merged 7 commits into
mainfrom
feat/self-learning-completion

Conversation

@thebtf
Copy link
Copy Markdown
Owner

@thebtf thebtf commented Mar 24, 2026

Summary

Massive roadmap implementation: 40+ tasks across 6 phases.

Phase 2: Dashboard Frontend

  • Bulk ops (delete/scope/tag) with confirmation dialogs
  • Tag cloud sidebar with clickable filters
  • Per-token usage stats
  • Auth-disabled warning badge
  • Vault encryption setup helper

Phase 3: Self-Learning

  • Injection floor (min 3 observations always injected)
  • Cross-session priming (1.3x boost for recent sessions)
  • Per-project adaptive relevance threshold (project_settings table)
  • Feedback→threshold adjustment loop
  • Dedup threshold raised 0.55→0.7
  • Manual search feedback signal

Phase 4: MCP Tools (already merged in PR #58)

Phase 5: Deployment

  • install.sh --client-only default
  • DEPLOYMENT.md naming fix

Phase 8a: Consistency Engine

  • Orphan vector cleanup, missing vector detection
  • Stale relation cleanup, FalkorDB drift detection
  • Embedding model change detection (system_config table)

Phase 8b: Knowledge Graph

  • Intentional links [[obs:1234]] parser
  • File→observation graph edges
  • GetCluster method in GraphStore

Phase 8c: Causal Linking

  • LLM causal classifier (fixed_by/corrects)
  • Wired into Detect() for bugfix/guidance types

Phase 8d: Document Storage

  • Migration 051: documents + document_comments tables
  • VersionedDocumentStore CRUD (7 methods)

Test plan

  • go build ./... passes
  • Dashboard bulk ops functional (manual walkthrough)
  • Tag cloud shows tags from /api/observations/tag-cloud
  • Injection floor: search with no matches still returns 3 observations
  • Maintenance cycle runs without errors

Summary by CodeRabbit

Release Notes

  • Новые возможности

    • Управление версиями документов с поддержкой комментариев
    • Адаптивные пороги релевантности для проектов
    • Усиление поиска на основе активных сессий
    • Облако тегов с фильтрацией наблюдений
    • Массовые операции: удаление, изменение области видимости, добавление тегов
    • Классификация причинно-следственных связей между наблюдениями
    • Статистика использования токенов
    • Поддержка шифрования хранилища
  • Улучшения

    • Оптимизированные алгоритмы поиска и инъекции контекста
    • Автоматическое обнаружение изменений модели встраивания
  • Документация

    • Обновлены инструкции развёртывания

thebtf added 6 commits March 25, 2026 01:36
…docs fix

- T029: Raise DedupSimilarityThreshold from 0.55 to 0.7 (pre-test confirmed safe)
- T031: Add manual search feedback signal in stop.js — detects engram
  tool usage during session, sends insufficient_injection signal
- T038: install.sh defaults to --client-only (skips engram-server binary)
- T039: Fix cmplus-server naming in DEPLOYMENT.md to engram-server
- T055: Parse [[obs:1234]] syntax in narratives → create bidirectional
  references/referenced_by graph edges
- T056: files_modified/files_read entries → modifies/reads graph edges
  using FNV-1a hash of file path as stable node ID
- Both integrated into existing Detect() pipeline (event-driven async)
- GraphStore interface: GetCluster(nodeID, maxNodes) returns cluster IDs
- FalkorDB: BFS traversal up to 3 hops with LIMIT
- NoopGraphStore: returns empty slice
…44/45)

- New causal_classifier.go: LLM prompt classifies observation pairs as
  fixed_by, corrects, or unrelated
- Wired into Detect() pipeline: triggers for bugfix/guidance types on
  top-3 similarity candidates only (~1 LLM call per 5 observations)
- SetCausalClassifier() method on Detector (opt-in, nil = disabled)
- ShouldClassify() filter: only bugfix and guidance types
Foundation for AI agent collaboration platform:
- documents: versioned, typed (markdown/task/review/decision),
  JSONB metadata (assignee/status/priority), author attribution
- document_comments: inline and general comments with line ranges
- Indexes: project+path+version, doc_type, document_id
… + document store

Phase 2 Frontend (T017-T021):
- Bulk action dropdown (delete/scope/tag) in ObservationsView
- Tag cloud sidebar with clickable filters
- Per-token stats (request count, last used) in TokensView
- Auth-disabled warning badge in AppSidebar
- Vault encryption setup helper in SystemView

Phase 3 Self-Learning (T023-T028):
- Injection floor: always inject at least N observations (default 3)
- Cross-session priming: 1.3x boost for recent sessions
- Adaptive per-project relevance threshold (project_settings table)
- Feedback-driven threshold adjustment (used→lower, ignored→raise)

Phase 8a Consistency Engine (T050-T054):
- Orphan vector cleanup (vectors without observations)
- Missing vector detection (observations without embeddings)
- Stale relation cleanup (broken source/target references)
- FalkorDB↔PostgreSQL drift detection + auto re-sync
- Embedding model change detection via system_config table

Phase 8d Document Store (T061):
- VersionedDocumentStore with Create/ReadLatest/ReadVersion/List/
  GetHistory/AddComment/GetComments GORM methods
- SHA-256 content hashing, version tracking, DISTINCT ON for latest
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 24, 2026

Описание

Обновления включают расширение системы хранения с новыми хранилищами для управления версионированными документами, параметрами проектов и отношениями наблюдений; добавление адаптивного порога релевантности на уровне проекта, повышение оценки на основе сеансов, логики минимального порога инъекции; введение классификации причинно-следственных связей на основе LLM; расширение миграций БД для трёх новых таблиц; обновления обработчиков работника для контекстного поиска и утилит наблюдений; улучшения интерфейса пользователя для наблюдений, токенов и вилки.

Изменения

Когорта / Файлы Резюме
Документация и развёртывание
docs/DEPLOYMENT.md
Обновлены инструкции Docker для использования имени образа/контейнера engram-server вместо cmplus-server.
Конфигурация
internal/config/config.go
Увеличен порог дедупликации сходства с 0,55 до 0,7; добавлены новые поля InjectionFloor (по умолчанию 3) и SessionBoost (по умолчанию 1,3) с поддержкой переменных окружения.
Миграции БД
internal/db/gorm/migrations.go
Добавлены три миграции: 049 для таблицы project_settings, 050 для system_config (хранилище ключ-значение), 051 для версионированных документов с таблицами documents и document_comments.
Вспомогательные функции БД
internal/db/gorm/helpers.go
Экспортированы функции преобразования ToModelObservation и ToModelRelations для доступа из других пакетов.
Хранилище параметров проекта
internal/db/gorm/project_settings_store.go
Новое хранилище GORM для управления параметрами проекта: получение/корректировка адаптивного порога релевантности с фиксацией в диапазон [0,1, 0,8] и инкрементированием счётчика обратной связи.
Хранилище наблюдений
internal/db/gorm/observation_store.go
Добавлены методы GetRecentSessionIDs для получения активных сеансов и GetTopImportanceObservations для выборки наблюдений по важности.
Хранилище версионированных документов
internal/db/gorm/versioned_document_store.go
Новое хранилище для управления версионированными документами с методами создания, чтения (последняя версия/конкретная версия/история), перечисления и управления комментариями к документам.
Граф отношений
internal/graph/store.go, internal/graph/falkordb/client.go, internal/graph/noop.go
Добавлен новый метод интерфейса GetCluster для получения наблюдений в одном кластере с использованием BFS-обхода на глубину до 3 шагов; реализовано в FalkorDB и Noop хранилищах.
Обслуживание
internal/maintenance/service.go
Расширены задачи обслуживания (10–14): удаление невостребованных векторов, переиндексация наблюдений без встроений, удаление устаревших отношений, обнаружение дрейфа графика, отслеживание изменений модели встроений; добавлены новые зависимости и метод IsEmbeddingModelChanged().
Классификация причинно-следственных связей
internal/relation/causal_classifier.go
Новая система для классификации отношений между наблюдениями на основе LLM с поддержкой меток fixed_by, corrects, unrelated; применяется только к типам ObsTypeBugfix и ObsTypeGuidance.
Обнаружение отношений
internal/relation/detector.go
Расширено для поддержки парсинга ссылок в нарративе (формат [[obs:<id>]]), создания рёбер на основе файлов и интеграции LLM-классификации причинно-следственных связей для топ-3 кандидатов.
Поиск и менеджер
internal/search/manager.go
Добавлена функция ApplySessionBoost для повышения оценок на основе недавних сеансов; новые методы для установки и получения адаптивного порога релевантности на уровне проекта.
Обработчики работника (контекст и оценка)
internal/worker/handlers_context.go, internal/worker/handlers_scoring.go
Введены адаптивный порог поиска на уровне проекта, повышение на основе сеансов и логика минимального порога инъекции в контекстном поиске; добавлена корректировка порога в обработчике утилит наблюдений.
Сервис работника
internal/worker/service.go
Добавлено новое поле projectSettingsStore; расширена инициализация обслуживания новыми зависимостями; интеграция параметров проекта с поиском.
Скрипты установки
scripts/install.sh
Добавлена поддержка режимов установки (client-only по умолчанию, --full для полной установки); сервер устанавливается только в режиме full.
Интерфейс (боковая панель)
ui/src/components/layout/AppSidebar.vue
Добавлена проверка отключённой аутентификации при монтировании компонента; условное скрытие раздела "токены" и отображение предупреждающего значка.
Интерфейс (наблюдения)
ui/src/views/ObservationsView.vue
Расширена поддержка массовых действий (архивирование, удаление, изменение области, добавление тегов); добавлено облако тегов с фильтрацией и выбор нескольких действий через встроенный ввод.
Интерфейс (система)
ui/src/views/SystemView.vue
Добавлена поддержка статуса хранилища ключей; новый раздел с инструкциями и кнопкой для копирования команды настройки в буфер обмена.
Интерфейс (токены)
ui/src/views/TokensView.vue
Введена ленивая загрузка статистики токенов (количество запросов, время последнего использования) с кнопкой "Загрузить статистику" для каждого токена.
Плагин (обработчик остановки)
plugin/engram/hooks/stop.js
Добавлено обнаружение недостаточной инъекции по вызовам функций поиска engram и отправка сигнала обратной связи в конце обработки остановки.

Диаграммы последовательности

sequenceDiagram
    participant Client
    participant SearchMgr
    participant ProjectSettings
    participant ObservationStore
    participant Database
    
    Client->>SearchMgr: HandleSearchByPrompt(project)
    SearchMgr->>ProjectSettings: GetProjectThreshold(project)
    ProjectSettings->>Database: Query threshold
    Database-->>ProjectSettings: threshold value
    ProjectSettings-->>SearchMgr: adaptive threshold
    
    SearchMgr->>ObservationStore: GetRecentSessionIDs(project, 2h ago)
    ObservationStore->>Database: Query active sessions
    Database-->>ObservationStore: session IDs
    ObservationStore-->>SearchMgr: map[string]bool
    
    SearchMgr->>SearchMgr: ApplySessionBoost(scores, sessionIDs)
    SearchMgr->>ObservationStore: GetTopImportanceObservations(project)
    ObservationStore->>Database: Query by importance
    Database-->>ObservationStore: observations
    ObservationStore-->>SearchMgr: []*Observation
    
    SearchMgr-->>Client: results with adaptive threshold & session boost
Loading
sequenceDiagram
    participant RelationDetector
    participant CausalClassifier
    participant LLMClient
    participant RelationStore
    participant Database
    
    RelationDetector->>RelationDetector: ParseObservationReferences(narrative)
    RelationDetector->>RelationStore: StoreRelation(references)
    RelationStore->>Database: Insert edge
    
    RelationDetector->>RelationDetector: ParseFileEdges(files_modified)
    RelationDetector->>RelationStore: StoreRelation(modifies)
    RelationStore->>Database: Insert edge
    
    alt Should Classify (BugFix or Guidance)
        RelationDetector->>CausalClassifier: ClassifyPair(obsA, obsB)
        CausalClassifier->>LLMClient: Complete(prompt)
        LLMClient-->>CausalClassifier: response text
        CausalClassifier->>CausalClassifier: ParseLabel(response)
        CausalClassifier-->>RelationDetector: label (fixed_by/corrects/unrelated)
        RelationDetector->>RelationStore: StoreRelation(causal type)
        RelationStore->>Database: Insert causal edge
    end
Loading

Оценка сложности рецензирования

🎯 4 (Сложно) | ⏱️ ~60 минут

Возможно связанные PR

  • PR #42: Модификация одного и того же файла миграций БД и расширение методов observation_store.go в обоих PR.
  • PR #22: Изменение подсистемы обнаружения отношений с добавлением поддержки классификации и интеграции в хранилище GORM.
  • PR #40: Модификация логики поиска и менеджера с вводом повышения оценок на основе сеансов и адаптивных порогов.

Рекомендуемые метки

ai-review:passed

Стихотворение

🐰 Документы теперь версийны и живут в почёте,
Проекты знают пороги — каждый в отчёте,
Сеансы поют в буста́х, файлы рождают рёбра,
Причины и следствия LLM раскрывает без отвёра,
От ядра к интерфейсу—адаптивность везде,
Engram растёт с размахом в цифровой беде! 🎉

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main objective by referencing the specific roadmap phases (2–5, 8a/8b/8c/8d) and key feature areas implemented in this changeset.
Docstring Coverage ✅ Passed Docstring coverage is 87.50% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/self-learning-completion

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 golangci-lint (2.11.3)

Error: can't load config: unsupported version of the configuration: "" See https://golangci-lint.run/docs/product/migration-guide for migration instructions
The command is terminated due to an error: can't load config: unsupported version of the configuration: "" See https://golangci-lint.run/docs/product/migration-guide for migration instructions


Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist
Copy link
Copy Markdown

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request delivers a substantial set of features across multiple phases of the roadmap, significantly enhancing the system's intelligence, maintainability, and user interface. It introduces advanced self-learning mechanisms, improves data consistency through new maintenance routines, expands the knowledge graph capabilities, and lays the foundation for versioned document storage and AI agent collaboration. The dashboard also receives usability upgrades with new bulk actions and filtering options.

Highlights

  • Dashboard Frontend Enhancements: Implemented bulk operations (delete, scope, tag) for observations, added a clickable tag cloud sidebar for filtering, and introduced per-token usage statistics. A warning badge for disabled authentication was also added.
  • Self-Learning Improvements: Introduced an injection floor to ensure a minimum number of observations are always injected, added cross-session priming to boost recent session observations, and implemented per-project adaptive relevance thresholds with a feedback adjustment loop. The deduplication similarity threshold was raised from 0.55 to 0.7.
  • Deployment Updates: Modified the install.sh script to support --client-only and --full installation modes, and updated Docker image names in DEPLOYMENT.md to engram-server.
  • Consistency Engine (Phase 8a): Added maintenance tasks for cleaning orphan vectors, detecting missing vectors, cleaning stale relations, detecting FalkorDB graph drift, and checking for embedding model changes.
  • Knowledge Graph (Phase 8b): Implemented parsing for [[obs:1234]] intentional links in narratives and created file-to-observation graph edges for files_modified and files_read. A GetCluster method was added to the graph store.
  • Causal Linking (Phase 8c): Introduced an LLM-based causal classifier to identify fixed_by and corrects relationships between bugfix/guidance observations.
  • Document Storage (Phase 8d): Added new database migrations for documents and document_comments tables, along with a VersionedDocumentStore for CRUD operations on versioned documents.
  • Vault Encryption Setup Helper: Added a UI helper in the System View to guide users through setting up vault encryption.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@thebtf
Copy link
Copy Markdown
Owner Author

thebtf commented Mar 24, 2026

@coderabbitai review

@thebtf
Copy link
Copy Markdown
Owner Author

thebtf commented Mar 24, 2026

@gemini-code-assist review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 24, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@thebtf
Copy link
Copy Markdown
Owner Author

thebtf commented Mar 24, 2026

@codex review

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces significant enhancements across the system, focusing on improved data management, relation detection, and search relevance. Key changes include new database migrations for project-specific settings, system configuration, and versioned document storage with comments. The maintenance service gains new tasks for cleaning orphan vectors, detecting missing vectors, cleaning stale relations, detecting graph drift, and checking for embedding model changes. Relation detection is expanded to include intentional links in narratives and file-based graph edges, alongside a new LLM-based causal classifier for observation pairs. Search functionality is enhanced with session boosting, adaptive per-project relevance thresholds, and an injection floor to ensure a minimum number of results. The UI is updated to support new batch actions for observations, display vault encryption status, and show token usage statistics. Review comments highlight several areas for improvement, including handling database errors in GetThreshold to avoid masking issues, refactoring GetCluster to use parameterized queries to prevent SQL injection, ensuring error propagation in migration rollbacks, standardizing CreatedAt fields to time.Time in document stores, and consistently generating negative file node IDs in the relation detector to prevent collisions.

Comment thread internal/db/gorm/project_settings_store.go Outdated
Comment thread internal/graph/falkordb/client.go
Comment thread internal/db/gorm/migrations.go
Comment thread internal/db/gorm/versioned_document_store.go Outdated
Comment thread internal/db/gorm/versioned_document_store.go Outdated
Comment thread internal/db/gorm/versioned_document_store.go Outdated
Comment thread internal/db/gorm/versioned_document_store.go Outdated
Comment thread internal/db/gorm/versioned_document_store.go Outdated
Comment thread internal/relation/detector.go Outdated
Comment thread internal/relation/detector.go Outdated
Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces several major features and improvements, including versioned document storage with comments, adaptive per-project relevance thresholds, and enhanced observation relationship detection using LLMs, intentional links, and file-based graph edges. The maintenance service now includes tasks for cleaning orphan vectors, detecting missing embeddings, cleaning stale relations, and monitoring graph drift and embedding model changes. Search functionality is improved with session boosting and an injection floor to ensure a minimum number of relevant results. The UI has been updated to support new batch actions for observations, a tag cloud for filtering, a vault encryption setup guide, and token usage statistics. A critical race condition was identified in the VersionedDocumentStore.Create method, where concurrent calls could lead to unique constraint violations due to non-atomic version number generation. Additionally, error handling in the 051_documents migration's Rollback function needs to be improved to ensure database consistency.

Comment thread internal/db/gorm/versioned_document_store.go Outdated
Comment thread internal/db/gorm/migrations.go
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

🟠 Major comments (18)
internal/relation/causal_classifier.go-13-21 (1)

13-21: ⚠️ Potential issue | 🟠 Major

Classifier выдаёт label'ы вне канонического RelationType.

Промпт на Lines 16-21 и parser на Lines 55-66 разрешают только fixed_by/corrects, но в pkg/models/relation.go из предоставленного контекста нет таких констант: канонический тип там fixes, а corrects не объявлен вовсе. Дальше internal/relation/detector.go просто приводит эту строку к models.RelationType, так что в граф уйдут неподдерживаемые значения. Здесь нужно либо перейти на vocabulary из модели, либо сначала расширить модель и всех потребителей новых relation type.

Also applies to: 54-67

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/relation/causal_classifier.go` around lines 13 - 21, The classifier
currently emits labels ("fixed_by", "corrects") that don't match the canonical
models.RelationType (which uses "fixes"), causing invalid relation values
downstream; update either the causalClassifierSystemPrompt labels to the exact
vocabulary in models.RelationType (e.g., "fixes" and "unrelated") or add a
deterministic mapping in the parser (the parsing code at ~Lines 55-66 in
causal_classifier.go) that translates "fixed_by" -> models.RelationType("fixes")
and "corrects" -> the appropriate canonical RelationType, and ensure
internal/relation/detector.go consumes only models.RelationType values (or
extend models.RelationType and all its consumers if you choose to add new
canonical values).
internal/relation/detector.go-257-286 (1)

257-286: ⚠️ Potential issue | 🟠 Major

Не создавайте references-связи без проверки project.

На Line 264 ID из narrative сразу превращается в relation. Если [[obs:1234]] укажет на observation из другого project, код на Lines 269-285 создаст межпроектные references/referenced_by-рёбра и сломает изоляцию графа. Перед StoreRelation здесь нужно убедиться, что refID существует и принадлежит тому же проекту.

Предлагаемый фикс
 			refID, parseErr := strconv.ParseInt(match[1], 10, 64)
 			if parseErr != nil || refID == obsID {
 				continue
 			}
+			refObs, loadErr := d.observationStore.GetObservationByID(ctx, refID)
+			if loadErr != nil || refObs == nil || refObs.Project != obs.Project {
+				continue
+			}
 			// Create bidirectional edges: current → referenced, referenced → current
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/relation/detector.go` around lines 257 - 286, The code is creating
references/referenced_by edges from parsed intentional links without verifying
the referenced observation's project; before calling
d.relationStore.StoreRelation for refRel/backRel, load the referenced
observation (e.g. via the existing observation store method like
GetByID/GetObservation) and verify it exists and its ProjectID matches the
current observation's project; only then create both models.ObservationRelation
entries (keep the checks around refID/obsID and the existing logging) and skip
storing relations if the referenced obs is missing or belongs to a different
project.
internal/relation/detector.go-290-317 (1)

290-317: ⚠️ Potential issue | 🟠 Major

fileNodeID нужно отделить от observation ID и привязать к project.

Line 296 обещает отрицательный pseudo-ID, но на Lines 297 и 312 сохраняется положительный hashString(filePath). В результате одинаковые пути вроде README.md схлопнутся между разными проектами, а сами file-ноды ещё и делят пространство ID с observation. Хэш лучше строить хотя бы от project + path и сохранять в отдельном диапазоне, например отрицательном.

Предлагаемый фикс
-		// Use file path hash as a pseudo node ID (negative to avoid collision with observation IDs)
-		fileNodeID := int64(hashString(filePath))
+		// Namespace file nodes by project and keep them negative to avoid colliding with observation IDs.
+		fileNodeID := -hashString(obs.Project + "\x00" + filePath)
 		rel := &models.ObservationRelation{
 			SourceID:     obsID,
 			TargetID:     fileNodeID,
 			RelationType: "modifies",
@@
-		fileNodeID := int64(hashString(filePath))
+		fileNodeID := -hashString(obs.Project + "\x00" + filePath)
 		rel := &models.ObservationRelation{
 			SourceID:     obsID,
 			TargetID:     fileNodeID,
 			RelationType: "reads",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/relation/detector.go` around lines 290 - 317, The file-node IDs are
colliding with observation IDs and across projects because fileNodeID is
currently set to int64(hashString(filePath)); update the ID derivation to
include the project identifier and map file nodes into a separate ID range
(e.g., negate the hash or prefix with project) so they cannot collide with
observation IDs: change usages around fileNodeID (in the obs.FilesModified and
obs.FilesRead loops where models.ObservationRelation is created and
relationStore.StoreRelation is called) to compute the hash from project+filePath
(via the existing hashString function) and then convert it into the distinct
negative/namespace range before assigning to TargetID; ensure any comments about
pseudo node ID reflect this new scheme and keep Confidence/RelationType logic
the same.
ui/src/views/ObservationsView.vue-100-105 (1)

100-105: ⚠️ Potential issue | 🟠 Major

Не просите scope свободным текстом.

Для PATCH /api/observations/bulk-scope сервер принимает только global или project (internal/worker/handlers_data.go:52-100), а эта форма предлагает ввести любую строку. В результате bulk change scope почти всегда упрётся в 400, если пользователь не знает точные литералы.

💡 Возможный фикс
-const batchScopeInput = ref('')
+const batchScopeInput = ref<'global' | 'project'>('project')
-<input
-  v-model="batchScopeInput"
-  type="text"
-  placeholder="Enter new scope..."
-  `@keydown.enter`="executeBatchAction"
-/>
+<select v-model="batchScopeInput">
+  <option value="project">project</option>
+  <option value="global">global</option>
+</select>

Also applies to: 369-389

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/src/views/ObservationsView.vue` around lines 100 - 105, The form currently
sends arbitrary text from batchScopeInput to the bulk-scope endpoint (see
batchAction.value === 'scope' branch and fetch('/api/observations/bulk-scope')),
but the server only accepts "global" or "project"; change the UI to
restrict/validate the value: replace the free-text input bound to
batchScopeInput with a select (or radio) offering only "global" and "project",
and add a client-side check before the fetch to ensure batchScopeInput is one of
those two literals (otherwise show an error and avoid calling the endpoint);
update any other places using batchScopeInput (e.g., the other scope-handling
code around lines 369-389) to the same constrained pattern.
ui/src/views/ObservationsView.vue-136-143 (1)

136-143: ⚠️ Potential issue | 🟠 Major

Фильтр Top Tags сейчас ничего не фильтрует.

filterByTag() меняет currentTagFilter и делает fetchPage(), но выбранный тег дальше нигде не используется. Ни запрос списка, ни filteredObservations не зависят от currentTagFilter, поэтому клик меняет только активное состояние пилюли.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/src/views/ObservationsView.vue` around lines 136 - 143, filterByTag
toggles currentTagFilter and calls fetchPage(), but neither the network request
nor the local computed filteredObservations use currentTagFilter; update the
logic so the selected tag actually filters results: modify fetchPage (or the API
params it calls) to include currentTagFilter.value when building the
query/params so the backend returns tag-filtered items, and/or change the
computed filteredObservations to apply a filter by currentTagFilter.value (e.g.,
compare observation.tags.includes(currentTagFilter.value)) so the UI reflects
the chosen tag; ensure the symbols mentioned (filterByTag, currentTagFilter,
fetchPage, filteredObservations) are updated accordingly and that offset.value
reset behavior remains.
ui/src/views/ObservationsView.vue-564-576 (1)

564-576: ⚠️ Potential issue | 🟠 Major

Замените span на семантический элемент button.

Элемент <span> с обработчиком клика не доступен с клавиатуры: он не фокусируется по Tab и не реагирует на Enter/Space. Для интерактивного управления требуется семантический элемент <button type="button"> с встроенной клавиатурной активацией и правильной семантикой.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/src/views/ObservationsView.vue` around lines 564 - 576, Replace the
non-semantic <span> interactive items with a proper button to restore keyboard
accessibility: in the v-for that iterates over tagCloud (keyed by item.tag)
change the element to a <button type="button"> and keep the :key, :class binding
and `@click`="filterByTag(item.tag)"; also ensure currentTagFilter logic is
preserved for styling and add an accessible state attribute (e.g.,
:aria-pressed="currentTagFilter === item.tag") so assistive tech and keyboard
users get proper semantics and state from the tag component.
ui/src/views/ObservationsView.vue-95-112 (1)

95-112: ⚠️ Potential issue | 🟠 Major

Добавьте проверку response.ok для batch-запросов.

fetch() не отклоняет Promise при ошибках 4xx/5xx — это приводит к тому, что операции delete/scope/tag могут завершиться ошибкой валидации или на сервере, но UI все равно пойдёт по успешной ветке: очистит выделение и перезагрузит список. Это критично для деструктивных операций.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/src/views/ObservationsView.vue` around lines 95 - 112, The batch action
handlers (the branches using batchAction.value that call fetch for
'/api/observations/bulk', '/api/observations/bulk-scope', and
'/api/observations/batch-tag') must check the fetch Response.ok and handle
non-OK responses before proceeding to clear selection or reload the list; for
each await fetch(...) call, inspect the returned response, if !response.ok read
and surface the error (e.g., throw or return the parsed JSON/error message) so
the caller can stop the success flow and show an error, and only clear
batchScopeInput.value / batchTagInput.value and reload when response.ok is true.
Ensure the check covers all three endpoints and use the same error-handling
pattern so validation/4xx/5xx results do not falsely trigger the success branch.
internal/worker/handlers_scoring.go-212-233 (1)

212-233: ⚠️ Potential issue | 🟠 Major

Порог адаптации привязывается не к тому проекту.

У этого endpoint в запросе нет project, поэтому на Line 226 вы берёте obs.Project. Для scope="global" или переиспользуемых observation это обновит чужой project_settings либо вообще ничего не обновит при пустом Project, хотя feedback пришёл из конкретной сессии текущего проекта. Нужен project/session_id в payload или lookup через session injection.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/worker/handlers_scoring.go` around lines 212 - 233, The
adaptive-threshold adjustment is using obs.Project (from observation) which can
be empty or belong to a different project; modify the handler to determine the
project from the incoming request/session first and only fall back to
observation.Project if needed. Concretely: add or read a project identifier from
the request payload (e.g., req.Project or req.SessionID) or resolve the session
from the request context (via session lookup) to obtain session.Project, then
call projectSettingsStore.AdjustThreshold(r.Context(), project, delta) using
that resolved project; keep the current observationStore.GetObservationByID(id)
only for fallback or validation, and ensure you skip AdjustThreshold when the
resolved project is empty to avoid touching other projects' settings.
plugin/engram/hooks/stop.js-356-390 (1)

356-390: ⚠️ Potential issue | 🟠 Major

Поиск manual search сейчас не видит structured tool calls.

Здесь проверяется только m.text, но parseTranscript() выше наполняет его через extractTextContent(), который сохраняет лишь part.type === 'text'. Если engram__search/engram__find_by_* приходят как tool_use/function-call блоки, insufficient_injection не отправится.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plugin/engram/hooks/stop.js` around lines 356 - 390, Manual-search detection
only checks assistant message text (m.text) but
parseTranscript()/extractTextContent() drops non-text parts, so
tool_use/function-call entries like engram__search or engram__find_by_* are
missed; update the detection inside the stop hook to also inspect assistant
message metadata for tool/function calls (e.g., check
message.function_call.name, message.tool_call?.name, message.type or
message.parts for tool entries) when computing
assistantFullText/manualSearchDetected (the messages array and variables
sessionID, lib.requestPost remain the same) and fall back to the existing text
check so tool-based calls trigger the insufficient_injection request.
internal/worker/service.go-972-975 (1)

972-975: ⚠️ Potential issue | 🟠 Major

Новый ProjectSettingsStore не переживает reinitializeDatabase().

Он создаётся только здесь. В path повторной инициализации БД ниже новый instance не создаётся и не пробрасывается обратно в searchMgr, поэтому после recreation базы adaptive thresholds будут читать/писать через устаревший DB handle.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/worker/service.go` around lines 972 - 975, ProjectSettingsStore is
only constructed once via gorm.NewProjectSettingsStore and assigned to
s.projectSettingsStore and searchMgr here, but reinitializeDatabase() recreates
the DB without recreating or re-registering that store; update
reinitializeDatabase() to create a fresh ProjectSettingsStore (call
gorm.NewProjectSettingsStore with the new DB handle), assign it to
s.projectSettingsStore, and call searchMgr.SetProjectSettingsStore(newStore) so
searchMgr and the service use the new DB handle after DB recreation.
internal/maintenance/service.go-645-678 (1)

645-678: ⚠️ Potential issue | 🟠 Major

Метрика drift сравнивает несопоставимые сущности.

Stats().NodeCount считает все graph nodes, а SQL ниже считает все активные observations. После добавления file-нод и при наличии observations без relations эти числа по определению расходятся, поэтому maintenance будет регулярно триггерить ложный full re-sync.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/maintenance/service.go` around lines 645 - 678, The drift metric is
comparing all graph nodes (stats.NodeCount from s.graphStore.Stats) to all
active observations (the obsCount SQL on table "observations"), which is invalid
when file-nodes or observations without relations exist; update the obsCount
query to only count observations that are actually linked into the graph (e.g.,
join or exists against the relations/edges table or whatever relation table
holds observation→node links) so obsCount reflects only observations that should
map to graph nodes, or alternatively compute graphCount to exclude file-type
nodes from stats.NodeCount; adjust the code around s.graphStore.Stats,
stats.NodeCount and the observations Count(...) query so both sides measure the
same entity set before computing drift.
internal/search/manager.go-351-355 (1)

351-355: ⚠️ Potential issue | 🟠 Major

0.3 нельзя использовать как sentinel для “unset”.

Проект, у которого threshold реально опустился до 0.3, здесь считается “не настроенным” и заменяется globalDefault. Если глобальный порог поднят выше 0.3, feedback-driven настройка для такого проекта больше никогда не сможет закрепиться на 0.3.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/search/manager.go` around lines 351 - 355, The code wrongly treats
the literal 0.3 as a sentinel for “unset” when deciding between threshold and
globalDefault; change the stored threshold to be explicitly nullable or
accompanied by an explicit flag instead of comparing to 0.3 (e.g., make
threshold a *float64 or add a ThresholdSet bool), update the logic where
threshold is read (the variable named threshold and the decision that currently
returns globalDefault) to check for nil/ThresholdSet rather than value equality,
and return globalDefault only when the stored value is truly absent; ensure any
callers/serializers are updated to handle the nullable/flagged representation.
internal/graph/falkordb/client.go-368-375 (1)

368-375: ⚠️ Potential issue | 🟠 Major

Сейчас это не GetCluster, а только 3-hop neighborhood.

[:REL*1..3] режет связанную компоненту на глубине 3, а LIMIT без сортировки по длине пути возвращает произвольное подмножество. Для длинных цепочек метод будет отдавать неполный и нестабильный “кластер”.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/graph/falkordb/client.go` around lines 368 - 375, The current query
in GetCluster (the query string building using "MATCH (a:Observation {id:
%d})-[:REL*1..3]-(b:Observation) ... LIMIT %d") only returns a 3‑hop
neighborhood and with LIMIT returns an arbitrary subset; change it to compute
the full connected component instead and make results stable: replace the fixed
depth pattern "[:REL*1..3]" with an unbounded variable-length pattern (e.g.
"[:REL*]" or use a shortest-path pattern like "MATCH
p=shortestPath((a)-[:REL*]-(b)) RETURN b.id, length(p) ORDER BY length(p) ASC"
if you must keep a LIMIT), and remove or pair LIMIT with an ORDER BY length(p)
so returned nodes are deterministic; update the query construction in the
GetCluster-related code that builds the query string to use the new pattern and
ordering.
internal/maintenance/service.go-618-629 (1)

618-629: ⚠️ Potential issue | 🟠 Major

SyncFromRelations не распространяет удаления в граф.

После удаления relation в PostgreSQL вызов graphStore.SyncFromRelations() не почистит FalkorDB: текущая реализация только MERGE-ит существующие рёбра. То же самое касается нижнего drift-recovery path, который вызывает этот же sync, поэтому stale edges в графе останутся.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/maintenance/service.go` around lines 618 - 629, The current call to
s.graphStore.SyncFromRelations(ctx, modelRelations) only MERGEs edges and thus
doesn’t remove stale FalkorDB edges; update the sync call to perform a full
reconciliation that also deletes edges missing from modelRelations — either by
calling an existing full-sync API on graphStore (e.g., SyncFromRelations(ctx,
modelRelations, FullSync/withDeletions option) or a dedicated method like
ReplaceRelations/SyncFromRelationsWithDeletions), or implement a two-step
prune-then-merge on s.graphStore (delete edges not in modelRelations then upsert
the modelRelations); apply the same change to the lower drift-recovery path that
currently uses gorm.ToModelRelations(...) and s.graphStore.SyncFromRelations so
both paths propagate deletions to FalkorDB and log failures via s.log.
internal/worker/handlers_context.go-1128-1141 (1)

1128-1141: ⚠️ Potential issue | 🟠 Major

Здесь считается не distinct-count, а сумма длин срезов.

totalInjected сейчас завышается, если один и тот же observation попал сразу в несколько секций. Тогда floor может не добрать записи, хотя уникальных observation ID всё ещё меньше минимума.

🛠 Вариант правки
-	totalInjected := len(recentFresh) + len(relevantObservations) + len(guidanceObservations) + len(alwaysInjectObservations)
+	injectedIDs := make(map[int64]struct{}, len(recentFresh)+len(relevantObservations)+len(guidanceObservations)+len(alwaysInjectObservations))
+	for _, section := range [][]*models.Observation{recentFresh, relevantObservations, guidanceObservations, alwaysInjectObservations} {
+		for _, obs := range section {
+			injectedIDs[obs.ID] = struct{}{}
+		}
+	}
+	totalInjected := len(injectedIDs)
 	if totalInjected < injectionFloor && s.observationStore != nil {
 		needed := injectionFloor - totalInjected
 		fillObs, fillErr := s.observationStore.GetTopImportanceObservations(ctx, project, needed+totalInjected)
 		if fillErr == nil {
 			for _, obs := range fillObs {
-				if _, already := recentIDs[obs.ID]; !already {
+				if _, already := injectedIDs[obs.ID]; !already {
 					recentFresh = append(recentFresh, obs)
+					injectedIDs[obs.ID] = struct{}{}
 					recentIDs[obs.ID] = struct{}{}
 					needed--
 					if needed == 0 {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/worker/handlers_context.go` around lines 1128 - 1141, The current
totalInjected calculation uses sum of slice lengths (totalInjected :=
len(recentFresh) + len(relevantObservations) + len(guidanceObservations) +
len(alwaysInjectObservations)) which overcounts when the same observation
appears in multiple slices; change the logic to count unique observation IDs
instead (compute a set/map of IDs across recentFresh, relevantObservations,
guidanceObservations, alwaysInjectObservations, then use its size as
totalInjected) before deciding to call
s.observationStore.GetTopImportanceObservations; ensure later checks that add to
recentFresh also consult and update the same recentIDs map so uniqueness is
preserved (referencing totalInjected, recentFresh, relevantObservations,
guidanceObservations, alwaysInjectObservations, recentIDs, and
observationStore.GetTopImportanceObservations).
internal/db/gorm/project_settings_store.go-57-63 (1)

57-63: ⚠️ Potential issue | 🟠 Major

Первый feedback для нового проекта сейчас теряется.

На insert-пути UPSERT пишет фиксированные 0.3 и 0, поэтому первый вызов AdjustThreshold() не применяет delta и не увеличивает feedback_count. Адаптивный threshold начинает реально меняться только со второго feedback-события.

🛠 Вариант правки
 	sql := `INSERT INTO project_settings (project, relevance_threshold, feedback_count, updated_at)
-	        VALUES (?, 0.3, 0, NOW())
+	        VALUES (?, GREATEST(0.1, LEAST(0.8, 0.3 + ?)), 1, NOW())
 	        ON CONFLICT (project) DO UPDATE
 	        SET relevance_threshold = GREATEST(0.1, LEAST(0.8, project_settings.relevance_threshold + ?)),
 	            feedback_count      = project_settings.feedback_count + 1,
 	            updated_at          = NOW()`
-	return s.db.WithContext(ctx).Exec(sql, project, delta).Error
+	return s.db.WithContext(ctx).Exec(sql, project, delta, delta).Error
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/db/gorm/project_settings_store.go` around lines 57 - 63, The INSERT
uses fixed initial values (0.3 and 0) so the very first AdjustThreshold call
ignores the delta and doesn't increment feedback_count; change the UPSERT so the
INSERT path applies the delta and sets feedback_count to 1 (e.g. VALUES (?, 0.3
+ ?, 1, NOW()) or use EXCLUDED.relevance_threshold) and keep the DO UPDATE logic
to add delta and increment feedback_count; also update the Exec parameter list
for s.db.WithContext(ctx).Exec(sql, ...) (or adjust the SQL to reference
EXCLUDED to avoid extra params) so the first call actually applies the delta and
increases feedback_count.
internal/db/gorm/versioned_document_store.go-83-106 (1)

83-106: ⚠️ Potential issue | 🟠 Major

MAX(version)+1 небезопасен при параллельных Create.

Два одновременных запроса для одного (path, project) могут прочитать один и тот же maxVersion и попытаться вставить одинаковый version. Под нагрузкой это даст спорадические ошибки вставки и потерю одного из обновлений.

🛠 Вариант правки
-	var maxVersion int
-	if err := s.db.WithContext(ctx).
-		Model(&VersionedDocument{}).
-		Where("path = ? AND project = ?", path, project).
-		Select("COALESCE(MAX(version), 0)").
-		Scan(&maxVersion).Error; err != nil {
-		return 0, fmt.Errorf("versioned_document_store: query max version: %w", err)
-	}
-
-	doc := VersionedDocument{
-		Path:        path,
-		Project:     project,
-		Version:     maxVersion + 1,
-		Content:     content,
-		ContentHash: contentHash,
-		DocType:     docType,
-		Metadata:    metadata,
-		Author:      author,
-		CreatedAt:   now,
-	}
-
-	if err := s.db.WithContext(ctx).Create(&doc).Error; err != nil {
-		return 0, fmt.Errorf("versioned_document_store: insert document: %w", err)
-	}
+	var doc VersionedDocument
+	if err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
+		if err := tx.Exec(
+			`SELECT pg_advisory_xact_lock(hashtext(?), hashtext(?))`,
+			project, path,
+		).Error; err != nil {
+			return fmt.Errorf("versioned_document_store: lock document stream: %w", err)
+		}
+
+		var maxVersion int
+		if err := tx.Model(&VersionedDocument{}).
+			Where("path = ? AND project = ?", path, project).
+			Select("COALESCE(MAX(version), 0)").
+			Scan(&maxVersion).Error; err != nil {
+			return fmt.Errorf("versioned_document_store: query max version: %w", err)
+		}
+
+		doc = VersionedDocument{
+			Path:        path,
+			Project:     project,
+			Version:     maxVersion + 1,
+			Content:     content,
+			ContentHash: contentHash,
+			DocType:     docType,
+			Metadata:    metadata,
+			Author:      author,
+			CreatedAt:   now,
+		}
+		if err := tx.Create(&doc).Error; err != nil {
+			return fmt.Errorf("versioned_document_store: insert document: %w", err)
+		}
+		return nil
+	}); err != nil {
+		return 0, err
+	}
 	return doc.ID, nil
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/db/gorm/versioned_document_store.go` around lines 83 - 106, Race on
computing MAX(version)+1 can yield duplicate versions under concurrent Create;
wrap the read-and-insert in a DB transaction and lock the relevant rows before
reading the max version. Specifically, in the method that builds
VersionedDocument and inserts it, use s.db.WithContext(ctx).Transaction(...) and
inside use the transaction (tx) to SELECT COALESCE(MAX(version),0) FOR UPDATE
(via GORM locking clause) for the given path and project into maxVersion, then
set Version = maxVersion+1 and insert via tx.Create(&doc); return or retry on
unique-constraint conflicts if needed. Ensure you reference VersionedDocument,
the maxVersion read, and the Create call so the atomic read+write is done inside
the transaction.
internal/worker/handlers_context.go-356-379 (1)

356-379: ⚠️ Potential issue | 🟠 Major

Сохраните obs_type при доборе через injection floor.

Если запрос пришёл с obs_type, этот блок добирает GetTopImportanceObservations(...) без повторной фильтрации и может вернуть наблюдения другого типа. В результате /api/context/search иногда нарушает собственный контракт фильтрации.

🛠 Вариант правки
 		fillObs, fillErr := s.observationStore.GetTopImportanceObservations(r.Context(), project, needed+len(clusteredObservations))
 		if fillErr == nil {
 			for _, obs := range fillObs {
+				if obsTypeFilter != "" && string(obs.Type) != obsTypeFilter {
+					continue
+				}
 				if _, already := includedIDs[obs.ID]; !already {
 					clusteredObservations = append(clusteredObservations, obs)
 					includedIDs[obs.ID] = struct{}{}
 					needed--
 					if needed == 0 {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/worker/handlers_context.go` around lines 356 - 379, The
injection-floor filler uses s.observationStore.GetTopImportanceObservations to
top-up clusteredObservations but doesn't respect the incoming obs_type filter;
update the filler (around variables injectionFloor, clusteredObservations and
the call to s.observationStore.GetTopImportanceObservations) to preserve the
requested obs_type by either passing the obs_type into the
GetTopImportanceObservations call if it supports a type/filter argument, or by
post-filtering fillObs to only append obs where obs.Type == requestedObsType (or
equivalent field) before deduplication and appending; ensure needed/de-dup logic
and includedIDs handling remain correct after applying this filter.
🟡 Minor comments (6)
ui/src/views/TokensView.vue-20-36 (1)

20-36: ⚠️ Potential issue | 🟡 Minor

Отсутствует credentials: 'include' в fetch-запросе.

Эндпоинт /api/auth/tokens/{id}/stats находится в защищённой секции маршрутов (см. handlers_auth.go). Без credentials: 'include' запрос может не пройти аутентификацию через cookie-сессию.

Также: при ошибке запроса tokenStats[tokenId] остаётся undefined, но повторная попытка заблокирована проверкой на строке 21. Пользователь не сможет повторить загрузку статистики.

🐛 Предложение: добавить credentials и обработку ошибок
 async function loadTokenStats(tokenId: string) {
-  if (tokenStats.value[tokenId] !== undefined || statsLoading.value[tokenId]) return
+  if (statsLoading.value[tokenId]) return
   statsLoading.value = { ...statsLoading.value, [tokenId]: true }
   try {
-    const res = await fetch(`/api/auth/tokens/${encodeURIComponent(tokenId)}/stats`)
+    const res = await fetch(`/api/auth/tokens/${encodeURIComponent(tokenId)}/stats`, {
+      credentials: 'include',
+    })
     if (res.ok) {
       const data: TokenStats = await res.json()
       tokenStats.value = { ...tokenStats.value, [tokenId]: data }
+    } else {
+      // Помечаем как null для возможности повторной попытки
+      tokenStats.value = { ...tokenStats.value, [tokenId]: null as unknown as TokenStats }
     }
   } catch {
     // Non-critical — stats are supplemental
   } finally {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/src/views/TokensView.vue` around lines 20 - 36, In loadTokenStats: include
credentials: 'include' in the fetch call to ensure cookie-based auth, and change
the early-return guard from tokenStats.value[tokenId] !== undefined to a
presence check using Object.prototype.hasOwnProperty.call(tokenStats.value,
tokenId) (or an equivalent key-owned check) so a failed fetch that left no key
does not block retries; keep statsLoading handling and only set
tokenStats.value[...] on successful res.ok, and ensure the catch/finally still
clears statsLoading[tokenId].
scripts/install.sh-176-179 (1)

176-179: ⚠️ Potential issue | 🟡 Minor

--client-only не очищает старый engram-server.

В client-only ветке новый бинарь просто не копируется, но уже существующий engram-server остаётся в $INSTALL_DIR. Потом Line 258 копирует весь каталог в cache, так что обновление поверх старой full-установки продолжит таскать server binary, хотя режим по умолчанию теперь client-only.

🧹 Минимальная очистка stale binary
     # Copy binaries (--client-only skips server binary)
     if [[ "$INSTALL_MODE" == "full" ]]; then
         cp "$tmp_dir/engram-server" "$INSTALL_DIR/" 2>/dev/null || true
+    else
+        rm -f "$INSTALL_DIR/engram-server"
     fi

Also applies to: 211-213

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/install.sh` around lines 176 - 179, When INSTALL_MODE is
"client-only" the script currently skips copying engram-server but does not
remove an existing stale binary; update the install logic around the
engram-server handling to explicitly remove any existing
"$INSTALL_DIR/engram-server" when INSTALL_MODE != "full" (i.e., in the
client-only branch) before proceeding, and apply the same cleanup in the other
similar block (the one referenced around lines handling tmp_dir copy at
211-213); use the INSTALL_MODE and INSTALL_DIR variables and the "engram-server"
filename to locate the code to modify.
docs/DEPLOYMENT.md-94-104 (1)

94-104: ⚠️ Potential issue | 🟡 Minor

Переименование в manual Docker осталось незавершённым.

В этом блоке сервер уже называется engram-server, но шаг выше всё ещё использует cmplus-postgres и cmplus-pgdata. В результате пример остаётся с двумя наборами имён и сбивает при копипасте.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/DEPLOYMENT.md` around lines 94 - 104, The deployment docs are
inconsistent: the server is named engram-server but the previous DB containers
still use cmplus-postgres and cmplus-pgdata; update those container names to
match the manual naming (e.g., rename cmplus-postgres -> engram-postgres and
cmplus-pgdata -> engram-pgdata or choose a single prefix) so references are
consistent with --name engram-server and the subsequent -e DATABASE_DSN; change
all occurrences of cmplus-postgres and cmplus-pgdata in the same block to the
chosen engram-* names and ensure the DATABASE_DSN host matches that container
name.
internal/db/gorm/helpers.go-105-110 (1)

105-110: ⚠️ Potential issue | 🟡 Minor

Сделайте экспортируемый mapper nil-safe.

Теперь это публичный helper. Передача nil извне приведёт к panic внутри toModelObservation, что для exported API слишком хрупко.

🛡️ Минимальное исправление
 func ToModelObservation(o *Observation) *models.Observation {
+	if o == nil {
+		return nil
+	}
 	return toModelObservation(o)
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/db/gorm/helpers.go` around lines 105 - 110, ToModelObservation
currently forwards to toModelObservation and will panic if passed nil; make the
exported mapper nil-safe by guarding against nil before delegating (or update
toModelObservation to handle nil) so passing a nil *Observation returns nil (or
a zero-value *models.Observation) instead of panicking; update
ToModelObservation to check if o == nil and return nil (and add a small unit
test for ToModelObservation nil input) while keeping toModelObservation
unchanged unless you prefer centralizing the nil check there.
internal/config/config.go-597-606 (1)

597-606: ⚠️ Potential issue | 🟡 Minor

Новые настройки сейчас нельзя задать через settings.json.

Для ENGRAM_INJECTION_FLOOR и ENGRAM_SESSION_BOOST добавлен только env-override блок. В секции JSON-разбора выше этих ключей нет, поэтому значения в ~/.engram/settings.json будут молча игнорироваться. Если они задуманы как env-only, лучше убрать json-теги, чтобы не создавать ложное ожидание.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/config/config.go` around lines 597 - 606, The JSON settings parser
currently lacks entries for ENGRAM_INJECTION_FLOOR and ENGRAM_SESSION_BOOST so
the env-only overrides in the config init (reading ENGRAM_INJECTION_FLOOR ->
cfg.InjectionFloor and ENGRAM_SESSION_BOOST -> cfg.SessionBoost) silently ignore
values in ~/.engram/settings.json; either add corresponding JSON-parsed fields
to the settings struct/JSON decode path so settings.json values populate
cfg.InjectionFloor and cfg.SessionBoost (and keep the env overrides as
fallbacks), or remove the json tags/consumer expectations for those keys to make
them clearly env-only; locate the settings struct and the JSON decode logic used
for loading settings.json and update it to include InjectionFloor and
SessionBoost (or remove json metadata) so behavior matches expectations.
internal/db/gorm/versioned_document_store.go-172-174 (1)

172-174: ⚠️ Potential issue | 🟡 Minor

Экранируйте % и _ в pathPrefix перед LIKE.

Сейчас pathPrefix+"%" трактует _ и % как wildcard-ы, поэтому prefix-фильтр может возвращать лишние документы для вполне обычных имён путей.

🛠 Вариант правки
 	if pathPrefix != "" {
-		clauses = append(clauses, "path LIKE ?")
-		args = append(args, pathPrefix+"%")
+		escapedPrefix := strings.NewReplacer(`\`, `\\`, `%`, `\%`, `_`, `\_`).Replace(pathPrefix)
+		clauses = append(clauses, `path LIKE ? ESCAPE '\'`)
+		args = append(args, escapedPrefix+"%")
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/db/gorm/versioned_document_store.go` around lines 172 - 174, В блоке
где формируются clauses/args для фильтра по path (переменные pathPrefix,
clauses, args и строка "path LIKE ?") нужно экранировать спецсимволы SQL-LIKE в
pathPrefix (заменить "%" на "\%" и "_" на "\_") и затем подставлять
экранированный префикс с "%" для поиска по префиксу; одновременно изменить
строку условия на "path LIKE ? ESCAPE '\\'" чтобы СУБД правильно
интерпретировала обратный слеш как экранирующий символ. Это гарантирует, что
обычные символы '_' и '%' в pathPrefix не будут трактоваться как wildcard'ы.
🧹 Nitpick comments (5)
ui/src/views/SystemView.vue (2)

60-70: Ошибка получения статуса vault не отображается пользователю.

vaultResult не включён в массив failures на строке 65, поэтому ошибки при получении статуса шифрования не будут показаны пользователю. Если это намеренно (некритичная функция), стоит добавить комментарий. Если статус vault важен — добавьте его в обработку ошибок.

♻️ Предложение: добавить vaultResult в обработку ошибок
     // Surface real errors (not AbortErrors for the current signal).
-    const failures = [h, v, g]
+    const failures = [h, v, g, vaultResult]
       .filter(r => r.status === 'rejected')
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/src/views/SystemView.vue` around lines 60 - 70, The vaultResult rejection
isn't included in the failures aggregation so vault-related errors won't be
surfaced; update the failure collection logic that builds failures (currently
from [h, v, g]) to also include vaultResult when its status is 'rejected' (or
otherwise include its reason) and then map/filter out AbortError the same way so
error.value receives vault errors; touch the code around the failures array
construction and the subsequent error.value assignment (symbols: vaultResult,
failures, error.value) and/or add a comment if omission was intentional.

82-88: Отсутствует обратная связь при неудачном копировании.

Если copyToClipboard возвращает false, пользователь не получает никакого уведомления о неудаче. Рекомендуется добавить обработку этого случая для улучшения UX.

♻️ Предложение: добавить обработку ошибки
 async function copyVaultSetupCommand() {
   const ok = await copyToClipboard('openssl rand -hex 32 > vault.key')
   if (ok) {
     vaultCopyFeedback.value = true
     setTimeout(() => { vaultCopyFeedback.value = false }, 2000)
+  } else {
+    // Можно показать уведомление или изменить состояние
+    console.warn('Failed to copy to clipboard')
   }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/src/views/SystemView.vue` around lines 82 - 88, Handle the case when
copyToClipboard returns false in copyVaultSetupCommand: when ok is false, set a
failure feedback state (e.g., create and set vaultCopyError.value = true or
reuse an existing error feedback ref), trigger a short timeout to clear it (like
the success path’s 2s), and optionally log the failure; update the template to
show the error feedback to the user so failed clipboard attempts surface in the
UI (refer to copyVaultSetupCommand, vaultCopyFeedback and copyToClipboard).
ui/src/components/layout/AppSidebar.vue (1)

19-33: Дублирование запроса к /api/auth/me.

Композабл useAuth (см. useAuth.ts) уже вызывает /api/auth/me при проверке аутентификации, но не возвращает поле auth_disabled. Текущая реализация создаёт второй запрос к тому же эндпоинту при монтировании компонента.

Рекомендуется расширить useAuth, чтобы он также возвращал authDisabled, избежав дублирования запросов.

♻️ Альтернативный подход: расширить useAuth

В composables/useAuth.ts:

const authDisabled = ref(false)

async function checkAuth(): Promise<void> {
  loading.value = true
  try {
    const res = await fetch('/api/auth/me', { credentials: 'include' })
    authenticated.value = res.ok
    if (res.ok) {
      const data = await res.json()
      authDisabled.value = data?.auth_disabled === true
    }
  } catch {
    authenticated.value = false
  } finally {
    loading.value = false
  }
}

return {
  // ...existing
  authDisabled: computed(() => authDisabled.value),
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/src/components/layout/AppSidebar.vue` around lines 19 - 33, The component
duplicates /api/auth/me—remove the local checkAuthDisabled in AppSidebar.vue and
instead extend the composable useAuth: add a ref authDisabled, update the
existing checkAuth (or checkAuthentication) to parse data?.auth_disabled and set
authDisabled.value when res.ok, and export authDisabled as a computed property
from useAuth so AppSidebar imports authDisabled from useAuth and reads that
reactive value; ensure the existing loading/authenticated behavior in checkAuth
is preserved.
ui/src/views/ObservationsView.vue (1)

92-114: tagCloud не синхронизирован с текущим контекстом списка.

loadTagCloud() всегда запрашивает глобальное облако и вызывается только при монтировании. После смены проекта или успешных batch-операций основной список уже обновлён, а сайдбар остаётся со старыми/глобальными тегами. Бэкенд уже поддерживает project query в internal/worker/handlers_tags.go:100-160, так что облако лучше перезагружать вместе с проектом и после мутаций.

Also applies to: 125-134, 249-250

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/src/views/ObservationsView.vue` around lines 92 - 114, The tagCloud
sidebar isn’t refreshed when the project context or after batch mutations
change; update ObservationsView.vue to call loadTagCloud with the current
project context (or trigger the same refresh path) whenever the active project
changes and after successful batch actions
(archive/delete/batch-scope/batch-tag) so tagCloud reflects project-scoped tags;
locate the batch handlers in the conditional using batchAction.value and the
loadTagCloud/tagCloud usage to add a reload call, and rely on the backend
project query support implemented in internal/worker/handlers_tags.go to fetch
project-scoped tags.
internal/db/gorm/observation_store.go (1)

1663-1675: GetTopImportanceObservations полностью дублирует GetActiveObservations.

Фильтры, сортировка и Limit здесь совпадают 1:1 с методом выше. Лучше сделать тонкую обёртку, иначе любая правка active/filter-логики легко разъедется между двумя путями.

♻️ Вариант упрощения
 func (s *ObservationStore) GetTopImportanceObservations(ctx context.Context, project string, limit int) ([]*models.Observation, error) {
-	var dbObservations []Observation
-	err := s.db.WithContext(ctx).
-		Scopes(projectScopeFilter(project), activeObservationFilter(), importanceOrdering()).
-		Limit(limit).
-		Find(&dbObservations).Error
-	if err != nil {
-		return nil, err
-	}
-	return toModelObservations(dbObservations), nil
+	return s.GetActiveObservations(ctx, project, limit)
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/db/gorm/observation_store.go` around lines 1663 - 1675,
GetTopImportanceObservations currently duplicates the query logic in
GetActiveObservations; refactor so the shared filtering/ordering is centralized:
either have GetTopImportanceObservations call GetActiveObservations (adding a
limit/importance ordering parameter) or extract the common scope/query
construction into a helper (e.g., buildActiveObservationQuery or
activeObservationsBase) used by both functions; update
GetTopImportanceObservations to reuse that helper or call GetActiveObservations
with the appropriate limit and importance ordering to avoid duplicate
filter/active-logic.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5424c372-94cb-45bb-8828-4feca764bc27

📥 Commits

Reviewing files that changed from the base of the PR and between 847fce4 and 1f7f379.

📒 Files selected for processing (23)
  • docs/DEPLOYMENT.md
  • internal/config/config.go
  • internal/db/gorm/helpers.go
  • internal/db/gorm/migrations.go
  • internal/db/gorm/observation_store.go
  • internal/db/gorm/project_settings_store.go
  • internal/db/gorm/versioned_document_store.go
  • internal/graph/falkordb/client.go
  • internal/graph/noop.go
  • internal/graph/store.go
  • internal/maintenance/service.go
  • internal/relation/causal_classifier.go
  • internal/relation/detector.go
  • internal/search/manager.go
  • internal/worker/handlers_context.go
  • internal/worker/handlers_scoring.go
  • internal/worker/service.go
  • plugin/engram/hooks/stop.js
  • scripts/install.sh
  • ui/src/components/layout/AppSidebar.vue
  • ui/src/views/ObservationsView.vue
  • ui/src/views/SystemView.vue
  • ui/src/views/TokensView.vue

Comment thread internal/db/gorm/migrations.go Outdated
Comment thread internal/maintenance/service.go Outdated
Comment thread internal/relation/detector.go
Comment thread scripts/install.sh
@thebtf
Copy link
Copy Markdown
Owner Author

thebtf commented Mar 24, 2026

🤖 PR Review MCP State (auto-managed, do not edit)
{
  "version": 2,
  "parentChildren": {},
  "resolvedNitpicks": {
    "coderabbit-nitpick-59b18d86-60": {
      "resolvedAt": "2026-03-24T23:09:49.627Z",
      "resolvedBy": "agent"
    },
    "coderabbit-nitpick-c15068bf-82": {
      "resolvedAt": "2026-03-24T23:09:55.297Z",
      "resolvedBy": "agent"
    },
    "coderabbit-nitpick-fef6a006-19": {
      "resolvedAt": "2026-03-24T23:10:38.039Z",
      "resolvedBy": "agent"
    },
    "coderabbit-nitpick-72d4b5d1-92": {
      "resolvedAt": "2026-03-24T23:11:22.670Z",
      "resolvedBy": "agent"
    },
    "coderabbit-nitpick-29056382-1663": {
      "resolvedAt": "2026-03-24T23:12:03.984Z",
      "resolvedBy": "agent"
    }
  },
  "updatedAt": "2026-03-24T23:12:04.448Z"
}

- project_settings: explicit error on DB failure (not silent 0.3)
- falkordb GetCluster: ParameterizedQuery instead of string interpolation
- migration 051: renamed to versioned_documents (avoid conflict with m017)
- versioned_document_store: time.Time fields, transaction on Create, table names
- detector: negative file node IDs, corrected causal classification pair order
- maintenance: fix SQL column names for stale relation cleanup
- install.sh: proper flag parsing (--full doesn't corrupt version arg)
- SystemView: vault copy error feedback
- AppSidebar: deduplicate auth/me fetch via useAuth composable
- ObservationsView: project-aware tag cloud + refresh after mutations
- observation_store: deduplicate GetTopImportanceObservations
@thebtf thebtf merged commit 0bd6069 into main Mar 24, 2026
1 check passed
@thebtf thebtf deleted the feat/self-learning-completion branch March 24, 2026 23:13
thebtf added a commit that referenced this pull request Apr 12, 2026
* refactor: move max_tokens from hardcoded 4096 to ENGRAM_LLM_MAX_TOKENS (#49)

Configurable via env var ENGRAM_LLM_MAX_TOKENS (default: 4096).
Stored in config.Config.LLMMaxTokens and OpenAIConfig.MaxTokens.
Removes magic number from LLM client.

Co-authored-by: Kirill Turanskiy <thebtf@users.noreply.github.com>

* docs: add pre-commit guardrails + re-benchmark tech debt items

* fix: scoring formulas — guidance type weight/decay + meaningful total_results (#50)

- Add type=guidance to typeWeights (1.8, highest) and typeHalfLife (365 days)
- Behavioral rules no longer decay in 7 days or get default weight 1.0
- sourceBoost 1.3 for LLM-extracted guidance (live user_behavior detection)
- total_results now counts observations with composite score > 0.05
  (was raw DB count — in high-dim space all observations passed threshold,
  showing "33 matches" for every query regardless of relevance)

Co-authored-by: Kirill Turanskiy <thebtf@users.noreply.github.com>

* fix: exclude behavioral rules from contradiction detection (#51)

Imported feedback rules (type=decision, concept=user-preference, title
starts with "Rule:") were all classified as contradicting each other
because classifyRelation marks any two decisions with different titles
and similarity > 0.7 as contradicts. 57 rules × 56 peers = 76 false
contradiction edges in the knowledge graph.

Added hasGuidanceConcept() check: skips contradiction detection for
observations that are behavioral rules (type=guidance, or concept
user-preference, or title prefix "Rule:").

Co-authored-by: Kirill Turanskiy <thebtf@users.noreply.github.com>

* chore: update marketplace for v1.6.4

* chore: update marketplace for v1.6.5

* fix: filter heartbeat and Telegram metadata from user prompts (#52)

Skip HEARTBEAT.md polling (openclaw every 30min) and Telegram
conversation/sender metadata from being stored as user prompts.
These are system-generated, not real user interactions.

Co-authored-by: Kirill Turanskiy <thebtf@users.noreply.github.com>

* feat: register PreCompact hook and add discovery logging

PreCompact hook was created but never registered in hooks.json.
Now registered with 10s timeout. Hook writes discovery data to
.agent/pre-compact-discovery.json for empirical testing of
available input fields (transcript_path verification for FR-2).

* feat: always-inject tier for behavioral rules (FR-1, FR-6)

Three-tier injection system: observations tagged with concept
"always-inject" are now fetched independently of similarity
matching and included in every session (session-start) and
every prompt (user-prompt) context.

Server changes:
- GetAlwaysInjectObservations query (concepts @> GIN index)
- GIN indexes on concepts, files_modified, files_read columns
- Migration 048 for all new indexes
- handleContextInject + handleSearchByPrompt return always_inject array
- AlwaysInjectLimit (default 20) and ProjectInjectLimit (default 15) config

Hook changes:
- session-start.js renders <user-behavior-rules> block before <engram-context>
- user-prompt.js merges always-inject + similarity-matched rules with dedup
- Plugin version bumped to 0.6.0

Also adds GetObservationsByFile and GetPreviousObservationInSession
queries for Phase 3 and Phase 4 (no callers yet).

* feat: PreCompact hook sends full transcript to backfill (FR-2)

Reads transcript JSONL at compaction time, parses all user/assistant
messages, and sends to /api/backfill/session in chunks of 50 messages.
Fire-and-forget with 5s timeout per chunk (Constitution Principle 3).

Fallback: if input.transcript_path is missing, derives path by
searching ~/.claude/projects/<hash>/<session>.jsonl.

Also writes discovery report to .agent/pre-compact-discovery.json
for empirical verification of available hook input fields.

* feat: PreToolUse file-context injection (FR-3)

New hook and endpoint for automatic file-specific knowledge
injection before Edit/Write operations.

Server:
- GET /api/context/by-file endpoint (handlers_context_file.go)
- Returns observations matching files_modified/files_read
- Graceful degradation: empty response on error (NFR-3)

Hook:
- pre-tool-use.js matches Edit/Write tools only
- Extracts file_path, queries /api/context/by-file
- Returns <file-context> XML block as systemMessage
- 200ms timeout with empty fallback
- Registered in hooks.json with "Edit|Write" matcher

* feat: causal chain linking — follows + prompted_by relations (FR-4, FR-5)

Observations within the same session are now automatically linked:
- "follows" relation: connects consecutive observations by prompt_number
- "prompted_by" relation: links observation to the user prompt that triggered it

Both relations are created via pure DB queries (< 10ms overhead per
observation, NFR-4) during the existing relation detection pipeline.

Changes:
- relation/detector.go: add temporal + prompt linking before similarity search
- prompt_store.go: add GetPromptForObservation query
- service.go: pass promptStore to NewDetector constructor

* refactor: extract shared normalizeEngramContent helper and normalize write-tool check

- Create plugin/openclaw-engram/src/hooks/content.ts with normalizeEngramContent()
  centralizing stripEngramContext + CONTENT_MAX_CHARS truncation used by both
  before-compaction and session-end hooks (eliminates duplicate implementations)
- Update before-compaction.ts and session-end.ts to import and use the shared helper
- Simplify WRITE_TOOLS Set to lowercase-only entries and normalize via
  toolName.toLowerCase() in isWriteOrEdit() for reliable case-insensitive matching

* fix: convert text columns to jsonb before GIN index creation

Migration 048 failed because concepts, files_modified, files_read
were stored as text type. PostgreSQL GIN indexes require jsonb.

Fix: ALTER COLUMN TYPE jsonb USING COALESCE(col::jsonb, '[]'::jsonb)
before CREATE INDEX. Also update GORM model tags from type:text to
type:jsonb for consistency.

* fix: Phase 1 — Security & Reliability (P0) (#57)

* fix: security and reliability improvements (Phase 1 T001-T005)

- T001/T002: Apply privacy.RedactSecrets to LLM extraction output
  before parsing observations (Constitution P9 fix). Both live
  extraction (processor.go) and backfill (handlers_backfill.go).
- T003: Expand CSP headers from `default-src 'self'` to full
  directive set with script/style/connect/img/font/frame rules.
- T004: Add truncated args (200 chars) to MCP tool call error log.
- T005: Add diagnostic state (llmClient configured status) to
  callLLM error messages for debugging.

* feat: MCP health monitoring, bounded semaphore, fire-and-forget vault (T006-T010)

- T006: New internal/mcp/health.go — atomic request/error counters
  with 5-minute sliding window for MCP endpoint monitoring
- T007: GET /api/mcp/health public endpoint registered
- T008: Streamable HTTP handler wired to health counters
- T009: Removed nil-semaphore unbounded goroutine fallback — always
  use bounded semaphore, drop on overflow with warning
- T010: vaultStoreDetectedSecrets now fire-and-forget with 3s timeout
  goroutine (Constitution P3 compliance)

* fix: address PR review findings — CSP hardening, args redaction, race fix

- CSP: add object-src 'none' + base-uri 'self' per Gemini review
- Redact args in error log before logging (prevent secret leakage)
- Fix TOCTOU race in MCPHealth.rotateWindowIfNeeded with CompareAndSwap
- TODO: migrate unsafe-inline to nonce/hash-based CSP

---------

Co-authored-by: Kirill Turanskiy <thebtf@users.noreply.github.com>

* feat: dashboard REST endpoints + MCP tool aliases (Phase 2+4) (#58)

Dashboard backend (Phase 2):
- POST /api/observations/batch-tag — bulk tag add/remove
- DELETE /api/observations/bulk — bulk delete observations
- PATCH /api/observations/bulk-scope — bulk scope change
- GET /api/observations/tag-cloud — top tags with counts
- GET /api/auth/tokens/:id/stats — per-token usage stats
- auth_disabled field in /api/auth/me response

MCP tools (Phase 4):
- find_by_file_context — wraps GetObservationsByFile
- include_all parameter for tools/list (+ cursor: "all" compat)
- Vault aliases: vault_store, vault_get, vault_list, vault_delete
- Document aliases: doc_list_collections, doc_get, doc_ingest, etc.

Co-authored-by: Kirill Turanskiy <thebtf@users.noreply.github.com>

* feat: Phases 2-5 + 8a/8b/8c/8d — dashboard, self-learning, consistency, documents (#59)

* feat: dedup threshold, manual search signal, install.sh client-only, docs fix

- T029: Raise DedupSimilarityThreshold from 0.55 to 0.7 (pre-test confirmed safe)
- T031: Add manual search feedback signal in stop.js — detects engram
  tool usage during session, sends insufficient_injection signal
- T038: install.sh defaults to --client-only (skips engram-server binary)
- T039: Fix cmplus-server naming in DEPLOYMENT.md to engram-server

* feat: intentional links + file→observation graph edges (FR-36, FR-37)

- T055: Parse [[obs:1234]] syntax in narratives → create bidirectional
  references/referenced_by graph edges
- T056: files_modified/files_read entries → modifies/reads graph edges
  using FNV-1a hash of file path as stable node ID
- Both integrated into existing Detect() pipeline (event-driven async)

* feat: add GetCluster to GraphStore interface (FR-38)

- GraphStore interface: GetCluster(nodeID, maxNodes) returns cluster IDs
- FalkorDB: BFS traversal up to 3 hops with LIMIT
- NoopGraphStore: returns empty slice

* feat: LLM causal classifier for error→fix and correction linking (FR-44/45)

- New causal_classifier.go: LLM prompt classifies observation pairs as
  fixed_by, corrects, or unrelated
- Wired into Detect() pipeline: triggers for bugfix/guidance types on
  top-3 similarity candidates only (~1 LLM call per 5 observations)
- SetCausalClassifier() method on Detector (opt-in, nil = disabled)
- ShouldClassify() filter: only bugfix and guidance types

* feat: migration 051 — documents + document_comments tables (FR-46)

Foundation for AI agent collaboration platform:
- documents: versioned, typed (markdown/task/review/decision),
  JSONB metadata (assignee/status/priority), author attribution
- document_comments: inline and general comments with line ranges
- Indexes: project+path+version, doc_type, document_id

* feat: Phase 2 frontend + Phase 3 self-learning + Phase 8a consistency + document store

Phase 2 Frontend (T017-T021):
- Bulk action dropdown (delete/scope/tag) in ObservationsView
- Tag cloud sidebar with clickable filters
- Per-token stats (request count, last used) in TokensView
- Auth-disabled warning badge in AppSidebar
- Vault encryption setup helper in SystemView

Phase 3 Self-Learning (T023-T028):
- Injection floor: always inject at least N observations (default 3)
- Cross-session priming: 1.3x boost for recent sessions
- Adaptive per-project relevance threshold (project_settings table)
- Feedback-driven threshold adjustment (used→lower, ignored→raise)

Phase 8a Consistency Engine (T050-T054):
- Orphan vector cleanup (vectors without observations)
- Missing vector detection (observations without embeddings)
- Stale relation cleanup (broken source/target references)
- FalkorDB↔PostgreSQL drift detection + auto re-sync
- Embedding model change detection via system_config table

Phase 8d Document Store (T061):
- VersionedDocumentStore with Create/ReadLatest/ReadVersion/List/
  GetHistory/AddComment/GetComments GORM methods
- SHA-256 content hashing, version tracking, DISTINCT ON for latest

* fix: address 12 PR review findings on Phase 2-8 implementation

- project_settings: explicit error on DB failure (not silent 0.3)
- falkordb GetCluster: ParameterizedQuery instead of string interpolation
- migration 051: renamed to versioned_documents (avoid conflict with m017)
- versioned_document_store: time.Time fields, transaction on Create, table names
- detector: negative file node IDs, corrected causal classification pair order
- maintenance: fix SQL column names for stale relation cleanup
- install.sh: proper flag parsing (--full doesn't corrupt version arg)
- SystemView: vault copy error feedback
- AppSidebar: deduplicate auth/me fetch via useAuth composable
- ObservationsView: project-aware tag cloud + refresh after mutations
- observation_store: deduplicate GetTopImportanceObservations

---------

Co-authored-by: Kirill Turanskiy <thebtf@users.noreply.github.com>

* feat: document MCP tools + OpenClaw message classification (T062-T064, T047-T049) (#60)

Document MCP tools (T062):
- 6 new tools: doc_create, doc_read, doc_update, doc_list, doc_history, doc_comment
- VersionedDocumentStore wired into MCP server and service
- T063 skipped (memory_get not an MCP tool)
- T064: embedding integration point marked with TODO

OpenClaw message classification (T047-T049):
- New message-classifier.ts with allowlist approach for heartbeat/system detection
- before-prompt-build.ts + after-tool-call.ts updated to use classifier
- source: "openclaw" added to observation storage calls
- always_inject rendering verified in context injection

Bump openclaw-engram version 1.3.1 → 1.4.0

Co-authored-by: Kirill Turanskiy <thebtf@users.noreply.github.com>

* chore: update marketplace for v1.7.0

* fix: write pre-compact discovery JSON to project dir (#61)

* fix: write pre-compact discovery JSON to project dir, not plugin dir

The PreCompact hook used __dirname-relative path to write
pre-compact-discovery.json, which resolved to the plugin install
cache instead of the project .agent/ directory. Use ctx.CWD instead.

* fix: simplify projectDir fallback in pre-compact hook

ctx.CWD is already derived from input.cwd in lib.js with type safety,
making the intermediate input.cwd check redundant and potentially unsafe
(truthy non-string values would bypass ctx.CWD's type guarantee).

---------

Co-authored-by: Kirill Turanskiy <thebtf@users.noreply.github.com>

* fix: dashboard type filter, tag cloud, SSE auth (#62)

* fix: server-side type filter, add guidance type, fix tag cloud SQL

- Add `type` query param to GET /api/observations for server-side filtering
- Add obsType param to GetAllRecentObservationsPaginated and
  GetObservationsByProjectStrictPaginated with optional WHERE clause
- Frontend: pass type to API, remove client-side filteredObservations filter
- Add `guidance` to ObservationType union, OBSERVATION_TYPES, TYPE_CONFIG
- Fix tag cloud SQL: COALESCE(is_superseded, 0) = 0 (bigint, not boolean)

* fix: support query param token auth for SSE EventSource

EventSource API cannot set custom headers. Add ?token= query param
fallback in auth middleware so SSE /api/events can authenticate.

* fix: address review findings — DRY query builder, restrict token query param

- Refactor observation store: extract buildBaseQuery helper to reduce
  duplication between paginated functions
- Restrict query param token auth to SSE-only endpoints (/api/events,
  /sse, /api/logs) instead of all routes

---------

Co-authored-by: Kirill Turanskiy <thebtf@users.noreply.github.com>

* fix: sidebar metrics use snake_case to match API response (#63)

RetrievalStats interface used PascalCase (TotalRequests) but API
returns snake_case (total_requests). Sidebar showed 0 for all
retrieval metrics. Fixed in api.ts, Sidebar.vue, AppSidebar.vue.

Co-authored-by: Kirill Turanskiy <thebtf@users.noreply.github.com>

* docs: update TECHNICAL_DEBT.md with dashboard findings (#64)

Add entries for type filter (resolved), SDK extraction types,
and dashboard memories view feature request.

Co-authored-by: Kirill Turanskiy <thebtf@users.noreply.github.com>

* chore: update marketplace for v1.7.1

* fix: reuse existing session in bulk import instead of creating phantom (#65)

- Add optional session_id to BulkImportRequest — if provided, uses
  CreateSDKSession with that ID (idempotent: INSERT OR IGNORE + fetch)
- If not provided, falls back to bulk-import-{timestamp} (backward compat)
- OpenClaw engram-remember tool now passes ctx.sessionId to bulkImport
- Fixes 403+ phantom bulk-import-* sessions in openclaw project
- Bump openclaw-engram 1.4.0 → 1.4.1

Co-authored-by: Kirill Turanskiy <thebtf@users.noreply.github.com>

* chore: update marketplace for v1.7.2

* fix: migration 052 — delete phantom bulk-import sessions (#66)

Cleanup 403+ phantom sdk_sessions created by bulk-import before PR #65.
Deletes sessions matching 'bulk-import-%' with prompt_counter=0.

Co-authored-by: Kirill Turanskiy <thebtf@users.noreply.github.com>

* chore: update marketplace for v1.7.3

* fix: LLM extraction now produces diverse observation types (#67)

Previously all extracted learnings were hardcoded to type=guidance.
Now:
- Prompt asks LLM to classify as guidance/decision/bugfix/discovery/
  feature/refactor/change
- learningToObsType() maps LLM type to observation type
- Legacy signal field still supported as fallback

Co-authored-by: Kirill Turanskiy <thebtf@users.noreply.github.com>

* fix: migration 053 — delete vault credentials with lost encryption key (#68)

All 15 credentials encrypted with auto-generated key that was lost
when Docker container was recreated. AES-256-GCM = unrecoverable.
Vault status confirmed mismatch_count=15 = credential_count=15.
Users will re-create credentials with current ENGRAM_VAULT_KEY.

Co-authored-by: Kirill Turanskiy <thebtf@users.noreply.github.com>

* chore: update marketplace for v1.7.4

* feat: observation status lifecycle (v1.8 Phase 1) (#69)

* feat: observation status lifecycle — migration, model, API, MCP (Phase 1 backend)

- Migration 054: status TEXT DEFAULT 'active' + status_reason TEXT + index
- Observation model: Status + StatusReason fields in GORM and shared models
- ObservationUpdate: Status + StatusReason fields for edit_observation
- Paginated queries: status filter param (backward compat, "" = all)
- Context injection: COALESCE(status, 'active') = 'active' on all query paths
- handleGetObservations: ?status= query param
- edit_observation MCP tool: status (enum active/resolved) + status_reason

* feat: observation status lifecycle UI (Phase 1 frontend)

- TypeScript: status + status_reason fields on Observation interface
- API client: status param in fetchObservationsPaginated, updateObservationStatus()
- ObservationsView: status pill toggle (All/Active/Resolved), resolve button
  with reason modal, resolved card styling (opacity-50 + line-through),
  reopen button (green), bulk resolve, status_reason tooltip

---------

Co-authored-by: Kirill Turanskiy <thebtf@users.noreply.github.com>

* feat: dashboard Memories view — filter, card variant, inline edit, delete (Phase 2) (#70)

- Backend: memory_type filter in paginated queries ("any" = all memories, specific value = exact match)
- handleGetObservations: ?memory_type= query param
- Frontend: All/Memories toggle, memory cards with purple accent + brain icon,
  scope badges (project/global), inline edit (title + narrative), delete with confirm

Co-authored-by: Kirill Turanskiy <thebtf@users.noreply.github.com>

* feat: pattern insight — LLM summary + source observations (Phase 3) (#71)

- New: GET /api/patterns/{id}/observations — resolve observation_ids
- New: POST /api/patterns/{id}/insight — LLM summary with cache
- New: internal/learning/pattern_insight.go — GeneratePatternInsight
- Frontend: inline expand on pattern card (replaces useless modal),
  skeleton loading, LLM summary + source observation list,
  cache indicator, unavailable fallback with retry

Co-authored-by: Kirill Turanskiy <thebtf@users.noreply.github.com>

* feat: pattern cleanup — orphan detection, confidence recalc, bulk archive (Phase 4) (#72)

- Orphan pattern detection: verify observation_ids against existing observations
- Batch confidence recalculation using existing formula
- POST /api/maintenance/patterns/cleanup with dry_run + threshold params
- Frontend: cleanup section with preview (dry_run) + confirm button

Co-authored-by: Kirill Turanskiy <thebtf@users.noreply.github.com>

* chore: mark v1.8 items resolved in TECHNICAL_DEBT.md (#73)

Co-authored-by: Kirill Turanskiy <thebtf@users.noreply.github.com>

* fix: remove unused TypeScript imports in usePatterns (CI build fix) (#74)

fetchPatternInsight and legacyInsights were declared but never read.
vue-tsc strict mode treats these as errors.

Co-authored-by: Kirill Turanskiy <thebtf@users.noreply.github.com>

* chore: update marketplace for v1.8.0

* fix: 3 post-v1.8.0 bugs — memories empty, insight timeout, filter mess (#75)

1. store_memory now sets memory_type via ClassifyMemoryType() — was never
   populated, causing Memories tab to show "No observations found"
2. Pattern insight: 5s context timeout + nil LLM guard — was hanging
   indefinitely when LLM model loading was slow
3. ObservationsView filter bar restructured: 2-row layout with project +
   view mode on top, type filters + status pills below with divider.
   Type filters hidden in Memories view.

Co-authored-by: Kirill Turanskiy <thebtf@users.noreply.github.com>

* chore: update marketplace for v1.8.0

* fix: migration 055 — backfill memory_type for existing store_memory observations (#76)

Existing observations from store_memory (source_type='manual') had empty
memory_type. Classifies using same logic as ClassifyMemoryType():
type=guidance→guidance, concepts keywords→decision/pattern/preference/etc,
default→context. Enables Memories tab to show historical memories.

Co-authored-by: Kirill Turanskiy <thebtf@users.noreply.github.com>

* fix: increase pattern insight LLM timeout from 5s to 30s (#77)

Ollama cold start can take 10-30s for model loading. 5s was too
aggressive for interactive (non-hot-path) insight generation.
Extraction pipeline works because it has no timeout constraint.

Co-authored-by: Kirill Turanskiy <thebtf@users.noreply.github.com>

* fix: backfill NULL status to 'active', COALESCE in paginated queries (#78)

A1 anomaly: migration 054 ADD COLUMN DEFAULT only sets value for new rows.
708 existing observations had status=NULL. Dashboard "Active" filter
matched 0 because WHERE status='active' skips NULLs.

- Migration 055: UPDATE SET status='active' WHERE NULL
- Paginated queries: COALESCE(status, 'active') = ? for safety
- Renumbered memory_type backfill to migration 056

Co-authored-by: Kirill Turanskiy <thebtf@users.noreply.github.com>

* fix: LLM API key falls back to embedding key when not set (#79)

ENGRAM_LLM_API_KEY was empty while ENGRAM_EMBEDDING_API_KEY was set.
Both point to same LiteLLM proxy but LLM chat completions sent
requests without auth → 401 → context deadline exceeded.

Now DefaultOpenAIConfig falls back to ENGRAM_EMBEDDING_API_KEY,
matching existing URL fallback pattern.

Co-authored-by: Kirill Turanskiy <thebtf@users.noreply.github.com>

* fix: pattern insight timeout 30s→120s for Ollama cold start (#80)

Ollama loads 9B model from disk in 30-60s on cold start.
qwen3-8b took 58s to respond with 20 tokens.
30s was not enough — increased to 120s for interactive insight.

Co-authored-by: Kirill Turanskiy <thebtf@users.noreply.github.com>

* chore: update marketplace for v1.8.0

* fix: increase OpenClaw inject/search timeout 5s→15s to prevent cooldown (#81)

Root cause: OpenClaw client default timeout = 5s. Inject endpoint returns
80KB+ with vector search, taking 0.7-2s normally but longer under load.
3 consecutive AbortController timeouts → AvailabilityTracker cooldown 60s →
ALL engram tools disabled (search, decisions, store_memory, recall).

Fix: explicit 15s timeout for getContextInject + searchContext.
Other endpoints (health=3s, selfcheck=5s, mark-injected=3s) keep shorter timeouts.

Co-authored-by: Kirill Turanskiy <thebtf@users.noreply.github.com>

* fix: add elapsed time + abort reason to OpenClaw client error logs (#82)

Inject/search requests abort with "This operation was aborted" but no
timing data — impossible to tell if it's timeout (5s), connection
refused (immediate), or slow response (2-4s).

Now logs: "[engram] POST /api/context/inject failed after 5003ms (timeout=5000ms)"
Also includes elapsed time for HTTP errors and success path.

Co-authored-by: Kirill Turanskiy <thebtf@users.noreply.github.com>

* feat: closed-loop learning Phase 1 — outcome tracking + injection binding (#83)

* feat: closed-loop learning Phase 1 — outcome tracking + injection binding

Foundations for closed-loop self-learning (Agent Lightning integration):

Schema:
- Migration 057: sdk_sessions outcome/outcome_reason/outcome_recorded_at/injection_strategy
- Migration 058: observation_injections junction table with session + observation indexes

Backend:
- InjectionStore: batch record, query by session, TTL cleanup
- DetermineSessionOutcome heuristic: success (bugfix/feature obs), partial, abandoned
- POST /api/sessions/{id}/outcome endpoint
- set_session_outcome MCP tool
- handleContextInject records injections to junction table (fire-and-forget)
- handleSessionMarkInjected also writes to junction table

New files: injection_store.go, outcome.go, handlers_learning.go, tools_learning.go

* feat: stop hook records session outcome for closed-loop learning (T010)

Heuristic: bugfix/feature observations = success, any obs = partial,
none = abandoned. Calls POST /api/sessions/{id}/outcome.
No transcript content parsing (NFR-4 compliant).

---------

Co-authored-by: Kirill Turanskiy <thebtf@users.noreply.github.com>

* feat: closed-loop learning Phase 2 — score propagation + effectiveness (#84)

Closes the feedback loop: session outcomes flow back to observation scores.

Schema:
- Migration 059: effectiveness_score, effectiveness_injections, effectiveness_successes on observations

Backend:
- PropagateOutcome: position-weighted utility_score adjustment (always_inject=1.0x,
  recent=0.8x, relevant=0.5x), ±0.05 per-session cap, [0,1] clamp
- ComputeEffectiveness: successes/injections with min_data threshold (10 sessions)
- GET /api/observations/{id}/effectiveness endpoint
- Scoring calculator: EffectivenessContrib blended into ImportanceScore (weight 0.3)
- Maintenance: periodic effectiveness recalc from junction table + 90-day TTL cleanup

New files: propagator.go, effectiveness.go

Co-authored-by: Kirill Turanskiy <thebtf@users.noreply.github.com>

* chore: update marketplace for v1.9.0

* feat: closed-loop learning Phase 3 — injection strategy A/B testing (#85)

4 strategies: baseline, effectiveness-weighted, recency-boosted, diverse.
Round-robin assignment per session (configurable: fixed mode available).

- Config: InjectionStrategies, InjectionStrategyMode, DefaultStrategy
- StrategySelector: atomic round-robin for thread safety
- applyStrategy(): re-sorts/filters observations per strategy
- Strategy recorded on sdk_sessions.injection_strategy
- GET /api/learning/strategies: per-strategy outcome rate comparison
- session-start.js: logs assigned strategy

Co-authored-by: Kirill Turanskiy <thebtf@users.noreply.github.com>

* feat: closed-loop learning Phase 4 — agent-specific learning (#86)

Per-agent effectiveness tracking: each agent gets its own effectiveness
scores for observations, enabling personalized injection.

- Migration 060: agent_observation_stats table (agent_id, observation_id PK)
- AgentStatsStore: upsert (atomic ON CONFLICT), batch lookup, single lookup
- PropagateAgentStats: updates per-agent counters alongside global
- handleContextInject: uses agent-specific effectiveness when agent has 10+ injections
- Effectiveness API: ?agent_id=X returns agent-specific stats

Co-authored-by: Kirill Turanskiy <thebtf@users.noreply.github.com>

* feat: closed-loop learning Phase 5 — APO-lite (automatic prompt optimization) (#87)

Low-effectiveness guidance auto-rewritten by LLM, A/B tested, condensed.

Schema:
- Migration 061: observation_versions table (versioned narratives)

Backend:
- VersionStore: create/get/set active version
- RewriteGuidance: LLM-based APO with effectiveness-aware prompt
- POST /api/maintenance/apo/rewrite endpoint (dry_run + apply modes)
- Maintenance: detect APO candidates (effectiveness < 0.4 after 15+ injections)
- applyActiveVersions: inject uses versioned narrative when available
- 3 format variants: bullet-only, concise, structured
- CondenseObservation: standalone utility for future auto-condensation

Co-authored-by: Kirill Turanskiy <thebtf@users.noreply.github.com>

* feat: closed-loop learning Phase 6 — auto signals + learning dashboard (#88)

Final implementation phase: hook-based reward signals + frontend visualization.

Hooks:
- post-tool-use.js: detect git commits, PRs, error streaks from tool metadata (NFR-4)
- stop.js: enhanced outcome with signal counts, upgrade partial→success on commits
- lib.js: cross-process signal store via temp files

Backend:
- Signal weights config (git_commit=1.0, pr_created=2.0, etc.)
- GET /api/learning/curve: daily outcome rates for learning curve chart

Frontend:
- Effectiveness badge on observation cards (green/yellow/red/gray dot)
- LearningView.vue: effectiveness distribution, learning curve, strategy comparison
- API: fetchLearningCurve, fetchStrategies, fetchEffectiveness
- Sidebar: Learning nav item + router route

Co-authored-by: Kirill Turanskiy <thebtf@users.noreply.github.com>

* chore: update marketplace for v2.0.0

* fix: split multi-statement migrations 058, 061 for PostgreSQL (#89)

PostgreSQL prepared statements reject multiple commands in a single
Exec(). Migrations 058 (observation_injections) and 061
(observation_versions) had CREATE TABLE + CREATE INDEX in one call.
Split into separate Exec() calls per statement.

Co-authored-by: Kirill Turanskiy <thebtf@users.noreply.github.com>

* chore: update marketplace for v2.0.0

* chore: update marketplace for v2.0.0 (#90)

Co-authored-by: Kirill Turanskiy <thebtf@users.noreply.github.com>

* chore: sync plugin versions to server v2.0.0 (#91)

All plugin versions now match server version:
- engram (Claude Code): 0.6.0 → 2.0.0
- openclaw-engram (npm): 1.4.3 → 2.0.0
- marketplace.json: already 2.0.0

Going forward: plugins bump to server version on every release (Constitution #15).

Co-authored-by: Kirill Turanskiy <thebtf@users.noreply.github.com>

* fix: escape Windows backslashes in file-context JSONB query (#92)

GetObservationsByFile used fmt.Sprintf to build JSON array for @>
operator. Windows paths like D:\Dev\... contain backslashes which
are invalid JSON escape sequences → SQLSTATE 22P02.

Fix: use json.Marshal([]string{filePath}) for proper escaping.

Co-authored-by: Kirill Turanskiy <thebtf@users.noreply.github.com>

* chore: bump plugin versions to 2.0.1 (#93)

Sync with server v2.0.1 (Constitution #15).
- engram plugin: 2.0.0 → 2.0.1
- openclaw-engram: 2.0.0 → 2.0.1

Co-authored-by: Kirill Turanskiy <thebtf@users.noreply.github.com>

* chore: update marketplace for v2.0.1

* test: fix 6 test failures — USERPROFILE, CSP, trivial filter, obs types (#94)

- config: set USERPROFILE alongside HOME for Windows (os.UserHomeDir reads USERPROFILE)
- worker: update CSP assertion to match stricter security headers
- sdk: change test tool names from Bash to Edit to bypass trivial filter
- sdk: add "guidance" to valid observation types map
- sdk: update Read/Grep expected skip behavior (whitelist approach)

Co-authored-by: Kirill Turanskiy <thebtf@users.noreply.github.com>

* feat: periodic outcome recording — server-side closed-loop trigger (#95)

Users don't close sessions (always continue) and PreCompact is rare
with 1M context. Stop hook never fires → outcome never recorded →
closed loop never closes.

Fix: server-side periodic job (every 15 minutes, configurable) finds
sessions with injection records but no outcome, determines outcome
from observation types, records + propagates.

- GetSessionsWithPendingOutcome: sessions with injections >10min old, no outcome
- runOutcomeRecorder: separate goroutine from heavy maintenance
- Config: ENGRAM_OUTCOME_RECORDER_INTERVAL_MINUTES (default 15)

Co-authored-by: Kirill Turanskiy <thebtf@users.noreply.github.com>

* chore: bump plugins to v2.0.2 (periodic outcome recorder) (#96)

Constitution #15: plugin versions track server.

Co-authored-by: Kirill Turanskiy <thebtf@users.noreply.github.com>

* chore: update marketplace for v2.0.2

* fix: add diagnostic logging to stop hook for investigation (#97)

Stop hook has zero traces in server logs — unclear if it's:
1. Not called by CC harness
2. Called but silently failing (catch returns '')
3. Called but session lookup fails

Added: health check marker (proves hook ran), session lookup error logging,
invalid session ID logging. Will reveal root cause on next session exit.

Co-authored-by: Kirill Turanskiy <thebtf@users.noreply.github.com>

* feat: store_memory accepts always_inject param for behavioral rules (#98)

When always_inject=true, adds "always-inject" concept to observation.
Observations with this concept are injected into every agent context
regardless of query relevance (user-prompt.js hook filters on it).

Closes gap: store_memory previously couldn't create behavioral rules
because it didn't set the always-inject concept marker.

Co-authored-by: Kirill Turanskiy <thebtf@users.noreply.github.com>

* fix: migration 062 — cleanup remaining phantom bulk-import sessions (#99)

6 remaining bulk-import-* sessions from before PR #65 fix.
Migration 052 cleaned most; this catches the rest.

Co-authored-by: Kirill Turanskiy <thebtf@users.noreply.github.com>

* chore: mark 2 tech debt items resolved (#100)

- Phantom bulk-import sessions: cleaned by migration 062 (PR #99)
- T027 post-deploy verification: composite scoring active in v2.0.2

Co-authored-by: Kirill Turanskiy <thebtf@users.noreply.github.com>

* chore: bump plugins to v2.0.3 + stop hook diagnostics (#101)

- engram plugin: 2.0.2 → 2.0.3
- openclaw-engram: 2.0.2 → 2.0.3
- stop.js: diagnostic file marker + error logging (PR #97 changes)

Co-authored-by: Kirill Turanskiy <thebtf@users.noreply.github.com>

* chore: update marketplace for v2.0.3

* fix: dedup skips suppressed observations + edit_observation always_inject (#102)

Two fixes for behavioral rules workflow:

1. store_memory dedup: suppressed/archived observations no longer block
   re-creation. Vector index doesn't exclude suppressed obs, so dedup
   now checks DB for is_suppressed/is_archived before rejecting.

2. edit_observation: accepts always_inject boolean. When true, adds
   "always-inject" concept to existing concepts. When false, removes it.
   Enables converting existing observations to behavioral rules.

Co-authored-by: Kirill Turanskiy <thebtf@users.noreply.github.com>

* chore: bump plugins to v2.0.4 (dedup fix + always_inject edit) (#103)

Co-authored-by: Kirill Turanskiy <thebtf@users.noreply.github.com>

* chore: update marketplace for v2.0.4

* fix: add effectiveness + status fields to ObservationJSON serialization (#104)

ObservationJSON struct was missing effectiveness_score,
effectiveness_injections, effectiveness_successes, status,
and status_reason fields. Observations list API returned these
as undefined → Learning Dashboard showed 100% "Insufficient data".

Fields existed on Observation struct but were never copied to
ObservationJSON in MarshalJSON.

Co-authored-by: Kirill Turanskiy <thebtf@users.noreply.github.com>

* chore: bump plugins to v2.0.5 (effectiveness JSON fix) (#105)

Co-authored-by: Kirill Turanskiy <thebtf@users.noreply.github.com>

* chore: update marketplace for v2.0.5

* fix: server-side effectiveness distribution for Learning Dashboard (#106)

Replaced client-side counting (fetch 500 obs, count tiers) with
server-side SQL aggregation endpoint.

- GET /api/learning/effectiveness-distribution: COUNT FILTER by tier
- GetEffectivenessDistribution: single SQL query, excludes archived/suppressed
- LearningView: uses server endpoint, no more fetchObservationsPaginated
- Removes 500-observation limit and 80KB+ unnecessary payload

Co-authored-by: Kirill Turanskiy <thebtf@users.noreply.github.com>

* chore: bump plugins to v2.0.6 (server-side effectiveness) (#107)

Co-authored-by: Kirill Turanskiy <thebtf@users.noreply.github.com>

* chore: update marketplace for v2.0.6

* fix: session outcome/strategy fields in API + session_id in inject (#108)

Three root causes for Learning Dashboard empty data:

1. GORM SDKSession model missing outcome/strategy fields — DB has data
   but GORM never reads it. Added 4 fields to both GORM and shared models.

2. session-start.js inject call missing session_id param — inject handler
   fell back to empty string → UpdateInjectionStrategy matched 0 rows.
   Now passes ctx.SessionID in inject URL.

3. toModelSDKSession mapping missing outcome/strategy fields.

Co-authored-by: Kirill Turanskiy <thebtf@users.noreply.github.com>

* chore: bump plugins to v2.0.7 (session fields + inject session_id) (#109)

Co-authored-by: Kirill Turanskiy <thebtf@users.noreply.github.com>

* chore: update marketplace for v2.0.7

* fix: effectiveness distribution excludes never-injected observations (#110)

"Insufficient data" showed 797 observations including those never
injected. Now only counts observations with effectiveness_injections > 0:
- Participated but <10 sessions → "Insufficient data" (will resolve)
- Never injected → excluded (dead weight, not actionable)

Co-authored-by: Kirill Turanskiy <thebtf@users.noreply.github.com>

* feat: session injection retrospective API (#111)

GET /api/sessions/{id}/injections — returns all observations injected
into a session with effectiveness metrics and summary stats.

Enables retrospective analysis: what was injected, noise vs signal,
effectiveness breakdown per section (always_inject/recent/relevant).

Co-authored-by: Kirill Turanskiy <thebtf@users.noreply.github.com>

* chore: update marketplace for v2.0.8

* chore: plugin-tool-consolidation spec + gap audit

- Gap audit report: plugin vs API analysis (68 MCP tools, 130 REST endpoints)
- New spec: plugin-tool-consolidation (6 FR, 4 NFR, 6 US)
- Plan: 5 phases, 34 tasks, analyze remediation applied
- Closed old mcp-tools-refactoring spec (FR7/FR8 → TECHNICAL_DEBT)

* chore: update task progress (Phases 1-5 complete)

* feat: plugin tool consolidation — all phases (FR-1 through FR-6) (#112)

* chore: plugin-tool-consolidation spec + gap audit

- Gap audit report: plugin vs API analysis (68 MCP tools, 130 REST endpoints)
- New spec: plugin-tool-consolidation (6 FR, 4 NFR, 6 US)
- Plan: 5 phases, 34 tasks, analyze remediation applied
- Closed old mcp-tools-refactoring spec (FR7/FR8 → TECHNICAL_DEBT)

* chore: remove 7 redundant MCP tool registrations (FR-1)

Remove from tools/list: get_context_timeline, get_timeline_by_query,
get_recent_context, find_by_file_context, get_observation_relationships,
get_graph_neighbors, doc_update.

All 7 tools remain callable via dispatch aliases in handleCallTool
for backward compatibility. Reduces tool count from 68 to 61.

Updates tests to match new tool set.

* fix: openclaw decisions endpoint + memory_forget suppress default (FR-2)

- engram_decisions now uses /api/decisions/search instead of searchContext
  + client-side type filter (B1 from audit)
- memory_forget defaults to suppress (reversible) instead of archive.
  Add permanent=true parameter for permanent archival (B2 from audit)
- Add suppressObservation() client method using bulk-status suppress action
- Add "suppress" action to server bulk-status handler
- Bump openclaw-engram to 2.0.9

* feat: expand openclaw tools — rate, suppress, outcome, file, timeline, vault (FR-3)

Add 9 new tools to openclaw-engram:
- engram_rate: rate observations as useful/not useful
- engram_suppress: reversible soft-hide from search
- engram_outcome: record session outcome for closed-loop learning
- engram_find_by_file: check what engram knows BEFORE modifying a file
- engram_timeline: fetch temporal observation context
- engram_changes: search preset for recent code changes
- engram_how_it_works: search preset for architecture/design
- engram_vault_store: securely store encrypted credentials
- engram_vault_get: retrieve and decrypt credentials

All tool descriptions include trigger conditions (NFR-3).
Add client methods: rateObservation, setSessionOutcome, getFileContext,
getTimeline, storeCredential, getCredential.
Add preset param to searchContext type.
Bump openclaw-engram to 2.0.10.
Total tools: 17 (was 8).

* feat: cc stop hook retrospective API + statusline learning metrics (FR-5, FR-6)

- stop.js: Replace /api/sessions/{id}/injected-observations + individual
  utility calls with single /api/sessions/{id}/injections (retrospective API).
  Fewer HTTP calls, enriched response with effectiveness data.
- statusline.js: Add learning effectiveness indicator with 60s client cache.
  Shows "eff:72%" (high tier percentage) or "eff:--" when no data.
  Fetches /api/learning/effectiveness-distribution in parallel with stats.

* feat: openclaw lifecycle hooks — outcome, utility, file context (FR-4)

- session_end: detect session outcome (success/partial/failure/abandoned)
  from conversation signals, record via /api/sessions/{id}/outcome.
  Handles gracefully when no DB session ID exists.
- before_tool_call: inject file-context observations before Write/Edit
  tools using /api/context/by-file. 200ms timeout, non-blocking.
- Register before_tool_call hook in index.ts.
- Bump openclaw-engram to 2.0.11.

* fix: address CodeRabbit review findings in spec/plan docs

- Fix edge case: memory_forget has only permanent param, not suppress
- Fix edge case: before_tool_call not after_tool_call
- Fix plan: version tracking says 2.0.x not 2.1.0

* fix: address CodeRabbit review — suppress cache + ID validation

- Suppress action now checks RowsAffected (not found = failed)
- Cache invalidation extended to suppress action (was archive-only)
- Unified ID validation in memory_forget: validate before branching

* fix: address CodeRabbit re-review findings (round 2)

CRIT: engram_outcome uses sessionDbId (not .id) from initSession response
MAJOR:
- stop.js: read injectionsResp.injections (wrapped response, not root array)
- before-tool-call: 500ms timeout (was 3s — too slow for pre-tool hook)
- session-end: use sessionDbId, soften heuristic (multi-word patterns),
  conservative default (partial, not abandoned)
- client.ts: timeline uses 15s timeout (matches searchContext),
  getFileContext accepts configurable timeoutMs

* fix: session outcome uses claude session ID string, not numeric DB ID

Sonnet lite review found: server UpdateSessionOutcome takes
claude_session_id string, not numeric DB ID. All outcome calls
(engram_outcome tool + session_end hook) now pass ctx.sessionId
directly — no initSession lookup needed.

- client.ts: setSessionOutcome accepts string, URL-encodes it
- engram-outcome.ts: removed initSession, pass claudeSessionId directly
- session-end.ts: simplified — no DB ID resolution needed

* fix: address all remaining CodeRabbit findings (round 3)

MAJOR:
- session_store.go: UpdateSessionOutcome only sets if outcome IS NULL —
  explicit engram_outcome tool takes priority over heuristic
- memory-forget.ts: strict integer regex + parseInt + isSafeInteger validation

MINOR:
- vault.ts: descriptive error messages for store/get failures
- vault.ts: comment about credential value in tool output
- before-tool-call.ts: doc says 500ms (matches code)
- TECHNICAL_DEBT.md: full spec path

---------

Co-authored-by: Kirill Turanskiy <thebtf@users.noreply.github.com>

* chore: resolve stash conflicts

* chore: update marketplace for v2.0.9

* chore: mcp-tool-api-consolidation spec (61→7 tools)

Full SpecKit pipeline: specify → clarify → plan → tasks → analyze.
Consolidates 61 MCP tools into 6 primary (recall/store/feedback/vault/docs/admin)
+ check_system_health. Backward-compatible dispatch aliases for all old names.
Target: >80% context window reduction (~6100 → ~900 tokens).
Also: 3 new dashboard bugs recorded in inbox.

* feat: MCP tool API consolidation — 61 tools → 7 primary (#113)

* chore: mcp-tool-api-consolidation spec (61→7 tools)

Full SpecKit pipeline: specify → clarify → plan → tasks → analyze.
Consolidates 61 MCP tools into 6 primary (recall/store/feedback/vault/docs/admin)
+ check_system_health. Backward-compatible dispatch aliases for all old names.
Target: >80% context window reduction (~6100 → ~900 tokens).
Also: 3 new dashboard bugs recorded in inbox.

* feat: create 6 primary tool routers (Phase 1 — FR-1 through FR-6)

New handler files that route consolidated tool actions to existing handlers:
- tools_recall.go: 12 actions (search, preset, by_file, by_concept, etc.)
- tools_store_consolidated.go: 4 actions (create, edit, merge, import)
- tools_feedback.go: 3 actions (rate, suppress, outcome)
- tools_vault_consolidated.go: 5 actions (store, get, list, delete, status)
- tools_docs_consolidated.go: 11 actions (create, read, list, history, etc.)
- tools_admin.go: 21 actions (bulk ops, tags, graph, maintenance, etc.)

Each is a thin routing layer — NO new business logic. All delegate
to existing handler functions via action parameter dispatch.

* feat: register 7 primary tools + alias dispatch (Phase 2 — FR-7, FR-8)

- Add primaryTools() returning 6 consolidated tools with flat schemas
- Default tools/list returns 7 tools (6 primary + check_system_health)
- cursor=all returns primary + 61 legacy alias tools
- callTool dispatch: primary names → consolidated handlers first,
  then fallthrough to legacy alias handlers
- All 61 original tool names continue to work via alias dispatch

* test: update MCP tests for 7 primary tools (Phase 3)

- TestHandleToolsList: expect 7 primary tools by default, legacy in cursor=all
- TestCallTool_ToolNameRecognition: verify primary + alias names in cursor=all
- Account for conditional tools (store_memory etc.) not present with nil stores

* docs: update engramInstructions for 7 consolidated tools (T018)

Replace 61 legacy tool references with 7 primary tools in the MCP
server instructions string. Shows action-based API: recall(action=...),
store(action=...), feedback(action=...), vault(action=...), docs(action=...),
admin(action=...), check_system_health(). Includes backward compat note.

---------

Co-authored-by: Kirill Turanskiy <thebtf@users.noreply.github.com>

* chore: mark all mcp-tool-api-consolidation tasks complete

* chore: update marketplace for v2.1.0

* chore: dashboard-bugfixes-v2 spec + TD cleanup

- New spec: 4 dashboard bugs (concept filter, type filter, 50/50 counts, summaries)
- Marked 3 TD items resolved (phantom sessions, vault lost key, MCP stubs)

* fix: dashboard concept filter, type filter, count display (#114) (#114)

* fix: dashboard concept filter, type filter, and count display

FR-1: Concept filter — add server-side concept param to handleGetObservations
and both paginated store methods. LIKE query on concepts JSON column.
Frontend passes concept from FilterTabs to fetchObservations.

FR-2: Type filter on HomeView — fetchObservations now passes type param.
Server already supported type filtering (obsType), was just not wired on home.

FR-3: Real counts — fetchObservations returns { observations, total }.
useTimeline tracks observationTotal from API response instead of array length.
"50 obs / 50 prompts" replaced with real counts.

Backend: handlers_data.go, observation_store.go (concept WHERE clause)
Frontend: api.ts (fetchObservations params), useTimeline.ts (server filter + totals)
Callers updated: handlers_import_export.go, detector.go (pass "" for concept)

* fix: use JSONB @> operator for concept filter instead of LIKE

---------

Co-authored-by: Kirill Turanskiy <thebtf@users.noreply.github.com>

* chore: update marketplace for v2.1.1

* fix: SDK extraction prompt bias + mark dashboard type filter resolved

- Reorder extraction prompt types: specific first (decision, feature, bugfix),
  general last (guidance). Add explicit note: "prefer specific over general"
- Mark "Dashboard Type Filter" TD as resolved v2.1.1 (PR #114)

* chore: mark 3 more TD items resolved (extraction types, type filter, namespace prefixes)

* chore: behavioral rules created (3 always_inject observations), mark TD resolved

* chore: mark 2 inbox bugs fixed (concept filter, counts) from PR #114

* chore: triage TD + inbox — mark DEFERRED/IMPLEMENTED items

TD: GPU contention and re-benchmark marked DEFERRED (external/infra)
Inbox: 5 ideas marked DEFERRED (future FR), 1 bug DEFERRED (external),
  user commands marked IMPLEMENTED (PR #115), 2 bugs marked FIXED (PR #114)
Spec: engram-user-commands pipeline artifacts

* feat: add 4 engram user commands (retro, stats, cleanup, export) (#115)

- /engram:retro — session retrospective (injection analysis, effectiveness, recommendations)
- /engram:stats — memory health dashboard (counts, types, effectiveness, learning curve, search analytics)
- /engram:cleanup — interactive observation curation (review, suppress, edit, merge low-quality items)
- /engram:export — export observations as markdown/JSON/JSONL with project/type filters

All commands use consolidated tool API (recall/store/feedback/admin).
Commands are markdown instruction files — no compilation needed.

Co-authored-by: Kirill Turanskiy <thebtf@users.noreply.github.com>

* chore: update marketplace for v2.1.2

* feat: pre-edit guardrails + session summarization on start (#116)

Wave 2: Pre-edit guardrails — pre-tool-use.js now separates warnings
(bugfix, guidance, anti-pattern, gotcha, security) from general context.
Warnings appear first with clear header so agent reviews them before editing.

Wave 3: Session summarization — session-start.js triggers summarization
of the most recent unsummarized session (fire-and-forget, 1 per start).
Workaround for CC bug #19225 (stop hook doesn't fire) so summaries
accumulate and appear on the Dashboard Summaries tab.

Co-authored-by: Kirill Turanskiy <thebtf@users.noreply.github.com>

* chore: mark pre-commit guardrails TD resolved v2.1.3

* chore: update marketplace for v2.1.3

* feat: config hot-reload without process restart (#117)

* feat: config hot-reload without process restart

Replace os.Exit(0) in reloadConfig with atomic config swap via
config.Reload(). Services calling config.Get() per-request pick up
new values automatically.

- config.go: add Reload() function — re-reads from disk, swaps global,
  returns list of changed fields
- service.go: reloadConfig() uses Reload() instead of os.Exit(0),
  broadcasts changed fields to dashboard via SSE

Port/token changes log a warning (still need manual restart).
All other config changes (model, embedding, context limits, reranking,
HyDE, maintenance) take effect immediately.

* fix: detect WorkerToken changes in hot-reload (requires restart)

---------

Co-authored-by: Kirill Turanskiy <thebtf@users.noreply.github.com>

* chore: mark config reload TD resolved v2.1.4

* chore: update marketplace for v2.1.4

* chore: mark GPU contention TD resolved (transient queue issue)

* feat: inbox features — session counter, consistency check, memory import (#118)

1. Dashboard: "Sessions Today" instead of "Active Sessions" (was always 0)
   — uses sessionsToday from stats API, not in-memory count

2. Consistency check endpoint: GET /api/maintenance/consistency
   — read-only orphan detection (vectors, relations, observations)
   — returns { orphan_vectors, observations_without_vectors, stale_relations, healthy }

3. memory_get store flag: memory_get(path="file.md", store=true)
   — reads .md file AND imports content into engram as observation
   — bridges local markdown files → engram persistent memory

4. Version bump: openclaw-engram 2.1.5

Co-authored-by: Kirill Turanskiy <thebtf@users.noreply.github.com>

* chore: mark 4 inbox items resolved/implemented (notes, consistency, indexes, bridge)

* chore: mark session tracking, CC bug, summaries as resolved/mitigated

* chore: audit specs (4 marked Implemented), close audit inbox item

* chore: mark OpenClaw architecture as external dependency, audit complete

* chore: update marketplace for v2.1.5

* feat: graph UX polish — local mode, search, visual styling (#119)

Phase 1: Local graph mode
- Route /graph/:observationId? with optional param
- Fetches /api/observations/{id}/graph?depth=N
- Anchor node: larger (25px), green border (#10b981)
- Depth selector (1/2/3) in toolbar
- "View in Graph" link on ObservationCard

Phase 2: Node search
- Search input in toolbar with match count
- Matching nodes: yellow border highlight
- Non-matching: dimmed (0.3 opacity)
- Enter key: focus camera on first match

Phase 3: Visual styling
- Node shadows, hover glow (white border)
- Edge colors mapped to relation types
- Curved edges (curvedCCW), dashed for low confidence
- Dot grid background
- Edge color legend sidebar

Co-authored-by: Kirill Turanskiy <thebtf@users.noreply.github.com>

* chore: mark graph UX polish implemented, all inbox items complete

* chore: update marketplace for v2.1.6

* chore: update benchmark to max_tokens:4096, 13 current models

* chore: mark re-benchmark TD resolved (script updated, ready to run)

* fix: benchmark parallel=1 default (avoid multi-model GPU overload)

* chore: bump openclaw-engram to 2.1.6 (match server version per Constitution #15)

* chore: remove legacy alias tools from tools/list entirely

Legacy tool names (search, store_memory, find_by_file, etc.) no longer
appear in tools/list at all — not even with cursor=all. Only 7 primary
tools shown. Dispatch aliases still work in callTool for backward compat
(zero runtime cost, zero context cost).

* fix: summaries — build content from session observations when no transcript (#120)

ProcessSummary now fetches session observations from DB when called
without lastAssistantMsg (e.g., from session-start summarizer).
Previously: empty msg → hasMeaningfulContent=false → skip always.
Now: empty msg → query observations by sdk_session_id → build summary
input from observation titles+narratives → generate LLM summary.

Co-authored-by: Kirill Turanskiy <thebtf@users.noreply.github.com>

* chore: dashboard-quality-v3 spec + 3 inbox bugs

* feat: dashboard quality v3 — search misses, sessions, pattern insights (#121)

Phase 1: Fix search misses display — unwrap miss_stats envelope, map miss_count→frequency
Phase 2: Sessions backend — add min_prompts, from, to filters to ListSDKSessions
Phase 3: Sessions frontend — pass min_prompts=1 (hide empty), wire date filters,
  clickable sessions with detail view (SessionDetailView.vue: metadata, injections, outcome)
Phase 4: Pattern insight background — maintenance Task 18 generates LLM insights
  for patterns with generic descriptions (5 per cycle), caches in pattern.description

Co-authored-by: Kirill Turanskiy <thebtf@users.noreply.github.com>

* chore: update marketplace for v2.1.7

* chore: save session state (v2.1.7, 10 PRs, all TD resolved)

* feat: dashboard UX polish — tooltips, cursor-pointer, hover transitions, color coding (#122)

- Tooltips: all action buttons have descriptive title attributes explaining
  what they do and whether reversible (Resolve, Suppress, Archive, Rate, Graph)
- Cursor-pointer: 32 additions across 3 files — all interactive elements
- Hover transitions: 27 duration-200 additions for consistent 200ms timing
- Color coding: destructive=red, resolve=green, reopen=blue, info=gray
- Existing ConfirmDialog already handles destructive action confirmation

3 files: ObservationsView.vue, ObservationCard.vue, ObservationDetailView.vue

Co-authored-by: Kirill Turanskiy <thebtf@users.noreply.github.com>

* chore: update marketplace for v2.1.8

* docs: summaries + concepts pipeline audit report with root causes and fixes

* chore: summaries-concepts-fix spec + updated inbox

* fix: summaries + concepts pipeline — 3 root causes from audit (#123)

FR-2: Add valid concept list to extraction systemPrompt (processor.go).
  LLM now knows which concepts to use instead of inventing random ones.
  Fixed example: user-preference → workflow.

FR-1: Add userPrompt fallback in ProcessSummary (processor.go).
  When both lastAssistantMsg and observations are empty, use the
  session's initial user prompt as summary input.

FR-3: Migration 055 — keyword-based concept backfill for 1047 existing
  observations. Assigns architecture, security, debugging, api, database,
  etc. based on title/narrative keyword matching. No LLM needed.

Audit: .agent/reports/summaries-concepts-audit-2026-03-28.md

Co-authored-by: Kirill Turanskiy <thebtf@users.noreply.github.com>

* chore: mark summaries-concepts tasks complete

* chore: mark dashboard-quality-v3 tasks complete (PR #121)

* chore: update marketplace for v2.1.9

* chore: save session state (v2.1.9, 11 PRs, session compacted)

* docs: investigate report — 13 findings (4 P1, 7 P2, 2 P3) across 12 areas

* docs: summaries pipeline investigation — root cause is trigger architecture, not code

* chore: server-summarizer-and-fixes spec + tasks

* fix: summaries + concepts pipeline — 3 root causes from audit (#123) (#124)

FR-1: Server-side periodic summarizer (maintenance Task 19)
  Scans sessions with prompts > 0 and no summary, older than 30min.
  Builds content from observations, calls LLM, stores in session_summaries.
  Cap: 3 per cycle. No client hook dependency.

FR-2: Pre-edit guardrails — remove guidance from warnings
  Only bugfix + concept-based (anti-pattern, gotcha, security) are warnings.
  Global behavioral rules no longer show as "WARNINGS" before every file edit.

FR-3: Remove client-side summarizer from session-start.js
  Replaced by server-side Task 19. Client workaround had bugs (sess.summary
  field doesn't exist, would re-summarize repeatedly).

FR-4: Circuit breaker recovery logging
  Logs "entering half-open state" and "recovered — LLM calls re-enabled"
  for diagnosing LLM availability from server logs.

Co-authored-by: Kirill Turanskiy <thebtf@users.noreply.github.com>

* chore: mark server-summarizer tasks complete (PR #124)

* chore: update marketplace for v2.2.0

* chore: mark graph-ux-polish tasks complete (PR #119)

* chore: mark stale tasks complete (dashboard-bugfixes-v2 PR#114, user-commands PR#115)

* chore: transfer investigate P1/P2 findings to inbox as actionable tasks

* chore: audit-bugfixes spec + tasks (P1+P2 from investigate)

* fix: audit bugfixes — P1+P2 findings from investigate report (#125)

T001: Summary dedup verified — NOT EXISTS check already correct
T002: OpenClaw before_tool_call — added BeforeToolCallResult to HookResult type
T003: Store content validation — error message clarified
T004: Summary userPrompt threshold lowered (50→10 chars)
T005: Migration 064 — backfill 5 missing concepts (why-it-exists, what-changed,
  anti-pattern, gotcha, trade-off) with keyword matching
T006: go build + tsc --noEmit verified clean

Co-authored-by: Kirill Turanskiy <thebtf@users.noreply.github.com>

* chore: mark audit-bugfixes complete, update inbox (v2.2.1)

* chore: update marketplace for v2.2.1

* docs: 3 ADRs from Cipher competitive analysis + investigation report

ADR-003: Reasoning Traces (System 2 Memory) — store HOW agent reasons, not just WHAT it decided
ADR-004: Dedicated Embedding Resilience — separate CB, health check, 4 states, auto-recovery
ADR-005: LLM-Driven Memory Extraction — extract_and_operate for autonomous observation creation

Investigation: 10 findings across 10 areas comparing Cipher vs Engram architecture

* chore: reasoning-traces spec (System 2 Memory from ADR-003)

* chore: reasoning-traces full SpecKit pipeline (clarify+plan+tasks+analyze)

* feat: reasoning traces (System 2 Memory) — Phases 1-3 (#126)

Phase 1: Data Model
- Migration 065: reasoning_traces table (steps JSONB, quality_score, task_context)
- GORM model ReasoningTrace with BeforeCreate hook
- ReasoningTraceStore with Create/GetBySession/SearchByProject

Phase 2: Extraction
- reasoning_detector.go: DetectReasoning() — 3+ pattern matches in 200+ char text
- Extraction + quality evaluation LLM prompts
- Async extraction in ProcessObservation (non-blocking goroutine)
- Quality threshold ≥ 0.5 to store

Phase 3: MCP Integration
- recall(action="reasoning") — searches traces by project, formats with step types
- "reasoning" added to recall tool action enum
- Wired into worker service (processor + MCP server)

ADR-003 implemented. Inspired by Cipher's System 2 dual memory architecture.

Co-authored-by: Kirill Turanskiy <thebtf@users.noreply.github.com>

* chore: mark reasoning-traces tasks complete (PR #126, v2.3.0)

* chore: embedding-resilience spec pipeline (ADR-004)

* chore: close 2 remaining P2 inbox items (metric documented, visual API-verified)

* chore: update marketplace for v2.3.0

* feat: dedicated embedding resilience layer (ADR-004) (#127)

- ResilientEmbedder wraps EmbeddingModel with 4-state circuit breaker:
  HEALTHY → DEGRADED (1+ failures) → DISABLED (5+ failures) → RECOVERING
- Health check goroutine probes every 30s when not HEALTHY
- Automatic recovery: probe succeeds → RECOVERING → next real request → HEALTHY
- Independent from LLM circuit breaker (embedding failures ≠ LLM failures)
- Thread-safe via sync/atomic
- Wired into worker service (init + reinit + shutdown)
- selfcheck handler reports embedding status with failure counts

Co-authored-by: Kirill Turanskiy <thebtf@users.noreply.github.com>

* chore: mark embedding-resilience tasks complete (PR #127)

* chore: extract-and-operate spec pipeline (ADR-005)

* chore: update marketplace for v2.3.1

* feat: store(action="extract") — LLM-driven memory extraction (ADR-005) (#128)

- New action on store tool: accepts raw content, uses LLM to extract observations
- Extraction prompt generates structured observations (type, title, narrative, concepts)
- Privacy: content redacted via RedactSecrets before LLM call
- Validation: min 50 chars, truncate at 32k, type validation with fallback
- Returns summary: {extracted, stored, duplicates, titles}
- Added "extract" to store tool action enum

Co-authored-by: Kirill Turanskiy <thebtf@users.noreply.github.com>

* chore: mark extract-and-operate tasks complete (PR #128, v2.4.0)

* chore: save session state (v2.4.0, 16 PRs, all complete)

* chore: update marketplace for v2.4.0

* docs: complete documentation rewrite for v2.4.0

- README: 48 legacy tools → 7 consolidated primary tools, marketing intro,
  architecture diagram with dashboard/LLM/embedding, What's New, Use Cases,
  Upgrading, Troubleshooting, MCP Tools reference with all actions
- CHANGELOG: 17 new entries (v2.0.7 through v2.4.0) with comparison links
- README.ru.md: full Russian translation synced to v2.4.0
- README.zh.md: full Chinese translation synced to v2.4.0

* chore: remove agent working state and test artifacts from tracking

- .agent/ was in .gitignore but files were committed before the rule
- .playwright-mcp/ screenshots are test artifacts, not source
- TECHNICAL_DEBT.md is agent-local state
- Updated .gitignore to cover all three

* perf: narrow PostToolUse hook matcher from * to Write|Edit|Bash|Agent|mcp__aimux

Eliminates ~50+ unnecessary node process spawns per research session.
Previously matcher * fired post-tool-use.js on every tool call including
Read, Grep, Glob, ToolSearch — which then hit skipTools early exit and
returned empty. Now Claude Code filters at matcher level, avoiding
process spawn entirely for read-only tools.

Removed skipTools map from post-tool-use.js (redundant with matcher).
Bumped plugin version 2.0.7 → 2.0.8.

* feat: strengthen MCP server instructions to assert memory exclusivity

- "Your ONLY Persistent Memory" — exclusivity claim over competing tools
- AFTER workflow mandatory: store decisions/discoveries after every task
- "Steps 4-6 are NOT optional" — directive-level store instruction
- "What to Store" section with concrete examples
- Workflow patterns now end with store, not just recall
- Counters Nia context manager competing for agent attention

* chore: update marketplace for v2.4.1

* chore: update marketplace for v2.4.1

* perf: stop re-injecting behavioral rules on every user prompt

Behavioral rules (user-preference concept + always-inject) are already
injected once by session-start.js via /api/context/inject. Re-injecting
them on every UserPromptSubmit via /api/context/search wasted ~4K tokens
per prompt (~17KB duplicated behavioral rules block).

Changes:
- Removed behavioral rules assembly from user-prompt.js
- Removed footer reminder (redundant with MCP server instructions)
- Only technical observations injected in <relevant-memory>
- Bumped plugin 2.4.1 → 2.4.2

* feat: minimum viable learning loop — close feedback loop + stop scope leak

Phase 1 (narrow scope):
- Remove includeGlobal=true from 3 vector search call sites in context
  handlers (search, file-context, inject). Observations from other projects
  no longer pollute context injection.
- Add project filter to GetAlwaysInjectObservations — only returns
  observations from current project or global scope (was: all projects).
- Client-side min similarity filter (>0.10) in user-prompt.js — observations
  with 0.00 relevance no longer injected.

Phase 2a (close the loop):
- Add Bayesian effectiveness multiplier to ApplyCompositeScoring.
  Formula: (successes + 1) / (injections + 2) with neutra…
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant