prism-mcp-server 20.2.1 → 20.2.2

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.
@@ -32,6 +32,7 @@ import { getLLMProvider } from "../utils/llm/factory.js";
32
32
  import { buildVaultDirectory } from "../utils/vaultExporter.js";
33
33
  import { redactSettings } from "../tools/commonHelpers.js";
34
34
  import { handleGraphRoutes } from "./graphRouter.js";
35
+ import { isDashboardSettingKeyAllowed, isDashboardSettingValueAllowed } from "./settingsPolicy.js";
35
36
  import { safeCompare, generateToken, isAuthenticated, createRateLimiter, initJWKS, } from "./authUtils.js";
36
37
  const PORT = parseInt(process.env.PRISM_DASHBOARD_PORT || "3000", 10);
37
38
  /** Read HTTP request body as string (Buffer-based to avoid GC thrash on large imports) */
@@ -633,26 +634,20 @@ return false;}
633
634
  try {
634
635
  const body = await readBody(req);
635
636
  const parsed = JSON.parse(body);
636
- if (parsed.key && parsed.value !== undefined) {
637
+ if (typeof parsed.key === "string" && parsed.key && parsed.value !== undefined) {
637
638
  // SECURITY: Allowlist of dashboard-settable keys to prevent
638
639
  // credential overwrite (SUPABASE_KEY, STRIPE_SECRET_KEY, etc.)
639
- const SETTABLE_KEYS = new Set([
640
- "PRISM_STORAGE",
641
- "embedding_provider", "embedding_model",
642
- "PRISM_ENABLE_HIVEMIND", "PRISM_DARK_FACTORY_ENABLED",
643
- "PRISM_TASK_ROUTER_ENABLED", "PRISM_SCHOLAR_ENABLED",
644
- "PRISM_HDC_ENABLED", "PRISM_ACTR_ENABLED",
645
- "PRISM_GRAPH_PRUNING_ENABLED",
646
- ]);
647
- const isSkillKey = parsed.key.startsWith("skill:");
648
- const isTTLKey = parsed.key.startsWith("ttl:");
649
- const isAutoloadKey = parsed.key.startsWith("autoload:");
650
- if (!SETTABLE_KEYS.has(parsed.key) && !isSkillKey && !isTTLKey && !isAutoloadKey) {
640
+ if (!isDashboardSettingKeyAllowed(parsed.key)) {
651
641
  res.writeHead(403, { "Content-Type": "application/json" });
652
642
  return res.end(JSON.stringify({ error: `Setting key "${parsed.key}" is not allowed via the dashboard.` }));
653
643
  }
644
+ const value = String(parsed.value);
645
+ if (!isDashboardSettingValueAllowed(parsed.key, value)) {
646
+ res.writeHead(400, { "Content-Type": "application/json" });
647
+ return res.end(JSON.stringify({ error: `Invalid value for setting "${parsed.key}".` }));
648
+ }
654
649
  const { setSetting } = await import("../storage/configStorage.js");
655
- await setSetting(parsed.key, String(parsed.value));
650
+ await setSetting(parsed.key, value);
656
651
  res.writeHead(200, { "Content-Type": "application/json" });
657
652
  return res.end(JSON.stringify({ ok: true, key: parsed.key, value: parsed.value }));
658
653
  }
@@ -0,0 +1,41 @@
1
+ const DASHBOARD_SETTABLE_KEYS = new Set([
2
+ "PRISM_STORAGE",
3
+ "dashboard_theme", "default_context_depth", "max_tokens",
4
+ "default_role", "agent_name", "autoload_projects",
5
+ "auto_capture", "hivemind_enabled", "task_router_enabled",
6
+ "embedding_provider", "embedding_model",
7
+ "PRISM_ENABLE_HIVEMIND", "PRISM_DARK_FACTORY_ENABLED",
8
+ "PRISM_TASK_ROUTER_ENABLED", "PRISM_SCHOLAR_ENABLED",
9
+ "PRISM_HDC_ENABLED", "PRISM_ACTR_ENABLED",
10
+ "PRISM_GRAPH_PRUNING_ENABLED",
11
+ ]);
12
+ /** Restrict the generic settings endpoint to documented dashboard controls. */
13
+ export function isDashboardSettingKeyAllowed(key) {
14
+ return DASHBOARD_SETTABLE_KEYS.has(key) || key.startsWith("ttl:") ||
15
+ key.startsWith("autoload:") || key.startsWith("repo_path:");
16
+ }
17
+ /** Validate settings that later enter tool descriptions, paths, or numeric limits. */
18
+ export function isDashboardSettingValueAllowed(key, value) {
19
+ if (value.length > 4096 || /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/.test(value))
20
+ return false;
21
+ if (key === "default_context_depth")
22
+ return ["quick", "standard", "deep"].includes(value);
23
+ if (key === "dashboard_theme")
24
+ return ["dark", "midnight", "purple"].includes(value);
25
+ if (key === "PRISM_STORAGE")
26
+ return ["auto", "local", "supabase"].includes(value);
27
+ if (key === "max_tokens")
28
+ return /^\d+$/.test(value) && Number(value) <= 100_000;
29
+ if (["auto_capture", "hivemind_enabled", "task_router_enabled"].includes(key)) {
30
+ return value === "true" || value === "false";
31
+ }
32
+ if (key === "agent_name")
33
+ return value.length <= 80 && !/[<>]/.test(value);
34
+ if (key === "default_role")
35
+ return /^[a-zA-Z0-9._:-]{0,64}$/.test(value);
36
+ if (key === "autoload_projects") {
37
+ const projects = value.split(",").map((project) => project.trim()).filter(Boolean);
38
+ return projects.length <= 20 && projects.every((project) => /^[a-zA-Z0-9][a-zA-Z0-9._/-]{0,127}$/.test(project));
39
+ }
40
+ return true;
41
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Canonical local-first orchestration contract shared by every MCP host.
3
+ *
4
+ * Keep this policy host-neutral. Claude Code, Gemini CLI, and Codex also get
5
+ * the same text in their native instruction files; Cursor and Claude Desktop
6
+ * receive it through the MCP initialize instructions and tool metadata.
7
+ */
8
+ export const LOCAL_FIRST_POLICY_LINES = [
9
+ "## Prism local-first orchestration",
10
+ "For bounded, verifiable delegated work, use Prism's local worker before any host-native or background subagent.",
11
+ "When session_task_route is available, call it first. If it returns target `claw`, call `prism_infer` with the",
12
+ "recommended arguments, `cloud_fallback: false`, and the `project` and `conversation_id` from this session when known.",
13
+ "Prism loads that project's dashboard-configured quick, standard, or deep memory and selects a RAM-safe local model.",
14
+ "Never create host-native or background subagents for routine work, never fan out, and never nest agents.",
15
+ "If local inference is unavailable, refused, degraded, or the task requires host tools or reserved judgment, continue",
16
+ "in the current host thread. A host-native subagent is a last resort: at most one, no nesting, using the configured",
17
+ "economy model. The current host remains responsible for verifying local output before using it.",
18
+ ];
19
+ export const LOCAL_FIRST_POLICY_ID = "local-first";
20
+ export const LOCAL_FIRST_POLICY_TEXT = LOCAL_FIRST_POLICY_LINES.join(" ");
@@ -142,14 +142,11 @@ function getFirstSearchContent() {
142
142
  instructions: [
143
143
  "Start a fresh conversation with your AI agent",
144
144
  'Ask: "What did we set up in our last session?"',
145
- "The agent will call knowledge_search or session_load_context",
145
+ "The agent will call session_bootstrap before answering",
146
146
  "You should see your saved memory returned!",
147
147
  ],
148
- codeSnippet: `// The agent calls:
149
- session_load_context({
150
- project: "my-first-project",
151
- level: "standard"
152
- })
148
+ codeSnippet: `// The agent calls once with no project or depth override:
149
+ session_bootstrap({})
153
150
  // → Returns your saved summary + any open TODOs`,
154
151
  nextStep: "advanced_tour",
155
152
  progress: 71,
package/dist/server.js CHANGED
@@ -18,7 +18,7 @@
18
18
  *
19
19
  * HOW MCP WORKS (simplified):
20
20
  * 1. The AI client (e.g., Claude) connects via stdin/stdout
21
- * 2. On connect, the client receives our capabilities (tools + prompts + resources)
21
+ * 2. On connect, the client receives our capabilities (tools + prompts + resources + logging)
22
22
  * 3. The client can:
23
23
  * - Call tools (brave_web_search, session_save_ledger, etc.)
24
24
  * - List/get prompts (/resume_session slash command)
@@ -83,10 +83,12 @@ import { ddInfo, ddError as ddLogError } from "./utils/ddLogger.js";
83
83
  import { inferenceMetricsHandler } from "./utils/inferenceMetrics.js";
84
84
  import { recordInvocation } from "./utils/analytics.js";
85
85
  import { BOUNDARIES_TEXT } from "./boundaries/boundaries.js";
86
+ import { triggerSkillManifestSync } from "./skillManifestSync.js";
87
+ import { LOCAL_FIRST_POLICY_TEXT } from "./localFirstPolicy.js";
86
88
  // ─── Import Tool Definitions (schemas) and Handlers (implementations) ─────
87
89
  import { WEB_SEARCH_TOOL, BRAVE_WEB_SEARCH_CODE_MODE_TOOL, LOCAL_SEARCH_TOOL, BRAVE_LOCAL_SEARCH_CODE_MODE_TOOL, CODE_MODE_TRANSFORM_TOOL, BRAVE_ANSWERS_TOOL, RESEARCH_PAPER_ANALYSIS_TOOL, webSearchHandler, braveWebSearchCodeModeHandler, localSearchHandler, braveLocalSearchCodeModeHandler, codeModeTransformHandler, braveAnswersHandler, researchPaperAnalysisHandler, } from "./tools/index.js";
88
90
  // Session memory tools — only used if Supabase is configured
89
- import { SESSION_SAVE_LEDGER_TOOL, SESSION_SAVE_HANDOFF_TOOL, SESSION_LOAD_CONTEXT_TOOL, KNOWLEDGE_SEARCH_TOOL, KNOWLEDGE_FORGET_TOOL,
91
+ import { SESSION_SAVE_LEDGER_TOOL, SESSION_SAVE_HANDOFF_TOOL, SESSION_LOAD_CONTEXT_TOOL, SESSION_BOOTSTRAP_TOOL, KNOWLEDGE_SEARCH_TOOL, KNOWLEDGE_FORGET_TOOL,
90
92
  // ─── v0.4.0: New tool definitions (Enhancements #2 and #4) ───
91
93
  SESSION_COMPACT_LEDGER_TOOL, SESSION_SEARCH_MEMORY_TOOL,
92
94
  // ─── v2.0: Time Travel tool definitions ───
@@ -118,7 +120,7 @@ ONBOARDING_WIZARD_TOOL, EXTRACT_ENTITIES_TOOL, API_ANALYTICS_TOOL, BACKUP_DATABA
118
120
  // v15.5: Knowledge Ingestion
119
121
  KNOWLEDGE_INGEST_TOOL,
120
122
  // v19.2: Inference Metrics
121
- INFERENCE_METRICS_TOOL, sessionSaveLedgerHandler, sessionSaveHandoffHandler, sessionLoadContextHandler, knowledgeSearchHandler, knowledgeForgetHandler,
123
+ INFERENCE_METRICS_TOOL, sessionSaveLedgerHandler, sessionSaveHandoffHandler, sessionLoadContextHandler, sessionBootstrapHandler, knowledgeSearchHandler, knowledgeForgetHandler,
122
124
  // ─── v0.4.0: New tool handlers ───
123
125
  compactLedgerHandler, sessionSearchMemoryHandler, backfillEmbeddingsHandler, sessionBackfillLinksHandler, sessionSynthesizeEdgesHandler, sessionCognitiveRouteHandler,
124
126
  // ─── v2.0: Time Travel handlers ───
@@ -174,40 +176,16 @@ const BASE_TOOLS = [
174
176
  RESEARCH_PAPER_ANALYSIS_TOOL, // gemini_research_paper_analysis — paper analysis
175
177
  PRISM_INFER_TOOL, // prism_infer — local-first inference (token-saving cascade)
176
178
  ];
177
- // ─── v4.1 FIX: Build Session Memory Tools dynamically ────────
178
- // The session_load_context tool description is dynamically modified
179
- // to include auto-load project instructions. Tool descriptions are
180
- // the ONLY mechanism guaranteed by ALL MCP clients (Antigravity,
181
- // Claude Code, Claude CLI). The `instructions` field and
182
- // sendLoggingMessage are NOT reliably surfaced by any client.
183
- //
184
- // buildSessionMemoryTools() is called inside createServer() AFTER
185
- // initConfigStorage() has run, so getSettingSync() returns real
186
- // dashboard config values.
187
- function buildSessionMemoryTools(autoloadList) {
188
- // Dynamically inject auto-load instruction into session_load_context description.
189
- // CRITICAL: The instruction is PREPENDED so it appears FIRST in the tool description.
190
- // LLMs read top-to-bottom — appending the instruction at the end caused it to be
191
- // deprioritized and skipped by both Antigravity and Claude CLI.
192
- let loadContextTool = SESSION_LOAD_CONTEXT_TOOL;
193
- if (autoloadList.length > 0) {
194
- const projectList = autoloadList.join(", ");
195
- // Read the preferred context depth from dashboard config (defaults to "standard")
196
- const defaultLevel = getSettingSync("default_context_depth", "standard");
197
- const autoLoadPrefix = `IMPORTANT: At the start of every conversation, you MUST call this tool ` +
198
- `for each of these projects: ${projectList}. Do this before any other work. ` +
199
- `Load level should be "${defaultLevel}" unless the user specifies otherwise. ` +
200
- `Do NOT skip this. Do NOT claim the tool is unavailable. The tool IS available — just call it.\n` +
201
- `Then display the agent identity from the response: \`🤖 Agent: <role> — <name>\`\n\n`;
202
- loadContextTool = {
203
- ...SESSION_LOAD_CONTEXT_TOOL,
204
- description: autoLoadPrefix + SESSION_LOAD_CONTEXT_TOOL.description,
205
- };
206
- }
179
+ // Hook-free first-turn startup is owned by session_bootstrap. Its static tool
180
+ // description is surfaced by native MCP hosts before any tool call and keeps
181
+ // project, depth, identity, and role selection inside dashboard configuration.
182
+ // session_load_context remains available for explicit reloads and legacy hosts.
183
+ function buildSessionMemoryTools() {
207
184
  return [
185
+ SESSION_BOOTSTRAP_TOOL, // session_bootstrap — hook-free configured first-turn greeting + context
208
186
  SESSION_SAVE_LEDGER_TOOL, // session_save_ledger — append immutable session log
209
187
  SESSION_SAVE_HANDOFF_TOOL, // session_save_handoff — upsert latest project state (now with OCC)
210
- loadContextTool, // session_load_context — progressive context loading (+ auto-load instruction)
188
+ SESSION_LOAD_CONTEXT_TOOL, // session_load_context — explicit project reload / legacy fallback
211
189
  KNOWLEDGE_SEARCH_TOOL, // knowledge_search — search accumulated knowledge
212
190
  KNOWLEDGE_FORGET_TOOL, // knowledge_forget — prune bad/old memories
213
191
  SESSION_COMPACT_LEDGER_TOOL, // session_compact_ledger — auto-compact old ledger entries (v0.4.0)
@@ -272,11 +250,9 @@ const activeSubscriptions = new Set();
272
250
  let storageReady = null;
273
251
  let storageIsReady = false;
274
252
  // ─── v5.2.1: Deferred Auto-Push Tracking ─────────────────────
275
- // Tracks whether any client has already called session_load_context.
276
- // Used by the deferred auto-push to skip redundant context injection.
277
- // This ensures Claude CLI (which calls the tool via its hook within
278
- // seconds) is never affected, while Antigravity gets a fallback push
279
- // when the model fails to comply with auto-load instructions.
253
+ // Tracks whether any client has already called session_bootstrap or
254
+ // session_load_context. Used by deferred auto-push to avoid duplicates when a
255
+ // native host complies with the hook-free first-turn instruction.
280
256
  let contextLoadedByClient = false;
281
257
  /**
282
258
  * Notifies subscribed clients that a resource has changed.
@@ -304,11 +280,14 @@ export function notifyResourceUpdate(project, server) {
304
280
  * - prompts: /resume_session — inject previous context before LLM thinks
305
281
  * - resources: memory://{project}/handoff — paperclip-attachable session state
306
282
  * with subscribe support for live refresh
283
+ * - logging: Diagnostic notifications for auto-push, Telepathy, and watchdog
284
+ * events. Clients may hide these; logging does not guarantee a
285
+ * visible assistant greeting.
307
286
  */
308
287
  export function getAllPossibleTools() {
309
288
  return [
310
289
  ...BASE_TOOLS,
311
- ...buildSessionMemoryTools([]),
290
+ ...buildSessionMemoryTools(),
312
291
  ...AGENT_REGISTRY_TOOLS,
313
292
  SESSION_TASK_ROUTE_TOOL,
314
293
  SESSION_START_PIPELINE_TOOL,
@@ -317,15 +296,8 @@ export function getAllPossibleTools() {
317
296
  ];
318
297
  }
319
298
  export function getAvailableTools() {
320
- // ─── v4.1 FIX: Auto-Load via Dynamic Tool Descriptions ────────
321
- // Read auto-load projects EXCLUSIVELY from dashboard config
322
- // (available after initConfigStorage() in startServer).
323
- //
324
- // ARCHITECTURE DECISION: We inject the auto-load instruction into
325
- // the session_load_context TOOL DESCRIPTION, not into `instructions`
326
- // or `sendLoggingMessage`. Tool descriptions are the ONLY mechanism
327
- // guaranteed by ALL MCP clients (Antigravity, Claude Code, Claude CLI).
328
- //
299
+ // Read auto-load projects exclusively from dashboard config so startup
300
+ // diagnostics and session_bootstrap share one source of truth.
329
301
  // The PRISM_AUTOLOAD_PROJECTS env var has been removed — the dashboard
330
302
  // is the single source of truth. This prevents mismatches between
331
303
  // env var and dashboard settings causing duplicate project loads.
@@ -335,7 +307,7 @@ export function getAvailableTools() {
335
307
  if (autoloadList.length > 0) {
336
308
  console.error(`[Prism] Auto-load projects (dashboard): ${autoloadList.join(', ')}`);
337
309
  }
338
- const SESSION_MEMORY_TOOLS = buildSessionMemoryTools(autoloadList);
310
+ const SESSION_MEMORY_TOOLS = buildSessionMemoryTools();
339
311
  return [
340
312
  ...BASE_TOOLS,
341
313
  ...SESSION_MEMORY_TOOLS,
@@ -344,6 +316,20 @@ export function getAvailableTools() {
344
316
  ...(PRISM_DARK_FACTORY_ENABLED ? [SESSION_START_PIPELINE_TOOL, SESSION_CHECK_PIPELINE_STATUS_TOOL, SESSION_ABORT_PIPELINE_TOOL] : []),
345
317
  ];
346
318
  }
319
+ export const PRISM_SERVER_INSTRUCTIONS = `Prism MCP — The Mind Palace for AI Agents. On the first user turn of every conversation, ` +
320
+ `including greetings, call session_bootstrap exactly once with {} before any user-facing response. ` +
321
+ `Emit no preamble. Print the complete tool result verbatim as the entire first-turn startup display, before any optional ` +
322
+ `answer. Do not summarize, paraphrase, rename headings, reformat, or omit any returned section. Preserve its order and ` +
323
+ `line content. For a greeting-only prompt, stop after the verbatim startup display. ` +
324
+ `Do not substitute session_load_context while session_bootstrap is available; use session_load_context ` +
325
+ `only for an explicit project reload or as an older-server fallback. ` +
326
+ `Use session_save_ledger to log completed work and session_save_handoff to preserve state for the next session. ` +
327
+ `Reuse the conversation_id returned by session_bootstrap in structuredContent for those saves and for ` +
328
+ `session_detect_drift, the 60-minute goal-alignment drift check. Do not add the id to the visible greeting.\n\n` +
329
+ `${LOCAL_FIRST_POLICY_TEXT}\n\n` +
330
+ `Architecture: session_save_ledger and session_save_handoff require context loaded by session_bootstrap ` +
331
+ `or session_load_context when a conversation_id is supplied. ${BOUNDARIES_TEXT} ` +
332
+ `All cloud inference routes through the Synalux portal for billing, tier-gating, and audit.`;
347
333
  export function createServer() {
348
334
  const ALL_TOOLS = getAvailableTools();
349
335
  const server = new Server({
@@ -352,14 +338,15 @@ export function createServer() {
352
338
  }, {
353
339
  capabilities: {
354
340
  tools: {},
341
+ logging: {},
355
342
  // ─── v0.4.0: Prompt capability (Enhancement #1) ───
356
343
  ...(SESSION_MEMORY_ENABLED ? { prompts: {} } : {}),
357
344
  // ─── v0.4.0: Resource capability (Enhancement #3) ───
358
345
  ...(SESSION_MEMORY_ENABLED ? { resources: { subscribe: true } } : {}),
359
346
  },
360
- // Supplementary signal not all clients support this field.
361
- // Primary mechanism is the dynamic tool description above.
362
- 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.\n\nArchitecture: session_save_ledger and session_save_handoff require a loaded project context (conversation_id that called session_load_context). ${BOUNDARIES_TEXT} All cloud inference routes through the Synalux portal for billing, tier-gating, and audit.`,
347
+ // Supplementary signal for clients that surface MCP server instructions.
348
+ // The matching session_bootstrap tool description is the primary path.
349
+ instructions: PRISM_SERVER_INSTRUCTIONS,
363
350
  });
364
351
  // ── Handler: Initialize ──
365
352
  // NOTE: The SDK's built-in _oninitialize() handles the Initialize request.
@@ -753,6 +740,12 @@ export function createServer() {
753
740
  contextLoadedByClient = true; // v5.2.1: suppress deferred auto-push
754
741
  result = await sessionLoadContextHandler(args);
755
742
  break;
743
+ case "session_bootstrap":
744
+ if (!SESSION_MEMORY_ENABLED)
745
+ throw new Error("Session memory not configured. Set SUPABASE_URL and SUPABASE_KEY.");
746
+ contextLoadedByClient = true;
747
+ result = await sessionBootstrapHandler(args);
748
+ break;
756
749
  case "knowledge_search":
757
750
  if (!SESSION_MEMORY_ENABLED)
758
751
  throw new Error("Session memory not configured. Set SUPABASE_URL and SUPABASE_KEY.");
@@ -1095,13 +1088,15 @@ export function createSandboxServer() {
1095
1088
  }, {
1096
1089
  capabilities: {
1097
1090
  tools: {},
1091
+ logging: {},
1098
1092
  prompts: {},
1099
1093
  resources: { subscribe: true },
1100
1094
  },
1095
+ instructions: PRISM_SERVER_INSTRUCTIONS,
1101
1096
  });
1102
1097
  // Register all tool listings unconditionally
1103
1098
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
1104
- tools: [...BASE_TOOLS, ...buildSessionMemoryTools([]), ...AGENT_REGISTRY_TOOLS, SESSION_TASK_ROUTE_TOOL, SESSION_START_PIPELINE_TOOL, SESSION_CHECK_PIPELINE_STATUS_TOOL, SESSION_ABORT_PIPELINE_TOOL],
1099
+ tools: [...BASE_TOOLS, ...buildSessionMemoryTools(), ...AGENT_REGISTRY_TOOLS, SESSION_TASK_ROUTE_TOOL, SESSION_START_PIPELINE_TOOL, SESSION_CHECK_PIPELINE_STATUS_TOOL, SESSION_ABORT_PIPELINE_TOOL],
1105
1100
  }));
1106
1101
  // Register prompts listing so scanners see resume_session
1107
1102
  server.setRequestHandler(ListPromptsRequestSchema, async () => ({
@@ -1234,6 +1229,26 @@ export async function startServer() {
1234
1229
  const transport = new StdioServerTransport();
1235
1230
  await server.connect(transport);
1236
1231
  console.error(`[Prism] MCP Server successfully started and listening on stdio...`);
1232
+ // Start the authoritative tier-skill refresh only after the MCP transport is
1233
+ // connected. session_load_context awaits this same single-flight promise,
1234
+ // while the initialize handshake is never held behind portal I/O. The
1235
+ // recurring refresh enforces tier changes even in long-lived native hosts
1236
+ // that do not call session_load_context again.
1237
+ const runSkillManifestSync = () => {
1238
+ void triggerSkillManifestSync().then((result) => {
1239
+ if (result.status === "failed" || result.status === "partial") {
1240
+ console.error(`[Prism Skill Sync] Refresh failed; retained last-good skills: ${result.error}`);
1241
+ }
1242
+ else if (result.conflicts.length > 0) {
1243
+ console.error(`[Prism Skill Sync] Preserved unowned/modified native skill conflicts: ${result.conflicts.join(", ")}`);
1244
+ }
1245
+ }).catch((error) => {
1246
+ console.error(`[Prism Skill Sync] Unexpected refresh failure: ${error instanceof Error ? error.message : String(error)}`);
1247
+ });
1248
+ };
1249
+ runSkillManifestSync();
1250
+ const skillManifestRefresh = setInterval(runSkillManifestSync, 5 * 60 * 1000);
1251
+ skillManifestRefresh.unref();
1237
1252
  // Register graceful shutdown handlers (SIGTERM, SIGINT, SIGHUP, stdin close).
1238
1253
  // The stdin close handler is critical — when MCP clients disconnect, they
1239
1254
  // often just close the pipe without sending a signal, leaving zombie processes.
@@ -1255,21 +1270,18 @@ export async function startServer() {
1255
1270
  ]).catch(err => {
1256
1271
  console.error(`[Prism] Storage pre-warm failed (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
1257
1272
  });
1258
- // ─── v4.1: Auto-Load via dynamic tool descriptions ──────────
1259
- // The session_load_context tool description is dynamically modified
1260
- // in createServer() buildSessionMemoryTools() to include the
1261
- // auto-load projects list. Tool descriptions are surfaced by ALL
1262
- // MCP clients — this is the primary mechanism.
1273
+ // ─── Hook-free auto-load via session_bootstrap ─────────────
1274
+ // Static tool and MCP server instructions make the first-turn call
1275
+ // explicit while dashboard settings remain server-owned.
1263
1276
  //
1264
1277
  // ─── v5.2.1: Deferred Auto-Push (fallback for non-compliant models) ──
1265
1278
  // After storage warms up, wait AUTOLOAD_PUSH_DELAY_MS. If the client
1266
- // (model) hasn't called session_load_context by then, push context via
1279
+ // (model) hasn't loaded context through either tool by then, push via
1267
1280
  // sendLoggingMessage as a last resort. This is a FALLBACK — it's not
1268
1281
  // guaranteed to be surfaced by all clients, but it's better than nothing.
1269
1282
  //
1270
- // Why 10 seconds? Claude CLI always calls the tool within 2-3 seconds
1271
- // via its SessionStart hook. Antigravity models that comply also call it
1272
- // within 5 seconds. 10s gives ample time for well-behaved clients.
1283
+ // Ten seconds gives native hosts time to honor the bootstrap instruction
1284
+ // while preserving the fallback for clients that do not.
1273
1285
  const AUTOLOAD_PUSH_DELAY_MS = 10_000;
1274
1286
  // Read autoload projects from dashboard config (same source as createServer)
1275
1287
  const pushAutoloadList = getSettingSync("autoload_projects", "")
@@ -1279,12 +1291,12 @@ export async function startServer() {
1279
1291
  storageReady?.then(async () => {
1280
1292
  // Wait for the delay period to give the model a chance to call the tool
1281
1293
  await new Promise(r => setTimeout(r, AUTOLOAD_PUSH_DELAY_MS));
1282
- // If the client already called session_load_context, skip the push
1294
+ // If the client already loaded context through either tool, skip it.
1283
1295
  if (contextLoadedByClient) {
1284
1296
  console.error(`[Prism] Auto-push skipped — client already loaded context`);
1285
1297
  return;
1286
1298
  }
1287
- console.error(`[Prism] Auto-push triggered — model did not call session_load_context within ${AUTOLOAD_PUSH_DELAY_MS / 1000}s`);
1299
+ console.error(`[Prism] Auto-push triggered — model did not call session_bootstrap within ${AUTOLOAD_PUSH_DELAY_MS / 1000}s`);
1288
1300
  // Load and push context for each autoload project
1289
1301
  try {
1290
1302
  const storage = await getStorage();
@@ -111,14 +111,15 @@ export function requireContextLoaded(conversationId) {
111
111
  return {
112
112
  blocked: true,
113
113
  error: "context_not_loaded: session expired (6 h TTL). Call " +
114
- "session_load_context(project, conversation_id) again to reload context. " +
114
+ "session_bootstrap(conversation_id) or session_load_context(project, conversation_id) again. " +
115
115
  "(Enforced server-side — applies to every host.)",
116
116
  };
117
117
  }
118
118
  if (!s || !s.contextLoaded) {
119
119
  return {
120
120
  blocked: true,
121
- error: "context_not_loaded: call session_load_context(project, conversation_id) " +
121
+ error: "context_not_loaded: call session_bootstrap(conversation_id) or " +
122
+ "session_load_context(project, conversation_id) " +
122
123
  "before this action. This project-scoped tool needs confirmed working context " +
123
124
  "to act correctly. (Enforced server-side — applies to every host.)",
124
125
  };
@@ -141,7 +142,8 @@ export function requireContextLoaded(conversationId) {
141
142
  /** Best-effort telemetry from prism_infer. Never affects a safety decision. */
142
143
  export function noteInferenceForSession(conversationId, info) {
143
144
  // Only update sessions that already exist — don't create ghost stubs for
144
- // conversations that never called session_load_context. Ghost stubs would
145
+ // conversations that never called session_bootstrap or session_load_context.
146
+ // Ghost stubs would
145
147
  // accumulate in the LRU and crowd out legitimate registered sessions.
146
148
  const s = sessions.get(conversationId);
147
149
  if (!s)