@totalreclaw/totalreclaw 3.3.12-rc.18 → 3.3.12-rc.19
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/SKILL.md +1 -1
- package/dist/index.js +19 -0
- package/dist/subgraph-store.js +183 -178
- package/dist/tr-cli.js +1 -1
- package/dist/trajectory-poller.js +3 -0
- package/index.ts +15 -0
- package/package.json +1 -1
- package/skill.json +1 -1
- package/subgraph-store.ts +190 -180
- package/tr-cli.ts +1 -1
- package/trajectory-poller.ts +15 -0
package/SKILL.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: totalreclaw
|
|
3
3
|
description: "End-to-end encrypted, decentralized memory for OpenClaw. A native kind:memory provider — recall is automatic via memory_search/memory_get, and facts are captured in the background. Trigger on 'install TotalReclaw', 'set up TotalReclaw', 'restore my recovery phrase', any recall request ('what do you remember about me', 'what's my X'), AND any explicit remember request ('remember X', 'save X')."
|
|
4
|
-
version: 3.3.12-rc.
|
|
4
|
+
version: 3.3.12-rc.19
|
|
5
5
|
author: TotalReclaw Team
|
|
6
6
|
license: MIT
|
|
7
7
|
homepage: https://totalreclaw.xyz
|
package/dist/index.js
CHANGED
|
@@ -4452,6 +4452,25 @@ const plugin = {
|
|
|
4452
4452
|
// ---------------------------------------------------------------
|
|
4453
4453
|
startTrajectoryPoller({
|
|
4454
4454
|
logger: api.logger,
|
|
4455
|
+
// 3.3.12-rc.19: re-assert slots.memory each poll tick. OpenClaw's
|
|
4456
|
+
// config-rewrite-after-restart can strip it after register()'s
|
|
4457
|
+
// self-heal (pop-os reinstall 2026-06-30 → slot reverted to disabled
|
|
4458
|
+
// memory-core → memory_search/memory_get never bound). Cheap fs
|
|
4459
|
+
// read; heals + restarts only if the slot is wrong.
|
|
4460
|
+
recheckSlot: () => {
|
|
4461
|
+
try {
|
|
4462
|
+
if (patchOpenClawConfig(undefined, pluginVersion ?? undefined) === 'patched') {
|
|
4463
|
+
api.logger.warn('TotalReclaw: slots.memory was wrong at poll — re-healed openclaw.json, restarting to apply');
|
|
4464
|
+
try {
|
|
4465
|
+
process.kill(process.pid, 'SIGUSR1');
|
|
4466
|
+
}
|
|
4467
|
+
catch { /* gateway shutting down */ }
|
|
4468
|
+
}
|
|
4469
|
+
}
|
|
4470
|
+
catch (err) {
|
|
4471
|
+
api.logger.warn(`TotalReclaw: slot recheck failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
4472
|
+
}
|
|
4473
|
+
},
|
|
4455
4474
|
ensureInitialized: () => ensureInitialized(api.logger),
|
|
4456
4475
|
isPairingPending: () => needsSetup,
|
|
4457
4476
|
isImportActive: () => _importInProgress,
|
package/dist/subgraph-store.js
CHANGED
|
@@ -233,6 +233,31 @@ async function withSenderLock(sender, fn) {
|
|
|
233
233
|
export function __resetSenderLocksForTests() {
|
|
234
234
|
_senderSubmissionLocks.clear();
|
|
235
235
|
}
|
|
236
|
+
// ---------------------------------------------------------------------------
|
|
237
|
+
// Test-only WASM mock seams (AA10 submit-path retry test)
|
|
238
|
+
// ---------------------------------------------------------------------------
|
|
239
|
+
/**
|
|
240
|
+
* Test-only seam: inject a mock WASM module.
|
|
241
|
+
*
|
|
242
|
+
* The AA10 submit-path retry test needs to drive the real submitFactBatchOnChain
|
|
243
|
+
* while controlling WASM behavior (deriveEoa, encodeBatchCall, hashUserOp,
|
|
244
|
+
* signUserOp, getEntryPointAddress). This seam swaps the module-level _wasm
|
|
245
|
+
* reference so the test can provide stubs.
|
|
246
|
+
*
|
|
247
|
+
* MUST be followed by __clearWasmForTests() in teardown.
|
|
248
|
+
*/
|
|
249
|
+
export function __setWasmForTests(mock) {
|
|
250
|
+
_wasm = mock;
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Test-only seam: restore the real WASM module.
|
|
254
|
+
*
|
|
255
|
+
* Clears a test-injected mock WASM and resets the module to null so the next
|
|
256
|
+
* getWasm() call reloads the real @totalreclaw/core.
|
|
257
|
+
*/
|
|
258
|
+
export function __clearWasmForTests() {
|
|
259
|
+
_wasm = null;
|
|
260
|
+
}
|
|
236
261
|
/**
|
|
237
262
|
* Check if a Smart Account is deployed and return factory/factoryData if not.
|
|
238
263
|
*
|
|
@@ -325,8 +350,6 @@ async function submitFactOnChainLocked(protobufPayload, config, eoa, sender) {
|
|
|
325
350
|
const gasPrices = await rpc('pimlico_getUserOperationGasPrice', []);
|
|
326
351
|
const fast = gasPrices.fast;
|
|
327
352
|
const rpcUrl = config.rpcUrl || CONFIG.rpcUrl || getDefaultRpcUrl(config.chainId);
|
|
328
|
-
// 4. Check if Smart Account is deployed (needed for factory/factoryData)
|
|
329
|
-
const { factory, factoryData } = await getInitCode(sender, eoa.address, rpcUrl);
|
|
330
353
|
// 5. Get nonce from EntryPoint via bundler RPC.
|
|
331
354
|
// Routing through the bundler lets Pimlico account for pending mempool
|
|
332
355
|
// UserOps, preventing AA25 nonce conflicts on rapid submissions.
|
|
@@ -336,79 +359,50 @@ async function submitFactOnChainLocked(protobufPayload, config, eoa, sender) {
|
|
|
336
359
|
const senderPadded = sender.slice(2).toLowerCase().padStart(64, '0');
|
|
337
360
|
const keyPadded = '0'.repeat(64);
|
|
338
361
|
const nonceCalldata = `0x35567e1a${senderPadded}${keyPadded}`;
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
const unsignedOp = {
|
|
356
|
-
sender,
|
|
357
|
-
nonce,
|
|
358
|
-
callData,
|
|
359
|
-
callGasLimit: '0x0',
|
|
360
|
-
verificationGasLimit: '0x0',
|
|
361
|
-
preVerificationGas: '0x0',
|
|
362
|
-
maxFeePerGas: fast.maxFeePerGas,
|
|
363
|
-
maxPriorityFeePerGas: fast.maxPriorityFeePerGas,
|
|
364
|
-
signature: DUMMY_SIGNATURE,
|
|
365
|
-
};
|
|
366
|
-
if (factory) {
|
|
367
|
-
unsignedOp.factory = factory;
|
|
368
|
-
unsignedOp.factoryData = factoryData;
|
|
362
|
+
// Helper to fetch nonce (with bundler fallback)
|
|
363
|
+
async function fetchNonce() {
|
|
364
|
+
try {
|
|
365
|
+
const nonceResult = await rpc('eth_call', [{ to: entryPoint, data: nonceCalldata }, 'latest']);
|
|
366
|
+
return nonceResult || '0x0';
|
|
367
|
+
}
|
|
368
|
+
catch {
|
|
369
|
+
// Fallback to public RPC if bundler doesn't support eth_call
|
|
370
|
+
const nonceJson = await rpcRequest({
|
|
371
|
+
url: rpcUrl,
|
|
372
|
+
headers: { 'Content-Type': 'application/json' },
|
|
373
|
+
method: 'eth_call',
|
|
374
|
+
params: [{ to: entryPoint, data: nonceCalldata }, 'latest'],
|
|
375
|
+
});
|
|
376
|
+
return nonceJson.result || '0x0';
|
|
377
|
+
}
|
|
369
378
|
}
|
|
370
|
-
//
|
|
371
|
-
const
|
|
372
|
-
|
|
373
|
-
//
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
const sigHex = signUserOp(hashHex, eoa.private_key);
|
|
377
|
-
unsignedOp.signature = `0x${sigHex}`;
|
|
378
|
-
// 9. Submit the signed UserOp (with AA25 nonce conflict retry)
|
|
379
|
+
// Track force-deployed senders for AA10 retry (local to this submission attempt)
|
|
380
|
+
const forceDeployed = new Set();
|
|
381
|
+
// Single retry loop: getInitCode → build UserOp → sponsor → sign → send
|
|
382
|
+
// On AA10 "sender already constructed", mark sender as force-deployed and retry.
|
|
383
|
+
// AA10 can occur at pm_sponsorUserOperation (initCode present on deployed sender)
|
|
384
|
+
// or eth_sendUserOperation (same root cause). This loop handles both.
|
|
379
385
|
let userOpHash;
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
const { factory: retryFactory, factoryData: retryFactoryData } = await getInitCode(sender, eoa.address, rpcUrl);
|
|
394
|
-
let retryNonce;
|
|
395
|
-
try {
|
|
396
|
-
const retryNonceResult = await rpc('eth_call', [{ to: entryPoint, data: nonceCalldata }, 'latest']);
|
|
397
|
-
retryNonce = retryNonceResult || '0x0';
|
|
398
|
-
}
|
|
399
|
-
catch {
|
|
400
|
-
const retryNonceJson = await rpcRequest({
|
|
401
|
-
url: rpcUrl,
|
|
402
|
-
headers: { 'Content-Type': 'application/json' },
|
|
403
|
-
method: 'eth_call',
|
|
404
|
-
params: [{ to: entryPoint, data: nonceCalldata }, 'latest'],
|
|
405
|
-
});
|
|
406
|
-
retryNonce = retryNonceJson.result || '0x0';
|
|
386
|
+
let attempt = 0;
|
|
387
|
+
const maxAttempts = 2;
|
|
388
|
+
while (attempt < maxAttempts) {
|
|
389
|
+
attempt++;
|
|
390
|
+
try {
|
|
391
|
+
// 4. Check if Smart Account is deployed (needed for factory/factoryData)
|
|
392
|
+
// If force-deployed, skip eth_getCode and return null initCode.
|
|
393
|
+
let factory = null;
|
|
394
|
+
let factoryData = null;
|
|
395
|
+
if (!forceDeployed.has(sender.toLowerCase())) {
|
|
396
|
+
const initCode = await getInitCode(sender, eoa.address, rpcUrl);
|
|
397
|
+
factory = initCode.factory;
|
|
398
|
+
factoryData = initCode.factoryData;
|
|
407
399
|
}
|
|
408
|
-
//
|
|
409
|
-
const
|
|
400
|
+
// Fetch fresh nonce for each attempt
|
|
401
|
+
const nonce = await fetchNonce();
|
|
402
|
+
// 6. Build unsigned UserOp (v0.7 fields, camelCase for Rust JSON serde)
|
|
403
|
+
const unsignedOp = {
|
|
410
404
|
sender,
|
|
411
|
-
nonce
|
|
405
|
+
nonce,
|
|
412
406
|
callData,
|
|
413
407
|
callGasLimit: '0x0',
|
|
414
408
|
verificationGasLimit: '0x0',
|
|
@@ -417,20 +411,42 @@ async function submitFactOnChainLocked(protobufPayload, config, eoa, sender) {
|
|
|
417
411
|
maxPriorityFeePerGas: fast.maxPriorityFeePerGas,
|
|
418
412
|
signature: DUMMY_SIGNATURE,
|
|
419
413
|
};
|
|
420
|
-
if (
|
|
421
|
-
|
|
422
|
-
|
|
414
|
+
if (factory) {
|
|
415
|
+
unsignedOp.factory = factory;
|
|
416
|
+
unsignedOp.factoryData = factoryData;
|
|
423
417
|
}
|
|
424
|
-
//
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
const
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
418
|
+
// 7. Get paymaster sponsorship (fills gas limits + paymaster fields)
|
|
419
|
+
// This is where AA10 "sender already constructed" can occur if initCode
|
|
420
|
+
// is present but the sender is already deployed.
|
|
421
|
+
const sponsorResult = await rpc('pm_sponsorUserOperation', [unsignedOp, entryPoint]);
|
|
422
|
+
Object.assign(unsignedOp, sponsorResult);
|
|
423
|
+
// 8. Hash and sign the UserOp via WASM
|
|
424
|
+
const opJson = JSON.stringify(unsignedOp);
|
|
425
|
+
const hashHex = getWasm().hashUserOp(opJson, entryPoint, BigInt(config.chainId));
|
|
426
|
+
const sigHex = signUserOp(hashHex, eoa.private_key);
|
|
427
|
+
unsignedOp.signature = `0x${sigHex}`;
|
|
428
|
+
// 9. Submit the signed UserOp
|
|
429
|
+
userOpHash = await rpc('eth_sendUserOperation', [unsignedOp, entryPoint]);
|
|
430
|
+
// Success — break out of retry loop
|
|
431
|
+
break;
|
|
432
432
|
}
|
|
433
|
-
|
|
433
|
+
catch (err) {
|
|
434
|
+
const msg = err?.message || '';
|
|
435
|
+
// AA10 "sender already constructed" or AA25 invalid nonce → retry
|
|
436
|
+
if (/AA25|AA10|invalid account nonce|already being processed/i.test(msg)) {
|
|
437
|
+
console.error(`AA25/AA10 detected (attempt ${attempt}/${maxAttempts}), retrying...`);
|
|
438
|
+
// On AA10, force-mark sender as deployed so next retry omits initCode
|
|
439
|
+
if (/AA10/i.test(msg)) {
|
|
440
|
+
forceDeployed.add(sender.toLowerCase());
|
|
441
|
+
console.error('AA10: force-marking sender as deployed, retrying without initCode');
|
|
442
|
+
}
|
|
443
|
+
// Wait for previous UserOp to mine before retrying with fresh nonce.
|
|
444
|
+
// Public RPC won't reflect the new nonce until the tx is on-chain.
|
|
445
|
+
await new Promise(r => setTimeout(r, 15000));
|
|
446
|
+
// Continue to next iteration of retry loop
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
// Not a retryable error — re-throw
|
|
434
450
|
throw err;
|
|
435
451
|
}
|
|
436
452
|
}
|
|
@@ -509,101 +525,51 @@ async function submitFactBatchOnChainLocked(protobufPayloads, config, eoa, sende
|
|
|
509
525
|
const gasPrices = await rpc('pimlico_getUserOperationGasPrice', []);
|
|
510
526
|
const fast = gasPrices.fast;
|
|
511
527
|
const rpcUrl = config.rpcUrl || CONFIG.rpcUrl || getDefaultRpcUrl(config.chainId);
|
|
512
|
-
// Check if Smart Account is deployed (needed for factory/factoryData)
|
|
513
|
-
const { factory, factoryData } = await getInitCode(sender, eoa.address, rpcUrl);
|
|
514
528
|
// Get nonce via bundler (accounts for pending mempool UserOps) with public RPC fallback
|
|
515
529
|
const senderPadded = sender.slice(2).toLowerCase().padStart(64, '0');
|
|
516
530
|
const keyPadded = '0'.repeat(64);
|
|
517
531
|
const nonceCalldata = `0x35567e1a${senderPadded}${keyPadded}`;
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
const nonceResult = await rpc('eth_call', [{ to: entryPoint, data: nonceCalldata }, 'latest']);
|
|
521
|
-
nonce = nonceResult || '0x0';
|
|
522
|
-
}
|
|
523
|
-
catch {
|
|
524
|
-
const nonceJson = await rpcRequest({
|
|
525
|
-
url: rpcUrl,
|
|
526
|
-
headers: { 'Content-Type': 'application/json' },
|
|
527
|
-
method: 'eth_call',
|
|
528
|
-
params: [{ to: entryPoint, data: nonceCalldata }, 'latest'],
|
|
529
|
-
});
|
|
530
|
-
nonce = nonceJson.result || '0x0';
|
|
531
|
-
}
|
|
532
|
-
// Build unsigned UserOp
|
|
533
|
-
const unsignedOp = {
|
|
534
|
-
sender,
|
|
535
|
-
nonce,
|
|
536
|
-
callData,
|
|
537
|
-
callGasLimit: '0x0',
|
|
538
|
-
verificationGasLimit: '0x0',
|
|
539
|
-
preVerificationGas: '0x0',
|
|
540
|
-
maxFeePerGas: fast.maxFeePerGas,
|
|
541
|
-
maxPriorityFeePerGas: fast.maxPriorityFeePerGas,
|
|
542
|
-
signature: DUMMY_SIGNATURE,
|
|
543
|
-
};
|
|
544
|
-
if (factory) {
|
|
545
|
-
unsignedOp.factory = factory;
|
|
546
|
-
unsignedOp.factoryData = factoryData;
|
|
547
|
-
}
|
|
548
|
-
// Gas estimation for batch operations — get accurate gas limits from Pimlico
|
|
549
|
-
// before paymaster sponsorship (can't bump after sponsorship as it invalidates
|
|
550
|
-
// the paymaster's signature, causing AA34).
|
|
551
|
-
if (protobufPayloads.length > 1) {
|
|
532
|
+
// Helper to fetch nonce (with bundler fallback)
|
|
533
|
+
async function fetchNonce() {
|
|
552
534
|
try {
|
|
553
|
-
const
|
|
554
|
-
|
|
555
|
-
unsignedOp.callGasLimit = gasEstimate.callGasLimit;
|
|
556
|
-
if (gasEstimate.verificationGasLimit)
|
|
557
|
-
unsignedOp.verificationGasLimit = gasEstimate.verificationGasLimit;
|
|
558
|
-
if (gasEstimate.preVerificationGas)
|
|
559
|
-
unsignedOp.preVerificationGas = gasEstimate.preVerificationGas;
|
|
535
|
+
const nonceResult = await rpc('eth_call', [{ to: entryPoint, data: nonceCalldata }, 'latest']);
|
|
536
|
+
return nonceResult || '0x0';
|
|
560
537
|
}
|
|
561
538
|
catch {
|
|
562
|
-
|
|
539
|
+
const nonceJson = await rpcRequest({
|
|
540
|
+
url: rpcUrl,
|
|
541
|
+
headers: { 'Content-Type': 'application/json' },
|
|
542
|
+
method: 'eth_call',
|
|
543
|
+
params: [{ to: entryPoint, data: nonceCalldata }, 'latest'],
|
|
544
|
+
});
|
|
545
|
+
return nonceJson.result || '0x0';
|
|
563
546
|
}
|
|
564
547
|
}
|
|
565
|
-
//
|
|
566
|
-
const
|
|
567
|
-
|
|
568
|
-
//
|
|
569
|
-
const opJson = JSON.stringify(unsignedOp);
|
|
570
|
-
const hashHex = getWasm().hashUserOp(opJson, entryPoint, BigInt(config.chainId));
|
|
571
|
-
const sigHex = signUserOp(hashHex, eoa.private_key);
|
|
572
|
-
unsignedOp.signature = `0x${sigHex}`;
|
|
573
|
-
// Submit (with AA25 nonce conflict retry)
|
|
548
|
+
// Track force-deployed senders for AA10 retry (local to this submission attempt)
|
|
549
|
+
const forceDeployed = new Set();
|
|
550
|
+
// Single retry loop: getInitCode → build UserOp → estimate → sponsor → sign → send
|
|
551
|
+
// On AA10 "sender already constructed", mark sender as force-deployed and retry.
|
|
574
552
|
let userOpHash;
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
const { factory: retryFactory, factoryData: retryFactoryData } = await getInitCode(sender, eoa.address, rpcUrl);
|
|
589
|
-
let retryNonce;
|
|
590
|
-
try {
|
|
591
|
-
const retryNonceResult = await rpc('eth_call', [{ to: entryPoint, data: nonceCalldata }, 'latest']);
|
|
592
|
-
retryNonce = retryNonceResult || '0x0';
|
|
593
|
-
}
|
|
594
|
-
catch {
|
|
595
|
-
const retryNonceJson = await rpcRequest({
|
|
596
|
-
url: rpcUrl,
|
|
597
|
-
headers: { 'Content-Type': 'application/json' },
|
|
598
|
-
method: 'eth_call',
|
|
599
|
-
params: [{ to: entryPoint, data: nonceCalldata }, 'latest'],
|
|
600
|
-
});
|
|
601
|
-
retryNonce = retryNonceJson.result || '0x0';
|
|
553
|
+
let attempt = 0;
|
|
554
|
+
const maxAttempts = 2;
|
|
555
|
+
while (attempt < maxAttempts) {
|
|
556
|
+
attempt++;
|
|
557
|
+
try {
|
|
558
|
+
// Check if Smart Account is deployed (needed for factory/factoryData)
|
|
559
|
+
// If force-deployed, skip eth_getCode and return null initCode.
|
|
560
|
+
let factory = null;
|
|
561
|
+
let factoryData = null;
|
|
562
|
+
if (!forceDeployed.has(sender.toLowerCase())) {
|
|
563
|
+
const initCode = await getInitCode(sender, eoa.address, rpcUrl);
|
|
564
|
+
factory = initCode.factory;
|
|
565
|
+
factoryData = initCode.factoryData;
|
|
602
566
|
}
|
|
603
|
-
//
|
|
604
|
-
const
|
|
567
|
+
// Fetch fresh nonce for each attempt
|
|
568
|
+
const nonce = await fetchNonce();
|
|
569
|
+
// Build unsigned UserOp
|
|
570
|
+
const unsignedOp = {
|
|
605
571
|
sender,
|
|
606
|
-
nonce
|
|
572
|
+
nonce,
|
|
607
573
|
callData,
|
|
608
574
|
callGasLimit: '0x0',
|
|
609
575
|
verificationGasLimit: '0x0',
|
|
@@ -612,20 +578,59 @@ async function submitFactBatchOnChainLocked(protobufPayloads, config, eoa, sende
|
|
|
612
578
|
maxPriorityFeePerGas: fast.maxPriorityFeePerGas,
|
|
613
579
|
signature: DUMMY_SIGNATURE,
|
|
614
580
|
};
|
|
615
|
-
if (
|
|
616
|
-
|
|
617
|
-
|
|
581
|
+
if (factory) {
|
|
582
|
+
unsignedOp.factory = factory;
|
|
583
|
+
unsignedOp.factoryData = factoryData;
|
|
618
584
|
}
|
|
619
|
-
//
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
585
|
+
// Gas estimation for batch operations — get accurate gas limits from Pimlico
|
|
586
|
+
// before paymaster sponsorship (can't bump after sponsorship as it invalidates
|
|
587
|
+
// the paymaster's signature, causing AA34).
|
|
588
|
+
if (protobufPayloads.length > 1) {
|
|
589
|
+
try {
|
|
590
|
+
const gasEstimate = await rpc('eth_estimateUserOperationGas', [unsignedOp, entryPoint]);
|
|
591
|
+
if (gasEstimate.callGasLimit)
|
|
592
|
+
unsignedOp.callGasLimit = gasEstimate.callGasLimit;
|
|
593
|
+
if (gasEstimate.verificationGasLimit)
|
|
594
|
+
unsignedOp.verificationGasLimit = gasEstimate.verificationGasLimit;
|
|
595
|
+
if (gasEstimate.preVerificationGas)
|
|
596
|
+
unsignedOp.preVerificationGas = gasEstimate.preVerificationGas;
|
|
597
|
+
}
|
|
598
|
+
catch {
|
|
599
|
+
// If estimation fails, let the paymaster handle it (default behavior)
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
// Paymaster sponsorship (uses gas limits from estimation above for batches)
|
|
603
|
+
// This is where AA10 "sender already constructed" can occur if initCode
|
|
604
|
+
// is present but the sender is already deployed.
|
|
605
|
+
const sponsorResult = await rpc('pm_sponsorUserOperation', [unsignedOp, entryPoint]);
|
|
606
|
+
Object.assign(unsignedOp, sponsorResult);
|
|
607
|
+
// Hash and sign via WASM
|
|
608
|
+
const opJson = JSON.stringify(unsignedOp);
|
|
609
|
+
const hashHex = getWasm().hashUserOp(opJson, entryPoint, BigInt(config.chainId));
|
|
610
|
+
const sigHex = signUserOp(hashHex, eoa.private_key);
|
|
611
|
+
unsignedOp.signature = `0x${sigHex}`;
|
|
612
|
+
// Submit the signed UserOp
|
|
613
|
+
userOpHash = await rpc('eth_sendUserOperation', [unsignedOp, entryPoint]);
|
|
614
|
+
// Success — break out of retry loop
|
|
615
|
+
break;
|
|
627
616
|
}
|
|
628
|
-
|
|
617
|
+
catch (err) {
|
|
618
|
+
const msg = err?.message || '';
|
|
619
|
+
// AA10 "sender already constructed" or AA25 invalid nonce → retry
|
|
620
|
+
if (/AA25|AA10|invalid account nonce|already being processed/i.test(msg)) {
|
|
621
|
+
console.error(`AA25/AA10 detected (batch, attempt ${attempt}/${maxAttempts}), retrying...`);
|
|
622
|
+
// On AA10, force-mark sender as deployed so next retry omits initCode
|
|
623
|
+
if (/AA10/i.test(msg)) {
|
|
624
|
+
forceDeployed.add(sender.toLowerCase());
|
|
625
|
+
console.error('AA10: force-marking sender as deployed, retrying without initCode');
|
|
626
|
+
}
|
|
627
|
+
// Wait for previous UserOp to mine before retrying with fresh nonce.
|
|
628
|
+
// Public RPC won't reflect the new nonce until the tx is on-chain.
|
|
629
|
+
await new Promise(r => setTimeout(r, 15000));
|
|
630
|
+
// Continue to next iteration of retry loop
|
|
631
|
+
continue;
|
|
632
|
+
}
|
|
633
|
+
// Not a retryable error — re-throw
|
|
629
634
|
throw err;
|
|
630
635
|
}
|
|
631
636
|
}
|
package/dist/tr-cli.js
CHANGED
|
@@ -51,7 +51,7 @@ const STATE_PATH = CONFIG.onboardingStatePath;
|
|
|
51
51
|
// Auto-synced by skill/scripts/sync-version.mjs from skill/plugin/package.json::version.
|
|
52
52
|
// Do not edit by hand — running tests will catch drift but the publish workflow
|
|
53
53
|
// rewrites this constant at the start of every npm/ClawHub publish.
|
|
54
|
-
const PLUGIN_VERSION = '3.3.12-rc.
|
|
54
|
+
const PLUGIN_VERSION = '3.3.12-rc.19';
|
|
55
55
|
function die(msg, code = 1) {
|
|
56
56
|
process.stderr.write(`tr: ${msg}\n`);
|
|
57
57
|
process.exit(code);
|
|
@@ -127,6 +127,9 @@ export function startTrajectoryPoller(deps, opts = {}) {
|
|
|
127
127
|
const stateFile = opts.stateFile ?? STATE_FILE;
|
|
128
128
|
const pollAndExtract = async () => {
|
|
129
129
|
try {
|
|
130
|
+
// 3.3.12-rc.19: re-assert slots.memory each tick in case OpenClaw's
|
|
131
|
+
// config-rewrite stripped it after register() (see deps.recheckSlot).
|
|
132
|
+
deps.recheckSlot?.();
|
|
130
133
|
await deps.ensureInitialized();
|
|
131
134
|
if (deps.isPairingPending())
|
|
132
135
|
return;
|
package/index.ts
CHANGED
|
@@ -5239,6 +5239,21 @@ const plugin = {
|
|
|
5239
5239
|
|
|
5240
5240
|
startTrajectoryPoller({
|
|
5241
5241
|
logger: api.logger,
|
|
5242
|
+
// 3.3.12-rc.19: re-assert slots.memory each poll tick. OpenClaw's
|
|
5243
|
+
// config-rewrite-after-restart can strip it after register()'s
|
|
5244
|
+
// self-heal (pop-os reinstall 2026-06-30 → slot reverted to disabled
|
|
5245
|
+
// memory-core → memory_search/memory_get never bound). Cheap fs
|
|
5246
|
+
// read; heals + restarts only if the slot is wrong.
|
|
5247
|
+
recheckSlot: () => {
|
|
5248
|
+
try {
|
|
5249
|
+
if (patchOpenClawConfig(undefined, pluginVersion ?? undefined) === 'patched') {
|
|
5250
|
+
api.logger.warn('TotalReclaw: slots.memory was wrong at poll — re-healed openclaw.json, restarting to apply');
|
|
5251
|
+
try { process.kill(process.pid, 'SIGUSR1'); } catch { /* gateway shutting down */ }
|
|
5252
|
+
}
|
|
5253
|
+
} catch (err) {
|
|
5254
|
+
api.logger.warn(`TotalReclaw: slot recheck failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
5255
|
+
}
|
|
5256
|
+
},
|
|
5242
5257
|
ensureInitialized: () => ensureInitialized(api.logger),
|
|
5243
5258
|
isPairingPending: () => needsSetup,
|
|
5244
5259
|
isImportActive: () => _importInProgress,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@totalreclaw/totalreclaw",
|
|
3
|
-
"version": "3.3.12-rc.
|
|
3
|
+
"version": "3.3.12-rc.19",
|
|
4
4
|
"description": "End-to-end encrypted, agent-portable memory for OpenClaw and any LLM-agent runtime. XChaCha20-Poly1305 with protobuf v4 + on-chain Memory Taxonomy v1 (claim / preference / directive / commitment / episode / summary).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
package/skill.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "totalreclaw",
|
|
3
|
-
"version": "3.3.12-rc.
|
|
3
|
+
"version": "3.3.12-rc.19",
|
|
4
4
|
"description": "End-to-end encrypted memory for AI agents — portable, yours forever. XChaCha20-Poly1305 E2EE: server never sees plaintext.",
|
|
5
5
|
"author": "TotalReclaw Team",
|
|
6
6
|
"license": "MIT",
|
package/subgraph-store.ts
CHANGED
|
@@ -294,6 +294,34 @@ export function __resetSenderLocksForTests(): void {
|
|
|
294
294
|
_senderSubmissionLocks.clear();
|
|
295
295
|
}
|
|
296
296
|
|
|
297
|
+
// ---------------------------------------------------------------------------
|
|
298
|
+
// Test-only WASM mock seams (AA10 submit-path retry test)
|
|
299
|
+
// ---------------------------------------------------------------------------
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Test-only seam: inject a mock WASM module.
|
|
303
|
+
*
|
|
304
|
+
* The AA10 submit-path retry test needs to drive the real submitFactBatchOnChain
|
|
305
|
+
* while controlling WASM behavior (deriveEoa, encodeBatchCall, hashUserOp,
|
|
306
|
+
* signUserOp, getEntryPointAddress). This seam swaps the module-level _wasm
|
|
307
|
+
* reference so the test can provide stubs.
|
|
308
|
+
*
|
|
309
|
+
* MUST be followed by __clearWasmForTests() in teardown.
|
|
310
|
+
*/
|
|
311
|
+
export function __setWasmForTests(mock: any): void {
|
|
312
|
+
_wasm = mock;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Test-only seam: restore the real WASM module.
|
|
317
|
+
*
|
|
318
|
+
* Clears a test-injected mock WASM and resets the module to null so the next
|
|
319
|
+
* getWasm() call reloads the real @totalreclaw/core.
|
|
320
|
+
*/
|
|
321
|
+
export function __clearWasmForTests(): void {
|
|
322
|
+
_wasm = null;
|
|
323
|
+
}
|
|
324
|
+
|
|
297
325
|
/**
|
|
298
326
|
* Check if a Smart Account is deployed and return factory/factoryData if not.
|
|
299
327
|
*
|
|
@@ -413,9 +441,6 @@ async function submitFactOnChainLocked(
|
|
|
413
441
|
|
|
414
442
|
const rpcUrl = config.rpcUrl || CONFIG.rpcUrl || getDefaultRpcUrl(config.chainId);
|
|
415
443
|
|
|
416
|
-
// 4. Check if Smart Account is deployed (needed for factory/factoryData)
|
|
417
|
-
const { factory, factoryData } = await getInitCode(sender, eoa.address, rpcUrl);
|
|
418
|
-
|
|
419
444
|
// 5. Get nonce from EntryPoint via bundler RPC.
|
|
420
445
|
// Routing through the bundler lets Pimlico account for pending mempool
|
|
421
446
|
// UserOps, preventing AA25 nonce conflicts on rapid submissions.
|
|
@@ -426,83 +451,55 @@ async function submitFactOnChainLocked(
|
|
|
426
451
|
const keyPadded = '0'.repeat(64);
|
|
427
452
|
const nonceCalldata = `0x35567e1a${senderPadded}${keyPadded}`;
|
|
428
453
|
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
454
|
+
// Helper to fetch nonce (with bundler fallback)
|
|
455
|
+
async function fetchNonce(): Promise<string> {
|
|
456
|
+
try {
|
|
457
|
+
const nonceResult = await rpc('eth_call', [{ to: entryPoint, data: nonceCalldata }, 'latest']);
|
|
458
|
+
return nonceResult || '0x0';
|
|
459
|
+
} catch {
|
|
460
|
+
// Fallback to public RPC if bundler doesn't support eth_call
|
|
461
|
+
const nonceJson = await rpcRequest({
|
|
462
|
+
url: rpcUrl,
|
|
463
|
+
headers: { 'Content-Type': 'application/json' },
|
|
464
|
+
method: 'eth_call',
|
|
465
|
+
params: [{ to: entryPoint, data: nonceCalldata }, 'latest'],
|
|
466
|
+
});
|
|
467
|
+
return (nonceJson.result as string) || '0x0';
|
|
468
|
+
}
|
|
442
469
|
}
|
|
443
470
|
|
|
444
|
-
//
|
|
445
|
-
const
|
|
446
|
-
sender,
|
|
447
|
-
nonce,
|
|
448
|
-
callData,
|
|
449
|
-
callGasLimit: '0x0',
|
|
450
|
-
verificationGasLimit: '0x0',
|
|
451
|
-
preVerificationGas: '0x0',
|
|
452
|
-
maxFeePerGas: fast.maxFeePerGas,
|
|
453
|
-
maxPriorityFeePerGas: fast.maxPriorityFeePerGas,
|
|
454
|
-
signature: DUMMY_SIGNATURE,
|
|
455
|
-
};
|
|
456
|
-
if (factory) {
|
|
457
|
-
unsignedOp.factory = factory;
|
|
458
|
-
unsignedOp.factoryData = factoryData;
|
|
459
|
-
}
|
|
471
|
+
// Track force-deployed senders for AA10 retry (local to this submission attempt)
|
|
472
|
+
const forceDeployed = new Set<string>();
|
|
460
473
|
|
|
461
|
-
//
|
|
462
|
-
|
|
463
|
-
|
|
474
|
+
// Single retry loop: getInitCode → build UserOp → sponsor → sign → send
|
|
475
|
+
// On AA10 "sender already constructed", mark sender as force-deployed and retry.
|
|
476
|
+
// AA10 can occur at pm_sponsorUserOperation (initCode present on deployed sender)
|
|
477
|
+
// or eth_sendUserOperation (same root cause). This loop handles both.
|
|
478
|
+
let userOpHash: string;
|
|
479
|
+
let attempt = 0;
|
|
480
|
+
const maxAttempts = 2;
|
|
464
481
|
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
const hashHex = getWasm().hashUserOp(opJson, entryPoint, BigInt(config.chainId));
|
|
468
|
-
const sigHex = signUserOp(hashHex, eoa.private_key);
|
|
469
|
-
unsignedOp.signature = `0x${sigHex}`;
|
|
482
|
+
while (attempt < maxAttempts) {
|
|
483
|
+
attempt++;
|
|
470
484
|
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
// so the retry below naturally picks up the post-deployment state.
|
|
481
|
-
|
|
482
|
-
// Wait for previous UserOp to mine before retrying with fresh nonce.
|
|
483
|
-
// Public RPC won't reflect the new nonce until the tx is on-chain.
|
|
484
|
-
await new Promise(r => setTimeout(r, 15000));
|
|
485
|
-
|
|
486
|
-
// Re-fetch initCode and nonce
|
|
487
|
-
const { factory: retryFactory, factoryData: retryFactoryData } = await getInitCode(sender, eoa.address, rpcUrl);
|
|
488
|
-
let retryNonce: string;
|
|
489
|
-
try {
|
|
490
|
-
const retryNonceResult = await rpc('eth_call', [{ to: entryPoint, data: nonceCalldata }, 'latest']);
|
|
491
|
-
retryNonce = retryNonceResult || '0x0';
|
|
492
|
-
} catch {
|
|
493
|
-
const retryNonceJson = await rpcRequest({
|
|
494
|
-
url: rpcUrl,
|
|
495
|
-
headers: { 'Content-Type': 'application/json' },
|
|
496
|
-
method: 'eth_call',
|
|
497
|
-
params: [{ to: entryPoint, data: nonceCalldata }, 'latest'],
|
|
498
|
-
});
|
|
499
|
-
retryNonce = (retryNonceJson.result as string) || '0x0';
|
|
485
|
+
try {
|
|
486
|
+
// 4. Check if Smart Account is deployed (needed for factory/factoryData)
|
|
487
|
+
// If force-deployed, skip eth_getCode and return null initCode.
|
|
488
|
+
let factory: string | null = null;
|
|
489
|
+
let factoryData: string | null = null;
|
|
490
|
+
if (!forceDeployed.has(sender.toLowerCase())) {
|
|
491
|
+
const initCode = await getInitCode(sender, eoa.address, rpcUrl);
|
|
492
|
+
factory = initCode.factory;
|
|
493
|
+
factoryData = initCode.factoryData;
|
|
500
494
|
}
|
|
501
495
|
|
|
502
|
-
//
|
|
503
|
-
const
|
|
496
|
+
// Fetch fresh nonce for each attempt
|
|
497
|
+
const nonce = await fetchNonce();
|
|
498
|
+
|
|
499
|
+
// 6. Build unsigned UserOp (v0.7 fields, camelCase for Rust JSON serde)
|
|
500
|
+
const unsignedOp: Record<string, any> = {
|
|
504
501
|
sender,
|
|
505
|
-
nonce
|
|
502
|
+
nonce,
|
|
506
503
|
callData,
|
|
507
504
|
callGasLimit: '0x0',
|
|
508
505
|
verificationGasLimit: '0x0',
|
|
@@ -511,21 +508,44 @@ async function submitFactOnChainLocked(
|
|
|
511
508
|
maxPriorityFeePerGas: fast.maxPriorityFeePerGas,
|
|
512
509
|
signature: DUMMY_SIGNATURE,
|
|
513
510
|
};
|
|
514
|
-
if (
|
|
515
|
-
|
|
516
|
-
|
|
511
|
+
if (factory) {
|
|
512
|
+
unsignedOp.factory = factory;
|
|
513
|
+
unsignedOp.factoryData = factoryData;
|
|
517
514
|
}
|
|
518
515
|
|
|
519
|
-
//
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
const
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
516
|
+
// 7. Get paymaster sponsorship (fills gas limits + paymaster fields)
|
|
517
|
+
// This is where AA10 "sender already constructed" can occur if initCode
|
|
518
|
+
// is present but the sender is already deployed.
|
|
519
|
+
const sponsorResult = await rpc('pm_sponsorUserOperation', [unsignedOp, entryPoint]);
|
|
520
|
+
Object.assign(unsignedOp, sponsorResult);
|
|
521
|
+
|
|
522
|
+
// 8. Hash and sign the UserOp via WASM
|
|
523
|
+
const opJson = JSON.stringify(unsignedOp);
|
|
524
|
+
const hashHex = getWasm().hashUserOp(opJson, entryPoint, BigInt(config.chainId));
|
|
525
|
+
const sigHex = signUserOp(hashHex, eoa.private_key);
|
|
526
|
+
unsignedOp.signature = `0x${sigHex}`;
|
|
527
|
+
|
|
528
|
+
// 9. Submit the signed UserOp
|
|
529
|
+
userOpHash = await rpc('eth_sendUserOperation', [unsignedOp, entryPoint]);
|
|
530
|
+
// Success — break out of retry loop
|
|
531
|
+
break;
|
|
532
|
+
} catch (err: any) {
|
|
533
|
+
const msg = err?.message || '';
|
|
534
|
+
// AA10 "sender already constructed" or AA25 invalid nonce → retry
|
|
535
|
+
if (/AA25|AA10|invalid account nonce|already being processed/i.test(msg)) {
|
|
536
|
+
console.error(`AA25/AA10 detected (attempt ${attempt}/${maxAttempts}), retrying...`);
|
|
537
|
+
// On AA10, force-mark sender as deployed so next retry omits initCode
|
|
538
|
+
if (/AA10/i.test(msg)) {
|
|
539
|
+
forceDeployed.add(sender.toLowerCase());
|
|
540
|
+
console.error('AA10: force-marking sender as deployed, retrying without initCode');
|
|
541
|
+
}
|
|
542
|
+
// Wait for previous UserOp to mine before retrying with fresh nonce.
|
|
543
|
+
// Public RPC won't reflect the new nonce until the tx is on-chain.
|
|
544
|
+
await new Promise(r => setTimeout(r, 15000));
|
|
545
|
+
// Continue to next iteration of retry loop
|
|
546
|
+
continue;
|
|
547
|
+
}
|
|
548
|
+
// Not a retryable error — re-throw
|
|
529
549
|
throw err;
|
|
530
550
|
}
|
|
531
551
|
}
|
|
@@ -625,104 +645,57 @@ async function submitFactBatchOnChainLocked(
|
|
|
625
645
|
|
|
626
646
|
const rpcUrl = config.rpcUrl || CONFIG.rpcUrl || getDefaultRpcUrl(config.chainId);
|
|
627
647
|
|
|
628
|
-
// Check if Smart Account is deployed (needed for factory/factoryData)
|
|
629
|
-
const { factory, factoryData } = await getInitCode(sender, eoa.address, rpcUrl);
|
|
630
|
-
|
|
631
648
|
// Get nonce via bundler (accounts for pending mempool UserOps) with public RPC fallback
|
|
632
649
|
const senderPadded = sender.slice(2).toLowerCase().padStart(64, '0');
|
|
633
650
|
const keyPadded = '0'.repeat(64);
|
|
634
651
|
const nonceCalldata = `0x35567e1a${senderPadded}${keyPadded}`;
|
|
635
652
|
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
const nonceResult = await rpc('eth_call', [{ to: entryPoint, data: nonceCalldata }, 'latest']);
|
|
639
|
-
nonce = nonceResult || '0x0';
|
|
640
|
-
} catch {
|
|
641
|
-
const nonceJson = await rpcRequest({
|
|
642
|
-
url: rpcUrl,
|
|
643
|
-
headers: { 'Content-Type': 'application/json' },
|
|
644
|
-
method: 'eth_call',
|
|
645
|
-
params: [{ to: entryPoint, data: nonceCalldata }, 'latest'],
|
|
646
|
-
});
|
|
647
|
-
nonce = (nonceJson.result as string) || '0x0';
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
// Build unsigned UserOp
|
|
651
|
-
const unsignedOp: Record<string, any> = {
|
|
652
|
-
sender,
|
|
653
|
-
nonce,
|
|
654
|
-
callData,
|
|
655
|
-
callGasLimit: '0x0',
|
|
656
|
-
verificationGasLimit: '0x0',
|
|
657
|
-
preVerificationGas: '0x0',
|
|
658
|
-
maxFeePerGas: fast.maxFeePerGas,
|
|
659
|
-
maxPriorityFeePerGas: fast.maxPriorityFeePerGas,
|
|
660
|
-
signature: DUMMY_SIGNATURE,
|
|
661
|
-
};
|
|
662
|
-
if (factory) {
|
|
663
|
-
unsignedOp.factory = factory;
|
|
664
|
-
unsignedOp.factoryData = factoryData;
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
// Gas estimation for batch operations — get accurate gas limits from Pimlico
|
|
668
|
-
// before paymaster sponsorship (can't bump after sponsorship as it invalidates
|
|
669
|
-
// the paymaster's signature, causing AA34).
|
|
670
|
-
if (protobufPayloads.length > 1) {
|
|
653
|
+
// Helper to fetch nonce (with bundler fallback)
|
|
654
|
+
async function fetchNonce(): Promise<string> {
|
|
671
655
|
try {
|
|
672
|
-
const
|
|
673
|
-
|
|
674
|
-
if (gasEstimate.verificationGasLimit) unsignedOp.verificationGasLimit = gasEstimate.verificationGasLimit;
|
|
675
|
-
if (gasEstimate.preVerificationGas) unsignedOp.preVerificationGas = gasEstimate.preVerificationGas;
|
|
656
|
+
const nonceResult = await rpc('eth_call', [{ to: entryPoint, data: nonceCalldata }, 'latest']);
|
|
657
|
+
return nonceResult || '0x0';
|
|
676
658
|
} catch {
|
|
677
|
-
|
|
659
|
+
const nonceJson = await rpcRequest({
|
|
660
|
+
url: rpcUrl,
|
|
661
|
+
headers: { 'Content-Type': 'application/json' },
|
|
662
|
+
method: 'eth_call',
|
|
663
|
+
params: [{ to: entryPoint, data: nonceCalldata }, 'latest'],
|
|
664
|
+
});
|
|
665
|
+
return (nonceJson.result as string) || '0x0';
|
|
678
666
|
}
|
|
679
667
|
}
|
|
680
668
|
|
|
681
|
-
//
|
|
682
|
-
const
|
|
683
|
-
Object.assign(unsignedOp, sponsorResult);
|
|
684
|
-
|
|
685
|
-
// Hash and sign via WASM
|
|
686
|
-
const opJson = JSON.stringify(unsignedOp);
|
|
687
|
-
const hashHex = getWasm().hashUserOp(opJson, entryPoint, BigInt(config.chainId));
|
|
688
|
-
const sigHex = signUserOp(hashHex, eoa.private_key);
|
|
689
|
-
unsignedOp.signature = `0x${sigHex}`;
|
|
669
|
+
// Track force-deployed senders for AA10 retry (local to this submission attempt)
|
|
670
|
+
const forceDeployed = new Set<string>();
|
|
690
671
|
|
|
691
|
-
//
|
|
672
|
+
// Single retry loop: getInitCode → build UserOp → estimate → sponsor → sign → send
|
|
673
|
+
// On AA10 "sender already constructed", mark sender as force-deployed and retry.
|
|
692
674
|
let userOpHash: string;
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
//
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
let retryNonce: string;
|
|
709
|
-
try {
|
|
710
|
-
const retryNonceResult = await rpc('eth_call', [{ to: entryPoint, data: nonceCalldata }, 'latest']);
|
|
711
|
-
retryNonce = retryNonceResult || '0x0';
|
|
712
|
-
} catch {
|
|
713
|
-
const retryNonceJson = await rpcRequest({
|
|
714
|
-
url: rpcUrl,
|
|
715
|
-
headers: { 'Content-Type': 'application/json' },
|
|
716
|
-
method: 'eth_call',
|
|
717
|
-
params: [{ to: entryPoint, data: nonceCalldata }, 'latest'],
|
|
718
|
-
});
|
|
719
|
-
retryNonce = (retryNonceJson.result as string) || '0x0';
|
|
675
|
+
let attempt = 0;
|
|
676
|
+
const maxAttempts = 2;
|
|
677
|
+
|
|
678
|
+
while (attempt < maxAttempts) {
|
|
679
|
+
attempt++;
|
|
680
|
+
|
|
681
|
+
try {
|
|
682
|
+
// Check if Smart Account is deployed (needed for factory/factoryData)
|
|
683
|
+
// If force-deployed, skip eth_getCode and return null initCode.
|
|
684
|
+
let factory: string | null = null;
|
|
685
|
+
let factoryData: string | null = null;
|
|
686
|
+
if (!forceDeployed.has(sender.toLowerCase())) {
|
|
687
|
+
const initCode = await getInitCode(sender, eoa.address, rpcUrl);
|
|
688
|
+
factory = initCode.factory;
|
|
689
|
+
factoryData = initCode.factoryData;
|
|
720
690
|
}
|
|
721
691
|
|
|
722
|
-
//
|
|
723
|
-
const
|
|
692
|
+
// Fetch fresh nonce for each attempt
|
|
693
|
+
const nonce = await fetchNonce();
|
|
694
|
+
|
|
695
|
+
// Build unsigned UserOp
|
|
696
|
+
const unsignedOp: Record<string, any> = {
|
|
724
697
|
sender,
|
|
725
|
-
nonce
|
|
698
|
+
nonce,
|
|
726
699
|
callData,
|
|
727
700
|
callGasLimit: '0x0',
|
|
728
701
|
verificationGasLimit: '0x0',
|
|
@@ -731,21 +704,58 @@ async function submitFactBatchOnChainLocked(
|
|
|
731
704
|
maxPriorityFeePerGas: fast.maxPriorityFeePerGas,
|
|
732
705
|
signature: DUMMY_SIGNATURE,
|
|
733
706
|
};
|
|
734
|
-
if (
|
|
735
|
-
|
|
736
|
-
|
|
707
|
+
if (factory) {
|
|
708
|
+
unsignedOp.factory = factory;
|
|
709
|
+
unsignedOp.factoryData = factoryData;
|
|
737
710
|
}
|
|
738
711
|
|
|
739
|
-
//
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
712
|
+
// Gas estimation for batch operations — get accurate gas limits from Pimlico
|
|
713
|
+
// before paymaster sponsorship (can't bump after sponsorship as it invalidates
|
|
714
|
+
// the paymaster's signature, causing AA34).
|
|
715
|
+
if (protobufPayloads.length > 1) {
|
|
716
|
+
try {
|
|
717
|
+
const gasEstimate = await rpc('eth_estimateUserOperationGas', [unsignedOp, entryPoint]);
|
|
718
|
+
if (gasEstimate.callGasLimit) unsignedOp.callGasLimit = gasEstimate.callGasLimit;
|
|
719
|
+
if (gasEstimate.verificationGasLimit) unsignedOp.verificationGasLimit = gasEstimate.verificationGasLimit;
|
|
720
|
+
if (gasEstimate.preVerificationGas) unsignedOp.preVerificationGas = gasEstimate.preVerificationGas;
|
|
721
|
+
} catch {
|
|
722
|
+
// If estimation fails, let the paymaster handle it (default behavior)
|
|
723
|
+
}
|
|
724
|
+
}
|
|
746
725
|
|
|
747
|
-
|
|
748
|
-
|
|
726
|
+
// Paymaster sponsorship (uses gas limits from estimation above for batches)
|
|
727
|
+
// This is where AA10 "sender already constructed" can occur if initCode
|
|
728
|
+
// is present but the sender is already deployed.
|
|
729
|
+
const sponsorResult = await rpc('pm_sponsorUserOperation', [unsignedOp, entryPoint]);
|
|
730
|
+
Object.assign(unsignedOp, sponsorResult);
|
|
731
|
+
|
|
732
|
+
// Hash and sign via WASM
|
|
733
|
+
const opJson = JSON.stringify(unsignedOp);
|
|
734
|
+
const hashHex = getWasm().hashUserOp(opJson, entryPoint, BigInt(config.chainId));
|
|
735
|
+
const sigHex = signUserOp(hashHex, eoa.private_key);
|
|
736
|
+
unsignedOp.signature = `0x${sigHex}`;
|
|
737
|
+
|
|
738
|
+
// Submit the signed UserOp
|
|
739
|
+
userOpHash = await rpc('eth_sendUserOperation', [unsignedOp, entryPoint]);
|
|
740
|
+
// Success — break out of retry loop
|
|
741
|
+
break;
|
|
742
|
+
} catch (err: any) {
|
|
743
|
+
const msg = err?.message || '';
|
|
744
|
+
// AA10 "sender already constructed" or AA25 invalid nonce → retry
|
|
745
|
+
if (/AA25|AA10|invalid account nonce|already being processed/i.test(msg)) {
|
|
746
|
+
console.error(`AA25/AA10 detected (batch, attempt ${attempt}/${maxAttempts}), retrying...`);
|
|
747
|
+
// On AA10, force-mark sender as deployed so next retry omits initCode
|
|
748
|
+
if (/AA10/i.test(msg)) {
|
|
749
|
+
forceDeployed.add(sender.toLowerCase());
|
|
750
|
+
console.error('AA10: force-marking sender as deployed, retrying without initCode');
|
|
751
|
+
}
|
|
752
|
+
// Wait for previous UserOp to mine before retrying with fresh nonce.
|
|
753
|
+
// Public RPC won't reflect the new nonce until the tx is on-chain.
|
|
754
|
+
await new Promise(r => setTimeout(r, 15000));
|
|
755
|
+
// Continue to next iteration of retry loop
|
|
756
|
+
continue;
|
|
757
|
+
}
|
|
758
|
+
// Not a retryable error — re-throw
|
|
749
759
|
throw err;
|
|
750
760
|
}
|
|
751
761
|
}
|
package/tr-cli.ts
CHANGED
|
@@ -68,7 +68,7 @@ const STATE_PATH = CONFIG.onboardingStatePath;
|
|
|
68
68
|
// Auto-synced by skill/scripts/sync-version.mjs from skill/plugin/package.json::version.
|
|
69
69
|
// Do not edit by hand — running tests will catch drift but the publish workflow
|
|
70
70
|
// rewrites this constant at the start of every npm/ClawHub publish.
|
|
71
|
-
const PLUGIN_VERSION = '3.3.12-rc.
|
|
71
|
+
const PLUGIN_VERSION = '3.3.12-rc.19';
|
|
72
72
|
|
|
73
73
|
function die(msg: string, code = 1): never {
|
|
74
74
|
process.stderr.write(`tr: ${msg}\n`);
|
package/trajectory-poller.ts
CHANGED
|
@@ -104,6 +104,18 @@ export interface TrajectoryPollerDeps {
|
|
|
104
104
|
error: (msg: string) => void;
|
|
105
105
|
};
|
|
106
106
|
|
|
107
|
+
/**
|
|
108
|
+
* 3.3.12-rc.19: optional slot-self-heal hook invoked once per poll
|
|
109
|
+
* tick. OpenClaw's config-rewrite-after-restart can strip
|
|
110
|
+
* plugins.slots.memory after register()'s self-heal ran (observed on
|
|
111
|
+
* pop-os reinstall 2026-06-30 → slot reverted to disabled memory-core
|
|
112
|
+
* → memory_search/memory_get never bound). The host wires this to
|
|
113
|
+
* patchOpenClawConfig + a SIGUSR1 restart so the slot is re-asserted
|
|
114
|
+
* on the next tick if it was clobbered. Runs before the pairing/
|
|
115
|
+
* import gates so it fires even when extraction is deferred.
|
|
116
|
+
*/
|
|
117
|
+
recheckSlot?: () => void;
|
|
118
|
+
|
|
107
119
|
/** Initialization gate — same one the agent_end hook uses. */
|
|
108
120
|
ensureInitialized: () => Promise<void>;
|
|
109
121
|
|
|
@@ -218,6 +230,9 @@ export function startTrajectoryPoller(
|
|
|
218
230
|
|
|
219
231
|
const pollAndExtract = async (): Promise<void> => {
|
|
220
232
|
try {
|
|
233
|
+
// 3.3.12-rc.19: re-assert slots.memory each tick in case OpenClaw's
|
|
234
|
+
// config-rewrite stripped it after register() (see deps.recheckSlot).
|
|
235
|
+
deps.recheckSlot?.();
|
|
221
236
|
await deps.ensureInitialized();
|
|
222
237
|
if (deps.isPairingPending()) return;
|
|
223
238
|
if (deps.isImportActive()) return;
|