recallnest 2.2.1 → 2.5.4

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 (227) hide show
  1. package/CHANGELOG.md +150 -0
  2. package/README.md +46 -35
  3. package/README_CN.md +41 -26
  4. package/assets/ui/app.js +1031 -0
  5. package/assets/ui/index.html +224 -0
  6. package/assets/ui/styles.css +781 -0
  7. package/config.json.example +4 -1
  8. package/connectors/examples/email-adapter.ts +73 -0
  9. package/connectors/examples/logs-adapter.ts +103 -0
  10. package/connectors/examples/rss-adapter.ts +95 -0
  11. package/docs/api-reference.md +1291 -0
  12. package/docs/architecture.md +168 -0
  13. package/docs/connector-spec.md +126 -0
  14. package/eval/cases-canary.json +58 -0
  15. package/eval/cases-scope-robustness.json +278 -0
  16. package/eval/cases.json +117 -20
  17. package/package.json +22 -1
  18. package/src/__tests__/access-tracker-novelty.test.ts +238 -0
  19. package/src/__tests__/activity-counter.test.ts +125 -0
  20. package/src/__tests__/adaptive-fidelity.test.ts +102 -0
  21. package/src/__tests__/admission-control.test.ts +206 -0
  22. package/src/__tests__/anchor-generator.test.ts +67 -0
  23. package/src/__tests__/auto-consolidation.test.ts +168 -0
  24. package/src/__tests__/auto-recall.test.ts +131 -0
  25. package/src/__tests__/babel-integration.test.ts +144 -0
  26. package/src/__tests__/batch-dedup.test.ts +65 -0
  27. package/src/__tests__/canary-eval.test.ts +114 -0
  28. package/src/__tests__/capture-engine.test.ts +1238 -0
  29. package/src/__tests__/capture-heuristic.test.ts +171 -0
  30. package/src/__tests__/cascade-forget.test.ts +210 -0
  31. package/src/__tests__/category-min-scores.test.ts +33 -0
  32. package/src/__tests__/cc8-cc1-essential-context.test.ts +298 -0
  33. package/src/__tests__/circuit-breaker.test.ts +99 -0
  34. package/src/__tests__/cluster-consolidation.test.ts +782 -0
  35. package/src/__tests__/confidence-metadata.test.ts +125 -0
  36. package/src/__tests__/confidence-tracker.test.ts +261 -0
  37. package/src/__tests__/conflict-advisor.test.ts +206 -0
  38. package/src/__tests__/conflict-escalation.test.ts +149 -0
  39. package/src/__tests__/conflict-lifecycle.test.ts +83 -0
  40. package/src/__tests__/conflict-output.test.ts +85 -0
  41. package/src/__tests__/conflict-store.test.ts +374 -0
  42. package/src/__tests__/connector-v1.test.ts +181 -0
  43. package/src/__tests__/consolidation-engine.test.ts +333 -0
  44. package/src/__tests__/constructive-retrieval.test.ts +509 -0
  45. package/src/__tests__/context-collapse-renderer.test.ts +214 -0
  46. package/src/__tests__/context-composer-pins.test.ts +64 -0
  47. package/src/__tests__/context-composer-task-selection.test.ts +24 -0
  48. package/src/__tests__/context-reconstructor.test.ts +65 -0
  49. package/src/__tests__/context-renderer.test.ts +91 -0
  50. package/src/__tests__/continuity-eval.test.ts +275 -0
  51. package/src/__tests__/continuity-schema.test.ts +97 -0
  52. package/src/__tests__/contracts.test.ts +216 -0
  53. package/src/__tests__/conversation-importer.test.ts +692 -0
  54. package/src/__tests__/core-recall-eval.test.ts +235 -0
  55. package/src/__tests__/core-summary.test.ts +109 -0
  56. package/src/__tests__/data-checkup.test.ts +276 -0
  57. package/src/__tests__/decay-exemption.test.ts +84 -0
  58. package/src/__tests__/dedup-actions.test.ts +58 -0
  59. package/src/__tests__/dedup-guard.test.ts +39 -0
  60. package/src/__tests__/deterministic-id.test.ts +39 -0
  61. package/src/__tests__/distill-lock.test.ts +305 -0
  62. package/src/__tests__/distill-session-argshape.test.ts +79 -0
  63. package/src/__tests__/doctor.test.ts +319 -0
  64. package/src/__tests__/dream-pipeline.test.ts +321 -0
  65. package/src/__tests__/drill-down.test.ts +108 -0
  66. package/src/__tests__/embedder.test.ts +122 -0
  67. package/src/__tests__/emotion-decay.test.ts +46 -0
  68. package/src/__tests__/emotion-detector.test.ts +56 -0
  69. package/src/__tests__/emotion-phase1-integration.test.ts +153 -0
  70. package/src/__tests__/emotion-retrieval.test.ts +56 -0
  71. package/src/__tests__/entity-resolver.test.ts +58 -0
  72. package/src/__tests__/env-config.test.ts +151 -0
  73. package/src/__tests__/error-signature-boost.test.ts +87 -0
  74. package/src/__tests__/error-signature.test.ts +95 -0
  75. package/src/__tests__/error-taxonomy.test.ts +129 -0
  76. package/src/__tests__/ethics-layer.test.ts +476 -0
  77. package/src/__tests__/event-segmenter.test.ts +153 -0
  78. package/src/__tests__/feature-flags-integration.test.ts +54 -0
  79. package/src/__tests__/frequency-tracker.test.ts +113 -0
  80. package/src/__tests__/graph-export.test.ts +489 -0
  81. package/src/__tests__/hotness-blend.test.ts +76 -0
  82. package/src/__tests__/ingest-chunking.test.ts +44 -0
  83. package/src/__tests__/ingest.test.ts +360 -0
  84. package/src/__tests__/interference-detector.test.ts +203 -0
  85. package/src/__tests__/kg-extractor.test.ts +192 -0
  86. package/src/__tests__/kg-store.test.ts +300 -0
  87. package/src/__tests__/llm-consolidation.test.ts +226 -0
  88. package/src/__tests__/maintenance-locks.test.ts +174 -0
  89. package/src/__tests__/mcp-efficiency.test.ts +288 -0
  90. package/src/__tests__/mcp-related-scopes.test.ts +151 -0
  91. package/src/__tests__/memory-boundaries.test.ts +250 -0
  92. package/src/__tests__/memory-evolution-b2-b3-e1.test.ts +345 -0
  93. package/src/__tests__/memory-evolution.test.ts +271 -0
  94. package/src/__tests__/memory-health-rebalance.test.ts +144 -0
  95. package/src/__tests__/memory-lint.test.ts +421 -0
  96. package/src/__tests__/memory-output.test.ts +215 -0
  97. package/src/__tests__/memory-promotion.test.ts +315 -0
  98. package/src/__tests__/migrate-dedup-ids.test.ts +65 -0
  99. package/src/__tests__/multi-vector.test.ts +242 -0
  100. package/src/__tests__/narrative-architecture.test.ts +456 -0
  101. package/src/__tests__/noise-filter.test.ts +205 -0
  102. package/src/__tests__/obsidian-connector.test.ts +363 -0
  103. package/src/__tests__/omitted-reporting.test.ts +98 -0
  104. package/src/__tests__/pending-review.test.ts +77 -0
  105. package/src/__tests__/pii-redact.test.ts +314 -0
  106. package/src/__tests__/post-retrieval-filter.test.ts +178 -0
  107. package/src/__tests__/ppr-traversal.test.ts +256 -0
  108. package/src/__tests__/prediction-engine.test.ts +349 -0
  109. package/src/__tests__/predictive-prospective-memory.test.ts +416 -0
  110. package/src/__tests__/preference-guard-smoke.test.ts +211 -0
  111. package/src/__tests__/preference-matcher.test.ts +230 -0
  112. package/src/__tests__/preference-slots.test.ts +205 -0
  113. package/src/__tests__/prospective-memory.test.ts +283 -0
  114. package/src/__tests__/query-entity-detector.test.ts +171 -0
  115. package/src/__tests__/query-expander-alias.test.ts +146 -0
  116. package/src/__tests__/query-expander.test.ts +34 -0
  117. package/src/__tests__/recall-governor.test.ts +227 -0
  118. package/src/__tests__/recall-mode.test.ts +64 -0
  119. package/src/__tests__/result-synthesizer.test.ts +198 -0
  120. package/src/__tests__/resume-context.test.ts +4898 -0
  121. package/src/__tests__/retrieval-trace.test.ts +213 -0
  122. package/src/__tests__/retriever-boundary-weight.test.ts +136 -0
  123. package/src/__tests__/retroactive-boost.test.ts +223 -0
  124. package/src/__tests__/salience-scoring.test.ts +106 -0
  125. package/src/__tests__/scope-inventory.test.ts +200 -0
  126. package/src/__tests__/scope-policy.test.ts +69 -0
  127. package/src/__tests__/scope-suggester.test.ts +80 -0
  128. package/src/__tests__/session-checkpoint.test.ts +291 -0
  129. package/src/__tests__/session-distiller.test.ts +640 -0
  130. package/src/__tests__/session-gc.test.ts +215 -0
  131. package/src/__tests__/skill-engine.test.ts +552 -0
  132. package/src/__tests__/skill-promotion.test.ts +601 -0
  133. package/src/__tests__/skill-verifier.test.ts +122 -0
  134. package/src/__tests__/source-heartbeat.test.ts +250 -0
  135. package/src/__tests__/store-patch-metadata.test.ts +205 -0
  136. package/src/__tests__/store-scope-audit.test.ts +81 -0
  137. package/src/__tests__/store-upsert.test.ts +184 -0
  138. package/src/__tests__/supersede-chain.test.ts +154 -0
  139. package/src/__tests__/temporal-parser.test.ts +241 -0
  140. package/src/__tests__/temporal-validity.test.ts +160 -0
  141. package/src/__tests__/term-registry.test.ts +121 -0
  142. package/src/__tests__/tool-error-surfacing.test.ts +98 -0
  143. package/src/__tests__/tool-output-compressor.test.ts +160 -0
  144. package/src/__tests__/topic-tag.test.ts +113 -0
  145. package/src/__tests__/trusted-memory.test.ts +839 -0
  146. package/src/__tests__/usage-tracker.test.ts +287 -0
  147. package/src/__tests__/version-manager.test.ts +213 -0
  148. package/src/__tests__/workflow-observation-skill-outcome.test.ts +352 -0
  149. package/src/__tests__/workflow-observation.test.ts +433 -0
  150. package/src/__tests__/write-verifier.test.ts +122 -0
  151. package/src/access-tracker.ts +38 -32
  152. package/src/activity-counter.ts +46 -18
  153. package/src/aliases.ts +133 -0
  154. package/src/api-server.ts +27 -7
  155. package/src/audit-log.ts +4 -2
  156. package/src/auto-consolidation.ts +8 -3
  157. package/src/auto-gc.ts +85 -21
  158. package/src/capture-engine.ts +54 -21
  159. package/src/cli.ts +199 -37
  160. package/src/confidence-tracker.ts +36 -18
  161. package/src/connector-types.ts +57 -0
  162. package/src/consolidation-engine.ts +109 -11
  163. package/src/context-composer.ts +8 -1
  164. package/src/conversation-importer.ts +25 -1
  165. package/src/data-checkup.ts +126 -7
  166. package/src/distill-lock.ts +194 -15
  167. package/src/doctor.ts +38 -2
  168. package/src/dream-pipeline.ts +118 -34
  169. package/src/env-config.ts +77 -0
  170. package/src/error-signature.ts +153 -0
  171. package/src/error-taxonomy.ts +115 -0
  172. package/src/eval.ts +251 -6
  173. package/src/idempotency.ts +8 -0
  174. package/src/ingest.ts +234 -10
  175. package/src/kg-extractor.ts +6 -2
  176. package/src/kg-store.ts +236 -44
  177. package/src/llm-consolidation.ts +2 -1
  178. package/src/mcp-server.ts +85 -1799
  179. package/src/mcp-tool-deps.ts +48 -0
  180. package/src/mcp-tools-advanced.ts +941 -0
  181. package/src/mcp-tools-core.ts +633 -0
  182. package/src/mcp-tools-governance.ts +475 -0
  183. package/src/memory-boundaries.ts +3 -1
  184. package/src/memory-evolution.ts +73 -1
  185. package/src/memory-lint.ts +183 -10
  186. package/src/memory-output.ts +114 -3
  187. package/src/memory-promotion.ts +246 -0
  188. package/src/memory-schema.ts +25 -2
  189. package/src/memory-store-port.ts +43 -0
  190. package/src/multi-vector.ts +2 -1
  191. package/src/narrative-schema.ts +2 -1
  192. package/src/noise-filter.ts +57 -6
  193. package/src/obsidian-connector.ts +282 -0
  194. package/src/pii-detector.ts +157 -5
  195. package/src/query-expander.ts +90 -9
  196. package/src/result-synthesizer.ts +2 -1
  197. package/src/retention-policy.ts +2 -1
  198. package/src/retriever.ts +259 -139
  199. package/src/runtime-config.ts +8 -1
  200. package/src/scope-suggester.ts +113 -0
  201. package/src/session-distiller.ts +23 -2
  202. package/src/session-engine.ts +2 -1
  203. package/src/session-schema.ts +3 -2
  204. package/src/session-store.ts +5 -0
  205. package/src/skill-engine.ts +78 -0
  206. package/src/skill-promotion.ts +247 -47
  207. package/src/skill-schema.ts +16 -3
  208. package/src/skill-verifier.ts +152 -0
  209. package/src/source-heartbeat.ts +108 -0
  210. package/src/store.ts +267 -38
  211. package/src/tool-output-compressor.ts +3 -1
  212. package/src/ui-server.ts +2 -1
  213. package/src/usage-tracker.ts +242 -0
  214. package/src/version-manager.ts +3 -2
  215. package/src/workflow-observation-engine.ts +2 -1
  216. package/src/workflow-observation-schema.ts +2 -1
  217. package/src/workflow-observation-store.ts +6 -2
  218. package/.env +0 -4
  219. package/CLAUDE.md +0 -46
  220. package/config.json +0 -55
  221. package/daily-recall.sh +0 -110
  222. package/logs/daily-recall.error.log +0 -0
  223. package/logs/daily-recall.log +0 -30
  224. package/logs/ingest-2026-04-11.log +0 -110
  225. package/logs/ingest-2026-04-12.log +0 -90
  226. package/logs/launchagent-stderr.log +0 -0
  227. package/logs/launchagent-stdout.log +0 -0
@@ -0,0 +1,633 @@
1
+ import { z } from "zod";
2
+ import { persistMemory } from "./capture-engine.js";
3
+ import { composeLightResumeContext, composeResumeContext } from "./context-composer.js";
4
+ import { renderMemories } from "./context-renderer.js";
5
+ import { formatSearchResults, formatBriefResults, formatFullResults, formatCollapsedResults } from "./memory-output.js";
6
+ import { DurableMemoryCategorySchema, StoreMemorySourceSchema, PrivacyTierSchema, isPredictiveMemoryEnabled } from "./memory-schema.js";
7
+ import type { PredictionContext } from "./prediction-engine.js";
8
+ import { setReminder, checkTriggers, fireReminder, suggestPredictedReminders, formatSuggestedReminders } from "./prospective-memory.js";
9
+ import { resolveRecallMode } from "./runtime-config.js";
10
+ import { buildRetrievalContext, resolveScopeSelection } from "./scope-policy.js";
11
+ import { createScopeSuggester } from "./scope-suggester.js";
12
+ import { buildSessionCheckpointResult } from "./session-engine.js";
13
+ import { formatCheckpointSaved, formatCheckpointSummary, formatResumeContext } from "./session-output.js";
14
+ import { matchesTemporalConstraint, type TemporalConstraint } from "./temporal-parser.js";
15
+ import { buildManagedCheckpointObservation, buildManagedResumeObservation } from "./workflow-observation-managed.js";
16
+ import { buildWorkflowObservationRecord } from "./workflow-observation-engine.js";
17
+ import type { ToolRegistryDeps } from "./mcp-tool-deps.js";
18
+ import type { RetrievalResult } from "./retriever.js";
19
+
20
+ export function registerCoreTools(deps: ToolRegistryDeps): void {
21
+ const { registerTool, getComponents, config, checkpointStore, conflictStore, workflowObservationStore, getKGExtractor, getKGStore, auditLogger } = deps;
22
+ const TOOL_DESCRIPTIONS = deps.toolDescriptions;
23
+ const TOOL_TIERS = deps.toolTiers;
24
+ /** P1-A: 60s 缓存的 scope 建议器,首次 0-hit 搜索时惰性初始化。 */
25
+ let scopeSuggestFn: ((input: string) => Promise<string[]>) | null = null;
26
+ const MAX_RELATED_SCOPE_SIDECARS = 2;
27
+
28
+ function relatedScopesFor(scope?: string): string[] {
29
+ if (!scope) return [];
30
+ const configured = config.scopeRelations?.[scope];
31
+ if (!Array.isArray(configured)) return [];
32
+
33
+ const seen = new Set([scope]);
34
+ const related: string[] = [];
35
+ for (const candidate of configured) {
36
+ const normalized = typeof candidate === "string" ? candidate.replace(/\s+/g, " ").trim() : "";
37
+ if (!normalized || seen.has(normalized)) continue;
38
+ seen.add(normalized);
39
+ related.push(normalized);
40
+ if (related.length >= MAX_RELATED_SCOPE_SIDECARS) break;
41
+ }
42
+ return related;
43
+ }
44
+
45
+ function applyExplicitTemporalFilters(
46
+ results: RetrievalResult[],
47
+ params: { after?: string; before?: string; limit: number },
48
+ ): RetrievalResult[] {
49
+ const { after, before, limit } = params;
50
+ if (!after && !before) return results;
51
+
52
+ const constraint: TemporalConstraint = {
53
+ type: (after && before) ? "range" : (after ? "after" : "before"),
54
+ startMs: after ? new Date(after).getTime() || undefined : undefined,
55
+ endMs: before ? new Date(before).getTime() || undefined : undefined,
56
+ anchor: `${after || ""}..${before || ""}`,
57
+ };
58
+
59
+ if (!constraint.startMs && !constraint.endMs) return results;
60
+ return results
61
+ .filter(r => matchesTemporalConstraint(r.entry.timestamp, constraint))
62
+ .slice(0, limit);
63
+ }
64
+
65
+ async function saveManagedObservation(observation: Parameters<typeof buildWorkflowObservationRecord>[0]): Promise<void> {
66
+ try {
67
+ const record = buildWorkflowObservationRecord(observation);
68
+ await workflowObservationStore.save(record);
69
+ } catch (error) {
70
+ console.error("[RecallNest MCP] Failed to persist managed workflow observation:", error);
71
+ }
72
+ }
73
+
74
+ registerTool(
75
+ "store_memory",
76
+ "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.",
77
+ {
78
+ text: z.string().min(1).max(4000).describe("Memory text to store"),
79
+ category: DurableMemoryCategorySchema.default("events").describe("Durable memory category"),
80
+ importance: z.number().min(0).max(1).default(0.7).describe("Importance score from 0 to 1"),
81
+ scope: z.string().min(1).max(160).describe("Required scope such as project:recallnest or session:abc123"),
82
+ source: StoreMemorySourceSchema.default("manual").describe("How this memory was captured"),
83
+ tags: z.array(z.string().min(1).max(40)).max(8).default([]).describe("Optional tags"),
84
+ canonicalKey: z.string().min(1).max(120).optional().describe("Optional stable key for merge/update semantics"),
85
+ 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."),
86
+ privacyTier: PrivacyTierSchema.default("durable").describe("Privacy tier: ephemeral (auto-expire, no KG), private (persist, no KG), durable (default), shared (cross-scope)"),
87
+ validUntil: z.union([z.string(), z.number()]).optional().describe("Optional expiration: ISO date string or ms timestamp. Memory will be deprioritized after this time."),
88
+ 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."),
89
+ confidence: z.union([
90
+ z.number().min(0).max(1),
91
+ z.object({
92
+ score: z.number().min(0).max(1),
93
+ reliability: z.enum(["direct", "inferred", "hearsay"]).optional(),
94
+ }),
95
+ ]).optional().describe("Optional confidence override: number (0-1) or {score, reliability}. Auto-assigned from source if omitted."),
96
+ },
97
+ async ({ text, category, importance, scope, source, tags, canonicalKey, topicTag, privacyTier, validUntil, eventTime, confidence }) => {
98
+ const { store, embedder } = getComponents();
99
+ const kgExtractor = getKGExtractor();
100
+ const stored = await persistMemory({
101
+ store,
102
+ embedder,
103
+ conflictStore,
104
+ kgExtractor,
105
+ auditLogger,
106
+ }, {
107
+ text,
108
+ category,
109
+ importance,
110
+ scope,
111
+ source,
112
+ tags,
113
+ canonicalKey,
114
+ topicTag,
115
+ privacyTier,
116
+ // F3: Pass temporal validity params (extracted by persistMemory before Zod parse)
117
+ validUntil,
118
+ eventTime,
119
+ // F1: Pass confidence override (extracted by persistMemory before Zod parse)
120
+ confidence,
121
+ });
122
+
123
+ return {
124
+ content: [{
125
+ type: "text" as const,
126
+ text: [
127
+ `Stored memory ${stored.id.slice(0, 8)}`,
128
+ `Disposition: ${stored.disposition}`,
129
+ ...(stored.rejectionReason ? [`Rejection reason: ${stored.rejectionReason}`] : []),
130
+ `Category: ${stored.category}`,
131
+ `Scope: ${stored.resolvedScope}`,
132
+ `Canonical key: ${stored.canonicalKey}`,
133
+ ...(stored.conflictId ? [`Conflict: ${stored.conflictId.slice(0, 8)}`] : []),
134
+ `Stored at: ${stored.storedAt}`,
135
+ ].join("\n"),
136
+ }],
137
+ };
138
+ }
139
+ );
140
+
141
+ registerTool(
142
+ "set_reminder",
143
+ "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.",
144
+ {
145
+ trigger: z.string().min(1).max(200).describe("Trigger condition — keywords that should activate this reminder"),
146
+ action: z.string().min(1).max(500).describe("What to remind about when the trigger fires"),
147
+ scope: z.string().min(1).max(160).describe("Required scope"),
148
+ expiresInDays: z.number().min(1).max(365).optional().describe("Optional: auto-expire after N days"),
149
+ },
150
+ async ({ trigger, action, scope, expiresInDays }) => {
151
+ const { store, embedder } = getComponents();
152
+ const entry = await setReminder(store, embedder, { trigger, action, scope, expiresInDays });
153
+
154
+ return {
155
+ content: [{
156
+ type: "text" as const,
157
+ text: [
158
+ `Reminder set: ${entry.id.slice(0, 8)}`,
159
+ `Trigger: "${trigger}"`,
160
+ `Action: ${action}`,
161
+ `Scope: ${scope}`,
162
+ ...(expiresInDays ? [`Expires in: ${expiresInDays} days`] : []),
163
+ ].join("\n"),
164
+ }],
165
+ };
166
+ }
167
+ );
168
+
169
+ registerTool(
170
+ "checkpoint_session",
171
+ "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.",
172
+ {
173
+ sessionId: z.string().min(1).max(160).describe("Current session identifier"),
174
+ scope: z.string().min(1).max(160).optional().describe("Optional shared scope; defaults to session:<sessionId>"),
175
+ summary: z.string().min(1).max(600).describe("Compact summary of the current work state"),
176
+ task: z.string().min(1).max(240).optional().describe("Optional task label"),
177
+ decisions: z.array(z.string().min(1).max(200)).max(6).default([]).describe("Key decisions already made"),
178
+ openLoops: z.array(z.string().min(1).max(200)).max(6).default([]).describe("Unresolved questions or pending items"),
179
+ nextActions: z.array(z.string().min(1).max(200)).max(6).default([]).describe("Next actions to take"),
180
+ entities: z.array(z.string().min(1).max(120)).max(8).default([]).describe("Relevant projects, tools, or people"),
181
+ files: z.array(z.string().min(1).max(220)).max(12).default([]).describe("Relevant files or paths"),
182
+ idempotencyKey: z.string().min(1).max(160).optional().describe("Optional stable request key; repeated saves with the same key replace the prior checkpoint"),
183
+ updatedAt: z.string().datetime().optional().describe("Optional override; defaults to now"),
184
+ },
185
+ async ({ sessionId, scope, summary, task, decisions, openLoops, nextActions, entities, files, idempotencyKey, updatedAt }) => {
186
+ const result = buildSessionCheckpointResult({
187
+ sessionId,
188
+ scope,
189
+ summary,
190
+ task,
191
+ decisions,
192
+ openLoops,
193
+ nextActions,
194
+ entities,
195
+ files,
196
+ idempotencyKey,
197
+ ...(updatedAt ? { updatedAt } : {}),
198
+ });
199
+ const storedRecord = await checkpointStore.save(result.record);
200
+ await saveManagedObservation(buildManagedCheckpointObservation({
201
+ ...result,
202
+ record: storedRecord,
203
+ }));
204
+ return {
205
+ content: [{
206
+ type: "text" as const,
207
+ text: formatCheckpointSaved(storedRecord),
208
+ }],
209
+ };
210
+ }
211
+ );
212
+
213
+ registerTool(
214
+ "latest_checkpoint",
215
+ "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.",
216
+ {
217
+ sessionId: z.string().min(1).max(160).optional().describe("Session identifier filter, e.g. 'abc123'"),
218
+ scope: z.string().min(1).max(160).optional().describe("Shared scope filter, e.g. 'project:recallnest'"),
219
+ },
220
+ async ({ sessionId, scope }) => {
221
+ const latest = await checkpointStore.getLatest({ sessionId, scope });
222
+ return {
223
+ content: [{
224
+ type: "text" as const,
225
+ text: formatCheckpointSummary(latest),
226
+ }],
227
+ };
228
+ }
229
+ );
230
+
231
+ registerTool(
232
+ "resume_context",
233
+ "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.",
234
+ {
235
+ task: z.string().min(1).max(500).optional().describe("Optional current task or question to bias recall"),
236
+ scope: z.string().min(1).max(160).optional().describe("Optional shared scope for project or terminal continuity"),
237
+ sessionId: z.string().min(1).max(160).optional().describe("Optional session identifier to recover the latest checkpoint"),
238
+ limitPerSection: z.number().int().min(1).max(6).default(3).describe("Max items per section"),
239
+ includeLatestCheckpoint: z.boolean().default(true).describe("Whether to include the latest checkpoint summary"),
240
+ profile: z.enum(["default", "writing", "debug", "fact-check"]).optional().describe("Retrieval profile"),
241
+ mode: z.enum(["full", "light", "summary", "off"]).optional().describe("Override recall mode: 'full' (default), 'light' (<300 tokens), 'summary' (checkpoint only), 'off'"),
242
+ },
243
+ async ({ task, scope, sessionId, limitPerSection, includeLatestCheckpoint, profile: profileName, mode: modeOverride }) => {
244
+ const effectiveMode = resolveRecallMode(config, modeOverride);
245
+
246
+ // --- off mode: no recall, guide agent to use search_memory ---
247
+ if (effectiveMode === "off") {
248
+ return {
249
+ content: [{
250
+ type: "text" as const,
251
+ text: "Recall mode is off. Use search_memory to retrieve specific memories on demand.",
252
+ }],
253
+ };
254
+ }
255
+
256
+ // --- summary mode: checkpoint only, lightweight ---
257
+ if (effectiveMode === "summary") {
258
+ const scopeSelection = resolveScopeSelection({
259
+ scope,
260
+ sessionId,
261
+ operation: "resume_context",
262
+ allowUnscoped: true,
263
+ });
264
+ const latest = await checkpointStore.getLatest({
265
+ sessionId,
266
+ scope: scopeSelection.resolvedScope,
267
+ });
268
+ const summaryText = formatCheckpointSummary(latest) +
269
+ "\n\nFor detailed recall, use search_memory with specific queries.";
270
+ await saveManagedObservation({
271
+ workflowId: "resume_context",
272
+ outcome: "success",
273
+ summary: `Managed resume_context returned summary-mode checkpoint${latest ? "" : " (none found)"}.`,
274
+ scope: scopeSelection.resolvedScope || scope || "global",
275
+ source: "managed:recallnest",
276
+ signal: "managed-resume-summary",
277
+ task,
278
+ tags: ["managed", "recallnest", "summary-mode"],
279
+ });
280
+ return {
281
+ content: [{
282
+ type: "text" as const,
283
+ text: summaryText,
284
+ }],
285
+ };
286
+ }
287
+
288
+ // --- light mode: <300 token ultra-light wake-up ---
289
+ if (effectiveMode === "light") {
290
+ const { retriever: lightRetriever } = getComponents(profileName);
291
+ const lightScope = resolveScopeSelection({
292
+ scope,
293
+ sessionId,
294
+ operation: "resume_context",
295
+ allowUnscoped: true,
296
+ });
297
+ const lightResult = await composeLightResumeContext({
298
+ retriever: lightRetriever,
299
+ checkpointStore,
300
+ }, {
301
+ task,
302
+ scope: lightScope.resolvedScope,
303
+ sessionId,
304
+ limitPerSection: limitPerSection,
305
+ includeLatestCheckpoint,
306
+ profile: profileName,
307
+ });
308
+ await saveManagedObservation({
309
+ workflowId: "resume_context",
310
+ outcome: "success",
311
+ summary: `Managed resume_context returned light-mode context (~${lightResult.text.length} chars).`,
312
+ scope: lightScope.resolvedScope || scope || "global",
313
+ source: "managed:recallnest",
314
+ signal: "managed-resume-light",
315
+ task,
316
+ tags: ["managed", "recallnest", "light-mode"],
317
+ });
318
+ return {
319
+ content: [{
320
+ type: "text" as const,
321
+ text: lightResult.text,
322
+ }],
323
+ };
324
+ }
325
+
326
+ // --- full mode: existing compose behavior ---
327
+ const { retriever, profile } = getComponents(profileName);
328
+ const scopeSelection = resolveScopeSelection({
329
+ scope,
330
+ sessionId,
331
+ operation: "resume_context",
332
+ allowUnscoped: true,
333
+ });
334
+ const context = await composeResumeContext({
335
+ retriever,
336
+ checkpointStore,
337
+ }, {
338
+ task,
339
+ scope: scopeSelection.resolvedScope,
340
+ sessionId,
341
+ limitPerSection,
342
+ includeLatestCheckpoint,
343
+ profile: profile.name,
344
+ });
345
+ await saveManagedObservation(buildManagedResumeObservation({
346
+ task,
347
+ scope,
348
+ sessionId,
349
+ }, context));
350
+
351
+ return {
352
+ content: [{
353
+ type: "text" as const,
354
+ text: formatResumeContext(context),
355
+ }],
356
+ };
357
+ }
358
+ );
359
+
360
+ registerTool(
361
+ "search_memory",
362
+ "Search indexed memories by hybrid relevance (vector + BM25 + reranking) and return ranked results with optional temporal filtering. The shown score is a fused ranking score (0-100%), NOT pure cosine similarity — read it as relative ranking within this result set, not as match confidence. 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.",
363
+ {
364
+ query: z.string().describe("Search query — natural language or keywords"),
365
+ limit: z.number().min(1).max(20).default(5).describe("Max results to return"),
366
+ scope: z.string().optional().describe("Optional explicit scope"),
367
+ sessionId: z.string().min(1).max(160).optional().describe("Optional session identifier to infer session:<id> scope"),
368
+ allScopes: z.boolean().default(false).describe("When true, explicitly allow cross-scope search"),
369
+ includeRelatedScopes: z.boolean().default(false).describe("When true, also query configured related scopes from scopeRelations and show them in a separate sidecar section. Requires an explicit or inferred scoped search; never changes the main result ranking."),
370
+ 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)"),
371
+ profile: z.enum(["default", "writing", "debug", "fact-check"]).optional().describe("Retrieval profile"),
372
+ render: z.enum(["verbatim", "highlight"]).default("verbatim").optional().describe("Result rendering mode: verbatim (default, original order) or highlight (reorder by contextual relevance to query)"),
373
+ after: z.string().optional().describe("Filter memories stored after this date (ISO format YYYY-MM-DD, or relative like '最近30天', 'last 7 days')"),
374
+ before: z.string().optional().describe("Filter memories stored before this date (ISO format YYYY-MM-DD, or relative)"),
375
+ 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的朋友')."),
376
+ includeArchived: z.boolean().default(false).optional().describe("When true, also return archived/superseded/consolidated memories (default: only active)"),
377
+ detail_level: z.enum(["brief", "normal", "full", "adaptive"]).default("normal").optional()
378
+ .describe("Result detail level: brief (ID+score+one-liner), normal (default, table), full (include metadata), adaptive (per-result L0/L1/L2 fidelity by relevance within an 8k token budget — high-relevance gets full text, lower gets summary/one-line)"),
379
+ topicTag: z.string().min(1).max(60).optional()
380
+ .describe("Filter by topic tag (e.g. 'auth', 'deploy', 'testing'). Only returns memories tagged with this topic."),
381
+ reconstruct: z.boolean().default(false).describe(
382
+ "Return LLM-synthesized reconstruction alongside raw results. Requires RECALLNEST_CONSTRUCTIVE_RETRIEVAL=true."
383
+ ),
384
+ 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."),
385
+ includeExpired: z.boolean().default(false).optional().describe("When true, include expired memories in results (demoted 80%). Default: only active/non-expired."),
386
+ },
387
+ async ({ query, limit, scope, sessionId, allScopes, includeRelatedScopes, category, profile: profileName, render, after, before, graph, includeArchived, detail_level, topicTag, reconstruct, validAt, includeExpired }) => {
388
+ const { retriever, profile } = getComponents(profileName);
389
+ const { llm } = getComponents();
390
+ const kgStoreInstance = getKGStore();
391
+ // Ensure KG store is attached to non-default profile retrievers for PPR
392
+ if (graph && kgStoreInstance) retriever.setKGStore(kgStoreInstance);
393
+ // Attach LLM client for constructive retrieval if available
394
+ if (reconstruct && llm) retriever.setLLMClient(llm);
395
+ let results = await retriever.retrieve(buildRetrievalContext({
396
+ query,
397
+ limit: (after || before || topicTag) ? limit * 3 : limit,
398
+ category,
399
+ scope,
400
+ sessionId,
401
+ allScopes,
402
+ graph,
403
+ includeArchived,
404
+ topicTag,
405
+ reconstruct,
406
+ // F3: Temporal validity filtering
407
+ validAt: validAt ? new Date(validAt).getTime() : undefined,
408
+ includeExpired: includeExpired ?? undefined,
409
+ }, {
410
+ operation: "search_memory",
411
+ }));
412
+
413
+ // Scope 0-hit fallback: 用户给的 scope 太严返回 0 hit 时,自动 allScopes=true 重试一次。
414
+ // 仅在 !allScopes 时触发,避免重复跨 scope 搜。
415
+ // P1-A: fallback 不再静默——输出段会披露"结果来自跨 scope 重试"并附相近 scope 建议,
416
+ // 否则拼错 scope 的调用方拿到跨 scope 结果却以为 scope 过滤生效了。
417
+ let scopeFallbackUsed = false;
418
+ if (results.length === 0 && !allScopes) {
419
+ scopeFallbackUsed = true;
420
+ results = await retriever.retrieve(buildRetrievalContext({
421
+ query,
422
+ limit: (after || before || topicTag) ? limit * 3 : limit,
423
+ category,
424
+ scope: undefined,
425
+ sessionId: undefined,
426
+ allScopes: true,
427
+ graph,
428
+ includeArchived,
429
+ topicTag,
430
+ reconstruct,
431
+ validAt: validAt ? new Date(validAt).getTime() : undefined,
432
+ includeExpired: includeExpired ?? undefined,
433
+ }, {
434
+ operation: "search_memory",
435
+ }));
436
+ }
437
+
438
+ // P-omitted: 在 after/before filter 创建新数组、丢失 ResultSet.omitted 之前快照。
439
+ const omittedInfo = (results as import("./retriever.js").RetrievalResultSet).omitted;
440
+
441
+ results = applyExplicitTemporalFilters(results, { after, before, limit });
442
+
443
+ // Apply context-aware rendering when requested
444
+ if (render === "highlight" && results.length > 0) {
445
+ const rendered = renderMemories(
446
+ results.map(r => ({ id: r.entry.id, text: r.entry.text, score: r.score, category: r.entry.category })),
447
+ query,
448
+ "highlight",
449
+ );
450
+ // Reorder results to match rendered order
451
+ const idOrder = new Map(rendered.memories.map((m, i) => [m.id, i]));
452
+ results.sort((a, b) => (idOrder.get(a.entry.id) ?? 999) - (idOrder.get(b.entry.id) ?? 999));
453
+ }
454
+
455
+ // Tier 3.4: Check for triggered reminders alongside search results
456
+ const { store, embedder } = getComponents();
457
+ const scopeFilter = scope ? [scope] : undefined;
458
+ const triggered = await checkTriggers(store, embedder, query, scopeFilter);
459
+ let reminderText = "";
460
+ if (triggered.length > 0) {
461
+ const firedActions: string[] = [];
462
+ for (const reminder of triggered) {
463
+ const action = await fireReminder(store, reminder.entryId, scopeFilter);
464
+ if (action) firedActions.push(action);
465
+ }
466
+ if (firedActions.length > 0) {
467
+ reminderText = "\n\n--- Triggered Reminders ---\n" +
468
+ firedActions.map(a => `- ${a}`).join("\n");
469
+ }
470
+ }
471
+
472
+ // HP-predictive: Surface predicted reminders alongside search results
473
+ let suggestedText = "";
474
+ if (isPredictiveMemoryEnabled()) {
475
+ try {
476
+ const recentCheckpoints = await checkpointStore.listRecent({ scope, limit: 5 });
477
+ const recentObservations = await workflowObservationStore.listRecent({ scope, limit: 20 });
478
+ const predictionCtx: PredictionContext = {
479
+ checkpoints: recentCheckpoints,
480
+ workflowObservations: recentObservations,
481
+ frequentMemories: [], // Populated by access tracker in future iteration
482
+ uncoveredTopics: results.length === 0 && query ? [query] : [],
483
+ };
484
+ const suggestions = await suggestPredictedReminders(store, embedder, predictionCtx, scope ?? "global");
485
+ suggestedText = formatSuggestedReminders(suggestions);
486
+ } catch {
487
+ // Prediction failure is non-critical — silently skip
488
+ }
489
+ }
490
+
491
+ const level = detail_level ?? "normal";
492
+ const formatAtLevel = (items: RetrievalResult[]) => {
493
+ if (level === "brief") return formatBriefResults(items, { query });
494
+ if (level === "full") return formatFullResults(items, { query, profile: profile.name });
495
+ // P-fidelity (点4): 按相关性分配 L0/L1/L2 保真度 + token 预算(复用 collapseResults)。
496
+ if (level === "adaptive") return formatCollapsedResults(items, { query, profile: profile.name });
497
+ return formatSearchResults(items, { query, profile: profile.name });
498
+ };
499
+
500
+ const relatedScopeSections: string[] = [];
501
+ if (includeRelatedScopes && !allScopes && !scopeFallbackUsed) {
502
+ const selection = resolveScopeSelection({
503
+ scope,
504
+ sessionId,
505
+ allScopes,
506
+ operation: "search_memory",
507
+ });
508
+ const sidecarLimit = Math.min(3, limit);
509
+
510
+ for (const relatedScope of relatedScopesFor(selection.resolvedScope)) {
511
+ let relatedResults = await retriever.retrieve(buildRetrievalContext({
512
+ query,
513
+ limit: (after || before || topicTag) ? sidecarLimit * 3 : sidecarLimit,
514
+ category,
515
+ scope: relatedScope,
516
+ graph,
517
+ includeArchived,
518
+ topicTag,
519
+ source: "auto-recall",
520
+ validAt: validAt ? new Date(validAt).getTime() : undefined,
521
+ includeExpired: includeExpired ?? undefined,
522
+ }, {
523
+ operation: "search_memory:related_scope",
524
+ }));
525
+
526
+ relatedResults = applyExplicitTemporalFilters(relatedResults, { after, before, limit: sidecarLimit });
527
+ if (render === "highlight" && relatedResults.length > 0) {
528
+ const rendered = renderMemories(
529
+ relatedResults.map(r => ({ id: r.entry.id, text: r.entry.text, score: r.score, category: r.entry.category })),
530
+ query,
531
+ "highlight",
532
+ );
533
+ const idOrder = new Map(rendered.memories.map((m, i) => [m.id, i]));
534
+ relatedResults.sort((a, b) => (idOrder.get(a.entry.id) ?? 999) - (idOrder.get(b.entry.id) ?? 999));
535
+ }
536
+
537
+ if (relatedResults.length > 0) {
538
+ relatedScopeSections.push(`### ${relatedScope}\n${formatAtLevel(relatedResults)}`);
539
+ }
540
+ }
541
+ }
542
+
543
+ const sections: string[] = [];
544
+
545
+ // Phase 4: Read reconstruction from first-class field (no metadata hack)
546
+ const reconstruction = (results as import("./retriever.js").RetrievalResultSet).reconstruction;
547
+ if (reconstruction?.reconstructed) {
548
+ const sourceIds = reconstruction.sources.map(s => s.id).join(", ");
549
+ const sourceTypes = [...new Set(reconstruction.sources.map(s => s.source.type))].join(", ");
550
+ sections.push(
551
+ `## Reconstructed Context (confidence: ${reconstruction.confidence.toFixed(2)}, coverage: ${reconstruction.coverage.toFixed(2)})\n${reconstruction.reconstructed}\n\nSources (${sourceTypes}): ${sourceIds}`
552
+ );
553
+ // Render contradictions if detected
554
+ if (reconstruction.contradictions.length > 0) {
555
+ const conflictLines = reconstruction.contradictions.map(c =>
556
+ `- \u26a0\ufe0f ${c.description} [${c.memoryIds.join(" vs ")}]`
557
+ );
558
+ sections.push(`### Contradictions Detected\n${conflictLines.join("\n")}`);
559
+ }
560
+ }
561
+
562
+ sections.push(formatAtLevel(results));
563
+
564
+ if (relatedScopeSections.length > 0) {
565
+ sections.push(`## Related scope results\n${relatedScopeSections.join("\n\n")}`);
566
+ }
567
+
568
+ // P-omitted: 因 limit 上限未返回的达标结果,明确告知调用方可提高 limit 看到更多。
569
+ // 仅在普通检索下报告;after/before/topicTag 模式内部用 limit*3 拉取,omitted 语义不一致,跳过避免误导。
570
+ // "up to" 措辞:omitted 是上界估计,可能含将被 validity filter 过滤的 expired 项(P3-2)。
571
+ if (!after && !before && !topicTag && omittedInfo && omittedInfo.count > 0) {
572
+ sections.push(
573
+ `ℹ️ Up to ${omittedInfo.count} more matching result(s) were capped by limit=${limit}. ` +
574
+ `Raise \`limit\` (max 20) or refine the query to surface them.`
575
+ );
576
+ }
577
+
578
+ // P1-A: 显式 scope 0 命中 → 相近 scope 提示(拼写漂移最常见;只提示不改写)
579
+ if (scope && (scopeFallbackUsed || results.length === 0)) {
580
+ try {
581
+ scopeSuggestFn ??= createScopeSuggester(async () => (await getComponents().store.stats()).scopeCounts);
582
+ const suggestions = (await scopeSuggestFn(scope)).filter(s => s !== scope);
583
+ const notes: string[] = [];
584
+ if (scopeFallbackUsed && results.length > 0) {
585
+ notes.push(`⚠️ scope '${scope}' 命中 0 条,以上结果来自自动跨 scope 重试(allScopes)。`);
586
+ }
587
+ if (suggestions.length > 0) {
588
+ notes.push(`相近 scope: [${suggestions.join(", ")}]`);
589
+ }
590
+ if (notes.length > 0) sections.push(notes.join("\n"));
591
+ } catch { /* suggestion is best-effort, never blocks search output */ }
592
+ }
593
+
594
+ return {
595
+ content: [{
596
+ type: "text" as const,
597
+ text: sections.join("\n\n") + reminderText + suggestedText,
598
+ }],
599
+ };
600
+ }
601
+ );
602
+
603
+ registerTool(
604
+ "list_tools",
605
+ "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.",
606
+ {
607
+ tier: z.enum(["core", "advanced", "full"]).default("advanced").optional()
608
+ .describe("Which tier of tools to list. Returns tools at this tier and below."),
609
+ },
610
+ async ({ tier }) => {
611
+ const requestedTier = tier ?? "advanced";
612
+ const tierOrder: Record<string, number> = { core: 0, advanced: 1, governance: 2 };
613
+ const maxOrder = requestedTier === "full" ? 2 : tierOrder[requestedTier] ?? 1;
614
+
615
+ const lines: string[] = [`Available tools (tier: ${requestedTier}):`];
616
+ for (const [toolName, toolTier] of Object.entries(TOOL_TIERS)) {
617
+ if ((tierOrder[toolTier] ?? 999) > maxOrder) continue;
618
+ const desc = TOOL_DESCRIPTIONS.get(toolName);
619
+ const oneLiner = desc
620
+ ? desc.split(/[.!]\s/)[0]?.slice(0, 100) ?? desc.slice(0, 100)
621
+ : "(no description)";
622
+ lines.push(`- ${toolName}: ${oneLiner}`);
623
+ }
624
+
625
+ return {
626
+ content: [{
627
+ type: "text" as const,
628
+ text: lines.join("\n"),
629
+ }],
630
+ };
631
+ },
632
+ );
633
+ }