@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/README.md +39 -5
- package/dist/index.d.ts +23 -1
- package/dist/index.js +664 -122
- package/dist/index.js.map +1 -1
- package/dist/names.d.ts +168 -0
- package/dist/names.js +216 -0
- package/dist/names.js.map +1 -0
- package/dist/render.d.ts +80 -6
- package/dist/render.js +99 -10
- package/dist/render.js.map +1 -1
- package/dist/requests.d.ts +69 -0
- package/dist/requests.js +76 -0
- package/dist/requests.js.map +1 -0
- package/dist/scope.d.ts +223 -0
- package/dist/scope.js +480 -0
- package/dist/scope.js.map +1 -0
- package/dist/teaching.d.ts +68 -24
- package/dist/teaching.js +124 -30
- package/dist/teaching.js.map +1 -1
- package/package.json +3 -2
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":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,EACL,YAAY,EACZ,YAAY,EACZ,cAAc,EACd,sBAAsB,GACvB,MAAM,YAAY,CAAC;AAEpB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,MAAM,UAAU,UAAU,CAAC,QAA4B;IACrD,IAAI,CAAC,QAAQ;QAAE,OAAO,kBAAkB,CAAC;IACzC,IAAI,QAAQ,CAAC,UAAU,CAAC,QAAQ,CAAC;QAAE,OAAO,WAAW,CAAC;IACtD,OAAO,YAAY,CAAC,QAAQ,CAAC,CAAC;AAChC,CAAC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,eAAe,CAAC,KAAyB,EAAE,KAAa;IACtE,IAAI,CAAC,KAAK;QAAE,OAAO,EAAE,CAAC;IACtB,MAAM,CAAC,GAAG,UAAU,CAAC,KAAK,CAAC,CAAC;IAC5B,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,IAAI,KAAK;QAAE,OAAO,EAAE,CAAC;IACxC,OAAO,CACL,+EAA+E;QAC/E,+EAA+E;QAC/E,6BAA6B;QAC7B,6BAA6B,IAAI,IAAI,CAAC,KAAK,CAAC,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI;QAC3E,kFAAkF;QAClF,0BAA0B,CAC3B,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;;;GAcG;AACH,SAAS,UAAU,CAAC,GAAW;IAC7B,MAAM,CAAC,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC;IACrB,MAAM,SAAS,GAAG,0BAA0B,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACrD,MAAM,UAAU,GAAG,kCAAkC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAC9D,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,UAAU,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,GAAG,EAAE,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAC/E,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;AACpC,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,MAAM,UAAU,gBAAgB,CAC9B,MAAc,EACd,YAA+B;IAE/B,0EAA0E;IAC1E,6EAA6E;IAC7E,6EAA6E;IAC7E,8EAA8E;IAC9E,kDAAkD;IAClD,MAAM,MAAM,GAAG,MAAM,CAAC;IACtB,IAAI,CAAC,MAAM;QAAE,OAAO,EAAE,CAAC;IACvB,MAAM,KAAK,GAAG,MAAM,CAAC,WAAW,EAAE,CAAC;IAEnC,kEAAkE;IAClE,EAAE;IACF,6EAA6E;IAC7E,0EAA0E;IAC1E,8EAA8E;IAC9E,yEAAyE;IACzE,8EAA8E;IAC9E,6EAA6E;IAC7E,0EAA0E;IAC1E,6DAA6D;IAC7D,EAAE;IACF,uEAAuE;IACvE,wEAAwE;IACxE,wEAAwE;IACxE,qCAAqC;IACrC,MAAM,aAAa,GAAG,YAAY,CAAC,MAAM,CAAC,CAAC;IAC3C,MAAM,IAAI,GAAG,aAAa;QACxB,CAAC,CAAC,YAAY;aACT,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,YAAY,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;aACjD,IAAI,CACH,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,MAAM,IAAI,CAAC,CAAC,IAAI,CAAC,WAAW,EAAE,KAAK,KAAK,IAAI,CAAC,CAAC,KAAK,KAAK,IAAI,CAC/E;QACL,CAAC,CAAC,SAAS,CAAC;IACd,IAAI,aAAa,IAAI,IAAI,EAAE,KAAK,EAAE,CAAC;QACjC,OAAO,sBAAsB,CAC3B,yDAAyD,aAAa,CAAC,OAAO,UAAU;YACtF,qBAAqB,IAAI,CAAC,KAAK,CAAC,OAAO,4CAA4C,EACrF,MAAM,EACN,IAAI,CAAC,IAAI,CACV,CAAC;IACJ,CAAC;IAED,4EAA4E;IAC5E,mEAAmE;IACnE,4EAA4E;IAC5E,2EAA2E;IAC3E,2EAA2E;IAC3E,6EAA6E;IAC7E,0EAA0E;IAC1E,EAAE;IACF,6EAA6E;IAC7E,2EAA2E;IAC3E,8EAA8E;IAC9E,4EAA4E;IAC5E,wEAAwE;IACxE,0EAA0E;IAC1E,EAAE;IACF,0EAA0E;IAC1E,8EAA8E;IAC9E,6EAA6E;IAC7E,4EAA4E;IAC5E,yEAAyE;IACzE,wEAAwE;IACxE,wEAAwE;IACxE,8EAA8E;IAC9E,+DAA+D;IAC/D,yEAAyE;IACzE,2CAA2C;IAC3C,OAAO,CACL,6FAA6F;QAC7F,gFAAgF;QAChF,oCAAoC,CACrC,CAAC;AACJ,CAAC;AAaD,6DAA6D;AAC7D,MAAM,UAAU,GAAG,CAAC,CAAC;AA+HrB;wCACwC;AACxC,MAAM,UAAU,aAAa,CAAC,KAAgB;IAC5C,MAAM,KAAK,GAAG,KAAK,CAAC,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC;IACjE,OAAO,MAAM,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;AAC3C,CAAC;AAED;;;;;;;;;;;GAWG;AACH,SAAS,MAAM,CAAC,KAAc;IAC5B,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IACrF,MAAM,CAAC,GAAG,KAAgC,CAAC;IAC3C,MAAM,EAAE,QAAQ,EAAE,GAAG,CAAC,CAAC;IACvB,IAAI,QAAQ,KAAK,SAAS,IAAI,QAAQ,KAAK,IAAI,IAAI,OAAO,QAAQ,KAAK,SAAS,EAAE,CAAC;QACjF,OAAO,IAAI,CAAC;IACd,CAAC;IACD,MAAM,GAAG,GAAG,CAAC,CAAU,EAAsB,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;IACxF,OAAO;QACL,OAAO,EAAE,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC;QACvB,IAAI,EAAE,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC;QACjB,OAAO,EAAE,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC;QACvB,IAAI,EAAE,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC;QACjB,KAAK,EAAE,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC;QACnB,QAAQ,EAAE,QAAQ,KAAK,IAAI;KAC5B,CAAC;AACJ,CAAC;AAED,2DAA2D;AAC3D,MAAM,YAAY,GAChB,iFAAiF;IACjF,8BAA8B,CAAC;AAEjC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,SAAS,aAAa,CACpB,IAA4B,EAC5B,QAAkD;IAElD,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,UAAU,CAAC,CAAC;IACzC,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE;QAC7B,MAAM,IAAI,GAAG,cAAc,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;QACpC,MAAM,MAAM,GAAG,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC;QACnC,MAAM,OAAO,GAAG,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QACzE,OAAO,OAAO,CAAC,CAAC,CAAC,OAAO,IAAI,cAAc,OAAO,GAAG,CAAC,CAAC,CAAC,OAAO,IAAI,EAAE,CAAC;IACvE,CAAC,CAAC,CAAC;IACH,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC;IACxC,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,YAAY,IAAI,2BAA2B,CAAC,CAAC,CAAC,EAAE,CAAC;IAEzE,OAAO,sBAAsB,CAC3B,GAAG,YAAY,MAAM,IAAI,CAAC,MAAM,QAAQ,IAAI,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,GAAG;QACrE,qBAAqB,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,IAAI,IAAI;QAChD,oDAAoD,EACtD,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAC7B,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,SAAS,gBAAgB,CAAC,QAAgB;IACxC,MAAM,MAAM,GAAG,QAAQ,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC;IACzC,OAAO,CACL,GAAG,YAAY,cAAc,QAAQ,eAAe,MAAM,IAAI;QAC9D,GAAG,QAAQ,KAAK,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,kBAAkB,8BAA8B;QACjF,oFAAoF;QACpF,qFAAqF;QACrF,sEAAsE,CACvE,CAAC;AACJ,CAAC;AAED;gFACgF;AAChF,SAAS,kBAAkB,CAAC,MAAuB;IACjD,MAAM,KAAK,GACT,MAAM,KAAK,cAAc;QACvB,CAAC,CAAC,6CAA6C;QAC/C,CAAC,CAAC,mEAAmE,CAAC;IAC1E,OAAO,CACL,GAAG,YAAY,KAAK,KAAK,oDAAoD;QAC7E,iEAAiE,CAClE,CAAC;AACJ,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,aAAa,CAC3B,OAAgB,EAChB,QAAkD;IAElD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;QAC5B,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,oBAAoB,EAAE,IAAI,EAAE,kBAAkB,CAAC,oBAAoB,CAAC,EAAE,CAAC;IAC5G,CAAC;IACD,MAAM,KAAK,GAAkB,EAAE,CAAC;IAChC,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;QAC5B,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;QAC3B,IAAI,IAAI,KAAK,IAAI,EAAE,CAAC;YAClB,OAAO;gBACL,KAAK,EAAE,SAAS;gBAChB,MAAM,EAAE,oBAAoB;gBAC5B,IAAI,EAAE,kBAAkB,CAAC,oBAAoB,CAAC;aAC/C,CAAC;QACJ,CAAC;QACD,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACnB,CAAC;IACD,MAAM,IAAI,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC;IAC9C,MAAM,QAAQ,GAAG,KAAK,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC;IAC5C,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACpB,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,aAAa,CAAC,IAAI,EAAE,QAAQ,CAAC,EAAE,CAAC;IACvF,CAAC;IACD,IAAI,QAAQ,GAAG,CAAC,EAAE,CAAC;QACjB,OAAO,EAAE,KAAK,EAAE,eAAe,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,gBAAgB,CAAC,QAAQ,CAAC,EAAE,CAAC;IACvF,CAAC;IACD,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC;AAC3B,CAAC;AAED;sEACsE;AACtE,MAAM,CAAC,KAAK,UAAU,cAAc,CAClC,UAAkC,EAClC,QAAkD;IAElD,IAAI,CAAC;QACH,OAAO,aAAa,CAAC,MAAM,UAAU,EAAE,EAAE,QAAQ,CAAC,CAAC;IACrD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,cAAc,EAAE,IAAI,EAAE,kBAAkB,CAAC,cAAc,CAAC,EAAE,CAAC;IAChG,CAAC;AACH,CAAC;AAED;;;;;;GAMG;AACH,MAAM,iBAAiB,GACrB,iFAAiF;IACjF,sFAAsF;IACtF,uEAAuE,CAAC;AAE1E;2EAC2E;AAC3E,SAAS,kBAAkB,CAAC,MAA2B,EAAE,MAAuB;IAC9E,MAAM,KAAK,GACT,MAAM,KAAK,cAAc;QACvB,CAAC,CAAC,+BAA+B;QACjC,CAAC,CAAC,qDAAqD,CAAC;IAC5D,OAAO,MAAM,KAAK,OAAO;QACvB,CAAC,CAAC,iFAAiF;YAC/E,GAAG,KAAK,2EAA2E;YACnF,oEAAoE;QACxE,CAAC,CAAC,oFAAoF;YAClF,QAAQ,KAAK,uEAAuE;YACpF,iEAAiE,CAAC;AAC1E,CAAC;AAED,SAAS,SAAS,CAChB,IAAY,EACZ,MAA2B,EAC3B,MAAuB;IAEvB,OAAO,EAAE,KAAK,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,kBAAkB,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,CAAC;AAChG,CAAC;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,KAAK,UAAU,eAAe,CAC5B,IAAY,EACZ,UAAkC,EAClC,UAAkC,EAClC,QAAkD;IAElD,IAAI,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC9B,IAAI,OAAgB,CAAC;QACrB,IAAI,CAAC;YACH,OAAO,GAAG,MAAM,UAAU,EAAE,CAAC;QAC/B,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,SAAS,CAAC,IAAI,EAAE,OAAO,EAAE,cAAc,CAAC,CAAC;QAClD,CAAC;QACD,MAAM,KAAK,GAAG,aAAa,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;QAC/C,IAAI,KAAK,CAAC,KAAK,KAAK,SAAS;YAAE,OAAO,SAAS,CAAC,IAAI,EAAE,OAAO,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;QAC7E,MAAM,GAAG,GAAG,KAAK,CAAC,KAAK,KAAK,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC;QACtD,MAAM,MAAM,GAAG,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,KAAK,IAAI,IAAI,SAAS,CAAC,CAAC,OAAO,EAAE,KAAK,IAAI,CAAC,CAAC;QACpF,OAAO,MAAM;YACX,CAAC,CAAC,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE;YAC5B,CAAC,CAAC,EAAE,KAAK,EAAE,cAAc,EAAE,IAAI,EAAE,IAAI,EAAE,iBAAiB,EAAE,CAAC;IAC/D,CAAC;IAED,IAAI,OAAgB,CAAC;IACrB,IAAI,CAAC;QACH,OAAO,GAAG,MAAM,UAAU,EAAE,CAAC;IAC/B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC,IAAI,EAAE,SAAS,EAAE,cAAc,CAAC,CAAC;IACpD,CAAC;IACD,MAAM,OAAO,GAAI,OAAoD,EAAE,OAAO,CAAC;IAC/E,6EAA6E;IAC7E,8EAA8E;IAC9E,4EAA4E;IAC5E,gFAAgF;IAChF,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,QAAQ,CAAC,EAAE,CAAC;QAC1E,OAAO,SAAS,CAAC,IAAI,EAAE,SAAS,EAAE,oBAAoB,CAAC,CAAC;IAC1D,CAAC;IACD,MAAM,KAAK,GAAG,OAAmB,CAAC;IAClC,IAAI,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC;QAAE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC;IAC5D,OAAO,EAAE,KAAK,EAAE,gBAAgB,EAAE,IAAI,EAAE,IAAI,EAAE,gBAAgB,CAAC,IAAI,EAAE,KAAK,CAAC,EAAE,CAAC;AAChF,CAAC;AAED;;;;;;;;;;;;;;;GAeG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAClC,QAA4B,EAC5B,UAAkC,EAClC,UAAkC,EAClC,QAAkD;IAElD,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,OAAO,EAAE,IAAI,EAAE,aAAa,EAAE,KAAK,EAAE,MAAM,cAAc,CAAC,UAAU,EAAE,QAAQ,CAAC,EAAE,CAAC;IACpF,CAAC;IACD,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC,QAAQ,EAAE,UAAU,EAAE,UAAU,EAAE,QAAQ,CAAC,EAAE,CAAC;AACrG,CAAC"}
|
package/dist/teaching.d.ts
CHANGED
|
@@ -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
|
|
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
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
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
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
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
|
-
}>,
|
|
102
|
+
}>, scope: ReadScope): Promise<string>;
|