@totalreclaw/totalreclaw 1.6.0 → 3.0.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.
@@ -21,6 +21,7 @@
21
21
  */
22
22
 
23
23
  import { getSubgraphConfig } from './subgraph-store.js';
24
+ import { CONFIG } from './config.js';
24
25
 
25
26
  export interface SubgraphSearchFact {
26
27
  id: string;
@@ -32,9 +33,9 @@ export interface SubgraphSearchFact {
32
33
  }
33
34
 
34
35
  /** Batch size for Phase 2 split queries. */
35
- const TRAPDOOR_BATCH_SIZE = parseInt(process.env.TOTALRECLAW_TRAPDOOR_BATCH_SIZE ?? '5', 10);
36
+ const TRAPDOOR_BATCH_SIZE = CONFIG.trapdoorBatchSize;
36
37
  /** Graph Studio / Graph Network hard limit on `first` argument. */
37
- const PAGE_SIZE = parseInt(process.env.TOTALRECLAW_SUBGRAPH_PAGE_SIZE ?? '1000', 10);
38
+ const PAGE_SIZE = CONFIG.pageSize;
38
39
 
39
40
  /**
40
41
  * Execute a single GraphQL query against the subgraph endpoint.
@@ -59,10 +60,18 @@ async function gqlQuery<T>(
59
60
  headers,
60
61
  body: JSON.stringify({ query, variables }),
61
62
  });
62
- if (!response.ok) return null;
63
- const json = await response.json() as { data?: T };
63
+ if (!response.ok) {
64
+ const body = await response.text().catch(() => '');
65
+ console.error(`[TotalReclaw] Subgraph query failed: HTTP ${response.status} — ${body.slice(0, 200)}`);
66
+ return null;
67
+ }
68
+ const json = await response.json() as { data?: T; error?: string; errors?: Array<{ message: string }> };
69
+ if (json.error || json.errors) {
70
+ console.error(`[TotalReclaw] Subgraph query error: ${json.error || json.errors?.map(e => e.message).join('; ')}`);
71
+ }
64
72
  return json.data ?? null;
65
- } catch {
73
+ } catch (err) {
74
+ console.error(`[TotalReclaw] Subgraph query exception: ${err instanceof Error ? err.message : String(err)}`);
66
75
  return null;
67
76
  }
68
77
  }
@@ -83,6 +92,7 @@ const SEARCH_QUERY = `
83
92
  encryptedEmbedding
84
93
  decayScore
85
94
  timestamp
95
+ createdAt
86
96
  isActive
87
97
  contentFp
88
98
  sequenceId
@@ -107,6 +117,7 @@ const PAGINATE_QUERY = `
107
117
  encryptedBlob
108
118
  encryptedEmbedding
109
119
  timestamp
120
+ createdAt
110
121
  decayScore
111
122
  isActive
112
123
  contentFp
@@ -249,6 +260,102 @@ export async function searchSubgraph(
249
260
  return Array.from(allResults.values());
250
261
  }
251
262
 
263
+ /**
264
+ * Broadened search: fetch recent active facts by owner without trapdoor filtering.
265
+ * Used as a fallback when trapdoor search returns 0 candidates (e.g., vague queries
266
+ * like "who am I?" where word trapdoors don't overlap with stored fact tokens).
267
+ */
268
+ export async function searchSubgraphBroadened(
269
+ owner: string,
270
+ maxCandidates: number,
271
+ authKeyHex?: string,
272
+ ): Promise<SubgraphSearchFact[]> {
273
+ const config = getSubgraphConfig();
274
+ const subgraphUrl = `${config.relayUrl}/v1/subgraph`;
275
+
276
+ const query = `
277
+ query BroadenedSearch($owner: Bytes!, $first: Int!) {
278
+ facts(
279
+ where: { owner: $owner, isActive: true }
280
+ first: $first
281
+ orderBy: timestamp
282
+ orderDirection: desc
283
+ ) {
284
+ id
285
+ encryptedBlob
286
+ encryptedEmbedding
287
+ decayScore
288
+ timestamp
289
+ createdAt
290
+ isActive
291
+ contentFp
292
+ sequenceId
293
+ version
294
+ }
295
+ }
296
+ `;
297
+
298
+ const data = await gqlQuery<{ facts?: SubgraphSearchFact[] }>(
299
+ subgraphUrl,
300
+ query,
301
+ { owner, first: Math.min(maxCandidates, 1000) },
302
+ authKeyHex,
303
+ );
304
+
305
+ return (data?.facts ?? []).filter(f => f.isActive !== false);
306
+ }
307
+
308
+ /**
309
+ * Fetch a single fact by its client-generated UUID.
310
+ *
311
+ * Used by the pin/unpin tools to retrieve a known fact's encrypted blob and
312
+ * metadata for re-encryption with an updated status. Returns null if the fact
313
+ * is not found, has been tombstoned (isActive=false), or the owner does not
314
+ * match the caller's Smart Account address (defense against stale IDs from
315
+ * another user's recall results).
316
+ */
317
+ export async function fetchFactById(
318
+ owner: string,
319
+ factId: string,
320
+ authKeyHex?: string,
321
+ ): Promise<(SubgraphSearchFact & { owner: string }) | null> {
322
+ const config = getSubgraphConfig();
323
+ const subgraphUrl = `${config.relayUrl}/v1/subgraph`;
324
+
325
+ const query = `
326
+ query GetFactById($id: ID!) {
327
+ fact(id: $id) {
328
+ id
329
+ owner
330
+ encryptedBlob
331
+ encryptedEmbedding
332
+ decayScore
333
+ timestamp
334
+ createdAt
335
+ isActive
336
+ contentFp
337
+ sequenceId
338
+ version
339
+ }
340
+ }
341
+ `;
342
+
343
+ const data = await gqlQuery<{ fact?: (SubgraphSearchFact & { owner: string }) | null }>(
344
+ subgraphUrl,
345
+ query,
346
+ { id: factId },
347
+ authKeyHex,
348
+ );
349
+
350
+ const fact = data?.fact;
351
+ if (!fact) return null;
352
+ if (fact.isActive === false) return null;
353
+ if (typeof fact.owner === 'string' && fact.owner.toLowerCase() !== owner.toLowerCase()) {
354
+ return null;
355
+ }
356
+ return fact;
357
+ }
358
+
252
359
  /**
253
360
  * Get fact count from the subgraph for dynamic pool sizing.
254
361
  * Uses the globalStates entity for a lightweight single-row lookup