@mnemoverse/mcp-memory-server 0.11.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -4
- package/dist/errors.d.ts +90 -7
- package/dist/errors.js +179 -45
- package/dist/errors.js.map +1 -1
- package/dist/names.d.ts +34 -4
- package/dist/names.js +50 -10
- package/dist/names.js.map +1 -1
- package/dist/render.d.ts +69 -7
- package/dist/render.js +97 -14
- package/dist/render.js.map +1 -1
- package/dist/requests.d.ts +34 -3
- package/dist/requests.js +12 -3
- package/dist/requests.js.map +1 -1
- package/dist/resources.js +5 -2
- package/dist/resources.js.map +1 -1
- package/dist/shared.d.ts +23 -2
- package/dist/shared.js +21 -1
- package/dist/shared.js.map +1 -1
- package/dist/tools.d.ts +64 -1
- package/dist/tools.js +638 -116
- package/dist/tools.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -325,28 +325,50 @@ saw and diff it against what the server serves today, by version.
|
|
|
325
325
|
not have one; once declared, that schema's fields are add-only under this
|
|
326
326
|
same rule. Where this server's output schema deliberately differs from the
|
|
327
327
|
hosted connector's, the CHANGELOG entry says so; today that is
|
|
328
|
+
`memory_id` in every output schema, a plain string here and a
|
|
329
|
+
GUID-validated string there (this package's ids are opaque);
|
|
328
330
|
`memory_list_recent`'s `next_cursor`, optional here (absent when the
|
|
329
331
|
service sent a continuation token this client will not pass on) and
|
|
330
|
-
required there
|
|
332
|
+
required there; `memory_stats`, which carries five optional fields
|
|
333
|
+
(`episodes`, `prototypes`, `hebbian_edges`, `avg_valence`,
|
|
334
|
+
`avg_importance`) the connector's schema does not declare; the room
|
|
335
|
+
tools (`memory_create_room`, `memory_invite_to_room`, `memory_join_room`,
|
|
336
|
+
`memory_list_rooms`), where several fields the connector marks required
|
|
337
|
+
are optional here (`name`, `scope`, `already_member`, and the invite's
|
|
338
|
+
`code`, `scope`, `room_address` and `expires_at`), because a value core
|
|
339
|
+
did not send is an honest outcome here rather than a placeholder; and
|
|
340
|
+
`memory_list_rooms` and `vault_list`, which drop a row with no usable
|
|
341
|
+
identity (`room_id`, `address`, `role`; `alias`, `context`) from the data,
|
|
342
|
+
reported once on stderr, instead of emitting empty strings into required
|
|
343
|
+
fields.
|
|
331
344
|
- **Removing or renaming a tool or a tool's input parameter, or dropping or
|
|
332
345
|
renaming a declared annotation field,** is announced one MINOR ahead: the
|
|
333
346
|
tool (or parameter) stays, its description says
|
|
334
347
|
`deprecated since x.y, removed in x.z`, and the change lands only in the
|
|
335
348
|
announced version, with its CHANGELOG line. A renamed parameter is accepted
|
|
336
349
|
under both names until then (0.11: `memory_feedback`'s `atom_ids` became
|
|
337
|
-
`memory_ids
|
|
350
|
+
`memory_ids`; its removal, first announced for 0.12, lands in 0.13, since
|
|
351
|
+
0.12 shipped sooner than that announcement assumed). A rename is announced by naming
|
|
338
352
|
both the old and the new name; the version pair alone does not say what a
|
|
339
353
|
client should look for. Because a MINOR may add a field but not remove one, a
|
|
340
354
|
renamed annotation field is declared under both names until the announced
|
|
341
355
|
version.
|
|
342
356
|
- Any difference between two servers of the same version is a bug. Report it
|
|
343
|
-
with both `tools/list` outputs.
|
|
357
|
+
with both `tools/list` outputs. One exception, by configuration: a server
|
|
358
|
+
built on the `/shared` entry point may ask for its own noun in the three
|
|
359
|
+
descriptions that name the server (`wording.serverNoun`, below), which
|
|
360
|
+
changes those three description strings and nothing else; tool names,
|
|
361
|
+
input and output schemas and annotations never vary by configuration.
|
|
344
362
|
|
|
345
|
-
The list above is the 0.
|
|
363
|
+
The list above is the 0.12 surface: ten tools, each declaring all four hints.
|
|
346
364
|
The hosted connector at `mcp.mnemoverse.com/mcp` serves the same ten.
|
|
347
365
|
|
|
348
366
|
**If the hosted connector stops answering in a session.** A client can keep showing the connector as connected while every call in that session fails with "not connected". Reconnecting it on claude.ai does not revive a session that is already stuck; reconnect from inside the session instead (in Claude Code, `/mcp`, then sign in again). Meanwhile this local server, set up with an API key from the same account as in the Quick Start, reaches the same memory and does not depend on that session's sign-in.
|
|
349
367
|
|
|
368
|
+
### Building a second MCP server on this package
|
|
369
|
+
|
|
370
|
+
`@mnemoverse/mcp-memory-server/shared` is the entry point another server registers these same tools from, instead of keeping its own copy (ADR-025, mnemoverse-core). It exports `registerMemoryTools`/`registerMemoryPrompts`/`registerMemoryResources`, the three typed error classes (`ApiError`, `NetworkError`, `UnreadableBodyError`), `MAX_RESULT_CHARS`/`capResult`, and two optional dependencies a hosted deployment injects to speak in its own voice: `wording` (its own server noun and error vocabulary, including an OAuth mode under which no 401 or 403 explanation names an API key) and `writeAuthor` (vouching for the end user behind a write). See [`docs/shared.md`](./docs/shared.md) for the full contract.
|
|
371
|
+
|
|
350
372
|
## Use cases
|
|
351
373
|
|
|
352
374
|
The pattern that pays off first is cross-tool continuity: a decision made while pairing in Claude Code is there when you open Cursor an hour later, and the preference you stated in VS Code holds in a ChatGPT session that evening. Teams use shared rooms the same way — one place where an agent's lessons about a codebase accumulate instead of being re-taught per seat. And because recall re-ranks from feedback, the memories that keep proving useful surface first, which matters once a store grows past what anyone curates by hand.
|
package/dist/errors.d.ts
CHANGED
|
@@ -76,6 +76,41 @@
|
|
|
76
76
|
* outright, see `validatedKeysUrl` below, but this constant stays the
|
|
77
77
|
* fallback for both). */
|
|
78
78
|
export declare const KEYS_URL = "https://console.mnemoverse.com/dashboard/keys";
|
|
79
|
+
/**
|
|
80
|
+
* How a server that registers these tools wants its failures worded (STEP4-2,
|
|
81
|
+
* STEP4-3, owner decisions 2026-09-24). Every field is optional, and every
|
|
82
|
+
* field's absence means exactly what this file already does today: the
|
|
83
|
+
* whole point is that a consumer supplying no `wording` at all gets
|
|
84
|
+
* byte-identical text to before this type existed.
|
|
85
|
+
*
|
|
86
|
+
* - `serverNoun`: what the three tool descriptions that name themselves
|
|
87
|
+
* call this deployment ("this server" vs "this connector"). Read by
|
|
88
|
+
* src/tools.ts at registration time; ignored by this module.
|
|
89
|
+
* - `auth`: which credential the CALLER holds, not which one core issued.
|
|
90
|
+
* "api-key" (default) keeps every existing MNEMOVERSE_API_KEY-flavoured
|
|
91
|
+
* sentence. "oauth" is for a server whose user never sees an API key at
|
|
92
|
+
* all (a hosted connector minting a key on their behalf): no 401/403
|
|
93
|
+
* explanation under this mode names MNEMOVERSE_API_KEY, an env var, an
|
|
94
|
+
* MCP config file, or the keys console; each says what an OAuth user can
|
|
95
|
+
* actually do instead (reconnect, sign in again, check the plan, wait
|
|
96
|
+
* for the retry window).
|
|
97
|
+
* - `keysUrl`: replaces {@link KEYS_URL} wherever the api-key vocabulary
|
|
98
|
+
* prints a console URL, for a deployment whose key-management page is
|
|
99
|
+
* not console.mnemoverse.com. Has no effect under `auth: "oauth"`, which
|
|
100
|
+
* prints no console URL at all.
|
|
101
|
+
* - `rawDetail`: whether the wire body's raw tail (STEP4-3; e.g. `Raw
|
|
102
|
+
* detail — Mnemoverse API error 401 on …`) is appended after the
|
|
103
|
+
* guidance. Defaults to `true`, unchanged from every release before this
|
|
104
|
+
* one. Held in reserve for a deployment whose core-side error envelopes
|
|
105
|
+
* are found to echo request content back in `details`: set to `false`
|
|
106
|
+
* only once that check finds something to hide.
|
|
107
|
+
*/
|
|
108
|
+
export interface Wording {
|
|
109
|
+
serverNoun?: "this server" | "this connector";
|
|
110
|
+
auth?: "api-key" | "oauth";
|
|
111
|
+
keysUrl?: string;
|
|
112
|
+
rawDetail?: boolean;
|
|
113
|
+
}
|
|
79
114
|
/** Everything known about one failed call, at the moment it failed. */
|
|
80
115
|
export interface ApiFailure {
|
|
81
116
|
/** HTTP status. */
|
|
@@ -146,12 +181,16 @@ export declare function parseErrorEnvelope(body: string): ErrorEnvelope;
|
|
|
146
181
|
export declare function retryAfterSeconds(header: string | null | undefined): number | undefined;
|
|
147
182
|
/**
|
|
148
183
|
* The agent-facing explanation for one failed call, with the raw body kept
|
|
149
|
-
* after it.
|
|
184
|
+
* after it (unless `wording.rawDetail === false`, STEP4-3).
|
|
150
185
|
*
|
|
151
186
|
* Total by construction: every status reaches a sentence, and the fallback says
|
|
152
187
|
* that it has no specific guidance instead of inventing some.
|
|
188
|
+
*
|
|
189
|
+
* `wording` (STEP4-2/3, owner 2026-09-24) is optional and, absent, resolves
|
|
190
|
+
* to exactly today's behaviour: every call site before this release passes
|
|
191
|
+
* none, so every existing pin stays byte-identical.
|
|
153
192
|
*/
|
|
154
|
-
export declare function explainApiFailure(f: ApiFailure): string;
|
|
193
|
+
export declare function explainApiFailure(f: ApiFailure, wording?: Wording): string;
|
|
155
194
|
/**
|
|
156
195
|
* The request never got an answer at all: DNS, connectivity, a host that does
|
|
157
196
|
* not listen, or this client's own probe deadline firing.
|
|
@@ -172,7 +211,7 @@ export declare function explainApiFailure(f: ApiFailure): string;
|
|
|
172
211
|
* that case now. Every sentence below asserts that nothing answered, so it may
|
|
173
212
|
* only be reached when nothing did.
|
|
174
213
|
*/
|
|
175
|
-
export declare function explainNetworkFailure(method: string, path: string, cause: unknown): string;
|
|
214
|
+
export declare function explainNetworkFailure(method: string, path: string, cause: unknown, wording?: Wording): string;
|
|
176
215
|
/** A 2xx-or-not response whose BODY this client could not turn into the JSON
|
|
177
216
|
* this API speaks. The status is known — a reply arrived — which is the whole
|
|
178
217
|
* difference between this and {@link ApiFailure} or a transport failure. */
|
|
@@ -212,7 +251,7 @@ export interface UnreadableBody {
|
|
|
212
251
|
* say whether the operation ran, because a body it cannot read is no evidence
|
|
213
252
|
* either way. That last clause matters most for a write.
|
|
214
253
|
*/
|
|
215
|
-
export declare function explainUnreadableBody(f: UnreadableBody): string;
|
|
254
|
+
export declare function explainUnreadableBody(f: UnreadableBody, wording?: Wording): string;
|
|
216
255
|
/**
|
|
217
256
|
* {@link explainUnreadableBody} as an error, so a caller can tell "a reply
|
|
218
257
|
* arrived and was unreadable" from "no reply arrived" by TYPE rather than by
|
|
@@ -222,14 +261,25 @@ export declare class UnreadableBodyError extends Error {
|
|
|
222
261
|
readonly status: number;
|
|
223
262
|
readonly method: string;
|
|
224
263
|
readonly path: string;
|
|
225
|
-
|
|
264
|
+
/** The bytes that failed to parse, when they were read at all. Kept so
|
|
265
|
+
* {@link withWording} re-renders the same arm of the explanation. */
|
|
266
|
+
readonly bodyPreview: string | undefined;
|
|
267
|
+
/** `wording` (STEP4-2/3): optional, and absent (every call site before this
|
|
268
|
+
* release) reproduces today's message exactly: see {@link explainUnreadableBody}. */
|
|
269
|
+
constructor(f: UnreadableBody, wording?: Wording);
|
|
270
|
+
/** The same failure, explained under `wording`: see {@link ApiError.withWording}. */
|
|
271
|
+
withWording(wording: Wording | undefined): UnreadableBodyError;
|
|
226
272
|
}
|
|
227
273
|
/** {@link explainNetworkFailure} as an error, so `instanceof` can tell a
|
|
228
274
|
* transport failure from an HTTP one without reading either message. */
|
|
229
275
|
export declare class NetworkError extends Error {
|
|
230
276
|
readonly method: string;
|
|
231
277
|
readonly path: string;
|
|
232
|
-
|
|
278
|
+
/** `wording` (STEP4-2/3): optional, and absent (every call site before this
|
|
279
|
+
* release) reproduces today's message exactly: see {@link explainNetworkFailure}. */
|
|
280
|
+
constructor(method: string, path: string, cause: unknown, wording?: Wording);
|
|
281
|
+
/** The same failure, explained under `wording`: see {@link ApiError.withWording}. */
|
|
282
|
+
withWording(wording: Wording | undefined): NetworkError;
|
|
233
283
|
}
|
|
234
284
|
/**
|
|
235
285
|
* A failed API call, as an ERROR OBJECT rather than a parsed string.
|
|
@@ -249,7 +299,22 @@ export declare class ApiError extends Error {
|
|
|
249
299
|
readonly path: string;
|
|
250
300
|
/** The engine's envelope, already parsed — so a caller never re-parses. */
|
|
251
301
|
readonly envelope: ErrorEnvelope;
|
|
252
|
-
|
|
302
|
+
/** The `Retry-After` header as it arrived, when the response carried one.
|
|
303
|
+
* Kept so {@link withWording} re-renders a 429 with its retry sentence. */
|
|
304
|
+
readonly retryAfter: string | null | undefined;
|
|
305
|
+
/** `wording` (STEP4-2/3): optional, and absent (every call site before this
|
|
306
|
+
* release) reproduces today's message exactly: see {@link explainApiFailure}. */
|
|
307
|
+
constructor(f: ApiFailure, wording?: Wording);
|
|
308
|
+
/**
|
|
309
|
+
* The same failure, explained under `wording`: a new instance of this
|
|
310
|
+
* class whose message is what the constructor would have produced with
|
|
311
|
+
* that `wording`, every field and {@link isBare404} unchanged. The message
|
|
312
|
+
* is a pure function of the failure and the wording, so re-rendering an
|
|
313
|
+
* instance under the wording it was built with gives the same text back.
|
|
314
|
+
* This is how `MemoryToolDeps.wording` reaches an error the consumer's
|
|
315
|
+
* `apiFetch` built without it: see {@link rewordFailure}.
|
|
316
|
+
*/
|
|
317
|
+
withWording(wording: Wording | undefined): ApiError;
|
|
253
318
|
/**
|
|
254
319
|
* A 404 the ENGINE did not answer: silence, or the router's literal
|
|
255
320
|
* defaults. This is what a path the deployment does not serve looks like
|
|
@@ -269,3 +334,21 @@ export declare class ApiError extends Error {
|
|
|
269
334
|
*/
|
|
270
335
|
get isBare404(): boolean;
|
|
271
336
|
}
|
|
337
|
+
/**
|
|
338
|
+
* Re-explain one of this module's three errors under `wording`; hand
|
|
339
|
+
* anything else back untouched.
|
|
340
|
+
*
|
|
341
|
+
* This is how `MemoryToolDeps.wording` reaches the error text (STEP4-2): a
|
|
342
|
+
* consumer's `apiFetch` constructs `ApiError`, `NetworkError` and
|
|
343
|
+
* `UnreadableBodyError` however it likes, and `registerMemoryTools` passes
|
|
344
|
+
* every rejection through here with the `wording` it was given, so the
|
|
345
|
+
* consumer states its wording once, on `deps`, and the constructors need no
|
|
346
|
+
* second copy. Re-rendering is idempotent (see {@link ApiError.withWording}),
|
|
347
|
+
* so an `apiFetch` that does pass the same `wording` to the constructors
|
|
348
|
+
* gets the same text either way; one that passed a different wording gets
|
|
349
|
+
* `deps.wording`, the one source of truth for this registration. A
|
|
350
|
+
* rejection that is none of the three classes (a plain `Error`, an
|
|
351
|
+
* `McpError`) is returned as is: it carries no wording to apply, and its
|
|
352
|
+
* identity may matter to whoever threw it.
|
|
353
|
+
*/
|
|
354
|
+
export declare function rewordFailure(e: unknown, wording: Wording | undefined): unknown;
|
package/dist/errors.js
CHANGED
|
@@ -89,6 +89,19 @@ const USAGE_URL = "https://console.mnemoverse.com/dashboard/usage";
|
|
|
89
89
|
* module is imported BY index.ts, so reading the value from there would be a
|
|
90
90
|
* cycle. Change one, grep for the other. */
|
|
91
91
|
const DEFAULT_API_URL = "https://core.mnemoverse.com/api/v1";
|
|
92
|
+
/** Defaults applied field-by-field, with a `typeof`/literal guard on each:
|
|
93
|
+
* `wording` crosses a public package boundary a caller controls only at
|
|
94
|
+
* compile time, so a malformed value at runtime degrades to the default
|
|
95
|
+
* instead of propagating (e.g. into a template literal, or a `Wording`
|
|
96
|
+
* field silently taking a fifth value no branch here checks for). */
|
|
97
|
+
function resolveWording(w) {
|
|
98
|
+
return {
|
|
99
|
+
serverNoun: w?.serverNoun === "this connector" ? "this connector" : "this server",
|
|
100
|
+
auth: w?.auth === "oauth" ? "oauth" : "api-key",
|
|
101
|
+
keysUrl: typeof w?.keysUrl === "string" && w.keysUrl !== "" ? w.keysUrl : KEYS_URL,
|
|
102
|
+
rawDetail: typeof w?.rawDetail === "boolean" ? w.rawDetail : true,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
92
105
|
/**
|
|
93
106
|
* Is `details.keys_url` safe to put in front of a user?
|
|
94
107
|
*
|
|
@@ -232,16 +245,42 @@ function has(message, needle) {
|
|
|
232
245
|
* "caller org not identified" for the same reason that clause runs first at
|
|
233
246
|
* all (a valid key must never be told to replace itself), and still before
|
|
234
247
|
* the substring guess, since a named reason needs no guessing.
|
|
248
|
+
*
|
|
249
|
+
* UNDER `wording.auth === "oauth"` (STEP4-2, owner, 2026-09-24) none of the
|
|
250
|
+
* above applies: the caller never held an API key at all, so `reason` (a
|
|
251
|
+
* diagnosis of WHICH key problem this is) and every key-flavoured branch
|
|
252
|
+
* below would be a wrong cause stated confidently. "Caller org not
|
|
253
|
+
* identified" is the one exception: it is about tenant identification, not
|
|
254
|
+
* about a key, and can fire under either credential type, so it is checked
|
|
255
|
+
* first regardless of `auth`, worded for whichever credential the caller
|
|
256
|
+
* actually holds. Every other 401 collapses to one OAuth-flavoured sentence:
|
|
257
|
+
* reconnect or sign in again, because that is the one thing an OAuth user
|
|
258
|
+
* can actually do about a 401, and naming a wrong one of five key reasons
|
|
259
|
+
* would be worse than naming none.
|
|
235
260
|
*/
|
|
236
|
-
function explain401(env) {
|
|
261
|
+
function explain401(env, wording) {
|
|
237
262
|
const m = env.message;
|
|
238
263
|
if (has(m, "caller org not identified")) {
|
|
239
|
-
return
|
|
240
|
-
"this
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
264
|
+
return wording.auth === "oauth"
|
|
265
|
+
? "Mnemoverse: this deployment could not identify a tenant account for " +
|
|
266
|
+
"this request (401). The caller's sign-in itself was not rejected " +
|
|
267
|
+
"— do NOT tell the user to reconnect over this. Room and " +
|
|
268
|
+
"shared-memory operations need the multi-tenant backend, which a " +
|
|
269
|
+
"self-hosted or static-auth deployment does not have. Quote the " +
|
|
270
|
+
"detail below, and do not retry the same call against this " +
|
|
271
|
+
"deployment."
|
|
272
|
+
: "Mnemoverse: this deployment could not identify a tenant account for " +
|
|
273
|
+
"this request (401). The API key itself was not rejected — do NOT " +
|
|
274
|
+
"tell the user to replace it. Room and shared-memory operations need " +
|
|
275
|
+
"the multi-tenant backend, which a self-hosted or static-auth " +
|
|
276
|
+
"deployment does not have. Quote the detail below, and do not retry " +
|
|
277
|
+
"the same call against this deployment.";
|
|
278
|
+
}
|
|
279
|
+
if (wording.auth === "oauth") {
|
|
280
|
+
return ("Mnemoverse: the user's sign-in was rejected (401). There is no " +
|
|
281
|
+
"credential for them to edit — tell them to disconnect and " +
|
|
282
|
+
"reconnect the app, or sign in again, to refresh their session. Do " +
|
|
283
|
+
"not retry until they do.");
|
|
245
284
|
}
|
|
246
285
|
// `details.reason` (an engine change not yet released as of this
|
|
247
286
|
// one): the engine's own diagnosis of WHICH key problem this is, five
|
|
@@ -254,7 +293,7 @@ function explain401(env) {
|
|
|
254
293
|
// for byte: `reason` is additive, never a replacement for a message this
|
|
255
294
|
// parser cannot yet interpret.
|
|
256
295
|
if (env.reason !== undefined) {
|
|
257
|
-
const keysUrl = env.keysUrl ??
|
|
296
|
+
const keysUrl = env.keysUrl ?? wording.keysUrl;
|
|
258
297
|
switch (env.reason) {
|
|
259
298
|
case "placeholder_key":
|
|
260
299
|
return ("Mnemoverse: your API key was rejected (401). The engine itself " +
|
|
@@ -309,7 +348,7 @@ function explain401(env) {
|
|
|
309
348
|
"MNEMOVERSE_API_KEY is not valid — if it still reads a docs " +
|
|
310
349
|
'placeholder such as "mk_live_YOUR_KEY" or "mk_live_USER_KEY" (any ' +
|
|
311
350
|
"value they did not create at the console themselves) it must be " +
|
|
312
|
-
`replaced with a real key from ${
|
|
351
|
+
`replaced with a real key from ${wording.keysUrl}. Do not retry until they ` +
|
|
313
352
|
"replace it.");
|
|
314
353
|
}
|
|
315
354
|
if (m !== undefined || env.code !== undefined) {
|
|
@@ -326,33 +365,48 @@ function explain401(env) {
|
|
|
326
365
|
"client cannot tell WHO refused. Quote the detail below and check the " +
|
|
327
366
|
"path to the API first.");
|
|
328
367
|
}
|
|
329
|
-
/** 403: when the ENGINE refused, the
|
|
330
|
-
* was refused — name WHICH refusal, from the engine's own message,
|
|
331
|
-
* blame the
|
|
332
|
-
* spoke: a proxy, a WAF or a tunnel also answers 403, in
|
|
333
|
-
* "the
|
|
334
|
-
* have seen is exactly the confident wrong
|
|
335
|
-
|
|
368
|
+
/** 403: when the ENGINE refused, the credential was accepted and then the
|
|
369
|
+
* request was refused — name WHICH refusal, from the engine's own message,
|
|
370
|
+
* and never blame the credential. But that first clause is only known when
|
|
371
|
+
* the engine actually spoke: a proxy, a WAF or a tunnel also answers 403, in
|
|
372
|
+
* HTML, and asserting "the credential identified the account fine" about a
|
|
373
|
+
* response the engine may never have seen is exactly the confident wrong
|
|
374
|
+
* cause this module exists to avoid.
|
|
375
|
+
*
|
|
376
|
+
* Under `wording.auth === "oauth"` every noun for the credential changes
|
|
377
|
+
* and nothing else does: the clause naming it as innocent ("The API key" /
|
|
378
|
+
* "Your sign-in"), the holder the room-permission causes speak about ("This
|
|
379
|
+
* key" / "This account", since an OAuth user holds no key and membership is
|
|
380
|
+
* the account's), and the one word the opaque-403 branch declines to blame
|
|
381
|
+
* ("the key" / "the sign-in"). Review round 2 on the wording slice: the
|
|
382
|
+
* first cut changed only the innocent clause and left "This key is not an
|
|
383
|
+
* active member" in the same sentence that had just told an OAuth user
|
|
384
|
+
* their sign-in was fine. */
|
|
385
|
+
function explain403(env, wording) {
|
|
386
|
+
const oauth = wording.auth === "oauth";
|
|
336
387
|
if (saidNothing(env)) {
|
|
388
|
+
const blamed = oauth ? "the sign-in" : "the key";
|
|
337
389
|
return ("Mnemoverse: this request was refused (403) by something that did not " +
|
|
338
390
|
"speak this API's error language — the body carries neither of the two " +
|
|
339
391
|
"shapes the engine produces. That points at a proxy, a gateway, or a " +
|
|
340
392
|
"MNEMOVERSE_API_URL aimed somewhere unexpected, and the engine may never " +
|
|
341
|
-
|
|
393
|
+
`have seen the request — so do not blame ${blamed} and do not blame room ` +
|
|
342
394
|
"permissions: this client cannot tell WHO refused it. Quote the detail " +
|
|
343
395
|
"below to the user, and do not retry until the path to the API is explained.");
|
|
344
396
|
}
|
|
397
|
+
const holder = oauth ? "This account" : "This key";
|
|
398
|
+
const holderLc = oauth ? "this account" : "this key";
|
|
345
399
|
const m = env.message;
|
|
346
400
|
const cause = has(m, "archiv")
|
|
347
401
|
? "The room you addressed is archived. An archived room refuses every read " +
|
|
348
402
|
"and every write, for its owner as much as for a member, and this client " +
|
|
349
403
|
"has no operation that reopens one."
|
|
350
404
|
: has(m, "not an active member") || has(m, "member of this room")
|
|
351
|
-
?
|
|
405
|
+
? `${holder} is not an active member of the room you addressed. Ask the ` +
|
|
352
406
|
"room's owner for an invite; memory_list_rooms shows the rooms it can " +
|
|
353
407
|
"already reach."
|
|
354
408
|
: has(m, "read-only")
|
|
355
|
-
?
|
|
409
|
+
? `${holder}'s membership in that room is read-only — it can read the ` +
|
|
356
410
|
"room but not write to it. Ask the room's owner for write access."
|
|
357
411
|
: has(m, "invalid room address")
|
|
358
412
|
? 'The room address was not in the form the engine accepts ' +
|
|
@@ -360,12 +414,13 @@ function explain403(env) {
|
|
|
360
414
|
"rather than composing one."
|
|
361
415
|
: has(m, "own this room")
|
|
362
416
|
? "That room belongs to another account, and only its owner can do " +
|
|
363
|
-
|
|
364
|
-
:
|
|
417
|
+
`this. memory_list_rooms shows which rooms ${holderLc} owns.`
|
|
418
|
+
: `Something about this request is not permitted for ${holderLc} — ` +
|
|
365
419
|
"most often the room it addressed. Check memory_list_rooms, and " +
|
|
366
420
|
"if nothing there explains it, tell the user exactly what was " +
|
|
367
421
|
"refused instead of guessing.";
|
|
368
|
-
|
|
422
|
+
const subject = oauth ? "Your sign-in" : "The API key";
|
|
423
|
+
return (`Mnemoverse: this request was refused (403). ${subject} is NOT the problem ` +
|
|
369
424
|
"— it identified the account fine, and this was a permission decision. " +
|
|
370
425
|
`${cause} Do not retry the same call: it will be refused again.`);
|
|
371
426
|
}
|
|
@@ -444,15 +499,20 @@ function explain404(f, env) {
|
|
|
444
499
|
}
|
|
445
500
|
/** 429: three causes, opposite advice. The envelope's `retryable` is the
|
|
446
501
|
* discriminator, because it is the only thing the engine states outright. */
|
|
447
|
-
function explain429(f, env) {
|
|
502
|
+
function explain429(f, env, wording) {
|
|
448
503
|
const secs = retryAfterSeconds(f.retryAfter);
|
|
449
504
|
const wait = secs === undefined ? "about a minute" : `${secs} seconds`;
|
|
450
505
|
if (env.retryable === true) {
|
|
506
|
+
// The one 429 sentence that names the credential holder (Sigma, review
|
|
507
|
+
// round 2 on the wording slice): "this key" for an API-key caller, "this
|
|
508
|
+
// account" for an OAuth user who holds no key. The other two branches
|
|
509
|
+
// speak about the account and the plan already.
|
|
510
|
+
const holder = wording.auth === "oauth" ? "this account" : "this key";
|
|
451
511
|
return ("Mnemoverse: rate-limited (429). This is the per-minute request limit and " +
|
|
452
512
|
`it clears by itself. Wait ${wait}, then make AT MOST ONE more attempt — ` +
|
|
453
513
|
"do not retry in a loop and do not fan out into more calls, which is what " +
|
|
454
514
|
"turns a one-minute limit into a sustained one. If the retry also fails, " +
|
|
455
|
-
|
|
515
|
+
`stop and tell the user ${holder} is hitting its rate limit.`);
|
|
456
516
|
}
|
|
457
517
|
if (env.retryable === false) {
|
|
458
518
|
return ("Mnemoverse: refused for quota, not for speed (429). Waiting will NOT " +
|
|
@@ -467,25 +527,30 @@ function explain429(f, env) {
|
|
|
467
527
|
}
|
|
468
528
|
/**
|
|
469
529
|
* The agent-facing explanation for one failed call, with the raw body kept
|
|
470
|
-
* after it.
|
|
530
|
+
* after it (unless `wording.rawDetail === false`, STEP4-3).
|
|
471
531
|
*
|
|
472
532
|
* Total by construction: every status reaches a sentence, and the fallback says
|
|
473
533
|
* that it has no specific guidance instead of inventing some.
|
|
534
|
+
*
|
|
535
|
+
* `wording` (STEP4-2/3, owner 2026-09-24) is optional and, absent, resolves
|
|
536
|
+
* to exactly today's behaviour: every call site before this release passes
|
|
537
|
+
* none, so every existing pin stays byte-identical.
|
|
474
538
|
*/
|
|
475
|
-
export function explainApiFailure(f) {
|
|
539
|
+
export function explainApiFailure(f, wording) {
|
|
540
|
+
const resolved = resolveWording(wording);
|
|
476
541
|
const env = parseErrorEnvelope(f.body);
|
|
477
542
|
let guidance;
|
|
478
543
|
if (f.status === 401) {
|
|
479
|
-
guidance = explain401(env);
|
|
544
|
+
guidance = explain401(env, resolved);
|
|
480
545
|
}
|
|
481
546
|
else if (f.status === 403) {
|
|
482
|
-
guidance = explain403(env);
|
|
547
|
+
guidance = explain403(env, resolved);
|
|
483
548
|
}
|
|
484
549
|
else if (f.status === 404) {
|
|
485
550
|
guidance = explain404(f, env);
|
|
486
551
|
}
|
|
487
552
|
else if (f.status === 429) {
|
|
488
|
-
guidance = explain429(f, env);
|
|
553
|
+
guidance = explain429(f, env, resolved);
|
|
489
554
|
}
|
|
490
555
|
else if (f.status === 400 || f.status === 422) {
|
|
491
556
|
guidance =
|
|
@@ -511,7 +576,7 @@ export function explainApiFailure(f) {
|
|
|
511
576
|
"specific guidance for that status. Do not invent a cause for the user — " +
|
|
512
577
|
"quote the detail below. One retry is acceptable; a loop is not.";
|
|
513
578
|
}
|
|
514
|
-
return `${guidance}\n\n${rawDetail(f)}
|
|
579
|
+
return resolved.rawDetail ? `${guidance}\n\n${rawDetail(f)}` : guidance;
|
|
515
580
|
}
|
|
516
581
|
/**
|
|
517
582
|
* A server-controlled string, made safe to quote inside model-facing text.
|
|
@@ -594,12 +659,13 @@ function refusedRedirect(cause) {
|
|
|
594
659
|
* that case now. Every sentence below asserts that nothing answered, so it may
|
|
595
660
|
* only be reached when nothing did.
|
|
596
661
|
*/
|
|
597
|
-
export function explainNetworkFailure(method, path, cause) {
|
|
662
|
+
export function explainNetworkFailure(method, path, cause, wording) {
|
|
663
|
+
const resolved = resolveWording(wording);
|
|
598
664
|
const name = cause instanceof Error ? cause.name : "";
|
|
599
665
|
const timedOut = name === "TimeoutError" || name === "AbortError";
|
|
600
666
|
const detail = cause instanceof Error ? `${cause.name}: ${cause.message}` : String(cause);
|
|
601
667
|
if (refusedRedirect(cause)) {
|
|
602
|
-
|
|
668
|
+
const head = `Mnemoverse: ${method} ${path} was answered with a REDIRECT, and this ` +
|
|
603
669
|
"client refused to follow it — so the API key was never sent to whatever " +
|
|
604
670
|
"the redirect pointed at. That refusal is the whole point: following a " +
|
|
605
671
|
"redirect re-sends the request headers to the new host, which hands a " +
|
|
@@ -610,7 +676,10 @@ export function explainNetworkFailure(method, path, cause) {
|
|
|
610
676
|
"network: the request arrived somewhere and was answered. Tell the user " +
|
|
611
677
|
"to check MNEMOVERSE_API_URL and point it straight at the API (the " +
|
|
612
678
|
`default is ${DEFAULT_API_URL}). Do not retry: the same address will ` +
|
|
613
|
-
|
|
679
|
+
"redirect again.";
|
|
680
|
+
return resolved.rawDetail
|
|
681
|
+
? `${head}\n\n${rawTransportDetail(method, path, detail)}`
|
|
682
|
+
: head;
|
|
614
683
|
}
|
|
615
684
|
const head = timedOut
|
|
616
685
|
? `Mnemoverse: the memory service did not answer ${method} ${path} in time.`
|
|
@@ -620,11 +689,11 @@ export function explainNetworkFailure(method, path, cause) {
|
|
|
620
689
|
? "The service may just be slow right now."
|
|
621
690
|
: "That is a connectivity or DNS problem, or MNEMOVERSE_API_URL pointing at " +
|
|
622
691
|
"a host that does not answer.";
|
|
623
|
-
|
|
692
|
+
const body = `${head} ${cause_} This is NOT a rejected API key and NOT a quota — no ` +
|
|
624
693
|
"reply arrived to say anything about either, so do not send the user to " +
|
|
625
694
|
"check their key. One retry is reasonable. If that also fails, tell the " +
|
|
626
|
-
"user memory is unreachable and carry on without it rather than retrying
|
|
627
|
-
|
|
695
|
+
"user memory is unreachable and carry on without it rather than retrying.";
|
|
696
|
+
return resolved.rawDetail ? `${body}\n\n${rawTransportDetail(method, path, detail)}` : body;
|
|
628
697
|
}
|
|
629
698
|
/** The debugging half of a transport failure, in one place so the redirect
|
|
630
699
|
* branch and the generic one cannot drift into two spellings of it. */
|
|
@@ -657,7 +726,8 @@ const MAX_PREVIEW_CHARS = 200;
|
|
|
657
726
|
* say whether the operation ran, because a body it cannot read is no evidence
|
|
658
727
|
* either way. That last clause matters most for a write.
|
|
659
728
|
*/
|
|
660
|
-
export function explainUnreadableBody(f) {
|
|
729
|
+
export function explainUnreadableBody(f, wording) {
|
|
730
|
+
const resolved = resolveWording(wording);
|
|
661
731
|
// TWO ARMS, because the two failures have different causes and the wrong one
|
|
662
732
|
// is a wrong instruction. A body that PARSED WRONG is a foreign answer — a
|
|
663
733
|
// portal, a proxy, a base URL aimed elsewhere. A body that stopped ARRIVING
|
|
@@ -680,10 +750,11 @@ export function explainUnreadableBody(f) {
|
|
|
680
750
|
`nothing about the user's key. One retry is reasonable; if it ` +
|
|
681
751
|
`repeats, tell the user the reply is arriving incomplete and quote ` +
|
|
682
752
|
`the detail below, status included.`;
|
|
683
|
-
|
|
753
|
+
const head = `Mnemoverse: something answered HTTP ${f.status} for ${f.method} ` +
|
|
684
754
|
`${f.path}, but ${body} Whether the operation itself ran is unknown ` +
|
|
685
755
|
`either way — if this was a write, treat it as neither saved nor ` +
|
|
686
|
-
|
|
756
|
+
"refused.";
|
|
757
|
+
return resolved.rawDetail ? `${head}\n\n${rawUnreadableDetail(f)}` : head;
|
|
687
758
|
}
|
|
688
759
|
/** The debugging half for an unreadable body: what stopped the read, and the
|
|
689
760
|
* first bytes of what arrived — through the same inert filter as
|
|
@@ -708,12 +779,25 @@ export class UnreadableBodyError extends Error {
|
|
|
708
779
|
status;
|
|
709
780
|
method;
|
|
710
781
|
path;
|
|
711
|
-
|
|
712
|
-
|
|
782
|
+
/** The bytes that failed to parse, when they were read at all. Kept so
|
|
783
|
+
* {@link withWording} re-renders the same arm of the explanation. */
|
|
784
|
+
bodyPreview;
|
|
785
|
+
/** `wording` (STEP4-2/3): optional, and absent (every call site before this
|
|
786
|
+
* release) reproduces today's message exactly: see {@link explainUnreadableBody}. */
|
|
787
|
+
constructor(f, wording) {
|
|
788
|
+
super(explainUnreadableBody(f, wording), { cause: f.cause });
|
|
713
789
|
this.name = "UnreadableBodyError";
|
|
714
790
|
this.status = f.status;
|
|
715
791
|
this.method = f.method;
|
|
716
792
|
this.path = f.path;
|
|
793
|
+
this.bodyPreview = f.bodyPreview;
|
|
794
|
+
}
|
|
795
|
+
/** The same failure, explained under `wording`: see {@link ApiError.withWording}. */
|
|
796
|
+
withWording(wording) {
|
|
797
|
+
const f = { status: this.status, method: this.method, path: this.path, cause: this.cause };
|
|
798
|
+
if (this.bodyPreview !== undefined)
|
|
799
|
+
f.bodyPreview = this.bodyPreview;
|
|
800
|
+
return new UnreadableBodyError(f, wording);
|
|
717
801
|
}
|
|
718
802
|
}
|
|
719
803
|
/**
|
|
@@ -744,12 +828,18 @@ function explain409(f) {
|
|
|
744
828
|
export class NetworkError extends Error {
|
|
745
829
|
method;
|
|
746
830
|
path;
|
|
747
|
-
|
|
748
|
-
|
|
831
|
+
/** `wording` (STEP4-2/3): optional, and absent (every call site before this
|
|
832
|
+
* release) reproduces today's message exactly: see {@link explainNetworkFailure}. */
|
|
833
|
+
constructor(method, path, cause, wording) {
|
|
834
|
+
super(explainNetworkFailure(method, path, cause, wording), { cause });
|
|
749
835
|
this.name = "NetworkError";
|
|
750
836
|
this.method = method;
|
|
751
837
|
this.path = path;
|
|
752
838
|
}
|
|
839
|
+
/** The same failure, explained under `wording`: see {@link ApiError.withWording}. */
|
|
840
|
+
withWording(wording) {
|
|
841
|
+
return new NetworkError(this.method, this.path, this.cause, wording);
|
|
842
|
+
}
|
|
753
843
|
}
|
|
754
844
|
/**
|
|
755
845
|
* A failed API call, as an ERROR OBJECT rather than a parsed string.
|
|
@@ -769,15 +859,36 @@ export class ApiError extends Error {
|
|
|
769
859
|
path;
|
|
770
860
|
/** The engine's envelope, already parsed — so a caller never re-parses. */
|
|
771
861
|
envelope;
|
|
772
|
-
|
|
773
|
-
|
|
862
|
+
/** The `Retry-After` header as it arrived, when the response carried one.
|
|
863
|
+
* Kept so {@link withWording} re-renders a 429 with its retry sentence. */
|
|
864
|
+
retryAfter;
|
|
865
|
+
/** `wording` (STEP4-2/3): optional, and absent (every call site before this
|
|
866
|
+
* release) reproduces today's message exactly: see {@link explainApiFailure}. */
|
|
867
|
+
constructor(f, wording) {
|
|
868
|
+
super(explainApiFailure(f, wording));
|
|
774
869
|
this.name = "ApiError";
|
|
775
870
|
this.status = f.status;
|
|
776
871
|
this.body = f.body;
|
|
777
872
|
this.method = f.method;
|
|
778
873
|
this.path = f.path;
|
|
874
|
+
this.retryAfter = f.retryAfter;
|
|
779
875
|
this.envelope = parseErrorEnvelope(f.body);
|
|
780
876
|
}
|
|
877
|
+
/**
|
|
878
|
+
* The same failure, explained under `wording`: a new instance of this
|
|
879
|
+
* class whose message is what the constructor would have produced with
|
|
880
|
+
* that `wording`, every field and {@link isBare404} unchanged. The message
|
|
881
|
+
* is a pure function of the failure and the wording, so re-rendering an
|
|
882
|
+
* instance under the wording it was built with gives the same text back.
|
|
883
|
+
* This is how `MemoryToolDeps.wording` reaches an error the consumer's
|
|
884
|
+
* `apiFetch` built without it: see {@link rewordFailure}.
|
|
885
|
+
*/
|
|
886
|
+
withWording(wording) {
|
|
887
|
+
const f = { status: this.status, body: this.body, method: this.method, path: this.path };
|
|
888
|
+
if (this.retryAfter !== undefined)
|
|
889
|
+
f.retryAfter = this.retryAfter;
|
|
890
|
+
return new ApiError(f, wording);
|
|
891
|
+
}
|
|
781
892
|
/**
|
|
782
893
|
* A 404 the ENGINE did not answer: silence, or the router's literal
|
|
783
894
|
* defaults. This is what a path the deployment does not serve looks like
|
|
@@ -799,4 +910,27 @@ export class ApiError extends Error {
|
|
|
799
910
|
return this.status === 404 && engineSilentOn404(this.envelope);
|
|
800
911
|
}
|
|
801
912
|
}
|
|
913
|
+
/**
|
|
914
|
+
* Re-explain one of this module's three errors under `wording`; hand
|
|
915
|
+
* anything else back untouched.
|
|
916
|
+
*
|
|
917
|
+
* This is how `MemoryToolDeps.wording` reaches the error text (STEP4-2): a
|
|
918
|
+
* consumer's `apiFetch` constructs `ApiError`, `NetworkError` and
|
|
919
|
+
* `UnreadableBodyError` however it likes, and `registerMemoryTools` passes
|
|
920
|
+
* every rejection through here with the `wording` it was given, so the
|
|
921
|
+
* consumer states its wording once, on `deps`, and the constructors need no
|
|
922
|
+
* second copy. Re-rendering is idempotent (see {@link ApiError.withWording}),
|
|
923
|
+
* so an `apiFetch` that does pass the same `wording` to the constructors
|
|
924
|
+
* gets the same text either way; one that passed a different wording gets
|
|
925
|
+
* `deps.wording`, the one source of truth for this registration. A
|
|
926
|
+
* rejection that is none of the three classes (a plain `Error`, an
|
|
927
|
+
* `McpError`) is returned as is: it carries no wording to apply, and its
|
|
928
|
+
* identity may matter to whoever threw it.
|
|
929
|
+
*/
|
|
930
|
+
export function rewordFailure(e, wording) {
|
|
931
|
+
if (e instanceof ApiError || e instanceof NetworkError || e instanceof UnreadableBodyError) {
|
|
932
|
+
return e.withWording(wording);
|
|
933
|
+
}
|
|
934
|
+
return e;
|
|
935
|
+
}
|
|
802
936
|
//# sourceMappingURL=errors.js.map
|