@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 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.16.0] - Unreleased
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
 
@@ -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
- const vaultNotes = await vault.storage.listNotes(filterProject !== undefined ? { project: filterProject } : undefined);
797
- for (const note of vaultNotes) {
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 = await vault.storage.listEmbeddings();
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
- const result = await vaultManager.findNote(id, cwd);
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
- const { project, entries } = await collectVisibleNotes(cwd, "all");
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
- return executeMerge(entries, mergePlan, defaultConsolidationMode, project, cwd, mode, policy, allowProtectedBranch);
3688
- case "prune-superseded":
3689
- return pruneSuperseded(projectNotes, mode ?? defaultConsolidationMode, project, cwd, policy, allowProtectedBranch);
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: