@totalreclaw/totalreclaw 3.3.12-rc.13 → 3.3.12-rc.15

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: totalreclaw
3
- description: "End-to-end encrypted, decentralized memory for OpenClaw. Set up an account once, then call totalreclaw_remember / totalreclaw_recall (or the tr CLI under hybrid-primary) instead of writing to MEMORY.md / USER.md / local files. Trigger on 'install TotalReclaw', 'set up TotalReclaw', 'restore my recovery phrase', any remember / recall request, AND any user statement that contains a preference / fact / decision / commitment about themselves."
4
- version: 3.3.12-rc.13
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.15
5
5
  author: TotalReclaw Team
6
6
  license: MIT
7
7
  homepage: https://totalreclaw.xyz
@@ -22,19 +22,17 @@ metadata:
22
22
  - persistent-context
23
23
  ---
24
24
 
25
- # TotalReclaw — OpenClaw plugin
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). The plugin handles install + account-setup autonomously; this skill tells the agent what to call and when.
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
- ## TOP RULE MEMORY STORAGE
29
+ ## How memory works here (read first)
30
30
 
31
- When the user states ANYTHING about themselves preferences, decisions, identity, work, location, plans, tools, commitments call `totalreclaw_remember` (or `tr remember --json "<fact>"` under hybrid-primary). NEVER write user facts to `MEMORY.md`, `USER.md`, or any local file. Those are agent-persona configuration; user memories live encrypted on-chain.
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
- **Trigger immediately, no "should I?":** "I prefer / like / love / hate X", "my favorite is X", "I'm X", "my name is X", "I live / work at X", "I use X", "I decided X", "I'm going to X", "from now on X", "we agreed X", "I'll X by Y", "remind me to X", "remember that X", "save X", "note X". If a message contains MULTIPLE facts, call `totalreclaw_remember` once per fact — never blob.
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
 
@@ -43,34 +41,40 @@ The 12-word recovery phrase MUST NEVER enter chat or the agent's LLM context. Th
43
41
  ## Install
44
42
 
45
43
  ```
46
- openclaw plugins install @totalreclaw/totalreclaw@rc
44
+ openclaw plugins install @totalreclaw/totalreclaw
47
45
  openclaw skills install totalreclaw
48
46
  ```
49
47
 
50
- Plugin is published on npm + ClawHub (`p-diogo`, GitHub-verified). Stable: drop `@rc`. Under hybrid-primary, the `tr` CLI is the primary path; native `totalreclaw_*` tools are also bound when OpenClaw exposes themuse whichever your runtime binds.
51
-
52
- ## Setup (plugin-driven)
48
+ 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
49
 
54
- The plugin auto-creates a pair session on first load when no credentials exist, writes `{url, pin, sid, expires_at_ms}` to `~/.totalreclaw/.pair-pending.json`, and injects a `before_agent_start` context block to you. **When you see that block, surface the URL and PIN to the user VERBATIM — read from the injected context; never invent values.** Confirm on pair completion: `✓ TotalReclaw set up.` (If credentials already exist: `✓ TotalReclaw is already set up.`)
50
+ ## Setup (pair user-initiated QR)
55
51
 
56
- If a restart is needed (rare — plugin self-handles via gateway config-watcher), issue the literal slash command `/totalreclaw-restart`. Never `kill`, `docker restart`, or prompt the user to restart.
52
+ Pairing is a deliberate, user-initiated QR flow (the plugin does **not** auto-pair on load). When credentials don't exist:
57
53
 
58
- ## Hybrid-primary CLI
54
+ 1. Run `tr pair --json` → returns `{"url":"https://…/pair/p/<id>#pk=…","pin":"123456","expires_at_ms":…}`.
55
+ 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."
56
+ 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.
57
+ 4. Confirm: `✓ TotalReclaw set up.` (If credentials already exist when first asked: `✓ TotalReclaw is already set up.`)
59
58
 
60
- OpenClaw 2026.5.x may strip non-bundled tool bindings (issue #223). Never rely on `totalreclaw_*` tool binding alone. Fall back via shell:
59
+ 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
60
 
62
61
  ```bash
63
62
  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" status --json # {"version":"...","hybrid_mode":true,"tool_count":17}
65
- node "$TR_CLI" remember --json "I work at Graph Foundation"
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
63
+ node "$TR_CLI" pair --json # start/re-pair
64
+ node "$TR_CLI" status --json # {"version":"…","paired":true,…}
68
65
  ```
69
66
 
70
- Pair is UNCONDITIONAL when no credentials — pasting the install URL IS the consent. `tr --help` lists every subcommand.
67
+ ## Restart (agent-driven, never user-manual)
68
+
69
+ 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.
70
+
71
+ ## Tools + CLI surface
72
+
73
+ **Recall (native memory contract — agent-facing):** `memory_search`, `memory_get`.
71
74
 
72
- ## Tools (16 native; same surface via `tr`)
75
+ **Explicit capture + curation (CLI `tr`, i.e. `node "$TR_CLI" …`):**
76
+ `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
77
 
74
- `totalreclaw_remember` · `totalreclaw_recall` · `totalreclaw_forget` · `totalreclaw_pair` · `totalreclaw_pin` · `totalreclaw_unpin` · `totalreclaw_retype` · `totalreclaw_set_scope` · `totalreclaw_export` · `totalreclaw_import_from` · `totalreclaw_import_batch` · `totalreclaw_consolidate` · `totalreclaw_status` · `totalreclaw_upgrade` · `totalreclaw_onboarding_start` · `totalreclaw_report_qa_bug` (RC only).
78
+ 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
79
 
76
- Full guides: <https://github.com/p-diogo/totalreclaw/blob/main/docs/guides/openclaw-setup.md>
80
+ Full guide: <https://github.com/p-diogo/totalreclaw/blob/main/docs/guides/openclaw-setup.md>
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. Free tier stays on 84532 (Base Sepolia); Pro tier flips to 100
106
- * (Gnosis mainnet). The relay routes Pro writes to Gnosis, so Pro-tier
107
- * UserOps MUST be signed against chain 100 otherwise the bundler rejects
108
- * the signature with AA23.
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
@@ -221,9 +221,8 @@ export const CONFIG = {
221
221
  : 'wss://api.totalreclaw.xyz')
222
222
  ).replace(/\/+$/, ''),
223
223
 
224
- // Chain — chainId is no longer user-configurable. It is auto-detected from
225
- // the relay billing response (free = Base Sepolia / 84532, Pro = Gnosis /
226
- // 100). The default here is used only before the first billing lookup
224
+ // Chain — chainId is no longer user-configurable. After the ops-1 single-
225
+ // chain migration, ALL tiers are on Gnosis (100). The default here is 100.
227
226
  // completes. Self-hosted users can still point at a custom DataEdge via
228
227
  // TOTALRECLAW_DATA_EDGE_ADDRESS / TOTALRECLAW_ENTRYPOINT_ADDRESS /
229
228
  // TOTALRECLAW_RPC_URL (undocumented; internal knobs).
@@ -233,7 +232,7 @@ export const CONFIG = {
233
232
  // not a literal — a literal would freeze all Pro-tier UserOps to the
234
233
  // wrong chainId and AA23 at the bundler.
235
234
  get chainId(): number {
236
- return _chainIdOverride ?? 84532;
235
+ return _chainIdOverride ?? 100;
237
236
  },
238
237
  dataEdgeAddress: process.env.TOTALRECLAW_DATA_EDGE_ADDRESS || '',
239
238
  entryPointAddress: process.env.TOTALRECLAW_ENTRYPOINT_ADDRESS || '',
package/dist/config.js CHANGED
@@ -90,10 +90,10 @@ export function getSessionId() {
90
90
  }
91
91
  /**
92
92
  * Runtime override for chain ID, set after the relay billing response is
93
- * read. Free tier stays on 84532 (Base Sepolia); Pro tier flips to 100
94
- * (Gnosis mainnet). The relay routes Pro writes to Gnosis, so Pro-tier
95
- * UserOps MUST be signed against chain 100 otherwise the bundler rejects
96
- * the signature with AA23.
93
+ * read. After the ops-1 single-chain migration (2026-05), ALL tiers (free
94
+ * + Pro) are on Gnosis mainnet (chain 100). The default below is 100.
95
+ * The relay routes all writes to Gnosis, so UserOps MUST be signed against
96
+ * chain 100 — otherwise the bundler rejects the signature with AA24.
97
97
  *
98
98
  * See index.ts: after the billing lookup completes, call
99
99
  * `setChainIdOverride(100)` for Pro users. Free users can leave the
@@ -202,9 +202,8 @@ export const CONFIG = {
202
202
  || (process.env.TOTALRECLAW_SERVER_URL
203
203
  ? process.env.TOTALRECLAW_SERVER_URL.replace(/^https?:\/\//, 'wss://').replace(/^http:/, 'ws:')
204
204
  : 'wss://api.totalreclaw.xyz')).replace(/\/+$/, ''),
205
- // Chain — chainId is no longer user-configurable. It is auto-detected from
206
- // the relay billing response (free = Base Sepolia / 84532, Pro = Gnosis /
207
- // 100). The default here is used only before the first billing lookup
205
+ // Chain — chainId is no longer user-configurable. After the ops-1 single-
206
+ // chain migration, ALL tiers are on Gnosis (100). The default here is 100.
208
207
  // completes. Self-hosted users can still point at a custom DataEdge via
209
208
  // TOTALRECLAW_DATA_EDGE_ADDRESS / TOTALRECLAW_ENTRYPOINT_ADDRESS /
210
209
  // TOTALRECLAW_RPC_URL (undocumented; internal knobs).
@@ -214,7 +213,7 @@ export const CONFIG = {
214
213
  // not a literal — a literal would freeze all Pro-tier UserOps to the
215
214
  // wrong chainId and AA23 at the bundler.
216
215
  get chainId() {
217
- return _chainIdOverride ?? 84532;
216
+ return _chainIdOverride ?? 100;
218
217
  },
219
218
  dataEdgeAddress: process.env.TOTALRECLAW_DATA_EDGE_ADDRESS || '',
220
219
  entryPointAddress: process.env.TOTALRECLAW_ENTRYPOINT_ADDRESS || '',
package/dist/index.js CHANGED
@@ -3524,6 +3524,17 @@ const plugin = {
3524
3524
  sessionsPath: CONFIG.pairSessionsPath,
3525
3525
  apiBase: '/plugin/totalreclaw/pair',
3526
3526
  logger: api.logger,
3527
+ // 3.3.14 — wire the relay URL so buildPairRoutes exposes the
3528
+ // in-process `/pair/init` route. The gateway process opens the
3529
+ // relay WebSocket directly (via openRemotePairSession from
3530
+ // pair-remote-client.ts), eliminating the 30s-subprocess-kill
3531
+ // 502 that the CLI path (tr pair) hit when OpenClaw's shell
3532
+ // tool killed the subprocess mid-pair. relayBaseUrl is sourced
3533
+ // from CONFIG.pairRelayUrl (config.ts reads it from the env
3534
+ // once, centrally) — never read from the environment inside
3535
+ // pair-http.ts (scanner-surface rule).
3536
+ relayBaseUrl: CONFIG.pairRelayUrl,
3537
+ initPairMode: 'either',
3527
3538
  validateMnemonic: (p) => validateMnemonic(p, wordlist),
3528
3539
  completePairing: async ({ mnemonic }) => {
3529
3540
  // Write credentials.json + flip state to 'active' via
@@ -3584,7 +3595,20 @@ const plugin = {
3584
3595
  api.registerHttpRoute({ path: bundle.startPath, handler: bundle.handlers.start, auth: 'plugin' });
3585
3596
  api.registerHttpRoute({ path: bundle.respondPath, handler: bundle.handlers.respond, auth: 'plugin' });
3586
3597
  api.registerHttpRoute({ path: bundle.statusPath, handler: bundle.handlers.status, auth: 'plugin' });
3587
- api.logger.info('TotalReclaw: registered 4 QR-pairing HTTP routes synchronously');
3598
+ // 3.3.14 in-process pair trigger. The bundle exposes initPath +
3599
+ // handlers.init ONLY when relayBaseUrl is wired (always true here,
3600
+ // since CONFIG.pairRelayUrl has a built-in default). Registered
3601
+ // with auth: 'plugin' (same as the other pair routes) so the
3602
+ // agent's localhost curl reaches it without a gateway bearer
3603
+ // token. The route opens the relay WS in the gateway process →
3604
+ // survives shell-tool timeouts, retries, SIGUSR1 reloads.
3605
+ if (bundle.initPath && bundle.handlers.init) {
3606
+ api.registerHttpRoute({ path: bundle.initPath, handler: bundle.handlers.init, auth: 'plugin' });
3607
+ api.logger.info('TotalReclaw: registered 5 QR-pairing HTTP routes synchronously (incl. in-process /pair/init)');
3608
+ }
3609
+ else {
3610
+ api.logger.info('TotalReclaw: registered 4 QR-pairing HTTP routes synchronously (in-process /pair/init not wired — no relay URL)');
3611
+ }
3588
3612
  }
3589
3613
  else {
3590
3614
  api.logger.warn('api.registerHttpRoute is unavailable on this OpenClaw version — /totalreclaw pair will not work. ' +
package/dist/pair-http.js CHANGED
@@ -2,7 +2,23 @@
2
2
  * pair-http — gateway-side HTTP route handlers for the v3.3.0 QR-pairing
3
3
  * flow. Registered via `api.registerHttpRoute` from `index.ts`.
4
4
  *
5
- * Three endpoints (all under /plugin/totalreclaw/pair/):
5
+ * Five endpoints (all under /plugin/totalreclaw/pair/):
6
+ *
7
+ * GET /plugin/totalreclaw/pair/init
8
+ * → IN-PROCESS pair trigger (3.3.14). Opens the relay WebSocket
9
+ * directly in the gateway process via `openRemotePairSession`
10
+ * (from pair-remote-client.ts), returns `{url, pin, sid,
11
+ * expires_at_ms}` immediately so the agent can surface URL+PIN
12
+ * to the user, and starts a BACKGROUND `awaitPhraseUpload` that
13
+ * blocks on the WS for the browser's encrypted phrase, decrypts
14
+ * locally, and invokes the injected `completePairing` callback
15
+ * (writes credentials.json + flips onboarding state). The WS
16
+ * lives in the gateway process — immune to the shell-tool's
17
+ * 30s subprocess timeout that killed the `tr pair` CLI path
18
+ * (relay returned 502 on /pair/respond when the subprocess
19
+ * died). This is the primary agent-facilitated pair path; the
20
+ * CLI `tr pair --json` remains as a fallback for non-agent
21
+ * scenarios.
6
22
  *
7
23
  * GET /plugin/totalreclaw/pair/finish?sid=<sid>
8
24
  * → returns the browser pairing page (HTML + inline JS + CSS).
@@ -37,6 +53,12 @@
37
53
  * - NO environment-variable reads. All config values flow in via
38
54
  * `PairHttpConfig`; callers read from `CONFIG` in `config.ts`.
39
55
  *
56
+ * Adding `openRemotePairSession` (which dials an outbound WebSocket to
57
+ * the relay) keeps this file env=N, net=Y → the env-harvesting rule
58
+ * requires BOTH an env read AND a request trigger in the same file;
59
+ * the relay base URL arrives via `PairHttpConfig.relayBaseUrl` (caller-
60
+ * injected), never read from the environment here.
61
+ *
40
62
  * Logging: NEVER logs the secondary code, the mnemonic, the gateway
41
63
  * private key, or raw request bodies. Session ids and status
42
64
  * transitions are logged at info/warn levels for diagnostics.
@@ -44,9 +66,11 @@
44
66
  import { consumePairSession, getPairSession, registerFailedSecondaryCode, rejectPairSession, transitionPairSession, MAX_SECONDARY_CODE_ATTEMPTS, } from './pair-session-store.js';
45
67
  import { compareSecondaryCodesCT, decryptPairingPayload } from './pair-crypto.js';
46
68
  import { renderPairPage } from './pair-page.js';
69
+ import { awaitPhraseUpload, openRemotePairSession, } from './pair-remote-client.js';
47
70
  /**
48
- * Build the four handlers. The caller registers each with
49
- * `api.registerHttpRoute({ path, handler })`.
71
+ * Build the route handlers. The caller registers each with
72
+ * `api.registerHttpRoute({ path, handler })`. When `cfg.relayBaseUrl` is
73
+ * set, the bundle also carries the in-process `/pair/init` handler.
50
74
  */
51
75
  export function buildPairRoutes(cfg) {
52
76
  const apiBase = cfg.apiBase.replace(/\/+$/, '');
@@ -283,7 +307,117 @@ export function buildPairRoutes(cfg) {
283
307
  mode: session.mode,
284
308
  });
285
309
  }
286
- return {
310
+ // ---------------------------------------------------------------
311
+ // 3.3.14 — In-process pair trigger (the 30s-subprocess-kill 502 fix)
312
+ // ---------------------------------------------------------------
313
+ //
314
+ // Defined unconditionally so the handler identity is stable for the
315
+ // lifetime of the bundle; it short-circuits with 503 when no relay
316
+ // URL was wired. Only attached to the returned bundle + registered
317
+ // as a route when `cfg.relayBaseUrl` is set (see the return below).
318
+ async function handleInit(req, res) {
319
+ if (!methodAllowed(req, ['GET'])) {
320
+ sendJson(res, 405, { error: 'method_not_allowed' });
321
+ return;
322
+ }
323
+ const relay = cfg.relayBaseUrl;
324
+ if (!relay) {
325
+ // Caller did not wire a relay URL — the in-process route is inert.
326
+ sendJson(res, 503, { error: 'init_not_configured' });
327
+ return;
328
+ }
329
+ // 1. Open the relay WebSocket IN-PROCESS (the gateway process owns the
330
+ // socket). This is the fix: the WS is no longer held by a CLI
331
+ // subprocess, so OpenClaw's 30s shell-tool timeout cannot kill it
332
+ // and the relay never sees a mid-pair disconnect → no more 502 on
333
+ // /pair/respond.
334
+ let session;
335
+ try {
336
+ const openOpts = {
337
+ relayBaseUrl: relay,
338
+ mode: cfg.initPairMode ?? 'either',
339
+ };
340
+ if (cfg.initWebSocketImpl) {
341
+ openOpts.webSocketImpl = cfg.initWebSocketImpl;
342
+ }
343
+ session = await openRemotePairSession(openOpts);
344
+ }
345
+ catch (err) {
346
+ const msg = err instanceof Error ? err.message : String(err);
347
+ cfg.logger.warn(`pair-http /init: relay session open failed: ${msg}`);
348
+ sendJson(res, 502, { error: 'relay_open_failed', detail: msg });
349
+ return;
350
+ }
351
+ const parsedExpiresMs = Date.parse(session.expiresAt);
352
+ const expiresAtMs = Number.isFinite(parsedExpiresMs)
353
+ ? parsedExpiresMs
354
+ : now() + 5 * 60_000;
355
+ // 2. Respond IMMEDIATELY with the URL + PIN + sid (relay token) +
356
+ // expiry. The agent reads this and surfaces the URL+PIN to the
357
+ // user. The relay token here plays the `sid` role for agent-side
358
+ // correlation (parity with the CLI JSON payload shape).
359
+ sendJson(res, 200, {
360
+ v: 1,
361
+ sid: session.token,
362
+ url: session.url,
363
+ pin: session.pin,
364
+ mode: session.mode,
365
+ expires_at_ms: expiresAtMs,
366
+ });
367
+ // 3. Start the background wait IN THE GATEWAY PROCESS. We do NOT
368
+ // await this from the request handler — the HTTP response has
369
+ // already been sent. The promise resolves when the browser
370
+ // uploads the encrypted phrase (the relay pushes a `forward`
371
+ // frame), the gateway decrypts locally, and `completePairing`
372
+ // writes credentials.json + flips onboarding state. Errors are
373
+ // logged but never reach the HTTP response (it's already gone).
374
+ //
375
+ // The injected `completePairing` callback receives the mnemonic
376
+ // + a session-shaped object. We adapt the relay session to the
377
+ // `PairSession`-like shape the existing handler signature
378
+ // expects; only the fields completePairing actually reads are
379
+ // populated, the rest default. The mnemonic is the load-bearing
380
+ // field (it writes credentials.json); the session shape carries
381
+ // sid/mode for log correlation.
382
+ void (async () => {
383
+ try {
384
+ const result = await awaitPhraseUpload(session, {
385
+ phraseValidator: validate,
386
+ timeoutMs: cfg.initAwaitTimeoutMs,
387
+ completePairing: async ({ mnemonic }) => {
388
+ // Adapt the relay session to the PairSession-like shape the
389
+ // existing CompletePairingHandler signature expects. Only
390
+ // sid + mode are load-bearing for log correlation; the
391
+ // crypto fields are unused (decryption already happened).
392
+ const sessionLike = {
393
+ sid: session.token,
394
+ mode: session.mode === 'generate' ? 'generate' : 'import',
395
+ };
396
+ return cfg.completePairing({ mnemonic, session: sessionLike });
397
+ },
398
+ });
399
+ if (result.state === 'active') {
400
+ cfg.logger.info(`pair-http /init: session ${redactSid(session.token)} completed in-process; onboarding active`);
401
+ }
402
+ else {
403
+ cfg.logger.warn(`pair-http /init: session ${redactSid(session.token)} completion non-active: ${result.error ?? 'unknown'}`);
404
+ }
405
+ }
406
+ catch (err) {
407
+ const msg = err instanceof Error ? err.message : String(err);
408
+ // Timeouts are expected when the user closes the browser without
409
+ // completing; log at info so a benign expire doesn't look like an
410
+ // error in the gateway log.
411
+ if (msg.includes('timeout') || msg.includes('closed')) {
412
+ cfg.logger.info(`pair-http /init: session ${redactSid(session.token)} expired/closed before completion`);
413
+ }
414
+ else {
415
+ cfg.logger.warn(`pair-http /init: session ${redactSid(session.token)} failed: ${msg}`);
416
+ }
417
+ }
418
+ })();
419
+ }
420
+ const bundle = {
287
421
  finishPath: `${apiBase}/finish`,
288
422
  startPath: `${apiBase}/start`,
289
423
  respondPath: `${apiBase}/respond`,
@@ -295,6 +429,15 @@ export function buildPairRoutes(cfg) {
295
429
  status: handleStatus,
296
430
  },
297
431
  };
432
+ // Only surface the in-process /init route when a relay URL is wired.
433
+ // Older callers that construct the bundle without `relayBaseUrl` get
434
+ // the original four-route shape (back-compat for tests + any external
435
+ // consumers of buildPairRoutes).
436
+ if (cfg.relayBaseUrl) {
437
+ bundle.initPath = `${apiBase}/init`;
438
+ bundle.handlers.init = handleInit;
439
+ }
440
+ return bundle;
298
441
  }
299
442
  // ---------------------------------------------------------------------------
300
443
  // Internals: body reading, response helpers, validation
package/dist/tr-cli.js CHANGED
@@ -51,7 +51,7 @@ const STATE_PATH = CONFIG.onboardingStatePath;
51
51
  // Auto-synced by skill/scripts/sync-version.mjs from skill/plugin/package.json::version.
52
52
  // Do not edit by hand — running tests will catch drift but the publish workflow
53
53
  // rewrites this constant at the start of every npm/ClawHub publish.
54
- const PLUGIN_VERSION = '3.3.12-rc.13';
54
+ const PLUGIN_VERSION = '3.3.12-rc.15';
55
55
  function die(msg, code = 1) {
56
56
  process.stderr.write(`tr: ${msg}\n`);
57
57
  process.exit(code);
package/index.ts CHANGED
@@ -4192,6 +4192,17 @@ const plugin = {
4192
4192
  sessionsPath: CONFIG.pairSessionsPath,
4193
4193
  apiBase: '/plugin/totalreclaw/pair',
4194
4194
  logger: api.logger,
4195
+ // 3.3.14 — wire the relay URL so buildPairRoutes exposes the
4196
+ // in-process `/pair/init` route. The gateway process opens the
4197
+ // relay WebSocket directly (via openRemotePairSession from
4198
+ // pair-remote-client.ts), eliminating the 30s-subprocess-kill
4199
+ // 502 that the CLI path (tr pair) hit when OpenClaw's shell
4200
+ // tool killed the subprocess mid-pair. relayBaseUrl is sourced
4201
+ // from CONFIG.pairRelayUrl (config.ts reads it from the env
4202
+ // once, centrally) — never read from the environment inside
4203
+ // pair-http.ts (scanner-surface rule).
4204
+ relayBaseUrl: CONFIG.pairRelayUrl,
4205
+ initPairMode: 'either',
4195
4206
  validateMnemonic: (p) => validateMnemonic(p, wordlist),
4196
4207
  completePairing: async ({ mnemonic }) => {
4197
4208
  // Write credentials.json + flip state to 'active' via
@@ -4252,7 +4263,19 @@ const plugin = {
4252
4263
  api.registerHttpRoute!({ path: bundle.startPath, handler: bundle.handlers.start, auth: 'plugin' });
4253
4264
  api.registerHttpRoute!({ path: bundle.respondPath, handler: bundle.handlers.respond, auth: 'plugin' });
4254
4265
  api.registerHttpRoute!({ path: bundle.statusPath, handler: bundle.handlers.status, auth: 'plugin' });
4255
- api.logger.info('TotalReclaw: registered 4 QR-pairing HTTP routes synchronously');
4266
+ // 3.3.14 in-process pair trigger. The bundle exposes initPath +
4267
+ // handlers.init ONLY when relayBaseUrl is wired (always true here,
4268
+ // since CONFIG.pairRelayUrl has a built-in default). Registered
4269
+ // with auth: 'plugin' (same as the other pair routes) so the
4270
+ // agent's localhost curl reaches it without a gateway bearer
4271
+ // token. The route opens the relay WS in the gateway process →
4272
+ // survives shell-tool timeouts, retries, SIGUSR1 reloads.
4273
+ if (bundle.initPath && bundle.handlers.init) {
4274
+ api.registerHttpRoute!({ path: bundle.initPath, handler: bundle.handlers.init, auth: 'plugin' });
4275
+ api.logger.info('TotalReclaw: registered 5 QR-pairing HTTP routes synchronously (incl. in-process /pair/init)');
4276
+ } else {
4277
+ api.logger.info('TotalReclaw: registered 4 QR-pairing HTTP routes synchronously (in-process /pair/init not wired — no relay URL)');
4278
+ }
4256
4279
  } else {
4257
4280
  api.logger.warn(
4258
4281
  'api.registerHttpRoute is unavailable on this OpenClaw version — /totalreclaw pair will not work. ' +
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@totalreclaw/totalreclaw",
3
- "version": "3.3.12-rc.13",
3
+ "version": "3.3.12-rc.15",
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": [
@@ -68,7 +68,7 @@
68
68
  "scripts": {
69
69
  "build": "rm -rf dist && tsc -p tsconfig.json --noCheck",
70
70
  "verify-tarball": "node ../scripts/verify-tarball.mjs",
71
- "test": "npx tsx batch-gate.test.ts && npx tsx manifest-shape.test.ts && npx tsx config-schema.test.ts && npx tsx config.test.ts && npx tsx relay-headers.test.ts && npx tsx scope-address-visible.test.ts && npx tsx llm-profile-reader.test.ts && npx tsx llm-client.test.ts && npx tsx llm-client-retry.test.ts && npx tsx llm-client-json-mode.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 && npx tsx onboard-pair-only.test.ts && npx tsx import-state.test.ts && npx tsx import-time-smoke.test.ts && npx tsx install-staging-cleanup.test.ts && npx tsx partial-install-detection.test.ts && npx tsx install-reload-idempotency.test.ts && npx tsx json-stdout-cleanliness.test.ts && npx tsx url-binding.test.ts && npx tsx fs-helpers.test.ts && npx tsx pair-cli-default-mode.test.ts && npx tsx embedding-fallback-tag.test.ts && npx tsx staging-banner-gate.test.ts && npx tsx restart-auth.test.ts && npx tsx inbound-user-tracker.test.ts && npx tsx register-command-name.test.ts && npx tsx skill-md-hybrid-primary.test.ts && npx tsx tr-cli-json-output.test.ts && npx tsx import-upgrade-cli.test.ts && npx tsx postinstall-validate.test.ts && npx tsx credential-provider.test.ts && npx tsx memory-runtime.test.ts && npx tsx tools.test.ts && npx tsx register-native.test.ts && npx tsx relay.test.ts && npx tsx vault-crypto.test.ts && npx tsx entry-env.test.ts && npx tsx trajectory-poller.test.ts",
71
+ "test": "npx tsx batch-gate.test.ts && npx tsx manifest-shape.test.ts && npx tsx config-schema.test.ts && npx tsx config.test.ts && npx tsx relay-headers.test.ts && npx tsx scope-address-visible.test.ts && npx tsx llm-profile-reader.test.ts && npx tsx llm-client.test.ts && npx tsx llm-client-retry.test.ts && npx tsx llm-client-json-mode.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 pair-http.test.ts && npx tsx pair-http-route-registration.test.ts && npx tsx pair-http-init.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 && npx tsx onboard-pair-only.test.ts && npx tsx import-state.test.ts && npx tsx import-time-smoke.test.ts && npx tsx install-staging-cleanup.test.ts && npx tsx partial-install-detection.test.ts && npx tsx install-reload-idempotency.test.ts && npx tsx json-stdout-cleanliness.test.ts && npx tsx url-binding.test.ts && npx tsx fs-helpers.test.ts && npx tsx pair-cli-default-mode.test.ts && npx tsx embedding-fallback-tag.test.ts && npx tsx staging-banner-gate.test.ts && npx tsx restart-auth.test.ts && npx tsx inbound-user-tracker.test.ts && npx tsx register-command-name.test.ts && npx tsx skill-md-native.test.ts &&npx tsx tr-cli-json-output.test.ts && npx tsx import-upgrade-cli.test.ts && npx tsx postinstall-validate.test.ts && npx tsx credential-provider.test.ts && npx tsx memory-runtime.test.ts && npx tsx tools.test.ts && npx tsx register-native.test.ts && npx tsx relay.test.ts && npx tsx vault-crypto.test.ts && npx tsx entry-env.test.ts && npx tsx trajectory-poller.test.ts",
72
72
  "smoke:dist": "npx tsx dist-esm-smoke.test.ts",
73
73
  "check-scanner": "node ../scripts/check-scanner.mjs",
74
74
  "check-version-drift": "node ../scripts/check-version-drift.mjs",
package/pair-http.ts CHANGED
@@ -2,7 +2,23 @@
2
2
  * pair-http — gateway-side HTTP route handlers for the v3.3.0 QR-pairing
3
3
  * flow. Registered via `api.registerHttpRoute` from `index.ts`.
4
4
  *
5
- * Three endpoints (all under /plugin/totalreclaw/pair/):
5
+ * Five endpoints (all under /plugin/totalreclaw/pair/):
6
+ *
7
+ * GET /plugin/totalreclaw/pair/init
8
+ * → IN-PROCESS pair trigger (3.3.14). Opens the relay WebSocket
9
+ * directly in the gateway process via `openRemotePairSession`
10
+ * (from pair-remote-client.ts), returns `{url, pin, sid,
11
+ * expires_at_ms}` immediately so the agent can surface URL+PIN
12
+ * to the user, and starts a BACKGROUND `awaitPhraseUpload` that
13
+ * blocks on the WS for the browser's encrypted phrase, decrypts
14
+ * locally, and invokes the injected `completePairing` callback
15
+ * (writes credentials.json + flips onboarding state). The WS
16
+ * lives in the gateway process — immune to the shell-tool's
17
+ * 30s subprocess timeout that killed the `tr pair` CLI path
18
+ * (relay returned 502 on /pair/respond when the subprocess
19
+ * died). This is the primary agent-facilitated pair path; the
20
+ * CLI `tr pair --json` remains as a fallback for non-agent
21
+ * scenarios.
6
22
  *
7
23
  * GET /plugin/totalreclaw/pair/finish?sid=<sid>
8
24
  * → returns the browser pairing page (HTML + inline JS + CSS).
@@ -37,6 +53,12 @@
37
53
  * - NO environment-variable reads. All config values flow in via
38
54
  * `PairHttpConfig`; callers read from `CONFIG` in `config.ts`.
39
55
  *
56
+ * Adding `openRemotePairSession` (which dials an outbound WebSocket to
57
+ * the relay) keeps this file env=N, net=Y → the env-harvesting rule
58
+ * requires BOTH an env read AND a request trigger in the same file;
59
+ * the relay base URL arrives via `PairHttpConfig.relayBaseUrl` (caller-
60
+ * injected), never read from the environment here.
61
+ *
40
62
  * Logging: NEVER logs the secondary code, the mnemonic, the gateway
41
63
  * private key, or raw request bodies. Session ids and status
42
64
  * transitions are logged at info/warn levels for diagnostics.
@@ -56,6 +78,11 @@ import {
56
78
  } from './pair-session-store.js';
57
79
  import { compareSecondaryCodesCT, decryptPairingPayload } from './pair-crypto.js';
58
80
  import { renderPairPage } from './pair-page.js';
81
+ import {
82
+ awaitPhraseUpload,
83
+ openRemotePairSession,
84
+ type RemotePairSession,
85
+ } from './pair-remote-client.js';
59
86
 
60
87
  // ---------------------------------------------------------------------------
61
88
  // Types
@@ -99,7 +126,7 @@ export type CompletePairingHandler = (inputs: {
99
126
  export interface PairHttpConfig {
100
127
  /** Absolute path to pair-sessions.json. */
101
128
  sessionsPath: string;
102
- /** Pathname prefix the three routes live under. */
129
+ /** Pathname prefix the routes live under. */
103
130
  apiBase: string;
104
131
  /** Writes credentials + flips state. Injected from index.ts. */
105
132
  completePairing: CompletePairingHandler;
@@ -111,6 +138,33 @@ export interface PairHttpConfig {
111
138
  maxBodyBytes?: number;
112
139
  /** If set, override BIP-39 validator for tests. Default does a word-count + wordlist check. */
113
140
  validateMnemonic?: (phrase: string) => boolean;
141
+ /**
142
+ * Relay base URL for the in-process `/pair/init` route (3.3.14). When
143
+ * set, `buildPairRoutes` exposes an `init` handler that opens the relay
144
+ * WebSocket directly in the gateway process (via `openRemotePairSession`).
145
+ * When omitted, the `init` handler is omitted from the bundle — older
146
+ * callers that haven't been updated still get the original 4 routes.
147
+ * Caller sources this from `CONFIG.pairRelayUrl`; NEVER read from the
148
+ * environment here (scanner-surface rule).
149
+ */
150
+ relayBaseUrl?: string;
151
+ /**
152
+ * Pair mode advertised in the relay open-frame for `/pair/init`.
153
+ * Defaults to 'either' (the relay will render both generate + import
154
+ * panels). Callers can pin 'generate' or 'import'.
155
+ */
156
+ initPairMode?: 'generate' | 'import' | 'either';
157
+ /**
158
+ * Override the WebSocket constructor used by `openRemotePairSession`
159
+ * for the `/pair/init` route. Tests inject a stub; production leaves
160
+ * this unset so the real `ws` client is used.
161
+ */
162
+ initWebSocketImpl?: typeof import('ws').WebSocket;
163
+ /**
164
+ * Override the forward-frame await timeout (ms) for the in-process
165
+ * background await. Defaults to the 5-minute relay TTL.
166
+ */
167
+ initAwaitTimeoutMs?: number;
114
168
  }
115
169
 
116
170
  /**
@@ -133,23 +187,32 @@ interface PairRespondBody {
133
187
  * Shape returned so the plugin wiring can invoke each handler directly.
134
188
  * Callers normally pass each one to `api.registerHttpRoute` — but we
135
189
  * also expose them in an object for tests.
190
+ *
191
+ * `initPath` / `handlers.init` are present ONLY when `PairHttpConfig`
192
+ * supplied a `relayBaseUrl` (3.3.14 in-process pair route). Older
193
+ * callers that omit it get back the original four-route bundle.
136
194
  */
137
195
  export interface PairRouteBundle {
138
196
  finishPath: string;
139
197
  startPath: string;
140
198
  respondPath: string;
141
199
  statusPath: string;
200
+ /** Present only when `cfg.relayBaseUrl` is set (in-process pair route). */
201
+ initPath?: string;
142
202
  handlers: {
143
203
  finish: (req: IncomingMessage, res: ServerResponse) => Promise<void>;
144
204
  start: (req: IncomingMessage, res: ServerResponse) => Promise<void>;
145
205
  respond: (req: IncomingMessage, res: ServerResponse) => Promise<void>;
146
206
  status: (req: IncomingMessage, res: ServerResponse) => Promise<void>;
207
+ /** Present only when `cfg.relayBaseUrl` is set (in-process pair route). */
208
+ init?: (req: IncomingMessage, res: ServerResponse) => Promise<void>;
147
209
  };
148
210
  }
149
211
 
150
212
  /**
151
- * Build the four handlers. The caller registers each with
152
- * `api.registerHttpRoute({ path, handler })`.
213
+ * Build the route handlers. The caller registers each with
214
+ * `api.registerHttpRoute({ path, handler })`. When `cfg.relayBaseUrl` is
215
+ * set, the bundle also carries the in-process `/pair/init` handler.
153
216
  */
154
217
  export function buildPairRoutes(cfg: PairHttpConfig): PairRouteBundle {
155
218
  const apiBase = cfg.apiBase.replace(/\/+$/, '');
@@ -402,7 +465,124 @@ export function buildPairRoutes(cfg: PairHttpConfig): PairRouteBundle {
402
465
  });
403
466
  }
404
467
 
405
- return {
468
+ // ---------------------------------------------------------------
469
+ // 3.3.14 — In-process pair trigger (the 30s-subprocess-kill 502 fix)
470
+ // ---------------------------------------------------------------
471
+ //
472
+ // Defined unconditionally so the handler identity is stable for the
473
+ // lifetime of the bundle; it short-circuits with 503 when no relay
474
+ // URL was wired. Only attached to the returned bundle + registered
475
+ // as a route when `cfg.relayBaseUrl` is set (see the return below).
476
+ async function handleInit(req: IncomingMessage, res: ServerResponse): Promise<void> {
477
+ if (!methodAllowed(req, ['GET'])) {
478
+ sendJson(res, 405, { error: 'method_not_allowed' });
479
+ return;
480
+ }
481
+ const relay = cfg.relayBaseUrl;
482
+ if (!relay) {
483
+ // Caller did not wire a relay URL — the in-process route is inert.
484
+ sendJson(res, 503, { error: 'init_not_configured' });
485
+ return;
486
+ }
487
+
488
+ // 1. Open the relay WebSocket IN-PROCESS (the gateway process owns the
489
+ // socket). This is the fix: the WS is no longer held by a CLI
490
+ // subprocess, so OpenClaw's 30s shell-tool timeout cannot kill it
491
+ // and the relay never sees a mid-pair disconnect → no more 502 on
492
+ // /pair/respond.
493
+ let session: RemotePairSession;
494
+ try {
495
+ const openOpts: Parameters<typeof openRemotePairSession>[0] = {
496
+ relayBaseUrl: relay,
497
+ mode: cfg.initPairMode ?? 'either',
498
+ };
499
+ if (cfg.initWebSocketImpl) {
500
+ openOpts.webSocketImpl = cfg.initWebSocketImpl;
501
+ }
502
+ session = await openRemotePairSession(openOpts);
503
+ } catch (err) {
504
+ const msg = err instanceof Error ? err.message : String(err);
505
+ cfg.logger.warn(`pair-http /init: relay session open failed: ${msg}`);
506
+ sendJson(res, 502, { error: 'relay_open_failed', detail: msg });
507
+ return;
508
+ }
509
+
510
+ const parsedExpiresMs = Date.parse(session.expiresAt);
511
+ const expiresAtMs = Number.isFinite(parsedExpiresMs)
512
+ ? parsedExpiresMs
513
+ : now() + 5 * 60_000;
514
+
515
+ // 2. Respond IMMEDIATELY with the URL + PIN + sid (relay token) +
516
+ // expiry. The agent reads this and surfaces the URL+PIN to the
517
+ // user. The relay token here plays the `sid` role for agent-side
518
+ // correlation (parity with the CLI JSON payload shape).
519
+ sendJson(res, 200, {
520
+ v: 1,
521
+ sid: session.token,
522
+ url: session.url,
523
+ pin: session.pin,
524
+ mode: session.mode,
525
+ expires_at_ms: expiresAtMs,
526
+ });
527
+
528
+ // 3. Start the background wait IN THE GATEWAY PROCESS. We do NOT
529
+ // await this from the request handler — the HTTP response has
530
+ // already been sent. The promise resolves when the browser
531
+ // uploads the encrypted phrase (the relay pushes a `forward`
532
+ // frame), the gateway decrypts locally, and `completePairing`
533
+ // writes credentials.json + flips onboarding state. Errors are
534
+ // logged but never reach the HTTP response (it's already gone).
535
+ //
536
+ // The injected `completePairing` callback receives the mnemonic
537
+ // + a session-shaped object. We adapt the relay session to the
538
+ // `PairSession`-like shape the existing handler signature
539
+ // expects; only the fields completePairing actually reads are
540
+ // populated, the rest default. The mnemonic is the load-bearing
541
+ // field (it writes credentials.json); the session shape carries
542
+ // sid/mode for log correlation.
543
+ void (async (): Promise<void> => {
544
+ try {
545
+ const result = await awaitPhraseUpload(session, {
546
+ phraseValidator: validate,
547
+ timeoutMs: cfg.initAwaitTimeoutMs,
548
+ completePairing: async ({ mnemonic }) => {
549
+ // Adapt the relay session to the PairSession-like shape the
550
+ // existing CompletePairingHandler signature expects. Only
551
+ // sid + mode are load-bearing for log correlation; the
552
+ // crypto fields are unused (decryption already happened).
553
+ const sessionLike = {
554
+ sid: session.token,
555
+ mode: session.mode === 'generate' ? 'generate' : 'import',
556
+ } as PairSession;
557
+ return cfg.completePairing({ mnemonic, session: sessionLike });
558
+ },
559
+ });
560
+ if (result.state === 'active') {
561
+ cfg.logger.info(
562
+ `pair-http /init: session ${redactSid(session.token)} completed in-process; onboarding active`,
563
+ );
564
+ } else {
565
+ cfg.logger.warn(
566
+ `pair-http /init: session ${redactSid(session.token)} completion non-active: ${result.error ?? 'unknown'}`,
567
+ );
568
+ }
569
+ } catch (err) {
570
+ const msg = err instanceof Error ? err.message : String(err);
571
+ // Timeouts are expected when the user closes the browser without
572
+ // completing; log at info so a benign expire doesn't look like an
573
+ // error in the gateway log.
574
+ if (msg.includes('timeout') || msg.includes('closed')) {
575
+ cfg.logger.info(
576
+ `pair-http /init: session ${redactSid(session.token)} expired/closed before completion`,
577
+ );
578
+ } else {
579
+ cfg.logger.warn(`pair-http /init: session ${redactSid(session.token)} failed: ${msg}`);
580
+ }
581
+ }
582
+ })();
583
+ }
584
+
585
+ const bundle: PairRouteBundle = {
406
586
  finishPath: `${apiBase}/finish`,
407
587
  startPath: `${apiBase}/start`,
408
588
  respondPath: `${apiBase}/respond`,
@@ -414,6 +594,15 @@ export function buildPairRoutes(cfg: PairHttpConfig): PairRouteBundle {
414
594
  status: handleStatus,
415
595
  },
416
596
  };
597
+ // Only surface the in-process /init route when a relay URL is wired.
598
+ // Older callers that construct the bundle without `relayBaseUrl` get
599
+ // the original four-route shape (back-compat for tests + any external
600
+ // consumers of buildPairRoutes).
601
+ if (cfg.relayBaseUrl) {
602
+ bundle.initPath = `${apiBase}/init`;
603
+ bundle.handlers.init = handleInit;
604
+ }
605
+ return bundle;
417
606
  }
418
607
 
419
608
  // ---------------------------------------------------------------------------
package/skill.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "totalreclaw",
3
- "version": "3.3.12-rc.13",
3
+ "version": "3.3.12-rc.15",
4
4
  "description": "End-to-end encrypted memory for AI agents — portable, yours forever. XChaCha20-Poly1305 E2EE: server never sees plaintext.",
5
5
  "author": "TotalReclaw Team",
6
6
  "license": "MIT",
package/tr-cli.ts CHANGED
@@ -68,7 +68,7 @@ const STATE_PATH = CONFIG.onboardingStatePath;
68
68
  // Auto-synced by skill/scripts/sync-version.mjs from skill/plugin/package.json::version.
69
69
  // Do not edit by hand — running tests will catch drift but the publish workflow
70
70
  // rewrites this constant at the start of every npm/ClawHub publish.
71
- const PLUGIN_VERSION = '3.3.12-rc.13';
71
+ const PLUGIN_VERSION = '3.3.12-rc.15';
72
72
 
73
73
  function die(msg: string, code = 1): never {
74
74
  process.stderr.write(`tr: ${msg}\n`);