@contextableai/openclaw-memory-rebac 0.3.4 → 0.3.6
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 +25 -1
- package/dist/backend.d.ts +14 -1
- package/dist/backends/graphiti.d.ts +7 -0
- package/dist/backends/graphiti.js +23 -0
- package/dist/config.d.ts +4 -0
- package/dist/config.js +17 -1
- package/dist/index.js +54 -42
- package/dist/search.d.ts +12 -11
- package/dist/search.js +25 -15
- package/docker/graphiti/Dockerfile +11 -1
- package/package.json +8 -8
package/README.md
CHANGED
|
@@ -163,6 +163,25 @@ When enabled (default: `true`), the plugin captures the last N messages from eac
|
|
|
163
163
|
- Skips messages shorter than 5 characters and injected context blocks
|
|
164
164
|
- Uses custom extraction instructions for entity/fact extraction
|
|
165
165
|
|
|
166
|
+
### Session Filtering
|
|
167
|
+
|
|
168
|
+
Both auto-recall and auto-capture can be filtered by session key pattern using the `sessionFilter` config option. This is useful for excluding cron jobs, monitoring sessions, or other automated processes that generate repetitive, low-value data.
|
|
169
|
+
|
|
170
|
+
```json
|
|
171
|
+
{
|
|
172
|
+
"sessionFilter": {
|
|
173
|
+
"excludePatterns": ["cron", "monitoring", "healthcheck"]
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
- **`excludePatterns`**: Array of strings. If the session key contains any of these substrings, auto-recall and auto-capture are skipped for that session.
|
|
179
|
+
- **`includePatterns`**: Array of strings. If set, only sessions whose key contains at least one of these substrings will trigger auto-recall/capture.
|
|
180
|
+
- If both are set, `excludePatterns` takes priority (exclude first, then check include).
|
|
181
|
+
- If neither is set, all sessions are captured (default behavior).
|
|
182
|
+
|
|
183
|
+
Filtered sessions still have full access to explicit memory tools (`memory_recall`, `memory_store`, `memory_forget`, `memory_status`). This means a cron job that consolidates memories can still use `memory_recall` and `memory_store` directly — only the automatic hooks are suppressed.
|
|
184
|
+
|
|
166
185
|
## Authorization Model
|
|
167
186
|
|
|
168
187
|
The SpiceDB schema defines four object types:
|
|
@@ -270,6 +289,8 @@ Agents without an `identities` entry (like service agents) are not linked to any
|
|
|
270
289
|
| `identities` | object | `{}` | Maps agent IDs to owner person IDs for cross-agent recall (see [Identity Linking](#identity-linking)) |
|
|
271
290
|
| `autoCapture` | boolean | `true` | Auto-capture conversations |
|
|
272
291
|
| `autoRecall` | boolean | `true` | Auto-inject relevant memories |
|
|
292
|
+
| `sessionFilter.excludePatterns` | string[] | `[]` | Skip auto-capture/recall for sessions matching any pattern |
|
|
293
|
+
| `sessionFilter.includePatterns` | string[] | `[]` | Only auto-capture/recall sessions matching at least one pattern |
|
|
273
294
|
| `customInstructions` | string | *(see below)* | Custom extraction instructions |
|
|
274
295
|
| `maxCaptureMessages` | integer | `10` | Max messages per auto-capture batch (1-50) |
|
|
275
296
|
|
|
@@ -389,7 +410,10 @@ OpenClaw has an exclusive `memory` slot — only one memory plugin is active at
|
|
|
389
410
|
"my-agent": "U0123ABC"
|
|
390
411
|
},
|
|
391
412
|
"autoCapture": true,
|
|
392
|
-
"autoRecall": true
|
|
413
|
+
"autoRecall": true,
|
|
414
|
+
"sessionFilter": {
|
|
415
|
+
"excludePatterns": ["cron"]
|
|
416
|
+
}
|
|
393
417
|
}
|
|
394
418
|
}
|
|
395
419
|
}
|
package/dist/backend.d.ts
CHANGED
|
@@ -74,7 +74,7 @@ export interface MemoryBackend {
|
|
|
74
74
|
}): Promise<StoreResult>;
|
|
75
75
|
/**
|
|
76
76
|
* Search within a single group's storage partition.
|
|
77
|
-
*
|
|
77
|
+
* Fallback path when searchGroups() is not available.
|
|
78
78
|
* Backends map their native result shape to SearchResult[].
|
|
79
79
|
*/
|
|
80
80
|
searchGroup(params: {
|
|
@@ -83,6 +83,19 @@ export interface MemoryBackend {
|
|
|
83
83
|
limit: number;
|
|
84
84
|
sessionId?: string;
|
|
85
85
|
}): Promise<SearchResult[]>;
|
|
86
|
+
/**
|
|
87
|
+
* Search across multiple groups in a single backend call.
|
|
88
|
+
* Preferred over per-group fan-out because the backend can apply its own
|
|
89
|
+
* cross-group relevance ranking (e.g. Graphiti's RRF: cosine + BM25).
|
|
90
|
+
* Results are ordered by relevance; response position is used as implicit score.
|
|
91
|
+
* Optional: falls back to per-group searchGroup() fan-out if not implemented.
|
|
92
|
+
*/
|
|
93
|
+
searchGroups?(params: {
|
|
94
|
+
query: string;
|
|
95
|
+
groupIds: string[];
|
|
96
|
+
limit: number;
|
|
97
|
+
sessionId?: string;
|
|
98
|
+
}): Promise<SearchResult[]>;
|
|
86
99
|
/**
|
|
87
100
|
* Optional backend-specific session enrichment, called from agent_end
|
|
88
101
|
* after store() for conversation auto-capture.
|
|
@@ -26,6 +26,7 @@ type FactResult = {
|
|
|
26
26
|
invalid_at: string | null;
|
|
27
27
|
created_at: string;
|
|
28
28
|
expired_at: string | null;
|
|
29
|
+
group_id?: string;
|
|
29
30
|
};
|
|
30
31
|
export type GraphitiConfig = {
|
|
31
32
|
endpoint: string;
|
|
@@ -56,6 +57,12 @@ export declare class GraphitiBackend implements MemoryBackend {
|
|
|
56
57
|
limit: number;
|
|
57
58
|
sessionId?: string;
|
|
58
59
|
}): Promise<SearchResult[]>;
|
|
60
|
+
searchGroups(params: {
|
|
61
|
+
query: string;
|
|
62
|
+
groupIds: string[];
|
|
63
|
+
limit: number;
|
|
64
|
+
sessionId?: string;
|
|
65
|
+
}): Promise<SearchResult[]>;
|
|
59
66
|
getConversationHistory(sessionId: string, lastN?: number): Promise<ConversationTurn[]>;
|
|
60
67
|
healthCheck(): Promise<boolean>;
|
|
61
68
|
getStatus(): Promise<Record<string, unknown>>;
|
|
@@ -107,6 +107,29 @@ export class GraphitiBackend {
|
|
|
107
107
|
created_at: f.created_at,
|
|
108
108
|
}));
|
|
109
109
|
}
|
|
110
|
+
async searchGroups(params) {
|
|
111
|
+
const { query, groupIds, limit } = params;
|
|
112
|
+
if (groupIds.length === 0)
|
|
113
|
+
return [];
|
|
114
|
+
const searchRequest = {
|
|
115
|
+
group_ids: groupIds,
|
|
116
|
+
query,
|
|
117
|
+
max_facts: limit,
|
|
118
|
+
};
|
|
119
|
+
const response = await this.restCall("POST", "/search", searchRequest);
|
|
120
|
+
const facts = response.facts ?? [];
|
|
121
|
+
// Derive implicit relevance score from response position —
|
|
122
|
+
// Graphiti's /search returns results ranked by RRF (cosine + BM25).
|
|
123
|
+
return facts.map((f, index) => ({
|
|
124
|
+
type: "fact",
|
|
125
|
+
uuid: f.uuid,
|
|
126
|
+
group_id: f.group_id ?? groupIds[0],
|
|
127
|
+
summary: f.fact,
|
|
128
|
+
context: f.name,
|
|
129
|
+
created_at: f.created_at,
|
|
130
|
+
score: 1.0 - index / Math.max(facts.length, 1),
|
|
131
|
+
}));
|
|
132
|
+
}
|
|
110
133
|
async getConversationHistory(sessionId, lastN = 10) {
|
|
111
134
|
const sessionGroup = `session-${sessionId.replace(/[^a-zA-Z0-9_-]/g, "-")}`;
|
|
112
135
|
try {
|
package/dist/config.d.ts
CHANGED
|
@@ -21,6 +21,10 @@ export type RebacMemoryConfig = {
|
|
|
21
21
|
autoCapture: boolean;
|
|
22
22
|
autoRecall: boolean;
|
|
23
23
|
maxCaptureMessages: number;
|
|
24
|
+
sessionFilter?: {
|
|
25
|
+
excludePatterns?: string[];
|
|
26
|
+
includePatterns?: string[];
|
|
27
|
+
};
|
|
24
28
|
};
|
|
25
29
|
export declare const rebacMemoryConfigSchema: {
|
|
26
30
|
parse(value: unknown): RebacMemoryConfig;
|
package/dist/config.js
CHANGED
|
@@ -25,6 +25,21 @@ function assertAllowedKeys(value, allowed, label) {
|
|
|
25
25
|
throw new Error(`${label} has unknown keys: ${unknown.join(", ")}`);
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
|
+
function parseSessionFilter(raw) {
|
|
29
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
30
|
+
return undefined;
|
|
31
|
+
const obj = raw;
|
|
32
|
+
assertAllowedKeys(obj, ["excludePatterns", "includePatterns"], "sessionFilter config");
|
|
33
|
+
const excludePatterns = Array.isArray(obj.excludePatterns)
|
|
34
|
+
? obj.excludePatterns.filter((p) => typeof p === "string")
|
|
35
|
+
: undefined;
|
|
36
|
+
const includePatterns = Array.isArray(obj.includePatterns)
|
|
37
|
+
? obj.includePatterns.filter((p) => typeof p === "string")
|
|
38
|
+
: undefined;
|
|
39
|
+
if (!excludePatterns?.length && !includePatterns?.length)
|
|
40
|
+
return undefined;
|
|
41
|
+
return { excludePatterns, includePatterns };
|
|
42
|
+
}
|
|
28
43
|
// ============================================================================
|
|
29
44
|
// Config schema
|
|
30
45
|
// ============================================================================
|
|
@@ -42,7 +57,7 @@ export const rebacMemoryConfigSchema = {
|
|
|
42
57
|
assertAllowedKeys(cfg, [
|
|
43
58
|
"backend", "spicedb",
|
|
44
59
|
"subjectType", "subjectId", "identities",
|
|
45
|
-
"autoCapture", "autoRecall", "maxCaptureMessages",
|
|
60
|
+
"autoCapture", "autoRecall", "maxCaptureMessages", "sessionFilter",
|
|
46
61
|
backendName,
|
|
47
62
|
], "openclaw-memory-rebac config");
|
|
48
63
|
// SpiceDB config (shared)
|
|
@@ -84,6 +99,7 @@ export const rebacMemoryConfigSchema = {
|
|
|
84
99
|
maxCaptureMessages: typeof cfg.maxCaptureMessages === "number" && cfg.maxCaptureMessages > 0
|
|
85
100
|
? cfg.maxCaptureMessages
|
|
86
101
|
: pluginDefaults.maxCaptureMessages,
|
|
102
|
+
sessionFilter: parseSessionFilter(cfg.sessionFilter),
|
|
87
103
|
};
|
|
88
104
|
},
|
|
89
105
|
};
|
package/dist/index.js
CHANGED
|
@@ -22,7 +22,7 @@ import { fileURLToPath } from "node:url";
|
|
|
22
22
|
import { rebacMemoryConfigSchema, createBackend, defaultGroupId } from "./config.js";
|
|
23
23
|
import { SpiceDbClient } from "./spicedb.js";
|
|
24
24
|
import { lookupAuthorizedGroups, lookupViewableFragments, lookupFragmentSourceGroups, lookupAgentOwner, writeFragmentRelationships, deleteFragmentRelationships, canDeleteFragment, canWriteToGroup, ensureGroupMembership, } from "./authorization.js";
|
|
25
|
-
import { searchAuthorizedMemories, formatDualResults,
|
|
25
|
+
import { searchAuthorizedMemories, formatDualResults, } from "./search.js";
|
|
26
26
|
import { registerCommands } from "./cli.js";
|
|
27
27
|
// ============================================================================
|
|
28
28
|
// Session helpers
|
|
@@ -34,6 +34,26 @@ function sessionGroupId(sessionId) {
|
|
|
34
34
|
function isSessionGroup(groupId) {
|
|
35
35
|
return groupId.startsWith("session-");
|
|
36
36
|
}
|
|
37
|
+
function isSessionAllowed(sessionKey, filter) {
|
|
38
|
+
if (!filter)
|
|
39
|
+
return true;
|
|
40
|
+
if (!sessionKey)
|
|
41
|
+
return true;
|
|
42
|
+
if (filter.excludePatterns?.length) {
|
|
43
|
+
for (const pattern of filter.excludePatterns) {
|
|
44
|
+
if (sessionKey.includes(pattern))
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (filter.includePatterns?.length) {
|
|
49
|
+
for (const pattern of filter.includePatterns) {
|
|
50
|
+
if (sessionKey.includes(pattern))
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
37
57
|
// ============================================================================
|
|
38
58
|
// Plugin Definition
|
|
39
59
|
// ============================================================================
|
|
@@ -131,27 +151,21 @@ const rebacMemoryPlugin = {
|
|
|
131
151
|
sessionGroups.push(sg);
|
|
132
152
|
}
|
|
133
153
|
}
|
|
134
|
-
//
|
|
135
|
-
const [
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
sessionId: state.sessionId,
|
|
150
|
-
})
|
|
151
|
-
: Promise.resolve([]),
|
|
152
|
-
]);
|
|
153
|
-
const sessionResults = deduplicateSessionResults(longTermResults, rawSessionResults);
|
|
154
|
-
const groupResults = [...longTermResults, ...sessionResults];
|
|
154
|
+
// Single multi-group search — lets the backend rank all results together
|
|
155
|
+
const allGroups = [...longTermGroups, ...sessionGroups];
|
|
156
|
+
const allGroupResults = allGroups.length > 0
|
|
157
|
+
? await searchAuthorizedMemories(backend, {
|
|
158
|
+
query,
|
|
159
|
+
groupIds: allGroups,
|
|
160
|
+
limit,
|
|
161
|
+
sessionId: state.sessionId,
|
|
162
|
+
})
|
|
163
|
+
: [];
|
|
164
|
+
// Split results by group type for formatting
|
|
165
|
+
const sessionGroupSet = new Set(sessionGroups);
|
|
166
|
+
const longTermResults = allGroupResults.filter((r) => !sessionGroupSet.has(r.group_id));
|
|
167
|
+
const sessionResults = allGroupResults.filter((r) => sessionGroupSet.has(r.group_id));
|
|
168
|
+
const groupResults = allGroupResults;
|
|
155
169
|
// Owner-aware fragment search: if the subject is an agent with an owner,
|
|
156
170
|
// also find fragments where the owner is in `involves`.
|
|
157
171
|
// Uses search-then-post-filter: search the source groups for query relevance,
|
|
@@ -466,6 +480,8 @@ const rebacMemoryPlugin = {
|
|
|
466
480
|
const state = getState(ctx?.agentId);
|
|
467
481
|
if (ctx?.sessionKey)
|
|
468
482
|
state.sessionId = ctx.sessionKey;
|
|
483
|
+
if (!isSessionAllowed(ctx?.sessionKey, cfg.sessionFilter))
|
|
484
|
+
return;
|
|
469
485
|
if (!event.prompt || event.prompt.length < 5)
|
|
470
486
|
return;
|
|
471
487
|
try {
|
|
@@ -477,26 +493,20 @@ const rebacMemoryPlugin = {
|
|
|
477
493
|
if (!sessionGroups.includes(sg))
|
|
478
494
|
sessionGroups.push(sg);
|
|
479
495
|
}
|
|
480
|
-
const
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
sessionId: state.sessionId,
|
|
495
|
-
})
|
|
496
|
-
: Promise.resolve([]),
|
|
497
|
-
]);
|
|
498
|
-
const sessionResults = deduplicateSessionResults(longTermResults, rawSessionResults);
|
|
499
|
-
const totalCount = longTermResults.length + sessionResults.length;
|
|
496
|
+
const autoRecallLimit = 8;
|
|
497
|
+
const allGroups = [...longTermGroups, ...sessionGroups];
|
|
498
|
+
const allResults = allGroups.length > 0
|
|
499
|
+
? await searchAuthorizedMemories(backend, {
|
|
500
|
+
query: event.prompt,
|
|
501
|
+
groupIds: allGroups,
|
|
502
|
+
limit: autoRecallLimit,
|
|
503
|
+
sessionId: state.sessionId,
|
|
504
|
+
})
|
|
505
|
+
: [];
|
|
506
|
+
const sessionGroupSet = new Set(sessionGroups);
|
|
507
|
+
const longTermResults = allResults.filter((r) => !sessionGroupSet.has(r.group_id));
|
|
508
|
+
const sessionResults = allResults.filter((r) => sessionGroupSet.has(r.group_id));
|
|
509
|
+
const totalCount = allResults.length;
|
|
500
510
|
const toolHint = "<memory-tools>\n" +
|
|
501
511
|
"You have knowledge-graph memory tools. Use them proactively:\n" +
|
|
502
512
|
"- memory_recall: Search for facts, preferences, people, decisions, or past context. Use this BEFORE saying you don't know or remember something.\n" +
|
|
@@ -521,6 +531,8 @@ const rebacMemoryPlugin = {
|
|
|
521
531
|
const state = getState(ctx?.agentId);
|
|
522
532
|
if (ctx?.sessionKey)
|
|
523
533
|
state.sessionId = ctx.sessionKey;
|
|
534
|
+
if (!isSessionAllowed(ctx?.sessionKey, cfg.sessionFilter))
|
|
535
|
+
return;
|
|
524
536
|
if (!event.success || !event.messages || event.messages.length === 0)
|
|
525
537
|
return;
|
|
526
538
|
try {
|
package/dist/search.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Multi-Group Search with backend-native relevance ranking.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Prefers MemoryBackend.searchGroups() — a single call with all group_ids
|
|
5
|
+
* so the backend applies cross-group relevance ranking (e.g. Graphiti RRF).
|
|
6
|
+
* Falls back to per-group fan-out via searchGroup() when unavailable.
|
|
7
7
|
*/
|
|
8
8
|
import type { MemoryBackend, SearchResult } from "./backend.js";
|
|
9
9
|
export type { SearchResult };
|
|
@@ -14,9 +14,14 @@ export type SearchOptions = {
|
|
|
14
14
|
sessionId?: string;
|
|
15
15
|
};
|
|
16
16
|
/**
|
|
17
|
-
* Search across multiple authorized group_ids
|
|
18
|
-
*
|
|
19
|
-
*
|
|
17
|
+
* Search across multiple authorized group_ids.
|
|
18
|
+
*
|
|
19
|
+
* Prefers backend.searchGroups() when available — sends all group_ids in a
|
|
20
|
+
* single call so the backend can apply cross-group relevance ranking
|
|
21
|
+
* (e.g. Graphiti's RRF: cosine similarity + BM25).
|
|
22
|
+
*
|
|
23
|
+
* Falls back to per-group fan-out via backend.searchGroup() when the backend
|
|
24
|
+
* doesn't implement multi-group search.
|
|
20
25
|
*/
|
|
21
26
|
export declare function searchAuthorizedMemories(backend: MemoryBackend, options: SearchOptions): Promise<SearchResult[]>;
|
|
22
27
|
/**
|
|
@@ -28,7 +33,3 @@ export declare function formatResultsForContext(results: SearchResult[]): string
|
|
|
28
33
|
* Session group_ids start with "session-".
|
|
29
34
|
*/
|
|
30
35
|
export declare function formatDualResults(longTermResults: SearchResult[], sessionResults: SearchResult[]): string;
|
|
31
|
-
/**
|
|
32
|
-
* Deduplicate session results against long-term results (by UUID).
|
|
33
|
-
*/
|
|
34
|
-
export declare function deduplicateSessionResults(longTermResults: SearchResult[], sessionResults: SearchResult[]): SearchResult[];
|
package/dist/search.js
CHANGED
|
@@ -1,24 +1,41 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Multi-Group Search with backend-native relevance ranking.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Prefers MemoryBackend.searchGroups() — a single call with all group_ids
|
|
5
|
+
* so the backend applies cross-group relevance ranking (e.g. Graphiti RRF).
|
|
6
|
+
* Falls back to per-group fan-out via searchGroup() when unavailable.
|
|
7
7
|
*/
|
|
8
8
|
// ============================================================================
|
|
9
9
|
// Search
|
|
10
10
|
// ============================================================================
|
|
11
11
|
/**
|
|
12
|
-
* Search across multiple authorized group_ids
|
|
13
|
-
*
|
|
14
|
-
*
|
|
12
|
+
* Search across multiple authorized group_ids.
|
|
13
|
+
*
|
|
14
|
+
* Prefers backend.searchGroups() when available — sends all group_ids in a
|
|
15
|
+
* single call so the backend can apply cross-group relevance ranking
|
|
16
|
+
* (e.g. Graphiti's RRF: cosine similarity + BM25).
|
|
17
|
+
*
|
|
18
|
+
* Falls back to per-group fan-out via backend.searchGroup() when the backend
|
|
19
|
+
* doesn't implement multi-group search.
|
|
15
20
|
*/
|
|
16
21
|
export async function searchAuthorizedMemories(backend, options) {
|
|
17
22
|
const { query, groupIds, limit = 10, sessionId } = options;
|
|
18
23
|
if (groupIds.length === 0) {
|
|
19
24
|
return [];
|
|
20
25
|
}
|
|
21
|
-
//
|
|
26
|
+
// Prefer single multi-group search — preserves backend relevance ranking
|
|
27
|
+
if (backend.searchGroups) {
|
|
28
|
+
const results = await backend.searchGroups({ query, groupIds, limit, sessionId });
|
|
29
|
+
// Deduplicate by UUID (defensive — single call shouldn't produce dupes)
|
|
30
|
+
const seen = new Set();
|
|
31
|
+
return results.filter((r) => {
|
|
32
|
+
if (seen.has(r.uuid))
|
|
33
|
+
return false;
|
|
34
|
+
seen.add(r.uuid);
|
|
35
|
+
return true;
|
|
36
|
+
}).slice(0, limit);
|
|
37
|
+
}
|
|
38
|
+
// Fallback: fan out parallel searches across all authorized groups
|
|
22
39
|
const promises = groupIds.map((groupId) => backend.searchGroup({ query, groupId, limit, sessionId }));
|
|
23
40
|
const resultSets = await Promise.allSettled(promises);
|
|
24
41
|
// Collect all successful results — silently skip failed group searches
|
|
@@ -89,10 +106,3 @@ function formatResultLine(r, idx) {
|
|
|
89
106
|
"completion";
|
|
90
107
|
return `${idx}. [${typeLabel}:${r.uuid}] ${r.summary} (${r.context})`;
|
|
91
108
|
}
|
|
92
|
-
/**
|
|
93
|
-
* Deduplicate session results against long-term results (by UUID).
|
|
94
|
-
*/
|
|
95
|
-
export function deduplicateSessionResults(longTermResults, sessionResults) {
|
|
96
|
-
const longTermIds = new Set(longTermResults.map((r) => r.uuid));
|
|
97
|
-
return sessionResults.filter((r) => !longTermIds.has(r.uuid));
|
|
98
|
-
}
|
|
@@ -5,9 +5,19 @@
|
|
|
5
5
|
# Extends the base zepai/graphiti image to:
|
|
6
6
|
# 1. Install sentence-transformers for BGE reranker
|
|
7
7
|
# 2. Overlay per-component config + startup to wire separate clients
|
|
8
|
+
#
|
|
9
|
+
# UPGRADE AUDIT (vs upstream getzep/graphiti v0.28.2, audited 2026-03-21):
|
|
10
|
+
# Overlay patches in startup.py — 5 of 6 still needed:
|
|
11
|
+
# [NEEDED] AsyncWorker crash-on-error recovery (startup.py:109-126)
|
|
12
|
+
# [NEEDED] Neo4j nested attribute sanitization (startup.py:128-250)
|
|
13
|
+
# [SAFE] None-index extract_edges fix (startup.py:252-298)
|
|
14
|
+
# — fixed upstream (name-based model); patch self-disables
|
|
15
|
+
# [NEEDED] IS_DUPLICATE_OF edge filtering (startup.py:152-169,216-227,310-318)
|
|
16
|
+
# [NEEDED] Self-referential edge filtering (startup.py:228-239)
|
|
17
|
+
# [NEEDED] Singleton client / per-request fix (startup.py:38-87)
|
|
8
18
|
###############################################################################
|
|
9
19
|
|
|
10
|
-
FROM zepai/graphiti:
|
|
20
|
+
FROM zepai/graphiti:0.22.0
|
|
11
21
|
|
|
12
22
|
# Base image runs as non-root "app" user; switch to root to install packages
|
|
13
23
|
USER root
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contextableai/openclaw-memory-rebac",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.6",
|
|
4
4
|
"description": "OpenClaw two-layer memory plugin: SpiceDB ReBAC authorization + Graphiti knowledge graph",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -43,16 +43,16 @@
|
|
|
43
43
|
"test:e2e": "OPENCLAW_LIVE_TEST=1 vitest run --config vitest.e2e.config.ts"
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
|
-
"@authzed/authzed-node": "
|
|
47
|
-
"@grpc/grpc-js": "
|
|
46
|
+
"@authzed/authzed-node": "1.6.1",
|
|
47
|
+
"@grpc/grpc-js": "1.14.3",
|
|
48
48
|
"@sinclair/typebox": "0.34.48",
|
|
49
|
-
"commander": "
|
|
49
|
+
"commander": "13.1.0"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
|
-
"dotenv": "
|
|
52
|
+
"dotenv": "17.3.1",
|
|
53
53
|
"openclaw": "*",
|
|
54
|
-
"typescript": "
|
|
55
|
-
"vitest": "
|
|
54
|
+
"typescript": "5.9.3",
|
|
55
|
+
"vitest": "4.0.18"
|
|
56
56
|
},
|
|
57
57
|
"publishConfig": {
|
|
58
58
|
"access": "public"
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
"./dist/index.js"
|
|
63
63
|
],
|
|
64
64
|
"install": {
|
|
65
|
-
"npmSpec": "@contextableai/openclaw-memory-rebac",
|
|
65
|
+
"npmSpec": "@contextableai/openclaw-memory-rebac@0.3.5",
|
|
66
66
|
"localPath": "extensions/openclaw-memory-rebac",
|
|
67
67
|
"defaultChoice": "npm"
|
|
68
68
|
}
|