@totalreclaw/totalreclaw 3.3.12 → 3.3.13
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 +25 -8
- package/{billing-cache.ts → billing/billing-cache.ts} +1 -1
- package/{relay-headers.ts → billing/relay-headers.ts} +1 -1
- package/{tr-cli-export-helper.ts → cli/tr-cli-export-helper.ts} +5 -5
- package/{tr-cli.ts → cli/tr-cli.ts} +20 -20
- package/{contradiction-sync.ts → contradiction/contradiction-sync.ts} +13 -13
- package/{digest-sync.ts → digest/digest-sync.ts} +32 -2
- package/dist/{billing-cache.js → billing/billing-cache.js} +1 -1
- package/dist/{relay-headers.js → billing/relay-headers.js} +1 -1
- package/dist/{tr-cli-export-helper.js → cli/tr-cli-export-helper.js} +5 -5
- package/dist/{tr-cli.js → cli/tr-cli.js} +19 -19
- package/dist/{contradiction-sync.js → contradiction/contradiction-sync.js} +13 -13
- package/dist/{digest-sync.js → digest/digest-sync.js} +32 -2
- package/dist/{download-ux.js → embedding/download-ux.js} +1 -1
- package/dist/{reranker.js → embedding/reranker.js} +13 -4
- package/dist/{claims-helper.js → extraction/claims-helper.js} +54 -58
- package/dist/extraction/consolidation.js +170 -0
- package/dist/{extractor.js → extraction/extractor.js} +17 -17
- package/dist/extraction/importance-filter.js +38 -0
- package/dist/{semantic-dedup.js → extraction/semantic-dedup.js} +2 -2
- package/dist/{batch-gate.js → import/batch-gate.js} +1 -1
- package/dist/import/import-runtime.js +692 -0
- package/dist/import-adapters/chatgpt-adapter.js +7 -7
- package/dist/import-adapters/gemini-adapter.js +13 -13
- package/dist/import-adapters/mem0-adapter.js +10 -10
- package/dist/index.js +111 -1094
- package/dist/{llm-client.js → llm/llm-client.js} +2 -2
- package/dist/{hot-cache-wrapper.js → memory/hot-cache-wrapper.js} +5 -5
- package/dist/{memory-runtime.js → memory/memory-runtime.js} +1 -1
- package/dist/{pin.js → memory/pin.js} +18 -18
- package/dist/{retype-setscope.js → memory/retype-setscope.js} +19 -19
- package/dist/{credential-provider.js → pairing/credential-provider.js} +2 -2
- package/dist/{first-run.js → pairing/first-run.js} +1 -1
- package/dist/{onboarding-cli.js → pairing/onboarding-cli.js} +20 -20
- package/dist/{pair-cli-relay.js → pairing/pair-cli-relay.js} +10 -10
- package/dist/{pair-crypto.js → pairing/pair-crypto.js} +1 -1
- package/dist/{pair-http.js → pairing/pair-http.js} +3 -3
- package/dist/{pair-remote-client.js → pairing/pair-remote-client.js} +10 -10
- package/dist/{pair-session-store.js → pairing/pair-session-store.js} +4 -4
- package/dist/runtime/config-schema.js +59 -0
- package/dist/runtime/format-helpers.js +257 -0
- package/dist/runtime/types.js +11 -0
- package/dist/{confirm-indexed.js → subgraph/confirm-indexed.js} +1 -1
- package/dist/{subgraph-search.js → subgraph/subgraph-search.js} +14 -14
- package/dist/{subgraph-store.js → subgraph/subgraph-store.js} +6 -6
- package/{download-ux.ts → embedding/download-ux.ts} +1 -1
- package/{reranker.ts → embedding/reranker.ts} +13 -4
- package/{claims-helper.ts → extraction/claims-helper.ts} +52 -60
- package/{consolidation.ts → extraction/consolidation.ts} +63 -154
- package/{extractor.ts → extraction/extractor.ts} +18 -17
- package/extraction/importance-filter.ts +50 -0
- package/{semantic-dedup.ts → extraction/semantic-dedup.ts} +2 -2
- package/{batch-gate.ts → import/batch-gate.ts} +1 -1
- package/import/import-runtime.ts +853 -0
- package/import-adapters/chatgpt-adapter.ts +7 -7
- package/import-adapters/gemini-adapter.ts +13 -13
- package/import-adapters/mem0-adapter.ts +10 -10
- package/index.ts +129 -1377
- package/{llm-client.ts → llm/llm-client.ts} +2 -2
- package/{hot-cache-wrapper.ts → memory/hot-cache-wrapper.ts} +5 -5
- package/{memory-runtime.ts → memory/memory-runtime.ts} +1 -1
- package/{pin.ts → memory/pin.ts} +20 -20
- package/{retype-setscope.ts → memory/retype-setscope.ts} +21 -21
- package/{tool-gating.ts → memory/tool-gating.ts} +1 -1
- package/package.json +6 -6
- package/{credential-provider.ts → pairing/credential-provider.ts} +2 -2
- package/{first-run.ts → pairing/first-run.ts} +1 -1
- package/{onboarding-cli.ts → pairing/onboarding-cli.ts} +20 -20
- package/{pair-cli-relay.ts → pairing/pair-cli-relay.ts} +10 -10
- package/{pair-crypto.ts → pairing/pair-crypto.ts} +1 -1
- package/{pair-http.ts → pairing/pair-http.ts} +3 -3
- package/{pair-remote-client.ts → pairing/pair-remote-client.ts} +10 -10
- package/{pair-session-store.ts → pairing/pair-session-store.ts} +4 -4
- package/runtime/config-schema.ts +64 -0
- package/runtime/format-helpers.ts +293 -0
- package/runtime/types.ts +118 -0
- package/skill.json +44 -10
- package/{confirm-indexed.ts → subgraph/confirm-indexed.ts} +1 -1
- package/{subgraph-search.ts → subgraph/subgraph-search.ts} +14 -14
- package/{subgraph-store.ts → subgraph/subgraph-store.ts} +10 -10
- package/dist/consolidation.js +0 -249
- /package/{api-client.ts → billing/api-client.ts} +0 -0
- /package/{gateway-url.ts → billing/gateway-url.ts} +0 -0
- /package/{inbound-user-tracker.ts → billing/inbound-user-tracker.ts} +0 -0
- /package/{relay.ts → billing/relay.ts} +0 -0
- /package/{crypto.ts → crypto/crypto.ts} +0 -0
- /package/{vault-crypto.ts → crypto/vault-crypto.ts} +0 -0
- /package/dist/{api-client.js → billing/api-client.js} +0 -0
- /package/dist/{gateway-url.js → billing/gateway-url.js} +0 -0
- /package/dist/{inbound-user-tracker.js → billing/inbound-user-tracker.js} +0 -0
- /package/dist/{relay.js → billing/relay.js} +0 -0
- /package/dist/{crypto.js → crypto/crypto.js} +0 -0
- /package/dist/{vault-crypto.js → crypto/vault-crypto.js} +0 -0
- /package/dist/{embedder-cache.js → embedding/embedder-cache.js} +0 -0
- /package/dist/{embedder-loader.js → embedding/embedder-loader.js} +0 -0
- /package/dist/{embedder-network.js → embedding/embedder-network.js} +0 -0
- /package/dist/{embedding.js → embedding/embedding.js} +0 -0
- /package/dist/{lsh.js → embedding/lsh.js} +0 -0
- /package/dist/{import-state-manager.js → import/import-state-manager.js} +0 -0
- /package/dist/{llm-profile-reader.js → llm/llm-profile-reader.js} +0 -0
- /package/dist/{native-memory.js → memory/native-memory.js} +0 -0
- /package/dist/{tool-gating.js → memory/tool-gating.js} +0 -0
- /package/dist/{tools.js → memory/tools.js} +0 -0
- /package/dist/{generate-mnemonic.js → pairing/generate-mnemonic.js} +0 -0
- /package/dist/{pair-cli.js → pairing/pair-cli.js} +0 -0
- /package/dist/{pair-page.js → pairing/pair-page.js} +0 -0
- /package/dist/{pair-qr.js → pairing/pair-qr.js} +0 -0
- /package/dist/{restart-auth.js → pairing/restart-auth.js} +0 -0
- /package/dist/{qa-bug-report.js → setup/qa-bug-report.js} +0 -0
- /package/dist/{skill-register.js → setup/skill-register.js} +0 -0
- /package/dist/{trajectory-poller.js → subgraph/trajectory-poller.js} +0 -0
- /package/{embedder-cache.ts → embedding/embedder-cache.ts} +0 -0
- /package/{embedder-loader.ts → embedding/embedder-loader.ts} +0 -0
- /package/{embedder-network.ts → embedding/embedder-network.ts} +0 -0
- /package/{embedding.ts → embedding/embedding.ts} +0 -0
- /package/{lsh.ts → embedding/lsh.ts} +0 -0
- /package/{import-state-manager.ts → import/import-state-manager.ts} +0 -0
- /package/{llm-profile-reader.ts → llm/llm-profile-reader.ts} +0 -0
- /package/{native-memory.ts → memory/native-memory.ts} +0 -0
- /package/{tools.ts → memory/tools.ts} +0 -0
- /package/{generate-mnemonic.ts → pairing/generate-mnemonic.ts} +0 -0
- /package/{pair-cli.ts → pairing/pair-cli.ts} +0 -0
- /package/{pair-page.ts → pairing/pair-page.ts} +0 -0
- /package/{pair-qr.ts → pairing/pair-qr.ts} +0 -0
- /package/{restart-auth.ts → pairing/restart-auth.ts} +0 -0
- /package/{qa-bug-report.ts → setup/qa-bug-report.ts} +0 -0
- /package/{skill-register.ts → setup/skill-register.ts} +0 -0
- /package/{trajectory-poller.ts → subgraph/trajectory-poller.ts} +0 -0
|
@@ -21,10 +21,10 @@ function getWasm() {
|
|
|
21
21
|
if (!_wasm) _wasm = requireWasm('@totalreclaw/core');
|
|
22
22
|
return _wasm;
|
|
23
23
|
}
|
|
24
|
-
import { CONFIG, getDataEdgeAddressOverride } from '
|
|
25
|
-
import { buildRelayHeaders } from '
|
|
26
|
-
import { rpcRequest, rpcWithRetry } from '
|
|
27
|
-
import { signUserOp } from '
|
|
24
|
+
import { CONFIG, getDataEdgeAddressOverride } from '../config.js';
|
|
25
|
+
import { buildRelayHeaders } from '../billing/relay-headers.js';
|
|
26
|
+
import { rpcRequest, rpcWithRetry } from '../billing/relay.js';
|
|
27
|
+
import { signUserOp } from '../crypto/vault-crypto.js';
|
|
28
28
|
|
|
29
29
|
// ---------------------------------------------------------------------------
|
|
30
30
|
// Types
|
|
@@ -501,7 +501,7 @@ async function submitFactOnChainLocked(
|
|
|
501
501
|
// AA10 can occur at pm_sponsorUserOperation (initCode present on deployed sender)
|
|
502
502
|
// or eth_sendUserOperation (same root cause). This loop handles both.
|
|
503
503
|
let userOpHash: string | undefined;
|
|
504
|
-
let lastErr:
|
|
504
|
+
let lastErr: unknown;
|
|
505
505
|
let attempt = 0;
|
|
506
506
|
const maxAttempts = 2;
|
|
507
507
|
|
|
@@ -555,8 +555,8 @@ async function submitFactOnChainLocked(
|
|
|
555
555
|
userOpHash = await rpc('eth_sendUserOperation', [unsignedOp, entryPoint]);
|
|
556
556
|
// Success — break out of retry loop
|
|
557
557
|
break;
|
|
558
|
-
} catch (err:
|
|
559
|
-
const msg = err
|
|
558
|
+
} catch (err: unknown) {
|
|
559
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
560
560
|
// AA10 "sender already constructed" or AA25 invalid nonce → retry
|
|
561
561
|
if (/AA25|AA10|invalid account nonce|already being processed/i.test(msg)) {
|
|
562
562
|
lastErr = err;
|
|
@@ -715,7 +715,7 @@ async function submitFactBatchOnChainLocked(
|
|
|
715
715
|
// Single retry loop: getInitCode → build UserOp → estimate → sponsor → sign → send
|
|
716
716
|
// On AA10 "sender already constructed", mark sender as force-deployed and retry.
|
|
717
717
|
let userOpHash: string | undefined;
|
|
718
|
-
let lastErr:
|
|
718
|
+
let lastErr: unknown;
|
|
719
719
|
let attempt = 0;
|
|
720
720
|
const maxAttempts = 2;
|
|
721
721
|
|
|
@@ -783,8 +783,8 @@ async function submitFactBatchOnChainLocked(
|
|
|
783
783
|
userOpHash = await rpc('eth_sendUserOperation', [unsignedOp, entryPoint]);
|
|
784
784
|
// Success — break out of retry loop
|
|
785
785
|
break;
|
|
786
|
-
} catch (err:
|
|
787
|
-
const msg = err
|
|
786
|
+
} catch (err: unknown) {
|
|
787
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
788
788
|
// AA10 "sender already constructed" or AA25 invalid nonce → retry
|
|
789
789
|
if (/AA25|AA10|invalid account nonce|already being processed/i.test(msg)) {
|
|
790
790
|
lastErr = err;
|
package/dist/consolidation.js
DELETED
|
@@ -1,249 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* TotalReclaw Plugin - Memory Consolidation & Near-Duplicate Detection
|
|
3
|
-
*
|
|
4
|
-
* Provides cross-session / cross-vault deduplication of stored facts using
|
|
5
|
-
* cosine similarity on their embeddings. Unlike semantic-dedup.ts (which
|
|
6
|
-
* handles within-batch dedup at threshold 0.9), this module handles:
|
|
7
|
-
*
|
|
8
|
-
* 1. Store-time dedup — before writing a new fact, check whether a
|
|
9
|
-
* near-duplicate already exists in the vault (findNearDuplicate).
|
|
10
|
-
* 2. Supersede logic — when a near-duplicate is found, decide whether
|
|
11
|
-
* the new fact should replace or be skipped (shouldSupersede).
|
|
12
|
-
* 3. Bulk consolidation — cluster all facts in the vault and identify
|
|
13
|
-
* groups of near-duplicates for cleanup (clusterFacts).
|
|
14
|
-
*
|
|
15
|
-
* Delegates core computation to `@totalreclaw/core` Rust WASM module where
|
|
16
|
-
* bindings are available. `shouldSupersede` uses the core directly.
|
|
17
|
-
* `findNearDuplicate` and `clusterFacts` use the core's `findBestNearDuplicate`
|
|
18
|
-
* and `clusterFacts` WASM functions when available, falling back to local
|
|
19
|
-
* implementations that use WASM-backed `cosineSimilarity`.
|
|
20
|
-
*
|
|
21
|
-
* Threshold helpers remain local; their env reads are centralized in
|
|
22
|
-
* entry.ts (env-reading seam, Task 1.3 of the OpenClaw native
|
|
23
|
-
* integration plan, 2026-06-21).
|
|
24
|
-
*/
|
|
25
|
-
import { createRequire } from 'node:module';
|
|
26
|
-
import { cosineSimilarity } from './reranker.js';
|
|
27
|
-
import { envNumber } from './entry.js';
|
|
28
|
-
// ---------------------------------------------------------------------------
|
|
29
|
-
// Lazy-load WASM core (mirrors claims-helper.ts / contradiction-sync.ts
|
|
30
|
-
// pattern — plays nicely under both the OpenClaw runtime (CJS-ish tsx) and
|
|
31
|
-
// bare Node ESM used by tests).
|
|
32
|
-
// ---------------------------------------------------------------------------
|
|
33
|
-
const requireWasm = createRequire(import.meta.url);
|
|
34
|
-
let _wasm = null;
|
|
35
|
-
function getWasm() {
|
|
36
|
-
if (!_wasm)
|
|
37
|
-
_wasm = requireWasm('@totalreclaw/core');
|
|
38
|
-
return _wasm;
|
|
39
|
-
}
|
|
40
|
-
// ---------------------------------------------------------------------------
|
|
41
|
-
// Configuration
|
|
42
|
-
// ---------------------------------------------------------------------------
|
|
43
|
-
/**
|
|
44
|
-
* Get the cosine similarity threshold for store-time dedup.
|
|
45
|
-
*
|
|
46
|
-
* Configurable via TOTALRECLAW_STORE_DEDUP_THRESHOLD env var.
|
|
47
|
-
* Must be a number in [0, 1]. Falls back to 0.85 if invalid or unset.
|
|
48
|
-
*/
|
|
49
|
-
export function getStoreDedupThreshold() {
|
|
50
|
-
return envNumber('TOTALRECLAW_STORE_DEDUP_THRESHOLD', 0.85, { min: 0, max: 1 });
|
|
51
|
-
}
|
|
52
|
-
/**
|
|
53
|
-
* Get the cosine similarity threshold for bulk consolidation clustering.
|
|
54
|
-
*
|
|
55
|
-
* Configurable via TOTALRECLAW_CONSOLIDATION_THRESHOLD env var.
|
|
56
|
-
* Must be a number in [0, 1]. Falls back to 0.88 if invalid or unset.
|
|
57
|
-
*/
|
|
58
|
-
export function getConsolidationThreshold() {
|
|
59
|
-
return envNumber('TOTALRECLAW_CONSOLIDATION_THRESHOLD', 0.88, { min: 0, max: 1 });
|
|
60
|
-
}
|
|
61
|
-
/** Maximum candidates to compare against during store-time dedup. */
|
|
62
|
-
export const STORE_DEDUP_MAX_CANDIDATES = 200;
|
|
63
|
-
// ---------------------------------------------------------------------------
|
|
64
|
-
// Store-time dedup
|
|
65
|
-
// ---------------------------------------------------------------------------
|
|
66
|
-
/**
|
|
67
|
-
* Find the best near-duplicate match for a new fact among existing candidates.
|
|
68
|
-
*
|
|
69
|
-
* Compares the new fact's embedding against all candidates using cosine
|
|
70
|
-
* similarity. Returns the candidate with the highest similarity above the
|
|
71
|
-
* threshold, or null if no match is found.
|
|
72
|
-
*
|
|
73
|
-
* Candidates without embeddings are skipped (fail-safe).
|
|
74
|
-
*
|
|
75
|
-
* @param newFactEmbedding - Embedding vector for the new fact
|
|
76
|
-
* @param candidates - Existing facts to compare against
|
|
77
|
-
* @param threshold - Cosine similarity threshold (e.g. 0.85)
|
|
78
|
-
* @returns - Best match above threshold, or null
|
|
79
|
-
*/
|
|
80
|
-
export function findNearDuplicate(newFactEmbedding, candidates, threshold) {
|
|
81
|
-
const wasm = getWasm();
|
|
82
|
-
// Use core's findBestNearDuplicate if available (added in core >=1.5.0;
|
|
83
|
-
// guaranteed present in core >=2.0.0 which this plugin depends on).
|
|
84
|
-
if (typeof wasm.findBestNearDuplicate === 'function') {
|
|
85
|
-
const existing = candidates
|
|
86
|
-
.filter((c) => c.embedding && c.embedding.length > 0)
|
|
87
|
-
.map((c) => ({ id: c.id, embedding: c.embedding }));
|
|
88
|
-
if (existing.length === 0)
|
|
89
|
-
return null;
|
|
90
|
-
const resultJs = wasm.findBestNearDuplicate(JSON.stringify(newFactEmbedding), JSON.stringify(existing), threshold);
|
|
91
|
-
if (resultJs == null)
|
|
92
|
-
return null;
|
|
93
|
-
const result = typeof resultJs === 'string' ? JSON.parse(resultJs) : resultJs;
|
|
94
|
-
const matched = candidates.find((c) => c.id === result.fact_id);
|
|
95
|
-
if (!matched)
|
|
96
|
-
return null;
|
|
97
|
-
return { existingFact: matched, similarity: result.similarity };
|
|
98
|
-
}
|
|
99
|
-
// Fallback: local loop using WASM-backed cosineSimilarity. Defensive only
|
|
100
|
-
// — core >=2.0.0 always exposes findBestNearDuplicate.
|
|
101
|
-
let bestMatch = null;
|
|
102
|
-
for (const candidate of candidates) {
|
|
103
|
-
if (!candidate.embedding || candidate.embedding.length === 0)
|
|
104
|
-
continue;
|
|
105
|
-
const similarity = cosineSimilarity(newFactEmbedding, candidate.embedding);
|
|
106
|
-
if (similarity >= threshold) {
|
|
107
|
-
if (!bestMatch || similarity > bestMatch.similarity) {
|
|
108
|
-
bestMatch = { existingFact: candidate, similarity };
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
return bestMatch;
|
|
113
|
-
}
|
|
114
|
-
// ---------------------------------------------------------------------------
|
|
115
|
-
// Supersede logic
|
|
116
|
-
// ---------------------------------------------------------------------------
|
|
117
|
-
/**
|
|
118
|
-
* Decide whether a new fact should supersede an existing near-duplicate.
|
|
119
|
-
*
|
|
120
|
-
* - Higher importance wins.
|
|
121
|
-
* - Equal importance: new fact supersedes (newer is preferred).
|
|
122
|
-
*
|
|
123
|
-
* Delegates to `@totalreclaw/core` WASM `shouldSupersede`.
|
|
124
|
-
*
|
|
125
|
-
* @param newImportance - Importance score of the new fact
|
|
126
|
-
* @param existingFact - The existing near-duplicate candidate
|
|
127
|
-
* @returns - 'supersede' if new fact should replace, 'skip' otherwise
|
|
128
|
-
*/
|
|
129
|
-
export function shouldSupersede(newImportance, existingFact) {
|
|
130
|
-
const wasm = getWasm();
|
|
131
|
-
return wasm.shouldSupersede(newImportance, existingFact.importance) ? 'supersede' : 'skip';
|
|
132
|
-
}
|
|
133
|
-
// ---------------------------------------------------------------------------
|
|
134
|
-
// Bulk consolidation
|
|
135
|
-
// ---------------------------------------------------------------------------
|
|
136
|
-
/**
|
|
137
|
-
* Cluster facts by semantic similarity using greedy single-pass clustering.
|
|
138
|
-
*
|
|
139
|
-
* Delegates to `@totalreclaw/core` WASM `clusterFacts` which performs the
|
|
140
|
-
* same greedy single-pass algorithm and representative selection. The WASM
|
|
141
|
-
* function returns ID-only clusters; this wrapper maps IDs back to full
|
|
142
|
-
* `DecryptedCandidate` objects for callers.
|
|
143
|
-
*
|
|
144
|
-
* Only returns clusters that have duplicates (i.e. more than one member).
|
|
145
|
-
* Facts without embeddings are not clustered.
|
|
146
|
-
*
|
|
147
|
-
* @param facts - All facts to cluster
|
|
148
|
-
* @param threshold - Cosine similarity threshold (e.g. 0.88)
|
|
149
|
-
* @returns - Clusters with duplicates (representative + duplicates)
|
|
150
|
-
*/
|
|
151
|
-
export function clusterFacts(facts, threshold) {
|
|
152
|
-
const wasm = getWasm();
|
|
153
|
-
// Use core's clusterFacts if available (added in core >=1.5.0;
|
|
154
|
-
// guaranteed present in core >=2.0.0 which this plugin depends on).
|
|
155
|
-
if (typeof wasm.clusterFacts === 'function') {
|
|
156
|
-
// Build ConsolidationCandidate JSON for WASM (snake_case fields).
|
|
157
|
-
const wasmCandidates = facts
|
|
158
|
-
.filter((f) => f.embedding && f.embedding.length > 0)
|
|
159
|
-
.map((f) => ({
|
|
160
|
-
id: f.id,
|
|
161
|
-
text: f.text,
|
|
162
|
-
embedding: f.embedding,
|
|
163
|
-
importance: f.importance,
|
|
164
|
-
decay_score: f.decayScore,
|
|
165
|
-
created_at: f.createdAt,
|
|
166
|
-
version: f.version,
|
|
167
|
-
}));
|
|
168
|
-
if (wasmCandidates.length === 0)
|
|
169
|
-
return [];
|
|
170
|
-
const resultJs = wasm.clusterFacts(JSON.stringify(wasmCandidates), threshold);
|
|
171
|
-
// WASM returns a JSON string: [{ representative: string, duplicates: string[] }]
|
|
172
|
-
const wasmClusters = typeof resultJs === 'string' ? JSON.parse(resultJs) : resultJs;
|
|
173
|
-
// Build a lookup map for fast ID -> DecryptedCandidate resolution.
|
|
174
|
-
const byId = new Map();
|
|
175
|
-
for (const f of facts)
|
|
176
|
-
byId.set(f.id, f);
|
|
177
|
-
// Map ID-only clusters back to full DecryptedCandidate objects.
|
|
178
|
-
// Filter out singleton clusters (no duplicates) to match the pre-WASM
|
|
179
|
-
// plugin contract — callers rely on `clusters.length === 0` when nothing
|
|
180
|
-
// duplicates anything.
|
|
181
|
-
const result = [];
|
|
182
|
-
for (const wc of wasmClusters) {
|
|
183
|
-
const rep = byId.get(wc.representative);
|
|
184
|
-
if (!rep)
|
|
185
|
-
continue;
|
|
186
|
-
const dups = wc.duplicates
|
|
187
|
-
.map((id) => byId.get(id))
|
|
188
|
-
.filter((d) => d !== undefined);
|
|
189
|
-
if (dups.length > 0) {
|
|
190
|
-
result.push({ representative: rep, duplicates: dups });
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
return result;
|
|
194
|
-
}
|
|
195
|
-
// Fallback: local greedy single-pass clustering using WASM-backed
|
|
196
|
-
// cosineSimilarity. Defensive only — core >=2.0.0 always exposes clusterFacts.
|
|
197
|
-
const clusters = [];
|
|
198
|
-
for (const fact of facts) {
|
|
199
|
-
if (!fact.embedding || fact.embedding.length === 0)
|
|
200
|
-
continue;
|
|
201
|
-
let assigned = false;
|
|
202
|
-
for (const cluster of clusters) {
|
|
203
|
-
const seed = cluster.members[0];
|
|
204
|
-
if (!seed.embedding)
|
|
205
|
-
continue;
|
|
206
|
-
const similarity = cosineSimilarity(fact.embedding, seed.embedding);
|
|
207
|
-
if (similarity >= threshold) {
|
|
208
|
-
cluster.members.push(fact);
|
|
209
|
-
assigned = true;
|
|
210
|
-
break;
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
if (!assigned) {
|
|
214
|
-
clusters.push({ members: [fact] });
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
const result = [];
|
|
218
|
-
for (const cluster of clusters) {
|
|
219
|
-
if (cluster.members.length < 2)
|
|
220
|
-
continue;
|
|
221
|
-
const representative = pickRepresentative(cluster.members);
|
|
222
|
-
const duplicates = cluster.members.filter((m) => m !== representative);
|
|
223
|
-
result.push({ representative, duplicates });
|
|
224
|
-
}
|
|
225
|
-
return result;
|
|
226
|
-
}
|
|
227
|
-
// ---------------------------------------------------------------------------
|
|
228
|
-
// Local helpers (used only in fallback paths)
|
|
229
|
-
// ---------------------------------------------------------------------------
|
|
230
|
-
/**
|
|
231
|
-
* Pick the best representative from a group of near-duplicate facts.
|
|
232
|
-
*
|
|
233
|
-
* Tiebreak order:
|
|
234
|
-
* 1. Highest decayScore
|
|
235
|
-
* 2. Most recent (highest createdAt)
|
|
236
|
-
* 3. Longest text
|
|
237
|
-
*/
|
|
238
|
-
function pickRepresentative(facts) {
|
|
239
|
-
let best = facts[0];
|
|
240
|
-
for (let i = 1; i < facts.length; i++) {
|
|
241
|
-
const f = facts[i];
|
|
242
|
-
if (f.decayScore > best.decayScore ||
|
|
243
|
-
(f.decayScore === best.decayScore && f.createdAt > best.createdAt) ||
|
|
244
|
-
(f.decayScore === best.decayScore && f.createdAt === best.createdAt && f.text.length > best.text.length)) {
|
|
245
|
-
best = f;
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
return best;
|
|
249
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|