@unblocklabs/unblock-memory 0.1.2 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +91 -19
- package/dist/src/config.d.ts +16 -2
- package/dist/src/config.js +78 -15
- package/dist/src/contracts.d.ts +18 -0
- package/dist/src/manager.d.ts +16 -8
- package/dist/src/manager.js +175 -25
- package/dist/src/plugin.js +53 -4
- package/dist/src/runtime.d.ts +2 -1
- package/dist/src/runtime.js +30 -7
- package/dist/src/session-projector.d.ts +21 -0
- package/dist/src/session-projector.js +163 -0
- package/dist/src/session-sync.d.ts +43 -0
- package/dist/src/session-sync.js +302 -0
- package/dist/src/sources.d.ts +7 -1
- package/dist/src/sources.js +32 -2
- package/openclaw.plugin.json +48 -8
- package/package.json +2 -2
package/dist/src/manager.js
CHANGED
|
@@ -2,11 +2,24 @@ import { mkdir } from "node:fs/promises";
|
|
|
2
2
|
import { dirname } from "node:path";
|
|
3
3
|
import chokidar from "chokidar";
|
|
4
4
|
import { ensureMemoryAnalysisSchema, latestAnalysisRunId, markMemoryAnalysisStale, readAnalysisSummary, readCluster, readClusters, runAnalysisWorker, } from "./analysis.js";
|
|
5
|
+
import { readSessionManifest, sessionMetadataByPath, syncSessionProjections, } from "./session-sync.js";
|
|
5
6
|
import { parseSafeVirtualPath } from "./sources.js";
|
|
6
7
|
const DEFAULT_READ_LINES = 120;
|
|
7
8
|
const MAX_READ_CHARS = 12_000;
|
|
8
9
|
const WATCH_DEBOUNCE_MS = 250;
|
|
9
10
|
const qmdModule = import("@unblocklabs/qmd");
|
|
11
|
+
function completedEmbeddingCount(result) {
|
|
12
|
+
if (result.errors > 0) {
|
|
13
|
+
throw new Error(`QMD failed to embed ${result.errors} chunk${result.errors === 1 ? "" : "s"}`);
|
|
14
|
+
}
|
|
15
|
+
return result.chunksEmbedded;
|
|
16
|
+
}
|
|
17
|
+
async function ensureSemanticChunking(store) {
|
|
18
|
+
const configured = store.internal.db.prepare("SELECT value FROM store_config WHERE key = 'embedding_chunk_strategy'").get();
|
|
19
|
+
if (configured?.value === "semantic")
|
|
20
|
+
return;
|
|
21
|
+
completedEmbeddingCount(await store.embed({ chunkStrategy: "semantic" }));
|
|
22
|
+
}
|
|
10
23
|
export function enableSecureDelete(store) {
|
|
11
24
|
store.internal.db.exec("PRAGMA secure_delete = ON");
|
|
12
25
|
}
|
|
@@ -78,7 +91,7 @@ function lineSpan(result) {
|
|
|
78
91
|
const endLine = startLine + Math.max(0, result.bestChunk.split("\n").length - 1);
|
|
79
92
|
return { startLine, endLine };
|
|
80
93
|
}
|
|
81
|
-
function lexicalResult(hit) {
|
|
94
|
+
function lexicalResult(hit, corpus, session) {
|
|
82
95
|
const body = hit.body ?? hit.title;
|
|
83
96
|
const endLine = Math.max(1, body.split("\n").length);
|
|
84
97
|
return {
|
|
@@ -89,9 +102,36 @@ function lexicalResult(hit) {
|
|
|
89
102
|
textScore: hit.score,
|
|
90
103
|
snippet: body,
|
|
91
104
|
source: "memory",
|
|
105
|
+
corpus,
|
|
106
|
+
...(session ? { session } : {}),
|
|
92
107
|
citation: `${hit.displayPath}#L1-L${endLine}`,
|
|
93
108
|
};
|
|
94
109
|
}
|
|
110
|
+
function sessionAllowedPaths(metadataByPath, collection, filter) {
|
|
111
|
+
const startedFrom = filter.startedFrom === undefined ? undefined : Date.parse(filter.startedFrom);
|
|
112
|
+
const startedTo = filter.startedTo === undefined ? undefined : Date.parse(filter.startedTo);
|
|
113
|
+
if (startedFrom !== undefined && !Number.isFinite(startedFrom)) {
|
|
114
|
+
throw new Error("memory_search sessionFilter.startedFrom must be an ISO 8601 timestamp");
|
|
115
|
+
}
|
|
116
|
+
if (startedTo !== undefined && !Number.isFinite(startedTo)) {
|
|
117
|
+
throw new Error("memory_search sessionFilter.startedTo must be an ISO 8601 timestamp");
|
|
118
|
+
}
|
|
119
|
+
if (startedFrom !== undefined && startedTo !== undefined && startedFrom > startedTo) {
|
|
120
|
+
throw new Error("memory_search sessionFilter.startedFrom must not be after startedTo");
|
|
121
|
+
}
|
|
122
|
+
const provider = filter.provider?.trim().toLowerCase();
|
|
123
|
+
const accountId = filter.accountId?.trim();
|
|
124
|
+
const conversationId = filter.conversationId?.trim();
|
|
125
|
+
const paths = [...metadataByPath].flatMap(([path, metadata]) => (startedFrom === undefined || metadata.startedAt >= startedFrom) &&
|
|
126
|
+
(startedTo === undefined || metadata.startedAt <= startedTo) &&
|
|
127
|
+
(provider === undefined || metadata.provider?.trim().toLowerCase() === provider) &&
|
|
128
|
+
(filter.chatType === undefined || metadata.chatType === filter.chatType) &&
|
|
129
|
+
(accountId === undefined || metadata.accountId?.trim() === accountId) &&
|
|
130
|
+
(conversationId === undefined || metadata.conversationId?.trim() === conversationId)
|
|
131
|
+
? [path]
|
|
132
|
+
: []);
|
|
133
|
+
return { [collection]: paths };
|
|
134
|
+
}
|
|
95
135
|
export class QmdMemoryManager {
|
|
96
136
|
#dbPath;
|
|
97
137
|
#workspaceDir;
|
|
@@ -99,6 +139,7 @@ export class QmdMemoryManager {
|
|
|
99
139
|
#storeFactory;
|
|
100
140
|
#analysisExecutable;
|
|
101
141
|
#analysisRunner;
|
|
142
|
+
#sessions;
|
|
102
143
|
#store;
|
|
103
144
|
#cleanupRemovedDocuments;
|
|
104
145
|
#operationChain;
|
|
@@ -109,6 +150,7 @@ export class QmdMemoryManager {
|
|
|
109
150
|
#closed = false;
|
|
110
151
|
#files = 0;
|
|
111
152
|
#dirty = true;
|
|
153
|
+
#sessionMetadata = new Map();
|
|
112
154
|
constructor(params) {
|
|
113
155
|
this.#dbPath = params.dbPath;
|
|
114
156
|
this.#workspaceDir = params.workspaceDir;
|
|
@@ -116,14 +158,20 @@ export class QmdMemoryManager {
|
|
|
116
158
|
this.#storeFactory = params.storeFactory;
|
|
117
159
|
this.#analysisExecutable = params.analysisExecutable;
|
|
118
160
|
this.#analysisRunner = params.analysisRunner ?? runAnalysisWorker;
|
|
161
|
+
this.#sessions = params.sessions;
|
|
119
162
|
}
|
|
120
163
|
async start() {
|
|
164
|
+
if (this.#sessions) {
|
|
165
|
+
this.#sessionMetadata = sessionMetadataByPath(await readSessionManifest(this.#sessions.manifestPath));
|
|
166
|
+
}
|
|
121
167
|
this.#startWatcher();
|
|
122
168
|
await this.sync({ reason: "first-use" });
|
|
123
169
|
await this.#watchReady;
|
|
124
170
|
}
|
|
125
171
|
#startWatcher() {
|
|
126
|
-
const paths = [...new Set([...this.#sources.values()]
|
|
172
|
+
const paths = [...new Set([...this.#sources.values()]
|
|
173
|
+
.filter((source) => source.kind === "files")
|
|
174
|
+
.map((source) => source.watchPath))];
|
|
127
175
|
if (paths.length === 0 || this.#watcher)
|
|
128
176
|
return;
|
|
129
177
|
this.#watcher = chokidar.watch(paths, {
|
|
@@ -176,20 +224,46 @@ export class QmdMemoryManager {
|
|
|
176
224
|
const prunedDocuments = await pruneStaleCollections(store, new Set(this.#collectionNames()));
|
|
177
225
|
if (prunedDocuments > 0)
|
|
178
226
|
markMemoryAnalysisStale(store.internal.db);
|
|
227
|
+
try {
|
|
228
|
+
await ensureSemanticChunking(store);
|
|
229
|
+
}
|
|
230
|
+
catch (error) {
|
|
231
|
+
await store.close();
|
|
232
|
+
throw error;
|
|
233
|
+
}
|
|
179
234
|
this.#cleanupRemovedDocuments = (changedDocuments) => {
|
|
180
235
|
cleanupRemovedDocuments(store, changedDocuments);
|
|
181
236
|
};
|
|
182
237
|
this.#store = store;
|
|
183
238
|
return store;
|
|
184
239
|
}
|
|
185
|
-
#collectionNames() {
|
|
186
|
-
|
|
240
|
+
#collectionNames(corpora) {
|
|
241
|
+
if (corpora === undefined)
|
|
242
|
+
return [...this.#sources.keys()];
|
|
243
|
+
if (corpora.length === 0)
|
|
244
|
+
throw new Error("memory_search corpora must not be empty");
|
|
245
|
+
const selected = new Set(corpora);
|
|
246
|
+
if (selected.has("all")) {
|
|
247
|
+
if (selected.size > 1)
|
|
248
|
+
throw new Error('memory_search corpus "all" must be used alone');
|
|
249
|
+
return [...this.#sources.keys()];
|
|
250
|
+
}
|
|
251
|
+
const known = new Set([...this.#sources.values()].map((source) => source.corpus));
|
|
252
|
+
const unknown = [...selected].find((corpus) => !known.has(corpus));
|
|
253
|
+
if (unknown)
|
|
254
|
+
throw new Error(`memory_search unknown corpus: ${unknown}`);
|
|
255
|
+
return [...this.#sources.values()]
|
|
256
|
+
.filter((source) => selected.has(source.corpus))
|
|
257
|
+
.map((source) => source.collection);
|
|
187
258
|
}
|
|
188
259
|
sync(params) {
|
|
189
260
|
const run = async () => {
|
|
190
261
|
const store = await this.#getStore();
|
|
191
262
|
this.#dirty = true;
|
|
192
|
-
const
|
|
263
|
+
const collections = [...this.#sources.values()]
|
|
264
|
+
.filter((source) => source.kind === "files")
|
|
265
|
+
.map((source) => source.collection);
|
|
266
|
+
const update = await store.update({ collections });
|
|
193
267
|
this.#cleanupRemovedDocuments?.(update.updated + update.removed);
|
|
194
268
|
const analysisStore = store;
|
|
195
269
|
const invalidatesAnalysis = update.indexed + update.updated + update.removed > 0 ||
|
|
@@ -198,17 +272,62 @@ export class QmdMemoryManager {
|
|
|
198
272
|
if (invalidatesAnalysis && analysisStore.internal) {
|
|
199
273
|
markMemoryAnalysisStale(analysisStore.internal.db);
|
|
200
274
|
}
|
|
201
|
-
|
|
202
|
-
|
|
275
|
+
let chunksEmbedded = 0;
|
|
276
|
+
for (const collection of collections.length > 0 ? collections : [undefined]) {
|
|
277
|
+
const embed = await store.embed({
|
|
278
|
+
...(collection ? { collection } : {}),
|
|
279
|
+
force: params?.force,
|
|
280
|
+
chunkStrategy: "semantic",
|
|
281
|
+
});
|
|
282
|
+
chunksEmbedded += completedEmbeddingCount(embed);
|
|
283
|
+
}
|
|
284
|
+
if (!invalidatesAnalysis && chunksEmbedded > 0 && analysisStore.internal) {
|
|
203
285
|
markMemoryAnalysisStale(analysisStore.internal.db);
|
|
204
286
|
}
|
|
205
287
|
const status = await store.getStatus();
|
|
206
|
-
const
|
|
207
|
-
this.#files =
|
|
288
|
+
const indexedCollections = await store.listCollections();
|
|
289
|
+
this.#files = indexedCollections.reduce((total, collection) => total + collection.active_count, 0);
|
|
208
290
|
this.#dirty = status.needsEmbedding > 0;
|
|
209
291
|
};
|
|
210
292
|
return this.#enqueue(run);
|
|
211
293
|
}
|
|
294
|
+
syncSessions(force = false) {
|
|
295
|
+
return this.#enqueue(async () => {
|
|
296
|
+
const sessions = this.#sessions;
|
|
297
|
+
if (!sessions)
|
|
298
|
+
throw new Error('memory session sync requires a configured "sessions" corpus');
|
|
299
|
+
const store = await this.#getStore();
|
|
300
|
+
const synced = await syncSessionProjections({
|
|
301
|
+
...sessions,
|
|
302
|
+
force,
|
|
303
|
+
index: async () => {
|
|
304
|
+
const update = await store.update({ collections: [sessions.collection] });
|
|
305
|
+
this.#cleanupRemovedDocuments?.(update.updated + update.removed);
|
|
306
|
+
const analysisStore = store;
|
|
307
|
+
const invalidatesAnalysis = update.indexed + update.updated + update.removed > 0 ||
|
|
308
|
+
update.needsEmbedding > 0;
|
|
309
|
+
if (invalidatesAnalysis && analysisStore.internal) {
|
|
310
|
+
markMemoryAnalysisStale(analysisStore.internal.db);
|
|
311
|
+
}
|
|
312
|
+
const embed = await store.embed({
|
|
313
|
+
collection: sessions.collection,
|
|
314
|
+
chunkStrategy: "semantic",
|
|
315
|
+
});
|
|
316
|
+
const chunksEmbedded = completedEmbeddingCount(embed);
|
|
317
|
+
if (!invalidatesAnalysis && chunksEmbedded > 0 && analysisStore.internal) {
|
|
318
|
+
markMemoryAnalysisStale(analysisStore.internal.db);
|
|
319
|
+
}
|
|
320
|
+
return chunksEmbedded;
|
|
321
|
+
},
|
|
322
|
+
});
|
|
323
|
+
this.#sessionMetadata = sessionMetadataByPath(synced.manifest);
|
|
324
|
+
const status = await store.getStatus();
|
|
325
|
+
const collections = await store.listCollections();
|
|
326
|
+
this.#files = collections.reduce((total, collection) => total + collection.active_count, 0);
|
|
327
|
+
this.#dirty = status.needsEmbedding > 0;
|
|
328
|
+
return synced.result;
|
|
329
|
+
});
|
|
330
|
+
}
|
|
212
331
|
recluster(options, signal) {
|
|
213
332
|
return this.#enqueue(async () => {
|
|
214
333
|
if (!this.#analysisExecutable) {
|
|
@@ -257,34 +376,57 @@ export class QmdMemoryManager {
|
|
|
257
376
|
return [];
|
|
258
377
|
if (this.#sources.size === 0)
|
|
259
378
|
return [];
|
|
379
|
+
const collections = this.#collectionNames(opts?.corpora);
|
|
260
380
|
opts?.signal?.throwIfAborted();
|
|
261
381
|
await this.#operationChain;
|
|
382
|
+
const sessions = this.#sessions;
|
|
383
|
+
const allowedPaths = opts?.sessionFilter && sessions && collections.includes(sessions.collection)
|
|
384
|
+
? sessionAllowedPaths(this.#sessionMetadata, sessions.collection, opts.sessionFilter)
|
|
385
|
+
: undefined;
|
|
262
386
|
const store = await this.#getStore();
|
|
263
387
|
if (opts?.lexicalOnly) {
|
|
264
388
|
const hits = await store.searchLex(query, {
|
|
265
389
|
limit: opts.maxResults ?? 5,
|
|
266
|
-
collection:
|
|
390
|
+
collection: collections,
|
|
391
|
+
});
|
|
392
|
+
return hits.flatMap((hit) => {
|
|
393
|
+
const corpus = this.#sources.get(hit.collectionName)?.corpus;
|
|
394
|
+
const prefix = `qmd://${hit.collectionName}/`;
|
|
395
|
+
const session = corpus === "sessions" && hit.filepath.startsWith(prefix)
|
|
396
|
+
? this.#sessionMetadata.get(hit.filepath.slice(prefix.length))
|
|
397
|
+
: undefined;
|
|
398
|
+
return hit.score >= (opts.minScore ?? 0) && corpus ? [lexicalResult(hit, corpus, session)] : [];
|
|
267
399
|
});
|
|
268
|
-
return hits
|
|
269
|
-
.filter((hit) => hit.score >= (opts.minScore ?? 0))
|
|
270
|
-
.map(lexicalResult);
|
|
271
400
|
}
|
|
272
401
|
const hits = await store.vsearch(query, {
|
|
273
|
-
collection:
|
|
402
|
+
collection: collections,
|
|
274
403
|
limit: opts?.maxResults ?? 5,
|
|
275
404
|
minScore: opts?.minScore ?? 0.3,
|
|
405
|
+
allowedPaths,
|
|
276
406
|
});
|
|
277
|
-
return hits.
|
|
407
|
+
return hits.flatMap((hit) => {
|
|
408
|
+
const collection = /^qmd:\/\/([^/]+)\//.exec(hit.file)?.[1];
|
|
409
|
+
const corpus = collection ? this.#sources.get(collection)?.corpus : undefined;
|
|
410
|
+
if (!corpus)
|
|
411
|
+
return [];
|
|
278
412
|
const span = lineSpan(hit);
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
413
|
+
const relativePath = collection && hit.file.startsWith(`qmd://${collection}/`)
|
|
414
|
+
? hit.file.slice(`qmd://${collection}/`.length)
|
|
415
|
+
: undefined;
|
|
416
|
+
const session = corpus === "sessions" && relativePath
|
|
417
|
+
? this.#sessionMetadata.get(relativePath)
|
|
418
|
+
: undefined;
|
|
419
|
+
return [{
|
|
420
|
+
path: hit.file,
|
|
421
|
+
...span,
|
|
422
|
+
score: hit.score,
|
|
423
|
+
vectorScore: hit.score,
|
|
424
|
+
snippet: hit.bestChunk,
|
|
425
|
+
source: "memory",
|
|
426
|
+
corpus,
|
|
427
|
+
...(session ? { session } : {}),
|
|
428
|
+
citation: `${hit.displayPath}#L${span.startLine}-L${span.endLine}`,
|
|
429
|
+
}];
|
|
288
430
|
});
|
|
289
431
|
}
|
|
290
432
|
async readFile(params) {
|
|
@@ -308,6 +450,12 @@ export class QmdMemoryManager {
|
|
|
308
450
|
});
|
|
309
451
|
}
|
|
310
452
|
status() {
|
|
453
|
+
const corpora = new Map();
|
|
454
|
+
for (const source of this.#sources.values()) {
|
|
455
|
+
const sources = corpora.get(source.corpus) ?? [];
|
|
456
|
+
sources.push(source);
|
|
457
|
+
corpora.set(source.corpus, sources);
|
|
458
|
+
}
|
|
311
459
|
return {
|
|
312
460
|
backend: "builtin",
|
|
313
461
|
provider: "unblock-memory",
|
|
@@ -318,7 +466,9 @@ export class QmdMemoryManager {
|
|
|
318
466
|
sources: ["memory"],
|
|
319
467
|
vector: { enabled: true, available: !this.#dirty },
|
|
320
468
|
custom: {
|
|
321
|
-
|
|
469
|
+
corpora: [...corpora].map(([name, sources]) => sources[0]?.kind === "sessions"
|
|
470
|
+
? { name, kind: "sessions", chatTypes: sources[0].chatTypes }
|
|
471
|
+
: { name, kind: "files", paths: sources.map((source) => source.configuredPath) }),
|
|
322
472
|
...(this.#watchError ? { watchError: this.#watchError } : {}),
|
|
323
473
|
},
|
|
324
474
|
};
|
package/dist/src/plugin.js
CHANGED
|
@@ -11,6 +11,19 @@ function getContext(ctx) {
|
|
|
11
11
|
}
|
|
12
12
|
const searchParameters = Type.Object({
|
|
13
13
|
query: Type.String({ pattern: "\\S" }),
|
|
14
|
+
corpora: Type.Optional(Type.Array(Type.String({ pattern: "\\S" }), { minItems: 1 })),
|
|
15
|
+
sessionFilter: Type.Optional(Type.Object({
|
|
16
|
+
startedFrom: Type.Optional(Type.String({ pattern: "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d{1,9})?(?:Z|[+-]\\d{2}:\\d{2})$" })),
|
|
17
|
+
startedTo: Type.Optional(Type.String({ pattern: "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d{1,9})?(?:Z|[+-]\\d{2}:\\d{2})$" })),
|
|
18
|
+
provider: Type.Optional(Type.String({ pattern: "\\S" })),
|
|
19
|
+
chatType: Type.Optional(Type.Union([
|
|
20
|
+
Type.Literal("channel"),
|
|
21
|
+
Type.Literal("group"),
|
|
22
|
+
Type.Literal("direct"),
|
|
23
|
+
])),
|
|
24
|
+
accountId: Type.Optional(Type.String({ pattern: "\\S" })),
|
|
25
|
+
conversationId: Type.Optional(Type.String({ pattern: "\\S" })),
|
|
26
|
+
}, { additionalProperties: false })),
|
|
14
27
|
maxResults: Type.Optional(Type.Integer({ minimum: 1, maximum: 20 })),
|
|
15
28
|
minScore: Type.Optional(Type.Number({ minimum: 0, maximum: 1 })),
|
|
16
29
|
}, { additionalProperties: false });
|
|
@@ -19,6 +32,9 @@ const getParameters = Type.Object({
|
|
|
19
32
|
from: Type.Optional(Type.Integer({ minimum: 1 })),
|
|
20
33
|
lines: Type.Optional(Type.Integer({ minimum: 1, maximum: 1000 })),
|
|
21
34
|
}, { additionalProperties: false });
|
|
35
|
+
const syncSessionsParameters = Type.Object({
|
|
36
|
+
force: Type.Optional(Type.Boolean()),
|
|
37
|
+
}, { additionalProperties: false });
|
|
22
38
|
function createSearchTool(runtime, ctx) {
|
|
23
39
|
const active = getContext(ctx);
|
|
24
40
|
if (!active)
|
|
@@ -26,15 +42,21 @@ function createSearchTool(runtime, ctx) {
|
|
|
26
42
|
return {
|
|
27
43
|
name: "memory_search",
|
|
28
44
|
label: "Memory Search",
|
|
29
|
-
description: "Search
|
|
45
|
+
description: "Search configured Markdown corpora with semantic vector retrieval. Omit corpora to search all of them.",
|
|
30
46
|
parameters: searchParameters,
|
|
31
47
|
async execute(_toolCallId, params, signal) {
|
|
32
|
-
const { query: untrimmedQuery, maxResults, minScore } = Value.Parse(searchParameters, params);
|
|
48
|
+
const { query: untrimmedQuery, corpora, sessionFilter, maxResults, minScore } = Value.Parse(searchParameters, params);
|
|
33
49
|
const query = untrimmedQuery.trim();
|
|
34
50
|
const { manager, error } = await runtime.getMemorySearchManager(active);
|
|
35
51
|
if (!manager)
|
|
36
52
|
return jsonResult({ results: [], error: error ?? "memory unavailable" });
|
|
37
|
-
const results = await manager.search(query, {
|
|
53
|
+
const results = await manager.search(query, {
|
|
54
|
+
corpora: corpora?.map((corpus) => corpus.trim()),
|
|
55
|
+
sessionFilter,
|
|
56
|
+
maxResults,
|
|
57
|
+
minScore,
|
|
58
|
+
signal,
|
|
59
|
+
});
|
|
38
60
|
return jsonResult({ results, provider: "unblock-memory" });
|
|
39
61
|
},
|
|
40
62
|
};
|
|
@@ -62,6 +84,32 @@ function createGetTool(runtime, ctx) {
|
|
|
62
84
|
},
|
|
63
85
|
};
|
|
64
86
|
}
|
|
87
|
+
function createSyncSessionsTool(runtime, ctx) {
|
|
88
|
+
const active = getContext(ctx);
|
|
89
|
+
if (!active)
|
|
90
|
+
return null;
|
|
91
|
+
return {
|
|
92
|
+
name: "memory_sync_sessions",
|
|
93
|
+
label: "Sync Memory Sessions",
|
|
94
|
+
description: "Project and index this agent's configured OpenClaw session transcripts.",
|
|
95
|
+
parameters: syncSessionsParameters,
|
|
96
|
+
async execute(_toolCallId, params) {
|
|
97
|
+
const { force } = Value.Parse(syncSessionsParameters, params);
|
|
98
|
+
const { manager, error } = await runtime.getMemorySearchManager(active);
|
|
99
|
+
if (!manager)
|
|
100
|
+
return jsonResult({ status: "unavailable", error: error ?? "memory unavailable" });
|
|
101
|
+
try {
|
|
102
|
+
return jsonResult(await manager.syncSessions(force));
|
|
103
|
+
}
|
|
104
|
+
catch (syncError) {
|
|
105
|
+
return jsonResult({
|
|
106
|
+
status: "unavailable",
|
|
107
|
+
error: syncError instanceof Error ? syncError.message : String(syncError),
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
}
|
|
65
113
|
const reclusterParameters = Type.Object({
|
|
66
114
|
space: Type.Optional(Type.Object({
|
|
67
115
|
method: Type.Optional(Type.Union([Type.Literal("umap"), Type.Literal("none")])),
|
|
@@ -219,7 +267,7 @@ export function resolveFlushPlan(params = {}) {
|
|
|
219
267
|
}
|
|
220
268
|
export function registerUnblockMemory(api) {
|
|
221
269
|
const config = resolveConfig(api.pluginConfig);
|
|
222
|
-
const runtime = new QmdMemoryRuntime(config.
|
|
270
|
+
const runtime = new QmdMemoryRuntime(config.corpora, config.analysis.executable);
|
|
223
271
|
const capability = {
|
|
224
272
|
deterministicRecallToolName: "memory_search",
|
|
225
273
|
supportsPrivateTranscriptRecall: false,
|
|
@@ -232,6 +280,7 @@ export function registerUnblockMemory(api) {
|
|
|
232
280
|
api.registerMemoryCapability(capability);
|
|
233
281
|
api.registerTool((ctx) => createSearchTool(runtime, ctx), { names: ["memory_search"] });
|
|
234
282
|
api.registerTool((ctx) => createGetTool(runtime, ctx), { names: ["memory_get"] });
|
|
283
|
+
api.registerTool((ctx) => createSyncSessionsTool(runtime, ctx), { names: ["memory_sync_sessions"] });
|
|
235
284
|
api.registerTool((ctx) => createReclusterTool(runtime, ctx), { names: ["memory_recluster"] });
|
|
236
285
|
api.registerTool((ctx) => createListClustersTool(runtime, ctx), { names: ["memory_list_clusters"] });
|
|
237
286
|
api.registerTool((ctx) => createFetchClusterTool(runtime, ctx), { names: ["memory_fetch_cluster"] });
|
package/dist/src/runtime.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import type { OpenClawConfig } from "openclaw/plugin-sdk/plugin-entry";
|
|
2
|
+
import type { CorpusConfig } from "./config.js";
|
|
2
3
|
import type { MemoryPluginRuntimeContract } from "./contracts.js";
|
|
3
4
|
import { QmdMemoryManager } from "./manager.js";
|
|
4
5
|
export declare class QmdMemoryRuntime implements MemoryPluginRuntimeContract {
|
|
5
6
|
#private;
|
|
6
|
-
constructor(
|
|
7
|
+
constructor(corpora: readonly CorpusConfig[], analysisExecutable?: string);
|
|
7
8
|
getMemorySearchManager(params: {
|
|
8
9
|
cfg: OpenClawConfig;
|
|
9
10
|
agentId: string;
|
package/dist/src/runtime.js
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
|
-
import { resolveAgentWorkspaceDir, resolveStateDir, } from "openclaw/plugin-sdk/memory-core-host-engine-foundation";
|
|
2
|
+
import { resolveAgentDir, resolveAgentWorkspaceDir, resolveStateDir, } from "openclaw/plugin-sdk/memory-core-host-engine-foundation";
|
|
3
|
+
import { resolveAgentIdentity } from "openclaw/plugin-sdk/agent-runtime";
|
|
3
4
|
import { QmdMemoryManager } from "./manager.js";
|
|
4
|
-
import {
|
|
5
|
+
import { resolveTimezone } from "./session-projector.js";
|
|
6
|
+
import { resolveSessionSource, resolveSources } from "./sources.js";
|
|
5
7
|
export class QmdMemoryRuntime {
|
|
6
|
-
#
|
|
8
|
+
#corpora;
|
|
7
9
|
#analysisExecutable;
|
|
8
10
|
#managers = new Map();
|
|
9
|
-
constructor(
|
|
10
|
-
this.#
|
|
11
|
+
constructor(corpora, analysisExecutable) {
|
|
12
|
+
this.#corpora = corpora;
|
|
11
13
|
this.#analysisExecutable = analysisExecutable;
|
|
12
14
|
}
|
|
13
15
|
async getMemorySearchManager(params) {
|
|
@@ -39,11 +41,32 @@ export class QmdMemoryRuntime {
|
|
|
39
41
|
}
|
|
40
42
|
async #createManager(cfg, agentId) {
|
|
41
43
|
const workspaceDir = resolveAgentWorkspaceDir(cfg, agentId);
|
|
44
|
+
const stateDir = join(resolveStateDir(), "agents", agentId, "unblock-memory");
|
|
45
|
+
const fileCorpora = this.#corpora.filter((corpus) => corpus.kind === "files");
|
|
46
|
+
const sessionCorpus = this.#corpora.find((corpus) => corpus.kind === "sessions");
|
|
47
|
+
const sources = resolveSources(workspaceDir, fileCorpora);
|
|
48
|
+
const sessionSource = sessionCorpus
|
|
49
|
+
? resolveSessionSource(join(stateDir, "sessions"), sessionCorpus.chatTypes)
|
|
50
|
+
: undefined;
|
|
51
|
+
if (sessionSource)
|
|
52
|
+
sources.push(sessionSource);
|
|
42
53
|
const manager = new QmdMemoryManager({
|
|
43
54
|
workspaceDir,
|
|
44
|
-
dbPath: join(
|
|
45
|
-
sources
|
|
55
|
+
dbPath: join(stateDir, "index.sqlite"),
|
|
56
|
+
sources,
|
|
46
57
|
analysisExecutable: this.#analysisExecutable,
|
|
58
|
+
...(sessionCorpus && sessionSource ? {
|
|
59
|
+
sessions: {
|
|
60
|
+
agentId,
|
|
61
|
+
agentName: resolveAgentIdentity(cfg, agentId)?.name?.trim() || agentId,
|
|
62
|
+
chatTypes: sessionCorpus.chatTypes,
|
|
63
|
+
collection: sessionSource.collection,
|
|
64
|
+
databasePath: join(resolveAgentDir(cfg, agentId), "openclaw-agent.sqlite"),
|
|
65
|
+
manifestPath: join(stateDir, "sessions-manifest.json"),
|
|
66
|
+
outputDir: sessionSource.root,
|
|
67
|
+
timezone: resolveTimezone(cfg.agents?.defaults?.userTimezone?.trim()),
|
|
68
|
+
},
|
|
69
|
+
} : {}),
|
|
47
70
|
});
|
|
48
71
|
await manager.start();
|
|
49
72
|
return manager;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ChatType } from "./config.js";
|
|
2
|
+
export type SessionMetadata = {
|
|
3
|
+
sessionId: string;
|
|
4
|
+
provider?: string;
|
|
5
|
+
chatType: ChatType;
|
|
6
|
+
accountId?: string;
|
|
7
|
+
conversationId?: string;
|
|
8
|
+
startedAt: number;
|
|
9
|
+
};
|
|
10
|
+
export type SessionProjectionInput = SessionMetadata & {
|
|
11
|
+
label?: string;
|
|
12
|
+
agentName: string;
|
|
13
|
+
timezone: string;
|
|
14
|
+
events: readonly {
|
|
15
|
+
eventJson: string;
|
|
16
|
+
createdAt: number;
|
|
17
|
+
}[];
|
|
18
|
+
};
|
|
19
|
+
export declare function projectSession(input: SessionProjectionInput): string | undefined;
|
|
20
|
+
export declare function sessionDocumentPath(metadata: SessionMetadata): string;
|
|
21
|
+
export declare function resolveTimezone(configured?: string): string;
|