claude-mem-lite 3.7.0 → 3.8.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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +3 -1
- package/README.zh-CN.md +1 -1
- package/hook-update.mjs +102 -2
- package/hook.mjs +403 -373
- package/install.mjs +666 -629
- package/lib/doctor-benchmark.mjs +4 -4
- package/lib/release-digest.mjs +106 -0
- package/lib/search-core.mjs +272 -16
- package/mem-cli.mjs +55 -174
- package/package.json +3 -2
- package/schema.mjs +7 -1
- package/scripts/setup.sh +2 -0
- package/search-engine.mjs +1 -1
- package/{server-internals.mjs → search-scoring.mjs} +6 -2
- package/server.mjs +72 -293
- package/source-files.mjs +5 -1
package/server.mjs
CHANGED
|
@@ -8,12 +8,12 @@ import { ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
|
8
8
|
import { truncate, typeIcon, inferProject, scrubSecrets, fmtDate, debugLog, debugCatch, isPathConfined } from './utils.mjs';
|
|
9
9
|
import { resolveProject as _resolveProjectShared } from './project-utils.mjs';
|
|
10
10
|
import { ensureDb, DB_PATH, DB_DIR, REGISTRY_DB_PATH } from './schema.mjs';
|
|
11
|
-
import { reRankWithContext, markSuperseded, autoBoostIfNeeded, runIdleCleanup, buildServerInstructions } from './
|
|
12
|
-
import { searchObservationsHybrid
|
|
11
|
+
import { reRankWithContext, markSuperseded, autoBoostIfNeeded, runIdleCleanup, buildServerInstructions } from './search-scoring.mjs';
|
|
12
|
+
import { searchObservationsHybrid } from './search-engine.mjs';
|
|
13
13
|
import { deepSearch, resolveDeepMode, shouldEscalateToDeep, autoDeepLlmReady } from './deep-search.mjs';
|
|
14
14
|
import { selectCompressionCandidates, groupByProjectWeek, compressGroup } from './lib/compress-core.mjs';
|
|
15
15
|
import { resolveAnchorToken, formatAnchorError, resolveQueryAnchor, fetchRecentTimeline, fetchTimelineWindow } from './lib/timeline-core.mjs';
|
|
16
|
-
import { buildSearchFtsQuery, parseDateBounds,
|
|
16
|
+
import { buildSearchFtsQuery, parseDateBounds, coreRunSearchPipeline } from './lib/search-core.mjs';
|
|
17
17
|
import {
|
|
18
18
|
cleanupBroken, decayAndMarkIdle, boostAccessed, demotePinned, mergeDuplicates,
|
|
19
19
|
purgeStale, purgeStalePreview, findDuplicates, maintenanceStats, rebuildVectors, vacuum,
|
|
@@ -177,91 +177,9 @@ function safeHandler(fn) {
|
|
|
177
177
|
// Observation-search core (FTS query/params builders, hybrid pipeline) lives in
|
|
178
178
|
// search-engine.mjs so mem-cli.mjs gets the identical implementation.
|
|
179
179
|
|
|
180
|
-
//
|
|
181
|
-
// (
|
|
182
|
-
//
|
|
183
|
-
// falls back to the module-level db for the normal MCP handler path.
|
|
184
|
-
function searchObservations(ctx) {
|
|
185
|
-
return searchObservationsHybrid(ctx.db ?? db, ctx);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
function searchSessions(ctx) {
|
|
189
|
-
const _db = ctx.db ?? db;
|
|
190
|
-
const { ftsQuery, searchType, args, epochFrom, epochTo, perSourceLimit, perSourceOffset, currentProject } = ctx;
|
|
191
|
-
const results = [];
|
|
192
|
-
|
|
193
|
-
if (ftsQuery) {
|
|
194
|
-
const rows = searchSessionsFts(_db, {
|
|
195
|
-
ftsQuery, project: args.project ?? null,
|
|
196
|
-
projectBoost: args.project ? null : currentProject,
|
|
197
|
-
epochFrom, epochTo, perSourceLimit, perSourceOffset,
|
|
198
|
-
});
|
|
199
|
-
for (const r of rows) {
|
|
200
|
-
results.push({ source: 'session', id: r.id, request: r.request, completed: r.completed, project: r.project, date: r.created_at, created_at_epoch: r.created_at_epoch, score: r.score });
|
|
201
|
-
}
|
|
202
|
-
} else if (!searchType) {
|
|
203
|
-
// Skip sessions in unfiltered no-query mode (too noisy)
|
|
204
|
-
} else {
|
|
205
|
-
const params = [];
|
|
206
|
-
const wheres = [];
|
|
207
|
-
if (args.project) { wheres.push('project = ?'); params.push(args.project); }
|
|
208
|
-
if (epochFrom !== null) { wheres.push('created_at_epoch >= ?'); params.push(epochFrom); }
|
|
209
|
-
if (epochTo !== null) { wheres.push('created_at_epoch <= ?'); params.push(epochTo); }
|
|
210
|
-
const where = wheres.length ? `WHERE ${wheres.join(' AND ')}` : '';
|
|
211
|
-
params.push(perSourceLimit, perSourceOffset);
|
|
212
|
-
const rows = _db.prepare(`
|
|
213
|
-
SELECT id, request, completed, project, created_at, created_at_epoch
|
|
214
|
-
FROM session_summaries ${where}
|
|
215
|
-
ORDER BY created_at_epoch DESC
|
|
216
|
-
LIMIT ? OFFSET ?
|
|
217
|
-
`).all(...params);
|
|
218
|
-
for (const r of rows) {
|
|
219
|
-
results.push({ source: 'session', id: r.id, request: r.request, completed: r.completed, project: r.project, date: r.created_at, created_at_epoch: r.created_at_epoch });
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
return results;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
function searchPrompts(ctx) {
|
|
227
|
-
const _db = ctx.db ?? db;
|
|
228
|
-
const { ftsQuery, searchType, args, epochFrom, epochTo, perSourceLimit, perSourceOffset } = ctx;
|
|
229
|
-
const results = [];
|
|
230
|
-
|
|
231
|
-
if (ftsQuery) {
|
|
232
|
-
// CJK precision gate + LIKE fallback live in the shared core (see
|
|
233
|
-
// lib/search-core.mjs for the leak rationale).
|
|
234
|
-
const rows = searchPromptsFts(_db, {
|
|
235
|
-
query: args.query, ftsQuery, project: args.project ?? null,
|
|
236
|
-
epochFrom, epochTo, perSourceLimit, perSourceOffset,
|
|
237
|
-
});
|
|
238
|
-
for (const r of rows) {
|
|
239
|
-
results.push({ source: 'prompt', id: r.id, text: r.prompt_text, session: r.content_session_id, date: r.created_at, created_at_epoch: r.created_at_epoch, score: r.score });
|
|
240
|
-
}
|
|
241
|
-
} else if (searchType === 'prompts') {
|
|
242
|
-
const params = [];
|
|
243
|
-
const wheres = [];
|
|
244
|
-
if (args.project) { wheres.push('s.project = ?'); params.push(args.project); }
|
|
245
|
-
if (epochFrom !== null) { wheres.push('p.created_at_epoch >= ?'); params.push(epochFrom); }
|
|
246
|
-
if (epochTo !== null) { wheres.push('p.created_at_epoch <= ?'); params.push(epochTo); }
|
|
247
|
-
const where = wheres.length ? `WHERE ${wheres.join(' AND ')}` : '';
|
|
248
|
-
params.push(perSourceLimit, perSourceOffset);
|
|
249
|
-
const rows = _db.prepare(`
|
|
250
|
-
SELECT p.id, p.prompt_text, p.content_session_id, p.created_at, p.created_at_epoch
|
|
251
|
-
FROM user_prompts p
|
|
252
|
-
JOIN sdk_sessions s ON p.content_session_id = s.content_session_id
|
|
253
|
-
${where}
|
|
254
|
-
ORDER BY p.created_at_epoch DESC
|
|
255
|
-
LIMIT ? OFFSET ?
|
|
256
|
-
`).all(...params);
|
|
257
|
-
for (const r of rows) {
|
|
258
|
-
results.push({ source: 'prompt', id: r.id, text: r.prompt_text, session: r.content_session_id, date: r.created_at, created_at_epoch: r.created_at_epoch });
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
return results;
|
|
263
|
-
}
|
|
264
|
-
|
|
180
|
+
// searchObservations / searchSessions / searchPrompts were consolidated into the
|
|
181
|
+
// shared coreRunSearchPipeline (lib/search-core.mjs). This surface is now a thin
|
|
182
|
+
// adapter (runSearchPipeline below); only output formatting stays local.
|
|
265
183
|
function formatSearchOutput(paginatedResults, args, ftsQuery, totalCount, orFallbackFired = false, isDeepSearch = false) {
|
|
266
184
|
if (paginatedResults.length === 0) {
|
|
267
185
|
const hint = [];
|
|
@@ -339,213 +257,74 @@ export async function handleSearchForTest(db, args, { llm, rerankLlm } = {}) {
|
|
|
339
257
|
}
|
|
340
258
|
|
|
341
259
|
async function runSearchPipeline(db, args, { llm, rerankLlm } = {}) {
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
const deepMode = resolveDeepMode(args.deep, { surface: 'mcp' });
|
|
366
|
-
// Opt-in LLM rerank (D#43): explicit-deep only — never on AUTO escalation — so
|
|
367
|
-
// no default search behaviour changes. Parity with CLI `search --deep --rerank`.
|
|
368
|
-
const rerank = args.rerank === true && deepMode === 'deep';
|
|
369
|
-
|
|
370
|
-
// Early return when query was provided but sanitized to nothing (all FTS5
|
|
371
|
-
// keywords/special chars). Skipped for deep/auto — deep's LLM rewrite may
|
|
372
|
-
// still produce searchable variants from a query the FTS sanitizer rejects,
|
|
373
|
-
// and auto could escalate similarly.
|
|
374
|
-
if (args.query && !ftsQuery && !epochFrom && !epochTo && !args.obs_type && !args.importance && deepMode === 'normal') {
|
|
375
|
-
return { ...formatSearchOutput([], args, ftsQuery, 0), escalated: false, results: [], total: 0, variants: null };
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
// When obs_type is specified, implicitly restrict to observations only.
|
|
379
|
-
// deep mode is observations-only too (deepSearch fuses hybrid-obs lists).
|
|
380
|
-
const effectiveType = deepMode === 'deep' ? 'observations' : (searchType || (args.obs_type ? 'observations' : undefined));
|
|
381
|
-
const isCrossSource = !effectiveType;
|
|
382
|
-
const ctx = { db, ftsQuery, searchType: effectiveType, args, epochFrom, epochTo, perSourceLimit, perSourceOffset, currentProject, limit };
|
|
383
|
-
const results = [];
|
|
384
|
-
let deepVariants = null;
|
|
385
|
-
let deepReranked = false;
|
|
386
|
-
let isDeep = deepMode === 'deep';
|
|
387
|
-
let escalated = false;
|
|
388
|
-
let escalatedObsCount = 0;
|
|
389
|
-
|
|
390
|
-
// Helper: run deepSearch and load results into the shared `results` array.
|
|
391
|
-
const runDeepInto = async ({ auto = false } = {}) => {
|
|
392
|
-
const { results: deepRows, variants, reranked } = await deepSearch(db, {
|
|
393
|
-
query: args.query,
|
|
394
|
-
project: args.project || null,
|
|
395
|
-
type: args.obs_type || null,
|
|
396
|
-
importance: args.importance || null,
|
|
397
|
-
branch: args.branch || null,
|
|
398
|
-
includeNoise: args.include_noise === true,
|
|
399
|
-
epochFrom, epochTo,
|
|
400
|
-
limit: perSourceLimit,
|
|
401
|
-
currentProject,
|
|
402
|
-
}, llm ? { llm, rerank: rerank && !auto, rerankLlm } : { auto, rerank: rerank && !auto, rerankLlm });
|
|
403
|
-
// Safe to reset: sessions/prompts are pushed AFTER the obs block, so nothing is lost here.
|
|
404
|
-
results.length = 0;
|
|
405
|
-
results.push(...deepRows);
|
|
406
|
-
deepVariants = variants;
|
|
407
|
-
deepReranked = reranked;
|
|
408
|
-
};
|
|
409
|
-
|
|
410
|
-
if (!effectiveType || effectiveType === 'observations') {
|
|
411
|
-
if (deepMode === 'deep') {
|
|
412
|
-
// Opt-in LLM multi-query/HyDE deep search: rewrite → per-variant hybrid
|
|
413
|
-
// search → RRF fusion, collapsing to the single query (== baseline) when
|
|
414
|
-
// the rewrite yields nothing (deep-search.mjs). Over-fetch perSourceLimit
|
|
415
|
-
// so the pagination slice below has room.
|
|
416
|
-
await runDeepInto();
|
|
417
|
-
} else {
|
|
418
|
-
results.push(...searchObservations(ctx));
|
|
419
|
-
// Auto-escalate: if normal search is weak (too few results or OR fallback
|
|
420
|
-
// fired — a vocabulary-mismatch symptom), escalate to deep. ctx is mutated
|
|
421
|
-
// by searchObservations to set ctx.orFallbackFired when the AND→OR relaxation
|
|
422
|
-
// fires, so we read it here after the call.
|
|
423
|
-
// results is already obs-only here (sessions/prompts pushed below), but the
|
|
424
|
-
// filter makes the invariant explicit and robust to future reordering.
|
|
425
|
-
const obsCountBeforeEscalation = results.length;
|
|
426
|
-
if (deepMode === 'auto' && autoDeepLlmReady(process.env, llm) && shouldEscalateToDeep(results.filter(r => r.source === 'obs'), ctx, { db, project: args.project || null })) {
|
|
427
|
-
await runDeepInto({ auto: true });
|
|
428
|
-
isDeep = true;
|
|
429
|
-
escalated = true;
|
|
430
|
-
escalatedObsCount = obsCountBeforeEscalation;
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
// Sessions and prompts are excluded when deep (obs-only invariant, #8735).
|
|
435
|
-
if ((!effectiveType || effectiveType === 'sessions') && !isDeep) results.push(...searchSessions(ctx));
|
|
436
|
-
if ((!effectiveType || effectiveType === 'prompts') && !isDeep) results.push(...searchPrompts(ctx));
|
|
437
|
-
|
|
438
|
-
// Type-list fallback: when obs_type is specified and FTS finds nothing,
|
|
439
|
-
// list recent observations of that type (user likely wants to browse by type)
|
|
440
|
-
if (results.length === 0 && args.obs_type) {
|
|
441
|
-
const typeWheres = ['COALESCE(compressed_into, 0) = 0', 'superseded_at IS NULL', 'type = ?'];
|
|
442
|
-
const typeParams = [args.obs_type];
|
|
443
|
-
if (args.project) { typeWheres.push('project = ?'); typeParams.push(args.project); }
|
|
444
|
-
if (epochFrom !== null) { typeWheres.push('created_at_epoch >= ?'); typeParams.push(epochFrom); }
|
|
445
|
-
if (epochTo !== null) { typeWheres.push('created_at_epoch <= ?'); typeParams.push(epochTo); }
|
|
446
|
-
if (args.importance) { typeWheres.push('COALESCE(importance, 1) >= ?'); typeParams.push(args.importance); }
|
|
447
|
-
typeParams.push(limit);
|
|
448
|
-
const typeRows = db.prepare(`
|
|
449
|
-
SELECT id, type, title, subtitle, project, created_at, importance, files_modified
|
|
450
|
-
FROM observations WHERE ${typeWheres.join(' AND ')}
|
|
451
|
-
ORDER BY created_at_epoch DESC LIMIT ?
|
|
452
|
-
`).all(...typeParams);
|
|
453
|
-
for (const r of typeRows) {
|
|
454
|
-
results.push({ source: 'obs', id: r.id, type: r.type, title: r.title, subtitle: r.subtitle, project: r.project, date: r.created_at, importance: r.importance, files_modified: r.files_modified, score: 0, snippet: '' });
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
// Cross-source score normalization (shared with CLI — lib/search-core.mjs):
|
|
459
|
-
// normalize each source to [-1, 0] before merging so observations (BM25 can
|
|
460
|
-
// reach -40) don't systematically outrank sessions (-6) and prompts (-1).
|
|
461
|
-
if (isCrossSource && results.length > 0 && ftsQuery) {
|
|
462
|
-
normalizeCrossSourceScores(results, 'source');
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
// Global sort (cross-source)
|
|
466
|
-
if (isCrossSource && results.length > 0) {
|
|
467
|
-
if (ftsQuery) {
|
|
468
|
-
results.sort((a, b) => (a.score ?? 0) - (b.score ?? 0));
|
|
469
|
-
} else {
|
|
470
|
-
results.sort((a, b) => (b.created_at_epoch ?? 0) - (a.created_at_epoch ?? 0));
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
// Re-rank observations by file context overlap and mark superseded.
|
|
475
|
-
// markSuperseded is pure correctness (stale-tag) and must run for deep results
|
|
476
|
-
// too, including the case where the ORIGINAL query sanitized to an empty
|
|
477
|
-
// ftsQuery but the rewrite still returned rows (F2). reRankWithContext + the
|
|
478
|
-
// re-sort are FTS-rank operations; deep rows are already RRF-ranked, so on the
|
|
479
|
-
// empty-ftsQuery deep path we tag-but-don't-reorder (keep RRF order).
|
|
480
|
-
if ((ftsQuery || isDeep) && results.some(r => r.source === 'obs')) {
|
|
481
|
-
const obsResults = results.filter(r => r.source === 'obs');
|
|
482
|
-
// When the deep candidates were explicitly LLM-reranked, that order is final:
|
|
483
|
-
// skip the file-context re-rank + re-sort (they would perturb the rerank order
|
|
484
|
-
// via score multiplication / score-sort). markSuperseded is pure stale-tagging
|
|
485
|
-
// and still runs. (D#43 — parity with the CLI deep path, which keeps array order.)
|
|
486
|
-
if (ftsQuery && !deepReranked) reRankWithContext(db, obsResults, currentProject);
|
|
487
|
-
markSuperseded(obsResults);
|
|
488
|
-
if (ftsQuery && !deepReranked) results.sort((a, b) => (a.score ?? 0) - (b.score ?? 0));
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
// Tier post-filter: batch-lookup full rows and classify (shared with CLI).
|
|
492
|
-
// Classification uses the explicitly-requested project, not the CWD-inferred
|
|
493
|
-
// one — see applyTierFilter for the cross-project rationale.
|
|
494
|
-
if (args.tier) {
|
|
495
|
-
const filtered = applyTierFilter(db, results, { tier: args.tier, sourceKey: 'source', currentProject: args.project || currentProject });
|
|
496
|
-
results.length = 0;
|
|
497
|
-
results.push(...filtered);
|
|
498
|
-
}
|
|
260
|
+
if (args.project) args = { ...args, project: resolveProject(args.project) };
|
|
261
|
+
const limit = args.limit ?? 20;
|
|
262
|
+
const offset = args.offset ?? 0;
|
|
263
|
+
// args.or: force OR from the start (CLI `search --or` parity). The default path
|
|
264
|
+
// still does AND with the engine's OR-fallback when AND returns 0.
|
|
265
|
+
const ftsQuery = buildSearchFtsQuery(args.query, { or: args.or });
|
|
266
|
+
const currentProject = inferProject();
|
|
267
|
+
|
|
268
|
+
const bounds = parseDateBounds(args.date_from, args.date_to);
|
|
269
|
+
if (!bounds.ok) throw new Error(`Invalid date_${bounds.bad}: "${bounds.value}" (use ISO 8601 or YYYY-MM-DD)`);
|
|
270
|
+
const { epochFrom, epochTo } = bounds;
|
|
271
|
+
|
|
272
|
+
// MCP defaults to 'auto' (escalate on weak results) unless overridden by
|
|
273
|
+
// args.deep or CLAUDE_MEM_AUTO_DEEP. Rerank is explicit-deep only (D#43).
|
|
274
|
+
const deepMode = resolveDeepMode(args.deep, { surface: 'mcp' });
|
|
275
|
+
const rerank = args.rerank === true && deepMode === 'deep';
|
|
276
|
+
|
|
277
|
+
// Early return when query was provided but sanitized to nothing (all FTS5
|
|
278
|
+
// keywords/special chars). Skipped for deep/auto (the LLM rewrite may still
|
|
279
|
+
// produce variants) and for filter-only listings (date/obs_type/importance).
|
|
280
|
+
if (args.query && !ftsQuery && !epochFrom && !epochTo && !args.obs_type && !args.importance && deepMode === 'normal') {
|
|
281
|
+
return { ...formatSearchOutput([], args, ftsQuery, 0), escalated: false, results: [], total: 0, variants: null };
|
|
282
|
+
}
|
|
499
283
|
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
:
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
// Discoverability signal for the opt-in rerank (D#43): tell the calling agent the
|
|
542
|
-
// candidates were LLM-reranked — parity with the CLI stderr note.
|
|
543
|
-
if (deepReranked && output.content?.[0]?.type === 'text') {
|
|
544
|
-
output.content[0].text += '\n\n[deep search: LLM-reranked the top candidates by relevance]';
|
|
545
|
-
}
|
|
284
|
+
// obs_type ⇒ observations-only; deep is observations-only too (deepSearch fuses
|
|
285
|
+
// hybrid-obs lists). args.type is the source filter (observations|sessions|prompts).
|
|
286
|
+
const effectiveType = deepMode === 'deep' ? 'observations' : (args.type || (args.obs_type ? 'observations' : undefined));
|
|
287
|
+
|
|
288
|
+
const r = await coreRunSearchPipeline(
|
|
289
|
+
{
|
|
290
|
+
db, currentProject, env: process.env,
|
|
291
|
+
searchObservationsHybrid, deepSearch, shouldEscalateToDeep, autoDeepLlmReady,
|
|
292
|
+
reRankWithContext, markSuperseded, llm, rerankLlm,
|
|
293
|
+
},
|
|
294
|
+
{
|
|
295
|
+
query: args.query, ftsQuery, effectiveSource: effectiveType, deepMode, rerank,
|
|
296
|
+
limit, offset, project: args.project ?? null, obsType: args.obs_type ?? null,
|
|
297
|
+
importance: args.importance ?? null, branch: args.branch ?? null,
|
|
298
|
+
includeNoise: args.include_noise === true, epochFrom, epochTo,
|
|
299
|
+
sort: args.sort || 'relevance', tier: args.tier ?? null,
|
|
300
|
+
// ── MCP surface policy ──
|
|
301
|
+
obsTypeFallback: true, // list-recent-by-type when 0 matches
|
|
302
|
+
crossSourceEpochSortNoFts: true, // epoch-sort cross-source with no ftsQuery
|
|
303
|
+
rerankPolicy: 'mcp', // (ftsQuery||isDeep) gate; re-rank/re-sort on ftsQuery&&!reranked
|
|
304
|
+
rerankProject: currentProject,
|
|
305
|
+
recentListingNoFts: true, // recent-listing for explicit --source with no ftsQuery
|
|
306
|
+
tolerateMissingFts: false,
|
|
307
|
+
tierPosition: 'late', // tier filter after re-rank
|
|
308
|
+
tierProject: args.project || currentProject,
|
|
309
|
+
}
|
|
310
|
+
);
|
|
311
|
+
|
|
312
|
+
// Observability: announce auto-escalation on stderr (parity with CLI deep note).
|
|
313
|
+
if (r.escalated) process.stderr.write(`[mem] auto-escalated to deep search (weak results: ${r.escalatedObsCount} hits)\n`);
|
|
314
|
+
|
|
315
|
+
const output = formatSearchOutput(r.page, args, ftsQuery, r.total, r.orFallbackFired, r.isDeep);
|
|
316
|
+
// Surface the rewrite to the calling agent (F13) + the rerank signal (D#43).
|
|
317
|
+
if (r.isDeep && r.variants && output.content?.[0]?.type === 'text') {
|
|
318
|
+
output.content[0].text += r.variants.length > 1
|
|
319
|
+
? `\n\n[deep search: rewrote into ${r.variants.length} variants — ${r.variants.slice(1).map(v => JSON.stringify(v)).join(', ')}]`
|
|
320
|
+
: '\n\n[deep search: rewrite produced no usable variants; searched the original query only (== baseline)]';
|
|
321
|
+
}
|
|
322
|
+
if (r.reranked && output.content?.[0]?.type === 'text') {
|
|
323
|
+
output.content[0].text += '\n\n[deep search: LLM-reranked the top candidates by relevance]';
|
|
324
|
+
}
|
|
546
325
|
|
|
547
|
-
|
|
548
|
-
|
|
326
|
+
// Expose structured fields for tests + the MCP content blob.
|
|
327
|
+
return { ...output, results: r.page, total: r.total, escalated: r.escalated, variants: r.variants, reranked: r.reranked };
|
|
549
328
|
}
|
|
550
329
|
|
|
551
330
|
server.registerTool(
|
package/source-files.mjs
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
export const SOURCE_FILES = [
|
|
8
8
|
// Entry points and top-level modules
|
|
9
|
-
'cli.mjs', 'cli-path.mjs', 'server.mjs', '
|
|
9
|
+
'cli.mjs', 'cli-path.mjs', 'server.mjs', 'search-scoring.mjs', 'search-engine.mjs', 'deep-search.mjs', 'rerank.mjs', 'tool-schemas.mjs',
|
|
10
10
|
'hook.mjs', 'hook-shared.mjs', 'hook-llm.mjs', 'hook-memory.mjs', 'skip-tools.mjs',
|
|
11
11
|
'hook-semaphore.mjs', 'hook-episode.mjs', 'hook-context.mjs', 'hook-handoff.mjs',
|
|
12
12
|
'hook-update.mjs', 'hook-optimize.mjs', 'hook-precompact.mjs',
|
|
@@ -73,6 +73,10 @@ export const SOURCE_FILES = [
|
|
|
73
73
|
// + auto-update lock). Must ship or a partial install/update skips them.
|
|
74
74
|
'lib/proc-lock.mjs',
|
|
75
75
|
'lib/atomic-write.mjs',
|
|
76
|
+
// P1 supply-chain: shared release-signing core (sha256 manifest + Ed25519
|
|
77
|
+
// verify). Imported by hook-update.mjs (verify) + scripts/sign-release.mjs (CI
|
|
78
|
+
// sign). Must ship or auto-update can't verify release signatures.
|
|
79
|
+
'lib/release-digest.mjs',
|
|
76
80
|
// v2.41 god-module split — mem-cli.mjs router + per-cmd handlers under cli/
|
|
77
81
|
'cli/common.mjs',
|
|
78
82
|
'cli/fts-check.mjs',
|