clawmem 0.11.3 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +1 -1
- package/CLAUDE.md +1 -1
- package/README.md +1 -1
- package/SKILL.md +1 -1
- package/package.json +1 -1
- package/src/mcp.ts +14 -12
- package/src/search-utils.ts +45 -0
package/AGENTS.md
CHANGED
|
@@ -553,7 +553,7 @@ User Query + optional intent hint
|
|
|
553
553
|
→ Reciprocal Rank Fusion (k=60, top candidateLimit, up to 4-way parallel legs)
|
|
554
554
|
→ Intent-Aware Chunk Selection (intent terms at 0.5× weight alongside query terms at 1.0×)
|
|
555
555
|
→ Cross-Encoder Reranking (4000 char context; intent prepended to rerank query; chunk dedup; batch cap=4)
|
|
556
|
-
→
|
|
556
|
+
→ Rerank/RRF Blend (blendRerank: 0.9·reranker + 0.1·normalized-RRF tiebreaker; reranker can promote over RRF #1; falls back to RRF order if reranker unavailable)
|
|
557
557
|
→ SAME Composite Scoring
|
|
558
558
|
→ MMR Diversity Filter (Jaccard bigram similarity > 0.6 → demoted, not removed)
|
|
559
559
|
```
|
package/CLAUDE.md
CHANGED
|
@@ -553,7 +553,7 @@ User Query + optional intent hint
|
|
|
553
553
|
→ Reciprocal Rank Fusion (k=60, top candidateLimit, up to 4-way parallel legs)
|
|
554
554
|
→ Intent-Aware Chunk Selection (intent terms at 0.5× weight alongside query terms at 1.0×)
|
|
555
555
|
→ Cross-Encoder Reranking (4000 char context; intent prepended to rerank query; chunk dedup; batch cap=4)
|
|
556
|
-
→
|
|
556
|
+
→ Rerank/RRF Blend (blendRerank: 0.9·reranker + 0.1·normalized-RRF tiebreaker; reranker can promote over RRF #1; falls back to RRF order if reranker unavailable)
|
|
557
557
|
→ SAME Composite Scoring
|
|
558
558
|
→ MMR Diversity Filter (Jaccard bigram similarity > 0.6 → demoted, not removed)
|
|
559
559
|
```
|
package/README.md
CHANGED
|
@@ -813,7 +813,7 @@ User Query + optional intent hint
|
|
|
813
813
|
→ Reciprocal Rank Fusion → slice to candidateLimit (default 30)
|
|
814
814
|
→ Intent-Aware Chunk Selection (intent terms at 0.5× weight alongside query terms at 1.0×)
|
|
815
815
|
→ Cross-Encoder Reranking (4000 char context; intent prepended; chunk dedup; batch cap=4)
|
|
816
|
-
→
|
|
816
|
+
→ Rerank/RRF Blend (blendRerank: 0.9·reranker + 0.1·normalized-RRF tiebreaker; reranker can promote over RRF #1; falls back to RRF order if reranker unavailable)
|
|
817
817
|
→ SAME Composite Scoring ((search × 0.5 + recency × 0.25 + confidence × 0.25) × qualityMultiplier × lengthNorm × coActivationBoost + pinBoost)
|
|
818
818
|
→ MMR Diversity Filter (Jaccard bigram similarity > 0.6 → demoted)
|
|
819
819
|
→ Ranked Results
|
package/SKILL.md
CHANGED
|
@@ -389,7 +389,7 @@ User Query + optional intent hint
|
|
|
389
389
|
-> Reciprocal Rank Fusion (k=60, top candidateLimit)
|
|
390
390
|
-> Intent-Aware Chunk Selection (intent terms at 0.5x weight alongside query terms at 1.0x)
|
|
391
391
|
-> Cross-Encoder Reranking (4000 char context; intent prepended to rerank query; chunk dedup; batch cap=4)
|
|
392
|
-
->
|
|
392
|
+
-> Rerank/RRF Blend (blendRerank: 0.9*reranker + 0.1*normalized-RRF tiebreaker; reranker can promote over RRF #1; falls back to RRF order if reranker unavailable)
|
|
393
393
|
-> Composite Scoring
|
|
394
394
|
-> MMR Diversity Filter (Jaccard bigram similarity > 0.6 -> demoted, not removed)
|
|
395
395
|
```
|
package/package.json
CHANGED
package/src/mcp.ts
CHANGED
|
@@ -31,7 +31,7 @@ import {
|
|
|
31
31
|
type EnrichedResult,
|
|
32
32
|
type CoActivationFn,
|
|
33
33
|
} from "./memory.ts";
|
|
34
|
-
import { enrichResults, reciprocalRankFusion, toRanked, type RankedResult } from "./search-utils.ts";
|
|
34
|
+
import { enrichResults, reciprocalRankFusion, toRanked, blendRerank, type RankedResult } from "./search-utils.ts";
|
|
35
35
|
import { applyMMRDiversity } from "./mmr.ts";
|
|
36
36
|
import { indexCollection, type IndexStats } from "./indexer.ts";
|
|
37
37
|
import { listCollections } from "./collections.ts";
|
|
@@ -753,19 +753,21 @@ This is the recommended entry point for ALL memory queries.`,
|
|
|
753
753
|
return { file: c.file, text };
|
|
754
754
|
});
|
|
755
755
|
|
|
756
|
-
|
|
756
|
+
let reranked: { file: string; score: number }[] = [];
|
|
757
|
+
try {
|
|
758
|
+
reranked = await store.rerank(query, chunksToRerank, DEFAULT_RERANK_MODEL, intent);
|
|
759
|
+
} catch {
|
|
760
|
+
reranked = []; // reranker unavailable → blendRerank falls back to pure RRF order
|
|
761
|
+
}
|
|
757
762
|
|
|
758
763
|
const candidateMap = new Map(candidates.map(c => [c.file, c]));
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
//
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
return { file: r.file, score: blendedScore };
|
|
767
|
-
});
|
|
768
|
-
blended.sort((a, b) => b.score - a.score);
|
|
764
|
+
|
|
765
|
+
// Blend the reranker (dominant signal) with a thin normalized-RRF tiebreaker; falls back
|
|
766
|
+
// to pure RRF order when the reranker is unavailable / all-zero. See blendRerank: the
|
|
767
|
+
// prior w·(1/rrfRank) blend made RRF rank-1 immovable by the reranker. Harness-validated
|
|
768
|
+
// 2026-06-25 (NL+KW known-item recall): lifts recall@1-5 + MRR@10 with no material pooled
|
|
769
|
+
// recall@10 regression.
|
|
770
|
+
const blended = blendRerank(candidates, reranked);
|
|
769
771
|
|
|
770
772
|
// Map to SearchResults for composite scoring — hydrate from DB when needed
|
|
771
773
|
const allSearchResults = [...store.searchFTS(query, 30)];
|
package/src/search-utils.ts
CHANGED
|
@@ -113,6 +113,51 @@ export function reciprocalRankFusion(
|
|
|
113
113
|
.map(v => ({ ...v.result, score: v.score }));
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
+
// =============================================================================
|
|
117
|
+
// Rerank / RRF Score Blend
|
|
118
|
+
// =============================================================================
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Blend a cross-encoder reranker's scores with the upstream RRF ranking.
|
|
122
|
+
*
|
|
123
|
+
* The reranker is the dominant relevance signal; normalized RRF contributes a thin tiebreaker.
|
|
124
|
+
* An earlier blend used `w·(1/rrfRank)` with `w≥0.75` on the top tier, which made RRF rank-1
|
|
125
|
+
* mathematically immovable by the reranker (0.75·(1/1) exceeds any rank-2 ceiling) — the
|
|
126
|
+
* reranker could never promote the best document to the top. This blend normalizes the RRF
|
|
127
|
+
* score to [0,1] and gives the reranker the dominant weight, so a strong rerank score CAN
|
|
128
|
+
* promote a doc over RRF #1. Harness-validated 2026-06-25 against NL+KW known-item recall:
|
|
129
|
+
* lifts recall@1-5 and MRR@10 with no material pooled recall@10 regression.
|
|
130
|
+
*
|
|
131
|
+
* Falls back to pure RRF order when the reranker is unavailable or returned no usable signal
|
|
132
|
+
* (empty, or all-zero — e.g. a total remote+local failure). Maps over `candidates` (not the
|
|
133
|
+
* rerank output) so partial rerank coverage can never drop a candidate; an unscored doc takes
|
|
134
|
+
* rerank score 0 (so it sorts on its thin `(1-rerankWeight)·rrfNorm` term) and unscored docs
|
|
135
|
+
* preserve their relative RRF order among themselves.
|
|
136
|
+
*
|
|
137
|
+
* @param candidates - RRF-ordered candidates; `score` is the RRF fusion score (positive).
|
|
138
|
+
* @param reranked - reranker output `{file, score in [0,1]}`; may be empty/partial/all-zero.
|
|
139
|
+
* @param rerankWeight - weight on the reranker term (default 0.9; `1-rerankWeight` on RRF).
|
|
140
|
+
* @returns candidates re-scored and sorted by blended score descending.
|
|
141
|
+
*/
|
|
142
|
+
export function blendRerank(
|
|
143
|
+
candidates: { file: string; score: number }[],
|
|
144
|
+
reranked: { file: string; score: number }[],
|
|
145
|
+
rerankWeight: number = 0.9
|
|
146
|
+
): { file: string; score: number }[] {
|
|
147
|
+
const rerankScoreMap = new Map(reranked.map(r => [r.file, r.score]));
|
|
148
|
+
const rerankUsable = reranked.length > 0 && reranked.some(r => Number.isFinite(r.score) && r.score > 0);
|
|
149
|
+
const maxRrf = candidates.reduce((m, c) => Math.max(m, c.score), 0) || 1;
|
|
150
|
+
return candidates
|
|
151
|
+
.map(c => {
|
|
152
|
+
const rrfNorm = c.score / maxRrf; // [0,1]
|
|
153
|
+
if (!rerankUsable) return { file: c.file, score: rrfNorm };
|
|
154
|
+
const rr = rerankScoreMap.get(c.file);
|
|
155
|
+
const rerankScore = Number.isFinite(rr) ? (rr as number) : 0;
|
|
156
|
+
return { file: c.file, score: (1 - rerankWeight) * rrfNorm + rerankWeight * rerankScore };
|
|
157
|
+
})
|
|
158
|
+
.sort((a, b) => b.score - a.score);
|
|
159
|
+
}
|
|
160
|
+
|
|
116
161
|
/**
|
|
117
162
|
* Convert a SearchResult to a RankedResult for use in RRF.
|
|
118
163
|
*/
|