@danielmarbach/mnemonic-mcp 0.19.4 → 0.20.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/CHANGELOG.md +17 -0
- package/README.md +2 -0
- package/build/index.js +109 -7
- package/build/index.js.map +1 -1
- package/build/lexical.d.ts +53 -0
- package/build/lexical.d.ts.map +1 -0
- package/build/lexical.js +112 -0
- package/build/lexical.js.map +1 -0
- package/build/recall.d.ts +21 -0
- package/build/recall.d.ts.map +1 -1
- package/build/recall.js +34 -1
- package/build/recall.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,23 @@ All notable changes to `mnemonic` will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
The format is loosely based on Keep a Changelog and uses semver-style version headings.
|
|
6
6
|
|
|
7
|
+
## [0.20.0] - 2026-04-04
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Hybrid recall: semantic search is now enhanced with lightweight lexical reranking over note projections. When semantic results are weak, a bounded lexical rescue path scans projections for additional candidates, improving exact-match and partial-query recall without changing the storage model or adding new infrastructure.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- `recall` results now include a `lexicalScore` field in structured output when computed, showing the lexical overlap contribution alongside the semantic score.
|
|
16
|
+
|
|
17
|
+
## [0.19.5] - 2026-04-04
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- `discover_tags` now returns schema-compliant structured output in both suggest and browse modes. Previously, internal ranking fields leaked into tag items, causing MCP callers to fail with `-32602` schema validation errors.
|
|
22
|
+
- `discover_tags` exact context matching now respects tag boundaries, so short tags like `io` no longer get boosted from substring hits inside larger words such as `mnemonic`.
|
|
23
|
+
|
|
7
24
|
## [0.19.4] - 2026-04-02
|
|
8
25
|
|
|
9
26
|
### Changed
|
package/README.md
CHANGED
|
@@ -308,6 +308,8 @@ Project identity derives from the **git remote URL**, normalized to a stable slu
|
|
|
308
308
|
|
|
309
309
|
`recall` with `cwd` searches both vaults. Project notes get a **+0.15 similarity boost** — a soft signal, not a hard filter — so global memories remain accessible while project context floats to the top.
|
|
310
310
|
|
|
311
|
+
**Hybrid recall** enhances semantic search with lightweight lexical reranking over note projections. When semantic results are weak, a bounded lexical rescue path scans projections for additional candidates, improving exact-match and partial-query recall without changing the storage model or adding new infrastructure. Lexical scores act as tiebreakers — they cannot overcome a large semantic gap but can reorder close candidates.
|
|
312
|
+
|
|
311
313
|
Temporal recall is opt-in via `mode: "temporal"`. It keeps semantic selection first, then enriches only the top matches with compact git-backed history so agents can inspect how a note evolved without turning recall into raw log or diff output.
|
|
312
314
|
|
|
313
315
|
**What temporal mode shows:**
|
package/build/index.js
CHANGED
|
@@ -10,10 +10,11 @@ import { embed, cosineSimilarity, embedModel } from "./embeddings.js";
|
|
|
10
10
|
import { buildTemporalHistoryEntry, computeConfidence, getNoteProvenance } from "./provenance.js";
|
|
11
11
|
import { enrichTemporalHistory } from "./temporal-interpretation.js";
|
|
12
12
|
import { getOrBuildProjection } from "./projections.js";
|
|
13
|
-
import { invalidateActiveProjectCache, getOrBuildVaultEmbeddings, getOrBuildVaultNoteList, getSessionCachedNote, } from "./cache.js";
|
|
13
|
+
import { invalidateActiveProjectCache, getOrBuildVaultEmbeddings, getOrBuildVaultNoteList, getSessionCachedNote, getSessionCachedProjection, setSessionCachedProjection, } from "./cache.js";
|
|
14
14
|
import { performance } from "perf_hooks";
|
|
15
15
|
import { filterRelationships, mergeRelationshipsFromNotes, normalizeMergePlanSourceIds, resolveEffectiveConsolidationMode, } from "./consolidate.js";
|
|
16
|
-
import { computeRecallMetadataBoost, selectRecallResults } from "./recall.js";
|
|
16
|
+
import { computeRecallMetadataBoost, computeHybridScore, selectRecallResults, applyLexicalReranking } from "./recall.js";
|
|
17
|
+
import { shouldTriggerLexicalRescue, computeLexicalScore, LEXICAL_RESCUE_CANDIDATE_LIMIT, LEXICAL_RESCUE_THRESHOLD, LEXICAL_RESCUE_RESULT_LIMIT, } from "./lexical.js";
|
|
17
18
|
import { getRelationshipPreview } from "./relationships.js";
|
|
18
19
|
import { cleanMarkdown } from "./markdown.js";
|
|
19
20
|
import { MnemonicConfigStore, readVaultSchemaVersion } from "./config.js";
|
|
@@ -1727,6 +1728,52 @@ server.registerTool("get_project_memory_policy", {
|
|
|
1727
1728
|
structuredContent,
|
|
1728
1729
|
};
|
|
1729
1730
|
});
|
|
1731
|
+
// ── Lexical rescue helper ─────────────────────────────────────────────────────
|
|
1732
|
+
async function collectLexicalRescueCandidates(vaults, query, project, scope, tags, existingIds) {
|
|
1733
|
+
const existingIdSet = new Set(existingIds.map((c) => c.id));
|
|
1734
|
+
const candidates = [];
|
|
1735
|
+
for (const vault of vaults) {
|
|
1736
|
+
const notes = await vault.storage.listNotes().catch(() => []);
|
|
1737
|
+
for (const note of notes) {
|
|
1738
|
+
if (existingIdSet.has(note.id))
|
|
1739
|
+
continue;
|
|
1740
|
+
if (tags && tags.length > 0) {
|
|
1741
|
+
const noteTags = new Set(note.tags);
|
|
1742
|
+
if (!tags.every((t) => noteTags.has(t)))
|
|
1743
|
+
continue;
|
|
1744
|
+
}
|
|
1745
|
+
const isProjectNote = note.project !== undefined;
|
|
1746
|
+
const isCurrentProject = project && note.project === project.id;
|
|
1747
|
+
if (scope === "project" && !isCurrentProject)
|
|
1748
|
+
continue;
|
|
1749
|
+
if (scope === "global" && isProjectNote)
|
|
1750
|
+
continue;
|
|
1751
|
+
const projection = await getOrBuildProjection(vault.storage, note).catch(() => undefined);
|
|
1752
|
+
if (!projection)
|
|
1753
|
+
continue;
|
|
1754
|
+
const lexicalScore = computeLexicalScore(query, projection.projectionText);
|
|
1755
|
+
if (lexicalScore < LEXICAL_RESCUE_THRESHOLD)
|
|
1756
|
+
continue;
|
|
1757
|
+
const metadataBoost = computeRecallMetadataBoost(getEffectiveMetadata(note));
|
|
1758
|
+
const boost = (isCurrentProject ? 0.15 : 0) + metadataBoost;
|
|
1759
|
+
candidates.push({
|
|
1760
|
+
id: note.id,
|
|
1761
|
+
score: 0,
|
|
1762
|
+
boosted: boost,
|
|
1763
|
+
vault,
|
|
1764
|
+
isCurrentProject: Boolean(isCurrentProject),
|
|
1765
|
+
lexicalScore,
|
|
1766
|
+
});
|
|
1767
|
+
if (candidates.length >= LEXICAL_RESCUE_CANDIDATE_LIMIT)
|
|
1768
|
+
break;
|
|
1769
|
+
}
|
|
1770
|
+
if (candidates.length >= LEXICAL_RESCUE_CANDIDATE_LIMIT)
|
|
1771
|
+
break;
|
|
1772
|
+
}
|
|
1773
|
+
return candidates
|
|
1774
|
+
.sort((a, b) => computeHybridScore(b) - computeHybridScore(a))
|
|
1775
|
+
.slice(0, LEXICAL_RESCUE_RESULT_LIMIT);
|
|
1776
|
+
}
|
|
1730
1777
|
// ── recall ────────────────────────────────────────────────────────────────────
|
|
1731
1778
|
server.registerTool("recall", {
|
|
1732
1779
|
title: "Recall",
|
|
@@ -1831,7 +1878,42 @@ server.registerTool("recall", {
|
|
|
1831
1878
|
scored.push({ id: rec.id, score: rawScore, boosted: rawScore + boost, vault, isCurrentProject: Boolean(isCurrentProject) });
|
|
1832
1879
|
}
|
|
1833
1880
|
}
|
|
1834
|
-
const
|
|
1881
|
+
const projectionTexts = new Map();
|
|
1882
|
+
for (const candidate of scored) {
|
|
1883
|
+
const note = await readCachedNote(candidate.vault, candidate.id).catch(() => null);
|
|
1884
|
+
if (!note) {
|
|
1885
|
+
continue;
|
|
1886
|
+
}
|
|
1887
|
+
const projection = await getOrBuildProjection(candidate.vault.storage, note).catch(() => undefined);
|
|
1888
|
+
if (!projection) {
|
|
1889
|
+
continue;
|
|
1890
|
+
}
|
|
1891
|
+
projectionTexts.set(candidate.id, projection.projectionText);
|
|
1892
|
+
if (project) {
|
|
1893
|
+
setSessionCachedProjection(project.id, candidate.id, projection);
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1896
|
+
// Apply lexical reranking over semantic candidates (fail-soft)
|
|
1897
|
+
const getProjectionText = (id) => {
|
|
1898
|
+
const inlineProjection = projectionTexts.get(id);
|
|
1899
|
+
if (inlineProjection) {
|
|
1900
|
+
return inlineProjection;
|
|
1901
|
+
}
|
|
1902
|
+
if (project) {
|
|
1903
|
+
const cached = getSessionCachedProjection(project.id, id);
|
|
1904
|
+
if (cached)
|
|
1905
|
+
return cached.projectionText;
|
|
1906
|
+
}
|
|
1907
|
+
return undefined;
|
|
1908
|
+
};
|
|
1909
|
+
const reranked = applyLexicalReranking(scored, query, getProjectionText);
|
|
1910
|
+
// Lexical rescue: when semantic results are weak, scan projections for additional candidates
|
|
1911
|
+
const topScore = reranked.length > 0 ? reranked[0].score : undefined;
|
|
1912
|
+
if (shouldTriggerLexicalRescue(topScore, reranked.length)) {
|
|
1913
|
+
const rescueCandidates = await collectLexicalRescueCandidates(vaults, query, project ?? undefined, scope, tags, reranked);
|
|
1914
|
+
reranked.push(...rescueCandidates);
|
|
1915
|
+
}
|
|
1916
|
+
const top = selectRecallResults(reranked, limit, scope);
|
|
1835
1917
|
if (top.length === 0) {
|
|
1836
1918
|
const structuredContent = { action: "recalled", query, scope: scope || "all", results: [] };
|
|
1837
1919
|
return { content: [{ type: "text", text: "No memories found matching that query." }], structuredContent };
|
|
@@ -2441,9 +2523,13 @@ function countTokenOverlap(tokens, other) {
|
|
|
2441
2523
|
}
|
|
2442
2524
|
return matches;
|
|
2443
2525
|
}
|
|
2526
|
+
function escapeRegex(value) {
|
|
2527
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2528
|
+
}
|
|
2444
2529
|
function hasExactTagContextMatch(tag, values) {
|
|
2445
2530
|
const normalizedTag = tag.toLowerCase();
|
|
2446
|
-
|
|
2531
|
+
const pattern = new RegExp(`(^|[^a-z0-9_-])${escapeRegex(normalizedTag)}([^a-z0-9_-]|$)`);
|
|
2532
|
+
return values.some(value => value ? pattern.test(value.toLowerCase()) : false);
|
|
2447
2533
|
}
|
|
2448
2534
|
server.registerTool("discover_tags", {
|
|
2449
2535
|
title: "Discover Tags",
|
|
@@ -2572,7 +2658,10 @@ server.registerTool("discover_tags", {
|
|
|
2572
2658
|
(tag.usageCount * 0.1) -
|
|
2573
2659
|
genericPenalty -
|
|
2574
2660
|
(!isTemporaryTarget && tag.isTemporaryOnly ? 2 : 0)
|
|
2575
|
-
: (tag.
|
|
2661
|
+
: (tag.exactContextMatch ? 6 : 0) +
|
|
2662
|
+
(tag.tagTokenOverlap * 3) +
|
|
2663
|
+
(tag.averageContextMatch * 2) +
|
|
2664
|
+
(tag.usageCount * 1.5) +
|
|
2576
2665
|
(tag.isTemporaryOnly ? -3 : 1) -
|
|
2577
2666
|
(!isTemporaryTarget && tag.isTemporaryOnly ? 2 : 0);
|
|
2578
2667
|
return {
|
|
@@ -2637,7 +2726,13 @@ server.registerTool("discover_tags", {
|
|
|
2637
2726
|
project: project ? { id: project.id, name: project.name } : undefined,
|
|
2638
2727
|
mode,
|
|
2639
2728
|
scope: scope || "all",
|
|
2640
|
-
tags: tags.slice(0, effectiveLimit).map(
|
|
2729
|
+
tags: tags.slice(0, effectiveLimit).map(tag => ({
|
|
2730
|
+
tag: tag.tag,
|
|
2731
|
+
usageCount: tag.usageCount,
|
|
2732
|
+
examples: tag.examples,
|
|
2733
|
+
lifecycleTypes: tag.lifecycleTypes,
|
|
2734
|
+
isTemporaryOnly: tag.isTemporaryOnly,
|
|
2735
|
+
})),
|
|
2641
2736
|
totalTags: tags.length,
|
|
2642
2737
|
totalNotes: entries.length,
|
|
2643
2738
|
vaultsSearched,
|
|
@@ -2648,7 +2743,14 @@ server.registerTool("discover_tags", {
|
|
|
2648
2743
|
project: project ? { id: project.id, name: project.name } : undefined,
|
|
2649
2744
|
mode,
|
|
2650
2745
|
scope: scope || "all",
|
|
2651
|
-
recommendedTags: tags.slice(0, effectiveLimit).map(
|
|
2746
|
+
recommendedTags: tags.slice(0, effectiveLimit).map(tag => ({
|
|
2747
|
+
tag: tag.tag,
|
|
2748
|
+
usageCount: tag.usageCount,
|
|
2749
|
+
example: tag.example,
|
|
2750
|
+
reason: tag.reason,
|
|
2751
|
+
lifecycleTypes: tag.lifecycleTypes,
|
|
2752
|
+
isTemporaryOnly: tag.isTemporaryOnly,
|
|
2753
|
+
})),
|
|
2652
2754
|
totalTags: tags.length,
|
|
2653
2755
|
totalNotes: entries.length,
|
|
2654
2756
|
vaultsSearched,
|