@totalreclaw/totalreclaw 3.3.0-rc.4 → 3.3.0-rc.5

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 (3) hide show
  1. package/CHANGELOG.md +98 -0
  2. package/index.ts +61 -55
  3. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -4,6 +4,104 @@ 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.0-rc.5] — 2026-04-20
8
+
9
+ Fifth release candidate for 3.3.0. Single ship-stopper fix for rc.4's
10
+ QR-pairing flow, root-caused by the auto-QA run against rc.4 artifacts
11
+ (report: `docs/notes/QA-plugin-3.3.0-rc.4-20260420-1517.md` in
12
+ `totalreclaw-internal`, thread at `totalreclaw-internal#21` comment
13
+ 4281568050). rc.2 (scanner), rc.3 (auth literal path), and rc.4 (auth
14
+ `'plugin'` literal + `ensureSessionsFileDir` mkdir before lock) fixes are
15
+ all preserved. No protocol / on-chain changes vs 3.3.0.
16
+
17
+ ### Fixed
18
+
19
+ - **`skill/plugin/index.ts` — register pair HTTP routes synchronously
20
+ (remove async IIFE)**. rc.2–rc.4 wrapped the 4 `api.registerHttpRoute`
21
+ calls in a fire-and-forget `(async () => { ... })()` block whose three
22
+ `await import(...)` calls (`./pair-http.js`, `@scure/bip39`, and
23
+ `@scure/bip39/wordlists/english.js`) settled one microtask AFTER the
24
+ SDK loader had already called `register()` and frozen the plugin's
25
+ HTTP-route registry. The 4 post-activation pushes landed on the
26
+ dispatcher's "inactive" copy and never reached the live router;
27
+ `openclaw plugins inspect totalreclaw --json | jq .httpRouteCount`
28
+ returned `0` on rc.4 despite both the `auth: 'plugin'` literal (rc.4)
29
+ and the `ensureSessionsFileDir` mkdir (rc.4) being correct. rc.5:
30
+
31
+ 1. `buildPairRoutes`, `validateMnemonic`, and `wordlist` are now
32
+ **static top-of-file imports** (alongside the existing
33
+ `onboarding-cli.ts` / `generate-mnemonic.ts` static imports of the
34
+ same modules — no new deps, no circular-dep risk).
35
+ 2. `writeOnboardingState` is added to the existing static
36
+ `./fs-helpers.js` import (it was the only dynamic import inside
37
+ the `completePairing` callback).
38
+ 3. The async IIFE is deleted. `buildPairRoutes(...)` and the 4
39
+ `api.registerHttpRoute({...})` calls are now in the synchronous
40
+ body of `register(api)`, inside the existing
41
+ `if (typeof api.registerHttpRoute === 'function')` guard. The
42
+ `else` branch and warning are unchanged. The post-registration
43
+ info log now reads `'registered 4 QR-pairing HTTP routes
44
+ synchronously'` for clearer debug output.
45
+ 4. `completePairing` remains `async` (it does disk I/O) — that is
46
+ fine because `registerHttpRoute` accepts async handlers. Only the
47
+ REGISTRATION had to be synchronous; the handler itself can
48
+ defer-to-microtask freely at runtime.
49
+
50
+ Scanner: static imports don't trigger any rule that dynamic imports
51
+ don't already trigger (verified via `node skill/scripts/check-scanner.mjs`,
52
+ 0 flags, 72 files scanned).
53
+
54
+ **Before (rc.4):**
55
+ ```ts
56
+ if (typeof api.registerHttpRoute === 'function') {
57
+ (async () => {
58
+ try {
59
+ const { buildPairRoutes } = await import('./pair-http.js');
60
+ const { validateMnemonic } = await import('@scure/bip39');
61
+ const { wordlist } = await import('@scure/bip39/wordlists/english.js');
62
+ const bundle = buildPairRoutes({ /* ... */ });
63
+ api.registerHttpRoute!({ path: bundle.finishPath, /*...*/, auth: 'plugin' });
64
+ api.registerHttpRoute!({ path: bundle.startPath, /*...*/, auth: 'plugin' });
65
+ api.registerHttpRoute!({ path: bundle.respondPath,/*...*/, auth: 'plugin' });
66
+ api.registerHttpRoute!({ path: bundle.statusPath, /*...*/, auth: 'plugin' });
67
+ // ^^ these 4 pushes happen AFTER register() has returned + the
68
+ // SDK loader has already activated the (empty) route registry.
69
+ } catch (err) { /* ... */ }
70
+ })();
71
+ }
72
+ ```
73
+
74
+ **After (rc.5):**
75
+ ```ts
76
+ // top of file
77
+ import { buildPairRoutes } from './pair-http.js';
78
+ import { validateMnemonic } from '@scure/bip39';
79
+ import { wordlist } from '@scure/bip39/wordlists/english.js';
80
+ // ... fs-helpers import now also includes writeOnboardingState
81
+
82
+ // inside register(api)
83
+ if (typeof api.registerHttpRoute === 'function') {
84
+ const bundle = buildPairRoutes({ /* ... */ });
85
+ api.registerHttpRoute!({ path: bundle.finishPath, /*...*/, auth: 'plugin' });
86
+ api.registerHttpRoute!({ path: bundle.startPath, /*...*/, auth: 'plugin' });
87
+ api.registerHttpRoute!({ path: bundle.respondPath, /*...*/, auth: 'plugin' });
88
+ api.registerHttpRoute!({ path: bundle.statusPath, /*...*/, auth: 'plugin' });
89
+ // ^^ these 4 pushes happen synchronously BEFORE register() returns,
90
+ // i.e. BEFORE the SDK loader activates the registry.
91
+ api.logger.info('TotalReclaw: registered 4 QR-pairing HTTP routes synchronously');
92
+ }
93
+ ```
94
+
95
+ - **`skill/plugin/pair-http-route-registration.test.ts` — rc.5 regression
96
+ guard**. The existing SIMULATION suite (27 assertions covering the 4
97
+ routes' `auth` literal, path shape, handler type) is preserved. Added
98
+ a new SYNCHRONY suite (14 assertions) that invokes `plugin.register(mockApi)`
99
+ with a minimal mocked OpenClaw API and asserts `mockApi.registerHttpRoute`
100
+ has been called 4 times IMMEDIATELY after `register()` returns — no
101
+ `await`, no tick wait. This assertion would fail under the rc.4 async-IIFE
102
+ implementation and guards against any future refactor that re-introduces
103
+ an async boundary at the registration site. Total: 41/41 passing.
104
+
7
105
  ## [3.3.0-rc.4] — 2026-04-20
8
106
 
9
107
  Fourth release candidate for 3.3.0. Two independent ship-stopper fixes for
package/index.ts CHANGED
@@ -133,10 +133,14 @@ import {
133
133
  isRunningInDocker,
134
134
  deleteFileIfExists,
135
135
  resolveOnboardingState,
136
+ writeOnboardingState,
136
137
  type OnboardingState,
137
138
  } from './fs-helpers.js';
138
139
  import { decideToolGate, isGatedToolName } from './tool-gating.js';
139
140
  import { detectFirstRun, buildWelcomePrepend, type GatewayMode } from './first-run.js';
141
+ import { buildPairRoutes } from './pair-http.js';
142
+ import { validateMnemonic } from '@scure/bip39';
143
+ import { wordlist } from '@scure/bip39/wordlists/english.js';
140
144
  import crypto from 'node:crypto';
141
145
 
142
146
  // ---------------------------------------------------------------------------
@@ -2711,62 +2715,64 @@ const plugin = {
2711
2715
  // encrypted mnemonic payload, and expose a status polled by the
2712
2716
  // CLI. See pair-http.ts and the 2026-04-20 design doc.
2713
2717
  if (typeof api.registerHttpRoute === 'function') {
2714
- (async () => {
2715
- try {
2716
- const { buildPairRoutes } = await import('./pair-http.js');
2717
- const { validateMnemonic } = await import('@scure/bip39');
2718
- const { wordlist } = await import('@scure/bip39/wordlists/english.js');
2719
- const bundle = buildPairRoutes({
2720
- sessionsPath: CONFIG.pairSessionsPath,
2721
- apiBase: '/plugin/totalreclaw/pair',
2722
- logger: api.logger,
2723
- validateMnemonic: (p) => validateMnemonic(p, wordlist),
2724
- completePairing: async ({ mnemonic }) => {
2725
- // Write credentials.json + flip state to 'active' via
2726
- // fs-helpers. This centralizes disk I/O off the
2727
- // pair-http surface (scanner isolation).
2728
- const creds = loadCredentialsJson(CREDENTIALS_PATH) ?? {};
2729
- const next = { ...creds, mnemonic };
2730
- if (!writeCredentialsJson(CREDENTIALS_PATH, next)) {
2731
- return { state: 'error', error: 'credentials_write_failed' };
2732
- }
2733
- // Hot-reload: update the runtime override so existing
2734
- // in-memory state picks up the new phrase without a
2735
- // process restart.
2736
- setRecoveryPhraseOverride(mnemonic);
2737
- // Flip onboarding state. writeOnboardingState is in
2738
- // fs-helpers; dynamic import to keep it out of any
2739
- // potential scanner collision surface in this file.
2740
- const { writeOnboardingState } = await import('./fs-helpers.js');
2741
- writeOnboardingState(CONFIG.onboardingStatePath, {
2742
- onboardingState: 'active',
2743
- createdBy: 'generate',
2744
- credentialsCreatedAt: new Date().toISOString(),
2745
- version: '3.3.0',
2746
- });
2747
- return { state: 'active' };
2748
- },
2718
+ // rc.5 the 4 `registerHttpRoute` calls MUST happen synchronously inside
2719
+ // `register(api)` because the SDK loader freezes the plugin's HTTP-route
2720
+ // registry as soon as `register()` returns. In rc.2–rc.4 this block was
2721
+ // wrapped in a fire-and-forget async IIFE whose `await import(...)`
2722
+ // settled one microtask AFTER the loader had already activated the
2723
+ // (empty) route list — the post-activation pushes landed on the
2724
+ // dispatcher's "inactive" copy and `openclaw plugins inspect
2725
+ // totalreclaw --json | jq .httpRouteCount` returned 0. See
2726
+ // `docs/notes/QA-plugin-3.3.0-rc.4-20260420-1517.md` (internal#21).
2727
+ // Moving `buildPairRoutes`, `@scure/bip39`, and `fs-helpers`
2728
+ // `writeOnboardingState` to static top-of-file imports keeps the
2729
+ // registration site synchronous and makes the call order deterministic.
2730
+ // `completePairing` remains async (it does disk I/O) that is fine,
2731
+ // since `registerHttpRoute` accepts async handlers; only the
2732
+ // REGISTRATION must be synchronous.
2733
+ const bundle = buildPairRoutes({
2734
+ sessionsPath: CONFIG.pairSessionsPath,
2735
+ apiBase: '/plugin/totalreclaw/pair',
2736
+ logger: api.logger,
2737
+ validateMnemonic: (p) => validateMnemonic(p, wordlist),
2738
+ completePairing: async ({ mnemonic }) => {
2739
+ // Write credentials.json + flip state to 'active' via
2740
+ // fs-helpers. This centralizes disk I/O off the
2741
+ // pair-http surface (scanner isolation).
2742
+ const creds = loadCredentialsJson(CREDENTIALS_PATH) ?? {};
2743
+ const next = { ...creds, mnemonic };
2744
+ if (!writeCredentialsJson(CREDENTIALS_PATH, next)) {
2745
+ return { state: 'error', error: 'credentials_write_failed' };
2746
+ }
2747
+ // Hot-reload: update the runtime override so existing
2748
+ // in-memory state picks up the new phrase without a
2749
+ // process restart.
2750
+ setRecoveryPhraseOverride(mnemonic);
2751
+ // Flip onboarding state.
2752
+ writeOnboardingState(CONFIG.onboardingStatePath, {
2753
+ onboardingState: 'active',
2754
+ createdBy: 'generate',
2755
+ credentialsCreatedAt: new Date().toISOString(),
2756
+ version: '3.3.0',
2749
2757
  });
2750
- // auth: 'plugin' — the 4 pair routes are reached from the operator's
2751
- // phone/laptop browser, which has no gateway bearer token. The plugin
2752
- // authenticates each request itself via (a) the in-memory pair session
2753
- // (sid + secondaryCode + single-use consumption), (b) ECDH + AEAD for
2754
- // the encrypted mnemonic payload. See gateway-cli dist
2755
- // `matchedPluginRoutesRequireGatewayAuth` / `enforcePluginRouteGatewayAuth`
2756
- // routes with `auth: 'gateway'` require a bearer token and 401 any
2757
- // browser caller, which is the wrong semantic for QR-pair. rc.3
2758
- // shipped `auth: 'gateway'` and the QA agent confirmed the routes
2759
- // were unreachable from a browser (QA-plugin-3.3.0-rc.3 report).
2760
- api.registerHttpRoute!({ path: bundle.finishPath, handler: bundle.handlers.finish, auth: 'plugin' });
2761
- api.registerHttpRoute!({ path: bundle.startPath, handler: bundle.handlers.start, auth: 'plugin' });
2762
- api.registerHttpRoute!({ path: bundle.respondPath, handler: bundle.handlers.respond, auth: 'plugin' });
2763
- api.registerHttpRoute!({ path: bundle.statusPath, handler: bundle.handlers.status, auth: 'plugin' });
2764
- api.logger.info('TotalReclaw: registered 4 QR-pairing HTTP routes');
2765
- } catch (err) {
2766
- const msg = err instanceof Error ? err.message : String(err);
2767
- api.logger.error(`TotalReclaw: failed to register pairing HTTP routes: ${msg}`);
2768
- }
2769
- })();
2758
+ return { state: 'active' };
2759
+ },
2760
+ });
2761
+ // auth: 'plugin' the 4 pair routes are reached from the operator's
2762
+ // phone/laptop browser, which has no gateway bearer token. The plugin
2763
+ // authenticates each request itself via (a) the in-memory pair session
2764
+ // (sid + secondaryCode + single-use consumption), (b) ECDH + AEAD for
2765
+ // the encrypted mnemonic payload. See gateway-cli dist
2766
+ // `matchedPluginRoutesRequireGatewayAuth` / `enforcePluginRouteGatewayAuth`
2767
+ // routes with `auth: 'gateway'` require a bearer token and 401 any
2768
+ // browser caller, which is the wrong semantic for QR-pair. rc.3
2769
+ // shipped `auth: 'gateway'` and the QA agent confirmed the routes
2770
+ // were unreachable from a browser (QA-plugin-3.3.0-rc.3 report).
2771
+ api.registerHttpRoute!({ path: bundle.finishPath, handler: bundle.handlers.finish, auth: 'plugin' });
2772
+ api.registerHttpRoute!({ path: bundle.startPath, handler: bundle.handlers.start, auth: 'plugin' });
2773
+ api.registerHttpRoute!({ path: bundle.respondPath, handler: bundle.handlers.respond, auth: 'plugin' });
2774
+ api.registerHttpRoute!({ path: bundle.statusPath, handler: bundle.handlers.status, auth: 'plugin' });
2775
+ api.logger.info('TotalReclaw: registered 4 QR-pairing HTTP routes synchronously');
2770
2776
  } else {
2771
2777
  api.logger.warn(
2772
2778
  'api.registerHttpRoute is unavailable on this OpenClaw version — /totalreclaw pair will not work. ' +
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@totalreclaw/totalreclaw",
3
- "version": "3.3.0-rc.4",
3
+ "version": "3.3.0-rc.5",
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": [