clawmem 0.11.3 → 0.13.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 +3 -1
- package/CLAUDE.md +3 -1
- package/README.md +2 -2
- package/SKILL.md +1 -1
- package/package.json +1 -1
- package/src/mcp.ts +19 -13
- package/src/memory.ts +27 -3
- package/src/search-utils.ts +45 -0
package/AGENTS.md
CHANGED
|
@@ -461,6 +461,8 @@ Recency intent detected ("latest", "recent", "last session"):
|
|
|
461
461
|
compositeScore = (0.10 × searchScore + 0.70 × recencyScore + 0.20 × confidenceScore) × qualityMultiplier × coActivationBoost
|
|
462
462
|
```
|
|
463
463
|
|
|
464
|
+
**`query` tool (v0.13.0+):** for non-recency queries the `query` tool uses retrieval-tuned weights `0.70 × searchScore + 0.15 × recencyScore + 0.15 × confidenceScore` (from a held-out judged eval). Recency intent still switches to the `0.10 / 0.70 / 0.20` weights above. `search`, `vsearch`, `memory_retrieve`, and the context-surfacing hook keep the `0.50 / 0.25 / 0.25` default.
|
|
465
|
+
|
|
464
466
|
| Content Type | Half-Life | Effect |
|
|
465
467
|
|--------------|-----------|--------|
|
|
466
468
|
| decision, deductive, preference, hub | ∞ | Never decay |
|
|
@@ -553,7 +555,7 @@ User Query + optional intent hint
|
|
|
553
555
|
→ Reciprocal Rank Fusion (k=60, top candidateLimit, up to 4-way parallel legs)
|
|
554
556
|
→ Intent-Aware Chunk Selection (intent terms at 0.5× weight alongside query terms at 1.0×)
|
|
555
557
|
→ Cross-Encoder Reranking (4000 char context; intent prepended to rerank query; chunk dedup; batch cap=4)
|
|
556
|
-
→
|
|
558
|
+
→ 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
559
|
→ SAME Composite Scoring
|
|
558
560
|
→ MMR Diversity Filter (Jaccard bigram similarity > 0.6 → demoted, not removed)
|
|
559
561
|
```
|
package/CLAUDE.md
CHANGED
|
@@ -461,6 +461,8 @@ Recency intent detected ("latest", "recent", "last session"):
|
|
|
461
461
|
compositeScore = (0.10 × searchScore + 0.70 × recencyScore + 0.20 × confidenceScore) × qualityMultiplier × coActivationBoost
|
|
462
462
|
```
|
|
463
463
|
|
|
464
|
+
**`query` tool (v0.13.0+):** for non-recency queries the `query` tool uses retrieval-tuned weights `0.70 × searchScore + 0.15 × recencyScore + 0.15 × confidenceScore` (from a held-out judged eval). Recency intent still switches to the `0.10 / 0.70 / 0.20` weights above. `search`, `vsearch`, `memory_retrieve`, and the context-surfacing hook keep the `0.50 / 0.25 / 0.25` default.
|
|
465
|
+
|
|
464
466
|
| Content Type | Half-Life | Effect |
|
|
465
467
|
|--------------|-----------|--------|
|
|
466
468
|
| decision, deductive, preference, hub | ∞ | Never decay |
|
|
@@ -553,7 +555,7 @@ User Query + optional intent hint
|
|
|
553
555
|
→ Reciprocal Rank Fusion (k=60, top candidateLimit, up to 4-way parallel legs)
|
|
554
556
|
→ Intent-Aware Chunk Selection (intent terms at 0.5× weight alongside query terms at 1.0×)
|
|
555
557
|
→ Cross-Encoder Reranking (4000 char context; intent prepended to rerank query; chunk dedup; batch cap=4)
|
|
556
|
-
→
|
|
558
|
+
→ 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
559
|
→ SAME Composite Scoring
|
|
558
560
|
→ MMR Diversity Filter (Jaccard bigram similarity > 0.6 → demoted, not removed)
|
|
559
561
|
```
|
package/README.md
CHANGED
|
@@ -813,8 +813,8 @@ 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
|
-
→
|
|
817
|
-
→
|
|
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
|
+
→ Composite Scoring (query-tuned: (search × 0.70 + recency × 0.15 + confidence × 0.15) × qualityMultiplier × lengthNorm × coActivationBoost + pinBoost; recency-intent queries → 0.10/0.70/0.20; other tools keep the 0.50/0.25/0.25 default)
|
|
818
818
|
→ MMR Diversity Filter (Jaccard bigram similarity > 0.6 → demoted)
|
|
819
819
|
→ Ranked Results
|
|
820
820
|
```
|
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
|
@@ -28,10 +28,11 @@ import {
|
|
|
28
28
|
import {
|
|
29
29
|
applyCompositeScoring,
|
|
30
30
|
hasRecencyIntent,
|
|
31
|
+
QUERY_WEIGHTS,
|
|
31
32
|
type EnrichedResult,
|
|
32
33
|
type CoActivationFn,
|
|
33
34
|
} from "./memory.ts";
|
|
34
|
-
import { enrichResults, reciprocalRankFusion, toRanked, type RankedResult } from "./search-utils.ts";
|
|
35
|
+
import { enrichResults, reciprocalRankFusion, toRanked, blendRerank, type RankedResult } from "./search-utils.ts";
|
|
35
36
|
import { applyMMRDiversity } from "./mmr.ts";
|
|
36
37
|
import { indexCollection, type IndexStats } from "./indexer.ts";
|
|
37
38
|
import { listCollections } from "./collections.ts";
|
|
@@ -753,19 +754,21 @@ This is the recommended entry point for ALL memory queries.`,
|
|
|
753
754
|
return { file: c.file, text };
|
|
754
755
|
});
|
|
755
756
|
|
|
756
|
-
|
|
757
|
+
let reranked: { file: string; score: number }[] = [];
|
|
758
|
+
try {
|
|
759
|
+
reranked = await store.rerank(query, chunksToRerank, DEFAULT_RERANK_MODEL, intent);
|
|
760
|
+
} catch {
|
|
761
|
+
reranked = []; // reranker unavailable → blendRerank falls back to pure RRF order
|
|
762
|
+
}
|
|
757
763
|
|
|
758
764
|
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);
|
|
765
|
+
|
|
766
|
+
// Blend the reranker (dominant signal) with a thin normalized-RRF tiebreaker; falls back
|
|
767
|
+
// to pure RRF order when the reranker is unavailable / all-zero. See blendRerank: the
|
|
768
|
+
// prior w·(1/rrfRank) blend made RRF rank-1 immovable by the reranker. Harness-validated
|
|
769
|
+
// 2026-06-25 (NL+KW known-item recall): lifts recall@1-5 + MRR@10 with no material pooled
|
|
770
|
+
// recall@10 regression.
|
|
771
|
+
const blended = blendRerank(candidates, reranked);
|
|
769
772
|
|
|
770
773
|
// Map to SearchResults for composite scoring — hydrate from DB when needed
|
|
771
774
|
const allSearchResults = [...store.searchFTS(query, 30)];
|
|
@@ -805,7 +808,10 @@ This is the recommended entry point for ALL memory queries.`,
|
|
|
805
808
|
|
|
806
809
|
const coFn = (path: string) => store.getCoActivated(path);
|
|
807
810
|
const enriched = enrichResults(store, searchResults, query);
|
|
808
|
-
|
|
811
|
+
// Phase B (§11.12): the `query` tool's hybrid+rerank pipeline uses QUERY_WEIGHTS (search 0.70) —
|
|
812
|
+
// the eval-validated re-weight. Recency intent still wins RECENCY_WEIGHTS by construction
|
|
813
|
+
// (applyCompositeScoring ignores options.weights when hasRecencyIntent(query) and !forceWeights).
|
|
814
|
+
let scored = applyCompositeScoring(enriched, query, coFn, { weights: QUERY_WEIGHTS })
|
|
809
815
|
.filter(r => r.compositeScore >= (minScore || 0));
|
|
810
816
|
if (diverse !== false) scored = applyMMRDiversity(scored);
|
|
811
817
|
scored = scored.slice(0, limit || 10);
|
package/src/memory.ts
CHANGED
|
@@ -185,6 +185,15 @@ export type CompositeWeights = {
|
|
|
185
185
|
|
|
186
186
|
export const DEFAULT_WEIGHTS: CompositeWeights = { search: 0.5, recency: 0.25, confidence: 0.25 };
|
|
187
187
|
export const RECENCY_WEIGHTS: CompositeWeights = { search: 0.1, recency: 0.7, confidence: 0.2 };
|
|
188
|
+
// Query-tool retrieval weights (Phase B, 2026-06-25). A held-out judged eval (n=199, GLM-5.2 judge,
|
|
189
|
+
// quadratic-weighted κ=0.681 vs an independent annotator) showed the default 0.50 search weight
|
|
190
|
+
// under-weights topical relevance for a work-memory vault: w_search 0.70 lifts graded NDCG@10 by +0.064
|
|
191
|
+
// (paired permutation p<1e-4, robust across precision/exploratory/temporal families) with ZERO freshness regression —
|
|
192
|
+
// the newest-correct doc is never demoted in the supersession guard (demotionRate 0), whereas 0.80
|
|
193
|
+
// demotes it out of the top-10 in 2/19 cases. Applied ONLY by the `query` tool's full hybrid+rerank
|
|
194
|
+
// pipeline (the path the eval mirrored). NOT applied under recency intent — RECENCY_WEIGHTS wins by
|
|
195
|
+
// construction in applyCompositeScoring. See BLEND-COMPOSITE-REBALANCE-DESIGN.md §11.12.
|
|
196
|
+
export const QUERY_WEIGHTS: CompositeWeights = { search: 0.7, recency: 0.15, confidence: 0.15 };
|
|
188
197
|
|
|
189
198
|
const RECENCY_PATTERNS = [
|
|
190
199
|
/\brecent(ly)?\b/i,
|
|
@@ -274,13 +283,28 @@ function canonicalMemoryMultiplier(path: string, contentType: string, query: str
|
|
|
274
283
|
return 1.0;
|
|
275
284
|
}
|
|
276
285
|
|
|
286
|
+
export type CompositeScoringOptions = {
|
|
287
|
+
/** Query-scoped weights override. Replaces DEFAULT_WEIGHTS; NOT applied under recency intent unless forceWeights. */
|
|
288
|
+
weights?: CompositeWeights;
|
|
289
|
+
/** Injected clock for deterministic scoring (tests/eval). Defaults to new Date(). */
|
|
290
|
+
now?: Date;
|
|
291
|
+
/** Test/experiment only: apply `weights` even under recency intent (bypass the RECENCY_WEIGHTS switch). */
|
|
292
|
+
forceWeights?: boolean;
|
|
293
|
+
};
|
|
294
|
+
|
|
277
295
|
export function applyCompositeScoring(
|
|
278
296
|
results: EnrichedResult[],
|
|
279
297
|
query: string,
|
|
280
|
-
coActivationFn?: CoActivationFn
|
|
298
|
+
coActivationFn?: CoActivationFn,
|
|
299
|
+
options?: CompositeScoringOptions
|
|
281
300
|
): ScoredResult[] {
|
|
282
|
-
const
|
|
283
|
-
|
|
301
|
+
const recencyIntent = hasRecencyIntent(query);
|
|
302
|
+
// Recency intent keeps RECENCY_WEIGHTS (production contract) unless forceWeights overrides (experiments).
|
|
303
|
+
// A query-scoped `weights` override otherwise replaces DEFAULT_WEIGHTS; absent options => exact prior behavior.
|
|
304
|
+
const weights = (recencyIntent && !options?.forceWeights)
|
|
305
|
+
? RECENCY_WEIGHTS
|
|
306
|
+
: (options?.weights ?? (recencyIntent ? RECENCY_WEIGHTS : DEFAULT_WEIGHTS));
|
|
307
|
+
const now = options?.now ?? new Date();
|
|
284
308
|
|
|
285
309
|
const scored = results.map(r => {
|
|
286
310
|
const recency = recencyScore(r.modifiedAt, r.contentType, now, r.accessCount, r.lastAccessedAt);
|
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
|
*/
|