feat: Phases 2-5 + 8a/8b/8c/8d — dashboard, self-learning, consistency, documents#59
Conversation
…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
ОписаниеОбновления включают расширение системы хранения с новыми хранилищами для управления версионированными документами, параметрами проектов и отношениями наблюдений; добавление адаптивного порога релевантности на уровне проекта, повышение оценки на основе сеансов, логики минимального порога инъекции; введение классификации причинно-следственных связей на основе LLM; расширение миграций БД для трёх новых таблиц; обновления обработчиков работника для контекстного поиска и утилит наблюдений; улучшения интерфейса пользователя для наблюдений, токенов и вилки. Изменения
Диаграммы последовательности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
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
Оценка сложности рецензирования🎯 4 (Сложно) | ⏱️ ~60 минут Возможно связанные PR
Рекомендуемые метки
Стихотворение
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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 Comment |
Summary of ChangesHello, 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
Using Gemini Code AssistThe 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
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 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
|
|
@coderabbitai review |
|
@gemini-code-assist review |
✅ Actions performedReview triggered.
|
|
@codex review |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 | 🟠 MajorClassifier выдаёт 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-hopneighborhood.
[: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" fiAlso 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-операций основной список уже обновлён, а сайдбар остаётся со старыми/глобальными тегами. Бэкенд уже поддерживаетprojectquery в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
📒 Files selected for processing (23)
docs/DEPLOYMENT.mdinternal/config/config.gointernal/db/gorm/helpers.gointernal/db/gorm/migrations.gointernal/db/gorm/observation_store.gointernal/db/gorm/project_settings_store.gointernal/db/gorm/versioned_document_store.gointernal/graph/falkordb/client.gointernal/graph/noop.gointernal/graph/store.gointernal/maintenance/service.gointernal/relation/causal_classifier.gointernal/relation/detector.gointernal/search/manager.gointernal/worker/handlers_context.gointernal/worker/handlers_scoring.gointernal/worker/service.goplugin/engram/hooks/stop.jsscripts/install.shui/src/components/layout/AppSidebar.vueui/src/views/ObservationsView.vueui/src/views/SystemView.vueui/src/views/TokensView.vue
🤖 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
* 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…
Summary
Massive roadmap implementation: 40+ tasks across 6 phases.
Phase 2: Dashboard Frontend
Phase 3: Self-Learning
Phase 4: MCP Tools (already merged in PR #58)
Phase 5: Deployment
Phase 8a: Consistency Engine
Phase 8b: Knowledge Graph
Phase 8c: Causal Linking
Phase 8d: Document Storage
Test plan
go build ./...passesSummary by CodeRabbit
Release Notes
Новые возможности
Улучшения
Документация