@danielmarbach/mnemonic-mcp 0.17.0 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +30 -0
- package/README.md +33 -1
- package/build/index.js +160 -62
- package/build/index.js.map +1 -1
- package/build/project-introspection.d.ts +17 -2
- package/build/project-introspection.d.ts.map +1 -1
- package/build/project-introspection.js +152 -4
- package/build/project-introspection.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 +272 -11
- package/build/structured-content.d.ts.map +1 -1
- package/build/structured-content.js +20 -1
- 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 +180 -0
- package/build/temporal-interpretation.js.map +1 -0
- package/build/theme-validation.d.ts +14 -0
- package/build/theme-validation.d.ts.map +1 -0
- package/build/theme-validation.js +46 -0
- package/build/theme-validation.js.map +1 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,36 @@ All notable changes to `mnemonic` will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
The format is loosely based on Keep a Changelog and uses semver-style version headings.
|
|
6
6
|
|
|
7
|
+
## [0.19.0] - 2026-03-28
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Role and importance inference (`src/role-suggestions.ts`): notes are automatically assigned a suggested `role` and `importance` from structural signals; inference is language-independent and never overwrites explicit frontmatter.
|
|
12
|
+
- `alwaysLoad: true` in note frontmatter marks a note as an explicit session anchor with highest recall and relationship-expansion priority.
|
|
13
|
+
- `recall` and relationship expansion now factor in effective role and importance, so summary and decision notes surface higher without manual tagging.
|
|
14
|
+
- Temporal recall history entries now include a `changeDescription` and notes include a `historySummary` capturing the overall evolution pattern; classification uses structural signals only and degrades gracefully when stats are unavailable.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- The workflow hint and user-facing docs now state that roles are optional prioritization hints, inferred roles stay internal-only, lifecycle remains the durability control, and default prioritization is language-independent.
|
|
19
|
+
|
|
20
|
+
## [0.18.0] - 2026-03-27
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
|
|
24
|
+
- Theme graduation from "other": keywords appearing across multiple notes become named themes automatically.
|
|
25
|
+
- Keyword extraction with synonym normalization for theme classification.
|
|
26
|
+
- `analyzeThemeQuality()` utility for checking theme distribution metrics.
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
|
|
30
|
+
- All 23 MCP tools now include complete SDK tool hints (`readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint`). Previously, read-only tools were missing `destructiveHint: false`.
|
|
31
|
+
- Mutating tool retry results now return explicit recovery guidance instead of vague `Retry: safe` text, including recovery kind and no-inference instructions for partial-persistence failures.
|
|
32
|
+
- Recovery precedence is now encoded in retry metadata: deterministic tool reconciliation is preferred where available, and same-vault retries must be replayed serially.
|
|
33
|
+
- Retry metadata now uses `attemptedCommit.subject` instead of `attemptedCommit.message`.
|
|
34
|
+
- `classifyTheme` now uses keyword-based graduation in addition to tag matching.
|
|
35
|
+
- `project_memory_summary` includes both fixed themes and dynamically promoted keywords.
|
|
36
|
+
|
|
7
37
|
## [0.17.0] - 2026-03-25
|
|
8
38
|
|
|
9
39
|
### 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
|
|
|
@@ -452,6 +470,18 @@ Imported notes are written to the main vault with `lifecycle: permanent` and `sc
|
|
|
452
470
|
| `update` | Update note content/title/tags/lifecycle, re-embeds always |
|
|
453
471
|
| `where_is_memory` | Show note's project association and storage location |
|
|
454
472
|
|
|
473
|
+
### Theme emergence
|
|
474
|
+
|
|
475
|
+
`project_memory_summary` categorizes notes by theme. Themes **emerge automatically** from your notes:
|
|
476
|
+
|
|
477
|
+
- **Tag-based classification** — notes with matching tags (e.g., `["decisions"]`, `["bugs"]`) are grouped immediately
|
|
478
|
+
- **Keyword graduation** — keywords that appear across multiple notes become named themes over time
|
|
479
|
+
- **"other" bucket** — notes that don't match any theme are grouped here; this shrinks as themes emerge
|
|
480
|
+
|
|
481
|
+
No predefined schema required. The system adapts to your project's vocabulary.
|
|
482
|
+
|
|
483
|
+
**Language handling:** The system degrades gracefully for non-English notes. Stopwords and synonyms are optional English enhancements; keywords that don't match pass through unchanged, allowing non-English keywords to graduate if they meet frequency thresholds.
|
|
484
|
+
|
|
455
485
|
### Relationships
|
|
456
486
|
|
|
457
487
|
Notes can be linked with typed edges stored in frontmatter:
|
|
@@ -544,6 +574,8 @@ mnemonic and Beads address complementary concerns. mnemonic is a **knowledge gra
|
|
|
544
574
|
|
|
545
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.
|
|
546
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
|
+
|
|
547
579
|
## Contributing
|
|
548
580
|
|
|
549
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, 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
|
}
|
|
@@ -693,9 +696,22 @@ function buildMutationRetryContract(args) {
|
|
|
693
696
|
if (args.commit.status !== "failed") {
|
|
694
697
|
return undefined;
|
|
695
698
|
}
|
|
699
|
+
const recoveryKind = args.preferredRecovery ?? (args.mutationApplied
|
|
700
|
+
? "manual-exact-git-recovery"
|
|
701
|
+
: "no-manual-recovery");
|
|
702
|
+
const recoveryReason = recoveryKind === "rerun-tool-call-serial"
|
|
703
|
+
? "Tool-level reconciliation exists for this mutation; rerun the same tool call serially for the affected vault."
|
|
704
|
+
: recoveryKind === "manual-exact-git-recovery"
|
|
705
|
+
? "Mutation is already persisted on disk; manual git recovery is allowed only with the exact attemptedCommit values."
|
|
706
|
+
: "Mutation was not applied deterministically; manual git recovery is not authorized.";
|
|
696
707
|
return {
|
|
708
|
+
recovery: {
|
|
709
|
+
kind: recoveryKind,
|
|
710
|
+
allowed: recoveryKind !== "no-manual-recovery",
|
|
711
|
+
reason: recoveryReason,
|
|
712
|
+
},
|
|
697
713
|
attemptedCommit: {
|
|
698
|
-
|
|
714
|
+
subject: args.commitMessage,
|
|
699
715
|
body: args.commitBody,
|
|
700
716
|
files: args.files,
|
|
701
717
|
cwd: args.cwd,
|
|
@@ -708,19 +724,69 @@ function buildMutationRetryContract(args) {
|
|
|
708
724
|
rationale: args.mutationApplied
|
|
709
725
|
? "Mutation is already persisted on disk; commit can be retried deterministically."
|
|
710
726
|
: "Mutation was not applied; retry may require re-running the operation.",
|
|
727
|
+
instructions: {
|
|
728
|
+
sourceOfTruth: recoveryKind === "manual-exact-git-recovery" ? "attemptedCommit" : "tool-response",
|
|
729
|
+
useExactSubject: recoveryKind === "manual-exact-git-recovery",
|
|
730
|
+
useExactBody: recoveryKind === "manual-exact-git-recovery",
|
|
731
|
+
useExactFiles: recoveryKind === "manual-exact-git-recovery",
|
|
732
|
+
forbidInferenceFromHistory: true,
|
|
733
|
+
forbidInferenceFromTitleOrSummary: true,
|
|
734
|
+
forbidParallelSameVaultRetries: true,
|
|
735
|
+
preferToolReconciliation: recoveryKind === "rerun-tool-call-serial",
|
|
736
|
+
rerunSameToolCallSerially: recoveryKind === "rerun-tool-call-serial",
|
|
737
|
+
},
|
|
711
738
|
};
|
|
712
739
|
}
|
|
713
740
|
function formatRetrySummary(retry) {
|
|
714
741
|
if (!retry) {
|
|
715
742
|
return undefined;
|
|
716
743
|
}
|
|
717
|
-
const safety = retry.retrySafe ? "safe" : "requires review";
|
|
718
744
|
const opLabel = retry.attemptedCommit.operation === "add" ? "add" : "commit";
|
|
719
745
|
const error = retry.attemptedCommit.error;
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
746
|
+
const lines = [];
|
|
747
|
+
switch (retry.recovery.kind) {
|
|
748
|
+
case "rerun-tool-call-serial":
|
|
749
|
+
lines.push("Recovery: rerun same tool call serially");
|
|
750
|
+
lines.push(retry.recovery.reason);
|
|
751
|
+
lines.push("Rerun the same mnemonic tool call one time for the affected vault.");
|
|
752
|
+
lines.push("Do not replay same-vault mutations in parallel.");
|
|
753
|
+
lines.push("Manual git recovery is not authorized for this failure.");
|
|
754
|
+
lines.push("Git failure:");
|
|
755
|
+
lines.push(`${opLabel}: ${error}`);
|
|
756
|
+
break;
|
|
757
|
+
case "manual-exact-git-recovery":
|
|
758
|
+
lines.push("Recovery: manual exact git recovery allowed");
|
|
759
|
+
lines.push(retry.recovery.reason);
|
|
760
|
+
lines.push("Use only the exact values below. Do not infer from git history, note title, summary, or repo state.");
|
|
761
|
+
lines.push("");
|
|
762
|
+
lines.push("Commit subject:");
|
|
763
|
+
lines.push(retry.attemptedCommit.subject);
|
|
764
|
+
if (retry.attemptedCommit.body) {
|
|
765
|
+
lines.push("");
|
|
766
|
+
lines.push("Commit body:");
|
|
767
|
+
lines.push(retry.attemptedCommit.body);
|
|
768
|
+
}
|
|
769
|
+
lines.push("");
|
|
770
|
+
lines.push("Files:");
|
|
771
|
+
for (const file of retry.attemptedCommit.files) {
|
|
772
|
+
lines.push(`- ${file}`);
|
|
773
|
+
}
|
|
774
|
+
lines.push("");
|
|
775
|
+
lines.push("Git failure:");
|
|
776
|
+
lines.push(`${opLabel}: ${error}`);
|
|
777
|
+
break;
|
|
778
|
+
case "no-manual-recovery":
|
|
779
|
+
lines.push("Recovery: no manual recovery authorized");
|
|
780
|
+
lines.push(retry.recovery.reason);
|
|
781
|
+
lines.push("Git failure:");
|
|
782
|
+
lines.push(`${opLabel}: ${error}`);
|
|
783
|
+
break;
|
|
784
|
+
default: {
|
|
785
|
+
const _exhaustive = retry.recovery.kind;
|
|
786
|
+
throw new Error(`Unknown recovery kind: ${_exhaustive}`);
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
return lines.join("\n");
|
|
724
790
|
}
|
|
725
791
|
function formatPersistenceSummary(persistence) {
|
|
726
792
|
const parts = [
|
|
@@ -731,14 +797,14 @@ function formatPersistenceSummary(persistence) {
|
|
|
731
797
|
if (persistence.embedding.reason) {
|
|
732
798
|
lines[0] += ` | embedding reason=${persistence.embedding.reason}`;
|
|
733
799
|
}
|
|
734
|
-
|
|
800
|
+
const retrySummary = formatRetrySummary(persistence.retry);
|
|
801
|
+
if (!retrySummary && persistence.git.commit === "failed" && persistence.git.commitError) {
|
|
735
802
|
const opLabel = persistence.git.commitOperation === "add" ? "add" : "commit";
|
|
736
803
|
lines.push(`Git ${opLabel} error: ${persistence.git.commitError}`);
|
|
737
804
|
}
|
|
738
805
|
if (persistence.git.push === "failed" && persistence.git.pushError) {
|
|
739
806
|
lines.push(`Git push error: ${persistence.git.pushError}`);
|
|
740
807
|
}
|
|
741
|
-
const retrySummary = formatRetrySummary(persistence.retry);
|
|
742
808
|
if (retrySummary) {
|
|
743
809
|
lines.push(retrySummary);
|
|
744
810
|
}
|
|
@@ -969,6 +1035,7 @@ server.registerTool("detect_project", {
|
|
|
969
1035
|
"- Use `recall` or `project_memory_summary` to orient on existing memory.",
|
|
970
1036
|
annotations: {
|
|
971
1037
|
readOnlyHint: true,
|
|
1038
|
+
destructiveHint: false,
|
|
972
1039
|
idempotentHint: true,
|
|
973
1040
|
openWorldHint: false,
|
|
974
1041
|
},
|
|
@@ -1023,6 +1090,7 @@ server.registerTool("get_project_identity", {
|
|
|
1023
1090
|
"- Use `set_project_identity` only if the wrong remote is defining identity.",
|
|
1024
1091
|
annotations: {
|
|
1025
1092
|
readOnlyHint: true,
|
|
1093
|
+
destructiveHint: false,
|
|
1026
1094
|
idempotentHint: true,
|
|
1027
1095
|
openWorldHint: false,
|
|
1028
1096
|
},
|
|
@@ -1179,6 +1247,7 @@ server.registerTool("list_migrations", {
|
|
|
1179
1247
|
"- Run `execute_migration` with `dryRun: true` first.",
|
|
1180
1248
|
annotations: {
|
|
1181
1249
|
readOnlyHint: true,
|
|
1250
|
+
destructiveHint: false,
|
|
1182
1251
|
idempotentHint: true,
|
|
1183
1252
|
openWorldHint: false,
|
|
1184
1253
|
},
|
|
@@ -1600,6 +1669,7 @@ server.registerTool("get_project_memory_policy", {
|
|
|
1600
1669
|
"- Call `remember` with explicit `scope` for a one-off override, or `set_project_memory_policy` to change defaults.",
|
|
1601
1670
|
annotations: {
|
|
1602
1671
|
readOnlyHint: true,
|
|
1672
|
+
destructiveHint: false,
|
|
1603
1673
|
idempotentHint: true,
|
|
1604
1674
|
openWorldHint: false,
|
|
1605
1675
|
},
|
|
@@ -1673,6 +1743,7 @@ server.registerTool("recall", {
|
|
|
1673
1743
|
"- Use `get`, `update`, `relate`, or `consolidate` based on the results.",
|
|
1674
1744
|
annotations: {
|
|
1675
1745
|
readOnlyHint: true,
|
|
1746
|
+
destructiveHint: false,
|
|
1676
1747
|
idempotentHint: true,
|
|
1677
1748
|
openWorldHint: true,
|
|
1678
1749
|
},
|
|
@@ -1749,7 +1820,8 @@ server.registerTool("recall", {
|
|
|
1749
1820
|
if (isProjectNote)
|
|
1750
1821
|
continue;
|
|
1751
1822
|
}
|
|
1752
|
-
const
|
|
1823
|
+
const metadataBoost = computeRecallMetadataBoost(getEffectiveMetadata(note));
|
|
1824
|
+
const boost = (isCurrentProject ? 0.15 : 0) + metadataBoost;
|
|
1753
1825
|
scored.push({ id: rec.id, score: rawScore, boosted: rawScore + boost, vault, isCurrentProject: Boolean(isCurrentProject) });
|
|
1754
1826
|
}
|
|
1755
1827
|
}
|
|
@@ -1774,13 +1846,17 @@ server.registerTool("recall", {
|
|
|
1774
1846
|
const provenance = await getNoteProvenance(vault.git, filePath);
|
|
1775
1847
|
const confidence = computeConfidence(note.lifecycle, note.updatedAt, centrality);
|
|
1776
1848
|
let history;
|
|
1849
|
+
let historySummary;
|
|
1777
1850
|
if (mode === "temporal") {
|
|
1778
1851
|
if (index < TEMPORAL_HISTORY_NOTE_LIMIT) {
|
|
1779
1852
|
const commits = await vault.git.getFileHistory(filePath, TEMPORAL_HISTORY_COMMIT_LIMIT);
|
|
1780
|
-
|
|
1853
|
+
const rawHistory = await Promise.all(commits.map(async (commit) => {
|
|
1781
1854
|
const stats = await vault.git.getCommitStats(filePath, commit.hash);
|
|
1782
1855
|
return buildTemporalHistoryEntry(commit, stats, verbose);
|
|
1783
1856
|
}));
|
|
1857
|
+
const enriched = enrichTemporalHistory(rawHistory);
|
|
1858
|
+
history = enriched.interpretedHistory;
|
|
1859
|
+
historySummary = enriched.historySummary;
|
|
1784
1860
|
}
|
|
1785
1861
|
}
|
|
1786
1862
|
// Add relationship preview for top N results (fail-soft)
|
|
@@ -1810,6 +1886,7 @@ server.registerTool("recall", {
|
|
|
1810
1886
|
provenance,
|
|
1811
1887
|
confidence,
|
|
1812
1888
|
history,
|
|
1889
|
+
historySummary,
|
|
1813
1890
|
relationships,
|
|
1814
1891
|
});
|
|
1815
1892
|
}
|
|
@@ -2107,6 +2184,7 @@ server.registerTool("get", {
|
|
|
2107
2184
|
"- Use `update`, `forget`, `move_memory`, or `relate` after inspection.",
|
|
2108
2185
|
annotations: {
|
|
2109
2186
|
readOnlyHint: true,
|
|
2187
|
+
destructiveHint: false,
|
|
2110
2188
|
idempotentHint: true,
|
|
2111
2189
|
openWorldHint: false,
|
|
2112
2190
|
},
|
|
@@ -2204,6 +2282,7 @@ server.registerTool("where_is_memory", {
|
|
|
2204
2282
|
"- Use `move_memory` if the storage location is wrong, or `get` for full inspection.",
|
|
2205
2283
|
annotations: {
|
|
2206
2284
|
readOnlyHint: true,
|
|
2285
|
+
destructiveHint: false,
|
|
2207
2286
|
idempotentHint: true,
|
|
2208
2287
|
openWorldHint: false,
|
|
2209
2288
|
},
|
|
@@ -2260,6 +2339,7 @@ server.registerTool("list", {
|
|
|
2260
2339
|
"- Use `get` for exact inspection or `update` / `consolidate` for cleanup.",
|
|
2261
2340
|
annotations: {
|
|
2262
2341
|
readOnlyHint: true,
|
|
2342
|
+
destructiveHint: false,
|
|
2263
2343
|
idempotentHint: true,
|
|
2264
2344
|
openWorldHint: false,
|
|
2265
2345
|
},
|
|
@@ -2370,6 +2450,7 @@ server.registerTool("discover_tags", {
|
|
|
2370
2450
|
"Read-only.",
|
|
2371
2451
|
annotations: {
|
|
2372
2452
|
readOnlyHint: true,
|
|
2453
|
+
destructiveHint: false,
|
|
2373
2454
|
idempotentHint: true,
|
|
2374
2455
|
openWorldHint: false,
|
|
2375
2456
|
},
|
|
@@ -2576,6 +2657,7 @@ server.registerTool("recent_memories", {
|
|
|
2576
2657
|
"- Use `get` for exact inspection or `update` to continue refining a recent note.",
|
|
2577
2658
|
annotations: {
|
|
2578
2659
|
readOnlyHint: true,
|
|
2660
|
+
destructiveHint: false,
|
|
2579
2661
|
idempotentHint: true,
|
|
2580
2662
|
openWorldHint: false,
|
|
2581
2663
|
},
|
|
@@ -2645,6 +2727,7 @@ server.registerTool("memory_graph", {
|
|
|
2645
2727
|
"- Use `get`, `relate`, `unrelate`, or `consolidate` based on what the graph reveals.",
|
|
2646
2728
|
annotations: {
|
|
2647
2729
|
readOnlyHint: true,
|
|
2730
|
+
destructiveHint: false,
|
|
2648
2731
|
idempotentHint: true,
|
|
2649
2732
|
openWorldHint: false,
|
|
2650
2733
|
},
|
|
@@ -2724,6 +2807,7 @@ server.registerTool("project_memory_summary", {
|
|
|
2724
2807
|
"- Use `recall` or `list` to drill down into specific areas.",
|
|
2725
2808
|
annotations: {
|
|
2726
2809
|
readOnlyHint: true,
|
|
2810
|
+
destructiveHint: false,
|
|
2727
2811
|
idempotentHint: true,
|
|
2728
2812
|
openWorldHint: false,
|
|
2729
2813
|
},
|
|
@@ -2764,18 +2848,50 @@ server.registerTool("project_memory_summary", {
|
|
|
2764
2848
|
return { content: [{ type: "text", text: `No memories found for project ${project.name}.` }], structuredContent };
|
|
2765
2849
|
}
|
|
2766
2850
|
const policyLine = await formatProjectPolicyLine(project.id);
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2851
|
+
const projectNoteIds = new Set(projectEntries.map(e => e.note.id));
|
|
2852
|
+
// Compute promoted themes from keywords (graduation system)
|
|
2853
|
+
const graduationResult = computeThemesWithGraduation(projectEntries.map(e => e.note));
|
|
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
|
+
}));
|
|
2883
|
+
// Categorize by theme with graduation (project-scoped only)
|
|
2770
2884
|
const themed = new Map();
|
|
2771
2885
|
for (const entry of projectEntries) {
|
|
2772
|
-
const theme =
|
|
2886
|
+
const theme = classifyThemeWithGraduation(entry.note, promotedThemes);
|
|
2773
2887
|
const bucket = themed.get(theme) ?? [];
|
|
2774
2888
|
bucket.push(entry);
|
|
2775
2889
|
themed.set(theme, bucket);
|
|
2776
2890
|
}
|
|
2777
|
-
// Theme order
|
|
2778
|
-
const
|
|
2891
|
+
// Theme order: fixed themes first, then promoted themes alphabetically, then "other"
|
|
2892
|
+
const fixedThemes = ["overview", "decisions", "tooling", "bugs", "architecture", "quality"];
|
|
2893
|
+
const dynamicThemes = graduationResult.promotedThemes.filter(t => !fixedThemes.includes(t));
|
|
2894
|
+
const themeOrder = [...fixedThemes, ...dynamicThemes.sort(), "other"];
|
|
2779
2895
|
// Calculate notes distribution (project-scoped only)
|
|
2780
2896
|
const projectVaultCount = projectEntries.filter(e => e.vault.isProject).length;
|
|
2781
2897
|
const mainVaultProjectEntries = projectEntries.filter(e => !e.vault.isProject);
|
|
@@ -2796,7 +2912,7 @@ server.registerTool("project_memory_summary", {
|
|
|
2796
2912
|
if (!bucket || bucket.length === 0)
|
|
2797
2913
|
continue;
|
|
2798
2914
|
// Sort by within-theme score
|
|
2799
|
-
const sorted = [...bucket].sort((a, b) => withinThemeScore(b.note) - withinThemeScore(a.note));
|
|
2915
|
+
const sorted = [...bucket].sort((a, b) => withinThemeScore(b.note, effectiveMetadataById.get(b.note.id)?.metadata) - withinThemeScore(a.note, effectiveMetadataById.get(a.note.id)?.metadata));
|
|
2800
2916
|
const top = sorted.slice(0, maxPerTheme);
|
|
2801
2917
|
sections.push(`\n${titleCaseTheme(theme)}:`);
|
|
2802
2918
|
sections.push(...top.map(e => `- ${e.note.title} (\`${e.note.id}\`)`));
|
|
@@ -2816,49 +2932,32 @@ server.registerTool("project_memory_summary", {
|
|
|
2816
2932
|
sections.push(`\nRecent activity (start here):`);
|
|
2817
2933
|
sections.push(...recent.map(e => `- ${e.note.updatedAt} — ${e.note.title}`));
|
|
2818
2934
|
// Anchor notes with diversity constraint (project-scoped only)
|
|
2819
|
-
// Separate tagged anchors (can have no relationships) from scored anchors (need relationships)
|
|
2820
|
-
const taggedAnchorEntries = projectEntries.filter(e => e.note.lifecycle === "permanent" &&
|
|
2821
|
-
e.note.tags.some(t => t.toLowerCase() === "anchor" || t.toLowerCase() === "alwaysload"));
|
|
2822
2935
|
const scoredAnchorCandidates = projectEntries
|
|
2823
|
-
.
|
|
2824
|
-
.
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
.
|
|
2936
|
+
.map(e => {
|
|
2937
|
+
const baselineContext = effectiveMetadataById.get(e.note.id);
|
|
2938
|
+
const metadata = baselineContext?.metadata;
|
|
2939
|
+
return {
|
|
2940
|
+
entry: e,
|
|
2941
|
+
metadata,
|
|
2942
|
+
score: anchorScore(e.note, themeCache, metadata),
|
|
2943
|
+
theme: themeCache.get(e.note.id) ?? "other",
|
|
2944
|
+
alwaysLoad: metadata?.alwaysLoad === true,
|
|
2945
|
+
explicitOrientationRole: metadata?.roleSource === "explicit" &&
|
|
2946
|
+
(metadata.role === "summary" || metadata.role === "decision"),
|
|
2947
|
+
hasVisibleGraphParticipation: (baselineContext?.visibleOutbound ?? 0) > 0 || (baselineContext?.inbound ?? 0) > 0,
|
|
2948
|
+
};
|
|
2949
|
+
})
|
|
2950
|
+
.filter(candidate => candidate.score > -Infinity)
|
|
2951
|
+
.filter(candidate => candidate.alwaysLoad || candidate.explicitOrientationRole || candidate.hasVisibleGraphParticipation)
|
|
2952
|
+
.sort((a, b) => b.score - a.score || a.entry.note.title.localeCompare(b.entry.note.title));
|
|
2839
2953
|
// Enforce max 2 per theme for scored anchors
|
|
2840
2954
|
const anchorThemeCounts = new Map();
|
|
2841
2955
|
const anchors = [];
|
|
2842
2956
|
const anchorIds = new Set();
|
|
2843
|
-
// Add tagged anchors first (capped at 10 total across all themes), scored by anchorScore
|
|
2844
|
-
for (const candidate of scoredTaggedAnchors.slice(0, 10)) {
|
|
2845
|
-
if (anchors.length >= 10)
|
|
2846
|
-
break;
|
|
2847
|
-
anchors.push({
|
|
2848
|
-
id: candidate.entry.note.id,
|
|
2849
|
-
title: candidate.entry.note.title,
|
|
2850
|
-
centrality: candidate.entry.note.relatedTo?.length ?? 0,
|
|
2851
|
-
connectionDiversity: computeConnectionDiversity(candidate.entry.note, themeCache),
|
|
2852
|
-
updatedAt: candidate.entry.note.updatedAt,
|
|
2853
|
-
});
|
|
2854
|
-
anchorIds.add(candidate.entry.note.id);
|
|
2855
|
-
}
|
|
2856
2957
|
// Add scored anchors with theme diversity constraint
|
|
2857
2958
|
for (const candidate of scoredAnchorCandidates) {
|
|
2858
2959
|
if (anchors.length >= 10)
|
|
2859
2960
|
break;
|
|
2860
|
-
if (anchorIds.has(candidate.entry.note.id))
|
|
2861
|
-
continue;
|
|
2862
2961
|
const theme = candidate.theme;
|
|
2863
2962
|
const themeCount = anchorThemeCounts.get(theme) ?? 0;
|
|
2864
2963
|
if (themeCount >= 2)
|
|
@@ -3051,7 +3150,7 @@ server.registerTool("project_memory_summary", {
|
|
|
3051
3150
|
id: e.note.id,
|
|
3052
3151
|
title: e.note.title,
|
|
3053
3152
|
updatedAt: e.note.updatedAt,
|
|
3054
|
-
theme:
|
|
3153
|
+
theme: classifyThemeWithGraduation(e.note, promotedThemes),
|
|
3055
3154
|
})),
|
|
3056
3155
|
anchors,
|
|
3057
3156
|
orientation,
|
|
@@ -3403,6 +3502,7 @@ server.registerTool("relate", {
|
|
|
3403
3502
|
cwd,
|
|
3404
3503
|
vault,
|
|
3405
3504
|
mutationApplied: true,
|
|
3505
|
+
preferredRecovery: "rerun-tool-call-serial",
|
|
3406
3506
|
});
|
|
3407
3507
|
const structuredContent = {
|
|
3408
3508
|
action: "related",
|
|
@@ -3452,6 +3552,7 @@ server.registerTool("relate", {
|
|
|
3452
3552
|
cwd,
|
|
3453
3553
|
vault,
|
|
3454
3554
|
mutationApplied: true,
|
|
3555
|
+
preferredRecovery: "rerun-tool-call-serial",
|
|
3455
3556
|
});
|
|
3456
3557
|
}
|
|
3457
3558
|
if (commitStatus.status === "committed") {
|
|
@@ -3574,6 +3675,7 @@ server.registerTool("unrelate", {
|
|
|
3574
3675
|
cwd,
|
|
3575
3676
|
vault,
|
|
3576
3677
|
mutationApplied: true,
|
|
3678
|
+
preferredRecovery: "rerun-tool-call-serial",
|
|
3577
3679
|
});
|
|
3578
3680
|
const structuredContent = {
|
|
3579
3681
|
action: "unrelated",
|
|
@@ -3617,6 +3719,7 @@ server.registerTool("unrelate", {
|
|
|
3617
3719
|
cwd,
|
|
3618
3720
|
vault,
|
|
3619
3721
|
mutationApplied: true,
|
|
3722
|
+
preferredRecovery: "rerun-tool-call-serial",
|
|
3620
3723
|
});
|
|
3621
3724
|
}
|
|
3622
3725
|
if (commitStatus.status === "committed") {
|
|
@@ -4512,18 +4615,13 @@ server.registerPrompt("mnemonic-workflow-hint", {
|
|
|
4512
4615
|
type: "text",
|
|
4513
4616
|
text: "## Mnemonic MCP workflow hints\n\n" +
|
|
4514
4617
|
"Avoid duplicate memories. Prefer inspecting and updating existing memories before creating new ones.\n\n" +
|
|
4515
|
-
"### Hard rules\n\n" +
|
|
4516
4618
|
"- REQUIRES: Before `remember`, call `recall` or `list` first.\n" +
|
|
4517
4619
|
"- If `recall` or `list` returns a plausible match, call `get` before deciding whether to `update` or `remember`.\n" +
|
|
4518
4620
|
"- If an existing memory already covers the topic, use `update`, not `remember`.\n" +
|
|
4519
4621
|
"- When unsure, prefer `recall` over `remember`.\n" +
|
|
4520
4622
|
"- For repo-related tasks, pass `cwd` so mnemonic can route project memories correctly.\n\n" +
|
|
4521
|
-
"
|
|
4522
|
-
"
|
|
4523
|
-
"2. If `recall` or `list` returns matching ids, use `get` to inspect the best match.\n" +
|
|
4524
|
-
"3. If one memory should be refined, call `update`.\n" +
|
|
4525
|
-
"4. If no memory covers the topic and tag choice is ambiguous, call `discover_tags` with note context, then call `remember`.\n" +
|
|
4526
|
-
"5. After storing or updating, use `relate` for strong connections, `consolidate` for overlap, and `move_memory` for wrong storage location.\n\n" +
|
|
4623
|
+
"Workflow: `recall`/`list` -> `get` -> `update` or `remember` -> `relate`/`consolidate`/`move_memory`. Use `discover_tags` only when tag choice is ambiguous.\n\n" +
|
|
4624
|
+
"Roles are optional prioritization hints, not schema. Lifecycle still governs durability. `role: plan` does not imply `temporary`. Inferred roles are internal hints only. Prioritization is language-independent by default.\n\n" +
|
|
4527
4625
|
"### Anti-patterns\n\n" +
|
|
4528
4626
|
"- Bad: call `remember` immediately because the user said 'remember'.\n" +
|
|
4529
4627
|
"- Good: `recall` or `list` first, then `get`, then `update` or `remember`.\n" +
|