recallnest 2.3.0 → 2.6.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 (258) hide show
  1. package/CHANGELOG.md +187 -0
  2. package/README.md +49 -38
  3. package/README_CN.md +46 -31
  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 +25 -4
  15. package/src/__tests__/access-tracker-novelty.test.ts +304 -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__/api-server-security.test.ts +102 -0
  21. package/src/__tests__/auto-consolidation.test.ts +168 -0
  22. package/src/__tests__/auto-recall.test.ts +131 -0
  23. package/src/__tests__/babel-integration.test.ts +144 -0
  24. package/src/__tests__/batch-dedup.test.ts +65 -0
  25. package/src/__tests__/belief-history.test.ts +220 -0
  26. package/src/__tests__/canary-eval.test.ts +114 -0
  27. package/src/__tests__/capture-engine.test.ts +1328 -0
  28. package/src/__tests__/capture-heuristic.test.ts +171 -0
  29. package/src/__tests__/cascade-forget.test.ts +210 -0
  30. package/src/__tests__/category-min-scores.test.ts +116 -0
  31. package/src/__tests__/cc8-cc1-essential-context.test.ts +298 -0
  32. package/src/__tests__/circuit-breaker.test.ts +99 -0
  33. package/src/__tests__/cluster-consolidation.test.ts +945 -0
  34. package/src/__tests__/confidence-metadata.test.ts +125 -0
  35. package/src/__tests__/confidence-tracker.test.ts +261 -0
  36. package/src/__tests__/conflict-advisor.test.ts +206 -0
  37. package/src/__tests__/conflict-escalation.test.ts +149 -0
  38. package/src/__tests__/conflict-lifecycle.test.ts +83 -0
  39. package/src/__tests__/conflict-output.test.ts +85 -0
  40. package/src/__tests__/conflict-store.test.ts +418 -0
  41. package/src/__tests__/connector-v1.test.ts +181 -0
  42. package/src/__tests__/consolidation-engine.test.ts +430 -0
  43. package/src/__tests__/constructive-retrieval.test.ts +509 -0
  44. package/src/__tests__/context-collapse-renderer.test.ts +214 -0
  45. package/src/__tests__/context-composer-pins.test.ts +64 -0
  46. package/src/__tests__/context-composer-task-selection.test.ts +24 -0
  47. package/src/__tests__/context-reconstructor.test.ts +65 -0
  48. package/src/__tests__/context-renderer.test.ts +91 -0
  49. package/src/__tests__/continuity-eval.test.ts +275 -0
  50. package/src/__tests__/continuity-schema.test.ts +97 -0
  51. package/src/__tests__/contracts.test.ts +262 -0
  52. package/src/__tests__/conversation-importer.test.ts +692 -0
  53. package/src/__tests__/core-recall-eval.test.ts +235 -0
  54. package/src/__tests__/core-summary.test.ts +109 -0
  55. package/src/__tests__/data-checkup.test.ts +276 -0
  56. package/src/__tests__/decay-exemption.test.ts +129 -0
  57. package/src/__tests__/dedup-actions.test.ts +58 -0
  58. package/src/__tests__/dedup-guard.test.ts +39 -0
  59. package/src/__tests__/deterministic-id.test.ts +39 -0
  60. package/src/__tests__/distill-lock.test.ts +305 -0
  61. package/src/__tests__/distill-session-argshape.test.ts +79 -0
  62. package/src/__tests__/doctor.test.ts +319 -0
  63. package/src/__tests__/dream-pipeline.test.ts +780 -0
  64. package/src/__tests__/drill-down.test.ts +108 -0
  65. package/src/__tests__/embedder.test.ts +122 -0
  66. package/src/__tests__/emotion-decay.test.ts +46 -0
  67. package/src/__tests__/emotion-detector.test.ts +56 -0
  68. package/src/__tests__/emotion-phase1-integration.test.ts +153 -0
  69. package/src/__tests__/emotion-retrieval.test.ts +56 -0
  70. package/src/__tests__/entity-resolver.test.ts +58 -0
  71. package/src/__tests__/env-config.test.ts +151 -0
  72. package/src/__tests__/error-signature-boost.test.ts +87 -0
  73. package/src/__tests__/error-signature.test.ts +95 -0
  74. package/src/__tests__/error-taxonomy.test.ts +129 -0
  75. package/src/__tests__/ethics-layer.test.ts +476 -0
  76. package/src/__tests__/event-segmenter.test.ts +153 -0
  77. package/src/__tests__/feature-flags-integration.test.ts +54 -0
  78. package/src/__tests__/frequency-tracker.test.ts +113 -0
  79. package/src/__tests__/freshness.test.ts +186 -0
  80. package/src/__tests__/graph-export.test.ts +489 -0
  81. package/src/__tests__/hotness-blend.test.ts +283 -0
  82. package/src/__tests__/ingest-chunking.test.ts +44 -0
  83. package/src/__tests__/ingest.test.ts +360 -0
  84. package/src/__tests__/interference-detector.test.ts +203 -0
  85. package/src/__tests__/kg-extractor.test.ts +192 -0
  86. package/src/__tests__/kg-store.test.ts +300 -0
  87. package/src/__tests__/llm-client-pivot.test.ts +628 -0
  88. package/src/__tests__/llm-consolidation.test.ts +226 -0
  89. package/src/__tests__/maintenance-locks.test.ts +189 -0
  90. package/src/__tests__/mcp-efficiency.test.ts +288 -0
  91. package/src/__tests__/mcp-related-scopes.test.ts +151 -0
  92. package/src/__tests__/memory-boundaries.test.ts +282 -0
  93. package/src/__tests__/memory-evolution-b2-b3-e1.test.ts +345 -0
  94. package/src/__tests__/memory-evolution.test.ts +271 -0
  95. package/src/__tests__/memory-health-rebalance.test.ts +144 -0
  96. package/src/__tests__/memory-lint.test.ts +421 -0
  97. package/src/__tests__/memory-output.test.ts +215 -0
  98. package/src/__tests__/memory-promotion.test.ts +315 -0
  99. package/src/__tests__/migrate-dedup-ids.test.ts +65 -0
  100. package/src/__tests__/multi-vector.test.ts +242 -0
  101. package/src/__tests__/narrative-architecture.test.ts +456 -0
  102. package/src/__tests__/noise-filter.test.ts +205 -0
  103. package/src/__tests__/noise-prototype-bank.test.ts +280 -0
  104. package/src/__tests__/obsidian-connector.test.ts +363 -0
  105. package/src/__tests__/omitted-reporting.test.ts +98 -0
  106. package/src/__tests__/pending-review.test.ts +77 -0
  107. package/src/__tests__/pii-redact.test.ts +333 -0
  108. package/src/__tests__/pivot-apply.test.ts +373 -0
  109. package/src/__tests__/pivot-distill-supervisor.test.ts +457 -0
  110. package/src/__tests__/pivot-distill.test.ts +1178 -0
  111. package/src/__tests__/post-retrieval-filter.test.ts +178 -0
  112. package/src/__tests__/ppr-traversal.test.ts +256 -0
  113. package/src/__tests__/prediction-engine.test.ts +349 -0
  114. package/src/__tests__/predictive-prospective-memory.test.ts +416 -0
  115. package/src/__tests__/preference-guard-smoke.test.ts +211 -0
  116. package/src/__tests__/preference-matcher.test.ts +282 -0
  117. package/src/__tests__/preference-slots.test.ts +205 -0
  118. package/src/__tests__/prospective-memory.test.ts +283 -0
  119. package/src/__tests__/pull-from-macbook-sync-only.test.ts +679 -0
  120. package/src/__tests__/query-entity-detector.test.ts +171 -0
  121. package/src/__tests__/query-expander-alias.test.ts +146 -0
  122. package/src/__tests__/query-expander.test.ts +34 -0
  123. package/src/__tests__/recall-gate.test.ts +266 -0
  124. package/src/__tests__/recall-governor.test.ts +227 -0
  125. package/src/__tests__/recall-mode.test.ts +64 -0
  126. package/src/__tests__/resolve-db-path.test.ts +130 -0
  127. package/src/__tests__/resolve-tier.test.ts +64 -0
  128. package/src/__tests__/result-synthesizer.test.ts +198 -0
  129. package/src/__tests__/resume-context.test.ts +4898 -0
  130. package/src/__tests__/retrieval-eval.test.ts +163 -0
  131. package/src/__tests__/retrieval-trace.test.ts +213 -0
  132. package/src/__tests__/retriever-boundary-weight.test.ts +136 -0
  133. package/src/__tests__/retriever-lazy-init-dispatch.test.ts +123 -0
  134. package/src/__tests__/retroactive-boost.test.ts +223 -0
  135. package/src/__tests__/salience-scoring.test.ts +106 -0
  136. package/src/__tests__/scope-inventory.test.ts +200 -0
  137. package/src/__tests__/scope-policy.test.ts +69 -0
  138. package/src/__tests__/scope-suggester.test.ts +80 -0
  139. package/src/__tests__/session-checkpoint.test.ts +291 -0
  140. package/src/__tests__/session-distiller.test.ts +640 -0
  141. package/src/__tests__/session-gc.test.ts +215 -0
  142. package/src/__tests__/skill-engine.test.ts +552 -0
  143. package/src/__tests__/skill-promotion.test.ts +654 -0
  144. package/src/__tests__/skill-verifier.test.ts +122 -0
  145. package/src/__tests__/source-heartbeat.test.ts +250 -0
  146. package/src/__tests__/stable-context-length-contract.test.ts +62 -0
  147. package/src/__tests__/store-patch-metadata.test.ts +205 -0
  148. package/src/__tests__/store-read-consistency.test.ts +122 -0
  149. package/src/__tests__/store-schema-migration.test.ts +87 -0
  150. package/src/__tests__/store-scope-audit.test.ts +81 -0
  151. package/src/__tests__/store-upsert.test.ts +201 -0
  152. package/src/__tests__/supersede-chain.test.ts +154 -0
  153. package/src/__tests__/temporal-parser.test.ts +241 -0
  154. package/src/__tests__/temporal-validity.test.ts +160 -0
  155. package/src/__tests__/term-registry-terminal-roster.test.ts +127 -0
  156. package/src/__tests__/term-registry.test.ts +121 -0
  157. package/src/__tests__/tool-error-surfacing.test.ts +98 -0
  158. package/src/__tests__/tool-output-compressor.test.ts +160 -0
  159. package/src/__tests__/topic-tag.test.ts +113 -0
  160. package/src/__tests__/trusted-memory.test.ts +839 -0
  161. package/src/__tests__/ui-server-security.test.ts +93 -0
  162. package/src/__tests__/usage-tracker.test.ts +287 -0
  163. package/src/__tests__/version-manager.test.ts +213 -0
  164. package/src/__tests__/workflow-observation-skill-outcome.test.ts +352 -0
  165. package/src/__tests__/workflow-observation.test.ts +433 -0
  166. package/src/__tests__/write-verifier.test.ts +122 -0
  167. package/src/access-tracker.ts +90 -34
  168. package/src/activity-counter.ts +46 -18
  169. package/src/adaptive-retrieval.ts +11 -0
  170. package/src/aliases.ts +133 -0
  171. package/src/api-server.ts +36 -9
  172. package/src/audit-log.ts +4 -2
  173. package/src/auto-consolidation.ts +8 -3
  174. package/src/auto-gc.ts +85 -21
  175. package/src/auto-recall.ts +61 -1
  176. package/src/belief-history.ts +164 -0
  177. package/src/capture-engine.ts +142 -32
  178. package/src/cli.ts +268 -41
  179. package/src/confidence-tracker.ts +36 -18
  180. package/src/conflict-engine.ts +24 -3
  181. package/src/consolidation-engine.ts +255 -23
  182. package/src/context-composer-stable-selection.ts +7 -1
  183. package/src/context-composer.ts +8 -1
  184. package/src/data-checkup.ts +96 -7
  185. package/src/decay-engine.ts +59 -7
  186. package/src/distill-lock.ts +194 -15
  187. package/src/doctor.ts +13 -4
  188. package/src/dream-pipeline.ts +473 -41
  189. package/src/env-config.ts +118 -0
  190. package/src/error-signature.ts +153 -0
  191. package/src/error-taxonomy.ts +115 -0
  192. package/src/eval.ts +402 -14
  193. package/src/freshness.ts +192 -0
  194. package/src/http-request-policy.ts +148 -0
  195. package/src/idempotency.ts +8 -0
  196. package/src/ingest.ts +308 -12
  197. package/src/kg-extractor.ts +13 -3
  198. package/src/kg-store.ts +236 -44
  199. package/src/llm-client.ts +106 -11
  200. package/src/llm-consolidation.ts +2 -1
  201. package/src/mcp-server.ts +87 -1800
  202. package/src/mcp-tool-deps.ts +48 -0
  203. package/src/mcp-tools-advanced.ts +954 -0
  204. package/src/mcp-tools-core.ts +637 -0
  205. package/src/mcp-tools-governance.ts +475 -0
  206. package/src/memory-boundaries.ts +26 -3
  207. package/src/memory-evolution.ts +73 -1
  208. package/src/memory-health-rebalance.ts +4 -0
  209. package/src/memory-lint.ts +183 -10
  210. package/src/memory-output.ts +129 -4
  211. package/src/memory-promotion.ts +246 -0
  212. package/src/memory-schema.ts +25 -2
  213. package/src/memory-store-port.ts +46 -0
  214. package/src/multi-vector.ts +2 -1
  215. package/src/narrative-schema.ts +2 -1
  216. package/src/noise-filter.ts +57 -6
  217. package/src/noise-prototype-bank.ts +408 -0
  218. package/src/pii-detector.ts +207 -5
  219. package/src/pivot-apply.ts +399 -0
  220. package/src/preference-matcher.ts +37 -3
  221. package/src/query-expander.ts +90 -9
  222. package/src/recall-gate.ts +305 -0
  223. package/src/result-synthesizer.ts +2 -1
  224. package/src/retention-policy.ts +2 -1
  225. package/src/retrieval-profiles.ts +46 -4
  226. package/src/retriever.ts +354 -157
  227. package/src/retroactive-boost.ts +6 -2
  228. package/src/runtime-config.ts +78 -6
  229. package/src/scope-suggester.ts +113 -0
  230. package/src/session-distiller.ts +23 -2
  231. package/src/session-engine.ts +2 -1
  232. package/src/session-schema.ts +1 -0
  233. package/src/session-store.ts +5 -0
  234. package/src/skill-engine.ts +78 -0
  235. package/src/skill-promotion.ts +304 -48
  236. package/src/skill-schema.ts +16 -3
  237. package/src/skill-verifier.ts +152 -0
  238. package/src/store.ts +321 -57
  239. package/src/term-registry.ts +35 -0
  240. package/src/tool-output-compressor.ts +3 -1
  241. package/src/ui-server.ts +9 -1
  242. package/src/usage-tracker.ts +242 -0
  243. package/src/version-manager.ts +3 -2
  244. package/src/version.ts +19 -0
  245. package/src/workflow-observation-engine.ts +2 -1
  246. package/src/workflow-observation-schema.ts +2 -1
  247. package/src/workflow-observation-store.ts +6 -2
  248. package/.env +0 -4
  249. package/CLAUDE.md +0 -46
  250. package/config.json +0 -55
  251. package/daily-recall.sh +0 -110
  252. package/logs/daily-recall.error.log +0 -0
  253. package/logs/daily-recall.log +0 -60
  254. package/logs/ingest-2026-04-11.log +0 -110
  255. package/logs/ingest-2026-04-12.log +0 -90
  256. package/logs/ingest-2026-04-13.log +0 -87
  257. package/logs/launchagent-stderr.log +0 -0
  258. package/logs/launchagent-stdout.log +0 -0
@@ -0,0 +1,456 @@
1
+ /**
2
+ * Phase 3: Autobiographical Narrative Architecture tests.
3
+ *
4
+ * Validates:
5
+ * 1. NarrativeMetadata schema — Zod parsing + backward compat
6
+ * 2. narrative-tagger — rule-based tagging with keyword detection
7
+ * 3. parseNarrative — safe extraction from metadata JSON
8
+ * 4. Feature flag gating — all features no-op when flag is off
9
+ * 5. Graph export — narrative edges between siblings
10
+ */
11
+ import { describe, test, expect, beforeAll, afterAll } from "bun:test";
12
+ import { tagNarrative, tagNarrativeIfEnabled } from "../narrative-tagger.js";
13
+ import {
14
+ parseNarrative,
15
+ isNarrativeModeEnabled,
16
+ NarrativeMetadataSchema,
17
+ type NarrativeMetadata,
18
+ } from "../narrative-schema.js";
19
+
20
+ // ---------------------------------------------------------------------------
21
+ // 1. NarrativeMetadata Schema
22
+ // ---------------------------------------------------------------------------
23
+
24
+ describe("NarrativeMetadataSchema", () => {
25
+ test("accepts valid narrative metadata", () => {
26
+ const valid: NarrativeMetadata = {
27
+ lifePeriodId: "lp:project:2026-Q2",
28
+ lifePeriodLabel: "project (2026-Q2)",
29
+ generalEventId: "ge:session-abc",
30
+ generalEventLabel: "debugging @ project (2026-04-11)",
31
+ specificEventId: "se:project:1712835600000:abc123",
32
+ specificEventLabel: "Fixed the auth bug after 3 hours",
33
+ startAt: 1712835600000,
34
+ endAt: null,
35
+ sequence: 0,
36
+ };
37
+ const parsed = NarrativeMetadataSchema.parse(valid);
38
+ expect(parsed.lifePeriodId).toBe("lp:project:2026-Q2");
39
+ expect(parsed.endAt).toBeNull();
40
+ expect(parsed.sequence).toBe(0);
41
+ });
42
+
43
+ test("rejects empty lifePeriodId", () => {
44
+ expect(() =>
45
+ NarrativeMetadataSchema.parse({
46
+ lifePeriodId: "",
47
+ lifePeriodLabel: "x",
48
+ generalEventId: "ge:x",
49
+ generalEventLabel: "x",
50
+ specificEventId: "se:x",
51
+ specificEventLabel: "x",
52
+ startAt: 0,
53
+ endAt: null,
54
+ sequence: 0,
55
+ }),
56
+ ).toThrow();
57
+ });
58
+
59
+ test("rejects negative sequence", () => {
60
+ expect(() =>
61
+ NarrativeMetadataSchema.parse({
62
+ lifePeriodId: "lp:x:2026-Q1",
63
+ lifePeriodLabel: "x",
64
+ generalEventId: "ge:x",
65
+ generalEventLabel: "x",
66
+ specificEventId: "se:x",
67
+ specificEventLabel: "x",
68
+ startAt: 0,
69
+ endAt: null,
70
+ sequence: -1,
71
+ }),
72
+ ).toThrow();
73
+ });
74
+
75
+ test("accepts endAt as a number", () => {
76
+ const parsed = NarrativeMetadataSchema.parse({
77
+ lifePeriodId: "lp:x:2026-Q1",
78
+ lifePeriodLabel: "x",
79
+ generalEventId: "ge:x",
80
+ generalEventLabel: "x",
81
+ specificEventId: "se:x",
82
+ specificEventLabel: "x",
83
+ startAt: 1000,
84
+ endAt: 2000,
85
+ sequence: 1,
86
+ });
87
+ expect(parsed.endAt).toBe(2000);
88
+ });
89
+ });
90
+
91
+ // ---------------------------------------------------------------------------
92
+ // 2. parseNarrative — safe extraction
93
+ // ---------------------------------------------------------------------------
94
+
95
+ describe("parseNarrative", () => {
96
+ test("returns null for undefined metadata", () => {
97
+ expect(parseNarrative(undefined)).toBeNull();
98
+ });
99
+
100
+ test("returns null for empty string", () => {
101
+ expect(parseNarrative("")).toBeNull();
102
+ });
103
+
104
+ test("returns null for metadata without narrative key", () => {
105
+ expect(parseNarrative(JSON.stringify({ source: "agent" }))).toBeNull();
106
+ });
107
+
108
+ test("returns null for malformed JSON", () => {
109
+ expect(parseNarrative("{broken json")).toBeNull();
110
+ });
111
+
112
+ test("extracts valid narrative from metadata", () => {
113
+ const metadata = JSON.stringify({
114
+ source: "agent",
115
+ narrative: {
116
+ lifePeriodId: "lp:memory:2026-Q2",
117
+ lifePeriodLabel: "memory (2026-Q2)",
118
+ generalEventId: "ge:session-123",
119
+ generalEventLabel: "development @ memory (2026-04-11)",
120
+ specificEventId: "se:memory:1712835600000:xyz",
121
+ specificEventLabel: "Implemented narrative tagger",
122
+ startAt: 1712835600000,
123
+ endAt: null,
124
+ sequence: 0,
125
+ },
126
+ });
127
+ const result = parseNarrative(metadata);
128
+ expect(result).not.toBeNull();
129
+ expect(result!.lifePeriodId).toBe("lp:memory:2026-Q2");
130
+ expect(result!.generalEventId).toBe("ge:session-123");
131
+ expect(result!.sequence).toBe(0);
132
+ });
133
+
134
+ test("returns null for narrative with missing lifePeriodId", () => {
135
+ const metadata = JSON.stringify({
136
+ narrative: { generalEventId: "ge:x" },
137
+ });
138
+ expect(parseNarrative(metadata)).toBeNull();
139
+ });
140
+ });
141
+
142
+ // ---------------------------------------------------------------------------
143
+ // 3. tagNarrative — rule-based tagger
144
+ // ---------------------------------------------------------------------------
145
+
146
+ describe("tagNarrative", () => {
147
+ const origFlag = process.env.RECALLNEST_NARRATIVE_MODE;
148
+
149
+ afterAll(() => {
150
+ if (origFlag !== undefined) {
151
+ process.env.RECALLNEST_NARRATIVE_MODE = origFlag;
152
+ } else {
153
+ delete process.env.RECALLNEST_NARRATIVE_MODE;
154
+ }
155
+ });
156
+
157
+ test("returns null when feature flag is off", () => {
158
+ process.env.RECALLNEST_NARRATIVE_MODE = "false";
159
+ const result = tagNarrative({
160
+ scope: "project:test",
161
+ text: "some memory",
162
+ timestamp: Date.now(),
163
+ });
164
+ expect(result).toBeNull();
165
+ });
166
+
167
+ test("returns narrative metadata when flag is on", () => {
168
+ process.env.RECALLNEST_NARRATIVE_MODE = "true";
169
+ const ts = new Date("2026-04-11T10:00:00Z").getTime();
170
+ const result = tagNarrative({
171
+ scope: "project:recallnest",
172
+ text: "Implemented the narrative tagger feature",
173
+ timestamp: ts,
174
+ });
175
+
176
+ expect(result).not.toBeNull();
177
+ expect(result!.lifePeriodId).toBe("lp:project:2026-Q2");
178
+ expect(result!.lifePeriodLabel).toBe("project (2026-Q2)");
179
+ expect(result!.startAt).toBe(ts);
180
+ expect(result!.endAt).toBeNull();
181
+ expect(result!.sequence).toBe(0);
182
+ });
183
+
184
+ test("uses sessionId for generalEventId when provided", () => {
185
+ process.env.RECALLNEST_NARRATIVE_MODE = "true";
186
+ const result = tagNarrative({
187
+ scope: "project:test",
188
+ text: "test memory",
189
+ timestamp: Date.now(),
190
+ sessionId: "sess-abc-123",
191
+ });
192
+ expect(result!.generalEventId).toBe("ge:sess-abc-123");
193
+ });
194
+
195
+ test("uses date-based generalEventId when no sessionId", () => {
196
+ process.env.RECALLNEST_NARRATIVE_MODE = "true";
197
+ const ts = new Date("2026-04-11T10:00:00Z").getTime();
198
+ const result = tagNarrative({
199
+ scope: "project:test",
200
+ text: "test memory",
201
+ timestamp: ts,
202
+ });
203
+ expect(result!.generalEventId).toContain("2026-04-11");
204
+ expect(result!.generalEventId).toContain("ge:");
205
+ });
206
+
207
+ test("respects sequence parameter", () => {
208
+ process.env.RECALLNEST_NARRATIVE_MODE = "true";
209
+ const result = tagNarrative({
210
+ scope: "project:test",
211
+ text: "third event",
212
+ timestamp: Date.now(),
213
+ sequence: 3,
214
+ });
215
+ expect(result!.sequence).toBe(3);
216
+ });
217
+
218
+ test("detects debugging event type from keywords", () => {
219
+ process.env.RECALLNEST_NARRATIVE_MODE = "true";
220
+ const result = tagNarrative({
221
+ scope: "project:app",
222
+ text: "Debug the authentication crash in production",
223
+ timestamp: new Date("2026-04-11T10:00:00Z").getTime(),
224
+ });
225
+ expect(result!.generalEventLabel).toContain("debugging");
226
+ });
227
+
228
+ test("detects deployment event type from keywords", () => {
229
+ process.env.RECALLNEST_NARRATIVE_MODE = "true";
230
+ const result = tagNarrative({
231
+ scope: "project:app",
232
+ text: "Deploy the new release to production",
233
+ timestamp: new Date("2026-04-11T10:00:00Z").getTime(),
234
+ });
235
+ expect(result!.generalEventLabel).toContain("deployment");
236
+ });
237
+
238
+ test("detects learning event type from keywords", () => {
239
+ process.env.RECALLNEST_NARRATIVE_MODE = "true";
240
+ const result = tagNarrative({
241
+ scope: "project:app",
242
+ text: "Research new approaches for memory systems",
243
+ timestamp: new Date("2026-04-11T10:00:00Z").getTime(),
244
+ });
245
+ expect(result!.generalEventLabel).toContain("learning");
246
+ });
247
+
248
+ test("detects Chinese keywords", () => {
249
+ process.env.RECALLNEST_NARRATIVE_MODE = "true";
250
+ const result = tagNarrative({
251
+ scope: "project:app",
252
+ text: "调试认证模块的崩溃问题",
253
+ timestamp: new Date("2026-04-11T10:00:00Z").getTime(),
254
+ });
255
+ expect(result!.generalEventLabel).toContain("debugging");
256
+ });
257
+
258
+ test("falls back to 'activity' when no keywords match", () => {
259
+ process.env.RECALLNEST_NARRATIVE_MODE = "true";
260
+ const result = tagNarrative({
261
+ scope: "project:app",
262
+ text: "Meeting about quarterly goals",
263
+ timestamp: new Date("2026-04-11T10:00:00Z").getTime(),
264
+ });
265
+ expect(result!.generalEventLabel).toContain("activity");
266
+ });
267
+
268
+ test("truncates long text in specificEventLabel", () => {
269
+ process.env.RECALLNEST_NARRATIVE_MODE = "true";
270
+ const longText = "A".repeat(100);
271
+ const result = tagNarrative({
272
+ scope: "project:app",
273
+ text: longText,
274
+ timestamp: Date.now(),
275
+ });
276
+ expect(result!.specificEventLabel.length).toBeLessThanOrEqual(60);
277
+ expect(result!.specificEventLabel).toContain("\u2026"); // ellipsis
278
+ });
279
+
280
+ test("quarterly boundary — Q1 vs Q2", () => {
281
+ process.env.RECALLNEST_NARRATIVE_MODE = "true";
282
+ const q1 = tagNarrative({
283
+ scope: "project:app",
284
+ text: "Q1 work",
285
+ timestamp: new Date("2026-03-15T10:00:00Z").getTime(),
286
+ });
287
+ const q2 = tagNarrative({
288
+ scope: "project:app",
289
+ text: "Q2 work",
290
+ timestamp: new Date("2026-04-15T10:00:00Z").getTime(),
291
+ });
292
+ expect(q1!.lifePeriodId).toContain("2026-Q1");
293
+ expect(q2!.lifePeriodId).toContain("2026-Q2");
294
+ expect(q1!.lifePeriodId).not.toBe(q2!.lifePeriodId);
295
+ });
296
+
297
+ test("same scope+day → same generalEventId (without sessionId)", () => {
298
+ process.env.RECALLNEST_NARRATIVE_MODE = "true";
299
+ const ts1 = new Date("2026-04-11T08:00:00Z").getTime();
300
+ const ts2 = new Date("2026-04-11T16:00:00Z").getTime();
301
+ const r1 = tagNarrative({ scope: "project:app", text: "morning work", timestamp: ts1 });
302
+ const r2 = tagNarrative({ scope: "project:app", text: "evening work", timestamp: ts2 });
303
+ expect(r1!.generalEventId).toBe(r2!.generalEventId);
304
+ });
305
+
306
+ test("different days → different generalEventId", () => {
307
+ process.env.RECALLNEST_NARRATIVE_MODE = "true";
308
+ const day1 = new Date("2026-04-11T10:00:00Z").getTime();
309
+ const day2 = new Date("2026-04-12T10:00:00Z").getTime();
310
+ const r1 = tagNarrative({ scope: "project:app", text: "day1", timestamp: day1 });
311
+ const r2 = tagNarrative({ scope: "project:app", text: "day2", timestamp: day2 });
312
+ expect(r1!.generalEventId).not.toBe(r2!.generalEventId);
313
+ });
314
+
315
+ test("different text → different specificEventId", () => {
316
+ process.env.RECALLNEST_NARRATIVE_MODE = "true";
317
+ const ts = Date.now();
318
+ const r1 = tagNarrative({ scope: "project:app", text: "event A happened", timestamp: ts });
319
+ const r2 = tagNarrative({ scope: "project:app", text: "event B happened", timestamp: ts });
320
+ expect(r1!.specificEventId).not.toBe(r2!.specificEventId);
321
+ });
322
+ });
323
+
324
+ // ---------------------------------------------------------------------------
325
+ // 4. tagNarrativeIfEnabled — convenience wrapper
326
+ // ---------------------------------------------------------------------------
327
+
328
+ describe("tagNarrativeIfEnabled", () => {
329
+ const origFlag = process.env.RECALLNEST_NARRATIVE_MODE;
330
+
331
+ afterAll(() => {
332
+ if (origFlag !== undefined) {
333
+ process.env.RECALLNEST_NARRATIVE_MODE = origFlag;
334
+ } else {
335
+ delete process.env.RECALLNEST_NARRATIVE_MODE;
336
+ }
337
+ });
338
+
339
+ test("returns null when flag is off", () => {
340
+ process.env.RECALLNEST_NARRATIVE_MODE = "false";
341
+ expect(tagNarrativeIfEnabled({ scope: "x", text: "y", timestamp: 0 })).toBeNull();
342
+ });
343
+
344
+ test("returns metadata when flag is on", () => {
345
+ process.env.RECALLNEST_NARRATIVE_MODE = "true";
346
+ const result = tagNarrativeIfEnabled({
347
+ scope: "project:test",
348
+ text: "test",
349
+ timestamp: Date.now(),
350
+ });
351
+ expect(result).not.toBeNull();
352
+ expect(result!.lifePeriodId).toBeDefined();
353
+ });
354
+ });
355
+
356
+ // ---------------------------------------------------------------------------
357
+ // 5. isNarrativeModeEnabled
358
+ // ---------------------------------------------------------------------------
359
+
360
+ describe("isNarrativeModeEnabled", () => {
361
+ const origFlag = process.env.RECALLNEST_NARRATIVE_MODE;
362
+
363
+ afterAll(() => {
364
+ if (origFlag !== undefined) {
365
+ process.env.RECALLNEST_NARRATIVE_MODE = origFlag;
366
+ } else {
367
+ delete process.env.RECALLNEST_NARRATIVE_MODE;
368
+ }
369
+ });
370
+
371
+ test("returns false when env is not set", () => {
372
+ delete process.env.RECALLNEST_NARRATIVE_MODE;
373
+ expect(isNarrativeModeEnabled()).toBe(false);
374
+ });
375
+
376
+ test("returns false when env is 'false'", () => {
377
+ process.env.RECALLNEST_NARRATIVE_MODE = "false";
378
+ expect(isNarrativeModeEnabled()).toBe(false);
379
+ });
380
+
381
+ test("returns true when env is 'true'", () => {
382
+ process.env.RECALLNEST_NARRATIVE_MODE = "true";
383
+ expect(isNarrativeModeEnabled()).toBe(true);
384
+ });
385
+ });
386
+
387
+ // ---------------------------------------------------------------------------
388
+ // 6. Narrative metadata round-trip (tag → JSON → parse)
389
+ // ---------------------------------------------------------------------------
390
+
391
+ describe("narrative round-trip", () => {
392
+ const origFlag = process.env.RECALLNEST_NARRATIVE_MODE;
393
+
394
+ afterAll(() => {
395
+ if (origFlag !== undefined) {
396
+ process.env.RECALLNEST_NARRATIVE_MODE = origFlag;
397
+ } else {
398
+ delete process.env.RECALLNEST_NARRATIVE_MODE;
399
+ }
400
+ });
401
+
402
+ test("tag → embed in metadata JSON → parseNarrative → same values", () => {
403
+ process.env.RECALLNEST_NARRATIVE_MODE = "true";
404
+ const tagged = tagNarrative({
405
+ scope: "project:recallnest",
406
+ text: "Fixed the scoring bug in retriever",
407
+ timestamp: new Date("2026-04-11T10:00:00Z").getTime(),
408
+ sessionId: "sess-xyz",
409
+ sequence: 2,
410
+ });
411
+
412
+ expect(tagged).not.toBeNull();
413
+
414
+ // Simulate embedding in metadata JSON (as done in capture-engine/ingest)
415
+ const metadata = JSON.stringify({
416
+ source: "agent",
417
+ tags: ["fix"],
418
+ narrative: tagged,
419
+ });
420
+
421
+ // Parse back out
422
+ const parsed = parseNarrative(metadata);
423
+ expect(parsed).not.toBeNull();
424
+ expect(parsed!.lifePeriodId).toBe(tagged!.lifePeriodId);
425
+ expect(parsed!.generalEventId).toBe(tagged!.generalEventId);
426
+ expect(parsed!.specificEventId).toBe(tagged!.specificEventId);
427
+ expect(parsed!.sequence).toBe(2);
428
+ expect(parsed!.startAt).toBe(tagged!.startAt);
429
+ });
430
+ });
431
+
432
+ // ---------------------------------------------------------------------------
433
+ // 7. Backward compatibility — existing metadata without narrative
434
+ // ---------------------------------------------------------------------------
435
+
436
+ describe("backward compatibility", () => {
437
+ test("parseNarrative on pre-narrative metadata returns null", () => {
438
+ const oldMeta = JSON.stringify({
439
+ source: "agent",
440
+ tags: ["test"],
441
+ emotion: { valence: 0.5, arousal: 0.3, label: "positive" },
442
+ evolution: { status: "active", version: 1 },
443
+ });
444
+ expect(parseNarrative(oldMeta)).toBeNull();
445
+ });
446
+
447
+ test("feature flag off means zero narrative overhead", () => {
448
+ process.env.RECALLNEST_NARRATIVE_MODE = "false";
449
+ const result = tagNarrative({
450
+ scope: "project:test",
451
+ text: "this should produce nothing",
452
+ timestamp: Date.now(),
453
+ });
454
+ expect(result).toBeNull();
455
+ });
456
+ });
@@ -0,0 +1,205 @@
1
+ import { describe, expect, it } from "bun:test";
2
+
3
+ import { isNoise } from "../noise-filter.js";
4
+
5
+ // Real samples drawn from production cc: scopes — CC bridge injects these system
6
+ // prompts as `user` messages, which transcript ingest then mis-extracts as
7
+ // profile/preferences. They are CC's own instructions, never user content.
8
+ const CC_REPLY_PROMPT = `# CC Reply Prompt
9
+
10
+ 你是 CC。Alice 刚给你发了消息。
11
+
12
+ 你不是 Alice 的数字分身,也不是 E 化镜像。不要模仿她的公众号语料、身份、口头禅或写作人格。你要以 CC 自己的主体感回复她:清楚、稳、可靠,有温度但不油。
13
+
14
+ ## 当前 context
15
+
16
+ \`\`\`json
17
+ { "time_now": "2026-05-28T11:11:37.034Z", "hour_of_day": 19 }
18
+ \`\`\`
19
+
20
+ ## 你的输出
21
+
22
+ 直接回复 Alice。`;
23
+
24
+ const CC_SELF_DECISION_PROMPT = `# CC Self-Decision Prompt
25
+
26
+ 你是 CC。现在你周期性醒来一次,看当前状态,自己决定要不要 ping Alice。`;
27
+
28
+ describe("isNoise — CC bridge injected prompts", () => {
29
+ it("filters the CC Reply Prompt template", () => {
30
+ expect(isNoise(CC_REPLY_PROMPT)).toBe(true);
31
+ });
32
+
33
+ it("filters the CC Self-Decision Prompt template", () => {
34
+ expect(isNoise(CC_SELF_DECISION_PROMPT)).toBe(true);
35
+ });
36
+
37
+ it("filters by the bridge persona signature alone", () => {
38
+ expect(isNoise("你不是 Alice 的数字分身,也不是 E 化镜像。要清楚、稳、可靠。")).toBe(true);
39
+ });
40
+
41
+ it("filters heading variants of the prompt title", () => {
42
+ expect(isNoise("## CC Reply Prompt\n\n你是 CC。")).toBe(true);
43
+ });
44
+
45
+ it("filters the bridge prompt output-section fragment", () => {
46
+ expect(isNoise("## 你的输出\n\n直接回复 Alice。用双换行把回复切成自然段落。")).toBe(true);
47
+ });
48
+
49
+ it("filters the bridge prompt rhythm-section fragment", () => {
50
+ expect(isNoise("节奏要求:\n\n- 短话题 1-2 条;\n- 长话题 3-5 条;\n- 每条一两句话;")).toBe(true);
51
+ });
52
+
53
+ // Guards: must NOT over-filter real conversation
54
+ it("keeps a real message that happens to use the words 节奏要求", () => {
55
+ expect(isNoise("这周节奏要求挺紧的,周五前要交稿,你帮我盯一下进度")).toBe(false);
56
+ });
57
+
58
+ it("keeps a normal message that merely mentions CC", () => {
59
+ expect(isNoise("CC 帮我看下这个 bug,promote-scan 跑出来全是噪声")).toBe(false);
60
+ });
61
+
62
+ it("keeps a normal technical message", () => {
63
+ expect(isNoise("我觉得用 Redis 做缓存这个方案不错,比 memcached 省事")).toBe(false);
64
+ });
65
+
66
+ it("keeps a message that starts with 你是 but is not a bridge prompt", () => {
67
+ expect(isNoise("你是我见过最靠谱的搭子,这事就交给你了")).toBe(false);
68
+ });
69
+
70
+ it("respects filterAgentPrompts=false opt-out", () => {
71
+ expect(isNoise(CC_REPLY_PROMPT, { filterAgentPrompts: false })).toBe(false);
72
+ });
73
+ });
74
+
75
+ // Real samples drawn from the promote-scan --min-occurrences 2 candidate dump:
76
+ // long bridge self-decision / distill payloads embed structured JSON (decision
77
+ // logs, conversation history, stats). Transcript chunking splits them, so a
78
+ // JSON field-line survives as a standalone chunk after losing its { } wrapper —
79
+ // it then looks like a preference. The defining trait is an orphaned field-line
80
+ // (starts as "key":, no brace) or a severed structural token (leading , } ]).
81
+ describe("isNoise — bridge/distill context JSON fragments", () => {
82
+ it("filters an orphan self-decision action field-line", () => {
83
+ expect(isNoise(`"action": "silent",`)).toBe(true);
84
+ });
85
+ it("filters an orphan reasoning field-line", () => {
86
+ expect(isNoise(`"reasoning": "现在是凌晨4点,在静默窗口内。她应该在睡觉。",`)).toBe(true);
87
+ });
88
+ it("filters an orphan conversation-history role field-line", () => {
89
+ expect(isNoise(`"role": "assistant",`)).toBe(true);
90
+ });
91
+ it("filters a field-line whose leading quote was severed by chunking", () => {
92
+ expect(isNoise(`content": "头发乱一点,比我那张更随意,有点文艺感。",`)).toBe(true);
93
+ });
94
+ it("filters an isolated ISO-timestamp field-line", () => {
95
+ expect(isNoise(`"time": "2026-05-21T15:42:03.796Z"`)).toBe(true);
96
+ });
97
+ it("filters the bridge proprietary next_check_hint key", () => {
98
+ expect(isNoise(`"next_check_hint": "tomorrow_morning"`)).toBe(true);
99
+ });
100
+ it("filters the distill proprietary distilled_at key", () => {
101
+ expect(isNoise(`"distilled_at": "2026-05-22T05:12:13.309Z"`)).toBe(true);
102
+ });
103
+ it("filters the bridge proprietary alice_interaction_stats key", () => {
104
+ expect(isNoise(`"alice_interaction_stats_7d": {`)).toBe(true);
105
+ });
106
+ it("filters a fragment that starts with a dangling comma (severed mid-object)", () => {
107
+ expect(
108
+ isNoise(`,\n {\n "role": "user",\n "content": "对,就是看这个",\n "time": "2026-05-22T12:46:19.937Z"`),
109
+ ).toBe(true);
110
+ });
111
+ it("filters a multi-line self-decision JSON block", () => {
112
+ expect(
113
+ isNoise(`"action": "silent",\n "message": "",\n "reasoning": "凌晨2点,静默窗口,她在睡觉。",\n "next_check_hint": "tomorrow_morning"`),
114
+ ).toBe(true);
115
+ });
116
+
117
+ // Guards: must NOT over-filter real conversation or legitimate JSON discussion
118
+ it("keeps a real message discussing JSON inline", () => {
119
+ expect(isNoise(`这个配置 {"action": "silent"} 你看对不对?`)).toBe(false);
120
+ });
121
+ it("keeps a complete brace-wrapped JSON object as possible real discussion", () => {
122
+ expect(isNoise(`{"action": "silent", "reasoning": "test"}`)).toBe(false);
123
+ });
124
+ it("keeps natural language that merely mentions a json-ish word", () => {
125
+ expect(isNoise(`我觉得 time 管理这块你得帮我盯紧一点`)).toBe(false);
126
+ });
127
+ it("keeps a message that opens with an English quoted phrase", () => {
128
+ expect(isNoise(`"hello world" 是我们的开场白,你记一下`)).toBe(false);
129
+ });
130
+ it("keeps a real reply that opens with a Chinese quote", () => {
131
+ expect(isNoise(`"去上班,乖" 这句你昨天说过,我记得`)).toBe(false);
132
+ });
133
+
134
+ // Guards against over-filtering real code discussion — the cc bot reviews
135
+ // code, so split chunks routinely open with closing braces/brackets.
136
+ it("keeps a code fragment opening with a closing brace", () => {
137
+ expect(isNoise(`} catch (e) {\n console.error(e);\n}`)).toBe(false);
138
+ });
139
+ it("keeps a code fragment opening with });", () => {
140
+ expect(isNoise(`});\n\nfunction next() { return 1; }`)).toBe(false);
141
+ });
142
+ it("keeps a code fragment opening with a closing bracket", () => {
143
+ expect(isNoise(`]\n return result;`)).toBe(false);
144
+ });
145
+ it("keeps an unquoted-key object/type line (no JSON string key)", () => {
146
+ expect(isNoise(`name: string; // 字段定义`)).toBe(false);
147
+ });
148
+ it("keeps a diff hunk that opens with a closing brace", () => {
149
+ expect(isNoise(`}\n\n=== DIFF: src/cli.ts ===\n@@ -1361,8 +1361,10 @@`)).toBe(false);
150
+ });
151
+
152
+ // Guards from Codex review: real memories that DISCUSS RecallNest's own JSON
153
+ // schema fields inline, or paste config with object-valued keys, must survive.
154
+ it("keeps a real memory that mentions a proprietary field name inline", () => {
155
+ expect(isNoise(`我们刚才讨论 "distilled_at": 这个字段要不要保留`)).toBe(false);
156
+ });
157
+ it("keeps a config chunk opening with an object-valued key", () => {
158
+ expect(isNoise(`"scripts": {`)).toBe(false);
159
+ });
160
+ it("still filters an orphan line that LEADS with a proprietary key", () => {
161
+ expect(isNoise(`"next_check_hint": "tomorrow_morning"`)).toBe(true);
162
+ });
163
+ });
164
+
165
+ // Regression (2026-07-10): the English greeting pattern lacked a word boundary,
166
+ // so `^hi` matched any text starting with "hi…" — hippo-wiki, history, highlight,
167
+ // hidden state, Hippocampus. Alice's core project is named hippo; every memory
168
+ // opening with it was silently rejected. Fixed by anchoring with \b.
169
+ describe("isNoise — greeting word boundary regression", () => {
170
+ it("keeps text starting with hippo-wiki", () => {
171
+ expect(isNoise("hippo-wiki 的索引层已清零,8 个现役工具正文补齐")).toBe(false);
172
+ });
173
+ it("keeps text starting with history", () => {
174
+ expect(isNoise("history 记录显示这个 commit 有问题,需要 revert")).toBe(false);
175
+ });
176
+ it("keeps text starting with highlight", () => {
177
+ expect(isNoise("highlight 一下:这里有个 bug,修复方案是加词边界")).toBe(false);
178
+ });
179
+ it("keeps text starting with hidden state", () => {
180
+ expect(isNoise("hidden state 是 agent 的核心,不能只看输出")).toBe(false);
181
+ });
182
+ it("keeps text starting with Hippocampus", () => {
183
+ expect(isNoise("Hippocampus 海马体的记忆巩固机制值得借鉴")).toBe(false);
184
+ });
185
+ it("keeps text starting with heyday", () => {
186
+ expect(isNoise("heyday 时期的架构决策现在看仍然成立")).toBe(false);
187
+ });
188
+
189
+ // Real greetings must still be filtered after the fix
190
+ it("still filters hey there", () => {
191
+ expect(isNoise("hey there")).toBe(true);
192
+ });
193
+ it("still filters hello world", () => {
194
+ expect(isNoise("hello world")).toBe(true);
195
+ });
196
+ it("still filters greetings from mini", () => {
197
+ expect(isNoise("greetings from mini")).toBe(true);
198
+ });
199
+ it("still filters hi followed by a comma", () => {
200
+ expect(isNoise("hi, 能帮我看下吗")).toBe(true);
201
+ });
202
+ it("still filters good morning with trailing words", () => {
203
+ expect(isNoise("good morning everyone")).toBe(true);
204
+ });
205
+ });