Skip to content

feat: always-inject tier for behavioral rules#53

Merged
thebtf merged 2 commits into
mainfrom
feat/always-inject
Mar 24, 2026
Merged

feat: always-inject tier for behavioral rules#53
thebtf merged 2 commits into
mainfrom
feat/always-inject

Conversation

@thebtf
Copy link
Copy Markdown
Owner

@thebtf thebtf commented Mar 24, 2026

Summary

  • Three-tier injection: observations tagged always-inject concept injected unconditionally in every session
  • GIN indexes on concepts, files_modified, files_read for fast JSONB queries
  • Configurable limits: ENGRAM_ALWAYS_INJECT_LIMIT (default 20), ENGRAM_PROJECT_INJECT_LIMIT (default 15)
  • Hooks render <user-behavior-rules> block BEFORE context/memory blocks
  • Plugin version bumped to 0.6.0

Test plan

  • Tag behavioral rules with always-inject concept via MCP tools
  • Start new session → verify <user-behavior-rules> appears
  • Verify rules appear regardless of prompt content
  • Verify GIN indexes created on server startup (migration 048)
  • Verify no regression in existing context injection

Summary by CodeRabbit

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

    • Добавлена функция автоматического включения релевантных наблюдений в контекст диалога
    • Реализована поддержка постоянно активных правил поведения системы
  • Улучшения

    • Оптимизирована производительность работы с базой данных через добавление индексов
    • Обновлена версия плагина до 0.6.0

thebtf added 2 commits March 24, 2026 19:40
PreCompact hook was created but never registered in hooks.json.
Now registered with 10s timeout. Hook writes discovery data to
.agent/pre-compact-discovery.json for empirical testing of
available input fields (transcript_path verification for FR-2).
Three-tier injection system: observations tagged with concept
"always-inject" are now fetched independently of similarity
matching and included in every session (session-start) and
every prompt (user-prompt) context.

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

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

Also adds GetObservationsByFile and GetPreviousObservationInSession
queries for Phase 3 and Phase 4 (no callers yet).
@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 the context injection mechanism by introducing an 'always-inject' tier for behavioral rules, ensuring critical instructions are consistently present. It also improves database query performance for observation retrieval through new GIN indexes and provides greater control over context injection limits. These changes aim to make the system more reliable and efficient in delivering relevant information.

Highlights

  • Three-tier Injection: Observations tagged with the 'always-inject' concept are now unconditionally injected in every session, forming a new tier of context.
  • GIN Indexes for Performance: New GIN indexes have been added to the concepts, files_modified, and files_read JSONB fields in the observations table to enable faster queries.
  • Configurable Injection Limits: New configuration options, ENGRAM_ALWAYS_INJECT_LIMIT (default 20) and ENGRAM_PROJECT_INJECT_LIMIT (default 15), have been introduced to control the maximum number of observations injected for each tier.
  • Behavioral Rules Placement: The <user-behavior-rules> block is now rendered BEFORE context/memory blocks in the prompt, ensuring these rules take precedence.
  • Plugin Version Bump: The plugin version has been updated to 0.6.0.
Using Gemini Code Assist

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

Invoking Gemini

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

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

Customization

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

Limitations & Feedback

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

Footnotes

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

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 24, 2026

Обзор

В этом PR добавляется функция "always-inject" для извлечения и внедрения наблюдений на основе концепций и файлов. Реализация включает два новых параметра конфигурации, миграцию базы данных с индексами, три метода запроса к хранилищу, обновления обработчиков контекста и интеграцию с хуками плагина для управления поведением.

Изменения

Когорта / Файлы Резюме
Конфигурация
internal/config/config.go
Добавлены два новых целочисленных поля: AlwaysInjectLimit (по умолчанию 20) и ProjectInjectLimit (по умолчанию 15). Оба поля инициализируются в Default() и читаются из переменных окружения в Load() с валидацией через strconv.Atoi.
Миграция БД
internal/db/gorm/migrations.go
Добавлена миграция 048, создающая три GIN-индекса на столбцы concepts, files_modified и files_read, а также составной индекс на (sdk_session_id, prompt_number DESC) с фильтром для несуперсеженных строк.
Методы хранилища наблюдений
internal/db/gorm/observation_store.go
Добавлены три новых метода: GetAlwaysInjectObservations() для извлечения наблюдений с концепцией "always-inject", GetObservationsByFile() для поиска по пути файла и GetPreviousObservationInSession() для получения предыдущего наблюдения в сессии.
Обработчики контекста
internal/worker/handlers_context.go
Добавлена логика извлечения "always-inject" наблюдений в handleSearchByPrompt и handleContextInject с дедупликацией в последнем. Результаты включены в новое поле always_inject в ответах обоих обработчиков.
Конфигурация плагина и хуки
plugin/engram/.claude-plugin/plugin.json, plugin/engram/hooks/hooks.json
Обновлена версия плагина с 0.5.1 на 0.6.0. Добавлена новая фаза хука PreCompact с командой для выполнения pre-compact.js.
Хук предварительной компактизации
plugin/engram/hooks/pre-compact.js
Новый скрипт, регистрирующий хук PreCompact для сбора информации о доступных ключах в контексте и входных данных, записи в файл .agent/pre-compact-discovery.json.
Обновления существующих хуков
plugin/engram/hooks/session-start.js, plugin/engram/hooks/user-prompt.js
session-start.js теперь добавляет опциональный блок <user-behavior-rules> с правилами "always-inject". user-prompt.js объединяет правила "always-inject" с правилами на основе подобия, дедуплицируя по rule.id.

Оценка трудозатрат на рецензирование кода

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

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

  • security: auth consolidation, vault singleton, cwd hardening, LLM prompt sanitization #10: Оба PR модифицируют одни и те же функции-обработчики в internal/worker/handlers_context.go — этот PR добавляет уровень извлечения "always-inject", а связанный PR изменяет переопределение рабочей директории в тех же обработчиках.
  • feat: self-learning utility signals #1: Оба PR затрагивают относящиеся к внедрению потоки (хуки плагина и обработчики рабочего для отслеживания/внедрения наблюдений) на уровне кода.

Предлагаемые метки

ai-review:passed

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

🐰 Морковка данных — "всегда внедри",
Индексы танцуют в строках БД,
Концепции с файлами в сети,
Хуки поют о судьбе мечты,
Версия шестая спешит сюда! 🥕✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main feature: introducing an always-inject tier for behavioral rules, which is the primary objective across multiple files in the changeset.

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

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

Warning

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

🔧 golangci-lint (2.11.3)

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


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

@gemini-code-assist
Copy link
Copy Markdown

Warning

Gemini encountered an error creating the review. You can try again by commenting /gemini review.

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: 6

🤖 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 588-592: The config currently reads ENGRA M_PROJECT_INJECT_LIMIT
into cfg.ProjectInjectLimit but that field is unused; either remove the
ProjectInjectLimit field and its environment parsing (the block in config.go and
the struct field ProjectInjectLimit) or implement its enforcement where project
items are injected (e.g., in the code paths that enqueue or process
project-level injections — locate functions like the project injection/ingest or
enqueuing routines and apply a limit check against cfg.ProjectInjectLimit before
creating/injecting items), and update any related tests and configuration docs
accordingly.

In `@internal/db/gorm/observation_store.go`:
- Around line 488-505: GetObservationsByFile currently returns observations
across all projects for a given file path causing potential data leakage; update
the function signature (GetObservationsByFile) to accept a project identifier
(e.g., projectID string or uint), add a WHERE clause to the GORM query to filter
by the project column (e.g., "project_id = ?"), and propagate the new parameter
to all callers so only observations with the matching project_id are returned
while keeping the existing activeObservationFilter(), importanceOrdering(), and
JSONB file matching logic intact.
- Around line 491-496: GetObservationsByFile in ObservationStore builds fileJSON
via fmt.Sprintf which doesn't escape special characters; replace that with a
proper JSON marshal of a string array (e.g. json.Marshal([]string{filePath}))
and use the resulting bytes/string for fileJSON, handling the marshal error and
returning it if non-nil so the DB Where("files_modified @> ? OR files_read @>
?", fileJSON, fileJSON) receives valid JSON-escaped input.
- Around line 470-486: Change GetAlwaysInjectObservations to accept a project
string and apply projectScopeFilter(project) so results include project-scoped
and global observations only; specifically, update the function signature to
func (s *ObservationStore) GetAlwaysInjectObservations(ctx context.Context,
project string, limit int) ([]*models.Observation, error) and add
Scopes(projectScopeFilter(project), activeObservationFilter(),
importanceOrdering()) to the query chain, then update all call sites that invoke
GetAlwaysInjectObservations to pass the current project value.

In `@plugin/engram/hooks/pre-compact.js`:
- Around line 37-45: The report path is built from __dirname so it ends up under
plugin/ rather than the repo root; change construction of logPath to join the
repository CWD from the hook context (e.g., ctx.CWD) with
'.agent/pre-compact-discovery.json' instead of using __dirname so the file is
written to the project root; update the code that creates dirs and writes
(fs.mkdirSync, fs.writeFileSync) to use this new logPath variable and keep the
same error logging behavior.

In `@plugin/engram/hooks/user-prompt.js`:
- Around line 86-96: The current dedup loop (handling allBehaviorRules ->
uniqueRules using seenRuleIds) skips deduping when rule.id is null, allowing
duplicates to be added; update the logic in the loop that walks allBehaviorRules
to apply a fallback deduplication key when rule.id is null by using a stable
identifier such as rule.title (or another unique stringified field) — e.g.
compute const key = (typeof rule.id === 'number' ? `id:${rule.id}` :
`title:${String(rule.title||'')}`) and track seen keys in seenRuleIds (or rename
to seenRuleKeys) so rules from alwaysInjectRules and behaviorRules without
numeric ids are still deduplicated before pushing to uniqueRules; ensure you
handle missing title gracefully (empty string) to avoid runtime errors.
🪄 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: c3f10c55-7bc3-498c-8747-6d0063e71ad9

📥 Commits

Reviewing files that changed from the base of the PR and between 476f992 and 715e7c0.

📒 Files selected for processing (9)
  • internal/config/config.go
  • internal/db/gorm/migrations.go
  • internal/db/gorm/observation_store.go
  • internal/worker/handlers_context.go
  • plugin/engram/.claude-plugin/plugin.json
  • plugin/engram/hooks/hooks.json
  • plugin/engram/hooks/pre-compact.js
  • plugin/engram/hooks/session-start.js
  • plugin/engram/hooks/user-prompt.js

Comment thread internal/config/config.go
Comment on lines +588 to +592
if v := strings.TrimSpace(os.Getenv("ENGRAM_PROJECT_INJECT_LIMIT")); v != "" {
if n, err := strconv.Atoi(v); err == nil && n > 0 {
cfg.ProjectInjectLimit = n
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Поиск использования ProjectInjectLimit в кодовой базе
rg -n "ProjectInjectLimit" --type=go

Repository: thebtf/engram

Length of output: 385


Удалите или реализуйте использование неиспользуемого поля конфигурации.

Поле ProjectInjectLimit определено в структуре конфигурации и загружается из переменной окружения ENGRAM_PROJECT_INJECT_LIMIT, но по результатам поиска в кодовой базе оно не используется ни в каком коде. Либо удалите это поле, если оно больше не требуется, либо реализуйте его использование.

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

In `@internal/config/config.go` around lines 588 - 592, The config currently reads
ENGRA M_PROJECT_INJECT_LIMIT into cfg.ProjectInjectLimit but that field is
unused; either remove the ProjectInjectLimit field and its environment parsing
(the block in config.go and the struct field ProjectInjectLimit) or implement
its enforcement where project items are injected (e.g., in the code paths that
enqueue or process project-level injections — locate functions like the project
injection/ingest or enqueuing routines and apply a limit check against
cfg.ProjectInjectLimit before creating/injecting items), and update any related
tests and configuration docs accordingly.

Comment on lines +470 to +486
// GetAlwaysInjectObservations retrieves observations tagged with the "always-inject" concept.
// These are unconditionally injected in every session regardless of similarity matching.
// Results are ordered by importance_score DESC, limited to the configured cap.
func (s *ObservationStore) GetAlwaysInjectObservations(ctx context.Context, limit int) ([]*models.Observation, error) {
var dbObservations []Observation
err := s.db.WithContext(ctx).
Scopes(activeObservationFilter(), importanceOrdering()).
Where("concepts @> ?", `["always-inject"]`).
Limit(limit).
Find(&dbObservations).Error

if err != nil {
return nil, err
}

return toModelObservations(dbObservations), nil
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Find callers of GetAlwaysInjectObservations to verify project filtering intent

ast-grep --pattern 'GetAlwaysInjectObservations($$$)'

# Also check handler context for how this is used
rg -n -A 10 'GetAlwaysInjectObservations' --type=go

Repository: thebtf/engram

Length of output: 3044


🏁 Script executed:

# Check ObservationStore struct and how it's initialized
rg -n "type ObservationStore struct" -A 5 --type=go

# Check how other methods in this file filter by project
rg -n "project" internal/db/gorm/observation_store.go --type=go | head -30

# Check activeObservationFilter implementation
rg -n "activeObservationFilter" -A 10 --type=go

Repository: thebtf/engram

Length of output: 8586


🏁 Script executed:

# Check projectScopeFilter implementation
rg -n "projectScopeFilter" -A 8 --type=go

# Also check the handler context to see if project is available there
sed -n '460,475p' internal/worker/handlers_context.go
sed -n '1068,1085p' internal/worker/handlers_context.go

Repository: thebtf/engram

Length of output: 4681


Добавьте фильтр проекта в GetAlwaysInjectObservations

Функция не фильтрует по project, что приводит к возврату наблюдений со статусом always-inject из ВСЕХ проектов, независимо от контекста вызова. Это нарушает изоляцию данных между проектами.

Сравните с аналогичными методами (GetActiveObservations, GetGuidanceObservations), которые принимают параметр project и используют projectScopeFilter(project) для включения как проектных, так и глобальных наблюдений в пределах одного проекта.

Измените сигнатуру:

func (s *ObservationStore) GetAlwaysInjectObservations(ctx context.Context, project string, limit int) ([]*models.Observation, error) {
    // Добавьте Scopes(projectScopeFilter(project), ...)
}

Обновите обе вызывающие строки (handlers_context.go:464, 1072) для передачи доступного project.

🤖 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 470 - 486, Change
GetAlwaysInjectObservations to accept a project string and apply
projectScopeFilter(project) so results include project-scoped and global
observations only; specifically, update the function signature to func (s
*ObservationStore) GetAlwaysInjectObservations(ctx context.Context, project
string, limit int) ([]*models.Observation, error) and add
Scopes(projectScopeFilter(project), activeObservationFilter(),
importanceOrdering()) to the query chain, then update all call sites that invoke
GetAlwaysInjectObservations to pass the current project value.

Comment on lines +488 to +505
// GetObservationsByFile retrieves observations related to a specific file path.
// Matches against both files_modified and files_read JSONB arrays.
// Results are ordered by importance_score DESC.
func (s *ObservationStore) GetObservationsByFile(ctx context.Context, filePath string, limit int) ([]*models.Observation, error) {
var dbObservations []Observation
fileJSON := fmt.Sprintf(`["%s"]`, filePath)
err := s.db.WithContext(ctx).
Scopes(activeObservationFilter(), importanceOrdering()).
Where("files_modified @> ? OR files_read @> ?", fileJSON, fileJSON).
Limit(limit).
Find(&dbObservations).Error

if err != nil {
return nil, err
}

return toModelObservations(dbObservations), nil
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Отсутствует фильтрация по проекту — возможная утечка данных.

Функция GetObservationsByFile не принимает параметр project и не фильтрует результаты по проекту. Это означает, что запрос вернёт наблюдения из ВСЕХ проектов, которые ссылаются на указанный путь к файлу.

Учитывая, что пути к файлам могут совпадать между проектами (например, README.md, main.go), это может привести к нежелательной утечке информации между проектами.

♻️ Предлагаемое исправление с добавлением фильтра по проекту
-func (s *ObservationStore) GetObservationsByFile(ctx context.Context, filePath string, limit int) ([]*models.Observation, error) {
+func (s *ObservationStore) GetObservationsByFile(ctx context.Context, project, filePath string, limit int) ([]*models.Observation, error) {
 	var dbObservations []Observation
-	fileJSON := fmt.Sprintf(`["%s"]`, filePath)
-	err := s.db.WithContext(ctx).
-		Scopes(activeObservationFilter(), importanceOrdering()).
+	fileJSONBytes, _ := json.Marshal([]string{filePath})
+	fileJSON := string(fileJSONBytes)
+	err := s.db.WithContext(ctx).
+		Scopes(projectScopeFilter(project), activeObservationFilter(), importanceOrdering()).
 		Where("files_modified @> ? OR files_read @> ?", fileJSON, fileJSON).
🤖 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 488 - 505,
GetObservationsByFile currently returns observations across all projects for a
given file path causing potential data leakage; update the function signature
(GetObservationsByFile) to accept a project identifier (e.g., projectID string
or uint), add a WHERE clause to the GORM query to filter by the project column
(e.g., "project_id = ?"), and propagate the new parameter to all callers so only
observations with the matching project_id are returned while keeping the
existing activeObservationFilter(), importanceOrdering(), and JSONB file
matching logic intact.

Comment on lines +491 to +496
func (s *ObservationStore) GetObservationsByFile(ctx context.Context, filePath string, limit int) ([]*models.Observation, error) {
var dbObservations []Observation
fileJSON := fmt.Sprintf(`["%s"]`, filePath)
err := s.db.WithContext(ctx).
Scopes(activeObservationFilter(), importanceOrdering()).
Where("files_modified @> ? OR files_read @> ?", fileJSON, fileJSON).
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Критическая ошибка: отсутствует экранирование JSON для filePath.

Использование fmt.Sprintf для построения JSON-строки небезопасно. Если filePath содержит специальные символы (", \, управляющие символы), результирующий JSON будет невалидным, что приведёт к ошибке запроса или некорректному поведению.

🐛 Предлагаемое исправление с использованием json.Marshal
 func (s *ObservationStore) GetObservationsByFile(ctx context.Context, filePath string, limit int) ([]*models.Observation, error) {
 	var dbObservations []Observation
-	fileJSON := fmt.Sprintf(`["%s"]`, filePath)
+	fileJSONBytes, err := json.Marshal([]string{filePath})
+	if err != nil {
+		return nil, fmt.Errorf("marshal file path: %w", err)
+	}
+	fileJSON := string(fileJSONBytes)
 	err := s.db.WithContext(ctx).
 		Scopes(activeObservationFilter(), importanceOrdering()).
 		Where("files_modified @> ? OR files_read @> ?", fileJSON, fileJSON).
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func (s *ObservationStore) GetObservationsByFile(ctx context.Context, filePath string, limit int) ([]*models.Observation, error) {
var dbObservations []Observation
fileJSON := fmt.Sprintf(`["%s"]`, filePath)
err := s.db.WithContext(ctx).
Scopes(activeObservationFilter(), importanceOrdering()).
Where("files_modified @> ? OR files_read @> ?", fileJSON, fileJSON).
func (s *ObservationStore) GetObservationsByFile(ctx context.Context, filePath string, limit int) ([]*models.Observation, error) {
var dbObservations []Observation
fileJSONBytes, marshalErr := json.Marshal([]string{filePath})
if marshalErr != nil {
return nil, fmt.Errorf("marshal file path: %w", marshalErr)
}
fileJSON := string(fileJSONBytes)
err := s.db.WithContext(ctx).
Scopes(activeObservationFilter(), importanceOrdering()).
Where("files_modified @> ? OR files_read @> ?", fileJSON, fileJSON).
🤖 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 491 - 496,
GetObservationsByFile in ObservationStore builds fileJSON via fmt.Sprintf which
doesn't escape special characters; replace that with a proper JSON marshal of a
string array (e.g. json.Marshal([]string{filePath})) and use the resulting
bytes/string for fileJSON, handling the marshal error and returning it if
non-nil so the DB Where("files_modified @> ? OR files_read @> ?", fileJSON,
fileJSON) receives valid JSON-escaped input.

Comment on lines +37 to +45
// Write to file so agent can read it
const logPath = path.join(__dirname, '..', '..', '..', '.agent', 'pre-compact-discovery.json');
try {
fs.mkdirSync(path.dirname(logPath), { recursive: true });
fs.writeFileSync(logPath, JSON.stringify(report, null, 2));
console.error(`[pre-compact] Discovery written to ${logPath}`);
} catch (e) {
console.error(`[pre-compact] Failed to write log: ${e.message}`);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Путь к файлу отчёта может не соответствовать ожиданиям.

logPath вычисляется как path.join(__dirname, '..', '..', '..', '.agent', ...), что разрешается относительно директории хука (plugin/engram/hooks/). Результат: plugin/.agent/pre-compact-discovery.json, а не корень проекта.

Если .agent/ должен быть в корне проекта пользователя, рассмотрите использование ctx.CWD или аналогичного контекстного пути.

🔧 Предложение: использовать CWD из контекста
-  const logPath = path.join(__dirname, '..', '..', '..', '.agent', 'pre-compact-discovery.json');
+  // Если CWD доступен в контексте, использовать его как базу
+  const projectRoot = ctx && ctx.CWD ? ctx.CWD : path.join(__dirname, '..', '..', '..');
+  const logPath = path.join(projectRoot, '.agent', 'pre-compact-discovery.json');
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plugin/engram/hooks/pre-compact.js` around lines 37 - 45, The report path is
built from __dirname so it ends up under plugin/ rather than the repo root;
change construction of logPath to join the repository CWD from the hook context
(e.g., ctx.CWD) with '.agent/pre-compact-discovery.json' instead of using
__dirname so the file is written to the project root; update the code that
creates dirs and writes (fs.mkdirSync, fs.writeFileSync) to use this new logPath
variable and keep the same error logging behavior.

Comment on lines +86 to +96
// Merge always-inject + similarity-matched behavioral rules into one block
const allBehaviorRules = [...alwaysInjectRules, ...behaviorRules];
// Deduplicate by ID (always-inject may overlap with similarity results)
const seenRuleIds = new Set();
const uniqueRules = [];
for (const rule of allBehaviorRules) {
const ruleId = rule && typeof rule.id === 'number' ? rule.id : null;
if (ruleId !== null && seenRuleIds.has(ruleId)) continue;
if (ruleId !== null) seenRuleIds.add(ruleId);
uniqueRules.push(rule);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Дедупликация не защищает от дубликатов с отсутствующим ID.

Если rule.id равен null или отсутствует, правило всё равно добавляется в uniqueRules без проверки на дубликаты. Это может привести к повторному включению одного и того же правила, если оно присутствует и в alwaysInjectRules, и в behaviorRules без валидного ID.

🛡️ Предложение: добавить fallback-дедупликацию по title
   const seenRuleIds = new Set();
+  const seenTitles = new Set();
   const uniqueRules = [];
   for (const rule of allBehaviorRules) {
     const ruleId = rule && typeof rule.id === 'number' ? rule.id : null;
     if (ruleId !== null && seenRuleIds.has(ruleId)) continue;
     if (ruleId !== null) seenRuleIds.add(ruleId);
+    // Fallback dedup by title when ID is missing
+    if (ruleId === null && rule && rule.title) {
+      if (seenTitles.has(rule.title)) continue;
+      seenTitles.add(rule.title);
+    }
     uniqueRules.push(rule);
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Merge always-inject + similarity-matched behavioral rules into one block
const allBehaviorRules = [...alwaysInjectRules, ...behaviorRules];
// Deduplicate by ID (always-inject may overlap with similarity results)
const seenRuleIds = new Set();
const uniqueRules = [];
for (const rule of allBehaviorRules) {
const ruleId = rule && typeof rule.id === 'number' ? rule.id : null;
if (ruleId !== null && seenRuleIds.has(ruleId)) continue;
if (ruleId !== null) seenRuleIds.add(ruleId);
uniqueRules.push(rule);
}
// Merge always-inject + similarity-matched behavioral rules into one block
const allBehaviorRules = [...alwaysInjectRules, ...behaviorRules];
// Deduplicate by ID (always-inject may overlap with similarity results)
const seenRuleIds = new Set();
const seenTitles = new Set();
const uniqueRules = [];
for (const rule of allBehaviorRules) {
const ruleId = rule && typeof rule.id === 'number' ? rule.id : null;
if (ruleId !== null && seenRuleIds.has(ruleId)) continue;
if (ruleId !== null) seenRuleIds.add(ruleId);
// Fallback dedup by title when ID is missing
if (ruleId === null && rule && rule.title) {
if (seenTitles.has(rule.title)) continue;
seenTitles.add(rule.title);
}
uniqueRules.push(rule);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plugin/engram/hooks/user-prompt.js` around lines 86 - 96, The current dedup
loop (handling allBehaviorRules -> uniqueRules using seenRuleIds) skips deduping
when rule.id is null, allowing duplicates to be added; update the logic in the
loop that walks allBehaviorRules to apply a fallback deduplication key when
rule.id is null by using a stable identifier such as rule.title (or another
unique stringified field) — e.g. compute const key = (typeof rule.id ===
'number' ? `id:${rule.id}` : `title:${String(rule.title||'')}`) and track seen
keys in seenRuleIds (or rename to seenRuleKeys) so rules from alwaysInjectRules
and behaviorRules without numeric ids are still deduplicated before pushing to
uniqueRules; ensure you handle missing title gracefully (empty string) to avoid
runtime errors.

@thebtf thebtf merged commit 715e7c0 into main Mar 24, 2026
2 checks passed
@thebtf thebtf deleted the feat/always-inject 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