@melandlabs/memory-store 1.2.4 → 1.3.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/README.md +47 -16
- package/dist/{config-DfvVZ--0.d.ts → config-CbE2t4SW.d.ts} +173 -18
- package/dist/http.d.ts +2 -1
- package/dist/http.js +636 -277
- package/dist/index.d.ts +10 -6
- package/dist/index.js +357 -105
- package/dist/mcp.d.ts +2 -1
- package/dist/mcp.js +659 -259
- package/dist/search/derive.d.ts +2 -1
- package/dist/search/distill.d.ts +2 -1
- package/dist/search/unified-search.d.ts +3 -2
- package/dist/search/unified-search.js +357 -105
- package/dist/server/cli-http.js +1008 -327
- package/dist/server/cli-mcp.js +1031 -309
- package/dist/server/cli-shared.d.ts +20 -7
- package/dist/server/cli-shared.js +370 -48
- package/dist/storage/postgres-raw-message-factory.d.ts +2 -1
- package/dist/storage/raw-message-store.d.ts +20 -2
- package/dist/storage/sqlite-raw-message-store.d.ts +8 -1
- package/dist/storage/sqlite-vector-index.d.ts +2 -1
- package/dist/storage/sqlite-vector-index.js +1 -1
- package/dist/{unified-search-A8Q3JHwU.d.ts → unified-search-2YQ6EGuQ.d.ts} +17 -6
- package/package.json +15 -11
package/dist/mcp.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { isFactType, vsaNormalizeVector, vsaAssertSameDim } from '@melandlabs/contracts';
|
|
2
|
-
import
|
|
2
|
+
import '@melandlabs/ai/memory';
|
|
3
3
|
import { ChromaVectorStore } from '@melandlabs/rag';
|
|
4
4
|
import { mkdirSync } from 'fs';
|
|
5
5
|
import { dirname } from 'path';
|
|
@@ -12,6 +12,7 @@ import { applyReflectedConsolidationPlan } from '@melandlabs/memory-consolidatio
|
|
|
12
12
|
import { isDerivedKind } from '@melandlabs/contracts/derived-fact';
|
|
13
13
|
import { isEntityKind } from '@melandlabs/contracts/entity-edge';
|
|
14
14
|
import { superpose, bind, unbind, cleanup, cosineSimilarity } from '@melandlabs/vsa';
|
|
15
|
+
import { createHash } from 'crypto';
|
|
15
16
|
|
|
16
17
|
var __defProp = Object.defineProperty;
|
|
17
18
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
@@ -22,27 +23,6 @@ var __export = (target, all) => {
|
|
|
22
23
|
for (var name in all)
|
|
23
24
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
24
25
|
};
|
|
25
|
-
function normalizeTimestampToMs(value) {
|
|
26
|
-
if (!Number.isFinite(value)) {
|
|
27
|
-
return 0;
|
|
28
|
-
}
|
|
29
|
-
if (value < 1e11) {
|
|
30
|
-
return Math.floor(value * 1e3);
|
|
31
|
-
}
|
|
32
|
-
return Math.floor(value);
|
|
33
|
-
}
|
|
34
|
-
function rawMessageToMemoryRecord(message) {
|
|
35
|
-
return {
|
|
36
|
-
id: message.messageId,
|
|
37
|
-
userId: message.userId,
|
|
38
|
-
timestamp: normalizeTimestampToMs(message.timestamp),
|
|
39
|
-
text: message.archivedAt ? void 0 : message.content,
|
|
40
|
-
mediaRefs: message.attachments?.map((item) => item.url).filter(Boolean),
|
|
41
|
-
embedding: message.embedding,
|
|
42
|
-
embeddingModel: message.embeddingModel,
|
|
43
|
-
embeddingContentHash: message.embeddingContentHash
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
26
|
var init_contracts = __esm({
|
|
47
27
|
"src/contracts.ts"() {
|
|
48
28
|
}
|
|
@@ -68,61 +48,9 @@ function getChromaStore(collectionName) {
|
|
|
68
48
|
collectionName
|
|
69
49
|
});
|
|
70
50
|
}
|
|
71
|
-
function normalizeTimestampToMs2(value) {
|
|
72
|
-
if (!Number.isFinite(value)) {
|
|
73
|
-
return 0;
|
|
74
|
-
}
|
|
75
|
-
if (value < 1e11) {
|
|
76
|
-
return Math.floor(value * 1e3);
|
|
77
|
-
}
|
|
78
|
-
return Math.floor(value);
|
|
79
|
-
}
|
|
80
51
|
function toBooleanValue(value) {
|
|
81
52
|
return value === true || value === "true" || value === 1 || value === "1";
|
|
82
53
|
}
|
|
83
|
-
function rawMessageToChunk(message) {
|
|
84
|
-
if (!message.embedding || message.embedding.length === 0) {
|
|
85
|
-
return null;
|
|
86
|
-
}
|
|
87
|
-
const record = rawMessageToMemoryRecord(message);
|
|
88
|
-
const document = buildMemoryRecordEmbeddingDocument(record);
|
|
89
|
-
if (!document.content) {
|
|
90
|
-
return null;
|
|
91
|
-
}
|
|
92
|
-
return {
|
|
93
|
-
id: message.messageId,
|
|
94
|
-
documentId: message.messageId,
|
|
95
|
-
content: document.content,
|
|
96
|
-
embedding: message.embedding,
|
|
97
|
-
metadata: {
|
|
98
|
-
sourceType: "raw_message",
|
|
99
|
-
userId: message.userId,
|
|
100
|
-
platform: message.platform,
|
|
101
|
-
botId: message.botId,
|
|
102
|
-
channel: message.channel,
|
|
103
|
-
person: message.person,
|
|
104
|
-
timestamp: normalizeTimestampToMs2(message.timestamp),
|
|
105
|
-
memoryStage: message.memoryStage,
|
|
106
|
-
embeddingModel: message.embeddingModel,
|
|
107
|
-
embeddingDimensions: message.embeddingDimensions,
|
|
108
|
-
contentHash: message.embeddingContentHash,
|
|
109
|
-
archived: Boolean(message.archivedAt),
|
|
110
|
-
role: typeof message.metadata?.role === "string" ? message.metadata.role : void 0
|
|
111
|
-
}
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
async function upsertRawMessagesToChroma(messages) {
|
|
115
|
-
if (!isRawMessageChromaEnabled() || messages.length === 0) {
|
|
116
|
-
return 0;
|
|
117
|
-
}
|
|
118
|
-
const chunks = messages.map(rawMessageToChunk).filter((chunk) => chunk !== null);
|
|
119
|
-
if (chunks.length === 0) {
|
|
120
|
-
return 0;
|
|
121
|
-
}
|
|
122
|
-
const store = getChromaStore(getRawMessagesCollectionName());
|
|
123
|
-
await store.addChunks(chunks);
|
|
124
|
-
return chunks.length;
|
|
125
|
-
}
|
|
126
54
|
async function searchRawMessagesWithChroma(input) {
|
|
127
55
|
if (!isRawMessageChromaEnabled() || input.queryEmbedding.length === 0) {
|
|
128
56
|
return [];
|
|
@@ -324,59 +252,73 @@ var init_raw_message_store = __esm({
|
|
|
324
252
|
}
|
|
325
253
|
});
|
|
326
254
|
|
|
327
|
-
// src/
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
255
|
+
// src/search/unified-search.ts
|
|
256
|
+
init_chroma_memory_index();
|
|
257
|
+
init_raw_message_store();
|
|
258
|
+
|
|
259
|
+
// src/search/applicability.ts
|
|
260
|
+
var APPLICABILITY_SCOPES = /* @__PURE__ */ new Set([
|
|
261
|
+
"global",
|
|
262
|
+
"task",
|
|
263
|
+
"conversation",
|
|
264
|
+
"channel",
|
|
265
|
+
"project",
|
|
266
|
+
"custom"
|
|
267
|
+
]);
|
|
268
|
+
function isRecord(value) {
|
|
269
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
270
|
+
}
|
|
271
|
+
function invalidRuntimeContext(message) {
|
|
272
|
+
throw new TypeError(`memory-store search runtime context: ${message}`);
|
|
273
|
+
}
|
|
274
|
+
function validateApplicabilityContext(value, index) {
|
|
275
|
+
const path = `applicabilityContexts[${index}]`;
|
|
276
|
+
if (!isRecord(value)) {
|
|
277
|
+
invalidRuntimeContext(`${path} must be an object`);
|
|
331
278
|
}
|
|
332
|
-
const
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
279
|
+
const scope = value.scope;
|
|
280
|
+
if (typeof scope !== "string" || !APPLICABILITY_SCOPES.has(scope)) {
|
|
281
|
+
invalidRuntimeContext(`${path}.scope is not supported`);
|
|
282
|
+
}
|
|
283
|
+
if (value.key !== void 0 && typeof value.key !== "string") {
|
|
284
|
+
invalidRuntimeContext(`${path}.key must be a string when provided`);
|
|
285
|
+
}
|
|
286
|
+
if (scope !== "global" && (typeof value.key !== "string" || value.key.trim().length === 0)) {
|
|
287
|
+
invalidRuntimeContext(`${path}.key must be non-empty for scope "${scope}"`);
|
|
288
|
+
}
|
|
289
|
+
for (const field of ["validFrom", "validUntil"]) {
|
|
290
|
+
const boundary = value[field];
|
|
291
|
+
if (boundary !== void 0 && (typeof boundary !== "number" || !Number.isFinite(boundary))) {
|
|
292
|
+
invalidRuntimeContext(`${path}.${field} must be a finite epoch-millisecond number`);
|
|
341
293
|
}
|
|
342
|
-
const vector = await deps.embedQuery({
|
|
343
|
-
userId: message.userId,
|
|
344
|
-
query: message.content
|
|
345
|
-
});
|
|
346
|
-
out.push({
|
|
347
|
-
...message,
|
|
348
|
-
embedding: vector,
|
|
349
|
-
embeddingModel: message.embeddingModel ?? "server",
|
|
350
|
-
embeddingDimensions: vector.length,
|
|
351
|
-
embeddingUpdatedAt: Date.now()
|
|
352
|
-
});
|
|
353
294
|
}
|
|
354
|
-
return out;
|
|
355
295
|
}
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
const anyMissing = incoming.some((m) => !Array.isArray(m.embedding) || m.embedding.length === 0);
|
|
363
|
-
const embedderWired = typeof unified?.embedQuery === "function";
|
|
364
|
-
if (embedOnInsert === true && anyMissing) {
|
|
365
|
-
return { messages: await embedMissingMessages(messages, unified ?? {}), warnings: [] };
|
|
366
|
-
}
|
|
367
|
-
if (embedOnInsert !== true && anyMissing && embedderWired) {
|
|
368
|
-
return {
|
|
369
|
-
messages: await embedMissingMessages(messages, unified ?? {}),
|
|
370
|
-
warnings: [AUTO_APPLIED_WARNING]
|
|
371
|
-
};
|
|
296
|
+
function resolveSearchRuntimeContext(input, runtimeContext, now = Date.now) {
|
|
297
|
+
if (runtimeContext === void 0) {
|
|
298
|
+
return void 0;
|
|
299
|
+
}
|
|
300
|
+
if (!isRecord(runtimeContext) || !Array.isArray(runtimeContext.applicabilityContexts)) {
|
|
301
|
+
invalidRuntimeContext("applicabilityContexts must be an array");
|
|
372
302
|
}
|
|
373
|
-
|
|
303
|
+
for (const [index, context] of runtimeContext.applicabilityContexts.entries()) {
|
|
304
|
+
validateApplicabilityContext(context, index);
|
|
305
|
+
}
|
|
306
|
+
let applicabilityAt;
|
|
307
|
+
if (input.asOf === void 0) {
|
|
308
|
+
applicabilityAt = now();
|
|
309
|
+
} else {
|
|
310
|
+
const parsed = typeof input.asOf === "string" ? Date.parse(input.asOf) : Number.NaN;
|
|
311
|
+
if (!Number.isFinite(parsed)) {
|
|
312
|
+
invalidRuntimeContext("asOf must be a parseable timestamp when a runtime context is supplied");
|
|
313
|
+
}
|
|
314
|
+
applicabilityAt = parsed;
|
|
315
|
+
}
|
|
316
|
+
return {
|
|
317
|
+
applicabilityContexts: runtimeContext.applicabilityContexts,
|
|
318
|
+
applicabilityAt
|
|
319
|
+
};
|
|
374
320
|
}
|
|
375
321
|
|
|
376
|
-
// src/search/unified-search.ts
|
|
377
|
-
init_chroma_memory_index();
|
|
378
|
-
init_raw_message_store();
|
|
379
|
-
|
|
380
322
|
// src/search/reranker.ts
|
|
381
323
|
async function applyReranker(reranker, query, results) {
|
|
382
324
|
if (!reranker || results.length === 0) {
|
|
@@ -463,7 +405,7 @@ function normalizeUnifiedMemoryMergeStrategy(value) {
|
|
|
463
405
|
return value === "rrf" ? "rrf" : "similarity";
|
|
464
406
|
}
|
|
465
407
|
function normalizeUnifiedMemoryReasoningStrategy(value, defaultStrategy = "none") {
|
|
466
|
-
if (value === "rewrite" || value === "iterative") {
|
|
408
|
+
if (value === "rewrite" || value === "iterative" || value === "union") {
|
|
467
409
|
return value;
|
|
468
410
|
}
|
|
469
411
|
return defaultStrategy;
|
|
@@ -570,6 +512,7 @@ function materializeSignals(lists, hit) {
|
|
|
570
512
|
function listNameToChannel(name) {
|
|
571
513
|
if (name === "memory-semantic") return "semantic";
|
|
572
514
|
if (name === "memory-bm25" || name === "memory-lexical") return "lexical";
|
|
515
|
+
if (name === "memory-hybrid") return "hybrid";
|
|
573
516
|
if (name === "memory-entity") return "entity";
|
|
574
517
|
return void 0;
|
|
575
518
|
}
|
|
@@ -627,7 +570,6 @@ async function resolveScopePeer(input) {
|
|
|
627
570
|
|
|
628
571
|
// src/search/gather-evidence.ts
|
|
629
572
|
var DEFAULT_TIERS = ["summary", "raw", "insight", "knowledge"];
|
|
630
|
-
var MAX_EVIDENCE_CHARS = 600;
|
|
631
573
|
function toSummaryResult(row) {
|
|
632
574
|
const start = typeof row.startTimestamp === "number" ? row.startTimestamp : void 0;
|
|
633
575
|
const end = typeof row.endTimestamp === "number" ? row.endTimestamp : void 0;
|
|
@@ -645,7 +587,7 @@ function toSummaryResult(row) {
|
|
|
645
587
|
}
|
|
646
588
|
};
|
|
647
589
|
}
|
|
648
|
-
async function gatherSummaries(deps, input, limit, threshold, logger, peerPeers) {
|
|
590
|
+
async function gatherSummaries(deps, input, limit, threshold, logger, peerPeers, runtimeContext) {
|
|
649
591
|
if (typeof deps.searchSummaries !== "function") {
|
|
650
592
|
return { hits: [], warnings: [] };
|
|
651
593
|
}
|
|
@@ -658,7 +600,8 @@ async function gatherSummaries(deps, input, limit, threshold, logger, peerPeers)
|
|
|
658
600
|
limit,
|
|
659
601
|
threshold,
|
|
660
602
|
authToken: input.authToken,
|
|
661
|
-
...peerPeers.length > 0 ? { peers: peerPeers } : {}
|
|
603
|
+
...peerPeers.length > 0 ? { peers: peerPeers } : {},
|
|
604
|
+
...runtimeContext ?? {}
|
|
662
605
|
});
|
|
663
606
|
const hits = rows.map(toSummaryResult);
|
|
664
607
|
return { hits, warnings: [] };
|
|
@@ -676,12 +619,6 @@ async function gatherSummaries(deps, input, limit, threshold, logger, peerPeers)
|
|
|
676
619
|
};
|
|
677
620
|
}
|
|
678
621
|
}
|
|
679
|
-
function truncate(input, max = MAX_EVIDENCE_CHARS) {
|
|
680
|
-
if (input.length <= max) {
|
|
681
|
-
return input;
|
|
682
|
-
}
|
|
683
|
-
return `${input.slice(0, max - 1)}\u2026`;
|
|
684
|
-
}
|
|
685
622
|
function buildSynthesisPrompt(input) {
|
|
686
623
|
const grouped = /* @__PURE__ */ new Map();
|
|
687
624
|
for (const item of input.evidence) {
|
|
@@ -695,9 +632,10 @@ function buildSynthesisPrompt(input) {
|
|
|
695
632
|
if (items.length === 0) {
|
|
696
633
|
continue;
|
|
697
634
|
}
|
|
698
|
-
const lines = items.map(
|
|
699
|
-
|
|
700
|
-
|
|
635
|
+
const lines = items.map((item, index) => {
|
|
636
|
+
const factSource = typeof item.metadata?.source === "string" && item.metadata.source.length > 0 ? item.metadata.source : item.source ?? "unknown";
|
|
637
|
+
return ` [${index + 1}] (${item.id}, source=${factSource}, score=${item.score.toFixed(4)}) ${item.snippet}`;
|
|
638
|
+
});
|
|
701
639
|
sections.push(`## ${tier}
|
|
702
640
|
${lines.join("\n")}`);
|
|
703
641
|
}
|
|
@@ -779,12 +717,15 @@ function coerceAnswer(text, schema) {
|
|
|
779
717
|
return text;
|
|
780
718
|
}
|
|
781
719
|
async function synthesizeAnswer(input) {
|
|
782
|
-
const { query,
|
|
783
|
-
const evidence =
|
|
784
|
-
id:
|
|
785
|
-
source:
|
|
786
|
-
snippet:
|
|
787
|
-
score:
|
|
720
|
+
const { query, responseSchema, deps, logger } = input;
|
|
721
|
+
const evidence = input.evidence.map((item) => ({
|
|
722
|
+
id: item.id,
|
|
723
|
+
source: mapEvidenceSourceToTier(item.source),
|
|
724
|
+
snippet: item.snippet,
|
|
725
|
+
score: item.score,
|
|
726
|
+
// Forward per-fact provenance (e.g. `metadata.source` from
|
|
727
|
+
// `opencontext add --source`) so the synthesis prompt can cite it.
|
|
728
|
+
metadata: item.metadata
|
|
788
729
|
}));
|
|
789
730
|
const warnings = [];
|
|
790
731
|
const complete = deps.reasoning?.complete;
|
|
@@ -815,21 +756,10 @@ async function synthesizeAnswer(input) {
|
|
|
815
756
|
return { answer: "", warnings };
|
|
816
757
|
}
|
|
817
758
|
}
|
|
818
|
-
function
|
|
819
|
-
|
|
820
|
-
if (
|
|
821
|
-
|
|
822
|
-
}
|
|
823
|
-
if (hit.type === "memory") {
|
|
824
|
-
return "raw";
|
|
825
|
-
}
|
|
826
|
-
if (hit.type === "insight") {
|
|
827
|
-
return "insight";
|
|
828
|
-
}
|
|
829
|
-
if (hit.type === "knowledge") {
|
|
830
|
-
return "knowledge";
|
|
831
|
-
}
|
|
832
|
-
return hit.type;
|
|
759
|
+
function mapEvidenceSourceToTier(source) {
|
|
760
|
+
if (source === "memory") return "raw";
|
|
761
|
+
if (source === "insights") return "insight";
|
|
762
|
+
return source;
|
|
833
763
|
}
|
|
834
764
|
async function resolveSearchScopePeers(input) {
|
|
835
765
|
return resolveScopePeer({
|
|
@@ -853,7 +783,7 @@ function deriveEntityKeywords(query) {
|
|
|
853
783
|
}
|
|
854
784
|
return out.slice(0, 16);
|
|
855
785
|
}
|
|
856
|
-
async function runEntitySearchForQuery(deps, input, limit, logger, peerPeers = []) {
|
|
786
|
+
async function runEntitySearchForQuery(deps, input, limit, logger, peerPeers = [], runtimeContext) {
|
|
857
787
|
if (typeof deps.entitySearch !== "function") {
|
|
858
788
|
return [];
|
|
859
789
|
}
|
|
@@ -871,7 +801,8 @@ async function runEntitySearchForQuery(deps, input, limit, logger, peerPeers = [
|
|
|
871
801
|
keywords,
|
|
872
802
|
limit: Math.ceil(limit / filters.length),
|
|
873
803
|
botId,
|
|
874
|
-
...peerPeers.length > 0 ? { peers: peerPeers } : {}
|
|
804
|
+
...peerPeers.length > 0 ? { peers: peerPeers } : {},
|
|
805
|
+
...runtimeContext ?? {}
|
|
875
806
|
})
|
|
876
807
|
)
|
|
877
808
|
)).flat();
|
|
@@ -959,6 +890,16 @@ function filterByDateRange(items, dateFrom, dateTo) {
|
|
|
959
890
|
return true;
|
|
960
891
|
});
|
|
961
892
|
}
|
|
893
|
+
function warnApplicabilityNotEnforced(warnings) {
|
|
894
|
+
if (warnings.some((warning) => warning.code === "memory_applicability_not_enforced")) {
|
|
895
|
+
return;
|
|
896
|
+
}
|
|
897
|
+
warnings.push({
|
|
898
|
+
source: "memory",
|
|
899
|
+
code: "memory_applicability_not_enforced",
|
|
900
|
+
message: "Built-in raw-message retrieval was skipped because it cannot enforce the supplied applicability context. Configure an applicability-aware raw-message provider."
|
|
901
|
+
});
|
|
902
|
+
}
|
|
962
903
|
async function embedQueryVariant(embedQuery, input, query) {
|
|
963
904
|
return embedQuery({
|
|
964
905
|
userId: input.userId,
|
|
@@ -966,10 +907,29 @@ async function embedQueryVariant(embedQuery, input, query) {
|
|
|
966
907
|
authToken: input.authToken
|
|
967
908
|
});
|
|
968
909
|
}
|
|
969
|
-
async function runSemanticSearchForEmbedding(deps, input, queryEmbedding, limit, threshold, logger, peerPeers
|
|
910
|
+
async function runSemanticSearchForEmbedding(deps, input, queryEmbedding, limit, threshold, logger, peerPeers, warnings, runtimeContext) {
|
|
970
911
|
const filters = input.botIds && input.botIds.length > 0 ? input.botIds.map((botId) => ({ botId })) : [{}];
|
|
971
912
|
let semantic = [];
|
|
972
|
-
if (
|
|
913
|
+
if (typeof deps.searchRawMessagesAnn === "function") {
|
|
914
|
+
const searchRawMessagesAnn = deps.searchRawMessagesAnn;
|
|
915
|
+
const factTypes = input.factTypes?.length ? input.factTypes : void 0;
|
|
916
|
+
const includeDeprecated = input.includeDeprecated === true;
|
|
917
|
+
semantic = (await Promise.all(
|
|
918
|
+
filters.map(
|
|
919
|
+
(filter) => searchRawMessagesAnn({
|
|
920
|
+
userId: input.userId,
|
|
921
|
+
queryEmbedding,
|
|
922
|
+
limit,
|
|
923
|
+
threshold,
|
|
924
|
+
botId: "botId" in filter ? filter.botId : void 0,
|
|
925
|
+
includeDeprecated,
|
|
926
|
+
...peerPeers.length > 0 ? { peers: peerPeers } : {},
|
|
927
|
+
...factTypes ? { factTypes } : {},
|
|
928
|
+
...runtimeContext ?? {}
|
|
929
|
+
})
|
|
930
|
+
)
|
|
931
|
+
)).flat().filter(isRawMemorySemanticResult).map(toMemoryResult);
|
|
932
|
+
} else if (runtimeContext === void 0 && isRawMessageChromaEnabled()) {
|
|
973
933
|
try {
|
|
974
934
|
semantic = (await Promise.all(
|
|
975
935
|
filters.map((filter) => {
|
|
@@ -994,24 +954,12 @@ async function runSemanticSearchForEmbedding(deps, input, queryEmbedding, limit,
|
|
|
994
954
|
error
|
|
995
955
|
);
|
|
996
956
|
}
|
|
997
|
-
} else if (typeof deps.searchRawMessagesAnn === "function") {
|
|
998
|
-
const searchRawMessagesAnn = deps.searchRawMessagesAnn;
|
|
999
|
-
const factTypes = input.factTypes?.length ? input.factTypes : void 0;
|
|
1000
|
-
semantic = (await Promise.all(
|
|
1001
|
-
filters.map(
|
|
1002
|
-
(filter) => searchRawMessagesAnn({
|
|
1003
|
-
userId: input.userId,
|
|
1004
|
-
queryEmbedding,
|
|
1005
|
-
limit,
|
|
1006
|
-
threshold,
|
|
1007
|
-
botId: "botId" in filter ? filter.botId : void 0,
|
|
1008
|
-
...peerPeers.length > 0 ? { peers: peerPeers } : {},
|
|
1009
|
-
...factTypes ? { factTypes } : {}
|
|
1010
|
-
})
|
|
1011
|
-
)
|
|
1012
|
-
)).flat().filter(isRawMemorySemanticResult).map(toMemoryResult);
|
|
1013
957
|
}
|
|
1014
|
-
if (semantic.length === 0) {
|
|
958
|
+
if (semantic.length === 0 && runtimeContext !== void 0) {
|
|
959
|
+
warnApplicabilityNotEnforced(warnings);
|
|
960
|
+
return [];
|
|
961
|
+
}
|
|
962
|
+
if (semantic.length === 0 && typeof deps.searchRawMessagesAnn !== "function") {
|
|
1015
963
|
try {
|
|
1016
964
|
const { getRawMessageManager: getRawMessageManager2 } = await Promise.resolve().then(() => (init_raw_message_store(), raw_message_store_exports));
|
|
1017
965
|
const manager2 = await getRawMessageManager2();
|
|
@@ -1022,6 +970,7 @@ async function runSemanticSearchForEmbedding(deps, input, queryEmbedding, limit,
|
|
|
1022
970
|
queryEmbedding,
|
|
1023
971
|
limit,
|
|
1024
972
|
threshold,
|
|
973
|
+
includeDeprecated: input.includeDeprecated === true,
|
|
1025
974
|
...peerPeers.length > 0 ? { peers: peerPeers } : {},
|
|
1026
975
|
...factTypes ? { factTypes } : {}
|
|
1027
976
|
});
|
|
@@ -1051,6 +1000,18 @@ function mergeByMaxScore(lists) {
|
|
|
1051
1000
|
}
|
|
1052
1001
|
return Array.from(best.values()).sort((a, b) => b.similarity - a.similarity);
|
|
1053
1002
|
}
|
|
1003
|
+
function dedupeChannelByParent(hits) {
|
|
1004
|
+
const strongest = /* @__PURE__ */ new Map();
|
|
1005
|
+
for (const hit of hits) {
|
|
1006
|
+
const sourceMessageId = hit.metadata?.sourceMessageId;
|
|
1007
|
+
const parentId = typeof sourceMessageId === "string" && sourceMessageId.length > 0 ? sourceMessageId : hit.id;
|
|
1008
|
+
const normalized = hit.type === "memory" && hit.id !== parentId ? { ...hit, id: parentId } : hit;
|
|
1009
|
+
const key = `${normalized.type}:${parentId}`;
|
|
1010
|
+
const current = strongest.get(key);
|
|
1011
|
+
if (!current || normalized.similarity > current.similarity) strongest.set(key, normalized);
|
|
1012
|
+
}
|
|
1013
|
+
return [...strongest.values()].sort((a, b) => b.similarity - a.similarity);
|
|
1014
|
+
}
|
|
1054
1015
|
function toIterativeRecallCandidate(hit) {
|
|
1055
1016
|
return {
|
|
1056
1017
|
id: hit.id,
|
|
@@ -1059,7 +1020,7 @@ function toIterativeRecallCandidate(hit) {
|
|
|
1059
1020
|
metadata: hit.metadata
|
|
1060
1021
|
};
|
|
1061
1022
|
}
|
|
1062
|
-
async function runLexicalSearchForKeywords(deps, input, keywords, limit, logger, peerPeers
|
|
1023
|
+
async function runLexicalSearchForKeywords(deps, input, keywords, limit, logger, peerPeers, warnings, runtimeContext) {
|
|
1063
1024
|
if (keywords.length === 0) {
|
|
1064
1025
|
return [];
|
|
1065
1026
|
}
|
|
@@ -1068,6 +1029,7 @@ async function runLexicalSearchForKeywords(deps, input, keywords, limit, logger,
|
|
|
1068
1029
|
const filters = input.botIds && input.botIds.length > 0 ? input.botIds : [void 0];
|
|
1069
1030
|
const searchRawMessagesLexical = deps.searchRawMessagesLexical;
|
|
1070
1031
|
const factTypes = input.factTypes?.length ? input.factTypes : void 0;
|
|
1032
|
+
const includeDeprecated = input.includeDeprecated === true;
|
|
1071
1033
|
return (await Promise.all(
|
|
1072
1034
|
filters.map(
|
|
1073
1035
|
(botId) => searchRawMessagesLexical({
|
|
@@ -1075,8 +1037,10 @@ async function runLexicalSearchForKeywords(deps, input, keywords, limit, logger,
|
|
|
1075
1037
|
keywords,
|
|
1076
1038
|
limit: Math.ceil(limit / filters.length),
|
|
1077
1039
|
botId,
|
|
1040
|
+
includeDeprecated,
|
|
1078
1041
|
...peerPeers.length > 0 ? { peers: peerPeers } : {},
|
|
1079
|
-
...factTypes ? { factTypes } : {}
|
|
1042
|
+
...factTypes ? { factTypes } : {},
|
|
1043
|
+
...runtimeContext ?? {}
|
|
1080
1044
|
})
|
|
1081
1045
|
)
|
|
1082
1046
|
)).flat().filter(isRawMemorySemanticResult).map((hit) => ({
|
|
@@ -1087,10 +1051,15 @@ async function runLexicalSearchForKeywords(deps, input, keywords, limit, logger,
|
|
|
1087
1051
|
logger.warn?.("[memory-store] lexical memory search failed:", error);
|
|
1088
1052
|
}
|
|
1089
1053
|
}
|
|
1054
|
+
if (runtimeContext !== void 0) {
|
|
1055
|
+
warnApplicabilityNotEnforced(warnings);
|
|
1056
|
+
return [];
|
|
1057
|
+
}
|
|
1090
1058
|
try {
|
|
1091
1059
|
const { lexicalSearchRawMessages: lexicalSearchRawMessages2 } = await Promise.resolve().then(() => (init_sqlite_raw_message_store(), sqlite_raw_message_store_exports));
|
|
1092
1060
|
const filters = input.botIds && input.botIds.length > 0 ? input.botIds : [void 0];
|
|
1093
1061
|
const factTypes = input.factTypes?.length ? input.factTypes : void 0;
|
|
1062
|
+
const includeDeprecated = input.includeDeprecated === true;
|
|
1094
1063
|
return (await Promise.all(
|
|
1095
1064
|
filters.map(
|
|
1096
1065
|
(botId) => lexicalSearchRawMessages2({
|
|
@@ -1098,6 +1067,7 @@ async function runLexicalSearchForKeywords(deps, input, keywords, limit, logger,
|
|
|
1098
1067
|
keywords,
|
|
1099
1068
|
limit: Math.ceil(limit / filters.length),
|
|
1100
1069
|
botId,
|
|
1070
|
+
includeDeprecated,
|
|
1101
1071
|
...factTypes ? { factTypes } : {}
|
|
1102
1072
|
})
|
|
1103
1073
|
)
|
|
@@ -1128,7 +1098,9 @@ function searchInputToUnified(input) {
|
|
|
1128
1098
|
mergeStrategy: input.mergeStrategy,
|
|
1129
1099
|
peerFilter: input.peerFilter,
|
|
1130
1100
|
reasoningStrategy: input.reasoningStrategy,
|
|
1131
|
-
factTypes: input.factTypes
|
|
1101
|
+
factTypes: input.factTypes,
|
|
1102
|
+
includeRetrievalDiagnostics: input.includeRetrievalDiagnostics,
|
|
1103
|
+
includeDeprecated: input.includeDeprecated
|
|
1132
1104
|
};
|
|
1133
1105
|
}
|
|
1134
1106
|
function mapHitToEvidenceSource(hit) {
|
|
@@ -1156,8 +1128,8 @@ function createUnifiedSearch(deps = {}) {
|
|
|
1156
1128
|
}
|
|
1157
1129
|
return normalizeUnifiedMemoryReasoningStrategy(deps.reasoning?.defaultStrategy, "none");
|
|
1158
1130
|
}
|
|
1159
|
-
async function runMemorySource(input, limit, threshold, warnings, reasoningStrategy = "none", reasoningInfo, peerPeers = []) {
|
|
1160
|
-
if (reasoningStrategy === "iterative" && !deps.reasoning?.iterativePlanner) {
|
|
1131
|
+
async function runMemorySource(input, limit, threshold, warnings, reasoningStrategy = "none", reasoningInfo, peerPeers = [], runtimeContext) {
|
|
1132
|
+
if ((reasoningStrategy === "iterative" || reasoningStrategy === "union") && !deps.reasoning?.iterativePlanner) {
|
|
1161
1133
|
warnings.push({
|
|
1162
1134
|
source: "memory",
|
|
1163
1135
|
code: "memory_iterative_planner_not_configured",
|
|
@@ -1173,7 +1145,8 @@ function createUnifiedSearch(deps = {}) {
|
|
|
1173
1145
|
});
|
|
1174
1146
|
if (reasoningInfo) reasoningInfo.degraded = true;
|
|
1175
1147
|
}
|
|
1176
|
-
|
|
1148
|
+
let unionEvidence = null;
|
|
1149
|
+
if ((reasoningStrategy === "iterative" || reasoningStrategy === "union") && deps.reasoning?.iterativePlanner) {
|
|
1177
1150
|
const planner = deps.reasoning.iterativePlanner;
|
|
1178
1151
|
const result = await planner.plan({
|
|
1179
1152
|
query: input.query,
|
|
@@ -1188,7 +1161,9 @@ function createUnifiedSearch(deps = {}) {
|
|
|
1188
1161
|
keywords2,
|
|
1189
1162
|
limit,
|
|
1190
1163
|
logger,
|
|
1191
|
-
peerPeers
|
|
1164
|
+
peerPeers,
|
|
1165
|
+
warnings,
|
|
1166
|
+
runtimeContext
|
|
1192
1167
|
);
|
|
1193
1168
|
let semanticHits = [];
|
|
1194
1169
|
if (typeof deps.embedQuery === "function") {
|
|
@@ -1202,7 +1177,9 @@ function createUnifiedSearch(deps = {}) {
|
|
|
1202
1177
|
limit,
|
|
1203
1178
|
threshold,
|
|
1204
1179
|
logger,
|
|
1205
|
-
peerPeers
|
|
1180
|
+
peerPeers,
|
|
1181
|
+
warnings,
|
|
1182
|
+
runtimeContext
|
|
1206
1183
|
);
|
|
1207
1184
|
} catch (error) {
|
|
1208
1185
|
logger.warn?.(
|
|
@@ -1230,16 +1207,24 @@ function createUnifiedSearch(deps = {}) {
|
|
|
1230
1207
|
reasoningInfo.degraded = true;
|
|
1231
1208
|
}
|
|
1232
1209
|
}
|
|
1233
|
-
const
|
|
1210
|
+
const evidence = result.evidence.map((candidate) => ({
|
|
1234
1211
|
type: "memory",
|
|
1235
1212
|
id: candidate.id,
|
|
1236
1213
|
content: candidate.content,
|
|
1237
1214
|
similarity: candidate.similarity,
|
|
1238
|
-
metadata: { ...candidate.metadata, reasoning:
|
|
1215
|
+
metadata: { ...candidate.metadata, reasoning: reasoningStrategy }
|
|
1239
1216
|
}));
|
|
1240
|
-
|
|
1217
|
+
if (reasoningStrategy === "iterative") {
|
|
1218
|
+
return { semantic: evidence, lexical: [] };
|
|
1219
|
+
}
|
|
1220
|
+
unionEvidence = evidence;
|
|
1241
1221
|
}
|
|
1242
1222
|
if (typeof deps.embedQuery !== "function") {
|
|
1223
|
+
warnings.push({
|
|
1224
|
+
source: "memory",
|
|
1225
|
+
code: "semantic_unavailable",
|
|
1226
|
+
message: "Semantic search is unavailable because no embedding provider is configured."
|
|
1227
|
+
});
|
|
1243
1228
|
warnings.push({
|
|
1244
1229
|
source: "memory",
|
|
1245
1230
|
code: "memory_lexical_search_fallback",
|
|
@@ -1247,7 +1232,16 @@ function createUnifiedSearch(deps = {}) {
|
|
|
1247
1232
|
});
|
|
1248
1233
|
const keywords2 = deriveLexicalKeywords(input.query);
|
|
1249
1234
|
const lexical2 = filterByDateRange(
|
|
1250
|
-
await runLexicalSearchForKeywords(
|
|
1235
|
+
await runLexicalSearchForKeywords(
|
|
1236
|
+
deps,
|
|
1237
|
+
input,
|
|
1238
|
+
keywords2,
|
|
1239
|
+
limit,
|
|
1240
|
+
logger,
|
|
1241
|
+
peerPeers,
|
|
1242
|
+
warnings,
|
|
1243
|
+
runtimeContext
|
|
1244
|
+
),
|
|
1251
1245
|
input.dateFrom,
|
|
1252
1246
|
input.dateTo
|
|
1253
1247
|
);
|
|
@@ -1255,6 +1249,26 @@ function createUnifiedSearch(deps = {}) {
|
|
|
1255
1249
|
}
|
|
1256
1250
|
let semantic = [];
|
|
1257
1251
|
const embedQuery = deps.embedQuery;
|
|
1252
|
+
const activeMergeStrategy = normalizeUnifiedMemoryMergeStrategy(
|
|
1253
|
+
input.mergeStrategy ?? deps.reasoning?.defaultMergeStrategy ?? "rrf"
|
|
1254
|
+
);
|
|
1255
|
+
if (reasoningStrategy === "none" && activeMergeStrategy === "rrf" && input.threshold === void 0 && typeof deps.searchRawMessagesHybrid === "function") {
|
|
1256
|
+
const queryEmbedding = await embedQueryVariant(embedQuery, input, input.query);
|
|
1257
|
+
const filters = input.botIds && input.botIds.length > 0 ? input.botIds : [void 0];
|
|
1258
|
+
const hybrid = (await Promise.all(
|
|
1259
|
+
filters.map(
|
|
1260
|
+
(botId) => deps.searchRawMessagesHybrid?.({
|
|
1261
|
+
userId: input.userId,
|
|
1262
|
+
query: input.query,
|
|
1263
|
+
queryEmbedding,
|
|
1264
|
+
limit: Math.ceil(limit / filters.length),
|
|
1265
|
+
botId,
|
|
1266
|
+
...runtimeContext ?? {}
|
|
1267
|
+
})
|
|
1268
|
+
)
|
|
1269
|
+
)).flatMap((items) => items ?? []).filter(isRawMemorySemanticResult).map(toMemoryResult);
|
|
1270
|
+
return { semantic: [], lexical: [], hybrid: filterByDateRange(hybrid, input.dateFrom, input.dateTo) };
|
|
1271
|
+
}
|
|
1258
1272
|
if (reasoningStrategy === "rewrite" && deps.reasoning?.queryRewriter) {
|
|
1259
1273
|
try {
|
|
1260
1274
|
const variants = await deps.reasoning.queryRewriter.rewrite({
|
|
@@ -1273,7 +1287,17 @@ function createUnifiedSearch(deps = {}) {
|
|
|
1273
1287
|
);
|
|
1274
1288
|
const lists = await Promise.all(
|
|
1275
1289
|
embeddings.map(
|
|
1276
|
-
(embedding) => runSemanticSearchForEmbedding(
|
|
1290
|
+
(embedding) => runSemanticSearchForEmbedding(
|
|
1291
|
+
deps,
|
|
1292
|
+
input,
|
|
1293
|
+
embedding,
|
|
1294
|
+
limit,
|
|
1295
|
+
threshold,
|
|
1296
|
+
logger,
|
|
1297
|
+
peerPeers,
|
|
1298
|
+
warnings,
|
|
1299
|
+
runtimeContext
|
|
1300
|
+
)
|
|
1277
1301
|
)
|
|
1278
1302
|
);
|
|
1279
1303
|
semantic = mergeByMaxScore(lists);
|
|
@@ -1296,7 +1320,9 @@ function createUnifiedSearch(deps = {}) {
|
|
|
1296
1320
|
limit,
|
|
1297
1321
|
threshold,
|
|
1298
1322
|
logger,
|
|
1299
|
-
peerPeers
|
|
1323
|
+
peerPeers,
|
|
1324
|
+
warnings,
|
|
1325
|
+
runtimeContext
|
|
1300
1326
|
);
|
|
1301
1327
|
}
|
|
1302
1328
|
let lexical = [];
|
|
@@ -1315,7 +1341,8 @@ function createUnifiedSearch(deps = {}) {
|
|
|
1315
1341
|
limit,
|
|
1316
1342
|
botId,
|
|
1317
1343
|
...peerPeers.length > 0 ? { peers: peerPeers } : {},
|
|
1318
|
-
...factTypes ? { factTypes } : {}
|
|
1344
|
+
...factTypes ? { factTypes } : {},
|
|
1345
|
+
...runtimeContext ?? {}
|
|
1319
1346
|
})
|
|
1320
1347
|
)
|
|
1321
1348
|
)).flat().filter(isRawMemorySemanticResult).map((hit) => ({
|
|
@@ -1341,7 +1368,7 @@ function createUnifiedSearch(deps = {}) {
|
|
|
1341
1368
|
let entity;
|
|
1342
1369
|
if (typeof deps.entitySearch === "function") {
|
|
1343
1370
|
entity = filterByDateRange(
|
|
1344
|
-
await runEntitySearchForQuery(deps, input, limit, logger, peerPeers),
|
|
1371
|
+
await runEntitySearchForQuery(deps, input, limit, logger, peerPeers, runtimeContext),
|
|
1345
1372
|
input.dateFrom,
|
|
1346
1373
|
input.dateTo
|
|
1347
1374
|
);
|
|
@@ -1363,6 +1390,37 @@ function createUnifiedSearch(deps = {}) {
|
|
|
1363
1390
|
message: "RRF merge requested but entity search is not configured; falling back to semantic+lexical RRF."
|
|
1364
1391
|
});
|
|
1365
1392
|
}
|
|
1393
|
+
if (unionEvidence) {
|
|
1394
|
+
const plannerHits = unionEvidence;
|
|
1395
|
+
const base = mergeByMaxScore([
|
|
1396
|
+
filterByDateRange(semantic, input.dateFrom, input.dateTo),
|
|
1397
|
+
filterByDateRange(lexical, input.dateFrom, input.dateTo)
|
|
1398
|
+
]);
|
|
1399
|
+
const maxBase = base.length > 0 ? Math.max(...base.map((h) => h.similarity)) : 0;
|
|
1400
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1401
|
+
const merged = [];
|
|
1402
|
+
plannerHits.forEach((hit, i) => {
|
|
1403
|
+
if (seen.has(hit.id)) {
|
|
1404
|
+
return;
|
|
1405
|
+
}
|
|
1406
|
+
seen.add(hit.id);
|
|
1407
|
+
merged.push({
|
|
1408
|
+
...hit,
|
|
1409
|
+
similarity: maxBase + (plannerHits.length - i) * 1e-6
|
|
1410
|
+
});
|
|
1411
|
+
});
|
|
1412
|
+
for (const hit of base) {
|
|
1413
|
+
if (merged.length >= limit) {
|
|
1414
|
+
break;
|
|
1415
|
+
}
|
|
1416
|
+
if (seen.has(hit.id)) {
|
|
1417
|
+
continue;
|
|
1418
|
+
}
|
|
1419
|
+
seen.add(hit.id);
|
|
1420
|
+
merged.push(hit);
|
|
1421
|
+
}
|
|
1422
|
+
return { semantic: merged.slice(0, limit), lexical: [] };
|
|
1423
|
+
}
|
|
1366
1424
|
const out = {
|
|
1367
1425
|
semantic: filterByDateRange(semantic, input.dateFrom, input.dateTo),
|
|
1368
1426
|
lexical: filterByDateRange(lexical, input.dateFrom, input.dateTo)
|
|
@@ -1372,14 +1430,16 @@ function createUnifiedSearch(deps = {}) {
|
|
|
1372
1430
|
}
|
|
1373
1431
|
return out;
|
|
1374
1432
|
}
|
|
1375
|
-
async function
|
|
1433
|
+
async function searchUnifiedMemoryWithRuntime(input, runtimeContext) {
|
|
1376
1434
|
const query = input.query.trim();
|
|
1377
1435
|
const sources = normalizeUnifiedMemorySearchSources(input.sources);
|
|
1378
1436
|
const limit = clampUnifiedMemorySearchLimit(input.limit);
|
|
1379
|
-
const threshold = clampUnifiedMemorySearchThreshold(input.threshold);
|
|
1380
1437
|
const mergeStrategy = normalizeUnifiedMemoryMergeStrategy(
|
|
1381
1438
|
input.mergeStrategy ?? deps.reasoning?.defaultMergeStrategy ?? "rrf"
|
|
1382
1439
|
);
|
|
1440
|
+
const candidateLimit = Math.min(50, Math.max(limit, limit * 4));
|
|
1441
|
+
const threshold = clampUnifiedMemorySearchThreshold(input.threshold);
|
|
1442
|
+
const memoryThreshold = input.threshold === void 0 && mergeStrategy === "rrf" ? Number.NEGATIVE_INFINITY : threshold;
|
|
1383
1443
|
const reasoningStrategy = resolveReasoningStrategy(input);
|
|
1384
1444
|
const warnings = [];
|
|
1385
1445
|
const peerScope = input.peerFilter && input.peerFilter.length > 0 ? await resolveScopePeer({
|
|
@@ -1399,23 +1459,46 @@ function createUnifiedSearch(deps = {}) {
|
|
|
1399
1459
|
};
|
|
1400
1460
|
}
|
|
1401
1461
|
let memorySubs;
|
|
1462
|
+
let retrievalStatus;
|
|
1402
1463
|
const reasoningInfo = { strategy: reasoningStrategy };
|
|
1403
1464
|
if (input.dateFrom || input.dateTo) {
|
|
1404
1465
|
reasoningInfo.dateRange = { from: input.dateFrom, to: input.dateTo };
|
|
1405
1466
|
}
|
|
1406
1467
|
if (sources.includes("memory")) {
|
|
1407
|
-
const hasRawProviders = typeof deps.searchRawMessagesAnn === "function" || typeof deps.searchRawMessagesLexical === "function";
|
|
1468
|
+
const hasRawProviders = typeof deps.searchRawMessagesAnn === "function" || typeof deps.searchRawMessagesHybrid === "function" || typeof deps.searchRawMessagesLexical === "function";
|
|
1408
1469
|
if (isRawMessageStorageAvailable() || hasRawProviders) {
|
|
1409
1470
|
try {
|
|
1410
1471
|
memorySubs = await runMemorySource(
|
|
1411
1472
|
input,
|
|
1412
|
-
|
|
1413
|
-
|
|
1473
|
+
candidateLimit,
|
|
1474
|
+
memoryThreshold,
|
|
1414
1475
|
warnings,
|
|
1415
1476
|
reasoningStrategy,
|
|
1416
1477
|
reasoningInfo,
|
|
1417
|
-
peerPeers
|
|
1478
|
+
peerPeers,
|
|
1479
|
+
runtimeContext
|
|
1418
1480
|
);
|
|
1481
|
+
memorySubs = {
|
|
1482
|
+
semantic: dedupeChannelByParent(memorySubs.semantic),
|
|
1483
|
+
lexical: dedupeChannelByParent(memorySubs.lexical),
|
|
1484
|
+
...memorySubs.hybrid ? { hybrid: dedupeChannelByParent(memorySubs.hybrid) } : {},
|
|
1485
|
+
...memorySubs.entity ? { entity: dedupeChannelByParent(memorySubs.entity) } : {}
|
|
1486
|
+
};
|
|
1487
|
+
if (deps.getRawMessageRetrievalStatus) {
|
|
1488
|
+
retrievalStatus = await deps.getRawMessageRetrievalStatus();
|
|
1489
|
+
}
|
|
1490
|
+
if (memorySubs.semantic.length === 0 && (memorySubs.hybrid?.length ?? 0) === 0 && typeof deps.embedQuery === "function") {
|
|
1491
|
+
let code = "semantic_candidates_empty";
|
|
1492
|
+
let message = "Semantic search was available but returned no candidates.";
|
|
1493
|
+
if (retrievalStatus?.embeddedChildCount === 0) {
|
|
1494
|
+
code = "semantic_unavailable";
|
|
1495
|
+
message = "No embedded RawMessage children are available for semantic search.";
|
|
1496
|
+
} else if (retrievalStatus?.embeddingDimensions && retrievalStatus.indexedDimensions.length > 0 && !retrievalStatus.indexedDimensions.includes(retrievalStatus.embeddingDimensions)) {
|
|
1497
|
+
code = "semantic_dimension_mismatch";
|
|
1498
|
+
message = `Query embedding dimension ${retrievalStatus.embeddingDimensions} does not match indexed child dimensions ${retrievalStatus.indexedDimensions.join(", ")}.`;
|
|
1499
|
+
}
|
|
1500
|
+
warnings.push({ source: "memory", code, message });
|
|
1501
|
+
}
|
|
1419
1502
|
} catch (error) {
|
|
1420
1503
|
logger.warn?.("[memory-store] memory source failed:", error);
|
|
1421
1504
|
warnings.push({
|
|
@@ -1438,12 +1521,13 @@ function createUnifiedSearch(deps = {}) {
|
|
|
1438
1521
|
const hits = await deps.searchInsights({
|
|
1439
1522
|
userId: input.userId,
|
|
1440
1523
|
query: input.query,
|
|
1441
|
-
limit,
|
|
1524
|
+
limit: candidateLimit,
|
|
1442
1525
|
threshold,
|
|
1443
1526
|
botIds: input.botIds,
|
|
1444
1527
|
includeArchived: input.includeArchivedInsights,
|
|
1445
1528
|
authToken: input.authToken,
|
|
1446
|
-
...peerPeers.length > 0 ? { peers: peerPeers } : {}
|
|
1529
|
+
...peerPeers.length > 0 ? { peers: peerPeers } : {},
|
|
1530
|
+
...runtimeContext ?? {}
|
|
1447
1531
|
});
|
|
1448
1532
|
for (const hit of hits) {
|
|
1449
1533
|
insightHits.push({
|
|
@@ -1470,12 +1554,13 @@ function createUnifiedSearch(deps = {}) {
|
|
|
1470
1554
|
userId: input.userId,
|
|
1471
1555
|
query: input.query,
|
|
1472
1556
|
options: {
|
|
1473
|
-
limit,
|
|
1557
|
+
limit: candidateLimit,
|
|
1474
1558
|
threshold,
|
|
1475
1559
|
documentIds: input.documentIds
|
|
1476
1560
|
},
|
|
1477
1561
|
authToken: input.authToken,
|
|
1478
|
-
...peerPeers.length > 0 ? { peers: peerPeers } : {}
|
|
1562
|
+
...peerPeers.length > 0 ? { peers: peerPeers } : {},
|
|
1563
|
+
...runtimeContext ?? {}
|
|
1479
1564
|
});
|
|
1480
1565
|
for (const hit of hits) {
|
|
1481
1566
|
knowledgeHits.push(toKnowledgeResult(hit));
|
|
@@ -1493,10 +1578,14 @@ function createUnifiedSearch(deps = {}) {
|
|
|
1493
1578
|
memorySubs,
|
|
1494
1579
|
insightHits,
|
|
1495
1580
|
knowledgeHits,
|
|
1496
|
-
limit,
|
|
1581
|
+
limit: candidateLimit,
|
|
1497
1582
|
strategy: mergeStrategy
|
|
1498
1583
|
});
|
|
1499
|
-
const
|
|
1584
|
+
const rerankerStartedAt = deps.reranker ? Date.now() : void 0;
|
|
1585
|
+
const reranked = await applyReranker(deps.reranker, input.query, merged);
|
|
1586
|
+
const rerankerLatencyMs = rerankerStartedAt === void 0 ? 0 : Date.now() - rerankerStartedAt;
|
|
1587
|
+
const rerankerOrderChanged = Boolean(deps.reranker) && merged.some((hit, index) => hit.id !== reranked[index]?.id);
|
|
1588
|
+
const ranked = reranked.slice(0, limit);
|
|
1500
1589
|
const output = {
|
|
1501
1590
|
query: input.query,
|
|
1502
1591
|
sources,
|
|
@@ -1504,28 +1593,51 @@ function createUnifiedSearch(deps = {}) {
|
|
|
1504
1593
|
count: ranked.length,
|
|
1505
1594
|
warnings
|
|
1506
1595
|
};
|
|
1596
|
+
if (input.includeRetrievalDiagnostics) {
|
|
1597
|
+
output.retrievalDiagnostics = {
|
|
1598
|
+
mergeStrategy,
|
|
1599
|
+
candidateLimit,
|
|
1600
|
+
backend: retrievalStatus?.backend,
|
|
1601
|
+
semanticDegradedReason: retrievalStatus?.semanticDegradedReason ?? warnings.find((warning) => warning.code.startsWith("semantic_"))?.code,
|
|
1602
|
+
candidateCounts: {
|
|
1603
|
+
semantic: memorySubs?.semantic.length ?? 0,
|
|
1604
|
+
lexical: memorySubs?.lexical.length ?? 0,
|
|
1605
|
+
hybrid: memorySubs?.hybrid?.length ?? 0,
|
|
1606
|
+
entity: memorySubs?.entity?.length ?? 0,
|
|
1607
|
+
fused: merged.length,
|
|
1608
|
+
final: ranked.length
|
|
1609
|
+
},
|
|
1610
|
+
channels: {
|
|
1611
|
+
semantic: memorySubs?.semantic ?? [],
|
|
1612
|
+
lexical: memorySubs?.lexical ?? [],
|
|
1613
|
+
...memorySubs?.hybrid ? { hybrid: memorySubs.hybrid } : {},
|
|
1614
|
+
...memorySubs?.entity ? { entity: memorySubs.entity } : {}
|
|
1615
|
+
},
|
|
1616
|
+
fusedBeforeRerank: merged,
|
|
1617
|
+
reranker: {
|
|
1618
|
+
enabled: Boolean(deps.reranker),
|
|
1619
|
+
provider: deps.rerankerInfo?.provider,
|
|
1620
|
+
model: deps.rerankerInfo?.model,
|
|
1621
|
+
inputCount: merged.length,
|
|
1622
|
+
outputCount: reranked.length,
|
|
1623
|
+
latencyMs: rerankerLatencyMs,
|
|
1624
|
+
orderChanged: rerankerOrderChanged
|
|
1625
|
+
},
|
|
1626
|
+
final: ranked
|
|
1627
|
+
};
|
|
1628
|
+
}
|
|
1507
1629
|
if (reasoningInfo && (reasoningInfo.strategy !== "none" || reasoningInfo.dateRange !== void 0)) {
|
|
1508
1630
|
output.reasoning = reasoningInfo;
|
|
1509
1631
|
}
|
|
1510
1632
|
return output;
|
|
1511
1633
|
}
|
|
1634
|
+
async function searchUnifiedMemory(input) {
|
|
1635
|
+
return searchUnifiedMemoryWithRuntime(input);
|
|
1636
|
+
}
|
|
1512
1637
|
async function searchRawMemorySemantically(input) {
|
|
1513
|
-
|
|
1514
|
-
const threshold = clampUnifiedMemorySearchThreshold(input.threshold);
|
|
1515
|
-
const reasoningStrategy = resolveReasoningStrategy(input);
|
|
1516
|
-
const mergeStrategy = normalizeUnifiedMemoryMergeStrategy(
|
|
1517
|
-
input.mergeStrategy ?? deps.reasoning?.defaultMergeStrategy ?? "rrf"
|
|
1518
|
-
);
|
|
1519
|
-
const sub = await runMemorySource(input, limit, threshold, [], reasoningStrategy);
|
|
1520
|
-
return mergeAcrossSources({
|
|
1521
|
-
memorySubs: sub,
|
|
1522
|
-
insightHits: [],
|
|
1523
|
-
knowledgeHits: [],
|
|
1524
|
-
limit,
|
|
1525
|
-
strategy: mergeStrategy
|
|
1526
|
-
});
|
|
1638
|
+
return (await searchUnifiedMemory({ ...input, sources: ["memory"] })).results;
|
|
1527
1639
|
}
|
|
1528
|
-
async function
|
|
1640
|
+
async function searchWithResolvedRuntime(input, runtimeContext) {
|
|
1529
1641
|
const query = input.query.trim();
|
|
1530
1642
|
const wantsSynthesis = typeof input.synthesize === "boolean" ? input.synthesize : typeof input.synthesize === "object" && input.synthesize !== null;
|
|
1531
1643
|
const synthesisSchema = typeof input.synthesize === "object" && input.synthesize !== null ? input.synthesize.responseSchema ?? input.responseSchema : input.responseSchema;
|
|
@@ -1557,19 +1669,24 @@ function createUnifiedSearch(deps = {}) {
|
|
|
1557
1669
|
let unifiedHits = [];
|
|
1558
1670
|
let sources = input.sources ? [...input.sources] : [];
|
|
1559
1671
|
let unifiedReasoning;
|
|
1672
|
+
let retrievalDiagnostics;
|
|
1560
1673
|
if (wantsRawTier || wantsInsightTier || wantsKnowledgeTier) {
|
|
1561
1674
|
const inferredSources = sources.length > 0 ? sources : [
|
|
1562
1675
|
...wantsRawTier ? ["memory"] : [],
|
|
1563
1676
|
...wantsInsightTier ? ["insights"] : [],
|
|
1564
1677
|
...wantsKnowledgeTier ? ["knowledge"] : []
|
|
1565
1678
|
];
|
|
1566
|
-
const unified = await
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1679
|
+
const unified = await searchUnifiedMemoryWithRuntime(
|
|
1680
|
+
{
|
|
1681
|
+
...searchInputToUnified(input),
|
|
1682
|
+
sources: inferredSources.length > 0 ? inferredSources : void 0
|
|
1683
|
+
},
|
|
1684
|
+
runtimeContext
|
|
1685
|
+
);
|
|
1570
1686
|
unifiedHits = unified.results;
|
|
1571
1687
|
sources = unified.sources;
|
|
1572
1688
|
unifiedReasoning = unified.reasoning;
|
|
1689
|
+
retrievalDiagnostics = unified.retrievalDiagnostics;
|
|
1573
1690
|
warnings.push(...unified.warnings);
|
|
1574
1691
|
}
|
|
1575
1692
|
let summaryHits = [];
|
|
@@ -1580,7 +1697,8 @@ function createUnifiedSearch(deps = {}) {
|
|
|
1580
1697
|
limit,
|
|
1581
1698
|
threshold,
|
|
1582
1699
|
logger,
|
|
1583
|
-
peerScope.peers
|
|
1700
|
+
peerScope.peers,
|
|
1701
|
+
runtimeContext
|
|
1584
1702
|
);
|
|
1585
1703
|
warnings.push(...summaryBucket.warnings);
|
|
1586
1704
|
summaryHits = summaryBucket.hits;
|
|
@@ -1605,7 +1723,8 @@ function createUnifiedSearch(deps = {}) {
|
|
|
1605
1723
|
source: mapHitToEvidenceSource(hit),
|
|
1606
1724
|
snippet: hit.content,
|
|
1607
1725
|
score: hit.similarity,
|
|
1608
|
-
timestamp: getCandidateTimestamp(hit.metadata)
|
|
1726
|
+
timestamp: getCandidateTimestamp(hit.metadata),
|
|
1727
|
+
metadata: hit.metadata
|
|
1609
1728
|
}));
|
|
1610
1729
|
const base = {
|
|
1611
1730
|
query,
|
|
@@ -1618,12 +1737,15 @@ function createUnifiedSearch(deps = {}) {
|
|
|
1618
1737
|
if (unifiedReasoning) {
|
|
1619
1738
|
base.reasoning = unifiedReasoning;
|
|
1620
1739
|
}
|
|
1740
|
+
if (retrievalDiagnostics) {
|
|
1741
|
+
base.retrievalDiagnostics = retrievalDiagnostics;
|
|
1742
|
+
}
|
|
1621
1743
|
if (!wantsSynthesis) {
|
|
1622
1744
|
return base;
|
|
1623
1745
|
}
|
|
1624
1746
|
const { answer, warnings: synthWarnings } = await synthesizeAnswer({
|
|
1625
1747
|
query,
|
|
1626
|
-
|
|
1748
|
+
evidence,
|
|
1627
1749
|
responseSchema: synthesisSchema,
|
|
1628
1750
|
deps,
|
|
1629
1751
|
logger
|
|
@@ -1632,6 +1754,10 @@ function createUnifiedSearch(deps = {}) {
|
|
|
1632
1754
|
base.warnings = [...base.warnings, ...synthWarnings];
|
|
1633
1755
|
return base;
|
|
1634
1756
|
}
|
|
1757
|
+
async function search(input, runtimeContext) {
|
|
1758
|
+
const resolvedRuntime = resolveSearchRuntimeContext(input, runtimeContext);
|
|
1759
|
+
return searchWithResolvedRuntime(input, resolvedRuntime);
|
|
1760
|
+
}
|
|
1635
1761
|
return {
|
|
1636
1762
|
search,
|
|
1637
1763
|
searchUnifiedMemory,
|
|
@@ -1647,6 +1773,7 @@ function mergeAcrossSources(input) {
|
|
|
1647
1773
|
for (const hit of [
|
|
1648
1774
|
...memorySubs.semantic,
|
|
1649
1775
|
...memorySubs.lexical,
|
|
1776
|
+
...memorySubs.hybrid ?? [],
|
|
1650
1777
|
...input.insightHits,
|
|
1651
1778
|
...input.knowledgeHits
|
|
1652
1779
|
]) {
|
|
@@ -1662,6 +1789,7 @@ function mergeAcrossSources(input) {
|
|
|
1662
1789
|
const all = [
|
|
1663
1790
|
...memorySubs.semantic,
|
|
1664
1791
|
...memorySubs.lexical,
|
|
1792
|
+
...memorySubs.hybrid ?? [],
|
|
1665
1793
|
...memorySubs.entity ?? [],
|
|
1666
1794
|
...input.insightHits,
|
|
1667
1795
|
...input.knowledgeHits
|
|
@@ -1673,6 +1801,9 @@ function buildChannelLists(memorySubs, insightHits, knowledgeHits) {
|
|
|
1673
1801
|
const lists = [];
|
|
1674
1802
|
if (memorySubs.semantic.length > 0) lists.push({ name: "memory-semantic", hits: memorySubs.semantic });
|
|
1675
1803
|
if (memorySubs.lexical.length > 0) lists.push({ name: "memory-bm25", hits: memorySubs.lexical });
|
|
1804
|
+
if (memorySubs.hybrid && memorySubs.hybrid.length > 0) {
|
|
1805
|
+
lists.push({ name: "memory-hybrid", hits: memorySubs.hybrid });
|
|
1806
|
+
}
|
|
1676
1807
|
if (memorySubs.entity && memorySubs.entity.length > 0) {
|
|
1677
1808
|
lists.push({ name: "memory-entity", hits: memorySubs.entity });
|
|
1678
1809
|
}
|
|
@@ -2137,8 +2268,250 @@ function createVsaRecall(storage) {
|
|
|
2137
2268
|
};
|
|
2138
2269
|
}
|
|
2139
2270
|
|
|
2271
|
+
// ../shared/src/tokens.ts
|
|
2272
|
+
var estimateTokens = (text) => {
|
|
2273
|
+
const chineseChars = (text.match(/[\u4e00-\u9fa5]/g) || []).length;
|
|
2274
|
+
const otherChars = text.length - chineseChars;
|
|
2275
|
+
const wordCount = Math.ceil(otherChars / 5);
|
|
2276
|
+
return chineseChars + wordCount;
|
|
2277
|
+
};
|
|
2278
|
+
|
|
2279
|
+
// ../shared/src/text-chunking.ts
|
|
2280
|
+
var RAW_MESSAGE_CHUNK_MAX_TOKENS = 400;
|
|
2281
|
+
var RAW_MESSAGE_CHUNK_OVERLAP_TOKENS = 80;
|
|
2282
|
+
var PARAGRAPH_BOUNDARIES = ["\n\n", "\r\n\r\n"];
|
|
2283
|
+
var SENTENCE_BOUNDARY = /[.!?。!?;;]\s*$/u;
|
|
2284
|
+
function largestEndWithinTokenBudget(text, start, maxTokens) {
|
|
2285
|
+
let low = start + 1;
|
|
2286
|
+
let high = text.length;
|
|
2287
|
+
let best = low;
|
|
2288
|
+
while (low <= high) {
|
|
2289
|
+
const middle = Math.floor((low + high) / 2);
|
|
2290
|
+
if (estimateTokens(text.slice(start, middle)) <= maxTokens) {
|
|
2291
|
+
best = middle;
|
|
2292
|
+
low = middle + 1;
|
|
2293
|
+
} else {
|
|
2294
|
+
high = middle - 1;
|
|
2295
|
+
}
|
|
2296
|
+
}
|
|
2297
|
+
return best;
|
|
2298
|
+
}
|
|
2299
|
+
function preferNaturalEnd(text, start, hardEnd) {
|
|
2300
|
+
if (hardEnd >= text.length) return text.length;
|
|
2301
|
+
const searchStart = start + Math.floor((hardEnd - start) * 0.6);
|
|
2302
|
+
const candidate = text.slice(searchStart, hardEnd);
|
|
2303
|
+
for (const boundary of PARAGRAPH_BOUNDARIES) {
|
|
2304
|
+
const index = candidate.lastIndexOf(boundary);
|
|
2305
|
+
if (index >= 0) return searchStart + index + boundary.length;
|
|
2306
|
+
}
|
|
2307
|
+
for (let offset = candidate.length; offset > 0; offset -= 1) {
|
|
2308
|
+
const prefix = candidate.slice(0, offset);
|
|
2309
|
+
if (SENTENCE_BOUNDARY.test(prefix)) return searchStart + offset;
|
|
2310
|
+
}
|
|
2311
|
+
for (let offset = candidate.length - 1; offset >= 0; offset -= 1) {
|
|
2312
|
+
if (/\s/u.test(candidate[offset] ?? "")) return searchStart + offset + 1;
|
|
2313
|
+
}
|
|
2314
|
+
return hardEnd;
|
|
2315
|
+
}
|
|
2316
|
+
function overlapStart(text, chunkStart, chunkEnd, overlapTokens) {
|
|
2317
|
+
if (overlapTokens <= 0) return chunkEnd;
|
|
2318
|
+
let low = chunkStart + 1;
|
|
2319
|
+
let high = chunkEnd;
|
|
2320
|
+
let best = chunkEnd;
|
|
2321
|
+
while (low <= high) {
|
|
2322
|
+
const middle = Math.floor((low + high) / 2);
|
|
2323
|
+
if (estimateTokens(text.slice(middle, chunkEnd)) <= overlapTokens) {
|
|
2324
|
+
best = middle;
|
|
2325
|
+
high = middle - 1;
|
|
2326
|
+
} else {
|
|
2327
|
+
low = middle + 1;
|
|
2328
|
+
}
|
|
2329
|
+
}
|
|
2330
|
+
return Math.min(chunkEnd, Math.max(chunkStart + 1, best));
|
|
2331
|
+
}
|
|
2332
|
+
function chunkTextByEstimatedTokens(text, options = {}) {
|
|
2333
|
+
if (text.length === 0) return [];
|
|
2334
|
+
const maxTokens = Math.max(1, Math.floor(options.maxTokens ?? RAW_MESSAGE_CHUNK_MAX_TOKENS));
|
|
2335
|
+
const overlapTokens = Math.max(0, Math.floor(options.overlapTokens ?? RAW_MESSAGE_CHUNK_OVERLAP_TOKENS));
|
|
2336
|
+
if (overlapTokens >= maxTokens) {
|
|
2337
|
+
throw new Error("overlapTokens must be smaller than maxTokens");
|
|
2338
|
+
}
|
|
2339
|
+
const chunks = [];
|
|
2340
|
+
let start = 0;
|
|
2341
|
+
while (start < text.length) {
|
|
2342
|
+
const hardEnd = largestEndWithinTokenBudget(text, start, maxTokens);
|
|
2343
|
+
const end = preferNaturalEnd(text, start, hardEnd);
|
|
2344
|
+
chunks.push({
|
|
2345
|
+
chunkIndex: chunks.length,
|
|
2346
|
+
startPosition: start,
|
|
2347
|
+
endPosition: end,
|
|
2348
|
+
content: text.slice(start, end)
|
|
2349
|
+
});
|
|
2350
|
+
if (end >= text.length) break;
|
|
2351
|
+
start = overlapStart(text, start, end, overlapTokens);
|
|
2352
|
+
}
|
|
2353
|
+
return chunks;
|
|
2354
|
+
}
|
|
2355
|
+
|
|
2356
|
+
// src/embed-on-insert.ts
|
|
2357
|
+
var AUTO_APPLIED_WARNING = {
|
|
2358
|
+
code: "embed_on_insert_auto_applied",
|
|
2359
|
+
message: "Caller did not explicitly enable embedOnInsert, but an embedding provider is configured. OpenContext generated the missing search embeddings."
|
|
2360
|
+
};
|
|
2361
|
+
var SEMANTIC_UNAVAILABLE_WARNING = {
|
|
2362
|
+
code: "semantic_index_unavailable",
|
|
2363
|
+
message: "No embedding provider is configured. RawMessage children were indexed for lexical search only; semantic search is unavailable until they are explicitly reindexed."
|
|
2364
|
+
};
|
|
2365
|
+
function sha256(value) {
|
|
2366
|
+
return createHash("sha256").update(value).digest("hex");
|
|
2367
|
+
}
|
|
2368
|
+
function isUsableEmbedding(embedding) {
|
|
2369
|
+
return Boolean(
|
|
2370
|
+
embedding?.length && embedding.every(Number.isFinite) && embedding.some((value) => value !== 0)
|
|
2371
|
+
);
|
|
2372
|
+
}
|
|
2373
|
+
async function embedText(text, userId, deps) {
|
|
2374
|
+
if (typeof deps.embedQuery !== "function" || text.length === 0) return void 0;
|
|
2375
|
+
const embedding = await deps.embedQuery({ userId, query: text });
|
|
2376
|
+
return isUsableEmbedding(embedding) ? embedding : void 0;
|
|
2377
|
+
}
|
|
2378
|
+
async function embedMissingMessages(messages, deps) {
|
|
2379
|
+
if (typeof deps.embedQuery !== "function") return messages;
|
|
2380
|
+
const out = [];
|
|
2381
|
+
for (const message of messages) {
|
|
2382
|
+
if (isUsableEmbedding(message.embedding) || !message.content) {
|
|
2383
|
+
out.push(message);
|
|
2384
|
+
continue;
|
|
2385
|
+
}
|
|
2386
|
+
const embedding = await embedText(message.content, message.userId, deps);
|
|
2387
|
+
out.push(
|
|
2388
|
+
embedding ? {
|
|
2389
|
+
...message,
|
|
2390
|
+
embedding,
|
|
2391
|
+
embeddingModel: message.embeddingModel ?? deps.embeddingInfo?.model ?? "server",
|
|
2392
|
+
embeddingContentHash: sha256(message.content),
|
|
2393
|
+
embeddingDimensions: embedding.length,
|
|
2394
|
+
embeddingUpdatedAt: Date.now()
|
|
2395
|
+
} : message
|
|
2396
|
+
);
|
|
2397
|
+
}
|
|
2398
|
+
return out;
|
|
2399
|
+
}
|
|
2400
|
+
async function prepareRawMessageIngest(incoming, embedOnInsert, unified) {
|
|
2401
|
+
const deps = unified ?? {};
|
|
2402
|
+
const embedderAvailable = typeof deps.embedQuery === "function" || typeof deps.embedDocuments === "function";
|
|
2403
|
+
const shouldEmbed = embedOnInsert === true || embedderAvailable;
|
|
2404
|
+
const messages = [];
|
|
2405
|
+
const chunks = [];
|
|
2406
|
+
let missingSemanticEmbedding = false;
|
|
2407
|
+
let generatedEmbedding = false;
|
|
2408
|
+
const plans = incoming.map((message) => ({
|
|
2409
|
+
message,
|
|
2410
|
+
pieces: chunkTextByEstimatedTokens(message.content)
|
|
2411
|
+
}));
|
|
2412
|
+
const generatedEmbeddings = /* @__PURE__ */ new Map();
|
|
2413
|
+
const pendingByUser = /* @__PURE__ */ new Map();
|
|
2414
|
+
for (const [messageIndex, plan] of plans.entries()) {
|
|
2415
|
+
for (const piece of plan.pieces) {
|
|
2416
|
+
const canReuseParentEmbedding = plan.pieces.length === 1 && isUsableEmbedding(plan.message.embedding);
|
|
2417
|
+
if (canReuseParentEmbedding || !shouldEmbed || piece.content.length === 0) continue;
|
|
2418
|
+
const pending = pendingByUser.get(plan.message.userId) ?? [];
|
|
2419
|
+
pending.push({ key: `${messageIndex}:${piece.chunkIndex}`, text: piece.content });
|
|
2420
|
+
pendingByUser.set(plan.message.userId, pending);
|
|
2421
|
+
}
|
|
2422
|
+
}
|
|
2423
|
+
for (const [userId, pending] of pendingByUser) {
|
|
2424
|
+
let embeddings;
|
|
2425
|
+
if (typeof deps.embedDocuments === "function") {
|
|
2426
|
+
const batch = await deps.embedDocuments({
|
|
2427
|
+
userId,
|
|
2428
|
+
texts: pending.map((item) => item.text)
|
|
2429
|
+
});
|
|
2430
|
+
if (batch.length !== pending.length) {
|
|
2431
|
+
throw new Error(
|
|
2432
|
+
`Document embedding count mismatch: expected ${pending.length}, received ${batch.length}`
|
|
2433
|
+
);
|
|
2434
|
+
}
|
|
2435
|
+
embeddings = batch.map((embedding) => isUsableEmbedding(embedding) ? embedding : void 0);
|
|
2436
|
+
} else {
|
|
2437
|
+
embeddings = [];
|
|
2438
|
+
for (const item of pending) {
|
|
2439
|
+
embeddings.push(await embedText(item.text, userId, deps));
|
|
2440
|
+
}
|
|
2441
|
+
}
|
|
2442
|
+
for (const [index, item] of pending.entries()) {
|
|
2443
|
+
const embedding = embeddings[index];
|
|
2444
|
+
if (embedding) generatedEmbeddings.set(item.key, embedding);
|
|
2445
|
+
}
|
|
2446
|
+
}
|
|
2447
|
+
for (const [messageIndex, plan] of plans.entries()) {
|
|
2448
|
+
const incomingMessage = plan.message;
|
|
2449
|
+
const pieces = plan.pieces;
|
|
2450
|
+
let message = incomingMessage;
|
|
2451
|
+
for (const piece of pieces) {
|
|
2452
|
+
const contentHash = sha256(piece.content);
|
|
2453
|
+
const canReuseParentEmbedding = pieces.length === 1 && isUsableEmbedding(incomingMessage.embedding);
|
|
2454
|
+
const embedding = canReuseParentEmbedding ? incomingMessage.embedding : generatedEmbeddings.get(`${messageIndex}:${piece.chunkIndex}`);
|
|
2455
|
+
generatedEmbedding = generatedEmbedding || Boolean(embedding && !canReuseParentEmbedding);
|
|
2456
|
+
if (!embedding) missingSemanticEmbedding = true;
|
|
2457
|
+
const embeddingModel = embedding ? canReuseParentEmbedding ? incomingMessage.embeddingModel : deps.embeddingInfo?.model ?? incomingMessage.embeddingModel ?? "server" : void 0;
|
|
2458
|
+
chunks.push({
|
|
2459
|
+
chunkId: `${incomingMessage.messageId}:chunk:${piece.chunkIndex}:${contentHash.slice(0, 16)}`,
|
|
2460
|
+
messageId: incomingMessage.messageId,
|
|
2461
|
+
userId: incomingMessage.userId,
|
|
2462
|
+
chunkIndex: piece.chunkIndex,
|
|
2463
|
+
chunkCount: pieces.length,
|
|
2464
|
+
startPosition: piece.startPosition,
|
|
2465
|
+
endPosition: piece.endPosition,
|
|
2466
|
+
content: piece.content,
|
|
2467
|
+
contentHash,
|
|
2468
|
+
embedding,
|
|
2469
|
+
embeddingModel,
|
|
2470
|
+
embeddingDimensions: embedding?.length,
|
|
2471
|
+
embeddingUpdatedAt: embedding ? Date.now() : void 0
|
|
2472
|
+
});
|
|
2473
|
+
if (pieces.length === 1 && embedding && !incomingMessage.embedding?.length) {
|
|
2474
|
+
message = {
|
|
2475
|
+
...incomingMessage,
|
|
2476
|
+
embedding,
|
|
2477
|
+
embeddingModel,
|
|
2478
|
+
embeddingContentHash: contentHash,
|
|
2479
|
+
embeddingDimensions: embedding.length,
|
|
2480
|
+
embeddingUpdatedAt: Date.now()
|
|
2481
|
+
};
|
|
2482
|
+
}
|
|
2483
|
+
}
|
|
2484
|
+
messages.push(message);
|
|
2485
|
+
}
|
|
2486
|
+
const warnings = [];
|
|
2487
|
+
if (generatedEmbedding && embedOnInsert !== true) warnings.push(AUTO_APPLIED_WARNING);
|
|
2488
|
+
if (missingSemanticEmbedding) warnings.push(SEMANTIC_UNAVAILABLE_WARNING);
|
|
2489
|
+
return { messages, chunks, warnings };
|
|
2490
|
+
}
|
|
2491
|
+
|
|
2492
|
+
// src/storage/raw-message-ingest.ts
|
|
2493
|
+
async function persistRawMessages(input) {
|
|
2494
|
+
const prepared = await prepareRawMessageIngest(input.messages, input.embedOnInsert, input.unified);
|
|
2495
|
+
let ids;
|
|
2496
|
+
if (typeof input.manager.storeMessagesWithSearchChunks === "function") {
|
|
2497
|
+
ids = await input.manager.storeMessagesWithSearchChunks(prepared.messages, prepared.chunks);
|
|
2498
|
+
} else {
|
|
2499
|
+
const legacyMessages = (input.embedOnInsert === true || typeof input.unified?.embedQuery === "function") && typeof input.unified?.embedQuery === "function" ? await embedMissingMessages(prepared.messages, input.unified) : prepared.messages;
|
|
2500
|
+
if (typeof input.manager.upsertRawMessages === "function") {
|
|
2501
|
+
await input.manager.upsertRawMessages({ userId: input.userId, messages: legacyMessages });
|
|
2502
|
+
} else if (typeof input.manager.storeMessages === "function") {
|
|
2503
|
+
ids = await input.manager.storeMessages(legacyMessages);
|
|
2504
|
+
} else {
|
|
2505
|
+
throw new Error("active raw-message manager exposes no supported write method");
|
|
2506
|
+
}
|
|
2507
|
+
}
|
|
2508
|
+
if (input.externalIndex) {
|
|
2509
|
+
await input.externalIndex.replaceMessages(prepared.messages, prepared.chunks);
|
|
2510
|
+
}
|
|
2511
|
+
return { count: prepared.messages.length, ids, warnings: prepared.warnings };
|
|
2512
|
+
}
|
|
2513
|
+
|
|
2140
2514
|
// src/mcp.ts
|
|
2141
|
-
init_chroma_memory_index();
|
|
2142
2515
|
init_raw_message_store();
|
|
2143
2516
|
init_sqlite_raw_message_store();
|
|
2144
2517
|
var DEFAULT_NAME = "@melandlabs/memory-store";
|
|
@@ -2151,13 +2524,37 @@ async function startMcpServer(options = {}) {
|
|
|
2151
2524
|
const rawStore = createRawMessageStore({
|
|
2152
2525
|
env: options.env
|
|
2153
2526
|
});
|
|
2154
|
-
const
|
|
2527
|
+
const rawMessageManager = await rawStore.getManager();
|
|
2528
|
+
const search = createUnifiedSearch({
|
|
2529
|
+
...options.unified,
|
|
2530
|
+
searchRawMessagesLexical: async (input) => {
|
|
2531
|
+
if (typeof rawMessageManager.lexicalSearchMessages !== "function") return [];
|
|
2532
|
+
const results = await rawMessageManager.lexicalSearchMessages(input);
|
|
2533
|
+
return results.filter(Boolean).map((result) => ({
|
|
2534
|
+
id: result.id,
|
|
2535
|
+
content: result.content,
|
|
2536
|
+
similarity: result.similarity,
|
|
2537
|
+
metadata: result.metadata ?? {}
|
|
2538
|
+
}));
|
|
2539
|
+
}
|
|
2540
|
+
});
|
|
2155
2541
|
const vsaDbPath = resolveSQLiteRawMessageDbPath(options.dbPath);
|
|
2156
2542
|
const vsaStorage = await getSQLiteVsaStore({ dbPath: vsaDbPath });
|
|
2157
2543
|
const vsa = createVsaRecall(vsaStorage);
|
|
2158
|
-
server.tool("memory.health", "Returns the health status of the memory store.", async () =>
|
|
2159
|
-
|
|
2160
|
-
|
|
2544
|
+
server.tool("memory.health", "Returns the health status of the memory store.", async () => {
|
|
2545
|
+
const retrieval = options.unified?.getRawMessageRetrievalStatus ? await options.unified.getRawMessageRetrievalStatus().catch(() => ({
|
|
2546
|
+
backend: "unknown",
|
|
2547
|
+
childCount: 0,
|
|
2548
|
+
embeddedChildCount: 0,
|
|
2549
|
+
indexedDimensions: [],
|
|
2550
|
+
semanticReady: false,
|
|
2551
|
+
lexicalReady: false,
|
|
2552
|
+
semanticDegradedReason: "health_check_failed"
|
|
2553
|
+
})) : void 0;
|
|
2554
|
+
return {
|
|
2555
|
+
content: [{ type: "text", text: JSON.stringify({ ok: true, retrieval }) }]
|
|
2556
|
+
};
|
|
2557
|
+
});
|
|
2161
2558
|
const searchSchema = {
|
|
2162
2559
|
userId: z.string(),
|
|
2163
2560
|
query: z.string().min(1),
|
|
@@ -2165,8 +2562,8 @@ async function startMcpServer(options = {}) {
|
|
|
2165
2562
|
tiers: z.array(z.enum(["summary", "raw", "insight", "knowledge"])).optional().describe(
|
|
2166
2563
|
"Per-tier evidence scope for synthesis. Default: ['summary','raw','insight','knowledge']. Ignored when `synthesize` is falsy."
|
|
2167
2564
|
),
|
|
2168
|
-
reasoningStrategy: z.enum(["none", "rewrite", "iterative"]).optional().describe(
|
|
2169
|
-
"LLM reasoning strategy. `rewrite` rephrases the query before embedding; `iterative` runs a planner that searches, notes evidence, and searches again. Requires `unified.reasoning.{queryRewriter,iterativePlanner}` to be wired into the server."
|
|
2565
|
+
reasoningStrategy: z.enum(["none", "rewrite", "iterative", "union"]).optional().describe(
|
|
2566
|
+
"LLM reasoning strategy. `rewrite` rephrases the query before embedding; `iterative` runs a planner that searches, notes evidence, and searches again; `union` merges the planner's evidence with the baseline top-k hits (evidence first, deduped, capped at limit). Requires `unified.reasoning.{queryRewriter,iterativePlanner}` to be wired into the server."
|
|
2170
2567
|
),
|
|
2171
2568
|
synthesize: z.union([z.boolean(), z.object({ responseSchema: z.record(z.string(), z.unknown()).optional() })]).optional().describe(
|
|
2172
2569
|
"Opt-in LLM synthesis. `true` returns `{ answer, evidence, results, warnings }`. `synthesize.responseSchema` extracts `answer` from a JSON payload."
|
|
@@ -2175,7 +2572,11 @@ async function startMcpServer(options = {}) {
|
|
|
2175
2572
|
// coerces it into `synthesize.responseSchema` when present.
|
|
2176
2573
|
responseSchema: z.record(z.string(), z.unknown()).optional(),
|
|
2177
2574
|
limit: z.number().int().min(1).max(50).default(10),
|
|
2178
|
-
threshold: z.number().min(-1).max(1).
|
|
2575
|
+
threshold: z.number().min(-1).max(1).optional().describe(
|
|
2576
|
+
"Optional similarity floor. Omit it to let the default RRF pipeline fuse the full candidate window."
|
|
2577
|
+
),
|
|
2578
|
+
mergeStrategy: z.enum(["rrf", "similarity"]).optional(),
|
|
2579
|
+
includeRetrievalDiagnostics: z.boolean().optional(),
|
|
2179
2580
|
botIds: z.array(z.string()).optional(),
|
|
2180
2581
|
documentIds: z.array(z.string()).optional(),
|
|
2181
2582
|
includeArchivedInsights: z.boolean().default(false),
|
|
@@ -2239,11 +2640,13 @@ async function startMcpServer(options = {}) {
|
|
|
2239
2640
|
sources: a.sources,
|
|
2240
2641
|
limit: a.limit,
|
|
2241
2642
|
threshold: a.threshold,
|
|
2643
|
+
mergeStrategy: a.mergeStrategy,
|
|
2644
|
+
includeRetrievalDiagnostics: a.includeRetrievalDiagnostics,
|
|
2242
2645
|
botIds: a.botIds,
|
|
2243
2646
|
documentIds: a.documentIds,
|
|
2244
2647
|
authToken: a.authToken,
|
|
2245
2648
|
includeArchivedInsights: a.includeArchivedInsights,
|
|
2246
|
-
reasoningStrategy: a.reasoningStrategy === "rewrite" || a.reasoningStrategy === "iterative" ? a.reasoningStrategy : void 0,
|
|
2649
|
+
reasoningStrategy: a.reasoningStrategy === "rewrite" || a.reasoningStrategy === "iterative" || a.reasoningStrategy === "union" ? a.reasoningStrategy : void 0,
|
|
2247
2650
|
...a.tiers ? { tiers: a.tiers } : {},
|
|
2248
2651
|
...wantsSynthesis ? {
|
|
2249
2652
|
synthesize: {
|
|
@@ -2269,37 +2672,24 @@ async function startMcpServer(options = {}) {
|
|
|
2269
2672
|
const a = args;
|
|
2270
2673
|
const manager2 = await rawStore.getManager();
|
|
2271
2674
|
const incoming = [a.message].map((m) => ({ ...m, userId: m.userId ?? a.userId }));
|
|
2272
|
-
const
|
|
2273
|
-
|
|
2274
|
-
a.
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
messages
|
|
2282
|
-
});
|
|
2283
|
-
} else if (typeof manager2.storeMessages === "function") {
|
|
2284
|
-
const ids = await manager2.storeMessages(messages);
|
|
2285
|
-
result = { inserted: ids.length, ids };
|
|
2286
|
-
} else {
|
|
2287
|
-
throw new Error("active raw-message manager exposes neither upsertRawMessages nor storeMessages");
|
|
2288
|
-
}
|
|
2289
|
-
try {
|
|
2290
|
-
await upsertRawMessagesToChroma(messages);
|
|
2291
|
-
} catch (error) {
|
|
2292
|
-
console.warn("[memory-store/mcp] chroma upsert failed:", error);
|
|
2293
|
-
}
|
|
2675
|
+
const persisted = await persistRawMessages({
|
|
2676
|
+
manager: manager2,
|
|
2677
|
+
userId: a.userId,
|
|
2678
|
+
messages: incoming,
|
|
2679
|
+
embedOnInsert: a.embedOnInsert,
|
|
2680
|
+
unified: options.unified,
|
|
2681
|
+
externalIndex: options.unified?.rawMessageChildIndex
|
|
2682
|
+
});
|
|
2683
|
+
const result = persisted.ids ? { inserted: persisted.ids.length, ids: persisted.ids } : { inserted: persisted.count };
|
|
2294
2684
|
return {
|
|
2295
2685
|
content: [
|
|
2296
2686
|
{
|
|
2297
2687
|
type: "text",
|
|
2298
2688
|
text: JSON.stringify({
|
|
2299
2689
|
ok: true,
|
|
2300
|
-
count:
|
|
2690
|
+
count: persisted.count,
|
|
2301
2691
|
result,
|
|
2302
|
-
...warnings.length > 0 ? { warnings } : {}
|
|
2692
|
+
...persisted.warnings.length > 0 ? { warnings: persisted.warnings } : {}
|
|
2303
2693
|
})
|
|
2304
2694
|
}
|
|
2305
2695
|
]
|
|
@@ -2639,7 +3029,17 @@ async function startMcpServer(options = {}) {
|
|
|
2639
3029
|
);
|
|
2640
3030
|
const okfMcpSpecifier = "@melandlabs/okf/mcp";
|
|
2641
3031
|
const { registerOkfTools } = await import(okfMcpSpecifier);
|
|
2642
|
-
registerOkfTools(server, rawStore
|
|
3032
|
+
registerOkfTools(server, rawStore, {
|
|
3033
|
+
writeMessages: async (userId, messages) => {
|
|
3034
|
+
await persistRawMessages({
|
|
3035
|
+
manager: rawMessageManager,
|
|
3036
|
+
userId,
|
|
3037
|
+
messages,
|
|
3038
|
+
unified: options.unified,
|
|
3039
|
+
externalIndex: options.unified?.rawMessageChildIndex
|
|
3040
|
+
});
|
|
3041
|
+
}
|
|
3042
|
+
});
|
|
2643
3043
|
const transport = new StdioServerTransport();
|
|
2644
3044
|
await server.connect(transport);
|
|
2645
3045
|
return server;
|