@totalreclaw/totalreclaw 3.3.1-rc.20 → 3.3.1-rc.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +47 -0
- package/api-client.ts +18 -11
- package/config.ts +60 -3
- package/crypto.ts +10 -2
- package/dist/api-client.js +17 -11
- package/dist/config.js +60 -3
- package/dist/crypto.js +10 -2
- package/dist/fs-helpers.js +82 -0
- package/dist/index.js +149 -34
- package/dist/lsh.js +7 -2
- package/dist/relay-headers.js +44 -0
- package/dist/subgraph-search.js +4 -3
- package/dist/subgraph-store.js +15 -10
- package/fs-helpers.ts +92 -0
- package/index.ts +166 -39
- package/lsh.ts +7 -2
- package/package.json +3 -2
- package/relay-headers.ts +50 -0
- package/subgraph-search.ts +4 -3
- package/subgraph-store.ts +15 -10
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,53 @@ All notable changes to `@totalreclaw/totalreclaw` (the OpenClaw plugin) are docu
|
|
|
4
4
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
### Install / runtime hygiene (issues #126, #128)
|
|
10
|
+
|
|
11
|
+
Two narrow fixes from the rc.20 user-QA findings — both around install /
|
|
12
|
+
boot-time output cleanliness, no behavior change to the steady-state plugin.
|
|
13
|
+
|
|
14
|
+
- **#126 — clean up `.openclaw-install-stage-*` siblings.** When
|
|
15
|
+
`openclaw plugins install @totalreclaw/totalreclaw` is interrupted mid-
|
|
16
|
+
extract (e.g. by an auto-gateway-restart triggered by the same install),
|
|
17
|
+
the npm staging directory `<extensionsDir>/.openclaw-install-stage-XXXXXX/`
|
|
18
|
+
survives. On the next gateway start, OpenClaw's plugin loader auto-
|
|
19
|
+
discovers BOTH `.../totalreclaw/` AND the orphan staging dir, registers
|
|
20
|
+
duplicate plugins, fires hooks twice, and prints a "duplicate-plugin-id"
|
|
21
|
+
warning every cycle. A user running `openclaw plugins list` sees two
|
|
22
|
+
`totalreclaw` rows.
|
|
23
|
+
|
|
24
|
+
Fix: `cleanupInstallStagingDirs(pluginDir)` runs at plugin register time
|
|
25
|
+
(one tick after the loader resolves our entrypoint). It scans the
|
|
26
|
+
extensions directory for `.openclaw-install-stage-*` siblings and
|
|
27
|
+
recursively removes each one. Best-effort — never crashes plugin init
|
|
28
|
+
on permission / race failures.
|
|
29
|
+
|
|
30
|
+
Regression: `install-staging-cleanup.test.ts` (16 assertions) covers
|
|
31
|
+
fresh install, idempotent re-run, package-root vs `dist/` invocation,
|
|
32
|
+
unrelated-dotfile preservation (`.git`, `.openclaw-cache`), and stray-
|
|
33
|
+
file (non-directory) skipping.
|
|
34
|
+
|
|
35
|
+
- **#128 — registerTool breadcrumbs no longer bleed into `--json` stdout.**
|
|
36
|
+
The rc.20 breadcrumb logs (`registerTool(totalreclaw_pair) returned. ...`
|
|
37
|
+
and the RC-only `totalreclaw_report_qa_bug registered ...`) were emitted
|
|
38
|
+
via `api.logger.info`, which OpenClaw routes to stdout decorated with
|
|
39
|
+
`[plugins] `. When a user invoked `openclaw agent --message "..." --json`
|
|
40
|
+
for programmatic parsing, the breadcrumb appeared on stdout alongside
|
|
41
|
+
the JSON-RPC body, breaking any naive `JSON.parse(stdout)`.
|
|
42
|
+
|
|
43
|
+
Fix: gate both breadcrumbs behind `CONFIG.verboseRegister`, OFF by
|
|
44
|
+
default. Ops can opt back in with `TOTALRECLAW_VERBOSE_REGISTER=1` (or
|
|
45
|
+
the general `TOTALRECLAW_DEBUG=1` toggle) when chasing a tool-injection
|
|
46
|
+
regression. Default-off keeps `openclaw agent --json` stdout clean.
|
|
47
|
+
|
|
48
|
+
Regression: `json-stdout-cleanliness.test.ts` (11 assertions) confirms
|
|
49
|
+
both breadcrumbs are wrapped in `if (CONFIG.verboseRegister)` blocks,
|
|
50
|
+
simulates the gated `--json` stdout path and `JSON.parse`s the result,
|
|
51
|
+
and exercises the env-var resolution (`TOTALRECLAW_VERBOSE_REGISTER`
|
|
52
|
+
-> `TOTALRECLAW_DEBUG` -> default false).
|
|
53
|
+
|
|
7
54
|
## [3.3.1-rc.16] — 2026-04-24
|
|
8
55
|
|
|
9
56
|
Fixes #92 — slow-host install times out during ONNX-runtime / embedding-model
|
package/api-client.ts
CHANGED
|
@@ -8,8 +8,15 @@
|
|
|
8
8
|
* Authorization: Bearer <hex-encoded-auth-key>
|
|
9
9
|
*
|
|
10
10
|
* The server hashes the auth key with SHA-256 to look up the user.
|
|
11
|
+
*
|
|
12
|
+
* Every outbound request goes through `buildRelayHeaders()` so the
|
|
13
|
+
* `X-TotalReclaw-Client` tag is set + the optional QA-tracing
|
|
14
|
+
* `X-TotalReclaw-Session` tag is forwarded when `TOTALRECLAW_SESSION_ID`
|
|
15
|
+
* is set. See `relay-headers.ts` and internal#127.
|
|
11
16
|
*/
|
|
12
17
|
|
|
18
|
+
import { buildRelayHeaders } from './relay-headers.js';
|
|
19
|
+
|
|
13
20
|
// ---------------------------------------------------------------------------
|
|
14
21
|
// Request / Response Types
|
|
15
22
|
// ---------------------------------------------------------------------------
|
|
@@ -126,7 +133,7 @@ export function createApiClient(serverUrl: string) {
|
|
|
126
133
|
): Promise<{ user_id: string }> {
|
|
127
134
|
const res = await fetch(`${baseUrl}/v1/register`, {
|
|
128
135
|
method: 'POST',
|
|
129
|
-
headers: { 'Content-Type': 'application/json'
|
|
136
|
+
headers: buildRelayHeaders({ 'Content-Type': 'application/json' }),
|
|
130
137
|
body: JSON.stringify({ auth_key_hash: authKeyHash, salt: saltHex }),
|
|
131
138
|
});
|
|
132
139
|
await assertOk(res, 'register');
|
|
@@ -160,10 +167,10 @@ export function createApiClient(serverUrl: string) {
|
|
|
160
167
|
): Promise<{ ids: string[]; duplicate_ids?: string[] }> {
|
|
161
168
|
const res = await fetch(`${baseUrl}/v1/store`, {
|
|
162
169
|
method: 'POST',
|
|
163
|
-
headers: {
|
|
170
|
+
headers: buildRelayHeaders({
|
|
164
171
|
'Content-Type': 'application/json',
|
|
165
172
|
Authorization: `Bearer ${authKeyHex}`,
|
|
166
|
-
},
|
|
173
|
+
}),
|
|
167
174
|
body: JSON.stringify({ user_id: userId, facts }),
|
|
168
175
|
});
|
|
169
176
|
await assertOk(res, 'store');
|
|
@@ -198,10 +205,10 @@ export function createApiClient(serverUrl: string) {
|
|
|
198
205
|
): Promise<SearchCandidate[]> {
|
|
199
206
|
const res = await fetch(`${baseUrl}/v1/search`, {
|
|
200
207
|
method: 'POST',
|
|
201
|
-
headers: {
|
|
208
|
+
headers: buildRelayHeaders({
|
|
202
209
|
'Content-Type': 'application/json',
|
|
203
210
|
Authorization: `Bearer ${authKeyHex}`,
|
|
204
|
-
},
|
|
211
|
+
}),
|
|
205
212
|
body: JSON.stringify({
|
|
206
213
|
user_id: userId,
|
|
207
214
|
trapdoors,
|
|
@@ -229,9 +236,9 @@ export function createApiClient(serverUrl: string) {
|
|
|
229
236
|
async deleteFact(factId: string, authKeyHex: string): Promise<void> {
|
|
230
237
|
const res = await fetch(`${baseUrl}/v1/facts/${encodeURIComponent(factId)}`, {
|
|
231
238
|
method: 'DELETE',
|
|
232
|
-
headers: {
|
|
239
|
+
headers: buildRelayHeaders({
|
|
233
240
|
Authorization: `Bearer ${authKeyHex}`,
|
|
234
|
-
},
|
|
241
|
+
}),
|
|
235
242
|
});
|
|
236
243
|
await assertOk(res, 'deleteFact');
|
|
237
244
|
const json = (await res.json()) as Record<string, unknown>;
|
|
@@ -254,10 +261,10 @@ export function createApiClient(serverUrl: string) {
|
|
|
254
261
|
async batchDelete(factIds: string[], authKeyHex: string): Promise<number> {
|
|
255
262
|
const res = await fetch(`${baseUrl}/v1/facts/batch-delete`, {
|
|
256
263
|
method: 'POST',
|
|
257
|
-
headers: {
|
|
264
|
+
headers: buildRelayHeaders({
|
|
258
265
|
'Content-Type': 'application/json',
|
|
259
266
|
Authorization: `Bearer ${authKeyHex}`,
|
|
260
|
-
},
|
|
267
|
+
}),
|
|
261
268
|
body: JSON.stringify({ fact_ids: factIds }),
|
|
262
269
|
});
|
|
263
270
|
await assertOk(res, 'batchDelete');
|
|
@@ -290,9 +297,9 @@ export function createApiClient(serverUrl: string) {
|
|
|
290
297
|
|
|
291
298
|
const res = await fetch(`${baseUrl}/v1/export?${params.toString()}`, {
|
|
292
299
|
method: 'GET',
|
|
293
|
-
headers: {
|
|
300
|
+
headers: buildRelayHeaders({
|
|
294
301
|
Authorization: `Bearer ${authKeyHex}`,
|
|
295
|
-
},
|
|
302
|
+
}),
|
|
296
303
|
});
|
|
297
304
|
await assertOk(res, 'exportFacts');
|
|
298
305
|
const json = (await res.json()) as Record<string, unknown>;
|
package/config.ts
CHANGED
|
@@ -9,8 +9,15 @@
|
|
|
9
9
|
*
|
|
10
10
|
* v1 env var cleanup — see `docs/guides/env-vars-reference.md`.
|
|
11
11
|
* Removed user-facing vars: TOTALRECLAW_CHAIN_ID, TOTALRECLAW_EMBEDDING_MODEL,
|
|
12
|
-
* TOTALRECLAW_STORE_DEDUP, TOTALRECLAW_LLM_MODEL,
|
|
13
|
-
*
|
|
12
|
+
* TOTALRECLAW_STORE_DEDUP, TOTALRECLAW_LLM_MODEL, TOTALRECLAW_TAXONOMY_VERSION.
|
|
13
|
+
*
|
|
14
|
+
* NOTE: ``TOTALRECLAW_SESSION_ID`` was in the removed list during the v1
|
|
15
|
+
* cleanup and silently rejected with a warning. That broke Axiom log tracing
|
|
16
|
+
* for QA — the qa-totalreclaw skill prescribes setting the var so relay logs
|
|
17
|
+
* are searchable by ``X-TotalReclaw-Session``. Restored as a SUPPORTED
|
|
18
|
+
* variable: read here, forwarded as the ``X-TotalReclaw-Session`` header on
|
|
19
|
+
* every outbound relay call. Mirrors the Python-side fix
|
|
20
|
+
* (`python/src/totalreclaw/agent/state.py`, v2.0.2). See internal#127.
|
|
14
21
|
* Removed legacy gates: TOTALRECLAW_CLAIM_FORMAT, TOTALRECLAW_DIGEST_MODE,
|
|
15
22
|
* TOTALRECLAW_AUTO_RESOLVE_MODE (the last one moved to an internal debug
|
|
16
23
|
* module; see `contradiction-sync.ts`).
|
|
@@ -33,7 +40,9 @@ const REMOVED_ENV_VARS = [
|
|
|
33
40
|
'TOTALRECLAW_EMBEDDING_MODEL',
|
|
34
41
|
'TOTALRECLAW_STORE_DEDUP',
|
|
35
42
|
'TOTALRECLAW_LLM_MODEL',
|
|
36
|
-
|
|
43
|
+
// NOTE: TOTALRECLAW_SESSION_ID was here before; restored as SUPPORTED
|
|
44
|
+
// (forwarded as X-TotalReclaw-Session header). Do NOT add it back to this
|
|
45
|
+
// list — see file header + internal#127.
|
|
37
46
|
'TOTALRECLAW_TAXONOMY_VERSION',
|
|
38
47
|
'TOTALRECLAW_CLAIM_FORMAT',
|
|
39
48
|
'TOTALRECLAW_DIGEST_MODE',
|
|
@@ -63,6 +72,27 @@ export function getRecoveryPhrase(): string {
|
|
|
63
72
|
return _recoveryPhraseOverride ?? process.env.TOTALRECLAW_RECOVERY_PHRASE ?? '';
|
|
64
73
|
}
|
|
65
74
|
|
|
75
|
+
/**
|
|
76
|
+
* Read the QA / observability session tag from the environment.
|
|
77
|
+
*
|
|
78
|
+
* When set, every outbound relay call adds the ``X-TotalReclaw-Session``
|
|
79
|
+
* header so relay logs (and Axiom queries) can be filtered by this tag —
|
|
80
|
+
* this is what the qa-totalreclaw skill relies on to scope log searches per
|
|
81
|
+
* QA run. When unset, returns ``null`` and the header is omitted.
|
|
82
|
+
*
|
|
83
|
+
* Read via getter (not snapshotted) so operators / test harnesses can flip
|
|
84
|
+
* the var between calls without reloading the module.
|
|
85
|
+
*
|
|
86
|
+
* Mirrors the Python-side ``RelayClient._session_id`` resolution priority.
|
|
87
|
+
* See internal#127 / `docs/guides/env-vars-reference.md`.
|
|
88
|
+
*/
|
|
89
|
+
export function getSessionId(): string | null {
|
|
90
|
+
const raw = process.env.TOTALRECLAW_SESSION_ID;
|
|
91
|
+
if (raw === undefined) return null;
|
|
92
|
+
const trimmed = raw.trim();
|
|
93
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
94
|
+
}
|
|
95
|
+
|
|
66
96
|
/**
|
|
67
97
|
* Runtime override for chain ID, set after the relay billing response is
|
|
68
98
|
* read. Free tier stays on 84532 (Base Sepolia); Pro tier flips to 100
|
|
@@ -92,6 +122,15 @@ export const CONFIG = {
|
|
|
92
122
|
get recoveryPhrase(): string {
|
|
93
123
|
return getRecoveryPhrase();
|
|
94
124
|
},
|
|
125
|
+
/**
|
|
126
|
+
* Optional QA / observability session tag forwarded to the relay as
|
|
127
|
+
* ``X-TotalReclaw-Session``. See `getSessionId()` above. Getter form so
|
|
128
|
+
* tests + harnesses can flip the env between calls. ``null`` when unset
|
|
129
|
+
* (header omitted).
|
|
130
|
+
*/
|
|
131
|
+
get sessionId(): string | null {
|
|
132
|
+
return getSessionId();
|
|
133
|
+
},
|
|
95
134
|
serverUrl: (process.env.TOTALRECLAW_SERVER_URL || 'https://api.totalreclaw.xyz').replace(/\/+$/, ''),
|
|
96
135
|
selfHosted: process.env.TOTALRECLAW_SELF_HOSTED === 'true',
|
|
97
136
|
credentialsPath: process.env.TOTALRECLAW_CREDENTIALS_PATH || path.join(home, '.totalreclaw', 'credentials.json'),
|
|
@@ -214,6 +253,24 @@ export const CONFIG = {
|
|
|
214
253
|
return process.env.TOTALRECLAW_QA_REPO || '';
|
|
215
254
|
},
|
|
216
255
|
|
|
256
|
+
// 3.3.1-rc.21 (issue #128): verbose-register flag. When enabled, the
|
|
257
|
+
// plugin emits opt-in `info`-level breadcrumbs after sensitive
|
|
258
|
+
// registerTool calls (currently `totalreclaw_pair`) to help ops/QA
|
|
259
|
+
// grep gateway logs for definitive proof the tool was declared.
|
|
260
|
+
// Default OFF — the breadcrumb is debug-grade and was bleeding into
|
|
261
|
+
// `openclaw agent --json` stdout, breaking programmatic parsers.
|
|
262
|
+
// Enable with either:
|
|
263
|
+
// TOTALRECLAW_VERBOSE_REGISTER=1 (specific opt-in)
|
|
264
|
+
// TOTALRECLAW_DEBUG=1 (general debug toggle)
|
|
265
|
+
// Read via getter so flipping the env at runtime takes effect on the
|
|
266
|
+
// next gateway start without a rebuild.
|
|
267
|
+
get verboseRegister(): boolean {
|
|
268
|
+
const specific = (process.env.TOTALRECLAW_VERBOSE_REGISTER ?? '').trim().toLowerCase();
|
|
269
|
+
if (specific === '1' || specific === 'true' || specific === 'yes') return true;
|
|
270
|
+
const general = (process.env.TOTALRECLAW_DEBUG ?? '').trim().toLowerCase();
|
|
271
|
+
return general === '1' || general === 'true' || general === 'yes';
|
|
272
|
+
},
|
|
273
|
+
|
|
217
274
|
// Paths
|
|
218
275
|
home,
|
|
219
276
|
billingCachePath: path.join(home, '.totalreclaw', 'billing-cache.json'),
|
package/crypto.ts
CHANGED
|
@@ -15,10 +15,18 @@
|
|
|
15
15
|
* -> HKDF-SHA256(seed, salt, "openmemory-dedup-v1", 32) -> dedupKey
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
// Lazy-load WASM
|
|
18
|
+
// Lazy-load WASM. Uses createRequire so this module loads cleanly under bare
|
|
19
|
+
// Node ESM — the shipped `dist/index.js` declares `"type":"module"`, where
|
|
20
|
+
// the CJS `require` global is undefined at runtime. Prior to the rc.21 fix
|
|
21
|
+
// this file called bare `require('@totalreclaw/core')` and every consumer
|
|
22
|
+
// died with `require is not defined`. Matches the pattern already used by
|
|
23
|
+
// claims-helper / consolidation / contradiction-sync / digest-sync / pin /
|
|
24
|
+
// retype-setscope. See issue #124.
|
|
25
|
+
import { createRequire } from 'node:module';
|
|
26
|
+
const requireWasm = createRequire(import.meta.url);
|
|
19
27
|
let _wasm: typeof import('@totalreclaw/core') | null = null;
|
|
20
28
|
function getWasm() {
|
|
21
|
-
if (!_wasm) _wasm =
|
|
29
|
+
if (!_wasm) _wasm = requireWasm('@totalreclaw/core');
|
|
22
30
|
return _wasm;
|
|
23
31
|
}
|
|
24
32
|
|
package/dist/api-client.js
CHANGED
|
@@ -8,7 +8,13 @@
|
|
|
8
8
|
* Authorization: Bearer <hex-encoded-auth-key>
|
|
9
9
|
*
|
|
10
10
|
* The server hashes the auth key with SHA-256 to look up the user.
|
|
11
|
+
*
|
|
12
|
+
* Every outbound request goes through `buildRelayHeaders()` so the
|
|
13
|
+
* `X-TotalReclaw-Client` tag is set + the optional QA-tracing
|
|
14
|
+
* `X-TotalReclaw-Session` tag is forwarded when `TOTALRECLAW_SESSION_ID`
|
|
15
|
+
* is set. See `relay-headers.ts` and internal#127.
|
|
11
16
|
*/
|
|
17
|
+
import { buildRelayHeaders } from './relay-headers.js';
|
|
12
18
|
// ---------------------------------------------------------------------------
|
|
13
19
|
// API Client Factory
|
|
14
20
|
// ---------------------------------------------------------------------------
|
|
@@ -56,7 +62,7 @@ export function createApiClient(serverUrl) {
|
|
|
56
62
|
async register(authKeyHash, saltHex) {
|
|
57
63
|
const res = await fetch(`${baseUrl}/v1/register`, {
|
|
58
64
|
method: 'POST',
|
|
59
|
-
headers: { 'Content-Type': 'application/json'
|
|
65
|
+
headers: buildRelayHeaders({ 'Content-Type': 'application/json' }),
|
|
60
66
|
body: JSON.stringify({ auth_key_hash: authKeyHash, salt: saltHex }),
|
|
61
67
|
});
|
|
62
68
|
await assertOk(res, 'register');
|
|
@@ -80,10 +86,10 @@ export function createApiClient(serverUrl) {
|
|
|
80
86
|
async store(userId, facts, authKeyHex) {
|
|
81
87
|
const res = await fetch(`${baseUrl}/v1/store`, {
|
|
82
88
|
method: 'POST',
|
|
83
|
-
headers: {
|
|
89
|
+
headers: buildRelayHeaders({
|
|
84
90
|
'Content-Type': 'application/json',
|
|
85
91
|
Authorization: `Bearer ${authKeyHex}`,
|
|
86
|
-
},
|
|
92
|
+
}),
|
|
87
93
|
body: JSON.stringify({ user_id: userId, facts }),
|
|
88
94
|
});
|
|
89
95
|
await assertOk(res, 'store');
|
|
@@ -109,10 +115,10 @@ export function createApiClient(serverUrl) {
|
|
|
109
115
|
async search(userId, trapdoors, maxCandidates, authKeyHex) {
|
|
110
116
|
const res = await fetch(`${baseUrl}/v1/search`, {
|
|
111
117
|
method: 'POST',
|
|
112
|
-
headers: {
|
|
118
|
+
headers: buildRelayHeaders({
|
|
113
119
|
'Content-Type': 'application/json',
|
|
114
120
|
Authorization: `Bearer ${authKeyHex}`,
|
|
115
|
-
},
|
|
121
|
+
}),
|
|
116
122
|
body: JSON.stringify({
|
|
117
123
|
user_id: userId,
|
|
118
124
|
trapdoors,
|
|
@@ -136,9 +142,9 @@ export function createApiClient(serverUrl) {
|
|
|
136
142
|
async deleteFact(factId, authKeyHex) {
|
|
137
143
|
const res = await fetch(`${baseUrl}/v1/facts/${encodeURIComponent(factId)}`, {
|
|
138
144
|
method: 'DELETE',
|
|
139
|
-
headers: {
|
|
145
|
+
headers: buildRelayHeaders({
|
|
140
146
|
Authorization: `Bearer ${authKeyHex}`,
|
|
141
|
-
},
|
|
147
|
+
}),
|
|
142
148
|
});
|
|
143
149
|
await assertOk(res, 'deleteFact');
|
|
144
150
|
const json = (await res.json());
|
|
@@ -157,10 +163,10 @@ export function createApiClient(serverUrl) {
|
|
|
157
163
|
async batchDelete(factIds, authKeyHex) {
|
|
158
164
|
const res = await fetch(`${baseUrl}/v1/facts/batch-delete`, {
|
|
159
165
|
method: 'POST',
|
|
160
|
-
headers: {
|
|
166
|
+
headers: buildRelayHeaders({
|
|
161
167
|
'Content-Type': 'application/json',
|
|
162
168
|
Authorization: `Bearer ${authKeyHex}`,
|
|
163
|
-
},
|
|
169
|
+
}),
|
|
164
170
|
body: JSON.stringify({ fact_ids: factIds }),
|
|
165
171
|
});
|
|
166
172
|
await assertOk(res, 'batchDelete');
|
|
@@ -185,9 +191,9 @@ export function createApiClient(serverUrl) {
|
|
|
185
191
|
params.set('cursor', cursor);
|
|
186
192
|
const res = await fetch(`${baseUrl}/v1/export?${params.toString()}`, {
|
|
187
193
|
method: 'GET',
|
|
188
|
-
headers: {
|
|
194
|
+
headers: buildRelayHeaders({
|
|
189
195
|
Authorization: `Bearer ${authKeyHex}`,
|
|
190
|
-
},
|
|
196
|
+
}),
|
|
191
197
|
});
|
|
192
198
|
await assertOk(res, 'exportFacts');
|
|
193
199
|
const json = (await res.json());
|
package/dist/config.js
CHANGED
|
@@ -9,8 +9,15 @@
|
|
|
9
9
|
*
|
|
10
10
|
* v1 env var cleanup — see `docs/guides/env-vars-reference.md`.
|
|
11
11
|
* Removed user-facing vars: TOTALRECLAW_CHAIN_ID, TOTALRECLAW_EMBEDDING_MODEL,
|
|
12
|
-
* TOTALRECLAW_STORE_DEDUP, TOTALRECLAW_LLM_MODEL,
|
|
13
|
-
*
|
|
12
|
+
* TOTALRECLAW_STORE_DEDUP, TOTALRECLAW_LLM_MODEL, TOTALRECLAW_TAXONOMY_VERSION.
|
|
13
|
+
*
|
|
14
|
+
* NOTE: ``TOTALRECLAW_SESSION_ID`` was in the removed list during the v1
|
|
15
|
+
* cleanup and silently rejected with a warning. That broke Axiom log tracing
|
|
16
|
+
* for QA — the qa-totalreclaw skill prescribes setting the var so relay logs
|
|
17
|
+
* are searchable by ``X-TotalReclaw-Session``. Restored as a SUPPORTED
|
|
18
|
+
* variable: read here, forwarded as the ``X-TotalReclaw-Session`` header on
|
|
19
|
+
* every outbound relay call. Mirrors the Python-side fix
|
|
20
|
+
* (`python/src/totalreclaw/agent/state.py`, v2.0.2). See internal#127.
|
|
14
21
|
* Removed legacy gates: TOTALRECLAW_CLAIM_FORMAT, TOTALRECLAW_DIGEST_MODE,
|
|
15
22
|
* TOTALRECLAW_AUTO_RESOLVE_MODE (the last one moved to an internal debug
|
|
16
23
|
* module; see `contradiction-sync.ts`).
|
|
@@ -30,7 +37,9 @@ const REMOVED_ENV_VARS = [
|
|
|
30
37
|
'TOTALRECLAW_EMBEDDING_MODEL',
|
|
31
38
|
'TOTALRECLAW_STORE_DEDUP',
|
|
32
39
|
'TOTALRECLAW_LLM_MODEL',
|
|
33
|
-
|
|
40
|
+
// NOTE: TOTALRECLAW_SESSION_ID was here before; restored as SUPPORTED
|
|
41
|
+
// (forwarded as X-TotalReclaw-Session header). Do NOT add it back to this
|
|
42
|
+
// list — see file header + internal#127.
|
|
34
43
|
'TOTALRECLAW_TAXONOMY_VERSION',
|
|
35
44
|
'TOTALRECLAW_CLAIM_FORMAT',
|
|
36
45
|
'TOTALRECLAW_DIGEST_MODE',
|
|
@@ -53,6 +62,27 @@ export function setRecoveryPhraseOverride(phrase) {
|
|
|
53
62
|
export function getRecoveryPhrase() {
|
|
54
63
|
return _recoveryPhraseOverride ?? process.env.TOTALRECLAW_RECOVERY_PHRASE ?? '';
|
|
55
64
|
}
|
|
65
|
+
/**
|
|
66
|
+
* Read the QA / observability session tag from the environment.
|
|
67
|
+
*
|
|
68
|
+
* When set, every outbound relay call adds the ``X-TotalReclaw-Session``
|
|
69
|
+
* header so relay logs (and Axiom queries) can be filtered by this tag —
|
|
70
|
+
* this is what the qa-totalreclaw skill relies on to scope log searches per
|
|
71
|
+
* QA run. When unset, returns ``null`` and the header is omitted.
|
|
72
|
+
*
|
|
73
|
+
* Read via getter (not snapshotted) so operators / test harnesses can flip
|
|
74
|
+
* the var between calls without reloading the module.
|
|
75
|
+
*
|
|
76
|
+
* Mirrors the Python-side ``RelayClient._session_id`` resolution priority.
|
|
77
|
+
* See internal#127 / `docs/guides/env-vars-reference.md`.
|
|
78
|
+
*/
|
|
79
|
+
export function getSessionId() {
|
|
80
|
+
const raw = process.env.TOTALRECLAW_SESSION_ID;
|
|
81
|
+
if (raw === undefined)
|
|
82
|
+
return null;
|
|
83
|
+
const trimmed = raw.trim();
|
|
84
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
85
|
+
}
|
|
56
86
|
/**
|
|
57
87
|
* Runtime override for chain ID, set after the relay billing response is
|
|
58
88
|
* read. Free tier stays on 84532 (Base Sepolia); Pro tier flips to 100
|
|
@@ -79,6 +109,15 @@ export const CONFIG = {
|
|
|
79
109
|
get recoveryPhrase() {
|
|
80
110
|
return getRecoveryPhrase();
|
|
81
111
|
},
|
|
112
|
+
/**
|
|
113
|
+
* Optional QA / observability session tag forwarded to the relay as
|
|
114
|
+
* ``X-TotalReclaw-Session``. See `getSessionId()` above. Getter form so
|
|
115
|
+
* tests + harnesses can flip the env between calls. ``null`` when unset
|
|
116
|
+
* (header omitted).
|
|
117
|
+
*/
|
|
118
|
+
get sessionId() {
|
|
119
|
+
return getSessionId();
|
|
120
|
+
},
|
|
82
121
|
serverUrl: (process.env.TOTALRECLAW_SERVER_URL || 'https://api.totalreclaw.xyz').replace(/\/+$/, ''),
|
|
83
122
|
selfHosted: process.env.TOTALRECLAW_SELF_HOSTED === 'true',
|
|
84
123
|
credentialsPath: process.env.TOTALRECLAW_CREDENTIALS_PATH || path.join(home, '.totalreclaw', 'credentials.json'),
|
|
@@ -192,6 +231,24 @@ export const CONFIG = {
|
|
|
192
231
|
get qaRepoOverride() {
|
|
193
232
|
return process.env.TOTALRECLAW_QA_REPO || '';
|
|
194
233
|
},
|
|
234
|
+
// 3.3.1-rc.21 (issue #128): verbose-register flag. When enabled, the
|
|
235
|
+
// plugin emits opt-in `info`-level breadcrumbs after sensitive
|
|
236
|
+
// registerTool calls (currently `totalreclaw_pair`) to help ops/QA
|
|
237
|
+
// grep gateway logs for definitive proof the tool was declared.
|
|
238
|
+
// Default OFF — the breadcrumb is debug-grade and was bleeding into
|
|
239
|
+
// `openclaw agent --json` stdout, breaking programmatic parsers.
|
|
240
|
+
// Enable with either:
|
|
241
|
+
// TOTALRECLAW_VERBOSE_REGISTER=1 (specific opt-in)
|
|
242
|
+
// TOTALRECLAW_DEBUG=1 (general debug toggle)
|
|
243
|
+
// Read via getter so flipping the env at runtime takes effect on the
|
|
244
|
+
// next gateway start without a rebuild.
|
|
245
|
+
get verboseRegister() {
|
|
246
|
+
const specific = (process.env.TOTALRECLAW_VERBOSE_REGISTER ?? '').trim().toLowerCase();
|
|
247
|
+
if (specific === '1' || specific === 'true' || specific === 'yes')
|
|
248
|
+
return true;
|
|
249
|
+
const general = (process.env.TOTALRECLAW_DEBUG ?? '').trim().toLowerCase();
|
|
250
|
+
return general === '1' || general === 'true' || general === 'yes';
|
|
251
|
+
},
|
|
195
252
|
// Paths
|
|
196
253
|
home,
|
|
197
254
|
billingCachePath: path.join(home, '.totalreclaw', 'billing-cache.json'),
|
package/dist/crypto.js
CHANGED
|
@@ -14,11 +14,19 @@
|
|
|
14
14
|
* -> HKDF-SHA256(seed, salt, "totalreclaw-encryption-key-v1", 32) -> encryptionKey
|
|
15
15
|
* -> HKDF-SHA256(seed, salt, "openmemory-dedup-v1", 32) -> dedupKey
|
|
16
16
|
*/
|
|
17
|
-
// Lazy-load WASM
|
|
17
|
+
// Lazy-load WASM. Uses createRequire so this module loads cleanly under bare
|
|
18
|
+
// Node ESM — the shipped `dist/index.js` declares `"type":"module"`, where
|
|
19
|
+
// the CJS `require` global is undefined at runtime. Prior to the rc.21 fix
|
|
20
|
+
// this file called bare `require('@totalreclaw/core')` and every consumer
|
|
21
|
+
// died with `require is not defined`. Matches the pattern already used by
|
|
22
|
+
// claims-helper / consolidation / contradiction-sync / digest-sync / pin /
|
|
23
|
+
// retype-setscope. See issue #124.
|
|
24
|
+
import { createRequire } from 'node:module';
|
|
25
|
+
const requireWasm = createRequire(import.meta.url);
|
|
18
26
|
let _wasm = null;
|
|
19
27
|
function getWasm() {
|
|
20
28
|
if (!_wasm)
|
|
21
|
-
_wasm =
|
|
29
|
+
_wasm = requireWasm('@totalreclaw/core');
|
|
22
30
|
return _wasm;
|
|
23
31
|
}
|
|
24
32
|
// ---------------------------------------------------------------------------
|
package/dist/fs-helpers.js
CHANGED
|
@@ -213,6 +213,88 @@ export function deleteFileIfExists(filePath) {
|
|
|
213
213
|
}
|
|
214
214
|
}
|
|
215
215
|
// ---------------------------------------------------------------------------
|
|
216
|
+
// Install-staging cleanup (issue #126 — rc.20 finding F3)
|
|
217
|
+
// ---------------------------------------------------------------------------
|
|
218
|
+
/**
|
|
219
|
+
* Clean up `.openclaw-install-stage-*` sibling directories left behind by
|
|
220
|
+
* an interrupted `openclaw plugins install` run.
|
|
221
|
+
*
|
|
222
|
+
* Background
|
|
223
|
+
* ----------
|
|
224
|
+
* `openclaw plugins install @totalreclaw/totalreclaw` extracts the npm
|
|
225
|
+
* tarball into a staging directory named
|
|
226
|
+
* `<extensionsDir>/.openclaw-install-stage-XXXXXX/` and then renames it
|
|
227
|
+
* to `<extensionsDir>/totalreclaw/` on success. If the install is
|
|
228
|
+
* interrupted partway through (e.g. an auto-gateway-restart triggered by
|
|
229
|
+
* the same install kills the process — see rc.20 QA finding F3), the
|
|
230
|
+
* staging dir survives. On the next gateway start, OpenClaw's plugin
|
|
231
|
+
* loader auto-discovers BOTH directories — the real `totalreclaw/` and
|
|
232
|
+
* the orphaned `.openclaw-install-stage-XXXXXX/` — and registers two
|
|
233
|
+
* copies of the plugin. Hooks fire twice, the user sees a duplicate
|
|
234
|
+
* `totalreclaw` row in `openclaw plugins list`, and the gateway log
|
|
235
|
+
* spams a duplicate-plugin-id warning every cycle.
|
|
236
|
+
*
|
|
237
|
+
* Fix scope: best-effort cleanup driven by the plugin itself at register
|
|
238
|
+
* time. We resolve the extensions dir as the parent of the loaded
|
|
239
|
+
* plugin's own directory, scan for `.openclaw-install-stage-*` siblings,
|
|
240
|
+
* and recursively remove each one. If anything fails (permission,
|
|
241
|
+
* race with a concurrent install), we swallow the error — the existing
|
|
242
|
+
* loader-warning behavior is no worse than before.
|
|
243
|
+
*
|
|
244
|
+
* Returns the list of staging-dir paths that were successfully removed.
|
|
245
|
+
* Callers may log this for ops visibility. Empty list on a clean install.
|
|
246
|
+
*
|
|
247
|
+
* Parameters
|
|
248
|
+
* ----------
|
|
249
|
+
* @param pluginDir Absolute path to the loaded plugin's directory
|
|
250
|
+
* (typically `<extensionsDir>/totalreclaw/dist`). The
|
|
251
|
+
* helper walks up to the parent that holds sibling
|
|
252
|
+
* plugin directories (the `extensions/` root).
|
|
253
|
+
* @param _now Optional clock injector for testing — defaults to
|
|
254
|
+
* Date.now().
|
|
255
|
+
*/
|
|
256
|
+
export function cleanupInstallStagingDirs(pluginDir, _now = Date.now) {
|
|
257
|
+
const removed = [];
|
|
258
|
+
try {
|
|
259
|
+
// pluginDir is `<extensionsDir>/totalreclaw/dist` after build, so the
|
|
260
|
+
// siblings live two levels up. Resolve both candidates so the helper
|
|
261
|
+
// works regardless of whether the caller passes the package root or
|
|
262
|
+
// its `dist/` subdir.
|
|
263
|
+
const candidates = [
|
|
264
|
+
path.resolve(pluginDir, '..'), // <extensionsDir>/totalreclaw → siblings dir if pluginDir is `dist`
|
|
265
|
+
path.resolve(pluginDir, '..', '..'), // <extensionsDir>/ → siblings dir if pluginDir is package root
|
|
266
|
+
];
|
|
267
|
+
for (const extensionsDir of candidates) {
|
|
268
|
+
let entries;
|
|
269
|
+
try {
|
|
270
|
+
entries = fs.readdirSync(extensionsDir);
|
|
271
|
+
}
|
|
272
|
+
catch {
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
for (const name of entries) {
|
|
276
|
+
if (!name.startsWith('.openclaw-install-stage-'))
|
|
277
|
+
continue;
|
|
278
|
+
const target = path.join(extensionsDir, name);
|
|
279
|
+
try {
|
|
280
|
+
const st = fs.lstatSync(target);
|
|
281
|
+
if (!st.isDirectory())
|
|
282
|
+
continue;
|
|
283
|
+
fs.rmSync(target, { recursive: true, force: true });
|
|
284
|
+
removed.push(target);
|
|
285
|
+
}
|
|
286
|
+
catch {
|
|
287
|
+
// Best-effort — skip unreadable / racy entries.
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
catch {
|
|
293
|
+
// Best-effort — never crash plugin init on cleanup failure.
|
|
294
|
+
}
|
|
295
|
+
return removed;
|
|
296
|
+
}
|
|
297
|
+
// ---------------------------------------------------------------------------
|
|
216
298
|
// Auto-bootstrap of credentials.json (3.1.0 first-run UX)
|
|
217
299
|
// ---------------------------------------------------------------------------
|
|
218
300
|
/**
|