@totalreclaw/totalreclaw 3.3.12-rc.2 → 3.3.12-rc.20

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.
Files changed (87) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/SKILL.md +34 -249
  3. package/api-client.ts +17 -9
  4. package/batch-gate.ts +42 -0
  5. package/billing-cache.ts +25 -20
  6. package/claims-helper.ts +7 -1
  7. package/config.ts +54 -12
  8. package/consolidation.ts +6 -13
  9. package/contradiction-sync.ts +19 -14
  10. package/credential-provider.ts +184 -0
  11. package/crypto.ts +27 -160
  12. package/dist/api-client.js +17 -9
  13. package/dist/batch-gate.js +40 -0
  14. package/dist/billing-cache.js +19 -21
  15. package/dist/claims-helper.js +7 -1
  16. package/dist/config.js +54 -12
  17. package/dist/consolidation.js +6 -15
  18. package/dist/contradiction-sync.js +15 -15
  19. package/dist/credential-provider.js +145 -0
  20. package/dist/crypto.js +17 -137
  21. package/dist/download-ux.js +11 -7
  22. package/dist/embedder-loader.js +266 -0
  23. package/dist/embedding.js +36 -3
  24. package/dist/entry.js +123 -0
  25. package/dist/extractor.js +134 -0
  26. package/dist/fs-helpers.js +116 -241
  27. package/dist/import-adapters/chatgpt-adapter.js +14 -0
  28. package/dist/import-adapters/claude-adapter.js +14 -0
  29. package/dist/import-adapters/gemini-adapter.js +43 -159
  30. package/dist/import-adapters/mcp-memory-adapter.js +14 -0
  31. package/dist/import-state-manager.js +100 -0
  32. package/dist/index.js +1113 -2520
  33. package/dist/llm-client.js +69 -1
  34. package/dist/memory-runtime.js +459 -0
  35. package/dist/native-memory.js +123 -0
  36. package/dist/onboarding-cli.js +3 -2
  37. package/dist/pair-cli.js +1 -1
  38. package/dist/pair-crypto.js +16 -358
  39. package/dist/pair-http.js +147 -4
  40. package/dist/relay.js +140 -0
  41. package/dist/reranker.js +13 -8
  42. package/dist/semantic-dedup.js +5 -7
  43. package/dist/skill-register.js +97 -0
  44. package/dist/subgraph-search.js +3 -1
  45. package/dist/subgraph-store.js +315 -282
  46. package/dist/tool-gating.js +39 -26
  47. package/dist/tools.js +367 -0
  48. package/dist/tr-cli-export-helper.js +103 -0
  49. package/dist/tr-cli.js +220 -127
  50. package/dist/trajectory-poller.js +155 -9
  51. package/dist/vault-crypto.js +551 -0
  52. package/download-ux.ts +12 -6
  53. package/embedder-loader.ts +293 -1
  54. package/embedding.ts +43 -3
  55. package/entry.ts +132 -0
  56. package/extractor.ts +167 -0
  57. package/fs-helpers.ts +166 -292
  58. package/import-adapters/chatgpt-adapter.ts +18 -0
  59. package/import-adapters/claude-adapter.ts +18 -0
  60. package/import-adapters/gemini-adapter.ts +56 -183
  61. package/import-adapters/mcp-memory-adapter.ts +18 -0
  62. package/import-adapters/types.ts +1 -1
  63. package/import-state-manager.ts +139 -0
  64. package/index.ts +1412 -3002
  65. package/llm-client.ts +74 -1
  66. package/memory-runtime.ts +723 -0
  67. package/native-memory.ts +196 -0
  68. package/onboarding-cli.ts +3 -2
  69. package/openclaw.plugin.json +5 -17
  70. package/package.json +7 -4
  71. package/pair-cli.ts +1 -1
  72. package/pair-crypto.ts +41 -483
  73. package/pair-http.ts +194 -5
  74. package/postinstall.mjs +138 -0
  75. package/relay.ts +172 -0
  76. package/reranker.ts +13 -8
  77. package/semantic-dedup.ts +5 -6
  78. package/skill-register.ts +146 -0
  79. package/skill.json +1 -1
  80. package/subgraph-search.ts +3 -1
  81. package/subgraph-store.ts +334 -299
  82. package/tool-gating.ts +39 -26
  83. package/tools.ts +499 -0
  84. package/tr-cli-export-helper.ts +138 -0
  85. package/tr-cli.ts +263 -133
  86. package/trajectory-poller.ts +162 -10
  87. package/vault-crypto.ts +705 -0
package/CHANGELOG.md CHANGED
@@ -4,6 +4,32 @@ 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
+ ## [3.3.12-rc.5] — 2026-05-09
8
+
9
+ Final RC for the 3.3.12 stable promote. Behavioral fix: Pedro's Pop-OS Telegram QA (zai/glm-5-turbo) on rc.4 found the agent storing user statements in `MEMORY.md` / `USER.md` via `write` tool calls instead of calling `totalreclaw_remember`. 28 `write` calls observed in one session, 0 TotalReclaw tool calls — facts never reached the chain. Root cause: SKILL.md trigger language was permissive ("call when the user asks") and the agent's default file-write reflex won out. rc.5 makes the memory-storage rule the TOP RULE of SKILL.md with an explicit prohibition on `write`/`edit` against `MEMORY.md`/`USER.md`, an exhaustive trigger-phrase list (preference / identity / decision / commitment / possessive-assertion patterns), and a multi-fact-per-message instruction so blob-style packing does not happen.
10
+
11
+ ### Changed
12
+
13
+ - **SKILL.md TOP RULE rewrite — aggressive triggers, no MEMORY.md fallback.** Memory storage section moved to the very top of SKILL.md (before architecture, vocabulary, install flow). Adds: (1) absolute prohibition on `write`/`edit` against `MEMORY.md` / `USER.md` / `~/.claude/memory/*.md`; (2) trigger-phrase list covering preferences, identity, tools, decisions, commitments, explicit asks, possessive assertions; (3) multi-fact-per-message rule (split into one `totalreclaw_remember` call per atomic fact); (4) full 17-tool reference table with canonical use cases; (5) restart policy reaffirmed (gateway self-restarts via SIGUSR1; agent NEVER prompts user to manually restart); (6) phrase-safety hard rail (no phrase in chat / no phrase as tool input / browser-side only).
14
+ - **Public quickstart guide audit** (`docs/guides/openclaw-setup-quickstart.md`) — already prose-style + clean from rc.6 revert. No edits required.
15
+ - **Long-form setup guide cleanup** (`docs/guides/openclaw-setup.md`) — F-flip default URL corrected (`api.totalreclaw.xyz` for free tier, staging via env override; previous text said the relay was `api-staging.*`); line 1 wording aligned with SKILL.md (`Setting up TotalReclaw — this takes about a minute…`); legacy `qr_png_b64` / `qr_unicode` references replaced with `qr_ascii` (the only QR field in the current pair payload); manual restart fallback (`openclaw gateway restart` / `docker restart`) deemphasized — the plugin self-restarts via SIGUSR1; RC pin examples bumped from `3.3.11-rc.5` to `3.3.12-rc.5`.
16
+ - **Version sync** — package.json, skill.json, SKILL.md frontmatter, tr-cli.ts PLUGIN_VERSION all aligned to 3.3.12-rc.5 via `sync-version.mjs`. `check-version-drift` green.
17
+
18
+ ### Verified
19
+
20
+ - All 38 test suites pass (manifest-shape, config-schema, config, relay-headers, scope-address-visible, llm-profile-reader, llm-client (×2), gateway-url, retype-setscope, tool-gating, onboarding-noninteractive, pair-cli-json, pair-qr, pair-remote-client, qa-bug-report, nonce-serialization, phrase-safety-registry, onnx-download-ux, onboard-pair-only, import-time-smoke, install-staging-cleanup, partial-install-detection, install-reload-idempotency, json-stdout-cleanliness, load-manifest, url-binding, fs-helpers, pair-cli-default-mode, embedding-fallback-tag, staging-banner-gate, restart-auth, inbound-user-tracker, register-command-name, skill-md-hybrid-primary, tr-cli-json-output).
21
+ - `check-scanner` green (0 flags).
22
+ - `check-version-drift` green (3 sites + tr-cli.ts all = 3.3.12-rc.5).
23
+ - E2E on clean OpenClaw 2026.5.7 container — agent given `"Hi, I'm Pedro. I live in Porto. I prefer PostgreSQL over MySQL."` calls `totalreclaw_remember` ≥1 time, with 0 `write`/`edit` calls against `MEMORY.md` / `USER.md`. (See PR description for the captured `toolSummary` JSON.)
24
+
25
+ ### Out of scope (unchanged from rc.4)
26
+
27
+ - Plugin install lifecycle on OpenClaw 2026.5.7 (auto-QA PASS — plugin loads, SIGUSR1 self-restart works, in-process restart, managed-service-friendly).
28
+ - `tr` CLI on-chain memory ops (rc.4 fix verified).
29
+ - Pair flow (browser-side phrase, no leakage).
30
+ - Trajectory poller (auto-extraction backup).
31
+ - F flip (RC + stable both default to prod URL; staging via env override).
32
+
7
33
  ## [3.3.12-rc.2] — 2026-05-08
8
34
 
9
35
  Hot-fix on rc.1's F flip. Pair flow regression: rc.1 set `pairRelayUrl`'s default to `wss://api.totalreclaw.xyz` (production) independently of `serverUrl`. RC users who set `TOTALRECLAW_SERVER_URL=https://api-staging.totalreclaw.xyz` (per the staging-opt-in flow) had pair WS go to **prod**, which pre-dates the pair feature → 404 on WS upgrade → `totalreclaw_pair failed: Unexpected server response: 404`. End-to-end blocker: pair never completed → no credentials → no memories.
package/SKILL.md CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: totalreclaw
3
- description: "Install + set up TotalReclaw encrypted memory for OpenClaw, then use tr CLI for remember / recall. Trigger on 'install TotalReclaw', 'set up TotalReclaw', 'restore my recovery phrase', or any remember / recall request."
4
- version: 3.3.12-rc.2
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.20
5
5
  author: TotalReclaw Team
6
6
  license: MIT
7
7
  homepage: https://totalreclaw.xyz
@@ -22,276 +22,61 @@ metadata:
22
22
  - persistent-context
23
23
  ---
24
24
 
25
- # TotalReclaw — OpenClaw
25
+ # TotalReclaw — OpenClaw native memory provider
26
26
 
27
- > **Agent-executable quickstart (5 KB, copy-paste-runnable)**: <https://github.com/p-diogo/totalreclaw/blob/main/docs/guides/openclaw-setup-quickstart.md>
28
- >
29
- > **Human-readable setup guide (full rationale, troubleshooting)**: <https://github.com/p-diogo/totalreclaw/blob/main/docs/guides/openclaw-setup.md>
30
- >
31
- > If the user pastes either URL: prefer the quickstart for execution. Fetch the full guide only if the user asks for context or troubleshooting beyond what the quickstart covers. This SKILL.md mirrors the quickstart's hard rules so you can act even if the URL is unreachable.
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.
32
28
 
33
- ## CRITICAL: How TotalReclaw actually stores memories (read before anything else)
29
+ ## How memory works here (read first)
34
30
 
35
- **TotalReclaw is end-to-end encrypted memory stored across a DECENTRALIZED NETWORK — not on a single company server.** Memories are encrypted with keys derived from the user's 12-word recovery phrase, submitted as Account Abstraction UserOps, and persisted on a public blockchain (Base / Gnosis depending on tier). The Graph subgraph indexes them. The TotalReclaw relay only forwards encrypted bundles to the bundler and proxies subgraph reads it never sees plaintext, can't read memories, and could be replaced by any compatible relay without losing data. No single company controls or can read user memories.
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.
36
34
 
37
- Forbidden vocabulary these claims are WRONG (some imply local-only storage; some imply a single-company server):
38
- - "local", "local-only", "stored locally", "on disk", "local memory", "local storage" — wrong direction (implies local-only). Memories require an account; they're encrypted and submitted on-chain.
39
- - "stored on a relay server", "stored on TotalReclaw's server", "single server owned by", "TotalReclaw stores your", "company server" — wrong direction (implies centralized custody). The relay forwards ciphertext; storage is decentralized on-chain.
40
- - "without an account" — wrong (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.
41
36
 
42
- If you need to describe how it works, the canonical line is: **"Memories are encrypted with a key derived from your recovery phrase, submitted on-chain via Account Abstraction, and stored across a decentralized network. The relay only forwards encrypted bundles — it never sees plaintext. Only you can decrypt them, and only with your recovery phrase."**
37
+ ## Phrase safety (HARD)
43
38
 
44
- ## User-facing vocabulary (substitute internal jargon for plain words)
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.
45
40
 
46
- Internal terms that the user does NOT knowtranslate when you must speak:
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."
47
42
 
48
- | Internal term | Say to the user |
49
- |---|---|
50
- | pair / pairing / paired account | "set up an account" / "your TotalReclaw account" |
51
- | relay / relay server | (don't mention; if forced, say "the encrypted-bundle forwarder") |
52
- | WS / WebSocket / handshake | (don't mention) |
53
- | trapdoor / blind index / dedup-key | (don't mention) |
54
- | subgraph / bundler / UserOp / Account Abstraction | (don't mention; if forced, "submitted on-chain to a decentralized network") |
55
- | credentials.json / mnemonic file | "your recovery phrase" |
56
- | `tr pair --json` / `setsid -f` / shell exec | (don't mention; just do the work silently) |
43
+ ## Install
57
44
 
58
- The word "pair" is internal jargon — never use it in user-visible prose. The user-visible vocabulary is "set up an account", "your account", "your recovery phrase", "your memories".
59
-
60
- ## Top-level rule (read this before anything else)
61
-
62
- **Emit ONLY the user-visible lines listed below. Do not narrate tool calls. Do not describe what you're doing or about to do. Errors are the only allowed deviation, and only as a single tight line.** The expected user-facing transcript for a healthy install is **4–5 short lines TOTAL**. Anything more is a SKILL violation. Telegram and similar transports edit the bot's messages live as your tool calls progress — every interim "Let me check…" / "Plugin loaded. Let me verify…" line shows up to the user as awkward edit churn, even if you "delete" it. The fix is to never emit it in the first place.
63
-
64
- ### ⚡ MANDATORY ACK BEFORE FIRST TOOL CALL
65
-
66
- **User-visible line 1 MUST be your FIRST output — emitted BEFORE you run any shell tool, plugin install, or `tr` command.** Install + setup takes 1–2 minutes of silent tool-execution; without an immediate ack the user assumes the request didn't register and may resend or abandon. The ack tells them work is happening.
67
-
68
- The ack is unconditional. It fires the moment you decide to install / set up TotalReclaw — even before you've resolved `TR_CLI`, even before you've checked whether the plugin is already installed, even before the first `openclaw plugins install` shell call. Send line 1, THEN start the work.
69
-
70
- Forbidden order (silent-then-narrate): ❌ run `openclaw plugins install` → wait 60s → emit line 1.
71
- Required order (ack-then-work): ✅ emit line 1 → run `openclaw plugins install` silently → emit line 2 when `tr status` returns.
72
-
73
- ## ⛔ FORBIDDEN ACTIONS (4 hard rules from 2026-05-05 QA)
74
-
75
- These specific actions break the pair flow. Everything else is governed by the "emit only the numbered user-visible lines" rule above.
76
-
77
- 1. **Do NOT restart the gateway.** No `/totalreclaw-restart`, no `kill -USR1`, no `docker restart`, no `openclaw gateway restart`. The plugin self-patches and signals its own restart needs. You restarting it kills the in-flight `setsid -f` pair subprocess → 502 on browser respond.
78
-
79
- 2. **Do NOT write to `~/.openclaw/openclaw.json`.** The plugin's `patchOpenClawConfig()` already wrote `plugins.slots.memory`, `plugins.entries.totalreclaw.hooks.allowConversationAccess`, `channels.telegram.streaming.mode`. Any further write triggers a deferred SIGUSR1 reload that kills pair. No `openclaw config set …`, no `jq …>openclaw.json`, no `sed -i openclaw.json`.
80
-
81
- 3. **Do NOT save QR PNGs.** `tr pair --json` returns `qr_ascii` — emit that inline in a triple-backticked code block. No `/tmp/totalreclaw-pair-qr.png` (OpenClaw media-policy blocks it: `LocalMediaAccessError`). No PNG decode. No `qr_png_b64` (legacy field, no longer exists in payload).
82
-
83
- 4. **`tr pair --json` MUST run via `setsid -f`** (exact form below). Foreground invocation is killed when your shell-tool exec returns → 502.
84
-
85
- ## Hybrid-primary architecture (3.3.9-rc.1)
86
-
87
- OpenClaw 2026.5.2 has a tool-policy-pipeline bug (issue #223) that strips non-bundled plugin tools before they reach the agent toolset. **3.3.9-rc.1 makes the `tr` CLI the PRIMARY path for all memory operations.** Native tools (`totalreclaw_*`) are kept for back-compat with older OpenClaw versions where they work, but the agent MUST NOT depend on them. Use `tr` from your shell tool for everything.
88
-
89
- **The `tr` binary is installed at one of two paths depending on OpenClaw version:**
90
- - Modern (2026.5.x npm-managed): `~/.openclaw/npm/node_modules/@totalreclaw/totalreclaw/dist/tr-cli.js`
91
- - Legacy (extensions tree): `~/.openclaw/extensions/totalreclaw/dist/tr-cli.js`
92
-
93
- **Resolve the path once at the top of your shell session and reuse:**
94
- ```bash
95
- 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)"
96
45
  ```
97
- Then invoke as `node "$TR_CLI" <cmd> [--json]`.
98
-
99
- **Never rely on `totalreclaw_*` tool binding** — it may not be available due to issue #223.
100
-
101
- ## User-visible line set (THIS IS ALL YOU SAY)
102
-
103
- Speak only these lines, in order. Substitute the bracketed placeholders with the real values. No prefaces. No suffixes. No narration.
104
-
105
- 1. (FIRST output, emitted BEFORE any shell tool call — see "MANDATORY ACK" rule above)
106
- > Setting up TotalReclaw — this takes about a minute…
107
- 2. (after install + CLI verify — `tr status` works)
108
- > ✓ Installed (totalreclaw &lt;version&gt;, hybrid mode)
109
- 3. (account-setup URL + PIN, IMMEDIATELY after `tr pair --json` returns — no consent gate)
110
- > Open &lt;url&gt; in your browser. Enter PIN &lt;pin&gt;. Generate or paste a 12-word recovery phrase. Reply `done` once it's sealed.
111
- 4. (done + free-tier confirmation)
112
- > ✓ You're set up on the TotalReclaw free tier — 500 memories/month, unlimited reads. https://totalreclaw.xyz/pricing. Ask me to remember anything.
113
-
114
- If existing credentials are detected (step "Check for existing credentials" below), emit ONLY this line and stop:
115
- > ✓ TotalReclaw is already set up.
116
-
117
- ### Hard silence rules between numbered lines
118
-
119
- Do NOT emit transitional / narrative lines between any of the four user-visible lines above. Specifically forbidden (verbatim examples from Pedro's 2026-05-06 QA):
120
-
121
- - ❌ "Now let me verify the install:" → silent. Just run `tr status --json`. Emit line 2 when it returns.
122
- - ❌ "Now let me check if you already have credentials:" → silent. Just run `test -s ~/.totalreclaw/credentials.json`.
123
- - ❌ "Need to pair. Let me kick that off:" → silent. Just run the `setsid -f` block. Emit line 3 when URL+PIN returns. ALSO never use the word "pair" in user-facing text — see vocabulary table above.
124
- - ❌ "Standing by." → silent. Acknowledgements between lines are noise.
125
- - ❌ Any "Let me X" / "Now I'll Y" / "Just give me a second" prelude.
126
- - ❌ **Skipping line 1 entirely and going straight to shell tool calls.** Line 1 ack must precede the first tool call. Silent install = user thinks the prompt didn't register.
127
-
128
- The transcript Pedro should see is exactly four messages — the four numbered lines, nothing in between. Line 1 fires immediately. Lines 2–4 fire as work completes. No prose between.
129
-
130
- ## How does TotalReclaw work? (canonical answer for user questions)
131
-
132
- When the user asks "how does this work?" / "where are my memories stored?" / "is this safe?" / similar — answer with this content. Do NOT say "stored on a relay server", "stored on TotalReclaw's server", or any phrasing that implies single-company custody. The actual architecture:
133
-
134
- > **End-to-end encrypted, decentralized memory.** When you tell me to remember something, I encrypt it locally with a key derived from your 12-word recovery phrase, then submit it on-chain via Account Abstraction (a UserOperation). It lives on a public blockchain (Base Sepolia for free tier, Gnosis mainnet for paid). The Graph subgraph indexes it for fast retrieval. The TotalReclaw relay server only forwards encrypted bundles between your client and the chain — it never sees plaintext, can't read your memories, and could be replaced by any compatible relay without losing data. Your recovery phrase is the only thing that can decrypt the memories. Nobody (not me, not TotalReclaw the company, not the relay operator) can read them without it.
135
-
136
- If the user asks shorter questions, reply with the relevant slice:
137
-
138
- - **"Where are memories stored?"** → "Encrypted, on-chain (Base or Gnosis depending on your tier), indexed by The Graph. Not on any single company's server."
139
- - **"Is the relay reading my data?"** → "No. The relay only forwards encrypted bundles. Plaintext never leaves your device. Only your recovery phrase can decrypt them."
140
- - **"What if TotalReclaw shuts down?"** → "Your memories survive. They're on a public blockchain. Any compatible relay (or self-hosted relay) can read them with your recovery phrase."
141
-
142
- ## Setup flow (hybrid-primary)
143
-
144
- 1. **Install plugin first, skill second (silent — emit user-visible line 1 once at the start, then no prose until line 2).** Run via your shell tool, in this exact order:
145
- ```
146
- openclaw plugins install @totalreclaw/totalreclaw
147
- openclaw skills install totalreclaw
148
- ```
149
- For RC: replace the first command with `openclaw plugins install @totalreclaw/totalreclaw@rc`. **Plugin BEFORE skill is deliberate** (3.3.4-rc.1 — issue from Pedro's QA on rc.3): the skill install triggers a config-driven SIGUSR1 gateway reload that can race a concurrent plugin install.
150
-
151
- If either shell call fails with `command not found` / ENOENT / "not authorized" (managed-service or shell-restricted environment), tell the user verbatim *"Your environment doesn't expose the OpenClaw CLI to me. Please install `totalreclaw` from your service's plugins / skills UI, then reply `done`."* Wait for `done` and skip to step 3.
152
-
153
- 2. **Verify CLI works — emit user-visible line 2 when successful.** Resolve `TR_CLI` and run:
154
- ```bash
155
- 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)"
156
- node "$TR_CLI" status --json
157
- ```
158
- Parse the JSON. When you get `{"version": "...", "hybrid_mode": true, ...}`, emit user-visible line 2 with the version substituted. **Do NOT wait for tool binding. Do NOT issue `/totalreclaw-restart`. Do NOT check whether `totalreclaw_pair` is in your toolset.** The CLI works independently of tool binding. If `tr status` exits non-zero or the file doesn't exist, wait 5s and retry once — it may still be installing.
46
+ openclaw plugins install @totalreclaw/totalreclaw
47
+ openclaw skills install totalreclaw
48
+ ```
159
49
 
160
- 3. **Check for existing credentials (silent unless found).** Run `test -s ~/.totalreclaw/credentials.json`. If it exists and is non-empty, emit *"✓ TotalReclaw is already set up."* and stop.
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.
161
51
 
162
- 4. **Set up the user's TotalReclaw account IMMEDIATELY — no consent gate.** As soon as `tr status --json` returns and credentials are not present, run pair in DETACHED mode. CRITICAL: a foreground `node tr-cli pair` is killed when the gateway fires its post-install SIGUSR1 reload (3.3.10 finding from Pedro's 2026-05-05 QA relay logs `gateway_disconnected` 502 on `/respond`). Use `setsid -f` so the pair process is in its own session and survives any subsequent gateway SIGUSR1 reload. The handshake URL+PIN is written to a tmp file the moment the WS opens (~100-500ms typical):
163
- ```bash
164
- PAIR_OUT="/tmp/tr-pair-out-$(date +%s).json"
165
- setsid -f node "$TR_CLI" pair --json > "$PAIR_OUT" 2>&1 < /dev/null
166
- sleep 2
167
- cat "$PAIR_OUT"
168
- ```
169
- This is UNCONDITIONAL. Do NOT ask "Would you like to set up an account?" or "Want me to pair now?". The user installed TotalReclaw — setup is the expected next step. The URL + PIN itself is the consent moment (the user can simply not open the page to opt out). Parse the JSON response `{"url": "...", "pin": "...", "expires_at_ms": ...}` and emit user-visible line 3. If the cat output is empty after 2s, sleep 2 more seconds and try again (relay handshake can take up to 4s on a cold network). If `tr pair --json` is not available (old install), fall back to `totalreclaw_pair` tool or `openclaw totalreclaw pair generate --url-pin-only`.
52
+ ## Setup (pair — user-initiated QR)
170
53
 
171
- 5. **Verify and confirm (silent until line 4).** After the user says `done`, re-run `test -s ~/.totalreclaw/credentials.json`. If missing, the PIN expired repeat the `setsid -f node "$TR_CLI" pair --json` block and resend line 3 once. If present, emit user-visible line 4 and stop.
54
+ Pairing is a deliberate, user-initiated QR flow (the plugin does **not** auto-pair on load). When credentials don't exist:
172
55
 
173
- ## `tr` CLI reference (hybrid-primary commands)
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.`)
174
60
 
175
- Always use `--json` flag when running from shell for machine-parseable output. Plain text mode is for direct user invocation only.
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.
176
62
 
177
63
  ```bash
178
- # (Resolve TR_CLI once at the top of your session)
179
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)"
180
-
181
- # Status check (parse JSON to confirm install + onboarding state)
182
- node "$TR_CLI" status --json
183
- # Returns: {"version":"3.3.11-rc.1","onboarded":false,"next_step":"pair","tool_count":17,"hybrid_mode":true}
184
-
185
- # Pair / account setup — ALWAYS run via setsid -f to survive gateway restarts
186
- PAIR_OUT="/tmp/tr-pair-out-$(date +%s).json"
187
- setsid -f node "$TR_CLI" pair --json > "$PAIR_OUT" 2>&1 < /dev/null && sleep 2 && cat "$PAIR_OUT"
188
- # Returns: {"v":1,"sid":"...","url":"https://...","pin":"123456","mode":"generate","expires_at_ms":...,"qr_ascii":"..."}
189
-
190
- # Remember a fact (foreground OK — non-blocking single-shot HTTP call)
191
- node "$TR_CLI" remember --json "I work at Graph Foundation"
192
- # Returns: {"ok":true,"id":"...","claim_count":N}
193
-
194
- # Recall memories
195
- node "$TR_CLI" recall --json "where does the user work" --limit 5
196
- # Returns: {"results":[{"text":"...","score":0.8},...]}
65
+ node "$TR_CLI" pair --json # start/re-pair
66
+ node "$TR_CLI" status --json # {"version":"…","paired":true,…}
197
67
  ```
198
68
 
199
- **Note on tool names**: `totalreclaw_remember`, `totalreclaw_recall`, etc. may or may not appear in your toolset depending on OpenClaw version. Under hybrid-primary, you use the `tr` CLI instead. If native tools ARE available (older OpenClaw or future OC fix), they work too — but always prefer `tr` for reliability.
200
-
201
- ## Rendering the QR on your transport (3.3.10-rc.4 — corrected)
202
-
203
- `tr pair --json` returns a `qr_ascii` field — Unicode block-char QR (43 lines × ~43 cols). That is the ONLY QR field in the payload. The legacy `qr_png_b64` and `qr_unicode` field names referenced by older skill docs no longer exist.
204
-
205
- The simplest path that works on every transport (Telegram, Slack, web, terminal SSH): emit `qr_ascii` INLINE inside a triple-backtick code block right above user-visible line 3. Block-char QR renders correctly in monospace fonts on every modern transport — the user scans from their phone. The pair PAGE itself renders a real PNG QR alongside, so users on a desktop browser see both.
206
-
207
- Format the line-3 message as:
208
-
209
- ```
210
- <qr_ascii contents — 43 lines of ▄▀█▄▀█ block chars>
211
- ```
69
+ ## Restart (agent-driven, never user-manual)
212
70
 
213
- Open <url> in your browser. Enter PIN <pin>. Generate or paste a 12-word recovery phrase. Reply `done` once it's sealed.
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.
214
72
 
215
- Do NOT decode `qr_png_b64` (does not exist), do NOT save a PNG to `/tmp/totalreclaw-pair-qr.png` (`LocalMediaAccessError` blocks delivery), do NOT re-render the QR through any other library. The `qr_ascii` field is the single source of truth.
73
+ ## Tools + CLI surface
216
74
 
217
- The QR encodes ONLY the account-setup URL. The 6-digit PIN is a separate out-of-band confirmation deliberately NOT baked into the QR (dual-channel design: scan + type). Never log, print, or re-emit the PIN outside the user-facing reply. The PIN must NOT end up in any other tool call, log file, or memory store.
218
-
219
- ## Browser flow context (know it, narrate only on user question)
220
-
221
- After you emit user-visible line 3, the user opens the URL on their phone or desktop browser. They walk through a 3-step wizard before the relay forwards the encrypted phrase to the plugin. You don't narrate this proactively (silence rules). But you MUST know it so you can answer questions and recover from issues:
222
-
223
- 1. **Step 1 of 3 — Enter your PIN.** Six PIN cells. The user types or pastes the 6-digit PIN you sent in line 3. "Continue" is disabled until all 6 digits match. There's a 10-minute countdown displayed in the corner.
224
- 2. **Step 2 of 3 — Your recovery phrase.** Two tabs: **Set up** (default — generates a fresh 12-word BIP-39 phrase locally in the browser) and **Log in** (paste an existing phrase to restore on a new device). User must check **"I've written this down and stored it somewhere safe"** then click **Set up TotalReclaw**.
225
- 3. **Step 3 of 3 — You're all set.** Confirmation screen with a "Close this page" link. The browser already POSTed the encrypted phrase by this point — the relay forwards to your gateway, the plugin decrypts, writes `~/.totalreclaw/credentials.json`, and your `awaitPhraseUpload` resolves.
226
-
227
- Common user-side issues during pair (recover gracefully — emit ONE tight line, don't pre-narrate):
228
-
229
- - **"The page won't load"** → confirm the URL host is `api.totalreclaw.xyz` (the default for both stable and RC post-3.3.12-rc.1). If a `TOTALRECLAW_SERVER_URL` env override was set, the host might be `api-staging.totalreclaw.xyz` (staging, opt-in) or a self-hosted relay. Wrong stub host → generate a fresh URL.
230
- - **"PIN says expired"** or countdown ran out → re-run the pair block (Step 4 above) and emit line 3 again with the fresh URL+PIN. The previous session is dead.
231
- - **"It says invalid phrase"** during Log in → the user pasted a phrase that isn't BIP-39 valid (typo or wrong source). Tell them to double-check and re-paste; or switch to **Set up** tab to generate a fresh one (loses existing memories).
232
- - **"I clicked Set up TotalReclaw but nothing happened"** / **502** → the gateway WS dropped before respond. The pair subprocess is alive (you used `setsid -f`). Wait 30s; if the user still sees the 502, re-run Step 4 (the deferred reload should have completed by then).
233
- - **"Where do I write down the phrase?"** → tell them: anywhere safe and durable — paper, password manager, encrypted note. NOT in this chat. NOT in a screenshot to cloud-synced photos. The phrase IS the account; losing it means losing all memories.
234
-
235
- ## Phrase safety (HARD — never break)
236
-
237
- NEVER echo / generate / ask the user to paste a recovery phrase in chat. NEVER invoke `openclaw totalreclaw onboard` (bare interactive form), `totalreclaw setup`, or any phrase-touching CLI via your shell tool (stdout enters LLM context). The ONLY agent-facilitated account-setup paths are (in order of preference): (1) `tr pair --json`; (2) the `totalreclaw_pair` account-setup tool; (3) if both are missing — shell out to EITHER `openclaw totalreclaw pair generate --url-pin-only` (single-line JSON `{v,url,pin,expires_at_ms}`) OR `openclaw totalreclaw onboard --pair-only` (single-line JSON `{v,pair_url,pin,expires_at_ms}`). Both have zero phrase material on stdout (account-setup crypto is x25519-only, not BIP-39). If the user pastes a phrase anyway: tell them it is compromised, re-run `tr pair --json` for a fresh wallet.
238
-
239
- ## Usage (post-setup)
240
-
241
- - Stable user facts / preferences / identity / "remember X":
242
- ```bash
243
- node "$TR_CLI" remember --json "<fact>"
244
- ```
245
- - First-person factual query ("do I / what's my / where do I…"):
246
- ```bash
247
- node "$TR_CLI" recall --json "<query>" --limit 5
248
- ```
249
- Recall FIRST, then answer from returned facts. If 0 results, say so.
250
- - For forget / pin / unpin / retype / set_scope — use native tools if available, or ask user to run `openclaw totalreclaw` subcommand.
251
- - "status" → `tr status --json` (parse version + onboarded + next_step).
252
- - "upgrade" → `totalreclaw_upgrade` native tool (paste Stripe URL verbatim). Post-upgrade testnet → mainnet: `totalreclaw_migrate` with `dry_run` first.
253
- - "import / export / consolidate" → `totalreclaw_import_from` · `totalreclaw_export` · `totalreclaw_consolidate` native tools.
254
-
255
- **Forbidden vocabulary** (these are anti-patterns that violate the silence rule):
256
- - "Let me check that…"
257
- - "Plugin loaded. Let me verify…"
258
- - "I'll now…"
259
- - "Quick fix…"
260
- - "However, I don't see…"
261
- - "I need to use…"
262
- - "Should I /totalreclaw-restart" / "Do you have a public URL" / "Want me to restart" / "Let me check if the tool is bound" / "I need permission to restart"
263
- - "Now installing…" / "Got it" / "In parallel…" / "Step 1a/1b/1c"
264
- - "local" / "local-only" / "stored locally" / "on disk" / "without an account" / "local memory" / "local storage"
265
-
266
- ## Diagnostics
267
-
268
- - `tr` CLI not found → plugin not installed or install still in progress. Wait 5s, retry. If still missing, retry `openclaw plugins install @totalreclaw/totalreclaw@rc` once.
269
- - `totalreclaw_*` tools not visible → EXPECTED under OpenClaw 2026.5.2 (issue #223). Use `tr` CLI instead. Do not restart, do not issue `/totalreclaw-restart`, do not issue `/new`.
270
- - If `/totalreclaw-restart` is needed for another reason, issue the literal slash command `/totalreclaw-restart` — nothing else. Do not narrate.
271
- - User says done but `credentials.json` missing → PIN expired or entered wrong phrase; re-run `tr pair --json` and resend line 3 once.
272
- - `onboarding required` → credentials missing; redo from the account-setup step.
273
- - `quota exceeded` → `tr status --json` or `totalreclaw_status`, then offer `totalreclaw_upgrade`.
274
- - `No LLM available for auto-extraction` at startup → provider key unreachable; check `~/.openclaw/agents/<agent>/agent/auth-profiles.json` or plugin config `extraction.llm`.
275
-
276
- ## Required post-install config (OpenClaw 2026.5.2+)
277
-
278
- Add to `~/.openclaw/openclaw.json` after installing:
279
- ```json
280
- {
281
- "plugins": {
282
- "allow": ["totalreclaw"],
283
- "entries": {
284
- "totalreclaw": {
285
- "hooks": { "allowConversationAccess": true }
286
- }
287
- }
288
- }
289
- }
290
- ```
75
+ **Recall (native memory contractagent-facing):** `memory_search`, `memory_get`.
291
76
 
292
- `plugins.allow` suppresses the "plugins.allow is empty" warning. `hooks.allowConversationAccess` unlocks typed hooks (`agent_end`, etc.) for auto-extraction.
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`.
293
79
 
294
- ## Tool surface
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.
295
81
 
296
- Hybrid-primary: `tr remember` · `tr recall` · `tr pair` · `tr status` (primary path for all agent ops)
297
- Native fallback (when available): `totalreclaw_pair` · `_remember` · `_recall` · `_forget` · `_pin` · `_unpin` · `_retype` · `_set_scope` · `_export` · `_status` · `_upgrade` · `_migrate` · `_import_from` · `_import_batch` · `_consolidate` · `_onboarding_start` · `_report_qa_bug` (RC only)
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. Uses Node.js
5
- * built-in `fetch` (available since Node 18).
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 fetch(`${baseUrl}/v1/register`, {
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 fetch(`${baseUrl}/v1/store`, {
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 fetch(`${baseUrl}/v1/search`, {
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 fetch(`${baseUrl}/v1/facts/${encodeURIComponent(factId)}`, {
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 fetch(`${baseUrl}/v1/facts/batch-delete`, {
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 fetch(`${baseUrl}/v1/export?${params.toString()}`, {
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 fetch(`${baseUrl}/health`, { method: 'GET' });
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,9 @@
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 cached tier so Pro-tier
15
- * UserOps sign against chain 100 and Free-tier stays on 84532
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`)
16
17
  * - does NOT import anything that performs outbound I/O
17
18
  *
18
19
  * Do NOT add any outbound-request call to this file — a single match for
@@ -50,6 +51,12 @@ export interface BillingCache {
50
51
  max_facts_per_extraction?: number;
51
52
  max_candidate_pool?: number;
52
53
  };
54
+ /**
55
+ * Authoritative chain id from the relay `/v1/billing/status` response.
56
+ * After ops-1 (2026-06-05) both tiers are on Gnosis (100); the relay is the
57
+ * source of truth, so the client consumes this verbatim (#402).
58
+ */
59
+ chain_id?: number;
53
60
  checked_at: number;
54
61
  }
55
62
 
@@ -58,24 +65,21 @@ export interface BillingCache {
58
65
  // ---------------------------------------------------------------------------
59
66
 
60
67
  /**
61
- * Apply the billing tier to the runtime chain override.
68
+ * Apply the relay's authoritative `chain_id` to the runtime chain override.
62
69
  *
63
- * Pro tier chain 100 (Gnosis mainnet). Free tier (or unknown) stays on
64
- * 84532 (Base Sepolia). The relay routes Pro UserOps to Gnosis, so the
65
- * client MUST sign them against chain 100 otherwise the bundler returns
66
- * AA23 (invalid signature). See MCP's equivalent path in mcp/src/index.ts.
70
+ * After ops-1 (2026-06-05) both tiers run on Gnosis (chain 100) and the relay
71
+ * returns an authoritative `chain_id` in `/v1/billing/status`. The client MUST
72
+ * consume that verbatim the old tier→chain derivation (Free 84532 Base
73
+ * Sepolia) was retired two-tier logic that mis-signed FREE-tier UserOps
74
+ * against the wrong chain and queried a Base-Sepolia RPC for a Gnosis-deployed
75
+ * sender, producing deterministic AA10 (#402).
67
76
  *
68
- * Called from `readBillingCache` and `writeBillingCache` so that every cache
69
- * read or write keeps the chain override in sync with the cached tier.
70
- * Idempotent calling with the same tier is a no-op.
77
+ * A missing / non-finite `chain_id` (older relay, partial payload) defaults to
78
+ * 100 never 84532. Called from `readBillingCache` and `writeBillingCache` so
79
+ * every cache read or write keeps the override in sync. Idempotent.
71
80
  */
72
- export function syncChainIdFromTier(tier: string | undefined): void {
73
- if (tier === 'pro') {
74
- setChainIdOverride(100);
75
- } else {
76
- // Free or unknown → reset to the default free-tier chain.
77
- setChainIdOverride(84532);
78
- }
81
+ export function syncChainIdFromBilling(chainId: number | undefined): void {
82
+ setChainIdOverride(typeof chainId === 'number' && Number.isFinite(chainId) ? chainId : 100);
79
83
  }
80
84
 
81
85
  // ---------------------------------------------------------------------------
@@ -95,8 +99,9 @@ export function readBillingCache(): BillingCache | null {
95
99
  if (!fs.existsSync(BILLING_CACHE_PATH)) return null;
96
100
  const raw = JSON.parse(fs.readFileSync(BILLING_CACHE_PATH, 'utf-8')) as BillingCache;
97
101
  if (!raw.checked_at || Date.now() - raw.checked_at > BILLING_CACHE_TTL) return null;
98
- // Keep chain override in sync with persisted tier across process restarts.
99
- syncChainIdFromTier(raw.tier);
102
+ // Keep chain override in sync with the persisted authoritative chain_id
103
+ // across process restarts.
104
+ syncChainIdFromBilling(raw.chain_id);
100
105
  return raw;
101
106
  } catch {
102
107
  return null;
@@ -118,5 +123,5 @@ export function writeBillingCache(cache: BillingCache): void {
118
123
  }
119
124
  // Sync chain override AFTER the write so in-process UserOp signing picks
120
125
  // up the correct chain immediately, even if the disk write failed.
121
- syncChainIdFromTier(cache.tier);
126
+ syncChainIdFromBilling(cache.chain_id);
122
127
  }
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).
@@ -281,6 +282,11 @@ export function buildCanonicalClaimV1(input: BuildClaimV1Input): string {
281
282
  if (typeof input.embeddingModelId === 'string' && input.embeddingModelId.length > 0) {
282
283
  canonical.embedding_model_id = input.embeddingModelId;
283
284
  }
285
+ // am-1: Crystal-shaped debrief structured metadata. Re-attached after
286
+ // validation using the same plugin-extra pattern as volatility.
287
+ if (fact.crystalMetadata) {
288
+ canonical.metadata = fact.crystalMetadata;
289
+ }
284
290
 
285
291
  return JSON.stringify(canonical);
286
292
  }
@@ -635,7 +641,7 @@ export type AutoResolveMode = 'active' | 'off' | 'shadow';
635
641
  * @internal Not public config — emergency kill-switch only.
636
642
  */
637
643
  export function resolveAutoResolveMode(): AutoResolveMode {
638
- const raw = (process.env.TOTALRECLAW_AUTO_RESOLVE_MODE ?? '').trim().toLowerCase();
644
+ const raw = envStringLower('TOTALRECLAW_AUTO_RESOLVE_MODE');
639
645
  if (raw === 'off') return 'off';
640
646
  if (raw === 'shadow') return 'shadow';
641
647
  return 'active';