@contextableai/openclaw-memory-graphiti 0.1.2 → 0.2.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.
- package/README.md +54 -0
- package/authorization.ts +27 -38
- package/bin/graphiti-mem.ts +142 -0
- package/cli.ts +525 -0
- package/config.ts +13 -1
- package/docker/docker-compose.yml +3 -2
- package/graphiti.ts +80 -7
- package/index.ts +154 -361
- package/openclaw.plugin.json +15 -1
- package/package.json +11 -6
- package/scripts/dev-start.sh +4 -0
- package/search.ts +34 -18
- package/spicedb.ts +190 -9
package/graphiti.ts
CHANGED
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
* session ID tracking, and SSE response parsing.
|
|
7
7
|
*
|
|
8
8
|
* Wraps core tools: add_memory, search_nodes, search_memory_facts,
|
|
9
|
-
* get_episodes, delete_episode, get_status
|
|
9
|
+
* get_episodes, delete_episode, get_status, get_entity_edge,
|
|
10
|
+
* delete_entity_edge, clear_graph.
|
|
10
11
|
*/
|
|
11
12
|
|
|
12
13
|
import { randomUUID } from "node:crypto";
|
|
@@ -49,6 +50,8 @@ export type GraphitiFact = {
|
|
|
49
50
|
|
|
50
51
|
export type AddEpisodeResult = {
|
|
51
52
|
episode_uuid: string;
|
|
53
|
+
/** Resolves to the real server-side UUID once Graphiti finishes processing. */
|
|
54
|
+
resolvedUuid: Promise<string>;
|
|
52
55
|
};
|
|
53
56
|
|
|
54
57
|
type JsonRpcRequest = {
|
|
@@ -74,8 +77,17 @@ export class GraphitiClient {
|
|
|
74
77
|
private sessionId: string | null = null;
|
|
75
78
|
private initPromise: Promise<void> | null = null;
|
|
76
79
|
|
|
80
|
+
/** Polling interval (ms) for UUID resolution after addEpisode. */
|
|
81
|
+
uuidPollIntervalMs = 3000;
|
|
82
|
+
/** Max polling attempts for UUID resolution (total wait = interval * attempts). */
|
|
83
|
+
uuidPollMaxAttempts = 30;
|
|
84
|
+
|
|
77
85
|
constructor(private readonly endpoint: string) {}
|
|
78
86
|
|
|
87
|
+
private get mcpUrl(): string {
|
|
88
|
+
return `${this.endpoint}/mcp`;
|
|
89
|
+
}
|
|
90
|
+
|
|
79
91
|
// --------------------------------------------------------------------------
|
|
80
92
|
// MCP Session Lifecycle
|
|
81
93
|
// --------------------------------------------------------------------------
|
|
@@ -100,7 +112,7 @@ export class GraphitiClient {
|
|
|
100
112
|
},
|
|
101
113
|
};
|
|
102
114
|
|
|
103
|
-
const response = await fetch(
|
|
115
|
+
const response = await fetch(this.mcpUrl, {
|
|
104
116
|
method: "POST",
|
|
105
117
|
headers: {
|
|
106
118
|
"Content-Type": "application/json",
|
|
@@ -128,7 +140,7 @@ export class GraphitiClient {
|
|
|
128
140
|
if (this.sessionId) {
|
|
129
141
|
headers["mcp-session-id"] = this.sessionId;
|
|
130
142
|
}
|
|
131
|
-
await fetch(
|
|
143
|
+
await fetch(this.mcpUrl, {
|
|
132
144
|
method: "POST",
|
|
133
145
|
headers,
|
|
134
146
|
body: JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" }),
|
|
@@ -138,7 +150,7 @@ export class GraphitiClient {
|
|
|
138
150
|
async close(): Promise<void> {
|
|
139
151
|
if (this.sessionId) {
|
|
140
152
|
try {
|
|
141
|
-
await fetch(
|
|
153
|
+
await fetch(this.mcpUrl, {
|
|
142
154
|
method: "DELETE",
|
|
143
155
|
headers: { "mcp-session-id": this.sessionId },
|
|
144
156
|
});
|
|
@@ -172,7 +184,7 @@ export class GraphitiClient {
|
|
|
172
184
|
headers["mcp-session-id"] = this.sessionId;
|
|
173
185
|
}
|
|
174
186
|
|
|
175
|
-
const response = await fetch(
|
|
187
|
+
const response = await fetch(this.mcpUrl, {
|
|
176
188
|
method: "POST",
|
|
177
189
|
headers,
|
|
178
190
|
body: JSON.stringify(request),
|
|
@@ -255,7 +267,7 @@ export class GraphitiClient {
|
|
|
255
267
|
// Note: The Graphiti MCP server's uuid parameter is for re-processing
|
|
256
268
|
// existing episodes, NOT for setting the UUID of new ones. We generate
|
|
257
269
|
// a client-side tracking UUID instead (it won't match the server-side UUID).
|
|
258
|
-
const trackingUuid = randomUUID()
|
|
270
|
+
const trackingUuid = `tmp-${randomUUID()}`;
|
|
259
271
|
|
|
260
272
|
// Prepend custom extraction instructions to episode_body since the MCP
|
|
261
273
|
// server doesn't support custom_extraction_instructions as a parameter.
|
|
@@ -281,7 +293,39 @@ export class GraphitiClient {
|
|
|
281
293
|
}
|
|
282
294
|
|
|
283
295
|
await this.callTool("add_memory", args);
|
|
284
|
-
|
|
296
|
+
|
|
297
|
+
// Graphiti's add_memory queues the episode for async LLM processing and
|
|
298
|
+
// returns only a "queued" message — no UUID. We poll getEpisodes in the
|
|
299
|
+
// background to discover the real server-side UUID by name match.
|
|
300
|
+
let resolvedUuid: Promise<string>;
|
|
301
|
+
if (params.group_id) {
|
|
302
|
+
resolvedUuid = this.resolveEpisodeUuid(params.name, params.group_id);
|
|
303
|
+
resolvedUuid.catch(() => {}); // Prevent unhandled rejection if caller ignores
|
|
304
|
+
} else {
|
|
305
|
+
resolvedUuid = Promise.resolve(trackingUuid);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return { episode_uuid: trackingUuid, resolvedUuid };
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
private async resolveEpisodeUuid(name: string, groupId: string): Promise<string> {
|
|
312
|
+
const totalTimeoutSec = (this.uuidPollMaxAttempts * this.uuidPollIntervalMs) / 1000;
|
|
313
|
+
for (let i = 0; i < this.uuidPollMaxAttempts; i++) {
|
|
314
|
+
await new Promise((r) => setTimeout(r, this.uuidPollIntervalMs));
|
|
315
|
+
try {
|
|
316
|
+
const episodes = await this.getEpisodes(groupId, 50);
|
|
317
|
+
const match = episodes.find((ep) => ep.name === name);
|
|
318
|
+
if (match) return match.uuid;
|
|
319
|
+
} catch (err) {
|
|
320
|
+
// Log transient errors to aid debugging
|
|
321
|
+
console.warn(
|
|
322
|
+
`[graphiti] UUID poll ${i + 1}/${this.uuidPollMaxAttempts} for "${name}" in "${groupId}" failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
throw new Error(
|
|
327
|
+
`Failed to resolve episode UUID for "${name}" in group "${groupId}" after ${totalTimeoutSec}s — episode not yet visible in get_episodes (Graphiti LLM processing may still be running)`,
|
|
328
|
+
);
|
|
285
329
|
}
|
|
286
330
|
|
|
287
331
|
async getEpisodes(groupId: string, lastN: number): Promise<GraphitiEpisode[]> {
|
|
@@ -305,6 +349,7 @@ export class GraphitiClient {
|
|
|
305
349
|
group_id?: string;
|
|
306
350
|
group_ids?: string[];
|
|
307
351
|
limit?: number;
|
|
352
|
+
entity_types?: string[];
|
|
308
353
|
}): Promise<GraphitiNode[]> {
|
|
309
354
|
const args: Record<string, unknown> = {
|
|
310
355
|
query: params.query,
|
|
@@ -316,6 +361,9 @@ export class GraphitiClient {
|
|
|
316
361
|
if (params.limit !== undefined) {
|
|
317
362
|
args.max_nodes = params.limit;
|
|
318
363
|
}
|
|
364
|
+
if (params.entity_types && params.entity_types.length > 0) {
|
|
365
|
+
args.entity_types = params.entity_types;
|
|
366
|
+
}
|
|
319
367
|
|
|
320
368
|
const result = await this.callTool("search_nodes", args);
|
|
321
369
|
return parseToolResult<GraphitiNode[]>(result, "nodes");
|
|
@@ -326,6 +374,7 @@ export class GraphitiClient {
|
|
|
326
374
|
group_id?: string;
|
|
327
375
|
group_ids?: string[];
|
|
328
376
|
limit?: number;
|
|
377
|
+
center_node_uuid?: string;
|
|
329
378
|
/** @deprecated No longer supported by the Graphiti MCP server */
|
|
330
379
|
created_after?: string;
|
|
331
380
|
}): Promise<GraphitiFact[]> {
|
|
@@ -339,10 +388,34 @@ export class GraphitiClient {
|
|
|
339
388
|
if (params.limit !== undefined) {
|
|
340
389
|
args.max_facts = params.limit;
|
|
341
390
|
}
|
|
391
|
+
if (params.center_node_uuid) {
|
|
392
|
+
args.center_node_uuid = params.center_node_uuid;
|
|
393
|
+
}
|
|
342
394
|
|
|
343
395
|
const result = await this.callTool("search_memory_facts", args);
|
|
344
396
|
return parseToolResult<GraphitiFact[]>(result, "facts");
|
|
345
397
|
}
|
|
398
|
+
|
|
399
|
+
// --------------------------------------------------------------------------
|
|
400
|
+
// Entity Edge Operations
|
|
401
|
+
// --------------------------------------------------------------------------
|
|
402
|
+
|
|
403
|
+
async getEntityEdge(uuid: string): Promise<GraphitiFact> {
|
|
404
|
+
const result = await this.callTool("get_entity_edge", { uuid });
|
|
405
|
+
return parseJsonResult<GraphitiFact>(result);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
async deleteEntityEdge(uuid: string): Promise<void> {
|
|
409
|
+
await this.callTool("delete_entity_edge", { uuid });
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
async clearGraph(groupIds?: string[]): Promise<void> {
|
|
413
|
+
const args: Record<string, unknown> = {};
|
|
414
|
+
if (groupIds && groupIds.length > 0) {
|
|
415
|
+
args.group_ids = groupIds;
|
|
416
|
+
}
|
|
417
|
+
await this.callTool("clear_graph", args);
|
|
418
|
+
}
|
|
346
419
|
}
|
|
347
420
|
|
|
348
421
|
// ============================================================================
|