@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 +2 -2
- package/crypto.ts +1 -1
- package/extractor-dedup.test.ts +1 -1
- package/extractor.ts +26 -18
- package/generate-mnemonic.ts +3 -3
- package/import-adapters/base-adapter.ts +1 -1
- package/import-adapters/import-adapters.test.ts +1 -1
- package/import-adapters/types.ts +1 -1
- package/index.ts +265 -104
- package/package.json +1 -1
- package/setup.sh +1 -1
- package/subgraph-store.ts +102 -3
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) |
|
|
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
|
|
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
|
package/extractor-dedup.test.ts
CHANGED
|
@@ -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
|
|
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
|
|
44
|
-
2. Focus on user-specific information
|
|
45
|
-
3. Skip generic knowledge, greetings, and
|
|
46
|
-
4.
|
|
47
|
-
5.
|
|
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
|
|
54
|
-
- episodic:
|
|
55
|
-
- goal: Objectives or
|
|
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
|
|
59
|
-
- UPDATE:
|
|
60
|
-
- DELETE: Contradicts an existing memory
|
|
61
|
-
- NOOP: Already captured
|
|
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)),
|
package/generate-mnemonic.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env npx tsx
|
|
2
2
|
/**
|
|
3
|
-
* Generate a BIP-39 12-word mnemonic for use as
|
|
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
|
|
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
|
|
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 });
|
package/import-adapters/types.ts
CHANGED
|
@@ -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 =
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
|
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
|
|
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.
|
|
341
|
+
const masterPassword = process.env.TOTALRECLAW_RECOVERY_PHRASE;
|
|
316
342
|
|
|
317
343
|
if (!masterPassword) {
|
|
318
344
|
needsSetup = true;
|
|
319
|
-
logger.info('
|
|
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
|
|
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
|
|
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>
|
|
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
|
|
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
|
-
/**
|
|
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
|
|
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
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
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
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
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
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2711
|
+
const maxFacts = getMaxFactsPerExtraction();
|
|
2712
|
+
if (importanceFiltered.length > maxFacts) {
|
|
2554
2713
|
api.logger.info(
|
|
2555
|
-
`Capped extraction from ${importanceFiltered.length} to ${
|
|
2714
|
+
`Capped extraction from ${importanceFiltered.length} to ${maxFacts} facts`,
|
|
2556
2715
|
);
|
|
2557
2716
|
}
|
|
2558
|
-
const facts = importanceFiltered.slice(0,
|
|
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
|
-
|
|
2754
|
+
const maxFactsCompact = getMaxFactsPerExtraction();
|
|
2755
|
+
if (compactImportanceFiltered.length > maxFactsCompact) {
|
|
2596
2756
|
api.logger.info(
|
|
2597
|
-
`Capped compaction extraction from ${compactImportanceFiltered.length} to ${
|
|
2757
|
+
`Capped compaction extraction from ${compactImportanceFiltered.length} to ${maxFactsCompact} facts`,
|
|
2598
2758
|
);
|
|
2599
2759
|
}
|
|
2600
|
-
const facts = compactImportanceFiltered.slice(0,
|
|
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
|
-
|
|
2796
|
+
const maxFactsReset = getMaxFactsPerExtraction();
|
|
2797
|
+
if (resetImportanceFiltered.length > maxFactsReset) {
|
|
2637
2798
|
api.logger.info(
|
|
2638
|
-
`Capped reset extraction from ${resetImportanceFiltered.length} to ${
|
|
2799
|
+
`Capped reset extraction from ${resetImportanceFiltered.length} to ${maxFactsReset} facts`,
|
|
2639
2800
|
);
|
|
2640
2801
|
}
|
|
2641
|
-
const facts = resetImportanceFiltered.slice(0,
|
|
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.
|
|
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
|
|
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 (
|
|
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
|
-
* -
|
|
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.
|
|
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,
|