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,1337 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * RecallNest MCP Server
4
+ *
5
+ * Exposes conversation memory search as MCP tools,
6
+ * so any MCP-compatible AI client (Claude Code, etc.)
7
+ * can search your indexed conversations.
8
+ *
9
+ * Tool tiers:
10
+ * - core: Always exposed (5 tools)
11
+ * - advanced: Exposed by default, includes core (15 tools)
12
+ * - full: All tools including governance (24 tools)
13
+ *
14
+ * Control: RECALLNEST_MCP_TIER=core|advanced|full
15
+ */
16
+
17
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
18
+
19
+ // ============================================================================
20
+ // Tier Configuration
21
+ // ============================================================================
22
+
23
+ type ToolTier = "core" | "advanced" | "governance";
24
+
25
+ const MCP_TIER = (process.env.RECALLNEST_MCP_TIER || "advanced") as "core" | "advanced" | "full";
26
+
27
+ const TOOL_TIERS: Record<string, ToolTier> = {
28
+ // Core (always)
29
+ resume_context: "core",
30
+ search_memory: "core",
31
+ store_memory: "core",
32
+ checkpoint_session: "core",
33
+ latest_checkpoint: "core",
34
+
35
+ set_reminder: "core",
36
+
37
+ // Advanced
38
+ auto_capture: "advanced",
39
+ store_case: "advanced",
40
+ store_workflow_pattern: "advanced",
41
+ promote_memory: "advanced",
42
+ explain_memory: "advanced",
43
+ distill_memory: "advanced",
44
+ brief_memory: "advanced",
45
+ pin_memory: "advanced",
46
+ list_assets: "advanced",
47
+ list_pins: "advanced",
48
+ memory_stats: "advanced",
49
+ memory_drill_down: "advanced",
50
+ export_memory: "advanced",
51
+
52
+ // Governance (CLI-only, not in MCP by default)
53
+ workflow_observe: "governance",
54
+ workflow_health: "governance",
55
+ workflow_evidence: "governance",
56
+ list_conflicts: "governance",
57
+ resolve_conflict: "governance",
58
+ audit_conflicts: "governance",
59
+ escalate_conflicts: "governance",
60
+ list_dirty_briefs: "governance",
61
+ clean_dirty_briefs: "governance",
62
+ consolidate_memories: "governance",
63
+ };
64
+
65
+ function shouldRegisterTool(toolName: string): boolean {
66
+ const tier = TOOL_TIERS[toolName];
67
+ if (!tier) return true; // unknown tools always register (backward compat)
68
+ if (MCP_TIER === "full") return true;
69
+ if (MCP_TIER === "advanced") return tier !== "governance";
70
+ if (MCP_TIER === "core") return tier === "core";
71
+ return true;
72
+ }
73
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
74
+ import { z } from "zod";
75
+ import type { RetrievalResult } from "./retriever.js";
76
+ import type { MemoryStore } from "./store.js";
77
+ import { distillResults, formatExplainResults, formatSearchResults, selectBriefSeedResults, summarizeResults } from "./memory-output.js";
78
+ import { archiveDirtyBriefAsset, assetSummaryLine, buildBriefAsset, buildPinAsset, listDirtyBriefAssets, listMemoryAssets, listPinAssets, saveBriefAsset, savePinAsset, writeExportArtifact } from "./memory-assets.js";
79
+ import { indexAsset, indexPinnedAsset } from "./asset-sync.js";
80
+ import { createComponentResolver, loadConfig, loadDotEnv, resolveRecallMode } from "./runtime-config.js";
81
+ import { DurableMemoryCategorySchema, StoreMemorySourceSchema } from "./memory-schema.js";
82
+ import { persistCaseMemory, persistMemory, persistMemoryBatch, persistWorkflowPattern, promoteMemory } from "./capture-engine.js";
83
+ import { autoCapture } from "./capture-heuristic.js";
84
+ import { ConsolidationEngine, formatConsolidationResult } from "./consolidation-engine.js";
85
+ import { renderMemories, type RenderMode } from "./context-renderer.js";
86
+ import { buildSessionCheckpointResult } from "./session-engine.js";
87
+ import { SessionCheckpointStore } from "./session-store.js";
88
+ import { composeResumeContext } from "./context-composer.js";
89
+ import { formatCheckpointSaved, formatCheckpointSummary, formatResumeContext } from "./session-output.js";
90
+ import { ConflictStatusSchema } from "./conflict-schema.js";
91
+ import { KGStore } from "./kg-store.js";
92
+ import { createKGExtractor, isKGModeEnabled, type KGExtractor } from "./kg-extractor.js";
93
+ import { resolveConflictCandidate } from "./conflict-engine.js";
94
+ import { escalateConflicts } from "./conflict-escalation.js";
95
+ import { ConflictCandidateStore } from "./conflict-store.js";
96
+ import { formatConflictAudit, formatConflictClusters, formatConflictEscalation, formatConflictList, formatConflictRecord, formatConflictResolution } from "./conflict-output.js";
97
+ import { CONFLICT_ATTENTION_LEVELS, summarizeConflictLifecycle } from "./conflict-lifecycle.js";
98
+ import { buildConflictAuditSummary, clusterConflicts } from "./conflict-advisor.js";
99
+ import { WorkflowObservationOutcomeSchema } from "./workflow-observation-schema.js";
100
+ import { buildWorkflowEvidence, buildWorkflowObservationRecord, inspectWorkflowDashboard, inspectWorkflowHealth } from "./workflow-observation-engine.js";
101
+ import { formatWorkflowEvidencePack, formatWorkflowHealthDashboard, formatWorkflowHealthReport, formatWorkflowObservationSaved } from "./workflow-observation-output.js";
102
+ import { WorkflowObservationStore } from "./workflow-observation-store.js";
103
+ import { buildManagedCheckpointObservation, buildManagedResumeObservation } from "./workflow-observation-managed.js";
104
+ import { buildRetrievalContext, resolveScopeSelection } from "./scope-policy.js";
105
+ import { matchesTemporalConstraint, type TemporalConstraint } from "./temporal-parser.js";
106
+ import { setReminder, checkTriggers, fireReminder, formatReminders } from "./prospective-memory.js";
107
+
108
+ function entryToRetrievalResult(entry: Awaited<ReturnType<MemoryStore["get"]>>): RetrievalResult {
109
+ if (!entry) {
110
+ throw new Error("Memory entry not found.");
111
+ }
112
+ return {
113
+ entry,
114
+ score: entry.importance || 0.7,
115
+ sources: {
116
+ fused: { score: entry.importance || 0.7 },
117
+ },
118
+ };
119
+ }
120
+
121
+ async function saveManagedObservation(observation: Parameters<typeof buildWorkflowObservationRecord>[0]): Promise<void> {
122
+ try {
123
+ const record = buildWorkflowObservationRecord(observation);
124
+ await workflowObservationStore.save(record);
125
+ } catch (error) {
126
+ console.error("[RecallNest MCP] Failed to persist managed workflow observation:", error);
127
+ }
128
+ }
129
+
130
+ // ============================================================================
131
+ // MCP Server
132
+ // ============================================================================
133
+
134
+ loadDotEnv();
135
+ const config = loadConfig();
136
+ const getComponents = createComponentResolver(config);
137
+ const { store, llm } = getComponents();
138
+ const checkpointStore = new SessionCheckpointStore();
139
+ const conflictStore = new ConflictCandidateStore();
140
+ const workflowObservationStore = new WorkflowObservationStore();
141
+
142
+ // Tier 4.1: Knowledge Graph triple extraction (gated by RECALLNEST_KG_MODE=true)
143
+ let kgExtractor: KGExtractor | null = null;
144
+ let kgStoreInstance: KGStore | null = null;
145
+ if (isKGModeEnabled() && llm) {
146
+ try {
147
+ kgStoreInstance = new KGStore({ dbPath: store.dbPath });
148
+ kgExtractor = createKGExtractor({ llmClient: llm, kgStore: kgStoreInstance });
149
+ // Attach KG store to default retriever for PPR graph traversal
150
+ const { retriever } = getComponents();
151
+ retriever.setKGStore(kgStoreInstance);
152
+ console.error("[RecallNest] KG triple extraction + graph traversal enabled");
153
+ } catch (err) {
154
+ console.error("[RecallNest] KG init failed:", err);
155
+ }
156
+ }
157
+
158
+ const server = new McpServer({
159
+ name: "recallnest",
160
+ version: "1.4.0",
161
+ });
162
+
163
+ // ============================================================================
164
+ // Tool Registration Helper (tier-aware)
165
+ // ============================================================================
166
+
167
+ type ToolSchema = Parameters<typeof server.tool>[2];
168
+ type ToolHandler = Parameters<typeof server.tool>[3];
169
+
170
+ function registerTool(name: string, description: string, schema: ToolSchema, handler: ToolHandler): void {
171
+ if (!shouldRegisterTool(name)) {
172
+ // stdout is reserved for MCP JSON-RPC on stdio transports.
173
+ console.error(`[MCP] Skipping ${name} (tier: ${TOOL_TIERS[name]})`);
174
+ return;
175
+ }
176
+ server.tool(name, description, schema, handler);
177
+ }
178
+
179
+ registerTool(
180
+ "workflow_observe",
181
+ "Store an append-only workflow observation for self-evolution. Use this to record whether a continuity primitive or reusable workflow succeeded, failed, was corrected by the user, or was missed entirely.",
182
+ {
183
+ workflowId: z.string().min(1).max(120).describe("Workflow primitive id, such as resume_context or checkpoint_session"),
184
+ outcome: WorkflowObservationOutcomeSchema.default("success").describe("success | failure | corrected | missed"),
185
+ summary: z.string().min(1).max(400).describe("Short description of what happened"),
186
+ scope: z.string().min(1).max(160).optional().describe("Optional scope such as project:recallnest"),
187
+ source: z.string().min(1).max(40).default("agent").describe("Source label such as agent, smoke, eval, or manual"),
188
+ signal: z.string().min(1).max(120).optional().describe("Optional failure/correction signal tag"),
189
+ task: z.string().min(1).max(240).optional().describe("Optional related task"),
190
+ tags: z.array(z.string().min(1).max(40)).max(8).default([]).describe("Optional tags"),
191
+ tools: z.array(z.string().min(1).max(60)).max(6).default([]).describe("Optional tools involved"),
192
+ },
193
+ async ({ workflowId, outcome, summary, scope, source, signal, task, tags, tools }) => {
194
+ const record = buildWorkflowObservationRecord({
195
+ workflowId,
196
+ outcome,
197
+ summary,
198
+ scope,
199
+ source,
200
+ signal,
201
+ task,
202
+ tags,
203
+ tools,
204
+ });
205
+ const stored = await workflowObservationStore.save(record);
206
+ return {
207
+ content: [{
208
+ type: "text" as const,
209
+ text: formatWorkflowObservationSaved(stored),
210
+ }],
211
+ };
212
+ },
213
+ );
214
+
215
+ registerTool(
216
+ "workflow_health",
217
+ "Inspect workflow observation health. With workflowId, return a 7d/30d health report; without workflowId, return a dashboard of the most degraded workflows.",
218
+ {
219
+ workflowId: z.string().min(1).max(120).optional().describe("Optional workflow primitive id"),
220
+ scope: z.string().min(1).max(160).optional().describe("Optional scope filter"),
221
+ limit: z.number().int().min(1).max(30).default(10).describe("Dashboard result limit when workflowId is omitted"),
222
+ },
223
+ async ({ workflowId, scope, limit }) => {
224
+ const text = workflowId
225
+ ? formatWorkflowHealthReport(await inspectWorkflowHealth(workflowObservationStore, { workflowId, scope }))
226
+ : formatWorkflowHealthDashboard(await inspectWorkflowDashboard(workflowObservationStore, { scope, limit }), scope);
227
+ return {
228
+ content: [{
229
+ type: "text" as const,
230
+ text,
231
+ }],
232
+ };
233
+ },
234
+ );
235
+
236
+ registerTool(
237
+ "workflow_evidence",
238
+ "Generate an evidence pack for a workflow primitive, including recent issue observations, top signals, and suggested next actions.",
239
+ {
240
+ workflowId: z.string().min(1).max(120).describe("Workflow primitive id"),
241
+ scope: z.string().min(1).max(160).optional().describe("Optional scope filter"),
242
+ limit: z.number().int().min(1).max(20).default(5).describe("Max recent issue observations to include"),
243
+ },
244
+ async ({ workflowId, scope, limit }) => {
245
+ const pack = await buildWorkflowEvidence(workflowObservationStore, {
246
+ workflowId,
247
+ scope,
248
+ limit,
249
+ });
250
+ return {
251
+ content: [{
252
+ type: "text" as const,
253
+ text: formatWorkflowEvidencePack(pack),
254
+ }],
255
+ };
256
+ },
257
+ );
258
+
259
+ registerTool(
260
+ "store_memory",
261
+ "Store a durable memory when the user shares a stable preference, identity fact, project entity, reusable pattern, or solved case that should survive future windows. Do not use this for transient task state; use it only for memory worth keeping.",
262
+ {
263
+ text: z.string().min(1).max(4000).describe("Memory text to store"),
264
+ category: DurableMemoryCategorySchema.default("events").describe("Durable memory category"),
265
+ importance: z.number().min(0).max(1).default(0.7).describe("Importance score from 0 to 1"),
266
+ scope: z.string().min(1).max(160).describe("Required scope such as project:recallnest or session:abc123"),
267
+ source: StoreMemorySourceSchema.default("manual").describe("How this memory was captured"),
268
+ tags: z.array(z.string().min(1).max(40)).max(8).default([]).describe("Optional tags"),
269
+ canonicalKey: z.string().min(1).max(120).optional().describe("Optional stable key for merge/update semantics"),
270
+ },
271
+ async ({ text, category, importance, scope, source, tags, canonicalKey }) => {
272
+ const { store, embedder } = getComponents();
273
+ const stored = await persistMemory({
274
+ store,
275
+ embedder,
276
+ conflictStore,
277
+ kgExtractor,
278
+ }, {
279
+ text,
280
+ category,
281
+ importance,
282
+ scope,
283
+ source,
284
+ tags,
285
+ canonicalKey,
286
+ });
287
+
288
+ return {
289
+ content: [{
290
+ type: "text" as const,
291
+ text: [
292
+ `Stored memory ${stored.id.slice(0, 8)}`,
293
+ `Disposition: ${stored.disposition}`,
294
+ `Category: ${stored.category}`,
295
+ `Scope: ${stored.resolvedScope}`,
296
+ `Canonical key: ${stored.canonicalKey}`,
297
+ ...(stored.conflictId ? [`Conflict: ${stored.conflictId.slice(0, 8)}`] : []),
298
+ `Stored at: ${stored.storedAt}`,
299
+ ].join("\n"),
300
+ }],
301
+ };
302
+ }
303
+ );
304
+
305
+ registerTool(
306
+ "store_workflow_pattern",
307
+ "Store a reusable workflow pattern as durable memory. Use this when you identify a repeatable process worth reusing across fresh windows, such as startup continuity, debugging routines, review flows, or handoff steps.",
308
+ {
309
+ title: z.string().min(1).max(120).describe("Short pattern title"),
310
+ trigger: z.string().min(1).max(240).describe("When this workflow should be used"),
311
+ steps: z.array(z.string().min(1).max(220)).min(1).max(8).describe("Ordered workflow steps"),
312
+ outcome: z.string().min(1).max(240).optional().describe("Optional expected outcome"),
313
+ tools: z.array(z.string().min(1).max(60)).max(6).default([]).describe("Optional tools, commands, or interfaces involved"),
314
+ importance: z.number().min(0).max(1).default(0.82).describe("Importance score from 0 to 1"),
315
+ scope: z.string().min(1).max(160).describe("Required scope such as project:recallnest or session:abc123"),
316
+ source: StoreMemorySourceSchema.default("agent").describe("How this pattern was captured"),
317
+ tags: z.array(z.string().min(1).max(40)).max(8).default([]).describe("Optional tags"),
318
+ canonicalKey: z.string().min(1).max(120).optional().describe("Optional stable key for merge/update semantics"),
319
+ },
320
+ async ({ title, trigger, steps, outcome, tools, importance, scope, source, tags, canonicalKey }) => {
321
+ const { store, embedder } = getComponents();
322
+ const stored = await persistWorkflowPattern({
323
+ store,
324
+ embedder,
325
+ conflictStore,
326
+ kgExtractor,
327
+ }, {
328
+ title,
329
+ trigger,
330
+ steps,
331
+ outcome,
332
+ tools,
333
+ importance,
334
+ scope,
335
+ source,
336
+ tags,
337
+ canonicalKey,
338
+ });
339
+
340
+ return {
341
+ content: [{
342
+ type: "text" as const,
343
+ text: [
344
+ `Stored workflow pattern ${stored.id.slice(0, 8)}`,
345
+ `Disposition: ${stored.disposition}`,
346
+ `Title: ${stored.title}`,
347
+ `Scope: ${stored.resolvedScope}`,
348
+ `Canonical key: ${stored.canonicalKey}`,
349
+ `Tags: ${stored.tags.join(", ") || "-"}`,
350
+ ...(stored.conflictId ? [`Conflict: ${stored.conflictId.slice(0, 8)}`] : []),
351
+ `Stored at: ${stored.storedAt}`,
352
+ ].join("\n"),
353
+ }],
354
+ };
355
+ }
356
+ );
357
+
358
+ registerTool(
359
+ "set_reminder",
360
+ "Set a prospective memory reminder: 'next time X comes up, remind me about Y'. The reminder is stored and automatically triggered during future retrievals when the trigger condition is matched.",
361
+ {
362
+ trigger: z.string().min(1).max(200).describe("Trigger condition — keywords that should activate this reminder"),
363
+ action: z.string().min(1).max(500).describe("What to remind about when the trigger fires"),
364
+ scope: z.string().min(1).max(160).describe("Required scope"),
365
+ expiresInDays: z.number().min(1).max(365).optional().describe("Optional: auto-expire after N days"),
366
+ },
367
+ async ({ trigger, action, scope, expiresInDays }) => {
368
+ const { store, embedder } = getComponents();
369
+ const entry = await setReminder(store, embedder, { trigger, action, scope, expiresInDays });
370
+
371
+ return {
372
+ content: [{
373
+ type: "text" as const,
374
+ text: [
375
+ `Reminder set: ${entry.id.slice(0, 8)}`,
376
+ `Trigger: "${trigger}"`,
377
+ `Action: ${action}`,
378
+ `Scope: ${scope}`,
379
+ ...(expiresInDays ? [`Expires in: ${expiresInDays} days`] : []),
380
+ ].join("\n"),
381
+ }],
382
+ };
383
+ }
384
+ );
385
+
386
+ registerTool(
387
+ "auto_capture",
388
+ "Extract memory-worthy items from a conversation turn using lightweight heuristics (zero LLM calls). Detects preferences, identity facts, decisions, corrections, explicit memory instructions, and workflow patterns. Items that pass salience filtering are stored as durable memories. Use this when you want to analyze a block of conversation text and automatically capture any signals worth remembering.",
389
+ {
390
+ text: z.string().min(1).max(8000).describe("Conversation text to analyze for memory-worthy signals"),
391
+ scope: z.string().min(1).max(160).describe("Required scope such as project:recallnest or session:abc123"),
392
+ source: StoreMemorySourceSchema.default("agent").describe("How this memory was captured"),
393
+ },
394
+ async ({ text, scope, source }) => {
395
+ const result = autoCapture(text);
396
+
397
+ if (result.skippedSalience) {
398
+ return {
399
+ content: [{
400
+ type: "text" as const,
401
+ text: "Skipped: text did not pass salience filter (too short, noise, or greeting)",
402
+ }],
403
+ };
404
+ }
405
+
406
+ if (result.items.length === 0) {
407
+ return {
408
+ content: [{
409
+ type: "text" as const,
410
+ text: "No memory-worthy signals detected in this text",
411
+ }],
412
+ };
413
+ }
414
+
415
+ const { store, embedder } = getComponents();
416
+ const stored = await persistMemoryBatch({
417
+ store,
418
+ embedder,
419
+ conflictStore,
420
+ kgExtractor,
421
+ }, {
422
+ scope,
423
+ source,
424
+ defaultImportance: 0.7,
425
+ memories: result.items.map((item) => ({
426
+ text: item.text,
427
+ category: item.category,
428
+ importance: item.importance,
429
+ tags: [`auto-capture:${item.sourceContext.replace(/\s+/g, "-")}`],
430
+ })),
431
+ });
432
+
433
+ const lines = stored.map((r, i) => {
434
+ const item = result.items[i];
435
+ return `${i + 1}. [${item.sourceContext}] ${r.disposition} → ${r.category} (${r.id.slice(0, 8)})`;
436
+ });
437
+
438
+ return {
439
+ content: [{
440
+ type: "text" as const,
441
+ text: [
442
+ `Auto-captured ${stored.length} item(s) from ${result.items.length} signal(s):`,
443
+ ...lines,
444
+ `Scope: ${scope}`,
445
+ ].join("\n"),
446
+ }],
447
+ };
448
+ }
449
+ );
450
+
451
+ registerTool(
452
+ "store_case",
453
+ "Store a reusable case as durable memory. Use this when you identify a concrete problem-and-solution pair worth reusing across future windows, such as a debugging fix, continuity cleanup, migration lesson, or implementation recovery.",
454
+ {
455
+ title: z.string().min(1).max(120).describe("Short case title"),
456
+ problem: z.string().min(1).max(320).describe("What problem happened"),
457
+ context: z.string().min(1).max(240).optional().describe("Optional context or preconditions"),
458
+ solutionSteps: z.array(z.string().min(1).max(220)).min(1).max(8).describe("Ordered solution steps"),
459
+ outcome: z.string().min(1).max(240).optional().describe("Optional result or resolution"),
460
+ tools: z.array(z.string().min(1).max(60)).max(6).default([]).describe("Optional tools, commands, or interfaces involved"),
461
+ importance: z.number().min(0).max(1).default(0.84).describe("Importance score from 0 to 1"),
462
+ scope: z.string().min(1).max(160).describe("Required scope such as project:recallnest or session:abc123"),
463
+ source: StoreMemorySourceSchema.default("agent").describe("How this case was captured"),
464
+ tags: z.array(z.string().min(1).max(40)).max(8).default([]).describe("Optional tags"),
465
+ canonicalKey: z.string().min(1).max(120).optional().describe("Optional stable key for merge/update semantics"),
466
+ },
467
+ async ({ title, problem, context, solutionSteps, outcome, tools, importance, scope, source, tags, canonicalKey }) => {
468
+ const { store, embedder } = getComponents();
469
+ const stored = await persistCaseMemory({
470
+ store,
471
+ embedder,
472
+ conflictStore,
473
+ kgExtractor,
474
+ }, {
475
+ title,
476
+ problem,
477
+ context,
478
+ solutionSteps,
479
+ outcome,
480
+ tools,
481
+ importance,
482
+ scope,
483
+ source,
484
+ tags,
485
+ canonicalKey,
486
+ });
487
+
488
+ return {
489
+ content: [{
490
+ type: "text" as const,
491
+ text: [
492
+ `Stored case ${stored.id.slice(0, 8)}`,
493
+ `Disposition: ${stored.disposition}`,
494
+ `Title: ${stored.title}`,
495
+ `Scope: ${stored.resolvedScope}`,
496
+ `Canonical key: ${stored.canonicalKey}`,
497
+ `Tags: ${stored.tags.join(", ") || "-"}`,
498
+ ...(stored.conflictId ? [`Conflict: ${stored.conflictId.slice(0, 8)}`] : []),
499
+ `Stored at: ${stored.storedAt}`,
500
+ ].join("\n"),
501
+ }],
502
+ };
503
+ }
504
+ );
505
+
506
+ registerTool(
507
+ "promote_memory",
508
+ "Promote an evidence memory into durable memory. Use this when a transcript snippet or imported artifact contains a fact worth keeping across windows, and you want an explicit authority upgrade instead of leaving it as raw evidence.",
509
+ {
510
+ memoryId: z.string().min(1).max(128).describe("Existing evidence memory ID or unique prefix"),
511
+ text: z.string().min(1).max(4000).optional().describe("Optional cleaned durable text; defaults to the source entry text"),
512
+ category: DurableMemoryCategorySchema.optional().describe("Optional target durable category; defaults to the source evidence category or its originalCategory"),
513
+ importance: z.number().min(0).max(1).default(0.78).describe("Importance score from 0 to 1"),
514
+ scope: z.string().min(1).max(160).describe("Required target scope such as project:recallnest or session:abc123"),
515
+ source: StoreMemorySourceSchema.default("agent").describe("How this promotion was captured"),
516
+ tags: z.array(z.string().min(1).max(40)).max(8).default([]).describe("Optional tags"),
517
+ canonicalKey: z.string().min(1).max(120).optional().describe("Optional stable key for merge/update semantics"),
518
+ },
519
+ async ({ memoryId, text, category, importance, scope, source, tags, canonicalKey }) => {
520
+ const { store, embedder } = getComponents();
521
+ const stored = await promoteMemory({
522
+ store,
523
+ embedder,
524
+ conflictStore,
525
+ kgExtractor,
526
+ }, {
527
+ memoryId,
528
+ text,
529
+ category,
530
+ importance,
531
+ scope,
532
+ source,
533
+ tags,
534
+ canonicalKey,
535
+ });
536
+
537
+ return {
538
+ content: [{
539
+ type: "text" as const,
540
+ text: stored.disposition === "conflict" && stored.conflictId
541
+ ? `Promotion conflict ${stored.conflictId.slice(0, 8)}\nIncoming: ${stored.sourceMemoryId.slice(0, 8)} (${stored.sourceCategory})\nExisting durable: ${stored.id.slice(0, 8)}\nCategory: ${stored.category}\nCanonical key: ${stored.canonicalKey}\nStatus: manual review required`
542
+ : `Promoted memory ${stored.id.slice(0, 8)}\nFrom: ${stored.sourceMemoryId.slice(0, 8)} (${stored.sourceCategory})\nDisposition: ${stored.disposition}\nCategory: ${stored.category}\nScope: ${stored.resolvedScope}\nCanonical key: ${stored.canonicalKey}\nStored at: ${stored.storedAt}`,
543
+ }],
544
+ };
545
+ }
546
+ );
547
+
548
+ registerTool(
549
+ "list_conflicts",
550
+ "List or inspect open conflict candidates when incoming evidence promotions disagree with an existing durable memory for the same canonical key.",
551
+ {
552
+ conflictId: z.string().min(1).max(128).optional().describe("Optional conflict ID to inspect a single record"),
553
+ status: ConflictStatusSchema.optional().describe("Optional status filter"),
554
+ attention: z.enum(CONFLICT_ATTENTION_LEVELS).optional().describe("Optional lifecycle filter: fresh / aging / stale / escalated / resolved"),
555
+ canonicalKey: z.string().min(1).max(120).optional().describe("Optional canonical key filter"),
556
+ groupBy: z.enum(["record", "cluster"]).default("record").describe("Whether to list individual conflicts or grouped clusters"),
557
+ limit: z.number().int().min(1).max(50).default(20).describe("Max conflicts to list"),
558
+ },
559
+ async ({ conflictId, status, attention, canonicalKey, groupBy, limit }) => {
560
+ if (conflictId) {
561
+ const record = await conflictStore.getById(conflictId);
562
+ return {
563
+ content: [{
564
+ type: "text" as const,
565
+ text: record ? formatConflictRecord(record) : `Conflict not found: ${conflictId}`,
566
+ }],
567
+ };
568
+ }
569
+
570
+ const records = (await conflictStore.listRecent({ status, canonicalKey, limit: Math.max(limit * 2, limit) }))
571
+ .filter((record) => !attention || summarizeConflictLifecycle(record).attention === attention)
572
+ .slice(0, limit);
573
+ return {
574
+ content: [{
575
+ type: "text" as const,
576
+ text: groupBy === "cluster"
577
+ ? formatConflictClusters(clusterConflicts(records))
578
+ : formatConflictList(records),
579
+ }],
580
+ };
581
+ }
582
+ );
583
+
584
+ registerTool(
585
+ "resolve_conflict",
586
+ "Resolve a stored conflict candidate by keeping the existing durable memory, accepting the incoming promoted evidence, or merging the two texts.",
587
+ {
588
+ conflictId: z.string().min(1).max(128).describe("Conflict ID to resolve"),
589
+ resolution: z.enum(["accept_incoming", "keep_existing", "merge"]).describe("How to resolve the conflict"),
590
+ mergedText: z.string().min(1).max(2000).optional().describe("Optional merged text override when resolution is merge"),
591
+ notes: z.string().min(1).max(320).optional().describe("Optional operator notes"),
592
+ },
593
+ async ({ conflictId, resolution, mergedText, notes }) => {
594
+ const { store, embedder } = getComponents();
595
+ const result = await resolveConflictCandidate({
596
+ store,
597
+ embedder,
598
+ conflictStore,
599
+ }, {
600
+ conflictId,
601
+ resolution,
602
+ ...(mergedText ? { mergedText } : {}),
603
+ notes,
604
+ });
605
+
606
+ return {
607
+ content: [{
608
+ type: "text" as const,
609
+ text: formatConflictResolution(result),
610
+ }],
611
+ };
612
+ }
613
+ );
614
+
615
+ registerTool(
616
+ "audit_conflicts",
617
+ "Generate a terminal-friendly conflict audit summary so you can see which stale or escalated conflict clusters should be reviewed first.",
618
+ {
619
+ status: ConflictStatusSchema.optional().describe("Optional status filter"),
620
+ canonicalKey: z.string().min(1).max(120).optional().describe("Optional canonical key filter"),
621
+ limit: z.number().int().min(1).max(500).default(100).describe("How many conflict records to scan"),
622
+ top: z.number().int().min(1).max(20).default(5).describe("How many priority clusters to show"),
623
+ },
624
+ async ({ status, canonicalKey, limit, top }) => {
625
+ const records = await conflictStore.listRecent({
626
+ status,
627
+ canonicalKey,
628
+ limit,
629
+ });
630
+ const summary = buildConflictAuditSummary(records, top);
631
+ return {
632
+ content: [{
633
+ type: "text" as const,
634
+ text: formatConflictAudit(summary),
635
+ }],
636
+ };
637
+ }
638
+ );
639
+
640
+ registerTool(
641
+ "escalate_conflicts",
642
+ "Preview or apply the conflict aging policy so stale or escalated open conflicts are explicitly marked for operator review.",
643
+ {
644
+ attention: z.enum(["stale", "escalated"]).default("stale").describe("Only consider stale or escalated conflicts"),
645
+ canonicalKey: z.string().min(1).max(120).optional().describe("Optional canonical key filter"),
646
+ limit: z.number().int().min(1).max(500).default(100).describe("How many open conflicts to scan"),
647
+ top: z.number().int().min(1).max(20).default(10).describe("How many eligible conflicts to include"),
648
+ apply: z.boolean().default(false).describe("When false, preview only. When true, persist escalation metadata."),
649
+ notes: z.string().min(1).max(320).optional().describe("Optional operator note when applying escalation"),
650
+ },
651
+ async ({ attention, canonicalKey, limit, top, apply, notes }) => {
652
+ const result = await escalateConflicts({
653
+ conflictStore,
654
+ }, {
655
+ attention,
656
+ canonicalKey,
657
+ limit,
658
+ top,
659
+ apply,
660
+ notes,
661
+ });
662
+ return {
663
+ content: [{
664
+ type: "text" as const,
665
+ text: formatConflictEscalation(result),
666
+ }],
667
+ };
668
+ }
669
+ );
670
+
671
+ registerTool(
672
+ "checkpoint_session",
673
+ "Store a compact checkpoint of the current work state. Use this when a task spans windows or terminals and you need the next session to recover decisions, open loops, and next actions without polluting durable memory.",
674
+ {
675
+ sessionId: z.string().min(1).max(160).describe("Current session identifier"),
676
+ scope: z.string().min(1).max(160).optional().describe("Optional shared scope; defaults to session:<sessionId>"),
677
+ summary: z.string().min(1).max(600).describe("Compact summary of the current work state"),
678
+ task: z.string().min(1).max(240).optional().describe("Optional task label"),
679
+ decisions: z.array(z.string().min(1).max(200)).max(6).default([]).describe("Key decisions already made"),
680
+ openLoops: z.array(z.string().min(1).max(200)).max(6).default([]).describe("Unresolved questions or pending items"),
681
+ nextActions: z.array(z.string().min(1).max(200)).max(6).default([]).describe("Next actions to take"),
682
+ entities: z.array(z.string().min(1).max(120)).max(8).default([]).describe("Relevant projects, tools, or people"),
683
+ files: z.array(z.string().min(1).max(220)).max(12).default([]).describe("Relevant files or paths"),
684
+ updatedAt: z.string().datetime().optional().describe("Optional override; defaults to now"),
685
+ },
686
+ async ({ sessionId, scope, summary, task, decisions, openLoops, nextActions, entities, files, updatedAt }) => {
687
+ const result = buildSessionCheckpointResult({
688
+ sessionId,
689
+ scope,
690
+ summary,
691
+ task,
692
+ decisions,
693
+ openLoops,
694
+ nextActions,
695
+ entities,
696
+ files,
697
+ ...(updatedAt ? { updatedAt } : {}),
698
+ });
699
+ const storedRecord = await checkpointStore.save(result.record);
700
+ await saveManagedObservation(buildManagedCheckpointObservation({
701
+ ...result,
702
+ record: storedRecord,
703
+ }));
704
+ return {
705
+ content: [{
706
+ type: "text" as const,
707
+ text: formatCheckpointSaved(storedRecord),
708
+ }],
709
+ };
710
+ }
711
+ );
712
+
713
+ registerTool(
714
+ "latest_checkpoint",
715
+ "Fetch the latest saved checkpoint for a session or shared scope. Useful for inspecting current work state before resume_context exists.",
716
+ {
717
+ sessionId: z.string().min(1).max(160).optional().describe("Optional session identifier filter"),
718
+ scope: z.string().min(1).max(160).optional().describe("Optional shared scope filter"),
719
+ },
720
+ async ({ sessionId, scope }) => {
721
+ const latest = await checkpointStore.getLatest({ sessionId, scope });
722
+ return {
723
+ content: [{
724
+ type: "text" as const,
725
+ text: formatCheckpointSummary(latest),
726
+ }],
727
+ };
728
+ }
729
+ );
730
+
731
+ registerTool(
732
+ "resume_context",
733
+ "Compose startup context for a fresh window by combining stable durable memory, relevant patterns and cases, plus the latest checkpoint for the current scope or session.",
734
+ {
735
+ task: z.string().min(1).max(500).optional().describe("Optional current task or question to bias recall"),
736
+ scope: z.string().min(1).max(160).optional().describe("Optional shared scope for project or terminal continuity"),
737
+ sessionId: z.string().min(1).max(160).optional().describe("Optional session identifier to recover the latest checkpoint"),
738
+ limitPerSection: z.number().int().min(1).max(6).default(3).describe("Max items per section"),
739
+ includeLatestCheckpoint: z.boolean().default(true).describe("Whether to include the latest checkpoint summary"),
740
+ profile: z.enum(["default", "writing", "debug", "fact-check"]).optional().describe("Retrieval profile"),
741
+ mode: z.enum(["full", "summary", "off"]).optional().describe("Override recall mode (default: from config.recallMode)"),
742
+ },
743
+ async ({ task, scope, sessionId, limitPerSection, includeLatestCheckpoint, profile: profileName, mode: modeOverride }) => {
744
+ const effectiveMode = resolveRecallMode(config, modeOverride);
745
+
746
+ // --- off mode: no recall, guide agent to use search_memory ---
747
+ if (effectiveMode === "off") {
748
+ return {
749
+ content: [{
750
+ type: "text" as const,
751
+ text: "Recall mode is off. Use search_memory to retrieve specific memories on demand.",
752
+ }],
753
+ };
754
+ }
755
+
756
+ // --- summary mode: checkpoint only, lightweight ---
757
+ if (effectiveMode === "summary") {
758
+ const scopeSelection = resolveScopeSelection({
759
+ scope,
760
+ sessionId,
761
+ operation: "resume_context",
762
+ allowUnscoped: true,
763
+ });
764
+ const latest = await checkpointStore.getLatest({
765
+ sessionId,
766
+ scope: scopeSelection.resolvedScope,
767
+ });
768
+ const summaryText = formatCheckpointSummary(latest) +
769
+ "\n\nFor detailed recall, use search_memory with specific queries.";
770
+ await saveManagedObservation({
771
+ workflowId: "resume_context",
772
+ outcome: "success",
773
+ summary: `Managed resume_context returned summary-mode checkpoint${latest ? "" : " (none found)"}.`,
774
+ scope: scopeSelection.resolvedScope || scope || "global",
775
+ source: "managed:recallnest",
776
+ signal: "managed-resume-summary",
777
+ task,
778
+ tags: ["managed", "recallnest", "summary-mode"],
779
+ });
780
+ return {
781
+ content: [{
782
+ type: "text" as const,
783
+ text: summaryText,
784
+ }],
785
+ };
786
+ }
787
+
788
+ // --- full mode: existing compose behavior ---
789
+ const { retriever, profile } = getComponents(profileName);
790
+ const scopeSelection = resolveScopeSelection({
791
+ scope,
792
+ sessionId,
793
+ operation: "resume_context",
794
+ allowUnscoped: true,
795
+ });
796
+ const context = await composeResumeContext({
797
+ retriever,
798
+ checkpointStore,
799
+ }, {
800
+ task,
801
+ scope: scopeSelection.resolvedScope,
802
+ sessionId,
803
+ limitPerSection,
804
+ includeLatestCheckpoint,
805
+ profile: profile.name,
806
+ });
807
+ await saveManagedObservation(buildManagedResumeObservation({
808
+ task,
809
+ scope,
810
+ sessionId,
811
+ }, context));
812
+
813
+ return {
814
+ content: [{
815
+ type: "text" as const,
816
+ text: formatResumeContext(context),
817
+ }],
818
+ };
819
+ }
820
+ );
821
+
822
+ // --- search_memory tool ---
823
+ registerTool(
824
+ "search_memory",
825
+ "IMPORTANT: Use this tool proactively at the start of tasks to recall relevant past conversations, decisions, and patterns. Search when: starting a new task, debugging, writing, making decisions, or when the user references past work. Do NOT wait for the user to ask you to search. Query with key nouns/verbs from the user's message.",
826
+ {
827
+ query: z.string().describe("Search query — natural language or keywords"),
828
+ limit: z.number().min(1).max(20).default(5).describe("Max results to return"),
829
+ scope: z.string().optional().describe("Optional explicit scope"),
830
+ sessionId: z.string().min(1).max(160).optional().describe("Optional session identifier to infer session:<id> scope"),
831
+ allScopes: z.boolean().default(false).describe("When true, explicitly allow cross-scope search"),
832
+ category: DurableMemoryCategorySchema.optional().describe("Filter by memory category: profile (identity/background), preferences (habits/style), entities (projects/tools/people), events (past happenings), cases (problem-solution pairs), patterns (reusable workflows)"),
833
+ profile: z.enum(["default", "writing", "debug", "fact-check"]).optional().describe("Retrieval profile"),
834
+ render: z.enum(["verbatim", "highlight"]).default("verbatim").optional().describe("Result rendering mode: verbatim (default, original order) or highlight (reorder by contextual relevance to query)"),
835
+ after: z.string().optional().describe("Filter memories stored after this date (ISO format YYYY-MM-DD, or relative like '最近30天', 'last 7 days')"),
836
+ before: z.string().optional().describe("Filter memories stored before this date (ISO format YYYY-MM-DD, or relative)"),
837
+ graph: z.boolean().default(false).optional().describe("Enable KG graph traversal (PPR) for relationship-aware search. Use when query involves entity relationships (e.g. 'what tools does Alice use', 'Bob的朋友')."),
838
+ },
839
+ async ({ query, limit, scope, sessionId, allScopes, category, profile: profileName, render, after, before, graph }) => {
840
+ const { retriever, profile } = getComponents(profileName);
841
+ // Ensure KG store is attached to non-default profile retrievers for PPR
842
+ if (graph && kgStoreInstance) retriever.setKGStore(kgStoreInstance);
843
+ let results = await retriever.retrieve(buildRetrievalContext({
844
+ query,
845
+ limit: (after || before) ? limit * 3 : limit, // Over-fetch when temporal filtering will reduce results
846
+ category,
847
+ scope,
848
+ sessionId,
849
+ allScopes,
850
+ graph,
851
+ }, {
852
+ operation: "search_memory",
853
+ }));
854
+
855
+ // Explicit temporal filtering from after/before params
856
+ if (after || before) {
857
+ const constraint: TemporalConstraint = {
858
+ type: (after && before) ? "range" : (after ? "after" : "before"),
859
+ startMs: after ? new Date(after).getTime() || undefined : undefined,
860
+ endMs: before ? new Date(before).getTime() || undefined : undefined,
861
+ anchor: `${after || ""}..${before || ""}`,
862
+ };
863
+ if (constraint.startMs || constraint.endMs) {
864
+ results = results
865
+ .filter(r => matchesTemporalConstraint(r.entry.timestamp, constraint))
866
+ .slice(0, limit);
867
+ }
868
+ }
869
+
870
+ // Apply context-aware rendering when requested
871
+ if (render === "highlight" && results.length > 0) {
872
+ const rendered = renderMemories(
873
+ results.map(r => ({ id: r.entry.id, text: r.entry.text, score: r.score, category: r.entry.category })),
874
+ query,
875
+ "highlight",
876
+ );
877
+ // Reorder results to match rendered order
878
+ const idOrder = new Map(rendered.memories.map((m, i) => [m.id, i]));
879
+ results.sort((a, b) => (idOrder.get(a.entry.id) ?? 999) - (idOrder.get(b.entry.id) ?? 999));
880
+ }
881
+
882
+ // Tier 3.4: Check for triggered reminders alongside search results
883
+ const { store, embedder } = getComponents();
884
+ const scopeFilter = scope ? [scope] : undefined;
885
+ const triggered = await checkTriggers(store, embedder, query, scopeFilter);
886
+ let reminderText = "";
887
+ if (triggered.length > 0) {
888
+ const firedActions: string[] = [];
889
+ for (const reminder of triggered) {
890
+ const action = await fireReminder(store, reminder.entryId, scopeFilter);
891
+ if (action) firedActions.push(action);
892
+ }
893
+ if (firedActions.length > 0) {
894
+ reminderText = "\n\n--- Triggered Reminders ---\n" +
895
+ firedActions.map(a => `- ${a}`).join("\n");
896
+ }
897
+ }
898
+
899
+ return {
900
+ content: [{
901
+ type: "text" as const,
902
+ text: formatSearchResults(results, { query, profile: profile.name }) + reminderText,
903
+ }],
904
+ };
905
+ }
906
+ );
907
+
908
+ registerTool(
909
+ "explain_memory",
910
+ "Explain why the indexed memories matched: retrieval path, freshness, file/session, and matched terms.",
911
+ {
912
+ query: z.string().describe("Search query — natural language or keywords"),
913
+ limit: z.number().min(1).max(20).default(5).describe("Max results to analyze"),
914
+ scope: z.string().optional().describe("Optional explicit scope"),
915
+ sessionId: z.string().min(1).max(160).optional().describe("Optional session identifier to infer session:<id> scope"),
916
+ allScopes: z.boolean().default(false).describe("When true, explicitly allow cross-scope search"),
917
+ category: DurableMemoryCategorySchema.optional().describe("Filter by memory category"),
918
+ profile: z.enum(["default", "writing", "debug", "fact-check"]).optional().describe("Retrieval profile"),
919
+ },
920
+ async ({ query, limit, scope, sessionId, allScopes, category, profile: profileName }) => {
921
+ const { retriever, profile } = getComponents(profileName);
922
+ const results = await retriever.retrieve(buildRetrievalContext({
923
+ query,
924
+ limit,
925
+ category,
926
+ scope,
927
+ sessionId,
928
+ allScopes,
929
+ }, {
930
+ operation: "explain_memory",
931
+ }));
932
+ return {
933
+ content: [{
934
+ type: "text" as const,
935
+ text: formatExplainResults(results, { query, profile: profile.name }),
936
+ }],
937
+ };
938
+ }
939
+ );
940
+
941
+ registerTool(
942
+ "distill_memory",
943
+ "Distill retrieved memories into a compact briefing with source map, takeaways, and reusable evidence.",
944
+ {
945
+ query: z.string().describe("Topic or task to distill"),
946
+ limit: z.number().min(1).max(20).default(8).describe("Max results to distill"),
947
+ scope: z.string().optional().describe("Optional explicit scope"),
948
+ sessionId: z.string().min(1).max(160).optional().describe("Optional session identifier to infer session:<id> scope"),
949
+ allScopes: z.boolean().default(false).describe("When true, explicitly allow cross-scope search"),
950
+ profile: z.enum(["default", "writing", "debug", "fact-check"]).optional().describe("Retrieval profile"),
951
+ },
952
+ async ({ query, limit, scope, sessionId, allScopes, profile: profileName }) => {
953
+ const { retriever, profile } = getComponents(profileName || "writing");
954
+ const results = await retriever.retrieve(buildRetrievalContext({
955
+ query,
956
+ limit,
957
+ scope,
958
+ sessionId,
959
+ allScopes,
960
+ }, {
961
+ operation: "distill_memory",
962
+ }));
963
+ return {
964
+ content: [{
965
+ type: "text" as const,
966
+ text: distillResults(results, { query, profile: profile.name }),
967
+ }],
968
+ };
969
+ }
970
+ );
971
+
972
+ registerTool(
973
+ "brief_memory",
974
+ "Create a structured memory brief from retrieved results and feed it back into recall.",
975
+ {
976
+ query: z.string().describe("Topic or task to turn into a memory brief"),
977
+ limit: z.number().min(1).max(20).default(8).describe("Max results to distill into the brief"),
978
+ scope: z.string().optional().describe("Optional explicit scope"),
979
+ sessionId: z.string().min(1).max(160).optional().describe("Optional session identifier to infer session:<id> scope"),
980
+ allScopes: z.boolean().default(false).describe("When true, explicitly allow cross-scope search"),
981
+ profile: z.enum(["default", "writing", "debug", "fact-check"]).optional().describe("Retrieval profile"),
982
+ title: z.string().optional().describe("Optional brief title"),
983
+ },
984
+ async ({ query, limit, scope, sessionId, allScopes, profile: profileName, title }) => {
985
+ const { retriever, profile, store, embedder } = getComponents(profileName || "writing");
986
+ const results = await retriever.retrieve(buildRetrievalContext({
987
+ query,
988
+ limit,
989
+ scope,
990
+ sessionId,
991
+ allScopes,
992
+ }, {
993
+ operation: "brief_memory",
994
+ }));
995
+ if (results.length === 0) {
996
+ return { content: [{ type: "text" as const, text: `No results found for: ${query}` }] };
997
+ }
998
+ const briefSeedResults = selectBriefSeedResults(results);
999
+ const summary = summarizeResults(briefSeedResults, { query, profile: profile.name });
1000
+ const asset = buildBriefAsset(summary, { title });
1001
+ const path = saveBriefAsset(asset);
1002
+ await indexAsset(store, embedder, asset);
1003
+
1004
+ return {
1005
+ content: [{
1006
+ type: "text" as const,
1007
+ text: `Created brief ${asset.id.slice(0, 8)}\nTitle: ${asset.title}\nHits: ${asset.hits}\nPath: ${path}`,
1008
+ }],
1009
+ };
1010
+ }
1011
+ );
1012
+
1013
+ registerTool(
1014
+ "pin_memory",
1015
+ "Promote one retrieved memory into a pinned asset for later reuse.",
1016
+ {
1017
+ memory_id: z.string().describe("Memory ID or unique prefix from search/explain output"),
1018
+ scope: z.string().optional().describe("Optional explicit scope"),
1019
+ sessionId: z.string().min(1).max(160).optional().describe("Optional session identifier to infer session:<id> scope"),
1020
+ allScopes: z.boolean().default(false).describe("When true, explicitly allow cross-scope reads"),
1021
+ title: z.string().optional().describe("Optional pinned title"),
1022
+ summary: z.string().optional().describe("Optional pinned summary"),
1023
+ query: z.string().optional().describe("Optional query that led to this pin"),
1024
+ profile: z.enum(["default", "writing", "debug", "fact-check"]).optional().describe("Retrieval profile"),
1025
+ },
1026
+ async ({ memory_id, scope, sessionId, allScopes, title, summary, query, profile: profileName }) => {
1027
+ const { store, embedder } = getComponents(profileName);
1028
+ const scopeSelection = resolveScopeSelection({
1029
+ scope,
1030
+ sessionId,
1031
+ allScopes,
1032
+ operation: "pin_memory",
1033
+ });
1034
+ const entry = await store.get(memory_id, scopeSelection.scopeFilter);
1035
+ if (!entry) {
1036
+ return { content: [{ type: "text" as const, text: `Memory not found: ${memory_id}` }] };
1037
+ }
1038
+
1039
+ await store.update(entry.id, { importance: Math.max(entry.importance || 0.7, 0.95) }, scopeSelection.scopeFilter);
1040
+ const asset = buildPinAsset(entryToRetrievalResult(entry), {
1041
+ title,
1042
+ summary,
1043
+ query,
1044
+ profile: profileName || "default",
1045
+ });
1046
+ const path = savePinAsset(asset);
1047
+ await indexPinnedAsset(store, embedder, asset);
1048
+
1049
+ return {
1050
+ content: [{
1051
+ type: "text" as const,
1052
+ text: `Pinned ${asset.id.slice(0, 8)} from memory ${entry.id.slice(0, 8)}\nTitle: ${asset.title}\nPath: ${path}`,
1053
+ }],
1054
+ };
1055
+ }
1056
+ );
1057
+
1058
+ registerTool(
1059
+ "export_memory",
1060
+ "Export a distilled memory briefing to a markdown or json artifact on disk.",
1061
+ {
1062
+ query: z.string().describe("Topic or task to export"),
1063
+ limit: z.number().min(1).max(20).default(8).describe("Max results to export"),
1064
+ scope: z.string().optional().describe("Optional explicit scope"),
1065
+ sessionId: z.string().min(1).max(160).optional().describe("Optional session identifier to infer session:<id> scope"),
1066
+ allScopes: z.boolean().default(false).describe("When true, explicitly allow cross-scope search"),
1067
+ profile: z.enum(["default", "writing", "debug", "fact-check"]).optional().describe("Retrieval profile"),
1068
+ format: z.enum(["md", "json"]).default("md").describe("Export format"),
1069
+ },
1070
+ async ({ query, limit, scope, sessionId, allScopes, profile: profileName, format }) => {
1071
+ const { retriever, profile } = getComponents(profileName || "writing");
1072
+ const results = await retriever.retrieve(buildRetrievalContext({
1073
+ query,
1074
+ limit,
1075
+ scope,
1076
+ sessionId,
1077
+ allScopes,
1078
+ }, {
1079
+ operation: "export_memory",
1080
+ }));
1081
+ const summary = distillResults(results, { query, profile: profile.name });
1082
+ const artifact = writeExportArtifact({
1083
+ query,
1084
+ profile: profile.name,
1085
+ results,
1086
+ summary,
1087
+ format,
1088
+ });
1089
+
1090
+ return {
1091
+ content: [{
1092
+ type: "text" as const,
1093
+ text: `Exported ${artifact.id.slice(0, 8)}\nFormat: ${artifact.format}\nPath: ${artifact.outputPath}`,
1094
+ }],
1095
+ };
1096
+ }
1097
+ );
1098
+
1099
+ registerTool(
1100
+ "list_assets",
1101
+ "List recent structured memory assets, including pinned memories and distilled briefs.",
1102
+ {
1103
+ limit: z.number().min(1).max(50).default(12).describe("Max assets to list"),
1104
+ },
1105
+ async ({ limit }) => {
1106
+ const rows = listMemoryAssets(limit);
1107
+ if (rows.length === 0) {
1108
+ return { content: [{ type: "text" as const, text: "No assets yet." }] };
1109
+ }
1110
+ const lines = [
1111
+ "Asset ID Kind Title Scope / Sources Date",
1112
+ "-------- ----- ----- --------------- ----------",
1113
+ ...rows.map(row => assetSummaryLine(row)),
1114
+ ];
1115
+ return { content: [{ type: "text" as const, text: lines.join("\n") }] };
1116
+ }
1117
+ );
1118
+
1119
+ registerTool(
1120
+ "list_dirty_briefs",
1121
+ "Preview dirty memory briefs that were generated before the current brief-cleanup rules.",
1122
+ {},
1123
+ async () => {
1124
+ const rows = listDirtyBriefAssets();
1125
+ if (rows.length === 0) {
1126
+ return { content: [{ type: "text" as const, text: "No dirty briefs found." }] };
1127
+ }
1128
+ const lines = [
1129
+ "Brief ID Title Scope Reasons",
1130
+ "-------- ----- ----- ----------------------------------------",
1131
+ ...rows.map(row => `${row.id.slice(0, 8)} ${row.title} [${row.scope}] ${row.reasons.join("; ")}`),
1132
+ ];
1133
+ return { content: [{ type: "text" as const, text: lines.join("\n") }] };
1134
+ }
1135
+ );
1136
+
1137
+ registerTool(
1138
+ "clean_dirty_briefs",
1139
+ "Archive dirty briefs and remove their indexed asset scopes. Use preview mode first if unsure.",
1140
+ {
1141
+ apply: z.boolean().default(false).describe("When false, preview only. When true, archive and delete indexed rows."),
1142
+ },
1143
+ async ({ apply }) => {
1144
+ const rows = listDirtyBriefAssets();
1145
+ if (rows.length === 0) {
1146
+ return { content: [{ type: "text" as const, text: "No dirty briefs found." }] };
1147
+ }
1148
+
1149
+ if (!apply) {
1150
+ const preview = rows.map(row => `${row.id.slice(0, 8)} ${row.title} [${row.scope}] ${row.reasons.join("; ")}`).join("\n");
1151
+ return {
1152
+ content: [{
1153
+ type: "text" as const,
1154
+ text: `Dirty briefs detected: ${rows.length}\n\n${preview}\n\nCall clean_dirty_briefs with apply=true to archive them.`,
1155
+ }],
1156
+ };
1157
+ }
1158
+
1159
+ let archived = 0;
1160
+ let deleted = 0;
1161
+ for (const row of rows) {
1162
+ archiveDirtyBriefAsset(row);
1163
+ archived += 1;
1164
+ deleted += await store.bulkDelete([row.scope]);
1165
+ }
1166
+
1167
+ return {
1168
+ content: [{
1169
+ type: "text" as const,
1170
+ text: `Dirty briefs: ${rows.length}\nArchived: ${archived}\nIndex rows deleted: ${deleted}`,
1171
+ }],
1172
+ };
1173
+ }
1174
+ );
1175
+
1176
+ registerTool(
1177
+ "consolidate_memories",
1178
+ "Run semantic consolidation on a scope: cluster similar memories, merge near-duplicates, link related entries, and detect contradictions. Dry-run by default — set apply=true to actually archive merged entries.",
1179
+ {
1180
+ scope: z.string().min(1).max(160).describe("Scope to consolidate (e.g. project:recallnest)"),
1181
+ clusterThreshold: z.number().min(0.5).max(1.0).default(0.82).describe("Min similarity to form a cluster (default 0.82)"),
1182
+ mergeThreshold: z.number().min(0.5).max(1.0).default(0.92).describe("Min similarity to merge/archive (default 0.92)"),
1183
+ maxEntries: z.number().min(10).max(2000).default(500).describe("Max entries to scan (default 500)"),
1184
+ apply: z.boolean().default(false).describe("When false, preview only (scan + report without archiving). When true, actually merge/archive."),
1185
+ },
1186
+ async ({ scope, clusterThreshold, mergeThreshold, maxEntries, apply }) => {
1187
+ const { store } = getComponents();
1188
+
1189
+ if (!apply) {
1190
+ // Dry-run: use a read-only wrapper that blocks writes
1191
+ const readOnlyStore = {
1192
+ list: store.list.bind(store),
1193
+ getById: store.getById.bind(store),
1194
+ vectorSearch: store.vectorSearch.bind(store),
1195
+ update: async () => null, // no-op in dry-run
1196
+ };
1197
+ const engine = new ConsolidationEngine(readOnlyStore, { clusterThreshold, mergeThreshold, maxEntriesPerRun: maxEntries });
1198
+ const result = await engine.run(scope);
1199
+ return {
1200
+ content: [{
1201
+ type: "text" as const,
1202
+ text: `[DRY-RUN] ${formatConsolidationResult(result)}\n\nRe-run with apply=true to execute merges.`,
1203
+ }],
1204
+ };
1205
+ }
1206
+
1207
+ const engine = new ConsolidationEngine(store, { clusterThreshold, mergeThreshold, maxEntriesPerRun: maxEntries });
1208
+ const result = await engine.run(scope);
1209
+ return {
1210
+ content: [{
1211
+ type: "text" as const,
1212
+ text: formatConsolidationResult(result),
1213
+ }],
1214
+ };
1215
+ }
1216
+ );
1217
+
1218
+ registerTool(
1219
+ "list_pins",
1220
+ "List recently pinned memory assets.",
1221
+ {
1222
+ limit: z.number().min(1).max(50).default(10).describe("Max pinned assets to list"),
1223
+ },
1224
+ async ({ limit }) => {
1225
+ const rows = listPinAssets(limit);
1226
+ if (rows.length === 0) {
1227
+ return { content: [{ type: "text" as const, text: "No pinned assets yet." }] };
1228
+ }
1229
+ const lines = [
1230
+ "Pin ID Title Scope Date",
1231
+ "-------- ----- ----- ----------",
1232
+ ...rows.map(row => `${row.id.slice(0, 8)} ${row.title} [${row.source.scope}] ${row.createdAt.slice(0, 10)}`),
1233
+ ];
1234
+ return { content: [{ type: "text" as const, text: lines.join("\n") }] };
1235
+ }
1236
+ );
1237
+
1238
+ // --- memory_stats tool ---
1239
+ registerTool(
1240
+ "memory_stats",
1241
+ "Show statistics of the indexed memory database",
1242
+ {},
1243
+ async () => {
1244
+ const stats = await store.stats();
1245
+
1246
+ // Aggregate by source prefix
1247
+ const sourceCounts: Record<string, number> = {};
1248
+ for (const [scope, count] of Object.entries(stats.scopeCounts)) {
1249
+ const prefix = scope.split(":")[0];
1250
+ sourceCounts[prefix] = (sourceCounts[prefix] || 0) + count;
1251
+ }
1252
+
1253
+ const lines = [
1254
+ `Total entries: ${stats.totalCount}`,
1255
+ "",
1256
+ "By source:",
1257
+ ...Object.entries(sourceCounts)
1258
+ .sort((a, b) => b[1] - a[1])
1259
+ .map(([src, count]) => ` ${src}: ${count}`),
1260
+ "",
1261
+ "By category:",
1262
+ ...Object.entries(stats.categoryCounts || {})
1263
+ .sort((a, b) => b[1] - a[1])
1264
+ .map(([cat, count]) => ` ${cat}: ${count}`),
1265
+ ];
1266
+
1267
+ return {
1268
+ content: [{ type: "text" as const, text: lines.join("\n") }],
1269
+ };
1270
+ }
1271
+ );
1272
+
1273
+ // ============================================================================
1274
+ // Memory Drill-Down Tool
1275
+ // ============================================================================
1276
+
1277
+ registerTool(
1278
+ "memory_drill_down",
1279
+ "Get deeper content for a memory entry. Use after seeing compact summaries to get the full text or structured overview.",
1280
+ {
1281
+ id: z.string().describe("Memory ID or prefix (at least 8 hex chars)"),
1282
+ level: z.enum(["overview", "full"]).optional().default("full")
1283
+ .describe("Content depth: 'overview' (L1) or 'full' (L2, default)"),
1284
+ },
1285
+ async ({ id, level }) => {
1286
+ try {
1287
+ const entry = await store.getById(id);
1288
+ if (!entry) {
1289
+ return {
1290
+ content: [{ type: "text" as const, text: `No memory found with ID: ${id}` }],
1291
+ };
1292
+ }
1293
+
1294
+ // Parse metadata for L0/L1/L2 content
1295
+ let meta: Record<string, unknown> = {};
1296
+ try {
1297
+ meta = JSON.parse(entry.metadata || "{}");
1298
+ } catch { /* malformed metadata, use raw text */ }
1299
+
1300
+ // Support both legacy short names (l0/l1) and current long names (l0_abstract/l1_overview/l2_content)
1301
+ const l0 = typeof meta.l0_abstract === "string" ? meta.l0_abstract : typeof meta.l0 === "string" ? meta.l0 : null;
1302
+ const l1 = typeof meta.l1_overview === "string" ? meta.l1_overview : typeof meta.l1 === "string" ? meta.l1 : null;
1303
+ const l2 = typeof meta.l2_content === "string" ? meta.l2_content : entry.text;
1304
+
1305
+ let content: string;
1306
+ if (level === "overview" && l1) {
1307
+ content = `## ${entry.category} (L1 Overview)\n\n${l1}`;
1308
+ } else {
1309
+ content = `## ${entry.category} (Full Content)\n\n${l2}`;
1310
+ }
1311
+
1312
+ const header = [
1313
+ `**ID**: ${entry.id}`,
1314
+ `**Category**: ${entry.category}`,
1315
+ `**Scope**: ${entry.scope}`,
1316
+ `**Importance**: ${entry.importance}`,
1317
+ `**Created**: ${new Date(entry.timestamp).toISOString()}`,
1318
+ l0 ? `**Abstract**: ${l0}` : null,
1319
+ ].filter(Boolean).join("\n");
1320
+
1321
+ return {
1322
+ content: [{ type: "text" as const, text: `${header}\n\n${content}` }],
1323
+ };
1324
+ } catch (err) {
1325
+ return {
1326
+ content: [{ type: "text" as const, text: `Error drilling down: ${String(err)}` }],
1327
+ };
1328
+ }
1329
+ },
1330
+ );
1331
+
1332
+ // ============================================================================
1333
+ // Start
1334
+ // ============================================================================
1335
+
1336
+ const transport = new StdioServerTransport();
1337
+ await server.connect(transport);