@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/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(`${this.endpoint}/mcp`, {
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(`${this.endpoint}/mcp`, {
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(`${this.endpoint}/mcp`, {
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(`${this.endpoint}/mcp`, {
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
- return { episode_uuid: trackingUuid };
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
  // ============================================================================