@exaudeus/memory-mcp 1.2.0 → 1.3.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/ephemeral.d.ts +7 -3
- package/dist/ephemeral.js +66 -23
- package/dist/formatters.js +22 -12
- package/dist/index.js +146 -89
- package/dist/lobe-resolution.d.ts +30 -0
- package/dist/lobe-resolution.js +67 -0
- package/dist/store.js +5 -2
- package/dist/thresholds.d.ts +3 -8
- package/dist/thresholds.js +4 -8
- package/dist/types.d.ts +5 -0
- package/package.json +1 -1
package/dist/ephemeral.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { TopicScope } from './types.js';
|
|
1
|
+
import type { EphemeralSeverity, TopicScope } from './types.js';
|
|
2
2
|
/** Run the TF-IDF classifier on a text. Returns probability 0-1 that content is ephemeral.
|
|
3
3
|
* Supports both v1 (unigrams only) and v2 (bigrams + engineered features) models. */
|
|
4
4
|
export declare function classifyEphemeral(title: string, content: string, topic?: string): number | null;
|
|
@@ -15,6 +15,10 @@ export interface EphemeralSignal {
|
|
|
15
15
|
* Returns an array of matched signals, empty if content looks durable.
|
|
16
16
|
* Pure function — no side effects, no I/O. */
|
|
17
17
|
export declare function detectEphemeralSignals(title: string, content: string, topic: TopicScope): readonly EphemeralSignal[];
|
|
18
|
-
/**
|
|
18
|
+
/** Derive aggregate severity from a set of detected signals.
|
|
19
|
+
* Single source of truth for the threshold logic — both formatEphemeralWarning
|
|
20
|
+
* and store.ts call this so the thresholds can only diverge in one place. */
|
|
21
|
+
export declare function getEphemeralSeverity(signals: readonly EphemeralSignal[]): EphemeralSeverity | null;
|
|
22
|
+
/** Format ephemeral signals into a visually prominent warning block.
|
|
19
23
|
* Returns undefined if no signals were detected. */
|
|
20
|
-
export declare function formatEphemeralWarning(signals: readonly EphemeralSignal[]): string | undefined;
|
|
24
|
+
export declare function formatEphemeralWarning(signals: readonly EphemeralSignal[], entryId: string): string | undefined;
|
package/dist/ephemeral.js
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
import { readFileSync } from 'fs';
|
|
19
19
|
import { dirname, join } from 'path';
|
|
20
20
|
import { fileURLToPath } from 'url';
|
|
21
|
+
import { WARN_SEPARATOR } from './thresholds.js';
|
|
21
22
|
let cachedModel = null;
|
|
22
23
|
function loadModel() {
|
|
23
24
|
if (cachedModel)
|
|
@@ -523,6 +524,7 @@ const SIGNALS = [
|
|
|
523
524
|
test: (_title, content) => {
|
|
524
525
|
const patterns = [
|
|
525
526
|
/[,;]\s+also\b/i, // "X works this way, also Y does Z"
|
|
527
|
+
/[.!?]\s+also[,\s]/i, // ". Also, Y does Z" (sentence-start also)
|
|
526
528
|
/[.!?]\s+additionally,/i, // ". Additionally, ..."
|
|
527
529
|
/[.!?]\s+furthermore,/i, // ". Furthermore, ..."
|
|
528
530
|
/\bunrelated:/i, // "Unrelated: ..."
|
|
@@ -589,30 +591,71 @@ export function detectEphemeralSignals(title, content, topic) {
|
|
|
589
591
|
}
|
|
590
592
|
return signals;
|
|
591
593
|
}
|
|
592
|
-
/**
|
|
593
|
-
*
|
|
594
|
-
|
|
594
|
+
/** Derive aggregate severity from a set of detected signals.
|
|
595
|
+
* Single source of truth for the threshold logic — both formatEphemeralWarning
|
|
596
|
+
* and store.ts call this so the thresholds can only diverge in one place. */
|
|
597
|
+
export function getEphemeralSeverity(signals) {
|
|
595
598
|
if (signals.length === 0)
|
|
596
|
-
return
|
|
599
|
+
return null;
|
|
597
600
|
const highCount = signals.filter(s => s.confidence === 'high').length;
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
601
|
+
if (highCount >= 2)
|
|
602
|
+
return 'high';
|
|
603
|
+
if (highCount === 1)
|
|
604
|
+
return 'medium';
|
|
605
|
+
return 'low';
|
|
606
|
+
}
|
|
607
|
+
/** Format ephemeral signals into a visually prominent warning block.
|
|
608
|
+
* Returns undefined if no signals were detected. */
|
|
609
|
+
export function formatEphemeralWarning(signals, entryId) {
|
|
610
|
+
const severity = getEphemeralSeverity(signals);
|
|
611
|
+
if (severity === null)
|
|
612
|
+
return undefined;
|
|
613
|
+
const signalLines = signals.map(s => ` - ${s.label}: ${s.detail}`).join('\n');
|
|
614
|
+
// Delete command without leading spaces — callsites add their own indentation.
|
|
615
|
+
const deleteCmd = `memory_correct(id: "${entryId}", action: "delete")`;
|
|
616
|
+
// Scale both the header and guidance text with severity.
|
|
617
|
+
// high: near-certain ephemeral → "ACTION REQUIRED" + prominent DELETE label.
|
|
618
|
+
// medium: likely ephemeral → "REVIEW NEEDED" + delete as an option.
|
|
619
|
+
// low: uncertain → "CHECK BELOW" + softest framing.
|
|
620
|
+
switch (severity) {
|
|
621
|
+
case 'high':
|
|
622
|
+
return [
|
|
623
|
+
WARN_SEPARATOR,
|
|
624
|
+
'⚠ EPHEMERAL CONTENT — ACTION REQUIRED ⚠',
|
|
625
|
+
WARN_SEPARATOR,
|
|
626
|
+
signalLines,
|
|
627
|
+
'',
|
|
628
|
+
'Will this still be true in 6 months? Almost certainly not.',
|
|
629
|
+
'',
|
|
630
|
+
`DELETE: ${deleteCmd}`,
|
|
631
|
+
'',
|
|
632
|
+
'If there is a lasting insight here, extract and rephrase',
|
|
633
|
+
'it as a present-tense fact before deleting this entry.',
|
|
634
|
+
WARN_SEPARATOR,
|
|
635
|
+
].join('\n');
|
|
636
|
+
case 'medium':
|
|
637
|
+
return [
|
|
638
|
+
WARN_SEPARATOR,
|
|
639
|
+
'⚠ POSSIBLY EPHEMERAL CONTENT — REVIEW NEEDED ⚠',
|
|
640
|
+
WARN_SEPARATOR,
|
|
641
|
+
signalLines,
|
|
642
|
+
'',
|
|
643
|
+
'Will this still be true in 6 months?',
|
|
644
|
+
` - If not: ${deleteCmd}`,
|
|
645
|
+
` - If so: rephrase as a present-tense fact (what is now true)`,
|
|
646
|
+
WARN_SEPARATOR,
|
|
647
|
+
].join('\n');
|
|
648
|
+
case 'low':
|
|
649
|
+
return [
|
|
650
|
+
WARN_SEPARATOR,
|
|
651
|
+
'⚠ POSSIBLY EPHEMERAL CONTENT — CHECK BELOW ⚠',
|
|
652
|
+
WARN_SEPARATOR,
|
|
653
|
+
signalLines,
|
|
654
|
+
'',
|
|
655
|
+
'Ask: will this still be true in 6 months?',
|
|
656
|
+
` - If not: ${deleteCmd}`,
|
|
657
|
+
` - If so: rephrase as a present-tense fact if possible`,
|
|
658
|
+
WARN_SEPARATOR,
|
|
659
|
+
].join('\n');
|
|
616
660
|
}
|
|
617
|
-
return lines.join('\n');
|
|
618
661
|
}
|
package/dist/formatters.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
//
|
|
3
3
|
// Pure functions — no side effects, no state. Each takes structured data
|
|
4
4
|
// and returns a formatted string for the tool response.
|
|
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, } from './thresholds.js';
|
|
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
7
|
/** Format the stale entries section for briefing/context responses */
|
|
8
8
|
export function formatStaleSection(staleDetails) {
|
|
@@ -17,23 +17,33 @@ export function formatStaleSection(staleDetails) {
|
|
|
17
17
|
}
|
|
18
18
|
/** Format the conflict detection warning for query/context responses */
|
|
19
19
|
export function formatConflictWarning(conflicts) {
|
|
20
|
-
const lines = [
|
|
20
|
+
const lines = [
|
|
21
|
+
WARN_SEPARATOR,
|
|
22
|
+
'⚠ CONFLICTING ENTRIES DETECTED — ACTION NEEDED ⚠',
|
|
23
|
+
WARN_SEPARATOR,
|
|
24
|
+
];
|
|
21
25
|
for (const c of conflicts) {
|
|
22
|
-
lines.push(`
|
|
23
|
-
lines.push(` vs
|
|
24
|
-
lines.push(`
|
|
25
|
-
|
|
26
|
+
lines.push(` ${c.a.id}: "${c.a.title}" (confidence: ${c.a.confidence}, ${c.a.created.substring(0, 10)})`);
|
|
27
|
+
lines.push(` vs`);
|
|
28
|
+
lines.push(` ${c.b.id}: "${c.b.title}" (confidence: ${c.b.confidence}, ${c.b.created.substring(0, 10)})`);
|
|
29
|
+
lines.push(` Similarity: ${(c.similarity * 100).toFixed(0)}%`);
|
|
30
|
+
lines.push('');
|
|
31
|
+
// Pre-fill which entry to delete so the agent can act immediately.
|
|
26
32
|
if (c.a.confidence !== c.b.confidence) {
|
|
27
|
-
const
|
|
28
|
-
|
|
33
|
+
const keep = c.a.confidence > c.b.confidence ? c.a : c.b;
|
|
34
|
+
const remove = c.a.confidence > c.b.confidence ? c.b : c.a;
|
|
35
|
+
lines.push(` Trust ${keep.id} (higher confidence). Delete the lower-confidence entry:`);
|
|
36
|
+
lines.push(` memory_correct(id: "${remove.id}", action: "delete")`);
|
|
29
37
|
}
|
|
30
38
|
else {
|
|
31
|
-
const
|
|
32
|
-
|
|
39
|
+
const keep = c.a.created > c.b.created ? c.a : c.b;
|
|
40
|
+
const remove = c.a.created > c.b.created ? c.b : c.a;
|
|
41
|
+
lines.push(` ${keep.id} is more recent — may supersede ${remove.id}:`);
|
|
42
|
+
lines.push(` memory_correct(id: "${remove.id}", action: "delete")`);
|
|
33
43
|
}
|
|
44
|
+
lines.push('');
|
|
34
45
|
}
|
|
35
|
-
lines.push(
|
|
36
|
-
lines.push('Consider: memory_correct to consolidate or clarify the difference between these entries.');
|
|
46
|
+
lines.push(WARN_SEPARATOR);
|
|
37
47
|
return lines.join('\n');
|
|
38
48
|
}
|
|
39
49
|
/** Format memory stats for a single lobe or global store */
|
package/dist/index.js
CHANGED
|
@@ -17,8 +17,9 @@ import { ConfigManager } from './config-manager.js';
|
|
|
17
17
|
import { normalizeArgs } from './normalize.js';
|
|
18
18
|
import { buildCrashReport, writeCrashReport, writeCrashReportSync, readLatestCrash, readCrashHistory, clearLatestCrash, formatCrashReport, formatCrashSummary, markServerStarted, } from './crash-journal.js';
|
|
19
19
|
import { formatStaleSection, formatConflictWarning, formatStats, formatBehaviorConfigSection, mergeTagFrequencies, buildQueryFooter, buildTagPrimerSection } from './formatters.js';
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
20
|
+
import { parseFilter } from './text-analyzer.js';
|
|
21
|
+
import { VOCABULARY_ECHO_LIMIT, WARN_SEPARATOR } from './thresholds.js';
|
|
22
|
+
import { matchRootsToLobeNames, buildLobeResolution } from './lobe-resolution.js';
|
|
22
23
|
let serverMode = { kind: 'running' };
|
|
23
24
|
const lobeHealth = new Map();
|
|
24
25
|
const serverStartTime = Date.now();
|
|
@@ -154,6 +155,43 @@ function inferLobeFromPaths(paths) {
|
|
|
154
155
|
return matchedLobes.size === 1 ? matchedLobes.values().next().value : undefined;
|
|
155
156
|
}
|
|
156
157
|
const server = new Server({ name: 'memory-mcp', version: '0.1.0' }, { capabilities: { tools: {} } });
|
|
158
|
+
// --- Lobe resolution for read operations ---
|
|
159
|
+
// When the agent doesn't specify a lobe, we determine which lobe(s) to search
|
|
160
|
+
// via a degradation ladder (see lobe-resolution.ts for the pure logic):
|
|
161
|
+
// 1. Single lobe configured → use it (unambiguous)
|
|
162
|
+
// 2. Multiple lobes → ask client for workspace roots via MCP roots/list
|
|
163
|
+
// 3. Fallback → global-only with a hint to specify the lobe
|
|
164
|
+
/** Resolve which lobes to search for a read operation when the agent omitted the lobe param.
|
|
165
|
+
* Wires the MCP server's listRoots into the pure resolution logic. */
|
|
166
|
+
async function resolveLobesForRead() {
|
|
167
|
+
const allLobeNames = configManager.getLobeNames();
|
|
168
|
+
// Short-circuit: single lobe is unambiguous
|
|
169
|
+
if (allLobeNames.length === 1) {
|
|
170
|
+
return buildLobeResolution(allLobeNames, allLobeNames);
|
|
171
|
+
}
|
|
172
|
+
// Multiple lobes — try MCP client roots
|
|
173
|
+
const clientCaps = server.getClientCapabilities();
|
|
174
|
+
if (clientCaps?.roots) {
|
|
175
|
+
try {
|
|
176
|
+
const { roots } = await server.listRoots();
|
|
177
|
+
if (roots && roots.length > 0) {
|
|
178
|
+
const lobeConfigs = allLobeNames
|
|
179
|
+
.map(name => {
|
|
180
|
+
const config = configManager.getLobeConfig(name);
|
|
181
|
+
return config ? { name, repoRoot: config.repoRoot } : undefined;
|
|
182
|
+
})
|
|
183
|
+
.filter((c) => c !== undefined);
|
|
184
|
+
const matched = matchRootsToLobeNames(roots, lobeConfigs);
|
|
185
|
+
return buildLobeResolution(allLobeNames, matched);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
catch (err) {
|
|
189
|
+
process.stderr.write(`[memory-mcp] listRoots failed: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
// Fallback — roots not available or no match
|
|
193
|
+
return buildLobeResolution(allLobeNames, []);
|
|
194
|
+
}
|
|
157
195
|
/** Build the shared lobe property for tool schemas — called on each ListTools request
|
|
158
196
|
* so the description and enum stay in sync after a hot-reload adds or removes lobes. */
|
|
159
197
|
function buildLobeProperty(currentLobeNames) {
|
|
@@ -162,7 +200,7 @@ function buildLobeProperty(currentLobeNames) {
|
|
|
162
200
|
type: 'string',
|
|
163
201
|
description: isSingle
|
|
164
202
|
? `Memory lobe name (defaults to "${currentLobeNames[0]}" if omitted)`
|
|
165
|
-
: `Memory lobe name.
|
|
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, only global knowledge (user/preferences) is returned — specify a lobe explicitly to access lobe-specific knowledge. Required for writes. Available: ${currentLobeNames.join(', ')}`,
|
|
166
204
|
enum: currentLobeNames.length > 1 ? [...currentLobeNames] : undefined,
|
|
167
205
|
};
|
|
168
206
|
}
|
|
@@ -302,7 +340,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
302
340
|
},
|
|
303
341
|
{
|
|
304
342
|
name: 'memory_context',
|
|
305
|
-
description: 'Session start AND pre-task lookup. Call with no args at session start to get user identity, preferences, and stale entries. Call with context to get task-specific knowledge.
|
|
343
|
+
description: 'Session start AND pre-task lookup. Call with no args at session start to get user identity, preferences, and stale entries. Call with context to get task-specific knowledge. When lobe is omitted, uses client workspace roots to select the matching lobe; falls back to global-only if roots are unavailable. Example: memory_context() or memory_context(context: "writing a Kotlin reducer")',
|
|
306
344
|
inputSchema: {
|
|
307
345
|
type: 'object',
|
|
308
346
|
properties: {
|
|
@@ -456,11 +494,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
456
494
|
}
|
|
457
495
|
storedResults.push({ title, result });
|
|
458
496
|
}
|
|
459
|
-
// Build response header
|
|
497
|
+
// Build response header.
|
|
498
|
+
// For high-severity ephemeral detections, flag the success line itself so agents
|
|
499
|
+
// who anchor on line 1 still see the problem before reading the block below.
|
|
460
500
|
const lines = [];
|
|
461
501
|
if (storedResults.length === 1) {
|
|
462
502
|
const { result } = storedResults[0];
|
|
463
|
-
|
|
503
|
+
const ephemeralFlag = result.ephemeralSeverity === 'high' ? ' (⚠ ephemeral — see below)' : '';
|
|
504
|
+
lines.push(`[${ctx.label}] Stored entry ${result.id} in ${result.topic} (confidence: ${result.confidence})${ephemeralFlag}`);
|
|
464
505
|
if (result.warning)
|
|
465
506
|
lines.push(`Note: ${result.warning}`);
|
|
466
507
|
}
|
|
@@ -468,7 +509,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
468
509
|
const { result: first } = storedResults[0];
|
|
469
510
|
lines.push(`[${ctx.label}] Stored ${storedResults.length} entries in ${first.topic} (confidence: ${first.confidence}):`);
|
|
470
511
|
for (const { title, result } of storedResults) {
|
|
471
|
-
|
|
512
|
+
const ephemeralFlag = result.ephemeralSeverity === 'high' ? ' ⚠' : '';
|
|
513
|
+
lines.push(` - ${result.id}: "${title}"${ephemeralFlag}`);
|
|
472
514
|
}
|
|
473
515
|
}
|
|
474
516
|
// Limit to at most 2 hint sections per response to prevent hint fatigue.
|
|
@@ -478,23 +520,35 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
478
520
|
let hintCount = 0;
|
|
479
521
|
for (const { title, result } of storedResults) {
|
|
480
522
|
const entryPrefix = storedResults.length > 1 ? `"${title}": ` : '';
|
|
481
|
-
// Dedup: surface related entries in the same topic
|
|
523
|
+
// Dedup: surface related entries in the same topic.
|
|
524
|
+
// Fill in both actual IDs so the agent can act immediately without looking them up.
|
|
482
525
|
if (result.relatedEntries && result.relatedEntries.length > 0 && hintCount < 2) {
|
|
483
526
|
hintCount++;
|
|
527
|
+
const top = result.relatedEntries[0];
|
|
484
528
|
lines.push('');
|
|
485
|
-
lines.push(
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
529
|
+
lines.push(WARN_SEPARATOR);
|
|
530
|
+
lines.push(`⚠ ${entryPrefix}SIMILAR ENTRY ALREADY EXISTS — CONSOLIDATE ⚠`);
|
|
531
|
+
lines.push(WARN_SEPARATOR);
|
|
532
|
+
lines.push(` ${top.id}: "${top.title}" (confidence: ${top.confidence})`);
|
|
533
|
+
lines.push(` ${top.content.length > 120 ? top.content.substring(0, 120) + '...' : top.content}`);
|
|
534
|
+
if (result.relatedEntries.length > 1) {
|
|
535
|
+
const extra = result.relatedEntries.length - 1;
|
|
536
|
+
lines.push(` ... and ${extra} more similar ${extra === 1 ? 'entry' : 'entries'}`);
|
|
489
537
|
}
|
|
490
538
|
lines.push('');
|
|
491
|
-
lines.push('
|
|
539
|
+
lines.push('If these overlap, consolidate:');
|
|
540
|
+
lines.push(` KEEP+UPDATE: memory_correct(id: "${top.id}", action: "replace", correction: "<merged content>")`);
|
|
541
|
+
lines.push(` DELETE new: memory_correct(id: "${result.id}", action: "delete")`);
|
|
542
|
+
lines.push(WARN_SEPARATOR);
|
|
492
543
|
}
|
|
493
|
-
// Ephemeral content warning —
|
|
544
|
+
// Ephemeral content warning — the formatted block already contains visual borders
|
|
545
|
+
// and pre-filled delete command from formatEphemeralWarning.
|
|
494
546
|
if (result.ephemeralWarning && hintCount < 2) {
|
|
495
547
|
hintCount++;
|
|
496
548
|
lines.push('');
|
|
497
|
-
|
|
549
|
+
if (entryPrefix)
|
|
550
|
+
lines.push(`${entryPrefix}:`);
|
|
551
|
+
lines.push(result.ephemeralWarning);
|
|
498
552
|
}
|
|
499
553
|
// Preference surfacing: show relevant preferences for non-preference entries
|
|
500
554
|
if (result.relevantPreferences && result.relevantPreferences.length > 0 && hintCount < 2) {
|
|
@@ -533,13 +587,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
533
587
|
branch: z.string().optional(),
|
|
534
588
|
}).parse(args ?? {});
|
|
535
589
|
const isGlobalQuery = GLOBAL_TOPICS.has(scope);
|
|
536
|
-
//
|
|
537
|
-
//
|
|
590
|
+
// Resolve which lobes to search.
|
|
591
|
+
// Global topics always route to globalStore. Lobe topics follow the degradation ladder.
|
|
538
592
|
let lobeEntries = [];
|
|
539
|
-
const entryLobeMap = new Map(); // entry id → lobe name
|
|
593
|
+
const entryLobeMap = new Map(); // entry id → lobe name
|
|
540
594
|
let label;
|
|
541
595
|
let primaryStore;
|
|
542
|
-
let
|
|
596
|
+
let queryGlobalOnlyHint;
|
|
543
597
|
if (isGlobalQuery) {
|
|
544
598
|
const ctx = resolveToolContext(rawLobe, { isGlobal: true });
|
|
545
599
|
if (!ctx.ok)
|
|
@@ -561,20 +615,30 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
561
615
|
lobeEntries = [...result.entries];
|
|
562
616
|
}
|
|
563
617
|
else {
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
618
|
+
const resolution = await resolveLobesForRead();
|
|
619
|
+
switch (resolution.kind) {
|
|
620
|
+
case 'resolved': {
|
|
621
|
+
label = resolution.label;
|
|
622
|
+
for (const lobeName of resolution.lobes) {
|
|
623
|
+
const store = configManager.getStore(lobeName);
|
|
624
|
+
if (!store)
|
|
625
|
+
continue;
|
|
626
|
+
if (!primaryStore)
|
|
627
|
+
primaryStore = store;
|
|
628
|
+
const result = await store.query(scope, detail, filter, branch);
|
|
629
|
+
if (resolution.lobes.length > 1) {
|
|
630
|
+
for (const e of result.entries)
|
|
631
|
+
entryLobeMap.set(e.id, lobeName);
|
|
632
|
+
}
|
|
633
|
+
lobeEntries.push(...result.entries);
|
|
634
|
+
}
|
|
635
|
+
break;
|
|
636
|
+
}
|
|
637
|
+
case 'global-only': {
|
|
638
|
+
label = 'global';
|
|
639
|
+
queryGlobalOnlyHint = resolution.hint;
|
|
640
|
+
break;
|
|
641
|
+
}
|
|
578
642
|
}
|
|
579
643
|
}
|
|
580
644
|
// For wildcard queries on non-global topics, also include global store entries
|
|
@@ -596,22 +660,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
596
660
|
})
|
|
597
661
|
.sort((a, b) => b.relevanceScore - a.relevanceScore);
|
|
598
662
|
// Build stores collection for tag frequency aggregation
|
|
663
|
+
// Only include stores that were actually searched
|
|
599
664
|
const searchedStores = [];
|
|
600
665
|
if (isGlobalQuery) {
|
|
601
666
|
searchedStores.push(globalStore);
|
|
602
667
|
}
|
|
603
|
-
else if (rawLobe) {
|
|
604
|
-
const store = configManager.getStore(rawLobe);
|
|
605
|
-
if (store)
|
|
606
|
-
searchedStores.push(store);
|
|
607
|
-
}
|
|
608
668
|
else {
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
const store = configManager.getStore(lobeName);
|
|
612
|
-
if (store)
|
|
613
|
-
searchedStores.push(store);
|
|
614
|
-
}
|
|
669
|
+
if (primaryStore)
|
|
670
|
+
searchedStores.push(primaryStore);
|
|
615
671
|
if (scope === '*')
|
|
616
672
|
searchedStores.push(globalStore);
|
|
617
673
|
}
|
|
@@ -620,23 +676,25 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
620
676
|
const filterGroups = filter ? parseFilter(filter) : [];
|
|
621
677
|
if (allEntries.length === 0) {
|
|
622
678
|
const footer = buildQueryFooter({ filterGroups, rawFilter: filter, tagFreq, resultCount: 0, scope });
|
|
679
|
+
const noResultHint = queryGlobalOnlyHint ? `\n\n> ${queryGlobalOnlyHint}` : '';
|
|
623
680
|
return {
|
|
624
681
|
content: [{
|
|
625
682
|
type: 'text',
|
|
626
|
-
text: `[${label}] No entries found for scope "${scope}"${filter ? ` with filter "${filter}"` : ''}
|
|
683
|
+
text: `[${label}] No entries found for scope "${scope}"${filter ? ` with filter "${filter}"` : ''}.${noResultHint}\n\n---\n${footer}`,
|
|
627
684
|
}],
|
|
628
685
|
};
|
|
629
686
|
}
|
|
687
|
+
const showQueryLobeLabels = entryLobeMap.size > 0;
|
|
630
688
|
const lines = allEntries.map(e => {
|
|
631
689
|
const freshIndicator = e.fresh ? '' : ' [stale]';
|
|
632
|
-
const lobeTag =
|
|
690
|
+
const lobeTag = showQueryLobeLabels ? ` [${entryLobeMap.get(e.id) ?? '?'}]` : '';
|
|
633
691
|
if (detail === 'brief') {
|
|
634
692
|
return `- **${e.title}** (${e.id}${lobeTag}, confidence: ${e.confidence})${freshIndicator}\n ${e.summary}`;
|
|
635
693
|
}
|
|
636
694
|
if (detail === 'full') {
|
|
637
695
|
const meta = [
|
|
638
696
|
`ID: ${e.id}`,
|
|
639
|
-
|
|
697
|
+
showQueryLobeLabels ? `Lobe: ${entryLobeMap.get(e.id) ?? '?'}` : null,
|
|
640
698
|
`Confidence: ${e.confidence}`,
|
|
641
699
|
`Trust: ${e.trust}`,
|
|
642
700
|
`Fresh: ${e.fresh}`,
|
|
@@ -670,6 +728,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
670
728
|
text += '\n\n' + formatConflictWarning(conflicts);
|
|
671
729
|
}
|
|
672
730
|
}
|
|
731
|
+
// Surface hint when we fell back to global-only
|
|
732
|
+
if (queryGlobalOnlyHint) {
|
|
733
|
+
text += `\n\n> ${queryGlobalOnlyHint}`;
|
|
734
|
+
}
|
|
673
735
|
// Build footer with query mode, tag vocabulary, and syntax reference
|
|
674
736
|
const footer = buildQueryFooter({ filterGroups, rawFilter: filter, tagFreq, resultCount: allEntries.length, scope });
|
|
675
737
|
text += `\n\n---\n${footer}`;
|
|
@@ -822,7 +884,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
822
884
|
const ctxEntryLobeMap = new Map(); // entry id → lobe name
|
|
823
885
|
let label;
|
|
824
886
|
let primaryStore;
|
|
825
|
-
let
|
|
887
|
+
let ctxGlobalOnlyHint;
|
|
826
888
|
if (rawLobe) {
|
|
827
889
|
const ctx = resolveToolContext(rawLobe);
|
|
828
890
|
if (!ctx.ok)
|
|
@@ -833,34 +895,29 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
833
895
|
allLobeResults.push(...lobeResults);
|
|
834
896
|
}
|
|
835
897
|
else {
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
// Minimum keyword matches required to avoid the penalty (at least 40% of context, min 2)
|
|
860
|
-
const minMatchCount = Math.max(2, Math.ceil(contextKwCount * CROSS_LOBE_MIN_MATCH_RATIO));
|
|
861
|
-
for (let i = 0; i < allLobeResults.length; i++) {
|
|
862
|
-
if (allLobeResults[i].matchedKeywords.length < minMatchCount) {
|
|
863
|
-
allLobeResults[i] = { ...allLobeResults[i], score: allLobeResults[i].score * CROSS_LOBE_WEAK_SCORE_PENALTY };
|
|
898
|
+
const resolution = await resolveLobesForRead();
|
|
899
|
+
switch (resolution.kind) {
|
|
900
|
+
case 'resolved': {
|
|
901
|
+
label = resolution.label;
|
|
902
|
+
for (const lobeName of resolution.lobes) {
|
|
903
|
+
const store = configManager.getStore(lobeName);
|
|
904
|
+
if (!store)
|
|
905
|
+
continue;
|
|
906
|
+
if (!primaryStore)
|
|
907
|
+
primaryStore = store;
|
|
908
|
+
const lobeResults = await store.contextSearch(context, max, undefined, threshold);
|
|
909
|
+
if (resolution.lobes.length > 1) {
|
|
910
|
+
for (const r of lobeResults)
|
|
911
|
+
ctxEntryLobeMap.set(r.entry.id, lobeName);
|
|
912
|
+
}
|
|
913
|
+
allLobeResults.push(...lobeResults);
|
|
914
|
+
}
|
|
915
|
+
break;
|
|
916
|
+
}
|
|
917
|
+
case 'global-only': {
|
|
918
|
+
label = 'global';
|
|
919
|
+
ctxGlobalOnlyHint = resolution.hint;
|
|
920
|
+
break;
|
|
864
921
|
}
|
|
865
922
|
}
|
|
866
923
|
}
|
|
@@ -880,28 +937,23 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
880
937
|
})
|
|
881
938
|
.slice(0, max);
|
|
882
939
|
// Build stores collection for tag frequency aggregation
|
|
940
|
+
// Only include stores that were actually searched (not all lobes)
|
|
883
941
|
const ctxSearchedStores = [globalStore];
|
|
884
|
-
if (
|
|
885
|
-
|
|
886
|
-
if (store)
|
|
887
|
-
ctxSearchedStores.push(store);
|
|
888
|
-
}
|
|
889
|
-
else {
|
|
890
|
-
for (const lobeName of configManager.getLobeNames()) {
|
|
891
|
-
const store = configManager.getStore(lobeName);
|
|
892
|
-
if (store)
|
|
893
|
-
ctxSearchedStores.push(store);
|
|
894
|
-
}
|
|
942
|
+
if (primaryStore && primaryStore !== globalStore) {
|
|
943
|
+
ctxSearchedStores.push(primaryStore);
|
|
895
944
|
}
|
|
896
945
|
const ctxTagFreq = mergeTagFrequencies(ctxSearchedStores);
|
|
897
946
|
// Parse filter for footer (context search has no filter, pass empty)
|
|
898
947
|
const ctxFilterGroups = [];
|
|
899
948
|
if (results.length === 0) {
|
|
900
949
|
const ctxFooter = buildQueryFooter({ filterGroups: ctxFilterGroups, rawFilter: undefined, tagFreq: ctxTagFreq, resultCount: 0, scope: 'context search' });
|
|
950
|
+
const noResultHint = ctxGlobalOnlyHint
|
|
951
|
+
? `\n\n> ${ctxGlobalOnlyHint}`
|
|
952
|
+
: '\n\nThis is fine — proceed without prior context. As you learn things worth remembering, store them with memory_store.';
|
|
901
953
|
return {
|
|
902
954
|
content: [{
|
|
903
955
|
type: 'text',
|
|
904
|
-
text: `[${label}] No relevant knowledge found for: "${context}"\n\
|
|
956
|
+
text: `[${label}] No relevant knowledge found for: "${context}"${noResultHint}\n\n---\n${ctxFooter}`,
|
|
905
957
|
}],
|
|
906
958
|
};
|
|
907
959
|
}
|
|
@@ -919,6 +971,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
919
971
|
...topicOrder.filter(t => byTopic.has(t)),
|
|
920
972
|
...Array.from(byTopic.keys()).filter(t => !topicOrder.includes(t)).sort(),
|
|
921
973
|
];
|
|
974
|
+
const showCtxLobeLabels = ctxEntryLobeMap.size > 0;
|
|
922
975
|
for (const topic of orderedTopics) {
|
|
923
976
|
const topicResults = byTopic.get(topic);
|
|
924
977
|
const heading = topic === 'user' ? 'About You'
|
|
@@ -930,7 +983,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
930
983
|
for (const r of topicResults) {
|
|
931
984
|
const marker = topic === 'gotchas' ? '[!] ' : topic === 'preferences' ? '[pref] ' : '';
|
|
932
985
|
const keywords = r.matchedKeywords.length > 0 ? ` (matched: ${r.matchedKeywords.join(', ')})` : '';
|
|
933
|
-
const lobeLabel =
|
|
986
|
+
const lobeLabel = showCtxLobeLabels ? ` [${ctxEntryLobeMap.get(r.entry.id) ?? '?'}]` : '';
|
|
934
987
|
const tagsSuffix = r.entry.tags?.length ? ` [tags: ${r.entry.tags.join(', ')}]` : '';
|
|
935
988
|
sections.push(`- **${marker}${r.entry.title}**${lobeLabel}: ${r.entry.content}${keywords}${tagsSuffix}`);
|
|
936
989
|
}
|
|
@@ -957,6 +1010,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
957
1010
|
sections.push(`---\n*Context loaded for: ${kwList} (${topicList}). ` +
|
|
958
1011
|
`This knowledge is now in your conversation — no need to call memory_context again for these terms this session.*`);
|
|
959
1012
|
}
|
|
1013
|
+
// Surface hint when we fell back to global-only
|
|
1014
|
+
if (ctxGlobalOnlyHint) {
|
|
1015
|
+
sections.push(`> ${ctxGlobalOnlyHint}`);
|
|
1016
|
+
}
|
|
960
1017
|
// Build footer (context search has no filter — it's natural language keyword matching)
|
|
961
1018
|
const ctxFooter = buildQueryFooter({ filterGroups: ctxFilterGroups, rawFilter: undefined, tagFreq: ctxTagFreq, resultCount: results.length, scope: 'context search' });
|
|
962
1019
|
sections.push(`---\n${ctxFooter}`);
|
|
@@ -0,0 +1,30 @@
|
|
|
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
|
+
export declare function buildLobeResolution(allLobeNames: readonly string[], matchedLobes: readonly string[]): LobeResolution;
|
|
@@ -0,0 +1,67 @@
|
|
|
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
|
+
export function buildLobeResolution(allLobeNames, matchedLobes) {
|
|
49
|
+
// Single lobe — always resolved, regardless of root matching
|
|
50
|
+
if (allLobeNames.length === 1) {
|
|
51
|
+
return { kind: 'resolved', lobes: allLobeNames, label: allLobeNames[0] };
|
|
52
|
+
}
|
|
53
|
+
// Multiple lobes with successful root match
|
|
54
|
+
if (matchedLobes.length > 0) {
|
|
55
|
+
return {
|
|
56
|
+
kind: 'resolved',
|
|
57
|
+
lobes: matchedLobes,
|
|
58
|
+
label: matchedLobes.length === 1 ? matchedLobes[0] : matchedLobes.join('+'),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
// Fallback — no lobes could be determined
|
|
62
|
+
return {
|
|
63
|
+
kind: 'global-only',
|
|
64
|
+
hint: `Multiple lobes available (${allLobeNames.join(', ')}) but none could be inferred from client workspace roots. ` +
|
|
65
|
+
`Specify lobe parameter for lobe-specific results.`,
|
|
66
|
+
};
|
|
67
|
+
}
|
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
|
};
|
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
|
} | {
|