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.
@@ -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, created_at, session_date)
438
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
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
  }
@@ -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
+ }
@@ -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: "v3.0: Agent role for Hivemind scoping (e.g., 'dev', 'qa', 'pm'). Defaults to 'global'.",
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: "v3.0: Agent role for Hivemind scoping (e.g., 'dev', 'qa', 'pm'). Defaults to 'global'.",
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: "v3.0: Agent role for Hivemind scoping (e.g., 'dev', 'qa', 'pm'). Defaults to 'global'. When set, also injects active_team roster.",
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
- } from "./sessionMemoryDefinitions.js";
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.1.1",
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",