@contextableai/openclaw-memory-rebac 0.1.0 → 0.1.2

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/search.ts DELETED
@@ -1,139 +0,0 @@
1
- /**
2
- * Parallel Multi-Group Search + Merge/Re-rank
3
- *
4
- * Backend-agnostic: delegates per-group search to MemoryBackend.searchGroup().
5
- * Issues parallel calls (one per authorized group_id), merges results,
6
- * deduplicates by UUID, and re-ranks by score then recency.
7
- */
8
-
9
- import type { MemoryBackend, SearchResult } from "./backend.js";
10
-
11
- export type { SearchResult };
12
-
13
- // ============================================================================
14
- // Search options
15
- // ============================================================================
16
-
17
- export type SearchOptions = {
18
- query: string;
19
- groupIds: string[];
20
- limit?: number;
21
- sessionId?: string;
22
- };
23
-
24
- // ============================================================================
25
- // Search
26
- // ============================================================================
27
-
28
- /**
29
- * Search across multiple authorized group_ids in parallel.
30
- * Merges and deduplicates results, returning up to `limit` items sorted by
31
- * score (desc) then recency (desc).
32
- */
33
- export async function searchAuthorizedMemories(
34
- backend: MemoryBackend,
35
- options: SearchOptions,
36
- ): Promise<SearchResult[]> {
37
- const { query, groupIds, limit = 10, sessionId } = options;
38
-
39
- if (groupIds.length === 0) {
40
- return [];
41
- }
42
-
43
- // Fan out parallel searches across all authorized groups
44
- const promises = groupIds.map((groupId) =>
45
- backend.searchGroup({ query, groupId, limit, sessionId }),
46
- );
47
-
48
- const resultSets = await Promise.allSettled(promises);
49
-
50
- // Collect all successful results — silently skip failed group searches
51
- const allResults: SearchResult[] = [];
52
- for (const result of resultSets) {
53
- if (result.status === "fulfilled") {
54
- allResults.push(...result.value);
55
- }
56
- }
57
-
58
- // Deduplicate by UUID
59
- const seen = new Set<string>();
60
- const deduped = allResults.filter((r) => {
61
- if (seen.has(r.uuid)) return false;
62
- seen.add(r.uuid);
63
- return true;
64
- });
65
-
66
- // Sort: score descending (when available), then recency descending
67
- deduped.sort((a, b) => {
68
- if (a.score !== undefined && b.score !== undefined && a.score !== b.score) {
69
- return b.score - a.score;
70
- }
71
- const dateA = new Date(a.created_at).getTime();
72
- const dateB = new Date(b.created_at).getTime();
73
- return dateB - dateA;
74
- });
75
-
76
- return deduped.slice(0, limit);
77
- }
78
-
79
- // ============================================================================
80
- // Format for agent context
81
- // ============================================================================
82
-
83
- /**
84
- * Format search results into a text block for injecting into agent context.
85
- */
86
- export function formatResultsForContext(results: SearchResult[]): string {
87
- if (results.length === 0) return "";
88
- return results.map((r, i) => formatResultLine(r, i + 1)).join("\n");
89
- }
90
-
91
- /**
92
- * Format results with long-term and session sections separated.
93
- * Session group_ids start with "session-".
94
- */
95
- export function formatDualResults(
96
- longTermResults: SearchResult[],
97
- sessionResults: SearchResult[],
98
- ): string {
99
- const parts: string[] = [];
100
- let idx = 1;
101
-
102
- for (const r of longTermResults) {
103
- parts.push(formatResultLine(r, idx++));
104
- }
105
-
106
- if (sessionResults.length > 0) {
107
- if (longTermResults.length > 0) parts.push("Session memories:");
108
- for (const r of sessionResults) {
109
- parts.push(formatResultLine(r, idx++));
110
- }
111
- }
112
-
113
- return parts.join("\n");
114
- }
115
-
116
- /**
117
- * Format a single search result line with type-prefixed UUID.
118
- * e.g. "[fact:da8650cb-...] Eric's birthday is Dec 17th (Eric -[HAS_BIRTHDAY]→ Dec 17th)"
119
- */
120
- function formatResultLine(r: SearchResult, idx: number): string {
121
- const typeLabel =
122
- r.type === "node" ? "entity" :
123
- r.type === "fact" ? "fact" :
124
- r.type === "chunk" ? "chunk" :
125
- r.type === "summary" ? "summary" :
126
- "completion";
127
- return `${idx}. [${typeLabel}:${r.uuid}] ${r.summary} (${r.context})`;
128
- }
129
-
130
- /**
131
- * Deduplicate session results against long-term results (by UUID).
132
- */
133
- export function deduplicateSessionResults(
134
- longTermResults: SearchResult[],
135
- sessionResults: SearchResult[],
136
- ): SearchResult[] {
137
- const longTermIds = new Set(longTermResults.map((r) => r.uuid));
138
- return sessionResults.filter((r) => !longTermIds.has(r.uuid));
139
- }
package/spicedb.ts DELETED
@@ -1,355 +0,0 @@
1
- /**
2
- * SpiceDB Client Wrapper
3
- *
4
- * Wraps @authzed/authzed-node for authorization operations:
5
- * WriteSchema, WriteRelationships, DeleteRelationships, BulkImportRelationships,
6
- * LookupResources, CheckPermission.
7
- */
8
-
9
- import { v1 } from "@authzed/authzed-node";
10
-
11
- // ============================================================================
12
- // Types
13
- // ============================================================================
14
-
15
- export type SpiceDbConfig = {
16
- endpoint: string;
17
- token: string;
18
- insecure: boolean;
19
- };
20
-
21
- export type RelationshipTuple = {
22
- resourceType: string;
23
- resourceId: string;
24
- relation: string;
25
- subjectType: string;
26
- subjectId: string;
27
- };
28
-
29
- export type ConsistencyMode =
30
- | { mode: "full" }
31
- | { mode: "at_least_as_fresh"; token: string }
32
- | { mode: "minimize_latency" };
33
-
34
- // ============================================================================
35
- // Client
36
- // ============================================================================
37
-
38
- export class SpiceDbClient {
39
- private client: ReturnType<typeof v1.NewClient>;
40
- private promises: ReturnType<typeof v1.NewClient>["promises"];
41
-
42
- constructor(config: SpiceDbConfig) {
43
- if (config.insecure) {
44
- this.client = v1.NewClient(
45
- config.token,
46
- config.endpoint,
47
- v1.ClientSecurity.INSECURE_PLAINTEXT_CREDENTIALS,
48
- );
49
- } else {
50
- this.client = v1.NewClient(config.token, config.endpoint);
51
- }
52
- this.promises = this.client.promises;
53
- }
54
-
55
- // --------------------------------------------------------------------------
56
- // Schema
57
- // --------------------------------------------------------------------------
58
-
59
- async writeSchema(schema: string): Promise<void> {
60
- const request = v1.WriteSchemaRequest.create({ schema });
61
- await this.promises.writeSchema(request);
62
- }
63
-
64
- async readSchema(): Promise<string> {
65
- const request = v1.ReadSchemaRequest.create({});
66
- const response = await this.promises.readSchema(request);
67
- return response.schemaText;
68
- }
69
-
70
- // --------------------------------------------------------------------------
71
- // Relationships
72
- // --------------------------------------------------------------------------
73
-
74
- async writeRelationships(tuples: RelationshipTuple[]): Promise<string | undefined> {
75
- const updates = tuples.map((t) =>
76
- v1.RelationshipUpdate.create({
77
- operation: v1.RelationshipUpdate_Operation.TOUCH,
78
- relationship: v1.Relationship.create({
79
- resource: v1.ObjectReference.create({
80
- objectType: t.resourceType,
81
- objectId: t.resourceId,
82
- }),
83
- relation: t.relation,
84
- subject: v1.SubjectReference.create({
85
- object: v1.ObjectReference.create({
86
- objectType: t.subjectType,
87
- objectId: t.subjectId,
88
- }),
89
- }),
90
- }),
91
- }),
92
- );
93
-
94
- const request = v1.WriteRelationshipsRequest.create({ updates });
95
- const response = await this.promises.writeRelationships(request);
96
- return response.writtenAt?.token;
97
- }
98
-
99
- async deleteRelationships(tuples: RelationshipTuple[]): Promise<void> {
100
- const updates = tuples.map((t) =>
101
- v1.RelationshipUpdate.create({
102
- operation: v1.RelationshipUpdate_Operation.DELETE,
103
- relationship: v1.Relationship.create({
104
- resource: v1.ObjectReference.create({
105
- objectType: t.resourceType,
106
- objectId: t.resourceId,
107
- }),
108
- relation: t.relation,
109
- subject: v1.SubjectReference.create({
110
- object: v1.ObjectReference.create({
111
- objectType: t.subjectType,
112
- objectId: t.subjectId,
113
- }),
114
- }),
115
- }),
116
- }),
117
- );
118
-
119
- const request = v1.WriteRelationshipsRequest.create({ updates });
120
- await this.promises.writeRelationships(request);
121
- }
122
-
123
- async deleteRelationshipsByFilter(params: {
124
- resourceType: string;
125
- resourceId: string;
126
- relation?: string;
127
- }): Promise<string | undefined> {
128
- const request = v1.DeleteRelationshipsRequest.create({
129
- relationshipFilter: v1.RelationshipFilter.create({
130
- resourceType: params.resourceType,
131
- optionalResourceId: params.resourceId,
132
- ...(params.relation ? { optionalRelation: params.relation } : {}),
133
- }),
134
- });
135
-
136
- const response = await this.promises.deleteRelationships(request);
137
- return response.deletedAt?.token;
138
- }
139
-
140
- // --------------------------------------------------------------------------
141
- // Bulk Import
142
- // --------------------------------------------------------------------------
143
-
144
- private toRelationship(t: RelationshipTuple) {
145
- return v1.Relationship.create({
146
- resource: v1.ObjectReference.create({
147
- objectType: t.resourceType,
148
- objectId: t.resourceId,
149
- }),
150
- relation: t.relation,
151
- subject: v1.SubjectReference.create({
152
- object: v1.ObjectReference.create({
153
- objectType: t.subjectType,
154
- objectId: t.subjectId,
155
- }),
156
- }),
157
- });
158
- }
159
-
160
- /**
161
- * Bulk import relationships using the streaming ImportBulkRelationships RPC.
162
- * More efficient than individual writeRelationships calls for large batches.
163
- * Falls back to batched writeRelationships if the streaming RPC is unavailable.
164
- */
165
- async bulkImportRelationships(
166
- tuples: RelationshipTuple[],
167
- batchSize = 1000,
168
- ): Promise<number> {
169
- if (tuples.length === 0) return 0;
170
-
171
- // Try streaming bulk import first
172
- if (typeof this.promises.bulkImportRelationships === "function") {
173
- return this.bulkImportViaStream(tuples, batchSize);
174
- }
175
-
176
- // Fallback: batched writeRelationships
177
- return this.bulkImportViaWrite(tuples, batchSize);
178
- }
179
-
180
- private bulkImportViaStream(
181
- tuples: RelationshipTuple[],
182
- batchSize: number,
183
- ): Promise<number> {
184
- return new Promise((resolve, reject) => {
185
- const stream = this.promises.bulkImportRelationships(
186
- (err: Error | null, response?: { numLoaded?: string }) => {
187
- if (err) reject(err);
188
- else resolve(Number(response?.numLoaded ?? tuples.length));
189
- },
190
- );
191
-
192
- stream.on("error", (err: Error) => {
193
- reject(err);
194
- });
195
-
196
- for (let i = 0; i < tuples.length; i += batchSize) {
197
- const chunk = tuples.slice(i, i + batchSize);
198
- stream.write(
199
- v1.BulkImportRelationshipsRequest.create({
200
- relationships: chunk.map((t) => this.toRelationship(t)),
201
- }),
202
- );
203
- }
204
-
205
- stream.end();
206
- });
207
- }
208
-
209
- private async bulkImportViaWrite(
210
- tuples: RelationshipTuple[],
211
- batchSize: number,
212
- ): Promise<number> {
213
- let total = 0;
214
- for (let i = 0; i < tuples.length; i += batchSize) {
215
- const chunk = tuples.slice(i, i + batchSize);
216
- await this.writeRelationships(chunk);
217
- total += chunk.length;
218
- }
219
- return total;
220
- }
221
-
222
- // --------------------------------------------------------------------------
223
- // Read Relationships
224
- // --------------------------------------------------------------------------
225
-
226
- /**
227
- * Read relationships matching a filter. Returns all tuples that match the
228
- * specified resource type, optional resource ID, optional relation, and
229
- * optional subject filter. Used by the cleanup command to find which
230
- * Graphiti episodes have SpiceDB authorization relationships.
231
- */
232
- async readRelationships(params: {
233
- resourceType: string;
234
- resourceId?: string;
235
- relation?: string;
236
- subjectType?: string;
237
- subjectId?: string;
238
- consistency?: ConsistencyMode;
239
- }): Promise<RelationshipTuple[]> {
240
- const filterFields: Record<string, unknown> = {
241
- resourceType: params.resourceType,
242
- };
243
- if (params.resourceId) {
244
- filterFields.optionalResourceId = params.resourceId;
245
- }
246
- if (params.relation) {
247
- filterFields.optionalRelation = params.relation;
248
- }
249
- if (params.subjectType) {
250
- const subjectFilter: Record<string, unknown> = {
251
- subjectType: params.subjectType,
252
- };
253
- if (params.subjectId) {
254
- subjectFilter.optionalSubjectId = params.subjectId;
255
- }
256
- filterFields.optionalSubjectFilter = v1.SubjectFilter.create(subjectFilter);
257
- }
258
-
259
- const request = v1.ReadRelationshipsRequest.create({
260
- relationshipFilter: v1.RelationshipFilter.create(filterFields),
261
- consistency: this.buildConsistency(params.consistency),
262
- });
263
-
264
- const results = await this.promises.readRelationships(request);
265
- const tuples: RelationshipTuple[] = [];
266
- for (const r of results) {
267
- const rel = r.relationship;
268
- if (!rel?.resource || !rel.subject?.object) continue;
269
- tuples.push({
270
- resourceType: rel.resource.objectType,
271
- resourceId: rel.resource.objectId,
272
- relation: rel.relation,
273
- subjectType: rel.subject.object.objectType,
274
- subjectId: rel.subject.object.objectId,
275
- });
276
- }
277
- return tuples;
278
- }
279
-
280
- // --------------------------------------------------------------------------
281
- // Permissions
282
- // --------------------------------------------------------------------------
283
-
284
- private buildConsistency(mode?: ConsistencyMode) {
285
- if (!mode || mode.mode === "minimize_latency") {
286
- return v1.Consistency.create({
287
- requirement: { oneofKind: "minimizeLatency", minimizeLatency: true },
288
- });
289
- }
290
- if (mode.mode === "at_least_as_fresh") {
291
- return v1.Consistency.create({
292
- requirement: {
293
- oneofKind: "atLeastAsFresh",
294
- atLeastAsFresh: v1.ZedToken.create({ token: mode.token }),
295
- },
296
- });
297
- }
298
- return v1.Consistency.create({
299
- requirement: { oneofKind: "fullyConsistent", fullyConsistent: true },
300
- });
301
- }
302
-
303
- async checkPermission(params: {
304
- resourceType: string;
305
- resourceId: string;
306
- permission: string;
307
- subjectType: string;
308
- subjectId: string;
309
- consistency?: ConsistencyMode;
310
- }): Promise<boolean> {
311
- const request = v1.CheckPermissionRequest.create({
312
- resource: v1.ObjectReference.create({
313
- objectType: params.resourceType,
314
- objectId: params.resourceId,
315
- }),
316
- permission: params.permission,
317
- subject: v1.SubjectReference.create({
318
- object: v1.ObjectReference.create({
319
- objectType: params.subjectType,
320
- objectId: params.subjectId,
321
- }),
322
- }),
323
- consistency: this.buildConsistency(params.consistency),
324
- });
325
-
326
- const response = await this.promises.checkPermission(request);
327
- return (
328
- response.permissionship ===
329
- v1.CheckPermissionResponse_Permissionship.HAS_PERMISSION
330
- );
331
- }
332
-
333
- async lookupResources(params: {
334
- resourceType: string;
335
- permission: string;
336
- subjectType: string;
337
- subjectId: string;
338
- consistency?: ConsistencyMode;
339
- }): Promise<string[]> {
340
- const request = v1.LookupResourcesRequest.create({
341
- resourceObjectType: params.resourceType,
342
- permission: params.permission,
343
- subject: v1.SubjectReference.create({
344
- object: v1.ObjectReference.create({
345
- objectType: params.subjectType,
346
- objectId: params.subjectId,
347
- }),
348
- }),
349
- consistency: this.buildConsistency(params.consistency),
350
- });
351
-
352
- const results = await this.promises.lookupResources(request);
353
- return results.map((r: { resourceObjectId: string }) => r.resourceObjectId);
354
- }
355
- }