@mem9/mem9 0.4.12 → 0.4.13

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/hooks.js CHANGED
@@ -11,6 +11,7 @@
11
11
  * Reference: OpenClaw's built-in memory-lancedb extension uses the same pattern.
12
12
  */
13
13
  import { isPendingProvisionError } from "./backend.js";
14
+ import { formatRuntimeQuotaNotice } from "./quota-error.js";
14
15
  // ---------------------------------------------------------------------------
15
16
  // Constants
16
17
  // ---------------------------------------------------------------------------
@@ -21,6 +22,7 @@ const MAX_CONTENT_LEN = 500; // truncate individual memory content in prompt
21
22
  // Ingest defaults — configurable via maxIngestBytes in plugin config
22
23
  const DEFAULT_MAX_INGEST_BYTES = 200_000; // ~200KB safe for most LLM context windows
23
24
  const MAX_INGEST_MESSAGES = 20; // absolute cap even if small messages
25
+ const BACKGROUND_QUOTA_PAUSED_LOG = "[mem9] memory saving paused by runtime quota";
24
26
  function previewText(text, maxLen = 160) {
25
27
  const normalized = text.replace(/\s+/g, " ").trim();
26
28
  if (normalized.length <= maxLen) {
@@ -166,6 +168,13 @@ export function registerHooks(api, backend, logger, options) {
166
168
  if (isPendingProvisionError(err)) {
167
169
  return;
168
170
  }
171
+ const quotaNotice = formatRuntimeQuotaNotice(err, "recall paused");
172
+ if (quotaNotice) {
173
+ logger.info(quotaNotice);
174
+ return {
175
+ prependContext: quotaNotice,
176
+ };
177
+ }
169
178
  // Graceful degradation — never block the LLM call
170
179
  logger.error(`[mem9] before_prompt_build failed: ${String(err)}`);
171
180
  }
@@ -215,6 +224,11 @@ export function registerHooks(api, backend, logger, options) {
215
224
  if (isPendingProvisionError(err)) {
216
225
  return;
217
226
  }
227
+ const quotaNotice = formatRuntimeQuotaNotice(err, "before_reset save paused");
228
+ if (quotaNotice) {
229
+ logger.info(BACKGROUND_QUOTA_PAUSED_LOG);
230
+ return;
231
+ }
218
232
  // Best-effort — never block /reset
219
233
  logger.error(`[mem9] before_reset save failed: ${String(err)}`);
220
234
  }
@@ -310,6 +324,11 @@ export function registerHooks(api, backend, logger, options) {
310
324
  if (isPendingProvisionError(err)) {
311
325
  return;
312
326
  }
327
+ const quotaNotice = formatRuntimeQuotaNotice(err, "agent_end ingest paused");
328
+ if (quotaNotice) {
329
+ logger.info(BACKGROUND_QUOTA_PAUSED_LOG);
330
+ return;
331
+ }
313
332
  // Best-effort — never fail the agent end phase
314
333
  }
315
334
  });
package/dist/index.js CHANGED
@@ -5,6 +5,7 @@ import path from "node:path";
5
5
  import { PendingProvisionError, isPendingProvisionError } from "./backend.js";
6
6
  import { DEFAULT_SEARCH_TIMEOUT_MS, DEFAULT_TIMEOUT_MS, ServerBackend, } from "./server-backend.js";
7
7
  import { registerHooks } from "./hooks.js";
8
+ import { toolErrorPayload } from "./quota-error.js";
8
9
  const DEFAULT_API_URL = "https://api.mem9.ai";
9
10
  const TIMEOUT_FIELDS = ["defaultTimeoutMs", "searchTimeoutMs"];
10
11
  const SHARED_PROVISION_DIR = path.join(os.homedir(), ".openclaw", "mem9", "provision");
@@ -47,6 +48,9 @@ function jsonResult(data) {
47
48
  function errorMessage(err) {
48
49
  return err instanceof Error ? err.message : String(err);
49
50
  }
51
+ function jsonToolError(err) {
52
+ return jsonResult(toolErrorPayload(err));
53
+ }
50
54
  function sleep(ms) {
51
55
  return new Promise((resolve) => setTimeout(resolve, ms));
52
56
  }
@@ -248,10 +252,7 @@ function buildTools(backend) {
248
252
  return jsonResult({ ok: true, data: result });
249
253
  }
250
254
  catch (err) {
251
- return jsonResult({
252
- ok: false,
253
- error: err instanceof Error ? err.message : String(err),
254
- });
255
+ return jsonToolError(err);
255
256
  }
256
257
  },
257
258
  },
@@ -287,10 +288,7 @@ function buildTools(backend) {
287
288
  return jsonResult({ ok: true, ...result });
288
289
  }
289
290
  catch (err) {
290
- return jsonResult({
291
- ok: false,
292
- error: err instanceof Error ? err.message : String(err),
293
- });
291
+ return jsonToolError(err);
294
292
  }
295
293
  },
296
294
  },
@@ -314,10 +312,7 @@ function buildTools(backend) {
314
312
  return jsonResult({ ok: true, data: result });
315
313
  }
316
314
  catch (err) {
317
- return jsonResult({
318
- ok: false,
319
- error: err instanceof Error ? err.message : String(err),
320
- });
315
+ return jsonToolError(err);
321
316
  }
322
317
  },
323
318
  },
@@ -349,10 +344,7 @@ function buildTools(backend) {
349
344
  return jsonResult({ ok: true, data: result });
350
345
  }
351
346
  catch (err) {
352
- return jsonResult({
353
- ok: false,
354
- error: err instanceof Error ? err.message : String(err),
355
- });
347
+ return jsonToolError(err);
356
348
  }
357
349
  },
358
350
  },
@@ -376,10 +368,7 @@ function buildTools(backend) {
376
368
  return jsonResult({ ok: true });
377
369
  }
378
370
  catch (err) {
379
- return jsonResult({
380
- ok: false,
381
- error: err instanceof Error ? err.message : String(err),
382
- });
371
+ return jsonToolError(err);
383
372
  }
384
373
  },
385
374
  },
@@ -0,0 +1,215 @@
1
+ export class Mem9HttpError extends Error {
2
+ status;
3
+ body;
4
+ data;
5
+ constructor(message, status, body, data) {
6
+ super(message);
7
+ this.status = status;
8
+ this.body = body;
9
+ this.data = data;
10
+ this.name = "Mem9HttpError";
11
+ }
12
+ }
13
+ function isRecord(value) {
14
+ return typeof value === "object" && value !== null && !Array.isArray(value);
15
+ }
16
+ function normalizeString(value) {
17
+ return typeof value === "string" ? value.trim() : "";
18
+ }
19
+ function normalizePositiveInteger(value) {
20
+ return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : null;
21
+ }
22
+ export function parseJsonOrUndefined(text) {
23
+ if (!text.trim()) {
24
+ return undefined;
25
+ }
26
+ try {
27
+ return JSON.parse(text);
28
+ }
29
+ catch {
30
+ return undefined;
31
+ }
32
+ }
33
+ export function messageFromErrorBody(status, body, data) {
34
+ if (isRecord(data)) {
35
+ const message = normalizeString(data.message);
36
+ if (message) {
37
+ return message;
38
+ }
39
+ const error = normalizeString(data.error);
40
+ if (error) {
41
+ return error;
42
+ }
43
+ }
44
+ const text = body.trim();
45
+ return text || `HTTP ${status}`;
46
+ }
47
+ function normalizeRecommendedAction(runtimeQuota) {
48
+ const nested = isRecord(runtimeQuota.recommendedAction) ? runtimeQuota.recommendedAction : {};
49
+ const providerActionCode = normalizeString(nested.providerActionCode);
50
+ const severity = normalizeString(nested.severity);
51
+ const type = normalizeString(nested.type);
52
+ const url = normalizeString(nested.url);
53
+ if (!providerActionCode && !severity && !type && !url) {
54
+ return null;
55
+ }
56
+ return {
57
+ ...(providerActionCode ? { providerActionCode } : {}),
58
+ ...(severity ? { severity } : {}),
59
+ ...(type ? { type } : {}),
60
+ ...(url ? { url } : {}),
61
+ };
62
+ }
63
+ function quotaGateReason(runtimeQuota) {
64
+ const quotaGateResult = isRecord(runtimeQuota.quotaGateResult) ? runtimeQuota.quotaGateResult : {};
65
+ return normalizeString(quotaGateResult.reason);
66
+ }
67
+ function retryAfterSeconds(runtimeQuota) {
68
+ const direct = normalizePositiveInteger(runtimeQuota.retryAfterSeconds);
69
+ if (direct !== null) {
70
+ return direct;
71
+ }
72
+ const quotaGateResult = isRecord(runtimeQuota.quotaGateResult) ? runtimeQuota.quotaGateResult : {};
73
+ const postQuotaRateLimit = isRecord(quotaGateResult.postQuotaRateLimit) ? quotaGateResult.postQuotaRateLimit : {};
74
+ return normalizePositiveInteger(postQuotaRateLimit.retryAfterSeconds);
75
+ }
76
+ function isPostQuotaRateLimited(denied) {
77
+ return denied.status === 429 ||
78
+ denied.quotaGateReason === "postQuotaRateLimitExceeded";
79
+ }
80
+ function quotaReason(denied) {
81
+ if (isPostQuotaRateLimited(denied)) {
82
+ return "this API key has reached the temporary request limit for this memory feature";
83
+ }
84
+ const providerActionCode = normalizeString(denied.recommendedAction?.providerActionCode);
85
+ if (providerActionCode === "claimApiKey") {
86
+ return "the included usage quota for this API key has been used up";
87
+ }
88
+ if (providerActionCode === "increaseSpendingLimit") {
89
+ return "the configured spending limit would be exceeded";
90
+ }
91
+ if (providerActionCode === "enableOnDemand") {
92
+ return "the included usage quota has been used up and on-demand usage is not enabled";
93
+ }
94
+ if (providerActionCode === "upgradePlan") {
95
+ return "the included usage quota for this mem9 account has been used up";
96
+ }
97
+ if (providerActionCode === "resolveAccountState") {
98
+ return "the current account or billing state blocks runtime memory access";
99
+ }
100
+ return "the runtime quota check blocked this request";
101
+ }
102
+ function quotaNoticeSubject(denied, operation) {
103
+ const meter = normalizeString(denied.meter);
104
+ if (meter === "memory_write_requests") {
105
+ return {
106
+ headline: "Mem9 memory saving is temporarily unavailable",
107
+ userState: "mem9 cannot save new memories right now",
108
+ };
109
+ }
110
+ if (meter === "memory_recall_requests") {
111
+ return {
112
+ headline: "Mem9 recall is temporarily unavailable",
113
+ userState: "mem9 cannot recall memories right now",
114
+ };
115
+ }
116
+ const operationText = normalizeString(operation).toLowerCase();
117
+ if (/\b(ingest|save|store|write)\b/.test(operationText)) {
118
+ return {
119
+ headline: "Mem9 memory saving is temporarily unavailable",
120
+ userState: "mem9 cannot save new memories right now",
121
+ };
122
+ }
123
+ if (/\b(recall|search)\b/.test(operationText)) {
124
+ return {
125
+ headline: "Mem9 recall is temporarily unavailable",
126
+ userState: "mem9 cannot recall memories right now",
127
+ };
128
+ }
129
+ return {
130
+ headline: "Mem9 memory is temporarily unavailable",
131
+ userState: "mem9 cannot complete the memory request right now",
132
+ };
133
+ }
134
+ function actionUrlForDenied(denied) {
135
+ return normalizeString(denied.recommendedAction?.url);
136
+ }
137
+ function actionInstruction(denied) {
138
+ const action = denied.recommendedAction;
139
+ const providerActionCode = normalizeString(action?.providerActionCode);
140
+ const actionUrl = actionUrlForDenied(denied);
141
+ if (!actionUrl) {
142
+ if (isPostQuotaRateLimited(denied)) {
143
+ return "Tell them that the quota/rate-limit check blocked this request and to retry later or open the mem9 console to review account and billing settings.";
144
+ }
145
+ return "Ask them to open the mem9 console to resolve the account or billing state.";
146
+ }
147
+ switch (providerActionCode) {
148
+ case "claimApiKey":
149
+ return `Ask them to open this link to sign in or create a mem9 account and claim this API key: ${actionUrl}. After claiming the key, they can upgrade their plan or set up billing to get more usage. Include the link exactly as written.`;
150
+ case "upgradePlan":
151
+ return `Ask them to open this link to upgrade their mem9 plan and get more included usage: ${actionUrl}. Include the link exactly as written.`;
152
+ case "increaseSpendingLimit":
153
+ return `Ask them to open this link to increase the mem9 spending limit or adjust billing settings: ${actionUrl}. Include the link exactly as written.`;
154
+ case "enableOnDemand":
155
+ return `Ask them to open this link to enable billing or on-demand usage for mem9: ${actionUrl}. Include the link exactly as written.`;
156
+ case "resolveAccountState":
157
+ return `Ask them to open this mem9 link to resolve the account or billing state: ${actionUrl}. Include the link exactly as written.`;
158
+ default:
159
+ return `Ask them to open this mem9 link to resolve the account or billing state: ${actionUrl}. Include the link exactly as written.`;
160
+ }
161
+ }
162
+ export function parseRuntimeQuotaDenied(value) {
163
+ const payload = value instanceof Mem9HttpError ? value.data : value;
164
+ if (!isRecord(payload)) {
165
+ return null;
166
+ }
167
+ const details = isRecord(payload.details) ? payload.details : {};
168
+ if (normalizeString(details.errorCategory) !== "runtime_quota_denied") {
169
+ return null;
170
+ }
171
+ const runtimeQuota = isRecord(details.runtimeQuota) ? details.runtimeQuota : {};
172
+ const recommendedAction = normalizeRecommendedAction(runtimeQuota);
173
+ const retryAfter = retryAfterSeconds(runtimeQuota);
174
+ const reason = quotaGateReason(runtimeQuota);
175
+ return {
176
+ status: value instanceof Mem9HttpError ? value.status : null,
177
+ code: "runtime_quota_denied",
178
+ message: normalizeString(payload.error) || "Runtime usage quota denied.",
179
+ ...(normalizeString(runtimeQuota.meter) ? { meter: normalizeString(runtimeQuota.meter) } : {}),
180
+ ...(reason ? { quotaGateReason: reason } : {}),
181
+ ...(retryAfter !== null ? { retryAfterSeconds: retryAfter } : {}),
182
+ ...(recommendedAction ? { recommendedAction } : {}),
183
+ };
184
+ }
185
+ export function formatRuntimeQuotaNotice(value, operation) {
186
+ const denied = parseRuntimeQuotaDenied(value);
187
+ if (!denied) {
188
+ return "";
189
+ }
190
+ const subject = quotaNoticeSubject(denied, operation);
191
+ return `${subject.headline} because ${quotaReason(denied)}. In your reply, briefly tell the user that ${subject.userState}. ${actionInstruction(denied)}`;
192
+ }
193
+ export function toolErrorPayload(error) {
194
+ const denied = parseRuntimeQuotaDenied(error);
195
+ if (denied) {
196
+ const actionUrl = actionUrlForDenied(denied);
197
+ return {
198
+ ok: false,
199
+ error: denied.message,
200
+ status_code: denied.status,
201
+ code: denied.code,
202
+ quota: {
203
+ code: denied.code,
204
+ message: denied.message,
205
+ ...(denied.retryAfterSeconds !== undefined ? { retryAfterSeconds: denied.retryAfterSeconds } : {}),
206
+ ...(denied.recommendedAction ? { recommendedAction: denied.recommendedAction } : {}),
207
+ },
208
+ ...(actionUrl ? { action_url: actionUrl } : {}),
209
+ };
210
+ }
211
+ return {
212
+ ok: false,
213
+ error: error instanceof Error ? error.message : String(error),
214
+ };
215
+ }
@@ -1,3 +1,4 @@
1
+ import { Mem9HttpError, messageFromErrorBody, parseJsonOrUndefined, } from "./quota-error.js";
1
2
  export const DEFAULT_TIMEOUT_MS = 8_000;
2
3
  export const DEFAULT_SEARCH_TIMEOUT_MS = 15_000;
3
4
  export class ServerBackend {
@@ -76,16 +77,22 @@ export class ServerBackend {
76
77
  try {
77
78
  return await this.request("GET", this.memoryPath(`/memories/${id}`));
78
79
  }
79
- catch {
80
- return null;
80
+ catch (err) {
81
+ if (err instanceof Mem9HttpError && err.status === 404) {
82
+ return null;
83
+ }
84
+ throw err;
81
85
  }
82
86
  }
83
87
  async update(id, input) {
84
88
  try {
85
89
  return await this.request("PUT", this.memoryPath(`/memories/${id}`), input);
86
90
  }
87
- catch {
88
- return null;
91
+ catch (err) {
92
+ if (err instanceof Mem9HttpError && err.status === 404) {
93
+ return null;
94
+ }
95
+ throw err;
89
96
  }
90
97
  }
91
98
  async remove(id) {
@@ -93,8 +100,11 @@ export class ServerBackend {
93
100
  await this.request("DELETE", this.memoryPath(`/memories/${id}`));
94
101
  return true;
95
102
  }
96
- catch {
97
- return false;
103
+ catch (err) {
104
+ if (err instanceof Mem9HttpError && err.status === 404) {
105
+ return false;
106
+ }
107
+ throw err;
98
108
  }
99
109
  }
100
110
  async ingest(input) {
@@ -119,10 +129,11 @@ export class ServerBackend {
119
129
  if (resp.status === 204) {
120
130
  return undefined;
121
131
  }
122
- const data = await resp.json();
132
+ const text = await resp.text();
123
133
  if (!resp.ok) {
124
- throw new Error(data.error || `HTTP ${resp.status}`);
134
+ const data = parseJsonOrUndefined(text);
135
+ throw new Mem9HttpError(messageFromErrorBody(resp.status, text, data), resp.status, text, data);
125
136
  }
126
- return data;
137
+ return JSON.parse(text);
127
138
  }
128
139
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mem9/mem9",
3
- "version": "0.4.12",
3
+ "version": "0.4.13",
4
4
  "description": "OpenClaw shared memory plugin — cloud-persistent memory with hybrid vector + keyword search via mnemo-server",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",