prism-mcp-server 4.0.0 → 4.3.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.
@@ -696,6 +696,19 @@ export function renderDashboardHTML(version) {
696
696
  </select>
697
697
  </div>
698
698
 
699
+ <div class="setting-row">
700
+ <div>
701
+ <div class="setting-label">Token Budget</div>
702
+ <div class="setting-desc">Max tokens for session_load_context (0 = unlimited)</div>
703
+ </div>
704
+ <input type="number" id="input-max-tokens"
705
+ placeholder="0"
706
+ min="0" max="100000" step="500"
707
+ style="padding: 0.2rem 0.5rem; background: var(--bg-hover); color: var(--text-primary); border: 1px solid var(--border-color); border-radius: 4px; font-size: 0.85rem; font-family: var(--font-mono); width: 90px; text-align: right;"
708
+ onchange="saveSetting('max_tokens', this.value)"
709
+ oninput="clearTimeout(this._t); this._t=setTimeout(()=>saveSetting('max_tokens',this.value),800)" />
710
+ </div>
711
+
699
712
  <div class="setting-section">Boot Settings <span class="boot-badge">Restart Required</span></div>
700
713
 
701
714
  <div class="setting-row">
@@ -716,6 +729,26 @@ export function renderDashboardHTML(version) {
716
729
  </select>
717
730
  </div>
718
731
 
732
+ <div class="setting-row" style="align-items:flex-start">
733
+ <div>
734
+ <div class="setting-label">Auto-Load Projects</div>
735
+ <div class="setting-desc">Select projects to auto-push context on startup</div>
736
+ </div>
737
+ <div id="autoload-checkboxes" style="display:flex;flex-direction:column;gap:4px;font-size:0.85rem;font-family:var(--font-mono);max-height:120px;overflow-y:auto;">
738
+ <span style="color:var(--text-muted);font-size:0.8rem">Loading…</span>
739
+ </div>
740
+ </div>
741
+
742
+ <div class="setting-row" style="align-items:flex-start">
743
+ <div>
744
+ <div class="setting-label">Project Repo Paths</div>
745
+ <div class="setting-desc">Map each project to its repo directory for save validation</div>
746
+ </div>
747
+ <div id="repopath-inputs" style="display:flex;flex-direction:column;gap:6px;font-size:0.85rem;max-height:160px;overflow-y:auto;">
748
+ <span style="color:var(--text-muted);font-size:0.8rem">Loading…</span>
749
+ </div>
750
+ </div>
751
+
719
752
  <div class="setting-section">Agent Identity</div>
720
753
 
721
754
  <div class="setting-row">
@@ -1278,6 +1311,88 @@ Example:\n## Dev Rules\n- Always write tests first\n- Use TypeScript strict mode
1278
1311
  }
1279
1312
 
1280
1313
 
1314
+ // ─── Auto-Load Checkboxes (v4.1) ─────────────────────────────────
1315
+ async function loadAutoloadCheckboxes() {
1316
+ var container = document.getElementById('autoload-checkboxes');
1317
+ if (!container) return;
1318
+ try {
1319
+ var projRes = await fetch('/api/projects');
1320
+ var projData = await projRes.json();
1321
+ var projects = projData.projects || [];
1322
+
1323
+ var settRes = await fetch('/api/settings');
1324
+ var settData = await settRes.json();
1325
+ var saved = (settData.settings || {}).autoload_projects || '';
1326
+ var selected = saved.split(',').map(function(s){ return s.trim(); }).filter(Boolean);
1327
+
1328
+ if (projects.length === 0) {
1329
+ container.innerHTML = '<span style="color:var(--text-muted);font-size:0.8rem">No projects found</span>';
1330
+ return;
1331
+ }
1332
+
1333
+ container.innerHTML = projects.map(function(p) {
1334
+ var checked = selected.indexOf(p) !== -1 ? ' checked' : '';
1335
+ return '<label style="display:flex;align-items:center;gap:6px;cursor:pointer;color:var(--text-primary)">' +
1336
+ '<input type="checkbox" value="' + escapeHtml(p) + '"' + checked +
1337
+ ' onchange="onAutoloadToggle()"' +
1338
+ ' style="accent-color:var(--accent-purple);cursor:pointer" />' +
1339
+ escapeHtml(p) + '</label>';
1340
+ }).join('');
1341
+ } catch(e) {
1342
+ container.innerHTML = '<span style="color:var(--accent-rose);font-size:0.8rem">Failed to load</span>';
1343
+ }
1344
+ }
1345
+
1346
+ function onAutoloadToggle() {
1347
+ var container = document.getElementById('autoload-checkboxes');
1348
+ if (!container) return;
1349
+ var boxes = container.querySelectorAll('input[type=checkbox]');
1350
+ var selected = [];
1351
+ for (var i = 0; i < boxes.length; i++) {
1352
+ if (boxes[i].checked) selected.push(boxes[i].value);
1353
+ }
1354
+ saveBootSetting('autoload_projects', selected.join(','));
1355
+ }
1356
+
1357
+ // ─── Project Repo Paths (v4.2) ─────────────────────────────────
1358
+ async function loadRepoPathInputs() {
1359
+ var container = document.getElementById('repopath-inputs');
1360
+ if (!container) return;
1361
+ try {
1362
+ var projRes = await fetch('/api/projects');
1363
+ var projData = await projRes.json();
1364
+ var projects = projData.projects || [];
1365
+
1366
+ var settRes = await fetch('/api/settings');
1367
+ var settData = await settRes.json();
1368
+ var settings = settData.settings || {};
1369
+
1370
+ if (projects.length === 0) {
1371
+ container.innerHTML = '<span style="color:var(--text-muted);font-size:0.8rem">No projects found</span>';
1372
+ return;
1373
+ }
1374
+
1375
+ container.innerHTML = projects.map(function(p) {
1376
+ var savedPath = settings['repo_path:' + p] || '';
1377
+ return '<div style="display:flex;align-items:center;gap:6px">' +
1378
+ '<span style="min-width:100px;color:var(--text-secondary);font-family:var(--font-mono);font-size:0.8rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="' + escapeHtml(p) + '">' + escapeHtml(p) + '</span>' +
1379
+ '<input type="text" value="' + escapeHtml(savedPath) + '"' +
1380
+ ' placeholder="/path/to/repo"' +
1381
+ ' data-project="' + escapeHtml(p) + '"' +
1382
+ ' style="flex:1;min-width:140px;padding:0.2rem 0.4rem;background:var(--bg-primary);color:var(--text-primary);border:1px solid var(--border-glass);border-radius:4px;font-size:0.8rem;font-family:var(--font-mono)"' +
1383
+ ' onchange="saveRepoPath(this.dataset.project, this.value)"' +
1384
+ ' oninput="clearTimeout(this._t); var self=this; this._t=setTimeout(function(){saveRepoPath(self.dataset.project, self.value)},1200)" />' +
1385
+ '</div>';
1386
+ }).join('');
1387
+ } catch(e) {
1388
+ container.innerHTML = '<span style="color:var(--accent-rose);font-size:0.8rem">Failed to load</span>';
1389
+ }
1390
+ }
1391
+
1392
+ async function saveRepoPath(project, path) {
1393
+ await saveSetting('repo_path:' + project, path.trim());
1394
+ }
1395
+
1281
1396
  async function loadSettings() {
1282
1397
  try {
1283
1398
  var res = await fetch('/api/settings');
@@ -1304,6 +1419,11 @@ Example:\n## Dev Rules\n- Always write tests first\n- Use TypeScript strict mode
1304
1419
  // Agent Identity
1305
1420
  if (s.default_role) document.getElementById('select-default-role').value = s.default_role;
1306
1421
  if (s.agent_name) document.getElementById('input-agent-name').value = s.agent_name;
1422
+ if (s.max_tokens) document.getElementById('input-max-tokens').value = s.max_tokens;
1423
+ // Autoload checkboxes are loaded dynamically
1424
+ loadAutoloadCheckboxes();
1425
+ // Repo path inputs are loaded dynamically
1426
+ loadRepoPathInputs();
1307
1427
  } catch(e) { console.warn('Settings load failed:', e); }
1308
1428
  }
1309
1429
 
@@ -0,0 +1,164 @@
1
+ /**
2
+ * Server Lifecycle Management
3
+ * Handles singleton PID locking, graceful shutdown, and SQLite handle cleanup.
4
+ *
5
+ * CRITICAL: All logging MUST use console.error() (stderr).
6
+ * Using console.log() (stdout) will corrupt the MCP JSON-RPC stream.
7
+ */
8
+ import * as fs from "fs";
9
+ import * as path from "path";
10
+ import * as os from "os";
11
+ import { execSync } from "child_process";
12
+ import { closeConfigStorage } from "./storage/configStorage.js";
13
+ import { getStorage } from "./storage/index.js";
14
+ const PRISM_DIR = path.join(os.homedir(), ".prism-mcp");
15
+ /**
16
+ * Instance-aware PID file.
17
+ * Set PRISM_INSTANCE env var to run multiple Prism MCP servers
18
+ * side-by-side (e.g. "athena-public" and "prism-mcp").
19
+ * Each instance gets its own PID file to prevent lock conflicts.
20
+ */
21
+ const INSTANCE_NAME = process.env.PRISM_INSTANCE || "default";
22
+ const PID_FILE = path.join(PRISM_DIR, `server-${INSTANCE_NAME}.pid`);
23
+ function log(msg) {
24
+ console.error(`[Prism Lifecycle] ${msg}`);
25
+ }
26
+ /**
27
+ * Checks if a process is an orphan (adopted by init/launchd, PPID=1).
28
+ * Returns false on Windows (PID logic is different there).
29
+ */
30
+ function isOrphanProcess(pid) {
31
+ if (process.platform === "win32") {
32
+ // Windows doesn't have reliable PPID checks via 'ps'.
33
+ // Safer to assume it's NOT an orphan to avoid killing valid instances.
34
+ return false;
35
+ }
36
+ try {
37
+ // 'ps -o ppid= -p PID' returns just the parent PID
38
+ const ppid = execSync(`ps -o ppid= -p ${pid}`, { encoding: "utf8" }).trim();
39
+ return ppid === "1";
40
+ }
41
+ catch {
42
+ // If ps fails (e.g. process gone), assume it's safe to claim
43
+ return true;
44
+ }
45
+ }
46
+ /**
47
+ * Ensures valid server execution state.
48
+ *
49
+ * LOGIC:
50
+ * 1. If --no-lock is passed, skip everything (testing mode).
51
+ * 2. If PID file exists:
52
+ * - If process is dead: Overwrite lock.
53
+ * - If process is alive AND is an orphan (PPID=1): Kill it (Zombie), then overwrite.
54
+ * - If process is alive AND has a parent: Log warning, allow coexistence (don't kill).
55
+ */
56
+ export function acquireLock() {
57
+ if (process.argv.includes("--no-lock")) {
58
+ log("Lock acquisition skipped (--no-lock flag)");
59
+ return;
60
+ }
61
+ if (!fs.existsSync(PRISM_DIR)) {
62
+ fs.mkdirSync(PRISM_DIR, { recursive: true });
63
+ }
64
+ if (fs.existsSync(PID_FILE)) {
65
+ try {
66
+ const oldPid = parseInt(fs.readFileSync(PID_FILE, "utf8").trim(), 10);
67
+ if (oldPid && oldPid !== process.pid) {
68
+ let isAlive = false;
69
+ try {
70
+ process.kill(oldPid, 0); // 0 signal checks for existence
71
+ isAlive = true;
72
+ }
73
+ catch {
74
+ isAlive = false;
75
+ }
76
+ if (isAlive) {
77
+ // Process exists. Is it a zombie?
78
+ if (isOrphanProcess(oldPid)) {
79
+ log(`Found zombie process (PID ${oldPid}, PPID=1). Terminating...`);
80
+ try {
81
+ process.kill(oldPid, "SIGTERM");
82
+ // Give it 100ms to die, then force kill if needed
83
+ setTimeout(() => {
84
+ try {
85
+ process.kill(oldPid, "SIGKILL");
86
+ }
87
+ catch { }
88
+ }, 100);
89
+ }
90
+ catch (e) {
91
+ log(`Failed to kill zombie: ${e}`);
92
+ }
93
+ }
94
+ else {
95
+ // It has a parent (e.g., another VS Code window or Claude Desktop)
96
+ log(`Existing server (PID ${oldPid}) is active and managed. Coexisting...`);
97
+ // We do NOT overwrite the PID file here.
98
+ // If we overwrite it, the *active* server will fail to clean up
99
+ // the PID file when it eventually shuts down.
100
+ return;
101
+ }
102
+ }
103
+ }
104
+ }
105
+ catch (err) {
106
+ log(`Warning: Failed to process existing PID file: ${err}`);
107
+ }
108
+ }
109
+ // Claim the lock for this process
110
+ try {
111
+ fs.writeFileSync(PID_FILE, process.pid.toString(), "utf8");
112
+ log(`Acquired singleton lock (PID ${process.pid})`);
113
+ }
114
+ catch (err) {
115
+ log(`Warning: Failed to write PID file: ${err}`);
116
+ }
117
+ }
118
+ /**
119
+ * Registers handlers to close SQLite file handles cleanly when the server stops.
120
+ */
121
+ export function registerShutdownHandlers() {
122
+ let shuttingDown = false;
123
+ const shutdown = async (reason) => {
124
+ if (shuttingDown)
125
+ return;
126
+ shuttingDown = true;
127
+ log(`Shutting down gracefully (${reason})...`);
128
+ try {
129
+ // 1. Close system settings DB
130
+ closeConfigStorage();
131
+ // 2. Close main ledger DB
132
+ const storage = await getStorage();
133
+ if (storage && typeof storage.close === "function") {
134
+ await storage.close();
135
+ }
136
+ // 3. Remove PID lockfile (only if WE own it)
137
+ if (fs.existsSync(PID_FILE)) {
138
+ try {
139
+ const storedPid = parseInt(fs.readFileSync(PID_FILE, "utf8").trim(), 10);
140
+ if (storedPid === process.pid) {
141
+ fs.unlinkSync(PID_FILE);
142
+ }
143
+ }
144
+ catch {
145
+ // Ignore read errors during shutdown
146
+ }
147
+ }
148
+ }
149
+ catch (err) {
150
+ log(`Error during shutdown cleanup: ${err}`);
151
+ }
152
+ finally {
153
+ process.exit(0);
154
+ }
155
+ };
156
+ // OS Signals
157
+ process.on("SIGINT", () => shutdown("SIGINT"));
158
+ process.on("SIGTERM", () => shutdown("SIGTERM"));
159
+ process.on("SIGHUP", () => shutdown("SIGHUP"));
160
+ // MCP Client Disconnect (CRITICAL)
161
+ process.stdin.on("close", () => {
162
+ shutdown("CLIENT_DISCONNECTED_STDIN_CLOSED");
163
+ });
164
+ }
package/dist/server.js CHANGED
@@ -61,6 +61,7 @@ SubscribeRequestSchema, UnsubscribeRequestSchema, } from "@modelcontextprotocol/
61
61
  import { SERVER_CONFIG, SESSION_MEMORY_ENABLED, PRISM_USER_ID, PRISM_ENABLE_HIVEMIND } from "./config.js";
62
62
  import { getSyncBus } from "./sync/factory.js";
63
63
  import { startDashboardServer } from "./dashboard/server.js";
64
+ import { acquireLock, registerShutdownHandlers } from "./lifecycle.js";
64
65
  // ─── v2.3.6 FIX: Use Storage Abstraction for Prompts/Resources ───
65
66
  // CRITICAL FIX: Previously imported supabaseRpc/supabaseGet directly,
66
67
  // which bypassed the storage abstraction layer and caused the server
@@ -101,6 +102,8 @@ sessionForgetMemoryHandler,
101
102
  knowledgeSetRetentionHandler,
102
103
  // v4.0: Active Behavioral Memory handlers
103
104
  sessionSaveExperienceHandler, knowledgeUpvoteHandler, knowledgeDownvoteHandler,
105
+ // v4.2: Knowledge Sync Rules
106
+ KNOWLEDGE_SYNC_RULES_TOOL, knowledgeSyncRulesHandler,
104
107
  // ─── v3.0: Agent Hivemind tools ───
105
108
  AGENT_REGISTRY_TOOLS, agentRegisterHandler, agentHeartbeatHandler, agentListTeamHandler, } from "./tools/index.js";
106
109
  // ─── Dynamic Tool Registration ───────────────────────────────────
@@ -114,43 +117,54 @@ const BASE_TOOLS = [
114
117
  BRAVE_ANSWERS_TOOL, // brave_answers — AI-grounded answers
115
118
  RESEARCH_PAPER_ANALYSIS_TOOL, // gemini_research_paper_analysis — paper analysis
116
119
  ];
117
- // Session memory tools: only added when SUPABASE_URL + SUPABASE_KEY are set
118
- // REVIEWER NOTE: v0.4.0 adds 2 new tools here:
119
- // - session_compact_ledger (Enhancement #2): auto-rollup of old ledger entries
120
- // - session_search_memory (Enhancement #4): semantic search via pgvector embeddings
121
- const SESSION_MEMORY_TOOLS = [
122
- SESSION_SAVE_LEDGER_TOOL, // session_save_ledger append immutable session log
123
- SESSION_SAVE_HANDOFF_TOOL, // session_save_handoff — upsert latest project state (now with OCC)
124
- SESSION_LOAD_CONTEXT_TOOL, // session_load_context progressive context loading
125
- KNOWLEDGE_SEARCH_TOOL, // knowledge_search search accumulated knowledge
126
- KNOWLEDGE_FORGET_TOOL, // knowledge_forget prune bad/old memories
127
- SESSION_COMPACT_LEDGER_TOOL, // session_compact_ledger — auto-compact old ledger entries (v0.4.0)
128
- SESSION_SEARCH_MEMORY_TOOL, // session_search_memory semantic search via embeddings (v0.4.0)
129
- MEMORY_HISTORY_TOOL, // memory_history — view version timeline (v2.0)
130
- MEMORY_CHECKOUT_TOOL, // memory_checkout — revert to past version (v2.0)
131
- // ─── v2.0: Visual Memory tools ───
132
- SESSION_SAVE_IMAGE_TOOL, // session_save_image — save image to media vault (v2.0)
133
- SESSION_VIEW_IMAGE_TOOL, // session_view_image — retrieve image from vault (v2.0)
134
- // ─── v2.2.0: Health Check tool ───
135
- SESSION_HEALTH_CHECK_TOOL, // session_health_check brain integrity checker (v2.2.0)
136
- // ─── Phase 2: GDPR Memory Deletion tool ───
137
- SESSION_FORGET_MEMORY_TOOL, // session_forget_memory — GDPR-compliant memory deletion (Phase 2)
138
- // ─── v3.1: TTL Retention tool ───
139
- KNOWLEDGE_SET_RETENTION_TOOL, // knowledge_set_retentionset auto-expiry TTL for a project
140
- // ─── v4.0: Active Behavioral Memory tools ───
141
- SESSION_SAVE_EXPERIENCE_TOOL, // session_save_experiencerecord typed experience events
142
- KNOWLEDGE_UPVOTE_TOOL, // knowledge_upvoteincrease entry importance
143
- KNOWLEDGE_DOWNVOTE_TOOL, // knowledge_downvotedecrease entry importance
144
- ];
145
- // Combine: always list ALL tools so scanners (Glama, Smithery, MCP Registry)
146
- // can enumerate the full capability set. Runtime guards in the CallTool handler
147
- // still prevent execution without valid Supabase credentials.
148
- const ALL_TOOLS = [
149
- ...BASE_TOOLS,
150
- ...SESSION_MEMORY_TOOLS,
151
- // v3.0: Agent Hivemind tools — only when PRISM_ENABLE_HIVEMIND=true
152
- ...(PRISM_ENABLE_HIVEMIND ? AGENT_REGISTRY_TOOLS : []),
153
- ];
120
+ // ─── v4.1 FIX: Build Session Memory Tools dynamically ────────
121
+ // The session_load_context tool description is dynamically modified
122
+ // to include auto-load project instructions. Tool descriptions are
123
+ // the ONLY mechanism guaranteed by ALL MCP clients (Antigravity,
124
+ // Claude Code, Claude CLI). The `instructions` field and
125
+ // sendLoggingMessage are NOT reliably surfaced by any client.
126
+ //
127
+ // buildSessionMemoryTools() is called inside createServer() AFTER
128
+ // initConfigStorage() has run, so getSettingSync() returns real
129
+ // dashboard config values.
130
+ function buildSessionMemoryTools(autoloadList) {
131
+ // Dynamically inject auto-load instruction into session_load_context description
132
+ let loadContextTool = SESSION_LOAD_CONTEXT_TOOL;
133
+ if (autoloadList.length > 0) {
134
+ const projectList = autoloadList.join(", ");
135
+ loadContextTool = {
136
+ ...SESSION_LOAD_CONTEXT_TOOL,
137
+ description: SESSION_LOAD_CONTEXT_TOOL.description +
138
+ `\n\nIMPORTANT: At the start of every conversation, you MUST call this tool for each of these projects: ${projectList}. Do this before any other work. Load level should be "standard" unless the user specifies otherwise.`,
139
+ };
140
+ }
141
+ return [
142
+ SESSION_SAVE_LEDGER_TOOL, // session_save_ledgerappend immutable session log
143
+ SESSION_SAVE_HANDOFF_TOOL, // session_save_handoff upsert latest project state (now with OCC)
144
+ loadContextTool, // session_load_contextprogressive context loading (+ auto-load instruction)
145
+ KNOWLEDGE_SEARCH_TOOL, // knowledge_searchsearch accumulated knowledge
146
+ KNOWLEDGE_FORGET_TOOL, // knowledge_forgetprune bad/old memories
147
+ SESSION_COMPACT_LEDGER_TOOL, // session_compact_ledger — auto-compact old ledger entries (v0.4.0)
148
+ SESSION_SEARCH_MEMORY_TOOL, // session_search_memory semantic search via embeddings (v0.4.0)
149
+ MEMORY_HISTORY_TOOL, // memory_history view version timeline (v2.0)
150
+ MEMORY_CHECKOUT_TOOL, // memory_checkout revert to past version (v2.0)
151
+ // ─── v2.0: Visual Memory tools ───
152
+ SESSION_SAVE_IMAGE_TOOL, // session_save_image — save image to media vault (v2.0)
153
+ SESSION_VIEW_IMAGE_TOOL, // session_view_image — retrieve image from vault (v2.0)
154
+ // ─── v2.2.0: Health Check tool ───
155
+ SESSION_HEALTH_CHECK_TOOL, // session_health_check brain integrity checker (v2.2.0)
156
+ // ─── Phase 2: GDPR Memory Deletion tool ───
157
+ SESSION_FORGET_MEMORY_TOOL, // session_forget_memory — GDPR-compliant memory deletion (Phase 2)
158
+ // ─── v3.1: TTL Retention tool ───
159
+ KNOWLEDGE_SET_RETENTION_TOOL, // knowledge_set_retention — set auto-expiry TTL for a project
160
+ // ─── v4.0: Active Behavioral Memory tools ───
161
+ SESSION_SAVE_EXPERIENCE_TOOL, // session_save_experience — record typed experience events
162
+ KNOWLEDGE_UPVOTE_TOOL, // knowledge_upvote — increase entry importance
163
+ KNOWLEDGE_DOWNVOTE_TOOL, // knowledge_downvote — decrease entry importance
164
+ // ─── v4.2: Knowledge Sync Rules tool ───
165
+ KNOWLEDGE_SYNC_RULES_TOOL, // knowledge_sync_rules — sync graduated insights to IDE rules files
166
+ ];
167
+ }
154
168
  // ─── v0.4.0: Resource Subscription Tracking ──────────────────────
155
169
  // REVIEWER NOTE: We track which project URIs clients have subscribed
156
170
  // to. When sessionSaveHandoffHandler successfully updates a project,
@@ -194,6 +208,35 @@ export function notifyResourceUpdate(project, server) {
194
208
  * with subscribe support for live refresh
195
209
  */
196
210
  export function createServer() {
211
+ // ─── v4.1 FIX: Auto-Load via Dynamic Tool Descriptions ────────
212
+ // Read auto-load projects EXCLUSIVELY from dashboard config
213
+ // (available after initConfigStorage() in startServer).
214
+ //
215
+ // ARCHITECTURE DECISION: We inject the auto-load instruction into
216
+ // the session_load_context TOOL DESCRIPTION, not into `instructions`
217
+ // or `sendLoggingMessage`. Tool descriptions are the ONLY mechanism
218
+ // guaranteed by ALL MCP clients (Antigravity, Claude Code, Claude CLI).
219
+ //
220
+ // The PRISM_AUTOLOAD_PROJECTS env var has been removed — the dashboard
221
+ // is the single source of truth. This prevents mismatches between
222
+ // env var and dashboard settings causing duplicate project loads.
223
+ const dashboardAutoload = getSettingSync("autoload_projects", "");
224
+ const autoloadList = dashboardAutoload
225
+ .split(",").map(p => p.trim()).filter(Boolean);
226
+ if (autoloadList.length > 0) {
227
+ console.error(`[Prism] Auto-load projects (dashboard): ${autoloadList.join(', ')}`);
228
+ }
229
+ // Build the dynamic tool list with auto-load instruction injected
230
+ const SESSION_MEMORY_TOOLS = buildSessionMemoryTools(autoloadList);
231
+ // Combine: always list ALL tools so scanners (Glama, Smithery, MCP Registry)
232
+ // can enumerate the full capability set. Runtime guards in the CallTool handler
233
+ // still prevent execution without valid Supabase credentials.
234
+ const ALL_TOOLS = [
235
+ ...BASE_TOOLS,
236
+ ...SESSION_MEMORY_TOOLS,
237
+ // v3.0: Agent Hivemind tools — only when PRISM_ENABLE_HIVEMIND=true
238
+ ...(PRISM_ENABLE_HIVEMIND ? AGENT_REGISTRY_TOOLS : []),
239
+ ];
197
240
  const server = new Server({
198
241
  name: SERVER_CONFIG.name,
199
242
  version: SERVER_CONFIG.version,
@@ -201,21 +244,13 @@ export function createServer() {
201
244
  capabilities: {
202
245
  tools: {},
203
246
  // ─── v0.4.0: Prompt capability (Enhancement #1) ───
204
- // REVIEWER NOTE: Declaring `prompts: {}` tells Claude Desktop
205
- // that we support the prompts/list and prompts/get methods.
206
- // This enables the /resume_session slash command in the UI.
207
- // Only enabled when Supabase is configured (prompts need
208
- // session data to be useful).
209
247
  ...(SESSION_MEMORY_ENABLED ? { prompts: {} } : {}),
210
248
  // ─── v0.4.0: Resource capability (Enhancement #3) ───
211
- // REVIEWER NOTE: Setting subscribe: true tells Claude Desktop
212
- // that we support resource subscriptions. When a user attaches
213
- // memory://project/handoff and the LLM later updates it,
214
- // we push an update notification so the attached context
215
- // is silently refreshed. Without subscribe:true, the
216
- // paperclipped context would become stale.
217
249
  ...(SESSION_MEMORY_ENABLED ? { resources: { subscribe: true } } : {}),
218
250
  },
251
+ // Supplementary signal — not all clients support this field.
252
+ // Primary mechanism is the dynamic tool description above.
253
+ instructions: `Prism MCP — The Mind Palace for AI Agents. This server provides persistent session memory, knowledge search, and context management tools. Use session_load_context to recover previous work state, session_save_ledger to log completed work, and session_save_handoff to preserve state for the next session.`,
219
254
  });
220
255
  // ── Handler: Initialize ──
221
256
  // NOTE: The SDK's built-in _oninitialize() handles the Initialize request.
@@ -607,6 +642,11 @@ export function createServer() {
607
642
  if (!SESSION_MEMORY_ENABLED)
608
643
  throw new Error("Session memory not configured. Set SUPABASE_URL and SUPABASE_KEY.");
609
644
  return await knowledgeDownvoteHandler(args);
645
+ // ─── v4.2: Knowledge Sync Rules Tool ───
646
+ case "knowledge_sync_rules":
647
+ if (!SESSION_MEMORY_ENABLED)
648
+ throw new Error("Session memory not configured. Set SUPABASE_URL and SUPABASE_KEY.");
649
+ return await knowledgeSyncRulesHandler(args);
610
650
  // ─── v3.0: Agent Hivemind Tools ───
611
651
  case "agent_register":
612
652
  if (!SESSION_MEMORY_ENABLED)
@@ -666,7 +706,7 @@ export function createSandboxServer() {
666
706
  });
667
707
  // Register all tool listings unconditionally
668
708
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
669
- tools: [...BASE_TOOLS, ...SESSION_MEMORY_TOOLS, ...AGENT_REGISTRY_TOOLS],
709
+ tools: [...BASE_TOOLS, ...buildSessionMemoryTools([]), ...AGENT_REGISTRY_TOOLS],
670
710
  }));
671
711
  // Register prompts listing so scanners see resume_session
672
712
  server.setRequestHandler(ListPromptsRequestSchema, async () => ({
@@ -715,6 +755,9 @@ export function createSandboxServer() {
715
755
  * responses to stdout. Log messages go to stderr.
716
756
  */
717
757
  export async function startServer() {
758
+ // MUST BE FIRST: Kill any zombie processes and acquire the singleton PID lock
759
+ // before touching SQLite. This prevents lock contention on prism-config.db.
760
+ acquireLock();
718
761
  // Pre-warm the config settings cache BEFORE connecting the MCP transport.
719
762
  // This ensures getSettingSync() returns real values (agent_name, default_role)
720
763
  // during the Initialize handshake — zero extra latency for resource reads.
@@ -723,6 +766,10 @@ export async function startServer() {
723
766
  const server = createServer();
724
767
  const transport = new StdioServerTransport();
725
768
  await server.connect(transport);
769
+ // Register graceful shutdown handlers (SIGTERM, SIGINT, SIGHUP, stdin close).
770
+ // The stdin close handler is critical — when MCP clients disconnect, they
771
+ // often just close the pipe without sending a signal, leaving zombie processes.
772
+ registerShutdownHandlers();
726
773
  // Pre-warm storage AFTER connecting — fired async so we never block the
727
774
  // stdio handshake. Supabase REST initialization can take 500ms–5s; blocking
728
775
  // on it before server.connect() was the root cause of the 1m 56s CLI delay.
@@ -740,6 +787,18 @@ export async function startServer() {
740
787
  ]).catch(err => {
741
788
  console.error(`[Prism] Storage pre-warm failed (non-fatal): ${err}`);
742
789
  });
790
+ // ─── v4.1: Auto-Load is handled via dynamic tool descriptions ──
791
+ // The session_load_context tool description is dynamically modified
792
+ // in createServer() → buildSessionMemoryTools() to include the
793
+ // auto-load projects list. This is the ONLY universally reliable
794
+ // mechanism — tool descriptions are surfaced by ALL MCP clients.
795
+ //
796
+ // Previous approaches that FAILED:
797
+ // - sendLoggingMessage: goes to debug logs, not AI conversation
798
+ // - instructions field: not supported by Claude Code or Claude CLI
799
+ //
800
+ // No runtime code needed here — the instruction is baked into the
801
+ // tool schema returned by ListTools.
743
802
  }
744
803
  // ─── v2.0 Step 6: Initialize SyncBus (Telepathy) ───
745
804
  // Fire-and-forget — SyncBus is non-critical for startup.
@@ -102,17 +102,36 @@ export async function getSetting(key, defaultValue = "") {
102
102
  export async function setSetting(key, value) {
103
103
  await initConfigStorage();
104
104
  const client = getClient();
105
- await client.execute({
106
- sql: `
107
- INSERT INTO system_settings (key, value, updated_at)
108
- VALUES (?, ?, CURRENT_TIMESTAMP)
109
- ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = CURRENT_TIMESTAMP
110
- `,
111
- args: [key, value],
112
- });
113
- // Keep the cache in sync so getSettingSync() reflects the new value immediately.
114
- if (settingsCache) {
115
- settingsCache[key] = value;
105
+ // Retry with exponential backoff for SQLITE_BUSY (concurrent writes).
106
+ // The dashboard and load tests can fire many parallel setting saves.
107
+ const MAX_RETRIES = 5;
108
+ const BASE_DELAY_MS = 20;
109
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
110
+ try {
111
+ await client.execute({
112
+ sql: `
113
+ INSERT INTO system_settings (key, value, updated_at)
114
+ VALUES (?, ?, CURRENT_TIMESTAMP)
115
+ ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = CURRENT_TIMESTAMP
116
+ `,
117
+ args: [key, value],
118
+ });
119
+ // Keep the cache in sync so getSettingSync() reflects the new value immediately.
120
+ if (settingsCache) {
121
+ settingsCache[key] = value;
122
+ }
123
+ return; // Success — exit
124
+ }
125
+ catch (err) {
126
+ const isBusy = err?.code === "SQLITE_BUSY" || err?.rawCode === 5;
127
+ if (isBusy && attempt < MAX_RETRIES) {
128
+ // Exponential backoff + jitter
129
+ const delay = BASE_DELAY_MS * Math.pow(2, attempt) + Math.random() * 10;
130
+ await new Promise(r => setTimeout(r, delay));
131
+ continue;
132
+ }
133
+ throw err; // Not SQLITE_BUSY or retries exhausted
134
+ }
116
135
  }
117
136
  }
118
137
  export async function getAllSettings() {
@@ -129,3 +148,17 @@ export async function getAllSettings() {
129
148
  }
130
149
  return settings;
131
150
  }
151
+ /**
152
+ * Closes the config SQLite client to release the file handle on prism-config.db.
153
+ * Called by the lifecycle module during graceful shutdown.
154
+ */
155
+ export function closeConfigStorage() {
156
+ if (configClient) {
157
+ try {
158
+ configClient.close();
159
+ }
160
+ catch (e) {
161
+ console.error(`[ConfigStorage] Error closing db:`, e);
162
+ }
163
+ }
164
+ }
@@ -1391,4 +1391,35 @@ export class SqliteStorage {
1391
1391
  });
1392
1392
  debugLog(`[SqliteStorage] Adjusted importance for ${id} by ${delta > 0 ? "+" : ""}${delta}`);
1393
1393
  }
1394
+ // ─── v4.2: Graduated Insights Query ──────────────────────────
1395
+ //
1396
+ // Returns ledger entries that have "graduated" — i.e., their
1397
+ // importance score has reached the threshold (default 7).
1398
+ // Used by knowledge_sync_rules to physically write insights
1399
+ // into .cursorrules / .clauderules files at the project repo path.
1400
+ async getGraduatedInsights(project, userId, minImportance = 7) {
1401
+ const result = await this.db.execute({
1402
+ sql: `SELECT id, project, user_id, role, summary, importance,
1403
+ event_type, decisions, created_at
1404
+ FROM session_ledger
1405
+ WHERE project = ? AND user_id = ?
1406
+ AND importance >= ?
1407
+ AND deleted_at IS NULL
1408
+ AND archived_at IS NULL
1409
+ ORDER BY importance DESC, created_at DESC`,
1410
+ args: [project, userId, minImportance],
1411
+ });
1412
+ return result.rows.map(row => ({
1413
+ id: row.id,
1414
+ project: row.project,
1415
+ user_id: row.user_id,
1416
+ role: row.role || "global",
1417
+ summary: row.summary,
1418
+ importance: Number(row.importance),
1419
+ event_type: row.event_type || "session",
1420
+ decisions: this.parseJsonColumn(row.decisions),
1421
+ created_at: row.created_at,
1422
+ conversation_id: "",
1423
+ }));
1424
+ }
1394
1425
  }