@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.
- package/dist/{config-CnbqSek5.d.ts → config-DfvVZ--0.d.ts} +137 -1
- package/dist/http.d.ts +11 -2
- package/dist/http.js +401 -21
- package/dist/index.d.ts +10 -5
- package/dist/index.js +351 -21
- package/dist/mcp.d.ts +12 -2
- package/dist/mcp.js +456 -23
- package/dist/search/derive.d.ts +93 -0
- package/dist/search/derive.js +145 -0
- package/dist/search/distill.d.ts +60 -0
- package/dist/search/distill.js +61 -0
- package/dist/search/unified-search.d.ts +4 -2
- package/dist/search/unified-search.js +180 -23
- package/dist/server/cli-http.js +401 -21
- package/dist/server/cli-mcp.js +454 -21
- package/dist/server/cli-shared.d.ts +3 -1
- package/dist/storage/postgres-raw-message-factory.d.ts +3 -1
- package/dist/storage/raw-message-store.d.ts +3 -1
- package/dist/storage/sqlite-raw-message-store.d.ts +3 -1
- package/dist/storage/sqlite-vector-index.d.ts +3 -1
- package/dist/{unified-search-Bft57TRH.d.ts → unified-search-A8Q3JHwU.d.ts} +1 -1
- package/package.json +4 -4
|
@@ -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,
|
|
2
|
-
export { U as UnifiedSearch, c as createUnifiedSearch } from '../unified-search-
|
|
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
|
|
786
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1507
|
-
|
|
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
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
}
|
|
1516
|
-
if (
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
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 };
|