@danielmarbach/mnemonic-mcp 0.9.0 → 0.11.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
@@ -15,6 +15,7 @@ import { CONSOLIDATION_MODES, PROTECTED_BRANCH_BEHAVIORS, PROJECT_POLICY_SCOPES,
15
15
  import { classifyTheme, summarizePreview, titleCaseTheme } from "./project-introspection.js";
16
16
  import { detectProject, getCurrentGitBranch, resolveProjectIdentity } from "./project.js";
17
17
  import { VaultManager } from "./vault.js";
18
+ import { checkBranchChange } from "./branch-tracker.js";
18
19
  import { Migrator } from "./migration.js";
19
20
  import { parseMemorySections } from "./import.js";
20
21
  import { defaultClaudeHome, defaultVaultPath, resolveUserPath } from "./paths.js";
@@ -331,6 +332,33 @@ async function resolveWriteVault(cwd, scope) {
331
332
  function describeProject(project) {
332
333
  return project ? `project '${project.name}' (${project.id})` : "global";
333
334
  }
335
+ /**
336
+ * Checks if the git branch has changed since the last operation for a directory.
337
+ * If a branch change is detected, automatically triggers sync to rebuild embeddings.
338
+ * Returns true if sync was triggered, false otherwise.
339
+ */
340
+ async function ensureBranchSynced(cwd) {
341
+ if (!cwd)
342
+ return false;
343
+ const previousBranch = await checkBranchChange(cwd);
344
+ if (!previousBranch)
345
+ return false; // No branch change or not in git repo
346
+ console.error(`[branch] Detected branch change from '${previousBranch}' — auto-syncing`);
347
+ // Trigger sync to rebuild embeddings
348
+ const mainResult = await vaultManager.main.git.sync();
349
+ console.error(`[branch] Main vault sync: ${JSON.stringify(mainResult)}`);
350
+ const projectVault = await vaultManager.getProjectVaultIfExists(cwd);
351
+ if (projectVault) {
352
+ const projectResult = await projectVault.git.sync();
353
+ console.error(`[branch] Project vault sync: ${JSON.stringify(projectResult)}`);
354
+ // Backfill embeddings after sync
355
+ const backfill = await backfillEmbeddingsAfterSync(projectVault.storage, "project vault", [], true);
356
+ console.error(`[branch] Project vault embedded ${backfill.embedded} notes`);
357
+ }
358
+ const mainBackfill = await backfillEmbeddingsAfterSync(vaultManager.main.storage, "main vault", [], true);
359
+ console.error(`[branch] Main vault embedded ${mainBackfill.embedded} notes`);
360
+ return true;
361
+ }
334
362
  function formatProjectIdentityText(identity) {
335
363
  const lines = [
336
364
  `Project identity:`,
@@ -592,12 +620,34 @@ function buildPersistenceStatus(args) {
592
620
  commitMessage: args.commitMessage,
593
621
  commitBody: args.commitBody,
594
622
  commitReason: args.commit.reason,
623
+ commitError: args.commit.error,
595
624
  pushReason: args.push.reason,
596
625
  pushError: args.push.error,
597
626
  },
627
+ retry: args.retry,
598
628
  durability: resolveDurability(args.commit, args.push),
599
629
  };
600
630
  }
631
+ function buildMutationRetryContract(args) {
632
+ if (args.commit.status !== "failed") {
633
+ return undefined;
634
+ }
635
+ return {
636
+ attemptedCommit: {
637
+ message: args.commitMessage,
638
+ body: args.commitBody,
639
+ files: args.files,
640
+ cwd: args.cwd,
641
+ vault: storageLabel(args.vault),
642
+ error: args.commit.error ?? "Unknown git commit failure",
643
+ },
644
+ mutationApplied: args.mutationApplied,
645
+ retrySafe: args.mutationApplied,
646
+ rationale: args.mutationApplied
647
+ ? "Mutation is already persisted on disk; commit can be retried deterministically."
648
+ : "Mutation was not applied; retry may require re-running the operation.",
649
+ };
650
+ }
601
651
  function formatPersistenceSummary(persistence) {
602
652
  const parts = [
603
653
  `Persistence: embedding ${persistence.embedding.status}`,
@@ -716,7 +766,7 @@ async function formatProjectPolicyLine(projectId) {
716
766
  }
717
767
  return `Policy: default write scope ${policy.defaultScope} (updated ${policy.updatedAt})`;
718
768
  }
719
- async function moveNoteBetweenVaults(found, targetVault, noteToWrite) {
769
+ async function moveNoteBetweenVaults(found, targetVault, noteToWrite, cwd) {
720
770
  const { note, vault: sourceVault } = found;
721
771
  const finalNote = noteToWrite ?? note;
722
772
  const embedding = await sourceVault.storage.readEmbedding(note.id);
@@ -733,7 +783,9 @@ async function moveNoteBetweenVaults(found, targetVault, noteToWrite) {
733
783
  noteTitle: finalNote.title,
734
784
  projectName: finalNote.projectName,
735
785
  });
736
- const targetCommit = await targetVault.git.commitWithStatus(`move: ${finalNote.title}`, [vaultManager.noteRelPath(targetVault, finalNote.id)], targetCommitBody);
786
+ const targetCommitMessage = `move: ${finalNote.title}`;
787
+ const targetCommitFiles = [vaultManager.noteRelPath(targetVault, finalNote.id)];
788
+ const targetCommit = await targetVault.git.commitWithStatus(targetCommitMessage, targetCommitFiles, targetCommitBody);
737
789
  const sourceCommitBody = formatCommitBody({
738
790
  summary: `Moved to ${targetVaultLabel}`,
739
791
  noteId: finalNote.id,
@@ -741,7 +793,18 @@ async function moveNoteBetweenVaults(found, targetVault, noteToWrite) {
741
793
  projectName: finalNote.projectName,
742
794
  });
743
795
  await sourceVault.git.commitWithStatus(`move: ${finalNote.title}`, [vaultManager.noteRelPath(sourceVault, finalNote.id)], sourceCommitBody);
744
- const targetPush = await pushAfterMutation(targetVault);
796
+ const targetPush = targetCommit.status === "committed"
797
+ ? await pushAfterMutation(targetVault)
798
+ : { status: "skipped", reason: "commit-failed" };
799
+ const retry = buildMutationRetryContract({
800
+ commit: targetCommit,
801
+ commitMessage: targetCommitMessage,
802
+ commitBody: targetCommitBody,
803
+ files: targetCommitFiles,
804
+ cwd,
805
+ vault: targetVault,
806
+ mutationApplied: true,
807
+ });
745
808
  if (sourceVault !== targetVault) {
746
809
  await pushAfterMutation(sourceVault);
747
810
  }
@@ -753,8 +816,9 @@ async function moveNoteBetweenVaults(found, targetVault, noteToWrite) {
753
816
  embedding: embedding ? { status: "written" } : { status: "skipped", reason: "no-source-embedding" },
754
817
  commit: targetCommit,
755
818
  push: targetPush,
756
- commitMessage: `move: ${finalNote.title}`,
819
+ commitMessage: targetCommitMessage,
757
820
  commitBody: targetCommitBody,
821
+ retry,
758
822
  }),
759
823
  };
760
824
  }
@@ -952,8 +1016,21 @@ server.registerTool("set_project_identity", {
952
1016
  `Resolved identity: ${candidateIdentity.project.id}\n` +
953
1017
  `Remote: ${remoteName}`,
954
1018
  });
955
- await vaultManager.main.git.commit(`identity: ${defaultProject.name} use remote ${remoteName}`, ["config.json"], commitBody);
956
- await pushAfterMutation(vaultManager.main);
1019
+ const commitMessage = `identity: ${defaultProject.name} use remote ${remoteName}`;
1020
+ const commitFiles = ["config.json"];
1021
+ const commitStatus = await vaultManager.main.git.commitWithStatus(commitMessage, commitFiles, commitBody);
1022
+ const pushStatus = commitStatus.status === "committed"
1023
+ ? await pushAfterMutation(vaultManager.main)
1024
+ : { status: "skipped", reason: "commit-failed" };
1025
+ const retry = buildMutationRetryContract({
1026
+ commit: commitStatus,
1027
+ commitMessage,
1028
+ commitBody,
1029
+ files: commitFiles,
1030
+ cwd,
1031
+ vault: vaultManager.main,
1032
+ mutationApplied: true,
1033
+ });
957
1034
  const structuredContent = {
958
1035
  action: "project_identity_set",
959
1036
  project: {
@@ -971,12 +1048,14 @@ server.registerTool("set_project_identity", {
971
1048
  remoteName,
972
1049
  updatedAt: now,
973
1050
  },
1051
+ retry,
974
1052
  };
975
1053
  return {
976
1054
  content: [{
977
1055
  type: "text",
978
1056
  text: `Project identity override set for ${defaultProject.name}: ` +
979
- `default=\`${defaultProject.id}\`, effective=\`${candidateIdentity.project.id}\`, remote=${remoteName}`,
1057
+ `default=\`${defaultProject.id}\`, effective=\`${candidateIdentity.project.id}\`, remote=${remoteName}` +
1058
+ `${commitStatus.status === "failed" ? `\nCommit failed; retry data included. Push status: ${pushStatus.status}.` : ""}`,
980
1059
  }],
981
1060
  structuredContent,
982
1061
  };
@@ -1067,6 +1146,7 @@ server.registerTool("execute_migration", {
1067
1146
  }),
1068
1147
  outputSchema: MigrationExecuteResultSchema,
1069
1148
  }, async ({ migrationName, dryRun, backup, cwd }) => {
1149
+ await ensureBranchSynced(cwd);
1070
1150
  try {
1071
1151
  const { results, vaultsProcessed } = await migrator.runMigration(migrationName, {
1072
1152
  dryRun,
@@ -1182,6 +1262,7 @@ server.registerTool("remember", {
1182
1262
  }),
1183
1263
  outputSchema: RememberResultSchema,
1184
1264
  }, async ({ title, content, tags, lifecycle, summary, cwd, scope, allowProtectedBranch = false }) => {
1265
+ await ensureBranchSynced(cwd);
1185
1266
  const project = await resolveProject(cwd);
1186
1267
  const cleanedContent = await cleanMarkdown(content);
1187
1268
  const policy = project ? await configStore.getProjectPolicy(project.id) : undefined;
@@ -1195,7 +1276,7 @@ server.registerTool("remember", {
1195
1276
  const protectedBranchCheck = await shouldBlockProtectedBranchCommit({
1196
1277
  cwd,
1197
1278
  writeScope,
1198
- automaticCommit: scope === undefined,
1279
+ automaticCommit: true,
1199
1280
  projectLabel: project ? `${project.name} (${project.id})` : "this context",
1200
1281
  policy,
1201
1282
  allowProtectedBranch,
@@ -1236,16 +1317,30 @@ server.registerTool("remember", {
1236
1317
  scope: writeScope,
1237
1318
  tags: tags,
1238
1319
  });
1239
- const commitStatus = await vault.git.commitWithStatus(`remember: ${title}`, [vaultManager.noteRelPath(vault, id)], commitBody);
1240
- const pushStatus = await pushAfterMutation(vault);
1320
+ const commitMessage = `remember: ${title}`;
1321
+ const commitFiles = [vaultManager.noteRelPath(vault, id)];
1322
+ const commitStatus = await vault.git.commitWithStatus(commitMessage, commitFiles, commitBody);
1323
+ const pushStatus = commitStatus.status === "committed"
1324
+ ? await pushAfterMutation(vault)
1325
+ : { status: "skipped", reason: "commit-failed" };
1326
+ const retry = buildMutationRetryContract({
1327
+ commit: commitStatus,
1328
+ commitMessage,
1329
+ commitBody,
1330
+ files: commitFiles,
1331
+ cwd,
1332
+ vault,
1333
+ mutationApplied: true,
1334
+ });
1241
1335
  const persistence = buildPersistenceStatus({
1242
1336
  storage: vault.storage,
1243
1337
  id,
1244
1338
  embedding: embeddingStatus,
1245
1339
  commit: commitStatus,
1246
1340
  push: pushStatus,
1247
- commitMessage: `remember: ${title}`,
1341
+ commitMessage,
1248
1342
  commitBody,
1343
+ retry,
1249
1344
  });
1250
1345
  const vaultLabel = vault.isProject ? " [project vault]" : " [main vault]";
1251
1346
  const textContent = `Remembered as \`${id}\` [${projectScope}, stored=${writeScope}]${vaultLabel}\n${formatPersistenceSummary(persistence)}`;
@@ -1345,8 +1440,21 @@ server.registerTool("set_project_memory_policy", {
1345
1440
  ? `\nProtected branch patterns: ${effectiveProtectedBranchPatterns.join(", ")}`
1346
1441
  : ""}`,
1347
1442
  });
1348
- await vaultManager.main.git.commit(`policy: ${project.name} default scope ${effectiveDefaultScope}`, ["config.json"], commitBody);
1349
- await pushAfterMutation(vaultManager.main);
1443
+ const commitMessage = `policy: ${project.name} default scope ${effectiveDefaultScope}`;
1444
+ const commitFiles = ["config.json"];
1445
+ const commitStatus = await vaultManager.main.git.commitWithStatus(commitMessage, commitFiles, commitBody);
1446
+ const pushStatus = commitStatus.status === "committed"
1447
+ ? await pushAfterMutation(vaultManager.main)
1448
+ : { status: "skipped", reason: "commit-failed" };
1449
+ const retry = buildMutationRetryContract({
1450
+ commit: commitStatus,
1451
+ commitMessage,
1452
+ commitBody,
1453
+ files: commitFiles,
1454
+ cwd,
1455
+ vault: vaultManager.main,
1456
+ mutationApplied: true,
1457
+ });
1350
1458
  const structuredContent = {
1351
1459
  action: "policy_set",
1352
1460
  project: { id: project.id, name: project.name },
@@ -1354,13 +1462,15 @@ server.registerTool("set_project_memory_policy", {
1354
1462
  consolidationMode: effectiveConsolidationMode,
1355
1463
  protectedBranchBehavior: effectiveProtectedBranchBehavior,
1356
1464
  protectedBranchPatterns: effectiveProtectedBranchPatterns,
1357
- timestamp: now,
1465
+ updatedAt: now,
1466
+ retry,
1358
1467
  };
1359
1468
  return {
1360
1469
  content: [{
1361
1470
  type: "text",
1362
1471
  text: `Project memory policy set for ${project.name}: defaultScope=${effectiveDefaultScope}` +
1363
- `${modeStr}${branchBehaviorStr}${branchPatternsStr}`,
1472
+ `${modeStr}${branchBehaviorStr}${branchPatternsStr}` +
1473
+ `${commitStatus.status === "failed" ? `\nCommit failed; retry data included. Push status: ${pushStatus.status}.` : ""}`,
1364
1474
  }],
1365
1475
  structuredContent,
1366
1476
  };
@@ -1470,9 +1580,24 @@ server.registerTool("recall", {
1470
1580
  }),
1471
1581
  outputSchema: RecallResultSchema,
1472
1582
  }, async ({ query, cwd, limit, minSimilarity, tags, scope }) => {
1583
+ await ensureBranchSynced(cwd);
1473
1584
  const project = await resolveProject(cwd);
1474
1585
  const queryVec = await embed(query);
1475
1586
  const vaults = await vaultManager.searchOrder(cwd);
1587
+ const noteCache = new Map();
1588
+ const noteCacheKey = (vault, id) => `${vault.storage.vaultPath}::${id}`;
1589
+ const readCachedNote = async (vault, id) => {
1590
+ const key = noteCacheKey(vault, id);
1591
+ const cached = noteCache.get(key);
1592
+ if (cached) {
1593
+ return cached;
1594
+ }
1595
+ const note = await vault.storage.readNote(id);
1596
+ if (note) {
1597
+ noteCache.set(key, note);
1598
+ }
1599
+ return note;
1600
+ };
1476
1601
  for (const vault of vaults) {
1477
1602
  await embedMissingNotes(vault.storage).catch(() => { });
1478
1603
  }
@@ -1483,7 +1608,7 @@ server.registerTool("recall", {
1483
1608
  const rawScore = cosineSimilarity(queryVec, rec.embedding);
1484
1609
  if (rawScore < minSimilarity)
1485
1610
  continue;
1486
- const note = await vault.storage.readNote(rec.id);
1611
+ const note = await readCachedNote(vault, rec.id);
1487
1612
  if (!note)
1488
1613
  continue;
1489
1614
  if (tags && tags.length > 0) {
@@ -1512,7 +1637,7 @@ server.registerTool("recall", {
1512
1637
  }
1513
1638
  const sections = [];
1514
1639
  for (const { id, score, vault } of top) {
1515
- const note = await vault.storage.readNote(id);
1640
+ const note = await readCachedNote(vault, id);
1516
1641
  if (note)
1517
1642
  sections.push(formatNote(note, score));
1518
1643
  }
@@ -1523,7 +1648,7 @@ server.registerTool("recall", {
1523
1648
  // Build structured results array
1524
1649
  const structuredResults = [];
1525
1650
  for (const { id, score, vault, boosted } of top) {
1526
- const note = await vault.storage.readNote(id);
1651
+ const note = await readCachedNote(vault, id);
1527
1652
  if (note) {
1528
1653
  structuredResults.push({
1529
1654
  id,
@@ -1590,6 +1715,7 @@ server.registerTool("update", {
1590
1715
  }),
1591
1716
  outputSchema: UpdateResultSchema,
1592
1717
  }, async ({ id, content, title, tags, lifecycle, summary, cwd, allowProtectedBranch = false }) => {
1718
+ await ensureBranchSynced(cwd);
1593
1719
  const found = await vaultManager.findNote(id, cwd);
1594
1720
  if (!found) {
1595
1721
  return { content: [{ type: "text", text: `No memory found with id '${id}'` }], isError: true };
@@ -1656,16 +1782,30 @@ server.registerTool("update", {
1656
1782
  projectName: updated.projectName,
1657
1783
  tags: updated.tags,
1658
1784
  });
1659
- const commitStatus = await vault.git.commitWithStatus(`update: ${updated.title}`, [vaultManager.noteRelPath(vault, id)], commitBody);
1660
- const pushStatus = await pushAfterMutation(vault);
1785
+ const commitMessage = `update: ${updated.title}`;
1786
+ const commitFiles = [vaultManager.noteRelPath(vault, id)];
1787
+ const commitStatus = await vault.git.commitWithStatus(commitMessage, commitFiles, commitBody);
1788
+ const pushStatus = commitStatus.status === "committed"
1789
+ ? await pushAfterMutation(vault)
1790
+ : { status: "skipped", reason: "commit-failed" };
1791
+ const retry = buildMutationRetryContract({
1792
+ commit: commitStatus,
1793
+ commitMessage,
1794
+ commitBody,
1795
+ files: commitFiles,
1796
+ cwd,
1797
+ vault,
1798
+ mutationApplied: true,
1799
+ });
1661
1800
  const persistence = buildPersistenceStatus({
1662
1801
  storage: vault.storage,
1663
1802
  id,
1664
1803
  embedding: embeddingStatus,
1665
1804
  commit: commitStatus,
1666
1805
  push: pushStatus,
1667
- commitMessage: `update: ${updated.title}`,
1806
+ commitMessage,
1668
1807
  commitBody,
1808
+ retry,
1669
1809
  });
1670
1810
  const structuredContent = {
1671
1811
  action: "updated",
@@ -1712,6 +1852,7 @@ server.registerTool("forget", {
1712
1852
  }),
1713
1853
  outputSchema: ForgetResultSchema,
1714
1854
  }, async ({ id, cwd, allowProtectedBranch = false }) => {
1855
+ await ensureBranchSynced(cwd);
1715
1856
  const found = await vaultManager.findNote(id, cwd);
1716
1857
  if (!found) {
1717
1858
  return { content: [{ type: "text", text: `No memory found with id '${id}'` }], isError: true };
@@ -1745,6 +1886,7 @@ server.registerTool("forget", {
1745
1886
  const vaultChanges = await removeRelationshipsToNoteIds([id]);
1746
1887
  // Always include the deleted note's path (git add on a deleted file stages the removal)
1747
1888
  addVaultChange(vaultChanges, noteVault, vaultManager.noteRelPath(noteVault, id));
1889
+ let retry;
1748
1890
  for (const [v, files] of vaultChanges) {
1749
1891
  const isPrimaryVault = v === noteVault;
1750
1892
  const summary = isPrimaryVault ? `Deleted note and cleaned up ${files.length - 1} reference(s)` : "Cleaned up dangling reference";
@@ -1754,8 +1896,22 @@ server.registerTool("forget", {
1754
1896
  noteTitle: note.title,
1755
1897
  projectName: note.projectName,
1756
1898
  });
1757
- await v.git.commit(`forget: ${note.title}`, files, commitBody);
1758
- await pushAfterMutation(v);
1899
+ const commitMessage = `forget: ${note.title}`;
1900
+ const commitStatus = await v.git.commitWithStatus(commitMessage, files, commitBody);
1901
+ if (!retry) {
1902
+ retry = buildMutationRetryContract({
1903
+ commit: commitStatus,
1904
+ commitMessage,
1905
+ commitBody,
1906
+ files,
1907
+ cwd,
1908
+ vault: v,
1909
+ mutationApplied: true,
1910
+ });
1911
+ }
1912
+ if (commitStatus.status === "committed") {
1913
+ await pushAfterMutation(v);
1914
+ }
1759
1915
  }
1760
1916
  const structuredContent = {
1761
1917
  action: "forgotten",
@@ -1765,6 +1921,7 @@ server.registerTool("forget", {
1765
1921
  projectName: note.projectName,
1766
1922
  relationshipsCleaned: vaultChanges.size > 0 ? Array.from(vaultChanges.values()).reduce((sum, files) => sum + files.length - 1, 0) : 0,
1767
1923
  vaultsModified: Array.from(vaultChanges.keys()).map(v => storageLabel(v)),
1924
+ retry,
1768
1925
  };
1769
1926
  return { content: [{ type: "text", text: `Forgotten '${id}' (${note.title})` }], structuredContent };
1770
1927
  });
@@ -1794,6 +1951,7 @@ server.registerTool("get", {
1794
1951
  }),
1795
1952
  outputSchema: GetResultSchema,
1796
1953
  }, async ({ ids, cwd }) => {
1954
+ await ensureBranchSynced(cwd);
1797
1955
  const found = [];
1798
1956
  const notFound = [];
1799
1957
  for (const id of ids) {
@@ -1864,6 +2022,7 @@ server.registerTool("where_is_memory", {
1864
2022
  }),
1865
2023
  outputSchema: WhereIsResultSchema,
1866
2024
  }, async ({ id, cwd }) => {
2025
+ await ensureBranchSynced(cwd);
1867
2026
  const found = await vaultManager.findNote(id, cwd);
1868
2027
  if (!found) {
1869
2028
  return { content: [{ type: "text", text: `No memory found with id '${id}'` }], isError: true };
@@ -1937,6 +2096,7 @@ server.registerTool("list", {
1937
2096
  }),
1938
2097
  outputSchema: ListResultSchema,
1939
2098
  }, async ({ cwd, scope, storedIn, tags, includeRelations, includePreview, includeStorage, includeUpdated }) => {
2099
+ await ensureBranchSynced(cwd);
1940
2100
  const { project, entries } = await collectVisibleNotes(cwd, scope, tags, storedIn);
1941
2101
  if (entries.length === 0) {
1942
2102
  const structuredContent = { action: "listed", count: 0, scope: scope || "all", storedIn: storedIn || "any", project: project ? { id: project.id, name: project.name } : undefined, notes: [] };
@@ -2009,6 +2169,7 @@ server.registerTool("recent_memories", {
2009
2169
  }),
2010
2170
  outputSchema: RecentResultSchema,
2011
2171
  }, async ({ cwd, scope, storedIn, limit, includePreview, includeStorage }) => {
2172
+ await ensureBranchSynced(cwd);
2012
2173
  const { project, entries } = await collectVisibleNotes(cwd, scope, undefined, storedIn);
2013
2174
  const recent = [...entries]
2014
2175
  .sort((a, b) => b.note.updatedAt.localeCompare(a.note.updatedAt))
@@ -2075,6 +2236,7 @@ server.registerTool("memory_graph", {
2075
2236
  }),
2076
2237
  outputSchema: MemoryGraphResultSchema,
2077
2238
  }, async ({ cwd, scope, storedIn, limit }) => {
2239
+ await ensureBranchSynced(cwd);
2078
2240
  const { project, entries } = await collectVisibleNotes(cwd, scope, undefined, storedIn);
2079
2241
  if (entries.length === 0) {
2080
2242
  const structuredContent = { action: "graph_shown", project: project?.id, projectName: project?.name, nodes: [], limit, truncated: false };
@@ -2151,6 +2313,7 @@ server.registerTool("project_memory_summary", {
2151
2313
  }),
2152
2314
  outputSchema: ProjectSummaryResultSchema,
2153
2315
  }, async ({ cwd, maxPerTheme, recentLimit }) => {
2316
+ await ensureBranchSynced(cwd);
2154
2317
  const { project, entries } = await collectVisibleNotes(cwd, "all");
2155
2318
  if (!project) {
2156
2319
  return { content: [{ type: "text", text: `Could not detect a project for: ${cwd}` }], isError: true };
@@ -2342,6 +2505,7 @@ server.registerTool("move_memory", {
2342
2505
  }),
2343
2506
  outputSchema: MoveResultSchema,
2344
2507
  }, async ({ id, target, vaultFolder, cwd, allowProtectedBranch = false }) => {
2508
+ await ensureBranchSynced(cwd);
2345
2509
  const found = await vaultManager.findNote(id, cwd);
2346
2510
  if (!found) {
2347
2511
  return { content: [{ type: "text", text: `No memory found with id '${id}'` }], isError: true };
@@ -2432,7 +2596,7 @@ server.registerTool("move_memory", {
2432
2596
  updatedAt: new Date().toISOString(),
2433
2597
  };
2434
2598
  }
2435
- const moveResult = await moveNoteBetweenVaults(found, targetVault, noteToWrite);
2599
+ const moveResult = await moveNoteBetweenVaults(found, targetVault, noteToWrite, cwd);
2436
2600
  const movedNote = moveResult.note;
2437
2601
  const associationValue = movedNote.projectName && movedNote.project
2438
2602
  ? `${movedNote.projectName} (${movedNote.project})`
@@ -2494,6 +2658,7 @@ server.registerTool("relate", {
2494
2658
  }),
2495
2659
  outputSchema: RelateResultSchema,
2496
2660
  }, async ({ fromId, toId, type, bidirectional, cwd }) => {
2661
+ await ensureBranchSynced(cwd);
2497
2662
  const [foundFrom, foundTo] = await Promise.all([
2498
2663
  vaultManager.findNote(fromId, cwd),
2499
2664
  vaultManager.findNote(toId, cwd),
@@ -2527,6 +2692,7 @@ server.registerTool("relate", {
2527
2692
  return { content: [{ type: "text", text: `Relationship already exists between '${fromId}' and '${toId}'` }], isError: true };
2528
2693
  }
2529
2694
  const modifiedNoteIds = [];
2695
+ let retry;
2530
2696
  for (const [vault, files] of vaultChanges) {
2531
2697
  const isFromVault = vault === fromVault;
2532
2698
  const thisNote = isFromVault ? fromNote : toNote;
@@ -2541,8 +2707,22 @@ server.registerTool("relate", {
2541
2707
  type,
2542
2708
  },
2543
2709
  });
2544
- await vault.git.commit(`relate: ${fromNote.title} ↔ ${toNote.title}`, files, commitBody);
2545
- await pushAfterMutation(vault);
2710
+ const commitMessage = `relate: ${fromNote.title} ↔ ${toNote.title}`;
2711
+ const commitStatus = await vault.git.commitWithStatus(commitMessage, files, commitBody);
2712
+ if (!retry) {
2713
+ retry = buildMutationRetryContract({
2714
+ commit: commitStatus,
2715
+ commitMessage,
2716
+ commitBody,
2717
+ files,
2718
+ cwd,
2719
+ vault,
2720
+ mutationApplied: true,
2721
+ });
2722
+ }
2723
+ if (commitStatus.status === "committed") {
2724
+ await pushAfterMutation(vault);
2725
+ }
2546
2726
  modifiedNoteIds.push(...files.map(f => path.basename(f, '.md')));
2547
2727
  }
2548
2728
  const dirStr = bidirectional ? "↔" : "→";
@@ -2553,6 +2733,7 @@ server.registerTool("relate", {
2553
2733
  type,
2554
2734
  bidirectional,
2555
2735
  notesModified: modifiedNoteIds,
2736
+ retry,
2556
2737
  };
2557
2738
  return {
2558
2739
  content: [{ type: "text", text: `Linked \`${fromId}\` ${dirStr} \`${toId}\` (${type})` }],
@@ -2587,6 +2768,7 @@ server.registerTool("unrelate", {
2587
2768
  }),
2588
2769
  outputSchema: RelateResultSchema,
2589
2770
  }, async ({ fromId, toId, bidirectional, cwd }) => {
2771
+ await ensureBranchSynced(cwd);
2590
2772
  const [foundFrom, foundTo] = await Promise.all([
2591
2773
  vaultManager.findNote(fromId, cwd),
2592
2774
  vaultManager.findNote(toId, cwd),
@@ -2616,6 +2798,7 @@ server.registerTool("unrelate", {
2616
2798
  if (vaultChanges.size === 0) {
2617
2799
  return { content: [{ type: "text", text: `No relationship found between '${fromId}' and '${toId}'` }], isError: true };
2618
2800
  }
2801
+ let retry;
2619
2802
  for (const [vault, files] of vaultChanges) {
2620
2803
  const found = foundFrom?.vault === vault ? foundFrom : foundTo;
2621
2804
  const commitBody = found
@@ -2625,8 +2808,22 @@ server.registerTool("unrelate", {
2625
2808
  projectName: found.note.projectName,
2626
2809
  })
2627
2810
  : undefined;
2628
- await vault.git.commit(`unrelate: ${fromId} ↔ ${toId}`, files, commitBody);
2629
- await pushAfterMutation(vault);
2811
+ const commitMessage = `unrelate: ${fromId} ↔ ${toId}`;
2812
+ const commitStatus = await vault.git.commitWithStatus(commitMessage, files, commitBody);
2813
+ if (!retry) {
2814
+ retry = buildMutationRetryContract({
2815
+ commit: commitStatus,
2816
+ commitMessage,
2817
+ commitBody,
2818
+ files,
2819
+ cwd,
2820
+ vault,
2821
+ mutationApplied: true,
2822
+ });
2823
+ }
2824
+ if (commitStatus.status === "committed") {
2825
+ await pushAfterMutation(vault);
2826
+ }
2630
2827
  }
2631
2828
  const modifiedNoteIds = [];
2632
2829
  for (const [vault, files] of vaultChanges) {
@@ -2639,6 +2836,7 @@ server.registerTool("unrelate", {
2639
2836
  type: "related-to", // not tracked for unrelate
2640
2837
  bidirectional,
2641
2838
  notesModified: modifiedNoteIds,
2839
+ retry,
2642
2840
  };
2643
2841
  return { content: [{ type: "text", text: `Removed relationship between \`${fromId}\` and \`${toId}\`` }], structuredContent };
2644
2842
  });
@@ -2707,6 +2905,7 @@ server.registerTool("consolidate", {
2707
2905
  }),
2708
2906
  outputSchema: ConsolidateResultSchema,
2709
2907
  }, async ({ cwd, strategy, mode, threshold, mergePlan, allowProtectedBranch = false }) => {
2908
+ await ensureBranchSynced(cwd);
2710
2909
  const project = await resolveProject(cwd);
2711
2910
  if (!project && cwd) {
2712
2911
  return { content: [{ type: "text", text: `Could not detect a project for: ${cwd}` }], isError: true };
@@ -2751,21 +2950,22 @@ async function detectDuplicates(entries, threshold, project) {
2751
2950
  const checked = new Set();
2752
2951
  let foundCount = 0;
2753
2952
  const duplicates = [];
2953
+ const embeddings = await loadEmbeddingsByNoteId(entries);
2754
2954
  for (let i = 0; i < entries.length; i++) {
2755
2955
  const entryA = entries[i];
2756
2956
  if (checked.has(entryA.note.id))
2757
2957
  continue;
2758
- const embeddingA = await entryA.vault.storage.readEmbedding(entryA.note.id);
2958
+ const embeddingA = embeddings.get(entryA.note.id);
2759
2959
  if (!embeddingA)
2760
2960
  continue;
2761
2961
  for (let j = i + 1; j < entries.length; j++) {
2762
2962
  const entryB = entries[j];
2763
2963
  if (checked.has(entryB.note.id))
2764
2964
  continue;
2765
- const embeddingB = await entryB.vault.storage.readEmbedding(entryB.note.id);
2965
+ const embeddingB = embeddings.get(entryB.note.id);
2766
2966
  if (!embeddingB)
2767
2967
  continue;
2768
- const similarity = cosineSimilarity(embeddingA.embedding, embeddingB.embedding);
2968
+ const similarity = cosineSimilarity(embeddingA, embeddingB);
2769
2969
  if (similarity >= threshold) {
2770
2970
  foundCount++;
2771
2971
  lines.push(`${foundCount}. ${entryA.note.title} (${entryA.note.id})`);
@@ -2895,11 +3095,12 @@ async function suggestMerges(entries, threshold, defaultConsolidationMode, proje
2895
3095
  const checked = new Set();
2896
3096
  let suggestionCount = 0;
2897
3097
  const suggestions = [];
3098
+ const embeddings = await loadEmbeddingsByNoteId(entries);
2898
3099
  for (let i = 0; i < entries.length; i++) {
2899
3100
  const entryA = entries[i];
2900
3101
  if (checked.has(entryA.note.id))
2901
3102
  continue;
2902
- const embeddingA = await entryA.vault.storage.readEmbedding(entryA.note.id);
3103
+ const embeddingA = embeddings.get(entryA.note.id);
2903
3104
  if (!embeddingA)
2904
3105
  continue;
2905
3106
  const similar = [];
@@ -2907,10 +3108,10 @@ async function suggestMerges(entries, threshold, defaultConsolidationMode, proje
2907
3108
  const entryB = entries[j];
2908
3109
  if (checked.has(entryB.note.id))
2909
3110
  continue;
2910
- const embeddingB = await entryB.vault.storage.readEmbedding(entryB.note.id);
3111
+ const embeddingB = embeddings.get(entryB.note.id);
2911
3112
  if (!embeddingB)
2912
3113
  continue;
2913
- const similarity = cosineSimilarity(embeddingA.embedding, embeddingB.embedding);
3114
+ const similarity = cosineSimilarity(embeddingA, embeddingB);
2914
3115
  if (similarity >= threshold) {
2915
3116
  similar.push({ entry: entryB, similarity });
2916
3117
  }
@@ -2972,6 +3173,16 @@ async function suggestMerges(entries, threshold, defaultConsolidationMode, proje
2972
3173
  };
2973
3174
  return { content: [{ type: "text", text: lines.join("\n") }], structuredContent };
2974
3175
  }
3176
+ async function loadEmbeddingsByNoteId(entries) {
3177
+ const embeddings = new Map();
3178
+ await Promise.all(entries.map(async (entry) => {
3179
+ const record = await entry.vault.storage.readEmbedding(entry.note.id);
3180
+ if (record) {
3181
+ embeddings.set(entry.note.id, record.embedding);
3182
+ }
3183
+ }));
3184
+ return embeddings;
3185
+ }
2975
3186
  async function executeMerge(entries, mergePlan, defaultConsolidationMode, project, cwd, explicitMode, policy, allowProtectedBranch = false) {
2976
3187
  const sourceIds = normalizeMergePlanSourceIds(mergePlan.sourceIds);
2977
3188
  const targetTitle = mergePlan.targetTitle.trim();
@@ -3162,6 +3373,7 @@ async function executeMerge(entries, mergePlan, defaultConsolidationMode, projec
3162
3373
  let targetPushStatus = { status: "skipped", reason: "no-remote" };
3163
3374
  let targetCommitBody;
3164
3375
  let targetCommitMessage;
3376
+ let targetCommitFiles;
3165
3377
  for (const [vault, files] of vaultChanges) {
3166
3378
  const isTargetVault = vault === targetVault;
3167
3379
  // Determine action and summary based on mode
@@ -3199,14 +3411,28 @@ async function executeMerge(entries, mergePlan, defaultConsolidationMode, projec
3199
3411
  });
3200
3412
  const commitMessage = `${action}: ${targetTitle}`;
3201
3413
  const commitStatus = await vault.git.commitWithStatus(commitMessage, files, commitBody);
3202
- const pushStatus = await pushAfterMutation(vault);
3414
+ const pushStatus = commitStatus.status === "committed"
3415
+ ? await pushAfterMutation(vault)
3416
+ : { status: "skipped", reason: "commit-failed" };
3203
3417
  if (isTargetVault) {
3204
3418
  targetCommitStatus = commitStatus;
3205
3419
  targetPushStatus = pushStatus;
3206
3420
  targetCommitBody = commitBody;
3207
3421
  targetCommitMessage = commitMessage;
3422
+ targetCommitFiles = [...files];
3208
3423
  }
3209
3424
  }
3425
+ const retry = targetCommitMessage && targetCommitFiles
3426
+ ? buildMutationRetryContract({
3427
+ commit: targetCommitStatus,
3428
+ commitMessage: targetCommitMessage,
3429
+ commitBody: targetCommitBody,
3430
+ files: targetCommitFiles,
3431
+ cwd,
3432
+ vault: targetVault,
3433
+ mutationApplied: true,
3434
+ })
3435
+ : undefined;
3210
3436
  const persistence = buildPersistenceStatus({
3211
3437
  storage: targetVault.storage,
3212
3438
  id: targetId,
@@ -3215,6 +3441,7 @@ async function executeMerge(entries, mergePlan, defaultConsolidationMode, projec
3215
3441
  push: targetPushStatus,
3216
3442
  commitMessage: targetCommitMessage,
3217
3443
  commitBody: targetCommitBody,
3444
+ retry,
3218
3445
  });
3219
3446
  const lines = [];
3220
3447
  lines.push(`Consolidated ${sourceIds.length} notes into '${targetId}'`);
@@ -3245,6 +3472,7 @@ async function executeMerge(entries, mergePlan, defaultConsolidationMode, projec
3245
3472
  notesProcessed: entries.length,
3246
3473
  notesModified: vaultChanges.size,
3247
3474
  persistence,
3475
+ retry,
3248
3476
  };
3249
3477
  return { content: [{ type: "text", text: lines.join("\n") }], structuredContent };
3250
3478
  }
@@ -3370,14 +3598,29 @@ async function pruneSuperseded(entries, consolidationMode, project, cwd, policy,
3370
3598
  }
3371
3599
  }
3372
3600
  // Commit changes per vault
3601
+ let retry;
3373
3602
  for (const [vault, files] of vaultChanges) {
3374
3603
  const prunedIds = files.map((f) => f.replace(/\.mnemonic\/notes\/(.+)\.md$/, "$1").replace(/notes\/(.+)\.md$/, "$1"));
3375
3604
  const commitBody = formatCommitBody({
3376
3605
  noteIds: prunedIds,
3377
3606
  description: `Pruned ${prunedIds.length} superseded note(s)\nNotes: ${prunedIds.join(", ")}`,
3378
3607
  });
3379
- await vault.git.commit(`prune: removed ${files.length} superseded note(s)`, files, commitBody);
3380
- await pushAfterMutation(vault);
3608
+ const commitMessage = `prune: removed ${files.length} superseded note(s)`;
3609
+ const commitStatus = await vault.git.commitWithStatus(commitMessage, files, commitBody);
3610
+ if (!retry) {
3611
+ retry = buildMutationRetryContract({
3612
+ commit: commitStatus,
3613
+ commitMessage,
3614
+ commitBody,
3615
+ files,
3616
+ cwd,
3617
+ vault,
3618
+ mutationApplied: true,
3619
+ });
3620
+ }
3621
+ if (commitStatus.status === "committed") {
3622
+ await pushAfterMutation(vault);
3623
+ }
3381
3624
  }
3382
3625
  lines.push("");
3383
3626
  lines.push(`Pruned ${supersededIds.size} note(s).`);
@@ -3388,6 +3631,7 @@ async function pruneSuperseded(entries, consolidationMode, project, cwd, policy,
3388
3631
  projectName: project?.name,
3389
3632
  notesProcessed: entries.length,
3390
3633
  notesModified: vaultChanges.size,
3634
+ retry,
3391
3635
  };
3392
3636
  return { content: [{ type: "text", text: lines.join("\n") }], structuredContent };
3393
3637
  }