clawmem 0.20.2 → 0.21.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/README.md +3 -0
- package/SKILL.md +1 -1
- package/docs/concepts/composite-scoring.md +3 -1
- package/docs/guides/inference-services.md +11 -2
- package/docs/guides/upgrading.md +21 -3
- 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 +10 -0
- 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 +22 -1
- package/src/graph-traversal.ts +32 -4
- package/src/mcp.ts +190 -64
- package/src/memory.ts +1 -0
- package/src/store.ts +366 -32
package/src/mcp.ts
CHANGED
|
@@ -114,6 +114,38 @@ function splitIntoWindows(text: string, windowSize: number, overlap = 200): stri
|
|
|
114
114
|
}
|
|
115
115
|
|
|
116
116
|
/** Classify query into retrieval mode based on signal patterns */
|
|
117
|
+
// =============================================================================
|
|
118
|
+
// Retrieval visibility policy (VSEARCH-TRUST-HARDENING (b))
|
|
119
|
+
// =============================================================================
|
|
120
|
+
|
|
121
|
+
// System-internal collections excluded from MCP retrieval by default. Hook precedent:
|
|
122
|
+
// FILTERED_PATHS in context-surfacing. Opt-ins: the includeInternal param, or an explicit
|
|
123
|
+
// collection filter naming an internal collection.
|
|
124
|
+
const INTERNAL_COLLECTIONS = ["_clawmem"];
|
|
125
|
+
|
|
126
|
+
function resolveExcludedCollections(includeInternal: boolean | undefined, collections?: string[]): string[] | undefined {
|
|
127
|
+
if (includeInternal) return undefined;
|
|
128
|
+
if (collections && collections.some(c => INTERNAL_COLLECTIONS.includes(c))) return undefined;
|
|
129
|
+
return INTERNAL_COLLECTIONS;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
type DegradedLeg = { leg: string; reason: "excluded-dominant" | "cap-truncation" };
|
|
133
|
+
|
|
134
|
+
// Only excluded-dominant carries the includeInternal advice — cap-truncation must stay
|
|
135
|
+
// truthful when the shortfall is dedup-driven (T5-M2).
|
|
136
|
+
function degradedGuidanceText(legs: DegradedLeg[]): string {
|
|
137
|
+
const anyExcludedDominant = legs.some(l => l.reason === "excluded-dominant");
|
|
138
|
+
return anyExcludedDominant
|
|
139
|
+
? "Note: nearest-neighbor region dominated by excluded internal docs — pass includeInternal:true or refine the query."
|
|
140
|
+
: "Note: vector results truncated at the scan cap.";
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Option-B weights knob (design (a)): default-off; flips search/vsearch/memory_retrieve
|
|
144
|
+
// non-recency scoring to the retrieval-tuned weights only when explicitly enabled.
|
|
145
|
+
function mcpDirectWeightsOption(): { weights: typeof QUERY_WEIGHTS } | undefined {
|
|
146
|
+
return loadVaultConfig().retrieval?.mcp_direct_tuned_weights ? { weights: QUERY_WEIGHTS } : undefined;
|
|
147
|
+
}
|
|
148
|
+
|
|
117
149
|
function classifyRetrievalMode(query: string): "keyword" | "semantic" | "causal" | "timeline" | "discovery" | "complex" | "hybrid" {
|
|
118
150
|
const q = query.toLowerCase();
|
|
119
151
|
|
|
@@ -161,7 +193,13 @@ function addLineNumbers(text: string, startLine: number = 1): string {
|
|
|
161
193
|
// MCP Server
|
|
162
194
|
// =============================================================================
|
|
163
195
|
|
|
164
|
-
|
|
196
|
+
/**
|
|
197
|
+
* Build the fully-registered MCP server WITHOUT connecting a transport.
|
|
198
|
+
* Extracted from startMcpServer so tests can drive the real tool handlers over an
|
|
199
|
+
* in-memory transport (route-level regressions for visibility exclusion + degraded
|
|
200
|
+
* markers). Returns the server plus a close() that releases every store handle.
|
|
201
|
+
*/
|
|
202
|
+
export function buildMcpServer(): { server: McpServer; store: Store; closeAllStores: () => void } {
|
|
165
203
|
const store = createStore(undefined, { busyTimeout: 5000 });
|
|
166
204
|
|
|
167
205
|
// Vault store cache: prevents connection churn, closed on shutdown
|
|
@@ -251,13 +289,16 @@ This is the recommended entry point for ALL memory queries.`,
|
|
|
251
289
|
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
290
|
limit: z.number().optional().default(10),
|
|
253
291
|
compact: z.boolean().optional().default(true),
|
|
292
|
+
includeInternal: z.boolean().optional().default(false).describe("Include system-internal _clawmem docs (observations/deductions) — excluded by default"),
|
|
254
293
|
vault: z.string().optional().describe("Named vault (omit for default vault)"),
|
|
255
294
|
},
|
|
256
295
|
},
|
|
257
|
-
async ({ query, mode, limit, compact, vault }) => {
|
|
296
|
+
async ({ query, mode, limit, compact, includeInternal, vault }) => {
|
|
258
297
|
const store = getStore(vault);
|
|
259
298
|
const effectiveMode = mode === "auto" ? classifyRetrievalMode(query) : mode;
|
|
260
299
|
const lim = limit || 10;
|
|
300
|
+
const excl = resolveExcludedCollections(includeInternal);
|
|
301
|
+
const degradedLegs: DegradedLeg[] = [];
|
|
261
302
|
|
|
262
303
|
// --- Timeline mode → session log ---
|
|
263
304
|
if (effectiveMode === "timeline") {
|
|
@@ -282,9 +323,16 @@ This is the recommended entry point for ALL memory queries.`,
|
|
|
282
323
|
if (effectiveMode === "causal") {
|
|
283
324
|
const llm = getDefaultLlamaCpp();
|
|
284
325
|
const intent = await classifyIntent(query, llm, store.db);
|
|
285
|
-
const bm25Results = store.searchFTS(query, 30);
|
|
326
|
+
const bm25Results = store.searchFTS(query, 30, undefined, undefined, undefined, excl);
|
|
286
327
|
let vecResults: SearchResult[] = [];
|
|
287
|
-
|
|
328
|
+
let causalDegraded: DegradedLeg | undefined;
|
|
329
|
+
try {
|
|
330
|
+
const det = await store.searchVecDetailed(query, DEFAULT_EMBED_MODEL, 30, { excludeCollections: excl });
|
|
331
|
+
vecResults = det.results;
|
|
332
|
+
// Causal mode runs ONE vector call — flat single-vector shape per the documented
|
|
333
|
+
// contract (T8-M5), not the multi-leg degradedLegs aggregate.
|
|
334
|
+
if (det.degraded && det.degradedReason) causalDegraded = { leg: "causal:vector", reason: det.degradedReason };
|
|
335
|
+
} catch (e) { rethrowIfFatalVectorError(e); /* else: no vectors */ }
|
|
288
336
|
const rrfWeights = intent.intent === 'WHY' ? [1.0, 1.5] : intent.intent === 'WHEN' ? [1.5, 1.0] : [1.0, 1.0];
|
|
289
337
|
const fusedRanked = reciprocalRankFusion([bm25Results.map(toRanked), vecResults.map(toRanked)], rrfWeights);
|
|
290
338
|
const allSearch = [...bm25Results, ...vecResults];
|
|
@@ -300,6 +348,9 @@ This is the recommended entry point for ALL memory queries.`,
|
|
|
300
348
|
const traversed = adaptiveTraversal(store.db, fused.slice(0, 10).map(r => ({ hash: r.hash, score: r.score })), {
|
|
301
349
|
maxDepth: 2, beamWidth: 5, budget: 30,
|
|
302
350
|
intent: intent.intent, queryEmbedding: anchorEmb.embedding,
|
|
351
|
+
// Visibility policy threaded INTO traversal (T3-H2): excluded nodes are pruned
|
|
352
|
+
// before beam selection and score normalization, not just hidden at hydration.
|
|
353
|
+
excludeCollections: excl,
|
|
303
354
|
});
|
|
304
355
|
const merged = mergeTraversalResults(store.db, fused.map(r => ({ hash: r.hash, score: r.score })), traversed);
|
|
305
356
|
// Hydrate merged results back to SearchResult format
|
|
@@ -307,7 +358,8 @@ This is the recommended entry point for ALL memory queries.`,
|
|
|
307
358
|
fused = merged.map(m => {
|
|
308
359
|
const orig = fusedMap.get(m.hash);
|
|
309
360
|
if (orig) return { ...orig, score: m.score };
|
|
310
|
-
// Graph-discovered node — hydrate from DB
|
|
361
|
+
// Graph-discovered node — hydrate from DB (defense-in-depth visibility guard;
|
|
362
|
+
// traversal-level pruning is the primary barrier)
|
|
311
363
|
const doc = store.db.prepare(`
|
|
312
364
|
SELECT d.collection, d.path, d.title, d.hash, c.doc as body, d.modified_at
|
|
313
365
|
FROM documents d
|
|
@@ -315,6 +367,7 @@ This is the recommended entry point for ALL memory queries.`,
|
|
|
315
367
|
WHERE d.hash = ? AND d.active = 1 AND d.invalidated_at IS NULL LIMIT 1
|
|
316
368
|
`).get(m.hash) as { collection: string; path: string; title: string; hash: string; body: string | null; modified_at: string } | undefined;
|
|
317
369
|
if (!doc) return null;
|
|
370
|
+
if (excl && excl.includes(doc.collection)) return null;
|
|
318
371
|
return {
|
|
319
372
|
filepath: `clawmem://${doc.collection}/${doc.path}`,
|
|
320
373
|
displayPath: `${doc.collection}/${doc.path}`,
|
|
@@ -335,56 +388,77 @@ This is the recommended entry point for ALL memory queries.`,
|
|
|
335
388
|
}
|
|
336
389
|
|
|
337
390
|
const enriched = enrichResults(store, fused, query);
|
|
338
|
-
const scored = applyCompositeScoring(enriched, query).slice(0, lim);
|
|
391
|
+
const scored = applyCompositeScoring(enriched, query, undefined, mcpDirectWeightsOption()).slice(0, lim);
|
|
339
392
|
const items = scored.map(r => ({
|
|
340
393
|
docid: `#${r.docid}`, path: r.displayPath, title: r.title,
|
|
341
394
|
score: Math.round(r.compositeScore * 100) / 100,
|
|
342
395
|
snippet: (r.body || "").substring(0, 150), content_type: r.contentType,
|
|
343
396
|
}));
|
|
397
|
+
const causalDegradedFields = causalDegraded ? { degraded: true as const, degradedReason: causalDegraded.reason } : {};
|
|
398
|
+
const degradedNote = causalDegraded ? `\n${degradedGuidanceText([causalDegraded])}` : "";
|
|
344
399
|
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 },
|
|
400
|
+
content: [{ type: "text", text: `[routed: causal, intent: ${intent.intent}] ${formatSearchSummary(items.map(i => ({ ...i, file: i.path, compositeScore: i.score, context: null })), query)}${degradedNote}` }],
|
|
401
|
+
structuredContent: { mode: effectiveMode, intent, results: items, ...causalDegradedFields },
|
|
347
402
|
};
|
|
348
403
|
}
|
|
349
404
|
|
|
350
|
-
// --- Complex mode → query decomposition ---
|
|
405
|
+
// --- Complex mode → query decomposition (multi-leg: per-clause degraded aggregation, T6-M1) ---
|
|
351
406
|
if (effectiveMode === "complex") {
|
|
352
407
|
const llm = getDefaultLlamaCpp();
|
|
353
408
|
const clauses = await decomposeQuery(query, llm, store.db);
|
|
354
409
|
const allResults: SearchResult[] = [];
|
|
410
|
+
let clauseIdx = 0;
|
|
355
411
|
for (const clause of clauses.sort((a, b) => a.priority - b.priority)) {
|
|
412
|
+
const clauseExcl = resolveExcludedCollections(includeInternal, clause.collections);
|
|
356
413
|
let results: SearchResult[] = [];
|
|
357
|
-
if (clause.type === 'bm25') results = store.searchFTS(clause.query, 20, undefined, clause.collections);
|
|
358
|
-
else if (clause.type === 'vector') {
|
|
359
|
-
|
|
414
|
+
if (clause.type === 'bm25') results = store.searchFTS(clause.query, 20, undefined, clause.collections, undefined, clauseExcl);
|
|
415
|
+
else if (clause.type === 'vector') {
|
|
416
|
+
try {
|
|
417
|
+
const det = await store.searchVecDetailed(clause.query, DEFAULT_EMBED_MODEL, 20, { collections: clause.collections, excludeCollections: clauseExcl });
|
|
418
|
+
results = det.results;
|
|
419
|
+
if (det.degraded && det.degradedReason) degradedLegs.push({ leg: `complex:clause${clauseIdx}:vector`, reason: det.degradedReason });
|
|
420
|
+
} catch (e) { rethrowIfFatalVectorError(e); /* */ }
|
|
421
|
+
}
|
|
422
|
+
else if (clause.type === 'graph') { results = store.searchFTS(clause.query, 15, undefined, clause.collections, undefined, clauseExcl); }
|
|
360
423
|
allResults.push(...results);
|
|
424
|
+
clauseIdx++;
|
|
361
425
|
}
|
|
362
426
|
const seen = new Set<string>();
|
|
363
427
|
const deduped = allResults.filter(r => { if (seen.has(r.filepath)) return false; seen.add(r.filepath); return true; });
|
|
364
428
|
const enriched = enrichResults(store, deduped, query);
|
|
365
|
-
const scored = applyCompositeScoring(enriched, query).slice(0, lim);
|
|
429
|
+
const scored = applyCompositeScoring(enriched, query, undefined, mcpDirectWeightsOption()).slice(0, lim);
|
|
366
430
|
const items = scored.map(r => ({
|
|
367
431
|
docid: `#${r.docid}`, path: r.displayPath, title: r.title,
|
|
368
432
|
score: Math.round(r.compositeScore * 100) / 100,
|
|
369
433
|
snippet: (r.body || "").substring(0, 150), content_type: r.contentType,
|
|
370
434
|
}));
|
|
435
|
+
const degradedNote = degradedLegs.length > 0 ? `\n${degradedGuidanceText(degradedLegs)}` : "";
|
|
371
436
|
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 },
|
|
437
|
+
content: [{ type: "text", text: `[routed: complex, ${clauses.length} clauses] ${formatSearchSummary(items.map(i => ({ ...i, file: i.path, compositeScore: i.score, context: null })), query)}${degradedNote}` }],
|
|
438
|
+
structuredContent: { mode: effectiveMode, clauses: clauses.length, results: items, ...(degradedLegs.length > 0 ? { degraded: true, degradedLegs } : {}) },
|
|
374
439
|
};
|
|
375
440
|
}
|
|
376
441
|
|
|
377
442
|
// --- Keyword / Semantic / Discovery / Hybrid modes ---
|
|
378
443
|
let results: SearchResult[] = [];
|
|
444
|
+
let singleLegDegraded: DegradedLeg | undefined;
|
|
379
445
|
if (effectiveMode === "keyword") {
|
|
380
|
-
results = store.searchFTS(query, lim);
|
|
446
|
+
results = store.searchFTS(query, lim, undefined, undefined, undefined, excl);
|
|
381
447
|
} else if (effectiveMode === "semantic" || effectiveMode === "discovery") {
|
|
382
|
-
try {
|
|
448
|
+
try {
|
|
449
|
+
const det = await store.searchVecDetailed(query, DEFAULT_EMBED_MODEL, lim, { excludeCollections: excl });
|
|
450
|
+
results = det.results;
|
|
451
|
+
if (det.degraded && det.degradedReason) singleLegDegraded = { leg: `${effectiveMode}:vector`, reason: det.degradedReason };
|
|
452
|
+
} catch (e) { rethrowIfFatalVectorError(e); results = store.searchFTS(query, lim, undefined, undefined, undefined, excl); }
|
|
383
453
|
} else {
|
|
384
454
|
// Hybrid: BM25 + vector + RRF
|
|
385
|
-
const bm25 = store.searchFTS(query, 30);
|
|
455
|
+
const bm25 = store.searchFTS(query, 30, undefined, undefined, undefined, excl);
|
|
386
456
|
let vec: SearchResult[] = [];
|
|
387
|
-
try {
|
|
457
|
+
try {
|
|
458
|
+
const det = await store.searchVecDetailed(query, DEFAULT_EMBED_MODEL, 30, { excludeCollections: excl });
|
|
459
|
+
vec = det.results;
|
|
460
|
+
if (det.degraded && det.degradedReason) singleLegDegraded = { leg: "hybrid:vector", reason: det.degradedReason };
|
|
461
|
+
} catch (e) { rethrowIfFatalVectorError(e); /* */ }
|
|
388
462
|
if (vec.length > 0) {
|
|
389
463
|
const fusedRanked = reciprocalRankFusion([bm25.map(toRanked), vec.map(toRanked)], [1.0, 1.0]);
|
|
390
464
|
const allSearch = [...bm25, ...vec];
|
|
@@ -398,7 +472,9 @@ This is the recommended entry point for ALL memory queries.`,
|
|
|
398
472
|
}
|
|
399
473
|
|
|
400
474
|
const enriched = enrichResults(store, results, query);
|
|
401
|
-
const scored = applyCompositeScoring(enriched, query).slice(0, lim);
|
|
475
|
+
const scored = applyCompositeScoring(enriched, query, undefined, mcpDirectWeightsOption()).slice(0, lim);
|
|
476
|
+
const modeDegraded = singleLegDegraded ? { degraded: true as const, degradedReason: singleLegDegraded.reason } : {};
|
|
477
|
+
const modeDegradedNote = singleLegDegraded ? `\n${degradedGuidanceText([singleLegDegraded])}` : "";
|
|
402
478
|
if (compact) {
|
|
403
479
|
const items = scored.map(r => ({
|
|
404
480
|
docid: `#${r.docid}`, path: r.displayPath, title: r.title,
|
|
@@ -406,8 +482,8 @@ This is the recommended entry point for ALL memory queries.`,
|
|
|
406
482
|
snippet: (r.body || "").substring(0, 150), content_type: r.contentType,
|
|
407
483
|
}));
|
|
408
484
|
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 },
|
|
485
|
+
content: [{ type: "text", text: `[routed: ${effectiveMode}] ${formatSearchSummary(items.map(i => ({ ...i, file: i.path, compositeScore: i.score, context: null })), query)}${modeDegradedNote}` }],
|
|
486
|
+
structuredContent: { mode: effectiveMode, results: items, ...modeDegraded },
|
|
411
487
|
};
|
|
412
488
|
}
|
|
413
489
|
const items: SearchResultItem[] = scored.map(r => {
|
|
@@ -420,8 +496,8 @@ This is the recommended entry point for ALL memory queries.`,
|
|
|
420
496
|
};
|
|
421
497
|
});
|
|
422
498
|
return {
|
|
423
|
-
content: [{ type: "text", text: `[routed: ${effectiveMode}] ${formatSearchSummary(items, query)}` }],
|
|
424
|
-
structuredContent: { mode: effectiveMode, results: items },
|
|
499
|
+
content: [{ type: "text", text: `[routed: ${effectiveMode}] ${formatSearchSummary(items, query)}${modeDegradedNote}` }],
|
|
500
|
+
structuredContent: { mode: effectiveMode, results: items, ...modeDegraded },
|
|
425
501
|
};
|
|
426
502
|
}
|
|
427
503
|
);
|
|
@@ -495,19 +571,21 @@ This is the recommended entry point for ALL memory queries.`,
|
|
|
495
571
|
minScore: z.number().optional().default(0),
|
|
496
572
|
collection: z.string().optional().describe("Filter to collection (single name or comma-separated)"),
|
|
497
573
|
compact: z.boolean().optional().default(false).describe("Return compact results (id, path, title, score, snippet) instead of full content"),
|
|
574
|
+
includeInternal: z.boolean().optional().default(false).describe("Include system-internal _clawmem docs (observations/deductions) — excluded by default"),
|
|
498
575
|
vault: z.string().optional().describe("Named vault (omit for default vault)"),
|
|
499
576
|
},
|
|
500
577
|
},
|
|
501
|
-
async ({ query, limit, minScore, collection, compact, vault }) => {
|
|
578
|
+
async ({ query, limit, minScore, collection, compact, includeInternal, vault }) => {
|
|
502
579
|
const store = getStore(vault);
|
|
503
580
|
const collections = collection
|
|
504
581
|
? collection.split(",").map(c => c.trim()).filter(Boolean)
|
|
505
582
|
: undefined;
|
|
506
|
-
const
|
|
583
|
+
const excl = resolveExcludedCollections(includeInternal, collections);
|
|
584
|
+
const results = store.searchFTS(query, limit || 10, undefined, collections, undefined, excl);
|
|
507
585
|
|
|
508
586
|
const coFn = (path: string) => store.getCoActivated(path);
|
|
509
587
|
const enriched = enrichResults(store, results, query);
|
|
510
|
-
const scored = applyCompositeScoring(enriched, query, coFn)
|
|
588
|
+
const scored = applyCompositeScoring(enriched, query, coFn, mcpDirectWeightsOption())
|
|
511
589
|
.filter(r => r.compositeScore >= (minScore || 0));
|
|
512
590
|
|
|
513
591
|
if (compact) {
|
|
@@ -556,10 +634,11 @@ This is the recommended entry point for ALL memory queries.`,
|
|
|
556
634
|
minScore: z.number().optional().default(0.3),
|
|
557
635
|
collection: z.string().optional().describe("Filter to collection (single name or comma-separated)"),
|
|
558
636
|
compact: z.boolean().optional().default(false).describe("Return compact results (id, path, title, score, snippet) instead of full content"),
|
|
637
|
+
includeInternal: z.boolean().optional().default(false).describe("Include system-internal _clawmem docs (observations/deductions) — excluded by default"),
|
|
559
638
|
vault: z.string().optional().describe("Named vault (omit for default vault)"),
|
|
560
639
|
},
|
|
561
640
|
},
|
|
562
|
-
async ({ query, limit, minScore, collection, compact, vault }) => {
|
|
641
|
+
async ({ query, limit, minScore, collection, compact, includeInternal, vault }) => {
|
|
563
642
|
const store = getStore(vault);
|
|
564
643
|
const tableExists = store.db.prepare(`SELECT name FROM sqlite_master WHERE type='table' AND name='vectors_vec'`).get();
|
|
565
644
|
if (!tableExists) {
|
|
@@ -569,11 +648,15 @@ This is the recommended entry point for ALL memory queries.`,
|
|
|
569
648
|
const collections = collection
|
|
570
649
|
? collection.split(",").map(c => c.trim()).filter(Boolean)
|
|
571
650
|
: undefined;
|
|
572
|
-
const
|
|
651
|
+
const excl = resolveExcludedCollections(includeInternal, collections);
|
|
652
|
+
const det = await store.searchVecDetailed(query, DEFAULT_EMBED_MODEL, limit || 10, { collections, excludeCollections: excl });
|
|
653
|
+
const results = det.results;
|
|
654
|
+
const vsDegraded = det.degraded && det.degradedReason ? { degraded: true as const, degradedReason: det.degradedReason } : {};
|
|
655
|
+
const vsDegradedNote = det.degraded && det.degradedReason ? `\n${degradedGuidanceText([{ leg: "vsearch", reason: det.degradedReason }])}` : "";
|
|
573
656
|
|
|
574
657
|
const coFn = (path: string) => store.getCoActivated(path);
|
|
575
658
|
const enriched = enrichResults(store, results, query);
|
|
576
|
-
const scored = applyCompositeScoring(enriched, query, coFn)
|
|
659
|
+
const scored = applyCompositeScoring(enriched, query, coFn, mcpDirectWeightsOption())
|
|
577
660
|
.filter(r => r.compositeScore >= (minScore || 0.3));
|
|
578
661
|
|
|
579
662
|
if (compact) {
|
|
@@ -583,7 +666,7 @@ This is the recommended entry point for ALL memory queries.`,
|
|
|
583
666
|
snippet: (r.body || "").substring(0, 150), content_type: r.contentType, modified_at: r.modifiedAt,
|
|
584
667
|
fragment: r.fragmentType ? { type: r.fragmentType, label: r.fragmentLabel } : undefined,
|
|
585
668
|
}));
|
|
586
|
-
return { content: [{ type: "text", text: formatSearchSummary(items.map(i => ({ ...i, file: i.path, compositeScore: i.score, context: null })), query) }], structuredContent: { results: items } };
|
|
669
|
+
return { content: [{ type: "text", text: `${formatSearchSummary(items.map(i => ({ ...i, file: i.path, compositeScore: i.score, context: null })), query)}${vsDegradedNote}` }], structuredContent: { results: items, ...vsDegraded } };
|
|
587
670
|
}
|
|
588
671
|
|
|
589
672
|
const items: SearchResultItem[] = scored.map(r => {
|
|
@@ -601,8 +684,8 @@ This is the recommended entry point for ALL memory queries.`,
|
|
|
601
684
|
});
|
|
602
685
|
|
|
603
686
|
return {
|
|
604
|
-
content: [{ type: "text", text: formatSearchSummary(items, query) }],
|
|
605
|
-
structuredContent: { results: items },
|
|
687
|
+
content: [{ type: "text", text: `${formatSearchSummary(items, query)}${vsDegradedNote}` }],
|
|
688
|
+
structuredContent: { results: items, ...vsDegraded },
|
|
606
689
|
};
|
|
607
690
|
}
|
|
608
691
|
);
|
|
@@ -625,14 +708,16 @@ This is the recommended entry point for ALL memory queries.`,
|
|
|
625
708
|
diverse: z.boolean().optional().default(true).describe("Apply MMR diversity filter to reduce near-duplicate results"),
|
|
626
709
|
intent: z.string().optional().describe("Domain intent hint for disambiguation — steers expansion, reranking, chunk selection, and snippet extraction"),
|
|
627
710
|
candidateLimit: z.number().optional().default(30).describe("Max candidates reaching the reranker (default 30)"),
|
|
711
|
+
includeInternal: z.boolean().optional().default(false).describe("Include system-internal _clawmem docs (observations/deductions) — excluded by default"),
|
|
628
712
|
vault: z.string().optional().describe("Named vault (omit for default vault)"),
|
|
629
713
|
},
|
|
630
714
|
},
|
|
631
|
-
async ({ query, limit, minScore, collection, compact, diverse, intent, candidateLimit, vault }) => {
|
|
715
|
+
async ({ query, limit, minScore, collection, compact, diverse, intent, candidateLimit, includeInternal, vault }) => {
|
|
632
716
|
const store = getStore(vault);
|
|
633
717
|
const candLimit = candidateLimit || 30;
|
|
634
718
|
const rankedLists: RankedResult[][] = [];
|
|
635
719
|
const docidMap = new Map<string, string>();
|
|
720
|
+
const degradedLegs: DegradedLeg[] = [];
|
|
636
721
|
const hasVectors = !!store.db.prepare(`SELECT name FROM sqlite_master WHERE type='table' AND name='vectors_vec'`).get();
|
|
637
722
|
|
|
638
723
|
// Step 0: Temporal constraint extraction (pure regex, ~0ms)
|
|
@@ -642,7 +727,8 @@ This is the recommended entry point for ALL memory queries.`,
|
|
|
642
727
|
const collections = collection
|
|
643
728
|
? collection.split(",").map(c => c.trim()).filter(Boolean)
|
|
644
729
|
: undefined;
|
|
645
|
-
const
|
|
730
|
+
const excl = resolveExcludedCollections(includeInternal, collections);
|
|
731
|
+
const initialFts = store.searchFTS(query, 20, undefined, collections, dateRange, excl);
|
|
646
732
|
const topScore = initialFts.length > 0 ? Math.abs(initialFts[0]!.score) : 0;
|
|
647
733
|
const secondScore = initialFts.length > 1 ? Math.abs(initialFts[1]!.score) : 0;
|
|
648
734
|
// When intent is provided, disable strong-signal bypass — the obvious BM25
|
|
@@ -663,30 +749,34 @@ This is the recommended entry point for ALL memory queries.`,
|
|
|
663
749
|
rankedLists.push(initialFts.map(r => ({ file: r.filepath, displayPath: r.displayPath, title: r.title, body: r.body || "", score: r.score })));
|
|
664
750
|
}
|
|
665
751
|
if (hasVectors) {
|
|
666
|
-
const
|
|
667
|
-
if (
|
|
668
|
-
|
|
669
|
-
|
|
752
|
+
const det = await store.searchVecDetailed(query, DEFAULT_EMBED_MODEL, 20, { collections, dateRange, excludeCollections: excl });
|
|
753
|
+
if (det.degraded && det.degradedReason) degradedLegs.push({ leg: "vector:original", reason: det.degradedReason });
|
|
754
|
+
if (det.results.length > 0) {
|
|
755
|
+
for (const r of det.results) docidMap.set(r.filepath, r.docid);
|
|
756
|
+
rankedLists.push(det.results.map(r => ({ file: r.filepath, displayPath: r.displayPath, title: r.title, body: r.body || "", score: r.score })));
|
|
670
757
|
}
|
|
671
758
|
}
|
|
672
759
|
// Lists contributed by the original query — these get the 2× RRF weight.
|
|
673
760
|
const numOriginalLists = rankedLists.length;
|
|
674
761
|
|
|
675
762
|
// Typed expansions — route by type: lex → FTS, vec/hyde → vector.
|
|
763
|
+
let expansionIdx = 0;
|
|
676
764
|
for (const eq of expanded) {
|
|
677
765
|
if (eq.type === 'lex') {
|
|
678
|
-
const ftsResults = store.searchFTS(eq.query, 20, undefined, collections, dateRange);
|
|
766
|
+
const ftsResults = store.searchFTS(eq.query, 20, undefined, collections, dateRange, excl);
|
|
679
767
|
if (ftsResults.length > 0) {
|
|
680
768
|
for (const r of ftsResults) docidMap.set(r.filepath, r.docid);
|
|
681
769
|
rankedLists.push(ftsResults.map(r => ({ file: r.filepath, displayPath: r.displayPath, title: r.title, body: r.body || "", score: r.score })));
|
|
682
770
|
}
|
|
683
771
|
} else if (hasVectors) {
|
|
684
|
-
const
|
|
685
|
-
if (
|
|
686
|
-
|
|
687
|
-
|
|
772
|
+
const det = await store.searchVecDetailed(eq.query, DEFAULT_EMBED_MODEL, 20, { collections, dateRange, excludeCollections: excl });
|
|
773
|
+
if (det.degraded && det.degradedReason) degradedLegs.push({ leg: `vector:expansion${expansionIdx}`, reason: det.degradedReason });
|
|
774
|
+
if (det.results.length > 0) {
|
|
775
|
+
for (const r of det.results) docidMap.set(r.filepath, r.docid);
|
|
776
|
+
rankedLists.push(det.results.map(r => ({ file: r.filepath, displayPath: r.displayPath, title: r.title, body: r.body || "", score: r.score })));
|
|
688
777
|
}
|
|
689
778
|
}
|
|
779
|
+
expansionIdx++;
|
|
690
780
|
}
|
|
691
781
|
|
|
692
782
|
// Step 2b: Temporal proximity channel (if dateRange detected)
|
|
@@ -694,14 +784,17 @@ This is the recommended entry point for ALL memory queries.`,
|
|
|
694
784
|
if (dateRange) {
|
|
695
785
|
const centerMs = (new Date(dateRange.start).getTime() + new Date(dateRange.end).getTime()) / 2;
|
|
696
786
|
const rangeMs = Math.max(new Date(dateRange.end).getTime() - new Date(dateRange.start).getTime(), 86400000);
|
|
787
|
+
const temporalExclSql = excl && excl.length > 0
|
|
788
|
+
? ` AND d.collection NOT IN (${excl.map(() => '?').join(',')})`
|
|
789
|
+
: "";
|
|
697
790
|
const temporalDocs = store.db.prepare(`
|
|
698
791
|
SELECT 'clawmem://' || d.collection || '/' || d.path as filepath,
|
|
699
792
|
d.collection || '/' || d.path as displayPath,
|
|
700
793
|
d.title, d.modified_at
|
|
701
794
|
FROM documents d
|
|
702
|
-
WHERE d.active = 1 AND d.invalidated_at IS NULL AND d.modified_at >= ? AND d.modified_at <=
|
|
795
|
+
WHERE d.active = 1 AND d.invalidated_at IS NULL AND d.modified_at >= ? AND d.modified_at <= ?${temporalExclSql}
|
|
703
796
|
ORDER BY d.modified_at DESC LIMIT 30
|
|
704
|
-
`).all(dateRange.start, dateRange.end) as { filepath: string; displayPath: string; title: string; modified_at: string }[];
|
|
797
|
+
`).all(dateRange.start, dateRange.end, ...(excl ?? [])) as { filepath: string; displayPath: string; title: string; modified_at: string }[];
|
|
705
798
|
|
|
706
799
|
if (temporalDocs.length > 0) {
|
|
707
800
|
const temporalRanked: RankedResult[] = temporalDocs.map(d => {
|
|
@@ -732,6 +825,7 @@ This is the recommended entry point for ALL memory queries.`,
|
|
|
732
825
|
WHERE d.id = ? AND d.active = 1 AND d.invalidated_at IS NULL LIMIT 1
|
|
733
826
|
`).get(en.docId) as { collection: string; path: string; title: string; body: string | null } | undefined;
|
|
734
827
|
if (!doc) return null;
|
|
828
|
+
if (excl && excl.includes(doc.collection)) return null;
|
|
735
829
|
return {
|
|
736
830
|
file: `clawmem://${doc.collection}/${doc.path}`,
|
|
737
831
|
displayPath: `${doc.collection}/${doc.path}`,
|
|
@@ -836,6 +930,10 @@ This is the recommended entry point for ALL memory queries.`,
|
|
|
836
930
|
if (diverse !== false) scored = applyMMRDiversity(scored);
|
|
837
931
|
scored = scored.slice(0, limit || 10);
|
|
838
932
|
|
|
933
|
+
// Multi-leg degraded aggregation (T5-M1): route marker = any(leg), per-leg reasons retained.
|
|
934
|
+
const queryDegraded = degradedLegs.length > 0 ? { degraded: true as const, degradedLegs } : {};
|
|
935
|
+
const queryDegradedNote = degradedLegs.length > 0 ? `\n${degradedGuidanceText(degradedLegs)}` : "";
|
|
936
|
+
|
|
839
937
|
if (compact) {
|
|
840
938
|
const items = scored.map(r => ({
|
|
841
939
|
docid: `#${docidMap.get(r.filepath) || r.docid}`, path: r.displayPath, title: r.title,
|
|
@@ -843,7 +941,7 @@ This is the recommended entry point for ALL memory queries.`,
|
|
|
843
941
|
snippet: (r.body || "").substring(0, 150), content_type: r.contentType, modified_at: r.modifiedAt,
|
|
844
942
|
fragment: r.fragmentType ? { type: r.fragmentType, label: r.fragmentLabel } : undefined,
|
|
845
943
|
}));
|
|
846
|
-
return { content: [{ type: "text", text: formatSearchSummary(items.map(i => ({ ...i, file: i.path, compositeScore: i.score, context: null })), query) }], structuredContent: { results: items } };
|
|
944
|
+
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
945
|
}
|
|
848
946
|
|
|
849
947
|
const items: SearchResultItem[] = scored.map(r => {
|
|
@@ -861,8 +959,8 @@ This is the recommended entry point for ALL memory queries.`,
|
|
|
861
959
|
});
|
|
862
960
|
|
|
863
961
|
return {
|
|
864
|
-
content: [{ type: "text", text: formatSearchSummary(items, query) }],
|
|
865
|
-
structuredContent: { results: items },
|
|
962
|
+
content: [{ type: "text", text: `${formatSearchSummary(items, query)}${queryDegradedNote}` }],
|
|
963
|
+
structuredContent: { results: items, ...queryDegraded },
|
|
866
964
|
};
|
|
867
965
|
}
|
|
868
966
|
);
|
|
@@ -1272,10 +1370,11 @@ This is the recommended entry point for ALL memory queries.`,
|
|
|
1272
1370
|
inputSchema: {
|
|
1273
1371
|
file: z.string().describe("Path of reference document"),
|
|
1274
1372
|
limit: z.number().optional().default(5),
|
|
1373
|
+
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
1374
|
vault: z.string().optional().describe("Named vault (omit for default vault)"),
|
|
1276
1375
|
},
|
|
1277
1376
|
},
|
|
1278
|
-
async ({ file, limit, vault }) => {
|
|
1377
|
+
async ({ file, limit, includeInternal, vault }) => {
|
|
1279
1378
|
const store = getStore(vault);
|
|
1280
1379
|
const tableExists = store.db.prepare(`SELECT name FROM sqlite_master WHERE type='table' AND name='vectors_vec'`).get();
|
|
1281
1380
|
if (!tableExists) {
|
|
@@ -1291,14 +1390,21 @@ This is the recommended entry point for ALL memory queries.`,
|
|
|
1291
1390
|
const body = store.getDocumentBody(result) ?? "";
|
|
1292
1391
|
const title = result.title || file;
|
|
1293
1392
|
|
|
1393
|
+
// Internal-reference auto-exception (T2-M7): exploring FROM an internal doc is an
|
|
1394
|
+
// explicit ask for the internal space — include internal neighbors.
|
|
1395
|
+
const refIsInternal = INTERNAL_COLLECTIONS.includes(result.collectionName);
|
|
1396
|
+
const excl = resolveExcludedCollections(includeInternal || refIsInternal);
|
|
1397
|
+
|
|
1294
1398
|
// Use the document's content as the search query
|
|
1295
1399
|
const queryText = `${title}\n${body.slice(0, 1000)}`;
|
|
1296
|
-
const
|
|
1400
|
+
const det = await store.searchVecDetailed(queryText, DEFAULT_EMBED_MODEL, (limit || 5) + 1, { excludeCollections: excl });
|
|
1297
1401
|
|
|
1298
1402
|
// Filter out the reference document itself
|
|
1299
|
-
const similar =
|
|
1403
|
+
const similar = det.results
|
|
1300
1404
|
.filter(r => r.filepath !== result.filepath)
|
|
1301
1405
|
.slice(0, limit || 5);
|
|
1406
|
+
const fsDegraded = det.degraded && det.degradedReason ? { degraded: true as const, degradedReason: det.degradedReason } : {};
|
|
1407
|
+
const fsDegradedNote = det.degraded && det.degradedReason ? `\n${degradedGuidanceText([{ leg: "find_similar", reason: det.degradedReason }])}` : "";
|
|
1302
1408
|
|
|
1303
1409
|
const items: SearchResultItem[] = similar.map(r => {
|
|
1304
1410
|
const { line, snippet } = extractSnippet(r.body || "", title, 200);
|
|
@@ -1313,8 +1419,8 @@ This is the recommended entry point for ALL memory queries.`,
|
|
|
1313
1419
|
});
|
|
1314
1420
|
|
|
1315
1421
|
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 },
|
|
1422
|
+
content: [{ type: "text", text: `${items.length} similar to "${title}":\n${items.map(i => ` ${i.file} (${Math.round(i.score * 100)}%)`).join('\n')}${fsDegradedNote}` }],
|
|
1423
|
+
structuredContent: { reference: file, results: items, ...fsDegraded },
|
|
1318
1424
|
};
|
|
1319
1425
|
}
|
|
1320
1426
|
);
|
|
@@ -1763,10 +1869,11 @@ This is the recommended entry point for ALL memory queries.`,
|
|
|
1763
1869
|
query: z.string().describe("Complex or multi-topic query"),
|
|
1764
1870
|
limit: z.number().optional().default(10),
|
|
1765
1871
|
compact: z.boolean().optional().default(true).describe("Return compact results"),
|
|
1872
|
+
includeInternal: z.boolean().optional().default(false).describe("Include system-internal _clawmem docs (observations/deductions) — excluded by default"),
|
|
1766
1873
|
vault: z.string().optional().describe("Named vault (omit for default vault)"),
|
|
1767
1874
|
},
|
|
1768
1875
|
},
|
|
1769
|
-
async ({ query, limit, compact, vault }) => {
|
|
1876
|
+
async ({ query, limit, compact, includeInternal, vault }) => {
|
|
1770
1877
|
const store = getStore(vault);
|
|
1771
1878
|
const llm = getDefaultLlamaCpp();
|
|
1772
1879
|
|
|
@@ -1777,18 +1884,25 @@ This is the recommended entry point for ALL memory queries.`,
|
|
|
1777
1884
|
const sortedClauses = [...clauses].sort((a, b) => a.priority - b.priority);
|
|
1778
1885
|
const allResults: SearchResult[] = [];
|
|
1779
1886
|
const clauseDetails: { type: string; query: string; priority: number; resultCount: number }[] = [];
|
|
1887
|
+
const degradedLegs: DegradedLeg[] = [];
|
|
1780
1888
|
|
|
1889
|
+
let clauseIdx = 0;
|
|
1781
1890
|
for (const clause of sortedClauses) {
|
|
1891
|
+
const clauseExcl = resolveExcludedCollections(includeInternal, clause.collections);
|
|
1782
1892
|
let results: SearchResult[] = [];
|
|
1783
1893
|
if (clause.type === 'bm25') {
|
|
1784
|
-
results = store.searchFTS(clause.query, 20, undefined, clause.collections);
|
|
1894
|
+
results = store.searchFTS(clause.query, 20, undefined, clause.collections, undefined, clauseExcl);
|
|
1785
1895
|
} else if (clause.type === 'vector') {
|
|
1786
|
-
|
|
1896
|
+
const det = await store.searchVecDetailed(clause.query, DEFAULT_EMBED_MODEL, 20, { collections: clause.collections, excludeCollections: clauseExcl });
|
|
1897
|
+
results = det.results;
|
|
1898
|
+
if (det.degraded && det.degradedReason) degradedLegs.push({ leg: `clause${clauseIdx}:vector`, reason: det.degradedReason });
|
|
1787
1899
|
} else if (clause.type === 'graph') {
|
|
1788
1900
|
// Graph clause: run intent_search-style retrieval
|
|
1789
1901
|
const intent = await classifyIntent(clause.query, llm, store.db);
|
|
1790
|
-
const bm25 = store.searchFTS(clause.query, 15, undefined, clause.collections);
|
|
1791
|
-
const
|
|
1902
|
+
const bm25 = store.searchFTS(clause.query, 15, undefined, clause.collections, undefined, clauseExcl);
|
|
1903
|
+
const detGraph = await store.searchVecDetailed(clause.query, DEFAULT_EMBED_MODEL, 15, { collections: clause.collections, excludeCollections: clauseExcl });
|
|
1904
|
+
const vec = detGraph.results;
|
|
1905
|
+
if (detGraph.degraded && detGraph.degradedReason) degradedLegs.push({ leg: `clause${clauseIdx}:graph:vector`, reason: detGraph.degradedReason });
|
|
1792
1906
|
const fused = reciprocalRankFusion([bm25.map(toRanked), vec.map(toRanked)], [1.0, 1.0]);
|
|
1793
1907
|
const searchMap = new Map([...bm25, ...vec].map(r => [r.filepath, r]));
|
|
1794
1908
|
results = fused
|
|
@@ -1801,13 +1915,15 @@ This is the recommended entry point for ALL memory queries.`,
|
|
|
1801
1915
|
if (anchorEmb) {
|
|
1802
1916
|
const traversed = adaptiveTraversal(store.db, results.slice(0, 5).map(r => ({ hash: r.hash, score: r.score })), {
|
|
1803
1917
|
maxDepth: 2, beamWidth: 3, budget: 15, intent: intent.intent, queryEmbedding: anchorEmb.embedding,
|
|
1918
|
+
// Visibility policy threaded INTO traversal (T6-H1): parity with memory_retrieve causal
|
|
1919
|
+
excludeCollections: clauseExcl,
|
|
1804
1920
|
});
|
|
1805
1921
|
const merged = mergeTraversalResults(store.db, results.map(r => ({ hash: r.hash, score: r.score })), traversed);
|
|
1806
1922
|
const expandedMap = new Map(results.map(r => [r.hash, r]));
|
|
1807
1923
|
results = merged.map(m => {
|
|
1808
1924
|
const existing = expandedMap.get(m.hash);
|
|
1809
1925
|
if (existing) return { ...existing, score: m.score };
|
|
1810
|
-
// Graph-discovered node — hydrate from DB
|
|
1926
|
+
// Graph-discovered node — hydrate from DB (defense-in-depth visibility guard)
|
|
1811
1927
|
const doc = store.db.prepare(`
|
|
1812
1928
|
SELECT d.collection, d.path, d.title, d.hash, c.doc as body, d.modified_at
|
|
1813
1929
|
FROM documents d
|
|
@@ -1815,6 +1931,7 @@ This is the recommended entry point for ALL memory queries.`,
|
|
|
1815
1931
|
WHERE d.hash = ? AND d.active = 1 AND d.invalidated_at IS NULL LIMIT 1
|
|
1816
1932
|
`).get(m.hash) as { collection: string; path: string; title: string; hash: string; body: string | null; modified_at: string } | undefined;
|
|
1817
1933
|
if (!doc) return null;
|
|
1934
|
+
if (clauseExcl && clauseExcl.includes(doc.collection)) return null;
|
|
1818
1935
|
return {
|
|
1819
1936
|
filepath: `clawmem://${doc.collection}/${doc.path}`,
|
|
1820
1937
|
displayPath: `${doc.collection}/${doc.path}`,
|
|
@@ -1835,6 +1952,7 @@ This is the recommended entry point for ALL memory queries.`,
|
|
|
1835
1952
|
}
|
|
1836
1953
|
clauseDetails.push({ type: clause.type, query: clause.query, priority: clause.priority, resultCount: results.length });
|
|
1837
1954
|
allResults.push(...results);
|
|
1955
|
+
clauseIdx++;
|
|
1838
1956
|
}
|
|
1839
1957
|
|
|
1840
1958
|
// Deduplicate by filepath, keeping highest score
|
|
@@ -1863,6 +1981,8 @@ This is the recommended entry point for ALL memory queries.`,
|
|
|
1863
1981
|
const scored = applyCompositeScoring(enriched, query, coFn).slice(0, limit || 10);
|
|
1864
1982
|
|
|
1865
1983
|
const planSummary = clauseDetails.map(c => ` ${c.type}(p${c.priority}): "${c.query}" → ${c.resultCount} results`).join("\n");
|
|
1984
|
+
const planDegraded = degradedLegs.length > 0 ? { degraded: true as const, degradedLegs } : {};
|
|
1985
|
+
const planDegradedNote = degradedLegs.length > 0 ? `\n${degradedGuidanceText(degradedLegs)}` : "";
|
|
1866
1986
|
|
|
1867
1987
|
if (compact) {
|
|
1868
1988
|
const items = scored.map(r => ({
|
|
@@ -1871,8 +1991,8 @@ This is the recommended entry point for ALL memory queries.`,
|
|
|
1871
1991
|
snippet: (r.body || "").substring(0, 150), content_type: r.contentType, modified_at: r.modifiedAt,
|
|
1872
1992
|
}));
|
|
1873
1993
|
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 },
|
|
1994
|
+
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}` }],
|
|
1995
|
+
structuredContent: { plan: clauseDetails, results: items, ...planDegraded },
|
|
1876
1996
|
};
|
|
1877
1997
|
}
|
|
1878
1998
|
|
|
@@ -1881,8 +2001,8 @@ This is the recommended entry point for ALL memory queries.`,
|
|
|
1881
2001
|
compositeScore: r.compositeScore, context: r.context, snippet: r.body?.slice(0, 300) || '', contentType: r.contentType,
|
|
1882
2002
|
}));
|
|
1883
2003
|
return {
|
|
1884
|
-
content: [{ type: "text", text: `Query Plan (${sortedClauses.length} clauses):\n${planSummary}\n\n${formatSearchSummary(items, query)}` }],
|
|
1885
|
-
structuredContent: { plan: clauseDetails, results: items },
|
|
2004
|
+
content: [{ type: "text", text: `Query Plan (${sortedClauses.length} clauses):\n${planSummary}\n\n${formatSearchSummary(items, query)}${planDegradedNote}` }],
|
|
2005
|
+
structuredContent: { plan: clauseDetails, results: items, ...planDegraded },
|
|
1886
2006
|
};
|
|
1887
2007
|
}
|
|
1888
2008
|
);
|
|
@@ -2650,6 +2770,12 @@ This is the recommended entry point for ALL memory queries.`,
|
|
|
2650
2770
|
}
|
|
2651
2771
|
);
|
|
2652
2772
|
|
|
2773
|
+
return { server, store, closeAllStores };
|
|
2774
|
+
}
|
|
2775
|
+
|
|
2776
|
+
export async function startMcpServer(): Promise<void> {
|
|
2777
|
+
const { server, store, closeAllStores } = buildMcpServer();
|
|
2778
|
+
|
|
2653
2779
|
// ---------------------------------------------------------------------------
|
|
2654
2780
|
// Connect
|
|
2655
2781
|
// ---------------------------------------------------------------------------
|
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,
|