@totalreclaw/totalreclaw 3.3.12-rc.9 → 3.3.12
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 +33 -27
- package/api-client.ts +17 -9
- package/batch-gate.ts +42 -0
- package/billing-cache.ts +72 -23
- package/claims-helper.ts +2 -1
- package/config.ts +95 -13
- package/consolidation.ts +6 -13
- package/contradiction-sync.ts +19 -14
- package/credential-provider.ts +184 -0
- package/crypto.ts +27 -160
- package/dist/api-client.js +17 -9
- package/dist/batch-gate.js +40 -0
- package/dist/billing-cache.js +54 -24
- package/dist/claims-helper.js +2 -1
- package/dist/config.js +91 -13
- package/dist/consolidation.js +6 -15
- package/dist/contradiction-sync.js +15 -15
- package/dist/credential-provider.js +145 -0
- package/dist/crypto.js +17 -137
- package/dist/download-ux.js +11 -7
- package/dist/embedder-loader.js +266 -0
- package/dist/embedding.js +36 -3
- package/dist/entry.js +123 -0
- package/dist/fs-helpers.js +75 -379
- package/dist/import-adapters/gemini-adapter.js +29 -159
- package/dist/index.js +864 -2631
- package/dist/memory-runtime.js +459 -0
- package/dist/native-memory.js +123 -0
- package/dist/onboarding-cli.js +4 -8
- package/dist/pair-cli-relay.js +1 -8
- package/dist/pair-cli.js +1 -1
- package/dist/pair-crypto.js +16 -358
- package/dist/pair-http.js +147 -4
- package/dist/relay.js +140 -0
- package/dist/reranker.js +13 -8
- package/dist/semantic-dedup.js +5 -7
- package/dist/skill-register.js +97 -0
- package/dist/subgraph-search.js +3 -1
- package/dist/subgraph-store.js +348 -290
- package/dist/tool-gating.js +39 -26
- package/dist/tools.js +367 -0
- package/dist/tr-cli-export-helper.js +3 -3
- package/dist/tr-cli.js +65 -156
- package/dist/trajectory-poller.js +155 -9
- package/dist/vault-crypto.js +551 -0
- package/download-ux.ts +12 -6
- package/embedder-loader.ts +293 -1
- package/embedding.ts +43 -3
- package/entry.ts +132 -0
- package/fs-helpers.ts +93 -458
- package/import-adapters/gemini-adapter.ts +38 -183
- package/index.ts +912 -2917
- package/memory-runtime.ts +723 -0
- package/native-memory.ts +196 -0
- package/onboarding-cli.ts +3 -9
- package/openclaw.plugin.json +5 -17
- package/package.json +6 -3
- package/pair-cli-relay.ts +0 -9
- package/pair-cli.ts +1 -1
- package/pair-crypto.ts +41 -483
- package/pair-http.ts +194 -5
- package/postinstall.mjs +138 -0
- package/relay.ts +172 -0
- package/reranker.ts +13 -8
- package/semantic-dedup.ts +5 -6
- package/skill-register.ts +146 -0
- package/skill.json +1 -1
- package/subgraph-search.ts +3 -1
- package/subgraph-store.ts +367 -307
- package/tool-gating.ts +39 -26
- package/tools.ts +499 -0
- package/tr-cli-export-helper.ts +3 -3
- package/tr-cli.ts +69 -182
- package/trajectory-poller.ts +162 -10
- package/vault-crypto.ts +705 -0
- package/auto-pair-on-load.ts +0 -308
- package/dist/auto-pair-on-load.js +0 -197
- package/dist/pair-pending-injection.js +0 -125
- package/pair-pending-injection.ts +0 -205
package/SKILL.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: totalreclaw
|
|
3
|
-
description: "End-to-end encrypted, decentralized memory for OpenClaw.
|
|
4
|
-
version: 3.3.12-rc.
|
|
3
|
+
description: "End-to-end encrypted, decentralized memory for OpenClaw. A native kind:memory provider — recall is automatic via memory_search/memory_get, and facts are captured in the background. Trigger on 'install TotalReclaw', 'set up TotalReclaw', 'restore my recovery phrase', any recall request ('what do you remember about me', 'what's my X'), AND any explicit remember request ('remember X', 'save X')."
|
|
4
|
+
version: 3.3.12-rc.23
|
|
5
5
|
author: TotalReclaw Team
|
|
6
6
|
license: MIT
|
|
7
7
|
homepage: https://totalreclaw.xyz
|
|
@@ -22,55 +22,61 @@ metadata:
|
|
|
22
22
|
- persistent-context
|
|
23
23
|
---
|
|
24
24
|
|
|
25
|
-
# TotalReclaw — OpenClaw
|
|
25
|
+
# TotalReclaw — OpenClaw native memory provider
|
|
26
26
|
|
|
27
|
-
End-to-end encrypted memory stored across a decentralized network (not on a single company server).
|
|
27
|
+
End-to-end encrypted memory stored across a decentralized network (not on a single company server). TotalReclaw is OpenClaw's **native `kind:"memory"` provider**: recall runs through the conventional `memory_search` / `memory_get` tools (the same surface `active-memory` uses), and **facts are captured automatically in the background** — you do not call a tool to remember each fact. This skill tells you the install + account-setup flow and when to drive recall explicitly.
|
|
28
28
|
|
|
29
|
-
##
|
|
29
|
+
## How memory works here (read first)
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
- **Recall is native.** `memory_search` and `memory_get` are the memory tools. OpenClaw's `active-memory` sub-agent routes recall through them and the plugin injects guidance telling you when to search. When the user asks about themselves ("what's my X?", "do I X?", "what do you remember?"), call `memory_search`. If 0 results, say so plainly — don't guess from session history.
|
|
32
|
+
- **Capture is automatic.** The plugin extracts facts from the conversation in the background (trajectory poller). You do **not** call a tool on every preference/fact the user states — that happens for you. Never write user facts to `MEMORY.md`, `USER.md`, or any local file; those are agent-persona config, user memories live encrypted on-chain.
|
|
33
|
+
- **Explicit remember only on request.** If the user explicitly says "remember X" / "save X" / "note X" / "don't forget X", use the CLI: `tr remember --json "X"`. One call per distinct fact.
|
|
32
34
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
When the user asks about themselves ("what's my X?", "do I X?", "what do you remember?"), call `totalreclaw_recall` FIRST. If 0 results, say so plainly — don't guess from session history.
|
|
36
|
-
|
|
37
|
-
Do not describe storage as `"local"`, `"local-only"`, `"stored locally"`, `"on disk"`, `"local memory"`, `"local storage"`, or `"without an account"` — memories are decentralized on-chain (not on any single company server) and an account is required to derive the encryption key.
|
|
35
|
+
Do not describe storage as `"local"`, `"local-only"`, `"stored locally"`, `"on disk"`, or `"without an account"` — memories are decentralized on-chain (not on any single company server) and an account is required to derive the encryption key.
|
|
38
36
|
|
|
39
37
|
## Phrase safety (HARD)
|
|
40
38
|
|
|
41
39
|
The 12-word recovery phrase MUST NEVER enter chat or the agent's LLM context. The plugin's browser-based pair flow handles phrase generation and import. Never echo it, never ask the user to paste it, never pass it to any tool. If the user pastes one anyway, tell them it's compromised and re-pair for a fresh wallet.
|
|
42
40
|
|
|
41
|
+
**HARD guardrail — install/reinstall/upgrade flows:** During install, reinstall, upgrade, or any verification steps, the agent MUST NOT read, list, cat, show, display, or inspect `credentials.json`, `~/.totalreclaw/credentials.json`, `~/.totalreclaw*/credentials.json`, `~/.openclaw/plugins/totalreclaw/credentials.json`, or ANY credentials/phrase/key/secrets file — for ANY reason, including "verifying the install." These files contain encryption key material and Smart Account addresses that MUST NOT enter the LLM context. Never run `ls`, `find`, `cat`, `show`, or similar commands against plugin directories to "check files."
|
|
42
|
+
|
|
43
43
|
## Install
|
|
44
44
|
|
|
45
45
|
```
|
|
46
|
-
openclaw plugins install @totalreclaw/totalreclaw
|
|
46
|
+
openclaw plugins install @totalreclaw/totalreclaw
|
|
47
47
|
openclaw skills install totalreclaw
|
|
48
48
|
```
|
|
49
49
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
## Setup (plugin-driven)
|
|
50
|
+
Published on npm + ClawHub (`p-diogo`, GitHub-verified). For a release candidate, pin with `@rc`; stable resolves from `latest`. The plugin self-applies any gateway config it needs on load and auto-reloads (see Restart below) — no manual gateway restart.
|
|
53
51
|
|
|
54
|
-
|
|
52
|
+
## Setup (pair — user-initiated QR)
|
|
55
53
|
|
|
56
|
-
|
|
54
|
+
Pairing is a deliberate, user-initiated QR flow (the plugin does **not** auto-pair on load). When credentials don't exist:
|
|
57
55
|
|
|
58
|
-
|
|
56
|
+
1. Run `tr pair --json` → returns `{"url":"https://…/pair/p/<id>#pk=…","pin":"123456","expires_at_ms":…}`.
|
|
57
|
+
2. Surface the **url** and **pin** to the user verbatim (read from the JSON; never invent values): "Open `<url>` in your browser, enter PIN `<pin>`, and generate or paste your 12-word recovery phrase. Reply done once it's sealed."
|
|
58
|
+
3. The user completes in the browser (phrase is generated/imported browser-side, encrypted, posted to the relay — never touches this chat). On completion `~/.totalreclaw/credentials.json` is written.
|
|
59
|
+
4. Confirm: `✓ TotalReclaw set up.` (If credentials already exist when first asked: `✓ TotalReclaw is already set up.`)
|
|
59
60
|
|
|
60
|
-
|
|
61
|
+
Pair is UNCONDITIONAL when no credentials — the user installing/requesting setup IS the consent; do not add a second consent gate. `tr --help` lists every subcommand.
|
|
61
62
|
|
|
62
63
|
```bash
|
|
63
64
|
TR_CLI="$(ls -d ~/.openclaw/npm/node_modules/@totalreclaw/totalreclaw/dist/tr-cli.js ~/.openclaw/extensions/totalreclaw/dist/tr-cli.js 2>/dev/null | head -1)"
|
|
64
|
-
node "$TR_CLI"
|
|
65
|
-
node "$TR_CLI"
|
|
66
|
-
node "$TR_CLI" recall --json "where does the user work" --limit 5 # {"results":[{"text":"...","score":0.8}]}
|
|
67
|
-
node "$TR_CLI" pair --json # {"url":"...","pin":"123456","expires_at_ms":...} — plugin's auto-pair is preferred; only use this for re-pair
|
|
65
|
+
node "$TR_CLI" pair --json # start/re-pair
|
|
66
|
+
node "$TR_CLI" status --json # {"version":"…","paired":true,…}
|
|
68
67
|
```
|
|
69
68
|
|
|
70
|
-
|
|
69
|
+
## Restart (agent-driven, never user-manual)
|
|
70
|
+
|
|
71
|
+
The plugin self-reloads: after install it writes the gateway config it needs and signals `SIGUSR1` to its own PID so the gateway picks it up without a manual restart. **If `memory_search`/`memory_get` are not bound** (tools didn't take effect on first load), issue the literal slash command `/totalreclaw-restart` **autonomously** — announce it in one line, never ask the user to authorize it, never stall, never `kill` / `docker restart` / prompt the user to restart. This mirrors the autonomous-restart pattern other runtimes use; the user should never perform a manual restart.
|
|
72
|
+
|
|
73
|
+
## Tools + CLI surface
|
|
74
|
+
|
|
75
|
+
**Recall (native memory contract — agent-facing):** `memory_search`, `memory_get`.
|
|
71
76
|
|
|
72
|
-
|
|
77
|
+
**Explicit capture + curation (CLI — `tr`, i.e. `node "$TR_CLI" …`):**
|
|
78
|
+
`tr remember` (explicit write) · `tr pin` / `tr unpin` · `tr retype` · `tr set_scope` · `tr status` · `tr export` · `tr pair`. Import + plan upgrade run via the gateway subcommand: `openclaw totalreclaw import from <source> --file <path> [--json]`, `openclaw totalreclaw upgrade [--json]`, `openclaw totalreclaw import status|abort`.
|
|
73
79
|
|
|
74
|
-
|
|
80
|
+
The legacy `totalreclaw_*` agent tools and the `tr recall` CLI are retired — recall is `memory_search`, explicit capture is `tr remember`. If a stale guide references them, follow this SKILL instead.
|
|
75
81
|
|
|
76
|
-
Full
|
|
82
|
+
Full guide: <https://github.com/p-diogo/totalreclaw/blob/main/docs/guides/openclaw-setup.md>
|
package/api-client.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* TotalReclaw Plugin - HTTP API Client
|
|
3
3
|
*
|
|
4
|
-
* Communicates with the TotalReclaw server over JSON/HTTP.
|
|
5
|
-
*
|
|
4
|
+
* Communicates with the TotalReclaw server over JSON/HTTP. All wire I/O
|
|
5
|
+
* goes through `relay.ts` (the plugin's single network site); this module
|
|
6
|
+
* owns request/response shape, status-checking, and error context.
|
|
6
7
|
*
|
|
7
8
|
* All authenticated endpoints expect:
|
|
8
9
|
* Authorization: Bearer <hex-encoded-auth-key>
|
|
@@ -16,6 +17,7 @@
|
|
|
16
17
|
*/
|
|
17
18
|
|
|
18
19
|
import { buildRelayHeaders } from './relay-headers.js';
|
|
20
|
+
import { relayFetch } from './relay.js';
|
|
19
21
|
|
|
20
22
|
// ---------------------------------------------------------------------------
|
|
21
23
|
// Request / Response Types
|
|
@@ -131,7 +133,8 @@ export function createApiClient(serverUrl: string) {
|
|
|
131
133
|
authKeyHash: string,
|
|
132
134
|
saltHex: string,
|
|
133
135
|
): Promise<{ user_id: string }> {
|
|
134
|
-
const res = await
|
|
136
|
+
const res = await relayFetch({
|
|
137
|
+
url: `${baseUrl}/v1/register`,
|
|
135
138
|
method: 'POST',
|
|
136
139
|
headers: buildRelayHeaders({ 'Content-Type': 'application/json' }),
|
|
137
140
|
body: JSON.stringify({ auth_key_hash: authKeyHash, salt: saltHex }),
|
|
@@ -165,7 +168,8 @@ export function createApiClient(serverUrl: string) {
|
|
|
165
168
|
facts: StoreFactPayload[],
|
|
166
169
|
authKeyHex: string,
|
|
167
170
|
): Promise<{ ids: string[]; duplicate_ids?: string[] }> {
|
|
168
|
-
const res = await
|
|
171
|
+
const res = await relayFetch({
|
|
172
|
+
url: `${baseUrl}/v1/store`,
|
|
169
173
|
method: 'POST',
|
|
170
174
|
headers: buildRelayHeaders({
|
|
171
175
|
'Content-Type': 'application/json',
|
|
@@ -203,7 +207,8 @@ export function createApiClient(serverUrl: string) {
|
|
|
203
207
|
maxCandidates: number,
|
|
204
208
|
authKeyHex: string,
|
|
205
209
|
): Promise<SearchCandidate[]> {
|
|
206
|
-
const res = await
|
|
210
|
+
const res = await relayFetch({
|
|
211
|
+
url: `${baseUrl}/v1/search`,
|
|
207
212
|
method: 'POST',
|
|
208
213
|
headers: buildRelayHeaders({
|
|
209
214
|
'Content-Type': 'application/json',
|
|
@@ -234,7 +239,8 @@ export function createApiClient(serverUrl: string) {
|
|
|
234
239
|
* @param authKeyHex Hex-encoded raw auth key for Bearer header.
|
|
235
240
|
*/
|
|
236
241
|
async deleteFact(factId: string, authKeyHex: string): Promise<void> {
|
|
237
|
-
const res = await
|
|
242
|
+
const res = await relayFetch({
|
|
243
|
+
url: `${baseUrl}/v1/facts/${encodeURIComponent(factId)}`,
|
|
238
244
|
method: 'DELETE',
|
|
239
245
|
headers: buildRelayHeaders({
|
|
240
246
|
Authorization: `Bearer ${authKeyHex}`,
|
|
@@ -259,7 +265,8 @@ export function createApiClient(serverUrl: string) {
|
|
|
259
265
|
* @returns The number of facts that were actually deleted.
|
|
260
266
|
*/
|
|
261
267
|
async batchDelete(factIds: string[], authKeyHex: string): Promise<number> {
|
|
262
|
-
const res = await
|
|
268
|
+
const res = await relayFetch({
|
|
269
|
+
url: `${baseUrl}/v1/facts/batch-delete`,
|
|
263
270
|
method: 'POST',
|
|
264
271
|
headers: buildRelayHeaders({
|
|
265
272
|
'Content-Type': 'application/json',
|
|
@@ -295,7 +302,8 @@ export function createApiClient(serverUrl: string) {
|
|
|
295
302
|
const params = new URLSearchParams({ limit: String(limit) });
|
|
296
303
|
if (cursor) params.set('cursor', cursor);
|
|
297
304
|
|
|
298
|
-
const res = await
|
|
305
|
+
const res = await relayFetch({
|
|
306
|
+
url: `${baseUrl}/v1/export?${params.toString()}`,
|
|
299
307
|
method: 'GET',
|
|
300
308
|
headers: buildRelayHeaders({
|
|
301
309
|
Authorization: `Bearer ${authKeyHex}`,
|
|
@@ -325,7 +333,7 @@ export function createApiClient(serverUrl: string) {
|
|
|
325
333
|
*/
|
|
326
334
|
async health(): Promise<boolean> {
|
|
327
335
|
try {
|
|
328
|
-
const res = await
|
|
336
|
+
const res = await relayFetch({ url: `${baseUrl}/health`, method: 'GET' });
|
|
329
337
|
return res.status === 200;
|
|
330
338
|
} catch {
|
|
331
339
|
return false;
|
package/batch-gate.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Boot-time chain-gate predicate for client-side `executeBatch` UserOp
|
|
3
|
+
* submission. Spec #281 §9 Phase 1 (item imp-16).
|
|
4
|
+
*
|
|
5
|
+
* The gate batches via `executeBatch` on Gnosis (chain 100). After ops-1
|
|
6
|
+
* (2026-06-05) BOTH tiers run on Gnosis, so both now batch — the chain==100
|
|
7
|
+
* check is retained as a safety guard (a non-100 chain, which shouldn't occur
|
|
8
|
+
* after ops-1, falls back to single-fact). The legacy Free ⇒ Base Sepolia
|
|
9
|
+
* (84532) single-fact path is gone with the retired two-tier routing (#402).
|
|
10
|
+
* The `TOTALRECLAW_GNOSIS_BATCH_ENABLED` env var is a hard kill-switch —
|
|
11
|
+
* setting it to `false` disables batching regardless of chain, so ops can
|
|
12
|
+
* revert to single-fact submission without a client redeploy. Behaviour of
|
|
13
|
+
* `shouldBatchOnChain` is unchanged by #402.
|
|
14
|
+
*
|
|
15
|
+
* Read at boot only (module-load time). Per-write reads would re-parse the
|
|
16
|
+
* env on every submission — too expensive for the auto-extraction hot path
|
|
17
|
+
* and pointless because the env doesn't change mid-process.
|
|
18
|
+
*
|
|
19
|
+
* Sibling work-leaves wire `shouldBatchOnChain` into `submitFactBatchOnChain`
|
|
20
|
+
* (TS) and `agent/lifecycle.py` (Python mirror in `batch_gate.py`); this
|
|
21
|
+
* module ships the primitive only.
|
|
22
|
+
*
|
|
23
|
+
* Env read is centralized in entry.ts (env-reading seam, Task 1.3 of the
|
|
24
|
+
* OpenClaw native integration plan, 2026-06-21).
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { isGnosisBatchEnabledAtBoot } from './entry.js';
|
|
28
|
+
|
|
29
|
+
const GNOSIS_CHAIN_ID = 100;
|
|
30
|
+
|
|
31
|
+
export function shouldBatchOnChain(chainId: number): boolean {
|
|
32
|
+
if (!isGnosisBatchEnabledAtBoot()) return false;
|
|
33
|
+
return chainId === GNOSIS_CHAIN_ID;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const __testing = {
|
|
37
|
+
readGateForTests(env: NodeJS.ProcessEnv, chainId: number): boolean {
|
|
38
|
+
const enabled = (env.TOTALRECLAW_GNOSIS_BATCH_ENABLED ?? '').toLowerCase() !== 'false';
|
|
39
|
+
if (!enabled) return false;
|
|
40
|
+
return chainId === GNOSIS_CHAIN_ID;
|
|
41
|
+
},
|
|
42
|
+
};
|
package/billing-cache.ts
CHANGED
|
@@ -11,8 +11,12 @@
|
|
|
11
11
|
* This module:
|
|
12
12
|
* - reads/writes `~/.totalreclaw/billing-cache.json` (path from CONFIG)
|
|
13
13
|
* - exports `BillingCache`, `BILLING_CACHE_PATH`, `BILLING_CACHE_TTL`
|
|
14
|
-
* - keeps the chain-id override in sync with the
|
|
15
|
-
*
|
|
14
|
+
* - keeps the chain-id override in sync with the relay's authoritative
|
|
15
|
+
* `chain_id` (after ops-1 both tiers are on Gnosis 100; the client consumes
|
|
16
|
+
* the relay value verbatim — see `syncChainIdFromBilling`)
|
|
17
|
+
* - keeps the DataEdge-address override in sync with the relay's
|
|
18
|
+
* authoritative `data_edge_address` (staging vs prod contract; consumed
|
|
19
|
+
* verbatim — see `syncDataEdgeAddressFromBilling`, #460)
|
|
16
20
|
* - does NOT import anything that performs outbound I/O
|
|
17
21
|
*
|
|
18
22
|
* Do NOT add any outbound-request call to this file — a single match for
|
|
@@ -23,7 +27,10 @@
|
|
|
23
27
|
|
|
24
28
|
import fs from 'node:fs';
|
|
25
29
|
import path from 'node:path';
|
|
26
|
-
import { CONFIG, setChainIdOverride } from './config.js';
|
|
30
|
+
import { CONFIG, setChainIdOverride, setDataEdgeAddressOverride } from './config.js';
|
|
31
|
+
|
|
32
|
+
/** A plausible EVM address — anything else from billing is ignored (#460). */
|
|
33
|
+
const DATA_EDGE_ADDRESS_RE = /^0x[0-9a-fA-F]{40}$/;
|
|
27
34
|
|
|
28
35
|
// ---------------------------------------------------------------------------
|
|
29
36
|
// Constants
|
|
@@ -50,6 +57,19 @@ export interface BillingCache {
|
|
|
50
57
|
max_facts_per_extraction?: number;
|
|
51
58
|
max_candidate_pool?: number;
|
|
52
59
|
};
|
|
60
|
+
/**
|
|
61
|
+
* Authoritative chain id from the relay `/v1/billing/status` response.
|
|
62
|
+
* After ops-1 (2026-06-05) both tiers are on Gnosis (100); the relay is the
|
|
63
|
+
* source of truth, so the client consumes this verbatim (#402).
|
|
64
|
+
*/
|
|
65
|
+
chain_id?: number;
|
|
66
|
+
/**
|
|
67
|
+
* Authoritative DataEdge contract address from the relay
|
|
68
|
+
* `/v1/billing/status` response. Staging returns the isolated staging
|
|
69
|
+
* DataEdge, production the prod DataEdge; the client consumes this verbatim
|
|
70
|
+
* so writes and reads land on the same contract (#460).
|
|
71
|
+
*/
|
|
72
|
+
data_edge_address?: string;
|
|
53
73
|
checked_at: number;
|
|
54
74
|
}
|
|
55
75
|
|
|
@@ -58,24 +78,49 @@ export interface BillingCache {
|
|
|
58
78
|
// ---------------------------------------------------------------------------
|
|
59
79
|
|
|
60
80
|
/**
|
|
61
|
-
* Apply the
|
|
81
|
+
* Apply the relay's authoritative `chain_id` to the runtime chain override.
|
|
62
82
|
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
83
|
+
* After ops-1 (2026-06-05) both tiers run on Gnosis (chain 100) and the relay
|
|
84
|
+
* returns an authoritative `chain_id` in `/v1/billing/status`. The client MUST
|
|
85
|
+
* consume that verbatim — the old tier→chain derivation (Free ⇒ 84532 Base
|
|
86
|
+
* Sepolia) was retired two-tier logic that mis-signed FREE-tier UserOps
|
|
87
|
+
* against the wrong chain and queried a Base-Sepolia RPC for a Gnosis-deployed
|
|
88
|
+
* sender, producing deterministic AA10 (#402).
|
|
67
89
|
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
90
|
+
* A missing / non-finite `chain_id` (older relay, partial payload) defaults to
|
|
91
|
+
* 100 — never 84532. Called from `readBillingCache` and `writeBillingCache` so
|
|
92
|
+
* every cache read or write keeps the override in sync. Idempotent.
|
|
71
93
|
*/
|
|
72
|
-
export function
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
94
|
+
export function syncChainIdFromBilling(chainId: number | undefined): void {
|
|
95
|
+
setChainIdOverride(typeof chainId === 'number' && Number.isFinite(chainId) ? chainId : 100);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
// DataEdge-address sync
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Apply the relay's authoritative `data_edge_address` to the runtime DataEdge
|
|
104
|
+
* override. Mirrors `syncChainIdFromBilling`.
|
|
105
|
+
*
|
|
106
|
+
* The relay routes each environment to its own DataEdge (staging is on-chain
|
|
107
|
+
* isolated). If the client ignores this and uses the WASM-baked default (the
|
|
108
|
+
* PROD DataEdge), writes against the staging relay mine on the prod contract
|
|
109
|
+
* while reads come from the staging subgraph → empty recall + phantom
|
|
110
|
+
* "stored=N" success (#460).
|
|
111
|
+
*
|
|
112
|
+
* A missing / malformed `data_edge_address` (older relay, partial payload,
|
|
113
|
+
* junk) clears the override (`null`) so resolution falls through to the WASM
|
|
114
|
+
* default — never a stale value. Only a plausible address
|
|
115
|
+
* (`0x` + 40 hex) is honored. Called from `readBillingCache` and
|
|
116
|
+
* `writeBillingCache` so every cache read or write keeps the override in sync.
|
|
117
|
+
* Idempotent. The explicit env override (`TOTALRECLAW_DATA_EDGE_ADDRESS`)
|
|
118
|
+
* still wins — it is the first term in `getSubgraphConfig`.
|
|
119
|
+
*/
|
|
120
|
+
export function syncDataEdgeAddressFromBilling(address: string | undefined): void {
|
|
121
|
+
setDataEdgeAddressOverride(
|
|
122
|
+
typeof address === 'string' && DATA_EDGE_ADDRESS_RE.test(address) ? address : null,
|
|
123
|
+
);
|
|
79
124
|
}
|
|
80
125
|
|
|
81
126
|
// ---------------------------------------------------------------------------
|
|
@@ -95,8 +140,10 @@ export function readBillingCache(): BillingCache | null {
|
|
|
95
140
|
if (!fs.existsSync(BILLING_CACHE_PATH)) return null;
|
|
96
141
|
const raw = JSON.parse(fs.readFileSync(BILLING_CACHE_PATH, 'utf-8')) as BillingCache;
|
|
97
142
|
if (!raw.checked_at || Date.now() - raw.checked_at > BILLING_CACHE_TTL) return null;
|
|
98
|
-
// Keep chain
|
|
99
|
-
|
|
143
|
+
// Keep chain + DataEdge overrides in sync with the persisted authoritative
|
|
144
|
+
// values across process restarts.
|
|
145
|
+
syncChainIdFromBilling(raw.chain_id);
|
|
146
|
+
syncDataEdgeAddressFromBilling(raw.data_edge_address);
|
|
100
147
|
return raw;
|
|
101
148
|
} catch {
|
|
102
149
|
return null;
|
|
@@ -116,7 +163,9 @@ export function writeBillingCache(cache: BillingCache): void {
|
|
|
116
163
|
} catch {
|
|
117
164
|
// Best-effort — don't block on cache write failure.
|
|
118
165
|
}
|
|
119
|
-
// Sync chain
|
|
120
|
-
//
|
|
121
|
-
|
|
166
|
+
// Sync chain + DataEdge overrides AFTER the write so in-process UserOp
|
|
167
|
+
// signing + subgraph reads pick up the correct chain and contract
|
|
168
|
+
// immediately, even if the disk write failed.
|
|
169
|
+
syncChainIdFromBilling(cache.chain_id);
|
|
170
|
+
syncDataEdgeAddressFromBilling(cache.data_edge_address);
|
|
122
171
|
}
|
package/claims-helper.ts
CHANGED
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
VALID_MEMORY_VOLATILITIES,
|
|
31
31
|
VALID_MEMORY_TYPES_V1,
|
|
32
32
|
} from './extractor.js';
|
|
33
|
+
import { envStringLower } from './entry.js';
|
|
33
34
|
|
|
34
35
|
// Lazy-load WASM. We use createRequire so this module loads cleanly under
|
|
35
36
|
// both the OpenClaw runtime (CJS-ish tsx) and bare Node ESM (used by tests).
|
|
@@ -640,7 +641,7 @@ export type AutoResolveMode = 'active' | 'off' | 'shadow';
|
|
|
640
641
|
* @internal Not public config — emergency kill-switch only.
|
|
641
642
|
*/
|
|
642
643
|
export function resolveAutoResolveMode(): AutoResolveMode {
|
|
643
|
-
const raw = (
|
|
644
|
+
const raw = envStringLower('TOTALRECLAW_AUTO_RESOLVE_MODE');
|
|
644
645
|
if (raw === 'off') return 'off';
|
|
645
646
|
if (raw === 'shadow') return 'shadow';
|
|
646
647
|
return 'active';
|
package/config.ts
CHANGED
|
@@ -102,10 +102,10 @@ export function getSessionId(): string | null {
|
|
|
102
102
|
|
|
103
103
|
/**
|
|
104
104
|
* Runtime override for chain ID, set after the relay billing response is
|
|
105
|
-
* read.
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
* the signature with
|
|
105
|
+
* read. After the ops-1 single-chain migration (2026-05), ALL tiers (free
|
|
106
|
+
* + Pro) are on Gnosis mainnet (chain 100). The default below is 100.
|
|
107
|
+
* The relay routes all writes to Gnosis, so UserOps MUST be signed against
|
|
108
|
+
* chain 100 — otherwise the bundler rejects the signature with AA24.
|
|
109
109
|
*
|
|
110
110
|
* See index.ts: after the billing lookup completes, call
|
|
111
111
|
* `setChainIdOverride(100)` for Pro users. Free users can leave the
|
|
@@ -122,6 +122,39 @@ export function __resetChainIdOverrideForTests(): void {
|
|
|
122
122
|
_chainIdOverride = null;
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
+
/**
|
|
126
|
+
* Runtime override for the DataEdge contract address, set after the relay
|
|
127
|
+
* billing response is read. The relay returns an authoritative
|
|
128
|
+
* `data_edge_address` in `/v1/billing/status` (staging routes to the isolated
|
|
129
|
+
* staging DataEdge, production to the prod DataEdge). Chain-aware clients MUST
|
|
130
|
+
* consume it verbatim — otherwise writes mine on the WASM-baked prod DataEdge
|
|
131
|
+
* while reads hit the relay's subgraph, so recall silently returns empty
|
|
132
|
+
* against staging (#460). Mirrors `_chainIdOverride`.
|
|
133
|
+
*
|
|
134
|
+
* `null` means "no billing override" → fall through to the WASM default. The
|
|
135
|
+
* explicit operator env override (`TOTALRECLAW_DATA_EDGE_ADDRESS`, exposed as
|
|
136
|
+
* `CONFIG.dataEdgeAddress`) always wins over this.
|
|
137
|
+
*/
|
|
138
|
+
let _dataEdgeAddressOverride: string | null = null;
|
|
139
|
+
|
|
140
|
+
export function setDataEdgeAddressOverride(address: string | null): void {
|
|
141
|
+
_dataEdgeAddressOverride = address;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Read the billing-sourced DataEdge override (or `''` when unset). Used by
|
|
146
|
+
* `getSubgraphConfig` as the middle term of the env → billing → WASM-default
|
|
147
|
+
* resolution order (#460).
|
|
148
|
+
*/
|
|
149
|
+
export function getDataEdgeAddressOverride(): string {
|
|
150
|
+
return _dataEdgeAddressOverride ?? '';
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Reset the DataEdge override — used by tests. */
|
|
154
|
+
export function __resetDataEdgeAddressOverrideForTests(): void {
|
|
155
|
+
_dataEdgeAddressOverride = null;
|
|
156
|
+
}
|
|
157
|
+
|
|
125
158
|
export const CONFIG = {
|
|
126
159
|
// Core — recoveryPhrase reads from override first, then env var.
|
|
127
160
|
// Use getRecoveryPhrase() for dynamic access; this property is for
|
|
@@ -148,6 +181,42 @@ export const CONFIG = {
|
|
|
148
181
|
serverUrl: (process.env.TOTALRECLAW_SERVER_URL || 'https://api.totalreclaw.xyz').replace(/\/+$/, ''),
|
|
149
182
|
selfHosted: process.env.TOTALRECLAW_SELF_HOSTED === 'true',
|
|
150
183
|
credentialsPath: process.env.TOTALRECLAW_CREDENTIALS_PATH || path.join(home, '.totalreclaw', 'credentials.json'),
|
|
184
|
+
// cred-3 stage 1 — credential-provider abstraction.
|
|
185
|
+
//
|
|
186
|
+
// `file` (default) preserves the legacy behavior: read/write
|
|
187
|
+
// `credentialsPath` directly via fs-helpers.
|
|
188
|
+
//
|
|
189
|
+
// `external` instructs the plugin to load credentials from an
|
|
190
|
+
// out-of-process source at boot. Two transports are supported (mutually
|
|
191
|
+
// exclusive — JSON wins if both set):
|
|
192
|
+
// 1. `externalCredentialsJson` — raw JSON string of CredentialsFile,
|
|
193
|
+
// injected at process start by the secret manager (Railway secrets,
|
|
194
|
+
// Docker `--env-file`, K8s envFrom). Most ergonomic for cloud
|
|
195
|
+
// deployments where the secret manager already exposes secrets as
|
|
196
|
+
// env vars.
|
|
197
|
+
// 2. `externalCredentialsPath` — filesystem path to a JSON file the
|
|
198
|
+
// secret manager mounts (Docker Compose `secrets:` block,
|
|
199
|
+
// K8s secret volumeMount, tmpfs populated by an ops wrapper that
|
|
200
|
+
// fetches from AWS Secrets Manager / Hetzner Vault before boot).
|
|
201
|
+
//
|
|
202
|
+
// `external` mode is read-only: write/clear are no-ops with a warning
|
|
203
|
+
// (the secret manager owns the source of truth — the plugin must not
|
|
204
|
+
// write back through this path).
|
|
205
|
+
//
|
|
206
|
+
// See `docs/guides/production-deployment.md` for the LUKS / dm-crypt
|
|
207
|
+
// pattern and managed-secret-store wiring examples.
|
|
208
|
+
credentialsProvider: (() => {
|
|
209
|
+
const v = (process.env.TOTALRECLAW_CREDENTIALS_PROVIDER ?? '').trim().toLowerCase();
|
|
210
|
+
return v === 'external' ? 'external' : 'file';
|
|
211
|
+
})() as 'file' | 'external',
|
|
212
|
+
externalCredentialsJson: (() => {
|
|
213
|
+
const v = (process.env.TOTALRECLAW_EXTERNAL_CREDENTIALS_JSON ?? '').trim();
|
|
214
|
+
return v.length > 0 ? v : null;
|
|
215
|
+
})() as string | null,
|
|
216
|
+
externalCredentialsPath: (() => {
|
|
217
|
+
const v = (process.env.TOTALRECLAW_EXTERNAL_CREDENTIALS_PATH ?? '').trim();
|
|
218
|
+
return v.length > 0 ? v : null;
|
|
219
|
+
})() as string | null,
|
|
151
220
|
// 3.2.0 onboarding state file — separate from credentials.json so it
|
|
152
221
|
// never contains secrets. Loaded on every plugin init + on every
|
|
153
222
|
// before_tool_call gate check.
|
|
@@ -185,21 +254,28 @@ export const CONFIG = {
|
|
|
185
254
|
: 'wss://api.totalreclaw.xyz')
|
|
186
255
|
).replace(/\/+$/, ''),
|
|
187
256
|
|
|
188
|
-
// Chain — chainId is no longer user-configurable.
|
|
189
|
-
//
|
|
190
|
-
// 100). The default here is used only before the first billing lookup
|
|
257
|
+
// Chain — chainId is no longer user-configurable. After the ops-1 single-
|
|
258
|
+
// chain migration, ALL tiers are on Gnosis (100). The default here is 100.
|
|
191
259
|
// completes. Self-hosted users can still point at a custom DataEdge via
|
|
192
260
|
// TOTALRECLAW_DATA_EDGE_ADDRESS / TOTALRECLAW_ENTRYPOINT_ADDRESS /
|
|
193
261
|
// TOTALRECLAW_RPC_URL (undocumented; internal knobs).
|
|
194
262
|
//
|
|
195
|
-
// Reads the runtime override set
|
|
196
|
-
// Falls back to
|
|
197
|
-
//
|
|
198
|
-
//
|
|
263
|
+
// Reads the runtime override set from the relay's authoritative chain_id
|
|
264
|
+
// (syncChainIdFromBilling). Falls back to 100 (Gnosis) pre-billing-lookup —
|
|
265
|
+
// after ops-1 both tiers are on Gnosis, so 84532 is never the default (#402).
|
|
266
|
+
// Must be a getter, not a literal — a literal would freeze UserOps to the
|
|
267
|
+
// wrong chainId and fail signature validation at the bundler.
|
|
199
268
|
get chainId(): number {
|
|
200
|
-
return _chainIdOverride ??
|
|
269
|
+
return _chainIdOverride ?? 100;
|
|
270
|
+
},
|
|
271
|
+
// Explicit operator env override for the DataEdge contract. Stays env-only:
|
|
272
|
+
// it is the FIRST term of `getSubgraphConfig`'s env → billing → WASM-default
|
|
273
|
+
// resolution order, so it wins over the relay's authoritative
|
|
274
|
+
// `data_edge_address` (see `getDataEdgeAddressOverride` / #460). Getter, not
|
|
275
|
+
// a literal, so it reflects the live env rather than freezing at module load.
|
|
276
|
+
get dataEdgeAddress(): string {
|
|
277
|
+
return process.env.TOTALRECLAW_DATA_EDGE_ADDRESS || '';
|
|
201
278
|
},
|
|
202
|
-
dataEdgeAddress: process.env.TOTALRECLAW_DATA_EDGE_ADDRESS || '',
|
|
203
279
|
entryPointAddress: process.env.TOTALRECLAW_ENTRYPOINT_ADDRESS || '',
|
|
204
280
|
rpcUrl: process.env.TOTALRECLAW_RPC_URL || '',
|
|
205
281
|
|
|
@@ -224,6 +300,12 @@ export const CONFIG = {
|
|
|
224
300
|
// See: docs/specs/totalreclaw/client-consistency.md
|
|
225
301
|
cosineThreshold: parseFloat(process.env.TOTALRECLAW_COSINE_THRESHOLD ?? '0.15'),
|
|
226
302
|
extractInterval: parseInt(process.env.TOTALRECLAW_EXTRACT_INTERVAL ?? process.env.TOTALRECLAW_EXTRACT_EVERY_TURNS ?? '3', 10),
|
|
303
|
+
// Self-hosted fallback for max-facts-per-extraction. `undefined` when the
|
|
304
|
+
// env var is unset so getMaxFactsPerExtraction() can fall through to the
|
|
305
|
+
// billing cache then the built-in MAX_FACTS_PER_EXTRACTION constant.
|
|
306
|
+
maxFactsPerExtraction: process.env.TOTALRECLAW_MAX_FACTS_PER_EXTRACTION
|
|
307
|
+
? parseInt(process.env.TOTALRECLAW_MAX_FACTS_PER_EXTRACTION, 10)
|
|
308
|
+
: undefined,
|
|
227
309
|
relevanceThreshold: parseFloat(process.env.TOTALRECLAW_RELEVANCE_THRESHOLD ?? '0.3'),
|
|
228
310
|
semanticSkipThreshold: parseFloat(process.env.TOTALRECLAW_SEMANTIC_SKIP_THRESHOLD ?? '0.85'),
|
|
229
311
|
cacheTtlMs: parseInt(process.env.TOTALRECLAW_CACHE_TTL_MS ?? String(5 * 60 * 1000), 10),
|
package/consolidation.ts
CHANGED
|
@@ -18,11 +18,14 @@
|
|
|
18
18
|
* and `clusterFacts` WASM functions when available, falling back to local
|
|
19
19
|
* implementations that use WASM-backed `cosineSimilarity`.
|
|
20
20
|
*
|
|
21
|
-
* Threshold helpers remain local
|
|
21
|
+
* Threshold helpers remain local; their env reads are centralized in
|
|
22
|
+
* entry.ts (env-reading seam, Task 1.3 of the OpenClaw native
|
|
23
|
+
* integration plan, 2026-06-21).
|
|
22
24
|
*/
|
|
23
25
|
|
|
24
26
|
import { createRequire } from 'node:module';
|
|
25
27
|
import { cosineSimilarity } from './reranker.js';
|
|
28
|
+
import { envNumber } from './entry.js';
|
|
26
29
|
|
|
27
30
|
// ---------------------------------------------------------------------------
|
|
28
31
|
// Lazy-load WASM core (mirrors claims-helper.ts / contradiction-sync.ts
|
|
@@ -47,12 +50,7 @@ function getWasm(): typeof import('@totalreclaw/core') {
|
|
|
47
50
|
* Must be a number in [0, 1]. Falls back to 0.85 if invalid or unset.
|
|
48
51
|
*/
|
|
49
52
|
export function getStoreDedupThreshold(): number {
|
|
50
|
-
|
|
51
|
-
if (envVal !== undefined) {
|
|
52
|
-
const parsed = parseFloat(envVal);
|
|
53
|
-
if (!isNaN(parsed) && parsed >= 0 && parsed <= 1) return parsed;
|
|
54
|
-
}
|
|
55
|
-
return 0.85;
|
|
53
|
+
return envNumber('TOTALRECLAW_STORE_DEDUP_THRESHOLD', 0.85, { min: 0, max: 1 });
|
|
56
54
|
}
|
|
57
55
|
|
|
58
56
|
/**
|
|
@@ -62,12 +60,7 @@ export function getStoreDedupThreshold(): number {
|
|
|
62
60
|
* Must be a number in [0, 1]. Falls back to 0.88 if invalid or unset.
|
|
63
61
|
*/
|
|
64
62
|
export function getConsolidationThreshold(): number {
|
|
65
|
-
|
|
66
|
-
if (envVal !== undefined) {
|
|
67
|
-
const parsed = parseFloat(envVal);
|
|
68
|
-
if (!isNaN(parsed) && parsed >= 0 && parsed <= 1) return parsed;
|
|
69
|
-
}
|
|
70
|
-
return 0.88;
|
|
63
|
+
return envNumber('TOTALRECLAW_CONSOLIDATION_THRESHOLD', 0.88, { min: 0, max: 1 });
|
|
71
64
|
}
|
|
72
65
|
|
|
73
66
|
/** Maximum candidates to compare against during store-time dedup. */
|