recallnest 1.4.1 → 2.2.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 (233) hide show
  1. package/.env +4 -0
  2. package/CLAUDE.md +7 -3
  3. package/README.md +202 -130
  4. package/README_CN.md +263 -190
  5. package/bin/recallnest.mjs +62 -0
  6. package/config.json +55 -0
  7. package/daily-recall.sh +23 -0
  8. package/integrations/setup-all.sh +31 -0
  9. package/logs/daily-recall.error.log +0 -0
  10. package/logs/daily-recall.log +30 -0
  11. package/logs/ingest-2026-04-11.log +110 -0
  12. package/logs/ingest-2026-04-12.log +90 -0
  13. package/logs/launchagent-stderr.log +0 -0
  14. package/logs/launchagent-stdout.log +0 -0
  15. package/package.json +10 -3
  16. package/src/access-tracker.ts +3 -2
  17. package/src/activity-counter.ts +109 -0
  18. package/src/adaptive-retrieval.ts +6 -5
  19. package/src/admission-control.ts +163 -0
  20. package/src/anchor-generator.ts +80 -0
  21. package/src/api-server.ts +10 -0
  22. package/src/audit-log.ts +122 -0
  23. package/src/auto-gc.ts +98 -29
  24. package/src/auto-recall.ts +33 -4
  25. package/src/capture-engine.ts +372 -23
  26. package/src/cascade-forget.ts +3 -2
  27. package/src/cli.ts +107 -5
  28. package/src/compat.ts +22 -0
  29. package/src/confidence-tracker.ts +210 -47
  30. package/src/conflict-store.ts +12 -3
  31. package/src/consolidation-engine.ts +296 -8
  32. package/src/context-collapse-renderer.ts +222 -0
  33. package/src/context-composer-stable-selection.ts +5 -1
  34. package/src/context-composer.ts +239 -4
  35. package/src/context-reconstructor.ts +404 -0
  36. package/src/conversation-importer.ts +441 -0
  37. package/src/data-checkup.ts +232 -0
  38. package/src/decay-engine.ts +84 -0
  39. package/src/distill-lock.ts +122 -0
  40. package/src/doctor.ts +8 -10
  41. package/src/dream-pipeline.ts +227 -0
  42. package/src/emotion-detector.ts +90 -0
  43. package/src/eval.ts +3 -2
  44. package/src/forget-engine.ts +269 -0
  45. package/src/frequency-tracker.ts +175 -0
  46. package/src/graph-export.ts +644 -0
  47. package/src/ingest.ts +36 -6
  48. package/src/interference-detector.ts +187 -0
  49. package/src/kg-extractor.ts +15 -24
  50. package/src/kg-store.ts +2 -1
  51. package/src/language-hook.ts +67 -0
  52. package/src/llm-client.ts +289 -6
  53. package/src/mcp-server.ts +707 -80
  54. package/src/memory-assets.ts +22 -5
  55. package/src/memory-evolution.ts +365 -0
  56. package/src/memory-lint.ts +429 -0
  57. package/src/memory-output.ts +69 -0
  58. package/src/memory-schema.ts +74 -2
  59. package/src/narrative-schema.ts +79 -0
  60. package/src/narrative-tagger.ts +164 -0
  61. package/src/pii-detector.ts +98 -0
  62. package/src/post-retrieval-filter.ts +76 -0
  63. package/src/prediction-engine.ts +253 -0
  64. package/src/preference-slots.ts +65 -2
  65. package/src/prospective-memory.ts +215 -0
  66. package/src/query-expander.ts +67 -0
  67. package/src/recall-governor.ts +183 -0
  68. package/src/retention-policy.ts +90 -0
  69. package/src/retriever.ts +828 -55
  70. package/src/rif.ts +34 -8
  71. package/src/runtime-config.ts +23 -8
  72. package/src/scope-inventory.ts +4 -3
  73. package/src/scope-policy.ts +4 -0
  74. package/src/session-distiller.ts +307 -0
  75. package/src/session-output.ts +40 -0
  76. package/src/session-schema.ts +37 -0
  77. package/src/session-store.ts +7 -2
  78. package/src/skill-engine.ts +208 -0
  79. package/src/skill-promotion.ts +293 -0
  80. package/src/skill-schema.ts +39 -0
  81. package/src/store.ts +96 -10
  82. package/src/topic-tag.ts +66 -0
  83. package/src/tracker.ts +3 -1
  84. package/src/ui-server.ts +80 -8
  85. package/src/workflow-observation-scope-review.ts +2 -1
  86. package/src/workflow-observation-store.ts +2 -1
  87. package/src/write-verifier.ts +126 -0
  88. package/.claude-plugin/CLAUDE.md +0 -29
  89. package/.claude-plugin/marketplace.json +0 -18
  90. package/.github/workflows/ci.yml +0 -28
  91. package/.mcp.json +0 -9
  92. package/CHANGELOG.md +0 -51
  93. package/DEMO.md +0 -94
  94. package/EVAL.md +0 -89
  95. package/FAILURES.md +0 -75
  96. package/OPERATIONS.md +0 -59
  97. package/ROADMAP.md +0 -198
  98. package/assets/recallnest-workbench.png +0 -0
  99. package/assets/ui/app.js +0 -807
  100. package/assets/ui/index.html +0 -154
  101. package/assets/ui/styles.css +0 -605
  102. package/assets/wechat-qr.jpg +0 -0
  103. package/daily-reflections/2026-03-05.md +0 -15
  104. package/daily-reflections/2026-03-06.md +0 -15
  105. package/daily-reflections/2026-03-07.md +0 -7
  106. package/daily-reflections/2026-03-08.md +0 -34
  107. package/daily-reflections/2026-03-09.md +0 -35
  108. package/daily-reflections/2026-03-10.md +0 -36
  109. package/daily-reflections/2026-03-11.md +0 -35
  110. package/daily-reflections/2026-03-12.md +0 -36
  111. package/daily-reflections/2026-03-13.md +0 -35
  112. package/daily-reflections/2026-03-14.md +0 -35
  113. package/daily-reflections/2026-03-15.md +0 -35
  114. package/daily-reflections/2026-03-16.md +0 -41
  115. package/daily-reflections/2026-03-17.md +0 -10
  116. package/daily-reflections/2026-03-18.md +0 -10
  117. package/daily-reflections/2026-03-19.md +0 -10
  118. package/daily-reflections/2026-03-20.md +0 -10
  119. package/daily-reflections/2026-03-21.md +0 -10
  120. package/daily-reflections/2026-03-22.md +0 -10
  121. package/daily-reflections/2026-03-23.md +0 -10
  122. package/daily-reflections/2026-03-24.md +0 -10
  123. package/daily-reflections/2026-03-25.md +0 -32
  124. package/daily-reflections/2026-03-26.md +0 -37
  125. package/daily-reflections/2026-03-27.md +0 -35
  126. package/daily-reflections/2026-03-28.md +0 -35
  127. package/daily-reflections/2026-03-29.md +0 -35
  128. package/daily-reflections/2026-03-30.md +0 -36
  129. package/daily-reflections/2026-03-31.md +0 -35
  130. package/daily-reflections/2026-04-01.md +0 -35
  131. package/docs/api-reference.md +0 -1291
  132. package/docs/architecture.md +0 -168
  133. package/docs/memory-boundary-contract.md +0 -226
  134. package/docs/memory-categories.md +0 -119
  135. package/docs/pipeline-audit-ultramemory.md +0 -177
  136. package/docs/plans/2026-03-08-query-expansion.md +0 -270
  137. package/docs/plans/2026-03-16-continuity-layer.md +0 -196
  138. package/docs/plans/mcp-tiering-plan.md +0 -126
  139. package/docs/research/recall-quality-audit-2026-03-22.md +0 -153
  140. package/docs/self-evolution.md +0 -198
  141. package/docs/superpowers/plans/2026-03-19-ingest-pipeline-upgrade.md +0 -782
  142. package/docs/superpowers/specs/2026-03-19-ingest-pipeline-upgrade-design.md +0 -77
  143. package/docs/superpowers/specs/2026-03-23-pull-claude-desktop-design.md +0 -95
  144. package/eval/continuity/results-2026-03-16.md +0 -117
  145. package/eval/continuity/results-2026-03-19.md +0 -453
  146. package/eval/continuity/runbook.md +0 -110
  147. package/eval/continuity/seed-cases.md +0 -54
  148. package/eval/reports/2026-03-06-abstract-query-baseline.md +0 -96
  149. package/eval/reports/2026-03-06-aws-ssh-baseline.md +0 -82
  150. package/eval/reports/2026-03-06-baseline.md +0 -82
  151. package/eval/reports/query-expansion.md +0 -138
  152. package/lm +0 -19
  153. package/plan-v1.2-distributable.md +0 -105
  154. package/scripts/archive-distilled-junk.ts +0 -116
  155. package/scripts/backfill-l0l1l2.ts +0 -98
  156. package/scripts/blind-scan.ts +0 -588
  157. package/scripts/cleanup-step1-exact-dups.ts +0 -117
  158. package/scripts/cleanup-step2-template-dups.ts +0 -113
  159. package/scripts/cleanup-step3-stale-content.ts +0 -149
  160. package/scripts/cleanup-step4-orphan-scopes.ts +0 -131
  161. package/scripts/delete-distilled-junk.ts +0 -88
  162. package/scripts/distill-facts.ts +0 -305
  163. package/scripts/health-check.ts +0 -343
  164. package/scripts/incremental-ingest.sh +0 -71
  165. package/scripts/mlp-health-check.ts +0 -382
  166. package/scripts/pull-claude-desktop.ts +0 -515
  167. package/scripts/re-ingest-cc.sh +0 -66
  168. package/scripts/rebalance-memory-health.ts +0 -181
  169. package/scripts/run-desktop-ingest.sh +0 -11
  170. package/scripts/start-server.sh +0 -12
  171. package/scripts/viz-dashboard.html +0 -423
  172. package/scripts/viz-export.ts +0 -703
  173. package/src/__tests__/access-tracker-novelty.test.ts +0 -228
  174. package/src/__tests__/auto-consolidation.test.ts +0 -168
  175. package/src/__tests__/auto-recall.test.ts +0 -131
  176. package/src/__tests__/batch-dedup.test.ts +0 -65
  177. package/src/__tests__/capture-engine.test.ts +0 -1142
  178. package/src/__tests__/capture-heuristic.test.ts +0 -171
  179. package/src/__tests__/cascade-forget.test.ts +0 -210
  180. package/src/__tests__/category-min-scores.test.ts +0 -33
  181. package/src/__tests__/confidence-tracker.test.ts +0 -196
  182. package/src/__tests__/conflict-advisor.test.ts +0 -206
  183. package/src/__tests__/conflict-escalation.test.ts +0 -149
  184. package/src/__tests__/conflict-lifecycle.test.ts +0 -83
  185. package/src/__tests__/conflict-output.test.ts +0 -85
  186. package/src/__tests__/conflict-store.test.ts +0 -374
  187. package/src/__tests__/consolidation-engine.test.ts +0 -181
  188. package/src/__tests__/context-composer-pins.test.ts +0 -64
  189. package/src/__tests__/context-composer-task-selection.test.ts +0 -24
  190. package/src/__tests__/context-renderer.test.ts +0 -91
  191. package/src/__tests__/continuity-eval.test.ts +0 -275
  192. package/src/__tests__/continuity-schema.test.ts +0 -97
  193. package/src/__tests__/core-recall-eval.test.ts +0 -235
  194. package/src/__tests__/core-summary.test.ts +0 -109
  195. package/src/__tests__/dedup-actions.test.ts +0 -58
  196. package/src/__tests__/deterministic-id.test.ts +0 -39
  197. package/src/__tests__/doctor.test.ts +0 -303
  198. package/src/__tests__/drill-down.test.ts +0 -108
  199. package/src/__tests__/embedder.test.ts +0 -122
  200. package/src/__tests__/entity-resolver.test.ts +0 -58
  201. package/src/__tests__/event-segmenter.test.ts +0 -153
  202. package/src/__tests__/hotness-blend.test.ts +0 -76
  203. package/src/__tests__/ingest.test.ts +0 -360
  204. package/src/__tests__/kg-extractor.test.ts +0 -184
  205. package/src/__tests__/kg-store.test.ts +0 -198
  206. package/src/__tests__/llm-consolidation.test.ts +0 -226
  207. package/src/__tests__/memory-boundaries.test.ts +0 -237
  208. package/src/__tests__/memory-health-rebalance.test.ts +0 -144
  209. package/src/__tests__/memory-output.test.ts +0 -215
  210. package/src/__tests__/multi-vector.test.ts +0 -242
  211. package/src/__tests__/ppr-traversal.test.ts +0 -256
  212. package/src/__tests__/preference-guard-smoke.test.ts +0 -211
  213. package/src/__tests__/preference-matcher.test.ts +0 -230
  214. package/src/__tests__/preference-slots.test.ts +0 -106
  215. package/src/__tests__/prospective-memory.test.ts +0 -283
  216. package/src/__tests__/query-entity-detector.test.ts +0 -171
  217. package/src/__tests__/query-expander.test.ts +0 -34
  218. package/src/__tests__/recall-mode.test.ts +0 -64
  219. package/src/__tests__/result-synthesizer.test.ts +0 -198
  220. package/src/__tests__/resume-context.test.ts +0 -4743
  221. package/src/__tests__/retrieval-trace.test.ts +0 -213
  222. package/src/__tests__/retriever-boundary-weight.test.ts +0 -136
  223. package/src/__tests__/retroactive-boost.test.ts +0 -223
  224. package/src/__tests__/scope-inventory.test.ts +0 -200
  225. package/src/__tests__/scope-policy.test.ts +0 -69
  226. package/src/__tests__/session-checkpoint.test.ts +0 -259
  227. package/src/__tests__/session-gc.test.ts +0 -215
  228. package/src/__tests__/store-scope-audit.test.ts +0 -81
  229. package/src/__tests__/temporal-parser.test.ts +0 -241
  230. package/src/__tests__/term-registry.test.ts +0 -121
  231. package/src/__tests__/tool-output-compressor.test.ts +0 -133
  232. package/src/__tests__/version-manager.test.ts +0 -213
  233. package/src/__tests__/workflow-observation.test.ts +0 -400
package/src/mcp-server.ts CHANGED
@@ -31,10 +31,12 @@ const TOOL_TIERS: Record<string, ToolTier> = {
31
31
  store_memory: "core",
32
32
  checkpoint_session: "core",
33
33
  latest_checkpoint: "core",
34
+ list_tools: "core",
34
35
 
35
36
  set_reminder: "core",
36
37
 
37
38
  // Advanced
39
+ batch_store: "advanced",
38
40
  auto_capture: "advanced",
39
41
  store_case: "advanced",
40
42
  store_workflow_pattern: "advanced",
@@ -46,8 +48,18 @@ const TOOL_TIERS: Record<string, ToolTier> = {
46
48
  list_assets: "advanced",
47
49
  list_pins: "advanced",
48
50
  memory_stats: "advanced",
51
+ data_checkup: "advanced",
52
+ memory_lint: "advanced",
53
+ export_graph: "advanced",
54
+ dream: "advanced",
49
55
  memory_drill_down: "advanced",
50
56
  export_memory: "advanced",
57
+ store_skill: "advanced",
58
+ retrieve_skill: "advanced",
59
+ import_conversations: "advanced",
60
+ distill_session: "advanced",
61
+ scan_skill_promotions: "governance",
62
+ forget_memory: "advanced",
51
63
 
52
64
  // Governance (CLI-only, not in MCP by default)
53
65
  workflow_observe: "governance",
@@ -71,21 +83,25 @@ function shouldRegisterTool(toolName: string): boolean {
71
83
  return true;
72
84
  }
73
85
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
86
+ import { autoRegisterBabelMemory } from "./language-hook.js";
74
87
  import { z } from "zod";
75
88
  import type { RetrievalResult } from "./retriever.js";
76
89
  import type { MemoryStore } from "./store.js";
77
- import { distillResults, formatExplainResults, formatSearchResults, selectBriefSeedResults, summarizeResults } from "./memory-output.js";
90
+ import { distillResults, formatExplainResults, formatSearchResults, formatBriefResults, formatFullResults, selectBriefSeedResults, summarizeResults } from "./memory-output.js";
78
91
  import { archiveDirtyBriefAsset, assetSummaryLine, buildBriefAsset, buildPinAsset, listDirtyBriefAssets, listMemoryAssets, listPinAssets, saveBriefAsset, savePinAsset, writeExportArtifact } from "./memory-assets.js";
79
92
  import { indexAsset, indexPinnedAsset } from "./asset-sync.js";
80
93
  import { createComponentResolver, loadConfig, loadDotEnv, resolveRecallMode } from "./runtime-config.js";
81
- import { DurableMemoryCategorySchema, StoreMemorySourceSchema } from "./memory-schema.js";
94
+ import { DurableMemoryCategorySchema, StoreMemorySourceSchema, PrivacyTierSchema, isPredictiveMemoryEnabled } from "./memory-schema.js";
82
95
  import { persistCaseMemory, persistMemory, persistMemoryBatch, persistWorkflowPattern, promoteMemory } from "./capture-engine.js";
96
+ import { persistSkill, retrieveSkills } from "./skill-engine.js";
97
+ import { SkillImplementationTypeSchema } from "./skill-schema.js";
98
+ import { scanForPromotions, formatPromotionResult } from "./skill-promotion.js";
83
99
  import { autoCapture } from "./capture-heuristic.js";
84
100
  import { ConsolidationEngine, formatConsolidationResult } from "./consolidation-engine.js";
85
101
  import { renderMemories, type RenderMode } from "./context-renderer.js";
86
102
  import { buildSessionCheckpointResult } from "./session-engine.js";
87
103
  import { SessionCheckpointStore } from "./session-store.js";
88
- import { composeResumeContext } from "./context-composer.js";
104
+ import { composeLightResumeContext, composeResumeContext } from "./context-composer.js";
89
105
  import { formatCheckpointSaved, formatCheckpointSummary, formatResumeContext } from "./session-output.js";
90
106
  import { ConflictStatusSchema } from "./conflict-schema.js";
91
107
  import { KGStore } from "./kg-store.js";
@@ -93,6 +109,10 @@ import { createKGExtractor, isKGModeEnabled, type KGExtractor } from "./kg-extra
93
109
  import { resolveConflictCandidate } from "./conflict-engine.js";
94
110
  import { escalateConflicts } from "./conflict-escalation.js";
95
111
  import { ConflictCandidateStore } from "./conflict-store.js";
112
+ import { runDataCheckup, formatCheckupReport } from "./data-checkup.js";
113
+ import { runMemoryLint, formatMemoryLintReport } from "./memory-lint.js";
114
+ import { exportMemoryGraph, formatGraphExportResult } from "./graph-export.js";
115
+ import { runDream, formatDreamResult } from "./dream-pipeline.js";
96
116
  import { formatConflictAudit, formatConflictClusters, formatConflictEscalation, formatConflictList, formatConflictRecord, formatConflictResolution } from "./conflict-output.js";
97
117
  import { CONFLICT_ATTENTION_LEVELS, summarizeConflictLifecycle } from "./conflict-lifecycle.js";
98
118
  import { buildConflictAuditSummary, clusterConflicts } from "./conflict-advisor.js";
@@ -103,7 +123,11 @@ import { WorkflowObservationStore } from "./workflow-observation-store.js";
103
123
  import { buildManagedCheckpointObservation, buildManagedResumeObservation } from "./workflow-observation-managed.js";
104
124
  import { buildRetrievalContext, resolveScopeSelection } from "./scope-policy.js";
105
125
  import { matchesTemporalConstraint, type TemporalConstraint } from "./temporal-parser.js";
106
- import { setReminder, checkTriggers, fireReminder, formatReminders } from "./prospective-memory.js";
126
+ import { setReminder, checkTriggers, fireReminder, formatReminders, suggestPredictedReminders, formatSuggestedReminders, acceptPredictedReminder, demotePredictedReminder } from "./prospective-memory.js";
127
+ import type { PredictionContext } from "./prediction-engine.js";
128
+ import { forgetMemory, forgetByScope } from "./forget-engine.js";
129
+ import { createAuditLogger } from "./audit-log.js";
130
+ // distill_session uses dynamic import("./session-distiller.js") at call time
107
131
 
108
132
  function entryToRetrievalResult(entry: Awaited<ReturnType<MemoryStore["get"]>>): RetrievalResult {
109
133
  if (!entry) {
@@ -167,12 +191,16 @@ const server = new McpServer({
167
191
  type ToolSchema = Parameters<typeof server.tool>[2];
168
192
  type ToolHandler = Parameters<typeof server.tool>[3];
169
193
 
194
+ /** Map of all registered tool names to their descriptions (populated during registration). */
195
+ const TOOL_DESCRIPTIONS = new Map<string, string>();
196
+
170
197
  function registerTool(name: string, description: string, schema: ToolSchema, handler: ToolHandler): void {
171
198
  if (!shouldRegisterTool(name)) {
172
199
  // stdout is reserved for MCP JSON-RPC on stdio transports.
173
200
  console.error(`[MCP] Skipping ${name} (tier: ${TOOL_TIERS[name]})`);
174
201
  return;
175
202
  }
203
+ TOOL_DESCRIPTIONS.set(name, description);
176
204
  server.tool(name, description, schema, handler);
177
205
  }
178
206
 
@@ -214,7 +242,7 @@ registerTool(
214
242
 
215
243
  registerTool(
216
244
  "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.",
245
+ "Inspect workflow observation health: 7d/30d report for one workflow or dashboard of degraded workflows. Read-only. Use when checking if continuity primitives are succeeding or degrading.",
218
246
  {
219
247
  workflowId: z.string().min(1).max(120).optional().describe("Optional workflow primitive id"),
220
248
  scope: z.string().min(1).max(160).optional().describe("Optional scope filter"),
@@ -235,10 +263,10 @@ registerTool(
235
263
 
236
264
  registerTool(
237
265
  "workflow_evidence",
238
- "Generate an evidence pack for a workflow primitive, including recent issue observations, top signals, and suggested next actions.",
266
+ "Generate an evidence pack for a workflow primitive with recent issues, top signals, and suggested actions. Read-only. Use when investigating why a workflow is degraded and you need concrete failure examples.",
239
267
  {
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"),
268
+ workflowId: z.string().min(1).max(120).describe("Workflow primitive id, e.g. 'resume_context'"),
269
+ scope: z.string().min(1).max(160).optional().describe("Scope filter, e.g. 'project:recallnest'"),
242
270
  limit: z.number().int().min(1).max(20).default(5).describe("Max recent issue observations to include"),
243
271
  },
244
272
  async ({ workflowId, scope, limit }) => {
@@ -267,8 +295,19 @@ registerTool(
267
295
  source: StoreMemorySourceSchema.default("manual").describe("How this memory was captured"),
268
296
  tags: z.array(z.string().min(1).max(40)).max(8).default([]).describe("Optional tags"),
269
297
  canonicalKey: z.string().min(1).max(120).optional().describe("Optional stable key for merge/update semantics"),
298
+ topicTag: z.string().min(1).max(60).optional().describe("Optional topic tag for intra-scope partitioning (e.g. 'auth', 'deploy', 'testing'). Auto-detected if omitted."),
299
+ privacyTier: PrivacyTierSchema.default("durable").describe("Privacy tier: ephemeral (auto-expire, no KG), private (persist, no KG), durable (default), shared (cross-scope)"),
300
+ validUntil: z.union([z.string(), z.number()]).optional().describe("Optional expiration: ISO date string or ms timestamp. Memory will be deprioritized after this time."),
301
+ eventTime: z.union([z.string(), z.number()]).optional().describe("Optional event time: when the event actually happened (ISO date or ms), distinct from storage time."),
302
+ confidence: z.union([
303
+ z.number().min(0).max(1),
304
+ z.object({
305
+ score: z.number().min(0).max(1),
306
+ reliability: z.enum(["direct", "inferred", "hearsay"]).optional(),
307
+ }),
308
+ ]).optional().describe("Optional confidence override: number (0-1) or {score, reliability}. Auto-assigned from source if omitted."),
270
309
  },
271
- async ({ text, category, importance, scope, source, tags, canonicalKey }) => {
310
+ async ({ text, category, importance, scope, source, tags, canonicalKey, topicTag, privacyTier, validUntil, eventTime, confidence }) => {
272
311
  const { store, embedder } = getComponents();
273
312
  const stored = await persistMemory({
274
313
  store,
@@ -283,6 +322,13 @@ registerTool(
283
322
  source,
284
323
  tags,
285
324
  canonicalKey,
325
+ topicTag,
326
+ privacyTier,
327
+ // F3: Pass temporal validity params (extracted by persistMemory before Zod parse)
328
+ validUntil,
329
+ eventTime,
330
+ // F1: Pass confidence override (extracted by persistMemory before Zod parse)
331
+ confidence,
286
332
  });
287
333
 
288
334
  return {
@@ -357,7 +403,7 @@ registerTool(
357
403
 
358
404
  registerTool(
359
405
  "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.",
406
+ "Set a prospective memory reminder that auto-triggers during future search_memory calls when the trigger keywords match. Side effect: stores a reminder entry. Use when you need a future nudge tied to a specific context.",
361
407
  {
362
408
  trigger: z.string().min(1).max(200).describe("Trigger condition — keywords that should activate this reminder"),
363
409
  action: z.string().min(1).max(500).describe("What to remind about when the trigger fires"),
@@ -505,7 +551,7 @@ registerTool(
505
551
 
506
552
  registerTool(
507
553
  "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.",
554
+ "Promote an evidence memory into durable memory with an authority upgrade. Side effect: creates a new durable entry linked to the source evidence. Use when a transcript snippet or imported artifact contains a fact worth keeping across windows.",
509
555
  {
510
556
  memoryId: z.string().min(1).max(128).describe("Existing evidence memory ID or unique prefix"),
511
557
  text: z.string().min(1).max(4000).optional().describe("Optional cleaned durable text; defaults to the source entry text"),
@@ -547,9 +593,9 @@ registerTool(
547
593
 
548
594
  registerTool(
549
595
  "list_conflicts",
550
- "List or inspect open conflict candidates when incoming evidence promotions disagree with an existing durable memory for the same canonical key.",
596
+ "List or inspect conflict candidates where promoted evidence disagrees with existing durable memory. Read-only. Use when reviewing pending conflicts before resolution.",
551
597
  {
552
- conflictId: z.string().min(1).max(128).optional().describe("Optional conflict ID to inspect a single record"),
598
+ conflictId: z.string().min(1).max(128).optional().describe("Conflict ID to inspect a single record, e.g. 'c1d2e3f4'"),
553
599
  status: ConflictStatusSchema.optional().describe("Optional status filter"),
554
600
  attention: z.enum(CONFLICT_ATTENTION_LEVELS).optional().describe("Optional lifecycle filter: fresh / aging / stale / escalated / resolved"),
555
601
  canonicalKey: z.string().min(1).max(120).optional().describe("Optional canonical key filter"),
@@ -583,7 +629,7 @@ registerTool(
583
629
 
584
630
  registerTool(
585
631
  "resolve_conflict",
586
- "Resolve a stored conflict candidate by keeping the existing durable memory, accepting the incoming promoted evidence, or merging the two texts.",
632
+ "Resolve a conflict candidate by keeping existing, accepting incoming, or merging texts. Side effect: updates conflict status and may modify durable memory. Use when list_conflicts shows open conflicts that need a decision.",
587
633
  {
588
634
  conflictId: z.string().min(1).max(128).describe("Conflict ID to resolve"),
589
635
  resolution: z.enum(["accept_incoming", "keep_existing", "merge"]).describe("How to resolve the conflict"),
@@ -614,7 +660,7 @@ registerTool(
614
660
 
615
661
  registerTool(
616
662
  "audit_conflicts",
617
- "Generate a terminal-friendly conflict audit summary so you can see which stale or escalated conflict clusters should be reviewed first.",
663
+ "Generate a conflict audit summary showing priority clusters by staleness and escalation level. Read-only. Use when triaging which conflict clusters to resolve first.",
618
664
  {
619
665
  status: ConflictStatusSchema.optional().describe("Optional status filter"),
620
666
  canonicalKey: z.string().min(1).max(120).optional().describe("Optional canonical key filter"),
@@ -639,7 +685,7 @@ registerTool(
639
685
 
640
686
  registerTool(
641
687
  "escalate_conflicts",
642
- "Preview or apply the conflict aging policy so stale or escalated open conflicts are explicitly marked for operator review.",
688
+ "Preview or apply conflict aging policy to mark stale conflicts for operator review. Side effect: when apply=true, persists escalation metadata. Use when conflicts have aged past their attention threshold.",
643
689
  {
644
690
  attention: z.enum(["stale", "escalated"]).default("stale").describe("Only consider stale or escalated conflicts"),
645
691
  canonicalKey: z.string().min(1).max(120).optional().describe("Optional canonical key filter"),
@@ -712,10 +758,10 @@ registerTool(
712
758
 
713
759
  registerTool(
714
760
  "latest_checkpoint",
715
- "Fetch the latest saved checkpoint for a session or shared scope. Useful for inspecting current work state before resume_context exists.",
761
+ "Fetch the most recent saved checkpoint for a session or shared scope. Read-only. Use when you need to inspect current work state without running a full resume_context.",
716
762
  {
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"),
763
+ sessionId: z.string().min(1).max(160).optional().describe("Session identifier filter, e.g. 'abc123'"),
764
+ scope: z.string().min(1).max(160).optional().describe("Shared scope filter, e.g. 'project:recallnest'"),
719
765
  },
720
766
  async ({ sessionId, scope }) => {
721
767
  const latest = await checkpointStore.getLatest({ sessionId, scope });
@@ -730,7 +776,7 @@ registerTool(
730
776
 
731
777
  registerTool(
732
778
  "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.",
779
+ "Compose startup context for a fresh window by combining durable memory, patterns, cases, and the latest checkpoint. Read-only. Use when entering a new session and you need to recover prior decisions, open loops, and next actions.",
734
780
  {
735
781
  task: z.string().min(1).max(500).optional().describe("Optional current task or question to bias recall"),
736
782
  scope: z.string().min(1).max(160).optional().describe("Optional shared scope for project or terminal continuity"),
@@ -738,7 +784,7 @@ registerTool(
738
784
  limitPerSection: z.number().int().min(1).max(6).default(3).describe("Max items per section"),
739
785
  includeLatestCheckpoint: z.boolean().default(true).describe("Whether to include the latest checkpoint summary"),
740
786
  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)"),
787
+ mode: z.enum(["full", "light", "summary", "off"]).optional().describe("Override recall mode: 'full' (default), 'light' (<300 tokens), 'summary' (checkpoint only), 'off'"),
742
788
  },
743
789
  async ({ task, scope, sessionId, limitPerSection, includeLatestCheckpoint, profile: profileName, mode: modeOverride }) => {
744
790
  const effectiveMode = resolveRecallMode(config, modeOverride);
@@ -785,6 +831,44 @@ registerTool(
785
831
  };
786
832
  }
787
833
 
834
+ // --- light mode: <300 token ultra-light wake-up ---
835
+ if (effectiveMode === "light") {
836
+ const { retriever: lightRetriever } = getComponents(profileName);
837
+ const lightScope = resolveScopeSelection({
838
+ scope,
839
+ sessionId,
840
+ operation: "resume_context",
841
+ allowUnscoped: true,
842
+ });
843
+ const lightResult = await composeLightResumeContext({
844
+ retriever: lightRetriever,
845
+ checkpointStore,
846
+ }, {
847
+ task,
848
+ scope: lightScope.resolvedScope,
849
+ sessionId,
850
+ limitPerSection: limitPerSection,
851
+ includeLatestCheckpoint,
852
+ profile: profileName,
853
+ });
854
+ await saveManagedObservation({
855
+ workflowId: "resume_context",
856
+ outcome: "success",
857
+ summary: `Managed resume_context returned light-mode context (~${lightResult.text.length} chars).`,
858
+ scope: lightScope.resolvedScope || scope || "global",
859
+ source: "managed:recallnest",
860
+ signal: "managed-resume-light",
861
+ task,
862
+ tags: ["managed", "recallnest", "light-mode"],
863
+ });
864
+ return {
865
+ content: [{
866
+ type: "text" as const,
867
+ text: lightResult.text,
868
+ }],
869
+ };
870
+ }
871
+
788
872
  // --- full mode: existing compose behavior ---
789
873
  const { retriever, profile } = getComponents(profileName);
790
874
  const scopeSelection = resolveScopeSelection({
@@ -822,7 +906,7 @@ registerTool(
822
906
  // --- search_memory tool ---
823
907
  registerTool(
824
908
  "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.",
909
+ "Search indexed memories by semantic similarity and return ranked results with optional temporal filtering. Read-only, but may fire stored reminders as a side effect. Use proactively at the start of tasks, when debugging, writing, or when the user references past work.",
826
910
  {
827
911
  query: z.string().describe("Search query — natural language or keywords"),
828
912
  limit: z.number().min(1).max(20).default(5).describe("Max results to return"),
@@ -835,19 +919,37 @@ registerTool(
835
919
  after: z.string().optional().describe("Filter memories stored after this date (ISO format YYYY-MM-DD, or relative like '最近30天', 'last 7 days')"),
836
920
  before: z.string().optional().describe("Filter memories stored before this date (ISO format YYYY-MM-DD, or relative)"),
837
921
  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的朋友')."),
922
+ includeArchived: z.boolean().default(false).optional().describe("When true, also return archived/superseded/consolidated memories (default: only active)"),
923
+ detail_level: z.enum(["brief", "normal", "full"]).default("normal").optional()
924
+ .describe("Result detail level: brief (ID+score+one-liner), normal (default, current behavior), full (include metadata)"),
925
+ topicTag: z.string().min(1).max(60).optional()
926
+ .describe("Filter by topic tag (e.g. 'auth', 'deploy', 'testing'). Only returns memories tagged with this topic."),
927
+ reconstruct: z.boolean().default(false).describe(
928
+ "Return LLM-synthesized reconstruction alongside raw results. Requires RECALLNEST_CONSTRUCTIVE_RETRIEVAL=true."
929
+ ),
930
+ validAt: z.string().optional().describe("Query memories valid at a specific point in time (ISO date, e.g. '2025-06-15'). Returns only memories whose validity window covers this date."),
931
+ includeExpired: z.boolean().default(false).optional().describe("When true, include expired memories in results (demoted 80%). Default: only active/non-expired."),
838
932
  },
839
- async ({ query, limit, scope, sessionId, allScopes, category, profile: profileName, render, after, before, graph }) => {
933
+ async ({ query, limit, scope, sessionId, allScopes, category, profile: profileName, render, after, before, graph, includeArchived, detail_level, topicTag, reconstruct, validAt, includeExpired }) => {
840
934
  const { retriever, profile } = getComponents(profileName);
841
935
  // Ensure KG store is attached to non-default profile retrievers for PPR
842
936
  if (graph && kgStoreInstance) retriever.setKGStore(kgStoreInstance);
937
+ // Attach LLM client for constructive retrieval if available
938
+ if (reconstruct && llm) retriever.setLLMClient(llm);
843
939
  let results = await retriever.retrieve(buildRetrievalContext({
844
940
  query,
845
- limit: (after || before) ? limit * 3 : limit, // Over-fetch when temporal filtering will reduce results
941
+ limit: (after || before || topicTag) ? limit * 3 : limit,
846
942
  category,
847
943
  scope,
848
944
  sessionId,
849
945
  allScopes,
850
946
  graph,
947
+ includeArchived,
948
+ topicTag,
949
+ reconstruct,
950
+ // F3: Temporal validity filtering
951
+ validAt: validAt ? new Date(validAt).getTime() : undefined,
952
+ includeExpired: includeExpired ?? undefined,
851
953
  }, {
852
954
  operation: "search_memory",
853
955
  }));
@@ -896,10 +998,59 @@ registerTool(
896
998
  }
897
999
  }
898
1000
 
1001
+ // HP-predictive: Surface predicted reminders alongside search results
1002
+ let suggestedText = "";
1003
+ if (isPredictiveMemoryEnabled()) {
1004
+ try {
1005
+ const recentCheckpoints = await checkpointStore.listRecent({ scope, limit: 5 });
1006
+ const recentObservations = await workflowObservationStore.listRecent({ scope, limit: 20 });
1007
+ const predictionCtx: PredictionContext = {
1008
+ checkpoints: recentCheckpoints,
1009
+ workflowObservations: recentObservations,
1010
+ frequentMemories: [], // Populated by access tracker in future iteration
1011
+ uncoveredTopics: results.length === 0 && query ? [query] : [],
1012
+ };
1013
+ const suggestions = await suggestPredictedReminders(store, embedder, predictionCtx, scope ?? "global");
1014
+ suggestedText = formatSuggestedReminders(suggestions);
1015
+ } catch {
1016
+ // Prediction failure is non-critical — silently skip
1017
+ }
1018
+ }
1019
+
1020
+ const level = detail_level ?? "normal";
1021
+ const sections: string[] = [];
1022
+
1023
+ // Phase 4: Read reconstruction from first-class field (no metadata hack)
1024
+ const reconstruction = (results as import("./retriever.js").RetrievalResultSet).reconstruction;
1025
+ if (reconstruction?.reconstructed) {
1026
+ const sourceIds = reconstruction.sources.map(s => s.id).join(", ");
1027
+ const sourceTypes = [...new Set(reconstruction.sources.map(s => s.source.type))].join(", ");
1028
+ sections.push(
1029
+ `## Reconstructed Context (confidence: ${reconstruction.confidence.toFixed(2)}, coverage: ${reconstruction.coverage.toFixed(2)})\n${reconstruction.reconstructed}\n\nSources (${sourceTypes}): ${sourceIds}`
1030
+ );
1031
+ // Render contradictions if detected
1032
+ if (reconstruction.contradictions.length > 0) {
1033
+ const conflictLines = reconstruction.contradictions.map(c =>
1034
+ `- \u26a0\ufe0f ${c.description} [${c.memoryIds.join(" vs ")}]`
1035
+ );
1036
+ sections.push(`### Contradictions Detected\n${conflictLines.join("\n")}`);
1037
+ }
1038
+ }
1039
+
1040
+ let body: string;
1041
+ if (level === "brief") {
1042
+ body = formatBriefResults(results, { query });
1043
+ } else if (level === "full") {
1044
+ body = formatFullResults(results, { query, profile: profile.name });
1045
+ } else {
1046
+ body = formatSearchResults(results, { query, profile: profile.name });
1047
+ }
1048
+ sections.push(body);
1049
+
899
1050
  return {
900
1051
  content: [{
901
1052
  type: "text" as const,
902
- text: formatSearchResults(results, { query, profile: profile.name }) + reminderText,
1053
+ text: sections.join("\n\n") + reminderText + suggestedText,
903
1054
  }],
904
1055
  };
905
1056
  }
@@ -907,15 +1058,15 @@ registerTool(
907
1058
 
908
1059
  registerTool(
909
1060
  "explain_memory",
910
- "Explain why the indexed memories matched: retrieval path, freshness, file/session, and matched terms.",
1061
+ "Explain why memories matched a query: retrieval path, freshness, scope, and matched terms. Read-only. Use when search results seem unexpected and you need to debug ranking or scope filtering.",
911
1062
  {
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"),
1063
+ query: z.string().describe("Search query to explain — natural language or keywords, e.g. 'auth migration'"),
1064
+ limit: z.number().min(1).max(20).default(5).describe("Maximum number of matched results to analyze and explain (default: 5)"),
1065
+ scope: z.string().optional().describe("Restrict to a specific scope, e.g. 'project:myapp'. Omit to use default scope"),
1066
+ sessionId: z.string().min(1).max(160).optional().describe("Session identifier to infer session-scoped search, e.g. 'abc123'"),
1067
+ allScopes: z.boolean().default(false).describe("Set to true to search across all scopes instead of the default scope"),
1068
+ category: DurableMemoryCategorySchema.optional().describe("Filter results by memory category, e.g. 'preference', 'decision', 'fact'"),
1069
+ profile: z.enum(["default", "writing", "debug", "fact-check"]).optional().describe("Retrieval profile that tunes ranking: 'debug' for technical, 'fact-check' for precision"),
919
1070
  },
920
1071
  async ({ query, limit, scope, sessionId, allScopes, category, profile: profileName }) => {
921
1072
  const { retriever, profile } = getComponents(profileName);
@@ -940,14 +1091,14 @@ registerTool(
940
1091
 
941
1092
  registerTool(
942
1093
  "distill_memory",
943
- "Distill retrieved memories into a compact briefing with source map, takeaways, and reusable evidence.",
1094
+ "Distill retrieved memories into a compact briefing with source map, key takeaways, and reusable evidence. Use this when you need a synthesized summary of stored knowledge on a topic rather than raw search results. Returns a structured briefing with citations. Read-only — does not modify stored memories.",
944
1095
  {
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"),
1096
+ query: z.string().describe("Natural language topic or task to distill, e.g. 'authentication migration decisions'"),
1097
+ limit: z.number().min(1).max(20).default(8).describe("Maximum number of retrieved memories to include in the distillation (default: 8)"),
1098
+ scope: z.string().optional().describe("Restrict search to a specific scope, e.g. 'project:myapp'. Omit to use the default scope"),
1099
+ sessionId: z.string().min(1).max(160).optional().describe("Session identifier to infer session-scoped search, e.g. 'abc123'"),
1100
+ allScopes: z.boolean().default(false).describe("Set to true to search across all scopes instead of the default scope"),
1101
+ profile: z.enum(["default", "writing", "debug", "fact-check"]).optional().describe("Retrieval profile that tunes ranking weights: 'writing' for narrative, 'debug' for technical, 'fact-check' for high-precision"),
951
1102
  },
952
1103
  async ({ query, limit, scope, sessionId, allScopes, profile: profileName }) => {
953
1104
  const { retriever, profile } = getComponents(profileName || "writing");
@@ -971,15 +1122,15 @@ registerTool(
971
1122
 
972
1123
  registerTool(
973
1124
  "brief_memory",
974
- "Create a structured memory brief from retrieved results and feed it back into recall.",
1125
+ "Create a structured memory brief by retrieving and summarizing relevant memories, then persist it as a reusable asset indexed for future recall. Use this when you want to consolidate scattered knowledge on a topic into a single retrievable document. Side effect: writes a new brief asset to disk and indexes it in the vector store for future search.",
975
1126
  {
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"),
1127
+ query: z.string().describe("Natural language topic or task to brief, e.g. 'deployment pipeline architecture decisions'"),
1128
+ limit: z.number().min(1).max(20).default(8).describe("Maximum number of source memories to include in the brief (default: 8)"),
1129
+ scope: z.string().optional().describe("Restrict search to a specific scope, e.g. 'project:myapp'. Omit to use the default scope"),
1130
+ sessionId: z.string().min(1).max(160).optional().describe("Session identifier to infer session-scoped search, e.g. 'abc123'"),
1131
+ allScopes: z.boolean().default(false).describe("Set to true to search across all scopes instead of the default scope"),
1132
+ profile: z.enum(["default", "writing", "debug", "fact-check"]).optional().describe("Retrieval profile that tunes ranking weights: 'writing' for narrative, 'debug' for technical, 'fact-check' for high-precision"),
1133
+ title: z.string().optional().describe("Human-readable title for the brief asset, e.g. 'Q1 Auth Migration Summary'. Auto-generated if omitted"),
983
1134
  },
984
1135
  async ({ query, limit, scope, sessionId, allScopes, profile: profileName, title }) => {
985
1136
  const { retriever, profile, store, embedder } = getComponents(profileName || "writing");
@@ -1012,16 +1163,16 @@ registerTool(
1012
1163
 
1013
1164
  registerTool(
1014
1165
  "pin_memory",
1015
- "Promote one retrieved memory into a pinned asset for later reuse.",
1166
+ "Pin a retrieved memory as a high-importance reusable asset on disk. Side effect: boosts importance to 0.95, writes pin asset file, and indexes it. Use when a search result is critical and should be surfaced in future recalls.",
1016
1167
  {
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"),
1168
+ memory_id: z.string().describe("Memory ID or unique prefix from search/explain output, e.g. 'a1b2c3d4'"),
1169
+ scope: z.string().optional().describe("Explicit scope filter, e.g. 'project:recallnest'"),
1170
+ sessionId: z.string().min(1).max(160).optional().describe("Session identifier to infer session:<id> scope, e.g. 'abc123'"),
1171
+ allScopes: z.boolean().default(false).describe("When true, allow cross-scope reads to find the memory"),
1172
+ title: z.string().optional().describe("Human-readable title for the pin, e.g. 'Auth migration decision'"),
1173
+ summary: z.string().optional().describe("Short summary override for the pinned asset"),
1174
+ query: z.string().optional().describe("Original query that led to this pin, e.g. 'auth decisions'"),
1175
+ profile: z.enum(["default", "writing", "debug", "fact-check"]).optional().describe("Retrieval profile for ranking, e.g. 'debug'"),
1025
1176
  },
1026
1177
  async ({ memory_id, scope, sessionId, allScopes, title, summary, query, profile: profileName }) => {
1027
1178
  const { store, embedder } = getComponents(profileName);
@@ -1057,15 +1208,15 @@ registerTool(
1057
1208
 
1058
1209
  registerTool(
1059
1210
  "export_memory",
1060
- "Export a distilled memory briefing to a markdown or json artifact on disk.",
1211
+ "Export a distilled memory briefing to a markdown or JSON file on disk. Side effect: writes an export artifact file. Use when you need an offline-readable snapshot of knowledge on a topic.",
1061
1212
  {
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"),
1213
+ query: z.string().describe("Topic or task to export, e.g. 'auth migration decisions'"),
1214
+ limit: z.number().min(1).max(20).default(8).describe("Maximum number of source memories to include in the export (default: 8)"),
1215
+ scope: z.string().optional().describe("Restrict to a specific scope, e.g. 'project:recallnest'. Omit to use default scope"),
1216
+ sessionId: z.string().min(1).max(160).optional().describe("Session identifier to infer session-scoped search, e.g. 'abc123'"),
1217
+ allScopes: z.boolean().default(false).describe("Set to true to search across all scopes instead of the default scope"),
1218
+ profile: z.enum(["default", "writing", "debug", "fact-check"]).optional().describe("Retrieval profile for ranking, e.g. 'writing'"),
1219
+ format: z.enum(["md", "json"]).default("md").describe("Export format: 'md' for markdown, 'json' for structured JSON"),
1069
1220
  },
1070
1221
  async ({ query, limit, scope, sessionId, allScopes, profile: profileName, format }) => {
1071
1222
  const { retriever, profile } = getComponents(profileName || "writing");
@@ -1098,9 +1249,9 @@ registerTool(
1098
1249
 
1099
1250
  registerTool(
1100
1251
  "list_assets",
1101
- "List recent structured memory assets, including pinned memories and distilled briefs.",
1252
+ "List recent structured memory assets (pinned memories and distilled briefs) sorted by creation date. Read-only. Use when you need an inventory of persisted knowledge artifacts — for example, before creating a new brief to avoid duplicates. Returns asset type, title, scope, creation date, and file path for each entry.",
1102
1253
  {
1103
- limit: z.number().min(1).max(50).default(12).describe("Max assets to list"),
1254
+ limit: z.number().min(1).max(50).default(12).describe("Maximum number of assets to return, sorted most-recent-first (default: 12, max: 50)"),
1104
1255
  },
1105
1256
  async ({ limit }) => {
1106
1257
  const rows = listMemoryAssets(limit);
@@ -1118,7 +1269,7 @@ registerTool(
1118
1269
 
1119
1270
  registerTool(
1120
1271
  "list_dirty_briefs",
1121
- "Preview dirty memory briefs that were generated before the current brief-cleanup rules.",
1272
+ "List memory briefs generated before current cleanup rules that may need re-indexing. Read-only. Use when auditing brief quality or before running clean_dirty_briefs.",
1122
1273
  {},
1123
1274
  async () => {
1124
1275
  const rows = listDirtyBriefAssets();
@@ -1136,9 +1287,9 @@ registerTool(
1136
1287
 
1137
1288
  registerTool(
1138
1289
  "clean_dirty_briefs",
1139
- "Archive dirty briefs and remove their indexed asset scopes. Use preview mode first if unsure.",
1290
+ "Archive dirty briefs and remove their indexed asset entries. Side effect: when apply=true, moves briefs to archive and deletes index rows. Use when list_dirty_briefs shows stale briefs that need cleanup.",
1140
1291
  {
1141
- apply: z.boolean().default(false).describe("When false, preview only. When true, archive and delete indexed rows."),
1292
+ apply: z.boolean().default(false).describe("When false, preview only (no writes). When true, archive briefs and delete indexed rows."),
1142
1293
  },
1143
1294
  async ({ apply }) => {
1144
1295
  const rows = listDirtyBriefAssets();
@@ -1159,9 +1310,17 @@ registerTool(
1159
1310
  let archived = 0;
1160
1311
  let deleted = 0;
1161
1312
  for (const row of rows) {
1162
- archiveDirtyBriefAsset(row);
1163
- archived += 1;
1164
- deleted += await store.bulkDelete([row.scope]);
1313
+ try {
1314
+ archiveDirtyBriefAsset(row);
1315
+ archived += 1;
1316
+ } catch (err) {
1317
+ console.error("[recallnest] Failed to archive dirty brief:", err instanceof Error ? err.message : String(err));
1318
+ }
1319
+ try {
1320
+ deleted += await store.bulkDelete([row.scope]);
1321
+ } catch (err) {
1322
+ console.error("[recallnest] Failed to delete dirty brief index rows:", err instanceof Error ? err.message : String(err));
1323
+ }
1165
1324
  }
1166
1325
 
1167
1326
  return {
@@ -1175,7 +1334,7 @@ registerTool(
1175
1334
 
1176
1335
  registerTool(
1177
1336
  "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.",
1337
+ "Run semantic consolidation: cluster similar memories, merge near-duplicates, and detect contradictions. Side effect: when apply=true, archives merged entries. Use when a scope has grown large and needs deduplication.",
1179
1338
  {
1180
1339
  scope: z.string().min(1).max(160).describe("Scope to consolidate (e.g. project:recallnest)"),
1181
1340
  clusterThreshold: z.number().min(0.5).max(1.0).default(0.82).describe("Min similarity to form a cluster (default 0.82)"),
@@ -1215,11 +1374,59 @@ registerTool(
1215
1374
  }
1216
1375
  );
1217
1376
 
1377
+ // --- forget_memory tool (Ethics Layer) ---
1378
+ registerTool(
1379
+ "forget_memory",
1380
+ "Permanently forget a memory with full cascade: delete primary entry, remove KG triples, demote related memories, and log an audit trail. Requires confirm=true for durable-tier memories. Use when the user explicitly requests a memory be forgotten, or to clean up sensitive/incorrect data.",
1381
+ {
1382
+ memoryId: z.string().min(1).max(128).describe("Memory ID to forget (full UUID or 8+ hex prefix)"),
1383
+ confirm: z.boolean().default(false).describe("Required confirmation — must be true for durable-tier memories"),
1384
+ reason: z.string().min(1).max(200).optional().describe("Reason for forgetting (recorded in audit trail)"),
1385
+ scope: z.string().min(1).max(160).optional().describe("Optional scope filter for permission check"),
1386
+ },
1387
+ async ({ memoryId, confirm, reason, scope }) => {
1388
+ const { store } = getComponents();
1389
+ const auditLogger = createAuditLogger();
1390
+ const scopeFilter = scope ? [scope] : undefined;
1391
+
1392
+ const result = await forgetMemory(
1393
+ { store, kgStore: kgStoreInstance, auditLogger },
1394
+ { memoryId, confirm, reason, scopeFilter },
1395
+ );
1396
+
1397
+ if (!result.success) {
1398
+ return {
1399
+ content: [{
1400
+ type: "text" as const,
1401
+ text: `❌ Forget failed: ${result.error}`,
1402
+ }],
1403
+ };
1404
+ }
1405
+
1406
+ const lines = [
1407
+ `✅ Memory ${result.memoryId.slice(0, 8)} forgotten.`,
1408
+ `Privacy tier: ${result.evidence?.privacyTier || "unknown"}`,
1409
+ `KG triples removed: ${result.kgTriplesRemoved ? "yes" : "no/N/A"}`,
1410
+ `Cascade demoted: ${result.cascadeResult.demotedCount} related memories`,
1411
+ ];
1412
+ if (result.evidence?.reason) {
1413
+ lines.push(`Reason: ${result.evidence.reason}`);
1414
+ }
1415
+
1416
+ return {
1417
+ content: [{
1418
+ type: "text" as const,
1419
+ text: lines.join("\n"),
1420
+ }],
1421
+ };
1422
+ },
1423
+ );
1424
+
1218
1425
  registerTool(
1219
1426
  "list_pins",
1220
- "List recently pinned memory assets.",
1427
+ "List pinned memory assets sorted by creation date, showing title, scope, importance score, and file path. Read-only. Use when you need to review high-value memories that were explicitly pinned via pin_memory, or to check if a topic already has a pinned reference before creating a new one.",
1221
1428
  {
1222
- limit: z.number().min(1).max(50).default(10).describe("Max pinned assets to list"),
1429
+ limit: z.number().min(1).max(50).default(10).describe("Maximum number of pinned assets to return, sorted most-recent-first (default: 10, max: 50)"),
1223
1430
  },
1224
1431
  async ({ limit }) => {
1225
1432
  const rows = listPinAssets(limit);
@@ -1238,7 +1445,7 @@ registerTool(
1238
1445
  // --- memory_stats tool ---
1239
1446
  registerTool(
1240
1447
  "memory_stats",
1241
- "Show statistics of the indexed memory database",
1448
+ "Show aggregate statistics of the memory database: total entries, counts by source and category. Read-only. Use when you need an overview of memory store health or size.",
1242
1449
  {},
1243
1450
  async () => {
1244
1451
  const stats = await store.stats();
@@ -1270,15 +1477,97 @@ registerTool(
1270
1477
  }
1271
1478
  );
1272
1479
 
1480
+ // ============================================================================
1481
+ // LC-P4: Data Checkup Tool
1482
+ // ============================================================================
1483
+
1484
+ registerTool(
1485
+ "data_checkup",
1486
+ "Run health checks on the memory database: vector dimensions, orphans, tier distribution, and conflict backlog. Read-only. Use when diagnosing data quality issues or before a consolidation run.",
1487
+ {},
1488
+ async () => {
1489
+ const openConflicts = (await conflictStore.listRecent({ status: "open", limit: 200 })).length;
1490
+ const report = await runDataCheckup({ store, openConflictCount: openConflicts });
1491
+ return {
1492
+ content: [{ type: "text" as const, text: formatCheckupReport(report) }],
1493
+ };
1494
+ }
1495
+ );
1496
+
1497
+ // ============================================================================
1498
+ // Memory Lint Tool
1499
+ // ============================================================================
1500
+
1501
+ registerTool(
1502
+ "memory_lint",
1503
+ "Run memory quality lint checks: contradictions, duplicates, stale entries, and orphans. Read-only. Returns a health score (0-100) and actionable findings. Use for periodic memory hygiene or before consolidation.",
1504
+ {
1505
+ scope: z.string().min(1).max(160).optional().describe("Optional scope filter, e.g. 'project:recallnest'. Omit to lint all scopes"),
1506
+ verbose: z.boolean().default(false).describe("Include all individual findings in output (default: summarized)"),
1507
+ },
1508
+ async ({ scope, verbose }) => {
1509
+ const report = await runMemoryLint({ store, scope, verbose });
1510
+ return {
1511
+ content: [{ type: "text" as const, text: formatMemoryLintReport(report) }],
1512
+ };
1513
+ }
1514
+ );
1515
+
1516
+ // ============================================================================
1517
+ // Knowledge Graph Export Tool
1518
+ // ============================================================================
1519
+
1520
+ registerTool(
1521
+ "export_graph",
1522
+ "Export memories as an interactive HTML knowledge graph. Creates a self-contained HTML file with a force-directed visualization. Open in any browser. Use when the user wants to visualize their memory network.",
1523
+ {
1524
+ scope: z.string().min(1).max(160).optional().describe("Optional scope filter"),
1525
+ maxNodes: z.number().int().min(10).max(500).default(200).describe("Maximum nodes to include (default 200)"),
1526
+ },
1527
+ async ({ scope, maxNodes }) => {
1528
+ const { path, graph } = await exportMemoryGraph(store, { scope, maxNodes });
1529
+ return {
1530
+ content: [{ type: "text" as const, text: formatGraphExportResult(path, graph) }],
1531
+ };
1532
+ }
1533
+ );
1534
+
1535
+ // ============================================================================
1536
+ // AD-1: Dream Pipeline Tool
1537
+ // ============================================================================
1538
+
1539
+ registerTool(
1540
+ "dream",
1541
+ "Run a full memory consolidation cycle (Orient, Gather, Consolidate, Prune). Side effect: may archive low-value entries and generate insight memories. Use when memory count is high and you need periodic maintenance.",
1542
+ {
1543
+ scope: z.string().min(1).max(160).optional().describe("Scope to consolidate, e.g. 'project:myapp'. Omit to consolidate across all scopes"),
1544
+ force: z.boolean().default(false).describe("Set to true to force consolidation even if recent write count is below the automatic threshold"),
1545
+ },
1546
+ async ({ scope, force }) => {
1547
+ const resolvedScope = scope || "project:default";
1548
+ const components = getComponents();
1549
+ const result = await runDream({
1550
+ store: components.store,
1551
+ llm: components.llm,
1552
+ embedder: components.embedder,
1553
+ scope: resolvedScope,
1554
+ force,
1555
+ });
1556
+ return {
1557
+ content: [{ type: "text" as const, text: formatDreamResult(result) }],
1558
+ };
1559
+ }
1560
+ );
1561
+
1273
1562
  // ============================================================================
1274
1563
  // Memory Drill-Down Tool
1275
1564
  // ============================================================================
1276
1565
 
1277
1566
  registerTool(
1278
1567
  "memory_drill_down",
1279
- "Get deeper content for a memory entry. Use after seeing compact summaries to get the full text or structured overview.",
1568
+ "Retrieve the full or overview-level content of a single memory entry. Read-only. Use when search returned compact summaries and you need the complete text or L1 overview.",
1280
1569
  {
1281
- id: z.string().describe("Memory ID or prefix (at least 8 hex chars)"),
1570
+ id: z.string().describe("Memory ID or unique prefix (at least 8 hex chars), e.g. 'a1b2c3d4'"),
1282
1571
  level: z.enum(["overview", "full"]).optional().default("full")
1283
1572
  .describe("Content depth: 'overview' (L1) or 'full' (L2, default)"),
1284
1573
  },
@@ -1329,9 +1618,347 @@ registerTool(
1329
1618
  },
1330
1619
  );
1331
1620
 
1621
+ // ============================================================================
1622
+ // Skill Memory Tools (D-1)
1623
+ // ============================================================================
1624
+
1625
+ registerTool(
1626
+ "store_skill",
1627
+ "Store an executable skill with trigger conditions, implementation, and verification steps. Side effect: persists a new skill entry and indexes it. Use when you identify a reusable procedure worth automating across sessions.",
1628
+ {
1629
+ name: z.string().min(1).max(120).describe("Unique skill identifier, e.g. 'deploy_production' or 'run_migrations'"),
1630
+ description: z.string().min(1).max(500).describe("Natural language description of what the skill does (used for semantic retrieval matching)"),
1631
+ triggerPattern: z.string().min(1).max(300).describe("Natural language pattern describing when to suggest this skill, e.g. 'user asks to deploy to production'"),
1632
+ implementationType: SkillImplementationTypeSchema.describe("Execution type: 'bash' for shell scripts, 'python' for Python code, 'mcp_tool_chain' for MCP sequences, 'instruction_sequence' for step-by-step instructions"),
1633
+ implementation: z.string().min(1).max(5000).describe("Executable content: the actual script, code, or instruction steps to run"),
1634
+ inputSchema: z.record(z.string(), z.unknown()).optional().describe("JSON Schema defining the skill's input parameters, e.g. {\"env\": {\"type\": \"string\"}}"),
1635
+ verification: z.string().max(500).optional().describe("Steps to verify the skill executed correctly, e.g. 'check deployment URL returns 200'"),
1636
+ scope: z.string().min(1).max(160).describe("Scope to store the skill under, e.g. 'project:recallnest'"),
1637
+ source: z.enum(["manual", "agent", "api"]).default("agent").describe("How this skill was captured: 'manual' by user, 'agent' by AI, or 'api' programmatically"),
1638
+ tags: z.array(z.string().max(60)).max(6).default([]).describe("Optional categorization tags, e.g. ['deployment', 'production']"),
1639
+ },
1640
+ async ({ name, description, triggerPattern, implementationType, implementation, inputSchema, verification, scope, source, tags }) => {
1641
+ const { store, embedder } = getComponents();
1642
+ const stored = await persistSkill(store, embedder, {
1643
+ name,
1644
+ description,
1645
+ triggerPattern,
1646
+ implementationType,
1647
+ implementation,
1648
+ inputSchema,
1649
+ verification,
1650
+ scope,
1651
+ source,
1652
+ tags,
1653
+ });
1654
+
1655
+ return {
1656
+ content: [{
1657
+ type: "text" as const,
1658
+ text: [
1659
+ `Stored skill ${stored.id.slice(0, 8)}`,
1660
+ `Name: ${stored.name}`,
1661
+ `Type: ${stored.implementationType}`,
1662
+ `Scope: ${stored.scope}`,
1663
+ `Tags: ${stored.tags.join(", ") || "-"}`,
1664
+ `Stored at: ${stored.storedAt}`,
1665
+ ].join("\n"),
1666
+ }],
1667
+ };
1668
+ },
1669
+ );
1670
+
1671
+ registerTool(
1672
+ "retrieve_skill",
1673
+ "Retrieve executable skills matching a task description by semantic similarity. Read-only. Use when you need a stored procedure to act on, not just recall knowledge.",
1674
+ {
1675
+ query: z.string().min(1).max(300).describe("Natural language task description to match, e.g. 'deploy the app to production'"),
1676
+ scope: z.string().min(1).max(160).optional().describe("Restrict to skills in a specific scope, e.g. 'project:myapp'. Omit to search all scopes"),
1677
+ limit: z.number().min(1).max(10).default(3).describe("Maximum number of matching skills to return, sorted by relevance (default: 3)"),
1678
+ },
1679
+ async ({ query, scope, limit }) => {
1680
+ const { store, embedder } = getComponents();
1681
+ const results = await retrieveSkills(store, embedder, query, scope, limit);
1682
+
1683
+ if (results.length === 0) {
1684
+ return {
1685
+ content: [{
1686
+ type: "text" as const,
1687
+ text: "No matching skills found.",
1688
+ }],
1689
+ };
1690
+ }
1691
+
1692
+ const formatted = results.map(({ skill, score }, index) => [
1693
+ `## ${index + 1}. ${skill.name} (score: ${score.toFixed(3)})`,
1694
+ `**Description**: ${skill.description}`,
1695
+ `**Trigger**: ${skill.triggerPattern}`,
1696
+ `**Type**: ${skill.implementationType}`,
1697
+ skill.verification ? `**Verification**: ${skill.verification}` : null,
1698
+ `**Tags**: ${skill.tags.join(", ") || "-"}`,
1699
+ "",
1700
+ "```",
1701
+ skill.implementation,
1702
+ "```",
1703
+ ].filter((line): line is string => line !== null).join("\n")).join("\n\n---\n\n");
1704
+
1705
+ return {
1706
+ content: [{
1707
+ type: "text" as const,
1708
+ text: `Found ${results.length} skill(s):\n\n${formatted}`,
1709
+ }],
1710
+ };
1711
+ },
1712
+ );
1713
+
1714
+ // --- D-2: scan_skill_promotions tool ---
1715
+ registerTool(
1716
+ "scan_skill_promotions",
1717
+ "Scan cases and patterns in a scope for potential promotion to reusable skills. Read-only. Use when you want to discover recurring procedures that deserve formalization as skills.",
1718
+ {
1719
+ scope: z.string().min(1).max(160).describe("Project scope to scan for promotion candidates"),
1720
+ minOccurrences: z.number().min(2).max(20).default(3).describe("Minimum similar cases to trigger a promotion suggestion"),
1721
+ },
1722
+ async ({ scope, minOccurrences }) => {
1723
+ const { store } = getComponents();
1724
+ const result = await scanForPromotions(store, scope, {
1725
+ minCaseOccurrences: minOccurrences,
1726
+ });
1727
+
1728
+ return {
1729
+ content: [{
1730
+ type: "text" as const,
1731
+ text: formatPromotionResult(result),
1732
+ }],
1733
+ };
1734
+ },
1735
+ );
1736
+
1737
+ // --- MCP-1: list_tools tool ---
1738
+ registerTool(
1739
+ "list_tools",
1740
+ "List available RecallNest tools with one-line descriptions, filtered by tier. Read-only. Use when you need to discover advanced or governance tools beyond the core set.",
1741
+ {
1742
+ tier: z.enum(["core", "advanced", "full"]).default("advanced").optional()
1743
+ .describe("Which tier of tools to list. Returns tools at this tier and below."),
1744
+ },
1745
+ async ({ tier }) => {
1746
+ const requestedTier = tier ?? "advanced";
1747
+ const tierOrder: Record<string, number> = { core: 0, advanced: 1, governance: 2 };
1748
+ const maxOrder = requestedTier === "full" ? 2 : tierOrder[requestedTier] ?? 1;
1749
+
1750
+ const lines: string[] = [`Available tools (tier: ${requestedTier}):`];
1751
+ for (const [toolName, toolTier] of Object.entries(TOOL_TIERS)) {
1752
+ if ((tierOrder[toolTier] ?? 999) > maxOrder) continue;
1753
+ const desc = TOOL_DESCRIPTIONS.get(toolName);
1754
+ const oneLiner = desc
1755
+ ? desc.split(/[.!]\s/)[0]?.slice(0, 100) ?? desc.slice(0, 100)
1756
+ : "(no description)";
1757
+ lines.push(`- ${toolName}: ${oneLiner}`);
1758
+ }
1759
+
1760
+ return {
1761
+ content: [{
1762
+ type: "text" as const,
1763
+ text: lines.join("\n"),
1764
+ }],
1765
+ };
1766
+ },
1767
+ );
1768
+
1769
+ // --- MCP-3: batch_store tool ---
1770
+ registerTool(
1771
+ "batch_store",
1772
+ "Store multiple memories in a single call with deduplication. Side effect: persists up to 20 entries. Use when you have several facts to store at once, more efficient than repeated store_memory calls.",
1773
+ {
1774
+ memories: z.array(z.object({
1775
+ text: z.string().min(1),
1776
+ category: DurableMemoryCategorySchema.default("events"),
1777
+ importance: z.number().min(0).max(1).default(0.7),
1778
+ tags: z.array(z.string()).max(6).default([]),
1779
+ })).min(1).max(20),
1780
+ scope: z.string().min(1).max(160),
1781
+ source: z.enum(["manual", "agent", "api"]).default("agent"),
1782
+ },
1783
+ async ({ memories, scope, source }) => {
1784
+ const { store, embedder } = getComponents();
1785
+ const stored = await persistMemoryBatch({
1786
+ store,
1787
+ embedder,
1788
+ conflictStore,
1789
+ kgExtractor,
1790
+ }, {
1791
+ scope,
1792
+ source,
1793
+ defaultImportance: 0.7,
1794
+ memories: memories.map((m) => ({
1795
+ text: m.text,
1796
+ category: m.category,
1797
+ importance: m.importance,
1798
+ tags: m.tags,
1799
+ })),
1800
+ });
1801
+
1802
+ const counts = { new: 0, deduped: 0, updated: 0 };
1803
+ for (const r of stored) {
1804
+ if (r.disposition === "deduped") counts.deduped++;
1805
+ else if (r.disposition === "updated") counts.updated++;
1806
+ else counts.new++;
1807
+ }
1808
+
1809
+ return {
1810
+ content: [{
1811
+ type: "text" as const,
1812
+ text: `Stored ${stored.length} memories (${counts.new} new, ${counts.deduped} deduped, ${counts.updated} updated)`,
1813
+ }],
1814
+ };
1815
+ },
1816
+ );
1817
+
1818
+ // --- MP-2: import_conversations tool ---
1819
+
1820
+ registerTool(
1821
+ "import_conversations",
1822
+ "Import a conversation file (Claude Code JSONL, Claude.ai JSON, ChatGPT JSON, Slack JSON, or plaintext) into memory. Auto-detects format or use explicit format parameter. Messages are normalized and stored via the standard persistMemory pipeline.",
1823
+ {
1824
+ content: z.string().min(1).max(500_000).describe("Raw file content to import"),
1825
+ scope: z.string().min(1).max(160).describe("Target scope for imported memories, e.g. 'project:myapp'"),
1826
+ format: z.enum(["auto", "claude-code", "claude-ai", "chatgpt", "slack", "plaintext"]).default("auto").describe("Conversation format. Use 'auto' to detect automatically."),
1827
+ },
1828
+ async ({ content, scope, format }) => {
1829
+ const { detectFormat, normalizeConversation, ingestNormalizedMessages } = await import("./conversation-importer.js");
1830
+ const { embedder } = getComponents();
1831
+
1832
+ const resolvedFormat = format === "auto" ? detectFormat(content) : format;
1833
+ const messages = normalizeConversation(content, resolvedFormat);
1834
+
1835
+ if (messages.length === 0) {
1836
+ return {
1837
+ content: [{
1838
+ type: "text" as const,
1839
+ text: `No messages found (detected format: ${resolvedFormat})`,
1840
+ }],
1841
+ };
1842
+ }
1843
+
1844
+ const result = await ingestNormalizedMessages(
1845
+ { store, embedder, llm, conflictStore, kgExtractor },
1846
+ messages,
1847
+ scope,
1848
+ );
1849
+
1850
+ return {
1851
+ content: [{
1852
+ type: "text" as const,
1853
+ text: [
1854
+ `Import complete (format: ${resolvedFormat})`,
1855
+ `Total: ${result.total}`,
1856
+ `Stored: ${result.stored}`,
1857
+ `Rejected: ${result.rejected}`,
1858
+ result.errors.length > 0 ? `Errors: ${result.errors.join("; ")}` : null,
1859
+ ].filter(Boolean).join("\n"),
1860
+ }],
1861
+ };
1862
+ },
1863
+ );
1864
+
1865
+ // --- S15: distill_session tool ---
1866
+ registerTool(
1867
+ "distill_session",
1868
+ "Distill a conversation session into structured knowledge and persist to long-term memory. Three layers: (1) microcompact clears old tool results at zero cost, (2) LLM summarizes into 9 dimensions, (3) extracts durable knowledge into RecallNest. Use when a session is ending or context is getting large. Side effect: persists extracted memories.",
1869
+ {
1870
+ messages: z.array(z.object({
1871
+ role: z.enum(["user", "assistant", "system"]),
1872
+ content: z.union([
1873
+ z.string(),
1874
+ z.array(z.object({
1875
+ type: z.enum(["text", "tool_use", "tool_result"]),
1876
+ name: z.string().optional(),
1877
+ id: z.string().optional(),
1878
+ input: z.record(z.unknown()).optional(),
1879
+ content: z.string().optional(),
1880
+ text: z.string().optional(),
1881
+ tool_use_id: z.string().optional(),
1882
+ })),
1883
+ ]),
1884
+ })).min(1).max(500).describe("Conversation messages to distill"),
1885
+ scope: z.string().min(1).max(160).describe("Memory scope for persisted knowledge, e.g. 'project:recallnest'"),
1886
+ preserveRecent: z.number().min(0).max(20).default(6).describe("Keep the N most recent messages verbatim (default: 6)"),
1887
+ keepRecentTools: z.number().min(0).max(20).default(5).describe("Keep the N most recent tool results during microcompact (default: 5)"),
1888
+ persist: z.boolean().default(true).describe("Whether to persist extracted knowledge to RecallNest (default: true)"),
1889
+ },
1890
+ async ({ messages, scope, preserveRecent, keepRecentTools, persist }) => {
1891
+ const { store, embedder } = getComponents();
1892
+ const { distillSession } = await import("./session-distiller.js");
1893
+
1894
+ const result = await distillSession(
1895
+ { messages, scope, preserveRecent, keepRecentTools, persist },
1896
+ {
1897
+ llm: llmClient,
1898
+ persistMemory: async (input) => {
1899
+ const stored = await persistMemory({
1900
+ store, embedder, conflictStore, kgExtractor,
1901
+ }, input);
1902
+ return { disposition: stored.disposition, id: stored.id };
1903
+ },
1904
+ },
1905
+ );
1906
+
1907
+ const lines = [
1908
+ `Microcompact: ${result.microcompact.toolsCleared} tool results cleared, ~${result.microcompact.tokensFreed} tokens freed`,
1909
+ ];
1910
+ if (result.summary) {
1911
+ lines.push(`Summary: 9-dimension structured summary generated`);
1912
+ }
1913
+ if (result.persisted) {
1914
+ const p = result.persisted;
1915
+ lines.push(`Persisted: ${p.memoriesStored} stored, ${p.memoriesDeduped} deduped, ${p.memoriesConflicted} conflicted, ${p.memoriesRejected} rejected`);
1916
+ }
1917
+
1918
+ return {
1919
+ content: [{
1920
+ type: "text" as const,
1921
+ text: lines.join("\n"),
1922
+ }],
1923
+ };
1924
+ },
1925
+ );
1926
+
1927
+ // ============================================================================
1928
+ // Global error handlers — prevent silent crashes from unhandled async errors
1929
+ // ============================================================================
1930
+
1931
+ process.on("unhandledRejection", (reason) => {
1932
+ console.error("[recallnest] Unhandled promise rejection:", reason instanceof Error ? reason.stack || reason.message : String(reason));
1933
+ });
1934
+
1935
+ process.on("uncaughtException", (err) => {
1936
+ console.error("[recallnest] Uncaught exception:", err.stack || err.message);
1937
+ // Give stderr a chance to flush before exiting
1938
+ setTimeout(() => process.exit(1), 100);
1939
+ });
1940
+
1332
1941
  // ============================================================================
1333
1942
  // Start
1334
1943
  // ============================================================================
1335
1944
 
1945
+ // Auto-register babel-memory language processor if installed (non-blocking)
1946
+ autoRegisterBabelMemory().then((ok) => {
1947
+ if (ok) console.error("[recallnest] babel-memory registered");
1948
+ }).catch((err) => {
1949
+ console.error("[recallnest] babel-memory registration failed:", err instanceof Error ? err.message : String(err));
1950
+ });
1951
+
1336
1952
  const transport = new StdioServerTransport();
1337
- await server.connect(transport);
1953
+ const CONNECT_TIMEOUT_MS = 30_000;
1954
+ try {
1955
+ await Promise.race([
1956
+ server.connect(transport),
1957
+ new Promise<never>((_, reject) =>
1958
+ setTimeout(() => reject(new Error("MCP server.connect() timed out after 30s")), CONNECT_TIMEOUT_MS)
1959
+ ),
1960
+ ]);
1961
+ } catch (err) {
1962
+ console.error("[recallnest] Fatal: MCP connection failed:", err instanceof Error ? err.message : String(err));
1963
+ process.exit(1);
1964
+ }