@totalreclaw/totalreclaw 3.3.3-rc.1 → 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/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.3-rc.1
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/dist/embedding.js CHANGED
@@ -53,10 +53,26 @@ 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 };
60
76
  }
61
77
  /**
62
78
  * 3.3.3-rc.1 (issue #187 — ONNX decouple): prefetch the embedder bundle
@@ -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;
package/dist/index.js CHANGED
@@ -511,10 +511,26 @@ let firstRunWelcomeShown = false;
511
511
  *
512
512
  * Goal: a fresh QA tester can't accidentally use an RC build for real data
513
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.
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.
516
524
  */
517
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;
518
534
  /**
519
535
  * Derive keys from the recovery phrase, load credentials, and register with
520
536
  * the server if this is the first run.
@@ -2410,18 +2426,32 @@ const plugin = {
2410
2426
  }
2411
2427
  // 3.3.1-rc.22 — wire the lazy-embedder runtime config so the first
2412
2428
  // `generateEmbedding()` call knows where to cache the bundle and
2413
- // which RC's GitHub Release to fetch from. `pluginVersion` may be
2414
- // `null` if package.json is unreadable; the embedder defaults to
2415
- // a "0.0.0-dev" tag in that case.
2416
- try {
2417
- configureEmbedder({
2418
- cacheRoot: CONFIG.embedderCachePath,
2419
- rcTag: pluginVersion ?? '0.0.0-dev',
2420
- });
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
+ }
2421
2451
  }
2422
- catch (err) {
2423
- const msg = err instanceof Error ? err.message : String(err);
2424
- 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.');
2425
2455
  }
2426
2456
  // 3.3.3-rc.1 (issue #187 — ONNX decouple): kick off a non-blocking
2427
2457
  // bundle prefetch so the ~700 MB embedder tarball starts streaming
@@ -2576,11 +2606,36 @@ const plugin = {
2576
2606
  });
2577
2607
  // 3.3.0 — `openclaw totalreclaw pair [generate|import]` attaches
2578
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.
2579
2614
  const { registerPairCli } = await import('./pair-cli.js');
2580
2615
  registerPairCli(program, {
2581
2616
  sessionsPath: CONFIG.pairSessionsPath,
2582
2617
  renderPairingUrl: (session) => buildPairingUrl(api, session),
2583
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
+ },
2584
2639
  });
2585
2640
  }, { commands: ['totalreclaw'] });
2586
2641
  }
@@ -4991,7 +5046,24 @@ const plugin = {
4991
5046
  // Build a one-shot prefix when the bundled default points at staging
4992
5047
  // AND the user hasn't overridden via env. This prefix is prepended
4993
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.
4994
5065
  let stagingBannerBlock = '';
5066
+ let stagingBannerSuppressed = false;
4995
5067
  if (!stagingBannerShown) {
4996
5068
  try {
4997
5069
  const usingStagingDefault = CONFIG.serverUrl.includes('api-staging.totalreclaw.xyz');
@@ -5004,9 +5076,11 @@ const plugin = {
5004
5076
  'For production, install the stable release: `openclaw plugins install ' +
5005
5077
  '@totalreclaw/totalreclaw` (no `@rc` suffix). To pin a custom server, set ' +
5006
5078
  '`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.');
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;
5010
5084
  }
5011
5085
  else {
5012
5086
  // Non-RC artifact OR user override — never fire the banner this
@@ -5019,6 +5093,33 @@ const plugin = {
5019
5093
  stagingBannerShown = true;
5020
5094
  }
5021
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
+ };
5022
5123
  await ensureInitialized(api.logger);
5023
5124
  // 3.2.0 onboarding pending: emit a non-secret guidance banner so
5024
5125
  // the LLM knows how to respond when the user asks about setup.
@@ -5047,7 +5148,7 @@ const plugin = {
5047
5148
  api.logger.warn(`First-run welcome check failed: ${msg}`);
5048
5149
  }
5049
5150
  return {
5050
- prependContext: stagingBannerBlock +
5151
+ prependContext: consumeBannerForPrepend() +
5051
5152
  welcomeBlock +
5052
5153
  '## TotalReclaw setup pending\n\n' +
5053
5154
  'TotalReclaw encrypted memory is installed but not yet set up on this machine. ' +
@@ -5146,7 +5247,7 @@ const plugin = {
5146
5247
  if (injectResult.promptText) {
5147
5248
  api.logger.info(`Digest injection: state=${injectResult.state}`);
5148
5249
  return {
5149
- prependContext: stagingBannerBlock +
5250
+ prependContext: consumeBannerForPrepend() +
5150
5251
  `## Your Memory\n\n${injectResult.promptText}` + welcomeBack + billingWarning,
5151
5252
  };
5152
5253
  }
@@ -5188,7 +5289,7 @@ const plugin = {
5188
5289
  const querySimilarity = cosineSimilarity(queryEmbedding, lastQueryEmbedding);
5189
5290
  if (querySimilarity > SEMANTIC_SKIP_THRESHOLD) {
5190
5291
  const lines = cachedFacts.slice(0, 8).map((f, i) => `${i + 1}. ${f.text} (importance: ${f.importance}/10, cached)`);
5191
- return { prependContext: stagingBannerBlock + `## Relevant Memories\n\n${lines.join('\n')}` + welcomeBack + billingWarning };
5292
+ return { prependContext: consumeBannerForPrepend() + `## Relevant Memories\n\n${lines.join('\n')}` + welcomeBack + billingWarning };
5192
5293
  }
5193
5294
  }
5194
5295
  // 3. Merge trapdoors — always include word trapdoors for small-dataset coverage.
@@ -5197,7 +5298,7 @@ const plugin = {
5197
5298
  // If we have cached facts and no trapdoors, return cached facts.
5198
5299
  if (allTrapdoors.length === 0 && cachedFacts.length > 0) {
5199
5300
  const lines = cachedFacts.slice(0, 8).map((f, i) => `${i + 1}. ${f.text} (importance: ${f.importance}/10, cached)`);
5200
- return { prependContext: stagingBannerBlock + `## Relevant Memories\n\n${lines.join('\n')}` + welcomeBack + billingWarning };
5301
+ return { prependContext: consumeBannerForPrepend() + `## Relevant Memories\n\n${lines.join('\n')}` + welcomeBack + billingWarning };
5201
5302
  }
5202
5303
  if (allTrapdoors.length === 0)
5203
5304
  return undefined;
@@ -5212,7 +5313,7 @@ const plugin = {
5212
5313
  // Subgraph query failed -- fall back to cached facts if available.
5213
5314
  if (cachedFacts.length > 0) {
5214
5315
  const lines = cachedFacts.slice(0, 8).map((f, i) => `${i + 1}. ${f.text} (importance: ${f.importance}/10, cached)`);
5215
- return { prependContext: stagingBannerBlock + `## Relevant Memories\n\n${lines.join('\n')}` + welcomeBack + billingWarning };
5316
+ return { prependContext: consumeBannerForPrepend() + `## Relevant Memories\n\n${lines.join('\n')}` + welcomeBack + billingWarning };
5216
5317
  }
5217
5318
  return undefined;
5218
5319
  }
@@ -5236,7 +5337,7 @@ const plugin = {
5236
5337
  // If subgraph returned no results but we have cache, use cache.
5237
5338
  if (subgraphResults.length === 0) {
5238
5339
  const lines = cachedFacts.slice(0, 8).map((f, i) => `${i + 1}. ${f.text} (importance: ${f.importance}/10, cached)`);
5239
- return { prependContext: stagingBannerBlock + `## Relevant Memories\n\n${lines.join('\n')}` + welcomeBack + billingWarning };
5340
+ return { prependContext: consumeBannerForPrepend() + `## Relevant Memories\n\n${lines.join('\n')}` + welcomeBack + billingWarning };
5240
5341
  }
5241
5342
  // 5. Decrypt subgraph results and build reranker input.
5242
5343
  const rerankerCandidates = [];
@@ -5311,7 +5412,7 @@ const plugin = {
5311
5412
  return `${i + 1}. ${typeTag}${m.text} (importance: ${importance}/10, ${age})`;
5312
5413
  });
5313
5414
  const contextString = `## Relevant Memories\n\n${lines.join('\n')}`;
5314
- return { prependContext: stagingBannerBlock + contextString + welcomeBack + billingWarning };
5415
+ return { prependContext: consumeBannerForPrepend() + contextString + welcomeBack + billingWarning };
5315
5416
  }
5316
5417
  // --- Server mode (existing behavior) ---
5317
5418
  // 1. Generate word trapdoors from the user prompt.
@@ -5393,7 +5494,7 @@ const plugin = {
5393
5494
  return `${i + 1}. ${m.text} (importance: ${importance}/10, ${age})`;
5394
5495
  });
5395
5496
  const contextString = `## Relevant Memories\n\n${lines.join('\n')}`;
5396
- return { prependContext: stagingBannerBlock + contextString + welcomeBack + billingWarning };
5497
+ return { prependContext: consumeBannerForPrepend() + contextString + welcomeBack + billingWarning };
5397
5498
  }
5398
5499
  catch (err) {
5399
5500
  // The hook must NEVER throw -- log and return undefined.