@modusensus/dsh-mneme 0.7.10 → 0.7.12

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.
Files changed (158) hide show
  1. package/README.en.md +437 -0
  2. package/README.md +364 -166
  3. package/bin/cli.mjs +603 -0
  4. package/{dsh-mneme/cordis.patch.yml → cordis.patch.yml} +1 -0
  5. package/lib/api-standalone.js +264 -0
  6. package/{dsh-mneme/lib → lib}/api.js +133 -289
  7. package/{dsh-mneme/lib → lib}/client.js +1008 -922
  8. package/{dsh-mneme/lib → lib}/config.js +54 -99
  9. package/{dsh-mneme/lib → lib}/dream/decisions.js +94 -143
  10. package/{dsh-mneme/lib → lib}/dream/sleep.js +12 -152
  11. package/{dsh-mneme/lib → lib}/dream.js +22 -168
  12. package/{dsh-mneme/src → lib}/hot-memory.js +0 -7
  13. package/{dsh-mneme/lib → lib}/index.js +45 -42
  14. package/{dsh-mneme/lib → lib}/inject.js +11 -53
  15. package/{dsh-mneme/src → lib}/mirror.js +1 -12
  16. package/{dsh-mneme/lib → lib}/quality-filter.js +1 -3
  17. package/{dsh-mneme/lib → lib}/reranker.js +1 -4
  18. package/{dsh-mneme/lib → lib}/service.js +20 -410
  19. package/{dsh-mneme/src → lib}/settings.js +34 -64
  20. package/{dsh-mneme/lib → lib}/store.js +38 -450
  21. package/{dsh-mneme/lib → lib}/summarize.js +10 -10
  22. package/lib/tools.js +274 -0
  23. package/package.json +45 -19
  24. package/{dsh-mneme/scripts → scripts}/e2e-dsh.js +3 -4
  25. package/{dsh-mneme/scripts → scripts}/sync-lib.js +5 -10
  26. package/src/api-standalone.js +264 -0
  27. package/{dsh-mneme/src → src}/api.js +133 -289
  28. package/{dsh-mneme/src → src}/config.js +54 -99
  29. package/{dsh-mneme/src → src}/dream/decisions.js +94 -143
  30. package/{dsh-mneme/src → src}/dream/sleep.js +12 -152
  31. package/{dsh-mneme/src → src}/dream.js +22 -168
  32. package/{dsh-mneme/lib → src}/hot-memory.js +0 -7
  33. package/{dsh-mneme/src → src}/index.js +45 -42
  34. package/{dsh-mneme/src → src}/inject.js +11 -53
  35. package/{dsh-mneme/lib → src}/mirror.js +1 -12
  36. package/{dsh-mneme/src → src}/quality-filter.js +1 -3
  37. package/{dsh-mneme/src → src}/reranker.js +1 -4
  38. package/{dsh-mneme/src → src}/service.js +20 -410
  39. package/{dsh-mneme/lib → src}/settings.js +34 -64
  40. package/{dsh-mneme/src → src}/store.js +38 -450
  41. package/{dsh-mneme/src → src}/summarize.js +10 -10
  42. package/src/tools.js +274 -0
  43. package/{dsh-mneme/test → test}/api.test.js +45 -156
  44. package/{dsh-mneme/test → test}/client.test.js +10 -206
  45. package/{dsh-mneme/test → test}/config.test.js +0 -21
  46. package/{dsh-mneme/test → test}/dream.test.js +1 -160
  47. package/{dsh-mneme/test → test}/graph-api.test.js +0 -34
  48. package/{dsh-mneme/test → test}/hot-memory.test.js +0 -29
  49. package/test/inject.test.js +120 -0
  50. package/{dsh-mneme/test → test}/llm-audit.test.js +4 -4
  51. package/{dsh-mneme/test → test}/reasoning-effort.test.js +0 -27
  52. package/{dsh-mneme/test → test}/recall-layer.test.js +5 -26
  53. package/{dsh-mneme/test → test}/reranker.test.js +0 -43
  54. package/{dsh-mneme/test → test}/service-search.test.js +0 -25
  55. package/{dsh-mneme/test → test}/service.test.js +0 -106
  56. package/{dsh-mneme/test → test}/settings.test.js +16 -35
  57. package/{dsh-mneme/test → test}/sleep.test.js +4 -171
  58. package/test/standalone-api.test.js +326 -0
  59. package/{dsh-mneme/test → test}/store.test.js +0 -76
  60. package/{dsh-mneme/test → test}/summarize.test.js +16 -15
  61. package/test/tools.test.js +265 -0
  62. package/.github/workflows/publish.yml +0 -58
  63. package/.github/workflows/test.yml +0 -32
  64. package/CHANGELOG.md +0 -91
  65. package/CONTRIBUTING.md +0 -293
  66. package/SECURITY.md +0 -718
  67. package/docs/devlog/2026-08-14-dsh-mneme-dev-log.md +0 -247
  68. package/docs/devlog/2026-08-15-dsh-mneme-audit-stress-dev-log.md +0 -145
  69. package/docs/devlog/2026-08-15-dsh-mneme-pipeline-dev-log.md +0 -56
  70. package/docs/devlog/2026-08-15-dsh-mneme-reflection-dev-log.md +0 -77
  71. package/docs/devlog/2026-08-15-dsh-mneme-review-fixes-dev-log.md +0 -64
  72. package/docs/devlog/2026-08-15-dsh-mneme-semantic-dev-log.md +0 -90
  73. package/dsh-mneme/CHANGELOG.md +0 -419
  74. package/dsh-mneme/LICENSE +0 -21
  75. package/dsh-mneme/README.md +0 -501
  76. package/dsh-mneme/docs/AGENT_MEMORY_RESEARCH.md +0 -183
  77. package/dsh-mneme/docs/ENTITIES.md +0 -245
  78. package/dsh-mneme/docs/LOCAL_MODEL.md +0 -141
  79. package/dsh-mneme/docs/MIGRATION.md +0 -127
  80. package/dsh-mneme/docs/SEMANTIC.md +0 -256
  81. package/dsh-mneme/docs/SLEEP.md +0 -163
  82. package/dsh-mneme/lib/dream/tag-extractor.js +0 -156
  83. package/dsh-mneme/lib/heat.js +0 -136
  84. package/dsh-mneme/lib/parser/tag.js +0 -59
  85. package/dsh-mneme/lib/parser/wiki-link.js +0 -38
  86. package/dsh-mneme/lib/search/tag-boost.js +0 -61
  87. package/dsh-mneme/lib/tools.js +0 -458
  88. package/dsh-mneme/package-lock.json +0 -1936
  89. package/dsh-mneme/package.json +0 -83
  90. package/dsh-mneme/scripts/check-sync.js +0 -42
  91. package/dsh-mneme/src/client.js +0 -2242
  92. package/dsh-mneme/src/dream/tag-extractor.js +0 -156
  93. package/dsh-mneme/src/heat.js +0 -136
  94. package/dsh-mneme/src/parser/tag.js +0 -59
  95. package/dsh-mneme/src/parser/wiki-link.js +0 -38
  96. package/dsh-mneme/src/search/tag-boost.js +0 -61
  97. package/dsh-mneme/src/tools.js +0 -458
  98. package/dsh-mneme/test/boundary-v0625.test.js +0 -82
  99. package/dsh-mneme/test/directory.test.js +0 -134
  100. package/dsh-mneme/test/heat.test.js +0 -148
  101. package/dsh-mneme/test/inject.test.js +0 -206
  102. package/dsh-mneme/test/layered-types-stats.test.js +0 -144
  103. package/dsh-mneme/test/lib-smoke.test.js +0 -109
  104. package/dsh-mneme/test/normalize-decisions.test.js +0 -120
  105. package/dsh-mneme/test/provenance.test.js +0 -103
  106. package/dsh-mneme/test/recall-runs.test.js +0 -93
  107. package/dsh-mneme/test/sleep-heat.test.js +0 -112
  108. package/dsh-mneme/test/tag-boost.test.js +0 -125
  109. package/dsh-mneme/test/tag.test.js +0 -426
  110. package/dsh-mneme/test/tools.test.js +0 -674
  111. package/dsh-mneme/test/updated-at-semantics.test.js +0 -113
  112. package/dsh-mneme/test/wiki-link.test.js +0 -332
  113. package//346/250/252/345/271/205.png +0 -0
  114. /package/{dsh-mneme/lib → lib}/commands.js +0 -0
  115. /package/{dsh-mneme/lib → lib}/dream/clustering.js +0 -0
  116. /package/{dsh-mneme/lib → lib}/embedding.js +0 -0
  117. /package/{dsh-mneme/lib → lib}/entities/extractor.js +0 -0
  118. /package/{dsh-mneme/lib → lib}/local-embedder.js +0 -0
  119. /package/{dsh-mneme/lib → lib}/search/adaptive.js +0 -0
  120. /package/{dsh-mneme/lib → lib}/search/bm25.js +0 -0
  121. /package/{dsh-mneme/lib → lib}/vector-index.js +0 -0
  122. /package/{dsh-mneme/scripts → scripts}/benchmark-embed.js +0 -0
  123. /package/{dsh-mneme/scripts → scripts}/benchmark-recall.js +0 -0
  124. /package/{dsh-mneme/scripts → scripts}/benchmark-rerank.js +0 -0
  125. /package/{dsh-mneme/scripts → scripts}/stress-dsh.js +0 -0
  126. /package/{dsh-mneme/src → src}/commands.js +0 -0
  127. /package/{dsh-mneme/src → src}/dream/clustering.js +0 -0
  128. /package/{dsh-mneme/src → src}/embedding.js +0 -0
  129. /package/{dsh-mneme/src → src}/entities/extractor.js +0 -0
  130. /package/{dsh-mneme/src → src}/local-embedder.js +0 -0
  131. /package/{dsh-mneme/src → src}/search/adaptive.js +0 -0
  132. /package/{dsh-mneme/src → src}/search/bm25.js +0 -0
  133. /package/{dsh-mneme/src → src}/vector-index.js +0 -0
  134. /package/{dsh-mneme/test → test}/audit.test.js +0 -0
  135. /package/{dsh-mneme/test → test}/benchmark.test.js +0 -0
  136. /package/{dsh-mneme/test → test}/clustering.test.js +0 -0
  137. /package/{dsh-mneme/test → test}/commands.test.js +0 -0
  138. /package/{dsh-mneme/test → test}/conflict-freeze.test.js +0 -0
  139. /package/{dsh-mneme/test → test}/entities.test.js +0 -0
  140. /package/{dsh-mneme/test → test}/epistemic.test.js +0 -0
  141. /package/{dsh-mneme/test → test}/fnew-0112.test.js +0 -0
  142. /package/{dsh-mneme/test → test}/fnew-03.test.js +0 -0
  143. /package/{dsh-mneme/test → test}/helpers/dream-mock.js +0 -0
  144. /package/{dsh-mneme/test → test}/local-embedder.test.js +0 -0
  145. /package/{dsh-mneme/test → test}/mirror-dirty.test.js +0 -0
  146. /package/{dsh-mneme/test → test}/mirror-edit-digest.test.js +0 -0
  147. /package/{dsh-mneme/test → test}/mirror-generation.test.js +0 -0
  148. /package/{dsh-mneme/test → test}/mirror.test.js +0 -0
  149. /package/{dsh-mneme/test → test}/peer-blockers.test.js +0 -0
  150. /package/{dsh-mneme/test → test}/policy-epoch.test.js +0 -0
  151. /package/{dsh-mneme/test → test}/quality-filter.test.js +0 -0
  152. /package/{dsh-mneme/test → test}/recall-evals.test.js +0 -0
  153. /package/{dsh-mneme/test → test}/receipt-chain.test.js +0 -0
  154. /package/{dsh-mneme/test → test}/reflection.test.js +0 -0
  155. /package/{dsh-mneme/test → test}/search-fusion.test.js +0 -0
  156. /package/{dsh-mneme/test → test}/semantic.test.js +0 -0
  157. /package/{dsh-mneme/test → test}/stress.test.js +0 -0
  158. /package/{dsh-mneme/test → test}/vector-index.test.js +0 -0
@@ -1,6 +1,5 @@
1
1
  import { DatabaseSync } from "node:sqlite";
2
2
  import { randomUUID } from "node:crypto";
3
- import { sanitizeTags } from "./parser/tag.js";
4
3
 
5
4
  const SCHEMA = `
6
5
  CREATE TABLE IF NOT EXISTS memories (
@@ -12,15 +11,12 @@ CREATE TABLE IF NOT EXISTS memories (
12
11
  importance INTEGER NOT NULL DEFAULT 3,
13
12
  forgotten INTEGER NOT NULL DEFAULT 0,
14
13
  archived INTEGER NOT NULL DEFAULT 0,
15
- session_disposed_at TEXT,
16
14
  source TEXT,
17
- session_id TEXT,
18
15
  content_history TEXT,
19
16
  embedding TEXT,
20
17
  epistemic_status TEXT NOT NULL DEFAULT 'subjective',
21
18
  last_accessed_at TEXT,
22
19
  _full_content TEXT,
23
- metadata TEXT, -- JSON: free-form extras (e.g. sleep entity_extracted_at)
24
20
  created_at TEXT NOT NULL,
25
21
  updated_at TEXT NOT NULL
26
22
  );
@@ -243,7 +239,10 @@ CREATE TABLE IF NOT EXISTS mirror_state (
243
239
  );
244
240
  `;
245
241
 
246
- const TYPES = new Set(["preference", "project", "decision", "history", "summary", "pattern", "user", "fact"]);
242
+ // Exported for API-layer type validation (standalone API POST /memories and
243
+ // the /status byType breakdown); the set itself stays the single source of
244
+ // truth for what store.save accepts.
245
+ export const TYPES = new Set(["preference", "project", "decision", "history", "summary", "pattern"]);
247
246
 
248
247
  // Epistemic status: what kind of evidence a memory rests on. Defaults to
249
248
  // 'subjective' so legacy rows (and rows without any signal) stay compatible.
@@ -315,14 +314,6 @@ function parseTags(raw) {
315
314
 
316
315
  function toRow(row) {
317
316
  if (!row) return undefined;
318
- let metadata;
319
- if (row.metadata != null) {
320
- try {
321
- metadata = JSON.parse(row.metadata);
322
- } catch {
323
- metadata = row.metadata;
324
- }
325
- }
326
317
  return {
327
318
  id: row.id,
328
319
  type: row.type,
@@ -332,13 +323,10 @@ function toRow(row) {
332
323
  importance: row.importance,
333
324
  forgotten: row.forgotten === 1,
334
325
  archived: row.archived === 1,
335
- session_disposed_at: row.session_disposed_at ?? undefined,
336
326
  source: row.source ?? undefined,
337
- session_id: row.session_id ?? undefined,
338
327
  content_history: parseJsonArray(row.content_history),
339
328
  quality_score: row.quality_score !== null && row.quality_score !== undefined ? Number(row.quality_score) : undefined,
340
329
  epistemic_status: row.epistemic_status ?? "subjective",
341
- metadata,
342
330
  created_at: row.created_at,
343
331
  updated_at: row.updated_at,
344
332
  last_accessed_at: row.last_accessed_at ?? undefined,
@@ -563,17 +551,6 @@ export function createStore(path) {
563
551
  db.exec("PRAGMA journal_mode = WAL;");
564
552
  db.exec(SCHEMA);
565
553
 
566
- // Wiki-link dedup (v0.6.1): a (from_entity, to_entity) pair is unique only for
567
- // relation_type='links_to'. This is a PARTIAL index scoped to links_to, so the
568
- // append-only semantics of all other relation types (uses/depends_on/part_of/
569
- // related_to/supersedes — the extractor and autoDream write these per-run
570
- // without global dedup, and supersedes rows carry distinct metadata like
571
- // attr_key/old_value) are preserved. Idempotent (IF NOT EXISTS), atomic, and
572
- // race-safe. Legacy DBs have no links_to rows yet, so the index builds cleanly
573
- // everywhere and never breaks plugin startup (a full-table UNIQUE index would
574
- // fail on legacy duplicates).
575
- db.exec("CREATE UNIQUE INDEX IF NOT EXISTS idx_relations_wikilink ON entity_relations(from_entity, to_entity, relation_type) WHERE relation_type = 'links_to'");
576
-
577
554
  // Schema migrations for legacy databases (idempotent). Each ADD COLUMN is
578
555
  // also race-safe: two concurrently-opening processes can both pass the
579
556
  // PRAGMA table_info check before either ALTERs, so the ALTER itself is
@@ -591,21 +568,12 @@ export function createStore(path) {
591
568
  };
592
569
 
593
570
  addColumn("memories", "archived", "ALTER TABLE memories ADD COLUMN archived INTEGER NOT NULL DEFAULT 0");
594
- addColumn("memories", "session_disposed_at", "ALTER TABLE memories ADD COLUMN session_disposed_at TEXT");
595
571
  addColumn("memories", "embedding", "ALTER TABLE memories ADD COLUMN embedding TEXT");
596
572
  addColumn("memories", "last_accessed_at", "ALTER TABLE memories ADD COLUMN last_accessed_at TEXT");
597
573
  addColumn("memories", "_full_content", "ALTER TABLE memories ADD COLUMN _full_content TEXT");
598
574
  addColumn("memories", "epistemic_status", "ALTER TABLE memories ADD COLUMN epistemic_status TEXT NOT NULL DEFAULT 'subjective'");
599
575
  addColumn("memories", "content_history", "ALTER TABLE memories ADD COLUMN content_history TEXT");
600
576
  addColumn("memories", "quality_score", "ALTER TABLE memories ADD COLUMN quality_score REAL");
601
- addColumn("memories", "session_id", "ALTER TABLE memories ADD COLUMN session_id TEXT");
602
- addColumn("memories", "metadata", "ALTER TABLE memories ADD COLUMN metadata TEXT");
603
-
604
- // Composite index for session-lifecycle queries (dispose/restore/listBySession).
605
- // Created post-migration, NOT in SCHEMA: on legacy DBs both columns arrive via
606
- // ADD COLUMN above, so the index would fail at db.exec(SCHEMA) time. CREATE
607
- // INDEX IF NOT EXISTS is atomic, so the two-process race is safe here.
608
- db.exec("CREATE INDEX IF NOT EXISTS idx_memories_session ON memories(session_id, session_disposed_at)");
609
577
 
610
578
  // Legacy dream_runs without policy_epoch → backfill with the default epoch.
611
579
  addColumn("dream_runs", "policy_epoch", "ALTER TABLE dream_runs ADD COLUMN policy_epoch INTEGER NOT NULL DEFAULT 0");
@@ -650,98 +618,37 @@ export function createStore(path) {
650
618
  return ts;
651
619
  }
652
620
 
653
- function count(type, { includeForgotten = false, includeArchived = false, includeDisposed = false } = {}) {
621
+ function count(type, { minImportance = null, source = null, includeForgotten = false, includeArchived = false } = {}) {
654
622
  const clauses = [];
655
623
  const params = [];
656
624
  if (type !== undefined) {
657
625
  clauses.push("type = ?");
658
626
  params.push(type);
659
627
  }
628
+ // Same filters as list() so a paged caller's total matches its rows.
629
+ if (minImportance != null) {
630
+ clauses.push("importance >= ?");
631
+ params.push(minImportance);
632
+ }
633
+ if (source != null) {
634
+ clauses.push("source = ?");
635
+ params.push(source);
636
+ }
660
637
  if (!includeForgotten) {
661
638
  clauses.push("forgotten = 0");
662
639
  }
663
640
  if (!includeArchived) {
664
641
  clauses.push("archived = 0");
665
642
  }
666
- if (!includeDisposed) {
667
- clauses.push("session_disposed_at IS NULL");
668
- }
669
643
  const where = clauses.length ? `WHERE ${clauses.join(" AND ")}` : "";
670
644
  return db.prepare(`SELECT count(*) AS c FROM memories ${where}`).get(...params).c;
671
645
  }
672
646
 
673
- /**
674
- * Aggregated stats for the Web panel layered overview: per-type distribution
675
- * plus a recent daily creation trend. Counts live (non-forgotten /
676
- * non-archived / not session-disposed) memories only, matching `count`.
677
- * created_at is ISO TEXT, so date strings compare lexicographically.
678
- */
679
- function stats({ days = 7 } = {}) {
680
- const LIVE = "forgotten = 0 AND archived = 0 AND session_disposed_at IS NULL";
681
- // Clamp + integer-coerce so fractional strings (e.g. ?days=7.5) can't
682
- // produce ragged trend windows (kimi-k2.7-code 复验 S2).
683
- days = Math.min(30, Math.max(1, Math.floor(Number(days) || 7)));
684
- const now = Date.now(); // single clock read so the window doesn't span a day boundary
685
- const byType = db.prepare(
686
- `SELECT type, count(*) AS c FROM memories WHERE ${LIVE} GROUP BY type`
687
- ).all();
688
- const byTypeCounts = {};
689
- for (const row of byType) byTypeCounts[row.type] = row.c;
690
- const since = new Date(now - (days - 1) * 86400000).toISOString().slice(0, 10);
691
- const trend = db.prepare(
692
- `SELECT substr(created_at, 1, 10) AS d, count(*) AS c FROM memories
693
- WHERE ${LIVE} AND created_at >= ? GROUP BY d ORDER BY d`
694
- ).all(since);
695
- const trendCounts = {};
696
- for (const row of trend) trendCounts[row.d] = row.c;
697
- const recent = [];
698
- for (let i = days - 1; i >= 0; i--) {
699
- const d = new Date(now - i * 86400000).toISOString().slice(0, 10);
700
- recent.push({ date: d, count: trendCounts[d] ?? 0 });
701
- }
702
- const total = Object.values(byTypeCounts).reduce((s, n) => s + n, 0);
703
- return { byType: byTypeCounts, total, recent, days };
704
- }
705
-
706
647
  function getById(id) {
707
648
  const row = db.prepare("SELECT * FROM memories WHERE id = ?").get(id);
708
649
  return toRow(row);
709
650
  }
710
651
 
711
- /**
712
- * issue #48: resolve a truncated id — as can leak through an agent's context
713
- * window when list/search output is shortened — by matching it as a prefix of
714
- * the id PRIMARY KEY. Exact lookups keep using getById; this only serves
715
- * resolving a *candidate* id. Returns up to 51 rows so the caller can tell
716
- * "unique" from "ambiguous" without a second query. LIKE wildcards are
717
- * stripped from the input (a valid id fragment is hex/UUID text, never % or
718
- * _). Prefix over a PK stays an index scan, so this is cheap even at scale.
719
- */
720
- function listByIdPrefix(idPrefix) {
721
- if (typeof idPrefix !== "string" || !idPrefix.trim()) return [];
722
- // LIKE 通配符不参与 id 匹配,一律剥掉。若剥完为空(如 id="%"),
723
- // 不能让 SQL 退化成 `LIKE '%'` 全表命中——那会让单条记忆被误删,
724
- // 一律视为无匹配返回。
725
- const safe = idPrefix.replace(/[\\%_]/g, "");
726
- if (!safe) return [];
727
- const rows = db.prepare("SELECT * FROM memories WHERE id LIKE ? LIMIT 51")
728
- .all(`${safe}%`);
729
- return rows.map(toRow);
730
- }
731
-
732
- /**
733
- * Case-insensitive exact title lookup (v0.6.1 wiki-link). COLLATE NOCASE
734
- * folds ASCII case (CJK titles are inherently case-free, so they match
735
- * verbatim). Returns the first matching memory or undefined. Best-effort —
736
- * used by wiki-link target resolution and the read APIs.
737
- */
738
- function findByTitle(title) {
739
- if (typeof title !== "string" || !title.trim()) return undefined;
740
- return toRow(db.prepare(
741
- "SELECT * FROM memories WHERE title = ? COLLATE NOCASE LIMIT 1"
742
- ).get(title.trim()));
743
- }
744
-
745
652
  function save(memory) {
746
653
  const id = memory.id ?? randomUUID();
747
654
  const type = memory.type;
@@ -762,8 +669,8 @@ export function createStore(path) {
762
669
  : inferEpistemicStatus(memory);
763
670
  runAtomically(() => {
764
671
  db.prepare(
765
- `INSERT INTO memories (id, type, title, content, tags, importance, forgotten, archived, source, session_id, content_history, quality_score, embedding, epistemic_status, created_at, updated_at)
766
- VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
672
+ `INSERT INTO memories (id, type, title, content, tags, importance, forgotten, archived, source, content_history, quality_score, embedding, epistemic_status, created_at, updated_at)
673
+ VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?, ?, ?, ?, ?, ?, ?)`
767
674
  ).run(
768
675
  id,
769
676
  type,
@@ -773,7 +680,6 @@ export function createStore(path) {
773
680
  importance,
774
681
  memory.archived ? 1 : 0,
775
682
  memory.source ?? null,
776
- memory.session_id ?? null,
777
683
  JSON.stringify(memory.content_history ?? []),
778
684
  Number.isFinite(memory.quality_score) ? memory.quality_score : null,
779
685
  embedding,
@@ -790,9 +696,6 @@ export function createStore(path) {
790
696
  return getById(id);
791
697
  }
792
698
 
793
- // v0.7.0 语义:updated_at 只记录内容变更,绝不当访问(与 last_accessed_at
794
- // 正交,sleep/heat 永不 fallback 到它)。合并/更新刷 updated_at 不应
795
- // 重置热度时钟 —— 防 autoDream 合并动作伪装成"刚被召回"。
796
699
  function update(id, patch) {
797
700
  const existing = getById(id);
798
701
  if (!existing) throw new Error(`memory not found: ${id}`);
@@ -845,50 +748,6 @@ export function createStore(path) {
845
748
  });
846
749
  }
847
750
 
848
- /**
849
- * Bounded scan for sleep's entity-extraction phase: oldest memories without
850
- * an entity_extracted_at stamp (row-level filters only — attr presence is a
851
- * cross-table check, so the caller filters the returned page via
852
- * getAttrsByMemory). Caller pages with {limit, offset}; a full-table scan +
853
- * JS filter would be O(N) per sleep cycle.
854
- */
855
- function listForEntityExtraction({ limit = 50, offset = 0 } = {}) {
856
- // node:sqlite has no StatementSync#pluck; all() returns row objects.
857
- return db
858
- .prepare(`
859
- SELECT id FROM memories
860
- WHERE (archived = 0 OR archived IS NULL)
861
- AND session_disposed_at IS NULL
862
- AND (forgotten = 0 OR forgotten IS NULL)
863
- AND type != 'summary'
864
- AND content IS NOT NULL AND content != ''
865
- AND (metadata IS NULL OR json_extract(metadata, '$.entity_extracted_at') IS NULL)
866
- ORDER BY created_at ASC
867
- LIMIT ? OFFSET ?
868
- `)
869
- .all(limit, offset)
870
- .map((row) => getById(row.id));
871
- }
872
-
873
- /**
874
- * Lightweight metadata-only update (used by sleep's batch entity extraction
875
- * to stamp `entity_extracted_at` without disturbing title/content/embedding
876
- * or the normal update semantics). Does not bump updated_at — metadata
877
- * stamps are bookkeeping, not content, so they must not fake freshness.
878
- * Incoming fields are MERGED into the existing metadata object so a stamp
879
- * never clobbers metadata another path just wrote.
880
- */
881
- function setMemoryMetadata(id, metadata) {
882
- const existing = getById(id);
883
- if (!existing) throw new Error(`memory not found: ${id}`);
884
- const patch = typeof metadata === "string"
885
- ? JSON.parse(metadata)
886
- : (metadata ?? {});
887
- const merged = { ...(existing.metadata ?? {}), ...patch };
888
- db.prepare("UPDATE memories SET metadata = ? WHERE id = ?").run(JSON.stringify(merged), id);
889
- return getById(id);
890
- }
891
-
892
751
  /**
893
752
  * Atomic compare-and-set update: applies `patch` only when the row still
894
753
  * carries `expectedUpdatedAt` (the version token read by the caller). Returns
@@ -969,45 +828,6 @@ export function createStore(path) {
969
828
  return getById(id);
970
829
  }
971
830
 
972
- // --- session lifecycle (v0.6.0) ------------------------------------------
973
- // Session dispose is orthogonal to `archived`: memory_archive is the user/AI
974
- // choosing to keep an entry long-term-but-quiet, while session_disposed_at
975
- // marks entries hidden because the session they were born in was deleted
976
- // (a reversible "undo" — restoreBySession clears it). They never clobber each
977
- // other: restoreBySession must not resurrect user-archived memories.
978
- // Mirrors list/search: disposed rows are hidden by default. A consumer that
979
- // needs to see the full picture (e.g. a restore flow that tells the user
980
- // "these N entries were hidden") opts in via includeDisposed.
981
- function listBySession(sessionId, { includeDisposed = false } = {}) {
982
- const disposedFilter = includeDisposed ? "" : "AND session_disposed_at IS NULL";
983
- const rows = db.prepare(
984
- `SELECT * FROM memories WHERE session_id = ? ${disposedFilter} ORDER BY updated_at DESC`
985
- ).all(sessionId);
986
- return rows.map(toRow);
987
- }
988
-
989
- // Idempotent by state guard, not timestamp compare (nowIso() differs every
990
- // call, so a fresh-timestamp re-dispose would spuriously count): dispose only
991
- // touches rows that are NOT yet disposed; restore only touches rows that ARE.
992
- // updated_at is deliberately left alone — this is a lifecycle flag, not
993
- // content — so a true flip is the sole trigger for a mirror generation.
994
- function setDisposedBySession(sessionId, disposed) {
995
- const at = disposed ? nowIso() : null;
996
- let affected = 0;
997
- runAtomically(() => {
998
- const result = disposed
999
- ? db.prepare(
1000
- "UPDATE memories SET session_disposed_at = ? WHERE session_id = ? AND session_disposed_at IS NULL"
1001
- ).run(at, sessionId)
1002
- : db.prepare(
1003
- "UPDATE memories SET session_disposed_at = NULL WHERE session_id = ? AND session_disposed_at IS NOT NULL"
1004
- ).run(sessionId);
1005
- affected = result.changes;
1006
- if (affected > 0) incrementGeneration();
1007
- });
1008
- return affected;
1009
- }
1010
-
1011
831
  // --- sleep-mode storage support (v0.4.0) ---------------------------------
1012
832
  // touchLastAccess stamps the read time on recall/inject paths. It deliberately
1013
833
  // does NOT bump the mirror generation: reads must not mark the mirror dirty.
@@ -1064,7 +884,6 @@ export function createStore(path) {
1064
884
  const rows = db.prepare(
1065
885
  `SELECT * FROM memories
1066
886
  WHERE forgotten = 0 AND archived = 0
1067
- AND session_disposed_at IS NULL
1068
887
  AND (last_accessed_at IS NULL OR last_accessed_at < ?)
1069
888
  ORDER BY COALESCE(last_accessed_at, created_at) ASC, id
1070
889
  LIMIT ?`
@@ -1072,26 +891,37 @@ export function createStore(path) {
1072
891
  return rows.map(toRow);
1073
892
  }
1074
893
 
1075
- function list({ type, limit = 50, offset = 0, includeForgotten = false, includeArchived = false, includeDisposed = false } = {}) {
894
+ function list({ type, limit = 50, offset = 0, order = "importance", includeForgotten = false, includeArchived = false, minImportance = null, source = null } = {}) {
1076
895
  const clauses = [];
1077
896
  const params = [];
1078
897
  if (type) {
1079
898
  clauses.push("type = ?");
1080
899
  params.push(type);
1081
900
  }
901
+ // Optional server-side filters: importance floor and exact source match.
902
+ // Both stay out of the query when unset so existing callers are unaffected.
903
+ if (minImportance != null) {
904
+ clauses.push("importance >= ?");
905
+ params.push(minImportance);
906
+ }
907
+ if (source) {
908
+ clauses.push("source = ?");
909
+ params.push(source);
910
+ }
1082
911
  if (!includeForgotten) {
1083
912
  clauses.push("forgotten = 0");
1084
913
  }
1085
914
  if (!includeArchived) {
1086
915
  clauses.push("archived = 0");
1087
916
  }
1088
- if (!includeDisposed) {
1089
- clauses.push("session_disposed_at IS NULL");
1090
- }
1091
917
  const { limit: lim, offset: off } = sanitizePage(limit, offset, 50);
1092
918
  const where = clauses.length ? `WHERE ${clauses.join(" AND ")}` : "";
919
+ // "chrono" is pure newest-first — the stable order paged browsing (month
920
+ // tree, infinite scroll) needs; importance ordering would interleave
921
+ // months across pages.
922
+ const orderBy = order === "chrono" ? "updated_at DESC, id DESC" : "importance DESC, updated_at DESC, id";
1093
923
  const rows = db.prepare(
1094
- `SELECT * FROM memories ${where} ORDER BY importance DESC, updated_at DESC, id LIMIT ? OFFSET ?`
924
+ `SELECT * FROM memories ${where} ORDER BY ${orderBy} LIMIT ? OFFSET ?`
1095
925
  ).all(...params, lim, off);
1096
926
  return rows.map(toRow);
1097
927
  }
@@ -1145,7 +975,7 @@ export function createStore(path) {
1145
975
  ).all(limit);
1146
976
  }
1147
977
 
1148
- function search(query, { limit = 20, includeArchived = false, includeDisposed = false } = {}) {
978
+ function search(query, { limit = 20, includeArchived = false } = {}) {
1149
979
  const q = String(query).trim();
1150
980
  if (!q) return [];
1151
981
  // Plain LIKE substring scan over title/content/tags (wildcards escaped so
@@ -1154,10 +984,9 @@ export function createStore(path) {
1154
984
  const like = `%${escapeLike(q)}%`;
1155
985
  const { limit: lim } = sanitizePage(limit, 0, 20);
1156
986
  const archivedFilter = includeArchived ? "" : "archived = 0 AND ";
1157
- const disposedFilter = includeDisposed ? "" : "session_disposed_at IS NULL AND ";
1158
987
  const rows = db.prepare(
1159
988
  `SELECT * FROM memories
1160
- WHERE ${archivedFilter}${disposedFilter}forgotten = 0 AND (title LIKE ? ESCAPE '\\' OR content LIKE ? ESCAPE '\\' OR tags LIKE ? ESCAPE '\\')
989
+ WHERE ${archivedFilter}forgotten = 0 AND (title LIKE ? ESCAPE '\\' OR content LIKE ? ESCAPE '\\' OR tags LIKE ? ESCAPE '\\')
1161
990
  ORDER BY
1162
991
  CASE WHEN title LIKE ? ESCAPE '\\' THEN 0 ELSE 1 END,
1163
992
  importance DESC,
@@ -1188,13 +1017,12 @@ export function createStore(path) {
1188
1017
  * Brute-force cosine similarity over embedded rows. Returns rows decorated
1189
1018
  * with a `score` (0..1). Only rows with a stored embedding participate.
1190
1019
  */
1191
- function searchVector(vector, { limit = 20, includeArchived = false, includeDisposed = false, threshold = 0 } = {}) {
1020
+ function searchVector(vector, { limit = 20, includeArchived = false, threshold = 0 } = {}) {
1192
1021
  if (!Array.isArray(vector) || !vector.length) return [];
1193
1022
  const archivedFilter = includeArchived ? "" : "archived = 0 AND ";
1194
- const disposedFilter = includeDisposed ? "" : "session_disposed_at IS NULL AND ";
1195
1023
  const rows = db.prepare(
1196
1024
  `SELECT * FROM memories
1197
- WHERE ${archivedFilter}${disposedFilter}forgotten = 0 AND embedding IS NOT NULL AND embedding != ''`
1025
+ WHERE ${archivedFilter}forgotten = 0 AND embedding IS NOT NULL AND embedding != ''`
1198
1026
  ).all();
1199
1027
  const scored = [];
1200
1028
  for (const row of rows) {
@@ -1400,19 +1228,6 @@ export function createStore(path) {
1400
1228
  return rows.map(toRecallRun);
1401
1229
  }
1402
1230
 
1403
- /**
1404
- * Rolling purge of the recall_runs production audit (v0.7.0): with recordRecall
1405
- * defaulting ON, the table would otherwise grow unboundedly on a busy store.
1406
- * Drop rows older than `days` (config.recallRetentionDays, default 90).
1407
- * created_at is an ISO TEXT string, so the ISO bound compares lexicographically.
1408
- * Returns the number of rows deleted (bookkeeping — never throws on empty).
1409
- */
1410
- function purgeRecallRunsOlderThan(days) {
1411
- const cutoff = new Date(Date.now() - days * 86400000).toISOString();
1412
- const result = db.prepare("DELETE FROM recall_runs WHERE created_at < ?").run(cutoff);
1413
- return result.changes;
1414
- }
1415
-
1416
1231
  // --- recall evaluation trail (方案 B: separate from the production audit) -
1417
1232
 
1418
1233
  /**
@@ -1810,173 +1625,9 @@ export function createStore(path) {
1810
1625
  return memories;
1811
1626
  }
1812
1627
 
1813
- // --- tag storage (v0.6.2) ------------------------------------------------
1814
- // Tags ride the snapshot-style entity_attrs table (attr_key='tags'), so one
1815
- // memory has exactly one live tags row; setMemoryTags invalidates any prior
1816
- // live row and inserts a fresh one (idempotent overwrite). entity_id is the
1817
- // memory id itself (the memory is its own tag entity), memory_id is kept so
1818
- // the existing memory-scoped attr queries (getAttrsByMemory / findMemoriesByAttr)
1819
- // and the bulk tag map all work without a special path.
1820
-
1821
- /** Normalize an arbitrary tags input to a deduplicated string array.
1822
- * Delegates to parser/tag.js sanitizeTags (shared validation with parseTags
1823
- * and the autoDream tag-extractor): strips a leading `#`, trims, drops
1824
- * non-strings/blanks/over-long/illegal-char tags. Kept as a thin alias so
1825
- * the tag write path validates identically to the parser path. */
1826
- function normalizeTags(tags) {
1827
- return sanitizeTags(tags);
1828
- }
1829
-
1830
- /**
1831
- * Set (overwrite) the live tag set for a memory. Exactly one tags row stays
1832
- * live per memory: any prior live row is invalidated first, then one fresh
1833
- * row is written (no-op when tags is empty — the invalidated row is removed
1834
- * so "clear tags" = no live row). Returns the stored tag array.
1835
- */
1836
- function setMemoryTags(memoryId, tags) {
1837
- const arr = normalizeTags(tags);
1838
- const now = nowIso();
1839
- // Atomic: the invalidation and the fresh row must land together, so a
1840
- // mid-write crash never leaves the old live row gone without a replacement.
1841
- // SAVEPOINT (not BEGIN) so this nests safely inside service.transaction().
1842
- db.exec("SAVEPOINT set_memory_tags");
1843
- try {
1844
- db.prepare(
1845
- `UPDATE entity_attrs SET valid_until = ?
1846
- WHERE attr_key = 'tags' AND memory_id = ? AND valid_until IS NULL`
1847
- ).run(now, memoryId);
1848
- if (arr.length) {
1849
- const id = randomUUID();
1850
- db.prepare(
1851
- `INSERT INTO entity_attrs (id, entity_id, attr_key, attr_value, memory_id, valid_from, valid_until, confidence, source)
1852
- VALUES (?, ?, 'tags', ?, ?, ?, NULL, 1.0, 'manual')`
1853
- ).run(id, memoryId, JSON.stringify(arr), memoryId, now);
1854
- }
1855
- // Keep memories.tags (the display/search column) as a mirror of the
1856
- // entity_attrs source of truth, so API rows and keyword search never
1857
- // drift from the directory/tag store (issue #31).
1858
- db.prepare("UPDATE memories SET tags = ?, updated_at = ? WHERE id = ?")
1859
- .run(JSON.stringify(arr), now, memoryId);
1860
- db.exec("RELEASE set_memory_tags");
1861
- } catch (e) {
1862
- db.exec("ROLLBACK TO set_memory_tags");
1863
- db.exec("RELEASE set_memory_tags");
1864
- throw e;
1865
- }
1866
- return arr;
1867
- }
1868
-
1869
- /** Live tags for a memory ([] when none / unknown). */
1870
- function getMemoryTags(memoryId) {
1871
- const row = db.prepare(
1872
- `SELECT attr_value FROM entity_attrs
1873
- WHERE attr_key = 'tags' AND memory_id = ? AND valid_until IS NULL
1874
- ORDER BY valid_from DESC LIMIT 1`
1875
- ).get(memoryId);
1876
- if (!row) return [];
1877
- try {
1878
- const arr = JSON.parse(row.attr_value);
1879
- return Array.isArray(arr) ? arr : [];
1880
- } catch {
1881
- return [];
1882
- }
1883
- }
1884
-
1885
- /** Bulk live-tags lookup for mirror rendering. Returns Map<memoryId, string[]>. */
1886
- function getMemoryTagsMap(ids) {
1887
- const out = new Map();
1888
- const list = (Array.isArray(ids) ? ids : []).filter(Boolean);
1889
- for (let i = 0; i < list.length; i += 100) {
1890
- const chunk = list.slice(i, i + 100);
1891
- const rows = db.prepare(
1892
- `SELECT memory_id, attr_value FROM entity_attrs
1893
- WHERE attr_key = 'tags' AND valid_until IS NULL
1894
- AND memory_id IN (${chunk.map(() => "?").join(",")})`
1895
- ).all(...chunk);
1896
- for (const row of rows) {
1897
- try {
1898
- const arr = JSON.parse(row.attr_value);
1899
- if (Array.isArray(arr) && arr.length) out.set(row.memory_id, arr);
1900
- } catch { /* corrupt row: skip */ }
1901
- }
1902
- }
1903
- return out;
1904
- }
1905
-
1906
- /**
1907
- * Memories carrying a live tags row that contains EVERY requested tag
1908
- * (AND semantics for a multi-tag query). attr_value is a JSON array, so the
1909
- * match uses quoted `"tag"` substrings — `tag:lin` never collides with
1910
- * `linux` because JSON array elements are quote-delimited. Only live rows
1911
- * (valid_until IS NULL) with a memory reference participate; each memory
1912
- * appears once.
1913
- */
1914
- function findMemoriesByTags(tags) {
1915
- const list = normalizeTags(tags);
1916
- if (!list.length) return [];
1917
- const where = list.map(() => `attr_value LIKE ? ESCAPE '\\'`).join(" AND ");
1918
- const params = list.map((t) => `%"${escapeLike(t)}"%`);
1919
- const rows = db.prepare(
1920
- `SELECT DISTINCT memory_id FROM entity_attrs
1921
- WHERE attr_key = 'tags' AND valid_until IS NULL
1922
- AND memory_id IS NOT NULL AND memory_id != ''
1923
- AND (${where})`
1924
- ).all(...params);
1925
- // Same live-memory filter as getDirectory/store.search: forgotten/archived/
1926
- // session-disposed memories are invisible to `tag:` recall.
1927
- const stmt = db.prepare(
1928
- "SELECT * FROM memories WHERE id = ? AND forgotten = 0 AND archived = 0 AND session_disposed_at IS NULL"
1929
- );
1930
- const memories = [];
1931
- for (const { memory_id } of rows) {
1932
- const row = stmt.get(memory_id);
1933
- if (row) memories.push(toRow(row));
1934
- }
1935
- return memories;
1936
- }
1937
-
1938
- /**
1939
- * Directory view (v0.6.3): group live memories by their entity_attrs-backed
1940
- * tag set. A memory carrying N tags appears under all N tag folders; a memory
1941
- * with no live tags lands in `untagged`. Only live rows participate —
1942
- * forgotten, archived and session-disposed memories are excluded. Groups are
1943
- * ordered by tag (locale-aware), group members and untagged follow the
1944
- * canonical memory order (importance DESC, updated_at DESC, id).
1945
- * @returns {{groups: {tag: string, memories: object[]}[], untagged: object[]}}
1946
- */
1947
- function getDirectory() {
1948
- const rows = db.prepare(
1949
- `SELECT * FROM memories
1950
- WHERE forgotten = 0 AND archived = 0 AND session_disposed_at IS NULL
1951
- ORDER BY importance DESC, updated_at DESC, id`
1952
- ).all();
1953
- const memories = rows.map(toRow);
1954
- const tagMap = getMemoryTagsMap(memories.map((m) => m.id));
1955
- const byTag = new Map(); // tag -> memory[]
1956
- const untagged = [];
1957
- for (const m of memories) {
1958
- const tags = tagMap.get(m.id);
1959
- if (!tags || tags.length === 0) {
1960
- untagged.push(m);
1961
- continue;
1962
- }
1963
- for (const tag of tags) {
1964
- if (!byTag.has(tag)) byTag.set(tag, []);
1965
- byTag.get(tag).push(m);
1966
- }
1967
- }
1968
- const groups = [...byTag.entries()]
1969
- .sort((a, b) => a[0].localeCompare(b[0]))
1970
- .map(([tag, ms]) => ({ tag, memories: ms }));
1971
- return { groups, untagged };
1972
- }
1973
-
1974
1628
  /**
1975
1629
  * Record a typed relation between two entities. metadata (optional) is a
1976
- * free-form JSON blob describing the relation. Relations are append-only
1977
- * callers that need idempotency (e.g. wiki-links, via saveWikiLinks) guard
1978
- * with their own existence check plus the partial links_to unique index
1979
- * (idx_relations_wikilink) as a race backstop.
1630
+ * free-form JSON blob describing the relation. Relations are append-only.
1980
1631
  */
1981
1632
  function saveRelation({ from_entity, to_entity, relation_type, memory_id, metadata }) {
1982
1633
  const id = randomUUID();
@@ -1990,56 +1641,7 @@ export function createStore(path) {
1990
1641
  `INSERT INTO entity_relations (id, from_entity, to_entity, relation_type, memory_id, created_at, metadata)
1991
1642
  VALUES (?, ?, ?, ?, ?, ?, ?)`
1992
1643
  ).run(id, from_entity, to_entity, relation_type, memory_id ?? null, now, metaStr);
1993
- return toRelation(db.prepare(
1994
- "SELECT * FROM entity_relations WHERE from_entity = ? AND to_entity = ? AND relation_type = ? LIMIT 1"
1995
- ).get(from_entity, to_entity, relation_type));
1996
- }
1997
-
1998
- /**
1999
- * Record wiki-link relations (v0.6.1). For each target title, resolve the
2000
- * target memory (case-insensitive title match via findByTitle) and write a
2001
- * links_to relation:
2002
- * from_entity = source memory title, to_entity = canonical target memory
2003
- * title, relation_type = 'links_to', memory_id = source memory id.
2004
- * Using the canonical resolved title keeps the graph case-consistent
2005
- * ([[beta]] and [[Beta]] collapse onto the same to_entity), so backlink
2006
- * lookups never fight the way a target was typed.
2007
- * Fail-safe: a target with no matching memory is skipped (never an error).
2008
- * Idempotent: an already-existing triple is a silent no-op (existence check
2009
- * here + the idx_relations_wikilink partial unique index as a race backstop),
2010
- * so `saved` only counts newly written relations. Returns { saved, skipped }.
2011
- */
2012
- function saveWikiLinks({ memoryId, title, targets }) {
2013
- const saved = [];
2014
- const skipped = [];
2015
- const seen = new Set(); // canonical (lowercased) targets already handled
2016
- const existsStmt = db.prepare(
2017
- "SELECT id FROM entity_relations WHERE from_entity = ? AND to_entity = ? AND relation_type = ?"
2018
- );
2019
- const list = Array.isArray(targets)
2020
- ? targets.filter((t) => typeof t === "string" && t.trim())
2021
- : [];
2022
- for (const raw of list) {
2023
- const target = raw.trim();
2024
- const key = target.toLowerCase();
2025
- if (seen.has(key)) continue; // dedupe within a single call (case-insensitive)
2026
- seen.add(key);
2027
- const targetMem = findByTitle(target);
2028
- if (!targetMem) {
2029
- skipped.push(target); // 目标不存在 → 跳过(Fail-safe)
2030
- continue;
2031
- }
2032
- const toEntity = targetMem.title;
2033
- if (existsStmt.get(title, toEntity, "links_to")) continue; // already linked → no-op
2034
- saved.push(saveRelation({
2035
- from_entity: title,
2036
- to_entity: toEntity,
2037
- relation_type: "links_to",
2038
- memory_id: memoryId,
2039
- metadata: { target_memory_id: targetMem.id }
2040
- }));
2041
- }
2042
- return { saved, skipped };
1644
+ return toRelation(db.prepare("SELECT * FROM entity_relations WHERE id = ?").get(id));
2043
1645
  }
2044
1646
 
2045
1647
  /**
@@ -2284,19 +1886,13 @@ export function createStore(path) {
2284
1886
  return {
2285
1887
  db,
2286
1888
  count,
2287
- stats,
2288
1889
  getById,
2289
- listByIdPrefix,
2290
1890
  save,
2291
1891
  update,
2292
- listForEntityExtraction,
2293
- setMemoryMetadata,
2294
1892
  compareAndUpdate,
2295
1893
  remove,
2296
1894
  setForget,
2297
1895
  setArchived,
2298
- listBySession,
2299
- setDisposedBySession,
2300
1896
  touchLastAccess,
2301
1897
  demoteToSummary,
2302
1898
  restoreContent,
@@ -2319,7 +1915,6 @@ export function createStore(path) {
2319
1915
  saveRecallRun,
2320
1916
  getRecallRun,
2321
1917
  listRecallRuns,
2322
- purgeRecallRunsOlderThan,
2323
1918
  saveRecallEval,
2324
1919
  getRecallEval,
2325
1920
  listRecallEvals,
@@ -2347,14 +1942,7 @@ export function createStore(path) {
2347
1942
  getAttrHistory,
2348
1943
  getAttrsByMemory,
2349
1944
  findMemoriesByAttr,
2350
- setMemoryTags,
2351
- getMemoryTags,
2352
- getMemoryTagsMap,
2353
- findMemoriesByTags,
2354
- getDirectory,
2355
1945
  saveRelation,
2356
- saveWikiLinks,
2357
- findByTitle,
2358
1946
  migrateAttrsToMemory,
2359
1947
  getRelations,
2360
1948
  setMirrorState,