recallnest 1.4.1 → 2.2.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.
Files changed (233) hide show
  1. package/.env +4 -0
  2. package/CLAUDE.md +7 -3
  3. package/README.md +202 -130
  4. package/README_CN.md +263 -190
  5. package/bin/recallnest.mjs +62 -0
  6. package/config.json +55 -0
  7. package/daily-recall.sh +23 -0
  8. package/integrations/setup-all.sh +31 -0
  9. package/logs/daily-recall.error.log +0 -0
  10. package/logs/daily-recall.log +30 -0
  11. package/logs/ingest-2026-04-11.log +110 -0
  12. package/logs/ingest-2026-04-12.log +90 -0
  13. package/logs/launchagent-stderr.log +0 -0
  14. package/logs/launchagent-stdout.log +0 -0
  15. package/package.json +10 -3
  16. package/src/access-tracker.ts +3 -2
  17. package/src/activity-counter.ts +109 -0
  18. package/src/adaptive-retrieval.ts +6 -5
  19. package/src/admission-control.ts +163 -0
  20. package/src/anchor-generator.ts +80 -0
  21. package/src/api-server.ts +10 -0
  22. package/src/audit-log.ts +122 -0
  23. package/src/auto-gc.ts +98 -29
  24. package/src/auto-recall.ts +33 -4
  25. package/src/capture-engine.ts +372 -23
  26. package/src/cascade-forget.ts +3 -2
  27. package/src/cli.ts +107 -5
  28. package/src/compat.ts +22 -0
  29. package/src/confidence-tracker.ts +210 -47
  30. package/src/conflict-store.ts +12 -3
  31. package/src/consolidation-engine.ts +296 -8
  32. package/src/context-collapse-renderer.ts +222 -0
  33. package/src/context-composer-stable-selection.ts +5 -1
  34. package/src/context-composer.ts +239 -4
  35. package/src/context-reconstructor.ts +404 -0
  36. package/src/conversation-importer.ts +441 -0
  37. package/src/data-checkup.ts +232 -0
  38. package/src/decay-engine.ts +84 -0
  39. package/src/distill-lock.ts +122 -0
  40. package/src/doctor.ts +8 -10
  41. package/src/dream-pipeline.ts +227 -0
  42. package/src/emotion-detector.ts +90 -0
  43. package/src/eval.ts +3 -2
  44. package/src/forget-engine.ts +269 -0
  45. package/src/frequency-tracker.ts +175 -0
  46. package/src/graph-export.ts +644 -0
  47. package/src/ingest.ts +36 -6
  48. package/src/interference-detector.ts +187 -0
  49. package/src/kg-extractor.ts +15 -24
  50. package/src/kg-store.ts +2 -1
  51. package/src/language-hook.ts +67 -0
  52. package/src/llm-client.ts +289 -6
  53. package/src/mcp-server.ts +707 -80
  54. package/src/memory-assets.ts +22 -5
  55. package/src/memory-evolution.ts +365 -0
  56. package/src/memory-lint.ts +429 -0
  57. package/src/memory-output.ts +69 -0
  58. package/src/memory-schema.ts +74 -2
  59. package/src/narrative-schema.ts +79 -0
  60. package/src/narrative-tagger.ts +164 -0
  61. package/src/pii-detector.ts +98 -0
  62. package/src/post-retrieval-filter.ts +76 -0
  63. package/src/prediction-engine.ts +253 -0
  64. package/src/preference-slots.ts +65 -2
  65. package/src/prospective-memory.ts +215 -0
  66. package/src/query-expander.ts +67 -0
  67. package/src/recall-governor.ts +183 -0
  68. package/src/retention-policy.ts +90 -0
  69. package/src/retriever.ts +828 -55
  70. package/src/rif.ts +34 -8
  71. package/src/runtime-config.ts +23 -8
  72. package/src/scope-inventory.ts +4 -3
  73. package/src/scope-policy.ts +4 -0
  74. package/src/session-distiller.ts +307 -0
  75. package/src/session-output.ts +40 -0
  76. package/src/session-schema.ts +37 -0
  77. package/src/session-store.ts +7 -2
  78. package/src/skill-engine.ts +208 -0
  79. package/src/skill-promotion.ts +293 -0
  80. package/src/skill-schema.ts +39 -0
  81. package/src/store.ts +96 -10
  82. package/src/topic-tag.ts +66 -0
  83. package/src/tracker.ts +3 -1
  84. package/src/ui-server.ts +80 -8
  85. package/src/workflow-observation-scope-review.ts +2 -1
  86. package/src/workflow-observation-store.ts +2 -1
  87. package/src/write-verifier.ts +126 -0
  88. package/.claude-plugin/CLAUDE.md +0 -29
  89. package/.claude-plugin/marketplace.json +0 -18
  90. package/.github/workflows/ci.yml +0 -28
  91. package/.mcp.json +0 -9
  92. package/CHANGELOG.md +0 -51
  93. package/DEMO.md +0 -94
  94. package/EVAL.md +0 -89
  95. package/FAILURES.md +0 -75
  96. package/OPERATIONS.md +0 -59
  97. package/ROADMAP.md +0 -198
  98. package/assets/recallnest-workbench.png +0 -0
  99. package/assets/ui/app.js +0 -807
  100. package/assets/ui/index.html +0 -154
  101. package/assets/ui/styles.css +0 -605
  102. package/assets/wechat-qr.jpg +0 -0
  103. package/daily-reflections/2026-03-05.md +0 -15
  104. package/daily-reflections/2026-03-06.md +0 -15
  105. package/daily-reflections/2026-03-07.md +0 -7
  106. package/daily-reflections/2026-03-08.md +0 -34
  107. package/daily-reflections/2026-03-09.md +0 -35
  108. package/daily-reflections/2026-03-10.md +0 -36
  109. package/daily-reflections/2026-03-11.md +0 -35
  110. package/daily-reflections/2026-03-12.md +0 -36
  111. package/daily-reflections/2026-03-13.md +0 -35
  112. package/daily-reflections/2026-03-14.md +0 -35
  113. package/daily-reflections/2026-03-15.md +0 -35
  114. package/daily-reflections/2026-03-16.md +0 -41
  115. package/daily-reflections/2026-03-17.md +0 -10
  116. package/daily-reflections/2026-03-18.md +0 -10
  117. package/daily-reflections/2026-03-19.md +0 -10
  118. package/daily-reflections/2026-03-20.md +0 -10
  119. package/daily-reflections/2026-03-21.md +0 -10
  120. package/daily-reflections/2026-03-22.md +0 -10
  121. package/daily-reflections/2026-03-23.md +0 -10
  122. package/daily-reflections/2026-03-24.md +0 -10
  123. package/daily-reflections/2026-03-25.md +0 -32
  124. package/daily-reflections/2026-03-26.md +0 -37
  125. package/daily-reflections/2026-03-27.md +0 -35
  126. package/daily-reflections/2026-03-28.md +0 -35
  127. package/daily-reflections/2026-03-29.md +0 -35
  128. package/daily-reflections/2026-03-30.md +0 -36
  129. package/daily-reflections/2026-03-31.md +0 -35
  130. package/daily-reflections/2026-04-01.md +0 -35
  131. package/docs/api-reference.md +0 -1291
  132. package/docs/architecture.md +0 -168
  133. package/docs/memory-boundary-contract.md +0 -226
  134. package/docs/memory-categories.md +0 -119
  135. package/docs/pipeline-audit-ultramemory.md +0 -177
  136. package/docs/plans/2026-03-08-query-expansion.md +0 -270
  137. package/docs/plans/2026-03-16-continuity-layer.md +0 -196
  138. package/docs/plans/mcp-tiering-plan.md +0 -126
  139. package/docs/research/recall-quality-audit-2026-03-22.md +0 -153
  140. package/docs/self-evolution.md +0 -198
  141. package/docs/superpowers/plans/2026-03-19-ingest-pipeline-upgrade.md +0 -782
  142. package/docs/superpowers/specs/2026-03-19-ingest-pipeline-upgrade-design.md +0 -77
  143. package/docs/superpowers/specs/2026-03-23-pull-claude-desktop-design.md +0 -95
  144. package/eval/continuity/results-2026-03-16.md +0 -117
  145. package/eval/continuity/results-2026-03-19.md +0 -453
  146. package/eval/continuity/runbook.md +0 -110
  147. package/eval/continuity/seed-cases.md +0 -54
  148. package/eval/reports/2026-03-06-abstract-query-baseline.md +0 -96
  149. package/eval/reports/2026-03-06-aws-ssh-baseline.md +0 -82
  150. package/eval/reports/2026-03-06-baseline.md +0 -82
  151. package/eval/reports/query-expansion.md +0 -138
  152. package/lm +0 -19
  153. package/plan-v1.2-distributable.md +0 -105
  154. package/scripts/archive-distilled-junk.ts +0 -116
  155. package/scripts/backfill-l0l1l2.ts +0 -98
  156. package/scripts/blind-scan.ts +0 -588
  157. package/scripts/cleanup-step1-exact-dups.ts +0 -117
  158. package/scripts/cleanup-step2-template-dups.ts +0 -113
  159. package/scripts/cleanup-step3-stale-content.ts +0 -149
  160. package/scripts/cleanup-step4-orphan-scopes.ts +0 -131
  161. package/scripts/delete-distilled-junk.ts +0 -88
  162. package/scripts/distill-facts.ts +0 -305
  163. package/scripts/health-check.ts +0 -343
  164. package/scripts/incremental-ingest.sh +0 -71
  165. package/scripts/mlp-health-check.ts +0 -382
  166. package/scripts/pull-claude-desktop.ts +0 -515
  167. package/scripts/re-ingest-cc.sh +0 -66
  168. package/scripts/rebalance-memory-health.ts +0 -181
  169. package/scripts/run-desktop-ingest.sh +0 -11
  170. package/scripts/start-server.sh +0 -12
  171. package/scripts/viz-dashboard.html +0 -423
  172. package/scripts/viz-export.ts +0 -703
  173. package/src/__tests__/access-tracker-novelty.test.ts +0 -228
  174. package/src/__tests__/auto-consolidation.test.ts +0 -168
  175. package/src/__tests__/auto-recall.test.ts +0 -131
  176. package/src/__tests__/batch-dedup.test.ts +0 -65
  177. package/src/__tests__/capture-engine.test.ts +0 -1142
  178. package/src/__tests__/capture-heuristic.test.ts +0 -171
  179. package/src/__tests__/cascade-forget.test.ts +0 -210
  180. package/src/__tests__/category-min-scores.test.ts +0 -33
  181. package/src/__tests__/confidence-tracker.test.ts +0 -196
  182. package/src/__tests__/conflict-advisor.test.ts +0 -206
  183. package/src/__tests__/conflict-escalation.test.ts +0 -149
  184. package/src/__tests__/conflict-lifecycle.test.ts +0 -83
  185. package/src/__tests__/conflict-output.test.ts +0 -85
  186. package/src/__tests__/conflict-store.test.ts +0 -374
  187. package/src/__tests__/consolidation-engine.test.ts +0 -181
  188. package/src/__tests__/context-composer-pins.test.ts +0 -64
  189. package/src/__tests__/context-composer-task-selection.test.ts +0 -24
  190. package/src/__tests__/context-renderer.test.ts +0 -91
  191. package/src/__tests__/continuity-eval.test.ts +0 -275
  192. package/src/__tests__/continuity-schema.test.ts +0 -97
  193. package/src/__tests__/core-recall-eval.test.ts +0 -235
  194. package/src/__tests__/core-summary.test.ts +0 -109
  195. package/src/__tests__/dedup-actions.test.ts +0 -58
  196. package/src/__tests__/deterministic-id.test.ts +0 -39
  197. package/src/__tests__/doctor.test.ts +0 -303
  198. package/src/__tests__/drill-down.test.ts +0 -108
  199. package/src/__tests__/embedder.test.ts +0 -122
  200. package/src/__tests__/entity-resolver.test.ts +0 -58
  201. package/src/__tests__/event-segmenter.test.ts +0 -153
  202. package/src/__tests__/hotness-blend.test.ts +0 -76
  203. package/src/__tests__/ingest.test.ts +0 -360
  204. package/src/__tests__/kg-extractor.test.ts +0 -184
  205. package/src/__tests__/kg-store.test.ts +0 -198
  206. package/src/__tests__/llm-consolidation.test.ts +0 -226
  207. package/src/__tests__/memory-boundaries.test.ts +0 -237
  208. package/src/__tests__/memory-health-rebalance.test.ts +0 -144
  209. package/src/__tests__/memory-output.test.ts +0 -215
  210. package/src/__tests__/multi-vector.test.ts +0 -242
  211. package/src/__tests__/ppr-traversal.test.ts +0 -256
  212. package/src/__tests__/preference-guard-smoke.test.ts +0 -211
  213. package/src/__tests__/preference-matcher.test.ts +0 -230
  214. package/src/__tests__/preference-slots.test.ts +0 -106
  215. package/src/__tests__/prospective-memory.test.ts +0 -283
  216. package/src/__tests__/query-entity-detector.test.ts +0 -171
  217. package/src/__tests__/query-expander.test.ts +0 -34
  218. package/src/__tests__/recall-mode.test.ts +0 -64
  219. package/src/__tests__/result-synthesizer.test.ts +0 -198
  220. package/src/__tests__/resume-context.test.ts +0 -4743
  221. package/src/__tests__/retrieval-trace.test.ts +0 -213
  222. package/src/__tests__/retriever-boundary-weight.test.ts +0 -136
  223. package/src/__tests__/retroactive-boost.test.ts +0 -223
  224. package/src/__tests__/scope-inventory.test.ts +0 -200
  225. package/src/__tests__/scope-policy.test.ts +0 -69
  226. package/src/__tests__/session-checkpoint.test.ts +0 -259
  227. package/src/__tests__/session-gc.test.ts +0 -215
  228. package/src/__tests__/store-scope-audit.test.ts +0 -81
  229. package/src/__tests__/temporal-parser.test.ts +0 -241
  230. package/src/__tests__/term-registry.test.ts +0 -121
  231. package/src/__tests__/tool-output-compressor.test.ts +0 -133
  232. package/src/__tests__/version-manager.test.ts +0 -213
  233. package/src/__tests__/workflow-observation.test.ts +0 -400
@@ -2,6 +2,7 @@ import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, statSync,
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { basename, extname, join, resolve } from "node:path";
4
4
 
5
+ import { metaDir } from "./compat.js";
5
6
  import type { MemoryEntry } from "./store.js";
6
7
  import type { RetrievalResult } from "./retriever.js";
7
8
  import type { RetrievalProfileName } from "./retrieval-profiles.js";
@@ -71,7 +72,7 @@ export interface DirtyBriefAssetRecord extends BriefAsset {
71
72
  scope: string;
72
73
  }
73
74
 
74
- const DATA_DIR = resolve(import.meta.dir, "../data");
75
+ const DATA_DIR = resolve(metaDir(import.meta), "../data");
75
76
  const PINS_DIR = join(DATA_DIR, "pins");
76
77
  const ASSETS_DIR = join(DATA_DIR, "assets");
77
78
  const ARCHIVE_DIR = join(DATA_DIR, "archive", "dirty-briefs");
@@ -165,7 +166,11 @@ export function buildPinAsset(result: RetrievalResult, options: {
165
166
  export function savePinAsset(asset: PinAsset): string {
166
167
  const dir = getPinsDir();
167
168
  const path = join(dir, `${asset.id}.json`);
168
- writeFileSync(path, JSON.stringify(asset, null, 2) + "\n");
169
+ try {
170
+ writeFileSync(path, JSON.stringify(asset, null, 2) + "\n");
171
+ } catch (err) {
172
+ console.error("[recallnest] Failed to save pin asset:", err instanceof Error ? err.message : String(err));
173
+ }
169
174
  return path;
170
175
  }
171
176
 
@@ -198,7 +203,11 @@ export function buildBriefAsset(
198
203
  export function saveBriefAsset(asset: BriefAsset): string {
199
204
  const dir = getAssetsDir();
200
205
  const path = join(dir, `${asset.id}.json`);
201
- writeFileSync(path, JSON.stringify(asset, null, 2) + "\n");
206
+ try {
207
+ writeFileSync(path, JSON.stringify(asset, null, 2) + "\n");
208
+ } catch (err) {
209
+ console.error("[recallnest] Failed to save brief asset:", err instanceof Error ? err.message : String(err));
210
+ }
202
211
  return path;
203
212
  }
204
213
 
@@ -332,7 +341,11 @@ export function writeExportArtifact(params: {
332
341
  };
333
342
 
334
343
  if (params.format === "json") {
335
- writeFileSync(outputPath, JSON.stringify(payload, null, 2) + "\n");
344
+ try {
345
+ writeFileSync(outputPath, JSON.stringify(payload, null, 2) + "\n");
346
+ } catch (err) {
347
+ console.error("[recallnest] Failed to write JSON export:", err instanceof Error ? err.message : String(err));
348
+ }
336
349
  } else {
337
350
  const lines = [
338
351
  `# Memory Export`,
@@ -365,7 +378,11 @@ export function writeExportArtifact(params: {
365
378
  lines.push("");
366
379
  });
367
380
 
368
- writeFileSync(outputPath, lines.join("\n"));
381
+ try {
382
+ writeFileSync(outputPath, lines.join("\n"));
383
+ } catch (err) {
384
+ console.error("[recallnest] Failed to write markdown export:", err instanceof Error ? err.message : String(err));
385
+ }
369
386
  }
370
387
 
371
388
  return {
@@ -0,0 +1,365 @@
1
+ /**
2
+ * Memory Evolution — lifecycle tracking for memory entries.
3
+ *
4
+ * Adds status, version, access tracking, supersede/consolidation links,
5
+ * and decay scoring to each memory. Fields live inside the existing
6
+ * metadata JSON so the LanceDB schema is unchanged.
7
+ *
8
+ * Design principles (from arXiv 2512.13564 survey):
9
+ * - Archive-first, delete-never
10
+ * - Supersede-on-conflict (old stays, new links to it)
11
+ * - Composite decay = 0.2 time + 0.3 frequency + 0.5 importance (base)
12
+ * With emotion: 0.15 time + 0.25 frequency + 0.45 importance + 0.15 emotionSalience
13
+ */
14
+
15
+ import { parseEmotion, isEmotionScoringEnabled } from "./memory-schema.js";
16
+
17
+ // ---------------------------------------------------------------------------
18
+ // Types
19
+ // ---------------------------------------------------------------------------
20
+
21
+ export type EvolutionStatus = "active" | "superseded" | "archived" | "consolidated" | "pending_review";
22
+
23
+ export interface EvolutionMetadata {
24
+ status: EvolutionStatus;
25
+ version: number;
26
+ accessCount: number;
27
+ lastAccessedAt: number | null;
28
+ supersededBy: string | null;
29
+ /** HP-1: Reverse link — this memory supersedes an older one */
30
+ supersedes: string | null;
31
+ /** HP-1: Why this memory replaced the old one */
32
+ evolutionNote: string | null;
33
+ consolidatedInto: string | null;
34
+ /** HP-5: This memory contributed to a cross-memory pattern discovery */
35
+ contributedToPattern: string | null;
36
+ sourceMemories: string[];
37
+ validFrom: number;
38
+ validUntil: number | null;
39
+ /** F3: Event actual time (ms) — distinct from memory creation time (validFrom). */
40
+ eventTime: number | null;
41
+ }
42
+
43
+ // ---------------------------------------------------------------------------
44
+ // Defaults
45
+ // ---------------------------------------------------------------------------
46
+
47
+ export function defaultEvolution(now?: number): EvolutionMetadata {
48
+ const ts = now ?? Date.now();
49
+ return {
50
+ status: "active",
51
+ version: 1,
52
+ accessCount: 0,
53
+ lastAccessedAt: null,
54
+ supersededBy: null,
55
+ supersedes: null,
56
+ evolutionNote: null,
57
+ consolidatedInto: null,
58
+ contributedToPattern: null,
59
+ sourceMemories: [],
60
+ validFrom: ts,
61
+ validUntil: null,
62
+ eventTime: null,
63
+ };
64
+ }
65
+
66
+ // ---------------------------------------------------------------------------
67
+ // Parse / Serialize
68
+ // ---------------------------------------------------------------------------
69
+
70
+ /**
71
+ * Extract evolution metadata from a memory's metadata JSON string.
72
+ * Returns sensible defaults when the field is absent (backward compat).
73
+ */
74
+ export function parseEvolution(metadata: string | undefined, fallbackTimestamp?: number): EvolutionMetadata {
75
+ if (!metadata) return defaultEvolution(fallbackTimestamp);
76
+ try {
77
+ const parsed = JSON.parse(metadata);
78
+ const evo = parsed?.evolution;
79
+ if (!evo) return defaultEvolution(fallbackTimestamp);
80
+ return {
81
+ status: evo.status ?? "active",
82
+ version: evo.version ?? 1,
83
+ accessCount: evo.accessCount ?? 0,
84
+ lastAccessedAt: evo.lastAccessedAt ?? null,
85
+ supersededBy: evo.supersededBy ?? null,
86
+ supersedes: evo.supersedes ?? null,
87
+ evolutionNote: evo.evolutionNote ?? null,
88
+ consolidatedInto: evo.consolidatedInto ?? null,
89
+ contributedToPattern: evo.contributedToPattern ?? null,
90
+ sourceMemories: Array.isArray(evo.sourceMemories) ? evo.sourceMemories : [],
91
+ validFrom: evo.validFrom ?? fallbackTimestamp ?? Date.now(),
92
+ validUntil: evo.validUntil ?? null,
93
+ eventTime: evo.eventTime ?? null,
94
+ };
95
+ } catch {
96
+ return defaultEvolution(fallbackTimestamp);
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Patch evolution fields into an existing metadata JSON string.
102
+ * Merges with existing evolution — does not wipe unset fields.
103
+ */
104
+ export function patchEvolution(
105
+ metadata: string | undefined,
106
+ patch: Partial<EvolutionMetadata>,
107
+ ): string {
108
+ let parsed: Record<string, unknown> = {};
109
+ if (metadata) {
110
+ try { parsed = JSON.parse(metadata); } catch { /* keep empty */ }
111
+ }
112
+ const existing = parsed.evolution as Partial<EvolutionMetadata> | undefined;
113
+ parsed.evolution = { ...existing, ...patch };
114
+ return JSON.stringify(parsed);
115
+ }
116
+
117
+ // ---------------------------------------------------------------------------
118
+ // Predicates
119
+ // ---------------------------------------------------------------------------
120
+
121
+ /** Is this memory currently active (not superseded/archived/consolidated)?
122
+ * HP-6: pending_review memories are also considered active for search purposes. */
123
+ export function isActiveMemory(metadata: string | undefined): boolean {
124
+ const evo = parseEvolution(metadata);
125
+ return evo.status === "active" || evo.status === "pending_review";
126
+ }
127
+
128
+ // ---------------------------------------------------------------------------
129
+ // Access Tracking
130
+ // ---------------------------------------------------------------------------
131
+
132
+ /**
133
+ * Record a retrieval hit: increment accessCount and update lastAccessedAt.
134
+ * Returns updated metadata JSON string.
135
+ */
136
+ export function recordAccess(metadata: string | undefined): string {
137
+ const evo = parseEvolution(metadata);
138
+ return patchEvolution(metadata, {
139
+ accessCount: evo.accessCount + 1,
140
+ lastAccessedAt: Date.now(),
141
+ });
142
+ }
143
+
144
+ // ---------------------------------------------------------------------------
145
+ // Supersede / Consolidate helpers
146
+ // ---------------------------------------------------------------------------
147
+
148
+ /**
149
+ * Mark a memory as superseded by a newer memory.
150
+ * Sets status, validUntil, and supersededBy link.
151
+ */
152
+ export function buildSupersedeMetadata(
153
+ oldMetadata: string | undefined,
154
+ newMemoryId: string,
155
+ ): string {
156
+ return patchEvolution(oldMetadata, {
157
+ status: "superseded",
158
+ validUntil: Date.now(),
159
+ supersededBy: newMemoryId,
160
+ });
161
+ }
162
+
163
+ /**
164
+ * HP-1: Build evolution metadata for the NEW memory that supersedes an old one.
165
+ * Sets supersedes link + optional evolution note (bidirectional with buildSupersedeMetadata).
166
+ */
167
+ export function buildSupersedeMetadataForNew(
168
+ newMetadata: string | undefined,
169
+ oldMemoryId: string,
170
+ evolutionNote?: string,
171
+ ): string {
172
+ return patchEvolution(newMetadata, {
173
+ supersedes: oldMemoryId,
174
+ evolutionNote: evolutionNote ?? null,
175
+ });
176
+ }
177
+
178
+ /**
179
+ * HP-1: Trace the evolution chain for a memory.
180
+ * Walks both directions: supersededBy (forward) and supersedes (backward).
181
+ * Returns an ordered timeline from oldest to newest.
182
+ */
183
+ export interface EvolutionTraceEntry {
184
+ id: string;
185
+ direction: "predecessor" | "self" | "successor";
186
+ status: EvolutionStatus;
187
+ evolutionNote: string | null;
188
+ validFrom: number;
189
+ validUntil: number | null;
190
+ }
191
+
192
+ export async function traceEvolution(
193
+ startId: string,
194
+ getEntry: (id: string) => Promise<{ metadata?: string; timestamp?: number } | null>,
195
+ maxDepth = 10,
196
+ ): Promise<EvolutionTraceEntry[]> {
197
+ const predecessors: EvolutionTraceEntry[] = [];
198
+ const successors: EvolutionTraceEntry[] = [];
199
+
200
+ // Walk backward (supersedes chain)
201
+ let currentId: string | null = startId;
202
+ let depth = 0;
203
+ const startEntry = await getEntry(startId);
204
+ const startEvo = parseEvolution(startEntry?.metadata, startEntry?.timestamp);
205
+
206
+ // First, walk backward via supersedes
207
+ currentId = startEvo.supersedes;
208
+ while (currentId && depth < maxDepth) {
209
+ const entry = await getEntry(currentId);
210
+ if (!entry) break;
211
+ const evo = parseEvolution(entry.metadata, entry.timestamp);
212
+ predecessors.unshift({
213
+ id: currentId,
214
+ direction: "predecessor",
215
+ status: evo.status,
216
+ evolutionNote: evo.evolutionNote,
217
+ validFrom: evo.validFrom,
218
+ validUntil: evo.validUntil,
219
+ });
220
+ currentId = evo.supersedes;
221
+ depth++;
222
+ }
223
+
224
+ // Self
225
+ const self: EvolutionTraceEntry = {
226
+ id: startId,
227
+ direction: "self",
228
+ status: startEvo.status,
229
+ evolutionNote: startEvo.evolutionNote,
230
+ validFrom: startEvo.validFrom,
231
+ validUntil: startEvo.validUntil,
232
+ };
233
+
234
+ // Walk forward (supersededBy chain)
235
+ currentId = startEvo.supersededBy;
236
+ depth = 0;
237
+ while (currentId && depth < maxDepth) {
238
+ const entry = await getEntry(currentId);
239
+ if (!entry) break;
240
+ const evo = parseEvolution(entry.metadata, entry.timestamp);
241
+ successors.push({
242
+ id: currentId,
243
+ direction: "successor",
244
+ status: evo.status,
245
+ evolutionNote: evo.evolutionNote,
246
+ validFrom: evo.validFrom,
247
+ validUntil: evo.validUntil,
248
+ });
249
+ currentId = evo.supersededBy;
250
+ depth++;
251
+ }
252
+
253
+ return [...predecessors, self, ...successors];
254
+ }
255
+
256
+ /**
257
+ * Mark a memory as consolidated into a higher-level memory.
258
+ * The original is kept (archive-first) but linked to the consolidated entry.
259
+ */
260
+ export function buildConsolidatedMetadata(
261
+ oldMetadata: string | undefined,
262
+ consolidatedMemoryId: string,
263
+ ): string {
264
+ return patchEvolution(oldMetadata, {
265
+ status: "consolidated",
266
+ consolidatedInto: consolidatedMemoryId,
267
+ });
268
+ }
269
+
270
+ /**
271
+ * Mark a memory as archived (low decay score for extended period).
272
+ */
273
+ export function buildArchivedMetadata(oldMetadata: string | undefined): string {
274
+ return patchEvolution(oldMetadata, { status: "archived" });
275
+ }
276
+
277
+ /**
278
+ * HP-6: Mark a memory as pending_review (low-confidence store).
279
+ * Pending memories participate in search but are prioritized for distill processing.
280
+ */
281
+ export function buildPendingReviewMetadata(metadata: string | undefined): string {
282
+ return patchEvolution(metadata, { status: "pending_review" });
283
+ }
284
+
285
+ /** HP-6: Check if a memory is pending review. */
286
+ export function isPendingReview(metadata: string | undefined): boolean {
287
+ return parseEvolution(metadata).status === "pending_review";
288
+ }
289
+
290
+ /** HP-6: Resolve pending → active (after distill re-evaluation confirms it). */
291
+ export function resolvePendingReview(metadata: string | undefined): string {
292
+ return patchEvolution(metadata, { status: "active" });
293
+ }
294
+
295
+ // ---------------------------------------------------------------------------
296
+ // Decay Scoring
297
+ // ---------------------------------------------------------------------------
298
+
299
+ const TIME_HALF_LIFE_DAYS = 90;
300
+
301
+ // Base weights (no emotion): 0.2 + 0.3 + 0.5 = 1.0
302
+ const TIME_WEIGHT_BASE = 0.2;
303
+ const FREQUENCY_WEIGHT_BASE = 0.3;
304
+ const IMPORTANCE_WEIGHT_BASE = 0.5;
305
+
306
+ // Emotion-enabled weights: 0.15 + 0.25 + 0.45 + 0.15 = 1.0
307
+ const TIME_WEIGHT_EMO = 0.15;
308
+ const FREQUENCY_WEIGHT_EMO = 0.25;
309
+ const IMPORTANCE_WEIGHT_EMO = 0.45;
310
+ const EMOTION_WEIGHT = 0.15;
311
+
312
+ /**
313
+ * Compute composite decay score (0–1, higher = more valuable to keep).
314
+ *
315
+ * Base: 0.2 × timeDecay + 0.3 × frequencyScore + 0.5 × importance
316
+ * Emotion: 0.15 × time + 0.25 × freq + 0.45 × importance + 0.15 × emotionSalience
317
+ *
318
+ * Time decay: exponential with 90-day half-life.
319
+ * Frequency: log(1 + accessCount) × recencyBoost, capped at 1.
320
+ * Importance: as-stored (0–1).
321
+ * Emotion salience: (|valence| + arousal) / 2 — composite mnemonic significance.
322
+ *
323
+ * @param metadata - Optional metadata JSON string for emotion extraction.
324
+ * When absent or emotion flag off, falls back to base weights.
325
+ */
326
+ export function computeDecayScore(
327
+ evo: EvolutionMetadata,
328
+ importance: number,
329
+ now?: number,
330
+ metadata?: string,
331
+ ): number {
332
+ const ts = now ?? Date.now();
333
+
334
+ // Time decay (Weibull-ish exponential)
335
+ const daysSinceCreation = Math.max(0, (ts - evo.validFrom) / 86_400_000);
336
+ const timeDecay = Math.pow(0.5, daysSinceCreation / TIME_HALF_LIFE_DAYS);
337
+
338
+ // Frequency score
339
+ const rawFreq = Math.log2(1 + evo.accessCount);
340
+ const recencyBoost = evo.lastAccessedAt
341
+ ? Math.pow(0.5, Math.max(0, (ts - evo.lastAccessedAt) / 86_400_000) / 30) // 30-day half-life for recency
342
+ : 0.5; // never accessed → neutral
343
+ const frequencyScore = Math.min(1, rawFreq * recencyBoost);
344
+
345
+ const clampedImportance = Math.max(0, Math.min(1, importance));
346
+
347
+ // HP-emo: Emotion-aware decay scoring
348
+ const useEmotion = isEmotionScoringEnabled() && !!metadata;
349
+ if (useEmotion) {
350
+ const emotion = parseEmotion(metadata);
351
+ const salience = emotion?.salience ?? 0;
352
+ return (
353
+ TIME_WEIGHT_EMO * timeDecay +
354
+ FREQUENCY_WEIGHT_EMO * frequencyScore +
355
+ IMPORTANCE_WEIGHT_EMO * clampedImportance +
356
+ EMOTION_WEIGHT * salience
357
+ );
358
+ }
359
+
360
+ return (
361
+ TIME_WEIGHT_BASE * timeDecay +
362
+ FREQUENCY_WEIGHT_BASE * frequencyScore +
363
+ IMPORTANCE_WEIGHT_BASE * clampedImportance
364
+ );
365
+ }