@modusensus/dsh-mneme 0.7.9 → 0.7.11

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 (155) hide show
  1. package/README.en.md +368 -0
  2. package/README.md +298 -167
  3. package/{dsh-mneme/cordis.patch.yml → cordis.patch.yml} +1 -0
  4. package/{dsh-mneme/lib → lib}/api.js +58 -284
  5. package/lib/client.js +2086 -0
  6. package/{dsh-mneme/src → lib}/config.js +6 -100
  7. package/{dsh-mneme/lib → lib}/dream/decisions.js +94 -143
  8. package/{dsh-mneme/src → lib}/dream/sleep.js +12 -152
  9. package/{dsh-mneme/lib → lib}/dream.js +22 -168
  10. package/{dsh-mneme/src → lib}/hot-memory.js +0 -7
  11. package/{dsh-mneme/src → lib}/index.js +6 -34
  12. package/{dsh-mneme/lib → lib}/inject.js +11 -53
  13. package/{dsh-mneme/src → lib}/mirror.js +1 -12
  14. package/{dsh-mneme/lib → lib}/quality-filter.js +1 -3
  15. package/{dsh-mneme/lib → lib}/reranker.js +1 -4
  16. package/{dsh-mneme/lib → lib}/service.js +20 -410
  17. package/{dsh-mneme/lib → lib}/settings.js +0 -70
  18. package/{dsh-mneme/lib → lib}/store.js +35 -450
  19. package/{dsh-mneme/lib → lib}/summarize.js +10 -10
  20. package/lib/tools.js +274 -0
  21. package/package.json +41 -19
  22. package/{dsh-mneme/scripts → scripts}/e2e-dsh.js +3 -3
  23. package/{dsh-mneme/scripts → scripts}/sync-lib.js +5 -10
  24. package/{dsh-mneme/src → src}/api.js +58 -284
  25. package/{dsh-mneme/lib → src}/config.js +6 -100
  26. package/{dsh-mneme/src → src}/dream/decisions.js +94 -143
  27. package/{dsh-mneme/lib → src}/dream/sleep.js +12 -152
  28. package/{dsh-mneme/src → src}/dream.js +22 -168
  29. package/{dsh-mneme/lib → src}/hot-memory.js +0 -7
  30. package/{dsh-mneme/lib → src}/index.js +6 -34
  31. package/{dsh-mneme/src → src}/inject.js +11 -53
  32. package/{dsh-mneme/lib → src}/mirror.js +1 -12
  33. package/{dsh-mneme/src → src}/quality-filter.js +1 -3
  34. package/{dsh-mneme/src → src}/reranker.js +1 -4
  35. package/{dsh-mneme/src → src}/service.js +20 -410
  36. package/{dsh-mneme/src → src}/settings.js +0 -70
  37. package/{dsh-mneme/src → src}/store.js +35 -450
  38. package/{dsh-mneme/src → src}/summarize.js +10 -10
  39. package/src/tools.js +274 -0
  40. package/{dsh-mneme/test → test}/api.test.js +1 -156
  41. package/{dsh-mneme/test → test}/client.test.js +22 -207
  42. package/{dsh-mneme/test → test}/config.test.js +0 -21
  43. package/{dsh-mneme/test → test}/dream.test.js +1 -160
  44. package/{dsh-mneme/test → test}/graph-api.test.js +0 -34
  45. package/{dsh-mneme/test → test}/hot-memory.test.js +0 -29
  46. package/test/inject.test.js +120 -0
  47. package/{dsh-mneme/test → test}/llm-audit.test.js +4 -4
  48. package/{dsh-mneme/test → test}/reasoning-effort.test.js +0 -27
  49. package/{dsh-mneme/test → test}/recall-layer.test.js +5 -26
  50. package/{dsh-mneme/test → test}/reranker.test.js +0 -43
  51. package/{dsh-mneme/test → test}/service-search.test.js +0 -25
  52. package/{dsh-mneme/test → test}/service.test.js +0 -106
  53. package/{dsh-mneme/test → test}/settings.test.js +0 -43
  54. package/{dsh-mneme/test → test}/sleep.test.js +4 -171
  55. package/{dsh-mneme/test → test}/store.test.js +0 -76
  56. package/{dsh-mneme/test → test}/summarize.test.js +16 -15
  57. package/test/tools.test.js +265 -0
  58. package/.github/workflows/publish.yml +0 -58
  59. package/.github/workflows/test.yml +0 -32
  60. package/CHANGELOG.md +0 -91
  61. package/CONTRIBUTING.md +0 -291
  62. package/SECURITY.md +0 -718
  63. package/docs/devlog/2026-08-14-dsh-mneme-dev-log.md +0 -247
  64. package/docs/devlog/2026-08-15-dsh-mneme-audit-stress-dev-log.md +0 -145
  65. package/docs/devlog/2026-08-15-dsh-mneme-pipeline-dev-log.md +0 -56
  66. package/docs/devlog/2026-08-15-dsh-mneme-reflection-dev-log.md +0 -77
  67. package/docs/devlog/2026-08-15-dsh-mneme-review-fixes-dev-log.md +0 -64
  68. package/docs/devlog/2026-08-15-dsh-mneme-semantic-dev-log.md +0 -90
  69. package/dsh-mneme/CHANGELOG.md +0 -406
  70. package/dsh-mneme/LICENSE +0 -21
  71. package/dsh-mneme/README.md +0 -498
  72. package/dsh-mneme/docs/AGENT_MEMORY_RESEARCH.md +0 -183
  73. package/dsh-mneme/docs/ENTITIES.md +0 -245
  74. package/dsh-mneme/docs/LOCAL_MODEL.md +0 -141
  75. package/dsh-mneme/docs/MIGRATION.md +0 -127
  76. package/dsh-mneme/docs/SEMANTIC.md +0 -256
  77. package/dsh-mneme/docs/SLEEP.md +0 -163
  78. package/dsh-mneme/lib/client.js +0 -2050
  79. package/dsh-mneme/lib/dream/tag-extractor.js +0 -156
  80. package/dsh-mneme/lib/heat.js +0 -136
  81. package/dsh-mneme/lib/parser/tag.js +0 -59
  82. package/dsh-mneme/lib/parser/wiki-link.js +0 -38
  83. package/dsh-mneme/lib/search/tag-boost.js +0 -61
  84. package/dsh-mneme/lib/tools.js +0 -458
  85. package/dsh-mneme/package-lock.json +0 -1936
  86. package/dsh-mneme/package.json +0 -83
  87. package/dsh-mneme/scripts/check-sync.js +0 -42
  88. package/dsh-mneme/src/client.js +0 -2050
  89. package/dsh-mneme/src/dream/tag-extractor.js +0 -156
  90. package/dsh-mneme/src/heat.js +0 -136
  91. package/dsh-mneme/src/parser/tag.js +0 -59
  92. package/dsh-mneme/src/parser/wiki-link.js +0 -38
  93. package/dsh-mneme/src/search/tag-boost.js +0 -61
  94. package/dsh-mneme/src/tools.js +0 -458
  95. package/dsh-mneme/test/boundary-v0625.test.js +0 -82
  96. package/dsh-mneme/test/directory.test.js +0 -134
  97. package/dsh-mneme/test/heat.test.js +0 -148
  98. package/dsh-mneme/test/inject.test.js +0 -206
  99. package/dsh-mneme/test/layered-types-stats.test.js +0 -144
  100. package/dsh-mneme/test/lib-smoke.test.js +0 -109
  101. package/dsh-mneme/test/normalize-decisions.test.js +0 -120
  102. package/dsh-mneme/test/provenance.test.js +0 -103
  103. package/dsh-mneme/test/recall-runs.test.js +0 -93
  104. package/dsh-mneme/test/sleep-heat.test.js +0 -112
  105. package/dsh-mneme/test/tag-boost.test.js +0 -125
  106. package/dsh-mneme/test/tag.test.js +0 -426
  107. package/dsh-mneme/test/tools.test.js +0 -674
  108. package/dsh-mneme/test/updated-at-semantics.test.js +0 -113
  109. package/dsh-mneme/test/wiki-link.test.js +0 -332
  110. package//346/250/252/345/271/205.png +0 -0
  111. /package/{dsh-mneme/lib → lib}/commands.js +0 -0
  112. /package/{dsh-mneme/lib → lib}/dream/clustering.js +0 -0
  113. /package/{dsh-mneme/lib → lib}/embedding.js +0 -0
  114. /package/{dsh-mneme/lib → lib}/entities/extractor.js +0 -0
  115. /package/{dsh-mneme/lib → lib}/local-embedder.js +0 -0
  116. /package/{dsh-mneme/lib → lib}/search/adaptive.js +0 -0
  117. /package/{dsh-mneme/lib → lib}/search/bm25.js +0 -0
  118. /package/{dsh-mneme/lib → lib}/vector-index.js +0 -0
  119. /package/{dsh-mneme/scripts → scripts}/benchmark-embed.js +0 -0
  120. /package/{dsh-mneme/scripts → scripts}/benchmark-recall.js +0 -0
  121. /package/{dsh-mneme/scripts → scripts}/benchmark-rerank.js +0 -0
  122. /package/{dsh-mneme/scripts → scripts}/stress-dsh.js +0 -0
  123. /package/{dsh-mneme/src → src}/commands.js +0 -0
  124. /package/{dsh-mneme/src → src}/dream/clustering.js +0 -0
  125. /package/{dsh-mneme/src → src}/embedding.js +0 -0
  126. /package/{dsh-mneme/src → src}/entities/extractor.js +0 -0
  127. /package/{dsh-mneme/src → src}/local-embedder.js +0 -0
  128. /package/{dsh-mneme/src → src}/search/adaptive.js +0 -0
  129. /package/{dsh-mneme/src → src}/search/bm25.js +0 -0
  130. /package/{dsh-mneme/src → src}/vector-index.js +0 -0
  131. /package/{dsh-mneme/test → test}/audit.test.js +0 -0
  132. /package/{dsh-mneme/test → test}/benchmark.test.js +0 -0
  133. /package/{dsh-mneme/test → test}/clustering.test.js +0 -0
  134. /package/{dsh-mneme/test → test}/commands.test.js +0 -0
  135. /package/{dsh-mneme/test → test}/conflict-freeze.test.js +0 -0
  136. /package/{dsh-mneme/test → test}/entities.test.js +0 -0
  137. /package/{dsh-mneme/test → test}/epistemic.test.js +0 -0
  138. /package/{dsh-mneme/test → test}/fnew-0112.test.js +0 -0
  139. /package/{dsh-mneme/test → test}/fnew-03.test.js +0 -0
  140. /package/{dsh-mneme/test → test}/helpers/dream-mock.js +0 -0
  141. /package/{dsh-mneme/test → test}/local-embedder.test.js +0 -0
  142. /package/{dsh-mneme/test → test}/mirror-dirty.test.js +0 -0
  143. /package/{dsh-mneme/test → test}/mirror-edit-digest.test.js +0 -0
  144. /package/{dsh-mneme/test → test}/mirror-generation.test.js +0 -0
  145. /package/{dsh-mneme/test → test}/mirror.test.js +0 -0
  146. /package/{dsh-mneme/test → test}/peer-blockers.test.js +0 -0
  147. /package/{dsh-mneme/test → test}/policy-epoch.test.js +0 -0
  148. /package/{dsh-mneme/test → test}/quality-filter.test.js +0 -0
  149. /package/{dsh-mneme/test → test}/recall-evals.test.js +0 -0
  150. /package/{dsh-mneme/test → test}/receipt-chain.test.js +0 -0
  151. /package/{dsh-mneme/test → test}/reflection.test.js +0 -0
  152. /package/{dsh-mneme/test → test}/search-fusion.test.js +0 -0
  153. /package/{dsh-mneme/test → test}/semantic.test.js +0 -0
  154. /package/{dsh-mneme/test → test}/stress.test.js +0 -0
  155. /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,7 @@ 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
+ const TYPES = new Set(["preference", "project", "decision", "history", "summary", "pattern"]);
247
243
 
248
244
  // Epistemic status: what kind of evidence a memory rests on. Defaults to
249
245
  // 'subjective' so legacy rows (and rows without any signal) stay compatible.
@@ -315,14 +311,6 @@ function parseTags(raw) {
315
311
 
316
312
  function toRow(row) {
317
313
  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
314
  return {
327
315
  id: row.id,
328
316
  type: row.type,
@@ -332,13 +320,10 @@ function toRow(row) {
332
320
  importance: row.importance,
333
321
  forgotten: row.forgotten === 1,
334
322
  archived: row.archived === 1,
335
- session_disposed_at: row.session_disposed_at ?? undefined,
336
323
  source: row.source ?? undefined,
337
- session_id: row.session_id ?? undefined,
338
324
  content_history: parseJsonArray(row.content_history),
339
325
  quality_score: row.quality_score !== null && row.quality_score !== undefined ? Number(row.quality_score) : undefined,
340
326
  epistemic_status: row.epistemic_status ?? "subjective",
341
- metadata,
342
327
  created_at: row.created_at,
343
328
  updated_at: row.updated_at,
344
329
  last_accessed_at: row.last_accessed_at ?? undefined,
@@ -563,17 +548,6 @@ export function createStore(path) {
563
548
  db.exec("PRAGMA journal_mode = WAL;");
564
549
  db.exec(SCHEMA);
565
550
 
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
551
  // Schema migrations for legacy databases (idempotent). Each ADD COLUMN is
578
552
  // also race-safe: two concurrently-opening processes can both pass the
579
553
  // PRAGMA table_info check before either ALTERs, so the ALTER itself is
@@ -591,21 +565,12 @@ export function createStore(path) {
591
565
  };
592
566
 
593
567
  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
568
  addColumn("memories", "embedding", "ALTER TABLE memories ADD COLUMN embedding TEXT");
596
569
  addColumn("memories", "last_accessed_at", "ALTER TABLE memories ADD COLUMN last_accessed_at TEXT");
597
570
  addColumn("memories", "_full_content", "ALTER TABLE memories ADD COLUMN _full_content TEXT");
598
571
  addColumn("memories", "epistemic_status", "ALTER TABLE memories ADD COLUMN epistemic_status TEXT NOT NULL DEFAULT 'subjective'");
599
572
  addColumn("memories", "content_history", "ALTER TABLE memories ADD COLUMN content_history TEXT");
600
573
  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
574
 
610
575
  // Legacy dream_runs without policy_epoch → backfill with the default epoch.
611
576
  addColumn("dream_runs", "policy_epoch", "ALTER TABLE dream_runs ADD COLUMN policy_epoch INTEGER NOT NULL DEFAULT 0");
@@ -650,98 +615,37 @@ export function createStore(path) {
650
615
  return ts;
651
616
  }
652
617
 
653
- function count(type, { includeForgotten = false, includeArchived = false, includeDisposed = false } = {}) {
618
+ function count(type, { minImportance = null, source = null, includeForgotten = false, includeArchived = false } = {}) {
654
619
  const clauses = [];
655
620
  const params = [];
656
621
  if (type !== undefined) {
657
622
  clauses.push("type = ?");
658
623
  params.push(type);
659
624
  }
625
+ // Same filters as list() so a paged caller's total matches its rows.
626
+ if (minImportance != null) {
627
+ clauses.push("importance >= ?");
628
+ params.push(minImportance);
629
+ }
630
+ if (source != null) {
631
+ clauses.push("source = ?");
632
+ params.push(source);
633
+ }
660
634
  if (!includeForgotten) {
661
635
  clauses.push("forgotten = 0");
662
636
  }
663
637
  if (!includeArchived) {
664
638
  clauses.push("archived = 0");
665
639
  }
666
- if (!includeDisposed) {
667
- clauses.push("session_disposed_at IS NULL");
668
- }
669
640
  const where = clauses.length ? `WHERE ${clauses.join(" AND ")}` : "";
670
641
  return db.prepare(`SELECT count(*) AS c FROM memories ${where}`).get(...params).c;
671
642
  }
672
643
 
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
644
  function getById(id) {
707
645
  const row = db.prepare("SELECT * FROM memories WHERE id = ?").get(id);
708
646
  return toRow(row);
709
647
  }
710
648
 
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
649
  function save(memory) {
746
650
  const id = memory.id ?? randomUUID();
747
651
  const type = memory.type;
@@ -762,8 +666,8 @@ export function createStore(path) {
762
666
  : inferEpistemicStatus(memory);
763
667
  runAtomically(() => {
764
668
  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, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
669
+ `INSERT INTO memories (id, type, title, content, tags, importance, forgotten, archived, source, content_history, quality_score, embedding, epistemic_status, created_at, updated_at)
670
+ VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?, ?, ?, ?, ?, ?, ?)`
767
671
  ).run(
768
672
  id,
769
673
  type,
@@ -773,7 +677,6 @@ export function createStore(path) {
773
677
  importance,
774
678
  memory.archived ? 1 : 0,
775
679
  memory.source ?? null,
776
- memory.session_id ?? null,
777
680
  JSON.stringify(memory.content_history ?? []),
778
681
  Number.isFinite(memory.quality_score) ? memory.quality_score : null,
779
682
  embedding,
@@ -790,9 +693,6 @@ export function createStore(path) {
790
693
  return getById(id);
791
694
  }
792
695
 
793
- // v0.7.0 语义:updated_at 只记录内容变更,绝不当访问(与 last_accessed_at
794
- // 正交,sleep/heat 永不 fallback 到它)。合并/更新刷 updated_at 不应
795
- // 重置热度时钟 —— 防 autoDream 合并动作伪装成"刚被召回"。
796
696
  function update(id, patch) {
797
697
  const existing = getById(id);
798
698
  if (!existing) throw new Error(`memory not found: ${id}`);
@@ -845,50 +745,6 @@ export function createStore(path) {
845
745
  });
846
746
  }
847
747
 
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
748
  /**
893
749
  * Atomic compare-and-set update: applies `patch` only when the row still
894
750
  * carries `expectedUpdatedAt` (the version token read by the caller). Returns
@@ -969,45 +825,6 @@ export function createStore(path) {
969
825
  return getById(id);
970
826
  }
971
827
 
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
828
  // --- sleep-mode storage support (v0.4.0) ---------------------------------
1012
829
  // touchLastAccess stamps the read time on recall/inject paths. It deliberately
1013
830
  // does NOT bump the mirror generation: reads must not mark the mirror dirty.
@@ -1064,7 +881,6 @@ export function createStore(path) {
1064
881
  const rows = db.prepare(
1065
882
  `SELECT * FROM memories
1066
883
  WHERE forgotten = 0 AND archived = 0
1067
- AND session_disposed_at IS NULL
1068
884
  AND (last_accessed_at IS NULL OR last_accessed_at < ?)
1069
885
  ORDER BY COALESCE(last_accessed_at, created_at) ASC, id
1070
886
  LIMIT ?`
@@ -1072,26 +888,37 @@ export function createStore(path) {
1072
888
  return rows.map(toRow);
1073
889
  }
1074
890
 
1075
- function list({ type, limit = 50, offset = 0, includeForgotten = false, includeArchived = false, includeDisposed = false } = {}) {
891
+ function list({ type, limit = 50, offset = 0, order = "importance", includeForgotten = false, includeArchived = false, minImportance = null, source = null } = {}) {
1076
892
  const clauses = [];
1077
893
  const params = [];
1078
894
  if (type) {
1079
895
  clauses.push("type = ?");
1080
896
  params.push(type);
1081
897
  }
898
+ // Optional server-side filters: importance floor and exact source match.
899
+ // Both stay out of the query when unset so existing callers are unaffected.
900
+ if (minImportance != null) {
901
+ clauses.push("importance >= ?");
902
+ params.push(minImportance);
903
+ }
904
+ if (source) {
905
+ clauses.push("source = ?");
906
+ params.push(source);
907
+ }
1082
908
  if (!includeForgotten) {
1083
909
  clauses.push("forgotten = 0");
1084
910
  }
1085
911
  if (!includeArchived) {
1086
912
  clauses.push("archived = 0");
1087
913
  }
1088
- if (!includeDisposed) {
1089
- clauses.push("session_disposed_at IS NULL");
1090
- }
1091
914
  const { limit: lim, offset: off } = sanitizePage(limit, offset, 50);
1092
915
  const where = clauses.length ? `WHERE ${clauses.join(" AND ")}` : "";
916
+ // "chrono" is pure newest-first — the stable order paged browsing (month
917
+ // tree, infinite scroll) needs; importance ordering would interleave
918
+ // months across pages.
919
+ const orderBy = order === "chrono" ? "updated_at DESC, id DESC" : "importance DESC, updated_at DESC, id";
1093
920
  const rows = db.prepare(
1094
- `SELECT * FROM memories ${where} ORDER BY importance DESC, updated_at DESC, id LIMIT ? OFFSET ?`
921
+ `SELECT * FROM memories ${where} ORDER BY ${orderBy} LIMIT ? OFFSET ?`
1095
922
  ).all(...params, lim, off);
1096
923
  return rows.map(toRow);
1097
924
  }
@@ -1145,7 +972,7 @@ export function createStore(path) {
1145
972
  ).all(limit);
1146
973
  }
1147
974
 
1148
- function search(query, { limit = 20, includeArchived = false, includeDisposed = false } = {}) {
975
+ function search(query, { limit = 20, includeArchived = false } = {}) {
1149
976
  const q = String(query).trim();
1150
977
  if (!q) return [];
1151
978
  // Plain LIKE substring scan over title/content/tags (wildcards escaped so
@@ -1154,10 +981,9 @@ export function createStore(path) {
1154
981
  const like = `%${escapeLike(q)}%`;
1155
982
  const { limit: lim } = sanitizePage(limit, 0, 20);
1156
983
  const archivedFilter = includeArchived ? "" : "archived = 0 AND ";
1157
- const disposedFilter = includeDisposed ? "" : "session_disposed_at IS NULL AND ";
1158
984
  const rows = db.prepare(
1159
985
  `SELECT * FROM memories
1160
- WHERE ${archivedFilter}${disposedFilter}forgotten = 0 AND (title LIKE ? ESCAPE '\\' OR content LIKE ? ESCAPE '\\' OR tags LIKE ? ESCAPE '\\')
986
+ WHERE ${archivedFilter}forgotten = 0 AND (title LIKE ? ESCAPE '\\' OR content LIKE ? ESCAPE '\\' OR tags LIKE ? ESCAPE '\\')
1161
987
  ORDER BY
1162
988
  CASE WHEN title LIKE ? ESCAPE '\\' THEN 0 ELSE 1 END,
1163
989
  importance DESC,
@@ -1188,13 +1014,12 @@ export function createStore(path) {
1188
1014
  * Brute-force cosine similarity over embedded rows. Returns rows decorated
1189
1015
  * with a `score` (0..1). Only rows with a stored embedding participate.
1190
1016
  */
1191
- function searchVector(vector, { limit = 20, includeArchived = false, includeDisposed = false, threshold = 0 } = {}) {
1017
+ function searchVector(vector, { limit = 20, includeArchived = false, threshold = 0 } = {}) {
1192
1018
  if (!Array.isArray(vector) || !vector.length) return [];
1193
1019
  const archivedFilter = includeArchived ? "" : "archived = 0 AND ";
1194
- const disposedFilter = includeDisposed ? "" : "session_disposed_at IS NULL AND ";
1195
1020
  const rows = db.prepare(
1196
1021
  `SELECT * FROM memories
1197
- WHERE ${archivedFilter}${disposedFilter}forgotten = 0 AND embedding IS NOT NULL AND embedding != ''`
1022
+ WHERE ${archivedFilter}forgotten = 0 AND embedding IS NOT NULL AND embedding != ''`
1198
1023
  ).all();
1199
1024
  const scored = [];
1200
1025
  for (const row of rows) {
@@ -1400,19 +1225,6 @@ export function createStore(path) {
1400
1225
  return rows.map(toRecallRun);
1401
1226
  }
1402
1227
 
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
1228
  // --- recall evaluation trail (方案 B: separate from the production audit) -
1417
1229
 
1418
1230
  /**
@@ -1810,173 +1622,9 @@ export function createStore(path) {
1810
1622
  return memories;
1811
1623
  }
1812
1624
 
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
1625
  /**
1975
1626
  * 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.
1627
+ * free-form JSON blob describing the relation. Relations are append-only.
1980
1628
  */
1981
1629
  function saveRelation({ from_entity, to_entity, relation_type, memory_id, metadata }) {
1982
1630
  const id = randomUUID();
@@ -1990,56 +1638,7 @@ export function createStore(path) {
1990
1638
  `INSERT INTO entity_relations (id, from_entity, to_entity, relation_type, memory_id, created_at, metadata)
1991
1639
  VALUES (?, ?, ?, ?, ?, ?, ?)`
1992
1640
  ).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 };
1641
+ return toRelation(db.prepare("SELECT * FROM entity_relations WHERE id = ?").get(id));
2043
1642
  }
2044
1643
 
2045
1644
  /**
@@ -2284,19 +1883,13 @@ export function createStore(path) {
2284
1883
  return {
2285
1884
  db,
2286
1885
  count,
2287
- stats,
2288
1886
  getById,
2289
- listByIdPrefix,
2290
1887
  save,
2291
1888
  update,
2292
- listForEntityExtraction,
2293
- setMemoryMetadata,
2294
1889
  compareAndUpdate,
2295
1890
  remove,
2296
1891
  setForget,
2297
1892
  setArchived,
2298
- listBySession,
2299
- setDisposedBySession,
2300
1893
  touchLastAccess,
2301
1894
  demoteToSummary,
2302
1895
  restoreContent,
@@ -2319,7 +1912,6 @@ export function createStore(path) {
2319
1912
  saveRecallRun,
2320
1913
  getRecallRun,
2321
1914
  listRecallRuns,
2322
- purgeRecallRunsOlderThan,
2323
1915
  saveRecallEval,
2324
1916
  getRecallEval,
2325
1917
  listRecallEvals,
@@ -2347,14 +1939,7 @@ export function createStore(path) {
2347
1939
  getAttrHistory,
2348
1940
  getAttrsByMemory,
2349
1941
  findMemoriesByAttr,
2350
- setMemoryTags,
2351
- getMemoryTags,
2352
- getMemoryTagsMap,
2353
- findMemoriesByTags,
2354
- getDirectory,
2355
1942
  saveRelation,
2356
- saveWikiLinks,
2357
- findByTitle,
2358
1943
  migrateAttrsToMemory,
2359
1944
  getRelations,
2360
1945
  setMirrorState,