prism-mcp-server 20.5.1 → 20.5.3
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.
|
@@ -268,6 +268,16 @@ export async function knowledgeSearchHandler(args) {
|
|
|
268
268
|
if (data.results && Array.isArray(data.results) && data.results.length > 0) {
|
|
269
269
|
const evidenceSnippets = data.results.map((r, i) => ({
|
|
270
270
|
source: `knowledge_search:${r.id ?? i}`,
|
|
271
|
+
// Carry the record date. Without it a two-year-old note and yesterday's
|
|
272
|
+
// are indistinguishable in the grounding evidence, so a stale memory is
|
|
273
|
+
// cited with the same confidence as a fresh one — local storage removes
|
|
274
|
+
// the external correction pressure that would otherwise catch it.
|
|
275
|
+
// session_date first: it is when the work HAPPENED, which is what a
|
|
276
|
+
// staleness signal needs. created_at is when the row was written and is
|
|
277
|
+
// assigned on save — it cannot be backdated, so an import would stamp
|
|
278
|
+
// every migrated memory as brand new, failing silently toward "fresh".
|
|
279
|
+
// sqlite.ts already treats session_date as authoritative.
|
|
280
|
+
recorded: r.session_date ?? r.created_at ?? r.updated_at ?? r.timestamp ?? undefined,
|
|
271
281
|
content: (r.content ?? r.summary ?? r.text ?? "").slice(0, 1000),
|
|
272
282
|
})).filter((s) => s.content);
|
|
273
283
|
if (evidenceSnippets.length > 0) {
|
|
@@ -624,6 +634,7 @@ export async function sessionSearchMemoryHandler(args) {
|
|
|
624
634
|
{
|
|
625
635
|
const evidenceSnippets = results.map((r, i) => ({
|
|
626
636
|
source: `session_search_memory:${r.id ?? i}`,
|
|
637
|
+
recorded: r.session_date ?? r.created_at ?? r.updated_at ?? r.timestamp ?? undefined,
|
|
627
638
|
content: (r.summary ?? "").slice(0, 1000),
|
|
628
639
|
})).filter((s) => s.content);
|
|
629
640
|
if (evidenceSnippets.length > 0) {
|
|
@@ -96,10 +96,50 @@ const MAX_SYMPTOM_SKILLS = 5;
|
|
|
96
96
|
*/
|
|
97
97
|
const SYMPTOM_SKILL_INLINE_MAX = 1_800;
|
|
98
98
|
const SYMPTOM_SKILL_BUDGET_SHARE = 0.4;
|
|
99
|
+
/** Below this the inlined rule is too clipped to be worth the space it costs. */
|
|
100
|
+
const SYMPTOM_SKILL_INLINE_MIN = 400;
|
|
101
|
+
/**
|
|
102
|
+
* The character budget a native startup display will actually be capped to.
|
|
103
|
+
*
|
|
104
|
+
* Single source of truth: capNativeStartupText caps against this, and the
|
|
105
|
+
* inlined rule is sized from it. Sizing the rule from the LEVEL constant
|
|
106
|
+
* instead let the suffix exceed the whole allowance — bootstrap divides the
|
|
107
|
+
* budget across rendered projects, so a per-project slice of 512 against an
|
|
108
|
+
* 1,800-char exempt suffix produced a 1,919-char display (275% over) with the
|
|
109
|
+
* session context entirely gone. The suffix is truncation-exempt by design;
|
|
110
|
+
* that only works if it is sized against the real budget.
|
|
111
|
+
*/
|
|
112
|
+
function effectiveNativeBudget(level, requestedMaxChars) {
|
|
113
|
+
const configuredLimit = NATIVE_STARTUP_MAX_CHARS[level];
|
|
114
|
+
return Math.max(512, Math.min(configuredLimit, requestedMaxChars ?? configuredLimit));
|
|
115
|
+
}
|
|
99
116
|
// Skill bodies are read via skillManifestSync.readNativeSkillBody, which owns
|
|
100
117
|
// the canonical root. That path is overridable per caller and per home, so a
|
|
101
118
|
// literal copied to this module would be wrong on any machine that overrides
|
|
102
119
|
// either — and would silently drift from the writer.
|
|
120
|
+
/**
|
|
121
|
+
* Drop the YAML frontmatter before inlining.
|
|
122
|
+
*
|
|
123
|
+
* `name`/`description`/`metadata` are routing and authoring metadata — the
|
|
124
|
+
* agent already has the name from the line above, and the rest is provenance.
|
|
125
|
+
* Measured at ~161 chars on data-before-code, which is what pushed the
|
|
126
|
+
* Anti-Patterns list past the cap and truncated it mid-word. Every character
|
|
127
|
+
* here is taken from the rule it is supposed to deliver.
|
|
128
|
+
*
|
|
129
|
+
* Returns "" for absent input so the caller's single truthiness check covers
|
|
130
|
+
* both "no body" and "frontmatter only".
|
|
131
|
+
*/
|
|
132
|
+
function stripSkillFrontmatter(raw) {
|
|
133
|
+
const text = (raw ?? "").trim();
|
|
134
|
+
if (!text.startsWith("---"))
|
|
135
|
+
return text;
|
|
136
|
+
// Closing fence must be its own line; a body line of "---" mid-document is
|
|
137
|
+
// not a terminator, so anchor on the newline pair.
|
|
138
|
+
const end = text.indexOf("\n---", 3);
|
|
139
|
+
if (end === -1)
|
|
140
|
+
return text; // unterminated frontmatter — inline as-is
|
|
141
|
+
return text.slice(text.indexOf("\n", end + 1) + 1).trim();
|
|
142
|
+
}
|
|
103
143
|
const NATIVE_STARTUP_MAX_CHARS = {
|
|
104
144
|
quick: 4_000,
|
|
105
145
|
standard: 8_000,
|
|
@@ -266,8 +306,7 @@ async function buildNativeSystemReadyBlock(snapshot, depth) {
|
|
|
266
306
|
`> - 🔄 **Skill sync:** ${SKILL_SYNC_STATUS_LABELS[snapshot.syncStatus]} · committed manifest${conflictSuffix}`;
|
|
267
307
|
}
|
|
268
308
|
function capNativeStartupText(text, level, requestedMaxChars, suffix = "") {
|
|
269
|
-
const
|
|
270
|
-
const maxChars = Math.max(512, Math.min(configuredLimit, requestedMaxChars ?? configuredLimit));
|
|
309
|
+
const maxChars = effectiveNativeBudget(level, requestedMaxChars);
|
|
271
310
|
if (text.length + suffix.length <= maxChars)
|
|
272
311
|
return text + suffix;
|
|
273
312
|
const marker = `\n\n… Additional ${level} context omitted to keep native startup within its display budget.`;
|
|
@@ -1290,9 +1329,13 @@ export async function sessionLoadContextHandler(args, options = {}) {
|
|
|
1290
1329
|
// the content — no MCP tool serves it. Three instruction rewrites
|
|
1291
1330
|
// failed on that gap. Inlining removes the indirection entirely.
|
|
1292
1331
|
const { readNativeSkillBody } = await import("../skillManifestSync.js");
|
|
1293
|
-
const body = (await readNativeSkillBody(shown[0]))
|
|
1294
|
-
|
|
1295
|
-
|
|
1332
|
+
const body = stripSkillFrontmatter(await readNativeSkillBody(shown[0]));
|
|
1333
|
+
// Size against the budget this display will ACTUALLY be capped to,
|
|
1334
|
+
// not the level constant — bootstrap divides it across projects.
|
|
1335
|
+
const cap = Math.min(SYMPTOM_SKILL_INLINE_MAX, Math.floor(effectiveNativeBudget(level, options.nativeMaxChars) * SYMPTOM_SKILL_BUDGET_SHARE));
|
|
1336
|
+
// Too tight to carry a useful rule: keep the name line, which is
|
|
1337
|
+
// small, and leave the remaining budget to the session context.
|
|
1338
|
+
if (body && cap >= SYMPTOM_SKILL_INLINE_MIN) {
|
|
1296
1339
|
const clipped = body.length > cap
|
|
1297
1340
|
? `${body.slice(0, cap).trimEnd()}\n… (rule truncated to fit the startup budget)`
|
|
1298
1341
|
: body;
|
|
@@ -55,7 +55,7 @@ const GROUNDED_SYNTHESIS_SYSTEM = "Answer the user's question using only the sup
|
|
|
55
55
|
"For clinical or behavioral topics, provide educational candidates for credentialed review, " +
|
|
56
56
|
"not individualized treatment instructions or professional sign-off. " +
|
|
57
57
|
"If the evidence is insufficient, say exactly what is missing instead of guessing.";
|
|
58
|
-
function extractMemorySources(result) {
|
|
58
|
+
export function extractMemorySources(result) {
|
|
59
59
|
if (result.isError) {
|
|
60
60
|
throw new Error(result.content[0]?.text || "knowledge_search failed");
|
|
61
61
|
}
|
|
@@ -82,6 +82,7 @@ function extractMemorySources(result) {
|
|
|
82
82
|
sources.push({
|
|
83
83
|
type: "memory",
|
|
84
84
|
source: snippet.source,
|
|
85
|
+
recorded: typeof snippet.recorded === "string" ? snippet.recorded : undefined,
|
|
85
86
|
content: snippet.content.slice(0, MAX_EVIDENCE_CHARS),
|
|
86
87
|
});
|
|
87
88
|
}
|
|
@@ -224,13 +225,49 @@ const DEFAULT_DEPS = {
|
|
|
224
225
|
function toEvidence(sources) {
|
|
225
226
|
return sources.map(({ source, content }) => ({ source, content }));
|
|
226
227
|
}
|
|
227
|
-
|
|
228
|
+
/**
|
|
229
|
+
* SQLite `CURRENT_TIMESTAMP` writes "YYYY-MM-DD HH:MM:SS" in UTC with no zone,
|
|
230
|
+
* and `Date.parse` reads a zone-less stamp as LOCAL time. West of UTC that puts
|
|
231
|
+
* a ten-minute-old record hours in the FUTURE, which the guard above then hid
|
|
232
|
+
* entirely — the age disappeared exactly when the memory was freshest. Formats
|
|
233
|
+
* are not uniform across tables (semantic_knowledge writes ISO+Z, memory_links
|
|
234
|
+
* does not), so normalise rather than assume.
|
|
235
|
+
*/
|
|
236
|
+
function parseRecordedAt(raw) {
|
|
237
|
+
const zoneless = /^\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2}(?:\.\d+)?$/;
|
|
238
|
+
return Date.parse(zoneless.test(raw) ? `${raw.replace(" ", "T")}Z` : raw);
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* " (recorded 2026-08-02, 431 days ago)" — or "" when the store gave no date.
|
|
242
|
+
* Never guesses: an absent date stays absent rather than defaulting to now,
|
|
243
|
+
* which would make the oldest memories look freshest.
|
|
244
|
+
*/
|
|
245
|
+
export function describeAge(source) {
|
|
246
|
+
const recorded = source.type === "memory" ? source.recorded : undefined;
|
|
247
|
+
if (!recorded)
|
|
248
|
+
return "";
|
|
249
|
+
const at = parseRecordedAt(recorded);
|
|
250
|
+
if (Number.isNaN(at))
|
|
251
|
+
return "";
|
|
252
|
+
const elapsed = Date.now() - at;
|
|
253
|
+
// Tolerate clock skew between machines: slightly-future stamps are "today",
|
|
254
|
+
// not silence. Only a stamp more than a day ahead is treated as bad data.
|
|
255
|
+
if (elapsed < -86_400_000)
|
|
256
|
+
return "";
|
|
257
|
+
const days = Math.max(0, Math.floor(elapsed / 86_400_000));
|
|
258
|
+
const day = recorded.slice(0, 10);
|
|
259
|
+
return days === 0 ? ` (recorded ${day}, today)` : ` (recorded ${day}, ${days} days ago)`;
|
|
260
|
+
}
|
|
261
|
+
export function buildGroundedEvidenceContext(sources) {
|
|
228
262
|
let remaining = MAX_SYNTHESIS_EVIDENCE_CHARS;
|
|
229
263
|
const evidenceBlocks = [];
|
|
230
264
|
for (const [index, source] of sources.entries()) {
|
|
231
265
|
if (remaining <= 0)
|
|
232
266
|
break;
|
|
233
|
-
|
|
267
|
+
// Age belongs in the label, not just the payload. An undated fragment
|
|
268
|
+
// cannot be reasoned about; a fragment labelled two years old can be
|
|
269
|
+
// weighed against fresher evidence or challenged outright.
|
|
270
|
+
const label = `[SOURCE ${index + 1}: ${source.source}${describeAge(source)}]`;
|
|
234
271
|
const availableForContent = Math.max(0, remaining - label.length - 1);
|
|
235
272
|
if (availableForContent === 0)
|
|
236
273
|
break;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "prism-mcp-server",
|
|
3
|
-
"version": "20.5.
|
|
3
|
+
"version": "20.5.3",
|
|
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",
|