@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 +19 -0
- package/dist/index.js +9 -20
- package/dist/quota-error.js +215 -0
- package/dist/server-backend.js +20 -9
- package/package.json +1 -1
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
+
}
|
package/dist/server-backend.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
132
|
+
const text = await resp.text();
|
|
123
133
|
if (!resp.ok) {
|
|
124
|
-
|
|
134
|
+
const data = parseJsonOrUndefined(text);
|
|
135
|
+
throw new Mem9HttpError(messageFromErrorBody(resp.status, text, data), resp.status, text, data);
|
|
125
136
|
}
|
|
126
|
-
return
|
|
137
|
+
return JSON.parse(text);
|
|
127
138
|
}
|
|
128
139
|
}
|
package/package.json
CHANGED