prism-mcp-server 5.2.0 → 5.5.0
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/README.md +308 -218
- package/dist/backgroundScheduler.js +327 -0
- package/dist/config.js +29 -0
- package/dist/dashboard/server.js +246 -0
- package/dist/dashboard/ui.js +216 -6
- package/dist/hivemindWatchdog.js +206 -0
- package/dist/lifecycle.js +59 -4
- package/dist/scholar/freeSearch.js +78 -0
- package/dist/scholar/webScholar.js +258 -0
- package/dist/sdm/sdmDecoder.js +75 -0
- package/dist/sdm/sdmEngine.js +158 -0
- package/dist/server.js +173 -11
- package/dist/storage/sqlite.js +298 -47
- package/dist/storage/supabase.js +114 -1
- package/dist/tools/agentRegistryDefinitions.js +11 -4
- package/dist/tools/agentRegistryHandlers.js +23 -5
- package/dist/tools/index.js +2 -2
- package/dist/tools/sessionMemoryDefinitions.js +46 -1
- package/dist/tools/sessionMemoryHandlers.js +210 -38
- package/dist/utils/briefing.js +1 -1
- package/dist/utils/crdtMerge.js +152 -0
- package/dist/utils/healthCheck.js +15 -0
- package/dist/utils/llm/adapters/gemini.js +3 -3
- package/package.json +9 -2
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { BRAVE_API_KEY, FIRECRAWL_API_KEY, PRISM_SCHOLAR_MAX_ARTICLES_PER_RUN, PRISM_USER_ID, PRISM_SCHOLAR_TOPICS, PRISM_ENABLE_HIVEMIND } from "../config.js";
|
|
2
|
+
import { getStorage } from "../storage/index.js";
|
|
3
|
+
import { debugLog } from "../utils/logger.js";
|
|
4
|
+
import { getLLMProvider } from "../utils/llm/factory.js";
|
|
5
|
+
import { randomUUID } from "node:crypto";
|
|
6
|
+
import { performWebSearchRaw } from "../utils/braveApi.js";
|
|
7
|
+
import { getTracer } from "../utils/telemetry.js";
|
|
8
|
+
import { searchYahooFree, scrapeArticleLocal } from "./freeSearch.js";
|
|
9
|
+
// ─── Hivemind Integration Helpers ────────────────────────────
|
|
10
|
+
const SCHOLAR_PROJECT = "prism-scholar";
|
|
11
|
+
const SCHOLAR_ROLE = "scholar";
|
|
12
|
+
/**
|
|
13
|
+
* Phase 1: Register the Scholar as a Hivemind agent and emit heartbeats.
|
|
14
|
+
* Shows up on the Dashboard Radar as 🧠 with the current research topic.
|
|
15
|
+
* Gracefully no-ops when Hivemind is disabled.
|
|
16
|
+
*/
|
|
17
|
+
async function hivemindRegister(topic) {
|
|
18
|
+
if (!PRISM_ENABLE_HIVEMIND)
|
|
19
|
+
return;
|
|
20
|
+
try {
|
|
21
|
+
const storage = await getStorage();
|
|
22
|
+
await storage.registerAgent({
|
|
23
|
+
project: SCHOLAR_PROJECT,
|
|
24
|
+
user_id: PRISM_USER_ID,
|
|
25
|
+
role: SCHOLAR_ROLE,
|
|
26
|
+
agent_name: "Web Scholar",
|
|
27
|
+
status: "active",
|
|
28
|
+
current_task: `Researching: ${topic}`,
|
|
29
|
+
});
|
|
30
|
+
debugLog(`[WebScholar] 🐝 Registered on Hivemind Radar (topic: ${topic})`);
|
|
31
|
+
}
|
|
32
|
+
catch (err) {
|
|
33
|
+
debugLog(`[WebScholar] Hivemind registration failed (non-fatal): ${err}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
async function hivemindHeartbeat(task) {
|
|
37
|
+
if (!PRISM_ENABLE_HIVEMIND)
|
|
38
|
+
return;
|
|
39
|
+
try {
|
|
40
|
+
const storage = await getStorage();
|
|
41
|
+
await storage.heartbeatAgent(SCHOLAR_PROJECT, PRISM_USER_ID, SCHOLAR_ROLE, task);
|
|
42
|
+
}
|
|
43
|
+
catch { /* non-fatal */ }
|
|
44
|
+
}
|
|
45
|
+
async function hivemindIdle() {
|
|
46
|
+
if (!PRISM_ENABLE_HIVEMIND)
|
|
47
|
+
return;
|
|
48
|
+
try {
|
|
49
|
+
const storage = await getStorage();
|
|
50
|
+
await storage.updateAgentStatus(SCHOLAR_PROJECT, PRISM_USER_ID, SCHOLAR_ROLE, "idle");
|
|
51
|
+
}
|
|
52
|
+
catch { /* non-fatal */ }
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Phase 2: Broadcast a Telepathy alert after a successful research run.
|
|
56
|
+
* Active dev/qa agents will see "[🐝 SCHOLAR]" in their next tool response.
|
|
57
|
+
* Uses console.error to log the broadcast — the Watchdog sweep will pick up
|
|
58
|
+
* the Scholar's state change and generate alerts for active agents.
|
|
59
|
+
*/
|
|
60
|
+
async function hivemindBroadcast(topic, articleCount) {
|
|
61
|
+
if (!PRISM_ENABLE_HIVEMIND)
|
|
62
|
+
return;
|
|
63
|
+
try {
|
|
64
|
+
const storage = await getStorage();
|
|
65
|
+
// Update Scholar's current_task so the Watchdog and Dashboard show the result
|
|
66
|
+
await storage.heartbeatAgent(SCHOLAR_PROJECT, PRISM_USER_ID, SCHOLAR_ROLE, `✅ Completed: "${topic}" — ${articleCount} articles synthesized`);
|
|
67
|
+
// Log the broadcast — visible to operators watching the process
|
|
68
|
+
console.error(`[WebScholar] 🐝 TELEPATHY: New research on "${topic}" — ` +
|
|
69
|
+
`${articleCount} articles synthesized. Active agents will see results in knowledge search.`);
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
debugLog(`[WebScholar] Telepathy broadcast failed (non-fatal): ${err}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Phase 3: Task-aware topic selection.
|
|
77
|
+
* If Hivemind is active, check what other agents are working on and
|
|
78
|
+
* bias toward configured topics that overlap with their active tasks.
|
|
79
|
+
* Falls back to random selection if no matches or Hivemind is off.
|
|
80
|
+
*/
|
|
81
|
+
async function selectTopic() {
|
|
82
|
+
const topics = PRISM_SCHOLAR_TOPICS;
|
|
83
|
+
if (!topics || topics.length === 0)
|
|
84
|
+
return "";
|
|
85
|
+
// Default: random pick
|
|
86
|
+
const randomPick = topics[Math.floor(Math.random() * topics.length)];
|
|
87
|
+
if (!PRISM_ENABLE_HIVEMIND)
|
|
88
|
+
return randomPick;
|
|
89
|
+
try {
|
|
90
|
+
const storage = await getStorage();
|
|
91
|
+
const allAgents = await storage.getAllAgents(PRISM_USER_ID);
|
|
92
|
+
const activeTasks = allAgents
|
|
93
|
+
.filter(a => a.role !== SCHOLAR_ROLE && a.status === "active" && a.current_task)
|
|
94
|
+
.map(a => a.current_task.toLowerCase());
|
|
95
|
+
if (activeTasks.length === 0)
|
|
96
|
+
return randomPick;
|
|
97
|
+
// Find configured topics that match keywords in active agent tasks
|
|
98
|
+
const taskText = activeTasks.join(" ");
|
|
99
|
+
const matched = topics.filter(t => taskText.includes(t.toLowerCase()));
|
|
100
|
+
if (matched.length > 0) {
|
|
101
|
+
const chosen = matched[Math.floor(Math.random() * matched.length)];
|
|
102
|
+
debugLog(`[WebScholar] 🐝 Task-aware topic: "${chosen}" (matched from active agent tasks)`);
|
|
103
|
+
return chosen;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
catch (err) {
|
|
107
|
+
debugLog(`[WebScholar] Task-aware selection failed (non-fatal), using random: ${err}`);
|
|
108
|
+
}
|
|
109
|
+
return randomPick;
|
|
110
|
+
}
|
|
111
|
+
// ─── Core Pipeline ───────────────────────────────────────────
|
|
112
|
+
/**
|
|
113
|
+
* Runs the Web Scholar pipeline:
|
|
114
|
+
* 1. Picks a topic (task-aware when Hivemind is on)
|
|
115
|
+
* 2. Registers on the Hivemind Radar
|
|
116
|
+
* 3. Searches Brave for recent articles
|
|
117
|
+
* 4. Scrapes articles as markdown using Firecrawl
|
|
118
|
+
* 5. Summarizes the findings via LLM
|
|
119
|
+
* 6. Injects the summary directly into Prism's semantic ledger
|
|
120
|
+
* 7. Broadcasts a Telepathy alert to active agents
|
|
121
|
+
*/
|
|
122
|
+
let isRunning = false;
|
|
123
|
+
export async function runWebScholar() {
|
|
124
|
+
if (isRunning) {
|
|
125
|
+
debugLog("[WebScholar] Skipped: already running");
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
isRunning = true;
|
|
129
|
+
const tracer = getTracer();
|
|
130
|
+
const span = tracer.startSpan("background.web_scholar");
|
|
131
|
+
try {
|
|
132
|
+
const useFreeFallback = !BRAVE_API_KEY || !FIRECRAWL_API_KEY;
|
|
133
|
+
if (!PRISM_SCHOLAR_TOPICS || PRISM_SCHOLAR_TOPICS.length === 0) {
|
|
134
|
+
debugLog("[WebScholar] Skipped: No topics configured in PRISM_SCHOLAR_TOPICS");
|
|
135
|
+
span.setAttribute("scholar.skipped_reason", "no_topics");
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
// 1. Pick a topic (task-aware when Hivemind is active)
|
|
139
|
+
const topic = await selectTopic();
|
|
140
|
+
if (!topic) {
|
|
141
|
+
span.setAttribute("scholar.skipped_reason", "no_topics");
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
debugLog(`[WebScholar] 🧠 Starting research on topic: "${topic}"`);
|
|
145
|
+
span.setAttribute("scholar.topic", topic);
|
|
146
|
+
// 2. Register on Hivemind Radar
|
|
147
|
+
await hivemindRegister(topic);
|
|
148
|
+
// 3. Search for articles
|
|
149
|
+
await hivemindHeartbeat(`Searching for: ${topic}`);
|
|
150
|
+
let urls = [];
|
|
151
|
+
if (useFreeFallback) {
|
|
152
|
+
debugLog("[WebScholar] API keys missing, falling back to Local Free Search (Yahoo + Readability)");
|
|
153
|
+
const ddgResults = await searchYahooFree(topic, PRISM_SCHOLAR_MAX_ARTICLES_PER_RUN);
|
|
154
|
+
urls = ddgResults.map(r => r.url).filter(Boolean);
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
const braveResponse = await performWebSearchRaw(topic, PRISM_SCHOLAR_MAX_ARTICLES_PER_RUN);
|
|
158
|
+
const braveData = JSON.parse(braveResponse);
|
|
159
|
+
urls = (braveData.web?.results || []).map((r) => r.url).filter(Boolean);
|
|
160
|
+
}
|
|
161
|
+
if (urls.length === 0) {
|
|
162
|
+
debugLog(`[WebScholar] No articles found for "${topic}"`);
|
|
163
|
+
span.setAttribute("scholar.skipped_reason", "no_search_results");
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
debugLog(`[WebScholar] Found ${urls.length} articles. Scraping...`);
|
|
167
|
+
span.setAttribute("scholar.articles_found", urls.length);
|
|
168
|
+
// 4. Scrape each URL
|
|
169
|
+
await hivemindHeartbeat(`Scraping ${urls.length} articles on: ${topic}`);
|
|
170
|
+
const scrapedTexts = [];
|
|
171
|
+
for (const url of urls) {
|
|
172
|
+
if (useFreeFallback) {
|
|
173
|
+
try {
|
|
174
|
+
debugLog(`[WebScholar] Scraping local fallback: ${url}`);
|
|
175
|
+
const article = await scrapeArticleLocal(url);
|
|
176
|
+
const trimmed = article.content.slice(0, 15_000);
|
|
177
|
+
scrapedTexts.push(`Source: ${url}\nTitle: ${article.title}\n\n${trimmed}\n\n---\n`);
|
|
178
|
+
}
|
|
179
|
+
catch (err) {
|
|
180
|
+
console.error(`[WebScholar] Failed to locally scrape ${url}:`, err);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
try {
|
|
185
|
+
debugLog(`[WebScholar] Scraping Firecrawl: ${url}`);
|
|
186
|
+
const scrapeRes = await fetch("https://api.firecrawl.dev/v1/scrape", {
|
|
187
|
+
method: "POST",
|
|
188
|
+
headers: {
|
|
189
|
+
"Content-Type": "application/json",
|
|
190
|
+
"Authorization": `Bearer ${FIRECRAWL_API_KEY}`
|
|
191
|
+
},
|
|
192
|
+
body: JSON.stringify({
|
|
193
|
+
url,
|
|
194
|
+
formats: ["markdown"],
|
|
195
|
+
})
|
|
196
|
+
});
|
|
197
|
+
if (!scrapeRes.ok) {
|
|
198
|
+
console.error(`[WebScholar] Firecrawl failed for ${url}: ${scrapeRes.status}`);
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
const result = (await scrapeRes.json());
|
|
202
|
+
if (result.success && result.data?.markdown) {
|
|
203
|
+
const trimmed = result.data.markdown.slice(0, 15_000);
|
|
204
|
+
scrapedTexts.push(`Source: ${url}\n\n${trimmed}\n\n---\n`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
catch (err) {
|
|
208
|
+
console.error(`[WebScholar] Failed to scrape ${url}:`, err);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
if (scrapedTexts.length === 0) {
|
|
213
|
+
debugLog(`[WebScholar] Could not extract markdown from any articles.`);
|
|
214
|
+
span.setAttribute("scholar.skipped_reason", "all_scrapes_failed");
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
span.setAttribute("scholar.articles_scraped", scrapedTexts.length);
|
|
218
|
+
// 5. Summarize findings using LLM
|
|
219
|
+
await hivemindHeartbeat(`Synthesizing ${scrapedTexts.length} articles on: ${topic}`);
|
|
220
|
+
debugLog(`[WebScholar] Summarizing ${scrapedTexts.length} articles...`);
|
|
221
|
+
const combinedText = scrapedTexts.join("\n");
|
|
222
|
+
const prompt = `You are an AI research assistant. You have been asked to research the topic: "${topic}".
|
|
223
|
+
Read the following scraped web articles and write a comprehensive, markdown-formatted report summarizing the key findings, trends, and actionable insights. Focus heavily on facts, data, and actual content. Do NOT just list the articles. Synthesize the information.
|
|
224
|
+
|
|
225
|
+
### Scraped Articles:
|
|
226
|
+
${combinedText}`;
|
|
227
|
+
const llm = getLLMProvider();
|
|
228
|
+
const summary = await llm.generateText(prompt);
|
|
229
|
+
// 6. Inject the summary back into Prism memory
|
|
230
|
+
await hivemindHeartbeat(`Saving research to ledger: ${topic}`);
|
|
231
|
+
const storage = await getStorage();
|
|
232
|
+
await storage.saveLedger({
|
|
233
|
+
id: randomUUID(),
|
|
234
|
+
project: "prism-scholar",
|
|
235
|
+
conversation_id: "scholar-bg-" + Date.now(),
|
|
236
|
+
user_id: PRISM_USER_ID,
|
|
237
|
+
role: "scholar",
|
|
238
|
+
summary: `Autonomous Web Scholar Research: ${topic}\n\n${summary}`,
|
|
239
|
+
keywords: [topic, "research", "autonomous", "scholar"],
|
|
240
|
+
event_type: "learning",
|
|
241
|
+
importance: 7,
|
|
242
|
+
created_at: new Date().toISOString()
|
|
243
|
+
});
|
|
244
|
+
debugLog(`[WebScholar] ✅ Research complete and saved to ledger under project 'prism-scholar'.`);
|
|
245
|
+
span.setAttribute("scholar.success", true);
|
|
246
|
+
// 7. Broadcast Telepathy alert to active agents
|
|
247
|
+
await hivemindBroadcast(topic, scrapedTexts.length);
|
|
248
|
+
}
|
|
249
|
+
catch (err) {
|
|
250
|
+
console.error("[WebScholar] Pipeline failed:", err);
|
|
251
|
+
span.setAttribute("scholar.error", String(err));
|
|
252
|
+
}
|
|
253
|
+
finally {
|
|
254
|
+
await hivemindIdle();
|
|
255
|
+
isRunning = false;
|
|
256
|
+
span.end();
|
|
257
|
+
}
|
|
258
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { getStorage } from "../storage/index.js";
|
|
2
|
+
import { getDefaultCompressor, deserialize } from "../utils/turboquant.js";
|
|
3
|
+
import { debugLog } from "../utils/logger.js";
|
|
4
|
+
/**
|
|
5
|
+
* Perform a fast JS-space Hamming distance scan across TurboQuant compressed embeddings.
|
|
6
|
+
* Used exclusively for decoding SDM superposed target vectors back into ledger entries.
|
|
7
|
+
*/
|
|
8
|
+
export async function decodeSdmVector(project, targetVector, matchCount = 5, similarityThreshold = 0.55) {
|
|
9
|
+
const t0 = performance.now();
|
|
10
|
+
const storage = await getStorage();
|
|
11
|
+
// 1. Fetch all compressed embeddings for the active project
|
|
12
|
+
const embeddings = await storage.getAllProjectEmbeddings(project);
|
|
13
|
+
if (embeddings.length === 0) {
|
|
14
|
+
debugLog(`[SdmDecoder] No embeddings found for project ${project}`);
|
|
15
|
+
return [];
|
|
16
|
+
}
|
|
17
|
+
// 2. Compress the target Float32Array into a 96-byte Uint8Array using TurboQuant
|
|
18
|
+
const compressor = getDefaultCompressor();
|
|
19
|
+
// compressor.compress takes number[]
|
|
20
|
+
const queryArray = Array.from(targetVector); // V8 optimized array conversion
|
|
21
|
+
const compressedTarget = compressor.compress(queryArray);
|
|
22
|
+
const targetQjl = compressedTarget.qjlSigns;
|
|
23
|
+
// Ensure byte length aligns with 32-bit words for fast XOR
|
|
24
|
+
// We copy the target Uint8Array into a perfectly aligned Int32Array buffer
|
|
25
|
+
const wordCount = targetQjl.length / 4;
|
|
26
|
+
const targetWords = new Int32Array(wordCount);
|
|
27
|
+
new Uint8Array(targetWords.buffer).set(targetQjl);
|
|
28
|
+
// 3. Scan all DB embeddings and compute Hamming distance on QJL signs
|
|
29
|
+
const matches = [];
|
|
30
|
+
// Pre-allocate scratchpad Int32Array to avoid GC pressure during decoding sweeps
|
|
31
|
+
const dbWords = new Int32Array(wordCount);
|
|
32
|
+
const dbBytes = new Uint8Array(dbWords.buffer);
|
|
33
|
+
for (const entry of embeddings) {
|
|
34
|
+
try {
|
|
35
|
+
const dbBuf = Buffer.from(entry.embedding_compressed.replace(/^v1\./, ""), "base64");
|
|
36
|
+
const compressedDb = deserialize(dbBuf);
|
|
37
|
+
const dbQjl = compressedDb.qjlSigns;
|
|
38
|
+
if (dbQjl.length !== targetQjl.length)
|
|
39
|
+
continue;
|
|
40
|
+
// Zero-allocation copy into the scratchpad
|
|
41
|
+
dbBytes.set(dbQjl);
|
|
42
|
+
let hammingDistance = 0;
|
|
43
|
+
for (let i = 0; i < wordCount; i++) {
|
|
44
|
+
// Bitwise XOR to find mismatched bits
|
|
45
|
+
let xor = targetWords[i] ^ dbWords[i];
|
|
46
|
+
// Brian Kernighan's algorithm or V8 Math.clz32 trick isn't natively popcnt,
|
|
47
|
+
// but simple bitwise popcount is fast enough in V8:
|
|
48
|
+
xor = xor - ((xor >>> 1) & 0x55555555);
|
|
49
|
+
xor = (xor & 0x33333333) + ((xor >>> 2) & 0x33333333);
|
|
50
|
+
hammingDistance += (((xor + (xor >>> 4)) & 0x0F0F0F0F) * 0x01010101) >>> 24;
|
|
51
|
+
}
|
|
52
|
+
// Convert distance to standard similarity score (1.0 = exact match)
|
|
53
|
+
const totalBits = targetQjl.byteLength * 8;
|
|
54
|
+
const similarity = 1 - (hammingDistance / totalBits);
|
|
55
|
+
if (similarity >= similarityThreshold) {
|
|
56
|
+
matches.push({
|
|
57
|
+
id: entry.id,
|
|
58
|
+
summary: entry.summary,
|
|
59
|
+
distance: hammingDistance,
|
|
60
|
+
similarity
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
// Ignore malformed embeddings
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
// 4. Sort by highest similarity
|
|
70
|
+
matches.sort((a, b) => b.similarity - a.similarity);
|
|
71
|
+
const topMatches = matches.slice(0, matchCount);
|
|
72
|
+
const t1 = performance.now();
|
|
73
|
+
debugLog(`[SdmDecoder] Decoded target vector against ${embeddings.length} entries in ${(t1 - t0).toFixed(2)}ms`);
|
|
74
|
+
return topMatches;
|
|
75
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { PRISM_DEFAULT_CONFIG, getDefaultCompressor } from '../utils/turboquant.js';
|
|
2
|
+
// M = 10,000 hard locations per project
|
|
3
|
+
const SDM_M = 10000;
|
|
4
|
+
// D_addr = 768 bits (binary QJL string length), represented as 24 Uint32s
|
|
5
|
+
const D_ADDR_UINT32 = PRISM_DEFAULT_CONFIG.d / 32;
|
|
6
|
+
// The L1 radius threshold for "activation" (how many bits can differ)
|
|
7
|
+
// For 768 bits, picking a threshold like 300 gives us sparse activation
|
|
8
|
+
const ACTIVATION_RADIUS = 300;
|
|
9
|
+
// We use PRNG seeded deterministically to generate the initial hard location addresses
|
|
10
|
+
// This ensures that restarts produce the exact same Kanerva address space
|
|
11
|
+
class PRNG {
|
|
12
|
+
seed;
|
|
13
|
+
constructor(seed) {
|
|
14
|
+
this.seed = seed;
|
|
15
|
+
}
|
|
16
|
+
nextUInt32() {
|
|
17
|
+
this.seed = Math.imul(this.seed ^ (this.seed >>> 15), 1 | this.seed);
|
|
18
|
+
this.seed ^= this.seed + Math.imul(this.seed ^ (this.seed >>> 7), 61 | this.seed);
|
|
19
|
+
const v = ((this.seed ^ (this.seed >>> 14)) >>> 0);
|
|
20
|
+
return v;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Fast Hamming Distance over Uint32 arrays
|
|
25
|
+
*/
|
|
26
|
+
export function hammingDistance(a, b) {
|
|
27
|
+
let sum = 0;
|
|
28
|
+
for (let i = 0; i < a.length; i++) {
|
|
29
|
+
let xor = a[i] ^ b[i];
|
|
30
|
+
// 32-bit popcount trick
|
|
31
|
+
xor -= ((xor >>> 1) & 0x55555555);
|
|
32
|
+
xor = (xor & 0x33333333) + ((xor >>> 2) & 0x33333333);
|
|
33
|
+
xor = (xor + (xor >>> 4)) & 0x0F0F0F0F;
|
|
34
|
+
sum += Math.imul(xor, 0x01010101) >>> 24;
|
|
35
|
+
}
|
|
36
|
+
return sum;
|
|
37
|
+
}
|
|
38
|
+
export class SparseDistributedMemory {
|
|
39
|
+
// Hard Locations: Addresses (M x 24 uint32)
|
|
40
|
+
addresses;
|
|
41
|
+
// Hard Locations: Counters (M x 768 float32)
|
|
42
|
+
counters;
|
|
43
|
+
constructor(seed = 42) {
|
|
44
|
+
this.addresses = new Array(SDM_M);
|
|
45
|
+
this.counters = new Array(SDM_M);
|
|
46
|
+
const prng = new PRNG(seed);
|
|
47
|
+
for (let i = 0; i < SDM_M; i++) {
|
|
48
|
+
const addr = new Uint32Array(D_ADDR_UINT32);
|
|
49
|
+
for (let j = 0; j < D_ADDR_UINT32; j++) {
|
|
50
|
+
addr[j] = prng.nextUInt32();
|
|
51
|
+
}
|
|
52
|
+
this.addresses[i] = addr;
|
|
53
|
+
this.counters[i] = new Float32Array(PRISM_DEFAULT_CONFIG.d);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/** Convert TurboQuant QJL bytes into Uint32Array for fast bit math */
|
|
57
|
+
blobToAddress(blob) {
|
|
58
|
+
const qjl = blob.qjlSigns; // Uint8Array of length 96 (768 bits)
|
|
59
|
+
const view = new DataView(qjl.buffer, qjl.byteOffset, qjl.byteLength);
|
|
60
|
+
const addr = new Uint32Array(D_ADDR_UINT32);
|
|
61
|
+
for (let i = 0; i < D_ADDR_UINT32; i++) {
|
|
62
|
+
// Needs little endian to match bit alignment expectations safely
|
|
63
|
+
addr[i] = view.getUint32(i * 4, true);
|
|
64
|
+
}
|
|
65
|
+
return addr;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Write a dense vector into the memory by routing it to activated counters
|
|
69
|
+
*/
|
|
70
|
+
write(vector, k = 20) {
|
|
71
|
+
const compressor = getDefaultCompressor();
|
|
72
|
+
const blob = compressor.compress(Array.from(vector));
|
|
73
|
+
const address = this.blobToAddress(blob);
|
|
74
|
+
const activated = this.getTopK(address, k);
|
|
75
|
+
for (const idx of activated) {
|
|
76
|
+
const c = this.counters[idx];
|
|
77
|
+
for (let j = 0; j < PRISM_DEFAULT_CONFIG.d; j++) {
|
|
78
|
+
c[j] += vector[j];
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
read(queryVector, k = 20) {
|
|
83
|
+
const compressor = getDefaultCompressor();
|
|
84
|
+
const blob = compressor.compress(Array.from(queryVector));
|
|
85
|
+
const address = this.blobToAddress(blob);
|
|
86
|
+
const result = new Float32Array(PRISM_DEFAULT_CONFIG.d);
|
|
87
|
+
const activated = this.getTopK(address, k);
|
|
88
|
+
for (const idx of activated) {
|
|
89
|
+
const c = this.counters[idx];
|
|
90
|
+
for (let j = 0; j < PRISM_DEFAULT_CONFIG.d; j++) {
|
|
91
|
+
result[j] += c[j];
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return this.l2Normalize(result);
|
|
95
|
+
}
|
|
96
|
+
getTopK(address, k) {
|
|
97
|
+
// Array of (dist, index)
|
|
98
|
+
const dists = new Array(SDM_M);
|
|
99
|
+
for (let i = 0; i < SDM_M; i++) {
|
|
100
|
+
dists[i] = { d: hammingDistance(address, this.addresses[i]), i };
|
|
101
|
+
}
|
|
102
|
+
// Partial sort to find top K
|
|
103
|
+
dists.sort((a, b) => a.d - b.d);
|
|
104
|
+
const result = new Array(k);
|
|
105
|
+
for (let i = 0; i < k; i++) {
|
|
106
|
+
result[i] = dists[i].i;
|
|
107
|
+
}
|
|
108
|
+
return result;
|
|
109
|
+
}
|
|
110
|
+
l2Normalize(vec) {
|
|
111
|
+
let sum = 0;
|
|
112
|
+
for (let i = 0; i < vec.length; i++) {
|
|
113
|
+
sum += vec[i] * vec[i];
|
|
114
|
+
}
|
|
115
|
+
if (sum === 0)
|
|
116
|
+
return vec;
|
|
117
|
+
const mag = Math.sqrt(sum);
|
|
118
|
+
for (let i = 0; i < vec.length; i++) {
|
|
119
|
+
vec[i] /= mag;
|
|
120
|
+
}
|
|
121
|
+
return vec;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Export the entire 10k x 768 counter matrix as a single 1D Float32Array
|
|
125
|
+
* for binary serialization to SQLite BLOB.
|
|
126
|
+
*/
|
|
127
|
+
exportState() {
|
|
128
|
+
const state = new Float32Array(SDM_M * PRISM_DEFAULT_CONFIG.d);
|
|
129
|
+
for (let i = 0; i < SDM_M; i++) {
|
|
130
|
+
state.set(this.counters[i], i * PRISM_DEFAULT_CONFIG.d);
|
|
131
|
+
}
|
|
132
|
+
return state;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Import a previously serialized 1D Float32Array matrix back into
|
|
136
|
+
* the 2D counters array.
|
|
137
|
+
*/
|
|
138
|
+
importState(state) {
|
|
139
|
+
if (state.length !== SDM_M * PRISM_DEFAULT_CONFIG.d) {
|
|
140
|
+
throw new Error(`Invalid SDM state size: expected ${SDM_M * PRISM_DEFAULT_CONFIG.d}, got ${state.length}`);
|
|
141
|
+
}
|
|
142
|
+
for (let i = 0; i < SDM_M; i++) {
|
|
143
|
+
// Subarray creates a fast view over the underlying buffer
|
|
144
|
+
this.counters[i] = state.subarray(i * PRISM_DEFAULT_CONFIG.d, (i + 1) * PRISM_DEFAULT_CONFIG.d);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
// Global Singleton per Project in memory
|
|
149
|
+
const _sdmInstances = new Map();
|
|
150
|
+
export function getSdmEngine(projectId) {
|
|
151
|
+
if (!_sdmInstances.has(projectId)) {
|
|
152
|
+
_sdmInstances.set(projectId, new SparseDistributedMemory());
|
|
153
|
+
}
|
|
154
|
+
return _sdmInstances.get(projectId);
|
|
155
|
+
}
|
|
156
|
+
export function getAllActiveSdmProjects() {
|
|
157
|
+
return Array.from(_sdmInstances.keys());
|
|
158
|
+
}
|