@danielmarbach/mnemonic-mcp 0.16.0 → 0.18.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/build/index.js CHANGED
@@ -9,13 +9,15 @@ 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";
15
17
  import { cleanMarkdown } from "./markdown.js";
16
18
  import { MnemonicConfigStore, readVaultSchemaVersion } from "./config.js";
17
19
  import { CONSOLIDATION_MODES, PROTECTED_BRANCH_BEHAVIORS, PROJECT_POLICY_SCOPES, WRITE_SCOPES, isProtectedBranch, resolveProtectedBranchBehavior, resolveProtectedBranchPatterns, resolveConsolidationMode, resolveWriteScope, } from "./project-memory-policy.js";
18
- import { classifyTheme, summarizePreview, titleCaseTheme, withinThemeScore, anchorScore, buildThemeCache, computeConnectionDiversity, } from "./project-introspection.js";
20
+ import { classifyTheme, classifyThemeWithGraduation, computeThemesWithGraduation, summarizePreview, titleCaseTheme, withinThemeScore, anchorScore, buildThemeCache, computeConnectionDiversity, } from "./project-introspection.js";
19
21
  import { detectProject, getCurrentGitBranch, resolveProjectIdentity } from "./project.js";
20
22
  import { VaultManager } from "./vault.js";
21
23
  import { checkBranchChange } from "./branch-tracker.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) {
@@ -689,9 +693,22 @@ function buildMutationRetryContract(args) {
689
693
  if (args.commit.status !== "failed") {
690
694
  return undefined;
691
695
  }
696
+ const recoveryKind = args.preferredRecovery ?? (args.mutationApplied
697
+ ? "manual-exact-git-recovery"
698
+ : "no-manual-recovery");
699
+ const recoveryReason = recoveryKind === "rerun-tool-call-serial"
700
+ ? "Tool-level reconciliation exists for this mutation; rerun the same tool call serially for the affected vault."
701
+ : recoveryKind === "manual-exact-git-recovery"
702
+ ? "Mutation is already persisted on disk; manual git recovery is allowed only with the exact attemptedCommit values."
703
+ : "Mutation was not applied deterministically; manual git recovery is not authorized.";
692
704
  return {
705
+ recovery: {
706
+ kind: recoveryKind,
707
+ allowed: recoveryKind !== "no-manual-recovery",
708
+ reason: recoveryReason,
709
+ },
693
710
  attemptedCommit: {
694
- message: args.commitMessage,
711
+ subject: args.commitMessage,
695
712
  body: args.commitBody,
696
713
  files: args.files,
697
714
  cwd: args.cwd,
@@ -704,19 +721,69 @@ function buildMutationRetryContract(args) {
704
721
  rationale: args.mutationApplied
705
722
  ? "Mutation is already persisted on disk; commit can be retried deterministically."
706
723
  : "Mutation was not applied; retry may require re-running the operation.",
724
+ instructions: {
725
+ sourceOfTruth: recoveryKind === "manual-exact-git-recovery" ? "attemptedCommit" : "tool-response",
726
+ useExactSubject: recoveryKind === "manual-exact-git-recovery",
727
+ useExactBody: recoveryKind === "manual-exact-git-recovery",
728
+ useExactFiles: recoveryKind === "manual-exact-git-recovery",
729
+ forbidInferenceFromHistory: true,
730
+ forbidInferenceFromTitleOrSummary: true,
731
+ forbidParallelSameVaultRetries: true,
732
+ preferToolReconciliation: recoveryKind === "rerun-tool-call-serial",
733
+ rerunSameToolCallSerially: recoveryKind === "rerun-tool-call-serial",
734
+ },
707
735
  };
708
736
  }
709
737
  function formatRetrySummary(retry) {
710
738
  if (!retry) {
711
739
  return undefined;
712
740
  }
713
- const safety = retry.retrySafe ? "safe" : "requires review";
714
741
  const opLabel = retry.attemptedCommit.operation === "add" ? "add" : "commit";
715
742
  const error = retry.attemptedCommit.error;
716
- return [
717
- `Retry: ${safety} | vault=${retry.attemptedCommit.vault} | files=${retry.attemptedCommit.files.length}`,
718
- `Git ${opLabel} error: ${error}`,
719
- ].join("\n");
743
+ const lines = [];
744
+ switch (retry.recovery.kind) {
745
+ case "rerun-tool-call-serial":
746
+ lines.push("Recovery: rerun same tool call serially");
747
+ lines.push(retry.recovery.reason);
748
+ lines.push("Rerun the same mnemonic tool call one time for the affected vault.");
749
+ lines.push("Do not replay same-vault mutations in parallel.");
750
+ lines.push("Manual git recovery is not authorized for this failure.");
751
+ lines.push("Git failure:");
752
+ lines.push(`${opLabel}: ${error}`);
753
+ break;
754
+ case "manual-exact-git-recovery":
755
+ lines.push("Recovery: manual exact git recovery allowed");
756
+ lines.push(retry.recovery.reason);
757
+ lines.push("Use only the exact values below. Do not infer from git history, note title, summary, or repo state.");
758
+ lines.push("");
759
+ lines.push("Commit subject:");
760
+ lines.push(retry.attemptedCommit.subject);
761
+ if (retry.attemptedCommit.body) {
762
+ lines.push("");
763
+ lines.push("Commit body:");
764
+ lines.push(retry.attemptedCommit.body);
765
+ }
766
+ lines.push("");
767
+ lines.push("Files:");
768
+ for (const file of retry.attemptedCommit.files) {
769
+ lines.push(`- ${file}`);
770
+ }
771
+ lines.push("");
772
+ lines.push("Git failure:");
773
+ lines.push(`${opLabel}: ${error}`);
774
+ break;
775
+ case "no-manual-recovery":
776
+ lines.push("Recovery: no manual recovery authorized");
777
+ lines.push(retry.recovery.reason);
778
+ lines.push("Git failure:");
779
+ lines.push(`${opLabel}: ${error}`);
780
+ break;
781
+ default: {
782
+ const _exhaustive = retry.recovery.kind;
783
+ throw new Error(`Unknown recovery kind: ${_exhaustive}`);
784
+ }
785
+ }
786
+ return lines.join("\n");
720
787
  }
721
788
  function formatPersistenceSummary(persistence) {
722
789
  const parts = [
@@ -727,14 +794,14 @@ function formatPersistenceSummary(persistence) {
727
794
  if (persistence.embedding.reason) {
728
795
  lines[0] += ` | embedding reason=${persistence.embedding.reason}`;
729
796
  }
730
- if (persistence.git.commit === "failed" && persistence.git.commitError) {
797
+ const retrySummary = formatRetrySummary(persistence.retry);
798
+ if (!retrySummary && persistence.git.commit === "failed" && persistence.git.commitError) {
731
799
  const opLabel = persistence.git.commitOperation === "add" ? "add" : "commit";
732
800
  lines.push(`Git ${opLabel} error: ${persistence.git.commitError}`);
733
801
  }
734
802
  if (persistence.git.push === "failed" && persistence.git.pushError) {
735
803
  lines.push(`Git push error: ${persistence.git.pushError}`);
736
804
  }
737
- const retrySummary = formatRetrySummary(persistence.retry);
738
805
  if (retrySummary) {
739
806
  lines.push(retrySummary);
740
807
  }
@@ -782,7 +849,7 @@ function vaultMatchesStorageScope(vault, storedIn) {
782
849
  // "project-vault" covers the primary project vault and all submodule vaults.
783
850
  return vault.isProject;
784
851
  }
785
- async function collectVisibleNotes(cwd, scope = "all", tags, storedIn = "any") {
852
+ async function collectVisibleNotes(cwd, scope = "all", tags, storedIn = "any", sessionProjectId) {
786
853
  const project = await resolveProject(cwd);
787
854
  const vaults = await vaultManager.searchOrder(cwd);
788
855
  let filterProject = undefined;
@@ -793,8 +860,23 @@ async function collectVisibleNotes(cwd, scope = "all", tags, storedIn = "any") {
793
860
  const seen = new Set();
794
861
  const entries = [];
795
862
  for (const vault of vaults) {
796
- const vaultNotes = await vault.storage.listNotes(filterProject !== undefined ? { project: filterProject } : undefined);
797
- for (const note of vaultNotes) {
863
+ let rawNotes;
864
+ if (sessionProjectId) {
865
+ const cached = await getOrBuildVaultNoteList(sessionProjectId, vault);
866
+ if (cached !== undefined) {
867
+ // Apply project filter on the full cached list
868
+ rawNotes = filterProject !== undefined
869
+ ? cached.filter((n) => filterProject === null ? !n.project : n.project === filterProject)
870
+ : cached;
871
+ }
872
+ else {
873
+ rawNotes = await vault.storage.listNotes(filterProject !== undefined ? { project: filterProject } : undefined);
874
+ }
875
+ }
876
+ else {
877
+ rawNotes = await vault.storage.listNotes(filterProject !== undefined ? { project: filterProject } : undefined);
878
+ }
879
+ for (const note of rawNotes) {
798
880
  if (seen.has(note.id)) {
799
881
  continue;
800
882
  }
@@ -950,6 +1032,7 @@ server.registerTool("detect_project", {
950
1032
  "- Use `recall` or `project_memory_summary` to orient on existing memory.",
951
1033
  annotations: {
952
1034
  readOnlyHint: true,
1035
+ destructiveHint: false,
953
1036
  idempotentHint: true,
954
1037
  openWorldHint: false,
955
1038
  },
@@ -1004,6 +1087,7 @@ server.registerTool("get_project_identity", {
1004
1087
  "- Use `set_project_identity` only if the wrong remote is defining identity.",
1005
1088
  annotations: {
1006
1089
  readOnlyHint: true,
1090
+ destructiveHint: false,
1007
1091
  idempotentHint: true,
1008
1092
  openWorldHint: false,
1009
1093
  },
@@ -1160,6 +1244,7 @@ server.registerTool("list_migrations", {
1160
1244
  "- Run `execute_migration` with `dryRun: true` first.",
1161
1245
  annotations: {
1162
1246
  readOnlyHint: true,
1247
+ destructiveHint: false,
1163
1248
  idempotentHint: true,
1164
1249
  openWorldHint: false,
1165
1250
  },
@@ -1442,6 +1527,7 @@ server.registerTool("remember", {
1442
1527
  timestamp: now,
1443
1528
  persistence,
1444
1529
  };
1530
+ invalidateActiveProjectCache();
1445
1531
  return {
1446
1532
  content: [{ type: "text", text: textContent }],
1447
1533
  structuredContent,
@@ -1580,6 +1666,7 @@ server.registerTool("get_project_memory_policy", {
1580
1666
  "- Call `remember` with explicit `scope` for a one-off override, or `set_project_memory_policy` to change defaults.",
1581
1667
  annotations: {
1582
1668
  readOnlyHint: true,
1669
+ destructiveHint: false,
1583
1670
  idempotentHint: true,
1584
1671
  openWorldHint: false,
1585
1672
  },
@@ -1653,6 +1740,7 @@ server.registerTool("recall", {
1653
1740
  "- Use `get`, `update`, `relate`, or `consolidate` based on the results.",
1654
1741
  annotations: {
1655
1742
  readOnlyHint: true,
1743
+ destructiveHint: false,
1656
1744
  idempotentHint: true,
1657
1745
  openWorldHint: true,
1658
1746
  },
@@ -1674,6 +1762,7 @@ server.registerTool("recall", {
1674
1762
  }),
1675
1763
  outputSchema: RecallResultSchema,
1676
1764
  }, async ({ query, cwd, limit, minSimilarity, mode, verbose, tags, scope }) => {
1765
+ const t0Recall = performance.now();
1677
1766
  await ensureBranchSynced(cwd);
1678
1767
  const project = await resolveProject(cwd);
1679
1768
  const queryVec = await embed(query);
@@ -1681,6 +1770,12 @@ server.registerTool("recall", {
1681
1770
  const noteCache = new Map();
1682
1771
  const noteCacheKey = (vault, id) => `${vault.storage.vaultPath}::${id}`;
1683
1772
  const readCachedNote = async (vault, id) => {
1773
+ // Check session cache first (populated when getOrBuildVaultEmbeddings was called)
1774
+ if (project) {
1775
+ const sessionNote = getSessionCachedNote(project.id, vault.storage.vaultPath, id);
1776
+ if (sessionNote !== undefined)
1777
+ return sessionNote;
1778
+ }
1684
1779
  const key = noteCacheKey(vault, id);
1685
1780
  const cached = noteCache.get(key);
1686
1781
  if (cached) {
@@ -1697,7 +1792,9 @@ server.registerTool("recall", {
1697
1792
  }
1698
1793
  const scored = [];
1699
1794
  for (const vault of vaults) {
1700
- const embeddings = await vault.storage.listEmbeddings();
1795
+ const embeddings = project
1796
+ ? (await getOrBuildVaultEmbeddings(project.id, vault)) ?? await vault.storage.listEmbeddings()
1797
+ : await vault.storage.listEmbeddings();
1701
1798
  for (const rec of embeddings) {
1702
1799
  const rawScore = cosineSimilarity(queryVec, rec.embedding);
1703
1800
  if (rawScore < minSimilarity)
@@ -1792,6 +1889,7 @@ server.registerTool("recall", {
1792
1889
  scope: scope || "all",
1793
1890
  results: structuredResults,
1794
1891
  };
1892
+ console.error(`[recall:timing] ${(performance.now() - t0Recall).toFixed(1)}ms`);
1795
1893
  return {
1796
1894
  content: [{ type: "text", text: textContent }],
1797
1895
  structuredContent,
@@ -1942,6 +2040,7 @@ server.registerTool("update", {
1942
2040
  lifecycle: updated.lifecycle,
1943
2041
  persistence,
1944
2042
  };
2043
+ invalidateActiveProjectCache();
1945
2044
  return { content: [{ type: "text", text: `Updated memory '${id}'\n${formatPersistenceSummary(persistence)}` }], structuredContent };
1946
2045
  });
1947
2046
  // ── forget ────────────────────────────────────────────────────────────────────
@@ -2048,6 +2147,7 @@ server.registerTool("forget", {
2048
2147
  retry,
2049
2148
  };
2050
2149
  const retrySummary = formatRetrySummary(retry);
2150
+ invalidateActiveProjectCache();
2051
2151
  return {
2052
2152
  content: [{
2053
2153
  type: "text",
@@ -2075,6 +2175,7 @@ server.registerTool("get", {
2075
2175
  "- Use `update`, `forget`, `move_memory`, or `relate` after inspection.",
2076
2176
  annotations: {
2077
2177
  readOnlyHint: true,
2178
+ destructiveHint: false,
2078
2179
  idempotentHint: true,
2079
2180
  openWorldHint: false,
2080
2181
  },
@@ -2085,12 +2186,26 @@ server.registerTool("get", {
2085
2186
  }),
2086
2187
  outputSchema: GetResultSchema,
2087
2188
  }, async ({ ids, cwd, includeRelationships }) => {
2189
+ const t0Get = performance.now();
2088
2190
  await ensureBranchSynced(cwd);
2089
2191
  const project = await resolveProject(cwd);
2090
2192
  const found = [];
2091
2193
  const notFound = [];
2092
2194
  for (const id of ids) {
2093
- const result = await vaultManager.findNote(id, cwd);
2195
+ // Check session cache before hitting storage
2196
+ let result = null;
2197
+ if (project) {
2198
+ for (const vault of vaultManager.allKnownVaults()) {
2199
+ const cached = getSessionCachedNote(project.id, vault.storage.vaultPath, id);
2200
+ if (cached !== undefined) {
2201
+ result = { note: cached, vault };
2202
+ break;
2203
+ }
2204
+ }
2205
+ }
2206
+ if (!result) {
2207
+ result = await vaultManager.findNote(id, cwd);
2208
+ }
2094
2209
  if (!result) {
2095
2210
  notFound.push(id);
2096
2211
  continue;
@@ -2138,6 +2253,7 @@ server.registerTool("get", {
2138
2253
  notes: found,
2139
2254
  notFound,
2140
2255
  };
2256
+ console.error(`[get:timing] ${(performance.now() - t0Get).toFixed(1)}ms`);
2141
2257
  return { content: [{ type: "text", text: lines.join("\n").trim() }], structuredContent };
2142
2258
  });
2143
2259
  // ── where_is_memory ───────────────────────────────────────────────────────────
@@ -2157,6 +2273,7 @@ server.registerTool("where_is_memory", {
2157
2273
  "- Use `move_memory` if the storage location is wrong, or `get` for full inspection.",
2158
2274
  annotations: {
2159
2275
  readOnlyHint: true,
2276
+ destructiveHint: false,
2160
2277
  idempotentHint: true,
2161
2278
  openWorldHint: false,
2162
2279
  },
@@ -2213,6 +2330,7 @@ server.registerTool("list", {
2213
2330
  "- Use `get` for exact inspection or `update` / `consolidate` for cleanup.",
2214
2331
  annotations: {
2215
2332
  readOnlyHint: true,
2333
+ destructiveHint: false,
2216
2334
  idempotentHint: true,
2217
2335
  openWorldHint: false,
2218
2336
  },
@@ -2323,6 +2441,7 @@ server.registerTool("discover_tags", {
2323
2441
  "Read-only.",
2324
2442
  annotations: {
2325
2443
  readOnlyHint: true,
2444
+ destructiveHint: false,
2326
2445
  idempotentHint: true,
2327
2446
  openWorldHint: false,
2328
2447
  },
@@ -2529,6 +2648,7 @@ server.registerTool("recent_memories", {
2529
2648
  "- Use `get` for exact inspection or `update` to continue refining a recent note.",
2530
2649
  annotations: {
2531
2650
  readOnlyHint: true,
2651
+ destructiveHint: false,
2532
2652
  idempotentHint: true,
2533
2653
  openWorldHint: false,
2534
2654
  },
@@ -2598,6 +2718,7 @@ server.registerTool("memory_graph", {
2598
2718
  "- Use `get`, `relate`, `unrelate`, or `consolidate` based on what the graph reveals.",
2599
2719
  annotations: {
2600
2720
  readOnlyHint: true,
2721
+ destructiveHint: false,
2601
2722
  idempotentHint: true,
2602
2723
  openWorldHint: false,
2603
2724
  },
@@ -2677,6 +2798,7 @@ server.registerTool("project_memory_summary", {
2677
2798
  "- Use `recall` or `list` to drill down into specific areas.",
2678
2799
  annotations: {
2679
2800
  readOnlyHint: true,
2801
+ destructiveHint: false,
2680
2802
  idempotentHint: true,
2681
2803
  openWorldHint: false,
2682
2804
  },
@@ -2690,8 +2812,11 @@ server.registerTool("project_memory_summary", {
2690
2812
  }),
2691
2813
  outputSchema: ProjectSummaryResultSchema,
2692
2814
  }, async ({ cwd, maxPerTheme, recentLimit, anchorLimit, includeRelatedGlobal, relatedGlobalLimit }) => {
2815
+ const t0Summary = performance.now();
2693
2816
  await ensureBranchSynced(cwd);
2694
- const { project, entries } = await collectVisibleNotes(cwd, "all");
2817
+ // Pre-resolve project so we can pass its id to collectVisibleNotes for session caching
2818
+ const preProject = await resolveProject(cwd);
2819
+ const { project, entries } = await collectVisibleNotes(cwd, "all", undefined, "any", preProject?.id);
2695
2820
  if (!project) {
2696
2821
  return { content: [{ type: "text", text: `Could not detect a project for: ${cwd}` }], isError: true };
2697
2822
  }
@@ -2715,17 +2840,23 @@ server.registerTool("project_memory_summary", {
2715
2840
  }
2716
2841
  const policyLine = await formatProjectPolicyLine(project.id);
2717
2842
  // Build theme cache for connection diversity scoring (project-scoped only)
2843
+ // This uses simple classifyTheme for consistent diversity calculations
2718
2844
  const themeCache = buildThemeCache(projectEntries.map(e => e.note));
2719
- // Categorize by theme (project-scoped only)
2845
+ // Compute promoted themes from keywords (graduation system)
2846
+ const graduationResult = computeThemesWithGraduation(projectEntries.map(e => e.note));
2847
+ const promotedThemes = new Set(graduationResult.promotedThemes);
2848
+ // Categorize by theme with graduation (project-scoped only)
2720
2849
  const themed = new Map();
2721
2850
  for (const entry of projectEntries) {
2722
- const theme = classifyTheme(entry.note);
2851
+ const theme = classifyThemeWithGraduation(entry.note, promotedThemes);
2723
2852
  const bucket = themed.get(theme) ?? [];
2724
2853
  bucket.push(entry);
2725
2854
  themed.set(theme, bucket);
2726
2855
  }
2727
- // Theme order for display
2728
- const themeOrder = ["overview", "decisions", "tooling", "bugs", "architecture", "quality", "other"];
2856
+ // Theme order: fixed themes first, then promoted themes alphabetically, then "other"
2857
+ const fixedThemes = ["overview", "decisions", "tooling", "bugs", "architecture", "quality"];
2858
+ const dynamicThemes = graduationResult.promotedThemes.filter(t => !fixedThemes.includes(t));
2859
+ const themeOrder = [...fixedThemes, ...dynamicThemes.sort(), "other"];
2729
2860
  // Calculate notes distribution (project-scoped only)
2730
2861
  const projectVaultCount = projectEntries.filter(e => e.vault.isProject).length;
2731
2862
  const mainVaultProjectEntries = projectEntries.filter(e => !e.vault.isProject);
@@ -3001,12 +3132,13 @@ server.registerTool("project_memory_summary", {
3001
3132
  id: e.note.id,
3002
3133
  title: e.note.title,
3003
3134
  updatedAt: e.note.updatedAt,
3004
- theme: classifyTheme(e.note),
3135
+ theme: classifyThemeWithGraduation(e.note, promotedThemes),
3005
3136
  })),
3006
3137
  anchors,
3007
3138
  orientation,
3008
3139
  relatedGlobal,
3009
3140
  };
3141
+ console.error(`[summary:timing] ${(performance.now() - t0Summary).toFixed(1)}ms`);
3010
3142
  return { content: [{ type: "text", text: sections.join("\n") }], structuredContent };
3011
3143
  });
3012
3144
  // ── sync ──────────────────────────────────────────────────────────────────────
@@ -3093,6 +3225,8 @@ server.registerTool("sync", {
3093
3225
  action: "synced",
3094
3226
  vaults: vaultResults,
3095
3227
  };
3228
+ // Vault contents may have changed via pull — discard session cache
3229
+ invalidateActiveProjectCache();
3096
3230
  return { content: [{ type: "text", text: lines.join("\n") }], structuredContent };
3097
3231
  });
3098
3232
  // ── move_memory ───────────────────────────────────────────────────────────────
@@ -3244,6 +3378,7 @@ server.registerTool("move_memory", {
3244
3378
  const associationText = metadataRewritten
3245
3379
  ? `Project association is now ${associationValue}.`
3246
3380
  : `Project association remains ${associationValue}.`;
3381
+ invalidateActiveProjectCache();
3247
3382
  return {
3248
3383
  content: [{
3249
3384
  type: "text",
@@ -3349,6 +3484,7 @@ server.registerTool("relate", {
3349
3484
  cwd,
3350
3485
  vault,
3351
3486
  mutationApplied: true,
3487
+ preferredRecovery: "rerun-tool-call-serial",
3352
3488
  });
3353
3489
  const structuredContent = {
3354
3490
  action: "related",
@@ -3398,6 +3534,7 @@ server.registerTool("relate", {
3398
3534
  cwd,
3399
3535
  vault,
3400
3536
  mutationApplied: true,
3537
+ preferredRecovery: "rerun-tool-call-serial",
3401
3538
  });
3402
3539
  }
3403
3540
  if (commitStatus.status === "committed") {
@@ -3416,6 +3553,7 @@ server.registerTool("relate", {
3416
3553
  retry,
3417
3554
  };
3418
3555
  const retrySummary = formatRetrySummary(retry);
3556
+ invalidateActiveProjectCache();
3419
3557
  return {
3420
3558
  content: [{
3421
3559
  type: "text",
@@ -3519,6 +3657,7 @@ server.registerTool("unrelate", {
3519
3657
  cwd,
3520
3658
  vault,
3521
3659
  mutationApplied: true,
3660
+ preferredRecovery: "rerun-tool-call-serial",
3522
3661
  });
3523
3662
  const structuredContent = {
3524
3663
  action: "unrelated",
@@ -3562,6 +3701,7 @@ server.registerTool("unrelate", {
3562
3701
  cwd,
3563
3702
  vault,
3564
3703
  mutationApplied: true,
3704
+ preferredRecovery: "rerun-tool-call-serial",
3565
3705
  });
3566
3706
  }
3567
3707
  if (commitStatus.status === "committed") {
@@ -3582,6 +3722,7 @@ server.registerTool("unrelate", {
3582
3722
  retry,
3583
3723
  };
3584
3724
  const retrySummary = formatRetrySummary(retry);
3725
+ invalidateActiveProjectCache();
3585
3726
  return {
3586
3727
  content: [{
3587
3728
  type: "text",
@@ -3680,13 +3821,19 @@ server.registerTool("consolidate", {
3680
3821
  return findClusters(projectNotes, project);
3681
3822
  case "suggest-merges":
3682
3823
  return suggestMerges(projectNotes, threshold, defaultConsolidationMode, project, mode);
3683
- case "execute-merge":
3824
+ case "execute-merge": {
3684
3825
  if (!mergePlan) {
3685
3826
  return { content: [{ type: "text", text: "execute-merge strategy requires a mergePlan with sourceIds and targetTitle." }], isError: true };
3686
3827
  }
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);
3828
+ const mergeResult = await executeMerge(entries, mergePlan, defaultConsolidationMode, project, cwd, mode, policy, allowProtectedBranch);
3829
+ invalidateActiveProjectCache();
3830
+ return mergeResult;
3831
+ }
3832
+ case "prune-superseded": {
3833
+ const pruneResult = await pruneSuperseded(projectNotes, mode ?? defaultConsolidationMode, project, cwd, policy, allowProtectedBranch);
3834
+ invalidateActiveProjectCache();
3835
+ return pruneResult;
3836
+ }
3690
3837
  case "dry-run":
3691
3838
  return dryRunAll(projectNotes, threshold, defaultConsolidationMode, project, mode);
3692
3839
  default: