prism-mcp-server 20.3.2 → 20.4.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/README.md CHANGED
@@ -61,6 +61,35 @@ features.
61
61
  <details>
62
62
  <summary>Release history (optional)</summary>
63
63
 
64
+ ## What's New in v20.4.0
65
+
66
+ ### An Explicitly Named Cloud Backend Fails Loud
67
+
68
+ Setting `PRISM_STORAGE=synalux` or `=supabase` with incomplete credentials used
69
+ to downgrade silently to local SQLite. The switch was logged to stderr, which
70
+ MCP hosts discard, so nothing surfaced it: sessions kept serving stale local
71
+ context while the cloud held newer history, and `context_source` read `local`
72
+ rather than any kind of warning. A session could run that way for weeks.
73
+
74
+ Naming a backend outright is a strong statement of intent, so it now throws —
75
+ naming the missing variables and the `PRISM_STORAGE=local` opt-out — instead of
76
+ quietly splitting your session history. `auto` is unchanged: it keeps its
77
+ documented `synalux > supabase > local` degradation, pinned by a test.
78
+
79
+ **Upgrade note:** if you explicitly set `PRISM_STORAGE=synalux|supabase` and
80
+ your credentials are incomplete, startup now fails with a named error instead
81
+ of silently using local data. That error is the fix — set the missing variable,
82
+ or choose `PRISM_STORAGE=local` deliberately. Default (`auto`) configs are
83
+ unaffected.
84
+
85
+ The throw is deliberately not treated as a recoverable startup fault: that path
86
+ exists for transient errors (rate limits, 5xx, DNS), which may degrade behind a
87
+ visible notice. A missing credential is a configuration fault and must not be
88
+ papered over.
89
+
90
+ Also: the skill block is now budgeted by default rather than only on request,
91
+ so a large skill payload cannot crowd out briefing and history.
92
+
64
93
  ## What's New in v20.3.2
65
94
 
66
95
  ### Web Scholar: SSRF Hardening
@@ -92,13 +92,34 @@ export async function getStorage() {
92
92
  debugLog(`[Prism Storage] Auto-resolved: ${requested}`);
93
93
  }
94
94
  // ─── Validate explicit backend has credentials ────────────────
95
+ // An explicitly requested cloud backend with missing credentials must fail
96
+ // loud. Silently serving local SQLite splits session history: the caller
97
+ // keeps working against a stale local copy while believing it is on the
98
+ // cloud, and console.error goes to stderr, which MCP hosts discard. "auto"
99
+ // already refuses to fall back for this exact reason (see above); naming a
100
+ // backend outright is a stronger statement of intent, so it must not be
101
+ // weaker about protecting history.
102
+ //
103
+ // Observed in the field: a base URL present without its API key (a
104
+ // `prism connect` run from a shell that never exported the key strips it)
105
+ // downgraded every subsequent session to local storage for weeks. The local
106
+ // copy kept serving months-old context while the cloud held current history,
107
+ // and nothing in-band surfaced the downgrade.
108
+ //
109
+ // This throw is deliberately NOT matched by isRecoverableStartupStorageError
110
+ // (startupRecovery.ts): a missing credential is a configuration fault, not a
111
+ // transient one, so startup must not paper over it with last-good context.
95
112
  if (requested === "synalux" && !(await ensureSynaluxCredentials())) {
96
- console.error("[Prism Storage] Synalux backend requested but PRISM_SYNALUX_BASE_URL/PRISM_SYNALUX_API_KEY are missing or invalid. Falling back to local storage.");
97
- requested = "local";
113
+ throw new Error("[Prism Storage] PRISM_STORAGE=synalux but Synalux credentials are missing or invalid " +
114
+ "(need PRISM_SYNALUX_BASE_URL and PRISM_SYNALUX_API_KEY). " +
115
+ "Refusing to fall back to local storage because that silently splits session history. " +
116
+ "Set PRISM_STORAGE=local explicitly if local-only storage is intended.");
98
117
  }
99
118
  if (requested === "supabase" && !(await ensureSupabaseCredentials())) {
100
- console.error("[Prism Storage] Supabase backend requested but SUPABASE_URL/SUPABASE_KEY are missing or invalid. Falling back to local storage.");
101
- requested = "local";
119
+ throw new Error("[Prism Storage] PRISM_STORAGE=supabase but Supabase credentials are missing or invalid " +
120
+ "(need SUPABASE_URL and SUPABASE_KEY). " +
121
+ "Refusing to fall back to local storage because that silently splits session history. " +
122
+ "Set PRISM_STORAGE=local explicitly if local-only storage is intended.");
102
123
  }
103
124
  // ─── Initialize ───────────────────────────────────────────────
104
125
  activeStorageBackend = requested;
@@ -1344,8 +1344,12 @@ export async function sessionLoadContextHandler(args, options = {}) {
1344
1344
  // exists to deliver. The protected floor may still exceed this tranche
1345
1345
  // (always inlined); the reserved 40% keeps history alive whenever the
1346
1346
  // caller's budget covers the floor at all.
1347
- const skillBudgetChars = maxTokens && maxTokens > 0 ? Math.floor(maxTokens * 3.5 * 0.6) : Number.POSITIVE_INFINITY;
1348
- const { assembleSkillBlock } = await import("../utils/skillBudget.js");
1347
+ // Armed by DEFAULT, not only when the caller passes max_tokens see
1348
+ // resolveSkillBudgetChars for why an unbudgeted default cost the agent its
1349
+ // entire response on 2026-08-01. `level` scales the tranche so `quick`
1350
+ // actually means quick.
1351
+ const { assembleSkillBlock, resolveSkillBudgetChars } = await import("../utils/skillBudget.js");
1352
+ const skillBudgetChars = resolveSkillBudgetChars(maxTokens, level);
1349
1353
  const budgeted = assembleSkillBlock(skillEntries, skillBudgetChars);
1350
1354
  skillBlock = budgeted.block;
1351
1355
  loadedSkills.push(...budgeted.inlined);
@@ -27,6 +27,51 @@ function render(e) {
27
27
  const label = e.category === "role" ? "ROLE SKILL" : "SKILL";
28
28
  return `\n\n[📜 ${label}: ${e.name}]\n${e.content.trim()}`;
29
29
  }
30
+ /**
31
+ * Skill tranche used when the caller sets no `max_tokens`.
32
+ *
33
+ * Sized against the ~25k-token host tool-result cap: at the 3.5 chars/token
34
+ * heuristic that is ~87k chars for the WHOLE response, so the skill block has
35
+ * to leave room for briefing, handoff, and history.
36
+ *
37
+ * These are ADDITIVE on top of the protected floor, not a total. Protected
38
+ * skills inline even when the budget is already blown (assembleSkillBlock), and
39
+ * the repo-measured v26 floor is ~39k chars on its own — so the ceiling here is
40
+ * roughly 87k - 39k - memory. `standard` matches the 8,400-char tranche the
41
+ * existing v26 shape test already treats as the standard budget (60% of 14k
42
+ * tokens); `deep` doubles it and still leaves headroom for deep history.
43
+ *
44
+ * `quick` is deliberately near-nothing — it is the setting a caller picks to
45
+ * minimize context, and before this it still inlined the full skill payload,
46
+ * because `level` gated only the memory portion, which is the small part.
47
+ *
48
+ * Every value is finite and > 0 on purpose: assembleSkillBlock treats ≤ 0 and
49
+ * non-finite as "unbudgeted", so a zero here would silently restore the very
50
+ * bug this table exists to fix.
51
+ */
52
+ export const DEFAULT_SKILL_BUDGET_CHARS = {
53
+ quick: 2_000,
54
+ standard: 8_400,
55
+ deep: 16_000,
56
+ };
57
+ /**
58
+ * Resolve the skill-block budget for one call.
59
+ *
60
+ * 2026-08-01: this previously evaluated to POSITIVE_INFINITY whenever
61
+ * `max_tokens` was absent — which is the documented default and therefore the
62
+ * common call shape. Routing v25 (76 -> 95 skills, 19 moved to auto-load) then
63
+ * pushed the unbudgeted block to 91,578 chars, past the host cap, and the host
64
+ * diverted the ENTIRE response to a file: the agent received no context at all.
65
+ * The budget must be armed by default, not only when a caller opts in.
66
+ */
67
+ export function resolveSkillBudgetChars(maxTokens, level) {
68
+ // 60% of the response allowance: skills must not saturate it, or the
69
+ // briefing and history this tool exists to deliver get truncated away.
70
+ if (typeof maxTokens === "number" && Number.isFinite(maxTokens) && maxTokens > 0) {
71
+ return Math.max(1, Math.floor(maxTokens * 3.5 * 0.6));
72
+ }
73
+ return DEFAULT_SKILL_BUDGET_CHARS[level] ?? DEFAULT_SKILL_BUDGET_CHARS.standard;
74
+ }
30
75
  /**
31
76
  * Assemble the skill block within `budgetChars`. `budgetChars` ≤ 0 or
32
77
  * non-finite means unbudgeted (legacy behavior: inline everything).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prism-mcp-server",
3
- "version": "20.3.2",
3
+ "version": "20.4.0",
4
4
  "mcpName": "io.github.dcostenco/prism-coder",
5
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",