@totalreclaw/totalreclaw 3.3.3-rc.1 → 3.3.4-rc.2
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 +18 -6
- package/dist/embedding.js +17 -1
- package/dist/fs-helpers.js +50 -4
- package/dist/index.js +170 -25
- package/dist/pair-cli-relay.js +278 -0
- package/dist/pair-cli.js +85 -17
- package/embedding.ts +18 -1
- package/fs-helpers.ts +49 -5
- package/index.ts +187 -27
- package/package.json +3 -2
- package/pair-cli-relay.ts +336 -0
- package/pair-cli.ts +118 -18
- package/skill.json +1 -1
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.
|
|
4
|
+
version: 3.3.4-rc.2
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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:
|
|
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
|
package/dist/fs-helpers.js
CHANGED
|
@@ -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
|
-
* -
|
|
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
|
-
|
|
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`
|
|
515
|
-
*
|
|
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.
|
|
2414
|
-
//
|
|
2415
|
-
//
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
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
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
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
|
}
|
|
@@ -4631,9 +4686,23 @@ const plugin = {
|
|
|
4631
4686
|
// Background task — writes credentials.json + flips state when
|
|
4632
4687
|
// the browser completes the flow. Tool handler returns
|
|
4633
4688
|
// immediately so the agent can tell the user the URL + PIN.
|
|
4689
|
+
//
|
|
4690
|
+
// 3.3.4-rc.2 (Pedro QA — pair flow stuck-session) — wrap the
|
|
4691
|
+
// WS-await in a 60s hard timeout. The relay drops sessions on
|
|
4692
|
+
// `ws_close` (separately patched relay-side); without this
|
|
4693
|
+
// bound the background task hangs in `waitNextMessage` for the
|
|
4694
|
+
// full 5-minute session TTL before resolving, and downstream
|
|
4695
|
+
// tooling that polls the gateway for completion sees stale
|
|
4696
|
+
// `processing` state at 129s/159s/189s. 60s is the user-side
|
|
4697
|
+
// deadline we surface in chat: long enough for a slow scan-
|
|
4698
|
+
// and-paste, short enough that a fresh URL request is the
|
|
4699
|
+
// obvious next step. Structured `timed_out` error in the log
|
|
4700
|
+
// lets ops grep for the failure mode independently of generic
|
|
4701
|
+
// ws-close errors.
|
|
4702
|
+
const PAIR_TOOL_HARD_TIMEOUT_MS = 60_000;
|
|
4634
4703
|
void (async () => {
|
|
4635
4704
|
try {
|
|
4636
|
-
|
|
4705
|
+
const phraseUploadPromise = awaitPhraseUpload(remoteSession, {
|
|
4637
4706
|
phraseValidator: (p) => validateMnemonic(p, wordlist),
|
|
4638
4707
|
completePairing: async ({ mnemonic }) => {
|
|
4639
4708
|
try {
|
|
@@ -4673,7 +4742,37 @@ const plugin = {
|
|
|
4673
4742
|
return { state: 'error', error: msg };
|
|
4674
4743
|
}
|
|
4675
4744
|
},
|
|
4745
|
+
// 3.3.4-rc.2 — also pass through to awaitPhraseUpload so its
|
|
4746
|
+
// internal `waitNextMessage` timer matches the outer race.
|
|
4747
|
+
timeoutMs: PAIR_TOOL_HARD_TIMEOUT_MS,
|
|
4748
|
+
});
|
|
4749
|
+
// 3.3.4-rc.2 — outer Promise.race guard. Resolves to a
|
|
4750
|
+
// sentinel ({ status: 'timed_out', ... }) so the catch
|
|
4751
|
+
// handler can distinguish a hard-timeout from a generic
|
|
4752
|
+
// ws-close error and surface it explicitly.
|
|
4753
|
+
const TIMEOUT_SENTINEL = {
|
|
4754
|
+
status: 'timed_out',
|
|
4755
|
+
message: `Pair flow timed out (${PAIR_TOOL_HARD_TIMEOUT_MS / 1000}s) — generate a new URL with totalreclaw_pair.`,
|
|
4756
|
+
};
|
|
4757
|
+
let hardTimer;
|
|
4758
|
+
const hardTimeoutPromise = new Promise((resolve) => {
|
|
4759
|
+
hardTimer = setTimeout(() => resolve(TIMEOUT_SENTINEL), PAIR_TOOL_HARD_TIMEOUT_MS);
|
|
4676
4760
|
});
|
|
4761
|
+
try {
|
|
4762
|
+
const raced = await Promise.race([
|
|
4763
|
+
phraseUploadPromise,
|
|
4764
|
+
hardTimeoutPromise,
|
|
4765
|
+
]);
|
|
4766
|
+
if (raced &&
|
|
4767
|
+
typeof raced === 'object' &&
|
|
4768
|
+
raced.status === 'timed_out') {
|
|
4769
|
+
api.logger.warn(`totalreclaw_pair(relay): hard timeout — ${raced.message} (token=${remoteSession.token.slice(0, 8)}…)`);
|
|
4770
|
+
}
|
|
4771
|
+
}
|
|
4772
|
+
finally {
|
|
4773
|
+
if (hardTimer)
|
|
4774
|
+
clearTimeout(hardTimer);
|
|
4775
|
+
}
|
|
4677
4776
|
}
|
|
4678
4777
|
catch (bgErr) {
|
|
4679
4778
|
// Expected on TTL expiry / user-aborts — log at warn, not error.
|
|
@@ -4991,7 +5090,24 @@ const plugin = {
|
|
|
4991
5090
|
// Build a one-shot prefix when the bundled default points at staging
|
|
4992
5091
|
// AND the user hasn't overridden via env. This prefix is prepended
|
|
4993
5092
|
// to whichever context block the rest of the hook produces.
|
|
5093
|
+
//
|
|
5094
|
+
// 3.3.4-rc.1 — fix: previously `stagingBannerShown` was set to
|
|
5095
|
+
// `true` AS SOON AS the block was built. If the rest of the hook
|
|
5096
|
+
// then returned `undefined` (e.g. zero memory matches on the first
|
|
5097
|
+
// turn — multiple paths around lines 6103-6325 do this), the
|
|
5098
|
+
// banner block was silently discarded AND the flag was already
|
|
5099
|
+
// flipped, so subsequent before_agent_start invocations never
|
|
5100
|
+
// reconstructed it. Net effect: QA on 3.3.3-rc.1 (Pedro
|
|
5101
|
+
// 2026-04-30) saw NO banner emitted across an entire conversation
|
|
5102
|
+
// even though the build was bound to staging.
|
|
5103
|
+
//
|
|
5104
|
+
// Fix: build the block on every call until it is actually
|
|
5105
|
+
// delivered (i.e., until at least one return path included it
|
|
5106
|
+
// in `prependContext`). The flag flips at the bottom of this
|
|
5107
|
+
// hook in `markBannerDelivered()` once we know the prependContext
|
|
5108
|
+
// path was taken.
|
|
4994
5109
|
let stagingBannerBlock = '';
|
|
5110
|
+
let stagingBannerSuppressed = false;
|
|
4995
5111
|
if (!stagingBannerShown) {
|
|
4996
5112
|
try {
|
|
4997
5113
|
const usingStagingDefault = CONFIG.serverUrl.includes('api-staging.totalreclaw.xyz');
|
|
@@ -5004,9 +5120,11 @@ const plugin = {
|
|
|
5004
5120
|
'For production, install the stable release: `openclaw plugins install ' +
|
|
5005
5121
|
'@totalreclaw/totalreclaw` (no `@rc` suffix). To pin a custom server, set ' +
|
|
5006
5122
|
'`TOTALRECLAW_SERVER_URL=https://api.totalreclaw.xyz` in your env.\n\n';
|
|
5007
|
-
stagingBannerShown
|
|
5008
|
-
|
|
5009
|
-
|
|
5123
|
+
// Do NOT set stagingBannerShown=true here — see comment above.
|
|
5124
|
+
// Logger emits once per gateway process; this is fine to
|
|
5125
|
+
// gate on the same flag because the logger is operator-
|
|
5126
|
+
// facing, not user-facing.
|
|
5127
|
+
stagingBannerSuppressed = true;
|
|
5010
5128
|
}
|
|
5011
5129
|
else {
|
|
5012
5130
|
// Non-RC artifact OR user override — never fire the banner this
|
|
@@ -5019,6 +5137,33 @@ const plugin = {
|
|
|
5019
5137
|
stagingBannerShown = true;
|
|
5020
5138
|
}
|
|
5021
5139
|
}
|
|
5140
|
+
// Operator-facing log: once per process, when we DETECT the
|
|
5141
|
+
// staging build (banner-shown semantics are about user
|
|
5142
|
+
// delivery; this log is independent).
|
|
5143
|
+
if (stagingBannerSuppressed && !stagingBannerLogged) {
|
|
5144
|
+
stagingBannerLogged = true;
|
|
5145
|
+
api.logger.warn('TotalReclaw: RC/staging build active (api-staging.totalreclaw.xyz). ' +
|
|
5146
|
+
'See docs/guides/release-process.md for the RC=staging / stable=production rule.');
|
|
5147
|
+
}
|
|
5148
|
+
/**
|
|
5149
|
+
* Helper — invoked inline at any `prependContext` site that
|
|
5150
|
+
* wants to lead with the staging banner. Returns the banner
|
|
5151
|
+
* string AND atomically marks the banner as delivered, so
|
|
5152
|
+
* subsequent hook calls in the same gateway-process lifetime
|
|
5153
|
+
* skip re-emission. Returns '' (empty) when no banner is due
|
|
5154
|
+
* (stable build, user override, or already delivered).
|
|
5155
|
+
*
|
|
5156
|
+
* Use this at every prependContext callsite that takes the
|
|
5157
|
+
* banner; do NOT inline `stagingBannerBlock` on its own — the
|
|
5158
|
+
* 3.3.4-rc.1 bug fix requires the marker flip to be coupled
|
|
5159
|
+
* to the actual delivery.
|
|
5160
|
+
*/
|
|
5161
|
+
const consumeBannerForPrepend = () => {
|
|
5162
|
+
if (stagingBannerBlock === '')
|
|
5163
|
+
return '';
|
|
5164
|
+
stagingBannerShown = true;
|
|
5165
|
+
return stagingBannerBlock;
|
|
5166
|
+
};
|
|
5022
5167
|
await ensureInitialized(api.logger);
|
|
5023
5168
|
// 3.2.0 onboarding pending: emit a non-secret guidance banner so
|
|
5024
5169
|
// the LLM knows how to respond when the user asks about setup.
|
|
@@ -5047,7 +5192,7 @@ const plugin = {
|
|
|
5047
5192
|
api.logger.warn(`First-run welcome check failed: ${msg}`);
|
|
5048
5193
|
}
|
|
5049
5194
|
return {
|
|
5050
|
-
prependContext:
|
|
5195
|
+
prependContext: consumeBannerForPrepend() +
|
|
5051
5196
|
welcomeBlock +
|
|
5052
5197
|
'## TotalReclaw setup pending\n\n' +
|
|
5053
5198
|
'TotalReclaw encrypted memory is installed but not yet set up on this machine. ' +
|
|
@@ -5146,7 +5291,7 @@ const plugin = {
|
|
|
5146
5291
|
if (injectResult.promptText) {
|
|
5147
5292
|
api.logger.info(`Digest injection: state=${injectResult.state}`);
|
|
5148
5293
|
return {
|
|
5149
|
-
prependContext:
|
|
5294
|
+
prependContext: consumeBannerForPrepend() +
|
|
5150
5295
|
`## Your Memory\n\n${injectResult.promptText}` + welcomeBack + billingWarning,
|
|
5151
5296
|
};
|
|
5152
5297
|
}
|
|
@@ -5188,7 +5333,7 @@ const plugin = {
|
|
|
5188
5333
|
const querySimilarity = cosineSimilarity(queryEmbedding, lastQueryEmbedding);
|
|
5189
5334
|
if (querySimilarity > SEMANTIC_SKIP_THRESHOLD) {
|
|
5190
5335
|
const lines = cachedFacts.slice(0, 8).map((f, i) => `${i + 1}. ${f.text} (importance: ${f.importance}/10, cached)`);
|
|
5191
|
-
return { prependContext:
|
|
5336
|
+
return { prependContext: consumeBannerForPrepend() + `## Relevant Memories\n\n${lines.join('\n')}` + welcomeBack + billingWarning };
|
|
5192
5337
|
}
|
|
5193
5338
|
}
|
|
5194
5339
|
// 3. Merge trapdoors — always include word trapdoors for small-dataset coverage.
|
|
@@ -5197,7 +5342,7 @@ const plugin = {
|
|
|
5197
5342
|
// If we have cached facts and no trapdoors, return cached facts.
|
|
5198
5343
|
if (allTrapdoors.length === 0 && cachedFacts.length > 0) {
|
|
5199
5344
|
const lines = cachedFacts.slice(0, 8).map((f, i) => `${i + 1}. ${f.text} (importance: ${f.importance}/10, cached)`);
|
|
5200
|
-
return { prependContext:
|
|
5345
|
+
return { prependContext: consumeBannerForPrepend() + `## Relevant Memories\n\n${lines.join('\n')}` + welcomeBack + billingWarning };
|
|
5201
5346
|
}
|
|
5202
5347
|
if (allTrapdoors.length === 0)
|
|
5203
5348
|
return undefined;
|
|
@@ -5212,7 +5357,7 @@ const plugin = {
|
|
|
5212
5357
|
// Subgraph query failed -- fall back to cached facts if available.
|
|
5213
5358
|
if (cachedFacts.length > 0) {
|
|
5214
5359
|
const lines = cachedFacts.slice(0, 8).map((f, i) => `${i + 1}. ${f.text} (importance: ${f.importance}/10, cached)`);
|
|
5215
|
-
return { prependContext:
|
|
5360
|
+
return { prependContext: consumeBannerForPrepend() + `## Relevant Memories\n\n${lines.join('\n')}` + welcomeBack + billingWarning };
|
|
5216
5361
|
}
|
|
5217
5362
|
return undefined;
|
|
5218
5363
|
}
|
|
@@ -5236,7 +5381,7 @@ const plugin = {
|
|
|
5236
5381
|
// If subgraph returned no results but we have cache, use cache.
|
|
5237
5382
|
if (subgraphResults.length === 0) {
|
|
5238
5383
|
const lines = cachedFacts.slice(0, 8).map((f, i) => `${i + 1}. ${f.text} (importance: ${f.importance}/10, cached)`);
|
|
5239
|
-
return { prependContext:
|
|
5384
|
+
return { prependContext: consumeBannerForPrepend() + `## Relevant Memories\n\n${lines.join('\n')}` + welcomeBack + billingWarning };
|
|
5240
5385
|
}
|
|
5241
5386
|
// 5. Decrypt subgraph results and build reranker input.
|
|
5242
5387
|
const rerankerCandidates = [];
|
|
@@ -5311,7 +5456,7 @@ const plugin = {
|
|
|
5311
5456
|
return `${i + 1}. ${typeTag}${m.text} (importance: ${importance}/10, ${age})`;
|
|
5312
5457
|
});
|
|
5313
5458
|
const contextString = `## Relevant Memories\n\n${lines.join('\n')}`;
|
|
5314
|
-
return { prependContext:
|
|
5459
|
+
return { prependContext: consumeBannerForPrepend() + contextString + welcomeBack + billingWarning };
|
|
5315
5460
|
}
|
|
5316
5461
|
// --- Server mode (existing behavior) ---
|
|
5317
5462
|
// 1. Generate word trapdoors from the user prompt.
|
|
@@ -5393,7 +5538,7 @@ const plugin = {
|
|
|
5393
5538
|
return `${i + 1}. ${m.text} (importance: ${importance}/10, ${age})`;
|
|
5394
5539
|
});
|
|
5395
5540
|
const contextString = `## Relevant Memories\n\n${lines.join('\n')}`;
|
|
5396
|
-
return { prependContext:
|
|
5541
|
+
return { prependContext: consumeBannerForPrepend() + contextString + welcomeBack + billingWarning };
|
|
5397
5542
|
}
|
|
5398
5543
|
catch (err) {
|
|
5399
5544
|
// The hook must NEVER throw -- log and return undefined.
|