@totalreclaw/totalreclaw 3.3.1-rc.2 → 3.3.1-rc.20

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 (64) hide show
  1. package/CHANGELOG.md +283 -0
  2. package/SKILL.md +50 -83
  3. package/config.ts +57 -0
  4. package/dist/api-client.js +220 -0
  5. package/dist/billing-cache.js +100 -0
  6. package/dist/claims-helper.js +606 -0
  7. package/dist/config.js +223 -0
  8. package/dist/consolidation.js +258 -0
  9. package/dist/contradiction-sync.js +1034 -0
  10. package/dist/crypto.js +130 -0
  11. package/dist/digest-sync.js +361 -0
  12. package/dist/download-ux.js +63 -0
  13. package/dist/embedding.js +86 -0
  14. package/dist/extractor.js +1225 -0
  15. package/dist/first-run.js +103 -0
  16. package/dist/fs-helpers.js +481 -0
  17. package/dist/gateway-url.js +197 -0
  18. package/dist/generate-mnemonic.js +13 -0
  19. package/dist/hot-cache-wrapper.js +101 -0
  20. package/dist/import-adapters/base-adapter.js +64 -0
  21. package/dist/import-adapters/chatgpt-adapter.js +238 -0
  22. package/dist/import-adapters/claude-adapter.js +114 -0
  23. package/dist/import-adapters/gemini-adapter.js +201 -0
  24. package/dist/import-adapters/index.js +26 -0
  25. package/dist/import-adapters/mcp-memory-adapter.js +219 -0
  26. package/dist/import-adapters/mem0-adapter.js +158 -0
  27. package/dist/import-adapters/types.js +1 -0
  28. package/dist/index.js +5233 -0
  29. package/dist/llm-client.js +686 -0
  30. package/dist/llm-profile-reader.js +346 -0
  31. package/dist/lsh.js +57 -0
  32. package/dist/onboarding-cli.js +750 -0
  33. package/dist/pair-cli.js +344 -0
  34. package/dist/pair-crypto.js +359 -0
  35. package/dist/pair-http.js +404 -0
  36. package/dist/pair-page.js +826 -0
  37. package/dist/pair-qr.js +107 -0
  38. package/dist/pair-remote-client.js +410 -0
  39. package/dist/pair-session-store.js +566 -0
  40. package/dist/pin.js +542 -0
  41. package/dist/qa-bug-report.js +301 -0
  42. package/dist/reranker.js +442 -0
  43. package/dist/retype-setscope.js +348 -0
  44. package/dist/semantic-dedup.js +75 -0
  45. package/dist/subgraph-search.js +288 -0
  46. package/dist/subgraph-store.js +689 -0
  47. package/dist/tool-gating.js +58 -0
  48. package/download-ux.ts +91 -0
  49. package/embedding.ts +32 -9
  50. package/fs-helpers.ts +32 -0
  51. package/gateway-url.ts +57 -9
  52. package/index.ts +436 -334
  53. package/llm-client.ts +211 -23
  54. package/onboarding-cli.ts +114 -1
  55. package/package.json +18 -5
  56. package/pair-cli.ts +76 -8
  57. package/pair-crypto.ts +34 -24
  58. package/pair-page.ts +28 -17
  59. package/pair-qr.ts +152 -0
  60. package/pair-remote-client.ts +540 -0
  61. package/qa-bug-report.ts +381 -0
  62. package/reranker.ts +73 -0
  63. package/retype-setscope.ts +12 -0
  64. package/subgraph-store.ts +94 -6
package/CHANGELOG.md CHANGED
@@ -4,6 +4,289 @@ 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.1-rc.16] — 2026-04-24
8
+
9
+ Fixes #92 — slow-host install times out during ONNX-runtime / embedding-model
10
+ download. ONNX stays mandatory (no opt-in flag); first-call download is now
11
+ wrapped with timeout, progress, and retry UX so slow connections succeed
12
+ instead of silently hanging until OpenClaw SIGTERMs.
13
+
14
+ ### Embedding-model download UX
15
+
16
+ - New `download-ux.ts` module — pure stdlib, no third-party imports — exposes
17
+ `downloadWithUX(label, fn, opts)`. Wraps a download promise with:
18
+ - **Per-attempt timeout**, default 600s (covers ~290 KB/s for the 344 MB
19
+ Harrier model). Configurable via env `TOTALRECLAW_ONNX_INSTALL_TIMEOUT`
20
+ (in seconds). Per-attempt timeout grows 1x/2x/4x across retries.
21
+ - **60s keep-alive log** during long downloads so users on slow networks
22
+ see "still downloading… (Ns elapsed)" rather than a frozen prompt.
23
+ - **3-attempt exponential-backoff retry** (5s/10s backoff between attempts)
24
+ to absorb transient network blips.
25
+ - **Loud actionable error** on exhaustion: names the env var to extend the
26
+ timeout and the exact `openclaw plugins install totalreclaw` command to
27
+ rerun.
28
+ - `embedding.ts` now wraps `AutoTokenizer.from_pretrained`,
29
+ `AutoModel.from_pretrained`, and the `pipeline()` call with
30
+ `downloadWithUX`. Prints a user-visible "Downloading embedding model
31
+ (~344MB) — this may take a few minutes on slower connections. Please wait."
32
+ message before the first download starts.
33
+ - ONNX remains a mandatory hard `dependency` (no `[embedding]`-style opt-in
34
+ extra). Recall accuracy is unchanged.
35
+ - Regression: `test_issue_92_onnx_download_ux.test.ts` exercises happy path,
36
+ transient failure → retry, full exhaustion, per-attempt timeout, and
37
+ keep-alive cadence. Wired into the plugin `npm test` chain.
38
+
39
+ ## [3.3.1-rc.14] — 2026-04-24
40
+
41
+ Coordinated version bump with Python `2.3.1rc14`. Two narrow bug fixes
42
+ found during rc.13 user QA on 2026-04-24:
43
+
44
+ ### RC-gated QA bug tool — target-repo hardening
45
+
46
+ `totalreclaw_report_qa_bug` now refuses to file to any repo that isn't
47
+ internal. rc.13 user QA surfaced agent-filed bug reports leaking to the
48
+ public `p-diogo/totalreclaw` tracker despite the tool's default target
49
+ being `p-diogo/totalreclaw-internal`.
50
+
51
+ - New env var: `TOTALRECLAW_QA_REPO` lets operators point the tool at a
52
+ private fork. The default stays `p-diogo/totalreclaw-internal`.
53
+ - New `resolveQaRepo(...)` guard: rejects any slug that is on the
54
+ public-repo denylist (includes `p-diogo/totalreclaw`,
55
+ `...-website`, `...-relay`, `...-plugin`, `...-hermes`) OR does not
56
+ end in `-internal`. The check runs before the HTTP POST is
57
+ constructed, so rejection never leaves the client.
58
+ - `CONFIG.qaRepoOverride` surfaces the env var through `config.ts`
59
+ (keeps scanner-sensitive `process.env` reads centralized).
60
+ - Regression test in `qa-bug-report.test.ts` mocks the public slug
61
+ and asserts `fetch` is NEVER called.
62
+
63
+ Labels on filing unchanged — still emits `qa-bug`, `pending-triage`,
64
+ `severity:<...>`, `component:<...>`, `rc:<...>`.
65
+
66
+ ### Relay pair page — PIN paste button UX
67
+
68
+ The paste button on the step-1 PIN screen was silently failing under
69
+ certain browser states. rc.14 rewrites the handler with a proper
70
+ error taxonomy:
71
+
72
+ - Capability probe up front — `navigator.clipboard.readText` missing →
73
+ clear "Paste unavailable on this browser" toast.
74
+ - `NotAllowedError` → "Clipboard access denied — type the 6 digits
75
+ manually" (covers iOS Safari permission denial).
76
+ - Empty clipboard → "Clipboard is empty — copy the PIN from your chat
77
+ first".
78
+ - Non-digit content → "Clipboard has no digits — copy the 6-digit PIN
79
+ first".
80
+ - Every failure path focuses the first PIN cell so the user can fall
81
+ through to manual typing without another click.
82
+ - Errors log to `console.warn` with name + message so future failures
83
+ are diagnosable from browser devtools.
84
+
85
+ The mockup at `docs/mockups/rc13-pair-wizard/wizard.js` gets the same
86
+ rewrite for parity — the relay's `scripts/sync-pair-preview.mjs`
87
+ regenerates `/pair-preview/` from this source.
88
+
89
+ Fix also applies to the "Paste all 12 words" import-grid button on the
90
+ relay production page (same taxonomy, same focus-fallback).
91
+
92
+ ## [3.3.1-rc.13] — 2026-04-24
93
+
94
+ Coordinated version bump with Python `2.3.1rc13`. No substantive
95
+ changes to the plugin's own TypeScript — the rc.13 fix lands on the
96
+ Hermes-side (`python/src/totalreclaw/hermes/pair_tool.py`) where the
97
+ asyncio lifecycle regression lived. We keep plugin + Python RC
98
+ numbers in lockstep so the release-pipeline tracker and
99
+ `qa-totalreclaw` skill carry both artifacts through QA as one
100
+ bundle.
101
+
102
+ See the corresponding entry in `python/CHANGELOG.md` for the full
103
+ design: the relay-pair WebSocket is now owned by a dedicated worker
104
+ thread (with its own event loop) so it survives the Hermes
105
+ tool-invocation loop teardown that destroyed the rc.10–rc.12 waiter
106
+ mid-recv and caused every pair attempt to 502.
107
+
108
+ The relay-served production pair page is also replaced with the
109
+ rc.13 wizard UX — a typeform-style 3-step flow (PIN → phrase → done)
110
+ mirroring the `docs/mockups/rc13-pair-wizard/` design. This lands in
111
+ the `totalreclaw-relay` repo PR, not here, but surfaces to every
112
+ OpenClaw user via the default relay pair flow.
113
+
114
+ ### Plugin local-mode pair page
115
+
116
+ `skill/plugin/pair-page.ts` (the local-mode fallback served when a
117
+ user sets `TOTALRECLAW_PAIR_MODE=local`) retains its rc.10–rc.12 UX
118
+ shape. The wizard UX port for this file is deferred to rc.14 pending
119
+ a design decision on whether to share a single CSS+JS asset across
120
+ all three pair pages (relay / Python local / plugin local) or keep
121
+ them independently inlined. Local-mode is rarely exercised — the
122
+ plugin defaults to the relay flow via the Hermes Python sidecar and
123
+ only falls back here for air-gapped setups.
124
+
125
+ ## [3.3.1-rc.12] — 2026-04-23
126
+
127
+ **Ship-stopper fix for rc.11.** The relay-served pair page's submit
128
+ button threw `NotSupportedError: Failed to execute 'importKey' on
129
+ 'SubtleCrypto': Algorithm: Unrecognized name` when the user clicked
130
+ "Seal key and finish". Root cause: `ChaCha20-Poly1305` is NOT
131
+ implemented in the Web Crypto API of Chrome / Safari / Edge — the
132
+ spec exposes `AES-GCM` as the only AEAD. rc.10/rc.11 never worked
133
+ end-to-end for any user; every pair attempt failed silently and the
134
+ token expired without logging a failure — GH issue #79.
135
+
136
+ rc.12 swaps the cipher suite from ChaCha20-Poly1305 to AES-256-GCM on
137
+ both sides (browser + gateway). Wire shape unchanged — still 12-byte
138
+ nonce, 16-byte tag, sid-bound AAD, base64url encoding. HKDF info bumped
139
+ from `totalreclaw-pair-v1` to `totalreclaw-pair-v2` so rc.11 ciphertexts
140
+ cannot collide with rc.12 keys (fail-closed on any version skew).
141
+
142
+ ### Changed
143
+ - `skill/plugin/pair-crypto.ts`: `aeadDecrypt` / `aeadEncryptWithSessionKey`
144
+ switched from `chacha20-poly1305` to `aes-256-gcm`. `HKDF_INFO` bumped
145
+ to `totalreclaw-pair-v2`.
146
+ - `skill/plugin/pair-page.ts` (local-mode pair page): WebCrypto
147
+ `ChaCha20-Poly1305` calls swapped to `AES-GCM`. Capability probe
148
+ function renamed `chaChaSupported` → `aesGcmSupported`.
149
+
150
+ ### Observability
151
+ - The relay's `pair-html.ts` (user-facing page) now reports phase-labelled
152
+ error messages so a network / encrypt / submit failure no longer masks
153
+ as a silent "stuck on acknowledge screen". Relay PR (fix/pair-aes-gcm-rc12)
154
+ is the canonical fix for the issue reported in #79.
155
+
156
+ ## [3.3.1-rc.11] — 2026-04-23
157
+
158
+ 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.
159
+
160
+ 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.
161
+
162
+ ### Added
163
+
164
+ - **`skill/plugin/pair-remote-client.ts`** — new. TypeScript mirror of `python/src/totalreclaw/pair/remote_client.py` (rc.10 Hermes):
165
+ - `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.
166
+ - `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.
167
+ - `pairViaRelay(...)` — one-shot convenience wrapper for tests and simple callers.
168
+ - **`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.
169
+ - **`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}`).
170
+ - **`TOTALRECLAW_PAIR_RELAY_URL`** env (plugin side) — self-hosters can point at their own relay. Defaults to `wss://api-staging.totalreclaw.xyz`.
171
+ - **`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.
172
+
173
+ ### Changed
174
+
175
+ - **`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.
176
+
177
+ ### Phrase-safety invariants (preserved)
178
+
179
+ - 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.
180
+ - 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.
181
+ - Session state is in-memory on the relay with a 5-minute TTL. Redis deferred to Phase 2 per the design blueprint.
182
+ - 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.
183
+
184
+ ### Mechanism / byte-compat
185
+
186
+ 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.
187
+
188
+ ## [3.3.1-rc.10] — 2026-04-23
189
+
190
+ 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.
191
+
192
+ 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.
193
+
194
+ ### Added (rebased from PR #76)
195
+
196
+ - **`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`).
197
+ - **`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.
198
+ - **SKILL.md "Rendering the QR on your transport" section** — per-transport agent rendering guidance (Telegram attachment, terminal inline, web chat `<img>` embed).
199
+ - **`qrcode` + `@types/qrcode`** runtime deps.
200
+
201
+ ### Removed (rc.5 phrase-safety carve-out closure, rebased)
202
+
203
+ - **`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.
204
+
205
+ Version bump reason: rc cadence keeps Python + plugin aligned so the release-pipeline tracker carries them through QA as one artifact set.
206
+
207
+ ## [3.3.1-rc.9] — 2026-04-23
208
+
209
+ 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.
210
+
211
+ ### Why a plugin bump when only Python changed
212
+
213
+ 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.
214
+
215
+ 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:
216
+
217
+ 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.
218
+ 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`.
219
+
220
+ 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.
221
+
222
+ ### Skipped
223
+
224
+ - **`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.
225
+
226
+ ## [3.3.1-rc.6] — 2026-04-22
227
+
228
+ 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.
229
+
230
+ ### Why a plugin bump when only Python changed
231
+
232
+ 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.6 documented on the Python side only; a later plugin bug would then have to skip to rc.7 to catch up. Much simpler to bump both in lockstep.
233
+
234
+ ### Skipped
235
+
236
+ - **`3.3.1-rc.5`** — PR #76 (branch `fix/plugin-3.3.1-rc.5-qr-display`) remained unmerged when the rc.4 Hermes regression was escalated. rc.5's QR-display work rebases onto rc.6 as a follow-up.
237
+
238
+ ## [3.3.1-rc.4] — 2026-04-22
239
+
240
+ Phrase-safety hardening: `totalreclaw_onboard` agent tool removed. Paired with Hermes Python `2.3.1rc4` (which ports the QR-pair flow to Python so Hermes users gain a phrase-safe agent setup path too).
241
+
242
+ ### Removed (phrase-safety enforcement — BREAKING for agent tool callers)
243
+
244
+ - **`totalreclaw_onboard` agent tool — REMOVED.** rc.3 shipped a `totalreclaw_onboard` tool that generated a fresh BIP-39 mnemonic in-process, wrote it to `credentials.json`, and returned `{scope_address, credentials_path}`. `emitPhrase: false` kept the mnemonic out of the tool's return payload, but NOTHING ARCHITECTURALLY PREVENTED leakage — a future patch could regress the flag, a different code path could echo the mnemonic in a log/error, or the mere existence of the tool signalled to agents that phrase generation inside chat is fine (it isn't). Per `project_phrase_safety_rule.md`: "recovery phrase MUST NEVER cross the LLM context in ANY form." rc.4 removes the registration. The underlying `runNonInteractiveOnboard` code path stays reachable via the CLI `openclaw totalreclaw onboard` — that path runs in the user's own terminal, OUTSIDE any agent shell, so phrase stdout never feeds back into LLM context.
245
+
246
+ ### Changed
247
+
248
+ - **`SKILL.md` — setup section rewritten.** `totalreclaw_pair` is now the canonical setup surface for all users (local or remote). The CLI wizard (`openclaw totalreclaw onboard`) is explicitly documented as user-terminal-only — agents MUST NOT invoke it via their shell tool. Tool surface table updated: `totalreclaw_onboard` removed, `totalreclaw_pair` promoted to canonical. `totalreclaw_onboarding_start` remains as a pointer-only tool for users who explicitly prefer local-terminal setup.
249
+ - **`index.ts` — `totalreclaw_pair` tool description updated.** Removed backref to `totalreclaw_onboard`; now instructs agents to always prefer pair, with `totalreclaw_onboarding_start` as the fallback pointer for local-terminal-only users.
250
+ - **`docs/guides/openclaw-setup.md` — QR pairing is now documented as the default setup flow.** CLI wizard moved to a user-terminal-only subsection with a prominent "do NOT run this through an agent shell" warning.
251
+
252
+ ### Tests
253
+
254
+ - **`phrase-safety-registry.test.ts`** — new. Text-scans `index.ts` for `api.registerTool({ name: '...' })` literals and asserts: (a) `totalreclaw_onboard` is NOT in the list; (b) `totalreclaw_pair` IS in the list; (c) no name contains phrase-adjacent tokens (`onboard_generate`, `generate_phrase`, `generate_mnemonic`, `restore_phrase`, `restore_mnemonic`, `mnemonic`). Runs as part of `npm test`.
255
+
256
+ ## [3.3.1-rc.3] — 2026-04-22
257
+
258
+ Patch RC bundling two stability fixes, one new RC-gated tool, two SKILL.md addendums, and a configurable LLM retry budget. All prior rc.1 + rc.2 fixes are preserved.
259
+
260
+ ### Changed
261
+
262
+ - **`llm-client.ts` — configurable `ZAI_BASE_URL` + auto-fallback on "Insufficient balance" 429.** rc.2 QA surfaced that GLM Coding Plan keys hitting the STANDARD zai endpoint (and PAYG keys hitting CODING) return HTTP 429 with body `"Insufficient balance or no resource package. Please recharge."` — misleading because the key itself is valid. rc.3: (a) accepts `ZAI_BASE_URL` env override via `config.ts` / `getZaiBaseUrl()`; (b) auto-detects the error signature and flips CODING ↔ STANDARD once per call (logged at INFO). SKILL.md now documents "GLM Coding Plan → leave unset; PAYG → set `ZAI_BASE_URL=https://api.z.ai/api/paas/v4`."
263
+ - **`llm-client.ts` — retry budget 7s → ~62s (configurable).** rc.1/rc.2 QA: 5–9 of 10 extraction windows returned 0 facts against multi-minute upstream 429 storms. The 3-attempt 1s/2s/4s backoff couldn't outlast a 9-minute outage. rc.3: 5 attempts, 2s/4s/8s/16s/32s backoff, total ~62s. Configurable via `TOTALRECLAW_LLM_RETRY_BUDGET_MS` env (default 60_000). First retry logs at INFO, rest at DEBUG (debounced — no spam during long outages). On exhaustion throws `LLMUpstreamOutageError` (structured, `attempts` + `lastStatus`) so extraction callers can recognise vs bail silently. Non-retryable errors (401/403/404/parse) still propagate as plain `Error`.
264
+ - **`subgraph-store.ts` — per-account submission mutex.** rc.2 logged 16 AA25 `invalid account nonce` events from concurrent `submitFactBatchOnChain` / `submitFactOnChain` calls racing at the `eth_call getNonce(sender, 0)` step. rc.3 wraps both submission entry points in a per-`sender` `Map<scopeAddress, Promise>` chain so only one UserOp is in flight per Smart Account at a time. The existing AA25-retry-with-fresh-nonce path is unchanged and still catches relay-side zombie UserOps.
265
+
266
+ ### Added
267
+
268
+ - **`totalreclaw_report_qa_bug`** (RC-gated tool) — lets agents file structured QA-bug issues to `p-diogo/totalreclaw-internal` without the maintainer opening a fresh issue per RC finding. Only registered when the plugin version matches the `-rc.` token (via `readPluginVersion` in `fs-helpers.ts` + `isRcBuild` in the new `qa-bug-report.ts`). Handler POSTs to `https://api.github.com/repos/.../issues` with `Authorization: Bearer <token>` where `token = CONFIG.qaGithubToken` (reads `TOTALRECLAW_QA_GITHUB_TOKEN` or `GITHUB_TOKEN`). Secrets (BIP-39 phrases, `sk-*`, `AIzaSy*`, Telegram bot tokens, bearer tokens, 64+ char hex blobs, 0x-private-keys, `token=`/`secret=` qualifiers) are redacted fail-close in `redactSecrets()` before POST. Stable builds never expose this tool. See SKILL.md "Filing QA bugs (RC builds only)" for trigger rules — always ask user before filing, never the same bug twice.
269
+ - **`skill/plugin/qa-bug-report.ts`** — new pure-logic + HTTP module. Exports `isRcBuild`, `redactSecrets`, `validateQaBugArgs`, `buildIssueBody`, `postQaBugIssue`. Unit-tested in `qa-bug-report.test.ts`.
270
+ - **`skill/plugin/nonce-serialization.test.ts`** — exercises the per-`sender` mutex primitive: same-sender serializes, different-sender runs in parallel, case-insensitive keying, first-call failure releases the lock for the next.
271
+ - **`fs-helpers.ts` — `readPluginVersion(packageJsonDir)`** — scanner-safe helper used by the RC gate. Resolves via `path.dirname(fileURLToPath(import.meta.url))` in `index.ts` and returns the `version` field from `package.json` next to the module.
272
+
273
+ ### SKILL.md
274
+
275
+ - **First-person recall rule.** rc.2 debug found agents skipped `totalreclaw_recall` in 5/5 attempts on "Where do I live?". SKILL.md now hard-rules it: any first-person factual query ("where do I live/work", "what do I prefer", "my [noun]", etc.) MUST call recall first. If recall returns 0, say "I don't have anything about that yet" rather than invent.
276
+ - **QA bug triggers.** New "Filing QA bugs (RC builds only)" section with the four triggers (repeated tool failure, user friction signals, setup errors, docs-vs-reality mismatch). Offer to file, never auto-file, never same bug twice.
277
+ - **zai endpoint + retry budget** documented in a new "zai provider configuration" section.
278
+
279
+ ### Tests
280
+
281
+ - `llm-client-retry.test.ts` extended from 29 → 59 assertions. Covers: balance-error detection, CODING↔STANDARD fallback URL helper, `ZAI_BASE_URL` env override, full fallback happy/sad paths, `LLMUpstreamOutageError` surfacing, budget short-circuit.
282
+ - `qa-bug-report.test.ts` — 57 assertions covering isRcBuild, redactSecrets (BIP-39 / sk- / AIza / Telegram / Bearer / hex / private-key / preservation of UUIDs+SHAs+addresses), validateQaBugArgs, buildIssueBody, postQaBugIssue success + all failure paths.
283
+ - `nonce-serialization.test.ts` — 9 assertions.
284
+ - All existing tests (`llm-client.test.ts`, `manifest-shape.test.ts`, etc.) unchanged and green.
285
+
286
+ ### Scanner
287
+
288
+ - `check-scanner.mjs` still passes (0 flags). The `TOTALRECLAW_QA_GITHUB_TOKEN` + `ZAI_BASE_URL` + `TOTALRECLAW_LLM_RETRY_BUDGET_MS` env reads live in `config.ts` (the env-harvesting-free house). `llm-client.ts`, `index.ts`, and `qa-bug-report.ts` all stay off `process.env`.
289
+
7
290
  ## [3.3.1-rc.2] — 2026-04-22
8
291
 
9
292
  Follow-up RC for the 3.3.1-rc.1 QA NO-GO
package/SKILL.md CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: totalreclaw
3
- description: "End-to-end encrypted memory for AI agents portable, yours forever. XChaCha20-Poly1305 E2EE: server never sees plaintext."
4
- version: 3.3.1-rc.2
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.11
5
5
  author: TotalReclaw Team
6
6
  license: MIT
7
7
  homepage: https://totalreclaw.xyz
@@ -22,95 +22,62 @@ metadata:
22
22
  - persistent-context
23
23
  ---
24
24
 
25
- # TotalReclaw — Agent Guide
25
+ # TotalReclaw — OpenClaw
26
26
 
27
- You are operating inside a host that has the TotalReclaw memory plugin loaded. This file tells you WHAT to call and WHEN. Every sentence is a rule.
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
- ## Setup state
29
+ ## Setup flow
30
30
 
31
- Before any memory tool, check `~/.totalreclaw/credentials.json`:
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
- - **Missing, empty, malformed, or no `mnemonic` key** -> NOT set up. You MUST NOT generate a phrase in chat, and you MUST NOT ask the user to paste one. Route:
34
- - Local gateway: call `totalreclaw_onboarding_start` (or `totalreclaw_onboard` if available); both return a pointer to `openclaw totalreclaw onboard`.
35
- - Remote gateway (VPS / Docker / no TTY): call `totalreclaw_pair` (v3.3.1+), OR instruct the user to run `openclaw totalreclaw pair generate --json`. Present the returned `{url, pin, qr_ascii}` to the user. The phrase is entered in their browser, never in this conversation.
36
- - **Present with a valid `mnemonic`** -> active. Use the tools below.
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.
37
39
 
38
- ## Core rules
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`.
39
41
 
40
- 1. **Phrase safety (NON-NEGOTIABLE).** The recovery phrase is a secret. Never generate one in chat, never echo one back, never log one in reasoning / tool payloads, never pass one to another tool. Direct users to `cat ~/.totalreclaw/credentials.json | jq -r .mnemonic`.
41
- 2. **Use the tools, not inline state.** `totalreclaw_remember` stores; `totalreclaw_recall` retrieves. Don't re-ask the user for past facts; don't paraphrase memories as your own recollection.
42
- 3. **Gateway restart is required after install.** If tools fail right after `openclaw plugins install @totalreclaw/totalreclaw`, tell the user to run `openclaw restart` or `docker restart openclaw-qa`.
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.
43
43
 
44
- ## Decision tree which tool, when
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.
45
45
 
46
- - **Fact-shaped content** (preferences, identity, commitments, decisions, directives, episodes): `totalreclaw_remember`. Don't ask permission for obvious user facts.
47
- - **Question about the past** ("what do you remember about…", "do I like…"): `totalreclaw_recall`.
48
- - **"Pin that" / "remember permanently"**: `totalreclaw_pin`. **"Unpin" / "can forget that"**: `totalreclaw_unpin`.
49
- - **"Forget X" / "delete that memory"**: `totalreclaw_forget` with the fact's id.
50
- - **"That's a preference, not a claim"**: `totalreclaw_retype`.
51
- - **"Put that under work"**: `totalreclaw_set_scope`.
52
- - **"Export my memories"**: `totalreclaw_export`.
53
- - **"What's my TotalReclaw status?"**: `totalreclaw_status`.
54
- - **"Set up TotalReclaw"** (no credentials): route per the Setup-state section above.
55
- - **"Import my Mem0 / ChatGPT / Claude / Gemini history"**: `totalreclaw_import_from` with `dry_run=true` first. Show the estimate, confirm, then run without `dry_run`. For >50 chunks, use `totalreclaw_import_batch` and report progress.
56
- - **"Upgrade" / "I want Pro"**: `totalreclaw_upgrade` returns a Stripe URL. After upgrade, offer `totalreclaw_migrate` (dry-run first) to move testnet memories to mainnet.
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.
60
+
61
+ ## Phrase safety (HARD — never break)
62
+
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.
64
+
65
+ ## Usage (post-setup)
66
+
67
+ - Stable user facts / preferences / identity / "remember X" → `totalreclaw_remember` (don't ask permission).
68
+ - First-person factual query ("do I / what's my / where do I …") → `totalreclaw_recall` FIRST, then answer from returned facts. If 0 results, say so.
69
+ - "forget / pin / unpin / retype / move to <scope>" X → `totalreclaw_recall` first for `factId`, then `totalreclaw_forget` / `_pin` / `_unpin` / `_retype` / `_set_scope`.
70
+ - "status" → `totalreclaw_status`. "upgrade" → `totalreclaw_upgrade` (paste Stripe URL verbatim). Post-upgrade testnet → mainnet: `totalreclaw_migrate` with `dry_run` first.
71
+ - "import / export / consolidate" → `totalreclaw_import_from` (`dry_run=true` first; `_import_batch` for >50 chunks) · `totalreclaw_export` · `totalreclaw_consolidate` (`dry_run` first).
72
+
73
+ ## Diagnostics
74
+
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).
76
+ - User says done but `credentials.json` missing → PIN expired or entered wrong phrase; call `totalreclaw_pair` again.
77
+ - `onboarding required` → credentials missing; redo from the pair step.
78
+ - `quota exceeded` → `totalreclaw_status`, then offer `totalreclaw_upgrade`.
79
+ - `No LLM available for auto-extraction` at startup → provider key unreachable; check `~/.openclaw/agents/<agent>/agent/auth-profiles.json` or plugin config `extraction.llm`.
57
80
 
58
81
  ## Tool surface
59
82
 
60
- Tools work only when credentials are active AND the gateway has been restarted post-install. If a tool returns "onboarding required", route back to onboarding.
61
-
62
- | Tool | Key params |
63
- |------|------------|
64
- | `totalreclaw_remember` | `text`, optional `type` (default `claim`), `importance` |
65
- | `totalreclaw_recall` | `query`, optional `k` (default 8, max 20) |
66
- | `totalreclaw_forget` | `factId` |
67
- | `totalreclaw_pin` / `totalreclaw_unpin` | `factId`, optional `reason` |
68
- | `totalreclaw_retype` | `factId`, `newType` |
69
- | `totalreclaw_set_scope` | `factId`, `scope` |
70
- | `totalreclaw_export` | optional `format` (`json` / `markdown`) |
71
- | `totalreclaw_status` | (none) |
72
- | `totalreclaw_upgrade` | (none) |
73
- | `totalreclaw_migrate` | optional `confirm` (dry-run by default) |
74
- | `totalreclaw_import_from` / `totalreclaw_import_batch` | `source`, `file_path` or `content`, `dry_run` |
75
- | `totalreclaw_consolidate` | optional `dry_run` |
76
- | `totalreclaw_onboarding_start` / `totalreclaw_onboard` | (none) — returns CLI pointer |
77
- | `totalreclaw_pair` | optional `mode` (`generate` / `import`) — returns `{url, pin, qr_ascii, expires_at_ms}` |
78
-
79
- ## Taxonomy
80
-
81
- **Types:** `claim` (default) / `preference` / `directive` (reusable rule) / `commitment` (future intent) / `episode` (event) / `summary` (derived synthesis).
82
-
83
- **Scopes:** `work` / `personal` (default) / `health` / `family` / `creative` / `finance` / `misc`.
84
-
85
- ## If a tool fails
86
-
87
- - Tell the user plainly. Don't retry blindly.
88
- - "onboarding required" -> route per Setup-state above.
89
- - "No LLM available for auto-extraction" (startup only, v3.3.1+) -> provider key not reachable. Point at `~/.openclaw/agents/<agent>/agent/auth-profiles.json` or the `plugins.entries.totalreclaw.config.extraction.llm` override.
90
- - Silent extraction failures -> suggest `openclaw totalreclaw status` or check `~/.totalreclaw/billing-cache.json` for rate-limit signals.
91
-
92
- ## Minimum viable interaction pattern
93
-
94
- ```
95
- User: "I live in Porto and prefer PostgreSQL."
96
- -> totalreclaw_remember({text: "User lives in Porto", type: "claim"})
97
- -> totalreclaw_remember({text: "User prefers PostgreSQL over MySQL", type: "preference"})
98
- -> respond naturally, don't list what you just saved.
99
-
100
- User: "What do you remember about me?"
101
- -> totalreclaw_recall({query: "user facts preferences identity"})
102
- -> summarize returned facts in your reply.
103
-
104
- User: "Set me up for TotalReclaw."
105
- -> check ~/.totalreclaw/credentials.json. If missing:
106
- local: totalreclaw_onboarding_start (or totalreclaw_onboard)
107
- remote: totalreclaw_pair -> present URL + PIN + QR
108
- -> follow the tool's instructions. Never invent a phrase.
109
- ```
110
-
111
- ## What NOT to do
112
-
113
- - Do NOT write memories to `MEMORY.md` or any cleartext file — that defeats E2EE.
114
- - Do NOT call `totalreclaw_remember` for transient in-session context.
115
- - Do NOT paste recovery phrases or API keys into chat.
116
- - Do NOT run `npx @totalreclaw/mcp-server setup` — deprecated path that corrupts credentials.
83
+ `totalreclaw_pair` (ONLY setup path) · `_remember` · `_recall` · `_forget` · `_pin` · `_unpin` · `_retype` · `_set_scope` · `_export` · `_status` · `_upgrade` · `_migrate` · `_import_from` · `_import_batch` · `_consolidate` · `_onboarding_start` (pointer to local-terminal wizard, for users explicitly rejecting the browser flow) · `_report_qa_bug` (RC only).
package/config.ts CHANGED
@@ -105,6 +105,21 @@ export const CONFIG = {
105
105
  // for 15-min TTL windows; 0600 mode.
106
106
  pairSessionsPath: process.env.TOTALRECLAW_PAIR_SESSIONS_PATH || path.join(home, '.totalreclaw', 'pair-sessions.json'),
107
107
 
108
+ // 3.3.1-rc.11 — pair-flow transport selector. Mirrors the Python-side
109
+ // `TOTALRECLAW_PAIR_MODE` env (rc.10). `'relay'` (default) routes
110
+ // `totalreclaw_pair` through the universal-reachability WebSocket relay at
111
+ // `TOTALRECLAW_PAIR_RELAY_URL`. `'local'` preserves the rc.4–rc.10 loopback
112
+ // HTTP flow (the plugin serves `/plugin/totalreclaw/pair/*` via
113
+ // `pair-http.ts`). Air-gapped / self-hosted users can pin `'local'` here.
114
+ pairMode: (() => {
115
+ const v = (process.env.TOTALRECLAW_PAIR_MODE ?? '').trim().toLowerCase();
116
+ return v === 'local' ? 'local' : 'relay';
117
+ })() as 'relay' | 'local',
118
+ // 3.3.1-rc.11 — relay base URL for the WebSocket-brokered pair flow.
119
+ // `wss://` preferred; `https://` is rewritten in the remote-client.
120
+ pairRelayUrl: (process.env.TOTALRECLAW_PAIR_RELAY_URL
121
+ || 'wss://api-staging.totalreclaw.xyz').replace(/\/+$/, ''),
122
+
108
123
  // Chain — chainId is no longer user-configurable. It is auto-detected from
109
124
  // the relay billing response (free = Base Sepolia / 84532, Pro = Gnosis /
110
125
  // 100). The default here is used only before the first billing lookup
@@ -157,6 +172,48 @@ export const CONFIG = {
157
172
  cerebras: process.env.CEREBRAS_API_KEY || '',
158
173
  } as Record<string, string>,
159
174
 
175
+ // 3.3.1-rc.3: zai base-URL override. Read via a getter so tests can
176
+ // mutate `process.env.ZAI_BASE_URL` between calls — the value is NOT
177
+ // frozen at module load. Default is the coding endpoint; the rc.3
178
+ // auto-fallback flips to the standard endpoint on an "Insufficient
179
+ // balance" 429.
180
+ get zaiBaseUrl(): string {
181
+ const override = process.env.ZAI_BASE_URL;
182
+ if (override && override.trim()) return override.trim().replace(/\/+$/, '');
183
+ return 'https://api.z.ai/api/coding/paas/v4';
184
+ },
185
+
186
+ // 3.3.1-rc.3: retry budget for chatCompletion. Default 60s covers
187
+ // multi-minute upstream outages. Read as a plain value (not getter)
188
+ // so tests that patch env need to reload the module — but the default
189
+ // suffices for production.
190
+ llmRetryBudgetMs: (() => {
191
+ const raw = process.env.TOTALRECLAW_LLM_RETRY_BUDGET_MS;
192
+ const parsed = raw ? parseInt(raw, 10) : NaN;
193
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : 60_000;
194
+ })(),
195
+
196
+ // 3.3.1-rc.3: GitHub personal-access token used by the RC-gated
197
+ // `totalreclaw_report_qa_bug` tool. `TOTALRECLAW_QA_GITHUB_TOKEN` is
198
+ // the dedicated variable; `GITHUB_TOKEN` is a fallback for CI-style
199
+ // setups where the same token is shared across tools. Read via getter
200
+ // so operators can set the var after the process starts (e.g. via a
201
+ // dotenv reload) and the next tool call picks it up.
202
+ get qaGithubToken(): string {
203
+ return process.env.TOTALRECLAW_QA_GITHUB_TOKEN || process.env.GITHUB_TOKEN || '';
204
+ },
205
+
206
+ // 3.3.1-rc.14: optional target-repo override for the RC-gated QA
207
+ // bug-report tool. The `qa-bug-report` module enforces a
208
+ // "slug ends in `-internal`" rule on whatever is resolved here, so
209
+ // this override is only useful for forks / mirrors of the internal
210
+ // tracker. Leaving unset uses the production default
211
+ // (`p-diogo/totalreclaw-internal`). Read via getter so operators can
212
+ // flip the var at runtime.
213
+ get qaRepoOverride(): string {
214
+ return process.env.TOTALRECLAW_QA_REPO || '';
215
+ },
216
+
160
217
  // Paths
161
218
  home,
162
219
  billingCachePath: path.join(home, '.totalreclaw', 'billing-cache.json'),