prism-mcp-server 7.8.7 → 8.0.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 CHANGED
@@ -12,7 +12,7 @@
12
12
 
13
13
  **Your AI agent forgets everything between sessions. Prism fixes that — then teaches it to think.**
14
14
 
15
- Prism v7.8 is a true **Cognitive Architecture** inspired by human brain mechanics. Beyond flat vector search, your agent now forms principles from experience, follows causal trains of thought, and possesses the self-awareness to know when it lacks information. **Your agents don't just remember; they learn.**
15
+ Prism v8.0 is a true **Cognitive Architecture** inspired by human brain mechanics. The new **Synapse Engine** replaces flat vector search with pure multi-hop graph propagation your agent now follows causal trains of thought across memory, forms principles from experience, and knows when it lacks information. **Your agents don't just remember; they think.**
16
16
 
17
17
  ```bash
18
18
  npx -y prism-mcp-server
@@ -28,6 +28,7 @@ Works with **Claude Desktop · Claude Code · Cursor · Windsurf · Cline · Gem
28
28
  - [Setup Guides](#setup-guides)
29
29
  - [Universal Import: Bring Your History](#universal-import-bring-your-history)
30
30
  - [What Makes Prism Different](#what-makes-prism-different)
31
+ - [Synapse Engine (v8.0)](#-synapse-engine-v80)
31
32
  - [Cognitive Architecture (v7.8)](#-cognitive-architecture-v78)
32
33
  - [Data Privacy & Egress](#data-privacy--egress)
33
34
  - [Use Cases](#use-cases)
@@ -54,7 +55,7 @@ Prism has three pillars:
54
55
 
55
56
  1. **🧠 Cognitive Memory** — Memories are ranked like a human brain: recently and frequently accessed context surfaces first, while stale context fades naturally via ACT-R activation decay. Raw experience consolidates into semantic principles through Hebbian learning. The result is retrieval quality that no flat vector search can match. *(See [Cognitive Architecture](#-cognitive-architecture-v78) and [Scientific Foundation](#-scientific-foundation).)*
56
57
 
57
- 2. **🔗 Multi-Hop Reasoning** — When your agent searches for "Error X", Prism doesn't just find logs mentioning "Error X". Spreading activation traverses the causal graph and brings back "Workaround Y", which is connected to "Architecture Decision Z" a literal train of thought. *(See [Cognitive Architecture](#-cognitive-architecture-v78).)*
58
+ 2. **⚡ Synapse Engine (GraphRAG)** — When your agent searches for "Error X", the Synapse Engine doesn't just find logs mentioning "Error X". Multi-hop energy propagation traverses the causal graph — dampened by fan effect, bounded by lateral inhibition — and surfaces "Workaround Y" connected to "Architecture Decision Z". Nodes discovered exclusively via graph traversal are tagged `[🌐 Synapse]` so you can *see* the engine working. *(See [Synapse Engine](#-synapse-engine-v80).)*
58
59
 
59
60
  3. **🏭 Autonomous Execution (Dark Factory)** — When you're ready, Prism can run coding tasks end-to-end with a fail-closed pipeline where an adversarial evaluator catches bugs the generator missed — before you ever see the PR. *(See [Dark Factory](#-dark-factory--adversarial-autonomous-pipelines).)*
60
61
 
@@ -459,6 +460,53 @@ When you trigger a Dark Factory pipeline, Prism doesn't just run your task — i
459
460
 
460
461
  ---
461
462
 
463
+ ## ⚡ Synapse Engine (v8.0)
464
+
465
+ > *Standard RAG retrieves documents. GraphRAG traverses relationships. The Synapse Engine does both — a pure, storage-agnostic multi-hop propagation engine that turns your agent's memory into an associative reasoning network.*
466
+
467
+ The Synapse Engine (v8.0) replaces the legacy SQL-coupled spreading activation with a **pure functional graph propagation core** inspired by ACT-R cognitive architecture. It is Prism's native, low-latency GraphRAG solution — no external graph database required.
468
+
469
+ ### Before vs After
470
+
471
+ | | **v7.x (Standard RAG)** | **v8.0 (Synapse Engine)** |
472
+ |---|---|---|
473
+ | **Query** | "Tell me about Project Apollo" | "Tell me about Project Apollo" |
474
+ | **Retrieval** | Returns the design doc (1 hop, cosine match) | Returns the design doc → follows `caused_by` edge to a developer's debugging session → discovers an old Slack thread about a critical auth bug |
475
+ | **Agent output** | Summarizes the design doc | Summarizes the design doc **and warns about the unresolved auth issue** |
476
+ | **Discovery tag** | — | `[🌐 Synapse]` marks the auth bug node, proving the engine found context the user didn't ask for |
477
+
478
+ ### How It Works
479
+
480
+ ```
481
+ Query: "Project Apollo status"
482
+
483
+ ┌───────────┼───────────────┐
484
+ ▼ ▼ ▼
485
+ [Design [Sprint [Deployment
486
+ Doc] Retro] Log]
487
+ │ 1.0 │ 0.8 │ 0.6 ← semantic anchors
488
+ │ │ │
489
+ ▼ ▼ ▼
490
+ [Dev [Auth Bug [Perf ← Synapse discovered
491
+ Profile Thread 🌐] Regression 🌐] (multi-hop)
492
+ 0.42] 0.38] 0.31]
493
+ ```
494
+
495
+ **Key design decisions:**
496
+
497
+ | Mechanism | Purpose |
498
+ |---|---|
499
+ | **Dampened Fan Effect** (`1/ln(degree+e)`) | Prevents hub nodes from flooding results |
500
+ | **Asymmetric Propagation** (fwd 100%, back 50%) | Preserves causal directionality |
501
+ | **Cyclic Loop Prevention** (`visitedEdges` set) | Prevents infinite energy amplification |
502
+ | **Sigmoid Normalization** | Structural scores can't overwhelm semantic base |
503
+ | **Lateral Inhibition** | Caps output to top-K most energized nodes |
504
+ | **Hybrid Scoring** (70% semantic / 30% structural) | Base relevance always matters |
505
+
506
+ > 💡 Synapse is **non-fatal** — if the graph traversal fails for any reason, search gracefully returns the original semantic matches. Zero risk of degraded search.
507
+
508
+ ---
509
+
462
510
  ## 🧠 Cognitive Architecture (v7.8)
463
511
 
464
512
  > *Prism v7.8 is our biggest leap forward yet. We have moved beyond flat vector search and implemented a true Cognitive Architecture inspired by human brain mechanics. With the new ACT-R Spreading Activation Engine, Episodic-to-Semantic memory consolidation, and Uncertainty-Aware Rejection Gates, Prism doesn't just store logs anymore — it forms principles, follows causal trains of thought, and possesses the self-awareness to know when it lacks information.*
@@ -691,9 +739,10 @@ The Generator strips the `console.log`, resubmits, and the next `EVALUATE` retur
691
739
 
692
740
  ## 🆕 What's New
693
741
 
694
- > **Current release: v7.8.7Cognitive Architecture**
742
+ > **Current release: v8.0.0Synapse Engine**
695
743
 
696
- - 🧠 **v7.8.xCognitive Architecture:** The biggest leap forward yet. Moved beyond flat vector search into a true cognitive architecture inspired by human brain mechanics. Episodic-to-Semantic memory consolidation (Hebbian learning), ACT-R Spreading Activation with multi-hop causal reasoning, Uncertainty-Aware Rejection Gate (your agent can say "I don't know"), and Dynamic Fast Weight Decay (semantic memories outlive episodic chatter by 2×). Validated by **LoCoMo-Plus benchmark** (arXiv 2602.10715) with Precision@K and MRR metrics. **Your agents don't just remember; they learn.** → [Cognitive Architecture](#-cognitive-architecture-v78)
744
+ - **v8.0.0Synapse Engine:** Pure, storage-agnostic multi-hop graph propagation engine replaces the legacy SQL-coupled spreading activation. O(T × M) bounded ACT-R energy propagation with dampened fan effect, asymmetric bidirectional flow, cyclic loop prevention, and sigmoid normalization. Full integration into both SQLite and Supabase backends. 5 new config knobs. Battle-hardened with NaN guards, config clamping, non-fatal enrichment, and 16 passing tests. **Memory search now follows the causal graph, not just keywords.** → [Synapse Engine](#synapse-engine-v80)
745
+ - 🧠 **v7.8.x — Cognitive Architecture:** Episodic-to-Semantic consolidation (Hebbian learning), ACT-R Spreading Activation with multi-hop causal reasoning, Uncertainty-Aware Rejection Gate, and Dynamic Fast Weight Decay. Validated by **LoCoMo-Plus benchmark**. → [Cognitive Architecture](#-cognitive-architecture-v78)
697
746
  - 🌐 **v7.7.0 — Cloud-Native SSE Transport:** Full unauthenticated and authenticated Server-Sent Events MCP support for seamless network deployments.
698
747
  - 🩺 **v7.5.0 — Intent Health Dashboard + Security Hardening:** Real-time 0–100 project health scoring (staleness × TODO load × decisions). 10 XSS injection vectors patched. Algorithm hardened with NaN guards and score ceiling.
699
748
  - ⚔️ **v7.4.0 — Adversarial Evaluation:** Split-brain anti-sycophancy pipeline. Generator and evaluator in isolated roles with evidence-bound findings.
@@ -937,6 +986,11 @@ Requires `PRISM_DARK_FACTORY_ENABLED=true`.
937
986
  | `PRISM_ACTR_WEIGHT_ACTIVATION` | No | Composite score ACT-R activation weight (default: `0.3`) |
938
987
  | `PRISM_ACTR_ACCESS_LOG_RETENTION_DAYS` | No | Days before access logs are pruned by background scheduler (default: `90`) |
939
988
  | `PRISM_DARK_FACTORY_ENABLED` | No | `"true"` to enable Dark Factory autonomous pipeline tools (`session_start_pipeline`, `session_check_pipeline_status`, `session_abort_pipeline`) |
989
+ | `PRISM_SYNAPSE_ENABLED` | No | `"true"` (default) to enable Synapse Engine graph propagation in search results |
990
+ | `PRISM_SYNAPSE_ITERATIONS` | No | Propagation iterations (default: `3`). Higher = deeper graph traversal |
991
+ | `PRISM_SYNAPSE_SPREAD_FACTOR` | No | Energy decay multiplier per hop (default: `0.8`). Range: 0.0–1.0 |
992
+ | `PRISM_SYNAPSE_LATERAL_INHIBITION` | No | Max nodes returned by Synapse (default: `7`, min: `1`) |
993
+ | `PRISM_SYNAPSE_SOFT_CAP` | No | Max candidate pool size during propagation (default: `20`, min: `1`) |
940
994
 
941
995
  </details>
942
996
 
@@ -964,8 +1018,8 @@ Prism is a **stdio-based MCP server** that manages persistent agent memory. Here
964
1018
  │ └──────┬───────┘ └──────────────┘ └────────────────┘ │
965
1019
  │ ↕ │
966
1020
  │ ┌────────────────────────────────────────────────────┐ │
967
- │ │ Cognitive Engine (v7.8) │ │
968
- │ │ • ACT-R Spreading Activation (multi-hop) │ │
1021
+ │ │ Cognitive Engine (v8.0) │ │
1022
+ │ │ • Synapse Engine (pure multi-hop propagation) │ │
969
1023
  │ │ • Episodic → Semantic Consolidation (Hebbian) │ │
970
1024
  │ │ • Uncertainty-Aware Rejection Gate │ │
971
1025
  │ │ • LoCoMo-Plus Benchmark Validation │ │
@@ -1068,10 +1122,11 @@ Prism has evolved from smart session logging into a **cognitive memory architect
1068
1122
 
1069
1123
  ## 📦 Milestones & Roadmap
1070
1124
 
1071
- > **Current: v7.8.7** — Cognitive Architecture ([CHANGELOG](CHANGELOG.md))
1125
+ > **Current: v8.0.0** — Synapse Engine ([CHANGELOG](CHANGELOG.md))
1072
1126
 
1073
1127
  | Release | Headline |
1074
1128
  |---------|----------|
1129
+ | **v8.0** | ⚡ Synapse Engine — Pure multi-hop GraphRAG propagation, storage-agnostic, NaN-hardened, `[🌐 Synapse]` discovery tags |
1075
1130
  | **v7.8** | 🧠 Cognitive Architecture — Hebbian consolidation, multi-hop reasoning, rejection gate, dynamic decay |
1076
1131
  | **v7.7** | 🌐 Cloud-Native SSE Transport |
1077
1132
  | **v7.5** | 🩺 Intent Health Dashboard + Security Hardening |
package/dist/config.js CHANGED
@@ -268,3 +268,17 @@ export const PRISM_DARK_FACTORY_ENABLED = process.env.PRISM_DARK_FACTORY_ENABLED
268
268
  export const PRISM_DARK_FACTORY_POLL_MS = parseInt(process.env.PRISM_DARK_FACTORY_POLL_MS || "30000", 10);
269
269
  /** Default max wall-clock time per pipeline (ms). Default: 15 minutes. */
270
270
  export const PRISM_DARK_FACTORY_MAX_RUNTIME_MS = parseInt(process.env.PRISM_DARK_FACTORY_MAX_RUNTIME_MS || "900000", 10);
271
+ // ─── v8.0: Synapse — Spreading Activation Engine ──────────────
272
+ // Multi-hop energy propagation through memory_links graph.
273
+ // Enabled by default. Set PRISM_SYNAPSE_ENABLED=false to fall back
274
+ // to 1-hop candidateScopedSpreadingActivation (v7.0 behavior).
275
+ /** Master switch for the Synapse engine. Enabled by default (opt-out). */
276
+ export const PRISM_SYNAPSE_ENABLED = (process.env.PRISM_SYNAPSE_ENABLED ?? "true") !== "false";
277
+ /** Number of propagation iterations (depth). Higher = deeper traversal, more latency. (Default: 3) */
278
+ export const PRISM_SYNAPSE_ITERATIONS = parseInt(process.env.PRISM_SYNAPSE_ITERATIONS || "3", 10);
279
+ /** Energy attenuation per hop. Must be < 1.0 for convergence. (Default: 0.8) */
280
+ export const PRISM_SYNAPSE_SPREAD_FACTOR = parseFloat(process.env.PRISM_SYNAPSE_SPREAD_FACTOR || "0.8");
281
+ /** Hard cap on final output nodes (lateral inhibition). (Default: 7) */
282
+ export const PRISM_SYNAPSE_LATERAL_INHIBITION = parseInt(process.env.PRISM_SYNAPSE_LATERAL_INHIBITION || "7", 10);
283
+ /** Soft cap on active nodes per iteration (prevents explosion). (Default: 20) */
284
+ export const PRISM_SYNAPSE_SOFT_CAP = parseInt(process.env.PRISM_SYNAPSE_SOFT_CAP || "20", 10);
@@ -1253,7 +1253,9 @@ Example:\n## Dev Rules\n- Always write tests first\n- Use TypeScript strict mode
1253
1253
  onchange="onEmbeddingProviderChange(this.value)">
1254
1254
  <option value="auto">🔄 Auto (same as Text Provider)</option>
1255
1255
  <option value="gemini">🔵 Gemini</option>
1256
- <option value="openai">🟢 OpenAI / Ollama</option>
1256
+ <option value="openai">🟢 OpenAI</option>
1257
+ <option value="voyage">🔮 Voyage AI</option>
1258
+ <option value="ollama">🟠 Ollama (Local)</option>
1257
1259
  </select>
1258
1260
  </div>
1259
1261
 
@@ -1261,7 +1263,7 @@ Example:\n## Dev Rules\n- Always write tests first\n- Use TypeScript strict mode
1261
1263
  <div id="anthropic-embed-warning" style="display:none;margin-top:0.5rem;padding:0.5rem 0.75rem;background:rgba(251,146,60,0.1);border:1px solid rgba(251,146,60,0.3);border-radius:6px;font-size:0.78rem;color:#fb923c;line-height:1.5">
1262
1264
  ⚠️ <strong>Anthropic has no native embedding API.</strong>
1263
1265
  Auto mode will route embeddings to <strong>Gemini</strong>.
1264
- Set Embedding Provider to <strong>OpenAI / Ollama</strong> to use a local model (e.g. <code>nomic-embed-text</code>).
1266
+ Set Embedding Provider to <strong>Ollama (Local)</strong> for free local embeddings, or <strong>Voyage AI</strong> for the Anthropic-recommended cloud pairing.
1265
1267
  </div>
1266
1268
 
1267
1269
  <!-- OpenAI embedding model field (shown when embedding_provider = openai) -->
@@ -1269,7 +1271,7 @@ Example:\n## Dev Rules\n- Always write tests first\n- Use TypeScript strict mode
1269
1271
  <div class="setting-row">
1270
1272
  <div>
1271
1273
  <div class="setting-label">Embedding Model</div>
1272
- <div class="setting-desc">Must output 768 dims. Ollama: nomic-embed-text · OpenAI: text-embedding-3-small</div>
1274
+ <div class="setting-desc">Must output 768 dims. Default: text-embedding-3-small</div>
1273
1275
  </div>
1274
1276
  <input type="text" id="input-openai-embedding-model"
1275
1277
  placeholder="text-embedding-3-small"
@@ -1279,9 +1281,61 @@ Example:\n## Dev Rules\n- Always write tests first\n- Use TypeScript strict mode
1279
1281
  </div>
1280
1282
  </div>
1281
1283
 
1284
+ <!-- Voyage AI embedding fields (shown when embedding_provider = voyage) -->
1285
+ <div id="embed-fields-voyage" style="display:none">
1286
+ <div class="setting-row">
1287
+ <div>
1288
+ <div class="setting-label">Voyage API Key</div>
1289
+ <div class="setting-desc">Get one free at <a href="https://dash.voyageai.com" target="_blank" style="color:var(--accent)">dash.voyageai.com</a></div>
1290
+ </div>
1291
+ <input type="password" id="input-voyage-api-key"
1292
+ placeholder="pa-…"
1293
+ style="padding: 0.2rem 0.5rem; background: var(--bg-hover); color: var(--text-primary); border: 1px solid var(--border-color); border-radius: 4px; font-size: 0.85rem; font-family: var(--font-mono); width: 180px;"
1294
+ onchange="saveBootSetting('VOYAGE_API_KEY', this.value)"
1295
+ oninput="clearTimeout(this._pv); var self=this; this._pv=setTimeout(function(){saveBootSetting('VOYAGE_API_KEY',self.value)},800)" />
1296
+ </div>
1297
+ <div class="setting-row">
1298
+ <div>
1299
+ <div class="setting-label">Voyage Model</div>
1300
+ <div class="setting-desc">voyage-code-3 (code) · voyage-3 (general). Both MRL → 768 dims.</div>
1301
+ </div>
1302
+ <input type="text" id="input-voyage-model"
1303
+ placeholder="voyage-code-3"
1304
+ style="padding: 0.2rem 0.5rem; background: var(--bg-hover); color: var(--text-primary); border: 1px solid var(--border-color); border-radius: 4px; font-size: 0.85rem; font-family: var(--font-mono); width: 160px;"
1305
+ onchange="saveBootSetting('voyage_model', this.value)"
1306
+ oninput="clearTimeout(this._pvm); var self=this; this._pvm=setTimeout(function(){saveBootSetting('voyage_model',self.value)},800)" />
1307
+ </div>
1308
+ </div>
1309
+
1310
+ <!-- Ollama embedding fields (shown when embedding_provider = ollama) -->
1311
+ <div id="embed-fields-ollama" style="display:none">
1312
+ <div class="setting-row">
1313
+ <div>
1314
+ <div class="setting-label">Ollama Base URL</div>
1315
+ <div class="setting-desc">Where Ollama is running locally</div>
1316
+ </div>
1317
+ <input type="text" id="input-ollama-base-url"
1318
+ placeholder="http://localhost:11434"
1319
+ style="padding: 0.2rem 0.5rem; background: var(--bg-hover); color: var(--text-primary); border: 1px solid var(--border-color); border-radius: 4px; font-size: 0.85rem; font-family: var(--font-mono); width: 220px;"
1320
+ onchange="saveBootSetting('ollama_base_url', this.value)"
1321
+ oninput="clearTimeout(this._pou); var self=this; this._pou=setTimeout(function(){saveBootSetting('ollama_base_url',self.value)},800)" />
1322
+ </div>
1323
+ <div class="setting-row">
1324
+ <div>
1325
+ <div class="setting-label">Embedding Model</div>
1326
+ <div class="setting-desc">Must output 768 dims. <code>nomic-embed-text</code> recommended.</div>
1327
+ </div>
1328
+ <input type="text" id="input-ollama-model"
1329
+ placeholder="nomic-embed-text"
1330
+ style="padding: 0.2rem 0.5rem; background: var(--bg-hover); color: var(--text-primary); border: 1px solid var(--border-color); border-radius: 4px; font-size: 0.85rem; font-family: var(--font-mono); width: 180px;"
1331
+ onchange="saveBootSetting('ollama_model', this.value)"
1332
+ oninput="clearTimeout(this._pom); var self=this; this._pom=setTimeout(function(){saveBootSetting('ollama_model',self.value)},800)" />
1333
+ </div>
1334
+ </div>
1335
+
1282
1336
  <div style="margin-top:1rem;padding:0.6rem 0.8rem;background:rgba(139,92,246,0.08);border:1px solid rgba(139,92,246,0.2);border-radius:6px;font-size:0.78rem;color:var(--text-secondary);line-height:1.5">
1283
- 💡 <strong>Cost-optimized setup:</strong> Text Provider → <code>Anthropic</code>, Embedding Provider → <code>OpenAI / Ollama</code>.<br>
1284
- Use Claude 3.5 Sonnet for reasoning &amp; <code>nomic-embed-text</code> (free, local) for embeddings.
1337
+ 💡 <strong>Zero-cost setup:</strong> Text Provider → <code>Anthropic</code>, Embedding Provider → <code>Ollama (Local)</code>.<br>
1338
+ Use Claude for reasoning &amp; <code>nomic-embed-text</code> (free, local, 768-dim native) for embeddings.
1285
1339
  </div>
1286
1340
 
1287
1341
  <span class="setting-saved" id="savedToastProviders">Saved ✓</span>
@@ -3135,8 +3189,10 @@ function onTextProviderChange(value) {
3135
3189
  // Called when the EMBEDDING provider dropdown changes.
3136
3190
  function onEmbeddingProviderChange(value) {
3137
3191
  var textVal = document.getElementById('select-text-provider').value;
3138
- // Show the OpenAI embedding model field only when embedding=openai
3192
+ // Show provider-specific fields based on the selected embedding provider
3139
3193
  document.getElementById('embed-fields-openai').style.display = value === 'openai' ? '' : 'none';
3194
+ document.getElementById('embed-fields-voyage').style.display = value === 'voyage' ? '' : 'none';
3195
+ document.getElementById('embed-fields-ollama').style.display = value === 'ollama' ? '' : 'none';
3140
3196
  refreshAnthropicWarning(textVal, value);
3141
3197
  saveBootSetting('embedding_provider', value);
3142
3198
  }
@@ -3174,7 +3230,17 @@ function loadAiProviderSettings() {
3174
3230
  if (embedSel)
3175
3231
  embedSel.value = embedProvider;
3176
3232
  document.getElementById('embed-fields-openai').style.display = embedProvider === 'openai' ? '' : 'none';
3233
+ document.getElementById('embed-fields-voyage').style.display = embedProvider === 'voyage' ? '' : 'none';
3234
+ document.getElementById('embed-fields-ollama').style.display = embedProvider === 'ollama' ? '' : 'none';
3177
3235
  refreshAnthropicWarning(textProvider, embedProvider);
3236
+ var vKey = document.getElementById('input-voyage-api-key');
3237
+ if (vKey) vKey.placeholder = s.VOYAGE_API_KEY ? '(key saved — paste to update)' : 'pa-…';
3238
+ var vMod = document.getElementById('input-voyage-model');
3239
+ if (vMod && s.voyage_model) vMod.value = s.voyage_model;
3240
+ var olUrl = document.getElementById('input-ollama-base-url');
3241
+ if (olUrl && s.ollama_base_url) olUrl.value = s.ollama_base_url;
3242
+ var olMod = document.getElementById('input-ollama-model');
3243
+ if (olMod && s.ollama_model) olMod.value = s.ollama_model;
3178
3244
  gKey = document.getElementById('input-google-api-key');
3179
3245
  if (gKey)
3180
3246
  gKey.placeholder = s.GOOGLE_API_KEY ? '(key saved — paste to update)' : 'AIza…';
@@ -0,0 +1,222 @@
1
+ /**
2
+ * Synapse — Spreading Activation Engine (v8.0)
3
+ *
4
+ * ═══════════════════════════════════════════════════════════════════
5
+ * PURPOSE:
6
+ * Multi-hop energy propagation through the memory_links graph.
7
+ * Replaces both the old SQL-coupled spreadingActivation.ts AND the
8
+ * shallow 1-hop candidateScopedSpreadingActivation() from v7.0.
9
+ *
10
+ * PAPER BASIS:
11
+ * ACT-R spreading activation (Anderson, 2004) extended with:
12
+ * - Dampened fan effect: 1/ln(degree + e)
13
+ * - Asymmetric bidirectional flow (forward 100%, backward 50%)
14
+ * - Lateral inhibition (soft cap per iteration, hard cap on output)
15
+ * - Visited-edge tracking to prevent cyclic energy amplification
16
+ *
17
+ * DESIGN:
18
+ * All functions are PURE — zero I/O, zero imports from storage or SQL.
19
+ * Link data is fetched via a LinkFetcher callback injected by the caller
20
+ * (SqliteStorage, SupabaseStorage, or test mock).
21
+ * NOTE: debugLog (stderr diagnostic) is the sole side effect — it only
22
+ * fires when PRISM_DEBUG_LOGGING=true and does not affect correctness.
23
+ *
24
+ * PERFORMANCE:
25
+ * Bounded by O(T × softCap) where T=iterations (default 3) and
26
+ * softCap=20. Worst case: 3 × 20 node expansions = 60 link fetches.
27
+ * Typical latency: 5-15ms on SQLite.
28
+ *
29
+ * FILES THAT IMPORT THIS:
30
+ * - src/storage/sqlite.ts (search methods)
31
+ * - src/tools/graphHandlers.ts (ACT-R re-ranking pipeline)
32
+ * ═══════════════════════════════════════════════════════════════════
33
+ */
34
+ import { debugLog } from "../utils/logger.js";
35
+ // ─── Default Configuration ────────────────────────────────────
36
+ export const DEFAULT_SYNAPSE_CONFIG = {
37
+ iterations: 3,
38
+ spreadFactor: 0.8,
39
+ lateralInhibition: 7,
40
+ softCap: 20,
41
+ };
42
+ // ─── Sigmoid Normalization ────────────────────────────────────
43
+ /**
44
+ * Squash activation energy into [0, 1] using a parameterized sigmoid.
45
+ *
46
+ * This prevents raw activation energy from overpowering the similarity
47
+ * component in the hybrid score. Without normalization, a node with
48
+ * 10 inbound paths could accumulate energy > 5.0, making the 0.3 weight
49
+ * mathematically dominate the 0.7 similarity weight.
50
+ *
51
+ * Calibration:
52
+ * midpoint = 0.5 — an activation of 0.5 maps to sigmoid output 0.5
53
+ * steepness = 2.0 — moderate discrimination
54
+ *
55
+ * @param energy - Raw accumulated activation energy
56
+ * @returns Normalized energy in (0, 1)
57
+ */
58
+ export function normalizeActivationEnergy(energy) {
59
+ if (!Number.isFinite(energy)) {
60
+ return energy > 0 ? 1.0 : 0.0;
61
+ }
62
+ const midpoint = 0.5;
63
+ const steepness = 2.0;
64
+ const exponent = -steepness * (energy - midpoint);
65
+ if (exponent > 500)
66
+ return 0;
67
+ if (exponent < -500)
68
+ return 1;
69
+ return 1 / (1 + Math.exp(exponent));
70
+ }
71
+ // ─── Core Engine ──────────────────────────────────────────────
72
+ /**
73
+ * Propagate activation energy through the memory_links graph.
74
+ *
75
+ * Algorithm:
76
+ * 1. Initialize active nodes with anchor similarity scores
77
+ * 2. For each iteration:
78
+ * a. Fetch ALL links connected to active nodes (via LinkFetcher)
79
+ * b. Compute per-source fan effect: 1 / ln(out-degree + e)
80
+ * c. Forward flow: source → target at S × strength × sourceEnergy / dampedFan
81
+ * d. Backward flow: target → source at (S × 0.5) × strength × targetEnergy
82
+ * e. Track visited edges to prevent cyclic re-traversal
83
+ * f. Soft lateral inhibition: keep top-softCap nodes
84
+ * 3. Final lateral inhibition: return top-M nodes
85
+ *
86
+ * @param anchors - Map of entry ID → initial activation (typically similarity score)
87
+ * @param linkFetcher - Async callback to batch-fetch links
88
+ * @param config - Engine configuration (uses defaults if omitted)
89
+ * @returns Array of SynapseResult sorted by activationEnergy descending
90
+ */
91
+ export async function propagateActivation(anchors, linkFetcher, config = {}) {
92
+ const startMs = performance.now();
93
+ const cfg = { ...DEFAULT_SYNAPSE_CONFIG, ...config };
94
+ // Validate: spreadFactor must be < 1.0 for convergence guarantee
95
+ if (cfg.spreadFactor >= 1.0) {
96
+ debugLog(`[synapse] WARNING: spreadFactor=${cfg.spreadFactor} >= 1.0, clamping to 0.99`);
97
+ cfg.spreadFactor = 0.99;
98
+ }
99
+ // Clamp minimums to prevent silent result drops
100
+ if (cfg.lateralInhibition < 1)
101
+ cfg.lateralInhibition = 1;
102
+ if (cfg.softCap < 1)
103
+ cfg.softCap = 1;
104
+ // State: current activation energy per node
105
+ let activeNodes = new Map();
106
+ // Track minimum hop distance from any anchor
107
+ const hopDistance = new Map();
108
+ // Track visited edges to prevent cyclic re-traversal
109
+ const visitedEdges = new Set();
110
+ // Total edges traversed for telemetry
111
+ let totalEdgesTraversed = 0;
112
+ // Initialize with anchor scores
113
+ for (const [id, score] of anchors) {
114
+ activeNodes.set(id, score);
115
+ hopDistance.set(id, 0);
116
+ }
117
+ // Short-circuit: no iterations = return anchors as-is
118
+ if (cfg.iterations <= 0 || anchors.size === 0) {
119
+ const results = buildResults(activeNodes, anchors, hopDistance, cfg.lateralInhibition);
120
+ return {
121
+ results,
122
+ telemetry: buildTelemetry(results, anchors, 0, 0, startMs),
123
+ };
124
+ }
125
+ // ─── Propagation Loop ────────────────────────────────────
126
+ for (let t = 0; t < cfg.iterations; t++) {
127
+ const currentIds = Array.from(activeNodes.keys());
128
+ if (currentIds.length === 0)
129
+ break;
130
+ // Fetch ALL links connected to currently active nodes
131
+ // No global LIMIT — engine controls explosion via softCap
132
+ let edges;
133
+ try {
134
+ edges = await linkFetcher(currentIds);
135
+ }
136
+ catch (err) {
137
+ debugLog(`[synapse] Link fetch failed at iteration ${t} (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
138
+ break;
139
+ }
140
+ totalEdgesTraversed += edges.length;
141
+ // Compute out-degree per active source node (for fan effect)
142
+ const outDegree = new Map();
143
+ for (const edge of edges) {
144
+ if (activeNodes.has(edge.source_id)) {
145
+ outDegree.set(edge.source_id, (outDegree.get(edge.source_id) || 0) + 1);
146
+ }
147
+ }
148
+ // Next-iteration activation: starts with current values (activation persists)
149
+ const nextNodes = new Map(activeNodes);
150
+ for (const edge of edges) {
151
+ const edgeKey = `${edge.source_id}->${edge.target_id}`;
152
+ const strength = Number.isFinite(edge.strength) ? Math.max(0, Math.min(1, edge.strength)) : 0;
153
+ // ── Forward flow: source is active, flows to target ──
154
+ if (activeNodes.has(edge.source_id) && !visitedEdges.has(edgeKey)) {
155
+ visitedEdges.add(edgeKey);
156
+ const sourceEnergy = activeNodes.get(edge.source_id);
157
+ const degree = outDegree.get(edge.source_id) || 1;
158
+ // Dampened fan effect: prevents hub nodes from broadcasting equally
159
+ const dampedFan = Math.log(degree + Math.E);
160
+ const flow = cfg.spreadFactor * (strength * sourceEnergy / dampedFan);
161
+ nextNodes.set(edge.target_id, (nextNodes.get(edge.target_id) || 0) + flow);
162
+ // Track hop distance (minimum)
163
+ const sourceHops = hopDistance.get(edge.source_id) ?? 0;
164
+ const currentTargetHops = hopDistance.get(edge.target_id);
165
+ if (currentTargetHops === undefined || sourceHops + 1 < currentTargetHops) {
166
+ hopDistance.set(edge.target_id, sourceHops + 1);
167
+ }
168
+ }
169
+ // ── Backward flow: target is active, flows backward to source at 50% ──
170
+ const reverseEdgeKey = `${edge.target_id}->${edge.source_id}`;
171
+ if (activeNodes.has(edge.target_id) && !visitedEdges.has(reverseEdgeKey)) {
172
+ visitedEdges.add(reverseEdgeKey);
173
+ const targetEnergy = activeNodes.get(edge.target_id);
174
+ const flow = (cfg.spreadFactor * 0.5) * (strength * targetEnergy);
175
+ nextNodes.set(edge.source_id, (nextNodes.get(edge.source_id) || 0) + flow);
176
+ // Track hop distance for backward discoveries
177
+ const targetHops = hopDistance.get(edge.target_id) ?? 0;
178
+ const currentSourceHops = hopDistance.get(edge.source_id);
179
+ if (currentSourceHops === undefined || targetHops + 1 < currentSourceHops) {
180
+ hopDistance.set(edge.source_id, targetHops + 1);
181
+ }
182
+ }
183
+ }
184
+ // Soft lateral inhibition: keep only top-softCap nodes to prevent explosion
185
+ const sorted = Array.from(nextNodes.entries()).sort((a, b) => b[1] - a[1]);
186
+ activeNodes = new Map(sorted.slice(0, cfg.softCap));
187
+ }
188
+ // ─── Final Output ────────────────────────────────────────
189
+ const results = buildResults(activeNodes, anchors, hopDistance, cfg.lateralInhibition);
190
+ return {
191
+ results,
192
+ telemetry: buildTelemetry(results, anchors, cfg.iterations, totalEdgesTraversed, startMs),
193
+ };
194
+ }
195
+ // ─── Helpers ──────────────────────────────────────────────────
196
+ function buildResults(activeNodes, anchors, hopDistance, lateralInhibition) {
197
+ // Sort by activation energy descending, then apply hard lateral inhibition
198
+ const sorted = Array.from(activeNodes.entries())
199
+ .sort((a, b) => b[1] - a[1])
200
+ .slice(0, lateralInhibition);
201
+ return sorted.map(([id, energy]) => ({
202
+ id,
203
+ activationEnergy: energy,
204
+ hopsFromAnchor: hopDistance.get(id) ?? 0,
205
+ isDiscovered: !anchors.has(id),
206
+ }));
207
+ }
208
+ function buildTelemetry(results, anchors, iterations, edgesTraversed, startMs) {
209
+ const energies = results.map(r => r.activationEnergy);
210
+ const discovered = results.filter(r => !anchors.has(r.id)).length;
211
+ return {
212
+ nodesReturned: results.length,
213
+ nodesDiscovered: discovered,
214
+ maxActivationEnergy: energies.length > 0 ? Math.max(...energies) : 0,
215
+ avgActivationEnergy: energies.length > 0
216
+ ? energies.reduce((a, b) => a + b, 0) / energies.length
217
+ : 0,
218
+ iterationsPerformed: iterations,
219
+ edgesTraversed,
220
+ durationMs: Math.round(performance.now() - startMs),
221
+ };
222
+ }
@@ -66,6 +66,7 @@ let testMe = createFreshTestMeMetrics();
66
66
  let schedulerSynthesis = createFreshSchedulerMetrics();
67
67
  let pruning = createFreshPruningMetrics();
68
68
  let cognitive = createFreshCognitiveMetrics();
69
+ let synapse = createFreshSynapseMetrics();
69
70
  function createFreshSynthesisMetrics() {
70
71
  return {
71
72
  runs_total: 0,
@@ -137,6 +138,18 @@ function createFreshCognitiveMetrics() {
137
138
  duration_buffer: new DurationBuffer(),
138
139
  };
139
140
  }
141
+ function createFreshSynapseMetrics() {
142
+ return {
143
+ evaluations_total: 0,
144
+ nodes_returned_last: 0,
145
+ nodes_discovered_last: 0,
146
+ edges_traversed_last: 0,
147
+ iterations_performed_last: 0,
148
+ max_activation_last: 0,
149
+ last_run_at: null,
150
+ duration_buffer: new DurationBuffer(),
151
+ };
152
+ }
140
153
  function emitGraphEvent(event) {
141
154
  try {
142
155
  debugLog(JSON.stringify(event));
@@ -298,6 +311,26 @@ export function recordCognitiveRoute(data) {
298
311
  duration_ms: data.duration_ms,
299
312
  });
300
313
  }
314
+ export function recordSynapseTelemetry(data) {
315
+ const now = new Date().toISOString();
316
+ synapse.evaluations_total++;
317
+ synapse.last_run_at = now;
318
+ synapse.nodes_returned_last = data.nodesReturned;
319
+ synapse.nodes_discovered_last = data.nodesDiscovered;
320
+ synapse.edges_traversed_last = data.edgesTraversed;
321
+ synapse.iterations_performed_last = data.iterationsPerformed;
322
+ synapse.max_activation_last = data.maxActivationEnergy;
323
+ synapse.duration_buffer.push(data.durationMs);
324
+ emitGraphEvent({
325
+ event: "synapse_propagation",
326
+ nodes_returned: data.nodesReturned,
327
+ nodes_discovered: data.nodesDiscovered,
328
+ edges_traversed: data.edgesTraversed,
329
+ iterations: data.iterationsPerformed,
330
+ max_activation: data.maxActivationEnergy,
331
+ duration_ms: data.durationMs,
332
+ });
333
+ }
301
334
  // ─── Warning Flag Computation ────────────────────────────────────
302
335
  function computeWarningFlags() {
303
336
  // Quality warning: >85% of candidates are below threshold (min 50 candidates)
@@ -419,6 +452,16 @@ export function getGraphMetricsSnapshot() {
419
452
  last_confidence: cognitive.last_confidence,
420
453
  duration_p50_ms: cognitive.duration_buffer.getP50(),
421
454
  },
455
+ synapse: {
456
+ evaluations_total: synapse.evaluations_total,
457
+ nodes_returned_last: synapse.nodes_returned_last,
458
+ nodes_discovered_last: synapse.nodes_discovered_last,
459
+ edges_traversed_last: synapse.edges_traversed_last,
460
+ iterations_performed_last: synapse.iterations_performed_last,
461
+ max_activation_last: synapse.max_activation_last,
462
+ last_run_at: synapse.last_run_at,
463
+ duration_p50_ms: synapse.duration_buffer.getP50(),
464
+ },
422
465
  slo: computeSloMetrics(),
423
466
  warnings: computeWarningFlags(),
424
467
  };
@@ -430,6 +473,7 @@ export function resetGraphMetricsForTests() {
430
473
  schedulerSynthesis = createFreshSchedulerMetrics();
431
474
  pruning = createFreshPruningMetrics();
432
475
  cognitive = createFreshCognitiveMetrics();
476
+ synapse = createFreshSynapseMetrics();
433
477
  sweepDurationMsLast = 0;
434
478
  sweepLastAt = null;
435
479
  }
@@ -21,7 +21,7 @@ import * as path from "path";
21
21
  import * as os from "os";
22
22
  import { randomUUID } from "crypto";
23
23
  import { AccessLogBuffer } from "../utils/accessLogBuffer.js";
24
- import { PRISM_ACTR_BUFFER_FLUSH_MS } from "../config.js";
24
+ import { PRISM_ACTR_BUFFER_FLUSH_MS, PRISM_SYNAPSE_ENABLED, PRISM_SYNAPSE_ITERATIONS, PRISM_SYNAPSE_SPREAD_FACTOR, PRISM_SYNAPSE_LATERAL_INHIBITION, PRISM_SYNAPSE_SOFT_CAP, } from "../config.js";
25
25
  import { getSetting as cfgGet, setSetting as cfgSet, getAllSettings as cfgGetAll } from "./configStorage.js";
26
26
  import { debugLog } from "../utils/logger.js";
27
27
  import { SafetyController } from "../darkfactory/safetyController.js";
@@ -1388,8 +1388,7 @@ export class SqliteStorage {
1388
1388
  decisions: r.decisions,
1389
1389
  files_changed: r.files_changed,
1390
1390
  }));
1391
- const { applySpreadingActivation } = await import("../memory/spreadingActivation.js");
1392
- const activated = await applySpreadingActivation(this.db, mappedAnchors, params.activation, params.userId);
1391
+ const activated = await this.applySynapse(mappedAnchors, params.activation, params.userId);
1393
1392
  return { count: activated.length, results: activated };
1394
1393
  }
1395
1394
  return { count: results.length, results };
@@ -1461,8 +1460,7 @@ export class SqliteStorage {
1461
1460
  decisions: r.decisions,
1462
1461
  files_changed: r.files_changed,
1463
1462
  }));
1464
- const { applySpreadingActivation } = await import("../memory/spreadingActivation.js");
1465
- const activated = await applySpreadingActivation(this.db, mappedAnchors, params.activation, params.userId);
1463
+ const activated = await this.applySynapse(mappedAnchors, params.activation, params.userId);
1466
1464
  return { count: activated.length, results: activated };
1467
1465
  }
1468
1466
  return { count: results.length, results };
@@ -1514,8 +1512,7 @@ export class SqliteStorage {
1514
1512
  last_accessed_at: r.last_accessed_at || null,
1515
1513
  }));
1516
1514
  if (params.activation?.enabled) {
1517
- const { applySpreadingActivation } = await import("../memory/spreadingActivation.js");
1518
- return applySpreadingActivation(this.db, baseResults, params.activation, params.userId);
1515
+ return this.applySynapse(baseResults, params.activation, params.userId);
1519
1516
  }
1520
1517
  return baseResults;
1521
1518
  }
@@ -1608,8 +1605,7 @@ export class SqliteStorage {
1608
1605
  debugLog(`[SqliteStorage] Tier-2 TurboQuant fallback: scored ${fallbackResult.rows.length} entries, ` +
1609
1606
  `${scored.length} above threshold`);
1610
1607
  if (params.activation?.enabled) {
1611
- const { applySpreadingActivation } = await import("../memory/spreadingActivation.js");
1612
- return applySpreadingActivation(this.db, baseResults, params.activation, params.userId);
1608
+ return this.applySynapse(baseResults, params.activation, params.userId);
1613
1609
  }
1614
1610
  return baseResults;
1615
1611
  }
@@ -1621,6 +1617,67 @@ export class SqliteStorage {
1621
1617
  }
1622
1618
  }
1623
1619
  }
1620
+ // ─── Synapse Engine Integration ────────────────────────────────
1621
+ async applySynapse(anchors, options, userId) {
1622
+ if (!PRISM_SYNAPSE_ENABLED || !options.enabled || anchors.length === 0)
1623
+ return anchors;
1624
+ try {
1625
+ const { propagateActivation, normalizeActivationEnergy } = await import("../memory/synapseEngine.js");
1626
+ const { recordSynapseTelemetry } = await import("../observability/graphMetrics.js");
1627
+ const anchorMap = new Map();
1628
+ for (const a of anchors)
1629
+ anchorMap.set(a.id, a.similarity ?? 1.0);
1630
+ const { results, telemetry } = await propagateActivation(anchorMap, async (nodeIds) => this.getLinksForNodes(nodeIds, userId), {
1631
+ iterations: options.iterations ?? PRISM_SYNAPSE_ITERATIONS,
1632
+ spreadFactor: options.spreadFactor ?? PRISM_SYNAPSE_SPREAD_FACTOR,
1633
+ lateralInhibition: options.lateralInhibition ?? PRISM_SYNAPSE_LATERAL_INHIBITION,
1634
+ softCap: PRISM_SYNAPSE_SOFT_CAP,
1635
+ });
1636
+ recordSynapseTelemetry(telemetry);
1637
+ const fullNodeMap = new Map();
1638
+ for (const a of anchors)
1639
+ fullNodeMap.set(a.id, a);
1640
+ const finalIds = results.map(r => r.id);
1641
+ const missingIds = finalIds.filter(id => !fullNodeMap.has(id));
1642
+ if (missingIds.length > 0) {
1643
+ const placeholders = missingIds.map(() => '?').join(',');
1644
+ const missingQuery = `
1645
+ SELECT id, project, summary, session_date, decisions, files_changed
1646
+ FROM session_ledger
1647
+ WHERE id IN (${placeholders}) AND deleted_at IS NULL AND user_id = ?
1648
+ `;
1649
+ const missingRes = await this.db.execute({ sql: missingQuery, args: [...missingIds, userId] });
1650
+ for (const row of missingRes.rows) {
1651
+ fullNodeMap.set(row.id, {
1652
+ id: row.id,
1653
+ project: row.project,
1654
+ summary: row.summary,
1655
+ session_date: row.session_date,
1656
+ decisions: this.parseJsonColumn(row.decisions),
1657
+ files_changed: this.parseJsonColumn(row.files_changed),
1658
+ similarity: 0.0,
1659
+ });
1660
+ }
1661
+ }
1662
+ const finalResults = [];
1663
+ for (const r of results) {
1664
+ if (fullNodeMap.has(r.id)) {
1665
+ const node = fullNodeMap.get(r.id);
1666
+ const normEnergy = normalizeActivationEnergy(r.activationEnergy);
1667
+ node.activationScore = normEnergy;
1668
+ node.rawActivationEnergy = r.activationEnergy;
1669
+ // Hybrid blend: 70% original match relevance, 30% structural energy
1670
+ node.hybridScore = (node.similarity * 0.7) + (normEnergy * 0.3);
1671
+ finalResults.push(node);
1672
+ }
1673
+ }
1674
+ return finalResults.sort((a, b) => (b.hybridScore || 0) - (a.hybridScore || 0));
1675
+ }
1676
+ catch (err) {
1677
+ debugLog(`[SqliteStorage] applySynapse failed (non-fatal, returning original anchors): ${err instanceof Error ? err.message : String(err)}`);
1678
+ return anchors;
1679
+ }
1680
+ }
1624
1681
  // ─── Compaction ────────────────────────────────────────────
1625
1682
  async getCompactionCandidates(threshold, keepRecent, userId) {
1626
1683
  const result = await this.db.execute({
@@ -2564,6 +2621,27 @@ export class SqliteStorage {
2564
2621
  });
2565
2622
  return (result.rowsAffected ?? 0) > 0;
2566
2623
  }
2624
+ async getLinksForNodes(nodeIds, userId) {
2625
+ if (nodeIds.length === 0)
2626
+ return [];
2627
+ const placeholders = nodeIds.map(() => "?").join(", ");
2628
+ const sql = `
2629
+ SELECT m.source_id, m.target_id, m.strength
2630
+ FROM memory_links m
2631
+ JOIN session_ledger s ON m.source_id = s.id
2632
+ JOIN session_ledger t ON m.target_id = t.id
2633
+ WHERE (m.source_id IN (${placeholders}) OR m.target_id IN (${placeholders}))
2634
+ AND s.user_id = ? AND s.deleted_at IS NULL AND s.archived_at IS NULL
2635
+ AND t.user_id = ? AND t.deleted_at IS NULL AND t.archived_at IS NULL
2636
+ `;
2637
+ const args = [...nodeIds, ...nodeIds, userId, userId];
2638
+ const result = await this.db.execute({ sql, args });
2639
+ return result.rows.map(r => ({
2640
+ source_id: r.source_id,
2641
+ target_id: r.target_id,
2642
+ strength: r.strength,
2643
+ }));
2644
+ }
2567
2645
  async getLinksFrom(sourceId, userId, minStrength = 0.0, limit = 25) {
2568
2646
  // JOIN session_ledger to enforce:
2569
2647
  // 1. Tenant isolation (target.user_id = userId)
@@ -15,7 +15,7 @@
15
15
  import { supabasePost, supabaseGet, supabaseRpc, supabasePatch, supabaseDelete, } from "../utils/supabaseApi.js";
16
16
  import { gzipSync, gunzipSync } from "node:zlib";
17
17
  import { debugLog } from "../utils/logger.js";
18
- import { PRISM_USER_ID } from "../config.js";
18
+ import { PRISM_USER_ID, PRISM_SYNAPSE_ENABLED, PRISM_SYNAPSE_ITERATIONS, PRISM_SYNAPSE_SPREAD_FACTOR, PRISM_SYNAPSE_LATERAL_INHIBITION, PRISM_SYNAPSE_SOFT_CAP, } from "../config.js";
19
19
  import { getSetting as cfgGet, setSetting as cfgSet, getAllSettings as cfgGetAll } from "./configStorage.js";
20
20
  import { runAutoMigrations } from "./supabaseMigrations.js";
21
21
  import { SafetyController } from "../darkfactory/safetyController.js";
@@ -221,6 +221,20 @@ export class SupabaseStorage {
221
221
  if (!data || !data.results || data.count === 0) {
222
222
  return null;
223
223
  }
224
+ // v8.0: Apply Synapse graph enrichment if activation is enabled
225
+ if (params.activation?.enabled && Array.isArray(data.results) && data.results.length > 0) {
226
+ const mappedAnchors = data.results.map((r) => ({
227
+ id: r.id,
228
+ project: r.project,
229
+ summary: r.summary,
230
+ similarity: 1.0, // FTS matches treated as 1.0 similarity
231
+ session_date: r.session_date,
232
+ decisions: Array.isArray(r.decisions) ? r.decisions : [],
233
+ files_changed: Array.isArray(r.files_changed) ? r.files_changed : [],
234
+ }));
235
+ const activated = await this.applySynapse(mappedAnchors, params.activation, params.userId);
236
+ return { count: activated.length, results: activated };
237
+ }
224
238
  return data;
225
239
  }
226
240
  catch (e) {
@@ -254,7 +268,11 @@ export class SupabaseStorage {
254
268
  p_user_id: params.userId,
255
269
  p_role: params.role || null,
256
270
  });
257
- return Array.isArray(result) ? result : [];
271
+ const baseResults = Array.isArray(result) ? result : [];
272
+ if (params.activation?.enabled && baseResults.length > 0) {
273
+ return this.applySynapse(baseResults, params.activation, params.userId);
274
+ }
275
+ return baseResults;
258
276
  }
259
277
  catch (tier1Err) {
260
278
  // ─── TIER 2 FALLBACK: TurboQuant JS-side scoring ─────────────────
@@ -305,7 +323,11 @@ export class SupabaseStorage {
305
323
  scored.sort((a, b) => b.similarity - a.similarity);
306
324
  debugLog(`[SupabaseStorage] Tier-2 TurboQuant fallback: scored ${rows.length} entries, ` +
307
325
  `${scored.length} above threshold`);
308
- return scored.slice(0, params.limit);
326
+ const tier2Results = scored.slice(0, params.limit);
327
+ if (params.activation?.enabled && tier2Results.length > 0) {
328
+ return this.applySynapse(tier2Results, params.activation, params.userId);
329
+ }
330
+ return tier2Results;
309
331
  }
310
332
  catch (tier2Err) {
311
333
  // Both tiers failed — return empty; caller falls through to FTS5
@@ -317,6 +339,79 @@ export class SupabaseStorage {
317
339
  }
318
340
  }
319
341
  }
342
+ // ─── Synapse Engine Integration (v8.0) ────────────────────────
343
+ //
344
+ // Storage-agnostic multi-hop activation enrichment.
345
+ // Mirrors SqliteStorage.applySynapse() — the Synapse engine itself
346
+ // is pure (zero I/O), receiving link data via the getLinksForNodes
347
+ // callback. Missing node metadata is fetched via Supabase REST.
348
+ async applySynapse(anchors, options, userId) {
349
+ if (!PRISM_SYNAPSE_ENABLED || !options.enabled || anchors.length === 0)
350
+ return anchors;
351
+ try {
352
+ const { propagateActivation, normalizeActivationEnergy } = await import("../memory/synapseEngine.js");
353
+ const { recordSynapseTelemetry } = await import("../observability/graphMetrics.js");
354
+ const anchorMap = new Map();
355
+ for (const a of anchors)
356
+ anchorMap.set(a.id, a.similarity ?? 1.0);
357
+ const { results, telemetry } = await propagateActivation(anchorMap, async (nodeIds) => this.getLinksForNodes(nodeIds, userId), {
358
+ iterations: options.iterations ?? PRISM_SYNAPSE_ITERATIONS,
359
+ spreadFactor: options.spreadFactor ?? PRISM_SYNAPSE_SPREAD_FACTOR,
360
+ lateralInhibition: options.lateralInhibition ?? PRISM_SYNAPSE_LATERAL_INHIBITION,
361
+ softCap: PRISM_SYNAPSE_SOFT_CAP,
362
+ });
363
+ recordSynapseTelemetry(telemetry);
364
+ // Build node lookup from anchor results
365
+ const fullNodeMap = new Map();
366
+ for (const a of anchors)
367
+ fullNodeMap.set(a.id, a);
368
+ // Fetch metadata for nodes discovered by propagation (not in original anchors)
369
+ const finalIds = results.map(r => r.id);
370
+ const missingIds = finalIds.filter(id => !fullNodeMap.has(id));
371
+ if (missingIds.length > 0) {
372
+ try {
373
+ const rows = await supabaseGet("session_ledger", {
374
+ id: `in.(${missingIds.join(",")})`,
375
+ user_id: `eq.${userId}`,
376
+ deleted_at: "is.null",
377
+ select: "id,project,summary,session_date,decisions,files_changed",
378
+ });
379
+ for (const row of (Array.isArray(rows) ? rows : [])) {
380
+ fullNodeMap.set(row.id, {
381
+ id: row.id,
382
+ project: row.project,
383
+ summary: row.summary,
384
+ session_date: row.session_date,
385
+ decisions: Array.isArray(row.decisions) ? row.decisions : [],
386
+ files_changed: Array.isArray(row.files_changed) ? row.files_changed : [],
387
+ similarity: 0.0,
388
+ });
389
+ }
390
+ }
391
+ catch (e) {
392
+ debugLog(`[SupabaseStorage] applySynapse: failed to fetch missing nodes: ${e instanceof Error ? e.message : String(e)}`);
393
+ }
394
+ }
395
+ // Compute hybrid scores and build final result set
396
+ const finalResults = [];
397
+ for (const r of results) {
398
+ if (fullNodeMap.has(r.id)) {
399
+ const node = fullNodeMap.get(r.id);
400
+ const normEnergy = normalizeActivationEnergy(r.activationEnergy);
401
+ node.activationScore = normEnergy;
402
+ node.rawActivationEnergy = r.activationEnergy;
403
+ // Hybrid blend: 70% original match relevance, 30% structural energy
404
+ node.hybridScore = (node.similarity * 0.7) + (normEnergy * 0.3);
405
+ finalResults.push(node);
406
+ }
407
+ }
408
+ return finalResults.sort((a, b) => (b.hybridScore || 0) - (a.hybridScore || 0));
409
+ }
410
+ catch (err) {
411
+ debugLog(`[SupabaseStorage] applySynapse failed (non-fatal, returning original anchors): ${err instanceof Error ? err.message : String(err)}`);
412
+ return anchors;
413
+ }
414
+ }
320
415
  // ─── Compaction ────────────────────────────────────────────
321
416
  async getCompactionCandidates(threshold, keepRecent, userId) {
322
417
  try {
@@ -994,6 +1089,26 @@ export class SupabaseStorage {
994
1089
  return [];
995
1090
  }
996
1091
  }
1092
+ async getLinksForNodes(nodeIds, userId) {
1093
+ if (!nodeIds || nodeIds.length === 0)
1094
+ return [];
1095
+ try {
1096
+ const result = await supabaseRpc("prism_get_links_for_nodes", {
1097
+ p_node_ids: nodeIds,
1098
+ p_user_id: userId
1099
+ });
1100
+ const rows = Array.isArray(result) ? result : [];
1101
+ return rows.map((r) => ({
1102
+ source_id: r.source_id,
1103
+ target_id: r.target_id,
1104
+ strength: r.strength
1105
+ }));
1106
+ }
1107
+ catch (e) {
1108
+ debugLog("[SupabaseStorage] getLinksForNodes failed: " + e.message);
1109
+ return [];
1110
+ }
1111
+ }
997
1112
  async countLinks(entryId, linkType) {
998
1113
  try {
999
1114
  const query = { source_id: `eq.${entryId}` };
@@ -51,7 +51,7 @@ const activeCompactions = new Set();
51
51
  * After saving, generates an embedding vector for the entry via fire-and-forget.
52
52
  */
53
53
  import { computeEffectiveImportance, recordMemoryAccess } from "../utils/cognitiveMemory.js";
54
- import { baseLevelActivation, candidateScopedSpreadingActivation, compositeRetrievalScore, } from "../utils/actrActivation.js";
54
+ import { baseLevelActivation, compositeRetrievalScore, } from "../utils/actrActivation.js";
55
55
  import { PRISM_ACTR_ENABLED, PRISM_ACTR_DECAY, PRISM_ACTR_WEIGHT_SIMILARITY, PRISM_ACTR_WEIGHT_ACTIVATION, PRISM_ACTR_SIGMOID_MIDPOINT, PRISM_ACTR_SIGMOID_STEEPNESS, PRISM_ACTR_MAX_ACCESSES_PER_ENTRY, } from "../config.js";
56
56
  import { HdcStateMachine } from "../sdm/stateMachine.js";
57
57
  import { ConceptDictionary } from "../sdm/conceptDictionary.js";
@@ -419,21 +419,8 @@ export async function sessionSearchMemoryHandler(args) {
419
419
  try {
420
420
  // Step A: Bulk-fetch access logs for all candidate IDs
421
421
  const accessLogMap = await storage.getAccessLog(resultIds, PRISM_ACTR_MAX_ACCESSES_PER_ENTRY);
422
- // Step B: Fetch outbound links for spreading activation
423
- const candidateIdSet = new Set(resultIds);
424
- const linksMap = new Map();
425
- for (const id of resultIds) {
426
- try {
427
- const links = await storage.getLinksFrom(id, PRISM_USER_ID, 0.0, 20);
428
- linksMap.set(id, links.map((l) => ({
429
- target_id: l.target_id,
430
- strength: l.strength ?? 1.0,
431
- })));
432
- }
433
- catch {
434
- linksMap.set(id, []);
435
- }
436
- }
422
+ // Step B: Removed. Synapse Engine (v8.0) handles multi-hop propagation
423
+ // at the storage layer when activation is enabled.
437
424
  // Step C: Compute activation for each result and re-rank
438
425
  actrMetrics = { baseLevels: [], spreadings: [], sigmoids: [], composites: [] };
439
426
  for (const r of results) {
@@ -450,9 +437,9 @@ export async function sessionSearchMemoryHandler(args) {
450
437
  // They should decay 50% slower than raw episodic chatter to retain long-term context.
451
438
  const decayRate = r.is_rollup ? PRISM_ACTR_DECAY * 0.5 : PRISM_ACTR_DECAY;
452
439
  const Bi = baseLevelActivation(timestamps, now, decayRate);
453
- // S_i: Candidate-scoped spreading activation
454
- const outboundLinks = linksMap.get(id) || [];
455
- const Si = candidateScopedSpreadingActivation(outboundLinks, candidateIdSet);
440
+ // S_i: Structural activation energy from Synapse logic
441
+ // (computed during applySynapse in storage layer)
442
+ const Si = (typeof r.rawActivationEnergy === 'number') ? r.rawActivationEnergy : 0;
456
443
  // Composite retrieval score
457
444
  const composite = compositeRetrievalScore(typeof r.similarity === "number" ? r.similarity : 0, Bi + Si, PRISM_ACTR_WEIGHT_SIMILARITY, PRISM_ACTR_WEIGHT_ACTIVATION, PRISM_ACTR_SIGMOID_MIDPOINT, PRISM_ACTR_SIGMOID_STEEPNESS);
458
445
  // Attach to result for re-sorting and display
@@ -111,6 +111,8 @@ export function baseLevelActivation(accessTimestamps, now, decayRate = ACT_R_DEF
111
111
  *
112
112
  * W = 1 / |candidateIds| (uniform attention weight across candidates)
113
113
  *
114
+ * @deprecated Use Synapse engine (v8.0) at the storage layer instead via `applySynapse()`.
115
+ *
114
116
  * @param outboundLinks - All outbound links from this memory entry
115
117
  * @param candidateIds - Set of entry IDs in the current search result set
116
118
  * @returns Spreading activation value (0 to ~1.0 range)
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Ollama Adapter (v1.0 — nomic-embed-text)
3
+ * ─────────────────────────────────────────────────────────────────────────────
4
+ * PURPOSE:
5
+ * Implements LLMProvider using Ollama's native /api/embed REST endpoint for
6
+ * fully local, zero-cost text embeddings. No API key required — Ollama runs
7
+ * on localhost.
8
+ *
9
+ * TEXT GENERATION:
10
+ * This adapter is embeddings-only. generateText() throws an explicit error.
11
+ * Set text_provider separately (anthropic, openai, or gemini).
12
+ *
13
+ * EMBEDDING DIMENSION PARITY (768 dims):
14
+ * Prism's SQLite (sqlite-vec) and Supabase (pgvector) schemas define
15
+ * embedding columns as EXACTLY 768 dimensions.
16
+ *
17
+ * nomic-embed-text natively outputs 768 dims — zero truncation needed.
18
+ * It is the recommended default local model for Prism.
19
+ *
20
+ * SUPPORTED MODELS (all confirmed 768-dim via Ollama):
21
+ * nomic-embed-text — 768 dims, 274MB, best quality/size trade-off ✅ DEFAULT
22
+ * nomic-embed-text:v1.5 — 768 dims, 274MB, same (stable alias)
23
+ *
24
+ * Models to AVOID with this adapter (wrong dim count):
25
+ * mxbai-embed-large — 1024 dims ❌ (use OpenAIAdapter instead)
26
+ * all-minilm — 384 dims ❌
27
+ * snowflake-arctic-embed — varies ❌
28
+ *
29
+ * BATCH EMBEDDINGS:
30
+ * Uses /api/embed (plural) which is the official Ollama batch endpoint
31
+ * introduced in Ollama ≥ 0.3.0. Falls back gracefully for older versions.
32
+ *
33
+ * CONFIG KEYS (Prism dashboard "AI Providers" tab OR environment variables):
34
+ * ollama_base_url — Base URL of Ollama server (default: http://localhost:11434)
35
+ * ollama_model — Embedding model (default: nomic-embed-text)
36
+ *
37
+ * USAGE:
38
+ * In the Prism dashboard, set:
39
+ * embedding_provider = ollama
40
+ * Optionally set ollama_base_url and ollama_model to override defaults.
41
+ *
42
+ * API REFERENCE:
43
+ * https://github.com/ollama/ollama/blob/main/docs/api.md#generate-embeddings
44
+ */
45
+ import { getSettingSync } from "../../../storage/configStorage.js";
46
+ import { debugLog } from "../../logger.js";
47
+ // ─── Constants ────────────────────────────────────────────────────────────────
48
+ // Must match Prism's DB schema (sqlite-vec and pgvector column sizes).
49
+ const EMBEDDING_DIMS = 768;
50
+ // Generous character cap — nomic-embed-text has an 8192-token context window.
51
+ const MAX_EMBEDDING_CHARS = 8000;
52
+ const DEFAULT_BASE_URL = "http://localhost:11434";
53
+ const DEFAULT_MODEL = "nomic-embed-text";
54
+ // Connection retry settings — handles the common "forgot to start Ollama" race.
55
+ const MAX_RETRIES = 2;
56
+ const RETRY_DELAY_MS = 500;
57
+ // ─── Adapter ─────────────────────────────────────────────────────────────────
58
+ export class OllamaAdapter {
59
+ baseUrl;
60
+ model;
61
+ constructor() {
62
+ this.baseUrl = getSettingSync("ollama_base_url", DEFAULT_BASE_URL).replace(/\/$/, "");
63
+ this.model = getSettingSync("ollama_model", DEFAULT_MODEL);
64
+ debugLog(`[OllamaAdapter] Initialized — baseUrl=${this.baseUrl}, model=${this.model}`);
65
+ }
66
+ // ─── Text Generation (Not Supported) ────────────────────────────────────
67
+ async generateText(_prompt, _systemInstruction) {
68
+ throw new Error("OllamaAdapter does not support text generation. " +
69
+ "Set text_provider to 'anthropic', 'openai', or 'gemini' in the dashboard.");
70
+ }
71
+ // ─── Batch Embedding Generation ─────────────────────────────────────────
72
+ async generateEmbeddings(texts) {
73
+ if (!texts || texts.length === 0)
74
+ return [];
75
+ const model = this.model;
76
+ // Word-safe truncation — consistent with Voyage and OpenAI adapters.
77
+ const truncatedTexts = texts.map(text => {
78
+ if (text.length > MAX_EMBEDDING_CHARS) {
79
+ const cut = text.slice(0, MAX_EMBEDDING_CHARS);
80
+ const lastSpace = cut.lastIndexOf(" ");
81
+ return lastSpace > 0 ? cut.slice(0, lastSpace) : cut;
82
+ }
83
+ return text;
84
+ });
85
+ debugLog(`[OllamaAdapter] generateEmbeddings — model=${model}, count=${truncatedTexts.length}`);
86
+ // Retry loop — catches ECONNREFUSED when Ollama service hasn't started yet.
87
+ let response;
88
+ let lastError = null;
89
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
90
+ try {
91
+ response = await fetch(`${this.baseUrl}/api/embed`, {
92
+ method: "POST",
93
+ headers: { "Content-Type": "application/json" },
94
+ body: JSON.stringify({ model, input: truncatedTexts }),
95
+ });
96
+ if (!response.ok) {
97
+ const errorText = await response.text().catch(() => "unknown error");
98
+ throw new Error(`[OllamaAdapter] /api/embed request failed — status=${response.status}: ${errorText}. ` +
99
+ `Make sure Ollama is running (ollama serve) and '${model}' has been pulled (ollama pull ${model}).`);
100
+ }
101
+ // Success — break out of retry loop.
102
+ lastError = null;
103
+ break;
104
+ }
105
+ catch (err) {
106
+ lastError = err instanceof Error ? err : new Error(String(err));
107
+ const isNetworkError = lastError.message.includes("ECONNREFUSED") ||
108
+ lastError.message.includes("fetch failed") ||
109
+ lastError.message.includes("ECONNRESET");
110
+ if (isNetworkError && attempt < MAX_RETRIES) {
111
+ debugLog(`[OllamaAdapter] Connection failed (attempt ${attempt + 1}/${MAX_RETRIES + 1}): ` +
112
+ `${lastError.message.substring(0, 80)}. Retrying in ${RETRY_DELAY_MS}ms...`);
113
+ await new Promise(resolve => setTimeout(resolve, RETRY_DELAY_MS));
114
+ continue;
115
+ }
116
+ throw lastError;
117
+ }
118
+ }
119
+ if (lastError)
120
+ throw lastError;
121
+ const data = (await response.json());
122
+ const embeddings = data?.embeddings;
123
+ if (!Array.isArray(embeddings) || embeddings.length === 0) {
124
+ throw new Error(`[OllamaAdapter] Empty embeddings response from model '${model}'.`);
125
+ }
126
+ if (embeddings.length !== texts.length) {
127
+ throw new Error(`[OllamaAdapter] Response length mismatch — expected ${texts.length}, got ${embeddings.length}.`);
128
+ }
129
+ // Validate dimensions and slice if model returned > 768 (shouldn't happen
130
+ // with nomic-embed-text but guards against model swaps).
131
+ return embeddings.map((emb, i) => {
132
+ if (emb.length > EMBEDDING_DIMS) {
133
+ debugLog(`[OllamaAdapter] Embedding[${i}] has ${emb.length} dims — truncating to ${EMBEDDING_DIMS}. ` +
134
+ `Consider using a model that natively outputs ${EMBEDDING_DIMS} dims (e.g. nomic-embed-text).`);
135
+ return emb.slice(0, EMBEDDING_DIMS);
136
+ }
137
+ if (emb.length !== EMBEDDING_DIMS) {
138
+ throw new Error(`[OllamaAdapter] Dimension mismatch at index ${i}: expected ${EMBEDDING_DIMS}, ` +
139
+ `got ${emb.length}. Model '${model}' is not compatible with Prism's 768-dim schema. ` +
140
+ `Use nomic-embed-text which natively outputs 768 dims.`);
141
+ }
142
+ return emb;
143
+ });
144
+ }
145
+ // ─── Single Embedding (delegates to batch) ───────────────────────────────
146
+ async generateEmbedding(text) {
147
+ if (!text || !text.trim()) {
148
+ throw new Error("[OllamaAdapter] generateEmbedding called with empty text.");
149
+ }
150
+ const results = await this.generateEmbeddings([text]);
151
+ return results[0];
152
+ }
153
+ }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * LLM Provider Factory (v4.5Voyage AI Embedding Support)
2
+ * LLM Provider Factory (v4.6Ollama Local Embedding Support)
3
3
  * ─────────────────────────────────────────────────────────────────────────────
4
4
  * PURPOSE:
5
5
  * Single point of resolution for the active LLMProvider.
@@ -11,7 +11,7 @@
11
11
  * Two independent settings control text and embedding routing:
12
12
  *
13
13
  * text_provider — "gemini" (default) | "openai" | "anthropic"
14
- * embedding_provider — "auto" (default) | "gemini" | "openai" | "voyage"
14
+ * embedding_provider — "auto" (default) | "gemini" | "openai" | "voyage" | "ollama"
15
15
  *
16
16
  * When embedding_provider = "auto":
17
17
  * * If text_provider is gemini or openai → use same provider for embeddings
@@ -24,8 +24,10 @@
24
24
  * text_provider=openai, embedding_provider=auto → OpenAI+OpenAI
25
25
  * text_provider=anthropic, embedding_provider=auto → Claude+Gemini (auto-bridge)
26
26
  * text_provider=anthropic, embedding_provider=voyage → Claude+Voyage (Anthropic-recommended)
27
- * text_provider=anthropic, embedding_provider=openai → Claude+Ollama (cost-optimized)
27
+ * text_provider=anthropic, embedding_provider=openai → Claude+OpenAI cloud embeddings
28
+ * text_provider=anthropic, embedding_provider=ollama → Claude+Ollama (fully local, zero-cost)
28
29
  * text_provider=gemini, embedding_provider=voyage → Gemini+Voyage (mixed)
30
+ * text_provider=gemini, embedding_provider=ollama → Gemini+Ollama (hybrid cloud/local)
29
31
  *
30
32
  * SINGLETON + GRACEFUL DEGRADATION:
31
33
  * Same as before — instance cached per process, errors fall back to Gemini.
@@ -44,6 +46,7 @@ import { GeminiAdapter } from "./adapters/gemini.js";
44
46
  import { OpenAIAdapter } from "./adapters/openai.js";
45
47
  import { AnthropicAdapter } from "./adapters/anthropic.js";
46
48
  import { VoyageAdapter } from "./adapters/voyage.js";
49
+ import { OllamaAdapter } from "./adapters/ollama.js";
47
50
  import { TracingLLMProvider } from "./adapters/traced.js";
48
51
  // Module-level singleton — one composed provider per MCP server process.
49
52
  let providerInstance = null;
@@ -62,10 +65,12 @@ function buildEmbeddingAdapter(type) {
62
65
  // Note: "anthropic" is intentionally absent from this switch.
63
66
  // Anthropic has no embedding API, so it can never be an embedding provider.
64
67
  // The factory resolves "auto" away from "anthropic" before calling this.
65
- // For Anthropic text users, "voyage" is the Anthropic-recommended pairing.
68
+ // For Anthropic text users, "voyage" is the recommended pairing;
69
+ // "ollama" is the fully local zero-cost alternative.
66
70
  switch (type) {
67
71
  case "openai": return new OpenAIAdapter();
68
72
  case "voyage": return new VoyageAdapter();
73
+ case "ollama": return new OllamaAdapter();
69
74
  case "gemini":
70
75
  default: return new GeminiAdapter();
71
76
  }
@@ -90,10 +95,15 @@ export function getLLMProvider() {
90
95
  let embedType = getSettingSync("embedding_provider", "auto");
91
96
  if (embedType === "auto") {
92
97
  if (process.env.VOYAGE_API_KEY) {
93
- // If Voyage is available, use it as the default embedding provider
94
- // since voyage-code-3 strongly outperforms general embeddings on code contexts.
98
+ // Voyage takes first priority when available voyage-code-3 strongly
99
+ // outperforms general embeddings on code contexts.
95
100
  embedType = "voyage";
96
101
  }
102
+ else if (process.env.OLLAMA_HOST || process.env.OLLAMA_BASE_URL) {
103
+ // Ollama is second priority: fully local, zero-cost, zero-latency.
104
+ // Activated when OLLAMA_HOST or OLLAMA_BASE_URL env var is set.
105
+ embedType = "ollama";
106
+ }
97
107
  else {
98
108
  // Anthropic has no embedding API — auto-bridge to Gemini.
99
109
  // For all other text providers, use the same provider for embeddings.
@@ -101,9 +111,9 @@ export function getLLMProvider() {
101
111
  if (textType === "anthropic") {
102
112
  console.error("[LLMFactory] text_provider=anthropic with embedding_provider=auto: " +
103
113
  "routing embeddings to GeminiAdapter (Anthropic has no native embedding API). " +
104
- "For the Anthropic-recommended pairing, set embedding_provider=voyage in the dashboard " +
105
- "(voyage-code-3 supports 768-dim output via MRL). " +
106
- "Alternatively, set embedding_provider=openai to use Ollama/OpenAI.");
114
+ "For the Anthropic-recommended pairing, set embedding_provider=voyage in the dashboard. " +
115
+ "For a fully local, zero-cost option, set embedding_provider=ollama " +
116
+ "(requires 'ollama pull nomic-embed-text').");
107
117
  }
108
118
  }
109
119
  }
@@ -16,6 +16,8 @@
16
16
  * - gemini.ts → Google Gemini (default; all methods including VLM)
17
17
  * - openai.ts → OpenAI Cloud + Ollama + LM Studio + vLLM
18
18
  * - anthropic.ts → Anthropic Claude (VLM supported; embeddings unsupported)
19
+ * - voyage.ts → Voyage AI (embeddings only; Anthropic-recommended pairing)
20
+ * - ollama.ts → Ollama native /api/embed (embeddings only; fully local, zero-cost)
19
21
  *
20
22
  * FACTORY RESOLUTION:
21
23
  * Never instantiate adapters directly. Always call:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prism-mcp-server",
3
- "version": "7.8.7",
3
+ "version": "8.0.0",
4
4
  "mcpName": "io.github.dcostenco/prism-mcp",
5
5
  "description": "The Mind Palace for AI Agents — a true Cognitive Architecture with Hebbian learning (episodic→semantic consolidation), ACT-R spreading activation (multi-hop causal reasoning), uncertainty-aware rejection gates (agents that know when they don't know), adversarial evaluation (anti-sycophancy), fail-closed Dark Factory pipelines, persistent memory (SQLite/Supabase), multi-agent Hivemind, time travel & visual dashboard. Zero-config local mode.",
6
6
  "module": "index.ts",