@totalreclaw/totalreclaw 3.3.2 → 3.3.4-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,155 @@ 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.3-rc.1] — 2026-04-30
8
+
9
+ Combined RC bundle:
10
+
11
+ - Fix the OpenClaw runtime-scanner regression that blocked `openclaw plugins
12
+ install @totalreclaw/totalreclaw` on stable 3.3.2 (Telegram QA, OpenClaw
13
+ 2026.4.22).
14
+ - Implement the codified RC=staging / stable=production environment-binding
15
+ rule from PR #165.
16
+ - Add a one-shot RC/staging banner so QA testers can't accidentally use an
17
+ RC build for real data.
18
+ - Decouple the ~700 MB embedder bundle download from the pair-completion
19
+ gate (issue [#187](https://github.com/p-diogo/totalreclaw-internal/issues/187)).
20
+ - Document the direct-node fallback for inside-gateway agents that hit
21
+ CLI deadlock (issue [#184](https://github.com/p-diogo/totalreclaw-internal/issues/184)).
22
+
23
+ ### Fixed — OpenClaw scanner blocking install on `child_process` import
24
+
25
+ User chat QA on stable 3.3.2 hit:
26
+
27
+ > The plugin install was blocked — OpenClaw flagged it because the plugin's
28
+ > `postinstall.mjs` uses `child_process` (shell execution), which triggers
29
+ > the dangerous-code-pattern safety gate.
30
+
31
+ Workaround was `--allow-dangerous`. Real fix (this RC): drop `postinstall.mjs`
32
+ entirely. The runtime `register(api)` path already (since 3.3.1-rc.21 / 22)
33
+ sweeps `.openclaw-install-stage-*` siblings AND clears the
34
+ `.tr-partial-install` marker, so the postinstall script was redundant.
35
+
36
+ - `skill/plugin/postinstall.mjs` deleted.
37
+ - `skill/plugin/postinstall-validation.test.ts` deleted (the script it
38
+ exercised no longer exists; the runtime equivalents are still covered by
39
+ `install-staging-cleanup.test.ts` + `partial-install-detection.test.ts` +
40
+ `install-reload-idempotency.test.ts`).
41
+ - `package.json` no longer declares `scripts.postinstall` and no longer
42
+ ships `postinstall.mjs` in the `files` array.
43
+
44
+ Behavior preserved:
45
+ - `preinstall` still writes `.tr-partial-install` (uses `node -e` only — no
46
+ `child_process` import).
47
+ - The `.tr-partial-install` marker is now cleared exclusively at plugin
48
+ load time by `register(api)`.
49
+ - `.openclaw-install-stage-*` orphan sweep happens at register() time via
50
+ `cleanupInstallStagingDirs(pluginDir)`.
51
+ - Critical deps (`@scure/bip39`, `@scure/bip39/wordlists/english.js`,
52
+ `@totalreclaw/core`, `@totalreclaw/client`, etc.) are imported at module
53
+ top of `index.ts` — if any is missing, the SDK loader surfaces the
54
+ import error directly AND the existing `.error.json` write path drops a
55
+ structured marker (issue #186 in 3.3.2-rc.1). The retry-by-respawn was
56
+ nice-to-have, not load-bearing.
57
+
58
+ OpenClaw's runtime scanner (different code path from the plugin's local
59
+ `check-scanner.mjs`) does NOT honor the `// scanner-sim: allow` comment.
60
+ The local scanner's previous guidance ("Moving the subprocess call into a
61
+ separate post-install helper that OpenClaw sandboxes") turned out to be
62
+ incorrect — the runtime scanner inspects the full tarball and flags any
63
+ `child_process` import regardless of file role. The local scanner now has
64
+ nothing to flag because `child_process` no longer appears anywhere in the
65
+ shipped tarball.
66
+
67
+ ### Added — ENV binding implementation (PR #165 codified rule)
68
+
69
+ | `release-type` | Default `TOTALRECLAW_SERVER_URL` | Audience |
70
+ |---|---|---|
71
+ | `rc` | `https://api-staging.totalreclaw.xyz` | QA only — never point real users here |
72
+ | `stable` | `https://api.totalreclaw.xyz` | Production users |
73
+
74
+ User env override (`TOTALRECLAW_SERVER_URL=...`) always wins.
75
+
76
+ Implementation:
77
+
78
+ - Source-of-truth in `config.ts` / `index.ts` / `subgraph-store.ts` /
79
+ `skill.json` now references `api-staging.totalreclaw.xyz` everywhere.
80
+ RC tarballs ship the staging URL by design.
81
+ - Stable publish workflows (`npm-publish.yml` + `publish-clawhub.yml`)
82
+ add a "Bind stable artifacts to production URLs" step that
83
+ sed-replaces `api-staging.totalreclaw.xyz` → `api.totalreclaw.xyz`
84
+ across `dist/**.js`, `skill.json`, and the SKILL.md / CLAWHUB.md /
85
+ CHANGELOG.md / README.md prose, before pack/publish.
86
+ - New `skill/scripts/check-url-binding.mjs` guard runs at
87
+ `prepublishOnly` time + as a workflow step. It asserts the right
88
+ invariant for the resolved release type (RC artifact MUST contain
89
+ `api-staging.totalreclaw.xyz`; stable artifact MUST contain
90
+ `api.totalreclaw.xyz` AND ZERO staging references). Misconfigured
91
+ artifacts fail the publish before reaching the registry.
92
+ - `prepublishOnly` reads `TOTALRECLAW_RELEASE_TYPE=stable|rc` (default
93
+ `rc` for safety) so local `npm publish` invocations also assert the
94
+ invariant.
95
+ - New `url-binding.test.ts` regression covers both modes against a
96
+ synthetic artifact tree.
97
+
98
+ ### Added — RC/staging banner (one-shot per gateway process)
99
+
100
+ When the bundled `serverUrl` resolves to `api-staging.totalreclaw.xyz`
101
+ AND the user has not overridden via env, the plugin emits a prominent
102
+ prependContext banner on the first non-trivial `before_agent_start`:
103
+
104
+ > ⚠️ TotalReclaw is running in RC / staging mode
105
+ >
106
+ > This build is bound to `api-staging.totalreclaw.xyz`. Staging has **no
107
+ > SLA** and may be wiped between QA cycles. Do **NOT** use this build for
108
+ > real data.
109
+ >
110
+ > For production, install the stable release: `openclaw plugins install
111
+ > @totalreclaw/totalreclaw` (no `@rc` suffix). To pin a custom server,
112
+ > set `TOTALRECLAW_SERVER_URL=https://api.totalreclaw.xyz` in your env.
113
+
114
+ Stable artifacts (where the workflow seded the URL to production) never
115
+ fire the banner. Per-process one-shot semantics — restart re-fires once.
116
+
117
+ ### Added — `totalreclaw_preload_embedder` tool + non-blocking prefetch (issue #187)
118
+
119
+ - New tool: `totalreclaw_preload_embedder` lets the agent download the
120
+ embedder bundle ahead of `totalreclaw_pair`. Includes a 500 MB
121
+ disk-space pre-flight (refuses if the cache mount is below threshold)
122
+ and surfaces a structured `{ status: cache_hit | fetched | failed }`
123
+ response.
124
+ - Register-time non-blocking prefetch: `register(api)` now fires
125
+ `prefetchEmbedderBundle()` as a fire-and-forget Promise immediately
126
+ after `configureEmbedder()`. The bundle download starts on gateway
127
+ boot, BEFORE the user completes pair — closing the catch-22 where the
128
+ bundle was only fetched on the first `generateEmbedding()` call (which
129
+ is gated behind `requireFullSetup()`).
130
+ - Toggle: `TOTALRECLAW_DISABLE_EMBEDDER_PREFETCH=1` skips the auto-prefetch
131
+ (CI / sandboxed-network environments). The next `generateEmbedding()`
132
+ call still triggers the download via the same idempotent path.
133
+
134
+ ### Documentation — direct-node fallback for CLI deadlock (issue #184)
135
+
136
+ - `docs/guides/openclaw-setup.md` Troubleshooting now documents the
137
+ filesystem-manifest probe (`.loaded.json` / `.error.json`) and the
138
+ `node ~/.openclaw/extensions/totalreclaw/dist/pair-cli.js
139
+ --url-pin-only` direct-node fallback for when the `openclaw` CLI
140
+ deadlocks (exit 124) inside gateway-internal agent shells.
141
+ - `skill/SKILL.md` mirrors the same fallbacks for the agent's own
142
+ instructions: prefer reading the `.loaded.json` manifest over
143
+ re-running `openclaw plugins list`; switch to direct-node `pair-cli.js`
144
+ when `totalreclaw_pair` itself hangs.
145
+
146
+ ### Known issues filed during this RC
147
+
148
+ Five new observation issues filed via the QA pipeline (severity:minor,
149
+ not blockers for 3.3.3-rc.1 promote):
150
+ - [#208](https://github.com/p-diogo/totalreclaw-internal/issues/208) — Hermes auto-extraction burst pattern can trip per-model rate limits
151
+ - [#209](https://github.com/p-diogo/totalreclaw-internal/issues/209) — `HERMES_MODEL` env swap doesn't propagate to running daemon
152
+ - [#210](https://github.com/p-diogo/totalreclaw-internal/issues/210) — Hermes Docker venv ships without pip
153
+ - [#211](https://github.com/p-diogo/totalreclaw-internal/issues/211) — ClawHub artifact's `package.json` retains rc-version label after stable promote
154
+ - [#212](https://github.com/p-diogo/totalreclaw-internal/issues/212) — `wipe-qa.sh` model-pin step appends duplicate `HERMES_MODEL` lines
155
+
7
156
  ## [3.3.2-rc.1] — 2026-04-27
8
157
 
9
158
  Hotfix bundle for the inside-gateway agent-flow ship-stoppers caught by the
package/SKILL.md CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: totalreclaw
3
3
  description: "Install + set up TotalReclaw encrypted memory for OpenClaw, then use totalreclaw_remember / totalreclaw_recall. Trigger on 'install TotalReclaw', 'set up TotalReclaw', 'restore my recovery phrase', or any remember / recall request."
4
- version: 3.3.2-rc.4
4
+ version: 3.3.4-rc.1
5
5
  author: TotalReclaw Team
6
6
  license: MIT
7
7
  homepage: https://totalreclaw.xyz
@@ -30,16 +30,28 @@ metadata:
30
30
 
31
31
  If `totalreclaw_*` tools are already bound to your toolset, SKIP install and jump to step 4. Otherwise run all steps in order.
32
32
 
33
- 1. **Install skill + plugin.** Run via your shell tool, in this order:
33
+ 1. **Install plugin first, skill second.** Run via your shell tool, in this exact order:
34
34
  ```
35
- openclaw skills install totalreclaw
36
35
  openclaw plugins install @totalreclaw/totalreclaw
36
+ openclaw skills install totalreclaw
37
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.
38
+ For RC: replace the first command with `openclaw plugins install @totalreclaw/totalreclaw@rc`. **Plugin BEFORE skill is deliberate** (3.3.4-rc.1 issue from Pedro's QA on rc.3): the skill install triggers a config-driven SIGUSR1 gateway reload that can race a concurrent plugin install. Plugin-first lets the plugin land cleanly, then the skill's reload picks it up on the next gateway loop. If you accidentally do skill-first and the plugin install dies mid-flight, just retry `openclaw plugins install @totalreclaw/totalreclaw` once it's idempotent.
39
+
40
+ If either shell call fails with `command not found` / ENOENT / "not authorized" (managed-service or shell-restricted environment), tell the user verbatim *"Your environment doesn't expose the OpenClaw CLI to me. Please install `totalreclaw` from your service's plugins / skills UI, then reply `done`."* Wait for `done` and skip to step 3.
41
+
42
+ 2. **Wait for auto-reload — then issue `/restart` autonomously if needed.** 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.
43
+
44
+ **If `totalreclaw_pair` is NOT visible after 10s:** issue OpenClaw's built-in `/restart` slash-command immediately and silently. **DO NOT ask the user "should I restart" / "do you want me to restart" / "let me check if the tool is bound" — just issue `/restart` and announce "Reloading gateway..." once.** This is in-process and drains active runs cleanly; your shell is NOT shelling out to `docker restart` or `openclaw gateway restart` (which would suicide the host). After `/restart`, wait ~3s and re-run `openclaw plugins list`.
45
+
46
+ **If `/restart` returns "You are not authorized to use this command":** the gateway's `channels.<your-channel>.allowFrom` allowlist is empty. **Do NOT keep retrying.** Surface this one-line user-facing fix and stop:
47
+
48
+ > Your channel needs to be allowlisted for slash commands. Run on the gateway host: `jq '.channels.<channel>.allowFrom = ["<your-id>"]' ~/.openclaw/openclaw.json | sponge ~/.openclaw/openclaw.json && docker restart <your-container-name>` (Docker self-host) — substitute `<channel>` with your channel name (e.g. `telegram`, `slack`), `<your-id>` with your user ID for that channel (Telegram: `@username` or numeric `chat_id`; Slack: workspace member ID), and `<your-container-name>` with your gateway container name (`docker ps` shows it). On a managed service, ask the service's support to enable slash commands for your channel. Reply `done` once it's back up.
49
+
50
+ For non-Docker / native deployments substitute `openclaw gateway restart` for the `docker restart` portion. As a session-level fallback while restart is gated, try `/new` (starts a fresh session within the same gateway process, may pick up freshly-bound tools without a full restart).
39
51
 
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`.
52
+ **Forbidden vocabulary** (these are anti-patterns when a tool is missing post-install): "Should I /restart", "Do you have a public URL", "Want me to restart", "Let me check if the tool is bound", "I need permission to restart". Just issue `/restart` immediately and announce.
41
53
 
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.
54
+ 3. **Verify `totalreclaw_pair` is bound.** If still missing after `/restart` (or after the user fixed allowFrom and said `done`), tell them the restart didn't take effect and ask them to retry. Do not loop the install.
43
55
 
44
56
  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
57
 
package/config.ts CHANGED
@@ -138,7 +138,15 @@ export const CONFIG = {
138
138
  get sessionId(): string | null {
139
139
  return getSessionId();
140
140
  },
141
- serverUrl: (process.env.TOTALRECLAW_SERVER_URL || 'https://api.totalreclaw.xyz').replace(/\/+$/, ''),
141
+ // 3.3.3-rc.1: source default is `api-staging.totalreclaw.xyz` per the
142
+ // codified RC=staging / stable=production rule (PR #165). The workflow
143
+ // step "Bind stable to production URLs" in `npm-publish.yml` /
144
+ // `publish-clawhub.yml` sed-replaces `api-staging.totalreclaw.xyz` ->
145
+ // `api.totalreclaw.xyz` across the built `dist/` tree (and skill.json /
146
+ // SKILL.md / CHANGELOG / CLAWHUB.md / package.json) when
147
+ // `release-type=stable`. RC publishes leave the staging URL untouched.
148
+ // User overrides via `TOTALRECLAW_SERVER_URL=...` always win.
149
+ serverUrl: (process.env.TOTALRECLAW_SERVER_URL || 'https://api-staging.totalreclaw.xyz').replace(/\/+$/, ''),
142
150
  selfHosted: process.env.TOTALRECLAW_SELF_HOSTED === 'true',
143
151
  credentialsPath: process.env.TOTALRECLAW_CREDENTIALS_PATH || path.join(home, '.totalreclaw', 'credentials.json'),
144
152
  // 3.2.0 onboarding state file — separate from credentials.json so it
@@ -184,6 +192,21 @@ export const CONFIG = {
184
192
  entryPointAddress: process.env.TOTALRECLAW_ENTRYPOINT_ADDRESS || '',
185
193
  rpcUrl: process.env.TOTALRECLAW_RPC_URL || '',
186
194
 
195
+ // 3.3.3-rc.1 (issue #187 — ONNX decouple): kill switch for the
196
+ // non-blocking embedder bundle prefetch fired from register(). Set to
197
+ // `1` in CI / sandboxed environments where the GitHub-Releases CDN is
198
+ // unreachable. The next call to generateEmbedding() still triggers the
199
+ // download via the same idempotent path.
200
+ embedderPrefetchDisabled: process.env.TOTALRECLAW_DISABLE_EMBEDDER_PREFETCH === '1',
201
+
202
+ // 3.3.3-rc.1 (PR #165 implementation): observable form of "did the user
203
+ // explicitly override the bundled-default server URL via env?". Used
204
+ // by the RC-staging banner check in index.ts so the banner suppresses
205
+ // when the user has pinned a custom URL (production or self-hosted).
206
+ // Lives here so index.ts stays free of process.env reads (scanner
207
+ // env-harvesting rule).
208
+ serverUrlEnvOverridden: !!process.env.TOTALRECLAW_SERVER_URL,
209
+
187
210
  // Tuning knobs — default values used only as local fallback for
188
211
  // self-hosted mode. Managed-service clients override these from the relay
189
212
  // billing response via `resolveTuning(...)`.
package/dist/config.js CHANGED
@@ -123,7 +123,15 @@ export const CONFIG = {
123
123
  get sessionId() {
124
124
  return getSessionId();
125
125
  },
126
- serverUrl: (process.env.TOTALRECLAW_SERVER_URL || 'https://api.totalreclaw.xyz').replace(/\/+$/, ''),
126
+ // 3.3.3-rc.1: source default is `api-staging.totalreclaw.xyz` per the
127
+ // codified RC=staging / stable=production rule (PR #165). The workflow
128
+ // step "Bind stable to production URLs" in `npm-publish.yml` /
129
+ // `publish-clawhub.yml` sed-replaces `api-staging.totalreclaw.xyz` ->
130
+ // `api.totalreclaw.xyz` across the built `dist/` tree (and skill.json /
131
+ // SKILL.md / CHANGELOG / CLAWHUB.md / package.json) when
132
+ // `release-type=stable`. RC publishes leave the staging URL untouched.
133
+ // User overrides via `TOTALRECLAW_SERVER_URL=...` always win.
134
+ serverUrl: (process.env.TOTALRECLAW_SERVER_URL || 'https://api-staging.totalreclaw.xyz').replace(/\/+$/, ''),
127
135
  selfHosted: process.env.TOTALRECLAW_SELF_HOSTED === 'true',
128
136
  credentialsPath: process.env.TOTALRECLAW_CREDENTIALS_PATH || path.join(home, '.totalreclaw', 'credentials.json'),
129
137
  // 3.2.0 onboarding state file — separate from credentials.json so it
@@ -166,6 +174,19 @@ export const CONFIG = {
166
174
  dataEdgeAddress: process.env.TOTALRECLAW_DATA_EDGE_ADDRESS || '',
167
175
  entryPointAddress: process.env.TOTALRECLAW_ENTRYPOINT_ADDRESS || '',
168
176
  rpcUrl: process.env.TOTALRECLAW_RPC_URL || '',
177
+ // 3.3.3-rc.1 (issue #187 — ONNX decouple): kill switch for the
178
+ // non-blocking embedder bundle prefetch fired from register(). Set to
179
+ // `1` in CI / sandboxed environments where the GitHub-Releases CDN is
180
+ // unreachable. The next call to generateEmbedding() still triggers the
181
+ // download via the same idempotent path.
182
+ embedderPrefetchDisabled: process.env.TOTALRECLAW_DISABLE_EMBEDDER_PREFETCH === '1',
183
+ // 3.3.3-rc.1 (PR #165 implementation): observable form of "did the user
184
+ // explicitly override the bundled-default server URL via env?". Used
185
+ // by the RC-staging banner check in index.ts so the banner suppresses
186
+ // when the user has pinned a custom URL (production or self-hosted).
187
+ // Lives here so index.ts stays free of process.env reads (scanner
188
+ // env-harvesting rule).
189
+ serverUrlEnvOverridden: !!process.env.TOTALRECLAW_SERVER_URL,
169
190
  // Tuning knobs — default values used only as local fallback for
170
191
  // self-hosted mode. Managed-service clients override these from the relay
171
192
  // billing response via `resolveTuning(...)`.
package/dist/embedding.js CHANGED
@@ -53,10 +53,50 @@ export function configureEmbedder(cfg) {
53
53
  function defaultCacheRoot() {
54
54
  return path.join(os.homedir(), '.totalreclaw', 'embedder');
55
55
  }
56
+ /**
57
+ * Last-known-good embedder bundle tag. Used ONLY as a hard-fallback when
58
+ * `configureEmbedder()` is never called by the orchestrator (defensive
59
+ * path — production code always wires it via index.ts register()).
60
+ *
61
+ * 3.3.4-rc.1 — pinned to v3.3.3-rc.1 because that is the most recent
62
+ * release at fix-time with a published `embedder-v1.tar.gz` asset. Earlier
63
+ * fallback `'0.0.0-dev'` (rc.22 → 3.3.3-rc.1) hard-coded a placeholder
64
+ * that resolved to a 404 GitHub Release URL; QA on 3.3.3-rc.1 (Pedro
65
+ * 2026-04-30) caught it because the cascade-cause (broken
66
+ * `readPluginVersion()` resolution) made the fallback fire on every cold
67
+ * start. Bumping this constant per RC is fine — the publish workflow auto-
68
+ * publishes the bundle for every RC tag (see scripts/build-embedder-
69
+ * bundle.mjs in the public repo).
70
+ */
71
+ const LAST_KNOWN_GOOD_RC_TAG = '3.3.3-rc.1';
56
72
  function activeRuntimeConfig() {
57
73
  if (runtimeConfig)
58
74
  return runtimeConfig;
59
- return { cacheRoot: defaultCacheRoot(), rcTag: '0.0.0-dev' };
75
+ return { cacheRoot: defaultCacheRoot(), rcTag: LAST_KNOWN_GOOD_RC_TAG };
76
+ }
77
+ /**
78
+ * 3.3.3-rc.1 (issue #187 — ONNX decouple): prefetch the embedder bundle
79
+ * WITHOUT loading the model into memory. Used to download the
80
+ * ~700 MB tarball pre-pair so the user does not hit the network round-trip
81
+ * mid-conversation. Idempotent — subsequent calls are cache-hit no-ops.
82
+ *
83
+ * Returns:
84
+ * - `'cache_hit'` if the bundle was already extracted + verified.
85
+ * - `'fetched'` if the bundle was downloaded this call.
86
+ * - throws on transport / extraction failure.
87
+ *
88
+ * Pre-flight is the caller's job (disk-space, network reachability) — this
89
+ * function focuses on the cache-resolve + fetch-on-miss path so it can also
90
+ * be reused as a fast cache-validation probe.
91
+ */
92
+ export async function prefetchEmbedderBundle(opts) {
93
+ const cfg = activeRuntimeConfig();
94
+ const loaded = await loadEmbedder({
95
+ cacheRoot: cfg.cacheRoot,
96
+ rcTag: cfg.rcTag,
97
+ log: opts?.log,
98
+ });
99
+ return loaded.wasFetched ? 'fetched' : 'cache_hit';
60
100
  }
61
101
  /** Lazily initialized state. */
62
102
  let pipelineExtractor = null;
@@ -74,9 +74,17 @@ export function ensureMemoryHeaderFile(workspace, header, markerSubstring = 'Tot
74
74
  * Read the plugin's own version string from `package.json`.
75
75
  *
76
76
  * Behaviour:
77
- * - Resolves `package.json` next to the caller-provided directory
77
+ * - Tries `package.json` next to the caller-provided directory first
78
78
  * (typically `path.dirname(fileURLToPath(import.meta.url))` from the
79
- * caller).
79
+ * caller — i.e., the directory of the running ESM module).
80
+ * - If that misses, walks up to 5 parent directories looking for a
81
+ * `package.json` whose `name` is `@totalreclaw/totalreclaw`. This
82
+ * covers the OpenClaw plugin sandbox case where the loaded module
83
+ * lives at `<pluginRoot>/dist/index.js` while `package.json` lives
84
+ * at `<pluginRoot>/package.json` (3.3.4-rc.1 fix — without this
85
+ * walk-up, the `.loaded.json` manifest gets `version=unknown` and
86
+ * all RC-gated logic that depends on the version string fails
87
+ * silently in production OpenClaw deployments).
80
88
  * - Returns the `version` field, or `null` on any I/O / parse error.
81
89
  *
82
90
  * Used by the RC-gated `totalreclaw_report_qa_bug` tool registration in
@@ -87,13 +95,51 @@ export function ensureMemoryHeaderFile(workspace, header, markerSubstring = 'Tot
87
95
  * helper — see the file-header guardrail.
88
96
  */
89
97
  export function readPluginVersion(packageJsonDir) {
98
+ // Direct hit (source-tree dev path; tests).
99
+ const direct = tryReadPluginPackageJson(path.join(packageJsonDir, 'package.json'));
100
+ if (direct)
101
+ return direct;
102
+ // Walk up — the running ESM module typically lives at
103
+ // `<pluginRoot>/dist/index.js`, so `packageJsonDir` is `<pluginRoot>/dist`
104
+ // and `package.json` is one level up. Bound the walk so a misconfigured
105
+ // path doesn't traverse the entire filesystem; 5 levels is more than
106
+ // enough for any realistic plugin layout (dist/, dist/cjs/, build/lib/).
107
+ let current = packageJsonDir;
108
+ for (let depth = 0; depth < 5; depth++) {
109
+ const parent = path.dirname(current);
110
+ if (parent === current)
111
+ break; // root reached
112
+ const candidate = path.join(parent, 'package.json');
113
+ const version = tryReadPluginPackageJson(candidate);
114
+ if (version)
115
+ return version;
116
+ current = parent;
117
+ }
118
+ return null;
119
+ }
120
+ /**
121
+ * Try to read `package.json` at `pkgPath`. Returns the `version` only if
122
+ * the file's `name` field matches `@totalreclaw/totalreclaw` — guards
123
+ * against accidentally returning the version of an outer host-package
124
+ * (e.g. when the plugin is bundled inside a parent app's tree).
125
+ *
126
+ * If `name` is absent (legacy / minimal package.json), accept the version
127
+ * anyway as a fallback — this is the existing behaviour preserved for
128
+ * anyone who manually trimmed their package.json.
129
+ */
130
+ function tryReadPluginPackageJson(pkgPath) {
90
131
  try {
91
- const pkgPath = path.join(packageJsonDir, 'package.json');
92
132
  if (!fs.existsSync(pkgPath))
93
133
  return null;
94
134
  const raw = fs.readFileSync(pkgPath, 'utf-8');
95
135
  const parsed = JSON.parse(raw);
96
- return typeof parsed.version === 'string' ? parsed.version : null;
136
+ if (typeof parsed.version !== 'string')
137
+ return null;
138
+ if (typeof parsed.name === 'string' && parsed.name !== '@totalreclaw/totalreclaw') {
139
+ // Wrong package — keep walking.
140
+ return null;
141
+ }
142
+ return parsed.version;
97
143
  }
98
144
  catch {
99
145
  return null;