prism-mcp-server 15.5.1 → 15.6.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 +16 -4
- package/dist/server.js +70 -0
- package/dist/storage/portalContracts.js +42 -0
- package/dist/storage/sqlite.js +16 -0
- package/dist/storage/synalux.js +17 -2
- package/dist/tools/graphHandlers.js +29 -3
- package/dist/tools/index.js +1 -1
- package/dist/tools/prismInferHandler.js +84 -6
- package/dist/utils/groundingVerifier.js +203 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,11 +4,11 @@
|
|
|
4
4
|
|
|
5
5
|
**Persistent memory + tool-calling intelligence for AI agents.** *(formerly Prism MCP)*
|
|
6
6
|
|
|
7
|
-
A Model Context Protocol server that gives Claude, Cursor, and other AI tools a Mind Palace — long-term memory that survives across sessions, with semantic search, cognitive routing, a visual dashboard, and the `prism-coder:1b7` / `prism-coder:8b` / `prism-coder:14b` / `prism-coder:32b` LLM fleet for offline tool-calling.
|
|
7
|
+
A Model Context Protocol server that gives Claude, Cursor, and other AI tools a Mind Palace — long-term memory that survives across sessions, with semantic search, cognitive routing, a visual dashboard, and the `prism-coder:1b7` / `prism-coder:8b` / `prism-coder:14b` / `prism-coder:32b` LLM fleet for offline tool-calling.
|
|
8
8
|
|
|
9
9
|
[](https://www.npmjs.com/package/prism-mcp-server)
|
|
10
10
|
[](https://marketplace.visualstudio.com/items?itemName=synalux-ai.synalux)
|
|
11
|
-
[](https://synalux.ai/prism-mcp)
|
|
12
12
|
[](https://github.com/modelcontextprotocol/servers)
|
|
13
13
|
[](https://smithery.ai/server/@dcostenco/prism-mcp)
|
|
14
14
|
[](LICENSE)
|
|
@@ -315,9 +315,21 @@ All on-device models are **free for every tier** — no subscription needed for
|
|
|
315
315
|
|
|
316
316
|
## Companions
|
|
317
317
|
|
|
318
|
-
### 🌐 Website
|
|
318
|
+
### 🌐 Website & Docs
|
|
319
319
|
|
|
320
|
-
**[prism-mcp
|
|
320
|
+
**[synalux.ai/prism-mcp](https://synalux.ai/prism-mcp)** — full documentation, dashboard, subscription plans, and model downloads.
|
|
321
|
+
|
|
322
|
+
### 💻 Web IDE — Synalux Coder
|
|
323
|
+
|
|
324
|
+
Use Prism Coder directly in your browser — no install required. Local-first IDE with the prism-coder agent built in. Connects to GitHub repos, Synalux Mail, Drive, and Source for cross-product workflows.
|
|
325
|
+
|
|
326
|
+
**[synalux.ai/coder](https://synalux.ai/coder)** · also reachable at **[synalux.ai/prism-ide](https://synalux.ai/prism-ide)**
|
|
327
|
+
|
|
328
|
+
| Feature | Detail |
|
|
329
|
+
|---|---|
|
|
330
|
+
| Agent | prism-coder:7b offline · Claude Sonnet 4 (Standard+) · Claude Opus 4 (Enterprise) |
|
|
331
|
+
| Integrations | GitHub repos, Synalux Mail, Drive, Source — same OAuth, no separate accounts |
|
|
332
|
+
| Compliance | Audit log on every turn · PHI redaction · air-gapped offline mode (HIPAA) |
|
|
321
333
|
|
|
322
334
|
### 🧩 VS Code Extension — Synalux
|
|
323
335
|
|
package/dist/server.js
CHANGED
|
@@ -89,6 +89,8 @@ MEMORY_HISTORY_TOOL, MEMORY_CHECKOUT_TOOL,
|
|
|
89
89
|
SESSION_SAVE_IMAGE_TOOL, SESSION_VIEW_IMAGE_TOOL,
|
|
90
90
|
// ─── v2.2.0: Health Check tool definition ───
|
|
91
91
|
SESSION_HEALTH_CHECK_TOOL,
|
|
92
|
+
// ─── Hygiene: embedding backfill (orphaned but handler-wired) ───
|
|
93
|
+
SESSION_BACKFILL_EMBEDDINGS_TOOL,
|
|
92
94
|
// ─── Phase 2: GDPR Memory Deletion tool definition ───
|
|
93
95
|
SESSION_FORGET_MEMORY_TOOL,
|
|
94
96
|
// ─── Phase 2: GDPR Export tool definition ───
|
|
@@ -199,6 +201,7 @@ function buildSessionMemoryTools(autoloadList) {
|
|
|
199
201
|
SESSION_VIEW_IMAGE_TOOL, // session_view_image — retrieve image from vault (v2.0)
|
|
200
202
|
// ─── v2.2.0: Health Check tool ───
|
|
201
203
|
SESSION_HEALTH_CHECK_TOOL, // session_health_check — brain integrity checker (v2.2.0)
|
|
204
|
+
SESSION_BACKFILL_EMBEDDINGS_TOOL, // session_backfill_embeddings — repair NULL embeddings (handler+route already wired)
|
|
202
205
|
// ─── Phase 2: GDPR Memory Deletion tool ───
|
|
203
206
|
SESSION_FORGET_MEMORY_TOOL, // session_forget_memory — GDPR-compliant memory deletion (Phase 2)
|
|
204
207
|
// ─── v3.1: TTL Retention tool ───
|
|
@@ -1062,6 +1065,73 @@ export function createSandboxServer() {
|
|
|
1062
1065
|
* responses to stdout. Log messages go to stderr.
|
|
1063
1066
|
*/
|
|
1064
1067
|
export async function startServer() {
|
|
1068
|
+
// Stale-dist guard. Catches the failure mode where src/ commits land but
|
|
1069
|
+
// `npm run build` is skipped, so Claude Desktop runs an outdated
|
|
1070
|
+
// dist/server.js (silent — tool fixes invisible in the running binary).
|
|
1071
|
+
// Read-only probe; safe to run before acquireLock(). No-ops in npm
|
|
1072
|
+
// installs where src/ isn't shipped alongside dist/.
|
|
1073
|
+
try {
|
|
1074
|
+
const { statSync, readdirSync, existsSync, readFileSync } = await import("fs");
|
|
1075
|
+
const { dirname, join, basename } = await import("path");
|
|
1076
|
+
const { fileURLToPath } = await import("url");
|
|
1077
|
+
// Derive layout from package.json so we don't hardcode "src" / "server.js"
|
|
1078
|
+
// / "node_modules". Falls back to sane defaults only if package.json is
|
|
1079
|
+
// missing (e.g. in unusual install layouts).
|
|
1080
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
1081
|
+
const repoRoot = join(here, "..");
|
|
1082
|
+
let distEntry = "server.js";
|
|
1083
|
+
let srcSubdir = "src";
|
|
1084
|
+
const skipDirPrefixes = ["."]; // dotfile dirs (.git, .cache, …)
|
|
1085
|
+
const skipDirNames = new Set(["node_modules"]); // npm convention
|
|
1086
|
+
try {
|
|
1087
|
+
const pkg = JSON.parse(readFileSync(join(repoRoot, "package.json"), "utf8"));
|
|
1088
|
+
if (typeof pkg.main === "string" && pkg.main.length > 0) {
|
|
1089
|
+
distEntry = basename(pkg.main);
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
catch { /* keep fallback */ }
|
|
1093
|
+
try {
|
|
1094
|
+
const tsconfig = JSON.parse(readFileSync(join(repoRoot, "tsconfig.json"), "utf8"));
|
|
1095
|
+
const rootDir = tsconfig?.compilerOptions?.rootDir;
|
|
1096
|
+
if (typeof rootDir === "string" && rootDir.length > 0) {
|
|
1097
|
+
srcSubdir = rootDir.replace(/^\.\//, "");
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
catch { /* keep fallback */ }
|
|
1101
|
+
const distPath = join(here, distEntry);
|
|
1102
|
+
const srcDir = join(repoRoot, srcSubdir);
|
|
1103
|
+
if (existsSync(distPath) && existsSync(srcDir)) {
|
|
1104
|
+
const distMtime = statSync(distPath).mtimeMs;
|
|
1105
|
+
const walk = (d) => {
|
|
1106
|
+
let newest = 0;
|
|
1107
|
+
for (const e of readdirSync(d, { withFileTypes: true })) {
|
|
1108
|
+
if (e.isDirectory()) {
|
|
1109
|
+
if (skipDirNames.has(e.name))
|
|
1110
|
+
continue;
|
|
1111
|
+
if (skipDirPrefixes.some(p => e.name.startsWith(p)))
|
|
1112
|
+
continue;
|
|
1113
|
+
newest = Math.max(newest, walk(join(d, e.name)));
|
|
1114
|
+
}
|
|
1115
|
+
else if (e.isFile()) {
|
|
1116
|
+
newest = Math.max(newest, statSync(join(d, e.name)).mtimeMs);
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
return newest;
|
|
1120
|
+
};
|
|
1121
|
+
const srcMtime = walk(srcDir);
|
|
1122
|
+
if (srcMtime > distMtime) {
|
|
1123
|
+
const msPerDay = 24 * 60 * 60 * 1000;
|
|
1124
|
+
const lagDays = Math.round((srcMtime - distMtime) / msPerDay);
|
|
1125
|
+
const bar = "═".repeat(72);
|
|
1126
|
+
console.error(`\n${bar}\n[Prism] ⚠️ STALE DIST — ${srcSubdir}/ is ${lagDays}d newer than ${distEntry}\n` +
|
|
1127
|
+
`[Prism] Running binary may be missing fixes/tools.\n` +
|
|
1128
|
+
`[Prism] Fix: cd ${repoRoot} && npm run build, then restart Claude Desktop.\n${bar}\n`);
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
catch {
|
|
1133
|
+
// Never block server boot on the freshness probe.
|
|
1134
|
+
}
|
|
1065
1135
|
// MUST BE FIRST: Kill any zombie processes and acquire the singleton PID lock
|
|
1066
1136
|
// before touching SQLite. This prevents lock contention on prism-config.db.
|
|
1067
1137
|
acquireLock();
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Portal wire contracts — Zod schemas for every action payload that
|
|
3
|
+
* prism-mcp-server sends to or receives from the Synalux portal.
|
|
4
|
+
*
|
|
5
|
+
* WHY THIS FILE EXISTS:
|
|
6
|
+
* The 2026-05-24 incident: `knowledge_search` sent `queryText` but
|
|
7
|
+
* the portal expected `query`. Both sides had passing unit tests
|
|
8
|
+
* because each test was written to match its own implementation,
|
|
9
|
+
* not the shared wire contract. This file is the single source of
|
|
10
|
+
* truth for that contract. A field rename here is a compile error
|
|
11
|
+
* in synalux.ts AND a schema-validation failure in route.ts —
|
|
12
|
+
* forcing both sides to update together.
|
|
13
|
+
*
|
|
14
|
+
* ADDING A NEW ACTION:
|
|
15
|
+
* 1. Add RequestSchema + ResponseSchema below.
|
|
16
|
+
* 2. Import and validate in synalux.ts (outgoing) + route.ts (incoming).
|
|
17
|
+
* 3. Add a schema-contract test in synalux-portal-contract.test.ts.
|
|
18
|
+
*/
|
|
19
|
+
import { z } from "zod";
|
|
20
|
+
// ─── knowledge_search ────────────────────────────────────────────
|
|
21
|
+
export const KnowledgeSearchRequestSchema = z.object({
|
|
22
|
+
action: z.literal("knowledge_search"),
|
|
23
|
+
project: z.string().optional(),
|
|
24
|
+
keywords: z.array(z.string()).default([]),
|
|
25
|
+
category: z.string().optional(),
|
|
26
|
+
/** Free-text filter applied via Postgres textSearch on summary.
|
|
27
|
+
* WIRE NAME: `query` — NOT `queryText` (incident 2026-05-24). */
|
|
28
|
+
query: z.string().optional(),
|
|
29
|
+
limit: z.number().int().min(1).max(50).default(10),
|
|
30
|
+
role: z.string().optional(),
|
|
31
|
+
/** 'user' returns only the caller's entries; 'workspace' broadens to all
|
|
32
|
+
* workspace_members rows after server-side membership verification.
|
|
33
|
+
* Optional with no default — the portal applies its own default (currently
|
|
34
|
+
* 'user') so this schema doesn't impose a policy on the wire format. */
|
|
35
|
+
scope: z.enum(["user", "workspace"]).optional(),
|
|
36
|
+
});
|
|
37
|
+
export const KnowledgeSearchResponseSchema = z.object({
|
|
38
|
+
status: z.literal("success"),
|
|
39
|
+
action: z.literal("knowledge_search"),
|
|
40
|
+
count: z.number(),
|
|
41
|
+
results: z.array(z.record(z.string(), z.unknown())),
|
|
42
|
+
});
|
package/dist/storage/sqlite.js
CHANGED
|
@@ -732,6 +732,16 @@ export class SqliteStorage {
|
|
|
732
732
|
// (e.g., { project: "eq.my-app", archived_at: "is.null" }).
|
|
733
733
|
// This parser converts those into SQL WHERE clauses + args so
|
|
734
734
|
// handlers work identically with both Supabase and SQLite.
|
|
735
|
+
// Bug 8.1: column names in WHERE clauses are interpolated directly into SQL.
|
|
736
|
+
// Values are parameterized (safe), but an unvalidated key like
|
|
737
|
+
// "1=1 OR summary" would inject arbitrary SQL into the condition.
|
|
738
|
+
// This allowlist is the same defense-in-depth pattern used in patchLedger.
|
|
739
|
+
static ALLOWED_FILTER_COLUMNS = new Set([
|
|
740
|
+
"id", "project", "user_id", "conversation_id", "summary",
|
|
741
|
+
"archived_at", "deleted_at", "is_rollup", "role", "event_type",
|
|
742
|
+
"created_at", "updated_at", "session_date", "importance", "title",
|
|
743
|
+
"agent_name", "last_accessed_at", "confidence_score", "rollup_count",
|
|
744
|
+
]);
|
|
735
745
|
parsePostgRESTFilters(params) {
|
|
736
746
|
const conditions = [];
|
|
737
747
|
const args = [];
|
|
@@ -779,6 +789,12 @@ export class SqliteStorage {
|
|
|
779
789
|
limit = parseInt(value, 10);
|
|
780
790
|
continue;
|
|
781
791
|
}
|
|
792
|
+
// Bug 8.1 guard: reject any key that isn't in the known column allowlist.
|
|
793
|
+
// Prevents SQL injection via column-name interpolation.
|
|
794
|
+
if (!SqliteStorage.ALLOWED_FILTER_COLUMNS.has(key)) {
|
|
795
|
+
throw new Error(`[SqliteStorage] parsePostgRESTFilters: rejected unknown filter column "${key}". ` +
|
|
796
|
+
`Allowed: ${[...SqliteStorage.ALLOWED_FILTER_COLUMNS].join(", ")}`);
|
|
797
|
+
}
|
|
782
798
|
// PostgREST filter operators
|
|
783
799
|
if (value.startsWith("eq.")) {
|
|
784
800
|
// Handle boolean mapping: SQLite uses 0/1 for booleans
|
package/dist/storage/synalux.js
CHANGED
|
@@ -34,6 +34,17 @@
|
|
|
34
34
|
import { SupabaseStorage } from "./supabase.js";
|
|
35
35
|
import { debugLog } from "../utils/logger.js";
|
|
36
36
|
import { PRISM_SYNALUX_BASE_URL, PRISM_SYNALUX_API_KEY } from "../config.js";
|
|
37
|
+
import { KnowledgeSearchRequestSchema } from "./portalContracts.js";
|
|
38
|
+
function resolveKnowledgeScope(callerScope) {
|
|
39
|
+
if (callerScope === "user" || callerScope === "workspace") {
|
|
40
|
+
return callerScope;
|
|
41
|
+
}
|
|
42
|
+
const envScope = process.env.PRISM_KNOWLEDGE_SCOPE;
|
|
43
|
+
if (envScope === "user" || envScope === "workspace") {
|
|
44
|
+
return envScope;
|
|
45
|
+
}
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
37
48
|
/** Refresh JWT this many ms before expiry to avoid edge-case 401s. */
|
|
38
49
|
const JWT_REFRESH_LEEWAY_MS = 60_000;
|
|
39
50
|
export class SynaluxStorage extends SupabaseStorage {
|
|
@@ -258,15 +269,19 @@ export class SynaluxStorage extends SupabaseStorage {
|
|
|
258
269
|
// filters. Falls back to plain text search when only queryText is
|
|
259
270
|
// supplied.
|
|
260
271
|
async searchKnowledge(params) {
|
|
261
|
-
const
|
|
272
|
+
const wireBody = KnowledgeSearchRequestSchema.parse({
|
|
262
273
|
action: "knowledge_search",
|
|
263
274
|
project: params.project ?? undefined,
|
|
264
275
|
keywords: params.keywords ?? [],
|
|
265
276
|
category: params.category ?? undefined,
|
|
266
|
-
|
|
277
|
+
query: params.queryText ?? undefined,
|
|
267
278
|
limit: params.limit ?? 10,
|
|
268
279
|
role: params.role ?? undefined,
|
|
280
|
+
// Scope precedence: explicit caller param > PRISM_KNOWLEDGE_SCOPE env
|
|
281
|
+
// > undefined (portal decides). No hardcoded default here.
|
|
282
|
+
scope: resolveKnowledgeScope(params.scope),
|
|
269
283
|
});
|
|
284
|
+
const result = await this.portalPost("/api/v1/prism/memory", wireBody);
|
|
270
285
|
const count = typeof result.count === "number" ? result.count : 0;
|
|
271
286
|
const results = Array.isArray(result.results) ? result.results : [];
|
|
272
287
|
return { count, results };
|
|
@@ -19,7 +19,6 @@ import { formatRulesBlock, applySentinelBlock } from "./commonHelpers.js";
|
|
|
19
19
|
import { debugLog } from "../utils/logger.js";
|
|
20
20
|
import { recordCognitiveRoute } from "../observability/graphMetrics.js";
|
|
21
21
|
import { getStorage } from "../storage/index.js";
|
|
22
|
-
import { toKeywordArray } from "../utils/keywordExtractor.js";
|
|
23
22
|
import { getLLMProvider } from "../utils/llm/factory.js";
|
|
24
23
|
import { getSetting } from "../storage/configStorage.js";
|
|
25
24
|
// ─── Phase 1: Explainability & Memory Lineage ────────────────
|
|
@@ -67,14 +66,21 @@ export async function knowledgeSearchHandler(args) {
|
|
|
67
66
|
debugLog(`[knowledge_search] Searching: project=${project || "all"}, query="${query || ""}", category=${category || "any"}, limit=${limit}`);
|
|
68
67
|
// Phase 1: Capture total start time for latency measurement
|
|
69
68
|
const totalStart = performance.now();
|
|
70
|
-
const searchKeywords = query ? toKeywordArray(query) : [];
|
|
71
69
|
const storage = await getStorage();
|
|
70
|
+
// NOTE: do NOT auto-derive keywords from the free-text query. The portal
|
|
71
|
+
// applies keywords as a hard .overlaps() AND-filter; deriving them from
|
|
72
|
+
// the query (via toKeywordArray) turns "BFCL" into keywords=['bfcl']
|
|
73
|
+
// which excludes every row whose keywords[] column doesn't contain
|
|
74
|
+
// 'bfcl' — even when the summary clearly matches "BFCL" via textSearch.
|
|
75
|
+
// Free-text search must rely on textSearch alone. Callers who want a
|
|
76
|
+
// keyword overlap filter should pass keywords explicitly via a future
|
|
77
|
+
// input-schema field, not via implicit extraction.
|
|
72
78
|
// Phase 1: Capture storage-specific start time to isolate DB latency
|
|
73
79
|
// from keyword extraction and other overhead
|
|
74
80
|
const storageStart = performance.now();
|
|
75
81
|
const data = await storage.searchKnowledge({
|
|
76
82
|
project: project || null,
|
|
77
|
-
keywords:
|
|
83
|
+
keywords: [],
|
|
78
84
|
category: category || null,
|
|
79
85
|
queryText: query || null,
|
|
80
86
|
limit: Math.min(limit, 50),
|
|
@@ -207,6 +213,16 @@ export async function knowledgeSearchHandler(args) {
|
|
|
207
213
|
catch (graphErr) {
|
|
208
214
|
debugLog(`[knowledge_search] Graph expansion failed (non-fatal): ${graphErr instanceof Error ? graphErr.message : String(graphErr)}`);
|
|
209
215
|
}
|
|
216
|
+
// Machine-readable evidence snippets for direct pass-through to prism_infer
|
|
217
|
+
if (data.results && Array.isArray(data.results) && data.results.length > 0) {
|
|
218
|
+
const evidenceSnippets = data.results.map((r, i) => ({
|
|
219
|
+
source: `knowledge_search:${r.id ?? i}`,
|
|
220
|
+
content: (r.content ?? r.summary ?? r.text ?? "").slice(0, 1000),
|
|
221
|
+
})).filter((s) => s.content);
|
|
222
|
+
if (evidenceSnippets.length > 0) {
|
|
223
|
+
contentBlocks.push({ type: "text", text: JSON.stringify({ evidence_snippets: evidenceSnippets }) });
|
|
224
|
+
}
|
|
225
|
+
}
|
|
210
226
|
return { content: contentBlocks, isError: false };
|
|
211
227
|
}
|
|
212
228
|
export async function knowledgeForgetHandler(args) {
|
|
@@ -553,6 +569,16 @@ export async function sessionSearchMemoryHandler(args) {
|
|
|
553
569
|
});
|
|
554
570
|
contentBlocks.push(traceToContentBlock(trace));
|
|
555
571
|
}
|
|
572
|
+
// Machine-readable evidence snippets for direct pass-through to prism_infer
|
|
573
|
+
{
|
|
574
|
+
const evidenceSnippets = results.map((r, i) => ({
|
|
575
|
+
source: `session_search_memory:${r.id ?? i}`,
|
|
576
|
+
content: (r.summary ?? "").slice(0, 1000),
|
|
577
|
+
})).filter((s) => s.content);
|
|
578
|
+
if (evidenceSnippets.length > 0) {
|
|
579
|
+
contentBlocks.push({ type: "text", text: JSON.stringify({ evidence_snippets: evidenceSnippets }) });
|
|
580
|
+
}
|
|
581
|
+
}
|
|
556
582
|
// ── v6.0 Phase 3: 1-Hop Graph Expansion ──────────────────
|
|
557
583
|
// After direct hits, traverse outbound links from each result to
|
|
558
584
|
// find associated memories. Graph-expanded results are BONUS — they
|
package/dist/tools/index.js
CHANGED
|
@@ -26,7 +26,7 @@ export { webSearchHandler, braveWebSearchCodeModeHandler, localSearchHandler, br
|
|
|
26
26
|
// This file always exports them — server.ts decides whether to include them in the tool list.
|
|
27
27
|
//
|
|
28
28
|
// v0.4.0: Added SESSION_COMPACT_LEDGER_TOOL and SESSION_SEARCH_MEMORY_TOOL
|
|
29
|
-
export { SESSION_SAVE_LEDGER_TOOL, SESSION_SAVE_HANDOFF_TOOL, SESSION_LOAD_CONTEXT_TOOL, KNOWLEDGE_SEARCH_TOOL, KNOWLEDGE_FORGET_TOOL, SESSION_COMPACT_LEDGER_TOOL, SESSION_SEARCH_MEMORY_TOOL, MEMORY_HISTORY_TOOL, MEMORY_CHECKOUT_TOOL, SESSION_SAVE_IMAGE_TOOL, SESSION_VIEW_IMAGE_TOOL, SESSION_HEALTH_CHECK_TOOL, SESSION_FORGET_MEMORY_TOOL, SESSION_EXPORT_MEMORY_TOOL, KNOWLEDGE_SET_RETENTION_TOOL, SESSION_SAVE_EXPERIENCE_TOOL, KNOWLEDGE_UPVOTE_TOOL, KNOWLEDGE_DOWNVOTE_TOOL, KNOWLEDGE_SYNC_RULES_TOOL, DEEP_STORAGE_PURGE_TOOL, SESSION_INTUITIVE_RECALL_TOOL, SESSION_BACKFILL_LINKS_TOOL, MAINTENANCE_VACUUM_TOOL, isDeepStoragePurgeArgs, SESSION_SYNTHESIZE_EDGES_TOOL, isSessionSynthesizeEdgesArgs, SESSION_COGNITIVE_ROUTE_TOOL, isSessionCognitiveRouteArgs, SESSION_TASK_ROUTE_TOOL, isSessionTaskRouteArgs, ONBOARDING_WIZARD_TOOL, EXTRACT_ENTITIES_TOOL, API_ANALYTICS_TOOL, BACKUP_DATABASE_TOOL, CONFIGURE_NOTIFICATIONS_TOOL, QUERY_MEMORY_NATURAL_TOOL } from "./sessionMemoryDefinitions.js";
|
|
29
|
+
export { SESSION_SAVE_LEDGER_TOOL, SESSION_SAVE_HANDOFF_TOOL, SESSION_LOAD_CONTEXT_TOOL, KNOWLEDGE_SEARCH_TOOL, KNOWLEDGE_FORGET_TOOL, SESSION_COMPACT_LEDGER_TOOL, SESSION_SEARCH_MEMORY_TOOL, MEMORY_HISTORY_TOOL, MEMORY_CHECKOUT_TOOL, SESSION_SAVE_IMAGE_TOOL, SESSION_VIEW_IMAGE_TOOL, SESSION_HEALTH_CHECK_TOOL, SESSION_BACKFILL_EMBEDDINGS_TOOL, SESSION_FORGET_MEMORY_TOOL, SESSION_EXPORT_MEMORY_TOOL, KNOWLEDGE_SET_RETENTION_TOOL, SESSION_SAVE_EXPERIENCE_TOOL, KNOWLEDGE_UPVOTE_TOOL, KNOWLEDGE_DOWNVOTE_TOOL, KNOWLEDGE_SYNC_RULES_TOOL, DEEP_STORAGE_PURGE_TOOL, SESSION_INTUITIVE_RECALL_TOOL, SESSION_BACKFILL_LINKS_TOOL, MAINTENANCE_VACUUM_TOOL, isDeepStoragePurgeArgs, SESSION_SYNTHESIZE_EDGES_TOOL, isSessionSynthesizeEdgesArgs, SESSION_COGNITIVE_ROUTE_TOOL, isSessionCognitiveRouteArgs, SESSION_TASK_ROUTE_TOOL, isSessionTaskRouteArgs, ONBOARDING_WIZARD_TOOL, EXTRACT_ENTITIES_TOOL, API_ANALYTICS_TOOL, BACKUP_DATABASE_TOOL, CONFIGURE_NOTIFICATIONS_TOOL, QUERY_MEMORY_NATURAL_TOOL } from "./sessionMemoryDefinitions.js";
|
|
30
30
|
// 1. Ledger (Core CRUD & State)
|
|
31
31
|
export { sessionSaveLedgerHandler, sessionSaveHandoffHandler, sessionLoadContextHandler, sessionSaveExperienceHandler, sessionSaveImageHandler, sessionViewImageHandler, memoryHistoryHandler, memoryCheckoutHandler, sessionForgetMemoryHandler, sessionExportMemoryHandler } from "./ledgerHandlers.js";
|
|
32
32
|
// 2. Graph (Semantic Search & Weighting)
|
|
@@ -24,6 +24,7 @@ import { getSynaluxJwt, invalidateSynaluxJwt } from "../utils/synaluxJwt.js";
|
|
|
24
24
|
import { getAvailableMemoryBytes } from "../utils/availableMemory.js";
|
|
25
25
|
import { PRISM_SYNALUX_BASE_URL, PRISM_LOCAL_LLM_URL, } from "../config.js";
|
|
26
26
|
import { debugLog } from "../utils/logger.js";
|
|
27
|
+
import { verifyGrounding } from "../utils/groundingVerifier.js";
|
|
27
28
|
// ─── Tool Definition ────────────────────────────────────────────
|
|
28
29
|
export const PRISM_INFER_TOOL = {
|
|
29
30
|
name: "prism_infer",
|
|
@@ -69,6 +70,37 @@ export const PRISM_INFER_TOOL = {
|
|
|
69
70
|
type: "number",
|
|
70
71
|
description: "Override per-call timeout. Default scales with model size: 32B=120s, 14B=60s, 8B=30s, 1.7B=15s.",
|
|
71
72
|
},
|
|
73
|
+
evidence: {
|
|
74
|
+
type: "array",
|
|
75
|
+
description: "Optional evidence snippets the model output must be grounded in. " +
|
|
76
|
+
"When supplied with `verify: true`, every assertive claim in the draft " +
|
|
77
|
+
"(numbers, names, dates, codes, $ amounts) must be ENTAILED by one of " +
|
|
78
|
+
"these snippets or the draft is refused.",
|
|
79
|
+
items: {
|
|
80
|
+
type: "object",
|
|
81
|
+
properties: {
|
|
82
|
+
source: { type: "string", description: "Label for the snippet (e.g. 'tool:knowledge_search#3')." },
|
|
83
|
+
content: { type: "string", description: "The evidence text itself." },
|
|
84
|
+
},
|
|
85
|
+
required: ["source", "content"],
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
verify: {
|
|
89
|
+
type: "boolean",
|
|
90
|
+
description: "Enable the L3 grounding verifier. Default: true when `evidence` is provided, " +
|
|
91
|
+
"false otherwise. When enabled, the model's draft is checked by a different model " +
|
|
92
|
+
"(prism-coder:1b7 by default) against the supplied `evidence`. Drafts with " +
|
|
93
|
+
"NEUTRAL or CONTRADICTED claims are refused.",
|
|
94
|
+
},
|
|
95
|
+
verifier_model: {
|
|
96
|
+
type: "string",
|
|
97
|
+
description: "Override the verifier model. Default: prism-coder:1b7.",
|
|
98
|
+
},
|
|
99
|
+
verifier_timeout_ms: {
|
|
100
|
+
type: "number",
|
|
101
|
+
description: "Override the verifier hard timeout. Default 2000 ms.",
|
|
102
|
+
default: 2000,
|
|
103
|
+
},
|
|
72
104
|
},
|
|
73
105
|
required: ["prompt"],
|
|
74
106
|
},
|
|
@@ -92,6 +124,23 @@ export function isPrismInferArgs(args) {
|
|
|
92
124
|
if (a.model_ceiling !== undefined &&
|
|
93
125
|
!["32b", "14b", "8b", "1b7"].includes(a.model_ceiling))
|
|
94
126
|
return false;
|
|
127
|
+
if (a.verify !== undefined && typeof a.verify !== "boolean")
|
|
128
|
+
return false;
|
|
129
|
+
if (a.verifier_model !== undefined && typeof a.verifier_model !== "string")
|
|
130
|
+
return false;
|
|
131
|
+
if (a.verifier_timeout_ms !== undefined && typeof a.verifier_timeout_ms !== "number")
|
|
132
|
+
return false;
|
|
133
|
+
if (a.evidence !== undefined) {
|
|
134
|
+
if (!Array.isArray(a.evidence))
|
|
135
|
+
return false;
|
|
136
|
+
for (const e of a.evidence) {
|
|
137
|
+
if (!e || typeof e !== "object")
|
|
138
|
+
return false;
|
|
139
|
+
const es = e;
|
|
140
|
+
if (typeof es.source !== "string" || typeof es.content !== "string")
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
95
144
|
return true;
|
|
96
145
|
}
|
|
97
146
|
// ─── Ollama helpers ────────────────────────────────────────────
|
|
@@ -269,15 +318,14 @@ export async function runInfer(args, deps) {
|
|
|
269
318
|
const timeout = args.timeout_ms ?? DEFAULT_TIMEOUTS[tier.tag] ?? 60_000;
|
|
270
319
|
const result = await deps.callLocal(deps.ollamaUrl, ollamaName, args.prompt, args.system, maxTokens, temperature, timeout);
|
|
271
320
|
if (result.ok) {
|
|
272
|
-
return {
|
|
273
|
-
output: result.text,
|
|
321
|
+
return await applyVerification(result.text, args, deps, {
|
|
274
322
|
backend: `ollama-${tier.tag.replace("prism-coder:", "")}`,
|
|
275
323
|
model_picked: tier.tag,
|
|
276
324
|
ram_free_mb: ramFreeMb,
|
|
277
325
|
latency_ms: Date.now() - t0,
|
|
278
326
|
used_cloud: false,
|
|
279
327
|
attempts,
|
|
280
|
-
};
|
|
328
|
+
});
|
|
281
329
|
}
|
|
282
330
|
attempts.push({ tier: tier.tag, reason: result.reason });
|
|
283
331
|
}
|
|
@@ -292,15 +340,14 @@ export async function runInfer(args, deps) {
|
|
|
292
340
|
const cloudTimeout = args.timeout_ms ?? 90_000;
|
|
293
341
|
const cloud = await deps.callCloud(args.prompt, maxTokens, cloudTimeout);
|
|
294
342
|
if (cloud.ok && cloud.output) {
|
|
295
|
-
return {
|
|
296
|
-
output: cloud.output,
|
|
343
|
+
return await applyVerification(cloud.output, args, deps, {
|
|
297
344
|
backend: cloud.backend ?? "synalux",
|
|
298
345
|
model_picked: null,
|
|
299
346
|
ram_free_mb: ramFreeMb,
|
|
300
347
|
latency_ms: Date.now() - t0,
|
|
301
348
|
used_cloud: true,
|
|
302
349
|
attempts,
|
|
303
|
-
};
|
|
350
|
+
});
|
|
304
351
|
}
|
|
305
352
|
attempts.push({ tier: "synalux", reason: cloud.reason ?? "unknown" });
|
|
306
353
|
}
|
|
@@ -312,6 +359,36 @@ export async function runInfer(args, deps) {
|
|
|
312
359
|
err.attempts = attempts;
|
|
313
360
|
throw err;
|
|
314
361
|
}
|
|
362
|
+
/**
|
|
363
|
+
* Wraps a successful inference result with the L3 grounding verifier
|
|
364
|
+
* when the caller opted in via `verify: true`. The verifier substitutes
|
|
365
|
+
* the model's draft with a refusal string if any claim is not entailed
|
|
366
|
+
* by the supplied evidence; we surface that as a non-null `verification`
|
|
367
|
+
* field so callers can route refusals separately from successes.
|
|
368
|
+
*/
|
|
369
|
+
async function applyVerification(draft, args, deps, partial) {
|
|
370
|
+
const shouldVerify = args.verify ?? (args.evidence !== undefined && args.evidence.length > 0);
|
|
371
|
+
if (!shouldVerify) {
|
|
372
|
+
return { ...partial, output: draft };
|
|
373
|
+
}
|
|
374
|
+
const verifier = deps.callVerifier ?? verifyGrounding;
|
|
375
|
+
const outcome = await verifier({
|
|
376
|
+
draft,
|
|
377
|
+
evidence: args.evidence ?? [],
|
|
378
|
+
verifierModel: args.verifier_model,
|
|
379
|
+
timeoutMs: args.verifier_timeout_ms,
|
|
380
|
+
ollamaUrl: deps.ollamaUrl,
|
|
381
|
+
});
|
|
382
|
+
return {
|
|
383
|
+
...partial,
|
|
384
|
+
output: outcome.finalText,
|
|
385
|
+
verification: {
|
|
386
|
+
action: outcome.action,
|
|
387
|
+
verifierChain: outcome.verifierChain,
|
|
388
|
+
refusalClaim: outcome.refusalClaim,
|
|
389
|
+
},
|
|
390
|
+
};
|
|
391
|
+
}
|
|
315
392
|
/**
|
|
316
393
|
* MCP-shaped handler. Wraps runInfer with real deps + MCP envelope.
|
|
317
394
|
*/
|
|
@@ -334,6 +411,7 @@ export async function prismInferHandler(args) {
|
|
|
334
411
|
` free_ram=${result.ram_free_mb}MB` +
|
|
335
412
|
` latency=${result.latency_ms}ms` +
|
|
336
413
|
` used_cloud=${result.used_cloud}` +
|
|
414
|
+
(result.verification ? ` verify=${result.verification.action}` : "") +
|
|
337
415
|
(result.attempts.length ? ` attempts=${JSON.stringify(result.attempts)}` : "");
|
|
338
416
|
return {
|
|
339
417
|
content: [
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* groundingVerifier — runtime accountability for prism_infer
|
|
3
|
+
* ============================================================
|
|
4
|
+
*
|
|
5
|
+
* When a caller passes `evidence` + `verify: true` to prism_infer, this
|
|
6
|
+
* module checks that every factual claim in the model's draft is
|
|
7
|
+
* entailed by one of the evidence snippets. Sibling of synalux-portal's
|
|
8
|
+
* chat-verifier — same architecture, lighter footprint (no DB audit,
|
|
9
|
+
* stateless MCP), pointed at free-form generation instead of tool-call
|
|
10
|
+
* responses.
|
|
11
|
+
*
|
|
12
|
+
* Cascade role: prism-coder:1b7 is the default verifier on every
|
|
13
|
+
* device (server, iPad). Larger tiers (8B/14B/32B) draft; 1b7 verifies.
|
|
14
|
+
* Different model from the drafter — satisfies the Patronus rule.
|
|
15
|
+
*
|
|
16
|
+
* Failure modes:
|
|
17
|
+
* - Verifier model unreachable / timeout → fail-closed refusal
|
|
18
|
+
* - Verifier returns malformed JSON → fail-closed refusal
|
|
19
|
+
* - NEUTRAL or CONTRADICTED claim → fail-closed refusal that names
|
|
20
|
+
* the failed claim
|
|
21
|
+
*
|
|
22
|
+
* The refusal text always names which claim couldn't be grounded so
|
|
23
|
+
* the calling agent can decide whether to retry with more evidence or
|
|
24
|
+
* fall back to cloud.
|
|
25
|
+
*/
|
|
26
|
+
import { PRISM_LOCAL_LLM_URL } from "../config.js";
|
|
27
|
+
// ─── Pre-checks ─────────────────────────────────────────────────────────
|
|
28
|
+
const ASSERTIVE_RX = /\b(?:\d{1,5}|[A-Z]\d{2}\.\d|ICD-?10|CPT|\$\d|\d{4}-\d{2}-\d{2}|[A-Z][a-z]{2,}\s+[A-Z][a-z]{2,})\b/;
|
|
29
|
+
/**
|
|
30
|
+
* Returns true when the draft makes at least one assertion that could be
|
|
31
|
+
* fabricated — numbers, dates, ICD/CPT codes, two-word names, dollar
|
|
32
|
+
* amounts. Conversational replies skip the verifier entirely.
|
|
33
|
+
*/
|
|
34
|
+
export function draftHasAssertiveClaims(draft) {
|
|
35
|
+
if (!draft)
|
|
36
|
+
return false;
|
|
37
|
+
return ASSERTIVE_RX.test(draft);
|
|
38
|
+
}
|
|
39
|
+
// ─── Verifier prompt (grammar-constrained JSON) ─────────────────────────
|
|
40
|
+
const VERIFIER_SYSTEM_PROMPT = `You are a strict factual-grounding verifier. Your job is to REJECT ungrounded claims.
|
|
41
|
+
Given EVIDENCE (one or more text snippets) and DRAFT_ANSWER, find every
|
|
42
|
+
factual claim (counts, names, dates, codes, dollar amounts) and assign:
|
|
43
|
+
|
|
44
|
+
ENTAILED — the EXACT value appears verbatim in EVIDENCE text, or is an
|
|
45
|
+
arithmetic identity (e.g. "3" and "three"). STRICT: if you
|
|
46
|
+
must infer, estimate, or extrapolate, it is NOT ENTAILED.
|
|
47
|
+
CONTRADICTED — the claim states a DIFFERENT value than what EVIDENCE says
|
|
48
|
+
for the same fact.
|
|
49
|
+
NEUTRAL — the claim is not addressed in EVIDENCE at all.
|
|
50
|
+
|
|
51
|
+
CRITICAL DEFAULT RULE: when in doubt, use NEUTRAL — never guess ENTAILED.
|
|
52
|
+
Prefer false negatives over false positives. If the evidence does not
|
|
53
|
+
explicitly state the value, it is NEUTRAL.
|
|
54
|
+
|
|
55
|
+
Do NOT report opinions, refusals, or hedges as claims. Conversational
|
|
56
|
+
phrasing ("Hello", "I can help") is not a claim.
|
|
57
|
+
|
|
58
|
+
Output JSON only — no prose, no apology.`;
|
|
59
|
+
const VERIFIER_JSON_SCHEMA = {
|
|
60
|
+
type: "object",
|
|
61
|
+
properties: {
|
|
62
|
+
claims: {
|
|
63
|
+
type: "array",
|
|
64
|
+
items: {
|
|
65
|
+
type: "object",
|
|
66
|
+
properties: {
|
|
67
|
+
text: { type: "string" },
|
|
68
|
+
verdict: { type: "string", enum: ["ENTAILED", "NEUTRAL", "CONTRADICTED"] },
|
|
69
|
+
evidence_span: { type: ["string", "null"] },
|
|
70
|
+
},
|
|
71
|
+
required: ["text", "verdict", "evidence_span"],
|
|
72
|
+
additionalProperties: false,
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
required: ["claims"],
|
|
77
|
+
additionalProperties: false,
|
|
78
|
+
};
|
|
79
|
+
// ─── Refusal text ───────────────────────────────────────────────────────
|
|
80
|
+
function refusalText(action, failedClaim) {
|
|
81
|
+
switch (action) {
|
|
82
|
+
case "refused_fabricated":
|
|
83
|
+
return `I can't ground "${failedClaim}" in the evidence provided. ` +
|
|
84
|
+
"If this claim is correct, supply the supporting source as evidence and retry.";
|
|
85
|
+
case "refused_no_evidence":
|
|
86
|
+
return `I can't ground "${failedClaim}" — no evidence was provided this turn. ` +
|
|
87
|
+
"Provide evidence snippets via the `evidence` argument and retry.";
|
|
88
|
+
case "refused_timeout":
|
|
89
|
+
return `I couldn't verify "${failedClaim}" within the allowed time. ` +
|
|
90
|
+
"The verifier model may be cold-loading; try again in a moment.";
|
|
91
|
+
case "served":
|
|
92
|
+
return ""; // unreachable
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
export async function verifyGrounding(opts) {
|
|
96
|
+
const verifierModel = opts.verifierModel ?? "prism-coder:1b7";
|
|
97
|
+
const timeoutMs = opts.timeoutMs ?? 2000;
|
|
98
|
+
const ollamaUrl = opts.ollamaUrl ?? PRISM_LOCAL_LLM_URL;
|
|
99
|
+
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
100
|
+
const verifierChain = [];
|
|
101
|
+
// Tier 0 — conversational drafts skip the verifier entirely.
|
|
102
|
+
if (!draftHasAssertiveClaims(opts.draft)) {
|
|
103
|
+
return {
|
|
104
|
+
action: "served",
|
|
105
|
+
finalText: opts.draft,
|
|
106
|
+
claims: [],
|
|
107
|
+
verifierChain,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
// Tier 0a — assertive draft with NO evidence is fail-closed:
|
|
111
|
+
// the model is making claims it cannot back up.
|
|
112
|
+
if (opts.evidence.length === 0) {
|
|
113
|
+
const claim = firstAssertiveSpan(opts.draft);
|
|
114
|
+
return {
|
|
115
|
+
action: "refused_no_evidence",
|
|
116
|
+
finalText: refusalText("refused_no_evidence", claim),
|
|
117
|
+
claims: [{ claim, verdict: "NEUTRAL", evidence_span: null }],
|
|
118
|
+
verifierChain,
|
|
119
|
+
refusalClaim: claim,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
// Tier 2 — NLI verifier call.
|
|
123
|
+
const t0 = Date.now();
|
|
124
|
+
const evidenceText = opts.evidence
|
|
125
|
+
.map((e, i) => `[${i}] ${e.source}\n${e.content}`)
|
|
126
|
+
.join("\n\n");
|
|
127
|
+
const payload = {
|
|
128
|
+
model: verifierModel,
|
|
129
|
+
messages: [
|
|
130
|
+
{ role: "system", content: VERIFIER_SYSTEM_PROMPT },
|
|
131
|
+
{ role: "user", content: `EVIDENCE:\n${evidenceText}\n\nDRAFT_ANSWER:\n${opts.draft}` },
|
|
132
|
+
],
|
|
133
|
+
stream: false,
|
|
134
|
+
response_format: {
|
|
135
|
+
type: "json_schema",
|
|
136
|
+
json_schema: { name: "verifier", schema: VERIFIER_JSON_SCHEMA, strict: true },
|
|
137
|
+
},
|
|
138
|
+
temperature: 0,
|
|
139
|
+
};
|
|
140
|
+
let parsedClaims = null;
|
|
141
|
+
try {
|
|
142
|
+
const res = await fetchImpl(`${ollamaUrl}/v1/chat/completions`, {
|
|
143
|
+
method: "POST",
|
|
144
|
+
headers: { "Content-Type": "application/json" },
|
|
145
|
+
body: JSON.stringify(payload),
|
|
146
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
147
|
+
});
|
|
148
|
+
if (!res.ok)
|
|
149
|
+
throw new Error(`HTTP ${res.status}`);
|
|
150
|
+
const data = (await res.json());
|
|
151
|
+
const content = data?.choices?.[0]?.message?.content;
|
|
152
|
+
if (typeof content !== "string")
|
|
153
|
+
throw new Error("no content");
|
|
154
|
+
const parsed = JSON.parse(content);
|
|
155
|
+
if (!parsed || !Array.isArray(parsed.claims))
|
|
156
|
+
throw new Error("malformed");
|
|
157
|
+
parsedClaims = parsed.claims.map((c) => ({
|
|
158
|
+
claim: String(c.text ?? ""),
|
|
159
|
+
verdict: ["ENTAILED", "NEUTRAL", "CONTRADICTED"].includes(c.verdict)
|
|
160
|
+
? c.verdict
|
|
161
|
+
: "NEUTRAL",
|
|
162
|
+
evidence_span: typeof c.evidence_span === "string" ? c.evidence_span : null,
|
|
163
|
+
}));
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
const latencyMs = Date.now() - t0;
|
|
167
|
+
verifierChain.push({ model: verifierModel, verdict: "NEUTRAL", latencyMs });
|
|
168
|
+
const claim = firstAssertiveSpan(opts.draft);
|
|
169
|
+
return {
|
|
170
|
+
action: "refused_timeout",
|
|
171
|
+
finalText: refusalText("refused_timeout", claim),
|
|
172
|
+
claims: [{ claim, verdict: "NEUTRAL", evidence_span: null }],
|
|
173
|
+
verifierChain,
|
|
174
|
+
refusalClaim: claim,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
const latencyMs = Date.now() - t0;
|
|
178
|
+
const failing = parsedClaims.find(c => c.verdict !== "ENTAILED");
|
|
179
|
+
const rollup = failing ? failing.verdict : "ENTAILED";
|
|
180
|
+
verifierChain.push({ model: verifierModel, verdict: rollup, latencyMs });
|
|
181
|
+
if (failing) {
|
|
182
|
+
return {
|
|
183
|
+
action: "refused_fabricated",
|
|
184
|
+
finalText: refusalText("refused_fabricated", failing.claim),
|
|
185
|
+
claims: parsedClaims,
|
|
186
|
+
verifierChain,
|
|
187
|
+
refusalClaim: failing.claim,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
return {
|
|
191
|
+
action: "served",
|
|
192
|
+
finalText: opts.draft,
|
|
193
|
+
claims: parsedClaims,
|
|
194
|
+
verifierChain,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
// ─── helpers ────────────────────────────────────────────────────────────
|
|
198
|
+
function firstAssertiveSpan(draft) {
|
|
199
|
+
const m = draft.match(ASSERTIVE_RX);
|
|
200
|
+
if (m)
|
|
201
|
+
return m[0];
|
|
202
|
+
return draft.slice(0, 80);
|
|
203
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "prism-mcp-server",
|
|
3
|
-
"version": "15.
|
|
3
|
+
"version": "15.6.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, 54 Agent Skills, Zero-Search HDC/HRR retrieval, HIPAA-hardened local-first storage, SLERP-optimized GRPO alignment) plus the prism-coder:7b / 14b open-weights LLM fleet.",
|
|
6
6
|
"module": "index.ts",
|