mcvay-mind 1.0.7 → 1.0.8

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/lib/metrics.js CHANGED
@@ -37,6 +37,7 @@ const DEFAULT_STATE = {
37
37
  unified_graph_traversal: { count: 0, total: 0, min: null, max: null, avg: 0 },
38
38
  keyword: { count: 0, total: 0, min: null, max: null, avg: 0 },
39
39
  semantic: { count: 0, total: 0, min: null, max: null, avg: 0 },
40
+ fast_semantic: { count: 0, total: 0, min: null, max: null, avg: 0 },
40
41
  graph: { count: 0, total: 0, min: null, max: null, avg: 0 },
41
42
  format: { count: 0, total: 0, min: null, max: null, avg: 0 },
42
43
  },
@@ -101,6 +102,7 @@ function mergeState(rawState) {
101
102
  unified_graph_traversal: mergeLatency(latency.unified_graph_traversal),
102
103
  keyword: mergeLatency(latency.keyword),
103
104
  semantic: mergeLatency(latency.semantic),
105
+ fast_semantic: mergeLatency(latency.fast_semantic),
104
106
  graph: mergeLatency(latency.graph),
105
107
  format: mergeLatency(latency.format),
106
108
  },
@@ -162,7 +164,16 @@ function recordSearch({
162
164
  } else {
163
165
  state.search.card_mode += 1;
164
166
  }
165
- const modeKey = `${mode || 'hybrid'}`.trim().toLowerCase() || 'hybrid';
167
+ let modeKey = `${mode || 'hybrid'}`.trim().toLowerCase() || 'hybrid';
168
+ if (
169
+ modeKey === 'fast:keyword'
170
+ && latencyBreakdown
171
+ && latencyBreakdown.fastSemanticMs !== null
172
+ && latencyBreakdown.fastSemanticMs !== undefined
173
+ && Number.isFinite(latencyBreakdown.fastSemanticMs)
174
+ ) {
175
+ modeKey = 'fast:hybrid';
176
+ }
166
177
  state.search.by_engine_mode[modeKey] = toNumber(state.search.by_engine_mode[modeKey], 0) + 1;
167
178
  updateLatency(state.latency_ms.search, durationMs);
168
179
  if (latencyBreakdown && typeof latencyBreakdown === 'object') {
@@ -172,6 +183,9 @@ function recordSearch({
172
183
  if (Number.isFinite(latencyBreakdown.semanticMs)) {
173
184
  updateLatency(state.latency_ms.semantic, latencyBreakdown.semanticMs);
174
185
  }
186
+ if (Number.isFinite(latencyBreakdown.fastSemanticMs)) {
187
+ updateLatency(state.latency_ms.fast_semantic, latencyBreakdown.fastSemanticMs);
188
+ }
175
189
  if (Number.isFinite(latencyBreakdown.graphMs)) {
176
190
  updateLatency(state.latency_ms.graph, latencyBreakdown.graphMs);
177
191
  }
package/lib/search.js CHANGED
@@ -39,6 +39,9 @@ const TYPE_PRIORITY = {
39
39
  const SEARCH_CACHE_MAX_ENTRIES = 200;
40
40
  const SEARCH_CACHE_TTL_MS = 60 * 1000;
41
41
  const EXPAND_INTENT_TERMS = new Set(['why', 'how', 'details', 'detail', 'context', 'full', 'explain', 'history']);
42
+ const FAST_SEMANTIC_MAX_TOP_N = 10;
43
+ const FAST_LATENCY_BUDGET_MS = 500;
44
+ const FAST_SEMANTIC_LATENCY_GUARD_MS = 400;
42
45
  const queryCache = new Map();
43
46
 
44
47
  function normalizeQuery(query = '') {
@@ -141,6 +144,14 @@ function shouldRunSemanticForBalanced(keywordResults = [], options = {}) {
141
144
  return topNorm < 0.62 || sparse;
142
145
  }
143
146
 
147
+ function shouldRunSemanticForFast(keywordResults = []) {
148
+ if (!keywordResults.length) return true;
149
+ const top = keywordResults[0];
150
+ const topNorm = normalizeKeywordScore(top && top.score);
151
+ const sparse = keywordResults.length < 3;
152
+ return topNorm < 0.5 || sparse;
153
+ }
154
+
144
155
  // ============================================================================
145
156
  // Keyword Extraction
146
157
  // ============================================================================
@@ -203,6 +214,14 @@ function computeHybridScore(memory, options, keywords, query, cwd, linkCounts =
203
214
  return (keywordNorm * keywordWeight) + (semanticNorm * semanticWeight) + (metadataNorm * 0.1);
204
215
  }
205
216
 
217
+ function computeSimpleFusionScore(memory, options = {}) {
218
+ const keywordWeight = Number.isFinite(options.keywordWeight) ? options.keywordWeight : 0.5;
219
+ const semanticWeight = Number.isFinite(options.semanticWeight) ? options.semanticWeight : 0.5;
220
+ const keywordNorm = normalizeKeywordScore(memory.keywordScore || memory.score || 0);
221
+ const semanticNorm = normalizeSemanticScore(memory.semanticScore || 0);
222
+ return (keywordWeight * keywordNorm) + (semanticWeight * semanticNorm);
223
+ }
224
+
206
225
  function normalizeBm25ForDisplay(score) {
207
226
  if (!Number.isFinite(score)) return 0;
208
227
  return 1 / (1 + Math.max(0, score));
@@ -417,6 +436,7 @@ function fuseResults(keywordResults, semanticResults, options, cwd = process.cwd
417
436
  const keywords = extractKeywords(query);
418
437
  const mode = options.mode || 'hybrid';
419
438
  const limit = options.limit || 10;
439
+ const simpleFusion = !!options.simpleFusion;
420
440
 
421
441
  if (mode === 'keyword') {
422
442
  return keywordResults.slice(0, limit);
@@ -460,7 +480,9 @@ function fuseResults(keywordResults, semanticResults, options, cwd = process.cwd
460
480
  }
461
481
 
462
482
  const fused = Array.from(bySlug.values()).map(m => {
463
- const hybrid = computeHybridScore(m, options, keywords, query, cwd, linkCounts);
483
+ const hybrid = simpleFusion
484
+ ? computeSimpleFusionScore(m, options)
485
+ : computeHybridScore(m, options, keywords, query, cwd, linkCounts);
464
486
  return {
465
487
  ...m,
466
488
  score: Math.round(hybrid * 150),
@@ -578,12 +600,14 @@ async function searchMemories(options, cwd = process.cwd()) {
578
600
  const phaseMs = {
579
601
  keywordMs: 0,
580
602
  semanticMs: 0,
603
+ fastSemanticMs: null,
581
604
  graphMs: 0,
582
605
  formatMs: 0,
583
606
  };
584
607
  let cacheHit = false;
585
608
  let cacheMiss = false;
586
609
  let errored = false;
610
+ let fastSemanticTriggered = false;
587
611
  const cacheKey = buildSearchCacheKey({
588
612
  ...options,
589
613
  query,
@@ -615,7 +639,37 @@ async function searchMemories(options, cwd = process.cwd()) {
615
639
  phaseMs.keywordMs = Date.now() - keywordStart;
616
640
 
617
641
  if (preset === 'fast' || !query || mode === 'keyword') {
618
- ranked = keywordResults;
642
+ const fastGateOpen = preset === 'fast'
643
+ && query
644
+ && shouldRunSemanticForFast(keywordResults)
645
+ && phaseMs.keywordMs < FAST_SEMANTIC_LATENCY_GUARD_MS
646
+ && (Date.now() - startMs) < FAST_LATENCY_BUDGET_MS;
647
+ if (fastGateOpen) {
648
+ fastSemanticTriggered = true;
649
+ const semanticStart = Date.now();
650
+ const fastRun = await runSemanticSearch({
651
+ ...options,
652
+ limit: Math.min(
653
+ FAST_SEMANTIC_MAX_TOP_N,
654
+ Number.isFinite(options.annTopN) ? options.annTopN : FAST_SEMANTIC_MAX_TOP_N,
655
+ ),
656
+ semanticBackend: 'ann',
657
+ }, cwd);
658
+ phaseMs.fastSemanticMs = Date.now() - semanticStart;
659
+ phaseMs.semanticMs += phaseMs.fastSemanticMs;
660
+ const canFuse = fastRun.available && fastRun.results.length > 0;
661
+ ranked = canFuse
662
+ ? fuseResults(
663
+ keywordResults,
664
+ fastRun.results,
665
+ { ...options, simpleFusion: true, mode: 'hybrid' },
666
+ cwd,
667
+ linkCounts,
668
+ )
669
+ : keywordResults;
670
+ } else {
671
+ ranked = keywordResults;
672
+ }
619
673
  } else if (preset === 'balanced') {
620
674
  const shouldSemantic = shouldRunSemanticForBalanced(keywordResults, options);
621
675
  if (shouldSemantic) {
@@ -686,7 +740,7 @@ async function searchMemories(options, cwd = process.cwd()) {
686
740
 
687
741
  metrics.recordSearch({
688
742
  resultMode: resultModeForMetrics,
689
- mode: `${preset}:${mode}`,
743
+ mode: (preset === 'fast' && fastSemanticTriggered) ? 'fast:hybrid' : `${preset}:${mode}`,
690
744
  durationMs: Date.now() - startMs,
691
745
  errored,
692
746
  latencyBreakdown: phaseMs,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcvay-mind",
3
- "version": "1.0.7",
3
+ "version": "1.0.8",
4
4
  "description": "Typed memory system with search, recall, and response guidance for agent workflows.",
5
5
  "main": "index.js",
6
6
  "bin": {