prism-mcp-server 3.1.1 → 4.2.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/dist/config.js CHANGED
@@ -114,6 +114,14 @@ export const PRISM_DEBUG_LOGGING = process.env.PRISM_DEBUG_LOGGING === "true";
114
114
  // doesn't increase tool count.
115
115
  // Set PRISM_ENABLE_HIVEMIND=true to unlock the Agent Registry tools.
116
116
  export const PRISM_ENABLE_HIVEMIND = process.env.PRISM_ENABLE_HIVEMIND === "true";
117
+ // ─── v4.1: Auto-Load Projects ────────────────────────────────
118
+ // Auto-load is configured exclusively via the Mind Palace dashboard
119
+ // ("Auto-Load Projects" checkboxes in Settings). The setting is stored
120
+ // in prism-config.db and read at startup via getSettingSync().
121
+ //
122
+ // The PRISM_AUTOLOAD_PROJECTS env var has been removed — the dashboard
123
+ // is the single source of truth. This prevents mismatches between
124
+ // env var and dashboard values causing duplicate project loads.
117
125
  if (PRISM_AUTO_CAPTURE) {
118
126
  // Use console.error instead of debugLog here to prevent circular dependency
119
127
  if (PRISM_DEBUG_LOGGING) {
@@ -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
@@ -84,7 +85,9 @@ SESSION_HEALTH_CHECK_TOOL,
84
85
  // ─── Phase 2: GDPR Memory Deletion tool definition ───
85
86
  SESSION_FORGET_MEMORY_TOOL,
86
87
  // ─── v3.1: TTL Retention tool ───
87
- KNOWLEDGE_SET_RETENTION_TOOL, sessionSaveLedgerHandler, sessionSaveHandoffHandler, sessionLoadContextHandler, knowledgeSearchHandler, knowledgeForgetHandler,
88
+ KNOWLEDGE_SET_RETENTION_TOOL,
89
+ // v4.0: Active Behavioral Memory tools
90
+ SESSION_SAVE_EXPERIENCE_TOOL, KNOWLEDGE_UPVOTE_TOOL, KNOWLEDGE_DOWNVOTE_TOOL, sessionSaveLedgerHandler, sessionSaveHandoffHandler, sessionLoadContextHandler, knowledgeSearchHandler, knowledgeForgetHandler,
88
91
  // ─── v0.4.0: New tool handlers ───
89
92
  compactLedgerHandler, sessionSearchMemoryHandler,
90
93
  // ─── v2.0: Time Travel handlers ───
@@ -97,6 +100,8 @@ sessionHealthCheckHandler,
97
100
  sessionForgetMemoryHandler,
98
101
  // ─── v3.1: TTL Retention handler ───
99
102
  knowledgeSetRetentionHandler,
103
+ // v4.0: Active Behavioral Memory handlers
104
+ sessionSaveExperienceHandler, knowledgeUpvoteHandler, knowledgeDownvoteHandler,
100
105
  // ─── v3.0: Agent Hivemind tools ───
101
106
  AGENT_REGISTRY_TOOLS, agentRegisterHandler, agentHeartbeatHandler, agentListTeamHandler, } from "./tools/index.js";
102
107
  // ─── Dynamic Tool Registration ───────────────────────────────────
@@ -110,39 +115,52 @@ const BASE_TOOLS = [
110
115
  BRAVE_ANSWERS_TOOL, // brave_answers — AI-grounded answers
111
116
  RESEARCH_PAPER_ANALYSIS_TOOL, // gemini_research_paper_analysis — paper analysis
112
117
  ];
113
- // Session memory tools: only added when SUPABASE_URL + SUPABASE_KEY are set
114
- // REVIEWER NOTE: v0.4.0 adds 2 new tools here:
115
- // - session_compact_ledger (Enhancement #2): auto-rollup of old ledger entries
116
- // - session_search_memory (Enhancement #4): semantic search via pgvector embeddings
117
- const SESSION_MEMORY_TOOLS = [
118
- SESSION_SAVE_LEDGER_TOOL, // session_save_ledger append immutable session log
119
- SESSION_SAVE_HANDOFF_TOOL, // session_save_handoff — upsert latest project state (now with OCC)
120
- SESSION_LOAD_CONTEXT_TOOL, // session_load_context progressive context loading
121
- KNOWLEDGE_SEARCH_TOOL, // knowledge_search search accumulated knowledge
122
- KNOWLEDGE_FORGET_TOOL, // knowledge_forget prune bad/old memories
123
- SESSION_COMPACT_LEDGER_TOOL, // session_compact_ledger — auto-compact old ledger entries (v0.4.0)
124
- SESSION_SEARCH_MEMORY_TOOL, // session_search_memory semantic search via embeddings (v0.4.0)
125
- MEMORY_HISTORY_TOOL, // memory_history — view version timeline (v2.0)
126
- MEMORY_CHECKOUT_TOOL, // memory_checkout — revert to past version (v2.0)
127
- // ─── v2.0: Visual Memory tools ───
128
- SESSION_SAVE_IMAGE_TOOL, // session_save_image — save image to media vault (v2.0)
129
- SESSION_VIEW_IMAGE_TOOL, // session_view_image — retrieve image from vault (v2.0)
130
- // ─── v2.2.0: Health Check tool ───
131
- SESSION_HEALTH_CHECK_TOOL, // session_health_check brain integrity checker (v2.2.0)
132
- // ─── Phase 2: GDPR Memory Deletion tool ───
133
- SESSION_FORGET_MEMORY_TOOL, // session_forget_memory — GDPR-compliant memory deletion (Phase 2)
134
- // ─── v3.1: TTL Retention tool ───
135
- KNOWLEDGE_SET_RETENTION_TOOL, // knowledge_set_retentionset auto-expiry TTL for a project
136
- ];
137
- // Combine: always list ALL tools so scanners (Glama, Smithery, MCP Registry)
138
- // can enumerate the full capability set. Runtime guards in the CallTool handler
139
- // still prevent execution without valid Supabase credentials.
140
- const ALL_TOOLS = [
141
- ...BASE_TOOLS,
142
- ...SESSION_MEMORY_TOOLS,
143
- // v3.0: Agent Hivemind tools only when PRISM_ENABLE_HIVEMIND=true
144
- ...(PRISM_ENABLE_HIVEMIND ? AGENT_REGISTRY_TOOLS : []),
145
- ];
118
+ // ─── v4.1 FIX: Build Session Memory Tools dynamically ────────
119
+ // The session_load_context tool description is dynamically modified
120
+ // to include auto-load project instructions. Tool descriptions are
121
+ // the ONLY mechanism guaranteed by ALL MCP clients (Antigravity,
122
+ // Claude Code, Claude CLI). The `instructions` field and
123
+ // sendLoggingMessage are NOT reliably surfaced by any client.
124
+ //
125
+ // buildSessionMemoryTools() is called inside createServer() AFTER
126
+ // initConfigStorage() has run, so getSettingSync() returns real
127
+ // dashboard config values.
128
+ function buildSessionMemoryTools(autoloadList) {
129
+ // Dynamically inject auto-load instruction into session_load_context description
130
+ let loadContextTool = SESSION_LOAD_CONTEXT_TOOL;
131
+ if (autoloadList.length > 0) {
132
+ const projectList = autoloadList.join(", ");
133
+ loadContextTool = {
134
+ ...SESSION_LOAD_CONTEXT_TOOL,
135
+ description: SESSION_LOAD_CONTEXT_TOOL.description +
136
+ `\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.`,
137
+ };
138
+ }
139
+ return [
140
+ SESSION_SAVE_LEDGER_TOOL, // session_save_ledgerappend immutable session log
141
+ SESSION_SAVE_HANDOFF_TOOL, // session_save_handoff — upsert latest project state (now with OCC)
142
+ loadContextTool, // session_load_context progressive context loading (+ auto-load instruction)
143
+ KNOWLEDGE_SEARCH_TOOL, // knowledge_search search accumulated knowledge
144
+ KNOWLEDGE_FORGET_TOOL, // knowledge_forget prune bad/old memories
145
+ SESSION_COMPACT_LEDGER_TOOL, // session_compact_ledger — auto-compact old ledger entries (v0.4.0)
146
+ SESSION_SEARCH_MEMORY_TOOL, // session_search_memory — semantic search via embeddings (v0.4.0)
147
+ MEMORY_HISTORY_TOOL, // memory_history — view version timeline (v2.0)
148
+ MEMORY_CHECKOUT_TOOL, // memory_checkout revert to past version (v2.0)
149
+ // ─── v2.0: Visual Memory tools ───
150
+ SESSION_SAVE_IMAGE_TOOL, // session_save_image — save image to media vault (v2.0)
151
+ SESSION_VIEW_IMAGE_TOOL, // session_view_image — retrieve image from vault (v2.0)
152
+ // ─── v2.2.0: Health Check tool ───
153
+ SESSION_HEALTH_CHECK_TOOL, // session_health_check — brain integrity checker (v2.2.0)
154
+ // ─── Phase 2: GDPR Memory Deletion tool ───
155
+ SESSION_FORGET_MEMORY_TOOL, // session_forget_memory — GDPR-compliant memory deletion (Phase 2)
156
+ // ─── v3.1: TTL Retention tool ───
157
+ KNOWLEDGE_SET_RETENTION_TOOL, // knowledge_set_retention — set auto-expiry TTL for a project
158
+ // ─── v4.0: Active Behavioral Memory tools ───
159
+ SESSION_SAVE_EXPERIENCE_TOOL, // session_save_experience — record typed experience events
160
+ KNOWLEDGE_UPVOTE_TOOL, // knowledge_upvote — increase entry importance
161
+ KNOWLEDGE_DOWNVOTE_TOOL, // knowledge_downvote — decrease entry importance
162
+ ];
163
+ }
146
164
  // ─── v0.4.0: Resource Subscription Tracking ──────────────────────
147
165
  // REVIEWER NOTE: We track which project URIs clients have subscribed
148
166
  // to. When sessionSaveHandoffHandler successfully updates a project,
@@ -186,6 +204,35 @@ export function notifyResourceUpdate(project, server) {
186
204
  * with subscribe support for live refresh
187
205
  */
188
206
  export function createServer() {
207
+ // ─── v4.1 FIX: Auto-Load via Dynamic Tool Descriptions ────────
208
+ // Read auto-load projects EXCLUSIVELY from dashboard config
209
+ // (available after initConfigStorage() in startServer).
210
+ //
211
+ // ARCHITECTURE DECISION: We inject the auto-load instruction into
212
+ // the session_load_context TOOL DESCRIPTION, not into `instructions`
213
+ // or `sendLoggingMessage`. Tool descriptions are the ONLY mechanism
214
+ // guaranteed by ALL MCP clients (Antigravity, Claude Code, Claude CLI).
215
+ //
216
+ // The PRISM_AUTOLOAD_PROJECTS env var has been removed — the dashboard
217
+ // is the single source of truth. This prevents mismatches between
218
+ // env var and dashboard settings causing duplicate project loads.
219
+ const dashboardAutoload = getSettingSync("autoload_projects", "");
220
+ const autoloadList = dashboardAutoload
221
+ .split(",").map(p => p.trim()).filter(Boolean);
222
+ if (autoloadList.length > 0) {
223
+ console.error(`[Prism] Auto-load projects (dashboard): ${autoloadList.join(', ')}`);
224
+ }
225
+ // Build the dynamic tool list with auto-load instruction injected
226
+ const SESSION_MEMORY_TOOLS = buildSessionMemoryTools(autoloadList);
227
+ // Combine: always list ALL tools so scanners (Glama, Smithery, MCP Registry)
228
+ // can enumerate the full capability set. Runtime guards in the CallTool handler
229
+ // still prevent execution without valid Supabase credentials.
230
+ const ALL_TOOLS = [
231
+ ...BASE_TOOLS,
232
+ ...SESSION_MEMORY_TOOLS,
233
+ // v3.0: Agent Hivemind tools — only when PRISM_ENABLE_HIVEMIND=true
234
+ ...(PRISM_ENABLE_HIVEMIND ? AGENT_REGISTRY_TOOLS : []),
235
+ ];
189
236
  const server = new Server({
190
237
  name: SERVER_CONFIG.name,
191
238
  version: SERVER_CONFIG.version,
@@ -193,21 +240,13 @@ export function createServer() {
193
240
  capabilities: {
194
241
  tools: {},
195
242
  // ─── v0.4.0: Prompt capability (Enhancement #1) ───
196
- // REVIEWER NOTE: Declaring `prompts: {}` tells Claude Desktop
197
- // that we support the prompts/list and prompts/get methods.
198
- // This enables the /resume_session slash command in the UI.
199
- // Only enabled when Supabase is configured (prompts need
200
- // session data to be useful).
201
243
  ...(SESSION_MEMORY_ENABLED ? { prompts: {} } : {}),
202
244
  // ─── v0.4.0: Resource capability (Enhancement #3) ───
203
- // REVIEWER NOTE: Setting subscribe: true tells Claude Desktop
204
- // that we support resource subscriptions. When a user attaches
205
- // memory://project/handoff and the LLM later updates it,
206
- // we push an update notification so the attached context
207
- // is silently refreshed. Without subscribe:true, the
208
- // paperclipped context would become stale.
209
245
  ...(SESSION_MEMORY_ENABLED ? { resources: { subscribe: true } } : {}),
210
246
  },
247
+ // Supplementary signal — not all clients support this field.
248
+ // Primary mechanism is the dynamic tool description above.
249
+ 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.`,
211
250
  });
212
251
  // ── Handler: Initialize ──
213
252
  // NOTE: The SDK's built-in _oninitialize() handles the Initialize request.
@@ -586,6 +625,19 @@ export function createServer() {
586
625
  if (!SESSION_MEMORY_ENABLED)
587
626
  throw new Error("Session memory not configured. Set SUPABASE_URL and SUPABASE_KEY.");
588
627
  return await knowledgeSetRetentionHandler(args);
628
+ // ─── v4.0: Active Behavioral Memory Tools ───
629
+ case "session_save_experience":
630
+ if (!SESSION_MEMORY_ENABLED)
631
+ throw new Error("Session memory not configured. Set SUPABASE_URL and SUPABASE_KEY.");
632
+ return await sessionSaveExperienceHandler(args);
633
+ case "knowledge_upvote":
634
+ if (!SESSION_MEMORY_ENABLED)
635
+ throw new Error("Session memory not configured. Set SUPABASE_URL and SUPABASE_KEY.");
636
+ return await knowledgeUpvoteHandler(args);
637
+ case "knowledge_downvote":
638
+ if (!SESSION_MEMORY_ENABLED)
639
+ throw new Error("Session memory not configured. Set SUPABASE_URL and SUPABASE_KEY.");
640
+ return await knowledgeDownvoteHandler(args);
589
641
  // ─── v3.0: Agent Hivemind Tools ───
590
642
  case "agent_register":
591
643
  if (!SESSION_MEMORY_ENABLED)
@@ -645,7 +697,7 @@ export function createSandboxServer() {
645
697
  });
646
698
  // Register all tool listings unconditionally
647
699
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
648
- tools: [...BASE_TOOLS, ...SESSION_MEMORY_TOOLS, ...AGENT_REGISTRY_TOOLS],
700
+ tools: [...BASE_TOOLS, ...buildSessionMemoryTools([]), ...AGENT_REGISTRY_TOOLS],
649
701
  }));
650
702
  // Register prompts listing so scanners see resume_session
651
703
  server.setRequestHandler(ListPromptsRequestSchema, async () => ({
@@ -694,6 +746,9 @@ export function createSandboxServer() {
694
746
  * responses to stdout. Log messages go to stderr.
695
747
  */
696
748
  export async function startServer() {
749
+ // MUST BE FIRST: Kill any zombie processes and acquire the singleton PID lock
750
+ // before touching SQLite. This prevents lock contention on prism-config.db.
751
+ acquireLock();
697
752
  // Pre-warm the config settings cache BEFORE connecting the MCP transport.
698
753
  // This ensures getSettingSync() returns real values (agent_name, default_role)
699
754
  // during the Initialize handshake — zero extra latency for resource reads.
@@ -702,6 +757,10 @@ export async function startServer() {
702
757
  const server = createServer();
703
758
  const transport = new StdioServerTransport();
704
759
  await server.connect(transport);
760
+ // Register graceful shutdown handlers (SIGTERM, SIGINT, SIGHUP, stdin close).
761
+ // The stdin close handler is critical — when MCP clients disconnect, they
762
+ // often just close the pipe without sending a signal, leaving zombie processes.
763
+ registerShutdownHandlers();
705
764
  // Pre-warm storage AFTER connecting — fired async so we never block the
706
765
  // stdio handshake. Supabase REST initialization can take 500ms–5s; blocking
707
766
  // on it before server.connect() was the root cause of the 1m 56s CLI delay.
@@ -719,6 +778,18 @@ export async function startServer() {
719
778
  ]).catch(err => {
720
779
  console.error(`[Prism] Storage pre-warm failed (non-fatal): ${err}`);
721
780
  });
781
+ // ─── v4.1: Auto-Load is handled via dynamic tool descriptions ──
782
+ // The session_load_context tool description is dynamically modified
783
+ // in createServer() → buildSessionMemoryTools() to include the
784
+ // auto-load projects list. This is the ONLY universally reliable
785
+ // mechanism — tool descriptions are surfaced by ALL MCP clients.
786
+ //
787
+ // Previous approaches that FAILED:
788
+ // - sendLoggingMessage: goes to debug logs, not AI conversation
789
+ // - instructions field: not supported by Claude Code or Claude CLI
790
+ //
791
+ // No runtime code needed here — the instruction is baked into the
792
+ // tool schema returned by ListTools.
722
793
  }
723
794
  // ─── v2.0 Step 6: Initialize SyncBus (Telepathy) ───
724
795
  // 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
+ }