@totalreclaw/totalreclaw 3.3.12 → 3.3.13-rc.1

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 (128) hide show
  1. package/SKILL.md +25 -8
  2. package/{billing-cache.ts → billing/billing-cache.ts} +1 -1
  3. package/{relay-headers.ts → billing/relay-headers.ts} +1 -1
  4. package/{tr-cli-export-helper.ts → cli/tr-cli-export-helper.ts} +5 -5
  5. package/{tr-cli.ts → cli/tr-cli.ts} +20 -20
  6. package/{contradiction-sync.ts → contradiction/contradiction-sync.ts} +13 -13
  7. package/{digest-sync.ts → digest/digest-sync.ts} +32 -2
  8. package/dist/{billing-cache.js → billing/billing-cache.js} +1 -1
  9. package/dist/{relay-headers.js → billing/relay-headers.js} +1 -1
  10. package/dist/{tr-cli-export-helper.js → cli/tr-cli-export-helper.js} +5 -5
  11. package/dist/{tr-cli.js → cli/tr-cli.js} +19 -19
  12. package/dist/{contradiction-sync.js → contradiction/contradiction-sync.js} +13 -13
  13. package/dist/{digest-sync.js → digest/digest-sync.js} +32 -2
  14. package/dist/{download-ux.js → embedding/download-ux.js} +1 -1
  15. package/dist/{reranker.js → embedding/reranker.js} +13 -4
  16. package/dist/{claims-helper.js → extraction/claims-helper.js} +54 -58
  17. package/dist/extraction/consolidation.js +170 -0
  18. package/dist/{extractor.js → extraction/extractor.js} +17 -17
  19. package/dist/extraction/importance-filter.js +38 -0
  20. package/dist/{semantic-dedup.js → extraction/semantic-dedup.js} +2 -2
  21. package/dist/{batch-gate.js → import/batch-gate.js} +1 -1
  22. package/dist/import/import-runtime.js +692 -0
  23. package/dist/import-adapters/chatgpt-adapter.js +7 -7
  24. package/dist/import-adapters/gemini-adapter.js +13 -13
  25. package/dist/import-adapters/mem0-adapter.js +10 -10
  26. package/dist/index.js +111 -1094
  27. package/dist/{llm-client.js → llm/llm-client.js} +2 -2
  28. package/dist/{hot-cache-wrapper.js → memory/hot-cache-wrapper.js} +5 -5
  29. package/dist/{memory-runtime.js → memory/memory-runtime.js} +1 -1
  30. package/dist/{pin.js → memory/pin.js} +18 -18
  31. package/dist/{retype-setscope.js → memory/retype-setscope.js} +19 -19
  32. package/dist/{credential-provider.js → pairing/credential-provider.js} +2 -2
  33. package/dist/{first-run.js → pairing/first-run.js} +1 -1
  34. package/dist/{onboarding-cli.js → pairing/onboarding-cli.js} +20 -20
  35. package/dist/{pair-cli-relay.js → pairing/pair-cli-relay.js} +10 -10
  36. package/dist/{pair-crypto.js → pairing/pair-crypto.js} +1 -1
  37. package/dist/{pair-http.js → pairing/pair-http.js} +3 -3
  38. package/dist/{pair-remote-client.js → pairing/pair-remote-client.js} +10 -10
  39. package/dist/{pair-session-store.js → pairing/pair-session-store.js} +4 -4
  40. package/dist/runtime/config-schema.js +59 -0
  41. package/dist/runtime/format-helpers.js +257 -0
  42. package/dist/runtime/types.js +11 -0
  43. package/dist/{confirm-indexed.js → subgraph/confirm-indexed.js} +1 -1
  44. package/dist/{subgraph-search.js → subgraph/subgraph-search.js} +14 -14
  45. package/dist/{subgraph-store.js → subgraph/subgraph-store.js} +6 -6
  46. package/{download-ux.ts → embedding/download-ux.ts} +1 -1
  47. package/{reranker.ts → embedding/reranker.ts} +13 -4
  48. package/{claims-helper.ts → extraction/claims-helper.ts} +52 -60
  49. package/{consolidation.ts → extraction/consolidation.ts} +63 -154
  50. package/{extractor.ts → extraction/extractor.ts} +18 -17
  51. package/extraction/importance-filter.ts +50 -0
  52. package/{semantic-dedup.ts → extraction/semantic-dedup.ts} +2 -2
  53. package/{batch-gate.ts → import/batch-gate.ts} +1 -1
  54. package/import/import-runtime.ts +853 -0
  55. package/import-adapters/chatgpt-adapter.ts +7 -7
  56. package/import-adapters/gemini-adapter.ts +13 -13
  57. package/import-adapters/mem0-adapter.ts +10 -10
  58. package/index.ts +129 -1377
  59. package/{llm-client.ts → llm/llm-client.ts} +2 -2
  60. package/{hot-cache-wrapper.ts → memory/hot-cache-wrapper.ts} +5 -5
  61. package/{memory-runtime.ts → memory/memory-runtime.ts} +1 -1
  62. package/{pin.ts → memory/pin.ts} +20 -20
  63. package/{retype-setscope.ts → memory/retype-setscope.ts} +21 -21
  64. package/{tool-gating.ts → memory/tool-gating.ts} +1 -1
  65. package/package.json +6 -6
  66. package/{credential-provider.ts → pairing/credential-provider.ts} +2 -2
  67. package/{first-run.ts → pairing/first-run.ts} +1 -1
  68. package/{onboarding-cli.ts → pairing/onboarding-cli.ts} +20 -20
  69. package/{pair-cli-relay.ts → pairing/pair-cli-relay.ts} +10 -10
  70. package/{pair-crypto.ts → pairing/pair-crypto.ts} +1 -1
  71. package/{pair-http.ts → pairing/pair-http.ts} +3 -3
  72. package/{pair-remote-client.ts → pairing/pair-remote-client.ts} +10 -10
  73. package/{pair-session-store.ts → pairing/pair-session-store.ts} +4 -4
  74. package/runtime/config-schema.ts +64 -0
  75. package/runtime/format-helpers.ts +293 -0
  76. package/runtime/types.ts +118 -0
  77. package/skill.json +44 -10
  78. package/{confirm-indexed.ts → subgraph/confirm-indexed.ts} +1 -1
  79. package/{subgraph-search.ts → subgraph/subgraph-search.ts} +14 -14
  80. package/{subgraph-store.ts → subgraph/subgraph-store.ts} +10 -10
  81. package/dist/consolidation.js +0 -249
  82. /package/{api-client.ts → billing/api-client.ts} +0 -0
  83. /package/{gateway-url.ts → billing/gateway-url.ts} +0 -0
  84. /package/{inbound-user-tracker.ts → billing/inbound-user-tracker.ts} +0 -0
  85. /package/{relay.ts → billing/relay.ts} +0 -0
  86. /package/{crypto.ts → crypto/crypto.ts} +0 -0
  87. /package/{vault-crypto.ts → crypto/vault-crypto.ts} +0 -0
  88. /package/dist/{api-client.js → billing/api-client.js} +0 -0
  89. /package/dist/{gateway-url.js → billing/gateway-url.js} +0 -0
  90. /package/dist/{inbound-user-tracker.js → billing/inbound-user-tracker.js} +0 -0
  91. /package/dist/{relay.js → billing/relay.js} +0 -0
  92. /package/dist/{crypto.js → crypto/crypto.js} +0 -0
  93. /package/dist/{vault-crypto.js → crypto/vault-crypto.js} +0 -0
  94. /package/dist/{embedder-cache.js → embedding/embedder-cache.js} +0 -0
  95. /package/dist/{embedder-loader.js → embedding/embedder-loader.js} +0 -0
  96. /package/dist/{embedder-network.js → embedding/embedder-network.js} +0 -0
  97. /package/dist/{embedding.js → embedding/embedding.js} +0 -0
  98. /package/dist/{lsh.js → embedding/lsh.js} +0 -0
  99. /package/dist/{import-state-manager.js → import/import-state-manager.js} +0 -0
  100. /package/dist/{llm-profile-reader.js → llm/llm-profile-reader.js} +0 -0
  101. /package/dist/{native-memory.js → memory/native-memory.js} +0 -0
  102. /package/dist/{tool-gating.js → memory/tool-gating.js} +0 -0
  103. /package/dist/{tools.js → memory/tools.js} +0 -0
  104. /package/dist/{generate-mnemonic.js → pairing/generate-mnemonic.js} +0 -0
  105. /package/dist/{pair-cli.js → pairing/pair-cli.js} +0 -0
  106. /package/dist/{pair-page.js → pairing/pair-page.js} +0 -0
  107. /package/dist/{pair-qr.js → pairing/pair-qr.js} +0 -0
  108. /package/dist/{restart-auth.js → pairing/restart-auth.js} +0 -0
  109. /package/dist/{qa-bug-report.js → setup/qa-bug-report.js} +0 -0
  110. /package/dist/{skill-register.js → setup/skill-register.js} +0 -0
  111. /package/dist/{trajectory-poller.js → subgraph/trajectory-poller.js} +0 -0
  112. /package/{embedder-cache.ts → embedding/embedder-cache.ts} +0 -0
  113. /package/{embedder-loader.ts → embedding/embedder-loader.ts} +0 -0
  114. /package/{embedder-network.ts → embedding/embedder-network.ts} +0 -0
  115. /package/{embedding.ts → embedding/embedding.ts} +0 -0
  116. /package/{lsh.ts → embedding/lsh.ts} +0 -0
  117. /package/{import-state-manager.ts → import/import-state-manager.ts} +0 -0
  118. /package/{llm-profile-reader.ts → llm/llm-profile-reader.ts} +0 -0
  119. /package/{native-memory.ts → memory/native-memory.ts} +0 -0
  120. /package/{tools.ts → memory/tools.ts} +0 -0
  121. /package/{generate-mnemonic.ts → pairing/generate-mnemonic.ts} +0 -0
  122. /package/{pair-cli.ts → pairing/pair-cli.ts} +0 -0
  123. /package/{pair-page.ts → pairing/pair-page.ts} +0 -0
  124. /package/{pair-qr.ts → pairing/pair-qr.ts} +0 -0
  125. /package/{restart-auth.ts → pairing/restart-auth.ts} +0 -0
  126. /package/{qa-bug-report.ts → setup/qa-bug-report.ts} +0 -0
  127. /package/{skill-register.ts → setup/skill-register.ts} +0 -0
  128. /package/{trajectory-poller.ts → subgraph/trajectory-poller.ts} +0 -0
package/SKILL.md CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: totalreclaw
3
3
  description: "End-to-end encrypted, decentralized memory for OpenClaw. A native kind:memory provider — recall is automatic via memory_search/memory_get, and facts are captured in the background. Trigger on 'install TotalReclaw', 'set up TotalReclaw', 'restore my recovery phrase', any recall request ('what do you remember about me', 'what's my X'), AND any explicit remember request ('remember X', 'save X')."
4
- version: 3.3.12-rc.23
4
+ version: 3.3.13-rc.1
5
5
  author: TotalReclaw Team
6
6
  license: MIT
7
7
  homepage: https://totalreclaw.xyz
@@ -45,24 +45,41 @@ The 12-word recovery phrase MUST NEVER enter chat or the agent's LLM context. Th
45
45
  ```
46
46
  openclaw plugins install @totalreclaw/totalreclaw
47
47
  openclaw skills install totalreclaw
48
+ openclaw plugins enable totalreclaw
48
49
  ```
49
50
 
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.
51
+ Published on npm + ClawHub (`p-diogo`, GitHub-verified). For a release candidate, pin with `@rc`; stable resolves from `latest`. Run `openclaw plugins enable totalreclaw` right after install, always — it binds the memory slot. On a clean install that's a no-op; on a **reinstall over a previous version** it's what stops the plugin from landing `Status: disabled` with the slot stuck at `memory-core` (a disabled plugin never registers its memory tools or pair routes, so pairing later fails with a 502). The plugin self-applies the rest of the gateway config it needs on load and auto-reloads (see Restart below) — no manual gateway restart.
52
+
53
+ **Reinstall recovery.** If install reports `already exists` / `delete it first`, do **not** add `--force` (it writes a broken `version=None` record). Clean the leftover package dir and reinstall — this never touches your credentials, which live under `~/.totalreclaw/`:
54
+
55
+ ```
56
+ openclaw plugins uninstall totalreclaw
57
+ rm -rf ~/.openclaw/npm/projects/*totalreclaw-totalreclaw*
58
+ openclaw plugins install @totalreclaw/totalreclaw
59
+ openclaw plugins enable totalreclaw
60
+ ```
51
61
 
52
62
  ## Setup (pair — user-initiated QR)
53
63
 
54
- Pairing is a deliberate, user-initiated QR flow (the plugin does **not** auto-pair on load). When credentials don't exist:
64
+ Pairing is a deliberate, user-initiated flow (the plugin does **not** auto-pair on load). When credentials don't exist:
65
+
66
+ 1. Mint a pair session via the plugin's **in-process HTTP route** — the gateway process owns the relay pair WebSocket, so it survives until the user completes:
67
+
68
+ ```bash
69
+ curl -s http://localhost:18789/plugin/totalreclaw/pair/init
70
+ ```
71
+
72
+ → returns `{"url":"https://…/pair/p/<id>#pk=…","pin":"123456","expires_at_ms":…}`.
55
73
 
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.
74
+ **Do NOT use `tr pair --json` for this.** The CLI holds the pair WebSocket in a subprocess that OpenClaw's ~30s shell-tool timeout kills — the WS then drops, and when the user submits their phrase the relay returns a **502** (`gateway_disconnected`). The in-process route above avoids that entirely. Only fall back to the CLI if the route is unreachable (no `curl`, or a pre-3.3.14 plugin), and then it MUST be detached so the WS outlives the shell exec: `setsid -f node "$TR_CLI" pair --json < /dev/null` — never run `tr pair` in the foreground.
75
+ 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." Emit the URL as plain text on its own line — the `#pk=` fragment breaks if wrapped in backticks/markdown.
76
+ 3. The user completes in the browser (phrase is generated/imported browser-side, encrypted, uploaded to the relay — never touches this chat). On completion `~/.totalreclaw/credentials.json` is written.
59
77
  4. Confirm: `✓ TotalReclaw set up.` (If credentials already exist when first asked: `✓ TotalReclaw is already set up.`)
60
78
 
61
79
  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.
62
80
 
63
81
  ```bash
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)"
65
- node "$TR_CLI" pair --json # start/re-pair
82
+ TR_CLI="$(ls -d ~/.openclaw/npm/node_modules/@totalreclaw/totalreclaw/dist/cli/tr-cli.js ~/.openclaw/extensions/totalreclaw/dist/cli/tr-cli.js 2>/dev/null | head -1)"
66
83
  node "$TR_CLI" status --json # {"version":"…","paired":true,…}
67
84
  ```
68
85
 
@@ -27,7 +27,7 @@
27
27
 
28
28
  import fs from 'node:fs';
29
29
  import path from 'node:path';
30
- import { CONFIG, setChainIdOverride, setDataEdgeAddressOverride } from './config.js';
30
+ import { CONFIG, setChainIdOverride, setDataEdgeAddressOverride } from '../config.js';
31
31
 
32
32
  /** A plausible EVM address — anything else from billing is ignored (#460). */
33
33
  const DATA_EDGE_ADDRESS_RE = /^0x[0-9a-fA-F]{40}$/;
@@ -18,7 +18,7 @@
18
18
  * the variable.
19
19
  */
20
20
 
21
- import { getSessionId } from './config.js';
21
+ import { getSessionId } from '../config.js';
22
22
 
23
23
  /** Default `X-TotalReclaw-Client` value. */
24
24
  export const DEFAULT_CLIENT_ID = 'openclaw-plugin';
@@ -13,9 +13,9 @@
13
13
  * pre-derived auth-key + wallet-address + encryption-key from the caller.
14
14
  */
15
15
 
16
- import { CONFIG } from './config.js';
17
- import { buildRelayHeaders } from './relay-headers.js';
18
- import { decrypt } from './crypto.js';
16
+ import { CONFIG } from '../config.js';
17
+ import { buildRelayHeaders } from '../billing/relay-headers.js';
18
+ import { decrypt } from '../crypto/crypto.js';
19
19
 
20
20
  /** Decode a hex blob written by submitFactBatchOnChain back to plaintext. */
21
21
  function fromHexBlob(hexBlob: string, encryptionKey: Buffer): string {
@@ -104,8 +104,8 @@ export async function exportAllFacts(
104
104
  ? { owner: walletAddress, first: PAGE_SIZE, lastId }
105
105
  : { owner: walletAddress, first: PAGE_SIZE };
106
106
 
107
- const data = await gql<{ facts?: FactRow[] }>(query, variables);
108
- const facts = data?.facts ?? [];
107
+ const subgraphResponse = await gql<{ facts?: FactRow[] }>(query, variables);
108
+ const facts = subgraphResponse?.facts ?? [];
109
109
  if (facts.length === 0) break;
110
110
 
111
111
  for (const f of facts) {
@@ -31,23 +31,23 @@
31
31
  * --json flag: all agent-facing CLI calls MUST use --json for clean machine-parseable output.
32
32
  * Plain text mode is for direct user CLI use only.
33
33
  *
34
- * Install: wired via package.json `bin.tr` → dist/tr-cli.js
35
- * Usage from container: `docker exec tr-openclaw node ~/.openclaw/extensions/totalreclaw/dist/tr-cli.js status --json`
34
+ * Install: wired via package.json `bin.tr` → dist/cli/tr-cli.js
35
+ * Usage from container: `docker exec tr-openclaw node ~/.openclaw/extensions/totalreclaw/dist/cli/tr-cli.js status --json`
36
36
  */
37
37
 
38
38
  import { randomUUID } from 'node:crypto';
39
39
 
40
- import { CONFIG, setRecoveryPhraseOverride } from './config.js';
41
- import { loadCredentialsJson } from './fs-helpers.js';
42
- import { printStatus } from './onboarding-cli.js';
40
+ import { CONFIG, setRecoveryPhraseOverride } from '../config.js';
41
+ import { loadCredentialsJson } from '../fs-helpers.js';
42
+ import { printStatus } from '../pairing/onboarding-cli.js';
43
43
  import {
44
44
  deriveKeys,
45
45
  computeAuthKeyHash,
46
46
  encrypt,
47
47
  generateBlindIndices,
48
48
  generateContentFingerprint,
49
- } from './crypto.js';
50
- import { createApiClient } from './api-client.js';
49
+ } from '../crypto/crypto.js';
50
+ import { createApiClient } from '../billing/api-client.js';
51
51
  import {
52
52
  encodeFactProtobuf,
53
53
  submitFactBatchOnChain,
@@ -55,7 +55,7 @@ import {
55
55
  getSubgraphConfig,
56
56
  PROTOBUF_VERSION_V4,
57
57
  type FactPayload,
58
- } from './subgraph-store.js';
58
+ } from '../subgraph/subgraph-store.js';
59
59
  import { exportAllFacts } from './tr-cli-export-helper.js';
60
60
 
61
61
  // ---------------------------------------------------------------------------
@@ -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.23';
71
+ const PLUGIN_VERSION = '3.3.13-rc.1';
72
72
 
73
73
  function die(msg: string, code = 1): never {
74
74
  process.stderr.write(`tr: ${msg}\n`);
@@ -124,7 +124,7 @@ interface CliContext {
124
124
  async function buildContext(): Promise<CliContext> {
125
125
  const creds = loadCredentialsJson(CREDENTIALS_PATH);
126
126
  if (!creds) {
127
- die('TotalReclaw is not set up. Run: node ~/.openclaw/extensions/totalreclaw/dist/tr-cli.js pair --json');
127
+ die('TotalReclaw is not set up. Run: node ~/.openclaw/extensions/totalreclaw/dist/cli/tr-cli.js pair --json');
128
128
  }
129
129
 
130
130
  const mnemonic =
@@ -244,8 +244,8 @@ async function cmdPair(args: string[]): Promise<void> {
244
244
  // Phrase-safety: pair-cli-relay.ts is x25519-only; mnemonic never appears.
245
245
  const outputMode = args.includes('--json') ? 'json' : args.includes('--url-pin') ? 'url-pin' : 'human';
246
246
 
247
- const { runRelayPairCli } = await import('./pair-cli-relay.js');
248
- const { defaultRenderQr, buildDefaultPairCliIo } = await import('./pair-cli.js');
247
+ const { runRelayPairCli } = await import('../pairing/pair-cli-relay.js');
248
+ const { defaultRenderQr, buildDefaultPairCliIo } = await import('../pairing/pair-cli.js');
249
249
 
250
250
  const io = buildDefaultPairCliIo();
251
251
  const outcome = await runRelayPairCli('generate', {
@@ -261,7 +261,7 @@ async function cmdPair(args: string[]): Promise<void> {
261
261
  deriveScopeAddress: undefined,
262
262
  renderQr: defaultRenderQr,
263
263
  io,
264
- outputMode: outputMode as import('./pair-cli.js').PairCliOutputMode,
264
+ outputMode: outputMode as import('../pairing/pair-cli.js').PairCliOutputMode,
265
265
  });
266
266
 
267
267
  if (outcome.status !== 'completed' && outcome.status !== 'canceled') {
@@ -333,12 +333,12 @@ async function cmdRemember(rawArgs: string[]): Promise<void> {
333
333
  authKeyHex: ctx.authKeyHex,
334
334
  walletAddress: ctx.walletAddress,
335
335
  };
336
- const result = await submitFactBatchOnChain([protobuf], config);
336
+ const submitResult = await submitFactBatchOnChain([protobuf], config);
337
337
 
338
- if (!result.success) {
338
+ if (!submitResult.success) {
339
339
  die(
340
340
  `remember failed: on-chain UserOp did not succeed (userOpHash=${
341
- result.userOpHash || 'none'
341
+ submitResult.userOpHash || 'none'
342
342
  })`,
343
343
  );
344
344
  }
@@ -404,18 +404,18 @@ async function cmdForget(rawArgs: string[]): Promise<void> {
404
404
  authKeyHex: ctx.authKeyHex,
405
405
  walletAddress: ctx.walletAddress,
406
406
  };
407
- const result = await submitFactBatchOnChain([protobuf], config);
407
+ const submitResult = await submitFactBatchOnChain([protobuf], config);
408
408
 
409
- if (!result.success) {
409
+ if (!submitResult.success) {
410
410
  die(
411
411
  `forget failed: on-chain tombstone did not succeed (userOpHash=${
412
- result.userOpHash || 'none'
412
+ submitResult.userOpHash || 'none'
413
413
  })`,
414
414
  );
415
415
  }
416
416
 
417
417
  if (jsonMode) {
418
- log(JSON.stringify({ ok: true, id: factId, tx_hash: result.txHash }));
418
+ log(JSON.stringify({ ok: true, id: factId, tx_hash: submitResult.txHash }));
419
419
  } else {
420
420
  log(`ok — tombstoned ${factId} (tx=${result.txHash || 'pending'})`);
421
421
  }
@@ -24,12 +24,12 @@ import {
24
24
  envNumber,
25
25
  envString,
26
26
  envStringLower,
27
- } from './entry.js';
27
+ } from '../entry.js';
28
28
  import {
29
29
  computeEntityTrapdoor,
30
30
  isDigestBlob,
31
31
  type AutoResolveMode,
32
- } from './claims-helper.js';
32
+ } from '../extraction/claims-helper.js';
33
33
 
34
34
  const requireWasm = createRequire(import.meta.url);
35
35
  let _wasm: typeof import('@totalreclaw/core') | null = null;
@@ -306,17 +306,17 @@ export interface CandidateClaim {
306
306
  */
307
307
  export function parseCandidateClaim(decryptedJson: string): CanonicalClaim | null {
308
308
  if (isDigestBlob(decryptedJson)) return null;
309
- let obj: Record<string, unknown>;
309
+ let parsed: Record<string, unknown>;
310
310
  try {
311
- obj = JSON.parse(decryptedJson) as Record<string, unknown>;
311
+ parsed = JSON.parse(decryptedJson) as Record<string, unknown>;
312
312
  } catch {
313
313
  return null;
314
314
  }
315
- if (typeof obj.t !== 'string' || typeof obj.c !== 'string') return null;
315
+ if (typeof parsed.t !== 'string' || typeof parsed.c !== 'string') return null;
316
316
  // Filter out infra categories — digests and entity rollup claims are not
317
317
  // user-facing knowledge and must never be considered contradictions.
318
- if (obj.c === 'dig' || obj.c === 'ent') return null;
319
- return obj as CanonicalClaim;
318
+ if (parsed.c === 'dig' || parsed.c === 'ent') return null;
319
+ return parsed as CanonicalClaim;
320
320
  }
321
321
 
322
322
  /** Is this candidate claim pinned (status `p`)? Delegates to WASM core. */
@@ -1052,9 +1052,9 @@ export function findDecisionForPin(
1052
1052
  ): DecisionLogEntry | null {
1053
1053
  if (!logContent || logContent.length === 0) return null;
1054
1054
  try {
1055
- const result = getWasm().findDecisionForPin(factId, role, logContent);
1056
- if (result === 'null') return null;
1057
- return JSON.parse(result) as DecisionLogEntry;
1055
+ const decisionJson = getWasm().findDecisionForPin(factId, role, logContent);
1056
+ if (decisionJson === 'null') return null;
1057
+ return JSON.parse(decisionJson) as DecisionLogEntry;
1058
1058
  } catch {
1059
1059
  // Fallback: local implementation if WASM fails.
1060
1060
  const lines = logContent.split('\n').filter((l) => l.length > 0);
@@ -1141,13 +1141,13 @@ export function buildFeedbackFromDecision(
1141
1141
  ): FeedbackEntry | null {
1142
1142
  if (!decision.winner_components || !decision.loser_components) return null;
1143
1143
  try {
1144
- const result = getWasm().buildFeedbackFromDecision(
1144
+ const feedbackJson = getWasm().buildFeedbackFromDecision(
1145
1145
  JSON.stringify(decision),
1146
1146
  action,
1147
1147
  Math.floor(nowUnixSeconds),
1148
1148
  );
1149
- if (result === 'null') return null;
1150
- return JSON.parse(result) as FeedbackEntry;
1149
+ if (feedbackJson === 'null') return null;
1150
+ return JSON.parse(feedbackJson) as FeedbackEntry;
1151
1151
  } catch {
1152
1152
  // Fallback: local implementation if WASM fails.
1153
1153
  if (action === 'pin_loser') {
@@ -21,7 +21,7 @@ import {
21
21
  isDigestStale,
22
22
  shouldRecompile,
23
23
  type DigestMode,
24
- } from './claims-helper.js';
24
+ } from '../extraction/claims-helper.js';
25
25
 
26
26
  const requireWasm = createRequire(import.meta.url);
27
27
  let _wasm: typeof import('@totalreclaw/core') | null = null;
@@ -134,9 +134,30 @@ export function endRecompile(): void {
134
134
  _recompileInProgress = false;
135
135
  }
136
136
 
137
+ /**
138
+ * Signature of the claim set that drove the last *scheduled* recompile —
139
+ * the recency probe's `maxCreatedAt` at the moment we called `recompileFn`.
140
+ * `null` means "no attempt yet", which is distinct from any real timestamp
141
+ * (including 0 for an empty vault), so the very first attempt is never
142
+ * mistaken for a repeat.
143
+ *
144
+ * Issue #455: a recompile's on-chain write (digest store + previous-digest
145
+ * tombstone) isn't visible to `loadLatestDigest` until the subgraph indexes
146
+ * it (can take ~20s). Every `before_agent_start` call in that window still
147
+ * sees the OLD digest against the SAME unchanged set of active claims, so
148
+ * `evaluateDigestState` keeps reporting stale+recompile and would fire a
149
+ * second (third, fourth…) UserOp for content that produces an identical
150
+ * digest. Tracking the signature we already acted on lets us skip those
151
+ * repeats while still recompiling the instant a genuinely newer claim
152
+ * (or a tombstone/supersession, which also bumps the recency probe's
153
+ * `maxCreatedAt`) shows up.
154
+ */
155
+ let _lastRecompileSignature: number | null = null;
156
+
137
157
  /** Test-only helper to reset module state between cases. */
138
158
  export function __resetDigestSyncState(): void {
139
159
  _recompileInProgress = false;
160
+ _lastRecompileSignature = null;
140
161
  }
141
162
 
142
163
  // ---------------------------------------------------------------------------
@@ -562,7 +583,16 @@ export async function maybeInjectDigest(
562
583
  });
563
584
 
564
585
  if (state.stale && state.recompile && !isRecompileInProgress()) {
565
- recompileFn(loaded.claimId);
586
+ // Coalesce: only fire if the claim set feeding the digest has actually
587
+ // moved since the last recompile we scheduled. Otherwise we're re-firing
588
+ // for the same content while the previous write is still propagating
589
+ // through subgraph indexing — see `_lastRecompileSignature` above.
590
+ if (probe.maxCreatedAt !== _lastRecompileSignature) {
591
+ _lastRecompileSignature = probe.maxCreatedAt;
592
+ recompileFn(loaded.claimId);
593
+ } else {
594
+ logger.info('Digest: skip recompile — claim set unchanged');
595
+ }
566
596
  }
567
597
 
568
598
  const promptText = typeof loaded.digest.prompt_text === 'string' ? loaded.digest.prompt_text : null;
@@ -26,7 +26,7 @@
26
26
  */
27
27
  import fs from 'node:fs';
28
28
  import path from 'node:path';
29
- import { CONFIG, setChainIdOverride, setDataEdgeAddressOverride } from './config.js';
29
+ import { CONFIG, setChainIdOverride, setDataEdgeAddressOverride } from '../config.js';
30
30
  /** A plausible EVM address — anything else from billing is ignored (#460). */
31
31
  const DATA_EDGE_ADDRESS_RE = /^0x[0-9a-fA-F]{40}$/;
32
32
  // ---------------------------------------------------------------------------
@@ -17,7 +17,7 @@
17
17
  * runs (see internal#127). This helper is the canonical re-entry point for
18
18
  * the variable.
19
19
  */
20
- import { getSessionId } from './config.js';
20
+ import { getSessionId } from '../config.js';
21
21
  /** Default `X-TotalReclaw-Client` value. */
22
22
  export const DEFAULT_CLIENT_ID = 'openclaw-plugin';
23
23
  /**
@@ -12,9 +12,9 @@
12
12
  * Phrase-safety: this module never touches the recovery phrase. It receives
13
13
  * pre-derived auth-key + wallet-address + encryption-key from the caller.
14
14
  */
15
- import { CONFIG } from './config.js';
16
- import { buildRelayHeaders } from './relay-headers.js';
17
- import { decrypt } from './crypto.js';
15
+ import { CONFIG } from '../config.js';
16
+ import { buildRelayHeaders } from '../billing/relay-headers.js';
17
+ import { decrypt } from '../crypto/crypto.js';
18
18
  /** Decode a hex blob written by submitFactBatchOnChain back to plaintext. */
19
19
  function fromHexBlob(hexBlob, encryptionKey) {
20
20
  const hex = hexBlob.startsWith('0x') ? hexBlob.slice(2) : hexBlob;
@@ -71,8 +71,8 @@ export async function exportAllFacts(walletAddress, authKeyHex, encryptionKey) {
71
71
  const variables = hasLastId
72
72
  ? { owner: walletAddress, first: PAGE_SIZE, lastId }
73
73
  : { owner: walletAddress, first: PAGE_SIZE };
74
- const data = await gql(query, variables);
75
- const facts = data?.facts ?? [];
74
+ const subgraphResponse = await gql(query, variables);
75
+ const facts = subgraphResponse?.facts ?? [];
76
76
  if (facts.length === 0)
77
77
  break;
78
78
  for (const f of facts) {
@@ -31,16 +31,16 @@
31
31
  * --json flag: all agent-facing CLI calls MUST use --json for clean machine-parseable output.
32
32
  * Plain text mode is for direct user CLI use only.
33
33
  *
34
- * Install: wired via package.json `bin.tr` → dist/tr-cli.js
35
- * Usage from container: `docker exec tr-openclaw node ~/.openclaw/extensions/totalreclaw/dist/tr-cli.js status --json`
34
+ * Install: wired via package.json `bin.tr` → dist/cli/tr-cli.js
35
+ * Usage from container: `docker exec tr-openclaw node ~/.openclaw/extensions/totalreclaw/dist/cli/tr-cli.js status --json`
36
36
  */
37
37
  import { randomUUID } from 'node:crypto';
38
- import { CONFIG, setRecoveryPhraseOverride } from './config.js';
39
- import { loadCredentialsJson } from './fs-helpers.js';
40
- import { printStatus } from './onboarding-cli.js';
41
- import { deriveKeys, computeAuthKeyHash, encrypt, generateBlindIndices, generateContentFingerprint, } from './crypto.js';
42
- import { createApiClient } from './api-client.js';
43
- import { encodeFactProtobuf, submitFactBatchOnChain, deriveSmartAccountAddress, getSubgraphConfig, PROTOBUF_VERSION_V4, } from './subgraph-store.js';
38
+ import { CONFIG, setRecoveryPhraseOverride } from '../config.js';
39
+ import { loadCredentialsJson } from '../fs-helpers.js';
40
+ import { printStatus } from '../pairing/onboarding-cli.js';
41
+ import { deriveKeys, computeAuthKeyHash, encrypt, generateBlindIndices, generateContentFingerprint, } from '../crypto/crypto.js';
42
+ import { createApiClient } from '../billing/api-client.js';
43
+ import { encodeFactProtobuf, submitFactBatchOnChain, deriveSmartAccountAddress, getSubgraphConfig, PROTOBUF_VERSION_V4, } from '../subgraph/subgraph-store.js';
44
44
  import { exportAllFacts } from './tr-cli-export-helper.js';
45
45
  // ---------------------------------------------------------------------------
46
46
  // Helpers
@@ -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.23';
54
+ const PLUGIN_VERSION = '3.3.13-rc.1';
55
55
  function die(msg, code = 1) {
56
56
  process.stderr.write(`tr: ${msg}\n`);
57
57
  process.exit(code);
@@ -85,7 +85,7 @@ function toHexBlob(plaintext, encryptionKey) {
85
85
  async function buildContext() {
86
86
  const creds = loadCredentialsJson(CREDENTIALS_PATH);
87
87
  if (!creds) {
88
- die('TotalReclaw is not set up. Run: node ~/.openclaw/extensions/totalreclaw/dist/tr-cli.js pair --json');
88
+ die('TotalReclaw is not set up. Run: node ~/.openclaw/extensions/totalreclaw/dist/cli/tr-cli.js pair --json');
89
89
  }
90
90
  const mnemonic = (typeof creds.mnemonic === 'string' && creds.mnemonic.trim()) ||
91
91
  (typeof creds.recovery_phrase === 'string' && creds.recovery_phrase.trim()) ||
@@ -192,8 +192,8 @@ async function cmdPair(args) {
192
192
  // The pair flow is relay-brokered (works through Docker NAT).
193
193
  // Phrase-safety: pair-cli-relay.ts is x25519-only; mnemonic never appears.
194
194
  const outputMode = args.includes('--json') ? 'json' : args.includes('--url-pin') ? 'url-pin' : 'human';
195
- const { runRelayPairCli } = await import('./pair-cli-relay.js');
196
- const { defaultRenderQr, buildDefaultPairCliIo } = await import('./pair-cli.js');
195
+ const { runRelayPairCli } = await import('../pairing/pair-cli-relay.js');
196
+ const { defaultRenderQr, buildDefaultPairCliIo } = await import('../pairing/pair-cli.js');
197
197
  const io = buildDefaultPairCliIo();
198
198
  const outcome = await runRelayPairCli('generate', {
199
199
  relayBaseUrl: CONFIG.pairRelayUrl,
@@ -271,9 +271,9 @@ async function cmdRemember(rawArgs) {
271
271
  authKeyHex: ctx.authKeyHex,
272
272
  walletAddress: ctx.walletAddress,
273
273
  };
274
- const result = await submitFactBatchOnChain([protobuf], config);
275
- if (!result.success) {
276
- die(`remember failed: on-chain UserOp did not succeed (userOpHash=${result.userOpHash || 'none'})`);
274
+ const submitResult = await submitFactBatchOnChain([protobuf], config);
275
+ if (!submitResult.success) {
276
+ die(`remember failed: on-chain UserOp did not succeed (userOpHash=${submitResult.userOpHash || 'none'})`);
277
277
  }
278
278
  if (jsonMode) {
279
279
  // JSON-first output for agent parsing.
@@ -331,12 +331,12 @@ async function cmdForget(rawArgs) {
331
331
  authKeyHex: ctx.authKeyHex,
332
332
  walletAddress: ctx.walletAddress,
333
333
  };
334
- const result = await submitFactBatchOnChain([protobuf], config);
335
- if (!result.success) {
336
- die(`forget failed: on-chain tombstone did not succeed (userOpHash=${result.userOpHash || 'none'})`);
334
+ const submitResult = await submitFactBatchOnChain([protobuf], config);
335
+ if (!submitResult.success) {
336
+ die(`forget failed: on-chain tombstone did not succeed (userOpHash=${submitResult.userOpHash || 'none'})`);
337
337
  }
338
338
  if (jsonMode) {
339
- log(JSON.stringify({ ok: true, id: factId, tx_hash: result.txHash }));
339
+ log(JSON.stringify({ ok: true, id: factId, tx_hash: submitResult.txHash }));
340
340
  }
341
341
  else {
342
342
  log(`ok — tombstoned ${factId} (tx=${result.txHash || 'pending'})`);
@@ -17,8 +17,8 @@
17
17
  import { createRequire } from 'node:module';
18
18
  import fs from 'node:fs';
19
19
  import path from 'node:path';
20
- import { envHomedir, envNumber, envString, envStringLower, } from './entry.js';
21
- import { computeEntityTrapdoor, isDigestBlob, } from './claims-helper.js';
20
+ import { envHomedir, envNumber, envString, envStringLower, } from '../entry.js';
21
+ import { computeEntityTrapdoor, isDigestBlob, } from '../extraction/claims-helper.js';
22
22
  const requireWasm = createRequire(import.meta.url);
23
23
  let _wasm = null;
24
24
  function getWasm() {
@@ -153,20 +153,20 @@ export async function saveWeightsFile(file) {
153
153
  export function parseCandidateClaim(decryptedJson) {
154
154
  if (isDigestBlob(decryptedJson))
155
155
  return null;
156
- let obj;
156
+ let parsed;
157
157
  try {
158
- obj = JSON.parse(decryptedJson);
158
+ parsed = JSON.parse(decryptedJson);
159
159
  }
160
160
  catch {
161
161
  return null;
162
162
  }
163
- if (typeof obj.t !== 'string' || typeof obj.c !== 'string')
163
+ if (typeof parsed.t !== 'string' || typeof parsed.c !== 'string')
164
164
  return null;
165
165
  // Filter out infra categories — digests and entity rollup claims are not
166
166
  // user-facing knowledge and must never be considered contradictions.
167
- if (obj.c === 'dig' || obj.c === 'ent')
167
+ if (parsed.c === 'dig' || parsed.c === 'ent')
168
168
  return null;
169
- return obj;
169
+ return parsed;
170
170
  }
171
171
  /** Is this candidate claim pinned (status `p`)? Delegates to WASM core. */
172
172
  export function isPinnedClaim(claim) {
@@ -715,10 +715,10 @@ export function findDecisionForPin(factId, role, logContent) {
715
715
  if (!logContent || logContent.length === 0)
716
716
  return null;
717
717
  try {
718
- const result = getWasm().findDecisionForPin(factId, role, logContent);
719
- if (result === 'null')
718
+ const decisionJson = getWasm().findDecisionForPin(factId, role, logContent);
719
+ if (decisionJson === 'null')
720
720
  return null;
721
- return JSON.parse(result);
721
+ return JSON.parse(decisionJson);
722
722
  }
723
723
  catch {
724
724
  // Fallback: local implementation if WASM fails.
@@ -812,10 +812,10 @@ export function buildFeedbackFromDecision(decision, action, nowUnixSeconds) {
812
812
  if (!decision.winner_components || !decision.loser_components)
813
813
  return null;
814
814
  try {
815
- const result = getWasm().buildFeedbackFromDecision(JSON.stringify(decision), action, Math.floor(nowUnixSeconds));
816
- if (result === 'null')
815
+ const feedbackJson = getWasm().buildFeedbackFromDecision(JSON.stringify(decision), action, Math.floor(nowUnixSeconds));
816
+ if (feedbackJson === 'null')
817
817
  return null;
818
- return JSON.parse(result);
818
+ return JSON.parse(feedbackJson);
819
819
  }
820
820
  catch {
821
821
  // Fallback: local implementation if WASM fails.
@@ -11,7 +11,7 @@
11
11
  * distinctive blind-index marker `DIGEST_TRAPDOOR`.
12
12
  */
13
13
  import { createRequire } from 'node:module';
14
- import { DIGEST_CLAIM_CAP, DIGEST_TRAPDOOR, buildDigestClaim, extractDigestFromClaim, hoursSince, isDigestStale, shouldRecompile, } from './claims-helper.js';
14
+ import { DIGEST_CLAIM_CAP, DIGEST_TRAPDOOR, buildDigestClaim, extractDigestFromClaim, hoursSince, isDigestStale, shouldRecompile, } from '../extraction/claims-helper.js';
15
15
  const requireWasm = createRequire(import.meta.url);
16
16
  let _wasm = null;
17
17
  function getWasm() {
@@ -76,9 +76,29 @@ export function tryBeginRecompile() {
76
76
  export function endRecompile() {
77
77
  _recompileInProgress = false;
78
78
  }
79
+ /**
80
+ * Signature of the claim set that drove the last *scheduled* recompile —
81
+ * the recency probe's `maxCreatedAt` at the moment we called `recompileFn`.
82
+ * `null` means "no attempt yet", which is distinct from any real timestamp
83
+ * (including 0 for an empty vault), so the very first attempt is never
84
+ * mistaken for a repeat.
85
+ *
86
+ * Issue #455: a recompile's on-chain write (digest store + previous-digest
87
+ * tombstone) isn't visible to `loadLatestDigest` until the subgraph indexes
88
+ * it (can take ~20s). Every `before_agent_start` call in that window still
89
+ * sees the OLD digest against the SAME unchanged set of active claims, so
90
+ * `evaluateDigestState` keeps reporting stale+recompile and would fire a
91
+ * second (third, fourth…) UserOp for content that produces an identical
92
+ * digest. Tracking the signature we already acted on lets us skip those
93
+ * repeats while still recompiling the instant a genuinely newer claim
94
+ * (or a tombstone/supersession, which also bumps the recency probe's
95
+ * `maxCreatedAt`) shows up.
96
+ */
97
+ let _lastRecompileSignature = null;
79
98
  /** Test-only helper to reset module state between cases. */
80
99
  export function __resetDigestSyncState() {
81
100
  _recompileInProgress = false;
101
+ _lastRecompileSignature = null;
82
102
  }
83
103
  // ---------------------------------------------------------------------------
84
104
  // Pure staleness + guard evaluation
@@ -351,7 +371,17 @@ export async function maybeInjectDigest(input) {
351
371
  hoursSinceCompilation: hoursSince(compiledAt, nowMs),
352
372
  });
353
373
  if (state.stale && state.recompile && !isRecompileInProgress()) {
354
- recompileFn(loaded.claimId);
374
+ // Coalesce: only fire if the claim set feeding the digest has actually
375
+ // moved since the last recompile we scheduled. Otherwise we're re-firing
376
+ // for the same content while the previous write is still propagating
377
+ // through subgraph indexing — see `_lastRecompileSignature` above.
378
+ if (probe.maxCreatedAt !== _lastRecompileSignature) {
379
+ _lastRecompileSignature = probe.maxCreatedAt;
380
+ recompileFn(loaded.claimId);
381
+ }
382
+ else {
383
+ logger.info('Digest: skip recompile — claim set unchanged');
384
+ }
355
385
  }
356
386
  const promptText = typeof loaded.digest.prompt_text === 'string' ? loaded.digest.prompt_text : null;
357
387
  return {
@@ -13,7 +13,7 @@
13
13
  * Env read is centralized in entry.ts (env-reading seam, Task 1.3 of the
14
14
  * OpenClaw native integration plan, 2026-06-21).
15
15
  */
16
- import { envNumber } from './entry.js';
16
+ import { envNumber } from '../entry.js';
17
17
  const DEFAULT_DOWNLOAD_TIMEOUT_MS = 600_000;
18
18
  const KEEPALIVE_INTERVAL_MS = 60_000;
19
19
  const MAX_DOWNLOAD_ATTEMPTS = 3;
@@ -21,12 +21,16 @@
21
21
  * Compute cosine similarity between two vectors.
22
22
  *
23
23
  * Returns dot(a, b) / (||a|| * ||b||).
24
- * Returns 0 if either vector has zero magnitude (avoids division by zero).
24
+ * Returns 0 if either vector has zero magnitude (avoids division by zero),
25
+ * or if the vectors differ in length — matching the authoritative core impl
26
+ * `cosine_similarity_f32` (rust/totalreclaw-core), which rejects mismatched
27
+ * dimensions rather than truncating to the shorter length. Keeping this local
28
+ * fallback byte-for-byte with core preserves cross-runtime rerank parity.
25
29
  */
26
30
  export function cosineSimilarity(a, b) {
27
- if (a.length === 0 || b.length === 0)
31
+ if (a.length !== b.length || a.length === 0)
28
32
  return 0;
29
- const len = Math.min(a.length, b.length);
33
+ const len = a.length;
30
34
  let dot = 0;
31
35
  let normA = 0;
32
36
  let normB = 0;
@@ -204,10 +208,15 @@ const SOURCE_WEIGHTS = {
204
208
  };
205
209
  const LEGACY_FALLBACK_WEIGHT = 0.85;
206
210
  export function getSourceWeight(source) {
211
+ // Missing source → legacy fallback (0.85), matching core's None path.
207
212
  if (!source)
208
213
  return LEGACY_FALLBACK_WEIGHT;
209
214
  const w = SOURCE_WEIGHTS[source.toLowerCase()];
210
- return w ?? 0.85; // unknown source moderate penalty
215
+ // An unknown NON-EMPTY source string mirrors core's
216
+ // `MemorySource::from_str_lossy`, whose catch-all arm maps any unrecognized
217
+ // token to `user-inferred` (rust/totalreclaw-core/src/claims.rs) — so an
218
+ // unknown string weighs the same as user-inferred, NOT the legacy fallback.
219
+ return w ?? SOURCE_WEIGHTS['user-inferred'];
211
220
  }
212
221
  // ---------------------------------------------------------------------------
213
222
  // Recency Scoring