prism-mcp-server 7.8.8 → 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 +62 -7
- package/dist/config.js +14 -0
- package/dist/memory/synapseEngine.js +222 -0
- package/dist/observability/graphMetrics.js +44 -0
- package/dist/storage/sqlite.js +87 -9
- package/dist/storage/supabase.js +118 -3
- package/dist/tools/graphHandlers.js +6 -19
- package/dist/utils/actrActivation.js +2 -0
- package/package.json +1 -1
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
|
|
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.
|
|
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:
|
|
742
|
+
> **Current release: v8.0.0 — Synapse Engine**
|
|
695
743
|
|
|
696
|
-
-
|
|
744
|
+
- ⚡ **v8.0.0 — Synapse 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 (
|
|
968
|
-
│ │ •
|
|
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:
|
|
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);
|
|
@@ -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
|
}
|
package/dist/storage/sqlite.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
package/dist/storage/supabase.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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,
|
|
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:
|
|
423
|
-
|
|
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:
|
|
454
|
-
|
|
455
|
-
const Si =
|
|
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)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "prism-mcp-server",
|
|
3
|
-
"version": "
|
|
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",
|