@danielmarbach/mnemonic-mcp 0.16.0 → 0.17.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 +8 -1
- package/README.md +1 -0
- package/build/cache.d.ts +66 -0
- package/build/cache.d.ts.map +1 -0
- package/build/cache.js +149 -0
- package/build/cache.js.map +1 -0
- package/build/index.js +72 -10
- package/build/index.js.map +1 -1
- package/build/structured-content.d.ts +12 -12
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,7 +4,14 @@ 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.
|
|
7
|
+
## [0.17.0] - 2026-03-25
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Active session project cache (`src/cache.ts`): notes and embeddings are cached in memory after the first access within an MCP session, so repeated calls to `recall`, `get`, and `project_memory_summary` skip redundant storage reads.
|
|
12
|
+
- The cache is invalidated automatically on every write-path tool (`remember`, `update`, `forget`, `relate`, `unrelate`, `move_memory`, `consolidate`, `sync`) and on branch switch.
|
|
13
|
+
|
|
14
|
+
## [0.16.0] - 2026-03-24
|
|
8
15
|
|
|
9
16
|
### Added
|
|
10
17
|
|
package/README.md
CHANGED
|
@@ -23,6 +23,7 @@ mnemonic is at the inception stage. The storage format (frontmatter schema, vaul
|
|
|
23
23
|
|
|
24
24
|
- Hundreds to low thousands of notes: excellent fit.
|
|
25
25
|
- Several thousand: often fine, depending on note size, machine speed, and embedding throughput.
|
|
26
|
+
- Within a session, notes and embeddings are cached after first access — repeated `recall`, `get`, and `project_memory_summary` calls skip storage reads regardless of vault size.
|
|
26
27
|
- Very large collections: expect pain points around reindex time, recall latency, and git churn.
|
|
27
28
|
- Many concurrent writers or massive scale: consider a dedicated database and indexing layer instead.
|
|
28
29
|
|
package/build/cache.d.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { Note, EmbeddingRecord } from "./storage.js";
|
|
2
|
+
import type { NoteProjection } from "./structured-content.js";
|
|
3
|
+
import type { Vault } from "./vault.js";
|
|
4
|
+
interface VaultCache {
|
|
5
|
+
notesById: Map<string, Note>;
|
|
6
|
+
noteList: Note[];
|
|
7
|
+
embeddings: EmbeddingRecord[];
|
|
8
|
+
}
|
|
9
|
+
export interface SessionProjectCache {
|
|
10
|
+
projectId: string;
|
|
11
|
+
/** Per-vault caches keyed by vaultPath. Built lazily per vault on first access. */
|
|
12
|
+
vaultCaches: Map<string, VaultCache>;
|
|
13
|
+
/** Projection cache shared across all cached vaults for this project. */
|
|
14
|
+
projectionsById: Map<string, NoteProjection>;
|
|
15
|
+
/** ISO timestamp of when this cache entry was first created. */
|
|
16
|
+
lastBuiltAt: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Discard the entire active project cache.
|
|
20
|
+
*
|
|
21
|
+
* Call this after any mutation that modifies notes, embeddings, or relationships
|
|
22
|
+
* so the next read rebuilds from storage. Safe to call when no cache exists (no-op).
|
|
23
|
+
*/
|
|
24
|
+
export declare function invalidateActiveProjectCache(): void;
|
|
25
|
+
/**
|
|
26
|
+
* Return the active project cache for the given projectId without triggering a build.
|
|
27
|
+
* Returns undefined when no cache exists or the cached project is different.
|
|
28
|
+
*/
|
|
29
|
+
export declare function getActiveProjectCache(projectId: string): SessionProjectCache | undefined;
|
|
30
|
+
/**
|
|
31
|
+
* Get the full note list for a vault from the session cache, building it lazily if needed.
|
|
32
|
+
*
|
|
33
|
+
* When the vault cache is first built, notes AND embeddings are loaded together so
|
|
34
|
+
* both are available for subsequent calls at no extra I/O cost.
|
|
35
|
+
*
|
|
36
|
+
* Fail-soft: returns `undefined` on error. Callers must fall back to direct storage access.
|
|
37
|
+
*/
|
|
38
|
+
export declare function getOrBuildVaultNoteList(projectId: string, vault: Vault): Promise<Note[] | undefined>;
|
|
39
|
+
/**
|
|
40
|
+
* Get the embeddings list for a vault from the session cache, building it lazily if needed.
|
|
41
|
+
*
|
|
42
|
+
* When the vault cache is first built, notes AND embeddings are loaded together so
|
|
43
|
+
* both are available for subsequent calls at no extra I/O cost.
|
|
44
|
+
*
|
|
45
|
+
* Fail-soft: returns `undefined` on error. Callers must fall back to direct storage access.
|
|
46
|
+
*/
|
|
47
|
+
export declare function getOrBuildVaultEmbeddings(projectId: string, vault: Vault): Promise<EmbeddingRecord[] | undefined>;
|
|
48
|
+
/**
|
|
49
|
+
* Look up a single note from an already-built vault cache.
|
|
50
|
+
* Returns `undefined` when the vault cache has not been built yet or the note is not found.
|
|
51
|
+
* Does NOT trigger a cache build — callers should use `getOrBuildVaultNoteList` or
|
|
52
|
+
* `getOrBuildVaultEmbeddings` to ensure the vault cache is warm first.
|
|
53
|
+
*/
|
|
54
|
+
export declare function getSessionCachedNote(projectId: string, vaultPath: string, noteId: string): Note | undefined;
|
|
55
|
+
/**
|
|
56
|
+
* Retrieve a cached projection for a note.
|
|
57
|
+
* Returns `undefined` when no cache or projection exists.
|
|
58
|
+
*/
|
|
59
|
+
export declare function getSessionCachedProjection(projectId: string, noteId: string): NoteProjection | undefined;
|
|
60
|
+
/**
|
|
61
|
+
* Store a projection in the session cache.
|
|
62
|
+
* No-op when no active cache exists for this project.
|
|
63
|
+
*/
|
|
64
|
+
export declare function setSessionCachedProjection(projectId: string, noteId: string, projection: NoteProjection): void;
|
|
65
|
+
export {};
|
|
66
|
+
//# sourceMappingURL=cache.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,IAAI,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAC1D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AAC9D,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAIxC,UAAU,UAAU;IAClB,SAAS,EAAE,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;IAC7B,QAAQ,EAAE,IAAI,EAAE,CAAC;IACjB,UAAU,EAAE,eAAe,EAAE,CAAC;CAC/B;AAED,MAAM,WAAW,mBAAmB;IAClC,SAAS,EAAE,MAAM,CAAC;IAClB,mFAAmF;IACnF,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IACrC,yEAAyE;IACzE,eAAe,EAAE,GAAG,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC;IAC7C,gEAAgE;IAChE,WAAW,EAAE,MAAM,CAAC;CACrB;AAqCD;;;;;GAKG;AACH,wBAAgB,4BAA4B,IAAI,IAAI,CAKnD;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,MAAM,GAAG,mBAAmB,GAAG,SAAS,CAIxF;AAED;;;;;;;GAOG;AACH,wBAAsB,uBAAuB,CAC3C,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,KAAK,GACX,OAAO,CAAC,IAAI,EAAE,GAAG,SAAS,CAAC,CA6B7B;AAED;;;;;;;GAOG;AACH,wBAAsB,yBAAyB,CAC7C,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,KAAK,GACX,OAAO,CAAC,eAAe,EAAE,GAAG,SAAS,CAAC,CA6BxC;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAClC,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,GACb,IAAI,GAAG,SAAS,CAIlB;AAED;;;GAGG;AACH,wBAAgB,0BAA0B,CACxC,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,GACb,cAAc,GAAG,SAAS,CAI5B;AAED;;;GAGG;AACH,wBAAgB,0BAA0B,CACxC,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,cAAc,GACzB,IAAI,CAIN"}
|
package/build/cache.js
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { performance } from "perf_hooks";
|
|
2
|
+
// ── Module-level singleton ─────────────────────────────────────────────────────
|
|
3
|
+
const sessionCaches = {};
|
|
4
|
+
// ── Internal helpers ───────────────────────────────────────────────────────────
|
|
5
|
+
function debugLog(event, message) {
|
|
6
|
+
console.error(`[${event}] ${message}`);
|
|
7
|
+
}
|
|
8
|
+
function ensureActiveProjectCache(projectId) {
|
|
9
|
+
const current = sessionCaches.activeProject;
|
|
10
|
+
if (current?.projectId === projectId) {
|
|
11
|
+
return current;
|
|
12
|
+
}
|
|
13
|
+
// Different project (or first use): create fresh cache
|
|
14
|
+
if (current) {
|
|
15
|
+
debugLog("cache:invalidate", `switching project from=${current.projectId} to=${projectId}`);
|
|
16
|
+
}
|
|
17
|
+
const fresh = {
|
|
18
|
+
projectId,
|
|
19
|
+
vaultCaches: new Map(),
|
|
20
|
+
projectionsById: new Map(),
|
|
21
|
+
lastBuiltAt: new Date().toISOString(),
|
|
22
|
+
};
|
|
23
|
+
sessionCaches.activeProject = fresh;
|
|
24
|
+
return fresh;
|
|
25
|
+
}
|
|
26
|
+
// ── Public API ─────────────────────────────────────────────────────────────────
|
|
27
|
+
/**
|
|
28
|
+
* Discard the entire active project cache.
|
|
29
|
+
*
|
|
30
|
+
* Call this after any mutation that modifies notes, embeddings, or relationships
|
|
31
|
+
* so the next read rebuilds from storage. Safe to call when no cache exists (no-op).
|
|
32
|
+
*/
|
|
33
|
+
export function invalidateActiveProjectCache() {
|
|
34
|
+
if (sessionCaches.activeProject) {
|
|
35
|
+
debugLog("cache:invalidate", `project=${sessionCaches.activeProject.projectId}`);
|
|
36
|
+
sessionCaches.activeProject = undefined;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Return the active project cache for the given projectId without triggering a build.
|
|
41
|
+
* Returns undefined when no cache exists or the cached project is different.
|
|
42
|
+
*/
|
|
43
|
+
export function getActiveProjectCache(projectId) {
|
|
44
|
+
const cache = sessionCaches.activeProject;
|
|
45
|
+
if (cache?.projectId === projectId)
|
|
46
|
+
return cache;
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Get the full note list for a vault from the session cache, building it lazily if needed.
|
|
51
|
+
*
|
|
52
|
+
* When the vault cache is first built, notes AND embeddings are loaded together so
|
|
53
|
+
* both are available for subsequent calls at no extra I/O cost.
|
|
54
|
+
*
|
|
55
|
+
* Fail-soft: returns `undefined` on error. Callers must fall back to direct storage access.
|
|
56
|
+
*/
|
|
57
|
+
export async function getOrBuildVaultNoteList(projectId, vault) {
|
|
58
|
+
const vaultPath = vault.storage.vaultPath;
|
|
59
|
+
const cache = ensureActiveProjectCache(projectId);
|
|
60
|
+
const existing = cache.vaultCaches.get(vaultPath);
|
|
61
|
+
if (existing) {
|
|
62
|
+
debugLog("cache:hit", `project=${projectId} vault=${vaultPath} notes=${existing.noteList.length}`);
|
|
63
|
+
return existing.noteList;
|
|
64
|
+
}
|
|
65
|
+
debugLog("cache:miss", `project=${projectId} vault=${vaultPath}`);
|
|
66
|
+
try {
|
|
67
|
+
const t0 = performance.now();
|
|
68
|
+
const [noteList, embeddings] = await Promise.all([
|
|
69
|
+
vault.storage.listNotes(),
|
|
70
|
+
vault.storage.listEmbeddings(),
|
|
71
|
+
]);
|
|
72
|
+
const notesById = new Map(noteList.map((n) => [n.id, n]));
|
|
73
|
+
cache.vaultCaches.set(vaultPath, { notesById, noteList, embeddings });
|
|
74
|
+
const ms = (performance.now() - t0).toFixed(1);
|
|
75
|
+
debugLog("cache:build", `project=${projectId} vault=${vaultPath} notes=${noteList.length} embeddings=${embeddings.length} time=${ms}ms`);
|
|
76
|
+
return noteList;
|
|
77
|
+
}
|
|
78
|
+
catch (err) {
|
|
79
|
+
debugLog("cache:fallback", `project=${projectId} vault=${vaultPath} error=${String(err)}`);
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Get the embeddings list for a vault from the session cache, building it lazily if needed.
|
|
85
|
+
*
|
|
86
|
+
* When the vault cache is first built, notes AND embeddings are loaded together so
|
|
87
|
+
* both are available for subsequent calls at no extra I/O cost.
|
|
88
|
+
*
|
|
89
|
+
* Fail-soft: returns `undefined` on error. Callers must fall back to direct storage access.
|
|
90
|
+
*/
|
|
91
|
+
export async function getOrBuildVaultEmbeddings(projectId, vault) {
|
|
92
|
+
const vaultPath = vault.storage.vaultPath;
|
|
93
|
+
const cache = ensureActiveProjectCache(projectId);
|
|
94
|
+
const existing = cache.vaultCaches.get(vaultPath);
|
|
95
|
+
if (existing) {
|
|
96
|
+
debugLog("cache:hit", `project=${projectId} vault=${vaultPath} embeddings=${existing.embeddings.length}`);
|
|
97
|
+
return existing.embeddings;
|
|
98
|
+
}
|
|
99
|
+
debugLog("cache:miss", `project=${projectId} vault=${vaultPath}`);
|
|
100
|
+
try {
|
|
101
|
+
const t0 = performance.now();
|
|
102
|
+
const [noteList, embeddings] = await Promise.all([
|
|
103
|
+
vault.storage.listNotes(),
|
|
104
|
+
vault.storage.listEmbeddings(),
|
|
105
|
+
]);
|
|
106
|
+
const notesById = new Map(noteList.map((n) => [n.id, n]));
|
|
107
|
+
cache.vaultCaches.set(vaultPath, { notesById, noteList, embeddings });
|
|
108
|
+
const ms = (performance.now() - t0).toFixed(1);
|
|
109
|
+
debugLog("cache:build", `project=${projectId} vault=${vaultPath} notes=${noteList.length} embeddings=${embeddings.length} time=${ms}ms`);
|
|
110
|
+
return embeddings;
|
|
111
|
+
}
|
|
112
|
+
catch (err) {
|
|
113
|
+
debugLog("cache:fallback", `project=${projectId} vault=${vaultPath} error=${String(err)}`);
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Look up a single note from an already-built vault cache.
|
|
119
|
+
* Returns `undefined` when the vault cache has not been built yet or the note is not found.
|
|
120
|
+
* Does NOT trigger a cache build — callers should use `getOrBuildVaultNoteList` or
|
|
121
|
+
* `getOrBuildVaultEmbeddings` to ensure the vault cache is warm first.
|
|
122
|
+
*/
|
|
123
|
+
export function getSessionCachedNote(projectId, vaultPath, noteId) {
|
|
124
|
+
const cache = sessionCaches.activeProject;
|
|
125
|
+
if (!cache || cache.projectId !== projectId)
|
|
126
|
+
return undefined;
|
|
127
|
+
return cache.vaultCaches.get(vaultPath)?.notesById.get(noteId);
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Retrieve a cached projection for a note.
|
|
131
|
+
* Returns `undefined` when no cache or projection exists.
|
|
132
|
+
*/
|
|
133
|
+
export function getSessionCachedProjection(projectId, noteId) {
|
|
134
|
+
const cache = sessionCaches.activeProject;
|
|
135
|
+
if (!cache || cache.projectId !== projectId)
|
|
136
|
+
return undefined;
|
|
137
|
+
return cache.projectionsById.get(noteId);
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Store a projection in the session cache.
|
|
141
|
+
* No-op when no active cache exists for this project.
|
|
142
|
+
*/
|
|
143
|
+
export function setSessionCachedProjection(projectId, noteId, projection) {
|
|
144
|
+
const cache = sessionCaches.activeProject;
|
|
145
|
+
if (!cache || cache.projectId !== projectId)
|
|
146
|
+
return;
|
|
147
|
+
cache.projectionsById.set(noteId, projection);
|
|
148
|
+
}
|
|
149
|
+
//# sourceMappingURL=cache.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache.js","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"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"}
|
package/build/index.js
CHANGED
|
@@ -9,6 +9,8 @@ import { NOTE_LIFECYCLES } from "./storage.js";
|
|
|
9
9
|
import { embed, cosineSimilarity, embedModel } from "./embeddings.js";
|
|
10
10
|
import { buildTemporalHistoryEntry, computeConfidence, getNoteProvenance } from "./provenance.js";
|
|
11
11
|
import { getOrBuildProjection } from "./projections.js";
|
|
12
|
+
import { invalidateActiveProjectCache, getOrBuildVaultEmbeddings, getOrBuildVaultNoteList, getSessionCachedNote, } from "./cache.js";
|
|
13
|
+
import { performance } from "perf_hooks";
|
|
12
14
|
import { filterRelationships, mergeRelationshipsFromNotes, normalizeMergePlanSourceIds, resolveEffectiveConsolidationMode, } from "./consolidate.js";
|
|
13
15
|
import { selectRecallResults } from "./recall.js";
|
|
14
16
|
import { getRelationshipPreview } from "./relationships.js";
|
|
@@ -360,6 +362,8 @@ async function ensureBranchSynced(cwd) {
|
|
|
360
362
|
}
|
|
361
363
|
const mainBackfill = await backfillEmbeddingsAfterSync(vaultManager.main.storage, "main vault", [], true);
|
|
362
364
|
console.error(`[branch] Main vault embedded ${mainBackfill.embedded} notes`);
|
|
365
|
+
// Vault contents changed — discard session cache so next access rebuilds from fresh state
|
|
366
|
+
invalidateActiveProjectCache();
|
|
363
367
|
return true;
|
|
364
368
|
}
|
|
365
369
|
function formatProjectIdentityText(identity) {
|
|
@@ -782,7 +786,7 @@ function vaultMatchesStorageScope(vault, storedIn) {
|
|
|
782
786
|
// "project-vault" covers the primary project vault and all submodule vaults.
|
|
783
787
|
return vault.isProject;
|
|
784
788
|
}
|
|
785
|
-
async function collectVisibleNotes(cwd, scope = "all", tags, storedIn = "any") {
|
|
789
|
+
async function collectVisibleNotes(cwd, scope = "all", tags, storedIn = "any", sessionProjectId) {
|
|
786
790
|
const project = await resolveProject(cwd);
|
|
787
791
|
const vaults = await vaultManager.searchOrder(cwd);
|
|
788
792
|
let filterProject = undefined;
|
|
@@ -793,8 +797,23 @@ async function collectVisibleNotes(cwd, scope = "all", tags, storedIn = "any") {
|
|
|
793
797
|
const seen = new Set();
|
|
794
798
|
const entries = [];
|
|
795
799
|
for (const vault of vaults) {
|
|
796
|
-
|
|
797
|
-
|
|
800
|
+
let rawNotes;
|
|
801
|
+
if (sessionProjectId) {
|
|
802
|
+
const cached = await getOrBuildVaultNoteList(sessionProjectId, vault);
|
|
803
|
+
if (cached !== undefined) {
|
|
804
|
+
// Apply project filter on the full cached list
|
|
805
|
+
rawNotes = filterProject !== undefined
|
|
806
|
+
? cached.filter((n) => filterProject === null ? !n.project : n.project === filterProject)
|
|
807
|
+
: cached;
|
|
808
|
+
}
|
|
809
|
+
else {
|
|
810
|
+
rawNotes = await vault.storage.listNotes(filterProject !== undefined ? { project: filterProject } : undefined);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
else {
|
|
814
|
+
rawNotes = await vault.storage.listNotes(filterProject !== undefined ? { project: filterProject } : undefined);
|
|
815
|
+
}
|
|
816
|
+
for (const note of rawNotes) {
|
|
798
817
|
if (seen.has(note.id)) {
|
|
799
818
|
continue;
|
|
800
819
|
}
|
|
@@ -1442,6 +1461,7 @@ server.registerTool("remember", {
|
|
|
1442
1461
|
timestamp: now,
|
|
1443
1462
|
persistence,
|
|
1444
1463
|
};
|
|
1464
|
+
invalidateActiveProjectCache();
|
|
1445
1465
|
return {
|
|
1446
1466
|
content: [{ type: "text", text: textContent }],
|
|
1447
1467
|
structuredContent,
|
|
@@ -1674,6 +1694,7 @@ server.registerTool("recall", {
|
|
|
1674
1694
|
}),
|
|
1675
1695
|
outputSchema: RecallResultSchema,
|
|
1676
1696
|
}, async ({ query, cwd, limit, minSimilarity, mode, verbose, tags, scope }) => {
|
|
1697
|
+
const t0Recall = performance.now();
|
|
1677
1698
|
await ensureBranchSynced(cwd);
|
|
1678
1699
|
const project = await resolveProject(cwd);
|
|
1679
1700
|
const queryVec = await embed(query);
|
|
@@ -1681,6 +1702,12 @@ server.registerTool("recall", {
|
|
|
1681
1702
|
const noteCache = new Map();
|
|
1682
1703
|
const noteCacheKey = (vault, id) => `${vault.storage.vaultPath}::${id}`;
|
|
1683
1704
|
const readCachedNote = async (vault, id) => {
|
|
1705
|
+
// Check session cache first (populated when getOrBuildVaultEmbeddings was called)
|
|
1706
|
+
if (project) {
|
|
1707
|
+
const sessionNote = getSessionCachedNote(project.id, vault.storage.vaultPath, id);
|
|
1708
|
+
if (sessionNote !== undefined)
|
|
1709
|
+
return sessionNote;
|
|
1710
|
+
}
|
|
1684
1711
|
const key = noteCacheKey(vault, id);
|
|
1685
1712
|
const cached = noteCache.get(key);
|
|
1686
1713
|
if (cached) {
|
|
@@ -1697,7 +1724,9 @@ server.registerTool("recall", {
|
|
|
1697
1724
|
}
|
|
1698
1725
|
const scored = [];
|
|
1699
1726
|
for (const vault of vaults) {
|
|
1700
|
-
const embeddings =
|
|
1727
|
+
const embeddings = project
|
|
1728
|
+
? (await getOrBuildVaultEmbeddings(project.id, vault)) ?? await vault.storage.listEmbeddings()
|
|
1729
|
+
: await vault.storage.listEmbeddings();
|
|
1701
1730
|
for (const rec of embeddings) {
|
|
1702
1731
|
const rawScore = cosineSimilarity(queryVec, rec.embedding);
|
|
1703
1732
|
if (rawScore < minSimilarity)
|
|
@@ -1792,6 +1821,7 @@ server.registerTool("recall", {
|
|
|
1792
1821
|
scope: scope || "all",
|
|
1793
1822
|
results: structuredResults,
|
|
1794
1823
|
};
|
|
1824
|
+
console.error(`[recall:timing] ${(performance.now() - t0Recall).toFixed(1)}ms`);
|
|
1795
1825
|
return {
|
|
1796
1826
|
content: [{ type: "text", text: textContent }],
|
|
1797
1827
|
structuredContent,
|
|
@@ -1942,6 +1972,7 @@ server.registerTool("update", {
|
|
|
1942
1972
|
lifecycle: updated.lifecycle,
|
|
1943
1973
|
persistence,
|
|
1944
1974
|
};
|
|
1975
|
+
invalidateActiveProjectCache();
|
|
1945
1976
|
return { content: [{ type: "text", text: `Updated memory '${id}'\n${formatPersistenceSummary(persistence)}` }], structuredContent };
|
|
1946
1977
|
});
|
|
1947
1978
|
// ── forget ────────────────────────────────────────────────────────────────────
|
|
@@ -2048,6 +2079,7 @@ server.registerTool("forget", {
|
|
|
2048
2079
|
retry,
|
|
2049
2080
|
};
|
|
2050
2081
|
const retrySummary = formatRetrySummary(retry);
|
|
2082
|
+
invalidateActiveProjectCache();
|
|
2051
2083
|
return {
|
|
2052
2084
|
content: [{
|
|
2053
2085
|
type: "text",
|
|
@@ -2085,12 +2117,26 @@ server.registerTool("get", {
|
|
|
2085
2117
|
}),
|
|
2086
2118
|
outputSchema: GetResultSchema,
|
|
2087
2119
|
}, async ({ ids, cwd, includeRelationships }) => {
|
|
2120
|
+
const t0Get = performance.now();
|
|
2088
2121
|
await ensureBranchSynced(cwd);
|
|
2089
2122
|
const project = await resolveProject(cwd);
|
|
2090
2123
|
const found = [];
|
|
2091
2124
|
const notFound = [];
|
|
2092
2125
|
for (const id of ids) {
|
|
2093
|
-
|
|
2126
|
+
// Check session cache before hitting storage
|
|
2127
|
+
let result = null;
|
|
2128
|
+
if (project) {
|
|
2129
|
+
for (const vault of vaultManager.allKnownVaults()) {
|
|
2130
|
+
const cached = getSessionCachedNote(project.id, vault.storage.vaultPath, id);
|
|
2131
|
+
if (cached !== undefined) {
|
|
2132
|
+
result = { note: cached, vault };
|
|
2133
|
+
break;
|
|
2134
|
+
}
|
|
2135
|
+
}
|
|
2136
|
+
}
|
|
2137
|
+
if (!result) {
|
|
2138
|
+
result = await vaultManager.findNote(id, cwd);
|
|
2139
|
+
}
|
|
2094
2140
|
if (!result) {
|
|
2095
2141
|
notFound.push(id);
|
|
2096
2142
|
continue;
|
|
@@ -2138,6 +2184,7 @@ server.registerTool("get", {
|
|
|
2138
2184
|
notes: found,
|
|
2139
2185
|
notFound,
|
|
2140
2186
|
};
|
|
2187
|
+
console.error(`[get:timing] ${(performance.now() - t0Get).toFixed(1)}ms`);
|
|
2141
2188
|
return { content: [{ type: "text", text: lines.join("\n").trim() }], structuredContent };
|
|
2142
2189
|
});
|
|
2143
2190
|
// ── where_is_memory ───────────────────────────────────────────────────────────
|
|
@@ -2690,8 +2737,11 @@ server.registerTool("project_memory_summary", {
|
|
|
2690
2737
|
}),
|
|
2691
2738
|
outputSchema: ProjectSummaryResultSchema,
|
|
2692
2739
|
}, async ({ cwd, maxPerTheme, recentLimit, anchorLimit, includeRelatedGlobal, relatedGlobalLimit }) => {
|
|
2740
|
+
const t0Summary = performance.now();
|
|
2693
2741
|
await ensureBranchSynced(cwd);
|
|
2694
|
-
|
|
2742
|
+
// Pre-resolve project so we can pass its id to collectVisibleNotes for session caching
|
|
2743
|
+
const preProject = await resolveProject(cwd);
|
|
2744
|
+
const { project, entries } = await collectVisibleNotes(cwd, "all", undefined, "any", preProject?.id);
|
|
2695
2745
|
if (!project) {
|
|
2696
2746
|
return { content: [{ type: "text", text: `Could not detect a project for: ${cwd}` }], isError: true };
|
|
2697
2747
|
}
|
|
@@ -3007,6 +3057,7 @@ server.registerTool("project_memory_summary", {
|
|
|
3007
3057
|
orientation,
|
|
3008
3058
|
relatedGlobal,
|
|
3009
3059
|
};
|
|
3060
|
+
console.error(`[summary:timing] ${(performance.now() - t0Summary).toFixed(1)}ms`);
|
|
3010
3061
|
return { content: [{ type: "text", text: sections.join("\n") }], structuredContent };
|
|
3011
3062
|
});
|
|
3012
3063
|
// ── sync ──────────────────────────────────────────────────────────────────────
|
|
@@ -3093,6 +3144,8 @@ server.registerTool("sync", {
|
|
|
3093
3144
|
action: "synced",
|
|
3094
3145
|
vaults: vaultResults,
|
|
3095
3146
|
};
|
|
3147
|
+
// Vault contents may have changed via pull — discard session cache
|
|
3148
|
+
invalidateActiveProjectCache();
|
|
3096
3149
|
return { content: [{ type: "text", text: lines.join("\n") }], structuredContent };
|
|
3097
3150
|
});
|
|
3098
3151
|
// ── move_memory ───────────────────────────────────────────────────────────────
|
|
@@ -3244,6 +3297,7 @@ server.registerTool("move_memory", {
|
|
|
3244
3297
|
const associationText = metadataRewritten
|
|
3245
3298
|
? `Project association is now ${associationValue}.`
|
|
3246
3299
|
: `Project association remains ${associationValue}.`;
|
|
3300
|
+
invalidateActiveProjectCache();
|
|
3247
3301
|
return {
|
|
3248
3302
|
content: [{
|
|
3249
3303
|
type: "text",
|
|
@@ -3416,6 +3470,7 @@ server.registerTool("relate", {
|
|
|
3416
3470
|
retry,
|
|
3417
3471
|
};
|
|
3418
3472
|
const retrySummary = formatRetrySummary(retry);
|
|
3473
|
+
invalidateActiveProjectCache();
|
|
3419
3474
|
return {
|
|
3420
3475
|
content: [{
|
|
3421
3476
|
type: "text",
|
|
@@ -3582,6 +3637,7 @@ server.registerTool("unrelate", {
|
|
|
3582
3637
|
retry,
|
|
3583
3638
|
};
|
|
3584
3639
|
const retrySummary = formatRetrySummary(retry);
|
|
3640
|
+
invalidateActiveProjectCache();
|
|
3585
3641
|
return {
|
|
3586
3642
|
content: [{
|
|
3587
3643
|
type: "text",
|
|
@@ -3680,13 +3736,19 @@ server.registerTool("consolidate", {
|
|
|
3680
3736
|
return findClusters(projectNotes, project);
|
|
3681
3737
|
case "suggest-merges":
|
|
3682
3738
|
return suggestMerges(projectNotes, threshold, defaultConsolidationMode, project, mode);
|
|
3683
|
-
case "execute-merge":
|
|
3739
|
+
case "execute-merge": {
|
|
3684
3740
|
if (!mergePlan) {
|
|
3685
3741
|
return { content: [{ type: "text", text: "execute-merge strategy requires a mergePlan with sourceIds and targetTitle." }], isError: true };
|
|
3686
3742
|
}
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
return
|
|
3743
|
+
const mergeResult = await executeMerge(entries, mergePlan, defaultConsolidationMode, project, cwd, mode, policy, allowProtectedBranch);
|
|
3744
|
+
invalidateActiveProjectCache();
|
|
3745
|
+
return mergeResult;
|
|
3746
|
+
}
|
|
3747
|
+
case "prune-superseded": {
|
|
3748
|
+
const pruneResult = await pruneSuperseded(projectNotes, mode ?? defaultConsolidationMode, project, cwd, policy, allowProtectedBranch);
|
|
3749
|
+
invalidateActiveProjectCache();
|
|
3750
|
+
return pruneResult;
|
|
3751
|
+
}
|
|
3690
3752
|
case "dry-run":
|
|
3691
3753
|
return dryRunAll(projectNotes, threshold, defaultConsolidationMode, project, mode);
|
|
3692
3754
|
default:
|