@danielmarbach/mnemonic-mcp 0.19.5 → 0.21.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 +27 -0
- package/README.md +3 -1
- package/build/git.d.ts +3 -0
- package/build/git.d.ts.map +1 -1
- package/build/git.js +142 -119
- package/build/git.js.map +1 -1
- package/build/index.js +229 -64
- package/build/index.js.map +1 -1
- package/build/lexical.d.ts +53 -0
- package/build/lexical.d.ts.map +1 -0
- package/build/lexical.js +112 -0
- package/build/lexical.js.map +1 -0
- package/build/project-introspection.d.ts +2 -0
- package/build/project-introspection.d.ts.map +1 -1
- package/build/project-introspection.js +58 -0
- package/build/project-introspection.js.map +1 -1
- package/build/recall.d.ts +21 -0
- package/build/recall.d.ts.map +1 -1
- package/build/recall.js +34 -1
- package/build/recall.js.map +1 -1
- package/build/structured-content.d.ts +104 -48
- package/build/structured-content.d.ts.map +1 -1
- package/build/structured-content.js +29 -26
- package/build/structured-content.js.map +1 -1
- package/package.json +1 -1
package/build/index.js
CHANGED
|
@@ -10,15 +10,16 @@ import { embed, cosineSimilarity, embedModel } from "./embeddings.js";
|
|
|
10
10
|
import { buildTemporalHistoryEntry, computeConfidence, getNoteProvenance } from "./provenance.js";
|
|
11
11
|
import { enrichTemporalHistory } from "./temporal-interpretation.js";
|
|
12
12
|
import { getOrBuildProjection } from "./projections.js";
|
|
13
|
-
import { invalidateActiveProjectCache, getOrBuildVaultEmbeddings, getOrBuildVaultNoteList, getSessionCachedNote, } from "./cache.js";
|
|
13
|
+
import { invalidateActiveProjectCache, getOrBuildVaultEmbeddings, getOrBuildVaultNoteList, getSessionCachedNote, getSessionCachedProjection, setSessionCachedProjection, } from "./cache.js";
|
|
14
14
|
import { performance } from "perf_hooks";
|
|
15
15
|
import { filterRelationships, mergeRelationshipsFromNotes, normalizeMergePlanSourceIds, resolveEffectiveConsolidationMode, } from "./consolidate.js";
|
|
16
|
-
import { computeRecallMetadataBoost, selectRecallResults } from "./recall.js";
|
|
16
|
+
import { computeRecallMetadataBoost, computeHybridScore, selectRecallResults, applyLexicalReranking } from "./recall.js";
|
|
17
|
+
import { shouldTriggerLexicalRescue, computeLexicalScore, LEXICAL_RESCUE_CANDIDATE_LIMIT, LEXICAL_RESCUE_THRESHOLD, LEXICAL_RESCUE_RESULT_LIMIT, } from "./lexical.js";
|
|
17
18
|
import { getRelationshipPreview } from "./relationships.js";
|
|
18
19
|
import { cleanMarkdown } from "./markdown.js";
|
|
19
20
|
import { MnemonicConfigStore, readVaultSchemaVersion } from "./config.js";
|
|
20
21
|
import { CONSOLIDATION_MODES, PROTECTED_BRANCH_BEHAVIORS, PROJECT_POLICY_SCOPES, WRITE_SCOPES, isProtectedBranch, resolveProtectedBranchBehavior, resolveProtectedBranchPatterns, resolveConsolidationMode, resolveWriteScope, } from "./project-memory-policy.js";
|
|
21
|
-
import { classifyTheme, classifyThemeWithGraduation, computeThemesWithGraduation, summarizePreview, titleCaseTheme, withinThemeScore, anchorScore, computeConnectionDiversity, } from "./project-introspection.js";
|
|
22
|
+
import { classifyTheme, classifyThemeWithGraduation, computeThemesWithGraduation, summarizePreview, titleCaseTheme, daysSinceUpdate, withinThemeScore, anchorScore, computeConnectionDiversity, workingStateScore, extractNextAction, } from "./project-introspection.js";
|
|
22
23
|
import { getEffectiveMetadata } from "./role-suggestions.js";
|
|
23
24
|
import { detectProject, getCurrentGitBranch, resolveProjectIdentity } from "./project.js";
|
|
24
25
|
import { VaultManager } from "./vault.js";
|
|
@@ -320,6 +321,18 @@ async function resolveProject(cwd) {
|
|
|
320
321
|
getProjectIdentityOverride: async (projectId) => configStore.getProjectIdentityOverride(projectId),
|
|
321
322
|
});
|
|
322
323
|
}
|
|
324
|
+
function toProjectRef(project) {
|
|
325
|
+
return project ? { id: project.id, name: project.name } : undefined;
|
|
326
|
+
}
|
|
327
|
+
function noteProjectRef(note) {
|
|
328
|
+
if (!note.project || !note.projectName) {
|
|
329
|
+
return undefined;
|
|
330
|
+
}
|
|
331
|
+
return {
|
|
332
|
+
id: note.project,
|
|
333
|
+
name: note.projectName,
|
|
334
|
+
};
|
|
335
|
+
}
|
|
323
336
|
async function resolveProjectIdentityForCwd(cwd) {
|
|
324
337
|
if (!cwd)
|
|
325
338
|
return undefined;
|
|
@@ -1727,6 +1740,52 @@ server.registerTool("get_project_memory_policy", {
|
|
|
1727
1740
|
structuredContent,
|
|
1728
1741
|
};
|
|
1729
1742
|
});
|
|
1743
|
+
// ── Lexical rescue helper ─────────────────────────────────────────────────────
|
|
1744
|
+
async function collectLexicalRescueCandidates(vaults, query, project, scope, tags, existingIds) {
|
|
1745
|
+
const existingIdSet = new Set(existingIds.map((c) => c.id));
|
|
1746
|
+
const candidates = [];
|
|
1747
|
+
for (const vault of vaults) {
|
|
1748
|
+
const notes = await vault.storage.listNotes().catch(() => []);
|
|
1749
|
+
for (const note of notes) {
|
|
1750
|
+
if (existingIdSet.has(note.id))
|
|
1751
|
+
continue;
|
|
1752
|
+
if (tags && tags.length > 0) {
|
|
1753
|
+
const noteTags = new Set(note.tags);
|
|
1754
|
+
if (!tags.every((t) => noteTags.has(t)))
|
|
1755
|
+
continue;
|
|
1756
|
+
}
|
|
1757
|
+
const isProjectNote = note.project !== undefined;
|
|
1758
|
+
const isCurrentProject = project && note.project === project.id;
|
|
1759
|
+
if (scope === "project" && !isCurrentProject)
|
|
1760
|
+
continue;
|
|
1761
|
+
if (scope === "global" && isProjectNote)
|
|
1762
|
+
continue;
|
|
1763
|
+
const projection = await getOrBuildProjection(vault.storage, note).catch(() => undefined);
|
|
1764
|
+
if (!projection)
|
|
1765
|
+
continue;
|
|
1766
|
+
const lexicalScore = computeLexicalScore(query, projection.projectionText);
|
|
1767
|
+
if (lexicalScore < LEXICAL_RESCUE_THRESHOLD)
|
|
1768
|
+
continue;
|
|
1769
|
+
const metadataBoost = computeRecallMetadataBoost(getEffectiveMetadata(note));
|
|
1770
|
+
const boost = (isCurrentProject ? 0.15 : 0) + metadataBoost;
|
|
1771
|
+
candidates.push({
|
|
1772
|
+
id: note.id,
|
|
1773
|
+
score: 0,
|
|
1774
|
+
boosted: boost,
|
|
1775
|
+
vault,
|
|
1776
|
+
isCurrentProject: Boolean(isCurrentProject),
|
|
1777
|
+
lexicalScore,
|
|
1778
|
+
});
|
|
1779
|
+
if (candidates.length >= LEXICAL_RESCUE_CANDIDATE_LIMIT)
|
|
1780
|
+
break;
|
|
1781
|
+
}
|
|
1782
|
+
if (candidates.length >= LEXICAL_RESCUE_CANDIDATE_LIMIT)
|
|
1783
|
+
break;
|
|
1784
|
+
}
|
|
1785
|
+
return candidates
|
|
1786
|
+
.sort((a, b) => computeHybridScore(b) - computeHybridScore(a))
|
|
1787
|
+
.slice(0, LEXICAL_RESCUE_RESULT_LIMIT);
|
|
1788
|
+
}
|
|
1730
1789
|
// ── recall ────────────────────────────────────────────────────────────────────
|
|
1731
1790
|
server.registerTool("recall", {
|
|
1732
1791
|
title: "Recall",
|
|
@@ -1768,9 +1827,10 @@ server.registerTool("recall", {
|
|
|
1768
1827
|
.describe("'project' = only this project's memories (project-scoped storage), " +
|
|
1769
1828
|
"'global' = only unscoped memories (main/global storage), " +
|
|
1770
1829
|
"'all' = both, with project notes boosted (default)"),
|
|
1830
|
+
lifecycle: z.enum(["temporary", "permanent"]).optional().describe("Filter results by lifecycle. Useful for recovering working-state with `lifecycle: temporary` after `project_memory_summary` orientation."),
|
|
1771
1831
|
}),
|
|
1772
1832
|
outputSchema: RecallResultSchema,
|
|
1773
|
-
}, async ({ query, cwd, limit, minSimilarity, mode, verbose, tags, scope }) => {
|
|
1833
|
+
}, async ({ query, cwd, limit, minSimilarity, mode, verbose, tags, scope, lifecycle }) => {
|
|
1774
1834
|
const t0Recall = performance.now();
|
|
1775
1835
|
await ensureBranchSynced(cwd);
|
|
1776
1836
|
const project = await resolveProject(cwd);
|
|
@@ -1816,6 +1876,9 @@ server.registerTool("recall", {
|
|
|
1816
1876
|
if (!tags.every((t) => noteTags.has(t)))
|
|
1817
1877
|
continue;
|
|
1818
1878
|
}
|
|
1879
|
+
if (lifecycle && note.lifecycle !== lifecycle) {
|
|
1880
|
+
continue;
|
|
1881
|
+
}
|
|
1819
1882
|
const isProjectNote = note.project !== undefined;
|
|
1820
1883
|
const isCurrentProject = project && note.project === project.id;
|
|
1821
1884
|
if (scope === "project") {
|
|
@@ -1831,7 +1894,42 @@ server.registerTool("recall", {
|
|
|
1831
1894
|
scored.push({ id: rec.id, score: rawScore, boosted: rawScore + boost, vault, isCurrentProject: Boolean(isCurrentProject) });
|
|
1832
1895
|
}
|
|
1833
1896
|
}
|
|
1834
|
-
const
|
|
1897
|
+
const projectionTexts = new Map();
|
|
1898
|
+
for (const candidate of scored) {
|
|
1899
|
+
const note = await readCachedNote(candidate.vault, candidate.id).catch(() => null);
|
|
1900
|
+
if (!note) {
|
|
1901
|
+
continue;
|
|
1902
|
+
}
|
|
1903
|
+
const projection = await getOrBuildProjection(candidate.vault.storage, note).catch(() => undefined);
|
|
1904
|
+
if (!projection) {
|
|
1905
|
+
continue;
|
|
1906
|
+
}
|
|
1907
|
+
projectionTexts.set(candidate.id, projection.projectionText);
|
|
1908
|
+
if (project) {
|
|
1909
|
+
setSessionCachedProjection(project.id, candidate.id, projection);
|
|
1910
|
+
}
|
|
1911
|
+
}
|
|
1912
|
+
// Apply lexical reranking over semantic candidates (fail-soft)
|
|
1913
|
+
const getProjectionText = (id) => {
|
|
1914
|
+
const inlineProjection = projectionTexts.get(id);
|
|
1915
|
+
if (inlineProjection) {
|
|
1916
|
+
return inlineProjection;
|
|
1917
|
+
}
|
|
1918
|
+
if (project) {
|
|
1919
|
+
const cached = getSessionCachedProjection(project.id, id);
|
|
1920
|
+
if (cached)
|
|
1921
|
+
return cached.projectionText;
|
|
1922
|
+
}
|
|
1923
|
+
return undefined;
|
|
1924
|
+
};
|
|
1925
|
+
const reranked = applyLexicalReranking(scored, query, getProjectionText);
|
|
1926
|
+
// Lexical rescue: when semantic results are weak, scan projections for additional candidates
|
|
1927
|
+
const topScore = reranked.length > 0 ? reranked[0].score : undefined;
|
|
1928
|
+
if (shouldTriggerLexicalRescue(topScore, reranked.length)) {
|
|
1929
|
+
const rescueCandidates = await collectLexicalRescueCandidates(vaults, query, project ?? undefined, scope, tags, reranked);
|
|
1930
|
+
reranked.push(...rescueCandidates);
|
|
1931
|
+
}
|
|
1932
|
+
const top = selectRecallResults(reranked, limit, scope);
|
|
1835
1933
|
if (top.length === 0) {
|
|
1836
1934
|
const structuredContent = { action: "recalled", query, scope: scope || "all", results: [] };
|
|
1837
1935
|
return { content: [{ type: "text", text: "No memories found matching that query." }], structuredContent };
|
|
@@ -1885,8 +1983,7 @@ server.registerTool("recall", {
|
|
|
1885
1983
|
title: note.title,
|
|
1886
1984
|
score,
|
|
1887
1985
|
boosted,
|
|
1888
|
-
project: note
|
|
1889
|
-
projectName: note.projectName,
|
|
1986
|
+
project: noteProjectRef(note),
|
|
1890
1987
|
vault: storageLabel(vault),
|
|
1891
1988
|
tags: note.tags,
|
|
1892
1989
|
lifecycle: note.lifecycle,
|
|
@@ -2060,8 +2157,7 @@ server.registerTool("update", {
|
|
|
2060
2157
|
title: updated.title,
|
|
2061
2158
|
fieldsModified: changes,
|
|
2062
2159
|
timestamp: now,
|
|
2063
|
-
project: updated
|
|
2064
|
-
projectName: updated.projectName,
|
|
2160
|
+
project: noteProjectRef(updated),
|
|
2065
2161
|
lifecycle: updated.lifecycle,
|
|
2066
2162
|
persistence,
|
|
2067
2163
|
};
|
|
@@ -2165,8 +2261,7 @@ server.registerTool("forget", {
|
|
|
2165
2261
|
action: "forgotten",
|
|
2166
2262
|
id,
|
|
2167
2263
|
title: note.title,
|
|
2168
|
-
project: note
|
|
2169
|
-
projectName: note.projectName,
|
|
2264
|
+
project: noteProjectRef(note),
|
|
2170
2265
|
relationshipsCleaned: vaultChanges.size > 0 ? Array.from(vaultChanges.values()).reduce((sum, files) => sum + files.length - 1, 0) : 0,
|
|
2171
2266
|
vaultsModified: Array.from(vaultChanges.keys()).map(v => storageLabel(v)),
|
|
2172
2267
|
retry,
|
|
@@ -2244,10 +2339,10 @@ server.registerTool("get", {
|
|
|
2244
2339
|
id: note.id,
|
|
2245
2340
|
title: note.title,
|
|
2246
2341
|
content: note.content,
|
|
2247
|
-
project: note
|
|
2248
|
-
projectName: note.projectName,
|
|
2342
|
+
project: noteProjectRef(note),
|
|
2249
2343
|
tags: note.tags,
|
|
2250
2344
|
lifecycle: note.lifecycle,
|
|
2345
|
+
alwaysLoad: note.alwaysLoad,
|
|
2251
2346
|
relatedTo: note.relatedTo,
|
|
2252
2347
|
createdAt: note.createdAt,
|
|
2253
2348
|
updatedAt: note.updatedAt,
|
|
@@ -2258,7 +2353,7 @@ server.registerTool("get", {
|
|
|
2258
2353
|
const lines = [];
|
|
2259
2354
|
for (const note of found) {
|
|
2260
2355
|
lines.push(`## ${note.title} (${note.id})`);
|
|
2261
|
-
lines.push(`project: ${note.
|
|
2356
|
+
lines.push(`project: ${note.project?.name ?? "global"} | stored: ${note.vault} | lifecycle: ${note.lifecycle}`);
|
|
2262
2357
|
if (note.tags.length > 0)
|
|
2263
2358
|
lines.push(`tags: ${note.tags.join(", ")}`);
|
|
2264
2359
|
lines.push("");
|
|
@@ -2323,8 +2418,7 @@ server.registerTool("where_is_memory", {
|
|
|
2323
2418
|
action: "located",
|
|
2324
2419
|
id: note.id,
|
|
2325
2420
|
title: note.title,
|
|
2326
|
-
project: note
|
|
2327
|
-
projectName: note.projectName,
|
|
2421
|
+
project: noteProjectRef(note),
|
|
2328
2422
|
vault: vaultLabel,
|
|
2329
2423
|
updatedAt: note.updatedAt,
|
|
2330
2424
|
relatedCount,
|
|
@@ -2402,8 +2496,7 @@ server.registerTool("list", {
|
|
|
2402
2496
|
const structuredNotes = entries.map(({ note, vault }) => ({
|
|
2403
2497
|
id: note.id,
|
|
2404
2498
|
title: note.title,
|
|
2405
|
-
project: note
|
|
2406
|
-
projectName: note.projectName,
|
|
2499
|
+
project: noteProjectRef(note),
|
|
2407
2500
|
tags: note.tags,
|
|
2408
2501
|
lifecycle: note.lifecycle,
|
|
2409
2502
|
vault: storageLabel(vault),
|
|
@@ -2704,16 +2797,21 @@ server.registerTool("recent_memories", {
|
|
|
2704
2797
|
limit: z.number().int().min(1).max(20).optional().default(5),
|
|
2705
2798
|
includePreview: z.boolean().optional().default(true),
|
|
2706
2799
|
includeStorage: z.boolean().optional().default(true),
|
|
2800
|
+
lifecycle: z.enum(["temporary", "permanent"]).optional().describe("Filter results by lifecycle. Useful for recovering working-state with `lifecycle: temporary` after `project_memory_summary` orientation."),
|
|
2707
2801
|
}),
|
|
2708
2802
|
outputSchema: RecentResultSchema,
|
|
2709
|
-
}, async ({ cwd, scope, storedIn, limit, includePreview, includeStorage }) => {
|
|
2803
|
+
}, async ({ cwd, scope, storedIn, limit, includePreview, includeStorage, lifecycle }) => {
|
|
2710
2804
|
await ensureBranchSynced(cwd);
|
|
2711
2805
|
const { project, entries } = await collectVisibleNotes(cwd, scope, undefined, storedIn);
|
|
2712
|
-
|
|
2806
|
+
let filteredEntries = entries;
|
|
2807
|
+
if (lifecycle) {
|
|
2808
|
+
filteredEntries = entries.filter(({ note }) => note.lifecycle === lifecycle);
|
|
2809
|
+
}
|
|
2810
|
+
const recent = [...filteredEntries]
|
|
2713
2811
|
.sort((a, b) => b.note.updatedAt.localeCompare(a.note.updatedAt))
|
|
2714
2812
|
.slice(0, limit);
|
|
2715
2813
|
if (recent.length === 0) {
|
|
2716
|
-
const structuredContent = { action: "recent_shown", project: project
|
|
2814
|
+
const structuredContent = { action: "recent_shown", project: toProjectRef(project), count: 0, limit: limit || 5, notes: [] };
|
|
2717
2815
|
return { content: [{ type: "text", text: "No memories found." }], structuredContent };
|
|
2718
2816
|
}
|
|
2719
2817
|
const header = project && scope !== "global"
|
|
@@ -2728,8 +2826,7 @@ server.registerTool("recent_memories", {
|
|
|
2728
2826
|
const structuredNotes = recent.map(({ note, vault }) => ({
|
|
2729
2827
|
id: note.id,
|
|
2730
2828
|
title: note.title,
|
|
2731
|
-
project: note
|
|
2732
|
-
projectName: note.projectName,
|
|
2829
|
+
project: noteProjectRef(note),
|
|
2733
2830
|
tags: note.tags,
|
|
2734
2831
|
lifecycle: note.lifecycle,
|
|
2735
2832
|
vault: storageLabel(vault),
|
|
@@ -2738,8 +2835,7 @@ server.registerTool("recent_memories", {
|
|
|
2738
2835
|
}));
|
|
2739
2836
|
const structuredContent = {
|
|
2740
2837
|
action: "recent_shown",
|
|
2741
|
-
project: project
|
|
2742
|
-
projectName: project?.name,
|
|
2838
|
+
project: toProjectRef(project),
|
|
2743
2839
|
count: recent.length,
|
|
2744
2840
|
limit: limit || 5,
|
|
2745
2841
|
notes: structuredNotes,
|
|
@@ -2778,7 +2874,7 @@ server.registerTool("memory_graph", {
|
|
|
2778
2874
|
await ensureBranchSynced(cwd);
|
|
2779
2875
|
const { project, entries } = await collectVisibleNotes(cwd, scope, undefined, storedIn);
|
|
2780
2876
|
if (entries.length === 0) {
|
|
2781
|
-
const structuredContent = { action: "graph_shown", project: project
|
|
2877
|
+
const structuredContent = { action: "graph_shown", project: toProjectRef(project), nodes: [], limit, truncated: false };
|
|
2782
2878
|
return { content: [{ type: "text", text: "No memories found." }], structuredContent };
|
|
2783
2879
|
}
|
|
2784
2880
|
const visibleIds = new Set(entries.map((entry) => entry.note.id));
|
|
@@ -2793,7 +2889,7 @@ server.registerTool("memory_graph", {
|
|
|
2793
2889
|
})
|
|
2794
2890
|
.filter(Boolean);
|
|
2795
2891
|
if (lines.length === 0) {
|
|
2796
|
-
const structuredContent = { action: "graph_shown", project: project
|
|
2892
|
+
const structuredContent = { action: "graph_shown", project: toProjectRef(project), nodes: [], limit, truncated: false };
|
|
2797
2893
|
return { content: [{ type: "text", text: "No relationships found for that scope." }], structuredContent };
|
|
2798
2894
|
}
|
|
2799
2895
|
const header = project && scope !== "global"
|
|
@@ -2817,8 +2913,7 @@ server.registerTool("memory_graph", {
|
|
|
2817
2913
|
.filter((node) => node.edges.length > 0);
|
|
2818
2914
|
const structuredContent = {
|
|
2819
2915
|
action: "graph_shown",
|
|
2820
|
-
project: project
|
|
2821
|
-
projectName: project?.name,
|
|
2916
|
+
project: toProjectRef(project),
|
|
2822
2917
|
nodes: structuredNodes,
|
|
2823
2918
|
limit,
|
|
2824
2919
|
truncated: structuredNodes.length < entries.filter(e => (e.note.relatedTo?.length ?? 0) > 0).length,
|
|
@@ -2838,6 +2933,7 @@ server.registerTool("project_memory_summary", {
|
|
|
2838
2933
|
"Returns:\n" +
|
|
2839
2934
|
"- A synthesized project-level summary based on stored memories\n" +
|
|
2840
2935
|
"- Bounded 1-hop relationship previews on orientation entry points (primaryEntry and suggestedNext)\n\n" +
|
|
2936
|
+
"- Optional compact working-state recovery hints when relevant temporary notes exist\n\n" +
|
|
2841
2937
|
"Read-only.\n\n" +
|
|
2842
2938
|
"Typical next step:\n" +
|
|
2843
2939
|
"- Use `recall` or `list` to drill down into specific areas.",
|
|
@@ -2978,6 +3074,48 @@ server.registerTool("project_memory_summary", {
|
|
|
2978
3074
|
.slice(0, recentLimit);
|
|
2979
3075
|
sections.push(`\nRecent activity (start here):`);
|
|
2980
3076
|
sections.push(...recent.map(e => `- ${e.note.updatedAt} — ${e.note.title}`));
|
|
3077
|
+
const temporaryEntries = projectEntries
|
|
3078
|
+
.filter((entry) => entry.note.lifecycle === "temporary")
|
|
3079
|
+
.map((entry) => {
|
|
3080
|
+
const metadata = effectiveMetadataById.get(entry.note.id)?.metadata;
|
|
3081
|
+
const score = workingStateScore(entry.note, metadata);
|
|
3082
|
+
const nextAction = extractNextAction(entry.note);
|
|
3083
|
+
const relatedCount = entry.note.relatedTo?.length ?? 0;
|
|
3084
|
+
const days = daysSinceUpdate(entry.note.updatedAt);
|
|
3085
|
+
const rationaleParts = [`updated ${days < 1 ? "today" : `${Math.round(days)}d ago`}`];
|
|
3086
|
+
if (relatedCount > 0)
|
|
3087
|
+
rationaleParts.push(`${relatedCount} linked note${relatedCount === 1 ? "" : "s"}`);
|
|
3088
|
+
if (nextAction)
|
|
3089
|
+
rationaleParts.push("explicit next action");
|
|
3090
|
+
if (metadata?.role === "plan" || metadata?.role === "context")
|
|
3091
|
+
rationaleParts.push(`${metadata.role} note`);
|
|
3092
|
+
return {
|
|
3093
|
+
entry,
|
|
3094
|
+
score,
|
|
3095
|
+
rationale: rationaleParts.join(", "),
|
|
3096
|
+
preview: summarizePreview(entry.note.content, 120),
|
|
3097
|
+
nextAction,
|
|
3098
|
+
};
|
|
3099
|
+
})
|
|
3100
|
+
.filter((candidate) => candidate.score > -Infinity)
|
|
3101
|
+
.sort((a, b) => b.score - a.score || b.entry.note.updatedAt.localeCompare(a.entry.note.updatedAt))
|
|
3102
|
+
.slice(0, 3);
|
|
3103
|
+
const workingState = temporaryEntries.length > 0
|
|
3104
|
+
? {
|
|
3105
|
+
summary: temporaryEntries.length === 1
|
|
3106
|
+
? `1 temporary note may help resume active work.`
|
|
3107
|
+
: `${temporaryEntries.length} temporary notes may help resume active work.`,
|
|
3108
|
+
recoveryHint: "Orient with project_memory_summary first, then inspect these temporary notes if you need to continue in-progress work.",
|
|
3109
|
+
notes: temporaryEntries.map(({ entry, rationale, preview, nextAction }) => ({
|
|
3110
|
+
id: entry.note.id,
|
|
3111
|
+
title: entry.note.title,
|
|
3112
|
+
updatedAt: entry.note.updatedAt,
|
|
3113
|
+
rationale,
|
|
3114
|
+
preview,
|
|
3115
|
+
nextAction,
|
|
3116
|
+
})),
|
|
3117
|
+
}
|
|
3118
|
+
: undefined;
|
|
2981
3119
|
// Anchor notes with diversity constraint (project-scoped only)
|
|
2982
3120
|
const scoredAnchorCandidates = projectEntries
|
|
2983
3121
|
.map(e => {
|
|
@@ -3127,11 +3265,14 @@ server.registerTool("project_memory_summary", {
|
|
|
3127
3265
|
}
|
|
3128
3266
|
const suggestedEnriched = await Promise.all(suggestedCandidates.map(enrichOrientationNote));
|
|
3129
3267
|
const suggestedRelationships = await Promise.all(suggestedCandidates.map(enrichOrientationNoteWithRelationships));
|
|
3268
|
+
const recentPermanent = recent.find((entry) => entry.note.lifecycle === "permanent");
|
|
3269
|
+
const fallbackEntry = recentPermanent ?? recent[0];
|
|
3270
|
+
const permanentOverrideUsed = Boolean(recentPermanent && recent[0] && recentPermanent.note.id !== recent[0].note.id);
|
|
3130
3271
|
// Enrich fallback primaryEntry when no anchors exist
|
|
3131
3272
|
let fallbackEnriched = {};
|
|
3132
3273
|
let fallbackRelationships = {};
|
|
3133
|
-
if (!primaryAnchor &&
|
|
3134
|
-
const fallbackNote =
|
|
3274
|
+
if (!primaryAnchor && fallbackEntry) {
|
|
3275
|
+
const fallbackNote = fallbackEntry.note;
|
|
3135
3276
|
const vault = noteVaultMap.get(fallbackNote.id);
|
|
3136
3277
|
if (vault) {
|
|
3137
3278
|
const filePath = `${vault.notesRelDir}/${fallbackNote.id}.md`;
|
|
@@ -3153,11 +3294,13 @@ server.registerTool("project_memory_summary", {
|
|
|
3153
3294
|
...primaryRelationships,
|
|
3154
3295
|
}
|
|
3155
3296
|
: {
|
|
3156
|
-
id:
|
|
3157
|
-
title:
|
|
3158
|
-
rationale:
|
|
3159
|
-
? "Most recent note — no high-centrality anchors found"
|
|
3160
|
-
:
|
|
3297
|
+
id: fallbackEntry?.note.id ?? projectEntries[0]?.note.id ?? "",
|
|
3298
|
+
title: fallbackEntry?.note.title ?? projectEntries[0]?.note.title ?? "No notes",
|
|
3299
|
+
rationale: permanentOverrideUsed
|
|
3300
|
+
? "Most recent permanent note — no high-centrality anchors found"
|
|
3301
|
+
: fallbackEntry
|
|
3302
|
+
? "Most recent note — no high-centrality anchors found"
|
|
3303
|
+
: "Only note available",
|
|
3161
3304
|
...fallbackEnriched,
|
|
3162
3305
|
...fallbackRelationships,
|
|
3163
3306
|
},
|
|
@@ -3203,6 +3346,18 @@ server.registerTool("project_memory_summary", {
|
|
|
3203
3346
|
sections.push(` - ${w}`);
|
|
3204
3347
|
}
|
|
3205
3348
|
}
|
|
3349
|
+
if (workingState) {
|
|
3350
|
+
sections.push(`\nWorking state:`);
|
|
3351
|
+
sections.push(workingState.summary);
|
|
3352
|
+
sections.push(`Recovery hint: ${workingState.recoveryHint}`);
|
|
3353
|
+
for (const note of workingState.notes) {
|
|
3354
|
+
sections.push(`- ${note.title} (\`${note.id}\`) — ${note.rationale}`);
|
|
3355
|
+
sections.push(` Preview: ${note.preview}`);
|
|
3356
|
+
if (note.nextAction) {
|
|
3357
|
+
sections.push(` Next action: ${note.nextAction}`);
|
|
3358
|
+
}
|
|
3359
|
+
}
|
|
3360
|
+
}
|
|
3206
3361
|
// Related global notes (optional, anchor-based similarity)
|
|
3207
3362
|
const structuredContent = {
|
|
3208
3363
|
action: "project_summary_shown",
|
|
@@ -3222,6 +3377,7 @@ server.registerTool("project_memory_summary", {
|
|
|
3222
3377
|
})),
|
|
3223
3378
|
anchors,
|
|
3224
3379
|
orientation,
|
|
3380
|
+
workingState,
|
|
3225
3381
|
relatedGlobal,
|
|
3226
3382
|
};
|
|
3227
3383
|
console.error(`[summary:timing] ${(performance.now() - t0Summary).toFixed(1)}ms`);
|
|
@@ -3977,8 +4133,7 @@ async function detectDuplicates(entries, threshold, project) {
|
|
|
3977
4133
|
const structuredContent = {
|
|
3978
4134
|
action: "consolidated",
|
|
3979
4135
|
strategy: "detect-duplicates",
|
|
3980
|
-
project: project
|
|
3981
|
-
projectName: project?.name,
|
|
4136
|
+
project: toProjectRef(project),
|
|
3982
4137
|
notesProcessed: entries.length,
|
|
3983
4138
|
notesModified: 0,
|
|
3984
4139
|
};
|
|
@@ -4065,8 +4220,7 @@ function findClusters(entries, project) {
|
|
|
4065
4220
|
const structuredContent = {
|
|
4066
4221
|
action: "consolidated",
|
|
4067
4222
|
strategy: "find-clusters",
|
|
4068
|
-
project: project
|
|
4069
|
-
projectName: project?.name,
|
|
4223
|
+
project: toProjectRef(project),
|
|
4070
4224
|
notesProcessed: entries.length,
|
|
4071
4225
|
notesModified: 0,
|
|
4072
4226
|
themeGroups,
|
|
@@ -4153,8 +4307,7 @@ async function suggestMerges(entries, threshold, defaultConsolidationMode, proje
|
|
|
4153
4307
|
const structuredContent = {
|
|
4154
4308
|
action: "consolidated",
|
|
4155
4309
|
strategy: "suggest-merges",
|
|
4156
|
-
project: project
|
|
4157
|
-
projectName: project?.name,
|
|
4310
|
+
project: toProjectRef(project),
|
|
4158
4311
|
notesProcessed: entries.length,
|
|
4159
4312
|
notesModified: 0,
|
|
4160
4313
|
};
|
|
@@ -4178,8 +4331,7 @@ async function executeMerge(entries, mergePlan, defaultConsolidationMode, projec
|
|
|
4178
4331
|
const structuredContent = {
|
|
4179
4332
|
action: "consolidated",
|
|
4180
4333
|
strategy: "execute-merge",
|
|
4181
|
-
project: project
|
|
4182
|
-
projectName: project?.name,
|
|
4334
|
+
project: toProjectRef(project),
|
|
4183
4335
|
notesProcessed: entries.length,
|
|
4184
4336
|
notesModified: 0,
|
|
4185
4337
|
warnings: ["execute-merge requires at least two distinct sourceIds."],
|
|
@@ -4190,8 +4342,7 @@ async function executeMerge(entries, mergePlan, defaultConsolidationMode, projec
|
|
|
4190
4342
|
const structuredContent = {
|
|
4191
4343
|
action: "consolidated",
|
|
4192
4344
|
strategy: "execute-merge",
|
|
4193
|
-
project: project
|
|
4194
|
-
projectName: project?.name,
|
|
4345
|
+
project: toProjectRef(project),
|
|
4195
4346
|
notesProcessed: entries.length,
|
|
4196
4347
|
notesModified: 0,
|
|
4197
4348
|
warnings: ["execute-merge requires a non-empty targetTitle."],
|
|
@@ -4206,8 +4357,7 @@ async function executeMerge(entries, mergePlan, defaultConsolidationMode, projec
|
|
|
4206
4357
|
const structuredContent = {
|
|
4207
4358
|
action: "consolidated",
|
|
4208
4359
|
strategy: "execute-merge",
|
|
4209
|
-
project: project
|
|
4210
|
-
projectName: project?.name,
|
|
4360
|
+
project: toProjectRef(project),
|
|
4211
4361
|
notesProcessed: entries.length,
|
|
4212
4362
|
notesModified: 0,
|
|
4213
4363
|
warnings: [`Source note '${id}' not found.`],
|
|
@@ -4242,8 +4392,7 @@ async function executeMerge(entries, mergePlan, defaultConsolidationMode, projec
|
|
|
4242
4392
|
const structuredContent = {
|
|
4243
4393
|
action: "consolidated",
|
|
4244
4394
|
strategy: "execute-merge",
|
|
4245
|
-
project: project
|
|
4246
|
-
projectName: project?.name,
|
|
4395
|
+
project: toProjectRef(project),
|
|
4247
4396
|
notesProcessed: entries.length,
|
|
4248
4397
|
notesModified: 0,
|
|
4249
4398
|
warnings: [message],
|
|
@@ -4455,8 +4604,7 @@ async function executeMerge(entries, mergePlan, defaultConsolidationMode, projec
|
|
|
4455
4604
|
const structuredContent = {
|
|
4456
4605
|
action: "consolidated",
|
|
4457
4606
|
strategy: "execute-merge",
|
|
4458
|
-
project: project
|
|
4459
|
-
projectName: project?.name,
|
|
4607
|
+
project: toProjectRef(project),
|
|
4460
4608
|
notesProcessed: entries.length,
|
|
4461
4609
|
notesModified: vaultChanges.size,
|
|
4462
4610
|
persistence,
|
|
@@ -4496,8 +4644,7 @@ async function pruneSuperseded(entries, consolidationMode, project, cwd, policy,
|
|
|
4496
4644
|
const structuredContent = {
|
|
4497
4645
|
action: "consolidated",
|
|
4498
4646
|
strategy: "prune-superseded",
|
|
4499
|
-
project: project
|
|
4500
|
-
projectName: project?.name,
|
|
4647
|
+
project: toProjectRef(project),
|
|
4501
4648
|
notesProcessed: entries.length,
|
|
4502
4649
|
notesModified: 0,
|
|
4503
4650
|
warnings: [`prune-superseded requires consolidationMode="delete". Current mode: ${consolidationMode}.`],
|
|
@@ -4529,8 +4676,7 @@ async function pruneSuperseded(entries, consolidationMode, project, cwd, policy,
|
|
|
4529
4676
|
const structuredContent = {
|
|
4530
4677
|
action: "consolidated",
|
|
4531
4678
|
strategy: "prune-superseded",
|
|
4532
|
-
project: project
|
|
4533
|
-
projectName: project?.name,
|
|
4679
|
+
project: toProjectRef(project),
|
|
4534
4680
|
notesProcessed: entries.length,
|
|
4535
4681
|
notesModified: 0,
|
|
4536
4682
|
};
|
|
@@ -4559,8 +4705,7 @@ async function pruneSuperseded(entries, consolidationMode, project, cwd, policy,
|
|
|
4559
4705
|
const structuredContent = {
|
|
4560
4706
|
action: "consolidated",
|
|
4561
4707
|
strategy: "prune-superseded",
|
|
4562
|
-
project: project
|
|
4563
|
-
projectName: project?.name,
|
|
4708
|
+
project: toProjectRef(project),
|
|
4564
4709
|
notesProcessed: entries.length,
|
|
4565
4710
|
notesModified: 0,
|
|
4566
4711
|
warnings: [message],
|
|
@@ -4615,8 +4760,7 @@ async function pruneSuperseded(entries, consolidationMode, project, cwd, policy,
|
|
|
4615
4760
|
const structuredContent = {
|
|
4616
4761
|
action: "consolidated",
|
|
4617
4762
|
strategy: "prune-superseded",
|
|
4618
|
-
project: project
|
|
4619
|
-
projectName: project?.name,
|
|
4763
|
+
project: toProjectRef(project),
|
|
4620
4764
|
notesProcessed: entries.length,
|
|
4621
4765
|
notesModified: vaultChanges.size,
|
|
4622
4766
|
retry,
|
|
@@ -4644,8 +4788,7 @@ async function dryRunAll(entries, threshold, defaultConsolidationMode, project,
|
|
|
4644
4788
|
const structuredContent = {
|
|
4645
4789
|
action: "consolidated",
|
|
4646
4790
|
strategy: "dry-run",
|
|
4647
|
-
project: project
|
|
4648
|
-
projectName: project?.name,
|
|
4791
|
+
project: toProjectRef(project),
|
|
4649
4792
|
notesProcessed: entries.length,
|
|
4650
4793
|
notesModified: 0,
|
|
4651
4794
|
};
|
|
@@ -4691,11 +4834,32 @@ server.registerPrompt("mnemonic-workflow-hint", {
|
|
|
4691
4834
|
"- For repo-related tasks, pass `cwd` so mnemonic can route project memories correctly.\n\n" +
|
|
4692
4835
|
"Workflow: `recall`/`list` -> `get` -> `update` or `remember` -> `relate`/`consolidate`/`move_memory`. Use `discover_tags` only when tag choice is ambiguous.\n\n" +
|
|
4693
4836
|
"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" +
|
|
4837
|
+
"### Working-state continuity\n\n" +
|
|
4838
|
+
"Preserve in-progress work as temporary notes when continuation value is high. Recovery happens after project orientation.\n\n" +
|
|
4839
|
+
"**Checkpoint note structure (temporary notes):**\n" +
|
|
4840
|
+
"- Title pattern: 'WIP: <topic>' or 'Checkpoint: <description>'\n" +
|
|
4841
|
+
"- Opening paragraph: current status and next immediate step\n" +
|
|
4842
|
+
"- Body: what was attempted, what worked, blockers, alternatives considered\n" +
|
|
4843
|
+
"- End with explicit next action and confidence level\n\n" +
|
|
4844
|
+
"**Checkpoint note guidance:**\n" +
|
|
4845
|
+
"- One checkpoint per active task or investigation thread\n" +
|
|
4846
|
+
"- Update the same checkpoint note as work progresses (don't create new ones)\n" +
|
|
4847
|
+
"- Link to related decisions: use `relate` to connect temporary checkpoints to permanent decisions\n" +
|
|
4848
|
+
"- Consolidate into a durable note when complete; let lifecycle defaults delete temporary scaffolding unless you intentionally need preserved history\n\n" +
|
|
4849
|
+
"**Recovery workflow:**\n" +
|
|
4850
|
+
"- Call `project_memory_summary` first for orientation (do not skip to recovery)\n" +
|
|
4851
|
+
"- Use `lifecycle: temporary` for active plans, WIP checkpoints, draft investigations, and unvalidated options\n" +
|
|
4852
|
+
"- Use `lifecycle: permanent` for decisions, discovered constraints, bug causes, and reusable lessons\n" +
|
|
4853
|
+
"- After orientation, recover working-state from temporary notes via `recall` with lifecycle filter\n" +
|
|
4854
|
+
"- Consolidate temporary notes into durable ones once knowledge stabilizes\n" +
|
|
4855
|
+
"- Recovery is a follow-on step, not a replacement for orientation\n\n" +
|
|
4694
4856
|
"### Anti-patterns\n\n" +
|
|
4695
4857
|
"- Bad: call `remember` immediately because the user said 'remember'.\n" +
|
|
4696
4858
|
"- Good: `recall` or `list` first, then `get`, then `update` or `remember`.\n" +
|
|
4697
4859
|
"- Bad: create another note when `recall` or `list` already found the same decision.\n" +
|
|
4698
|
-
"- Good: `update` the existing memory and relate it if needed.\n
|
|
4860
|
+
"- Good: `update` the existing memory and relate it if needed.\n" +
|
|
4861
|
+
"- Bad: skip orientation and jump straight to working-state recovery.\n" +
|
|
4862
|
+
"- Good: `project_memory_summary` first, then recover temporary notes.\n\n" +
|
|
4699
4863
|
"### Storage model\n\n" +
|
|
4700
4864
|
"Memories can live in:\n" +
|
|
4701
4865
|
"- `main-vault` for global knowledge\n" +
|
|
@@ -4708,7 +4872,8 @@ server.registerPrompt("mnemonic-workflow-hint", {
|
|
|
4708
4872
|
"### Tiny examples\n\n" +
|
|
4709
4873
|
"- Existing bug note found by `recall` -> inspect with `get` -> refine with `update`.\n" +
|
|
4710
4874
|
"- No matching note found by `recall` -> optional `discover_tags` with note context -> create with `remember`.\n" +
|
|
4711
|
-
"- Two notes overlap heavily -> inspect -> clean up with `consolidate
|
|
4875
|
+
"- Two notes overlap heavily -> inspect -> clean up with `consolidate`.\n" +
|
|
4876
|
+
"- Resume work: `project_memory_summary` -> `recall` (lifecycle: temporary) -> continue from temporary notes.",
|
|
4712
4877
|
},
|
|
4713
4878
|
},
|
|
4714
4879
|
],
|