@exaudeus/memory-mcp 1.4.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.
package/README.md CHANGED
@@ -8,7 +8,7 @@ A Model Context Protocol (MCP) server that gives AI coding agents persistent, ev
8
8
  |------|-------------|
9
9
  | `memory_context` | Session start AND pre-task lookup. Call with no args for user + preferences + stale nudges; call with `context` for task-specific knowledge |
10
10
  | `memory_query` | Structured search with brief/standard/full detail levels and AND/OR/NOT filter syntax. Scope defaults to `"*"` (all topics) |
11
- | `memory_store` | Store a knowledge entry with dedup detection, preference surfacing, and lobe auto-detection from file paths |
11
+ | `memory_store` | Store a knowledge entry with dedup detection, preference surfacing, lobe auto-detection, and a review-required gate for likely-ephemeral content |
12
12
  | `memory_correct` | Correct, update, or delete an existing entry (suggests storing as preference) |
13
13
  | `memory_bootstrap` | First-use scan to seed knowledge from repo structure, README, and build files |
14
14
 
@@ -33,6 +33,7 @@ A Model Context Protocol (MCP) server that gives AI coding agents persistent, ev
33
33
 
34
34
  - **Dedup detection**: When you store an entry, the response shows similar existing entries in the same topic (>35% keyword overlap) with consolidation instructions
35
35
  - **Preference surfacing**: Storing a non-preference entry shows relevant preferences that might conflict
36
+ - **Ephemeral review gate**: Likely-ephemeral content is blocked before persistence by default. Re-run `memory_store(..., durabilityDecision: "store-anyway")` only when you intentionally want to keep it.
36
37
  - **Piggyback hints**: `memory_correct` suggests storing corrections as reusable preferences
37
38
  - **`memory_context`**: Describe your task in natural language and get ranked results across all topics with topic-based boosting (preferences 1.8x, gotchas 1.5x)
38
39
 
@@ -108,10 +109,12 @@ If no `memory-config.json` is found, the server falls back to environment variab
108
109
  1. **Edit `memory-config.json`** (create if it doesn't exist)
109
110
  2. **Add lobe entry:**
110
111
  ```json
112
+ {
111
113
  "my-project": {
112
114
  "root": "$HOME/git/my-project",
113
115
  "budgetMB": 2
114
116
  }
117
+ }
115
118
  ```
116
119
  3. **Restart the memory MCP server**
117
120
  4. **Verify:** Use `memory_list_lobes` to confirm it loaded
package/dist/index.js CHANGED
@@ -215,7 +215,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
215
215
  // Example comes first — agents form their call shape from the first concrete pattern they see.
216
216
  // "entries" (not "content") signals a collection; fighting the "content = string" prior
217
217
  // is an architectural fix rather than patching the description after the fact.
218
- 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 stored in the alwaysInclude lobe (shared across projects). One insight per object; use multiple objects instead of bundling.',
218
+ 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 stored in the alwaysInclude lobe (shared across projects). One insight per object; use multiple objects instead of bundling. If content looks likely-ephemeral, the tool returns a review-required response; re-run with durabilityDecision: "store-anyway" only when deliberate.',
219
219
  inputSchema: {
220
220
  type: 'object',
221
221
  properties: {
@@ -272,6 +272,12 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
272
272
  description: 'Category labels for exact-match retrieval (lowercase slugs). Query with filter: "#tag". Example: ["auth", "critical-path", "mite-combat"]',
273
273
  default: [],
274
274
  },
275
+ durabilityDecision: {
276
+ type: 'string',
277
+ enum: ['default', 'store-anyway'],
278
+ description: 'Write intent for content that may require review. Use "default" normally. Use "store-anyway" only when intentionally persisting content after a review-required response.',
279
+ default: 'default',
280
+ },
275
281
  },
276
282
  required: ['topic', 'entries'],
277
283
  },
@@ -442,7 +448,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
442
448
  };
443
449
  }
444
450
  case 'memory_store': {
445
- const { lobe: rawLobe, topic: rawTopic, entries: rawEntries, sources, references, trust: rawTrust, tags: rawTags } = z.object({
451
+ const { lobe: rawLobe, topic: rawTopic, entries: rawEntries, sources, references, trust: rawTrust, tags: rawTags, durabilityDecision } = z.object({
446
452
  lobe: z.string().optional(),
447
453
  topic: z.string(),
448
454
  // Accept a bare {title, fact} object in addition to the canonical array form.
@@ -456,6 +462,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
456
462
  references: z.array(z.string()).default([]),
457
463
  trust: z.enum(['user', 'agent-confirmed', 'agent-inferred']).default('agent-inferred'),
458
464
  tags: z.array(z.string()).default([]),
465
+ durabilityDecision: z.enum(['default', 'store-anyway']).default('default'),
459
466
  }).parse(args);
460
467
  // Validate topic at boundary
461
468
  const topic = parseTopicScope(rawTopic);
@@ -494,8 +501,26 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
494
501
  : trust;
495
502
  const storedResults = [];
496
503
  for (const { title, fact } of rawEntries) {
497
- const result = await ctx.store.store(topic, title, fact, sources, effectiveTrust, references, rawTags);
498
- if (!result.stored) {
504
+ const result = await ctx.store.store(topic, title, fact, sources, effectiveTrust, references, rawTags, durabilityDecision);
505
+ if (result.kind === 'review-required') {
506
+ const lines = [
507
+ `[${ctx.label}] Review required before storing "${title}".`,
508
+ '',
509
+ `Severity: ${result.severity}`,
510
+ 'Signals:',
511
+ ...result.signals.map(signal => `- ${signal.label}: ${signal.detail}`),
512
+ '',
513
+ result.warning,
514
+ '',
515
+ 'If this knowledge is still worth persisting, re-run with:',
516
+ `memory_store(topic: "${topic}", entries: [{title: "${title}", fact: "${fact.replace(/"/g, '\\"')}"}], trust: "${effectiveTrust}", durabilityDecision: "store-anyway"${rawTags.length > 0 ? `, tags: ${JSON.stringify(rawTags)}` : ''}${sources.length > 0 ? `, sources: ${JSON.stringify(sources)}` : ''}${references.length > 0 ? `, references: ${JSON.stringify(references)}` : ''})`,
517
+ ];
518
+ return {
519
+ content: [{ type: 'text', text: lines.join('\n') }],
520
+ isError: true,
521
+ };
522
+ }
523
+ if (result.kind === 'rejected') {
499
524
  return {
500
525
  content: [{ type: 'text', text: `[${ctx.label}] Failed to store "${title}": ${result.warning}` }],
501
526
  isError: true,
@@ -1092,8 +1117,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1092
1117
  if (!ctx.ok)
1093
1118
  return contextError(ctx);
1094
1119
  const results = await ctx.store.bootstrap();
1095
- const stored = results.filter(r => r.stored);
1096
- const failed = results.filter(r => !r.stored);
1120
+ const stored = results.filter((r) => r.kind === 'stored');
1121
+ const failed = results.filter((r) => r.kind !== 'stored');
1097
1122
  let text = `## [${ctx.label}] Bootstrap Complete\n\nStored ${stored.length} entries:`;
1098
1123
  for (const r of stored) {
1099
1124
  text += `\n- ${r.id}: ${r.topic} (${r.file})`;
package/dist/store.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { MemoryEntry, TopicScope, TrustLevel, DetailLevel, QueryResult, StoreResult, CorrectResult, MemoryStats, BriefingResult, ConflictPair, MemoryConfig } from './types.js';
1
+ import type { MemoryEntry, TopicScope, TrustLevel, DetailLevel, DurabilityDecision, QueryResult, StoreResult, CorrectResult, MemoryStats, BriefingResult, ConflictPair, MemoryConfig } from './types.js';
2
2
  export declare class MarkdownMemoryStore {
3
3
  private readonly config;
4
4
  private readonly memoryPath;
@@ -13,7 +13,7 @@ export declare class MarkdownMemoryStore {
13
13
  /** Initialize the store: create memory dir and load existing entries */
14
14
  init(): Promise<void>;
15
15
  /** Store a new knowledge entry */
16
- store(topic: TopicScope, title: string, content: string, sources?: string[], trust?: TrustLevel, references?: string[], rawTags?: string[]): Promise<StoreResult>;
16
+ store(topic: TopicScope, title: string, content: string, sources?: string[], trust?: TrustLevel, references?: string[], rawTags?: string[], durabilityDecision?: DurabilityDecision): Promise<StoreResult>;
17
17
  /** Query knowledge by scope and detail level */
18
18
  query(scope: string, detail?: DetailLevel, filter?: string, branchFilter?: string): Promise<QueryResult>;
19
19
  /** Generate a session-start briefing */
package/dist/store.js CHANGED
@@ -41,15 +41,35 @@ export class MarkdownMemoryStore {
41
41
  await this.reloadFromDisk();
42
42
  }
43
43
  /** Store a new knowledge entry */
44
- async store(topic, title, content, sources = [], trust = 'agent-inferred', references = [], rawTags = []) {
44
+ async store(topic, title, content, sources = [], trust = 'agent-inferred', references = [], rawTags = [], durabilityDecision = 'default') {
45
45
  // Check storage budget — null means we can't measure, allow the write
46
46
  const currentSize = await this.getStorageSize();
47
47
  if (currentSize !== null && currentSize >= this.config.storageBudgetBytes) {
48
48
  return {
49
- stored: false, topic,
49
+ kind: 'rejected', stored: false, topic,
50
50
  warning: `Storage budget exceeded (${this.formatBytes(currentSize)} / ${this.formatBytes(this.config.storageBudgetBytes)}). Delete or correct existing entries to free space.`,
51
51
  };
52
52
  }
53
+ const ephemeralSignals = topic !== 'recent-work'
54
+ ? detectEphemeralSignals(title, content, topic)
55
+ : [];
56
+ const ephemeralSeverity = getEphemeralSeverity(ephemeralSignals);
57
+ const requiresReview = ephemeralSeverity === 'medium' || ephemeralSeverity === 'high';
58
+ if (durabilityDecision === 'default' && requiresReview) {
59
+ return {
60
+ kind: 'review-required',
61
+ stored: false,
62
+ topic,
63
+ severity: ephemeralSeverity,
64
+ warning: `Likely ephemeral content detected. Review the signals and re-run with durabilityDecision: "store-anyway" if this knowledge should still be persisted.`,
65
+ signals: ephemeralSignals.map(signal => ({
66
+ id: signal.id,
67
+ label: signal.label,
68
+ detail: signal.detail,
69
+ confidence: signal.confidence,
70
+ })),
71
+ };
72
+ }
53
73
  const id = this.generateId(topic);
54
74
  const now = this.clock.isoNow();
55
75
  const confidence = DEFAULT_CONFIDENCE[trust];
@@ -85,15 +105,9 @@ export class MarkdownMemoryStore {
85
105
  const relevantPreferences = (topic !== 'preferences' && topic !== 'user')
86
106
  ? this.findRelevantPreferences(entry)
87
107
  : undefined;
88
- // Soft ephemeral detection — warn but never block
89
- const ephemeralSignals = topic !== 'recent-work'
90
- ? detectEphemeralSignals(title, content, topic)
91
- : [];
92
- // getEphemeralSeverity is the single source of threshold logic shared with formatEphemeralWarning.
93
- const ephemeralSeverity = getEphemeralSeverity(ephemeralSignals);
94
108
  const ephemeralWarning = formatEphemeralWarning(ephemeralSignals, id);
95
109
  return {
96
- stored: true, id, topic, file, confidence, warning, ephemeralWarning,
110
+ kind: 'stored', stored: true, id, topic, file, confidence, warning, ephemeralWarning,
97
111
  ephemeralSeverity: ephemeralSeverity ?? undefined,
98
112
  relatedEntries: relatedEntries.length > 0 ? relatedEntries : undefined,
99
113
  relevantPreferences: relevantPreferences && relevantPreferences.length > 0 ? relevantPreferences : undefined,
@@ -384,7 +398,7 @@ export class MarkdownMemoryStore {
384
398
  catch { /* not a git repo or git not available */ }
385
399
  // 5. Fallback: if nothing was detected, store a minimal overview so the lobe
386
400
  // is never left completely empty after bootstrap (makes memory_context useful immediately).
387
- const storedCount = results.filter(r => r.stored).length;
401
+ const storedCount = results.filter(r => r.kind === 'stored').length;
388
402
  if (storedCount === 0) {
389
403
  try {
390
404
  const topLevel = await fs.readdir(repoRoot, { withFileTypes: true });
package/dist/types.d.ts CHANGED
@@ -89,8 +89,21 @@ export interface RelatedEntry {
89
89
  readonly confidence: number;
90
90
  readonly trust: TrustLevel;
91
91
  }
92
+ /** Caller intent for writes that may require a durability decision.
93
+ * Explicit domain type avoids booleanly-typed override semantics. */
94
+ export type DurabilityDecision = 'default' | 'store-anyway';
95
+ /** Structured signal attached to a review-required store outcome.
96
+ * Duplicated here (rather than importing from ephemeral.ts) to keep the store
97
+ * result contract independent of a concrete detection implementation. */
98
+ export interface ReviewSignal {
99
+ readonly id: string;
100
+ readonly label: string;
101
+ readonly detail: string;
102
+ readonly confidence: 'high' | 'medium' | 'low';
103
+ }
92
104
  /** Result of a memory store operation — discriminated union eliminates impossible states */
93
105
  export type StoreResult = {
106
+ readonly kind: 'stored';
94
107
  readonly stored: true;
95
108
  readonly id: string;
96
109
  readonly topic: TopicScope;
@@ -104,6 +117,14 @@ export type StoreResult = {
104
117
  readonly relatedEntries?: readonly RelatedEntry[];
105
118
  readonly relevantPreferences?: readonly RelatedEntry[];
106
119
  } | {
120
+ readonly kind: 'review-required';
121
+ readonly stored: false;
122
+ readonly topic: TopicScope;
123
+ readonly severity: EphemeralSeverity;
124
+ readonly warning: string;
125
+ readonly signals: readonly ReviewSignal[];
126
+ } | {
127
+ readonly kind: 'rejected';
107
128
  readonly stored: false;
108
129
  readonly topic: TopicScope;
109
130
  readonly warning: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exaudeus/memory-mcp",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "description": "Codebase memory MCP server - persistent, evolving knowledge for AI coding agents",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",