Skip to content

fix(recall): scope L1 search by session_key for agent/user isolation#134

Open
YOMXXX wants to merge 1 commit into
TencentCloud:mainfrom
YOMXXX:fix/111-session-isolation
Open

fix(recall): scope L1 search by session_key for agent/user isolation#134
YOMXXX wants to merge 1 commit into
TencentCloud:mainfrom
YOMXXX:fix/111-session-isolation

Conversation

@YOMXXX
Copy link
Copy Markdown
Contributor

@YOMXXX YOMXXX commented Jun 3, 2026

Summary

Fixes #111searchMemories was performing global L1 searches across every captured session_key, so in a multi-agent or multi-user setup the auto-recall hook pulled the wrong agent's / wrong user's memories into the prompt. The OP filed it as a serious cross-contamination bug with a clean root-cause analysis; this PR implements their proposed fix.

Root cause (reproduced from the issue)

handleBeforeRecall(userText, sessionKey)performAutoRecall({...sessionKey})performAutoRecallInner(params) already carried sessionKey, but the next call searchMemories(userText, …) dropped it. From that point on:

  • searchByKeyword ran vectorStore.searchL1Fts(ftsQuery, maxResults * 2) with no scoping.
  • searchByEmbedding ran vectorStore.searchL1Vector(queryEmbedding, maxResults * 2) with no scoping.
  • searchHybrid did the same in parallel and RRF-merged.
  • The tool entry executeMemorySearch had no sessionKey parameter at all, while the sibling executeConversationSearch already had one.

l1_fts.session_key is UNINDEXED so server-side WHERE is not an option — client-side filter on the raw L1SearchResult / L1FtsResult (both already carry session_key) is the only safe path.

Change

  • src/core/hooks/auto-recall.ts — thread sessionKey from performAutoRecallInner into searchMemories and the four strategy branches (searchByKeyword, searchByEmbedding, searchHybrid, native-hybrid). Add applySessionFilter helper that drops raw FTS / vector results whose session_key !== sessionKey. Each strategy now over-retrieves (×4 single, ×5 hybrid RRF) when scoped so the post-filter pool can still fill maxResults.
  • src/core/tools/memory-search.ts — add sessionKey? to executeMemorySearch params; add session_key to MemorySearchResultItem so RRF-merged items carry the key. Apply the session filter alongside the existing type / scene filters; over-retrieve when scoped.
  • src/core/types.ts — add sessionKey?: string to MemorySearchParams.
  • src/core/tdai-core.ts — forward params.sessionKey into executeMemorySearch.
  • index.ts — expose session_key as an optional parameter on tdai_memory_search, mirroring tdai_conversation_search.

Semantics

  • Auto-recall hook: strictly scoped to the active sessionKey. The hook is already short-circuited upstream when sessionKey is empty (see resolveSessionKey in index.ts), so non-empty + strict-match is the right default.
  • tdai_memory_search tool: session_key is optional. Agents that want cross-session recall ("summarize everything you know about the user") can still omit it; agents that want isolation pass it through. Same shape as tdai_conversation_search to keep the tool surface consistent.

What's intentionally NOT in this PR

  • No SQL schema change (no migration). l1_fts.session_key stays UNINDEXED; client-side filter is fine for the recall budget (maxResults is single digits) and the over-retrieve multiplier compensates for the post-filter drop. If profiling later shows the multiplier is hurting BM25 quality, we can add an indexed mirror or a composite FTS index in a follow-up.
  • No backfill / migration for existing records — their session_key is already correctly stamped at write time.
  • No automated isolation test — the repo currently ships no tracked .test.ts files (verified via git ls-files), so a one-off test for this PR wouldn't run anywhere. Verification is by hand below.

Test plan

  • npm run build passes locally (tsdown clean).
  • Two sessions on the same backend (e.g. main agent and coder agent under the same user, or two users under the same host):
    • capture distinct L1 memories from each session
    • trigger auto-recall from session A — confirm only A's records appear in <relevant-memories> (and the [session-filter:*] debug log shows the drop count)
    • same for session B
  • tdai_memory_search called without session_key still returns the global pool (no regression for the cross-session use case).
  • tdai_memory_search called with session_key=<current> filters correctly.
  • Native-hybrid path (TCVDB backend with nativeHybridSearch=true) still works — verified that we now over-retrieve and post-filter rather than re-issue the request.

`searchMemories` previously discarded the caller's `sessionKey` before
hitting the L1 store, so:

- auto-recall (`before_prompt_build` → `performAutoRecall`) merged every
  session's L1 records into one global pool. In a multi-agent setup
  (main, coder, omoc_coder, …) recall for one agent injected the other
  agents' memories; with multiple users on the same backend the same
  cross-contamination happens across users.
- the `tdai_memory_search` tool had no way to scope its results, while
  the sibling `tdai_conversation_search` already accepted `session_key`.

Wire `sessionKey` all the way through:

- src/core/hooks/auto-recall.ts: thread `sessionKey` from
  `performAutoRecallInner` into `searchMemories` and the four
  strategy branches (`searchByKeyword`, `searchByEmbedding`,
  `searchHybrid`, native-hybrid). Add an `applySessionFilter` helper
  that drops raw FTS / vector results whose `session_key` does not
  match. Each strategy now over-retrieves (×4 for single-strategy,
  ×5 for hybrid RRF) when filtering so the post-filter pool can still
  fill `maxResults`. `l1_fts.session_key` is UNINDEXED so server-side
  WHERE is not possible — client-side filter is the only safe path.
- src/core/tools/memory-search.ts: add `sessionKey?` to the
  `executeMemorySearch` params and a `session_key` field to
  `MemorySearchResultItem` so FTS / vector items carry the key
  through RRF merge. Apply the session filter alongside `type` and
  `scene` filters, and over-retrieve candidates when scoped.
- src/core/types.ts: add `sessionKey?: string` to `MemorySearchParams`.
- src/core/tdai-core.ts: forward `params.sessionKey` into
  `executeMemorySearch`.
- index.ts: expose `session_key` as an optional parameter on the
  `tdai_memory_search` tool, mirroring the existing
  `tdai_conversation_search` shape so agents can opt into scoping.

The auto-recall path is now strictly isolated by `sessionKey` — the
key was already non-empty by the time `handleBeforeRecall` ran
(empty keys are skipped earlier in index.ts), so this is the right
default for the recall hook. The tool keeps `session_key` optional
because an agent may legitimately want to recall across sessions on
demand.

Fixes TencentCloud#111

Signed-off-by: 李冠辰 <liguanchen@xiaomi.com>
@Maxwell-Code07
Copy link
Copy Markdown
Collaborator

Thanks for the contribution! We've received your PR fixing the cross-session memory pollution bug and will review it internally. We'll get back to you once we have results. Appreciate your help in improving multi-agent isolation!

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.

searchMemories has no agent/user-level isolation — multi-agent memory cross-contamination

2 participants