@mnemoverse/mcp-memory-server 0.8.0 → 0.8.2

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/scope.js ADDED
@@ -0,0 +1,480 @@
1
+ /**
2
+ * Search-scope honesty — the note that turns "nothing found" from an assertion
3
+ * about the WORLD into a statement about where we actually looked.
4
+ *
5
+ * Why this module exists (incident 2026-08-07). Two agents lost a working day
6
+ * to the same silent failure: a message was written into a shared room, and an
7
+ * unscoped `memory_list_recent` answered "Nothing new since your watermark."
8
+ * The write was fine, the index was fine, and the read was fine — an UNSCOPED
9
+ * read simply never covers rooms.
10
+ *
11
+ * That is not a filter oversight; it is how the store is built. A room is a
12
+ * separate storage tenant (core ADR-019): with no `domain`, the request runs
13
+ * against the caller's OWN org, and room atoms live in the room's org. So the
14
+ * unscoped feed cannot see them and never could.
15
+ *
16
+ * The defect is therefore not the scoping — it is the WORDING. "Nothing new"
17
+ * is a claim about absence, and absence is only knowable within the scope you
18
+ * searched. An empty result from an unscoped read must therefore say what it did
19
+ * not cover: which rooms went unsearched and how to read them, that every room
20
+ * it has is archived and reachable by no path, or that it could not find out.
21
+ * Silence is correct in exactly one case — the caller has no rooms, so nothing
22
+ * was missed — and that case is a distinct state below rather than the falsy
23
+ * spelling of the other three.
24
+ *
25
+ * Kept out of index.ts so the copy can be unit-tested without booting the
26
+ * stdio transport, matching render.ts and teaching.ts.
27
+ */
28
+ import { domainPhrase, exactLiteral, roomNamePhrase, withDomainEscapeLegend, } from "./names.js";
29
+ /**
30
+ * How to NAME the scope inside a sentence, so the sentence is true on its own
31
+ * rather than corrected by a paragraph underneath it.
32
+ *
33
+ * "Nothing new since your watermark." was left untouched by the first two
34
+ * passes of this release — the very sentence this module's header names as the
35
+ * lie that cost two agents a day. A note was appended to it instead, so the
36
+ * assembled answer read as two voices: one asserting you were caught up, the
37
+ * next saying that assertion was meaningless. The sibling branch in memory_read
38
+ * had it right all along by naming its filters inside the sentence (review,
39
+ * 2026-08-08).
40
+ *
41
+ * The name inside that sentence then went through safeInline, which made the
42
+ * sentence false in the case that matters most: a read on `" engineering"` — the
43
+ * padded second store this release is about — answered `Nothing in
44
+ * "engineering"`, naming a DIFFERENT store, and a whitespace-only scope printed
45
+ * as `""`, i.e. as no scope at all. It is now the exact literal (src/names.ts),
46
+ * and when a name cannot be reproduced the sentence names nothing rather than
47
+ * naming something else.
48
+ *
49
+ * A room address stays unnamed ("that room") — unchanged, and deliberately: the
50
+ * address is already in the caller's hand, and this sentence has no need to
51
+ * print another principal's display string. Where a room's NAME is printed at
52
+ * all ({@link liveRoomsNote} below, the room tools in index.ts) it goes through
53
+ * `roomNamePhrase` (src/names.ts) — exactly, or declared unprintable — never
54
+ * through the lossy sanitiser, which renamed "проект" to "(unnamed room)".
55
+ *
56
+ * Lives here rather than in index.ts so the sentence can be unit-tested: this is
57
+ * the module about saying where we looked, and index.ts opens a stdio transport
58
+ * on import.
59
+ */
60
+ export function scopeLabel(searched) {
61
+ if (!searched)
62
+ return "your own domains";
63
+ if (searched.startsWith("xroom:"))
64
+ return "that room";
65
+ return domainPhrase(searched);
66
+ }
67
+ /**
68
+ * "Nothing new since your watermark" is also what you get for a watermark in
69
+ * the FUTURE — a mixed-up timezone or a bad relative-date calculation reads
70
+ * exactly like a clean bill of health (dogfood, 2026-08-07). If the caller's
71
+ * `since` is ahead of now, say so and show the current time; that is the whole
72
+ * diagnosis, and the caller cannot reach it from "nothing new".
73
+ *
74
+ * The explaining clause claims a fact about TIME, never about any store. It
75
+ * used to read "nothing has been written after it YET" — an absence claim over
76
+ * EVERY store, printed directly above the scope disclosure admitting a room
77
+ * went unsearched, and premised on an admittedly-approximate client clock
78
+ * (review, 2026-08-08). The window is empty because it asks for entries newer
79
+ * than a moment that has not happened; what any store holds is not this note's
80
+ * to assert.
81
+ *
82
+ * `nowMs` is injected so the note is testable without freezing the clock.
83
+ */
84
+ export function futureSinceNote(since, nowMs) {
85
+ if (!since)
86
+ return "";
87
+ const t = parseAsUtc(since);
88
+ if (t === null || t <= nowMs)
89
+ return "";
90
+ return (`\n\nNote: that watermark is in the FUTURE — it asks for entries newer than a ` +
91
+ `moment that has not happened yet, so an empty result here says nothing about ` +
92
+ `whether you are caught up. ` +
93
+ `This client's clock reads ${new Date(nowMs).toISOString().slice(0, 16)}Z ` +
94
+ `(the server's may differ slightly). Check the watermark you passed — a timezone ` +
95
+ `slip is the usual cause.`);
96
+ }
97
+ /**
98
+ * Parse an ISO-8601 instant the way the SERVER does: an offset-less value is
99
+ * UTC, not local time.
100
+ *
101
+ * `Date.parse("2026-08-08T13:00:00")` returns LOCAL midnight-relative millis,
102
+ * while both tool descriptions here and core's schema say "naive = UTC". The
103
+ * mismatch made this note lie in both directions (review, 2026-08-08): west of
104
+ * UTC a perfectly sane watermark was declared to be in the future and every
105
+ * clause of the note was false; east of UTC a genuinely future watermark was
106
+ * shifted into the past and the note stayed silent, missing the one case it
107
+ * exists for.
108
+ *
109
+ * Date-only values ("2026-08-08") are already parsed as UTC by spec, so only
110
+ * date-TIME values without an offset need the Z.
111
+ */
112
+ function parseAsUtc(iso) {
113
+ const s = iso.trim();
114
+ const hasOffset = /(?:Z|[+-]\d{2}:?\d{2})$/i.test(s);
115
+ const isDateTime = /\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}/.test(s);
116
+ const t = Date.parse(isDateTime && !hasOffset ? `${s.replace(" ", "T")}Z` : s);
117
+ return Number.isNaN(t) ? null : t;
118
+ }
119
+ /**
120
+ * The disclosure for the {@link NamedScope} arm `no-such-domain`: the caller
121
+ * scoped a read to a domain that is not in `/memory/stats.domains`.
122
+ *
123
+ * WHAT IT DOES, which is not what it was called. Until 0.8.1 this was
124
+ * `nearestDomainNote`, and the name outlived the behaviour twice over: the
125
+ * "nearest"/prefix guess it was named for was deleted as unsound (see the body),
126
+ * and even before that it was never a nearest-neighbour search. What remains is
127
+ * two branches, and neither is a proximity search:
128
+ *
129
+ * 1. an EXACT case-insensitive twin, when one exists in the known list and
130
+ * both names can be printed reproducibly — the casing slip that silently
131
+ * forks a namespace, which is the one miss a reader can act on
132
+ * (dogfood, 2026-08-07);
133
+ * 2. otherwise a NAME-FREE statement that no store OF THE CALLER'S OWN
134
+ * carries that exact name — bounded to its evidence: `/memory/stats.domains`
135
+ * covers the caller's own org bucket only, so rooms and other principals'
136
+ * stores are invisible to it and the sentence may not claim them (review,
137
+ * 2026-08-08; the destructive sibling in index.ts already said "in your
138
+ * own store"). It is always safe to say and must never go quiet — silence
139
+ * here is byte-identical to "the store is there, your query merely
140
+ * missed".
141
+ *
142
+ * Deliberately conservative: only case differs. A whitespace or zero-width twin
143
+ * (`" engineering"` beside `"engineering"`) is NOT diagnosed here — extending the
144
+ * rule is a behaviour decision, and `memory_stats` closes that loop by printing
145
+ * both names exactly. Recorded in CHANGELOG's "Known and NOT fixed here".
146
+ */
147
+ export function noSuchDomainNote(domain, knownDomains) {
148
+ // The RAW name, exactly as searched. This used to trim — a second copy of
149
+ // the mistake the caller had already made: a read on " engineering" searched
150
+ // the padded store, then this checked "engineering", matched it, and the one
151
+ // sentence that would have explained the miss was suppressed precisely when a
152
+ // stray space had caused it (review, 2026-08-08).
153
+ const wanted = domain;
154
+ if (!wanted)
155
+ return "";
156
+ const lower = wanted.toLowerCase();
157
+ // Match on the RAW values, print them through the EXACT renderer.
158
+ //
159
+ // This branch names two stores and invites the reader to act on one of them,
160
+ // so it may only print a name it can reproduce. It used to render through
161
+ // safeInline and guard the branch with `sanitize(x) === x`, which was correct
162
+ // in spirit and expensive in practice: safeInline's charset is ASCII, so
163
+ // every Cyrillic name — ordinary in this workspace — failed the guard and the
164
+ // most useful sentence this module has went silent for a whole alphabet. (An
165
+ // even earlier draft printed through the sanitiser without the guard, and
166
+ // asserted facts about ":acme", a name that exists nowhere.)
167
+ //
168
+ // src/names.ts prints exactly or returns null, so the guard is now the
169
+ // renderer itself and "проект" can be named as "проект". Both sides are
170
+ // checked: naming a twin we cannot reproduce would send the reader to a
171
+ // store whose name we just invented.
172
+ const wantedLiteral = exactLiteral(wanted);
173
+ const twin = wantedLiteral
174
+ ? knownDomains
175
+ .map((d) => ({ name: d, exact: exactLiteral(d) }))
176
+ .find((c) => c.name !== wanted && c.name.toLowerCase() === lower && c.exact !== null)
177
+ : undefined;
178
+ if (wantedLiteral && twin?.exact) {
179
+ return withDomainEscapeLegend(`\n\nDomain names are matched exactly, including case: ${wantedLiteral.literal} is not ` +
180
+ `the same store as ${twin.exact.literal}, which does exist. Did you mean that one?`, wanted, twin.name);
181
+ }
182
+ // NO "closest match" — the branch the old function NAME advertised, deleted
183
+ // rather than repaired. It named the FIRST element of an unordered
184
+ // `SELECT DISTINCT domain` having any prefix relation in either direction —
185
+ // so it was not a nearest neighbour, it could differ between two identical
186
+ // calls, and a one-character domain became the confidently-named "closest"
187
+ // match for every name starting with that letter. This module's own contract
188
+ // says a wrong guess is worse than silence, because the reader trusts it.
189
+ //
190
+ // The absence claim is narrowed twice: names match byte-for-byte, so a store
191
+ // whose name carries a leading space or a zero-width character is real but
192
+ // unreachable from a clean spelling — hence "that exact name", never "no such
193
+ // store". And it is bounded to the caller's OWN stores, because that is all
194
+ // the evidence covers: the known list is `/memory/stats.domains`, which
195
+ // reports one org bucket and sees no room and no other principal's store.
196
+ //
197
+ // NO pointer to memory_stats here, and the reason has CHANGED. A previous
198
+ // draft ended "— memory_stats quotes each name so you can see them", and that
199
+ // was removed because it could not work: the sanitiser collapsed and trimmed
200
+ // whitespace BEFORE the quotes went on, so " engineering" and "engineering"
201
+ // printed identically, and the reader was sent to a check that could not
202
+ // reveal the thing (reviews, 2026-08-08). memory_stats now prints exact
203
+ // literals (src/names.ts), so that objection is gone — the check works.
204
+ // Whether this sentence should carry the pointer again is a copy decision and
205
+ // not part of the renderer change; its absence stays pinned by
206
+ // test/scope.test.ts until someone decides. What must not come back is a
207
+ // pointer to a surface that cannot answer.
208
+ return (`\n\nNo store of your own has that exact name. Names match byte-for-byte, so a stray space, ` +
209
+ `a different case, or an invisible character makes a separate store — one that ` +
210
+ `exists and holds its own memories.`);
211
+ }
212
+ /** How many rooms to name before collapsing into a count. */
213
+ const MAX_LISTED = 5;
214
+ /** The disclosure for either kind of scope — "" only where the type says there
215
+ * is genuinely nothing to disclose. */
216
+ export function readScopeNote(scope) {
217
+ const known = scope.kind === "named" ? scope.named : scope.rooms;
218
+ return "note" in known ? known.note : "";
219
+ }
220
+ /**
221
+ * One room record, with every field narrowed to the type it claims.
222
+ *
223
+ * Returns null when the value cannot be read as a room — which makes the WHOLE
224
+ * payload `unknown` rather than silently contributing a room-shaped blank. A
225
+ * non-boolean `archived` is rejected for the same reason: we would have to guess
226
+ * whether the room is readable, and the guess decides whether the answer says
227
+ * "re-run with domain set" or "nothing in there is reachable".
228
+ *
229
+ * It is also a latent crash fixed: `safeInline` does `(s ?? "").replace(…)`, so
230
+ * a numeric `name` on the wire used to throw inside a renderer.
231
+ */
232
+ function asRoom(value) {
233
+ if (typeof value !== "object" || value === null || Array.isArray(value))
234
+ return null;
235
+ const r = value;
236
+ const { archived } = r;
237
+ if (archived !== undefined && archived !== null && typeof archived !== "boolean") {
238
+ return null;
239
+ }
240
+ const str = (v) => (typeof v === "string" ? v : undefined);
241
+ return {
242
+ room_id: str(r.room_id),
243
+ name: str(r.name),
244
+ address: str(r.address),
245
+ role: str(r.role),
246
+ scope: str(r.scope),
247
+ archived: archived === true,
248
+ };
249
+ }
250
+ /** The opening clause every unscoped disclosure shares. */
251
+ const SCOPE_PREFIX = `\n\nScope: your own domains only. Shared rooms are separate stores and are NOT ` +
252
+ `included in an unscoped read`;
253
+ /**
254
+ * The rooms an unscoped read did not cover, named so they can be read.
255
+ *
256
+ * Archived rooms are deliberately NOT mentioned here. A counting clause was
257
+ * added and removed inside this release because it only ever rendered for
258
+ * owners — core filters archived rooms out of the member query entirely and
259
+ * hard-codes `archived=false` on joined rows — and because it promised recovery
260
+ * "until it is unarchived" when core has archive with no inverse (reviews,
261
+ * 2026-08-08). Both objections still stand for the MIXED case, which is this
262
+ * one: there is a readable room to point at, and that is what a reader can act
263
+ * on. The case where every room is archived is a different state with a
264
+ * different sentence (see below), because there silence is not an option: it
265
+ * either dangles a colon or lets the answer claim the memory is empty.
266
+ *
267
+ * The NAME is printed exactly (src/names.ts, `roomNamePhrase`) even though it is
268
+ * OWNER-chosen and display-only. This note used to render it through the
269
+ * injected sanitiser, which did not make hostile names harmless so much as it
270
+ * made ordinary names WRONG: two live rooms named "проект" and "план" both
271
+ * printed "(unnamed room)" in the very note that asks the reader to pick one
272
+ * (review, 2026-08-08). The exact literal keeps the anti-injection property —
273
+ * one line, quotes and backslashes and invisibles escaped — and a name that
274
+ * cannot be printed within the cap is declared unprintable rather than altered.
275
+ * The escape legend rides on the note itself, because this note only appears on
276
+ * UNSCOPED answers, where the assembling call sites pass no room names as
277
+ * legend candidates (they have `searched === undefined` in hand, not the list).
278
+ *
279
+ * `sanitize` (render.ts's safeInline) still guards the machine-shaped fields —
280
+ * `address` and `room_id`, which core charset-validates — as a defensive pass,
281
+ * unchanged.
282
+ */
283
+ function liveRoomsNote(live, sanitize) {
284
+ const listed = live.slice(0, MAX_LISTED);
285
+ const shown = listed.map((r) => {
286
+ const name = roomNamePhrase(r.name);
287
+ const roomId = sanitize(r.room_id);
288
+ const address = sanitize(r.address) || (roomId ? `xroom:${roomId}` : "");
289
+ return address ? ` - ${name} — domain="${address}"` : ` - ${name}`;
290
+ });
291
+ const rest = live.length - shown.length;
292
+ const more = rest > 0 ? `\n …and ${rest} more (memory_list_rooms)` : "";
293
+ return withDomainEscapeLegend(`${SCOPE_PREFIX} — ${live.length} room${live.length === 1 ? "" : "s"} ` +
294
+ `went unsearched:\n${shown.join("\n")}${more}\n` +
295
+ `Re-run with domain set to one of these to read it.`, ...listed.map((r) => r.name));
296
+ }
297
+ /**
298
+ * Every room the caller has is archived — the state that used to be spelled as
299
+ * an empty note plus `roomsFound: true`, i.e. as a promise of a disclosure that
300
+ * had been filtered out one function earlier.
301
+ *
302
+ * What it may claim, and why each clause is safe: an archived room refuses every
303
+ * read for owner and member alike (core resolves the room, then returns 403
304
+ * "Room is archived"); nothing in the client, the data plane or the portal plane
305
+ * clears the flag, so this connector genuinely has no operation that reopens
306
+ * one; and archiving is a soft flag, so the content is still there. The word
307
+ * "unarchive" is avoided on purpose — the withdrawn draft promised recovery
308
+ * "until it is unarchived", and no such operation exists to wait for.
309
+ *
310
+ * The rooms are counted, not named: there is no address that would do the reader
311
+ * any good, and an owner-chosen name is only worth surfacing when it is
312
+ * actionable.
313
+ */
314
+ function archivedOnlyNote(archived) {
315
+ const plural = archived === 1 ? "" : "s";
316
+ return (`${SCOPE_PREFIX}. You have ${archived} shared room${plural}, ` +
317
+ `${archived === 1 ? "which is" : "all of which are"} archived. An archived room ` +
318
+ `refuses every read, for its owner as much as for a member, and this client has no ` +
319
+ `operation that reopens one. Archiving did not delete what is in there, so a memory ` +
320
+ `you remember writing can be real and still be unreachable from here.`);
321
+ }
322
+ /** We could not look. Says so, and still states the boundary — dropping the
323
+ * caveat because the probe failed is the silence this module exists to end. */
324
+ function uncheckedRoomsNote(reason) {
325
+ const cause = reason === "fetch-failed"
326
+ ? "the room list could not be fetched just now"
327
+ : "the room list came back in a shape this client does not recognise";
328
+ return (`${SCOPE_PREFIX}; ${cause}, so this answer cannot say whether any room went ` +
329
+ `unsearched. Check memory_list_rooms and re-run with domain set.`);
330
+ }
331
+ /**
332
+ * Classify a `/memory/rooms` body. Total: every payload maps to exactly one
333
+ * state, and a payload that is not a list of rooms maps to `unknown` — never to
334
+ * `none`, which is the substitution that turned a contract violation into "you
335
+ * have no rooms".
336
+ */
337
+ export function classifyRooms(payload, sanitize) {
338
+ if (!Array.isArray(payload)) {
339
+ return { state: "unknown", reason: "unrecognised-shape", note: uncheckedRoomsNote("unrecognised-shape") };
340
+ }
341
+ const rooms = [];
342
+ for (const entry of payload) {
343
+ const room = asRoom(entry);
344
+ if (room === null) {
345
+ return {
346
+ state: "unknown",
347
+ reason: "unrecognised-shape",
348
+ note: uncheckedRoomsNote("unrecognised-shape"),
349
+ };
350
+ }
351
+ rooms.push(room);
352
+ }
353
+ const live = rooms.filter((r) => !r.archived);
354
+ const archived = rooms.length - live.length;
355
+ if (live.length > 0) {
356
+ return { state: "live", rooms, live, archived, note: liveRoomsNote(live, sanitize) };
357
+ }
358
+ if (archived > 0) {
359
+ return { state: "archived-only", rooms, archived, note: archivedOnlyNote(archived) };
360
+ }
361
+ return { state: "none" };
362
+ }
363
+ /** Fetch the caller's rooms and classify them. Never throws — a failure is a
364
+ * state, not an exception, because every caller has to render it. */
365
+ export async function probeRoomScope(fetchRooms, sanitize) {
366
+ try {
367
+ return classifyRooms(await fetchRooms(), sanitize);
368
+ }
369
+ catch {
370
+ return { state: "unknown", reason: "fetch-failed", note: uncheckedRoomsNote("fetch-failed") };
371
+ }
372
+ }
373
+ /**
374
+ * Not in the room list — and the reason is not only "wrong address or not a
375
+ * member", which is what this used to assert. A room the caller merely JOINED
376
+ * disappears from the list the moment its owner archives it (core filters
377
+ * `is_archived = FALSE` out of the member query), so for exactly the invited
378
+ * teammate this release is about, the old two-cause sentence was false.
379
+ */
380
+ const NO_SUCH_ROOM_NOTE = `\n\nThat room is not in your list — the address may be wrong, you may not be a ` +
381
+ `member, or it may be archived: a room you only JOINED disappears from the list once ` +
382
+ `its owner archives it. memory_list_rooms shows the ones you can read.`;
383
+ /** "We could not look" for a named scope, which the old code spelled as `""` —
384
+ * i.e. exactly like "the store is there and your query merely missed". */
385
+ function uncheckedNamedNote(probed, reason) {
386
+ const cause = reason === "fetch-failed"
387
+ ? "could not be fetched just now"
388
+ : "came back in a shape this client does not recognise";
389
+ return probed === "rooms"
390
+ ? `\n\nWhether you are a member of that room could not be checked — the room list ` +
391
+ `${cause}. So this empty result is not evidence that the address is wrong or that ` +
392
+ `you are not in it. memory_list_rooms shows the rooms you can read.`
393
+ : `\n\nWhether a store with that exact name exists could not be checked — the domain ` +
394
+ `list ${cause}. So this empty result is not evidence that the name is wrong; names ` +
395
+ `are matched byte-for-byte, and memory_stats lists them exactly.`;
396
+ }
397
+ function unchecked(name, probed, reason) {
398
+ return { state: "unchecked", name, probed, reason, note: uncheckedNamedNote(probed, reason) };
399
+ }
400
+ /**
401
+ * Probe for the one store a scoped read named.
402
+ *
403
+ * A ROOM ADDRESS IS CHECKED AGAINST THE ROOM LIST, never against `/memory/stats`
404
+ * — that exclusion is the whole reason this release exists. `memory_stats`
405
+ * reports the caller's own org bucket only, and not one of an account's twelve
406
+ * live rooms appears in its domain list, so probing it for an `xroom:` address
407
+ * would confidently answer "No store of your own has that exact name" about a
408
+ * room the caller is a member of: the false-absence claim being fixed,
409
+ * reintroduced by the fix (CodeRabbit, #65).
410
+ *
411
+ * The argument is the RAW value that was sent, never a trimmed copy. A read on
412
+ * `" engineering"` searched the padded store while a trimmed diagnosis checked
413
+ * `"engineering"`, found it, and stayed silent — suppressing the one note that
414
+ * explains the miss precisely when a stray space had caused it.
415
+ *
416
+ * Private because `name` must be non-empty, which only {@link probeReadScope}
417
+ * can guarantee: `searchedScope` maps `""` to `undefined`, so an empty domain is
418
+ * not a scope at all and takes the own-domains path.
419
+ */
420
+ async function probeNamedScope(name, fetchStats, fetchRooms, sanitize) {
421
+ if (name.startsWith("xroom:")) {
422
+ let payload;
423
+ try {
424
+ payload = await fetchRooms();
425
+ }
426
+ catch {
427
+ return unchecked(name, "rooms", "fetch-failed");
428
+ }
429
+ const rooms = classifyRooms(payload, sanitize);
430
+ if (rooms.state === "unknown")
431
+ return unchecked(name, "rooms", rooms.reason);
432
+ const all = rooms.state === "none" ? [] : rooms.rooms;
433
+ const member = all.some((r) => r.address === name || `xroom:${r.room_id}` === name);
434
+ return member
435
+ ? { state: "present", name }
436
+ : { state: "no-such-room", name, note: NO_SUCH_ROOM_NOTE };
437
+ }
438
+ let payload;
439
+ try {
440
+ payload = await fetchStats();
441
+ }
442
+ catch {
443
+ return unchecked(name, "domains", "fetch-failed");
444
+ }
445
+ const domains = payload?.domains;
446
+ // Core's response model declares `domains: list[str]` with no default, so it
447
+ // is always present and always an array of strings on a 200. A missing key or
448
+ // a non-string element therefore means the body is not core's, and the only
449
+ // honest reading is that we did not get to look — not that the store is absent.
450
+ if (!Array.isArray(domains) || domains.some((d) => typeof d !== "string")) {
451
+ return unchecked(name, "domains", "unrecognised-shape");
452
+ }
453
+ const known = domains;
454
+ if (known.includes(name))
455
+ return { state: "present", name };
456
+ return { state: "no-such-domain", name, note: noSuchDomainNote(name, known) };
457
+ }
458
+ /**
459
+ * Build the {@link ReadScope} for one read: where it looked, and what we know
460
+ * about what it therefore did not cover. One GET per call — the room list for
461
+ * an unscoped read or an `xroom:` address, `/memory/stats` for a plain domain
462
+ * — and callers reach it on zero-result paths only. That is MORE probing than
463
+ * 0.8.0 did, not "exactly as before" (an earlier version of this comment):
464
+ * 0.8.0 made no probe at all on a domain-scoped, room-scoped or filtered read
465
+ * and none anywhere in the feed; only the plain unscoped read probed stats.
466
+ * The added reads are disclosed in the 0.8.1 CHANGELOG entry, rate cost
467
+ * included.
468
+ *
469
+ * `searched` must be the value that went over the wire — `searchedScope(domain)`
470
+ * — so that the diagnosis and the request can never describe different stores.
471
+ * A falsy value takes the own-domains path because that is what the request did:
472
+ * core filters on `domain is not None`, and `searchedScope` drops `""`.
473
+ */
474
+ export async function probeReadScope(searched, fetchStats, fetchRooms, sanitize) {
475
+ if (!searched) {
476
+ return { kind: "own-domains", rooms: await probeRoomScope(fetchRooms, sanitize) };
477
+ }
478
+ return { kind: "named", named: await probeNamedScope(searched, fetchStats, fetchRooms, sanitize) };
479
+ }
480
+ //# sourceMappingURL=scope.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scope.js","sourceRoot":"","sources":["../src/scope.ts"],"names":[],"mappings":"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"}
@@ -9,6 +9,7 @@
9
9
  * constraints stay: no over-claiming, and the never-store-secrets safety line
10
10
  * is kept on write surfaces.
11
11
  */
12
+ import type { ReadScope } from "./scope.js";
12
13
  /**
13
14
  * Server-level instructions, passed to the McpServer constructor and landed
14
15
  * verbatim in the connected model's system prompt by MCP clients.
@@ -19,40 +20,83 @@
19
20
  * - Active polarity: no "only when the user explicitly asks" gating.
20
21
  * - Mentions every tool family: read/write/stats/feedback, delete, rooms, vault.
21
22
  */
22
- export declare 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 \u2014 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.";
23
+ export declare 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 \u2014 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
24
  /** The pre-existing zero-result message — kept as the fail-open fallback. */
24
25
  export declare const NO_MATCH_MESSAGE = "No memories found for this query.";
25
26
  /** Hint for an UNSCOPED no-match against a non-empty store: widen the query.
26
27
  * (No drop-the-filter clause — none was set; advising to remove a filter that
27
28
  * does not exist nudges the model into confabulating state.) */
28
29
  export declare 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 declare const NO_MATCH_SCOPED_HINT = " Try a broader query, or drop the domain filter to search all domains.";
32
30
  /**
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).
31
+ * Hint for a DOMAIN-SCOPED no-match: widen the query, or try another domain —
32
+ * and NEVER drop the scope.
33
+ *
34
+ * This doc used to read "here a filter genuinely exists, so suggesting to drop
35
+ * it is honest and actionable", which described a draft that no longer exists:
36
+ * the clause "…or drop the domain filter to search all domains" was deleted, and
37
+ * a test now forbids it. The advice was actively harmful when the domain is a
38
+ * room — an unscoped read does not cover rooms at all, so dropping the filter is
39
+ * the one move guaranteed to lose the content the reader is hunting, and it is
40
+ * the step that cost two agents a day on 2026-08-07.
41
+ */
42
+ export declare const NO_MATCH_SCOPED_HINT = " Try a broader query, or a different domain.";
43
+ /**
44
+ * The honest line for an account whose OWN domains are empty but whose rooms
45
+ * are not — an invited teammate whose every memory lives in shared rooms.
46
+ *
47
+ * Without this, such a caller was greeted with EMPTY_STORE_WELCOME: "your
48
+ * memory is empty, nothing has been saved yet, which is why this search
49
+ * returned nothing" — three false clauses, immediately contradicted by the
50
+ * scope note appended underneath, in the same payload (review, 2026-08-08).
51
+ * That reader is precisely the person from the incident this release is about.
52
+ *
53
+ * ENDS WITH A COLON, so it is only ever emitted together with the disclosure
54
+ * that follows it. That pairing used to be two independent decisions and they
55
+ * disagreed: this line was chosen from a flag counting ALL rooms while the
56
+ * disclosure was built from the LIVE ones, so an account whose only room was
57
+ * archived received the colon and nothing after it. Both now come out of the
58
+ * same {@link RoomScope} arm, and the arm that has nothing to disclose has no
59
+ * `note` field to forget.
60
+ */
61
+ export declare const EMPTY_PERSONAL_STORE_WITH_ROOMS = "Nothing in your own domains \u2014 they hold no memories yet. That is not the whole picture:";
62
+ /**
63
+ * The same situation with the room probe UNANSWERED. Says the one true thing —
64
+ * the personal store measured zero and the rest could not be checked — and
65
+ * deliberately does NOT say "nothing has been saved yet", which was reachable
66
+ * here and is a claim about a scope this client failed to reach.
67
+ */
68
+ export declare const EMPTY_PERSONAL_STORE_ROOMS_UNCHECKED = "Nothing in your own domains \u2014 they hold no memories yet. Whether that is the whole picture could not be checked:";
69
+ /**
70
+ * First-contact greeting: shown ONLY when a read comes back empty, the personal
71
+ * store holds zero memories, AND the room probe answered that there are no rooms
72
+ * — i.e. the very first read of this account's life, established rather than
73
+ * assumed. Seeds the ANSWER, not the store: one functional paragraph that says
74
+ * what this store is, how to save the first memory, and one next step.
38
75
  */
39
76
  export declare const EMPTY_STORE_WELCOME: string;
40
77
  /**
41
- * Decide what a zero-result memory_read should say.
42
- *
43
- * DOMAIN-SCOPED reads (`scopedToDomain` — the caller passed a `domain` arg,
44
- * e.g. a shared room) never greet and never probe: the stats call measures the
45
- * PERSONAL store, so on a scoped read it could claim "the store is empty"
46
- * about a domain that has memories the query merely missed (review finding).
47
- * The scoped copy suggests dropping the filter — honest, one actually exists.
48
- *
49
- * UNSCOPED reads make ONE stats call (only ever on the zero-result path) to
50
- * distinguish "store is truly empty" from "no match for this query":
51
- * - total_atoms === 0 → EMPTY_STORE_WELCOME (first-contact greeting)
52
- * - total_atoms > 0 → no-match + the broaden hint (no filter clause)
53
- * - stats throws/malformed → the plain old no-match message (fail-open to the
54
- * pre-greeting behavior; never surfaces an error, never writes anything)
78
+ * Assemble the WHOLE answer for a zero-result read: the head sentence and the
79
+ * disclosure that belongs with it, in one place.
80
+ *
81
+ * They are assembled together on purpose. While the head came from here and the
82
+ * tail was concatenated by the caller, the two could be — and were — derived
83
+ * from different facts: a head promising "that is not the whole picture:" with
84
+ * an empty tail after it, and a first-contact greeting under a tail admitting
85
+ * the room list could not be fetched. Every arm below returns a complete answer,
86
+ * so a head cannot outlive the evidence for it.
87
+ *
88
+ * A NAMED scope never greets and never probes stats: that probe measures the
89
+ * PERSONAL store, so on a scoped read it could claim "the store is empty" about
90
+ * a domain whose memories the query merely missed. Its four arms are the four
91
+ * things we can know about the name.
92
+ *
93
+ * An UNSCOPED scope makes ONE stats call, on this path only:
94
+ * - stats unreachable / no number → the plain no-match message + the disclosure
95
+ * - total_atoms > 0 → no-match + broaden hint + the disclosure
96
+ * - total_atoms === 0 → one head per room state, each with its own
97
+ * disclosure; only `rooms.state === "none"` may claim the memory is empty,
98
+ * because only there has the claim been established.
55
99
  */
56
100
  export declare function buildReadEmptyResponse(fetchStats: () => Promise<{
57
101
  total_atoms?: number;
58
- }>, scopedToDomain?: boolean): Promise<string>;
102
+ }>, scope: ReadScope): Promise<string>;