@danielmarbach/mnemonic-mcp 0.26.0 → 0.26.2

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
@@ -6,6 +6,21 @@ The format is loosely based on Keep a Changelog and uses semver-style version he
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.26.2] - 2026-04-26
10
+
11
+ ### Fixed
12
+
13
+ - `update` skips write/commit/embed when no fields actually changed.
14
+ - `update` `fieldsModified` tracks actual value differences, not parameter presence.
15
+ - Metadata-only updates no longer trigger re-embedding.
16
+
17
+ ## [0.26.1] - 2026-04-26
18
+
19
+ ### Changed
20
+
21
+ - Improved `heading` selector error message to suggest `headingStartsWith` for prefix matching when exact headings are uncertain.
22
+ - `mnemonic-rpi-workflow` skill: added explicit handoff checkpoints between Research→Plan and Plan→Implement to prevent proceeding without user confirmation at natural workflow boundaries.
23
+
9
24
  ## [0.26.0] - 2026-04-25
10
25
 
11
26
  ### Added
package/build/index.js CHANGED
@@ -19,6 +19,7 @@ import { shouldTriggerLexicalRescue, prepareTfIdfCorpusFromTokenizedDocuments, r
19
19
  import { getRelationshipPreview } from "./relationships.js";
20
20
  import { MarkdownLintError, cleanMarkdown } from "./markdown.js";
21
21
  import { applySemanticPatches } from "./semantic-patch.js";
22
+ import { hasActualChanges, computeFieldsModified } from "./update-detect-changes.js";
22
23
  import { MnemonicConfigStore, readVaultSchemaVersion } from "./config.js";
23
24
  import { CONSOLIDATION_MODES, PROTECTED_BRANCH_BEHAVIORS, PROJECT_POLICY_SCOPES, WRITE_SCOPES, isProtectedBranch, resolveProtectedBranchBehavior, resolveProtectedBranchPatterns, resolveConsolidationMode, resolveWriteScope, } from "./project-memory-policy.js";
24
25
  import { classifyTheme, classifyThemeWithGraduation, computeThemesWithGraduation, summarizePreview, titleCaseTheme, daysSinceUpdate, withinThemeScore, anchorScore, computeConnectionDiversity, workingStateScore, extractNextAction, } from "./project-introspection.js";
@@ -2316,64 +2317,136 @@ server.registerTool("update", {
2316
2317
  }
2317
2318
  }
2318
2319
  const cleanedContent = content === undefined ? undefined : await cleanMarkdown(content);
2319
- const updated = {
2320
- ...note,
2321
- title: title ?? note.title,
2322
- content: patchedContent ?? cleanedContent ?? note.content,
2323
- tags: tags ?? note.tags,
2324
- lifecycle: lifecycle ?? note.lifecycle,
2325
- ...(role !== undefined ? { role } : (note.role ? { role: note.role } : {})),
2326
- alwaysLoad: alwaysLoad !== undefined ? alwaysLoad : note.alwaysLoad,
2327
- updatedAt: now,
2328
- };
2329
- if (updated.project) {
2330
- const accessCandidates = getRecentSessionNoteAccesses(updated.project)
2320
+ const resolvedTitle = title ?? note.title;
2321
+ const resolvedContent = patchedContent ?? cleanedContent ?? note.content;
2322
+ const resolvedTags = tags ?? note.tags;
2323
+ const resolvedLifecycle = lifecycle ?? note.lifecycle;
2324
+ const resolvedRole = role !== undefined ? role : (note.role ? note.role : undefined);
2325
+ const resolvedAlwaysLoad = alwaysLoad !== undefined ? alwaysLoad : note.alwaysLoad;
2326
+ let relatedToChanged = false;
2327
+ let resolvedRelatedTo = note.relatedTo;
2328
+ if (note.project) {
2329
+ const accessCandidates = getRecentSessionNoteAccesses(note.project)
2331
2330
  .map((entry) => {
2332
- const cachedNote = getSessionCachedNote(updated.project, entry.vaultPath, entry.noteId)
2333
- ?? getRecentSessionAccessNote(updated.project, entry.vaultPath, entry.noteId);
2331
+ const cachedNote = getSessionCachedNote(note.project, entry.vaultPath, entry.noteId)
2332
+ ?? getRecentSessionAccessNote(note.project, entry.vaultPath, entry.noteId);
2334
2333
  return cachedNote
2335
2334
  ? { note: cachedNote, accessedAt: entry.accessedAt, accessKind: entry.accessKind, score: entry.score }
2336
2335
  : null;
2337
2336
  })
2338
2337
  .filter((entry) => entry !== null);
2339
- const autoRelationships = suggestAutoRelationships(updated, accessCandidates);
2338
+ const autoRelationships = suggestAutoRelationships({
2339
+ ...note,
2340
+ title: resolvedTitle,
2341
+ content: resolvedContent,
2342
+ tags: resolvedTags,
2343
+ lifecycle: resolvedLifecycle,
2344
+ alwaysLoad: resolvedAlwaysLoad,
2345
+ }, accessCandidates);
2340
2346
  if (autoRelationships.length > 0) {
2341
- const existing = [...(updated.relatedTo ?? [])];
2347
+ const existing = [...(note.relatedTo ?? [])];
2342
2348
  for (const relationship of autoRelationships) {
2343
2349
  if (!existing.some((rel) => rel.id === relationship.id && rel.type === relationship.type)) {
2344
2350
  existing.push(relationship);
2345
2351
  }
2346
2352
  }
2347
- updated.relatedTo = existing;
2348
- }
2353
+ resolvedRelatedTo = existing;
2354
+ relatedToChanged = true;
2355
+ }
2356
+ }
2357
+ const changes = computeFieldsModified({
2358
+ patchedContent,
2359
+ originalContent: note.content,
2360
+ contentExplicitlyProvided: content !== undefined,
2361
+ semanticPatchProvided: semanticPatch !== undefined && semanticPatch.length > 0,
2362
+ newTitle: resolvedTitle,
2363
+ originalTitle: note.title,
2364
+ titleExplicitlyProvided: title !== undefined,
2365
+ newLifecycle: resolvedLifecycle,
2366
+ originalLifecycle: note.lifecycle,
2367
+ lifecycleExplicitlyProvided: lifecycle !== undefined,
2368
+ newRole: resolvedRole,
2369
+ originalRole: note.role,
2370
+ roleExplicitlySet: role !== undefined,
2371
+ newTags: resolvedTags,
2372
+ originalTags: note.tags,
2373
+ tagsExplicitlyProvided: tags !== undefined,
2374
+ newAlwaysLoad: resolvedAlwaysLoad,
2375
+ originalAlwaysLoad: note.alwaysLoad,
2376
+ alwaysLoadExplicitlyProvided: alwaysLoad !== undefined,
2377
+ relatedToChanged,
2378
+ });
2379
+ const hasChanges = hasActualChanges({
2380
+ content: cleanedContent,
2381
+ originalContent: note.content,
2382
+ title,
2383
+ originalTitle: note.title,
2384
+ tags,
2385
+ originalTags: note.tags,
2386
+ lifecycle,
2387
+ originalLifecycle: note.lifecycle,
2388
+ role,
2389
+ originalRole: note.role,
2390
+ roleExplicitlySet: role !== undefined,
2391
+ alwaysLoad,
2392
+ originalAlwaysLoad: note.alwaysLoad,
2393
+ semanticPatchApplied: semanticPatch !== undefined && semanticPatch.length > 0,
2394
+ relatedToChanged,
2395
+ });
2396
+ if (!hasChanges) {
2397
+ const noOpPersistence = {
2398
+ notePath: vault.storage.notePath(id),
2399
+ embeddingPath: vault.storage.embeddingPath(id),
2400
+ embedding: { status: "skipped", model: embedModel, reason: "no-changes" },
2401
+ git: {
2402
+ commit: "skipped",
2403
+ push: "skipped",
2404
+ commitReason: "no-changes",
2405
+ pushReason: "no-changes",
2406
+ },
2407
+ durability: "local-only",
2408
+ };
2409
+ return {
2410
+ content: [{ type: "text", text: `No changes to memory '${id}'` }],
2411
+ structuredContent: {
2412
+ action: "updated",
2413
+ id,
2414
+ title: note.title,
2415
+ fieldsModified: [],
2416
+ timestamp: note.updatedAt,
2417
+ project: noteProjectRef(note),
2418
+ lifecycle: note.lifecycle,
2419
+ role: note.role,
2420
+ persistence: noOpPersistence,
2421
+ },
2422
+ };
2349
2423
  }
2424
+ const updated = {
2425
+ ...note,
2426
+ title: resolvedTitle,
2427
+ content: resolvedContent,
2428
+ tags: resolvedTags,
2429
+ lifecycle: resolvedLifecycle,
2430
+ ...(role !== undefined ? { role: resolvedRole } : (note.role ? { role: note.role } : {})),
2431
+ alwaysLoad: resolvedAlwaysLoad,
2432
+ updatedAt: now,
2433
+ relatedTo: resolvedRelatedTo,
2434
+ };
2350
2435
  await vault.storage.writeNote(updated);
2351
- let embeddingStatus = { status: "written" };
2352
- try {
2353
- const text = await embedTextForNote(vault.storage, updated);
2354
- const vector = await embed(text);
2355
- await vault.storage.writeEmbedding({ id, model: embedModel, embedding: vector, updatedAt: now });
2436
+ const shouldReembed = patchedContent !== undefined || cleanedContent !== undefined;
2437
+ let embeddingStatus = { status: "skipped", reason: shouldReembed ? undefined : "metadata-only" };
2438
+ if (shouldReembed) {
2439
+ try {
2440
+ const text = await embedTextForNote(vault.storage, updated);
2441
+ const vector = await embed(text);
2442
+ await vault.storage.writeEmbedding({ id, model: embedModel, embedding: vector, updatedAt: now });
2443
+ embeddingStatus = { status: "written" };
2444
+ }
2445
+ catch (err) {
2446
+ embeddingStatus = { status: "skipped", reason: err instanceof Error ? err.message : String(err) };
2447
+ console.error(`[embedding] Re-embed failed for '${id}': ${err}`);
2448
+ }
2356
2449
  }
2357
- catch (err) {
2358
- embeddingStatus = { status: "skipped", reason: err instanceof Error ? err.message : String(err) };
2359
- console.error(`[embedding] Re-embed failed for '${id}': ${err}`);
2360
- }
2361
- // Build change summary (LLM-provided or auto-generated)
2362
- const changes = [];
2363
- if (title !== undefined && title !== note.title)
2364
- changes.push("title");
2365
- if (content !== undefined)
2366
- changes.push("content");
2367
- if (semanticPatch !== undefined)
2368
- changes.push("semanticPatch");
2369
- if (tags !== undefined)
2370
- changes.push("tags");
2371
- if (lifecycle !== undefined && lifecycle !== note.lifecycle)
2372
- changes.push("lifecycle");
2373
- if (role !== undefined && role !== note.role)
2374
- changes.push("role");
2375
- if (alwaysLoad !== undefined && alwaysLoad !== note.alwaysLoad)
2376
- changes.push("alwaysLoad");
2377
2450
  const changeDesc = changes.length > 0 ? `Updated ${changes.join(", ")}` : "No changes";
2378
2451
  const commitSummary = summary ?? changeDesc;
2379
2452
  const commitBody = formatCommitBody({