@contextableai/openclaw-memory-rebac 0.1.3 → 0.1.5
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/dist/backend.d.ts +1 -1
- package/dist/backends/graphiti.d.ts +1 -1
- package/dist/backends/graphiti.js +5 -2
- package/dist/index.js +3 -1
- package/docker/graphiti/startup.py +57 -2
- package/package.json +1 -1
package/dist/backend.d.ts
CHANGED
|
@@ -117,7 +117,7 @@ export interface MemoryBackend {
|
|
|
117
117
|
* Optional: not all backends support sub-dataset deletion.
|
|
118
118
|
* Returns true if deleted, false if the backend doesn't support it.
|
|
119
119
|
*/
|
|
120
|
-
deleteFragment?(uuid: string): Promise<boolean>;
|
|
120
|
+
deleteFragment?(uuid: string, type?: string): Promise<boolean>;
|
|
121
121
|
/**
|
|
122
122
|
* Discover fragment (fact/edge) UUIDs that were extracted from a stored episode.
|
|
123
123
|
* Called after store() resolves the episode ID to write per-fragment SpiceDB
|
|
@@ -61,7 +61,7 @@ export declare class GraphitiBackend implements MemoryBackend {
|
|
|
61
61
|
getStatus(): Promise<Record<string, unknown>>;
|
|
62
62
|
deleteGroup(groupId: string): Promise<void>;
|
|
63
63
|
listGroups(): Promise<BackendDataset[]>;
|
|
64
|
-
deleteFragment(uuid: string): Promise<boolean>;
|
|
64
|
+
deleteFragment(uuid: string, type?: string): Promise<boolean>;
|
|
65
65
|
getEpisodes(groupId: string, lastN: number): Promise<GraphitiEpisode[]>;
|
|
66
66
|
discoverFragmentIds(episodeId: string): Promise<string[]>;
|
|
67
67
|
getEntityEdge(uuid: string): Promise<FactResult>;
|
|
@@ -146,8 +146,11 @@ export class GraphitiBackend {
|
|
|
146
146
|
// Graphiti has no list-groups API; the CLI can query SpiceDB for this
|
|
147
147
|
return [];
|
|
148
148
|
}
|
|
149
|
-
async deleteFragment(uuid) {
|
|
150
|
-
|
|
149
|
+
async deleteFragment(uuid, type) {
|
|
150
|
+
const path = type === "fact"
|
|
151
|
+
? `/entity-edge/${encodeURIComponent(uuid)}`
|
|
152
|
+
: `/episode/${encodeURIComponent(uuid)}`;
|
|
153
|
+
await this.restCall("DELETE", path);
|
|
151
154
|
return true;
|
|
152
155
|
}
|
|
153
156
|
// --------------------------------------------------------------------------
|
package/dist/index.js
CHANGED
|
@@ -283,6 +283,7 @@ const rebacMemoryPlugin = {
|
|
|
283
283
|
// Parse optional type prefix — strip it to get the bare UUID
|
|
284
284
|
const colonIdx = id.indexOf(":");
|
|
285
285
|
let uuid;
|
|
286
|
+
let fragmentType;
|
|
286
287
|
if (colonIdx > 0 && colonIdx < 10) {
|
|
287
288
|
const prefix = id.slice(0, colonIdx);
|
|
288
289
|
// "entity" type cannot be deleted this way (graph-backend specific)
|
|
@@ -292,6 +293,7 @@ const rebacMemoryPlugin = {
|
|
|
292
293
|
details: { action: "error", id },
|
|
293
294
|
};
|
|
294
295
|
}
|
|
296
|
+
fragmentType = prefix;
|
|
295
297
|
uuid = id.slice(colonIdx + 1);
|
|
296
298
|
}
|
|
297
299
|
else {
|
|
@@ -323,7 +325,7 @@ const rebacMemoryPlugin = {
|
|
|
323
325
|
// Attempt backend deletion (optional — not all backends support it)
|
|
324
326
|
if (backend.deleteFragment) {
|
|
325
327
|
try {
|
|
326
|
-
await backend.deleteFragment(uuid);
|
|
328
|
+
await backend.deleteFragment(uuid, fragmentType);
|
|
327
329
|
}
|
|
328
330
|
catch (err) {
|
|
329
331
|
api.logger.warn(`openclaw-memory-rebac: backend deletion failed for ${uuid}: ${err}`);
|
|
@@ -149,6 +149,25 @@ def patch():
|
|
|
149
149
|
'created_at', 'labels',
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
+
# Edge names that represent deduplication artifacts, not real semantic facts.
|
|
153
|
+
# Older graphiti-core versions created these during node resolution; the LLM
|
|
154
|
+
# may also extract them as relationships. Neither source is useful — upstream
|
|
155
|
+
# abandoned IS_DUPLICATE_OF edges and the information is redundant with node
|
|
156
|
+
# UUID reuse. Matched case-insensitively with space/underscore normalization.
|
|
157
|
+
# See: https://github.com/contextablemark/openclaw-memory-rebac/issues/12
|
|
158
|
+
RESERVED_EDGE_NAMES = {
|
|
159
|
+
'IS_DUPLICATE_OF',
|
|
160
|
+
'DUPLICATE_OF',
|
|
161
|
+
'HAS_DUPLICATE',
|
|
162
|
+
'DUPLICATES',
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
def _is_reserved_edge_name(name):
|
|
166
|
+
"""Check if an edge name is a deduplication artifact."""
|
|
167
|
+
if not name:
|
|
168
|
+
return False
|
|
169
|
+
return name.strip().upper().replace(" ", "_") in RESERVED_EDGE_NAMES
|
|
170
|
+
|
|
152
171
|
def _sanitize_attributes(attrs, reserved_keys):
|
|
153
172
|
"""Flatten non-primitive values and strip reserved keys to prevent clobber."""
|
|
154
173
|
if not attrs:
|
|
@@ -194,6 +213,31 @@ def patch():
|
|
|
194
213
|
list((edge.attributes or {}).keys()),
|
|
195
214
|
edge.source_node_uuid, edge.target_node_uuid,
|
|
196
215
|
)
|
|
216
|
+
# Filter deduplication-artifact edges (IS_DUPLICATE_OF and variants).
|
|
217
|
+
# These are not real facts — they are structural metadata that upstream
|
|
218
|
+
# graphiti-core has since abandoned.
|
|
219
|
+
original_edge_count = len(entity_edges)
|
|
220
|
+
entity_edges = [e for e in entity_edges if not _is_reserved_edge_name(e.name)]
|
|
221
|
+
dup_filtered = original_edge_count - len(entity_edges)
|
|
222
|
+
if dup_filtered:
|
|
223
|
+
logger.warning(
|
|
224
|
+
"DIAG duplicate_edge_filtered: removed %d IS_DUPLICATE_OF-family "
|
|
225
|
+
"edge(s) at bulk_add level", dup_filtered,
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
# Filter self-referential edges (entity relating to itself).
|
|
229
|
+
pre_self_count = len(entity_edges)
|
|
230
|
+
entity_edges = [
|
|
231
|
+
e for e in entity_edges
|
|
232
|
+
if e.source_node_uuid != e.target_node_uuid
|
|
233
|
+
]
|
|
234
|
+
self_ref_count = pre_self_count - len(entity_edges)
|
|
235
|
+
if self_ref_count:
|
|
236
|
+
logger.warning(
|
|
237
|
+
"DIAG self_ref_edge_filtered: removed %d self-referential edge(s)",
|
|
238
|
+
self_ref_count,
|
|
239
|
+
)
|
|
240
|
+
|
|
197
241
|
return await original_bulk_add(
|
|
198
242
|
driver, episodic_nodes, episodic_edges,
|
|
199
243
|
entity_nodes, entity_edges, embedder,
|
|
@@ -253,15 +297,26 @@ def patch():
|
|
|
253
297
|
except Exception as _patch_err:
|
|
254
298
|
logger.warning("Could not patch ExtractedEdges for None-index filtering: %s", _patch_err)
|
|
255
299
|
|
|
256
|
-
# Fallback: catch any remaining TypeError from the whole function
|
|
300
|
+
# Fallback: catch any remaining TypeError from the whole function,
|
|
301
|
+
# and filter IS_DUPLICATE_OF edges early (before embedding computation).
|
|
257
302
|
async def safe_extract_edges(*args, **kwargs):
|
|
258
303
|
try:
|
|
259
|
-
|
|
304
|
+
result = await original_extract_edges(*args, **kwargs)
|
|
260
305
|
except TypeError as e:
|
|
261
306
|
if "not supported between instances" in str(e):
|
|
262
307
|
logger.warning("extract_edges skipped due to LLM output issue: %s", e)
|
|
263
308
|
return []
|
|
264
309
|
raise
|
|
310
|
+
if result:
|
|
311
|
+
original_len = len(result)
|
|
312
|
+
result = [e for e in result if not _is_reserved_edge_name(e.name)]
|
|
313
|
+
dropped = original_len - len(result)
|
|
314
|
+
if dropped:
|
|
315
|
+
logger.warning(
|
|
316
|
+
"DIAG duplicate_edge_filtered: removed %d IS_DUPLICATE_OF-family "
|
|
317
|
+
"edge(s) at extract_edges level", dropped,
|
|
318
|
+
)
|
|
319
|
+
return result
|
|
265
320
|
|
|
266
321
|
edge_ops_mod.extract_edges = safe_extract_edges
|
|
267
322
|
# Patch the local binding in graphiti.py
|
package/package.json
CHANGED