prism-mcp-server 20.2.0 → 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.
@@ -38,8 +38,8 @@ const CLAW_KEYWORDS = [
38
38
  "add field", "add column", "add property",
39
39
  "remove unused", "delete unused", "clean up",
40
40
  ];
41
- /** Keywords that suggest the task requires the host model's reasoning. */
42
- const HOST_KEYWORDS = [
41
+ /** Reserved judgment or host-tool boundaries that local inference cannot own. */
42
+ const HOST_BOUNDARY_KEYWORDS = [
43
43
  "architect", "architecture", "redesign", "design system",
44
44
  "debug complex", "investigate", "root cause", "diagnose",
45
45
  "security audit", "vulnerability", "penetration",
@@ -49,10 +49,56 @@ const HOST_KEYWORDS = [
49
49
  "migration strategy", "data migration",
50
50
  "api design", "schema design", "database design",
51
51
  "code review", "review the", "analyze the",
52
- "explain how", "explain why", "understand",
53
- "complex logic", "algorithm", "concurrent", "race condition",
52
+ "concurrent", "race condition",
54
53
  "integrate multiple", "cross-cutting",
55
- "plan", "strategy", "roadmap",
54
+ "implementation plan", "create a plan", "strategy", "roadmap",
55
+ ];
56
+ /** Explicit workflows that require host-side tools or external state. */
57
+ const HOST_TOOL_WORKFLOW_KEYWORDS = [
58
+ "inspect the repository", "search the repository", "read files",
59
+ "run command", "execute command", "run the tests", "run tests",
60
+ "apply the patch", "edit the file", "modify the file",
61
+ "commit", "push", "deploy", "publish",
62
+ "use the browser", "query the database", "database query",
63
+ ];
64
+ /**
65
+ * Natural-language action groups that reveal a repository workflow even when
66
+ * the prompt does not use one of the exact phrases above. Local inference can
67
+ * draft a bounded artifact, but it cannot own a read -> mutate -> verify tool
68
+ * sequence. Requiring two distinct groups avoids treating a single request
69
+ * such as "update version" as a host-only workflow.
70
+ */
71
+ const HOST_TOOL_ACTION_GROUPS = [
72
+ {
73
+ label: "repository inspection",
74
+ patterns: [
75
+ /\b(?:read|inspect|search|open)\b[^.!?\n]{0,100}\b(?:repository|repo|codebase|source|files?|test harness)\b/i,
76
+ ],
77
+ },
78
+ {
79
+ label: "workspace mutation",
80
+ patterns: [
81
+ /\b(?:persist|save|update|edit|modify|apply)\b[^.!?\n]{0,100}\b(?:regression tests?|tests?|files?|code|source|workflow|harness|patch|repository|repo)\b/i,
82
+ ],
83
+ },
84
+ {
85
+ label: "execution or verification",
86
+ patterns: [
87
+ /\b(?:run|execute)\b[^.!?\n]{0,100}\b(?:tests?|test suite|verification|build|type-?check|lint|commands?)\b/i,
88
+ /\b(?:verify|validate)\b[^.!?\n]{0,100}\b(?:tests?|build|behavior|workflow)\b/i,
89
+ ],
90
+ },
91
+ ];
92
+ /** Complexity signals that should select 27B when the task is bounded. */
93
+ const HIGH_COMPLEXITY_KEYWORDS = [
94
+ "complex logic", "algorithm", "dynamic programming", "constraint solver",
95
+ "parser", "compiler", "state machine", "graph traversal", "backtracking",
96
+ "multiple edge cases", "complete specification",
97
+ ];
98
+ /** Evidence that a difficult request is still a self-contained inference job. */
99
+ const BOUNDED_TASK_MARKERS = [
100
+ "self-contained", "standalone", "bounded", "single function", "one function",
101
+ "single file", "one file", "pure function", "complete specification",
56
102
  ];
57
103
  /** Conjunctions and sequential markers that indicate multi-step tasks. */
58
104
  const MULTI_STEP_MARKERS = [
@@ -64,6 +110,15 @@ const MULTI_STEP_MARKERS = [
64
110
  // Note: removed bare "1.", "2.", "3." — too many false positives
65
111
  // on version numbers (v1.2.3), decimals, and IP addresses.
66
112
  ];
113
+ const MAX_BOUNDED_FILES = 2;
114
+ const MAX_HOST_ROUTABLE_FILES = 5;
115
+ const MULTI_STEP_HOST_THRESHOLD = 2;
116
+ const HOST_TOOL_ACTION_GROUP_THRESHOLD = 2;
117
+ const HIGH_COMPLEXITY_MIN_SCORE = 7;
118
+ const VERY_HIGH_COMPLEXITY_MIN_SCORE = 8;
119
+ const VERY_HIGH_COMPLEXITY_SIGNAL_COUNT = 2;
120
+ const BOUNDED_HIGH_COMPLEXITY_CONFIDENCE = 0.85;
121
+ const HARD_HOST_BOUNDARY_CONFIDENCE = 0.95;
67
122
  // ─── Heuristic Engine ────────────────────────────────────────
68
123
  /**
69
124
  * Count how many keywords from a list appear in the text (case-insensitive).
@@ -78,19 +133,65 @@ function countKeywordHits(text, keywords) {
78
133
  }
79
134
  return hits;
80
135
  }
136
+ function findHostToolActionGroups(description) {
137
+ return HOST_TOOL_ACTION_GROUPS
138
+ .filter(({ patterns }) => patterns.some((pattern) => pattern.test(description)))
139
+ .map(({ label }) => label);
140
+ }
81
141
  /**
82
142
  * Compute a claw-affinity score from keyword analysis.
83
143
  * Returns a value between -1.0 (strongly host) and +1.0 (strongly claw).
84
144
  */
85
145
  function keywordSignal(description) {
86
146
  const clawHits = countKeywordHits(description, CLAW_KEYWORDS);
87
- const hostHits = countKeywordHits(description, HOST_KEYWORDS);
147
+ const hostHits = countKeywordHits(description, HOST_BOUNDARY_KEYWORDS);
88
148
  const total = clawHits + hostHits;
89
149
  if (total === 0)
90
150
  return 0; // No signal — neutral
91
151
  // Normalized difference: positive = claw, negative = host
92
152
  return (clawHits - hostHits) / total;
93
153
  }
154
+ /**
155
+ * Decide whether the task is eligible for local inference independently from
156
+ * how much model capacity it needs. A hard host boundary always wins.
157
+ */
158
+ function assessDelegability(args) {
159
+ const description = args.task_description;
160
+ const files = args.files_involved;
161
+ const boundaryKeywordHits = countKeywordHits(description, HOST_BOUNDARY_KEYWORDS);
162
+ const toolWorkflowHits = countKeywordHits(description, HOST_TOOL_WORKFLOW_KEYWORDS);
163
+ const toolActionGroups = findHostToolActionGroups(description);
164
+ const multiStepHits = countKeywordHits(description, MULTI_STEP_MARKERS);
165
+ const highComplexityHits = countKeywordHits(description, HIGH_COMPLEXITY_KEYWORDS);
166
+ const boundedByFiles = Boolean(files && files.length > 0 && files.length <= MAX_BOUNDED_FILES);
167
+ const boundedByDescription = countKeywordHits(description, BOUNDED_TASK_MARKERS) > 0;
168
+ const reasons = [];
169
+ if (boundaryKeywordHits > 0)
170
+ reasons.push("reserved host judgment");
171
+ if (toolWorkflowHits > 0)
172
+ reasons.push("host tools or external state required");
173
+ if (toolActionGroups.length >= HOST_TOOL_ACTION_GROUP_THRESHOLD) {
174
+ reasons.push(`host workflow actions: ${toolActionGroups.join(", ")}`);
175
+ }
176
+ if (multiStepHits >= MULTI_STEP_HOST_THRESHOLD)
177
+ reasons.push("multi-step workflow");
178
+ if ((files?.length ?? 0) > MAX_HOST_ROUTABLE_FILES)
179
+ reasons.push("cross-file scope");
180
+ if (args.estimated_scope === "refactor")
181
+ reasons.push("refactor scope");
182
+ const minimumComplexity = highComplexityHits >= VERY_HIGH_COMPLEXITY_SIGNAL_COUNT
183
+ ? VERY_HIGH_COMPLEXITY_MIN_SCORE
184
+ : highComplexityHits > 0
185
+ ? HIGH_COMPLEXITY_MIN_SCORE
186
+ : 1;
187
+ return {
188
+ hardHostBoundary: reasons.length > 0,
189
+ boundedHighComplexity: highComplexityHits > 0 && (boundedByFiles || boundedByDescription),
190
+ highComplexityHits,
191
+ minimumComplexity,
192
+ reasons,
193
+ };
194
+ }
94
195
  /**
95
196
  * Compute a claw-affinity score from file count.
96
197
  * ≤2 files → strongly claw (+1.0)
@@ -193,6 +294,22 @@ const WEIGHTS = {
193
294
  length: 0.10,
194
295
  multiStep: 0.10,
195
296
  };
297
+ /**
298
+ * Forward the deterministic complexity signal without choosing a model.
299
+ * prism_infer owns tier/thinking selection because it also sees the loaded
300
+ * memory size, installed models, live RAM, entitlements, and explicit caller
301
+ * overrides.
302
+ */
303
+ function buildRecommendedArgs(args, complexityScore) {
304
+ return {
305
+ prompt: args.task_description,
306
+ ...(args.project ? { project: args.project } : {}),
307
+ mode: "code",
308
+ task_complexity: complexityScore,
309
+ cloud_fallback: false,
310
+ escalation: "report",
311
+ };
312
+ }
196
313
  // ─── Router Core ─────────────────────────────────────────────
197
314
  /**
198
315
  * Compute the routing recommendation. Pure function.
@@ -216,6 +333,7 @@ export function computeRoute(args) {
216
333
  const sc = scopeSignal(estimated_scope);
217
334
  const ln = lengthSignal(task_description);
218
335
  const ms = multiStepSignal(task_description);
336
+ const delegability = assessDelegability(args);
219
337
  // ── Weighted composite score: [-1.0, +1.0] ──
220
338
  // Positive = claw-favoring, Negative = host-favoring
221
339
  const composite = kw * WEIGHTS.keyword +
@@ -228,12 +346,20 @@ export function computeRoute(args) {
228
346
  // composite +1.0 → complexity 1 (trivial)
229
347
  // composite -1.0 → complexity 10 (very complex)
230
348
  const complexityRaw = Math.round(5.5 - composite * 4.5);
231
- const complexity_score = Math.max(1, Math.min(10, complexityRaw));
349
+ const complexity_score = Math.max(delegability.minimumComplexity, Math.max(1, Math.min(10, complexityRaw)));
232
350
  // ── Determine target ──
233
- const isClaw = composite > 0 && complexity_score <= PRISM_TASK_ROUTER_MAX_CLAW_COMPLEXITY;
351
+ const locallyEligible = composite > 0 || delegability.boundedHighComplexity;
352
+ const isClaw = !delegability.hardHostBoundary &&
353
+ locallyEligible &&
354
+ complexity_score <= PRISM_TASK_ROUTER_MAX_CLAW_COMPLEXITY;
234
355
  // ── Confidence: distance from the decision boundary ──
235
356
  // Higher absolute composite → higher confidence
236
- const confidence = Math.min(0.99, Math.round((0.5 + Math.abs(composite) * 0.5) * 100) / 100);
357
+ const rawConfidence = Math.min(0.99, Math.round((0.5 + Math.abs(composite) * 0.5) * 100) / 100);
358
+ const confidence = delegability.hardHostBoundary
359
+ ? Math.max(rawConfidence, HARD_HOST_BOUNDARY_CONFIDENCE)
360
+ : delegability.boundedHighComplexity
361
+ ? Math.max(rawConfidence, BOUNDED_HIGH_COMPLEXITY_CONFIDENCE)
362
+ : rawConfidence;
237
363
  // ── Apply confidence threshold ──
238
364
  // If confidence is too low, default to host (safer)
239
365
  const target = isClaw && confidence >= PRISM_TASK_ROUTER_CONFIDENCE_THRESHOLD ? "claw" : "host";
@@ -251,6 +377,12 @@ export function computeRoute(args) {
251
377
  signals.push(`multi-step detected (${ms.toFixed(1)})`);
252
378
  if (ln !== 0)
253
379
  signals.push(`length signal: ${ln.toFixed(1)}`);
380
+ if (delegability.boundedHighComplexity) {
381
+ signals.push(`bounded high-complexity workload (${delegability.highComplexityHits} signal${delegability.highComplexityHits === 1 ? "" : "s"})`);
382
+ }
383
+ if (delegability.hardHostBoundary) {
384
+ signals.push(`host boundary: ${delegability.reasons.join(", ")}`);
385
+ }
254
386
  const rationale = target === "claw"
255
387
  ? `Task is delegable to the local agent. Signals: ${signals.join("; ") || "neutral"}.`
256
388
  : `Task should remain with the host model. Signals: ${signals.join("; ") || "neutral"}.`;
@@ -259,8 +391,14 @@ export function computeRoute(args) {
259
391
  confidence,
260
392
  complexity_score,
261
393
  rationale,
262
- recommended_tool: target === "claw" ? "claw_run_task" : null,
394
+ recommended_tool: target === "claw" ? "prism_infer" : null,
395
+ ...(target === "claw" ? {
396
+ recommended_args: buildRecommendedArgs(args, complexity_score),
397
+ } : {}),
263
398
  _rawComposite: composite,
399
+ _hardHostBoundary: delegability.hardHostBoundary,
400
+ _boundedHighComplexity: delegability.boundedHighComplexity,
401
+ _minimumComplexity: delegability.minimumComplexity,
264
402
  };
265
403
  }
266
404
  // ─── MCP Handler ─────────────────────────────────────────────
@@ -282,9 +420,9 @@ export async function sessionTaskRouteHandler(args) {
282
420
  isError: true,
283
421
  };
284
422
  }
285
- // Delegation opt-in gate: if delegation_enabled is not "true", always route to host.
286
- // This enforces the prism-infer-delegation skill's "off by default" rule in code.
287
- const delegationEnabled = await getSetting("delegation_enabled", "false");
423
+ // Local-first is the product default. An explicit dashboard/config value of
424
+ // "false" remains an operator-owned opt-out and always routes to the host.
425
+ const delegationEnabled = await getSetting("delegation_enabled", "true");
288
426
  if (delegationEnabled !== "true") {
289
427
  return {
290
428
  content: [{
@@ -293,7 +431,7 @@ export async function sessionTaskRouteHandler(args) {
293
431
  target: "host",
294
432
  confidence: 1.0,
295
433
  complexity_score: 5,
296
- rationale: "Delegation is off (default). Enable with: configure_notifications({setting: 'delegation_enabled', value: 'true'}) or via the Prism dashboard.",
434
+ rationale: "Local delegation was explicitly disabled in Prism settings.",
297
435
  recommended_tool: null,
298
436
  delegation_enabled: false,
299
437
  }),
@@ -313,14 +451,25 @@ export async function sessionTaskRouteHandler(args) {
313
451
  const adjustedComposite = Math.max(-1.0, Math.min(1.0, (result._rawComposite || 0) + exp.bias));
314
452
  // Recalculate target and complexity if bias flipped the composite sign
315
453
  const complexityRaw = Math.round(5.5 - adjustedComposite * 4.5);
316
- const complexity_score = Math.max(1, Math.min(10, complexityRaw));
317
- const isClaw = adjustedComposite > 0 && complexity_score <= PRISM_TASK_ROUTER_MAX_CLAW_COMPLEXITY;
318
- const confidence = Math.min(0.99, Math.round((0.5 + Math.abs(adjustedComposite) * 0.5) * 100) / 100);
454
+ const complexity_score = Math.max(result._minimumComplexity ?? 1, Math.max(1, Math.min(10, complexityRaw)));
455
+ const locallyEligible = adjustedComposite > 0 || result._boundedHighComplexity === true;
456
+ const isClaw = result._hardHostBoundary !== true &&
457
+ locallyEligible &&
458
+ complexity_score <= PRISM_TASK_ROUTER_MAX_CLAW_COMPLEXITY;
459
+ const rawConfidence = Math.min(0.99, Math.round((0.5 + Math.abs(adjustedComposite) * 0.5) * 100) / 100);
460
+ const confidence = result._hardHostBoundary
461
+ ? Math.max(rawConfidence, HARD_HOST_BOUNDARY_CONFIDENCE)
462
+ : result._boundedHighComplexity
463
+ ? Math.max(rawConfidence, BOUNDED_HIGH_COMPLEXITY_CONFIDENCE)
464
+ : rawConfidence;
319
465
  const target = isClaw && confidence >= PRISM_TASK_ROUTER_CONFIDENCE_THRESHOLD ? "claw" : "host";
320
466
  result.target = target;
321
467
  result.confidence = confidence;
322
468
  result.complexity_score = complexity_score;
323
- result.recommended_tool = target === "claw" ? "claw_run_task" : null;
469
+ result.recommended_tool = target === "claw" ? "prism_infer" : null;
470
+ result.recommended_args = target === "claw"
471
+ ? buildRecommendedArgs(args, complexity_score)
472
+ : undefined;
324
473
  result.experience = {
325
474
  bias: exp.bias,
326
475
  sample_count: exp.sampleCount,
@@ -333,8 +482,6 @@ export async function sessionTaskRouteHandler(args) {
333
482
  // Note: intentionally throwing away the error to keep the original raw heuristic result.
334
483
  }
335
484
  }
336
- // Remove the private field from the final output
337
- delete result._rawComposite;
338
485
  // ── v9.x: Local LLM second-opinion for low-confidence cases ──────────────
339
486
  // When confidence is below the threshold AND local LLM is enabled,
340
487
  // ask prism-coder:9b to break the tie. This is purely additive — if the
@@ -345,20 +492,28 @@ export async function sessionTaskRouteHandler(args) {
345
492
  const llmTarget = await askLocalLlmForRoute(args.task_description);
346
493
  if (llmTarget) {
347
494
  const prev = result.target;
348
- result.target = llmTarget;
349
- // Re-derive complexity_score to stay consistent with the new target
350
- // so downstream consumers see a coherent { target, complexity_score } pair.
351
- if (llmTarget === "claw" && result.complexity_score > PRISM_TASK_ROUTER_MAX_CLAW_COMPLEXITY) {
352
- result.complexity_score = PRISM_TASK_ROUTER_MAX_CLAW_COMPLEXITY;
353
- }
495
+ const llmCanDelegate = llmTarget === "claw" &&
496
+ result._hardHostBoundary !== true &&
497
+ result.complexity_score <= PRISM_TASK_ROUTER_MAX_CLAW_COMPLEXITY;
498
+ const target = llmCanDelegate ? "claw" : "host";
499
+ result.target = target;
500
+ result.recommended_tool = target === "claw" ? "prism_infer" : null;
501
+ result.recommended_args = target === "claw"
502
+ ? buildRecommendedArgs(args, result.complexity_score)
503
+ : undefined;
354
504
  result.rationale +=
355
- ` [prism-coder override: heuristic confidence ${result.confidence.toFixed(2)} < threshold → LLM voted "${llmTarget}" (was "${prev}")]`;
505
+ ` [prism-coder review: heuristic confidence ${result.confidence.toFixed(2)} < threshold → LLM voted "${llmTarget}"; resolved "${target}" (was "${prev}")]`;
356
506
  }
357
507
  }
358
508
  catch {
359
509
  // Non-fatal: LLM second-opinion failure never blocks routing
360
510
  }
361
511
  }
512
+ // Remove internal decision evidence from the public tool response.
513
+ delete result._rawComposite;
514
+ delete result._hardHostBoundary;
515
+ delete result._boundedHighComplexity;
516
+ delete result._minimumComplexity;
362
517
  return {
363
518
  content: [
364
519
  {
@@ -382,8 +537,8 @@ async function askLocalLlmForRoute(description) {
382
537
  .replace(/</g, "&lt;").replace(/>/g, "&gt;");
383
538
  const prompt = `You are a task routing classifier for an AI coding assistant.\n` +
384
539
  `Decision logic:\n` +
385
- ` - "claw": simple, isolated, well-defined tasks (rename file, fix typo, add test)\n` +
386
- ` - "host": complex, multi-step, architectural, or ambiguous tasks (audit, redesign, plan)\n\n` +
540
+ ` - "claw": bounded, self-contained, well-defined inference work. It may be difficult if it needs no host tools or reserved judgment.\n` +
541
+ ` - "host": architecture, security, investigation, review, multi-step/tool-required workflows, or ambiguous work.\n\n` +
387
542
  `CRITICAL: You MUST use the following structural tags:\n` +
388
543
  `<|synalux_think|>\n[Internal reasoning about complexity]\n</|synalux_think|>\n\n` +
389
544
  `<|tool_call|>\nclaw\n</|tool_call|>\n\n` +
@@ -68,7 +68,11 @@ export function clampCeiling(requested, planCeiling) {
68
68
  }
69
69
  // ── Fetch ─────────────────────────────────────────────────────────
70
70
  async function fetchEntitlements() {
71
- if (!SYNALUX_CONFIGURED || !PRISM_SYNALUX_BASE_URL) {
71
+ // Re-read process.env because dashboard/bootstrap configuration can inject
72
+ // credentials after config.ts captured its module-load constants.
73
+ const baseUrl = process.env.PRISM_SYNALUX_BASE_URL?.trim() || PRISM_SYNALUX_BASE_URL;
74
+ const apiKey = process.env.PRISM_SYNALUX_API_KEY?.trim();
75
+ if ((!SYNALUX_CONFIGURED && !apiKey) || !baseUrl) {
72
76
  debugLog("[entitlements] no Synalux auth configured — free tier");
73
77
  return { ...FREE_ENTITLEMENTS, source: "unconfigured" };
74
78
  }
@@ -78,7 +82,7 @@ async function fetchEntitlements() {
78
82
  return { ...FREE_ENTITLEMENTS, source: "fallback_free" };
79
83
  }
80
84
  try {
81
- const url = `${PRISM_SYNALUX_BASE_URL}/api/v1/prism/entitlements`;
85
+ const url = `${baseUrl}/api/v1/prism/entitlements`;
82
86
  const res = await fetch(url, {
83
87
  method: "GET",
84
88
  headers: { Authorization: `Bearer ${jwt}` },
@@ -13,6 +13,8 @@
13
13
  */
14
14
  import { debugLog } from "./logger.js";
15
15
  import { appendInferMetric, queryInferMetrics } from "../storage/inferMetricsLedger.js";
16
+ import { ingestPanelMetrics } from "../storage/panelMetricsSpool.js";
17
+ const PANEL_CALLER = "panel";
16
18
  // T1 fix: content-aware token estimator. Replaces flat text.length / 4 which
17
19
  // underestimates emoji (~2 UTF-16 units but 1.5-2.5 BPE tokens) and CJK
18
20
  // (~1 char ≈ 1 token) by 15-40%, and overestimates dense code (~3.3 chars/token).
@@ -169,9 +171,13 @@ export function resetInferenceMetrics() {
169
171
  }
170
172
  export async function inferenceMetricsHandler(args) {
171
173
  if (args?.period === "all") {
174
+ const ingest = await ingestPanelMetrics();
172
175
  const agg = await queryInferMetrics();
173
176
  if (!agg || agg.total === 0) {
174
- return { content: [{ type: "text", text: "No persisted prism_infer calls yet (ledger empty)." }] };
177
+ const warning = ingest.failed_files > 0
178
+ ? ` Panel spool ingestion failed for ${ingest.failed_files} file(s); retained for retry.`
179
+ : "";
180
+ return { content: [{ type: "text", text: `No persisted inference calls yet (ledger empty).${warning}` }] };
175
181
  }
176
182
  const localPct = agg.total ? Math.round((agg.local / agg.total) * 100) : 0;
177
183
  const cloudPct = agg.total ? Math.round((agg.cloud / agg.total) * 100) : 0;
@@ -181,13 +187,26 @@ export async function inferenceMetricsHandler(args) {
181
187
  const byB = Object.entries(agg.by_backend)
182
188
  .sort((a, b) => b[1] - a[1])
183
189
  .map(([k, v]) => ` ${k}: ${v}`).join("\n");
190
+ const panel = agg.by_caller[PANEL_CALLER];
191
+ const panelPct = panel?.total ? Math.round((panel.local / panel.total) * 100) : 0;
192
+ const panelLine = panel
193
+ ? `Panel local serve rate: ${panelPct}% (${panel.local}/${panel.total}; cloud ${panel.cloud})`
194
+ : "Panel local serve rate: no panel calls recorded";
195
+ let ingestNote = "";
196
+ if (ingest.invalid > 0)
197
+ ingestNote = `discarded ${ingest.invalid} invalid panel row(s)`;
198
+ if (ingest.failed_files > 0) {
199
+ ingestNote += `${ingestNote ? "; " : ""}retained ${ingest.failed_files} panel file(s) for retry`;
200
+ }
201
+ const ingestLine = ingestNote ? `\nPanel spool: ${ingestNote}` : "";
184
202
  return {
185
203
  content: [{
186
204
  type: "text",
187
- text: `📊 Delegation Metrics — ALL TIME (persisted ledger, ${span})\n` +
205
+ text: `📊 Inference Metrics — ALL TIME (persisted ledger, ${span})\n` +
188
206
  `Total calls: ${agg.total} — Local: ${agg.local} (${localPct}%) | Cloud: ${agg.cloud} (${cloudPct}%)\n` +
207
+ `${panelLine}\n` +
189
208
  `Prompt tokens: ${agg.prompt_tokens} | Completion tokens: ${agg.completion_tokens}\n` +
190
- `Avg latency: ${agg.avg_latency_ms}ms\nBy backend:\n${byB}`,
209
+ `Avg latency: ${agg.avg_latency_ms}ms\nBy backend:\n${byB}${ingestLine}`,
191
210
  }],
192
211
  };
193
212
  }
@@ -13,10 +13,10 @@
13
13
  * This saves 11GB+ RAM vs 27b and keeps response times fast.
14
14
  *
15
15
  * tag weights need free ctx role
16
- * prism-coder:27b ~16 GB ≥ 20 GB 32K quality (on-demand, Qwen3.5 DeltaNet, 100% BFCL)
17
- * prism-coder:9b ~ 5.8 GB ≥ 8 GB 32K default router (Qwen3.5, 100% BFCL)
16
+ * prism-coder:27b ~16 GB ≥ 20 GB 4K quality (on-demand, Qwen3.5 DeltaNet, 100% BFCL)
17
+ * prism-coder:9b ~ 5.8 GB ≥ 8 GB 4K default router (Qwen3.5, 100% BFCL)
18
18
  * prism-coder:4b ~ 3.4 GB ≥ 5 GB 32K verifier (Qwen3.5, 100%)
19
- * prism-coder:2b ~ 2.3 GB ≥ 3 GB 8K mobile / iPhone (Qwen3.5, 99.1%)
19
+ * prism-coder:2b ~ 2.3 GB ≥ 3 GB 32K mobile / iPhone (Qwen3.5, 99.1%)
20
20
  *
21
21
  * Below 3 GB free → no local pick (caller must use cloud).
22
22
  */
@@ -32,7 +32,12 @@ let inFlight = null;
32
32
  * Concurrent callers share a single in-flight exchange (no thundering herd).
33
33
  */
34
34
  export async function getSynaluxJwt() {
35
- if (!PRISM_SYNALUX_BASE_URL || !PRISM_SYNALUX_API_KEY) {
35
+ // Re-read process.env because storage/dashboard configuration can inject
36
+ // credentials after config.ts captured its module-load constants.
37
+ const baseUrl = process.env.PRISM_SYNALUX_BASE_URL?.trim() ||
38
+ process.env.SYNALUX_BASE_URL?.trim() || PRISM_SYNALUX_BASE_URL;
39
+ const apiKey = process.env.PRISM_SYNALUX_API_KEY?.trim() || PRISM_SYNALUX_API_KEY;
40
+ if (!baseUrl || !apiKey) {
36
41
  return null;
37
42
  }
38
43
  const now = Date.now();
@@ -43,11 +48,11 @@ export async function getSynaluxJwt() {
43
48
  return inFlight;
44
49
  inFlight = (async () => {
45
50
  try {
46
- const url = `${PRISM_SYNALUX_BASE_URL}/api/v1/auth/jwt`;
51
+ const url = `${baseUrl}/api/v1/auth/jwt`;
47
52
  const res = await fetch(url, {
48
53
  method: "POST",
49
54
  headers: {
50
- "Authorization": `Bearer ${PRISM_SYNALUX_API_KEY}`,
55
+ "Authorization": `Bearer ${apiKey}`,
51
56
  "Content-Type": "application/json",
52
57
  },
53
58
  signal: AbortSignal.timeout(10_000),
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "prism-mcp-server",
3
- "version": "20.2.0",
3
+ "version": "20.2.2",
4
4
  "mcpName": "io.github.dcostenco/prism-coder",
5
- "description": "Prism Coder — Cognitive memory + tool-calling intelligence for AI agents. Mind Palace persistent memory (BFCL Gold Certified, 100% Tool-Call Accuracy, 114 Agent Skills, PHI Guard, Tier Enforcement, Prompt-Based Skill Routing, Zero-Search HDC/HRR retrieval, HRR Semantic Drift Detection across BCBA/Coding/AAC domains, HIPAA-hardened local-first storage, SLERP-optimized GRPO alignment) plus the prism-coder 1.7B–32B open-weights LLM fleet.",
5
+ "description": "Prism Coder — Cognitive memory + tool-calling intelligence for AI agents. Mind Palace persistent memory (BFCL Gold Certified, 100% Tool-Call Accuracy, 114 Agent Skills, PHI Guard, Tier Enforcement, Prompt-Based Skill Routing, Zero-Search HDC/HRR retrieval, HRR Semantic Drift Detection across BCBA/Coding/AAC domains, HIPAA-hardened local or subscription-gated Synalux storage, SLERP-optimized GRPO alignment) plus the prism-coder 1.7B–32B open-weights LLM fleet.",
6
6
  "module": "index.ts",
7
7
  "type": "module",
8
8
  "main": "dist/server.js",
@@ -16,6 +16,8 @@
16
16
  "dist"
17
17
  ],
18
18
  "scripts": {
19
+ "clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"",
20
+ "prebuild": "npm run clean",
19
21
  "build": "tsc && npm run chmod-bins",
20
22
  "chmod-bins": "node -e \"['dist/cli.js','dist/server.js','dist/utils/universalImporter.js'].forEach(f => { try { require('fs').chmodSync(f, 0o755); } catch (e) { console.warn('chmod skipped', f, e.message); } })\"",
21
23
  "prepublishOnly": "npm run build",
@@ -25,6 +27,8 @@
25
27
  "test:watch": "vitest",
26
28
  "test:load": "vitest run tests/load/",
27
29
  "test:ci": "vitest run --reporter=junit --outputFile=test-results.xml",
30
+ "test:routing:live": "npm run build && node scripts/prism-infer-live-test.mjs",
31
+ "test:routing:models": "npm run build && node scripts/prism-infer-live-test.mjs --infer",
28
32
  "test:mcp": "node ./test_cross_mcp.js",
29
33
  "import": "node dist/utils/universalImporter.js"
30
34
  },