@melandlabs/memory-store 1.1.7 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -0
- package/dist/{config-CnbqSek5.d.ts → config-DfvVZ--0.d.ts} +137 -1
- package/dist/http.d.ts +11 -2
- package/dist/http.js +466 -56
- package/dist/index.d.ts +10 -5
- package/dist/index.js +351 -21
- package/dist/mcp.d.ts +12 -2
- package/dist/mcp.js +516 -53
- package/dist/search/derive.d.ts +93 -0
- package/dist/search/derive.js +145 -0
- package/dist/search/distill.d.ts +60 -0
- package/dist/search/distill.js +61 -0
- package/dist/search/unified-search.d.ts +4 -2
- package/dist/search/unified-search.js +180 -23
- package/dist/server/cli-http.js +466 -56
- package/dist/server/cli-mcp.js +520 -52
- package/dist/server/cli-shared.d.ts +3 -1
- package/dist/storage/postgres-raw-message-factory.d.ts +3 -1
- package/dist/storage/raw-message-store.d.ts +3 -1
- package/dist/storage/sqlite-raw-message-store.d.ts +3 -1
- package/dist/storage/sqlite-vector-index.d.ts +3 -1
- package/dist/{unified-search-Bft57TRH.d.ts → unified-search-A8Q3JHwU.d.ts} +1 -1
- package/package.json +6 -6
package/dist/server/cli-mcp.js
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { vsaNormalizeVector, vsaAssertSameDim } from '@melandlabs/contracts';
|
|
2
|
+
import { isFactType, vsaNormalizeVector, vsaAssertSameDim } from '@melandlabs/contracts';
|
|
3
3
|
import { buildMemoryRecordEmbeddingDocument } from '@melandlabs/ai/memory';
|
|
4
4
|
import { ChromaVectorStore } from '@melandlabs/rag';
|
|
5
5
|
import { mkdirSync } from 'fs';
|
|
6
6
|
import { dirname } from 'path';
|
|
7
7
|
import { getOpenContextPath } from '@melandlabs/env-config';
|
|
8
|
-
import { getSQLiteVsaStore, SQLiteRawMessageManager } from '@melandlabs/sqlite';
|
|
8
|
+
import { getSQLiteVsaStore, closeSQLiteVsaStore, SQLiteRawMessageManager } from '@melandlabs/sqlite';
|
|
9
9
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
10
10
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
11
11
|
import { z } from 'zod';
|
|
12
12
|
import { applyReflectedConsolidationPlan } from '@melandlabs/memory-consolidation';
|
|
13
|
+
import { isDerivedKind } from '@melandlabs/contracts/derived-fact';
|
|
14
|
+
import { isEntityKind } from '@melandlabs/contracts/entity-edge';
|
|
13
15
|
import { superpose, bind, unbind, cleanup, cosineSimilarity } from '@melandlabs/vsa';
|
|
14
16
|
|
|
15
17
|
var __defProp = Object.defineProperty;
|
|
@@ -323,6 +325,55 @@ var init_raw_message_store = __esm({
|
|
|
323
325
|
}
|
|
324
326
|
});
|
|
325
327
|
|
|
328
|
+
// src/embed-on-insert.ts
|
|
329
|
+
async function embedMissingMessages(messages, deps) {
|
|
330
|
+
if (typeof deps.embedQuery !== "function") {
|
|
331
|
+
return messages;
|
|
332
|
+
}
|
|
333
|
+
const out = [];
|
|
334
|
+
for (const message of messages) {
|
|
335
|
+
if (Array.isArray(message.embedding) && message.embedding.length > 0) {
|
|
336
|
+
out.push(message);
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
if (typeof message.content !== "string" || message.content.length === 0) {
|
|
340
|
+
out.push(message);
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
const vector = await deps.embedQuery({
|
|
344
|
+
userId: message.userId,
|
|
345
|
+
query: message.content
|
|
346
|
+
});
|
|
347
|
+
out.push({
|
|
348
|
+
...message,
|
|
349
|
+
embedding: vector,
|
|
350
|
+
embeddingModel: message.embeddingModel ?? "server",
|
|
351
|
+
embeddingDimensions: vector.length,
|
|
352
|
+
embeddingUpdatedAt: Date.now()
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
return out;
|
|
356
|
+
}
|
|
357
|
+
var AUTO_APPLIED_WARNING = {
|
|
358
|
+
code: "embed_on_insert_auto_applied",
|
|
359
|
+
message: "Caller set `embedOnInsert` falsy but the active vector backend needs an embedding per row. Server filled in the missing embeddings; pass `embedOnInsert: true` to opt in explicitly."
|
|
360
|
+
};
|
|
361
|
+
async function applyEmbedOnInsertPolicy(incoming, embedOnInsert, unified) {
|
|
362
|
+
const messages = incoming;
|
|
363
|
+
const anyMissing = incoming.some((m) => !Array.isArray(m.embedding) || m.embedding.length === 0);
|
|
364
|
+
const embedderWired = typeof unified?.embedQuery === "function";
|
|
365
|
+
if (embedOnInsert === true && anyMissing) {
|
|
366
|
+
return { messages: await embedMissingMessages(messages, unified ?? {}), warnings: [] };
|
|
367
|
+
}
|
|
368
|
+
if (embedOnInsert !== true && anyMissing && embedderWired) {
|
|
369
|
+
return {
|
|
370
|
+
messages: await embedMissingMessages(messages, unified ?? {}),
|
|
371
|
+
warnings: [AUTO_APPLIED_WARNING]
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
return { messages, warnings: [] };
|
|
375
|
+
}
|
|
376
|
+
|
|
326
377
|
// src/search/unified-search.ts
|
|
327
378
|
init_chroma_memory_index();
|
|
328
379
|
init_raw_message_store();
|
|
@@ -363,6 +414,9 @@ async function applyReranker(reranker, query, results) {
|
|
|
363
414
|
}
|
|
364
415
|
|
|
365
416
|
// src/search/utilities.ts
|
|
417
|
+
function deriveLexicalKeywords(query) {
|
|
418
|
+
return query.toLowerCase().split(/[^\p{L}\p{N}]+/u).filter((token) => token.length >= 2).slice(0, 16);
|
|
419
|
+
}
|
|
366
420
|
var DEFAULT_LIMIT = 10;
|
|
367
421
|
var DEFAULT_THRESHOLD = 0.7;
|
|
368
422
|
var DEFAULT_SOURCES = ["memory", "insights", "knowledge"];
|
|
@@ -476,6 +530,50 @@ function toMemoryResult(result) {
|
|
|
476
530
|
metadata: result.metadata
|
|
477
531
|
};
|
|
478
532
|
}
|
|
533
|
+
function materializeSignals(lists, hit) {
|
|
534
|
+
const channels = [];
|
|
535
|
+
let semantic;
|
|
536
|
+
let lexical;
|
|
537
|
+
let entity;
|
|
538
|
+
for (const list of lists) {
|
|
539
|
+
const channel = listNameToChannel(list.name);
|
|
540
|
+
if (!channel) continue;
|
|
541
|
+
if (channels.includes(channel)) continue;
|
|
542
|
+
let found;
|
|
543
|
+
for (const candidate of list.hits) {
|
|
544
|
+
if (candidate.type === hit.type && candidate.id === hit.id) {
|
|
545
|
+
found = candidate;
|
|
546
|
+
break;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
if (!found) continue;
|
|
550
|
+
channels.push(channel);
|
|
551
|
+
if (channel === "semantic" && semantic === void 0) {
|
|
552
|
+
semantic = found.similarity;
|
|
553
|
+
} else if (channel === "lexical" && lexical === void 0) {
|
|
554
|
+
lexical = found.similarity;
|
|
555
|
+
} else if (channel === "entity" && entity === void 0) {
|
|
556
|
+
entity = found.similarity;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
if (channels.length === 0) {
|
|
560
|
+
return void 0;
|
|
561
|
+
}
|
|
562
|
+
const out = { channels };
|
|
563
|
+
if (semantic !== void 0) out.semantic = semantic;
|
|
564
|
+
if (lexical !== void 0) out.lexical = lexical;
|
|
565
|
+
if (entity !== void 0) out.entity = entity;
|
|
566
|
+
if (typeof hit.metadata.rrfScore === "number") {
|
|
567
|
+
out.rrf = hit.metadata.rrfScore;
|
|
568
|
+
}
|
|
569
|
+
return out;
|
|
570
|
+
}
|
|
571
|
+
function listNameToChannel(name) {
|
|
572
|
+
if (name === "memory-semantic") return "semantic";
|
|
573
|
+
if (name === "memory-bm25" || name === "memory-lexical") return "lexical";
|
|
574
|
+
if (name === "memory-entity") return "entity";
|
|
575
|
+
return void 0;
|
|
576
|
+
}
|
|
479
577
|
function isRawMemorySemanticResult(result) {
|
|
480
578
|
if (!result || typeof result !== "object") {
|
|
481
579
|
return false;
|
|
@@ -743,8 +841,65 @@ async function resolveSearchScopePeers(input) {
|
|
|
743
841
|
}
|
|
744
842
|
|
|
745
843
|
// src/search/unified-search.ts
|
|
746
|
-
function
|
|
747
|
-
|
|
844
|
+
function deriveEntityKeywords(query) {
|
|
845
|
+
const proper = Array.from(new Set(query.match(/\b[A-Z][a-zA-Z]{2,}\b/g) ?? [])).map((w) => w.toLowerCase());
|
|
846
|
+
const lexical = deriveLexicalKeywords(query);
|
|
847
|
+
const seen = /* @__PURE__ */ new Set();
|
|
848
|
+
const out = [];
|
|
849
|
+
for (const word of [...proper, ...lexical]) {
|
|
850
|
+
if (word.length < 2) continue;
|
|
851
|
+
if (seen.has(word)) continue;
|
|
852
|
+
seen.add(word);
|
|
853
|
+
out.push(word);
|
|
854
|
+
}
|
|
855
|
+
return out.slice(0, 16);
|
|
856
|
+
}
|
|
857
|
+
async function runEntitySearchForQuery(deps, input, limit, logger, peerPeers = []) {
|
|
858
|
+
if (typeof deps.entitySearch !== "function") {
|
|
859
|
+
return [];
|
|
860
|
+
}
|
|
861
|
+
const keywords = deriveEntityKeywords(input.query);
|
|
862
|
+
if (keywords.length === 0) {
|
|
863
|
+
return [];
|
|
864
|
+
}
|
|
865
|
+
try {
|
|
866
|
+
const entitySearch = deps.entitySearch;
|
|
867
|
+
const filters = input.botIds && input.botIds.length > 0 ? input.botIds : [void 0];
|
|
868
|
+
const matches = (await Promise.all(
|
|
869
|
+
filters.map(
|
|
870
|
+
(botId) => entitySearch({
|
|
871
|
+
userId: input.userId,
|
|
872
|
+
keywords,
|
|
873
|
+
limit: Math.ceil(limit / filters.length),
|
|
874
|
+
botId,
|
|
875
|
+
...peerPeers.length > 0 ? { peers: peerPeers } : {}
|
|
876
|
+
})
|
|
877
|
+
)
|
|
878
|
+
)).flat();
|
|
879
|
+
return matches.filter(
|
|
880
|
+
(m) => typeof m?.messageId === "string" && typeof m?.label === "string" && typeof m?.score === "number"
|
|
881
|
+
).map((m) => ({
|
|
882
|
+
type: "memory",
|
|
883
|
+
id: m.messageId,
|
|
884
|
+
// The host's `entitySearch` returns match ids + labels only,
|
|
885
|
+
// so we surface the entity label as `content`. When the same
|
|
886
|
+
// `messageId` is also returned by the semantic/lexical channel
|
|
887
|
+
// (RRF path), the real message content is preserved via the
|
|
888
|
+
// shared `(type, id)` key; otherwise callers can re-fetch the
|
|
889
|
+
// original message by `id` (set in metadata as `sourceMessageId`).
|
|
890
|
+
content: m.label,
|
|
891
|
+
similarity: m.score,
|
|
892
|
+
metadata: {
|
|
893
|
+
scoring: "entity",
|
|
894
|
+
entityLabel: m.label,
|
|
895
|
+
isEntityProjection: true,
|
|
896
|
+
sourceMessageId: m.messageId
|
|
897
|
+
}
|
|
898
|
+
}));
|
|
899
|
+
} catch (error) {
|
|
900
|
+
logger.warn?.("[memory-store] entity search failed:", error);
|
|
901
|
+
return [];
|
|
902
|
+
}
|
|
748
903
|
}
|
|
749
904
|
function parseDateBoundary(value, role) {
|
|
750
905
|
const trimmed = value.trim();
|
|
@@ -1184,10 +1339,39 @@ function createUnifiedSearch(deps = {}) {
|
|
|
1184
1339
|
});
|
|
1185
1340
|
}
|
|
1186
1341
|
}
|
|
1187
|
-
|
|
1342
|
+
let entity;
|
|
1343
|
+
if (typeof deps.entitySearch === "function") {
|
|
1344
|
+
entity = filterByDateRange(
|
|
1345
|
+
await runEntitySearchForQuery(deps, input, limit, logger, peerPeers),
|
|
1346
|
+
input.dateFrom,
|
|
1347
|
+
input.dateTo
|
|
1348
|
+
);
|
|
1349
|
+
if (entity.length === 0) {
|
|
1350
|
+
entity = void 0;
|
|
1351
|
+
} else if (normalizeUnifiedMemoryMergeStrategy(
|
|
1352
|
+
input.mergeStrategy ?? deps.reasoning?.defaultMergeStrategy ?? "rrf"
|
|
1353
|
+
) !== "rrf") {
|
|
1354
|
+
warnings.push({
|
|
1355
|
+
source: "memory",
|
|
1356
|
+
code: "memory_entity_requires_rrf",
|
|
1357
|
+
message: "Entity channel results are fused via RRF; with mergeStrategy:'similarity' standalone entity hits are suppressed (only the entity channel signal is kept on co-occurring hits). Request mergeStrategy:'rrf' to surface entity matches."
|
|
1358
|
+
});
|
|
1359
|
+
}
|
|
1360
|
+
} else if (input.mergeStrategy === "rrf") {
|
|
1361
|
+
warnings.push({
|
|
1362
|
+
source: "memory",
|
|
1363
|
+
code: "memory_entity_search_not_configured",
|
|
1364
|
+
message: "RRF merge requested but entity search is not configured; falling back to semantic+lexical RRF."
|
|
1365
|
+
});
|
|
1366
|
+
}
|
|
1367
|
+
const out = {
|
|
1188
1368
|
semantic: filterByDateRange(semantic, input.dateFrom, input.dateTo),
|
|
1189
1369
|
lexical: filterByDateRange(lexical, input.dateFrom, input.dateTo)
|
|
1190
1370
|
};
|
|
1371
|
+
if (entity && entity.length > 0) {
|
|
1372
|
+
out.entity = entity;
|
|
1373
|
+
}
|
|
1374
|
+
return out;
|
|
1191
1375
|
}
|
|
1192
1376
|
async function searchUnifiedMemory(input) {
|
|
1193
1377
|
const query = input.query.trim();
|
|
@@ -1458,32 +1642,56 @@ function createUnifiedSearch(deps = {}) {
|
|
|
1458
1642
|
}
|
|
1459
1643
|
function mergeAcrossSources(input) {
|
|
1460
1644
|
const memorySubs = input.memorySubs ?? { semantic: [], lexical: [] };
|
|
1645
|
+
const lists = buildChannelLists(memorySubs, input.insightHits, input.knowledgeHits);
|
|
1646
|
+
if (input.strategy !== "rrf") {
|
|
1647
|
+
const seen = /* @__PURE__ */ new Map();
|
|
1648
|
+
for (const hit of [
|
|
1649
|
+
...memorySubs.semantic,
|
|
1650
|
+
...memorySubs.lexical,
|
|
1651
|
+
...input.insightHits,
|
|
1652
|
+
...input.knowledgeHits
|
|
1653
|
+
]) {
|
|
1654
|
+
const key = `${hit.type}::${hit.id}`;
|
|
1655
|
+
const existing = seen.get(key);
|
|
1656
|
+
if (!existing || hit.similarity > existing.similarity) {
|
|
1657
|
+
seen.set(key, hit);
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
const merged2 = mergeUnifiedMemorySearchResults(Array.from(seen.values()), input.limit);
|
|
1661
|
+
return attachSignals(merged2, lists);
|
|
1662
|
+
}
|
|
1461
1663
|
const all = [
|
|
1462
1664
|
...memorySubs.semantic,
|
|
1463
1665
|
...memorySubs.lexical,
|
|
1666
|
+
...memorySubs.entity ?? [],
|
|
1464
1667
|
...input.insightHits,
|
|
1465
1668
|
...input.knowledgeHits
|
|
1466
1669
|
];
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1670
|
+
const merged = lists.length <= 1 ? mergeUnifiedMemorySearchResults(all, input.limit) : mergeUnifiedMemorySearchResultsRrf(lists, input.limit);
|
|
1671
|
+
return attachSignals(merged, lists);
|
|
1672
|
+
}
|
|
1673
|
+
function buildChannelLists(memorySubs, insightHits, knowledgeHits) {
|
|
1470
1674
|
const lists = [];
|
|
1471
|
-
if (memorySubs.semantic.length > 0) {
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
lists.push({ name: "memory-bm25", hits: memorySubs.lexical });
|
|
1476
|
-
}
|
|
1477
|
-
if (input.insightHits.length > 0) {
|
|
1478
|
-
lists.push({ name: "insights", hits: input.insightHits });
|
|
1479
|
-
}
|
|
1480
|
-
if (input.knowledgeHits.length > 0) {
|
|
1481
|
-
lists.push({ name: "knowledge", hits: input.knowledgeHits });
|
|
1675
|
+
if (memorySubs.semantic.length > 0) lists.push({ name: "memory-semantic", hits: memorySubs.semantic });
|
|
1676
|
+
if (memorySubs.lexical.length > 0) lists.push({ name: "memory-bm25", hits: memorySubs.lexical });
|
|
1677
|
+
if (memorySubs.entity && memorySubs.entity.length > 0) {
|
|
1678
|
+
lists.push({ name: "memory-entity", hits: memorySubs.entity });
|
|
1482
1679
|
}
|
|
1483
|
-
if (
|
|
1484
|
-
|
|
1680
|
+
if (insightHits.length > 0) lists.push({ name: "insights", hits: insightHits });
|
|
1681
|
+
if (knowledgeHits.length > 0) lists.push({ name: "knowledge", hits: knowledgeHits });
|
|
1682
|
+
return lists;
|
|
1683
|
+
}
|
|
1684
|
+
function attachSignals(hits, lists) {
|
|
1685
|
+
if (hits.length === 0 || lists.length === 0) {
|
|
1686
|
+
return hits;
|
|
1485
1687
|
}
|
|
1486
|
-
return
|
|
1688
|
+
return hits.map((hit) => {
|
|
1689
|
+
const signals = materializeSignals(lists, hit);
|
|
1690
|
+
if (!signals) {
|
|
1691
|
+
return hit;
|
|
1692
|
+
}
|
|
1693
|
+
return { ...hit, signals };
|
|
1694
|
+
});
|
|
1487
1695
|
}
|
|
1488
1696
|
|
|
1489
1697
|
// src/search/apply-reflect.ts
|
|
@@ -1602,6 +1810,177 @@ function makePlanSummary(plan) {
|
|
|
1602
1810
|
}
|
|
1603
1811
|
return lines.join("\n");
|
|
1604
1812
|
}
|
|
1813
|
+
var DEFAULT_CANDIDATE_LIMIT = 50;
|
|
1814
|
+
async function deriveFacts(deps, input, logger = console) {
|
|
1815
|
+
const warnings = [];
|
|
1816
|
+
if (typeof deps.deriver !== "function") {
|
|
1817
|
+
warnings.push({
|
|
1818
|
+
code: "derive_deriver_not_configured",
|
|
1819
|
+
message: "No `deriver` is wired into the unified search deps; returning an empty fact list."
|
|
1820
|
+
});
|
|
1821
|
+
return { facts: [], warnings };
|
|
1822
|
+
}
|
|
1823
|
+
const limit = Math.max(1, Math.min(500, input.candidateLimit ?? DEFAULT_CANDIDATE_LIMIT));
|
|
1824
|
+
if (!input.candidateTexts || input.candidateTexts.length === 0) {
|
|
1825
|
+
if (!input.query) {
|
|
1826
|
+
warnings.push({
|
|
1827
|
+
code: "derive_fallback_query_noise",
|
|
1828
|
+
message: "No `query` supplied and no `candidateTexts`; lexical fallback derives keywords from `userId + botIds`, which is rarely useful. Pass `query` (or `candidateTexts`) for topical derivation."
|
|
1829
|
+
});
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1832
|
+
let candidateTexts;
|
|
1833
|
+
if (input.candidateTexts && input.candidateTexts.length > 0) {
|
|
1834
|
+
candidateTexts = input.candidateTexts;
|
|
1835
|
+
} else {
|
|
1836
|
+
candidateTexts = await loadCandidateTexts(deps, input, limit, logger);
|
|
1837
|
+
if (candidateTexts.length === 0) {
|
|
1838
|
+
warnings.push({
|
|
1839
|
+
code: "derive_no_candidates",
|
|
1840
|
+
message: "No candidate fact texts available for derivation"
|
|
1841
|
+
});
|
|
1842
|
+
return { facts: [], warnings };
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
let facts;
|
|
1846
|
+
try {
|
|
1847
|
+
facts = await deps.deriver({
|
|
1848
|
+
userId: input.userId,
|
|
1849
|
+
userScope: {
|
|
1850
|
+
userId: input.userId,
|
|
1851
|
+
botIds: input.botIds,
|
|
1852
|
+
dateFrom: input.dateFrom,
|
|
1853
|
+
dateTo: input.dateTo
|
|
1854
|
+
},
|
|
1855
|
+
recentFactTexts: candidateTexts,
|
|
1856
|
+
...input.window ? { window: input.window } : {}
|
|
1857
|
+
});
|
|
1858
|
+
} catch (error) {
|
|
1859
|
+
logger.warn?.("[memory-store/derive] deriver threw:", error);
|
|
1860
|
+
warnings.push({
|
|
1861
|
+
code: "derive_deriver_failed",
|
|
1862
|
+
message: error.message ?? "deriver failed"
|
|
1863
|
+
});
|
|
1864
|
+
return { facts: [], warnings };
|
|
1865
|
+
}
|
|
1866
|
+
if (!Array.isArray(facts)) {
|
|
1867
|
+
warnings.push({
|
|
1868
|
+
code: "derive_deriver_returned_invalid_shape",
|
|
1869
|
+
message: `deriver returned ${typeof facts}; expected DerivedFact[]`
|
|
1870
|
+
});
|
|
1871
|
+
return { facts: [], warnings };
|
|
1872
|
+
}
|
|
1873
|
+
const normalized = [];
|
|
1874
|
+
const now = Date.now();
|
|
1875
|
+
for (const fact of facts) {
|
|
1876
|
+
if (!fact || typeof fact.text !== "string" || fact.text.length === 0 || typeof fact.kind !== "string" || !isDerivedKind(fact.kind) || !Array.isArray(fact.sources)) {
|
|
1877
|
+
continue;
|
|
1878
|
+
}
|
|
1879
|
+
normalized.push({
|
|
1880
|
+
...fact,
|
|
1881
|
+
derivedAt: typeof fact.derivedAt === "number" ? fact.derivedAt : now,
|
|
1882
|
+
sources: fact.sources.filter((s) => typeof s === "string")
|
|
1883
|
+
});
|
|
1884
|
+
}
|
|
1885
|
+
if (input.persist && normalized.length > 0) {
|
|
1886
|
+
try {
|
|
1887
|
+
await input.persist(normalized);
|
|
1888
|
+
} catch (error) {
|
|
1889
|
+
logger.warn?.("[memory-store/derive] persist callback threw:", error);
|
|
1890
|
+
warnings.push({
|
|
1891
|
+
code: "derive_persist_failed",
|
|
1892
|
+
message: error.message ?? "derive persist failed"
|
|
1893
|
+
});
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1896
|
+
return { facts: normalized, warnings };
|
|
1897
|
+
}
|
|
1898
|
+
async function loadCandidateTexts(deps, input, limit, logger) {
|
|
1899
|
+
if (typeof deps.searchRawMessagesLexical !== "function") {
|
|
1900
|
+
return [];
|
|
1901
|
+
}
|
|
1902
|
+
const queryText = (input.query ?? `${input.userId} ${input.botIds?.join(" ") ?? ""}`).trim();
|
|
1903
|
+
const keywords = deriveLexicalKeywords(queryText);
|
|
1904
|
+
if (keywords.length === 0) {
|
|
1905
|
+
return [];
|
|
1906
|
+
}
|
|
1907
|
+
try {
|
|
1908
|
+
const filters = input.botIds && input.botIds.length > 0 ? input.botIds : [void 0];
|
|
1909
|
+
const factTypes = input.factTypes?.length ? input.factTypes : void 0;
|
|
1910
|
+
const hits = (await Promise.all(
|
|
1911
|
+
filters.map(
|
|
1912
|
+
(botId) => deps.searchRawMessagesLexical?.({
|
|
1913
|
+
userId: input.userId,
|
|
1914
|
+
keywords,
|
|
1915
|
+
limit: Math.ceil(limit / filters.length),
|
|
1916
|
+
botId,
|
|
1917
|
+
...input.peers && input.peers.length > 0 ? { peers: input.peers } : {},
|
|
1918
|
+
...factTypes ? { factTypes } : {}
|
|
1919
|
+
})
|
|
1920
|
+
)
|
|
1921
|
+
)).flat().filter(isRawMemorySemanticResult);
|
|
1922
|
+
return hits.map((h) => toMemoryResult(h).content);
|
|
1923
|
+
} catch (error) {
|
|
1924
|
+
logger.warn?.("[memory-store/derive] lexical candidate fetch failed:", error);
|
|
1925
|
+
return [];
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1928
|
+
async function distillRawMessage(deps, input, logger = console) {
|
|
1929
|
+
const warnings = [];
|
|
1930
|
+
if (typeof deps.entityExtractor !== "function") {
|
|
1931
|
+
warnings.push({
|
|
1932
|
+
code: "distill_extractor_not_configured",
|
|
1933
|
+
message: "No `entityExtractor` is wired into the unified search deps; returning an empty entity list."
|
|
1934
|
+
});
|
|
1935
|
+
return { edges: [], warnings };
|
|
1936
|
+
}
|
|
1937
|
+
let edges;
|
|
1938
|
+
try {
|
|
1939
|
+
edges = await deps.entityExtractor({
|
|
1940
|
+
userId: input.userId,
|
|
1941
|
+
messageId: input.messageId,
|
|
1942
|
+
content: input.content
|
|
1943
|
+
});
|
|
1944
|
+
} catch (error) {
|
|
1945
|
+
logger.warn?.("[memory-store/distill] extractor threw:", error);
|
|
1946
|
+
warnings.push({
|
|
1947
|
+
code: "distill_extractor_failed",
|
|
1948
|
+
message: error.message ?? "entity extractor failed"
|
|
1949
|
+
});
|
|
1950
|
+
return { edges: [], warnings };
|
|
1951
|
+
}
|
|
1952
|
+
if (!Array.isArray(edges)) {
|
|
1953
|
+
warnings.push({
|
|
1954
|
+
code: "distill_extractor_returned_invalid_shape",
|
|
1955
|
+
message: `entityExtractor returned ${typeof edges}; expected EntityEdge[]`
|
|
1956
|
+
});
|
|
1957
|
+
return { edges: [], warnings };
|
|
1958
|
+
}
|
|
1959
|
+
const normalized = [];
|
|
1960
|
+
const now = Date.now();
|
|
1961
|
+
for (const edge of edges) {
|
|
1962
|
+
if (!edge || typeof edge.label !== "string" || edge.label.length === 0 || typeof edge.kind !== "string" || !isEntityKind(edge.kind) || typeof edge.relation !== "string" || typeof edge.sourceMessageId !== "string") {
|
|
1963
|
+
continue;
|
|
1964
|
+
}
|
|
1965
|
+
normalized.push({
|
|
1966
|
+
...edge,
|
|
1967
|
+
label: edge.label.trim().toLowerCase(),
|
|
1968
|
+
extractedAt: typeof edge.extractedAt === "number" ? edge.extractedAt : now
|
|
1969
|
+
});
|
|
1970
|
+
}
|
|
1971
|
+
if (input.persist && normalized.length > 0) {
|
|
1972
|
+
try {
|
|
1973
|
+
await input.persist(normalized);
|
|
1974
|
+
} catch (error) {
|
|
1975
|
+
logger.warn?.("[memory-store/distill] persist callback threw:", error);
|
|
1976
|
+
warnings.push({
|
|
1977
|
+
code: "distill_persist_failed",
|
|
1978
|
+
message: error.message ?? "distill persist failed"
|
|
1979
|
+
});
|
|
1980
|
+
}
|
|
1981
|
+
}
|
|
1982
|
+
return { edges: normalized, warnings };
|
|
1983
|
+
}
|
|
1605
1984
|
var DEFAULT_MAX_FACTS = 1e3;
|
|
1606
1985
|
var LOW_CONFIDENCE_THRESHOLD = 0.1;
|
|
1607
1986
|
function toHRR(data) {
|
|
@@ -1765,34 +2144,6 @@ init_raw_message_store();
|
|
|
1765
2144
|
init_sqlite_raw_message_store();
|
|
1766
2145
|
var DEFAULT_NAME = "@melandlabs/memory-store";
|
|
1767
2146
|
var DEFAULT_VERSION = "0.1.0";
|
|
1768
|
-
async function embedMissingMessages(messages, deps) {
|
|
1769
|
-
if (typeof deps.embedQuery !== "function") {
|
|
1770
|
-
return messages;
|
|
1771
|
-
}
|
|
1772
|
-
const out = [];
|
|
1773
|
-
for (const message of messages) {
|
|
1774
|
-
if (Array.isArray(message.embedding) && message.embedding.length > 0) {
|
|
1775
|
-
out.push(message);
|
|
1776
|
-
continue;
|
|
1777
|
-
}
|
|
1778
|
-
if (typeof message.content !== "string" || message.content.length === 0) {
|
|
1779
|
-
out.push(message);
|
|
1780
|
-
continue;
|
|
1781
|
-
}
|
|
1782
|
-
const vector = await deps.embedQuery({
|
|
1783
|
-
userId: message.userId,
|
|
1784
|
-
query: message.content
|
|
1785
|
-
});
|
|
1786
|
-
out.push({
|
|
1787
|
-
...message,
|
|
1788
|
-
embedding: vector,
|
|
1789
|
-
embeddingModel: message.embeddingModel ?? "server",
|
|
1790
|
-
embeddingDimensions: vector.length,
|
|
1791
|
-
embeddingUpdatedAt: Date.now()
|
|
1792
|
-
});
|
|
1793
|
-
}
|
|
1794
|
-
return out;
|
|
1795
|
-
}
|
|
1796
2147
|
async function startMcpServer(options = {}) {
|
|
1797
2148
|
const server = new McpServer({
|
|
1798
2149
|
name: options.name ?? DEFAULT_NAME,
|
|
@@ -1919,7 +2270,11 @@ async function startMcpServer(options = {}) {
|
|
|
1919
2270
|
const a = args;
|
|
1920
2271
|
const manager2 = await rawStore.getManager();
|
|
1921
2272
|
const incoming = [a.message].map((m) => ({ ...m, userId: m.userId ?? a.userId }));
|
|
1922
|
-
const messages
|
|
2273
|
+
const { messages, warnings } = await applyEmbedOnInsertPolicy(
|
|
2274
|
+
incoming,
|
|
2275
|
+
a.embedOnInsert,
|
|
2276
|
+
options.unified
|
|
2277
|
+
);
|
|
1923
2278
|
let result;
|
|
1924
2279
|
if (typeof manager2.upsertRawMessages === "function") {
|
|
1925
2280
|
result = await manager2.upsertRawMessages({
|
|
@@ -1941,7 +2296,12 @@ async function startMcpServer(options = {}) {
|
|
|
1941
2296
|
content: [
|
|
1942
2297
|
{
|
|
1943
2298
|
type: "text",
|
|
1944
|
-
text: JSON.stringify({
|
|
2299
|
+
text: JSON.stringify({
|
|
2300
|
+
ok: true,
|
|
2301
|
+
count: messages.length,
|
|
2302
|
+
result,
|
|
2303
|
+
...warnings.length > 0 ? { warnings } : {}
|
|
2304
|
+
})
|
|
1945
2305
|
}
|
|
1946
2306
|
]
|
|
1947
2307
|
};
|
|
@@ -2003,6 +2363,109 @@ async function startMcpServer(options = {}) {
|
|
|
2003
2363
|
};
|
|
2004
2364
|
}
|
|
2005
2365
|
);
|
|
2366
|
+
const distillSchema = {
|
|
2367
|
+
userId: z.string(),
|
|
2368
|
+
messageId: z.string().min(1),
|
|
2369
|
+
content: z.string().min(1)
|
|
2370
|
+
};
|
|
2371
|
+
server.registerTool(
|
|
2372
|
+
"memory.distill",
|
|
2373
|
+
{
|
|
2374
|
+
title: "Distill Entities",
|
|
2375
|
+
description: "Extract `EntityEdge`s from a single raw message via the host's `unified.entityExtractor`. Returns `{ edges, warnings }`. Without the extractor wired in, returns an empty list with a `distill_extractor_not_configured` warning. Persistence is the host's responsibility \u2014 the MCP transport cannot carry host-side `persist` callbacks. Call `distillRawMessage` directly from host code if you need round-trip persistence.",
|
|
2376
|
+
// biome-ignore lint/suspicious/noExplicitAny: MCP SDK expects Zod schema type
|
|
2377
|
+
inputSchema: distillSchema
|
|
2378
|
+
},
|
|
2379
|
+
async (args) => {
|
|
2380
|
+
const a = args;
|
|
2381
|
+
const input = {
|
|
2382
|
+
userId: a.userId,
|
|
2383
|
+
messageId: a.messageId,
|
|
2384
|
+
content: a.content
|
|
2385
|
+
};
|
|
2386
|
+
try {
|
|
2387
|
+
const result = await distillRawMessage(options.unified ?? {}, input, console);
|
|
2388
|
+
return {
|
|
2389
|
+
content: [{ type: "text", text: JSON.stringify(result) }]
|
|
2390
|
+
};
|
|
2391
|
+
} catch (error) {
|
|
2392
|
+
console.error("[memory-store/mcp] distill failed:", error);
|
|
2393
|
+
return {
|
|
2394
|
+
content: [
|
|
2395
|
+
{
|
|
2396
|
+
type: "text",
|
|
2397
|
+
text: JSON.stringify({ error: error.message ?? "distill failed" })
|
|
2398
|
+
}
|
|
2399
|
+
],
|
|
2400
|
+
isError: true
|
|
2401
|
+
};
|
|
2402
|
+
}
|
|
2403
|
+
}
|
|
2404
|
+
);
|
|
2405
|
+
const deriveSchema = {
|
|
2406
|
+
userId: z.string(),
|
|
2407
|
+
query: z.string().optional().describe(
|
|
2408
|
+
"Optional topical query used to derive lexical keywords when `candidateTexts` is omitted. Loop-engine schedulers should pass the topic they want synthesized facts about (e.g. 'cat preferences'). Without it the lexical fallback uses `userId + botIds` which is rarely useful."
|
|
2409
|
+
),
|
|
2410
|
+
botIds: z.array(z.string()).optional(),
|
|
2411
|
+
peers: z.array(
|
|
2412
|
+
z.object({
|
|
2413
|
+
kind: z.enum(["user", "agent"]),
|
|
2414
|
+
id: z.string().min(1)
|
|
2415
|
+
})
|
|
2416
|
+
).optional().describe("Optional peer scope forwarded to the lexical candidate-fetch fallback."),
|
|
2417
|
+
factTypes: z.array(z.string()).optional().describe("Optional FactType filter forwarded to the lexical candidate-fetch fallback."),
|
|
2418
|
+
dateFrom: z.string().optional(),
|
|
2419
|
+
dateTo: z.string().optional(),
|
|
2420
|
+
windowFrom: z.number().optional().describe("Optional time-window start (ms) forwarded to the deriver."),
|
|
2421
|
+
windowTo: z.number().optional().describe("Optional time-window end (ms) forwarded to the deriver."),
|
|
2422
|
+
candidateTexts: z.array(z.string()).optional().describe(
|
|
2423
|
+
"Pre-computed candidate fact texts. When omitted, the SDK pulls up to `candidateLimit` (default 50) texts via the lexical sub-query."
|
|
2424
|
+
),
|
|
2425
|
+
candidateLimit: z.number().int().min(1).max(500).optional()
|
|
2426
|
+
};
|
|
2427
|
+
server.registerTool(
|
|
2428
|
+
"memory.derive",
|
|
2429
|
+
{
|
|
2430
|
+
title: "Derive Facts",
|
|
2431
|
+
description: "Synthesize `DerivedFact`s over a window of candidate fact texts via the host's `unified.deriver`. Returns `{ facts, warnings }`. Without the deriver wired in, returns an empty list with a `derive_deriver_not_configured` warning. Persistence is the host's responsibility \u2014 the MCP transport cannot carry host-side `persist` callbacks. Call `deriveFacts` directly from host code if you need round-trip persistence.",
|
|
2432
|
+
// biome-ignore lint/suspicious/noExplicitAny: MCP SDK expects Zod schema type
|
|
2433
|
+
inputSchema: deriveSchema
|
|
2434
|
+
},
|
|
2435
|
+
async (args) => {
|
|
2436
|
+
const a = args;
|
|
2437
|
+
const window = a.windowFrom !== void 0 && a.windowTo !== void 0 ? { from: a.windowFrom, to: a.windowTo } : void 0;
|
|
2438
|
+
const input = {
|
|
2439
|
+
userId: a.userId,
|
|
2440
|
+
...a.query ? { query: a.query } : {},
|
|
2441
|
+
botIds: a.botIds,
|
|
2442
|
+
...a.peers && a.peers.length > 0 ? { peers: a.peers } : {},
|
|
2443
|
+
...a.factTypes && a.factTypes.length > 0 ? { factTypes: a.factTypes.filter(isFactType) } : {},
|
|
2444
|
+
dateFrom: a.dateFrom,
|
|
2445
|
+
dateTo: a.dateTo,
|
|
2446
|
+
...window ? { window } : {},
|
|
2447
|
+
...a.candidateTexts ? { candidateTexts: a.candidateTexts } : {},
|
|
2448
|
+
...a.candidateLimit !== void 0 ? { candidateLimit: a.candidateLimit } : {}
|
|
2449
|
+
};
|
|
2450
|
+
try {
|
|
2451
|
+
const result = await deriveFacts(options.unified ?? {}, input, console);
|
|
2452
|
+
return {
|
|
2453
|
+
content: [{ type: "text", text: JSON.stringify(result) }]
|
|
2454
|
+
};
|
|
2455
|
+
} catch (error) {
|
|
2456
|
+
console.error("[memory-store/mcp] derive failed:", error);
|
|
2457
|
+
return {
|
|
2458
|
+
content: [
|
|
2459
|
+
{
|
|
2460
|
+
type: "text",
|
|
2461
|
+
text: JSON.stringify({ error: error.message ?? "derive failed" })
|
|
2462
|
+
}
|
|
2463
|
+
],
|
|
2464
|
+
isError: true
|
|
2465
|
+
};
|
|
2466
|
+
}
|
|
2467
|
+
}
|
|
2468
|
+
);
|
|
2006
2469
|
const vsaVocabularyEntry = {
|
|
2007
2470
|
label: z.string().min(1),
|
|
2008
2471
|
vector: z.array(z.number()).describe("Float32 vector for the vocabulary entry.")
|
|
@@ -2982,6 +3445,11 @@ async function main() {
|
|
|
2982
3445
|
const shutdown = async (signal) => {
|
|
2983
3446
|
console.error(`[memory-store/mcp] ${signal} received, shutting down\u2026`);
|
|
2984
3447
|
await server.close();
|
|
3448
|
+
try {
|
|
3449
|
+
await closeSQLiteVsaStore();
|
|
3450
|
+
} catch (error) {
|
|
3451
|
+
console.error("[memory-store/mcp] closeSQLiteVsaStore failed:", error);
|
|
3452
|
+
}
|
|
2985
3453
|
process.exit(0);
|
|
2986
3454
|
};
|
|
2987
3455
|
process.once("SIGINT", shutdown);
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { U as UnifiedSearchDeps } from '../config-
|
|
2
|
+
import { U as UnifiedSearchDeps } from '../config-DfvVZ--0.js';
|
|
3
3
|
import '@melandlabs/memory-consolidation';
|
|
4
4
|
import '@melandlabs/contracts/peer';
|
|
5
5
|
import '@melandlabs/contracts';
|
|
6
|
+
import '@melandlabs/contracts/derived-fact';
|
|
7
|
+
import '@melandlabs/contracts/entity-edge';
|
|
6
8
|
|
|
7
9
|
/**
|
|
8
10
|
* Shared argv parsing + `unified.*` wiring for the memory-store CLI bins.
|