prism-mcp-server 8.0.2 → 9.0.4
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 +80 -9
- package/dist/cli.js +0 -0
- package/dist/config.js +42 -5
- package/dist/dashboard/server.js +118 -0
- package/dist/dashboard/ui.js +211 -1
- package/dist/memory/cognitiveBudget.js +224 -0
- package/dist/memory/surprisalGate.js +119 -0
- package/dist/memory/synapseEngine.js +28 -2
- package/dist/memory/valenceEngine.js +234 -0
- package/dist/scholar/webScholar.js +7 -6
- package/dist/server.js +60 -19
- package/dist/storage/index.js +53 -9
- package/dist/storage/sqlite.js +103 -11
- package/dist/storage/supabase.js +74 -5
- package/dist/storage/supabaseMigrations.js +30 -0
- package/dist/sync/factory.js +5 -1
- package/dist/tools/graphHandlers.js +24 -2
- package/dist/tools/ledgerHandlers.js +122 -4
- package/dist/utils/universalImporter.js +0 -0
- package/package.json +13 -3
- package/dist/dashboard/ui.tmp.js +0 -3475
- package/dist/test-cli.js +0 -18
- package/dist/tools/sessionMemoryHandlers.js +0 -2633
- package/dist/utils/embeddingApi.js +0 -104
- package/dist/utils/googleAi.js +0 -88
- package/dist/utils/testUniversalImporter.js +0 -10
- package/dist/verification/renameDetector.js +0 -170
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Valence Engine — Affect-Tagged Memory (v9.0)
|
|
3
|
+
*
|
|
4
|
+
* ═══════════════════════════════════════════════════════════════════
|
|
5
|
+
* PURPOSE:
|
|
6
|
+
* Implements Affective Cognitive Routing — every memory gets a
|
|
7
|
+
* "gut feeling" score from -1.0 (trauma) to +1.0 (success).
|
|
8
|
+
* Agents get warned when approaching historically problematic
|
|
9
|
+
* topics, and get green-light signals for proven-successful paths.
|
|
10
|
+
*
|
|
11
|
+
* AFFECTIVE SALIENCE PRINCIPLE:
|
|
12
|
+
* In human psychology, highly emotional memories — both extreme
|
|
13
|
+
* joy and extreme trauma — are retrieved MORE easily, not less.
|
|
14
|
+
* Therefore, the retrieval score uses |valence| (absolute magnitude)
|
|
15
|
+
* to BOOST salience, while the SIGN (±) is used purely for
|
|
16
|
+
* prompt injection / UX warnings.
|
|
17
|
+
*
|
|
18
|
+
* This prevents the Valence Retrieval Paradox where a failure
|
|
19
|
+
* memory gets pushed below the retrieval threshold, causing the
|
|
20
|
+
* agent to repeat the exact same mistake.
|
|
21
|
+
*
|
|
22
|
+
* DESIGN:
|
|
23
|
+
* All functions are PURE — zero I/O, zero imports from storage.
|
|
24
|
+
* Valence propagation through the Synapse graph uses energy-weighted
|
|
25
|
+
* transfer with fan-dampened flow and strict [-1, 1] clamping.
|
|
26
|
+
*
|
|
27
|
+
* FILES THAT IMPORT THIS:
|
|
28
|
+
* - src/storage/sqlite.ts (auto-derive valence on save)
|
|
29
|
+
* - src/tools/graphHandlers.ts (hybrid scoring + UX warnings)
|
|
30
|
+
* - src/memory/synapseEngine.ts (valence propagation)
|
|
31
|
+
* ═══════════════════════════════════════════════════════════════════
|
|
32
|
+
*/
|
|
33
|
+
// ─── Valence Derivation ───────────────────────────────────────
|
|
34
|
+
/**
|
|
35
|
+
* Deterministic mapping from experience event type to valence score.
|
|
36
|
+
*
|
|
37
|
+
* | Event Type | Valence | Rationale |
|
|
38
|
+
* |---------------------|---------|------------------------------------|
|
|
39
|
+
* | success | +0.8 | Positive reinforcement |
|
|
40
|
+
* | failure | -0.8 | Strong negative signal |
|
|
41
|
+
* | correction | -0.6 | User had to fix agent |
|
|
42
|
+
* | learning | +0.4 | New knowledge acquired |
|
|
43
|
+
* | validation_result | ±0.6 | Pass → +0.6, Fail → -0.6 |
|
|
44
|
+
* | session / default | 0.0 | Neutral — no sentiment signal |
|
|
45
|
+
*
|
|
46
|
+
* @param eventType - The experience event type from session_ledger
|
|
47
|
+
* @param notes - Optional notes field (for validation_result pass/fail)
|
|
48
|
+
* @returns Valence score in [-1.0, +1.0]
|
|
49
|
+
*/
|
|
50
|
+
export function deriveValence(eventType, notes) {
|
|
51
|
+
if (!eventType || eventType === 'session')
|
|
52
|
+
return 0.0;
|
|
53
|
+
switch (eventType) {
|
|
54
|
+
case 'success':
|
|
55
|
+
return 0.8;
|
|
56
|
+
case 'failure':
|
|
57
|
+
return -0.8;
|
|
58
|
+
case 'correction':
|
|
59
|
+
return -0.6;
|
|
60
|
+
case 'learning':
|
|
61
|
+
return 0.4;
|
|
62
|
+
case 'validation_result':
|
|
63
|
+
// Check notes for pass/fail indication
|
|
64
|
+
if (notes) {
|
|
65
|
+
const lower = notes.toLowerCase();
|
|
66
|
+
if (lower.includes('pass') || lower.includes('success') || lower.includes('green')) {
|
|
67
|
+
return 0.6;
|
|
68
|
+
}
|
|
69
|
+
if (lower.includes('fail') || lower.includes('error') || lower.includes('blocked')) {
|
|
70
|
+
return -0.6;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// Ambiguous validation result → slightly negative (cautious)
|
|
74
|
+
return -0.2;
|
|
75
|
+
default:
|
|
76
|
+
return 0.0;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
// ─── Retrieval Salience (Magnitude-Based) ─────────────────────
|
|
80
|
+
/**
|
|
81
|
+
* Compute the retrieval salience boost from valence.
|
|
82
|
+
*
|
|
83
|
+
* Uses ABSOLUTE MAGNITUDE — both extreme positive and extreme negative
|
|
84
|
+
* memories are more salient (more retrievable). The sign is preserved
|
|
85
|
+
* separately for UX warnings.
|
|
86
|
+
*
|
|
87
|
+
* @param valence - Raw valence score in [-1.0, +1.0]
|
|
88
|
+
* @returns Salience boost in [0.0, 1.0]
|
|
89
|
+
*/
|
|
90
|
+
export function valenceSalience(valence) {
|
|
91
|
+
if (valence == null || !Number.isFinite(valence))
|
|
92
|
+
return 0.0;
|
|
93
|
+
return Math.min(1.0, Math.abs(valence));
|
|
94
|
+
}
|
|
95
|
+
// ─── UX Warning / Signal Tags ─────────────────────────────────
|
|
96
|
+
/**
|
|
97
|
+
* Format a valence score into a human-readable emoji tag for display
|
|
98
|
+
* in search results and context output.
|
|
99
|
+
*
|
|
100
|
+
* @param valence - Raw valence score in [-1.0, +1.0]
|
|
101
|
+
* @returns Emoji tag string, or empty string for neutral
|
|
102
|
+
*/
|
|
103
|
+
export function formatValenceTag(valence) {
|
|
104
|
+
if (valence == null || !Number.isFinite(valence))
|
|
105
|
+
return '';
|
|
106
|
+
if (valence <= -0.5)
|
|
107
|
+
return '🔴';
|
|
108
|
+
if (valence <= -0.2)
|
|
109
|
+
return '🟠';
|
|
110
|
+
if (valence >= 0.5)
|
|
111
|
+
return '🟢';
|
|
112
|
+
if (valence >= 0.2)
|
|
113
|
+
return '🔵';
|
|
114
|
+
return '🟡'; // Neutral zone (-0.2 to +0.2)
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Determine if a set of retrieved memories should trigger a
|
|
118
|
+
* negative valence warning in the response.
|
|
119
|
+
*
|
|
120
|
+
* @param avgValence - Average valence across top results
|
|
121
|
+
* @param threshold - Warning threshold (default: -0.3)
|
|
122
|
+
* @returns true if the agent should be warned about historical friction
|
|
123
|
+
*/
|
|
124
|
+
export function shouldWarnNegativeValence(avgValence, threshold = -0.3) {
|
|
125
|
+
return Number.isFinite(avgValence) && avgValence < threshold;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Generate a contextual warning message based on average valence.
|
|
129
|
+
*
|
|
130
|
+
* @param avgValence - Average valence across top results
|
|
131
|
+
* @returns Warning/signal string to inject into MCP response, or null
|
|
132
|
+
*/
|
|
133
|
+
export function generateValenceWarning(avgValence) {
|
|
134
|
+
if (!Number.isFinite(avgValence))
|
|
135
|
+
return null;
|
|
136
|
+
if (avgValence < -0.5) {
|
|
137
|
+
return '⚠️ **Caution:** This topic is strongly correlated with historical failures and corrections. Consider reviewing past decisions before proceeding.';
|
|
138
|
+
}
|
|
139
|
+
if (avgValence < -0.3) {
|
|
140
|
+
return '⚠️ **Warning:** This area has mixed historical outcomes. Approach with awareness of prior friction.';
|
|
141
|
+
}
|
|
142
|
+
if (avgValence > 0.5) {
|
|
143
|
+
return '🟢 **High Signal:** This path has historically led to successful outcomes.';
|
|
144
|
+
}
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Propagate valence through Synapse activation results.
|
|
149
|
+
*
|
|
150
|
+
* Each node's propagated valence is computed as the energy-weighted
|
|
151
|
+
* average of its sources' valence, with fan-dampening to prevent
|
|
152
|
+
* hub explosion. The final value is strictly clamped to [-1.0, +1.0].
|
|
153
|
+
*
|
|
154
|
+
* IMPORTANT — Fan-Dampening:
|
|
155
|
+
* If 50 neutral nodes point to 1 negative node, the negative valence
|
|
156
|
+
* must NOT multiply to -50.0. The incoming valence is averaged over
|
|
157
|
+
* the fan-in count, then clamped.
|
|
158
|
+
*
|
|
159
|
+
* Algorithm:
|
|
160
|
+
* For each non-anchor node with incoming energy flows:
|
|
161
|
+
* propagatedValence = Σ(flow_weight × source_valence) / Σ(flow_weight)
|
|
162
|
+
* Clamped to [-1.0, +1.0].
|
|
163
|
+
*
|
|
164
|
+
* Anchor nodes retain their original valence unchanged.
|
|
165
|
+
*
|
|
166
|
+
* @param synapseResults - Node IDs with their activation energy from Synapse
|
|
167
|
+
* @param valenceLookup - Map from entry ID → raw valence (from DB)
|
|
168
|
+
* @param flowWeights - Map from `targetId` → Array<{ sourceId, weight }> representing
|
|
169
|
+
* the energy flows that contributed to each node's activation
|
|
170
|
+
* @returns Map from entry ID → propagated valence
|
|
171
|
+
*/
|
|
172
|
+
export function propagateValence(synapseResults, valenceLookup, flowWeights) {
|
|
173
|
+
const result = new Map();
|
|
174
|
+
for (const node of synapseResults) {
|
|
175
|
+
// Anchor nodes: use their direct valence
|
|
176
|
+
if (!node.isDiscovered) {
|
|
177
|
+
const directValence = valenceLookup.get(node.id) ?? 0.0;
|
|
178
|
+
result.set(node.id, clampValence(directValence));
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
// Discovered nodes: compute energy-weighted average from source flows
|
|
182
|
+
const flows = flowWeights?.get(node.id);
|
|
183
|
+
if (!flows || flows.length === 0) {
|
|
184
|
+
// No flow data → use direct valence if available, else neutral
|
|
185
|
+
result.set(node.id, clampValence(valenceLookup.get(node.id) ?? 0.0));
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
let weightedValenceSum = 0;
|
|
189
|
+
let totalWeight = 0;
|
|
190
|
+
for (const flow of flows) {
|
|
191
|
+
const sourceValence = valenceLookup.get(flow.sourceId) ?? result.get(flow.sourceId) ?? 0.0;
|
|
192
|
+
const absWeight = Math.abs(flow.weight);
|
|
193
|
+
weightedValenceSum += absWeight * sourceValence;
|
|
194
|
+
totalWeight += absWeight;
|
|
195
|
+
}
|
|
196
|
+
const propagated = totalWeight > 0 ? weightedValenceSum / totalWeight : 0.0;
|
|
197
|
+
result.set(node.id, clampValence(propagated));
|
|
198
|
+
}
|
|
199
|
+
return result;
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Clamp a valence value to the valid range [-1.0, +1.0].
|
|
203
|
+
* Returns 0.0 for non-finite values.
|
|
204
|
+
*/
|
|
205
|
+
export function clampValence(v) {
|
|
206
|
+
if (!Number.isFinite(v))
|
|
207
|
+
return 0.0;
|
|
208
|
+
return Math.max(-1.0, Math.min(1.0, v));
|
|
209
|
+
}
|
|
210
|
+
// ─── Hybrid Score Component ───────────────────────────────────
|
|
211
|
+
/**
|
|
212
|
+
* Compute the hybrid retrieval score incorporating valence salience.
|
|
213
|
+
*
|
|
214
|
+
* Formula: 0.65 × similarity + 0.25 × normalizedActivation + 0.1 × |valence|
|
|
215
|
+
*
|
|
216
|
+
* The valence component uses ABSOLUTE MAGNITUDE — both extreme positive
|
|
217
|
+
* and extreme negative memories get a retrieval boost. Only the sign
|
|
218
|
+
* matters for UX warnings, not for ranking.
|
|
219
|
+
*
|
|
220
|
+
* @param similarity - Semantic similarity score [0, 1]
|
|
221
|
+
* @param normalizedActivation - Sigmoid-normalized activation energy [0, 1]
|
|
222
|
+
* @param valence - Raw valence score [-1, +1]
|
|
223
|
+
* @param weights - Optional weight overrides
|
|
224
|
+
* @returns Hybrid score in [0, 1]
|
|
225
|
+
*/
|
|
226
|
+
export function computeHybridScoreWithValence(similarity, normalizedActivation, valence, weights = {}) {
|
|
227
|
+
const wSim = weights.similarity ?? 0.65;
|
|
228
|
+
const wAct = weights.activation ?? 0.25;
|
|
229
|
+
const wVal = weights.valence ?? 0.10;
|
|
230
|
+
const safeSim = Number.isFinite(similarity) ? Math.max(0, Math.min(1, similarity)) : 0;
|
|
231
|
+
const safeAct = Number.isFinite(normalizedActivation) ? Math.max(0, Math.min(1, normalizedActivation)) : 0;
|
|
232
|
+
const safeVal = valenceSalience(valence); // Already returns [0, 1] magnitude
|
|
233
|
+
return wSim * safeSim + wAct * safeAct + wVal * safeVal;
|
|
234
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { BRAVE_API_KEY, FIRECRAWL_API_KEY, TAVILY_API_KEY, PRISM_SCHOLAR_MAX_ARTICLES_PER_RUN, PRISM_USER_ID, PRISM_SCHOLAR_TOPICS,
|
|
1
|
+
import { BRAVE_API_KEY, FIRECRAWL_API_KEY, TAVILY_API_KEY, PRISM_SCHOLAR_MAX_ARTICLES_PER_RUN, PRISM_USER_ID, PRISM_SCHOLAR_TOPICS, PRISM_ENABLE_HIVEMIND_ENV } from "../config.js";
|
|
2
|
+
import { getSettingSync } from "../storage/configStorage.js";
|
|
2
3
|
import { getStorage } from "../storage/index.js";
|
|
3
4
|
import { debugLog } from "../utils/logger.js";
|
|
4
5
|
import { getLLMProvider } from "../utils/llm/factory.js";
|
|
@@ -16,7 +17,7 @@ const SCHOLAR_ROLE = "scholar";
|
|
|
16
17
|
* Gracefully no-ops when Hivemind is disabled.
|
|
17
18
|
*/
|
|
18
19
|
async function hivemindRegister(topic) {
|
|
19
|
-
if (
|
|
20
|
+
if (getSettingSync("hivemind_enabled", String(PRISM_ENABLE_HIVEMIND_ENV)) !== "true")
|
|
20
21
|
return;
|
|
21
22
|
try {
|
|
22
23
|
const storage = await getStorage();
|
|
@@ -35,7 +36,7 @@ async function hivemindRegister(topic) {
|
|
|
35
36
|
}
|
|
36
37
|
}
|
|
37
38
|
async function hivemindHeartbeat(task) {
|
|
38
|
-
if (
|
|
39
|
+
if (getSettingSync("hivemind_enabled", String(PRISM_ENABLE_HIVEMIND_ENV)) !== "true")
|
|
39
40
|
return;
|
|
40
41
|
try {
|
|
41
42
|
const storage = await getStorage();
|
|
@@ -44,7 +45,7 @@ async function hivemindHeartbeat(task) {
|
|
|
44
45
|
catch { /* non-fatal */ }
|
|
45
46
|
}
|
|
46
47
|
async function hivemindIdle() {
|
|
47
|
-
if (
|
|
48
|
+
if (getSettingSync("hivemind_enabled", String(PRISM_ENABLE_HIVEMIND_ENV)) !== "true")
|
|
48
49
|
return;
|
|
49
50
|
try {
|
|
50
51
|
const storage = await getStorage();
|
|
@@ -59,7 +60,7 @@ async function hivemindIdle() {
|
|
|
59
60
|
* the Scholar's state change and generate alerts for active agents.
|
|
60
61
|
*/
|
|
61
62
|
async function hivemindBroadcast(topic, articleCount) {
|
|
62
|
-
if (
|
|
63
|
+
if (getSettingSync("hivemind_enabled", String(PRISM_ENABLE_HIVEMIND_ENV)) !== "true")
|
|
63
64
|
return;
|
|
64
65
|
try {
|
|
65
66
|
const storage = await getStorage();
|
|
@@ -85,7 +86,7 @@ async function selectTopic() {
|
|
|
85
86
|
return "";
|
|
86
87
|
// Default: random pick
|
|
87
88
|
const randomPick = topics[Math.floor(Math.random() * topics.length)];
|
|
88
|
-
if (
|
|
89
|
+
if (getSettingSync("hivemind_enabled", String(PRISM_ENABLE_HIVEMIND_ENV)) !== "true")
|
|
89
90
|
return randomPick;
|
|
90
91
|
try {
|
|
91
92
|
const storage = await getStorage();
|
package/dist/server.js
CHANGED
|
@@ -58,7 +58,7 @@ ListResourcesRequestSchema, ListResourceTemplatesRequestSchema, ReadResourceRequ
|
|
|
58
58
|
// Claude Desktop that the attached resource has changed.
|
|
59
59
|
// Without this, the paperclipped context becomes stale.
|
|
60
60
|
SubscribeRequestSchema, UnsubscribeRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
61
|
-
import { SERVER_CONFIG, SESSION_MEMORY_ENABLED, PRISM_USER_ID,
|
|
61
|
+
import { SERVER_CONFIG, SESSION_MEMORY_ENABLED, PRISM_USER_ID, PRISM_ENABLE_HIVEMIND_ENV, WATCHDOG_INTERVAL_MS, WATCHDOG_STALE_MIN, WATCHDOG_FROZEN_MIN, WATCHDOG_OFFLINE_MIN, WATCHDOG_LOOP_THRESHOLD, PRISM_SCHEDULER_ENABLED, PRISM_SCHEDULER_INTERVAL_MS, PRISM_SCHOLAR_ENABLED, PRISM_HDC_ENABLED, PRISM_TASK_ROUTER_ENABLED_ENV, PRISM_DARK_FACTORY_ENABLED_ENV, } from "./config.js";
|
|
62
62
|
import { startWatchdog, drainAlerts } from "./hivemindWatchdog.js";
|
|
63
63
|
import { startScheduler, startScholarScheduler } from "./backgroundScheduler.js";
|
|
64
64
|
import { startDarkFactoryRunner } from "./darkfactory/runner.js";
|
|
@@ -72,7 +72,7 @@ import { acquireLock, registerShutdownHandlers } from "./lifecycle.js";
|
|
|
72
72
|
// error wrapper. Now uses getStorage() which routes through the
|
|
73
73
|
// correct backend (Supabase or SQLite) with proper error handling.
|
|
74
74
|
import { getStorage } from "./storage/index.js";
|
|
75
|
-
import { getSettingSync, initConfigStorage } from "./storage/configStorage.js";
|
|
75
|
+
import { getSettingSync, initConfigStorage, setSetting } from "./storage/configStorage.js";
|
|
76
76
|
import { getTracer, initTelemetry } from "./utils/telemetry.js";
|
|
77
77
|
import { context as otelContext, trace, SpanStatusCode } from "@opentelemetry/api";
|
|
78
78
|
// ─── Import Tool Definitions (schemas) and Handlers (implementations) ─────
|
|
@@ -288,9 +288,9 @@ export function getAvailableTools() {
|
|
|
288
288
|
return [
|
|
289
289
|
...BASE_TOOLS,
|
|
290
290
|
...SESSION_MEMORY_TOOLS,
|
|
291
|
-
...(
|
|
291
|
+
...(getSettingSync("hivemind_enabled", String(PRISM_ENABLE_HIVEMIND_ENV)) === "true" ? AGENT_REGISTRY_TOOLS : []),
|
|
292
292
|
...(getSettingSync("task_router_enabled", String(PRISM_TASK_ROUTER_ENABLED_ENV)) === "true" ? [SESSION_TASK_ROUTE_TOOL] : []),
|
|
293
|
-
...(
|
|
293
|
+
...(getSettingSync("dark_factory_enabled", String(PRISM_DARK_FACTORY_ENABLED_ENV)) === "true" ? [SESSION_START_PIPELINE_TOOL, SESSION_CHECK_PIPELINE_STATUS_TOOL, SESSION_ABORT_PIPELINE_TOOL] : []),
|
|
294
294
|
];
|
|
295
295
|
}
|
|
296
296
|
export function createServer() {
|
|
@@ -797,22 +797,22 @@ export function createServer() {
|
|
|
797
797
|
case "agent_register":
|
|
798
798
|
if (!SESSION_MEMORY_ENABLED)
|
|
799
799
|
throw new Error("Session memory not configured.");
|
|
800
|
-
if (
|
|
801
|
-
throw new Error("Hivemind not enabled.
|
|
800
|
+
if (getSettingSync("hivemind_enabled", String(PRISM_ENABLE_HIVEMIND_ENV)) !== "true")
|
|
801
|
+
throw new Error("Hivemind not enabled. Enable it in the dashboard or set PRISM_ENABLE_HIVEMIND=true.");
|
|
802
802
|
result = await agentRegisterHandler(args);
|
|
803
803
|
break;
|
|
804
804
|
case "agent_heartbeat":
|
|
805
805
|
if (!SESSION_MEMORY_ENABLED)
|
|
806
806
|
throw new Error("Session memory not configured.");
|
|
807
|
-
if (
|
|
808
|
-
throw new Error("Hivemind not enabled.
|
|
807
|
+
if (getSettingSync("hivemind_enabled", String(PRISM_ENABLE_HIVEMIND_ENV)) !== "true")
|
|
808
|
+
throw new Error("Hivemind not enabled. Enable it in the dashboard or set PRISM_ENABLE_HIVEMIND=true.");
|
|
809
809
|
result = await agentHeartbeatHandler(args);
|
|
810
810
|
break;
|
|
811
811
|
case "agent_list_team":
|
|
812
812
|
if (!SESSION_MEMORY_ENABLED)
|
|
813
813
|
throw new Error("Session memory not configured.");
|
|
814
|
-
if (
|
|
815
|
-
throw new Error("Hivemind not enabled.
|
|
814
|
+
if (getSettingSync("hivemind_enabled", String(PRISM_ENABLE_HIVEMIND_ENV)) !== "true")
|
|
815
|
+
throw new Error("Hivemind not enabled. Enable it in the dashboard or set PRISM_ENABLE_HIVEMIND=true.");
|
|
816
816
|
result = await agentListTeamHandler(args);
|
|
817
817
|
break;
|
|
818
818
|
// ─── v7.1: Task Router ───
|
|
@@ -827,22 +827,22 @@ export function createServer() {
|
|
|
827
827
|
case "session_start_pipeline":
|
|
828
828
|
if (!SESSION_MEMORY_ENABLED)
|
|
829
829
|
throw new Error("Session memory not configured.");
|
|
830
|
-
if (
|
|
831
|
-
throw new Error("Dark Factory not enabled.
|
|
830
|
+
if (getSettingSync("dark_factory_enabled", String(PRISM_DARK_FACTORY_ENABLED_ENV)) !== "true")
|
|
831
|
+
throw new Error("Dark Factory not enabled. Enable it in the dashboard or set PRISM_DARK_FACTORY_ENABLED=true.");
|
|
832
832
|
result = await sessionStartPipelineHandler(args);
|
|
833
833
|
break;
|
|
834
834
|
case "session_check_pipeline_status":
|
|
835
835
|
if (!SESSION_MEMORY_ENABLED)
|
|
836
836
|
throw new Error("Session memory not configured.");
|
|
837
|
-
if (
|
|
838
|
-
throw new Error("Dark Factory not enabled.
|
|
837
|
+
if (getSettingSync("dark_factory_enabled", String(PRISM_DARK_FACTORY_ENABLED_ENV)) !== "true")
|
|
838
|
+
throw new Error("Dark Factory not enabled. Enable it in the dashboard or set PRISM_DARK_FACTORY_ENABLED=true.");
|
|
839
839
|
result = await sessionCheckPipelineStatusHandler(args);
|
|
840
840
|
break;
|
|
841
841
|
case "session_abort_pipeline":
|
|
842
842
|
if (!SESSION_MEMORY_ENABLED)
|
|
843
843
|
throw new Error("Session memory not configured.");
|
|
844
|
-
if (
|
|
845
|
-
throw new Error("Dark Factory not enabled.
|
|
844
|
+
if (getSettingSync("dark_factory_enabled", String(PRISM_DARK_FACTORY_ENABLED_ENV)) !== "true")
|
|
845
|
+
throw new Error("Dark Factory not enabled. Enable it in the dashboard or set PRISM_DARK_FACTORY_ENABLED=true.");
|
|
846
846
|
result = await sessionAbortPipelineHandler(args);
|
|
847
847
|
break;
|
|
848
848
|
default:
|
|
@@ -856,7 +856,7 @@ export function createServer() {
|
|
|
856
856
|
// CRITICAL: Append alerts DIRECTLY to tool response content
|
|
857
857
|
// so the LLM actually reads them. sendLoggingMessage goes to
|
|
858
858
|
// debug logs which the LLM never sees.
|
|
859
|
-
if (
|
|
859
|
+
if (getSettingSync("hivemind_enabled", String(PRISM_ENABLE_HIVEMIND_ENV)) === "true" && result && !result.isError) {
|
|
860
860
|
const project = args?.project;
|
|
861
861
|
if (typeof project === "string") {
|
|
862
862
|
const alerts = drainAlerts(project);
|
|
@@ -974,6 +974,41 @@ export function createSandboxServer() {
|
|
|
974
974
|
* is standard for MCP — it reads JSON-RPC from stdin and writes
|
|
975
975
|
* responses to stdout. Log messages go to stderr.
|
|
976
976
|
*/
|
|
977
|
+
/**
|
|
978
|
+
* v9.2: One-time env-to-configStorage migration.
|
|
979
|
+
*
|
|
980
|
+
* Seeds prism-config.db with values from env vars using "setIfAbsent" semantics:
|
|
981
|
+
* - If the key already has a value in configStorage (e.g., set via the dashboard),
|
|
982
|
+
* it is NEVER overwritten.
|
|
983
|
+
* - If the key has no value yet AND the env var is set, we seed the db with the
|
|
984
|
+
* env-var value so the dashboard UI correctly reflects the existing configuration.
|
|
985
|
+
*
|
|
986
|
+
* This is idempotent — safe to call on every startup. After the first run it becomes
|
|
987
|
+
* a no-op since all keys will already have values in configStorage.
|
|
988
|
+
*
|
|
989
|
+
* Covers: feature flags (Hivemind, Task Router, Dark Factory) and Supabase credentials.
|
|
990
|
+
*/
|
|
991
|
+
async function migrateEnvToConfigStorage() {
|
|
992
|
+
const migrations = [
|
|
993
|
+
// Feature flags
|
|
994
|
+
{ dbKey: "hivemind_enabled", envValue: process.env.PRISM_ENABLE_HIVEMIND ? (process.env.PRISM_ENABLE_HIVEMIND === "true" ? "true" : "false") : undefined },
|
|
995
|
+
{ dbKey: "task_router_enabled", envValue: process.env.PRISM_TASK_ROUTER_ENABLED ? (process.env.PRISM_TASK_ROUTER_ENABLED === "true" ? "true" : "false") : undefined },
|
|
996
|
+
{ dbKey: "dark_factory_enabled", envValue: process.env.PRISM_DARK_FACTORY_ENABLED ? (process.env.PRISM_DARK_FACTORY_ENABLED === "true" ? "true" : "false") : undefined },
|
|
997
|
+
// Supabase credentials — only migrate if present and non-empty
|
|
998
|
+
{ dbKey: "SUPABASE_URL", envValue: process.env.SUPABASE_URL || undefined },
|
|
999
|
+
{ dbKey: "SUPABASE_KEY", envValue: process.env.SUPABASE_KEY || undefined },
|
|
1000
|
+
{ dbKey: "PRISM_STORAGE", envValue: process.env.PRISM_STORAGE || undefined },
|
|
1001
|
+
];
|
|
1002
|
+
for (const { dbKey, envValue } of migrations) {
|
|
1003
|
+
if (!envValue)
|
|
1004
|
+
continue; // env var not set — nothing to migrate
|
|
1005
|
+
const existing = getSettingSync(dbKey, "");
|
|
1006
|
+
if (existing !== "")
|
|
1007
|
+
continue; // already has a value — never overwrite
|
|
1008
|
+
await setSetting(dbKey, envValue);
|
|
1009
|
+
console.error(`[Prism] Migrated env var → configStorage: ${dbKey} = ${dbKey.toLowerCase().includes("key") ? "***" : envValue}`);
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
977
1012
|
export async function startServer() {
|
|
978
1013
|
// MUST BE FIRST: Kill any zombie processes and acquire the singleton PID lock
|
|
979
1014
|
// before touching SQLite. This prevents lock contention on prism-config.db.
|
|
@@ -983,6 +1018,12 @@ export async function startServer() {
|
|
|
983
1018
|
// during the Initialize handshake — zero extra latency for resource reads.
|
|
984
1019
|
// initConfigStorage() is local SQLite only (~5ms), safe to await.
|
|
985
1020
|
await initConfigStorage();
|
|
1021
|
+
// v9.2: One-time env-to-configStorage migration.
|
|
1022
|
+
// For users who previously set feature flags/Supabase credentials via env
|
|
1023
|
+
// vars, seed configStorage with those values IF the key doesn't exist yet.
|
|
1024
|
+
// This is "setIfAbsent" logic — it never overwrites a dashboard-set value.
|
|
1025
|
+
// After this runs, the dashboard toggles reflect the actual runtime state.
|
|
1026
|
+
await migrateEnvToConfigStorage();
|
|
986
1027
|
// v4.6.0: Initialize OTel AFTER the settings cache is warm so that
|
|
987
1028
|
// initTelemetry() can read otel_enabled/otel_endpoint from getSettingSync()
|
|
988
1029
|
// synchronously. This is a synchronous call — no await needed.
|
|
@@ -1144,7 +1185,7 @@ export async function startServer() {
|
|
|
1144
1185
|
// Start the server-side health monitor after storage is warm.
|
|
1145
1186
|
// Runs every WATCHDOG_INTERVAL_MS (default 60s) to detect
|
|
1146
1187
|
// frozen agents, infinite loops, and task overruns.
|
|
1147
|
-
if (
|
|
1188
|
+
if (getSettingSync("hivemind_enabled", String(PRISM_ENABLE_HIVEMIND_ENV)) === "true" && SESSION_MEMORY_ENABLED) {
|
|
1148
1189
|
storageReady?.then(() => {
|
|
1149
1190
|
startWatchdog({
|
|
1150
1191
|
intervalMs: WATCHDOG_INTERVAL_MS,
|
|
@@ -1184,7 +1225,7 @@ export async function startServer() {
|
|
|
1184
1225
|
// Autonomous pipeline orchestration engine. Picks up RUNNING
|
|
1185
1226
|
// pipelines and advances them through PLAN → EXECUTE → VERIFY
|
|
1186
1227
|
// cycles. Non-blocking — uses setInterval to yield between ticks.
|
|
1187
|
-
if (
|
|
1228
|
+
if (getSettingSync("dark_factory_enabled", String(PRISM_DARK_FACTORY_ENABLED_ENV)) === "true" && SESSION_MEMORY_ENABLED) {
|
|
1188
1229
|
storageReady?.then(() => {
|
|
1189
1230
|
startDarkFactoryRunner();
|
|
1190
1231
|
}).catch(err => {
|
package/dist/storage/index.js
CHANGED
|
@@ -1,26 +1,70 @@
|
|
|
1
|
-
import { PRISM_STORAGE as ENV_PRISM_STORAGE, SUPABASE_CONFIGURED } from "../config.js";
|
|
2
1
|
import { debugLog } from "../utils/logger.js";
|
|
3
2
|
import { SupabaseStorage } from "./supabase.js";
|
|
4
3
|
import { getSetting } from "./configStorage.js";
|
|
5
4
|
let storageInstance = null;
|
|
6
5
|
export let activeStorageBackend = "local";
|
|
6
|
+
/** Validate that a string is an http(s) URL (mirrors logic in config.ts). */
|
|
7
|
+
function isHttpUrl(value) {
|
|
8
|
+
try {
|
|
9
|
+
const parsed = new URL(value);
|
|
10
|
+
return parsed.protocol === "http:" || parsed.protocol === "https:";
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
7
16
|
/**
|
|
8
17
|
* Returns the singleton storage backend.
|
|
9
18
|
*
|
|
10
19
|
* On first call: creates and initializes the appropriate backend.
|
|
11
20
|
* On subsequent calls: returns the cached instance.
|
|
21
|
+
*
|
|
22
|
+
* SUPABASE CREDENTIAL RESOLUTION ORDER (v9.2):
|
|
23
|
+
* 1. configStorage (prism-config.db) (set via Mind Palace dashboard)
|
|
24
|
+
* 2. process.env.SUPABASE_URL / SUPABASE_KEY (env var fallback)
|
|
25
|
+
*
|
|
26
|
+
* If credentials are found only in configStorage, they are injected into
|
|
27
|
+
* process.env so that supabaseApi.ts (which reads module-level constants
|
|
28
|
+
* from config.ts) picks them up on the same startup cycle.
|
|
12
29
|
*/
|
|
13
30
|
export async function getStorage() {
|
|
14
31
|
if (storageInstance)
|
|
15
32
|
return storageInstance;
|
|
16
|
-
//
|
|
17
|
-
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
33
|
+
// SOURCE OF TRUTH: prism-config.db (dashboard) → env fallback → "local" default
|
|
34
|
+
// DB wins because the dashboard is the authoritative source post-migration.
|
|
35
|
+
const dbStorage = await getSetting("PRISM_STORAGE", "");
|
|
36
|
+
const requestedBackend = (dbStorage || process.env.PRISM_STORAGE || "local");
|
|
37
|
+
if (requestedBackend === "supabase") {
|
|
38
|
+
// ─── Resolve credentials: configStorage → env var fallback ──────────
|
|
39
|
+
// v9.2: DB (dashboard) is the source of truth for Supabase credentials,
|
|
40
|
+
// consistent with PRISM_STORAGE resolution above. If the user configured
|
|
41
|
+
// Supabase via the dashboard, the values live in configStorage. Env vars
|
|
42
|
+
// are only used as a fallback for users who haven't migrated yet.
|
|
43
|
+
const resolvedUrl = await getSetting("SUPABASE_URL", "") ||
|
|
44
|
+
process.env.SUPABASE_URL ||
|
|
45
|
+
"";
|
|
46
|
+
const resolvedKey = await getSetting("SUPABASE_KEY", "") ||
|
|
47
|
+
await getSetting("SUPABASE_SERVICE_ROLE_KEY", "") ||
|
|
48
|
+
process.env.SUPABASE_KEY ||
|
|
49
|
+
"";
|
|
50
|
+
const isConfigured = !!resolvedUrl && !!resolvedKey && isHttpUrl(resolvedUrl);
|
|
51
|
+
if (!isConfigured) {
|
|
52
|
+
activeStorageBackend = "local";
|
|
53
|
+
console.error("[Prism Storage] Supabase backend requested but credentials are missing or invalid " +
|
|
54
|
+
"(checked both process.env and prism-config.db). Falling back to local storage.\n" +
|
|
55
|
+
" → Configure via Mind Palace dashboard (Settings → Storage Backend → Supabase) or set SUPABASE_URL / SUPABASE_KEY env vars.");
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
// Inject resolved credentials into process.env so supabaseApi.ts
|
|
59
|
+
// (which reads config.ts module-level constants) can use them.
|
|
60
|
+
// This is safe: process.env injection only affects in-process lookups;
|
|
61
|
+
// it doesn't mutate the shell environment of the parent process.
|
|
62
|
+
// Always overwrite — DB is the source of truth post-v9.2.
|
|
63
|
+
process.env.SUPABASE_URL = resolvedUrl;
|
|
64
|
+
process.env.SUPABASE_KEY = resolvedKey;
|
|
65
|
+
activeStorageBackend = "supabase";
|
|
66
|
+
debugLog(`[Prism Storage] Supabase credentials resolved (source: ${await getSetting("SUPABASE_URL", "") ? "configStorage" : "env"})`);
|
|
67
|
+
}
|
|
24
68
|
}
|
|
25
69
|
else {
|
|
26
70
|
activeStorageBackend = requestedBackend;
|