@melandlabs/memory-store 0.2.7 → 0.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/dist/index.js CHANGED
@@ -170,7 +170,7 @@ __export(raw_message_store_exports, {
170
170
  getRawMessageStorageBackend: () => getRawMessageStorageBackend,
171
171
  isRawMessageStorageAvailable: () => isRawMessageStorageAvailable
172
172
  });
173
- function resolveBackend(env) {
173
+ function resolveBackend(_env) {
174
174
  const explicit = process.env.OPENCONTEXT_MEMORY_STORE_BACKEND?.trim().toLowerCase();
175
175
  if (explicit === "postgres") return "postgres";
176
176
  return "sqlite";
@@ -303,6 +303,12 @@ function mergeUnifiedMemorySearchResults(results, limit, options = {}) {
303
303
  function normalizeUnifiedMemoryMergeStrategy(value) {
304
304
  return value === "rrf" ? "rrf" : "similarity";
305
305
  }
306
+ function normalizeUnifiedMemoryReasoningStrategy(value, defaultStrategy = "none") {
307
+ if (value === "rewrite" || value === "iterative") {
308
+ return value;
309
+ }
310
+ return defaultStrategy;
311
+ }
306
312
  var DEFAULT_RRF_K = 60;
307
313
  function mergeUnifiedMemorySearchResultsRrf(lists, limit, k = DEFAULT_RRF_K) {
308
314
  if (!Array.isArray(lists) || lists.length === 0) {
@@ -325,7 +331,13 @@ function mergeUnifiedMemorySearchResultsRrf(lists, limit, k = DEFAULT_RRF_K) {
325
331
  }
326
332
  }
327
333
  }
328
- const ranked = order.map((key) => scores.get(key)).sort((a, b) => {
334
+ const ranked = order.map((key) => {
335
+ const entry = scores.get(key);
336
+ if (!entry) {
337
+ throw new Error(`Invariant violation: missing RRF score for key "${key}"`);
338
+ }
339
+ return entry;
340
+ }).sort((a, b) => {
329
341
  if (b.rrf !== a.rrf) {
330
342
  return b.rrf - a.rrf;
331
343
  }
@@ -370,14 +382,222 @@ function isRawMemorySemanticResult(result) {
370
382
  function deriveLexicalKeywords(query) {
371
383
  return query.toLowerCase().split(/[^\p{L}\p{N}]+/u).filter((token) => token.length >= 2).slice(0, 16);
372
384
  }
385
+ function parseDateBoundary(value, role) {
386
+ const trimmed = value.trim();
387
+ if (!trimmed) {
388
+ return void 0;
389
+ }
390
+ const dateOnlyMatch = trimmed.match(/^(\d{4})-(\d{2})-(\d{2})$/);
391
+ if (dateOnlyMatch) {
392
+ const year = Number.parseInt(dateOnlyMatch[1], 10);
393
+ const month = Number.parseInt(dateOnlyMatch[2], 10) - 1;
394
+ const day = Number.parseInt(dateOnlyMatch[3], 10);
395
+ if (role === "from") {
396
+ return Date.UTC(year, month, day);
397
+ }
398
+ return Date.UTC(year, month, day + 1) - 1;
399
+ }
400
+ const parsed = Date.parse(trimmed);
401
+ if (Number.isNaN(parsed)) {
402
+ return void 0;
403
+ }
404
+ return parsed;
405
+ }
406
+ function getCandidateTimestamp(metadata) {
407
+ for (const key of ["timestamp", "createdAt", "time"]) {
408
+ const value = metadata[key];
409
+ if (typeof value === "number") {
410
+ return value;
411
+ }
412
+ if (typeof value === "string") {
413
+ const parsed = Date.parse(value);
414
+ if (!Number.isNaN(parsed)) {
415
+ return parsed;
416
+ }
417
+ }
418
+ }
419
+ return void 0;
420
+ }
421
+ function filterByDateRange(items, dateFrom, dateTo) {
422
+ if (!dateFrom && !dateTo) {
423
+ return items;
424
+ }
425
+ const fromMs = dateFrom ? parseDateBoundary(dateFrom, "from") : void 0;
426
+ const toMs = dateTo ? parseDateBoundary(dateTo, "to") : void 0;
427
+ if (fromMs === void 0 && toMs === void 0) {
428
+ return items;
429
+ }
430
+ return items.filter((item) => {
431
+ const ts = getCandidateTimestamp(item.metadata);
432
+ if (ts === void 0) {
433
+ return true;
434
+ }
435
+ if (fromMs !== void 0 && ts < fromMs) {
436
+ return false;
437
+ }
438
+ if (toMs !== void 0 && ts > toMs) {
439
+ return false;
440
+ }
441
+ return true;
442
+ });
443
+ }
444
+ async function embedQueryVariant(embedQuery, input, query) {
445
+ return embedQuery({
446
+ userId: input.userId,
447
+ query,
448
+ authToken: input.authToken
449
+ });
450
+ }
451
+ async function runSemanticSearchForEmbedding(deps, input, queryEmbedding, limit, threshold, logger) {
452
+ const filters = input.botIds && input.botIds.length > 0 ? input.botIds.map((botId) => ({ botId })) : [{}];
453
+ let semantic = [];
454
+ if (isRawMessageChromaEnabled()) {
455
+ try {
456
+ semantic = (await Promise.all(
457
+ filters.map((filter) => {
458
+ const botId = "botId" in filter ? filter.botId : void 0;
459
+ return searchRawMessagesWithChroma({
460
+ userId: input.userId,
461
+ queryEmbedding,
462
+ limit,
463
+ threshold,
464
+ botId
465
+ });
466
+ })
467
+ )).flat().map(toMemoryResult);
468
+ logger.log?.("[memory-store] Raw message semantic search completed", {
469
+ backend: "chroma",
470
+ dimensions: queryEmbedding.length,
471
+ count: semantic.length
472
+ });
473
+ } catch (error) {
474
+ logger.warn?.(
475
+ "[memory-store] Chroma raw message search failed; falling back to database search:",
476
+ error
477
+ );
478
+ }
479
+ } else if (typeof deps.searchRawMessagesAnn === "function") {
480
+ const searchRawMessagesAnn = deps.searchRawMessagesAnn;
481
+ semantic = (await Promise.all(
482
+ filters.map(
483
+ (filter) => searchRawMessagesAnn({
484
+ userId: input.userId,
485
+ queryEmbedding,
486
+ limit,
487
+ threshold,
488
+ botId: "botId" in filter ? filter.botId : void 0
489
+ })
490
+ )
491
+ )).flat().filter(isRawMemorySemanticResult).map(toMemoryResult);
492
+ }
493
+ if (semantic.length === 0) {
494
+ try {
495
+ const { getRawMessageManager: getRawMessageManager2 } = await Promise.resolve().then(() => (init_raw_message_store(), raw_message_store_exports));
496
+ const manager2 = await getRawMessageManager2();
497
+ if (typeof manager2.searchMessagesSemantically === "function") {
498
+ const results = await manager2.searchMessagesSemantically({
499
+ userId: input.userId,
500
+ queryEmbedding,
501
+ limit,
502
+ threshold
503
+ });
504
+ semantic = results.map((r) => ({
505
+ type: "memory",
506
+ id: r.id,
507
+ content: r.content,
508
+ similarity: r.similarity,
509
+ metadata: r.metadata ?? {}
510
+ }));
511
+ }
512
+ } catch (error) {
513
+ logger.warn?.("[memory-store] SQLite semantic search failed:", error);
514
+ }
515
+ }
516
+ return semantic;
517
+ }
518
+ function mergeByMaxScore(lists) {
519
+ const best = /* @__PURE__ */ new Map();
520
+ for (const list of lists) {
521
+ for (const hit of list) {
522
+ const existing = best.get(hit.id);
523
+ if (!existing || hit.similarity > existing.similarity) {
524
+ best.set(hit.id, hit);
525
+ }
526
+ }
527
+ }
528
+ return Array.from(best.values()).sort((a, b) => b.similarity - a.similarity);
529
+ }
530
+ function toIterativeRecallCandidate(hit) {
531
+ return {
532
+ id: hit.id,
533
+ content: hit.content,
534
+ similarity: hit.similarity,
535
+ metadata: hit.metadata
536
+ };
537
+ }
538
+ async function runLexicalSearchForKeywords(deps, input, keywords, limit, logger) {
539
+ if (keywords.length === 0) {
540
+ return [];
541
+ }
542
+ if (typeof deps.searchRawMessagesLexical === "function") {
543
+ try {
544
+ const filters = input.botIds && input.botIds.length > 0 ? input.botIds : [void 0];
545
+ const searchRawMessagesLexical = deps.searchRawMessagesLexical;
546
+ return (await Promise.all(
547
+ filters.map(
548
+ (botId) => searchRawMessagesLexical({
549
+ userId: input.userId,
550
+ keywords,
551
+ limit: Math.ceil(limit / filters.length),
552
+ botId
553
+ })
554
+ )
555
+ )).flat().filter(isRawMemorySemanticResult).map((hit) => ({
556
+ ...toMemoryResult(hit),
557
+ metadata: { ...hit.metadata, scoring: "bm25" }
558
+ }));
559
+ } catch (error) {
560
+ logger.warn?.("[memory-store] lexical memory search failed:", error);
561
+ }
562
+ }
563
+ try {
564
+ const { lexicalSearchRawMessages: lexicalSearchRawMessages2 } = await Promise.resolve().then(() => (init_sqlite_raw_message_store(), sqlite_raw_message_store_exports));
565
+ const filters = input.botIds && input.botIds.length > 0 ? input.botIds : [void 0];
566
+ return (await Promise.all(
567
+ filters.map(
568
+ (botId) => lexicalSearchRawMessages2({
569
+ userId: input.userId,
570
+ keywords,
571
+ limit: Math.ceil(limit / filters.length),
572
+ botId
573
+ })
574
+ )
575
+ )).flat().filter(Boolean).map(
576
+ (r) => toMemoryResult(
577
+ r
578
+ )
579
+ );
580
+ } catch (error) {
581
+ logger.warn?.("[memory-store] SQLite lexical search failed:", error);
582
+ }
583
+ return [];
584
+ }
373
585
  function createUnifiedSearch(deps = {}) {
374
586
  const logger = deps.logger ?? console;
587
+ function resolveReasoningStrategy(input) {
588
+ const requested = normalizeUnifiedMemoryReasoningStrategy(input.reasoningStrategy);
589
+ if (requested !== "none") {
590
+ return requested;
591
+ }
592
+ return normalizeUnifiedMemoryReasoningStrategy(deps.reasoning?.defaultStrategy, "none");
593
+ }
375
594
  async function searchUnifiedMemory(input) {
376
595
  const query = input.query.trim();
377
596
  const sources = normalizeUnifiedMemorySearchSources(input.sources);
378
597
  const limit = clampUnifiedMemorySearchLimit(input.limit);
379
598
  const threshold = clampUnifiedMemorySearchThreshold(input.threshold);
380
599
  const mergeStrategy = normalizeUnifiedMemoryMergeStrategy(input.mergeStrategy);
600
+ const reasoningStrategy = resolveReasoningStrategy(input);
381
601
  const warnings = [];
382
602
  if (!query) {
383
603
  return {
@@ -389,10 +609,21 @@ function createUnifiedSearch(deps = {}) {
389
609
  };
390
610
  }
391
611
  let memorySubs;
612
+ const reasoningInfo = { strategy: reasoningStrategy };
613
+ if (input.dateFrom || input.dateTo) {
614
+ reasoningInfo.dateRange = { from: input.dateFrom, to: input.dateTo };
615
+ }
392
616
  if (sources.includes("memory")) {
393
617
  if (isRawMessageStorageAvailable()) {
394
618
  try {
395
- memorySubs = await runMemorySource(input, limit, threshold, warnings);
619
+ memorySubs = await runMemorySource(
620
+ input,
621
+ limit,
622
+ threshold,
623
+ warnings,
624
+ reasoningStrategy,
625
+ reasoningInfo
626
+ );
396
627
  } catch (error) {
397
628
  logger.warn?.("[memory-store] memory source failed:", error);
398
629
  warnings.push({
@@ -471,121 +702,155 @@ function createUnifiedSearch(deps = {}) {
471
702
  limit,
472
703
  strategy: mergeStrategy
473
704
  });
474
- return {
705
+ const output = {
475
706
  query: input.query,
476
707
  sources,
477
708
  results: merged,
478
709
  count: merged.length,
479
710
  warnings
480
711
  };
712
+ if (reasoningInfo && (reasoningInfo.strategy !== "none" || reasoningInfo.dateRange !== void 0)) {
713
+ output.reasoning = reasoningInfo;
714
+ }
715
+ return output;
481
716
  }
482
- async function runMemorySource(input, limit, threshold, warnings) {
717
+ async function runMemorySource(input, limit, threshold, warnings, reasoningStrategy = "none", reasoningInfo) {
718
+ if (reasoningStrategy === "iterative" && !deps.reasoning?.iterativePlanner) {
719
+ warnings.push({
720
+ source: "memory",
721
+ code: "memory_iterative_planner_not_configured",
722
+ message: "Iterative reasoning requested but no iterativePlanner is configured; falling back to default search."
723
+ });
724
+ if (reasoningInfo) reasoningInfo.degraded = true;
725
+ }
726
+ if (reasoningStrategy === "rewrite" && !deps.reasoning?.queryRewriter) {
727
+ warnings.push({
728
+ source: "memory",
729
+ code: "memory_query_rewrite_not_configured",
730
+ message: "Rewrite reasoning requested but no queryRewriter is configured; falling back to default search."
731
+ });
732
+ if (reasoningInfo) reasoningInfo.degraded = true;
733
+ }
734
+ if (reasoningStrategy === "iterative" && deps.reasoning?.iterativePlanner) {
735
+ const planner = deps.reasoning.iterativePlanner;
736
+ const result = await planner.plan({
737
+ query: input.query,
738
+ dateFrom: input.dateFrom,
739
+ dateTo: input.dateTo,
740
+ executor: {
741
+ search: async (request) => {
742
+ const keywords2 = request.keywords.length > 0 ? request.keywords : deriveLexicalKeywords(input.query);
743
+ const lexicalHits = await runLexicalSearchForKeywords(deps, input, keywords2, limit, logger);
744
+ let semanticHits = [];
745
+ if (typeof deps.embedQuery === "function") {
746
+ try {
747
+ const semanticQuery = keywords2.join(" ");
748
+ const queryEmbedding = await embedQueryVariant(deps.embedQuery, input, semanticQuery);
749
+ semanticHits = await runSemanticSearchForEmbedding(
750
+ deps,
751
+ input,
752
+ queryEmbedding,
753
+ limit,
754
+ threshold,
755
+ logger
756
+ );
757
+ } catch (error) {
758
+ logger.warn?.(
759
+ "[memory-store] Iterative semantic search failed; using lexical results only:",
760
+ error
761
+ );
762
+ }
763
+ }
764
+ const candidates = filterByDateRange(
765
+ mergeByMaxScore([
766
+ lexicalHits.map(toIterativeRecallCandidate),
767
+ semanticHits.map(toIterativeRecallCandidate)
768
+ ]),
769
+ request.dateFrom,
770
+ request.dateTo
771
+ );
772
+ return { candidates };
773
+ }
774
+ }
775
+ });
776
+ if (reasoningInfo) {
777
+ reasoningInfo.iterations = result.stats.iterations;
778
+ reasoningInfo.evidenceCount = result.evidence.length;
779
+ if (planner.lastDegraded?.()) {
780
+ reasoningInfo.degraded = true;
781
+ }
782
+ }
783
+ const semantic2 = result.evidence.map((candidate) => ({
784
+ type: "memory",
785
+ id: candidate.id,
786
+ content: candidate.content,
787
+ similarity: candidate.similarity,
788
+ metadata: { ...candidate.metadata, reasoning: "iterative" }
789
+ }));
790
+ return { semantic: semantic2, lexical: [] };
791
+ }
483
792
  if (typeof deps.embedQuery !== "function") {
484
793
  warnings.push({
485
794
  source: "memory",
486
795
  code: "memory_lexical_search_fallback",
487
796
  message: "Semantic search not configured, using keyword search as fallback"
488
797
  });
489
- const { lexicalSearchRawMessages: lexicalSearchRawMessages2 } = await Promise.resolve().then(() => (init_sqlite_raw_message_store(), sqlite_raw_message_store_exports));
490
- const keywords = deriveLexicalKeywords(input.query);
491
- let lexical2 = [];
492
- if (keywords.length > 0) {
493
- try {
494
- const filters2 = input.botIds && input.botIds.length > 0 ? input.botIds : [void 0];
495
- const results = await Promise.all(
496
- filters2.map(
497
- (botId) => lexicalSearchRawMessages2({
498
- userId: input.userId,
499
- keywords,
500
- limit: Math.ceil(limit / filters2.length),
501
- botId
502
- })
503
- )
504
- );
505
- lexical2 = results.flat().filter(Boolean).map(toMemoryResult);
506
- } catch (error) {
507
- logger.warn?.("[memory-store] Default lexical search failed:", error);
508
- }
509
- }
798
+ const keywords2 = deriveLexicalKeywords(input.query);
799
+ const lexical2 = filterByDateRange(
800
+ await runLexicalSearchForKeywords(deps, input, keywords2, limit, logger),
801
+ input.dateFrom,
802
+ input.dateTo
803
+ );
510
804
  return { semantic: [], lexical: lexical2 };
511
805
  }
512
- const queryEmbedding = await deps.embedQuery({
513
- userId: input.userId,
514
- query: input.query,
515
- authToken: input.authToken
516
- });
517
- const filters = input.botIds && input.botIds.length > 0 ? input.botIds.map((botId) => ({ botId })) : [{}];
518
806
  let semantic = [];
519
- if (isRawMessageChromaEnabled()) {
807
+ const embedQuery = deps.embedQuery;
808
+ if (reasoningStrategy === "rewrite" && deps.reasoning?.queryRewriter) {
520
809
  try {
521
- semantic = (await Promise.all(
522
- filters.map((filter) => {
523
- const botId = "botId" in filter ? filter.botId : void 0;
524
- return searchRawMessagesWithChroma({
525
- userId: input.userId,
526
- queryEmbedding,
527
- limit,
528
- threshold,
529
- botId
530
- });
531
- })
532
- )).flat().map(toMemoryResult);
533
- logger.log?.("[memory-store] Raw message semantic search completed", {
534
- backend: "chroma",
535
- dimensions: queryEmbedding.length,
536
- count: semantic.length
810
+ const variants = await deps.reasoning.queryRewriter.rewrite({
811
+ query: input.query,
812
+ userId: input.userId,
813
+ authToken: input.authToken
537
814
  });
538
- } catch (error) {
539
- logger.warn?.(
540
- "[memory-store] Chroma raw message search failed; falling back to database search:",
541
- error
815
+ if (reasoningInfo) {
816
+ reasoningInfo.rewrittenQueries = variants;
817
+ if (deps.reasoning.queryRewriter.lastDegraded?.()) {
818
+ reasoningInfo.degraded = true;
819
+ }
820
+ }
821
+ const embeddings = await Promise.all(
822
+ variants.map((variant) => embedQueryVariant(embedQuery, input, variant))
823
+ );
824
+ const lists = await Promise.all(
825
+ embeddings.map(
826
+ (embedding) => runSemanticSearchForEmbedding(deps, input, embedding, limit, threshold, logger)
827
+ )
542
828
  );
829
+ semantic = mergeByMaxScore(lists);
830
+ } catch (error) {
831
+ logger.warn?.("[memory-store] Query rewriting failed; falling back to original query:", error);
832
+ warnings.push({
833
+ source: "memory",
834
+ code: "memory_query_rewrite_failed",
835
+ message: `Query rewriting failed: ${error.message ?? "Unknown error"}. Using original query.`
836
+ });
837
+ if (reasoningInfo) reasoningInfo.degraded = true;
543
838
  }
544
- } else if (typeof deps.searchRawMessagesAnn === "function") {
545
- semantic = (await Promise.all(
546
- filters.map(
547
- (filter) => deps.searchRawMessagesAnn({
548
- userId: input.userId,
549
- queryEmbedding,
550
- limit,
551
- threshold,
552
- botId: "botId" in filter ? filter.botId : void 0
553
- })
554
- )
555
- )).flat().filter(isRawMemorySemanticResult).map(toMemoryResult);
556
839
  }
557
840
  if (semantic.length === 0) {
558
- try {
559
- const { getRawMessageManager: getRawMessageManager2 } = await Promise.resolve().then(() => (init_raw_message_store(), raw_message_store_exports));
560
- const manager2 = await getRawMessageManager2();
561
- if (typeof manager2.searchMessagesSemantically === "function") {
562
- const results = await manager2.searchMessagesSemantically({
563
- userId: input.userId,
564
- queryEmbedding,
565
- limit,
566
- threshold
567
- });
568
- semantic = results.map((r) => ({
569
- type: "memory",
570
- id: r.id,
571
- content: r.content,
572
- similarity: r.similarity,
573
- metadata: r.metadata ?? {}
574
- }));
575
- }
576
- } catch (error) {
577
- logger.warn?.("[memory-store] SQLite semantic search failed:", error);
578
- }
841
+ const queryEmbedding = await embedQueryVariant(embedQuery, input, input.query);
842
+ semantic = await runSemanticSearchForEmbedding(deps, input, queryEmbedding, limit, threshold, logger);
579
843
  }
580
844
  let lexical = [];
581
- if (typeof deps.searchRawMessagesLexical === "function") {
582
- const keywords = deriveLexicalKeywords(input.query);
583
- if (keywords.length > 0) {
845
+ const keywords = deriveLexicalKeywords(input.query);
846
+ if (keywords.length > 0) {
847
+ if (typeof deps.searchRawMessagesLexical === "function") {
584
848
  try {
585
849
  const lexFilters = input.botIds && input.botIds.length > 0 ? input.botIds : [void 0];
850
+ const searchRawMessagesLexical = deps.searchRawMessagesLexical;
586
851
  lexical = (await Promise.all(
587
852
  lexFilters.map(
588
- (botId) => deps.searchRawMessagesLexical({
853
+ (botId) => searchRawMessagesLexical({
589
854
  userId: input.userId,
590
855
  keywords,
591
856
  limit,
@@ -604,20 +869,24 @@ function createUnifiedSearch(deps = {}) {
604
869
  message: error.message ?? "memory_lexical_search_failed"
605
870
  });
606
871
  }
872
+ } else if (input.mergeStrategy === "rrf") {
873
+ warnings.push({
874
+ source: "memory",
875
+ code: "memory_lexical_search_not_configured",
876
+ message: "RRF merge requested but lexical search is not configured; falling back to semantic-only RRF."
877
+ });
607
878
  }
608
- } else if (input.mergeStrategy === "rrf") {
609
- warnings.push({
610
- source: "memory",
611
- code: "memory_lexical_search_not_configured",
612
- message: "RRF merge requested but lexical search is not configured; falling back to semantic-only RRF."
613
- });
614
879
  }
615
- return { semantic, lexical };
880
+ return {
881
+ semantic: filterByDateRange(semantic, input.dateFrom, input.dateTo),
882
+ lexical: filterByDateRange(lexical, input.dateFrom, input.dateTo)
883
+ };
616
884
  }
617
885
  async function searchRawMemorySemantically(input) {
618
886
  const limit = clampUnifiedMemorySearchLimit(input.limit);
619
887
  const threshold = clampUnifiedMemorySearchThreshold(input.threshold);
620
- const sub = await runMemorySource(input, limit, threshold, []);
888
+ const reasoningStrategy = resolveReasoningStrategy(input);
889
+ const sub = await runMemorySource(input, limit, threshold, [], reasoningStrategy);
621
890
  return mergeAcrossSources({
622
891
  memorySubs: sub,
623
892
  insightHits: [],
@@ -662,6 +931,362 @@ function mergeAcrossSources(input) {
662
931
  init_raw_message_store();
663
932
  init_raw_message_store();
664
933
  init_postgres_raw_message_factory();
934
+
935
+ // src/search/query-rewriter.ts
936
+ var SYSTEM_PROMPT = `You are a memory retrieval assistant.
937
+
938
+ The memory store contains a user's past messages, written in the first person (e.g. "I told you about my sister's wedding", "I prefer dark mode").
939
+
940
+ Your job is NOT to answer the question. Your job is to rewrite the assistant's question into a short, natural question that the USER would ask their own memory log to find the relevant entry.
941
+
942
+ Rules:
943
+ - Use first person: "Did I tell you about ...?", "What did I say about ...?", "Have I mentioned ...?"
944
+ - Do NOT address the user as "you".
945
+ - Do NOT answer the question.
946
+ - Output each rephrasing on its own line, prefixed with "- ".
947
+ - Keep each rephrasing under 20 words.
948
+ - Output exactly the requested number of lines.`;
949
+ function buildPrompt(query, numVariants) {
950
+ const label = numVariants === 1 ? "1 alternative rephrasing" : `${numVariants} alternative rephrasings`;
951
+ return `${SYSTEM_PROMPT}
952
+
953
+ Output exactly ${label}.
954
+
955
+ Example:
956
+ Question: What is the name of my cat?
957
+ - Did I tell you the name of my cat?
958
+
959
+ Question: ${query}
960
+ Output:`;
961
+ }
962
+ function sanitizeLine(text) {
963
+ return text.replace(/^\s*["'`]+|["'`]+\s*$/g, "").replace(/^Rewritten:\s*/i, "").trim();
964
+ }
965
+ function parseVariants(raw, maxVariants, original) {
966
+ const lines = raw.split(/\r?\n/).map((line) => sanitizeLine(line.replace(/^\s*-\s+/, ""))).filter((line) => line.length > 0);
967
+ if (lines.length === 0) return [];
968
+ const seen = /* @__PURE__ */ new Set([original.toLowerCase()]);
969
+ const variants = [];
970
+ for (const line of lines) {
971
+ if (variants.length >= maxVariants) break;
972
+ const key = line.toLowerCase();
973
+ if (seen.has(key)) continue;
974
+ seen.add(key);
975
+ variants.push(line);
976
+ }
977
+ return variants;
978
+ }
979
+ function createUserVoiceRewriter(options) {
980
+ const { complete, maxVariants = 1, disabled = false } = options;
981
+ const wantedVariants = Math.max(0, Math.floor(maxVariants));
982
+ let lastDegraded = false;
983
+ return {
984
+ async rewrite(input) {
985
+ if (disabled || !input.query || !input.query.trim()) {
986
+ lastDegraded = false;
987
+ return [input.query];
988
+ }
989
+ const original = input.query.trim();
990
+ if (wantedVariants === 0) {
991
+ lastDegraded = false;
992
+ return [original];
993
+ }
994
+ try {
995
+ const raw = await complete(buildPrompt(original, wantedVariants));
996
+ const variants = parseVariants(raw, wantedVariants, original);
997
+ if (variants.length === 0) {
998
+ lastDegraded = true;
999
+ return [original];
1000
+ }
1001
+ lastDegraded = false;
1002
+ return [original, ...variants];
1003
+ } catch {
1004
+ lastDegraded = true;
1005
+ return [original];
1006
+ }
1007
+ },
1008
+ lastDegraded() {
1009
+ return lastDegraded;
1010
+ }
1011
+ };
1012
+ }
1013
+ function createIdentityRewriter() {
1014
+ return {
1015
+ async rewrite(input) {
1016
+ return [input.query];
1017
+ }
1018
+ };
1019
+ }
1020
+
1021
+ // src/search/iterative-recall.ts
1022
+ var SYSTEM_PROMPT2 = `You are a research assistant collecting evidence from a user's conversation history. You are NOT answering the question; only gather the most useful memory.
1023
+
1024
+ Actions:
1025
+ search \u2014 search by concise keywords. Previously returned chunks are automatically excluded.
1026
+ Action Input: {"keywords":["keyword1","keyword2"],"date_from":"YYYY-MM-DD","date_to":"YYYY-MM-DD"}
1027
+ The date fields are optional; omit them unless an absolute date range is useful.
1028
+
1029
+ note \u2014 save relevant results from the LAST search by their 1-based result number.
1030
+ Action Input: {"indices":[1,3]}
1031
+
1032
+ finish \u2014 finish collecting evidence.
1033
+ Action Input: {}
1034
+
1035
+ Workflow: search -> note -> search again -> note -> finish.
1036
+ Review every result. Call note before another search because the prior result list is replaced. Use diverse keywords on later searches. Never answer the question.
1037
+
1038
+ Output exactly:
1039
+ Thought: one short sentence
1040
+ Action: one action name
1041
+ Action Input: valid JSON object`;
1042
+ function deriveKeywordsFromQuery(query) {
1043
+ return query.toLowerCase().split(/[^\p{L}\p{N}]+/u).filter((token) => token.length >= 2);
1044
+ }
1045
+ function formatDateHint(dateFrom, dateTo) {
1046
+ if (!dateFrom && !dateTo) {
1047
+ return void 0;
1048
+ }
1049
+ const fromText = dateFrom ?? "the beginning";
1050
+ const toText = dateTo ?? "now";
1051
+ return `Restrict searches to the date range ${fromText} to ${toText}. You may still emit narrower date_from/date_to bounds when useful.`;
1052
+ }
1053
+ function buildInitialPrompt(query, dateHint) {
1054
+ const hintText = dateHint ? `
1055
+ ${dateHint}` : "";
1056
+ return `${SYSTEM_PROMPT2}
1057
+
1058
+ Question to research: ${query}${hintText}
1059
+ Search the conversation history and collect all relevant evidence. Start with a broad keyword search.`;
1060
+ }
1061
+ function buildObservationPrompt(hits) {
1062
+ if (hits.length === 0) {
1063
+ return "Observation: No matching memory found.\nRespond with the next action only.";
1064
+ }
1065
+ const blocks = hits.map((hit, index) => {
1066
+ const lines = [`Result ${index + 1} | score=${hit.similarity.toFixed(4)}`];
1067
+ for (const line of hit.content.split("\n")) {
1068
+ if (line.trim()) lines.push(line);
1069
+ }
1070
+ return lines.join("\n");
1071
+ });
1072
+ return `Observation:
1073
+ ${blocks.join("\n\n")}
1074
+
1075
+ Respond with the next action only.`;
1076
+ }
1077
+ function stripCodeFences(text) {
1078
+ return text.replace(/```(?:json)?\s*([\s\S]*?)\s*```/g, "$1");
1079
+ }
1080
+ function extractJsonObject(text) {
1081
+ const start = text.indexOf("{");
1082
+ if (start === -1) {
1083
+ return void 0;
1084
+ }
1085
+ let depth = 0;
1086
+ let inString = false;
1087
+ let escaped = false;
1088
+ for (let i = start; i < text.length; i += 1) {
1089
+ const ch = text[i];
1090
+ if (escaped) {
1091
+ escaped = false;
1092
+ continue;
1093
+ }
1094
+ if (inString) {
1095
+ if (ch === "\\") {
1096
+ escaped = true;
1097
+ } else if (ch === '"') {
1098
+ inString = false;
1099
+ }
1100
+ continue;
1101
+ }
1102
+ if (ch === '"') {
1103
+ inString = true;
1104
+ } else if (ch === "{") {
1105
+ depth += 1;
1106
+ } else if (ch === "}") {
1107
+ depth -= 1;
1108
+ if (depth === 0) {
1109
+ return text.slice(start, i + 1);
1110
+ }
1111
+ }
1112
+ }
1113
+ return void 0;
1114
+ }
1115
+ function parseAction(text) {
1116
+ const cleaned = stripCodeFences(text);
1117
+ const actionMatch = cleaned.match(/Action:\s*([\w_]+)/);
1118
+ if (!actionMatch) return null;
1119
+ const action = actionMatch[1].trim().toLowerCase();
1120
+ if (action === "finish") {
1121
+ return { type: "finish" };
1122
+ }
1123
+ const inputMatch = cleaned.match(/Action Input:\s*/s);
1124
+ if (!inputMatch || inputMatch.index === void 0) return null;
1125
+ const jsonText = extractJsonObject(cleaned.slice(inputMatch.index + inputMatch[0].length));
1126
+ if (!jsonText) return null;
1127
+ let parsedJson = null;
1128
+ for (const candidate of [jsonText, jsonText.replace(/'/g, '"')]) {
1129
+ try {
1130
+ parsedJson = JSON.parse(candidate);
1131
+ break;
1132
+ } catch {
1133
+ }
1134
+ }
1135
+ if (!parsedJson) return null;
1136
+ if (action === "search") {
1137
+ const keywords = parsedJson.keywords;
1138
+ if (!Array.isArray(keywords) || !keywords.every((k) => typeof k === "string")) {
1139
+ return null;
1140
+ }
1141
+ return {
1142
+ type: "search",
1143
+ keywords,
1144
+ dateFrom: typeof parsedJson.date_from === "string" ? parsedJson.date_from : void 0,
1145
+ dateTo: typeof parsedJson.date_to === "string" ? parsedJson.date_to : void 0
1146
+ };
1147
+ }
1148
+ if (action === "note") {
1149
+ const indices = parsedJson.indices;
1150
+ if (!Array.isArray(indices) || !indices.every((i) => typeof i === "number")) {
1151
+ return null;
1152
+ }
1153
+ return { type: "note", indices };
1154
+ }
1155
+ return null;
1156
+ }
1157
+ function createIterativeRecallPlanner(deps) {
1158
+ const { complete, options = {} } = deps;
1159
+ const defaults = {
1160
+ maxIterations: 4,
1161
+ searchTopK: 5,
1162
+ disabled: false,
1163
+ fallbackToBaseline: true
1164
+ };
1165
+ let lastDegraded = false;
1166
+ let iterationsWithValidAction = 0;
1167
+ return {
1168
+ lastDegraded: () => lastDegraded,
1169
+ async plan(input) {
1170
+ const opts = {
1171
+ ...defaults,
1172
+ ...options,
1173
+ ...input.options
1174
+ };
1175
+ lastDegraded = false;
1176
+ iterationsWithValidAction = 0;
1177
+ if (opts.disabled || !input.query.trim()) {
1178
+ return { evidence: [], stats: { iterations: 0, searches: 0, notes: 0 } };
1179
+ }
1180
+ const messages = [
1181
+ { role: "system", content: SYSTEM_PROMPT2 },
1182
+ {
1183
+ role: "user",
1184
+ content: buildInitialPrompt(input.query, formatDateHint(input.dateFrom, input.dateTo))
1185
+ }
1186
+ ];
1187
+ const evidence = /* @__PURE__ */ new Map();
1188
+ const excludedIds = /* @__PURE__ */ new Set();
1189
+ let lastHits = [];
1190
+ let searches = 0;
1191
+ let notes = 0;
1192
+ let iterations = 0;
1193
+ let completeThrew = false;
1194
+ for (let i = 0; i < opts.maxIterations; i += 1) {
1195
+ iterations = i + 1;
1196
+ let reply;
1197
+ try {
1198
+ reply = await complete(messages.map((m) => `${m.role}: ${m.content}`).join("\n\n"));
1199
+ } catch {
1200
+ completeThrew = true;
1201
+ break;
1202
+ }
1203
+ messages.push({ role: "assistant", content: reply });
1204
+ const action = parseAction(reply);
1205
+ if (!action) {
1206
+ messages.push({
1207
+ role: "user",
1208
+ content: "Observation: Use exactly one valid action: search, note, or finish. Respond with the next action only."
1209
+ });
1210
+ continue;
1211
+ }
1212
+ iterationsWithValidAction += 1;
1213
+ if (action.type === "finish") {
1214
+ break;
1215
+ }
1216
+ if (action.type === "search") {
1217
+ const result = await input.executor.search({
1218
+ keywords: action.keywords,
1219
+ dateFrom: action.dateFrom ?? input.dateFrom,
1220
+ dateTo: action.dateTo ?? input.dateTo
1221
+ });
1222
+ lastHits = result.candidates.filter((c) => !excludedIds.has(c.id)).slice(0, opts.searchTopK);
1223
+ for (const hit of lastHits) {
1224
+ excludedIds.add(hit.id);
1225
+ }
1226
+ searches += 1;
1227
+ messages.push({ role: "user", content: buildObservationPrompt(lastHits) });
1228
+ continue;
1229
+ }
1230
+ if (action.type === "note") {
1231
+ let saved = 0;
1232
+ for (const index of action.indices) {
1233
+ if (index >= 1 && index <= lastHits.length) {
1234
+ const hit = lastHits[index - 1];
1235
+ if (!evidence.has(hit.id)) {
1236
+ evidence.set(hit.id, hit);
1237
+ saved += 1;
1238
+ }
1239
+ }
1240
+ }
1241
+ notes += saved;
1242
+ messages.push({
1243
+ role: "user",
1244
+ content: `Observation: Saved ${saved} new result(s). Total notes: ${evidence.size}.
1245
+ Respond with the next action only.`
1246
+ });
1247
+ }
1248
+ }
1249
+ let fallbackRan = false;
1250
+ if (evidence.size === 0 && opts.fallbackToBaseline) {
1251
+ fallbackRan = true;
1252
+ if (lastHits.length > 0) {
1253
+ for (const hit of lastHits) {
1254
+ if (!evidence.has(hit.id)) {
1255
+ evidence.set(hit.id, hit);
1256
+ }
1257
+ }
1258
+ } else {
1259
+ const baseline = await input.executor.search({
1260
+ keywords: deriveKeywordsFromQuery(input.query),
1261
+ dateFrom: input.dateFrom,
1262
+ dateTo: input.dateTo
1263
+ });
1264
+ searches += 1;
1265
+ for (const hit of baseline.candidates.slice(0, opts.searchTopK)) {
1266
+ if (!evidence.has(hit.id)) {
1267
+ evidence.set(hit.id, hit);
1268
+ }
1269
+ }
1270
+ }
1271
+ }
1272
+ lastDegraded = completeThrew || iterationsWithValidAction === 0 || fallbackRan;
1273
+ return {
1274
+ evidence: Array.from(evidence.values()),
1275
+ stats: { iterations, searches, notes }
1276
+ };
1277
+ }
1278
+ };
1279
+ }
1280
+ function createIdentityIterativePlanner() {
1281
+ return {
1282
+ lastDegraded: () => false,
1283
+ async plan() {
1284
+ return { evidence: [], stats: { iterations: 0, searches: 0, notes: 0 } };
1285
+ }
1286
+ };
1287
+ }
1288
+
1289
+ // src/index.ts
665
1290
  async function createMemoryStore(config = {}) {
666
1291
  const dbPath = config.dbPath ?? (config.db && "path" in config.db ? config.db.path : void 0);
667
1292
  const raw = createRawMessageStore({ env: config.env, dbPath });
@@ -676,4 +1301,4 @@ async function createMemoryStore(config = {}) {
676
1301
  };
677
1302
  }
678
1303
 
679
- export { clampUnifiedMemorySearchLimit, clampUnifiedMemorySearchThreshold, clearPostgresFactory, closeRawMessageStore, createMemoryStore, createRawMessageStore, createUnifiedSearch, getRawMessageManager, getRawMessageStorageBackend, hasPostgresFactory, isRawMemorySemanticResult, isRawMessageStorageAvailable, mergeUnifiedMemorySearchResults, normalizeUnifiedMemorySearchSources, registerPostgresFactory, resolvePostgresFactory, toKnowledgeResult, toMemoryResult };
1304
+ export { clampUnifiedMemorySearchLimit, clampUnifiedMemorySearchThreshold, clearPostgresFactory, closeRawMessageStore, createIdentityIterativePlanner, createIdentityRewriter, createIterativeRecallPlanner, createMemoryStore, createRawMessageStore, createUnifiedSearch, createUserVoiceRewriter, getRawMessageManager, getRawMessageStorageBackend, hasPostgresFactory, isRawMemorySemanticResult, isRawMessageStorageAvailable, mergeUnifiedMemorySearchResults, normalizeUnifiedMemoryReasoningStrategy, normalizeUnifiedMemorySearchSources, registerPostgresFactory, resolvePostgresFactory, toKnowledgeResult, toMemoryResult };