@danielmarbach/mnemonic-mcp 0.18.0 → 0.19.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,6 +4,19 @@ 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.19.0] - 2026-03-28
8
+
9
+ ### Added
10
+
11
+ - Role and importance inference (`src/role-suggestions.ts`): notes are automatically assigned a suggested `role` and `importance` from structural signals; inference is language-independent and never overwrites explicit frontmatter.
12
+ - `alwaysLoad: true` in note frontmatter marks a note as an explicit session anchor with highest recall and relationship-expansion priority.
13
+ - `recall` and relationship expansion now factor in effective role and importance, so summary and decision notes surface higher without manual tagging.
14
+ - Temporal recall history entries now include a `changeDescription` and notes include a `historySummary` capturing the overall evolution pattern; classification uses structural signals only and degrades gracefully when stats are unavailable.
15
+
16
+ ### Changed
17
+
18
+ - The workflow hint and user-facing docs now state that roles are optional prioritization hints, inferred roles stay internal-only, lifecycle remains the durability control, and default prioritization is language-independent.
19
+
7
20
  ## [0.18.0] - 2026-03-27
8
21
 
9
22
  ### Added
package/README.md CHANGED
@@ -310,6 +310,16 @@ Project identity derives from the **git remote URL**, normalized to a stable slu
310
310
 
311
311
  Temporal recall is opt-in via `mode: "temporal"`. It keeps semantic selection first, then enriches only the top matches with compact git-backed history so agents can inspect how a note evolved without turning recall into raw log or diff output.
312
312
 
313
+ **What temporal mode shows:**
314
+
315
+ - **Per-change descriptions** (`changeDescription`): human-readable summaries like "Expanded the note with additional detail" or "Minor refinement to existing content."
316
+ - **Note-level history summaries** (`historySummary`): overall patterns like "The core decision remained stable while rationale and examples expanded." or "The note was connected to related work through incremental updates."
317
+ - **Semantic change categories**: create, refine, expand, clarify, connect, restructure, reverse, unknown
318
+
319
+ **How it works:**
320
+
321
+ mnemonic interprets change semantically using structural and statistical signals (size ratios, heading changes, section movements) rather than language-dependent analysis. Raw diffs are intentionally NOT part of default temporal output—you get interpretive summaries that explain what kind of change happened, not patch noise.
322
+
313
323
  Use `verbose: true` together with temporal mode when you want richer change stats such as additions, deletions, files changed, and change classification. Those stats describe the whole commit that touched the note, not a raw diff excerpt, so recall stays bounded and does not return full diffs.
314
324
 
315
325
  The `scope` parameter on `recall` narrows results:
@@ -325,6 +335,14 @@ Each note carries a `lifecycle`:
325
335
  - `"permanent"` *(default)* — durable knowledge for future sessions
326
336
  - `"temporary"` — working-state scaffolding (plans, WIP checkpoints) that can be cleaned up once consolidated
327
337
 
338
+ ### Roles and lifecycle
339
+
340
+ Roles are optional prioritization hints, not required schema. mnemonic infers a `role` and `importance` from structural signals (heading count, bullet density, inbound references, relationship types) — inference is language-independent and never overwrites explicit frontmatter. Valid roles: `summary`, `decision`, `plan`, `log`, `reference`. Valid importance values: `high`, `normal`.
341
+
342
+ Set `alwaysLoad: true` in a note's frontmatter to mark it as an explicit session anchor; it receives the highest recall and relationship-expansion priority regardless of inferred role.
343
+
344
+ mnemonic works without roles. Inferred roles stay internal-only, prioritization is language-independent by default, and lifecycle remains the separate durability axis. A note with `role: plan` can still be either `temporary` or `permanent`.
345
+
328
346
  ### Note format
329
347
 
330
348
  Notes are standard markdown with YAML frontmatter:
@@ -422,7 +440,7 @@ Imported notes are written to the main vault with `lifecycle: permanent` and `sc
422
440
 
423
441
  | Prompt | Description |
424
442
  |--------|-------------|
425
- | `mnemonic-workflow-hint` | Optional. Returns an imperative decision protocol for weaker and stronger models: use `recall` or `list` first, inspect with `get`, update existing memories, remember only when nothing matches, then organize with `relate`, `consolidate`, or `move_memory`. Not auto-injected request it on demand. |
443
+ | `mnemonic-workflow-hint` | Optional. Returns a compact decision protocol: use `recall` or `list` first, inspect with `get`, update existing memories, remember only when nothing matches, then organize with `relate`, `consolidate`, or `move_memory`. Also reminds models that roles are optional prioritization hints, inferred roles are internal-only, prioritization is language-independent by default, and lifecycle stays separate. |
426
444
 
427
445
  ## Tools
428
446
 
@@ -556,6 +574,8 @@ mnemonic and Beads address complementary concerns. mnemonic is a **knowledge gra
556
574
 
557
575
  mnemonic distinguishes between two lifecycle states. `temporary` notes capture evolving working-state: hypotheses, in-progress plans, experiment results, draft reasoning. `permanent` notes capture durable knowledge: decisions, root cause explanations, architectural guidance, lessons learned. As an investigation progresses, a cluster of temporary notes is typically `consolidate`d into one or more permanent notes, and the scaffolding is discarded. This two-phase lifecycle keeps exploratory thinking from polluting long-term memory while still giving agents a place to reason incrementally before committing to a conclusion.
558
576
 
577
+ Roles, when present, are separate from lifecycle: they help prioritization and retrieval, not retention policy. mnemonic still works without roles, and any inferred role metadata remains an internal hint rather than part of the user-facing note contract.
578
+
559
579
  ## Contributing
560
580
 
561
581
  See [`CONTRIBUTING.md`](CONTRIBUTING.md) for development setup, dogfooding workflow, testing requirements, and pull request guidelines.
package/build/index.js CHANGED
@@ -8,16 +8,18 @@ import { promises as fs } from "fs";
8
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
+ import { enrichTemporalHistory } from "./temporal-interpretation.js";
11
12
  import { getOrBuildProjection } from "./projections.js";
12
13
  import { invalidateActiveProjectCache, getOrBuildVaultEmbeddings, getOrBuildVaultNoteList, getSessionCachedNote, } from "./cache.js";
13
14
  import { performance } from "perf_hooks";
14
15
  import { filterRelationships, mergeRelationshipsFromNotes, normalizeMergePlanSourceIds, resolveEffectiveConsolidationMode, } from "./consolidate.js";
15
- import { selectRecallResults } from "./recall.js";
16
+ import { computeRecallMetadataBoost, selectRecallResults } from "./recall.js";
16
17
  import { getRelationshipPreview } from "./relationships.js";
17
18
  import { cleanMarkdown } from "./markdown.js";
18
19
  import { MnemonicConfigStore, readVaultSchemaVersion } from "./config.js";
19
20
  import { CONSOLIDATION_MODES, PROTECTED_BRANCH_BEHAVIORS, PROJECT_POLICY_SCOPES, WRITE_SCOPES, isProtectedBranch, resolveProtectedBranchBehavior, resolveProtectedBranchPatterns, resolveConsolidationMode, resolveWriteScope, } from "./project-memory-policy.js";
20
- import { classifyTheme, classifyThemeWithGraduation, computeThemesWithGraduation, summarizePreview, titleCaseTheme, withinThemeScore, anchorScore, buildThemeCache, computeConnectionDiversity, } from "./project-introspection.js";
21
+ import { classifyTheme, classifyThemeWithGraduation, computeThemesWithGraduation, summarizePreview, titleCaseTheme, withinThemeScore, anchorScore, computeConnectionDiversity, } from "./project-introspection.js";
22
+ import { getEffectiveMetadata } from "./role-suggestions.js";
21
23
  import { detectProject, getCurrentGitBranch, resolveProjectIdentity } from "./project.js";
22
24
  import { VaultManager } from "./vault.js";
23
25
  import { checkBranchChange } from "./branch-tracker.js";
@@ -405,7 +407,8 @@ function formatTemporalHistory(history) {
405
407
  const lines = ["**history:**"];
406
408
  for (const entry of history) {
407
409
  const summary = entry.summary ? ` — ${entry.summary}` : "";
408
- lines.push(`- \`${entry.commitHash.slice(0, 7)}\` ${entry.timestamp} ${entry.message}${summary}`);
410
+ const changeDesc = entry.changeDescription ? ` (${entry.changeDescription})` : "";
411
+ lines.push(`- \`${entry.commitHash.slice(0, 7)}\` ${entry.timestamp} — ${entry.message}${summary}${changeDesc}`);
409
412
  }
410
413
  return lines.join("\n");
411
414
  }
@@ -1817,7 +1820,8 @@ server.registerTool("recall", {
1817
1820
  if (isProjectNote)
1818
1821
  continue;
1819
1822
  }
1820
- const boost = isCurrentProject ? 0.15 : 0;
1823
+ const metadataBoost = computeRecallMetadataBoost(getEffectiveMetadata(note));
1824
+ const boost = (isCurrentProject ? 0.15 : 0) + metadataBoost;
1821
1825
  scored.push({ id: rec.id, score: rawScore, boosted: rawScore + boost, vault, isCurrentProject: Boolean(isCurrentProject) });
1822
1826
  }
1823
1827
  }
@@ -1842,13 +1846,17 @@ server.registerTool("recall", {
1842
1846
  const provenance = await getNoteProvenance(vault.git, filePath);
1843
1847
  const confidence = computeConfidence(note.lifecycle, note.updatedAt, centrality);
1844
1848
  let history;
1849
+ let historySummary;
1845
1850
  if (mode === "temporal") {
1846
1851
  if (index < TEMPORAL_HISTORY_NOTE_LIMIT) {
1847
1852
  const commits = await vault.git.getFileHistory(filePath, TEMPORAL_HISTORY_COMMIT_LIMIT);
1848
- history = await Promise.all(commits.map(async (commit) => {
1853
+ const rawHistory = await Promise.all(commits.map(async (commit) => {
1849
1854
  const stats = await vault.git.getCommitStats(filePath, commit.hash);
1850
1855
  return buildTemporalHistoryEntry(commit, stats, verbose);
1851
1856
  }));
1857
+ const enriched = enrichTemporalHistory(rawHistory);
1858
+ history = enriched.interpretedHistory;
1859
+ historySummary = enriched.historySummary;
1852
1860
  }
1853
1861
  }
1854
1862
  // Add relationship preview for top N results (fail-soft)
@@ -1878,6 +1886,7 @@ server.registerTool("recall", {
1878
1886
  provenance,
1879
1887
  confidence,
1880
1888
  history,
1889
+ historySummary,
1881
1890
  relationships,
1882
1891
  });
1883
1892
  }
@@ -2839,12 +2848,38 @@ server.registerTool("project_memory_summary", {
2839
2848
  return { content: [{ type: "text", text: `No memories found for project ${project.name}.` }], structuredContent };
2840
2849
  }
2841
2850
  const policyLine = await formatProjectPolicyLine(project.id);
2842
- // Build theme cache for connection diversity scoring (project-scoped only)
2843
- // This uses simple classifyTheme for consistent diversity calculations
2844
- const themeCache = buildThemeCache(projectEntries.map(e => e.note));
2851
+ const projectNoteIds = new Set(projectEntries.map(e => e.note.id));
2845
2852
  // Compute promoted themes from keywords (graduation system)
2846
2853
  const graduationResult = computeThemesWithGraduation(projectEntries.map(e => e.note));
2847
2854
  const promotedThemes = new Set(graduationResult.promotedThemes);
2855
+ const themeCache = graduationResult.themeAssignments;
2856
+ const inboundReferences = new Map();
2857
+ const linkedByPermanentNotes = new Map();
2858
+ for (const entry of projectEntries) {
2859
+ for (const rel of entry.note.relatedTo ?? []) {
2860
+ if (!projectNoteIds.has(rel.id)) {
2861
+ continue;
2862
+ }
2863
+ inboundReferences.set(rel.id, (inboundReferences.get(rel.id) ?? 0) + 1);
2864
+ if (entry.note.lifecycle === "permanent") {
2865
+ linkedByPermanentNotes.set(rel.id, (linkedByPermanentNotes.get(rel.id) ?? 0) + 1);
2866
+ }
2867
+ }
2868
+ }
2869
+ const effectiveMetadataById = new Map(projectEntries.map((entry) => {
2870
+ const inbound = inboundReferences.get(entry.note.id) ?? 0;
2871
+ const visibleOutbound = (entry.note.relatedTo ?? []).filter((rel) => projectNoteIds.has(rel.id)).length;
2872
+ const metadata = getEffectiveMetadata(entry.note, {
2873
+ inboundReferences: inbound,
2874
+ linkedByPermanentNotes: linkedByPermanentNotes.get(entry.note.id) ?? 0,
2875
+ anchorCandidate: entry.note.lifecycle === "permanent" && (visibleOutbound > 0 || inbound > 0),
2876
+ });
2877
+ return [entry.note.id, {
2878
+ metadata,
2879
+ inbound,
2880
+ visibleOutbound,
2881
+ }];
2882
+ }));
2848
2883
  // Categorize by theme with graduation (project-scoped only)
2849
2884
  const themed = new Map();
2850
2885
  for (const entry of projectEntries) {
@@ -2877,7 +2912,7 @@ server.registerTool("project_memory_summary", {
2877
2912
  if (!bucket || bucket.length === 0)
2878
2913
  continue;
2879
2914
  // Sort by within-theme score
2880
- const sorted = [...bucket].sort((a, b) => withinThemeScore(b.note) - withinThemeScore(a.note));
2915
+ const sorted = [...bucket].sort((a, b) => withinThemeScore(b.note, effectiveMetadataById.get(b.note.id)?.metadata) - withinThemeScore(a.note, effectiveMetadataById.get(a.note.id)?.metadata));
2881
2916
  const top = sorted.slice(0, maxPerTheme);
2882
2917
  sections.push(`\n${titleCaseTheme(theme)}:`);
2883
2918
  sections.push(...top.map(e => `- ${e.note.title} (\`${e.note.id}\`)`));
@@ -2897,49 +2932,32 @@ server.registerTool("project_memory_summary", {
2897
2932
  sections.push(`\nRecent activity (start here):`);
2898
2933
  sections.push(...recent.map(e => `- ${e.note.updatedAt} — ${e.note.title}`));
2899
2934
  // Anchor notes with diversity constraint (project-scoped only)
2900
- // Separate tagged anchors (can have no relationships) from scored anchors (need relationships)
2901
- const taggedAnchorEntries = projectEntries.filter(e => e.note.lifecycle === "permanent" &&
2902
- e.note.tags.some(t => t.toLowerCase() === "anchor" || t.toLowerCase() === "alwaysload"));
2903
2935
  const scoredAnchorCandidates = projectEntries
2904
- .filter(e => e.note.lifecycle === "permanent" && (e.note.relatedTo?.length ?? 0) > 0)
2905
- .map(e => ({
2906
- entry: e,
2907
- score: anchorScore(e.note, themeCache),
2908
- theme: classifyTheme(e.note),
2909
- }))
2910
- .filter(x => x.score > -Infinity)
2911
- .sort((a, b) => b.score - a.score);
2912
- // Score tagged anchors too, so they can compete for primaryEntry
2913
- const scoredTaggedAnchors = taggedAnchorEntries
2914
- .map(e => ({
2915
- entry: e,
2916
- score: anchorScore(e.note, themeCache),
2917
- theme: classifyTheme(e.note),
2918
- }))
2919
- .sort((a, b) => b.score - a.score);
2936
+ .map(e => {
2937
+ const baselineContext = effectiveMetadataById.get(e.note.id);
2938
+ const metadata = baselineContext?.metadata;
2939
+ return {
2940
+ entry: e,
2941
+ metadata,
2942
+ score: anchorScore(e.note, themeCache, metadata),
2943
+ theme: themeCache.get(e.note.id) ?? "other",
2944
+ alwaysLoad: metadata?.alwaysLoad === true,
2945
+ explicitOrientationRole: metadata?.roleSource === "explicit" &&
2946
+ (metadata.role === "summary" || metadata.role === "decision"),
2947
+ hasVisibleGraphParticipation: (baselineContext?.visibleOutbound ?? 0) > 0 || (baselineContext?.inbound ?? 0) > 0,
2948
+ };
2949
+ })
2950
+ .filter(candidate => candidate.score > -Infinity)
2951
+ .filter(candidate => candidate.alwaysLoad || candidate.explicitOrientationRole || candidate.hasVisibleGraphParticipation)
2952
+ .sort((a, b) => b.score - a.score || a.entry.note.title.localeCompare(b.entry.note.title));
2920
2953
  // Enforce max 2 per theme for scored anchors
2921
2954
  const anchorThemeCounts = new Map();
2922
2955
  const anchors = [];
2923
2956
  const anchorIds = new Set();
2924
- // Add tagged anchors first (capped at 10 total across all themes), scored by anchorScore
2925
- for (const candidate of scoredTaggedAnchors.slice(0, 10)) {
2926
- if (anchors.length >= 10)
2927
- break;
2928
- anchors.push({
2929
- id: candidate.entry.note.id,
2930
- title: candidate.entry.note.title,
2931
- centrality: candidate.entry.note.relatedTo?.length ?? 0,
2932
- connectionDiversity: computeConnectionDiversity(candidate.entry.note, themeCache),
2933
- updatedAt: candidate.entry.note.updatedAt,
2934
- });
2935
- anchorIds.add(candidate.entry.note.id);
2936
- }
2937
2957
  // Add scored anchors with theme diversity constraint
2938
2958
  for (const candidate of scoredAnchorCandidates) {
2939
2959
  if (anchors.length >= 10)
2940
2960
  break;
2941
- if (anchorIds.has(candidate.entry.note.id))
2942
- continue;
2943
2961
  const theme = candidate.theme;
2944
2962
  const themeCount = anchorThemeCounts.get(theme) ?? 0;
2945
2963
  if (themeCount >= 2)
@@ -4597,18 +4615,13 @@ server.registerPrompt("mnemonic-workflow-hint", {
4597
4615
  type: "text",
4598
4616
  text: "## Mnemonic MCP workflow hints\n\n" +
4599
4617
  "Avoid duplicate memories. Prefer inspecting and updating existing memories before creating new ones.\n\n" +
4600
- "### Hard rules\n\n" +
4601
4618
  "- REQUIRES: Before `remember`, call `recall` or `list` first.\n" +
4602
4619
  "- If `recall` or `list` returns a plausible match, call `get` before deciding whether to `update` or `remember`.\n" +
4603
4620
  "- If an existing memory already covers the topic, use `update`, not `remember`.\n" +
4604
4621
  "- When unsure, prefer `recall` over `remember`.\n" +
4605
4622
  "- For repo-related tasks, pass `cwd` so mnemonic can route project memories correctly.\n\n" +
4606
- "### Decision protocol\n\n" +
4607
- "1. Need to store, refine, or connect knowledge about a topic? Start with `recall`, or use `list` when you need deterministic browsing by scope, storage, or tags.\n" +
4608
- "2. If `recall` or `list` returns matching ids, use `get` to inspect the best match.\n" +
4609
- "3. If one memory should be refined, call `update`.\n" +
4610
- "4. If no memory covers the topic and tag choice is ambiguous, call `discover_tags` with note context, then call `remember`.\n" +
4611
- "5. After storing or updating, use `relate` for strong connections, `consolidate` for overlap, and `move_memory` for wrong storage location.\n\n" +
4623
+ "Workflow: `recall`/`list` -> `get` -> `update` or `remember` -> `relate`/`consolidate`/`move_memory`. Use `discover_tags` only when tag choice is ambiguous.\n\n" +
4624
+ "Roles are optional prioritization hints, not schema. Lifecycle still governs durability. `role: plan` does not imply `temporary`. Inferred roles are internal hints only. Prioritization is language-independent by default.\n\n" +
4612
4625
  "### Anti-patterns\n\n" +
4613
4626
  "- Bad: call `remember` immediately because the user said 'remember'.\n" +
4614
4627
  "- Good: `recall` or `list` first, then `get`, then `update` or `remember`.\n" +