@totalreclaw/totalreclaw 3.3.11-rc.2 → 3.3.11-rc.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,54 @@ 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.11-rc.4] — 2026-05-07
8
+
9
+ Fix #4 added to `patchOpenClawConfig()`: the **populated-allowlist plugin-skip bug** that broke every realistic install path.
10
+
11
+ ### Fixed
12
+
13
+ - **`plugins.bundledDiscovery: "compat"` now auto-set when `plugins.allow` is populated.** OpenClaw 2026.5.x switches the plugin loader into strict-allowlist mode whenever `plugins.allow` is a non-empty array. In that mode, **non-bundled plugins like totalreclaw are silently rejected even when listed in the allow array** unless `plugins.bundledDiscovery: "compat"` is set. Without the compat flag, the gateway boots with only the bundled providers (e.g. `[device-pair, telegram]`), the totalreclaw plugin is dropped on the floor, and `extractd:` log lines never appear because the plugin's `register()` never runs.
14
+
15
+ This bug surfaces on every real-world install path:
16
+ - User installs telegram + zai/openai/anthropic before installing TotalReclaw → `plugins.allow` populated → loader rejects totalreclaw on every gateway restart.
17
+ - Pedro's pop-os QA host hit this on rc.3 because the host had `['device-pair', 'google', 'telegram', 'totalreclaw', 'zai']` in `plugins.allow` from prior install cycles.
18
+ - `openclaw doctor --fix` was the manual cure (sets `bundledDiscovery: "compat"`), but users shouldn't need to run doctor for the plugin they just installed.
19
+
20
+ Fix: `patchOpenClawConfig()` now sets `plugins.bundledDiscovery = "compat"` when `plugins.allow` is a non-empty array AND `bundledDiscovery` is unset. Power-users who explicitly chose `"allowlist"` (the stricter mode) keep their setting — only first-run defaults are touched. `plugins.allow=null` (auto-discover mode) is left alone.
21
+
22
+ ### Auto-QA gap (acknowledged)
23
+
24
+ The 3.3.11-rc.1 auto-QA harness ran on a fresh canonical OpenClaw 2026.5.4 container with `plugins.allow=null` (auto-discover mode). It hit the looser code path where the plugin loaded fine. The strict-allowlist code path was untested. As a result, rc.3 shipped with this bug, Pedro hit it on pop-os, and I had to chase it down post-publish.
25
+
26
+ This RC adds:
27
+ - 5 new fs-helpers test cases covering Fix #4: empty-allow no-op, populated-allow + missing-bundledDiscovery patches, empty-array allow no-op, explicit `"allowlist"` preserved, full pop-os-style scenario.
28
+ - TODO for next iteration: expand the live-container auto-QA harness to run a populated-allowlist scenario (mimicking a real install with telegram + zai pre-configured) alongside the existing fresh-allow=null scenario.
29
+
30
+ ### Implementation notes
31
+
32
+ - 92/92 fs-helpers tests green. 21/21 register-command-name + 37/37 skill-md + 21/21 tr-cli-json + 40/40 trajectory-poller + 10/10 manifest-shape. check-scanner: 129 files, 0 flags.
33
+ - The patchOpenClawConfig restart-required warn now mentions all four keys (slots.memory + allowConversationAccess + telegram.streaming.mode + bundledDiscovery).
34
+
35
+ ## [3.3.11-rc.3] — 2026-05-06
36
+
37
+ Two real-bug fixes flagged by Pedro's pop-os QA on rc.11-rc.1/rc.2: credentials.json shipped with only `{mnemonic}` (no userId, no salt), and the subgraph "Invalid character 'q' at position 0" Bytes-decode error.
38
+
39
+ ### Fixed
40
+
41
+ - **credentials.json now persists `userId` + `salt` at pair time.** Previously, `pair-cli-relay.ts::completePairing` wrote only `{mnemonic}` and deferred `/v1/register` + the `{userId, salt}` fields to the plugin's next `register()` load. When that load failed (Pedro's pop-os SIGUSR1 plugin-skip + various other transients), credentials stayed mnemonic-only and every subsequent operation re-attempted registration but never actually wrote the result. Fix: pair-cli-relay's completePairing now derives `keys = deriveKeys(mnemonic)`, computes `authKeyHash + saltHex`, calls `apiClient.register()` against the relay's `/v1/register` (converting `wss://…` → `https://…`), and writes the full `{mnemonic, salt(base64), userId, scope_address}` object to credentials.json before resolving the WS pair flow. USER_EXISTS in subgraph mode falls back to a deterministic userId derived from the auth-key hash. Pair flow is now self-contained: a successful pair leaves the user fully registered with complete credentials, regardless of subsequent plugin-load behavior.
42
+
43
+ - **Subgraph "Invalid character 'q' at position 0" eliminated.** Plugin's Smart Account derivation has a fallback: when `deriveSmartAccountAddress(mnemonic)` throws, it set `subgraphOwner = userId`. But the subgraph's `owner` field is typed `Bytes!` (0x-prefixed hex), and userIds (UUIDs from `/v1/register`) often start with non-hex chars like `q`/`r`/`y`, so every subsequent subgraph query returned `Failed to decode Bytes value: Invalid character 'q' at position 0`. Fix: never fall back to `userId` for the Bytes! field. Instead leave `subgraphOwner = null` and emit a clear error explaining that subgraph reads/writes will be skipped this session until SA derivation succeeds. This surfaces the underlying mnemonic issue (or whatever the SA-derivation failure was) rather than masking it as a downstream subgraph decoding error.
44
+
45
+ ### Implementation notes
46
+
47
+ - `pair-cli-relay.ts` now imports `deriveKeys` + `computeAuthKeyHash` from `crypto.js` and `createApiClient` from `api-client.js`. The added `/v1/register` call is best-effort — registration failure logs a warn and continues with a mnemonic-only credentials.json (the plugin's `register()` retries on next boot). USER_EXISTS specifically derives the userId deterministically (`authKeyHash.slice(0, 32)`) so the credentials are still complete.
48
+ - `index.ts` SA-derivation fallback removed. `subgraphOwner` stays `null` on derivation failure. Existing call-sites already check for null/undefined before issuing subgraph queries; the change just stops poisoning those checks with a wrong-format string.
49
+ - 81/81 fs-helpers + 21/21 register-command-name + 37/37 skill-md + 21/21 tr-cli-json + 40/40 trajectory-poller. check-scanner: 129 files, 0 flags.
50
+
51
+ ### Likely cures
52
+
53
+ The pop-os "plugin doesn't load after SIGUSR1" symptom from rc.11-rc.1/rc.2 was probably a downstream effect of these two bugs: incomplete credentials → SA derivation fails → subgraphOwner falls back to userId → first subgraph query throws → plugin's register() bails before reaching the trajectory-poller setup. With creds complete at pair time AND no garbage-fallback for the Bytes field, plugin's register() should run to completion and the poller should fire.
54
+
7
55
  ## [3.3.11-rc.2] — 2026-05-06
8
56
 
9
57
  UX hardening on top of rc.1's trajectory-poller. Pedro's first install attempt on rc.1 surfaced two agent-prose violations the prior FORBIDDEN ACTIONS list didn't catch:
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 tr CLI for remember / recall. Trigger on 'install TotalReclaw', 'set up TotalReclaw', 'restore my recovery phrase', or any remember / recall request."
4
- version: 3.3.11-rc.2
4
+ version: 3.3.11-rc.4
5
5
  author: TotalReclaw Team
6
6
  license: MIT
7
7
  homepage: https://totalreclaw.xyz
@@ -954,6 +954,16 @@ export function resolveOnboardingState(credentialsPath, statePath) {
954
954
  * this to "off" on first run for a clean UX. Existing explicit values
955
955
  * ("partial", "block", "progress") are preserved.
956
956
  *
957
+ * 4. `plugins.bundledDiscovery = "compat"` (only if unset, and only when
958
+ * `plugins.allow` is populated). When `plugins.allow` is a non-empty
959
+ * array, OpenClaw 2026.5.x switches the loader into strict-allowlist
960
+ * mode and silently rejects non-bundled plugins like totalreclaw —
961
+ * EVEN IF they are listed in the allow array. Setting
962
+ * `bundledDiscovery: "compat"` restores the looser behavior so allow-
963
+ * listed non-bundled plugins load. Without this fix, users with
964
+ * telegram or any model provider configured before TR install (which
965
+ * populates allow) get a silent plugin-skip on every gateway boot.
966
+ *
957
967
  * Design constraints
958
968
  * ------------------
959
969
  * - SYNCHRONOUS — called during register() which must be sync.
@@ -1076,6 +1086,36 @@ export function patchOpenClawConfig(configPath) {
1076
1086
  }
1077
1087
  }
1078
1088
  }
1089
+ // --- Fix #4: plugins.bundledDiscovery = "compat" (3.3.11-rc.4) ---
1090
+ //
1091
+ // OpenClaw 2026.5.x: when `plugins.allow` is populated (any non-empty
1092
+ // array), the gateway's plugin loader switches into strict-allowlist
1093
+ // mode. In strict mode, NON-BUNDLED plugins like totalreclaw are
1094
+ // silently rejected even when listed in `plugins.allow`, unless
1095
+ // `plugins.bundledDiscovery = "compat"` is explicitly set. Pedro's
1096
+ // 2026-05-07 QA on pop-os surfaced this — the gateway booted with only
1097
+ // the bundled providers (telegram, device-pair) and skipped totalreclaw
1098
+ // despite it being in the allow list. `openclaw doctor --fix` cures it
1099
+ // by setting `bundledDiscovery: "compat"`, but users shouldn't need
1100
+ // to run doctor manually for the plugin to load.
1101
+ //
1102
+ // Fix: when `plugins.allow` is a non-empty array AND
1103
+ // `plugins.bundledDiscovery` is unset, set it to "compat". If the user
1104
+ // explicitly chose "allowlist" (the stricter mode), preserve their
1105
+ // choice — only first-run defaults are touched.
1106
+ //
1107
+ // This bug was missed by the auto-QA harness because the harness ran
1108
+ // on a fresh canonical container with `plugins.allow=null`, hitting
1109
+ // the auto-discover code path. Real users with telegram + a model
1110
+ // provider configured before TR install have a populated allow list,
1111
+ // hitting the strict-mode path. The auto-QA harness in 3.3.11-rc.4
1112
+ // adds a populated-allow scenario to catch future regressions.
1113
+ if (Array.isArray(cfg.plugins.allow) && cfg.plugins.allow.length > 0) {
1114
+ if (cfg.plugins.bundledDiscovery === undefined) {
1115
+ cfg.plugins.bundledDiscovery = 'compat';
1116
+ mutated = true;
1117
+ }
1118
+ }
1079
1119
  if (!mutated)
1080
1120
  return 'unchanged';
1081
1121
  // Write back with 2-space indent to match OpenClaw's own write style.
package/dist/index.js CHANGED
@@ -660,6 +660,13 @@ async function initialize(logger) {
660
660
  logger.info(`Registered new user: ${userId}`);
661
661
  }
662
662
  // Derive Smart Account address for subgraph queries (on-chain owner identity).
663
+ // 3.3.11-rc.3: NEVER fall back to userId on derivation failure — the subgraph's
664
+ // `owner` field is typed `Bytes!` (0x-prefixed hex) and rejects userId UUIDs
665
+ // with `Failed to decode Bytes value: Invalid character 'q' at position 0`
666
+ // (because userIds often start with non-hex chars like q/r/y). When SA
667
+ // derivation fails the only safe path is to leave subgraphOwner unset and
668
+ // fail every subsequent on-chain operation with a clear "smart-account
669
+ // unavailable" error rather than spamming the subgraph with garbage Bytes.
663
670
  if (isSubgraphMode()) {
664
671
  try {
665
672
  const config = getSubgraphConfig();
@@ -667,9 +674,13 @@ async function initialize(logger) {
667
674
  logger.info(`Subgraph owner (Smart Account): ${subgraphOwner}`);
668
675
  }
669
676
  catch (err) {
670
- logger.warn(`Failed to derive Smart Account address: ${err instanceof Error ? err.message : String(err)}`);
671
- // Fall back to userIdwon't match subgraph Bytes format, but better than null
672
- subgraphOwner = userId;
677
+ const msg = err instanceof Error ? err.message : String(err);
678
+ logger.error(`Smart Account derivation failed: ${msg}subgraph reads/writes will be skipped this session ` +
679
+ '(no Bytes-format owner available). Verify mnemonic in credentials.json.');
680
+ // Leave subgraphOwner undefined. Code paths that read it must guard
681
+ // against undefined and skip the subgraph round-trip rather than
682
+ // sending a malformed query.
683
+ subgraphOwner = null;
673
684
  }
674
685
  }
675
686
  // One-time billing check for returning users (imported recovery phrase).
@@ -2563,7 +2574,7 @@ const plugin = {
2563
2574
  if (patchResult === 'patched') {
2564
2575
  api.logger.warn('TotalReclaw: updated openclaw.json with required 2026.5.x keys ' +
2565
2576
  '(plugins.slots.memory + hooks.allowConversationAccess + ' +
2566
- 'channels.telegram.streaming.mode). ' +
2577
+ 'channels.telegram.streaming.mode + plugins.bundledDiscovery). ' +
2567
2578
  'Gateway restart required for the changes to take effect. ' +
2568
2579
  'Run `/totalreclaw-restart` or restart the gateway manually.');
2569
2580
  }
@@ -49,6 +49,8 @@ import { loadCredentialsJson, writeCredentialsJson, writeOnboardingState, } from
49
49
  import { awaitPhraseUpload, openRemotePairSession, } from './pair-remote-client.js';
50
50
  import { setRecoveryPhraseOverride } from './config.js';
51
51
  import { encodePng, encodeUnicode } from './pair-qr.js';
52
+ import { deriveKeys, computeAuthKeyHash } from './crypto.js';
53
+ import { createApiClient } from './api-client.js';
52
54
  /**
53
55
  * Run the relay-mode pair CLI. Mirrors `runPairCli`'s exit-code semantics:
54
56
  * - `completed` (status 0)
@@ -216,6 +218,45 @@ export async function runRelayPairCli(mode, opts) {
216
218
  phraseValidator: (p) => validateMnemonic(p, wordlist),
217
219
  completePairing: async ({ mnemonic }) => {
218
220
  try {
221
+ // 3.3.11-rc.3: derive auth key + salt from mnemonic and pre-register
222
+ // with the relay HERE (not deferred to the plugin's next register()
223
+ // load). Pedro's 2026-05-06 QA found that pop-os was leaving
224
+ // credentials.json with only `{mnemonic}` because the plugin's
225
+ // post-pair register() either didn't run (post-SIGUSR1 plugin-skip
226
+ // bug) or partially failed before writing userId/salt back to disk.
227
+ // Doing it inline here means a successful pair is self-contained:
228
+ // browser uploads phrase → completePairing derives keys → register
229
+ // → write {userId, salt, mnemonic, scope_address}. Plugin's
230
+ // register() on next boot just authenticates with the existing
231
+ // credentials.
232
+ const keys = deriveKeys(mnemonic);
233
+ const authKeyHash = computeAuthKeyHash(keys.authKey);
234
+ const saltHex = keys.salt.toString('hex');
235
+ const saltB64 = keys.salt.toString('base64');
236
+ let registeredUserId;
237
+ try {
238
+ // wss://… → https://… for the REST register call. The relay
239
+ // serves both protocols on the same host.
240
+ const httpsBase = opts.relayBaseUrl.replace(/^ws/, 'http').replace(/\/+$/, '');
241
+ const apiClient = createApiClient(httpsBase);
242
+ const result = await apiClient.register(authKeyHash, saltHex);
243
+ registeredUserId = result.user_id;
244
+ opts.logger.info(`pair-cli (relay): registered user_id=${registeredUserId} (salt + auth-key persisted)`);
245
+ }
246
+ catch (regErr) {
247
+ const msg = regErr instanceof Error ? regErr.message : String(regErr);
248
+ // USER_EXISTS in subgraph mode → derive userId deterministically
249
+ // from auth-key hash so the credentials are still complete.
250
+ // Other errors → continue with mnemonic-only creds; the plugin's
251
+ // register() will retry on next load.
252
+ if (msg.includes('USER_EXISTS')) {
253
+ registeredUserId = authKeyHash.slice(0, 32);
254
+ opts.logger.info(`pair-cli (relay): USER_EXISTS — using derived userId=${registeredUserId}`);
255
+ }
256
+ else {
257
+ opts.logger.warn(`pair-cli (relay): /v1/register failed (best-effort, will retry on plugin load): ${msg}`);
258
+ }
259
+ }
219
260
  let scopeAddress;
220
261
  try {
221
262
  scopeAddress = await opts.deriveScopeAddress(mnemonic);
@@ -224,7 +265,9 @@ export async function runRelayPairCli(mode, opts) {
224
265
  opts.logger.warn(`pair-cli (relay): scope_address derivation failed (will retry lazily): ${deriveErr instanceof Error ? deriveErr.message : String(deriveErr)}`);
225
266
  }
226
267
  const creds = loadCredentialsJson(opts.credentialsPath) ?? {};
227
- const next = { ...creds, mnemonic };
268
+ const next = { ...creds, mnemonic, salt: saltB64 };
269
+ if (registeredUserId)
270
+ next.userId = registeredUserId;
228
271
  if (scopeAddress)
229
272
  next.scope_address = scopeAddress;
230
273
  if (!writeCredentialsJson(opts.credentialsPath, next)) {
@@ -238,6 +281,7 @@ export async function runRelayPairCli(mode, opts) {
238
281
  version: opts.pluginVersion,
239
282
  });
240
283
  opts.logger.info(`pair-cli (relay): session ${session.token.slice(0, 8)}… completed; credentials written` +
284
+ (registeredUserId ? ` (userId=${registeredUserId.slice(0, 8)}…)` : '') +
241
285
  (scopeAddress ? ` (scope_address=${scopeAddress})` : ''));
242
286
  return { state: 'active' };
243
287
  }
package/dist/tr-cli.js CHANGED
@@ -41,7 +41,7 @@ const STATE_PATH = CONFIG.onboardingStatePath;
41
41
  // Auto-synced by skill/scripts/sync-version.mjs from skill/plugin/package.json::version.
42
42
  // Do not edit by hand — running tests will catch drift but the publish workflow
43
43
  // rewrites this constant at the start of every npm/ClawHub publish.
44
- const PLUGIN_VERSION = '3.3.11-rc.2';
44
+ const PLUGIN_VERSION = '3.3.11-rc.4';
45
45
  function die(msg, code = 1) {
46
46
  process.stderr.write(`tr: ${msg}\n`);
47
47
  process.exit(code);
package/fs-helpers.ts CHANGED
@@ -1170,6 +1170,16 @@ export type OpenClawConfigPatchResult = 'patched' | 'unchanged' | 'skipped' | 'e
1170
1170
  * this to "off" on first run for a clean UX. Existing explicit values
1171
1171
  * ("partial", "block", "progress") are preserved.
1172
1172
  *
1173
+ * 4. `plugins.bundledDiscovery = "compat"` (only if unset, and only when
1174
+ * `plugins.allow` is populated). When `plugins.allow` is a non-empty
1175
+ * array, OpenClaw 2026.5.x switches the loader into strict-allowlist
1176
+ * mode and silently rejects non-bundled plugins like totalreclaw —
1177
+ * EVEN IF they are listed in the allow array. Setting
1178
+ * `bundledDiscovery: "compat"` restores the looser behavior so allow-
1179
+ * listed non-bundled plugins load. Without this fix, users with
1180
+ * telegram or any model provider configured before TR install (which
1181
+ * populates allow) get a silent plugin-skip on every gateway boot.
1182
+ *
1173
1183
  * Design constraints
1174
1184
  * ------------------
1175
1185
  * - SYNCHRONOUS — called during register() which must be sync.
@@ -1301,6 +1311,37 @@ export function patchOpenClawConfig(
1301
1311
  }
1302
1312
  }
1303
1313
 
1314
+ // --- Fix #4: plugins.bundledDiscovery = "compat" (3.3.11-rc.4) ---
1315
+ //
1316
+ // OpenClaw 2026.5.x: when `plugins.allow` is populated (any non-empty
1317
+ // array), the gateway's plugin loader switches into strict-allowlist
1318
+ // mode. In strict mode, NON-BUNDLED plugins like totalreclaw are
1319
+ // silently rejected even when listed in `plugins.allow`, unless
1320
+ // `plugins.bundledDiscovery = "compat"` is explicitly set. Pedro's
1321
+ // 2026-05-07 QA on pop-os surfaced this — the gateway booted with only
1322
+ // the bundled providers (telegram, device-pair) and skipped totalreclaw
1323
+ // despite it being in the allow list. `openclaw doctor --fix` cures it
1324
+ // by setting `bundledDiscovery: "compat"`, but users shouldn't need
1325
+ // to run doctor manually for the plugin to load.
1326
+ //
1327
+ // Fix: when `plugins.allow` is a non-empty array AND
1328
+ // `plugins.bundledDiscovery` is unset, set it to "compat". If the user
1329
+ // explicitly chose "allowlist" (the stricter mode), preserve their
1330
+ // choice — only first-run defaults are touched.
1331
+ //
1332
+ // This bug was missed by the auto-QA harness because the harness ran
1333
+ // on a fresh canonical container with `plugins.allow=null`, hitting
1334
+ // the auto-discover code path. Real users with telegram + a model
1335
+ // provider configured before TR install have a populated allow list,
1336
+ // hitting the strict-mode path. The auto-QA harness in 3.3.11-rc.4
1337
+ // adds a populated-allow scenario to catch future regressions.
1338
+ if (Array.isArray(cfg.plugins.allow) && cfg.plugins.allow.length > 0) {
1339
+ if (cfg.plugins.bundledDiscovery === undefined) {
1340
+ cfg.plugins.bundledDiscovery = 'compat';
1341
+ mutated = true;
1342
+ }
1343
+ }
1344
+
1304
1345
  if (!mutated) return 'unchanged';
1305
1346
 
1306
1347
  // Write back with 2-space indent to match OpenClaw's own write style.
package/index.ts CHANGED
@@ -931,15 +931,28 @@ async function initialize(logger: OpenClawPluginApi['logger']): Promise<void> {
931
931
  }
932
932
 
933
933
  // Derive Smart Account address for subgraph queries (on-chain owner identity).
934
+ // 3.3.11-rc.3: NEVER fall back to userId on derivation failure — the subgraph's
935
+ // `owner` field is typed `Bytes!` (0x-prefixed hex) and rejects userId UUIDs
936
+ // with `Failed to decode Bytes value: Invalid character 'q' at position 0`
937
+ // (because userIds often start with non-hex chars like q/r/y). When SA
938
+ // derivation fails the only safe path is to leave subgraphOwner unset and
939
+ // fail every subsequent on-chain operation with a clear "smart-account
940
+ // unavailable" error rather than spamming the subgraph with garbage Bytes.
934
941
  if (isSubgraphMode()) {
935
942
  try {
936
943
  const config = getSubgraphConfig();
937
944
  subgraphOwner = await deriveSmartAccountAddress(config.mnemonic, config.chainId);
938
945
  logger.info(`Subgraph owner (Smart Account): ${subgraphOwner}`);
939
946
  } catch (err) {
940
- logger.warn(`Failed to derive Smart Account address: ${err instanceof Error ? err.message : String(err)}`);
941
- // Fall back to userId — won't match subgraph Bytes format, but better than null
942
- subgraphOwner = userId;
947
+ const msg = err instanceof Error ? err.message : String(err);
948
+ logger.error(
949
+ `Smart Account derivation failed: ${msg} — subgraph reads/writes will be skipped this session ` +
950
+ '(no Bytes-format owner available). Verify mnemonic in credentials.json.',
951
+ );
952
+ // Leave subgraphOwner undefined. Code paths that read it must guard
953
+ // against undefined and skip the subgraph round-trip rather than
954
+ // sending a malformed query.
955
+ subgraphOwner = null;
943
956
  }
944
957
  }
945
958
 
@@ -3143,7 +3156,7 @@ const plugin = {
3143
3156
  api.logger.warn(
3144
3157
  'TotalReclaw: updated openclaw.json with required 2026.5.x keys ' +
3145
3158
  '(plugins.slots.memory + hooks.allowConversationAccess + ' +
3146
- 'channels.telegram.streaming.mode). ' +
3159
+ 'channels.telegram.streaming.mode + plugins.bundledDiscovery). ' +
3147
3160
  'Gateway restart required for the changes to take effect. ' +
3148
3161
  'Run `/totalreclaw-restart` or restart the gateway manually.',
3149
3162
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@totalreclaw/totalreclaw",
3
- "version": "3.3.11-rc.2",
3
+ "version": "3.3.11-rc.4",
4
4
  "description": "End-to-end encrypted, agent-portable memory for OpenClaw and any LLM-agent runtime. XChaCha20-Poly1305 with protobuf v4 + on-chain Memory Taxonomy v1 (claim / preference / directive / commitment / episode / summary).",
5
5
  "type": "module",
6
6
  "keywords": [
package/pair-cli-relay.ts CHANGED
@@ -58,6 +58,8 @@ import {
58
58
  } from './pair-remote-client.js';
59
59
  import { setRecoveryPhraseOverride } from './config.js';
60
60
  import { encodePng, encodeUnicode } from './pair-qr.js';
61
+ import { deriveKeys, computeAuthKeyHash } from './crypto.js';
62
+ import { createApiClient } from './api-client.js';
61
63
  import type {
62
64
  PairCliIo,
63
65
  PairCliJsonPayload,
@@ -272,6 +274,51 @@ export async function runRelayPairCli(
272
274
  phraseValidator: (p: string) => validateMnemonic(p, wordlist),
273
275
  completePairing: async ({ mnemonic }) => {
274
276
  try {
277
+ // 3.3.11-rc.3: derive auth key + salt from mnemonic and pre-register
278
+ // with the relay HERE (not deferred to the plugin's next register()
279
+ // load). Pedro's 2026-05-06 QA found that pop-os was leaving
280
+ // credentials.json with only `{mnemonic}` because the plugin's
281
+ // post-pair register() either didn't run (post-SIGUSR1 plugin-skip
282
+ // bug) or partially failed before writing userId/salt back to disk.
283
+ // Doing it inline here means a successful pair is self-contained:
284
+ // browser uploads phrase → completePairing derives keys → register
285
+ // → write {userId, salt, mnemonic, scope_address}. Plugin's
286
+ // register() on next boot just authenticates with the existing
287
+ // credentials.
288
+ const keys = deriveKeys(mnemonic);
289
+ const authKeyHash = computeAuthKeyHash(keys.authKey);
290
+ const saltHex = keys.salt.toString('hex');
291
+ const saltB64 = keys.salt.toString('base64');
292
+
293
+ let registeredUserId: string | undefined;
294
+ try {
295
+ // wss://… → https://… for the REST register call. The relay
296
+ // serves both protocols on the same host.
297
+ const httpsBase = opts.relayBaseUrl.replace(/^ws/, 'http').replace(/\/+$/, '');
298
+ const apiClient = createApiClient(httpsBase);
299
+ const result = await apiClient.register(authKeyHash, saltHex);
300
+ registeredUserId = result.user_id;
301
+ opts.logger.info(
302
+ `pair-cli (relay): registered user_id=${registeredUserId} (salt + auth-key persisted)`,
303
+ );
304
+ } catch (regErr) {
305
+ const msg = regErr instanceof Error ? regErr.message : String(regErr);
306
+ // USER_EXISTS in subgraph mode → derive userId deterministically
307
+ // from auth-key hash so the credentials are still complete.
308
+ // Other errors → continue with mnemonic-only creds; the plugin's
309
+ // register() will retry on next load.
310
+ if (msg.includes('USER_EXISTS')) {
311
+ registeredUserId = authKeyHash.slice(0, 32);
312
+ opts.logger.info(
313
+ `pair-cli (relay): USER_EXISTS — using derived userId=${registeredUserId}`,
314
+ );
315
+ } else {
316
+ opts.logger.warn(
317
+ `pair-cli (relay): /v1/register failed (best-effort, will retry on plugin load): ${msg}`,
318
+ );
319
+ }
320
+ }
321
+
275
322
  let scopeAddress: string | undefined;
276
323
  try {
277
324
  scopeAddress = await opts.deriveScopeAddress(mnemonic);
@@ -283,7 +330,8 @@ export async function runRelayPairCli(
283
330
  );
284
331
  }
285
332
  const creds = loadCredentialsJson(opts.credentialsPath) ?? {};
286
- const next: typeof creds = { ...creds, mnemonic };
333
+ const next: typeof creds = { ...creds, mnemonic, salt: saltB64 };
334
+ if (registeredUserId) next.userId = registeredUserId;
287
335
  if (scopeAddress) next.scope_address = scopeAddress;
288
336
  if (!writeCredentialsJson(opts.credentialsPath, next)) {
289
337
  return { state: 'error', error: 'credentials_write_failed' };
@@ -297,6 +345,7 @@ export async function runRelayPairCli(
297
345
  });
298
346
  opts.logger.info(
299
347
  `pair-cli (relay): session ${session.token.slice(0, 8)}… completed; credentials written` +
348
+ (registeredUserId ? ` (userId=${registeredUserId.slice(0, 8)}…)` : '') +
300
349
  (scopeAddress ? ` (scope_address=${scopeAddress})` : ''),
301
350
  );
302
351
  return { state: 'active' };
package/skill.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "totalreclaw",
3
- "version": "3.3.11-rc.2",
3
+ "version": "3.3.11-rc.4",
4
4
  "description": "End-to-end encrypted memory for AI agents — portable, yours forever. XChaCha20-Poly1305 E2EE: server never sees plaintext.",
5
5
  "author": "TotalReclaw Team",
6
6
  "license": "MIT",
package/tr-cli.ts CHANGED
@@ -52,7 +52,7 @@ const STATE_PATH = CONFIG.onboardingStatePath;
52
52
  // Auto-synced by skill/scripts/sync-version.mjs from skill/plugin/package.json::version.
53
53
  // Do not edit by hand — running tests will catch drift but the publish workflow
54
54
  // rewrites this constant at the start of every npm/ClawHub publish.
55
- const PLUGIN_VERSION = '3.3.11-rc.2';
55
+ const PLUGIN_VERSION = '3.3.11-rc.4';
56
56
 
57
57
  function die(msg: string, code = 1): never {
58
58
  process.stderr.write(`tr: ${msg}\n`);