Skip to content

feat(rag): sqlite-vec + Titan Text Embeddings V2 for closest-past-result recall - #5

Merged
equationalapplications merged 19 commits into
mainfrom
feat/rag-sqlite-vec-titan
Aug 9, 2026
Merged

feat(rag): sqlite-vec + Titan Text Embeddings V2 for closest-past-result recall#5
equationalapplications merged 19 commits into
mainfrom
feat/rag-sqlite-vec-titan

Conversation

@equationalapplications

Copy link
Copy Markdown
Owner

Summary

  • Implements RAG over the writer's memory.db: each posted notification is embedded with Amazon Titan Text Embeddings V2 and stored in a sqlite-vec vec0 virtual table (agent_embeddings), demonstrating SQLite as a vector database, not just a database server.
  • Per fetch run: embed the raw value → KNN-search for the closest same-source past notification → fold that into the Bedrock prompt (formatter may reference it) → post → insert the notification (recording the match + distance) → embed the posted message and store it for future lookups. Both embedding calls are isolated with the same per-source error handling runFetch already has for fetch/format/post failures — a Titan outage never blocks a Discord post.
  • status endpoint's recentNotifications[] now exposes nearestMatch (source/message/date/cosine distance or null), read from two new plain columns (nearest_match_id, nearest_match_distance) — the reader never runs a vector query itself.
  • New docs/08-rag-vector-search.md explains the design; docs/01-architecture.md and README.md updated to reference it.
  • IAM grant added for amazon.titan-embed-text-v2:0.

Notable deviation from the plan

The plan's INSERT INTO agent_embeddings (notification_id, ...) VALUES (?, ...) bound notification_id as a plain JS number. Verified against the installed sqlite-vec v0.1.9 + better-sqlite3 v13 that this throws "Only integers are allowed for primary key values" when bound as a parameter (though the identical value works fine as an inlined literal via db.exec, and as an ordinary table's rowid). Fixed by binding BigInt(notificationId) instead, in both src/rag/similarity.ts's insertEmbedding and the corresponding schema test — documented inline at both call sites.

Test plan

  • npm test — 105/105 passing
  • npm run typecheck — clean
  • docker build — succeeds; verified better-sqlite3 + sqlite-vec load correctly inside the built image
  • cdk synth — succeeds with the new Titan ARN present in the synthesized IAM policy
  • Manual local-fetch end-to-end run — not exercised in this environment (no .env with DISCORD_WEBHOOK_URL configured); code path is covered by tests/fetch.test.ts's local-embedder tests

🤖 Generated with Claude Code

@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 39 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: de9e3a79-f81a-4810-abf7-57104d459285

📥 Commits

Reviewing files that changed from the base of the PR and between 98c8d39 and c96510e.

📒 Files selected for processing (2)
  • src/embed/titan.ts
  • tests/titan.test.ts
📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added similarity-based retrieval of past messages from the same source for formatting context.
    • Added Titan-based cloud embeddings and deterministic local embedding support.
    • Notification status now displays matching message details, timestamp, source, and distance.
    • Embedding failures are reported without blocking message posting.
  • Documentation

    • Added guidance for vector search, embeddings, storage, matching behavior, and limitations.
    • Updated architecture documentation and README links.
  • Tests

    • Expanded coverage for matching, status details, database setup, retries, and failure handling.

Walkthrough

The change adds SQLite vector storage with sqlite-vec, Titan and local embedding providers, same-source nearest-message retrieval, Bedrock prompt context, nearest-match status data, migrations, IAM access, and documentation.

Changes

Vector retrieval pipeline

Layer / File(s) Summary
Vector storage and search
package.json, src/db/*, src/rag/similarity.ts, tests/db.test.ts, tests/similarity.test.ts
The database loads sqlite-vec, creates agent_embeddings, migrates nearest-match columns, and supports same-source nearest-match queries and vector insertion.
Embedding providers and wiring
src/embed/*, infra/stack.ts, src/handler.ts, src/localFetch.ts, tests/titan.test.ts
The change adds deterministic local embeddings and Titan V2 embeddings with validation, retry handling, error mapping, IAM access, and fetch integration.
Fetch matching and formatting
src/agent/fetch.ts, src/format/*, tests/fetch.test.ts, tests/bedrock.test.ts
Fetch embeds raw values, passes matching history to the formatter, stores formatted-message embeddings, and isolates embedding failures.
Status reporting and documentation
src/agent/status.ts, tests/status.test.ts, README.md, docs/*
Status responses expose nearest-match metadata. Documentation describes the vector-search workflow, limits, and failure behavior.

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

Sequence Diagram(s)

sequenceDiagram
  participant Source
  participant runFetch
  participant Embedder
  participant SQLiteVec
  participant Bedrock
  participant Discord
  Source->>runFetch: provide changed value
  runFetch->>Embedder: embed raw value
  Embedder-->>runFetch: return query vector
  runFetch->>SQLiteVec: find same-source nearest match
  SQLiteVec-->>runFetch: return match or null
  runFetch->>Bedrock: format value with similarPast
  Bedrock-->>runFetch: return formatted message
  runFetch->>Discord: post formatted message
  runFetch->>Embedder: embed formatted message
  runFetch->>SQLiteVec: store notification vector
Loading

Possibly related PRs

Poem

A rabbit hops through vectors bright,
Finds past messages by cosine light.
Titan embeds and SQLite stores,
Bedrock adds familiar context doors.
New matches bloom in every run—
Hop, hop, the RAG work’s done!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 58.33% 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
Title check ✅ Passed The title clearly identifies the main RAG change using sqlite-vec and Titan Text Embeddings V2 for closest past-result recall.
Description check ✅ Passed The description directly explains the RAG implementation, embedding workflow, database changes, error handling, tests, and documentation updates.
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 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch

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

@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: 2

🤖 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 `@docs/01-architecture.md`:
- Around line 46-51: Update the architecture description to reflect the actual
sequence: runFetch embeds and searches the raw value before formatting, then
posts the formatted message to Discord, and only afterward embeds and stores
that posted message in sqlite-vec. Remove the implication that the stored
notification embedding supplies the preceding prompt context.

In `@src/embed/titan.ts`:
- Around line 79-83: Update the embedding validation after JSON.parse in the
Titan response flow to require parsed.embedding to be an array of exactly
DIMENSIONS entries, with every entry being a finite number, before returning it.
Preserve the existing malformed-response exception path and align its message
with the expanded validation if needed.
🪄 Autofix

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: 4df01681-03f4-4d58-85a6-1cdba4b5e057

📥 Commits

Reviewing files that changed from the base of the PR and between 1d805e7 and 5b065de.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (23)
  • README.md
  • docs/01-architecture.md
  • docs/08-rag-vector-search.md
  • infra/stack.ts
  • package.json
  • src/agent/fetch.ts
  • src/agent/status.ts
  • src/db/bootstrap.ts
  • src/db/open.ts
  • src/db/schema.ts
  • src/embed/local.ts
  • src/embed/titan.ts
  • src/format/bedrock.ts
  • src/format/types.ts
  • src/handler.ts
  • src/localFetch.ts
  • src/rag/similarity.ts
  • tests/bedrock.test.ts
  • tests/db.test.ts
  • tests/fetch.test.ts
  • tests/similarity.test.ts
  • tests/status.test.ts
  • tests/titan.test.ts

Comment thread docs/01-architecture.md Outdated
Comment thread src/embed/titan.ts
claude added 2 commits August 8, 2026 23:33
- docs/01-architecture.md described the pre-format search embedding as
  happening after the Discord post; document the actual two-step order.
- src/embed/titan.ts now requires exactly DIMENSIONS finite numbers before
  accepting a Titan response, since agent_embeddings is FLOAT[256].

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@equationalapplications

Copy link
Copy Markdown
Owner Author

/fix-pr follow-up

Commit: 86e5752

Review resolution

  • docs/01-architecture.md (embedding sequence): Fixed — rewrote the "Bedrock calls" paragraph to state the actual two-step order: embed the raw value and search before formatting, then embed and store the posted notification after the Discord post.
  • src/embed/titan.ts (embedding validation): Fixed — attempt() now rejects any response whose embedding isn't exactly DIMENSIONS (256) finite numbers, not just a non-empty array. Malformed-response error path and message left unchanged. Added two tests (wrong length, non-finite entry) and widened the existing fixtures to 256-dim vectors in tests/titan.test.ts.

Verification

  • typecheck — pass (npm run typecheck)
  • lint — N/A (no lint script in package.json)
  • tests — pass, 107/107 (npm test)

Copilot AI 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.

Pull request overview

This PR adds a lightweight RAG-style “closest past result” recall feature to the tutorial agent by embedding notifications with Amazon Titan Text Embeddings V2 and storing/querying them via a sqlite-vec vec0 table inside the existing memory.db snapshot.

Changes:

  • Add Titan embedder + local embedder abstraction and integrate embedding + nearest-match lookup into the fetch writer path.
  • Add agent_embeddings (vec0) plus nearest_match_id / nearest_match_distance storage, and expose nearestMatch on the status endpoint (read-only, no vector queries in the reader).
  • Add tests and docs covering embedding behavior, similarity search, schema/bootstrapping, and the end-to-end fetch/status contract.

Reviewed changes

Copilot reviewed 24 out of 25 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
tests/titan.test.ts Unit tests for Titan embedder request/response validation + retry/error mapping
tests/status.test.ts Updates status expectations and adds nearestMatch coverage
tests/similarity.test.ts Exercises sqlite-vec KNN + same-source filtering and embedding insertion
tests/fetch.test.ts Validates nearest-match recording + embed failure isolation in runFetch
tests/db.test.ts Ensures schema creates vec0 table, loads sqlite-vec, and adds nearest_match columns idempotently
tests/bedrock.test.ts Ensures formatter prompt includes/omits nearestMatch context appropriately
src/rag/similarity.ts New sqlite-vec KNN query + embedding insertion helpers
src/localFetch.ts Wires local embedder into the local fetch entrypoint
src/handler.ts Wires Titan embedder into the deployed handler path
src/format/types.ts Extends formatter interface to accept optional similarity context
src/format/bedrock.ts Includes nearestMatch context in the Bedrock user prompt
src/embed/titan.ts Implements Titan v2 embedding via InvokeModel with retry/error mapping
src/embed/local.ts Adds deterministic local embedder for non-AWS testing/dev
src/db/schema.ts Adds agent_embeddings vec0 virtual table to DDL
src/db/open.ts Loads sqlite-vec extension for writer DB connections
src/db/bootstrap.ts Adds idempotent nearest_match column migration logic
src/agent/status.ts Joins nearest-match info into status output (no vector query)
src/agent/fetch.ts Performs nearest-match lookup + stores embeddings, with per-source isolation
README.md Documents sqlite-vec + Titan embeddings feature at a high level
package.json Adds sqlite-vec dependency
package-lock.json Locks sqlite-vec and platform optional deps
infra/stack.ts Adds IAM resource for Titan embed model invocation
docs/superpowers/specs/2026-08-08-rag-sqlite-vec-titan-design.md Marks the design spec as implemented
docs/08-rag-vector-search.md New doc explaining the end-to-end vector-search design
docs/01-architecture.md Updates architecture doc to mention embedding + vector lookup flow

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/agent/status.ts Outdated
Comment thread src/agent/status.ts Outdated
…h data

Two related robustness fixes for the status endpoint:

1. Pre-RAG snapshots don't have `nearest_match_id` / `nearest_match_distance`
   on `agent_notifications`. The read-only status endpoint can be invoked
   against such a snapshot before the next fetch run migrates it. Previously
   the joined query would throw `no such column: n.nearest_match_distance`
   and the endpoint would fail. Now `queryStatus` feature-detects the columns
   via `PRAGMA table_info(agent_notifications)` (mirroring the bootstrap
   pattern) and falls back to the pre-RAG query shape, returning
   `nearestMatch: null` for every row.

2. `nearestMatch` was emitted whenever `matched_source` was non-null, with
   `nearest_match_distance`, `matched_formatted_message`, and
   `matched_posted_at` force-cast to non-null. A partial-write row (e.g.,
   `nearest_match_id` set but `nearest_match_distance` null after an isolated
   embed failure, or a matched row deleted under the unenforced FK) would
   produce an object with `distance: null` against the declared `number`
   type. Now guards on all four dependent columns being non-null before
   constructing `nearestMatch`; any missing component collapses to `null`,
   matching the existing "no match to show" outcome for first notifications.

Adds two tests:
- pre-RAG snapshot returns `nearestMatch: null` for all rows without throwing
- partial-data row with null distance returns `nearestMatch: null`

Co-Authored-By: Claude <noreply@anthropic.com>
@equationalapplications

Copy link
Copy Markdown
Owner Author

/fix-pr follow-up

Commit: bb45e80

Review resolution

  • src/agent/status.ts — older snapshot has no RAG columns: Fixed. queryStatus now feature-detects nearest_match_id / nearest_match_distance via PRAGMA table_info(agent_notifications) (mirroring addNearestMatchColumnsIfMissing in src/db/bootstrap.ts) and falls back to the pre-RAG query shape when absent, returning nearestMatch: null for every row. This means the read-only status endpoint no longer throws no such column: n.nearest_match_distance if invoked against a memory.db written before the RAG feature shipped, before the next fetch run has a chance to migrate it.
  • src/agent/status.ts — partial-data row emits distance: null despite number type: Fixed. nearestMatch now requires all four dependent columns — matched_source, matched_formatted_message, matched_posted_at, and nearest_match_distance — to be non-null before constructing the object. Any missing component collapses to null, matching the existing "no complete match to show" outcome for first-ever notifications and isolated embed failures.

Verification

  • typecheck — pass (npm run typecheck)
  • tests — pass, 109/109 (npm test), up from 105 with two new tests:
    • falls back to nearestMatch: null when the snapshot predates the RAG columns — seeds a memory.db whose agent_notifications lacks the RAG columns and asserts the endpoint returns the row with nearestMatch: null instead of throwing.
    • returns nearestMatch: null when nearest_match_id is set but nearest_match_distance is null — seeds a partial-write row and asserts the nullable guard returns null instead of an object with distance: null.

@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: 1

🤖 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 `@tests/titan.test.ts`:
- Around line 88-91: Update the non-finite-value test using embeddingResponse so
it supplies a raw JSON response body containing 1e400 instead of serializing
badVector with Number.NaN. Keep the test asserting that the response with an
Infinity entry throws without retrying, ensuring JSON.parse produces the
non-finite value.
🪄 Autofix

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: 5372c8f3-e812-4110-93f1-19266927aac9

📥 Commits

Reviewing files that changed from the base of the PR and between 4b0d3df and bb45e80.

📒 Files selected for processing (5)
  • docs/01-architecture.md
  • src/agent/status.ts
  • src/embed/titan.ts
  • tests/status.test.ts
  • tests/titan.test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • docs/01-architecture.md
  • src/embed/titan.ts

Comment thread tests/titan.test.ts Outdated

Copilot AI 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.

Pull request overview

Copilot reviewed 24 out of 25 changed files in this pull request and generated no new comments.

Suppressed comments (2)

src/agent/status.ts:148

  • The comment here says nearest_match_distance is written separately from nearest_match_id in runFetch, but runFetch inserts both columns in the same statement. Keeping this comment accurate will avoid future confusion about how a NULL distance could arise.
      // Guard on every dependent column being non-null, not just `matched_source`.
      // `nearest_match_distance` is written separately from `nearest_match_id` in
      // `runFetch`, so a partial-write row (or one whose matched row was deleted
      // out from under the unenforced FK) can have the join columns populated but
      // the distance null — emitting `distance: null` against a `number` type would

tests/status.test.ts:322

  • This test comment explains the NULL nearest_match_distance scenario as if runFetch could write the id without the distance when embedding fails, but runFetch currently inserts both fields together (and writes both NULL when embedding/match fails). Updating the comment will keep it aligned with the actual writer behavior.
    // Partial-write row: the join lands on a real matched notification, but the
    // distance column is null (e.g., the embed step failed and was isolated for
    // the match-lookup but still wrote the id without the distance). Emitting an
    // object with `distance: null` against the declared `number` type would be a
    // silent lie — guard on every dependent column being non-null and return null.

`embeddingResponse(badVector)` serializes Number.NaN as null, so the
test was failing the `typeof === 'number'` check first and never
reached the Number.isFinite guard. Hand-craft a response body that
contains `1e400` so JSON.parse produces Infinity and exercises the
finite-value check directly.
@equationalapplications

Copy link
Copy Markdown
Owner Author

/fix-pr follow-up

Commit: 98c8d39

Review resolution

  • tests/titan.test.ts:88-91embeddingResponse(badVector) serialized Number.NaN as null, failing the typeof === 'number' check before Number.isFinite ever ran. Fixed: Hand-crafted a raw response body containing 1e400 (which JSON.parse reads as Infinity) so the test now exercises the finite-value guard directly. Test still asserts a single non-retrying throw.
  • src/agent/status.ts:148 and tests/status.test.ts:322 comments about nearest_match_distance being written separately from nearest_match_idNot applied: Both comments are marked suppressed by the reviewer; runFetch does insert both columns in a single statement and the comments were deemed inaccurate. Left as-is per the reviewer's suppression call.

Verification

  • npx vitest run tests/titan.test.ts — 7/7 passing (covers the non-finite path)
  • npm run typecheck — clean
  • npm test — 109/109 passing

@equationalapplications
equationalapplications requested a balanced review from Copilot August 9, 2026 10:52

Copilot AI 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.

Pull request overview

Copilot reviewed 24 out of 25 changed files in this pull request and generated 1 comment.

Suppressed comments (1)

src/embed/titan.ts:85

  • This message is also emitted when Titan returns an array with the wrong dimension or a non-finite element, so it incorrectly reports that no embedding was returned and hides the actual response-contract failure. Use a message that describes an invalid embedding and the expected shape.
      throw new Error(`Titan returned no embedding for model "${MODEL_ID}"`);

Comment thread src/embed/titan.ts
Each InvokeModel send() now carries an AbortSignal.timeout(5s) so a
stalled SDK call can't eat the 30s Lambda budget (infra/stack.ts:87)
before the per-source try/catch in runFetch gets a chance to isolate
the failure (RAG design spec §6). Addresses the copilot review on
PR #5: 'This pre-post Bedrock call has no request deadline...'

The 5s per-attempt budget leaves wall-clock for the worst-case
surrounding work (pre-embed + retry, KNN, format + retry, post,
DB inserts, post-embed + retry) inside the Lambda timeout. If this
ever grows, prefer raising the Lambda timeout over extending this
constant — Titan latency is normally well under 2s in the happy path.

Adds a regression test verifying the AbortSignal is wired through
the HttpHandlerOptions passed to client.send().
@equationalapplications

Copy link
Copy Markdown
Owner Author

/fix-pr follow-up

Commit: c96510e

Review resolution

  • Thread / topic: src/embed/titan.ts — no request deadline on the pre-post Bedrock call could let a stalled send() eat the 30s Lambda budget, contradicting the spec guarantee that a Titan outage does not block notifications — Fixed by adding AbortSignal.timeout(REQUEST_TIMEOUT_MS) (5s) to client.send(new InvokeModelCommand(...), { abortSignal }). The new constant carries an inline budget-reasoning comment that maps back to the Lambda timeout, the surrounding work (KNN, Converse format, Discord post, DB inserts, second embed), and the spec guarantee. Added a regression test in tests/titan.test.ts that verifies the AbortSignal is wired through HttpHandlerOptions. (files: src/embed/titan.ts, tests/titan.test.ts)

Verification

  • typecheck — pass (npm run typecheck)
  • lint — N/A (no lint script in package.json)
  • tests — pass (npm test, 110/110, +1 new test)

@equationalapplications
equationalapplications merged commit 834cc2b into main Aug 9, 2026
1 check passed
@equationalapplications
equationalapplications deleted the feat/rag-sqlite-vec-titan branch August 9, 2026 11:04
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.

3 participants