@totalreclaw/totalreclaw 3.3.4-rc.1 → 3.3.5-rc.1

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/CHANGELOG.md CHANGED
@@ -4,6 +4,37 @@ All notable changes to `@totalreclaw/totalreclaw` (the OpenClaw plugin) are docu
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [3.3.5-rc.1] — 2026-04-30
8
+
9
+ UX bundle from Pedro's QA on 3.3.4-rc.2:
10
+
11
+ - **Drop the "yes/no" consent gate before pair setup.** The yes-gate was added
12
+ in PR #160's safety-tone rewrite to give safety-trained agents an explicit
13
+ green light before account creation. In practice the user already consents
14
+ by pasting "Install TotalReclaw" + the canonical URL, and the URL+PIN handed
15
+ back from `totalreclaw_pair` is the real consent moment (the user has to
16
+ open the page in their browser to proceed). The extra gate added friction
17
+ for zero security benefit. Removed from `skill/plugin/SKILL.md`,
18
+ `skill/SKILL.md`, and `docs/guides/openclaw-setup.md`. The Hermes guide
19
+ retains its own gate (handled separately).
20
+ - **Tighten silence rules to suppress chat-channel mid-edit churn.** OpenClaw's
21
+ Telegram channel edits the bot's messages live as the agent's tool calls
22
+ progress, so every "Let me check…" / "Plugin loaded. Let me verify…" /
23
+ "I'll now…" line shows up to the user as visible edit churn even if the
24
+ transport later "deletes" or rewrites it. Added a top-level rule at the
25
+ start of SKILL.md: "Emit ONLY the user-visible lines. Do not narrate tool
26
+ calls. Do not describe what you're doing or about to do." Strengthened the
27
+ forbidden-vocabulary deny-list with the exact patterns Pedro saw in his
28
+ QA today. Re-stated the canonical user-visible line set as 5–6 lines
29
+ TOTAL.
30
+ - **Strengthen `/restart` instructions.** Pedro's chat agent on rc.4-rc.2
31
+ again said "I need permission to restart" instead of issuing the slash
32
+ command — same anti-pattern that PR #163 / #173 / #174 tried to suppress.
33
+ Made the SKILL.md instruction more imperative and concrete: "Your IMMEDIATE
34
+ next message must be the literal slash command `/restart` — nothing else."
35
+ Added explicit "Do not propose alternatives" guidance for the unauthorized
36
+ fallback path. Mirrored in `docs/guides/openclaw-setup.md`.
37
+
7
38
  ## [3.3.3-rc.1] — 2026-04-30
8
39
 
9
40
  Combined RC bundle:
package/SKILL.md CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: totalreclaw
3
3
  description: "Install + set up TotalReclaw encrypted memory for OpenClaw, then use totalreclaw_remember / totalreclaw_recall. Trigger on 'install TotalReclaw', 'set up TotalReclaw', 'restore my recovery phrase', or any remember / recall request."
4
- version: 3.3.4-rc.1
4
+ version: 3.3.5-rc.1
5
5
  author: TotalReclaw Team
6
6
  license: MIT
7
7
  homepage: https://totalreclaw.xyz
@@ -26,11 +26,35 @@ metadata:
26
26
 
27
27
  > The canonical install flow lives at <https://github.com/p-diogo/totalreclaw/blob/main/docs/guides/openclaw-setup.md>. If a user pastes that URL in chat, fetch it and follow its agent-instructions section. This SKILL.md mirrors the same steps so you can act even if the URL is unreachable.
28
28
 
29
+ ## Top-level rule (read this before anything else)
30
+
31
+ **Emit ONLY the user-visible lines listed below. Do not narrate tool calls. Do not describe what you're doing or about to do. Errors are the only allowed deviation, and only as a single tight line.** The expected user-facing transcript for a healthy install is **5–6 short lines TOTAL** (lines 1, 2, 5, 6 always; lines 3 and 4 only if the corresponding condition fires). Anything more is a SKILL violation. Telegram and similar transports edit the bot's messages live as your tool calls progress — every interim "Let me check…" / "Plugin loaded. Let me verify…" line shows up to the user as awkward edit churn, even if you "delete" it. The fix is to never emit it in the first place.
32
+
33
+ ## User-visible line set (THIS IS ALL YOU SAY)
34
+
35
+ Speak only these lines, in order. Substitute the bracketed placeholders with the real values. No prefaces. No suffixes. No narration.
36
+
37
+ 1. (start of install)
38
+ > Installing TotalReclaw…
39
+ 2. (after install + auto-reload + `totalreclaw_pair` is bound)
40
+ > ✓ Installed (totalreclaw &lt;version&gt;)
41
+ 3. (optional — only if auto-reload didn't fire and you need to issue `/restart`)
42
+ > Restarting OpenClaw to load the plugin…
43
+ 4. (optional — only if you successfully disabled the built-in memory tool to prevent dual-write)
44
+ > Disabled built-in memory tool.
45
+ 5. (account-setup URL + PIN, immediately after `totalreclaw_pair` returns)
46
+ > Open &lt;url&gt; in your browser. Enter PIN &lt;pin&gt;. Generate or paste a 12-word recovery phrase. Reply `done` once it's sealed.
47
+ 6. (done + free-tier confirmation)
48
+ > ✓ You're set up on the TotalReclaw free tier — 500 memories/month, unlimited reads. https://totalreclaw.xyz/pricing. Ask me to remember anything.
49
+
50
+ If existing credentials are detected (step "Check for existing credentials" below), emit ONLY this line and stop:
51
+ > ✓ TotalReclaw is already set up.
52
+
29
53
  ## Setup flow
30
54
 
31
55
  If `totalreclaw_*` tools are already bound to your toolset, SKIP install and jump to step 4. Otherwise run all steps in order.
32
56
 
33
- 1. **Install plugin first, skill second.** Run via your shell tool, in this exact order:
57
+ 1. **Install plugin first, skill second (silent — emit user-visible line 1 once at the start, then no prose until line 2).** Run via your shell tool, in this exact order:
34
58
  ```
35
59
  openclaw plugins install @totalreclaw/totalreclaw
36
60
  openclaw skills install totalreclaw
@@ -39,26 +63,39 @@ If `totalreclaw_*` tools are already bound to your toolset, SKIP install and jum
39
63
 
40
64
  If either shell call fails with `command not found` / ENOENT / "not authorized" (managed-service or shell-restricted environment), tell the user verbatim *"Your environment doesn't expose the OpenClaw CLI to me. Please install `totalreclaw` from your service's plugins / skills UI, then reply `done`."* Wait for `done` and skip to step 3.
41
65
 
42
- 2. **Wait for auto-reload — then issue `/restart` autonomously if needed.** OpenClaw's default config (`gateway.reload.mode = "hybrid"`) auto-restarts the gateway via a file-watcher within 1-3 seconds. Wait ~5s, then run `openclaw plugins list | grep totalreclaw` and confirm `totalreclaw_pair` is in your toolset.
66
+ 2. **Wait for auto-reload — then issue `/restart` autonomously if needed (silent — emit user-visible line 2 on success, line 3 only if you had to restart).** OpenClaw's default config (`gateway.reload.mode = "hybrid"`) auto-restarts the gateway via a file-watcher within 1-3 seconds. Wait ~5s, then run `openclaw plugins list | grep totalreclaw` and confirm `totalreclaw_pair` is in your toolset. When bound, emit user-visible line 2 and proceed.
67
+
68
+ **If `totalreclaw_pair` is NOT visible after 10s — restart imperative (read carefully):**
69
+
70
+ > Your IMMEDIATE next message must be the literal slash command `/restart` — nothing else. Do not narrate. Do not ask permission. Do not describe what `/restart` does. Do not propose alternatives. Just send `/restart`. You may emit user-visible line 3 (`Restarting OpenClaw to load the plugin…`) ONCE in the same turn, then issue `/restart`. Nothing else.
71
+
72
+ `/restart` is in-process — it drains active runs cleanly; your shell is NOT shelling out to `docker restart` or `openclaw gateway restart` (those would suicide the host). After `/restart`, wait ~3s and re-run `openclaw plugins list`.
43
73
 
44
- **If `totalreclaw_pair` is NOT visible after 10s:** issue OpenClaw's built-in `/restart` slash-command immediately and silently. **DO NOT ask the user "should I restart" / "do you want me to restart" / "let me check if the tool is bound" just issue `/restart` and announce "Reloading gateway..." once.** This is in-process and drains active runs cleanly; your shell is NOT shelling out to `docker restart` or `openclaw gateway restart` (which would suicide the host). After `/restart`, wait ~3s and re-run `openclaw plugins list`.
74
+ **If `/restart` returns "You are not authorized to use this command":** surface this verbatim and stop. Do NOT loop `/restart` calls. Do NOT propose alternatives.
45
75
 
46
- **If `/restart` returns "You are not authorized to use this command":** the gateway's `channels.<your-channel>.allowFrom` allowlist is empty. **Do NOT keep retrying.** Surface this one-line user-facing fix and stop:
76
+ > Your Telegram channel needs `allowFrom` configured. Run on your host: `jq '.channels.<channel>.allowFrom = ["<your-id>"]' ~/.openclaw/openclaw.json | sponge ~/.openclaw/openclaw.json && docker restart <your-container-name>`. Substitute `<channel>` (e.g. `telegram`, `slack`), `<your-id>` (Telegram: `@username` or numeric `chat_id`; Slack: workspace member ID), and `<your-container-name>` (`docker ps` shows it). For non-Docker installs substitute `openclaw gateway restart` for the `docker restart` portion. On a managed service, ask the service's support to enable slash commands. Reply `done` after.
47
77
 
48
- > Your channel needs to be allowlisted for slash commands. Run on the gateway host: `jq '.channels.<channel>.allowFrom = ["<your-id>"]' ~/.openclaw/openclaw.json | sponge ~/.openclaw/openclaw.json && docker restart <your-container-name>` (Docker self-host) substitute `<channel>` with your channel name (e.g. `telegram`, `slack`), `<your-id>` with your user ID for that channel (Telegram: `@username` or numeric `chat_id`; Slack: workspace member ID), and `<your-container-name>` with your gateway container name (`docker ps` shows it). On a managed service, ask the service's support to enable slash commands for your channel. Reply `done` once it's back up.
78
+ Then wait for `done`. Do NOT retry the install. Do NOT keep restarting. As a last-resort session-level fallback while restart is gated, try `/new` once (fresh session within the same gateway process, may pick up freshly-bound tools without a full restart).
49
79
 
50
- For non-Docker / native deployments substitute `openclaw gateway restart` for the `docker restart` portion. As a session-level fallback while restart is gated, try `/new` (starts a fresh session within the same gateway process, may pick up freshly-bound tools without a full restart).
80
+ **Forbidden vocabulary** (these are anti-patterns they show up to the user even if your transport edits them later, and they violate the silence rule):
81
+ - "Let me check that…"
82
+ - "Plugin loaded. Let me verify…"
83
+ - "I'll now…"
84
+ - "Quick fix…"
85
+ - "However, I don't see…"
86
+ - "I need to use…"
87
+ - "Should I /restart" / "Do you have a public URL" / "Want me to restart" / "Let me check if the tool is bound" / "I need permission to restart"
88
+ - "Now installing…" / "Got it" / "In parallel…" / "Step 1a/1b/1c"
51
89
 
52
- **Forbidden vocabulary** (these are anti-patterns when a tool is missing post-install): "Should I /restart", "Do you have a public URL", "Want me to restart", "Let me check if the tool is bound", "I need permission to restart". Just issue `/restart` immediately and announce.
90
+ Substitute all of these with silence the next user-visible line in the set, or nothing.
53
91
 
54
92
  3. **Verify `totalreclaw_pair` is bound.** If still missing after `/restart` (or after the user fixed allowFrom and said `done`), tell them the restart didn't take effect and ask them to retry. Do not loop the install.
55
93
 
56
- 4. **Check for existing credentials.** Run `test -s ~/.totalreclaw/credentials.json`. If it exists and is non-empty, confirm *"TotalReclaw is already set up."* and stop.
94
+ 4. **Check for existing credentials (silent unless found).** Run `test -s ~/.totalreclaw/credentials.json`. If it exists and is non-empty, emit *"TotalReclaw is already set up."* and stop.
57
95
 
58
- 5. **Set up the user's TotalReclaw account.** Tell the user: *"I'll walk you through setting up your TotalReclaw account."* Then call `totalreclaw_pair` (the account-setup tool — name kept for backward compatibility). Returns `{url, pin, qr_ascii, qr_png_b64, qr_unicode, expires_at_ms}` (see "Rendering the QR" below). Relay verbatim:
59
- > *Open <url> in your browser. Generate a new 12-word recovery phrase there or paste an existing one — the phrase stays in your browser, the relay only sees ciphertext. Confirm PIN <pin>. Reply `done` once the page says it's sealed.*
96
+ 5. **Set up the user's TotalReclaw account (emit user-visible line 5 once, after `totalreclaw_pair` returns).** Call `totalreclaw_pair` silently (the account-setup tool — name kept for backward compatibility). It returns `{url, pin, qr_ascii, qr_png_b64, qr_unicode, expires_at_ms}` (see "Rendering the QR" below). Then emit user-visible line 5 with `<url>` and `<pin>` substituted. Do not pre-narrate ("I'll walk you through…", "Setting up your account now…") — line 5 itself is the only thing the user needs to see. The URL + PIN itself is the real consent moment: surfacing them verbatim is what gives the user the chance to opt out by simply not opening the page.
60
97
 
61
- 6. **Verify and confirm.** After user says `done`, re-run `test -s ~/.totalreclaw/credentials.json`. If missing, the PIN expired — call `totalreclaw_pair` again and resend. If present, confirm *"Your TotalReclaw account is set up. Ask me to remember or recall anything."*
98
+ 6. **Verify and confirm (silent until line 6).** After the user says `done`, re-run `test -s ~/.totalreclaw/credentials.json`. If missing, the PIN expired — call `totalreclaw_pair` again silently and resend line 5 once. If present, emit user-visible line 6 and stop.
62
99
 
63
100
  ## Rendering the QR on your transport (rc.5+)
64
101
 
package/dist/index.js CHANGED
@@ -4686,9 +4686,23 @@ const plugin = {
4686
4686
  // Background task — writes credentials.json + flips state when
4687
4687
  // the browser completes the flow. Tool handler returns
4688
4688
  // immediately so the agent can tell the user the URL + PIN.
4689
+ //
4690
+ // 3.3.4-rc.2 (Pedro QA — pair flow stuck-session) — wrap the
4691
+ // WS-await in a 60s hard timeout. The relay drops sessions on
4692
+ // `ws_close` (separately patched relay-side); without this
4693
+ // bound the background task hangs in `waitNextMessage` for the
4694
+ // full 5-minute session TTL before resolving, and downstream
4695
+ // tooling that polls the gateway for completion sees stale
4696
+ // `processing` state at 129s/159s/189s. 60s is the user-side
4697
+ // deadline we surface in chat: long enough for a slow scan-
4698
+ // and-paste, short enough that a fresh URL request is the
4699
+ // obvious next step. Structured `timed_out` error in the log
4700
+ // lets ops grep for the failure mode independently of generic
4701
+ // ws-close errors.
4702
+ const PAIR_TOOL_HARD_TIMEOUT_MS = 60_000;
4689
4703
  void (async () => {
4690
4704
  try {
4691
- await awaitPhraseUpload(remoteSession, {
4705
+ const phraseUploadPromise = awaitPhraseUpload(remoteSession, {
4692
4706
  phraseValidator: (p) => validateMnemonic(p, wordlist),
4693
4707
  completePairing: async ({ mnemonic }) => {
4694
4708
  try {
@@ -4728,7 +4742,37 @@ const plugin = {
4728
4742
  return { state: 'error', error: msg };
4729
4743
  }
4730
4744
  },
4745
+ // 3.3.4-rc.2 — also pass through to awaitPhraseUpload so its
4746
+ // internal `waitNextMessage` timer matches the outer race.
4747
+ timeoutMs: PAIR_TOOL_HARD_TIMEOUT_MS,
4748
+ });
4749
+ // 3.3.4-rc.2 — outer Promise.race guard. Resolves to a
4750
+ // sentinel ({ status: 'timed_out', ... }) so the catch
4751
+ // handler can distinguish a hard-timeout from a generic
4752
+ // ws-close error and surface it explicitly.
4753
+ const TIMEOUT_SENTINEL = {
4754
+ status: 'timed_out',
4755
+ message: `Pair flow timed out (${PAIR_TOOL_HARD_TIMEOUT_MS / 1000}s) — generate a new URL with totalreclaw_pair.`,
4756
+ };
4757
+ let hardTimer;
4758
+ const hardTimeoutPromise = new Promise((resolve) => {
4759
+ hardTimer = setTimeout(() => resolve(TIMEOUT_SENTINEL), PAIR_TOOL_HARD_TIMEOUT_MS);
4731
4760
  });
4761
+ try {
4762
+ const raced = await Promise.race([
4763
+ phraseUploadPromise,
4764
+ hardTimeoutPromise,
4765
+ ]);
4766
+ if (raced &&
4767
+ typeof raced === 'object' &&
4768
+ raced.status === 'timed_out') {
4769
+ api.logger.warn(`totalreclaw_pair(relay): hard timeout — ${raced.message} (token=${remoteSession.token.slice(0, 8)}…)`);
4770
+ }
4771
+ }
4772
+ finally {
4773
+ if (hardTimer)
4774
+ clearTimeout(hardTimer);
4775
+ }
4732
4776
  }
4733
4777
  catch (bgErr) {
4734
4778
  // Expected on TTL expiry / user-aborts — log at warn, not error.
@@ -44,7 +44,7 @@
44
44
  * `CONFIG`. See `index.ts` wire-up.
45
45
  */
46
46
  import { validateMnemonic } from '@scure/bip39';
47
- import { wordlist } from '@scure/bip39/wordlists/english';
47
+ import { wordlist } from '@scure/bip39/wordlists/english.js';
48
48
  import { loadCredentialsJson, writeCredentialsJson, writeOnboardingState, } from './fs-helpers.js';
49
49
  import { awaitPhraseUpload, openRemotePairSession, } from './pair-remote-client.js';
50
50
  import { setRecoveryPhraseOverride } from './config.js';
package/index.ts CHANGED
@@ -5496,9 +5496,23 @@ const plugin = {
5496
5496
  // Background task — writes credentials.json + flips state when
5497
5497
  // the browser completes the flow. Tool handler returns
5498
5498
  // immediately so the agent can tell the user the URL + PIN.
5499
+ //
5500
+ // 3.3.4-rc.2 (Pedro QA — pair flow stuck-session) — wrap the
5501
+ // WS-await in a 60s hard timeout. The relay drops sessions on
5502
+ // `ws_close` (separately patched relay-side); without this
5503
+ // bound the background task hangs in `waitNextMessage` for the
5504
+ // full 5-minute session TTL before resolving, and downstream
5505
+ // tooling that polls the gateway for completion sees stale
5506
+ // `processing` state at 129s/159s/189s. 60s is the user-side
5507
+ // deadline we surface in chat: long enough for a slow scan-
5508
+ // and-paste, short enough that a fresh URL request is the
5509
+ // obvious next step. Structured `timed_out` error in the log
5510
+ // lets ops grep for the failure mode independently of generic
5511
+ // ws-close errors.
5512
+ const PAIR_TOOL_HARD_TIMEOUT_MS = 60_000;
5499
5513
  void (async () => {
5500
5514
  try {
5501
- await awaitPhraseUpload(remoteSession, {
5515
+ const phraseUploadPromise = awaitPhraseUpload(remoteSession, {
5502
5516
  phraseValidator: (p: string) =>
5503
5517
  validateMnemonic(p, wordlist),
5504
5518
  completePairing: async ({ mnemonic }) => {
@@ -5546,7 +5560,48 @@ const plugin = {
5546
5560
  return { state: 'error', error: msg };
5547
5561
  }
5548
5562
  },
5563
+ // 3.3.4-rc.2 — also pass through to awaitPhraseUpload so its
5564
+ // internal `waitNextMessage` timer matches the outer race.
5565
+ timeoutMs: PAIR_TOOL_HARD_TIMEOUT_MS,
5549
5566
  });
5567
+ // 3.3.4-rc.2 — outer Promise.race guard. Resolves to a
5568
+ // sentinel ({ status: 'timed_out', ... }) so the catch
5569
+ // handler can distinguish a hard-timeout from a generic
5570
+ // ws-close error and surface it explicitly.
5571
+ const TIMEOUT_SENTINEL: {
5572
+ status: 'timed_out';
5573
+ message: string;
5574
+ } = {
5575
+ status: 'timed_out',
5576
+ message:
5577
+ `Pair flow timed out (${PAIR_TOOL_HARD_TIMEOUT_MS / 1000}s) — generate a new URL with totalreclaw_pair.`,
5578
+ };
5579
+ let hardTimer: ReturnType<typeof setTimeout> | undefined;
5580
+ const hardTimeoutPromise = new Promise<typeof TIMEOUT_SENTINEL>(
5581
+ (resolve) => {
5582
+ hardTimer = setTimeout(
5583
+ () => resolve(TIMEOUT_SENTINEL),
5584
+ PAIR_TOOL_HARD_TIMEOUT_MS,
5585
+ );
5586
+ },
5587
+ );
5588
+ try {
5589
+ const raced = await Promise.race([
5590
+ phraseUploadPromise,
5591
+ hardTimeoutPromise,
5592
+ ]);
5593
+ if (
5594
+ raced &&
5595
+ typeof raced === 'object' &&
5596
+ (raced as { status?: unknown }).status === 'timed_out'
5597
+ ) {
5598
+ api.logger.warn(
5599
+ `totalreclaw_pair(relay): hard timeout — ${(raced as typeof TIMEOUT_SENTINEL).message} (token=${remoteSession.token.slice(0, 8)}…)`,
5600
+ );
5601
+ }
5602
+ } finally {
5603
+ if (hardTimer) clearTimeout(hardTimer);
5604
+ }
5550
5605
  } catch (bgErr: unknown) {
5551
5606
  // Expected on TTL expiry / user-aborts — log at warn, not error.
5552
5607
  const bgMsg = bgErr instanceof Error ? bgErr.message : String(bgErr);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@totalreclaw/totalreclaw",
3
- "version": "3.3.4-rc.1",
3
+ "version": "3.3.5-rc.1",
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": [
@@ -31,6 +31,7 @@
31
31
  "author": "TotalReclaw Team",
32
32
  "license": "MIT",
33
33
  "dependencies": {
34
+ "@scure/bip39": "^2.2.0",
34
35
  "@totalreclaw/client": "^1.2.0",
35
36
  "@totalreclaw/core": "^2.1.1",
36
37
  "@types/qrcode": "^1.5.6",
package/pair-cli-relay.ts CHANGED
@@ -45,7 +45,7 @@
45
45
  */
46
46
 
47
47
  import { validateMnemonic } from '@scure/bip39';
48
- import { wordlist } from '@scure/bip39/wordlists/english';
48
+ import { wordlist } from '@scure/bip39/wordlists/english.js';
49
49
 
50
50
  import {
51
51
  loadCredentialsJson,
package/skill.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "totalreclaw",
3
- "version": "3.3.4-rc.1",
3
+ "version": "3.3.5-rc.1",
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",