@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/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 top = selectRecallResults(scored, limit, scope);
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.project,
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.project,
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.project,
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.project,
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.projectName ?? note.project ?? "global"} | stored: ${note.vault} | lifecycle: ${note.lifecycle}`);
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.project,
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.project,
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
- const recent = [...entries]
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?.id, projectName: project?.name, count: 0, limit: limit || 5, notes: [] };
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.project,
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?.id,
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?.id, projectName: project?.name, nodes: [], limit, truncated: false };
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?.id, projectName: project?.name, nodes: [], limit, truncated: false };
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?.id,
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 && recent[0]) {
3134
- const fallbackNote = recent[0].note;
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: recent[0]?.note.id ?? projectEntries[0]?.note.id ?? "",
3157
- title: recent[0]?.note.title ?? projectEntries[0]?.note.title ?? "No notes",
3158
- rationale: recent[0]
3159
- ? "Most recent note — no high-centrality anchors found"
3160
- : "Only note available",
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?.id,
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?.id,
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?.id,
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?.id,
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?.id,
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?.id,
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?.id,
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?.id,
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?.id,
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?.id,
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?.id,
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?.id,
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?.id,
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\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
  ],