Skip to content

fix: surface saved memories in smart_search/recall (#265)#269

Merged
rohitg00 merged 2 commits intomainfrom
fix/265-memory-search-fallback
May 10, 2026
Merged

fix: surface saved memories in smart_search/recall (#265)#269
rohitg00 merged 2 commits intomainfrom
fix/265-memory-search-fallback

Conversation

@rohitg00
Copy link
Copy Markdown
Owner

@rohitg00 rohitg00 commented May 10, 2026

Summary

  • memory_save succeeded but memory_smart_search / memory_recall always returned [] — the save→search flow was effectively write-only.
  • Two enrichment sites silently dropped memory hits because they queried KV.observations(sessionId, obsId), while mem::remember writes to KV.memories under a synthetic sessionId.
  • Both sites now fall back to KV.memories and coerce the Memory into a CompressedObservation.

Affected paths

  • HybridSearch.enrichResults → powers /smart-search, MCP memory_smart_search
  • mem::search obsResults map → powers /search, MCP memory_recall

The first site was the one called out in the issue. The second was found while validating end-to-end and has the same bug — without it memory_recall would still be broken even after the smart-search fix.

Live e2e (iii-engine 0.11.2 + agentmemory 0.9.5, EMBEDDING_PROVIDER=local)

Pre-fix:

POST /agentmemory/remember           → mem_xxx (saved, visible in /memories)
POST /agentmemory/smart-search       → { results: [] }
POST /agentmemory/search             → memory not present

Post-fix:

POST /agentmemory/smart-search       → results[0].obsId === mem_xxx (score 0.016)
POST /agentmemory/search             → results[0].obsId === mem_xxx (score 4.43)

Tests

  • test/hybrid-search.test.ts — new regression case: BM25 indexes a memory, enrichResults falls back to KV.memories.
  • test/search.test.ts — new regression case: mem::search surfaces memories saved to KV.memories.
  • Full suite: 856 passing.

Closes #265.

Summary by CodeRabbit

  • Bug Fixes

    • Search now falls back to retrieve saved memories when direct observation lookups fail, surfacing results that previously went missing.
  • New Features

    • Indexing and memory-saving flows now ensure saved memories are converted and indexed so they appear in search.
  • Tests

    • Added tests to validate the fallback and indexing behavior for saved memories in search results.

Review Change Stack

mem::remember persists memories to KV.memories under a synthetic
sessionId ("memory" or memory.sessionIds[0]). The BM25 index sees that
synthetic sessionId, but KV.observations under it never has an entry —
so both enrichment paths silently dropped every memory hit:

- HybridSearch.enrichResults (powers /smart-search, memory_smart_search)
- mem::search obsResults map (powers /search, memory_recall)

Result: memory_save succeeds and the memory shows up in the viewer and
GET /memories, but every search/recall returns []. The save→search flow
was effectively write-only.

Both sites now try KV.observations first, then fall back to KV.memories
and coerce the Memory record into a CompressedObservation. Verified live
end-to-end against an iii-engine 0.11.2 stack: prior to the fix
/smart-search and /search both returned [] for a saved memory; after the
fix the memory surfaces with score > 0 in both responses.

Regression tests added to test/hybrid-search.test.ts and test/search.test.ts.
@vercel
Copy link
Copy Markdown

vercel Bot commented May 10, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
agentmemory Ready Ready Preview, Comment May 10, 2026 6:14pm

Request Review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 10, 2026

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e3bd8e55-5fe3-437e-9525-22ac04a55ca7

📥 Commits

Reviewing files that changed from the base of the PR and between a496705 and 044245a.

📒 Files selected for processing (4)
  • src/functions/remember.ts
  • src/functions/search.ts
  • src/state/hybrid-search.ts
  • src/state/memory-utils.ts

📝 Walkthrough

Walkthrough

This PR adds memoryToObservation and updates enrichment and indexing paths so searches fall back from KV.observations to KV.memories (converted to observation shape), plus tests and test setup changes to validate the fallback.

Changes

Memory Search Fallback

Layer / File(s) Summary
Type & Imports
src/state/hybrid-search.ts, src/functions/search.ts, src/functions/remember.ts
Imported Memory type and memoryToObservation where enrichment or conversion is required.
Conversion Helper
src/state/memory-utils.ts
Added exported memoryToObservation(memory: Memory): CompressedObservation mapping Memory → CompressedObservation (sessionId from first sessionId or "memory", type "decision", timestamp from createdAt, narrative/facts from content, importance from strength).
Indexing (remember & rebuildIndex)
src/functions/remember.ts, src/functions/search.ts
Replaced file-local adapter with shared memoryToObservation when indexing memories in remember and rebuildIndex paths.
HybridSearch Enrichment
src/state/hybrid-search.ts
enrichResults now attempts KV.observations(sessionId, obsId) and falls back to KV.memories(obsId) with memoryToObservation conversion if needed; missing entries are filtered out.
mem::search Enrichment
src/functions/search.ts
Second-pass enrichment changed to per-candidate parallel lookups with error handling: try observations, fallback to memories + conversion, omit null loads.
Tests / Test Setup
test/search.test.ts, test/hybrid-search.test.ts
Added tests asserting saved memories are returned via fallback; added getSearchIndex().clear() in beforeEach; updated imports to include getSearchIndex and rebuildIndex.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant HybridSearch
  participant KV_Observations
  participant KV_Memories
  Client->>HybridSearch: search(query)
  activate HybridSearch
  HybridSearch->>KV_Observations: lookup observation (sessionId, obsId)
  KV_Observations-->>HybridSearch: not found / null
  HybridSearch->>KV_Memories: lookup memory (obsId)
  KV_Memories-->>HybridSearch: memory record / null
  alt memory found
    HybridSearch->>HybridSearch: memoryToObservation convert
    HybridSearch-->>Client: enriched result
  else not found
    HybridSearch-->>Client: no enriched result (filtered)
  end
  deactivate HybridSearch
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

"🐰 I hopped through KV in search of a clue,
Fell back to mem_store and stitched content anew.
From saved Memory I stitched an observation's face,
Now search finds the tale in its rightful place.
Hooray — memories return from the quieted space!"

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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 'fix: surface saved memories in smart_search/recall (#265)' clearly summarizes the main change—adding fallback logic to surface saved memories in search results.
Linked Issues check ✅ Passed The PR fully addresses the objectives from issue #265: it implements fallback logic in enrichResults() and mem::search to load from KV.memories when observations are absent, coerces Memory records into CompressedObservation shape, and includes regression tests validating the fix.
Out of Scope Changes check ✅ Passed All changes are directly scoped to resolving issue #265: modifications to enrichResults() and mem::search fallback logic, helper conversion functions, and regression tests. No unrelated refactoring or feature additions detected.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/265-memory-search-fallback

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.

❤️ Share

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

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (4)
src/functions/search.ts (3)

9-14: ⚡ Quick win

Remove comment explaining WHAT the code does.

The comment explains what the helper does rather than why. The function name memoryAsIndexable and the return type CompressedObservation already make the purpose clear. As per coding guidelines, avoid code comments explaining WHAT — use clear naming instead.

♻️ Proposed fix
-// Memories share the same searchable fields as observations (title +
-// content + concepts + files), so we wrap them in the observation shape
-// before indexing. Type is normalized to "decision" to keep memories
-// distinguishable in result metadata. Mirrors the helper in
-// functions/remember.ts; kept inline here to avoid a circular import
-// (remember.ts imports from this file).
 function memoryAsIndexable(memory: Memory): CompressedObservation {
🤖 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/functions/search.ts` around lines 9 - 14, Remove the explanatory block
comment that describes what the helper does; instead rely on the clear function
name memoryAsIndexable and its return type CompressedObservation. Locate the
comment immediately above the memoryAsIndexable helper and delete those comment
lines so only the function and its signature remain (do not change
memoryAsIndexable implementation or the CompressedObservation type).

167-170: ⚡ Quick win

Remove comment explaining WHAT the code does.

The comment explains what the fallback logic does rather than documenting why it's needed. As per coding guidelines, avoid code comments explaining WHAT — use clear naming instead.

♻️ Proposed fix
-      // Second pass: load observations in parallel. Fall back to
-      // KV.memories when the observation lookup misses — entries indexed
-      // via mem::remember live in the memories scope under a synthetic
-      // sessionId, so the observation key never exists (`#265`).
       const obsResults = await Promise.all(
🤖 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/functions/search.ts` around lines 167 - 170, Remove the explanatory
"Second pass: load observations..." comment in src/functions/search.ts and
delete any similar comments that describe WHAT the code does; instead, if
rationale is needed, add a brief WHY note referencing the underlying bug (e.g.,
"#265") above the fallback logic that mentions only the reason (observations may
be indexed under a synthetic sessionId) and keep variable/function names
(KV.memories, mem::remember, sessionId, observation lookup) clear and
self-descriptive so the code itself documents the behavior.

43-46: ⚡ Quick win

Remove comment explaining WHAT the code does.

The comment explains what the separate walk does rather than why it's necessary. As per coding guidelines, avoid code comments explaining WHAT — use clear naming instead.

♻️ Proposed fix
-  // Memories live in their own KV scope outside per-session observation
-  // scopes, so they need a separate walk. Without this, mem::remember
-  // entries vanish from BM25 on every restart even after the live-write
-  // fix in remember.ts (`#257`).
   try {
🤖 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/functions/search.ts` around lines 43 - 46, Remove the existing
WHAT-focused comment that explains how the separate walk works and either delete
it or replace it with a brief WHY-focused note that explains the necessity: that
memories are stored in a separate KV scope and therefore require a distinct walk
to ensure mem::remember entries are preserved in the BM25 index across restarts
(referenced in remember.ts), keeping the code intent clear without describing
internal mechanics; update the comment near the separate-walk code in search.ts
to state that this is required to maintain persistent BM25 entries for the
memory KV scope, and ensure naming and function calls (e.g., mem::remember,
BM25) remain self-explanatory in the code itself.
src/state/hybrid-search.ts (1)

296-299: ⚡ Quick win

Remove comment explaining WHAT the code does.

The comment explains what the fallback logic does rather than why it's necessary. As per coding guidelines, avoid code comments explaining WHAT — use clear naming instead.

♻️ Proposed fix
         if (obs) return obs;
-        // Fallback: indexed entry may originate from mem::remember, which
-        // writes to KV.memories with a synthetic sessionId ("memory" or the
-        // memory's first associated session). Coerce the Memory record into
-        // a CompressedObservation so search/recall surface saved memories.
         const mem = await this.kv
🤖 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/hybrid-search.ts` around lines 296 - 299, Remove the explanatory
comment block that describes what the fallback does and instead make the code
self-documenting: delete the multi-line comment mentioning mem::remember,
KV.memories, synthetic sessionId, and the coercion into CompressedObservation;
if clarity is needed rename the local variable(s) and helper(s) involved in the
fallback (e.g., the variable that holds the coerced object and any helper
function used for coercion such as the CompressedObservation construction or a
helper like coerceToCompressedObservation) so the intent is clear from names
alone; ensure any remaining brief comment (if absolutely necessary) states only
WHY the fallback exists, not WHAT it does.
🤖 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/state/hybrid-search.ts`:
- Around line 325-338: The memoryToObservation implementation duplicates
memoryAsIndexable; extract a single helper (e.g., memoryToObservation) into a
new shared module (suggested name: memory-utils.ts) and have both
src/state/hybrid-search.ts and src/functions/search.ts import and call that
helper instead of their local implementations; specifically, move the existing
conversion logic (fields id, sessionId from sessionIds[0] ?? "memory",
timestamp, type:"decision", title, facts:[content], narrative:content, concepts,
files, importance) into the shared function and replace both local functions
memoryToObservation and memoryAsIndexable with imports from the new module.

---

Nitpick comments:
In `@src/functions/search.ts`:
- Around line 9-14: Remove the explanatory block comment that describes what the
helper does; instead rely on the clear function name memoryAsIndexable and its
return type CompressedObservation. Locate the comment immediately above the
memoryAsIndexable helper and delete those comment lines so only the function and
its signature remain (do not change memoryAsIndexable implementation or the
CompressedObservation type).
- Around line 167-170: Remove the explanatory "Second pass: load
observations..." comment in src/functions/search.ts and delete any similar
comments that describe WHAT the code does; instead, if rationale is needed, add
a brief WHY note referencing the underlying bug (e.g., "#265") above the
fallback logic that mentions only the reason (observations may be indexed under
a synthetic sessionId) and keep variable/function names (KV.memories,
mem::remember, sessionId, observation lookup) clear and self-descriptive so the
code itself documents the behavior.
- Around line 43-46: Remove the existing WHAT-focused comment that explains how
the separate walk works and either delete it or replace it with a brief
WHY-focused note that explains the necessity: that memories are stored in a
separate KV scope and therefore require a distinct walk to ensure mem::remember
entries are preserved in the BM25 index across restarts (referenced in
remember.ts), keeping the code intent clear without describing internal
mechanics; update the comment near the separate-walk code in search.ts to state
that this is required to maintain persistent BM25 entries for the memory KV
scope, and ensure naming and function calls (e.g., mem::remember, BM25) remain
self-explanatory in the code itself.

In `@src/state/hybrid-search.ts`:
- Around line 296-299: Remove the explanatory comment block that describes what
the fallback does and instead make the code self-documenting: delete the
multi-line comment mentioning mem::remember, KV.memories, synthetic sessionId,
and the coercion into CompressedObservation; if clarity is needed rename the
local variable(s) and helper(s) involved in the fallback (e.g., the variable
that holds the coerced object and any helper function used for coercion such as
the CompressedObservation construction or a helper like
coerceToCompressedObservation) so the intent is clear from names alone; ensure
any remaining brief comment (if absolutely necessary) states only WHY the
fallback exists, not WHAT it does.
🪄 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: 54f01ecc-23f3-4ac8-86f8-69b2791cceab

📥 Commits

Reviewing files that changed from the base of the PR and between 0322da8 and a496705.

📒 Files selected for processing (4)
  • src/functions/search.ts
  • src/state/hybrid-search.ts
  • test/hybrid-search.test.ts
  • test/search.test.ts

Comment thread src/state/hybrid-search.ts Outdated
Three copies of the Memory→CompressedObservation conversion existed:
hybrid-search.ts (added in #265 fix), search.ts, remember.ts. Consolidate
into src/state/memory-utils.ts and import from all three callsites.

Behavior unchanged. Full suite: 854 passing.
@rohitg00 rohitg00 merged commit 5fc73f2 into main May 10, 2026
4 of 5 checks passed
@rohitg00 rohitg00 deleted the fix/265-memory-search-fallback branch May 10, 2026 18:14
@rohitg00 rohitg00 mentioned this pull request May 10, 2026
rohitg00 added a commit that referenced this pull request May 10, 2026
Three reliability fixes from #269/#270/#271:

- search/recall surfaces saved memories (closes #265)
- MCP shim proxies full server tool set (closes #234)
- session/subagent hooks no longer block startup (closes #221)

Also fixes packages/mcp version drift — was stuck at 0.9.4 through v0.9.5,
now lockstepped with main.
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.

Bug: smart_search/recall return empty results for memories saved via memory_save (enrichResults queries wrong KV store)

1 participant