clawmem 0.20.2 → 0.22.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 +6 -6
- package/README.md +6 -3
- package/SKILL.md +6 -6
- package/docs/concepts/architecture.md +1 -1
- package/docs/concepts/composite-scoring.md +6 -4
- package/docs/guides/inference-services.md +11 -2
- package/docs/guides/upgrading.md +33 -3
- package/docs/introduction.md +1 -1
- package/docs/quickstart.md +1 -1
- package/docs/reference/cli.md +5 -3
- package/docs/reference/configuration.md +1 -0
- package/docs/reference/mcp-tools.md +15 -2
- package/docs/troubleshooting.md +7 -4
- package/package.json +1 -1
- package/src/busy-retry.ts +34 -0
- package/src/canary.ts +364 -0
- package/src/clawmem.ts +271 -22
- package/src/config.ts +29 -1
- package/src/graph-traversal.ts +32 -4
- package/src/mcp.ts +207 -63
- package/src/memory.ts +1 -0
- package/src/scoring-regime.ts +79 -0
- package/src/store.ts +366 -32
package/src/mcp.ts
CHANGED
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
type CoActivationFn,
|
|
35
35
|
} from "./memory.ts";
|
|
36
36
|
import { enrichResults, reciprocalRankFusion, toRanked, blendRerank, type RankedResult } from "./search-utils.ts";
|
|
37
|
+
import { selectScoringRegime, rankRawPrimary, VECTOR_SCORE_BASIS, COMPOSITE_SCORE_BASIS } from "./scoring-regime.ts";
|
|
37
38
|
import { applyMMRDiversity } from "./mmr.ts";
|
|
38
39
|
import { indexCollection, type IndexStats } from "./indexer.ts";
|
|
39
40
|
import { listCollections } from "./collections.ts";
|
|
@@ -114,6 +115,32 @@ function splitIntoWindows(text: string, windowSize: number, overlap = 200): stri
|
|
|
114
115
|
}
|
|
115
116
|
|
|
116
117
|
/** Classify query into retrieval mode based on signal patterns */
|
|
118
|
+
// =============================================================================
|
|
119
|
+
// Retrieval visibility policy (VSEARCH-TRUST-HARDENING (b))
|
|
120
|
+
// =============================================================================
|
|
121
|
+
|
|
122
|
+
// System-internal collections excluded from MCP retrieval by default. Hook precedent:
|
|
123
|
+
// FILTERED_PATHS in context-surfacing. Opt-ins: the includeInternal param, or an explicit
|
|
124
|
+
// collection filter naming an internal collection.
|
|
125
|
+
const INTERNAL_COLLECTIONS = ["_clawmem"];
|
|
126
|
+
|
|
127
|
+
function resolveExcludedCollections(includeInternal: boolean | undefined, collections?: string[]): string[] | undefined {
|
|
128
|
+
if (includeInternal) return undefined;
|
|
129
|
+
if (collections && collections.some(c => INTERNAL_COLLECTIONS.includes(c))) return undefined;
|
|
130
|
+
return INTERNAL_COLLECTIONS;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
type DegradedLeg = { leg: string; reason: "excluded-dominant" | "cap-truncation" };
|
|
134
|
+
|
|
135
|
+
// Only excluded-dominant carries the includeInternal advice — cap-truncation must stay
|
|
136
|
+
// truthful when the shortfall is dedup-driven (T5-M2).
|
|
137
|
+
function degradedGuidanceText(legs: DegradedLeg[]): string {
|
|
138
|
+
const anyExcludedDominant = legs.some(l => l.reason === "excluded-dominant");
|
|
139
|
+
return anyExcludedDominant
|
|
140
|
+
? "Note: nearest-neighbor region dominated by excluded internal docs — pass includeInternal:true or refine the query."
|
|
141
|
+
: "Note: vector results truncated at the scan cap.";
|
|
142
|
+
}
|
|
143
|
+
|
|
117
144
|
function classifyRetrievalMode(query: string): "keyword" | "semantic" | "causal" | "timeline" | "discovery" | "complex" | "hybrid" {
|
|
118
145
|
const q = query.toLowerCase();
|
|
119
146
|
|
|
@@ -161,7 +188,13 @@ function addLineNumbers(text: string, startLine: number = 1): string {
|
|
|
161
188
|
// MCP Server
|
|
162
189
|
// =============================================================================
|
|
163
190
|
|
|
164
|
-
|
|
191
|
+
/**
|
|
192
|
+
* Build the fully-registered MCP server WITHOUT connecting a transport.
|
|
193
|
+
* Extracted from startMcpServer so tests can drive the real tool handlers over an
|
|
194
|
+
* in-memory transport (route-level regressions for visibility exclusion + degraded
|
|
195
|
+
* markers). Returns the server plus a close() that releases every store handle.
|
|
196
|
+
*/
|
|
197
|
+
export function buildMcpServer(): { server: McpServer; store: Store; closeAllStores: () => void } {
|
|
165
198
|
const store = createStore(undefined, { busyTimeout: 5000 });
|
|
166
199
|
|
|
167
200
|
// Vault store cache: prevents connection churn, closed on shutdown
|
|
@@ -251,13 +284,16 @@ This is the recommended entry point for ALL memory queries.`,
|
|
|
251
284
|
mode: z.enum(["auto", "keyword", "semantic", "causal", "timeline", "discovery", "complex", "hybrid"]).optional().default("auto").describe("Override auto-detection: keyword=BM25, semantic=vector, causal=graph traversal, timeline=session history, discovery=similar docs, complex=multi-topic, hybrid=full pipeline"),
|
|
252
285
|
limit: z.number().optional().default(10),
|
|
253
286
|
compact: z.boolean().optional().default(true),
|
|
287
|
+
includeInternal: z.boolean().optional().default(false).describe("Include system-internal _clawmem docs (observations/deductions) — excluded by default"),
|
|
254
288
|
vault: z.string().optional().describe("Named vault (omit for default vault)"),
|
|
255
289
|
},
|
|
256
290
|
},
|
|
257
|
-
async ({ query, mode, limit, compact, vault }) => {
|
|
291
|
+
async ({ query, mode, limit, compact, includeInternal, vault }) => {
|
|
258
292
|
const store = getStore(vault);
|
|
259
293
|
const effectiveMode = mode === "auto" ? classifyRetrievalMode(query) : mode;
|
|
260
294
|
const lim = limit || 10;
|
|
295
|
+
const excl = resolveExcludedCollections(includeInternal);
|
|
296
|
+
const degradedLegs: DegradedLeg[] = [];
|
|
261
297
|
|
|
262
298
|
// --- Timeline mode → session log ---
|
|
263
299
|
if (effectiveMode === "timeline") {
|
|
@@ -282,9 +318,16 @@ This is the recommended entry point for ALL memory queries.`,
|
|
|
282
318
|
if (effectiveMode === "causal") {
|
|
283
319
|
const llm = getDefaultLlamaCpp();
|
|
284
320
|
const intent = await classifyIntent(query, llm, store.db);
|
|
285
|
-
const bm25Results = store.searchFTS(query, 30);
|
|
321
|
+
const bm25Results = store.searchFTS(query, 30, undefined, undefined, undefined, excl);
|
|
286
322
|
let vecResults: SearchResult[] = [];
|
|
287
|
-
|
|
323
|
+
let causalDegraded: DegradedLeg | undefined;
|
|
324
|
+
try {
|
|
325
|
+
const det = await store.searchVecDetailed(query, DEFAULT_EMBED_MODEL, 30, { excludeCollections: excl });
|
|
326
|
+
vecResults = det.results;
|
|
327
|
+
// Causal mode runs ONE vector call — flat single-vector shape per the documented
|
|
328
|
+
// contract (T8-M5), not the multi-leg degradedLegs aggregate.
|
|
329
|
+
if (det.degraded && det.degradedReason) causalDegraded = { leg: "causal:vector", reason: det.degradedReason };
|
|
330
|
+
} catch (e) { rethrowIfFatalVectorError(e); /* else: no vectors */ }
|
|
288
331
|
const rrfWeights = intent.intent === 'WHY' ? [1.0, 1.5] : intent.intent === 'WHEN' ? [1.5, 1.0] : [1.0, 1.0];
|
|
289
332
|
const fusedRanked = reciprocalRankFusion([bm25Results.map(toRanked), vecResults.map(toRanked)], rrfWeights);
|
|
290
333
|
const allSearch = [...bm25Results, ...vecResults];
|
|
@@ -300,6 +343,9 @@ This is the recommended entry point for ALL memory queries.`,
|
|
|
300
343
|
const traversed = adaptiveTraversal(store.db, fused.slice(0, 10).map(r => ({ hash: r.hash, score: r.score })), {
|
|
301
344
|
maxDepth: 2, beamWidth: 5, budget: 30,
|
|
302
345
|
intent: intent.intent, queryEmbedding: anchorEmb.embedding,
|
|
346
|
+
// Visibility policy threaded INTO traversal (T3-H2): excluded nodes are pruned
|
|
347
|
+
// before beam selection and score normalization, not just hidden at hydration.
|
|
348
|
+
excludeCollections: excl,
|
|
303
349
|
});
|
|
304
350
|
const merged = mergeTraversalResults(store.db, fused.map(r => ({ hash: r.hash, score: r.score })), traversed);
|
|
305
351
|
// Hydrate merged results back to SearchResult format
|
|
@@ -307,7 +353,8 @@ This is the recommended entry point for ALL memory queries.`,
|
|
|
307
353
|
fused = merged.map(m => {
|
|
308
354
|
const orig = fusedMap.get(m.hash);
|
|
309
355
|
if (orig) return { ...orig, score: m.score };
|
|
310
|
-
// Graph-discovered node — hydrate from DB
|
|
356
|
+
// Graph-discovered node — hydrate from DB (defense-in-depth visibility guard;
|
|
357
|
+
// traversal-level pruning is the primary barrier)
|
|
311
358
|
const doc = store.db.prepare(`
|
|
312
359
|
SELECT d.collection, d.path, d.title, d.hash, c.doc as body, d.modified_at
|
|
313
360
|
FROM documents d
|
|
@@ -315,6 +362,7 @@ This is the recommended entry point for ALL memory queries.`,
|
|
|
315
362
|
WHERE d.hash = ? AND d.active = 1 AND d.invalidated_at IS NULL LIMIT 1
|
|
316
363
|
`).get(m.hash) as { collection: string; path: string; title: string; hash: string; body: string | null; modified_at: string } | undefined;
|
|
317
364
|
if (!doc) return null;
|
|
365
|
+
if (excl && excl.includes(doc.collection)) return null;
|
|
318
366
|
return {
|
|
319
367
|
filepath: `clawmem://${doc.collection}/${doc.path}`,
|
|
320
368
|
displayPath: `${doc.collection}/${doc.path}`,
|
|
@@ -341,23 +389,34 @@ This is the recommended entry point for ALL memory queries.`,
|
|
|
341
389
|
score: Math.round(r.compositeScore * 100) / 100,
|
|
342
390
|
snippet: (r.body || "").substring(0, 150), content_type: r.contentType,
|
|
343
391
|
}));
|
|
392
|
+
const causalDegradedFields = causalDegraded ? { degraded: true as const, degradedReason: causalDegraded.reason } : {};
|
|
393
|
+
const degradedNote = causalDegraded ? `\n${degradedGuidanceText([causalDegraded])}` : "";
|
|
344
394
|
return {
|
|
345
|
-
content: [{ type: "text", text: `[routed: causal, intent: ${intent.intent}] ${formatSearchSummary(items.map(i => ({ ...i, file: i.path, compositeScore: i.score, context: null })), query)}` }],
|
|
346
|
-
structuredContent: { mode: effectiveMode, intent, results: items },
|
|
395
|
+
content: [{ type: "text", text: `[routed: causal, intent: ${intent.intent}] ${formatSearchSummary(items.map(i => ({ ...i, file: i.path, compositeScore: i.score, context: null })), query)}${degradedNote}` }],
|
|
396
|
+
structuredContent: { mode: effectiveMode, intent, results: items, ...causalDegradedFields },
|
|
347
397
|
};
|
|
348
398
|
}
|
|
349
399
|
|
|
350
|
-
// --- Complex mode → query decomposition ---
|
|
400
|
+
// --- Complex mode → query decomposition (multi-leg: per-clause degraded aggregation, T6-M1) ---
|
|
351
401
|
if (effectiveMode === "complex") {
|
|
352
402
|
const llm = getDefaultLlamaCpp();
|
|
353
403
|
const clauses = await decomposeQuery(query, llm, store.db);
|
|
354
404
|
const allResults: SearchResult[] = [];
|
|
405
|
+
let clauseIdx = 0;
|
|
355
406
|
for (const clause of clauses.sort((a, b) => a.priority - b.priority)) {
|
|
407
|
+
const clauseExcl = resolveExcludedCollections(includeInternal, clause.collections);
|
|
356
408
|
let results: SearchResult[] = [];
|
|
357
|
-
if (clause.type === 'bm25') results = store.searchFTS(clause.query, 20, undefined, clause.collections);
|
|
358
|
-
else if (clause.type === 'vector') {
|
|
359
|
-
|
|
409
|
+
if (clause.type === 'bm25') results = store.searchFTS(clause.query, 20, undefined, clause.collections, undefined, clauseExcl);
|
|
410
|
+
else if (clause.type === 'vector') {
|
|
411
|
+
try {
|
|
412
|
+
const det = await store.searchVecDetailed(clause.query, DEFAULT_EMBED_MODEL, 20, { collections: clause.collections, excludeCollections: clauseExcl });
|
|
413
|
+
results = det.results;
|
|
414
|
+
if (det.degraded && det.degradedReason) degradedLegs.push({ leg: `complex:clause${clauseIdx}:vector`, reason: det.degradedReason });
|
|
415
|
+
} catch (e) { rethrowIfFatalVectorError(e); /* */ }
|
|
416
|
+
}
|
|
417
|
+
else if (clause.type === 'graph') { results = store.searchFTS(clause.query, 15, undefined, clause.collections, undefined, clauseExcl); }
|
|
360
418
|
allResults.push(...results);
|
|
419
|
+
clauseIdx++;
|
|
361
420
|
}
|
|
362
421
|
const seen = new Set<string>();
|
|
363
422
|
const deduped = allResults.filter(r => { if (seen.has(r.filepath)) return false; seen.add(r.filepath); return true; });
|
|
@@ -368,23 +427,37 @@ This is the recommended entry point for ALL memory queries.`,
|
|
|
368
427
|
score: Math.round(r.compositeScore * 100) / 100,
|
|
369
428
|
snippet: (r.body || "").substring(0, 150), content_type: r.contentType,
|
|
370
429
|
}));
|
|
430
|
+
const degradedNote = degradedLegs.length > 0 ? `\n${degradedGuidanceText(degradedLegs)}` : "";
|
|
371
431
|
return {
|
|
372
|
-
content: [{ type: "text", text: `[routed: complex, ${clauses.length} clauses] ${formatSearchSummary(items.map(i => ({ ...i, file: i.path, compositeScore: i.score, context: null })), query)}` }],
|
|
373
|
-
structuredContent: { mode: effectiveMode, clauses: clauses.length, results: items },
|
|
432
|
+
content: [{ type: "text", text: `[routed: complex, ${clauses.length} clauses] ${formatSearchSummary(items.map(i => ({ ...i, file: i.path, compositeScore: i.score, context: null })), query)}${degradedNote}` }],
|
|
433
|
+
structuredContent: { mode: effectiveMode, clauses: clauses.length, results: items, ...(degradedLegs.length > 0 ? { degraded: true, degradedLegs } : {}) },
|
|
374
434
|
};
|
|
375
435
|
}
|
|
376
436
|
|
|
377
437
|
// --- Keyword / Semantic / Discovery / Hybrid modes ---
|
|
378
438
|
let results: SearchResult[] = [];
|
|
439
|
+
let singleLegDegraded: DegradedLeg | undefined;
|
|
440
|
+
// semantic/discovery: the raw regime applies ONLY to results the vector leg actually
|
|
441
|
+
// served — the FTS fallback's scores are not cosine, so it keeps composite scoring.
|
|
442
|
+
let vectorLegServed = false;
|
|
379
443
|
if (effectiveMode === "keyword") {
|
|
380
|
-
results = store.searchFTS(query, lim);
|
|
444
|
+
results = store.searchFTS(query, lim, undefined, undefined, undefined, excl);
|
|
381
445
|
} else if (effectiveMode === "semantic" || effectiveMode === "discovery") {
|
|
382
|
-
try {
|
|
446
|
+
try {
|
|
447
|
+
const det = await store.searchVecDetailed(query, DEFAULT_EMBED_MODEL, lim, { excludeCollections: excl });
|
|
448
|
+
results = det.results;
|
|
449
|
+
vectorLegServed = true;
|
|
450
|
+
if (det.degraded && det.degradedReason) singleLegDegraded = { leg: `${effectiveMode}:vector`, reason: det.degradedReason };
|
|
451
|
+
} catch (e) { rethrowIfFatalVectorError(e); results = store.searchFTS(query, lim, undefined, undefined, undefined, excl); }
|
|
383
452
|
} else {
|
|
384
453
|
// Hybrid: BM25 + vector + RRF
|
|
385
|
-
const bm25 = store.searchFTS(query, 30);
|
|
454
|
+
const bm25 = store.searchFTS(query, 30, undefined, undefined, undefined, excl);
|
|
386
455
|
let vec: SearchResult[] = [];
|
|
387
|
-
try {
|
|
456
|
+
try {
|
|
457
|
+
const det = await store.searchVecDetailed(query, DEFAULT_EMBED_MODEL, 30, { excludeCollections: excl });
|
|
458
|
+
vec = det.results;
|
|
459
|
+
if (det.degraded && det.degradedReason) singleLegDegraded = { leg: "hybrid:vector", reason: det.degradedReason };
|
|
460
|
+
} catch (e) { rethrowIfFatalVectorError(e); /* */ }
|
|
388
461
|
if (vec.length > 0) {
|
|
389
462
|
const fusedRanked = reciprocalRankFusion([bm25.map(toRanked), vec.map(toRanked)], [1.0, 1.0]);
|
|
390
463
|
const allSearch = [...bm25, ...vec];
|
|
@@ -398,7 +471,19 @@ This is the recommended entry point for ALL memory queries.`,
|
|
|
398
471
|
}
|
|
399
472
|
|
|
400
473
|
const enriched = enrichResults(store, results, query);
|
|
401
|
-
|
|
474
|
+
// v0.22.0 two-regime scoring (VSEARCH-RAW-PRIMARY-DESIGN.md R1/R4): vector-served
|
|
475
|
+
// semantic/discovery results on non-recency queries rank by raw cosine (metadata
|
|
476
|
+
// breaks exact ties only). Keyword/hybrid modes and the FTS fallback keep composite —
|
|
477
|
+
// their scores are not cosine, so the raw contract cannot hold there.
|
|
478
|
+
const rawRegime = vectorLegServed
|
|
479
|
+
&& (effectiveMode === "semantic" || effectiveMode === "discovery")
|
|
480
|
+
&& selectScoringRegime(query) === "raw";
|
|
481
|
+
const retrieveScoreBasis = rawRegime ? VECTOR_SCORE_BASIS : COMPOSITE_SCORE_BASIS;
|
|
482
|
+
const scored = rawRegime
|
|
483
|
+
? rankRawPrimary(enriched, query).slice(0, lim)
|
|
484
|
+
: applyCompositeScoring(enriched, query).slice(0, lim);
|
|
485
|
+
const modeDegraded = singleLegDegraded ? { degraded: true as const, degradedReason: singleLegDegraded.reason } : {};
|
|
486
|
+
const modeDegradedNote = singleLegDegraded ? `\n${degradedGuidanceText([singleLegDegraded])}` : "";
|
|
402
487
|
if (compact) {
|
|
403
488
|
const items = scored.map(r => ({
|
|
404
489
|
docid: `#${r.docid}`, path: r.displayPath, title: r.title,
|
|
@@ -406,8 +491,8 @@ This is the recommended entry point for ALL memory queries.`,
|
|
|
406
491
|
snippet: (r.body || "").substring(0, 150), content_type: r.contentType,
|
|
407
492
|
}));
|
|
408
493
|
return {
|
|
409
|
-
content: [{ type: "text", text: `[routed: ${effectiveMode}] ${formatSearchSummary(items.map(i => ({ ...i, file: i.path, compositeScore: i.score, context: null })), query)}` }],
|
|
410
|
-
structuredContent: { mode: effectiveMode, results: items },
|
|
494
|
+
content: [{ type: "text", text: `[routed: ${effectiveMode}] ${formatSearchSummary(items.map(i => ({ ...i, file: i.path, compositeScore: i.score, context: null })), query)}${modeDegradedNote}` }],
|
|
495
|
+
structuredContent: { mode: effectiveMode, results: items, scoreBasis: retrieveScoreBasis, ...modeDegraded },
|
|
411
496
|
};
|
|
412
497
|
}
|
|
413
498
|
const items: SearchResultItem[] = scored.map(r => {
|
|
@@ -420,8 +505,8 @@ This is the recommended entry point for ALL memory queries.`,
|
|
|
420
505
|
};
|
|
421
506
|
});
|
|
422
507
|
return {
|
|
423
|
-
content: [{ type: "text", text: `[routed: ${effectiveMode}] ${formatSearchSummary(items, query)}` }],
|
|
424
|
-
structuredContent: { mode: effectiveMode, results: items },
|
|
508
|
+
content: [{ type: "text", text: `[routed: ${effectiveMode}] ${formatSearchSummary(items, query)}${modeDegradedNote}` }],
|
|
509
|
+
structuredContent: { mode: effectiveMode, results: items, scoreBasis: retrieveScoreBasis, ...modeDegraded },
|
|
425
510
|
};
|
|
426
511
|
}
|
|
427
512
|
);
|
|
@@ -495,15 +580,17 @@ This is the recommended entry point for ALL memory queries.`,
|
|
|
495
580
|
minScore: z.number().optional().default(0),
|
|
496
581
|
collection: z.string().optional().describe("Filter to collection (single name or comma-separated)"),
|
|
497
582
|
compact: z.boolean().optional().default(false).describe("Return compact results (id, path, title, score, snippet) instead of full content"),
|
|
583
|
+
includeInternal: z.boolean().optional().default(false).describe("Include system-internal _clawmem docs (observations/deductions) — excluded by default"),
|
|
498
584
|
vault: z.string().optional().describe("Named vault (omit for default vault)"),
|
|
499
585
|
},
|
|
500
586
|
},
|
|
501
|
-
async ({ query, limit, minScore, collection, compact, vault }) => {
|
|
587
|
+
async ({ query, limit, minScore, collection, compact, includeInternal, vault }) => {
|
|
502
588
|
const store = getStore(vault);
|
|
503
589
|
const collections = collection
|
|
504
590
|
? collection.split(",").map(c => c.trim()).filter(Boolean)
|
|
505
591
|
: undefined;
|
|
506
|
-
const
|
|
592
|
+
const excl = resolveExcludedCollections(includeInternal, collections);
|
|
593
|
+
const results = store.searchFTS(query, limit || 10, undefined, collections, undefined, excl);
|
|
507
594
|
|
|
508
595
|
const coFn = (path: string) => store.getCoActivated(path);
|
|
509
596
|
const enriched = enrichResults(store, results, query);
|
|
@@ -553,13 +640,14 @@ This is the recommended entry point for ALL memory queries.`,
|
|
|
553
640
|
inputSchema: {
|
|
554
641
|
query: z.string().describe("Natural language query"),
|
|
555
642
|
limit: z.number().optional().default(10),
|
|
556
|
-
minScore: z.number().optional().default
|
|
643
|
+
minScore: z.number().optional().describe("Score floor. Non-recency queries filter the RAW cosine score (default: no filter; explicit 0 honored; cosine values are embedding-model-specific). Recency-intent queries keep the composite-scale default 0.3."),
|
|
557
644
|
collection: z.string().optional().describe("Filter to collection (single name or comma-separated)"),
|
|
558
645
|
compact: z.boolean().optional().default(false).describe("Return compact results (id, path, title, score, snippet) instead of full content"),
|
|
646
|
+
includeInternal: z.boolean().optional().default(false).describe("Include system-internal _clawmem docs (observations/deductions) — excluded by default"),
|
|
559
647
|
vault: z.string().optional().describe("Named vault (omit for default vault)"),
|
|
560
648
|
},
|
|
561
649
|
},
|
|
562
|
-
async ({ query, limit, minScore, collection, compact, vault }) => {
|
|
650
|
+
async ({ query, limit, minScore, collection, compact, includeInternal, vault }) => {
|
|
563
651
|
const store = getStore(vault);
|
|
564
652
|
const tableExists = store.db.prepare(`SELECT name FROM sqlite_master WHERE type='table' AND name='vectors_vec'`).get();
|
|
565
653
|
if (!tableExists) {
|
|
@@ -569,12 +657,25 @@ This is the recommended entry point for ALL memory queries.`,
|
|
|
569
657
|
const collections = collection
|
|
570
658
|
? collection.split(",").map(c => c.trim()).filter(Boolean)
|
|
571
659
|
: undefined;
|
|
572
|
-
const
|
|
660
|
+
const excl = resolveExcludedCollections(includeInternal, collections);
|
|
661
|
+
const det = await store.searchVecDetailed(query, DEFAULT_EMBED_MODEL, limit || 10, { collections, excludeCollections: excl });
|
|
662
|
+
const results = det.results;
|
|
663
|
+
const vsDegraded = det.degraded && det.degradedReason ? { degraded: true as const, degradedReason: det.degradedReason } : {};
|
|
664
|
+
const vsDegradedNote = det.degraded && det.degradedReason ? `\n${degradedGuidanceText([{ leg: "vsearch", reason: det.degradedReason }])}` : "";
|
|
573
665
|
|
|
574
666
|
const coFn = (path: string) => store.getCoActivated(path);
|
|
575
667
|
const enriched = enrichResults(store, results, query);
|
|
576
|
-
|
|
577
|
-
|
|
668
|
+
// v0.22.0 two-regime scoring (VSEARCH-RAW-PRIMARY-DESIGN.md R1–R4): non-recency
|
|
669
|
+
// queries rank by raw cosine — metadata (incl. pin) breaks exact score ties only,
|
|
670
|
+
// and minScore (if given) filters the raw score with NO default floor. Recency-intent
|
|
671
|
+
// queries keep the pre-v0.22.0 composite behavior including its 0.3 default.
|
|
672
|
+
const regime = selectScoringRegime(query);
|
|
673
|
+
const vsScoreBasis = regime === "raw" ? VECTOR_SCORE_BASIS : COMPOSITE_SCORE_BASIS;
|
|
674
|
+
const scored = regime === "raw"
|
|
675
|
+
? rankRawPrimary(enriched, query, coFn).filter(r => minScore === undefined || r.compositeScore >= minScore)
|
|
676
|
+
// Recency branch preserves v0.21 semantics EXACTLY, including `||`: an explicit
|
|
677
|
+
// minScore of 0 still applies the 0.3 composite floor (R4 unchanged-recency contract).
|
|
678
|
+
: applyCompositeScoring(enriched, query, coFn).filter(r => r.compositeScore >= (minScore || 0.3));
|
|
578
679
|
|
|
579
680
|
if (compact) {
|
|
580
681
|
const items = scored.map(r => ({
|
|
@@ -583,7 +684,7 @@ This is the recommended entry point for ALL memory queries.`,
|
|
|
583
684
|
snippet: (r.body || "").substring(0, 150), content_type: r.contentType, modified_at: r.modifiedAt,
|
|
584
685
|
fragment: r.fragmentType ? { type: r.fragmentType, label: r.fragmentLabel } : undefined,
|
|
585
686
|
}));
|
|
586
|
-
return { content: [{ type: "text", text: formatSearchSummary(items.map(i => ({ ...i, file: i.path, compositeScore: i.score, context: null })), query) }], structuredContent: { results: items } };
|
|
687
|
+
return { content: [{ type: "text", text: `${formatSearchSummary(items.map(i => ({ ...i, file: i.path, compositeScore: i.score, context: null })), query)}${vsDegradedNote}` }], structuredContent: { results: items, scoreBasis: vsScoreBasis, ...vsDegraded } };
|
|
587
688
|
}
|
|
588
689
|
|
|
589
690
|
const items: SearchResultItem[] = scored.map(r => {
|
|
@@ -601,8 +702,8 @@ This is the recommended entry point for ALL memory queries.`,
|
|
|
601
702
|
});
|
|
602
703
|
|
|
603
704
|
return {
|
|
604
|
-
content: [{ type: "text", text: formatSearchSummary(items, query) }],
|
|
605
|
-
structuredContent: { results: items },
|
|
705
|
+
content: [{ type: "text", text: `${formatSearchSummary(items, query)}${vsDegradedNote}` }],
|
|
706
|
+
structuredContent: { results: items, scoreBasis: vsScoreBasis, ...vsDegraded },
|
|
606
707
|
};
|
|
607
708
|
}
|
|
608
709
|
);
|
|
@@ -625,14 +726,16 @@ This is the recommended entry point for ALL memory queries.`,
|
|
|
625
726
|
diverse: z.boolean().optional().default(true).describe("Apply MMR diversity filter to reduce near-duplicate results"),
|
|
626
727
|
intent: z.string().optional().describe("Domain intent hint for disambiguation — steers expansion, reranking, chunk selection, and snippet extraction"),
|
|
627
728
|
candidateLimit: z.number().optional().default(30).describe("Max candidates reaching the reranker (default 30)"),
|
|
729
|
+
includeInternal: z.boolean().optional().default(false).describe("Include system-internal _clawmem docs (observations/deductions) — excluded by default"),
|
|
628
730
|
vault: z.string().optional().describe("Named vault (omit for default vault)"),
|
|
629
731
|
},
|
|
630
732
|
},
|
|
631
|
-
async ({ query, limit, minScore, collection, compact, diverse, intent, candidateLimit, vault }) => {
|
|
733
|
+
async ({ query, limit, minScore, collection, compact, diverse, intent, candidateLimit, includeInternal, vault }) => {
|
|
632
734
|
const store = getStore(vault);
|
|
633
735
|
const candLimit = candidateLimit || 30;
|
|
634
736
|
const rankedLists: RankedResult[][] = [];
|
|
635
737
|
const docidMap = new Map<string, string>();
|
|
738
|
+
const degradedLegs: DegradedLeg[] = [];
|
|
636
739
|
const hasVectors = !!store.db.prepare(`SELECT name FROM sqlite_master WHERE type='table' AND name='vectors_vec'`).get();
|
|
637
740
|
|
|
638
741
|
// Step 0: Temporal constraint extraction (pure regex, ~0ms)
|
|
@@ -642,7 +745,8 @@ This is the recommended entry point for ALL memory queries.`,
|
|
|
642
745
|
const collections = collection
|
|
643
746
|
? collection.split(",").map(c => c.trim()).filter(Boolean)
|
|
644
747
|
: undefined;
|
|
645
|
-
const
|
|
748
|
+
const excl = resolveExcludedCollections(includeInternal, collections);
|
|
749
|
+
const initialFts = store.searchFTS(query, 20, undefined, collections, dateRange, excl);
|
|
646
750
|
const topScore = initialFts.length > 0 ? Math.abs(initialFts[0]!.score) : 0;
|
|
647
751
|
const secondScore = initialFts.length > 1 ? Math.abs(initialFts[1]!.score) : 0;
|
|
648
752
|
// When intent is provided, disable strong-signal bypass — the obvious BM25
|
|
@@ -663,30 +767,34 @@ This is the recommended entry point for ALL memory queries.`,
|
|
|
663
767
|
rankedLists.push(initialFts.map(r => ({ file: r.filepath, displayPath: r.displayPath, title: r.title, body: r.body || "", score: r.score })));
|
|
664
768
|
}
|
|
665
769
|
if (hasVectors) {
|
|
666
|
-
const
|
|
667
|
-
if (
|
|
668
|
-
|
|
669
|
-
|
|
770
|
+
const det = await store.searchVecDetailed(query, DEFAULT_EMBED_MODEL, 20, { collections, dateRange, excludeCollections: excl });
|
|
771
|
+
if (det.degraded && det.degradedReason) degradedLegs.push({ leg: "vector:original", reason: det.degradedReason });
|
|
772
|
+
if (det.results.length > 0) {
|
|
773
|
+
for (const r of det.results) docidMap.set(r.filepath, r.docid);
|
|
774
|
+
rankedLists.push(det.results.map(r => ({ file: r.filepath, displayPath: r.displayPath, title: r.title, body: r.body || "", score: r.score })));
|
|
670
775
|
}
|
|
671
776
|
}
|
|
672
777
|
// Lists contributed by the original query — these get the 2× RRF weight.
|
|
673
778
|
const numOriginalLists = rankedLists.length;
|
|
674
779
|
|
|
675
780
|
// Typed expansions — route by type: lex → FTS, vec/hyde → vector.
|
|
781
|
+
let expansionIdx = 0;
|
|
676
782
|
for (const eq of expanded) {
|
|
677
783
|
if (eq.type === 'lex') {
|
|
678
|
-
const ftsResults = store.searchFTS(eq.query, 20, undefined, collections, dateRange);
|
|
784
|
+
const ftsResults = store.searchFTS(eq.query, 20, undefined, collections, dateRange, excl);
|
|
679
785
|
if (ftsResults.length > 0) {
|
|
680
786
|
for (const r of ftsResults) docidMap.set(r.filepath, r.docid);
|
|
681
787
|
rankedLists.push(ftsResults.map(r => ({ file: r.filepath, displayPath: r.displayPath, title: r.title, body: r.body || "", score: r.score })));
|
|
682
788
|
}
|
|
683
789
|
} else if (hasVectors) {
|
|
684
|
-
const
|
|
685
|
-
if (
|
|
686
|
-
|
|
687
|
-
|
|
790
|
+
const det = await store.searchVecDetailed(eq.query, DEFAULT_EMBED_MODEL, 20, { collections, dateRange, excludeCollections: excl });
|
|
791
|
+
if (det.degraded && det.degradedReason) degradedLegs.push({ leg: `vector:expansion${expansionIdx}`, reason: det.degradedReason });
|
|
792
|
+
if (det.results.length > 0) {
|
|
793
|
+
for (const r of det.results) docidMap.set(r.filepath, r.docid);
|
|
794
|
+
rankedLists.push(det.results.map(r => ({ file: r.filepath, displayPath: r.displayPath, title: r.title, body: r.body || "", score: r.score })));
|
|
688
795
|
}
|
|
689
796
|
}
|
|
797
|
+
expansionIdx++;
|
|
690
798
|
}
|
|
691
799
|
|
|
692
800
|
// Step 2b: Temporal proximity channel (if dateRange detected)
|
|
@@ -694,14 +802,17 @@ This is the recommended entry point for ALL memory queries.`,
|
|
|
694
802
|
if (dateRange) {
|
|
695
803
|
const centerMs = (new Date(dateRange.start).getTime() + new Date(dateRange.end).getTime()) / 2;
|
|
696
804
|
const rangeMs = Math.max(new Date(dateRange.end).getTime() - new Date(dateRange.start).getTime(), 86400000);
|
|
805
|
+
const temporalExclSql = excl && excl.length > 0
|
|
806
|
+
? ` AND d.collection NOT IN (${excl.map(() => '?').join(',')})`
|
|
807
|
+
: "";
|
|
697
808
|
const temporalDocs = store.db.prepare(`
|
|
698
809
|
SELECT 'clawmem://' || d.collection || '/' || d.path as filepath,
|
|
699
810
|
d.collection || '/' || d.path as displayPath,
|
|
700
811
|
d.title, d.modified_at
|
|
701
812
|
FROM documents d
|
|
702
|
-
WHERE d.active = 1 AND d.invalidated_at IS NULL AND d.modified_at >= ? AND d.modified_at <=
|
|
813
|
+
WHERE d.active = 1 AND d.invalidated_at IS NULL AND d.modified_at >= ? AND d.modified_at <= ?${temporalExclSql}
|
|
703
814
|
ORDER BY d.modified_at DESC LIMIT 30
|
|
704
|
-
`).all(dateRange.start, dateRange.end) as { filepath: string; displayPath: string; title: string; modified_at: string }[];
|
|
815
|
+
`).all(dateRange.start, dateRange.end, ...(excl ?? [])) as { filepath: string; displayPath: string; title: string; modified_at: string }[];
|
|
705
816
|
|
|
706
817
|
if (temporalDocs.length > 0) {
|
|
707
818
|
const temporalRanked: RankedResult[] = temporalDocs.map(d => {
|
|
@@ -732,6 +843,7 @@ This is the recommended entry point for ALL memory queries.`,
|
|
|
732
843
|
WHERE d.id = ? AND d.active = 1 AND d.invalidated_at IS NULL LIMIT 1
|
|
733
844
|
`).get(en.docId) as { collection: string; path: string; title: string; body: string | null } | undefined;
|
|
734
845
|
if (!doc) return null;
|
|
846
|
+
if (excl && excl.includes(doc.collection)) return null;
|
|
735
847
|
return {
|
|
736
848
|
file: `clawmem://${doc.collection}/${doc.path}`,
|
|
737
849
|
displayPath: `${doc.collection}/${doc.path}`,
|
|
@@ -836,6 +948,10 @@ This is the recommended entry point for ALL memory queries.`,
|
|
|
836
948
|
if (diverse !== false) scored = applyMMRDiversity(scored);
|
|
837
949
|
scored = scored.slice(0, limit || 10);
|
|
838
950
|
|
|
951
|
+
// Multi-leg degraded aggregation (T5-M1): route marker = any(leg), per-leg reasons retained.
|
|
952
|
+
const queryDegraded = degradedLegs.length > 0 ? { degraded: true as const, degradedLegs } : {};
|
|
953
|
+
const queryDegradedNote = degradedLegs.length > 0 ? `\n${degradedGuidanceText(degradedLegs)}` : "";
|
|
954
|
+
|
|
839
955
|
if (compact) {
|
|
840
956
|
const items = scored.map(r => ({
|
|
841
957
|
docid: `#${docidMap.get(r.filepath) || r.docid}`, path: r.displayPath, title: r.title,
|
|
@@ -843,7 +959,7 @@ This is the recommended entry point for ALL memory queries.`,
|
|
|
843
959
|
snippet: (r.body || "").substring(0, 150), content_type: r.contentType, modified_at: r.modifiedAt,
|
|
844
960
|
fragment: r.fragmentType ? { type: r.fragmentType, label: r.fragmentLabel } : undefined,
|
|
845
961
|
}));
|
|
846
|
-
return { content: [{ type: "text", text: formatSearchSummary(items.map(i => ({ ...i, file: i.path, compositeScore: i.score, context: null })), query) }], structuredContent: { results: items } };
|
|
962
|
+
return { content: [{ type: "text", text: `${formatSearchSummary(items.map(i => ({ ...i, file: i.path, compositeScore: i.score, context: null })), query)}${queryDegradedNote}` }], structuredContent: { results: items, ...queryDegraded } };
|
|
847
963
|
}
|
|
848
964
|
|
|
849
965
|
const items: SearchResultItem[] = scored.map(r => {
|
|
@@ -861,8 +977,8 @@ This is the recommended entry point for ALL memory queries.`,
|
|
|
861
977
|
});
|
|
862
978
|
|
|
863
979
|
return {
|
|
864
|
-
content: [{ type: "text", text: formatSearchSummary(items, query) }],
|
|
865
|
-
structuredContent: { results: items },
|
|
980
|
+
content: [{ type: "text", text: `${formatSearchSummary(items, query)}${queryDegradedNote}` }],
|
|
981
|
+
structuredContent: { results: items, ...queryDegraded },
|
|
866
982
|
};
|
|
867
983
|
}
|
|
868
984
|
);
|
|
@@ -1272,10 +1388,11 @@ This is the recommended entry point for ALL memory queries.`,
|
|
|
1272
1388
|
inputSchema: {
|
|
1273
1389
|
file: z.string().describe("Path of reference document"),
|
|
1274
1390
|
limit: z.number().optional().default(5),
|
|
1391
|
+
includeInternal: z.boolean().optional().default(false).describe("Include system-internal _clawmem docs — excluded by default (auto-included when the REFERENCE doc is itself internal)"),
|
|
1275
1392
|
vault: z.string().optional().describe("Named vault (omit for default vault)"),
|
|
1276
1393
|
},
|
|
1277
1394
|
},
|
|
1278
|
-
async ({ file, limit, vault }) => {
|
|
1395
|
+
async ({ file, limit, includeInternal, vault }) => {
|
|
1279
1396
|
const store = getStore(vault);
|
|
1280
1397
|
const tableExists = store.db.prepare(`SELECT name FROM sqlite_master WHERE type='table' AND name='vectors_vec'`).get();
|
|
1281
1398
|
if (!tableExists) {
|
|
@@ -1291,14 +1408,21 @@ This is the recommended entry point for ALL memory queries.`,
|
|
|
1291
1408
|
const body = store.getDocumentBody(result) ?? "";
|
|
1292
1409
|
const title = result.title || file;
|
|
1293
1410
|
|
|
1411
|
+
// Internal-reference auto-exception (T2-M7): exploring FROM an internal doc is an
|
|
1412
|
+
// explicit ask for the internal space — include internal neighbors.
|
|
1413
|
+
const refIsInternal = INTERNAL_COLLECTIONS.includes(result.collectionName);
|
|
1414
|
+
const excl = resolveExcludedCollections(includeInternal || refIsInternal);
|
|
1415
|
+
|
|
1294
1416
|
// Use the document's content as the search query
|
|
1295
1417
|
const queryText = `${title}\n${body.slice(0, 1000)}`;
|
|
1296
|
-
const
|
|
1418
|
+
const det = await store.searchVecDetailed(queryText, DEFAULT_EMBED_MODEL, (limit || 5) + 1, { excludeCollections: excl });
|
|
1297
1419
|
|
|
1298
1420
|
// Filter out the reference document itself
|
|
1299
|
-
const similar =
|
|
1421
|
+
const similar = det.results
|
|
1300
1422
|
.filter(r => r.filepath !== result.filepath)
|
|
1301
1423
|
.slice(0, limit || 5);
|
|
1424
|
+
const fsDegraded = det.degraded && det.degradedReason ? { degraded: true as const, degradedReason: det.degradedReason } : {};
|
|
1425
|
+
const fsDegradedNote = det.degraded && det.degradedReason ? `\n${degradedGuidanceText([{ leg: "find_similar", reason: det.degradedReason }])}` : "";
|
|
1302
1426
|
|
|
1303
1427
|
const items: SearchResultItem[] = similar.map(r => {
|
|
1304
1428
|
const { line, snippet } = extractSnippet(r.body || "", title, 200);
|
|
@@ -1313,8 +1437,8 @@ This is the recommended entry point for ALL memory queries.`,
|
|
|
1313
1437
|
});
|
|
1314
1438
|
|
|
1315
1439
|
return {
|
|
1316
|
-
content: [{ type: "text", text: `${items.length} similar to "${title}":\n${items.map(i => ` ${i.file} (${Math.round(i.score * 100)}%)`).join('\n')}` }],
|
|
1317
|
-
structuredContent: { reference: file, results: items },
|
|
1440
|
+
content: [{ type: "text", text: `${items.length} similar to "${title}":\n${items.map(i => ` ${i.file} (${Math.round(i.score * 100)}%)`).join('\n')}${fsDegradedNote}` }],
|
|
1441
|
+
structuredContent: { reference: file, results: items, ...fsDegraded },
|
|
1318
1442
|
};
|
|
1319
1443
|
}
|
|
1320
1444
|
);
|
|
@@ -1763,10 +1887,11 @@ This is the recommended entry point for ALL memory queries.`,
|
|
|
1763
1887
|
query: z.string().describe("Complex or multi-topic query"),
|
|
1764
1888
|
limit: z.number().optional().default(10),
|
|
1765
1889
|
compact: z.boolean().optional().default(true).describe("Return compact results"),
|
|
1890
|
+
includeInternal: z.boolean().optional().default(false).describe("Include system-internal _clawmem docs (observations/deductions) — excluded by default"),
|
|
1766
1891
|
vault: z.string().optional().describe("Named vault (omit for default vault)"),
|
|
1767
1892
|
},
|
|
1768
1893
|
},
|
|
1769
|
-
async ({ query, limit, compact, vault }) => {
|
|
1894
|
+
async ({ query, limit, compact, includeInternal, vault }) => {
|
|
1770
1895
|
const store = getStore(vault);
|
|
1771
1896
|
const llm = getDefaultLlamaCpp();
|
|
1772
1897
|
|
|
@@ -1777,18 +1902,25 @@ This is the recommended entry point for ALL memory queries.`,
|
|
|
1777
1902
|
const sortedClauses = [...clauses].sort((a, b) => a.priority - b.priority);
|
|
1778
1903
|
const allResults: SearchResult[] = [];
|
|
1779
1904
|
const clauseDetails: { type: string; query: string; priority: number; resultCount: number }[] = [];
|
|
1905
|
+
const degradedLegs: DegradedLeg[] = [];
|
|
1780
1906
|
|
|
1907
|
+
let clauseIdx = 0;
|
|
1781
1908
|
for (const clause of sortedClauses) {
|
|
1909
|
+
const clauseExcl = resolveExcludedCollections(includeInternal, clause.collections);
|
|
1782
1910
|
let results: SearchResult[] = [];
|
|
1783
1911
|
if (clause.type === 'bm25') {
|
|
1784
|
-
results = store.searchFTS(clause.query, 20, undefined, clause.collections);
|
|
1912
|
+
results = store.searchFTS(clause.query, 20, undefined, clause.collections, undefined, clauseExcl);
|
|
1785
1913
|
} else if (clause.type === 'vector') {
|
|
1786
|
-
|
|
1914
|
+
const det = await store.searchVecDetailed(clause.query, DEFAULT_EMBED_MODEL, 20, { collections: clause.collections, excludeCollections: clauseExcl });
|
|
1915
|
+
results = det.results;
|
|
1916
|
+
if (det.degraded && det.degradedReason) degradedLegs.push({ leg: `clause${clauseIdx}:vector`, reason: det.degradedReason });
|
|
1787
1917
|
} else if (clause.type === 'graph') {
|
|
1788
1918
|
// Graph clause: run intent_search-style retrieval
|
|
1789
1919
|
const intent = await classifyIntent(clause.query, llm, store.db);
|
|
1790
|
-
const bm25 = store.searchFTS(clause.query, 15, undefined, clause.collections);
|
|
1791
|
-
const
|
|
1920
|
+
const bm25 = store.searchFTS(clause.query, 15, undefined, clause.collections, undefined, clauseExcl);
|
|
1921
|
+
const detGraph = await store.searchVecDetailed(clause.query, DEFAULT_EMBED_MODEL, 15, { collections: clause.collections, excludeCollections: clauseExcl });
|
|
1922
|
+
const vec = detGraph.results;
|
|
1923
|
+
if (detGraph.degraded && detGraph.degradedReason) degradedLegs.push({ leg: `clause${clauseIdx}:graph:vector`, reason: detGraph.degradedReason });
|
|
1792
1924
|
const fused = reciprocalRankFusion([bm25.map(toRanked), vec.map(toRanked)], [1.0, 1.0]);
|
|
1793
1925
|
const searchMap = new Map([...bm25, ...vec].map(r => [r.filepath, r]));
|
|
1794
1926
|
results = fused
|
|
@@ -1801,13 +1933,15 @@ This is the recommended entry point for ALL memory queries.`,
|
|
|
1801
1933
|
if (anchorEmb) {
|
|
1802
1934
|
const traversed = adaptiveTraversal(store.db, results.slice(0, 5).map(r => ({ hash: r.hash, score: r.score })), {
|
|
1803
1935
|
maxDepth: 2, beamWidth: 3, budget: 15, intent: intent.intent, queryEmbedding: anchorEmb.embedding,
|
|
1936
|
+
// Visibility policy threaded INTO traversal (T6-H1): parity with memory_retrieve causal
|
|
1937
|
+
excludeCollections: clauseExcl,
|
|
1804
1938
|
});
|
|
1805
1939
|
const merged = mergeTraversalResults(store.db, results.map(r => ({ hash: r.hash, score: r.score })), traversed);
|
|
1806
1940
|
const expandedMap = new Map(results.map(r => [r.hash, r]));
|
|
1807
1941
|
results = merged.map(m => {
|
|
1808
1942
|
const existing = expandedMap.get(m.hash);
|
|
1809
1943
|
if (existing) return { ...existing, score: m.score };
|
|
1810
|
-
// Graph-discovered node — hydrate from DB
|
|
1944
|
+
// Graph-discovered node — hydrate from DB (defense-in-depth visibility guard)
|
|
1811
1945
|
const doc = store.db.prepare(`
|
|
1812
1946
|
SELECT d.collection, d.path, d.title, d.hash, c.doc as body, d.modified_at
|
|
1813
1947
|
FROM documents d
|
|
@@ -1815,6 +1949,7 @@ This is the recommended entry point for ALL memory queries.`,
|
|
|
1815
1949
|
WHERE d.hash = ? AND d.active = 1 AND d.invalidated_at IS NULL LIMIT 1
|
|
1816
1950
|
`).get(m.hash) as { collection: string; path: string; title: string; hash: string; body: string | null; modified_at: string } | undefined;
|
|
1817
1951
|
if (!doc) return null;
|
|
1952
|
+
if (clauseExcl && clauseExcl.includes(doc.collection)) return null;
|
|
1818
1953
|
return {
|
|
1819
1954
|
filepath: `clawmem://${doc.collection}/${doc.path}`,
|
|
1820
1955
|
displayPath: `${doc.collection}/${doc.path}`,
|
|
@@ -1835,6 +1970,7 @@ This is the recommended entry point for ALL memory queries.`,
|
|
|
1835
1970
|
}
|
|
1836
1971
|
clauseDetails.push({ type: clause.type, query: clause.query, priority: clause.priority, resultCount: results.length });
|
|
1837
1972
|
allResults.push(...results);
|
|
1973
|
+
clauseIdx++;
|
|
1838
1974
|
}
|
|
1839
1975
|
|
|
1840
1976
|
// Deduplicate by filepath, keeping highest score
|
|
@@ -1863,6 +1999,8 @@ This is the recommended entry point for ALL memory queries.`,
|
|
|
1863
1999
|
const scored = applyCompositeScoring(enriched, query, coFn).slice(0, limit || 10);
|
|
1864
2000
|
|
|
1865
2001
|
const planSummary = clauseDetails.map(c => ` ${c.type}(p${c.priority}): "${c.query}" → ${c.resultCount} results`).join("\n");
|
|
2002
|
+
const planDegraded = degradedLegs.length > 0 ? { degraded: true as const, degradedLegs } : {};
|
|
2003
|
+
const planDegradedNote = degradedLegs.length > 0 ? `\n${degradedGuidanceText(degradedLegs)}` : "";
|
|
1866
2004
|
|
|
1867
2005
|
if (compact) {
|
|
1868
2006
|
const items = scored.map(r => ({
|
|
@@ -1871,8 +2009,8 @@ This is the recommended entry point for ALL memory queries.`,
|
|
|
1871
2009
|
snippet: (r.body || "").substring(0, 150), content_type: r.contentType, modified_at: r.modifiedAt,
|
|
1872
2010
|
}));
|
|
1873
2011
|
return {
|
|
1874
|
-
content: [{ type: "text", text: `Query Plan (${sortedClauses.length} clauses):\n${planSummary}\n\n${formatSearchSummary(items.map(i => ({ ...i, file: i.path, compositeScore: i.score, context: null })), query)}` }],
|
|
1875
|
-
structuredContent: { plan: clauseDetails, results: items },
|
|
2012
|
+
content: [{ type: "text", text: `Query Plan (${sortedClauses.length} clauses):\n${planSummary}\n\n${formatSearchSummary(items.map(i => ({ ...i, file: i.path, compositeScore: i.score, context: null })), query)}${planDegradedNote}` }],
|
|
2013
|
+
structuredContent: { plan: clauseDetails, results: items, ...planDegraded },
|
|
1876
2014
|
};
|
|
1877
2015
|
}
|
|
1878
2016
|
|
|
@@ -1881,8 +2019,8 @@ This is the recommended entry point for ALL memory queries.`,
|
|
|
1881
2019
|
compositeScore: r.compositeScore, context: r.context, snippet: r.body?.slice(0, 300) || '', contentType: r.contentType,
|
|
1882
2020
|
}));
|
|
1883
2021
|
return {
|
|
1884
|
-
content: [{ type: "text", text: `Query Plan (${sortedClauses.length} clauses):\n${planSummary}\n\n${formatSearchSummary(items, query)}` }],
|
|
1885
|
-
structuredContent: { plan: clauseDetails, results: items },
|
|
2022
|
+
content: [{ type: "text", text: `Query Plan (${sortedClauses.length} clauses):\n${planSummary}\n\n${formatSearchSummary(items, query)}${planDegradedNote}` }],
|
|
2023
|
+
structuredContent: { plan: clauseDetails, results: items, ...planDegraded },
|
|
1886
2024
|
};
|
|
1887
2025
|
}
|
|
1888
2026
|
);
|
|
@@ -2650,6 +2788,12 @@ This is the recommended entry point for ALL memory queries.`,
|
|
|
2650
2788
|
}
|
|
2651
2789
|
);
|
|
2652
2790
|
|
|
2791
|
+
return { server, store, closeAllStores };
|
|
2792
|
+
}
|
|
2793
|
+
|
|
2794
|
+
export async function startMcpServer(): Promise<void> {
|
|
2795
|
+
const { server, store, closeAllStores } = buildMcpServer();
|
|
2796
|
+
|
|
2653
2797
|
// ---------------------------------------------------------------------------
|
|
2654
2798
|
// Connect
|
|
2655
2799
|
// ---------------------------------------------------------------------------
|
package/src/memory.ts
CHANGED
|
@@ -199,6 +199,7 @@ export const QUERY_WEIGHTS: CompositeWeights = { search: 0.7, recency: 0.15, con
|
|
|
199
199
|
|
|
200
200
|
const RECENCY_PATTERNS = [
|
|
201
201
|
/\brecent(ly)?\b/i,
|
|
202
|
+
/\blatest\b/i,
|
|
202
203
|
/\blast\s+(session|time|week|month|few\s+days)\b/i,
|
|
203
204
|
/\bleft\s+off\b/i,
|
|
204
205
|
/\bwhere\s+(was|were)\s+(we|i)\b/i,
|