@dcrays/mbh-memory 0.1.1-beta.6 → 0.1.1-beta.8
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 +2 -2
- package/cordis.patch.yml +6 -6
- package/package.json +3 -1
- package/plugin/index.d.ts +5 -1
- package/plugin/index.js +179 -16
- package/src/index.d.ts +1 -0
- package/src/index.js +1 -0
- package/src/local-memory.d.ts +18 -0
- package/src/local-memory.js +109 -0
- package/src/tools.js +2 -2
package/README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# @dcrays/mbh-memory
|
|
2
2
|
|
|
3
|
-
书灵墨宝的 DSH
|
|
3
|
+
书灵墨宝的 DSH 长期记忆插件。加载后注册 `get_memory` 与 `set_memory`:未配置 `baseUrl` 时原子写入 `$DSH_HOME/memory/users/<account-hash>/MEMORY.md`,配置后使用原有云端 HTTP API。
|
|
4
4
|
|
|
5
|
-
单一发布包同时适用于生产与测试桌面。API 地址由 Cordis 插件配置下发,不读取 `.env
|
|
5
|
+
单一发布包同时适用于生产与测试桌面。API 地址由 Cordis 插件配置下发,不读取 `.env`;本地与云端数据不会自动互相覆盖。
|
package/cordis.patch.yml
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
- insert:
|
|
2
|
-
- id: mbh-memory
|
|
3
|
-
name: '@dcrays/mbh-memory'
|
|
4
|
-
config:
|
|
5
|
-
enabled: true
|
|
6
|
-
baseUrl: ""
|
|
1
|
+
- insert:
|
|
2
|
+
- id: mbh-memory
|
|
3
|
+
name: '@dcrays/mbh-memory'
|
|
4
|
+
config:
|
|
5
|
+
enabled: true
|
|
6
|
+
baseUrl: ""
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dcrays/mbh-memory",
|
|
3
|
-
"version": "0.1.1-beta.
|
|
3
|
+
"version": "0.1.1-beta.8",
|
|
4
4
|
"description": "DeepSeek Harness memory plugin for Mobook",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "plugin/index.js",
|
|
@@ -30,6 +30,8 @@
|
|
|
30
30
|
},
|
|
31
31
|
"peerDependencies": {
|
|
32
32
|
"@deepseek-ai/cordis": "^4.0.2",
|
|
33
|
+
"@deepseek-ai/dsh-atomic-write": "0.1.5-rc.2",
|
|
34
|
+
"@deepseek-ai/dsh-home-paths": "0.1.5-rc.2",
|
|
33
35
|
"@deepseek-ai/dsh-tools": "0.1.5-rc.2",
|
|
34
36
|
"@deepseek-ai/schemastery": "^3.18.1"
|
|
35
37
|
},
|
package/plugin/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Context } from '@deepseek-ai/cordis';
|
|
2
2
|
import { Service } from '@deepseek-ai/cordis';
|
|
3
3
|
import z from '@deepseek-ai/schemastery';
|
|
4
|
-
import { type MemoryApiValue } from '../src/index.js';
|
|
4
|
+
import { type MemoryApiValue, type MemoryOperations } from '../src/index.js';
|
|
5
5
|
export declare const name = "mbh-memory";
|
|
6
6
|
export declare const inject: string[];
|
|
7
7
|
export interface Config {
|
|
@@ -13,6 +13,9 @@ export interface Config {
|
|
|
13
13
|
maxResponseBytes: number;
|
|
14
14
|
}
|
|
15
15
|
export declare const Config: z<Config>;
|
|
16
|
+
export declare function localMemoryAccountScope(metadata: unknown): string;
|
|
17
|
+
export declare function resolveLocalMemoryFile(): string;
|
|
18
|
+
export declare function createMemoryOperations(config: Config): MemoryOperations | undefined;
|
|
16
19
|
declare module '@deepseek-ai/cordis' {
|
|
17
20
|
interface Context {
|
|
18
21
|
mbhMemory: MbhMemoryService;
|
|
@@ -24,6 +27,7 @@ export declare class MbhMemoryService extends Service {
|
|
|
24
27
|
constructor(ctx: Context, config: Config);
|
|
25
28
|
isEnabled(): boolean;
|
|
26
29
|
isConfigured(): boolean;
|
|
30
|
+
backend(): 'disabled' | 'local' | 'cloud';
|
|
27
31
|
getMemory(signal?: AbortSignal): Promise<MemoryApiValue>;
|
|
28
32
|
setMemory(memory: string, signal?: AbortSignal): Promise<MemoryApiValue>;
|
|
29
33
|
}
|
package/plugin/index.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { createRequire as __mobookCreateRequire } from 'node:module'; const require = __mobookCreateRequire(import.meta.url);
|
|
2
2
|
|
|
3
3
|
// dist/plugin/index.js
|
|
4
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
5
|
+
import { readFileSync } from "node:fs";
|
|
4
6
|
import { Service } from "@deepseek-ai/cordis";
|
|
7
|
+
import { dshHomePath } from "@deepseek-ai/dsh-home-paths";
|
|
5
8
|
import z from "@deepseek-ai/schemastery";
|
|
6
9
|
|
|
7
10
|
// dist/src/api-client.js
|
|
@@ -133,6 +136,121 @@ var MemoryApiClient = class {
|
|
|
133
136
|
}
|
|
134
137
|
};
|
|
135
138
|
|
|
139
|
+
// dist/src/local-memory.js
|
|
140
|
+
import { createHash } from "node:crypto";
|
|
141
|
+
import { mkdir, readFile } from "node:fs/promises";
|
|
142
|
+
import { dirname } from "node:path";
|
|
143
|
+
import { withFileLock, writeFileAtomic } from "@deepseek-ai/dsh-atomic-write";
|
|
144
|
+
var HEADER = `# Long-term Memory
|
|
145
|
+
|
|
146
|
+
<!-- Managed by mbh-memory. This Markdown file is the canonical local memory source. -->
|
|
147
|
+
`;
|
|
148
|
+
var LocalMemoryError = class extends Error {
|
|
149
|
+
code;
|
|
150
|
+
constructor(code, message, options) {
|
|
151
|
+
super(message, options);
|
|
152
|
+
this.code = code;
|
|
153
|
+
this.name = "LocalMemoryError";
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
function normalizeMemory(memory) {
|
|
157
|
+
return memory.replaceAll("\r\n", "\n").trim();
|
|
158
|
+
}
|
|
159
|
+
function entryId(memory) {
|
|
160
|
+
return createHash("sha256").update(memory).digest("hex");
|
|
161
|
+
}
|
|
162
|
+
function entry(memory, now) {
|
|
163
|
+
const id = entryId(memory);
|
|
164
|
+
return {
|
|
165
|
+
id,
|
|
166
|
+
markdown: `
|
|
167
|
+
## ${now.toISOString()}
|
|
168
|
+
|
|
169
|
+
<!-- mbh-memory:id=${id} -->
|
|
170
|
+
|
|
171
|
+
${memory}
|
|
172
|
+
`
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
function isMissing(error) {
|
|
176
|
+
return error?.code === "ENOENT";
|
|
177
|
+
}
|
|
178
|
+
var LocalMemoryStore = class {
|
|
179
|
+
config;
|
|
180
|
+
now;
|
|
181
|
+
constructor(config) {
|
|
182
|
+
this.config = config;
|
|
183
|
+
if (!config.file.trim())
|
|
184
|
+
throw new LocalMemoryError("INVALID_CONFIG", "mbh-memory local file is required");
|
|
185
|
+
if (!Number.isSafeInteger(config.maxMemoryChars) || config.maxMemoryChars <= 0) {
|
|
186
|
+
throw new LocalMemoryError("INVALID_CONFIG", "mbh-memory maxMemoryChars must be a positive integer");
|
|
187
|
+
}
|
|
188
|
+
if (!Number.isSafeInteger(config.maxFileBytes) || config.maxFileBytes <= 0) {
|
|
189
|
+
throw new LocalMemoryError("INVALID_CONFIG", "mbh-memory maxFileBytes must be a positive integer");
|
|
190
|
+
}
|
|
191
|
+
this.now = config.now ?? (() => /* @__PURE__ */ new Date());
|
|
192
|
+
}
|
|
193
|
+
async getMemory(signal) {
|
|
194
|
+
signal?.throwIfAborted();
|
|
195
|
+
let markdown;
|
|
196
|
+
try {
|
|
197
|
+
markdown = await readFile(this.config.file, "utf8");
|
|
198
|
+
} catch (error) {
|
|
199
|
+
if (isMissing(error))
|
|
200
|
+
return { backend: "local", memory: "" };
|
|
201
|
+
throw new LocalMemoryError("STORAGE_ERROR", "mbh-memory local memory could not be read", { cause: error });
|
|
202
|
+
}
|
|
203
|
+
signal?.throwIfAborted();
|
|
204
|
+
if (Buffer.byteLength(markdown) > this.config.maxFileBytes) {
|
|
205
|
+
throw new LocalMemoryError("STORAGE_LIMIT", `mbh-memory local memory exceeds ${this.config.maxFileBytes} bytes`);
|
|
206
|
+
}
|
|
207
|
+
return { backend: "local", memory: markdown };
|
|
208
|
+
}
|
|
209
|
+
async setMemory(memory, signal) {
|
|
210
|
+
const normalized = normalizeMemory(memory);
|
|
211
|
+
if (!normalized)
|
|
212
|
+
throw new LocalMemoryError("INVALID_MEMORY", "memory must not be empty");
|
|
213
|
+
if (normalized.length > this.config.maxMemoryChars) {
|
|
214
|
+
throw new LocalMemoryError("INVALID_MEMORY", `memory exceeds ${this.config.maxMemoryChars} characters`);
|
|
215
|
+
}
|
|
216
|
+
signal?.throwIfAborted();
|
|
217
|
+
await mkdir(dirname(this.config.file), { recursive: true, mode: 448 }).catch((error) => {
|
|
218
|
+
throw new LocalMemoryError("STORAGE_ERROR", "mbh-memory local memory directory could not be prepared", {
|
|
219
|
+
cause: error
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
try {
|
|
223
|
+
return await withFileLock(this.config.file, async () => {
|
|
224
|
+
signal?.throwIfAborted();
|
|
225
|
+
let current = HEADER;
|
|
226
|
+
try {
|
|
227
|
+
current = await readFile(this.config.file, "utf8");
|
|
228
|
+
} catch (error) {
|
|
229
|
+
if (!isMissing(error))
|
|
230
|
+
throw error;
|
|
231
|
+
}
|
|
232
|
+
const nextEntry = entry(normalized, this.now());
|
|
233
|
+
if (current.includes(`<!-- mbh-memory:id=${nextEntry.id} -->`)) {
|
|
234
|
+
return { backend: "local", saved: true, duplicate: true };
|
|
235
|
+
}
|
|
236
|
+
const prefix = current.trimEnd() || HEADER.trimEnd();
|
|
237
|
+
const next = `${prefix}
|
|
238
|
+
${nextEntry.markdown}`;
|
|
239
|
+
if (Buffer.byteLength(next) > this.config.maxFileBytes) {
|
|
240
|
+
throw new LocalMemoryError("STORAGE_LIMIT", `mbh-memory local memory would exceed ${this.config.maxFileBytes} bytes`);
|
|
241
|
+
}
|
|
242
|
+
signal?.throwIfAborted();
|
|
243
|
+
await writeFileAtomic(this.config.file, next, { mode: 384, dirMode: 448 });
|
|
244
|
+
return { backend: "local", saved: true, duplicate: false };
|
|
245
|
+
}, { waitMs: 5e3 });
|
|
246
|
+
} catch (error) {
|
|
247
|
+
if (error instanceof LocalMemoryError || signal?.aborted)
|
|
248
|
+
throw error;
|
|
249
|
+
throw new LocalMemoryError("STORAGE_ERROR", "mbh-memory local memory could not be written", { cause: error });
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
|
|
136
254
|
// dist/src/tools.js
|
|
137
255
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
138
256
|
function render(value) {
|
|
@@ -141,7 +259,7 @@ function render(value) {
|
|
|
141
259
|
function createMemoryTools(memory) {
|
|
142
260
|
const getMemory = defineTool({
|
|
143
261
|
name: "get_memory",
|
|
144
|
-
description: "Read the user's long-term memory from
|
|
262
|
+
description: "Read the user's long-term memory from Mobook's configured memory storage. Use it only when prior preferences, facts, or experience are relevant to the current request.",
|
|
145
263
|
parameters: {},
|
|
146
264
|
output: {
|
|
147
265
|
schema: { type: "json" },
|
|
@@ -153,7 +271,7 @@ function createMemoryTools(memory) {
|
|
|
153
271
|
});
|
|
154
272
|
const setMemory = defineTool({
|
|
155
273
|
name: "set_memory",
|
|
156
|
-
description: "Write one durable memory through the configured Mobook memory
|
|
274
|
+
description: "Write one durable memory through the configured Mobook memory storage. Use this when the user explicitly asks you to remember something useful for future conversations. Never store passwords, access tokens, API keys, or other secrets.",
|
|
157
275
|
parameters: {
|
|
158
276
|
memory: {
|
|
159
277
|
type: "string",
|
|
@@ -186,19 +304,58 @@ var Config = z.object({
|
|
|
186
304
|
maxMemoryChars: z.natural().min(1).default(2e4),
|
|
187
305
|
maxResponseBytes: z.natural().min(1).default(128 * 1024)
|
|
188
306
|
});
|
|
307
|
+
function record(value) {
|
|
308
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
309
|
+
}
|
|
310
|
+
function identifier(value) {
|
|
311
|
+
if (typeof value === "number" && Number.isSafeInteger(value))
|
|
312
|
+
return String(value);
|
|
313
|
+
if (typeof value !== "string")
|
|
314
|
+
return void 0;
|
|
315
|
+
return value.trim() || void 0;
|
|
316
|
+
}
|
|
317
|
+
function localMemoryAccountScope(metadata) {
|
|
318
|
+
const account = record(record(metadata)?.account);
|
|
319
|
+
const userId = identifier(account?.userId);
|
|
320
|
+
if (!userId || userId === "0")
|
|
321
|
+
return "anonymous";
|
|
322
|
+
const identity = JSON.stringify([userId, identifier(account?.appId) ?? "", identifier(account?.domainId) ?? ""]);
|
|
323
|
+
return createHash2("sha256").update(identity).digest("hex").slice(0, 32);
|
|
324
|
+
}
|
|
325
|
+
function resolveLocalMemoryFile() {
|
|
326
|
+
let metadata;
|
|
327
|
+
try {
|
|
328
|
+
metadata = JSON.parse(readFileSync(dshHomePath("mobook.json"), "utf8"));
|
|
329
|
+
} catch {
|
|
330
|
+
metadata = void 0;
|
|
331
|
+
}
|
|
332
|
+
return dshHomePath("memory", "users", localMemoryAccountScope(metadata), "MEMORY.md");
|
|
333
|
+
}
|
|
334
|
+
function createMemoryOperations(config) {
|
|
335
|
+
if (!config.enabled)
|
|
336
|
+
return void 0;
|
|
337
|
+
if (config.baseUrl.trim()) {
|
|
338
|
+
return new MemoryApiClient({
|
|
339
|
+
baseUrl: config.baseUrl.trim(),
|
|
340
|
+
...config.authorizationToken.trim() ? { authorizationToken: config.authorizationToken.trim() } : {},
|
|
341
|
+
timeoutMs: config.timeoutMs,
|
|
342
|
+
maxMemoryChars: config.maxMemoryChars,
|
|
343
|
+
maxResponseBytes: config.maxResponseBytes
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
return new LocalMemoryStore({
|
|
347
|
+
file: resolveLocalMemoryFile(),
|
|
348
|
+
maxMemoryChars: config.maxMemoryChars,
|
|
349
|
+
maxFileBytes: config.maxResponseBytes
|
|
350
|
+
});
|
|
351
|
+
}
|
|
189
352
|
var MbhMemoryService = class extends Service {
|
|
190
353
|
config;
|
|
191
354
|
operations;
|
|
192
355
|
constructor(ctx, config) {
|
|
193
356
|
super(ctx, MBH_MEMORY_SERVICE_KEY);
|
|
194
357
|
this.config = config;
|
|
195
|
-
this.operations = config
|
|
196
|
-
baseUrl: config.baseUrl.trim(),
|
|
197
|
-
...config.authorizationToken.trim() ? { authorizationToken: config.authorizationToken.trim() } : {},
|
|
198
|
-
timeoutMs: config.timeoutMs,
|
|
199
|
-
maxMemoryChars: config.maxMemoryChars,
|
|
200
|
-
maxResponseBytes: config.maxResponseBytes
|
|
201
|
-
}) : void 0;
|
|
358
|
+
this.operations = createMemoryOperations(config);
|
|
202
359
|
}
|
|
203
360
|
isEnabled() {
|
|
204
361
|
return this.config.enabled;
|
|
@@ -206,14 +363,19 @@ var MbhMemoryService = class extends Service {
|
|
|
206
363
|
isConfigured() {
|
|
207
364
|
return this.operations !== void 0;
|
|
208
365
|
}
|
|
366
|
+
backend() {
|
|
367
|
+
if (!this.operations)
|
|
368
|
+
return "disabled";
|
|
369
|
+
return this.config.baseUrl.trim() ? "cloud" : "local";
|
|
370
|
+
}
|
|
209
371
|
getMemory(signal) {
|
|
210
372
|
if (!this.operations)
|
|
211
|
-
return Promise.reject(new Error("mbh-memory
|
|
373
|
+
return Promise.reject(new Error("mbh-memory storage is disabled"));
|
|
212
374
|
return this.operations.getMemory(signal);
|
|
213
375
|
}
|
|
214
376
|
setMemory(memory, signal) {
|
|
215
377
|
if (!this.operations)
|
|
216
|
-
return Promise.reject(new Error("mbh-memory
|
|
378
|
+
return Promise.reject(new Error("mbh-memory storage is disabled"));
|
|
217
379
|
return this.operations.setMemory(memory, signal);
|
|
218
380
|
}
|
|
219
381
|
};
|
|
@@ -223,12 +385,10 @@ function apply(ctx, config) {
|
|
|
223
385
|
const logger = ctx.logger(name);
|
|
224
386
|
if (!service.isEnabled())
|
|
225
387
|
return () => void 0;
|
|
226
|
-
if (!service.isConfigured())
|
|
227
|
-
logger.info("%s plugin loaded without an API baseUrl; memory tools are unavailable", name);
|
|
388
|
+
if (!service.isConfigured())
|
|
228
389
|
return () => void 0;
|
|
229
|
-
}
|
|
230
390
|
const unregister = createMemoryTools(service).map((tool) => ctx.tools.register(tool));
|
|
231
|
-
logger.info("%s plugin loaded with get_memory and set_memory tools", name);
|
|
391
|
+
logger.info("%s plugin loaded with get_memory and set_memory tools using %s storage", name, service.backend());
|
|
232
392
|
return () => {
|
|
233
393
|
for (const dispose of unregister.reverse())
|
|
234
394
|
dispose();
|
|
@@ -239,6 +399,9 @@ export {
|
|
|
239
399
|
Config,
|
|
240
400
|
MbhMemoryService,
|
|
241
401
|
apply,
|
|
402
|
+
createMemoryOperations,
|
|
242
403
|
inject,
|
|
243
|
-
|
|
404
|
+
localMemoryAccountScope,
|
|
405
|
+
name,
|
|
406
|
+
resolveLocalMemoryFile
|
|
244
407
|
};
|
package/src/index.d.ts
CHANGED
package/src/index.js
CHANGED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { MemoryApiValue, MemoryOperations } from './api-client.js';
|
|
2
|
+
export interface LocalMemoryStoreConfig {
|
|
3
|
+
file: string;
|
|
4
|
+
maxMemoryChars: number;
|
|
5
|
+
maxFileBytes: number;
|
|
6
|
+
now?: () => Date;
|
|
7
|
+
}
|
|
8
|
+
export declare class LocalMemoryError extends Error {
|
|
9
|
+
readonly code: 'INVALID_CONFIG' | 'INVALID_MEMORY' | 'STORAGE_LIMIT' | 'STORAGE_ERROR';
|
|
10
|
+
constructor(code: 'INVALID_CONFIG' | 'INVALID_MEMORY' | 'STORAGE_LIMIT' | 'STORAGE_ERROR', message: string, options?: ErrorOptions);
|
|
11
|
+
}
|
|
12
|
+
export declare class LocalMemoryStore implements MemoryOperations {
|
|
13
|
+
private readonly config;
|
|
14
|
+
private readonly now;
|
|
15
|
+
constructor(config: LocalMemoryStoreConfig);
|
|
16
|
+
getMemory(signal?: AbortSignal): Promise<MemoryApiValue>;
|
|
17
|
+
setMemory(memory: string, signal?: AbortSignal): Promise<MemoryApiValue>;
|
|
18
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { mkdir, readFile } from 'node:fs/promises';
|
|
3
|
+
import { dirname } from 'node:path';
|
|
4
|
+
import { withFileLock, writeFileAtomic } from '@deepseek-ai/dsh-atomic-write';
|
|
5
|
+
const HEADER = `# Long-term Memory
|
|
6
|
+
|
|
7
|
+
<!-- Managed by mbh-memory. This Markdown file is the canonical local memory source. -->
|
|
8
|
+
`;
|
|
9
|
+
export class LocalMemoryError extends Error {
|
|
10
|
+
code;
|
|
11
|
+
constructor(code, message, options) {
|
|
12
|
+
super(message, options);
|
|
13
|
+
this.code = code;
|
|
14
|
+
this.name = 'LocalMemoryError';
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function normalizeMemory(memory) {
|
|
18
|
+
return memory.replaceAll('\r\n', '\n').trim();
|
|
19
|
+
}
|
|
20
|
+
function entryId(memory) {
|
|
21
|
+
return createHash('sha256').update(memory).digest('hex');
|
|
22
|
+
}
|
|
23
|
+
function entry(memory, now) {
|
|
24
|
+
const id = entryId(memory);
|
|
25
|
+
return {
|
|
26
|
+
id,
|
|
27
|
+
markdown: `\n## ${now.toISOString()}\n\n<!-- mbh-memory:id=${id} -->\n\n${memory}\n`
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function isMissing(error) {
|
|
31
|
+
return error?.code === 'ENOENT';
|
|
32
|
+
}
|
|
33
|
+
export class LocalMemoryStore {
|
|
34
|
+
config;
|
|
35
|
+
now;
|
|
36
|
+
constructor(config) {
|
|
37
|
+
this.config = config;
|
|
38
|
+
if (!config.file.trim())
|
|
39
|
+
throw new LocalMemoryError('INVALID_CONFIG', 'mbh-memory local file is required');
|
|
40
|
+
if (!Number.isSafeInteger(config.maxMemoryChars) || config.maxMemoryChars <= 0) {
|
|
41
|
+
throw new LocalMemoryError('INVALID_CONFIG', 'mbh-memory maxMemoryChars must be a positive integer');
|
|
42
|
+
}
|
|
43
|
+
if (!Number.isSafeInteger(config.maxFileBytes) || config.maxFileBytes <= 0) {
|
|
44
|
+
throw new LocalMemoryError('INVALID_CONFIG', 'mbh-memory maxFileBytes must be a positive integer');
|
|
45
|
+
}
|
|
46
|
+
this.now = config.now ?? (() => new Date());
|
|
47
|
+
}
|
|
48
|
+
async getMemory(signal) {
|
|
49
|
+
signal?.throwIfAborted();
|
|
50
|
+
let markdown;
|
|
51
|
+
try {
|
|
52
|
+
markdown = await readFile(this.config.file, 'utf8');
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
if (isMissing(error))
|
|
56
|
+
return { backend: 'local', memory: '' };
|
|
57
|
+
throw new LocalMemoryError('STORAGE_ERROR', 'mbh-memory local memory could not be read', { cause: error });
|
|
58
|
+
}
|
|
59
|
+
signal?.throwIfAborted();
|
|
60
|
+
if (Buffer.byteLength(markdown) > this.config.maxFileBytes) {
|
|
61
|
+
throw new LocalMemoryError('STORAGE_LIMIT', `mbh-memory local memory exceeds ${this.config.maxFileBytes} bytes`);
|
|
62
|
+
}
|
|
63
|
+
return { backend: 'local', memory: markdown };
|
|
64
|
+
}
|
|
65
|
+
async setMemory(memory, signal) {
|
|
66
|
+
const normalized = normalizeMemory(memory);
|
|
67
|
+
if (!normalized)
|
|
68
|
+
throw new LocalMemoryError('INVALID_MEMORY', 'memory must not be empty');
|
|
69
|
+
if (normalized.length > this.config.maxMemoryChars) {
|
|
70
|
+
throw new LocalMemoryError('INVALID_MEMORY', `memory exceeds ${this.config.maxMemoryChars} characters`);
|
|
71
|
+
}
|
|
72
|
+
signal?.throwIfAborted();
|
|
73
|
+
await mkdir(dirname(this.config.file), { recursive: true, mode: 0o700 }).catch((error) => {
|
|
74
|
+
throw new LocalMemoryError('STORAGE_ERROR', 'mbh-memory local memory directory could not be prepared', {
|
|
75
|
+
cause: error
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
try {
|
|
79
|
+
return await withFileLock(this.config.file, async () => {
|
|
80
|
+
signal?.throwIfAborted();
|
|
81
|
+
let current = HEADER;
|
|
82
|
+
try {
|
|
83
|
+
current = await readFile(this.config.file, 'utf8');
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
if (!isMissing(error))
|
|
87
|
+
throw error;
|
|
88
|
+
}
|
|
89
|
+
const nextEntry = entry(normalized, this.now());
|
|
90
|
+
if (current.includes(`<!-- mbh-memory:id=${nextEntry.id} -->`)) {
|
|
91
|
+
return { backend: 'local', saved: true, duplicate: true };
|
|
92
|
+
}
|
|
93
|
+
const prefix = current.trimEnd() || HEADER.trimEnd();
|
|
94
|
+
const next = `${prefix}\n${nextEntry.markdown}`;
|
|
95
|
+
if (Buffer.byteLength(next) > this.config.maxFileBytes) {
|
|
96
|
+
throw new LocalMemoryError('STORAGE_LIMIT', `mbh-memory local memory would exceed ${this.config.maxFileBytes} bytes`);
|
|
97
|
+
}
|
|
98
|
+
signal?.throwIfAborted();
|
|
99
|
+
await writeFileAtomic(this.config.file, next, { mode: 0o600, dirMode: 0o700 });
|
|
100
|
+
return { backend: 'local', saved: true, duplicate: false };
|
|
101
|
+
}, { waitMs: 5_000 });
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
if (error instanceof LocalMemoryError || signal?.aborted)
|
|
105
|
+
throw error;
|
|
106
|
+
throw new LocalMemoryError('STORAGE_ERROR', 'mbh-memory local memory could not be written', { cause: error });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
package/src/tools.js
CHANGED
|
@@ -5,7 +5,7 @@ function render(value) {
|
|
|
5
5
|
export function createMemoryTools(memory) {
|
|
6
6
|
const getMemory = defineTool({
|
|
7
7
|
name: 'get_memory',
|
|
8
|
-
description: "Read the user's long-term memory from
|
|
8
|
+
description: "Read the user's long-term memory from Mobook's configured memory storage. Use it only when prior preferences, facts, or experience are relevant to the current request.",
|
|
9
9
|
parameters: {},
|
|
10
10
|
output: {
|
|
11
11
|
schema: { type: 'json' },
|
|
@@ -17,7 +17,7 @@ export function createMemoryTools(memory) {
|
|
|
17
17
|
});
|
|
18
18
|
const setMemory = defineTool({
|
|
19
19
|
name: 'set_memory',
|
|
20
|
-
description: 'Write one durable memory through the configured Mobook memory
|
|
20
|
+
description: 'Write one durable memory through the configured Mobook memory storage. Use this when the user explicitly asks you to remember something useful for future conversations. Never store passwords, access tokens, API keys, or other secrets.',
|
|
21
21
|
parameters: {
|
|
22
22
|
memory: {
|
|
23
23
|
type: 'string',
|