@melandlabs/memory-store 0.2.7 → 0.3.1

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