Skip to content

feat: Engram v2 — Memory Evolution#3

Merged
thebtf merged 12 commits into
mainfrom
feat/memory-evolution
Mar 12, 2026
Merged

feat: Engram v2 — Memory Evolution#3
thebtf merged 12 commits into
mainfrom
feat/memory-evolution

Conversation

@thebtf
Copy link
Copy Markdown
Owner

@thebtf thebtf commented Mar 11, 2026

Summary

Major evolution of engram's memory system across 4 phases + dead code cleanup:

  • Phase 1: Stable Project Identity — Replace path-based project ID (SHA-256(absolutePath)[0:6]) with git-remote-based ID (SHA-256(remote_url + "/" + relative_path)[0:8]). Same repo on Windows and Linux now produces identical project ID. Dual-ID migration re-associates existing observations.
  • Phase 2: Explicit Memory Management — New store_memory and recall_memory MCP tools. Agents can now create memories explicitly (not just passive capture via hooks). Content governance: hard limit 10K chars, soft limit 1K, dedup threshold 0.92, hierarchical tag expansion.
  • Phase 3: Global Cross-Project Memory — Fix BuildWhereFilter vector search bug that excluded scope='global' observations. New WhereFilter/WhereClause types enable OR group SQL generation. Global observations now properly surface cross-project.
  • Phase 4: Tool Discoverability — Updated engramInstructions and skill file to document store_memory/recall_memory as Tier 1 tools.
  • ONNX Cleanup — Removed ~49 MB of dead ONNX embedding + reranking code (33 files, -63K lines). Production always uses API providers.

53 files changed, +696 / -63,305 lines.

Test plan

  • Clone engram to two different paths — verify same git-remote-based project ID
  • Non-git directories fall back to path-based hash
  • Call store_memory via MCP — verify observation created with correct type/tags/scope
  • Call recall_memory — verify hybrid search returns relevant results
  • Content >10K chars rejected; >1K chars truncated
  • Dedup: storing near-identical memory returns duplicate: true
  • Create observation with scope=global in project A, search from project B — must appear
  • go build ./... succeeds (except pre-existing go-tree-sitter CGo issue on Windows)
  • go vet clean on all engram packages
  • Docker build succeeds with existing EMBEDDING_PROVIDER=openai config

Summary by CodeRabbit

  • Новые функции

    • Инструменты явного сохранения памяти (store_memory) и восстановления (recall_memory).
    • Полнофункциональное управление учётными данными: сохранение, получение, список, удаление и статус хранилища.
    • Стабильная генерация идентификаторов проекта через git (с откатом к старому ID).
  • Улучшения

    • Поддержка глобальной области памяти (метка [GLOBAL]) и обновлённая документация/гайд.
    • Новые параметры лимитов памяти и дедупликации; провайдер встраиваний по умолчанию переключён на API.
    • Поиск/вставка контекста фильтруют учётные данные.
  • Безопасность

    • Шифрование секретов с управлением ключами, отпечатками и проверкой целостности.
  • База данных

    • Добавлена поддержка проектов в миграциях.

thebtf added 5 commits March 11, 2026 19:39
Replace path-based project ID (SHA-256(absolutePath)[0:6]) with
git-remote-based composite ID (SHA-256(remote_url/relative_path)[0:8]).

- hooks/lib.js: getGitRemoteID() + LegacyProjectID() with fallback
- sessions/parser.go: GitRemoteProjectID() aligns indexer to same scheme
- migrations.go: 030_projects_table with GIN index on legacy_ids
- project_store.go: UpsertProject with dual-ID migration, ResolveProjectID
- models.go: Project GORM model

Zero-downtime: hooks send both project + legacy_project, server
re-associates observations in background via idempotent UPDATE.
Non-git directories keep path-based fallback.
…ory)

Add store_memory and recall_memory MCP tools for agents to explicitly
create and retrieve memories, beyond passive hook capture.

- tools_memory.go: handleStoreMemory with content governance (hard/soft
  limits), keyword-based type classification, hierarchical tag expansion,
  cosine dedup (0.92 threshold), and scope auto-detection
- tools_memory.go: handleRecallMemory wrapping hybrid search with
  text/items/detailed output formats
- observation.go: add SourceManual source type constant
- config.go: add StoreMemory* configuration fields
- server.go: register tool definitions + dispatch cases
Fix BuildWhereFilter vector search bug to include global-scoped
observations via OR clause. Wire SearchParams.IncludeGlobal into
hybrid search. Surface [GLOBAL] tag in context injection hooks.

- types.go: WhereFilter/WhereClause types replacing map[string]any,
  BuildWhereFilter now accepts includeGlobal bool for OR project/scope
- pgvector/client.go: Query accepts WhereFilter, builds OR SQL groups
- interface.go: updated Client.Query signature
- manager.go: pass IncludeGlobal to BuildWhereFilter in hybridSearch
- Updated all 13 BuildWhereFilter callers across 9 files
- user-prompt.js + session-start.js: [GLOBAL] prefix on global obs
Update engramInstructions with Memory Management tier-1 section,
store_memory/recall_memory in Quick Start, and revised Common Mistakes.
Update SKILL.md with top 12 tools (was 10), explicit memory workflow,
and store_memory/recall_memory usage examples.
ONNX inference was never used in production — docker-compose defaults
to EMBEDDING_PROVIDER=openai and RERANKING_PROVIDER=api. Remove ~49 MB
of dead code: ONNX models, runtime libraries, BGE service, cross-encoder
reranker, platform-specific lib loaders, and related tests.

- Delete internal/embedding/service_onnx.go, lib_*.go, assets/
- Delete internal/reranking/service.go, factory*.go, assets/
- Simplify embedding.Service to always use OpenAI provider
- Remove builtin embedding dimension override in store.go
- Update config defaults: EmbeddingProvider "builtin" -> "openai"
- Clean ONNX references from worker/service.go and config comments
- Remove onnxruntime_go and sugarme/tokenizer dependencies
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 11, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Удалены локальные ONNX‑модули embedding и reranking; добавлены инструменты памяти и управления секретами (store_memory, recall_memory, credential tools) с Vault‑шифрованием; введён структурированный WhereFilter для векторных запросов; добавлена таблица projects и git‑remote‑based project ID; обновлены вызовы векторов и миграции.

Changes

Cohort / File(s) Summary
Удаление локальных ONNX-ресурсов
internal/embedding/..., internal/reranking/...
Удалены файлы, встроенные бинарные ассеты, фабрики, сервисы и тесты, реализующие локальные ONNX‑модели для embedding и reranking.
Инициализация embedding в cmd
cmd/mcp/main.go
Заменён вызов embedding.NewService() на embedding.NewServiceFromConfig(); при ошибке логируется и embedding/vector отключаются.
MCP: memory tools
internal/mcp/tools_memory.go, internal/mcp/server.go
Добавлены store_memory и recall_memory: валидация, лимиты, дедупликация через вектор, сохранение в observation store, форматирование результатов и интеграция в список инструментов.
MCP: credential tools
internal/mcp/tools_credential.go, internal/mcp/server.go
Добавлены хэндлеры: store_credential, get_credential, list_credentials, delete_credential, vault_status с шифрованием через Vault и соответствующими ответами.
Крипто‑слой (Vault)
internal/crypto/vault.go, internal/crypto/vault_test.go
Новый Vault: загрузка/генерация 32B ключа (env/file/auto), fingerprint, AES‑256‑GCM Encrypt/Decrypt; покрытие юнит‑тестами.
Структурная фильтрация векторов
internal/vector/types.go, internal/vector/interface.go, internal/vector/pgvector/client.go
Введены WhereFilter/WhereClause с поддержкой OR‑групп; BuildWhereFilter принимает includeGlobal и возвращает WhereFilter; интерфейс и pgvector клиент адаптированы.
Адаптация вызовов фильтрации
internal/worker/*, internal/search/manager.go, internal/telemetry/similarity.go, internal/mcp/server.go, internal/instincts/dedup.go
Обновлены вызовы BuildWhereFilter(..., includeGlobal) и вызовы vector.Client.Query на новый тип фильтра; search теперь фильтрует результаты типа credential.
DB: проекты и миграции
internal/db/gorm/migrations.go, internal/db/gorm/models.go, internal/db/gorm/project_store.go, internal/db/gorm/observation_store.go
Добавлена миграция и модель projects; Upsert/Resolve/reassociate; observation расширены полями EncryptedSecret и EncryptionKeyFingerprint; добавлены методы работы с credentials.
Sessions / cross-repo project ID
internal/sessions/parser.go, plugin/engram/hooks/lib.js
Добавлен GitRemoteProjectID: стабильный ID из git remote + относительного пути; fallback — legacy hash; JS‑плагин использует git‑remote ID и экспортирует LegacyProjectID.
Конфигурация памяти и зависимости
internal/config/config.go, go.mod, internal/db/gorm/store.go
Добавлены параметры памяти (StoreMemoryHardLimit, StoreMemorySoftLimit, StoreMemoryDedupThreshold, StoreMemorySummarize) и env для шифрования; Default EmbeddingProvider → openai; изменения в go.mod.
UI/плагин и документация
plugin/engram/hooks/session-start.js, plugin/engram/hooks/user-prompt.js, plugin/engram/skills/memory/SKILL.md
Добавлен маркер [GLOBAL] для глобальных наблюдений, user‑prompt фильтрует credential, обновлена документация и примеры store_memory/recall_memory.
Мелкие изменения и рефакторинг
internal/pipeline/deterministic.go, internal/worker/service.go, internal/instincts/dedup.go, pkg/models/observation.go
Удалены ONNX‑константы/фабрики reranker, упрощён createReranker, добавлены ObsTypeCredential и SourceManual, комментарии и вызовы BuildWhereFilter обновлены.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant MCP as "MCP Server"
    participant Vault as "Vault"
    participant Vector as "Vector Store"
    participant Obs as "Observation Store"
    participant Search as "Search Manager"

    Client->>MCP: store_memory(args)
    MCP->>Vault: ensure vault (init / fingerprint)
    MCP->>Vector: dedup query (WhereFilter)
    Vector-->>MCP: dedup result
    alt duplicate found
        MCP->>Client: return existing observation summary
    else not duplicate
        MCP->>Obs: persist ParsedObservation (may include encrypted secret)
        Obs-->>MCP: id
        MCP->>Search: index item
        MCP->>Client: return stored id/title/type/scope
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 Я закопал строку в engram — словно семя,

Vault хранит ключ в тиши, шифр — как зеркала имя.
Если встречу дубль — скажу: «Уже есть у нас»,
А новое растёт пушистой памяти стезёй.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'feat: Engram v2 — Memory Evolution' accurately and clearly represents the main objective of this substantial changeset, which centers on evolving engram's memory system with new tools, project ID computation, and credential management while removing ONNX code.
Docstring Coverage ✅ Passed Docstring coverage is 83.33% which is sufficient. The required threshold is 80.00%.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/memory-evolution

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

@gemini-code-assist
Copy link
Copy Markdown

Summary of Changes

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

This pull request significantly enhances Engram's memory system through a multi-phase evolution. It introduces a more robust and cross-platform project identification mechanism, empowers agents with explicit memory management tools, and improves the discoverability of global knowledge. Concurrently, it cleans up substantial legacy ONNX-related code, simplifying the system and focusing on API-based external providers for embedding and reranking services.

Highlights

  • Stable Project Identity: Replaced path-based project IDs with git-remote-based IDs, ensuring consistent project identification across different environments (e.g., Windows and Linux). A dual-ID migration re-associates existing observations to the new canonical IDs.
  • Explicit Memory Management: Introduced new store_memory and recall_memory tools, allowing agents to explicitly create and retrieve memories. This includes content governance with hard/soft limits, deduplication, and hierarchical tag expansion.
  • Global Cross-Project Memory: Fixed a vector search bug that prevented scope='global' observations from being surfaced. New WhereFilter/WhereClause types enable OR group SQL generation, ensuring global observations are properly discoverable across projects.
  • Tool Discoverability: Updated engramInstructions and skill documentation to highlight store_memory and recall_memory as Tier 1 tools, improving their visibility and encouraging proactive use.
  • ONNX Cleanup: Removed approximately 49 MB of dead ONNX embedding and reranking code, including 33 files and over 63,000 lines, streamlining the codebase as production environments exclusively use API providers.
Changelog
  • cmd/mcp/main.go
    • Updated the embedding service initialization to use NewServiceFromConfig().
  • go.mod
    • Removed direct dependencies on github.com/sugarme/tokenizer and github.com/yalue/onnxruntime_go.
    • Removed the replace directive for github.com/sugarme/tokenizer.
    • Moved github.com/jackc/pgx/v5 from direct to indirect dependency.
    • Removed several indirect dependencies related to ONNX runtime and tokenizer libraries.
  • go.sum
    • Removed entries corresponding to the removed github.com/sugarme/tokenizer and github.com/yalue/onnxruntime_go dependencies.
    • Removed various indirect dependency entries related to ONNX and tokenizer.
  • internal/config/config.go
    • Removed the 'onnx' option from the RerankingProvider comment.
    • Added new configuration fields: StoreMemoryHardLimit, StoreMemorySoftLimit, StoreMemoryDedupThreshold, and StoreMemorySummarize.
    • Updated the default EmbeddingProvider from "builtin" to "openai".
  • internal/db/gorm/migrations.go
    • Added a new migration (030_projects_table) to create a projects table.
    • The projects table includes fields for id, git_remote, relative_path, legacy_ids, display_name, and created_at.
    • Created unique and GIN indexes on the projects table for efficient lookups and conflict resolution.
  • internal/db/gorm/models.go
    • Added the Project struct definition, representing a repository's stable identity for cross-platform project ID resolution.
  • internal/db/gorm/project_store.go
    • Added a new file containing functions for managing project identities.
    • Implemented UpsertProject to register or update project records, handling legacy ID migration.
    • Implemented reassociateObservations to migrate observations from old project IDs to canonical ones in the background.
    • Implemented ResolveProjectID to check for and return canonical project IDs from legacy aliases.
  • internal/db/gorm/store.go
    • Removed logic that adjusted embedding dimensions based on a 'builtin' embedding provider, simplifying migration setup.
  • internal/embedding/assets.go
    • Removed the file, eliminating embedded ONNX model and tokenizer assets.
  • internal/embedding/assets/.model_version
    • Removed the file, eliminating the embedded model version.
  • internal/embedding/assets/lib/darwin-arm64/.version
    • Removed the file, eliminating the embedded ONNX runtime version for Darwin ARM64.
  • internal/embedding/assets/lib/linux-arm64/.version
    • Removed the file, eliminating the embedded ONNX runtime version for Linux ARM64.
  • internal/embedding/assets/lib/windows-amd64/.version
    • Removed the file, eliminating the embedded ONNX runtime version for Windows AMD64.
  • internal/embedding/assets/model.onnx
    • Removed the file, eliminating the embedded ONNX model.
  • internal/embedding/lib_darwin.go
    • Removed the file, eliminating Darwin-specific ONNX library handling.
  • internal/embedding/lib_darwin_arm64.go
    • Removed the file, eliminating Darwin ARM64-specific ONNX library embedding.
  • internal/embedding/lib_linux_amd64.go
    • Removed the file, eliminating Linux AMD64-specific ONNX library embedding.
  • internal/embedding/lib_linux_arm64.go
    • Removed the file, eliminating Linux ARM64-specific ONNX library embedding.
  • internal/embedding/lib_windows.go
    • Removed the file, eliminating Windows-specific ONNX library handling.
  • internal/embedding/lib_windows_amd64.go
    • Removed the file, eliminating Windows AMD64-specific ONNX library embedding.
  • internal/embedding/model.go
    • Removed PoolingStrategy, ONNXConfig, and ONNXConfigurer types, along with related comments, as ONNX models are no longer supported.
  • internal/embedding/service.go
    • Simplified NewServiceFromConfig to always use the OpenAI embedding provider.
    • Removed NewService, NewServiceWithModel functions, and constants like EmbeddingDim, MaxSequenceLength.
    • Removed meanPooling and clsPooling utility functions, which were specific to ONNX model processing.
  • internal/embedding/service_test.go
    • Removed the file, eliminating tests for the ONNX-based embedding service.
  • internal/instincts/dedup.go
    • Updated the vector.BuildWhereFilter call to include the includeGlobal parameter, defaulting to false.
  • internal/mcp/server.go
    • Updated engramInstructions to include new store_memory and recall_memory tools.
    • Added a new 'Memory Management' tool category to the instructions.
    • Revised the 'Common Mistakes' section to reflect the new store_memory tool.
    • Added new store_memory and recall_memory tool definitions to the handleToolsList function.
    • Implemented handlers for store_memory and recall_memory tool calls.
    • Updated existing vector.BuildWhereFilter calls to pass the new includeGlobal parameter.
  • internal/mcp/tools_memory.go
    • Added a new file implementing handleStoreMemory and handleRecallMemory functions for explicit memory management.
    • handleStoreMemory manages content truncation, observation classification, hierarchical tag expansion, and deduplication.
    • handleRecallMemory performs semantic searches for memories with flexible output formats.
  • internal/pipeline/deterministic.go
    • Removed a comment referencing BGE's 512-token MaxSequenceLength, as the BGE model is no longer used.
  • internal/reranking/assets.go
    • Removed the file, eliminating embedded reranking assets.
  • internal/reranking/assets/model.onnx
    • Removed the file, eliminating the embedded reranking ONNX model.
  • internal/reranking/assets/tokenizer_config.json
    • Removed the file, eliminating the embedded reranking tokenizer configuration.
  • internal/reranking/factory.go
    • Removed the file, eliminating the ONNX reranker factory.
  • internal/reranking/factory_onnx.go
    • Removed the file, eliminating the ONNX reranker factory implementation.
  • internal/reranking/interface.go
    • Updated comments to reflect that only API-based rerankers are now supported.
  • internal/reranking/service.go
    • Removed the file, eliminating the ONNX-based reranking service implementation.
  • internal/reranking/service_test.go
    • Removed the file, eliminating tests for the ONNX-based reranking service.
  • internal/search/manager.go
    • Updated the vector.BuildWhereFilter call to correctly pass the includeGlobal parameter based on search parameters.
  • internal/sessions/parser.go
    • Added os/exec import for running external commands.
    • Modified ProjectID to prioritize a stable git-remote-based ID, falling back to path-based if not in a git repository.
    • Added GitRemoteProjectID to compute project IDs from git remote URLs and relative paths.
    • Added runGitCommand utility function for executing git commands.
  • internal/telemetry/similarity.go
    • Updated the vector.BuildWhereFilter call to include the includeGlobal parameter, defaulting to false.
  • internal/vector/interface.go
    • Changed the where parameter type in the Query method from map[string]any to the new WhereFilter struct.
  • internal/vector/pgvector/client.go
    • Updated the Query method to accept the new vector.WhereFilter type.
    • Modified SQL generation logic to correctly handle WhereFilter's Clauses and OrGroup conditions.
  • internal/vector/types.go
    • Defined new WhereFilter and WhereClause structs to support more complex query filtering, including OR conditions.
    • Updated BuildWhereFilter to return a WhereFilter struct and to correctly handle includeGlobal logic for project and global scopes.
  • internal/worker/handlers_backfill.go
    • Updated vector.BuildWhereFilter calls to use the new WhereFilter struct, passing an empty filter for semantic deduplication.
  • internal/worker/handlers_context.go
    • Updated vector.BuildWhereFilter calls to use the new WhereFilter struct, defaulting includeGlobal to false.
  • internal/worker/handlers_data.go
    • Updated vector.BuildWhereFilter calls to use the new WhereFilter struct, defaulting includeGlobal to false.
  • internal/worker/service.go
    • Removed the 'onnx' case from createReranker, as ONNX reranking is no longer supported.
    • Updated comments related to reranker creation to reflect the removal of ONNX.
  • pkg/models/observation.go
    • Added SourceManual to the SourceType enum, indicating observations explicitly stored by agents.
  • plugin/engram/hooks/lib.js
    • Added getGitRemoteID function to compute project IDs based on git remote URL and relative path.
    • Added LegacyProjectID function to retrieve the old path-based project ID for migration purposes.
    • Updated ProjectIDWithName to prioritize getGitRemoteID for stable project IDs, falling back to LegacyProjectID.
    • Modified RunHook to pass both Project (canonical) and LegacyProject IDs to the backend.
  • plugin/engram/hooks/session-start.js
    • Modified observation display logic to include a [GLOBAL] tag for global-scoped observations in the context builder.
  • plugin/engram/hooks/user-prompt.js
    • Modified observation display logic to include a [GLOBAL] tag for global-scoped observations in the context builder.
  • plugin/engram/skills/memory/SKILL.md
    • Updated the core principle description to emphasize explicit storage and curation.
    • Added store_memory and recall_memory to the 'When to Use Engram Tools' section.
    • Increased the 'Top Tools' count from 10 to 12 and added store_memory and recall_memory as the top two tools with usage examples.
    • Updated the 'Starting Work' section to include recall_memory and store_memory guidance.
    • Revised the 'Common Mistakes' section to advise using store_memory for important insights instead of relying solely on hooks.
Using Gemini Code Assist

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

Invoking Gemini

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

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

Customization

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

Limitations & Feedback

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

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

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

Copy link
Copy Markdown

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

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a significant evolution of the memory system, encompassing stable project identity, explicit memory management, global cross-project memory, and enhanced tool discoverability. A major cleanup was performed by removing ONNX-based embedding and reranking code, streamlining dependencies and shifting towards API-based providers. The new store_memory and recall_memory tools, alongside the projects table and a more flexible WhereFilter for vector queries, represent robust new functionalities. The changes are well-aligned with the provided summary and test plan.

Comment thread plugin/engram/hooks/lib.js
Comment thread internal/mcp/tools_memory.go Outdated
Comment thread internal/mcp/tools_memory.go
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 10

🧹 Nitpick comments (3)
internal/config/config.go (1)

246-249: Рассмотрите добавление поддержки переменных окружения для новых полей конфигурации.

Для новых полей StoreMemory* не предусмотрены переопределения через переменные окружения в функции Load(). Это может затруднить настройку в контейнерных средах, где env-переменные предпочтительнее файлов конфигурации.

💡 Пример добавления env-переопределений
// В функции Load(), после существующих env-переопределений:
if v := strings.TrimSpace(os.Getenv("ENGRAM_STORE_MEMORY_HARD_LIMIT")); v != "" {
	if n, err := strconv.Atoi(v); err == nil && n > 0 {
		cfg.StoreMemoryHardLimit = n
	}
}
if v := strings.TrimSpace(os.Getenv("ENGRAM_STORE_MEMORY_SOFT_LIMIT")); v != "" {
	if n, err := strconv.Atoi(v); err == nil && n > 0 {
		cfg.StoreMemorySoftLimit = n
	}
}
if v := strings.TrimSpace(os.Getenv("ENGRAM_STORE_MEMORY_DEDUP_THRESHOLD")); v != "" {
	if f, err := strconv.ParseFloat(v, 64); err == nil && f > 0 && f <= 1.0 {
		cfg.StoreMemoryDedupThreshold = f
	}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/config/config.go` around lines 246 - 249, Добавьте поддержку
переопределения новых полей конфигурации через переменные окружения в функции
Load(): для полей StoreMemoryHardLimit и StoreMemorySoftLimit читайте
соответствующие ENGRAM_STORE_MEMORY_HARD_LIMIT / ENGRAM_STORE_MEMORY_SOFT_LIMIT
через os.Getenv, очищайте строку с strings.TrimSpace, парсьте в int через
strconv.Atoi и применяйте только при успешном парсинге и значении >0; для
StoreMemoryDedupThreshold читайте ENGRAM_STORE_MEMORY_DEDUP_THRESHOLD и парсьте
в float64 через strconv.ParseFloat, проверяя диапазон >0 и <=1.0; не забывайте
оставить StoreMemorySummarize доступным через переменную окружения (например
ENGRAM_STORE_MEMORY_SUMMARIZE) и парсить её как булево значение. Это позволит
контейнерным/CI средам переопределять эти настройки.
internal/pipeline/deterministic.go (1)

241-271: Устаревший комментарий с упоминанием BGE.

Комментарий на строке 265 (~400 tokens for BGE) ссылается на модель BGE, которая удалена в этом PR. Учитывая переход на OpenAI-провайдер по умолчанию (text-embedding-3-small), рекомендуется обновить комментарий для отражения актуального контекста.

💡 Предлагаемое исправление
-	// Rough truncation at ~1500 chars (~400 tokens for BGE)
+	// Rough truncation at ~1500 chars (~375 tokens)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/pipeline/deterministic.go` around lines 241 - 271, Комментарий в
функции FormatEmbeddingText упоминает устаревшую модель BGE ("~400 tokens for
BGE"); обновите этот комментарий, чтобы он отражал текущий контекст (например,
заменить на ссылку на используемую модель text-embedding-3-small или сделать его
нейтральным, например: "Rough truncation at ~1500 chars (approx token budget for
embedding model)"). Найдите функцию FormatEmbeddingText и отредактируйте строку
комментария над трюнкейтом (текущая "Rough truncation at ~1500 chars (~400
tokens for BGE)") на актуальную формулировку или уберите модель-специфику.
internal/worker/handlers_context.go (1)

99-99: Рассмотрите возможность оптимизации: фильтрация проекта на уровне вектора.

Пустой проект в BuildWhereFilter означает, что векторный поиск вернёт результаты из всех проектов, а фильтрация по проекту происходит позже в ExtractObservationIDs (строка 147). Это может быть неэффективно при большом количестве данных.

Если глобальные наблюдения должны быть включены, рассмотрите:

where := vector.BuildWhereFilter(vector.DocTypeObservation, project, true)

Это позволит фильтровать на уровне базы данных, возвращая только project=X OR scope=global.

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

In `@internal/worker/handlers_context.go` at line 99, Здесь фильтр векторного
поиска строится с пустым project
(vector.BuildWhereFilter(vector.DocTypeObservation, "", false)), из‑за чего
поиск вернёт все проекты и фильтрация по проекту выполняется позже в
ExtractObservationIDs; замените вызов на использование project и флага включения
глобальных наблюдений (т.е. вызвать
vector.BuildWhereFilter(vector.DocTypeObservation, project, true) или
эквивалент), чтобы переносить фильтрацию проекта/глобальных записей в запрос к
векторной БД и уменьшить объём возвращаемых результатов; поправьте параметры
вызова в том же контексте и убедитесь, что логика ExtractObservationIDs остаётся
совместимой с новым предфильтром.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/db/gorm/models.go`:
- Around line 347-351: The Project.LegacyIDs field uses models.JSONStringArray
but the DB stores native PostgreSQL TEXT[] (used with array_append() and @>),
causing JSONStringArray.Scan to fail on PostgreSQL array format; replace the
field type with pq.StringArray (from github.com/lib/pq), update the struct tag
to match a PostgreSQL array (e.g. `gorm:"column:legacy_ids;type:text[]"`), add
the pq import, and remove/stop relying on JSONStringArray.Scan—this makes
Project.LegacyIDs, any reads in project_store.go, and usages of
array_append()/@> handle PostgreSQL TEXT[] correctly.

In `@internal/db/gorm/project_store.go`:
- Around line 50-56: The background goroutine launches reassociateObservations
without a context or timeout, which can leak work after parent cancellation;
modify this to derive a context with timeout/cancel (e.g., ctx, cancel :=
context.WithTimeout(parentCtx, timeout)) and run the goroutine with that ctx,
defer cancel, and pass a context-aware DB to reassociateObservations (either add
a ctx parameter to reassociateObservations or call db.WithContext(ctx) inside
the goroutine) so the operation respects cancellation and timeouts while still
logging failures tied to legacyID/newID.

In `@internal/mcp/server.go`:
- Around line 1562-1563: The similarity search is excluding global memories
because the where filter calls
vector.BuildWhereFilter(vector.DocTypeObservation, params.Project, false);
update both places (the call at the shown diff and the other occurrence around
lines 2307-2308) to pass includeGlobal=true so project-scoped searches also
include scope='global'; i.e., change the third argument to true so functions
like find_similar_observations and suggest_consolidations return results that
include global duplicates when querying via s.vectorClient.Query.

In `@internal/mcp/tools_memory.go`:
- Around line 22-30: The params struct currently parses Importance but never
applies it to the saved observation; update the save path to propagate
params.Importance into the stored observation (e.g., set observation.Importance
= params.Importance or use the same scaling logic used elsewhere) before calling
the store/save function so the persisted observation reflects the provided
priority; apply the same fix for the other identical block referenced (lines
~139-149) to ensure both code paths use the Importance field from params.
- Around line 109-123: The dedup logic uses params.Project directly (possibly
empty) and calls vector.BuildWhereFilter with includeGlobal=false, causing
memories to be stored under "" or to miss global matches; fix by normalizing the
project before building the filter and querying: compute project :=
params.Project; if project == "" use the service's current/default project (e.g.
s.defaultProject or s.CurrentProject) and then call
vector.BuildWhereFilter(vector.DocTypeObservation, project, true) so global
observations are included in the dedup scope; apply the same normalized project
when calling StoreObservation and anywhere else you pass params.Project
(references: BuildWhereFilter, Query usage that sets where, and
StoreObservation).

In `@internal/search/manager.go`:
- Line 881: The call to vector.BuildWhereFilter currently mis-handles
params.Scope=="global"; change it to handle three cases explicitly: if
params.Scope == "global" call vector.BuildWhereFilter(docType, "" , true) to
return only global results; if params.Scope == "project" call
vector.BuildWhereFilter(docType, params.Project, false) to return only project
results; otherwise (params.Scope == "") call vector.BuildWhereFilter(docType,
params.Project, params.IncludeGlobal) to preserve existing behavior. Use
params.Scope, params.Project and params.IncludeGlobal to implement this
branching before invoking BuildWhereFilter.

In `@internal/sessions/parser.go`:
- Around line 210-218: The runGitCommand function currently uses exec.Command
without a timeout which can hang; update runGitCommand to create a context with
a timeout (matching the JS implementation, e.g. 3s), use
exec.CommandContext(ctx, "git", fullArgs...) and defer cancel(), then run
cmd.Output() (or CombinedOutput if you want stderr included) so the call is
killed on timeout and you still return the trimmed stdout and the error;
reference the runGitCommand function and replace the exec.Command invocation
with exec.CommandContext using a context.WithTimeout.
- Around line 179-188: ProjectID currently returns only an 8-char hash, causing
mismatch with JS which prefixes the directory name; change ProjectID to build
IDs as dirName + "_" + hash: obtain dirName with filepath.Base(cwdPath), call
GitRemoteProjectID(cwdPath) and if it returns a non-empty id return dirName +
"_" + id, otherwise compute sha256 of cwdPath, take the first 6 hex chars and
return dirName + "_" + hex[:6]; also update GitRemoteProjectID to return the
same hash form expected by JS for git-based repos (i.e., the hash portion only,
not prefixed) so ProjectID composes the final dirName_prefix consistently.

In `@internal/vector/pgvector/client.go`:
- Around line 153-167: The loop that assembles WHERE SQL uses unvalidated
WhereClause.Column directly in fmt.Sprintf, allowing injection via column
identifiers; update the Query/BuildWhereFilter path to validate each
clause.Column against an allowlist (e.g., a set/slice of permitted column names)
before appending to whereClauses, and return an error if a column is not
allowed; ensure both the OrGroup branch and the single-clause branch check the
column name (referencing WhereClause.Column, where.Clauses loop, and
BuildWhereFilter/Query) so only vetted identifiers are used when formatting the
"%s = $%d" pieces while continuing to parameterize values into args.

In `@plugin/engram/skills/memory/SKILL.md`:
- Around line 42-49: The SKILL.md references a non-existent tool name "merge"
which will cause a tool-not-found error; update the documentation to reference
the actual exported tool name merge_observations (or alternatively export an
alias called merge from internal/mcp/server.go), e.g., replace "merge" with
"merge_observations" in SKILL.md or add an exported alias function named merge
that forwards to merge_observations so the agent can find the tool.

---

Nitpick comments:
In `@internal/config/config.go`:
- Around line 246-249: Добавьте поддержку переопределения новых полей
конфигурации через переменные окружения в функции Load(): для полей
StoreMemoryHardLimit и StoreMemorySoftLimit читайте соответствующие
ENGRAM_STORE_MEMORY_HARD_LIMIT / ENGRAM_STORE_MEMORY_SOFT_LIMIT через os.Getenv,
очищайте строку с strings.TrimSpace, парсьте в int через strconv.Atoi и
применяйте только при успешном парсинге и значении >0; для
StoreMemoryDedupThreshold читайте ENGRAM_STORE_MEMORY_DEDUP_THRESHOLD и парсьте
в float64 через strconv.ParseFloat, проверяя диапазон >0 и <=1.0; не забывайте
оставить StoreMemorySummarize доступным через переменную окружения (например
ENGRAM_STORE_MEMORY_SUMMARIZE) и парсить её как булево значение. Это позволит
контейнерным/CI средам переопределять эти настройки.

In `@internal/pipeline/deterministic.go`:
- Around line 241-271: Комментарий в функции FormatEmbeddingText упоминает
устаревшую модель BGE ("~400 tokens for BGE"); обновите этот комментарий, чтобы
он отражал текущий контекст (например, заменить на ссылку на используемую модель
text-embedding-3-small или сделать его нейтральным, например: "Rough truncation
at ~1500 chars (approx token budget for embedding model)"). Найдите функцию
FormatEmbeddingText и отредактируйте строку комментария над трюнкейтом (текущая
"Rough truncation at ~1500 chars (~400 tokens for BGE)") на актуальную
формулировку или уберите модель-специфику.

In `@internal/worker/handlers_context.go`:
- Line 99: Здесь фильтр векторного поиска строится с пустым project
(vector.BuildWhereFilter(vector.DocTypeObservation, "", false)), из‑за чего
поиск вернёт все проекты и фильтрация по проекту выполняется позже в
ExtractObservationIDs; замените вызов на использование project и флага включения
глобальных наблюдений (т.е. вызвать
vector.BuildWhereFilter(vector.DocTypeObservation, project, true) или
эквивалент), чтобы переносить фильтрацию проекта/глобальных записей в запрос к
векторной БД и уменьшить объём возвращаемых результатов; поправьте параметры
вызова в том же контексте и убедитесь, что логика ExtractObservationIDs остаётся
совместимой с новым предфильтром.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ca4261b9-f2a3-4dbb-9315-15c45e739c14

📥 Commits

Reviewing files that changed from the base of the PR and between 46fa37b and aaea638.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (52)
  • cmd/mcp/main.go
  • go.mod
  • internal/config/config.go
  • internal/db/gorm/migrations.go
  • internal/db/gorm/models.go
  • internal/db/gorm/project_store.go
  • internal/db/gorm/store.go
  • internal/embedding/assets.go
  • internal/embedding/assets/.model_version
  • internal/embedding/assets/lib/darwin-arm64/.version
  • internal/embedding/assets/lib/linux-arm64/.version
  • internal/embedding/assets/lib/windows-amd64/.version
  • internal/embedding/assets/model.onnx
  • internal/embedding/assets/tokenizer.json
  • internal/embedding/lib_darwin.go
  • internal/embedding/lib_darwin_arm64.go
  • internal/embedding/lib_linux_amd64.go
  • internal/embedding/lib_linux_arm64.go
  • internal/embedding/lib_windows.go
  • internal/embedding/lib_windows_amd64.go
  • internal/embedding/model.go
  • internal/embedding/service.go
  • internal/embedding/service_onnx.go
  • internal/embedding/service_test.go
  • internal/instincts/dedup.go
  • internal/mcp/server.go
  • internal/mcp/tools_memory.go
  • internal/pipeline/deterministic.go
  • internal/reranking/assets.go
  • internal/reranking/assets/model.onnx
  • internal/reranking/assets/tokenizer.json
  • internal/reranking/assets/tokenizer_config.json
  • internal/reranking/factory.go
  • internal/reranking/factory_onnx.go
  • internal/reranking/interface.go
  • internal/reranking/service.go
  • internal/reranking/service_test.go
  • internal/search/manager.go
  • internal/sessions/parser.go
  • internal/telemetry/similarity.go
  • internal/vector/interface.go
  • internal/vector/pgvector/client.go
  • internal/vector/types.go
  • internal/worker/handlers_backfill.go
  • internal/worker/handlers_context.go
  • internal/worker/handlers_data.go
  • internal/worker/service.go
  • pkg/models/observation.go
  • plugin/engram/hooks/lib.js
  • plugin/engram/hooks/session-start.js
  • plugin/engram/hooks/user-prompt.js
  • plugin/engram/skills/memory/SKILL.md
💤 Files with no reviewable changes (23)
  • internal/embedding/assets/model.onnx
  • internal/embedding/assets/lib/windows-amd64/.version
  • internal/embedding/lib_darwin_arm64.go
  • internal/embedding/lib_linux_arm64.go
  • internal/reranking/service_test.go
  • internal/embedding/model.go
  • internal/embedding/lib_windows.go
  • internal/reranking/assets.go
  • internal/reranking/factory_onnx.go
  • internal/reranking/factory.go
  • internal/embedding/assets/lib/linux-arm64/.version
  • internal/embedding/lib_darwin.go
  • internal/embedding/assets/.model_version
  • internal/embedding/service_onnx.go
  • internal/db/gorm/store.go
  • internal/embedding/service_test.go
  • internal/embedding/assets.go
  • internal/reranking/assets/tokenizer_config.json
  • internal/embedding/lib_linux_amd64.go
  • internal/reranking/assets/model.onnx
  • internal/reranking/service.go
  • internal/embedding/assets/lib/darwin-arm64/.version
  • internal/embedding/lib_windows_amd64.go

Comment thread internal/db/gorm/models.go Outdated
Comment thread internal/db/gorm/project_store.go Outdated
Comment thread internal/mcp/server.go Outdated
Comment thread internal/mcp/tools_memory.go
Comment thread internal/mcp/tools_memory.go
Comment thread internal/search/manager.go Outdated
Comment thread internal/sessions/parser.go Outdated
Comment thread internal/sessions/parser.go
Comment thread internal/vector/pgvector/client.go
Comment thread plugin/engram/skills/memory/SKILL.md
- Update SKILL.md frontmatter description to mention store_memory/recall_memory
  explicitly so skill matching triggers on explicit storage intent
- Add "Engram vs Claude Code File Memory" comparison table to SKILL.md
- Add "Engram vs File-Based Memory" guidance to engramInstructions
- Rule of thumb: searchable knowledge → engram, static instructions → file memory
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
plugin/engram/skills/memory/SKILL.md (1)

205-205: ⚠️ Potential issue | 🟡 Minor

Несоответствие: "top 10" vs "Top 12".

Заголовок раздела на строке 53 теперь Top 12 Tools, но здесь указано "beyond the top 10". Следует обновить для согласованности.

-These tools cover specialized use cases beyond the top 10:
+These tools cover specialized use cases beyond the top 12:
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plugin/engram/skills/memory/SKILL.md` at line 205, The sentence "These tools
cover specialized use cases beyond the top 10" is inconsistent with the section
header "Top 12 Tools"; update that sentence to reference "top 12" instead of
"top 10" so the text matches the heading (locate the sentence in SKILL.md that
follows the "Top 12 Tools" heading and change "top 10" → "top 12").
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@plugin/engram/skills/memory/SKILL.md`:
- Line 205: The sentence "These tools cover specialized use cases beyond the top
10" is inconsistent with the section header "Top 12 Tools"; update that sentence
to reference "top 12" instead of "top 10" so the text matches the heading
(locate the sentence in SKILL.md that follows the "Top 12 Tools" heading and
change "top 10" → "top 12").

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b3fe57f6-8daf-49cc-9171-b8bdd6eff5fa

📥 Commits

Reviewing files that changed from the base of the PR and between aaea638 and 8e1794d.

📒 Files selected for processing (2)
  • internal/mcp/server.go
  • plugin/engram/skills/memory/SKILL.md

thebtf added 2 commits March 11, 2026 23:35
Add encrypted credential storage infrastructure:
- internal/crypto/vault.go: AES-256-GCM encrypt/decrypt, key loading
  (env var → file → auto-generate), SHA-256 fingerprint
- internal/crypto/vault_test.go: 10 tests (round-trip, tamper, nonce
  randomness, key formats, auto-generation, edge cases)
- internal/db/gorm/migrations.go: migration 031 adds encrypted_secret
  (BYTEA) and encryption_key_fingerprint (TEXT) columns, updates CHECK
  constraint to include 'credential' type
- internal/db/gorm/models.go: add credential fields to GORM model
- internal/db/gorm/observation_store.go: GetCredential (with project
  shadowing), ListCredentials, DeleteCredential, CountCredentials
- internal/config/config.go: ENGRAM_ENCRYPTION_KEY_FILE and
  ENGRAM_ENCRYPTION_KEY env var support
- pkg/models/observation.go: ObsTypeCredential constant, encrypted
  fields in ParsedObservation

Security: parameterized ORDER BY via gorm.Expr (no SQL injection),
COALESCE(is_archived, 0) for NULL-safety, key fingerprint mismatch
detection (refuse-to-corrupt, never serve corrupted plaintext).
5 MCP tools for credential management:
- store_credential: encrypt and store (AES-256-GCM, lazy vault init)
- get_credential: decrypt with fingerprint verification
- list_credentials: names + metadata only (no values)
- delete_credential: remove by name
- vault_status: key status, fingerprint, credential count

4 leak prevention barriers:
- internal/search/manager.go: filterCredentials() strips credential
  type from UnifiedSearch results before caching
- plugin/engram/hooks/user-prompt.js: safeObservations filter excludes
  type='credential' from context injection
- internal/mcp/tools_memory.go: privacy.ContainsSecrets() guard rejects
  plaintext secrets in store_memory (use store_credential instead)
- Credentials only accessible via dedicated MCP tools, never via
  search/recall_memory/find_by_file
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 12

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/db/gorm/observation_store.go (1)

153-173: ⚠️ Potential issue | 🔴 Critical

Credential записи теперь попадают под обычный retention-cleanup.

Секреты сохраняются через тот же StoreObservation(), а ниже эта же ветка безусловно ставит проект в очередь CleanupOldObservations() (Lines 1013-1046), который удаляет старые записи без фильтра по типу. Как только проект перевалит за лимит, старые credential-наблюдения начнут тихо исчезать.

🤖 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 153 - 173, The credential
observations are being stored via StoreObservation() but not marked, so
CleanupOldObservations() removes them indiscriminately; detect credential
observations when building dbObs (obs.EncryptedSecret != "" or obs.Type ==
CredentialType) and set a new marker field on Observation (e.g.,
ProtectedFromRetentionCleanup bool) when creating dbObs in observation_store.go,
and update CleanupOldObservations() to skip/defer deletion of records with
ProtectedFromRetentionCleanup == true; ensure the Observation model, the db
insert path in StoreObservation(), and the CleanupOldObservations() deletion
query/logic are all updated to use that marker.
♻️ Duplicate comments (5)
internal/db/gorm/models.go (1)

349-353: ⚠️ Potential issue | 🔴 Critical

LegacyIDs всё ещё несовместим с TEXT[].

Миграция 030_projects_table создаёт legacy_ids TEXT[], а models.JSONStringArray из pkg/models/observation.go (Lines 112-143) ожидает JSON вида ["a","b"], не PostgreSQL array literal вида {a,b}. На чтении/записи через эту модель вы снова упрётесь в ошибку сериализации; здесь нужен тип, который реально умеет TEXT[], например pq.StringArray.

💡 Предлагаемое исправление
+import "github.com/lib/pq"
+
 type Project struct {
 	GitRemote    sql.NullString         `gorm:"column:git_remote;index"`
 	RelativePath sql.NullString         `gorm:"column:relative_path"`
 	DisplayName  sql.NullString         `gorm:"column:display_name"`
-	LegacyIDs    models.JSONStringArray `gorm:"column:legacy_ids;type:text"`
+	LegacyIDs    pq.StringArray         `gorm:"column:legacy_ids;type:text[]"`
 	ID           string                 `gorm:"primaryKey"`
 	CreatedAt    time.Time              `gorm:"autoCreateTime"`
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/db/gorm/models.go` around lines 349 - 353, The Project model's
LegacyIDs field uses models.JSONStringArray (from pkg/models/observation.go)
which serializes JSON (["a","b"]) but the DB migration created legacy_ids as a
PostgreSQL TEXT[] and requires a Postgres array type; replace LegacyIDs' type
with a Postgres-aware array type (e.g., pq.StringArray) and update the GORM tag
to use the proper Postgres array column type (e.g., `type:text[]`) so
reads/writes use the DB array literal format; ensure imports are adjusted and
that the Project struct's LegacyIDs and any code that marshals/unmarshals it are
updated to the new type.
internal/search/manager.go (1)

900-900: ⚠️ Potential issue | 🟠 Major

Scope="global" всё ещё строит неверный where-фильтр.

На Line 900 при params.Scope == "global" сюда по-прежнему попадает includeGlobal=false, поэтому BuildWhereFilter() строит project-only фильтр. Дополнительно internal/vector/types.go (Lines 103-121) вообще не умеет выразить режим global-only отдельным clause, так что для явного global-поиска здесь всё ещё нужен отдельный путь.

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

In `@internal/search/manager.go` at line 900, When params.Scope == "global" the
call to vector.BuildWhereFilter(docType, params.Project, params.IncludeGlobal ||
params.Scope == "") still passes includeGlobal=false and produces a project-only
filter; fix by adding an explicit branch for global-only: if params.Scope ==
"global" call a dedicated global-only builder (or extend BuildWhereFilter to
accept a scope argument) so it constructs a clause that matches only global
entries, otherwise keep the existing logic for project or mixed searches; update
any call sites to use vector.BuildWhereFilter(docType, params.Project,
true/false) or a new vector.BuildGlobalWhereFilter to ensure global-only queries
are expressible (see symbols: params.Scope, params.IncludeGlobal,
vector.BuildWhereFilter and internal/vector/types.go types that represent global
vs project clauses).
internal/mcp/server.go (1)

1636-1637: ⚠️ Potential issue | 🟠 Major

Similarity-пути всё ещё скрывают global memories.

Оба вызова по-прежнему передают includeGlobal=false, поэтому find_similar_observations и suggest_consolidations будут пропускать глобальные дубликаты и кандидатов на merge, хотя Phase 3 добавлял для этого OR-фильтр.

🛠️ Прямая правка для обоих мест
-	where := vector.BuildWhereFilter(vector.DocTypeObservation, params.Project, false)
+	where := vector.BuildWhereFilter(vector.DocTypeObservation, params.Project, true)

Also applies to: 2381-2382

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

In `@internal/mcp/server.go` around lines 1636 - 1637, Both Query calls build a
where filter with includeGlobal=false which hides global memories; update the
BuildWhereFilter calls to pass includeGlobal=true so global observations are
included. Specifically, change the
vector.BuildWhereFilter(vector.DocTypeObservation, params.Project, false) used
before s.vectorClient.Query (and the same pattern at the other occurrence) to
vector.BuildWhereFilter(vector.DocTypeObservation, params.Project, true) so
find_similar_observations / suggest_consolidations will consider global
duplicates and merge candidates.
internal/mcp/tools_memory.go (2)

23-31: ⚠️ Potential issue | 🟠 Major

importance всё ещё ни на что не влияет.

Поле парсится и рекламируется в schema, но после StoreObservation никак не применяется. Для клиента это выглядит как успешное сохранение приоритета, хотя у записи остаётся дефолтный score.

Also applies to: 145-148

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

In `@internal/mcp/tools_memory.go` around lines 23 - 31, The parsed Importance
from the request (params.Importance in the params struct) is never applied after
parsing—pass it into the storage flow by setting the observation score or
priority before calling StoreObservation: populate the created observation's
Score (or Priority) field from params.Importance (or convert to the expected
score type) and ensure StoreObservation receives that observation; update any
helper/constructor used to build the observation (the code path that creates the
observation object prior to calling StoreObservation) so the importance is not
ignored.

115-129: ⚠️ Potential issue | 🟠 Major

Дедуп всё ещё пропускает global и пишет память под пустым project.

BuildWhereFilter(..., params.Project, false) по-прежнему исключает scope='global', хотя именно это исправлялось в Phase 3. Плюс при пустом project запись всё ещё уходит в StoreObservation(..., "", ...), несмотря на обещание "defaults to current".

🛠️ Минимальная безопасная правка
+	project := strings.TrimSpace(params.Project)
+	if project == "" {
+		return "", fmt.Errorf("project is required until current-project resolution is implemented")
+	}
+
 	// Dedup check: skip if very similar observation already exists.
 	if s.vectorClient != nil && s.vectorClient.IsConnected() {
-		where := vector.BuildWhereFilter(vector.DocTypeObservation, params.Project, false)
+		where := vector.BuildWhereFilter(vector.DocTypeObservation, project, true)
 		similar, err := s.vectorClient.Query(ctx, params.Content, 1, where)
 		if err == nil && len(similar) > 0 && similar[0].Similarity >= dedupThreshold {
@@
-	id, _, err := s.observationStore.StoreObservation(ctx, "", params.Project, obs, 0, 0)
+	id, _, err := s.observationStore.StoreObservation(ctx, "", project, obs, 0, 0)

Also applies to: 145-145

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

In `@internal/mcp/tools_memory.go` around lines 115 - 129, The dedup logic is
still excluding global-scope and allows empty project writes: update the call to
vector.BuildWhereFilter in the dedup block to include global scope (i.e., pass
the flag that keeps scope='global' rather than false) so global observations are
considered when querying via s.vectorClient.Query; additionally ensure when
params.Project is empty you substitute the effective current project before
calling BuildWhereFilter and before calling StoreObservation (i.e., normalize
params.Project to the default/current project value used elsewhere in this
component), so queries and StoreObservation(...) never run with an empty
project.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/config/config.go`:
- Around line 119-120: Add json:"-" struct tags to the sensitive Config fields
so they are excluded from JSON serialization: update the Config struct to
annotate EncryptionKeyFile and EncryptionKey with `json:"-"` (same style used
for DatabaseDSN) to prevent these env-only keys from leaking when calling
json.Marshal on Config.

In `@internal/crypto/vault.go`:
- Around line 54-75: The current os.Stat(keyFile) handling treats any stat error
as "file missing" and proceeds to generate a new key; change the logic in the
vault key-loading block that uses os.Stat and keyFile so that if statErr != nil
you first check if os.IsNotExist(statErr) and only generate a new key in that
case, otherwise return the statErr (wrapped with context) immediately instead of
overwriting the existing key; update the conditional around loadKeyFromFile/key
generation to explicitly handle these two paths and ensure the error returned
includes context about failing to stat the key file.

In `@internal/db/gorm/migrations.go`:
- Around line 1090-1100: The Rollback in the migration currently re-adds the old
constraint chk_observations_type which forbids type = 'credential', so if any
observations have type = 'credential' the ADD CONSTRAINT will fail; update the
Rollback (the anonymous Rollback func that runs tx.Exec(...)) to either 1) clean
or rewrite offending rows before re-adding the constraint (e.g. run an UPDATE
observations SET type = 'discovery' WHERE type = 'credential' OR DELETE FROM
observations WHERE type = 'credential' as appropriate, then re-add the
constraint), or 2) mark the rollback as irreversible by returning a non-nil
error (or removing the ADD CONSTRAINT statements) so the migration framework
won't attempt a dangerous restore; reference the Rollback function, the tx.Exec
calls, and the chk_observations_type constraint when making the change.

In `@internal/db/gorm/observation_store.go`:
- Around line 1249-1254: The current DeleteCredential method can delete both
project-scoped and global credentials (and archived matches) when names collide;
update DeleteCredential to first resolve exactly one Observation using the same
selection logic as GetCredential (matching type "credential", title or narrative
== name, prefer project-scoped over global per GetCredential rules, and exclude
archived/soft-deleted rows if GetCredential does), then perform the DELETE by
the resolved Observation.ID (or return an error if zero or multiple matches) so
only the intended record is removed; refer to the
ObservationStore.DeleteCredential and ObservationStore.GetCredential selection
logic and the Observation.ID field to implement this two-step lookup-then-delete
flow.

In `@internal/mcp/tools_credential.go`:
- Around line 40-58: handleStoreCredential currently sets a default scope
"project" but doesn't resolve the provided project to the canonical/current
project namespace, causing cross-project name collisions; fix by calling the
shared resolveProject() helper to normalize/resolve params.Project before
persisting when params.Scope == "project" (and use that same normalized value in
GetCredential, ListCredentials and DeleteCredential), i.e., update
handleStoreCredential to resolve and replace params.Project via
resolveProject(), and ensure GetCredential, ListCredentials and DeleteCredential
use the same resolveProject() logic for lookups/deletions so project-scoped
credentials are isolated consistently.
- Around line 250-257: handleVaultStatus is making a non-read-only call by
invoking getVault() which can create and persist a vault key; instead, avoid
initializing the vault and do a passive existence check (e.g., check the env var
or the vault key file path with os.Stat or add a helper like crypto.VaultExists)
to determine keyConfigured and fingerprint should remain empty unless a real
vault is already present; update handleVaultStatus to remove getVault() usage,
perform a file/env existence check for the vault key, set keyConfigured based on
that check, and only call v.Fingerprint() when you actually load a pre-existing
vault (or provide a safe crypto function to read only the fingerprint without
creating files).

In `@internal/mcp/tools_memory.go`:
- Around line 183-220: The recall_memory tool is missing a project scope: add a
"project" field to the params struct and the recall_memory InputSchema in
internal/mcp/server.go, then populate search.SearchParams.Project (or resolve
and set the current project string) before calling s.searchMgr.UnifiedSearch;
ensure you reference the recall_memory handler that builds params, the
search.SearchParams struct (add Project), and avoid leaving Project empty so
downstream BuildWhereFilter will apply project filtering.
- Around line 53-60: The code currently uses byte-based len(...) and slicing
which can split UTF-8 runes; change the checks and truncation in the
store-memory logic to operate on runes: use
utf8.RuneCountInString(params.Content) (or convert to []rune) to compare against
hardLimit and softLimit, and when truncating do params.Content =
string([]rune(params.Content)[:softLimit]) so multibyte characters aren’t cut in
half; keep the same error message and log but ensure limits are treated as
rune/character counts.

In `@internal/search/manager.go`:
- Around line 727-731: filterCredentials is being called too late so raw
executeSearch() results can be cached by putInCache (and by warmFrequentQueries)
before sanitation; move credential filtering into the common execution path so
no code path calls putInCache or returns an executeSearch() result without first
calling filterCredentials. Concretely, ensure executeSearch (and any callers
like warmFrequentQueries and the code that computes cacheKey) sanitize the
searchResult via filterCredentials(searchResult) immediately after
receive/create and before any m.putInCache(cacheKey, searchResult) or return, so
all cache writes and returns use the filtered result.
- Around line 741-753: В функции filterCredentials замените текущую логику
проверки типа записи: помимо r.Type == "credential" также учитывайте случаи,
когда r.Type == "observation" и реальный тип хранится в r.Metadata["obs_type"];
то есть пропускайте (не добавляйте в filtered) записи, если r.Type ==
"credential" или (r.Type == "observation" && r.Metadata["obs_type"] ==
"credential"). Обновите присвоение result.Results и result.TotalCount как
сейчас; используйте имена функций/полей filterCredentials, SearchResult.Type и
SearchResult.Metadata["obs_type"] чтобы найти место правки.

In `@plugin/engram/hooks/user-prompt.js`:
- Around line 38-40: Сейчас searchIds собирается прямо из safeObservations,
из‑за чего /mark-injected получает ID для записей, которые потом могут быть
удалены при dedup/token trimming и не попасть в contextToInject; перенесите
формирование searchIds после шагов dedup и token trimming (то есть после
функции/blocks, которые производят dedup и обрезку токенов) и используйте ту же
результирующую коллекцию contextToInject (или её переменную) для заполнения
searchIds, чтобы /mark-injected и метрики получали точный набор ID; обновите код
вокруг safeObservations, searchIds и вызова /mark-injected соответственно.
- Around line 165-167: Вставка полей памяти (title, facts, narrative, возможно
obsType) в XML-подобный блок через contextBuilder происходит без экранирования и
подвержена prompt-injection; перед объединением в строку в том месте, где
формируется contextBuilder (`contextBuilder += \`## ${idx}. [${obsType}]
${title}${scopeTag}${scoreTag}\n\`;`), пропустите title, facts и narrative через
тот же защитный хелпер escapeXmlTags (используемый в session-start.js) — т.е.
заменить сырой title/facts/narrative на escapeXmlTags(title) и т.д., чтобы любые
символы `<`, `>`, `</relevant-memory>` и т.п. были экранированы до вставки.

---

Outside diff comments:
In `@internal/db/gorm/observation_store.go`:
- Around line 153-173: The credential observations are being stored via
StoreObservation() but not marked, so CleanupOldObservations() removes them
indiscriminately; detect credential observations when building dbObs
(obs.EncryptedSecret != "" or obs.Type == CredentialType) and set a new marker
field on Observation (e.g., ProtectedFromRetentionCleanup bool) when creating
dbObs in observation_store.go, and update CleanupOldObservations() to skip/defer
deletion of records with ProtectedFromRetentionCleanup == true; ensure the
Observation model, the db insert path in StoreObservation(), and the
CleanupOldObservations() deletion query/logic are all updated to use that
marker.

---

Duplicate comments:
In `@internal/db/gorm/models.go`:
- Around line 349-353: The Project model's LegacyIDs field uses
models.JSONStringArray (from pkg/models/observation.go) which serializes JSON
(["a","b"]) but the DB migration created legacy_ids as a PostgreSQL TEXT[] and
requires a Postgres array type; replace LegacyIDs' type with a Postgres-aware
array type (e.g., pq.StringArray) and update the GORM tag to use the proper
Postgres array column type (e.g., `type:text[]`) so reads/writes use the DB
array literal format; ensure imports are adjusted and that the Project struct's
LegacyIDs and any code that marshals/unmarshals it are updated to the new type.

In `@internal/mcp/server.go`:
- Around line 1636-1637: Both Query calls build a where filter with
includeGlobal=false which hides global memories; update the BuildWhereFilter
calls to pass includeGlobal=true so global observations are included.
Specifically, change the vector.BuildWhereFilter(vector.DocTypeObservation,
params.Project, false) used before s.vectorClient.Query (and the same pattern at
the other occurrence) to vector.BuildWhereFilter(vector.DocTypeObservation,
params.Project, true) so find_similar_observations / suggest_consolidations will
consider global duplicates and merge candidates.

In `@internal/mcp/tools_memory.go`:
- Around line 23-31: The parsed Importance from the request (params.Importance
in the params struct) is never applied after parsing—pass it into the storage
flow by setting the observation score or priority before calling
StoreObservation: populate the created observation's Score (or Priority) field
from params.Importance (or convert to the expected score type) and ensure
StoreObservation receives that observation; update any helper/constructor used
to build the observation (the code path that creates the observation object
prior to calling StoreObservation) so the importance is not ignored.
- Around line 115-129: The dedup logic is still excluding global-scope and
allows empty project writes: update the call to vector.BuildWhereFilter in the
dedup block to include global scope (i.e., pass the flag that keeps
scope='global' rather than false) so global observations are considered when
querying via s.vectorClient.Query; additionally ensure when params.Project is
empty you substitute the effective current project before calling
BuildWhereFilter and before calling StoreObservation (i.e., normalize
params.Project to the default/current project value used elsewhere in this
component), so queries and StoreObservation(...) never run with an empty
project.

In `@internal/search/manager.go`:
- Line 900: When params.Scope == "global" the call to
vector.BuildWhereFilter(docType, params.Project, params.IncludeGlobal ||
params.Scope == "") still passes includeGlobal=false and produces a project-only
filter; fix by adding an explicit branch for global-only: if params.Scope ==
"global" call a dedicated global-only builder (or extend BuildWhereFilter to
accept a scope argument) so it constructs a clause that matches only global
entries, otherwise keep the existing logic for project or mixed searches; update
any call sites to use vector.BuildWhereFilter(docType, params.Project,
true/false) or a new vector.BuildGlobalWhereFilter to ensure global-only queries
are expressible (see symbols: params.Scope, params.IncludeGlobal,
vector.BuildWhereFilter and internal/vector/types.go types that represent global
vs project clauses).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 168fbf2c-eb8c-4c10-bdab-b7b9c2e4a18b

📥 Commits

Reviewing files that changed from the base of the PR and between 8e1794d and cee146f.

📒 Files selected for processing (12)
  • internal/config/config.go
  • internal/crypto/vault.go
  • internal/crypto/vault_test.go
  • internal/db/gorm/migrations.go
  • internal/db/gorm/models.go
  • internal/db/gorm/observation_store.go
  • internal/mcp/server.go
  • internal/mcp/tools_credential.go
  • internal/mcp/tools_memory.go
  • internal/search/manager.go
  • pkg/models/observation.go
  • plugin/engram/hooks/user-prompt.js

Comment thread internal/config/config.go Outdated
Comment thread internal/crypto/vault.go
Comment thread internal/db/gorm/migrations.go
Comment thread internal/db/gorm/observation_store.go Outdated
Comment thread internal/mcp/tools_credential.go
Comment thread internal/mcp/tools_memory.go
Comment thread internal/search/manager.go Outdated
Comment thread internal/search/manager.go Outdated
Comment thread plugin/engram/hooks/user-prompt.js Outdated
Comment thread plugin/engram/hooks/user-prompt.js
7 fixes from multi-model code review (3 HIGH, 2 MEDIUM, 2 LOW):

- C1: Validate scope parameter server-side (reject invalid values)
- C3: filterCredentials returns new struct (fix singleflight data race)
- C4: Scope-aware DeleteCredential (prevent cross-project global deletion)
- S1: Use synthetic "credential:vault" session ID (fix unique constraint)
- S4: Trim raw-bytes fallback in loadKeyFromFile (handle trailing newline)
- S6: Use subtle.ConstantTimeCompare for fingerprint matching
- C2: Document sync.Once permanent-failure semantics

5 items tracked in TECHNICAL_DEBT.md for follow-up.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (5)
internal/mcp/tools_credential.go (2)

272-280: ⚠️ Potential issue | 🟠 Major

handleVaultStatus имеет побочный эффект — создание ключа.

Вызов getVault() в status endpoint может создать новый vault.key файл через crypto.NewVault() (см. internal/crypto/vault.go:56-77). Для read-only status endpoint это неожиданное поведение.

Рекомендуется добавить passive check для существования ключа без его создания.

🔍 Предлагаемое решение

Добавить в crypto пакет функцию пассивной проверки:

// VaultKeyExists checks if a vault key is configured without creating one.
func VaultKeyExists(cfg *config.Config) bool {
	if cfg.EncryptionKey != "" {
		return true
	}
	if cfg.EncryptionKeyFile != "" {
		if _, err := os.Stat(cfg.EncryptionKeyFile); err == nil {
			return true
		}
	}
	keyFile := filepath.Join(config.DataDir(), "vault.key")
	_, err := os.Stat(keyFile)
	return err == nil
}

Затем использовать в handleVaultStatus:

 func (s *Server) handleVaultStatus(ctx context.Context, _ json.RawMessage) (string, error) {
-	v, vErr := getVault()
-
-	keyConfigured := vErr == nil && v != nil
-	fingerprint := ""
-	if keyConfigured {
-		fingerprint = v.Fingerprint()
-	}
+	cfg := config.Get()
+	keyConfigured := crypto.VaultKeyExists(cfg)
+	fingerprint := ""
+	if keyConfigured {
+		if v, err := getVault(); err == nil {
+			fingerprint = v.Fingerprint()
+		}
+	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/mcp/tools_credential.go` around lines 272 - 280, handleVaultStatus
currently calls getVault() which can create a new vault key as a side effect;
replace that with a passive existence check to avoid creating files during a
read-only status request. Add a new crypto.VaultKeyExists(cfg *config.Config)
bool (or similar) that checks cfg.EncryptionKey, cfg.EncryptionKeyFile, and the
presence of the data-dir vault.key without creating anything, then change
handleVaultStatus to call VaultKeyExists instead of getVault() and only call
v.Fingerprint() when an existing vault instance is explicitly loaded elsewhere;
ensure you reference the getVault and handleVaultStatus symbols when making the
change.

247-255: ⚠️ Potential issue | 🟠 Major

Та же проблема валидации project при scope="project".

Аналогично handleStoreCredential, если scope="project" и params.Project пустой, удаление может не найти ожидаемый credential или удалить не тот.

Добавьте такую же валидацию project.

🔒 Предлагаемое исправление
 	if params.Scope == "" {
 		params.Scope = "project"
 	}
 	switch params.Scope {
 	case "project", "global":
 		// valid
 	default:
 		return "", fmt.Errorf("invalid scope %q: must be \"project\" or \"global\"", params.Scope)
 	}
+	if params.Scope == "project" && params.Project == "" {
+		return "", fmt.Errorf("project is required for project-scoped credentials")
+	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/mcp/tools_credential.go` around lines 247 - 255, When scope is
"project" the code must validate that params.Project is non-empty (same as
handleStoreCredential); add a check (e.g. if params.Scope == "project" &&
params.Project == "") that returns an error like fmt.Errorf("missing project for
scope \"project\"") before proceeding to delete so the deletion cannot run with
an empty project and accidentally target the wrong credential; mirror the
validation logic used in handleStoreCredential and reference params.Scope and
params.Project in your change.
internal/db/gorm/observation_store.go (1)

1248-1274: ⚠️ Potential issue | 🟡 Minor

Улучшение по сравнению с предыдущей версией, но остаётся edge case.

Добавление параметра scope решает проблему из предыдущего ревью — теперь project и global credentials удаляются раздельно. Однако если существует несколько credentials с одинаковым именем в одном scope (например, две project-scoped credentials с name="api-key"), будут удалены все совпадения.

Для большей безопасности можно использовать двухэтапный подход: сначала найти одну запись, затем удалить по ID.

🛡️ Предлагаемое уточнение
 func (s *ObservationStore) DeleteCredential(ctx context.Context, name, project, scope string) error {
 	if scope == "" {
 		scope = "project"
 	}
+
+	// Find exactly one credential to delete
+	var obs Observation
 	query := s.db.WithContext(ctx).
 		Where("type = ?", "credential").
 		Where("(title = ? OR narrative = ?)", name, name).
 		Where("COALESCE(is_archived, 0) = 0")
 	if scope == "global" {
 		query = query.Where("scope = 'global'")
 	} else {
 		query = query.Where("project = ? AND scope = 'project'", project)
 	}
-	result := query.Delete(&Observation{})
+
+	if err := query.First(&obs).Error; err != nil {
+		if errors.Is(err, gorm.ErrRecordNotFound) {
+			return fmt.Errorf("credential %q not found", name)
+		}
+		return fmt.Errorf("find credential: %w", err)
+	}
+
+	result := s.db.WithContext(ctx).Delete(&Observation{}, obs.ID)
 	if result.Error != nil {
 		return fmt.Errorf("delete credential: %w", result.Error)
 	}
-	if result.RowsAffected == 0 {
-		return fmt.Errorf("credential %q not found", name)
-	}
 	return nil
 }
🤖 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 1248 - 1274,
DeleteCredential currently deletes all matching observations by name+scope;
instead first query for a single Observation (respecting the same scope logic)
using s.db.WithContext(ctx).Where(...).First(&obs) and if found call
s.db.WithContext(ctx).Delete(&Observation{}, obs.ID) so only that specific
record is removed; keep the existing scope defaulting logic and error handling
(return not-found if First returns gorm.ErrRecordNotFound, wrap other DB errors
similarly) and reference the DeleteCredential method, ObservationStore receiver,
and Observation model/ID when making the change.
internal/search/manager.go (2)

749-753: ⚠️ Potential issue | 🔴 Critical

Неверное поле для фильтрации — credential-наблюдения не будут отфильтрованы.

observationToResult() устанавливает Type = "observation" для всех наблюдений, а реальный тип (ObsTypeCredential) хранится в Metadata["obs_type"]. Текущая проверка r.Type != "credential" пропустит все credential-записи, так как их Type равен "observation".

🐛 Предлагаемое исправление
 func filterCredentials(result *UnifiedSearchResult) *UnifiedSearchResult {
 	if result == nil {
 		return nil
 	}
 	filtered := make([]SearchResult, 0, len(result.Results))
 	for _, r := range result.Results {
-		if r.Type != "credential" {
-			filtered = append(filtered, r)
+		// Skip credential observations (stored with Type="observation", obs_type="credential")
+		if r.Type == "observation" {
+			if obsType, _ := r.Metadata["obs_type"].(string); obsType == "credential" {
+				continue
+			}
 		}
+		filtered = append(filtered, r)
 	}
 	out := *result
 	out.Results = filtered
 	out.TotalCount = len(filtered)
 	return &out
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/search/manager.go` around lines 749 - 753, В цикле, где фильтруются
результаты (перебор result.Results), проверка должна опираться не на r.Type, а
на реальный тип наблюдения, который observationToResult() кладёт в
Metadata["obs_type"]; замените условие r.Type != "credential" на проверку
значения в r.Metadata — например пропускайте/фильтруйте записи, у которых
r.Metadata["obs_type"] == ObsTypeCredential — при этом защититесь от
отсутствующего ключа или нестрокового значения (проверить наличие и привести к
string) перед сравнением; используйте существующие символы
observationToResult(), ObsTypeCredential и result.Results/filtered для
локализации правки.

905-905: ⚠️ Potential issue | 🔴 Critical

Параметр Scope="global" возвращает результаты проекта вместо только глобальных результатов.

Текущая логика params.IncludeGlobal || params.Scope == "" при Scope="global" вычисляется как false || false = false, что приводит к фильтрации только по проекту, игнорируя глобальную область видимости.

Требуется обработка трёх случаев:

  • Scope="global": только глобальные результаты (scope='global')
  • Scope="project": только результаты проекта
  • Scope="": оба типа результатов (проект и глобальные)
Предлагаемое исправление
-	where := vector.BuildWhereFilter(docType, params.Project, params.IncludeGlobal || params.Scope == "")
+	var where vector.WhereFilter
+	switch params.Scope {
+	case "global":
+		// Only global results
+		where = vector.BuildWhereFilter(docType, "", false)
+		where.Clauses = append(where.Clauses, vector.WhereClause{Column: "scope", Value: "global"})
+	case "project":
+		// Only project results
+		where = vector.BuildWhereFilter(docType, params.Project, false)
+	default:
+		// Both project and global results
+		where = vector.BuildWhereFilter(docType, params.Project, true)
+	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/search/manager.go` at line 905, The current single boolean
expression mishandles params.Scope; replace the inline call to
vector.BuildWhereFilter with explicit handling of the three scope cases: if
params.Scope == "global" call vector.BuildWhereFilter(docType, "", true) to
return only global results; else if params.Scope == "project" call
vector.BuildWhereFilter(docType, params.Project, false) to return only project
results; else (params.Scope == "") call vector.BuildWhereFilter(docType,
params.Project, true) to return both project and global results. Use the
params.Scope, params.Project, and vector.BuildWhereFilter symbols to locate and
update the code.
🧹 Nitpick comments (1)
internal/mcp/tools_credential.go (1)

15-36: Документация sync.Once семантики — хорошо, но тесты могут быть проблемой.

Комментарий корректно описывает permanent-failure семантику sync.Once. Однако package-level состояние усложняет изоляцию тестов.

Для улучшения тестируемости можно рассмотреть передачу vault как зависимости в Server или добавить функцию сброса для тестов.

♻️ Опциональное улучшение для тестируемости
+// resetVaultForTesting resets the vault singleton. For tests only.
+func resetVaultForTesting() {
+	vaultOnce = sync.Once{}
+	sharedVault = nil
+	vaultInitErr = nil
+}

Или переместить vault в поле Server и инициализировать при создании сервера.

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

In `@internal/mcp/tools_credential.go` around lines 15 - 36, The package-level
vault (sharedVault, vaultInitErr, vaultOnce) makes tests brittle; add a
test-only reset function (e.g., ResetVaultForTests) in the same package that
reinitializes vaultOnce (assign sync.Once{}), and clears sharedVault and
vaultInitErr so getVault can be rerun in tests, and mark it with a test-only
build tag (or a //go:build test) so it is not included in production;
alternatively, refactor to inject the vault into Server (add a Vault field on
Server and initialize it during Server creation instead of using getVault) and
update call-sites that reference getVault to use server.Vault.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/mcp/tools_credential.go`:
- Around line 60-68: When params.Scope is "project" we must enforce that
params.Project is non-empty; update the validation around the existing switch on
params.Scope to return an error (e.g., fmt.Errorf("project must be provided when
scope==\"project\"")) if params.Scope == "project" && params.Project == "";
apply the same check at the other similar validation site referenced (the other
scope check) so credentials cannot be stored without a project association.

In `@internal/search/manager.go`:
- Around line 727-738: warmFrequentQueries currently caches raw results from
executeSearch (via m.putInCache) and can leak credentials; update
warmFrequentQueries to mirror UnifiedSearch by filtering the executeSearch
return before caching: call result, err := m.executeSearch(...), then create
filtered := filterCredentials(result) (ensuring you return a new struct to avoid
mutating shared singleflight results) and call m.putInCache(cacheKey, filtered)
instead of caching the unfiltered result.

---

Duplicate comments:
In `@internal/db/gorm/observation_store.go`:
- Around line 1248-1274: DeleteCredential currently deletes all matching
observations by name+scope; instead first query for a single Observation
(respecting the same scope logic) using
s.db.WithContext(ctx).Where(...).First(&obs) and if found call
s.db.WithContext(ctx).Delete(&Observation{}, obs.ID) so only that specific
record is removed; keep the existing scope defaulting logic and error handling
(return not-found if First returns gorm.ErrRecordNotFound, wrap other DB errors
similarly) and reference the DeleteCredential method, ObservationStore receiver,
and Observation model/ID when making the change.

In `@internal/mcp/tools_credential.go`:
- Around line 272-280: handleVaultStatus currently calls getVault() which can
create a new vault key as a side effect; replace that with a passive existence
check to avoid creating files during a read-only status request. Add a new
crypto.VaultKeyExists(cfg *config.Config) bool (or similar) that checks
cfg.EncryptionKey, cfg.EncryptionKeyFile, and the presence of the data-dir
vault.key without creating anything, then change handleVaultStatus to call
VaultKeyExists instead of getVault() and only call v.Fingerprint() when an
existing vault instance is explicitly loaded elsewhere; ensure you reference the
getVault and handleVaultStatus symbols when making the change.
- Around line 247-255: When scope is "project" the code must validate that
params.Project is non-empty (same as handleStoreCredential); add a check (e.g.
if params.Scope == "project" && params.Project == "") that returns an error like
fmt.Errorf("missing project for scope \"project\"") before proceeding to delete
so the deletion cannot run with an empty project and accidentally target the
wrong credential; mirror the validation logic used in handleStoreCredential and
reference params.Scope and params.Project in your change.

In `@internal/search/manager.go`:
- Around line 749-753: В цикле, где фильтруются результаты (перебор
result.Results), проверка должна опираться не на r.Type, а на реальный тип
наблюдения, который observationToResult() кладёт в Metadata["obs_type"];
замените условие r.Type != "credential" на проверку значения в r.Metadata —
например пропускайте/фильтруйте записи, у которых r.Metadata["obs_type"] ==
ObsTypeCredential — при этом защититесь от отсутствующего ключа или нестрокового
значения (проверить наличие и привести к string) перед сравнением; используйте
существующие символы observationToResult(), ObsTypeCredential и
result.Results/filtered для локализации правки.
- Line 905: The current single boolean expression mishandles params.Scope;
replace the inline call to vector.BuildWhereFilter with explicit handling of the
three scope cases: if params.Scope == "global" call
vector.BuildWhereFilter(docType, "", true) to return only global results; else
if params.Scope == "project" call vector.BuildWhereFilter(docType,
params.Project, false) to return only project results; else (params.Scope == "")
call vector.BuildWhereFilter(docType, params.Project, true) to return both
project and global results. Use the params.Scope, params.Project, and
vector.BuildWhereFilter symbols to locate and update the code.

---

Nitpick comments:
In `@internal/mcp/tools_credential.go`:
- Around line 15-36: The package-level vault (sharedVault, vaultInitErr,
vaultOnce) makes tests brittle; add a test-only reset function (e.g.,
ResetVaultForTests) in the same package that reinitializes vaultOnce (assign
sync.Once{}), and clears sharedVault and vaultInitErr so getVault can be rerun
in tests, and mark it with a test-only build tag (or a //go:build test) so it is
not included in production; alternatively, refactor to inject the vault into
Server (add a Vault field on Server and initialize it during Server creation
instead of using getVault) and update call-sites that reference getVault to use
server.Vault.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: edfa3355-7960-442f-b583-2561fae39074

📥 Commits

Reviewing files that changed from the base of the PR and between cee146f and 5a66818.

📒 Files selected for processing (5)
  • TECHNICAL_DEBT.md
  • internal/crypto/vault.go
  • internal/db/gorm/observation_store.go
  • internal/mcp/tools_credential.go
  • internal/search/manager.go
✅ Files skipped from review due to trivial changes (1)
  • TECHNICAL_DEBT.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • internal/crypto/vault.go

Comment thread internal/mcp/tools_credential.go
Comment thread internal/search/manager.go
- S2: add key_source field to vault_status (env/file/auto_generated)
- S3: simplify GetCredential/DeleteCredential WHERE to title-only
- S7: log migration 031 rollback errors instead of swallowing
- S8: deduplicate expandTagHierarchy — tools_memory.go calls shared func
- Add credential management section to engramInstructions
- Add credential tools documentation to SKILL.md
- Mark resolved items in TECHNICAL_DEBT.md
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (8)
internal/crypto/vault.go (1)

58-82: ⚠️ Potential issue | 🔴 Critical

Ошибка os.Stat, отличная от отсутствия файла, приводит к генерации нового ключа.

Блок else на строке 67 обрабатывает ВСЕ ошибки os.Stat как "файл отсутствует". Если vault.key существует, но Stat падает из-за недостатка прав или I/O ошибки, код сгенерирует новый ключ и перезапишет существующий, делая все сохранённые credentials недешифруемыми.

Необходимо явно проверять os.IsNotExist(statErr) перед генерацией нового ключа.
,

🐛 Предлагаемое исправление
 	default:
 		keyFile := filepath.Join(config.DataDir(), "vault.key")
-		if _, statErr := os.Stat(keyFile); statErr == nil {
+		_, statErr := os.Stat(keyFile)
+		switch {
+		case statErr == nil:
 			key, err = loadKeyFromFile(keyFile)
 			if err != nil {
 				return nil, fmt.Errorf("load auto-generated key from %q: %w", keyFile, err)
 			}
 			log.Info().Str("file", keyFile).Msg("vault: loaded auto-generated key")
 			return &Vault{key: key, fingerprint: computeFingerprint(key), source: "auto_generated"}, nil
-		} else {
+		case os.IsNotExist(statErr):
 			key = make([]byte, 32)
 			if _, err = io.ReadFull(rand.Reader, key); err != nil {
 				return nil, fmt.Errorf("generate encryption key: %w", err)
 			}
 			if err = os.MkdirAll(filepath.Dir(keyFile), 0700); err != nil {
 				return nil, fmt.Errorf("create key directory %q: %w", filepath.Dir(keyFile), err)
 			}
 			if err = saveKeyToFile(keyFile, key); err != nil {
 				return nil, fmt.Errorf("save generated key to %q: %w", keyFile, err)
 			}
 			log.Warn().
 				Str("file", keyFile).
 				Msg("vault: auto-generated new encryption key — BACK UP THIS FILE to avoid losing access to stored credentials")
 			return &Vault{key: key, fingerprint: computeFingerprint(key), source: "auto_generated"}, nil
+		default:
+			return nil, fmt.Errorf("stat vault key %q: %w", keyFile, statErr)
 		}
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/crypto/vault.go` around lines 58 - 82, The os.Stat error handling in
the default branch incorrectly treats any statErr as "file not found" and
proceeds to generate and overwrite a key; update the logic around the
os.Stat(keyFile) result so you only generate a new key when
os.IsNotExist(statErr) is true, and for any other non-nil statErr return an
error (propagate a wrapped error) instead of creating a new key; locate the
check around keyFile/os.Stat, and adjust the control flow that currently calls
loadKeyFromFile, make([]byte, 32), saveKeyToFile, and returns a Vault to handle
the non-existence case separately and error out on other statErr values.
internal/mcp/tools_memory.go (4)

23-31: ⚠️ Potential issue | 🟠 Major

Параметр importance по-прежнему декоративный.

Поле парсится и рекламируется в schema, но на сохранённую observation не влияет: StoreObservation вызывается как раньше, а значение приоритета нигде не применяется.

💡 Минимальная правка
 	var params struct {
 		Tags       []string `json:"tags"`
 		Content    string   `json:"content"`
 		Title      string   `json:"title"`
 		Type       string   `json:"type"`
 		Scope      string   `json:"scope"`
 		Project    string   `json:"project"`
-		Importance float64  `json:"importance"`
+		Importance *float64 `json:"importance"`
 	}
@@
 	id, _, err := s.observationStore.StoreObservation(ctx, "", params.Project, obs, 0, 0)
 	if err != nil {
 		return "", fmt.Errorf("store observation: %w", err)
 	}
+	if params.Importance != nil {
+		if *params.Importance < 0 || *params.Importance > 1 {
+			return "", fmt.Errorf("importance must be between 0 and 1")
+		}
+		if err := s.observationStore.UpdateImportanceScore(ctx, id, *params.Importance); err != nil {
+			return "", fmt.Errorf("set importance: %w", err)
+		}
+	}

Also applies to: 143-145

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

In `@internal/mcp/tools_memory.go` around lines 23 - 31, The incoming params
struct includes an Importance field but it's never applied when saving; update
the code path that calls StoreObservation so the parsed params.Importance is
propagated to the saved observation (e.g., set observation.Priority =
params.Importance or pass it as an argument into StoreObservation) — locate
where params is used (the params struct definition and the call site to
StoreObservation) and ensure the importance value is applied to the observation
before persisting; also mirror this change for the other occurrence noted around
lines 143-145 where StoreObservation is invoked.

53-57: ⚠️ Potential issue | 🟠 Major

Лимиты и усечение здесь считаются в байтах, а не в символах.

len(string) и s[:n] в Go работают по байтам, поэтому многобайтный UTF-8 текст может обрезаться раньше обещанного лимита и даже посередине руны. Переведите эти проверки и усечения на utf8.RuneCountInString/[]rune для content, truncateTitle и текстового preview.

#!/bin/bash
rg -n 'len\(params\.Content\)|params\.Content = params\.Content\[:softLimit\]|if len\(content\) <= maxLen|truncated := content\[:maxLen\]|if len\(content\) > 300|content = content\[:300\]' internal/mcp/tools_memory.go

Also applies to: 165-169, 281-282

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

In `@internal/mcp/tools_memory.go` around lines 53 - 57, The byte-based length
checks and slicing for params.Content (using len(params.Content) and
params.Content[:softLimit]) must be converted to rune-aware operations: replace
byte-counting with utf8.RuneCountInString for comparisons and convert strings to
[]rune for truncation, e.g., in the blocks around the hardLimit/softLimit checks
and the other occurrences you flagged (including truncateTitle and any
preview/truncated variables at the later sites), so you compare rune counts and
slice rune slices to the desired max runes rather than slicing bytes.

181-210: ⚠️ Potential issue | 🟠 Major

recall_memory по-прежнему не умеет искать в project-скоупе.

В handler нет поля project, а search.SearchParams собирается без Project, поэтому поиск уходит с пустым project и снимает ожидаемое ограничение current + global. Добавьте project в параметры handler и синхронизируйте InputSchema в internal/mcp/server.go.

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

In `@internal/mcp/tools_memory.go` around lines 181 - 210, Handler for
recall_memory lacks a project scope so searches run with empty project and
include global results; update the handler and schemas to accept and forward
project: add a Project string `json:"project"` field to the local params struct
used in the recall_memory handler, populate search.SearchParams.Project =
params.Project when building searchParams (in the same function that constructs
search.SearchParams), and update the handler's InputSchema in
internal/mcp/server.go to include the project field so the API/validation and
server-side handler stay in sync.

29-30: ⚠️ Potential issue | 🟠 Major

store_memory всё ещё сохраняет в "" и дедупит по неправильному скоупу.

project из контракта здесь не нормализуется: пустая строка уходит и в StoreObservation, и в BuildWhereFilter. В итоге память может записываться под "", dedup при пустом project теряет project-фильтр целиком, а при непустом includeGlobal=false всё равно пропускает global memories, которые потом будут видны через recall/search.

Also applies to: 113-116, 143-143

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

In `@internal/mcp/tools_memory.go` around lines 29 - 30, The bug is that the
incoming contract's project string is not normalized before being used, so code
paths in store_memory, StoreObservation, and BuildWhereFilter receive an empty
"" project which causes incorrect storage under "" and wrong dedup/filter scopes
(and includeGlobal logic bypasses project checks). Fix by normalizing the
contract project once at the start of the store_memory flow (e.g., compute a
normalizedProject or nil-equivalent), then pass that normalized value into
StoreObservation, BuildWhereFilter, and any dedup/includeGlobal checks (instead
of the raw contract.Project) so dedup and recall/search honor the correct
project scope. Ensure all usages referenced (store_memory, StoreObservation,
BuildWhereFilter and includeGlobal handling) are changed to use the normalized
value.
plugin/engram/skills/memory/SKILL.md (1)

48-48: ⚠️ Potential issue | 🟡 Minor

Здесь всё ещё указан несуществующий tool merge.

В MCP экспортируется merge_observations, поэтому эта инструкция снова приведёт к tool not found. Замените merge на merge_observations.

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

In `@plugin/engram/skills/memory/SKILL.md` at line 48, The documentation
references a non-existent tool name "merge" (seen in the line "Found duplicate
memories?   → suggest_consolidations / merge"); update that reference to the
exported tool name "merge_observations" so the instruction matches the actual
MCP export; search for and replace any occurrences of the symbol "merge" in
SKILL.md (or this specific line) with "merge_observations" to prevent "tool not
found" errors.
internal/mcp/server.go (1)

1646-1647: ⚠️ Potential issue | 🟠 Major

Similarity-поиск всё ещё скрывает global memories.

Оба вызова BuildWhereFilter(..., params.Project, false) противоречат Phase 3: find_similar_observations и suggest_consolidations по-прежнему не видят scope='global', из-за чего будут пропускаться глобальные дубликаты.

💡 Минимальная правка
-	where := vector.BuildWhereFilter(vector.DocTypeObservation, params.Project, false)
+	where := vector.BuildWhereFilter(vector.DocTypeObservation, params.Project, true)
...
-		where := vector.BuildWhereFilter(vector.DocTypeObservation, params.Project, false)
+		where := vector.BuildWhereFilter(vector.DocTypeObservation, params.Project, true)

Also applies to: 2391-2392

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

In `@internal/mcp/server.go` around lines 1646 - 1647, The similarity search is
excluding global memories because BuildWhereFilter is called with the local-only
flag set to false; update the calls to
vector.BuildWhereFilter(vector.DocTypeObservation, params.Project, true) so the
filter includes scope='global' (fix the occurrences used by
find_similar_observations and suggest_consolidations that call
s.vectorClient.Query with the resulting where) to ensure global observations are
visible to the similarity queries.
internal/db/gorm/migrations.go (1)

1090-1102: ⚠️ Potential issue | 🟠 Major

Откат 031_credential_storage всё ещё может “успешно” завершиться в полуоткаченном состоянии.

Если в observations уже есть строки с type = 'credential', восстановление старого chk_observations_type снова упадёт. Сейчас ошибка только логируется, а rollback возвращает nil, поэтому мигратор посчитает откат успешным при несовместимой схеме. Либо очистите/перепишите такие строки перед восстановлением constraint, либо сделайте rollback явно необратимым и возвращайте ошибку.

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

In `@internal/db/gorm/migrations.go` around lines 1090 - 1102, The Rollback
function in migration 031_credential_storage currently logs errors but returns
nil, allowing a "half-rolled-back" state if rows with type = 'credential' exist
and the ALTER ... ADD CONSTRAINT chk_observations_type fails; fix it by making
the rollback deterministic: either (A) clean or rewrite incompatible rows before
re-adding the constraint (e.g., run an UPDATE/DELETE on observations where
type='credential' prior to the ALTER in the Rollback function identified by
Rollback and sqls), or (B) make the rollback fail fast by returning the error
when tx.Exec(s).Error is non-nil so the migrator treats the rollback as failed
(replace the log-only handling in Rollback with an immediate return err). Ensure
you modify the Rollback logic that constructs sqls and the loop over tx.Exec to
implement one of these approaches.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@internal/crypto/vault.go`:
- Around line 58-82: The os.Stat error handling in the default branch
incorrectly treats any statErr as "file not found" and proceeds to generate and
overwrite a key; update the logic around the os.Stat(keyFile) result so you only
generate a new key when os.IsNotExist(statErr) is true, and for any other
non-nil statErr return an error (propagate a wrapped error) instead of creating
a new key; locate the check around keyFile/os.Stat, and adjust the control flow
that currently calls loadKeyFromFile, make([]byte, 32), saveKeyToFile, and
returns a Vault to handle the non-existence case separately and error out on
other statErr values.

In `@internal/db/gorm/migrations.go`:
- Around line 1090-1102: The Rollback function in migration
031_credential_storage currently logs errors but returns nil, allowing a
"half-rolled-back" state if rows with type = 'credential' exist and the ALTER
... ADD CONSTRAINT chk_observations_type fails; fix it by making the rollback
deterministic: either (A) clean or rewrite incompatible rows before re-adding
the constraint (e.g., run an UPDATE/DELETE on observations where
type='credential' prior to the ALTER in the Rollback function identified by
Rollback and sqls), or (B) make the rollback fail fast by returning the error
when tx.Exec(s).Error is non-nil so the migrator treats the rollback as failed
(replace the log-only handling in Rollback with an immediate return err). Ensure
you modify the Rollback logic that constructs sqls and the loop over tx.Exec to
implement one of these approaches.

In `@internal/mcp/server.go`:
- Around line 1646-1647: The similarity search is excluding global memories
because BuildWhereFilter is called with the local-only flag set to false; update
the calls to vector.BuildWhereFilter(vector.DocTypeObservation, params.Project,
true) so the filter includes scope='global' (fix the occurrences used by
find_similar_observations and suggest_consolidations that call
s.vectorClient.Query with the resulting where) to ensure global observations are
visible to the similarity queries.

In `@internal/mcp/tools_memory.go`:
- Around line 23-31: The incoming params struct includes an Importance field but
it's never applied when saving; update the code path that calls StoreObservation
so the parsed params.Importance is propagated to the saved observation (e.g.,
set observation.Priority = params.Importance or pass it as an argument into
StoreObservation) — locate where params is used (the params struct definition
and the call site to StoreObservation) and ensure the importance value is
applied to the observation before persisting; also mirror this change for the
other occurrence noted around lines 143-145 where StoreObservation is invoked.
- Around line 53-57: The byte-based length checks and slicing for params.Content
(using len(params.Content) and params.Content[:softLimit]) must be converted to
rune-aware operations: replace byte-counting with utf8.RuneCountInString for
comparisons and convert strings to []rune for truncation, e.g., in the blocks
around the hardLimit/softLimit checks and the other occurrences you flagged
(including truncateTitle and any preview/truncated variables at the later
sites), so you compare rune counts and slice rune slices to the desired max
runes rather than slicing bytes.
- Around line 181-210: Handler for recall_memory lacks a project scope so
searches run with empty project and include global results; update the handler
and schemas to accept and forward project: add a Project string `json:"project"`
field to the local params struct used in the recall_memory handler, populate
search.SearchParams.Project = params.Project when building searchParams (in the
same function that constructs search.SearchParams), and update the handler's
InputSchema in internal/mcp/server.go to include the project field so the
API/validation and server-side handler stay in sync.
- Around line 29-30: The bug is that the incoming contract's project string is
not normalized before being used, so code paths in store_memory,
StoreObservation, and BuildWhereFilter receive an empty "" project which causes
incorrect storage under "" and wrong dedup/filter scopes (and includeGlobal
logic bypasses project checks). Fix by normalizing the contract project once at
the start of the store_memory flow (e.g., compute a normalizedProject or
nil-equivalent), then pass that normalized value into StoreObservation,
BuildWhereFilter, and any dedup/includeGlobal checks (instead of the raw
contract.Project) so dedup and recall/search honor the correct project scope.
Ensure all usages referenced (store_memory, StoreObservation, BuildWhereFilter
and includeGlobal handling) are changed to use the normalized value.

In `@plugin/engram/skills/memory/SKILL.md`:
- Line 48: The documentation references a non-existent tool name "merge" (seen
in the line "Found duplicate memories?   → suggest_consolidations / merge");
update that reference to the exported tool name "merge_observations" so the
instruction matches the actual MCP export; search for and replace any
occurrences of the symbol "merge" in SKILL.md (or this specific line) with
"merge_observations" to prevent "tool not found" errors.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e5721199-c26e-424b-b66f-d2b30ef410b2

📥 Commits

Reviewing files that changed from the base of the PR and between 5a66818 and 840ca3c.

📒 Files selected for processing (8)
  • TECHNICAL_DEBT.md
  • internal/crypto/vault.go
  • internal/db/gorm/migrations.go
  • internal/db/gorm/observation_store.go
  • internal/mcp/server.go
  • internal/mcp/tools_credential.go
  • internal/mcp/tools_memory.go
  • plugin/engram/skills/memory/SKILL.md
✅ Files skipped from review due to trivial changes (1)
  • TECHNICAL_DEBT.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • internal/mcp/tools_credential.go

Go hooks now compute GitRemoteProjectID (8-char, cross-platform) with
LegacyProjectID fallback. Both JS and Go session-start hooks send
legacy_project, git_remote, relative_path to the server.

handleContextInject reads these params and calls UpsertProject async,
which re-associates existing observations from old path-based IDs to
new git-remote-based IDs (idempotent, non-blocking).

Session parser ProjectID now includes dirName_ prefix to match hook
format.
@thebtf
Copy link
Copy Markdown
Owner Author

thebtf commented Mar 11, 2026

@codex review

Security:
- SQL injection prevention: allowlist for WhereClause.Column in pgvector
- Credential leak prevention: json:"-" on encryption config fields
- Prompt injection prevention: XML-escape memory content in user-prompt hook
- Cache leak fix: filter credentials before caching in warmFrequentQueries

Correctness:
- Fix pq.StringArray for PostgreSQL TEXT[] column (Project.LegacyIDs)
- Fix BuildWhereFilter scope handling (global/project/both)
- Fix credential filtering: check obs_type metadata, not just Type field
- Wire importance parameter to UpdateImportanceScore in store_memory
- Fix recall_memory project scope propagation
- Fix dedup to include global observations

Robustness:
- Add context timeout to UpsertProject background goroutine
- Add 3s timeout to git commands in session parser
- Fix os.Stat error handling in vault (don't overwrite on permission errors)
- Add VaultExists helper for passive vault checks
- UTF-8 rune-aware content limits in store_memory
- Safe migration 031 rollback with credential type conversion
- Collect searchIds after dedup/trimming in user-prompt hook
@thebtf
Copy link
Copy Markdown
Owner Author

thebtf commented Mar 12, 2026

🤖 PR Review MCP State (auto-managed, do not edit)
{
  "version": 2,
  "parentChildren": {},
  "resolvedNitpicks": {
    "coderabbit-nitpick-863e1856-15": {
      "resolvedAt": "2026-03-12T00:10:01.950Z",
      "resolvedBy": "agent"
    }
  },
  "updatedAt": "2026-03-12T00:10:02.589Z"
}

@thebtf thebtf merged commit 0d6c72d into main Mar 12, 2026
2 checks passed
thebtf added a commit that referenced this pull request Apr 5, 2026
User prompt context:
- Server-side session prompt cache (sync.Map with 2h eviction)
- handleSessionInit stores last user prompt per session
- ProcessObservation forwards user prompt to BuildObservationPrompt
- <user_intent> tag added to extraction prompt — LLM now sees WHY
  the agent performed an action, not just WHAT it did

Mid-session extract-learnings:
- PreCompact hook calls /api/sessions/{id}/extract-learnings with
  last 20 messages (4000 token budget)
- Idempotency: skips if guidance observations already extracted for session
- Fire-and-forget (Constitution #3: non-blocking hooks)

Data model:
- UserPrompt field added to ObservationData (session manager queue)
- CountBySessionAndType for idempotency checks
@thebtf thebtf deleted the feat/memory-evolution branch May 7, 2026 06:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant