recallnest 2.6.1 → 3.0.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 (207) hide show
  1. package/CHANGELOG.md +292 -0
  2. package/README.md +280 -195
  3. package/README_CN.md +266 -199
  4. package/config.json.example +8 -1
  5. package/docs/api-reference.md +1 -1
  6. package/integrations/README.md +1 -1
  7. package/integrations/agy/README.md +73 -0
  8. package/integrations/agy/setup.sh +75 -0
  9. package/integrations/setup-all.sh +1 -1
  10. package/package.json +7 -5
  11. package/src/access-tracker.ts +10 -1
  12. package/src/activity-counter.ts +69 -11
  13. package/src/age-label.ts +29 -0
  14. package/src/audit-log.ts +35 -0
  15. package/src/auto-gc.ts +97 -30
  16. package/src/capture-engine.ts +26 -4
  17. package/src/cli.ts +224 -10
  18. package/src/consolidation-engine.ts +317 -123
  19. package/src/context-composer-stable.ts +5 -1
  20. package/src/data-checkup.ts +17 -3
  21. package/src/dream-pipeline.ts +90 -12
  22. package/src/embedder.ts +44 -5
  23. package/src/env-config.ts +71 -0
  24. package/src/forget-engine.ts +8 -2
  25. package/src/gateway-server.ts +272 -0
  26. package/src/ingest.ts +248 -22
  27. package/src/kg-extractor.ts +17 -0
  28. package/src/llm-client.ts +86 -25
  29. package/src/mcp-server.ts +2 -1
  30. package/src/mcp-tools-advanced.ts +7 -2
  31. package/src/mcp-tools-core.ts +11 -1
  32. package/src/mcp-tools-governance.ts +45 -3
  33. package/src/memory-evolution.ts +16 -0
  34. package/src/memory-output.ts +47 -8
  35. package/src/memory-promotion.ts +280 -0
  36. package/src/memory-store-port.ts +32 -3
  37. package/src/memory-utility.ts +314 -0
  38. package/src/pivot-apply.ts +52 -1
  39. package/src/pivot-evidence.ts +128 -0
  40. package/src/ppr-traversal.ts +25 -1
  41. package/src/recall-ledger.ts +76 -0
  42. package/src/retriever.ts +82 -9
  43. package/src/runtime-config.ts +9 -0
  44. package/src/scope-policy.ts +22 -1
  45. package/src/session-engine.ts +11 -4
  46. package/src/session-output.ts +13 -4
  47. package/src/session-store.ts +2 -3
  48. package/src/skill-promotion.ts +218 -1
  49. package/src/store.ts +231 -71
  50. package/src/synthesis-contract.ts +286 -0
  51. package/src/version-manager.ts +83 -0
  52. package/src/workflow-observation-schema.ts +26 -0
  53. package/integrations/gemini-cli/README.md +0 -57
  54. package/integrations/gemini-cli/setup.sh +0 -64
  55. package/src/__tests__/access-tracker-novelty.test.ts +0 -304
  56. package/src/__tests__/activity-counter.test.ts +0 -125
  57. package/src/__tests__/adaptive-fidelity.test.ts +0 -102
  58. package/src/__tests__/admission-control.test.ts +0 -206
  59. package/src/__tests__/anchor-generator.test.ts +0 -67
  60. package/src/__tests__/api-server-security.test.ts +0 -102
  61. package/src/__tests__/auto-consolidation.test.ts +0 -168
  62. package/src/__tests__/auto-recall.test.ts +0 -131
  63. package/src/__tests__/babel-integration.test.ts +0 -144
  64. package/src/__tests__/batch-dedup.test.ts +0 -65
  65. package/src/__tests__/belief-history.test.ts +0 -220
  66. package/src/__tests__/canary-eval.test.ts +0 -114
  67. package/src/__tests__/capture-engine.test.ts +0 -1328
  68. package/src/__tests__/capture-heuristic.test.ts +0 -171
  69. package/src/__tests__/cascade-forget.test.ts +0 -210
  70. package/src/__tests__/category-min-scores.test.ts +0 -116
  71. package/src/__tests__/cc8-cc1-essential-context.test.ts +0 -298
  72. package/src/__tests__/circuit-breaker.test.ts +0 -99
  73. package/src/__tests__/cluster-consolidation.test.ts +0 -945
  74. package/src/__tests__/confidence-metadata.test.ts +0 -125
  75. package/src/__tests__/confidence-tracker.test.ts +0 -261
  76. package/src/__tests__/conflict-advisor.test.ts +0 -206
  77. package/src/__tests__/conflict-escalation.test.ts +0 -149
  78. package/src/__tests__/conflict-lifecycle.test.ts +0 -83
  79. package/src/__tests__/conflict-output.test.ts +0 -85
  80. package/src/__tests__/conflict-store.test.ts +0 -418
  81. package/src/__tests__/connector-v1.test.ts +0 -181
  82. package/src/__tests__/consolidation-engine.test.ts +0 -430
  83. package/src/__tests__/constructive-retrieval.test.ts +0 -509
  84. package/src/__tests__/context-collapse-renderer.test.ts +0 -214
  85. package/src/__tests__/context-composer-pins.test.ts +0 -64
  86. package/src/__tests__/context-composer-task-selection.test.ts +0 -24
  87. package/src/__tests__/context-reconstructor.test.ts +0 -65
  88. package/src/__tests__/context-renderer.test.ts +0 -91
  89. package/src/__tests__/continuity-eval.test.ts +0 -275
  90. package/src/__tests__/continuity-schema.test.ts +0 -97
  91. package/src/__tests__/contracts.test.ts +0 -262
  92. package/src/__tests__/conversation-importer.test.ts +0 -692
  93. package/src/__tests__/core-recall-eval.test.ts +0 -235
  94. package/src/__tests__/core-summary.test.ts +0 -109
  95. package/src/__tests__/data-checkup.test.ts +0 -276
  96. package/src/__tests__/decay-exemption.test.ts +0 -129
  97. package/src/__tests__/dedup-actions.test.ts +0 -58
  98. package/src/__tests__/dedup-guard.test.ts +0 -39
  99. package/src/__tests__/deterministic-id.test.ts +0 -39
  100. package/src/__tests__/distill-lock.test.ts +0 -305
  101. package/src/__tests__/distill-session-argshape.test.ts +0 -79
  102. package/src/__tests__/doctor.test.ts +0 -319
  103. package/src/__tests__/dream-pipeline.test.ts +0 -780
  104. package/src/__tests__/drill-down.test.ts +0 -108
  105. package/src/__tests__/embedder.test.ts +0 -122
  106. package/src/__tests__/emotion-decay.test.ts +0 -46
  107. package/src/__tests__/emotion-detector.test.ts +0 -56
  108. package/src/__tests__/emotion-phase1-integration.test.ts +0 -153
  109. package/src/__tests__/emotion-retrieval.test.ts +0 -56
  110. package/src/__tests__/entity-resolver.test.ts +0 -58
  111. package/src/__tests__/env-config.test.ts +0 -151
  112. package/src/__tests__/error-signature-boost.test.ts +0 -87
  113. package/src/__tests__/error-signature.test.ts +0 -95
  114. package/src/__tests__/error-taxonomy.test.ts +0 -129
  115. package/src/__tests__/ethics-layer.test.ts +0 -476
  116. package/src/__tests__/event-segmenter.test.ts +0 -153
  117. package/src/__tests__/feature-flags-integration.test.ts +0 -54
  118. package/src/__tests__/frequency-tracker.test.ts +0 -113
  119. package/src/__tests__/freshness.test.ts +0 -186
  120. package/src/__tests__/graph-export.test.ts +0 -489
  121. package/src/__tests__/hotness-blend.test.ts +0 -283
  122. package/src/__tests__/ingest-chunking.test.ts +0 -44
  123. package/src/__tests__/ingest.test.ts +0 -360
  124. package/src/__tests__/interference-detector.test.ts +0 -203
  125. package/src/__tests__/kg-extractor.test.ts +0 -192
  126. package/src/__tests__/kg-store.test.ts +0 -300
  127. package/src/__tests__/llm-client-pivot.test.ts +0 -628
  128. package/src/__tests__/llm-consolidation.test.ts +0 -226
  129. package/src/__tests__/maintenance-locks.test.ts +0 -189
  130. package/src/__tests__/mcp-efficiency.test.ts +0 -288
  131. package/src/__tests__/mcp-related-scopes.test.ts +0 -151
  132. package/src/__tests__/memory-boundaries.test.ts +0 -282
  133. package/src/__tests__/memory-evolution-b2-b3-e1.test.ts +0 -345
  134. package/src/__tests__/memory-evolution.test.ts +0 -271
  135. package/src/__tests__/memory-health-rebalance.test.ts +0 -144
  136. package/src/__tests__/memory-lint.test.ts +0 -421
  137. package/src/__tests__/memory-output.test.ts +0 -215
  138. package/src/__tests__/memory-promotion.test.ts +0 -315
  139. package/src/__tests__/migrate-dedup-ids.test.ts +0 -65
  140. package/src/__tests__/multi-vector.test.ts +0 -242
  141. package/src/__tests__/narrative-architecture.test.ts +0 -456
  142. package/src/__tests__/noise-filter.test.ts +0 -205
  143. package/src/__tests__/noise-prototype-bank.test.ts +0 -280
  144. package/src/__tests__/obsidian-connector.test.ts +0 -363
  145. package/src/__tests__/omitted-reporting.test.ts +0 -98
  146. package/src/__tests__/pending-review.test.ts +0 -77
  147. package/src/__tests__/pii-redact.test.ts +0 -333
  148. package/src/__tests__/pivot-apply.test.ts +0 -373
  149. package/src/__tests__/pivot-distill-supervisor.test.ts +0 -457
  150. package/src/__tests__/pivot-distill.test.ts +0 -1178
  151. package/src/__tests__/post-retrieval-filter.test.ts +0 -178
  152. package/src/__tests__/ppr-traversal.test.ts +0 -256
  153. package/src/__tests__/prediction-engine.test.ts +0 -349
  154. package/src/__tests__/predictive-prospective-memory.test.ts +0 -416
  155. package/src/__tests__/preference-guard-smoke.test.ts +0 -211
  156. package/src/__tests__/preference-matcher.test.ts +0 -282
  157. package/src/__tests__/preference-slots.test.ts +0 -205
  158. package/src/__tests__/prospective-memory.test.ts +0 -283
  159. package/src/__tests__/pull-from-macbook-sync-only.test.ts +0 -679
  160. package/src/__tests__/query-entity-detector.test.ts +0 -171
  161. package/src/__tests__/query-expander-alias.test.ts +0 -146
  162. package/src/__tests__/query-expander.test.ts +0 -34
  163. package/src/__tests__/recall-gate.test.ts +0 -266
  164. package/src/__tests__/recall-governor.test.ts +0 -227
  165. package/src/__tests__/recall-mode.test.ts +0 -64
  166. package/src/__tests__/resolve-db-path.test.ts +0 -130
  167. package/src/__tests__/resolve-tier.test.ts +0 -64
  168. package/src/__tests__/result-synthesizer.test.ts +0 -198
  169. package/src/__tests__/resume-context.test.ts +0 -4898
  170. package/src/__tests__/retrieval-eval.test.ts +0 -163
  171. package/src/__tests__/retrieval-trace.test.ts +0 -213
  172. package/src/__tests__/retriever-boundary-weight.test.ts +0 -136
  173. package/src/__tests__/retriever-lazy-init-dispatch.test.ts +0 -123
  174. package/src/__tests__/retroactive-boost.test.ts +0 -223
  175. package/src/__tests__/salience-scoring.test.ts +0 -106
  176. package/src/__tests__/scope-inventory.test.ts +0 -200
  177. package/src/__tests__/scope-policy.test.ts +0 -69
  178. package/src/__tests__/scope-suggester.test.ts +0 -80
  179. package/src/__tests__/session-checkpoint.test.ts +0 -291
  180. package/src/__tests__/session-distiller.test.ts +0 -640
  181. package/src/__tests__/session-gc.test.ts +0 -215
  182. package/src/__tests__/skill-engine.test.ts +0 -552
  183. package/src/__tests__/skill-promotion.test.ts +0 -654
  184. package/src/__tests__/skill-verifier.test.ts +0 -122
  185. package/src/__tests__/source-heartbeat.test.ts +0 -250
  186. package/src/__tests__/stable-context-length-contract.test.ts +0 -62
  187. package/src/__tests__/store-patch-metadata.test.ts +0 -205
  188. package/src/__tests__/store-read-consistency.test.ts +0 -122
  189. package/src/__tests__/store-schema-migration.test.ts +0 -87
  190. package/src/__tests__/store-scope-audit.test.ts +0 -81
  191. package/src/__tests__/store-upsert.test.ts +0 -201
  192. package/src/__tests__/supersede-chain.test.ts +0 -154
  193. package/src/__tests__/temporal-parser.test.ts +0 -241
  194. package/src/__tests__/temporal-validity.test.ts +0 -160
  195. package/src/__tests__/term-registry-terminal-roster.test.ts +0 -127
  196. package/src/__tests__/term-registry.test.ts +0 -121
  197. package/src/__tests__/tool-error-surfacing.test.ts +0 -98
  198. package/src/__tests__/tool-output-compressor.test.ts +0 -160
  199. package/src/__tests__/topic-tag.test.ts +0 -113
  200. package/src/__tests__/trusted-memory.test.ts +0 -839
  201. package/src/__tests__/ui-server-security.test.ts +0 -93
  202. package/src/__tests__/usage-tracker.test.ts +0 -287
  203. package/src/__tests__/version-manager.test.ts +0 -213
  204. package/src/__tests__/workflow-observation-skill-outcome.test.ts +0 -352
  205. package/src/__tests__/workflow-observation.test.ts +0 -433
  206. package/src/__tests__/write-verifier.test.ts +0 -122
  207. /package/integrations/{gemini-cli/gemini-md-snippet.md → agy/agy-md-snippet.md} +0 -0
@@ -1,206 +0,0 @@
1
- import { describe, expect, it } from "bun:test";
2
-
3
- import {
4
- checkAdmission,
5
- resolveAdmissionConfig,
6
- ScopeRateLimiter,
7
- } from "../admission-control.js";
8
-
9
- // ---------------------------------------------------------------------------
10
- // resolveAdmissionConfig
11
- // ---------------------------------------------------------------------------
12
-
13
- describe("resolveAdmissionConfig", () => {
14
- it("returns defaults when no overrides", () => {
15
- const cfg = resolveAdmissionConfig();
16
- expect(cfg.minTextLength).toBe(10);
17
- expect(cfg.noiseFilterEnabled).toBe(true);
18
- expect(cfg.minImportance).toBe(0.2);
19
- expect(cfg.maxWritesPerScope).toBe(50);
20
- expect(cfg.rateLimitWindowMs).toBe(60_000);
21
- });
22
-
23
- it("merges partial overrides", () => {
24
- const cfg = resolveAdmissionConfig({ minImportance: 0.5 });
25
- expect(cfg.minImportance).toBe(0.5);
26
- expect(cfg.minTextLength).toBe(10); // unchanged
27
- });
28
- });
29
-
30
- // ---------------------------------------------------------------------------
31
- // checkAdmission — text length
32
- // ---------------------------------------------------------------------------
33
-
34
- describe("checkAdmission — text length", () => {
35
- it("rejects text shorter than minTextLength", () => {
36
- const result = checkAdmission("short", 0.8, "project:test");
37
- expect(result.verdict).toBe("rejected");
38
- expect(result.reason).toBe("text_too_short");
39
- });
40
-
41
- it("rejects empty text", () => {
42
- const result = checkAdmission("", 0.8, "project:test");
43
- expect(result.verdict).toBe("rejected");
44
- expect(result.reason).toBe("text_too_short");
45
- });
46
-
47
- it("rejects whitespace-only text", () => {
48
- const result = checkAdmission(" ", 0.8, "project:test");
49
- expect(result.verdict).toBe("rejected");
50
- expect(result.reason).toBe("text_too_short");
51
- });
52
-
53
- it("accepts text at exactly minTextLength", () => {
54
- const result = checkAdmission("abcdefghij", 0.8, "project:test");
55
- expect(result.verdict).toBe("accepted");
56
- });
57
-
58
- it("respects custom minTextLength", () => {
59
- const result = checkAdmission("hello world", 0.8, "project:test", undefined, { minTextLength: 20 });
60
- expect(result.verdict).toBe("rejected");
61
- expect(result.reason).toBe("text_too_short");
62
- });
63
- });
64
-
65
- // ---------------------------------------------------------------------------
66
- // checkAdmission — noise filter
67
- // ---------------------------------------------------------------------------
68
-
69
- describe("checkAdmission — noise filter", () => {
70
- it("rejects denial patterns", () => {
71
- const result = checkAdmission("I don't have any information about that topic", 0.8, "project:test");
72
- expect(result.verdict).toBe("rejected");
73
- expect(result.reason).toBe("noise_detected");
74
- });
75
-
76
- it("rejects boilerplate greetings", () => {
77
- const result = checkAdmission("hello there!", 0.8, "project:test");
78
- expect(result.verdict).toBe("rejected");
79
- expect(result.reason).toBe("noise_detected");
80
- });
81
-
82
- it("can be disabled via config", () => {
83
- const result = checkAdmission(
84
- "I don't have any information about that topic",
85
- 0.8,
86
- "project:test",
87
- undefined,
88
- { noiseFilterEnabled: false },
89
- );
90
- expect(result.verdict).toBe("accepted");
91
- });
92
- });
93
-
94
- // ---------------------------------------------------------------------------
95
- // checkAdmission — importance floor
96
- // ---------------------------------------------------------------------------
97
-
98
- describe("checkAdmission — importance floor", () => {
99
- it("rejects importance below default threshold (0.2)", () => {
100
- const result = checkAdmission("This is a valid memory text", 0.1, "project:test");
101
- expect(result.verdict).toBe("rejected");
102
- expect(result.reason).toBe("importance_too_low");
103
- });
104
-
105
- it("accepts importance at exactly the threshold", () => {
106
- const result = checkAdmission("This is a valid memory text", 0.2, "project:test");
107
- expect(result.verdict).toBe("accepted");
108
- });
109
-
110
- it("accepts importance above threshold", () => {
111
- const result = checkAdmission("This is a valid memory text", 0.8, "project:test");
112
- expect(result.verdict).toBe("accepted");
113
- });
114
-
115
- it("respects custom minImportance", () => {
116
- const result = checkAdmission("This is a valid memory text", 0.4, "project:test", undefined, { minImportance: 0.5 });
117
- expect(result.verdict).toBe("rejected");
118
- expect(result.reason).toBe("importance_too_low");
119
- });
120
- });
121
-
122
- // ---------------------------------------------------------------------------
123
- // ScopeRateLimiter
124
- // ---------------------------------------------------------------------------
125
-
126
- describe("ScopeRateLimiter", () => {
127
- it("allows writes within limit", () => {
128
- const limiter = new ScopeRateLimiter();
129
- const cfg = resolveAdmissionConfig({ maxWritesPerScope: 3 });
130
- expect(limiter.check("s1", cfg)).toBe(true);
131
- expect(limiter.check("s1", cfg)).toBe(true);
132
- expect(limiter.check("s1", cfg)).toBe(true);
133
- });
134
-
135
- it("blocks writes exceeding limit", () => {
136
- const limiter = new ScopeRateLimiter();
137
- const cfg = resolveAdmissionConfig({ maxWritesPerScope: 2 });
138
- expect(limiter.check("s1", cfg)).toBe(true);
139
- expect(limiter.check("s1", cfg)).toBe(true);
140
- expect(limiter.check("s1", cfg)).toBe(false); // 3rd blocked
141
- });
142
-
143
- it("tracks scopes independently", () => {
144
- const limiter = new ScopeRateLimiter();
145
- const cfg = resolveAdmissionConfig({ maxWritesPerScope: 1 });
146
- expect(limiter.check("s1", cfg)).toBe(true);
147
- expect(limiter.check("s2", cfg)).toBe(true);
148
- expect(limiter.check("s1", cfg)).toBe(false); // s1 exhausted
149
- expect(limiter.check("s2", cfg)).toBe(false); // s2 exhausted
150
- });
151
-
152
- it("reports correct size", () => {
153
- const limiter = new ScopeRateLimiter();
154
- const cfg = resolveAdmissionConfig();
155
- limiter.check("a", cfg);
156
- limiter.check("b", cfg);
157
- expect(limiter.size).toBe(2);
158
- });
159
-
160
- it("clears all state", () => {
161
- const limiter = new ScopeRateLimiter();
162
- const cfg = resolveAdmissionConfig();
163
- limiter.check("a", cfg);
164
- limiter.clear();
165
- expect(limiter.size).toBe(0);
166
- });
167
- });
168
-
169
- // ---------------------------------------------------------------------------
170
- // checkAdmission — rate limiting
171
- // ---------------------------------------------------------------------------
172
-
173
- describe("checkAdmission — rate limiting", () => {
174
- it("rejects when rate limit exceeded", () => {
175
- const limiter = new ScopeRateLimiter();
176
- const cfg = { maxWritesPerScope: 2 } as const;
177
- // First two pass
178
- checkAdmission("valid memory text here", 0.8, "project:test", limiter, cfg);
179
- checkAdmission("another valid memory text", 0.8, "project:test", limiter, cfg);
180
- // Third is rate limited
181
- const result = checkAdmission("yet another valid text", 0.8, "project:test", limiter, cfg);
182
- expect(result.verdict).toBe("rejected");
183
- expect(result.reason).toBe("rate_limited");
184
- });
185
-
186
- it("skips rate limiting when no limiter provided", () => {
187
- const result = checkAdmission("valid memory text here", 0.8, "project:test");
188
- expect(result.verdict).toBe("accepted");
189
- });
190
- });
191
-
192
- // ---------------------------------------------------------------------------
193
- // checkAdmission — happy path
194
- // ---------------------------------------------------------------------------
195
-
196
- describe("checkAdmission — happy path", () => {
197
- it("accepts valid memory with all checks passing", () => {
198
- const result = checkAdmission(
199
- "User prefers dark mode in all applications",
200
- 0.8,
201
- "project:test",
202
- );
203
- expect(result.verdict).toBe("accepted");
204
- expect(result.reason).toBeUndefined();
205
- });
206
- });
@@ -1,67 +0,0 @@
1
- import { describe, it, expect } from "bun:test";
2
- import { generateAnchor } from "../anchor-generator.js";
3
-
4
- describe("generateAnchor", () => {
5
- it("returns undefined for short text (≤80 chars)", () => {
6
- expect(generateAnchor("轮巡仓库每日检查")).toBeUndefined();
7
- expect(generateAnchor("Short text that fits in 80 chars easily")).toBeUndefined();
8
- });
9
-
10
- it("extracts first sentence from long text", () => {
11
- const text = "每天轮巡4个win4r仓库,检查Issue和PR。" +
12
- "包括 ClawTeam-OpenClaw、openclaw-a2a-gateway、memory-lancedb-pro、UltraMemory。" +
13
- "Issue 用中文回复,PR 用英文 review。不要重复回复已处理的。";
14
- const anchor = generateAnchor(text);
15
- expect(anchor).toBeDefined();
16
- expect(anchor!.length).toBeLessThanOrEqual(80);
17
- expect(anchor).toBe("每天轮巡4个win4r仓库,检查Issue和PR");
18
- });
19
-
20
- it("uses workflow pattern title from metadata", () => {
21
- const text = "A very long workflow pattern description that spans multiple lines and paragraphs explaining the full procedure";
22
- const metadata = {
23
- workflowPattern: {
24
- title: "Daily repo patrol",
25
- trigger: "Every morning",
26
- },
27
- };
28
- const anchor = generateAnchor(text, metadata);
29
- expect(anchor).toBe("Daily repo patrol");
30
- });
31
-
32
- it("uses case memory title from metadata", () => {
33
- const text = "A very long case description that spans multiple lines and paragraphs explaining the full case and its resolution";
34
- const metadata = {
35
- caseMemory: {
36
- title: "Fix dedup over-kill in ingest",
37
- problem: "72% of chunks were being dropped",
38
- },
39
- };
40
- const anchor = generateAnchor(text, metadata);
41
- expect(anchor).toBe("Fix dedup over-kill in ingest");
42
- });
43
-
44
- it("truncates long first sentences at CJK char boundary", () => {
45
- const text = "这是一个非常非常非常非常非常非常非常非常非常非常非常非常非常非常长的第一句话," +
46
- "超过了八十个字符的限制需要被截断到合适的长度。" +
47
- "第二句话在这里提供更多上下文信息用于测试截断功能是否正常工作。";
48
- const anchor = generateAnchor(text);
49
- expect(anchor).toBeDefined();
50
- expect(anchor!.length).toBeLessThanOrEqual(80);
51
- });
52
-
53
- it("truncates long English sentences at word boundary", () => {
54
- const text = "This is a very long first sentence that definitely exceeds the eighty character limit by quite a significant margin. Second sentence here.";
55
- const anchor = generateAnchor(text);
56
- expect(anchor).toBeDefined();
57
- expect(anchor!.length).toBeLessThanOrEqual(80);
58
- // Should not end mid-word
59
- expect(anchor!).not.toMatch(/\s$/);
60
- });
61
-
62
- it("handles text with only newline separators", () => {
63
- const text = "First line of important information that is a reasonable sentence\nSecond line with more details and context that expands on the first point";
64
- const anchor = generateAnchor(text);
65
- expect(anchor).toBe("First line of important information that is a reasonable sentence");
66
- });
67
- });
@@ -1,102 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
-
3
- import {
4
- enforceLocalHttpRequestPolicy,
5
- LOCAL_JSON_BODY_LIMIT_BYTES,
6
- } from "../http-request-policy.js";
7
-
8
- function apiRequest({
9
- method = "GET",
10
- host = "localhost:4318",
11
- headers = {},
12
- body,
13
- }: {
14
- method?: string;
15
- host?: string;
16
- headers?: Record<string, string>;
17
- body?: string;
18
- } = {}): Request {
19
- return new Request("http://localhost:4318/v1/store", {
20
- method,
21
- headers: { host, ...headers },
22
- body,
23
- });
24
- }
25
-
26
- describe("RecallNest API local HTTP policy", () => {
27
- test("keeps local CLI JSON requests without Origin usable", async () => {
28
- const result = await enforceLocalHttpRequestPolicy(apiRequest({
29
- method: "POST",
30
- headers: { "content-type": "application/json" },
31
- body: JSON.stringify({ text: "local CLI", scope: "test:local" }),
32
- }));
33
-
34
- expect(result).toEqual({ allowed: true });
35
- });
36
-
37
- test("allows loopback GET requests without browser headers", async () => {
38
- const result = await enforceLocalHttpRequestPolicy(apiRequest());
39
-
40
- expect(result).toEqual({ allowed: true });
41
- });
42
-
43
- test("rejects malformed or non-local Host headers", async () => {
44
- const malformed = await enforceLocalHttpRequestPolicy(apiRequest({ host: "localhost@attacker.example" }));
45
- const nonLocal = await enforceLocalHttpRequestPolicy(apiRequest({ host: "192.168.1.10:4318" }));
46
-
47
- expect(malformed).toEqual({ allowed: false, status: 403, message: "Host is not allowed." });
48
- expect(nonLocal).toEqual({ allowed: false, status: 403, message: "Host is not allowed." });
49
- });
50
-
51
- test("rejects opaque origins and same-site cross-origin requests", async () => {
52
- const opaqueOrigin = await enforceLocalHttpRequestPolicy(apiRequest({
53
- headers: { origin: "null" },
54
- }));
55
- const sameSite = await enforceLocalHttpRequestPolicy(apiRequest({
56
- headers: { "sec-fetch-site": "same-site" },
57
- }));
58
-
59
- expect(opaqueOrigin).toEqual({ allowed: false, status: 403, message: "Origin is not allowed." });
60
- expect(sameSite).toEqual({ allowed: false, status: 403, message: "Cross-site requests are not allowed." });
61
- });
62
-
63
- test("requires application/json for API POST requests", async () => {
64
- const missing = await enforceLocalHttpRequestPolicy(apiRequest({
65
- method: "POST",
66
- body: "{}",
67
- }));
68
- const form = await enforceLocalHttpRequestPolicy(apiRequest({
69
- method: "POST",
70
- headers: { "content-type": "application/x-www-form-urlencoded" },
71
- body: "text=example",
72
- }));
73
-
74
- expect(missing).toEqual({
75
- allowed: false,
76
- status: 415,
77
- message: "Content-Type must be application/json.",
78
- });
79
- expect(form).toEqual({
80
- allowed: false,
81
- status: 415,
82
- message: "Content-Type must be application/json.",
83
- });
84
- });
85
-
86
- test("rejects declared JSON bodies over the limit before route handling", async () => {
87
- const result = await enforceLocalHttpRequestPolicy(apiRequest({
88
- method: "POST",
89
- headers: {
90
- "content-type": "application/json",
91
- "content-length": String(LOCAL_JSON_BODY_LIMIT_BYTES + 1),
92
- },
93
- body: "{}",
94
- }));
95
-
96
- expect(result).toEqual({
97
- allowed: false,
98
- status: 413,
99
- message: `Request body exceeds ${LOCAL_JSON_BODY_LIMIT_BYTES} bytes.`,
100
- });
101
- });
102
- });
@@ -1,168 +0,0 @@
1
- import { describe, expect, it, beforeEach } from "bun:test";
2
-
3
- import {
4
- maybeConsolidate,
5
- resetConsolidationState,
6
- DEFAULT_AUTO_CONSOLIDATION_CONFIG,
7
- type AutoConsolidationConfig,
8
- } from "../auto-consolidation.js";
9
- import type { MemoryEntry, MemorySearchResult } from "../store.js";
10
-
11
- // ---------------------------------------------------------------------------
12
- // Helpers
13
- // ---------------------------------------------------------------------------
14
-
15
- function makeEntry(id: string, text: string, overrides: Partial<MemoryEntry> = {}): MemoryEntry {
16
- return {
17
- id,
18
- text,
19
- vector: [1, 0, 0],
20
- category: "events",
21
- scope: "project:test",
22
- importance: 0.7,
23
- timestamp: Date.now(),
24
- metadata: "{}",
25
- ...overrides,
26
- };
27
- }
28
-
29
- function createMockStore(entries: MemoryEntry[]) {
30
- const data = new Map(entries.map(e => [e.id, { ...e }]));
31
- return {
32
- async list(scopeFilter?: string[], _category?: string, limit = 500, _offset = 0) {
33
- return [...data.values()]
34
- .filter(e => !scopeFilter || scopeFilter.some(s => e.scope === s))
35
- .slice(0, limit);
36
- },
37
- async getById(id: string) {
38
- return data.get(id) ?? null;
39
- },
40
- async vectorSearch(_vector: number[], _limit = 5, _minScore = 0.3, _scopeFilter?: string[]) {
41
- return [] as MemorySearchResult[];
42
- },
43
- async update(id: string, updates: any, _scopeFilter?: string[]) {
44
- const entry = data.get(id);
45
- if (!entry) return null;
46
- Object.assign(entry, updates);
47
- return entry;
48
- },
49
- async stats(scopeFilter?: string[]) {
50
- const filtered = [...data.values()].filter(
51
- e => !scopeFilter || scopeFilter.some(s => e.scope === s),
52
- );
53
- return { totalCount: filtered.length, scopeCounts: {}, categoryCounts: {} };
54
- },
55
- hasFtsSupport: false,
56
- } as any;
57
- }
58
-
59
- // ---------------------------------------------------------------------------
60
- // Tests
61
- // ---------------------------------------------------------------------------
62
-
63
- describe("auto-consolidation", () => {
64
- beforeEach(() => {
65
- resetConsolidationState();
66
- });
67
-
68
- it("skips when not enough new memories", async () => {
69
- const entries = Array.from({ length: 10 }, (_, i) =>
70
- makeEntry(`id-${i}`, `memory ${i}`),
71
- );
72
- const store = createMockStore(entries);
73
-
74
- const result = await maybeConsolidate(store, "project:test");
75
- expect(result.triggered).toBe(false);
76
- expect(result.reason).toBe("insufficient_new_memories");
77
- });
78
-
79
- it("skips when too soon since last run", async () => {
80
- // Start with 60 entries, trigger first run, then grow to 120 so
81
- // the memory-count gate passes but the time gate blocks.
82
- const data: MemoryEntry[] = Array.from({ length: 60 }, (_, i) =>
83
- makeEntry(`id-${i}`, `memory ${i}`),
84
- );
85
- const store = createMockStore(data);
86
-
87
- const config: AutoConsolidationConfig = {
88
- minNewMemories: 50,
89
- minHoursSinceLastRun: 12,
90
- consolidation: DEFAULT_AUTO_CONSOLIDATION_CONFIG.consolidation,
91
- };
92
-
93
- // First run: triggers (never run before, 60 > 50 new memories)
94
- const first = await maybeConsolidate(store, "project:test", config);
95
- expect(first.triggered).toBe(true);
96
-
97
- // Grow the store so memory-count gate would pass
98
- for (let i = 60; i < 120; i++) {
99
- data.push(makeEntry(`id-${i}`, `memory ${i}`));
100
- }
101
- const bigStore = createMockStore(data);
102
-
103
- // Second run immediately: time gate blocks (< 12 hours)
104
- const second = await maybeConsolidate(bigStore, "project:test", config);
105
- expect(second.triggered).toBe(false);
106
- expect(second.reason).toBe("too_soon");
107
- });
108
-
109
- it("triggers when both conditions met", async () => {
110
- const entries = Array.from({ length: 60 }, (_, i) =>
111
- makeEntry(`id-${i}`, `memory ${i}`),
112
- );
113
- const store = createMockStore(entries);
114
-
115
- const config: AutoConsolidationConfig = {
116
- minNewMemories: 50,
117
- minHoursSinceLastRun: 0, // disable time gate for test
118
- consolidation: {
119
- clusterThreshold: 0.82,
120
- mergeThreshold: 0.92,
121
- maxEntriesPerRun: 500,
122
- },
123
- };
124
-
125
- const result = await maybeConsolidate(store, "project:test", config);
126
- expect(result.triggered).toBe(true);
127
- expect(result.consolidation).toBeDefined();
128
- expect(result.consolidation!.scope).toBe("project:test");
129
- });
130
-
131
- it("returns consolidation result when triggered", async () => {
132
- const entries = Array.from({ length: 60 }, (_, i) =>
133
- makeEntry(`id-${i}`, `memory ${i}`),
134
- );
135
- const store = createMockStore(entries);
136
-
137
- const config: AutoConsolidationConfig = {
138
- minNewMemories: 50,
139
- minHoursSinceLastRun: 0,
140
- consolidation: {
141
- clusterThreshold: 0.82,
142
- mergeThreshold: 0.92,
143
- maxEntriesPerRun: 500,
144
- },
145
- };
146
-
147
- const result = await maybeConsolidate(store, "project:test", config);
148
- expect(result.triggered).toBe(true);
149
- expect(result.consolidation!.originalCount).toBe(60);
150
- });
151
-
152
- it("respects custom thresholds", async () => {
153
- const entries = Array.from({ length: 200 }, (_, i) =>
154
- makeEntry(`id-${i}`, `memory ${i}`),
155
- );
156
- const store = createMockStore(entries);
157
-
158
- const config: AutoConsolidationConfig = {
159
- minNewMemories: 300, // higher than available
160
- minHoursSinceLastRun: 0,
161
- consolidation: DEFAULT_AUTO_CONSOLIDATION_CONFIG.consolidation,
162
- };
163
-
164
- const result = await maybeConsolidate(store, "project:test", config);
165
- expect(result.triggered).toBe(false);
166
- expect(result.reason).toBe("insufficient_new_memories");
167
- });
168
- });
@@ -1,131 +0,0 @@
1
- import { describe, expect, it } from "bun:test";
2
-
3
- import { runAutoRecall } from "../auto-recall.js";
4
- import type { RetrievalContext, RetrievalResult } from "../retriever.js";
5
- import type { SessionCheckpointRecord } from "../session-schema.js";
6
-
7
- function buildResult(id: string, category: "profile" | "preferences" | "entities" | "patterns" | "cases" | "events", text: string, scope = "project:test"): RetrievalResult {
8
- return {
9
- entry: {
10
- id,
11
- text,
12
- vector: [],
13
- category,
14
- scope,
15
- importance: 0.8,
16
- timestamp: Date.parse("2026-03-17T00:00:00.000Z"),
17
- metadata: "{}",
18
- },
19
- score: 0.9,
20
- sources: {
21
- fused: { score: 0.9 },
22
- },
23
- };
24
- }
25
-
26
- describe("runAutoRecall", () => {
27
- it("reuses an explicit scope for focused recall after composing resume context", async () => {
28
- const calls: RetrievalContext[] = [];
29
- const retriever = {
30
- async retrieve(context: RetrievalContext): Promise<RetrievalResult[]> {
31
- calls.push(context);
32
- if (context.category === "profile") {
33
- return [buildResult("profile-1", "profile", "Profile: RecallNest maintainer.", "project:alpha")];
34
- }
35
- if (!context.category) {
36
- return [buildResult("event-1", "events", "RecallNest uses Weibull decay.", "project:alpha")];
37
- }
38
- return [];
39
- },
40
- };
41
-
42
- const response = await runAutoRecall({
43
- retriever,
44
- checkpointStore: { async getLatest() { return null; } },
45
- listPins: () => [],
46
- }, {
47
- message: "那个衰减曲线用的什么公式来着",
48
- scope: "project:alpha",
49
- limit: 5,
50
- profile: "default",
51
- operation: "test:auto-recall",
52
- });
53
-
54
- expect(response.mode).toBe("resume+search");
55
- expect(response.resolvedScope).toBe("project:alpha");
56
- expect(response.results[0]?.entry.id).toBe("event-1");
57
- const searchCall = calls.find((call) => !call.category);
58
- expect(searchCall?.scopeFilter).toEqual(["project:alpha"]);
59
- expect(searchCall?.source).toBe("auto-recall");
60
- });
61
-
62
- it("uses the checkpoint-resolved scope when the request only carries sessionId", async () => {
63
- const calls: RetrievalContext[] = [];
64
- const checkpoint: SessionCheckpointRecord = {
65
- checkpointId: "checkpoint-1",
66
- sessionId: "session-123",
67
- resolvedScope: "project:recallnest",
68
- summary: "Continue proactive recall work.",
69
- task: "Implement auto recall route",
70
- decisions: [],
71
- openLoops: [],
72
- nextActions: [],
73
- entities: [],
74
- files: [],
75
- updatedAt: "2026-03-17T10:00:00.000Z",
76
- };
77
-
78
- const retriever = {
79
- async retrieve(context: RetrievalContext): Promise<RetrievalResult[]> {
80
- calls.push(context);
81
- if (!context.category) {
82
- return [buildResult("event-1", "events", "Add a true auto recall route.", "project:recallnest")];
83
- }
84
- return [];
85
- },
86
- };
87
-
88
- const response = await runAutoRecall({
89
- retriever,
90
- checkpointStore: {
91
- async getLatest(query?: { sessionId?: string }) {
92
- return query?.sessionId === "session-123" ? checkpoint : null;
93
- },
94
- },
95
- listPins: () => [],
96
- }, {
97
- message: "继续做主动 recall",
98
- sessionId: "session-123",
99
- operation: "test:auto-recall",
100
- });
101
-
102
- expect(response.resolvedScope).toBe("project:recallnest");
103
- expect(response.resume.latestCheckpoint?.resolvedScope).toBe("project:recallnest");
104
- const searchCall = calls.find((call) => !call.category);
105
- expect(searchCall?.scopeFilter).toEqual(["project:recallnest"]);
106
- });
107
-
108
- it("falls back to resume-only mode when no scope can be inferred for focused search", async () => {
109
- const calls: RetrievalContext[] = [];
110
- const retriever = {
111
- async retrieve(context: RetrievalContext): Promise<RetrievalResult[]> {
112
- calls.push(context);
113
- return [];
114
- },
115
- };
116
-
117
- const response = await runAutoRecall({
118
- retriever,
119
- checkpointStore: { async getLatest() { return null; } },
120
- listPins: () => [],
121
- }, {
122
- message: "what do you remember about this",
123
- operation: "test:auto-recall",
124
- });
125
-
126
- expect(response.mode).toBe("resume-only");
127
- expect(response.results).toHaveLength(0);
128
- expect(response.searchSkippedReason).toContain("No explicit or inferred scope");
129
- expect(calls.some((call) => !call.category)).toBe(false);
130
- });
131
- });