@contextableai/openclaw-memory-graphiti 0.2.5 → 0.2.7

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/config.ts CHANGED
@@ -55,10 +55,11 @@ function assertAllowedKeys(value: Record<string, unknown>, allowed: string[], la
55
55
 
56
56
  export const graphitiMemoryConfigSchema = {
57
57
  parse(value: unknown): GraphitiMemoryConfig {
58
- if (!value || typeof value !== "object" || Array.isArray(value)) {
59
- throw new Error("openclaw-memory-graphiti config required");
58
+ if (Array.isArray(value)) {
59
+ throw new Error("openclaw-memory-graphiti config must be an object, not an array");
60
60
  }
61
- const cfg = value as Record<string, unknown>;
61
+ // Accept undefined/null (installer writes no config key) — treat as empty {}
62
+ const cfg = (value && typeof value === "object" ? value : {}) as Record<string, unknown>;
62
63
  assertAllowedKeys(
63
64
  cfg,
64
65
  [
package/graphiti.ts CHANGED
@@ -80,7 +80,7 @@ export class GraphitiClient {
80
80
  /** Polling interval (ms) for UUID resolution after addEpisode. */
81
81
  uuidPollIntervalMs = 3000;
82
82
  /** Max polling attempts for UUID resolution (total wait = interval * attempts). */
83
- uuidPollMaxAttempts = 30;
83
+ uuidPollMaxAttempts = 80;
84
84
 
85
85
  constructor(private readonly endpoint: string) {}
86
86
 
package/index.ts CHANGED
@@ -12,6 +12,7 @@
12
12
 
13
13
  import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
14
14
  import { Type } from "@sinclair/typebox";
15
+ import { randomUUID } from "node:crypto";
15
16
  import { readFileSync } from "node:fs";
16
17
  import { join, dirname } from "node:path";
17
18
  import { fileURLToPath } from "node:url";
@@ -117,9 +118,9 @@ const memoryGraphitiPlugin = {
117
118
  name: "memory_recall",
118
119
  label: "Memory Recall",
119
120
  description:
120
- "Search through memories using the knowledge graph. Returns entities and facts the current user is authorized to see. Supports session, long-term, or combined scope.",
121
+ "Search through memories using the knowledge graph. Returns entities and facts the current user is authorized to see. Supports session, long-term, or combined scope. REQUIRES a search query.",
121
122
  parameters: Type.Object({
122
- query: Type.String({ description: "Search query" }),
123
+ query: Type.String({ description: "REQUIRED: Search query for semantic matching" }),
123
124
  limit: Type.Optional(Type.Number({ description: "Max results (default: 10)" })),
124
125
  scope: Type.Optional(
125
126
  Type.Union(
@@ -249,7 +250,7 @@ const memoryGraphitiPlugin = {
249
250
  Type.Array(Type.String(), { description: "Person/agent IDs involved in this memory" }),
250
251
  ),
251
252
  group_id: Type.Optional(
252
- Type.String({ description: "Target group for this memory (default: configured group)" }),
253
+ Type.String({ description: "Target group ID (optional, uses your default group if omitted)" }),
253
254
  ),
254
255
  longTerm: Type.Optional(
255
256
  Type.Boolean({ description: "Store as long-term memory (default: true). Set to false for session-scoped." }),
@@ -270,10 +271,22 @@ const memoryGraphitiPlugin = {
270
271
  longTerm?: boolean;
271
272
  };
272
273
 
274
+ // Sanitize group_id: SpiceDB requires alphanumeric + _|\\-=+ only (no spaces!)
275
+ const sanitizeGroupId = (id?: string): string | undefined => {
276
+ if (!id) return undefined;
277
+ const trimmed = id.trim();
278
+ // If it looks like a description rather than an ID, ignore it
279
+ if (trimmed.includes(' ') || trimmed.toLowerCase().includes('configured')) {
280
+ return undefined;
281
+ }
282
+ return trimmed;
283
+ };
284
+
273
285
  // Resolve target group: explicit > longTerm flag > default
274
286
  let targetGroupId: string;
275
- if (group_id) {
276
- targetGroupId = group_id;
287
+ const sanitizedGroupId = sanitizeGroupId(group_id);
288
+ if (sanitizedGroupId) {
289
+ targetGroupId = sanitizedGroupId;
277
290
  } else if (!longTerm && currentSessionId) {
278
291
  targetGroupId = sessionGroupId(currentSessionId);
279
292
  } else {
@@ -312,8 +325,10 @@ const memoryGraphitiPlugin = {
312
325
  }
313
326
 
314
327
  // 1. Add episode to Graphiti
328
+ // Generate unique episode name to avoid collisions
329
+ const episodeName = `memory_${randomUUID()}`;
315
330
  const result = await graphiti.addEpisode({
316
- name: `memory_${Date.now()}`,
331
+ name: episodeName,
317
332
  episode_body: content,
318
333
  source_description,
319
334
  group_id: targetGroupId,
@@ -682,8 +697,10 @@ const memoryGraphitiPlugin = {
682
697
  }
683
698
  }
684
699
 
700
+ // Generate unique episode name to avoid collisions
701
+ const episodeName = `auto_capture_${randomUUID()}`;
685
702
  const result = await graphiti.addEpisode({
686
- name: `auto_capture_${Date.now()}`,
703
+ name: episodeName,
687
704
  episode_body: episodeBody,
688
705
  source_description: "auto-captured conversation",
689
706
  group_id: targetGroupId,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contextableai/openclaw-memory-graphiti",
3
- "version": "0.2.5",
3
+ "version": "0.2.7",
4
4
  "description": "OpenClaw two-layer memory plugin: SpiceDB authorization + Graphiti knowledge graph",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -5,15 +5,25 @@
5
5
  # Services run in the background; logs go to .dev/logs/.
6
6
  # PID files are written to .dev/pids/.
7
7
  #
8
- # Environment variables (all optional):
9
- # OPENAI_API_KEY Required by Graphiti for entity extraction
8
+ # Environment variables:
9
+ # OPENAI_API_KEY Required by Graphiti for entity extraction (unless using local models)
10
10
  # SPICEDB_TOKEN Pre-shared key (default: dev_token)
11
11
  # SPICEDB_PORT gRPC port (default: 50051)
12
12
  # SPICEDB_DATASTORE "memory" (default) or "postgres"
13
13
  # SPICEDB_DB_URI Postgres connection URI (when SPICEDB_DATASTORE=postgres)
14
14
  # FALKORDB_PORT Redis port (default: 6379)
15
15
  # GRAPHITI_PORT HTTP port (default: 8000)
16
+ # GRAPHITI_PATH Path to Graphiti git clone (default: ../graphiti)
16
17
  # EPISODE_ID_PREFIX Prefix for Graphiti episode UUIDs (default: epi-)
18
+ #
19
+ # Optional: Use local models instead of OpenAI
20
+ # LLM_PROVIDER "openai_generic" (enables custom LLM endpoint)
21
+ # LLM_MODEL Model name (e.g., "nvidia/Qwen3-Next-80B-A3B-Instruct-NVFP4")
22
+ # LLM_API_URL Custom LLM endpoint (e.g., "http://dgx-spark:8000/v1")
23
+ # EMBEDDER_MODEL Embedding model (e.g., "nomic-embed-text:v1.5")
24
+ # EMBEDDER_DIMENSIONS Embedding dimensions (e.g., 768 for nomic, 1536 for OpenAI)
25
+ # EMBEDDER_API_URL Custom embedder endpoint (e.g., "http://minisforum:11434/v1")
26
+ # RERANKER_PROVIDER Reranker provider: "openai", "gemini", or "bge" (default: bge)
17
27
  # -------------------------------------------------------------------
18
28
  set -euo pipefail
19
29
 
@@ -35,6 +45,17 @@ SPICEDB_DATASTORE="${SPICEDB_DATASTORE:-memory}"
35
45
  SPICEDB_DB_URI="${SPICEDB_DB_URI:-postgres://spicedb:spicedb_dev@127.0.0.1:5432/spicedb?sslmode=disable}"
36
46
  FALKORDB_PORT="${FALKORDB_PORT:-6379}"
37
47
  GRAPHITI_PORT="${GRAPHITI_PORT:-8000}"
48
+ GRAPHITI_PATH="${GRAPHITI_PATH:-$PROJECT_DIR/../graphiti}"
49
+
50
+ # Local model endpoints (for your dev environment)
51
+ LLM_PROVIDER="${LLM_PROVIDER:-openai_generic}"
52
+ LLM_MODEL="${LLM_MODEL:-nvidia/Qwen3-Next-80B-A3B-Instruct-NVFP4}"
53
+ LLM_API_URL="${LLM_API_URL:-http://dgx-spark:8000/v1}"
54
+ LLM_MAX_TOKENS="${LLM_MAX_TOKENS:-8192}"
55
+ EMBEDDER_MODEL="${EMBEDDER_MODEL:-nomic-embed-text:v1.5}"
56
+ EMBEDDER_DIMENSIONS="${EMBEDDER_DIMENSIONS:-768}"
57
+ EMBEDDER_API_URL="${EMBEDDER_API_URL:-http://minisforum:11434/v1}"
58
+ RERANKER_PROVIDER="${RERANKER_PROVIDER:-bge}"
38
59
 
39
60
  mkdir -p "$DEV_DIR/logs" "$DEV_DIR/pids" "$DEV_DIR/data/falkordb"
40
61
 
@@ -71,8 +92,9 @@ else
71
92
  --port "$FALKORDB_PORT" \
72
93
  --dir "$DEV_DIR/data/falkordb" \
73
94
  --daemonize no \
74
- --save "" \
75
- --appendonly no \
95
+ --save "300 10 60 1000" \
96
+ --appendonly yes \
97
+ --appendfsync everysec \
76
98
  --loglevel notice \
77
99
  > "$DEV_DIR/logs/falkordb.log" 2>&1 &
78
100
  echo $! > "$DEV_DIR/pids/falkordb.pid"
@@ -178,23 +200,30 @@ fi
178
200
  if is_running "$DEV_DIR/pids/graphiti.pid"; then
179
201
  echo "==> Graphiti MCP server already running (pid $(cat "$DEV_DIR/pids/graphiti.pid"))"
180
202
  else
181
- if [ -z "${OPENAI_API_KEY:-}" ]; then
182
- echo ""
183
- echo "WARNING: OPENAI_API_KEY is not set."
184
- echo "Graphiti needs it for entity extraction and embeddings."
185
- echo "Set it in .env or export it before running this script."
186
- echo ""
187
- fi
188
-
189
203
  echo "==> Starting Graphiti MCP server on port $GRAPHITI_PORT..."
204
+ echo " LLM: $LLM_PROVIDER / $LLM_MODEL"
205
+ echo " LLM endpoint: $LLM_API_URL"
206
+ echo " Embedder: $EMBEDDER_MODEL (${EMBEDDER_DIMENSIONS}d)"
207
+ echo " Embedder endpoint: $EMBEDDER_API_URL"
208
+ echo " Reranker: $RERANKER_PROVIDER"
190
209
 
191
- GRAPHITI_DIR="$DEV_DIR/graphiti/mcp_server"
210
+ GRAPHITI_DIR="$GRAPHITI_PATH/mcp_server"
192
211
 
193
212
  # Set environment for Graphiti
194
- export OPENAI_API_KEY="${OPENAI_API_KEY:-}"
213
+ export OPENAI_API_KEY="${OPENAI_API_KEY:-not-needed}"
195
214
  export FALKORDB_URI="redis://localhost:$FALKORDB_PORT"
196
215
  export EPISODE_ID_PREFIX="${EPISODE_ID_PREFIX:-epi-}"
197
216
 
217
+ # Export local model configuration
218
+ export LLM_PROVIDER
219
+ export LLM_MODEL
220
+ export LLM_API_URL
221
+ export LLM_MAX_TOKENS
222
+ export EMBEDDER_MODEL
223
+ export EMBEDDER_DIMENSIONS
224
+ export EMBEDDER_API_URL
225
+ export RERANKER_PROVIDER
226
+
198
227
  cd "$GRAPHITI_DIR"
199
228
  uv run main.py \
200
229
  --transport http \