recallnest 3.0.0 → 4.0.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 (246) hide show
  1. package/CHANGELOG.md +304 -0
  2. package/README.md +251 -258
  3. package/README_CN.md +226 -247
  4. package/assets/ui/app.js +6 -1
  5. package/bin/recallnest.mjs +20 -37
  6. package/docs/api-reference.md +8 -0
  7. package/docs/architecture.md +18 -13
  8. package/eval/cases-canary.json +107 -9
  9. package/integrations/examples/claude-agent-sdk/memory-agent.ts +2 -0
  10. package/integrations/examples/langchain/memory-chain.py +4 -0
  11. package/integrations/examples/openai-agents-sdk/memory-agent.py +4 -0
  12. package/package.json +12 -3
  13. package/src/activity-counter.ts +1 -0
  14. package/src/adaptive-retrieval.ts +23 -0
  15. package/src/api-server.ts +13 -11
  16. package/src/asset-sync.ts +26 -2
  17. package/src/audit-log.ts +2 -0
  18. package/src/auto-gc.ts +4 -3
  19. package/src/belief-history.ts +137 -31
  20. package/src/capture-engine.ts +459 -71
  21. package/src/cascade-forget.ts +7 -3
  22. package/src/checkpoint-mirror.ts +417 -0
  23. package/src/cli.ts +574 -54
  24. package/src/conflict-engine.ts +43 -15
  25. package/src/consolidation-engine.ts +24 -3
  26. package/src/context-collapse-renderer.ts +8 -1
  27. package/src/context-composer-stable.ts +5 -1
  28. package/src/context-composer.ts +11 -2
  29. package/src/distill-lock.ts +246 -74
  30. package/src/dream-pipeline.ts +3 -1
  31. package/src/embedder.ts +24 -0
  32. package/src/env-config.ts +136 -0
  33. package/src/flock.ts +97 -0
  34. package/src/forget-engine.ts +342 -58
  35. package/src/gateway-server.ts +4 -0
  36. package/src/ingest-plan.ts +56 -0
  37. package/src/ingest.ts +294 -121
  38. package/src/kg-contradiction.ts +753 -0
  39. package/src/kg-extractor.ts +17 -0
  40. package/src/kg-store.ts +1 -1
  41. package/src/known-item-eval.ts +455 -0
  42. package/src/llm-client.ts +13 -26
  43. package/src/llm-consolidation.ts +2 -2
  44. package/src/mcp-tool-deps.ts +6 -0
  45. package/src/mcp-tools-advanced.ts +80 -45
  46. package/src/mcp-tools-core.ts +23 -17
  47. package/src/mcp-tools-governance.ts +13 -7
  48. package/src/memory-assets.ts +144 -8
  49. package/src/memory-boundaries.ts +19 -0
  50. package/src/memory-evolution.ts +9 -0
  51. package/src/memory-lint.ts +1 -1
  52. package/src/memory-output.ts +311 -14
  53. package/src/memory-reconcile.ts +1437 -0
  54. package/src/memory-schema.ts +11 -0
  55. package/src/memory-store-port.ts +1 -0
  56. package/src/minis-archive.ts +151 -0
  57. package/src/noise-filter.ts +15 -2
  58. package/src/pii-detector.ts +31 -1
  59. package/src/pivot-apply.ts +55 -5
  60. package/src/pivot-evidence.ts +128 -0
  61. package/src/ppr-traversal.ts +25 -1
  62. package/src/preference-matcher.ts +40 -28
  63. package/src/prospective-memory.ts +7 -4
  64. package/src/retriever.ts +297 -57
  65. package/src/retroactive-boost.ts +3 -2
  66. package/src/runtime-config.ts +15 -1
  67. package/src/scope-policy.ts +1 -0
  68. package/src/session-distiller.ts +6 -2
  69. package/src/session-engine.ts +11 -4
  70. package/src/session-output.ts +20 -1
  71. package/src/session-schema.ts +5 -2
  72. package/src/session-store.ts +59 -12
  73. package/src/skill-promotion.ts +67 -1
  74. package/src/store-receipts.ts +109 -0
  75. package/src/store-write-budget.ts +37 -0
  76. package/src/store.ts +298 -57
  77. package/src/text-fingerprint.ts +25 -0
  78. package/src/trigger-store.ts +443 -0
  79. package/src/ui-server.ts +22 -21
  80. package/src/version-manager.ts +19 -5
  81. package/src/write-verifier.ts +16 -11
  82. package/src/__tests__/access-tracker-novelty.test.ts +0 -324
  83. package/src/__tests__/activity-counter.test.ts +0 -125
  84. package/src/__tests__/adaptive-fidelity.test.ts +0 -102
  85. package/src/__tests__/admission-control.test.ts +0 -206
  86. package/src/__tests__/age-label.test.ts +0 -33
  87. package/src/__tests__/anchor-generator.test.ts +0 -67
  88. package/src/__tests__/api-server-security.test.ts +0 -102
  89. package/src/__tests__/auto-consolidation.test.ts +0 -168
  90. package/src/__tests__/auto-recall.test.ts +0 -131
  91. package/src/__tests__/babel-integration.test.ts +0 -144
  92. package/src/__tests__/batch-dedup.test.ts +0 -65
  93. package/src/__tests__/belief-history.test.ts +0 -220
  94. package/src/__tests__/canary-eval.test.ts +0 -114
  95. package/src/__tests__/capture-engine.test.ts +0 -1328
  96. package/src/__tests__/capture-heuristic.test.ts +0 -171
  97. package/src/__tests__/cascade-forget.test.ts +0 -210
  98. package/src/__tests__/category-min-scores.test.ts +0 -116
  99. package/src/__tests__/cc8-cc1-essential-context.test.ts +0 -298
  100. package/src/__tests__/circuit-breaker.test.ts +0 -99
  101. package/src/__tests__/cluster-consolidation.test.ts +0 -1236
  102. package/src/__tests__/confidence-metadata.test.ts +0 -125
  103. package/src/__tests__/confidence-tracker.test.ts +0 -261
  104. package/src/__tests__/conflict-advisor.test.ts +0 -206
  105. package/src/__tests__/conflict-escalation.test.ts +0 -149
  106. package/src/__tests__/conflict-lifecycle.test.ts +0 -83
  107. package/src/__tests__/conflict-output.test.ts +0 -85
  108. package/src/__tests__/conflict-store.test.ts +0 -418
  109. package/src/__tests__/connector-v1.test.ts +0 -181
  110. package/src/__tests__/consolidation-engine.test.ts +0 -520
  111. package/src/__tests__/constructive-retrieval.test.ts +0 -509
  112. package/src/__tests__/context-collapse-renderer.test.ts +0 -214
  113. package/src/__tests__/context-composer-pins.test.ts +0 -64
  114. package/src/__tests__/context-composer-task-selection.test.ts +0 -24
  115. package/src/__tests__/context-reconstructor.test.ts +0 -65
  116. package/src/__tests__/context-renderer.test.ts +0 -91
  117. package/src/__tests__/continuity-eval.test.ts +0 -275
  118. package/src/__tests__/continuity-schema.test.ts +0 -97
  119. package/src/__tests__/contracts.test.ts +0 -262
  120. package/src/__tests__/conversation-importer.test.ts +0 -692
  121. package/src/__tests__/core-recall-eval.test.ts +0 -235
  122. package/src/__tests__/core-summary.test.ts +0 -109
  123. package/src/__tests__/data-checkup.test.ts +0 -304
  124. package/src/__tests__/decay-exemption.test.ts +0 -129
  125. package/src/__tests__/dedup-actions.test.ts +0 -58
  126. package/src/__tests__/dedup-guard.test.ts +0 -39
  127. package/src/__tests__/deterministic-id.test.ts +0 -39
  128. package/src/__tests__/distill-lock.test.ts +0 -305
  129. package/src/__tests__/distill-session-argshape.test.ts +0 -79
  130. package/src/__tests__/doctor.test.ts +0 -319
  131. package/src/__tests__/dream-pipeline.test.ts +0 -820
  132. package/src/__tests__/dream-scope-isolation.test.ts +0 -386
  133. package/src/__tests__/dream-write-batching.test.ts +0 -211
  134. package/src/__tests__/drill-down.test.ts +0 -108
  135. package/src/__tests__/embedder.test.ts +0 -122
  136. package/src/__tests__/emotion-decay.test.ts +0 -46
  137. package/src/__tests__/emotion-detector.test.ts +0 -56
  138. package/src/__tests__/emotion-phase1-integration.test.ts +0 -153
  139. package/src/__tests__/emotion-retrieval.test.ts +0 -56
  140. package/src/__tests__/entity-resolver.test.ts +0 -58
  141. package/src/__tests__/env-config-invariant.test.ts +0 -80
  142. package/src/__tests__/env-config.test.ts +0 -211
  143. package/src/__tests__/error-signature-boost.test.ts +0 -87
  144. package/src/__tests__/error-signature.test.ts +0 -95
  145. package/src/__tests__/error-taxonomy.test.ts +0 -129
  146. package/src/__tests__/ethics-layer.test.ts +0 -476
  147. package/src/__tests__/event-segmenter.test.ts +0 -153
  148. package/src/__tests__/feature-flags-integration.test.ts +0 -54
  149. package/src/__tests__/frequency-tracker.test.ts +0 -113
  150. package/src/__tests__/freshness.test.ts +0 -186
  151. package/src/__tests__/graph-export.test.ts +0 -489
  152. package/src/__tests__/hotness-blend.test.ts +0 -283
  153. package/src/__tests__/ingest-chunking.test.ts +0 -44
  154. package/src/__tests__/ingest-scope-prefix.test.ts +0 -69
  155. package/src/__tests__/ingest.test.ts +0 -360
  156. package/src/__tests__/interference-detector.test.ts +0 -203
  157. package/src/__tests__/kg-extractor.test.ts +0 -192
  158. package/src/__tests__/kg-store.test.ts +0 -300
  159. package/src/__tests__/llm-client-pivot.test.ts +0 -628
  160. package/src/__tests__/llm-consolidation.test.ts +0 -226
  161. package/src/__tests__/maintenance-locks.test.ts +0 -197
  162. package/src/__tests__/mcp-efficiency.test.ts +0 -288
  163. package/src/__tests__/mcp-related-scopes.test.ts +0 -151
  164. package/src/__tests__/memory-boundaries.test.ts +0 -282
  165. package/src/__tests__/memory-evolution-b2-b3-e1.test.ts +0 -362
  166. package/src/__tests__/memory-evolution.test.ts +0 -271
  167. package/src/__tests__/memory-health-rebalance.test.ts +0 -144
  168. package/src/__tests__/memory-lint.test.ts +0 -421
  169. package/src/__tests__/memory-output.test.ts +0 -224
  170. package/src/__tests__/memory-promotion.test.ts +0 -315
  171. package/src/__tests__/memory-utility.test.ts +0 -256
  172. package/src/__tests__/migrate-dedup-ids.test.ts +0 -65
  173. package/src/__tests__/multi-vector.test.ts +0 -242
  174. package/src/__tests__/narrative-architecture.test.ts +0 -456
  175. package/src/__tests__/noise-filter.test.ts +0 -205
  176. package/src/__tests__/noise-prototype-bank.test.ts +0 -280
  177. package/src/__tests__/obsidian-connector.test.ts +0 -363
  178. package/src/__tests__/omitted-reporting.test.ts +0 -98
  179. package/src/__tests__/openai-contract.test.ts +0 -513
  180. package/src/__tests__/pending-review.test.ts +0 -77
  181. package/src/__tests__/pii-redact.test.ts +0 -333
  182. package/src/__tests__/pivot-apply.test.ts +0 -373
  183. package/src/__tests__/pivot-distill-supervisor.test.ts +0 -457
  184. package/src/__tests__/pivot-distill.test.ts +0 -1178
  185. package/src/__tests__/post-retrieval-filter.test.ts +0 -178
  186. package/src/__tests__/ppr-traversal.test.ts +0 -256
  187. package/src/__tests__/prediction-engine.test.ts +0 -349
  188. package/src/__tests__/predictive-prospective-memory.test.ts +0 -416
  189. package/src/__tests__/preference-guard-smoke.test.ts +0 -211
  190. package/src/__tests__/preference-matcher.test.ts +0 -282
  191. package/src/__tests__/preference-slots.test.ts +0 -205
  192. package/src/__tests__/promotion-distinct-sources.test.ts +0 -162
  193. package/src/__tests__/prospective-memory.test.ts +0 -283
  194. package/src/__tests__/pull-from-macbook-sync-only.test.ts +0 -679
  195. package/src/__tests__/query-entity-detector.test.ts +0 -171
  196. package/src/__tests__/query-expander-alias.test.ts +0 -146
  197. package/src/__tests__/query-expander.test.ts +0 -34
  198. package/src/__tests__/recall-gate.test.ts +0 -266
  199. package/src/__tests__/recall-governor.test.ts +0 -227
  200. package/src/__tests__/recall-mode.test.ts +0 -64
  201. package/src/__tests__/resolve-db-path.test.ts +0 -130
  202. package/src/__tests__/resolve-tier.test.ts +0 -64
  203. package/src/__tests__/result-synthesizer.test.ts +0 -198
  204. package/src/__tests__/resume-context.test.ts +0 -4898
  205. package/src/__tests__/retrieval-eval.test.ts +0 -163
  206. package/src/__tests__/retrieval-trace.test.ts +0 -213
  207. package/src/__tests__/retrieve-audit-revision.test.ts +0 -255
  208. package/src/__tests__/retriever-boundary-weight.test.ts +0 -136
  209. package/src/__tests__/retriever-lazy-init-dispatch.test.ts +0 -123
  210. package/src/__tests__/retroactive-boost.test.ts +0 -223
  211. package/src/__tests__/salience-scoring.test.ts +0 -106
  212. package/src/__tests__/scope-inventory.test.ts +0 -200
  213. package/src/__tests__/scope-policy.test.ts +0 -69
  214. package/src/__tests__/scope-suggester.test.ts +0 -80
  215. package/src/__tests__/session-checkpoint.test.ts +0 -291
  216. package/src/__tests__/session-distiller.test.ts +0 -640
  217. package/src/__tests__/session-gc.test.ts +0 -215
  218. package/src/__tests__/skill-engine.test.ts +0 -552
  219. package/src/__tests__/skill-promotion.test.ts +0 -654
  220. package/src/__tests__/skill-verifier.test.ts +0 -122
  221. package/src/__tests__/source-heartbeat.test.ts +0 -254
  222. package/src/__tests__/stable-context-length-contract.test.ts +0 -62
  223. package/src/__tests__/store-patch-metadata.test.ts +0 -235
  224. package/src/__tests__/store-read-consistency.test.ts +0 -122
  225. package/src/__tests__/store-schema-migration.test.ts +0 -87
  226. package/src/__tests__/store-scope-audit.test.ts +0 -81
  227. package/src/__tests__/store-upsert.test.ts +0 -201
  228. package/src/__tests__/supersede-chain.test.ts +0 -154
  229. package/src/__tests__/synthesis-contract.test.ts +0 -252
  230. package/src/__tests__/synthesis-promotion.test.ts +0 -369
  231. package/src/__tests__/temporal-parser.test.ts +0 -241
  232. package/src/__tests__/temporal-validity.test.ts +0 -160
  233. package/src/__tests__/term-registry-terminal-roster.test.ts +0 -127
  234. package/src/__tests__/term-registry.test.ts +0 -121
  235. package/src/__tests__/tool-error-surfacing.test.ts +0 -98
  236. package/src/__tests__/tool-output-compressor.test.ts +0 -160
  237. package/src/__tests__/topic-tag.test.ts +0 -113
  238. package/src/__tests__/trusted-memory.test.ts +0 -867
  239. package/src/__tests__/ui-server-security.test.ts +0 -93
  240. package/src/__tests__/usage-tracker.test.ts +0 -287
  241. package/src/__tests__/utility-weight.test.ts +0 -57
  242. package/src/__tests__/verbatim-self-recall.test.ts +0 -279
  243. package/src/__tests__/version-manager.test.ts +0 -248
  244. package/src/__tests__/workflow-observation-skill-outcome.test.ts +0 -352
  245. package/src/__tests__/workflow-observation.test.ts +0 -433
  246. package/src/__tests__/write-verifier.test.ts +0 -122
@@ -1,108 +0,0 @@
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
- });
@@ -1,122 +0,0 @@
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
- });
@@ -1,46 +0,0 @@
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
- });
@@ -1,56 +0,0 @@
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
- });
@@ -1,153 +0,0 @@
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
- });
@@ -1,56 +0,0 @@
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
- });
@@ -1,58 +0,0 @@
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
- });
@@ -1,80 +0,0 @@
1
- import { describe, expect, it } from "bun:test";
2
- import { readdirSync, readFileSync, statSync } from "node:fs";
3
- import { join } from "node:path";
4
-
5
- /**
6
- * Architecture guard for the invariant env-config.ts declares in its own header:
7
- * it is the single source of truth for every fixed `RECALLNEST_*` env read, and
8
- * consumers must call its accessors instead of touching `process.env` directly.
9
- *
10
- * That invariant was documented but unenforced, and it had already sprung a leak:
11
- * `cli.ts` read RECALLNEST_DREAM_BUDGET_MS inline and — precisely because it
12
- * bypassed the accessor layer — was the one read in the repo with no
13
- * `Number.isFinite` guard, so `""` silently became a 0ms dream budget and any
14
- * non-numeric value became a NaN deadline that disabled the guard entirely.
15
- * Bypassing the chokepoint IS how the guard gets lost; hence a test, not a note.
16
- *
17
- * Baseline: 0 violations as of 2026-08-18. If this goes red, the fix is normally
18
- * to add an accessor to env-config.ts — not to widen EXEMPT.
19
- *
20
- * Deliberately NOT matched (these are the exemptions env-config.ts's header
21
- * already names, and each reads differently so the pattern never sees them):
22
- * - scope-policy.ts → `env.RECALLNEST_*` on an injected env object
23
- * - store.ts → RECALLNEST_NS is a local constant, not an env var
24
- * - runtime-config / embedder / llm-client → dynamic `process.env[name]`
25
- * config-template expansion, which cannot be statically consolidated
26
- */
27
-
28
- // Add a file here ONLY with a reason, and only when an accessor genuinely can't
29
- // work (e.g. the read must happen before env-config's module-init boundary).
30
- const EXEMPT: ReadonlyArray<{ file: string; why: string }> = [];
31
-
32
- const DIRECT_READ = /process\.env\.RECALLNEST_[A-Z_0-9]+/g;
33
-
34
- function collectSourceFiles(dir: string, out: string[] = []): string[] {
35
- for (const entry of readdirSync(dir)) {
36
- const full = join(dir, entry);
37
- if (statSync(full).isDirectory()) {
38
- if (entry === "__tests__" || entry === "node_modules") continue;
39
- collectSourceFiles(full, out);
40
- continue;
41
- }
42
- if (!entry.endsWith(".ts") || entry.endsWith(".test.ts") || entry === "env-config.ts") continue;
43
- out.push(full);
44
- }
45
- return out;
46
- }
47
-
48
- describe("env-config is the chokepoint for RECALLNEST_* reads", () => {
49
- it("no src/ module reads process.env.RECALLNEST_* directly", () => {
50
- const exemptFiles = new Set(EXEMPT.map((e) => e.file));
51
- const violations: string[] = [];
52
-
53
- for (const file of collectSourceFiles("src")) {
54
- if (exemptFiles.has(file)) continue;
55
- const lines = readFileSync(file, "utf8").split("\n");
56
- lines.forEach((line, i) => {
57
- for (const hit of line.match(DIRECT_READ) ?? []) {
58
- violations.push(`${file}:${i + 1} ${hit}`);
59
- }
60
- });
61
- }
62
-
63
- expect(
64
- violations,
65
- violations.length === 0
66
- ? ""
67
- : `Direct RECALLNEST_* env reads bypass env-config.ts's guards:\n ${violations.join("\n ")}\n` +
68
- `Fix: add an accessor to src/env-config.ts (with its parsing + fallback) and call it here.`,
69
- ).toEqual([]);
70
- });
71
-
72
- it("guard actually detects a violation (so a green run means something)", () => {
73
- const synthetic = 'const x = Number(process.env.RECALLNEST_SOMETHING_NEW ?? 1);';
74
- expect(synthetic.match(DIRECT_READ)).toHaveLength(1);
75
- // And it must not fire on the exempted read styles:
76
- expect('env.RECALLNEST_SESSION_ID'.match(DIRECT_READ)).toBeNull();
77
- expect('process.env[envVar]'.match(DIRECT_READ)).toBeNull();
78
- expect('const RECALLNEST_NS = "recallnest:v1";'.match(DIRECT_READ)).toBeNull();
79
- });
80
- });