dsh-tdai-memory 0.4.1 → 0.4.2

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.
Files changed (70) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +10 -0
  3. package/README.zh.md +9 -0
  4. package/THIRD-PARTY-NOTICES.md +20 -20
  5. package/client.js +119 -33
  6. package/cordis.patch.yml +8 -8
  7. package/index.js +11 -11
  8. package/locale/en.json +6 -0
  9. package/locale/zh.json +6 -0
  10. package/package.json +6 -3
  11. package/recall-inject.js +88 -86
  12. package/recall-runtime.js +42 -0
  13. package/vendor/tdai/adapters/index.js +15 -15
  14. package/vendor/tdai/adapters/openclaw/host-adapter.js +83 -83
  15. package/vendor/tdai/adapters/openclaw/index.js +5 -5
  16. package/vendor/tdai/adapters/openclaw/llm-runner.js +69 -69
  17. package/vendor/tdai/adapters/standalone/host-adapter.js +58 -58
  18. package/vendor/tdai/adapters/standalone/index.js +5 -5
  19. package/vendor/tdai/core/conversation/l0-recorder.js +420 -420
  20. package/vendor/tdai/core/hooks/auto-capture.js +236 -236
  21. package/vendor/tdai/core/hooks/auto-recall.js +640 -640
  22. package/vendor/tdai/core/index.js +8 -8
  23. package/vendor/tdai/core/persona/persona-generator.js +184 -184
  24. package/vendor/tdai/core/persona/persona-trigger.js +94 -94
  25. package/vendor/tdai/core/profile/profile-sync.js +200 -200
  26. package/vendor/tdai/core/prompts/l1-dedup.js +72 -72
  27. package/vendor/tdai/core/prompts/l1-extraction.js +34 -34
  28. package/vendor/tdai/core/prompts/persona-generation.js +35 -35
  29. package/vendor/tdai/core/prompts/scene-extraction.js +45 -45
  30. package/vendor/tdai/core/record/l1-dedup.js +318 -318
  31. package/vendor/tdai/core/record/l1-extractor.js +379 -379
  32. package/vendor/tdai/core/record/l1-reader.js +178 -178
  33. package/vendor/tdai/core/record/l1-writer.js +141 -141
  34. package/vendor/tdai/core/report/reporter.js +86 -86
  35. package/vendor/tdai/core/scene/filename-normalizer.js +161 -161
  36. package/vendor/tdai/core/scene/scene-extractor.js +410 -410
  37. package/vendor/tdai/core/scene/scene-format.js +53 -53
  38. package/vendor/tdai/core/scene/scene-index.js +82 -82
  39. package/vendor/tdai/core/scene/scene-navigation.js +57 -57
  40. package/vendor/tdai/core/seed/input.js +404 -404
  41. package/vendor/tdai/core/seed/types.js +9 -9
  42. package/vendor/tdai/core/store/bm25-client.js +117 -117
  43. package/vendor/tdai/core/store/bm25-local.js +68 -68
  44. package/vendor/tdai/core/store/embedding.js +632 -632
  45. package/vendor/tdai/core/store/factory.js +95 -95
  46. package/vendor/tdai/core/store/search-utils.js +54 -54
  47. package/vendor/tdai/core/store/sqlite.js +1632 -1632
  48. package/vendor/tdai/core/store/tcvdb-client.js +221 -221
  49. package/vendor/tdai/core/store/tcvdb.js +1047 -1047
  50. package/vendor/tdai/core/store/types.js +17 -17
  51. package/vendor/tdai/core/tools/conversation-search.js +199 -199
  52. package/vendor/tdai/core/tools/memory-search.js +207 -207
  53. package/vendor/tdai/core/types.js +12 -12
  54. package/vendor/tdai/utils/backup.js +189 -189
  55. package/vendor/tdai/utils/checkpoint.js +360 -360
  56. package/vendor/tdai/utils/clean-context-runner.js +472 -472
  57. package/vendor/tdai/utils/ensure-hook-policy.js +213 -213
  58. package/vendor/tdai/utils/env.js +13 -13
  59. package/vendor/tdai/utils/managed-timer.js +127 -127
  60. package/vendor/tdai/utils/manifest.js +91 -91
  61. package/vendor/tdai/utils/memory-cleaner.js +300 -300
  62. package/vendor/tdai/utils/no-think-fetch.js +109 -109
  63. package/vendor/tdai/utils/openclaw-state-dir.js +26 -26
  64. package/vendor/tdai/utils/pipeline-factory.js +517 -517
  65. package/vendor/tdai/utils/pipeline-manager.js +892 -892
  66. package/vendor/tdai/utils/sanitize.js +360 -360
  67. package/vendor/tdai/utils/serial-queue.js +103 -103
  68. package/vendor/tdai/utils/session-filter.js +88 -88
  69. package/vendor/tdai/utils/text-utils.js +29 -29
  70. package/vendor/tdai/utils/time.js +252 -252
@@ -1,517 +1,517 @@
1
- /**
2
- * Pipeline factory: shared infrastructure for creating and wiring
3
- * MemoryPipelineManager instances with VectorStore, EmbeddingService,
4
- * L1 runner, L2 runner, L3 runner, and persister.
5
- *
6
- * Used by both:
7
- * - `index.ts` (live plugin runtime)
8
- * - `seed-runtime.ts` (standalone seed CLI command)
9
- *
10
- * This avoids duplicating VectorStore init, L1/L2/L3 extraction logic,
11
- * persister wiring, and destroy sequences across multiple callers.
12
- */
13
- import fs from "node:fs";
14
- import path from "node:path";
15
- import { MemoryPipelineManager } from "./pipeline-manager.js";
16
- import { SessionFilter } from "./session-filter.js";
17
- import { extractL1Memories } from "../core/record/l1-extractor.js";
18
- import { readConversationMessagesGroupedBySessionId } from "../core/conversation/l0-recorder.js";
19
- import { CheckpointManager } from "./checkpoint.js";
20
- import { createStoreBundle } from "../core/store/factory.js";
21
- import { readManifest, writeManifest, buildStoreInfo, diffStoreBinding, } from "./manifest.js";
22
- import { SceneExtractor } from "../core/scene/scene-extractor.js";
23
- import { PersonaTrigger } from "../core/persona/persona-trigger.js";
24
- import { PersonaGenerator } from "../core/persona/persona-generator.js";
25
- import { pullProfilesToLocal, syncLocalProfilesToStore } from "../core/profile/profile-sync.js";
26
- const TAG = "[memory-tdai] [pipeline-factory]";
27
- function supportsProfileSyncWrite(store) {
28
- return !!(store?.syncProfiles || store?.deleteProfiles);
29
- }
30
- // ============================
31
- // Data directory init
32
- // ============================
33
- /**
34
- * Ensure all required data subdirectories exist under `pluginDataDir`.
35
- * Safe to call multiple times (mkdirSync with `recursive: true`).
36
- */
37
- export function initDataDirectories(dataDir) {
38
- const dirs = ["conversations", "records", "scene_blocks", ".metadata", ".backup"];
39
- for (const sub of dirs) {
40
- fs.mkdirSync(path.join(dataDir, sub), { recursive: true });
41
- }
42
- }
43
- /**
44
- * Cached store init promises — keyed by `pluginDataDir` so that different
45
- * data directories (e.g. live runtime vs. seed output) each get their own
46
- * store instance, while concurrent callers for the *same* directory share
47
- * one initialization.
48
- */
49
- const _storeInitCache = new Map();
50
- /**
51
- * Initialize store backend and (optionally) EmbeddingService.
52
- *
53
- * **Once-async semantics per dataDir**: the first call for a given
54
- * `pluginDataDir` creates the store and caches the result; subsequent
55
- * calls with the same dir return the cached Promise immediately.
56
- * Call `resetStores()` during shutdown to clear the cache.
57
- *
58
- * Supports both SQLite (sync init) and TCVDB (async init) backends.
59
- */
60
- export function initStores(cfg, pluginDataDir, logger) {
61
- const key = pluginDataDir;
62
- if (!_storeInitCache.has(key)) {
63
- _storeInitCache.set(key, _doInitStores(cfg, pluginDataDir, logger));
64
- }
65
- return _storeInitCache.get(key);
66
- }
67
- /**
68
- * Reset the cached store singleton(s).
69
- *
70
- * Call this during `gateway_stop` (after closing the actual store/embedding
71
- * resources) so that a subsequent `register()` on hot-restart can
72
- * re-initialize fresh instances.
73
- *
74
- * @param pluginDataDir If provided, only clear the cache for that dir.
75
- * If omitted, clear all cached stores.
76
- */
77
- export function resetStores(pluginDataDir) {
78
- if (pluginDataDir) {
79
- _storeInitCache.delete(pluginDataDir);
80
- }
81
- else {
82
- _storeInitCache.clear();
83
- }
84
- }
85
- /**
86
- * Internal: actual store initialization logic (called once by the cache).
87
- */
88
- async function _doInitStores(cfg, pluginDataDir, logger) {
89
- let vectorStore;
90
- let embeddingService;
91
- let needsReindex = false;
92
- let reindexReason;
93
- try {
94
- const bundle = createStoreBundle(cfg, {
95
- dataDir: pluginDataDir,
96
- logger,
97
- });
98
- vectorStore = bundle.store;
99
- embeddingService = bundle.embedding ?? undefined;
100
- const providerInfo = embeddingService?.getProviderInfo();
101
- const initResult = await vectorStore.init(providerInfo);
102
- if (vectorStore.isDegraded()) {
103
- logger.warn(`${TAG} Store is in degraded mode, falling back to keyword dedup`);
104
- vectorStore = undefined;
105
- embeddingService = undefined;
106
- }
107
- else {
108
- logger.debug?.(`${TAG} Store initialized: backend=${cfg.storeBackend}, provider=${cfg.embedding.provider}`);
109
- needsReindex = initResult.needsReindex;
110
- reindexReason = initResult.reason;
111
- // ── Manifest: first-write + config-drift detection ──
112
- try {
113
- const currentStoreInfo = buildStoreInfo(bundle.storeSnapshot);
114
- const existing = readManifest(pluginDataDir);
115
- if (!existing) {
116
- // First init — write manifest
117
- const manifest = {
118
- version: 1,
119
- createdAt: new Date().toISOString(),
120
- store: currentStoreInfo,
121
- seed: null,
122
- };
123
- writeManifest(pluginDataDir, manifest);
124
- logger.debug?.(`${TAG} Manifest created: ${JSON.stringify(currentStoreInfo)}`);
125
- }
126
- else {
127
- // Compare persisted store binding against current config
128
- const diffs = diffStoreBinding(existing.store, currentStoreInfo);
129
- if (diffs.length > 0) {
130
- logger.debug?.(`${TAG} Store config differs from initial binding recorded in manifest ` +
131
- `(${diffs.join("; ")}). ` +
132
- `This is expected if the storage backend was switched intentionally.`);
133
- }
134
- }
135
- }
136
- catch (err) {
137
- logger.warn(`${TAG} Failed to read/write manifest (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
138
- }
139
- }
140
- }
141
- catch (err) {
142
- logger.warn(`${TAG} Store init failed; vector/FTS recall and dedup conflict detection will be unavailable: ${err instanceof Error ? err.message : String(err)}`);
143
- vectorStore = undefined;
144
- embeddingService = undefined;
145
- }
146
- return { vectorStore, embeddingService, needsReindex, reindexReason };
147
- }
148
- // ============================
149
- // L1 Runner factory
150
- // ============================
151
- /**
152
- * Create the standard L1 runner function.
153
- *
154
- * Reads L0 messages (from VectorStore DB or JSONL fallback), groups by sessionId,
155
- * runs extractL1Memories for each group, and updates the checkpoint cursor.
156
- */
157
- export function createL1Runner(opts) {
158
- const { pluginDataDir, cfg, openclawConfig, vectorStore, embeddingService, logger, getInstanceId, llmRunner } = opts;
159
- const config = openclawConfig;
160
- return async ({ sessionKey }) => {
161
- if (!config && !llmRunner) {
162
- logger.debug?.(`${TAG} [l1] No OpenClaw config and no LLM runner, skipping L1 extraction`);
163
- return { processedCount: 0 };
164
- }
165
- const checkpoint = new CheckpointManager(pluginDataDir, logger);
166
- const cp = await checkpoint.read();
167
- const runnerState = checkpoint.getRunnerState(cp, sessionKey);
168
- logger.info(`${TAG} [l1] Session ${sessionKey}: l1_cursor=${runnerState.last_l1_cursor || "(start)"}`);
169
- try {
170
- let groups;
171
- let maxRecordedAtMs = 0;
172
- if (vectorStore && !vectorStore.isDegraded()) {
173
- const l1Cursor = runnerState.last_l1_cursor > 0
174
- ? runnerState.last_l1_cursor
175
- : undefined;
176
- const dbGroups = await vectorStore.queryL0GroupedBySessionId(sessionKey, l1Cursor);
177
- groups = dbGroups.map((g) => ({
178
- sessionId: g.sessionId,
179
- messages: g.messages.map((m) => ({
180
- id: m.id,
181
- role: m.role,
182
- content: m.content,
183
- timestamp: m.timestamp,
184
- })),
185
- }));
186
- // Compute max recordedAtMs across all groups for cursor advancement
187
- for (const g of dbGroups) {
188
- for (const m of g.messages) {
189
- if (m.recordedAtMs > maxRecordedAtMs)
190
- maxRecordedAtMs = m.recordedAtMs;
191
- }
192
- }
193
- logger.debug?.(`${TAG} [l1] L0 data source: VectorStore DB`);
194
- }
195
- else {
196
- logger.debug?.(`${TAG} [l1] L0 data source: JSONL files (VectorStore unavailable)`);
197
- const jsonlGroups = await readConversationMessagesGroupedBySessionId(sessionKey, pluginDataDir, runnerState.last_l1_cursor || undefined, logger, 50);
198
- groups = jsonlGroups.map((g) => ({
199
- sessionId: g.sessionId,
200
- messages: g.messages,
201
- }));
202
- // Compute max recordedAtMs from JSONL groups
203
- for (const g of jsonlGroups) {
204
- for (const m of g.messages) {
205
- if (m.recordedAtMs > maxRecordedAtMs)
206
- maxRecordedAtMs = m.recordedAtMs;
207
- }
208
- }
209
- }
210
- if (groups.length === 0) {
211
- logger.debug?.(`${TAG} [l1] No new L0 messages for session ${sessionKey}`);
212
- return { processedCount: 0 };
213
- }
214
- const totalMessages = groups.reduce((sum, g) => sum + g.messages.length, 0);
215
- logger.info(`${TAG} [l1] Processing ${totalMessages} L0 messages across ${groups.length} sessionId group(s) for session ${sessionKey}`);
216
- let totalExtracted = 0;
217
- let totalStored = 0;
218
- let lastSceneName;
219
- for (const group of groups) {
220
- logger.debug?.(`${TAG} [l1] Group sessionId=${group.sessionId || "(empty)"}: ${group.messages.length} messages`);
221
- const l1Result = await extractL1Memories({
222
- messages: group.messages,
223
- sessionKey,
224
- sessionId: group.sessionId,
225
- baseDir: pluginDataDir,
226
- config,
227
- options: {
228
- enableDedup: cfg.extraction.enableDedup,
229
- maxMemoriesPerSession: cfg.extraction.maxMemoriesPerSession,
230
- model: cfg.extraction.model,
231
- previousSceneName: lastSceneName ?? (runnerState.last_scene_name || undefined),
232
- vectorStore,
233
- embeddingService,
234
- conflictRecallTopK: cfg.embedding.conflictRecallTopK,
235
- embeddingTimeoutMs: cfg.embedding.captureTimeoutMs ?? cfg.embedding.timeoutMs,
236
- llmRunner,
237
- },
238
- logger,
239
- instanceId: getInstanceId?.(),
240
- });
241
- totalExtracted += l1Result.extractedCount;
242
- totalStored += l1Result.storedCount;
243
- if (l1Result.lastSceneName) {
244
- lastSceneName = l1Result.lastSceneName;
245
- }
246
- }
247
- // Use maxRecordedAtMs (write time) as cursor — always positive, TCVDB-safe
248
- await checkpoint.markL1ExtractionComplete(sessionKey, totalStored, maxRecordedAtMs || undefined, lastSceneName);
249
- logger.info(`${TAG} [l1] L1 complete: extracted=${totalExtracted}, stored=${totalStored} (${groups.length} group(s))`);
250
- return { processedCount: totalMessages };
251
- }
252
- catch (err) {
253
- logger.error(`${TAG} [l1] L1 failed: ${err instanceof Error ? err.stack ?? err.message : String(err)}`);
254
- throw err;
255
- }
256
- };
257
- }
258
- // ============================
259
- // Persister factory
260
- // ============================
261
- /**
262
- * Create the standard pipeline state persister.
263
- * Saves pipeline session states to the checkpoint file.
264
- */
265
- export function createPersister(pluginDataDir, logger) {
266
- return async (states) => {
267
- const checkpoint = new CheckpointManager(pluginDataDir, logger);
268
- await checkpoint.mergePipelineStates(states);
269
- };
270
- }
271
- // ============================
272
- // L2 Runner factory
273
- // ============================
274
- /**
275
- * Create the standard L2 runner function (scene extraction).
276
- *
277
- * Reads L1 memory records (incremental via VectorStore or JSONL fallback),
278
- * runs SceneExtractor, and returns the latest cursor for pipeline-manager
279
- * to track incremental progress.
280
- *
281
- * Used by both `index.ts` (live runtime) and `seed-runtime.ts` (seed CLI).
282
- */
283
- export function createL2Runner(opts) {
284
- const { pluginDataDir, cfg, openclawConfig, vectorStore, logger, instanceId, llmRunner } = opts;
285
- let profileBaseline = new Map();
286
- return async (sessionKey, cursor) => {
287
- logger.debug?.(`${TAG} [L2] session=${sessionKey}, updatedAfter=${cursor ?? "(full)"}`);
288
- if (!openclawConfig && !llmRunner) {
289
- logger.warn(`${TAG} [L2] No OpenClaw config and no LLM runner, skipping scene extraction`);
290
- return;
291
- }
292
- let records;
293
- if (vectorStore?.pullProfiles && !vectorStore.isDegraded()) {
294
- profileBaseline = await pullProfilesToLocal(pluginDataDir, vectorStore, logger);
295
- }
296
- if (vectorStore && !vectorStore.isDegraded()) {
297
- const { queryMemoryRecords } = await import("../core/record/l1-reader.js");
298
- const memRecords = await queryMemoryRecords(vectorStore, {
299
- sessionKey,
300
- updatedAfter: cursor,
301
- }, logger);
302
- if (memRecords.length === 0) {
303
- logger.debug?.(`${TAG} [L2] No new L1 records since cursor (session=${sessionKey}, updatedAfter=${cursor ?? "(full)"}), skipping scene extraction`);
304
- return { skipped: true, latestCursor: cursor || undefined };
305
- }
306
- logger.debug?.(`${TAG} [L2] Incremental query returned ${memRecords.length} record(s) (session=${sessionKey})`);
307
- records = memRecords.map((r) => ({
308
- content: r.content,
309
- created_at: r.createdAt,
310
- id: r.id,
311
- updatedAt: r.updatedAt,
312
- }));
313
- }
314
- else {
315
- logger.debug?.(`${TAG} [L2] VectorStore unavailable, falling back to JSONL read (session=${sessionKey})`);
316
- const { readMemoryRecords } = await import("../core/record/l1-reader.js");
317
- let sessionRecords = await readMemoryRecords(sessionKey, pluginDataDir, logger);
318
- if (cursor) {
319
- const beforeCount = sessionRecords.length;
320
- sessionRecords = sessionRecords.filter((r) => {
321
- const t = r.updatedAt || r.createdAt || "";
322
- return t > cursor;
323
- });
324
- logger.debug?.(`${TAG} [L2] JSONL time filter: ${beforeCount} → ${sessionRecords.length} record(s) (updatedAfter=${cursor})`);
325
- }
326
- if (sessionRecords.length === 0) {
327
- logger.debug?.(`${TAG} [L2] No new L1 records found (JSONL fallback, session=${sessionKey}), skipping scene extraction`);
328
- return { latestCursor: cursor || undefined };
329
- }
330
- records = sessionRecords.map((r) => ({
331
- content: r.content,
332
- created_at: r.createdAt,
333
- id: r.id,
334
- updatedAt: r.updatedAt,
335
- }));
336
- }
337
- const extractor = new SceneExtractor({
338
- dataDir: pluginDataDir,
339
- config: openclawConfig,
340
- model: cfg.persona.model,
341
- maxScenes: cfg.persona.maxScenes,
342
- sceneBackupCount: cfg.persona.sceneBackupCount,
343
- logger,
344
- instanceId,
345
- llmRunner,
346
- });
347
- const memories = records.map((r) => ({
348
- content: r.content,
349
- created_at: r.created_at,
350
- id: r.id,
351
- }));
352
- const preCheckpoint = new CheckpointManager(pluginDataDir, logger);
353
- const preState = await preCheckpoint.read();
354
- const preScenesProcessed = preState.scenes_processed;
355
- const preMemoriesSince = preState.memories_since_last_persona;
356
- const preTotalProcessed = preState.total_processed;
357
- const extractResult = await extractor.extract(memories);
358
- if (extractResult.success && extractResult.memoriesProcessed > 0) {
359
- const checkpoint = new CheckpointManager(pluginDataDir, logger);
360
- const postState = await checkpoint.read();
361
- if (postState.scenes_processed < preScenesProcessed ||
362
- postState.total_processed < preTotalProcessed) {
363
- logger.warn(`${TAG} [L2] ⚠️ Checkpoint corruption detected! ` +
364
- `scenes_processed: ${preScenesProcessed} → ${postState.scenes_processed}, ` +
365
- `total_processed: ${preTotalProcessed} → ${postState.total_processed}, ` +
366
- `memories_since: ${preMemoriesSince} → ${postState.memories_since_last_persona}. ` +
367
- `Repairing...`);
368
- await checkpoint.write({
369
- ...postState,
370
- scenes_processed: Math.max(postState.scenes_processed, preScenesProcessed),
371
- total_processed: Math.max(postState.total_processed, preTotalProcessed),
372
- memories_since_last_persona: Math.max(postState.memories_since_last_persona, preMemoriesSince),
373
- });
374
- logger.info(`${TAG} [L2] Checkpoint repaired`);
375
- }
376
- if (vectorStore && supportsProfileSyncWrite(vectorStore)) {
377
- await syncLocalProfilesToStore(pluginDataDir, vectorStore, profileBaseline, logger);
378
- }
379
- await checkpoint.incrementScenesProcessed();
380
- const latestCursor = records.reduce((latest, r) => {
381
- return r.updatedAt > latest ? r.updatedAt : latest;
382
- }, "");
383
- logger.debug?.(`${TAG} [L2] Extraction complete: processed=${extractResult.memoriesProcessed}, latestCursor=${latestCursor}`);
384
- return { latestCursor: latestCursor || undefined };
385
- }
386
- };
387
- }
388
- // ============================
389
- // L3 Runner factory
390
- // ============================
391
- /**
392
- * Create the standard L3 runner function (persona generation).
393
- *
394
- * Uses PersonaTrigger to check if generation is needed, then runs
395
- * PersonaGenerator. Used by both `index.ts` and `seed-runtime.ts`.
396
- */
397
- export function createL3Runner(opts) {
398
- const { pluginDataDir, cfg, openclawConfig, vectorStore, logger, instanceId, llmRunner } = opts;
399
- return async () => {
400
- const trigger = new PersonaTrigger({
401
- dataDir: pluginDataDir,
402
- interval: cfg.persona.triggerEveryN,
403
- logger,
404
- });
405
- const { should, reason } = await trigger.shouldGenerate();
406
- if (!should) {
407
- logger.debug?.(`${TAG} [L3] Persona generation not needed`);
408
- return;
409
- }
410
- if (!openclawConfig && !llmRunner) {
411
- logger.warn(`${TAG} [L3] No OpenClaw config and no LLM runner, skipping persona generation`);
412
- return;
413
- }
414
- // Pull remote profiles to establish fresh baseline before generation.
415
- // This ensures syncLocalProfilesToStore() has correct baselineVersion
416
- // for the optimistic-lock check instead of defaulting to 0.
417
- let profileBaseline = new Map();
418
- if (vectorStore?.pullProfiles && !vectorStore.isDegraded()) {
419
- profileBaseline = await pullProfilesToLocal(pluginDataDir, vectorStore, logger);
420
- }
421
- logger.info(`${TAG} [L3] Starting persona generation: ${reason}`);
422
- const generator = new PersonaGenerator({
423
- dataDir: pluginDataDir,
424
- config: openclawConfig,
425
- model: cfg.persona.model,
426
- backupCount: cfg.persona.backupCount,
427
- logger,
428
- instanceId,
429
- llmRunner,
430
- });
431
- const genResult = await generator.generateLocalPersona(reason);
432
- if (!genResult) {
433
- logger.info(`${TAG} [L3] Persona generation skipped (no changes)`);
434
- return;
435
- }
436
- if (vectorStore && supportsProfileSyncWrite(vectorStore)) {
437
- await syncLocalProfilesToStore(pluginDataDir, vectorStore, profileBaseline, logger);
438
- }
439
- const checkpoint = new CheckpointManager(pluginDataDir, logger);
440
- const cp = await checkpoint.read();
441
- await checkpoint.markPersonaGenerated(cp.total_processed);
442
- logger.info(`${TAG} [L3] Persona generation succeeded`);
443
- };
444
- }
445
- // ============================
446
- // Pipeline Manager factory
447
- // ============================
448
- /**
449
- * Create a MemoryPipelineManager with the standard config mapping.
450
- */
451
- export function createPipelineManager(cfg, logger, sessionFilter) {
452
- return new MemoryPipelineManager({
453
- everyNConversations: cfg.pipeline.everyNConversations,
454
- enableWarmup: cfg.pipeline.enableWarmup,
455
- l1: { idleTimeoutSeconds: cfg.pipeline.l1IdleTimeoutSeconds },
456
- l2: {
457
- delayAfterL1Seconds: cfg.pipeline.l2DelayAfterL1Seconds,
458
- minIntervalSeconds: cfg.pipeline.l2MinIntervalSeconds,
459
- maxIntervalSeconds: cfg.pipeline.l2MaxIntervalSeconds,
460
- sessionActiveWindowHours: cfg.pipeline.sessionActiveWindowHours,
461
- },
462
- }, logger, sessionFilter ?? new SessionFilter([]));
463
- }
464
- // ============================
465
- // Full pipeline factory
466
- // ============================
467
- /**
468
- * Create a fully wired pipeline instance: VectorStore + EmbeddingService +
469
- * MemoryPipelineManager with L1 runner and persister attached.
470
- *
471
- * This is the high-level entry point used by both `index.ts` and `seed-runtime.ts`.
472
- * Callers should attach L2/L3 runners after creation using `createL2Runner()`
473
- * and `createL3Runner()` from this module.
474
- */
475
- export async function createPipeline(opts) {
476
- const { pluginDataDir, cfg, openclawConfig, logger, sessionFilter, l1LlmRunner } = opts;
477
- // Ensure data directories exist
478
- initDataDirectories(pluginDataDir);
479
- // Initialize stores (once-async: reuses cached result if already initialized)
480
- const stores = await initStores(cfg, pluginDataDir, logger);
481
- const { vectorStore, embeddingService } = stores;
482
- // Create pipeline manager
483
- const scheduler = createPipelineManager(cfg, logger, sessionFilter);
484
- // Wire L1 runner
485
- scheduler.setL1Runner(createL1Runner({
486
- pluginDataDir,
487
- cfg,
488
- openclawConfig,
489
- vectorStore,
490
- embeddingService,
491
- logger,
492
- llmRunner: l1LlmRunner,
493
- }));
494
- // Wire persister
495
- scheduler.setPersister(createPersister(pluginDataDir, logger));
496
- // Destroy function
497
- const destroy = async () => {
498
- logger.info(`${TAG} Destroying pipeline...`);
499
- await scheduler.destroy();
500
- if (vectorStore) {
501
- logger.info(`${TAG} Closing VectorStore`);
502
- vectorStore.close();
503
- }
504
- if (embeddingService?.close) {
505
- try {
506
- logger.info(`${TAG} Closing EmbeddingService`);
507
- await embeddingService.close();
508
- }
509
- catch (err) {
510
- logger.warn(`${TAG} Error closing EmbeddingService: ${err instanceof Error ? err.message : String(err)}`);
511
- }
512
- }
513
- resetStores(pluginDataDir);
514
- logger.info(`${TAG} Pipeline destroyed`);
515
- };
516
- return { scheduler, vectorStore, embeddingService, destroy };
517
- }
1
+ /**
2
+ * Pipeline factory: shared infrastructure for creating and wiring
3
+ * MemoryPipelineManager instances with VectorStore, EmbeddingService,
4
+ * L1 runner, L2 runner, L3 runner, and persister.
5
+ *
6
+ * Used by both:
7
+ * - `index.ts` (live plugin runtime)
8
+ * - `seed-runtime.ts` (standalone seed CLI command)
9
+ *
10
+ * This avoids duplicating VectorStore init, L1/L2/L3 extraction logic,
11
+ * persister wiring, and destroy sequences across multiple callers.
12
+ */
13
+ import fs from "node:fs";
14
+ import path from "node:path";
15
+ import { MemoryPipelineManager } from "./pipeline-manager.js";
16
+ import { SessionFilter } from "./session-filter.js";
17
+ import { extractL1Memories } from "../core/record/l1-extractor.js";
18
+ import { readConversationMessagesGroupedBySessionId } from "../core/conversation/l0-recorder.js";
19
+ import { CheckpointManager } from "./checkpoint.js";
20
+ import { createStoreBundle } from "../core/store/factory.js";
21
+ import { readManifest, writeManifest, buildStoreInfo, diffStoreBinding, } from "./manifest.js";
22
+ import { SceneExtractor } from "../core/scene/scene-extractor.js";
23
+ import { PersonaTrigger } from "../core/persona/persona-trigger.js";
24
+ import { PersonaGenerator } from "../core/persona/persona-generator.js";
25
+ import { pullProfilesToLocal, syncLocalProfilesToStore } from "../core/profile/profile-sync.js";
26
+ const TAG = "[memory-tdai] [pipeline-factory]";
27
+ function supportsProfileSyncWrite(store) {
28
+ return !!(store?.syncProfiles || store?.deleteProfiles);
29
+ }
30
+ // ============================
31
+ // Data directory init
32
+ // ============================
33
+ /**
34
+ * Ensure all required data subdirectories exist under `pluginDataDir`.
35
+ * Safe to call multiple times (mkdirSync with `recursive: true`).
36
+ */
37
+ export function initDataDirectories(dataDir) {
38
+ const dirs = ["conversations", "records", "scene_blocks", ".metadata", ".backup"];
39
+ for (const sub of dirs) {
40
+ fs.mkdirSync(path.join(dataDir, sub), { recursive: true });
41
+ }
42
+ }
43
+ /**
44
+ * Cached store init promises — keyed by `pluginDataDir` so that different
45
+ * data directories (e.g. live runtime vs. seed output) each get their own
46
+ * store instance, while concurrent callers for the *same* directory share
47
+ * one initialization.
48
+ */
49
+ const _storeInitCache = new Map();
50
+ /**
51
+ * Initialize store backend and (optionally) EmbeddingService.
52
+ *
53
+ * **Once-async semantics per dataDir**: the first call for a given
54
+ * `pluginDataDir` creates the store and caches the result; subsequent
55
+ * calls with the same dir return the cached Promise immediately.
56
+ * Call `resetStores()` during shutdown to clear the cache.
57
+ *
58
+ * Supports both SQLite (sync init) and TCVDB (async init) backends.
59
+ */
60
+ export function initStores(cfg, pluginDataDir, logger) {
61
+ const key = pluginDataDir;
62
+ if (!_storeInitCache.has(key)) {
63
+ _storeInitCache.set(key, _doInitStores(cfg, pluginDataDir, logger));
64
+ }
65
+ return _storeInitCache.get(key);
66
+ }
67
+ /**
68
+ * Reset the cached store singleton(s).
69
+ *
70
+ * Call this during `gateway_stop` (after closing the actual store/embedding
71
+ * resources) so that a subsequent `register()` on hot-restart can
72
+ * re-initialize fresh instances.
73
+ *
74
+ * @param pluginDataDir If provided, only clear the cache for that dir.
75
+ * If omitted, clear all cached stores.
76
+ */
77
+ export function resetStores(pluginDataDir) {
78
+ if (pluginDataDir) {
79
+ _storeInitCache.delete(pluginDataDir);
80
+ }
81
+ else {
82
+ _storeInitCache.clear();
83
+ }
84
+ }
85
+ /**
86
+ * Internal: actual store initialization logic (called once by the cache).
87
+ */
88
+ async function _doInitStores(cfg, pluginDataDir, logger) {
89
+ let vectorStore;
90
+ let embeddingService;
91
+ let needsReindex = false;
92
+ let reindexReason;
93
+ try {
94
+ const bundle = createStoreBundle(cfg, {
95
+ dataDir: pluginDataDir,
96
+ logger,
97
+ });
98
+ vectorStore = bundle.store;
99
+ embeddingService = bundle.embedding ?? undefined;
100
+ const providerInfo = embeddingService?.getProviderInfo();
101
+ const initResult = await vectorStore.init(providerInfo);
102
+ if (vectorStore.isDegraded()) {
103
+ logger.warn(`${TAG} Store is in degraded mode, falling back to keyword dedup`);
104
+ vectorStore = undefined;
105
+ embeddingService = undefined;
106
+ }
107
+ else {
108
+ logger.debug?.(`${TAG} Store initialized: backend=${cfg.storeBackend}, provider=${cfg.embedding.provider}`);
109
+ needsReindex = initResult.needsReindex;
110
+ reindexReason = initResult.reason;
111
+ // ── Manifest: first-write + config-drift detection ──
112
+ try {
113
+ const currentStoreInfo = buildStoreInfo(bundle.storeSnapshot);
114
+ const existing = readManifest(pluginDataDir);
115
+ if (!existing) {
116
+ // First init — write manifest
117
+ const manifest = {
118
+ version: 1,
119
+ createdAt: new Date().toISOString(),
120
+ store: currentStoreInfo,
121
+ seed: null,
122
+ };
123
+ writeManifest(pluginDataDir, manifest);
124
+ logger.debug?.(`${TAG} Manifest created: ${JSON.stringify(currentStoreInfo)}`);
125
+ }
126
+ else {
127
+ // Compare persisted store binding against current config
128
+ const diffs = diffStoreBinding(existing.store, currentStoreInfo);
129
+ if (diffs.length > 0) {
130
+ logger.debug?.(`${TAG} Store config differs from initial binding recorded in manifest ` +
131
+ `(${diffs.join("; ")}). ` +
132
+ `This is expected if the storage backend was switched intentionally.`);
133
+ }
134
+ }
135
+ }
136
+ catch (err) {
137
+ logger.warn(`${TAG} Failed to read/write manifest (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
138
+ }
139
+ }
140
+ }
141
+ catch (err) {
142
+ logger.warn(`${TAG} Store init failed; vector/FTS recall and dedup conflict detection will be unavailable: ${err instanceof Error ? err.message : String(err)}`);
143
+ vectorStore = undefined;
144
+ embeddingService = undefined;
145
+ }
146
+ return { vectorStore, embeddingService, needsReindex, reindexReason };
147
+ }
148
+ // ============================
149
+ // L1 Runner factory
150
+ // ============================
151
+ /**
152
+ * Create the standard L1 runner function.
153
+ *
154
+ * Reads L0 messages (from VectorStore DB or JSONL fallback), groups by sessionId,
155
+ * runs extractL1Memories for each group, and updates the checkpoint cursor.
156
+ */
157
+ export function createL1Runner(opts) {
158
+ const { pluginDataDir, cfg, openclawConfig, vectorStore, embeddingService, logger, getInstanceId, llmRunner } = opts;
159
+ const config = openclawConfig;
160
+ return async ({ sessionKey }) => {
161
+ if (!config && !llmRunner) {
162
+ logger.debug?.(`${TAG} [l1] No OpenClaw config and no LLM runner, skipping L1 extraction`);
163
+ return { processedCount: 0 };
164
+ }
165
+ const checkpoint = new CheckpointManager(pluginDataDir, logger);
166
+ const cp = await checkpoint.read();
167
+ const runnerState = checkpoint.getRunnerState(cp, sessionKey);
168
+ logger.info(`${TAG} [l1] Session ${sessionKey}: l1_cursor=${runnerState.last_l1_cursor || "(start)"}`);
169
+ try {
170
+ let groups;
171
+ let maxRecordedAtMs = 0;
172
+ if (vectorStore && !vectorStore.isDegraded()) {
173
+ const l1Cursor = runnerState.last_l1_cursor > 0
174
+ ? runnerState.last_l1_cursor
175
+ : undefined;
176
+ const dbGroups = await vectorStore.queryL0GroupedBySessionId(sessionKey, l1Cursor);
177
+ groups = dbGroups.map((g) => ({
178
+ sessionId: g.sessionId,
179
+ messages: g.messages.map((m) => ({
180
+ id: m.id,
181
+ role: m.role,
182
+ content: m.content,
183
+ timestamp: m.timestamp,
184
+ })),
185
+ }));
186
+ // Compute max recordedAtMs across all groups for cursor advancement
187
+ for (const g of dbGroups) {
188
+ for (const m of g.messages) {
189
+ if (m.recordedAtMs > maxRecordedAtMs)
190
+ maxRecordedAtMs = m.recordedAtMs;
191
+ }
192
+ }
193
+ logger.debug?.(`${TAG} [l1] L0 data source: VectorStore DB`);
194
+ }
195
+ else {
196
+ logger.debug?.(`${TAG} [l1] L0 data source: JSONL files (VectorStore unavailable)`);
197
+ const jsonlGroups = await readConversationMessagesGroupedBySessionId(sessionKey, pluginDataDir, runnerState.last_l1_cursor || undefined, logger, 50);
198
+ groups = jsonlGroups.map((g) => ({
199
+ sessionId: g.sessionId,
200
+ messages: g.messages,
201
+ }));
202
+ // Compute max recordedAtMs from JSONL groups
203
+ for (const g of jsonlGroups) {
204
+ for (const m of g.messages) {
205
+ if (m.recordedAtMs > maxRecordedAtMs)
206
+ maxRecordedAtMs = m.recordedAtMs;
207
+ }
208
+ }
209
+ }
210
+ if (groups.length === 0) {
211
+ logger.debug?.(`${TAG} [l1] No new L0 messages for session ${sessionKey}`);
212
+ return { processedCount: 0 };
213
+ }
214
+ const totalMessages = groups.reduce((sum, g) => sum + g.messages.length, 0);
215
+ logger.info(`${TAG} [l1] Processing ${totalMessages} L0 messages across ${groups.length} sessionId group(s) for session ${sessionKey}`);
216
+ let totalExtracted = 0;
217
+ let totalStored = 0;
218
+ let lastSceneName;
219
+ for (const group of groups) {
220
+ logger.debug?.(`${TAG} [l1] Group sessionId=${group.sessionId || "(empty)"}: ${group.messages.length} messages`);
221
+ const l1Result = await extractL1Memories({
222
+ messages: group.messages,
223
+ sessionKey,
224
+ sessionId: group.sessionId,
225
+ baseDir: pluginDataDir,
226
+ config,
227
+ options: {
228
+ enableDedup: cfg.extraction.enableDedup,
229
+ maxMemoriesPerSession: cfg.extraction.maxMemoriesPerSession,
230
+ model: cfg.extraction.model,
231
+ previousSceneName: lastSceneName ?? (runnerState.last_scene_name || undefined),
232
+ vectorStore,
233
+ embeddingService,
234
+ conflictRecallTopK: cfg.embedding.conflictRecallTopK,
235
+ embeddingTimeoutMs: cfg.embedding.captureTimeoutMs ?? cfg.embedding.timeoutMs,
236
+ llmRunner,
237
+ },
238
+ logger,
239
+ instanceId: getInstanceId?.(),
240
+ });
241
+ totalExtracted += l1Result.extractedCount;
242
+ totalStored += l1Result.storedCount;
243
+ if (l1Result.lastSceneName) {
244
+ lastSceneName = l1Result.lastSceneName;
245
+ }
246
+ }
247
+ // Use maxRecordedAtMs (write time) as cursor — always positive, TCVDB-safe
248
+ await checkpoint.markL1ExtractionComplete(sessionKey, totalStored, maxRecordedAtMs || undefined, lastSceneName);
249
+ logger.info(`${TAG} [l1] L1 complete: extracted=${totalExtracted}, stored=${totalStored} (${groups.length} group(s))`);
250
+ return { processedCount: totalMessages };
251
+ }
252
+ catch (err) {
253
+ logger.error(`${TAG} [l1] L1 failed: ${err instanceof Error ? err.stack ?? err.message : String(err)}`);
254
+ throw err;
255
+ }
256
+ };
257
+ }
258
+ // ============================
259
+ // Persister factory
260
+ // ============================
261
+ /**
262
+ * Create the standard pipeline state persister.
263
+ * Saves pipeline session states to the checkpoint file.
264
+ */
265
+ export function createPersister(pluginDataDir, logger) {
266
+ return async (states) => {
267
+ const checkpoint = new CheckpointManager(pluginDataDir, logger);
268
+ await checkpoint.mergePipelineStates(states);
269
+ };
270
+ }
271
+ // ============================
272
+ // L2 Runner factory
273
+ // ============================
274
+ /**
275
+ * Create the standard L2 runner function (scene extraction).
276
+ *
277
+ * Reads L1 memory records (incremental via VectorStore or JSONL fallback),
278
+ * runs SceneExtractor, and returns the latest cursor for pipeline-manager
279
+ * to track incremental progress.
280
+ *
281
+ * Used by both `index.ts` (live runtime) and `seed-runtime.ts` (seed CLI).
282
+ */
283
+ export function createL2Runner(opts) {
284
+ const { pluginDataDir, cfg, openclawConfig, vectorStore, logger, instanceId, llmRunner } = opts;
285
+ let profileBaseline = new Map();
286
+ return async (sessionKey, cursor) => {
287
+ logger.debug?.(`${TAG} [L2] session=${sessionKey}, updatedAfter=${cursor ?? "(full)"}`);
288
+ if (!openclawConfig && !llmRunner) {
289
+ logger.warn(`${TAG} [L2] No OpenClaw config and no LLM runner, skipping scene extraction`);
290
+ return;
291
+ }
292
+ let records;
293
+ if (vectorStore?.pullProfiles && !vectorStore.isDegraded()) {
294
+ profileBaseline = await pullProfilesToLocal(pluginDataDir, vectorStore, logger);
295
+ }
296
+ if (vectorStore && !vectorStore.isDegraded()) {
297
+ const { queryMemoryRecords } = await import("../core/record/l1-reader.js");
298
+ const memRecords = await queryMemoryRecords(vectorStore, {
299
+ sessionKey,
300
+ updatedAfter: cursor,
301
+ }, logger);
302
+ if (memRecords.length === 0) {
303
+ logger.debug?.(`${TAG} [L2] No new L1 records since cursor (session=${sessionKey}, updatedAfter=${cursor ?? "(full)"}), skipping scene extraction`);
304
+ return { skipped: true, latestCursor: cursor || undefined };
305
+ }
306
+ logger.debug?.(`${TAG} [L2] Incremental query returned ${memRecords.length} record(s) (session=${sessionKey})`);
307
+ records = memRecords.map((r) => ({
308
+ content: r.content,
309
+ created_at: r.createdAt,
310
+ id: r.id,
311
+ updatedAt: r.updatedAt,
312
+ }));
313
+ }
314
+ else {
315
+ logger.debug?.(`${TAG} [L2] VectorStore unavailable, falling back to JSONL read (session=${sessionKey})`);
316
+ const { readMemoryRecords } = await import("../core/record/l1-reader.js");
317
+ let sessionRecords = await readMemoryRecords(sessionKey, pluginDataDir, logger);
318
+ if (cursor) {
319
+ const beforeCount = sessionRecords.length;
320
+ sessionRecords = sessionRecords.filter((r) => {
321
+ const t = r.updatedAt || r.createdAt || "";
322
+ return t > cursor;
323
+ });
324
+ logger.debug?.(`${TAG} [L2] JSONL time filter: ${beforeCount} → ${sessionRecords.length} record(s) (updatedAfter=${cursor})`);
325
+ }
326
+ if (sessionRecords.length === 0) {
327
+ logger.debug?.(`${TAG} [L2] No new L1 records found (JSONL fallback, session=${sessionKey}), skipping scene extraction`);
328
+ return { latestCursor: cursor || undefined };
329
+ }
330
+ records = sessionRecords.map((r) => ({
331
+ content: r.content,
332
+ created_at: r.createdAt,
333
+ id: r.id,
334
+ updatedAt: r.updatedAt,
335
+ }));
336
+ }
337
+ const extractor = new SceneExtractor({
338
+ dataDir: pluginDataDir,
339
+ config: openclawConfig,
340
+ model: cfg.persona.model,
341
+ maxScenes: cfg.persona.maxScenes,
342
+ sceneBackupCount: cfg.persona.sceneBackupCount,
343
+ logger,
344
+ instanceId,
345
+ llmRunner,
346
+ });
347
+ const memories = records.map((r) => ({
348
+ content: r.content,
349
+ created_at: r.created_at,
350
+ id: r.id,
351
+ }));
352
+ const preCheckpoint = new CheckpointManager(pluginDataDir, logger);
353
+ const preState = await preCheckpoint.read();
354
+ const preScenesProcessed = preState.scenes_processed;
355
+ const preMemoriesSince = preState.memories_since_last_persona;
356
+ const preTotalProcessed = preState.total_processed;
357
+ const extractResult = await extractor.extract(memories);
358
+ if (extractResult.success && extractResult.memoriesProcessed > 0) {
359
+ const checkpoint = new CheckpointManager(pluginDataDir, logger);
360
+ const postState = await checkpoint.read();
361
+ if (postState.scenes_processed < preScenesProcessed ||
362
+ postState.total_processed < preTotalProcessed) {
363
+ logger.warn(`${TAG} [L2] ⚠️ Checkpoint corruption detected! ` +
364
+ `scenes_processed: ${preScenesProcessed} → ${postState.scenes_processed}, ` +
365
+ `total_processed: ${preTotalProcessed} → ${postState.total_processed}, ` +
366
+ `memories_since: ${preMemoriesSince} → ${postState.memories_since_last_persona}. ` +
367
+ `Repairing...`);
368
+ await checkpoint.write({
369
+ ...postState,
370
+ scenes_processed: Math.max(postState.scenes_processed, preScenesProcessed),
371
+ total_processed: Math.max(postState.total_processed, preTotalProcessed),
372
+ memories_since_last_persona: Math.max(postState.memories_since_last_persona, preMemoriesSince),
373
+ });
374
+ logger.info(`${TAG} [L2] Checkpoint repaired`);
375
+ }
376
+ if (vectorStore && supportsProfileSyncWrite(vectorStore)) {
377
+ await syncLocalProfilesToStore(pluginDataDir, vectorStore, profileBaseline, logger);
378
+ }
379
+ await checkpoint.incrementScenesProcessed();
380
+ const latestCursor = records.reduce((latest, r) => {
381
+ return r.updatedAt > latest ? r.updatedAt : latest;
382
+ }, "");
383
+ logger.debug?.(`${TAG} [L2] Extraction complete: processed=${extractResult.memoriesProcessed}, latestCursor=${latestCursor}`);
384
+ return { latestCursor: latestCursor || undefined };
385
+ }
386
+ };
387
+ }
388
+ // ============================
389
+ // L3 Runner factory
390
+ // ============================
391
+ /**
392
+ * Create the standard L3 runner function (persona generation).
393
+ *
394
+ * Uses PersonaTrigger to check if generation is needed, then runs
395
+ * PersonaGenerator. Used by both `index.ts` and `seed-runtime.ts`.
396
+ */
397
+ export function createL3Runner(opts) {
398
+ const { pluginDataDir, cfg, openclawConfig, vectorStore, logger, instanceId, llmRunner } = opts;
399
+ return async () => {
400
+ const trigger = new PersonaTrigger({
401
+ dataDir: pluginDataDir,
402
+ interval: cfg.persona.triggerEveryN,
403
+ logger,
404
+ });
405
+ const { should, reason } = await trigger.shouldGenerate();
406
+ if (!should) {
407
+ logger.debug?.(`${TAG} [L3] Persona generation not needed`);
408
+ return;
409
+ }
410
+ if (!openclawConfig && !llmRunner) {
411
+ logger.warn(`${TAG} [L3] No OpenClaw config and no LLM runner, skipping persona generation`);
412
+ return;
413
+ }
414
+ // Pull remote profiles to establish fresh baseline before generation.
415
+ // This ensures syncLocalProfilesToStore() has correct baselineVersion
416
+ // for the optimistic-lock check instead of defaulting to 0.
417
+ let profileBaseline = new Map();
418
+ if (vectorStore?.pullProfiles && !vectorStore.isDegraded()) {
419
+ profileBaseline = await pullProfilesToLocal(pluginDataDir, vectorStore, logger);
420
+ }
421
+ logger.info(`${TAG} [L3] Starting persona generation: ${reason}`);
422
+ const generator = new PersonaGenerator({
423
+ dataDir: pluginDataDir,
424
+ config: openclawConfig,
425
+ model: cfg.persona.model,
426
+ backupCount: cfg.persona.backupCount,
427
+ logger,
428
+ instanceId,
429
+ llmRunner,
430
+ });
431
+ const genResult = await generator.generateLocalPersona(reason);
432
+ if (!genResult) {
433
+ logger.info(`${TAG} [L3] Persona generation skipped (no changes)`);
434
+ return;
435
+ }
436
+ if (vectorStore && supportsProfileSyncWrite(vectorStore)) {
437
+ await syncLocalProfilesToStore(pluginDataDir, vectorStore, profileBaseline, logger);
438
+ }
439
+ const checkpoint = new CheckpointManager(pluginDataDir, logger);
440
+ const cp = await checkpoint.read();
441
+ await checkpoint.markPersonaGenerated(cp.total_processed);
442
+ logger.info(`${TAG} [L3] Persona generation succeeded`);
443
+ };
444
+ }
445
+ // ============================
446
+ // Pipeline Manager factory
447
+ // ============================
448
+ /**
449
+ * Create a MemoryPipelineManager with the standard config mapping.
450
+ */
451
+ export function createPipelineManager(cfg, logger, sessionFilter) {
452
+ return new MemoryPipelineManager({
453
+ everyNConversations: cfg.pipeline.everyNConversations,
454
+ enableWarmup: cfg.pipeline.enableWarmup,
455
+ l1: { idleTimeoutSeconds: cfg.pipeline.l1IdleTimeoutSeconds },
456
+ l2: {
457
+ delayAfterL1Seconds: cfg.pipeline.l2DelayAfterL1Seconds,
458
+ minIntervalSeconds: cfg.pipeline.l2MinIntervalSeconds,
459
+ maxIntervalSeconds: cfg.pipeline.l2MaxIntervalSeconds,
460
+ sessionActiveWindowHours: cfg.pipeline.sessionActiveWindowHours,
461
+ },
462
+ }, logger, sessionFilter ?? new SessionFilter([]));
463
+ }
464
+ // ============================
465
+ // Full pipeline factory
466
+ // ============================
467
+ /**
468
+ * Create a fully wired pipeline instance: VectorStore + EmbeddingService +
469
+ * MemoryPipelineManager with L1 runner and persister attached.
470
+ *
471
+ * This is the high-level entry point used by both `index.ts` and `seed-runtime.ts`.
472
+ * Callers should attach L2/L3 runners after creation using `createL2Runner()`
473
+ * and `createL3Runner()` from this module.
474
+ */
475
+ export async function createPipeline(opts) {
476
+ const { pluginDataDir, cfg, openclawConfig, logger, sessionFilter, l1LlmRunner } = opts;
477
+ // Ensure data directories exist
478
+ initDataDirectories(pluginDataDir);
479
+ // Initialize stores (once-async: reuses cached result if already initialized)
480
+ const stores = await initStores(cfg, pluginDataDir, logger);
481
+ const { vectorStore, embeddingService } = stores;
482
+ // Create pipeline manager
483
+ const scheduler = createPipelineManager(cfg, logger, sessionFilter);
484
+ // Wire L1 runner
485
+ scheduler.setL1Runner(createL1Runner({
486
+ pluginDataDir,
487
+ cfg,
488
+ openclawConfig,
489
+ vectorStore,
490
+ embeddingService,
491
+ logger,
492
+ llmRunner: l1LlmRunner,
493
+ }));
494
+ // Wire persister
495
+ scheduler.setPersister(createPersister(pluginDataDir, logger));
496
+ // Destroy function
497
+ const destroy = async () => {
498
+ logger.info(`${TAG} Destroying pipeline...`);
499
+ await scheduler.destroy();
500
+ if (vectorStore) {
501
+ logger.info(`${TAG} Closing VectorStore`);
502
+ vectorStore.close();
503
+ }
504
+ if (embeddingService?.close) {
505
+ try {
506
+ logger.info(`${TAG} Closing EmbeddingService`);
507
+ await embeddingService.close();
508
+ }
509
+ catch (err) {
510
+ logger.warn(`${TAG} Error closing EmbeddingService: ${err instanceof Error ? err.message : String(err)}`);
511
+ }
512
+ }
513
+ resetStores(pluginDataDir);
514
+ logger.info(`${TAG} Pipeline destroyed`);
515
+ };
516
+ return { scheduler, vectorStore, embeddingService, destroy };
517
+ }