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

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,166 @@ 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.6] — 2026-04-20
8
+
9
+ Sixth release candidate for 3.3.0. Single manifest-only fix for the
10
+ root cause of every rc.2–rc.5 HTTP-route failure: the gateway's startup
11
+ registry pin silently excluded our plugin because the manifest declared
12
+ `kind: "memory"`. All prior fixes (scanner, auth literal, sync
13
+ registration) are preserved. No code changes in `index.ts` or any other
14
+ source file. No protocol / on-chain changes vs 3.3.0.
15
+
16
+ See research report: `docs/notes/RESEARCH-openclaw-http-route-plumbing-20260420-1608.md`
17
+ in `totalreclaw-internal`, and `totalreclaw-internal#21` comment 4282038854.
18
+
19
+ ### Fixed
20
+
21
+ - **`skill/plugin/openclaw.plugin.json` — drop `"kind": "memory"`**.
22
+ `resolveGatewayStartupPluginIds` (channel-plugin-ids-*.js) excludes
23
+ plugins with `kind: "memory"` from the gateway's startup set unless
24
+ they also declare a configured channel. Because TotalReclaw has no
25
+ channel, `loadGatewayPlugins` (gateway-cli-*.js:19807–19813) took an
26
+ empty-list early return, passed an empty HTTP route registry to
27
+ `createGatewayRuntimeState`, and `pinActivePluginHttpRouteRegistry`
28
+ locked that empty registry. The plugin still loaded later via the
29
+ memory-backend path and pushed its 4 routes into a NEW registry, but
30
+ `setActivePluginRegistry`'s `syncTrackedSurface` early-returns when
31
+ `surface.pinned === true` (runtime-*.js:60–67). Net: every `/pair/*`
32
+ HTTP route returned 404/SPA-fallthrough at runtime despite
33
+ `httpRouteCount: 4` in `openclaw plugins inspect`.
34
+
35
+ Removing `"kind": "memory"` from the manifest restores startup
36
+ inclusion via the sidecar path (`hasRuntimeContractSurface` becomes
37
+ false), so the gateway pins a registry that already contains the 4
38
+ routes.
39
+
40
+ **The JS plugin definition (`index.ts` line ~2626) still returns
41
+ `kind: 'memory' as const`.** The OpenClaw loader re-merges the JS
42
+ definition into `record.kind` at line 2090, so memory-slot matching
43
+ via `config.slots.memory === "totalreclaw"` still works and all
44
+ memory-gated behavior is unchanged.
45
+
46
+ This is a workaround for an upstream OpenClaw bug — see "Upstream
47
+ OpenClaw bug" section in the linked PR for the bug report draft and
48
+ proposed proper fixes.
49
+
50
+ ### Added
51
+
52
+ - **`skill/plugin/manifest-shape.test.ts`** — dual-assertion regression
53
+ guard documenting the intentional manifest/JS asymmetry:
54
+ 1. `openclaw.plugin.json` does NOT contain `"kind": "memory"` (guard
55
+ against accidentally re-adding).
56
+ 2. The exported plugin definition in `index.ts` DOES have
57
+ `kind: 'memory' as const` (guard against accidental removal from
58
+ JS, which would break memory-slot matching).
59
+
60
+ ### Unchanged
61
+
62
+ No changes to: `index.ts`, `pair-http.ts`, or any other source file.
63
+ Scanner-sim: 0 flags. Tarball contents: same files; diff is
64
+ `openclaw.plugin.json` (1 line removed) + `package.json` version bump +
65
+ `CHANGELOG.md`.
66
+
67
+ ---
68
+
69
+ ## [3.3.0-rc.5] — 2026-04-20
70
+
71
+ Fifth release candidate for 3.3.0. Single ship-stopper fix for rc.4's
72
+ QR-pairing flow, root-caused by the auto-QA run against rc.4 artifacts
73
+ (report: `docs/notes/QA-plugin-3.3.0-rc.4-20260420-1517.md` in
74
+ `totalreclaw-internal`, thread at `totalreclaw-internal#21` comment
75
+ 4281568050). rc.2 (scanner), rc.3 (auth literal path), and rc.4 (auth
76
+ `'plugin'` literal + `ensureSessionsFileDir` mkdir before lock) fixes are
77
+ all preserved. No protocol / on-chain changes vs 3.3.0.
78
+
79
+ ### Fixed
80
+
81
+ - **`skill/plugin/index.ts` — register pair HTTP routes synchronously
82
+ (remove async IIFE)**. rc.2–rc.4 wrapped the 4 `api.registerHttpRoute`
83
+ calls in a fire-and-forget `(async () => { ... })()` block whose three
84
+ `await import(...)` calls (`./pair-http.js`, `@scure/bip39`, and
85
+ `@scure/bip39/wordlists/english.js`) settled one microtask AFTER the
86
+ SDK loader had already called `register()` and frozen the plugin's
87
+ HTTP-route registry. The 4 post-activation pushes landed on the
88
+ dispatcher's "inactive" copy and never reached the live router;
89
+ `openclaw plugins inspect totalreclaw --json | jq .httpRouteCount`
90
+ returned `0` on rc.4 despite both the `auth: 'plugin'` literal (rc.4)
91
+ and the `ensureSessionsFileDir` mkdir (rc.4) being correct. rc.5:
92
+
93
+ 1. `buildPairRoutes`, `validateMnemonic`, and `wordlist` are now
94
+ **static top-of-file imports** (alongside the existing
95
+ `onboarding-cli.ts` / `generate-mnemonic.ts` static imports of the
96
+ same modules — no new deps, no circular-dep risk).
97
+ 2. `writeOnboardingState` is added to the existing static
98
+ `./fs-helpers.js` import (it was the only dynamic import inside
99
+ the `completePairing` callback).
100
+ 3. The async IIFE is deleted. `buildPairRoutes(...)` and the 4
101
+ `api.registerHttpRoute({...})` calls are now in the synchronous
102
+ body of `register(api)`, inside the existing
103
+ `if (typeof api.registerHttpRoute === 'function')` guard. The
104
+ `else` branch and warning are unchanged. The post-registration
105
+ info log now reads `'registered 4 QR-pairing HTTP routes
106
+ synchronously'` for clearer debug output.
107
+ 4. `completePairing` remains `async` (it does disk I/O) — that is
108
+ fine because `registerHttpRoute` accepts async handlers. Only the
109
+ REGISTRATION had to be synchronous; the handler itself can
110
+ defer-to-microtask freely at runtime.
111
+
112
+ Scanner: static imports don't trigger any rule that dynamic imports
113
+ don't already trigger (verified via `node skill/scripts/check-scanner.mjs`,
114
+ 0 flags, 72 files scanned).
115
+
116
+ **Before (rc.4):**
117
+ ```ts
118
+ if (typeof api.registerHttpRoute === 'function') {
119
+ (async () => {
120
+ try {
121
+ const { buildPairRoutes } = await import('./pair-http.js');
122
+ const { validateMnemonic } = await import('@scure/bip39');
123
+ const { wordlist } = await import('@scure/bip39/wordlists/english.js');
124
+ const bundle = buildPairRoutes({ /* ... */ });
125
+ api.registerHttpRoute!({ path: bundle.finishPath, /*...*/, auth: 'plugin' });
126
+ api.registerHttpRoute!({ path: bundle.startPath, /*...*/, auth: 'plugin' });
127
+ api.registerHttpRoute!({ path: bundle.respondPath,/*...*/, auth: 'plugin' });
128
+ api.registerHttpRoute!({ path: bundle.statusPath, /*...*/, auth: 'plugin' });
129
+ // ^^ these 4 pushes happen AFTER register() has returned + the
130
+ // SDK loader has already activated the (empty) route registry.
131
+ } catch (err) { /* ... */ }
132
+ })();
133
+ }
134
+ ```
135
+
136
+ **After (rc.5):**
137
+ ```ts
138
+ // top of file
139
+ import { buildPairRoutes } from './pair-http.js';
140
+ import { validateMnemonic } from '@scure/bip39';
141
+ import { wordlist } from '@scure/bip39/wordlists/english.js';
142
+ // ... fs-helpers import now also includes writeOnboardingState
143
+
144
+ // inside register(api)
145
+ if (typeof api.registerHttpRoute === 'function') {
146
+ const bundle = buildPairRoutes({ /* ... */ });
147
+ api.registerHttpRoute!({ path: bundle.finishPath, /*...*/, auth: 'plugin' });
148
+ api.registerHttpRoute!({ path: bundle.startPath, /*...*/, auth: 'plugin' });
149
+ api.registerHttpRoute!({ path: bundle.respondPath, /*...*/, auth: 'plugin' });
150
+ api.registerHttpRoute!({ path: bundle.statusPath, /*...*/, auth: 'plugin' });
151
+ // ^^ these 4 pushes happen synchronously BEFORE register() returns,
152
+ // i.e. BEFORE the SDK loader activates the registry.
153
+ api.logger.info('TotalReclaw: registered 4 QR-pairing HTTP routes synchronously');
154
+ }
155
+ ```
156
+
157
+ - **`skill/plugin/pair-http-route-registration.test.ts` — rc.5 regression
158
+ guard**. The existing SIMULATION suite (27 assertions covering the 4
159
+ routes' `auth` literal, path shape, handler type) is preserved. Added
160
+ a new SYNCHRONY suite (14 assertions) that invokes `plugin.register(mockApi)`
161
+ with a minimal mocked OpenClaw API and asserts `mockApi.registerHttpRoute`
162
+ has been called 4 times IMMEDIATELY after `register()` returns — no
163
+ `await`, no tick wait. This assertion would fail under the rc.4 async-IIFE
164
+ implementation and guards against any future refactor that re-introduces
165
+ an async boundary at the registration site. Total: 41/41 passing.
166
+
7
167
  ## [3.3.0-rc.4] — 2026-04-20
8
168
 
9
169
  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. ' +
@@ -1,7 +1,6 @@
1
1
  {
2
2
  "id": "totalreclaw",
3
3
  "name": "TotalReclaw",
4
- "kind": "memory",
5
4
  "description": "End-to-end encrypted memory vault for AI agents",
6
5
  "configSchema": {
7
6
  "type": "object",
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.6",
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": [
@@ -50,6 +50,7 @@
50
50
  "skill.json"
51
51
  ],
52
52
  "scripts": {
53
+ "test": "npx tsx manifest-shape.test.ts",
53
54
  "check-scanner": "node ../scripts/check-scanner.mjs",
54
55
  "prepublishOnly": "node ../scripts/check-scanner.mjs"
55
56
  },