@exaudeus/memory-mcp 1.2.0 → 1.4.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/config-manager.d.ts +6 -0
- package/dist/config-manager.js +13 -1
- package/dist/config.js +45 -3
- package/dist/ephemeral.d.ts +7 -3
- package/dist/ephemeral.js +66 -23
- package/dist/formatters.js +22 -12
- package/dist/index.js +256 -248
- package/dist/lobe-resolution.d.ts +34 -0
- package/dist/lobe-resolution.js +89 -0
- package/dist/store.d.ts +3 -0
- package/dist/store.js +11 -2
- package/dist/thresholds.d.ts +3 -8
- package/dist/thresholds.js +4 -8
- package/dist/types.d.ts +6 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -7,18 +7,17 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
|
7
7
|
import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
8
8
|
import { z } from 'zod';
|
|
9
9
|
import path from 'path';
|
|
10
|
-
import os from 'os';
|
|
11
|
-
import { existsSync, writeFileSync } from 'fs';
|
|
12
10
|
import { readFile, writeFile } from 'fs/promises';
|
|
13
11
|
import { MarkdownMemoryStore } from './store.js';
|
|
14
|
-
import {
|
|
12
|
+
import { parseTopicScope, parseTrustLevel } from './types.js';
|
|
15
13
|
import { getLobeConfigs } from './config.js';
|
|
16
14
|
import { ConfigManager } from './config-manager.js';
|
|
17
15
|
import { normalizeArgs } from './normalize.js';
|
|
18
16
|
import { buildCrashReport, writeCrashReport, writeCrashReportSync, readLatestCrash, readCrashHistory, clearLatestCrash, formatCrashReport, formatCrashSummary, markServerStarted, } from './crash-journal.js';
|
|
19
17
|
import { formatStaleSection, formatConflictWarning, formatStats, formatBehaviorConfigSection, mergeTagFrequencies, buildQueryFooter, buildTagPrimerSection } from './formatters.js';
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
18
|
+
import { parseFilter } from './text-analyzer.js';
|
|
19
|
+
import { VOCABULARY_ECHO_LIMIT, WARN_SEPARATOR } from './thresholds.js';
|
|
20
|
+
import { matchRootsToLobeNames, buildLobeResolution } from './lobe-resolution.js';
|
|
22
21
|
let serverMode = { kind: 'running' };
|
|
23
22
|
const lobeHealth = new Map();
|
|
24
23
|
const serverStartTime = Date.now();
|
|
@@ -71,21 +70,12 @@ const stores = new Map();
|
|
|
71
70
|
const lobeNames = Array.from(lobeConfigs.keys());
|
|
72
71
|
// ConfigManager will be initialized after stores are set up
|
|
73
72
|
let configManager;
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const
|
|
77
|
-
const globalStore = new MarkdownMemoryStore({
|
|
78
|
-
repoRoot: os.homedir(),
|
|
79
|
-
memoryPath: globalMemoryPath,
|
|
80
|
-
storageBudgetBytes: DEFAULT_STORAGE_BUDGET_BYTES,
|
|
81
|
-
});
|
|
73
|
+
/** Topics that auto-route to the first alwaysInclude lobe when no lobe is specified on writes.
|
|
74
|
+
* This is a backwards-compat shim — agents historically wrote these without specifying a lobe. */
|
|
75
|
+
const ALWAYS_INCLUDE_WRITE_TOPICS = new Set(['user', 'preferences']);
|
|
82
76
|
/** Resolve a raw lobe name to a validated store + display label.
|
|
83
77
|
* After this call, consumers use ctx.label — the raw lobe is not in scope. */
|
|
84
|
-
function resolveToolContext(rawLobe
|
|
85
|
-
// Global topics always route to the global store
|
|
86
|
-
if (opts?.isGlobal) {
|
|
87
|
-
return { ok: true, store: globalStore, label: 'global' };
|
|
88
|
-
}
|
|
78
|
+
function resolveToolContext(rawLobe) {
|
|
89
79
|
const lobeNames = configManager.getLobeNames();
|
|
90
80
|
// Default to single lobe when omitted
|
|
91
81
|
const lobe = rawLobe || (lobeNames.length === 1 ? lobeNames[0] : undefined);
|
|
@@ -154,6 +144,45 @@ function inferLobeFromPaths(paths) {
|
|
|
154
144
|
return matchedLobes.size === 1 ? matchedLobes.values().next().value : undefined;
|
|
155
145
|
}
|
|
156
146
|
const server = new Server({ name: 'memory-mcp', version: '0.1.0' }, { capabilities: { tools: {} } });
|
|
147
|
+
// --- Lobe resolution for read operations ---
|
|
148
|
+
// When the agent doesn't specify a lobe, we determine which lobe(s) to search
|
|
149
|
+
// via a degradation ladder (see lobe-resolution.ts for the pure logic):
|
|
150
|
+
// 1. Single lobe configured → use it (unambiguous)
|
|
151
|
+
// 2. Multiple lobes → ask client for workspace roots via MCP roots/list
|
|
152
|
+
// 3. Fallback → global-only with a hint to specify the lobe
|
|
153
|
+
/** Resolve which lobes to search for a read operation when the agent omitted the lobe param.
|
|
154
|
+
* Wires the MCP server's listRoots into the pure resolution logic. */
|
|
155
|
+
async function resolveLobesForRead(isFirstMemoryToolCall = true) {
|
|
156
|
+
const allLobeNames = configManager.getLobeNames();
|
|
157
|
+
const alwaysIncludeLobes = configManager.getAlwaysIncludeLobes();
|
|
158
|
+
// Short-circuit: single lobe is unambiguous — no need for root matching.
|
|
159
|
+
// Handles both plain single-lobe and single-lobe-that-is-alwaysInclude cases.
|
|
160
|
+
if (allLobeNames.length === 1) {
|
|
161
|
+
return buildLobeResolution(allLobeNames, allLobeNames, alwaysIncludeLobes, isFirstMemoryToolCall);
|
|
162
|
+
}
|
|
163
|
+
// Multiple lobes — try MCP client roots
|
|
164
|
+
const clientCaps = server.getClientCapabilities();
|
|
165
|
+
if (clientCaps?.roots) {
|
|
166
|
+
try {
|
|
167
|
+
const { roots } = await server.listRoots();
|
|
168
|
+
if (roots && roots.length > 0) {
|
|
169
|
+
const lobeConfigs = allLobeNames
|
|
170
|
+
.map(name => {
|
|
171
|
+
const config = configManager.getLobeConfig(name);
|
|
172
|
+
return config ? { name, repoRoot: config.repoRoot } : undefined;
|
|
173
|
+
})
|
|
174
|
+
.filter((c) => c !== undefined);
|
|
175
|
+
const matched = matchRootsToLobeNames(roots, lobeConfigs);
|
|
176
|
+
return buildLobeResolution(allLobeNames, matched, alwaysIncludeLobes, isFirstMemoryToolCall);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
catch (err) {
|
|
180
|
+
process.stderr.write(`[memory-mcp] listRoots failed: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
// Fallback — roots not available or no match
|
|
184
|
+
return buildLobeResolution(allLobeNames, [], alwaysIncludeLobes, isFirstMemoryToolCall);
|
|
185
|
+
}
|
|
157
186
|
/** Build the shared lobe property for tool schemas — called on each ListTools request
|
|
158
187
|
* so the description and enum stay in sync after a hot-reload adds or removes lobes. */
|
|
159
188
|
function buildLobeProperty(currentLobeNames) {
|
|
@@ -162,7 +191,7 @@ function buildLobeProperty(currentLobeNames) {
|
|
|
162
191
|
type: 'string',
|
|
163
192
|
description: isSingle
|
|
164
193
|
? `Memory lobe name (defaults to "${currentLobeNames[0]}" if omitted)`
|
|
165
|
-
: `Memory lobe name.
|
|
194
|
+
: `Memory lobe name. When omitted for reads, the server uses the client's workspace roots to select the matching lobe. If roots are unavailable and no alwaysInclude lobes are configured, specify a lobe explicitly to access lobe-specific knowledge. Required for writes. Available: ${currentLobeNames.join(', ')}`,
|
|
166
195
|
enum: currentLobeNames.length > 1 ? [...currentLobeNames] : undefined,
|
|
167
196
|
};
|
|
168
197
|
}
|
|
@@ -186,7 +215,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
186
215
|
// Example comes first — agents form their call shape from the first concrete pattern they see.
|
|
187
216
|
// "entries" (not "content") signals a collection; fighting the "content = string" prior
|
|
188
217
|
// 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
|
|
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.',
|
|
190
219
|
inputSchema: {
|
|
191
220
|
type: 'object',
|
|
192
221
|
properties: {
|
|
@@ -272,6 +301,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
272
301
|
type: 'string',
|
|
273
302
|
description: 'Branch for recent-work. Omit = current branch, "*" = all branches.',
|
|
274
303
|
},
|
|
304
|
+
isFirstMemoryToolCall: {
|
|
305
|
+
type: 'boolean',
|
|
306
|
+
description: 'Set true on first memory call in a conversation to include identity/preferences from alwaysInclude lobes. Set false on subsequent calls to skip redundant global knowledge.',
|
|
307
|
+
default: true,
|
|
308
|
+
},
|
|
275
309
|
},
|
|
276
310
|
required: [],
|
|
277
311
|
},
|
|
@@ -302,7 +336,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
302
336
|
},
|
|
303
337
|
{
|
|
304
338
|
name: 'memory_context',
|
|
305
|
-
description: 'Session start AND pre-task lookup. Call with no args at session start to get user identity, preferences, and stale entries. Call with context to get task-specific knowledge.
|
|
339
|
+
description: 'Session start AND pre-task lookup. Call with no args at session start to get user identity, preferences, and stale entries. Call with context to get task-specific knowledge. When lobe is omitted, uses client workspace roots to select the matching lobe; falls back to global-only if roots are unavailable. Example: memory_context() or memory_context(context: "writing a Kotlin reducer")',
|
|
306
340
|
inputSchema: {
|
|
307
341
|
type: 'object',
|
|
308
342
|
properties: {
|
|
@@ -321,6 +355,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
321
355
|
description: 'Min keyword match ratio 0-1 (default: 0.2). Higher = stricter.',
|
|
322
356
|
default: 0.2,
|
|
323
357
|
},
|
|
358
|
+
isFirstMemoryToolCall: {
|
|
359
|
+
type: 'boolean',
|
|
360
|
+
description: 'Set true on first memory call in a conversation to include identity/preferences from alwaysInclude lobes. Set false on subsequent calls to skip redundant global knowledge.',
|
|
361
|
+
default: true,
|
|
362
|
+
},
|
|
324
363
|
},
|
|
325
364
|
required: [],
|
|
326
365
|
},
|
|
@@ -388,16 +427,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
388
427
|
case 'memory_list_lobes': {
|
|
389
428
|
// Delegates to shared builder — same data as memory://lobes resource
|
|
390
429
|
const lobeInfo = await buildLobeInfo();
|
|
391
|
-
const
|
|
430
|
+
const alwaysIncludeNames = configManager.getAlwaysIncludeLobes();
|
|
392
431
|
const result = {
|
|
393
432
|
serverMode: serverMode.kind,
|
|
394
|
-
globalStore: {
|
|
395
|
-
memoryPath: globalMemoryPath,
|
|
396
|
-
entries: globalStats.totalEntries,
|
|
397
|
-
storageUsed: globalStats.storageSize,
|
|
398
|
-
topics: 'user, preferences (shared across all lobes)',
|
|
399
|
-
},
|
|
400
433
|
lobes: lobeInfo,
|
|
434
|
+
alwaysIncludeLobes: alwaysIncludeNames,
|
|
401
435
|
configFile: configFileDisplay(),
|
|
402
436
|
configSource: configOrigin.source,
|
|
403
437
|
totalLobes: lobeInfo.length,
|
|
@@ -439,12 +473,25 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
439
473
|
const allPaths = [...sources, ...references];
|
|
440
474
|
effectiveLobe = inferLobeFromPaths(allPaths);
|
|
441
475
|
}
|
|
476
|
+
// Auto-route user/preferences writes to the first alwaysInclude lobe when no lobe specified.
|
|
477
|
+
// This preserves the previous behavior where these topics auto-routed to the global store.
|
|
478
|
+
if (!effectiveLobe && ALWAYS_INCLUDE_WRITE_TOPICS.has(topic)) {
|
|
479
|
+
const alwaysIncludeLobes = configManager.getAlwaysIncludeLobes();
|
|
480
|
+
if (alwaysIncludeLobes.length > 0) {
|
|
481
|
+
effectiveLobe = alwaysIncludeLobes[0];
|
|
482
|
+
}
|
|
483
|
+
}
|
|
442
484
|
// Resolve store — after this point, rawLobe is never used again
|
|
443
|
-
const
|
|
444
|
-
const ctx = resolveToolContext(effectiveLobe, { isGlobal });
|
|
485
|
+
const ctx = resolveToolContext(effectiveLobe);
|
|
445
486
|
if (!ctx.ok)
|
|
446
487
|
return contextError(ctx);
|
|
447
|
-
|
|
488
|
+
// Auto-promote trust for global topics: agents writing user/preferences without explicit
|
|
489
|
+
// trust: "user" still get full confidence. Preserves pre-unification behavior where the
|
|
490
|
+
// global store always stored these at user trust — removing this would silently downgrade
|
|
491
|
+
// identity entries to confidence 0.70 (see philosophy: "Observability as a constraint").
|
|
492
|
+
const effectiveTrust = ALWAYS_INCLUDE_WRITE_TOPICS.has(topic) && trust === 'agent-inferred'
|
|
493
|
+
? 'user'
|
|
494
|
+
: trust;
|
|
448
495
|
const storedResults = [];
|
|
449
496
|
for (const { title, fact } of rawEntries) {
|
|
450
497
|
const result = await ctx.store.store(topic, title, fact, sources, effectiveTrust, references, rawTags);
|
|
@@ -456,11 +503,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
456
503
|
}
|
|
457
504
|
storedResults.push({ title, result });
|
|
458
505
|
}
|
|
459
|
-
// Build response header
|
|
506
|
+
// Build response header.
|
|
507
|
+
// For high-severity ephemeral detections, flag the success line itself so agents
|
|
508
|
+
// who anchor on line 1 still see the problem before reading the block below.
|
|
460
509
|
const lines = [];
|
|
461
510
|
if (storedResults.length === 1) {
|
|
462
511
|
const { result } = storedResults[0];
|
|
463
|
-
|
|
512
|
+
const ephemeralFlag = result.ephemeralSeverity === 'high' ? ' (⚠ ephemeral — see below)' : '';
|
|
513
|
+
lines.push(`[${ctx.label}] Stored entry ${result.id} in ${result.topic} (confidence: ${result.confidence})${ephemeralFlag}`);
|
|
464
514
|
if (result.warning)
|
|
465
515
|
lines.push(`Note: ${result.warning}`);
|
|
466
516
|
}
|
|
@@ -468,7 +518,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
468
518
|
const { result: first } = storedResults[0];
|
|
469
519
|
lines.push(`[${ctx.label}] Stored ${storedResults.length} entries in ${first.topic} (confidence: ${first.confidence}):`);
|
|
470
520
|
for (const { title, result } of storedResults) {
|
|
471
|
-
|
|
521
|
+
const ephemeralFlag = result.ephemeralSeverity === 'high' ? ' ⚠' : '';
|
|
522
|
+
lines.push(` - ${result.id}: "${title}"${ephemeralFlag}`);
|
|
472
523
|
}
|
|
473
524
|
}
|
|
474
525
|
// Limit to at most 2 hint sections per response to prevent hint fatigue.
|
|
@@ -478,23 +529,35 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
478
529
|
let hintCount = 0;
|
|
479
530
|
for (const { title, result } of storedResults) {
|
|
480
531
|
const entryPrefix = storedResults.length > 1 ? `"${title}": ` : '';
|
|
481
|
-
// Dedup: surface related entries in the same topic
|
|
532
|
+
// Dedup: surface related entries in the same topic.
|
|
533
|
+
// Fill in both actual IDs so the agent can act immediately without looking them up.
|
|
482
534
|
if (result.relatedEntries && result.relatedEntries.length > 0 && hintCount < 2) {
|
|
483
535
|
hintCount++;
|
|
536
|
+
const top = result.relatedEntries[0];
|
|
484
537
|
lines.push('');
|
|
485
|
-
lines.push(
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
538
|
+
lines.push(WARN_SEPARATOR);
|
|
539
|
+
lines.push(`⚠ ${entryPrefix}SIMILAR ENTRY ALREADY EXISTS — CONSOLIDATE ⚠`);
|
|
540
|
+
lines.push(WARN_SEPARATOR);
|
|
541
|
+
lines.push(` ${top.id}: "${top.title}" (confidence: ${top.confidence})`);
|
|
542
|
+
lines.push(` ${top.content.length > 120 ? top.content.substring(0, 120) + '...' : top.content}`);
|
|
543
|
+
if (result.relatedEntries.length > 1) {
|
|
544
|
+
const extra = result.relatedEntries.length - 1;
|
|
545
|
+
lines.push(` ... and ${extra} more similar ${extra === 1 ? 'entry' : 'entries'}`);
|
|
489
546
|
}
|
|
490
547
|
lines.push('');
|
|
491
|
-
lines.push('
|
|
548
|
+
lines.push('If these overlap, consolidate:');
|
|
549
|
+
lines.push(` KEEP+UPDATE: memory_correct(id: "${top.id}", action: "replace", correction: "<merged content>")`);
|
|
550
|
+
lines.push(` DELETE new: memory_correct(id: "${result.id}", action: "delete")`);
|
|
551
|
+
lines.push(WARN_SEPARATOR);
|
|
492
552
|
}
|
|
493
|
-
// Ephemeral content warning —
|
|
553
|
+
// Ephemeral content warning — the formatted block already contains visual borders
|
|
554
|
+
// and pre-filled delete command from formatEphemeralWarning.
|
|
494
555
|
if (result.ephemeralWarning && hintCount < 2) {
|
|
495
556
|
hintCount++;
|
|
496
557
|
lines.push('');
|
|
497
|
-
|
|
558
|
+
if (entryPrefix)
|
|
559
|
+
lines.push(`${entryPrefix}:`);
|
|
560
|
+
lines.push(result.ephemeralWarning);
|
|
498
561
|
}
|
|
499
562
|
// Preference surfacing: show relevant preferences for non-preference entries
|
|
500
563
|
if (result.relevantPreferences && result.relevantPreferences.length > 0 && hintCount < 2) {
|
|
@@ -525,33 +588,26 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
525
588
|
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
526
589
|
}
|
|
527
590
|
case 'memory_query': {
|
|
528
|
-
const { lobe: rawLobe, scope, detail, filter, branch } = z.object({
|
|
591
|
+
const { lobe: rawLobe, scope, detail, filter, branch, isFirstMemoryToolCall: rawIsFirst } = z.object({
|
|
529
592
|
lobe: z.string().optional(),
|
|
530
593
|
scope: z.string().default('*'),
|
|
531
594
|
detail: z.enum(['brief', 'standard', 'full']).default('brief'),
|
|
532
595
|
filter: z.string().optional(),
|
|
533
596
|
branch: z.string().optional(),
|
|
597
|
+
isFirstMemoryToolCall: z.boolean().default(true),
|
|
534
598
|
}).parse(args ?? {});
|
|
535
|
-
|
|
536
|
-
//
|
|
537
|
-
//
|
|
599
|
+
// Force-include alwaysInclude lobes when querying a global topic (user/preferences),
|
|
600
|
+
// regardless of isFirstMemoryToolCall — the agent explicitly asked for this data.
|
|
601
|
+
// Philosophy: "Determinism over cleverness" — same query produces same results.
|
|
602
|
+
const topicScope = parseTopicScope(scope);
|
|
603
|
+
const effectiveIsFirst = rawIsFirst || (topicScope !== null && ALWAYS_INCLUDE_WRITE_TOPICS.has(topicScope));
|
|
604
|
+
// Resolve which lobes to search — unified path for all topics.
|
|
538
605
|
let lobeEntries = [];
|
|
539
|
-
const entryLobeMap = new Map(); // entry id → lobe name
|
|
606
|
+
const entryLobeMap = new Map(); // entry id → lobe name
|
|
540
607
|
let label;
|
|
541
608
|
let primaryStore;
|
|
542
|
-
let
|
|
543
|
-
if (
|
|
544
|
-
const ctx = resolveToolContext(rawLobe, { isGlobal: true });
|
|
545
|
-
if (!ctx.ok)
|
|
546
|
-
return contextError(ctx);
|
|
547
|
-
label = ctx.label;
|
|
548
|
-
primaryStore = ctx.store;
|
|
549
|
-
const result = await ctx.store.query(scope, detail, filter, branch);
|
|
550
|
-
for (const e of result.entries)
|
|
551
|
-
entryLobeMap.set(e.id, 'global');
|
|
552
|
-
lobeEntries = [...result.entries];
|
|
553
|
-
}
|
|
554
|
-
else if (rawLobe) {
|
|
609
|
+
let queryGlobalOnlyHint;
|
|
610
|
+
if (rawLobe) {
|
|
555
611
|
const ctx = resolveToolContext(rawLobe);
|
|
556
612
|
if (!ctx.ok)
|
|
557
613
|
return contextError(ctx);
|
|
@@ -561,33 +617,35 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
561
617
|
lobeEntries = [...result.entries];
|
|
562
618
|
}
|
|
563
619
|
else {
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
620
|
+
const resolution = await resolveLobesForRead(effectiveIsFirst);
|
|
621
|
+
switch (resolution.kind) {
|
|
622
|
+
case 'resolved': {
|
|
623
|
+
label = resolution.label;
|
|
624
|
+
for (const lobeName of resolution.lobes) {
|
|
625
|
+
const store = configManager.getStore(lobeName);
|
|
626
|
+
if (!store)
|
|
627
|
+
continue;
|
|
628
|
+
if (!primaryStore)
|
|
629
|
+
primaryStore = store;
|
|
630
|
+
const result = await store.query(scope, detail, filter, branch);
|
|
631
|
+
if (resolution.lobes.length > 1) {
|
|
632
|
+
for (const e of result.entries)
|
|
633
|
+
entryLobeMap.set(e.id, lobeName);
|
|
634
|
+
}
|
|
635
|
+
lobeEntries.push(...result.entries);
|
|
636
|
+
}
|
|
637
|
+
break;
|
|
638
|
+
}
|
|
639
|
+
case 'global-only': {
|
|
640
|
+
label = 'global';
|
|
641
|
+
queryGlobalOnlyHint = resolution.hint;
|
|
642
|
+
break;
|
|
643
|
+
}
|
|
578
644
|
}
|
|
579
645
|
}
|
|
580
|
-
//
|
|
581
|
-
let globalEntries = [];
|
|
582
|
-
if (scope === '*' && !isGlobalQuery) {
|
|
583
|
-
const globalResult = await globalStore.query('*', detail, filter);
|
|
584
|
-
for (const e of globalResult.entries)
|
|
585
|
-
entryLobeMap.set(e.id, 'global');
|
|
586
|
-
globalEntries = [...globalResult.entries];
|
|
587
|
-
}
|
|
588
|
-
// Merge global + lobe entries, dedupe by id, sort by relevance score
|
|
646
|
+
// Dedupe by id, sort by relevance score
|
|
589
647
|
const seenQueryIds = new Set();
|
|
590
|
-
const allEntries =
|
|
648
|
+
const allEntries = lobeEntries
|
|
591
649
|
.filter(e => {
|
|
592
650
|
if (seenQueryIds.has(e.id))
|
|
593
651
|
return false;
|
|
@@ -597,46 +655,32 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
597
655
|
.sort((a, b) => b.relevanceScore - a.relevanceScore);
|
|
598
656
|
// Build stores collection for tag frequency aggregation
|
|
599
657
|
const searchedStores = [];
|
|
600
|
-
if (
|
|
601
|
-
searchedStores.push(
|
|
602
|
-
}
|
|
603
|
-
else if (rawLobe) {
|
|
604
|
-
const store = configManager.getStore(rawLobe);
|
|
605
|
-
if (store)
|
|
606
|
-
searchedStores.push(store);
|
|
607
|
-
}
|
|
608
|
-
else {
|
|
609
|
-
// All lobes + global when doing wildcard search
|
|
610
|
-
for (const lobeName of configManager.getLobeNames()) {
|
|
611
|
-
const store = configManager.getStore(lobeName);
|
|
612
|
-
if (store)
|
|
613
|
-
searchedStores.push(store);
|
|
614
|
-
}
|
|
615
|
-
if (scope === '*')
|
|
616
|
-
searchedStores.push(globalStore);
|
|
617
|
-
}
|
|
658
|
+
if (primaryStore)
|
|
659
|
+
searchedStores.push(primaryStore);
|
|
618
660
|
const tagFreq = mergeTagFrequencies(searchedStores);
|
|
619
661
|
// Parse filter once for both filtering (already done) and footer display
|
|
620
662
|
const filterGroups = filter ? parseFilter(filter) : [];
|
|
621
663
|
if (allEntries.length === 0) {
|
|
622
664
|
const footer = buildQueryFooter({ filterGroups, rawFilter: filter, tagFreq, resultCount: 0, scope });
|
|
665
|
+
const noResultHint = queryGlobalOnlyHint ? `\n\n> ${queryGlobalOnlyHint}` : '';
|
|
623
666
|
return {
|
|
624
667
|
content: [{
|
|
625
668
|
type: 'text',
|
|
626
|
-
text: `[${label}] No entries found for scope "${scope}"${filter ? ` with filter "${filter}"` : ''}
|
|
669
|
+
text: `[${label}] No entries found for scope "${scope}"${filter ? ` with filter "${filter}"` : ''}.${noResultHint}\n\n---\n${footer}`,
|
|
627
670
|
}],
|
|
628
671
|
};
|
|
629
672
|
}
|
|
673
|
+
const showQueryLobeLabels = entryLobeMap.size > 0;
|
|
630
674
|
const lines = allEntries.map(e => {
|
|
631
675
|
const freshIndicator = e.fresh ? '' : ' [stale]';
|
|
632
|
-
const lobeTag =
|
|
676
|
+
const lobeTag = showQueryLobeLabels ? ` [${entryLobeMap.get(e.id) ?? '?'}]` : '';
|
|
633
677
|
if (detail === 'brief') {
|
|
634
678
|
return `- **${e.title}** (${e.id}${lobeTag}, confidence: ${e.confidence})${freshIndicator}\n ${e.summary}`;
|
|
635
679
|
}
|
|
636
680
|
if (detail === 'full') {
|
|
637
681
|
const meta = [
|
|
638
682
|
`ID: ${e.id}`,
|
|
639
|
-
|
|
683
|
+
showQueryLobeLabels ? `Lobe: ${entryLobeMap.get(e.id) ?? '?'}` : null,
|
|
640
684
|
`Confidence: ${e.confidence}`,
|
|
641
685
|
`Trust: ${e.trust}`,
|
|
642
686
|
`Fresh: ${e.fresh}`,
|
|
@@ -670,6 +714,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
670
714
|
text += '\n\n' + formatConflictWarning(conflicts);
|
|
671
715
|
}
|
|
672
716
|
}
|
|
717
|
+
// Surface hint when we fell back to global-only
|
|
718
|
+
if (queryGlobalOnlyHint) {
|
|
719
|
+
text += `\n\n> ${queryGlobalOnlyHint}`;
|
|
720
|
+
}
|
|
673
721
|
// Build footer with query mode, tag vocabulary, and syntax reference
|
|
674
722
|
const footer = buildQueryFooter({ filterGroups, rawFilter: filter, tagFreq, resultCount: allEntries.length, scope });
|
|
675
723
|
text += `\n\n---\n${footer}`;
|
|
@@ -696,26 +744,45 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
696
744
|
isError: true,
|
|
697
745
|
};
|
|
698
746
|
}
|
|
699
|
-
// Resolve store —
|
|
700
|
-
|
|
701
|
-
|
|
747
|
+
// Resolve store — if no lobe specified, probe alwaysInclude lobes first (read-only)
|
|
748
|
+
// to find where user/pref entries live, then apply the correction only to the owning store.
|
|
749
|
+
// Philosophy: "Prefer atomicity for correctness" — never call correct() speculatively.
|
|
750
|
+
let effectiveCorrectLobe = rawLobe;
|
|
751
|
+
if (!effectiveCorrectLobe) {
|
|
752
|
+
const alwaysIncludeLobes = configManager.getAlwaysIncludeLobes();
|
|
753
|
+
for (const lobeName of alwaysIncludeLobes) {
|
|
754
|
+
const store = configManager.getStore(lobeName);
|
|
755
|
+
if (!store)
|
|
756
|
+
continue;
|
|
757
|
+
try {
|
|
758
|
+
if (await store.hasEntry(id)) {
|
|
759
|
+
effectiveCorrectLobe = lobeName;
|
|
760
|
+
break;
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
catch (err) {
|
|
764
|
+
process.stderr.write(`[memory-mcp] Warning: hasEntry probe failed for lobe "${lobeName}": ${err instanceof Error ? err.message : String(err)}\n`);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
// If we probed alwaysInclude lobes and didn't find the entry, provide a richer error
|
|
769
|
+
// than the generic "Lobe is required" from resolveToolContext.
|
|
770
|
+
if (!effectiveCorrectLobe && !rawLobe) {
|
|
771
|
+
const searchedLobes = configManager.getAlwaysIncludeLobes();
|
|
772
|
+
const allLobes = configManager.getLobeNames();
|
|
773
|
+
const searchedNote = searchedLobes.length > 0
|
|
774
|
+
? `Searched alwaysInclude lobes (${searchedLobes.join(', ')}) — entry not found. `
|
|
775
|
+
: '';
|
|
776
|
+
return {
|
|
777
|
+
content: [{ type: 'text', text: `Entry "${id}" not found. ${searchedNote}Specify the lobe that contains it. Available: ${allLobes.join(', ')}` }],
|
|
778
|
+
isError: true,
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
const ctx = resolveToolContext(effectiveCorrectLobe);
|
|
702
782
|
if (!ctx.ok)
|
|
703
783
|
return contextError(ctx);
|
|
704
784
|
const result = await ctx.store.correct(id, correction ?? '', action);
|
|
705
785
|
if (!result.corrected) {
|
|
706
|
-
// If not found in the targeted store, try the other one as fallback
|
|
707
|
-
if (isGlobalEntry) {
|
|
708
|
-
const lobeCtx = resolveToolContext(rawLobe);
|
|
709
|
-
if (lobeCtx.ok) {
|
|
710
|
-
const lobeResult = await lobeCtx.store.correct(id, correction ?? '', action);
|
|
711
|
-
if (lobeResult.corrected) {
|
|
712
|
-
const text = action === 'delete'
|
|
713
|
-
? `[${lobeCtx.label}] Deleted entry ${id}.`
|
|
714
|
-
: `[${lobeCtx.label}] Corrected entry ${id} (action: ${action}, confidence: ${lobeResult.newConfidence}, trust: ${lobeResult.trust}).`;
|
|
715
|
-
return { content: [{ type: 'text', text }] };
|
|
716
|
-
}
|
|
717
|
-
}
|
|
718
|
-
}
|
|
719
786
|
return {
|
|
720
787
|
content: [{ type: 'text', text: `[${ctx.label}] Failed to correct: ${result.error}` }],
|
|
721
788
|
isError: true,
|
|
@@ -737,11 +804,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
737
804
|
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
738
805
|
}
|
|
739
806
|
case 'memory_context': {
|
|
740
|
-
const { lobe: rawLobe, context, maxResults, minMatch } = z.object({
|
|
807
|
+
const { lobe: rawLobe, context, maxResults, minMatch, isFirstMemoryToolCall: rawIsFirst } = z.object({
|
|
741
808
|
lobe: z.string().optional(),
|
|
742
809
|
context: z.string().optional(),
|
|
743
810
|
maxResults: z.number().optional(),
|
|
744
811
|
minMatch: z.number().min(0).max(1).optional(),
|
|
812
|
+
isFirstMemoryToolCall: z.boolean().default(true),
|
|
745
813
|
}).parse(args ?? {});
|
|
746
814
|
// --- Briefing mode: no context provided → user + preferences + stale nudges ---
|
|
747
815
|
if (!context) {
|
|
@@ -758,22 +826,18 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
758
826
|
const degradedSection = degradedLobeNames.length > 0
|
|
759
827
|
? `## ⚠ Degraded Lobes: ${degradedLobeNames.join(', ')}\nRun **memory_diagnose** for details.\n`
|
|
760
828
|
: '';
|
|
761
|
-
// Global store holds user + preferences — always included
|
|
762
|
-
const globalBriefing = await globalStore.briefing(300);
|
|
763
829
|
const sections = [];
|
|
764
830
|
if (crashSection)
|
|
765
831
|
sections.push(crashSection);
|
|
766
832
|
if (degradedSection)
|
|
767
833
|
sections.push(degradedSection);
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
}
|
|
771
|
-
// Collect stale entries and entry counts across all lobes
|
|
834
|
+
// Collect briefing, stale entries, and entry counts across all lobes
|
|
835
|
+
// (alwaysInclude lobes are in the lobe list — no separate global store query needed)
|
|
772
836
|
const allStale = [];
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
837
|
+
let totalEntries = 0;
|
|
838
|
+
let totalStale = 0;
|
|
839
|
+
// Give alwaysInclude lobes a higher token budget (identity/preferences are high-value)
|
|
840
|
+
const alwaysIncludeSet = new Set(configManager.getAlwaysIncludeLobes());
|
|
777
841
|
for (const lobeName of allBriefingLobeNames) {
|
|
778
842
|
const health = configManager.getLobeHealth(lobeName);
|
|
779
843
|
if (health?.status === 'degraded')
|
|
@@ -781,7 +845,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
781
845
|
const store = configManager.getStore(lobeName);
|
|
782
846
|
if (!store)
|
|
783
847
|
continue;
|
|
784
|
-
const
|
|
848
|
+
const budget = alwaysIncludeSet.has(lobeName) ? 300 : 100;
|
|
849
|
+
const lobeBriefing = await store.briefing(budget);
|
|
850
|
+
if (alwaysIncludeSet.has(lobeName) && lobeBriefing.entryCount > 0) {
|
|
851
|
+
sections.push(lobeBriefing.briefing);
|
|
852
|
+
}
|
|
785
853
|
if (lobeBriefing.staleDetails)
|
|
786
854
|
allStale.push(...lobeBriefing.staleDetails);
|
|
787
855
|
totalEntries += lobeBriefing.entryCount;
|
|
@@ -794,7 +862,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
794
862
|
sections.push('No knowledge stored yet. As you work, store observations with memory_store. Try memory_bootstrap to seed initial knowledge from the repo.');
|
|
795
863
|
}
|
|
796
864
|
// Tag primer: show tag vocabulary if tags exist across any lobe
|
|
797
|
-
const briefingStores = [
|
|
865
|
+
const briefingStores = [];
|
|
798
866
|
for (const lobeName of allBriefingLobeNames) {
|
|
799
867
|
const store = configManager.getStore(lobeName);
|
|
800
868
|
if (store)
|
|
@@ -822,7 +890,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
822
890
|
const ctxEntryLobeMap = new Map(); // entry id → lobe name
|
|
823
891
|
let label;
|
|
824
892
|
let primaryStore;
|
|
825
|
-
let
|
|
893
|
+
let ctxGlobalOnlyHint;
|
|
826
894
|
if (rawLobe) {
|
|
827
895
|
const ctx = resolveToolContext(rawLobe);
|
|
828
896
|
if (!ctx.ok)
|
|
@@ -833,44 +901,35 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
833
901
|
allLobeResults.push(...lobeResults);
|
|
834
902
|
}
|
|
835
903
|
else {
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
// Minimum keyword matches required to avoid the penalty (at least 40% of context, min 2)
|
|
860
|
-
const minMatchCount = Math.max(2, Math.ceil(contextKwCount * CROSS_LOBE_MIN_MATCH_RATIO));
|
|
861
|
-
for (let i = 0; i < allLobeResults.length; i++) {
|
|
862
|
-
if (allLobeResults[i].matchedKeywords.length < minMatchCount) {
|
|
863
|
-
allLobeResults[i] = { ...allLobeResults[i], score: allLobeResults[i].score * CROSS_LOBE_WEAK_SCORE_PENALTY };
|
|
904
|
+
const resolution = await resolveLobesForRead(rawIsFirst);
|
|
905
|
+
switch (resolution.kind) {
|
|
906
|
+
case 'resolved': {
|
|
907
|
+
label = resolution.label;
|
|
908
|
+
for (const lobeName of resolution.lobes) {
|
|
909
|
+
const store = configManager.getStore(lobeName);
|
|
910
|
+
if (!store)
|
|
911
|
+
continue;
|
|
912
|
+
if (!primaryStore)
|
|
913
|
+
primaryStore = store;
|
|
914
|
+
const lobeResults = await store.contextSearch(context, max, undefined, threshold);
|
|
915
|
+
if (resolution.lobes.length > 1) {
|
|
916
|
+
for (const r of lobeResults)
|
|
917
|
+
ctxEntryLobeMap.set(r.entry.id, lobeName);
|
|
918
|
+
}
|
|
919
|
+
allLobeResults.push(...lobeResults);
|
|
920
|
+
}
|
|
921
|
+
break;
|
|
922
|
+
}
|
|
923
|
+
case 'global-only': {
|
|
924
|
+
label = 'global';
|
|
925
|
+
ctxGlobalOnlyHint = resolution.hint;
|
|
926
|
+
break;
|
|
864
927
|
}
|
|
865
928
|
}
|
|
866
929
|
}
|
|
867
|
-
//
|
|
868
|
-
const globalResults = await globalStore.contextSearch(context, max, undefined, threshold);
|
|
869
|
-
for (const r of globalResults)
|
|
870
|
-
ctxEntryLobeMap.set(r.entry.id, 'global');
|
|
871
|
-
// Merge, dedupe by entry id, re-sort by score, take top N
|
|
930
|
+
// Dedupe by entry id, re-sort by score, take top N
|
|
872
931
|
const seenIds = new Set();
|
|
873
|
-
const results =
|
|
932
|
+
const results = allLobeResults
|
|
874
933
|
.sort((a, b) => b.score - a.score)
|
|
875
934
|
.filter(r => {
|
|
876
935
|
if (seenIds.has(r.entry.id))
|
|
@@ -880,28 +939,21 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
880
939
|
})
|
|
881
940
|
.slice(0, max);
|
|
882
941
|
// Build stores collection for tag frequency aggregation
|
|
883
|
-
const ctxSearchedStores = [
|
|
884
|
-
if (
|
|
885
|
-
|
|
886
|
-
if (store)
|
|
887
|
-
ctxSearchedStores.push(store);
|
|
888
|
-
}
|
|
889
|
-
else {
|
|
890
|
-
for (const lobeName of configManager.getLobeNames()) {
|
|
891
|
-
const store = configManager.getStore(lobeName);
|
|
892
|
-
if (store)
|
|
893
|
-
ctxSearchedStores.push(store);
|
|
894
|
-
}
|
|
895
|
-
}
|
|
942
|
+
const ctxSearchedStores = [];
|
|
943
|
+
if (primaryStore)
|
|
944
|
+
ctxSearchedStores.push(primaryStore);
|
|
896
945
|
const ctxTagFreq = mergeTagFrequencies(ctxSearchedStores);
|
|
897
946
|
// Parse filter for footer (context search has no filter, pass empty)
|
|
898
947
|
const ctxFilterGroups = [];
|
|
899
948
|
if (results.length === 0) {
|
|
900
949
|
const ctxFooter = buildQueryFooter({ filterGroups: ctxFilterGroups, rawFilter: undefined, tagFreq: ctxTagFreq, resultCount: 0, scope: 'context search' });
|
|
950
|
+
const noResultHint = ctxGlobalOnlyHint
|
|
951
|
+
? `\n\n> ${ctxGlobalOnlyHint}`
|
|
952
|
+
: '\n\nThis is fine — proceed without prior context. As you learn things worth remembering, store them with memory_store.';
|
|
901
953
|
return {
|
|
902
954
|
content: [{
|
|
903
955
|
type: 'text',
|
|
904
|
-
text: `[${label}] No relevant knowledge found for: "${context}"\n\
|
|
956
|
+
text: `[${label}] No relevant knowledge found for: "${context}"${noResultHint}\n\n---\n${ctxFooter}`,
|
|
905
957
|
}],
|
|
906
958
|
};
|
|
907
959
|
}
|
|
@@ -919,6 +971,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
919
971
|
...topicOrder.filter(t => byTopic.has(t)),
|
|
920
972
|
...Array.from(byTopic.keys()).filter(t => !topicOrder.includes(t)).sort(),
|
|
921
973
|
];
|
|
974
|
+
const showCtxLobeLabels = ctxEntryLobeMap.size > 0;
|
|
922
975
|
for (const topic of orderedTopics) {
|
|
923
976
|
const topicResults = byTopic.get(topic);
|
|
924
977
|
const heading = topic === 'user' ? 'About You'
|
|
@@ -930,7 +983,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
930
983
|
for (const r of topicResults) {
|
|
931
984
|
const marker = topic === 'gotchas' ? '[!] ' : topic === 'preferences' ? '[pref] ' : '';
|
|
932
985
|
const keywords = r.matchedKeywords.length > 0 ? ` (matched: ${r.matchedKeywords.join(', ')})` : '';
|
|
933
|
-
const lobeLabel =
|
|
986
|
+
const lobeLabel = showCtxLobeLabels ? ` [${ctxEntryLobeMap.get(r.entry.id) ?? '?'}]` : '';
|
|
934
987
|
const tagsSuffix = r.entry.tags?.length ? ` [tags: ${r.entry.tags.join(', ')}]` : '';
|
|
935
988
|
sections.push(`- **${marker}${r.entry.title}**${lobeLabel}: ${r.entry.content}${keywords}${tagsSuffix}`);
|
|
936
989
|
}
|
|
@@ -957,6 +1010,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
957
1010
|
sections.push(`---\n*Context loaded for: ${kwList} (${topicList}). ` +
|
|
958
1011
|
`This knowledge is now in your conversation — no need to call memory_context again for these terms this session.*`);
|
|
959
1012
|
}
|
|
1013
|
+
// Surface hint when we fell back to global-only
|
|
1014
|
+
if (ctxGlobalOnlyHint) {
|
|
1015
|
+
sections.push(`> ${ctxGlobalOnlyHint}`);
|
|
1016
|
+
}
|
|
960
1017
|
// Build footer (context search has no filter — it's natural language keyword matching)
|
|
961
1018
|
const ctxFooter = buildQueryFooter({ filterGroups: ctxFilterGroups, rawFilter: undefined, tagFreq: ctxTagFreq, resultCount: results.length, scope: 'context search' });
|
|
962
1019
|
sections.push(`---\n${ctxFooter}`);
|
|
@@ -966,24 +1023,27 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
966
1023
|
const { lobe: rawLobe } = z.object({
|
|
967
1024
|
lobe: z.string().optional(),
|
|
968
1025
|
}).parse(args ?? {});
|
|
969
|
-
// Always include global stats
|
|
970
|
-
const globalStats = await globalStore.stats();
|
|
971
1026
|
// Single lobe stats
|
|
972
1027
|
if (rawLobe) {
|
|
973
1028
|
const ctx = resolveToolContext(rawLobe);
|
|
974
1029
|
if (!ctx.ok)
|
|
975
1030
|
return contextError(ctx);
|
|
976
1031
|
const result = await ctx.store.stats();
|
|
977
|
-
const
|
|
978
|
-
|
|
1032
|
+
const alwaysIncludeSet = new Set(configManager.getAlwaysIncludeLobes());
|
|
1033
|
+
const label = alwaysIncludeSet.has(rawLobe) ? `${ctx.label} (alwaysInclude)` : ctx.label;
|
|
1034
|
+
return { content: [{ type: 'text', text: formatStats(label, result) }] };
|
|
979
1035
|
}
|
|
980
1036
|
// Combined stats across all lobes
|
|
981
|
-
const sections = [
|
|
1037
|
+
const sections = [];
|
|
982
1038
|
const allLobeNames = configManager.getLobeNames();
|
|
1039
|
+
const alwaysIncludeSet = new Set(configManager.getAlwaysIncludeLobes());
|
|
983
1040
|
for (const lobeName of allLobeNames) {
|
|
984
1041
|
const store = configManager.getStore(lobeName);
|
|
1042
|
+
if (!store)
|
|
1043
|
+
continue;
|
|
985
1044
|
const result = await store.stats();
|
|
986
|
-
|
|
1045
|
+
const label = alwaysIncludeSet.has(lobeName) ? `${lobeName} (alwaysInclude)` : lobeName;
|
|
1046
|
+
sections.push(formatStats(label, result));
|
|
987
1047
|
}
|
|
988
1048
|
return { content: [{ type: 'text', text: sections.join('\n\n---\n\n') }] };
|
|
989
1049
|
}
|
|
@@ -1144,14 +1204,6 @@ async function buildDiagnosticsText(showFullCrashHistory) {
|
|
|
1144
1204
|
}
|
|
1145
1205
|
}
|
|
1146
1206
|
sections.push('');
|
|
1147
|
-
try {
|
|
1148
|
-
const globalStats = await globalStore.stats();
|
|
1149
|
-
sections.push(`- **global store**: ✅ healthy (${globalStats.totalEntries} entries, ${globalStats.storageSize})`);
|
|
1150
|
-
}
|
|
1151
|
-
catch (e) {
|
|
1152
|
-
sections.push(`- **global store**: ❌ error — ${e instanceof Error ? e.message : e}`);
|
|
1153
|
-
}
|
|
1154
|
-
sections.push('');
|
|
1155
1207
|
// Active behavior config — shows effective values and highlights user overrides
|
|
1156
1208
|
sections.push('### Active Behavior Config');
|
|
1157
1209
|
sections.push(formatBehaviorConfigSection(configBehavior));
|
|
@@ -1203,15 +1255,6 @@ async function main() {
|
|
|
1203
1255
|
process.stderr.write(`[memory-mcp] Previous crash detected (${age}s ago): ${previousCrash.type} — ${previousCrash.error}\n`);
|
|
1204
1256
|
process.stderr.write(`[memory-mcp] Crash report will be shown in memory_context and memory_diagnose.\n`);
|
|
1205
1257
|
}
|
|
1206
|
-
// Initialize global store (user + preferences, shared across all lobes)
|
|
1207
|
-
try {
|
|
1208
|
-
await globalStore.init();
|
|
1209
|
-
process.stderr.write(`[memory-mcp] Global store → ${globalMemoryPath}\n`);
|
|
1210
|
-
}
|
|
1211
|
-
catch (error) {
|
|
1212
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
1213
|
-
process.stderr.write(`[memory-mcp] WARNING: Global store init failed: ${msg}\n`);
|
|
1214
|
-
}
|
|
1215
1258
|
// Initialize each lobe independently — a broken lobe shouldn't prevent others from working
|
|
1216
1259
|
let healthyLobes = 0;
|
|
1217
1260
|
for (const [name, config] of lobeConfigs) {
|
|
@@ -1269,46 +1312,6 @@ async function main() {
|
|
|
1269
1312
|
};
|
|
1270
1313
|
process.stderr.write(`[memory-mcp] ⚠ DEGRADED: ${healthyLobes}/${lobeConfigs.size} lobes healthy.\n`);
|
|
1271
1314
|
}
|
|
1272
|
-
// Migrate: move user + preferences entries from lobe stores to global store.
|
|
1273
|
-
// State-driven guard: skip if already completed (marker file present).
|
|
1274
|
-
const migrationMarker = path.join(globalMemoryPath, '.migrated');
|
|
1275
|
-
if (!existsSync(migrationMarker)) {
|
|
1276
|
-
let migrated = 0;
|
|
1277
|
-
for (const [name, store] of stores) {
|
|
1278
|
-
for (const topic of ['user', 'preferences']) {
|
|
1279
|
-
try {
|
|
1280
|
-
const result = await store.query(topic, 'full');
|
|
1281
|
-
for (const entry of result.entries) {
|
|
1282
|
-
try {
|
|
1283
|
-
const globalResult = await globalStore.query(topic, 'full');
|
|
1284
|
-
const alreadyExists = globalResult.entries.some(g => g.title === entry.title);
|
|
1285
|
-
if (!alreadyExists && entry.content) {
|
|
1286
|
-
const trust = parseTrustLevel(entry.trust ?? 'user') ?? 'user';
|
|
1287
|
-
await globalStore.store(topic, entry.title, entry.content, [...(entry.sources ?? [])], trust);
|
|
1288
|
-
process.stderr.write(`[memory-mcp] Migrated ${entry.id} ("${entry.title}") from [${name}] → global\n`);
|
|
1289
|
-
migrated++;
|
|
1290
|
-
}
|
|
1291
|
-
await store.correct(entry.id, '', 'delete');
|
|
1292
|
-
process.stderr.write(`[memory-mcp] Removed ${entry.id} from [${name}] (now in global)\n`);
|
|
1293
|
-
}
|
|
1294
|
-
catch (entryError) {
|
|
1295
|
-
process.stderr.write(`[memory-mcp] Migration error for ${entry.id} in [${name}]: ${entryError}\n`);
|
|
1296
|
-
}
|
|
1297
|
-
}
|
|
1298
|
-
}
|
|
1299
|
-
catch (topicError) {
|
|
1300
|
-
process.stderr.write(`[memory-mcp] Migration error querying ${topic} in [${name}]: ${topicError}\n`);
|
|
1301
|
-
}
|
|
1302
|
-
}
|
|
1303
|
-
}
|
|
1304
|
-
// Write marker atomically — future startups skip this block entirely
|
|
1305
|
-
try {
|
|
1306
|
-
writeFileSync(migrationMarker, new Date().toISOString(), 'utf-8');
|
|
1307
|
-
if (migrated > 0)
|
|
1308
|
-
process.stderr.write(`[memory-mcp] Migration complete: ${migrated} entries moved to global store.\n`);
|
|
1309
|
-
}
|
|
1310
|
-
catch { /* marker write is best-effort */ }
|
|
1311
|
-
}
|
|
1312
1315
|
// Initialize ConfigManager with current config state
|
|
1313
1316
|
configManager = new ConfigManager(configPath, { configs: lobeConfigs, origin: configOrigin }, stores, lobeHealth);
|
|
1314
1317
|
const transport = new StdioServerTransport();
|
|
@@ -1336,7 +1339,12 @@ async function main() {
|
|
|
1336
1339
|
});
|
|
1337
1340
|
await server.connect(transport);
|
|
1338
1341
|
const modeStr = serverMode.kind === 'running' ? '' : ` [${serverMode.kind.toUpperCase()}]`;
|
|
1339
|
-
|
|
1342
|
+
const alwaysIncludeNames = configManager.getAlwaysIncludeLobes();
|
|
1343
|
+
const aiLabel = alwaysIncludeNames.length > 0 ? ` (alwaysInclude: ${alwaysIncludeNames.join(', ')})` : '';
|
|
1344
|
+
if (alwaysIncludeNames.length > 1) {
|
|
1345
|
+
process.stderr.write(`[memory-mcp] Warning: ${alwaysIncludeNames.length} lobes have alwaysInclude: true (${alwaysIncludeNames.join(', ')}). Writes to user/preferences will route to the first one ("${alwaysIncludeNames[0]}"). This is likely a misconfiguration — typically only one lobe should be alwaysInclude.\n`);
|
|
1346
|
+
}
|
|
1347
|
+
process.stderr.write(`[memory-mcp] Server started${modeStr} with ${healthyLobes}/${lobeConfigs.size} lobe(s)${aiLabel}\n`);
|
|
1340
1348
|
// Graceful shutdown on signals
|
|
1341
1349
|
const shutdown = () => {
|
|
1342
1350
|
process.stderr.write('[memory-mcp] Shutting down gracefully.\n');
|