@totalreclaw/totalreclaw 3.3.1-rc.16 → 3.3.1-rc.17

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 CHANGED
@@ -4,38 +4,6 @@ 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.1-rc.16] — 2026-04-24
8
-
9
- Fixes #92 — slow-host install times out during ONNX-runtime / embedding-model
10
- download. ONNX stays mandatory (no opt-in flag); first-call download is now
11
- wrapped with timeout, progress, and retry UX so slow connections succeed
12
- instead of silently hanging until OpenClaw SIGTERMs.
13
-
14
- ### Embedding-model download UX
15
-
16
- - New `download-ux.ts` module — pure stdlib, no third-party imports — exposes
17
- `downloadWithUX(label, fn, opts)`. Wraps a download promise with:
18
- - **Per-attempt timeout**, default 600s (covers ~290 KB/s for the 344 MB
19
- Harrier model). Configurable via env `TOTALRECLAW_ONNX_INSTALL_TIMEOUT`
20
- (in seconds). Per-attempt timeout grows 1x/2x/4x across retries.
21
- - **60s keep-alive log** during long downloads so users on slow networks
22
- see "still downloading… (Ns elapsed)" rather than a frozen prompt.
23
- - **3-attempt exponential-backoff retry** (5s/10s backoff between attempts)
24
- to absorb transient network blips.
25
- - **Loud actionable error** on exhaustion: names the env var to extend the
26
- timeout and the exact `openclaw plugins install totalreclaw` command to
27
- rerun.
28
- - `embedding.ts` now wraps `AutoTokenizer.from_pretrained`,
29
- `AutoModel.from_pretrained`, and the `pipeline()` call with
30
- `downloadWithUX`. Prints a user-visible "Downloading embedding model
31
- (~344MB) — this may take a few minutes on slower connections. Please wait."
32
- message before the first download starts.
33
- - ONNX remains a mandatory hard `dependency` (no `[embedding]`-style opt-in
34
- extra). Recall accuracy is unchanged.
35
- - Regression: `test_issue_92_onnx_download_ux.test.ts` exercises happy path,
36
- transient failure → retry, full exhaustion, per-attempt timeout, and
37
- keep-alive cadence. Wired into the plugin `npm test` chain.
38
-
39
7
  ## [3.3.1-rc.14] — 2026-04-24
40
8
 
41
9
  Coordinated version bump with Python `2.3.1rc14`. Two narrow bug fixes
package/SKILL.md CHANGED
@@ -66,7 +66,7 @@ The QR encodes ONLY the pair URL. The 6-digit PIN is a separate out-of-band conf
66
66
 
67
67
  ## Phrase safety (HARD — never break)
68
68
 
69
- NEVER echo / generate / ask the user to paste a recovery phrase in chat. NEVER invoke `openclaw totalreclaw onboard`, `totalreclaw setup`, or any phrase-touching CLI via your shell tool (stdout enters LLM context). The ONLY agent-facilitated setup path is `totalreclaw_pair`. If the user pastes a phrase anyway: tell them it is compromised, call `totalreclaw_pair` with `mode=generate` for a fresh wallet.
69
+ NEVER echo / generate / ask the user to paste a recovery phrase in chat. NEVER invoke `openclaw totalreclaw onboard`, `totalreclaw setup`, or any phrase-touching CLI via your shell tool (stdout enters LLM context). The ONLY agent-facilitated setup paths are (in order of preference): (1) `totalreclaw_pair` tool; (2) if the tool is missing from your tool list — a known gap for some container-based agents, issue #87 — shell out to `openclaw totalreclaw pair generate --url-pin-only`, which emits a single-line JSON `{v,url,pin,expires_at_ms}` with zero phrase material on stdout (pair-crypto is x25519-only, not BIP-39). If the user pastes a phrase anyway: tell them it is compromised, call `totalreclaw_pair` with `mode=generate` for a fresh wallet.
70
70
 
71
71
  ## Usage (post-setup)
72
72
 
@@ -78,7 +78,7 @@ NEVER echo / generate / ask the user to paste a recovery phrase in chat. NEVER i
78
78
 
79
79
  ## Diagnostics
80
80
 
81
- - `totalreclaw_*` tools not visible → plugin not installed or gateway not restarted. Verify via `openclaw plugins list | grep totalreclaw`. If listed, ask the user to restart the gateway.
81
+ - `totalreclaw_*` tools not visible → plugin not installed or gateway not restarted. Verify via `openclaw plugins list | grep totalreclaw`. If listed but tools still absent (container-agent gap, issue #87), shell out to `openclaw totalreclaw pair generate --url-pin-only` on the gateway host and hand the user the `url` + `pin` from the JSON payload — do NOT suggest `openclaw totalreclaw onboard` (leaks phrase on stdout).
82
82
  - User says done but `credentials.json` missing → PIN expired or entered wrong phrase; call `totalreclaw_pair` again.
83
83
  - `onboarding required` → credentials missing; redo from the pair step.
84
84
  - `quota exceeded` → `totalreclaw_status`, then offer `totalreclaw_upgrade`.
package/embedding.ts CHANGED
@@ -9,17 +9,10 @@
9
9
  * `TOTALRECLAW_EMBEDDING_MODEL` user-facing env var was removed in v1.
10
10
  *
11
11
  * Dependencies: @huggingface/transformers
12
- *
13
- * Download UX (rc.16, fixes #92):
14
- * First-call download is wrapped via `downloadWithUX` from `download-ux.ts`
15
- * — configurable timeout (`TOTALRECLAW_ONNX_INSTALL_TIMEOUT`, default 600s),
16
- * 60s keep-alive, 3-attempt exponential-backoff retry, loud actionable
17
- * failure. Slow-bandwidth hosts no longer see a silent freeze.
18
12
  */
19
13
 
20
14
  // @ts-ignore - @huggingface/transformers types may not be perfect
21
15
  import { AutoTokenizer, AutoModel, pipeline, type FeatureExtractionPipeline } from '@huggingface/transformers';
22
- import { downloadWithUX, getDownloadTimeoutMs } from './download-ux.js';
23
16
 
24
17
  interface ModelConfig {
25
18
  id: string;
@@ -61,36 +54,20 @@ export async function generateEmbedding(
61
54
  ): Promise<number[]> {
62
55
  if (!activeModel) {
63
56
  activeModel = getModelConfig();
64
- const timeoutSec = Math.floor(getDownloadTimeoutMs() / 1000);
65
- console.error(
66
- `[TotalReclaw] Downloading embedding model (${activeModel.size}) — this may take a few minutes on slower connections. Please wait.`,
67
- );
68
- console.error(
69
- `[TotalReclaw] One-time setup. Per-attempt timeout: ${timeoutSec}s (configurable via TOTALRECLAW_ONNX_INSTALL_TIMEOUT). Cached after first download.`,
70
- );
57
+ console.error(`[TotalReclaw] Downloading embedding model (${activeModel.size}, one-time setup)...`);
58
+ console.error('[TotalReclaw] This enables semantic search across your encrypted memories.');
71
59
 
72
60
  if (activeModel.pooling === 'sentence_embedding') {
73
61
  // Harrier: use AutoModel (pipeline doesn't support sentence_embedding output)
74
- autoTokenizer = await downloadWithUX(
75
- 'tokenizer',
76
- () => AutoTokenizer.from_pretrained(activeModel!.id),
77
- );
78
- autoModel = await downloadWithUX(
79
- 'embedding model',
80
- () =>
81
- AutoModel.from_pretrained(activeModel!.id, {
82
- dtype: activeModel!.dtype as any,
83
- }),
84
- );
62
+ autoTokenizer = await AutoTokenizer.from_pretrained(activeModel.id);
63
+ autoModel = await AutoModel.from_pretrained(activeModel.id, {
64
+ dtype: activeModel.dtype as any,
65
+ });
85
66
  } else {
86
67
  // e5-small / Qwen: use pipeline
87
- pipelineExtractor = await downloadWithUX(
88
- 'embedding pipeline',
89
- () =>
90
- pipeline('feature-extraction', activeModel!.id, {
91
- dtype: activeModel!.dtype as any,
92
- }),
93
- );
68
+ pipelineExtractor = await pipeline('feature-extraction', activeModel.id, {
69
+ dtype: activeModel.dtype as any,
70
+ });
94
71
  }
95
72
  console.error('[TotalReclaw] Embedding model ready. Future startups will be instant.');
96
73
  }
package/index.ts CHANGED
@@ -5412,11 +5412,18 @@ const plugin = {
5412
5412
  '## TotalReclaw setup pending\n\n' +
5413
5413
  'TotalReclaw encrypted memory is installed but not yet set up on this machine. ' +
5414
5414
  'If the user asks about memory features or wants to configure TotalReclaw, ' +
5415
- 'call the `totalreclaw_onboarding_start` tool for instructions.\n\n' +
5415
+ 'call the `totalreclaw_pair` tool it returns a pairing URL + 6-digit PIN ' +
5416
+ 'the user opens in a browser (phrase is generated and encrypted in-browser, ' +
5417
+ 'never leaves their device in plaintext). If `totalreclaw_pair` is not in ' +
5418
+ 'your tool list (container-based agents sometimes miss it), shell out to ' +
5419
+ '`openclaw totalreclaw pair generate --url-pin-only` on the gateway host — ' +
5420
+ 'it emits a single JSON line `{v,url,pin,expires_at_ms}` with no phrase ' +
5421
+ 'material; hand the user the `url` and `pin`.\n\n' +
5416
5422
  '**Do NOT** attempt to generate, display, or relay a recovery phrase in chat. ' +
5417
- 'All phrase handling happens on the user\'s local terminal via ' +
5418
- '`openclaw totalreclaw onboard` this keeps the phrase out of the LLM ' +
5419
- 'provider\'s logs.',
5423
+ '**Do NOT** run `openclaw totalreclaw onboard` that CLI emits the recovery ' +
5424
+ 'phrase on stdout and would leak it into the LLM transcript. Use `pair` ' +
5425
+ '(tool or `--url-pin-only` CLI) instead; `onboard` is reserved for users ' +
5426
+ 'running it directly in their own local terminal.',
5420
5427
  };
5421
5428
  }
5422
5429
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@totalreclaw/totalreclaw",
3
- "version": "3.3.1-rc.16",
3
+ "version": "3.3.1-rc.17",
4
4
  "description": "End-to-end encrypted, agent-portable memory for OpenClaw and any LLM-agent runtime. XChaCha20-Poly1305 with protobuf v4 + on-chain Memory Taxonomy v1 (claim / preference / directive / commitment / episode / summary).",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -54,7 +54,7 @@
54
54
  "skill.json"
55
55
  ],
56
56
  "scripts": {
57
- "test": "npx tsx manifest-shape.test.ts && npx tsx config-schema.test.ts && npx tsx llm-profile-reader.test.ts && npx tsx llm-client.test.ts && npx tsx llm-client-retry.test.ts && npx tsx gateway-url.test.ts && npx tsx retype-setscope.test.ts && npx tsx tool-gating.test.ts && npx tsx onboarding-noninteractive.test.ts && npx tsx pair-cli-json.test.ts && npx tsx pair-qr.test.ts && npx tsx pair-remote-client.test.ts && npx tsx qa-bug-report.test.ts && npx tsx nonce-serialization.test.ts && npx tsx phrase-safety-registry.test.ts && npx tsx test_issue_92_onnx_download_ux.test.ts",
57
+ "test": "npx tsx manifest-shape.test.ts && npx tsx config-schema.test.ts && npx tsx llm-profile-reader.test.ts && npx tsx llm-client.test.ts && npx tsx llm-client-retry.test.ts && npx tsx gateway-url.test.ts && npx tsx retype-setscope.test.ts && npx tsx tool-gating.test.ts && npx tsx onboarding-noninteractive.test.ts && npx tsx pair-cli-json.test.ts && npx tsx pair-qr.test.ts && npx tsx pair-remote-client.test.ts && npx tsx qa-bug-report.test.ts && npx tsx nonce-serialization.test.ts && npx tsx phrase-safety-registry.test.ts",
58
58
  "check-scanner": "node ../scripts/check-scanner.mjs",
59
59
  "prepublishOnly": "node ../scripts/check-scanner.mjs"
60
60
  },
package/pair-cli.ts CHANGED
@@ -85,8 +85,15 @@ export interface PairCliOutcome {
85
85
  * as the session reaches a terminal state — same status-code
86
86
  * semantics as 'human' (0 on completed, 1 on expired/rejected/error,
87
87
  * 130 on canceled).
88
+ * - 'url-pin': (3.3.1-rc.15, issue #87) headless container-agent fallback.
89
+ * Emits ONLY `{ v, url, pin, expires_at_ms }` — no QR ASCII, no SID,
90
+ * no mode echo. Use when a container-based agent cannot see the
91
+ * `totalreclaw_pair` tool (OpenClaw gateway-to-container tool-injection
92
+ * gap) and must shell out to the CLI. Guarantees zero phrase material
93
+ * on stdout by construction — pair-crypto is x25519-only and the slim
94
+ * payload carries nothing BIP-39-adjacent.
88
95
  */
89
- export type PairCliOutputMode = 'human' | 'json';
96
+ export type PairCliOutputMode = 'human' | 'json' | 'url-pin';
90
97
 
91
98
  /**
92
99
  * JSON payload emitted by runPairCli when outputMode === 'json'. Printed
@@ -103,6 +110,17 @@ export interface PairCliJsonPayload {
103
110
  qr_ascii: string;
104
111
  }
105
112
 
113
+ /**
114
+ * Slim payload for outputMode === 'url-pin'. Intentionally a subset of
115
+ * `PairCliJsonPayload` with no QR ASCII, SID, or mode echo. Issue #87.
116
+ */
117
+ export interface PairCliUrlPinPayload {
118
+ v: 1;
119
+ url: string;
120
+ pin: string;
121
+ expires_at_ms: number;
122
+ }
123
+
106
124
  // ---------------------------------------------------------------------------
107
125
  // Default stdout IO
108
126
  // ---------------------------------------------------------------------------
@@ -213,9 +231,11 @@ export async function runPairCli(
213
231
  return { status: 'error', error: msg };
214
232
  }
215
233
 
216
- // 2. Render the QR promise-based so both human + json modes share it.
234
+ // 2. Build the URL unconditionally, but only render the QR for modes
235
+ // that actually emit it. url-pin mode skips the renderer entirely —
236
+ // no CPU cost, no qrcode-terminal import, no ASCII on stdout.
217
237
  const url = deps.renderPairingUrl(session);
218
- const qrAscii = await new Promise<string>((resolve) => {
238
+ const qrAscii = outputMode === 'url-pin' ? '' : await new Promise<string>((resolve) => {
219
239
  // Guard against QR renderers that never fire their callback (shouldn't
220
240
  // happen with qrcode-terminal, but defensive): a 10-second timeout
221
241
  // returns an empty string so we never hang the pairing flow.
@@ -241,8 +261,16 @@ export async function runPairCli(
241
261
  }
242
262
  });
243
263
 
244
- // 3. Emit the visible surface (JSON first — single line — or human copy).
245
- if (outputMode === 'json') {
264
+ // 3. Emit the visible surface (JSON/url-pin first — single line — or human copy).
265
+ if (outputMode === 'url-pin') {
266
+ const payload: PairCliUrlPinPayload = {
267
+ v: 1,
268
+ url,
269
+ pin: session.secondaryCode,
270
+ expires_at_ms: session.expiresAtMs,
271
+ };
272
+ stdout.write(JSON.stringify(payload) + '\n');
273
+ } else if (outputMode === 'json') {
246
274
  const payload: PairCliJsonPayload = {
247
275
  v: 1,
248
276
  sid: session.sid,
@@ -276,7 +304,9 @@ export async function runPairCli(
276
304
  canceled = true;
277
305
  });
278
306
 
279
- // 5. Poll
307
+ // 5. Poll — status transitions only surface in human mode; json/url-pin
308
+ // modes stay silent after the single payload line so agents parsing
309
+ // stdout get one JSON line and an exit code, nothing else.
280
310
  const emitStatus = (text: string): void => {
281
311
  if (outputMode === 'human') stdout.write(text);
282
312
  };
@@ -399,14 +429,19 @@ export function registerPairCli(
399
429
  'Pair a remote browser device to this gateway (mode = generate | import; default generate)',
400
430
  )
401
431
  .option('--json', 'Emit a single JSON payload (url/pin/sid/qr_ascii) instead of the human-readable banner. Enables agent-driven pairing.')
432
+ .option('--url-pin-only', 'Emit ONLY {v,url,pin,expires_at_ms} — no QR ASCII, no SID, no mode echo. Headless fallback for container-based agents where the totalreclaw_pair tool is not injected (issue #87). Zero phrase exposure on stdout.')
402
433
  .option('--timeout <sec>', 'Session TTL in seconds (default: 900 = 15 min, matches pair-session-store default)')
403
434
  .action(async (...args: unknown[]) => {
404
435
  // commander passes: [modeArg, options, cmd]
405
436
  const modeRaw = typeof args[0] === 'string' ? args[0] : undefined;
406
- const opts = (args[1] ?? {}) as { json?: boolean; timeout?: string | number };
437
+ const opts = (args[1] ?? {}) as { json?: boolean; urlPinOnly?: boolean; timeout?: string | number };
407
438
  const mode: PairCliMode =
408
439
  modeRaw === 'import' || modeRaw === 'imp' ? 'import' : 'generate';
409
- const outputMode: PairCliOutputMode = opts.json ? 'json' : 'human';
440
+ // --url-pin-only wins over --json when both are passed, since it is
441
+ // strictly the tighter surface (no QR, no SID). The flag is a subset.
442
+ const outputMode: PairCliOutputMode = opts.urlPinOnly
443
+ ? 'url-pin'
444
+ : opts.json ? 'json' : 'human';
410
445
  let ttlSeconds: number | undefined;
411
446
  if (typeof opts.timeout === 'number' && Number.isFinite(opts.timeout)) {
412
447
  ttlSeconds = opts.timeout;
package/download-ux.ts DELETED
@@ -1,91 +0,0 @@
1
- /**
2
- * download-ux.ts — Wrapper for heavy first-call downloads (rc.16, fixes #92).
3
- *
4
- * Wraps a download promise with:
5
- * - per-attempt timeout (default 600s, override via TOTALRECLAW_ONNX_INSTALL_TIMEOUT in seconds)
6
- * - 60s keep-alive log so slow-bandwidth users don't think it's frozen
7
- * - 3-attempt exponential-backoff retry (per-attempt timeout grows 1x/2x/4x)
8
- * - loud actionable error after exhaustion
9
- *
10
- * No third-party imports here — pure stdlib so the unit test can exercise it
11
- * without pulling the heavy `@huggingface/transformers` chain.
12
- */
13
-
14
- const DEFAULT_DOWNLOAD_TIMEOUT_MS = 600_000;
15
- const KEEPALIVE_INTERVAL_MS = 60_000;
16
- const MAX_DOWNLOAD_ATTEMPTS = 3;
17
-
18
- export function getDownloadTimeoutMs(): number {
19
- const raw = process.env.TOTALRECLAW_ONNX_INSTALL_TIMEOUT;
20
- if (!raw) return DEFAULT_DOWNLOAD_TIMEOUT_MS;
21
- const parsed = Number(raw);
22
- if (!Number.isFinite(parsed) || parsed <= 0) return DEFAULT_DOWNLOAD_TIMEOUT_MS;
23
- // Spec accepts seconds; convert to ms.
24
- return Math.floor(parsed * 1000);
25
- }
26
-
27
- export interface DownloadWithUXOpts {
28
- /** Override the per-attempt base timeout in ms (env var takes precedence by default). */
29
- timeoutMs?: number;
30
- /** Override the keep-alive cadence in ms. */
31
- keepaliveMs?: number;
32
- /** Override the max attempts. */
33
- maxAttempts?: number;
34
- /** Logger override (defaults to console.error). */
35
- log?: (msg: string) => void;
36
- /** Sleep override for tests; defaults to setTimeout. */
37
- sleep?: (ms: number) => Promise<void>;
38
- }
39
-
40
- export async function downloadWithUX<T>(
41
- label: string,
42
- download: () => Promise<T>,
43
- opts?: DownloadWithUXOpts,
44
- ): Promise<T> {
45
- const baseTimeoutMs = opts?.timeoutMs ?? getDownloadTimeoutMs();
46
- const keepaliveMs = opts?.keepaliveMs ?? KEEPALIVE_INTERVAL_MS;
47
- const maxAttempts = opts?.maxAttempts ?? MAX_DOWNLOAD_ATTEMPTS;
48
- const log = opts?.log ?? ((msg: string) => console.error(msg));
49
- const sleep = opts?.sleep ?? ((ms: number) => new Promise(r => setTimeout(r, ms)));
50
-
51
- let lastErr: unknown = null;
52
-
53
- for (let attempt = 1; attempt <= maxAttempts; attempt++) {
54
- const attemptTimeoutMs = baseTimeoutMs * Math.pow(2, attempt - 1);
55
- const startedAt = Date.now();
56
- const keepaliveTimer = setInterval(() => {
57
- const elapsedSec = Math.floor((Date.now() - startedAt) / 1000);
58
- log(`[TotalReclaw] ${label}: still downloading… (${elapsedSec}s elapsed, attempt ${attempt}/${maxAttempts})`);
59
- }, keepaliveMs);
60
-
61
- try {
62
- const result = await Promise.race([
63
- download(),
64
- new Promise<never>((_, reject) =>
65
- setTimeout(
66
- () => reject(new Error(`Download timeout after ${Math.floor(attemptTimeoutMs / 1000)}s (attempt ${attempt}/${maxAttempts})`)),
67
- attemptTimeoutMs,
68
- ),
69
- ),
70
- ]);
71
- clearInterval(keepaliveTimer);
72
- return result;
73
- } catch (err) {
74
- clearInterval(keepaliveTimer);
75
- lastErr = err;
76
- const msg = err instanceof Error ? err.message : String(err);
77
- if (attempt < maxAttempts) {
78
- const backoffMs = Math.min(5_000 * Math.pow(2, attempt - 1), 30_000);
79
- log(`[TotalReclaw] ${label}: attempt ${attempt} failed (${msg}). Retrying in ${Math.floor(backoffMs / 1000)}s…`);
80
- await sleep(backoffMs);
81
- }
82
- }
83
- }
84
-
85
- const finalMsg = lastErr instanceof Error ? lastErr.message : String(lastErr);
86
- throw new Error(
87
- `[TotalReclaw] Embedding model download failed after ${maxAttempts} attempts (last error: ${finalMsg}). ` +
88
- `Check your network connection and retry: \`openclaw plugins install totalreclaw\`. ` +
89
- `On slow connections, set TOTALRECLAW_ONNX_INSTALL_TIMEOUT=1200 (in seconds) to extend the per-attempt timeout.`,
90
- );
91
- }