recallnest 1.4.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 (261) hide show
  1. package/.claude-plugin/CLAUDE.md +29 -0
  2. package/.claude-plugin/marketplace.json +18 -0
  3. package/.env.example +3 -0
  4. package/.github/workflows/ci.yml +28 -0
  5. package/.mcp.json +9 -0
  6. package/CHANGELOG.md +51 -0
  7. package/CLAUDE.md +42 -0
  8. package/DEMO.md +94 -0
  9. package/EVAL.md +89 -0
  10. package/FAILURES.md +75 -0
  11. package/LICENSE +21 -0
  12. package/OPERATIONS.md +59 -0
  13. package/README.md +427 -0
  14. package/README_CN.md +425 -0
  15. package/ROADMAP.md +198 -0
  16. package/assets/recallnest-workbench.png +0 -0
  17. package/assets/ui/app.js +807 -0
  18. package/assets/ui/index.html +154 -0
  19. package/assets/ui/styles.css +605 -0
  20. package/assets/wechat-qr.jpg +0 -0
  21. package/config.json.example +38 -0
  22. package/daily-recall.sh +87 -0
  23. package/daily-reflections/2026-03-05.md +15 -0
  24. package/daily-reflections/2026-03-06.md +15 -0
  25. package/daily-reflections/2026-03-07.md +7 -0
  26. package/daily-reflections/2026-03-08.md +34 -0
  27. package/daily-reflections/2026-03-09.md +35 -0
  28. package/daily-reflections/2026-03-10.md +36 -0
  29. package/daily-reflections/2026-03-11.md +35 -0
  30. package/daily-reflections/2026-03-12.md +36 -0
  31. package/daily-reflections/2026-03-13.md +35 -0
  32. package/daily-reflections/2026-03-14.md +35 -0
  33. package/daily-reflections/2026-03-15.md +35 -0
  34. package/daily-reflections/2026-03-16.md +41 -0
  35. package/daily-reflections/2026-03-17.md +10 -0
  36. package/daily-reflections/2026-03-18.md +10 -0
  37. package/daily-reflections/2026-03-19.md +10 -0
  38. package/daily-reflections/2026-03-20.md +10 -0
  39. package/daily-reflections/2026-03-21.md +10 -0
  40. package/daily-reflections/2026-03-22.md +10 -0
  41. package/daily-reflections/2026-03-23.md +10 -0
  42. package/daily-reflections/2026-03-24.md +10 -0
  43. package/daily-reflections/2026-03-25.md +32 -0
  44. package/daily-reflections/2026-03-26.md +37 -0
  45. package/daily-reflections/2026-03-27.md +35 -0
  46. package/daily-reflections/2026-03-28.md +35 -0
  47. package/daily-reflections/2026-03-29.md +35 -0
  48. package/daily-reflections/2026-03-30.md +36 -0
  49. package/daily-reflections/2026-03-31.md +35 -0
  50. package/daily-reflections/2026-04-01.md +35 -0
  51. package/docs/api-reference.md +1291 -0
  52. package/docs/architecture.md +168 -0
  53. package/docs/memory-boundary-contract.md +226 -0
  54. package/docs/memory-categories.md +119 -0
  55. package/docs/pipeline-audit-ultramemory.md +177 -0
  56. package/docs/plans/2026-03-08-query-expansion.md +270 -0
  57. package/docs/plans/2026-03-16-continuity-layer.md +196 -0
  58. package/docs/plans/mcp-tiering-plan.md +126 -0
  59. package/docs/research/recall-quality-audit-2026-03-22.md +153 -0
  60. package/docs/self-evolution.md +198 -0
  61. package/docs/superpowers/plans/2026-03-19-ingest-pipeline-upgrade.md +782 -0
  62. package/docs/superpowers/specs/2026-03-19-ingest-pipeline-upgrade-design.md +77 -0
  63. package/docs/superpowers/specs/2026-03-23-pull-claude-desktop-design.md +95 -0
  64. package/eval/cases.json +85 -0
  65. package/eval/continuity/case-seeds.json +90 -0
  66. package/eval/continuity/cases.json +302 -0
  67. package/eval/continuity/memory-seeds.json +26 -0
  68. package/eval/continuity/pattern-seeds.json +100 -0
  69. package/eval/continuity/results-2026-03-16.md +117 -0
  70. package/eval/continuity/results-2026-03-19.md +453 -0
  71. package/eval/continuity/runbook.md +110 -0
  72. package/eval/continuity/seed-cases.md +54 -0
  73. package/eval/reports/2026-03-06-abstract-query-baseline.md +96 -0
  74. package/eval/reports/2026-03-06-aws-ssh-baseline.md +82 -0
  75. package/eval/reports/2026-03-06-baseline.md +82 -0
  76. package/eval/reports/query-expansion.md +138 -0
  77. package/integrations/README.md +57 -0
  78. package/integrations/claude-code/README.md +133 -0
  79. package/integrations/claude-code/claude-md-snippet.md +31 -0
  80. package/integrations/claude-code/setup.sh +66 -0
  81. package/integrations/claude-code/smoke-continuity.sh +452 -0
  82. package/integrations/codex/README.md +49 -0
  83. package/integrations/codex/agents-md-snippet.md +31 -0
  84. package/integrations/codex/setup.sh +58 -0
  85. package/integrations/examples/claude-agent-sdk/memory-agent.ts +182 -0
  86. package/integrations/examples/langchain/memory-chain.py +111 -0
  87. package/integrations/examples/openai-agents-sdk/memory-agent.py +109 -0
  88. package/integrations/gemini-cli/README.md +57 -0
  89. package/integrations/gemini-cli/gemini-md-snippet.md +31 -0
  90. package/integrations/gemini-cli/setup.sh +64 -0
  91. package/integrations/shared/install-managed-block.sh +51 -0
  92. package/lm +19 -0
  93. package/package.json +50 -0
  94. package/plan-v1.2-distributable.md +105 -0
  95. package/scripts/archive-distilled-junk.ts +116 -0
  96. package/scripts/backfill-l0l1l2.ts +98 -0
  97. package/scripts/blind-scan.ts +588 -0
  98. package/scripts/cleanup-step1-exact-dups.ts +117 -0
  99. package/scripts/cleanup-step2-template-dups.ts +113 -0
  100. package/scripts/cleanup-step3-stale-content.ts +149 -0
  101. package/scripts/cleanup-step4-orphan-scopes.ts +131 -0
  102. package/scripts/delete-distilled-junk.ts +88 -0
  103. package/scripts/distill-facts.ts +305 -0
  104. package/scripts/health-check.ts +343 -0
  105. package/scripts/incremental-ingest.sh +71 -0
  106. package/scripts/mlp-health-check.ts +382 -0
  107. package/scripts/pull-claude-desktop.ts +515 -0
  108. package/scripts/re-ingest-cc.sh +66 -0
  109. package/scripts/rebalance-memory-health.ts +181 -0
  110. package/scripts/run-desktop-ingest.sh +11 -0
  111. package/scripts/start-server.sh +12 -0
  112. package/scripts/viz-dashboard.html +423 -0
  113. package/scripts/viz-export.ts +703 -0
  114. package/src/__tests__/access-tracker-novelty.test.ts +228 -0
  115. package/src/__tests__/auto-consolidation.test.ts +168 -0
  116. package/src/__tests__/auto-recall.test.ts +131 -0
  117. package/src/__tests__/batch-dedup.test.ts +65 -0
  118. package/src/__tests__/capture-engine.test.ts +1142 -0
  119. package/src/__tests__/capture-heuristic.test.ts +171 -0
  120. package/src/__tests__/cascade-forget.test.ts +210 -0
  121. package/src/__tests__/category-min-scores.test.ts +33 -0
  122. package/src/__tests__/confidence-tracker.test.ts +196 -0
  123. package/src/__tests__/conflict-advisor.test.ts +206 -0
  124. package/src/__tests__/conflict-escalation.test.ts +149 -0
  125. package/src/__tests__/conflict-lifecycle.test.ts +83 -0
  126. package/src/__tests__/conflict-output.test.ts +85 -0
  127. package/src/__tests__/conflict-store.test.ts +374 -0
  128. package/src/__tests__/consolidation-engine.test.ts +181 -0
  129. package/src/__tests__/context-composer-pins.test.ts +64 -0
  130. package/src/__tests__/context-composer-task-selection.test.ts +24 -0
  131. package/src/__tests__/context-renderer.test.ts +91 -0
  132. package/src/__tests__/continuity-eval.test.ts +275 -0
  133. package/src/__tests__/continuity-schema.test.ts +97 -0
  134. package/src/__tests__/core-recall-eval.test.ts +235 -0
  135. package/src/__tests__/core-summary.test.ts +109 -0
  136. package/src/__tests__/dedup-actions.test.ts +58 -0
  137. package/src/__tests__/deterministic-id.test.ts +39 -0
  138. package/src/__tests__/doctor.test.ts +303 -0
  139. package/src/__tests__/drill-down.test.ts +108 -0
  140. package/src/__tests__/embedder.test.ts +122 -0
  141. package/src/__tests__/entity-resolver.test.ts +58 -0
  142. package/src/__tests__/event-segmenter.test.ts +153 -0
  143. package/src/__tests__/hotness-blend.test.ts +76 -0
  144. package/src/__tests__/ingest.test.ts +360 -0
  145. package/src/__tests__/kg-extractor.test.ts +184 -0
  146. package/src/__tests__/kg-store.test.ts +198 -0
  147. package/src/__tests__/llm-consolidation.test.ts +226 -0
  148. package/src/__tests__/memory-boundaries.test.ts +237 -0
  149. package/src/__tests__/memory-health-rebalance.test.ts +144 -0
  150. package/src/__tests__/memory-output.test.ts +215 -0
  151. package/src/__tests__/multi-vector.test.ts +242 -0
  152. package/src/__tests__/ppr-traversal.test.ts +256 -0
  153. package/src/__tests__/preference-guard-smoke.test.ts +211 -0
  154. package/src/__tests__/preference-matcher.test.ts +230 -0
  155. package/src/__tests__/preference-slots.test.ts +106 -0
  156. package/src/__tests__/prospective-memory.test.ts +283 -0
  157. package/src/__tests__/query-entity-detector.test.ts +171 -0
  158. package/src/__tests__/query-expander.test.ts +34 -0
  159. package/src/__tests__/recall-mode.test.ts +64 -0
  160. package/src/__tests__/result-synthesizer.test.ts +198 -0
  161. package/src/__tests__/resume-context.test.ts +4743 -0
  162. package/src/__tests__/retrieval-trace.test.ts +213 -0
  163. package/src/__tests__/retriever-boundary-weight.test.ts +136 -0
  164. package/src/__tests__/retroactive-boost.test.ts +223 -0
  165. package/src/__tests__/scope-inventory.test.ts +200 -0
  166. package/src/__tests__/scope-policy.test.ts +69 -0
  167. package/src/__tests__/session-checkpoint.test.ts +259 -0
  168. package/src/__tests__/session-gc.test.ts +215 -0
  169. package/src/__tests__/store-scope-audit.test.ts +81 -0
  170. package/src/__tests__/temporal-parser.test.ts +241 -0
  171. package/src/__tests__/term-registry.test.ts +121 -0
  172. package/src/__tests__/tool-output-compressor.test.ts +133 -0
  173. package/src/__tests__/version-manager.test.ts +213 -0
  174. package/src/__tests__/workflow-observation.test.ts +400 -0
  175. package/src/access-tracker.ts +316 -0
  176. package/src/adaptive-retrieval.ts +64 -0
  177. package/src/api-server.ts +667 -0
  178. package/src/asset-sync.ts +79 -0
  179. package/src/auto-consolidation.ts +133 -0
  180. package/src/auto-gc.ts +124 -0
  181. package/src/auto-recall.ts +95 -0
  182. package/src/capture-engine.ts +931 -0
  183. package/src/capture-heuristic.ts +177 -0
  184. package/src/cascade-forget.ts +135 -0
  185. package/src/chunker.ts +253 -0
  186. package/src/cli.ts +1917 -0
  187. package/src/confidence-tracker.ts +166 -0
  188. package/src/conflict-advisor.ts +433 -0
  189. package/src/conflict-engine.ts +181 -0
  190. package/src/conflict-escalation.ts +138 -0
  191. package/src/conflict-lifecycle.ts +67 -0
  192. package/src/conflict-output.ts +218 -0
  193. package/src/conflict-schema.ts +130 -0
  194. package/src/conflict-store.ts +127 -0
  195. package/src/consolidation-engine.ts +256 -0
  196. package/src/context-composer-pins.ts +183 -0
  197. package/src/context-composer-queries.ts +121 -0
  198. package/src/context-composer-scope.ts +56 -0
  199. package/src/context-composer-stable-selection.ts +200 -0
  200. package/src/context-composer-stable.ts +217 -0
  201. package/src/context-composer-task-fallbacks.ts +36 -0
  202. package/src/context-composer-task-ranking.ts +197 -0
  203. package/src/context-composer-task-results.ts +137 -0
  204. package/src/context-composer-task-selection.ts +342 -0
  205. package/src/context-composer-text.ts +57 -0
  206. package/src/context-composer.ts +300 -0
  207. package/src/context-renderer.ts +179 -0
  208. package/src/decay-engine.ts +189 -0
  209. package/src/doctor.ts +543 -0
  210. package/src/embedder.ts +557 -0
  211. package/src/entity-resolver.ts +131 -0
  212. package/src/eval.ts +632 -0
  213. package/src/event-segmenter.ts +216 -0
  214. package/src/ingest.ts +1822 -0
  215. package/src/kg-extractor.ts +217 -0
  216. package/src/kg-store.ts +353 -0
  217. package/src/legacy-scope-review.ts +53 -0
  218. package/src/llm-client.ts +629 -0
  219. package/src/llm-consolidation.ts +168 -0
  220. package/src/mcp-server.ts +1337 -0
  221. package/src/memory-assets.ts +475 -0
  222. package/src/memory-boundaries.ts +354 -0
  223. package/src/memory-health-rebalance.ts +220 -0
  224. package/src/memory-output.ts +474 -0
  225. package/src/memory-schema.ts +184 -0
  226. package/src/multi-vector.ts +234 -0
  227. package/src/noise-filter.ts +155 -0
  228. package/src/ppr-traversal.ts +279 -0
  229. package/src/preference-matcher.ts +174 -0
  230. package/src/preference-slots.ts +259 -0
  231. package/src/prospective-memory.ts +198 -0
  232. package/src/query-entity-detector.ts +207 -0
  233. package/src/query-expander.ts +98 -0
  234. package/src/result-synthesizer.ts +114 -0
  235. package/src/retrieval-profiles.ts +101 -0
  236. package/src/retrieval-trace.ts +94 -0
  237. package/src/retriever.ts +1338 -0
  238. package/src/retroactive-boost.ts +154 -0
  239. package/src/rif.ts +73 -0
  240. package/src/runtime-config.ts +172 -0
  241. package/src/schema-utils.ts +56 -0
  242. package/src/scope-inventory.ts +310 -0
  243. package/src/scope-policy.ts +142 -0
  244. package/src/session-engine.ts +168 -0
  245. package/src/session-output.ts +96 -0
  246. package/src/session-schema.ts +60 -0
  247. package/src/session-store.ts +293 -0
  248. package/src/stderr-log.ts +16 -0
  249. package/src/store.ts +903 -0
  250. package/src/temporal-parser.ts +318 -0
  251. package/src/term-registry.ts +791 -0
  252. package/src/tool-output-compressor.ts +115 -0
  253. package/src/tracker.ts +57 -0
  254. package/src/ui-server.ts +430 -0
  255. package/src/version-manager.ts +172 -0
  256. package/src/workflow-observation-engine.ts +316 -0
  257. package/src/workflow-observation-managed.ts +83 -0
  258. package/src/workflow-observation-output.ts +86 -0
  259. package/src/workflow-observation-schema.ts +87 -0
  260. package/src/workflow-observation-scope-review.ts +409 -0
  261. package/src/workflow-observation-store.ts +69 -0
@@ -0,0 +1,310 @@
1
+ import { existsSync, readFileSync, readdirSync } from "node:fs";
2
+ import { basename, join, resolve } from "node:path";
3
+
4
+ import { suppressesLegacyScopeIssue } from "./legacy-scope-review.js";
5
+ import { classifyLegacyScope, type LegacyScopeAudit, type LegacyScopeIssueKind, type MemoryStore } from "./store.js";
6
+
7
+ export type ScopeInventoryLayer =
8
+ | "memories"
9
+ | "pins"
10
+ | "session-checkpoints"
11
+ | "workflow-observations";
12
+
13
+ export interface ScopeInventorySample {
14
+ layer: ScopeInventoryLayer;
15
+ id: string;
16
+ kind: LegacyScopeIssueKind;
17
+ scope: string | null;
18
+ context: string;
19
+ preview: string;
20
+ recordedAt?: string;
21
+ sourcePath?: string;
22
+ }
23
+
24
+ export interface ScopeInventoryLayerReport {
25
+ layer: ScopeInventoryLayer;
26
+ scannedCount: number;
27
+ anomalyCount: number;
28
+ invalidCount: number;
29
+ reviewedCount: number;
30
+ counts: Record<LegacyScopeIssueKind, number>;
31
+ samples: ScopeInventorySample[];
32
+ recommendation: string;
33
+ }
34
+
35
+ export interface ScopeInventoryReport {
36
+ generatedAt: string;
37
+ sampleLimit: number;
38
+ totalScannedCount: number;
39
+ totalAnomalyCount: number;
40
+ totalInvalidCount: number;
41
+ totalReviewedCount: number;
42
+ layers: ScopeInventoryLayerReport[];
43
+ }
44
+
45
+ export interface ScopeInventoryOptions {
46
+ store: MemoryStore;
47
+ sampleLimit?: number;
48
+ pinsDir?: string;
49
+ checkpointsDir?: string;
50
+ workflowObservationsDir?: string;
51
+ }
52
+
53
+ interface FileLayerScanOptions {
54
+ dir: string;
55
+ layer: ScopeInventoryLayer;
56
+ sampleLimit: number;
57
+ readScope: (record: Record<string, unknown>) => unknown;
58
+ readId: (record: Record<string, unknown>, path: string) => string;
59
+ readContext: (record: Record<string, unknown>) => string;
60
+ readPreview: (record: Record<string, unknown>) => string;
61
+ readRecordedAt?: (record: Record<string, unknown>) => string | undefined;
62
+ recommendation: string;
63
+ }
64
+
65
+ function emptyCounts(): Record<LegacyScopeIssueKind, number> {
66
+ return {
67
+ missing: 0,
68
+ empty: 0,
69
+ global: 0,
70
+ };
71
+ }
72
+
73
+ function clampLimit(value: number | undefined, min: number, max: number, fallback: number): number {
74
+ if (!Number.isFinite(value)) return fallback;
75
+ return Math.min(max, Math.max(min, Math.floor(value as number)));
76
+ }
77
+
78
+ function clip(text: string, maxLen = 72): string {
79
+ const compact = text.replace(/\s+/g, " ").trim();
80
+ if (!compact) return "-";
81
+ return compact.length <= maxLen ? compact : `${compact.slice(0, maxLen - 3)}...`;
82
+ }
83
+
84
+ function formatLayerLabel(layer: ScopeInventoryLayer): string {
85
+ switch (layer) {
86
+ case "memories":
87
+ return "memories";
88
+ case "pins":
89
+ return "pins";
90
+ case "session-checkpoints":
91
+ return "session-checkpoints";
92
+ case "workflow-observations":
93
+ return "workflow-observations";
94
+ }
95
+ }
96
+
97
+ function normalizeRecordedAt(value: unknown): string | undefined {
98
+ if (typeof value !== "string") return undefined;
99
+ return Number.isFinite(Date.parse(value)) ? value : undefined;
100
+ }
101
+
102
+ function timestampToIso(value: number): string | undefined {
103
+ if (!Number.isFinite(value)) return undefined;
104
+ return new Date(value).toISOString();
105
+ }
106
+
107
+ function mapMemoryAudit(audit: LegacyScopeAudit): ScopeInventorySample[] {
108
+ return audit.samples.map((sample) => ({
109
+ layer: "memories",
110
+ id: sample.id,
111
+ kind: sample.kind,
112
+ scope: sample.scope,
113
+ context: sample.category,
114
+ preview: clip(sample.text, 80),
115
+ recordedAt: timestampToIso(sample.timestamp),
116
+ }));
117
+ }
118
+
119
+ function scanJsonLayer(options: FileLayerScanOptions): ScopeInventoryLayerReport {
120
+ const counts = emptyCounts();
121
+ if (!existsSync(options.dir)) {
122
+ return {
123
+ layer: options.layer,
124
+ scannedCount: 0,
125
+ anomalyCount: 0,
126
+ invalidCount: 0,
127
+ reviewedCount: 0,
128
+ counts,
129
+ samples: [],
130
+ recommendation: options.recommendation,
131
+ };
132
+ }
133
+
134
+ const files = readdirSync(options.dir)
135
+ .filter((name) => name.endsWith(".json"))
136
+ .map((name) => join(options.dir, name));
137
+
138
+ const anomalies: ScopeInventorySample[] = [];
139
+ let invalidCount = 0;
140
+ let reviewedCount = 0;
141
+
142
+ for (const path of files) {
143
+ let parsed: Record<string, unknown>;
144
+ try {
145
+ parsed = JSON.parse(readFileSync(path, "utf-8")) as Record<string, unknown>;
146
+ } catch {
147
+ invalidCount += 1;
148
+ continue;
149
+ }
150
+
151
+ const rawScope = options.readScope(parsed);
152
+ const kind = classifyLegacyScope(rawScope);
153
+ if (!kind) continue;
154
+ if (suppressesLegacyScopeIssue(parsed.legacyScopeReview, kind)) {
155
+ reviewedCount += 1;
156
+ continue;
157
+ }
158
+
159
+ counts[kind] += 1;
160
+ anomalies.push({
161
+ layer: options.layer,
162
+ id: options.readId(parsed, path),
163
+ kind,
164
+ scope: typeof rawScope === "string" ? rawScope : rawScope == null ? null : String(rawScope),
165
+ context: options.readContext(parsed),
166
+ preview: clip(options.readPreview(parsed), 80),
167
+ recordedAt: options.readRecordedAt?.(parsed),
168
+ sourcePath: path,
169
+ });
170
+ }
171
+
172
+ anomalies.sort((a, b) => {
173
+ const timeDiff = Date.parse(b.recordedAt || "") - Date.parse(a.recordedAt || "");
174
+ if (Number.isFinite(timeDiff) && timeDiff !== 0) return timeDiff;
175
+ return a.id.localeCompare(b.id);
176
+ });
177
+
178
+ return {
179
+ layer: options.layer,
180
+ scannedCount: files.length,
181
+ anomalyCount: counts.missing + counts.empty + counts.global,
182
+ invalidCount,
183
+ reviewedCount,
184
+ counts,
185
+ samples: anomalies.slice(0, options.sampleLimit),
186
+ recommendation: options.recommendation,
187
+ };
188
+ }
189
+
190
+ export async function collectScopeInventory(options: ScopeInventoryOptions): Promise<ScopeInventoryReport> {
191
+ const sampleLimit = clampLimit(options.sampleLimit, 1, 200, 20);
192
+ const [stats, audit] = await Promise.all([
193
+ options.store.stats(),
194
+ options.store.auditLegacyScopes(sampleLimit),
195
+ ]);
196
+
197
+ const memoryLayer: ScopeInventoryLayerReport = {
198
+ layer: "memories",
199
+ scannedCount: stats.totalCount,
200
+ anomalyCount: audit.totalCount,
201
+ invalidCount: 0,
202
+ reviewedCount: 0,
203
+ counts: audit.counts,
204
+ samples: mapMemoryAudit(audit),
205
+ recommendation: "Migrate or delete historical unscoped memory rows before removing compatibility shims.",
206
+ };
207
+
208
+ const pinsDir = options.pinsDir || resolve(import.meta.dir, "../data/pins");
209
+ const checkpointsDir = options.checkpointsDir || resolve(import.meta.dir, "../data/session-checkpoints");
210
+ const workflowObservationsDir = options.workflowObservationsDir || resolve(import.meta.dir, "../data/workflow-observations");
211
+
212
+ const pinLayer = scanJsonLayer({
213
+ dir: pinsDir,
214
+ layer: "pins",
215
+ sampleLimit,
216
+ readScope: (record) => (record.source as Record<string, unknown> | undefined)?.scope,
217
+ readId: (record, path) => typeof record.id === "string" ? record.id : basename(path, ".json"),
218
+ readContext: (record) => typeof record.type === "string" ? record.type : "pin",
219
+ readPreview: (record) => {
220
+ if (typeof record.title === "string" && record.title.trim()) return record.title;
221
+ if (typeof record.summary === "string" && record.summary.trim()) return record.summary;
222
+ return "Pinned asset with legacy source scope";
223
+ },
224
+ readRecordedAt: (record) =>
225
+ normalizeRecordedAt(record.updatedAt) || normalizeRecordedAt(record.createdAt),
226
+ recommendation: "Review pinned assets that still point at global/unscoped sources; re-pin them from a scoped memory if needed.",
227
+ });
228
+
229
+ const checkpointLayer = scanJsonLayer({
230
+ dir: checkpointsDir,
231
+ layer: "session-checkpoints",
232
+ sampleLimit,
233
+ readScope: (record) => record.resolvedScope,
234
+ readId: (record, path) => typeof record.checkpointId === "string" ? record.checkpointId : basename(path, ".json"),
235
+ readContext: (record) => typeof record.sessionId === "string" ? record.sessionId : "checkpoint",
236
+ readPreview: (record) => typeof record.summary === "string" ? record.summary : "Checkpoint with legacy scope state",
237
+ readRecordedAt: (record) => normalizeRecordedAt(record.updatedAt),
238
+ recommendation: "Review or delete legacy checkpoint files with missing/empty/global resolvedScope before relying on them for handoff context.",
239
+ });
240
+
241
+ const workflowLayer = scanJsonLayer({
242
+ dir: workflowObservationsDir,
243
+ layer: "workflow-observations",
244
+ sampleLimit,
245
+ readScope: (record) => record.resolvedScope,
246
+ readId: (record, path) => typeof record.observationId === "string" ? record.observationId : basename(path, ".json"),
247
+ readContext: (record) => typeof record.workflowId === "string" ? record.workflowId : "workflow",
248
+ readPreview: (record) => typeof record.summary === "string" ? record.summary : "Workflow observation with legacy scope state",
249
+ readRecordedAt: (record) => normalizeRecordedAt(record.recordedAt),
250
+ recommendation: "Review or delete legacy workflow observation files with missing/empty/global resolvedScope; they should stay outside normal continuity recall.",
251
+ });
252
+
253
+ const layers = [memoryLayer, pinLayer, checkpointLayer, workflowLayer];
254
+ return {
255
+ generatedAt: new Date().toISOString(),
256
+ sampleLimit,
257
+ totalScannedCount: layers.reduce((sum, layer) => sum + layer.scannedCount, 0),
258
+ totalAnomalyCount: layers.reduce((sum, layer) => sum + layer.anomalyCount, 0),
259
+ totalInvalidCount: layers.reduce((sum, layer) => sum + layer.invalidCount, 0),
260
+ totalReviewedCount: layers.reduce((sum, layer) => sum + layer.reviewedCount, 0),
261
+ layers,
262
+ };
263
+ }
264
+
265
+ function formatScope(value: string | null): string {
266
+ if (value == null) return "<missing>";
267
+ return value.trim() ? value.trim() : "<empty>";
268
+ }
269
+
270
+ export function formatScopeInventoryReport(report: ScopeInventoryReport): string {
271
+ const lines = [
272
+ "Scope inventory",
273
+ "",
274
+ ` Generated at : ${report.generatedAt}`,
275
+ ` Records scanned: ${report.totalScannedCount}`,
276
+ ` Anomalies : ${report.totalAnomalyCount}`,
277
+ ` Invalid files : ${report.totalInvalidCount}`,
278
+ ` Reviewed keeps : ${report.totalReviewedCount}`,
279
+ "",
280
+ " By layer:",
281
+ ];
282
+
283
+ for (const layer of report.layers) {
284
+ lines.push(
285
+ ` ${formatLayerLabel(layer.layer).padEnd(22)} scanned ${String(layer.scannedCount).padStart(4)} anomalies ${String(layer.anomalyCount).padStart(3)} invalid ${String(layer.invalidCount).padStart(3)} reviewed ${String(layer.reviewedCount).padStart(3)} missing ${String(layer.counts.missing).padStart(3)} empty ${String(layer.counts.empty).padStart(3)} global ${String(layer.counts.global).padStart(3)}`
286
+ );
287
+ }
288
+
289
+ const anomalyLayers = report.layers.filter((layer) => layer.samples.length > 0);
290
+ if (anomalyLayers.length === 0) {
291
+ if (report.totalInvalidCount === 0) {
292
+ lines.push("", "No unresolved legacy scope anomalies found across memories, pins, checkpoints, or workflow observations.");
293
+ }
294
+ return lines.join("\n");
295
+ }
296
+
297
+ for (const layer of anomalyLayers) {
298
+ lines.push("", ` [${formatLayerLabel(layer.layer)}]`);
299
+ lines.push(" ID Kind Scope Context Date Preview");
300
+ lines.push(" -------- ------- ----------- ------------------ ---------- -------");
301
+ for (const sample of layer.samples) {
302
+ lines.push(
303
+ ` ${sample.id.slice(0, 8).padEnd(8)} ${sample.kind.padEnd(7)} ${formatScope(sample.scope).padEnd(11)} ${clip(sample.context, 18).padEnd(18)} ${(sample.recordedAt || "-").slice(0, 10).padEnd(10)} ${clip(sample.preview, 56)}`
304
+ );
305
+ }
306
+ lines.push(` Next: ${layer.recommendation}`);
307
+ }
308
+
309
+ return lines.join("\n");
310
+ }
@@ -0,0 +1,142 @@
1
+ import type { RetrievalContext } from "./retriever.js";
2
+
3
+ function normalizeScopeValue(value: string | undefined): string | undefined {
4
+ const trimmed = typeof value === "string" ? value.replace(/\s+/g, " ").trim() : "";
5
+ return trimmed || undefined;
6
+ }
7
+
8
+ function envScopeCandidate(env: NodeJS.ProcessEnv): { scope?: string; inferredFrom?: string } {
9
+ const explicitEnvKeys = [
10
+ "RECALLNEST_DEFAULT_SCOPE",
11
+ "RECALLNEST_SCOPE",
12
+ "RECALLNEST_PROJECT_SCOPE",
13
+ ] as const;
14
+
15
+ for (const key of explicitEnvKeys) {
16
+ const value = normalizeScopeValue(env[key]);
17
+ if (value) {
18
+ return {
19
+ scope: value,
20
+ inferredFrom: key,
21
+ };
22
+ }
23
+ }
24
+
25
+ const sessionId = normalizeScopeValue(env.RECALLNEST_SESSION_ID);
26
+ if (sessionId) {
27
+ return {
28
+ scope: `session:${sessionId}`,
29
+ inferredFrom: "RECALLNEST_SESSION_ID",
30
+ };
31
+ }
32
+
33
+ return {};
34
+ }
35
+
36
+ export function resolveSessionScope(sessionId?: string): string | undefined {
37
+ const normalized = normalizeScopeValue(sessionId);
38
+ return normalized ? `session:${normalized}` : undefined;
39
+ }
40
+
41
+ export function matchesScopeFilter(rowScope: string, scopeFilter?: string[]): boolean {
42
+ if (!scopeFilter || scopeFilter.length === 0) return true;
43
+ return scopeFilter.some((scope) => scope.includes(":") ? rowScope === scope : rowScope.startsWith(scope));
44
+ }
45
+
46
+ export interface ScopeSelectionOptions {
47
+ scope?: string;
48
+ sessionId?: string;
49
+ allScopes?: boolean;
50
+ operation: string;
51
+ env?: NodeJS.ProcessEnv;
52
+ allowUnscoped?: boolean;
53
+ }
54
+
55
+ export interface ScopeSelection {
56
+ allScopes: boolean;
57
+ resolvedScope?: string;
58
+ scopeFilter?: string[];
59
+ inferredFrom?: string;
60
+ }
61
+
62
+ export function resolveScopeSelection(options: ScopeSelectionOptions): ScopeSelection {
63
+ if (options.allScopes) {
64
+ return {
65
+ allScopes: true,
66
+ scopeFilter: undefined,
67
+ inferredFrom: "allScopes",
68
+ };
69
+ }
70
+
71
+ const explicitScope = normalizeScopeValue(options.scope);
72
+ if (explicitScope) {
73
+ return {
74
+ allScopes: false,
75
+ resolvedScope: explicitScope,
76
+ scopeFilter: [explicitScope],
77
+ inferredFrom: "scope",
78
+ };
79
+ }
80
+
81
+ const sessionScope = resolveSessionScope(options.sessionId);
82
+ if (sessionScope) {
83
+ return {
84
+ allScopes: false,
85
+ resolvedScope: sessionScope,
86
+ scopeFilter: [sessionScope],
87
+ inferredFrom: "sessionId",
88
+ };
89
+ }
90
+
91
+ const envSelection = envScopeCandidate(options.env || process.env);
92
+ if (envSelection.scope) {
93
+ return {
94
+ allScopes: false,
95
+ resolvedScope: envSelection.scope,
96
+ scopeFilter: [envSelection.scope],
97
+ inferredFrom: envSelection.inferredFrom,
98
+ };
99
+ }
100
+
101
+ if (options.allowUnscoped) {
102
+ return {
103
+ allScopes: false,
104
+ scopeFilter: undefined,
105
+ };
106
+ }
107
+
108
+ throw new Error(
109
+ `${options.operation} requires a scope. Pass scope explicitly, provide sessionId, or set ` +
110
+ `RECALLNEST_DEFAULT_SCOPE / RECALLNEST_SCOPE / RECALLNEST_SESSION_ID. ` +
111
+ `Use allScopes=true only for explicit cross-scope reads.`,
112
+ );
113
+ }
114
+
115
+ export function buildRetrievalContext(
116
+ base: Omit<RetrievalContext, "scopeFilter"> & {
117
+ scope?: string;
118
+ sessionId?: string;
119
+ allScopes?: boolean;
120
+ },
121
+ options: Pick<ScopeSelectionOptions, "operation" | "env" | "allowUnscoped">,
122
+ ): RetrievalContext {
123
+ const selection = resolveScopeSelection({
124
+ scope: base.scope,
125
+ sessionId: base.sessionId,
126
+ allScopes: base.allScopes,
127
+ operation: options.operation,
128
+ env: options.env,
129
+ allowUnscoped: options.allowUnscoped,
130
+ });
131
+
132
+ return {
133
+ query: base.query,
134
+ limit: base.limit,
135
+ category: base.category,
136
+ source: base.source,
137
+ includeArchived: base.includeArchived,
138
+ trace: base.trace,
139
+ graph: base.graph,
140
+ scopeFilter: selection.scopeFilter,
141
+ };
142
+ }
@@ -0,0 +1,168 @@
1
+ import { randomUUID } from "node:crypto";
2
+
3
+ import {
4
+ type SessionCheckpointInput,
5
+ SessionCheckpointInputSchema,
6
+ type SessionCheckpointRecord,
7
+ SessionCheckpointRecordSchema,
8
+ } from "./session-schema.js";
9
+
10
+ const REPO_STATE_PATTERN = /\bgit status\b|未提交|staged|uncommitted|已修改文件|modified files?|untracked|新增文件|dirty repo|dirty worktree/iu;
11
+ const SENTENCE_SPLIT_PATTERN = /(?:\r?\n)+|(?<=[。!?!?;;])\s+/u;
12
+ const CHECKPOINT_SANITIZABLE_FIELDS = ["summary", "task", "decisions", "openLoops", "nextActions"] as const;
13
+
14
+ type CheckpointSanitizableField = typeof CHECKPOINT_SANITIZABLE_FIELDS[number];
15
+
16
+ interface SanitizedStringResult {
17
+ value: string;
18
+ changed: boolean;
19
+ }
20
+
21
+ interface SanitizedListResult {
22
+ value: string[];
23
+ changed: boolean;
24
+ }
25
+
26
+ export interface SessionCheckpointSanitizationReport {
27
+ changed: boolean;
28
+ changedFields: CheckpointSanitizableField[];
29
+ }
30
+
31
+ export type CheckpointQuality = "rich" | "minimal";
32
+
33
+ export interface SessionCheckpointBuildResult {
34
+ record: SessionCheckpointRecord;
35
+ sanitization: SessionCheckpointSanitizationReport;
36
+ quality: CheckpointQuality;
37
+ }
38
+
39
+ function hasRepoStateText(text: string): boolean {
40
+ return REPO_STATE_PATTERN.test(text);
41
+ }
42
+
43
+ function normalizeCheckpointText(text: string): string {
44
+ return text.replace(/\s+/g, " ").trim();
45
+ }
46
+
47
+ function sanitizeCheckpointSummary(summary: string): SanitizedStringResult {
48
+ const parts = summary
49
+ .split(SENTENCE_SPLIT_PATTERN)
50
+ .map(normalizeCheckpointText)
51
+ .filter(Boolean);
52
+ const kept = parts.filter((part) => !hasRepoStateText(part));
53
+ const value = kept[0] ? kept.join(" ") : "Checkpoint captured current task state without repo-state details.";
54
+ return {
55
+ value,
56
+ changed: kept.length !== parts.length,
57
+ };
58
+ }
59
+
60
+ function sanitizeCheckpointList(items: string[], fallbackWhenRemoved?: string): SanitizedListResult {
61
+ const kept = items
62
+ .map(normalizeCheckpointText)
63
+ .filter(Boolean)
64
+ .filter((item) => !hasRepoStateText(item));
65
+
66
+ if (kept.length > 0) {
67
+ return {
68
+ value: kept,
69
+ changed: kept.length !== items.length,
70
+ };
71
+ }
72
+ if (fallbackWhenRemoved && items.some((item) => hasRepoStateText(String(item)))) {
73
+ return {
74
+ value: [fallbackWhenRemoved],
75
+ changed: true,
76
+ };
77
+ }
78
+ return {
79
+ value: kept,
80
+ changed: kept.length !== items.length,
81
+ };
82
+ }
83
+
84
+ /**
85
+ * Normalize checkpoint scope to a canonical form:
86
+ * - lowercase
87
+ * - bare project names (e.g. "recallnest", "RecallNest") get "project:" prefix
88
+ * - session:* and project:* prefixes are preserved
89
+ */
90
+ export function normalizeCheckpointScope(scope: string): string {
91
+ const trimmed = scope.trim().toLowerCase();
92
+ if (trimmed.startsWith("session:") || trimmed.startsWith("project:")) return trimmed;
93
+ if (trimmed.startsWith("memory:") || trimmed.startsWith("eval:") || trimmed.startsWith("asset:")) return trimmed;
94
+ // Bare project name — add project: prefix if it looks like an identifier (no spaces, no colons)
95
+ if (/^[a-z0-9_-]+$/i.test(trimmed)) return `project:${trimmed}`;
96
+ return trimmed;
97
+ }
98
+
99
+ export function resolveCheckpointScope(input: Pick<SessionCheckpointInput, "sessionId" | "scope">): string {
100
+ const raw = input.scope || `session:${input.sessionId}`;
101
+ return normalizeCheckpointScope(raw);
102
+ }
103
+
104
+ export function buildSessionCheckpointResult(rawInput: unknown): SessionCheckpointBuildResult {
105
+ const input = SessionCheckpointInputSchema.parse(rawInput);
106
+ const summary = sanitizeCheckpointSummary(input.summary);
107
+ const taskChanged = Boolean(input.task && hasRepoStateText(input.task));
108
+ const decisions = sanitizeCheckpointList(input.decisions);
109
+ const openLoops = sanitizeCheckpointList(
110
+ input.openLoops,
111
+ "Current repo state still needs local verification if it matters for the next task.",
112
+ );
113
+ const nextActions = sanitizeCheckpointList(
114
+ input.nextActions,
115
+ "Verify current repo state locally if it matters for the next task.",
116
+ );
117
+ const sanitizedInput: SessionCheckpointInput = {
118
+ ...input,
119
+ summary: summary.value,
120
+ task: taskChanged ? undefined : input.task,
121
+ decisions: decisions.value,
122
+ openLoops: openLoops.value,
123
+ nextActions: nextActions.value,
124
+ };
125
+ const changedFields = CHECKPOINT_SANITIZABLE_FIELDS.filter((field) => {
126
+ switch (field) {
127
+ case "summary":
128
+ return summary.changed;
129
+ case "task":
130
+ return taskChanged;
131
+ case "decisions":
132
+ return decisions.changed;
133
+ case "openLoops":
134
+ return openLoops.changed;
135
+ case "nextActions":
136
+ return nextActions.changed;
137
+ default:
138
+ return false;
139
+ }
140
+ });
141
+
142
+ const record = SessionCheckpointRecordSchema.parse({
143
+ ...sanitizedInput,
144
+ checkpointId: randomUUID(),
145
+ resolvedScope: resolveCheckpointScope(sanitizedInput),
146
+ });
147
+
148
+ const FALLBACK_SUMMARY = "Checkpoint captured current task state without repo-state details.";
149
+ const isFallbackSummary = record.summary === FALLBACK_SUMMARY;
150
+ const hasStructuredContent =
151
+ record.decisions.length > 0 ||
152
+ record.openLoops.length > 0 ||
153
+ record.nextActions.length > 0;
154
+ const quality: CheckpointQuality = isFallbackSummary && !hasStructuredContent ? "minimal" : "rich";
155
+
156
+ return {
157
+ record,
158
+ sanitization: {
159
+ changed: changedFields.length > 0,
160
+ changedFields,
161
+ },
162
+ quality,
163
+ };
164
+ }
165
+
166
+ export function buildSessionCheckpointRecord(rawInput: unknown): SessionCheckpointRecord {
167
+ return buildSessionCheckpointResult(rawInput).record;
168
+ }