Skip to content

Commit a8360db

Browse files
p-diogoclaude
andcommitted
fix: QA-driven fixes for Hermes + OpenClaw integrations
Hermes: - Guide: ~600MB → ~34MB disk space (e5-small model) - Setup tool: generate mnemonic when none provided (via eth_account) - Schema: recovery_phrase now optional - Bump to 1.2.0 for PyPI republish OpenClaw: - Pimlico 429: add exponential backoff retry (1s/2s/4s, max 3) - Lazy-load @totalreclaw/core WASM (fixes first-boot crash) - Add INFO-level txHash logging for successful stores - Guide: note about restart after first install Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent d34e4a5 commit a8360db

10 files changed

Lines changed: 140 additions & 71 deletions

File tree

‎docs/guides/beta-tester-guide.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ openclaw skills install totalreclaw
3232
openclaw plugins install ~/.openclaw/workspace/skills/totalreclaw
3333
```
3434

35-
Restart the gateway if prompted. You may see a "1 suspicious code pattern" warning -- this is normal (the plugin reads configuration and makes encrypted network calls).
35+
Restart the gateway if prompted. You may see a "1 suspicious code pattern" warning -- this is normal (the plugin reads configuration and makes encrypted network calls). If the plugin fails to load on the very first install, restart the gateway once more -- npm dependencies may still be installing in the background.
3636

3737
**Step 2: Set up your recovery phrase**
3838

‎docs/guides/hermes-setup.md‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ Set up TotalReclaw as the encrypted memory layer for your Hermes Agent. Your mem
77
- **Hermes Agent v0.5.0+** (with plugin lifecycle hooks)
88
- **Python 3.11+**
99
- **C compiler** (gcc/g++) -- needed to build PyStemmer from source
10-
- ~600 MB disk space for the local embedding model (one-time download)
10+
- ~34 MB disk space for the local embedding model (one-time download)
1111

1212
> **Docker users:** On slim images (e.g., `python:3.12-slim`), install a C compiler first:
1313
> ```bash
@@ -57,8 +57,8 @@ The first time you mention anything worth remembering, the agent will:
5757

5858
1. Detect that TotalReclaw is not configured yet
5959
2. Ask: "Do you have an existing recovery phrase, or should I generate a new one?"
60-
3. Run `totalreclaw_setup` with your phrase
61-
4. Register with the relay, download the embedding model, and activate
60+
3. Run `totalreclaw_setup` — if you don't have a phrase, the tool generates one automatically
61+
4. Register with the relay, download the embedding model (~34 MB, one-time), and activate
6262

6363
Your recovery phrase and credentials are saved to `~/.totalreclaw/credentials.json` (owner-only permissions). On subsequent restarts, the plugin loads them automatically.
6464

@@ -179,9 +179,9 @@ Or set the env var (auto-registers with relay on first use):
179179
export TOTALRECLAW_RECOVERY_PHRASE="your twelve word phrase here"
180180
```
181181

182-
### Embedding model download slow
182+
### Embedding model download
183183

184-
The first recall/remember downloads ~600 MB. Subsequent calls use the cached model. If download fails, the plugin falls back to keyword-only search (no semantic similarity).
184+
The first recall/remember downloads ~34 MB (e5-small model). Subsequent calls use the cached model. If download fails, the plugin falls back to keyword-only search (no semantic similarity).
185185

186186
### Permission denied on model download (Docker)
187187

‎python/pyproject.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "totalreclaw"
7-
version = "1.1.0"
7+
version = "1.2.0"
88
description = "End-to-end encrypted memory for AI agents — Python client"
99
readme = "README.md"
1010
license = "MIT"

‎python/src/totalreclaw/hermes/schemas.py‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -81,17 +81,17 @@
8181
SETUP = {
8282
"name": "totalreclaw_setup",
8383
"description": (
84-
"Configure TotalReclaw with a recovery phrase. Run this once to set up "
85-
"the encrypted memory vault."
84+
"Configure TotalReclaw with a recovery phrase. If no phrase is provided, "
85+
"a new one is generated automatically. Run this once to set up the "
86+
"encrypted memory vault."
8687
),
8788
"parameters": {
8889
"type": "object",
8990
"properties": {
9091
"recovery_phrase": {
9192
"type": "string",
92-
"description": "BIP-39 recovery phrase (12 or 24 words)",
93+
"description": "BIP-39 recovery phrase (12 or 24 words). If omitted, a new phrase is generated.",
9394
},
9495
},
95-
"required": ["recovery_phrase"],
9696
},
9797
}

‎python/src/totalreclaw/hermes/tools.py‎

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -123,17 +123,31 @@ async def status(args: dict, state: "PluginState", **kwargs) -> str:
123123

124124

125125
def setup(args: dict, state: "PluginState", **kwargs) -> str:
126-
"""Configure TotalReclaw credentials."""
126+
"""Configure TotalReclaw credentials. Generates a new recovery phrase if none provided."""
127127
recovery_phrase = args.get("recovery_phrase", "").strip()
128+
generated = False
129+
128130
if not recovery_phrase:
129-
return json.dumps({"error": "No recovery phrase provided"})
131+
# Generate a new BIP-39 mnemonic using eth_account (already a dependency)
132+
try:
133+
from eth_account import Account
134+
Account.enable_unaudited_hdwallet_features()
135+
_acct, recovery_phrase = Account.create_with_mnemonic()
136+
generated = True
137+
except Exception as e:
138+
logger.error("Failed to generate recovery phrase: %s", e)
139+
return json.dumps({"error": f"Failed to generate recovery phrase: {e}"})
130140

131141
try:
132142
state.configure(recovery_phrase)
133-
return json.dumps({
143+
result = {
134144
"configured": True,
135145
"wallet_address": state.get_client().wallet_address,
136-
})
146+
}
147+
if generated:
148+
result["recovery_phrase"] = recovery_phrase
149+
result["generated"] = True
150+
return json.dumps(result)
137151
except Exception as e:
138152
logger.error("totalreclaw_setup failed: %s", e)
139153
return json.dumps({"error": str(e)})

‎python/tests/test_hermes_plugin.py‎

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,9 @@ def test_status_schema(self):
170170

171171
def test_setup_schema(self):
172172
assert schemas.SETUP["name"] == "totalreclaw_setup"
173-
assert "recovery_phrase" in schemas.SETUP["parameters"]["required"]
173+
assert "recovery_phrase" in schemas.SETUP["parameters"]["properties"]
174+
# recovery_phrase is optional (generates one if omitted)
175+
assert "required" not in schemas.SETUP["parameters"]
174176

175177

176178
class TestTools:
@@ -201,13 +203,18 @@ async def test_recall_not_configured(self):
201203
result = json.loads(await recall({"query": "test"}, state))
202204
assert "error" in result
203205

204-
def test_setup_no_phrase(self):
206+
def test_setup_no_phrase_generates_one(self):
205207
from totalreclaw.hermes.tools import setup
206208
with patch.dict(os.environ, {}, clear=True):
207209
with patch.object(Path, "exists", return_value=False):
208210
state = PluginState()
209-
result = json.loads(setup({"recovery_phrase": ""}, state))
210-
assert "error" in result
211+
with patch.object(Path, "write_text"), patch.object(Path, "chmod"), \
212+
patch.object(Path, "mkdir", return_value=None):
213+
result = json.loads(setup({"recovery_phrase": ""}, state))
214+
assert result["configured"] is True
215+
assert result["generated"] is True
216+
assert len(result["recovery_phrase"].split()) == 12
217+
assert result["wallet_address"].startswith("0x")
211218

212219
def test_setup_success(self):
213220
from totalreclaw.hermes.tools import setup

‎skill/plugin/crypto.ts‎

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,12 @@
1515
* -> HKDF-SHA256(seed, salt, "openmemory-dedup-v1", 32) -> dedupKey
1616
*/
1717

18-
import * as wasm from '@totalreclaw/core';
18+
// Lazy-load WASM to avoid crash when npm install hasn't finished yet.
19+
let _wasm: typeof import('@totalreclaw/core') | null = null;
20+
function getWasm() {
21+
if (!_wasm) _wasm = require('@totalreclaw/core');
22+
return _wasm;
23+
}
1924

2025
// ---------------------------------------------------------------------------
2126
// BIP-39 Validation
@@ -58,9 +63,9 @@ export function deriveKeys(
5863
// Try strict validation first, fall back to lenient
5964
let result: { auth_key: string; encryption_key: string; dedup_key: string; salt: string };
6065
try {
61-
result = wasm.deriveKeysFromMnemonic(trimmed);
66+
result = getWasm().deriveKeysFromMnemonic(trimmed);
6267
} catch {
63-
result = wasm.deriveKeysFromMnemonicLenient(trimmed);
68+
result = getWasm().deriveKeysFromMnemonicLenient(trimmed);
6469
}
6570

6671
return {
@@ -84,7 +89,7 @@ export function deriveLshSeed(
8489
password: string,
8590
salt: Buffer,
8691
): Uint8Array {
87-
const seedHex = wasm.deriveLshSeed(password.trim(), salt.toString('hex'));
92+
const seedHex = getWasm().deriveLshSeed(password.trim(), salt.toString('hex'));
8893
return new Uint8Array(Buffer.from(seedHex, 'hex'));
8994
}
9095

@@ -96,7 +101,7 @@ export function deriveLshSeed(
96101
* Compute the SHA-256 hash of the auth key.
97102
*/
98103
export function computeAuthKeyHash(authKey: Buffer): string {
99-
return wasm.computeAuthKeyHash(authKey.toString('hex'));
104+
return getWasm().computeAuthKeyHash(authKey.toString('hex'));
100105
}
101106

102107
// ---------------------------------------------------------------------------
@@ -110,14 +115,14 @@ export function computeAuthKeyHash(authKey: Buffer): string {
110115
* [iv: 12 bytes][tag: 16 bytes][ciphertext: variable]
111116
*/
112117
export function encrypt(plaintext: string, encryptionKey: Buffer): string {
113-
return wasm.encrypt(plaintext, encryptionKey.toString('hex'));
118+
return getWasm().encrypt(plaintext, encryptionKey.toString('hex'));
114119
}
115120

116121
/**
117122
* Decrypt a base64-encoded AES-256-GCM blob back to a UTF-8 string.
118123
*/
119124
export function decrypt(encryptedBase64: string, encryptionKey: Buffer): string {
120-
return wasm.decrypt(encryptedBase64, encryptionKey.toString('hex'));
125+
return getWasm().decrypt(encryptedBase64, encryptionKey.toString('hex'));
121126
}
122127

123128
// ---------------------------------------------------------------------------
@@ -131,7 +136,7 @@ export function decrypt(encryptedBase64: string, encryptionKey: Buffer): string
131136
* and SHA-256 hashing.
132137
*/
133138
export function generateBlindIndices(text: string): string[] {
134-
return wasm.generateBlindIndices(text);
139+
return getWasm().generateBlindIndices(text);
135140
}
136141

137142
// ---------------------------------------------------------------------------
@@ -144,5 +149,5 @@ export function generateBlindIndices(text: string): string[] {
144149
* @returns 64-character hex string.
145150
*/
146151
export function generateContentFingerprint(plaintext: string, dedupKey: Buffer): string {
147-
return wasm.generateContentFingerprint(plaintext, dedupKey.toString('hex'));
152+
return getWasm().generateContentFingerprint(plaintext, dedupKey.toString('hex'));
148153
}

‎skill/plugin/index.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1867,6 +1867,7 @@ const plugin = {
18671867
if (!result.success) {
18681868
throw new Error(`On-chain submission failed (tx=${result.txHash?.slice(0, 10) || 'none'}…)`);
18691869
}
1870+
api.logger.info(`totalreclaw_remember: stored on-chain (tx=${result.txHash.slice(0, 10)}…)`);
18701871
} else {
18711872
await apiClient!.store(userId!, [factPayload], authKeyHex!);
18721873
}

‎skill/plugin/lsh.ts‎

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,12 @@
77
* Default parameters: 32 bits per table, 20 tables.
88
*/
99

10-
import { WasmLshHasher } from '@totalreclaw/core';
10+
// Lazy-load WASM to avoid crash when npm install hasn't finished yet.
11+
let _WasmLshHasher: typeof import('@totalreclaw/core')['WasmLshHasher'] | null = null;
12+
function getWasmLshHasher() {
13+
if (!_WasmLshHasher) _WasmLshHasher = require('@totalreclaw/core').WasmLshHasher;
14+
return _WasmLshHasher!;
15+
}
1116

1217
/**
1318
* Random Hyperplane LSH hasher.
@@ -16,7 +21,7 @@ import { WasmLshHasher } from '@totalreclaw/core';
1621
* Construct once per session; call `hash()` for every store/search operation.
1722
*/
1823
export class LSHHasher {
19-
private inner: WasmLshHasher;
24+
private inner: InstanceType<typeof import('@totalreclaw/core')['WasmLshHasher']>;
2025

2126
/**
2227
* Create a new LSH hasher.
@@ -33,7 +38,7 @@ export class LSHHasher {
3338
nBits: number = 32,
3439
) {
3540
const seedHex = Buffer.from(seed).toString('hex');
36-
this.inner = WasmLshHasher.withParams(seedHex, dims, nTables, nBits);
41+
this.inner = getWasmLshHasher().withParams(seedHex, dims, nTables, nBits);
3742
}
3843

3944
/**

0 commit comments

Comments
 (0)