Skip to content

[AI-assisted] Fix memory_grep default session recall - #342

Closed
IWhatsskill wants to merge 3 commits into
xDarkicex:mainfrom
IWhatsskill:fix-memory-grep-session-collections-20260612
Closed

[AI-assisted] Fix memory_grep default session recall#342
IWhatsskill wants to merge 3 commits into
xDarkicex:mainfrom
IWhatsskill:fix-memory-grep-session-collections-20260612

Conversation

@IWhatsskill

@IWhatsskill IWhatsskill commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Problem: memory_grep searched session_raw:<id> for message scope but missed exact active-session hits stored in LibraVDB's default session:<id> recall collection.
  • Solution: search the default session:<id> collection as part of the message grep bucket while keeping summary and message searches independent.
  • What changed: memory_grep now queries session_raw:<id> and session:<id> separately for message results when a session ID is available, skips malformed empty-session message collection names, treats non-object grep metadata as missing metadata, filters exact/regex matches before deduplication, and keeps the highest-scored duplicate turn ID.
  • What did NOT change (scope boundary): no daemon, manifest, activation, lifecycle, dependency, lockfile, storage schema, auth, migration, memory_search, or memory_expand behavior changed.

Motivation

memory_grep is the exact-match fallback for active-session recall. Current main already uses the default session:<id> collection for normal session recall, but memory_grep(scope="messages") still searched only session_raw:<id>. That made exact grep unreliable for active-session text indexed in the default session collection.

Change Type

  • Bug fix
  • Feature
  • Refactor required for the fix
  • Docs
  • Security hardening
  • Chore/infra

Scope

  • Gateway / orchestration
  • Skills / tool execution
  • Auth / tokens
  • Memory / storage
  • Integrations
  • API / contracts
  • UI / DX
  • CI/CD / infra

Linked Issue/PR

Real behavior proof

  • Behavior or issue addressed: memory_grep(scope="messages") now finds exact active-session hits stored under the default session:<id> collection.
  • Real environment tested: Linux throwaway checkout of current main at 96b7694 plus this patch through head e00b029, using Node v24.16.0 and pnpm@9.15.9.
  • Exact steps or command run after this patch:
corepack pnpm@9 install --frozen-lockfile --store-dir throwaway-pnpm-store
git diff --check
corepack pnpm@9 exec tsc -p tsconfig.tests.json
node --test .ts-build/test/unit/memory-recall.test.js
node --test .ts-build/test/unit/memory-tools.test.js .ts-build/test/unit/memory-runtime.test.js .ts-build/test/unit/before-turn.test.js
corepack pnpm@9 exec tsc --noEmit
corepack pnpm@9 run build
  • Evidence after fix:
memory-recall.test.js: tests 12, pass 12, fail 0
memory-tools.test.js + memory-runtime.test.js + before-turn.test.js: tests 34, pass 34, fail 0
tsc -p tsconfig.tests.json: pass
tsc --noEmit: pass
pnpm run build: pass
git diff --check: pass
  • Observed result after fix: the targeted tests verify default-session-only message hits are returned, summary-only scope does not query session:<id>, empty session IDs do not query malformed message collections like session_raw:, syntactically valid non-object message metadata falls back to role: "unknown" without dropping the hit, scope="both" preserves independent summary/message budgets, non-matching duplicate IDs no longer suppress matching hits, and duplicate matching turn IDs keep the highest score.
  • What was not tested: production daemon data with a live packaged OpenClaw host session, external hosted providers, GitHub Actions completion, and maintainer-hosted production runtime.
  • Before evidence:
Current main at 96b7694:
memory_grep(scope="summaries") -> searchText(collection="session_summary:<id>")
memory_grep(scope="messages")  -> searchText(collection="session_raw:<id>")

Missing before this patch:
memory_grep(scope="messages") did not search collection="session:<id>"

Root Cause

  • Root cause: memory_grep hard-coded the experimental message collection session_raw:<id> but did not include the default session recall collection session:<id>.
  • Missing detection / guardrail: tests only asserted the summary active-session fallback, not the default message recall collection used by ordinary session recall, the empty-session message collection guard, or best-effort handling for non-object message metadata.
  • Contributing context (if known): an earlier implementation tried to solve this with one mixed multi-collection search stream, but review correctly identified top-k starvation, scope leakage, provenance assumptions, and dedup ordering risks.

Regression Test Plan

  • Coverage level that should have caught this:
    • Unit test
    • Seam / integration test
    • End-to-end test
    • Existing coverage already sufficient
  • Target test or file: test/unit/memory-recall.test.ts
  • Scenario the test should lock in: memory_grep(scope="messages") searches both session_raw:<id> and default session:<id> for non-empty sessions, returns a hit that exists only in the default session collection, does not query malformed message collections when no session ID is available, and keeps matching turns when metadataJson parses to a non-object such as JSON null.
  • Why this is the smallest reliable guardrail: the bug is in plugin-side collection selection and result filtering before daemon search results are returned, so fake-client unit tests can assert exact collection calls and result behavior.
  • Existing test that already covers this (if any): none before this patch.
  • If no new test is added, why not: New focused tests were added.

User-visible / Behavior Changes

memory_grep can now find exact active-session text stored in the default session recall collection. Existing summary search and raw-message search behavior remain available.

Diagram

Before:
memory_grep(scope="messages")
  -> session_raw:<id> only
  -> misses hits stored only in session:<id>

After:
memory_grep(scope="messages")
  -> session_raw:<id>
  -> session:<id>
  -> exact/regex filter
  -> dedupe matching turns by highest score

Empty session ID:
memory_grep(scope="messages")
  -> no message collection lookup

Security Impact

  • New permissions/capabilities? No.
  • Secrets/tokens handling changed? No.
  • New/changed network calls? No new endpoint; message grep may issue one additional SearchText request for the active session's default session:<id> collection.
  • Command/tool execution surface changed? No.
  • Data access scope changed? No new durable namespace; this aligns memory_grep with the same active-session collection already used by LibraVDB session recall.
  • If any Yes, explain risk + mitigation: The additional lookup is limited to a non-empty current active session ID and covered by unit tests that assert summary scope does not query session:<id> and empty message scope does not query session_raw:.

Repro + Verification

Environment

  • OS: Linux throwaway checkout
  • Runtime/container: Node v24.16.0, pnpm@9.15.9
  • Model/provider: Not applicable
  • Integration/channel (if any): LibraVDB recall tool path with fake daemon clients for deterministic collection routing
  • Relevant config (redacted): no credentials, tokens, private runtime config, or external provider account used

Steps

  1. Check out current main at 96b7694.
  2. Apply this patch.
  3. Install dependencies with pnpm@9 and a throwaway store.
  4. Compile test artifacts with tsc -p tsconfig.tests.json.
  5. Run targeted memory-recall tests.
  6. Run adjacent memory/runtime/before-turn unit tests.
  7. Run TypeScript no-emit, production build, and whitespace diff checks.

Expected

  • memory_grep(scope="summaries") searches only session_summary:<id>.
  • memory_grep(scope="messages") searches session_raw:<id> and session:<id>.
  • memory_grep(scope="messages") with no session ID does not query malformed message collections.
  • Non-object message metadata falls back to role: "unknown" without dropping matching turns.
  • memory_grep(scope="both") keeps independent summary and message result budgets.
  • Duplicate turn IDs are deduplicated after exact/regex filtering, keeping the highest score.
  • Targeted and adjacent tests, TypeScript, build, and whitespace checks pass.

Actual

  • Summary scope stayed isolated to session_summary:<id>.
  • Message scope returned the default-session-only hit.
  • Empty message scope made no malformed session_raw: lookup.
  • A matching turn with metadataJson set to JSON null returned with role: "unknown".
  • scope="both" returned one summary and one message with limit=1.
  • A non-matching duplicate no longer suppressed a later matching duplicate.
  • The highest-scored duplicate matching turn was retained.
  • Targeted and adjacent tests, TypeScript, build, and whitespace checks passed.

Evidence

  • Failing test/log before + passing after
  • Trace/log snippets
  • Screenshot/recording
  • Perf numbers (if relevant)
git diff --check: pass
corepack pnpm@9 exec tsc -p tsconfig.tests.json: pass

memory-recall.test.js:
tests 12
pass 12
fail 0

memory-tools.test.js + memory-runtime.test.js + before-turn.test.js:
tests 34
pass 34
fail 0

corepack pnpm@9 exec tsc --noEmit: pass
corepack pnpm@9 run build: pass

Human Verification

  • Verified scenarios: default-session-only message hit, empty-session message collection guard, non-object message metadata fallback, summary-only scope isolation, independent scope="both" budgets, dedup after exact matching, highest-score duplicate selection, role fallback from metadata, targeted recall tools, adjacent memory/runtime/before-turn units, TypeScript, production build, and whitespace diff check.
  • Edge cases checked: non-matching duplicate before matching duplicate, same turn ID across raw/default session collections, default active session ID fallback, missing active session ID for message scope, JSON null metadata, and no dependency/lockfile changes.
  • What you did not verify: live packaged OpenClaw host session, external hosted providers, GitHub Actions completion, full unit suite, host-flow integration, plugin inspector, and maintainer-hosted production daemon behavior.

Review Conversations

  • I replied to or resolved every bot review conversation I addressed in this PR.
  • I left unresolved only the conversations that still need reviewer or maintainer judgment.

CodeRabbit's PR #342 review thread about empty sessionId message collection names is addressed by commit ad33283 and was resolved by coderabbitai[bot] after re-review. No human GitHub review reply or manual thread resolve was posted. The code also addresses the prior #303 top-level review feedback about mixed top-k, summaries scope, dedup-before-filtering, duplicate selection, and collection provenance.

The Vale/compoodment request-changes review about non-object metadataJson is addressed by commit e00b029. No human GitHub review reply or manual thread resolve was posted; reviewer re-review is still needed.

Compatibility / Migration

  • Backward compatible? Yes.
  • Config/env changes? No.
  • Migration needed? No.
  • If yes, exact upgrade steps: No upgrade steps required.

Risks and Mitigations

  • Risk: searching one additional active-session collection could return duplicate turn IDs.
    • Mitigation: message results are exact/regex filtered first, then deduplicated by turn ID with highest score retained.
  • Risk: summary results could be starved by high-scoring turn results.
    • Mitigation: summary and message searches remain separate; session:<id> is never queried for scope="summaries".
  • Risk: daemon results may not include collection provenance in metadata.
    • Mitigation: message classification no longer depends on metadataJson.collection; the code knows which bucket it is searching.
  • Risk: missing session context could create malformed message collection lookups.
    • Mitigation: message grep returns no message collections for an empty session ID, covered by regression test.
  • Risk: syntactically valid non-object metadata such as JSON null could throw while reading role and drop matching message results.
    • Mitigation: grep metadata parsing now only returns non-null, non-array objects; other parsed values fall back to {} and role: "unknown", covered by regression test.

Summary

This PR fixes a correctness bug in memory_grep(scope="messages") where active-session message hits stored in the default session:<id> collection were missed because only the experimental session_raw:<id> collection was queried.

Changes

src/tools/memory-recall.ts

Algorithmic Changes:

  • Collection querying: Replaced single-collection search (session_raw:${sessionId}) with dual-collection queries via new helper buildMessageGrepCollections(sessionId), which returns ["session_raw:${sessionId}", "session:${sessionId}"] for non-empty session IDs or [] for empty ones (guards malformed collection names).
  • Deduplication: Introduced a Map<turnId, {turnId, snippet, role, score}> to deduplicate results per turnId, retaining the highest-scored duplicate when the same turn appears in both collections.
  • Filtering order: Exact/regex matching via safeMatch() is applied before deduplication, ensuring only exact matches contribute to the deduped set.
  • Helper functions:
    • parseGrepMetadata() - Safely decodes and validates metadata JSON; returns empty object for malformed/non-object metadata.
    • readGrepRole() - Extracts role field from parsed metadata with "unknown" fallback.
    • buildMessageGrepCollections() - O(1), returns 0 or 2 collection names.

Complexity Analysis:

  • Big O: Worst-case remains O(k·L + k log k) where k = searchK ≈ min(limit·3, 200) and L = average text snippet length. The sorting of deduped results (dedupedTurns.sort()) dominates. However, number of SearchText requests increases from 1 to 2 (constant factor of 2× for active session).

    • Per collection: O(k) iterations over results
    • Per result: O(L) for safeMatch(), O(L) for truncateSnippet(), O(M) for metadata parse (M = metadata size), O(1) for Map operations
    • Final sort: O(k log k) across all unique turnIds from both collections
    • Total: O(2 × (k·L + k·M)) + O(k log k) ≈ O(k·L + k log k) - asymptotically identical to single-collection baseline
  • Cyclomatic Complexity: No regression. Loop structure (for each of 2 collections → filter results → dedup → sort) adds ~1 conditional branch but maintains baseline complexity of ~6–7 (comparable to original ~5–6).

  • Space Complexity: O(k) for bestTurnById Map and O(k) for sorted results—no regression vs. single-collection approach.

Mitigations for 2× search overhead:

  • Early filtering: safeMatch() is applied per result before Map insertion, reducing actual objects stored from 400 (2×200) to only those matching the pattern.
  • Score-keeping: Only the highest-scored duplicate is retained, reducing final result cardinality.
  • Bounded limit: searchK capped at 200, making absolute cost predictable (max 400 results processed, typically < 50 after filtering).

test/unit/memory-recall.test.ts

Coverage Additions:

  • Guard test: Validates that empty session IDs do not trigger message collection queries (prevents malformed session_raw: and session: lookups).
  • Dual-collection test: Confirms queries over both session_raw:active-session and session:active-session, validating metadata decoding (role extraction with "unknown" fallback).
  • Non-object metadata test: Verifies that non-object metadata (e.g., null) is treated as missing and role defaults to "unknown".
  • Independent budgets test: Asserts that scope="both" maintains separate per-collection result limits for summaries and messages (e.g., limit:1 can yield 1 summary + 1 message).
  • Deduplication-after-filtering test: Confirms exact matching occurs before deduplication (high-score semantic neighbors without pattern match are excluded).
  • Highest-score-retention test: Validates that when the same turnId appears in multiple collections, the higher-scored variant is retained.

Test Infrastructure:

  • Introduced typed FakeSearchResult interface with optional metadataJson: Uint8Array.
  • Added CollectionRecallClient subclass of FakeRecallClient to return per-collection results based on the collection parameter.
  • encodeMetadata() helper for test fixture construction.

Known Risks & Limitations

  • Production daemon data: Not tested against real-world LibraVDB daemon collections; behavior with malformed or missing session: collections is untested in production.
  • External hosted providers: No validation against external/cloud-hosted LibraVDB instances.
  • Additional SearchText latency: One extra search per active-session grep query; impact on slow network/storage backends not measured.

Unaffected

  • Daemon, manifest, activation, lifecycle, dependencies, lockfiles, storage schema, auth, and migration logic.
  • memory_search, memory_expand, memory_describe tools and their prompt guidance.
  • Summary search logic (remains independent; session_summary:<id> queried only for scope="summaries" or scope="both").

@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f0ad816a-6740-4a13-a156-4fb2b567a9ff

📥 Commits

Reviewing files that changed from the base of the PR and between 412d0bf and e00b029.

📒 Files selected for processing (2)
  • src/tools/memory-recall.ts
  • test/unit/memory-recall.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/tools/memory-recall.ts

📝 Walkthrough

Walkthrough

The memory_grep message search now queries multiple per-session collections, parses turn metadata safely to derive roles, deduplicates results by turnId keeping the highest-scored candidate, and assembles sorted turns; tests and test helpers were added to validate collection selection, metadata decoding, budgeting, and deduplication behavior.

Changes

Message grep deduplication and collection-aware search

Layer / File(s) Summary
GrepSearchResult type and metadata utilities
src/tools/memory-recall.ts
Introduces an internal GrepSearchResult type and helper functions parseGrepMetadata, readGrepRole, and buildMessageGrepCollections for safe metadata parsing, role extraction with fallback, and per-session collection selection.
Memory grep message search with deduplication
src/tools/memory-recall.ts
The memory_grep "messages"/"both" branch queries multiple session collections, builds per-result candidates using parsed metadata (role), deduplicates by turnId using a Map to retain the highest-score candidate, sorts by score, and appends results into the output under existing budgets and counters.
Test infrastructure and memory grep coverage
test/unit/memory-recall.test.ts
Adds FakeSearchResult typing, encodeMetadata helper, and CollectionRecallClient test subclass. Extends tests to assert collection selection, metadata decoding (including non-object metadata -> unknown), independent summary vs message budgeting for scope: "both", exact-match deduplication, and highest-score retention across duplicate raw collections.

Sequence Diagram(s)

(Skipped — changes are focused and the flow is simple enough to follow from the summaries.)

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested labels

release:patch

Suggested reviewers

  • compoodment
  • fuller-stack-dev

Poem

🐰 In dusty logs I hop and comb,

I parse each byte to find a home,
I gather turns from many stacks,
I keep the best and skip repeats,
Tests applaud my tidy feats.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.50% 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 memory_grep default session recall' clearly identifies the main fix: addressing a bug where memory_grep was not searching the default session collection.
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.

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


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.

@IWhatsskill
IWhatsskill marked this pull request as ready for review June 12, 2026 14:08

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 `@src/tools/memory-recall.ts`:
- Around line 209-215: The function buildMessageGrepCollections always pushes
`session_raw:${sessionId}` even when sessionId is empty, causing malformed
collection names; update buildMessageGrepCollections to guard `session_raw` the
same as `session:${sessionId}` (or return/throw early when sessionId is empty)
so both collections are only added when sessionId.length > 0; locate and modify
buildMessageGrepCollections and ensure this aligns with how `memory_grep`
derives sessionId (readStr/getSessionId) to avoid querying `session_raw:`.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d3b3260f-1dd9-4d6b-9490-58b9bdddee14

📥 Commits

Reviewing files that changed from the base of the PR and between 96b7694 and 412d0bf.

📒 Files selected for processing (2)
  • src/tools/memory-recall.ts
  • test/unit/memory-recall.test.ts

Comment thread src/tools/memory-recall.ts

@compoodment compoodment left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Vale Review

Verdict: request changes. The rewrite fixes the big #303 problems: summary/message searches stay independent, dedup now happens after exact/regex filtering, duplicate turn IDs keep the higher score, and the code no longer depends on collection provenance from metadataJson. That part is solid.

Finding

  1. Major: non-object metadata can now make memory_grep drop all message results.

parseGrepMetadata() returns the parsed JSON value cast as Record<string, unknown> (src/tools/memory-recall.ts:193-201), but it does not verify that the value is actually a non-null object. readGrepRole() then immediately reads meta.role (src/tools/memory-recall.ts:204-206). If the daemon returns syntactically valid but non-object metadata such as JSON null, that property access throws.

That is a regression from the previous inline metadata parsing, where the try covered both JSON.parse(...) and the meta.role read, so malformed/non-object metadata merely fell back to role: "unknown".

I reproduced this against the PR branch with a fake matching turn:

metadataJson = TextEncoder().encode("null")
WARN memory_grep failed: Cannot read properties of null (reading 'role')
{"pattern":"needle","mode":"text","totalMatches":0,"summaries":[],"turns":[],"truncated":false}

That means one weird metadata payload can make memory_grep(scope="messages") return zero results even though the text matched. Since metadata parsing is explicitly best-effort everywhere else in this file, this helper should preserve that behavior.

Fix direction: make parseGrepMetadata() return {} unless the parsed value is a non-null, non-array object, or make readGrepRole() guard before reading role. Add a regression test with metadataJson set to JSON null and assert the matching turn is still returned with role: "unknown".

Verification

I ran:

corepack pnpm@9 exec tsc -p tsconfig.tests.json
node --test .ts-build/test/unit/memory-recall.test.js
git diff --check upstream/main..review/pr-342

Current added tests pass, but they do not cover this non-object metadata case.

@compoodment compoodment left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Re-reviewed latest head e00b029 after @vale review.

Verdict: approve. The previous blocker is fixed: parseGrepMetadata() now ignores non-null/non-array violations by returning {}, readGrepRole() falls back to unknown, and the regression test with JSON null metadata keeps the matching turn instead of dropping results.

I also checked the session collection behavior while I was here. Empty session IDs now avoid malformed message collection queries, memory_grep(scope="messages") checks both raw and default session collections when a session is present, exact filtering happens before dedup, and duplicate turn IDs keep the higher score.

Verification run:

corepack pnpm@9 exec tsc -p tsconfig.tests.json
node --test .ts-build/test/unit/memory-recall.test.js
git diff --check origin/main...HEAD

Result: 12/12 focused unit tests passed. CodeRabbit's latest check is also passing. I did not run host-backed daemon tests for this review.

– Vale

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.

2 participants