dsh-tdai-memory 0.1.0
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/LICENSE +21 -0
- package/README.md +56 -0
- package/THIRD-PARTY-NOTICES.md +20 -0
- package/index.js +328 -0
- package/package.json +53 -0
- package/recall-inject.js +80 -0
- package/vendor/tdai/adapters/index.js +15 -0
- package/vendor/tdai/adapters/openclaw/host-adapter.js +83 -0
- package/vendor/tdai/adapters/openclaw/index.js +5 -0
- package/vendor/tdai/adapters/openclaw/llm-runner.js +69 -0
- package/vendor/tdai/adapters/standalone/host-adapter.js +58 -0
- package/vendor/tdai/adapters/standalone/index.js +5 -0
- package/vendor/tdai/adapters/standalone/llm-runner.js +246 -0
- package/vendor/tdai/config.js +363 -0
- package/vendor/tdai/core/conversation/l0-recorder.js +420 -0
- package/vendor/tdai/core/hooks/auto-capture.js +236 -0
- package/vendor/tdai/core/hooks/auto-recall.js +653 -0
- package/vendor/tdai/core/index.js +8 -0
- package/vendor/tdai/core/persona/persona-generator.js +184 -0
- package/vendor/tdai/core/persona/persona-trigger.js +94 -0
- package/vendor/tdai/core/profile/profile-sync.js +200 -0
- package/vendor/tdai/core/prompts/l1-dedup.js +136 -0
- package/vendor/tdai/core/prompts/l1-extraction.js +132 -0
- package/vendor/tdai/core/prompts/persona-generation.js +160 -0
- package/vendor/tdai/core/prompts/scene-extraction.js +269 -0
- package/vendor/tdai/core/record/l1-dedup.js +318 -0
- package/vendor/tdai/core/record/l1-extractor.js +379 -0
- package/vendor/tdai/core/record/l1-reader.js +178 -0
- package/vendor/tdai/core/record/l1-writer.js +141 -0
- package/vendor/tdai/core/report/reporter.js +86 -0
- package/vendor/tdai/core/scene/filename-normalizer.js +161 -0
- package/vendor/tdai/core/scene/scene-extractor.js +410 -0
- package/vendor/tdai/core/scene/scene-format.js +53 -0
- package/vendor/tdai/core/scene/scene-index.js +82 -0
- package/vendor/tdai/core/scene/scene-navigation.js +60 -0
- package/vendor/tdai/core/seed/input.js +404 -0
- package/vendor/tdai/core/seed/seed-runtime.js +314 -0
- package/vendor/tdai/core/seed/types.js +9 -0
- package/vendor/tdai/core/store/bm25-client.js +117 -0
- package/vendor/tdai/core/store/bm25-local.js +68 -0
- package/vendor/tdai/core/store/embedding.js +632 -0
- package/vendor/tdai/core/store/factory.js +95 -0
- package/vendor/tdai/core/store/search-utils.js +54 -0
- package/vendor/tdai/core/store/sqlite.js +1822 -0
- package/vendor/tdai/core/store/tcvdb-client.js +221 -0
- package/vendor/tdai/core/store/tcvdb.js +1047 -0
- package/vendor/tdai/core/store/types.js +17 -0
- package/vendor/tdai/core/tdai-core.js +446 -0
- package/vendor/tdai/core/tools/conversation-search.js +199 -0
- package/vendor/tdai/core/tools/memory-search.js +207 -0
- package/vendor/tdai/core/types.js +12 -0
- package/vendor/tdai/utils/backup.js +189 -0
- package/vendor/tdai/utils/checkpoint.js +356 -0
- package/vendor/tdai/utils/clean-context-runner.js +472 -0
- package/vendor/tdai/utils/ensure-hook-policy.js +213 -0
- package/vendor/tdai/utils/env.js +13 -0
- package/vendor/tdai/utils/managed-timer.js +127 -0
- package/vendor/tdai/utils/manifest.js +91 -0
- package/vendor/tdai/utils/memory-cleaner.js +300 -0
- package/vendor/tdai/utils/no-think-fetch.js +109 -0
- package/vendor/tdai/utils/openclaw-state-dir.js +26 -0
- package/vendor/tdai/utils/pipeline-factory.js +517 -0
- package/vendor/tdai/utils/pipeline-manager.js +892 -0
- package/vendor/tdai/utils/sanitize.js +360 -0
- package/vendor/tdai/utils/serial-queue.js +103 -0
- package/vendor/tdai/utils/session-filter.js +88 -0
- package/vendor/tdai/utils/text-utils.js +29 -0
- package/vendor/tdai/utils/time.js +252 -0
|
@@ -0,0 +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
|
+
}
|