From 328cf1db6a498adc5903f7da1436a8499d89c927 Mon Sep 17 00:00:00 2001 From: Frad LEE Date: Thu, 19 Feb 2026 00:54:31 +0800 Subject: [PATCH 1/4] feat(memory): add path-based weights for QMD search --- src/config/types.memory.ts | 1 + src/memory/backend-config.ts | 2 ++ src/memory/qmd-manager.ts | 51 +++++++++++++++++++++++++++++++----- 3 files changed, 47 insertions(+), 7 deletions(-) diff --git a/src/config/types.memory.ts b/src/config/types.memory.ts index 54581f65fac..fb78f0eaecf 100644 --- a/src/config/types.memory.ts +++ b/src/config/types.memory.ts @@ -20,6 +20,7 @@ export type MemoryQmdConfig = { update?: MemoryQmdUpdateConfig; limits?: MemoryQmdLimitsConfig; scope?: SessionSendPolicyConfig; + weights?: Record; }; export type MemoryQmdMcporterConfig = { diff --git a/src/memory/backend-config.ts b/src/memory/backend-config.ts index da1c13819a3..174bd7a7518 100644 --- a/src/memory/backend-config.ts +++ b/src/memory/backend-config.ts @@ -67,6 +67,7 @@ export type ResolvedQmdConfig = { limits: ResolvedQmdLimitsConfig; includeDefaultMemory: boolean; scope?: SessionSendPolicyConfig; + weights?: Record; }; const DEFAULT_BACKEND: MemoryBackend = "builtin"; @@ -344,6 +345,7 @@ export function resolveMemoryBackendConfig(params: { }, limits: resolveLimits(qmdCfg?.limits), scope: qmdCfg?.scope ?? DEFAULT_QMD_SCOPE, + weights: qmdCfg?.weights, }; return { diff --git a/src/memory/qmd-manager.ts b/src/memory/qmd-manager.ts index 55fe04b2173..8962b947afe 100644 --- a/src/memory/qmd-manager.ts +++ b/src/memory/qmd-manager.ts @@ -622,6 +622,9 @@ export class QmdMemoryManager implements MemorySearchManager { this.qmd.limits.maxResults, opts?.maxResults ?? this.qmd.limits.maxResults, ); + // Fetch more results to allow for client-side re-ranking (weighting) + const fetchLimit = this.qmd.weights ? limit * 5 : limit; + const collectionNames = this.listManagedCollectionNames(); if (collectionNames.length === 0) { log.warn("qmd query skipped: no managed collections configured"); @@ -645,7 +648,7 @@ export class QmdMemoryManager implements MemorySearchManager { return await this.runMcporterAcrossCollections({ tool, query: trimmed, - limit, + limit: fetchLimit, minScore, collectionNames, }); @@ -654,7 +657,7 @@ export class QmdMemoryManager implements MemorySearchManager { mcporter: this.qmd.mcporter, tool, query: trimmed, - limit, + limit: fetchLimit, minScore, collection: collectionNames[0], timeoutMs: this.qmd.limits.timeoutMs, @@ -663,12 +666,12 @@ export class QmdMemoryManager implements MemorySearchManager { if (collectionNames.length > 1) { return await this.runQueryAcrossCollections( trimmed, - limit, + fetchLimit, collectionNames, qmdSearchCommand, ); } - const args = this.buildSearchArgs(qmdSearchCommand, trimmed, limit); + const args = this.buildSearchArgs(qmdSearchCommand, trimmed, fetchLimit); args.push(...this.buildCollectionFilterArgs(collectionNames)); // Always scope to managed collections (default + custom). Even for `search`/`vsearch`, // pass collection filters; if a given QMD build rejects these flags, we fall back to `query`. @@ -688,9 +691,9 @@ export class QmdMemoryManager implements MemorySearchManager { ); try { if (collectionNames.length > 1) { - return await this.runQueryAcrossCollections(trimmed, limit, collectionNames, "query"); + return await this.runQueryAcrossCollections(trimmed, fetchLimit, collectionNames, "query"); } - const fallbackArgs = this.buildSearchArgs("query", trimmed, limit); + const fallbackArgs = this.buildSearchArgs("query", trimmed, fetchLimit); fallbackArgs.push(...this.buildCollectionFilterArgs(collectionNames)); const fallback = await this.runQmd(fallbackArgs, { timeoutMs: this.qmd.limits.timeoutMs, @@ -717,6 +720,9 @@ export class QmdMemoryManager implements MemorySearchManager { parsed = await runSearchAttempt(false); } const results: MemorySearchResult[] = []; + const weights = this.qmd.weights || {}; + const hasWeights = Object.keys(weights).length > 0; + for (const entry of parsed) { const doc = await this.resolveDocLocation(entry.docid, { preferredCollection: entry.collection, @@ -727,7 +733,17 @@ export class QmdMemoryManager implements MemorySearchManager { } const snippet = entry.snippet?.slice(0, this.qmd.limits.maxSnippetChars) ?? ""; const lines = this.extractSnippetLines(snippet); - const score = typeof entry.score === "number" ? entry.score : 0; + let score = typeof entry.score === "number" ? entry.score : 0; + + // Apply weights + if (hasWeights) { + for (const [pattern, weight] of Object.entries(weights)) { + if (this.matchesPath(doc.rel, pattern)) { + score *= weight; + } + } + } + const minScore = opts?.minScore ?? 0; if (score < minScore) { continue; @@ -741,9 +757,30 @@ export class QmdMemoryManager implements MemorySearchManager { source: doc.source, }); } + + // Re-sort if weights were applied + if (hasWeights) { + results.sort((a, b) => b.score - a.score); + } + return this.clampResultsByInjectedChars(this.diversifyResultsBySource(results, limit)); } + private matchesPath(target: string, pattern: string): boolean { + // Simple glob matching + // Handle "dir/**" + if (pattern.endsWith("/**")) { + const prefix = pattern.slice(0, -3); + return target.startsWith(prefix + "/") || target === prefix; + } + // Handle "*.md" + if (pattern.startsWith("*")) { + return target.endsWith(pattern.slice(1)); + } + // Exact match + return target === pattern; + } + async sync(params?: { reason?: string; force?: boolean; From 61c0cc0a87859f2b6268a5240228c110ff494035 Mon Sep 17 00:00:00 2001 From: Frad LEE Date: Thu, 19 Feb 2026 01:17:27 +0800 Subject: [PATCH 2/4] fix(memory): avoid over-fetching when weights config is empty --- src/memory/qmd-manager.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/memory/qmd-manager.ts b/src/memory/qmd-manager.ts index 8962b947afe..f19177a7e39 100644 --- a/src/memory/qmd-manager.ts +++ b/src/memory/qmd-manager.ts @@ -623,7 +623,8 @@ export class QmdMemoryManager implements MemorySearchManager { opts?.maxResults ?? this.qmd.limits.maxResults, ); // Fetch more results to allow for client-side re-ranking (weighting) - const fetchLimit = this.qmd.weights ? limit * 5 : limit; + const hasWeights = this.qmd.weights && Object.keys(this.qmd.weights).length > 0; + const fetchLimit = hasWeights ? limit * 5 : limit; const collectionNames = this.listManagedCollectionNames(); if (collectionNames.length === 0) { @@ -721,7 +722,6 @@ export class QmdMemoryManager implements MemorySearchManager { } const results: MemorySearchResult[] = []; const weights = this.qmd.weights || {}; - const hasWeights = Object.keys(weights).length > 0; for (const entry of parsed) { const doc = await this.resolveDocLocation(entry.docid, { From 5daa4b0b626e4eb1d5accedb1f7ee7afb07e4671 Mon Sep 17 00:00:00 2001 From: Frad LEE Date: Thu, 19 Feb 2026 01:19:13 +0800 Subject: [PATCH 3/4] fix(memory): improved glob matching for **/*.ext patterns --- src/memory/qmd-manager.ts | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/memory/qmd-manager.ts b/src/memory/qmd-manager.ts index f19177a7e39..700bbe7bc10 100644 --- a/src/memory/qmd-manager.ts +++ b/src/memory/qmd-manager.ts @@ -692,7 +692,12 @@ export class QmdMemoryManager implements MemorySearchManager { ); try { if (collectionNames.length > 1) { - return await this.runQueryAcrossCollections(trimmed, fetchLimit, collectionNames, "query"); + return await this.runQueryAcrossCollections( + trimmed, + fetchLimit, + collectionNames, + "query", + ); } const fallbackArgs = this.buildSearchArgs("query", trimmed, fetchLimit); fallbackArgs.push(...this.buildCollectionFilterArgs(collectionNames)); @@ -768,11 +773,25 @@ export class QmdMemoryManager implements MemorySearchManager { private matchesPath(target: string, pattern: string): boolean { // Simple glob matching + if (pattern === "**") { + return true; + } + // Handle "dir/**" if (pattern.endsWith("/**")) { const prefix = pattern.slice(0, -3); return target.startsWith(prefix + "/") || target === prefix; } + + // Handle "**/*.md" or "**/foo" + if (pattern.startsWith("**/")) { + const rest = pattern.slice(3); + if (rest.startsWith("*")) { + return target.endsWith(rest.slice(1)); + } + return target.endsWith("/" + rest) || target === rest; + } + // Handle "*.md" if (pattern.startsWith("*")) { return target.endsWith(pattern.slice(1)); From 6a717bfa0600974983ceab3d9dabf8ccea9072ca Mon Sep 17 00:00:00 2001 From: Frad LEE Date: Thu, 26 Feb 2026 15:02:33 +0800 Subject: [PATCH 4/4] fix(memory): fix qmd weights from pr review - Use first-match logic for weight patterns - Add zod validation for positive weight values - Defer minscore filtering until after re-ranking Ensures predictable weight application and proper config validation. Co-Authored-By: Claude Opus 4.6 --- src/config/zod-schema.ts | 1 + src/memory/qmd-manager.ts | 10 ++++++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/config/zod-schema.ts b/src/config/zod-schema.ts index 6ea3bd00287..7725e64569b 100644 --- a/src/config/zod-schema.ts +++ b/src/config/zod-schema.ts @@ -103,6 +103,7 @@ const MemoryQmdSchema = z update: MemoryQmdUpdateSchema.optional(), limits: MemoryQmdLimitsSchema.optional(), scope: SessionSendPolicySchema.optional(), + weights: z.record(z.string(), z.number().positive()).optional(), }) .strict(); diff --git a/src/memory/qmd-manager.ts b/src/memory/qmd-manager.ts index 700bbe7bc10..2e93ce0b528 100644 --- a/src/memory/qmd-manager.ts +++ b/src/memory/qmd-manager.ts @@ -633,6 +633,8 @@ export class QmdMemoryManager implements MemorySearchManager { } const qmdSearchCommand = this.qmd.searchMode; const mcporterEnabled = this.qmd.mcporter.enabled; + // When weights are enabled, defer minScore filtering until after re-ranking + const upstreamMinScore = hasWeights ? 0 : (opts?.minScore ?? 0); const runSearchAttempt = async ( allowMissingCollectionRepair: boolean, ): Promise => { @@ -644,13 +646,12 @@ export class QmdMemoryManager implements MemorySearchManager { : qmdSearchCommand === "vsearch" ? "vector_search" : "deep_search"; - const minScore = opts?.minScore ?? 0; if (collectionNames.length > 1) { return await this.runMcporterAcrossCollections({ tool, query: trimmed, limit: fetchLimit, - minScore, + minScore: upstreamMinScore, collectionNames, }); } @@ -659,7 +660,7 @@ export class QmdMemoryManager implements MemorySearchManager { tool, query: trimmed, limit: fetchLimit, - minScore, + minScore: upstreamMinScore, collection: collectionNames[0], timeoutMs: this.qmd.limits.timeoutMs, }); @@ -740,11 +741,12 @@ export class QmdMemoryManager implements MemorySearchManager { const lines = this.extractSnippetLines(snippet); let score = typeof entry.score === "number" ? entry.score : 0; - // Apply weights + // Apply weights (first matching pattern wins) if (hasWeights) { for (const [pattern, weight] of Object.entries(weights)) { if (this.matchesPath(doc.rel, pattern)) { score *= weight; + break; } } }