@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.
@@ -1,5 +1,5 @@
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';
@@ -9,6 +9,8 @@ import { getSQLiteVsaStore, closeSQLiteVsaStore, SQLiteRawMessageManager } from
9
9
  import { serve } from '@hono/node-server';
10
10
  import { Hono } from 'hono';
11
11
  import { applyReflectedConsolidationPlan } from '@melandlabs/memory-consolidation';
12
+ import { isDerivedKind } from '@melandlabs/contracts/derived-fact';
13
+ import { isEntityKind } from '@melandlabs/contracts/entity-edge';
12
14
  import { superpose, bind, unbind, cleanup, cosineSimilarity } from '@melandlabs/vsa';
13
15
 
14
16
  var __defProp = Object.defineProperty;
@@ -322,6 +324,55 @@ var init_raw_message_store = __esm({
322
324
  }
323
325
  });
324
326
 
327
+ // src/embed-on-insert.ts
328
+ async function embedMissingMessages(messages, deps) {
329
+ if (typeof deps.embedQuery !== "function") {
330
+ return messages;
331
+ }
332
+ const out = [];
333
+ for (const message of messages) {
334
+ if (Array.isArray(message.embedding) && message.embedding.length > 0) {
335
+ out.push(message);
336
+ continue;
337
+ }
338
+ if (typeof message.content !== "string" || message.content.length === 0) {
339
+ out.push(message);
340
+ continue;
341
+ }
342
+ const vector = await deps.embedQuery({
343
+ userId: message.userId,
344
+ query: message.content
345
+ });
346
+ out.push({
347
+ ...message,
348
+ embedding: vector,
349
+ embeddingModel: message.embeddingModel ?? "server",
350
+ embeddingDimensions: vector.length,
351
+ embeddingUpdatedAt: Date.now()
352
+ });
353
+ }
354
+ return out;
355
+ }
356
+ var AUTO_APPLIED_WARNING = {
357
+ code: "embed_on_insert_auto_applied",
358
+ 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."
359
+ };
360
+ async function applyEmbedOnInsertPolicy(incoming, embedOnInsert, unified) {
361
+ const messages = incoming;
362
+ const anyMissing = incoming.some((m) => !Array.isArray(m.embedding) || m.embedding.length === 0);
363
+ const embedderWired = typeof unified?.embedQuery === "function";
364
+ if (embedOnInsert === true && anyMissing) {
365
+ return { messages: await embedMissingMessages(messages, unified ?? {}), warnings: [] };
366
+ }
367
+ if (embedOnInsert !== true && anyMissing && embedderWired) {
368
+ return {
369
+ messages: await embedMissingMessages(messages, unified ?? {}),
370
+ warnings: [AUTO_APPLIED_WARNING]
371
+ };
372
+ }
373
+ return { messages, warnings: [] };
374
+ }
375
+
325
376
  // src/search/unified-search.ts
326
377
  init_chroma_memory_index();
327
378
  init_raw_message_store();
@@ -362,6 +413,9 @@ async function applyReranker(reranker, query, results) {
362
413
  }
363
414
 
364
415
  // src/search/utilities.ts
416
+ function deriveLexicalKeywords(query) {
417
+ return query.toLowerCase().split(/[^\p{L}\p{N}]+/u).filter((token) => token.length >= 2).slice(0, 16);
418
+ }
365
419
  var DEFAULT_LIMIT = 10;
366
420
  var DEFAULT_THRESHOLD = 0.7;
367
421
  var DEFAULT_SOURCES = ["memory", "insights", "knowledge"];
@@ -475,6 +529,50 @@ function toMemoryResult(result) {
475
529
  metadata: result.metadata
476
530
  };
477
531
  }
532
+ function materializeSignals(lists, hit) {
533
+ const channels = [];
534
+ let semantic;
535
+ let lexical;
536
+ let entity;
537
+ for (const list of lists) {
538
+ const channel = listNameToChannel(list.name);
539
+ if (!channel) continue;
540
+ if (channels.includes(channel)) continue;
541
+ let found;
542
+ for (const candidate of list.hits) {
543
+ if (candidate.type === hit.type && candidate.id === hit.id) {
544
+ found = candidate;
545
+ break;
546
+ }
547
+ }
548
+ if (!found) continue;
549
+ channels.push(channel);
550
+ if (channel === "semantic" && semantic === void 0) {
551
+ semantic = found.similarity;
552
+ } else if (channel === "lexical" && lexical === void 0) {
553
+ lexical = found.similarity;
554
+ } else if (channel === "entity" && entity === void 0) {
555
+ entity = found.similarity;
556
+ }
557
+ }
558
+ if (channels.length === 0) {
559
+ return void 0;
560
+ }
561
+ const out = { channels };
562
+ if (semantic !== void 0) out.semantic = semantic;
563
+ if (lexical !== void 0) out.lexical = lexical;
564
+ if (entity !== void 0) out.entity = entity;
565
+ if (typeof hit.metadata.rrfScore === "number") {
566
+ out.rrf = hit.metadata.rrfScore;
567
+ }
568
+ return out;
569
+ }
570
+ function listNameToChannel(name) {
571
+ if (name === "memory-semantic") return "semantic";
572
+ if (name === "memory-bm25" || name === "memory-lexical") return "lexical";
573
+ if (name === "memory-entity") return "entity";
574
+ return void 0;
575
+ }
478
576
  function isRawMemorySemanticResult(result) {
479
577
  if (!result || typeof result !== "object") {
480
578
  return false;
@@ -742,8 +840,65 @@ async function resolveSearchScopePeers(input) {
742
840
  }
743
841
 
744
842
  // src/search/unified-search.ts
745
- function deriveLexicalKeywords(query) {
746
- return query.toLowerCase().split(/[^\p{L}\p{N}]+/u).filter((token) => token.length >= 2).slice(0, 16);
843
+ function deriveEntityKeywords(query) {
844
+ const proper = Array.from(new Set(query.match(/\b[A-Z][a-zA-Z]{2,}\b/g) ?? [])).map((w) => w.toLowerCase());
845
+ const lexical = deriveLexicalKeywords(query);
846
+ const seen = /* @__PURE__ */ new Set();
847
+ const out = [];
848
+ for (const word of [...proper, ...lexical]) {
849
+ if (word.length < 2) continue;
850
+ if (seen.has(word)) continue;
851
+ seen.add(word);
852
+ out.push(word);
853
+ }
854
+ return out.slice(0, 16);
855
+ }
856
+ async function runEntitySearchForQuery(deps, input, limit, logger, peerPeers = []) {
857
+ if (typeof deps.entitySearch !== "function") {
858
+ return [];
859
+ }
860
+ const keywords = deriveEntityKeywords(input.query);
861
+ if (keywords.length === 0) {
862
+ return [];
863
+ }
864
+ try {
865
+ const entitySearch = deps.entitySearch;
866
+ const filters = input.botIds && input.botIds.length > 0 ? input.botIds : [void 0];
867
+ const matches = (await Promise.all(
868
+ filters.map(
869
+ (botId) => entitySearch({
870
+ userId: input.userId,
871
+ keywords,
872
+ limit: Math.ceil(limit / filters.length),
873
+ botId,
874
+ ...peerPeers.length > 0 ? { peers: peerPeers } : {}
875
+ })
876
+ )
877
+ )).flat();
878
+ return matches.filter(
879
+ (m) => typeof m?.messageId === "string" && typeof m?.label === "string" && typeof m?.score === "number"
880
+ ).map((m) => ({
881
+ type: "memory",
882
+ id: m.messageId,
883
+ // The host's `entitySearch` returns match ids + labels only,
884
+ // so we surface the entity label as `content`. When the same
885
+ // `messageId` is also returned by the semantic/lexical channel
886
+ // (RRF path), the real message content is preserved via the
887
+ // shared `(type, id)` key; otherwise callers can re-fetch the
888
+ // original message by `id` (set in metadata as `sourceMessageId`).
889
+ content: m.label,
890
+ similarity: m.score,
891
+ metadata: {
892
+ scoring: "entity",
893
+ entityLabel: m.label,
894
+ isEntityProjection: true,
895
+ sourceMessageId: m.messageId
896
+ }
897
+ }));
898
+ } catch (error) {
899
+ logger.warn?.("[memory-store] entity search failed:", error);
900
+ return [];
901
+ }
747
902
  }
748
903
  function parseDateBoundary(value, role) {
749
904
  const trimmed = value.trim();
@@ -1183,10 +1338,39 @@ function createUnifiedSearch(deps = {}) {
1183
1338
  });
1184
1339
  }
1185
1340
  }
1186
- return {
1341
+ let entity;
1342
+ if (typeof deps.entitySearch === "function") {
1343
+ entity = filterByDateRange(
1344
+ await runEntitySearchForQuery(deps, input, limit, logger, peerPeers),
1345
+ input.dateFrom,
1346
+ input.dateTo
1347
+ );
1348
+ if (entity.length === 0) {
1349
+ entity = void 0;
1350
+ } else if (normalizeUnifiedMemoryMergeStrategy(
1351
+ input.mergeStrategy ?? deps.reasoning?.defaultMergeStrategy ?? "rrf"
1352
+ ) !== "rrf") {
1353
+ warnings.push({
1354
+ source: "memory",
1355
+ code: "memory_entity_requires_rrf",
1356
+ 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."
1357
+ });
1358
+ }
1359
+ } else if (input.mergeStrategy === "rrf") {
1360
+ warnings.push({
1361
+ source: "memory",
1362
+ code: "memory_entity_search_not_configured",
1363
+ message: "RRF merge requested but entity search is not configured; falling back to semantic+lexical RRF."
1364
+ });
1365
+ }
1366
+ const out = {
1187
1367
  semantic: filterByDateRange(semantic, input.dateFrom, input.dateTo),
1188
1368
  lexical: filterByDateRange(lexical, input.dateFrom, input.dateTo)
1189
1369
  };
1370
+ if (entity && entity.length > 0) {
1371
+ out.entity = entity;
1372
+ }
1373
+ return out;
1190
1374
  }
1191
1375
  async function searchUnifiedMemory(input) {
1192
1376
  const query = input.query.trim();
@@ -1457,32 +1641,56 @@ function createUnifiedSearch(deps = {}) {
1457
1641
  }
1458
1642
  function mergeAcrossSources(input) {
1459
1643
  const memorySubs = input.memorySubs ?? { semantic: [], lexical: [] };
1644
+ const lists = buildChannelLists(memorySubs, input.insightHits, input.knowledgeHits);
1645
+ if (input.strategy !== "rrf") {
1646
+ const seen = /* @__PURE__ */ new Map();
1647
+ for (const hit of [
1648
+ ...memorySubs.semantic,
1649
+ ...memorySubs.lexical,
1650
+ ...input.insightHits,
1651
+ ...input.knowledgeHits
1652
+ ]) {
1653
+ const key = `${hit.type}::${hit.id}`;
1654
+ const existing = seen.get(key);
1655
+ if (!existing || hit.similarity > existing.similarity) {
1656
+ seen.set(key, hit);
1657
+ }
1658
+ }
1659
+ const merged2 = mergeUnifiedMemorySearchResults(Array.from(seen.values()), input.limit);
1660
+ return attachSignals(merged2, lists);
1661
+ }
1460
1662
  const all = [
1461
1663
  ...memorySubs.semantic,
1462
1664
  ...memorySubs.lexical,
1665
+ ...memorySubs.entity ?? [],
1463
1666
  ...input.insightHits,
1464
1667
  ...input.knowledgeHits
1465
1668
  ];
1466
- if (input.strategy !== "rrf") {
1467
- return mergeUnifiedMemorySearchResults(all, input.limit);
1468
- }
1669
+ const merged = lists.length <= 1 ? mergeUnifiedMemorySearchResults(all, input.limit) : mergeUnifiedMemorySearchResultsRrf(lists, input.limit);
1670
+ return attachSignals(merged, lists);
1671
+ }
1672
+ function buildChannelLists(memorySubs, insightHits, knowledgeHits) {
1469
1673
  const lists = [];
1470
- if (memorySubs.semantic.length > 0) {
1471
- lists.push({ name: "memory-semantic", hits: memorySubs.semantic });
1472
- }
1473
- if (memorySubs.lexical.length > 0) {
1474
- lists.push({ name: "memory-bm25", hits: memorySubs.lexical });
1475
- }
1476
- if (input.insightHits.length > 0) {
1477
- lists.push({ name: "insights", hits: input.insightHits });
1478
- }
1479
- if (input.knowledgeHits.length > 0) {
1480
- lists.push({ name: "knowledge", hits: input.knowledgeHits });
1674
+ if (memorySubs.semantic.length > 0) lists.push({ name: "memory-semantic", hits: memorySubs.semantic });
1675
+ if (memorySubs.lexical.length > 0) lists.push({ name: "memory-bm25", hits: memorySubs.lexical });
1676
+ if (memorySubs.entity && memorySubs.entity.length > 0) {
1677
+ lists.push({ name: "memory-entity", hits: memorySubs.entity });
1481
1678
  }
1482
- if (lists.length <= 1) {
1483
- return mergeUnifiedMemorySearchResults(all, input.limit);
1679
+ if (insightHits.length > 0) lists.push({ name: "insights", hits: insightHits });
1680
+ if (knowledgeHits.length > 0) lists.push({ name: "knowledge", hits: knowledgeHits });
1681
+ return lists;
1682
+ }
1683
+ function attachSignals(hits, lists) {
1684
+ if (hits.length === 0 || lists.length === 0) {
1685
+ return hits;
1484
1686
  }
1485
- return mergeUnifiedMemorySearchResultsRrf(lists, input.limit);
1687
+ return hits.map((hit) => {
1688
+ const signals = materializeSignals(lists, hit);
1689
+ if (!signals) {
1690
+ return hit;
1691
+ }
1692
+ return { ...hit, signals };
1693
+ });
1486
1694
  }
1487
1695
 
1488
1696
  // src/search/apply-reflect.ts
@@ -1601,6 +1809,177 @@ function makePlanSummary(plan) {
1601
1809
  }
1602
1810
  return lines.join("\n");
1603
1811
  }
1812
+ var DEFAULT_CANDIDATE_LIMIT = 50;
1813
+ async function deriveFacts(deps, input, logger = console) {
1814
+ const warnings = [];
1815
+ if (typeof deps.deriver !== "function") {
1816
+ warnings.push({
1817
+ code: "derive_deriver_not_configured",
1818
+ message: "No `deriver` is wired into the unified search deps; returning an empty fact list."
1819
+ });
1820
+ return { facts: [], warnings };
1821
+ }
1822
+ const limit = Math.max(1, Math.min(500, input.candidateLimit ?? DEFAULT_CANDIDATE_LIMIT));
1823
+ if (!input.candidateTexts || input.candidateTexts.length === 0) {
1824
+ if (!input.query) {
1825
+ warnings.push({
1826
+ code: "derive_fallback_query_noise",
1827
+ 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."
1828
+ });
1829
+ }
1830
+ }
1831
+ let candidateTexts;
1832
+ if (input.candidateTexts && input.candidateTexts.length > 0) {
1833
+ candidateTexts = input.candidateTexts;
1834
+ } else {
1835
+ candidateTexts = await loadCandidateTexts(deps, input, limit, logger);
1836
+ if (candidateTexts.length === 0) {
1837
+ warnings.push({
1838
+ code: "derive_no_candidates",
1839
+ message: "No candidate fact texts available for derivation"
1840
+ });
1841
+ return { facts: [], warnings };
1842
+ }
1843
+ }
1844
+ let facts;
1845
+ try {
1846
+ facts = await deps.deriver({
1847
+ userId: input.userId,
1848
+ userScope: {
1849
+ userId: input.userId,
1850
+ botIds: input.botIds,
1851
+ dateFrom: input.dateFrom,
1852
+ dateTo: input.dateTo
1853
+ },
1854
+ recentFactTexts: candidateTexts,
1855
+ ...input.window ? { window: input.window } : {}
1856
+ });
1857
+ } catch (error) {
1858
+ logger.warn?.("[memory-store/derive] deriver threw:", error);
1859
+ warnings.push({
1860
+ code: "derive_deriver_failed",
1861
+ message: error.message ?? "deriver failed"
1862
+ });
1863
+ return { facts: [], warnings };
1864
+ }
1865
+ if (!Array.isArray(facts)) {
1866
+ warnings.push({
1867
+ code: "derive_deriver_returned_invalid_shape",
1868
+ message: `deriver returned ${typeof facts}; expected DerivedFact[]`
1869
+ });
1870
+ return { facts: [], warnings };
1871
+ }
1872
+ const normalized = [];
1873
+ const now = Date.now();
1874
+ for (const fact of facts) {
1875
+ if (!fact || typeof fact.text !== "string" || fact.text.length === 0 || typeof fact.kind !== "string" || !isDerivedKind(fact.kind) || !Array.isArray(fact.sources)) {
1876
+ continue;
1877
+ }
1878
+ normalized.push({
1879
+ ...fact,
1880
+ derivedAt: typeof fact.derivedAt === "number" ? fact.derivedAt : now,
1881
+ sources: fact.sources.filter((s) => typeof s === "string")
1882
+ });
1883
+ }
1884
+ if (input.persist && normalized.length > 0) {
1885
+ try {
1886
+ await input.persist(normalized);
1887
+ } catch (error) {
1888
+ logger.warn?.("[memory-store/derive] persist callback threw:", error);
1889
+ warnings.push({
1890
+ code: "derive_persist_failed",
1891
+ message: error.message ?? "derive persist failed"
1892
+ });
1893
+ }
1894
+ }
1895
+ return { facts: normalized, warnings };
1896
+ }
1897
+ async function loadCandidateTexts(deps, input, limit, logger) {
1898
+ if (typeof deps.searchRawMessagesLexical !== "function") {
1899
+ return [];
1900
+ }
1901
+ const queryText = (input.query ?? `${input.userId} ${input.botIds?.join(" ") ?? ""}`).trim();
1902
+ const keywords = deriveLexicalKeywords(queryText);
1903
+ if (keywords.length === 0) {
1904
+ return [];
1905
+ }
1906
+ try {
1907
+ const filters = input.botIds && input.botIds.length > 0 ? input.botIds : [void 0];
1908
+ const factTypes = input.factTypes?.length ? input.factTypes : void 0;
1909
+ const hits = (await Promise.all(
1910
+ filters.map(
1911
+ (botId) => deps.searchRawMessagesLexical?.({
1912
+ userId: input.userId,
1913
+ keywords,
1914
+ limit: Math.ceil(limit / filters.length),
1915
+ botId,
1916
+ ...input.peers && input.peers.length > 0 ? { peers: input.peers } : {},
1917
+ ...factTypes ? { factTypes } : {}
1918
+ })
1919
+ )
1920
+ )).flat().filter(isRawMemorySemanticResult);
1921
+ return hits.map((h) => toMemoryResult(h).content);
1922
+ } catch (error) {
1923
+ logger.warn?.("[memory-store/derive] lexical candidate fetch failed:", error);
1924
+ return [];
1925
+ }
1926
+ }
1927
+ async function distillRawMessage(deps, input, logger = console) {
1928
+ const warnings = [];
1929
+ if (typeof deps.entityExtractor !== "function") {
1930
+ warnings.push({
1931
+ code: "distill_extractor_not_configured",
1932
+ message: "No `entityExtractor` is wired into the unified search deps; returning an empty entity list."
1933
+ });
1934
+ return { edges: [], warnings };
1935
+ }
1936
+ let edges;
1937
+ try {
1938
+ edges = await deps.entityExtractor({
1939
+ userId: input.userId,
1940
+ messageId: input.messageId,
1941
+ content: input.content
1942
+ });
1943
+ } catch (error) {
1944
+ logger.warn?.("[memory-store/distill] extractor threw:", error);
1945
+ warnings.push({
1946
+ code: "distill_extractor_failed",
1947
+ message: error.message ?? "entity extractor failed"
1948
+ });
1949
+ return { edges: [], warnings };
1950
+ }
1951
+ if (!Array.isArray(edges)) {
1952
+ warnings.push({
1953
+ code: "distill_extractor_returned_invalid_shape",
1954
+ message: `entityExtractor returned ${typeof edges}; expected EntityEdge[]`
1955
+ });
1956
+ return { edges: [], warnings };
1957
+ }
1958
+ const normalized = [];
1959
+ const now = Date.now();
1960
+ for (const edge of edges) {
1961
+ 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") {
1962
+ continue;
1963
+ }
1964
+ normalized.push({
1965
+ ...edge,
1966
+ label: edge.label.trim().toLowerCase(),
1967
+ extractedAt: typeof edge.extractedAt === "number" ? edge.extractedAt : now
1968
+ });
1969
+ }
1970
+ if (input.persist && normalized.length > 0) {
1971
+ try {
1972
+ await input.persist(normalized);
1973
+ } catch (error) {
1974
+ logger.warn?.("[memory-store/distill] persist callback threw:", error);
1975
+ warnings.push({
1976
+ code: "distill_persist_failed",
1977
+ message: error.message ?? "distill persist failed"
1978
+ });
1979
+ }
1980
+ }
1981
+ return { edges: normalized, warnings };
1982
+ }
1604
1983
  var DEFAULT_MAX_FACTS = 1e3;
1605
1984
  var LOW_CONFIDENCE_THRESHOLD = 0.1;
1606
1985
  function toHRR(data) {
@@ -1762,34 +2141,6 @@ function createVsaRecall(storage) {
1762
2141
  init_chroma_memory_index();
1763
2142
  init_raw_message_store();
1764
2143
  init_sqlite_raw_message_store();
1765
- async function embedMissingMessages(messages, deps) {
1766
- if (typeof deps.embedQuery !== "function") {
1767
- return messages;
1768
- }
1769
- const out = [];
1770
- for (const message of messages) {
1771
- if (Array.isArray(message.embedding) && message.embedding.length > 0) {
1772
- out.push(message);
1773
- continue;
1774
- }
1775
- if (typeof message.content !== "string" || message.content.length === 0) {
1776
- out.push(message);
1777
- continue;
1778
- }
1779
- const vector = await deps.embedQuery({
1780
- userId: message.userId,
1781
- query: message.content
1782
- });
1783
- out.push({
1784
- ...message,
1785
- embedding: vector,
1786
- embeddingModel: message.embeddingModel ?? "server",
1787
- embeddingDimensions: vector.length,
1788
- embeddingUpdatedAt: Date.now()
1789
- });
1790
- }
1791
- return out;
1792
- }
1793
2144
  async function startHttpServer(options = {}) {
1794
2145
  const port = options.port ?? Number.parseInt(process.env.MEMORY_HTTP_PORT ?? "7421", 10);
1795
2146
  const host = options.host ?? process.env.MEMORY_HTTP_HOST ?? "127.0.0.1";
@@ -1850,6 +2201,56 @@ async function startHttpServer(options = {}) {
1850
2201
  const result = await search.search(input);
1851
2202
  return c.json(result);
1852
2203
  });
2204
+ app.post("/v1/distill", async (c) => {
2205
+ const body = await c.req.json().catch(() => ({}));
2206
+ const userId = typeof body.userId === "string" ? body.userId : null;
2207
+ const messageId = typeof body.messageId === "string" ? body.messageId : null;
2208
+ const content = typeof body.content === "string" ? body.content : null;
2209
+ if (!userId || !messageId || content === null || content.length === 0) {
2210
+ return c.json({ error: "userId, messageId, and a non-empty content are required" }, 400);
2211
+ }
2212
+ const input = { userId, messageId, content };
2213
+ try {
2214
+ const result = await distillRawMessage(options.unified ?? {}, input, console);
2215
+ return c.json(result);
2216
+ } catch (error) {
2217
+ console.error("[memory-store/http] distill failed:", error);
2218
+ return c.json({ error: error.message ?? "distill failed" }, 500);
2219
+ }
2220
+ });
2221
+ app.post("/v1/derive", async (c) => {
2222
+ const body = await c.req.json().catch(() => ({}));
2223
+ const userId = typeof body.userId === "string" ? body.userId : null;
2224
+ if (!userId) {
2225
+ return c.json({ error: "userId is required" }, 400);
2226
+ }
2227
+ const windowFrom = typeof body.windowFrom === "number" ? body.windowFrom : void 0;
2228
+ const windowTo = typeof body.windowTo === "number" ? body.windowTo : void 0;
2229
+ const window = windowFrom !== void 0 && windowTo !== void 0 ? { from: windowFrom, to: windowTo } : void 0;
2230
+ const peers = Array.isArray(body.peers) ? body.peers.filter(
2231
+ (p) => (p?.kind === "user" || p?.kind === "agent") && typeof p?.id === "string" && p.id.length > 0
2232
+ ) : void 0;
2233
+ const factTypes = Array.isArray(body.factTypes) ? body.factTypes.filter(isFactType) : void 0;
2234
+ const input = {
2235
+ userId,
2236
+ ...typeof body.query === "string" ? { query: body.query } : {},
2237
+ botIds: Array.isArray(body.botIds) ? body.botIds : void 0,
2238
+ dateFrom: typeof body.dateFrom === "string" ? body.dateFrom : void 0,
2239
+ dateTo: typeof body.dateTo === "string" ? body.dateTo : void 0,
2240
+ ...window ? { window } : {},
2241
+ ...Array.isArray(body.candidateTexts) ? { candidateTexts: body.candidateTexts } : {},
2242
+ ...typeof body.candidateLimit === "number" ? { candidateLimit: body.candidateLimit } : {},
2243
+ ...peers && peers.length > 0 ? { peers } : {},
2244
+ ...factTypes && factTypes.length > 0 ? { factTypes } : {}
2245
+ };
2246
+ try {
2247
+ const result = await deriveFacts(options.unified ?? {}, input, console);
2248
+ return c.json(result);
2249
+ } catch (error) {
2250
+ console.error("[memory-store/http] derive failed:", error);
2251
+ return c.json({ error: error.message ?? "derive failed" }, 500);
2252
+ }
2253
+ });
1853
2254
  app.post("/v1/consolidate:apply", async (c) => {
1854
2255
  const body = await c.req.json().catch(() => ({}));
1855
2256
  const userId = typeof body.userId === "string" ? body.userId : null;
@@ -1890,11 +2291,15 @@ async function startHttpServer(options = {}) {
1890
2291
  return c.json({ error: "userId and messages[] required" }, 400);
1891
2292
  }
1892
2293
  const manager3 = await rawStore.getManager();
1893
- const incoming = body.messages;
1894
- const messages = body.embedOnInsert === true ? await embedMissingMessages(
1895
- incoming.map((m) => ({ ...m, userId: m.userId ?? body.userId })),
1896
- options.unified ?? {}
1897
- ) : incoming.map((m) => ({ ...m, userId: m.userId ?? body.userId }));
2294
+ const incoming = body.messages.map((m) => ({
2295
+ ...m,
2296
+ userId: m.userId ?? body.userId
2297
+ }));
2298
+ const { messages, warnings } = await applyEmbedOnInsertPolicy(
2299
+ incoming,
2300
+ body.embedOnInsert,
2301
+ options.unified
2302
+ );
1898
2303
  let result;
1899
2304
  if (typeof manager3.upsertRawMessages === "function") {
1900
2305
  result = await manager3.upsertRawMessages({
@@ -1936,7 +2341,12 @@ async function startHttpServer(options = {}) {
1936
2341
  } catch (error) {
1937
2342
  console.warn("[memory-store/http] sqlite-vec vector update failed:", error);
1938
2343
  }
1939
- return c.json({ ok: true, count: messages.length, result });
2344
+ return c.json({
2345
+ ok: true,
2346
+ count: messages.length,
2347
+ result,
2348
+ ...warnings.length > 0 ? { warnings } : {}
2349
+ });
1940
2350
  });
1941
2351
  app.get("/v1/raw-messages/:id", async (c) => {
1942
2352
  const id = c.req.param("id");
@@ -2040,9 +2450,9 @@ async function startHttpServer(options = {}) {
2040
2450
  url: `http://${host}:${port}`,
2041
2451
  port,
2042
2452
  async stop() {
2453
+ await new Promise((resolve) => server.close(() => resolve()));
2043
2454
  await rawStore.close();
2044
2455
  await closeSQLiteVsaStore();
2045
- server.close();
2046
2456
  }
2047
2457
  };
2048
2458
  }