@totalreclaw/totalreclaw 3.3.1-rc.8 → 3.3.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.
Files changed (81) hide show
  1. package/CHANGELOG.md +268 -1
  2. package/SKILL.md +29 -23
  3. package/api-client.ts +18 -11
  4. package/claims-helper.ts +47 -1
  5. package/config.ts +108 -4
  6. package/confirm-indexed.ts +191 -0
  7. package/crypto.ts +10 -2
  8. package/dist/api-client.js +226 -0
  9. package/dist/billing-cache.js +100 -0
  10. package/dist/claims-helper.js +624 -0
  11. package/dist/config.js +297 -0
  12. package/dist/confirm-indexed.js +127 -0
  13. package/dist/consolidation.js +258 -0
  14. package/dist/contradiction-sync.js +1034 -0
  15. package/dist/crypto.js +138 -0
  16. package/dist/digest-sync.js +361 -0
  17. package/dist/download-ux.js +63 -0
  18. package/dist/embedder-cache.js +185 -0
  19. package/dist/embedder-loader.js +121 -0
  20. package/dist/embedder-network.js +301 -0
  21. package/dist/embedding.js +141 -0
  22. package/dist/extractor.js +1225 -0
  23. package/dist/first-run.js +103 -0
  24. package/dist/fs-helpers.js +725 -0
  25. package/dist/gateway-url.js +197 -0
  26. package/dist/generate-mnemonic.js +13 -0
  27. package/dist/hot-cache-wrapper.js +101 -0
  28. package/dist/import-adapters/base-adapter.js +64 -0
  29. package/dist/import-adapters/chatgpt-adapter.js +238 -0
  30. package/dist/import-adapters/claude-adapter.js +114 -0
  31. package/dist/import-adapters/gemini-adapter.js +201 -0
  32. package/dist/import-adapters/index.js +26 -0
  33. package/dist/import-adapters/mcp-memory-adapter.js +219 -0
  34. package/dist/import-adapters/mem0-adapter.js +158 -0
  35. package/dist/import-adapters/types.js +1 -0
  36. package/dist/index.js +5388 -0
  37. package/dist/llm-client.js +687 -0
  38. package/dist/llm-profile-reader.js +346 -0
  39. package/dist/lsh.js +62 -0
  40. package/dist/onboarding-cli.js +750 -0
  41. package/dist/pair-cli.js +344 -0
  42. package/dist/pair-crypto.js +359 -0
  43. package/dist/pair-http.js +404 -0
  44. package/dist/pair-page.js +826 -0
  45. package/dist/pair-qr.js +107 -0
  46. package/dist/pair-remote-client.js +410 -0
  47. package/dist/pair-session-store.js +566 -0
  48. package/dist/pin.js +556 -0
  49. package/dist/qa-bug-report.js +301 -0
  50. package/dist/relay-headers.js +44 -0
  51. package/dist/reranker.js +409 -0
  52. package/dist/retype-setscope.js +368 -0
  53. package/dist/semantic-dedup.js +75 -0
  54. package/dist/subgraph-search.js +289 -0
  55. package/dist/subgraph-store.js +694 -0
  56. package/dist/tool-gating.js +58 -0
  57. package/download-ux.ts +91 -0
  58. package/embedder-cache.ts +230 -0
  59. package/embedder-loader.ts +189 -0
  60. package/embedder-network.ts +350 -0
  61. package/embedding.ts +118 -27
  62. package/fs-helpers.ts +277 -0
  63. package/gateway-url.ts +57 -9
  64. package/index.ts +469 -250
  65. package/llm-client.ts +4 -3
  66. package/lsh.ts +7 -2
  67. package/onboarding-cli.ts +114 -1
  68. package/package.json +24 -5
  69. package/pair-cli.ts +76 -8
  70. package/pair-crypto.ts +34 -24
  71. package/pair-page.ts +28 -17
  72. package/pair-qr.ts +152 -0
  73. package/pair-remote-client.ts +540 -0
  74. package/pin.ts +31 -0
  75. package/qa-bug-report.ts +84 -2
  76. package/relay-headers.ts +50 -0
  77. package/reranker.ts +40 -0
  78. package/retype-setscope.ts +69 -8
  79. package/skill.json +1 -1
  80. package/subgraph-search.ts +4 -3
  81. package/subgraph-store.ts +15 -10
package/CHANGELOG.md CHANGED
@@ -4,6 +4,272 @@ 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
+ ## [Unreleased]
8
+
9
+ ### Install / runtime hygiene (issues #126, #128)
10
+
11
+ Two narrow fixes from the rc.20 user-QA findings — both around install /
12
+ boot-time output cleanliness, no behavior change to the steady-state plugin.
13
+
14
+ - **#126 — clean up `.openclaw-install-stage-*` siblings.** When
15
+ `openclaw plugins install @totalreclaw/totalreclaw` is interrupted mid-
16
+ extract (e.g. by an auto-gateway-restart triggered by the same install),
17
+ the npm staging directory `<extensionsDir>/.openclaw-install-stage-XXXXXX/`
18
+ survives. On the next gateway start, OpenClaw's plugin loader auto-
19
+ discovers BOTH `.../totalreclaw/` AND the orphan staging dir, registers
20
+ duplicate plugins, fires hooks twice, and prints a "duplicate-plugin-id"
21
+ warning every cycle. A user running `openclaw plugins list` sees two
22
+ `totalreclaw` rows.
23
+
24
+ Fix: `cleanupInstallStagingDirs(pluginDir)` runs at plugin register time
25
+ (one tick after the loader resolves our entrypoint). It scans the
26
+ extensions directory for `.openclaw-install-stage-*` siblings and
27
+ recursively removes each one. Best-effort — never crashes plugin init
28
+ on permission / race failures.
29
+
30
+ Regression: `install-staging-cleanup.test.ts` (16 assertions) covers
31
+ fresh install, idempotent re-run, package-root vs `dist/` invocation,
32
+ unrelated-dotfile preservation (`.git`, `.openclaw-cache`), and stray-
33
+ file (non-directory) skipping.
34
+
35
+ - **#128 — registerTool breadcrumbs no longer bleed into `--json` stdout.**
36
+ The rc.20 breadcrumb logs (`registerTool(totalreclaw_pair) returned. ...`
37
+ and the RC-only `totalreclaw_report_qa_bug registered ...`) were emitted
38
+ via `api.logger.info`, which OpenClaw routes to stdout decorated with
39
+ `[plugins] `. When a user invoked `openclaw agent --message "..." --json`
40
+ for programmatic parsing, the breadcrumb appeared on stdout alongside
41
+ the JSON-RPC body, breaking any naive `JSON.parse(stdout)`.
42
+
43
+ Fix: gate both breadcrumbs behind `CONFIG.verboseRegister`, OFF by
44
+ default. Ops can opt back in with `TOTALRECLAW_VERBOSE_REGISTER=1` (or
45
+ the general `TOTALRECLAW_DEBUG=1` toggle) when chasing a tool-injection
46
+ regression. Default-off keeps `openclaw agent --json` stdout clean.
47
+
48
+ Regression: `json-stdout-cleanliness.test.ts` (11 assertions) confirms
49
+ both breadcrumbs are wrapped in `if (CONFIG.verboseRegister)` blocks,
50
+ simulates the gated `--json` stdout path and `JSON.parse`s the result,
51
+ and exercises the env-var resolution (`TOTALRECLAW_VERBOSE_REGISTER`
52
+ -> `TOTALRECLAW_DEBUG` -> default false).
53
+
54
+ ## [3.3.1-rc.16] — 2026-04-24
55
+
56
+ Fixes #92 — slow-host install times out during ONNX-runtime / embedding-model
57
+ download. ONNX stays mandatory (no opt-in flag); first-call download is now
58
+ wrapped with timeout, progress, and retry UX so slow connections succeed
59
+ instead of silently hanging until OpenClaw SIGTERMs.
60
+
61
+ ### Embedding-model download UX
62
+
63
+ - New `download-ux.ts` module — pure stdlib, no third-party imports — exposes
64
+ `downloadWithUX(label, fn, opts)`. Wraps a download promise with:
65
+ - **Per-attempt timeout**, default 600s (covers ~290 KB/s for the 344 MB
66
+ Harrier model). Configurable via env `TOTALRECLAW_ONNX_INSTALL_TIMEOUT`
67
+ (in seconds). Per-attempt timeout grows 1x/2x/4x across retries.
68
+ - **60s keep-alive log** during long downloads so users on slow networks
69
+ see "still downloading… (Ns elapsed)" rather than a frozen prompt.
70
+ - **3-attempt exponential-backoff retry** (5s/10s backoff between attempts)
71
+ to absorb transient network blips.
72
+ - **Loud actionable error** on exhaustion: names the env var to extend the
73
+ timeout and the exact `openclaw plugins install totalreclaw` command to
74
+ rerun.
75
+ - `embedding.ts` now wraps `AutoTokenizer.from_pretrained`,
76
+ `AutoModel.from_pretrained`, and the `pipeline()` call with
77
+ `downloadWithUX`. Prints a user-visible "Downloading embedding model
78
+ (~344MB) — this may take a few minutes on slower connections. Please wait."
79
+ message before the first download starts.
80
+ - ONNX remains a mandatory hard `dependency` (no `[embedding]`-style opt-in
81
+ extra). Recall accuracy is unchanged.
82
+ - Regression: `test_issue_92_onnx_download_ux.test.ts` exercises happy path,
83
+ transient failure → retry, full exhaustion, per-attempt timeout, and
84
+ keep-alive cadence. Wired into the plugin `npm test` chain.
85
+
86
+ ## [3.3.1-rc.14] — 2026-04-24
87
+
88
+ Coordinated version bump with Python `2.3.1rc14`. Two narrow bug fixes
89
+ found during rc.13 user QA on 2026-04-24:
90
+
91
+ ### RC-gated QA bug tool — target-repo hardening
92
+
93
+ `totalreclaw_report_qa_bug` now refuses to file to any repo that isn't
94
+ internal. rc.13 user QA surfaced agent-filed bug reports leaking to the
95
+ public `p-diogo/totalreclaw` tracker despite the tool's default target
96
+ being `p-diogo/totalreclaw-internal`.
97
+
98
+ - New env var: `TOTALRECLAW_QA_REPO` lets operators point the tool at a
99
+ private fork. The default stays `p-diogo/totalreclaw-internal`.
100
+ - New `resolveQaRepo(...)` guard: rejects any slug that is on the
101
+ public-repo denylist (includes `p-diogo/totalreclaw`,
102
+ `...-website`, `...-relay`, `...-plugin`, `...-hermes`) OR does not
103
+ end in `-internal`. The check runs before the HTTP POST is
104
+ constructed, so rejection never leaves the client.
105
+ - `CONFIG.qaRepoOverride` surfaces the env var through `config.ts`
106
+ (keeps scanner-sensitive `process.env` reads centralized).
107
+ - Regression test in `qa-bug-report.test.ts` mocks the public slug
108
+ and asserts `fetch` is NEVER called.
109
+
110
+ Labels on filing unchanged — still emits `qa-bug`, `pending-triage`,
111
+ `severity:<...>`, `component:<...>`, `rc:<...>`.
112
+
113
+ ### Relay pair page — PIN paste button UX
114
+
115
+ The paste button on the step-1 PIN screen was silently failing under
116
+ certain browser states. rc.14 rewrites the handler with a proper
117
+ error taxonomy:
118
+
119
+ - Capability probe up front — `navigator.clipboard.readText` missing →
120
+ clear "Paste unavailable on this browser" toast.
121
+ - `NotAllowedError` → "Clipboard access denied — type the 6 digits
122
+ manually" (covers iOS Safari permission denial).
123
+ - Empty clipboard → "Clipboard is empty — copy the PIN from your chat
124
+ first".
125
+ - Non-digit content → "Clipboard has no digits — copy the 6-digit PIN
126
+ first".
127
+ - Every failure path focuses the first PIN cell so the user can fall
128
+ through to manual typing without another click.
129
+ - Errors log to `console.warn` with name + message so future failures
130
+ are diagnosable from browser devtools.
131
+
132
+ The mockup at `docs/mockups/rc13-pair-wizard/wizard.js` gets the same
133
+ rewrite for parity — the relay's `scripts/sync-pair-preview.mjs`
134
+ regenerates `/pair-preview/` from this source.
135
+
136
+ Fix also applies to the "Paste all 12 words" import-grid button on the
137
+ relay production page (same taxonomy, same focus-fallback).
138
+
139
+ ## [3.3.1-rc.13] — 2026-04-24
140
+
141
+ Coordinated version bump with Python `2.3.1rc13`. No substantive
142
+ changes to the plugin's own TypeScript — the rc.13 fix lands on the
143
+ Hermes-side (`python/src/totalreclaw/hermes/pair_tool.py`) where the
144
+ asyncio lifecycle regression lived. We keep plugin + Python RC
145
+ numbers in lockstep so the release-pipeline tracker and
146
+ `qa-totalreclaw` skill carry both artifacts through QA as one
147
+ bundle.
148
+
149
+ See the corresponding entry in `python/CHANGELOG.md` for the full
150
+ design: the relay-pair WebSocket is now owned by a dedicated worker
151
+ thread (with its own event loop) so it survives the Hermes
152
+ tool-invocation loop teardown that destroyed the rc.10–rc.12 waiter
153
+ mid-recv and caused every pair attempt to 502.
154
+
155
+ The relay-served production pair page is also replaced with the
156
+ rc.13 wizard UX — a typeform-style 3-step flow (PIN → phrase → done)
157
+ mirroring the `docs/mockups/rc13-pair-wizard/` design. This lands in
158
+ the `totalreclaw-relay` repo PR, not here, but surfaces to every
159
+ OpenClaw user via the default relay pair flow.
160
+
161
+ ### Plugin local-mode pair page
162
+
163
+ `skill/plugin/pair-page.ts` (the local-mode fallback served when a
164
+ user sets `TOTALRECLAW_PAIR_MODE=local`) retains its rc.10–rc.12 UX
165
+ shape. The wizard UX port for this file is deferred to rc.14 pending
166
+ a design decision on whether to share a single CSS+JS asset across
167
+ all three pair pages (relay / Python local / plugin local) or keep
168
+ them independently inlined. Local-mode is rarely exercised — the
169
+ plugin defaults to the relay flow via the Hermes Python sidecar and
170
+ only falls back here for air-gapped setups.
171
+
172
+ ## [3.3.1-rc.12] — 2026-04-23
173
+
174
+ **Ship-stopper fix for rc.11.** The relay-served pair page's submit
175
+ button threw `NotSupportedError: Failed to execute 'importKey' on
176
+ 'SubtleCrypto': Algorithm: Unrecognized name` when the user clicked
177
+ "Seal key and finish". Root cause: `ChaCha20-Poly1305` is NOT
178
+ implemented in the Web Crypto API of Chrome / Safari / Edge — the
179
+ spec exposes `AES-GCM` as the only AEAD. rc.10/rc.11 never worked
180
+ end-to-end for any user; every pair attempt failed silently and the
181
+ token expired without logging a failure — GH issue #79.
182
+
183
+ rc.12 swaps the cipher suite from ChaCha20-Poly1305 to AES-256-GCM on
184
+ both sides (browser + gateway). Wire shape unchanged — still 12-byte
185
+ nonce, 16-byte tag, sid-bound AAD, base64url encoding. HKDF info bumped
186
+ from `totalreclaw-pair-v1` to `totalreclaw-pair-v2` so rc.11 ciphertexts
187
+ cannot collide with rc.12 keys (fail-closed on any version skew).
188
+
189
+ ### Changed
190
+ - `skill/plugin/pair-crypto.ts`: `aeadDecrypt` / `aeadEncryptWithSessionKey`
191
+ switched from `chacha20-poly1305` to `aes-256-gcm`. `HKDF_INFO` bumped
192
+ to `totalreclaw-pair-v2`.
193
+ - `skill/plugin/pair-page.ts` (local-mode pair page): WebCrypto
194
+ `ChaCha20-Poly1305` calls swapped to `AES-GCM`. Capability probe
195
+ function renamed `chaChaSupported` → `aesGcmSupported`.
196
+
197
+ ### Observability
198
+ - The relay's `pair-html.ts` (user-facing page) now reports phase-labelled
199
+ error messages so a network / encrypt / submit failure no longer masks
200
+ as a silent "stuck on acknowledge screen". Relay PR (fix/pair-aes-gcm-rc12)
201
+ is the canonical fix for the issue reported in #79.
202
+
203
+ ## [3.3.1-rc.11] — 2026-04-23
204
+
205
+ OpenClaw-side universal pair reachability — the plugin's `totalreclaw_pair` tool now routes through the relay WebSocket by default, mirroring the Python `2.3.1rc10` pivot on the Hermes side. The URL returned to the user is `https://api-staging.totalreclaw.xyz/pair/p/<token>#pk=<gateway_pubkey>` instead of the previous `http://<gateway-host>:<port>/plugin/totalreclaw/pair/finish?sid=<sid>#pk=…`. Managed hosts, Docker-in-cloud setups, phone-scan-QR flows, and split-network operators can now complete pairing without the browser needing loopback or LAN access to the gateway.
206
+
207
+ Paired with Hermes Python `2.3.1rc11` — both clients now reach for the relay by default, and `TOTALRECLAW_PAIR_MODE=local` on either side restores the rc.4–rc.10 loopback flow for air-gapped / self-hosted deployments.
208
+
209
+ ### Added
210
+
211
+ - **`skill/plugin/pair-remote-client.ts`** — new. TypeScript mirror of `python/src/totalreclaw/pair/remote_client.py` (rc.10 Hermes):
212
+ - `openRemotePairSession({ relayBaseUrl?, pin?, clientId?, mode? })` — generates an ephemeral x25519 keypair via the existing `pair-crypto.ts` module, opens a WebSocket to `/pair/session/open`, sends `{type:"open", gateway_pubkey, pin, client_id, mode}`, and returns a `RemotePairSession` handle containing the user-facing URL (with `#pk=` fragment), PIN, token, expiry, and the live WebSocket.
213
+ - `awaitPhraseUpload(session, { completePairing, phraseValidator?, timeoutMs? })` — blocks on the kept-open WebSocket until the relay pushes `{type:"forward", client_pubkey, nonce, ciphertext}`. Decrypts locally via `decryptPairingPayload` using the gateway's private key (same ECDH + HKDF + ChaCha20-Poly1305 primitives as rc.10's loopback flow — byte-compatible with Python's `pair.crypto`). Runs the caller-supplied `completePairing` handler and sends `{type:"ack"}` back on success or `{type:"nack", error}` on validator / decrypt / completion failure.
214
+ - `pairViaRelay(...)` — one-shot convenience wrapper for tests and simple callers.
215
+ - **`ws` runtime dep** (`^8.18.3`) + **`@types/ws`** — pure-JS WebSocket client. Transitive already via `@totalreclaw/core`; rc.11 promotes it to a direct dep so the plugin's own import graph is explicit.
216
+ - **`TOTALRECLAW_PAIR_MODE`** env (plugin side) — mirrors the Python env. Unset or any non-`local` value routes through the relay; `local` preserves the rc.4–rc.10 loopback HTTP server served by `pair-http.ts` (`/plugin/totalreclaw/pair/{finish,start,respond,status}`).
217
+ - **`TOTALRECLAW_PAIR_RELAY_URL`** env (plugin side) — self-hosters can point at their own relay. Defaults to `wss://api-staging.totalreclaw.xyz`.
218
+ - **`skill/plugin/pair-remote-client.test.ts`** — 20 assertions across 5 scenarios: happy-path round-trip, invalid-phrase nack, relay open error, decrypt failure, https-to-wss scheme conversion. Runs against a local `ws` server stub — no network dependency.
219
+
220
+ ### Changed
221
+
222
+ - **`totalreclaw_pair` tool** now branches on `CONFIG.pairMode`. In relay mode it returns the URL + PIN immediately and schedules a background task that blocks on the WebSocket until the browser completes (or the TTL lapses). Credentials-write happens in that background task via the same `loadCredentialsJson` / `writeCredentialsJson` / `setRecoveryPhraseOverride` / `writeOnboardingState` side-effect chain that the loopback `pair-http.respond` handler uses — so the onboarding-state flip remains identical. Tool payload shape unchanged (`{url, pin, expires_at_ms, qr_ascii, qr_png_b64, qr_unicode, mode}`) except for a new `transport: 'relay' | 'local'` field that tooling (QA harness, telemetry) can use to confirm which path served a given URL.
223
+
224
+ ### Phrase-safety invariants (preserved)
225
+
226
+ - Relay is blind: the gateway's ephemeral x25519 private key never leaves the plugin host. The relay forwards opaque ciphertext; it cannot derive the symmetric key.
227
+ - PIN is out-of-band: the user reads the PIN from agent chat and types it into the browser. The relay stores the PIN in memory only; logs carry no PIN, no ciphertext, no pubkey, no phrase.
228
+ - Session state is in-memory on the relay with a 5-minute TTL. Redis deferred to Phase 2 per the design blueprint.
229
+ - Backwards-compat: `TOTALRECLAW_PAIR_MODE=local` preserves every bit of the rc.4–rc.10 flow — same loopback HTTP server, same session store, same browser page, same decrypt handler.
230
+
231
+ ### Mechanism / byte-compat
232
+
233
+ The crypto is a literal TypeScript binding against the same `pair-crypto.ts` module `pair-http.ts` already imports. No new cipher suite, no new wire format — only the transport (WebSocket to relay + relay-served HTML page) differs from the loopback path. A ciphertext produced by the relay-served `pair-html.ts` page decrypts under the same gateway private key using the same `decryptPairingPayload(...)` call path. This is deliberate: `pair-crypto.ts` is the byte-compat anchor shared with Python's `pair.crypto`, and rc.11 extends that anchor to the relay wire.
234
+
235
+ ## [3.3.1-rc.10] — 2026-04-23
236
+
237
+ Coordinated version bump with Hermes Python `2.3.1rc10`. rc.10 ships the relay-brokered pair flow — see `python/CHANGELOG.md` (the `2.3.1rc10` entry) for the full design. The `totalreclaw_pair` pair URL on the OpenClaw plugin side still uses the gateway-loopback HTTP server (the OpenClaw plugin runs in-process alongside a browser on the same host for most deployments, so the loopback URL actually reaches the user). The relay-brokered path is currently Hermes-side only — the OpenClaw plugin can pick it up in a later RC if the same universal-reachability problem starts biting OpenClaw users.
238
+
239
+ Bundled into rc.10: the previously-parked rc.5 QR display layer from PR #76 (`pair-qr.ts` + `pair-qr.test.ts`, tool-payload `qr_png_b64` + `qr_unicode` fields, `totalreclaw_setup` / `totalreclaw_onboarding_start` stub removal). All rebased onto main via the chore/rc.10-qr-rebase-pr76 branch.
240
+
241
+ ### Added (rebased from PR #76)
242
+
243
+ - **`skill/plugin/pair-qr.ts`** — new. QR encoder module wrapping `qrcode` (PNG) + `qrcode-terminal` (Unicode block). Same contract as the Python side (`totalreclaw.pair.qr`).
244
+ - **`totalreclaw_pair` tool payload** — the `details` block now carries `qr_png_b64` (base64 PNG for image transports) and `qr_unicode` (terminal block-char string) alongside the existing `qr_ascii`. URL + PIN unchanged.
245
+ - **SKILL.md "Rendering the QR on your transport" section** — per-transport agent rendering guidance (Telegram attachment, terminal inline, web chat `<img>` embed).
246
+ - **`qrcode` + `@types/qrcode`** runtime deps.
247
+
248
+ ### Removed (rc.5 phrase-safety carve-out closure, rebased)
249
+
250
+ - **`totalreclaw_setup` + `totalreclaw_onboarding_start`** agent tools — both were neutered pointer stubs in rc.4; rc.5 auto-QA flagged them as future-regression surface and their mere presence signalled to agents that "phrase handling happens here". Deleted outright in rc.5, preserved through rc.10. `skill/plugin/phrase-safety-registry.test.ts` now asserts neither name is registered.
251
+
252
+ Version bump reason: rc cadence keeps Python + plugin aligned so the release-pipeline tracker carries them through QA as one artifact set.
253
+
254
+ ## [3.3.1-rc.9] — 2026-04-23
255
+
256
+ Coordinated version bump with Hermes Python `2.3.1rc9`. Plugin code itself is unchanged from `3.3.1-rc.6` (the first-run banner fix lives entirely on the Python side — `totalreclaw.onboarding.maybe_emit_welcome`). The rc.9 bundle ships the Hermes-side banner suppression and keeps plugin + Python versions aligned so the release-pipeline tracker can carry them through QA as one artifact set.
257
+
258
+ ### Why a plugin bump when only Python changed
259
+
260
+ Our RC cadence publishes both registries from the same bundle. Out-of-sync version tags cause downstream confusion (the `qa-totalreclaw` skill and the release-pipeline tracker both key on a single RC-number per wave). Skipping the plugin bump would leave rc.9 documented on the Python side only; a later plugin bug would then have to skip to rc.10 to catch up. Much simpler to bump both in lockstep.
261
+
262
+ See `python/CHANGELOG.md` (the `2.3.1rc9` entry) for the underlying fix: suppress the first-run welcome banner emitted by `totalreclaw.onboarding.maybe_emit_welcome`. Two problems surfaced during the rc.8 Hermes auto-QA run:
263
+
264
+ 1. **Chat-breaker.** The banner dominated `hermes chat -q` stdout when credentials were absent, breaking the QA harness's `session_id` parsing on every fresh install.
265
+ 2. **Phrase-safety violation.** The banner told users to `Run: totalreclaw setup` — a CLI that emits the recovery phrase to stdout. In an agent-driven context, stdout is echoed back into LLM context, so the phrase would cross the LLM boundary in violation of `project_phrase_safety_rule.md`.
266
+
267
+ Agent-driven setup now routes through the `totalreclaw_pair` tool (browser-side crypto, phrase-safe) per SKILL.md. User-in-terminal setup still runs through `totalreclaw setup` / `openclaw totalreclaw onboard` OUTSIDE any agent context.
268
+
269
+ ### Skipped
270
+
271
+ - **`3.3.1-rc.7`** and **`3.3.1-rc.8`** — registry-only bumps from 2026-04-22 workflow dispatches; the git repo on `main` carried rc.6 code unchanged through both publishes.
272
+
7
273
  ## [3.3.1-rc.6] — 2026-04-22
8
274
 
9
275
  Coordinated version bump with Hermes Python `2.3.1rc6`. Plugin code itself is unchanged from `3.3.1-rc.4` (the OpenClaw plugin's `register()` path already wired every tool advertised in `skill.yaml`). The rc.6 bundle ships the Hermes-side tool-registration fix and keeps plugin + Python versions aligned so the release-pipeline tracker can carry them through QA as one artifact set.
@@ -227,7 +493,8 @@ See: `plans/2026-04-22-plugin-3.3.1-provider-agnostic-llm.md` (internal).
227
493
  - `--json` — emits a structured payload (requires `--non-interactive`).
228
494
  - `--mode <generate|restore>` — skip the menu prompt.
229
495
  - `--phrase <12-or-24>` — required for `--mode restore`; `-` reads stdin.
230
- - `--emit-phrase` — opt-in path that includes the plaintext phrase in the
496
+ - `--emit-phrase` — historic opt-in flag (do not invoke via agent shell:
497
+ forbidden by the phrase-safety rule); included plaintext phrase in the
231
498
  JSON payload. Default omits the phrase; the agent should direct the
232
499
  user to read `~/.totalreclaw/credentials.json` in their terminal.
233
500
 
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.1-rc.6
4
+ version: 3.3.1-rc.22
5
5
  author: TotalReclaw Team
6
6
  license: MIT
7
7
  homepage: https://totalreclaw.xyz
@@ -24,37 +24,43 @@ metadata:
24
24
 
25
25
  # TotalReclaw — OpenClaw
26
26
 
27
- ## Fast path — "install totalreclaw" (Approach A, preferred)
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
- ClawHub classifies TotalReclaw as a skill. `openclaw skills install totalreclaw` registers this SKILL.md into your agent context BEFORE the plugin is installed (so you may be reading this with no `totalreclaw_*` tools yet).
29
+ ## Setup flow
30
30
 
31
- If the user says "install totalreclaw" (or any variant) AND no `totalreclaw_*` tools are in your toolset:
31
+ If `totalreclaw_*` tools are already bound to your toolset, SKIP install and jump to step 4. Otherwise run all steps in order.
32
32
 
33
- 1. Run via your shell tool: `openclaw plugins install totalreclaw`. (For RC candidates: `openclaw plugins install @totalreclaw/totalreclaw@rc` — the `rc` dist-tag on npm is the latest candidate.)
34
- 2. Tell the user: *"Plugin installed. Restarting the gateway so the tools bind."* Then ask the user to restart the gateway: `openclaw gateway restart` (native) or `docker restart tr-openclaw` (Docker). The agent-facing shell CANNOT self-restart the gateway it's running inside of — this step has to be a user action. Wait for the user to confirm the restart is done.
35
- 3. After restart: verify `totalreclaw_pair` is now in your toolset. If not, tell the user the restart didn't take effect and to retry the restart command. Do not loop the install.
36
- 4. Check `~/.totalreclaw/credentials.json` (via `test -s ~/.totalreclaw/credentials.json`). If it exists, confirm *"TotalReclaw is already set up."* and stop.
37
- 5. Call `totalreclaw_pair`. Returns `{url, pin, qr_ascii, expires_at_ms}`.
38
- 6. Relay verbatim: *"Open <url> in your browser. Enter your 12-word recovery phrase (or let the browser generate one). Confirm PIN <pin>. I'll wait for you to say done."*
39
- 7. After user says done: verify `~/.totalreclaw/credentials.json`. If missing, call `totalreclaw_pair` again (PIN likely expired) and resend.
40
- 8. Confirm: *"TotalReclaw set up ✓. Ask me to remember or recall anything — try `totalreclaw_remember` or ask me to remember something."*
33
+ 1. **Install skill + plugin.** Run via your shell tool, in this order:
34
+ ```
35
+ openclaw skills install totalreclaw
36
+ openclaw plugins install @totalreclaw/totalreclaw
37
+ ```
38
+ For RC: replace the second command with `openclaw plugins install @totalreclaw/totalreclaw@rc`. 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
39
 
42
- If `totalreclaw_*` tools ARE already in your toolset when the user asks, SKIP the install step and jump to step 4.
40
+ 2. **Wait for auto-reload — do NOT restart the gateway yourself.** 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. If it does NOT appear within 10s, the user has `gateway.reload.mode = off` — fall back: tell the user verbatim *"Auto-reload didn't fire. Please restart your OpenClaw gateway: `openclaw gateway restart` (native), `docker restart <your-container-name>` (Docker self-host — substitute the actual name; `docker ps` shows it), or your managed service's restart control. Reply `done` once it's back up."* Do NOT invoke the restart yourself — your shell is running inside the gateway you'd be restarting (catch-22). Wait for the user's `done`.
43
41
 
44
- ## Explicit two-step setup flow (Approach B, fallback)
42
+ 3. **Verify `totalreclaw_pair` is bound.** If still missing after the user said `done`, tell them the restart didn't take effect and to retry. Do not loop the install.
45
43
 
46
- Used when the user pastes a verbose prompt describing skill install + plugin install + restart + pair (e.g. *"TotalReclaw is a skill at 'totalreclaw' on ClawHub. Install it, then install the plugin, restart the gateway, and walk me through the QR pairing flow"*) OR when a tool returns `onboarding required`.
44
+ 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.
47
45
 
48
- 1. `openclaw plugins install @totalreclaw/totalreclaw` (stable). For RC: `@totalreclaw/totalreclaw@rc`.
49
- 2. Restart gateway so plugin routes + tools bind. Docker: `docker restart tr-openclaw`. Native: `openclaw gateway restart`. Wait ~5s; confirm the gateway is back up. (If you are running inside the gateway, ask the user to run the restart command and wait for confirmation.)
50
- 3. Call `totalreclaw_pair`. Returns `{url, pin, qr_ascii, expires_at_ms}`.
51
- 4. Relay to user verbatim: *"Open <url> in your browser. Enter your 12-word recovery phrase (or let the browser generate one). Confirm PIN <pin>. I'll wait for you to say done."*
52
- 5. After user says done: verify `~/.totalreclaw/credentials.json` exists and is non-empty. If missing, call `totalreclaw_pair` again (PIN likely expired) and resend.
53
- 6. Confirm: *"TotalReclaw is set up. Ask me to remember or recall anything."*
46
+ 5. **Pair.** Call `totalreclaw_pair`. Returns `{url, pin, qr_ascii, qr_png_b64, qr_unicode, expires_at_ms}` (see "Rendering the QR" below). Relay verbatim:
47
+ > *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.*
48
+
49
+ 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 *"TotalReclaw is set up. Ask me to remember or recall anything."*
50
+
51
+ ## Rendering the QR on your transport (rc.5+)
52
+
53
+ When you receive the `totalreclaw_pair` payload, render the QR based on your chat transport:
54
+
55
+ - **Transport supports image attachments** (Telegram, Slack, web chat, any transport with file/image upload): decode `qr_png_b64` from base64 to bytes, save to a tempfile with a `.png` suffix, and attach the tempfile as an image. Also show the URL + PIN as text so users who prefer clicking can still do so.
56
+ - **Terminal-only transport** (OpenClaw native CLI chat, bare SSH shell): emit `qr_unicode` inline in your reply. It renders as block characters in most terminal fonts; the user scans from their phone.
57
+ - **Web chat with inline-image support**: embed as `<img src="data:image/png;base64,<qr_png_b64>" alt="Pair QR">` if the chat client renders HTML; otherwise fall back to attachment.
58
+
59
+ The QR encodes ONLY the pair URL. The 6-digit PIN is a separate out-of-band confirmation — deliberately NOT baked into the QR (dual-channel design: scan + type). Never log, print, or re-emit the PIN outside the user-facing reply. The PIN must NOT end up in any other tool call, log file, or memory store.
54
60
 
55
61
  ## Phrase safety (HARD — never break)
56
62
 
57
- NEVER echo / generate / ask the user to paste a recovery phrase in chat. NEVER invoke `openclaw totalreclaw onboard`, `totalreclaw setup`, or any phrase-touching CLI via your shell tool (stdout enters LLM context). The ONLY agent-facilitated setup path is `totalreclaw_pair`. If the user pastes a phrase anyway: tell them it is compromised, call `totalreclaw_pair` with `mode=generate` for a fresh wallet.
63
+ NEVER echo / generate / ask the user to paste a recovery phrase in chat. NEVER invoke `openclaw totalreclaw onboard` (bare interactive form), `totalreclaw setup`, or any phrase-touching CLI via your shell tool (stdout enters LLM context). The ONLY agent-facilitated setup paths are (in order of preference): (1) `totalreclaw_pair` tool; (2) if the tool is missing from your tool list — a known gap for some container-based agents, issue #87 — shell out to EITHER `openclaw totalreclaw pair generate --url-pin-only` (single-line JSON `{v,url,pin,expires_at_ms}`) OR `openclaw totalreclaw onboard --pair-only` (single-line JSON `{v,pair_url,pin,expires_at_ms}`, issue #95 — same pair flow, `pair_url` key naming). Both have zero phrase material on stdout (pair-crypto is x25519-only, not BIP-39). If the user pastes a phrase anyway: tell them it is compromised, call `totalreclaw_pair` with `mode=generate` for a fresh wallet.
58
64
 
59
65
  ## Usage (post-setup)
60
66
 
@@ -66,7 +72,7 @@ NEVER echo / generate / ask the user to paste a recovery phrase in chat. NEVER i
66
72
 
67
73
  ## Diagnostics
68
74
 
69
- - `totalreclaw_*` tools not visible → plugin not installed or gateway not restarted. Verify via `openclaw plugins list | grep totalreclaw`. If listed, ask the user to restart the gateway.
75
+ - `totalreclaw_*` tools not visible → plugin not installed or auto-reload didn't fire. Verify via `openclaw plugins list | grep totalreclaw` (or, on a managed service that doesn't expose the CLI, ask the user to confirm via their service's plugins UI). If `totalreclaw` is listed but tools are still absent, the gateway probably hasn't completed its auto-restart yet (give it 5-10s) OR `gateway.reload.mode = off` is set — instruct manual restart as fallback: `openclaw gateway restart` (native), `docker restart <your-container-name>` (Docker self-host), or the managed service's restart control. If the plugin is listed and the gateway has been restarted but tools are still missing (container-agent gap, issue #87), shell out to `openclaw totalreclaw pair generate --url-pin-only` on the gateway host and hand the user the `url` + `pin` from the JSON payload — OR `openclaw totalreclaw onboard --pair-only` (same flow, returns `pair_url` instead of `url`, issue #95). If your shell doesn't reach the gateway host (managed service), tell the user to install/enable the plugin via their service's plugins UI and restart the gateway via the service's restart control. Do NOT suggest the bare `openclaw totalreclaw onboard` (leaks phrase on stdout — deprecated in rc.18, removed next RC).
70
76
  - User says done but `credentials.json` missing → PIN expired or entered wrong phrase; call `totalreclaw_pair` again.
71
77
  - `onboarding required` → credentials missing; redo from the pair step.
72
78
  - `quota exceeded` → `totalreclaw_status`, then offer `totalreclaw_upgrade`.
package/api-client.ts CHANGED
@@ -8,8 +8,15 @@
8
8
  * Authorization: Bearer <hex-encoded-auth-key>
9
9
  *
10
10
  * The server hashes the auth key with SHA-256 to look up the user.
11
+ *
12
+ * Every outbound request goes through `buildRelayHeaders()` so the
13
+ * `X-TotalReclaw-Client` tag is set + the optional QA-tracing
14
+ * `X-TotalReclaw-Session` tag is forwarded when `TOTALRECLAW_SESSION_ID`
15
+ * is set. See `relay-headers.ts` and internal#127.
11
16
  */
12
17
 
18
+ import { buildRelayHeaders } from './relay-headers.js';
19
+
13
20
  // ---------------------------------------------------------------------------
14
21
  // Request / Response Types
15
22
  // ---------------------------------------------------------------------------
@@ -126,7 +133,7 @@ export function createApiClient(serverUrl: string) {
126
133
  ): Promise<{ user_id: string }> {
127
134
  const res = await fetch(`${baseUrl}/v1/register`, {
128
135
  method: 'POST',
129
- headers: { 'Content-Type': 'application/json', 'X-TotalReclaw-Client': 'openclaw-plugin' },
136
+ headers: buildRelayHeaders({ 'Content-Type': 'application/json' }),
130
137
  body: JSON.stringify({ auth_key_hash: authKeyHash, salt: saltHex }),
131
138
  });
132
139
  await assertOk(res, 'register');
@@ -160,10 +167,10 @@ export function createApiClient(serverUrl: string) {
160
167
  ): Promise<{ ids: string[]; duplicate_ids?: string[] }> {
161
168
  const res = await fetch(`${baseUrl}/v1/store`, {
162
169
  method: 'POST',
163
- headers: {
170
+ headers: buildRelayHeaders({
164
171
  'Content-Type': 'application/json',
165
172
  Authorization: `Bearer ${authKeyHex}`,
166
- },
173
+ }),
167
174
  body: JSON.stringify({ user_id: userId, facts }),
168
175
  });
169
176
  await assertOk(res, 'store');
@@ -198,10 +205,10 @@ export function createApiClient(serverUrl: string) {
198
205
  ): Promise<SearchCandidate[]> {
199
206
  const res = await fetch(`${baseUrl}/v1/search`, {
200
207
  method: 'POST',
201
- headers: {
208
+ headers: buildRelayHeaders({
202
209
  'Content-Type': 'application/json',
203
210
  Authorization: `Bearer ${authKeyHex}`,
204
- },
211
+ }),
205
212
  body: JSON.stringify({
206
213
  user_id: userId,
207
214
  trapdoors,
@@ -229,9 +236,9 @@ export function createApiClient(serverUrl: string) {
229
236
  async deleteFact(factId: string, authKeyHex: string): Promise<void> {
230
237
  const res = await fetch(`${baseUrl}/v1/facts/${encodeURIComponent(factId)}`, {
231
238
  method: 'DELETE',
232
- headers: {
239
+ headers: buildRelayHeaders({
233
240
  Authorization: `Bearer ${authKeyHex}`,
234
- },
241
+ }),
235
242
  });
236
243
  await assertOk(res, 'deleteFact');
237
244
  const json = (await res.json()) as Record<string, unknown>;
@@ -254,10 +261,10 @@ export function createApiClient(serverUrl: string) {
254
261
  async batchDelete(factIds: string[], authKeyHex: string): Promise<number> {
255
262
  const res = await fetch(`${baseUrl}/v1/facts/batch-delete`, {
256
263
  method: 'POST',
257
- headers: {
264
+ headers: buildRelayHeaders({
258
265
  'Content-Type': 'application/json',
259
266
  Authorization: `Bearer ${authKeyHex}`,
260
- },
267
+ }),
261
268
  body: JSON.stringify({ fact_ids: factIds }),
262
269
  });
263
270
  await assertOk(res, 'batchDelete');
@@ -290,9 +297,9 @@ export function createApiClient(serverUrl: string) {
290
297
 
291
298
  const res = await fetch(`${baseUrl}/v1/export?${params.toString()}`, {
292
299
  method: 'GET',
293
- headers: {
300
+ headers: buildRelayHeaders({
294
301
  Authorization: `Bearer ${authKeyHex}`,
295
- },
302
+ }),
296
303
  });
297
304
  await assertOk(res, 'exportFacts');
298
305
  const json = (await res.json()) as Record<string, unknown>;
package/claims-helper.ts CHANGED
@@ -96,6 +96,14 @@ export interface BuildClaimInput {
96
96
  sourceAgent: string;
97
97
  /** Creation timestamp. Defaults to now. */
98
98
  extractedAt?: string;
99
+ /**
100
+ * 3.3.1-rc.22 — optional embedding-model id stamped on the claim for
101
+ * forward-compat. Defaults to omitted (callers that already know the
102
+ * active embedder pass it in; legacy paths leave it unset). The field
103
+ * is plugin-scoped — it survives the core validator's strip pass via
104
+ * the same re-attach path used for `schema_version` / `volatility`.
105
+ */
106
+ embeddingModelId?: string;
99
107
  }
100
108
 
101
109
  /**
@@ -112,7 +120,7 @@ export interface BuildClaimInput {
112
120
  * payload (see `subgraph-store.ts::encodeFactProtobuf`).
113
121
  */
114
122
  export function buildCanonicalClaim(input: BuildClaimInput): string {
115
- const { fact, importance, extractedAt } = input;
123
+ const { fact, importance, extractedAt, embeddingModelId } = input;
116
124
 
117
125
  // Defensive: ensure fact.source is always populated before v1 validation.
118
126
  // `applyProvenanceFilterLax` should have set this upstream; this is the
@@ -125,6 +133,7 @@ export function buildCanonicalClaim(input: BuildClaimInput): string {
125
133
  fact: factWithSource,
126
134
  importance,
127
135
  createdAt: extractedAt,
136
+ embeddingModelId,
128
137
  });
129
138
  }
130
139
 
@@ -173,6 +182,14 @@ export interface BuildClaimV1Input {
173
182
  * when provided.
174
183
  */
175
184
  pinStatus?: PinStatus;
185
+ /**
186
+ * 3.3.1-rc.22 — optional embedding-model id stamped on the claim for
187
+ * distillation forward-compat. Survives the core validator strip pass
188
+ * via the same re-attach path used for `schema_version` / `volatility`.
189
+ * When omitted the field is not emitted (legacy claims remain untagged
190
+ * and are read back as "unspecified").
191
+ */
192
+ embeddingModelId?: string;
176
193
  }
177
194
 
178
195
  /**
@@ -257,6 +274,13 @@ export function buildCanonicalClaimV1(input: BuildClaimV1Input): string {
257
274
  if (fact.volatility && (VALID_MEMORY_VOLATILITIES as readonly string[]).includes(fact.volatility)) {
258
275
  canonical.volatility = fact.volatility;
259
276
  }
277
+ // 3.3.1-rc.22 — forward-compat embedder marker. Plugin-only field;
278
+ // survives the core validator via re-attach. Future distillation
279
+ // detects this on read to re-embed selectively without forcing a
280
+ // vault-wide rebuild.
281
+ if (typeof input.embeddingModelId === 'string' && input.embeddingModelId.length > 0) {
282
+ canonical.embedding_model_id = input.embeddingModelId;
283
+ }
260
284
 
261
285
  return JSON.stringify(canonical);
262
286
  }
@@ -313,6 +337,11 @@ export interface BuildV1ClaimBlobInput {
313
337
  * non-pin write.
314
338
  */
315
339
  pinStatus?: PinStatus;
340
+ /**
341
+ * 3.3.1-rc.22 — optional embedding-model id stamped on the claim for
342
+ * distillation forward-compat. See `BuildClaimV1Input.embeddingModelId`.
343
+ */
344
+ embeddingModelId?: string;
316
345
  }
317
346
 
318
347
  /**
@@ -374,6 +403,10 @@ export function buildV1ClaimBlob(input: BuildV1ClaimBlobInput): string {
374
403
  if (input.volatility && (VALID_MEMORY_VOLATILITIES as readonly string[]).includes(input.volatility)) {
375
404
  canonical.volatility = input.volatility;
376
405
  }
406
+ // 3.3.1-rc.22 — see `buildCanonicalClaimV1` comment.
407
+ if (typeof input.embeddingModelId === 'string' && input.embeddingModelId.length > 0) {
408
+ canonical.embedding_model_id = input.embeddingModelId;
409
+ }
377
410
  return JSON.stringify(canonical);
378
411
  }
379
412
 
@@ -431,6 +464,13 @@ export interface V1BlobReadResult {
431
464
  * when the writer explicitly omitted the field (treated as `"unpinned"`).
432
465
  */
433
466
  pinStatus?: PinStatus;
467
+ /**
468
+ * 3.3.1-rc.22 — embedder identity tag. Absent on claims written by
469
+ * older plugin versions. Forward-compat marker; consumers MAY use it
470
+ * to decide whether a claim's stored embedding matches the active
471
+ * embedder before letting cosine similarity make a relevance call.
472
+ */
473
+ embeddingModelId?: string;
434
474
  }
435
475
 
436
476
  export function readV1Blob(decrypted: string): V1BlobReadResult | null {
@@ -497,6 +537,12 @@ export function readV1Blob(decrypted: string): V1BlobReadResult | null {
497
537
  result.pinStatus = ps;
498
538
  }
499
539
  }
540
+ // 3.3.1-rc.22 — pull the embedder identity tag through. Plugin-only
541
+ // field added by `buildCanonicalClaimV1` / `buildV1ClaimBlob` after
542
+ // core validation.
543
+ if (typeof obj.embedding_model_id === 'string' && obj.embedding_model_id.length > 0) {
544
+ result.embeddingModelId = obj.embedding_model_id;
545
+ }
500
546
 
501
547
  return result;
502
548
  } catch {