@danielmarbach/mnemonic-mcp 0.19.5 → 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 +10 -0
- package/README.md +2 -0
- package/build/index.js +85 -3
- 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,16 @@ 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
|
+
|
|
7
17
|
## [0.19.5] - 2026-04-04
|
|
8
18
|
|
|
9
19
|
### Fixed
|
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 };
|