@mnemoverse/mcp-memory-server 0.8.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/teaching.js CHANGED
@@ -19,57 +19,151 @@
19
19
  * - Active polarity: no "only when the user explicitly asks" gating.
20
20
  * - Mentions every tool family: read/write/stats/feedback, delete, rooms, vault.
21
21
  */
22
- export const SERVER_INSTRUCTIONS = "You own this long-term memory. It persists across sessions and every AI tool this user connects (Claude, ChatGPT, editors). Use it as a habit: call memory_read before answering anything that may have come up before, and memory_write the moment you learn a durable fact, preference, or decision — don't wait to be asked. Rate recalls with memory_feedback so good ones surface faster; memory_stats shows counts; memory_list_recent: newest first; prune with memory_delete; memory_delete_domain wipes a domain, only with user go-ahead. Rooms (memory_create_room, memory_invite_to_room, memory_join_room, memory_list_rooms) share memory with others; vault_list names stored secrets by alias, never values. Never store passwords, API keys, payment data, MFA codes, government IDs, or health records.";
22
+ export const SERVER_INSTRUCTIONS = "You own this long-term memory. It persists across sessions and every AI tool this user connects. Use it as a habit: memory_read before answering anything that may have come up; memory_write the moment you learn a durable fact, preference or decision — don't wait to be asked. Shared rooms are SEPARATE stores: to read one, pass its address as domain (memory_list_rooms); unscoped reads never cover rooms. Rate recalls with memory_feedback; memory_stats shows counts; memory_list_recent is newest-first; memory_delete prunes; memory_delete_domain wipes a domain, only with user go-ahead. Rooms: memory_create_room, memory_invite_to_room, memory_join_room. vault_list names secrets by alias, never values. Never store passwords, API keys, payment data, MFA codes, government IDs, or health records.";
23
23
  /** The pre-existing zero-result message — kept as the fail-open fallback. */
24
24
  export const NO_MATCH_MESSAGE = "No memories found for this query.";
25
25
  /** Hint for an UNSCOPED no-match against a non-empty store: widen the query.
26
26
  * (No drop-the-filter clause — none was set; advising to remove a filter that
27
27
  * does not exist nudges the model into confabulating state.) */
28
28
  export const NO_MATCH_HINT = " Try a broader query.";
29
- /** Hint for a DOMAIN-SCOPED no-match: here a filter genuinely exists, so
30
- * suggesting to drop it is honest and actionable. */
31
- export const NO_MATCH_SCOPED_HINT = " Try a broader query, or drop the domain filter to search all domains.";
32
29
  /**
33
- * First-contact greeting: shown ONLY when a read comes back empty AND the
34
- * store holds zero memories — i.e. the very first read of this account's life.
35
- * Seeds the ANSWER, not the store: one functional paragraph that says what
36
- * this store is, how to save the first memory, and one next step. It can never
37
- * appear again once anything is stored (total_atoms > 0 takes the other branch).
30
+ * Hint for a DOMAIN-SCOPED no-match: widen the query, or try another domain —
31
+ * and NEVER drop the scope.
32
+ *
33
+ * This doc used to read "here a filter genuinely exists, so suggesting to drop
34
+ * it is honest and actionable", which described a draft that no longer exists:
35
+ * the clause "…or drop the domain filter to search all domains" was deleted, and
36
+ * a test now forbids it. The advice was actively harmful when the domain is a
37
+ * room — an unscoped read does not cover rooms at all, so dropping the filter is
38
+ * the one move guaranteed to lose the content the reader is hunting, and it is
39
+ * the step that cost two agents a day on 2026-08-07.
40
+ */
41
+ export const NO_MATCH_SCOPED_HINT = " Try a broader query, or a different domain.";
42
+ /**
43
+ * The honest line for an account whose OWN domains are empty but whose rooms
44
+ * are not — an invited teammate whose every memory lives in shared rooms.
45
+ *
46
+ * Without this, such a caller was greeted with EMPTY_STORE_WELCOME: "your
47
+ * memory is empty, nothing has been saved yet, which is why this search
48
+ * returned nothing" — three false clauses, immediately contradicted by the
49
+ * scope note appended underneath, in the same payload (review, 2026-08-08).
50
+ * That reader is precisely the person from the incident this release is about.
51
+ *
52
+ * ENDS WITH A COLON, so it is only ever emitted together with the disclosure
53
+ * that follows it. That pairing used to be two independent decisions and they
54
+ * disagreed: this line was chosen from a flag counting ALL rooms while the
55
+ * disclosure was built from the LIVE ones, so an account whose only room was
56
+ * archived received the colon and nothing after it. Both now come out of the
57
+ * same {@link RoomScope} arm, and the arm that has nothing to disclose has no
58
+ * `note` field to forget.
59
+ */
60
+ export const EMPTY_PERSONAL_STORE_WITH_ROOMS = "Nothing in your own domains — they hold no memories yet. That is not the whole picture:";
61
+ /**
62
+ * The same situation with the room probe UNANSWERED. Says the one true thing —
63
+ * the personal store measured zero and the rest could not be checked — and
64
+ * deliberately does NOT say "nothing has been saved yet", which was reachable
65
+ * here and is a claim about a scope this client failed to reach.
66
+ */
67
+ export const EMPTY_PERSONAL_STORE_ROOMS_UNCHECKED = "Nothing in your own domains — they hold no memories yet. Whether that is the whole picture could not be checked:";
68
+ /**
69
+ * First-contact greeting: shown ONLY when a read comes back empty, the personal
70
+ * store holds zero memories, AND the room probe answered that there are no rooms
71
+ * — i.e. the very first read of this account's life, established rather than
72
+ * assumed. Seeds the ANSWER, not the store: one functional paragraph that says
73
+ * what this store is, how to save the first memory, and one next step.
38
74
  */
39
75
  export const EMPTY_STORE_WELCOME = "Your long-term memory is empty — nothing has been saved yet, which is why this search returned nothing. " +
40
76
  "This store is your own persistent memory: whatever you save survives across sessions and across every AI tool this user has connected. " +
41
77
  'Save the first memory now with memory_write, e.g. content: "User prefers TypeScript strict mode" — future sessions will recall it with memory_read.';
42
78
  /**
43
- * Decide what a zero-result memory_read should say.
79
+ * Compile-time exhaustiveness. Adding a state to `RoomScope` / `NamedScope`
80
+ * without answering for it here is a type error, which is the point: the state
81
+ * this release exists to fix ("we could not check") was reachable precisely
82
+ * because it had no arm of its own and fell into the falsy one. Returns the
83
+ * neutral message rather than throwing, so an unreachable branch can never take
84
+ * a handler down.
85
+ */
86
+ function noStateLeftUnhandled(state) {
87
+ void state;
88
+ return NO_MATCH_MESSAGE;
89
+ }
90
+ /** The disclosure a room state owes the reader — "" only for the state whose
91
+ * type says there is nothing to disclose. */
92
+ function roomsTail(rooms) {
93
+ return "note" in rooms ? rooms.note : "";
94
+ }
95
+ /**
96
+ * Assemble the WHOLE answer for a zero-result read: the head sentence and the
97
+ * disclosure that belongs with it, in one place.
98
+ *
99
+ * They are assembled together on purpose. While the head came from here and the
100
+ * tail was concatenated by the caller, the two could be — and were — derived
101
+ * from different facts: a head promising "that is not the whole picture:" with
102
+ * an empty tail after it, and a first-contact greeting under a tail admitting
103
+ * the room list could not be fetched. Every arm below returns a complete answer,
104
+ * so a head cannot outlive the evidence for it.
44
105
  *
45
- * DOMAIN-SCOPED reads (`scopedToDomain` — the caller passed a `domain` arg,
46
- * e.g. a shared room) never greet and never probe: the stats call measures the
47
- * PERSONAL store, so on a scoped read it could claim "the store is empty"
48
- * about a domain that has memories the query merely missed (review finding).
49
- * The scoped copy suggests dropping the filter — honest, one actually exists.
106
+ * A NAMED scope never greets and never probes stats: that probe measures the
107
+ * PERSONAL store, so on a scoped read it could claim "the store is empty" about
108
+ * a domain whose memories the query merely missed. Its four arms are the four
109
+ * things we can know about the name.
50
110
  *
51
- * UNSCOPED reads make ONE stats call (only ever on the zero-result path) to
52
- * distinguish "store is truly empty" from "no match for this query":
53
- * - total_atoms === 0 → EMPTY_STORE_WELCOME (first-contact greeting)
54
- * - total_atoms > 0 → no-match + the broaden hint (no filter clause)
55
- * - stats throws/malformed → the plain old no-match message (fail-open to the
56
- * pre-greeting behavior; never surfaces an error, never writes anything)
111
+ * An UNSCOPED scope makes ONE stats call, on this path only:
112
+ * - stats unreachable / no number → the plain no-match message + the disclosure
113
+ * - total_atoms > 0 → no-match + broaden hint + the disclosure
114
+ * - total_atoms === 0 → one head per room state, each with its own
115
+ * disclosure; only `rooms.state === "none"` may claim the memory is empty,
116
+ * because only there has the claim been established.
57
117
  */
58
- export async function buildReadEmptyResponse(fetchStats, scopedToDomain = false) {
59
- if (scopedToDomain)
60
- return NO_MATCH_MESSAGE + NO_MATCH_SCOPED_HINT;
118
+ export async function buildReadEmptyResponse(fetchStats, scope) {
119
+ if (scope.kind === "named") {
120
+ const named = scope.named;
121
+ switch (named.state) {
122
+ case "present":
123
+ // The store is there; the query missed. The hint suggests a wider query
124
+ // or another domain and never suggests dropping the scope — when the
125
+ // domain is a room, dropping it is the one move that guarantees the
126
+ // content stays hidden (the 2026-08-07 incident).
127
+ return NO_MATCH_MESSAGE + NO_MATCH_SCOPED_HINT;
128
+ case "no-such-domain":
129
+ case "no-such-room":
130
+ // A SPECIFIC diagnosis replaces the generic advice; it does not follow
131
+ // it. Printed the other way round, a model reading top-down went off to
132
+ // widen a query against a store that does not exist.
133
+ return NO_MATCH_MESSAGE + named.note;
134
+ case "unchecked":
135
+ // Not a diagnosis — an admission. So the generic advice stays, and the
136
+ // admission is added rather than substituted. This is the case that used
137
+ // to be spelled exactly like "present".
138
+ return NO_MATCH_MESSAGE + NO_MATCH_SCOPED_HINT + named.note;
139
+ default:
140
+ return noStateLeftUnhandled(named);
141
+ }
142
+ }
143
+ const rooms = scope.rooms;
61
144
  let totalAtoms;
62
145
  try {
63
146
  totalAtoms = (await fetchStats())?.total_atoms;
64
147
  }
65
148
  catch {
66
- return NO_MATCH_MESSAGE;
149
+ return NO_MATCH_MESSAGE + roomsTail(rooms);
67
150
  }
68
- if (totalAtoms === 0)
69
- return EMPTY_STORE_WELCOME;
70
- if (typeof totalAtoms === "number" && totalAtoms > 0) {
71
- return NO_MATCH_MESSAGE + NO_MATCH_HINT;
151
+ if (typeof totalAtoms !== "number")
152
+ return NO_MATCH_MESSAGE + roomsTail(rooms);
153
+ if (totalAtoms > 0)
154
+ return NO_MATCH_MESSAGE + NO_MATCH_HINT + roomsTail(rooms);
155
+ // The personal bucket measured zero. `total_atoms` counts ONE org, so what
156
+ // that means for the account depends entirely on the rooms.
157
+ switch (rooms.state) {
158
+ case "none":
159
+ return EMPTY_STORE_WELCOME;
160
+ case "live":
161
+ case "archived-only":
162
+ return EMPTY_PERSONAL_STORE_WITH_ROOMS + rooms.note;
163
+ case "unknown":
164
+ return EMPTY_PERSONAL_STORE_ROOMS_UNCHECKED + rooms.note;
165
+ default:
166
+ return noStateLeftUnhandled(rooms);
72
167
  }
73
- return NO_MATCH_MESSAGE;
74
168
  }
75
169
  //# sourceMappingURL=teaching.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"teaching.js","sourceRoot":"","sources":["../src/teaching.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAC9B,2xBAA2xB,CAAC;AAE9xB,6EAA6E;AAC7E,MAAM,CAAC,MAAM,gBAAgB,GAAG,mCAAmC,CAAC;AAEpE;;iEAEiE;AACjE,MAAM,CAAC,MAAM,aAAa,GAAG,uBAAuB,CAAC;AAErD;sDACsD;AACtD,MAAM,CAAC,MAAM,oBAAoB,GAC/B,wEAAwE,CAAC;AAE3E;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAC9B,0GAA0G;IAC1G,yIAAyI;IACzI,qJAAqJ,CAAC;AAExJ;;;;;;;;;;;;;;;GAeG;AACH,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAC1C,UAAmD,EACnD,cAAc,GAAG,KAAK;IAEtB,IAAI,cAAc;QAAE,OAAO,gBAAgB,GAAG,oBAAoB,CAAC;IACnE,IAAI,UAA8B,CAAC;IACnC,IAAI,CAAC;QACH,UAAU,GAAG,CAAC,MAAM,UAAU,EAAE,CAAC,EAAE,WAAW,CAAC;IACjD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,gBAAgB,CAAC;IAC1B,CAAC;IACD,IAAI,UAAU,KAAK,CAAC;QAAE,OAAO,mBAAmB,CAAC;IACjD,IAAI,OAAO,UAAU,KAAK,QAAQ,IAAI,UAAU,GAAG,CAAC,EAAE,CAAC;QACrD,OAAO,gBAAgB,GAAG,aAAa,CAAC;IAC1C,CAAC;IACD,OAAO,gBAAgB,CAAC;AAC1B,CAAC"}
1
+ {"version":3,"file":"teaching.js","sourceRoot":"","sources":["../src/teaching.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mnemoverse/mcp-memory-server",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
4
4
  "description": "Hosted persistent memory for AI agents that learns which facts help and lets the rest fade — one key across Claude Code, Cursor, VS Code & ChatGPT, no infra to run",
5
5
  "type": "module",
6
6
  "bin": {
@@ -15,11 +15,12 @@
15
15
  "package.json"
16
16
  ],
17
17
  "scripts": {
18
- "build": "tsc",
18
+ "build": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && tsc",
19
19
  "dev": "tsc --watch",
20
20
  "start": "node dist/index.js",
21
21
  "test": "vitest run",
22
22
  "test:watch": "vitest",
23
+ "typecheck:test": "tsc -p tsconfig.test.json",
23
24
  "generate:configs": "node scripts/generate-configs.mjs",
24
25
  "verify:configs": "node scripts/generate-configs.mjs --check",
25
26
  "check:release-sync": "node scripts/check-release-sync.mjs",