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 +29 -0
- package/dist/storage/index.js +25 -4
- package/dist/tools/ledgerHandlers.js +6 -2
- package/dist/utils/skillBudget.js +45 -0
- package/package.json +1 -1
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
|
package/dist/storage/index.js
CHANGED
|
@@ -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
|
-
|
|
97
|
-
|
|
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
|
-
|
|
101
|
-
|
|
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
|
-
|
|
1348
|
-
|
|
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
|
+
"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",
|