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,291 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { mkdtempSync, readdirSync, rmSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+
6
+ import { buildSessionCheckpointRecord, buildSessionCheckpointResult, normalizeCheckpointScope, resolveCheckpointScope, type CheckpointQuality } from "../session-engine.js";
7
+ import { formatCheckpointRecallSummary, formatCheckpointSaved, formatCheckpointSummary, formatResumeContext } from "../session-output.js";
8
+ import { SessionCheckpointStore } from "../session-store.js";
9
+
10
+ describe("session checkpoint engine", () => {
11
+ it("defaults checkpoint scope to session:<sessionId>", () => {
12
+ const record = buildSessionCheckpointRecord({
13
+ sessionId: "session-abc",
14
+ summary: "Implement checkpoint storage",
15
+ });
16
+
17
+ expect(resolveCheckpointScope(record)).toBe("session:session-abc");
18
+ expect(record.resolvedScope).toBe("session:session-abc");
19
+ });
20
+ });
21
+
22
+ describe("SessionCheckpointStore", () => {
23
+ it("saves and retrieves latest checkpoints by scope and session", async () => {
24
+ const dir = mkdtempSync(join(tmpdir(), "recallnest-checkpoints-"));
25
+ try {
26
+ const store = new SessionCheckpointStore(dir);
27
+
28
+ const first = await store.save(buildSessionCheckpointRecord({
29
+ sessionId: "session-1",
30
+ scope: "agent:codex",
31
+ summary: "First checkpoint",
32
+ nextActions: ["Implement checkpoint_session"],
33
+ updatedAt: "2026-03-16T03:00:00.000Z",
34
+ }));
35
+
36
+ const second = await store.save(buildSessionCheckpointRecord({
37
+ sessionId: "session-2",
38
+ scope: "agent:codex",
39
+ summary: "Second checkpoint",
40
+ updatedAt: "2026-03-16T03:05:00.000Z",
41
+ }));
42
+
43
+ const latestByScope = await store.getLatest({ scope: "agent:codex" });
44
+ const latestBySession = await store.getLatest({ sessionId: "session-1" });
45
+
46
+ expect(latestByScope?.checkpointId).toBe(second.checkpointId);
47
+ expect(latestBySession?.checkpointId).toBe(first.checkpointId);
48
+ } finally {
49
+ rmSync(dir, { recursive: true, force: true });
50
+ }
51
+ });
52
+
53
+ it("uses idempotencyKey to replace retried checkpoint files", async () => {
54
+ const dir = mkdtempSync(join(tmpdir(), "recallnest-checkpoint-idempotency-"));
55
+ try {
56
+ const store = new SessionCheckpointStore(dir);
57
+ const baseInput = {
58
+ sessionId: "session-retry",
59
+ scope: "project:recallnest",
60
+ summary: "Save retry-safe checkpoint state.",
61
+ decisions: ["Use explicit idempotency keys for client retries"],
62
+ idempotencyKey: "checkpoint-request-123",
63
+ };
64
+
65
+ const first = await store.save(buildSessionCheckpointRecord({
66
+ ...baseInput,
67
+ updatedAt: "2026-06-26T01:00:00.000Z",
68
+ }));
69
+ const retried = await store.save(buildSessionCheckpointRecord({
70
+ ...baseInput,
71
+ updatedAt: "2026-06-26T01:01:00.000Z",
72
+ }));
73
+
74
+ expect(retried.checkpointId).toBe(first.checkpointId);
75
+ expect(readdirSync(dir).filter((name) => name.endsWith(".json"))).toHaveLength(1);
76
+
77
+ const recent = await store.listRecent({ sessionId: "session-retry", limit: 10 });
78
+ expect(recent).toHaveLength(1);
79
+ expect(recent[0]?.updatedAt).toBe("2026-06-26T01:01:00.000Z");
80
+ } finally {
81
+ rmSync(dir, { recursive: true, force: true });
82
+ }
83
+ });
84
+ });
85
+
86
+ describe("session checkpoint output", () => {
87
+ it("formats saved and summary views", () => {
88
+ const record = buildSessionCheckpointRecord({
89
+ sessionId: "session-xyz",
90
+ summary: "Current task is implementing session checkpoints",
91
+ decisions: ["Keep checkpoints out of LanceDB"],
92
+ openLoops: ["Need resume_context next"],
93
+ nextActions: ["Add latest checkpoint API"],
94
+ });
95
+
96
+ expect(formatCheckpointSaved(record)).toContain("Keep checkpoints out of LanceDB");
97
+ expect(formatCheckpointSummary(record)).toContain("Latest checkpoint");
98
+ expect(formatCheckpointSummary(null)).toBe("No checkpoint found.");
99
+ });
100
+
101
+ it("enriches recall checkpoint summaries with missing entity hints only once", () => {
102
+ const record = buildSessionCheckpointRecord({
103
+ sessionId: "session-entity-hints",
104
+ summary: "Raised continuity coverage for RecallNest startup recovery.",
105
+ entities: ["RecallNest", "smoke:claude-continuity", "resume_context"],
106
+ });
107
+
108
+ expect(formatCheckpointRecallSummary(record)).toBe(
109
+ "Raised continuity coverage for RecallNest startup recovery. Entities: smoke:claude-continuity, resume_context",
110
+ );
111
+ });
112
+
113
+ it("includes recall-only guidance in formatted resume context output", () => {
114
+ const output = formatResumeContext({
115
+ summary: "Stable context: Preference: 用户不喜欢 AI 味太重的文案语气。",
116
+ resolvedScope: "project:writing",
117
+ stableContext: ["Preference: 用户不喜欢 AI 味太重的文案语气。"],
118
+ relevantPatterns: [],
119
+ recentCases: [],
120
+ latestCheckpoint: {
121
+ sessionId: "session-writing",
122
+ resolvedScope: "project:writing",
123
+ summary: "Continue writing-style preference recovery.",
124
+ updatedAt: "2026-03-16T04:30:00.000Z",
125
+ },
126
+ responseMode: "recall-only",
127
+ responseGuidance: "Recall-only mode: answer from the recalled stable context item only.",
128
+ generatedAt: "2026-03-16T04:40:00.000Z",
129
+ });
130
+
131
+ expect(output).toContain("Scope: project:writing");
132
+ expect(output).toContain("Response mode: recall-only");
133
+ expect(output).toContain("Guidance: Recall-only mode");
134
+ expect(output).toContain("Stable context:");
135
+ });
136
+
137
+ it("sanitizes repo-state text out of checkpoint content", () => {
138
+ const result = buildSessionCheckpointResult({
139
+ sessionId: "session-repo-state",
140
+ summary: "Only resumed context in this window. git status shows many modified files.",
141
+ task: "Handle git status follow-up",
142
+ decisions: ["Keep repo-state out of checkpoints."],
143
+ openLoops: ["git status shows many uncommitted changes that still need review"],
144
+ nextActions: ["Process modified files after checking git status locally"],
145
+ });
146
+ const { record } = result;
147
+
148
+ expect(record.summary).toBe("Checkpoint captured current task state without repo-state details.");
149
+ expect(record.task).toBeUndefined();
150
+ expect(record.decisions).toEqual(["Keep repo-state out of checkpoints."]);
151
+ expect(record.openLoops).toEqual(["Current repo state still needs local verification if it matters for the next task."]);
152
+ expect(record.nextActions).toEqual(["Verify current repo state locally if it matters for the next task."]);
153
+ expect(result.sanitization.changed).toBe(true);
154
+ expect(result.sanitization.changedFields).toEqual(["summary", "task", "openLoops", "nextActions"]);
155
+ });
156
+
157
+ it("does not mark sanitization when checkpoint content is already clean", () => {
158
+ const result = buildSessionCheckpointResult({
159
+ sessionId: "session-clean",
160
+ summary: "Continue wiring workflow observations into managed continuity.",
161
+ task: "Hook managed continuity observations",
162
+ decisions: ["Keep observations out of durable memory."],
163
+ openLoops: ["Need to update the managed snippets."],
164
+ nextActions: ["Add MCP and HTTP auto-observation coverage."],
165
+ });
166
+
167
+ expect(result.sanitization.changed).toBe(false);
168
+ expect(result.sanitization.changedFields).toEqual([]);
169
+ });
170
+ });
171
+
172
+ describe("checkpoint scope normalization", () => {
173
+ it("normalizes bare project names to project: prefix", () => {
174
+ expect(normalizeCheckpointScope("recallnest")).toBe("project:recallnest");
175
+ expect(normalizeCheckpointScope("RecallNest")).toBe("project:recallnest");
176
+ expect(normalizeCheckpointScope("telegram-bridge")).toBe("project:telegram-bridge");
177
+ });
178
+
179
+ it("preserves existing prefixes but lowercases", () => {
180
+ expect(normalizeCheckpointScope("project:RecallNest")).toBe("project:recallnest");
181
+ expect(normalizeCheckpointScope("session:abc")).toBe("session:abc");
182
+ expect(normalizeCheckpointScope("eval:continuity")).toBe("eval:continuity");
183
+ });
184
+
185
+ it("applies normalization at save time via resolveCheckpointScope", () => {
186
+ expect(resolveCheckpointScope({ sessionId: "s1", scope: "RecallNest" })).toBe("project:recallnest");
187
+ expect(resolveCheckpointScope({ sessionId: "s1", scope: "project:RecallNest" })).toBe("project:recallnest");
188
+ expect(resolveCheckpointScope({ sessionId: "s1" })).toBe("session:s1");
189
+ });
190
+
191
+ it("matches checkpoints across scope variants in getLatest", async () => {
192
+ const dir = mkdtempSync(join(tmpdir(), "recallnest-scope-norm-"));
193
+ try {
194
+ const store = new SessionCheckpointStore(dir);
195
+
196
+ // Save with bare "recallnest" — will be normalized to "project:recallnest"
197
+ await store.save(buildSessionCheckpointRecord({
198
+ sessionId: "s-bare",
199
+ scope: "recallnest",
200
+ summary: "Bare scope checkpoint",
201
+ }));
202
+
203
+ // Save with "project:RecallNest" — will be normalized to "project:recallnest"
204
+ const later = buildSessionCheckpointRecord({
205
+ sessionId: "s-cased",
206
+ scope: "project:RecallNest",
207
+ summary: "Cased scope checkpoint",
208
+ });
209
+ // Manually set a later timestamp
210
+ (later as any).updatedAt = new Date(Date.now() + 1000).toISOString().replace(/\.\d+Z$/, ".000Z");
211
+ await store.save(later);
212
+
213
+ // Query with canonical "project:recallnest" should find both, return latest
214
+ const latest = await store.getLatest({ scope: "project:recallnest" });
215
+ expect(latest).not.toBeNull();
216
+ expect(latest!.summary).toBe("Cased scope checkpoint");
217
+
218
+ // Query with bare "recallnest" should also work
219
+ const latestBare = await store.getLatest({ scope: "recallnest" });
220
+ expect(latestBare).not.toBeNull();
221
+ expect(latestBare!.summary).toBe("Cased scope checkpoint");
222
+ } finally {
223
+ rmSync(dir, { recursive: true, force: true });
224
+ }
225
+ });
226
+ });
227
+
228
+ describe("checkpoint quality gate", () => {
229
+ it("classifies checkpoints with fallback summary and empty fields as minimal", () => {
230
+ const result = buildSessionCheckpointResult({
231
+ sessionId: "s-empty",
232
+ scope: "project:recallnest",
233
+ summary: "git status shows 5 modified files and 3 untracked files",
234
+ });
235
+ // Summary was sanitized to fallback, no decisions/openLoops/nextActions
236
+ expect(result.quality).toBe("minimal");
237
+ expect(result.sanitization.changed).toBe(true);
238
+ });
239
+
240
+ it("classifies checkpoints with real content as rich", () => {
241
+ const result = buildSessionCheckpointResult({
242
+ sessionId: "s-rich",
243
+ scope: "project:recallnest",
244
+ summary: "Completed scope normalization and checkpoint quality gate.",
245
+ decisions: ["Normalize scope at save and query time"],
246
+ });
247
+ expect(result.quality).toBe("rich");
248
+ });
249
+
250
+ it("classifies checkpoints with fallback summary but structured fields as rich", () => {
251
+ const result = buildSessionCheckpointResult({
252
+ sessionId: "s-mixed",
253
+ scope: "project:recallnest",
254
+ summary: "git status shows dirty worktree",
255
+ decisions: ["Keep using bun for builds"],
256
+ });
257
+ // Summary sanitized to fallback but decisions non-empty → still rich
258
+ expect(result.quality).toBe("rich");
259
+ });
260
+
261
+ it("prefers rich checkpoints over minimal ones in getLatest", async () => {
262
+ const dir = mkdtempSync(join(tmpdir(), "recallnest-quality-gate-"));
263
+ try {
264
+ const store = new SessionCheckpointStore(dir);
265
+
266
+ // Save a rich checkpoint first (older)
267
+ await store.save(buildSessionCheckpointRecord({
268
+ sessionId: "s-rich",
269
+ scope: "project:recallnest",
270
+ summary: "Good checkpoint with real content.",
271
+ decisions: ["Keep checkpoints clean"],
272
+ updatedAt: "2026-03-22T01:00:00.000Z",
273
+ }));
274
+
275
+ // Save a minimal checkpoint later (newer) — all-sanitized fallback
276
+ await store.save(buildSessionCheckpointRecord({
277
+ sessionId: "s-minimal",
278
+ scope: "project:recallnest",
279
+ summary: "Checkpoint captured current task state without repo-state details.",
280
+ updatedAt: "2026-03-22T02:00:00.000Z",
281
+ }));
282
+
283
+ const latest = await store.getLatest({ scope: "project:recallnest" });
284
+ expect(latest).not.toBeNull();
285
+ // Should return the rich one even though it's older
286
+ expect(latest!.summary).toBe("Good checkpoint with real content.");
287
+ } finally {
288
+ rmSync(dir, { recursive: true, force: true });
289
+ }
290
+ });
291
+ });