@cognee/cognee-openclaw 2026.5.21 → 2026.7.9

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,416 @@
1
+ import { existsSync } from "node:fs";
2
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { homedir } from "node:os";
5
+ import { runPluginCommandWithTimeout } from "openclaw/plugin-sdk/sandbox";
6
+ const COGNEE_PLUGIN_BASE = join(homedir(), ".cognee-plugin");
7
+ const API_KEY_CACHE_PATH = join(COGNEE_PLUGIN_BASE, "api_key.json");
8
+ // Combined install-and-boot script written to ~/.cognee-plugin/ensure_and_boot.py on first use.
9
+ // Handles: (1) creating the venv + installing cognee if absent, (2) booting uvicorn.
10
+ // Self-daemonizes so runPluginCommandWithTimeout returns in < 1 s regardless of install time.
11
+ // Inlining avoids import.meta.url path resolution issues in ts-jest.
12
+ const ENSURE_SCRIPT_PATH = join(COGNEE_PLUGIN_BASE, "ensure_and_boot.py");
13
+ const ENSURE_SCRIPT_CONTENT = [
14
+ "import subprocess, sys, os, json, time",
15
+ "",
16
+ "BASE = os.path.join(os.path.expanduser('~'), '.cognee-plugin')",
17
+ "VENV_DIR = os.path.join(BASE, 'venv')",
18
+ "_bin = 'Scripts' if os.name == 'nt' else 'bin'",
19
+ "_ext = '.exe' if os.name == 'nt' else ''",
20
+ "VENV_PYTHON = os.path.join(VENV_DIR, _bin, 'python' + _ext)",
21
+ "PIP = os.path.join(VENV_DIR, _bin, 'pip' + _ext)",
22
+ "UV_DIR = os.path.join(BASE, 'uv')",
23
+ "UV_BIN = os.path.join(UV_DIR, 'uv' + _ext)",
24
+ "READY_MARKER = os.path.join(BASE, '.venv-ready.json')",
25
+ "INSTALL_LOCK = os.path.join(BASE, 'venv-install.lock')",
26
+ "COGNEE_VERSION = '1.2.2.dev3'",
27
+ "",
28
+ "# Self-daemonize so the caller returns immediately.",
29
+ "if '--daemon' not in sys.argv:",
30
+ " port_arg = sys.argv[1] if len(sys.argv) > 1 else '8011'",
31
+ " p = subprocess.Popen(",
32
+ " [sys.executable, __file__, port_arg, '--daemon'],",
33
+ " start_new_session=True, close_fds=True,",
34
+ " stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,",
35
+ " )",
36
+ " print(p.pid)",
37
+ " sys.exit(0)",
38
+ "",
39
+ "PORT = next((a for a in sys.argv[1:] if a != '--daemon'), '8011')",
40
+ "",
41
+ "def pid_alive(pid):",
42
+ " if pid <= 1: return False",
43
+ " try: os.kill(pid, 0); return True",
44
+ " except ProcessLookupError: return False",
45
+ " except PermissionError: return True",
46
+ " except Exception: return False",
47
+ "",
48
+ "def acquire_lock(path, stale_secs=720):",
49
+ " os.makedirs(os.path.dirname(path) or '.', exist_ok=True)",
50
+ " now = time.time()",
51
+ " if os.path.exists(path):",
52
+ " try:",
53
+ " d = json.loads(open(path).read())",
54
+ " pid = int(d.get('pid', 0) or 0)",
55
+ " created = float(d.get('created_at', 0) or 0)",
56
+ " if pid_alive(pid) and now - created < stale_secs: return False",
57
+ " os.unlink(path)",
58
+ " except Exception: pass",
59
+ " try:",
60
+ " fd = os.open(path, os.O_CREAT | os.O_EXCL | os.O_WRONLY)",
61
+ " with os.fdopen(fd, 'w') as f: json.dump({'pid': os.getpid(), 'created_at': now}, f)",
62
+ " return True",
63
+ " except FileExistsError: return False",
64
+ "",
65
+ "def release_lock(path):",
66
+ " try: os.unlink(path)",
67
+ " except Exception: pass",
68
+ "",
69
+ "def install_cognee():",
70
+ " os.makedirs(BASE, exist_ok=True)",
71
+ " if not acquire_lock(INSTALL_LOCK):",
72
+ " deadline = time.monotonic() + 720",
73
+ " while time.monotonic() < deadline:",
74
+ " if os.path.exists(VENV_PYTHON): return True",
75
+ " time.sleep(0.5)",
76
+ " return os.path.exists(VENV_PYTHON)",
77
+ " try:",
78
+ " import shutil",
79
+ " uv = UV_BIN if os.path.exists(UV_BIN) else (shutil.which('uv') or '')",
80
+ " if uv:",
81
+ " uv_env = os.environ.copy()",
82
+ " if not os.path.exists(VENV_PYTHON):",
83
+ " subprocess.run(",
84
+ " [uv, 'venv', VENV_DIR, '--python', '3.12'],",
85
+ " env=uv_env, check=True, capture_output=True, timeout=300,",
86
+ " )",
87
+ " subprocess.run(",
88
+ " [uv, 'pip', 'install', '--upgrade', '--python', VENV_PYTHON,",
89
+ " f'cognee=={COGNEE_VERSION}'],",
90
+ " env=uv_env, check=True, capture_output=True, timeout=600,",
91
+ " )",
92
+ " elif not os.path.exists(VENV_PYTHON):",
93
+ " subprocess.run(",
94
+ " [sys.executable, '-m', 'venv', VENV_DIR],",
95
+ " check=True, capture_output=True, timeout=120,",
96
+ " )",
97
+ " subprocess.run(",
98
+ " [PIP, 'install', '--upgrade', f'cognee=={COGNEE_VERSION}'],",
99
+ " check=True, capture_output=True, timeout=600,",
100
+ " )",
101
+ " tmp = READY_MARKER + '.tmp'",
102
+ " with open(tmp, 'w') as f:",
103
+ " json.dump({'cognee_version': COGNEE_VERSION, 'python': VENV_PYTHON,",
104
+ " 'updated_at': time.time()}, f)",
105
+ " os.replace(tmp, READY_MARKER)",
106
+ " return True",
107
+ " except Exception: return False",
108
+ " finally: release_lock(INSTALL_LOCK)",
109
+ "",
110
+ "def boot_server():",
111
+ " if not os.path.exists(VENV_PYTHON): return",
112
+ " home = os.path.expanduser('~')",
113
+ " env = dict(os.environ)",
114
+ " env['COGNEE_AGENT_MODE'] = 'true'",
115
+ " env['AUTO_FEEDBACK'] = 'true'",
116
+ " env['CACHING'] = 'true'",
117
+ " env['SYSTEM_ROOT_DIRECTORY'] = os.path.join(home, '.cognee', 'system')",
118
+ " env['DATA_ROOT_DIRECTORY'] = os.path.join(home, '.cognee', 'data')",
119
+ " env['CACHE_ROOT_DIRECTORY'] = os.path.join(home, '.cognee', 'cache')",
120
+ " subprocess.Popen(",
121
+ " [VENV_PYTHON, '-m', 'uvicorn', 'cognee.api.client:app', '--port', PORT],",
122
+ " env=env, start_new_session=True, close_fds=True,",
123
+ " stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,",
124
+ " )",
125
+ "",
126
+ "def needs_install():",
127
+ " if not os.path.exists(VENV_PYTHON): return True",
128
+ " if os.path.exists(READY_MARKER):",
129
+ " try:",
130
+ " d = json.loads(open(READY_MARKER).read())",
131
+ " if d.get('cognee_version') == COGNEE_VERSION: return False",
132
+ " except Exception: pass",
133
+ " return True",
134
+ "",
135
+ "if needs_install():",
136
+ " install_cognee()",
137
+ "boot_server()",
138
+ ].join("\n");
139
+ // System Python candidates for running ensure_and_boot.py on a cold machine
140
+ // (before the plugin venv exists). Only stdlib is needed so any Python 3 works.
141
+ const SYSTEM_PYTHON_CANDIDATES = [
142
+ "/usr/bin/python3", // macOS Xcode CLT + most Linux
143
+ "/usr/local/bin/python3", // some Linux, older Homebrew
144
+ "/opt/homebrew/bin/python3", // Homebrew on Apple Silicon
145
+ ];
146
+ function findSystemPython() {
147
+ for (const p of SYSTEM_PYTHON_CANDIDATES) {
148
+ if (existsSync(p))
149
+ return p;
150
+ }
151
+ return "python3"; // PATH fallback
152
+ }
153
+ async function saveApiKeyCache(baseUrl, key) {
154
+ try {
155
+ await mkdir(COGNEE_PLUGIN_BASE, { recursive: true });
156
+ await writeFile(API_KEY_CACHE_PATH, JSON.stringify({ base_url: baseUrl, api_key: key, updated_at: new Date().toISOString() }), "utf-8");
157
+ }
158
+ catch { /* best-effort */ }
159
+ }
160
+ /**
161
+ * Resolve a permanent Cognee API key for this deployment, using the same
162
+ * strategy as the claude-code and codex integrations:
163
+ * 1. COGNEE_API_KEY env
164
+ * 2. Cached key in ~/.cognee-plugin/api_key.json
165
+ * 3. Existing key returned by GET /api/v1/auth/api-keys
166
+ * 4. Mint a new one via POST /api/v1/auth/api-keys and cache it
167
+ *
168
+ * The client's ensureAuth() has already run before this is called, so
169
+ * the HTTP calls go out authenticated. Returns "" if every path fails
170
+ * (e.g. older Cognee that doesn't expose the api-keys endpoints).
171
+ */
172
+ export async function resolveOrMintApiKey(client, logger) {
173
+ const envKey = (process.env["COGNEE_API_KEY"] ?? "").trim();
174
+ if (envKey)
175
+ return envKey;
176
+ try {
177
+ const cache = JSON.parse(await readFile(API_KEY_CACHE_PATH, "utf-8"));
178
+ const key = typeof cache.api_key === "string" ? cache.api_key.trim() : "";
179
+ if (key)
180
+ return key;
181
+ }
182
+ catch { /* cache miss */ }
183
+ try {
184
+ const keys = await client.listApiKeys();
185
+ const key = Array.isArray(keys) ? (keys[0]?.key ?? "").trim() : "";
186
+ if (key) {
187
+ await saveApiKeyCache(client.baseUrl, key);
188
+ return key;
189
+ }
190
+ }
191
+ catch (e) {
192
+ logger.warn?.(`cognee-openclaw: list API keys failed: ${String(e)}`);
193
+ }
194
+ try {
195
+ const { key } = await client.createApiKey("openclaw-bootstrap");
196
+ const trimmed = (key ?? "").trim();
197
+ if (trimmed) {
198
+ await saveApiKeyCache(client.baseUrl, trimmed);
199
+ logger.info?.("cognee-openclaw: minted new API key");
200
+ return trimmed;
201
+ }
202
+ }
203
+ catch (e) {
204
+ logger.warn?.(`cognee-openclaw: create API key failed: ${String(e)}`);
205
+ }
206
+ return "";
207
+ }
208
+ export function isLocalUrl(url) {
209
+ try {
210
+ const parsed = new URL(url);
211
+ return ["localhost", "127.0.0.1", "0.0.0.0", "::1"].includes(parsed.hostname);
212
+ }
213
+ catch {
214
+ return false;
215
+ }
216
+ }
217
+ /**
218
+ * Write ensure_and_boot.py and run it via the plugin sandbox.
219
+ * The script self-daemonizes, so this returns in < 1 s regardless of whether
220
+ * cognee needs to be installed from scratch.
221
+ */
222
+ export async function bootServerIfNeeded(baseUrl, logger) {
223
+ if (!isLocalUrl(baseUrl))
224
+ return;
225
+ let port = 8011;
226
+ try {
227
+ const parsed = new URL(baseUrl);
228
+ if (parsed.port) {
229
+ const p = parseInt(parsed.port, 10);
230
+ if (Number.isFinite(p) && p > 0)
231
+ port = p;
232
+ }
233
+ }
234
+ catch { /* keep default */ }
235
+ const python = findSystemPython();
236
+ await mkdir(COGNEE_PLUGIN_BASE, { recursive: true });
237
+ await writeFile(ENSURE_SCRIPT_PATH, ENSURE_SCRIPT_CONTENT, "utf-8");
238
+ const result = await runPluginCommandWithTimeout({
239
+ argv: [python, ENSURE_SCRIPT_PATH, String(port)],
240
+ timeoutMs: 5_000,
241
+ });
242
+ if (result.code !== 0) {
243
+ logger.warn?.(`cognee-openclaw: boot script exited ${result.code}: ${result.stderr}`);
244
+ }
245
+ }
246
+ // ---------------------------------------------------------------------------
247
+ // Exit-watcher: detached Python process that deregisters an agent session
248
+ // when the OpenClaw gateway process dies (by any means: Ctrl+C, SIGKILL,
249
+ // crash, clean exit). One watcher per registration, all watching the same
250
+ // gateway PID. State lives in ~/.openclaw/cognee/ separate from the shared
251
+ // Cognee venv infrastructure in ~/.cognee-plugin/.
252
+ // ---------------------------------------------------------------------------
253
+ const OPENCLAW_STATE_DIR = join(homedir(), ".openclaw", "cognee");
254
+ const EXIT_WATCHER_SCRIPT_PATH = join(OPENCLAW_STATE_DIR, "exit-watcher.py");
255
+ const EXIT_WATCHERS_DIR = join(OPENCLAW_STATE_DIR, "exit-watchers");
256
+ const EXIT_WATCHER_CONTENT = [
257
+ "import json, os, sys, time, urllib.request",
258
+ "",
259
+ "POLL = 2.0",
260
+ "LOG_PATH = os.path.join(os.path.expanduser('~'), '.openclaw', 'cognee', 'exit-watcher.log')",
261
+ "",
262
+ "def log(msg):",
263
+ " try:",
264
+ " with open(LOG_PATH, 'a') as f:",
265
+ " f.write(time.strftime('%Y-%m-%dT%H:%M:%S') + ' ' + str(msg) + '\\n')",
266
+ " except Exception: pass",
267
+ "",
268
+ "def pid_alive(pid):",
269
+ " if pid <= 1: return False",
270
+ " try: os.kill(pid, 0); return True",
271
+ " except ProcessLookupError: return False",
272
+ " except PermissionError: return True",
273
+ " except Exception: return False",
274
+ "",
275
+ "def owns_pidfile(path):",
276
+ " try: return int(open(path).read().strip()) == os.getpid()",
277
+ " except Exception: return False",
278
+ "",
279
+ "def post_json(base_url, path, payload, api_key, timeout):",
280
+ " url = base_url.rstrip('/') + path",
281
+ " data = json.dumps(payload).encode()",
282
+ " req = urllib.request.Request(url, data=data, method='POST',",
283
+ " headers={'Content-Type': 'application/json'})",
284
+ " # X-Api-Key only: an API key is not a JWT, and a bogus Bearer can be",
285
+ " # rejected by JWT-validating servers before the key is considered.",
286
+ " if api_key:",
287
+ " req.add_header('X-Api-Key', api_key)",
288
+ " with urllib.request.urlopen(req, timeout=timeout) as r:",
289
+ " return r.read()",
290
+ "",
291
+ "def bridge_session(base_url, dataset_name, session_id, api_key):",
292
+ " # Bridge the server-side session cache into the permanent graph BEFORE",
293
+ " # unregistering: unregister may drop activeAgents to 0, and in",
294
+ " # COGNEE_AGENT_MODE the server then shuts down mid-pipeline.",
295
+ " # run_in_background=false so the call returns only when the bridge is done.",
296
+ " try:",
297
+ " body = post_json(base_url, '/api/v1/improve',",
298
+ " {'dataset_name': dataset_name, 'session_ids': [session_id],",
299
+ " 'run_in_background': False}, api_key, 120)",
300
+ " log(f'improve ok session={session_id} body={body.decode()[:200]}')",
301
+ " except Exception as e:",
302
+ " log(f'improve error session={session_id} type={type(e).__name__} err={e}')",
303
+ "",
304
+ "def deregister(base_url, name, api_key):",
305
+ " try:",
306
+ " body = post_json(base_url, '/api/v1/agents/unregister',",
307
+ " {'agent_session_name': name}, api_key, 10)",
308
+ " log(f'deregister ok name={name} body={body.decode()[:200]}')",
309
+ " except Exception as e:",
310
+ " log(f'deregister error name={name} type={type(e).__name__} err={e}')",
311
+ "",
312
+ "if '--daemon' not in sys.argv:",
313
+ " import subprocess",
314
+ " subprocess.Popen([sys.executable, __file__, sys.argv[1], '--daemon'],",
315
+ " start_new_session=True, close_fds=True,",
316
+ " stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)",
317
+ " sys.exit(0)",
318
+ "",
319
+ "try: a = json.loads(next(x for x in sys.argv[1:] if x != '--daemon'))",
320
+ "except Exception as e: log(f'arg parse error: {e}'); sys.exit(0)",
321
+ "",
322
+ "gw_pid = int(a.get('gateway_pid', 0))",
323
+ "name = str(a.get('agent_session_name', ''))",
324
+ "base_url = str(a.get('base_url', 'http://localhost:8011'))",
325
+ "api_key = str(a.get('api_key', '') or '')",
326
+ "pidfile = str(a.get('pidfile', ''))",
327
+ "dataset_name = str(a.get('dataset_name', '') or '')",
328
+ "cognee_session_id = str(a.get('cognee_session_id', '') or '')",
329
+ "",
330
+ "if not gw_pid or not name or not pidfile:",
331
+ " log(f'bad args gw_pid={gw_pid} name={name} pidfile={pidfile}'); sys.exit(0)",
332
+ "",
333
+ "try:",
334
+ " os.makedirs(os.path.dirname(pidfile) or '.', exist_ok=True)",
335
+ " open(pidfile, 'w').write(str(os.getpid()))",
336
+ "except Exception as e:",
337
+ " log(f'pidfile write error: {e}'); sys.exit(0)",
338
+ "",
339
+ "log(f'started pid={os.getpid()} gw_pid={gw_pid} name={name}')",
340
+ "",
341
+ "while owns_pidfile(pidfile) and pid_alive(gw_pid):",
342
+ " time.sleep(POLL)",
343
+ "",
344
+ "if owns_pidfile(pidfile):",
345
+ " log(f'gateway dead, deregistering name={name}')",
346
+ " if dataset_name and cognee_session_id:",
347
+ " bridge_session(base_url, dataset_name, cognee_session_id, api_key)",
348
+ " deregister(base_url, name, api_key)",
349
+ " try: os.unlink(pidfile)",
350
+ " except Exception: pass",
351
+ "else:",
352
+ " log(f'pidfile gone (clean exit) name={name}')",
353
+ ].join("\n");
354
+ /** Returns the pidfile path for an exit-watcher tracking the given session name. */
355
+ export function exitWatcherPidfilePath(agentSessionName) {
356
+ const safe = agentSessionName.replace(/[^a-zA-Z0-9_-]/g, "-").slice(0, 200);
357
+ return join(EXIT_WATCHERS_DIR, `${safe}.pid`);
358
+ }
359
+ /**
360
+ * Spawn a detached exit-watcher that fires when the gateway process
361
+ * (gatewayPid) exits for any reason. If datasetName + cogneeSessionId are
362
+ * given, it first bridges that session's cache into the graph via
363
+ * /api/v1/improve, then calls /api/v1/agents/unregister for agentSessionName.
364
+ * Returns immediately — the actual watcher runs as a separate OS process.
365
+ * Signal clean deregistration by deleting the pidfile (exitWatcherPidfilePath);
366
+ * the watcher detects the missing pidfile and self-exits without making an HTTP call.
367
+ */
368
+ export async function spawnExitWatcher(params) {
369
+ try {
370
+ await mkdir(EXIT_WATCHERS_DIR, { recursive: true });
371
+ await writeFile(EXIT_WATCHER_SCRIPT_PATH, EXIT_WATCHER_CONTENT, "utf-8");
372
+ const args = JSON.stringify({
373
+ gateway_pid: params.gatewayPid,
374
+ agent_session_name: params.agentSessionName,
375
+ base_url: params.baseUrl,
376
+ api_key: params.apiKey ?? "",
377
+ pidfile: params.pidfilePath,
378
+ dataset_name: params.datasetName ?? "",
379
+ cognee_session_id: params.cogneeSessionId ?? "",
380
+ });
381
+ const python = findSystemPython();
382
+ const result = await runPluginCommandWithTimeout({
383
+ argv: [python, EXIT_WATCHER_SCRIPT_PATH, args],
384
+ timeoutMs: 5_000,
385
+ });
386
+ if (result.code !== 0) {
387
+ params.logger.warn?.(`cognee-openclaw: exit-watcher spawn failed (exit ${result.code}): ${result.stderr}`);
388
+ }
389
+ }
390
+ catch (e) {
391
+ params.logger.warn?.(`cognee-openclaw: exit-watcher spawn error: ${String(e)}`);
392
+ }
393
+ }
394
+ /**
395
+ * Poll the Cognee /health endpoint until it returns 200 or the timeout elapses.
396
+ * Returns a Promise so callers can chain deferred work without blocking.
397
+ * Rejects with an Error on timeout.
398
+ */
399
+ export async function waitForServerHealth(baseUrl, timeoutMs = 600_000) {
400
+ const healthUrl = `${baseUrl.replace(/\/$/, "")}/health`;
401
+ const deadline = Date.now() + timeoutMs;
402
+ while (Date.now() < deadline) {
403
+ await new Promise((r) => setTimeout(r, 2_000));
404
+ try {
405
+ const ctrl = new AbortController();
406
+ const t = setTimeout(() => ctrl.abort(), 2_000);
407
+ const resp = await fetch(healthUrl, { signal: ctrl.signal });
408
+ clearTimeout(t);
409
+ if (resp.ok)
410
+ return;
411
+ }
412
+ catch { /* not ready yet */ }
413
+ }
414
+ throw new Error(`Cognee server not healthy after ${Math.round(timeoutMs / 1000)}s`);
415
+ }
416
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/server.ts"],"names":[],"mappings":"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"}
@@ -3,13 +3,25 @@ import type { CogneePluginConfig, MemoryFile, MemoryScope, ScopedSyncIndexes, Sy
3
3
  export declare function syncFiles(client: CogneeHttpClient, changedFiles: MemoryFile[], fullFiles: MemoryFile[], syncIndex: SyncIndex, cfg: Required<CogneePluginConfig>, logger: {
4
4
  info?: (msg: string) => void;
5
5
  warn?: (msg: string) => void;
6
- }, overrideDatasetName?: string): Promise<SyncResult & {
6
+ }, overrideDatasetName?: string,
7
+ /**
8
+ * Persist the mutated syncIndex to the legacy single-scope file. Callers that
9
+ * own their own persistence (scoped sync, per-agent sync) pass false so they
10
+ * don't clobber sync-index.json. Defaults to true for legacy single-scope use.
11
+ */
12
+ persistIndex?: boolean): Promise<SyncResult & {
7
13
  datasetId?: string;
8
14
  }>;
9
15
  export declare function syncFilesScoped(client: CogneeHttpClient, changedFiles: MemoryFile[], fullFiles: MemoryFile[], scopedIndexes: ScopedSyncIndexes, cfg: Required<CogneePluginConfig>, logger: {
10
16
  info?: (msg: string) => void;
11
17
  warn?: (msg: string) => void;
12
- }, runtimeAgentId?: string): Promise<SyncResult & {
18
+ }, runtimeAgentId?: string,
19
+ /**
20
+ * Restrict processing to these scopes. Used to sync only the shared scopes
21
+ * (`company`/`user`) from the default workspace when per-agent memory owns
22
+ * the `agent` scope. When omitted, all scopes are processed (legacy behavior).
23
+ */
24
+ onlyScopes?: MemoryScope[]): Promise<SyncResult & {
13
25
  datasetIds: Record<MemoryScope, string | undefined>;
14
26
  }>;
15
27
  export declare let COGNIFY_POLL_INTERVAL_MS: number;
package/dist/src/sync.js CHANGED
@@ -10,10 +10,37 @@ import { datasetNameForScope, routeFileToScope } from "./scope.js";
10
10
  // counterpart, and the existing 1.0.3 update endpoint already triggers a
11
11
  // follow-up cognify internally, so the fan-out is the same).
12
12
  // ---------------------------------------------------------------------------
13
- export async function syncFiles(client, changedFiles, fullFiles, syncIndex, cfg, logger, overrideDatasetName) {
13
+ export async function syncFiles(client, changedFiles, fullFiles, syncIndex, cfg, logger, overrideDatasetName,
14
+ /**
15
+ * Persist the mutated syncIndex to the legacy single-scope file. Callers that
16
+ * own their own persistence (scoped sync, per-agent sync) pass false so they
17
+ * don't clobber sync-index.json. Defaults to true for legacy single-scope use.
18
+ */
19
+ persistIndex = true) {
14
20
  const result = { added: 0, updated: 0, skipped: 0, errors: 0, deleted: 0 };
15
21
  const dsName = overrideDatasetName || cfg.datasetName;
16
22
  let datasetId = syncIndex.datasetId;
23
+ if (datasetId && syncIndex.datasetName && syncIndex.datasetName !== dsName) {
24
+ logger.info?.(`cognee-openclaw: cached datasetId belongs to "${syncIndex.datasetName}" but current dataset is "${dsName}" — ignoring stale datasetId`);
25
+ datasetId = undefined;
26
+ syncIndex.datasetId = undefined;
27
+ }
28
+ function isDatasetAccessError(message) {
29
+ const msg = message.toLowerCase();
30
+ return (msg.includes("(401)") ||
31
+ msg.includes("(403)") ||
32
+ msg.includes("(404)") ||
33
+ msg.includes("unauthorizeddataaccesserror") ||
34
+ msg.includes("not accessible") ||
35
+ msg.includes("permission denied"));
36
+ }
37
+ function isUpdateFallbackError(message) {
38
+ const msg = message.toLowerCase();
39
+ return (msg.includes("404") ||
40
+ msg.includes("409") ||
41
+ msg.includes("not found") ||
42
+ isDatasetAccessError(message));
43
+ }
17
44
  // Partition changed files into "needs add" vs "needs update".
18
45
  // We process updates per-file (PATCH /update) and batch all adds into a
19
46
  // single /remember call at the end of the loop.
@@ -47,7 +74,11 @@ export async function syncFiles(client, changedFiles, fullFiles, syncIndex, cfg,
47
74
  }
48
75
  catch (updateError) {
49
76
  const errorMsg = updateError instanceof Error ? updateError.message : String(updateError);
50
- if (errorMsg.includes("404") || errorMsg.includes("409") || errorMsg.includes("not found")) {
77
+ if (isUpdateFallbackError(errorMsg)) {
78
+ if (isDatasetAccessError(errorMsg)) {
79
+ datasetId = undefined;
80
+ syncIndex.datasetId = undefined;
81
+ }
51
82
  logger.info?.(`cognee-openclaw: update failed for ${file.path}, falling back to remember`);
52
83
  // fall through to add path
53
84
  }
@@ -61,11 +92,11 @@ export async function syncFiles(client, changedFiles, fullFiles, syncIndex, cfg,
61
92
  toAdd.push(file);
62
93
  }
63
94
  if (toAdd.length > 0) {
64
- try {
95
+ const rememberBatch = async (resolvedDatasetId) => {
65
96
  const rememberResponse = await client.remember({
66
97
  files: toAdd.map((f) => ({ filePath: f.path, data: wrapWithMetadata(f) })),
67
98
  datasetName: dsName,
68
- datasetId,
99
+ datasetId: resolvedDatasetId,
69
100
  });
70
101
  if (rememberResponse.datasetId && rememberResponse.datasetId !== datasetId) {
71
102
  datasetId = rememberResponse.datasetId;
@@ -85,13 +116,32 @@ export async function syncFiles(client, changedFiles, fullFiles, syncIndex, cfg,
85
116
  }
86
117
  syncIndex.datasetId = datasetId;
87
118
  syncIndex.datasetName = dsName;
119
+ };
120
+ try {
121
+ await rememberBatch(datasetId);
88
122
  }
89
123
  catch (error) {
90
- // One failed batch fails every queued file surface them all so the
91
- // caller's error count reflects the actual workload, not just one entry.
92
- for (const file of toAdd) {
93
- result.errors++;
94
- logger.warn?.(`cognee-openclaw: failed to sync ${file.path}: ${error instanceof Error ? error.message : String(error)}`);
124
+ const errorMsg = error instanceof Error ? error.message : String(error);
125
+ if (isDatasetAccessError(errorMsg)) {
126
+ datasetId = undefined;
127
+ syncIndex.datasetId = undefined;
128
+ try {
129
+ await rememberBatch(undefined);
130
+ }
131
+ catch (retryError) {
132
+ for (const file of toAdd) {
133
+ result.errors++;
134
+ logger.warn?.(`cognee-openclaw: failed to sync ${file.path}: ${retryError instanceof Error ? retryError.message : String(retryError)}`);
135
+ }
136
+ }
137
+ }
138
+ else {
139
+ // One failed batch fails every queued file — surface them all so the
140
+ // caller's error count reflects the actual workload, not just one entry.
141
+ for (const file of toAdd) {
142
+ result.errors++;
143
+ logger.warn?.(`cognee-openclaw: failed to sync ${file.path}: ${error instanceof Error ? error.message : String(error)}`);
144
+ }
95
145
  }
96
146
  }
97
147
  }
@@ -124,13 +174,20 @@ export async function syncFiles(client, changedFiles, fullFiles, syncIndex, cfg,
124
174
  }
125
175
  }
126
176
  }
127
- await saveSyncIndex(syncIndex);
177
+ if (persistIndex)
178
+ await saveSyncIndex(syncIndex);
128
179
  return { ...result, datasetId };
129
180
  }
130
181
  // ---------------------------------------------------------------------------
131
182
  // Multi-scope sync
132
183
  // ---------------------------------------------------------------------------
133
- export async function syncFilesScoped(client, changedFiles, fullFiles, scopedIndexes, cfg, logger, runtimeAgentId) {
184
+ export async function syncFilesScoped(client, changedFiles, fullFiles, scopedIndexes, cfg, logger, runtimeAgentId,
185
+ /**
186
+ * Restrict processing to these scopes. Used to sync only the shared scopes
187
+ * (`company`/`user`) from the default workspace when per-agent memory owns
188
+ * the `agent` scope. When omitted, all scopes are processed (legacy behavior).
189
+ */
190
+ onlyScopes) {
134
191
  const totalResult = { added: 0, updated: 0, skipped: 0, errors: 0, deleted: 0 };
135
192
  const datasetIds = { company: undefined, user: undefined, agent: undefined };
136
193
  // Group changed files by scope
@@ -150,11 +207,14 @@ export async function syncFilesScoped(client, changedFiles, fullFiles, scopedInd
150
207
  fullByScope.set(scope, list);
151
208
  }
152
209
  // Determine which scopes need processing
210
+ const scopeFilter = onlyScopes ? new Set(onlyScopes) : null;
153
211
  const allScopes = new Set([
154
212
  ...changedByScope.keys(),
155
213
  ...Object.keys(scopedIndexes),
156
214
  ]);
157
215
  for (const scope of allScopes) {
216
+ if (scopeFilter && !scopeFilter.has(scope))
217
+ continue;
158
218
  const dsName = datasetNameForScope(scope, cfg, runtimeAgentId);
159
219
  const scopeChanged = changedByScope.get(scope) ?? [];
160
220
  const scopeFull = fullByScope.get(scope) ?? [];
@@ -167,7 +227,7 @@ export async function syncFilesScoped(client, changedFiles, fullFiles, scopedInd
167
227
  if (scopeChanged.length === 0 && !hasDeletedFiles)
168
228
  continue;
169
229
  logger.info?.(`cognee-openclaw: [${scope}] syncing ${scopeChanged.length} changed file(s) to dataset "${dsName}"${hasDeletedFiles ? " + deletions" : ""}`);
170
- const result = await syncFiles(client, scopeChanged, scopeFull, scopeIndex, cfg, logger, dsName);
230
+ const result = await syncFiles(client, scopeChanged, scopeFull, scopeIndex, cfg, logger, dsName, false);
171
231
  totalResult.added += result.added;
172
232
  totalResult.updated += result.updated;
173
233
  totalResult.skipped += result.skipped;