@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/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,36 @@ 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` whose `prependContext` actually
515
+ * lands on the LLM (3.3.4-rc.1 — see fix below). A fresh gateway restart
516
+ * re-fires it once.
517
+ *
518
+ * 3.3.4-rc.1 fix: through 3.3.3-rc.1 this flag was set to true as soon as
519
+ * the banner BLOCK was built — but multiple hook return paths returned
520
+ * `undefined` (zero-match cases), so the banner block was silently dropped
521
+ * AND the flag was flipped, suppressing all subsequent attempts. Now the
522
+ * flag flips ONLY when a return path actually includes the block in its
523
+ * `prependContext`, via the `markBannerDelivered()` closure.
524
+ */
525
+ let stagingBannerShown = false;
526
+ /**
527
+ * 3.3.4-rc.1 — operator-facing "this is an RC build" log fires once per
528
+ * gateway process, independent of whether the user-facing banner has
529
+ * been delivered yet. Without this split, the warn-log was tied to the
530
+ * same flag as the user-facing banner and got dropped together when
531
+ * the hook returned `undefined`.
532
+ */
533
+ let stagingBannerLogged = false;
504
534
  /**
505
535
  * Derive keys from the recovery phrase, load credentials, and register with
506
536
  * the server if this is the first run.
@@ -514,7 +544,9 @@ let firstRunWelcomeShown = false;
514
544
  * directs the caller to `openclaw totalreclaw onboard`.
515
545
  */
516
546
  async function initialize(logger) {
517
- const serverUrl = CONFIG.serverUrl || 'https://api.totalreclaw.xyz';
547
+ // 3.3.3-rc.1: staging is the source default per #165. Stable build-time
548
+ // sed-replace flips `api-staging` -> `api` in dist/.
549
+ const serverUrl = CONFIG.serverUrl || 'https://api-staging.totalreclaw.xyz';
518
550
  let masterPassword = CONFIG.recoveryPhrase;
519
551
  // 3.2.0: if the env var is unset, probe credentials.json for a
520
552
  // pre-existing mnemonic (written either by the CLI wizard on this machine
@@ -2394,26 +2426,63 @@ const plugin = {
2394
2426
  }
2395
2427
  // 3.3.1-rc.22 — wire the lazy-embedder runtime config so the first
2396
2428
  // `generateEmbedding()` call knows where to cache the bundle and
2397
- // which RC's GitHub Release to fetch from. `pluginVersion` may be
2398
- // `null` if package.json is unreadable; the embedder defaults to
2399
- // a "0.0.0-dev" tag in that case.
2400
- try {
2401
- configureEmbedder({
2402
- cacheRoot: CONFIG.embedderCachePath,
2403
- rcTag: pluginVersion ?? '0.0.0-dev',
2404
- });
2429
+ // which RC's GitHub Release to fetch from.
2430
+ //
2431
+ // 3.3.4-rc.1 when readPluginVersion() returns null (rare, but
2432
+ // possible if package.json is unreadable inside the OpenClaw
2433
+ // sandbox), we previously passed the literal `'0.0.0-dev'` which
2434
+ // resolves to a 404 GitHub Release URL. Now we let `embedding.ts`
2435
+ // fall back to its `LAST_KNOWN_GOOD_RC_TAG` constant by SKIPPING
2436
+ // the configure call entirely in the null case — the
2437
+ // `activeRuntimeConfig()` helper picks the constant up. This way
2438
+ // the constant lives in one place (embedding.ts) and the orch-
2439
+ // estrator just doesn't fight it.
2440
+ if (pluginVersion) {
2441
+ try {
2442
+ configureEmbedder({
2443
+ cacheRoot: CONFIG.embedderCachePath,
2444
+ rcTag: pluginVersion,
2445
+ });
2446
+ }
2447
+ catch (err) {
2448
+ const msg = err instanceof Error ? err.message : String(err);
2449
+ api.logger.warn(`TotalReclaw: configureEmbedder failed (will use defaults): ${msg}`);
2450
+ }
2405
2451
  }
2406
- catch (err) {
2407
- const msg = err instanceof Error ? err.message : String(err);
2408
- api.logger.warn(`TotalReclaw: configureEmbedder failed (will use defaults): ${msg}`);
2452
+ else {
2453
+ api.logger.warn('TotalReclaw: pluginVersion unresolved embedder will fall back to LAST_KNOWN_GOOD_RC_TAG. ' +
2454
+ 'Investigate package.json resolution; see fs-helpers.readPluginVersion docs.');
2455
+ }
2456
+ // 3.3.3-rc.1 (issue #187 — ONNX decouple): kick off a non-blocking
2457
+ // bundle prefetch so the ~700 MB embedder tarball starts streaming
2458
+ // as soon as the gateway boots, BEFORE the user completes
2459
+ // `totalreclaw_pair`. Decouples the model download from the
2460
+ // pair-completion gate the previous flow imposed via
2461
+ // `requireFullSetup()` -> first `generateEmbedding()` call.
2462
+ // Fire-and-forget — never awaits, never throws on failure (the next
2463
+ // `generateEmbedding()` call retries via the same idempotent path).
2464
+ // Disabled when `TOTALRECLAW_DISABLE_EMBEDDER_PREFETCH=1` (CI / tests
2465
+ // where the network is sandboxed away). The env read lives in
2466
+ // config.ts; we read the resolved CONFIG flag here so this file
2467
+ // stays scanner-clean (no env lookups in index.ts).
2468
+ if (!CONFIG.embedderPrefetchDisabled) {
2469
+ prefetchEmbedderBundle({ log: (msg) => api.logger.info(msg) })
2470
+ .then((result) => {
2471
+ api.logger.info(`TotalReclaw: embedder prefetch ${result === 'fetched' ? 'completed (downloaded bundle)' : 'cache hit'}`);
2472
+ })
2473
+ .catch((err) => {
2474
+ const msg = err instanceof Error ? err.message : String(err);
2475
+ api.logger.warn(`TotalReclaw: embedder prefetch failed (non-fatal — will retry on first generateEmbedding): ${msg}`);
2476
+ });
2409
2477
  }
2410
2478
  // 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.
2479
+ // The `preinstall` npm script writes `.tr-partial-install`; clearing
2480
+ // it has been the runtime's job since 3.3.3-rc.1 dropped postinstall.mjs
2481
+ // (OpenClaw scanner blocked the install on the subprocess-spawn import
2482
+ // see 3.3.3-rc.1 PR). If we have gotten this far the loader did
2483
+ // register us meaning the install succeeded enough to be useful —
2484
+ // so any lingering marker is stale. Clear it so the next retry's
2485
+ // detector does not see a false positive.
2417
2486
  //
2418
2487
  // 3.3.1-rc.22 (rc.21 finding #6) — gateway/reload upstream caveat:
2419
2488
  // OpenClaw's config-watcher fires `gateway/reload` when
@@ -2537,11 +2606,36 @@ const plugin = {
2537
2606
  });
2538
2607
  // 3.3.0 — `openclaw totalreclaw pair [generate|import]` attaches
2539
2608
  // alongside the existing `onboard` + `status` subcommands.
2609
+ //
2610
+ // 3.3.4-rc.1 — wire `runRelayPairCli` so the CLI defaults to the
2611
+ // same relay-brokered URL surface the agent tool uses. The local
2612
+ // (gateway-loopback) flow is still available via `--local`. See
2613
+ // pair-cli.ts header for the rationale.
2540
2614
  const { registerPairCli } = await import('./pair-cli.js');
2541
2615
  registerPairCli(program, {
2542
2616
  sessionsPath: CONFIG.pairSessionsPath,
2543
2617
  renderPairingUrl: (session) => buildPairingUrl(api, session),
2544
2618
  logger: api.logger,
2619
+ runRelayPairCli: async (cliMode, runOpts) => {
2620
+ const { runRelayPairCli } = await import('./pair-cli-relay.js');
2621
+ return runRelayPairCli(cliMode, {
2622
+ relayBaseUrl: CONFIG.pairRelayUrl,
2623
+ credentialsPath: CREDENTIALS_PATH,
2624
+ onboardingStatePath: CONFIG.onboardingStatePath,
2625
+ logger: api.logger,
2626
+ pluginVersion: pluginVersion ?? '3.3.4-rc.1',
2627
+ deriveScopeAddress: async (mnemonic) => {
2628
+ try {
2629
+ return await deriveSmartAccountAddress(mnemonic, CONFIG.chainId);
2630
+ }
2631
+ catch (err) {
2632
+ api.logger.warn(`relay pair-cli: scope-address derivation failed (will retry lazily): ${err instanceof Error ? err.message : String(err)}`);
2633
+ return undefined;
2634
+ }
2635
+ },
2636
+ ...runOpts,
2637
+ });
2638
+ },
2545
2639
  });
2546
2640
  }, { commands: ['totalreclaw'] });
2547
2641
  }
@@ -3509,6 +3603,114 @@ const plugin = {
3509
3603
  },
3510
3604
  }, { name: 'totalreclaw_status' });
3511
3605
  // ---------------------------------------------------------------
3606
+ // Tool: totalreclaw_preload_embedder (3.3.3-rc.1 — issue #187)
3607
+ // ---------------------------------------------------------------
3608
+ //
3609
+ // Decouples the ~700 MB embedder bundle download from the pair-
3610
+ // completion gate. The agent can call this BEFORE
3611
+ // `totalreclaw_pair` to pre-flight disk space, kick off the
3612
+ // download, and surface the completion state. The non-blocking
3613
+ // prefetch in `register()` already starts the download
3614
+ // unconditionally; this tool is an explicit on-demand hook for
3615
+ // agents that want to confirm completion (or trigger a retry on
3616
+ // network failure) without first completing pair.
3617
+ //
3618
+ // Behavior:
3619
+ // - Disk-space pre-flight: refuses if free disk < 500 MB on the
3620
+ // embedder cache mount. Surfaces the path + free bytes so the
3621
+ // user can clear space.
3622
+ // - Triggers `prefetchEmbedderBundle()` (idempotent — cache hit
3623
+ // returns immediately).
3624
+ // - Returns a structured success message with status:
3625
+ // `cache_hit | fetched | failed`.
3626
+ //
3627
+ // Phrase-safety: this tool does NOT touch credentials.json,
3628
+ // mnemonics, or keys. It only touches `~/.totalreclaw/embedder/`
3629
+ // and the GitHub Releases CDN.
3630
+ api.registerTool({
3631
+ name: 'totalreclaw_preload_embedder',
3632
+ label: 'Preload Embedder',
3633
+ 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/.',
3634
+ parameters: {
3635
+ type: 'object',
3636
+ properties: {},
3637
+ additionalProperties: false,
3638
+ },
3639
+ async execute() {
3640
+ try {
3641
+ // Disk-space pre-flight: refuse if < 500 MB free on the
3642
+ // embedder cache mount. Best-effort — if statfs fails, we
3643
+ // proceed without the pre-flight rather than blocking.
3644
+ const fsModule = await import('node:fs');
3645
+ const cacheRoot = CONFIG.embedderCachePath;
3646
+ const REQUIRED_BYTES = 500 * 1024 * 1024;
3647
+ try {
3648
+ // Find the deepest existing parent so statfs has a real
3649
+ // mount to measure (loadEmbedder will mkdir under it
3650
+ // anyway).
3651
+ let probeDir = cacheRoot;
3652
+ while (true) {
3653
+ try {
3654
+ fsModule.statSync(probeDir);
3655
+ break;
3656
+ }
3657
+ catch {
3658
+ const parent = nodePath.dirname(probeDir);
3659
+ if (parent === probeDir)
3660
+ break;
3661
+ probeDir = parent;
3662
+ }
3663
+ }
3664
+ const stats = fsModule.statfsSync?.(probeDir);
3665
+ if (stats) {
3666
+ const bavail = typeof stats.bavail === 'bigint' ? Number(stats.bavail) : stats.bavail;
3667
+ const bsize = typeof stats.bsize === 'bigint' ? Number(stats.bsize) : stats.bsize;
3668
+ const freeBytes = bavail * bsize;
3669
+ if (freeBytes > 0 && freeBytes < REQUIRED_BYTES) {
3670
+ const freeMb = Math.round(freeBytes / (1024 * 1024));
3671
+ return {
3672
+ content: [{
3673
+ type: 'text',
3674
+ text: `Insufficient free disk space for embedder bundle. Required: 500 MB. Available at ${probeDir}: ${freeMb} MB. Free up space and retry.`,
3675
+ }],
3676
+ details: { status: 'failed', reason: 'disk_space', free_mb: freeMb, required_mb: 500, cache_root: cacheRoot },
3677
+ };
3678
+ }
3679
+ }
3680
+ }
3681
+ catch {
3682
+ // statfs probe failed — surface a soft warning in logs
3683
+ // but proceed with the download anyway.
3684
+ api.logger.info('totalreclaw_preload_embedder: disk-space probe unavailable, proceeding without pre-flight');
3685
+ }
3686
+ // Trigger the prefetch. This is idempotent (cache hit returns
3687
+ // immediately) so it's safe to invoke even when the
3688
+ // background prefetch from register() already completed.
3689
+ const result = await prefetchEmbedderBundle({
3690
+ log: (msg) => api.logger.info(msg),
3691
+ });
3692
+ const human = result === 'fetched'
3693
+ ? `Embedder bundle downloaded and cached at ${cacheRoot}. Subsequent embedding calls run in-memory.`
3694
+ : `Embedder bundle already cached at ${cacheRoot} — no download needed.`;
3695
+ return {
3696
+ content: [{ type: 'text', text: human }],
3697
+ details: { status: result, cache_root: cacheRoot },
3698
+ };
3699
+ }
3700
+ catch (err) {
3701
+ const message = err instanceof Error ? err.message : String(err);
3702
+ api.logger.error(`totalreclaw_preload_embedder failed: ${message}`);
3703
+ return {
3704
+ content: [{
3705
+ type: 'text',
3706
+ text: `Embedder bundle preload failed: ${humanizeError(message)}. The plugin will retry on first embedding call.`,
3707
+ }],
3708
+ details: { status: 'failed', reason: 'fetch_error', message },
3709
+ };
3710
+ }
3711
+ },
3712
+ }, { name: 'totalreclaw_preload_embedder' });
3713
+ // ---------------------------------------------------------------
3512
3714
  // Tool: totalreclaw_consolidate
3513
3715
  // ---------------------------------------------------------------
3514
3716
  api.registerTool({
@@ -4840,6 +5042,84 @@ const plugin = {
4840
5042
  if (!evt?.prompt || evt.prompt.length < 5) {
4841
5043
  return undefined;
4842
5044
  }
5045
+ // 3.3.3-rc.1 — RC-staging banner (PR #165 implementation).
5046
+ // Build a one-shot prefix when the bundled default points at staging
5047
+ // AND the user hasn't overridden via env. This prefix is prepended
5048
+ // to whichever context block the rest of the hook produces.
5049
+ //
5050
+ // 3.3.4-rc.1 — fix: previously `stagingBannerShown` was set to
5051
+ // `true` AS SOON AS the block was built. If the rest of the hook
5052
+ // then returned `undefined` (e.g. zero memory matches on the first
5053
+ // turn — multiple paths around lines 6103-6325 do this), the
5054
+ // banner block was silently discarded AND the flag was already
5055
+ // flipped, so subsequent before_agent_start invocations never
5056
+ // reconstructed it. Net effect: QA on 3.3.3-rc.1 (Pedro
5057
+ // 2026-04-30) saw NO banner emitted across an entire conversation
5058
+ // even though the build was bound to staging.
5059
+ //
5060
+ // Fix: build the block on every call until it is actually
5061
+ // delivered (i.e., until at least one return path included it
5062
+ // in `prependContext`). The flag flips at the bottom of this
5063
+ // hook in `markBannerDelivered()` once we know the prependContext
5064
+ // path was taken.
5065
+ let stagingBannerBlock = '';
5066
+ let stagingBannerSuppressed = false;
5067
+ if (!stagingBannerShown) {
5068
+ try {
5069
+ const usingStagingDefault = CONFIG.serverUrl.includes('api-staging.totalreclaw.xyz');
5070
+ const userOverrode = CONFIG.serverUrlEnvOverridden;
5071
+ if (usingStagingDefault && !userOverrode) {
5072
+ stagingBannerBlock =
5073
+ '## ⚠️ TotalReclaw is running in RC / staging mode\n\n' +
5074
+ 'This build is bound to `api-staging.totalreclaw.xyz`. Staging has **no SLA** ' +
5075
+ 'and may be wiped between QA cycles. Do **NOT** use this build for real data.\n\n' +
5076
+ 'For production, install the stable release: `openclaw plugins install ' +
5077
+ '@totalreclaw/totalreclaw` (no `@rc` suffix). To pin a custom server, set ' +
5078
+ '`TOTALRECLAW_SERVER_URL=https://api.totalreclaw.xyz` in your env.\n\n';
5079
+ // Do NOT set stagingBannerShown=true here — see comment above.
5080
+ // Logger emits once per gateway process; this is fine to
5081
+ // gate on the same flag because the logger is operator-
5082
+ // facing, not user-facing.
5083
+ stagingBannerSuppressed = true;
5084
+ }
5085
+ else {
5086
+ // Non-RC artifact OR user override — never fire the banner this
5087
+ // gateway-process lifetime.
5088
+ stagingBannerShown = true;
5089
+ }
5090
+ }
5091
+ catch {
5092
+ // Best-effort; never block session start on banner derivation.
5093
+ stagingBannerShown = true;
5094
+ }
5095
+ }
5096
+ // Operator-facing log: once per process, when we DETECT the
5097
+ // staging build (banner-shown semantics are about user
5098
+ // delivery; this log is independent).
5099
+ if (stagingBannerSuppressed && !stagingBannerLogged) {
5100
+ stagingBannerLogged = true;
5101
+ api.logger.warn('TotalReclaw: RC/staging build active (api-staging.totalreclaw.xyz). ' +
5102
+ 'See docs/guides/release-process.md for the RC=staging / stable=production rule.');
5103
+ }
5104
+ /**
5105
+ * Helper — invoked inline at any `prependContext` site that
5106
+ * wants to lead with the staging banner. Returns the banner
5107
+ * string AND atomically marks the banner as delivered, so
5108
+ * subsequent hook calls in the same gateway-process lifetime
5109
+ * skip re-emission. Returns '' (empty) when no banner is due
5110
+ * (stable build, user override, or already delivered).
5111
+ *
5112
+ * Use this at every prependContext callsite that takes the
5113
+ * banner; do NOT inline `stagingBannerBlock` on its own — the
5114
+ * 3.3.4-rc.1 bug fix requires the marker flip to be coupled
5115
+ * to the actual delivery.
5116
+ */
5117
+ const consumeBannerForPrepend = () => {
5118
+ if (stagingBannerBlock === '')
5119
+ return '';
5120
+ stagingBannerShown = true;
5121
+ return stagingBannerBlock;
5122
+ };
4843
5123
  await ensureInitialized(api.logger);
4844
5124
  // 3.2.0 onboarding pending: emit a non-secret guidance banner so
4845
5125
  // the LLM knows how to respond when the user asks about setup.
@@ -4868,7 +5148,8 @@ const plugin = {
4868
5148
  api.logger.warn(`First-run welcome check failed: ${msg}`);
4869
5149
  }
4870
5150
  return {
4871
- prependContext: welcomeBlock +
5151
+ prependContext: consumeBannerForPrepend() +
5152
+ welcomeBlock +
4872
5153
  '## TotalReclaw setup pending\n\n' +
4873
5154
  'TotalReclaw encrypted memory is installed but not yet set up on this machine. ' +
4874
5155
  'If the user asks about memory features or wants to configure TotalReclaw, ' +
@@ -4966,7 +5247,8 @@ const plugin = {
4966
5247
  if (injectResult.promptText) {
4967
5248
  api.logger.info(`Digest injection: state=${injectResult.state}`);
4968
5249
  return {
4969
- prependContext: `## Your Memory\n\n${injectResult.promptText}` + welcomeBack + billingWarning,
5250
+ prependContext: consumeBannerForPrepend() +
5251
+ `## Your Memory\n\n${injectResult.promptText}` + welcomeBack + billingWarning,
4970
5252
  };
4971
5253
  }
4972
5254
  }
@@ -5007,7 +5289,7 @@ const plugin = {
5007
5289
  const querySimilarity = cosineSimilarity(queryEmbedding, lastQueryEmbedding);
5008
5290
  if (querySimilarity > SEMANTIC_SKIP_THRESHOLD) {
5009
5291
  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 };
5292
+ return { prependContext: consumeBannerForPrepend() + `## Relevant Memories\n\n${lines.join('\n')}` + welcomeBack + billingWarning };
5011
5293
  }
5012
5294
  }
5013
5295
  // 3. Merge trapdoors — always include word trapdoors for small-dataset coverage.
@@ -5016,7 +5298,7 @@ const plugin = {
5016
5298
  // If we have cached facts and no trapdoors, return cached facts.
5017
5299
  if (allTrapdoors.length === 0 && cachedFacts.length > 0) {
5018
5300
  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 };
5301
+ return { prependContext: consumeBannerForPrepend() + `## Relevant Memories\n\n${lines.join('\n')}` + welcomeBack + billingWarning };
5020
5302
  }
5021
5303
  if (allTrapdoors.length === 0)
5022
5304
  return undefined;
@@ -5031,7 +5313,7 @@ const plugin = {
5031
5313
  // Subgraph query failed -- fall back to cached facts if available.
5032
5314
  if (cachedFacts.length > 0) {
5033
5315
  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 };
5316
+ return { prependContext: consumeBannerForPrepend() + `## Relevant Memories\n\n${lines.join('\n')}` + welcomeBack + billingWarning };
5035
5317
  }
5036
5318
  return undefined;
5037
5319
  }
@@ -5055,7 +5337,7 @@ const plugin = {
5055
5337
  // If subgraph returned no results but we have cache, use cache.
5056
5338
  if (subgraphResults.length === 0) {
5057
5339
  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 };
5340
+ return { prependContext: consumeBannerForPrepend() + `## Relevant Memories\n\n${lines.join('\n')}` + welcomeBack + billingWarning };
5059
5341
  }
5060
5342
  // 5. Decrypt subgraph results and build reranker input.
5061
5343
  const rerankerCandidates = [];
@@ -5130,7 +5412,7 @@ const plugin = {
5130
5412
  return `${i + 1}. ${typeTag}${m.text} (importance: ${importance}/10, ${age})`;
5131
5413
  });
5132
5414
  const contextString = `## Relevant Memories\n\n${lines.join('\n')}`;
5133
- return { prependContext: contextString + welcomeBack + billingWarning };
5415
+ return { prependContext: consumeBannerForPrepend() + contextString + welcomeBack + billingWarning };
5134
5416
  }
5135
5417
  // --- Server mode (existing behavior) ---
5136
5418
  // 1. Generate word trapdoors from the user prompt.
@@ -5212,7 +5494,7 @@ const plugin = {
5212
5494
  return `${i + 1}. ${m.text} (importance: ${importance}/10, ${age})`;
5213
5495
  });
5214
5496
  const contextString = `## Relevant Memories\n\n${lines.join('\n')}`;
5215
- return { prependContext: contextString + welcomeBack + billingWarning };
5497
+ return { prependContext: consumeBannerForPrepend() + contextString + welcomeBack + billingWarning };
5216
5498
  }
5217
5499
  catch (err) {
5218
5500
  // 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';