Skip to content

feat(rag): add encrypted embeddings foundation#211

Open
AnthonyRonning wants to merge 6 commits into
masterfrom
codex/rag-encrypted-embeddings
Open

feat(rag): add encrypted embeddings foundation#211
AnthonyRonning wants to merge 6 commits into
masterfrom
codex/rag-encrypted-embeddings

Conversation

@AnthonyRonning

@AnthonyRonning AnthonyRonning commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

Summary

  • add encrypted embedding storage/retrieval primitives for the RAG foundation
  • add the RAG web module and hook it into the existing OpenAI request flow
  • add user embedding model plumbing

Verification

  • nix develop -c cargo check

Notes

  • This PR is the original RAG commit isolated on top of latest master.
  • PCR update intentionally omitted for this review PR; deployment PCRs will be regenerated separately before deploy.

Open in Devin Review

Summary by CodeRabbit

  • New Features
    • Enhanced saved-content search with stronger filtering, improved ranking, and token-aware truncation.
    • Added DB-backed, cache-coordinated embedding retrieval plus richer embeddings status.
  • Bug Fixes
    • More reliably clear cached embeddings when performing password resets or account deletions.
    • Deleting embeddings now updates search/status by removing missing entries.
  • Performance
    • Added “hot path” database indexes to speed up common user-embedding lookups.
    • Optimized vector loading/decryption via coordinated caching and top-K candidate retrieval.

@coderabbitai

coderabbitai Bot commented Jun 27, 2026

Copy link
Copy Markdown

Review Change Stack

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

Adds a user-embedding storage model, a cache-backed RAG search pipeline with ranking and filters, OpenAI embeddings request/billing refactoring, and cache eviction wiring on sensitive account changes.

Changes

RAG Embedding Pipeline

Layer / File(s) Summary
UserEmbedding model and indexes
src/models/mod.rs, src/models/user_embeddings.rs, migrations/2026-07-07-000001_user_embeddings_hot_path_indexes/*
Adds the user_embeddings module, Diesel model and insert/delete helpers, and three hot-path indexes with matching down-migration cleanup and non-transactional metadata.
RagCache and search pipeline
src/rag.rs
Adds cache, vector, ranking, insert, search, delete, status, and test coverage for the RAG embeddings flow.
OpenAI embeddings request flow
src/web/openai.rs, src/web/mod.rs
Adds centralized embeddings request construction and billing handling, and re-exports get_embedding_vector.
AppState rag_cache wiring
src/main.rs
Adds rag_cache to application state, initializes it, and evicts cached embeddings after password reset and account deletion.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant search_user_embeddings
  participant RagCache
  participant Database
  participant EmbeddingService

  Client->>search_user_embeddings: query text + filters
  search_user_embeddings->>EmbeddingService: embed query text
  EmbeddingService-->>search_user_embeddings: query vector
  search_user_embeddings->>RagCache: get(user_id)
  alt cache miss
    search_user_embeddings->>Database: load embeddings
    Database-->>search_user_embeddings: vector rows
    search_user_embeddings->>RagCache: put(user_id, embeddings)
  end
  search_user_embeddings->>search_user_embeddings: rank candidates and fetch content
  search_user_embeddings-->>Client: RagSearchOutcome
Loading
sequenceDiagram
  participant Client
  participant proxy_embeddings
  participant request_embeddings_with_billing
  participant request_embeddings_from_primary
  participant PrimaryProvider

  Client->>proxy_embeddings: embeddings request
  proxy_embeddings->>request_embeddings_with_billing: forward request
  request_embeddings_with_billing->>request_embeddings_from_primary: canonicalized request
  request_embeddings_from_primary->>PrimaryProvider: POST /v1/embeddings
  PrimaryProvider-->>request_embeddings_from_primary: response_json + usage
  request_embeddings_with_billing-->>proxy_embeddings: response_json + prompt_tokens
  proxy_embeddings-->>Client: embeddings response
Loading

Possibly related PRs

Poem

I dug a cache beneath the clover,
for vectors warm and embeddings over.
With hops and ranks and tokens neat,
the search comes back both swift and sweet. 🐇

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 44.44% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: the encrypted embeddings foundation for RAG.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/rag-encrypted-embeddings

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

devin-ai-integration[bot]

This comment was marked as resolved.

coderabbitai[bot]

This comment was marked as resolved.

@AnthonyRonning
AnthonyRonning force-pushed the codex/rag-encrypted-embeddings branch from 84e1c61 to 55ecf39 Compare July 7, 2026 02:40
@AnthonyRonning
AnthonyRonning force-pushed the codex/rag-encrypted-embeddings branch from 55ecf39 to a111aa3 Compare July 7, 2026 04:04

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

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

⚠️ Outside diff range comments (3)
src/main.rs (1)

1706-1712: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Delete stale encrypted embeddings on destructive password reset.

Evicting the cache is not enough here: the reset creates a new user seed/key, so persisted embeddings encrypted with the old key become undecryptable but still count toward storage and scans. Delete the user’s embedding rows as part of the destructive reset flow, or explicitly re-encrypt them if that is intended.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main.rs` around lines 1706 - 1712, The destructive password reset flow in
complete_destructive_password_reset currently only evicts the RAG cache, which
leaves persisted embeddings encrypted with the old user key behind. Update this
path in the reset handling around self.db.complete_destructive_password_reset
and self.rag_cache.lock().await.evict_user(...) to also remove the user’s stored
embedding rows, or re-encrypt them if that is the intended behavior, so stale
encrypted embeddings do not remain in storage.
src/rag.rs (2)

624-638: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Mirror the id tie-break in bounded heap replacement.

Ord prefers lower ids after score/token ties, but the replacement check omits that final tie-break, so exact ties can keep the wrong candidate.

Proposed fix
             let ordering = score
                 .total_cmp(&min.score)
-                .then_with(|| min.token_count.cmp(&e.token_count));
+                .then_with(|| min.token_count.cmp(&e.token_count))
+                .then_with(|| min.id.cmp(&e.id));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/rag.rs` around lines 624 - 638, The bounded heap replacement logic in the
ranking loop is missing the final id tie-break that HeapItem::Ord uses, so exact
score and token_count ties can preserve the wrong entry. Update the comparison
in the heap replacement path that builds and compares HeapItem values to mirror
the full ordering from Ord, including the id comparison after score and
token_count, so the same candidate wins in both the heap and replacement check.

902-929: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Check cheap quota limits before generating embeddings.

Both insert paths call the external embedding/billing flow before enforcing insert-rate and row limits, so over-quota requests still incur embedding cost before being rejected. Split the cheap rate/row checks ahead of embed_text_via_tinfoil, then keep the exact byte check before insert.

Also applies to: 981-996

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/rag.rs` around lines 902 - 929, In the insert flow in rag.rs, the cheap
quota checks are happening too late because embed_text_via_tinfoil is called
before insert-rate and row limits are enforced, causing avoidable
embedding/billing work for requests that will be rejected. Move the inexpensive
quota validation ahead of embedding in the insert path, keeping the exact
text-size and projected-byte checks where they are needed before the actual
insert. Apply the same reordering to the other insert path mentioned in the
review so both paths use the same early-check pattern around
embed_text_via_tinfoil, ensure_storage_limits, and the insert validation logic.
♻️ Duplicate comments (1)
migrations/2026-07-07-000001_user_embeddings_hot_path_indexes/down.sql (1)

1-3: 🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

Mirror the CONCURRENTLY fix for rollback drops.

Same lock-blocking concern applies to DROP INDEX here — a normal drop takes an ACCESS EXCLUSIVE lock. This should be addressed consistently with the up.sql fix (same transaction-wrapping caveat applies).

🔧 Proposed fix
-DROP INDEX IF EXISTS idx_user_embeddings_user_model_conversation_created_id;
-DROP INDEX IF EXISTS idx_user_embeddings_user_model_source_created_id;
-DROP INDEX IF EXISTS idx_user_embeddings_user_model_created_id;
+DROP INDEX CONCURRENTLY IF EXISTS idx_user_embeddings_user_model_conversation_created_id;
+DROP INDEX CONCURRENTLY IF EXISTS idx_user_embeddings_user_model_source_created_id;
+DROP INDEX CONCURRENTLY IF EXISTS idx_user_embeddings_user_model_created_id;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@migrations/2026-07-07-000001_user_embeddings_hot_path_indexes/down.sql`
around lines 1 - 3, The rollback in down.sql drops the same indexes without the
non-blocking form, so mirror the up.sql approach by changing the DROP INDEX
statements for idx_user_embeddings_user_model_conversation_created_id,
idx_user_embeddings_user_model_source_created_id, and
idx_user_embeddings_user_model_created_id to use CONCURRENTLY. Keep in mind the
migration must not be wrapped in a transaction when using the concurrent drop,
so adjust the migration setup consistently with the existing up.sql fix.

Source: Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@migrations/2026-07-07-000001_user_embeddings_hot_path_indexes/up.sql`:
- Around line 1-9: The three index builds in the user_embeddings hot path
migration are currently blocking because they use plain CREATE INDEX. Update the
migration’s CREATE INDEX statements for
idx_user_embeddings_user_model_created_id,
idx_user_embeddings_user_model_source_created_id, and
idx_user_embeddings_user_model_conversation_created_id to use CREATE INDEX
CONCURRENTLY, and add a metadata.toml for this migration with run_in_transaction
= false so Diesel can execute it outside a transaction.

In `@src/main.rs`:
- Line 24: The application router is missing the RAG routes, so the new web APIs
are not being exposed. Update the router setup in main.rs where the existing
route groups are mounted to import and merge the RAG route set alongside
instructions_routes, login_routes, oauth_routes, openai_routes, and
protected_routes, ensuring the new subsystem is reachable through the app’s
router configuration.

In `@src/rag.rs`:
- Around line 1360-1371: The duplicate-load path in
load_user_embeddings_for_search is leaving the in-flight marker behind when the
awaited load fails, which can cause later callers to keep timing out. Update the
branch around the await in src/rag.rs so the cache cleanup in
rag_cache.finish_load(user_id) (or equivalent in-flight removal) runs even when
load_user_embeddings_for_search returns an error, using a guard/defer-style
cleanup tied to the load_user_embeddings_for_search / cache.put flow.

---

Outside diff comments:
In `@src/main.rs`:
- Around line 1706-1712: The destructive password reset flow in
complete_destructive_password_reset currently only evicts the RAG cache, which
leaves persisted embeddings encrypted with the old user key behind. Update this
path in the reset handling around self.db.complete_destructive_password_reset
and self.rag_cache.lock().await.evict_user(...) to also remove the user’s stored
embedding rows, or re-encrypt them if that is the intended behavior, so stale
encrypted embeddings do not remain in storage.

In `@src/rag.rs`:
- Around line 624-638: The bounded heap replacement logic in the ranking loop is
missing the final id tie-break that HeapItem::Ord uses, so exact score and
token_count ties can preserve the wrong entry. Update the comparison in the heap
replacement path that builds and compares HeapItem values to mirror the full
ordering from Ord, including the id comparison after score and token_count, so
the same candidate wins in both the heap and replacement check.
- Around line 902-929: In the insert flow in rag.rs, the cheap quota checks are
happening too late because embed_text_via_tinfoil is called before insert-rate
and row limits are enforced, causing avoidable embedding/billing work for
requests that will be rejected. Move the inexpensive quota validation ahead of
embedding in the insert path, keeping the exact text-size and projected-byte
checks where they are needed before the actual insert. Apply the same reordering
to the other insert path mentioned in the review so both paths use the same
early-check pattern around embed_text_via_tinfoil, ensure_storage_limits, and
the insert validation logic.

---

Duplicate comments:
In `@migrations/2026-07-07-000001_user_embeddings_hot_path_indexes/down.sql`:
- Around line 1-3: The rollback in down.sql drops the same indexes without the
non-blocking form, so mirror the up.sql approach by changing the DROP INDEX
statements for idx_user_embeddings_user_model_conversation_created_id,
idx_user_embeddings_user_model_source_created_id, and
idx_user_embeddings_user_model_created_id to use CONCURRENTLY. Keep in mind the
migration must not be wrapped in a transaction when using the concurrent drop,
so adjust the migration setup consistently with the existing up.sql fix.
🪄 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: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 2221ba81-ea9e-4b86-9599-94927732874f

📥 Commits

Reviewing files that changed from the base of the PR and between a111aa3 and 712a3db.

📒 Files selected for processing (5)
  • migrations/2026-07-07-000001_user_embeddings_hot_path_indexes/down.sql
  • migrations/2026-07-07-000001_user_embeddings_hot_path_indexes/up.sql
  • src/main.rs
  • src/rag.rs
  • src/web/mod.rs
💤 Files with no reviewable changes (1)
  • src/web/mod.rs

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 3

Caution

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

⚠️ Outside diff range comments (3)
src/main.rs (1)

1706-1712: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Delete stale encrypted embeddings on destructive password reset.

Evicting the cache is not enough here: the reset creates a new user seed/key, so persisted embeddings encrypted with the old key become undecryptable but still count toward storage and scans. Delete the user’s embedding rows as part of the destructive reset flow, or explicitly re-encrypt them if that is intended.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main.rs` around lines 1706 - 1712, The destructive password reset flow in
complete_destructive_password_reset currently only evicts the RAG cache, which
leaves persisted embeddings encrypted with the old user key behind. Update this
path in the reset handling around self.db.complete_destructive_password_reset
and self.rag_cache.lock().await.evict_user(...) to also remove the user’s stored
embedding rows, or re-encrypt them if that is the intended behavior, so stale
encrypted embeddings do not remain in storage.
src/rag.rs (2)

624-638: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Mirror the id tie-break in bounded heap replacement.

Ord prefers lower ids after score/token ties, but the replacement check omits that final tie-break, so exact ties can keep the wrong candidate.

Proposed fix
             let ordering = score
                 .total_cmp(&min.score)
-                .then_with(|| min.token_count.cmp(&e.token_count));
+                .then_with(|| min.token_count.cmp(&e.token_count))
+                .then_with(|| min.id.cmp(&e.id));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/rag.rs` around lines 624 - 638, The bounded heap replacement logic in the
ranking loop is missing the final id tie-break that HeapItem::Ord uses, so exact
score and token_count ties can preserve the wrong entry. Update the comparison
in the heap replacement path that builds and compares HeapItem values to mirror
the full ordering from Ord, including the id comparison after score and
token_count, so the same candidate wins in both the heap and replacement check.

902-929: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Check cheap quota limits before generating embeddings.

Both insert paths call the external embedding/billing flow before enforcing insert-rate and row limits, so over-quota requests still incur embedding cost before being rejected. Split the cheap rate/row checks ahead of embed_text_via_tinfoil, then keep the exact byte check before insert.

Also applies to: 981-996

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/rag.rs` around lines 902 - 929, In the insert flow in rag.rs, the cheap
quota checks are happening too late because embed_text_via_tinfoil is called
before insert-rate and row limits are enforced, causing avoidable
embedding/billing work for requests that will be rejected. Move the inexpensive
quota validation ahead of embedding in the insert path, keeping the exact
text-size and projected-byte checks where they are needed before the actual
insert. Apply the same reordering to the other insert path mentioned in the
review so both paths use the same early-check pattern around
embed_text_via_tinfoil, ensure_storage_limits, and the insert validation logic.
♻️ Duplicate comments (1)
migrations/2026-07-07-000001_user_embeddings_hot_path_indexes/down.sql (1)

1-3: 🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

Mirror the CONCURRENTLY fix for rollback drops.

Same lock-blocking concern applies to DROP INDEX here — a normal drop takes an ACCESS EXCLUSIVE lock. This should be addressed consistently with the up.sql fix (same transaction-wrapping caveat applies).

🔧 Proposed fix
-DROP INDEX IF EXISTS idx_user_embeddings_user_model_conversation_created_id;
-DROP INDEX IF EXISTS idx_user_embeddings_user_model_source_created_id;
-DROP INDEX IF EXISTS idx_user_embeddings_user_model_created_id;
+DROP INDEX CONCURRENTLY IF EXISTS idx_user_embeddings_user_model_conversation_created_id;
+DROP INDEX CONCURRENTLY IF EXISTS idx_user_embeddings_user_model_source_created_id;
+DROP INDEX CONCURRENTLY IF EXISTS idx_user_embeddings_user_model_created_id;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@migrations/2026-07-07-000001_user_embeddings_hot_path_indexes/down.sql`
around lines 1 - 3, The rollback in down.sql drops the same indexes without the
non-blocking form, so mirror the up.sql approach by changing the DROP INDEX
statements for idx_user_embeddings_user_model_conversation_created_id,
idx_user_embeddings_user_model_source_created_id, and
idx_user_embeddings_user_model_created_id to use CONCURRENTLY. Keep in mind the
migration must not be wrapped in a transaction when using the concurrent drop,
so adjust the migration setup consistently with the existing up.sql fix.

Source: Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@migrations/2026-07-07-000001_user_embeddings_hot_path_indexes/up.sql`:
- Around line 1-9: The three index builds in the user_embeddings hot path
migration are currently blocking because they use plain CREATE INDEX. Update the
migration’s CREATE INDEX statements for
idx_user_embeddings_user_model_created_id,
idx_user_embeddings_user_model_source_created_id, and
idx_user_embeddings_user_model_conversation_created_id to use CREATE INDEX
CONCURRENTLY, and add a metadata.toml for this migration with run_in_transaction
= false so Diesel can execute it outside a transaction.

In `@src/main.rs`:
- Line 24: The application router is missing the RAG routes, so the new web APIs
are not being exposed. Update the router setup in main.rs where the existing
route groups are mounted to import and merge the RAG route set alongside
instructions_routes, login_routes, oauth_routes, openai_routes, and
protected_routes, ensuring the new subsystem is reachable through the app’s
router configuration.

In `@src/rag.rs`:
- Around line 1360-1371: The duplicate-load path in
load_user_embeddings_for_search is leaving the in-flight marker behind when the
awaited load fails, which can cause later callers to keep timing out. Update the
branch around the await in src/rag.rs so the cache cleanup in
rag_cache.finish_load(user_id) (or equivalent in-flight removal) runs even when
load_user_embeddings_for_search returns an error, using a guard/defer-style
cleanup tied to the load_user_embeddings_for_search / cache.put flow.

---

Outside diff comments:
In `@src/main.rs`:
- Around line 1706-1712: The destructive password reset flow in
complete_destructive_password_reset currently only evicts the RAG cache, which
leaves persisted embeddings encrypted with the old user key behind. Update this
path in the reset handling around self.db.complete_destructive_password_reset
and self.rag_cache.lock().await.evict_user(...) to also remove the user’s stored
embedding rows, or re-encrypt them if that is the intended behavior, so stale
encrypted embeddings do not remain in storage.

In `@src/rag.rs`:
- Around line 624-638: The bounded heap replacement logic in the ranking loop is
missing the final id tie-break that HeapItem::Ord uses, so exact score and
token_count ties can preserve the wrong entry. Update the comparison in the heap
replacement path that builds and compares HeapItem values to mirror the full
ordering from Ord, including the id comparison after score and token_count, so
the same candidate wins in both the heap and replacement check.
- Around line 902-929: In the insert flow in rag.rs, the cheap quota checks are
happening too late because embed_text_via_tinfoil is called before insert-rate
and row limits are enforced, causing avoidable embedding/billing work for
requests that will be rejected. Move the inexpensive quota validation ahead of
embedding in the insert path, keeping the exact text-size and projected-byte
checks where they are needed before the actual insert. Apply the same reordering
to the other insert path mentioned in the review so both paths use the same
early-check pattern around embed_text_via_tinfoil, ensure_storage_limits, and
the insert validation logic.

---

Duplicate comments:
In `@migrations/2026-07-07-000001_user_embeddings_hot_path_indexes/down.sql`:
- Around line 1-3: The rollback in down.sql drops the same indexes without the
non-blocking form, so mirror the up.sql approach by changing the DROP INDEX
statements for idx_user_embeddings_user_model_conversation_created_id,
idx_user_embeddings_user_model_source_created_id, and
idx_user_embeddings_user_model_created_id to use CONCURRENTLY. Keep in mind the
migration must not be wrapped in a transaction when using the concurrent drop,
so adjust the migration setup consistently with the existing up.sql fix.
🪄 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: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 2221ba81-ea9e-4b86-9599-94927732874f

📥 Commits

Reviewing files that changed from the base of the PR and between a111aa3 and 712a3db.

📒 Files selected for processing (5)
  • migrations/2026-07-07-000001_user_embeddings_hot_path_indexes/down.sql
  • migrations/2026-07-07-000001_user_embeddings_hot_path_indexes/up.sql
  • src/main.rs
  • src/rag.rs
  • src/web/mod.rs
💤 Files with no reviewable changes (1)
  • src/web/mod.rs
🛑 Comments failed to post (3)
migrations/2026-07-07-000001_user_embeddings_hot_path_indexes/up.sql (1)

1-9: 🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Does Diesel ORM support disabling transaction wrapping for a single migration to allow CREATE INDEX CONCURRENTLY?

💡 Result:

Yes, Diesel ORM supports disabling transaction wrapping for individual migrations, which is required for operations like CREATE INDEX CONCURRENTLY in PostgreSQL [1][2][3]. To disable the default transaction wrapping for a migration, you can use the following methods: For file-based migrations: Create a metadata.toml file within the specific migration directory and set the run_in_transaction flag to false [1][2]: # migrations/_<migration_name>/metadata.toml run_in_transaction = false For Rust-based migrations: If you are implementing migrations using Rust code, you can call the without_transaction method on the builder [4][2]: // Example for Rust-based migration RustMigration::new.up(|conn| { /* your SQL here */ }).without_transaction // Disables transaction wrapping.build Alternatively, if you are implementing the TypedMigration trait directly, you can return false from the run_in_transaction method [2][5]. Because CREATE INDEX CONCURRENTLY cannot run inside a transaction block in PostgreSQL, using these settings ensures the migration executes correctly without triggering an error [6][3][7].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== migration files =="
git ls-files 'migrations/2026-07-07-000001_user_embeddings_hot_path_indexes/**' || true

echo
echo "== nearby Diesel config references =="
rg -n --hidden --glob '!target' --glob '!node_modules' \
  'run_in_transaction|metadata\.toml|diesel_migrations|migrations' \
  Cargo.toml . 2>/dev/null | sed -n '1,200p'

echo
echo "== migration directory contents =="
find migrations/2026-07-07-000001_user_embeddings_hot_path_indexes -maxdepth 2 -type f | sort

Repository: OpenSecretCloud/opensecret

Length of output: 3157


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== diesel.toml =="
cat -n diesel.toml

echo
echo "== src/migrations.rs =="
cat -n src/migrations.rs

echo
echo "== migration directory metadata files =="
find migrations/2026-07-07-000001_user_embeddings_hot_path_indexes -maxdepth 1 -type f -name 'metadata.toml' -o -name '*.toml' -o -name '*.sql' | sort | xargs -r -I{} sh -c 'echo "--- {}"; cat -n "{}"; echo'

Repository: OpenSecretCloud/opensecret

Length of output: 9026


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n --hidden --glob '!target' --glob '!node_modules' \
  'MigrationHarness|run_pending_migrations|embed_migrations|embedded_migrations|diesel_migrations|metadata\.toml|run_in_transaction' \
  src migrations Cargo.toml . 2>/dev/null | sed -n '1,240p'

Repository: OpenSecretCloud/opensecret

Length of output: 164


Use CONCURRENTLY for these indexes
These CREATE INDEX statements will block writes while they build. Switch them to CREATE INDEX CONCURRENTLY and add metadata.toml with run_in_transaction = false so Diesel can run this migration outside a transaction.

🧰 Tools
🪛 Squawk (2.59.0)

[warning] 2-3: During normal index creation, table updates are blocked, but reads are still allowed. Use concurrently to avoid blocking writes.

(require-concurrent-index-creation)


[warning] 5-6: During normal index creation, table updates are blocked, but reads are still allowed. Use concurrently to avoid blocking writes.

(require-concurrent-index-creation)


[warning] 8-9: During normal index creation, table updates are blocked, but reads are still allowed. Use concurrently to avoid blocking writes.

(require-concurrent-index-creation)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@migrations/2026-07-07-000001_user_embeddings_hot_path_indexes/up.sql` around
lines 1 - 9, The three index builds in the user_embeddings hot path migration
are currently blocking because they use plain CREATE INDEX. Update the
migration’s CREATE INDEX statements for
idx_user_embeddings_user_model_created_id,
idx_user_embeddings_user_model_source_created_id, and
idx_user_embeddings_user_model_conversation_created_id to use CREATE INDEX
CONCURRENTLY, and add a metadata.toml for this migration with run_in_transaction
= false so Diesel can execute it outside a transaction.

Source: Linters/SAST tools

src/main.rs (1)

24-24: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Mount the RAG routes in the application router.

The RAG route import/merge is absent, so the new RAG web APIs are not reachable even though the PR adds the subsystem.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main.rs` at line 24, The application router is missing the RAG routes, so
the new web APIs are not being exposed. Update the router setup in main.rs where
the existing route groups are mounted to import and merge the RAG route set
alongside instructions_routes, login_routes, oauth_routes, openai_routes, and
protected_routes, ensuring the new subsystem is reachable through the app’s
router configuration.
src/rag.rs (1)

1360-1371: 🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Always clear the in-flight marker on duplicate load errors.

The timeout-duplicate branch returns early on load_user_embeddings_for_search(...).await?, so an error leaves in_flight_loads stuck and future callers can loop through wait timeouts indefinitely.

Proposed fix
-                let loaded = load_user_embeddings_for_search(
+                let loaded = load_user_embeddings_for_search(
                     state,
                     user_id,
                     user_key,
                     filters,
                     limits.scan_limit,
                 )
-                .await?;
+                .await;
 
                 let mut cache = state.rag_cache.lock().await;
-                let cached = cache.put(user_id, loaded.embeddings.clone(), loaded.scan_limit_hit);
+                if let Ok(loaded) = &loaded {
+                    let cached = cache.put(user_id, loaded.embeddings.clone(), loaded.scan_limit_hit);
+                    info!(
+                        target: "rag",
+                        user_id = %user_id,
+                        cached,
+                        loaded_rows = loaded.embeddings.len(),
+                        cache_bytes_per_user = cached_embeddings_bytes(&loaded.embeddings),
+                        total_cache_bytes = cache.total_bytes,
+                        scan_limit_hit = loaded.scan_limit_hit,
+                        reason = "timeout_duplicate",
+                        "rag_cache_store_after_load"
+                    );
+                }
                 cache.finish_load(user_id);
-                info!(
-                    target: "rag",
-                    user_id = %user_id,
-                    cached,
-                    loaded_rows = loaded.embeddings.len(),
-                    cache_bytes_per_user = cached_embeddings_bytes(&loaded.embeddings),
-                    total_cache_bytes = cache.total_bytes,
-                    scan_limit_hit = loaded.scan_limit_hit,
-                    reason = "timeout_duplicate",
-                    "rag_cache_store_after_load"
-                );
-                return Ok(loaded);
+                return loaded;
📝 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.

                let loaded = load_user_embeddings_for_search(
                    state,
                    user_id,
                    user_key,
                    filters,
                    limits.scan_limit,
                )
                .await;

                let mut cache = state.rag_cache.lock().await;
                if let Ok(loaded) = &loaded {
                    let cached = cache.put(user_id, loaded.embeddings.clone(), loaded.scan_limit_hit);
                    info!(
                        target: "rag",
                        user_id = %user_id,
                        cached,
                        loaded_rows = loaded.embeddings.len(),
                        cache_bytes_per_user = cached_embeddings_bytes(&loaded.embeddings),
                        total_cache_bytes = cache.total_bytes,
                        scan_limit_hit = loaded.scan_limit_hit,
                        reason = "timeout_duplicate",
                        "rag_cache_store_after_load"
                    );
                }
                cache.finish_load(user_id);
                return loaded;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/rag.rs` around lines 1360 - 1371, The duplicate-load path in
load_user_embeddings_for_search is leaving the in-flight marker behind when the
awaited load fails, which can cause later callers to keep timing out. Update the
branch around the await in src/rag.rs so the cache cleanup in
rag_cache.finish_load(user_id) (or equivalent in-flight removal) runs even when
load_user_embeddings_for_search returns an error, using a guard/defer-style
cleanup tied to the load_user_embeddings_for_search / cache.put flow.

devin-ai-integration[bot]

This comment was marked as resolved.

@AnthonyRonning

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

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.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Devin Review found 1 new potential issue.

Open in Devin Review

Comment thread src/web/openai.rs
Comment on lines +2146 to +2191
pub async fn get_embedding_vector(
state: &Arc<AppState>,
user: &User,
auth_method: AuthMethod,
model: &str,
input: &str,
dimensions: Option<i32>,
) -> Result<(Vec<f32>, i32), ApiError> {
let request = EmbeddingRequest {
input: Value::String(input.to_string()),
model: model.to_string(),
encoding_format: Some("float".to_string()),
dimensions,
user: None,
};

let (response_json, _provider, prompt_tokens) =
request_embeddings_with_billing(state, user, auth_method, &request).await?;

let embedding = response_json
.get("data")
.and_then(|v| v.as_array())
.and_then(|arr| arr.first())
.and_then(|obj| obj.get("embedding"))
.and_then(|v| v.as_array())
.ok_or(ApiError::InternalServerError)?;

let mut vector: Vec<f32> = Vec::with_capacity(embedding.len());
for v in embedding {
let f = v.as_f64().ok_or(ApiError::InternalServerError)?;
vector.push(f as f32);
}

if let Some(dim) = dimensions {
if vector.len() != dim as usize {
error!(
"Embeddings response dim mismatch: expected={}, got={}",
dim,
vector.len()
);
return Err(ApiError::InternalServerError);
}
}

Ok((vector, prompt_tokens))
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔍 New get_embedding_vector public API bills every RAG internal embedding call

The newly exported get_embedding_vector function (src/web/openai.rs:2146) calls request_embeddings_with_billing, which means every RAG insert and search operation triggers a billing event via publish_usage_event_internal. This is called from embed_text_via_tinfoil (src/rag.rs:697-712) for both insert_archival_embedding and search_user_embeddings_with_options. This is likely intentional (embedding API calls have real cost), but it means RAG search queries incur billing for the query embedding in addition to any completion billing. Worth confirming this is the desired billing behavior.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
src/rag.rs (2)

839-880: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Avoid the duplicate project storage-stats scan.

ensure_pre_embedding_limits (Line 928) already runs load_project_embedding_storage_stats, then ensure_storage_limits immediately reloads it again at Line 860. On the insert path this is a full aggregate scan repeated needlessly (and the whole limit block already runs once as a pre-check and again at insert time). Return the already-loaded stats from ensure_pre_embedding_limits and reuse them here.

♻️ Sketch
// ensure_pre_embedding_limits already computes project_stats;
// return (user_stats, project_stats) and reuse in ensure_storage_limits
// instead of calling load_project_embedding_storage_stats a second time.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/rag.rs` around lines 839 - 880, The storage limit check in
ensure_storage_limits is reloading project embedding stats even though
ensure_pre_embedding_limits already computed them, causing a duplicate aggregate
scan. Update ensure_pre_embedding_limits to return both user_stats and
project_stats, then change ensure_storage_limits to reuse the returned
project_stats instead of calling load_project_embedding_storage_stats again;
keep the existing limit comparisons and warning logs in ensure_storage_limits.

1406-1443: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Cache-hit path recomputes byte size O(n) just for a log field.

cached_embeddings_bytes(&embeddings) at Line 1421 re-sums every cached row on each cache hit, even though CacheEntry.bytes is already tracked. For large users (up to scan_limit rows) this adds avoidable per-search CPU on the hottest path. Consider having get also return the stored byte count (or omit cache_bytes_per_user from this hit log).

♻️ Sketch
// return the cached byte size from get():
pub fn get(&mut self, user_id: Uuid) -> Option<(Arc<Vec<CachedEmbedding>>, bool, usize)> {
    // ... include entry.bytes in the tuple, then log it directly.
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/rag.rs` around lines 1406 - 1443, The cache-hit logging in
load_cacheable_embeddings_with_coordination is recomputing
cached_embeddings_bytes(&embeddings) on every hit, which is avoidable work on a
hot path. Update Cache::get (or the cache-hit branch in
load_cacheable_embeddings_with_coordination) to return the stored byte count
from CacheEntry.bytes and log that value directly, or remove
cache_bytes_per_user from the hit log if it is not needed.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/rag.rs`:
- Around line 839-880: The storage limit check in ensure_storage_limits is
reloading project embedding stats even though ensure_pre_embedding_limits
already computed them, causing a duplicate aggregate scan. Update
ensure_pre_embedding_limits to return both user_stats and project_stats, then
change ensure_storage_limits to reuse the returned project_stats instead of
calling load_project_embedding_storage_stats again; keep the existing limit
comparisons and warning logs in ensure_storage_limits.
- Around line 1406-1443: The cache-hit logging in
load_cacheable_embeddings_with_coordination is recomputing
cached_embeddings_bytes(&embeddings) on every hit, which is avoidable work on a
hot path. Update Cache::get (or the cache-hit branch in
load_cacheable_embeddings_with_coordination) to return the stored byte count
from CacheEntry.bytes and log that value directly, or remove
cache_bytes_per_user from the hit log if it is not needed.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 87dc8337-e39b-4062-82d4-fe6882c97692

📥 Commits

Reviewing files that changed from the base of the PR and between a111aa3 and 37045ab.

📒 Files selected for processing (6)
  • migrations/2026-07-07-000001_user_embeddings_hot_path_indexes/down.sql
  • migrations/2026-07-07-000001_user_embeddings_hot_path_indexes/metadata.toml
  • migrations/2026-07-07-000001_user_embeddings_hot_path_indexes/up.sql
  • src/main.rs
  • src/rag.rs
  • src/web/mod.rs
💤 Files with no reviewable changes (1)
  • src/web/mod.rs
✅ Files skipped from review due to trivial changes (1)
  • migrations/2026-07-07-000001_user_embeddings_hot_path_indexes/metadata.toml
🚧 Files skipped from review as they are similar to previous changes (2)
  • migrations/2026-07-07-000001_user_embeddings_hot_path_indexes/down.sql
  • src/main.rs

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