@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.
@@ -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()].map((source) => source.watchPath))];
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
- return [...this.#sources.keys()];
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 update = await store.update();
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
- const embed = await store.embed({ force: params?.force, chunkStrategy: "semantic" });
202
- if (!invalidatesAnalysis && embed.chunksEmbedded > 0 && analysisStore.internal) {
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 collections = await store.listCollections();
207
- this.#files = collections.reduce((total, collection) => total + collection.active_count, 0);
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: this.#collectionNames(),
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: this.#collectionNames(),
402
+ collection: collections,
274
403
  limit: opts?.maxResults ?? 5,
275
404
  minScore: opts?.minScore ?? 0.3,
405
+ allowedPaths,
276
406
  });
277
- return hits.map((hit) => {
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
- return {
280
- path: hit.file,
281
- ...span,
282
- score: hit.score,
283
- vectorScore: hit.score,
284
- snippet: hit.bestChunk,
285
- source: "memory",
286
- citation: `${hit.displayPath}#L${span.startLine}-L${span.endLine}`,
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
- paths: [...this.#sources.values()].map((source) => source.configuredPath),
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
  };
@@ -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 canonical Markdown memory with semantic vector retrieval.",
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, { maxResults, minScore, signal });
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.paths, config.analysis.executable);
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"] });
@@ -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(paths: readonly string[], analysisExecutable?: string);
7
+ constructor(corpora: readonly CorpusConfig[], analysisExecutable?: string);
7
8
  getMemorySearchManager(params: {
8
9
  cfg: OpenClawConfig;
9
10
  agentId: string;
@@ -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 { resolveSource } from "./sources.js";
5
+ import { resolveTimezone } from "./session-projector.js";
6
+ import { resolveSessionSource, resolveSources } from "./sources.js";
5
7
  export class QmdMemoryRuntime {
6
- #paths;
8
+ #corpora;
7
9
  #analysisExecutable;
8
10
  #managers = new Map();
9
- constructor(paths, analysisExecutable) {
10
- this.#paths = paths;
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(resolveStateDir(), "agents", agentId, "unblock-memory", "index.sqlite"),
45
- sources: this.#paths.map((source) => resolveSource(workspaceDir, source)),
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;