prism-mcp-server 20.2.3 → 20.2.4
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 +13 -0
- package/dist/cli.js +49 -21
- package/dist/storage/synalux.js +42 -23
- package/dist/tools/ledgerHandlers.js +78 -26
- package/dist/utils/memoryQuality.js +51 -0
- package/dist/utils/startupRecovery.js +13 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -57,6 +57,19 @@ features.
|
|
|
57
57
|
<details>
|
|
58
58
|
<summary>Release history (optional)</summary>
|
|
59
59
|
|
|
60
|
+
## What's New in v20.2.4
|
|
61
|
+
|
|
62
|
+
### Reliable Session Memory That Shows Work, Not Greetings
|
|
63
|
+
Greeting-only assistant replies are skipped before ledger writes. Existing
|
|
64
|
+
greeting rows are filtered at read time across native startup, MCP context, and
|
|
65
|
+
`prism load --json`, while entries containing decisions, TODOs, changed files,
|
|
66
|
+
or non-session events remain visible. Historical rows are not destructively
|
|
67
|
+
deleted. If Synalux has a transient startup failure, Prism displays one bounded
|
|
68
|
+
local last-good snapshot and clearly labels it; permanent authorization or
|
|
69
|
+
validation failures still fail loud, and later writes remain cloud-routed.
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
60
73
|
## What's New in v20.2.2
|
|
61
74
|
|
|
62
75
|
### One Local-First Workflow Across Every Agent
|
package/dist/cli.js
CHANGED
|
@@ -10,22 +10,25 @@ import { getCurrentGitState } from './utils/git.js';
|
|
|
10
10
|
import { sessionBootstrapHandler, sessionLoadContextHandler, sessionSaveLedgerHandler, sessionSaveHandoffHandler, } from './tools/ledgerHandlers.js';
|
|
11
11
|
import { configureClaudeAgentPolicy, configureClaudeNativeStartup, configureCodexAgentPolicy, configureCodexNativeStartup, configureGeminiAgentPolicy, configureGeminiNativeStartup, connectHosts, migrateLegacyClaudeHooks, migrateLegacyClaudeInstructions, migrateLegacyClaudeManagedStartup, migrateLegacyClaudeProjectMcp, normalizeHostName, } from './connect.js';
|
|
12
12
|
import { runBrowserCli } from './browserCli.js';
|
|
13
|
+
import { filterPrismMemoryContext } from './utils/memoryQuality.js';
|
|
14
|
+
import { isRecoverableStartupStorageError } from './utils/startupRecovery.js';
|
|
13
15
|
const program = new Command();
|
|
14
16
|
/** Build the stable `prism load --json` envelope from depth-specific context. */
|
|
15
17
|
export function buildLoadJsonOutput(project, data, level, metadata) {
|
|
18
|
+
const filteredData = filterPrismMemoryContext(data);
|
|
16
19
|
let history = [];
|
|
17
20
|
let historyLimit = 0;
|
|
18
21
|
if (level === 'standard') {
|
|
19
|
-
history = Array.isArray(
|
|
22
|
+
history = Array.isArray(filteredData.recent_sessions) ? filteredData.recent_sessions : [];
|
|
20
23
|
historyLimit = 5;
|
|
21
24
|
}
|
|
22
25
|
else if (level === 'deep') {
|
|
23
26
|
// Canonical deep context uses session_history. Fall back only when that
|
|
24
27
|
// field is absent so older portal responses remain consumable.
|
|
25
|
-
history = Array.isArray(
|
|
26
|
-
?
|
|
27
|
-
: Array.isArray(
|
|
28
|
-
?
|
|
28
|
+
history = Array.isArray(filteredData.session_history)
|
|
29
|
+
? filteredData.session_history
|
|
30
|
+
: Array.isArray(filteredData.recent_sessions)
|
|
31
|
+
? filteredData.recent_sessions
|
|
29
32
|
: [];
|
|
30
33
|
historyLimit = 50;
|
|
31
34
|
}
|
|
@@ -33,15 +36,15 @@ export function buildLoadJsonOutput(project, data, level, metadata) {
|
|
|
33
36
|
agent_name: metadata.agentName || null,
|
|
34
37
|
handoff: [{
|
|
35
38
|
project,
|
|
36
|
-
role: metadata.effectiveRole ||
|
|
37
|
-
last_summary:
|
|
38
|
-
pending_todo:
|
|
39
|
-
active_decisions:
|
|
40
|
-
keywords:
|
|
41
|
-
key_context:
|
|
42
|
-
active_branch:
|
|
43
|
-
version:
|
|
44
|
-
updated_at:
|
|
39
|
+
role: metadata.effectiveRole || filteredData.role || 'global',
|
|
40
|
+
last_summary: filteredData.last_summary || null,
|
|
41
|
+
pending_todo: filteredData.pending_todo || null,
|
|
42
|
+
active_decisions: filteredData.active_decisions || null,
|
|
43
|
+
keywords: filteredData.keywords || null,
|
|
44
|
+
key_context: filteredData.key_context || null,
|
|
45
|
+
active_branch: filteredData.active_branch || null,
|
|
46
|
+
version: filteredData.version ?? null,
|
|
47
|
+
updated_at: filteredData.updated_at || null,
|
|
45
48
|
}],
|
|
46
49
|
// Keep the established key for callers while sourcing the canonical field
|
|
47
50
|
// for the requested depth.
|
|
@@ -61,21 +64,46 @@ program
|
|
|
61
64
|
.description('Prism — The Mind Palace for AI Agents')
|
|
62
65
|
.version(SERVER_CONFIG.version);
|
|
63
66
|
/** Print the canonical hook-free first-turn display used by the MCP tool. */
|
|
67
|
+
export const isRecoverableBootstrapStorageError = isRecoverableStartupStorageError;
|
|
68
|
+
async function printBootstrapDisplay() {
|
|
69
|
+
const result = await sessionBootstrapHandler({});
|
|
70
|
+
const output = result.content?.map((part) => part?.text).filter(Boolean).join('\n') || '';
|
|
71
|
+
if (!output)
|
|
72
|
+
throw new Error('session_bootstrap returned no display content');
|
|
73
|
+
console.log(output);
|
|
74
|
+
if (result.isError)
|
|
75
|
+
process.exitCode = 1;
|
|
76
|
+
}
|
|
64
77
|
export async function runBootstrapCommand() {
|
|
78
|
+
const previousStorage = process.env.PRISM_STORAGE;
|
|
65
79
|
try {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
80
|
+
try {
|
|
81
|
+
await printBootstrapDisplay();
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
if (!isRecoverableBootstrapStorageError(err))
|
|
85
|
+
throw err;
|
|
86
|
+
// Startup is a read-only display. If paid cloud memory is temporarily
|
|
87
|
+
// unavailable, retry this one read against the local last-good database
|
|
88
|
+
// instead of blocking the host's entire first turn. Do not persist the
|
|
89
|
+
// override: normal saves still use the configured subscription backend.
|
|
90
|
+
await closeStorage().catch(() => { });
|
|
91
|
+
process.env.PRISM_STORAGE = 'local';
|
|
92
|
+
console.error('Prism cloud startup unavailable; using local last-good context for this startup.');
|
|
93
|
+
await printBootstrapDisplay();
|
|
94
|
+
}
|
|
73
95
|
}
|
|
74
96
|
catch (err) {
|
|
75
97
|
console.error(`Bootstrap failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
76
98
|
process.exitCode = 1;
|
|
77
99
|
}
|
|
78
100
|
finally {
|
|
101
|
+
if (previousStorage === undefined) {
|
|
102
|
+
delete process.env.PRISM_STORAGE;
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
process.env.PRISM_STORAGE = previousStorage;
|
|
106
|
+
}
|
|
79
107
|
await closeStorage().catch(() => { });
|
|
80
108
|
}
|
|
81
109
|
}
|
package/dist/storage/synalux.js
CHANGED
|
@@ -47,12 +47,16 @@ function resolveKnowledgeScope(callerScope) {
|
|
|
47
47
|
}
|
|
48
48
|
/** Refresh JWT this many ms before expiry to avoid edge-case 401s. */
|
|
49
49
|
const JWT_REFRESH_LEEWAY_MS = 60_000;
|
|
50
|
+
function buildContextLoadKey(project, level, userId, role) {
|
|
51
|
+
return JSON.stringify({ project, level, userId, role: role ?? "" });
|
|
52
|
+
}
|
|
50
53
|
export class SynaluxStorage extends SupabaseStorage {
|
|
51
54
|
baseUrl;
|
|
52
55
|
refreshToken;
|
|
53
56
|
cachedJwt = null;
|
|
54
57
|
cachedJwtExpiresAt = 0;
|
|
55
58
|
inflightExchange = null;
|
|
59
|
+
inflightContextLoads = new Map();
|
|
56
60
|
constructor() {
|
|
57
61
|
super();
|
|
58
62
|
const url = process.env.PRISM_SYNALUX_BASE_URL || PRISM_SYNALUX_BASE_URL;
|
|
@@ -203,31 +207,46 @@ export class SynaluxStorage extends SupabaseStorage {
|
|
|
203
207
|
}
|
|
204
208
|
// ─── Context ─────────────────────────────────────────────────
|
|
205
209
|
async loadContext(project, level, userId, role) {
|
|
206
|
-
const
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
return
|
|
219
|
-
|
|
220
|
-
|
|
210
|
+
const key = buildContextLoadKey(project, level, userId, role);
|
|
211
|
+
const existing = this.inflightContextLoads.get(key);
|
|
212
|
+
if (existing)
|
|
213
|
+
return existing;
|
|
214
|
+
const request = (async () => {
|
|
215
|
+
const result = await this.portalPost("/api/v1/prism/memory", {
|
|
216
|
+
action: "load_context",
|
|
217
|
+
project,
|
|
218
|
+
level,
|
|
219
|
+
user_id: userId,
|
|
220
|
+
role,
|
|
221
|
+
});
|
|
222
|
+
// Current portals return the canonical flat `context`. Older deployed
|
|
223
|
+
// portals returned `{ handoff, recent_sessions }`; normalize that envelope
|
|
224
|
+
// here so the formatter can still see last_summary, TODOs, version, and
|
|
225
|
+
// session history during a rolling portal/client upgrade.
|
|
226
|
+
if (result.context === null)
|
|
227
|
+
return null;
|
|
228
|
+
if (result.context && typeof result.context === "object" && !Array.isArray(result.context)) {
|
|
229
|
+
return result.context;
|
|
230
|
+
}
|
|
231
|
+
if (result.handoff === null)
|
|
232
|
+
return null;
|
|
233
|
+
if (result.handoff && typeof result.handoff === "object" && !Array.isArray(result.handoff)) {
|
|
234
|
+
return {
|
|
235
|
+
...result.handoff,
|
|
236
|
+
recent_sessions: Array.isArray(result.recent_sessions) ? result.recent_sessions : [],
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
return result;
|
|
240
|
+
})();
|
|
241
|
+
this.inflightContextLoads.set(key, request);
|
|
242
|
+
try {
|
|
243
|
+
return await request;
|
|
221
244
|
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
...result.handoff,
|
|
227
|
-
recent_sessions: Array.isArray(result.recent_sessions) ? result.recent_sessions : [],
|
|
228
|
-
};
|
|
245
|
+
finally {
|
|
246
|
+
if (this.inflightContextLoads.get(key) === request) {
|
|
247
|
+
this.inflightContextLoads.delete(key);
|
|
248
|
+
}
|
|
229
249
|
}
|
|
230
|
-
return result;
|
|
231
250
|
}
|
|
232
251
|
// ─── Forget memory (GDPR surgical deletion) ──────────────────
|
|
233
252
|
// Phase 3 Tier A: route both soft and hard delete through the
|
|
@@ -31,6 +31,7 @@ import { getCurrentGitState, getGitDrift } from "../utils/git.js";
|
|
|
31
31
|
import { getSetting, getAllSettings, refreshConfigStorageCache } from "../storage/configStorage.js";
|
|
32
32
|
import { mergeHandoff, dbToHandoffSchema, sanitizeForMerge } from "../utils/crdtMerge.js";
|
|
33
33
|
import { resolveProject } from "../utils/projectResolver.js";
|
|
34
|
+
import { isRecoverableStartupStorageError, LOCAL_STARTUP_FALLBACK_NOTICE, } from "../utils/startupRecovery.js";
|
|
34
35
|
import { PRISM_USER_ID, PRISM_AUTO_CAPTURE, PRISM_CAPTURE_PORTS } from "../config.js";
|
|
35
36
|
import { captureLocalEnvironment } from "../utils/autoCapture.js";
|
|
36
37
|
import { fireCaptionAsync } from "../utils/imageCaptioner.js";
|
|
@@ -264,6 +265,7 @@ function compactWithOmissionCount(value, maxChars) {
|
|
|
264
265
|
*/
|
|
265
266
|
import { computeEffectiveImportance, recordMemoryAccess } from "../utils/cognitiveMemory.js";
|
|
266
267
|
import { formatInferenceMetrics, resetInferenceMetrics, getInferenceSnapshot } from "../utils/inferenceMetrics.js";
|
|
268
|
+
import { filterPrismMemoryContext, isGreetingOnlyMemoryEntry } from "../utils/memoryQuality.js";
|
|
267
269
|
export async function sessionSaveLedgerHandler(args) {
|
|
268
270
|
if (!isSessionSaveLedgerArgs(args)) {
|
|
269
271
|
throw new Error("Invalid arguments for session_save_ledger");
|
|
@@ -289,6 +291,16 @@ export async function sessionSaveLedgerHandler(args) {
|
|
|
289
291
|
const files_changed = args.files_changed ? sanitizeArray(args.files_changed) : undefined;
|
|
290
292
|
const decisions = args.decisions ? sanitizeArray(args.decisions) : undefined;
|
|
291
293
|
const role = args.role;
|
|
294
|
+
if (isGreetingOnlyMemoryEntry({ summary, todos, files_changed, decisions })) {
|
|
295
|
+
return {
|
|
296
|
+
content: [{
|
|
297
|
+
type: "text",
|
|
298
|
+
text: (_saveLedgerGateWarning ? `⚠️ ${_saveLedgerGateWarning}\n\n` : "") +
|
|
299
|
+
"ℹ️ Greeting-only turn skipped; no ledger entry was written.",
|
|
300
|
+
}],
|
|
301
|
+
isError: false,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
292
304
|
const storage = await getStorage();
|
|
293
305
|
// ─── Project mismatch validation (v13 — hard-rejects on mismatch) ───
|
|
294
306
|
// Replaces the old soft-warning behavior that allowed cross-project
|
|
@@ -817,7 +829,7 @@ export async function sessionLoadContextHandler(args, options = {}) {
|
|
|
817
829
|
const protectedFallbackNames = new Set(protectedFallbackEntries.map((entry) => entry.name));
|
|
818
830
|
const manifestSnapshot = await resolveNativeSkillManifestSnapshot(skillSyncResult);
|
|
819
831
|
const entitledSkillNames = new Set(manifestSnapshot.names);
|
|
820
|
-
const storage = await getStorage();
|
|
832
|
+
const storage = options.storageOverride ?? await getStorage();
|
|
821
833
|
const effectiveRole = role || await getSetting("default_role", "") || undefined;
|
|
822
834
|
const loadEntitledRoleSkill = async () => {
|
|
823
835
|
if (!effectiveRole)
|
|
@@ -830,11 +842,11 @@ export async function sessionLoadContextHandler(args, options = {}) {
|
|
|
830
842
|
}
|
|
831
843
|
return await getSetting(`user_skill:${effectiveRole}`, "");
|
|
832
844
|
};
|
|
833
|
-
const
|
|
845
|
+
const loadedData = await storage.loadContext(project, level, PRISM_USER_ID, effectiveRole); // v3.0: role with dashboard fallback
|
|
834
846
|
// F4 fix: inject protected skills even for fresh projects.
|
|
835
847
|
// Previously this returned before skill injection, leaving new projects with zero
|
|
836
848
|
// behavioral guardrails for the entire session. Now protected skills always load.
|
|
837
|
-
if (!
|
|
849
|
+
if (!loadedData) {
|
|
838
850
|
let freshSkillBlock = "";
|
|
839
851
|
const freshLoadedSkills = new Set();
|
|
840
852
|
if (includeSkillContent) {
|
|
@@ -896,6 +908,7 @@ export async function sessionLoadContextHandler(args, options = {}) {
|
|
|
896
908
|
isError: false,
|
|
897
909
|
};
|
|
898
910
|
}
|
|
911
|
+
const data = filterPrismMemoryContext(loadedData);
|
|
899
912
|
const version = data?.version;
|
|
900
913
|
const versionNote = version
|
|
901
914
|
? `\n\n🔑 Session version: ${version}. Pass expected_version: ${version} when saving handoff.`
|
|
@@ -1416,11 +1429,13 @@ export async function sessionLoadContextHandler(args, options = {}) {
|
|
|
1416
1429
|
isError: false,
|
|
1417
1430
|
};
|
|
1418
1431
|
}
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1432
|
+
async function createLocalStartupStorage() {
|
|
1433
|
+
const { SqliteStorage } = await import("../storage/sqlite.js");
|
|
1434
|
+
const storage = new SqliteStorage();
|
|
1435
|
+
await storage.initialize(true);
|
|
1436
|
+
return storage;
|
|
1437
|
+
}
|
|
1438
|
+
export async function sessionBootstrapHandler(args = {}, options = {}) {
|
|
1424
1439
|
if (typeof args !== "object" || args === null || Array.isArray(args)) {
|
|
1425
1440
|
throw new Error("Invalid arguments for session_bootstrap");
|
|
1426
1441
|
}
|
|
@@ -1487,39 +1502,76 @@ export async function sessionBootstrapHandler(args = {}) {
|
|
|
1487
1502
|
: "";
|
|
1488
1503
|
const omissionLength = omittedProjectsText ? omittedProjectsText.length + 2 : 0;
|
|
1489
1504
|
const separatorsLength = Math.max(0, renderedProjectCount - 1) * 2;
|
|
1490
|
-
perProjectMaxChars = Math.floor((startupMaxChars - startupHeader.length - systemReadyBlock.length -
|
|
1505
|
+
perProjectMaxChars = Math.floor((startupMaxChars - startupHeader.length - systemReadyBlock.length - LOCAL_STARTUP_FALLBACK_NOTICE.length -
|
|
1506
|
+
6 - omissionLength - separatorsLength) /
|
|
1491
1507
|
renderedProjectCount);
|
|
1492
1508
|
if (perProjectMaxChars >= 512 || renderedProjectCount === 1)
|
|
1493
1509
|
break;
|
|
1494
1510
|
renderedProjectCount -= 1;
|
|
1495
1511
|
}
|
|
1496
|
-
const
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1512
|
+
const renderedProjects = projects.slice(0, renderedProjectCount);
|
|
1513
|
+
const loadProjects = async (storageOverride) => {
|
|
1514
|
+
const loaded = [];
|
|
1515
|
+
let hadError = false;
|
|
1516
|
+
for (const project of renderedProjects) {
|
|
1517
|
+
const result = await sessionLoadContextHandler({
|
|
1518
|
+
project,
|
|
1519
|
+
level: depth,
|
|
1520
|
+
role: defaultRole || undefined,
|
|
1521
|
+
conversation_id: conversationId,
|
|
1522
|
+
prompt: input.prompt,
|
|
1523
|
+
}, {
|
|
1524
|
+
includeSkillContent: false,
|
|
1525
|
+
nativeMaxChars: perProjectMaxChars,
|
|
1526
|
+
skillSyncResult,
|
|
1527
|
+
storageOverride,
|
|
1528
|
+
});
|
|
1529
|
+
const text = result.content?.map((part) => part?.text).filter(Boolean).join("\n") ||
|
|
1530
|
+
`No session context found for project "${project}".`;
|
|
1531
|
+
loaded.push(text);
|
|
1532
|
+
hadError ||= result.isError === true;
|
|
1533
|
+
}
|
|
1534
|
+
return { loaded, hadError };
|
|
1535
|
+
};
|
|
1536
|
+
let localStorage = null;
|
|
1537
|
+
let usedLocalFallback = false;
|
|
1538
|
+
let startupContext;
|
|
1539
|
+
try {
|
|
1540
|
+
try {
|
|
1541
|
+
startupContext = await loadProjects();
|
|
1542
|
+
}
|
|
1543
|
+
catch (error) {
|
|
1544
|
+
if (!isRecoverableStartupStorageError(error))
|
|
1545
|
+
throw error;
|
|
1546
|
+
localStorage = await (options.localStorageFactory ?? createLocalStartupStorage)();
|
|
1547
|
+
usedLocalFallback = true;
|
|
1548
|
+
startupContext = await loadProjects(localStorage);
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
finally {
|
|
1552
|
+
if (localStorage) {
|
|
1553
|
+
try {
|
|
1554
|
+
await localStorage.close();
|
|
1555
|
+
}
|
|
1556
|
+
catch (error) {
|
|
1557
|
+
debugLog(`[session_bootstrap] Local fallback close failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1510
1560
|
}
|
|
1561
|
+
const fallbackNotice = usedLocalFallback ? `${LOCAL_STARTUP_FALLBACK_NOTICE}\n\n` : "";
|
|
1511
1562
|
return {
|
|
1512
1563
|
content: [{
|
|
1513
1564
|
type: "text",
|
|
1514
|
-
text: `${startupHeader}\n\n${loaded.join("\n\n")}` +
|
|
1565
|
+
text: `${startupHeader}\n\n${fallbackNotice}${startupContext.loaded.join("\n\n")}` +
|
|
1515
1566
|
(omittedProjectsText ? `\n\n${omittedProjectsText}` : "") +
|
|
1516
1567
|
`\n\n${systemReadyBlock}`,
|
|
1517
1568
|
}],
|
|
1518
|
-
isError: hadError,
|
|
1569
|
+
isError: startupContext.hadError,
|
|
1519
1570
|
structuredContent: {
|
|
1520
1571
|
conversation_id: conversationId,
|
|
1521
|
-
projects:
|
|
1572
|
+
projects: renderedProjects,
|
|
1522
1573
|
depth,
|
|
1574
|
+
context_source: usedLocalFallback ? "local-last-good" : activeStorageBackend,
|
|
1523
1575
|
},
|
|
1524
1576
|
};
|
|
1525
1577
|
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
const SOURCE_LABEL_PATTERN = /^\s*\[[^\]]+\]\s*/u;
|
|
2
|
+
const GREETING_ONLY_PATTERN = /^(?:hi|hello|hey|ready)(?:[\s.!?,…—-]|[\u{1F300}-\u{1FAFF}]|\uFE0F)*$/iu;
|
|
3
|
+
const GREETING_OPENING_PATTERN = /^(?:hi(?:\s+there)?|hello|hey)\b/iu;
|
|
4
|
+
const ASSISTANCE_INVITATION_PATTERN = /\b(?:(?:how|what)\s+(?:can|may)\s+i\s+(?:help|assist)|let\s+me\s+know\s+what\s+you\s+need)\b/iu;
|
|
5
|
+
const SUBSTANTIVE_OUTCOME_PATTERN = /\b(?:added|built|changed|completed|configured|created|debugged|deployed|fixed|implemented|investigated|removed|repaired|resolved|tested|updated|verified|wrote)\b/iu;
|
|
6
|
+
const STRUCTURED_WORK_FIELDS = ["decisions", "todos", "files_changed"];
|
|
7
|
+
function hasStructuredWork(entry) {
|
|
8
|
+
return STRUCTURED_WORK_FIELDS.some((field) => {
|
|
9
|
+
const value = entry[field];
|
|
10
|
+
if (value === undefined || value === null)
|
|
11
|
+
return false;
|
|
12
|
+
return !Array.isArray(value) || value.length > 0;
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
/** Greeting-only assistant replies are presentation, not durable work. */
|
|
16
|
+
export function isGreetingOnlyMemoryEntry(entry) {
|
|
17
|
+
if (hasStructuredWork(entry))
|
|
18
|
+
return false;
|
|
19
|
+
if (typeof entry.event_type === "string" && entry.event_type !== "session")
|
|
20
|
+
return false;
|
|
21
|
+
if (typeof entry.summary !== "string")
|
|
22
|
+
return false;
|
|
23
|
+
const summary = entry.summary.replace(SOURCE_LABEL_PATTERN, "").trim();
|
|
24
|
+
if (!summary)
|
|
25
|
+
return false;
|
|
26
|
+
if (GREETING_ONLY_PATTERN.test(summary))
|
|
27
|
+
return true;
|
|
28
|
+
return GREETING_OPENING_PATTERN.test(summary)
|
|
29
|
+
&& ASSISTANCE_INVITATION_PATTERN.test(summary)
|
|
30
|
+
&& !SUBSTANTIVE_OUTCOME_PATTERN.test(summary);
|
|
31
|
+
}
|
|
32
|
+
export function filterGreetingOnlyMemoryEntries(entries) {
|
|
33
|
+
return entries.filter((entry) => !isGreetingOnlyMemoryEntry(entry));
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Defensive local-storage fallback for the portal-owned memory policy.
|
|
37
|
+
* Returns a copy so storage responses remain immutable for other consumers.
|
|
38
|
+
*/
|
|
39
|
+
export function filterPrismMemoryContext(data) {
|
|
40
|
+
const filtered = { ...data };
|
|
41
|
+
if (isGreetingOnlyMemoryEntry({ summary: data.last_summary })) {
|
|
42
|
+
filtered.last_summary = null;
|
|
43
|
+
}
|
|
44
|
+
if (Array.isArray(data.recent_sessions)) {
|
|
45
|
+
filtered.recent_sessions = filterGreetingOnlyMemoryEntries(data.recent_sessions);
|
|
46
|
+
}
|
|
47
|
+
if (Array.isArray(data.session_history)) {
|
|
48
|
+
filtered.session_history = filterGreetingOnlyMemoryEntries(data.session_history);
|
|
49
|
+
}
|
|
50
|
+
return filtered;
|
|
51
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
const RECOVERABLE_STARTUP_STORAGE_ERROR = /(?:rate limit|(?:HTTP|status)\s*(?:408|425|429|5\d{2})\b|network error|fetch failed|timed?\s*out|timeout|ECONN(?:RESET|REFUSED)|ENOTFOUND|EAI_AGAIN|socket hang up)/i;
|
|
2
|
+
const RECOVERABLE_ENTITLEMENT_PROBE_ERROR = /\[Prism Storage\]\s+Could not verify the Synalux cloud-memory entitlement\b/i;
|
|
3
|
+
export const LOCAL_STARTUP_FALLBACK_NOTICE = "⚠️ Synalux cloud context is temporarily unavailable; showing local last-good context for this startup only.";
|
|
4
|
+
/**
|
|
5
|
+
* Startup may degrade to the local last-good snapshot only for transient
|
|
6
|
+
* storage failures. Validation, formatting, and programmer errors must still
|
|
7
|
+
* fail loud instead of being hidden by an unrelated fallback.
|
|
8
|
+
*/
|
|
9
|
+
export function isRecoverableStartupStorageError(error) {
|
|
10
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
11
|
+
return RECOVERABLE_STARTUP_STORAGE_ERROR.test(message)
|
|
12
|
+
|| RECOVERABLE_ENTITLEMENT_PROBE_ERROR.test(message);
|
|
13
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "prism-mcp-server",
|
|
3
|
-
"version": "20.2.
|
|
3
|
+
"version": "20.2.4",
|
|
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",
|