@totalreclaw/totalreclaw 3.3.1-rc.20 → 3.3.1-rc.22
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 +49 -1
- package/SKILL.md +1 -1
- package/api-client.ts +18 -11
- package/claims-helper.ts +47 -1
- package/config.ts +82 -4
- package/confirm-indexed.ts +191 -0
- package/crypto.ts +10 -2
- package/dist/api-client.js +17 -11
- package/dist/claims-helper.js +19 -1
- package/dist/config.js +78 -4
- package/dist/confirm-indexed.js +127 -0
- package/dist/crypto.js +10 -2
- package/dist/embedder-cache.js +185 -0
- package/dist/embedder-loader.js +121 -0
- package/dist/embedder-network.js +301 -0
- package/dist/embedding.js +88 -33
- package/dist/fs-helpers.js +244 -0
- package/dist/index.js +223 -68
- package/dist/llm-client.js +4 -3
- package/dist/lsh.js +7 -2
- package/dist/pin.js +15 -1
- package/dist/relay-headers.js +44 -0
- package/dist/reranker.js +19 -52
- package/dist/retype-setscope.js +25 -5
- package/dist/subgraph-search.js +4 -3
- package/dist/subgraph-store.js +15 -10
- package/embedder-cache.ts +230 -0
- package/embedder-loader.ts +189 -0
- package/embedder-network.ts +350 -0
- package/embedding.ts +119 -51
- package/fs-helpers.ts +277 -0
- package/index.ts +252 -86
- package/llm-client.ts +4 -3
- package/lsh.ts +7 -2
- package/package.json +11 -5
- package/pin.ts +31 -0
- package/relay-headers.ts +50 -0
- package/reranker.ts +19 -52
- package/retype-setscope.ts +57 -8
- package/skill.json +1 -1
- 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
|
|
@@ -446,7 +493,8 @@ See: `plans/2026-04-22-plugin-3.3.1-provider-agnostic-llm.md` (internal).
|
|
|
446
493
|
- `--json` — emits a structured payload (requires `--non-interactive`).
|
|
447
494
|
- `--mode <generate|restore>` — skip the menu prompt.
|
|
448
495
|
- `--phrase <12-or-24>` — required for `--mode restore`; `-` reads stdin.
|
|
449
|
-
- `--emit-phrase` — opt-in
|
|
496
|
+
- `--emit-phrase` — historic opt-in flag (do not invoke via agent shell:
|
|
497
|
+
forbidden by the phrase-safety rule); included plaintext phrase in the
|
|
450
498
|
JSON payload. Default omits the phrase; the agent should direct the
|
|
451
499
|
user to read `~/.totalreclaw/credentials.json` in their terminal.
|
|
452
500
|
|
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.1-rc.
|
|
4
|
+
version: 3.3.1-rc.22
|
|
5
5
|
author: TotalReclaw Team
|
|
6
6
|
license: MIT
|
|
7
7
|
homepage: https://totalreclaw.xyz
|
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/claims-helper.ts
CHANGED
|
@@ -96,6 +96,14 @@ export interface BuildClaimInput {
|
|
|
96
96
|
sourceAgent: string;
|
|
97
97
|
/** Creation timestamp. Defaults to now. */
|
|
98
98
|
extractedAt?: string;
|
|
99
|
+
/**
|
|
100
|
+
* 3.3.1-rc.22 — optional embedding-model id stamped on the claim for
|
|
101
|
+
* forward-compat. Defaults to omitted (callers that already know the
|
|
102
|
+
* active embedder pass it in; legacy paths leave it unset). The field
|
|
103
|
+
* is plugin-scoped — it survives the core validator's strip pass via
|
|
104
|
+
* the same re-attach path used for `schema_version` / `volatility`.
|
|
105
|
+
*/
|
|
106
|
+
embeddingModelId?: string;
|
|
99
107
|
}
|
|
100
108
|
|
|
101
109
|
/**
|
|
@@ -112,7 +120,7 @@ export interface BuildClaimInput {
|
|
|
112
120
|
* payload (see `subgraph-store.ts::encodeFactProtobuf`).
|
|
113
121
|
*/
|
|
114
122
|
export function buildCanonicalClaim(input: BuildClaimInput): string {
|
|
115
|
-
const { fact, importance, extractedAt } = input;
|
|
123
|
+
const { fact, importance, extractedAt, embeddingModelId } = input;
|
|
116
124
|
|
|
117
125
|
// Defensive: ensure fact.source is always populated before v1 validation.
|
|
118
126
|
// `applyProvenanceFilterLax` should have set this upstream; this is the
|
|
@@ -125,6 +133,7 @@ export function buildCanonicalClaim(input: BuildClaimInput): string {
|
|
|
125
133
|
fact: factWithSource,
|
|
126
134
|
importance,
|
|
127
135
|
createdAt: extractedAt,
|
|
136
|
+
embeddingModelId,
|
|
128
137
|
});
|
|
129
138
|
}
|
|
130
139
|
|
|
@@ -173,6 +182,14 @@ export interface BuildClaimV1Input {
|
|
|
173
182
|
* when provided.
|
|
174
183
|
*/
|
|
175
184
|
pinStatus?: PinStatus;
|
|
185
|
+
/**
|
|
186
|
+
* 3.3.1-rc.22 — optional embedding-model id stamped on the claim for
|
|
187
|
+
* distillation forward-compat. Survives the core validator strip pass
|
|
188
|
+
* via the same re-attach path used for `schema_version` / `volatility`.
|
|
189
|
+
* When omitted the field is not emitted (legacy claims remain untagged
|
|
190
|
+
* and are read back as "unspecified").
|
|
191
|
+
*/
|
|
192
|
+
embeddingModelId?: string;
|
|
176
193
|
}
|
|
177
194
|
|
|
178
195
|
/**
|
|
@@ -257,6 +274,13 @@ export function buildCanonicalClaimV1(input: BuildClaimV1Input): string {
|
|
|
257
274
|
if (fact.volatility && (VALID_MEMORY_VOLATILITIES as readonly string[]).includes(fact.volatility)) {
|
|
258
275
|
canonical.volatility = fact.volatility;
|
|
259
276
|
}
|
|
277
|
+
// 3.3.1-rc.22 — forward-compat embedder marker. Plugin-only field;
|
|
278
|
+
// survives the core validator via re-attach. Future distillation
|
|
279
|
+
// detects this on read to re-embed selectively without forcing a
|
|
280
|
+
// vault-wide rebuild.
|
|
281
|
+
if (typeof input.embeddingModelId === 'string' && input.embeddingModelId.length > 0) {
|
|
282
|
+
canonical.embedding_model_id = input.embeddingModelId;
|
|
283
|
+
}
|
|
260
284
|
|
|
261
285
|
return JSON.stringify(canonical);
|
|
262
286
|
}
|
|
@@ -313,6 +337,11 @@ export interface BuildV1ClaimBlobInput {
|
|
|
313
337
|
* non-pin write.
|
|
314
338
|
*/
|
|
315
339
|
pinStatus?: PinStatus;
|
|
340
|
+
/**
|
|
341
|
+
* 3.3.1-rc.22 — optional embedding-model id stamped on the claim for
|
|
342
|
+
* distillation forward-compat. See `BuildClaimV1Input.embeddingModelId`.
|
|
343
|
+
*/
|
|
344
|
+
embeddingModelId?: string;
|
|
316
345
|
}
|
|
317
346
|
|
|
318
347
|
/**
|
|
@@ -374,6 +403,10 @@ export function buildV1ClaimBlob(input: BuildV1ClaimBlobInput): string {
|
|
|
374
403
|
if (input.volatility && (VALID_MEMORY_VOLATILITIES as readonly string[]).includes(input.volatility)) {
|
|
375
404
|
canonical.volatility = input.volatility;
|
|
376
405
|
}
|
|
406
|
+
// 3.3.1-rc.22 — see `buildCanonicalClaimV1` comment.
|
|
407
|
+
if (typeof input.embeddingModelId === 'string' && input.embeddingModelId.length > 0) {
|
|
408
|
+
canonical.embedding_model_id = input.embeddingModelId;
|
|
409
|
+
}
|
|
377
410
|
return JSON.stringify(canonical);
|
|
378
411
|
}
|
|
379
412
|
|
|
@@ -431,6 +464,13 @@ export interface V1BlobReadResult {
|
|
|
431
464
|
* when the writer explicitly omitted the field (treated as `"unpinned"`).
|
|
432
465
|
*/
|
|
433
466
|
pinStatus?: PinStatus;
|
|
467
|
+
/**
|
|
468
|
+
* 3.3.1-rc.22 — embedder identity tag. Absent on claims written by
|
|
469
|
+
* older plugin versions. Forward-compat marker; consumers MAY use it
|
|
470
|
+
* to decide whether a claim's stored embedding matches the active
|
|
471
|
+
* embedder before letting cosine similarity make a relevance call.
|
|
472
|
+
*/
|
|
473
|
+
embeddingModelId?: string;
|
|
434
474
|
}
|
|
435
475
|
|
|
436
476
|
export function readV1Blob(decrypted: string): V1BlobReadResult | null {
|
|
@@ -497,6 +537,12 @@ export function readV1Blob(decrypted: string): V1BlobReadResult | null {
|
|
|
497
537
|
result.pinStatus = ps;
|
|
498
538
|
}
|
|
499
539
|
}
|
|
540
|
+
// 3.3.1-rc.22 — pull the embedder identity tag through. Plugin-only
|
|
541
|
+
// field added by `buildCanonicalClaimV1` / `buildV1ClaimBlob` after
|
|
542
|
+
// core validation.
|
|
543
|
+
if (typeof obj.embedding_model_id === 'string' && obj.embedding_model_id.length > 0) {
|
|
544
|
+
result.embeddingModelId = obj.embedding_model_id;
|
|
545
|
+
}
|
|
500
546
|
|
|
501
547
|
return result;
|
|
502
548
|
} catch {
|
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,18 +40,27 @@ 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',
|
|
40
49
|
] as const;
|
|
41
50
|
|
|
51
|
+
// Migration guide URL — kept as a constant so the regression test can assert
|
|
52
|
+
// the exact link text in the warning. Pointing at GitHub raw-blob is more
|
|
53
|
+
// useful than the relative repo path: operators copying the warning out of
|
|
54
|
+
// stderr usually do not have the repo cloned. rc.22 finding #4.
|
|
55
|
+
export const ENV_VARS_REFERENCE_URL =
|
|
56
|
+
'https://github.com/p-diogo/totalreclaw/blob/main/docs/guides/env-vars-reference.md';
|
|
57
|
+
|
|
42
58
|
function warnRemovedEnvVars(warn: (msg: string) => void = console.warn): void {
|
|
43
59
|
const set = REMOVED_ENV_VARS.filter((name) => process.env[name] !== undefined);
|
|
44
60
|
if (set.length === 0) return;
|
|
45
61
|
warn(
|
|
46
62
|
`TotalReclaw: ignoring removed env var(s): ${set.join(', ')}. ` +
|
|
47
|
-
`
|
|
63
|
+
`Migration guide: ${ENV_VARS_REFERENCE_URL}`,
|
|
48
64
|
);
|
|
49
65
|
}
|
|
50
66
|
|
|
@@ -63,6 +79,27 @@ export function getRecoveryPhrase(): string {
|
|
|
63
79
|
return _recoveryPhraseOverride ?? process.env.TOTALRECLAW_RECOVERY_PHRASE ?? '';
|
|
64
80
|
}
|
|
65
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Read the QA / observability session tag from the environment.
|
|
84
|
+
*
|
|
85
|
+
* When set, every outbound relay call adds the ``X-TotalReclaw-Session``
|
|
86
|
+
* header so relay logs (and Axiom queries) can be filtered by this tag —
|
|
87
|
+
* this is what the qa-totalreclaw skill relies on to scope log searches per
|
|
88
|
+
* QA run. When unset, returns ``null`` and the header is omitted.
|
|
89
|
+
*
|
|
90
|
+
* Read via getter (not snapshotted) so operators / test harnesses can flip
|
|
91
|
+
* the var between calls without reloading the module.
|
|
92
|
+
*
|
|
93
|
+
* Mirrors the Python-side ``RelayClient._session_id`` resolution priority.
|
|
94
|
+
* See internal#127 / `docs/guides/env-vars-reference.md`.
|
|
95
|
+
*/
|
|
96
|
+
export function getSessionId(): string | null {
|
|
97
|
+
const raw = process.env.TOTALRECLAW_SESSION_ID;
|
|
98
|
+
if (raw === undefined) return null;
|
|
99
|
+
const trimmed = raw.trim();
|
|
100
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
101
|
+
}
|
|
102
|
+
|
|
66
103
|
/**
|
|
67
104
|
* Runtime override for chain ID, set after the relay billing response is
|
|
68
105
|
* read. Free tier stays on 84532 (Base Sepolia); Pro tier flips to 100
|
|
@@ -92,6 +129,15 @@ export const CONFIG = {
|
|
|
92
129
|
get recoveryPhrase(): string {
|
|
93
130
|
return getRecoveryPhrase();
|
|
94
131
|
},
|
|
132
|
+
/**
|
|
133
|
+
* Optional QA / observability session tag forwarded to the relay as
|
|
134
|
+
* ``X-TotalReclaw-Session``. See `getSessionId()` above. Getter form so
|
|
135
|
+
* tests + harnesses can flip the env between calls. ``null`` when unset
|
|
136
|
+
* (header omitted).
|
|
137
|
+
*/
|
|
138
|
+
get sessionId(): string | null {
|
|
139
|
+
return getSessionId();
|
|
140
|
+
},
|
|
95
141
|
serverUrl: (process.env.TOTALRECLAW_SERVER_URL || 'https://api.totalreclaw.xyz').replace(/\/+$/, ''),
|
|
96
142
|
selfHosted: process.env.TOTALRECLAW_SELF_HOSTED === 'true',
|
|
97
143
|
credentialsPath: process.env.TOTALRECLAW_CREDENTIALS_PATH || path.join(home, '.totalreclaw', 'credentials.json'),
|
|
@@ -214,11 +260,43 @@ export const CONFIG = {
|
|
|
214
260
|
return process.env.TOTALRECLAW_QA_REPO || '';
|
|
215
261
|
},
|
|
216
262
|
|
|
263
|
+
// 3.3.1-rc.21 (issue #128): verbose-register flag. When enabled, the
|
|
264
|
+
// plugin emits opt-in `info`-level breadcrumbs after sensitive
|
|
265
|
+
// registerTool calls (currently `totalreclaw_pair`) to help ops/QA
|
|
266
|
+
// grep gateway logs for definitive proof the tool was declared.
|
|
267
|
+
// Default OFF — the breadcrumb is debug-grade and was bleeding into
|
|
268
|
+
// `openclaw agent --json` stdout, breaking programmatic parsers.
|
|
269
|
+
// Enable with either:
|
|
270
|
+
// TOTALRECLAW_VERBOSE_REGISTER=1 (specific opt-in)
|
|
271
|
+
// TOTALRECLAW_DEBUG=1 (general debug toggle)
|
|
272
|
+
// Read via getter so flipping the env at runtime takes effect on the
|
|
273
|
+
// next gateway start without a rebuild.
|
|
274
|
+
get verboseRegister(): boolean {
|
|
275
|
+
const specific = (process.env.TOTALRECLAW_VERBOSE_REGISTER ?? '').trim().toLowerCase();
|
|
276
|
+
if (specific === '1' || specific === 'true' || specific === 'yes') return true;
|
|
277
|
+
const general = (process.env.TOTALRECLAW_DEBUG ?? '').trim().toLowerCase();
|
|
278
|
+
return general === '1' || general === 'true' || general === 'yes';
|
|
279
|
+
},
|
|
280
|
+
|
|
217
281
|
// Paths
|
|
218
282
|
home,
|
|
219
283
|
billingCachePath: path.join(home, '.totalreclaw', 'billing-cache.json'),
|
|
220
284
|
cachePath: process.env.TOTALRECLAW_CACHE_PATH || path.join(home, '.totalreclaw', 'cache.enc'),
|
|
221
285
|
openclawWorkspace: path.join(home, '.openclaw', 'workspace'),
|
|
286
|
+
|
|
287
|
+
// 3.3.1-rc.22 — lazy embedder bundle cache. The embedder
|
|
288
|
+
// (`@huggingface/transformers` + `onnxruntime-node` + the q4 ONNX
|
|
289
|
+
// model) is no longer shipped inside the plugin tarball; it is fetched
|
|
290
|
+
// on first `embed()` call from a versioned GitHub Release and cached
|
|
291
|
+
// here. Separate path from `cachePath` (encrypted vault cache) so the
|
|
292
|
+
// two never collide. See `embedder-loader.ts`.
|
|
293
|
+
embedderCachePath: process.env.TOTALRECLAW_EMBEDDER_CACHE_PATH || path.join(home, '.totalreclaw', 'embedder'),
|
|
294
|
+
|
|
295
|
+
// 3.3.1-rc.22 — override the GitHub-Releases URL templates. Only useful
|
|
296
|
+
// for air-gapped / mirror deployments and self-hosted CI. Empty string
|
|
297
|
+
// falls back to the static defaults baked into the embedder code path.
|
|
298
|
+
embedderBundleUrlTemplate: process.env.TOTALRECLAW_EMBEDDER_BUNDLE_URL || '',
|
|
299
|
+
embedderManifestUrlTemplate: process.env.TOTALRECLAW_EMBEDDER_MANIFEST_URL || '',
|
|
222
300
|
} as const;
|
|
223
301
|
|
|
224
302
|
// ---------------------------------------------------------------------------
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read-after-write primitive — confirm a fact id has been indexed by the
|
|
3
|
+
* subgraph after an on-chain mutation (retype / set_scope / pin / unpin /
|
|
4
|
+
* forget).
|
|
5
|
+
*
|
|
6
|
+
* Wraps the pure-compute halves exported by `@totalreclaw/core`
|
|
7
|
+
* (`wasmConfirmIndexedQuery`, `wasmConfirmIndexedParse`) in a host-side
|
|
8
|
+
* polling loop. Subgraph indexer lag on Gnosis production runs 5-30s; this
|
|
9
|
+
* helper polls every `pollIntervalMs` (default 1000ms) up to `timeoutMs`
|
|
10
|
+
* (default 30000ms).
|
|
11
|
+
*
|
|
12
|
+
* Why this exists
|
|
13
|
+
* ---------------
|
|
14
|
+
* Pre-fix, mutation tools returned `{success: true}` based on the bundler
|
|
15
|
+
* ack alone. A user who immediately ran `totalreclaw_export` would see the
|
|
16
|
+
* pre-mutation state, because the subgraph indexer hadn't yet observed the
|
|
17
|
+
* L1 inclusion. Confusing UX, root cause of rc.18 finding #117.
|
|
18
|
+
*
|
|
19
|
+
* Post-fix, mutation tools call `confirmIndexed(newFactId)` after submitting
|
|
20
|
+
* the batched UserOp; on success they return normally, on timeout they
|
|
21
|
+
* return `{success: true, partial: true, ...}` with the chain write
|
|
22
|
+
* acknowledged but the indexer-level confirmation withheld.
|
|
23
|
+
*
|
|
24
|
+
* Mnemonic isolation: this helper never touches the mnemonic, encryption
|
|
25
|
+
* key, or any decrypted blob. Only reads the public {id, isActive,
|
|
26
|
+
* blockNumber} of a fact.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { createRequire } from 'node:module';
|
|
30
|
+
import { getSubgraphConfig } from './subgraph-store.js';
|
|
31
|
+
import { buildRelayHeaders } from './relay-headers.js';
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The four `wasmConfirmIndexed*` exports below ship in `@totalreclaw/core@2.3.x`
|
|
35
|
+
* (the `confirm` module added in 2.2.x). Older type declarations don't list
|
|
36
|
+
* them — we use a structural cast so the plugin's `--noCheck` build is
|
|
37
|
+
* unblocked and runtime resolution is via dynamic require.
|
|
38
|
+
*/
|
|
39
|
+
type ConfirmIndexedCore = typeof import('@totalreclaw/core') & {
|
|
40
|
+
wasmConfirmIndexedQuery(): string;
|
|
41
|
+
wasmConfirmIndexedParse(responseJson: string): boolean;
|
|
42
|
+
wasmConfirmIndexedDefaultPollMs(): number;
|
|
43
|
+
wasmConfirmIndexedDefaultTimeoutMs(): number;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const requireWasm = createRequire(import.meta.url);
|
|
47
|
+
let _wasm: ConfirmIndexedCore | null = null;
|
|
48
|
+
function getWasm(): ConfirmIndexedCore {
|
|
49
|
+
if (!_wasm) _wasm = requireWasm('@totalreclaw/core') as unknown as ConfirmIndexedCore;
|
|
50
|
+
return _wasm!;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Result of a confirm-indexed poll loop. */
|
|
54
|
+
export interface ConfirmIndexedResult {
|
|
55
|
+
/** True if the fact was found and `isActive == true` before the timeout. */
|
|
56
|
+
indexed: boolean;
|
|
57
|
+
/** Number of poll attempts before resolution (or timeout). */
|
|
58
|
+
attempts: number;
|
|
59
|
+
/** Total wall-clock ms spent polling. */
|
|
60
|
+
elapsedMs: number;
|
|
61
|
+
/** Last error message from the GraphQL adapter, if any. */
|
|
62
|
+
lastError?: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface ConfirmIndexedOptions {
|
|
66
|
+
/** Override the default 1s poll interval. */
|
|
67
|
+
pollIntervalMs?: number;
|
|
68
|
+
/** Override the default 30s total timeout. */
|
|
69
|
+
timeoutMs?: number;
|
|
70
|
+
/** Override `${relayUrl}/v1/subgraph` (test injection point). */
|
|
71
|
+
subgraphUrl?: string;
|
|
72
|
+
/** Override the auth-key-hex header (defaults to none). */
|
|
73
|
+
authKeyHex?: string;
|
|
74
|
+
/**
|
|
75
|
+
* Direction of the read-after-write check.
|
|
76
|
+
* - `"active"` (default) — wait until fact is found AND `isActive == true`.
|
|
77
|
+
* Use after pin/unpin/retype/set_scope (we're confirming the *new* fact
|
|
78
|
+
* id has appeared).
|
|
79
|
+
* - `"inactive"` — wait until fact either disappears OR `isActive ==
|
|
80
|
+
* false`. Use after `forget`, where the *original* fact id is being
|
|
81
|
+
* tombstoned and a confirm-indexed wait should resolve once the
|
|
82
|
+
* subgraph has flipped it.
|
|
83
|
+
*/
|
|
84
|
+
expect?: 'active' | 'inactive';
|
|
85
|
+
/**
|
|
86
|
+
* Test injection — caller-provided `fetch`-compatible POST. Defaults to
|
|
87
|
+
* the global `fetch`. Letting tests stub this out avoids a network mock
|
|
88
|
+
* dependency.
|
|
89
|
+
*/
|
|
90
|
+
poster?: (url: string, body: string, headers: Record<string, string>) => Promise<{
|
|
91
|
+
ok: boolean;
|
|
92
|
+
status: number;
|
|
93
|
+
text: () => Promise<string>;
|
|
94
|
+
}>;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Poll the subgraph until the new fact id is indexed-and-active, or the
|
|
99
|
+
* timeout elapses. Returns a result object describing the outcome — never
|
|
100
|
+
* throws on indexer-level transient errors; the caller decides whether to
|
|
101
|
+
* surface a `partial: true` flag based on `result.indexed`.
|
|
102
|
+
*
|
|
103
|
+
* The host's submitBatch already returned a tx hash before this is called,
|
|
104
|
+
* so on `indexed: false` the on-chain write is still acknowledged — just not
|
|
105
|
+
* yet visible in the read API.
|
|
106
|
+
*/
|
|
107
|
+
export async function confirmIndexed(
|
|
108
|
+
factId: string,
|
|
109
|
+
options: ConfirmIndexedOptions = {},
|
|
110
|
+
): Promise<ConfirmIndexedResult> {
|
|
111
|
+
// WASM bindings may be unavailable (e.g. core@<2.3.0 not yet published).
|
|
112
|
+
// In that case the chain write has still succeeded — confirm step is
|
|
113
|
+
// observational only. Return `indexed: false` so callers surface
|
|
114
|
+
// `partial: true` rather than fail the whole tool invocation.
|
|
115
|
+
let wasm: ConfirmIndexedCore;
|
|
116
|
+
let query: string;
|
|
117
|
+
let pollIntervalMs: number;
|
|
118
|
+
let timeoutMs: number;
|
|
119
|
+
try {
|
|
120
|
+
wasm = getWasm();
|
|
121
|
+
pollIntervalMs = options.pollIntervalMs ?? Number(wasm.wasmConfirmIndexedDefaultPollMs?.() ?? 1000);
|
|
122
|
+
timeoutMs = options.timeoutMs ?? Number(wasm.wasmConfirmIndexedDefaultTimeoutMs?.() ?? 30000);
|
|
123
|
+
query = wasm.wasmConfirmIndexedQuery();
|
|
124
|
+
} catch (err) {
|
|
125
|
+
return {
|
|
126
|
+
indexed: false,
|
|
127
|
+
attempts: 0,
|
|
128
|
+
elapsedMs: 0,
|
|
129
|
+
lastError: `confirm-indexed wasm bindings unavailable: ${err instanceof Error ? err.message : String(err)}`,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
const subgraphUrl =
|
|
133
|
+
options.subgraphUrl ?? `${getSubgraphConfig().relayUrl}/v1/subgraph`;
|
|
134
|
+
|
|
135
|
+
const overrides: Record<string, string> = {
|
|
136
|
+
'Content-Type': 'application/json',
|
|
137
|
+
};
|
|
138
|
+
if (options.authKeyHex) overrides['Authorization'] = `Bearer ${options.authKeyHex}`;
|
|
139
|
+
const headers = buildRelayHeaders(overrides);
|
|
140
|
+
|
|
141
|
+
const body = JSON.stringify({ query, variables: { id: factId } });
|
|
142
|
+
|
|
143
|
+
const poster =
|
|
144
|
+
options.poster ??
|
|
145
|
+
(async (url, b, h) => {
|
|
146
|
+
const r = await fetch(url, { method: 'POST', headers: h, body: b });
|
|
147
|
+
return { ok: r.ok, status: r.status, text: () => r.text() };
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const expect = options.expect ?? 'active';
|
|
151
|
+
const start = Date.now();
|
|
152
|
+
let attempts = 0;
|
|
153
|
+
let lastError: string | undefined;
|
|
154
|
+
|
|
155
|
+
while (Date.now() - start < timeoutMs) {
|
|
156
|
+
attempts++;
|
|
157
|
+
try {
|
|
158
|
+
const r = await poster(subgraphUrl, body, headers);
|
|
159
|
+
if (r.ok) {
|
|
160
|
+
const txt = await r.text();
|
|
161
|
+
try {
|
|
162
|
+
// wasmConfirmIndexedParse returns `true` when fact is present AND
|
|
163
|
+
// isActive==true. For `expect: 'inactive'` we invert: a `false`
|
|
164
|
+
// (fact missing OR present-but-inactive) is the resolution signal.
|
|
165
|
+
const isActive = wasm.wasmConfirmIndexedParse(txt);
|
|
166
|
+
const resolved = expect === 'active' ? isActive : !isActive;
|
|
167
|
+
if (resolved) {
|
|
168
|
+
return { indexed: true, attempts, elapsedMs: Date.now() - start };
|
|
169
|
+
}
|
|
170
|
+
} catch (parseErr) {
|
|
171
|
+
lastError = parseErr instanceof Error ? parseErr.message : String(parseErr);
|
|
172
|
+
}
|
|
173
|
+
} else {
|
|
174
|
+
lastError = `HTTP ${r.status}`;
|
|
175
|
+
}
|
|
176
|
+
} catch (err) {
|
|
177
|
+
lastError = err instanceof Error ? err.message : String(err);
|
|
178
|
+
}
|
|
179
|
+
// Sleep before the next attempt — but only if there's still budget.
|
|
180
|
+
const remaining = timeoutMs - (Date.now() - start);
|
|
181
|
+
if (remaining <= 0) break;
|
|
182
|
+
await new Promise((res) => setTimeout(res, Math.min(pollIntervalMs, remaining)));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
indexed: false,
|
|
187
|
+
attempts,
|
|
188
|
+
elapsedMs: Date.now() - start,
|
|
189
|
+
lastError,
|
|
190
|
+
};
|
|
191
|
+
}
|
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
|
|