fix(embedding): guard provider responses against dimension mismatches#248
Conversation
Closes rohitg00#247. Embedding providers in src/providers/embedding/ trust that the API returns vectors matching their declared dimensions. None of them check result.length === this.dimensions. When that breaks, the failure is silent: src/state/vector-index.ts:10 returns 0 from cosineSimilarity on length mismatch, so a wrong-size vector gets stored, never matches anything, and the affected memory becomes invisible without a single error surfacing. Add a single dimension-check wrapper at the EmbeddingProvider boundary in createEmbeddingProvider() / createImageEmbeddingProvider(). Every provider inherits the guard for free; new providers added later are covered automatically. Throws a descriptive error naming the provider, the call site (embed / embedBatch[i] / embedImage), expected vs got dimensions. Tests cover the happy path and each method's mismatch path. Signed-off-by: ammarsaleh50 <ammar.alammar23@gmail.com>
|
@AmmarSaleh50 is attempting to deploy a commit to the rohitg00's projects Team on Vercel. A member of the Team first needs to authorize it. |
📝 WalkthroughWalkthroughAdds exported withDimensionGuard(provider) that enforces provider.dimensions on embed/embedBatch/(optional)embedImage outputs and wraps embedding factories; adds VectorIndex.validateDimensions(expected) and a restore-time check that compares persisted vectors to the active provider, optionally discarding or failing on mismatches; includes tests for the guard and index validation. ChangesEmbedding Dimension Validation & Index Safety
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related issues
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
src/providers/embedding/index.ts (1)
52-55: ⚡ Quick winRemove the WHAT-style comment block and let naming carry intent
This block explains behavior in prose; repo guidelines for
src/**/*.tsask to avoid WHAT-comments.As per coding guidelines: “Avoid code comments explaining WHAT — use clear naming instead”.
🤖 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/providers/embedding/index.ts` around lines 52 - 55, Delete the WHAT-style prose block that describes the silent failure and instead make the code self-describing: remove the comment and add/rename a boundary function in vector-index.ts to validate dimensions (e.g., ensureDimensionsMatchOrThrow) or rename cosineSimilarity to reflect its behavior (e.g., cosineSimilarityOrThrowOnDimensionMismatch), and call that validator/wrapper from the embedding entrypoint so mismatched-length vectors throw instead of silently returning 0; this keeps intent in names rather than a prose comment.
🤖 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 `@src/providers/embedding/index.ts`:
- Around line 66-75: The wrapper currently constructs a plain object assigned to
wrapped which loses the provider's prototype and breaks instanceof checks (e.g.,
GeminiEmbeddingProvider, OpenAIEmbeddingProvider); instead create the wrapper
with the original prototype (use Object.create(provider)) so class identity is
preserved, then override/assign the name, dimensions, embed and embedBatch
properties while keeping embed/embedBatch calls wrapped with the existing
check(...) logic to validate outputs.
---
Nitpick comments:
In `@src/providers/embedding/index.ts`:
- Around line 52-55: Delete the WHAT-style prose block that describes the silent
failure and instead make the code self-describing: remove the comment and
add/rename a boundary function in vector-index.ts to validate dimensions (e.g.,
ensureDimensionsMatchOrThrow) or rename cosineSimilarity to reflect its behavior
(e.g., cosineSimilarityOrThrowOnDimensionMismatch), and call that
validator/wrapper from the embedding entrypoint so mismatched-length vectors
throw instead of silently returning 0; this keeps intent in names rather than a
prose comment.
🪄 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: defaults
Review profile: CHILL
Plan: Pro
Run ID: 733047e6-120c-4fb4-8253-e272662741b7
📒 Files selected for processing (2)
src/providers/embedding/index.tstest/embedding-provider.test.ts
Address CodeRabbit review on rohitg00#248: the wrapper built a plain object instead of preserving the provider's prototype chain, which broke the existing toBeInstanceOf(GeminiEmbeddingProvider) / toBeInstanceOf(OpenAIEmbeddingProvider) checks against createEmbeddingProvider() in test/embedding-provider.test.ts. Switch the wrapper to Object.create(provider) so the prototype chain is preserved. `name` and `dimensions` fall through to the underlying provider; only `embed` / `embedBatch` / `embedImage` are overridden to insert the dimension check. Add a regression test asserting that `withDimensionGuard` preserves `instanceof`. Signed-off-by: ammarsaleh50 <ammar.alammar23@gmail.com>
|
This is a clean, well-thought fix. Reviewed end to end: Approach is right. Prototype-chain preservation is the detail that matters. Per-vector check on Test coverage is comprehensive. Good and bad paths for embed, embedBatch, and embedImage. The fake-provider helper is the right shape for this. Together with #246 (Gemini migration that explicitly passes No blockers. Approving — also in the land-soon bucket, holding for @rohitg00. Two things to consider after merge (not blocking):
|
|
Re-reviewed independently against the alternatives we'd consider, not just rubber-stamping. Conclusion: this is the right approach. Specifically: Why factory-boundary > the alternatives
The The per-vector index in Together with #246, your two PRs close the silent-corruption window front-to-back: #246 explicitly passes One real follow-up gap (not blocking this PR): The Worth a follow-up issue: validate dimensions against the active provider at Verdict: APPROVE. This was the highest-quality PR in the current backlog — issue + fix + tests in one well-scoped contribution. Strong work, @AmmarSaleh50. Holding for @rohitg00 to push the merge button. |
…atches active provider The factory-boundary dimension guard in this PR catches wrong-dim vectors on the live-API write path. The persistence restore path is the symmetric on-ramp: IndexPersistence.load() at src/state/index-persistence.ts:62-66 deserializes vectors directly from KV with no dimension check. If a user persists an index built against an N-dim provider and then swaps embedding configuration (EMBEDDING_PROVIDER, OPENAI_EMBEDDING_MODEL, local model upgrade, etc.), the restore brings old-dim vectors back into a freshly-instantiated VectorIndex while live observations write new-dim vectors alongside. cosineSimilarity returns 0 on every cross- dim pair — same silent recall degradation rohitg00#247 documents, just on a different on-ramp. This commit adds: 1. VectorIndex.firstDimensions() — exposes the dimension of any stored vector (or 0 if empty). All vectors in a single index are expected to share a dimension; the first entry is representative because the live-write path is now gated by the guard added earlier in this PR. 2. A startup check in src/index.ts after IndexPersistence.load(). When the persisted index has a different dimension than the active provider, the default behavior is to refuse to start with a clear error message: [agentmemory] Refusing to start: persisted vector index has dimension 384, but the active provider (openai) declares 1536. Loading would silently corrupt search (cross-dimension cosine returns 0). Choose one: - Re-embed the existing index against the new provider, then start. - Set AGENTMEMORY_DROP_STALE_INDEX=true to discard the persisted vectors and rebuild from live observations. - Switch the embedding provider back to the one that wrote the index. 3. Opt-in escape hatch: AGENTMEMORY_DROP_STALE_INDEX=true logs a warning, discards the persisted vectors, and lets the live path rebuild over time. Friendlier for users who deliberately swap providers. Test: VectorIndex.firstDimensions() returns 0 for empty and the correct size for populated indexes (small + 1536-dim). Closes rohitg00#256.
|
Pushed What the commit adds1. 2. Dimension validation at startup in
3. Test in Why it stays in scope
Tested locally:
If you'd rather have this as a separate PR for clean attribution / lighter review surface, say the word and I'll split it. But I think the bundle is more useful here. |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
src/index.ts (1)
339-345: ⚡ Quick winTrim explanatory inline comments and rely on the guard logic + naming.
This block can stay clear without the added “what” comments.
As per coding guidelines, "Avoid code comments explaining WHAT — use clear naming instead".
🤖 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/index.ts` around lines 339 - 345, Remove the verbose explanatory inline comments in the vector-dimension guard block and replace them with a concise comment that names the guard behavior; keep a short note like "Refuse to load vectors with mismatched dimension." and rely on the existing guard logic (the dimension check that refuses loading and the same defense used by the live-write path referenced in the code) and symbols such as cosineSimilarity to make intent clear; do not change the guard logic itself—only trim the explanatory text to a single-line summary.src/state/vector-index.ts (1)
69-72: ⚡ Quick winRemove inline “what” comments and keep this self-descriptive via naming.
These comments describe behavior directly and can be removed to align with the project convention.
♻️ Proposed cleanup
- // Dimension of any stored vector, or 0 if the index is empty. All vectors - // in a single index are expected to share the same dimension; the first - // entry is representative because the live-write path is gated by the - // dimension guard in src/providers/embedding/index.ts. firstDimensions(): number {As per coding guidelines, "Avoid code comments explaining WHAT — use clear naming instead".
🤖 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/state/vector-index.ts` around lines 69 - 72, Remove the inline "what" comment block describing vector dimensions and make the code self-descriptive: delete the three-line comment and, if needed, rename the exposed identifier (e.g., dimension or vectorDimension/embeddingDimension) or add a clearer identifier so the comment's information is conveyed by the name of the variable/property in the vector index module (e.g., vectorDimension or embeddingDimension) and any brief docstring can be limited to why rather than what.
🤖 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 `@src/index.ts`:
- Around line 346-348: The code currently uses loaded.vector.firstDimensions()
(persistedDim) to compare against embeddingProvider?.dimensions (activeDim),
which misses mixed-dimension entries; update the validation to iterate over all
persisted vectors in loaded.vector and verify each vector's dimension equals
activeDim (or fail/skip any that don't) instead of relying on firstDimensions();
apply the same fix to the other similar check around the second occurrence
(lines referenced in the review). Ensure you log or surface which vector(s) were
invalid and prevent restoring indexes containing mismatched-dimension vectors.
---
Nitpick comments:
In `@src/index.ts`:
- Around line 339-345: Remove the verbose explanatory inline comments in the
vector-dimension guard block and replace them with a concise comment that names
the guard behavior; keep a short note like "Refuse to load vectors with
mismatched dimension." and rely on the existing guard logic (the dimension check
that refuses loading and the same defense used by the live-write path referenced
in the code) and symbols such as cosineSimilarity to make intent clear; do not
change the guard logic itself—only trim the explanatory text to a single-line
summary.
In `@src/state/vector-index.ts`:
- Around line 69-72: Remove the inline "what" comment block describing vector
dimensions and make the code self-descriptive: delete the three-line comment
and, if needed, rename the exposed identifier (e.g., dimension or
vectorDimension/embeddingDimension) or add a clearer identifier so the comment's
information is conveyed by the name of the variable/property in the vector index
module (e.g., vectorDimension or embeddingDimension) and any brief docstring can
be limited to why rather than what.
🪄 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: defaults
Review profile: CHILL
Plan: Pro
Run ID: 3ff42410-6f54-4f6f-af90-3e3af81c357d
📒 Files selected for processing (3)
src/index.tssrc/state/vector-index.tstest/vector-index-dimensions.test.ts
Review feedback on 57311c6: firstDimensions() only sampled the first stored vector. A legacy on-disk index that mixes dimensions inside one file (possible when a previous run swapped embedding model mid-session before the live-API guard in this PR existed) would slip past — the first vector might match while later ones don't, and the silent cross-dim cosine corruption returns through the back door. - Replace VectorIndex.firstDimensions() with validateDimensions(expected) that walks every entry, returns the obsIds whose dim doesn't match, and reports the set of distinct dims actually seen on disk. - src/index.ts now refuses to load whenever any single vector mismatches (not just the bulk dim). Error message reports how many of the total are bad, the distinct dims seen on disk, and the first 5 mismatched obsIds so the user can spot-check. - Test rewrite covers: empty index, all-match, partial mismatch (the case firstDimensions() missed), and entire-index mismatch. The reviewer also flagged "the other similar check around the second occurrence" — verified there is no second occurrence in the codebase. The only restoreFrom site is bm25Index.restoreFrom at src/index.ts:333 which restores a BM25 index that doesn't carry dimensions. Skipping.
|
Pushed Verification per finding
The only What changed
Validation
PR is now ready for merge from my side. |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
src/state/vector-index.ts (1)
69-76: ⚡ Quick winRemove the explanatory WHAT block comment and rely on naming/types.
This comment narrates behavior rather than encoding intent in code, which conflicts with the repo rule for
src/**/*.ts.As per coding guidelines, "Avoid code comments explaining WHAT — use clear naming instead".
🤖 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/state/vector-index.ts` around lines 69 - 76, Remove the multi-line explanatory WHAT comment above the vector-walking logic and rely on clear naming and types instead: delete that block and ensure the surrounding symbols (the function that walks stored vectors, and its return fields/variables named mismatches, seenDimensions, and expected) have descriptive names and proper TypeScript typings so the behavior is obvious without the long narrative comment.
🤖 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/state/vector-index.ts`:
- Around line 69-76: Remove the multi-line explanatory WHAT comment above the
vector-walking logic and rely on clear naming and types instead: delete that
block and ensure the surrounding symbols (the function that walks stored
vectors, and its return fields/variables named mismatches, seenDimensions, and
expected) have descriptive names and proper TypeScript typings so the behavior
is obvious without the long narrative comment.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 25263d18-d616-414f-835a-3bb694367c83
📒 Files selected for processing (3)
src/index.tssrc/state/vector-index.tstest/vector-index-dimensions.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- src/index.ts
Bug-fix patch focused on search recall correctness and plugin compatibility. Pins iii-engine to v0.11.2 because v0.11.6 introduces a new sandbox-everything-via-`iii worker add` model that agentmemory hasn't been refactored for yet — pin lifts once that refactor lands. Adds a hard guard against silent vector-index corruption, fixes BM25 indexing for memories saved via memory_save, and lands four Hermes plugin fixes. Per AGENTS.md release checklist: - package.json version 0.9.4 -> 0.9.5 - src/version.ts VERSION constant - src/types.ts ExportData version union - src/functions/export-import.ts supportedVersions Set - test/export-import.test.ts assertion - plugin/.claude-plugin/plugin.json version - CHANGELOG.md detailed entries with contributor shoutouts Headlines (full detail in CHANGELOG): Fixed: - BM25 search now indexes memories saved via memory_save (#258, #257) Thanks @Nizar-BenHamida for the precise repro. - Embedding providers no longer silently corrupt the vector index when an API returns wrong-dimension vectors (#248, #247, #256) Thanks @AmmarSaleh50 for issue + fix + tests. - Hermes handle_tool_call returns JSON strings, not raw dicts (#255, #254) Thanks @KyoMio for the Anthropic-protocol repro. - Hermes status reflects real service state on systemd installs (#253, #250) Thanks @OptionalCoin for tracing it to env-source divergence. - Hermes hooks accept passthrough kwargs (#252, #249) Thanks @OptionalCoin again for the log analysis. - agentmemory demo now seeds observations correctly (#251, #229) Thanks @seishonagon for root-cause analysis. - LLM compression / summarization timeouts increased (#213) Thanks @xuli500177. - Pi / OpenClaw / Hermes integration plugin fixes (#230) Thanks @deepmroot. Changed: - iii-engine pinned to v0.11.2 across every install path (#260). v0.11.6 introduces a new `iii worker add` sandbox model that agentmemory still pre-dates; pin lifts when we refactor agentmemory to register as a sandboxed worker. Override with AGENTMEMORY_III_VERSION=<version> for users who've migrated manually. - README documents iii worker add extension surface (#242). - README iii Console install/launch commands corrected (#243). Validated: 852/852 tests pass, npm run build clean.
Bug-fix patch focused on search recall correctness and plugin compatibility. Pins iii-engine to v0.11.2 because v0.11.6 introduces a new sandbox-everything-via-`iii worker add` model that agentmemory hasn't been refactored for yet — pin lifts once that refactor lands. Adds a hard guard against silent vector-index corruption, fixes BM25 indexing for memories saved via memory_save, and lands four Hermes plugin fixes. Per AGENTS.md release checklist: - package.json version 0.9.4 -> 0.9.5 - src/version.ts VERSION constant - src/types.ts ExportData version union - src/functions/export-import.ts supportedVersions Set - test/export-import.test.ts assertion - plugin/.claude-plugin/plugin.json version - CHANGELOG.md detailed entries with contributor shoutouts Headlines (full detail in CHANGELOG): Fixed: - BM25 search now indexes memories saved via memory_save (#258, #257) Thanks @Nizar-BenHamida for the precise repro. - Embedding providers no longer silently corrupt the vector index when an API returns wrong-dimension vectors (#248, #247, #256) Thanks @AmmarSaleh50 for issue + fix + tests. - Hermes handle_tool_call returns JSON strings, not raw dicts (#255, #254) Thanks @KyoMio for the Anthropic-protocol repro. - Hermes status reflects real service state on systemd installs (#253, #250) Thanks @OptionalCoin for tracing it to env-source divergence. - Hermes hooks accept passthrough kwargs (#252, #249) Thanks @OptionalCoin again for the log analysis. - agentmemory demo now seeds observations correctly (#251, #229) Thanks @seishonagon for root-cause analysis. - LLM compression / summarization timeouts increased (#213) Thanks @xuli500177. - Pi / OpenClaw / Hermes integration plugin fixes (#230) Thanks @deepmroot. Changed: - iii-engine pinned to v0.11.2 across every install path (#260). v0.11.6 introduces a new `iii worker add` sandbox model that agentmemory still pre-dates; pin lifts when we refactor agentmemory to register as a sandboxed worker. Override with AGENTMEMORY_III_VERSION=<version> for users who've migrated manually. - README documents iii worker add extension surface (#242). - README iii Console install/launch commands corrected (#243). Validated: 852/852 tests pass, npm run build clean.
Bug-fix patch focused on search recall correctness and plugin compatibility. Pins iii-engine to v0.11.2 because v0.11.6 introduces a new sandbox-everything-via-`iii worker add` model that agentmemory hasn't been refactored for yet — pin lifts once that refactor lands. Adds a hard guard against silent vector-index corruption, fixes BM25 indexing for memories saved via memory_save, and lands four Hermes plugin fixes. Per AGENTS.md release checklist: - package.json version 0.9.4 -> 0.9.5 - src/version.ts VERSION constant - src/types.ts ExportData version union - src/functions/export-import.ts supportedVersions Set - test/export-import.test.ts assertion - plugin/.claude-plugin/plugin.json version - CHANGELOG.md detailed entries with contributor shoutouts Headlines (full detail in CHANGELOG): Fixed: - BM25 search now indexes memories saved via memory_save (#258, #257) Thanks @Nizar-BenHamida for the precise repro. - Embedding providers no longer silently corrupt the vector index when an API returns wrong-dimension vectors (#248, #247, #256) Thanks @AmmarSaleh50 for issue + fix + tests. - Hermes handle_tool_call returns JSON strings, not raw dicts (#255, #254) Thanks @KyoMio for the Anthropic-protocol repro. - Hermes status reflects real service state on systemd installs (#253, #250) Thanks @OptionalCoin for tracing it to env-source divergence. - Hermes hooks accept passthrough kwargs (#252, #249) Thanks @OptionalCoin again for the log analysis. - agentmemory demo now seeds observations correctly (#251, #229) Thanks @seishonagon for root-cause analysis. - LLM compression / summarization timeouts increased (#213) Thanks @xuli500177. - Pi / OpenClaw / Hermes integration plugin fixes (#230) Thanks @deepmroot. Changed: - iii-engine pinned to v0.11.2 across every install path (#260). v0.11.6 introduces a new `iii worker add` sandbox model that agentmemory still pre-dates; pin lifts when we refactor agentmemory to register as a sandboxed worker. Override with AGENTMEMORY_III_VERSION=<version> for users who've migrated manually. - README documents iii worker add extension surface (#242). - README iii Console install/launch commands corrected (#243). Validated: 852/852 tests pass, npm run build clean.
…x param Four reviewer findings applied. One skipped with reason: APPLIED: 1. Per-session isolation in migrateVectorIndex observations phase. The previous shape wrapped the entire sessions for-loop in one try/catch — a single bad session (kv.list throws, embedBatch rejects, etc.) aborted every subsequent session and silently truncated the migration. The loop now has per-session try/catch; failures increment `failed`, append the session id to a new `failedSessions` array on the result, and the loop continues. The kv.list<sessions> call itself is now guarded separately — if THAT fails the whole migration aborts (no sessions to iterate), but with `failed` recorded and `failedSessions` empty so the caller can distinguish. 2. Drop `oldIndex` parameter from migrateVectorIndex signature. It was dead — no read path inside the function. Three test call sites updated to match the new signature migrateVectorIndex(kv, newProvider). Confused the API contract; if we ever DO need a previous-index argument it can come back with a real use case. 3. Dim guard on every embedding result inside migrateVectorIndex, in both the memories phase and the per-session observations phase. Extracted into a local isValidEmbedding() helper that mirrors the shape of search.ts::vectorIndexAddGuarded's guard: log structured warn + skip on mismatch. Without this, a misconfigured provider returning the wrong-length Float32Array would corrupt the rebuilt index, defeating the #248 persistence-load guard. 4. Rename `vi` -> `vectorIndex` in test/vector-index-populate.test.ts so the let binding doesn't shadow the vitest `vi` import. Practical impact: future tests in that file can use `vi.fn()` / `vi.spyOn()` without renaming this binding first. Cosmetic but cheap. SKIPPED (with reason): - Use embedBatch in rebuildIndex (search.ts): perf optimization, not correctness. Already called out as deferred in the previous review's commit message. Cold-restart speed matters but the fix here is bigger refactor (collect-into-batches with metadata, validate each, add only successful) and worth its own PR. Will pick up when we also do the write-behind queue for live-write batching. Result envelope: Added `failedSessions: string[]` to MigrateVectorIndexResult so the caller can decide whether to retry per-session or just abort. Existing fields (success, totalProcessed, failed, vectorSize) unchanged in shape. 886 / 886 tests pass. Build clean.
…#295, supersedes #296) (#327) * fix #2: BM25 tokenizer now preserves Cyrillic/Unicode via \p{L}\p{N} regex Signed-off-by: Nikita Nosov <20nik.nosov21@gmail.com> * fix #1: add getVectorIndex/setVectorIndex + vector embedding in rebuildIndex Signed-off-by: Nikita Nosov <20nik.nosov21@gmail.com> * fix #1+#3: vectorIndex.add in remember/observe/compress + setVectorIndex in index.ts Signed-off-by: Nikita Nosov <20nik.nosov21@gmail.com> * fix: add migration script, Cyrillic BM25 tests, vector-populate tests, rebuildIndex vector test Adds migrateVectorIndex utility for re-embedding when embedding provider dimensions change. Adds comprehensive test coverage for Cyrillic tokenization, vector index population on remember/rebuildIndex, and migration edge cases. Signed-off-by: Nikita Nosov <20nik.nosov21@gmail.com> * chore: remove internal research spec from PR branch Signed-off-by: Nikita Nosov <20nik.nosov21@gmail.com> * refactor(search): centralize vector-index writes via vectorIndexAddGuarded Pre-merge hardening on top of @nik1t7n's PR #296. Original PR had vectorIndex.add() inlined at four sites (remember, observe, compress, rebuildIndex) with the same try/catch shape repeated each time. That open-codes three sharp edges per site: 1. Missing dimension guard at the write boundary. Per #248, persistence load already refuses to start when persisted vectors mismatch the active provider's dimensions — but the live-write path had no symmetric check. A mis-configured embedding provider returning a different-length Float32Array would silently corrupt the in-memory index until next restart cleared it. 2. No input clipping. Memory.content can be arbitrarily large (the MemorySlot path even allows 20KB content per slot). Most embedding providers cap input around 8k tokens and 400 on overflow. A single oversize memory could throw at the provider and lose the vector entry; worse, on local-Ollama-style providers it can cost real inference time. 3. Inconsistent error logging (rebuildIndex had `catch {}` swallowing the error entirely, the other three sites logged via logger.warn with slightly different shape). Consolidates into a single exported `vectorIndexAddGuarded()` in src/functions/search.ts: - Reads vectorIndex + currentEmbeddingProvider from module-level state (same singletons the PR added). - Clips embed input to 16k chars (EMBED_MAX_CHARS) — well under every provider's documented limit, leaves headroom for chars-per-token variance across languages. - Validates embedding.length === provider.dimensions before calling vi.add(); logs structured warning and skips on mismatch. - Single logger.warn on embed failure with provider name + error message + indexed-item kind ("memory" | "observation" | "synthetic") + id. - Returns boolean (true on success) so callers can branch if they want, but every existing call site fire-and-forgets — the contract is "soft-fail: index entry skipped, upstream save still succeeds". Rewrites the four call sites (remember.ts, observe.ts synthetic branch, compress.ts post-LLM, search.ts rebuildIndex memories + obs loops) to call vectorIndexAddGuarded() instead of the inline try/catch. Side effect: cleans up the lingering `catch {}` in rebuildIndex that was swallowing rebuild-time embed failures silently — every failure now surfaces in logs at a consistent shape. Tests: - test/multimodal.test.ts mocked `../src/functions/search.js` and exposed only getSearchIndex; needed vectorIndexAddGuarded added to the mock surface or test fails with "No vectorIndexAddGuarded export is defined on the search.js mock". Added the export to the mock. - All other tests (search-index Cyrillic, search rebuildIndex vector, vector-index-populate, migrate-vector-index) green on the helper refactor — no behavioural change beyond the three guards above. 886 / 886 tests pass. Build clean. Out-of-scope (deferred to follow-ups): - Blocking await on every save. The embed call is on the request hot path. For local Ollama / on-device providers this is fine; for network providers it adds 200ms-2s latency per remember/observe. Should be async / fire-and-forget with a write-behind queue — separate perf PR after this lands. - Periodic persistence flush of the vector index. Today the index is serialized at process exit (per IndexPersistence). Runtime adds between flushes survive in memory but die on crash. Acceptable for this PR; future PR can add a debounced flush. - Batching in rebuildIndex. Today the rebuild path embeds one doc at a time. embedBatch() exists on every provider and would be ~5-10x faster for cold restarts on large corpora. Defer. * fix(search): tighter migrate guard + rebuild vector clear + test hygiene Five reviewer findings applied. Five skipped with reason: APPLIED: 1. migrate-vector-index.ts filter now requires m.content (and rejects empty/whitespace). Without this, undefined m.content would concat to "title undefined" and poison the embed input. Memory.content is declared string but #277-era data shapes had cases where it could be missing — defensive filter. 2. migrate-vector-index.ts success expression now derives from the failed counter (success: failed === 0). Previously hardcoded true so callers couldn't rely on the boolean alone. 3. search.ts rebuildIndex now calls vectorIndex?.clear() before the repopulation loops. Symmetric to the BM25 idx.clear() that was already there. Without this, memories deleted between runs leave orphan embeddings in the vector store after every rebuild. 4. test/vector-index-dimensions.test.ts assertion tightened from toBeGreaterThan(0) to toBe(1) for both totalProcessed and vectorSize. The seeded fixture has exactly one observation; loose assertion would hide regressions where the migration produced extra/duplicate vectors. 5. test/vector-index-populate.test.ts switched to beforeEach/afterEach for the setVectorIndex / setEmbeddingProvider lifecycle. Manual cleanup in each test body was easy to drift on additions. PLUS: 6. compress.ts BM25 add now wrapped in try/catch matching the remember.ts pattern for parity. Helps cross-file symmetry even though SearchIndex.add doesn't throw on valid input. SKIPPED (with reason): - Wrapping vectorIndexAddGuarded calls in additional try/catch at remember.ts / observe.ts / compress.ts: the helper has internal try/catch (src/functions/search.ts:67-89). It NEVER throws. Adding outer try/catch is dead code. The reviewer missed the helper's contract. - vector-index-dimensions.test.ts mockKV.delete -> real deletion: the tests in that file don't exercise deletion semantics, and migrateVectorIndex itself never calls kv.delete. Touching it would add noise without effect on any assertion. - recordAudit + Promise.allSettled in remember.ts: out of scope. recordAudit is already called earlier in the function. Parallelizing cascade-update with vector-add changes failure semantics (cascade- update is currently fire-and-forget) — separate perf PR if wanted. - validateDimensions(N) assertion in vector-index-dimensions.test.ts: vectorSize is the COUNT of stored vectors, not their dimensions. Adding a validateDimensions check on the result would require exposing the new index. Out of scope for assertion tightening. 886 / 886 tests pass. Build clean. * fix(migrate): per-session isolation + dim guard + drop unused oldIndex param Four reviewer findings applied. One skipped with reason: APPLIED: 1. Per-session isolation in migrateVectorIndex observations phase. The previous shape wrapped the entire sessions for-loop in one try/catch — a single bad session (kv.list throws, embedBatch rejects, etc.) aborted every subsequent session and silently truncated the migration. The loop now has per-session try/catch; failures increment `failed`, append the session id to a new `failedSessions` array on the result, and the loop continues. The kv.list<sessions> call itself is now guarded separately — if THAT fails the whole migration aborts (no sessions to iterate), but with `failed` recorded and `failedSessions` empty so the caller can distinguish. 2. Drop `oldIndex` parameter from migrateVectorIndex signature. It was dead — no read path inside the function. Three test call sites updated to match the new signature migrateVectorIndex(kv, newProvider). Confused the API contract; if we ever DO need a previous-index argument it can come back with a real use case. 3. Dim guard on every embedding result inside migrateVectorIndex, in both the memories phase and the per-session observations phase. Extracted into a local isValidEmbedding() helper that mirrors the shape of search.ts::vectorIndexAddGuarded's guard: log structured warn + skip on mismatch. Without this, a misconfigured provider returning the wrong-length Float32Array would corrupt the rebuilt index, defeating the #248 persistence-load guard. 4. Rename `vi` -> `vectorIndex` in test/vector-index-populate.test.ts so the let binding doesn't shadow the vitest `vi` import. Practical impact: future tests in that file can use `vi.fn()` / `vi.spyOn()` without renaming this binding first. Cosmetic but cheap. SKIPPED (with reason): - Use embedBatch in rebuildIndex (search.ts): perf optimization, not correctness. Already called out as deferred in the previous review's commit message. Cold-restart speed matters but the fix here is bigger refactor (collect-into-batches with metadata, validate each, add only successful) and worth its own PR. Will pick up when we also do the write-behind queue for live-write batching. Result envelope: Added `failedSessions: string[]` to MigrateVectorIndexResult so the caller can decide whether to retry per-session or just abort. Existing fields (success, totalProcessed, failed, vectorSize) unchanged in shape. 886 / 886 tests pass. Build clean. * fix(migrate): attribute batch-level failures to correct count + distinguish list-fail Two new reviewer findings on migrate-vector-index. Six earlier findings re-flagged in the same review block had already been addressed in prior commits on this branch (verified inline): - compress.ts BM25 try/catch (commit d2098a5) - migrate filter requires m.content (commit d2098a5) - migrate success: failed === 0 (commit d2098a5) - rebuildIndex vector clear (commit d2098a5) - observe.ts / remember.ts outer try/catch — SKIPPED with reason (vectorIndexAddGuarded has internal try/catch, NEVER throws) - mockKV.delete no-op — SKIPPED with reason (no test exercises it) NEW FINDINGS APPLIED: 1. Catch block in memories phase counted +1 for the entire batch regardless of batch size. If kv.list returned 250 memories and embedBatch threw, `failed` reflected 1 missed embedding when reality is 250. textMems is now declared outside the try so the catch can read its length. If kv.list itself threw before textMems was populated (length 0), we fall back to +1 (something failed but the batch size is unknown). If embedBatch threw, `failed += textMems.length` so the counter is honest. 2. kv.list<sessions> failure path returned `failed: 1, failedSessions: []`. Indistinguishable from "0 sessions, all OK" from the caller's perspective — they can't tell whether the enumeration itself blew up or simply found no sessions. The path now pushes a marker string "<sessions-list-failed>" into failedSessions before returning. Marker keeps the existing schema (failedSessions: string[]) — no boolean flag added — and gives callers a sentinel they can grep for. Per-session entries continue to use real session ids, so the marker is unambiguous. 886 / 886 tests pass. Build clean. * test: swap Cyrillic non-ASCII tokenizer fixtures for Greek The two regression tests added in this branch for the BM25 non-ASCII tokenizer fix used Russian-language fixtures ("Проверка памяти", "Тестируем поиск по кириллице", etc). Switching to Greek-language equivalents ("Προβολή μνήμης", "Δοκιμάζουμε αναζήτηση σε ελληνικά", etc). Reasoning: the test fixtures need to exercise the same code path — non-ASCII Unicode letters that the old ASCII-only \w regex stripped. Greek code points (U+0370..U+03FF) hit \p{L} identically to Cyrillic (U+0400..U+04FF), so the regression coverage is preserved. Greek is also less likely to be misread as politically loaded content in a public repo. Test ids renamed from obs_cyrillic to obs_greek; test names from "indexes and finds Cyrillic text" to "indexes and finds non-ASCII (Greek) text" (and the mixed-script variant likewise). Branch name stays as-is to keep PR continuity but the shipped test fixtures are language-neutral going forward. 11/11 search-index tests pass with the swap. Full suite 886/886. --------- Signed-off-by: Nikita Nosov <20nik.nosov21@gmail.com> Co-authored-by: Nikita Nosov <20nik.nosov21@gmail.com>
* chore(providers): bump Gemini defaults to current GA models Bundles two upstream PRs into one chore — both are blocking real users today and both are simple default-string bumps with no API contract change. LLM default (was PR #368, @yut304) - `gemini-2.0-flash` is deprecated in Google's Gemini API and returns 429 rate-limit errors under load. Replace the default with `gemini-flash-latest`. Users on a pinned `GEMINI_MODEL` in `~/.agentmemory/.env` are unaffected. Embedding default (was PR #246, @AmmarSaleh50) - `text-embedding-004` is deprecated (shutdown Jan 14 2026). Replace with `gemini-embedding-001` (GA): 100+ languages, MRL dims (768 / 1536 / 3072), 2048-token input. - URL path changes from `:batchEmbedContent` to `:batchEmbedContents` (plural — the new model's batch endpoint). - Each request now sends `outputDimensionality: 768` so the returned vectors match the existing index dim guard from #248 — no reindex needed. - L2-normalize each returned vector before pushing to the result array. `gemini-embedding-001` does not normalize by default, unlike `text-embedding-004`. Without this the cosine-similarity math elsewhere in the search pipeline (which assumes unit-length vectors) collapses. Verified - `npm test` clean: 903 / 903. - `npm run build` clean. Closes #368, closes #246. * fix(gemini): pin LLM default to gemini-2.5-flash + warn-once on zero-norm Addresses CodeRabbit findings on PR #370. 1. Pin Gemini LLM default to gemini-2.5-flash. `gemini-flash-latest` is a moving alias that points to whatever Google promotes next. Production behaviour should be deterministic from a release perspective — users who upgrade agentmemory should not also get a Gemini model rotation in the same step. Switch the default to the current stable GA model `gemini-2.5-flash`. Users who want the moving alias keep getting it via `GEMINI_MODEL=gemini-flash-latest` in `~/.agentmemory/.env`. 2. Warn-once on zero-norm embedding in l2Normalize. `gemini-embedding-001` can return a zero-norm vector for degenerate input. The previous code silently returned the zero vector — downstream cosine-similarity math then divides by zero and the call site sees `NaN` scores with no signal as to why. Emit a one-time stderr warning naming the model + vector length so operators can correlate index quality dips with upstream embedding regressions. Behaviour otherwise unchanged: return the zero vector and let BM25 carry the search signal. Throwing was the other option — rejected because a single bad embedding in a 100-item batch would abort the whole batch and surface as an indexing pipeline halt. Soft-fail + warn matches the rest of the embedding provider error handling. Skipped finding: - `outputDimensionality` → `output_dimensionality` snake_case rename. CodeRabbit asserts the REST API expects snake_case. The Gemini REST API actually uses camelCase on the wire — confirmed against ai.google.dev/api/embeddings (field labelled `outputDimensionality` in the REST schema; the Python SDK alone uses snake_case and translates internally). Current code is correct as-shipped; the snake_case rename would silently break the dim override. Verified: 903 / 903 tests pass; build clean.
Closes #247.
What
Adds a dimension-check wrapper at the
EmbeddingProviderboundary insrc/providers/embedding/index.ts.createEmbeddingProvider()andcreateImageEmbeddingProvider()now wrap every provider so that any returnedFloat32Arraywhose length differs fromprovider.dimensionsthrows a descriptive error.Why
Embedding providers in
src/providers/embedding/(gemini, openai, voyage, cohere, openrouter, local, clip) trust that the API returns vectors matching their declared dimensions. None of them validateresult.length === this.dimensions. When the assumption breaks, the failure is silent:src/state/vector-index.ts:10returns0fromcosineSimilarityon length mismatch instead of throwing.This came up during review of #246 (gemini deprecation), where CodeRabbit flagged the gap on a single provider. Per-provider guards would be 7× duplication and easy to miss for new providers, so the fix lives at the factory boundary instead.
Changes
src/providers/embedding/index.tswithDimensionGuard(provider)that returns a wrapper checkingembed,embedBatch, and (when present)embedImageresults.createEmbeddingProvider()andcreateImageEmbeddingProvider()apply the wrapper to every constructed provider.embed/embedBatch[i]/embedImage), expected dimension, and actual dimension.test/embedding-provider.test.tsembedthrows.embedBatchthrows with the index.embedImageis guarded when present and absent when not.How to verify
npm install npm test -- test/embedding-provider.test.tsThe four new tests under
describe(\"withDimensionGuard\")should pass alongside the existing suite.Notes
new GeminiEmbeddingProvider(...), which is unwrapped, while only the factory entry points apply the guard.Summary by CodeRabbit
New Features
Tests