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,96 @@
1
+ import type { ResumeContextResponse, SessionCheckpointRecord } from "./session-schema.js";
2
+ import { cleanText } from "./context-composer-text.js";
3
+
4
+ function listBlock(label: string, items: string[]): string[] {
5
+ if (items.length === 0) return [];
6
+ return [
7
+ `${label}:`,
8
+ ...items.map((item, index) => `${index + 1}. ${item}`),
9
+ ];
10
+ }
11
+
12
+ export function formatCheckpointRecallSummary(record: SessionCheckpointRecord): string {
13
+ const parts: string[] = [];
14
+ const baseSummary = record.summary.trim();
15
+ if (baseSummary) {
16
+ parts.push(baseSummary);
17
+ }
18
+
19
+ const baseLower = baseSummary.toLowerCase();
20
+ const missingEntities = record.entities
21
+ .filter((entity) => entity.trim().length > 0 && !baseLower.includes(entity.toLowerCase()))
22
+ .slice(0, 2);
23
+ if (missingEntities.length > 0) {
24
+ parts.push(`Entities: ${missingEntities.join(", ")}`);
25
+ }
26
+
27
+ return cleanText(parts.join(" "), 600);
28
+ }
29
+
30
+ export function formatCheckpointSaved(record: SessionCheckpointRecord): string {
31
+ const lines = [
32
+ `Checkpoint ${record.checkpointId.slice(0, 8)}`,
33
+ `Session: ${record.sessionId}`,
34
+ `Scope: ${record.resolvedScope}`,
35
+ `Updated: ${record.updatedAt}`,
36
+ `Summary: ${record.summary}`,
37
+ ...listBlock("Decisions", record.decisions),
38
+ ...listBlock("Open loops", record.openLoops),
39
+ ...listBlock("Next actions", record.nextActions),
40
+ ];
41
+ return lines.join("\n");
42
+ }
43
+
44
+ export function formatCheckpointSummary(record: SessionCheckpointRecord | null): string {
45
+ if (!record) return "No checkpoint found.";
46
+
47
+ const lines = [
48
+ `Latest checkpoint`,
49
+ `Session: ${record.sessionId}`,
50
+ `Scope: ${record.resolvedScope}`,
51
+ `Updated: ${record.updatedAt}`,
52
+ `Summary: ${record.summary}`,
53
+ ];
54
+ if (record.nextActions.length > 0) {
55
+ lines.push(`Next: ${record.nextActions.slice(0, 3).join(" | ")}`);
56
+ }
57
+ return lines.join("\n");
58
+ }
59
+
60
+ export function formatResumeContext(response: ResumeContextResponse): string {
61
+ const lines = [
62
+ "Resume context",
63
+ `Generated: ${response.generatedAt}`,
64
+ `Summary: ${response.summary}`,
65
+ ];
66
+
67
+ if (response.resolvedScope) {
68
+ lines.push(`Scope: ${response.resolvedScope}`);
69
+ }
70
+
71
+ if (response.responseMode !== "default") {
72
+ lines.push(`Response mode: ${response.responseMode}`);
73
+ }
74
+
75
+ if (response.responseGuidance) {
76
+ lines.push(`Guidance: ${response.responseGuidance}`);
77
+ }
78
+
79
+ lines.push(
80
+ ...listBlock("Stable context", response.stableContext),
81
+ ...listBlock("Relevant patterns", response.relevantPatterns),
82
+ ...listBlock("Recent cases", response.recentCases),
83
+ );
84
+
85
+ if (response.latestCheckpoint) {
86
+ lines.push("Latest checkpoint:");
87
+ lines.push(`Session: ${response.latestCheckpoint.sessionId}`);
88
+ if (response.latestCheckpoint.resolvedScope) {
89
+ lines.push(`Scope: ${response.latestCheckpoint.resolvedScope}`);
90
+ }
91
+ lines.push(`Updated: ${response.latestCheckpoint.updatedAt}`);
92
+ lines.push(`Summary: ${response.latestCheckpoint.summary}`);
93
+ }
94
+
95
+ return lines.join("\n");
96
+ }
@@ -0,0 +1,60 @@
1
+ import { z } from "zod";
2
+
3
+ import { boundedStringSchema, identifierSchema, normalizedStringListSchema, optionalBoundedStringSchema } from "./schema-utils.js";
4
+
5
+ export const RetrievalProfileSchema = z.enum(["default", "writing", "debug", "fact-check"]);
6
+
7
+ export const SessionCheckpointInputSchema = z.object({
8
+ sessionId: identifierSchema("sessionId"),
9
+ scope: optionalBoundedStringSchema(160),
10
+ summary: boundedStringSchema("summary", 600),
11
+ task: optionalBoundedStringSchema(240),
12
+ decisions: normalizedStringListSchema("decisions", 6, 200),
13
+ openLoops: normalizedStringListSchema("openLoops", 6, 200),
14
+ nextActions: normalizedStringListSchema("nextActions", 6, 200),
15
+ entities: normalizedStringListSchema("entities", 8, 120),
16
+ files: normalizedStringListSchema("files", 12, 220),
17
+ updatedAt: z.string().datetime().default(() => new Date().toISOString()),
18
+ });
19
+
20
+ export const SessionCheckpointRecordSchema = SessionCheckpointInputSchema.extend({
21
+ checkpointId: identifierSchema("checkpointId", 128),
22
+ resolvedScope: identifierSchema("resolvedScope", 160),
23
+ });
24
+
25
+ export const ResumeContextRequestSchema = z.object({
26
+ task: optionalBoundedStringSchema(500),
27
+ scope: optionalBoundedStringSchema(160),
28
+ sessionId: optionalBoundedStringSchema(160),
29
+ limitPerSection: z.number().int().min(1).max(6).default(3),
30
+ includeLatestCheckpoint: z.boolean().default(true),
31
+ profile: RetrievalProfileSchema.optional(),
32
+ });
33
+
34
+ export const ResumeCheckpointSummarySchema = z.object({
35
+ sessionId: identifierSchema("sessionId"),
36
+ resolvedScope: optionalBoundedStringSchema(160),
37
+ summary: boundedStringSchema("summary", 600),
38
+ updatedAt: z.string().datetime(),
39
+ });
40
+
41
+ export const ResumeResponseModeSchema = z.enum(["default", "recall-only"]);
42
+
43
+ export const ResumeContextResponseSchema = z.object({
44
+ summary: boundedStringSchema("summary", 800),
45
+ resolvedScope: optionalBoundedStringSchema(160),
46
+ stableContext: normalizedStringListSchema("stableContext", 6, 220),
47
+ relevantPatterns: normalizedStringListSchema("relevantPatterns", 6, 220),
48
+ recentCases: normalizedStringListSchema("recentCases", 6, 220),
49
+ latestCheckpoint: ResumeCheckpointSummarySchema.optional(),
50
+ responseMode: ResumeResponseModeSchema.default("default"),
51
+ responseGuidance: optionalBoundedStringSchema(400),
52
+ generatedAt: z.string().datetime(),
53
+ });
54
+
55
+ export type SessionCheckpointInput = z.infer<typeof SessionCheckpointInputSchema>;
56
+ export type SessionCheckpointRecord = z.infer<typeof SessionCheckpointRecordSchema>;
57
+ export type ResumeContextRequest = z.infer<typeof ResumeContextRequestSchema>;
58
+ export type ResumeContextResponse = z.infer<typeof ResumeContextResponseSchema>;
59
+ export type RetrievalProfileName = z.infer<typeof RetrievalProfileSchema>;
60
+ export type ResumeResponseMode = z.infer<typeof ResumeResponseModeSchema>;
@@ -0,0 +1,293 @@
1
+ import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
2
+ import { basename, join, resolve } from "node:path";
3
+
4
+ import type { SessionCheckpointRecord } from "./session-schema.js";
5
+ import { SessionCheckpointRecordSchema } from "./session-schema.js";
6
+ import { normalizeCheckpointScope, type CheckpointQuality } from "./session-engine.js";
7
+
8
+ const FALLBACK_SUMMARY = "Checkpoint captured current task state without repo-state details.";
9
+
10
+ export function classifyCheckpointQuality(record: SessionCheckpointRecord): CheckpointQuality {
11
+ const isFallback = record.summary === FALLBACK_SUMMARY;
12
+ const hasContent = record.decisions.length > 0 || record.openLoops.length > 0 || record.nextActions.length > 0;
13
+ return isFallback && !hasContent ? "minimal" : "rich";
14
+ }
15
+
16
+ export interface SessionCheckpointQuery {
17
+ sessionId?: string;
18
+ scope?: string;
19
+ limit?: number;
20
+ }
21
+
22
+ function ensureDir(dir: string): string {
23
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
24
+ return dir;
25
+ }
26
+
27
+ function sortNewestFirst(records: SessionCheckpointRecord[]): SessionCheckpointRecord[] {
28
+ return [...records].sort((a, b) => {
29
+ const timeDiff = Date.parse(b.updatedAt) - Date.parse(a.updatedAt);
30
+ if (timeDiff !== 0) return timeDiff;
31
+ return b.checkpointId.localeCompare(a.checkpointId);
32
+ });
33
+ }
34
+
35
+ export interface CheckpointGcOptions {
36
+ dryRun?: boolean;
37
+ now?: Date;
38
+ /** Keep N newest rich checkpoints per resolvedScope (default: 5) */
39
+ keepPerScope?: number;
40
+ /** Keep 1 checkpoint per sessionId if younger than this many hours (default: 48) */
41
+ sessionTtlHours?: number;
42
+ /** Delete minimal checkpoints older than this many days (default: 7) */
43
+ minimalTtlDays?: number;
44
+ /** Archive rich checkpoints older than this many days (default: 30) */
45
+ archiveAfterDays?: number;
46
+ /** Hard-delete archived files older than this many days (default: 90) */
47
+ archiveTtlDays?: number;
48
+ }
49
+
50
+ export interface CheckpointGcFileAction {
51
+ file: string;
52
+ checkpointId: string;
53
+ resolvedScope: string;
54
+ quality: CheckpointQuality;
55
+ updatedAt: string;
56
+ action: "keep" | "archive" | "delete";
57
+ reason: string;
58
+ }
59
+
60
+ export interface CheckpointGcResult {
61
+ kept: number;
62
+ archived: number;
63
+ deleted: number;
64
+ errors: string[];
65
+ actions: CheckpointGcFileAction[];
66
+ }
67
+
68
+ export class SessionCheckpointStore {
69
+ constructor(private readonly dir = resolve(import.meta.dir, "../data/session-checkpoints")) {}
70
+
71
+ get dataDir(): string {
72
+ return ensureDir(this.dir);
73
+ }
74
+
75
+ async save(record: SessionCheckpointRecord): Promise<SessionCheckpointRecord> {
76
+ const parsed = SessionCheckpointRecordSchema.parse(record);
77
+ const timestampToken = parsed.updatedAt.replace(/[:.]/g, "-");
78
+ const path = join(this.dataDir, `${timestampToken}-${parsed.checkpointId}.json`);
79
+ writeFileSync(path, JSON.stringify(parsed, null, 2) + "\n");
80
+ return parsed;
81
+ }
82
+
83
+ async listRecent(query: SessionCheckpointQuery = {}): Promise<SessionCheckpointRecord[]> {
84
+ const { sessionId, scope, limit = 20 } = query;
85
+ const files = readdirSync(this.dataDir)
86
+ .filter((name) => name.endsWith(".json"))
87
+ .map((name) => join(this.dataDir, name));
88
+
89
+ const normalizedQueryScope = scope ? normalizeCheckpointScope(scope) : undefined;
90
+ const items: SessionCheckpointRecord[] = [];
91
+ for (const path of files) {
92
+ try {
93
+ const parsed = SessionCheckpointRecordSchema.parse(
94
+ JSON.parse(readFileSync(path, "utf-8")),
95
+ );
96
+ if (sessionId && parsed.sessionId !== sessionId) continue;
97
+ if (normalizedQueryScope) {
98
+ const normalizedRecordScope = normalizeCheckpointScope(parsed.resolvedScope ?? parsed.scope ?? "");
99
+ if (normalizedRecordScope !== normalizedQueryScope) continue;
100
+ }
101
+ items.push(parsed);
102
+ } catch {
103
+ // Skip corrupt checkpoint files.
104
+ }
105
+ }
106
+
107
+ return sortNewestFirst(items).slice(0, limit);
108
+ }
109
+
110
+ async getLatest(query: SessionCheckpointQuery = {}): Promise<SessionCheckpointRecord | null> {
111
+ // Look at recent checkpoints and prefer rich ones over minimal/fallback ones
112
+ const recent = await this.listRecent({ ...query, limit: 5 });
113
+ if (recent.length === 0) return null;
114
+ const rich = recent.find((r) => classifyCheckpointQuality(r) === "rich");
115
+ return rich || recent[0];
116
+ }
117
+
118
+ private get archiveDir(): string {
119
+ return ensureDir(resolve(this.dir, "../archive/session-checkpoints"));
120
+ }
121
+
122
+ gc(options: CheckpointGcOptions = {}): CheckpointGcResult {
123
+ const {
124
+ dryRun = false,
125
+ now = new Date(),
126
+ keepPerScope = 5,
127
+ sessionTtlHours = 48,
128
+ minimalTtlDays = 7,
129
+ archiveAfterDays = 30,
130
+ archiveTtlDays = 90,
131
+ } = options;
132
+
133
+ const nowMs = now.getTime();
134
+ const DAY_MS = 86_400_000;
135
+ const HOUR_MS = 3_600_000;
136
+
137
+ const errors: string[] = [];
138
+ const actions: CheckpointGcFileAction[] = [];
139
+
140
+ // --- Phase 1: Load and classify all checkpoint files ---
141
+ const dir = this.dataDir;
142
+ const files = readdirSync(dir).filter((name) => name.endsWith(".json"));
143
+
144
+ interface ParsedEntry {
145
+ path: string;
146
+ record: SessionCheckpointRecord;
147
+ quality: CheckpointQuality;
148
+ ageMs: number;
149
+ scope: string;
150
+ }
151
+
152
+ const entries: ParsedEntry[] = [];
153
+ for (const name of files) {
154
+ const path = join(dir, name);
155
+ try {
156
+ const record = SessionCheckpointRecordSchema.parse(JSON.parse(readFileSync(path, "utf-8")));
157
+ const quality = classifyCheckpointQuality(record);
158
+ const scope = normalizeCheckpointScope(record.resolvedScope ?? record.scope ?? `session:${record.sessionId}`);
159
+ const ageMs = nowMs - Date.parse(record.updatedAt);
160
+ entries.push({ path, record, quality, ageMs, scope });
161
+ } catch (err) {
162
+ errors.push(`parse error: ${name}: ${err instanceof Error ? err.message : String(err)}`);
163
+ }
164
+ }
165
+
166
+ // --- Phase 2: Build protected set ---
167
+ const protectedPaths = new Set<string>();
168
+ const protectedReasons = new Map<string, string>();
169
+
170
+ // Rule 1: Latest N rich per scope
171
+ const byScope = new Map<string, ParsedEntry[]>();
172
+ for (const entry of entries) {
173
+ const list = byScope.get(entry.scope) ?? [];
174
+ list.push(entry);
175
+ byScope.set(entry.scope, list);
176
+ }
177
+ for (const [scope, list] of byScope) {
178
+ const richSorted = list
179
+ .filter((e) => e.quality === "rich")
180
+ .sort((a, b) => a.ageMs - b.ageMs); // newest first (smallest age)
181
+ for (const entry of richSorted.slice(0, keepPerScope)) {
182
+ protectedPaths.add(entry.path);
183
+ protectedReasons.set(entry.path, `latest ${keepPerScope} rich for scope ${scope}`);
184
+ }
185
+ }
186
+
187
+ // Rule 2: Latest 1 per session within sessionTtlHours
188
+ const bySession = new Map<string, ParsedEntry[]>();
189
+ for (const entry of entries) {
190
+ const list = bySession.get(entry.record.sessionId) ?? [];
191
+ list.push(entry);
192
+ bySession.set(entry.record.sessionId, list);
193
+ }
194
+ for (const [, list] of bySession) {
195
+ const newest = list.sort((a, b) => a.ageMs - b.ageMs)[0];
196
+ if (newest && newest.ageMs < sessionTtlHours * HOUR_MS) {
197
+ if (!protectedPaths.has(newest.path)) {
198
+ protectedPaths.add(newest.path);
199
+ protectedReasons.set(newest.path, `latest checkpoint for session within ${sessionTtlHours}h`);
200
+ }
201
+ }
202
+ }
203
+
204
+ // --- Phase 3: Decide action for each entry ---
205
+ for (const entry of entries) {
206
+ const ageDays = entry.ageMs / DAY_MS;
207
+ let action: "keep" | "archive" | "delete";
208
+ let reason: string;
209
+
210
+ if (protectedPaths.has(entry.path)) {
211
+ action = "keep";
212
+ reason = protectedReasons.get(entry.path) ?? "protected";
213
+ } else if (entry.quality === "minimal" && ageDays > minimalTtlDays) {
214
+ action = "delete";
215
+ reason = `minimal checkpoint older than ${minimalTtlDays} days (${Math.floor(ageDays)}d)`;
216
+ } else if (entry.quality === "rich" && ageDays > archiveAfterDays) {
217
+ action = "archive";
218
+ reason = `rich checkpoint older than ${archiveAfterDays} days (${Math.floor(ageDays)}d), not in latest ${keepPerScope} per scope`;
219
+ } else {
220
+ action = "keep";
221
+ reason = `${entry.quality} checkpoint within retention window (${Math.floor(ageDays)}d)`;
222
+ }
223
+
224
+ actions.push({
225
+ file: basename(entry.path),
226
+ checkpointId: entry.record.checkpointId,
227
+ resolvedScope: entry.scope,
228
+ quality: entry.quality,
229
+ updatedAt: entry.record.updatedAt,
230
+ action,
231
+ reason,
232
+ });
233
+ }
234
+
235
+ // --- Phase 4: Execute actions (unless dry-run) ---
236
+ let kept = 0;
237
+ let archived = 0;
238
+ let deleted = 0;
239
+
240
+ for (const item of actions) {
241
+ if (item.action === "keep") {
242
+ kept++;
243
+ continue;
244
+ }
245
+ if (dryRun) {
246
+ if (item.action === "archive") archived++;
247
+ else deleted++;
248
+ continue;
249
+ }
250
+ const srcPath = join(dir, item.file);
251
+ try {
252
+ if (item.action === "archive") {
253
+ renameSync(srcPath, join(this.archiveDir, item.file));
254
+ archived++;
255
+ } else {
256
+ unlinkSync(srcPath);
257
+ deleted++;
258
+ }
259
+ } catch (err) {
260
+ errors.push(`${item.action} failed: ${item.file}: ${err instanceof Error ? err.message : String(err)}`);
261
+ kept++; // count as kept since we couldn't move/delete it
262
+ }
263
+ }
264
+
265
+ // --- Phase 5: Sweep archived directory for expired files ---
266
+ const archDir = this.archiveDir;
267
+ const archivedFiles = readdirSync(archDir).filter((name) => name.endsWith(".json"));
268
+ for (const name of archivedFiles) {
269
+ const path = join(archDir, name);
270
+ try {
271
+ const mtime = statSync(path).mtimeMs;
272
+ const ageDays = (nowMs - mtime) / DAY_MS;
273
+ if (ageDays > archiveTtlDays) {
274
+ if (!dryRun) unlinkSync(path);
275
+ deleted++;
276
+ actions.push({
277
+ file: `archive/${name}`,
278
+ checkpointId: name.replace(/.*-([0-9a-f-]{36})\.json$/, "$1"),
279
+ resolvedScope: "archived",
280
+ quality: "rich",
281
+ updatedAt: new Date(mtime).toISOString(),
282
+ action: "delete",
283
+ reason: `archived file older than ${archiveTtlDays} days (${Math.floor(ageDays)}d)`,
284
+ });
285
+ }
286
+ } catch (err) {
287
+ errors.push(`archive sweep: ${name}: ${err instanceof Error ? err.message : String(err)}`);
288
+ }
289
+ }
290
+
291
+ return { kept, archived, deleted, errors, actions };
292
+ }
293
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Keep library- and MCP-path logs off stdout.
3
+ * stdout is reserved for MCP protocol frames and CLI result output.
4
+ */
5
+
6
+ export function logInfo(...args: unknown[]): void {
7
+ console.error(...args);
8
+ }
9
+
10
+ export function logWarn(...args: unknown[]): void {
11
+ console.warn(...args);
12
+ }
13
+
14
+ export function logError(...args: unknown[]): void {
15
+ console.error(...args);
16
+ }