feat(rag): sqlite-vec + Titan Text Embeddings V2 for closest-past-result recall - #5
Conversation
…tests Plain Uint8Array from TextEncoder doesn't satisfy InvokeModelCommandOutput.body's real type under this repo's noUncheckedIndexedAccess/strict typecheck.
|
Warning Review limit reachedYou’ve reached a temporary PR review limit under our Fair Usage Limits Policy. Next review available in: 39 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the 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 configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
📝 WalkthroughSummary by CodeRabbit
WalkthroughThe change adds SQLite vector storage with ChangesVector retrieval pipeline
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
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
Comment |
There was a problem hiding this comment.
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
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (23)
README.mddocs/01-architecture.mddocs/08-rag-vector-search.mdinfra/stack.tspackage.jsonsrc/agent/fetch.tssrc/agent/status.tssrc/db/bootstrap.tssrc/db/open.tssrc/db/schema.tssrc/embed/local.tssrc/embed/titan.tssrc/format/bedrock.tssrc/format/types.tssrc/handler.tssrc/localFetch.tssrc/rag/similarity.tstests/bedrock.test.tstests/db.test.tstests/fetch.test.tstests/similarity.test.tstests/status.test.tstests/titan.test.ts
- 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>
/fix-pr follow-upCommit: Review resolution
Verification
|
There was a problem hiding this comment.
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
fetchwriter path. - Add
agent_embeddings(vec0) plusnearest_match_id/nearest_match_distancestorage, and exposenearestMatchon thestatusendpoint (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.
…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>
/fix-pr follow-upCommit: Review resolution
Verification
|
There was a problem hiding this comment.
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
📒 Files selected for processing (5)
docs/01-architecture.mdsrc/agent/status.tssrc/embed/titan.tstests/status.test.tstests/titan.test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
- docs/01-architecture.md
- src/embed/titan.ts
There was a problem hiding this comment.
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_distanceis written separately fromnearest_match_idinrunFetch, butrunFetchinserts 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_distancescenario as ifrunFetchcould write the id without the distance when embedding fails, butrunFetchcurrently 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.
/fix-pr follow-upCommit: Review resolution
Verification
|
There was a problem hiding this comment.
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}"`);
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().
/fix-pr follow-upCommit: Review resolution
Verification
|
Summary
memory.db: each posted notification is embedded with Amazon Titan Text Embeddings V2 and stored in asqlite-vecvec0virtual table (agent_embeddings), demonstrating SQLite as a vector database, not just a database server.fetchrun: 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 handlingrunFetchalready has for fetch/format/post failures — a Titan outage never blocks a Discord post.statusendpoint'srecentNotifications[]now exposesnearestMatch(source/message/date/cosine distance ornull), read from two new plain columns (nearest_match_id,nearest_match_distance) — the reader never runs a vector query itself.docs/08-rag-vector-search.mdexplains the design;docs/01-architecture.mdandREADME.mdupdated to reference it.amazon.titan-embed-text-v2:0.Notable deviation from the plan
The plan's
INSERT INTO agent_embeddings (notification_id, ...) VALUES (?, ...)boundnotification_idas a plain JS number. Verified against the installedsqlite-vecv0.1.9 +better-sqlite3v13 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 viadb.exec, and as an ordinary table's rowid). Fixed by bindingBigInt(notificationId)instead, in bothsrc/rag/similarity.ts'sinsertEmbeddingand the corresponding schema test — documented inline at both call sites.Test plan
npm test— 105/105 passingnpm run typecheck— cleandocker build— succeeds; verifiedbetter-sqlite3+sqlite-vecload correctly inside the built imagecdk synth— succeeds with the new Titan ARN present in the synthesized IAM policylocal-fetchend-to-end run — not exercised in this environment (no.envwithDISCORD_WEBHOOK_URLconfigured); code path is covered bytests/fetch.test.ts's local-embedder tests🤖 Generated with Claude Code