@cognee/cognee-openclaw 2026.2.4 → 2026.3.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.
@@ -0,0 +1,17 @@
1
+ import type { DatasetState, MemoryScope, ScopedSyncIndexes, SyncIndex } from "./types.js";
2
+ export declare const STATE_DIR: string;
3
+ export declare const STATE_PATH: string;
4
+ export declare const SYNC_INDEX_PATH: string;
5
+ export declare const SCOPED_SYNC_INDEX_PATH: string;
6
+ export declare function loadDatasetState(): Promise<DatasetState>;
7
+ export declare function saveDatasetState(state: DatasetState): Promise<void>;
8
+ export declare function loadSyncIndex(): Promise<SyncIndex>;
9
+ export declare function saveSyncIndex(state: SyncIndex): Promise<void>;
10
+ export declare function loadScopedSyncIndexes(): Promise<ScopedSyncIndexes>;
11
+ export declare function saveScopedSyncIndexes(indexes: ScopedSyncIndexes): Promise<void>;
12
+ /**
13
+ * Fix #7: Migrate legacy single-scope sync index into multi-scope indexes.
14
+ * Moves all entries from the old sync-index.json into the specified default scope.
15
+ * After migration, the legacy file is left in place (harmless) but no longer read.
16
+ */
17
+ export declare function migrateLegacyIndex(defaultScope: MemoryScope): Promise<ScopedSyncIndexes | null>;
@@ -0,0 +1,107 @@
1
+ import { promises as fs } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { homedir } from "node:os";
4
+ // ---------------------------------------------------------------------------
5
+ // State file paths
6
+ // ---------------------------------------------------------------------------
7
+ export const STATE_DIR = join(homedir(), ".openclaw", "memory", "cognee");
8
+ export const STATE_PATH = join(STATE_DIR, "datasets.json");
9
+ export const SYNC_INDEX_PATH = join(STATE_DIR, "sync-index.json");
10
+ export const SCOPED_SYNC_INDEX_PATH = join(STATE_DIR, "scoped-sync-indexes.json");
11
+ // ---------------------------------------------------------------------------
12
+ // Dataset state (maps dataset name -> dataset ID)
13
+ // ---------------------------------------------------------------------------
14
+ export async function loadDatasetState() {
15
+ try {
16
+ const raw = await fs.readFile(STATE_PATH, "utf-8");
17
+ const parsed = JSON.parse(raw);
18
+ if (!parsed || typeof parsed !== "object")
19
+ return {};
20
+ return parsed;
21
+ }
22
+ catch (error) {
23
+ if (error.code === "ENOENT")
24
+ return {};
25
+ throw error;
26
+ }
27
+ }
28
+ export async function saveDatasetState(state) {
29
+ await fs.mkdir(dirname(STATE_PATH), { recursive: true });
30
+ await fs.writeFile(STATE_PATH, JSON.stringify(state, null, 2), "utf-8");
31
+ }
32
+ // ---------------------------------------------------------------------------
33
+ // Sync index (legacy single-scope)
34
+ // ---------------------------------------------------------------------------
35
+ export async function loadSyncIndex() {
36
+ try {
37
+ const raw = await fs.readFile(SYNC_INDEX_PATH, "utf-8");
38
+ const parsed = JSON.parse(raw);
39
+ if (!parsed || typeof parsed !== "object")
40
+ return { entries: {} };
41
+ const record = parsed;
42
+ record.entries ??= {};
43
+ return record;
44
+ }
45
+ catch (error) {
46
+ if (error.code === "ENOENT")
47
+ return { entries: {} };
48
+ throw error;
49
+ }
50
+ }
51
+ export async function saveSyncIndex(state) {
52
+ await fs.mkdir(dirname(SYNC_INDEX_PATH), { recursive: true });
53
+ await fs.writeFile(SYNC_INDEX_PATH, JSON.stringify(state, null, 2), "utf-8");
54
+ }
55
+ // ---------------------------------------------------------------------------
56
+ // Scoped sync indexes (multi-scope)
57
+ //
58
+ // Fix #6: On load, we validate that keys are valid MemoryScope values
59
+ // and discard any garbage entries (e.g. typos like "compnay").
60
+ //
61
+ // Fix #7: Migration support — when switching from single to multi-scope,
62
+ // we migrate the legacy sync index into the appropriate scope.
63
+ // ---------------------------------------------------------------------------
64
+ const VALID_SCOPES = new Set(["company", "user", "agent"]);
65
+ export async function loadScopedSyncIndexes() {
66
+ try {
67
+ const raw = await fs.readFile(SCOPED_SYNC_INDEX_PATH, "utf-8");
68
+ const parsed = JSON.parse(raw);
69
+ if (!parsed || typeof parsed !== "object")
70
+ return {};
71
+ // Validate keys are valid scopes
72
+ const result = {};
73
+ for (const [key, value] of Object.entries(parsed)) {
74
+ if (VALID_SCOPES.has(key) && value && typeof value === "object") {
75
+ const idx = value;
76
+ idx.entries ??= {};
77
+ result[key] = idx;
78
+ }
79
+ }
80
+ return result;
81
+ }
82
+ catch (error) {
83
+ if (error.code === "ENOENT")
84
+ return {};
85
+ throw error;
86
+ }
87
+ }
88
+ export async function saveScopedSyncIndexes(indexes) {
89
+ await fs.mkdir(dirname(SCOPED_SYNC_INDEX_PATH), { recursive: true });
90
+ await fs.writeFile(SCOPED_SYNC_INDEX_PATH, JSON.stringify(indexes, null, 2), "utf-8");
91
+ }
92
+ /**
93
+ * Fix #7: Migrate legacy single-scope sync index into multi-scope indexes.
94
+ * Moves all entries from the old sync-index.json into the specified default scope.
95
+ * After migration, the legacy file is left in place (harmless) but no longer read.
96
+ */
97
+ export async function migrateLegacyIndex(defaultScope) {
98
+ const legacy = await loadSyncIndex();
99
+ if (Object.keys(legacy.entries).length === 0)
100
+ return null;
101
+ const scoped = {
102
+ [defaultScope]: { ...legacy },
103
+ };
104
+ await saveScopedSyncIndexes(scoped);
105
+ return scoped;
106
+ }
107
+ //# sourceMappingURL=persistence.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"persistence.js","sourceRoot":"","sources":["../../src/persistence.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
2
+ declare const memoryCogneePlugin: {
3
+ id: string;
4
+ name: string;
5
+ description: string;
6
+ kind: "memory";
7
+ register(api: OpenClawPluginApi): void;
8
+ };
9
+ export default memoryCogneePlugin;
@@ -0,0 +1,510 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { MEMORY_SCOPES } from "./types.js";
3
+ import { CogneeHttpClient } from "./client.js";
4
+ import { resolveConfig } from "./config.js";
5
+ import { collectMemoryFiles } from "./files.js";
6
+ import { loadDatasetState, loadScopedSyncIndexes, loadSyncIndex, migrateLegacyIndex, SYNC_INDEX_PATH, } from "./persistence.js";
7
+ import { datasetNameForScope, isMultiScopeEnabled, routeFileToScope } from "./scope.js";
8
+ import { syncFiles, syncFilesScoped } from "./sync.js";
9
+ // ---------------------------------------------------------------------------
10
+ // Plugin registration
11
+ // ---------------------------------------------------------------------------
12
+ const memoryCogneePlugin = {
13
+ id: "cognee-openclaw",
14
+ name: "Memory (Cognee)",
15
+ description: "Cognee-backed memory with multi-scope support (company/user/agent), session tracking, and auto-recall",
16
+ kind: "memory",
17
+ register(api) {
18
+ const cfg = resolveConfig(api.pluginConfig);
19
+ const client = new CogneeHttpClient(cfg.baseUrl, cfg.apiKey, cfg.username, cfg.password, cfg.requestTimeoutMs, cfg.ingestionTimeoutMs);
20
+ const multiScope = isMultiScopeEnabled(cfg);
21
+ // Legacy single-scope state
22
+ let datasetId;
23
+ let syncIndex = { entries: {} };
24
+ // Multi-scope state
25
+ let scopedIndexes = {};
26
+ // Session state
27
+ let sessionId;
28
+ let resolvedWorkspaceDir;
29
+ // Load persisted state on startup
30
+ const stateReady = Promise.all([
31
+ loadDatasetState()
32
+ .then((state) => {
33
+ if (!multiScope) {
34
+ datasetId = state[cfg.datasetName];
35
+ }
36
+ })
37
+ .catch((error) => {
38
+ api.logger.warn?.(`cognee-openclaw: failed to load dataset state: ${String(error)}`);
39
+ }),
40
+ multiScope
41
+ ? loadScopedSyncIndexes()
42
+ .then(async (indexes) => {
43
+ // Fix #7: Migrate legacy index if scoped indexes are empty
44
+ if (Object.keys(indexes).length === 0) {
45
+ const migrated = await migrateLegacyIndex(cfg.defaultWriteScope);
46
+ if (migrated) {
47
+ scopedIndexes = migrated;
48
+ api.logger.info?.(`cognee-openclaw: migrated legacy sync index to scope "${cfg.defaultWriteScope}"`);
49
+ return;
50
+ }
51
+ }
52
+ scopedIndexes = indexes;
53
+ })
54
+ .catch((error) => {
55
+ api.logger.warn?.(`cognee-openclaw: failed to load scoped sync indexes: ${String(error)}`);
56
+ })
57
+ : loadSyncIndex()
58
+ .then((state) => {
59
+ syncIndex = state;
60
+ if (!datasetId && state.datasetId && state.datasetName === cfg.datasetName) {
61
+ datasetId = state.datasetId;
62
+ }
63
+ })
64
+ .catch((error) => {
65
+ api.logger.warn?.(`cognee-openclaw: failed to load sync index: ${String(error)}`);
66
+ }),
67
+ ]);
68
+ // Fix #8: Log when scopes have no dataset ID during recall
69
+ async function getRecallDatasetIds() {
70
+ const state = await loadDatasetState();
71
+ const ids = [];
72
+ const missingScopes = [];
73
+ if (multiScope) {
74
+ for (const scope of cfg.recallScopes) {
75
+ const dsName = datasetNameForScope(scope, cfg);
76
+ const dsId = state[dsName] ?? scopedIndexes[scope]?.datasetId;
77
+ if (dsId) {
78
+ ids.push(dsId);
79
+ }
80
+ else {
81
+ missingScopes.push(scope);
82
+ }
83
+ }
84
+ }
85
+ else {
86
+ if (datasetId)
87
+ ids.push(datasetId);
88
+ }
89
+ return { ids, missingScopes };
90
+ }
91
+ // Helper: run sync
92
+ async function runSync(workspaceDir, logger) {
93
+ await stateReady;
94
+ const files = await collectMemoryFiles(workspaceDir);
95
+ if (files.length === 0) {
96
+ logger.info?.("cognee-openclaw: no memory files found");
97
+ return { added: 0, updated: 0, skipped: 0, errors: 0, deleted: 0 };
98
+ }
99
+ logger.info?.(`cognee-openclaw: found ${files.length} memory file(s), syncing...`);
100
+ if (multiScope) {
101
+ return syncFilesScoped(client, files, files, scopedIndexes, cfg, logger);
102
+ }
103
+ else {
104
+ const result = await syncFiles(client, files, files, syncIndex, cfg, logger);
105
+ if (result.datasetId)
106
+ datasetId = result.datasetId;
107
+ return result;
108
+ }
109
+ }
110
+ // ------------------------------------------------------------------
111
+ // CLI commands
112
+ // ------------------------------------------------------------------
113
+ api.registerCli((ctx) => {
114
+ const cognee = ctx.program.command("cognee").description("Cognee memory management");
115
+ const cliWorkspaceDir = ctx.workspaceDir || process.cwd();
116
+ cognee
117
+ .command("index")
118
+ .description("Sync memory files to Cognee (add new, update changed, skip unchanged)")
119
+ .action(async () => {
120
+ const result = await runSync(cliWorkspaceDir, ctx.logger);
121
+ const summary = `Sync complete: ${result.added} added, ${result.updated} updated, ${result.deleted} deleted, ${result.skipped} unchanged, ${result.errors} errors`;
122
+ ctx.logger.info?.(summary);
123
+ console.log(summary);
124
+ process.exit(0);
125
+ });
126
+ cognee
127
+ .command("status")
128
+ .description("Show Cognee sync state")
129
+ .action(async () => {
130
+ await stateReady;
131
+ const files = await collectMemoryFiles(cliWorkspaceDir);
132
+ if (multiScope) {
133
+ const state = await loadDatasetState();
134
+ for (const scope of MEMORY_SCOPES) {
135
+ const dsName = datasetNameForScope(scope, cfg);
136
+ const scopeIndex = scopedIndexes[scope] ?? { entries: {} };
137
+ const entryCount = Object.keys(scopeIndex.entries).length;
138
+ const scopeFiles = files.filter(f => routeFileToScope(f.path, cfg.scopeRouting, cfg.defaultWriteScope) === scope);
139
+ let dirty = 0, newCount = 0;
140
+ for (const file of scopeFiles) {
141
+ const existing = scopeIndex.entries[file.path];
142
+ if (!existing)
143
+ newCount++;
144
+ else if (existing.hash !== file.hash)
145
+ dirty++;
146
+ }
147
+ console.log(`\n[${scope.toUpperCase()}] Dataset: ${dsName}`);
148
+ console.log(` Dataset ID: ${state[dsName] ?? scopeIndex.datasetId ?? "(not set)"}`);
149
+ console.log(` Indexed files: ${entryCount}`);
150
+ console.log(` Workspace files: ${scopeFiles.length}`);
151
+ console.log(` New (unindexed): ${newCount}`);
152
+ console.log(` Changed (dirty): ${dirty}`);
153
+ }
154
+ }
155
+ else {
156
+ const entryCount = Object.keys(syncIndex.entries).length;
157
+ const entriesWithDataId = Object.values(syncIndex.entries).filter((e) => e.dataId).length;
158
+ let dirty = 0, newCount = 0;
159
+ for (const file of files) {
160
+ const existing = syncIndex.entries[file.path];
161
+ if (!existing)
162
+ newCount++;
163
+ else if (existing.hash !== file.hash)
164
+ dirty++;
165
+ }
166
+ console.log([
167
+ `Dataset: ${syncIndex.datasetName ?? cfg.datasetName}`,
168
+ `Dataset ID: ${datasetId ?? syncIndex.datasetId ?? "(not set)"}`,
169
+ `Indexed files: ${entryCount} (${entriesWithDataId} with data ID)`,
170
+ `Workspace files: ${files.length}`,
171
+ `New (unindexed): ${newCount}`,
172
+ `Changed (dirty): ${dirty}`,
173
+ `Sync index: ${SYNC_INDEX_PATH}`,
174
+ ].join("\n"));
175
+ }
176
+ process.exit(0);
177
+ });
178
+ cognee
179
+ .command("health")
180
+ .description("Check Cognee API connectivity")
181
+ .action(async () => {
182
+ try {
183
+ const result = await client.health();
184
+ console.log(`Cognee API: OK (${cfg.baseUrl})`);
185
+ if (result.status)
186
+ console.log(`Status: ${result.status}`);
187
+ }
188
+ catch (error) {
189
+ console.log(`Cognee API: UNREACHABLE (${cfg.baseUrl})`);
190
+ console.log(`Error: ${error instanceof Error ? error.message : String(error)}`);
191
+ process.exit(1);
192
+ }
193
+ process.exit(0);
194
+ });
195
+ cognee
196
+ .command("setup")
197
+ .description("Configure OpenClaw to use Cognee for memory (default: replaces built-in, --hybrid: alongside built-in)")
198
+ .option("--hybrid", "Keep built-in memory providers enabled alongside Cognee")
199
+ .action(async (opts) => {
200
+ const { loadConfig, writeConfigFile } = api.runtime.config;
201
+ const config = loadConfig();
202
+ // Set Cognee as the memory slot
203
+ config.plugins ??= {};
204
+ config.plugins.slots ??= {};
205
+ config.plugins.slots.memory = "cognee-openclaw";
206
+ config.plugins.entries ??= {};
207
+ const entries = config.plugins.entries;
208
+ if (opts.hybrid) {
209
+ // Hybrid mode: keep built-in memory enabled
210
+ entries["memory-core"] ??= { enabled: true };
211
+ entries["memory-core"].enabled = true;
212
+ }
213
+ else {
214
+ // Exclusive mode: disable built-in memory providers
215
+ entries["memory-core"] = { enabled: false };
216
+ entries["memory-lancedb"] = { enabled: false };
217
+ }
218
+ // Ensure cognee-openclaw is enabled
219
+ entries["cognee-openclaw"] ??= { enabled: true };
220
+ entries["cognee-openclaw"].enabled = true;
221
+ await writeConfigFile(config);
222
+ if (opts.hybrid) {
223
+ console.log("Cognee memory setup complete (hybrid mode):");
224
+ console.log(" - Memory slot set to cognee-openclaw");
225
+ console.log(" - memory-core enabled (built-in memory active)");
226
+ console.log("\nBoth Cognee recall and built-in memory search are active.");
227
+ }
228
+ else {
229
+ console.log("Cognee memory setup complete:");
230
+ console.log(" - Memory slot set to cognee-openclaw");
231
+ console.log(" - memory-core disabled");
232
+ console.log(" - memory-lancedb disabled");
233
+ }
234
+ console.log("\nRun 'openclaw cognee health' to verify Cognee connectivity.");
235
+ process.exit(0);
236
+ });
237
+ cognee
238
+ .command("scopes")
239
+ .description("Show memory scope routing for current workspace files")
240
+ .action(async () => {
241
+ const files = await collectMemoryFiles(cliWorkspaceDir);
242
+ if (files.length === 0) {
243
+ console.log("No memory files found.");
244
+ process.exit(0);
245
+ }
246
+ if (!multiScope) {
247
+ console.log(`Multi-scope mode is OFF. All files go to dataset "${cfg.datasetName}".`);
248
+ console.log(`Set companyDataset, userDatasetPrefix, or agentDatasetPrefix to enable.`);
249
+ process.exit(0);
250
+ }
251
+ const grouped = { company: [], user: [], agent: [] };
252
+ for (const file of files) {
253
+ const scope = routeFileToScope(file.path, cfg.scopeRouting, cfg.defaultWriteScope);
254
+ grouped[scope].push(file.path);
255
+ }
256
+ for (const scope of MEMORY_SCOPES) {
257
+ const dsName = datasetNameForScope(scope, cfg);
258
+ console.log(`\n[${scope.toUpperCase()}] -> dataset "${dsName}"`);
259
+ if (grouped[scope].length === 0)
260
+ console.log(" (no files)");
261
+ else
262
+ for (const p of grouped[scope])
263
+ console.log(` ${p}`);
264
+ }
265
+ process.exit(0);
266
+ });
267
+ }, { commands: ["cognee"] });
268
+ // ------------------------------------------------------------------
269
+ // Auto-sync on startup (with health check)
270
+ // ------------------------------------------------------------------
271
+ if (cfg.autoIndex) {
272
+ api.registerService({
273
+ id: "cognee-auto-sync",
274
+ async start(ctx) {
275
+ resolvedWorkspaceDir = ctx.workspaceDir || process.cwd();
276
+ try {
277
+ await client.health();
278
+ }
279
+ catch (error) {
280
+ ctx.logger.warn?.(`cognee-openclaw: Cognee API unreachable at ${cfg.baseUrl} — auto-sync disabled for this session. Error: ${String(error)}`);
281
+ return;
282
+ }
283
+ if (cfg.enableSessions) {
284
+ sessionId = `openclaw-${randomUUID()}`;
285
+ ctx.logger.info?.(`cognee-openclaw: session ${sessionId}`);
286
+ }
287
+ try {
288
+ const result = await runSync(resolvedWorkspaceDir, ctx.logger);
289
+ ctx.logger.info?.(`cognee-openclaw: auto-sync complete: ${result.added} added, ${result.updated} updated, ${result.deleted} deleted, ${result.skipped} unchanged`);
290
+ }
291
+ catch (error) {
292
+ ctx.logger.warn?.(`cognee-openclaw: auto-sync failed: ${String(error)}`);
293
+ }
294
+ },
295
+ });
296
+ }
297
+ // ------------------------------------------------------------------
298
+ // Auto-recall: inject memories before each agent run
299
+ // ------------------------------------------------------------------
300
+ if (cfg.autoRecall) {
301
+ api.on("before_agent_start", async (event, ctx) => {
302
+ await stateReady;
303
+ if (!event.prompt || event.prompt.length < 5) {
304
+ api.logger.debug?.("cognee-openclaw: skipping recall (prompt too short)");
305
+ return;
306
+ }
307
+ const { ids: recallDatasetIds, missingScopes } = await getRecallDatasetIds();
308
+ // Fix #8: Log missing scopes so users know what's not being searched
309
+ if (missingScopes.length > 0) {
310
+ api.logger.info?.(`cognee-openclaw: scope(s) not yet indexed (no data): ${missingScopes.join(", ")}`);
311
+ }
312
+ if (recallDatasetIds.length === 0) {
313
+ api.logger.debug?.("cognee-openclaw: skipping recall (no datasetIds)");
314
+ return;
315
+ }
316
+ try {
317
+ if (multiScope) {
318
+ // Fix #10: Use Promise.allSettled for resilience
319
+ const state = await loadDatasetState();
320
+ const searchPromises = cfg.recallScopes.map(async (scope) => {
321
+ const dsName = datasetNameForScope(scope, cfg);
322
+ const dsId = state[dsName] ?? scopedIndexes[scope]?.datasetId;
323
+ if (!dsId)
324
+ return null;
325
+ const results = await client.search({
326
+ queryText: event.prompt,
327
+ searchType: cfg.searchType,
328
+ datasetIds: [dsId],
329
+ searchPrompt: cfg.searchPrompt,
330
+ maxTokens: cfg.maxTokens,
331
+ sessionId,
332
+ });
333
+ const filtered = results
334
+ .filter((r) => r.score >= cfg.minScore)
335
+ .slice(0, cfg.maxResults);
336
+ return filtered.length > 0 ? { scope, results: filtered } : null;
337
+ });
338
+ // Fix #10: allSettled — inject whatever succeeds, log failures
339
+ const settled = await Promise.allSettled(searchPromises);
340
+ const scopeResults = {};
341
+ for (let i = 0; i < settled.length; i++) {
342
+ const outcome = settled[i];
343
+ const scope = cfg.recallScopes[i];
344
+ if (outcome.status === "fulfilled" && outcome.value) {
345
+ scopeResults[outcome.value.scope] = outcome.value.results;
346
+ }
347
+ else if (outcome.status === "rejected") {
348
+ api.logger.warn?.(`cognee-openclaw: recall failed for scope ${scope}: ${String(outcome.reason)}`);
349
+ }
350
+ }
351
+ if (Object.keys(scopeResults).length === 0) {
352
+ api.logger.debug?.("cognee-openclaw: search returned no results above minScore");
353
+ return;
354
+ }
355
+ const sections = [];
356
+ for (const scope of cfg.recallScopes) {
357
+ const results = scopeResults[scope];
358
+ if (!results || results.length === 0)
359
+ continue;
360
+ const payload = JSON.stringify(results.map((r) => ({ id: r.id, score: r.score, text: r.text, metadata: r.metadata })), null, 2);
361
+ sections.push(`<${scope}_memory>\n${payload}\n</${scope}_memory>`);
362
+ }
363
+ const totalResults = Object.values(scopeResults).reduce((sum, arr) => sum + arr.length, 0);
364
+ api.logger.info?.(`cognee-openclaw: injecting ${totalResults} memories across ${Object.keys(scopeResults).length} scope(s)`);
365
+ return { prependContext: `<cognee_memories>\n${sections.join("\n")}\n</cognee_memories>` };
366
+ }
367
+ else {
368
+ // Legacy single-scope
369
+ const results = await client.search({
370
+ queryText: event.prompt,
371
+ searchType: cfg.searchType,
372
+ datasetIds: recallDatasetIds,
373
+ searchPrompt: cfg.searchPrompt,
374
+ maxTokens: cfg.maxTokens,
375
+ sessionId,
376
+ });
377
+ const filtered = results
378
+ .filter((r) => r.score >= cfg.minScore)
379
+ .slice(0, cfg.maxResults);
380
+ if (filtered.length === 0) {
381
+ api.logger.debug?.("cognee-openclaw: search returned no results above minScore");
382
+ return;
383
+ }
384
+ const payload = JSON.stringify(filtered.map((r) => ({ id: r.id, score: r.score, text: r.text, metadata: r.metadata })), null, 2);
385
+ api.logger.info?.(`cognee-openclaw: injecting ${filtered.length} memories`);
386
+ return { prependContext: `<cognee_memories>\nRelevant memories:\n${payload}\n</cognee_memories>` };
387
+ }
388
+ }
389
+ catch (error) {
390
+ api.logger.warn?.(`cognee-openclaw: recall failed: ${String(error)}`);
391
+ }
392
+ });
393
+ }
394
+ // ------------------------------------------------------------------
395
+ // Post-agent sync + session persistence
396
+ // ------------------------------------------------------------------
397
+ if (cfg.autoIndex) {
398
+ api.on("agent_end", async (event, ctx) => {
399
+ if (!event.success)
400
+ return;
401
+ await stateReady;
402
+ const workspaceDir = resolvedWorkspaceDir || process.cwd();
403
+ // Fix #4: Actually persist the session into the knowledge graph
404
+ if (cfg.enableSessions && cfg.persistSessionsAfterEnd && sessionId) {
405
+ try {
406
+ // Determine target dataset for session persistence
407
+ const targetDatasetIds = [];
408
+ if (multiScope) {
409
+ const state = await loadDatasetState();
410
+ const agentDsName = datasetNameForScope("agent", cfg);
411
+ const agentDsId = state[agentDsName] ?? scopedIndexes.agent?.datasetId;
412
+ if (agentDsId)
413
+ targetDatasetIds.push(agentDsId);
414
+ }
415
+ else if (datasetId) {
416
+ targetDatasetIds.push(datasetId);
417
+ }
418
+ if (targetDatasetIds.length > 0) {
419
+ // Call Cognee's session persistence endpoint via the generic fetchJson
420
+ await client.fetchJson("/api/v1/sessions/persist", {
421
+ method: "POST",
422
+ headers: { "Content-Type": "application/json" },
423
+ body: JSON.stringify({
424
+ session_ids: [sessionId],
425
+ dataset_ids: targetDatasetIds,
426
+ }),
427
+ }).catch(() => {
428
+ // Session persistence endpoint may not exist on all Cognee versions.
429
+ // Fail silently — this is an enhancement, not critical path.
430
+ api.logger.debug?.("cognee-openclaw: session persistence endpoint not available");
431
+ });
432
+ }
433
+ }
434
+ catch (error) {
435
+ api.logger.warn?.(`cognee-openclaw: session persistence failed: ${String(error)}`);
436
+ }
437
+ }
438
+ // Sync file changes
439
+ try {
440
+ if (multiScope) {
441
+ try {
442
+ const freshIndexes = await loadScopedSyncIndexes();
443
+ scopedIndexes = freshIndexes;
444
+ }
445
+ catch { /* fall through */ }
446
+ const files = await collectMemoryFiles(workspaceDir);
447
+ let hasChanges = false;
448
+ for (const file of files) {
449
+ const scope = routeFileToScope(file.path, cfg.scopeRouting, cfg.defaultWriteScope);
450
+ const scopeIndex = scopedIndexes[scope];
451
+ if (!scopeIndex) {
452
+ hasChanges = true;
453
+ break;
454
+ }
455
+ const existing = scopeIndex.entries[file.path];
456
+ if (!existing || existing.hash !== file.hash) {
457
+ hasChanges = true;
458
+ break;
459
+ }
460
+ }
461
+ if (!hasChanges) {
462
+ const currentPaths = new Set(files.map(f => f.path));
463
+ for (const scopeIndex of Object.values(scopedIndexes)) {
464
+ if (scopeIndex && Object.keys(scopeIndex.entries).some(p => !currentPaths.has(p))) {
465
+ hasChanges = true;
466
+ break;
467
+ }
468
+ }
469
+ }
470
+ if (!hasChanges)
471
+ return;
472
+ api.logger.info?.("cognee-openclaw: detected changes, syncing across scopes...");
473
+ const result = await syncFilesScoped(client, files, files, scopedIndexes, cfg, api.logger);
474
+ api.logger.info?.(`cognee-openclaw: post-agent sync: ${result.added} added, ${result.updated} updated, ${result.deleted} deleted`);
475
+ }
476
+ else {
477
+ try {
478
+ const freshIndex = await loadSyncIndex();
479
+ syncIndex.entries = freshIndex.entries;
480
+ if (freshIndex.datasetId)
481
+ syncIndex.datasetId = freshIndex.datasetId;
482
+ if (freshIndex.datasetName)
483
+ syncIndex.datasetName = freshIndex.datasetName;
484
+ }
485
+ catch { /* fall through */ }
486
+ const files = await collectMemoryFiles(workspaceDir);
487
+ const changedFiles = files.filter((f) => {
488
+ const existing = syncIndex.entries[f.path];
489
+ return !existing || existing.hash !== f.hash;
490
+ });
491
+ const currentPaths = new Set(files.map(f => f.path));
492
+ const hasDeletedFiles = Object.keys(syncIndex.entries).some(p => !currentPaths.has(p));
493
+ if (changedFiles.length === 0 && !hasDeletedFiles)
494
+ return;
495
+ api.logger.info?.(`cognee-openclaw: detected ${changedFiles.length} changed file(s)${hasDeletedFiles ? " + deletions" : ""}, syncing...`);
496
+ const result = await syncFiles(client, changedFiles, files, syncIndex, cfg, api.logger);
497
+ if (result.datasetId)
498
+ datasetId = result.datasetId;
499
+ api.logger.info?.(`cognee-openclaw: post-agent sync: ${result.added} added, ${result.updated} updated, ${result.deleted} deleted`);
500
+ }
501
+ }
502
+ catch (error) {
503
+ api.logger.warn?.(`cognee-openclaw: post-agent sync failed: ${String(error)}`);
504
+ }
505
+ });
506
+ }
507
+ },
508
+ };
509
+ export default memoryCogneePlugin;
510
+ //# sourceMappingURL=plugin.js.map