@cognee/cognee-openclaw 2026.4.13 → 2026.6.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -6
- package/dist/src/client.js +7 -7
- package/dist/src/client.js.map +1 -1
- package/dist/src/config.d.ts +1 -0
- package/dist/src/config.js +9 -2
- package/dist/src/config.js.map +1 -1
- package/dist/src/files.js +6 -0
- package/dist/src/files.js.map +1 -1
- package/dist/src/persistence.d.ts +13 -1
- package/dist/src/persistence.js +59 -0
- package/dist/src/persistence.js.map +1 -1
- package/dist/src/plugin.js +338 -69
- package/dist/src/plugin.js.map +1 -1
- package/dist/src/scope.d.ts +11 -9
- package/dist/src/scope.js +18 -16
- package/dist/src/scope.js.map +1 -1
- package/dist/src/sync.d.ts +14 -2
- package/dist/src/sync.js +27 -13
- package/dist/src/sync.js.map +1 -1
- package/dist/src/types.d.ts +18 -0
- package/package.json +1 -1
package/dist/src/plugin.js
CHANGED
|
@@ -1,12 +1,29 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
// SyncResult is used as the return type of the per-agent sync helpers below.
|
|
2
4
|
import { MEMORY_SCOPES } from "./types.js";
|
|
3
5
|
import { CogneeHttpClient } from "./client.js";
|
|
4
6
|
import { resolveConfig } from "./config.js";
|
|
5
7
|
import { collectMemoryFiles } from "./files.js";
|
|
6
8
|
import { buildMemoryFlushPlan } from "./flush-plan.js";
|
|
7
|
-
import { loadDatasetState, loadScopedSyncIndexes, loadSyncIndex, saveDatasetState, saveScopedSyncIndexes, saveSyncIndex, migrateLegacyIndex, SYNC_INDEX_PATH, } from "./persistence.js";
|
|
8
|
-
import { datasetNameForScope,
|
|
9
|
+
import { loadDatasetState, loadScopedSyncIndexes, loadSyncIndex, loadAgentSyncIndexes, saveDatasetState, saveScopedSyncIndexes, saveSyncIndex, saveAgentSyncIndexes, migrateLegacyIndex, migrateAgentScopeToPerAgent, SYNC_INDEX_PATH, } from "./persistence.js";
|
|
10
|
+
import { datasetNameForScope, isMultiScopeEnabled, normalizeAgentId, routeFileToScope } from "./scope.js";
|
|
9
11
|
import { syncFiles, syncFilesScoped } from "./sync.js";
|
|
12
|
+
/** Expand a leading `~` in a workspace path to the user's home directory. */
|
|
13
|
+
function expandHome(p) {
|
|
14
|
+
if (!p)
|
|
15
|
+
return p;
|
|
16
|
+
if (p === "~")
|
|
17
|
+
return homedir();
|
|
18
|
+
if (p.startsWith("~/"))
|
|
19
|
+
return join(homedir(), p.slice(2));
|
|
20
|
+
return p;
|
|
21
|
+
}
|
|
22
|
+
// Module-scope dedupe so a duplicate register() (e.g. plugin loaded twice via
|
|
23
|
+
// different module specifiers) doesn't run startup auto-sync twice for the
|
|
24
|
+
// same workspace. The in-closure autoSyncStarted flag inside register() can't
|
|
25
|
+
// catch this because each register() call gets its own closure.
|
|
26
|
+
const autoSyncedWorkspaces = new Set();
|
|
10
27
|
const memoryCogneePlugin = {
|
|
11
28
|
id: "cognee-openclaw",
|
|
12
29
|
name: "Memory (Cognee)",
|
|
@@ -14,6 +31,23 @@ const memoryCogneePlugin = {
|
|
|
14
31
|
kind: "memory",
|
|
15
32
|
register(api) {
|
|
16
33
|
const cfg = resolveConfig(api.pluginConfig);
|
|
34
|
+
// Auto-enable per-agent memory when the gateway hosts more than one agent,
|
|
35
|
+
// unless the plugin config set `perAgentMemory` explicitly. This keeps
|
|
36
|
+
// single-agent installs (the common case) on the legacy shared behavior so
|
|
37
|
+
// the upgrade is non-breaking; multi-agent gateways get per-agent isolation.
|
|
38
|
+
const perAgentExplicit = typeof api.pluginConfig?.perAgentMemory === "boolean";
|
|
39
|
+
if (!perAgentExplicit) {
|
|
40
|
+
try {
|
|
41
|
+
const agentList = api.runtime?.config?.loadConfig?.()?.agents?.list;
|
|
42
|
+
if (Array.isArray(agentList) && agentList.length > 1) {
|
|
43
|
+
cfg.perAgentMemory = true;
|
|
44
|
+
api.logger.info?.(`cognee-openclaw: per-agent memory auto-enabled (${agentList.length} agents configured)`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
api.logger.debug?.(`cognee-openclaw: could not read agents.list for perAgentMemory auto-enable: ${String(error)}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
17
51
|
const client = new CogneeHttpClient(cfg.baseUrl, cfg.apiKey, cfg.username, cfg.password, cfg.requestTimeoutMs, cfg.ingestionTimeoutMs, cfg.mode);
|
|
18
52
|
const multiScope = isMultiScopeEnabled(cfg);
|
|
19
53
|
api.registerMemoryFlushPlan?.(buildMemoryFlushPlan);
|
|
@@ -21,14 +55,46 @@ const memoryCogneePlugin = {
|
|
|
21
55
|
// Legacy single-scope state
|
|
22
56
|
let datasetId;
|
|
23
57
|
let syncIndex = { entries: {} };
|
|
24
|
-
// Multi-scope state
|
|
58
|
+
// Multi-scope state (company/user shared; agent scope lives in agentIndexes
|
|
59
|
+
// when perAgentMemory is on).
|
|
25
60
|
let scopedIndexes = {};
|
|
61
|
+
// Per-agent agent-scope state (perAgentMemory mode), keyed by normalized agentId.
|
|
62
|
+
let agentIndexes = {};
|
|
63
|
+
const perAgentMemory = multiScope && cfg.perAgentMemory;
|
|
64
|
+
// Serialize sync work per agent so concurrent turns of the SAME agent don't
|
|
65
|
+
// double-run. Keyed by normalized agentId.
|
|
66
|
+
const agentLocks = new Map();
|
|
67
|
+
function withAgentLock(agentId, fn) {
|
|
68
|
+
const prev = agentLocks.get(agentId) ?? Promise.resolve();
|
|
69
|
+
const next = prev.catch(() => { }).then(fn);
|
|
70
|
+
agentLocks.set(agentId, next.catch(() => { }));
|
|
71
|
+
return next;
|
|
72
|
+
}
|
|
73
|
+
// Global lock around the read-modify-write of agent-sync-indexes.json.
|
|
74
|
+
// DIFFERENT agents are NOT serialized by agentLocks, so without this their
|
|
75
|
+
// concurrent load→mutate→save would clobber each other's bucket (they share
|
|
76
|
+
// one file). This makes the reload+set+save atomic so distinct buckets merge.
|
|
77
|
+
let indexSaveChain = Promise.resolve();
|
|
78
|
+
function withIndexSaveLock(fn) {
|
|
79
|
+
const next = indexSaveChain.catch(() => { }).then(fn);
|
|
80
|
+
indexSaveChain = next.catch(() => { });
|
|
81
|
+
return next;
|
|
82
|
+
}
|
|
26
83
|
// Session state
|
|
27
84
|
let sessionId;
|
|
85
|
+
// Cached as a fallback for paths that may lack ctx.
|
|
86
|
+
let lastAgentId;
|
|
87
|
+
let lastWorkspaceDir;
|
|
88
|
+
// Per-agent workspace cache (normalized agentId -> workspaceDir), populated
|
|
89
|
+
// on agent_end. session_end's ctx carries agentId but NOT workspaceDir, so
|
|
90
|
+
// this lets the final sweep find the right agent's workspace without falling
|
|
91
|
+
// back to a single global (which mis-attributes when >1 agent is active).
|
|
92
|
+
const agentWorkspaces = new Map();
|
|
28
93
|
let resolvedWorkspaceDir;
|
|
29
94
|
let resolveServiceReady;
|
|
30
95
|
const serviceReady = new Promise((r) => { resolveServiceReady = r; });
|
|
31
|
-
//
|
|
96
|
+
// Hoisted so CLI processes can suppress the gateway's auto-sync timer.
|
|
97
|
+
let autoSyncStarted = false;
|
|
32
98
|
const stateReady = Promise.all([
|
|
33
99
|
loadDatasetState()
|
|
34
100
|
.then((state) => {
|
|
@@ -52,6 +118,18 @@ const memoryCogneePlugin = {
|
|
|
52
118
|
}
|
|
53
119
|
}
|
|
54
120
|
scopedIndexes = indexes;
|
|
121
|
+
})
|
|
122
|
+
.then(async () => {
|
|
123
|
+
if (!perAgentMemory)
|
|
124
|
+
return;
|
|
125
|
+
// Move any legacy shared `agent` scope entry into the per-agent map.
|
|
126
|
+
const migrated = await migrateAgentScopeToPerAgent(normalizeAgentId(undefined, cfg));
|
|
127
|
+
if (migrated) {
|
|
128
|
+
api.logger.info?.("cognee-openclaw: migrated shared agent scope index to per-agent");
|
|
129
|
+
// Reload shared indexes (migration removed the agent entry from them).
|
|
130
|
+
scopedIndexes = await loadScopedSyncIndexes();
|
|
131
|
+
}
|
|
132
|
+
agentIndexes = await loadAgentSyncIndexes();
|
|
55
133
|
})
|
|
56
134
|
.catch((error) => {
|
|
57
135
|
api.logger.warn?.(`cognee-openclaw: failed to load scoped sync indexes: ${String(error)}`);
|
|
@@ -67,6 +145,15 @@ const memoryCogneePlugin = {
|
|
|
67
145
|
api.logger.warn?.(`cognee-openclaw: failed to load sync index: ${String(error)}`);
|
|
68
146
|
}),
|
|
69
147
|
]);
|
|
148
|
+
// Resolve the locally-cached fallback dataset id for a scope. For the agent
|
|
149
|
+
// scope under perAgentMemory, that's the per-agent index; otherwise the
|
|
150
|
+
// shared scoped index.
|
|
151
|
+
function scopeFallbackDatasetId(scope, runtimeAgentId) {
|
|
152
|
+
if (scope === "agent" && perAgentMemory) {
|
|
153
|
+
return agentIndexes[normalizeAgentId(runtimeAgentId, cfg)]?.datasetId;
|
|
154
|
+
}
|
|
155
|
+
return scopedIndexes[scope]?.datasetId;
|
|
156
|
+
}
|
|
70
157
|
// Fix #8: Log when scopes have no dataset ID during recall
|
|
71
158
|
async function getRecallDatasetIds(runtimeAgentId) {
|
|
72
159
|
const state = await loadDatasetState();
|
|
@@ -75,7 +162,7 @@ const memoryCogneePlugin = {
|
|
|
75
162
|
if (multiScope) {
|
|
76
163
|
for (const scope of cfg.recallScopes) {
|
|
77
164
|
const dsName = datasetNameForScope(scope, cfg, runtimeAgentId);
|
|
78
|
-
const dsId = state[dsName] ??
|
|
165
|
+
const dsId = state[dsName] ?? scopeFallbackDatasetId(scope, runtimeAgentId);
|
|
79
166
|
if (dsId) {
|
|
80
167
|
ids.push(dsId);
|
|
81
168
|
}
|
|
@@ -90,8 +177,79 @@ const memoryCogneePlugin = {
|
|
|
90
177
|
}
|
|
91
178
|
return { ids, missingScopes };
|
|
92
179
|
}
|
|
93
|
-
//
|
|
94
|
-
|
|
180
|
+
// Sync ONE agent's `agent`-scope files from its own workspace into its own
|
|
181
|
+
// dataset + per-agent index. Serialized per agentId. Used by per-agent mode.
|
|
182
|
+
async function syncAgentScope(workspaceDir, rawAgentId, logger) {
|
|
183
|
+
await stateReady;
|
|
184
|
+
const agentId = normalizeAgentId(rawAgentId, cfg);
|
|
185
|
+
return withAgentLock(agentId, async () => {
|
|
186
|
+
const allFiles = await collectMemoryFiles(workspaceDir);
|
|
187
|
+
const agentFiles = allFiles.filter((f) => routeFileToScope(f.path, cfg.scopeRouting, cfg.defaultWriteScope) === "agent");
|
|
188
|
+
// Start from this agent's latest persisted bucket.
|
|
189
|
+
const idx = (await loadAgentSyncIndexes())[agentId] ?? { entries: {} };
|
|
190
|
+
const dsName = datasetNameForScope("agent", cfg, agentId);
|
|
191
|
+
// syncFiles mutates `idx` in place (entries/dataIds) but does not persist
|
|
192
|
+
// it (persistIndex=false); we own persistence below.
|
|
193
|
+
const result = await syncFiles(client, agentFiles, agentFiles, idx, cfg, logger, dsName, false);
|
|
194
|
+
// Atomic merge-save: reload the latest on-disk map (may include other
|
|
195
|
+
// agents' buckets written meanwhile), set just our bucket, save.
|
|
196
|
+
await withIndexSaveLock(async () => {
|
|
197
|
+
const latest = await loadAgentSyncIndexes();
|
|
198
|
+
latest[agentId] = idx;
|
|
199
|
+
await saveAgentSyncIndexes(latest);
|
|
200
|
+
agentIndexes = latest;
|
|
201
|
+
});
|
|
202
|
+
return result;
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
// Sync ONLY the shared scopes (company/user) from a workspace. Never run
|
|
206
|
+
// from a per-agent workspace (it lacks company/user files and would forget
|
|
207
|
+
// them); only from the default/gateway workspace at startup.
|
|
208
|
+
async function syncSharedScopes(workspaceDir, logger) {
|
|
209
|
+
await stateReady;
|
|
210
|
+
const files = await collectMemoryFiles(workspaceDir);
|
|
211
|
+
return syncFilesScoped(client, files, files, scopedIndexes, cfg, logger, undefined, ["company", "user"]);
|
|
212
|
+
}
|
|
213
|
+
// Seed every configured agent's files from its own workspace (startup/CLI).
|
|
214
|
+
async function seedAllAgents(defaultWorkspace, logger) {
|
|
215
|
+
const config = api.runtime?.config?.loadConfig?.();
|
|
216
|
+
const list = config?.agents?.list;
|
|
217
|
+
const defWs = expandHome(config?.agents?.defaults?.workspace) || defaultWorkspace;
|
|
218
|
+
const agents = Array.isArray(list) && list.length > 0
|
|
219
|
+
? list
|
|
220
|
+
: [{ id: cfg.agentId, workspace: defWs }];
|
|
221
|
+
for (const a of agents) {
|
|
222
|
+
const ws = expandHome(a.workspace) || defWs;
|
|
223
|
+
if (!ws)
|
|
224
|
+
continue;
|
|
225
|
+
try {
|
|
226
|
+
const r = await syncAgentScope(ws, a.id, logger);
|
|
227
|
+
logger.info?.(`cognee-openclaw: seeded agent "${normalizeAgentId(a.id, cfg)}": ${r.added} added, ${r.updated} updated, ${r.deleted} deleted, ${r.skipped} unchanged`);
|
|
228
|
+
}
|
|
229
|
+
catch (e) {
|
|
230
|
+
logger.warn?.(`cognee-openclaw: failed to seed agent "${a.id}": ${String(e)}`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
// Resolve an agent's workspace from OpenClaw config (agents.list[].workspace
|
|
235
|
+
// by agentId), with sensible fallbacks. Used by the per-agent file paths so
|
|
236
|
+
// startup seeding and the agent_end/session_end sweeps always read the SAME
|
|
237
|
+
// directory — otherwise a runtime ctx.workspaceDir that differs from the
|
|
238
|
+
// seed workspace makes the sweep see the seeded file as "missing" and forget
|
|
239
|
+
// it. Resolving from config (the single source of truth) avoids that.
|
|
240
|
+
function resolveAgentWorkspace(rawAgentId) {
|
|
241
|
+
const target = normalizeAgentId(rawAgentId, cfg);
|
|
242
|
+
try {
|
|
243
|
+
const config = api.runtime?.config?.loadConfig?.();
|
|
244
|
+
const list = config?.agents?.list;
|
|
245
|
+
const match = list?.find((a) => normalizeAgentId(a.id, cfg) === target);
|
|
246
|
+
return expandHome(match?.workspace) || expandHome(config?.agents?.defaults?.workspace) || resolvedWorkspaceDir;
|
|
247
|
+
}
|
|
248
|
+
catch {
|
|
249
|
+
return resolvedWorkspaceDir;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
async function runSync(workspaceDir, logger, runtimeAgentId) {
|
|
95
253
|
await stateReady;
|
|
96
254
|
const files = await collectMemoryFiles(workspaceDir);
|
|
97
255
|
if (files.length === 0) {
|
|
@@ -99,8 +257,14 @@ const memoryCogneePlugin = {
|
|
|
99
257
|
return { added: 0, updated: 0, skipped: 0, errors: 0, deleted: 0 };
|
|
100
258
|
}
|
|
101
259
|
logger.info?.(`cognee-openclaw: found ${files.length} memory file(s), syncing...`);
|
|
102
|
-
if (
|
|
103
|
-
|
|
260
|
+
if (perAgentMemory) {
|
|
261
|
+
// Per-agent mode: this path syncs only the shared scopes (company/user)
|
|
262
|
+
// from the given workspace; the `agent` scope is handled per agent via
|
|
263
|
+
// syncAgentScope/seedAllAgents (each from its own workspace).
|
|
264
|
+
return syncFilesScoped(client, files, files, scopedIndexes, cfg, logger, runtimeAgentId, ["company", "user"]);
|
|
265
|
+
}
|
|
266
|
+
else if (multiScope) {
|
|
267
|
+
return syncFilesScoped(client, files, files, scopedIndexes, cfg, logger, runtimeAgentId);
|
|
104
268
|
}
|
|
105
269
|
else {
|
|
106
270
|
const result = await syncFiles(client, files, files, syncIndex, cfg, logger);
|
|
@@ -113,10 +277,12 @@ const memoryCogneePlugin = {
|
|
|
113
277
|
datasetId = undefined;
|
|
114
278
|
syncIndex = { entries: {} };
|
|
115
279
|
scopedIndexes = {};
|
|
280
|
+
agentIndexes = {};
|
|
116
281
|
await Promise.all([
|
|
117
282
|
saveDatasetState({}),
|
|
118
283
|
saveSyncIndex({ entries: {} }),
|
|
119
284
|
saveScopedSyncIndexes({}),
|
|
285
|
+
saveAgentSyncIndexes({}),
|
|
120
286
|
]);
|
|
121
287
|
}
|
|
122
288
|
async function clearLocalStateForDataset(datasetName) {
|
|
@@ -135,6 +301,8 @@ const memoryCogneePlugin = {
|
|
|
135
301
|
if (multiScope) {
|
|
136
302
|
let changed = false;
|
|
137
303
|
for (const scope of MEMORY_SCOPES) {
|
|
304
|
+
if (scope === "agent" && perAgentMemory)
|
|
305
|
+
continue; // handled per-agent below
|
|
138
306
|
const expectedName = datasetNameForScope(scope, cfg);
|
|
139
307
|
const idx = scopedIndexes[scope];
|
|
140
308
|
const actualName = idx?.datasetName ?? expectedName;
|
|
@@ -147,6 +315,20 @@ const memoryCogneePlugin = {
|
|
|
147
315
|
await saveScopedSyncIndexes(scopedIndexes);
|
|
148
316
|
}
|
|
149
317
|
}
|
|
318
|
+
if (perAgentMemory) {
|
|
319
|
+
agentIndexes = await loadAgentSyncIndexes();
|
|
320
|
+
let agentChanged = false;
|
|
321
|
+
for (const [agentId, idx] of Object.entries(agentIndexes)) {
|
|
322
|
+
const expectedName = datasetNameForScope("agent", cfg, agentId);
|
|
323
|
+
const actualName = idx.datasetName ?? expectedName;
|
|
324
|
+
if (actualName === datasetName || expectedName === datasetName) {
|
|
325
|
+
delete agentIndexes[agentId];
|
|
326
|
+
agentChanged = true;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
if (agentChanged)
|
|
330
|
+
await saveAgentSyncIndexes(agentIndexes);
|
|
331
|
+
}
|
|
150
332
|
}
|
|
151
333
|
// ------------------------------------------------------------------
|
|
152
334
|
// CLI commands
|
|
@@ -154,10 +336,28 @@ const memoryCogneePlugin = {
|
|
|
154
336
|
api.registerCli((ctx) => {
|
|
155
337
|
const cognee = ctx.program.command("cognee").description("Cognee memory management");
|
|
156
338
|
const cliWorkspaceDir = ctx.workspaceDir || process.cwd();
|
|
339
|
+
autoSyncStarted = true;
|
|
157
340
|
cognee
|
|
158
341
|
.command("index")
|
|
159
342
|
.description("Sync memory files to Cognee (add new, update changed, skip unchanged)")
|
|
160
|
-
.
|
|
343
|
+
.option("--agent <id>", "Per-agent mode: sync only this agent's workspace")
|
|
344
|
+
.action(async (opts) => {
|
|
345
|
+
if (perAgentMemory) {
|
|
346
|
+
if (opts.agent) {
|
|
347
|
+
// Resolve this agent's workspace from config; fall back to cwd.
|
|
348
|
+
const config = api.runtime?.config?.loadConfig?.();
|
|
349
|
+
const list = config?.agents?.list;
|
|
350
|
+
const match = list?.find((a) => normalizeAgentId(a.id, cfg) === normalizeAgentId(opts.agent, cfg));
|
|
351
|
+
const ws = expandHome(match?.workspace) || cliWorkspaceDir;
|
|
352
|
+
const r = await syncAgentScope(ws, opts.agent, ctx.logger);
|
|
353
|
+
console.log(`Sync complete [agent=${normalizeAgentId(opts.agent, cfg)}]: ${r.added} added, ${r.updated} updated, ${r.deleted} deleted, ${r.skipped} unchanged, ${r.errors} errors`);
|
|
354
|
+
process.exit(0);
|
|
355
|
+
}
|
|
356
|
+
const shared = await runSync(cliWorkspaceDir, ctx.logger); // company/user
|
|
357
|
+
await seedAllAgents(cliWorkspaceDir, ctx.logger); // each agent's own files
|
|
358
|
+
console.log(`Shared sync complete: ${shared.added} added, ${shared.updated} updated, ${shared.deleted} deleted, ${shared.skipped} unchanged. Per-agent files seeded (see log).`);
|
|
359
|
+
process.exit(0);
|
|
360
|
+
}
|
|
161
361
|
const result = await runSync(cliWorkspaceDir, ctx.logger);
|
|
162
362
|
const summary = `Sync complete: ${result.added} added, ${result.updated} updated, ${result.deleted} deleted, ${result.skipped} unchanged, ${result.errors} errors`;
|
|
163
363
|
ctx.logger.info?.(summary);
|
|
@@ -172,7 +372,10 @@ const memoryCogneePlugin = {
|
|
|
172
372
|
const files = await collectMemoryFiles(cliWorkspaceDir);
|
|
173
373
|
if (multiScope) {
|
|
174
374
|
const state = await loadDatasetState();
|
|
175
|
-
|
|
375
|
+
// Shared scopes (company/user). In per-agent mode the agent scope is
|
|
376
|
+
// reported per-agent below instead of here.
|
|
377
|
+
const scopesToShow = perAgentMemory ? ["company", "user"] : MEMORY_SCOPES;
|
|
378
|
+
for (const scope of scopesToShow) {
|
|
176
379
|
const dsName = datasetNameForScope(scope, cfg);
|
|
177
380
|
const scopeIndex = scopedIndexes[scope] ?? { entries: {} };
|
|
178
381
|
const entryCount = Object.keys(scopeIndex.entries).length;
|
|
@@ -192,6 +395,23 @@ const memoryCogneePlugin = {
|
|
|
192
395
|
console.log(` New (unindexed): ${newCount}`);
|
|
193
396
|
console.log(` Changed (dirty): ${dirty}`);
|
|
194
397
|
}
|
|
398
|
+
if (perAgentMemory) {
|
|
399
|
+
agentIndexes = await loadAgentSyncIndexes();
|
|
400
|
+
const config = api.runtime?.config?.loadConfig?.();
|
|
401
|
+
const list = config?.agents?.list;
|
|
402
|
+
const agentKeys = new Set(Object.keys(agentIndexes));
|
|
403
|
+
for (const a of list ?? [])
|
|
404
|
+
agentKeys.add(normalizeAgentId(a.id, cfg));
|
|
405
|
+
if (agentKeys.size === 0)
|
|
406
|
+
agentKeys.add(normalizeAgentId(undefined, cfg));
|
|
407
|
+
for (const agentId of agentKeys) {
|
|
408
|
+
const idx = agentIndexes[agentId] ?? { entries: {} };
|
|
409
|
+
const dsName = datasetNameForScope("agent", cfg, agentId);
|
|
410
|
+
console.log(`\n[AGENT:${agentId}] Dataset: ${dsName}`);
|
|
411
|
+
console.log(` Dataset ID: ${state[dsName] ?? idx.datasetId ?? "(not set)"}`);
|
|
412
|
+
console.log(` Indexed files: ${Object.keys(idx.entries).length}`);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
195
415
|
}
|
|
196
416
|
else {
|
|
197
417
|
const entryCount = Object.keys(syncIndex.entries).length;
|
|
@@ -366,12 +586,31 @@ const memoryCogneePlugin = {
|
|
|
366
586
|
console.log(`Forget failed: ${result.error ?? "unknown error"}`);
|
|
367
587
|
process.exit(1);
|
|
368
588
|
});
|
|
589
|
+
cognee
|
|
590
|
+
.command("improve")
|
|
591
|
+
.description("Bridge session-cache QAs (and any feedback) into the permanent graph. With --session-id, scopes to that session; otherwise improves the dataset in general.")
|
|
592
|
+
.option("--session-id <id>", "Session to bridge")
|
|
593
|
+
.option("--dataset <name>", "Dataset name (default: configured datasetName)")
|
|
594
|
+
.action(async (opts) => {
|
|
595
|
+
const dsName = opts.dataset ?? (multiScope ? datasetNameForScope("agent", cfg) : cfg.datasetName);
|
|
596
|
+
try {
|
|
597
|
+
const result = await client.improve({
|
|
598
|
+
datasetName: dsName,
|
|
599
|
+
...(opts.sessionId ? { sessionIds: [opts.sessionId] } : {}),
|
|
600
|
+
});
|
|
601
|
+
console.log(`Improve dispatched for dataset "${dsName}"${opts.sessionId ? ` (sessionId=${opts.sessionId})` : ""} — status=${result.status ?? "?"}`);
|
|
602
|
+
process.exit(0);
|
|
603
|
+
}
|
|
604
|
+
catch (error) {
|
|
605
|
+
console.log(`Improve failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
606
|
+
process.exit(1);
|
|
607
|
+
}
|
|
608
|
+
});
|
|
369
609
|
}, { commands: ["cognee"] });
|
|
370
610
|
// ------------------------------------------------------------------
|
|
371
611
|
// Auto-sync on startup (with health check)
|
|
372
612
|
// ------------------------------------------------------------------
|
|
373
613
|
if (cfg.autoIndex) {
|
|
374
|
-
let autoSyncStarted = false;
|
|
375
614
|
const runAutoSync = async (workspaceDir) => {
|
|
376
615
|
if (autoSyncStarted)
|
|
377
616
|
return;
|
|
@@ -379,6 +618,12 @@ const memoryCogneePlugin = {
|
|
|
379
618
|
resolvedWorkspaceDir = workspaceDir || process.cwd();
|
|
380
619
|
resolveServiceReady?.();
|
|
381
620
|
const logger = api.logger;
|
|
621
|
+
// Dedupe across duplicate register() calls in the same process.
|
|
622
|
+
if (autoSyncedWorkspaces.has(resolvedWorkspaceDir)) {
|
|
623
|
+
logger.debug?.(`cognee-openclaw: auto-sync already ran for ${resolvedWorkspaceDir} in this process, skipping`);
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
autoSyncedWorkspaces.add(resolvedWorkspaceDir);
|
|
382
627
|
try {
|
|
383
628
|
await client.health();
|
|
384
629
|
}
|
|
@@ -386,13 +631,14 @@ const memoryCogneePlugin = {
|
|
|
386
631
|
logger.warn?.(`cognee-openclaw: Cognee API unreachable at ${cfg.baseUrl} — auto-sync disabled for this session. Error: ${String(error)}`);
|
|
387
632
|
return;
|
|
388
633
|
}
|
|
389
|
-
if (cfg.enableSessions && !sessionId) {
|
|
390
|
-
sessionId = `openclaw-${randomUUID()}`;
|
|
391
|
-
logger.info?.(`cognee-openclaw: session ${sessionId}`);
|
|
392
|
-
}
|
|
393
634
|
try {
|
|
394
635
|
const result = await runSync(resolvedWorkspaceDir, logger);
|
|
395
636
|
logger.info?.(`cognee-openclaw: auto-sync complete: ${result.added} added, ${result.updated} updated, ${result.deleted} deleted, ${result.skipped} unchanged`);
|
|
637
|
+
// Per-agent mode: seed each configured agent's files from its OWN
|
|
638
|
+
// workspace (runSync above only handled the shared company/user scopes).
|
|
639
|
+
if (perAgentMemory) {
|
|
640
|
+
await seedAllAgents(resolvedWorkspaceDir, logger);
|
|
641
|
+
}
|
|
396
642
|
}
|
|
397
643
|
catch (error) {
|
|
398
644
|
logger.warn?.(`cognee-openclaw: auto-sync failed: ${String(error)}`);
|
|
@@ -427,6 +673,9 @@ const memoryCogneePlugin = {
|
|
|
427
673
|
if (cfg.autoRecall) {
|
|
428
674
|
api.on("before_prompt_build", async (event, ctx) => {
|
|
429
675
|
await stateReady;
|
|
676
|
+
// session_start isn't fired in every openclaw flow; sync from ctx on every hook.
|
|
677
|
+
if (cfg.enableSessions && ctx.sessionId)
|
|
678
|
+
sessionId = ctx.sessionId;
|
|
430
679
|
if (!event.prompt || event.prompt.length < 5) {
|
|
431
680
|
api.logger.debug?.("cognee-openclaw: skipping recall (prompt too short)");
|
|
432
681
|
return;
|
|
@@ -446,7 +695,7 @@ const memoryCogneePlugin = {
|
|
|
446
695
|
const state = await loadDatasetState();
|
|
447
696
|
const searchPromises = cfg.recallScopes.map(async (scope) => {
|
|
448
697
|
const dsName = datasetNameForScope(scope, cfg, ctx.agentId);
|
|
449
|
-
const dsId = state[dsName] ??
|
|
698
|
+
const dsId = state[dsName] ?? scopeFallbackDatasetId(scope, ctx.agentId);
|
|
450
699
|
if (!dsId)
|
|
451
700
|
return null;
|
|
452
701
|
const results = await client.recall({
|
|
@@ -527,50 +776,31 @@ const memoryCogneePlugin = {
|
|
|
527
776
|
if (!event.success)
|
|
528
777
|
return;
|
|
529
778
|
await Promise.all([stateReady, serviceReady]);
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
if (cfg.enableSessions &&
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
if (
|
|
547
|
-
|
|
548
|
-
await client.fetchAPI("/api/v1/sessions/persist", {
|
|
549
|
-
method: "POST",
|
|
550
|
-
headers: { "Content-Type": "application/json" },
|
|
551
|
-
body: JSON.stringify({
|
|
552
|
-
session_ids: [sessionId],
|
|
553
|
-
dataset_ids: targetDatasetIds,
|
|
554
|
-
}),
|
|
555
|
-
}).catch(() => {
|
|
556
|
-
// Session persistence endpoint may not exist on all Cognee versions.
|
|
557
|
-
// Fail silently — this is an enhancement, not critical path.
|
|
558
|
-
api.logger.debug?.("cognee-openclaw: session persistence endpoint not available");
|
|
559
|
-
});
|
|
779
|
+
lastAgentId = ctx.agentId;
|
|
780
|
+
lastWorkspaceDir = ctx.workspaceDir || resolvedWorkspaceDir;
|
|
781
|
+
if (cfg.enableSessions && ctx.sessionId)
|
|
782
|
+
sessionId = ctx.sessionId;
|
|
783
|
+
const workspaceDir = ctx.workspaceDir || resolvedWorkspaceDir;
|
|
784
|
+
// Remember this agent's workspace so session_end can sweep the right one.
|
|
785
|
+
if (workspaceDir)
|
|
786
|
+
agentWorkspaces.set(normalizeAgentId(ctx.agentId, cfg), workspaceDir);
|
|
787
|
+
try {
|
|
788
|
+
if (perAgentMemory) {
|
|
789
|
+
// Sync ONLY this agent's agent-scope files from its OWN workspace
|
|
790
|
+
// into its own dataset + per-agent index. Resolve the workspace from
|
|
791
|
+
// config (not ctx.workspaceDir) so it matches the startup seed and a
|
|
792
|
+
// mismatched runtime cwd can't make the sweep "forget" the seed file.
|
|
793
|
+
const agentWs = resolveAgentWorkspace(ctx.agentId) || workspaceDir;
|
|
794
|
+
const result = await syncAgentScope(agentWs, ctx.agentId, api.logger);
|
|
795
|
+
if (result.added || result.updated || result.deleted) {
|
|
796
|
+
api.logger.info?.(`cognee-openclaw: post-agent sync [agent=${normalizeAgentId(ctx.agentId, cfg)}]: ${result.added} added, ${result.updated} updated, ${result.deleted} deleted`);
|
|
560
797
|
}
|
|
561
798
|
}
|
|
562
|
-
|
|
563
|
-
api.logger.warn?.(`cognee-openclaw: session persistence failed: ${String(error)}`);
|
|
564
|
-
}
|
|
565
|
-
}
|
|
566
|
-
// Sync file changes
|
|
567
|
-
try {
|
|
568
|
-
if (multiScope) {
|
|
799
|
+
else if (multiScope) {
|
|
569
800
|
try {
|
|
570
|
-
|
|
571
|
-
scopedIndexes = freshIndexes;
|
|
801
|
+
scopedIndexes = await loadScopedSyncIndexes();
|
|
572
802
|
}
|
|
573
|
-
catch { /*
|
|
803
|
+
catch { /* keep cached scopedIndexes */ }
|
|
574
804
|
const files = await collectMemoryFiles(workspaceDir);
|
|
575
805
|
let hasChanges = false;
|
|
576
806
|
for (const file of files) {
|
|
@@ -610,7 +840,7 @@ const memoryCogneePlugin = {
|
|
|
610
840
|
if (freshIndex.datasetName)
|
|
611
841
|
syncIndex.datasetName = freshIndex.datasetName;
|
|
612
842
|
}
|
|
613
|
-
catch { /*
|
|
843
|
+
catch { /* keep cached syncIndex */ }
|
|
614
844
|
const files = await collectMemoryFiles(workspaceDir);
|
|
615
845
|
const changedFiles = files.filter((f) => {
|
|
616
846
|
const existing = syncIndex.entries[f.path];
|
|
@@ -631,20 +861,59 @@ const memoryCogneePlugin = {
|
|
|
631
861
|
api.logger.warn?.(`cognee-openclaw: post-agent sync failed: ${String(error)}`);
|
|
632
862
|
}
|
|
633
863
|
});
|
|
864
|
+
api.on("session_start", async (event) => {
|
|
865
|
+
if (cfg.enableSessions)
|
|
866
|
+
sessionId = event.sessionId;
|
|
867
|
+
});
|
|
634
868
|
// Final sweep when the openclaw session closes. Catches memory file
|
|
635
|
-
// edits that happened outside an agent_end
|
|
636
|
-
//
|
|
637
|
-
|
|
869
|
+
// edits that happened outside an agent_end.
|
|
870
|
+
//
|
|
871
|
+
// CRITICAL: resolve the agent + session from THIS event's ctx, not the
|
|
872
|
+
// global lastAgentId. With >1 agent active, lastAgentId is whichever agent
|
|
873
|
+
// ran most recently, so using it would bridge one agent's session into
|
|
874
|
+
// another agent's dataset.
|
|
875
|
+
// PluginHookSessionContext carries agentId + sessionId, so prefer those.
|
|
876
|
+
api.on("session_end", async (event, ctx) => {
|
|
638
877
|
await Promise.all([stateReady, serviceReady]);
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
878
|
+
const endAgentId = ctx?.agentId ?? lastAgentId;
|
|
879
|
+
const endSessionId = ctx?.sessionId ?? event.sessionId;
|
|
880
|
+
if (!ctx?.agentId) {
|
|
881
|
+
api.logger.debug?.(`cognee-openclaw: session_end without ctx.agentId; falling back to lastAgentId="${endAgentId ?? "(none)"}"`);
|
|
882
|
+
}
|
|
883
|
+
// Per-agent: resolve from config (matches the startup seed). Otherwise
|
|
884
|
+
// fall back to the cached workspace for this agent.
|
|
885
|
+
const sweepWorkspace = perAgentMemory
|
|
886
|
+
? (resolveAgentWorkspace(endAgentId) || lastWorkspaceDir || resolvedWorkspaceDir)
|
|
887
|
+
: (agentWorkspaces.get(normalizeAgentId(endAgentId, cfg)) || lastWorkspaceDir || resolvedWorkspaceDir);
|
|
888
|
+
if (sweepWorkspace) {
|
|
889
|
+
try {
|
|
890
|
+
if (perAgentMemory) {
|
|
891
|
+
// Final sweep for THIS session's agent, from its own workspace.
|
|
892
|
+
const result = await syncAgentScope(sweepWorkspace, endAgentId, api.logger);
|
|
893
|
+
api.logger.info?.(`cognee-openclaw: session-end sync [agent=${normalizeAgentId(endAgentId, cfg)}]: ${result.added} added, ${result.updated} updated, ${result.deleted} deleted`);
|
|
894
|
+
}
|
|
895
|
+
else {
|
|
896
|
+
const result = await runSync(sweepWorkspace, api.logger, endAgentId);
|
|
897
|
+
api.logger.info?.(`cognee-openclaw: session-end sync: ${result.added} added, ${result.updated} updated, ${result.deleted} deleted`);
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
catch (error) {
|
|
901
|
+
api.logger.warn?.(`cognee-openclaw: session-end sync failed: ${String(error)}`);
|
|
902
|
+
}
|
|
644
903
|
}
|
|
645
|
-
|
|
646
|
-
|
|
904
|
+
// Bridge session-cache QAs (including any auto-captured feedback) into
|
|
905
|
+
// THIS session's agent dataset — keyed by endAgentId + endSessionId.
|
|
906
|
+
if (cfg.improveOnSessionEnd && endSessionId) {
|
|
907
|
+
const dsName = multiScope ? datasetNameForScope("agent", cfg, endAgentId) : cfg.datasetName;
|
|
908
|
+
try {
|
|
909
|
+
const result = await client.improve({ datasetName: dsName, sessionIds: [endSessionId] });
|
|
910
|
+
api.logger.info?.(`cognee-openclaw: session-end improve dispatched for session ${endSessionId} -> dataset "${dsName}" (status=${result.status ?? "?"})`);
|
|
911
|
+
}
|
|
912
|
+
catch (error) {
|
|
913
|
+
api.logger.warn?.(`cognee-openclaw: session-end improve failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
914
|
+
}
|
|
647
915
|
}
|
|
916
|
+
sessionId = undefined;
|
|
648
917
|
});
|
|
649
918
|
}
|
|
650
919
|
},
|