recallnest 2.3.0 → 2.5.4

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 (221) hide show
  1. package/CHANGELOG.md +150 -0
  2. package/README.md +17 -31
  3. package/README_CN.md +12 -22
  4. package/assets/ui/app.js +1031 -0
  5. package/assets/ui/index.html +224 -0
  6. package/assets/ui/styles.css +781 -0
  7. package/config.json.example +4 -1
  8. package/docs/api-reference.md +1291 -0
  9. package/docs/architecture.md +168 -0
  10. package/docs/connector-spec.md +126 -0
  11. package/eval/cases-canary.json +58 -0
  12. package/eval/cases-scope-robustness.json +278 -0
  13. package/eval/cases.json +117 -20
  14. package/package.json +22 -1
  15. package/src/__tests__/access-tracker-novelty.test.ts +238 -0
  16. package/src/__tests__/activity-counter.test.ts +125 -0
  17. package/src/__tests__/adaptive-fidelity.test.ts +102 -0
  18. package/src/__tests__/admission-control.test.ts +206 -0
  19. package/src/__tests__/anchor-generator.test.ts +67 -0
  20. package/src/__tests__/auto-consolidation.test.ts +168 -0
  21. package/src/__tests__/auto-recall.test.ts +131 -0
  22. package/src/__tests__/babel-integration.test.ts +144 -0
  23. package/src/__tests__/batch-dedup.test.ts +65 -0
  24. package/src/__tests__/canary-eval.test.ts +114 -0
  25. package/src/__tests__/capture-engine.test.ts +1238 -0
  26. package/src/__tests__/capture-heuristic.test.ts +171 -0
  27. package/src/__tests__/cascade-forget.test.ts +210 -0
  28. package/src/__tests__/category-min-scores.test.ts +33 -0
  29. package/src/__tests__/cc8-cc1-essential-context.test.ts +298 -0
  30. package/src/__tests__/circuit-breaker.test.ts +99 -0
  31. package/src/__tests__/cluster-consolidation.test.ts +782 -0
  32. package/src/__tests__/confidence-metadata.test.ts +125 -0
  33. package/src/__tests__/confidence-tracker.test.ts +261 -0
  34. package/src/__tests__/conflict-advisor.test.ts +206 -0
  35. package/src/__tests__/conflict-escalation.test.ts +149 -0
  36. package/src/__tests__/conflict-lifecycle.test.ts +83 -0
  37. package/src/__tests__/conflict-output.test.ts +85 -0
  38. package/src/__tests__/conflict-store.test.ts +374 -0
  39. package/src/__tests__/connector-v1.test.ts +181 -0
  40. package/src/__tests__/consolidation-engine.test.ts +333 -0
  41. package/src/__tests__/constructive-retrieval.test.ts +509 -0
  42. package/src/__tests__/context-collapse-renderer.test.ts +214 -0
  43. package/src/__tests__/context-composer-pins.test.ts +64 -0
  44. package/src/__tests__/context-composer-task-selection.test.ts +24 -0
  45. package/src/__tests__/context-reconstructor.test.ts +65 -0
  46. package/src/__tests__/context-renderer.test.ts +91 -0
  47. package/src/__tests__/continuity-eval.test.ts +275 -0
  48. package/src/__tests__/continuity-schema.test.ts +97 -0
  49. package/src/__tests__/contracts.test.ts +216 -0
  50. package/src/__tests__/conversation-importer.test.ts +692 -0
  51. package/src/__tests__/core-recall-eval.test.ts +235 -0
  52. package/src/__tests__/core-summary.test.ts +109 -0
  53. package/src/__tests__/data-checkup.test.ts +276 -0
  54. package/src/__tests__/decay-exemption.test.ts +84 -0
  55. package/src/__tests__/dedup-actions.test.ts +58 -0
  56. package/src/__tests__/dedup-guard.test.ts +39 -0
  57. package/src/__tests__/deterministic-id.test.ts +39 -0
  58. package/src/__tests__/distill-lock.test.ts +305 -0
  59. package/src/__tests__/distill-session-argshape.test.ts +79 -0
  60. package/src/__tests__/doctor.test.ts +319 -0
  61. package/src/__tests__/dream-pipeline.test.ts +321 -0
  62. package/src/__tests__/drill-down.test.ts +108 -0
  63. package/src/__tests__/embedder.test.ts +122 -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__/entity-resolver.test.ts +58 -0
  69. package/src/__tests__/env-config.test.ts +151 -0
  70. package/src/__tests__/error-signature-boost.test.ts +87 -0
  71. package/src/__tests__/error-signature.test.ts +95 -0
  72. package/src/__tests__/error-taxonomy.test.ts +129 -0
  73. package/src/__tests__/ethics-layer.test.ts +476 -0
  74. package/src/__tests__/event-segmenter.test.ts +153 -0
  75. package/src/__tests__/feature-flags-integration.test.ts +54 -0
  76. package/src/__tests__/frequency-tracker.test.ts +113 -0
  77. package/src/__tests__/graph-export.test.ts +489 -0
  78. package/src/__tests__/hotness-blend.test.ts +76 -0
  79. package/src/__tests__/ingest-chunking.test.ts +44 -0
  80. package/src/__tests__/ingest.test.ts +360 -0
  81. package/src/__tests__/interference-detector.test.ts +203 -0
  82. package/src/__tests__/kg-extractor.test.ts +192 -0
  83. package/src/__tests__/kg-store.test.ts +300 -0
  84. package/src/__tests__/llm-consolidation.test.ts +226 -0
  85. package/src/__tests__/maintenance-locks.test.ts +174 -0
  86. package/src/__tests__/mcp-efficiency.test.ts +288 -0
  87. package/src/__tests__/mcp-related-scopes.test.ts +151 -0
  88. package/src/__tests__/memory-boundaries.test.ts +250 -0
  89. package/src/__tests__/memory-evolution-b2-b3-e1.test.ts +345 -0
  90. package/src/__tests__/memory-evolution.test.ts +271 -0
  91. package/src/__tests__/memory-health-rebalance.test.ts +144 -0
  92. package/src/__tests__/memory-lint.test.ts +421 -0
  93. package/src/__tests__/memory-output.test.ts +215 -0
  94. package/src/__tests__/memory-promotion.test.ts +315 -0
  95. package/src/__tests__/migrate-dedup-ids.test.ts +65 -0
  96. package/src/__tests__/multi-vector.test.ts +242 -0
  97. package/src/__tests__/narrative-architecture.test.ts +456 -0
  98. package/src/__tests__/noise-filter.test.ts +205 -0
  99. package/src/__tests__/obsidian-connector.test.ts +363 -0
  100. package/src/__tests__/omitted-reporting.test.ts +98 -0
  101. package/src/__tests__/pending-review.test.ts +77 -0
  102. package/src/__tests__/pii-redact.test.ts +314 -0
  103. package/src/__tests__/post-retrieval-filter.test.ts +178 -0
  104. package/src/__tests__/ppr-traversal.test.ts +256 -0
  105. package/src/__tests__/prediction-engine.test.ts +349 -0
  106. package/src/__tests__/predictive-prospective-memory.test.ts +416 -0
  107. package/src/__tests__/preference-guard-smoke.test.ts +211 -0
  108. package/src/__tests__/preference-matcher.test.ts +230 -0
  109. package/src/__tests__/preference-slots.test.ts +205 -0
  110. package/src/__tests__/prospective-memory.test.ts +283 -0
  111. package/src/__tests__/query-entity-detector.test.ts +171 -0
  112. package/src/__tests__/query-expander-alias.test.ts +146 -0
  113. package/src/__tests__/query-expander.test.ts +34 -0
  114. package/src/__tests__/recall-governor.test.ts +227 -0
  115. package/src/__tests__/recall-mode.test.ts +64 -0
  116. package/src/__tests__/result-synthesizer.test.ts +198 -0
  117. package/src/__tests__/resume-context.test.ts +4898 -0
  118. package/src/__tests__/retrieval-trace.test.ts +213 -0
  119. package/src/__tests__/retriever-boundary-weight.test.ts +136 -0
  120. package/src/__tests__/retroactive-boost.test.ts +223 -0
  121. package/src/__tests__/salience-scoring.test.ts +106 -0
  122. package/src/__tests__/scope-inventory.test.ts +200 -0
  123. package/src/__tests__/scope-policy.test.ts +69 -0
  124. package/src/__tests__/scope-suggester.test.ts +80 -0
  125. package/src/__tests__/session-checkpoint.test.ts +291 -0
  126. package/src/__tests__/session-distiller.test.ts +640 -0
  127. package/src/__tests__/session-gc.test.ts +215 -0
  128. package/src/__tests__/skill-engine.test.ts +552 -0
  129. package/src/__tests__/skill-promotion.test.ts +601 -0
  130. package/src/__tests__/skill-verifier.test.ts +122 -0
  131. package/src/__tests__/source-heartbeat.test.ts +250 -0
  132. package/src/__tests__/store-patch-metadata.test.ts +205 -0
  133. package/src/__tests__/store-scope-audit.test.ts +81 -0
  134. package/src/__tests__/store-upsert.test.ts +184 -0
  135. package/src/__tests__/supersede-chain.test.ts +154 -0
  136. package/src/__tests__/temporal-parser.test.ts +241 -0
  137. package/src/__tests__/temporal-validity.test.ts +160 -0
  138. package/src/__tests__/term-registry.test.ts +121 -0
  139. package/src/__tests__/tool-error-surfacing.test.ts +98 -0
  140. package/src/__tests__/tool-output-compressor.test.ts +160 -0
  141. package/src/__tests__/topic-tag.test.ts +113 -0
  142. package/src/__tests__/trusted-memory.test.ts +839 -0
  143. package/src/__tests__/usage-tracker.test.ts +287 -0
  144. package/src/__tests__/version-manager.test.ts +213 -0
  145. package/src/__tests__/workflow-observation-skill-outcome.test.ts +352 -0
  146. package/src/__tests__/workflow-observation.test.ts +433 -0
  147. package/src/__tests__/write-verifier.test.ts +122 -0
  148. package/src/access-tracker.ts +38 -32
  149. package/src/activity-counter.ts +46 -18
  150. package/src/aliases.ts +133 -0
  151. package/src/api-server.ts +27 -7
  152. package/src/audit-log.ts +4 -2
  153. package/src/auto-consolidation.ts +8 -3
  154. package/src/auto-gc.ts +85 -21
  155. package/src/capture-engine.ts +54 -21
  156. package/src/cli.ts +169 -37
  157. package/src/confidence-tracker.ts +36 -18
  158. package/src/consolidation-engine.ts +109 -11
  159. package/src/context-composer.ts +8 -1
  160. package/src/data-checkup.ts +95 -6
  161. package/src/distill-lock.ts +194 -15
  162. package/src/doctor.ts +9 -2
  163. package/src/dream-pipeline.ts +118 -34
  164. package/src/env-config.ts +77 -0
  165. package/src/error-signature.ts +153 -0
  166. package/src/error-taxonomy.ts +115 -0
  167. package/src/eval.ts +251 -6
  168. package/src/idempotency.ts +8 -0
  169. package/src/ingest.ts +31 -10
  170. package/src/kg-extractor.ts +6 -2
  171. package/src/kg-store.ts +236 -44
  172. package/src/llm-consolidation.ts +2 -1
  173. package/src/mcp-server.ts +85 -1799
  174. package/src/mcp-tool-deps.ts +48 -0
  175. package/src/mcp-tools-advanced.ts +941 -0
  176. package/src/mcp-tools-core.ts +633 -0
  177. package/src/mcp-tools-governance.ts +475 -0
  178. package/src/memory-boundaries.ts +3 -1
  179. package/src/memory-evolution.ts +73 -1
  180. package/src/memory-lint.ts +183 -10
  181. package/src/memory-output.ts +114 -3
  182. package/src/memory-promotion.ts +246 -0
  183. package/src/memory-schema.ts +25 -2
  184. package/src/memory-store-port.ts +43 -0
  185. package/src/multi-vector.ts +2 -1
  186. package/src/narrative-schema.ts +2 -1
  187. package/src/noise-filter.ts +57 -6
  188. package/src/pii-detector.ts +157 -5
  189. package/src/query-expander.ts +90 -9
  190. package/src/result-synthesizer.ts +2 -1
  191. package/src/retention-policy.ts +2 -1
  192. package/src/retriever.ts +259 -139
  193. package/src/runtime-config.ts +8 -1
  194. package/src/scope-suggester.ts +113 -0
  195. package/src/session-distiller.ts +23 -2
  196. package/src/session-engine.ts +2 -1
  197. package/src/session-schema.ts +1 -0
  198. package/src/session-store.ts +5 -0
  199. package/src/skill-engine.ts +78 -0
  200. package/src/skill-promotion.ts +247 -47
  201. package/src/skill-schema.ts +16 -3
  202. package/src/skill-verifier.ts +152 -0
  203. package/src/store.ts +267 -38
  204. package/src/tool-output-compressor.ts +3 -1
  205. package/src/ui-server.ts +2 -1
  206. package/src/usage-tracker.ts +242 -0
  207. package/src/version-manager.ts +3 -2
  208. package/src/workflow-observation-engine.ts +2 -1
  209. package/src/workflow-observation-schema.ts +2 -1
  210. package/src/workflow-observation-store.ts +6 -2
  211. package/.env +0 -4
  212. package/CLAUDE.md +0 -46
  213. package/config.json +0 -55
  214. package/daily-recall.sh +0 -110
  215. package/logs/daily-recall.error.log +0 -0
  216. package/logs/daily-recall.log +0 -60
  217. package/logs/ingest-2026-04-11.log +0 -110
  218. package/logs/ingest-2026-04-12.log +0 -90
  219. package/logs/ingest-2026-04-13.log +0 -87
  220. package/logs/launchagent-stderr.log +0 -0
  221. package/logs/launchagent-stdout.log +0 -0
@@ -0,0 +1,108 @@
1
+ import { describe, it, expect } from "bun:test";
2
+
3
+ describe("Tiered storage: L0/L1/L2 metadata parsing", () => {
4
+ it("parses L0/L1/L2 from metadata JSON", () => {
5
+ const metadata = JSON.stringify({
6
+ l0_abstract: "User prefers TypeScript",
7
+ l1_overview: "User consistently chooses TypeScript over JavaScript for new projects, citing type safety.",
8
+ l2_content: "Full detailed content here...",
9
+ });
10
+ const parsed = JSON.parse(metadata);
11
+ expect(parsed.l0_abstract).toBe("User prefers TypeScript");
12
+ expect(parsed.l1_overview.length).toBeGreaterThan(parsed.l0_abstract.length);
13
+ });
14
+
15
+ it("handles missing L0/L1 fields gracefully", () => {
16
+ const metadata = JSON.stringify({ accessCount: 5 });
17
+ const parsed = JSON.parse(metadata);
18
+ expect(parsed.l0_abstract).toBeUndefined();
19
+ expect(parsed.l1_overview).toBeUndefined();
20
+ });
21
+
22
+ it("handles malformed metadata JSON", () => {
23
+ const metadata = "not-json";
24
+ let parsed = null;
25
+ try {
26
+ parsed = JSON.parse(metadata);
27
+ } catch {
28
+ // Expected
29
+ }
30
+ expect(parsed).toBeNull();
31
+ });
32
+ });
33
+
34
+ describe("Tiered storage: drill-down level selection", () => {
35
+ it("overview level returns L1 when available", () => {
36
+ const meta = {
37
+ l0_abstract: "Short summary",
38
+ l1_overview: "Detailed overview with context and reasoning",
39
+ };
40
+ const fullText = "Very long full text content...";
41
+ const level = "overview";
42
+
43
+ const result = level === "overview" && meta.l1_overview
44
+ ? meta.l1_overview
45
+ : fullText;
46
+
47
+ expect(result).toBe(meta.l1_overview);
48
+ });
49
+
50
+ it("overview level falls back to full text when L1 missing", () => {
51
+ const meta: Record<string, string> = { l0_abstract: "Short summary" };
52
+ const fullText = "Very long full text content...";
53
+ const level = "overview";
54
+
55
+ const result = level === "overview" && meta.l1_overview
56
+ ? meta.l1_overview
57
+ : fullText;
58
+
59
+ expect(result).toBe(fullText);
60
+ });
61
+
62
+ it("full level always returns full text", () => {
63
+ const meta = {
64
+ l0_abstract: "Short summary",
65
+ l1_overview: "Detailed overview",
66
+ };
67
+ const fullText = "Very long full text content...";
68
+ const level = "full";
69
+
70
+ const result = level === "full" ? fullText : meta.l1_overview || fullText;
71
+ expect(result).toBe(fullText);
72
+ });
73
+ });
74
+
75
+ describe("Tiered storage: recallDepthDefault config", () => {
76
+ it("accepts valid depth values", () => {
77
+ for (const depth of ["l0", "l1", "full"]) {
78
+ expect(["l0", "l1", "full"].includes(depth)).toBe(true);
79
+ }
80
+ });
81
+
82
+ it("rejects invalid depth values", () => {
83
+ for (const depth of ["l3", "summary", "compact"]) {
84
+ expect(["l0", "l1", "full"].includes(depth)).toBe(false);
85
+ }
86
+ });
87
+
88
+ it("defaults to full when not set (backward compat)", () => {
89
+ const configValue = undefined;
90
+ const resolved = configValue || "full";
91
+ expect(resolved).toBe("full");
92
+ });
93
+ });
94
+
95
+ describe("Tiered storage: L0 injection format", () => {
96
+ it("L0 abstract is compact enough for context injection", () => {
97
+ const l0 = "User prefers TypeScript for all new projects";
98
+ // L0 should be under 100 tokens (~400 chars)
99
+ expect(l0.length).toBeLessThan(400);
100
+ });
101
+
102
+ it("L1 overview provides medium detail", () => {
103
+ const l1 = "## Preference\nUser consistently chooses TypeScript over JavaScript. Cited reasons: type safety, better IDE support, easier refactoring.";
104
+ // L1 should be under 500 tokens (~2000 chars)
105
+ expect(l1.length).toBeLessThan(2000);
106
+ expect(l1.length).toBeGreaterThan(50);
107
+ });
108
+ });
@@ -0,0 +1,122 @@
1
+ import { describe, expect, it } from "bun:test";
2
+
3
+ import { Embedder } from "../embedder.js";
4
+
5
+ describe("Embedder transient retry", () => {
6
+ it("retries a transient single-embedding connection error", async () => {
7
+ const embedder = new Embedder({
8
+ provider: "openai-compatible",
9
+ apiKey: "test-key",
10
+ model: "text-embedding-3-small",
11
+ dimensions: 3,
12
+ });
13
+
14
+ let calls = 0;
15
+ (embedder as any).client = {
16
+ embeddings: {
17
+ create: async () => {
18
+ calls += 1;
19
+ if (calls === 1) {
20
+ throw new Error("Connection error. ECONNRESET");
21
+ }
22
+ return {
23
+ data: [{ embedding: [1, 2, 3] }],
24
+ };
25
+ },
26
+ },
27
+ };
28
+
29
+ await expect(embedder.embedPassage("hello")).resolves.toEqual([1, 2, 3]);
30
+ expect(calls).toBe(2);
31
+ });
32
+
33
+ it("retries a transient batch embedding connection error", async () => {
34
+ const embedder = new Embedder({
35
+ provider: "openai-compatible",
36
+ apiKey: "test-key",
37
+ model: "text-embedding-3-small",
38
+ dimensions: 3,
39
+ });
40
+
41
+ let calls = 0;
42
+ (embedder as any).client = {
43
+ embeddings: {
44
+ create: async () => {
45
+ calls += 1;
46
+ if (calls === 1) {
47
+ throw new Error("The socket connection was closed unexpectedly. ECONNRESET");
48
+ }
49
+ return {
50
+ data: [
51
+ { embedding: [1, 2, 3] },
52
+ { embedding: [4, 5, 6] },
53
+ ],
54
+ };
55
+ },
56
+ },
57
+ };
58
+
59
+ await expect(embedder.embedBatchPassage(["a", "b"])).resolves.toEqual([
60
+ [1, 2, 3],
61
+ [4, 5, 6],
62
+ ]);
63
+ expect(calls).toBe(2);
64
+ });
65
+
66
+ it("does not retry non-transient embedding errors", async () => {
67
+ const embedder = new Embedder({
68
+ provider: "openai-compatible",
69
+ apiKey: "test-key",
70
+ model: "text-embedding-3-small",
71
+ dimensions: 3,
72
+ });
73
+
74
+ let calls = 0;
75
+ (embedder as any).client = {
76
+ embeddings: {
77
+ create: async () => {
78
+ calls += 1;
79
+ throw new Error("Authentication failed");
80
+ },
81
+ },
82
+ };
83
+
84
+ await expect(embedder.embedPassage("hello")).rejects.toThrow("Failed to generate embedding: Authentication failed");
85
+ expect(calls).toBe(1);
86
+ });
87
+
88
+ it("limits concurrent embedding requests", async () => {
89
+ const embedder = new Embedder({
90
+ provider: "openai-compatible",
91
+ apiKey: "test-key",
92
+ model: "text-embedding-3-small",
93
+ dimensions: 3,
94
+ });
95
+
96
+ (embedder as any)._maxConcurrentRequests = 1;
97
+
98
+ let inFlight = 0;
99
+ let maxInFlight = 0;
100
+ (embedder as any).client = {
101
+ embeddings: {
102
+ create: async () => {
103
+ inFlight += 1;
104
+ maxInFlight = Math.max(maxInFlight, inFlight);
105
+ await new Promise((resolve) => setTimeout(resolve, 20));
106
+ inFlight -= 1;
107
+ return {
108
+ data: [{ embedding: [1, 2, 3] }],
109
+ };
110
+ },
111
+ },
112
+ };
113
+
114
+ await Promise.all([
115
+ embedder.embedPassage("first"),
116
+ embedder.embedPassage("second"),
117
+ embedder.embedPassage("third"),
118
+ ]);
119
+
120
+ expect(maxInFlight).toBe(1);
121
+ });
122
+ });
@@ -0,0 +1,46 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import { weibullDecay, adjustHalfLifeForEmotion, computeArousalBoost } from "../decay-engine.js";
3
+
4
+ describe("emotion-adjusted decay", () => {
5
+ test("returns base half-life when no emotion", () => {
6
+ expect(adjustHalfLifeForEmotion(60, undefined)).toBe(60);
7
+ expect(adjustHalfLifeForEmotion(60, null)).toBe(60);
8
+ });
9
+
10
+ test("strong negative emotion extends half-life ~27%", () => {
11
+ const adjusted = adjustHalfLifeForEmotion(60, { valence: -0.9, arousal: 0.5, label: "frustration" });
12
+ expect(adjusted).toBeCloseTo(60 * 1.27, 0);
13
+ });
14
+
15
+ test("strong positive emotion extends half-life ~24%", () => {
16
+ const adjusted = adjustHalfLifeForEmotion(60, { valence: 0.8, arousal: 0.3, label: "excitement" });
17
+ expect(adjusted).toBeCloseTo(60 * 1.24, 0);
18
+ });
19
+
20
+ test("neutral emotion has negligible effect", () => {
21
+ const adjusted = adjustHalfLifeForEmotion(60, { valence: 0.05, arousal: 0.1, label: "neutral" });
22
+ expect(adjusted).toBeCloseTo(60, 0);
23
+ });
24
+
25
+ test("emotional memory decays slower at 30 days", () => {
26
+ const baseHL = 60;
27
+ const emotionalHL = adjustHalfLifeForEmotion(baseHL, { valence: -0.8, arousal: 0.7, label: "frustration" });
28
+ const neutralDecay = weibullDecay(30, baseHL, "working");
29
+ const emotionalDecay = weibullDecay(30, emotionalHL, "working");
30
+ expect(emotionalDecay).toBeGreaterThan(neutralDecay);
31
+ });
32
+
33
+ test("arousal boost is 1.0 for zero arousal", () => {
34
+ expect(computeArousalBoost({ valence: 0.5, arousal: 0, label: "positive" })).toBe(1.0);
35
+ });
36
+
37
+ test("arousal boost max ~1.1 for high arousal", () => {
38
+ const boost = computeArousalBoost({ valence: 0.5, arousal: 0.9, label: "excitement" });
39
+ expect(boost).toBeCloseTo(1.09, 1);
40
+ });
41
+
42
+ test("arousal boost is 1.0 for null emotion", () => {
43
+ expect(computeArousalBoost(undefined)).toBe(1.0);
44
+ expect(computeArousalBoost(null)).toBe(1.0);
45
+ });
46
+ });
@@ -0,0 +1,56 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import { detectEmotion } from "../emotion-detector.js";
3
+
4
+ describe("emotion-detector: heuristic", () => {
5
+ test("detects strong negative text", () => {
6
+ const result = detectEmotion("This bug is driving me crazy, everything is broken and wrong");
7
+ expect(result.valence).toBeLessThan(-0.3);
8
+ expect(result.label).toBe("negative");
9
+ });
10
+
11
+ test("detects strong positive text", () => {
12
+ const result = detectEmotion("Finally solved it! Works perfectly, great progress today");
13
+ expect(result.valence).toBeGreaterThan(0.3);
14
+ expect(result.label).toBe("positive");
15
+ });
16
+
17
+ test("detects neutral text", () => {
18
+ const result = detectEmotion("The user prefers dark mode and uses VS Code");
19
+ expect(Math.abs(result.valence)).toBeLessThan(0.3);
20
+ expect(result.label).toBe("neutral");
21
+ });
22
+
23
+ test("detects high arousal", () => {
24
+ const result = detectEmotion("URGENT! Critical production issue, fix immediately!");
25
+ expect(result.arousal).toBeGreaterThan(0.3);
26
+ });
27
+
28
+ test("detects low arousal for calm text", () => {
29
+ const result = detectEmotion("The default configuration uses port 3000");
30
+ expect(result.arousal).toBeLessThan(0.3);
31
+ });
32
+
33
+ test("handles mixed signals with net positive", () => {
34
+ const result = detectEmotion("Had a frustrating bug but finally solved it perfectly");
35
+ expect(result.valence).toBeGreaterThan(0);
36
+ });
37
+
38
+ test("handles empty text gracefully", () => {
39
+ const result = detectEmotion("");
40
+ expect(result.valence).toBe(0);
41
+ expect(result.arousal).toBe(0);
42
+ expect(result.label).toBe("neutral");
43
+ });
44
+
45
+ test("clamps valence to [-1, 1]", () => {
46
+ const result = detectEmotion("broken error wrong bug failure hate broken error wrong");
47
+ expect(result.valence).toBeGreaterThanOrEqual(-1);
48
+ expect(result.valence).toBeLessThanOrEqual(1);
49
+ });
50
+
51
+ test("clamps arousal to [0, 1]", () => {
52
+ const result = detectEmotion("URGENT! CRITICAL! IMMEDIATELY! ASAP!");
53
+ expect(result.arousal).toBeGreaterThanOrEqual(0);
54
+ expect(result.arousal).toBeLessThanOrEqual(1);
55
+ });
56
+ });
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Phase 1 integration tests: Emotional Valence in Decay pipeline.
3
+ *
4
+ * Validates the full wiring:
5
+ * 1. detectEmotion → salience + source fields
6
+ * 2. computeDecayScore → emotion-aware weights (0.15/0.25/0.45/0.15)
7
+ * 3. Backward compatibility when emotion flag is off or metadata absent
8
+ */
9
+ import { describe, test, expect } from "bun:test";
10
+ import { detectEmotion } from "../emotion-detector.js";
11
+ import { computeDecayScore, type EvolutionMetadata } from "../memory-evolution.js";
12
+
13
+ // ---------------------------------------------------------------------------
14
+ // 1. detectEmotion: salience + source fields
15
+ // ---------------------------------------------------------------------------
16
+
17
+ describe("detectEmotion salience & source", () => {
18
+ test("returns salience field as (|valence| + arousal) / 2", () => {
19
+ const result = detectEmotion("Finally fixed that horrible production bug after 3 days!");
20
+ expect(result.salience).toBeDefined();
21
+ expect(typeof result.salience).toBe("number");
22
+ // salience = (|valence| + arousal) / 2
23
+ const expected = (Math.abs(result.valence) + result.arousal) / 2;
24
+ expect(result.salience).toBeCloseTo(expected, 5);
25
+ });
26
+
27
+ test("returns source: keyword", () => {
28
+ const result = detectEmotion("This is a great success!");
29
+ expect(result.source).toBe("keyword");
30
+ });
31
+
32
+ test("neutral text returns salience 0", () => {
33
+ const result = detectEmotion("The meeting is at 3pm tomorrow.");
34
+ expect(result.salience).toBe(0);
35
+ });
36
+
37
+ test("empty text returns salience 0 and source keyword", () => {
38
+ const result = detectEmotion("");
39
+ expect(result.salience).toBe(0);
40
+ expect(result.source).toBe("keyword");
41
+ });
42
+
43
+ test("high emotion + high arousal gives high salience", () => {
44
+ const result = detectEmotion("CRITICAL BUG! Everything is broken! Urgent fix needed!");
45
+ expect(result.salience).toBeGreaterThan(0.2);
46
+ });
47
+
48
+ test("salience is clamped to [0, 1]", () => {
49
+ const result = detectEmotion("fail fail fail broken broken crash crash urgent urgent critical critical !!!");
50
+ expect(result.salience).toBeGreaterThanOrEqual(0);
51
+ expect(result.salience).toBeLessThanOrEqual(1);
52
+ });
53
+ });
54
+
55
+ // ---------------------------------------------------------------------------
56
+ // 2. computeDecayScore: emotion-aware weights
57
+ // ---------------------------------------------------------------------------
58
+
59
+ describe("computeDecayScore with emotion", () => {
60
+ const now = Date.now();
61
+ const baseEvo: EvolutionMetadata = {
62
+ status: "active",
63
+ version: 1,
64
+ accessCount: 3,
65
+ lastAccessedAt: now - 7 * 86_400_000, // 7 days ago
66
+ supersededBy: null,
67
+ supersedes: null,
68
+ evolutionNote: null,
69
+ consolidatedInto: null,
70
+ contributedToPattern: null,
71
+ sourceMemories: [],
72
+ validFrom: now - 30 * 86_400_000, // 30 days old
73
+ validUntil: null,
74
+ };
75
+
76
+ // Save and restore env
77
+ const origFlag = process.env.RECALLNEST_EMOTION_SCORING;
78
+
79
+ test("with emotion flag ON and metadata, emotional memory scores higher", () => {
80
+ process.env.RECALLNEST_EMOTION_SCORING = "true";
81
+ try {
82
+ const emotionalMeta = JSON.stringify({
83
+ emotion: { valence: -0.9, arousal: 0.8, label: "frustration", salience: 0.85, source: "keyword" },
84
+ });
85
+ const neutralMeta = JSON.stringify({
86
+ emotion: { valence: 0, arousal: 0, label: "neutral", salience: 0, source: "keyword" },
87
+ });
88
+
89
+ const emotionalScore = computeDecayScore(baseEvo, 0.5, now, emotionalMeta);
90
+ const neutralScore = computeDecayScore(baseEvo, 0.5, now, neutralMeta);
91
+
92
+ expect(emotionalScore).toBeGreaterThan(neutralScore);
93
+ } finally {
94
+ process.env.RECALLNEST_EMOTION_SCORING = origFlag;
95
+ }
96
+ });
97
+
98
+ test("with emotion flag ON, weights sum close to 1.0", () => {
99
+ process.env.RECALLNEST_EMOTION_SCORING = "true";
100
+ try {
101
+ // At max values: time=1, freq=1, importance=1, salience=1
102
+ // Score should be 0.15 + 0.25 + 0.45 + 0.15 = 1.0
103
+ const freshEvo: EvolutionMetadata = {
104
+ ...baseEvo,
105
+ accessCount: 100,
106
+ lastAccessedAt: now,
107
+ validFrom: now, // just created
108
+ };
109
+ const maxMeta = JSON.stringify({
110
+ emotion: { valence: 1, arousal: 1, label: "positive", salience: 1, source: "keyword" },
111
+ });
112
+
113
+ const score = computeDecayScore(freshEvo, 1.0, now, maxMeta);
114
+ expect(score).toBeCloseTo(1.0, 1);
115
+ } finally {
116
+ process.env.RECALLNEST_EMOTION_SCORING = origFlag;
117
+ }
118
+ });
119
+
120
+ test("backward compat: no metadata param still works", () => {
121
+ const score = computeDecayScore(baseEvo, 0.5, now);
122
+ expect(score).toBeGreaterThan(0);
123
+ expect(score).toBeLessThanOrEqual(1);
124
+ });
125
+
126
+ test("backward compat: metadata without emotion still works", () => {
127
+ process.env.RECALLNEST_EMOTION_SCORING = "true";
128
+ try {
129
+ const noEmotionMeta = JSON.stringify({ tier: "working" });
130
+ const score = computeDecayScore(baseEvo, 0.5, now, noEmotionMeta);
131
+ expect(score).toBeGreaterThan(0);
132
+ expect(score).toBeLessThanOrEqual(1);
133
+ } finally {
134
+ process.env.RECALLNEST_EMOTION_SCORING = origFlag;
135
+ }
136
+ });
137
+
138
+ test("with emotion flag OFF, metadata is ignored (base weights)", () => {
139
+ process.env.RECALLNEST_EMOTION_SCORING = "";
140
+ try {
141
+ const emotionalMeta = JSON.stringify({
142
+ emotion: { valence: -0.9, arousal: 0.8, label: "frustration", salience: 0.85, source: "keyword" },
143
+ });
144
+
145
+ const withMeta = computeDecayScore(baseEvo, 0.5, now, emotionalMeta);
146
+ const withoutMeta = computeDecayScore(baseEvo, 0.5, now);
147
+
148
+ expect(withMeta).toBeCloseTo(withoutMeta, 5);
149
+ } finally {
150
+ process.env.RECALLNEST_EMOTION_SCORING = origFlag;
151
+ }
152
+ });
153
+ });
@@ -0,0 +1,56 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import { applyEmotionWeight } from "../retriever.js";
3
+
4
+ describe("applyEmotionWeight", () => {
5
+ const makeResult = (id: string, score: number, emotionJson?: object) => ({
6
+ id,
7
+ text: "test",
8
+ vector: [],
9
+ category: "events" as const,
10
+ scope: "test",
11
+ importance: 0.5,
12
+ timestamp: Date.now(),
13
+ metadata: emotionJson ? JSON.stringify({ emotion: emotionJson }) : "{}",
14
+ score,
15
+ sources: {},
16
+ });
17
+
18
+ test("null query emotion returns unchanged", () => {
19
+ const results = [makeResult("a", 0.8, { valence: 0.9, arousal: 0.5 })];
20
+ const output = applyEmotionWeight(results, null);
21
+ expect(output[0].score).toBe(0.8);
22
+ });
23
+
24
+ test("low-valence query returns unchanged", () => {
25
+ const queryEmotion = { valence: 0.1, arousal: 0, label: "neutral" };
26
+ const results = [makeResult("a", 0.8, { valence: 0.9, arousal: 0.5 })];
27
+ const output = applyEmotionWeight(results, queryEmotion);
28
+ expect(output[0].score).toBe(0.8);
29
+ });
30
+
31
+ test("negative query boosts negative memories", () => {
32
+ const queryEmotion = { valence: -0.8, arousal: 0.5, label: "frustration" };
33
+ const results = [
34
+ makeResult("neg", 0.7, { valence: -0.7, arousal: 0.5 }),
35
+ makeResult("pos", 0.7, { valence: 0.8, arousal: 0.3 }),
36
+ ];
37
+ const output = applyEmotionWeight(results, queryEmotion);
38
+ expect(output.find(r => r.id === "neg")!.score).toBeGreaterThan(
39
+ output.find(r => r.id === "pos")!.score
40
+ );
41
+ });
42
+
43
+ test("no emotion data leaves score unchanged", () => {
44
+ const queryEmotion = { valence: -0.8, arousal: 0.5, label: "negative" };
45
+ const results = [makeResult("no-emo", 0.7)];
46
+ const output = applyEmotionWeight(results, queryEmotion);
47
+ expect(output[0].score).toBe(0.7);
48
+ });
49
+
50
+ test("max boost is 15%", () => {
51
+ const queryEmotion = { valence: 1.0, arousal: 1.0, label: "positive" };
52
+ const results = [makeResult("a", 1.0, { valence: 1.0, arousal: 1.0 })];
53
+ const output = applyEmotionWeight(results, queryEmotion);
54
+ expect(output[0].score).toBeCloseTo(1.15, 2);
55
+ });
56
+ });
@@ -0,0 +1,58 @@
1
+ import { describe, expect, it } from "bun:test";
2
+
3
+ import { EntityResolver, BUILTIN_ALIASES, defaultResolver, resolveQueryEntities } from "../entity-resolver.js";
4
+
5
+ describe("EntityResolver", () => {
6
+ it("resolves known aliases", () => {
7
+ expect(defaultResolver.resolve("ts")).toBe("typescript");
8
+ expect(defaultResolver.resolve("TS")).toBe("typescript");
9
+ expect(defaultResolver.resolve("k8s")).toBe("kubernetes");
10
+ expect(defaultResolver.resolve("pg")).toBe("postgresql");
11
+ });
12
+
13
+ it("returns term as-is for unknown aliases", () => {
14
+ expect(defaultResolver.resolve("unknownThing")).toBe("unknownThing");
15
+ expect(defaultResolver.resolve("")).toBe("");
16
+ });
17
+
18
+ it("supports user aliases that override builtins", () => {
19
+ const resolver = new EntityResolver(BUILTIN_ALIASES, { "ts": "my-custom-ts" });
20
+ expect(resolver.resolve("ts")).toBe("my-custom-ts");
21
+ });
22
+
23
+ it("resolveText replaces aliases in context", () => {
24
+ const text = "I'm using TS with k8s and pg for the backend";
25
+ const resolved = defaultResolver.resolveText(text);
26
+ expect(resolved).toContain("typescript");
27
+ expect(resolved).toContain("kubernetes");
28
+ expect(resolved).toContain("postgresql");
29
+ });
30
+
31
+ it("resolveText handles longer aliases first", () => {
32
+ const text = "Using docker-compose for local dev";
33
+ const resolved = defaultResolver.resolveText(text);
34
+ expect(resolved).toContain("docker compose");
35
+ });
36
+
37
+ it("hasAlias returns true for known aliases", () => {
38
+ expect(defaultResolver.hasAlias("ts")).toBe(true);
39
+ expect(defaultResolver.hasAlias("unknown")).toBe(false);
40
+ });
41
+
42
+ it("size returns the number of aliases", () => {
43
+ expect(defaultResolver.size).toBeGreaterThan(40);
44
+ });
45
+ });
46
+
47
+ describe("resolveQueryEntities", () => {
48
+ it("normalizes entities in a search query", () => {
49
+ const result = resolveQueryEntities("search for ts config in k8s");
50
+ expect(result).toContain("typescript");
51
+ expect(result).toContain("kubernetes");
52
+ });
53
+
54
+ it("passes through queries with no aliases", () => {
55
+ const result = resolveQueryEntities("how to deploy a web app");
56
+ expect(result).toBe("how to deploy a web app");
57
+ });
58
+ });