prism-mcp-server 3.1.1 → 4.2.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 +344 -111
- package/dist/config.js +8 -0
- package/dist/dashboard/ui.js +120 -0
- package/dist/lifecycle.js +164 -0
- package/dist/server.js +117 -46
- package/dist/storage/configStorage.js +44 -11
- package/dist/storage/sqlite.js +94 -2
- package/dist/storage/supabase.js +35 -0
- package/dist/storage/supabaseMigrations.js +110 -0
- package/dist/tools/index.js +2 -2
- package/dist/tools/sessionMemoryDefinitions.js +111 -3
- package/dist/tools/sessionMemoryHandlers.js +155 -5
- package/package.json +1 -1
package/dist/storage/sqlite.js
CHANGED
|
@@ -303,6 +303,36 @@ export class SqliteStorage {
|
|
|
303
303
|
updated_at TEXT DEFAULT (datetime('now'))
|
|
304
304
|
)
|
|
305
305
|
`);
|
|
306
|
+
// ─── v4.0 Migration: Active Behavioral Memory ──────────────
|
|
307
|
+
// Three new columns for typed experience events and insight graduation.
|
|
308
|
+
// Uses the proven idempotent try/catch pattern for safe ALTER TABLE.
|
|
309
|
+
try {
|
|
310
|
+
await this.db.execute(`ALTER TABLE session_ledger ADD COLUMN event_type TEXT DEFAULT 'session'`);
|
|
311
|
+
debugLog("[SqliteStorage] v4.0 migration: added event_type column");
|
|
312
|
+
}
|
|
313
|
+
catch (e) {
|
|
314
|
+
if (!e.message?.includes("duplicate column name"))
|
|
315
|
+
throw e;
|
|
316
|
+
}
|
|
317
|
+
try {
|
|
318
|
+
await this.db.execute(`ALTER TABLE session_ledger ADD COLUMN confidence_score INTEGER DEFAULT NULL`);
|
|
319
|
+
debugLog("[SqliteStorage] v4.0 migration: added confidence_score column");
|
|
320
|
+
}
|
|
321
|
+
catch (e) {
|
|
322
|
+
if (!e.message?.includes("duplicate column name"))
|
|
323
|
+
throw e;
|
|
324
|
+
}
|
|
325
|
+
try {
|
|
326
|
+
await this.db.execute(`ALTER TABLE session_ledger ADD COLUMN importance INTEGER DEFAULT 0`);
|
|
327
|
+
debugLog("[SqliteStorage] v4.0 migration: added importance column");
|
|
328
|
+
}
|
|
329
|
+
catch (e) {
|
|
330
|
+
if (!e.message?.includes("duplicate column name"))
|
|
331
|
+
throw e;
|
|
332
|
+
}
|
|
333
|
+
// Composite indexes for behavioral queries (idempotent via IF NOT EXISTS)
|
|
334
|
+
await this.db.execute(`CREATE INDEX IF NOT EXISTS idx_ledger_event_type ON session_ledger(event_type)`);
|
|
335
|
+
await this.db.execute(`CREATE INDEX IF NOT EXISTS idx_ledger_importance ON session_ledger(importance DESC)`);
|
|
306
336
|
}
|
|
307
337
|
// ─── PostgREST Filter Parser ───────────────────────────────
|
|
308
338
|
//
|
|
@@ -434,8 +464,10 @@ export class SqliteStorage {
|
|
|
434
464
|
await this.db.execute({
|
|
435
465
|
sql: `INSERT INTO session_ledger
|
|
436
466
|
(id, project, conversation_id, user_id, role, summary, todos, files_changed,
|
|
437
|
-
decisions, keywords, is_rollup, rollup_count, title, agent_name,
|
|
438
|
-
|
|
467
|
+
decisions, keywords, is_rollup, rollup_count, title, agent_name,
|
|
468
|
+
event_type, confidence_score, importance,
|
|
469
|
+
created_at, session_date)
|
|
470
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
439
471
|
args: [
|
|
440
472
|
id,
|
|
441
473
|
entry.project,
|
|
@@ -451,6 +483,9 @@ export class SqliteStorage {
|
|
|
451
483
|
entry.rollup_count || 0,
|
|
452
484
|
entry.is_rollup ? `Session Rollup (${entry.rollup_count || 0} entries)` : null,
|
|
453
485
|
entry.is_rollup ? "prism-compactor" : null,
|
|
486
|
+
entry.event_type || "session", // v4.0: default to 'session'
|
|
487
|
+
entry.confidence_score ?? null, // v4.0: nullable
|
|
488
|
+
entry.importance || 0, // v4.0: default to 0
|
|
454
489
|
now,
|
|
455
490
|
now,
|
|
456
491
|
],
|
|
@@ -674,6 +709,27 @@ export class SqliteStorage {
|
|
|
674
709
|
context.active_decisions = this.parseJsonColumn(handoff.active_decisions);
|
|
675
710
|
context.active_branch = handoff.active_branch;
|
|
676
711
|
context.key_context = handoff.key_context;
|
|
712
|
+
// ─── v4.0: Behavioral Warnings (Standard & Deep) ────────────
|
|
713
|
+
// Hoisted above the branch so both levels get warnings without duplication.
|
|
714
|
+
// Filters: role-scoped, non-archived, non-deleted, high importance.
|
|
715
|
+
const warningsResult = await this.db.execute({
|
|
716
|
+
sql: `SELECT summary, importance
|
|
717
|
+
FROM session_ledger
|
|
718
|
+
WHERE project = ? AND user_id = ? AND role = ?
|
|
719
|
+
AND event_type = 'correction'
|
|
720
|
+
AND importance >= 3
|
|
721
|
+
AND deleted_at IS NULL
|
|
722
|
+
AND archived_at IS NULL
|
|
723
|
+
ORDER BY importance DESC
|
|
724
|
+
LIMIT 5`,
|
|
725
|
+
args: [project, userId, effectiveRole],
|
|
726
|
+
});
|
|
727
|
+
if (warningsResult.rows.length > 0) {
|
|
728
|
+
context.behavioral_warnings = warningsResult.rows.map(r => ({
|
|
729
|
+
summary: r.summary,
|
|
730
|
+
importance: r.importance,
|
|
731
|
+
}));
|
|
732
|
+
}
|
|
677
733
|
if (level === "standard") {
|
|
678
734
|
// Add recent ledger entries (role-scoped)
|
|
679
735
|
const recentLedger = await this.db.execute({
|
|
@@ -1297,6 +1353,42 @@ export class SqliteStorage {
|
|
|
1297
1353
|
});
|
|
1298
1354
|
const expired = result.rowsAffected || 0;
|
|
1299
1355
|
debugLog(`[SqliteStorage] TTL sweep: expired ${expired} entries for "${project}" (cutoff: ${cutoffStr})`);
|
|
1356
|
+
// ─── v4.0: Importance Decay ──────────────────────────────────
|
|
1357
|
+
// Decay importance of experience entries not referenced in 30 days.
|
|
1358
|
+
// This prevents "Insight Bloat" — old corrections that are no longer
|
|
1359
|
+
// relevant gradually lose their weight and stop appearing as warnings.
|
|
1360
|
+
// Only targets typed experience events (event_type != 'session'),
|
|
1361
|
+
// so regular session logs are never affected.
|
|
1362
|
+
const decayResult = await this.db.execute({
|
|
1363
|
+
sql: `UPDATE session_ledger
|
|
1364
|
+
SET importance = MAX(0, importance - 1)
|
|
1365
|
+
WHERE project = ? AND user_id = ?
|
|
1366
|
+
AND importance > 0
|
|
1367
|
+
AND event_type != 'session'
|
|
1368
|
+
AND created_at < datetime('now', '-30 days')
|
|
1369
|
+
AND deleted_at IS NULL`,
|
|
1370
|
+
args: [project, userId],
|
|
1371
|
+
});
|
|
1372
|
+
const decayed = decayResult.rowsAffected || 0;
|
|
1373
|
+
if (decayed > 0) {
|
|
1374
|
+
debugLog(`[SqliteStorage] Importance decay: reduced ${decayed} entries for "${project}"`);
|
|
1375
|
+
}
|
|
1300
1376
|
return { expired };
|
|
1301
1377
|
}
|
|
1378
|
+
// ─── v4.0: Insight Graduation ──────────────────────────────────
|
|
1379
|
+
//
|
|
1380
|
+
// Adjusts the importance score of a ledger entry.
|
|
1381
|
+
// Used by knowledge_upvote (+1) and knowledge_downvote (-1).
|
|
1382
|
+
// Importance is clamped via MAX(0, ...) — never goes negative.
|
|
1383
|
+
// Entries reaching importance >= 7 are considered "graduated"
|
|
1384
|
+
// and will appear prominently in behavioral warnings.
|
|
1385
|
+
async adjustImportance(id, delta, userId) {
|
|
1386
|
+
await this.db.execute({
|
|
1387
|
+
sql: `UPDATE session_ledger
|
|
1388
|
+
SET importance = MAX(0, importance + ?)
|
|
1389
|
+
WHERE id = ? AND user_id = ?`,
|
|
1390
|
+
args: [delta, id, userId],
|
|
1391
|
+
});
|
|
1392
|
+
debugLog(`[SqliteStorage] Adjusted importance for ${id} by ${delta > 0 ? "+" : ""}${delta}`);
|
|
1393
|
+
}
|
|
1302
1394
|
}
|
package/dist/storage/supabase.js
CHANGED
|
@@ -15,10 +15,18 @@
|
|
|
15
15
|
import { supabasePost, supabaseGet, supabaseRpc, supabasePatch, supabaseDelete, } from "../utils/supabaseApi.js";
|
|
16
16
|
import { debugLog } from "../utils/logger.js";
|
|
17
17
|
import { getSetting as cfgGet, setSetting as cfgSet, getAllSettings as cfgGetAll } from "./configStorage.js";
|
|
18
|
+
import { runAutoMigrations } from "./supabaseMigrations.js";
|
|
18
19
|
export class SupabaseStorage {
|
|
19
20
|
// ─── Lifecycle ─────────────────────────────────────────────
|
|
20
21
|
async initialize() {
|
|
21
22
|
debugLog("[SupabaseStorage] Initialized (REST API, stateless)");
|
|
23
|
+
// Auto-apply pending schema migrations (non-fatal)
|
|
24
|
+
try {
|
|
25
|
+
await runAutoMigrations();
|
|
26
|
+
}
|
|
27
|
+
catch (err) {
|
|
28
|
+
console.error("[SupabaseStorage] Auto-migration failed. Server will continue, but some tools may be unstable.", err instanceof Error ? err.message : err);
|
|
29
|
+
}
|
|
22
30
|
}
|
|
23
31
|
async close() {
|
|
24
32
|
debugLog("[SupabaseStorage] Closed (no-op for REST)");
|
|
@@ -41,6 +49,10 @@ export class SupabaseStorage {
|
|
|
41
49
|
title: `Session Rollup (${entry.rollup_count || 0} entries)`,
|
|
42
50
|
agent_name: "prism-compactor",
|
|
43
51
|
}),
|
|
52
|
+
// v4.0: Active Behavioral Memory fields
|
|
53
|
+
event_type: entry.event_type || "session",
|
|
54
|
+
...(entry.confidence_score !== undefined && { confidence_score: entry.confidence_score }),
|
|
55
|
+
importance: entry.importance || 0,
|
|
44
56
|
};
|
|
45
57
|
return supabasePost("session_ledger", record);
|
|
46
58
|
}
|
|
@@ -348,4 +360,27 @@ export class SupabaseStorage {
|
|
|
348
360
|
debugLog(`[SupabaseStorage] TTL sweep completed for "${project}" (cutoff: ${cutoffStr})`);
|
|
349
361
|
return { expired: 0 };
|
|
350
362
|
}
|
|
363
|
+
// ─── v4.0: Insight Graduation ──────────────────────────────────
|
|
364
|
+
async adjustImportance(id, delta, userId) {
|
|
365
|
+
// Supabase PATCH can't do MAX(0, importance + delta) directly.
|
|
366
|
+
// Fetch current value first, compute new, then patch.
|
|
367
|
+
try {
|
|
368
|
+
const data = await supabaseGet("session_ledger", {
|
|
369
|
+
id: `eq.${id}`,
|
|
370
|
+
user_id: `eq.${userId}`,
|
|
371
|
+
select: "importance",
|
|
372
|
+
});
|
|
373
|
+
const rows = Array.isArray(data) ? data : [];
|
|
374
|
+
const current = rows[0]?.importance ?? 0;
|
|
375
|
+
const newVal = Math.max(0, current + delta);
|
|
376
|
+
await supabasePatch("session_ledger", { importance: newVal }, {
|
|
377
|
+
id: `eq.${id}`,
|
|
378
|
+
user_id: `eq.${userId}`,
|
|
379
|
+
});
|
|
380
|
+
debugLog(`[SupabaseStorage] Adjusted importance for ${id} by ${delta > 0 ? "+" : ""}${delta} (${current} → ${newVal})`);
|
|
381
|
+
}
|
|
382
|
+
catch (e) {
|
|
383
|
+
debugLog("[SupabaseStorage] adjustImportance failed: " + (e instanceof Error ? e.message : String(e)));
|
|
384
|
+
}
|
|
385
|
+
}
|
|
351
386
|
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Supabase Auto-Migration Runner (v4.1)
|
|
3
|
+
*
|
|
4
|
+
* On server startup, this module checks the `prism_schema_versions` table
|
|
5
|
+
* and applies any pending DDL migrations via the `prism_apply_ddl` RPC.
|
|
6
|
+
*
|
|
7
|
+
* ═══════════════════════════════════════════════════════════════════
|
|
8
|
+
* HOW IT WORKS:
|
|
9
|
+
* 1. For each migration in MIGRATIONS[], call prism_apply_ddl(version, name, sql)
|
|
10
|
+
* 2. The Postgres function checks if the version is already applied (idempotent)
|
|
11
|
+
* 3. If not applied, it EXECUTE's the SQL and records the version
|
|
12
|
+
*
|
|
13
|
+
* GRACEFUL DEGRADATION:
|
|
14
|
+
* If prism_apply_ddl doesn't exist (PGRST202), the runner logs a
|
|
15
|
+
* warning and skips — the server still starts, but v4+ tools may
|
|
16
|
+
* fail against an old schema.
|
|
17
|
+
*
|
|
18
|
+
* SECURITY NOTE:
|
|
19
|
+
* prism_apply_ddl is SECURITY DEFINER (runs as postgres owner).
|
|
20
|
+
* The prism_schema_versions table has RLS: only service_role can write.
|
|
21
|
+
* ═══════════════════════════════════════════════════════════════════
|
|
22
|
+
*/
|
|
23
|
+
import { supabaseRpc } from "../utils/supabaseApi.js";
|
|
24
|
+
/**
|
|
25
|
+
* All Supabase DDL migrations.
|
|
26
|
+
*
|
|
27
|
+
* IMPORTANT: Only add migrations for schema changes that Supabase
|
|
28
|
+
* users need. SQLite handles its own schema in sqlite.ts.
|
|
29
|
+
*
|
|
30
|
+
* Each `sql` string is passed to Postgres EXECUTE — it runs as a
|
|
31
|
+
* single transaction. Use IF NOT EXISTS / IF EXISTS guards generously.
|
|
32
|
+
*/
|
|
33
|
+
export const MIGRATIONS = [
|
|
34
|
+
{
|
|
35
|
+
version: 26,
|
|
36
|
+
name: "active_behavioral_memory",
|
|
37
|
+
sql: `
|
|
38
|
+
-- v4.0: Active Behavioral Memory columns
|
|
39
|
+
ALTER TABLE session_ledger ADD COLUMN IF NOT EXISTS event_type TEXT NOT NULL DEFAULT 'session';
|
|
40
|
+
ALTER TABLE session_ledger ADD COLUMN IF NOT EXISTS confidence_score INTEGER DEFAULT NULL;
|
|
41
|
+
ALTER TABLE session_ledger ADD COLUMN IF NOT EXISTS importance INTEGER NOT NULL DEFAULT 0;
|
|
42
|
+
|
|
43
|
+
-- Soft-delete / archival columns
|
|
44
|
+
ALTER TABLE session_ledger ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ DEFAULT NULL;
|
|
45
|
+
ALTER TABLE session_ledger ADD COLUMN IF NOT EXISTS archived_at TIMESTAMPTZ DEFAULT NULL;
|
|
46
|
+
ALTER TABLE session_ledger ADD COLUMN IF NOT EXISTS deleted_reason TEXT DEFAULT NULL;
|
|
47
|
+
|
|
48
|
+
-- Indexes
|
|
49
|
+
CREATE INDEX IF NOT EXISTS idx_ledger_event_type ON session_ledger(event_type);
|
|
50
|
+
CREATE INDEX IF NOT EXISTS idx_ledger_importance ON session_ledger(importance DESC);
|
|
51
|
+
|
|
52
|
+
-- Partial index for high-priority warnings
|
|
53
|
+
CREATE INDEX IF NOT EXISTS idx_ledger_behavioral_warnings
|
|
54
|
+
ON session_ledger(project, user_id, role, importance DESC)
|
|
55
|
+
WHERE event_type = 'correction' AND importance >= 3
|
|
56
|
+
AND deleted_at IS NULL AND archived_at IS NULL;
|
|
57
|
+
`,
|
|
58
|
+
},
|
|
59
|
+
// Future migrations go here (version 28+)
|
|
60
|
+
];
|
|
61
|
+
/**
|
|
62
|
+
* Current schema version — derived from the MIGRATIONS array.
|
|
63
|
+
* Automatically updates when new migrations are added.
|
|
64
|
+
* Used for logging and diagnostics.
|
|
65
|
+
*/
|
|
66
|
+
export const CURRENT_SCHEMA_VERSION = MIGRATIONS.length > 0 ? MIGRATIONS[MIGRATIONS.length - 1].version : 27;
|
|
67
|
+
// ─── Runner ──────────────────────────────────────────────────────
|
|
68
|
+
/**
|
|
69
|
+
* Run all pending auto-migrations on Supabase startup.
|
|
70
|
+
*
|
|
71
|
+
* Called from SupabaseStorage.initialize(). Non-fatal: if the
|
|
72
|
+
* migration infrastructure (027) hasn't been applied, the runner
|
|
73
|
+
* logs a warning and returns silently.
|
|
74
|
+
*/
|
|
75
|
+
export async function runAutoMigrations() {
|
|
76
|
+
if (MIGRATIONS.length === 0) {
|
|
77
|
+
return; // Nothing to apply
|
|
78
|
+
}
|
|
79
|
+
console.error(`[Prism Auto-Migration] Schema v${CURRENT_SCHEMA_VERSION} — checking ${MIGRATIONS.length} migration(s)…`);
|
|
80
|
+
for (const migration of MIGRATIONS) {
|
|
81
|
+
try {
|
|
82
|
+
const result = await supabaseRpc("prism_apply_ddl", {
|
|
83
|
+
p_version: migration.version,
|
|
84
|
+
p_name: migration.name,
|
|
85
|
+
p_sql: migration.sql,
|
|
86
|
+
});
|
|
87
|
+
// Parse the JSON result from the RPC
|
|
88
|
+
const data = (typeof result === "string" ? JSON.parse(result) : result);
|
|
89
|
+
if (data?.status === "applied") {
|
|
90
|
+
console.error(`[Prism Auto-Migration] ✅ Applied migration ${migration.version}: ${migration.name}`);
|
|
91
|
+
}
|
|
92
|
+
else if (data?.status === "already_applied") {
|
|
93
|
+
// Silent skip — expected for idempotent restarts
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
98
|
+
// PGRST202 = function not found → migration infra (027) not applied yet
|
|
99
|
+
if (errMsg.includes("PGRST202") || errMsg.includes("Could not find the function")) {
|
|
100
|
+
console.error("[Prism Auto-Migration] ⚠️ prism_apply_ddl() not found. " +
|
|
101
|
+
"Apply migration 027_auto_migration_infra.sql to enable auto-migrations.\n" +
|
|
102
|
+
" Run: supabase db push (or apply the SQL in the Supabase Dashboard SQL Editor)");
|
|
103
|
+
return; // Stop — no point trying further migrations
|
|
104
|
+
}
|
|
105
|
+
// Any other error: log and throw to surface the problem
|
|
106
|
+
console.error(`[Prism Auto-Migration] ❌ Migration ${migration.version} (${migration.name}) failed: ${errMsg}`);
|
|
107
|
+
throw err;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
package/dist/tools/index.js
CHANGED
|
@@ -26,8 +26,8 @@ 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, KNOWLEDGE_SET_RETENTION_TOOL } from "./sessionMemoryDefinitions.js";
|
|
30
|
-
export { sessionSaveLedgerHandler, sessionSaveHandoffHandler, sessionLoadContextHandler, knowledgeSearchHandler, knowledgeForgetHandler, sessionSearchMemoryHandler, backfillEmbeddingsHandler, memoryHistoryHandler, memoryCheckoutHandler, sessionSaveImageHandler, sessionViewImageHandler, sessionHealthCheckHandler, sessionForgetMemoryHandler, knowledgeSetRetentionHandler } from "./sessionMemoryHandlers.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_FORGET_MEMORY_TOOL, KNOWLEDGE_SET_RETENTION_TOOL, SESSION_SAVE_EXPERIENCE_TOOL, KNOWLEDGE_UPVOTE_TOOL, KNOWLEDGE_DOWNVOTE_TOOL } from "./sessionMemoryDefinitions.js";
|
|
30
|
+
export { sessionSaveLedgerHandler, sessionSaveHandoffHandler, sessionLoadContextHandler, knowledgeSearchHandler, knowledgeForgetHandler, sessionSearchMemoryHandler, backfillEmbeddingsHandler, memoryHistoryHandler, memoryCheckoutHandler, sessionSaveImageHandler, sessionViewImageHandler, sessionHealthCheckHandler, sessionForgetMemoryHandler, knowledgeSetRetentionHandler, sessionSaveExperienceHandler, knowledgeUpvoteHandler, knowledgeDownvoteHandler } from "./sessionMemoryHandlers.js";
|
|
31
31
|
// ── Compaction Handler (v0.4.0 — Enhancement #2) ──
|
|
32
32
|
// The compaction handler is in a separate file because it's significantly
|
|
33
33
|
// more complex than the other session memory handlers (chunked Gemini
|
|
@@ -37,7 +37,7 @@ export const SESSION_SAVE_LEDGER_TOOL = {
|
|
|
37
37
|
},
|
|
38
38
|
role: {
|
|
39
39
|
type: "string",
|
|
40
|
-
description: "
|
|
40
|
+
description: "Optional. Agent role for Hivemind scoping (e.g., 'dev', 'qa', 'pm'). Omit to let the server auto-resolve from dashboard settings.",
|
|
41
41
|
},
|
|
42
42
|
},
|
|
43
43
|
required: ["project", "conversation_id", "summary"],
|
|
@@ -90,7 +90,7 @@ export const SESSION_SAVE_HANDOFF_TOOL = {
|
|
|
90
90
|
},
|
|
91
91
|
role: {
|
|
92
92
|
type: "string",
|
|
93
|
-
description: "
|
|
93
|
+
description: "Optional. Agent role for Hivemind scoping (e.g., 'dev', 'qa', 'pm'). Omit to let the server auto-resolve from dashboard settings.",
|
|
94
94
|
},
|
|
95
95
|
},
|
|
96
96
|
required: ["project"],
|
|
@@ -119,7 +119,12 @@ export const SESSION_LOAD_CONTEXT_TOOL = {
|
|
|
119
119
|
},
|
|
120
120
|
role: {
|
|
121
121
|
type: "string",
|
|
122
|
-
description: "
|
|
122
|
+
description: "Optional. Agent role for Hivemind scoping (e.g., 'dev', 'qa', 'pm'). Omit to let the server auto-resolve from dashboard settings. When set, also injects active_team roster.",
|
|
123
|
+
},
|
|
124
|
+
// v4.0: Token Budget
|
|
125
|
+
max_tokens: {
|
|
126
|
+
type: "integer",
|
|
127
|
+
description: "Maximum token budget for context response. Uses 1 token ≈ 4 chars heuristic. When set, the response is truncated to fit within the budget. Default: unlimited.",
|
|
123
128
|
},
|
|
124
129
|
},
|
|
125
130
|
required: ["project"],
|
|
@@ -635,3 +640,106 @@ export function isKnowledgeSetRetentionArgs(args) {
|
|
|
635
640
|
"ttl_days" in args &&
|
|
636
641
|
typeof args.ttl_days === "number");
|
|
637
642
|
}
|
|
643
|
+
// ─── v4.0: Active Behavioral Memory Tools ────────────────────
|
|
644
|
+
export const SESSION_SAVE_EXPERIENCE_TOOL = {
|
|
645
|
+
name: "session_save_experience",
|
|
646
|
+
description: "Record a typed experience event. Unlike session_save_ledger (flat logs), " +
|
|
647
|
+
"this captures structured behavioral data for pattern detection.\n\n" +
|
|
648
|
+
"Event Types:\n" +
|
|
649
|
+
"- **correction**: Agent was corrected by user\n" +
|
|
650
|
+
"- **success**: Task completed successfully\n" +
|
|
651
|
+
"- **failure**: Task failed\n" +
|
|
652
|
+
"- **learning**: New knowledge acquired",
|
|
653
|
+
inputSchema: {
|
|
654
|
+
type: "object",
|
|
655
|
+
properties: {
|
|
656
|
+
project: {
|
|
657
|
+
type: "string",
|
|
658
|
+
description: "Project identifier.",
|
|
659
|
+
},
|
|
660
|
+
event_type: {
|
|
661
|
+
type: "string",
|
|
662
|
+
enum: ["correction", "success", "failure", "learning"],
|
|
663
|
+
description: "Type of behavioral event.",
|
|
664
|
+
},
|
|
665
|
+
context: {
|
|
666
|
+
type: "string",
|
|
667
|
+
description: "What the agent was doing when the event occurred.",
|
|
668
|
+
},
|
|
669
|
+
action: {
|
|
670
|
+
type: "string",
|
|
671
|
+
description: "What action was tried.",
|
|
672
|
+
},
|
|
673
|
+
outcome: {
|
|
674
|
+
type: "string",
|
|
675
|
+
description: "What happened as a result.",
|
|
676
|
+
},
|
|
677
|
+
correction: {
|
|
678
|
+
type: "string",
|
|
679
|
+
description: "What should have been done instead (for correction type).",
|
|
680
|
+
},
|
|
681
|
+
confidence_score: {
|
|
682
|
+
type: "integer",
|
|
683
|
+
minimum: 1,
|
|
684
|
+
maximum: 100,
|
|
685
|
+
description: "Agent's confidence in the outcome (1-100).",
|
|
686
|
+
},
|
|
687
|
+
role: {
|
|
688
|
+
type: "string",
|
|
689
|
+
description: "Optional. Agent role for Hivemind scoping. Omit to let the server auto-resolve from dashboard settings.",
|
|
690
|
+
},
|
|
691
|
+
},
|
|
692
|
+
required: ["project", "event_type", "context", "action", "outcome"],
|
|
693
|
+
},
|
|
694
|
+
};
|
|
695
|
+
export function isSessionSaveExperienceArgs(args) {
|
|
696
|
+
return (typeof args === "object" &&
|
|
697
|
+
args !== null &&
|
|
698
|
+
"project" in args &&
|
|
699
|
+
typeof args.project === "string" &&
|
|
700
|
+
"event_type" in args &&
|
|
701
|
+
typeof args.event_type === "string" &&
|
|
702
|
+
"context" in args &&
|
|
703
|
+
typeof args.context === "string" &&
|
|
704
|
+
"action" in args &&
|
|
705
|
+
typeof args.action === "string" &&
|
|
706
|
+
"outcome" in args &&
|
|
707
|
+
typeof args.outcome === "string");
|
|
708
|
+
}
|
|
709
|
+
export const KNOWLEDGE_UPVOTE_TOOL = {
|
|
710
|
+
name: "knowledge_upvote",
|
|
711
|
+
description: "Upvote a memory entry to increase its importance (graduation). " +
|
|
712
|
+
"Entries with importance >= 7 become 'graduated' insights that always " +
|
|
713
|
+
"surface in behavioral warnings.",
|
|
714
|
+
inputSchema: {
|
|
715
|
+
type: "object",
|
|
716
|
+
properties: {
|
|
717
|
+
id: {
|
|
718
|
+
type: "string",
|
|
719
|
+
description: "The UUID of the ledger entry to upvote.",
|
|
720
|
+
},
|
|
721
|
+
},
|
|
722
|
+
required: ["id"],
|
|
723
|
+
},
|
|
724
|
+
};
|
|
725
|
+
export const KNOWLEDGE_DOWNVOTE_TOOL = {
|
|
726
|
+
name: "knowledge_downvote",
|
|
727
|
+
description: "Downvote a memory entry to decrease its importance. " +
|
|
728
|
+
"Importance cannot go below 0.",
|
|
729
|
+
inputSchema: {
|
|
730
|
+
type: "object",
|
|
731
|
+
properties: {
|
|
732
|
+
id: {
|
|
733
|
+
type: "string",
|
|
734
|
+
description: "The UUID of the ledger entry to downvote.",
|
|
735
|
+
},
|
|
736
|
+
},
|
|
737
|
+
required: ["id"],
|
|
738
|
+
},
|
|
739
|
+
};
|
|
740
|
+
export function isKnowledgeVoteArgs(args) {
|
|
741
|
+
return (typeof args === "object" &&
|
|
742
|
+
args !== null &&
|
|
743
|
+
"id" in args &&
|
|
744
|
+
typeof args.id === "string");
|
|
745
|
+
}
|
|
@@ -33,7 +33,8 @@ import { captureLocalEnvironment } from "../utils/autoCapture.js";
|
|
|
33
33
|
import { isSessionSaveLedgerArgs, isSessionSaveHandoffArgs, isSessionLoadContextArgs, isKnowledgeSearchArgs, isKnowledgeForgetArgs, isSessionSearchMemoryArgs, isBackfillEmbeddingsArgs, isMemoryHistoryArgs, isMemoryCheckoutArgs, isSessionHealthCheckArgs, // v2.2.0: health check type guard
|
|
34
34
|
isSessionForgetMemoryArgs, // Phase 2: GDPR-compliant memory deletion type guard
|
|
35
35
|
isKnowledgeSetRetentionArgs, // v3.1: TTL retention policy type guard
|
|
36
|
-
|
|
36
|
+
// v4.0: Active Behavioral Memory type guards
|
|
37
|
+
isSessionSaveExperienceArgs, isKnowledgeVoteArgs, } from "./sessionMemoryDefinitions.js";
|
|
37
38
|
// v3.1: In-memory debounce lock for auto-compaction.
|
|
38
39
|
// Prevents multiple concurrent Gemini compaction tasks for the same project
|
|
39
40
|
// when many agents call session_save_ledger at the same time.
|
|
@@ -54,6 +55,23 @@ export async function sessionSaveLedgerHandler(args) {
|
|
|
54
55
|
}
|
|
55
56
|
const { project, conversation_id, summary, todos, files_changed, decisions, role } = args;
|
|
56
57
|
const storage = await getStorage();
|
|
58
|
+
// ─── Repo path mismatch validation (v4.2) ───
|
|
59
|
+
let repoPathWarning = "";
|
|
60
|
+
if (files_changed && files_changed.length > 0) {
|
|
61
|
+
try {
|
|
62
|
+
const configuredPath = await getSetting(`repo_path:${project}`, "");
|
|
63
|
+
if (configuredPath && configuredPath.trim()) {
|
|
64
|
+
const normalizedPath = configuredPath.trim().replace(/\\/g, "/").replace(/\/+$/, ""); // normalize + strip trailing slash
|
|
65
|
+
const mismatched = files_changed.filter((f) => !f.replace(/\\/g, "/").startsWith(normalizedPath));
|
|
66
|
+
if (mismatched.length === files_changed.length) {
|
|
67
|
+
repoPathWarning = `\n\n⚠️ Project mismatch: none of the files_changed paths match repo_path "${normalizedPath}" ` +
|
|
68
|
+
`configured for project "${project}". Consider saving under the correct project.`;
|
|
69
|
+
debugLog(`[session_save_ledger] Repo path mismatch for "${project}": expected prefix "${normalizedPath}"`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
catch { /* getSetting non-fatal */ }
|
|
74
|
+
}
|
|
57
75
|
debugLog(`[session_save_ledger] Saving ledger entry for project="${project}"`);
|
|
58
76
|
// Auto-extract keywords from summary + decisions for knowledge accumulation
|
|
59
77
|
const combinedText = [summary, ...(decisions || [])].join(" ");
|
|
@@ -129,6 +147,7 @@ export async function sessionSaveLedgerHandler(args) {
|
|
|
129
147
|
(files_changed?.length ? `Files changed: ${files_changed.length}\n` : "") +
|
|
130
148
|
(decisions?.length ? `Decisions: ${decisions.length}\n` : "") +
|
|
131
149
|
(GOOGLE_API_KEY ? `📊 Embedding generation queued for semantic search.\n` : "") +
|
|
150
|
+
repoPathWarning +
|
|
132
151
|
`\nRaw response: ${JSON.stringify(result)}`,
|
|
133
152
|
}],
|
|
134
153
|
isError: false,
|
|
@@ -359,6 +378,8 @@ export async function sessionLoadContextHandler(args) {
|
|
|
359
378
|
throw new Error("Invalid arguments for session_load_context");
|
|
360
379
|
}
|
|
361
380
|
const { project, level = "standard", role } = args;
|
|
381
|
+
const maxTokens = args.max_tokens
|
|
382
|
+
|| parseInt(await getSetting("max_tokens", "0"), 10) || undefined; // v4.0: arg > dashboard setting > none
|
|
362
383
|
const agentName = await getSetting("agent_name", "");
|
|
363
384
|
const validLevels = ["quick", "standard", "deep"];
|
|
364
385
|
if (!validLevels.includes(level)) {
|
|
@@ -533,11 +554,27 @@ export async function sessionLoadContextHandler(args) {
|
|
|
533
554
|
const skillPart = skillLoaded ? ` · 📜 \`${effectiveRole}\` skill loaded` : (effectiveRole ? " · 📜 No skill configured" : "");
|
|
534
555
|
greetingBlock = `\n\n[👤 AGENT IDENTITY]\n${namePart}${rolePart}${skillPart}`;
|
|
535
556
|
}
|
|
557
|
+
// Build the response object before v4.0 augmentations
|
|
558
|
+
let responseText = `📋 Session context for "${project}" (${level}):\n\n${formattedContext.trim()}${driftReport}${briefingBlock}${greetingBlock}${visualMemoryBlock}${skillBlock}${versionNote}`;
|
|
559
|
+
// ─── v4.0: Behavioral Warnings Injection ───────────────────
|
|
560
|
+
// If loadContext returned behavioral_warnings, add them to the
|
|
561
|
+
// formatted output so the agent sees them prominently.
|
|
562
|
+
const behavWarnings = data?.behavioral_warnings;
|
|
563
|
+
if (behavWarnings && behavWarnings.length > 0) {
|
|
564
|
+
responseText += `\n\n[⚠️ BEHAVIORAL WARNINGS]\n` +
|
|
565
|
+
behavWarnings.map(w => `- ${w.summary} (importance: ${w.importance})`).join("\n");
|
|
566
|
+
}
|
|
567
|
+
// ─── v4.0: Token Budget Truncation ─────────────────────────
|
|
568
|
+
// 1 token ≈ 4 chars heuristic. Truncate if response exceeds budget.
|
|
569
|
+
if (maxTokens && maxTokens > 0) {
|
|
570
|
+
const maxChars = maxTokens * 4;
|
|
571
|
+
if (responseText.length > maxChars) {
|
|
572
|
+
responseText = responseText.slice(0, maxChars) + "\n\n[… truncated to fit token budget]";
|
|
573
|
+
debugLog(`[session_load_context] Truncated response to ${maxTokens} tokens (${maxChars} chars)`);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
536
576
|
return {
|
|
537
|
-
content: [{
|
|
538
|
-
type: "text",
|
|
539
|
-
text: `📋 Session context for "${project}" (${level}):\n\n${formattedContext.trim()}${driftReport}${briefingBlock}${greetingBlock}${visualMemoryBlock}${skillBlock}${versionNote}`,
|
|
540
|
-
}],
|
|
577
|
+
content: [{ type: "text", text: responseText }],
|
|
541
578
|
isError: false,
|
|
542
579
|
};
|
|
543
580
|
}
|
|
@@ -1553,3 +1590,116 @@ export async function knowledgeSetRetentionHandler(args) {
|
|
|
1553
1590
|
isError: false,
|
|
1554
1591
|
};
|
|
1555
1592
|
}
|
|
1593
|
+
// ─── v4.0: Experience Save Handler ───────────────────────────
|
|
1594
|
+
/**
|
|
1595
|
+
* Records a typed experience event for behavioral pattern detection.
|
|
1596
|
+
* Unlike session_save_ledger (flat logs), this captures structured
|
|
1597
|
+
* context → action → outcome data with confidence scoring.
|
|
1598
|
+
*
|
|
1599
|
+
* Corrections start with importance = 1 to jumpstart visibility;
|
|
1600
|
+
* all other event types start at 0.
|
|
1601
|
+
*/
|
|
1602
|
+
export async function sessionSaveExperienceHandler(args) {
|
|
1603
|
+
if (!isSessionSaveExperienceArgs(args)) {
|
|
1604
|
+
throw new Error("Invalid arguments for session_save_experience");
|
|
1605
|
+
}
|
|
1606
|
+
const { project, event_type, context: ctx, action, outcome, correction, confidence_score, role } = args;
|
|
1607
|
+
const storage = await getStorage();
|
|
1608
|
+
debugLog(`[session_save_experience] Recording ${event_type} event for project="${project}"`);
|
|
1609
|
+
// Format structured summary from event fields
|
|
1610
|
+
let summary = `[${event_type.toUpperCase()}] ${ctx} → ${action} → ${outcome}`;
|
|
1611
|
+
if (event_type === "correction" && correction) {
|
|
1612
|
+
summary += ` | CORRECTION: ${correction}`;
|
|
1613
|
+
}
|
|
1614
|
+
// Auto-extract keywords from the structured summary
|
|
1615
|
+
const keywords = toKeywordArray(summary);
|
|
1616
|
+
debugLog(`[session_save_experience] Extracted ${keywords.length} keywords: ${keywords.slice(0, 5).join(", ")}...`);
|
|
1617
|
+
const effectiveRole = role || await getSetting("default_role", "global");
|
|
1618
|
+
const result = await storage.saveLedger({
|
|
1619
|
+
project,
|
|
1620
|
+
conversation_id: "experience-event",
|
|
1621
|
+
user_id: PRISM_USER_ID,
|
|
1622
|
+
role: effectiveRole,
|
|
1623
|
+
event_type,
|
|
1624
|
+
summary,
|
|
1625
|
+
decisions: [
|
|
1626
|
+
`Context: ${ctx}`,
|
|
1627
|
+
`Action: ${action}`,
|
|
1628
|
+
`Outcome: ${outcome}`,
|
|
1629
|
+
...(correction ? [`Correction: ${correction}`] : []),
|
|
1630
|
+
],
|
|
1631
|
+
keywords,
|
|
1632
|
+
confidence_score: typeof confidence_score === "number" ? confidence_score : undefined,
|
|
1633
|
+
// Corrections start with importance 1 to jumpstart visibility
|
|
1634
|
+
importance: event_type === "correction" ? 1 : 0,
|
|
1635
|
+
});
|
|
1636
|
+
// Fire-and-forget embedding generation
|
|
1637
|
+
if (GOOGLE_API_KEY && result) {
|
|
1638
|
+
const embeddingText = summary;
|
|
1639
|
+
const savedEntry = Array.isArray(result) ? result[0] : result;
|
|
1640
|
+
const entryId = savedEntry?.id;
|
|
1641
|
+
if (entryId) {
|
|
1642
|
+
generateEmbedding(embeddingText)
|
|
1643
|
+
.then(async (embedding) => {
|
|
1644
|
+
await storage.patchLedger(entryId, {
|
|
1645
|
+
embedding: JSON.stringify(embedding),
|
|
1646
|
+
});
|
|
1647
|
+
debugLog(`[session_save_experience] Embedding saved for entry ${entryId}`);
|
|
1648
|
+
})
|
|
1649
|
+
.catch((err) => {
|
|
1650
|
+
console.error(`[session_save_experience] Embedding failed (non-fatal): ${err.message}`);
|
|
1651
|
+
});
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
return {
|
|
1655
|
+
content: [{
|
|
1656
|
+
type: "text",
|
|
1657
|
+
text: `✅ Experience recorded: ${event_type} for project "${project}"\n` +
|
|
1658
|
+
`Summary: ${summary}\n` +
|
|
1659
|
+
(confidence_score !== undefined ? `Confidence: ${confidence_score}%\n` : "") +
|
|
1660
|
+
`Importance: ${event_type === "correction" ? 1 : 0} (upvote to increase)`,
|
|
1661
|
+
}],
|
|
1662
|
+
isError: false,
|
|
1663
|
+
};
|
|
1664
|
+
}
|
|
1665
|
+
// ─── v4.0: Knowledge Upvote Handler ──────────────────────────
|
|
1666
|
+
/**
|
|
1667
|
+
* Upvotes a ledger entry to increase its importance.
|
|
1668
|
+
* Entries reaching importance >= 7 are considered "graduated"
|
|
1669
|
+
* and will always surface as Behavioral Warnings.
|
|
1670
|
+
*/
|
|
1671
|
+
export async function knowledgeUpvoteHandler(args) {
|
|
1672
|
+
if (!isKnowledgeVoteArgs(args)) {
|
|
1673
|
+
throw new Error("Invalid arguments for knowledge_upvote");
|
|
1674
|
+
}
|
|
1675
|
+
const storage = await getStorage();
|
|
1676
|
+
await storage.adjustImportance(args.id, 1, PRISM_USER_ID);
|
|
1677
|
+
debugLog(`[knowledge_upvote] Upvoted entry ${args.id}`);
|
|
1678
|
+
return {
|
|
1679
|
+
content: [{
|
|
1680
|
+
type: "text",
|
|
1681
|
+
text: `👍 Entry ${args.id} upvoted (+1 importance).`,
|
|
1682
|
+
}],
|
|
1683
|
+
isError: false,
|
|
1684
|
+
};
|
|
1685
|
+
}
|
|
1686
|
+
// ─── v4.0: Knowledge Downvote Handler ────────────────────────
|
|
1687
|
+
/**
|
|
1688
|
+
* Downvotes a ledger entry to decrease its importance.
|
|
1689
|
+
* Importance is clamped at 0 (never goes negative).
|
|
1690
|
+
*/
|
|
1691
|
+
export async function knowledgeDownvoteHandler(args) {
|
|
1692
|
+
if (!isKnowledgeVoteArgs(args)) {
|
|
1693
|
+
throw new Error("Invalid arguments for knowledge_downvote");
|
|
1694
|
+
}
|
|
1695
|
+
const storage = await getStorage();
|
|
1696
|
+
await storage.adjustImportance(args.id, -1, PRISM_USER_ID);
|
|
1697
|
+
debugLog(`[knowledge_downvote] Downvoted entry ${args.id}`);
|
|
1698
|
+
return {
|
|
1699
|
+
content: [{
|
|
1700
|
+
type: "text",
|
|
1701
|
+
text: `👎 Entry ${args.id} downvoted (-1 importance).`,
|
|
1702
|
+
}],
|
|
1703
|
+
isError: false,
|
|
1704
|
+
};
|
|
1705
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "prism-mcp-server",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.2.0",
|
|
4
4
|
"mcpName": "io.github.dcostenco/prism-mcp",
|
|
5
5
|
"description": "The Mind Palace for AI Agents — local-first MCP server with persistent memory (SQLite/Supabase), visual dashboard, time travel, multi-agent sync, Morning Briefings, reality drift detection, code mode templates, semantic vector search, and Brave Search + Gemini analysis. Zero-config local mode.",
|
|
6
6
|
"module": "index.ts",
|