@melandlabs/memory-store 1.1.8 → 1.2.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.
@@ -0,0 +1,145 @@
1
+ import { isDerivedKind } from '@melandlabs/contracts/derived-fact';
2
+
3
+ // src/search/derive.ts
4
+
5
+ // src/search/utilities.ts
6
+ function deriveLexicalKeywords(query) {
7
+ return query.toLowerCase().split(/[^\p{L}\p{N}]+/u).filter((token) => token.length >= 2).slice(0, 16);
8
+ }
9
+ var DEFAULT_SOURCES = ["memory", "insights", "knowledge"];
10
+ new Set(DEFAULT_SOURCES);
11
+ function toMemoryResult(result) {
12
+ return {
13
+ type: "memory",
14
+ id: result.id,
15
+ content: result.content,
16
+ similarity: result.similarity,
17
+ metadata: result.metadata
18
+ };
19
+ }
20
+ function isRawMemorySemanticResult(result) {
21
+ if (!result || typeof result !== "object") {
22
+ return false;
23
+ }
24
+ const item = result;
25
+ return typeof item.id === "string" && typeof item.content === "string" && typeof item.similarity === "number" && Boolean(item.metadata) && typeof item.metadata === "object";
26
+ }
27
+
28
+ // src/search/derive.ts
29
+ var DEFAULT_CANDIDATE_LIMIT = 50;
30
+ async function deriveFacts(deps, input, logger = console) {
31
+ const warnings = [];
32
+ if (typeof deps.deriver !== "function") {
33
+ warnings.push({
34
+ code: "derive_deriver_not_configured",
35
+ message: "No `deriver` is wired into the unified search deps; returning an empty fact list."
36
+ });
37
+ return { facts: [], warnings };
38
+ }
39
+ const limit = Math.max(1, Math.min(500, input.candidateLimit ?? DEFAULT_CANDIDATE_LIMIT));
40
+ if (!input.candidateTexts || input.candidateTexts.length === 0) {
41
+ if (!input.query) {
42
+ warnings.push({
43
+ code: "derive_fallback_query_noise",
44
+ message: "No `query` supplied and no `candidateTexts`; lexical fallback derives keywords from `userId + botIds`, which is rarely useful. Pass `query` (or `candidateTexts`) for topical derivation."
45
+ });
46
+ }
47
+ }
48
+ let candidateTexts;
49
+ if (input.candidateTexts && input.candidateTexts.length > 0) {
50
+ candidateTexts = input.candidateTexts;
51
+ } else {
52
+ candidateTexts = await loadCandidateTexts(deps, input, limit, logger);
53
+ if (candidateTexts.length === 0) {
54
+ warnings.push({
55
+ code: "derive_no_candidates",
56
+ message: "No candidate fact texts available for derivation"
57
+ });
58
+ return { facts: [], warnings };
59
+ }
60
+ }
61
+ let facts;
62
+ try {
63
+ facts = await deps.deriver({
64
+ userId: input.userId,
65
+ userScope: {
66
+ userId: input.userId,
67
+ botIds: input.botIds,
68
+ dateFrom: input.dateFrom,
69
+ dateTo: input.dateTo
70
+ },
71
+ recentFactTexts: candidateTexts,
72
+ ...input.window ? { window: input.window } : {}
73
+ });
74
+ } catch (error) {
75
+ logger.warn?.("[memory-store/derive] deriver threw:", error);
76
+ warnings.push({
77
+ code: "derive_deriver_failed",
78
+ message: error.message ?? "deriver failed"
79
+ });
80
+ return { facts: [], warnings };
81
+ }
82
+ if (!Array.isArray(facts)) {
83
+ warnings.push({
84
+ code: "derive_deriver_returned_invalid_shape",
85
+ message: `deriver returned ${typeof facts}; expected DerivedFact[]`
86
+ });
87
+ return { facts: [], warnings };
88
+ }
89
+ const normalized = [];
90
+ const now = Date.now();
91
+ for (const fact of facts) {
92
+ if (!fact || typeof fact.text !== "string" || fact.text.length === 0 || typeof fact.kind !== "string" || !isDerivedKind(fact.kind) || !Array.isArray(fact.sources)) {
93
+ continue;
94
+ }
95
+ normalized.push({
96
+ ...fact,
97
+ derivedAt: typeof fact.derivedAt === "number" ? fact.derivedAt : now,
98
+ sources: fact.sources.filter((s) => typeof s === "string")
99
+ });
100
+ }
101
+ if (input.persist && normalized.length > 0) {
102
+ try {
103
+ await input.persist(normalized);
104
+ } catch (error) {
105
+ logger.warn?.("[memory-store/derive] persist callback threw:", error);
106
+ warnings.push({
107
+ code: "derive_persist_failed",
108
+ message: error.message ?? "derive persist failed"
109
+ });
110
+ }
111
+ }
112
+ return { facts: normalized, warnings };
113
+ }
114
+ async function loadCandidateTexts(deps, input, limit, logger) {
115
+ if (typeof deps.searchRawMessagesLexical !== "function") {
116
+ return [];
117
+ }
118
+ const queryText = (input.query ?? `${input.userId} ${input.botIds?.join(" ") ?? ""}`).trim();
119
+ const keywords = deriveLexicalKeywords(queryText);
120
+ if (keywords.length === 0) {
121
+ return [];
122
+ }
123
+ try {
124
+ const filters = input.botIds && input.botIds.length > 0 ? input.botIds : [void 0];
125
+ const factTypes = input.factTypes?.length ? input.factTypes : void 0;
126
+ const hits = (await Promise.all(
127
+ filters.map(
128
+ (botId) => deps.searchRawMessagesLexical?.({
129
+ userId: input.userId,
130
+ keywords,
131
+ limit: Math.ceil(limit / filters.length),
132
+ botId,
133
+ ...input.peers && input.peers.length > 0 ? { peers: input.peers } : {},
134
+ ...factTypes ? { factTypes } : {}
135
+ })
136
+ )
137
+ )).flat().filter(isRawMemorySemanticResult);
138
+ return hits.map((h) => toMemoryResult(h).content);
139
+ } catch (error) {
140
+ logger.warn?.("[memory-store/derive] lexical candidate fetch failed:", error);
141
+ return [];
142
+ }
143
+ }
144
+
145
+ export { deriveFacts };
@@ -0,0 +1,60 @@
1
+ import { EntityEdge } from '@melandlabs/contracts/entity-edge';
2
+ import { U as UnifiedSearchDeps } from '../config-DfvVZ--0.js';
3
+ import '@melandlabs/memory-consolidation';
4
+ import '@melandlabs/contracts/peer';
5
+ import '@melandlabs/contracts';
6
+ import '@melandlabs/contracts/derived-fact';
7
+
8
+ /**
9
+ * `distill` primitive — extract `EntityEdge`s from a single raw message.
10
+ *
11
+ * The host wires in an LLM extractor via `UnifiedSearchDeps.entityExtractor`.
12
+ * When the dep is absent, the primitive short-circuits with a
13
+ * `distill_extractor_not_configured` warning — matching the
14
+ * `memory_iterative_planner_not_configured` precedent so callers can
15
+ * detect the degraded mode uniformly.
16
+ *
17
+ * Persistence is intentionally the host's responsibility: the SDK
18
+ * surfaces the edges plus an optional `persist` callback that the
19
+ * caller supplies on the input. This keeps the memory-store decoupled
20
+ * from any entity-store schema and lets hosts reuse their existing
21
+ * graph nodes / SQLite tables / Loop-engine queues without an extra
22
+ * migration.
23
+ *
24
+ * The module is intentionally pure (no I/O of its own beyond calling
25
+ * the extractor and `persist`). That keeps it unit-testable with a
26
+ * fake extractor and a counter on `persist`.
27
+ */
28
+
29
+ interface DistillInput {
30
+ userId: string;
31
+ messageId: string;
32
+ content: string;
33
+ /**
34
+ * Optional host-side persistence callback. Invoked exactly once
35
+ * with the extracted edges when an extractor is wired in. Hosts
36
+ * that want to batch-distill should call `distillRawMessage` in a
37
+ * loop and persist themselves.
38
+ */
39
+ persist?: (edges: EntityEdge[]) => Promise<void>;
40
+ }
41
+ interface DistillWarning {
42
+ code: string;
43
+ message: string;
44
+ }
45
+ interface DistillOutput {
46
+ edges: EntityEdge[];
47
+ warnings: DistillWarning[];
48
+ }
49
+ /**
50
+ * Run the entity extractor over a single raw message.
51
+ *
52
+ * - No extractor configured → `{ edges: [], warnings: [...] }`.
53
+ * - Extractor configured → call it, await `persist` if provided,
54
+ * return the edges. Errors in the extractor (or the persist
55
+ * callback) are surfaced as warnings and the function never
56
+ * throws — distill is best-effort by design.
57
+ */
58
+ declare function distillRawMessage(deps: Pick<UnifiedSearchDeps, "entityExtractor">, input: DistillInput, logger?: Pick<Console, "warn">): Promise<DistillOutput>;
59
+
60
+ export { type DistillInput, type DistillOutput, type DistillWarning, distillRawMessage };
@@ -0,0 +1,61 @@
1
+ import { isEntityKind } from '@melandlabs/contracts/entity-edge';
2
+
3
+ // src/search/distill.ts
4
+ async function distillRawMessage(deps, input, logger = console) {
5
+ const warnings = [];
6
+ if (typeof deps.entityExtractor !== "function") {
7
+ warnings.push({
8
+ code: "distill_extractor_not_configured",
9
+ message: "No `entityExtractor` is wired into the unified search deps; returning an empty entity list."
10
+ });
11
+ return { edges: [], warnings };
12
+ }
13
+ let edges;
14
+ try {
15
+ edges = await deps.entityExtractor({
16
+ userId: input.userId,
17
+ messageId: input.messageId,
18
+ content: input.content
19
+ });
20
+ } catch (error) {
21
+ logger.warn?.("[memory-store/distill] extractor threw:", error);
22
+ warnings.push({
23
+ code: "distill_extractor_failed",
24
+ message: error.message ?? "entity extractor failed"
25
+ });
26
+ return { edges: [], warnings };
27
+ }
28
+ if (!Array.isArray(edges)) {
29
+ warnings.push({
30
+ code: "distill_extractor_returned_invalid_shape",
31
+ message: `entityExtractor returned ${typeof edges}; expected EntityEdge[]`
32
+ });
33
+ return { edges: [], warnings };
34
+ }
35
+ const normalized = [];
36
+ const now = Date.now();
37
+ for (const edge of edges) {
38
+ if (!edge || typeof edge.label !== "string" || edge.label.length === 0 || typeof edge.kind !== "string" || !isEntityKind(edge.kind) || typeof edge.relation !== "string" || typeof edge.sourceMessageId !== "string") {
39
+ continue;
40
+ }
41
+ normalized.push({
42
+ ...edge,
43
+ label: edge.label.trim().toLowerCase(),
44
+ extractedAt: typeof edge.extractedAt === "number" ? edge.extractedAt : now
45
+ });
46
+ }
47
+ if (input.persist && normalized.length > 0) {
48
+ try {
49
+ await input.persist(normalized);
50
+ } catch (error) {
51
+ logger.warn?.("[memory-store/distill] persist callback threw:", error);
52
+ warnings.push({
53
+ code: "distill_persist_failed",
54
+ message: error.message ?? "distill persist failed"
55
+ });
56
+ }
57
+ }
58
+ return { edges: normalized, warnings };
59
+ }
60
+
61
+ export { distillRawMessage };
@@ -1,6 +1,8 @@
1
- export { c as SearchEvidence, d as SearchInput, e as SearchOutput, i as SearchSource, S as SearchTier, j as UnifiedMemoryMergeStrategy, k as UnifiedMemoryRankedList, l as UnifiedMemoryReasoningStrategy, f as UnifiedMemorySearchInput, g as UnifiedMemorySearchOutput, h as UnifiedMemorySearchResult, m as UnifiedMemorySearchSource, b as UnifiedMemorySearchWarning, n as clampUnifiedMemorySearchLimit, o as clampUnifiedMemorySearchThreshold, p as isRawMemorySemanticResult, q as mergeUnifiedMemorySearchResults, r as mergeUnifiedMemorySearchResultsRrf, s as normalizeUnifiedMemoryMergeStrategy, t as normalizeUnifiedMemoryReasoningStrategy, u as normalizeUnifiedMemorySearchSources, v as toKnowledgeResult, w as toMemoryResult } from '../config-CnbqSek5.js';
2
- export { U as UnifiedSearch, c as createUnifiedSearch } from '../unified-search-Bft57TRH.js';
1
+ export { H as HitChannel, i as HitSignals, c as SearchEvidence, d as SearchInput, e as SearchOutput, j as SearchSource, S as SearchTier, k as UnifiedMemoryMergeStrategy, l as UnifiedMemoryRankedList, m as UnifiedMemoryReasoningStrategy, f as UnifiedMemorySearchInput, g as UnifiedMemorySearchOutput, h as UnifiedMemorySearchResult, n as UnifiedMemorySearchSource, b as UnifiedMemorySearchWarning, o as clampUnifiedMemorySearchLimit, p as clampUnifiedMemorySearchThreshold, q as deriveLexicalKeywords, r as isRawMemorySemanticResult, s as listNameToChannel, t as materializeSignals, u as mergeUnifiedMemorySearchResults, v as mergeUnifiedMemorySearchResultsRrf, w as normalizeUnifiedMemoryMergeStrategy, x as normalizeUnifiedMemoryReasoningStrategy, y as normalizeUnifiedMemorySearchSources, z as toKnowledgeResult, A as toMemoryResult } from '../config-DfvVZ--0.js';
2
+ export { U as UnifiedSearch, c as createUnifiedSearch } from '../unified-search-A8Q3JHwU.js';
3
3
  import '@melandlabs/memory-consolidation';
4
4
  import '@melandlabs/contracts/peer';
5
5
  import '@melandlabs/contracts';
6
+ import '@melandlabs/contracts/derived-fact';
7
+ import '@melandlabs/contracts/entity-edge';
6
8
  import '@melandlabs/ai/memory';
@@ -250,6 +250,9 @@ init_chroma_memory_index();
250
250
  init_raw_message_store();
251
251
 
252
252
  // src/search/utilities.ts
253
+ function deriveLexicalKeywords(query) {
254
+ return query.toLowerCase().split(/[^\p{L}\p{N}]+/u).filter((token) => token.length >= 2).slice(0, 16);
255
+ }
253
256
  var DEFAULT_LIMIT = 10;
254
257
  var DEFAULT_THRESHOLD = 0.7;
255
258
  var DEFAULT_SOURCES = ["memory", "insights", "knowledge"];
@@ -363,6 +366,50 @@ function toMemoryResult(result) {
363
366
  metadata: result.metadata
364
367
  };
365
368
  }
369
+ function materializeSignals(lists, hit) {
370
+ const channels = [];
371
+ let semantic;
372
+ let lexical;
373
+ let entity;
374
+ for (const list of lists) {
375
+ const channel = listNameToChannel(list.name);
376
+ if (!channel) continue;
377
+ if (channels.includes(channel)) continue;
378
+ let found;
379
+ for (const candidate of list.hits) {
380
+ if (candidate.type === hit.type && candidate.id === hit.id) {
381
+ found = candidate;
382
+ break;
383
+ }
384
+ }
385
+ if (!found) continue;
386
+ channels.push(channel);
387
+ if (channel === "semantic" && semantic === void 0) {
388
+ semantic = found.similarity;
389
+ } else if (channel === "lexical" && lexical === void 0) {
390
+ lexical = found.similarity;
391
+ } else if (channel === "entity" && entity === void 0) {
392
+ entity = found.similarity;
393
+ }
394
+ }
395
+ if (channels.length === 0) {
396
+ return void 0;
397
+ }
398
+ const out = { channels };
399
+ if (semantic !== void 0) out.semantic = semantic;
400
+ if (lexical !== void 0) out.lexical = lexical;
401
+ if (entity !== void 0) out.entity = entity;
402
+ if (typeof hit.metadata.rrfScore === "number") {
403
+ out.rrf = hit.metadata.rrfScore;
404
+ }
405
+ return out;
406
+ }
407
+ function listNameToChannel(name) {
408
+ if (name === "memory-semantic") return "semantic";
409
+ if (name === "memory-bm25" || name === "memory-lexical") return "lexical";
410
+ if (name === "memory-entity") return "entity";
411
+ return void 0;
412
+ }
366
413
  function isRawMemorySemanticResult(result) {
367
414
  if (!result || typeof result !== "object") {
368
415
  return false;
@@ -782,8 +829,65 @@ async function resolveSearchScopePeers(input) {
782
829
  }
783
830
 
784
831
  // src/search/unified-search.ts
785
- function deriveLexicalKeywords(query) {
786
- return query.toLowerCase().split(/[^\p{L}\p{N}]+/u).filter((token) => token.length >= 2).slice(0, 16);
832
+ function deriveEntityKeywords(query) {
833
+ const proper = Array.from(new Set(query.match(/\b[A-Z][a-zA-Z]{2,}\b/g) ?? [])).map((w) => w.toLowerCase());
834
+ const lexical = deriveLexicalKeywords(query);
835
+ const seen = /* @__PURE__ */ new Set();
836
+ const out = [];
837
+ for (const word of [...proper, ...lexical]) {
838
+ if (word.length < 2) continue;
839
+ if (seen.has(word)) continue;
840
+ seen.add(word);
841
+ out.push(word);
842
+ }
843
+ return out.slice(0, 16);
844
+ }
845
+ async function runEntitySearchForQuery(deps, input, limit, logger, peerPeers = []) {
846
+ if (typeof deps.entitySearch !== "function") {
847
+ return [];
848
+ }
849
+ const keywords = deriveEntityKeywords(input.query);
850
+ if (keywords.length === 0) {
851
+ return [];
852
+ }
853
+ try {
854
+ const entitySearch = deps.entitySearch;
855
+ const filters = input.botIds && input.botIds.length > 0 ? input.botIds : [void 0];
856
+ const matches = (await Promise.all(
857
+ filters.map(
858
+ (botId) => entitySearch({
859
+ userId: input.userId,
860
+ keywords,
861
+ limit: Math.ceil(limit / filters.length),
862
+ botId,
863
+ ...peerPeers.length > 0 ? { peers: peerPeers } : {}
864
+ })
865
+ )
866
+ )).flat();
867
+ return matches.filter(
868
+ (m) => typeof m?.messageId === "string" && typeof m?.label === "string" && typeof m?.score === "number"
869
+ ).map((m) => ({
870
+ type: "memory",
871
+ id: m.messageId,
872
+ // The host's `entitySearch` returns match ids + labels only,
873
+ // so we surface the entity label as `content`. When the same
874
+ // `messageId` is also returned by the semantic/lexical channel
875
+ // (RRF path), the real message content is preserved via the
876
+ // shared `(type, id)` key; otherwise callers can re-fetch the
877
+ // original message by `id` (set in metadata as `sourceMessageId`).
878
+ content: m.label,
879
+ similarity: m.score,
880
+ metadata: {
881
+ scoring: "entity",
882
+ entityLabel: m.label,
883
+ isEntityProjection: true,
884
+ sourceMessageId: m.messageId
885
+ }
886
+ }));
887
+ } catch (error) {
888
+ logger.warn?.("[memory-store] entity search failed:", error);
889
+ return [];
890
+ }
787
891
  }
788
892
  function parseDateBoundary(value, role) {
789
893
  const trimmed = value.trim();
@@ -1223,10 +1327,39 @@ function createUnifiedSearch(deps = {}) {
1223
1327
  });
1224
1328
  }
1225
1329
  }
1226
- return {
1330
+ let entity;
1331
+ if (typeof deps.entitySearch === "function") {
1332
+ entity = filterByDateRange(
1333
+ await runEntitySearchForQuery(deps, input, limit, logger, peerPeers),
1334
+ input.dateFrom,
1335
+ input.dateTo
1336
+ );
1337
+ if (entity.length === 0) {
1338
+ entity = void 0;
1339
+ } else if (normalizeUnifiedMemoryMergeStrategy(
1340
+ input.mergeStrategy ?? deps.reasoning?.defaultMergeStrategy ?? "rrf"
1341
+ ) !== "rrf") {
1342
+ warnings.push({
1343
+ source: "memory",
1344
+ code: "memory_entity_requires_rrf",
1345
+ message: "Entity channel results are fused via RRF; with mergeStrategy:'similarity' standalone entity hits are suppressed (only the entity channel signal is kept on co-occurring hits). Request mergeStrategy:'rrf' to surface entity matches."
1346
+ });
1347
+ }
1348
+ } else if (input.mergeStrategy === "rrf") {
1349
+ warnings.push({
1350
+ source: "memory",
1351
+ code: "memory_entity_search_not_configured",
1352
+ message: "RRF merge requested but entity search is not configured; falling back to semantic+lexical RRF."
1353
+ });
1354
+ }
1355
+ const out = {
1227
1356
  semantic: filterByDateRange(semantic, input.dateFrom, input.dateTo),
1228
1357
  lexical: filterByDateRange(lexical, input.dateFrom, input.dateTo)
1229
1358
  };
1359
+ if (entity && entity.length > 0) {
1360
+ out.entity = entity;
1361
+ }
1362
+ return out;
1230
1363
  }
1231
1364
  async function searchUnifiedMemory(input) {
1232
1365
  const query = input.query.trim();
@@ -1497,32 +1630,56 @@ function createUnifiedSearch(deps = {}) {
1497
1630
  }
1498
1631
  function mergeAcrossSources(input) {
1499
1632
  const memorySubs = input.memorySubs ?? { semantic: [], lexical: [] };
1633
+ const lists = buildChannelLists(memorySubs, input.insightHits, input.knowledgeHits);
1634
+ if (input.strategy !== "rrf") {
1635
+ const seen = /* @__PURE__ */ new Map();
1636
+ for (const hit of [
1637
+ ...memorySubs.semantic,
1638
+ ...memorySubs.lexical,
1639
+ ...input.insightHits,
1640
+ ...input.knowledgeHits
1641
+ ]) {
1642
+ const key = `${hit.type}::${hit.id}`;
1643
+ const existing = seen.get(key);
1644
+ if (!existing || hit.similarity > existing.similarity) {
1645
+ seen.set(key, hit);
1646
+ }
1647
+ }
1648
+ const merged2 = mergeUnifiedMemorySearchResults(Array.from(seen.values()), input.limit);
1649
+ return attachSignals(merged2, lists);
1650
+ }
1500
1651
  const all = [
1501
1652
  ...memorySubs.semantic,
1502
1653
  ...memorySubs.lexical,
1654
+ ...memorySubs.entity ?? [],
1503
1655
  ...input.insightHits,
1504
1656
  ...input.knowledgeHits
1505
1657
  ];
1506
- if (input.strategy !== "rrf") {
1507
- return mergeUnifiedMemorySearchResults(all, input.limit);
1508
- }
1658
+ const merged = lists.length <= 1 ? mergeUnifiedMemorySearchResults(all, input.limit) : mergeUnifiedMemorySearchResultsRrf(lists, input.limit);
1659
+ return attachSignals(merged, lists);
1660
+ }
1661
+ function buildChannelLists(memorySubs, insightHits, knowledgeHits) {
1509
1662
  const lists = [];
1510
- if (memorySubs.semantic.length > 0) {
1511
- lists.push({ name: "memory-semantic", hits: memorySubs.semantic });
1512
- }
1513
- if (memorySubs.lexical.length > 0) {
1514
- lists.push({ name: "memory-bm25", hits: memorySubs.lexical });
1515
- }
1516
- if (input.insightHits.length > 0) {
1517
- lists.push({ name: "insights", hits: input.insightHits });
1518
- }
1519
- if (input.knowledgeHits.length > 0) {
1520
- lists.push({ name: "knowledge", hits: input.knowledgeHits });
1521
- }
1522
- if (lists.length <= 1) {
1523
- return mergeUnifiedMemorySearchResults(all, input.limit);
1524
- }
1525
- return mergeUnifiedMemorySearchResultsRrf(lists, input.limit);
1663
+ if (memorySubs.semantic.length > 0) lists.push({ name: "memory-semantic", hits: memorySubs.semantic });
1664
+ if (memorySubs.lexical.length > 0) lists.push({ name: "memory-bm25", hits: memorySubs.lexical });
1665
+ if (memorySubs.entity && memorySubs.entity.length > 0) {
1666
+ lists.push({ name: "memory-entity", hits: memorySubs.entity });
1667
+ }
1668
+ if (insightHits.length > 0) lists.push({ name: "insights", hits: insightHits });
1669
+ if (knowledgeHits.length > 0) lists.push({ name: "knowledge", hits: knowledgeHits });
1670
+ return lists;
1671
+ }
1672
+ function attachSignals(hits, lists) {
1673
+ if (hits.length === 0 || lists.length === 0) {
1674
+ return hits;
1675
+ }
1676
+ return hits.map((hit) => {
1677
+ const signals = materializeSignals(lists, hit);
1678
+ if (!signals) {
1679
+ return hit;
1680
+ }
1681
+ return { ...hit, signals };
1682
+ });
1526
1683
  }
1527
1684
 
1528
- export { clampUnifiedMemorySearchLimit, clampUnifiedMemorySearchThreshold, createUnifiedSearch, isRawMemorySemanticResult, mergeUnifiedMemorySearchResults, mergeUnifiedMemorySearchResultsRrf, normalizeUnifiedMemoryMergeStrategy, normalizeUnifiedMemoryReasoningStrategy, normalizeUnifiedMemorySearchSources, toKnowledgeResult, toMemoryResult };
1685
+ export { clampUnifiedMemorySearchLimit, clampUnifiedMemorySearchThreshold, createUnifiedSearch, deriveLexicalKeywords, isRawMemorySemanticResult, listNameToChannel, materializeSignals, mergeUnifiedMemorySearchResults, mergeUnifiedMemorySearchResultsRrf, normalizeUnifiedMemoryMergeStrategy, normalizeUnifiedMemoryReasoningStrategy, normalizeUnifiedMemorySearchSources, toKnowledgeResult, toMemoryResult };