recallnest 1.4.1 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (166) hide show
  1. package/.dockerignore +11 -0
  2. package/CLAUDE.md +7 -3
  3. package/Dockerfile +20 -0
  4. package/README.md +202 -130
  5. package/README_CN.md +263 -190
  6. package/ROADMAP.md +128 -81
  7. package/SKILL.md +250 -0
  8. package/assets/dashboard.png +0 -0
  9. package/assets/knowledge-graph.png +0 -0
  10. package/assets/screenshots/ui-full.png +0 -0
  11. package/assets/screenshots/ui-overview.png +0 -0
  12. package/assets/screenshots/ui-search.png +0 -0
  13. package/assets/ui/app.js +227 -3
  14. package/assets/ui/index.html +81 -11
  15. package/assets/ui/styles.css +178 -2
  16. package/bin/recallnest.mjs +62 -0
  17. package/bun.lock +337 -0
  18. package/daily-recall.sh +23 -0
  19. package/daily-reflections/2026-04-02.md +35 -0
  20. package/daily-reflections/2026-04-03.md +37 -0
  21. package/daily-reflections/2026-04-04.md +37 -0
  22. package/daily-reflections/2026-04-05.md +35 -0
  23. package/daily-reflections/2026-04-06.md +36 -0
  24. package/daily-reflections/2026-04-07.md +35 -0
  25. package/daily-reflections/2026-04-08.md +35 -0
  26. package/daily-reflections/2026-04-12.md +35 -0
  27. package/data/alias-map.example.json +22 -0
  28. package/docs/plans/2026-04-01-recall-boost.md +100 -0
  29. package/docs/superpowers/plans/2026-04-11-constructive-retrieval-emotion.md +1195 -0
  30. package/docs/superpowers/specs/2026-04-11-constructive-retrieval-emotion-design.md +398 -0
  31. package/glama.json +6 -0
  32. package/integrations/setup-all.sh +31 -0
  33. package/package.json +10 -3
  34. package/plan-philosophy-upgrades.md +363 -0
  35. package/plan-product-upgrades.md +155 -0
  36. package/plan.md +140 -0
  37. package/research-memory-evolution.md +440 -0
  38. package/research-rl-agent-memory.md +330 -0
  39. package/scripts/backfill-anchors.ts +130 -0
  40. package/scripts/diagnose-vector-search.ts +132 -0
  41. package/scripts/migrate-language.ts +75 -0
  42. package/scripts/verify-emotion-decay.ts +148 -0
  43. package/src/__tests__/activity-counter.test.ts +96 -0
  44. package/src/__tests__/admission-control.test.ts +206 -0
  45. package/src/__tests__/anchor-generator.test.ts +67 -0
  46. package/src/__tests__/babel-integration.test.ts +139 -0
  47. package/src/__tests__/capture-engine.test.ts +5 -3
  48. package/src/__tests__/cascade-forget.test.ts +1 -1
  49. package/src/__tests__/cc8-cc1-essential-context.test.ts +298 -0
  50. package/src/__tests__/circuit-breaker.test.ts +99 -0
  51. package/src/__tests__/cluster-consolidation.test.ts +712 -0
  52. package/src/__tests__/confidence-metadata.test.ts +125 -0
  53. package/src/__tests__/confidence-tracker.test.ts +53 -0
  54. package/src/__tests__/consolidation-engine.test.ts +1 -1
  55. package/src/__tests__/constructive-retrieval.test.ts +509 -0
  56. package/src/__tests__/context-collapse-renderer.test.ts +214 -0
  57. package/src/__tests__/context-reconstructor.test.ts +65 -0
  58. package/src/__tests__/conversation-importer.test.ts +692 -0
  59. package/src/__tests__/data-checkup.test.ts +225 -0
  60. package/src/__tests__/decay-exemption.test.ts +84 -0
  61. package/src/__tests__/dedup-guard.test.ts +39 -0
  62. package/src/__tests__/distill-lock.test.ts +176 -0
  63. package/src/__tests__/dream-pipeline.test.ts +232 -0
  64. package/src/__tests__/emotion-decay.test.ts +46 -0
  65. package/src/__tests__/emotion-detector.test.ts +56 -0
  66. package/src/__tests__/emotion-phase1-integration.test.ts +153 -0
  67. package/src/__tests__/emotion-retrieval.test.ts +56 -0
  68. package/src/__tests__/ethics-layer.test.ts +476 -0
  69. package/src/__tests__/feature-flags-integration.test.ts +54 -0
  70. package/src/__tests__/frequency-tracker.test.ts +113 -0
  71. package/src/__tests__/graph-export.test.ts +489 -0
  72. package/src/__tests__/interference-detector.test.ts +203 -0
  73. package/src/__tests__/mcp-efficiency.test.ts +288 -0
  74. package/src/__tests__/memory-evolution-b2-b3-e1.test.ts +243 -0
  75. package/src/__tests__/memory-evolution.test.ts +271 -0
  76. package/src/__tests__/memory-lint.test.ts +271 -0
  77. package/src/__tests__/narrative-architecture.test.ts +456 -0
  78. package/src/__tests__/pending-review.test.ts +77 -0
  79. package/src/__tests__/post-retrieval-filter.test.ts +178 -0
  80. package/src/__tests__/prediction-engine.test.ts +349 -0
  81. package/src/__tests__/predictive-prospective-memory.test.ts +416 -0
  82. package/src/__tests__/preference-slots.test.ts +99 -0
  83. package/src/__tests__/query-expander-alias.test.ts +89 -0
  84. package/src/__tests__/recall-governor.test.ts +227 -0
  85. package/src/__tests__/resume-context.test.ts +156 -1
  86. package/src/__tests__/salience-scoring.test.ts +106 -0
  87. package/src/__tests__/session-distiller.test.ts +611 -0
  88. package/src/__tests__/skill-engine.test.ts +546 -0
  89. package/src/__tests__/skill-promotion.test.ts +349 -0
  90. package/src/__tests__/supersede-chain.test.ts +154 -0
  91. package/src/__tests__/temporal-validity.test.ts +160 -0
  92. package/src/__tests__/topic-tag.test.ts +113 -0
  93. package/src/__tests__/trusted-memory.test.ts +828 -0
  94. package/src/__tests__/write-verifier.test.ts +122 -0
  95. package/src/access-tracker.ts +3 -2
  96. package/src/activity-counter.ts +109 -0
  97. package/src/adaptive-retrieval.ts +6 -5
  98. package/src/admission-control.ts +163 -0
  99. package/src/anchor-generator.ts +80 -0
  100. package/src/api-server.ts +10 -0
  101. package/src/audit-log.ts +122 -0
  102. package/src/auto-gc.ts +98 -29
  103. package/src/auto-recall.ts +33 -4
  104. package/src/capture-engine.ts +372 -23
  105. package/src/cascade-forget.ts +3 -2
  106. package/src/cli.ts +107 -5
  107. package/src/compat.ts +22 -0
  108. package/src/confidence-tracker.ts +210 -47
  109. package/src/conflict-store.ts +12 -3
  110. package/src/consolidation-engine.ts +296 -8
  111. package/src/context-collapse-renderer.ts +222 -0
  112. package/src/context-composer-stable-selection.ts +5 -1
  113. package/src/context-composer.ts +239 -4
  114. package/src/context-reconstructor.ts +404 -0
  115. package/src/conversation-importer.ts +441 -0
  116. package/src/data-checkup.ts +232 -0
  117. package/src/decay-engine.ts +84 -0
  118. package/src/distill-lock.ts +122 -0
  119. package/src/doctor.ts +8 -10
  120. package/src/dream-pipeline.ts +227 -0
  121. package/src/emotion-detector.ts +90 -0
  122. package/src/eval.ts +3 -2
  123. package/src/forget-engine.ts +269 -0
  124. package/src/frequency-tracker.ts +175 -0
  125. package/src/graph-export.ts +644 -0
  126. package/src/ingest.ts +36 -6
  127. package/src/interference-detector.ts +187 -0
  128. package/src/kg-extractor.ts +15 -24
  129. package/src/kg-store.ts +2 -1
  130. package/src/language-hook.ts +67 -0
  131. package/src/llm-client.ts +289 -6
  132. package/src/mcp-server.ts +707 -80
  133. package/src/memory-assets.ts +22 -5
  134. package/src/memory-evolution.ts +365 -0
  135. package/src/memory-lint.ts +429 -0
  136. package/src/memory-output.ts +69 -0
  137. package/src/memory-schema.ts +74 -2
  138. package/src/narrative-schema.ts +79 -0
  139. package/src/narrative-tagger.ts +164 -0
  140. package/src/pii-detector.ts +98 -0
  141. package/src/post-retrieval-filter.ts +76 -0
  142. package/src/prediction-engine.ts +253 -0
  143. package/src/preference-slots.ts +65 -2
  144. package/src/prospective-memory.ts +215 -0
  145. package/src/query-expander.ts +67 -0
  146. package/src/recall-governor.ts +183 -0
  147. package/src/retention-policy.ts +90 -0
  148. package/src/retriever.ts +828 -55
  149. package/src/rif.ts +34 -8
  150. package/src/runtime-config.ts +23 -8
  151. package/src/scope-inventory.ts +4 -3
  152. package/src/scope-policy.ts +4 -0
  153. package/src/session-distiller.ts +307 -0
  154. package/src/session-output.ts +40 -0
  155. package/src/session-schema.ts +37 -0
  156. package/src/session-store.ts +7 -2
  157. package/src/skill-engine.ts +208 -0
  158. package/src/skill-promotion.ts +293 -0
  159. package/src/skill-schema.ts +39 -0
  160. package/src/store.ts +96 -10
  161. package/src/topic-tag.ts +66 -0
  162. package/src/tracker.ts +3 -1
  163. package/src/ui-server.ts +80 -8
  164. package/src/workflow-observation-scope-review.ts +2 -1
  165. package/src/workflow-observation-store.ts +2 -1
  166. package/src/write-verifier.ts +126 -0
@@ -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
+ }