Skip to content

fix: call vector cleanup on all observation delete paths#32

Merged
thebtf merged 2 commits into
mainfrom
fix/orphan-vector-cleanup
Mar 20, 2026
Merged

fix: call vector cleanup on all observation delete paths#32
thebtf merged 2 commits into
mainfrom
fix/orphan-vector-cleanup

Conversation

@thebtf
Copy link
Copy Markdown
Owner

@thebtf thebtf commented Mar 20, 2026

Root Cause

DeleteObservation and DeleteObservations did not call cleanupFunc, leaving orphaned vectors in pgvector. Only processCleanup (project-based background cleanup) properly synced deletions to the vector store.

Result: 227,377 vectors for 186 observations — ~227K orphaned embeddings from dedup and MCP bulk_delete operations.

Fix

Both delete methods now call s.cleanupFunc(ctx, ids) after successful deletion, matching the existing pattern in processCleanup.

Test plan

  • Go build passes
  • Delete observation via MCP → verify vector also deleted
  • Dedup path → verify old observation's vector cleaned up

Summary by CodeRabbit

  • Исправления ошибок

    • Устранено некорректное поведение при удалении: очистка ресурсов теперь выполняется только после успешного удаления записей.
    • Статус-эндпоинт больше не вызывает побочных действий по инициализации хранилища ключей — данные о отпечатке/источнике ключа возвращаются только если хранилище уже инициализировано.
  • Chores

    • Порядок чтения ключа шифрования изменён: сначала ENGRAM_VAULT_KEY, затем запасной ENGRAM_ENCRYPTION_KEY.

Root cause: DeleteObservation and DeleteObservations did not call
cleanupFunc, leaving orphaned vectors in pgvector. Only processCleanup
(project-based background cleanup) called cleanupFunc correctly.

Result: 227K vectors for 186 observations — orphaned embeddings from
dedup and MCP bulk_delete that never synced to vector store.

Fix: Both delete methods now call s.cleanupFunc(ctx, ids) after
successful deletion, matching the pattern in processCleanup.
@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 addresses a critical data consistency problem by ensuring that when observations are deleted, their associated vector embeddings are also properly removed from the vector store. Previously, manual deletion methods failed to trigger this cleanup, leading to a buildup of orphaned vector data. The fix integrates the necessary cleanup function calls directly into the deletion logic, preventing future data discrepancies and maintaining a clean vector store.

Highlights

  • Vector Store Cleanup: The DeleteObservation and DeleteObservations methods in the ObservationStore now explicitly call cleanupFunc to ensure corresponding vector embeddings are removed from the pgvector store.
  • Data Consistency: This change resolves an issue where deleting observations could leave orphaned vectors, improving data consistency between the observation store and the vector store.
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 20, 2026

Обзор

Обновлены: удаление наблюдений теперь вызывает функцию очистки после успешных GORM-удалений; загрузка конфигурации теперь предпочитает ENGRAM_VAULT_KEY с запасным ENGRAM_ENCRYPTION_KEY; обработчик статуса хранилища не инициирует getVault() при установленном ключе и читает метаданные только из s.vault, если он инициализирован.

Изменения

Когорта / Файл(ы) Краткое описание
Удаление наблюдений
internal/db/gorm/observation_store.go
DeleteObservations и DeleteObservation теперь проверяют result.Error/RowsAffected и вызывают s.cleanupFunc(ctx, ids) только при успехе и если cleanupFunc != nil. Возвращают nil при успешном удалении.
Загрузка конфигурации — ключи
internal/config/config.go
Load() читает ENGRAM_VAULT_KEY в приоритете и использует ENGRAM_ENCRYPTION_KEY только как запасной алиас; поле cfg.EncryptionKey назначается как ранее. Добавлен комментарий об алиасе.
Обработчик статуса хранилища (vault)
internal/worker/handlers_vault.go
handleVaultStatus больше не вызывает s.getVault() при keyConfigured == true; при наличии s.vault берет Fingerprint() и KeySource() напрямую, иначе оставляет поля пустыми, избегая побочных эффектов автогенерации ключа.

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

sequenceDiagram
    participant Client as Клиент
    participant Handler as handleVaultStatus
    participant Service as Worker.s (s)
    participant Vault as Vault (s.vault)

    Клиент->>Handler: GET /vault/status
    Handler->>Service: check keyConfigured
    alt keyConfigured == true
        alt s.vault != nil
            Service->>Vault: Fingerprint(), KeySource()
            Vault-->>Service: fingerprint, keySource
        else s.vault == nil
            Service-->>Handler: нет данных (не вызываем getVault)
        end
    else keyConfigured == false
        Handler->>Service: s.getVault()
        Service->>Vault: (getVault) инициализация/чтение
        Vault-->>Service: vault metadata
    end
    Service-->>Handler: status payload
    Handler-->>Клиент: 200 {fingerprint, keySource}
Loading
sequenceDiagram
    participant Caller as Код, вызывающий удаление
    participant Store as ObservationStore
    participant DB as GORM/DB
    participant Cleanup as cleanupFunc

    Caller->>Store: DeleteObservations(ctx, ids)
    Store->>DB: DELETE ... (GORM)
    DB-->>Store: result (Error, RowsAffected)
    alt result.Error == nil and RowsAffected > 0 and cleanupFunc != nil
        Store->>Cleanup: cleanupFunc(ctx, ids)
        Cleanup-->>Store: done
    end
    Store-->>Caller: возвращает nil (при успехе) или ошибку
Loading

Оценка объёма проверки кода

🎯 3 (Moderate) | ⏱️ ~20 minutes

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

🐰
Я — кролик, что трусит по коду в свет,
Удалил следы — и всё вновь чисто тут.
Ключ выбрал путь, где два есть пути, —
И статус тихо лишь смотрит, не будит снов.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and accurately describes the main change: adding vector cleanup calls to all observation deletion paths in the codebase.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/orphan-vector-cleanup

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.

@thebtf
Copy link
Copy Markdown
Owner Author

thebtf commented Mar 20, 2026

@coderabbitai review

@thebtf
Copy link
Copy Markdown
Owner Author

thebtf commented Mar 20, 2026

@gemini-code-assist review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 20, 2026

✅ Actions performed

Review triggered.

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

@thebtf
Copy link
Copy Markdown
Owner Author

thebtf commented Mar 20, 2026

@codex review

Copy link
Copy Markdown

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

Choose a reason for hiding this comment

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

Code Review

This pull request addresses an issue where deleting observations did not trigger the corresponding vector cleanup, leading to orphaned embeddings. The fix correctly adds calls to cleanupFunc in DeleteObservations and DeleteObservation. The change in DeleteObservation is solid. For DeleteObservations, I've suggested an improvement to make the cleanup more precise and efficient by only passing the IDs of actually deleted observations to the cleanup function, which aligns better with existing patterns in the codebase.

Comment thread internal/db/gorm/observation_store.go
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 correctly addresses an issue where deleting observations could leave orphaned vectors in the vector store. The changes in DeleteObservation and DeleteObservations now trigger the necessary cleanup. The implementation for DeleteObservation is sound. For DeleteObservations, I've suggested an improvement to enhance correctness and atomicity by using a transaction, ensuring the cleanup function is called only for observations that were verifiably deleted. This also prevents potential race conditions.

Comment thread internal/db/gorm/observation_store.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: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1a741a27-6487-4b37-89bd-0b50752a38e1

📥 Commits

Reviewing files that changed from the base of the PR and between 96e0917 and 1496ba3.

📒 Files selected for processing (1)
  • internal/db/gorm/observation_store.go

Comment thread internal/db/gorm/observation_store.go
1. ENGRAM_VAULT_KEY is now the primary env var (ENGRAM_ENCRYPTION_KEY as alias)
   Docker template and UI hint both use ENGRAM_VAULT_KEY — code now matches.

2. handleVaultStatus no longer calls getVault(), preventing auto-generation
   of a new key on read-only status checks. Previously, Refresh on the Vault
   page would trigger NewVault() → auto-generate vault.key → switch from
   Disabled to Enabled as a side effect.
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.

🧹 Nitpick comments (2)
internal/worker/handlers_vault.go (2)

319-319: Рассмотрите обновление backup_reminder для соответствия новому имени переменной.

В backup_reminder указан ENGRAM_ENCRYPTION_KEY, но в config.go теперь ENGRAM_VAULT_KEY является основным именем. Для согласованности документации:

📝 Предлагаемое изменение
-		"backup_reminder":  "Back up vault.key (or set ENGRAM_ENCRYPTION_KEY) — losing this key makes stored credentials unrecoverable",
+		"backup_reminder":  "Back up vault.key (or set ENGRAM_VAULT_KEY) — losing this key makes stored credentials unrecoverable",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/worker/handlers_vault.go` at line 319, Update the backup_reminder
message to reference the new environment variable name: replace the mention of
ENGRAM_ENCRYPTION_KEY with ENGRAM_VAULT_KEY in the map entry keyed by
"backup_reminder" so the displayed guidance matches the new canonical variable
defined in config (ensure the string in the "backup_reminder" value now reads
something like ENGRAM_VAULT_KEY).

299-305: Логика корректна, но возможно неинформативное состояние при ленивой инициализации.

Изменение правильно избегает побочных эффектов в read-only эндпоинте. Однако при keyConfigured == true и s.vault == nil (до первого использования vault) ответ будет содержать:

{
  "key_configured": true,
  "fingerprint": "",
  "key_source": ""
}

Это может сбить с толку пользователей — ключ сконфигурирован, но fingerprint пустой. Рассмотрите возможность добавления поля vault_initialized или возврата null вместо пустых строк для более явного разделения состояний.

💡 Опциональное улучшение
 	writeJSON(w, map[string]any{
 		"key_configured":   keyConfigured,
+		"vault_initialized": s.vault != nil,
 		"key_source":       keySource,
 		"fingerprint":      fingerprint,
 		"credential_count": count,
 		"backup_reminder":  "Back up vault.key (or set ENGRAM_ENCRYPTION_KEY) — losing this key makes stored credentials unrecoverable",
 	})
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/worker/handlers_vault.go` around lines 299 - 305, When keyConfigured
is true but s.vault is nil the response currently shows empty
fingerprint/key_source which is ambiguous; update the vault metadata response
produced in the handler that uses keyConfigured, s.vault, fingerprint and
keySource to include an explicit vault_initialized boolean (true when s.vault !=
nil) or return null for fingerprint/key_source when s.vault == nil instead of
empty strings. Locate the block that currently sets fingerprint =
s.vault.Fingerprint() / keySource = s.vault.KeySource() and change the response
construction to set vault_initialized = (s.vault != nil) and set
fingerprint/key_source to null (or omit them) when vault_initialized is false,
ensuring JSON marshaling produces nulls (use pointer/string nil or proper struct
tags) and preserve existing behavior when s.vault is present.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@internal/worker/handlers_vault.go`:
- Line 319: Update the backup_reminder message to reference the new environment
variable name: replace the mention of ENGRAM_ENCRYPTION_KEY with
ENGRAM_VAULT_KEY in the map entry keyed by "backup_reminder" so the displayed
guidance matches the new canonical variable defined in config (ensure the string
in the "backup_reminder" value now reads something like ENGRAM_VAULT_KEY).
- Around line 299-305: When keyConfigured is true but s.vault is nil the
response currently shows empty fingerprint/key_source which is ambiguous; update
the vault metadata response produced in the handler that uses keyConfigured,
s.vault, fingerprint and keySource to include an explicit vault_initialized
boolean (true when s.vault != nil) or return null for fingerprint/key_source
when s.vault == nil instead of empty strings. Locate the block that currently
sets fingerprint = s.vault.Fingerprint() / keySource = s.vault.KeySource() and
change the response construction to set vault_initialized = (s.vault != nil) and
set fingerprint/key_source to null (or omit them) when vault_initialized is
false, ensuring JSON marshaling produces nulls (use pointer/string nil or proper
struct tags) and preserve existing behavior when s.vault is present.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e2ce4e7c-5411-4262-a7ef-e1245948146c

📥 Commits

Reviewing files that changed from the base of the PR and between 1496ba3 and e36f39c.

📒 Files selected for processing (2)
  • internal/config/config.go
  • internal/worker/handlers_vault.go

@thebtf
Copy link
Copy Markdown
Owner Author

thebtf commented Mar 20, 2026

🤖 PR Review MCP State (auto-managed, do not edit)
{
  "version": 2,
  "parentChildren": {},
  "resolvedNitpicks": {
    "coderabbit-nitpick-6d29c539-319": {
      "resolvedAt": "2026-03-20T22:39:22.406Z",
      "resolvedBy": "agent"
    },
    "coderabbit-nitpick-d043dd39-299": {
      "resolvedAt": "2026-03-20T22:39:25.064Z",
      "resolvedBy": "agent"
    }
  },
  "updatedAt": "2026-03-20T22:39:25.530Z"
}

@thebtf thebtf merged commit c5b4a20 into main Mar 20, 2026
2 checks passed
@thebtf thebtf deleted the fix/orphan-vector-cleanup branch March 20, 2026 22:39
thebtf added a commit that referenced this pull request Apr 12, 2026
Agents couldn't rate observations because the injected <relevant-memory>
block showed "## 1. [DECISION] Title" with no ID. The MCP feedback tool
requires feedback(action="rate", id=N) but N was never visible.

Now renders as "## 1. [DECISION] Title (id:12345)" in both:
- session-start.js (initial context injection)
- user-prompt.js (per-prompt context injection)

Closes #32. Plugin version bumped to 3.7.5.
thebtf added a commit that referenced this pull request Apr 12, 2026
Agents couldn't rate observations because the injected <relevant-memory>
block showed "## 1. [DECISION] Title" with no ID. The MCP feedback tool
requires feedback(action="rate", id=N) but N was never visible.

Now renders as "## 1. [DECISION] Title (id:12345)" in both:
- session-start.js (initial context injection)
- user-prompt.js (per-prompt context injection)

Closes #32. Plugin version bumped to 3.7.5.
thebtf added a commit that referenced this pull request Apr 12, 2026
…oop (#139)

Agents couldn't rate observations because the injected <relevant-memory>
block showed "## 1. [DECISION] Title" with no ID. The MCP feedback tool
requires feedback(action="rate", id=N) but N was never visible.

Now renders as "## 1. [DECISION] Title (id:12345)" in both:
- session-start.js (initial context injection)
- user-prompt.js (per-prompt context injection)

Closes #32. Plugin version bumped to 3.7.5.

Co-authored-by: Kirill Turanskiy <thebtf@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant