@totalreclaw/totalreclaw 3.3.2 → 3.3.3-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.3-rc.1
5
5
  author: TotalReclaw Team
6
6
  license: MIT
7
7
  homepage: https://totalreclaw.xyz
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
@@ -58,6 +58,30 @@ function activeRuntimeConfig() {
58
58
  return runtimeConfig;
59
59
  return { cacheRoot: defaultCacheRoot(), rcTag: '0.0.0-dev' };
60
60
  }
61
+ /**
62
+ * 3.3.3-rc.1 (issue #187 — ONNX decouple): prefetch the embedder bundle
63
+ * WITHOUT loading the model into memory. Used to download the
64
+ * ~700 MB tarball pre-pair so the user does not hit the network round-trip
65
+ * mid-conversation. Idempotent — subsequent calls are cache-hit no-ops.
66
+ *
67
+ * Returns:
68
+ * - `'cache_hit'` if the bundle was already extracted + verified.
69
+ * - `'fetched'` if the bundle was downloaded this call.
70
+ * - throws on transport / extraction failure.
71
+ *
72
+ * Pre-flight is the caller's job (disk-space, network reachability) — this
73
+ * function focuses on the cache-resolve + fetch-on-miss path so it can also
74
+ * be reused as a fast cache-validation probe.
75
+ */
76
+ export async function prefetchEmbedderBundle(opts) {
77
+ const cfg = activeRuntimeConfig();
78
+ const loaded = await loadEmbedder({
79
+ cacheRoot: cfg.cacheRoot,
80
+ rcTag: cfg.rcTag,
81
+ log: opts?.log,
82
+ });
83
+ return loaded.wasFetched ? 'fetched' : 'cache_hit';
84
+ }
61
85
  /** Lazily initialized state. */
62
86
  let pipelineExtractor = null;
63
87
  let autoTokenizer = null;
package/dist/index.js CHANGED
@@ -48,7 +48,7 @@
48
48
  import { deriveKeys, deriveLshSeed, computeAuthKeyHash, encrypt, decrypt, generateBlindIndices, generateContentFingerprint, } from './crypto.js';
49
49
  import { createApiClient } from './api-client.js';
50
50
  import { extractFacts, extractDebrief, isValidMemoryType, parseEntity, VALID_MEMORY_TYPES, LEGACY_V0_MEMORY_TYPES, VALID_MEMORY_SOURCES, VALID_MEMORY_SCOPES, EXTRACTION_SYSTEM_PROMPT, extractFactsForCompaction, } from './extractor.js';
51
- import { initLLMClient, resolveLLMConfig, chatCompletion, generateEmbedding, getEmbeddingDims, getEmbeddingModelId, configureEmbedder, } from './llm-client.js';
51
+ import { initLLMClient, resolveLLMConfig, chatCompletion, generateEmbedding, getEmbeddingDims, getEmbeddingModelId, configureEmbedder, prefetchEmbedderBundle, } from './llm-client.js';
52
52
  import { defaultAuthProfilesRoot, readAllProfileKeys, dedupeByProvider, } from './llm-profile-reader.js';
53
53
  import { LSHHasher } from './lsh.js';
54
54
  import { rerank, cosineSimilarity, detectQueryIntent, INTENT_WEIGHTS } from './reranker.js';
@@ -501,6 +501,20 @@ let firstRunAfterInit = true;
501
501
  * skips. A fresh gateway restart resets it back to `false`.
502
502
  */
503
503
  let firstRunWelcomeShown = false;
504
+ /**
505
+ * 3.3.3-rc.1 — RC-mode staging-only banner (PR #165 implementation).
506
+ *
507
+ * Fires ONCE per gateway process when:
508
+ * - the bundled-default `serverUrl` resolves to `api-staging.totalreclaw.xyz`
509
+ * (RC artifact, not stable), AND
510
+ * - the user has NOT overridden via `TOTALRECLAW_SERVER_URL=...` env.
511
+ *
512
+ * Goal: a fresh QA tester can't accidentally use an RC build for real data
513
+ * without seeing a clear "RC = staging, no SLA, may be wiped" warning.
514
+ * One-shot at the first `before_agent_start` after register(); never spams
515
+ * per-turn. A fresh gateway restart re-fires it once.
516
+ */
517
+ let stagingBannerShown = false;
504
518
  /**
505
519
  * Derive keys from the recovery phrase, load credentials, and register with
506
520
  * the server if this is the first run.
@@ -514,7 +528,9 @@ let firstRunWelcomeShown = false;
514
528
  * directs the caller to `openclaw totalreclaw onboard`.
515
529
  */
516
530
  async function initialize(logger) {
517
- const serverUrl = CONFIG.serverUrl || 'https://api.totalreclaw.xyz';
531
+ // 3.3.3-rc.1: staging is the source default per #165. Stable build-time
532
+ // sed-replace flips `api-staging` -> `api` in dist/.
533
+ const serverUrl = CONFIG.serverUrl || 'https://api-staging.totalreclaw.xyz';
518
534
  let masterPassword = CONFIG.recoveryPhrase;
519
535
  // 3.2.0: if the env var is unset, probe credentials.json for a
520
536
  // pre-existing mnemonic (written either by the CLI wizard on this machine
@@ -2407,13 +2423,36 @@ const plugin = {
2407
2423
  const msg = err instanceof Error ? err.message : String(err);
2408
2424
  api.logger.warn(`TotalReclaw: configureEmbedder failed (will use defaults): ${msg}`);
2409
2425
  }
2426
+ // 3.3.3-rc.1 (issue #187 — ONNX decouple): kick off a non-blocking
2427
+ // bundle prefetch so the ~700 MB embedder tarball starts streaming
2428
+ // as soon as the gateway boots, BEFORE the user completes
2429
+ // `totalreclaw_pair`. Decouples the model download from the
2430
+ // pair-completion gate the previous flow imposed via
2431
+ // `requireFullSetup()` -> first `generateEmbedding()` call.
2432
+ // Fire-and-forget — never awaits, never throws on failure (the next
2433
+ // `generateEmbedding()` call retries via the same idempotent path).
2434
+ // Disabled when `TOTALRECLAW_DISABLE_EMBEDDER_PREFETCH=1` (CI / tests
2435
+ // where the network is sandboxed away). The env read lives in
2436
+ // config.ts; we read the resolved CONFIG flag here so this file
2437
+ // stays scanner-clean (no env lookups in index.ts).
2438
+ if (!CONFIG.embedderPrefetchDisabled) {
2439
+ prefetchEmbedderBundle({ log: (msg) => api.logger.info(msg) })
2440
+ .then((result) => {
2441
+ api.logger.info(`TotalReclaw: embedder prefetch ${result === 'fetched' ? 'completed (downloaded bundle)' : 'cache hit'}`);
2442
+ })
2443
+ .catch((err) => {
2444
+ const msg = err instanceof Error ? err.message : String(err);
2445
+ api.logger.warn(`TotalReclaw: embedder prefetch failed (non-fatal — will retry on first generateEmbedding): ${msg}`);
2446
+ });
2447
+ }
2410
2448
  // 3.3.1-rc.22 (rc.21 finding #5): self-heal partial-install marker.
2411
- // The `preinstall` npm script writes `.tr-partial-install`; the
2412
- // `postinstall` script removes it on a successful install. If we
2413
- // have gotten this far the loader did register us — meaning the
2414
- // install succeeded enough to be useful so any lingering marker
2415
- // (e.g. npm ran preinstall but postinstall misfired) is stale.
2416
- // Clear it so the next retry's detector does not see a false positive.
2449
+ // The `preinstall` npm script writes `.tr-partial-install`; clearing
2450
+ // it has been the runtime's job since 3.3.3-rc.1 dropped postinstall.mjs
2451
+ // (OpenClaw scanner blocked the install on the subprocess-spawn import
2452
+ // see 3.3.3-rc.1 PR). If we have gotten this far the loader did
2453
+ // register us meaning the install succeeded enough to be useful —
2454
+ // so any lingering marker is stale. Clear it so the next retry's
2455
+ // detector does not see a false positive.
2417
2456
  //
2418
2457
  // 3.3.1-rc.22 (rc.21 finding #6) — gateway/reload upstream caveat:
2419
2458
  // OpenClaw's config-watcher fires `gateway/reload` when
@@ -3509,6 +3548,114 @@ const plugin = {
3509
3548
  },
3510
3549
  }, { name: 'totalreclaw_status' });
3511
3550
  // ---------------------------------------------------------------
3551
+ // Tool: totalreclaw_preload_embedder (3.3.3-rc.1 — issue #187)
3552
+ // ---------------------------------------------------------------
3553
+ //
3554
+ // Decouples the ~700 MB embedder bundle download from the pair-
3555
+ // completion gate. The agent can call this BEFORE
3556
+ // `totalreclaw_pair` to pre-flight disk space, kick off the
3557
+ // download, and surface the completion state. The non-blocking
3558
+ // prefetch in `register()` already starts the download
3559
+ // unconditionally; this tool is an explicit on-demand hook for
3560
+ // agents that want to confirm completion (or trigger a retry on
3561
+ // network failure) without first completing pair.
3562
+ //
3563
+ // Behavior:
3564
+ // - Disk-space pre-flight: refuses if free disk < 500 MB on the
3565
+ // embedder cache mount. Surfaces the path + free bytes so the
3566
+ // user can clear space.
3567
+ // - Triggers `prefetchEmbedderBundle()` (idempotent — cache hit
3568
+ // returns immediately).
3569
+ // - Returns a structured success message with status:
3570
+ // `cache_hit | fetched | failed`.
3571
+ //
3572
+ // Phrase-safety: this tool does NOT touch credentials.json,
3573
+ // mnemonics, or keys. It only touches `~/.totalreclaw/embedder/`
3574
+ // and the GitHub Releases CDN.
3575
+ api.registerTool({
3576
+ name: 'totalreclaw_preload_embedder',
3577
+ label: 'Preload Embedder',
3578
+ description: 'Download the local-embedding model bundle ahead of pair completion. Use this when the user wants to set up TotalReclaw on a slow connection or run an offline-after-setup workflow. Returns success once the ~700 MB bundle is cached at ~/.totalreclaw/embedder/.',
3579
+ parameters: {
3580
+ type: 'object',
3581
+ properties: {},
3582
+ additionalProperties: false,
3583
+ },
3584
+ async execute() {
3585
+ try {
3586
+ // Disk-space pre-flight: refuse if < 500 MB free on the
3587
+ // embedder cache mount. Best-effort — if statfs fails, we
3588
+ // proceed without the pre-flight rather than blocking.
3589
+ const fsModule = await import('node:fs');
3590
+ const cacheRoot = CONFIG.embedderCachePath;
3591
+ const REQUIRED_BYTES = 500 * 1024 * 1024;
3592
+ try {
3593
+ // Find the deepest existing parent so statfs has a real
3594
+ // mount to measure (loadEmbedder will mkdir under it
3595
+ // anyway).
3596
+ let probeDir = cacheRoot;
3597
+ while (true) {
3598
+ try {
3599
+ fsModule.statSync(probeDir);
3600
+ break;
3601
+ }
3602
+ catch {
3603
+ const parent = nodePath.dirname(probeDir);
3604
+ if (parent === probeDir)
3605
+ break;
3606
+ probeDir = parent;
3607
+ }
3608
+ }
3609
+ const stats = fsModule.statfsSync?.(probeDir);
3610
+ if (stats) {
3611
+ const bavail = typeof stats.bavail === 'bigint' ? Number(stats.bavail) : stats.bavail;
3612
+ const bsize = typeof stats.bsize === 'bigint' ? Number(stats.bsize) : stats.bsize;
3613
+ const freeBytes = bavail * bsize;
3614
+ if (freeBytes > 0 && freeBytes < REQUIRED_BYTES) {
3615
+ const freeMb = Math.round(freeBytes / (1024 * 1024));
3616
+ return {
3617
+ content: [{
3618
+ type: 'text',
3619
+ text: `Insufficient free disk space for embedder bundle. Required: 500 MB. Available at ${probeDir}: ${freeMb} MB. Free up space and retry.`,
3620
+ }],
3621
+ details: { status: 'failed', reason: 'disk_space', free_mb: freeMb, required_mb: 500, cache_root: cacheRoot },
3622
+ };
3623
+ }
3624
+ }
3625
+ }
3626
+ catch {
3627
+ // statfs probe failed — surface a soft warning in logs
3628
+ // but proceed with the download anyway.
3629
+ api.logger.info('totalreclaw_preload_embedder: disk-space probe unavailable, proceeding without pre-flight');
3630
+ }
3631
+ // Trigger the prefetch. This is idempotent (cache hit returns
3632
+ // immediately) so it's safe to invoke even when the
3633
+ // background prefetch from register() already completed.
3634
+ const result = await prefetchEmbedderBundle({
3635
+ log: (msg) => api.logger.info(msg),
3636
+ });
3637
+ const human = result === 'fetched'
3638
+ ? `Embedder bundle downloaded and cached at ${cacheRoot}. Subsequent embedding calls run in-memory.`
3639
+ : `Embedder bundle already cached at ${cacheRoot} — no download needed.`;
3640
+ return {
3641
+ content: [{ type: 'text', text: human }],
3642
+ details: { status: result, cache_root: cacheRoot },
3643
+ };
3644
+ }
3645
+ catch (err) {
3646
+ const message = err instanceof Error ? err.message : String(err);
3647
+ api.logger.error(`totalreclaw_preload_embedder failed: ${message}`);
3648
+ return {
3649
+ content: [{
3650
+ type: 'text',
3651
+ text: `Embedder bundle preload failed: ${humanizeError(message)}. The plugin will retry on first embedding call.`,
3652
+ }],
3653
+ details: { status: 'failed', reason: 'fetch_error', message },
3654
+ };
3655
+ }
3656
+ },
3657
+ }, { name: 'totalreclaw_preload_embedder' });
3658
+ // ---------------------------------------------------------------
3512
3659
  // Tool: totalreclaw_consolidate
3513
3660
  // ---------------------------------------------------------------
3514
3661
  api.registerTool({
@@ -4840,6 +4987,38 @@ const plugin = {
4840
4987
  if (!evt?.prompt || evt.prompt.length < 5) {
4841
4988
  return undefined;
4842
4989
  }
4990
+ // 3.3.3-rc.1 — RC-staging banner (PR #165 implementation).
4991
+ // Build a one-shot prefix when the bundled default points at staging
4992
+ // AND the user hasn't overridden via env. This prefix is prepended
4993
+ // to whichever context block the rest of the hook produces.
4994
+ let stagingBannerBlock = '';
4995
+ if (!stagingBannerShown) {
4996
+ try {
4997
+ const usingStagingDefault = CONFIG.serverUrl.includes('api-staging.totalreclaw.xyz');
4998
+ const userOverrode = CONFIG.serverUrlEnvOverridden;
4999
+ if (usingStagingDefault && !userOverrode) {
5000
+ stagingBannerBlock =
5001
+ '## ⚠️ TotalReclaw is running in RC / staging mode\n\n' +
5002
+ 'This build is bound to `api-staging.totalreclaw.xyz`. Staging has **no SLA** ' +
5003
+ 'and may be wiped between QA cycles. Do **NOT** use this build for real data.\n\n' +
5004
+ 'For production, install the stable release: `openclaw plugins install ' +
5005
+ '@totalreclaw/totalreclaw` (no `@rc` suffix). To pin a custom server, set ' +
5006
+ '`TOTALRECLAW_SERVER_URL=https://api.totalreclaw.xyz` in your env.\n\n';
5007
+ stagingBannerShown = true;
5008
+ api.logger.warn('TotalReclaw: RC/staging build active (api-staging.totalreclaw.xyz). ' +
5009
+ 'See docs/guides/release-process.md for the RC=staging / stable=production rule.');
5010
+ }
5011
+ else {
5012
+ // Non-RC artifact OR user override — never fire the banner this
5013
+ // gateway-process lifetime.
5014
+ stagingBannerShown = true;
5015
+ }
5016
+ }
5017
+ catch {
5018
+ // Best-effort; never block session start on banner derivation.
5019
+ stagingBannerShown = true;
5020
+ }
5021
+ }
4843
5022
  await ensureInitialized(api.logger);
4844
5023
  // 3.2.0 onboarding pending: emit a non-secret guidance banner so
4845
5024
  // the LLM knows how to respond when the user asks about setup.
@@ -4868,7 +5047,8 @@ const plugin = {
4868
5047
  api.logger.warn(`First-run welcome check failed: ${msg}`);
4869
5048
  }
4870
5049
  return {
4871
- prependContext: welcomeBlock +
5050
+ prependContext: stagingBannerBlock +
5051
+ welcomeBlock +
4872
5052
  '## TotalReclaw setup pending\n\n' +
4873
5053
  'TotalReclaw encrypted memory is installed but not yet set up on this machine. ' +
4874
5054
  'If the user asks about memory features or wants to configure TotalReclaw, ' +
@@ -4966,7 +5146,8 @@ const plugin = {
4966
5146
  if (injectResult.promptText) {
4967
5147
  api.logger.info(`Digest injection: state=${injectResult.state}`);
4968
5148
  return {
4969
- prependContext: `## Your Memory\n\n${injectResult.promptText}` + welcomeBack + billingWarning,
5149
+ prependContext: stagingBannerBlock +
5150
+ `## Your Memory\n\n${injectResult.promptText}` + welcomeBack + billingWarning,
4970
5151
  };
4971
5152
  }
4972
5153
  }
@@ -5007,7 +5188,7 @@ const plugin = {
5007
5188
  const querySimilarity = cosineSimilarity(queryEmbedding, lastQueryEmbedding);
5008
5189
  if (querySimilarity > SEMANTIC_SKIP_THRESHOLD) {
5009
5190
  const lines = cachedFacts.slice(0, 8).map((f, i) => `${i + 1}. ${f.text} (importance: ${f.importance}/10, cached)`);
5010
- return { prependContext: `## Relevant Memories\n\n${lines.join('\n')}` + welcomeBack + billingWarning };
5191
+ return { prependContext: stagingBannerBlock + `## Relevant Memories\n\n${lines.join('\n')}` + welcomeBack + billingWarning };
5011
5192
  }
5012
5193
  }
5013
5194
  // 3. Merge trapdoors — always include word trapdoors for small-dataset coverage.
@@ -5016,7 +5197,7 @@ const plugin = {
5016
5197
  // If we have cached facts and no trapdoors, return cached facts.
5017
5198
  if (allTrapdoors.length === 0 && cachedFacts.length > 0) {
5018
5199
  const lines = cachedFacts.slice(0, 8).map((f, i) => `${i + 1}. ${f.text} (importance: ${f.importance}/10, cached)`);
5019
- return { prependContext: `## Relevant Memories\n\n${lines.join('\n')}` + welcomeBack + billingWarning };
5200
+ return { prependContext: stagingBannerBlock + `## Relevant Memories\n\n${lines.join('\n')}` + welcomeBack + billingWarning };
5020
5201
  }
5021
5202
  if (allTrapdoors.length === 0)
5022
5203
  return undefined;
@@ -5031,7 +5212,7 @@ const plugin = {
5031
5212
  // Subgraph query failed -- fall back to cached facts if available.
5032
5213
  if (cachedFacts.length > 0) {
5033
5214
  const lines = cachedFacts.slice(0, 8).map((f, i) => `${i + 1}. ${f.text} (importance: ${f.importance}/10, cached)`);
5034
- return { prependContext: `## Relevant Memories\n\n${lines.join('\n')}` + welcomeBack + billingWarning };
5215
+ return { prependContext: stagingBannerBlock + `## Relevant Memories\n\n${lines.join('\n')}` + welcomeBack + billingWarning };
5035
5216
  }
5036
5217
  return undefined;
5037
5218
  }
@@ -5055,7 +5236,7 @@ const plugin = {
5055
5236
  // If subgraph returned no results but we have cache, use cache.
5056
5237
  if (subgraphResults.length === 0) {
5057
5238
  const lines = cachedFacts.slice(0, 8).map((f, i) => `${i + 1}. ${f.text} (importance: ${f.importance}/10, cached)`);
5058
- return { prependContext: `## Relevant Memories\n\n${lines.join('\n')}` + welcomeBack + billingWarning };
5239
+ return { prependContext: stagingBannerBlock + `## Relevant Memories\n\n${lines.join('\n')}` + welcomeBack + billingWarning };
5059
5240
  }
5060
5241
  // 5. Decrypt subgraph results and build reranker input.
5061
5242
  const rerankerCandidates = [];
@@ -5130,7 +5311,7 @@ const plugin = {
5130
5311
  return `${i + 1}. ${typeTag}${m.text} (importance: ${importance}/10, ${age})`;
5131
5312
  });
5132
5313
  const contextString = `## Relevant Memories\n\n${lines.join('\n')}`;
5133
- return { prependContext: contextString + welcomeBack + billingWarning };
5314
+ return { prependContext: stagingBannerBlock + contextString + welcomeBack + billingWarning };
5134
5315
  }
5135
5316
  // --- Server mode (existing behavior) ---
5136
5317
  // 1. Generate word trapdoors from the user prompt.
@@ -5212,7 +5393,7 @@ const plugin = {
5212
5393
  return `${i + 1}. ${m.text} (importance: ${importance}/10, ${age})`;
5213
5394
  });
5214
5395
  const contextString = `## Relevant Memories\n\n${lines.join('\n')}`;
5215
- return { prependContext: contextString + welcomeBack + billingWarning };
5396
+ return { prependContext: stagingBannerBlock + contextString + welcomeBack + billingWarning };
5216
5397
  }
5217
5398
  catch (err) {
5218
5399
  // The hook must NEVER throw -- log and return undefined.
@@ -684,4 +684,6 @@ async function chatCompletionAnthropic(config, messages, maxTokens, temperature,
684
684
  // (Harrier-OSS-v1-270M ONNX model). No API key needed. The native deps +
685
685
  // model are lazy-fetched from a pinned GitHub Release on first call —
686
686
  // see embedding.ts + embedder-loader.ts.
687
- export { generateEmbedding, getEmbeddingDims, getEmbeddingModelId, configureEmbedder } from './embedding.js';
687
+ export { generateEmbedding, getEmbeddingDims, getEmbeddingModelId, configureEmbedder,
688
+ // 3.3.3-rc.1 (#187): pre-pair bundle prefetch
689
+ prefetchEmbedderBundle, } from './embedding.js';
@@ -675,7 +675,7 @@ export function isSubgraphMode() {
675
675
  *
676
676
  * After the v1 env var cleanup, clients only need:
677
677
  * - TOTALRECLAW_RECOVERY_PHRASE -- BIP-39 mnemonic
678
- * - TOTALRECLAW_SERVER_URL -- relay server URL (default: https://api.totalreclaw.xyz)
678
+ * - TOTALRECLAW_SERVER_URL -- relay server URL (source default: https://api-staging.totalreclaw.xyz; stable build: https://api.totalreclaw.xyz — swapped in at publish time per PR #165)
679
679
  * - TOTALRECLAW_SELF_HOSTED -- set "true" to use self-hosted server (default: managed service)
680
680
  *
681
681
  * Chain ID is no longer configurable via env — it is auto-detected from the
@@ -683,7 +683,8 @@ export function isSubgraphMode() {
683
683
  */
684
684
  export function getSubgraphConfig() {
685
685
  return {
686
- relayUrl: CONFIG.serverUrl || 'https://api.totalreclaw.xyz',
686
+ // 3.3.3-rc.1: staging by default in source; stable workflow seds.
687
+ relayUrl: CONFIG.serverUrl || 'https://api-staging.totalreclaw.xyz',
687
688
  mnemonic: CONFIG.recoveryPhrase,
688
689
  cachePath: CONFIG.cachePath,
689
690
  chainId: CONFIG.chainId,
package/embedding.ts CHANGED
@@ -91,6 +91,31 @@ function activeRuntimeConfig(): EmbedderRuntimeConfig {
91
91
  return { cacheRoot: defaultCacheRoot(), rcTag: '0.0.0-dev' };
92
92
  }
93
93
 
94
+ /**
95
+ * 3.3.3-rc.1 (issue #187 — ONNX decouple): prefetch the embedder bundle
96
+ * WITHOUT loading the model into memory. Used to download the
97
+ * ~700 MB tarball pre-pair so the user does not hit the network round-trip
98
+ * mid-conversation. Idempotent — subsequent calls are cache-hit no-ops.
99
+ *
100
+ * Returns:
101
+ * - `'cache_hit'` if the bundle was already extracted + verified.
102
+ * - `'fetched'` if the bundle was downloaded this call.
103
+ * - throws on transport / extraction failure.
104
+ *
105
+ * Pre-flight is the caller's job (disk-space, network reachability) — this
106
+ * function focuses on the cache-resolve + fetch-on-miss path so it can also
107
+ * be reused as a fast cache-validation probe.
108
+ */
109
+ export async function prefetchEmbedderBundle(opts?: { log?: (msg: string) => void }): Promise<'cache_hit' | 'fetched'> {
110
+ const cfg = activeRuntimeConfig();
111
+ const loaded = await loadEmbedder({
112
+ cacheRoot: cfg.cacheRoot,
113
+ rcTag: cfg.rcTag,
114
+ log: opts?.log,
115
+ });
116
+ return loaded.wasFetched ? 'fetched' : 'cache_hit';
117
+ }
118
+
94
119
  /** Lazily initialized state. */
95
120
  let pipelineExtractor: any = null;
96
121
  let autoTokenizer: any = null;