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
@@ -0,0 +1,429 @@
1
+ /**
2
+ * Memory Lint Engine — Content Quality Checker
3
+ *
4
+ * A read-only quality checker for memory content. Separate from data-checkup.ts
5
+ * which checks infrastructure integrity (vector dims, tier distribution, etc.),
6
+ * Memory Lint checks *content* quality:
7
+ *
8
+ * 1. Contradictions — memories that say opposite things about the same topic
9
+ * 2. Duplicates — near-identical memories by vector cosine similarity
10
+ * 3. Stale — memories never or rarely accessed and old enough to review
11
+ * 4. Orphans — memories with missing scope or broken consolidation links
12
+ *
13
+ * Produces a health score (0-100) and a human-readable report.
14
+ */
15
+
16
+ import type { MemoryEntry, MemoryStore } from "./store.js";
17
+ import { cosineSimilarity } from "./multi-vector.js";
18
+ import { parseEvolution, isActiveMemory } from "./memory-evolution.js";
19
+
20
+ // ---------------------------------------------------------------------------
21
+ // Types
22
+ // ---------------------------------------------------------------------------
23
+
24
+ export type LintSeverity = "info" | "warning" | "error";
25
+
26
+ export interface LintFinding {
27
+ check: string; // e.g., "contradiction", "duplicate", "stale", "orphan"
28
+ severity: LintSeverity;
29
+ detail: string;
30
+ memoryIds: string[];
31
+ }
32
+
33
+ export interface MemoryLintReport {
34
+ findings: LintFinding[];
35
+ healthScore: number; // 0-100
36
+ totalScanned: number;
37
+ timestamp: string;
38
+ summary: {
39
+ contradictions: number;
40
+ duplicates: number;
41
+ staleMemories: number;
42
+ orphans: number;
43
+ };
44
+ }
45
+
46
+ export interface LintDeps {
47
+ store: Pick<MemoryStore, "list">;
48
+ scope?: string;
49
+ verbose?: boolean;
50
+ }
51
+
52
+ // ---------------------------------------------------------------------------
53
+ // Constants
54
+ // ---------------------------------------------------------------------------
55
+
56
+ /** Maximum entries to compare per scope+category group (performance guard). */
57
+ const MAX_ENTRIES_PER_GROUP = 100;
58
+
59
+ /** Cosine similarity threshold for duplicate detection. */
60
+ const DUPLICATE_THRESHOLD = 0.92;
61
+
62
+ /**
63
+ * Minimum vector similarity to even consider two entries as potential contradictions.
64
+ * Real contradictions are about the SAME topic but say opposite things,
65
+ * so they should have moderate semantic similarity.
66
+ */
67
+ const CONTRADICTION_SIMILARITY_FLOOR = 0.45;
68
+
69
+ /**
70
+ * Categories where contradictions are meaningful.
71
+ * Append-only categories (events, cases) naturally contain "opposite" entries
72
+ * from different points in time — those are not contradictions.
73
+ */
74
+ const CONTRADICTION_CATEGORIES = new Set(["profile", "preferences", "entities", "patterns"]);
75
+
76
+ /** Entries not accessed in this many days are candidates for staleness. */
77
+ const STALE_DAYS = 90;
78
+
79
+ const STALE_MS = STALE_DAYS * 24 * 60 * 60 * 1000;
80
+
81
+ // ---------------------------------------------------------------------------
82
+ // Contradiction Detection (inline copy from consolidation-engine.ts)
83
+ // ---------------------------------------------------------------------------
84
+
85
+ /**
86
+ * Heuristic contradiction detection between two memory texts.
87
+ * Checks for negation patterns and requires at least one shared significant
88
+ * term to reduce false positives.
89
+ */
90
+ function detectContradiction(textA: string, textB: string): boolean {
91
+ const a = textA.toLowerCase();
92
+ const b = textB.toLowerCase();
93
+
94
+ const negationPairs: [RegExp, RegExp][] = [
95
+ [/\bnot\b/, /\b(?:always|must|should|is|are|was|were)\b/],
96
+ [/\bnever\b/, /\b(?:always|every|each)\b/],
97
+ [/\bdisable/, /\benable/],
98
+ [/不要|不用|别/, /必须|一定|总是/],
99
+ [/从不/, /每次|总是|一直/],
100
+ ];
101
+
102
+ for (const [negRe, posRe] of negationPairs) {
103
+ if ((negRe.test(a) && posRe.test(b)) || (negRe.test(b) && posRe.test(a))) {
104
+ // Require at least one shared significant term to reduce false positives
105
+ const wordsA = new Set(a.split(/\s+/).filter(w => w.length > 3));
106
+ const wordsB = new Set(b.split(/\s+/).filter(w => w.length > 3));
107
+ for (const w of wordsA) {
108
+ if (wordsB.has(w)) return true;
109
+ }
110
+ }
111
+ }
112
+ return false;
113
+ }
114
+
115
+ // ---------------------------------------------------------------------------
116
+ // Helpers
117
+ // ---------------------------------------------------------------------------
118
+
119
+ /** Truncate text for display in findings. */
120
+ function truncate(text: string, maxLen = 60): string {
121
+ if (text.length <= maxLen) return text;
122
+ return text.slice(0, maxLen - 3) + "...";
123
+ }
124
+
125
+ /**
126
+ * Group entries by scope + category key.
127
+ * Within each group, sort by importance descending and cap at MAX_ENTRIES_PER_GROUP.
128
+ */
129
+ function groupEntries(entries: MemoryEntry[]): Map<string, MemoryEntry[]> {
130
+ const groups = new Map<string, MemoryEntry[]>();
131
+
132
+ for (const entry of entries) {
133
+ const key = `${entry.scope ?? ""}::${entry.category}`;
134
+ if (!groups.has(key)) groups.set(key, []);
135
+ groups.get(key)!.push(entry);
136
+ }
137
+
138
+ // Sort each group by importance descending, cap at limit
139
+ for (const [key, group] of groups) {
140
+ group.sort((a, b) => b.importance - a.importance);
141
+ if (group.length > MAX_ENTRIES_PER_GROUP) {
142
+ groups.set(key, group.slice(0, MAX_ENTRIES_PER_GROUP));
143
+ }
144
+ }
145
+
146
+ return groups;
147
+ }
148
+
149
+ // ---------------------------------------------------------------------------
150
+ // Check 1: Contradictions
151
+ // ---------------------------------------------------------------------------
152
+
153
+ function findContradictions(entries: MemoryEntry[]): LintFinding[] {
154
+ const findings: LintFinding[] = [];
155
+
156
+ // Only check merge-type categories where contradictions are meaningful
157
+ const eligible = entries.filter(e => CONTRADICTION_CATEGORIES.has(e.category));
158
+ const groups = groupEntries(eligible);
159
+
160
+ for (const [, group] of groups) {
161
+ if (group.length < 2) continue;
162
+
163
+ for (let i = 0; i < group.length; i++) {
164
+ for (let j = i + 1; j < group.length; j++) {
165
+ // Pre-filter: entries must be about the same topic (moderate vector similarity)
166
+ const sim = cosineSimilarity(group[i].vector, group[j].vector);
167
+ if (sim < CONTRADICTION_SIMILARITY_FLOOR) continue;
168
+
169
+ if (detectContradiction(group[i].text, group[j].text)) {
170
+ findings.push({
171
+ check: "contradiction",
172
+ severity: "warning",
173
+ detail: `"${truncate(group[i].text)}" vs "${truncate(group[j].text)}"`,
174
+ memoryIds: [group[i].id, group[j].id],
175
+ });
176
+ }
177
+ }
178
+ }
179
+ }
180
+
181
+ return findings;
182
+ }
183
+
184
+ // ---------------------------------------------------------------------------
185
+ // Check 2: Duplicates
186
+ // ---------------------------------------------------------------------------
187
+
188
+ function findDuplicates(entries: MemoryEntry[]): LintFinding[] {
189
+ const findings: LintFinding[] = [];
190
+ const groups = groupEntries(entries);
191
+
192
+ for (const [, group] of groups) {
193
+ if (group.length < 2) continue;
194
+
195
+ for (let i = 0; i < group.length; i++) {
196
+ for (let j = i + 1; j < group.length; j++) {
197
+ const sim = cosineSimilarity(group[i].vector, group[j].vector);
198
+ if (sim >= DUPLICATE_THRESHOLD) {
199
+ findings.push({
200
+ check: "duplicate",
201
+ severity: "warning",
202
+ detail: `"${truncate(group[i].text)}" -- ${(sim * 100).toFixed(1)}% similar [${truncate(group[j].text)}]`,
203
+ memoryIds: [group[i].id, group[j].id],
204
+ });
205
+ }
206
+ }
207
+ }
208
+ }
209
+
210
+ return findings;
211
+ }
212
+
213
+ // ---------------------------------------------------------------------------
214
+ // Check 3: Stale Memories
215
+ // ---------------------------------------------------------------------------
216
+
217
+ function findStaleMemories(entries: MemoryEntry[]): LintFinding[] {
218
+ const findings: LintFinding[] = [];
219
+ const now = Date.now();
220
+
221
+ for (const entry of entries) {
222
+ const evo = parseEvolution(entry.metadata, entry.timestamp);
223
+
224
+ // Stale = entry itself is old enough AND (lastAccessedAt is null or > 90 days ago) AND accessCount <= 1
225
+ const entryAge = now - entry.timestamp;
226
+ if (entryAge < STALE_MS) continue; // too new to be considered stale
227
+
228
+ const lastAccess = evo.lastAccessedAt;
229
+ const isOldAccess = lastAccess === null || (now - lastAccess > STALE_MS);
230
+
231
+ if (isOldAccess && evo.accessCount <= 1) {
232
+ const ageDays = Math.floor((now - entry.timestamp) / (24 * 60 * 60 * 1000));
233
+ findings.push({
234
+ check: "stale",
235
+ severity: "info",
236
+ detail: `${ageDays}d old, ${evo.accessCount} access(es): "${truncate(entry.text, 50)}"`,
237
+ memoryIds: [entry.id],
238
+ });
239
+ }
240
+ }
241
+
242
+ return findings;
243
+ }
244
+
245
+ // ---------------------------------------------------------------------------
246
+ // Check 4: Orphans
247
+ // ---------------------------------------------------------------------------
248
+
249
+ function findOrphans(entries: MemoryEntry[]): LintFinding[] {
250
+ const findings: LintFinding[] = [];
251
+
252
+ // Build set of all loaded IDs for consolidation link checking
253
+ const allIds = new Set(entries.map(e => e.id));
254
+
255
+ for (const entry of entries) {
256
+ // Missing or schema scope
257
+ if (!entry.scope || entry.scope.trim() === "" || entry.scope === "__schema__") {
258
+ findings.push({
259
+ check: "orphan",
260
+ severity: "info",
261
+ detail: `Missing/empty scope: "${truncate(entry.text, 50)}"`,
262
+ memoryIds: [entry.id],
263
+ });
264
+ continue;
265
+ }
266
+
267
+ // Broken consolidation link
268
+ const evo = parseEvolution(entry.metadata, entry.timestamp);
269
+ if (evo.consolidatedInto && !allIds.has(evo.consolidatedInto)) {
270
+ findings.push({
271
+ check: "orphan",
272
+ severity: "warning",
273
+ detail: `Broken consolidation link -> ${evo.consolidatedInto.slice(0, 12)}...: "${truncate(entry.text, 40)}"`,
274
+ memoryIds: [entry.id],
275
+ });
276
+ }
277
+ }
278
+
279
+ return findings;
280
+ }
281
+
282
+ // ---------------------------------------------------------------------------
283
+ // Health Score
284
+ // ---------------------------------------------------------------------------
285
+
286
+ /**
287
+ * Compute a 0-100 health score from finding counts.
288
+ *
289
+ * Weights:
290
+ * - Contradictions: -10 each (most dangerous, conflicting guidance)
291
+ * - Duplicates: -5 each (waste and confusion risk)
292
+ * - Stale: -0.5 each (minor, many are expected)
293
+ * - Orphans: -3 each (moderate, broken references)
294
+ */
295
+ export function computeHealthScore(
296
+ summary: MemoryLintReport["summary"],
297
+ _total: number,
298
+ ): number {
299
+ const penalty =
300
+ summary.contradictions * 10 +
301
+ summary.duplicates * 5 +
302
+ Math.floor(summary.staleMemories * 0.5) +
303
+ summary.orphans * 3;
304
+
305
+ return Math.max(0, Math.min(100, 100 - penalty));
306
+ }
307
+
308
+ // ---------------------------------------------------------------------------
309
+ // Main
310
+ // ---------------------------------------------------------------------------
311
+
312
+ /**
313
+ * Run all memory lint checks and produce a report.
314
+ *
315
+ * @param deps.store - Memory store (only `list` is needed)
316
+ * @param deps.scope - Optional scope filter; undefined = all scopes
317
+ * @param deps.verbose - Reserved for future use
318
+ */
319
+ export async function runMemoryLint(deps: LintDeps): Promise<MemoryLintReport> {
320
+ const scopeFilter = deps.scope ? [deps.scope] : undefined;
321
+ const allEntries = await deps.store.list(scopeFilter, undefined, 10000, 0);
322
+
323
+ // Filter to active entries only
324
+ const entries = allEntries.filter(e => isActiveMemory(e.metadata));
325
+
326
+ // Run all checks
327
+ const contradictions = findContradictions(entries);
328
+ const duplicates = findDuplicates(entries);
329
+ const stale = findStaleMemories(entries);
330
+ const orphans = findOrphans(entries);
331
+
332
+ const findings = [...contradictions, ...duplicates, ...stale, ...orphans];
333
+
334
+ const summary = {
335
+ contradictions: contradictions.length,
336
+ duplicates: duplicates.length,
337
+ staleMemories: stale.length,
338
+ orphans: orphans.length,
339
+ };
340
+
341
+ return {
342
+ findings,
343
+ healthScore: computeHealthScore(summary, entries.length),
344
+ totalScanned: entries.length,
345
+ timestamp: new Date().toISOString(),
346
+ summary,
347
+ };
348
+ }
349
+
350
+ // ---------------------------------------------------------------------------
351
+ // Formatter
352
+ // ---------------------------------------------------------------------------
353
+
354
+ /** Format a lint report as human-readable text. */
355
+ export function formatMemoryLintReport(report: MemoryLintReport): string {
356
+ const lines: string[] = [];
357
+
358
+ // All clear case
359
+ if (report.findings.length === 0) {
360
+ lines.push("Memory Lint: All Clear!");
361
+ lines.push(`Scanned: ${report.totalScanned} active memories`);
362
+ lines.push(`Health Score: ${report.healthScore}/100`);
363
+ return lines.join("\n");
364
+ }
365
+
366
+ // Header
367
+ lines.push(`Memory Lint Report (${report.timestamp})`);
368
+ lines.push("=".repeat(44));
369
+ lines.push(`Scanned: ${report.totalScanned} active memories`);
370
+ lines.push("");
371
+
372
+ // Contradictions
373
+ if (report.summary.contradictions > 0) {
374
+ const items = report.findings.filter(f => f.check === "contradiction");
375
+ lines.push(`Contradictions (${items.length}):`);
376
+ for (const f of items) {
377
+ lines.push(` - ${f.detail} [${f.memoryIds.map(id => id.slice(0, 8)).join(", ")}]`);
378
+ }
379
+ lines.push("");
380
+ }
381
+
382
+ // Duplicates
383
+ if (report.summary.duplicates > 0) {
384
+ const items = report.findings.filter(f => f.check === "duplicate");
385
+ lines.push(`Duplicates (${items.length}):`);
386
+ for (const f of items) {
387
+ lines.push(` - ${f.detail} [${f.memoryIds.map(id => id.slice(0, 8)).join(", ")}]`);
388
+ }
389
+ lines.push("");
390
+ }
391
+
392
+ // Stale
393
+ if (report.summary.staleMemories > 0) {
394
+ const items = report.findings.filter(f => f.check === "stale");
395
+ lines.push(`Stale (${items.length}):`);
396
+ if (items.length <= 5) {
397
+ for (const f of items) {
398
+ lines.push(` - ${f.detail}`);
399
+ }
400
+ } else {
401
+ // Summarize when many
402
+ lines.push(` - ${items.length} memories not accessed in ${STALE_DAYS}+ days`);
403
+ }
404
+ lines.push("");
405
+ }
406
+
407
+ // Orphans
408
+ if (report.summary.orphans > 0) {
409
+ const items = report.findings.filter(f => f.check === "orphan");
410
+ const missingScope = items.filter(f => f.severity === "info");
411
+ const brokenLinks = items.filter(f => f.severity === "warning");
412
+
413
+ lines.push(`Orphans (${items.length}):`);
414
+ if (missingScope.length > 0) {
415
+ lines.push(` - ${missingScope.length} memor${missingScope.length === 1 ? "y" : "ies"} with missing scope`);
416
+ }
417
+ if (brokenLinks.length > 0) {
418
+ for (const f of brokenLinks) {
419
+ lines.push(` - ${f.detail} [${f.memoryIds.map(id => id.slice(0, 8)).join(", ")}]`);
420
+ }
421
+ }
422
+ lines.push("");
423
+ }
424
+
425
+ // Health score
426
+ lines.push(`Health Score: ${report.healthScore}/100`);
427
+
428
+ return lines.join("\n");
429
+ }
@@ -2,6 +2,9 @@ import type { MemoryEntry } from "./store.js";
2
2
  import type { RetrievalResult } from "./retriever.js";
3
3
  import type { RetrievalProfileName } from "./retrieval-profiles.js";
4
4
  import { extractMemoryProvenance } from "./memory-boundaries.js";
5
+ import { extractMultiVectorText } from "./multi-vector.js";
6
+ import { parseNarrative } from "./narrative-schema.js";
7
+ import { getConfidence, getConfidenceMetadata } from "./confidence-tracker.js";
5
8
 
6
9
  interface MemoryMetadata {
7
10
  source?: string;
@@ -167,6 +170,7 @@ function getRetrievalPath(result: RetrievalResult): string {
167
170
  if (result.sources.vector) parts.push("vector");
168
171
  if (result.sources.bm25) parts.push("bm25");
169
172
  if (result.sources.reranked) parts.push("reranked");
173
+ if (result.sources.narrativeSibling) parts.push("narrative");
170
174
  return parts.join("+") || "direct";
171
175
  }
172
176
 
@@ -289,6 +293,71 @@ function buildSearchRow(index: number, query: string, result: RetrievalResult):
289
293
  ];
290
294
  }
291
295
 
296
+ function extractBriefExcerpt(result: RetrievalResult): string {
297
+ const { l0 } = extractMultiVectorText(result.entry.metadata);
298
+ const raw = l0 || result.entry.text;
299
+ return cleanSnippet(raw, 80);
300
+ }
301
+
302
+ export function formatBriefResults(
303
+ results: RetrievalResult[],
304
+ context: { query: string },
305
+ ): string {
306
+ if (results.length === 0) return "No results found.";
307
+ const lines = [`Query: ${context.query}`, `Hits: ${results.length}`, ""];
308
+ for (let i = 0; i < results.length; i++) {
309
+ const r = results[i];
310
+ const excerpt = extractBriefExcerpt(r);
311
+ lines.push(`#${i + 1} ${r.entry.id.slice(0, 8)} ${Math.round(r.score * 100)}% — ${excerpt}`);
312
+ }
313
+ return lines.join("\n");
314
+ }
315
+
316
+ export function formatFullResults(
317
+ results: RetrievalResult[],
318
+ context: RenderContext,
319
+ ): string {
320
+ if (results.length === 0) return "No results found.";
321
+
322
+ const lines = [
323
+ `Query : ${context.query}`,
324
+ `Profile : ${context.profile}`,
325
+ `Hits : ${results.length}`,
326
+ "",
327
+ "# ID Score Category Tier Source Date Retrieval Path File / Snippet",
328
+ "-- -------- ----- ------------ ---------- ------- ---------- -------------------- --------------",
329
+ ];
330
+
331
+ for (let i = 0; i < results.length; i++) {
332
+ const row = buildSearchRow(i, context.query, results[i]);
333
+ lines.push(`${row[0]} ${row[1]} ${row[2]} ${row[3]} ${row[4]} ${row[5]} ${row[6]} ${row[7]} ${row[8]} | ${row[9]}`);
334
+ lines.push(` prov : ${getProvenanceSummary(results[i])}`);
335
+ // Full mode: append metadata details
336
+ const meta = parseMetadata(results[i].entry);
337
+ const evolution = typeof meta.evolutionStatus === "string" ? meta.evolutionStatus : "-";
338
+ const accessCount = typeof meta.accessCount === "number" ? String(meta.accessCount) : "-";
339
+ const importance = results[i].entry.importance.toFixed(2);
340
+ const tags = Array.isArray(meta.tags) ? (meta.tags as string[]).join(", ") : "-";
341
+ // Emotion metadata (from emotion-detector)
342
+ const emotionPart = meta.emotion && typeof meta.emotion === "object"
343
+ ? ` emotion=${(meta.emotion as Record<string, unknown>).label ?? "-"}(v=${(meta.emotion as Record<string, unknown>).valence ?? 0},a=${(meta.emotion as Record<string, unknown>).arousal ?? 0})`
344
+ : "";
345
+ // HP-narrative: Narrative metadata (from narrative-tagger)
346
+ const narrative = parseNarrative(results[i].entry.metadata);
347
+ const narrativePart = narrative
348
+ ? ` narrative=${narrative.lifePeriodLabel}/${narrative.generalEventLabel}`
349
+ : "";
350
+ // F1: Confidence metadata
351
+ const confMeta = getConfidenceMetadata(results[i].entry);
352
+ const confPart = confMeta
353
+ ? ` confidence=${confMeta.score.toFixed(2)}(${confMeta.reliability})`
354
+ : "";
355
+ lines.push(` meta : evolution=${evolution} accessCount=${accessCount} importance=${importance} tags=[${tags}]${confPart}${emotionPart}${narrativePart}`);
356
+ }
357
+
358
+ return lines.join("\n");
359
+ }
360
+
292
361
  export function formatSearchResults(
293
362
  results: RetrievalResult[],
294
363
  context: RenderContext,
@@ -24,18 +24,80 @@ export const ALL_MEMORY_CATEGORIES = [
24
24
  ...LEGACY_MEMORY_CATEGORIES,
25
25
  ] as const;
26
26
 
27
- export const STORE_MEMORY_SOURCES = ["manual", "agent", "api"] as const;
27
+ export const STORE_MEMORY_SOURCES = ["manual", "agent", "api", "session_distill", "conversation_import"] as const;
28
28
 
29
29
  export const DurableMemoryCategorySchema = z.enum(DURABLE_MEMORY_CATEGORIES);
30
30
  export const MemoryCategorySchema = z.enum(ALL_MEMORY_CATEGORIES);
31
31
  export const StoreMemorySourceSchema = z.enum(STORE_MEMORY_SOURCES);
32
- export const WriteDispositionSchema = z.enum(["stored", "updated", "deduped", "promoted", "conflict"]);
32
+ export const WriteDispositionSchema = z.enum(["stored", "updated", "deduped", "promoted", "conflict", "rejected"]);
33
33
 
34
34
  export type DurableMemoryCategory = z.infer<typeof DurableMemoryCategorySchema>;
35
35
  export type MemoryCategoryValue = z.infer<typeof MemoryCategorySchema>;
36
36
  export type StoreMemorySource = z.infer<typeof StoreMemorySourceSchema>;
37
37
  export type WriteDisposition = z.infer<typeof WriteDispositionSchema>;
38
38
 
39
+ // --- Emotional Valence (Philosophy of Memory: Affective Memory) ---
40
+ export interface EmotionMetadata {
41
+ /** Negative (-1) to Positive (+1) */
42
+ valence: number;
43
+ /** Calm (0) to Excited (1) */
44
+ arousal: number;
45
+ /** Human-readable label */
46
+ label?: string;
47
+ /** Composite mnemonic significance: (|valence| + arousal) / 2, range 0-1 */
48
+ salience?: number;
49
+ /** Detection method */
50
+ source?: "keyword" | "llm" | "user";
51
+ }
52
+
53
+ export const EmotionMetadataSchema = z.object({
54
+ valence: z.number().min(-1).max(1),
55
+ arousal: z.number().min(0).max(1),
56
+ label: z.string().max(30).optional(),
57
+ salience: z.number().min(0).max(1).optional(),
58
+ source: z.enum(["keyword", "llm", "user"]).optional(),
59
+ });
60
+
61
+ /** Parse emotion from metadata JSON string, returns null if absent */
62
+ export function parseEmotion(metadata: string | undefined): EmotionMetadata | null {
63
+ if (!metadata) return null;
64
+ try {
65
+ const parsed = JSON.parse(metadata);
66
+ if (parsed.emotion && typeof parsed.emotion.valence === "number") {
67
+ return EmotionMetadataSchema.parse(parsed.emotion);
68
+ }
69
+ } catch { /* malformed metadata - safe to ignore */ }
70
+ return null;
71
+ }
72
+
73
+ /** Default neutral emotion for memories without emotion data */
74
+ export const NEUTRAL_EMOTION: EmotionMetadata = { valence: 0, arousal: 0, label: "neutral" };
75
+
76
+ // --- Privacy Tier (Philosophy of Memory: Ethics Layer) ---
77
+ /**
78
+ * Privacy tiers control how aggressively a memory is persisted, extracted, and shared.
79
+ *
80
+ * - ephemeral: auto-expire, no KG extraction, no sharing — session scratch
81
+ * - private: persist but no KG extraction, no sharing — personal notes
82
+ * - durable: full lifecycle, KG extraction, standard decay — default
83
+ * - shared: marked safe for cross-scope retrieval — collaborative knowledge
84
+ */
85
+ export const PRIVACY_TIERS = ["ephemeral", "private", "durable", "shared"] as const;
86
+ export type PrivacyTier = (typeof PRIVACY_TIERS)[number];
87
+ export const PrivacyTierSchema = z.enum(PRIVACY_TIERS);
88
+
89
+ /** Parse privacyTier from metadata JSON string, returns "durable" (default) if absent */
90
+ export function parsePrivacyTier(metadata: string | undefined): PrivacyTier {
91
+ if (!metadata) return "durable";
92
+ try {
93
+ const parsed = JSON.parse(metadata);
94
+ if (parsed.privacyTier && PRIVACY_TIERS.includes(parsed.privacyTier)) {
95
+ return parsed.privacyTier;
96
+ }
97
+ } catch { /* malformed metadata - safe default */ }
98
+ return "durable";
99
+ }
100
+
39
101
  export const MemoryTextSchema = boundedStringSchema("text", 4000);
40
102
  export const MemoryScopeSchema = optionalBoundedStringSchema(160);
41
103
  export const RequiredMemoryScopeSchema = boundedStringSchema("scope", 160);
@@ -64,6 +126,8 @@ export const StoreMemoryInputSchema = z.object({
64
126
  source: StoreMemorySourceSchema.default("manual"),
65
127
  tags: MemoryTagsSchema,
66
128
  canonicalKey: CanonicalKeySchema,
129
+ topicTag: z.string().min(1).max(60).optional(),
130
+ privacyTier: PrivacyTierSchema.default("durable"),
67
131
  });
68
132
 
69
133
  export const StoredMemoryRecordSchema = StoreMemoryInputSchema.extend({
@@ -182,3 +246,11 @@ export type CaseMemoryInput = z.infer<typeof CaseMemoryInputSchema>;
182
246
  export type StoredCaseMemoryRecord = z.infer<typeof StoredCaseMemoryRecordSchema>;
183
247
  export type PromoteMemoryInput = z.infer<typeof PromoteMemoryInputSchema>;
184
248
  export type StoredPromotedMemoryRecord = z.infer<typeof StoredPromotedMemoryRecordSchema>;
249
+
250
+ export function isEmotionScoringEnabled(): boolean {
251
+ return process.env.RECALLNEST_EMOTION_SCORING === "true";
252
+ }
253
+
254
+ export function isPredictiveMemoryEnabled(): boolean {
255
+ return process.env.RECALLNEST_PREDICTIVE_MEMORY === "true";
256
+ }