@crowley/rag-mcp 1.3.0 → 1.5.0

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.
@@ -19,7 +19,13 @@ export declare const DEFAULT_ENRICHABLE_TOOLS: Set<string>;
19
19
  export declare const DEFAULT_SKIP_TOOLS: Set<string>;
20
20
  export declare class ContextEnricher {
21
21
  private config;
22
+ private cache;
23
+ private static CACHE_TTL_MS;
22
24
  constructor(config?: Partial<EnrichmentConfig>);
25
+ /**
26
+ * Clear enrichment cache (call on session end).
27
+ */
28
+ clearCache(): void;
23
29
  /**
24
30
  * Before hook: auto-recall relevant memories/patterns/ADRs.
25
31
  * Returns a context prefix string or null if nothing relevant found.
@@ -45,6 +45,8 @@ export const DEFAULT_SKIP_TOOLS = new Set([
45
45
  ]);
46
46
  export class ContextEnricher {
47
47
  config;
48
+ cache = new Map();
49
+ static CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
48
50
  constructor(config = {}) {
49
51
  this.config = {
50
52
  enrichableTools: config.enrichableTools ?? DEFAULT_ENRICHABLE_TOOLS,
@@ -54,6 +56,12 @@ export class ContextEnricher {
54
56
  timeoutMs: config.timeoutMs ?? 2000,
55
57
  };
56
58
  }
59
+ /**
60
+ * Clear enrichment cache (call on session end).
61
+ */
62
+ clearCache() {
63
+ this.cache.clear();
64
+ }
57
65
  /**
58
66
  * Before hook: auto-recall relevant memories/patterns/ADRs.
59
67
  * Returns a context prefix string or null if nothing relevant found.
@@ -68,11 +76,29 @@ export class ContextEnricher {
68
76
  const query = this.extractQuery(args);
69
77
  if (!query)
70
78
  return null;
79
+ // Check per-session cache
80
+ const cacheKey = `${ctx.activeSessionId || 'no-session'}:${query.slice(0, 100)}`;
81
+ const cached = this.cache.get(cacheKey);
82
+ if (cached && Date.now() < cached.expiresAt) {
83
+ return cached.result;
84
+ }
71
85
  try {
72
86
  const memories = await this.recallWithTimeout(query, ctx);
73
- if (memories.length === 0)
74
- return null;
75
- return this.formatContext(memories);
87
+ const result = memories.length === 0 ? null : this.formatContext(memories);
88
+ // Store in cache
89
+ this.cache.set(cacheKey, {
90
+ result,
91
+ expiresAt: Date.now() + ContextEnricher.CACHE_TTL_MS,
92
+ });
93
+ // Evict expired entries lazily (every 50 calls)
94
+ if (this.cache.size > 100) {
95
+ const now = Date.now();
96
+ for (const [key, entry] of this.cache) {
97
+ if (now > entry.expiresAt)
98
+ this.cache.delete(key);
99
+ }
100
+ }
101
+ return result;
76
102
  }
77
103
  catch {
78
104
  // Enrichment should never break tool calls
@@ -150,8 +176,10 @@ export class ContextEnricher {
150
176
  const controller = new AbortController();
151
177
  const timeout = setTimeout(() => controller.abort(), this.config.timeoutMs);
152
178
  try {
153
- // Parallel recall: general memories + decisions/ADRs
154
- const [memoriesRes, decisionsRes] = await Promise.all([
179
+ // Parallel recall: general memories + decisions/ADRs + LTM (when enabled)
180
+ const graphRecallEnabled = process.env.GRAPH_RECALL_ENABLED === "true";
181
+ const consolidationEnabled = process.env.CONSOLIDATION_ENABLED === "true";
182
+ const recalls = [
155
183
  ctx.api
156
184
  .post("/api/memory/recall-durable", {
157
185
  projectName: ctx.projectName,
@@ -168,7 +196,19 @@ export class ContextEnricher {
168
196
  type: "decision",
169
197
  }, { signal: controller.signal })
170
198
  .catch(() => null),
171
- ]);
199
+ ];
200
+ // Phase 2+4: also recall from LTM (episodic+semantic with Ebbinghaus decay)
201
+ if (consolidationEnabled) {
202
+ recalls.push(ctx.api
203
+ .post("/api/memory/recall-ltm", {
204
+ projectName: ctx.projectName,
205
+ query,
206
+ limit: this.config.maxAutoRecall,
207
+ graphRecall: graphRecallEnabled,
208
+ }, { signal: controller.signal })
209
+ .catch(() => null));
210
+ }
211
+ const [memoriesRes, decisionsRes, ltmRes] = await Promise.all(recalls);
172
212
  const memories = [];
173
213
  const seenIds = new Set();
174
214
  // Process general memories
@@ -197,6 +237,23 @@ export class ContextEnricher {
197
237
  }
198
238
  }
199
239
  }
240
+ // Process LTM results (episodic + semantic)
241
+ if (ltmRes?.data?.results) {
242
+ for (const r of ltmRes.data.results) {
243
+ const mem = r.memory;
244
+ const id = mem?.id;
245
+ if (r.score >= this.config.minRelevance && id && !seenIds.has(id)) {
246
+ seenIds.add(id);
247
+ memories.push({
248
+ type: mem?.subtype || mem?.type || "insight",
249
+ content: mem?.content || "",
250
+ score: r.score,
251
+ });
252
+ }
253
+ }
254
+ }
255
+ // Sort by score and limit
256
+ memories.sort((a, b) => b.score - a.score);
200
257
  return memories.slice(0, this.config.maxAutoRecall + 2);
201
258
  }
202
259
  finally {
@@ -155,6 +155,43 @@ export function trackUsage(name, args, startTime, success, result, errorMessage,
155
155
  })
156
156
  .catch(() => { });
157
157
  }
158
+ // ── Sensory buffer ─────────────────────────────────────────
159
+ /** Extract file paths from tool args */
160
+ function extractFiles(args) {
161
+ const files = [];
162
+ for (const key of ['file', 'filePath', 'currentFile', 'path']) {
163
+ const v = args[key];
164
+ if (typeof v === 'string' && v.length > 0)
165
+ files.push(v);
166
+ }
167
+ const arr = args.affectedFiles || args.files;
168
+ if (Array.isArray(arr)) {
169
+ for (const f of arr) {
170
+ if (typeof f === 'string')
171
+ files.push(f);
172
+ }
173
+ }
174
+ return files.slice(0, 20);
175
+ }
176
+ /** Fire-and-forget: capture tool event in sensory buffer */
177
+ function appendToSensoryBuffer(name, args, startTime, success, resultText, ctx) {
178
+ if (!ctx.activeSessionId)
179
+ return;
180
+ if (TRACKING_EXCLUDE.has(name))
181
+ return;
182
+ ctx.api
183
+ .post("/api/sensory/append", {
184
+ projectName: ctx.projectName,
185
+ sessionId: ctx.activeSessionId,
186
+ toolName: name,
187
+ inputSummary: summarizeInput(name, args).slice(0, 500),
188
+ outputSummary: (resultText || "").slice(0, 500),
189
+ filesTouched: extractFiles(args),
190
+ success,
191
+ durationMs: Date.now() - startTime,
192
+ })
193
+ .catch(() => { });
194
+ }
158
195
  // ── Error formatting ────────────────────────────────────────
159
196
  /** Format an error caught during tool execution */
160
197
  export function formatToolError(error, ctx) {
@@ -207,6 +244,8 @@ export function wrapHandler(name, handler, deps) {
207
244
  }
208
245
  // Track usage (fire-and-forget)
209
246
  trackUsage(name, args, startTime, true, text, undefined, ctx);
247
+ // Capture in sensory buffer (fire-and-forget)
248
+ appendToSensoryBuffer(name, args, startTime, true, text, ctx);
210
249
  // Prepend context/warnings if available
211
250
  const prefix = [warningPrefix, contextPrefix].filter(Boolean).join('');
212
251
  if (prefix) {
@@ -221,6 +260,8 @@ export function wrapHandler(name, handler, deps) {
221
260
  const errorMessage = formatToolError(error, ctx);
222
261
  // Track failed usage (fire-and-forget)
223
262
  trackUsage(name, args, startTime, false, "", errorMessage, ctx);
263
+ // Capture failure in sensory buffer (fire-and-forget)
264
+ appendToSensoryBuffer(name, args, startTime, false, errorMessage, ctx);
224
265
  return errorMessage;
225
266
  }
226
267
  };
@@ -61,19 +61,22 @@ export function createMemoryTools(projectName) {
61
61
  description: "Retrieve relevant memories based on context. Searches agent memory for past decisions, insights, and notes related to the query.",
62
62
  schema: z.object({
63
63
  query: z.string().describe("What to recall (semantic search)"),
64
- type: z.enum(["decision", "insight", "context", "todo", "conversation", "note", "all"]).optional().describe("Filter by memory type (default: all)"),
64
+ type: z.enum(["decision", "insight", "context", "todo", "conversation", "note", "procedure", "all"]).optional().describe("Filter by memory type (default: all)"),
65
65
  limit: z.coerce.number().optional().describe("Max memories to retrieve (default: 5)"),
66
+ graphRecall: z.boolean().optional().describe("Enable graph-aware recall with spreading activation (default: false)"),
66
67
  }),
67
68
  annotations: TOOL_ANNOTATIONS["recall"],
68
69
  handler: async (args, ctx) => {
69
70
  const query = args.query;
70
71
  const type = args.type || "all";
71
72
  const limit = args.limit || 5;
73
+ const graphRecall = args.graphRecall || false;
72
74
  const response = await ctx.api.post("/api/memory/recall", {
73
75
  projectName: ctx.projectName,
74
76
  query,
75
77
  type,
76
78
  limit,
79
+ graphRecall,
77
80
  });
78
81
  const results = response.data.results || [];
79
82
  if (results.length === 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crowley/rag-mcp",
3
- "version": "1.3.0",
3
+ "version": "1.5.0",
4
4
  "description": "Universal RAG MCP Server for any project",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",