#16968: fix(qmd): per-collection search to prevent large collections drowning out smaller ones
stale
size: L
## Summary
- Problem: When using `search`/`vsearch` mode with multiple QMD collections, all collections are queried in a single `qmd search` call. Large collections (e.g. `custom-1` with 432 docs) dominate the top-N results, causing smaller collections like `memory-dir` (38 docs) to return **zero results** — even though the index has matching data.
- Why it matters: Users with both custom report collections and memory directories lose all memory-dir recall. The agent cannot retrieve any personal memory/knowledge-base content.
- What changed:
- Generalized `runQueryAcrossCollections` → `runSearchAcrossCollections` supporting `search`/`vsearch`/`query` commands
- All multi-collection searches now query each collection independently and merge results by best score per docid
- Added automatic fallback: if per-collection `search` reports unsupported flags, falls back to per-collection `query`
- Self-healing retry for missing collections and `--no-expand` fallback (from earlier commits on this branch)
- `resolveDocLocation` scoped to managed collections only
- What did NOT change (scope boundary): Single-collection search paths remain unchanged. No config schema changes. No changes to qmd CLI invocation format.
## Change Type (select all)
- [x] Bug fix
## Scope (select all touched areas)
- [x] Memory / storage
## Linked Issue/PR
- Related #11727
## User-visible / Behavior Changes
Memory search now returns results from all configured collections proportionally, instead of being dominated by the largest collection. No config changes needed.
## Security Impact (required)
- New permissions/capabilities? `No`
- Secrets/tokens handling changed? `No`
- New/changed network calls? `No`
- Command/tool execution surface changed? `No`
- Data access scope changed? `No`
## Repro + Verification
### Environment
- OS: macOS (Darwin arm64)
- Runtime: Node.js
- QMD searchMode: `search` (default)
- Collections: `memory-root` (1 doc), `memory-alt` (1 doc), `memory-dir` (38 docs), `custom-1` (432 docs)
### Steps
1. Configure QMD with default settings (searchMode=search) and multiple collections of varying sizes
2. Run `memory_search("context explosion")`
3. Observe results
### Expected
Results from both `custom-1` and `memory-dir` collections appear
### Actual
Before fix: Only `custom-1` results returned; `memory-dir` results completely absent
After fix: Both collections contribute results, merged by best score per docid
## Evidence
- [x] Failing test/log before + passing after
Verified via direct qmd CLI:
```
# All collections together → only custom-1 results (memory-dir drowned out)
qmd query "context explosion" --json -n 5 -c memory-root -c memory-alt -c memory-dir -c custom-1
→ 5/5 results from custom-1, 0 from memory-dir
# memory-dir alone → results exist
qmd query "context explosion" --json -n 5 -c memory-dir
→ 5/5 results from memory-dir (score 0.9, 0.51, 0.38, 0.35, 0.32)
```
37/37 unit tests passing after changes.
## Human Verification (required)
- Verified scenarios: Confirmed via production QMD index (~/.openclaw/agents/main/qmd) that per-collection search returns memory-dir results that were previously invisible
- Edge cases checked: Single collection (no behavior change), unsupported flag fallback (search → query), missing collection recovery
- What you did **not** verify: vsearch mode (no vsearch-capable qmd binary available for testing)
## Compatibility / Migration
- Backward compatible? `Yes`
- Config/env changes? `No`
- Migration needed? `No`
## Failure Recovery (if this breaks)
- How to disable/revert this change quickly: Set `searchMode: "query"` in config (query mode already had per-collection logic)
- Files/config to restore: `src/memory/qmd-manager.ts`
- Known bad symptoms reviewers should watch for: Increased qmd process spawns (N collections × 1 search per query instead of 1 combined search)
## Risks and Mitigations
- Risk: More qmd subprocess invocations per search (one per collection instead of one total)
- Mitigation: Typical setups have 3-5 collections; each per-collection query is faster than a combined query over a large index. Net latency impact is minimal.
<!-- greptile_comment -->
<h3>Greptile Summary</h3>
This PR fixes a real problem where large QMD collections (e.g. 432 docs) dominated top-N results and prevented smaller collections (e.g. 38 docs) from contributing any results. The core change generalizes `runQueryAcrossCollections` → `runSearchAcrossCollections` to support `search`, `vsearch`, and `query` commands, querying each collection independently and merging results by best score per docid.
Additional improvements include:
- `--no-expand` optimization for query mode via `buildSearchArgs`, with a static capability flag and automatic fallback
- Self-healing recovery for missing collections during search (`recoverMissingCollection`)
- `resolveDocLocation` now scoped to managed collections only (prevents returning results from unmanaged collections)
- Non-blocking bootstrap: when `waitForBootSync` is false, `create()` no longer blocks on `ensureCollections()`
- Boot update no longer blocks search via `waitForPendingUpdateBeforeSearch` skipping when reason is "boot"
Issues found:
- `runSearchAcrossCollections` in `query` mode does not handle `--no-expand` rejection. When `buildSearchArgs("query", ...)` includes `--no-expand` and the qmd binary rejects it, the error is caught as an "unsupported option" and the loop breaks, but the fallback guard (`command !== "query"`) prevents retry. This silently returns partial/empty results, and `noExpandSupported` is never set to `false` since that logic only exists in the `runQuery` inner function. This affects the first multi-collection search on systems where qmd doesn't support `--no-expand`.
<h3>Confidence Score: 3/5</h3>
- The PR solves a genuine recall problem and is mostly well-implemented, but has one logic gap in --no-expand fallback handling for multi-collection query paths that should be addressed before merge.
- Score of 3 reflects solid overall implementation with good test coverage, but one confirmed logic bug: runSearchAcrossCollections in query mode silently returns partial/empty results when --no-expand is rejected, with no retry or flag update. This bug affects the common multi-collection query code path on systems with older qmd binaries.
- Pay close attention to `src/memory/qmd-manager.ts`, specifically the `runSearchAcrossCollections` method's error handling when `command === "query"` and `--no-expand` is unsupported.
<sub>Last reviewed commit: 7bd347b</sub>
<!-- greptile_other_comments_section -->
<!-- /greptile_comment -->
Most Similar PRs
#21868: fix: qmd search/vsearch silently return empty with multiple -c coll...
by jacksclaw · 2026-02-20
91.4%
#20966: fix(memory/qmd): migrate orphaned unscoped collections on upgrade
by marcodelpin · 2026-02-19
84.4%
#22937: fix: remove legacy unsuffixed QMD collections on upgrade
by sud0n1m-ziggy · 2026-02-21
83.6%
#21978: perf: parallelize QMD multi-collection queries
by maximalmargin · 2026-02-20
79.6%
#9624: fix(memory): resolve QMD search returning empty results [AI-assisted]
by kowshik24 · 2026-02-05
79.5%
#16917: fix(memory): close stale SQLite connection after qmd update
by zerone0x · 2026-02-15
79.0%
#20085: Fix QMD memory_search empty results when docid key changes
by rylena · 2026-02-18
77.3%
#19022: memory: support per-agent QMD collection paths
by Whoaa512 · 2026-02-17
77.3%
#9381: Fix: Allow QMD CLI memory search when scope is restrictive
by vishaltandale00 · 2026-02-05
77.3%
#11364: fix(memory/qmd): prevent cascading failure when query fails or retu...
by blazerui · 2026-02-07
75.7%