@exaudeus/memory-mcp 1.1.0 → 1.2.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.
- package/dist/ephemeral.js +105 -3
- package/dist/index.js +91 -54
- package/dist/normalize.js +0 -5
- package/package.json +1 -1
package/dist/ephemeral.js
CHANGED
|
@@ -228,11 +228,66 @@ const SIGNALS = [
|
|
|
228
228
|
/\b(doesn'?t|don'?t) (handle|support|implement).*\byet\b/,
|
|
229
229
|
/\bis (underway|not finished|not complete)\b/,
|
|
230
230
|
/\bblocked on\b/,
|
|
231
|
+
// Work-remaining section markers (colon required to avoid matching prose)
|
|
232
|
+
/\bremaining\s*:/, /\bopen items?\s*:/, /\bstill to do\s*:/,
|
|
233
|
+
/\bpending (tasks?|items?|work)\s*:/,
|
|
231
234
|
];
|
|
232
235
|
const m = firstMatch(content, patterns);
|
|
233
236
|
return m ? `contains "${m[0]}" — task tracking doesn't belong in long-term memory` : undefined;
|
|
234
237
|
},
|
|
235
238
|
},
|
|
239
|
+
// ── Completed task / work summary ──────────────────────────────────────
|
|
240
|
+
// Catches session work summaries stored as permanent knowledge.
|
|
241
|
+
// "Documentation updates complete", "Completed the migration", "All X validated".
|
|
242
|
+
{
|
|
243
|
+
id: 'completed-task',
|
|
244
|
+
label: 'Completed task announcement',
|
|
245
|
+
confidence: 'high',
|
|
246
|
+
test: (title, content) => {
|
|
247
|
+
const titlePatterns = [
|
|
248
|
+
// "Documentation updates complete", "migration done", "task finished"
|
|
249
|
+
/\b(update|migration|refactor|implementation|task|feature|docs?|documentation|sync|work|changes?|setup)\s+(complete[d]?|done|finished?)\b/,
|
|
250
|
+
// "Complete: X" or "Done - X" as a status prefix in the title
|
|
251
|
+
/^(complete[d]?|done|finished?)\s*[-–—:]/,
|
|
252
|
+
];
|
|
253
|
+
const contentPatterns = [
|
|
254
|
+
// Content that opens with a completion verb — strongest signal
|
|
255
|
+
/^completed?\s+\w/,
|
|
256
|
+
// "Successfully completed/deployed/merged/migrated..."
|
|
257
|
+
/\bsuccessfully\s+(completed?|deployed?|merged?|migrated?|updated?|implemented?|refactored?)\b/,
|
|
258
|
+
// "All cross-references validated", "All tests done"
|
|
259
|
+
// [\w-] (no \s) restricts to single-word or hyphenated nouns, preventing
|
|
260
|
+
// "Every code fix is validated" (a durable policy) from matching
|
|
261
|
+
/\b(all|every)\s+\S[\w-]{0,25}\s+(validated?|complete[d]?|verified?|done)\b/,
|
|
262
|
+
// "has been completed/finished/merged/deployed"
|
|
263
|
+
/\b(has|have)\s+been\s+(completed?|finished?|merged?|deployed?)\b/,
|
|
264
|
+
];
|
|
265
|
+
const tm = firstMatch(title, titlePatterns);
|
|
266
|
+
if (tm)
|
|
267
|
+
return `title contains "${tm[0]}" — task completion announcements aren't durable knowledge`;
|
|
268
|
+
const cm = firstMatch(content, contentPatterns);
|
|
269
|
+
return cm ? `contains "${cm[0]}" — task completion announcements aren't durable knowledge` : undefined;
|
|
270
|
+
},
|
|
271
|
+
},
|
|
272
|
+
// ── Diff stats / work metrics ──────────────────────────────────────────
|
|
273
|
+
// Quantitative work summaries ("14 docs modified", "508 additions, 173 deletions")
|
|
274
|
+
// never appear in durable knowledge — they're always session activity reports.
|
|
275
|
+
{
|
|
276
|
+
id: 'diff-stats',
|
|
277
|
+
label: 'Diff stats or work metrics',
|
|
278
|
+
confidence: 'high',
|
|
279
|
+
test: (_title, _content, raw) => {
|
|
280
|
+
const content = raw.content.toLowerCase();
|
|
281
|
+
const patterns = [
|
|
282
|
+
// Git-style: "(508 additions, 173 deletions)" or "508 additions, 173 deletions"
|
|
283
|
+
/\b\d+\s+additions?,\s*\d+\s+deletions?\b/,
|
|
284
|
+
// Work quantity: "14 docs modified", "5 files changed"
|
|
285
|
+
/\b\d+\s+(docs?|files?|tests?|endpoints?|functions?|modules?|classes?|pages?)\s+(modified|changed|updated|added|created|deleted)\b/,
|
|
286
|
+
];
|
|
287
|
+
const m = firstMatch(content, patterns);
|
|
288
|
+
return m ? `"${m[0]}" — quantitative work metrics are session activity, not lasting knowledge` : undefined;
|
|
289
|
+
},
|
|
290
|
+
},
|
|
236
291
|
// ── Stack traces / debug logs ──────────────────────────────────────────
|
|
237
292
|
{
|
|
238
293
|
id: 'stack-trace',
|
|
@@ -386,6 +441,27 @@ const SIGNALS = [
|
|
|
386
441
|
return m ? `"${m[0]}" — store the decision or fact, not the meeting reference` : undefined;
|
|
387
442
|
},
|
|
388
443
|
},
|
|
444
|
+
// ── Shipped / released / merged ────────────────────────────────────────
|
|
445
|
+
// Positive deployment/release language not already caught by temporal.
|
|
446
|
+
// ("just deployed" is caught by temporal; this catches statements without "just".)
|
|
447
|
+
{
|
|
448
|
+
id: 'shipped',
|
|
449
|
+
label: 'Deployed, released, or merged',
|
|
450
|
+
confidence: 'medium',
|
|
451
|
+
test: (_title, content) => {
|
|
452
|
+
const patterns = [
|
|
453
|
+
// "was/got/has been deployed to production/staging/main" — past-tense anchor prevents
|
|
454
|
+
// firing on policy conventions like "is deployed to production before release"
|
|
455
|
+
/\b(?:was|were|got|has been)\s+deployed?\s+(?:to|into)\s+(production|prod|staging|main|master)\b/,
|
|
456
|
+
// "was/got/has been merged into main/master/develop/trunk"
|
|
457
|
+
/\b(?:was|were|got|has been)\s+merged?\s+(?:to|into)\s+(main|master|develop|trunk)\b/,
|
|
458
|
+
// "released to production" or "released as v1.2" or "released as version 1.2"
|
|
459
|
+
/\breleased?\s+(?:to\s+(?:production|prod)|(?:as\s+)?(?:version\s+)?v?\d+\.\d+)\b/,
|
|
460
|
+
];
|
|
461
|
+
const m = firstMatch(content, patterns);
|
|
462
|
+
return m ? `"${m[0]}" — deployment/release events are ephemeral; store the resulting behavior instead` : undefined;
|
|
463
|
+
},
|
|
464
|
+
},
|
|
389
465
|
// ── Pending decision / under evaluation ─────────────────────────────────
|
|
390
466
|
{
|
|
391
467
|
id: 'pending-decision',
|
|
@@ -435,6 +511,30 @@ const SIGNALS = [
|
|
|
435
511
|
return m ? `"${m[0]}" — metric changes are often transient observations` : undefined;
|
|
436
512
|
},
|
|
437
513
|
},
|
|
514
|
+
// ── Bundling conjunctions ──────────────────────────────────────────────
|
|
515
|
+
// Catches agents that put multiple unrelated facts in a single entry using
|
|
516
|
+
// explicit linking language. Only fires on sentence-boundary connectors
|
|
517
|
+
// (comma or start of new sentence) to avoid false positives on prose like
|
|
518
|
+
// "X and Y work together" or "also useful for Z".
|
|
519
|
+
{
|
|
520
|
+
id: 'bundling-conjunction',
|
|
521
|
+
label: 'Multiple facts bundled',
|
|
522
|
+
confidence: 'low',
|
|
523
|
+
test: (_title, content) => {
|
|
524
|
+
const patterns = [
|
|
525
|
+
/[,;]\s+also\b/i, // "X works this way, also Y does Z"
|
|
526
|
+
/[.!?]\s+additionally,/i, // ". Additionally, ..."
|
|
527
|
+
/[.!?]\s+furthermore,/i, // ". Furthermore, ..."
|
|
528
|
+
/\bunrelated:/i, // "Unrelated: ..."
|
|
529
|
+
/\bseparately:/i, // "Separately: ..."
|
|
530
|
+
/[.!?]\s+on a (separate|different|unrelated) note[,:]?/i,
|
|
531
|
+
];
|
|
532
|
+
const m = firstMatch(content, patterns);
|
|
533
|
+
return m
|
|
534
|
+
? `"${m[0]}" — consider splitting into separate entries (one insight per entry)`
|
|
535
|
+
: undefined;
|
|
536
|
+
},
|
|
537
|
+
},
|
|
438
538
|
// ── Very short content ─────────────────────────────────────────────────
|
|
439
539
|
{
|
|
440
540
|
id: 'too-short',
|
|
@@ -502,15 +602,17 @@ export function formatEphemeralWarning(signals) {
|
|
|
502
602
|
'',
|
|
503
603
|
];
|
|
504
604
|
// Scale the guidance with confidence — high-confidence gets direct advice,
|
|
505
|
-
// low-confidence gets a softer suggestion to let the agent decide
|
|
605
|
+
// low-confidence gets a softer suggestion to let the agent decide.
|
|
606
|
+
// Always include the positive redirect: store state, not events.
|
|
506
607
|
if (highCount >= 2) {
|
|
507
608
|
lines.push('This is almost certainly session-specific. Consider deleting after your session.');
|
|
609
|
+
lines.push('If there is a lasting insight here, rephrase it as a present-tense fact: what is now true about the codebase?');
|
|
508
610
|
}
|
|
509
611
|
else if (highCount === 1) {
|
|
510
|
-
lines.push('If this is a lasting insight,
|
|
612
|
+
lines.push('If this is a lasting insight, rephrase it as a present-tense fact (what is now true) rather than an action report (what you did). If session-specific, consider deleting after your session.');
|
|
511
613
|
}
|
|
512
614
|
else {
|
|
513
|
-
lines.push('
|
|
615
|
+
lines.push('If this is durable knowledge, rephrase as a present-tense fact: what is now true? If it describes what you did rather than what is, consider deleting after your session.');
|
|
514
616
|
}
|
|
515
617
|
return lines.join('\n');
|
|
516
618
|
}
|
package/dist/index.js
CHANGED
|
@@ -183,7 +183,10 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
183
183
|
// and memory_stats. The handler still works if called directly.
|
|
184
184
|
{
|
|
185
185
|
name: 'memory_store',
|
|
186
|
-
|
|
186
|
+
// Example comes first — agents form their call shape from the first concrete pattern they see.
|
|
187
|
+
// "entries" (not "content") signals a collection; fighting the "content = string" prior
|
|
188
|
+
// is an architectural fix rather than patching the description after the fact.
|
|
189
|
+
description: 'memory_store(topic: "gotchas", entries: [{title: "Build cache", fact: "Must clean build after Tuist changes"}, {title: "Tuist version", fact: "Project requires Tuist 4.x"}], tags: ["build"]). Stores enduring knowledge — (1) Codebase facts (architecture, conventions, gotchas, modules): what IS true now, not past actions. Wrong: "Completed migration to StateFlow." Right: "All ViewModels use StateFlow." (2) User knowledge (user, preferences): who the person is, how they work. "user" and "preferences" are global. One insight per object; use multiple objects instead of bundling.',
|
|
187
190
|
inputSchema: {
|
|
188
191
|
type: 'object',
|
|
189
192
|
properties: {
|
|
@@ -196,13 +199,25 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
196
199
|
description: 'Predefined: user | preferences | architecture | conventions | gotchas | recent-work. Custom namespace: modules/<name> (e.g. modules/brainstorm, modules/game-design, modules/api-notes). Use modules/<name> for any domain that doesn\'t fit the built-in topics.',
|
|
197
200
|
enum: ['user', 'preferences', 'architecture', 'conventions', 'gotchas', 'recent-work'],
|
|
198
201
|
},
|
|
199
|
-
|
|
200
|
-
type: '
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
202
|
+
entries: {
|
|
203
|
+
type: 'array',
|
|
204
|
+
// Type annotation first — agents trained on code read type signatures before prose.
|
|
205
|
+
description: 'Array<{title: string, fact: string}> — not a string. One object per insight. title: short label (2-5 words). fact: codebase topics → present-tense state ("X uses Y", not "Completed X"); user/preferences → what the person expressed. Wrong: one object bundling two facts. Right: two objects.',
|
|
206
|
+
items: {
|
|
207
|
+
type: 'object',
|
|
208
|
+
properties: {
|
|
209
|
+
title: {
|
|
210
|
+
type: 'string',
|
|
211
|
+
description: 'Short label for this insight (2-5 words)',
|
|
212
|
+
},
|
|
213
|
+
fact: {
|
|
214
|
+
type: 'string',
|
|
215
|
+
description: 'The insight itself — one focused fact or observation',
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
required: ['title', 'fact'],
|
|
219
|
+
},
|
|
220
|
+
minItems: 1,
|
|
206
221
|
},
|
|
207
222
|
sources: {
|
|
208
223
|
type: 'array',
|
|
@@ -229,7 +244,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
229
244
|
default: [],
|
|
230
245
|
},
|
|
231
246
|
},
|
|
232
|
-
required: ['topic', '
|
|
247
|
+
required: ['topic', 'entries'],
|
|
233
248
|
},
|
|
234
249
|
},
|
|
235
250
|
{
|
|
@@ -393,11 +408,16 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
393
408
|
};
|
|
394
409
|
}
|
|
395
410
|
case 'memory_store': {
|
|
396
|
-
const { lobe: rawLobe, topic: rawTopic,
|
|
411
|
+
const { lobe: rawLobe, topic: rawTopic, entries: rawEntries, sources, references, trust: rawTrust, tags: rawTags } = z.object({
|
|
397
412
|
lobe: z.string().optional(),
|
|
398
413
|
topic: z.string(),
|
|
399
|
-
title
|
|
400
|
-
|
|
414
|
+
// Accept a bare {title, fact} object in addition to the canonical array form.
|
|
415
|
+
// Only objects are auto-wrapped — strings and other primitives still fail with
|
|
416
|
+
// a type error, preserving the "validate at boundaries" invariant.
|
|
417
|
+
entries: z.preprocess((val) => (val !== null && !Array.isArray(val) && typeof val === 'object' ? [val] : val), z.array(z.object({
|
|
418
|
+
title: z.string().min(1),
|
|
419
|
+
fact: z.string().min(1),
|
|
420
|
+
})).min(1)),
|
|
401
421
|
sources: z.array(z.string()).default([]),
|
|
402
422
|
references: z.array(z.string()).default([]),
|
|
403
423
|
trust: z.enum(['user', 'agent-confirmed', 'agent-inferred']).default('agent-inferred'),
|
|
@@ -424,54 +444,71 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
424
444
|
const ctx = resolveToolContext(effectiveLobe, { isGlobal });
|
|
425
445
|
if (!ctx.ok)
|
|
426
446
|
return contextError(ctx);
|
|
427
|
-
const
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
447
|
+
const effectiveTrust = isGlobal && trust === 'agent-inferred' ? 'user' : trust;
|
|
448
|
+
const storedResults = [];
|
|
449
|
+
for (const { title, fact } of rawEntries) {
|
|
450
|
+
const result = await ctx.store.store(topic, title, fact, sources, effectiveTrust, references, rawTags);
|
|
451
|
+
if (!result.stored) {
|
|
452
|
+
return {
|
|
453
|
+
content: [{ type: 'text', text: `[${ctx.label}] Failed to store "${title}": ${result.warning}` }],
|
|
454
|
+
isError: true,
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
storedResults.push({ title, result });
|
|
458
|
+
}
|
|
459
|
+
// Build response header
|
|
460
|
+
const lines = [];
|
|
461
|
+
if (storedResults.length === 1) {
|
|
462
|
+
const { result } = storedResults[0];
|
|
463
|
+
lines.push(`[${ctx.label}] Stored entry ${result.id} in ${result.topic} (confidence: ${result.confidence})`);
|
|
464
|
+
if (result.warning)
|
|
465
|
+
lines.push(`Note: ${result.warning}`);
|
|
466
|
+
}
|
|
467
|
+
else {
|
|
468
|
+
const { result: first } = storedResults[0];
|
|
469
|
+
lines.push(`[${ctx.label}] Stored ${storedResults.length} entries in ${first.topic} (confidence: ${first.confidence}):`);
|
|
470
|
+
for (const { title, result } of storedResults) {
|
|
471
|
+
lines.push(` - ${result.id}: "${title}"`);
|
|
472
|
+
}
|
|
435
473
|
}
|
|
436
|
-
const lines = [
|
|
437
|
-
`[${ctx.label}] Stored entry ${result.id} in ${result.topic} (confidence: ${result.confidence})`,
|
|
438
|
-
];
|
|
439
|
-
if (result.warning)
|
|
440
|
-
lines.push(`Note: ${result.warning}`);
|
|
441
474
|
// Limit to at most 2 hint sections per response to prevent hint fatigue.
|
|
442
475
|
// Priority: dedup > ephemeral > preferences (dedup is actionable and high-signal,
|
|
443
476
|
// ephemeral warnings affect entry quality, preferences are informational).
|
|
477
|
+
// For multi-entry batches, hints reference the first triggering entry.
|
|
444
478
|
let hintCount = 0;
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
lines.push(
|
|
452
|
-
|
|
479
|
+
for (const { title, result } of storedResults) {
|
|
480
|
+
const entryPrefix = storedResults.length > 1 ? `"${title}": ` : '';
|
|
481
|
+
// Dedup: surface related entries in the same topic
|
|
482
|
+
if (result.relatedEntries && result.relatedEntries.length > 0 && hintCount < 2) {
|
|
483
|
+
hintCount++;
|
|
484
|
+
lines.push('');
|
|
485
|
+
lines.push(`⚠ ${entryPrefix}Similar entries found in the same topic:`);
|
|
486
|
+
for (const r of result.relatedEntries) {
|
|
487
|
+
lines.push(` - ${r.id}: "${r.title}" (confidence: ${r.confidence})`);
|
|
488
|
+
lines.push(` Content: ${r.content.length > 120 ? r.content.substring(0, 120) + '...' : r.content}`);
|
|
489
|
+
}
|
|
490
|
+
lines.push('');
|
|
491
|
+
lines.push('To consolidate: memory_correct(id: "<old-id>", action: "replace", correction: "<merged content>") then memory_correct(id: "<new-id>", action: "delete")');
|
|
453
492
|
}
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
lines.push(
|
|
493
|
+
// Ephemeral content warning — soft nudge, never blocking
|
|
494
|
+
if (result.ephemeralWarning && hintCount < 2) {
|
|
495
|
+
hintCount++;
|
|
496
|
+
lines.push('');
|
|
497
|
+
lines.push(`⏳ ${entryPrefix}${result.ephemeralWarning}`);
|
|
498
|
+
}
|
|
499
|
+
// Preference surfacing: show relevant preferences for non-preference entries
|
|
500
|
+
if (result.relevantPreferences && result.relevantPreferences.length > 0 && hintCount < 2) {
|
|
501
|
+
hintCount++;
|
|
502
|
+
lines.push('');
|
|
503
|
+
lines.push(`📌 ${entryPrefix}Relevant preferences:`);
|
|
504
|
+
for (const p of result.relevantPreferences) {
|
|
505
|
+
lines.push(` - [pref] ${p.title}: ${p.content.length > 120 ? p.content.substring(0, 120) + '...' : p.content}`);
|
|
506
|
+
}
|
|
507
|
+
lines.push('');
|
|
508
|
+
lines.push('Review the stored entry against these preferences for potential conflicts.');
|
|
470
509
|
}
|
|
471
|
-
lines.push('');
|
|
472
|
-
lines.push('Review the stored entry against these preferences for potential conflicts.');
|
|
473
510
|
}
|
|
474
|
-
// Vocabulary echo: show existing tags to drive convergence
|
|
511
|
+
// Vocabulary echo: show existing tags to drive convergence (once per response)
|
|
475
512
|
if (hintCount < 2) {
|
|
476
513
|
const tagFreq = ctx.store.getTagFrequency();
|
|
477
514
|
if (tagFreq.size > 0) {
|
|
@@ -1032,8 +1069,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1032
1069
|
const lobeNames = configManager.getLobeNames();
|
|
1033
1070
|
hint = `\n\nHint: lobe is required. Use memory_list_lobes to see available lobes. Available: ${lobeNames.join(', ')}`;
|
|
1034
1071
|
}
|
|
1035
|
-
else if (message.includes('"topic"') || message.includes('"
|
|
1036
|
-
hint = '\n\nHint: memory_store requires: topic (architecture|conventions|gotchas|recent-work|modules/<name>), title,
|
|
1072
|
+
else if (message.includes('"topic"') || message.includes('"entries"')) {
|
|
1073
|
+
hint = '\n\nHint: memory_store requires: topic (architecture|conventions|gotchas|recent-work|modules/<name>) and entries (Array<{title, fact}>). Example: entries: [{title: "Build cache", fact: "Must clean build after Tuist changes"}]. Use modules/<name> for custom namespaces.';
|
|
1037
1074
|
}
|
|
1038
1075
|
else if (message.includes('"scope"')) {
|
|
1039
1076
|
hint = '\n\nHint: memory_query requires: lobe, scope (architecture|conventions|gotchas|recent-work|modules/<name>|* for all)';
|
package/dist/normalize.js
CHANGED
|
@@ -6,11 +6,6 @@
|
|
|
6
6
|
/** Canonical param name aliases — maps guessed names to their correct form */
|
|
7
7
|
const PARAM_ALIASES = {
|
|
8
8
|
// memory_store aliases
|
|
9
|
-
key: 'title',
|
|
10
|
-
name: 'title',
|
|
11
|
-
value: 'content',
|
|
12
|
-
body: 'content',
|
|
13
|
-
text: 'content',
|
|
14
9
|
refs: 'references',
|
|
15
10
|
// memory_query aliases
|
|
16
11
|
query: 'filter',
|