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.
@@ -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, PRISM_ENABLE_HIVEMIND } from "../config.js";
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 (!PRISM_ENABLE_HIVEMIND)
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 (!PRISM_ENABLE_HIVEMIND)
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 (!PRISM_ENABLE_HIVEMIND)
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 (!PRISM_ENABLE_HIVEMIND)
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 (!PRISM_ENABLE_HIVEMIND)
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, PRISM_ENABLE_HIVEMIND, 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, } from "./config.js";
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
- ...(PRISM_ENABLE_HIVEMIND ? AGENT_REGISTRY_TOOLS : []),
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
- ...(PRISM_DARK_FACTORY_ENABLED ? [SESSION_START_PIPELINE_TOOL, SESSION_CHECK_PIPELINE_STATUS_TOOL, SESSION_ABORT_PIPELINE_TOOL] : []),
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 (!PRISM_ENABLE_HIVEMIND)
801
- throw new Error("Hivemind not enabled. Set PRISM_ENABLE_HIVEMIND=true.");
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 (!PRISM_ENABLE_HIVEMIND)
808
- throw new Error("Hivemind not enabled. Set PRISM_ENABLE_HIVEMIND=true.");
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 (!PRISM_ENABLE_HIVEMIND)
815
- throw new Error("Hivemind not enabled. Set PRISM_ENABLE_HIVEMIND=true.");
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 (!PRISM_DARK_FACTORY_ENABLED)
831
- throw new Error("Dark Factory not enabled. Set PRISM_DARK_FACTORY_ENABLED=true.");
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 (!PRISM_DARK_FACTORY_ENABLED)
838
- throw new Error("Dark Factory not enabled. Set PRISM_DARK_FACTORY_ENABLED=true.");
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 (!PRISM_DARK_FACTORY_ENABLED)
845
- throw new Error("Dark Factory not enabled. Set PRISM_DARK_FACTORY_ENABLED=true.");
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 (PRISM_ENABLE_HIVEMIND && result && !result.isError) {
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 (PRISM_ENABLE_HIVEMIND && SESSION_MEMORY_ENABLED) {
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 (PRISM_DARK_FACTORY_ENABLED && SESSION_MEMORY_ENABLED) {
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 => {
@@ -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
- // Use environment variable if explicitly set, otherwise fall back to db config
17
- const envStorage = process.env.PRISM_STORAGE;
18
- const requestedBackend = (envStorage || await getSetting("PRISM_STORAGE", ENV_PRISM_STORAGE));
19
- // Guardrail: if Supabase is requested but credentials are unresolved/invalid,
20
- // transparently fall back to local mode to keep dashboard + core tools usable.
21
- if (requestedBackend === "supabase" && !SUPABASE_CONFIGURED) {
22
- activeStorageBackend = "local";
23
- console.error("[Prism Storage] Supabase backend requested but SUPABASE_URL/SUPABASE_KEY are invalid or unresolved. Falling back to local storage.");
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;