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