@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
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** Outcome of resolving which lobes to search when the agent didn't specify one. */
|
|
2
|
+
export type LobeResolution = {
|
|
3
|
+
readonly kind: 'resolved';
|
|
4
|
+
readonly lobes: readonly string[];
|
|
5
|
+
readonly label: string;
|
|
6
|
+
} | {
|
|
7
|
+
readonly kind: 'global-only';
|
|
8
|
+
readonly hint: string;
|
|
9
|
+
};
|
|
10
|
+
/** A root URI from the MCP client (e.g. "file:///Users/me/projects/zillow"). */
|
|
11
|
+
export interface ClientRoot {
|
|
12
|
+
readonly uri: string;
|
|
13
|
+
}
|
|
14
|
+
/** Minimal lobe config needed for matching — just the repo root path. */
|
|
15
|
+
export interface LobeRootConfig {
|
|
16
|
+
readonly name: string;
|
|
17
|
+
readonly repoRoot: string;
|
|
18
|
+
}
|
|
19
|
+
/** Match MCP client workspace root URIs against known lobe repo roots.
|
|
20
|
+
* Returns matched lobe names, or empty array if none match.
|
|
21
|
+
*
|
|
22
|
+
* Matching rules:
|
|
23
|
+
* - file:// URIs are stripped to filesystem paths
|
|
24
|
+
* - Both paths are normalized via path.resolve
|
|
25
|
+
* - A match occurs when either path is equal to or nested inside the other,
|
|
26
|
+
* checked at path-separator boundaries (no partial-name false positives) */
|
|
27
|
+
export declare function matchRootsToLobeNames(clientRoots: readonly ClientRoot[], lobeConfigs: readonly LobeRootConfig[]): readonly string[];
|
|
28
|
+
/** Build a LobeResolution from the available lobe names and matched lobes.
|
|
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;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// Pure lobe resolution logic — extracted for testability.
|
|
2
|
+
//
|
|
3
|
+
// When the agent doesn't specify a lobe, we determine which lobe(s) to search
|
|
4
|
+
// via a degradation ladder:
|
|
5
|
+
// 1. Single lobe configured → use it (unambiguous)
|
|
6
|
+
// 2. Multiple lobes → match client workspace roots against lobe repo roots
|
|
7
|
+
// 3. Fallback → global-only with a hint to specify the lobe
|
|
8
|
+
//
|
|
9
|
+
// This prevents cross-lobe leakage (e.g. game design lore surfacing in an Android MR review).
|
|
10
|
+
import path from 'path';
|
|
11
|
+
/** Check if `child` is equal to or nested under `parent` with path-boundary awareness.
|
|
12
|
+
* Prevents false matches like "/projects/zillow-tools" matching "/projects/zillow". */
|
|
13
|
+
function isPathPrefixOf(parent, child) {
|
|
14
|
+
if (child === parent)
|
|
15
|
+
return true;
|
|
16
|
+
// Ensure the prefix ends at a path separator boundary
|
|
17
|
+
const withSep = parent.endsWith(path.sep) ? parent : parent + path.sep;
|
|
18
|
+
return child.startsWith(withSep);
|
|
19
|
+
}
|
|
20
|
+
/** Match MCP client workspace root URIs against known lobe repo roots.
|
|
21
|
+
* Returns matched lobe names, or empty array if none match.
|
|
22
|
+
*
|
|
23
|
+
* Matching rules:
|
|
24
|
+
* - file:// URIs are stripped to filesystem paths
|
|
25
|
+
* - Both paths are normalized via path.resolve
|
|
26
|
+
* - A match occurs when either path is equal to or nested inside the other,
|
|
27
|
+
* checked at path-separator boundaries (no partial-name false positives) */
|
|
28
|
+
export function matchRootsToLobeNames(clientRoots, lobeConfigs) {
|
|
29
|
+
if (clientRoots.length === 0 || lobeConfigs.length === 0)
|
|
30
|
+
return [];
|
|
31
|
+
const matchedLobes = new Set();
|
|
32
|
+
for (const root of clientRoots) {
|
|
33
|
+
// MCP roots use file:// URIs — strip the scheme to get the filesystem path
|
|
34
|
+
const rootPath = root.uri.startsWith('file://') ? root.uri.slice(7) : root.uri;
|
|
35
|
+
const normalizedRoot = path.resolve(rootPath);
|
|
36
|
+
for (const lobe of lobeConfigs) {
|
|
37
|
+
const normalizedLobe = path.resolve(lobe.repoRoot);
|
|
38
|
+
// Match if one path is equal to or nested inside the other
|
|
39
|
+
if (isPathPrefixOf(normalizedLobe, normalizedRoot) || isPathPrefixOf(normalizedRoot, normalizedLobe)) {
|
|
40
|
+
matchedLobes.add(lobe.name);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return Array.from(matchedLobes);
|
|
45
|
+
}
|
|
46
|
+
/** Build a LobeResolution from the available lobe names and matched lobes.
|
|
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) {
|
|
53
|
+
// Single lobe — always resolved, regardless of root matching
|
|
54
|
+
if (allLobeNames.length === 1 && alwaysIncludeLobes.length === 0) {
|
|
55
|
+
return { kind: 'resolved', lobes: allLobeNames, label: allLobeNames[0] };
|
|
56
|
+
}
|
|
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);
|
|
77
|
+
return {
|
|
78
|
+
kind: 'resolved',
|
|
79
|
+
lobes,
|
|
80
|
+
label: lobes.length === 1 ? lobes[0] : lobes.join('+'),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
// Fallback — no lobes could be determined
|
|
84
|
+
return {
|
|
85
|
+
kind: 'global-only',
|
|
86
|
+
hint: `Multiple lobes available (${allLobeNames.join(', ')}) but none could be inferred from client workspace roots. ` +
|
|
87
|
+
`Specify lobe parameter for lobe-specific results.`,
|
|
88
|
+
};
|
|
89
|
+
}
|
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
|
@@ -10,7 +10,7 @@ import { DEFAULT_CONFIDENCE, realClock, parseTopicScope, parseTrustLevel, parseT
|
|
|
10
10
|
import { DEDUP_SIMILARITY_THRESHOLD, CONFLICT_SIMILARITY_THRESHOLD_SAME_TOPIC, CONFLICT_SIMILARITY_THRESHOLD_CROSS_TOPIC, CONFLICT_MIN_CONTENT_CHARS, OPPOSITION_PAIRS, PREFERENCE_SURFACE_THRESHOLD, REFERENCE_BOOST_MULTIPLIER, 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, TAG_MATCH_BOOST, } from './thresholds.js';
|
|
11
11
|
import { realGitService } from './git-service.js';
|
|
12
12
|
import { extractKeywords, stem, similarity, matchesFilter, computeRelevanceScore, } from './text-analyzer.js';
|
|
13
|
-
import { detectEphemeralSignals, formatEphemeralWarning } from './ephemeral.js';
|
|
13
|
+
import { detectEphemeralSignals, formatEphemeralWarning, getEphemeralSeverity } from './ephemeral.js';
|
|
14
14
|
// Used only by bootstrap() for git log — not part of the GitService boundary
|
|
15
15
|
// because bootstrap is a one-shot utility, not a recurring operation
|
|
16
16
|
const execFileAsync = promisify(execFile);
|
|
@@ -89,9 +89,12 @@ export class MarkdownMemoryStore {
|
|
|
89
89
|
const ephemeralSignals = topic !== 'recent-work'
|
|
90
90
|
? detectEphemeralSignals(title, content, topic)
|
|
91
91
|
: [];
|
|
92
|
-
|
|
92
|
+
// getEphemeralSeverity is the single source of threshold logic shared with formatEphemeralWarning.
|
|
93
|
+
const ephemeralSeverity = getEphemeralSeverity(ephemeralSignals);
|
|
94
|
+
const ephemeralWarning = formatEphemeralWarning(ephemeralSignals, id);
|
|
93
95
|
return {
|
|
94
96
|
stored: true, id, topic, file, confidence, warning, ephemeralWarning,
|
|
97
|
+
ephemeralSeverity: ephemeralSeverity ?? undefined,
|
|
95
98
|
relatedEntries: relatedEntries.length > 0 ? relatedEntries : undefined,
|
|
96
99
|
relevantPreferences: relevantPreferences && relevantPreferences.length > 0 ? relevantPreferences : undefined,
|
|
97
100
|
};
|
|
@@ -253,6 +256,12 @@ export class MarkdownMemoryStore {
|
|
|
253
256
|
suggestion,
|
|
254
257
|
};
|
|
255
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
|
+
}
|
|
256
265
|
/** Correct an existing entry */
|
|
257
266
|
async correct(id, correction, action) {
|
|
258
267
|
// Reload to ensure we have the latest
|
package/dist/thresholds.d.ts
CHANGED
|
@@ -16,14 +16,6 @@ export declare const CONFLICT_MIN_CONTENT_CHARS = 50;
|
|
|
16
16
|
export declare const OPPOSITION_PAIRS: ReadonlyArray<readonly [string, string]>;
|
|
17
17
|
/** Score multiplier when a reference path basename matches the context keywords. */
|
|
18
18
|
export declare const REFERENCE_BOOST_MULTIPLIER = 1.3;
|
|
19
|
-
/** Score multiplier applied to weak cross-lobe results in multi-lobe context search.
|
|
20
|
-
* Prevents generic software terms (e.g. "codebase", "structure") from surfacing
|
|
21
|
-
* entries from unrelated repos with high confidence/topic-boost scores. */
|
|
22
|
-
export declare const CROSS_LOBE_WEAK_SCORE_PENALTY = 0.5;
|
|
23
|
-
/** Fraction of context keywords an entry must match to avoid the cross-lobe penalty.
|
|
24
|
-
* E.g. 0.40 means an entry must match at least 40% of the context keywords (minimum 2)
|
|
25
|
-
* to be treated as a strong cross-lobe match. */
|
|
26
|
-
export declare const CROSS_LOBE_MIN_MATCH_RATIO = 0.4;
|
|
27
19
|
/** Per-topic scoring boost factors for contextSearch().
|
|
28
20
|
* Higher = more likely to surface for any given context. */
|
|
29
21
|
export declare const TOPIC_BOOST: Record<string, number>;
|
|
@@ -51,3 +43,6 @@ export declare const TAG_MATCH_BOOST = 1.5;
|
|
|
51
43
|
export declare const VOCABULARY_ECHO_LIMIT = 8;
|
|
52
44
|
/** Maximum tags shown in query/context footer. */
|
|
53
45
|
export declare const MAX_FOOTER_TAGS = 12;
|
|
46
|
+
/** Visual separator for warning blocks in tool responses.
|
|
47
|
+
* Width chosen to stand out as a block boundary in any terminal or chat rendering. */
|
|
48
|
+
export declare const WARN_SEPARATOR: string;
|
package/dist/thresholds.js
CHANGED
|
@@ -42,14 +42,6 @@ export const OPPOSITION_PAIRS = [
|
|
|
42
42
|
];
|
|
43
43
|
/** Score multiplier when a reference path basename matches the context keywords. */
|
|
44
44
|
export const REFERENCE_BOOST_MULTIPLIER = 1.30;
|
|
45
|
-
/** Score multiplier applied to weak cross-lobe results in multi-lobe context search.
|
|
46
|
-
* Prevents generic software terms (e.g. "codebase", "structure") from surfacing
|
|
47
|
-
* entries from unrelated repos with high confidence/topic-boost scores. */
|
|
48
|
-
export const CROSS_LOBE_WEAK_SCORE_PENALTY = 0.50;
|
|
49
|
-
/** Fraction of context keywords an entry must match to avoid the cross-lobe penalty.
|
|
50
|
-
* E.g. 0.40 means an entry must match at least 40% of the context keywords (minimum 2)
|
|
51
|
-
* to be treated as a strong cross-lobe match. */
|
|
52
|
-
export const CROSS_LOBE_MIN_MATCH_RATIO = 0.40;
|
|
53
45
|
/** Per-topic scoring boost factors for contextSearch().
|
|
54
46
|
* Higher = more likely to surface for any given context. */
|
|
55
47
|
export const TOPIC_BOOST = {
|
|
@@ -87,3 +79,7 @@ export const TAG_MATCH_BOOST = 1.5;
|
|
|
87
79
|
export const VOCABULARY_ECHO_LIMIT = 8;
|
|
88
80
|
/** Maximum tags shown in query/context footer. */
|
|
89
81
|
export const MAX_FOOTER_TAGS = 12;
|
|
82
|
+
// ─── Display formatting constants ───────────────────────────────────────────
|
|
83
|
+
/** Visual separator for warning blocks in tool responses.
|
|
84
|
+
* Width chosen to stand out as a block boundary in any terminal or chat rendering. */
|
|
85
|
+
export const WARN_SEPARATOR = '='.repeat(52);
|
package/dist/types.d.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
/** Trust levels for knowledge sources, ordered by reliability */
|
|
2
2
|
export type TrustLevel = 'user' | 'agent-confirmed' | 'agent-inferred';
|
|
3
|
+
/** Ephemeral detection severity — three distinct levels so consumers can branch exhaustively.
|
|
4
|
+
* Separated from EphemeralSignal.confidence to represent the aggregate outcome of all signals. */
|
|
5
|
+
export type EphemeralSeverity = 'high' | 'medium' | 'low';
|
|
3
6
|
/** Parse a raw string into a TrustLevel, returning null for invalid input */
|
|
4
7
|
export declare function parseTrustLevel(raw: string): TrustLevel | null;
|
|
5
8
|
/** Predefined topic scopes for organizing knowledge */
|
|
@@ -96,6 +99,8 @@ export type StoreResult = {
|
|
|
96
99
|
readonly warning?: string;
|
|
97
100
|
/** Soft warning when content looks ephemeral — informational, never blocking */
|
|
98
101
|
readonly ephemeralWarning?: string;
|
|
102
|
+
/** Aggregate severity of all ephemeral signals that fired — absent when none fired */
|
|
103
|
+
readonly ephemeralSeverity?: EphemeralSeverity;
|
|
99
104
|
readonly relatedEntries?: readonly RelatedEntry[];
|
|
100
105
|
readonly relevantPreferences?: readonly RelatedEntry[];
|
|
101
106
|
} | {
|
|
@@ -190,6 +195,7 @@ export interface MemoryConfig {
|
|
|
190
195
|
readonly repoRoot: string;
|
|
191
196
|
readonly memoryPath: string;
|
|
192
197
|
readonly storageBudgetBytes: number;
|
|
198
|
+
readonly alwaysInclude: boolean;
|
|
193
199
|
readonly behavior?: BehaviorConfig;
|
|
194
200
|
readonly clock?: Clock;
|
|
195
201
|
readonly git?: GitService;
|