@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/index.ts CHANGED
@@ -81,6 +81,7 @@ import {
81
81
  getEmbeddingDims,
82
82
  getEmbeddingModelId,
83
83
  configureEmbedder,
84
+ prefetchEmbedderBundle,
84
85
  } from './llm-client.js';
85
86
  import {
86
87
  defaultAuthProfilesRoot,
@@ -748,6 +749,38 @@ let firstRunAfterInit = true;
748
749
  */
749
750
  let firstRunWelcomeShown = false;
750
751
 
752
+ /**
753
+ * 3.3.3-rc.1 — RC-mode staging-only banner (PR #165 implementation).
754
+ *
755
+ * Fires ONCE per gateway process when:
756
+ * - the bundled-default `serverUrl` resolves to `api-staging.totalreclaw.xyz`
757
+ * (RC artifact, not stable), AND
758
+ * - the user has NOT overridden via `TOTALRECLAW_SERVER_URL=...` env.
759
+ *
760
+ * Goal: a fresh QA tester can't accidentally use an RC build for real data
761
+ * without seeing a clear "RC = staging, no SLA, may be wiped" warning.
762
+ * One-shot at the first `before_agent_start` whose `prependContext` actually
763
+ * lands on the LLM (3.3.4-rc.1 — see fix below). A fresh gateway restart
764
+ * re-fires it once.
765
+ *
766
+ * 3.3.4-rc.1 fix: through 3.3.3-rc.1 this flag was set to true as soon as
767
+ * the banner BLOCK was built — but multiple hook return paths returned
768
+ * `undefined` (zero-match cases), so the banner block was silently dropped
769
+ * AND the flag was flipped, suppressing all subsequent attempts. Now the
770
+ * flag flips ONLY when a return path actually includes the block in its
771
+ * `prependContext`, via the `markBannerDelivered()` closure.
772
+ */
773
+ let stagingBannerShown = false;
774
+
775
+ /**
776
+ * 3.3.4-rc.1 — operator-facing "this is an RC build" log fires once per
777
+ * gateway process, independent of whether the user-facing banner has
778
+ * been delivered yet. Without this split, the warn-log was tied to the
779
+ * same flag as the user-facing banner and got dropped together when
780
+ * the hook returned `undefined`.
781
+ */
782
+ let stagingBannerLogged = false;
783
+
751
784
  /**
752
785
  * Derive keys from the recovery phrase, load credentials, and register with
753
786
  * the server if this is the first run.
@@ -761,7 +794,9 @@ let firstRunWelcomeShown = false;
761
794
  * directs the caller to `openclaw totalreclaw onboard`.
762
795
  */
763
796
  async function initialize(logger: OpenClawPluginApi['logger']): Promise<void> {
764
- const serverUrl = CONFIG.serverUrl || 'https://api.totalreclaw.xyz';
797
+ // 3.3.3-rc.1: staging is the source default per #165. Stable build-time
798
+ // sed-replace flips `api-staging` -> `api` in dist/.
799
+ const serverUrl = CONFIG.serverUrl || 'https://api-staging.totalreclaw.xyz';
765
800
  let masterPassword = CONFIG.recoveryPhrase;
766
801
 
767
802
  // 3.2.0: if the env var is unset, probe credentials.json for a
@@ -2954,26 +2989,67 @@ const plugin = {
2954
2989
 
2955
2990
  // 3.3.1-rc.22 — wire the lazy-embedder runtime config so the first
2956
2991
  // `generateEmbedding()` call knows where to cache the bundle and
2957
- // which RC's GitHub Release to fetch from. `pluginVersion` may be
2958
- // `null` if package.json is unreadable; the embedder defaults to
2959
- // a "0.0.0-dev" tag in that case.
2960
- try {
2961
- configureEmbedder({
2962
- cacheRoot: CONFIG.embedderCachePath,
2963
- rcTag: pluginVersion ?? '0.0.0-dev',
2964
- });
2965
- } catch (err) {
2966
- const msg = err instanceof Error ? err.message : String(err);
2967
- api.logger.warn(`TotalReclaw: configureEmbedder failed (will use defaults): ${msg}`);
2992
+ // which RC's GitHub Release to fetch from.
2993
+ //
2994
+ // 3.3.4-rc.1 when readPluginVersion() returns null (rare, but
2995
+ // possible if package.json is unreadable inside the OpenClaw
2996
+ // sandbox), we previously passed the literal `'0.0.0-dev'` which
2997
+ // resolves to a 404 GitHub Release URL. Now we let `embedding.ts`
2998
+ // fall back to its `LAST_KNOWN_GOOD_RC_TAG` constant by SKIPPING
2999
+ // the configure call entirely in the null case — the
3000
+ // `activeRuntimeConfig()` helper picks the constant up. This way
3001
+ // the constant lives in one place (embedding.ts) and the orch-
3002
+ // estrator just doesn't fight it.
3003
+ if (pluginVersion) {
3004
+ try {
3005
+ configureEmbedder({
3006
+ cacheRoot: CONFIG.embedderCachePath,
3007
+ rcTag: pluginVersion,
3008
+ });
3009
+ } catch (err) {
3010
+ const msg = err instanceof Error ? err.message : String(err);
3011
+ api.logger.warn(`TotalReclaw: configureEmbedder failed (will use defaults): ${msg}`);
3012
+ }
3013
+ } else {
3014
+ api.logger.warn(
3015
+ 'TotalReclaw: pluginVersion unresolved — embedder will fall back to LAST_KNOWN_GOOD_RC_TAG. ' +
3016
+ 'Investigate package.json resolution; see fs-helpers.readPluginVersion docs.',
3017
+ );
3018
+ }
3019
+
3020
+ // 3.3.3-rc.1 (issue #187 — ONNX decouple): kick off a non-blocking
3021
+ // bundle prefetch so the ~700 MB embedder tarball starts streaming
3022
+ // as soon as the gateway boots, BEFORE the user completes
3023
+ // `totalreclaw_pair`. Decouples the model download from the
3024
+ // pair-completion gate the previous flow imposed via
3025
+ // `requireFullSetup()` -> first `generateEmbedding()` call.
3026
+ // Fire-and-forget — never awaits, never throws on failure (the next
3027
+ // `generateEmbedding()` call retries via the same idempotent path).
3028
+ // Disabled when `TOTALRECLAW_DISABLE_EMBEDDER_PREFETCH=1` (CI / tests
3029
+ // where the network is sandboxed away). The env read lives in
3030
+ // config.ts; we read the resolved CONFIG flag here so this file
3031
+ // stays scanner-clean (no env lookups in index.ts).
3032
+ if (!CONFIG.embedderPrefetchDisabled) {
3033
+ prefetchEmbedderBundle({ log: (msg) => api.logger.info(msg) })
3034
+ .then((result) => {
3035
+ api.logger.info(`TotalReclaw: embedder prefetch ${result === 'fetched' ? 'completed (downloaded bundle)' : 'cache hit'}`);
3036
+ })
3037
+ .catch((err: unknown) => {
3038
+ const msg = err instanceof Error ? err.message : String(err);
3039
+ api.logger.warn(
3040
+ `TotalReclaw: embedder prefetch failed (non-fatal — will retry on first generateEmbedding): ${msg}`,
3041
+ );
3042
+ });
2968
3043
  }
2969
3044
 
2970
3045
  // 3.3.1-rc.22 (rc.21 finding #5): self-heal partial-install marker.
2971
- // The `preinstall` npm script writes `.tr-partial-install`; the
2972
- // `postinstall` script removes it on a successful install. If we
2973
- // have gotten this far the loader did register us — meaning the
2974
- // install succeeded enough to be useful so any lingering marker
2975
- // (e.g. npm ran preinstall but postinstall misfired) is stale.
2976
- // Clear it so the next retry's detector does not see a false positive.
3046
+ // The `preinstall` npm script writes `.tr-partial-install`; clearing
3047
+ // it has been the runtime's job since 3.3.3-rc.1 dropped postinstall.mjs
3048
+ // (OpenClaw scanner blocked the install on the subprocess-spawn import
3049
+ // see 3.3.3-rc.1 PR). If we have gotten this far the loader did
3050
+ // register us meaning the install succeeded enough to be useful —
3051
+ // so any lingering marker is stale. Clear it so the next retry's
3052
+ // detector does not see a false positive.
2977
3053
  //
2978
3054
  // 3.3.1-rc.22 (rc.21 finding #6) — gateway/reload upstream caveat:
2979
3055
  // OpenClaw's config-watcher fires `gateway/reload` when
@@ -3107,11 +3183,39 @@ const plugin = {
3107
3183
  });
3108
3184
  // 3.3.0 — `openclaw totalreclaw pair [generate|import]` attaches
3109
3185
  // alongside the existing `onboard` + `status` subcommands.
3186
+ //
3187
+ // 3.3.4-rc.1 — wire `runRelayPairCli` so the CLI defaults to the
3188
+ // same relay-brokered URL surface the agent tool uses. The local
3189
+ // (gateway-loopback) flow is still available via `--local`. See
3190
+ // pair-cli.ts header for the rationale.
3110
3191
  const { registerPairCli } = await import('./pair-cli.js');
3111
3192
  registerPairCli(program as import('commander').Command, {
3112
3193
  sessionsPath: CONFIG.pairSessionsPath,
3113
3194
  renderPairingUrl: (session) => buildPairingUrl(api, session),
3114
3195
  logger: api.logger,
3196
+ runRelayPairCli: async (cliMode, runOpts) => {
3197
+ const { runRelayPairCli } = await import('./pair-cli-relay.js');
3198
+ return runRelayPairCli(cliMode, {
3199
+ relayBaseUrl: CONFIG.pairRelayUrl,
3200
+ credentialsPath: CREDENTIALS_PATH,
3201
+ onboardingStatePath: CONFIG.onboardingStatePath,
3202
+ logger: api.logger,
3203
+ pluginVersion: pluginVersion ?? '3.3.4-rc.1',
3204
+ deriveScopeAddress: async (mnemonic: string) => {
3205
+ try {
3206
+ return await deriveSmartAccountAddress(mnemonic, CONFIG.chainId);
3207
+ } catch (err) {
3208
+ api.logger.warn(
3209
+ `relay pair-cli: scope-address derivation failed (will retry lazily): ${
3210
+ err instanceof Error ? err.message : String(err)
3211
+ }`,
3212
+ );
3213
+ return undefined;
3214
+ }
3215
+ },
3216
+ ...runOpts,
3217
+ });
3218
+ },
3115
3219
  });
3116
3220
  },
3117
3221
  { commands: ['totalreclaw'] },
@@ -4202,6 +4306,120 @@ const plugin = {
4202
4306
  { name: 'totalreclaw_status' },
4203
4307
  );
4204
4308
 
4309
+ // ---------------------------------------------------------------
4310
+ // Tool: totalreclaw_preload_embedder (3.3.3-rc.1 — issue #187)
4311
+ // ---------------------------------------------------------------
4312
+ //
4313
+ // Decouples the ~700 MB embedder bundle download from the pair-
4314
+ // completion gate. The agent can call this BEFORE
4315
+ // `totalreclaw_pair` to pre-flight disk space, kick off the
4316
+ // download, and surface the completion state. The non-blocking
4317
+ // prefetch in `register()` already starts the download
4318
+ // unconditionally; this tool is an explicit on-demand hook for
4319
+ // agents that want to confirm completion (or trigger a retry on
4320
+ // network failure) without first completing pair.
4321
+ //
4322
+ // Behavior:
4323
+ // - Disk-space pre-flight: refuses if free disk < 500 MB on the
4324
+ // embedder cache mount. Surfaces the path + free bytes so the
4325
+ // user can clear space.
4326
+ // - Triggers `prefetchEmbedderBundle()` (idempotent — cache hit
4327
+ // returns immediately).
4328
+ // - Returns a structured success message with status:
4329
+ // `cache_hit | fetched | failed`.
4330
+ //
4331
+ // Phrase-safety: this tool does NOT touch credentials.json,
4332
+ // mnemonics, or keys. It only touches `~/.totalreclaw/embedder/`
4333
+ // and the GitHub Releases CDN.
4334
+
4335
+ api.registerTool(
4336
+ {
4337
+ name: 'totalreclaw_preload_embedder',
4338
+ label: 'Preload Embedder',
4339
+ description:
4340
+ '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/.',
4341
+ parameters: {
4342
+ type: 'object',
4343
+ properties: {},
4344
+ additionalProperties: false,
4345
+ },
4346
+ async execute() {
4347
+ try {
4348
+ // Disk-space pre-flight: refuse if < 500 MB free on the
4349
+ // embedder cache mount. Best-effort — if statfs fails, we
4350
+ // proceed without the pre-flight rather than blocking.
4351
+ const fsModule = await import('node:fs');
4352
+ const cacheRoot = CONFIG.embedderCachePath;
4353
+ const REQUIRED_BYTES = 500 * 1024 * 1024;
4354
+ try {
4355
+ // Find the deepest existing parent so statfs has a real
4356
+ // mount to measure (loadEmbedder will mkdir under it
4357
+ // anyway).
4358
+ let probeDir = cacheRoot;
4359
+ while (true) {
4360
+ try {
4361
+ fsModule.statSync(probeDir);
4362
+ break;
4363
+ } catch {
4364
+ const parent = nodePath.dirname(probeDir);
4365
+ if (parent === probeDir) break;
4366
+ probeDir = parent;
4367
+ }
4368
+ }
4369
+ const stats = (fsModule as unknown as {
4370
+ statfsSync?: (p: string) => { bavail: bigint | number; bsize: bigint | number };
4371
+ }).statfsSync?.(probeDir);
4372
+ if (stats) {
4373
+ const bavail = typeof stats.bavail === 'bigint' ? Number(stats.bavail) : stats.bavail;
4374
+ const bsize = typeof stats.bsize === 'bigint' ? Number(stats.bsize) : stats.bsize;
4375
+ const freeBytes = bavail * bsize;
4376
+ if (freeBytes > 0 && freeBytes < REQUIRED_BYTES) {
4377
+ const freeMb = Math.round(freeBytes / (1024 * 1024));
4378
+ return {
4379
+ content: [{
4380
+ type: 'text',
4381
+ text: `Insufficient free disk space for embedder bundle. Required: 500 MB. Available at ${probeDir}: ${freeMb} MB. Free up space and retry.`,
4382
+ }],
4383
+ details: { status: 'failed', reason: 'disk_space', free_mb: freeMb, required_mb: 500, cache_root: cacheRoot },
4384
+ };
4385
+ }
4386
+ }
4387
+ } catch {
4388
+ // statfs probe failed — surface a soft warning in logs
4389
+ // but proceed with the download anyway.
4390
+ api.logger.info('totalreclaw_preload_embedder: disk-space probe unavailable, proceeding without pre-flight');
4391
+ }
4392
+
4393
+ // Trigger the prefetch. This is idempotent (cache hit returns
4394
+ // immediately) so it's safe to invoke even when the
4395
+ // background prefetch from register() already completed.
4396
+ const result = await prefetchEmbedderBundle({
4397
+ log: (msg) => api.logger.info(msg),
4398
+ });
4399
+ const human =
4400
+ result === 'fetched'
4401
+ ? `Embedder bundle downloaded and cached at ${cacheRoot}. Subsequent embedding calls run in-memory.`
4402
+ : `Embedder bundle already cached at ${cacheRoot} — no download needed.`;
4403
+ return {
4404
+ content: [{ type: 'text', text: human }],
4405
+ details: { status: result, cache_root: cacheRoot },
4406
+ };
4407
+ } catch (err: unknown) {
4408
+ const message = err instanceof Error ? err.message : String(err);
4409
+ api.logger.error(`totalreclaw_preload_embedder failed: ${message}`);
4410
+ return {
4411
+ content: [{
4412
+ type: 'text',
4413
+ text: `Embedder bundle preload failed: ${humanizeError(message)}. The plugin will retry on first embedding call.`,
4414
+ }],
4415
+ details: { status: 'failed', reason: 'fetch_error', message },
4416
+ };
4417
+ }
4418
+ },
4419
+ },
4420
+ { name: 'totalreclaw_preload_embedder' },
4421
+ );
4422
+
4205
4423
  // ---------------------------------------------------------------
4206
4424
  // Tool: totalreclaw_consolidate
4207
4425
  // ---------------------------------------------------------------
@@ -5672,6 +5890,84 @@ const plugin = {
5672
5890
  return undefined;
5673
5891
  }
5674
5892
 
5893
+ // 3.3.3-rc.1 — RC-staging banner (PR #165 implementation).
5894
+ // Build a one-shot prefix when the bundled default points at staging
5895
+ // AND the user hasn't overridden via env. This prefix is prepended
5896
+ // to whichever context block the rest of the hook produces.
5897
+ //
5898
+ // 3.3.4-rc.1 — fix: previously `stagingBannerShown` was set to
5899
+ // `true` AS SOON AS the block was built. If the rest of the hook
5900
+ // then returned `undefined` (e.g. zero memory matches on the first
5901
+ // turn — multiple paths around lines 6103-6325 do this), the
5902
+ // banner block was silently discarded AND the flag was already
5903
+ // flipped, so subsequent before_agent_start invocations never
5904
+ // reconstructed it. Net effect: QA on 3.3.3-rc.1 (Pedro
5905
+ // 2026-04-30) saw NO banner emitted across an entire conversation
5906
+ // even though the build was bound to staging.
5907
+ //
5908
+ // Fix: build the block on every call until it is actually
5909
+ // delivered (i.e., until at least one return path included it
5910
+ // in `prependContext`). The flag flips at the bottom of this
5911
+ // hook in `markBannerDelivered()` once we know the prependContext
5912
+ // path was taken.
5913
+ let stagingBannerBlock = '';
5914
+ let stagingBannerSuppressed = false;
5915
+ if (!stagingBannerShown) {
5916
+ try {
5917
+ const usingStagingDefault = CONFIG.serverUrl.includes('api-staging.totalreclaw.xyz');
5918
+ const userOverrode = CONFIG.serverUrlEnvOverridden;
5919
+ if (usingStagingDefault && !userOverrode) {
5920
+ stagingBannerBlock =
5921
+ '## ⚠️ TotalReclaw is running in RC / staging mode\n\n' +
5922
+ 'This build is bound to `api-staging.totalreclaw.xyz`. Staging has **no SLA** ' +
5923
+ 'and may be wiped between QA cycles. Do **NOT** use this build for real data.\n\n' +
5924
+ 'For production, install the stable release: `openclaw plugins install ' +
5925
+ '@totalreclaw/totalreclaw` (no `@rc` suffix). To pin a custom server, set ' +
5926
+ '`TOTALRECLAW_SERVER_URL=https://api.totalreclaw.xyz` in your env.\n\n';
5927
+ // Do NOT set stagingBannerShown=true here — see comment above.
5928
+ // Logger emits once per gateway process; this is fine to
5929
+ // gate on the same flag because the logger is operator-
5930
+ // facing, not user-facing.
5931
+ stagingBannerSuppressed = true;
5932
+ } else {
5933
+ // Non-RC artifact OR user override — never fire the banner this
5934
+ // gateway-process lifetime.
5935
+ stagingBannerShown = true;
5936
+ }
5937
+ } catch {
5938
+ // Best-effort; never block session start on banner derivation.
5939
+ stagingBannerShown = true;
5940
+ }
5941
+ }
5942
+ // Operator-facing log: once per process, when we DETECT the
5943
+ // staging build (banner-shown semantics are about user
5944
+ // delivery; this log is independent).
5945
+ if (stagingBannerSuppressed && !stagingBannerLogged) {
5946
+ stagingBannerLogged = true;
5947
+ api.logger.warn(
5948
+ 'TotalReclaw: RC/staging build active (api-staging.totalreclaw.xyz). ' +
5949
+ 'See docs/guides/release-process.md for the RC=staging / stable=production rule.',
5950
+ );
5951
+ }
5952
+ /**
5953
+ * Helper — invoked inline at any `prependContext` site that
5954
+ * wants to lead with the staging banner. Returns the banner
5955
+ * string AND atomically marks the banner as delivered, so
5956
+ * subsequent hook calls in the same gateway-process lifetime
5957
+ * skip re-emission. Returns '' (empty) when no banner is due
5958
+ * (stable build, user override, or already delivered).
5959
+ *
5960
+ * Use this at every prependContext callsite that takes the
5961
+ * banner; do NOT inline `stagingBannerBlock` on its own — the
5962
+ * 3.3.4-rc.1 bug fix requires the marker flip to be coupled
5963
+ * to the actual delivery.
5964
+ */
5965
+ const consumeBannerForPrepend = (): string => {
5966
+ if (stagingBannerBlock === '') return '';
5967
+ stagingBannerShown = true;
5968
+ return stagingBannerBlock;
5969
+ };
5970
+
5675
5971
  await ensureInitialized(api.logger);
5676
5972
 
5677
5973
  // 3.2.0 onboarding pending: emit a non-secret guidance banner so
@@ -5701,6 +5997,7 @@ const plugin = {
5701
5997
  }
5702
5998
  return {
5703
5999
  prependContext:
6000
+ consumeBannerForPrepend() +
5704
6001
  welcomeBlock +
5705
6002
  '## TotalReclaw setup pending\n\n' +
5706
6003
  'TotalReclaw encrypted memory is installed but not yet set up on this machine. ' +
@@ -5802,6 +6099,7 @@ const plugin = {
5802
6099
  api.logger.info(`Digest injection: state=${injectResult.state}`);
5803
6100
  return {
5804
6101
  prependContext:
6102
+ consumeBannerForPrepend() +
5805
6103
  `## Your Memory\n\n${injectResult.promptText}` + welcomeBack + billingWarning,
5806
6104
  };
5807
6105
  }
@@ -5848,7 +6146,7 @@ const plugin = {
5848
6146
  const lines = cachedFacts.slice(0, 8).map((f, i) =>
5849
6147
  `${i + 1}. ${f.text} (importance: ${f.importance}/10, cached)`,
5850
6148
  );
5851
- return { prependContext: `## Relevant Memories\n\n${lines.join('\n')}` + welcomeBack + billingWarning };
6149
+ return { prependContext: consumeBannerForPrepend() + `## Relevant Memories\n\n${lines.join('\n')}` + welcomeBack + billingWarning };
5852
6150
  }
5853
6151
  }
5854
6152
 
@@ -5861,7 +6159,7 @@ const plugin = {
5861
6159
  const lines = cachedFacts.slice(0, 8).map((f, i) =>
5862
6160
  `${i + 1}. ${f.text} (importance: ${f.importance}/10, cached)`,
5863
6161
  );
5864
- return { prependContext: `## Relevant Memories\n\n${lines.join('\n')}` + welcomeBack + billingWarning };
6162
+ return { prependContext: consumeBannerForPrepend() + `## Relevant Memories\n\n${lines.join('\n')}` + welcomeBack + billingWarning };
5865
6163
  }
5866
6164
 
5867
6165
  if (allTrapdoors.length === 0) return undefined;
@@ -5878,7 +6176,7 @@ const plugin = {
5878
6176
  const lines = cachedFacts.slice(0, 8).map((f, i) =>
5879
6177
  `${i + 1}. ${f.text} (importance: ${f.importance}/10, cached)`,
5880
6178
  );
5881
- return { prependContext: `## Relevant Memories\n\n${lines.join('\n')}` + welcomeBack + billingWarning };
6179
+ return { prependContext: consumeBannerForPrepend() + `## Relevant Memories\n\n${lines.join('\n')}` + welcomeBack + billingWarning };
5882
6180
  }
5883
6181
  return undefined;
5884
6182
  }
@@ -5905,7 +6203,7 @@ const plugin = {
5905
6203
  const lines = cachedFacts.slice(0, 8).map((f, i) =>
5906
6204
  `${i + 1}. ${f.text} (importance: ${f.importance}/10, cached)`,
5907
6205
  );
5908
- return { prependContext: `## Relevant Memories\n\n${lines.join('\n')}` + welcomeBack + billingWarning };
6206
+ return { prependContext: consumeBannerForPrepend() + `## Relevant Memories\n\n${lines.join('\n')}` + welcomeBack + billingWarning };
5909
6207
  }
5910
6208
 
5911
6209
  // 5. Decrypt subgraph results and build reranker input.
@@ -5995,7 +6293,7 @@ const plugin = {
5995
6293
  });
5996
6294
  const contextString = `## Relevant Memories\n\n${lines.join('\n')}`;
5997
6295
 
5998
- return { prependContext: contextString + welcomeBack + billingWarning };
6296
+ return { prependContext: consumeBannerForPrepend() + contextString + welcomeBack + billingWarning };
5999
6297
  }
6000
6298
 
6001
6299
  // --- Server mode (existing behavior) ---
@@ -6099,7 +6397,7 @@ const plugin = {
6099
6397
  });
6100
6398
  const contextString = `## Relevant Memories\n\n${lines.join('\n')}`;
6101
6399
 
6102
- return { prependContext: contextString + welcomeBack + billingWarning };
6400
+ return { prependContext: consumeBannerForPrepend() + contextString + welcomeBack + billingWarning };
6103
6401
  } catch (err: unknown) {
6104
6402
  // The hook must NEVER throw -- log and return undefined.
6105
6403
  const message = err instanceof Error ? err.message : String(err);
package/llm-client.ts CHANGED
@@ -911,4 +911,11 @@ async function chatCompletionAnthropic(
911
911
  // (Harrier-OSS-v1-270M ONNX model). No API key needed. The native deps +
912
912
  // model are lazy-fetched from a pinned GitHub Release on first call —
913
913
  // see embedding.ts + embedder-loader.ts.
914
- export { generateEmbedding, getEmbeddingDims, getEmbeddingModelId, configureEmbedder } from './embedding.js';
914
+ export {
915
+ generateEmbedding,
916
+ getEmbeddingDims,
917
+ getEmbeddingModelId,
918
+ configureEmbedder,
919
+ // 3.3.3-rc.1 (#187): pre-pair bundle prefetch
920
+ prefetchEmbedderBundle,
921
+ } from './embedding.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@totalreclaw/totalreclaw",
3
- "version": "3.3.2",
3
+ "version": "3.3.4-rc.1",
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": [
@@ -58,21 +58,20 @@
58
58
  "README.md",
59
59
  "CHANGELOG.md",
60
60
  "CLAWHUB.md",
61
- "skill.json",
62
- "postinstall.mjs"
61
+ "skill.json"
63
62
  ],
64
63
  "scripts": {
65
64
  "build": "rm -rf dist && tsc -p tsconfig.json --noCheck",
66
65
  "verify-tarball": "node ../scripts/verify-tarball.mjs",
67
- "test": "npx tsx manifest-shape.test.ts && npx tsx config-schema.test.ts && npx tsx config.test.ts && npx tsx relay-headers.test.ts && npx tsx scope-address-visible.test.ts && npx tsx llm-profile-reader.test.ts && npx tsx llm-client.test.ts && npx tsx llm-client-retry.test.ts && npx tsx gateway-url.test.ts && npx tsx retype-setscope.test.ts && npx tsx tool-gating.test.ts && npx tsx onboarding-noninteractive.test.ts && npx tsx pair-cli-json.test.ts && npx tsx pair-qr.test.ts && npx tsx pair-remote-client.test.ts && npx tsx qa-bug-report.test.ts && npx tsx nonce-serialization.test.ts && npx tsx phrase-safety-registry.test.ts && npx tsx test_issue_92_onnx_download_ux.test.ts && npx tsx onboard-pair-only.test.ts && npx tsx import-time-smoke.test.ts && npx tsx install-staging-cleanup.test.ts && npx tsx partial-install-detection.test.ts && npx tsx install-reload-idempotency.test.ts && npx tsx json-stdout-cleanliness.test.ts && npx tsx load-manifest.test.ts && npx tsx postinstall-validation.test.ts",
66
+ "test": "npx tsx manifest-shape.test.ts && npx tsx config-schema.test.ts && npx tsx config.test.ts && npx tsx relay-headers.test.ts && npx tsx scope-address-visible.test.ts && npx tsx llm-profile-reader.test.ts && npx tsx llm-client.test.ts && npx tsx llm-client-retry.test.ts && npx tsx gateway-url.test.ts && npx tsx retype-setscope.test.ts && npx tsx tool-gating.test.ts && npx tsx onboarding-noninteractive.test.ts && npx tsx pair-cli-json.test.ts && npx tsx pair-qr.test.ts && npx tsx pair-remote-client.test.ts && npx tsx qa-bug-report.test.ts && npx tsx nonce-serialization.test.ts && npx tsx phrase-safety-registry.test.ts && npx tsx test_issue_92_onnx_download_ux.test.ts && npx tsx onboard-pair-only.test.ts && npx tsx import-time-smoke.test.ts && npx tsx install-staging-cleanup.test.ts && npx tsx partial-install-detection.test.ts && npx tsx install-reload-idempotency.test.ts && npx tsx json-stdout-cleanliness.test.ts && npx tsx load-manifest.test.ts && npx tsx url-binding.test.ts && npx tsx fs-helpers.test.ts && npx tsx pair-cli-default-mode.test.ts && npx tsx embedding-fallback-tag.test.ts && npx tsx staging-banner-gate.test.ts",
68
67
  "smoke:dist": "npx tsx dist-esm-smoke.test.ts",
69
68
  "check-scanner": "node ../scripts/check-scanner.mjs",
70
69
  "check-version-drift": "node ../scripts/check-version-drift.mjs",
70
+ "check-url-binding": "node ../scripts/check-url-binding.mjs",
71
71
  "sync-version": "node ../scripts/sync-version.mjs",
72
72
  "preinstall": "node -e \"try{require('fs').writeFileSync('.tr-partial-install','');}catch{}\"",
73
- "postinstall": "node ./postinstall.mjs",
74
73
  "prepack": "npm run build",
75
- "prepublishOnly": "node ../scripts/check-scanner.mjs && node ../scripts/check-version-drift.mjs"
74
+ "prepublishOnly": "node ../scripts/check-scanner.mjs && node ../scripts/check-version-drift.mjs && node ../scripts/check-url-binding.mjs --release-type=${TOTALRECLAW_RELEASE_TYPE:-rc}"
76
75
  },
77
76
  "openclaw": {
78
77
  "extensions": [