@exaudeus/memory-mcp 1.3.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/index.js +125 -174
- package/dist/lobe-resolution.d.ts +6 -2
- package/dist/lobe-resolution.js +29 -7
- package/dist/store.d.ts +3 -0
- package/dist/store.js +6 -0
- package/dist/types.d.ts +1 -0
- package/package.json +1 -1
package/dist/config-manager.d.ts
CHANGED
|
@@ -27,10 +27,14 @@ export declare class ConfigManager {
|
|
|
27
27
|
private stores;
|
|
28
28
|
private lobeHealth;
|
|
29
29
|
private configMtime;
|
|
30
|
+
/** Cached alwaysInclude lobe names — recomputed atomically on reload. */
|
|
31
|
+
private cachedAlwaysIncludeLobes;
|
|
30
32
|
protected statFile(path: string): Promise<{
|
|
31
33
|
mtimeMs: number;
|
|
32
34
|
}>;
|
|
33
35
|
constructor(configPath: string, initial: LoadedConfig, initialStores: Map<string, MarkdownMemoryStore>, initialHealth: Map<string, LobeHealth>);
|
|
36
|
+
/** Derive alwaysInclude lobe names from config — pure, no side effects. */
|
|
37
|
+
private static computeAlwaysIncludeLobes;
|
|
34
38
|
/**
|
|
35
39
|
* Ensure config is fresh. Call at the start of every tool handler.
|
|
36
40
|
* Stats config file, reloads if mtime changed. Graceful on all errors.
|
|
@@ -46,4 +50,6 @@ export declare class ConfigManager {
|
|
|
46
50
|
getLobeHealth(lobe: string): LobeHealth | undefined;
|
|
47
51
|
getConfigOrigin(): ConfigOrigin;
|
|
48
52
|
getLobeConfig(lobe: string): MemoryConfig | undefined;
|
|
53
|
+
/** Returns lobe names where alwaysInclude is true. Cached; rebuilt atomically on hot-reload. */
|
|
54
|
+
getAlwaysIncludeLobes(): readonly string[];
|
|
49
55
|
}
|
package/dist/config-manager.js
CHANGED
|
@@ -28,6 +28,13 @@ export class ConfigManager {
|
|
|
28
28
|
this.stores = initialStores;
|
|
29
29
|
this.lobeHealth = initialHealth;
|
|
30
30
|
this.configMtime = Date.now(); // Initial mtime (will be updated on first stat)
|
|
31
|
+
this.cachedAlwaysIncludeLobes = ConfigManager.computeAlwaysIncludeLobes(this.lobeConfigs);
|
|
32
|
+
}
|
|
33
|
+
/** Derive alwaysInclude lobe names from config — pure, no side effects. */
|
|
34
|
+
static computeAlwaysIncludeLobes(configs) {
|
|
35
|
+
return Array.from(configs.entries())
|
|
36
|
+
.filter(([, config]) => config.alwaysInclude === true)
|
|
37
|
+
.map(([name]) => name);
|
|
31
38
|
}
|
|
32
39
|
/**
|
|
33
40
|
* Ensure config is fresh. Call at the start of every tool handler.
|
|
@@ -90,12 +97,13 @@ export class ConfigManager {
|
|
|
90
97
|
});
|
|
91
98
|
}
|
|
92
99
|
}
|
|
93
|
-
// Atomic swap
|
|
100
|
+
// Atomic swap — all derived state recomputed together
|
|
94
101
|
this.configOrigin = newConfig.origin;
|
|
95
102
|
this.lobeConfigs = newConfig.configs;
|
|
96
103
|
this.stores = newStores;
|
|
97
104
|
this.lobeHealth = newHealth;
|
|
98
105
|
this.configMtime = newMtime;
|
|
106
|
+
this.cachedAlwaysIncludeLobes = ConfigManager.computeAlwaysIncludeLobes(newConfig.configs);
|
|
99
107
|
const lobeCount = newConfig.configs.size;
|
|
100
108
|
const degradedCount = Array.from(newHealth.values()).filter(h => h.status === 'degraded').length;
|
|
101
109
|
const timestamp = new Date().toISOString();
|
|
@@ -123,4 +131,8 @@ export class ConfigManager {
|
|
|
123
131
|
getLobeConfig(lobe) {
|
|
124
132
|
return this.lobeConfigs.get(lobe);
|
|
125
133
|
}
|
|
134
|
+
/** Returns lobe names where alwaysInclude is true. Cached; rebuilt atomically on hot-reload. */
|
|
135
|
+
getAlwaysIncludeLobes() {
|
|
136
|
+
return this.cachedAlwaysIncludeLobes;
|
|
137
|
+
}
|
|
126
138
|
}
|
package/dist/config.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
//
|
|
3
3
|
// Priority: memory-config.json → env vars → single-repo default
|
|
4
4
|
// Graceful degradation: each source falls through to the next on failure.
|
|
5
|
-
import { readFileSync } from 'fs';
|
|
5
|
+
import { readFileSync, existsSync, readdirSync } from 'fs';
|
|
6
6
|
import { execFileSync } from 'child_process';
|
|
7
7
|
import path from 'path';
|
|
8
8
|
import os from 'os';
|
|
@@ -81,6 +81,42 @@ function resolveMemoryPath(repoRoot, workspaceName, explicitMemoryDir) {
|
|
|
81
81
|
}
|
|
82
82
|
return path.join(os.homedir(), '.memory-mcp', workspaceName);
|
|
83
83
|
}
|
|
84
|
+
/** If no lobe has alwaysInclude: true AND the legacy global store directory has actual entries,
|
|
85
|
+
* auto-create a "global" lobe pointing to it. Protects existing users who haven't updated their config.
|
|
86
|
+
* Only fires when the dir contains .md files — an empty dir doesn't trigger creation. */
|
|
87
|
+
function ensureAlwaysIncludeLobe(configs, behavior) {
|
|
88
|
+
const hasAlwaysInclude = Array.from(configs.values()).some(c => c.alwaysInclude);
|
|
89
|
+
if (hasAlwaysInclude)
|
|
90
|
+
return;
|
|
91
|
+
// Don't overwrite a user-defined "global" lobe — warn instead.
|
|
92
|
+
// Philosophy: "Make illegal states unrepresentable" — silently replacing config is a hidden state.
|
|
93
|
+
if (configs.has('global')) {
|
|
94
|
+
process.stderr.write(`[memory-mcp] Lobe "global" exists but has no alwaysInclude flag. ` +
|
|
95
|
+
`Add "alwaysInclude": true to your global lobe config to include it in all reads.\n`);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const globalPath = path.join(os.homedir(), '.memory-mcp', 'global');
|
|
99
|
+
if (!existsSync(globalPath))
|
|
100
|
+
return;
|
|
101
|
+
// Only auto-create if the dir has actual memory entries (not just an empty directory)
|
|
102
|
+
try {
|
|
103
|
+
const files = readdirSync(globalPath);
|
|
104
|
+
if (!files.some(f => f.endsWith('.md')))
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
process.stderr.write(`[memory-mcp] Warning: could not read legacy global store at ${globalPath}: ${err}\n`);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
configs.set('global', {
|
|
112
|
+
repoRoot: os.homedir(),
|
|
113
|
+
memoryPath: globalPath,
|
|
114
|
+
storageBudgetBytes: DEFAULT_STORAGE_BUDGET_BYTES,
|
|
115
|
+
alwaysInclude: true,
|
|
116
|
+
behavior,
|
|
117
|
+
});
|
|
118
|
+
process.stderr.write(`[memory-mcp] Auto-created "global" lobe (alwaysInclude) from existing ${globalPath}\n`);
|
|
119
|
+
}
|
|
84
120
|
/** Load lobe configs with priority: memory-config.json -> env vars -> single-repo default */
|
|
85
121
|
export function getLobeConfigs() {
|
|
86
122
|
const configs = new Map();
|
|
@@ -105,13 +141,15 @@ export function getLobeConfigs() {
|
|
|
105
141
|
repoRoot,
|
|
106
142
|
memoryPath: resolveMemoryPath(repoRoot, name, config.memoryDir),
|
|
107
143
|
storageBudgetBytes: (config.budgetMB ?? 2) * 1024 * 1024,
|
|
144
|
+
alwaysInclude: config.alwaysInclude ?? false,
|
|
108
145
|
behavior,
|
|
109
146
|
});
|
|
110
147
|
}
|
|
111
148
|
if (configs.size > 0) {
|
|
149
|
+
// Reuse the already-parsed behavior config for the alwaysInclude fallback
|
|
150
|
+
const resolvedBehavior = external.behavior ? behavior : undefined;
|
|
151
|
+
ensureAlwaysIncludeLobe(configs, resolvedBehavior);
|
|
112
152
|
process.stderr.write(`[memory-mcp] Loaded ${configs.size} lobe(s) from memory-config.json\n`);
|
|
113
|
-
// Pass resolved behavior at the top-level so diagnostics can surface active values
|
|
114
|
-
const resolvedBehavior = external.behavior ? parseBehaviorConfig(external.behavior) : undefined;
|
|
115
153
|
return { configs, origin: { source: 'file', path: configPath }, behavior: resolvedBehavior };
|
|
116
154
|
}
|
|
117
155
|
}
|
|
@@ -137,9 +175,11 @@ export function getLobeConfigs() {
|
|
|
137
175
|
repoRoot,
|
|
138
176
|
memoryPath: resolveMemoryPath(repoRoot, name, explicitDir),
|
|
139
177
|
storageBudgetBytes: storageBudget,
|
|
178
|
+
alwaysInclude: false,
|
|
140
179
|
});
|
|
141
180
|
}
|
|
142
181
|
if (configs.size > 0) {
|
|
182
|
+
ensureAlwaysIncludeLobe(configs);
|
|
143
183
|
process.stderr.write(`[memory-mcp] Loaded ${configs.size} lobe(s) from MEMORY_MCP_WORKSPACES env var\n`);
|
|
144
184
|
return { configs, origin: { source: 'env' } };
|
|
145
185
|
}
|
|
@@ -156,7 +196,9 @@ export function getLobeConfigs() {
|
|
|
156
196
|
repoRoot,
|
|
157
197
|
memoryPath: resolveMemoryPath(repoRoot, 'default', explicitDir),
|
|
158
198
|
storageBudgetBytes: storageBudget,
|
|
199
|
+
alwaysInclude: false,
|
|
159
200
|
});
|
|
201
|
+
// No ensureAlwaysIncludeLobe here — single-repo default users have everything in one lobe
|
|
160
202
|
process.stderr.write(`[memory-mcp] Using single-lobe default mode (cwd: ${repoRoot})\n`);
|
|
161
203
|
return { configs, origin: { source: 'default' } };
|
|
162
204
|
}
|
package/dist/index.js
CHANGED
|
@@ -7,11 +7,9 @@ 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';
|
|
@@ -72,21 +70,12 @@ const stores = new Map();
|
|
|
72
70
|
const lobeNames = Array.from(lobeConfigs.keys());
|
|
73
71
|
// ConfigManager will be initialized after stores are set up
|
|
74
72
|
let configManager;
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
const
|
|
78
|
-
const globalStore = new MarkdownMemoryStore({
|
|
79
|
-
repoRoot: os.homedir(),
|
|
80
|
-
memoryPath: globalMemoryPath,
|
|
81
|
-
storageBudgetBytes: DEFAULT_STORAGE_BUDGET_BYTES,
|
|
82
|
-
});
|
|
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']);
|
|
83
76
|
/** Resolve a raw lobe name to a validated store + display label.
|
|
84
77
|
* After this call, consumers use ctx.label — the raw lobe is not in scope. */
|
|
85
|
-
function resolveToolContext(rawLobe
|
|
86
|
-
// Global topics always route to the global store
|
|
87
|
-
if (opts?.isGlobal) {
|
|
88
|
-
return { ok: true, store: globalStore, label: 'global' };
|
|
89
|
-
}
|
|
78
|
+
function resolveToolContext(rawLobe) {
|
|
90
79
|
const lobeNames = configManager.getLobeNames();
|
|
91
80
|
// Default to single lobe when omitted
|
|
92
81
|
const lobe = rawLobe || (lobeNames.length === 1 ? lobeNames[0] : undefined);
|
|
@@ -163,11 +152,13 @@ const server = new Server({ name: 'memory-mcp', version: '0.1.0' }, { capabiliti
|
|
|
163
152
|
// 3. Fallback → global-only with a hint to specify the lobe
|
|
164
153
|
/** Resolve which lobes to search for a read operation when the agent omitted the lobe param.
|
|
165
154
|
* Wires the MCP server's listRoots into the pure resolution logic. */
|
|
166
|
-
async function resolveLobesForRead() {
|
|
155
|
+
async function resolveLobesForRead(isFirstMemoryToolCall = true) {
|
|
167
156
|
const allLobeNames = configManager.getLobeNames();
|
|
168
|
-
|
|
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.
|
|
169
160
|
if (allLobeNames.length === 1) {
|
|
170
|
-
return buildLobeResolution(allLobeNames, allLobeNames);
|
|
161
|
+
return buildLobeResolution(allLobeNames, allLobeNames, alwaysIncludeLobes, isFirstMemoryToolCall);
|
|
171
162
|
}
|
|
172
163
|
// Multiple lobes — try MCP client roots
|
|
173
164
|
const clientCaps = server.getClientCapabilities();
|
|
@@ -182,7 +173,7 @@ async function resolveLobesForRead() {
|
|
|
182
173
|
})
|
|
183
174
|
.filter((c) => c !== undefined);
|
|
184
175
|
const matched = matchRootsToLobeNames(roots, lobeConfigs);
|
|
185
|
-
return buildLobeResolution(allLobeNames, matched);
|
|
176
|
+
return buildLobeResolution(allLobeNames, matched, alwaysIncludeLobes, isFirstMemoryToolCall);
|
|
186
177
|
}
|
|
187
178
|
}
|
|
188
179
|
catch (err) {
|
|
@@ -190,7 +181,7 @@ async function resolveLobesForRead() {
|
|
|
190
181
|
}
|
|
191
182
|
}
|
|
192
183
|
// Fallback — roots not available or no match
|
|
193
|
-
return buildLobeResolution(allLobeNames, []);
|
|
184
|
+
return buildLobeResolution(allLobeNames, [], alwaysIncludeLobes, isFirstMemoryToolCall);
|
|
194
185
|
}
|
|
195
186
|
/** Build the shared lobe property for tool schemas — called on each ListTools request
|
|
196
187
|
* so the description and enum stay in sync after a hot-reload adds or removes lobes. */
|
|
@@ -200,7 +191,7 @@ function buildLobeProperty(currentLobeNames) {
|
|
|
200
191
|
type: 'string',
|
|
201
192
|
description: isSingle
|
|
202
193
|
? `Memory lobe name (defaults to "${currentLobeNames[0]}" if omitted)`
|
|
203
|
-
: `Memory lobe name. When omitted for reads, the server uses the client's workspace roots to select the matching lobe. If roots are unavailable
|
|
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(', ')}`,
|
|
204
195
|
enum: currentLobeNames.length > 1 ? [...currentLobeNames] : undefined,
|
|
205
196
|
};
|
|
206
197
|
}
|
|
@@ -224,7 +215,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
224
215
|
// Example comes first — agents form their call shape from the first concrete pattern they see.
|
|
225
216
|
// "entries" (not "content") signals a collection; fighting the "content = string" prior
|
|
226
217
|
// is an architectural fix rather than patching the description after the fact.
|
|
227
|
-
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.',
|
|
228
219
|
inputSchema: {
|
|
229
220
|
type: 'object',
|
|
230
221
|
properties: {
|
|
@@ -310,6 +301,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
310
301
|
type: 'string',
|
|
311
302
|
description: 'Branch for recent-work. Omit = current branch, "*" = all branches.',
|
|
312
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
|
+
},
|
|
313
309
|
},
|
|
314
310
|
required: [],
|
|
315
311
|
},
|
|
@@ -359,6 +355,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
359
355
|
description: 'Min keyword match ratio 0-1 (default: 0.2). Higher = stricter.',
|
|
360
356
|
default: 0.2,
|
|
361
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
|
+
},
|
|
362
363
|
},
|
|
363
364
|
required: [],
|
|
364
365
|
},
|
|
@@ -426,16 +427,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
426
427
|
case 'memory_list_lobes': {
|
|
427
428
|
// Delegates to shared builder — same data as memory://lobes resource
|
|
428
429
|
const lobeInfo = await buildLobeInfo();
|
|
429
|
-
const
|
|
430
|
+
const alwaysIncludeNames = configManager.getAlwaysIncludeLobes();
|
|
430
431
|
const result = {
|
|
431
432
|
serverMode: serverMode.kind,
|
|
432
|
-
globalStore: {
|
|
433
|
-
memoryPath: globalMemoryPath,
|
|
434
|
-
entries: globalStats.totalEntries,
|
|
435
|
-
storageUsed: globalStats.storageSize,
|
|
436
|
-
topics: 'user, preferences (shared across all lobes)',
|
|
437
|
-
},
|
|
438
433
|
lobes: lobeInfo,
|
|
434
|
+
alwaysIncludeLobes: alwaysIncludeNames,
|
|
439
435
|
configFile: configFileDisplay(),
|
|
440
436
|
configSource: configOrigin.source,
|
|
441
437
|
totalLobes: lobeInfo.length,
|
|
@@ -477,12 +473,25 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
477
473
|
const allPaths = [...sources, ...references];
|
|
478
474
|
effectiveLobe = inferLobeFromPaths(allPaths);
|
|
479
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
|
+
}
|
|
480
484
|
// Resolve store — after this point, rawLobe is never used again
|
|
481
|
-
const
|
|
482
|
-
const ctx = resolveToolContext(effectiveLobe, { isGlobal });
|
|
485
|
+
const ctx = resolveToolContext(effectiveLobe);
|
|
483
486
|
if (!ctx.ok)
|
|
484
487
|
return contextError(ctx);
|
|
485
|
-
|
|
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;
|
|
486
495
|
const storedResults = [];
|
|
487
496
|
for (const { title, fact } of rawEntries) {
|
|
488
497
|
const result = await ctx.store.store(topic, title, fact, sources, effectiveTrust, references, rawTags);
|
|
@@ -579,33 +588,26 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
579
588
|
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
580
589
|
}
|
|
581
590
|
case 'memory_query': {
|
|
582
|
-
const { lobe: rawLobe, scope, detail, filter, branch } = z.object({
|
|
591
|
+
const { lobe: rawLobe, scope, detail, filter, branch, isFirstMemoryToolCall: rawIsFirst } = z.object({
|
|
583
592
|
lobe: z.string().optional(),
|
|
584
593
|
scope: z.string().default('*'),
|
|
585
594
|
detail: z.enum(['brief', 'standard', 'full']).default('brief'),
|
|
586
595
|
filter: z.string().optional(),
|
|
587
596
|
branch: z.string().optional(),
|
|
597
|
+
isFirstMemoryToolCall: z.boolean().default(true),
|
|
588
598
|
}).parse(args ?? {});
|
|
589
|
-
|
|
590
|
-
//
|
|
591
|
-
//
|
|
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.
|
|
592
605
|
let lobeEntries = [];
|
|
593
606
|
const entryLobeMap = new Map(); // entry id → lobe name
|
|
594
607
|
let label;
|
|
595
608
|
let primaryStore;
|
|
596
609
|
let queryGlobalOnlyHint;
|
|
597
|
-
if (
|
|
598
|
-
const ctx = resolveToolContext(rawLobe, { isGlobal: true });
|
|
599
|
-
if (!ctx.ok)
|
|
600
|
-
return contextError(ctx);
|
|
601
|
-
label = ctx.label;
|
|
602
|
-
primaryStore = ctx.store;
|
|
603
|
-
const result = await ctx.store.query(scope, detail, filter, branch);
|
|
604
|
-
for (const e of result.entries)
|
|
605
|
-
entryLobeMap.set(e.id, 'global');
|
|
606
|
-
lobeEntries = [...result.entries];
|
|
607
|
-
}
|
|
608
|
-
else if (rawLobe) {
|
|
610
|
+
if (rawLobe) {
|
|
609
611
|
const ctx = resolveToolContext(rawLobe);
|
|
610
612
|
if (!ctx.ok)
|
|
611
613
|
return contextError(ctx);
|
|
@@ -615,7 +617,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
615
617
|
lobeEntries = [...result.entries];
|
|
616
618
|
}
|
|
617
619
|
else {
|
|
618
|
-
const resolution = await resolveLobesForRead();
|
|
620
|
+
const resolution = await resolveLobesForRead(effectiveIsFirst);
|
|
619
621
|
switch (resolution.kind) {
|
|
620
622
|
case 'resolved': {
|
|
621
623
|
label = resolution.label;
|
|
@@ -641,17 +643,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
641
643
|
}
|
|
642
644
|
}
|
|
643
645
|
}
|
|
644
|
-
//
|
|
645
|
-
let globalEntries = [];
|
|
646
|
-
if (scope === '*' && !isGlobalQuery) {
|
|
647
|
-
const globalResult = await globalStore.query('*', detail, filter);
|
|
648
|
-
for (const e of globalResult.entries)
|
|
649
|
-
entryLobeMap.set(e.id, 'global');
|
|
650
|
-
globalEntries = [...globalResult.entries];
|
|
651
|
-
}
|
|
652
|
-
// Merge global + lobe entries, dedupe by id, sort by relevance score
|
|
646
|
+
// Dedupe by id, sort by relevance score
|
|
653
647
|
const seenQueryIds = new Set();
|
|
654
|
-
const allEntries =
|
|
648
|
+
const allEntries = lobeEntries
|
|
655
649
|
.filter(e => {
|
|
656
650
|
if (seenQueryIds.has(e.id))
|
|
657
651
|
return false;
|
|
@@ -660,17 +654,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
660
654
|
})
|
|
661
655
|
.sort((a, b) => b.relevanceScore - a.relevanceScore);
|
|
662
656
|
// Build stores collection for tag frequency aggregation
|
|
663
|
-
// Only include stores that were actually searched
|
|
664
657
|
const searchedStores = [];
|
|
665
|
-
if (
|
|
666
|
-
searchedStores.push(
|
|
667
|
-
}
|
|
668
|
-
else {
|
|
669
|
-
if (primaryStore)
|
|
670
|
-
searchedStores.push(primaryStore);
|
|
671
|
-
if (scope === '*')
|
|
672
|
-
searchedStores.push(globalStore);
|
|
673
|
-
}
|
|
658
|
+
if (primaryStore)
|
|
659
|
+
searchedStores.push(primaryStore);
|
|
674
660
|
const tagFreq = mergeTagFrequencies(searchedStores);
|
|
675
661
|
// Parse filter once for both filtering (already done) and footer display
|
|
676
662
|
const filterGroups = filter ? parseFilter(filter) : [];
|
|
@@ -758,26 +744,45 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
758
744
|
isError: true,
|
|
759
745
|
};
|
|
760
746
|
}
|
|
761
|
-
// Resolve store —
|
|
762
|
-
|
|
763
|
-
|
|
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);
|
|
764
782
|
if (!ctx.ok)
|
|
765
783
|
return contextError(ctx);
|
|
766
784
|
const result = await ctx.store.correct(id, correction ?? '', action);
|
|
767
785
|
if (!result.corrected) {
|
|
768
|
-
// If not found in the targeted store, try the other one as fallback
|
|
769
|
-
if (isGlobalEntry) {
|
|
770
|
-
const lobeCtx = resolveToolContext(rawLobe);
|
|
771
|
-
if (lobeCtx.ok) {
|
|
772
|
-
const lobeResult = await lobeCtx.store.correct(id, correction ?? '', action);
|
|
773
|
-
if (lobeResult.corrected) {
|
|
774
|
-
const text = action === 'delete'
|
|
775
|
-
? `[${lobeCtx.label}] Deleted entry ${id}.`
|
|
776
|
-
: `[${lobeCtx.label}] Corrected entry ${id} (action: ${action}, confidence: ${lobeResult.newConfidence}, trust: ${lobeResult.trust}).`;
|
|
777
|
-
return { content: [{ type: 'text', text }] };
|
|
778
|
-
}
|
|
779
|
-
}
|
|
780
|
-
}
|
|
781
786
|
return {
|
|
782
787
|
content: [{ type: 'text', text: `[${ctx.label}] Failed to correct: ${result.error}` }],
|
|
783
788
|
isError: true,
|
|
@@ -799,11 +804,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
799
804
|
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
800
805
|
}
|
|
801
806
|
case 'memory_context': {
|
|
802
|
-
const { lobe: rawLobe, context, maxResults, minMatch } = z.object({
|
|
807
|
+
const { lobe: rawLobe, context, maxResults, minMatch, isFirstMemoryToolCall: rawIsFirst } = z.object({
|
|
803
808
|
lobe: z.string().optional(),
|
|
804
809
|
context: z.string().optional(),
|
|
805
810
|
maxResults: z.number().optional(),
|
|
806
811
|
minMatch: z.number().min(0).max(1).optional(),
|
|
812
|
+
isFirstMemoryToolCall: z.boolean().default(true),
|
|
807
813
|
}).parse(args ?? {});
|
|
808
814
|
// --- Briefing mode: no context provided → user + preferences + stale nudges ---
|
|
809
815
|
if (!context) {
|
|
@@ -820,22 +826,18 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
820
826
|
const degradedSection = degradedLobeNames.length > 0
|
|
821
827
|
? `## ⚠ Degraded Lobes: ${degradedLobeNames.join(', ')}\nRun **memory_diagnose** for details.\n`
|
|
822
828
|
: '';
|
|
823
|
-
// Global store holds user + preferences — always included
|
|
824
|
-
const globalBriefing = await globalStore.briefing(300);
|
|
825
829
|
const sections = [];
|
|
826
830
|
if (crashSection)
|
|
827
831
|
sections.push(crashSection);
|
|
828
832
|
if (degradedSection)
|
|
829
833
|
sections.push(degradedSection);
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
}
|
|
833
|
-
// 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)
|
|
834
836
|
const allStale = [];
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
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());
|
|
839
841
|
for (const lobeName of allBriefingLobeNames) {
|
|
840
842
|
const health = configManager.getLobeHealth(lobeName);
|
|
841
843
|
if (health?.status === 'degraded')
|
|
@@ -843,7 +845,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
843
845
|
const store = configManager.getStore(lobeName);
|
|
844
846
|
if (!store)
|
|
845
847
|
continue;
|
|
846
|
-
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
|
+
}
|
|
847
853
|
if (lobeBriefing.staleDetails)
|
|
848
854
|
allStale.push(...lobeBriefing.staleDetails);
|
|
849
855
|
totalEntries += lobeBriefing.entryCount;
|
|
@@ -856,7 +862,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
856
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.');
|
|
857
863
|
}
|
|
858
864
|
// Tag primer: show tag vocabulary if tags exist across any lobe
|
|
859
|
-
const briefingStores = [
|
|
865
|
+
const briefingStores = [];
|
|
860
866
|
for (const lobeName of allBriefingLobeNames) {
|
|
861
867
|
const store = configManager.getStore(lobeName);
|
|
862
868
|
if (store)
|
|
@@ -895,7 +901,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
895
901
|
allLobeResults.push(...lobeResults);
|
|
896
902
|
}
|
|
897
903
|
else {
|
|
898
|
-
const resolution = await resolveLobesForRead();
|
|
904
|
+
const resolution = await resolveLobesForRead(rawIsFirst);
|
|
899
905
|
switch (resolution.kind) {
|
|
900
906
|
case 'resolved': {
|
|
901
907
|
label = resolution.label;
|
|
@@ -921,13 +927,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
921
927
|
}
|
|
922
928
|
}
|
|
923
929
|
}
|
|
924
|
-
//
|
|
925
|
-
const globalResults = await globalStore.contextSearch(context, max, undefined, threshold);
|
|
926
|
-
for (const r of globalResults)
|
|
927
|
-
ctxEntryLobeMap.set(r.entry.id, 'global');
|
|
928
|
-
// Merge, dedupe by entry id, re-sort by score, take top N
|
|
930
|
+
// Dedupe by entry id, re-sort by score, take top N
|
|
929
931
|
const seenIds = new Set();
|
|
930
|
-
const results =
|
|
932
|
+
const results = allLobeResults
|
|
931
933
|
.sort((a, b) => b.score - a.score)
|
|
932
934
|
.filter(r => {
|
|
933
935
|
if (seenIds.has(r.entry.id))
|
|
@@ -937,11 +939,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
937
939
|
})
|
|
938
940
|
.slice(0, max);
|
|
939
941
|
// Build stores collection for tag frequency aggregation
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
if (primaryStore && primaryStore !== globalStore) {
|
|
942
|
+
const ctxSearchedStores = [];
|
|
943
|
+
if (primaryStore)
|
|
943
944
|
ctxSearchedStores.push(primaryStore);
|
|
944
|
-
}
|
|
945
945
|
const ctxTagFreq = mergeTagFrequencies(ctxSearchedStores);
|
|
946
946
|
// Parse filter for footer (context search has no filter, pass empty)
|
|
947
947
|
const ctxFilterGroups = [];
|
|
@@ -1023,24 +1023,27 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1023
1023
|
const { lobe: rawLobe } = z.object({
|
|
1024
1024
|
lobe: z.string().optional(),
|
|
1025
1025
|
}).parse(args ?? {});
|
|
1026
|
-
// Always include global stats
|
|
1027
|
-
const globalStats = await globalStore.stats();
|
|
1028
1026
|
// Single lobe stats
|
|
1029
1027
|
if (rawLobe) {
|
|
1030
1028
|
const ctx = resolveToolContext(rawLobe);
|
|
1031
1029
|
if (!ctx.ok)
|
|
1032
1030
|
return contextError(ctx);
|
|
1033
1031
|
const result = await ctx.store.stats();
|
|
1034
|
-
const
|
|
1035
|
-
|
|
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) }] };
|
|
1036
1035
|
}
|
|
1037
1036
|
// Combined stats across all lobes
|
|
1038
|
-
const sections = [
|
|
1037
|
+
const sections = [];
|
|
1039
1038
|
const allLobeNames = configManager.getLobeNames();
|
|
1039
|
+
const alwaysIncludeSet = new Set(configManager.getAlwaysIncludeLobes());
|
|
1040
1040
|
for (const lobeName of allLobeNames) {
|
|
1041
1041
|
const store = configManager.getStore(lobeName);
|
|
1042
|
+
if (!store)
|
|
1043
|
+
continue;
|
|
1042
1044
|
const result = await store.stats();
|
|
1043
|
-
|
|
1045
|
+
const label = alwaysIncludeSet.has(lobeName) ? `${lobeName} (alwaysInclude)` : lobeName;
|
|
1046
|
+
sections.push(formatStats(label, result));
|
|
1044
1047
|
}
|
|
1045
1048
|
return { content: [{ type: 'text', text: sections.join('\n\n---\n\n') }] };
|
|
1046
1049
|
}
|
|
@@ -1201,14 +1204,6 @@ async function buildDiagnosticsText(showFullCrashHistory) {
|
|
|
1201
1204
|
}
|
|
1202
1205
|
}
|
|
1203
1206
|
sections.push('');
|
|
1204
|
-
try {
|
|
1205
|
-
const globalStats = await globalStore.stats();
|
|
1206
|
-
sections.push(`- **global store**: ✅ healthy (${globalStats.totalEntries} entries, ${globalStats.storageSize})`);
|
|
1207
|
-
}
|
|
1208
|
-
catch (e) {
|
|
1209
|
-
sections.push(`- **global store**: ❌ error — ${e instanceof Error ? e.message : e}`);
|
|
1210
|
-
}
|
|
1211
|
-
sections.push('');
|
|
1212
1207
|
// Active behavior config — shows effective values and highlights user overrides
|
|
1213
1208
|
sections.push('### Active Behavior Config');
|
|
1214
1209
|
sections.push(formatBehaviorConfigSection(configBehavior));
|
|
@@ -1260,15 +1255,6 @@ async function main() {
|
|
|
1260
1255
|
process.stderr.write(`[memory-mcp] Previous crash detected (${age}s ago): ${previousCrash.type} — ${previousCrash.error}\n`);
|
|
1261
1256
|
process.stderr.write(`[memory-mcp] Crash report will be shown in memory_context and memory_diagnose.\n`);
|
|
1262
1257
|
}
|
|
1263
|
-
// Initialize global store (user + preferences, shared across all lobes)
|
|
1264
|
-
try {
|
|
1265
|
-
await globalStore.init();
|
|
1266
|
-
process.stderr.write(`[memory-mcp] Global store → ${globalMemoryPath}\n`);
|
|
1267
|
-
}
|
|
1268
|
-
catch (error) {
|
|
1269
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
1270
|
-
process.stderr.write(`[memory-mcp] WARNING: Global store init failed: ${msg}\n`);
|
|
1271
|
-
}
|
|
1272
1258
|
// Initialize each lobe independently — a broken lobe shouldn't prevent others from working
|
|
1273
1259
|
let healthyLobes = 0;
|
|
1274
1260
|
for (const [name, config] of lobeConfigs) {
|
|
@@ -1326,46 +1312,6 @@ async function main() {
|
|
|
1326
1312
|
};
|
|
1327
1313
|
process.stderr.write(`[memory-mcp] ⚠ DEGRADED: ${healthyLobes}/${lobeConfigs.size} lobes healthy.\n`);
|
|
1328
1314
|
}
|
|
1329
|
-
// Migrate: move user + preferences entries from lobe stores to global store.
|
|
1330
|
-
// State-driven guard: skip if already completed (marker file present).
|
|
1331
|
-
const migrationMarker = path.join(globalMemoryPath, '.migrated');
|
|
1332
|
-
if (!existsSync(migrationMarker)) {
|
|
1333
|
-
let migrated = 0;
|
|
1334
|
-
for (const [name, store] of stores) {
|
|
1335
|
-
for (const topic of ['user', 'preferences']) {
|
|
1336
|
-
try {
|
|
1337
|
-
const result = await store.query(topic, 'full');
|
|
1338
|
-
for (const entry of result.entries) {
|
|
1339
|
-
try {
|
|
1340
|
-
const globalResult = await globalStore.query(topic, 'full');
|
|
1341
|
-
const alreadyExists = globalResult.entries.some(g => g.title === entry.title);
|
|
1342
|
-
if (!alreadyExists && entry.content) {
|
|
1343
|
-
const trust = parseTrustLevel(entry.trust ?? 'user') ?? 'user';
|
|
1344
|
-
await globalStore.store(topic, entry.title, entry.content, [...(entry.sources ?? [])], trust);
|
|
1345
|
-
process.stderr.write(`[memory-mcp] Migrated ${entry.id} ("${entry.title}") from [${name}] → global\n`);
|
|
1346
|
-
migrated++;
|
|
1347
|
-
}
|
|
1348
|
-
await store.correct(entry.id, '', 'delete');
|
|
1349
|
-
process.stderr.write(`[memory-mcp] Removed ${entry.id} from [${name}] (now in global)\n`);
|
|
1350
|
-
}
|
|
1351
|
-
catch (entryError) {
|
|
1352
|
-
process.stderr.write(`[memory-mcp] Migration error for ${entry.id} in [${name}]: ${entryError}\n`);
|
|
1353
|
-
}
|
|
1354
|
-
}
|
|
1355
|
-
}
|
|
1356
|
-
catch (topicError) {
|
|
1357
|
-
process.stderr.write(`[memory-mcp] Migration error querying ${topic} in [${name}]: ${topicError}\n`);
|
|
1358
|
-
}
|
|
1359
|
-
}
|
|
1360
|
-
}
|
|
1361
|
-
// Write marker atomically — future startups skip this block entirely
|
|
1362
|
-
try {
|
|
1363
|
-
writeFileSync(migrationMarker, new Date().toISOString(), 'utf-8');
|
|
1364
|
-
if (migrated > 0)
|
|
1365
|
-
process.stderr.write(`[memory-mcp] Migration complete: ${migrated} entries moved to global store.\n`);
|
|
1366
|
-
}
|
|
1367
|
-
catch { /* marker write is best-effort */ }
|
|
1368
|
-
}
|
|
1369
1315
|
// Initialize ConfigManager with current config state
|
|
1370
1316
|
configManager = new ConfigManager(configPath, { configs: lobeConfigs, origin: configOrigin }, stores, lobeHealth);
|
|
1371
1317
|
const transport = new StdioServerTransport();
|
|
@@ -1393,7 +1339,12 @@ async function main() {
|
|
|
1393
1339
|
});
|
|
1394
1340
|
await server.connect(transport);
|
|
1395
1341
|
const modeStr = serverMode.kind === 'running' ? '' : ` [${serverMode.kind.toUpperCase()}]`;
|
|
1396
|
-
|
|
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`);
|
|
1397
1348
|
// Graceful shutdown on signals
|
|
1398
1349
|
const shutdown = () => {
|
|
1399
1350
|
process.stderr.write('[memory-mcp] Shutting down gracefully.\n');
|
|
@@ -26,5 +26,9 @@ export interface LobeRootConfig {
|
|
|
26
26
|
* checked at path-separator boundaries (no partial-name false positives) */
|
|
27
27
|
export declare function matchRootsToLobeNames(clientRoots: readonly ClientRoot[], lobeConfigs: readonly LobeRootConfig[]): readonly string[];
|
|
28
28
|
/** Build a LobeResolution from the available lobe names and matched lobes.
|
|
29
|
-
* Encodes the degradation ladder as a pure function.
|
|
30
|
-
|
|
29
|
+
* Encodes the degradation ladder as a pure function.
|
|
30
|
+
*
|
|
31
|
+
* When isFirstMemoryToolCall is true (default), alwaysIncludeLobes are appended
|
|
32
|
+
* to the resolved set (deduped). When false, they are excluded — the agent has
|
|
33
|
+
* already loaded global knowledge in this conversation. */
|
|
34
|
+
export declare function buildLobeResolution(allLobeNames: readonly string[], matchedLobes: readonly string[], alwaysIncludeLobes?: readonly string[], isFirstMemoryToolCall?: boolean): LobeResolution;
|
package/dist/lobe-resolution.js
CHANGED
|
@@ -44,18 +44,40 @@ export function matchRootsToLobeNames(clientRoots, lobeConfigs) {
|
|
|
44
44
|
return Array.from(matchedLobes);
|
|
45
45
|
}
|
|
46
46
|
/** Build a LobeResolution from the available lobe names and matched lobes.
|
|
47
|
-
* Encodes the degradation ladder as a pure function.
|
|
48
|
-
|
|
47
|
+
* Encodes the degradation ladder as a pure function.
|
|
48
|
+
*
|
|
49
|
+
* When isFirstMemoryToolCall is true (default), alwaysIncludeLobes are appended
|
|
50
|
+
* to the resolved set (deduped). When false, they are excluded — the agent has
|
|
51
|
+
* already loaded global knowledge in this conversation. */
|
|
52
|
+
export function buildLobeResolution(allLobeNames, matchedLobes, alwaysIncludeLobes = [], isFirstMemoryToolCall = true) {
|
|
49
53
|
// Single lobe — always resolved, regardless of root matching
|
|
50
|
-
if (allLobeNames.length === 1) {
|
|
54
|
+
if (allLobeNames.length === 1 && alwaysIncludeLobes.length === 0) {
|
|
51
55
|
return { kind: 'resolved', lobes: allLobeNames, label: allLobeNames[0] };
|
|
52
56
|
}
|
|
53
|
-
//
|
|
54
|
-
|
|
57
|
+
// Build the base resolved set
|
|
58
|
+
let baseLobes;
|
|
59
|
+
if (allLobeNames.length === 1) {
|
|
60
|
+
baseLobes = allLobeNames;
|
|
61
|
+
}
|
|
62
|
+
else if (matchedLobes.length > 0) {
|
|
63
|
+
baseLobes = matchedLobes;
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
baseLobes = [];
|
|
67
|
+
}
|
|
68
|
+
// Append alwaysInclude lobes when isFirstMemoryToolCall is true (deduped)
|
|
69
|
+
const resolvedSet = new Set(baseLobes);
|
|
70
|
+
if (isFirstMemoryToolCall) {
|
|
71
|
+
for (const lobe of alwaysIncludeLobes) {
|
|
72
|
+
resolvedSet.add(lobe);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (resolvedSet.size > 0) {
|
|
76
|
+
const lobes = Array.from(resolvedSet);
|
|
55
77
|
return {
|
|
56
78
|
kind: 'resolved',
|
|
57
|
-
lobes
|
|
58
|
-
label:
|
|
79
|
+
lobes,
|
|
80
|
+
label: lobes.length === 1 ? lobes[0] : lobes.join('+'),
|
|
59
81
|
};
|
|
60
82
|
}
|
|
61
83
|
// Fallback — no lobes could be determined
|
package/dist/store.d.ts
CHANGED
|
@@ -18,6 +18,9 @@ export declare class MarkdownMemoryStore {
|
|
|
18
18
|
query(scope: string, detail?: DetailLevel, filter?: string, branchFilter?: string): Promise<QueryResult>;
|
|
19
19
|
/** Generate a session-start briefing */
|
|
20
20
|
briefing(maxTokens?: number): Promise<BriefingResult>;
|
|
21
|
+
/** Check if an entry exists by ID — read-only, no side effects beyond disk reload.
|
|
22
|
+
* Use this to probe for entry ownership before calling correct(). */
|
|
23
|
+
hasEntry(id: string): Promise<boolean>;
|
|
21
24
|
/** Correct an existing entry */
|
|
22
25
|
correct(id: string, correction: string, action: 'append' | 'replace' | 'delete'): Promise<CorrectResult>;
|
|
23
26
|
/** Get memory health statistics */
|
package/dist/store.js
CHANGED
|
@@ -256,6 +256,12 @@ export class MarkdownMemoryStore {
|
|
|
256
256
|
suggestion,
|
|
257
257
|
};
|
|
258
258
|
}
|
|
259
|
+
/** Check if an entry exists by ID — read-only, no side effects beyond disk reload.
|
|
260
|
+
* Use this to probe for entry ownership before calling correct(). */
|
|
261
|
+
async hasEntry(id) {
|
|
262
|
+
await this.reloadFromDisk();
|
|
263
|
+
return this.entries.has(id);
|
|
264
|
+
}
|
|
259
265
|
/** Correct an existing entry */
|
|
260
266
|
async correct(id, correction, action) {
|
|
261
267
|
// Reload to ensure we have the latest
|
package/dist/types.d.ts
CHANGED
|
@@ -195,6 +195,7 @@ export interface MemoryConfig {
|
|
|
195
195
|
readonly repoRoot: string;
|
|
196
196
|
readonly memoryPath: string;
|
|
197
197
|
readonly storageBudgetBytes: number;
|
|
198
|
+
readonly alwaysInclude: boolean;
|
|
198
199
|
readonly behavior?: BehaviorConfig;
|
|
199
200
|
readonly clock?: Clock;
|
|
200
201
|
readonly git?: GitService;
|