Skip to content

fix: preserve cross-tenant search id collisions#364

Open
compoodment wants to merge 1 commit into
mainfrom
vale/fix-cross-tenant-search-dedupe
Open

fix: preserve cross-tenant search id collisions#364
compoodment wants to merge 1 commit into
mainfrom
vale/fix-cross-tenant-search-dedupe

Conversation

@compoodment

@compoodment compoodment commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Preserve cross-tenant searchTextCollections hits when different readable tenants return the same record id by deduping on tenant + id.
  • Restore the primary tenant key with finally after fan-out.
  • Refresh stale slot, manifest, and host-flow expectations for current registered tools and assemble normalization so repository gates reflect current behavior.

Quality

Quality: Q5/5
Name: cross-tenant search drops same-id hits from later readable tenants
Evidence: focused regression failed before the fix, preserving only tenant-a when tenant-a and tenant-b both returned shared-id.
Repro: the focused libravdb-client unit test failed before the patch and passes after it.
Impact: agents with cross-tenant read access can silently miss relevant memories from other tenants when daemon-local record ids collide.
Fix confidence: high; dedupe scope now matches the tenant isolation boundary.
Risk: low; single-tenant path is unchanged, and multi-tenant fan-out still dedupes duplicate ids within the same tenant.

Testing

  • pnpm run clean:test && pnpm exec tsc -p tsconfig.tests.json && node --test .ts-build/test/unit/libravdb-client.test.js
  • pnpm run test:ts
  • pnpm run test:integration
  • pnpm check

– Vale

What changed

  • Fixed searchTextCollections fan-out deduping to preserve same-record hits across tenants by keying on tenant + id instead of id alone.
  • Ensured the primary tenant key is restored in a finally block after multi-tenant fan-out.
  • Updated integration/unit expectations to match current registered tool behavior and revised host-flow/assemble normalization output.

Tests updated

  • Checklist validation expectations expanded for additional user/card, rule, and persona tool contracts.
  • Host-flow expectations updated for recalled memory formatting and compaction behavior.
  • Slot conflict expectations centralized around runtime tool and active hook name constants.
  • Added a unit test covering cross-tenant search results and tenant-key restoration.

Complexity review

  • No Big-O or cyclomatic complexity regressions were introduced.

@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Fixes cross-tenant deduplication in searchTextCollections by keying dedupe on tenant+id instead of id alone, and wraps tenant-key swapping in try/finally for reliable restoration. Updates corresponding unit test and adjusts assertions in slot-conflict, host-flow, and checklist-validation tests.

Changes

Cross-tenant search fan-out fix

Layer / File(s) Summary
Multi-tenant search dedup and tenant-key restore
src/libravdb-client.ts, test/unit/libravdb-client.test.ts
searchTextCollections now dedupes aggregated results by tenant+id and restores tenantKey in a try/finally; new unit test verifies per-tenant hits and unchanged tenant key.

Test expectation updates

Layer / File(s) Summary
Slot-conflict test constants and assertions
test/unit/slot-conflict.test.ts
Adds RUNTIME_TOOL_NAMES and ACTIVE_HOOK_NAMES constants and reuses them in registration success assertions.
Host-flow assemble assertion updates
test/integration/host-flow.test.ts
Updates expected systemPromptAddition format, force-compaction truncation checks, and tightens warning-count assertion.
Checklist validation entries
test/integration/checklist-validation.test.ts
Expands expected checklist identifiers with user card, rule, and persona operations.

Estimated code review effort: 2 (Simple) | ~12 minutes

Possibly related PRs

Suggested labels: release:patch

Poem

A rabbit hopped through tenant lanes,
Deduping ids amid the chains,
With try and finally, keys restored,
No more mixed-up search records stored,
Tests now hop in perfect sync — 🐇✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main fix: preserving results when different tenants return the same search ID.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

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.

@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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/libravdb-client.ts (1)

450-470: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Keep tenant context off shared client state. The runtime reuses a single LibravDBClient across sessions, and before_prompt_build mutates client.setTenantKey(...) on that same instance. In searchTextCollections, swapping this.tenantKey across await boundaries lets overlapping RPCs pick up the wrong tenant header, so reads/writes can be routed to the wrong tenant. Pass tenant context per request or serialize tenant swaps.

🤖 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/libravdb-client.ts` around lines 450 - 470, `searchTextCollections` is
mutating shared tenant state on `LibravDBClient`, which can leak the wrong
tenant across overlapping calls. Update the method so it does not swap
`this.tenantKey` around each `await`; instead pass the tenant context per
request in `client.searchTextCollections` or otherwise isolate each tenant call.
Keep the dedupe loop and `readTenants` iteration in place, but ensure tenant
selection is local to each RPC and not stored on the shared client instance.
🧹 Nitpick comments (2)
test/unit/libravdb-client.test.ts (1)

360-388: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Good coverage for the happy path; consider covering the error-path restoration.

The try/finally addition specifically guards against tenantKey leaking when a per-tenant call throws, but this test only exercises the success path. A stub that rejects for one tenant would directly validate the finally restoration this PR introduces.

Optional: add error-path test
+test("tenantKey is restored even if a tenant read throws", async () => {
+  const client = new LibravDBClient({ endpoint: "tcp:127.0.0.1:9" });
+  (client as any).client = {
+    searchTextCollections: async () => {
+      if ((client as any).tenantKey === "tenant-a") throw new Error("boom");
+      return { results: [] };
+    },
+  };
+  client.setTenantKey("primary");
+  client.setReadTenants(["tenant-a", "tenant-b"]);
+  await client.searchTextCollections({ collections: ["global"], text: "x", k: 5 });
+  assert.equal((client as any).tenantKey, "primary");
+});
🤖 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 `@test/unit/libravdb-client.test.ts` around lines 360 - 388, Add an error-path
test for LibravDBClient.searchTextCollections to verify the new try/finally
restoration logic. In the existing cross-tenant test area, stub the underlying
client.searchTextCollections to reject for one tenant (using the
tenantKey/tenant loop behavior) and assert that tenantKey is restored to its
original value even after the failure. Keep the focus on the
searchTextCollections method and the tenantKey state transition.
src/libravdb-client.ts (1)

466-466: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Per-tenant read failures are silently swallowed with no observability.

If a tenant's searchTextCollections call fails (auth error, network issue, etc.), it's dropped without any logging, making cross-tenant search failures invisible in production.

Optional: add lightweight logging on skipped tenant reads
-        } catch { /* skip failed tenant reads */ }
+        } catch (err) {
+          // best-effort fan-out — log but don't fail the aggregate search
+          console.warn(`searchTextCollections: skipped tenant ${t}`, err);
+        }
🤖 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/libravdb-client.ts` at line 466, The per-tenant read failure in the
`searchTextCollections` flow is being swallowed by the empty `catch` block, so
add lightweight logging there before skipping the tenant. Update the
retry/tenant iteration path in `src/libravdb-client.ts` to capture the thrown
error and emit a minimal warning with tenant/context details, while still
continuing to the next tenant. Keep the existing behavior of skipping failed
reads, but make the failure observable through the relevant search helper that
wraps `searchTextCollections`.
🤖 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.

Outside diff comments:
In `@src/libravdb-client.ts`:
- Around line 450-470: `searchTextCollections` is mutating shared tenant state
on `LibravDBClient`, which can leak the wrong tenant across overlapping calls.
Update the method so it does not swap `this.tenantKey` around each `await`;
instead pass the tenant context per request in `client.searchTextCollections` or
otherwise isolate each tenant call. Keep the dedupe loop and `readTenants`
iteration in place, but ensure tenant selection is local to each RPC and not
stored on the shared client instance.

---

Nitpick comments:
In `@src/libravdb-client.ts`:
- Line 466: The per-tenant read failure in the `searchTextCollections` flow is
being swallowed by the empty `catch` block, so add lightweight logging there
before skipping the tenant. Update the retry/tenant iteration path in
`src/libravdb-client.ts` to capture the thrown error and emit a minimal warning
with tenant/context details, while still continuing to the next tenant. Keep the
existing behavior of skipping failed reads, but make the failure observable
through the relevant search helper that wraps `searchTextCollections`.

In `@test/unit/libravdb-client.test.ts`:
- Around line 360-388: Add an error-path test for
LibravDBClient.searchTextCollections to verify the new try/finally restoration
logic. In the existing cross-tenant test area, stub the underlying
client.searchTextCollections to reject for one tenant (using the
tenantKey/tenant loop behavior) and assert that tenantKey is restored to its
original value even after the failure. Keep the focus on the
searchTextCollections method and the tenantKey state transition.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1a0549cb-81f4-4e39-afe4-ea6cf0659004

📥 Commits

Reviewing files that changed from the base of the PR and between 96467dd and 85cf576.

📒 Files selected for processing (5)
  • src/libravdb-client.ts
  • test/integration/checklist-validation.test.ts
  • test/integration/host-flow.test.ts
  • test/unit/libravdb-client.test.ts
  • test/unit/slot-conflict.test.ts

@compoodment

Copy link
Copy Markdown
Collaborator Author

Vale Review — PR #364

Quality: Q4/5 — sharp
Head: 85cf576

Findings:

  • src/libravdb-client.ts:456 — searchTextCollections() still swaps this.tenantKey on the shared client immediately before an awaited RPC, so two overlapping calls on the same LibravDBClient can interleave and send a request under the wrong tenant key. The finally restores the key after the loop, but it does not isolate tenant selection per request. major

Proof gaps: No local gate run; finding is from current head source and requires a concurrency regression test to pin down.

Verdict: request-changes — the dedupe fix is good, but tenant fan-out still routes through shared mutable state across await boundaries.

– 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.

1 participant