@totalreclaw/totalreclaw 1.2.0 → 1.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 CHANGED
@@ -79,9 +79,9 @@ Most of the time you won't use these directly -- the automatic hooks handle memo
79
79
  | Tier | Memories | Reads | Storage | Price |
80
80
  |------|----------|-------|---------|-------|
81
81
  | **Free** | 500/month | Unlimited | Testnet (trial) | $0 |
82
- | **Pro** | Unlimited | Unlimited | Permanent on-chain (Gnosis) | $5/month |
82
+ | **Pro** | Unlimited | Unlimited | Permanent on-chain (Gnosis) | See `totalreclaw_status` |
83
83
 
84
- Pay with card via Stripe. Counter resets monthly.
84
+ Pay with card via Stripe. Use `totalreclaw_status` to check current pricing. Counter resets monthly.
85
85
 
86
86
  ## Using with Other Agents
87
87
 
package/crypto.ts CHANGED
@@ -89,7 +89,7 @@ function deriveKeysFromMnemonic(
89
89
  }
90
90
 
91
91
  /**
92
- * Derive auth, encryption, and dedup keys from a master password.
92
+ * Derive auth, encryption, and dedup keys from a recovery phrase.
93
93
  *
94
94
  * If the password is a valid BIP-39 mnemonic (12 or 24 words), keys are
95
95
  * derived from the 512-bit BIP-39 seed via HKDF. Otherwise, the legacy
@@ -57,7 +57,7 @@ function parseFactsResponse(response: string): ExtractedFact[] {
57
57
  : 'ADD';
58
58
  return {
59
59
  text: String(fact.text).slice(0, 512),
60
- type: (['fact', 'preference', 'decision', 'episodic', 'goal'].includes(String(fact.type))
60
+ type: (['fact', 'preference', 'decision', 'episodic', 'goal', 'context', 'summary'].includes(String(fact.type))
61
61
  ? String(fact.type)
62
62
  : 'fact') as ExtractedFact['type'],
63
63
  importance: Math.max(1, Math.min(10, Number(fact.importance) || 5)),
package/extractor.ts CHANGED
@@ -15,7 +15,7 @@ export type ExtractionAction = 'ADD' | 'UPDATE' | 'DELETE' | 'NOOP';
15
15
 
16
16
  export interface ExtractedFact {
17
17
  text: string;
18
- type: 'fact' | 'preference' | 'decision' | 'episodic' | 'goal';
18
+ type: 'fact' | 'preference' | 'decision' | 'episodic' | 'goal' | 'context' | 'summary';
19
19
  importance: number; // 1-10
20
20
  action: ExtractionAction;
21
21
  existingFactId?: string;
@@ -37,28 +37,36 @@ interface ConversationMessage {
37
37
  // Extraction Prompt
38
38
  // ---------------------------------------------------------------------------
39
39
 
40
- const EXTRACTION_SYSTEM_PROMPT = `You are a memory extraction engine. Analyze the conversation and extract atomic facts worth remembering long-term.
40
+ const EXTRACTION_SYSTEM_PROMPT = `You are a memory extraction engine. Analyze the conversation and extract valuable long-term memories.
41
41
 
42
42
  Rules:
43
- 1. Each fact must be a single, atomic piece of information
44
- 2. Focus on user-specific information: preferences, decisions, facts about them, their goals
45
- 3. Skip generic knowledge, greetings, and small talk
46
- 4. Skip information that is only relevant to the current conversation
47
- 5. Score importance 1-10 (7+ = worth storing, below 7 = skip)
48
- 6. Only extract facts with importance >= 6
43
+ 1. Each memory must be a single, self-contained piece of information
44
+ 2. Focus on user-specific information that would be useful in future conversations
45
+ 3. Skip generic knowledge, greetings, small talk, and ephemeral task coordination
46
+ 4. Score importance 1-10 (6+ = worth storing)
47
+ 5. Only extract memories with importance >= 6
49
48
 
50
49
  Types:
51
- - fact: Objective information about the user
52
- - preference: Likes, dislikes, or preferences
53
- - decision: Choices the user has made
54
- - episodic: Events or experiences
55
- - goal: Objectives or targets
50
+ - fact: Objective information about the user (name, location, job, relationships)
51
+ - preference: Likes, dislikes, or preferences ("prefers dark mode", "allergic to peanuts")
52
+ - decision: Choices WITH reasoning ("chose PostgreSQL because data is relational and needs ACID")
53
+ - episodic: Notable events or experiences ("deployed v1.0 to production on March 15")
54
+ - goal: Objectives, targets, or plans ("wants to launch public beta by end of Q1")
55
+ - context: Active project/task context ("working on TotalReclaw v1.2, staging on Base Sepolia")
56
+ - summary: Key outcome or conclusion from a discussion ("agreed to use phased rollout for migration")
57
+
58
+ Extraction guidance:
59
+ - For decisions: ALWAYS include the reasoning. "Chose X" is weak. "Chose X because Y" is strong.
60
+ - For context: Capture what the user is actively working on, including versions, environments, and status.
61
+ - For summaries: Only extract when a conversation reaches a clear conclusion or agreement.
62
+ - For facts: Prefer specific over vague. "Lives in Lisbon" beats "lives in Europe".
63
+ - Decisions and context should be importance >= 7 (they are high-value for future conversations).
56
64
 
57
65
  Actions (compare against existing memories if provided):
58
- - ADD: New fact, no conflict with existing memories
59
- - UPDATE: Modifies or refines an existing memory (provide existingFactId)
60
- - DELETE: Contradicts an existing memory the old one is now wrong (provide existingFactId)
61
- - NOOP: Already captured in existing memories or not worth storing
66
+ - ADD: New memory, no conflict with existing
67
+ - UPDATE: Refines or corrects an existing memory (provide existingFactId)
68
+ - DELETE: Contradicts an existing memory -- the old one is now wrong (provide existingFactId)
69
+ - NOOP: Already captured or not worth storing
62
70
 
63
71
  Return a JSON array (no markdown, no code fences):
64
72
  [{"text": "...", "type": "...", "importance": N, "action": "ADD|UPDATE|DELETE|NOOP", "existingFactId": "..."}, ...]
@@ -158,7 +166,7 @@ function parseFactsResponse(response: string): ExtractedFact[] {
158
166
  : 'ADD'; // Default to ADD for backward compatibility
159
167
  return {
160
168
  text: String(fact.text).slice(0, 512),
161
- type: (['fact', 'preference', 'decision', 'episodic', 'goal'].includes(String(fact.type))
169
+ type: (['fact', 'preference', 'decision', 'episodic', 'goal', 'context', 'summary'].includes(String(fact.type))
162
170
  ? String(fact.type)
163
171
  : 'fact') as ExtractedFact['type'],
164
172
  importance: Math.max(1, Math.min(10, Number(fact.importance) || 5)),
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env npx tsx
2
2
  /**
3
- * Generate a BIP-39 12-word mnemonic for use as TOTALRECLAW_MASTER_PASSWORD.
3
+ * Generate a BIP-39 12-word mnemonic for use as TOTALRECLAW_RECOVERY_PHRASE.
4
4
  *
5
5
  * Usage: npx tsx generate-mnemonic.ts
6
6
  */
@@ -8,7 +8,7 @@ import { generateMnemonic } from '@scure/bip39';
8
8
  import { wordlist } from '@scure/bip39/wordlists/english.js';
9
9
 
10
10
  const mnemonic = generateMnemonic(wordlist, 128);
11
- console.log('\n Your TotalReclaw master mnemonic (12 words):\n');
11
+ console.log('\n Your TotalReclaw recovery phrase (12 words):\n');
12
12
  console.log(` ${mnemonic}\n`);
13
13
  console.log(' WRITE THIS DOWN. If you lose it, your memories are unrecoverable.');
14
- console.log(' Set it as TOTALRECLAW_MASTER_PASSWORD in your .env file.\n');
14
+ console.log(' Set it as TOTALRECLAW_RECOVERY_PHRASE in your .env file.\n');
@@ -44,7 +44,7 @@ export abstract class BaseImportAdapter {
44
44
  const text = fact.text.trim().slice(0, 512);
45
45
 
46
46
  // Normalize type
47
- const validTypes = ['fact', 'preference', 'decision', 'episodic', 'goal'] as const;
47
+ const validTypes = ['fact', 'preference', 'decision', 'episodic', 'goal', 'context', 'summary'] as const;
48
48
  const type = validTypes.includes(fact.type as typeof validTypes[number])
49
49
  ? (fact.type as NormalizedFact['type'])
50
50
  : 'fact';
@@ -453,7 +453,7 @@ async function runTests(): Promise<void> {
453
453
 
454
454
  // --- type normalization ---
455
455
  {
456
- const validTypes = ['fact', 'preference', 'decision', 'episodic', 'goal'] as const;
456
+ const validTypes = ['fact', 'preference', 'decision', 'episodic', 'goal', 'context', 'summary'] as const;
457
457
 
458
458
  for (const t of validTypes) {
459
459
  const result = testAdapter.testValidateFact({ text: 'test fact', type: t });
@@ -6,7 +6,7 @@ export interface NormalizedFact {
6
6
  /** The atomic fact text (max 512 chars) */
7
7
  text: string;
8
8
  /** Fact type matching TotalReclaw's taxonomy */
9
- type: 'fact' | 'preference' | 'decision' | 'episodic' | 'goal';
9
+ type: 'fact' | 'preference' | 'decision' | 'episodic' | 'goal' | 'context' | 'summary';
10
10
  /** Importance score 1-10 */
11
11
  importance: number;
12
12
  /** Original source system */
package/index.ts CHANGED
@@ -9,6 +9,7 @@
9
9
  * - totalreclaw_status -- check billing/subscription status
10
10
  * - totalreclaw_consolidate -- scan and merge near-duplicate memories
11
11
  * - totalreclaw_import_from -- import memories from other tools (Mem0, MCP Memory, etc.)
12
+ * - totalreclaw_upgrade -- create Stripe checkout for Pro upgrade
12
13
  *
13
14
  * Also registers a `before_agent_start` hook that automatically injects
14
15
  * relevant memories into the agent's context.
@@ -41,7 +42,7 @@ import {
41
42
  STORE_DEDUP_MAX_CANDIDATES,
42
43
  type DecryptedCandidate,
43
44
  } from './consolidation.js';
44
- import { isSubgraphMode, getSubgraphConfig, encodeFactProtobuf, submitFactOnChain, deriveSmartAccountAddress, type FactPayload } from './subgraph-store.js';
45
+ import { isSubgraphMode, getSubgraphConfig, encodeFactProtobuf, submitFactOnChain, submitFactBatchOnChain, deriveSmartAccountAddress, type FactPayload } from './subgraph-store.js';
45
46
  import { searchSubgraph, getSubgraphFactCount } from './subgraph-search.js';
46
47
  import { PluginHotCache, type HotFact } from './hot-cache-wrapper.js';
47
48
  import crypto from 'node:crypto';
@@ -134,6 +135,9 @@ const MAX_FACTS_PER_EXTRACTION = 15;
134
135
  // Store-time near-duplicate detection (consolidation module)
135
136
  const STORE_DEDUP_ENABLED = process.env.TOTALRECLAW_STORE_DEDUP !== 'false';
136
137
 
138
+ // One-time welcome-back message for returning Pro users (set during init, consumed by first before_agent_start)
139
+ let welcomeBackMessage: string | null = null;
140
+
137
141
  // B2: Minimum relevance threshold — cosine below this means no memory injection
138
142
  const RELEVANCE_THRESHOLD = parseFloat(process.env.TOTALRECLAW_RELEVANCE_THRESHOLD ?? '0.3');
139
143
 
@@ -142,7 +146,7 @@ const RELEVANCE_THRESHOLD = parseFloat(process.env.TOTALRECLAW_RELEVANCE_THRESHO
142
146
  // ---------------------------------------------------------------------------
143
147
 
144
148
  const BILLING_CACHE_PATH = path.join(process.env.HOME ?? '/home/node', '.totalreclaw', 'billing-cache.json');
145
- const BILLING_CACHE_TTL = 12 * 60 * 60 * 1000; // 12 hours
149
+ const BILLING_CACHE_TTL = 2 * 60 * 60 * 1000; // 2 hours
146
150
  const QUOTA_WARNING_THRESHOLD = 0.8; // 80%
147
151
 
148
152
  interface BillingCache {
@@ -153,6 +157,9 @@ interface BillingCache {
153
157
  llm_dedup?: boolean;
154
158
  custom_extract_interval?: boolean;
155
159
  min_extract_interval?: number;
160
+ extraction_interval?: number;
161
+ max_facts_per_extraction?: number;
162
+ max_candidate_pool?: number;
156
163
  };
157
164
  checked_at: number;
158
165
  }
@@ -192,12 +199,25 @@ function isLlmDedupEnabled(): boolean {
192
199
 
193
200
  /**
194
201
  * Get the effective extraction interval.
195
- * Unified to 3 turns for all tiers (quota is per-transaction, not per-memory).
202
+ * Server-side config takes priority (from billing cache), then env var fallback.
203
+ * This allows the relay admin to tune extraction without an npm publish.
196
204
  */
197
205
  function getExtractInterval(): number {
206
+ const cache = readBillingCache();
207
+ if (cache?.features?.extraction_interval != null) return cache.features.extraction_interval;
198
208
  return AUTO_EXTRACT_EVERY_TURNS_ENV;
199
209
  }
200
210
 
211
+ /**
212
+ * Get the max facts per extraction cycle.
213
+ * Server-side config takes priority (from billing cache), then env var / constant fallback.
214
+ */
215
+ function getMaxFactsPerExtraction(): number {
216
+ const cache = readBillingCache();
217
+ if (cache?.features?.max_facts_per_extraction != null) return cache.features.max_facts_per_extraction;
218
+ return MAX_FACTS_PER_EXTRACTION;
219
+ }
220
+
201
221
  /**
202
222
  * Ensure MEMORY.md has a TotalReclaw header so the agent knows encrypted
203
223
  * memories are injected automatically via the before_agent_start hook.
@@ -255,12 +275,18 @@ const FACT_COUNT_CACHE_TTL = 5 * 60 * 1000;
255
275
  /**
256
276
  * Compute the candidate pool size from a fact count.
257
277
  *
258
- * Formula: pool = min(max(factCount * 3, 400), 5000)
278
+ * Server-side config takes priority (from billing cache), then local fallback.
279
+ * The server computes the optimal pool based on vault size and tier caps.
280
+ *
281
+ * Local fallback formula: pool = min(max(factCount * 3, 400), 5000)
259
282
  * - At least 400 candidates (even for tiny vaults)
260
283
  * - At most 5000 candidates (to bound decryption + reranking cost)
261
284
  * - 3x fact count in between
262
285
  */
263
286
  function computeCandidatePool(factCount: number): number {
287
+ const cache = readBillingCache();
288
+ if (cache?.features?.max_candidate_pool != null) return cache.features.max_candidate_pool;
289
+ // Fallback to local formula if no server config
264
290
  return Math.min(Math.max(factCount * 3, 400), 5000);
265
291
  }
266
292
 
@@ -302,21 +328,21 @@ async function getFactCount(logger: OpenClawPluginApi['logger']): Promise<number
302
328
  // Initialisation
303
329
  // ---------------------------------------------------------------------------
304
330
 
305
- /** True when master password is missing — tools return setup instructions. */
331
+ /** True when recovery phrase is missing — tools return setup instructions. */
306
332
  let needsSetup = false;
307
333
 
308
334
  /**
309
- * Derive keys from the master password, load or create credentials, and
335
+ * Derive keys from the recovery phrase, load or create credentials, and
310
336
  * register with the server if this is the first run.
311
337
  */
312
338
  async function initialize(logger: OpenClawPluginApi['logger']): Promise<void> {
313
339
  const serverUrl =
314
340
  process.env.TOTALRECLAW_SERVER_URL || 'https://api.totalreclaw.xyz';
315
- const masterPassword = process.env.TOTALRECLAW_MASTER_PASSWORD;
341
+ const masterPassword = process.env.TOTALRECLAW_RECOVERY_PHRASE;
316
342
 
317
343
  if (!masterPassword) {
318
344
  needsSetup = true;
319
- logger.info('TOTALRECLAW_MASTER_PASSWORD not set — setup required (see SKILL.md Post-Install Setup)');
345
+ logger.info('TOTALRECLAW_RECOVERY_PHRASE not set — setup required (see SKILL.md Post-Install Setup)');
320
346
  return;
321
347
  }
322
348
 
@@ -399,6 +425,45 @@ async function initialize(logger: OpenClawPluginApi['logger']): Promise<void> {
399
425
  subgraphOwner = userId;
400
426
  }
401
427
  }
428
+
429
+ // One-time billing check for returning users (imported recovery phrase).
430
+ // If they already have an active Pro subscription, inform them on next conversation start.
431
+ if (existingUserId && authKeyHex) {
432
+ try {
433
+ const walletAddr = subgraphOwner || userId || '';
434
+ if (walletAddr) {
435
+ const billingUrl = (process.env.TOTALRECLAW_SERVER_URL || 'https://api.totalreclaw.xyz').replace(/\/+$/, '');
436
+ const resp = await fetch(`${billingUrl}/v1/billing/status?wallet_address=${encodeURIComponent(walletAddr)}`, {
437
+ method: 'GET',
438
+ headers: {
439
+ 'Authorization': `Bearer ${authKeyHex}`,
440
+ 'Accept': 'application/json',
441
+ 'X-TotalReclaw-Client': 'openclaw-plugin',
442
+ },
443
+ });
444
+ if (resp.ok) {
445
+ const billingData = await resp.json() as Record<string, unknown>;
446
+ const tier = billingData.tier as string;
447
+ const expiresAt = billingData.expires_at as string | undefined;
448
+ // Populate billing cache for future use.
449
+ writeBillingCache({
450
+ tier: tier || 'free',
451
+ free_writes_used: (billingData.free_writes_used as number) ?? 0,
452
+ free_writes_limit: (billingData.free_writes_limit as number) ?? 0,
453
+ features: billingData.features as BillingCache['features'] | undefined,
454
+ checked_at: Date.now(),
455
+ });
456
+ if (tier === 'pro' && expiresAt) {
457
+ const expiryDate = new Date(expiresAt).toLocaleDateString();
458
+ welcomeBackMessage = `Welcome back! Your Pro subscription is active (expires: ${expiryDate}).`;
459
+ logger.info(`Returning Pro user detected — expires ${expiryDate}`);
460
+ }
461
+ }
462
+ }
463
+ } catch {
464
+ // Best-effort — don't block initialization on billing check failure.
465
+ }
466
+ }
402
467
  }
403
468
 
404
469
  function isDocker(): boolean {
@@ -412,27 +477,27 @@ function isDocker(): boolean {
412
477
  function buildSetupErrorMsg(): string {
413
478
  const base =
414
479
  'TotalReclaw setup required:\n' +
415
- '1. Set TOTALRECLAW_MASTER_PASSWORD — ask the user if they have an existing recovery phrase or generate a new 12-word BIP-39 mnemonic.\n' +
480
+ '1. Set TOTALRECLAW_RECOVERY_PHRASE — ask the user if they have an existing recovery phrase or generate a new 12-word BIP-39 mnemonic.\n' +
416
481
  '2. Restart the gateway to apply changes.\n' +
417
482
  ' (Optional: set TOTALRECLAW_SELF_HOSTED=true if using your own server instead of the managed service.)\n\n';
418
483
 
419
484
  if (isDocker()) {
420
485
  return base +
421
486
  'Running in Docker — pass env vars via `-e` flags or your compose file:\n' +
422
- ' -e TOTALRECLAW_MASTER_PASSWORD="word1 word2 ..."';
487
+ ' -e TOTALRECLAW_RECOVERY_PHRASE="word1 word2 ..."';
423
488
  }
424
489
 
425
490
  if (process.platform === 'darwin') {
426
491
  return base +
427
492
  'Running on macOS — add env vars to the LaunchAgent plist at\n' +
428
493
  '~/Library/LaunchAgents/ai.openclaw.gateway.plist under <key>EnvironmentVariables</key>:\n' +
429
- ' <key>TOTALRECLAW_MASTER_PASSWORD</key><string>word1 word2 ...</string>\n' +
494
+ ' <key>TOTALRECLAW_RECOVERY_PHRASE</key><string>word1 word2 ...</string>\n' +
430
495
  'Then run: openclaw gateway restart';
431
496
  }
432
497
 
433
498
  return base +
434
499
  'Running on Linux — add env vars to the systemd unit override or your shell profile:\n' +
435
- ' export TOTALRECLAW_MASTER_PASSWORD="word1 word2 ..."\n' +
500
+ ' export TOTALRECLAW_RECOVERY_PHRASE="word1 word2 ..."\n' +
436
501
  'Then run: openclaw gateway restart';
437
502
  }
438
503
 
@@ -463,7 +528,7 @@ async function requireFullSetup(logger: OpenClawPluginApi['logger']): Promise<vo
463
528
  // LSH + Embedding helpers
464
529
  // ---------------------------------------------------------------------------
465
530
 
466
- /** Master password cached for LSH seed derivation (set during initialize()). */
531
+ /** Recovery phrase cached for LSH seed derivation (set during initialize()). */
467
532
  let masterPasswordCache: string | null = null;
468
533
  /** Salt cached for LSH seed derivation (set during initialize()). */
469
534
  let saltCache: Buffer | null = null;
@@ -472,7 +537,7 @@ let saltCache: Buffer | null = null;
472
537
  * Get or initialize the LSH hasher.
473
538
  *
474
539
  * The hasher is created lazily because it needs:
475
- * 1. The master password + salt (available after initialize())
540
+ * 1. The recovery phrase + salt (available after initialize())
476
541
  * 2. The embedding dimensions (available after initLLMClient())
477
542
  *
478
543
  * If the provider doesn't support embeddings, this returns null and
@@ -857,9 +922,13 @@ async function storeExtractedFacts(
857
922
  }
858
923
 
859
924
  // Phase 3: Store the deduplicated facts (with optional store-time dedup).
925
+ // In subgraph mode, collect all protobuf payloads (tombstones + new facts)
926
+ // and submit them in a single batched UserOp for gas efficiency.
860
927
  let stored = 0;
861
928
  let superseded = 0;
862
929
  let skipped = 0;
930
+ const pendingPayloads: Buffer[] = []; // Batched subgraph payloads
931
+ let preparedForSubgraph = 0;
863
932
 
864
933
  for (const fact of dedupedFacts) {
865
934
  try {
@@ -881,24 +950,19 @@ async function storeExtractedFacts(
881
950
  if (fact.action === 'DELETE' && fact.existingFactId) {
882
951
  // Tombstone the old fact, don't store anything new.
883
952
  if (isSubgraphMode()) {
884
- try {
885
- const tombConfig = { ...getSubgraphConfig(), authKeyHex: authKeyHex!, walletAddress: subgraphOwner ?? undefined };
886
- const tombstone: FactPayload = {
887
- id: fact.existingFactId,
888
- timestamp: new Date().toISOString(),
889
- owner: subgraphOwner || userId!,
890
- encryptedBlob: '00',
891
- blindIndices: [],
892
- decayScore: 0,
893
- source: 'tombstone',
894
- contentFp: '',
895
- agentId: 'openclaw-plugin-auto',
896
- };
897
- await submitFactOnChain(encodeFactProtobuf(tombstone), tombConfig);
898
- logger.info(`LLM dedup: DELETE — tombstoned ${fact.existingFactId} on-chain`);
899
- } catch (tombErr) {
900
- logger.warn(`LLM dedup: DELETE failed for ${fact.existingFactId}: ${tombErr instanceof Error ? tombErr.message : String(tombErr)}`);
901
- }
953
+ const tombstone: FactPayload = {
954
+ id: fact.existingFactId,
955
+ timestamp: new Date().toISOString(),
956
+ owner: subgraphOwner || userId!,
957
+ encryptedBlob: '00',
958
+ blindIndices: [],
959
+ decayScore: 0,
960
+ source: 'tombstone',
961
+ contentFp: '',
962
+ agentId: 'openclaw-plugin-auto',
963
+ };
964
+ pendingPayloads.push(encodeFactProtobuf(tombstone));
965
+ logger.info(`LLM dedup: DELETE — queued tombstone for ${fact.existingFactId}`);
902
966
  } else if (apiClient && authKeyHex) {
903
967
  try {
904
968
  await apiClient.deleteFact(fact.existingFactId, authKeyHex);
@@ -914,24 +978,19 @@ async function storeExtractedFacts(
914
978
  if (fact.action === 'UPDATE' && fact.existingFactId) {
915
979
  // Tombstone the old fact, then fall through to store the new version.
916
980
  if (isSubgraphMode()) {
917
- try {
918
- const tombConfig = { ...getSubgraphConfig(), authKeyHex: authKeyHex!, walletAddress: subgraphOwner ?? undefined };
919
- const tombstone: FactPayload = {
920
- id: fact.existingFactId,
921
- timestamp: new Date().toISOString(),
922
- owner: subgraphOwner || userId!,
923
- encryptedBlob: '00',
924
- blindIndices: [],
925
- decayScore: 0,
926
- source: 'tombstone',
927
- contentFp: '',
928
- agentId: 'openclaw-plugin-auto',
929
- };
930
- await submitFactOnChain(encodeFactProtobuf(tombstone), tombConfig);
931
- logger.info(`LLM dedup: UPDATE — tombstoned ${fact.existingFactId} on-chain, storing replacement`);
932
- } catch (tombErr) {
933
- logger.warn(`LLM dedup: UPDATE tombstone failed for ${fact.existingFactId}: ${tombErr instanceof Error ? tombErr.message : String(tombErr)}`);
934
- }
981
+ const tombstone: FactPayload = {
982
+ id: fact.existingFactId,
983
+ timestamp: new Date().toISOString(),
984
+ owner: subgraphOwner || userId!,
985
+ encryptedBlob: '00',
986
+ blindIndices: [],
987
+ decayScore: 0,
988
+ source: 'tombstone',
989
+ contentFp: '',
990
+ agentId: 'openclaw-plugin-auto',
991
+ };
992
+ pendingPayloads.push(encodeFactProtobuf(tombstone));
993
+ logger.info(`LLM dedup: UPDATE — queued tombstone for ${fact.existingFactId}, storing replacement`);
935
994
  } else if (apiClient && authKeyHex) {
936
995
  try {
937
996
  await apiClient.deleteFact(fact.existingFactId, authKeyHex);
@@ -969,29 +1028,21 @@ async function storeExtractedFacts(
969
1028
  }
970
1029
  // action === 'supersede': delete old fact, inherit higher importance
971
1030
  if (isSubgraphMode()) {
972
- try {
973
- const tombConfig = { ...getSubgraphConfig(), authKeyHex: authKeyHex!, walletAddress: subgraphOwner ?? undefined };
974
- const tombstone: FactPayload = {
975
- id: dupResult.match.id,
976
- timestamp: new Date().toISOString(),
977
- owner: subgraphOwner || userId!,
978
- encryptedBlob: '00',
979
- blindIndices: [],
980
- decayScore: 0,
981
- source: 'tombstone',
982
- contentFp: '',
983
- agentId: 'openclaw-plugin-auto',
984
- };
985
- const tombProtobuf = encodeFactProtobuf(tombstone);
986
- await submitFactOnChain(tombProtobuf, tombConfig);
987
- logger.info(
988
- `Store-time dedup: superseded ${dupResult.match.id} on-chain (sim=${dupResult.similarity.toFixed(3)})`,
989
- );
990
- } catch (tombErr) {
991
- logger.warn(
992
- `Store-time dedup: failed to tombstone ${dupResult.match.id}: ${tombErr instanceof Error ? tombErr.message : String(tombErr)}`,
993
- );
994
- }
1031
+ const tombstone: FactPayload = {
1032
+ id: dupResult.match.id,
1033
+ timestamp: new Date().toISOString(),
1034
+ owner: subgraphOwner || userId!,
1035
+ encryptedBlob: '00',
1036
+ blindIndices: [],
1037
+ decayScore: 0,
1038
+ source: 'tombstone',
1039
+ contentFp: '',
1040
+ agentId: 'openclaw-plugin-auto',
1041
+ };
1042
+ pendingPayloads.push(encodeFactProtobuf(tombstone));
1043
+ logger.info(
1044
+ `Store-time dedup: queued supersede for ${dupResult.match.id} (sim=${dupResult.similarity.toFixed(3)})`,
1045
+ );
995
1046
  } else if (apiClient && authKeyHex) {
996
1047
  try {
997
1048
  await apiClient.deleteFact(dupResult.match.id, authKeyHex);
@@ -1024,20 +1075,7 @@ async function storeExtractedFacts(
1024
1075
  const contentFp = generateContentFingerprint(fact.text, dedupKey);
1025
1076
  const factId = crypto.randomUUID();
1026
1077
 
1027
- const payload: StoreFactPayload = {
1028
- id: factId,
1029
- timestamp: new Date().toISOString(),
1030
- encrypted_blob: encryptedBlob,
1031
- blind_indices: allIndices,
1032
- decay_score: effectiveImportance,
1033
- source: 'auto-extraction',
1034
- content_fp: contentFp,
1035
- agent_id: 'openclaw-plugin-auto',
1036
- encrypted_embedding: embeddingResult?.encryptedEmbedding,
1037
- };
1038
-
1039
1078
  if (isSubgraphMode()) {
1040
- const config = { ...getSubgraphConfig(), authKeyHex: authKeyHex!, walletAddress: subgraphOwner ?? undefined };
1041
1079
  const protobuf = encodeFactProtobuf({
1042
1080
  id: factId,
1043
1081
  timestamp: new Date().toISOString(),
@@ -1050,11 +1088,23 @@ async function storeExtractedFacts(
1050
1088
  agentId: 'openclaw-plugin-auto',
1051
1089
  encryptedEmbedding: embeddingResult?.encryptedEmbedding,
1052
1090
  });
1053
- await submitFactOnChain(protobuf, config);
1091
+ pendingPayloads.push(protobuf);
1092
+ preparedForSubgraph++;
1054
1093
  } else {
1094
+ const payload: StoreFactPayload = {
1095
+ id: factId,
1096
+ timestamp: new Date().toISOString(),
1097
+ encrypted_blob: encryptedBlob,
1098
+ blind_indices: allIndices,
1099
+ decay_score: effectiveImportance,
1100
+ source: 'auto-extraction',
1101
+ content_fp: contentFp,
1102
+ agent_id: 'openclaw-plugin-auto',
1103
+ encrypted_embedding: embeddingResult?.encryptedEmbedding,
1104
+ };
1055
1105
  await apiClient.store(userId, [payload], authKeyHex);
1106
+ stored++;
1056
1107
  }
1057
- stored++;
1058
1108
  } catch (err: unknown) {
1059
1109
  // Check for 403 / quota exceeded — invalidate billing cache so next
1060
1110
  // before_agent_start re-fetches and warns the user.
@@ -1068,6 +1118,28 @@ async function storeExtractedFacts(
1068
1118
  }
1069
1119
  }
1070
1120
 
1121
+ // Batch-submit all subgraph payloads in a single UserOp (gas-efficient).
1122
+ if (pendingPayloads.length > 0 && isSubgraphMode()) {
1123
+ try {
1124
+ const batchConfig = { ...getSubgraphConfig(), authKeyHex: authKeyHex!, walletAddress: subgraphOwner ?? undefined };
1125
+ const result = await submitFactBatchOnChain(pendingPayloads, batchConfig);
1126
+ if (result.success) {
1127
+ stored += preparedForSubgraph;
1128
+ logger.info(`Batch submitted ${result.batchSize} payloads in 1 UserOp (tx=${result.txHash.slice(0, 10)}…)`);
1129
+ } else {
1130
+ logger.warn(`Batch UserOp failed on-chain (tx=${result.txHash.slice(0, 10)}…)`);
1131
+ }
1132
+ } catch (err: unknown) {
1133
+ const errMsg = err instanceof Error ? err.message : String(err);
1134
+ if (errMsg.includes('403') || errMsg.toLowerCase().includes('quota')) {
1135
+ try { fs.unlinkSync(BILLING_CACHE_PATH); } catch { /* ignore */ }
1136
+ logger.warn(`Quota exceeded during batch submit — billing cache invalidated. ${errMsg}`);
1137
+ } else {
1138
+ logger.warn(`Batch submission failed: ${errMsg}`);
1139
+ }
1140
+ }
1141
+ }
1142
+
1071
1143
  if (stored > 0 || superseded > 0 || skipped > 0) {
1072
1144
  logger.info(`Auto-extraction results: stored=${stored}, superseded=${superseded}, skipped=${skipped}`);
1073
1145
  }
@@ -1239,7 +1311,7 @@ const plugin = {
1239
1311
  },
1240
1312
  type: {
1241
1313
  type: 'string',
1242
- enum: ['fact', 'preference', 'decision', 'episodic', 'goal'],
1314
+ enum: ['fact', 'preference', 'decision', 'episodic', 'goal', 'context', 'summary'],
1243
1315
  description: 'The kind of memory (default: fact)',
1244
1316
  },
1245
1317
  importance: {
@@ -2154,6 +2226,85 @@ const plugin = {
2154
2226
  { name: 'totalreclaw_import_from' },
2155
2227
  );
2156
2228
 
2229
+ // ---------------------------------------------------------------
2230
+ // Tool: totalreclaw_upgrade
2231
+ // ---------------------------------------------------------------
2232
+
2233
+ api.registerTool(
2234
+ {
2235
+ name: 'totalreclaw_upgrade',
2236
+ label: 'Upgrade to Pro',
2237
+ description:
2238
+ 'Upgrade to TotalReclaw Pro for unlimited encrypted memories. ' +
2239
+ 'Returns a Stripe checkout URL for the user to complete payment via credit/debit card.',
2240
+ parameters: {
2241
+ type: 'object',
2242
+ properties: {},
2243
+ additionalProperties: false,
2244
+ },
2245
+ async execute() {
2246
+ try {
2247
+ await requireFullSetup(api.logger);
2248
+
2249
+ if (!authKeyHex) {
2250
+ return {
2251
+ content: [{ type: 'text', text: 'Auth credentials are not available. Please initialize first.' }],
2252
+ };
2253
+ }
2254
+
2255
+ const serverUrl = (process.env.TOTALRECLAW_SERVER_URL || 'https://api.totalreclaw.xyz').replace(/\/+$/, '');
2256
+ const walletAddr = subgraphOwner || userId || '';
2257
+
2258
+ if (!walletAddr) {
2259
+ return {
2260
+ content: [{ type: 'text', text: 'Wallet address not available. Please ensure the plugin is fully initialized.' }],
2261
+ };
2262
+ }
2263
+
2264
+ const response = await fetch(`${serverUrl}/v1/billing/checkout`, {
2265
+ method: 'POST',
2266
+ headers: {
2267
+ 'Authorization': `Bearer ${authKeyHex}`,
2268
+ 'Content-Type': 'application/json',
2269
+ 'X-TotalReclaw-Client': 'openclaw-plugin',
2270
+ },
2271
+ body: JSON.stringify({
2272
+ wallet_address: walletAddr,
2273
+ tier: 'pro',
2274
+ }),
2275
+ });
2276
+
2277
+ if (!response.ok) {
2278
+ const body = await response.text().catch(() => '');
2279
+ return {
2280
+ content: [{ type: 'text', text: `Failed to create checkout session (HTTP ${response.status}): ${body || response.statusText}` }],
2281
+ };
2282
+ }
2283
+
2284
+ const data = await response.json() as { checkout_url?: string };
2285
+
2286
+ if (!data.checkout_url) {
2287
+ return {
2288
+ content: [{ type: 'text', text: 'Failed to create checkout session: no checkout URL returned.' }],
2289
+ };
2290
+ }
2291
+
2292
+ return {
2293
+ content: [{ type: 'text', text: `Open this URL to upgrade to Pro: ${data.checkout_url}` }],
2294
+ details: { checkout_url: data.checkout_url },
2295
+ };
2296
+ } catch (err: unknown) {
2297
+ const message = err instanceof Error ? err.message : String(err);
2298
+ api.logger.error(`totalreclaw_upgrade failed: ${message}`);
2299
+ return {
2300
+ content: [{ type: 'text', text: `Failed to create checkout session: ${message}` }],
2301
+ };
2302
+ }
2303
+ },
2304
+ },
2305
+ { name: 'totalreclaw_upgrade' },
2306
+ );
2307
+
2157
2308
  // ---------------------------------------------------------------
2158
2309
  // Hook: before_agent_start
2159
2310
  // ---------------------------------------------------------------
@@ -2184,6 +2335,13 @@ const plugin = {
2184
2335
  };
2185
2336
  }
2186
2337
 
2338
+ // One-time welcome-back message for returning Pro users.
2339
+ let welcomeBack = '';
2340
+ if (welcomeBackMessage) {
2341
+ welcomeBack = `\n\n${welcomeBackMessage}`;
2342
+ welcomeBackMessage = null; // Consume — only show once
2343
+ }
2344
+
2187
2345
  // Billing cache check — warn if quota is approaching limit.
2188
2346
  let billingWarning = '';
2189
2347
  try {
@@ -2257,7 +2415,7 @@ const plugin = {
2257
2415
  const lines = cachedFacts.slice(0, 8).map((f, i) =>
2258
2416
  `${i + 1}. ${f.text} (importance: ${f.importance}/10, cached)`,
2259
2417
  );
2260
- return { prependContext: `## Relevant Memories\n\n${lines.join('\n')}` + billingWarning };
2418
+ return { prependContext: `## Relevant Memories\n\n${lines.join('\n')}` + welcomeBack + billingWarning };
2261
2419
  }
2262
2420
  }
2263
2421
 
@@ -2270,7 +2428,7 @@ const plugin = {
2270
2428
  const lines = cachedFacts.slice(0, 8).map((f, i) =>
2271
2429
  `${i + 1}. ${f.text} (importance: ${f.importance}/10, cached)`,
2272
2430
  );
2273
- return { prependContext: `## Relevant Memories\n\n${lines.join('\n')}` + billingWarning };
2431
+ return { prependContext: `## Relevant Memories\n\n${lines.join('\n')}` + welcomeBack + billingWarning };
2274
2432
  }
2275
2433
 
2276
2434
  if (allTrapdoors.length === 0) return undefined;
@@ -2287,7 +2445,7 @@ const plugin = {
2287
2445
  const lines = cachedFacts.slice(0, 8).map((f, i) =>
2288
2446
  `${i + 1}. ${f.text} (importance: ${f.importance}/10, cached)`,
2289
2447
  );
2290
- return { prependContext: `## Relevant Memories\n\n${lines.join('\n')}` + billingWarning };
2448
+ return { prependContext: `## Relevant Memories\n\n${lines.join('\n')}` + welcomeBack + billingWarning };
2291
2449
  }
2292
2450
  return undefined;
2293
2451
  }
@@ -2299,7 +2457,7 @@ const plugin = {
2299
2457
  const lines = cachedFacts.slice(0, 8).map((f, i) =>
2300
2458
  `${i + 1}. ${f.text} (importance: ${f.importance}/10, cached)`,
2301
2459
  );
2302
- return { prependContext: `## Relevant Memories\n\n${lines.join('\n')}` + billingWarning };
2460
+ return { prependContext: `## Relevant Memories\n\n${lines.join('\n')}` + welcomeBack + billingWarning };
2303
2461
  }
2304
2462
 
2305
2463
  // 5. Decrypt subgraph results and build reranker input.
@@ -2405,7 +2563,7 @@ const plugin = {
2405
2563
  });
2406
2564
  const contextString = `## Relevant Memories\n\n${lines.join('\n')}`;
2407
2565
 
2408
- return { prependContext: contextString + billingWarning };
2566
+ return { prependContext: contextString + welcomeBack + billingWarning };
2409
2567
  }
2410
2568
 
2411
2569
  // --- Server mode (existing behavior) ---
@@ -2517,7 +2675,7 @@ const plugin = {
2517
2675
  });
2518
2676
  const contextString = `## Relevant Memories\n\n${lines.join('\n')}`;
2519
2677
 
2520
- return { prependContext: contextString + billingWarning };
2678
+ return { prependContext: contextString + welcomeBack + billingWarning };
2521
2679
  } catch (err: unknown) {
2522
2680
  // The hook must NEVER throw -- log and return undefined.
2523
2681
  const message = err instanceof Error ? err.message : String(err);
@@ -2550,12 +2708,13 @@ const plugin = {
2550
2708
  : [];
2551
2709
  const rawFacts = await extractFacts(evt.messages, 'turn', existingMemories);
2552
2710
  const { kept: importanceFiltered } = filterByImportance(rawFacts, api.logger);
2553
- if (importanceFiltered.length > MAX_FACTS_PER_EXTRACTION) {
2711
+ const maxFacts = getMaxFactsPerExtraction();
2712
+ if (importanceFiltered.length > maxFacts) {
2554
2713
  api.logger.info(
2555
- `Capped extraction from ${importanceFiltered.length} to ${MAX_FACTS_PER_EXTRACTION} facts`,
2714
+ `Capped extraction from ${importanceFiltered.length} to ${maxFacts} facts`,
2556
2715
  );
2557
2716
  }
2558
- const facts = importanceFiltered.slice(0, MAX_FACTS_PER_EXTRACTION);
2717
+ const facts = importanceFiltered.slice(0, maxFacts);
2559
2718
  if (facts.length > 0) {
2560
2719
  await storeExtractedFacts(facts, api.logger);
2561
2720
  }
@@ -2592,12 +2751,13 @@ const plugin = {
2592
2751
  : [];
2593
2752
  const rawCompactFacts = await extractFacts(evt.messages, 'full', existingMemories);
2594
2753
  const { kept: compactImportanceFiltered } = filterByImportance(rawCompactFacts, api.logger);
2595
- if (compactImportanceFiltered.length > MAX_FACTS_PER_EXTRACTION) {
2754
+ const maxFactsCompact = getMaxFactsPerExtraction();
2755
+ if (compactImportanceFiltered.length > maxFactsCompact) {
2596
2756
  api.logger.info(
2597
- `Capped compaction extraction from ${compactImportanceFiltered.length} to ${MAX_FACTS_PER_EXTRACTION} facts`,
2757
+ `Capped compaction extraction from ${compactImportanceFiltered.length} to ${maxFactsCompact} facts`,
2598
2758
  );
2599
2759
  }
2600
- const facts = compactImportanceFiltered.slice(0, MAX_FACTS_PER_EXTRACTION);
2760
+ const facts = compactImportanceFiltered.slice(0, maxFactsCompact);
2601
2761
  if (facts.length > 0) {
2602
2762
  await storeExtractedFacts(facts, api.logger);
2603
2763
  }
@@ -2633,12 +2793,13 @@ const plugin = {
2633
2793
  : [];
2634
2794
  const rawResetFacts = await extractFacts(evt.messages, 'full', existingMemories);
2635
2795
  const { kept: resetImportanceFiltered } = filterByImportance(rawResetFacts, api.logger);
2636
- if (resetImportanceFiltered.length > MAX_FACTS_PER_EXTRACTION) {
2796
+ const maxFactsReset = getMaxFactsPerExtraction();
2797
+ if (resetImportanceFiltered.length > maxFactsReset) {
2637
2798
  api.logger.info(
2638
- `Capped reset extraction from ${resetImportanceFiltered.length} to ${MAX_FACTS_PER_EXTRACTION} facts`,
2799
+ `Capped reset extraction from ${resetImportanceFiltered.length} to ${maxFactsReset} facts`,
2639
2800
  );
2640
2801
  }
2641
- const facts = resetImportanceFiltered.slice(0, MAX_FACTS_PER_EXTRACTION);
2802
+ const facts = resetImportanceFiltered.slice(0, maxFactsReset);
2642
2803
  if (facts.length > 0) {
2643
2804
  await storeExtractedFacts(facts, api.logger);
2644
2805
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@totalreclaw/totalreclaw",
3
- "version": "1.2.0",
3
+ "version": "1.4.0",
4
4
  "description": "End-to-end encrypted memory for AI agents — portable, yours forever. Automatic extraction, semantic search, and on-chain storage",
5
5
  "type": "module",
6
6
  "keywords": [
package/setup.sh CHANGED
@@ -16,4 +16,4 @@ echo " cd testbed/functional-test"
16
16
  echo " docker compose -f docker-compose.functional-test.yml up -d"
17
17
  echo ""
18
18
  echo "The plugin will auto-register on first use."
19
- echo "Set TOTALRECLAW_MASTER_PASSWORD in your .env file."
19
+ echo "Set TOTALRECLAW_RECOVERY_PHRASE in your .env file."
package/subgraph-store.ts CHANGED
@@ -189,7 +189,7 @@ export async function submitFactOnChain(
189
189
  }
190
190
 
191
191
  if (!config.mnemonic) {
192
- throw new Error('Mnemonic (TOTALRECLAW_MASTER_PASSWORD) is required for on-chain submission');
192
+ throw new Error('Mnemonic (TOTALRECLAW_RECOVERY_PHRASE) is required for on-chain submission');
193
193
  }
194
194
 
195
195
  const chain = getChainFromId(config.chainId);
@@ -281,6 +281,105 @@ export async function submitFactOnChain(
281
281
  };
282
282
  }
283
283
 
284
+ /**
285
+ * Submit multiple facts on-chain in a single ERC-4337 UserOp (batched).
286
+ *
287
+ * Each protobuf payload becomes one call in a multi-call UserOp. The
288
+ * DataEdge contract emits a separate Log(bytes) event per call, and the
289
+ * subgraph indexes each event independently (by txHash + logIndex).
290
+ *
291
+ * Falls back to single-fact path for batches of 1 (no multicall overhead).
292
+ */
293
+ export async function submitFactBatchOnChain(
294
+ protobufPayloads: Buffer[],
295
+ config: SubgraphStoreConfig,
296
+ ): Promise<{ txHash: string; userOpHash: string; success: boolean; batchSize: number }> {
297
+ if (!protobufPayloads.length) {
298
+ return { txHash: '', userOpHash: '', success: true, batchSize: 0 };
299
+ }
300
+
301
+ // Single fact — use standard path (avoids multicall overhead)
302
+ if (protobufPayloads.length === 1) {
303
+ const result = await submitFactOnChain(protobufPayloads[0], config);
304
+ return { ...result, batchSize: 1 };
305
+ }
306
+
307
+ if (!config.relayUrl) {
308
+ throw new Error('Relay URL (TOTALRECLAW_SERVER_URL) is required for on-chain submission');
309
+ }
310
+ if (!config.mnemonic) {
311
+ throw new Error('Mnemonic (TOTALRECLAW_RECOVERY_PHRASE) is required for on-chain submission');
312
+ }
313
+
314
+ const chain = getChainFromId(config.chainId);
315
+ const bundlerRpcUrl = getRelayBundlerUrl(config.relayUrl);
316
+ const dataEdgeAddress = config.dataEdgeAddress as Address;
317
+ const entryPointAddr = (config.entryPointAddress || entryPoint07Address) as Address;
318
+
319
+ const headers: Record<string, string> = {
320
+ 'X-TotalReclaw-Client': 'openclaw-plugin',
321
+ };
322
+ if (config.authKeyHex) headers['Authorization'] = `Bearer ${config.authKeyHex}`;
323
+ if (config.walletAddress) headers['X-Wallet-Address'] = config.walletAddress;
324
+
325
+ const authTransport = Object.keys(headers).length > 0
326
+ ? http(bundlerRpcUrl, { fetchOptions: { headers } })
327
+ : http(bundlerRpcUrl);
328
+
329
+ const ownerAccount = mnemonicToAccount(config.mnemonic);
330
+ const publicClient = createPublicClient({
331
+ chain,
332
+ transport: config.rpcUrl ? http(config.rpcUrl) : http(),
333
+ });
334
+
335
+ const pimlicoClient = createPimlicoClient({
336
+ chain,
337
+ transport: authTransport,
338
+ entryPoint: {
339
+ address: entryPointAddr,
340
+ version: '0.7',
341
+ },
342
+ });
343
+
344
+ const smartAccount = await toSimpleSmartAccount({
345
+ client: publicClient,
346
+ owner: ownerAccount,
347
+ entryPoint: {
348
+ address: entryPointAddr,
349
+ version: '0.7',
350
+ },
351
+ });
352
+
353
+ const smartAccountClient = createSmartAccountClient({
354
+ account: smartAccount,
355
+ chain,
356
+ bundlerTransport: authTransport,
357
+ paymaster: pimlicoClient,
358
+ userOperation: {
359
+ estimateFeesPerGas: async () => {
360
+ return (await pimlicoClient.getUserOperationGasPrice()).fast;
361
+ },
362
+ },
363
+ });
364
+
365
+ // Build multi-call batch: each payload → one call to DataEdge fallback()
366
+ const calls = protobufPayloads.map(payload => ({
367
+ to: dataEdgeAddress,
368
+ value: 0n,
369
+ data: `0x${payload.toString('hex')}` as Hex,
370
+ }));
371
+
372
+ const userOpHash = await smartAccountClient.sendUserOperation({ calls });
373
+ const receipt = await pimlicoClient.waitForUserOperationReceipt({ hash: userOpHash });
374
+
375
+ return {
376
+ txHash: receipt.receipt.transactionHash,
377
+ userOpHash,
378
+ success: receipt.success,
379
+ batchSize: protobufPayloads.length,
380
+ };
381
+ }
382
+
284
383
  // ---------------------------------------------------------------------------
285
384
  // Configuration
286
385
  // ---------------------------------------------------------------------------
@@ -299,7 +398,7 @@ export function isSubgraphMode(): boolean {
299
398
  * Get subgraph configuration from environment variables.
300
399
  *
301
400
  * After the relay refactor, clients only need:
302
- * - TOTALRECLAW_MASTER_PASSWORD -- BIP-39 mnemonic
401
+ * - TOTALRECLAW_RECOVERY_PHRASE -- BIP-39 mnemonic
303
402
  * - TOTALRECLAW_SERVER_URL -- relay server URL (default: https://api.totalreclaw.xyz)
304
403
  * - TOTALRECLAW_SELF_HOSTED -- set "true" to use self-hosted server (default: managed service)
305
404
  * - TOTALRECLAW_CHAIN_ID -- optional, defaults to 100 (Gnosis mainnet)
@@ -338,7 +437,7 @@ export async function deriveSmartAccountAddress(mnemonic: string, chainId?: numb
338
437
  export function getSubgraphConfig(): SubgraphStoreConfig {
339
438
  return {
340
439
  relayUrl: process.env.TOTALRECLAW_SERVER_URL || 'https://api.totalreclaw.xyz',
341
- mnemonic: process.env.TOTALRECLAW_MASTER_PASSWORD || '',
440
+ mnemonic: process.env.TOTALRECLAW_RECOVERY_PHRASE || '',
342
441
  cachePath: process.env.TOTALRECLAW_CACHE_PATH || `${process.env.HOME}/.totalreclaw/cache.enc`,
343
442
  chainId: parseInt(process.env.TOTALRECLAW_CHAIN_ID || '100'),
344
443
  dataEdgeAddress: process.env.TOTALRECLAW_DATA_EDGE_ADDRESS || DEFAULT_DATA_EDGE_ADDRESS,