@exaudeus/memory-mcp 1.7.0 → 1.8.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.d.ts +20 -1
- package/dist/config.js +70 -6
- package/dist/embedder.d.ts +22 -0
- package/dist/embedder.js +50 -0
- package/dist/formatters.d.ts +5 -0
- package/dist/formatters.js +18 -1
- package/dist/index.js +34 -2
- package/dist/store.d.ts +14 -1
- package/dist/store.js +56 -2
- package/dist/thresholds.d.ts +4 -0
- package/dist/thresholds.js +4 -0
- package/dist/types.d.ts +19 -0
- package/package.json +1 -1
package/dist/config.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { MemoryConfig, BehaviorConfig } from './types.js';
|
|
1
|
+
import type { MemoryConfig, BehaviorConfig, EmbedderConfig } from './types.js';
|
|
2
|
+
import type { Embedder } from './embedder.js';
|
|
2
3
|
/** How the config was loaded — discriminated union so configFilePath
|
|
3
4
|
* only exists when source is 'file' (illegal states unrepresentable) */
|
|
4
5
|
export type ConfigOrigin = {
|
|
@@ -15,6 +16,8 @@ export interface LoadedConfig {
|
|
|
15
16
|
readonly origin: ConfigOrigin;
|
|
16
17
|
/** Resolved behavior config — present when a "behavior" block was found in memory-config.json */
|
|
17
18
|
readonly behavior?: BehaviorConfig;
|
|
19
|
+
/** Resolved embedder — shared across all lobes. Constructed from config or auto-detected. */
|
|
20
|
+
readonly embedder?: Embedder;
|
|
18
21
|
}
|
|
19
22
|
interface MemoryConfigFileBehavior {
|
|
20
23
|
staleDaysStandard?: number;
|
|
@@ -23,10 +26,26 @@ interface MemoryConfigFileBehavior {
|
|
|
23
26
|
maxDedupSuggestions?: number;
|
|
24
27
|
maxConflictPairs?: number;
|
|
25
28
|
}
|
|
29
|
+
interface MemoryConfigFileEmbedder {
|
|
30
|
+
provider?: string;
|
|
31
|
+
model?: string;
|
|
32
|
+
baseUrl?: string;
|
|
33
|
+
timeoutMs?: number;
|
|
34
|
+
dimensions?: number;
|
|
35
|
+
}
|
|
26
36
|
/** Parse and validate a behavior config block, falling back to defaults for each field.
|
|
27
37
|
* Warns to stderr for unknown keys (likely typos) and out-of-range values.
|
|
28
38
|
* Exported for testing — validates and clamps all fields. */
|
|
29
39
|
export declare function parseBehaviorConfig(raw?: MemoryConfigFileBehavior): BehaviorConfig;
|
|
40
|
+
/** Parse and validate an embedder config block.
|
|
41
|
+
* Returns undefined when block is absent (auto-detect mode).
|
|
42
|
+
* Exported for testing. */
|
|
43
|
+
export declare function parseEmbedderConfig(raw?: MemoryConfigFileEmbedder): EmbedderConfig | undefined;
|
|
44
|
+
/** Create an Embedder from config.
|
|
45
|
+
* - provider "none" → null (keyword-only)
|
|
46
|
+
* - provider "ollama" → LazyEmbedder wrapping OllamaEmbedder with config params
|
|
47
|
+
* - No config (auto-detect) → LazyEmbedder wrapping default OllamaEmbedder */
|
|
48
|
+
export declare function createEmbedderFromConfig(config?: EmbedderConfig): Embedder | undefined;
|
|
30
49
|
/** Load lobe configs with priority: memory-config.json -> env vars -> single-repo default */
|
|
31
50
|
export declare function getLobeConfigs(): LoadedConfig;
|
|
32
51
|
export {};
|
package/dist/config.js
CHANGED
|
@@ -7,6 +7,7 @@ import { execFileSync } from 'child_process';
|
|
|
7
7
|
import path from 'path';
|
|
8
8
|
import os from 'os';
|
|
9
9
|
import { DEFAULT_STORAGE_BUDGET_BYTES } from './types.js';
|
|
10
|
+
import { OllamaEmbedder, LazyEmbedder } from './embedder.js';
|
|
10
11
|
import { DEFAULT_STALE_DAYS_STANDARD, DEFAULT_STALE_DAYS_PREFERENCES, DEFAULT_MAX_STALE_IN_BRIEFING, DEFAULT_MAX_DEDUP_SUGGESTIONS, DEFAULT_MAX_CONFLICT_PAIRS, } from './thresholds.js';
|
|
11
12
|
/** Validate and clamp a numeric threshold to a given range.
|
|
12
13
|
* Returns the default if the value is missing, NaN, or out of range. */
|
|
@@ -46,6 +47,61 @@ export function parseBehaviorConfig(raw) {
|
|
|
46
47
|
maxConflictPairs: clampThreshold(raw.maxConflictPairs, DEFAULT_MAX_CONFLICT_PAIRS, 1, 5),
|
|
47
48
|
};
|
|
48
49
|
}
|
|
50
|
+
/** Known embedder config keys — used to warn on typos/unknown fields. */
|
|
51
|
+
const KNOWN_EMBEDDER_KEYS = new Set([
|
|
52
|
+
'provider', 'model', 'baseUrl', 'timeoutMs', 'dimensions',
|
|
53
|
+
]);
|
|
54
|
+
const VALID_PROVIDERS = new Set(['ollama', 'none']);
|
|
55
|
+
/** Parse and validate an embedder config block.
|
|
56
|
+
* Returns undefined when block is absent (auto-detect mode).
|
|
57
|
+
* Exported for testing. */
|
|
58
|
+
export function parseEmbedderConfig(raw) {
|
|
59
|
+
if (!raw)
|
|
60
|
+
return undefined;
|
|
61
|
+
// Warn on unrecognized keys
|
|
62
|
+
for (const key of Object.keys(raw)) {
|
|
63
|
+
if (!KNOWN_EMBEDDER_KEYS.has(key)) {
|
|
64
|
+
process.stderr.write(`[memory-mcp] Unknown embedder config key "${key}" — ignored. ` +
|
|
65
|
+
`Valid keys: ${Array.from(KNOWN_EMBEDDER_KEYS).join(', ')}\n`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// Validate provider — default to 'ollama' if present but not set
|
|
69
|
+
const provider = raw.provider && VALID_PROVIDERS.has(raw.provider)
|
|
70
|
+
? raw.provider
|
|
71
|
+
: 'ollama';
|
|
72
|
+
if (raw.provider && !VALID_PROVIDERS.has(raw.provider)) {
|
|
73
|
+
process.stderr.write(`[memory-mcp] Unknown embedder provider "${raw.provider}" — using "ollama". Valid: ${Array.from(VALID_PROVIDERS).join(', ')}\n`);
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
provider,
|
|
77
|
+
model: raw.model,
|
|
78
|
+
baseUrl: raw.baseUrl,
|
|
79
|
+
timeoutMs: raw.timeoutMs !== undefined
|
|
80
|
+
? clampThreshold(raw.timeoutMs, 5000, 500, 30000)
|
|
81
|
+
: undefined,
|
|
82
|
+
dimensions: raw.dimensions !== undefined
|
|
83
|
+
? clampThreshold(raw.dimensions, 384, 64, 4096)
|
|
84
|
+
: undefined,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
/** Create an Embedder from config.
|
|
88
|
+
* - provider "none" → null (keyword-only)
|
|
89
|
+
* - provider "ollama" → LazyEmbedder wrapping OllamaEmbedder with config params
|
|
90
|
+
* - No config (auto-detect) → LazyEmbedder wrapping default OllamaEmbedder */
|
|
91
|
+
export function createEmbedderFromConfig(config) {
|
|
92
|
+
// Explicit opt-out
|
|
93
|
+
if (config?.provider === 'none')
|
|
94
|
+
return undefined;
|
|
95
|
+
// Explicit or default Ollama config
|
|
96
|
+
const candidate = new OllamaEmbedder({
|
|
97
|
+
model: config?.model,
|
|
98
|
+
baseUrl: config?.baseUrl,
|
|
99
|
+
timeoutMs: config?.timeoutMs,
|
|
100
|
+
dimensions: config?.dimensions,
|
|
101
|
+
});
|
|
102
|
+
// Both explicit "ollama" and auto-detect use LazyEmbedder for fast startup
|
|
103
|
+
return new LazyEmbedder(candidate);
|
|
104
|
+
}
|
|
49
105
|
function resolveRoot(root) {
|
|
50
106
|
return root
|
|
51
107
|
.replace(/^\$HOME\b/, process.env.HOME ?? '')
|
|
@@ -84,7 +140,7 @@ function resolveMemoryPath(repoRoot, workspaceName, explicitMemoryDir) {
|
|
|
84
140
|
/** If no lobe has alwaysInclude: true AND the legacy global store directory has actual entries,
|
|
85
141
|
* auto-create a "global" lobe pointing to it. Protects existing users who haven't updated their config.
|
|
86
142
|
* Only fires when the dir contains .md files — an empty dir doesn't trigger creation. */
|
|
87
|
-
function ensureAlwaysIncludeLobe(configs, behavior) {
|
|
143
|
+
function ensureAlwaysIncludeLobe(configs, behavior, embedder) {
|
|
88
144
|
const hasAlwaysInclude = Array.from(configs.values()).some(c => c.alwaysInclude);
|
|
89
145
|
if (hasAlwaysInclude)
|
|
90
146
|
return;
|
|
@@ -114,6 +170,7 @@ function ensureAlwaysIncludeLobe(configs, behavior) {
|
|
|
114
170
|
storageBudgetBytes: DEFAULT_STORAGE_BUDGET_BYTES,
|
|
115
171
|
alwaysInclude: true,
|
|
116
172
|
behavior,
|
|
173
|
+
embedder,
|
|
117
174
|
});
|
|
118
175
|
process.stderr.write(`[memory-mcp] Auto-created "global" lobe (alwaysInclude) from existing ${globalPath}\n`);
|
|
119
176
|
}
|
|
@@ -131,6 +188,8 @@ export function getLobeConfigs() {
|
|
|
131
188
|
else {
|
|
132
189
|
// Parse global behavior config once — applies to all lobes
|
|
133
190
|
const behavior = parseBehaviorConfig(external.behavior);
|
|
191
|
+
const embedderConfig = parseEmbedderConfig(external.embedder);
|
|
192
|
+
const embedder = createEmbedderFromConfig(embedderConfig);
|
|
134
193
|
for (const [name, config] of Object.entries(external.lobes)) {
|
|
135
194
|
if (!config.root) {
|
|
136
195
|
process.stderr.write(`[memory-mcp] Skipping lobe "${name}": missing "root" field\n`);
|
|
@@ -143,14 +202,15 @@ export function getLobeConfigs() {
|
|
|
143
202
|
storageBudgetBytes: (config.budgetMB ?? 2) * 1024 * 1024,
|
|
144
203
|
alwaysInclude: config.alwaysInclude ?? false,
|
|
145
204
|
behavior,
|
|
205
|
+
embedder,
|
|
146
206
|
});
|
|
147
207
|
}
|
|
148
208
|
if (configs.size > 0) {
|
|
149
209
|
// Reuse the already-parsed behavior config for the alwaysInclude fallback
|
|
150
210
|
const resolvedBehavior = external.behavior ? behavior : undefined;
|
|
151
|
-
ensureAlwaysIncludeLobe(configs, resolvedBehavior);
|
|
211
|
+
ensureAlwaysIncludeLobe(configs, resolvedBehavior, embedder);
|
|
152
212
|
process.stderr.write(`[memory-mcp] Loaded ${configs.size} lobe(s) from memory-config.json\n`);
|
|
153
|
-
return { configs, origin: { source: 'file', path: configPath }, behavior: resolvedBehavior };
|
|
213
|
+
return { configs, origin: { source: 'file', path: configPath }, behavior: resolvedBehavior, embedder };
|
|
154
214
|
}
|
|
155
215
|
}
|
|
156
216
|
}
|
|
@@ -162,6 +222,8 @@ export function getLobeConfigs() {
|
|
|
162
222
|
process.stderr.write(`[memory-mcp] Failed to parse memory-config.json: ${message}\n`);
|
|
163
223
|
}
|
|
164
224
|
}
|
|
225
|
+
// Auto-detect embedder for env var and default modes (no config file)
|
|
226
|
+
const autoEmbedder = createEmbedderFromConfig(undefined);
|
|
165
227
|
// 2. Try env var multi-repo mode
|
|
166
228
|
const workspacesJson = process.env.MEMORY_MCP_WORKSPACES;
|
|
167
229
|
if (workspacesJson) {
|
|
@@ -176,12 +238,13 @@ export function getLobeConfigs() {
|
|
|
176
238
|
memoryPath: resolveMemoryPath(repoRoot, name, explicitDir),
|
|
177
239
|
storageBudgetBytes: storageBudget,
|
|
178
240
|
alwaysInclude: false,
|
|
241
|
+
embedder: autoEmbedder,
|
|
179
242
|
});
|
|
180
243
|
}
|
|
181
244
|
if (configs.size > 0) {
|
|
182
|
-
ensureAlwaysIncludeLobe(configs);
|
|
245
|
+
ensureAlwaysIncludeLobe(configs, undefined, autoEmbedder);
|
|
183
246
|
process.stderr.write(`[memory-mcp] Loaded ${configs.size} lobe(s) from MEMORY_MCP_WORKSPACES env var\n`);
|
|
184
|
-
return { configs, origin: { source: 'env' } };
|
|
247
|
+
return { configs, origin: { source: 'env' }, embedder: autoEmbedder };
|
|
185
248
|
}
|
|
186
249
|
}
|
|
187
250
|
catch (e) {
|
|
@@ -197,8 +260,9 @@ export function getLobeConfigs() {
|
|
|
197
260
|
memoryPath: resolveMemoryPath(repoRoot, 'default', explicitDir),
|
|
198
261
|
storageBudgetBytes: storageBudget,
|
|
199
262
|
alwaysInclude: false,
|
|
263
|
+
embedder: autoEmbedder,
|
|
200
264
|
});
|
|
201
265
|
// No ensureAlwaysIncludeLobe here — single-repo default users have everything in one lobe
|
|
202
266
|
process.stderr.write(`[memory-mcp] Using single-lobe default mode (cwd: ${repoRoot})\n`);
|
|
203
|
-
return { configs, origin: { source: 'default' } };
|
|
267
|
+
return { configs, origin: { source: 'default' }, embedder: autoEmbedder };
|
|
204
268
|
}
|
package/dist/embedder.d.ts
CHANGED
|
@@ -60,6 +60,28 @@ export declare class FakeEmbedder implements Embedder {
|
|
|
60
60
|
constructor(dimensions?: number);
|
|
61
61
|
embed(text: string, _signal?: AbortSignal): Promise<EmbedResult>;
|
|
62
62
|
}
|
|
63
|
+
/** Lazy auto-detecting embedder — probes on first use, caches the result.
|
|
64
|
+
* Re-probes on failure after a TTL window so the system recovers if
|
|
65
|
+
* Ollama starts after MCP startup.
|
|
66
|
+
*
|
|
67
|
+
* Implements the same Embedder interface — the store never knows it's lazy.
|
|
68
|
+
* The probe uses the candidate's own timeout (5s for cold starts).
|
|
69
|
+
* The caller's signal is only forwarded to the actual embed call, not the probe. */
|
|
70
|
+
export declare class LazyEmbedder implements Embedder {
|
|
71
|
+
readonly dimensions: number;
|
|
72
|
+
private inner;
|
|
73
|
+
private lastProbeTime;
|
|
74
|
+
private hasLoggedUnavailable;
|
|
75
|
+
private readonly candidate;
|
|
76
|
+
private readonly reprobeIntervalMs;
|
|
77
|
+
private readonly now;
|
|
78
|
+
constructor(candidate: Embedder, opts?: {
|
|
79
|
+
readonly reprobeIntervalMs?: number;
|
|
80
|
+
/** Injectable clock for testing — default Date.now */
|
|
81
|
+
readonly now?: () => number;
|
|
82
|
+
});
|
|
83
|
+
embed(text: string, signal?: AbortSignal): Promise<EmbedResult>;
|
|
84
|
+
}
|
|
63
85
|
/** Batch embed texts sequentially. Pure composition over Embedder.embed().
|
|
64
86
|
* Sequential because local Ollama benefits from serialized requests (single GPU/CPU).
|
|
65
87
|
* Not on the interface — interface segregation. */
|
package/dist/embedder.js
CHANGED
|
@@ -130,6 +130,56 @@ function trigramHash(trigram, buckets) {
|
|
|
130
130
|
}
|
|
131
131
|
return ((hash % buckets) + buckets) % buckets;
|
|
132
132
|
}
|
|
133
|
+
// ─── LazyEmbedder ─────────────────────────────────────────────────────────
|
|
134
|
+
/** Default reprobe interval — how long to wait before retrying after a failed probe.
|
|
135
|
+
* 2 minutes balances responsiveness (recovery after Ollama starts) with
|
|
136
|
+
* avoiding excessive probes when Ollama isn't installed. */
|
|
137
|
+
const DEFAULT_REPROBE_INTERVAL_MS = 2 * 60 * 1000;
|
|
138
|
+
/** Lazy auto-detecting embedder — probes on first use, caches the result.
|
|
139
|
+
* Re-probes on failure after a TTL window so the system recovers if
|
|
140
|
+
* Ollama starts after MCP startup.
|
|
141
|
+
*
|
|
142
|
+
* Implements the same Embedder interface — the store never knows it's lazy.
|
|
143
|
+
* The probe uses the candidate's own timeout (5s for cold starts).
|
|
144
|
+
* The caller's signal is only forwarded to the actual embed call, not the probe. */
|
|
145
|
+
export class LazyEmbedder {
|
|
146
|
+
constructor(candidate, opts) {
|
|
147
|
+
this.inner = null;
|
|
148
|
+
this.lastProbeTime = -Infinity;
|
|
149
|
+
this.hasLoggedUnavailable = false;
|
|
150
|
+
this.candidate = candidate;
|
|
151
|
+
this.dimensions = candidate.dimensions;
|
|
152
|
+
this.reprobeIntervalMs = opts?.reprobeIntervalMs ?? DEFAULT_REPROBE_INTERVAL_MS;
|
|
153
|
+
this.now = opts?.now ?? Date.now;
|
|
154
|
+
}
|
|
155
|
+
async embed(text, signal) {
|
|
156
|
+
const now = this.now();
|
|
157
|
+
const shouldProbe = !this.inner && (now - this.lastProbeTime >= this.reprobeIntervalMs);
|
|
158
|
+
if (shouldProbe) {
|
|
159
|
+
this.lastProbeTime = now;
|
|
160
|
+
// Probe without caller's signal — use candidate's default timeout (5s)
|
|
161
|
+
// so cold model loads aren't aborted by a tight query-time timeout
|
|
162
|
+
const probe = await this.candidate.embed('probe');
|
|
163
|
+
if (probe.ok) {
|
|
164
|
+
this.inner = this.candidate;
|
|
165
|
+
if (this.hasLoggedUnavailable) {
|
|
166
|
+
// Recovery after previous failure — notify
|
|
167
|
+
process.stderr.write('[memory-mcp] Embedding provider recovered — semantic search active\n');
|
|
168
|
+
this.hasLoggedUnavailable = false;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
else if (!this.hasLoggedUnavailable) {
|
|
172
|
+
// Only log first failure — avoid noisy repeated warnings
|
|
173
|
+
process.stderr.write(`[memory-mcp] Embedding provider not available — using keyword-only search (will retry in ${Math.round(this.reprobeIntervalMs / 1000)}s)\n`);
|
|
174
|
+
this.hasLoggedUnavailable = true;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
if (!this.inner) {
|
|
178
|
+
return { ok: false, failure: { kind: 'provider-unavailable', reason: 'auto-detect: provider not available' } };
|
|
179
|
+
}
|
|
180
|
+
return this.inner.embed(text, signal);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
133
183
|
// ─── Batch utility ────────────────────────────────────────────────────────
|
|
134
184
|
/** Batch embed texts sequentially. Pure composition over Embedder.embed().
|
|
135
185
|
* Sequential because local Ollama benefits from serialized requests (single GPU/CPU).
|
package/dist/formatters.d.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import type { MemoryStats, StaleEntry, ConflictPair, BehaviorConfig } from './types.js';
|
|
2
2
|
import { type FilterGroup } from './text-analyzer.js';
|
|
3
3
|
import type { MarkdownMemoryStore } from './store.js';
|
|
4
|
+
/** Format the search mode indicator for context/recall responses.
|
|
5
|
+
* Pure function — no I/O, no state.
|
|
6
|
+
*
|
|
7
|
+
* Shows whether semantic search is active and vector coverage. */
|
|
8
|
+
export declare function formatSearchMode(embedderAvailable: boolean, vectorCount: number, totalCount: number): string;
|
|
4
9
|
/** Format the stale entries section for briefing/context responses */
|
|
5
10
|
export declare function formatStaleSection(staleDetails: readonly StaleEntry[]): string;
|
|
6
11
|
/** Format the conflict detection warning for query/context responses */
|
package/dist/formatters.js
CHANGED
|
@@ -4,6 +4,22 @@
|
|
|
4
4
|
// and returns a formatted string for the tool response.
|
|
5
5
|
import { DEFAULT_STALE_DAYS_STANDARD, DEFAULT_STALE_DAYS_PREFERENCES, DEFAULT_MAX_STALE_IN_BRIEFING, DEFAULT_MAX_DEDUP_SUGGESTIONS, DEFAULT_MAX_CONFLICT_PAIRS, MAX_FOOTER_TAGS, WARN_SEPARATOR, } from './thresholds.js';
|
|
6
6
|
import { analyzeFilterGroups } from './text-analyzer.js';
|
|
7
|
+
/** Format the search mode indicator for context/recall responses.
|
|
8
|
+
* Pure function — no I/O, no state.
|
|
9
|
+
*
|
|
10
|
+
* Shows whether semantic search is active and vector coverage. */
|
|
11
|
+
export function formatSearchMode(embedderAvailable, vectorCount, totalCount) {
|
|
12
|
+
if (!embedderAvailable) {
|
|
13
|
+
return '*Search: keyword-only (install Ollama for semantic search)*';
|
|
14
|
+
}
|
|
15
|
+
if (vectorCount === 0 && totalCount > 0) {
|
|
16
|
+
return `*Search: semantic + keyword (0/${totalCount} entries vectorized — run memory_reembed)*`;
|
|
17
|
+
}
|
|
18
|
+
if (totalCount === 0) {
|
|
19
|
+
return '*Search: semantic + keyword (no entries yet)*';
|
|
20
|
+
}
|
|
21
|
+
return `*Search: semantic + keyword (${vectorCount}/${totalCount} entries vectorized)*`;
|
|
22
|
+
}
|
|
7
23
|
/** Format the stale entries section for briefing/context responses */
|
|
8
24
|
export function formatStaleSection(staleDetails) {
|
|
9
25
|
const lines = [
|
|
@@ -61,11 +77,12 @@ export function formatStats(lobe, result) {
|
|
|
61
77
|
.join('\n')
|
|
62
78
|
: ' (none)';
|
|
63
79
|
const corruptLine = result.corruptFiles > 0 ? `\n**Corrupt files:** ${result.corruptFiles}` : '';
|
|
80
|
+
const vectorLine = `\n**Vectors:** ${result.vectorCount}/${result.totalEntries} entries vectorized`;
|
|
64
81
|
return [
|
|
65
82
|
`## [${lobe}] Memory Stats`,
|
|
66
83
|
``,
|
|
67
84
|
`**Memory location:** ${result.memoryPath}`,
|
|
68
|
-
`**Total entries:** ${result.totalEntries}${corruptLine}`,
|
|
85
|
+
`**Total entries:** ${result.totalEntries}${corruptLine}${vectorLine}`,
|
|
69
86
|
`**Storage:** ${result.storageSize} / ${Math.round(result.storageBudgetBytes / 1024 / 1024)}MB budget`,
|
|
70
87
|
``,
|
|
71
88
|
`### By Topic`,
|
package/dist/index.js
CHANGED
|
@@ -14,7 +14,7 @@ import { getLobeConfigs } from './config.js';
|
|
|
14
14
|
import { ConfigManager } from './config-manager.js';
|
|
15
15
|
import { normalizeArgs } from './normalize.js';
|
|
16
16
|
import { buildCrashReport, writeCrashReport, writeCrashReportSync, readLatestCrash, readCrashHistory, clearLatestCrash, formatCrashReport, formatCrashSummary, markServerStarted, } from './crash-journal.js';
|
|
17
|
-
import { formatStaleSection, formatConflictWarning, formatStats, formatBehaviorConfigSection, mergeTagFrequencies, buildQueryFooter, buildBriefingTagPrimerSections } from './formatters.js';
|
|
17
|
+
import { formatStaleSection, formatConflictWarning, formatStats, formatBehaviorConfigSection, mergeTagFrequencies, buildQueryFooter, buildBriefingTagPrimerSections, formatSearchMode } from './formatters.js';
|
|
18
18
|
import { parseFilter } from './text-analyzer.js';
|
|
19
19
|
import { VOCABULARY_ECHO_LIMIT, WARN_SEPARATOR } from './thresholds.js';
|
|
20
20
|
import { matchRootsToLobeNames, buildLobeResolution } from './lobe-resolution.js';
|
|
@@ -398,6 +398,8 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
398
398
|
// memory_diagnose is intentionally hidden from the tool list — it clutters
|
|
399
399
|
// agent tool discovery and should only be called when directed by error messages
|
|
400
400
|
// or crash reports. The handler still works if called directly.
|
|
401
|
+
// memory_reembed is hidden — utility for generating/regenerating embeddings.
|
|
402
|
+
// Surfaced via hint in memory_context when >50% of entries lack vectors.
|
|
401
403
|
] };
|
|
402
404
|
});
|
|
403
405
|
// --- Tool handlers ---
|
|
@@ -974,10 +976,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
974
976
|
const noResultHint = ctxGlobalOnlyHint
|
|
975
977
|
? `\n\n> ${ctxGlobalOnlyHint}`
|
|
976
978
|
: '\n\nThis is fine — proceed without prior context. As you learn things worth remembering, store them with memory_store.';
|
|
979
|
+
// Mode indicator on no-results path — helps diagnose why nothing was found
|
|
980
|
+
const modeHint = primaryStore
|
|
981
|
+
? `\n${formatSearchMode(primaryStore.hasEmbedder, primaryStore.vectorCount, primaryStore.entryCount)}`
|
|
982
|
+
: '';
|
|
977
983
|
return {
|
|
978
984
|
content: [{
|
|
979
985
|
type: 'text',
|
|
980
|
-
text: `[${label}] No relevant knowledge found for: "${context}"${noResultHint}\n\n---\n${ctxFooter}`,
|
|
986
|
+
text: `[${label}] No relevant knowledge found for: "${context}"${noResultHint}${modeHint}\n\n---\n${ctxFooter}`,
|
|
981
987
|
}],
|
|
982
988
|
};
|
|
983
989
|
}
|
|
@@ -1020,6 +1026,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1020
1026
|
sections.push(formatConflictWarning(ctxConflicts));
|
|
1021
1027
|
}
|
|
1022
1028
|
}
|
|
1029
|
+
// Search mode indicator — lightweight getters, no extra disk reload
|
|
1030
|
+
if (primaryStore) {
|
|
1031
|
+
sections.push(formatSearchMode(primaryStore.hasEmbedder, primaryStore.vectorCount, primaryStore.entryCount));
|
|
1032
|
+
}
|
|
1023
1033
|
// Collect all matched keywords and topics for the dedup hint
|
|
1024
1034
|
const allMatchedKeywords = new Set();
|
|
1025
1035
|
const matchedTopics = new Set();
|
|
@@ -1071,6 +1081,28 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1071
1081
|
}
|
|
1072
1082
|
return { content: [{ type: 'text', text: sections.join('\n\n---\n\n') }] };
|
|
1073
1083
|
}
|
|
1084
|
+
case 'memory_reembed': {
|
|
1085
|
+
const { lobe: rawLobe } = z.object({
|
|
1086
|
+
lobe: z.string().optional(),
|
|
1087
|
+
}).parse(args ?? {});
|
|
1088
|
+
const lobeName = rawLobe ?? lobeNames[0];
|
|
1089
|
+
const ctx = resolveToolContext(lobeName);
|
|
1090
|
+
if (!ctx.ok)
|
|
1091
|
+
return contextError(ctx);
|
|
1092
|
+
const result = await ctx.store.reEmbed();
|
|
1093
|
+
if (result.error) {
|
|
1094
|
+
return { content: [{ type: 'text', text: `[${ctx.label}] Re-embed failed: ${result.error}` }] };
|
|
1095
|
+
}
|
|
1096
|
+
const parts = [
|
|
1097
|
+
`[${ctx.label}] Re-embedded ${result.embedded} entries`,
|
|
1098
|
+
`(${result.skipped} skipped, ${result.failed} failed).`,
|
|
1099
|
+
];
|
|
1100
|
+
// Hint if many entries were vectorized
|
|
1101
|
+
if (result.embedded > 0) {
|
|
1102
|
+
parts.push('\nSemantic search is now active for these entries.');
|
|
1103
|
+
}
|
|
1104
|
+
return { content: [{ type: 'text', text: parts.join(' ') }] };
|
|
1105
|
+
}
|
|
1074
1106
|
case 'memory_bootstrap': {
|
|
1075
1107
|
const { lobe: rawLobe, root, budgetMB } = z.object({
|
|
1076
1108
|
lobe: z.string().optional(),
|
package/dist/store.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { MemoryEntry, TopicScope, TrustLevel, DetailLevel, DurabilityDecision, QueryResult, StoreResult, CorrectResult, MemoryStats, BriefingResult, ConflictPair, MemoryConfig } from './types.js';
|
|
1
|
+
import type { MemoryEntry, TopicScope, TrustLevel, DetailLevel, DurabilityDecision, QueryResult, StoreResult, CorrectResult, MemoryStats, ReEmbedResult, BriefingResult, ConflictPair, MemoryConfig } from './types.js';
|
|
2
2
|
import type { ScoredEntry } from './ranking.js';
|
|
3
3
|
export declare class MarkdownMemoryStore {
|
|
4
4
|
private readonly config;
|
|
@@ -13,6 +13,14 @@ export declare class MarkdownMemoryStore {
|
|
|
13
13
|
/** Resolved behavior thresholds — user config merged over defaults.
|
|
14
14
|
* Centralizes threshold resolution so every caller gets the same value. */
|
|
15
15
|
private get behavior();
|
|
16
|
+
/** Whether an embedder is configured — for mode indicator display. Read-only. */
|
|
17
|
+
get hasEmbedder(): boolean;
|
|
18
|
+
/** Count of vectorized entries — lightweight, no disk reload.
|
|
19
|
+
* For mode indicator display. Use stats() for full diagnostics. */
|
|
20
|
+
get vectorCount(): number;
|
|
21
|
+
/** Count of total entries — lightweight, no disk reload.
|
|
22
|
+
* For mode indicator display. Use stats() for full diagnostics. */
|
|
23
|
+
get entryCount(): number;
|
|
16
24
|
/** Initialize the store: create memory dir and load existing entries */
|
|
17
25
|
init(): Promise<void>;
|
|
18
26
|
/** Store a new knowledge entry */
|
|
@@ -26,6 +34,11 @@ export declare class MarkdownMemoryStore {
|
|
|
26
34
|
hasEntry(id: string): Promise<boolean>;
|
|
27
35
|
/** Correct an existing entry */
|
|
28
36
|
correct(id: string, correction: string, action: 'append' | 'replace' | 'delete'): Promise<CorrectResult>;
|
|
37
|
+
/** Re-embed all entries that don't have vectors.
|
|
38
|
+
* Idempotent: entries already in the vectors map are skipped.
|
|
39
|
+
* Early-exit: if the first embed fails, returns immediately (avoids burning through
|
|
40
|
+
* all entries just to discover the embedder is unavailable). */
|
|
41
|
+
reEmbed(): Promise<ReEmbedResult>;
|
|
29
42
|
/** Get memory health statistics */
|
|
30
43
|
stats(): Promise<MemoryStats>;
|
|
31
44
|
/** Bootstrap: scan repo structure and seed initial knowledge */
|
package/dist/store.js
CHANGED
|
@@ -7,7 +7,7 @@ import crypto from 'crypto';
|
|
|
7
7
|
import { execFile } from 'child_process';
|
|
8
8
|
import { promisify } from 'util';
|
|
9
9
|
import { DEFAULT_CONFIDENCE, realClock, parseTopicScope, parseTrustLevel, parseTags, asEmbeddingVector } from './types.js';
|
|
10
|
-
import { DEDUP_SIMILARITY_THRESHOLD, DEDUP_SEMANTIC_THRESHOLD, SEMANTIC_MIN_SIMILARITY, CONFLICT_SIMILARITY_THRESHOLD_SAME_TOPIC, CONFLICT_SIMILARITY_THRESHOLD_CROSS_TOPIC, CONFLICT_MIN_CONTENT_CHARS, OPPOSITION_PAIRS, PREFERENCE_SURFACE_THRESHOLD, TOPIC_BOOST, MODULE_TOPIC_BOOST, USER_ALWAYS_INCLUDE_SCORE_FRACTION, DEFAULT_STALE_DAYS_STANDARD, DEFAULT_STALE_DAYS_PREFERENCES, DEFAULT_MAX_STALE_IN_BRIEFING, DEFAULT_MAX_DEDUP_SUGGESTIONS, DEFAULT_MAX_CONFLICT_PAIRS, DEFAULT_MAX_PREFERENCE_SUGGESTIONS, } from './thresholds.js';
|
|
10
|
+
import { DEDUP_SIMILARITY_THRESHOLD, DEDUP_SEMANTIC_THRESHOLD, SEMANTIC_MIN_SIMILARITY, CONFLICT_SIMILARITY_THRESHOLD_SAME_TOPIC, CONFLICT_SIMILARITY_THRESHOLD_CROSS_TOPIC, CONFLICT_MIN_CONTENT_CHARS, OPPOSITION_PAIRS, PREFERENCE_SURFACE_THRESHOLD, TOPIC_BOOST, MODULE_TOPIC_BOOST, USER_ALWAYS_INCLUDE_SCORE_FRACTION, QUERY_EMBED_TIMEOUT_MS, DEFAULT_STALE_DAYS_STANDARD, DEFAULT_STALE_DAYS_PREFERENCES, DEFAULT_MAX_STALE_IN_BRIEFING, DEFAULT_MAX_DEDUP_SUGGESTIONS, DEFAULT_MAX_CONFLICT_PAIRS, DEFAULT_MAX_PREFERENCE_SUGGESTIONS, } from './thresholds.js';
|
|
11
11
|
import { realGitService } from './git-service.js';
|
|
12
12
|
import { extractKeywords, similarity, cosineSimilarity, matchesFilter, computeRelevanceScore, } from './text-analyzer.js';
|
|
13
13
|
import { keywordRank, semanticRank, mergeRankings } from './ranking.js';
|
|
@@ -38,6 +38,20 @@ export class MarkdownMemoryStore {
|
|
|
38
38
|
maxConflictPairs: b.maxConflictPairs ?? DEFAULT_MAX_CONFLICT_PAIRS,
|
|
39
39
|
};
|
|
40
40
|
}
|
|
41
|
+
/** Whether an embedder is configured — for mode indicator display. Read-only. */
|
|
42
|
+
get hasEmbedder() {
|
|
43
|
+
return this.embedder !== null;
|
|
44
|
+
}
|
|
45
|
+
/** Count of vectorized entries — lightweight, no disk reload.
|
|
46
|
+
* For mode indicator display. Use stats() for full diagnostics. */
|
|
47
|
+
get vectorCount() {
|
|
48
|
+
return this.vectors.size;
|
|
49
|
+
}
|
|
50
|
+
/** Count of total entries — lightweight, no disk reload.
|
|
51
|
+
* For mode indicator display. Use stats() for full diagnostics. */
|
|
52
|
+
get entryCount() {
|
|
53
|
+
return this.entries.size;
|
|
54
|
+
}
|
|
41
55
|
/** Initialize the store: create memory dir and load existing entries */
|
|
42
56
|
async init() {
|
|
43
57
|
await fs.mkdir(this.memoryPath, { recursive: true });
|
|
@@ -336,6 +350,44 @@ export class MarkdownMemoryStore {
|
|
|
336
350
|
}
|
|
337
351
|
return { corrected: true, id, action, newConfidence: 1.0, trust: 'user' };
|
|
338
352
|
}
|
|
353
|
+
/** Re-embed all entries that don't have vectors.
|
|
354
|
+
* Idempotent: entries already in the vectors map are skipped.
|
|
355
|
+
* Early-exit: if the first embed fails, returns immediately (avoids burning through
|
|
356
|
+
* all entries just to discover the embedder is unavailable). */
|
|
357
|
+
async reEmbed() {
|
|
358
|
+
if (!this.embedder) {
|
|
359
|
+
return { embedded: 0, skipped: 0, failed: 0, error: 'No embedder configured' };
|
|
360
|
+
}
|
|
361
|
+
await this.reloadFromDisk();
|
|
362
|
+
// Probe: try embedding a short text to check availability before iterating
|
|
363
|
+
const probe = await this.embedder.embed('probe');
|
|
364
|
+
if (!probe.ok) {
|
|
365
|
+
return { embedded: 0, skipped: 0, failed: 0, error: `Embedder unavailable: ${probe.failure.kind}` };
|
|
366
|
+
}
|
|
367
|
+
let embedded = 0;
|
|
368
|
+
let skipped = 0;
|
|
369
|
+
let failed = 0;
|
|
370
|
+
for (const entry of this.entries.values()) {
|
|
371
|
+
// Entry already has a vector with correct dimensions — skip
|
|
372
|
+
if (this.vectors.has(entry.id)) {
|
|
373
|
+
skipped++;
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
// Embed the entry
|
|
377
|
+
const embedText = `${entry.title}\n\n${entry.content}`;
|
|
378
|
+
const result = await this.embedder.embed(embedText);
|
|
379
|
+
if (result.ok) {
|
|
380
|
+
const file = this.entryToRelativePath(entry);
|
|
381
|
+
await this.persistVector(file, result.vector);
|
|
382
|
+
this.vectors.set(entry.id, result.vector);
|
|
383
|
+
embedded++;
|
|
384
|
+
}
|
|
385
|
+
else {
|
|
386
|
+
failed++;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
return { embedded, skipped, failed };
|
|
390
|
+
}
|
|
339
391
|
/** Get memory health statistics */
|
|
340
392
|
async stats() {
|
|
341
393
|
await this.reloadFromDisk();
|
|
@@ -368,6 +420,7 @@ export class MarkdownMemoryStore {
|
|
|
368
420
|
return {
|
|
369
421
|
totalEntries: allEntries.length,
|
|
370
422
|
corruptFiles: this.corruptFileCount,
|
|
423
|
+
vectorCount: this.vectors.size,
|
|
371
424
|
byTopic, byTrust, byFreshness, byTag,
|
|
372
425
|
storageSize: this.formatBytes(storageSize ?? 0),
|
|
373
426
|
storageBudgetBytes: this.config.storageBudgetBytes,
|
|
@@ -483,7 +536,8 @@ export class MarkdownMemoryStore {
|
|
|
483
536
|
const debug = process.env.MEMORY_MCP_DEBUG === '1';
|
|
484
537
|
let semanticResults = [];
|
|
485
538
|
if (this.embedder) {
|
|
486
|
-
const
|
|
539
|
+
const querySignal = AbortSignal.timeout(QUERY_EMBED_TIMEOUT_MS);
|
|
540
|
+
const queryResult = await this.embedder.embed(context, querySignal);
|
|
487
541
|
if (queryResult.ok) {
|
|
488
542
|
// In debug mode, get ALL scores (threshold=0) for calibration logging
|
|
489
543
|
const rawSemanticResults = semanticRank(allEntries, this.vectors, queryResult.vector, debug ? 0 : SEMANTIC_MIN_SIMILARITY, ctx);
|
package/dist/thresholds.d.ts
CHANGED
|
@@ -30,6 +30,10 @@ export declare const SEMANTIC_MIN_SIMILARITY = 0.45;
|
|
|
30
30
|
* disruptive than missing real ones. Two entries must be quite similar to be
|
|
31
31
|
* flagged as potential duplicates. */
|
|
32
32
|
export declare const DEDUP_SEMANTIC_THRESHOLD = 0.8;
|
|
33
|
+
/** Query-time embed timeout — tighter than store-time (5s) for responsiveness.
|
|
34
|
+
* Model-warm latency is ~10ms; 2s covers machine-under-load with margin.
|
|
35
|
+
* Cold starts handled by LazyEmbedder's probe (which uses the full 5s). */
|
|
36
|
+
export declare const QUERY_EMBED_TIMEOUT_MS = 2000;
|
|
33
37
|
/** Score multiplier when a reference path basename matches the context keywords. */
|
|
34
38
|
export declare const REFERENCE_BOOST_MULTIPLIER = 1.3;
|
|
35
39
|
/** Per-topic scoring boost factors for contextSearch().
|
package/dist/thresholds.js
CHANGED
|
@@ -56,6 +56,10 @@ export const SEMANTIC_MIN_SIMILARITY = 0.45;
|
|
|
56
56
|
* disruptive than missing real ones. Two entries must be quite similar to be
|
|
57
57
|
* flagged as potential duplicates. */
|
|
58
58
|
export const DEDUP_SEMANTIC_THRESHOLD = 0.80;
|
|
59
|
+
/** Query-time embed timeout — tighter than store-time (5s) for responsiveness.
|
|
60
|
+
* Model-warm latency is ~10ms; 2s covers machine-under-load with margin.
|
|
61
|
+
* Cold starts handled by LazyEmbedder's probe (which uses the full 5s). */
|
|
62
|
+
export const QUERY_EMBED_TIMEOUT_MS = 2000;
|
|
59
63
|
/** Score multiplier when a reference path basename matches the context keywords. */
|
|
60
64
|
export const REFERENCE_BOOST_MULTIPLIER = 1.30;
|
|
61
65
|
/** Per-topic scoring boost factors for contextSearch().
|
package/dist/types.d.ts
CHANGED
|
@@ -156,6 +156,7 @@ export type CorrectResult = {
|
|
|
156
156
|
export interface MemoryStats {
|
|
157
157
|
readonly totalEntries: number;
|
|
158
158
|
readonly corruptFiles: number;
|
|
159
|
+
readonly vectorCount: number;
|
|
159
160
|
readonly byTopic: Record<string, number>;
|
|
160
161
|
readonly byTrust: Record<TrustLevel, number>;
|
|
161
162
|
readonly byFreshness: {
|
|
@@ -222,6 +223,24 @@ export interface BehaviorConfig {
|
|
|
222
223
|
/** Maximum conflict pairs shown per query/context response. Default: 2. Range: 1–5. */
|
|
223
224
|
readonly maxConflictPairs?: number;
|
|
224
225
|
}
|
|
226
|
+
/** Supported embedding providers — closed union for exhaustive handling. */
|
|
227
|
+
export type EmbedderProvider = 'ollama' | 'none';
|
|
228
|
+
/** Embedding configuration from memory-config.json "embedder" block.
|
|
229
|
+
* All fields optional except provider — defaults are sensible for nomic-embed-text on localhost. */
|
|
230
|
+
export interface EmbedderConfig {
|
|
231
|
+
readonly provider: EmbedderProvider;
|
|
232
|
+
readonly model?: string;
|
|
233
|
+
readonly baseUrl?: string;
|
|
234
|
+
readonly timeoutMs?: number;
|
|
235
|
+
readonly dimensions?: number;
|
|
236
|
+
}
|
|
237
|
+
/** Result of a re-embed operation */
|
|
238
|
+
export interface ReEmbedResult {
|
|
239
|
+
readonly embedded: number;
|
|
240
|
+
readonly skipped: number;
|
|
241
|
+
readonly failed: number;
|
|
242
|
+
readonly error?: string;
|
|
243
|
+
}
|
|
225
244
|
/** Configuration for the memory MCP */
|
|
226
245
|
export interface MemoryConfig {
|
|
227
246
|
readonly repoRoot: string;
|