@contextableai/openclaw-memory-rebac 0.3.7 → 0.4.0
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 +81 -15
- package/dist/authorization.d.ts +20 -0
- package/dist/authorization.js +60 -0
- package/dist/backend.d.ts +18 -0
- package/dist/backends/evermemos.d.ts +84 -0
- package/dist/backends/evermemos.defaults.json +10 -0
- package/dist/backends/evermemos.js +404 -0
- package/dist/backends/registry.js +2 -0
- package/dist/config.d.ts +2 -0
- package/dist/config.js +15 -1
- package/dist/index.d.ts +6 -0
- package/dist/index.js +169 -5
- package/docker/docker-compose.evermemos.yml +21 -0
- package/docker/{docker-compose.yml → docker-compose.graphiti.yml} +1 -1
- package/docker/evermemos/.env.example +85 -0
- package/docker/evermemos/Dockerfile +133 -0
- package/docker/evermemos/docker-compose.yml +52 -0
- package/docker/evermemos/trace_overlay.py +128 -0
- package/docker/graphiti/Dockerfile +4 -2
- package/docker/graphiti/startup.py +25 -0
- package/docker/spicedb/docker-compose.yml +4 -4
- package/package.json +6 -4
- package/schema.zed +6 -2
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EverMemOSBackend — MemoryBackend implementation backed by the EverMemOS FastAPI REST server.
|
|
3
|
+
*
|
|
4
|
+
* EverMemOS communicates via standard HTTP REST endpoints on port 1995.
|
|
5
|
+
* Messages are processed through the MemCell pipeline: boundary detection →
|
|
6
|
+
* parallel LLM extraction of episodic memories, foresight, event logs, and profiles.
|
|
7
|
+
*
|
|
8
|
+
* store() returns immediately with a generated message_id as the fragment anchor.
|
|
9
|
+
* With @timeout_to_background(), store may return 202 Accepted for background processing.
|
|
10
|
+
*
|
|
11
|
+
* discoverFragmentIds() polls a custom trace overlay endpoint to resolve the message_id
|
|
12
|
+
* to actual MongoDB ObjectIds of derived memories. These ObjectIds are what search
|
|
13
|
+
* results return, enabling fragment-level SpiceDB authorization (involves, share).
|
|
14
|
+
*
|
|
15
|
+
* resolveAnchors() provides lazy resolution at recall time: if discoverFragmentIds()
|
|
16
|
+
* timed out during store, unresolved anchors can be resolved later when someone
|
|
17
|
+
* actually tries to recall the involved fragments.
|
|
18
|
+
*/
|
|
19
|
+
import { randomUUID } from "node:crypto";
|
|
20
|
+
import { request as undiciRequest } from "undici";
|
|
21
|
+
/**
|
|
22
|
+
* Map EverMemOS memory types to the SearchResult type union.
|
|
23
|
+
*
|
|
24
|
+
* episodic_memory → "chunk" (narrative text chunks)
|
|
25
|
+
* profile → "summary" (distilled user characteristics)
|
|
26
|
+
* foresight → "summary" (future-oriented predictions)
|
|
27
|
+
* event_log → "fact" (discrete factual events)
|
|
28
|
+
*/
|
|
29
|
+
function mapMemoryType(memoryType) {
|
|
30
|
+
switch (memoryType) {
|
|
31
|
+
case "episodic_memory": return "chunk";
|
|
32
|
+
case "event_log": return "fact";
|
|
33
|
+
case "profile":
|
|
34
|
+
case "foresight":
|
|
35
|
+
return "summary";
|
|
36
|
+
default: return "chunk";
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Build a context prefix from the EverMemOS memory type.
|
|
41
|
+
* This allows downstream consumers to distinguish memory kinds.
|
|
42
|
+
*/
|
|
43
|
+
function contextPrefix(memoryType) {
|
|
44
|
+
switch (memoryType) {
|
|
45
|
+
case "episodic_memory": return "episode";
|
|
46
|
+
case "profile": return "profile";
|
|
47
|
+
case "foresight": return "foresight";
|
|
48
|
+
case "event_log": return "event";
|
|
49
|
+
default: return memoryType;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Extract the primary content from an EverMemOS memory based on its type.
|
|
54
|
+
* Each memory type stores its content in a different field.
|
|
55
|
+
*/
|
|
56
|
+
function extractContent(m) {
|
|
57
|
+
switch (m.memory_type) {
|
|
58
|
+
case "episodic_memory": return m.episode ?? m.summary ?? "";
|
|
59
|
+
case "foresight": return m.foresight ?? "";
|
|
60
|
+
case "event_log": return m.summary ?? "";
|
|
61
|
+
case "profile": return m.summary ?? "";
|
|
62
|
+
default: return m.summary ?? m.episode ?? "";
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
export class EverMemOSBackend {
|
|
66
|
+
config;
|
|
67
|
+
name = "evermemos";
|
|
68
|
+
requestTimeoutMs;
|
|
69
|
+
/**
|
|
70
|
+
* Tracks pending stores: messageId → groupId.
|
|
71
|
+
* Populated by store(), consumed by discoverFragmentIds().
|
|
72
|
+
*/
|
|
73
|
+
pendingStores = new Map();
|
|
74
|
+
constructor(config) {
|
|
75
|
+
this.config = config;
|
|
76
|
+
this.requestTimeoutMs = config.requestTimeoutMs ?? 30000;
|
|
77
|
+
}
|
|
78
|
+
// --------------------------------------------------------------------------
|
|
79
|
+
// REST transport
|
|
80
|
+
// --------------------------------------------------------------------------
|
|
81
|
+
async restCall(method, path, body, queryParams) {
|
|
82
|
+
let url = `${this.config.endpoint}${path}`;
|
|
83
|
+
if (queryParams) {
|
|
84
|
+
const params = new URLSearchParams(queryParams);
|
|
85
|
+
url += `?${params.toString()}`;
|
|
86
|
+
}
|
|
87
|
+
// Node.js fetch rejects body on GET requests (per spec). EverMemOS's
|
|
88
|
+
// search endpoint is GET-only with a JSON request body, so we use
|
|
89
|
+
// undici.request which permits body on any method.
|
|
90
|
+
if (method === "GET" && body !== undefined) {
|
|
91
|
+
const res = await undiciRequest(url, {
|
|
92
|
+
method: "GET",
|
|
93
|
+
headers: { "content-type": "application/json" },
|
|
94
|
+
body: JSON.stringify(body),
|
|
95
|
+
signal: AbortSignal.timeout(this.requestTimeoutMs),
|
|
96
|
+
});
|
|
97
|
+
if (res.statusCode < 200 || res.statusCode >= 300) {
|
|
98
|
+
const text = await res.body.text().catch(() => "");
|
|
99
|
+
throw new Error(`EverMemOS REST ${method} ${path} failed: ${res.statusCode} ${text}`);
|
|
100
|
+
}
|
|
101
|
+
const ct = res.headers["content-type"] ?? "";
|
|
102
|
+
if (ct.includes("application/json")) {
|
|
103
|
+
return (await res.body.json());
|
|
104
|
+
}
|
|
105
|
+
return {};
|
|
106
|
+
}
|
|
107
|
+
const opts = {
|
|
108
|
+
method,
|
|
109
|
+
headers: { "Content-Type": "application/json" },
|
|
110
|
+
signal: AbortSignal.timeout(this.requestTimeoutMs),
|
|
111
|
+
};
|
|
112
|
+
if (body !== undefined) {
|
|
113
|
+
opts.body = JSON.stringify(body);
|
|
114
|
+
}
|
|
115
|
+
const response = await fetch(url, opts);
|
|
116
|
+
if (!response.ok) {
|
|
117
|
+
const text = await response.text().catch(() => "");
|
|
118
|
+
throw new Error(`EverMemOS REST ${method} ${path} failed: ${response.status} ${text}`);
|
|
119
|
+
}
|
|
120
|
+
const ct = response.headers.get("content-type") ?? "";
|
|
121
|
+
if (ct.includes("application/json")) {
|
|
122
|
+
return (await response.json());
|
|
123
|
+
}
|
|
124
|
+
return {};
|
|
125
|
+
}
|
|
126
|
+
// --------------------------------------------------------------------------
|
|
127
|
+
// MemoryBackend implementation
|
|
128
|
+
// --------------------------------------------------------------------------
|
|
129
|
+
async store(params) {
|
|
130
|
+
const messageId = randomUUID();
|
|
131
|
+
const request = {
|
|
132
|
+
message_id: messageId,
|
|
133
|
+
create_time: new Date().toISOString(),
|
|
134
|
+
sender: this.config.defaultSenderId,
|
|
135
|
+
content: params.content,
|
|
136
|
+
group_id: params.groupId,
|
|
137
|
+
role: "user",
|
|
138
|
+
refer_list: [],
|
|
139
|
+
};
|
|
140
|
+
await this.restCall("POST", "/api/v1/memories", request);
|
|
141
|
+
// EverMemOS processes messages asynchronously through its MemCell pipeline.
|
|
142
|
+
// customPrompt is ignored — EverMemOS handles extraction internally.
|
|
143
|
+
//
|
|
144
|
+
// We return our generated message_id as the fragment anchor for SpiceDB.
|
|
145
|
+
// discoverFragmentIds() will resolve this to actual MongoDB ObjectIds via
|
|
146
|
+
// the trace overlay endpoint, enabling fragment-level involves/share.
|
|
147
|
+
this.pendingStores.set(messageId, { groupId: params.groupId });
|
|
148
|
+
return { fragmentId: Promise.resolve(messageId) };
|
|
149
|
+
}
|
|
150
|
+
async searchGroup(params) {
|
|
151
|
+
const { query, groupId, limit } = params;
|
|
152
|
+
const body = {
|
|
153
|
+
query,
|
|
154
|
+
group_id: groupId,
|
|
155
|
+
top_k: limit,
|
|
156
|
+
retrieve_method: this.config.retrieveMethod,
|
|
157
|
+
memory_types: this.config.memoryTypes,
|
|
158
|
+
};
|
|
159
|
+
const response = await this.restCall("GET", "/api/v1/memories/search", body);
|
|
160
|
+
// Response nests memories under group-id keys:
|
|
161
|
+
// result.memories: [{ "group-a": [mem1, mem2, ...] }]
|
|
162
|
+
// result.scores: [{ "group-a": [0.95, 0.87, ...] }]
|
|
163
|
+
const memoryGroups = response.result?.memories ?? [];
|
|
164
|
+
const scoreGroups = response.result?.scores ?? [];
|
|
165
|
+
const flat = [];
|
|
166
|
+
for (let gi = 0; gi < memoryGroups.length; gi++) {
|
|
167
|
+
const groupObj = memoryGroups[gi];
|
|
168
|
+
const scoreObj = scoreGroups[gi] ?? {};
|
|
169
|
+
for (const [gid, mems] of Object.entries(groupObj)) {
|
|
170
|
+
const scores = scoreObj[gid] ?? [];
|
|
171
|
+
for (let mi = 0; mi < mems.length; mi++) {
|
|
172
|
+
flat.push({ mem: mems[mi], score: scores[mi] ?? 0 });
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return flat.map(({ mem: m, score }, index) => ({
|
|
177
|
+
type: mapMemoryType(m.memory_type),
|
|
178
|
+
uuid: m.id,
|
|
179
|
+
group_id: m.group_id ?? groupId,
|
|
180
|
+
summary: extractContent(m),
|
|
181
|
+
context: `${contextPrefix(m.memory_type)}: ${m.subject ?? ""}`.trim(),
|
|
182
|
+
created_at: m.timestamp ?? m.created_at ?? "",
|
|
183
|
+
score: score || 1.0 - index / Math.max(flat.length, 1),
|
|
184
|
+
}));
|
|
185
|
+
}
|
|
186
|
+
// searchGroups intentionally not implemented — search.ts fan-out handles multi-group.
|
|
187
|
+
// EverMemOS has no native cross-group ranking.
|
|
188
|
+
async enrichSession(params) {
|
|
189
|
+
const meta = {
|
|
190
|
+
group_id: params.groupId,
|
|
191
|
+
user_details: {
|
|
192
|
+
[this.config.defaultSenderId]: {
|
|
193
|
+
role: "user",
|
|
194
|
+
last_message: params.userMsg.slice(0, 200),
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
try {
|
|
199
|
+
await this.restCall("POST", "/api/v1/memories/conversation-meta", meta);
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
// Best-effort — conversation metadata enrichment is non-critical
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
async getConversationHistory(sessionId, lastN = 10) {
|
|
206
|
+
const sessionGroup = `session-${sessionId.replace(/[^a-zA-Z0-9_-]/g, "-")}`;
|
|
207
|
+
try {
|
|
208
|
+
const queryParams = {
|
|
209
|
+
group_id: sessionGroup,
|
|
210
|
+
memory_type: "episodic_memory",
|
|
211
|
+
user_id: this.config.defaultSenderId,
|
|
212
|
+
top_k: String(lastN),
|
|
213
|
+
};
|
|
214
|
+
const response = await this.restCall("GET", "/api/v1/memories", undefined, queryParams);
|
|
215
|
+
const memories = response.memories ?? [];
|
|
216
|
+
return memories.map((m) => ({
|
|
217
|
+
query: "",
|
|
218
|
+
answer: m.episode ?? m.summary ?? "",
|
|
219
|
+
created_at: m.created_at,
|
|
220
|
+
}));
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
return [];
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
async healthCheck() {
|
|
227
|
+
try {
|
|
228
|
+
const response = await fetch(`${this.config.endpoint}/health`, { signal: AbortSignal.timeout(5000) });
|
|
229
|
+
return response.ok;
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
async getStatus() {
|
|
236
|
+
return {
|
|
237
|
+
backend: "evermemos",
|
|
238
|
+
endpoint: this.config.endpoint,
|
|
239
|
+
retrieveMethod: this.config.retrieveMethod,
|
|
240
|
+
memoryTypes: this.config.memoryTypes,
|
|
241
|
+
healthy: await this.healthCheck(),
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
async deleteGroup(groupId) {
|
|
245
|
+
// LIMITATION: EverMemOS DELETE only soft-deletes MemCells. Derived memories
|
|
246
|
+
// (episodic, foresight, event_log) remain searchable. See:
|
|
247
|
+
// https://github.com/EverMind-AI/EverMemOS/issues/148
|
|
248
|
+
await this.restCall("DELETE", "/api/v1/memories", {
|
|
249
|
+
event_id: "__all__",
|
|
250
|
+
user_id: "__all__",
|
|
251
|
+
group_id: groupId,
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
async listGroups() {
|
|
255
|
+
// EverMemOS has no list-groups API
|
|
256
|
+
return [];
|
|
257
|
+
}
|
|
258
|
+
async deleteFragment(uuid, _type) {
|
|
259
|
+
// LIMITATION: EverMemOS DELETE only soft-deletes MemCells, not derived memories.
|
|
260
|
+
// The uuid from search results is a derived memory _id (episodic/foresight/event_log),
|
|
261
|
+
// not a MemCell _id, so the API will return "not found" for most fragment deletes.
|
|
262
|
+
// See: https://github.com/EverMind-AI/EverMemOS/issues/148
|
|
263
|
+
try {
|
|
264
|
+
await this.restCall("DELETE", "/api/v1/memories", {
|
|
265
|
+
event_id: uuid,
|
|
266
|
+
user_id: "__all__",
|
|
267
|
+
group_id: "__all__",
|
|
268
|
+
});
|
|
269
|
+
return true;
|
|
270
|
+
}
|
|
271
|
+
catch {
|
|
272
|
+
return false;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
// --------------------------------------------------------------------------
|
|
276
|
+
// Fragment discovery via trace overlay
|
|
277
|
+
// --------------------------------------------------------------------------
|
|
278
|
+
/**
|
|
279
|
+
* Poll the trace overlay endpoint to discover MongoDB ObjectIds of derived
|
|
280
|
+
* memories (episodic, foresight, event_log) produced from a stored message.
|
|
281
|
+
*
|
|
282
|
+
* Called by index.ts after store() resolves the fragmentId Promise.
|
|
283
|
+
* Returns ObjectIds that match what search results return, enabling
|
|
284
|
+
* fragment-level SpiceDB authorization (involves, share/unshare).
|
|
285
|
+
*/
|
|
286
|
+
async discoverFragmentIds(messageId) {
|
|
287
|
+
const pending = this.pendingStores.get(messageId);
|
|
288
|
+
if (!pending)
|
|
289
|
+
return [];
|
|
290
|
+
this.pendingStores.delete(messageId);
|
|
291
|
+
const pollInterval = this.config.discoveryPollIntervalMs ?? 3000;
|
|
292
|
+
const timeout = this.config.discoveryTimeoutMs ?? 120000;
|
|
293
|
+
const maxAttempts = Math.ceil(timeout / pollInterval);
|
|
294
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
295
|
+
await new Promise((r) => setTimeout(r, pollInterval));
|
|
296
|
+
try {
|
|
297
|
+
const trace = await this.restCall("GET", `/api/v1/memories/trace/${encodeURIComponent(messageId)}`);
|
|
298
|
+
if (trace.status === "not_found")
|
|
299
|
+
return [];
|
|
300
|
+
if (trace.status === "complete" && trace.all_ids.length > 0) {
|
|
301
|
+
return trace.all_ids;
|
|
302
|
+
}
|
|
303
|
+
// status === "processing" → keep polling
|
|
304
|
+
}
|
|
305
|
+
catch {
|
|
306
|
+
// Trace endpoint may not be available (older image) — keep trying
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return []; // Timeout — fallback writes the messageId anchor to SpiceDB
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Lazy resolution: resolve unmatched SpiceDB anchor UUIDs to actual
|
|
313
|
+
* searchable fragment IDs. Called during recall when viewable fragment IDs
|
|
314
|
+
* from SpiceDB don't match any search results.
|
|
315
|
+
*/
|
|
316
|
+
async resolveAnchors(anchorIds) {
|
|
317
|
+
const result = new Map();
|
|
318
|
+
for (const anchor of anchorIds) {
|
|
319
|
+
try {
|
|
320
|
+
const trace = await this.restCall("GET", `/api/v1/memories/trace/${encodeURIComponent(anchor)}`);
|
|
321
|
+
if (trace.status === "complete" && trace.all_ids.length > 0) {
|
|
322
|
+
result.set(anchor, trace.all_ids);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
catch {
|
|
326
|
+
// anchor may not be a message_id (e.g. already an ObjectId) — skip
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
return result;
|
|
330
|
+
}
|
|
331
|
+
// --------------------------------------------------------------------------
|
|
332
|
+
// Backend-specific CLI commands
|
|
333
|
+
// --------------------------------------------------------------------------
|
|
334
|
+
registerCliCommands(cmd) {
|
|
335
|
+
cmd
|
|
336
|
+
.command("foresight")
|
|
337
|
+
.description("[evermemos] List foresight entries for a group")
|
|
338
|
+
.option("--group <id>", "Group ID")
|
|
339
|
+
.option("--last <n>", "Number of results", "10")
|
|
340
|
+
.action(async (opts) => {
|
|
341
|
+
const groupId = opts.group ?? this.config.defaultGroupId;
|
|
342
|
+
try {
|
|
343
|
+
const body = {
|
|
344
|
+
query: "",
|
|
345
|
+
group_id: groupId,
|
|
346
|
+
top_k: Number(opts.last),
|
|
347
|
+
retrieve_method: "keyword",
|
|
348
|
+
memory_types: ["foresight"],
|
|
349
|
+
};
|
|
350
|
+
const response = await this.restCall("GET", "/api/v1/memories/search", body);
|
|
351
|
+
// Flatten group-keyed response
|
|
352
|
+
const flat = (response.result?.memories ?? [])
|
|
353
|
+
.flatMap((g) => Object.values(g).flat());
|
|
354
|
+
console.log(JSON.stringify(flat, null, 2));
|
|
355
|
+
}
|
|
356
|
+
catch (err) {
|
|
357
|
+
console.error(`Failed to fetch foresight: ${err instanceof Error ? err.message : String(err)}`);
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
cmd
|
|
361
|
+
.command("conversation-meta")
|
|
362
|
+
.description("[evermemos] View conversation metadata for a group")
|
|
363
|
+
.option("--group <id>", "Group ID")
|
|
364
|
+
.action(async (opts) => {
|
|
365
|
+
const groupId = opts.group ?? this.config.defaultGroupId;
|
|
366
|
+
try {
|
|
367
|
+
const queryParams = { group_id: groupId };
|
|
368
|
+
const response = await this.restCall("GET", "/api/v1/memories/conversation-meta", undefined, queryParams);
|
|
369
|
+
console.log(JSON.stringify(response, null, 2));
|
|
370
|
+
}
|
|
371
|
+
catch (err) {
|
|
372
|
+
console.error(`Failed to fetch conversation metadata: ${err instanceof Error ? err.message : String(err)}`);
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
cmd
|
|
376
|
+
.command("clear-memories")
|
|
377
|
+
.description("[evermemos] Clear all memories for a group")
|
|
378
|
+
.option("--group <id...>", "Group ID(s)")
|
|
379
|
+
.option("--confirm", "Required safety flag", false)
|
|
380
|
+
.action(async (opts) => {
|
|
381
|
+
if (!opts.confirm) {
|
|
382
|
+
console.log("Destructive operation. Pass --confirm to proceed.");
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
const groups = opts.group ?? [];
|
|
386
|
+
if (groups.length === 0) {
|
|
387
|
+
console.log("No groups specified. Use --group <id> to specify groups.");
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
for (const g of groups) {
|
|
391
|
+
await this.deleteGroup(g);
|
|
392
|
+
console.log(`Cleared group: ${g}`);
|
|
393
|
+
}
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
// ============================================================================
|
|
398
|
+
// Backend module exports (used by backends/registry.ts)
|
|
399
|
+
// ============================================================================
|
|
400
|
+
import everMemOSDefaults from "./evermemos.defaults.json" with { type: "json" };
|
|
401
|
+
export const defaults = everMemOSDefaults;
|
|
402
|
+
export function create(config) {
|
|
403
|
+
return new EverMemOSBackend(config);
|
|
404
|
+
}
|
package/dist/config.d.ts
CHANGED
|
@@ -18,6 +18,8 @@ export type RebacMemoryConfig = {
|
|
|
18
18
|
subjectId: string;
|
|
19
19
|
/** Maps agent IDs to their owner person IDs (e.g., Slack user IDs). */
|
|
20
20
|
identities: Record<string, string>;
|
|
21
|
+
/** Maps group IDs to their owner person IDs (for admin-level sharing). */
|
|
22
|
+
groupOwners: Record<string, string[]>;
|
|
21
23
|
autoCapture: boolean;
|
|
22
24
|
autoRecall: boolean;
|
|
23
25
|
maxCaptureMessages: number;
|
package/dist/config.js
CHANGED
|
@@ -56,7 +56,7 @@ export const rebacMemoryConfigSchema = {
|
|
|
56
56
|
// Top-level allowed keys: shared keys + the backend name key
|
|
57
57
|
assertAllowedKeys(cfg, [
|
|
58
58
|
"backend", "spicedb",
|
|
59
|
-
"subjectType", "subjectId", "identities",
|
|
59
|
+
"subjectType", "subjectId", "identities", "groupOwners",
|
|
60
60
|
"autoCapture", "autoRecall", "maxCaptureMessages", "sessionFilter",
|
|
61
61
|
backendName,
|
|
62
62
|
], "openclaw-memory-rebac config");
|
|
@@ -79,6 +79,19 @@ export const rebacMemoryConfigSchema = {
|
|
|
79
79
|
}
|
|
80
80
|
}
|
|
81
81
|
}
|
|
82
|
+
// Parse groupOwners: { "slack-engineering": ["U0123", "U0456"] }
|
|
83
|
+
const groupOwnersRaw = cfg.groupOwners;
|
|
84
|
+
const groupOwners = {};
|
|
85
|
+
if (groupOwnersRaw && typeof groupOwnersRaw === "object" && !Array.isArray(groupOwnersRaw)) {
|
|
86
|
+
for (const [groupId, owners] of Object.entries(groupOwnersRaw)) {
|
|
87
|
+
if (Array.isArray(owners)) {
|
|
88
|
+
groupOwners[groupId] = owners.filter((o) => typeof o === "string" && o.trim() !== "");
|
|
89
|
+
}
|
|
90
|
+
else if (typeof owners === "string" && owners.trim()) {
|
|
91
|
+
groupOwners[groupId] = [owners.trim()];
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
82
95
|
return {
|
|
83
96
|
backend: backendName,
|
|
84
97
|
spicedb: {
|
|
@@ -94,6 +107,7 @@ export const rebacMemoryConfigSchema = {
|
|
|
94
107
|
subjectType,
|
|
95
108
|
subjectId,
|
|
96
109
|
identities,
|
|
110
|
+
groupOwners,
|
|
97
111
|
autoCapture: cfg.autoCapture !== false,
|
|
98
112
|
autoRecall: cfg.autoRecall !== false,
|
|
99
113
|
maxCaptureMessages: typeof cfg.maxCaptureMessages === "number" && cfg.maxCaptureMessages > 0
|
package/dist/index.d.ts
CHANGED
|
@@ -16,6 +16,12 @@
|
|
|
16
16
|
* falling back to config-level subjectType/subjectId when agentId is absent.
|
|
17
17
|
*/
|
|
18
18
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
19
|
+
/**
|
|
20
|
+
* Strip OpenClaw envelope metadata from message text before Graphiti ingestion.
|
|
21
|
+
* Removes channel headers, sender/message-id meta lines, and memory injection
|
|
22
|
+
* blocks that would pollute entity extraction.
|
|
23
|
+
*/
|
|
24
|
+
export declare function stripEnvelopeMetadata(text: string): string;
|
|
19
25
|
declare const rebacMemoryPlugin: {
|
|
20
26
|
id: string;
|
|
21
27
|
name: string;
|