@danielmarbach/mnemonic-mcp 0.18.0 → 0.19.1
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 +23 -0
- package/README.md +21 -1
- package/build/index.js +98 -53
- package/build/index.js.map +1 -1
- package/build/project-introspection.d.ts +5 -2
- package/build/project-introspection.d.ts.map +1 -1
- package/build/project-introspection.js +33 -4
- package/build/project-introspection.js.map +1 -1
- package/build/projections.d.ts +4 -1
- package/build/projections.d.ts.map +1 -1
- package/build/projections.js +14 -2
- package/build/projections.js.map +1 -1
- package/build/provenance.d.ts +1 -0
- package/build/provenance.d.ts.map +1 -1
- package/build/provenance.js.map +1 -1
- package/build/recall.d.ts +2 -0
- package/build/recall.d.ts.map +1 -1
- package/build/recall.js +23 -0
- package/build/recall.js.map +1 -1
- package/build/relationships.d.ts +3 -2
- package/build/relationships.d.ts.map +1 -1
- package/build/relationships.js +46 -5
- package/build/relationships.js.map +1 -1
- package/build/role-suggestions.d.ts +20 -0
- package/build/role-suggestions.d.ts.map +1 -0
- package/build/role-suggestions.js +125 -0
- package/build/role-suggestions.js.map +1 -0
- package/build/storage.d.ts +7 -0
- package/build/storage.d.ts.map +1 -1
- package/build/storage.js +29 -0
- package/build/storage.js.map +1 -1
- package/build/structured-content.d.ts +15 -0
- package/build/structured-content.d.ts.map +1 -1
- package/build/structured-content.js +3 -0
- package/build/structured-content.js.map +1 -1
- package/build/temporal-interpretation.d.ts +40 -0
- package/build/temporal-interpretation.d.ts.map +1 -0
- package/build/temporal-interpretation.js +192 -0
- package/build/temporal-interpretation.js.map +1 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,29 @@ 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.1] - 2026-03-28
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- Thin dynamic theme buckets (fewer than 2 notes) are now collapsed into "other" in `project_memory_summary`, reducing theme noise without affecting fixed themes.
|
|
12
|
+
- `suggestedNext` in `project_memory_summary` now prefers theme-diverse anchors, avoiding suggestions that repeat the primary entry's theme.
|
|
13
|
+
- `changeDescription` for `unknown`-category history entries now uses stats to produce "Substantially updated the note." or "Minor update to the note." instead of always "Updated the note."
|
|
14
|
+
- `summarizeHistory` now produces more informative summaries for `unknown`-dominant multi-commit notes, using total additions and substantial-update count as signals.
|
|
15
|
+
- `isProjectionStale` now skips re-embedding when `updatedAt` differs but projected content is unchanged, avoiding unnecessary re-embeds for relationship-only note changes.
|
|
16
|
+
|
|
17
|
+
## [0.19.0] - 2026-03-28
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
- 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.
|
|
22
|
+
- `alwaysLoad: true` in note frontmatter marks a note as an explicit session anchor with highest recall and relationship-expansion priority.
|
|
23
|
+
- `recall` and relationship expansion now factor in effective role and importance, so summary and decision notes surface higher without manual tagging.
|
|
24
|
+
- 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.
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
|
|
28
|
+
- 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.
|
|
29
|
+
|
|
7
30
|
## [0.18.0] - 2026-03-27
|
|
8
31
|
|
|
9
32
|
### 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
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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) {
|
|
@@ -2857,6 +2892,17 @@ server.registerTool("project_memory_summary", {
|
|
|
2857
2892
|
const fixedThemes = ["overview", "decisions", "tooling", "bugs", "architecture", "quality"];
|
|
2858
2893
|
const dynamicThemes = graduationResult.promotedThemes.filter(t => !fixedThemes.includes(t));
|
|
2859
2894
|
const themeOrder = [...fixedThemes, ...dynamicThemes.sort(), "other"];
|
|
2895
|
+
// Collapse thin dynamic-theme buckets (< 2 notes) into "other" to reduce noise.
|
|
2896
|
+
// Fixed themes are kept even when small; only keyword-graduated themes are collapsed.
|
|
2897
|
+
const fixedThemeSet = new Set(fixedThemes);
|
|
2898
|
+
for (const [theme, bucket] of Array.from(themed.entries())) {
|
|
2899
|
+
if (!fixedThemeSet.has(theme) && theme !== "other" && bucket.length < 2) {
|
|
2900
|
+
const otherBucket = themed.get("other") ?? [];
|
|
2901
|
+
otherBucket.push(...bucket);
|
|
2902
|
+
themed.set("other", otherBucket);
|
|
2903
|
+
themed.delete(theme);
|
|
2904
|
+
}
|
|
2905
|
+
}
|
|
2860
2906
|
// Calculate notes distribution (project-scoped only)
|
|
2861
2907
|
const projectVaultCount = projectEntries.filter(e => e.vault.isProject).length;
|
|
2862
2908
|
const mainVaultProjectEntries = projectEntries.filter(e => !e.vault.isProject);
|
|
@@ -2877,7 +2923,7 @@ server.registerTool("project_memory_summary", {
|
|
|
2877
2923
|
if (!bucket || bucket.length === 0)
|
|
2878
2924
|
continue;
|
|
2879
2925
|
// Sort by within-theme score
|
|
2880
|
-
const sorted = [...bucket].sort((a, b) => withinThemeScore(b.note) - withinThemeScore(a.note));
|
|
2926
|
+
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
2927
|
const top = sorted.slice(0, maxPerTheme);
|
|
2882
2928
|
sections.push(`\n${titleCaseTheme(theme)}:`);
|
|
2883
2929
|
sections.push(...top.map(e => `- ${e.note.title} (\`${e.note.id}\`)`));
|
|
@@ -2897,49 +2943,32 @@ server.registerTool("project_memory_summary", {
|
|
|
2897
2943
|
sections.push(`\nRecent activity (start here):`);
|
|
2898
2944
|
sections.push(...recent.map(e => `- ${e.note.updatedAt} — ${e.note.title}`));
|
|
2899
2945
|
// 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
2946
|
const scoredAnchorCandidates = projectEntries
|
|
2904
|
-
.
|
|
2905
|
-
.
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
.
|
|
2947
|
+
.map(e => {
|
|
2948
|
+
const baselineContext = effectiveMetadataById.get(e.note.id);
|
|
2949
|
+
const metadata = baselineContext?.metadata;
|
|
2950
|
+
return {
|
|
2951
|
+
entry: e,
|
|
2952
|
+
metadata,
|
|
2953
|
+
score: anchorScore(e.note, themeCache, metadata),
|
|
2954
|
+
theme: themeCache.get(e.note.id) ?? "other",
|
|
2955
|
+
alwaysLoad: metadata?.alwaysLoad === true,
|
|
2956
|
+
explicitOrientationRole: metadata?.roleSource === "explicit" &&
|
|
2957
|
+
(metadata.role === "summary" || metadata.role === "decision"),
|
|
2958
|
+
hasVisibleGraphParticipation: (baselineContext?.visibleOutbound ?? 0) > 0 || (baselineContext?.inbound ?? 0) > 0,
|
|
2959
|
+
};
|
|
2960
|
+
})
|
|
2961
|
+
.filter(candidate => candidate.score > -Infinity)
|
|
2962
|
+
.filter(candidate => candidate.alwaysLoad || candidate.explicitOrientationRole || candidate.hasVisibleGraphParticipation)
|
|
2963
|
+
.sort((a, b) => b.score - a.score || a.entry.note.title.localeCompare(b.entry.note.title));
|
|
2920
2964
|
// Enforce max 2 per theme for scored anchors
|
|
2921
2965
|
const anchorThemeCounts = new Map();
|
|
2922
2966
|
const anchors = [];
|
|
2923
2967
|
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
2968
|
// Add scored anchors with theme diversity constraint
|
|
2938
2969
|
for (const candidate of scoredAnchorCandidates) {
|
|
2939
2970
|
if (anchors.length >= 10)
|
|
2940
2971
|
break;
|
|
2941
|
-
if (anchorIds.has(candidate.entry.note.id))
|
|
2942
|
-
continue;
|
|
2943
2972
|
const theme = candidate.theme;
|
|
2944
2973
|
const themeCount = anchorThemeCounts.get(theme) ?? 0;
|
|
2945
2974
|
if (themeCount >= 2)
|
|
@@ -3039,8 +3068,29 @@ server.registerTool("project_memory_summary", {
|
|
|
3039
3068
|
};
|
|
3040
3069
|
const primaryEnriched = primaryAnchor ? await enrichOrientationNote(primaryAnchor) : {};
|
|
3041
3070
|
const primaryRelationships = primaryAnchor ? await enrichOrientationNoteWithRelationships(primaryAnchor) : {};
|
|
3042
|
-
|
|
3043
|
-
|
|
3071
|
+
// Select theme-diverse suggestedNext: avoid repeating the primary anchor's theme.
|
|
3072
|
+
// Backfills without constraint if not enough theme-distinct candidates exist.
|
|
3073
|
+
const primaryTheme = primaryAnchor ? (themeCache.get(primaryAnchor.id) ?? "other") : "other";
|
|
3074
|
+
const usedSuggestedThemes = new Set([primaryTheme]);
|
|
3075
|
+
const suggestedCandidates = [];
|
|
3076
|
+
for (const anchor of anchors.slice(1)) {
|
|
3077
|
+
if (suggestedCandidates.length >= 3)
|
|
3078
|
+
break;
|
|
3079
|
+
const anchorTheme = themeCache.get(anchor.id) ?? "other";
|
|
3080
|
+
if (!usedSuggestedThemes.has(anchorTheme)) {
|
|
3081
|
+
suggestedCandidates.push(anchor);
|
|
3082
|
+
usedSuggestedThemes.add(anchorTheme);
|
|
3083
|
+
}
|
|
3084
|
+
}
|
|
3085
|
+
for (const anchor of anchors.slice(1)) {
|
|
3086
|
+
if (suggestedCandidates.length >= 3)
|
|
3087
|
+
break;
|
|
3088
|
+
if (!suggestedCandidates.includes(anchor)) {
|
|
3089
|
+
suggestedCandidates.push(anchor);
|
|
3090
|
+
}
|
|
3091
|
+
}
|
|
3092
|
+
const suggestedEnriched = await Promise.all(suggestedCandidates.map(enrichOrientationNote));
|
|
3093
|
+
const suggestedRelationships = await Promise.all(suggestedCandidates.map(enrichOrientationNoteWithRelationships));
|
|
3044
3094
|
// Enrich fallback primaryEntry when no anchors exist
|
|
3045
3095
|
let fallbackEnriched = {};
|
|
3046
3096
|
let fallbackRelationships = {};
|
|
@@ -3075,7 +3125,7 @@ server.registerTool("project_memory_summary", {
|
|
|
3075
3125
|
...fallbackEnriched,
|
|
3076
3126
|
...fallbackRelationships,
|
|
3077
3127
|
},
|
|
3078
|
-
suggestedNext:
|
|
3128
|
+
suggestedNext: suggestedCandidates.map((a, i) => ({
|
|
3079
3129
|
id: a.id,
|
|
3080
3130
|
title: a.title,
|
|
3081
3131
|
rationale: `Centrality ${a.centrality}, connects ${a.connectionDiversity} themes`,
|
|
@@ -4597,18 +4647,13 @@ server.registerPrompt("mnemonic-workflow-hint", {
|
|
|
4597
4647
|
type: "text",
|
|
4598
4648
|
text: "## Mnemonic MCP workflow hints\n\n" +
|
|
4599
4649
|
"Avoid duplicate memories. Prefer inspecting and updating existing memories before creating new ones.\n\n" +
|
|
4600
|
-
"### Hard rules\n\n" +
|
|
4601
4650
|
"- REQUIRES: Before `remember`, call `recall` or `list` first.\n" +
|
|
4602
4651
|
"- If `recall` or `list` returns a plausible match, call `get` before deciding whether to `update` or `remember`.\n" +
|
|
4603
4652
|
"- If an existing memory already covers the topic, use `update`, not `remember`.\n" +
|
|
4604
4653
|
"- When unsure, prefer `recall` over `remember`.\n" +
|
|
4605
4654
|
"- For repo-related tasks, pass `cwd` so mnemonic can route project memories correctly.\n\n" +
|
|
4606
|
-
"
|
|
4607
|
-
"
|
|
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" +
|
|
4655
|
+
"Workflow: `recall`/`list` -> `get` -> `update` or `remember` -> `relate`/`consolidate`/`move_memory`. Use `discover_tags` only when tag choice is ambiguous.\n\n" +
|
|
4656
|
+
"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
4657
|
"### Anti-patterns\n\n" +
|
|
4613
4658
|
"- Bad: call `remember` immediately because the user said 'remember'.\n" +
|
|
4614
4659
|
"- Good: `recall` or `list` first, then `get`, then `update` or `remember`.\n" +
|