prism-mcp-server 8.0.0 → 8.0.2

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 CHANGED
@@ -20,7 +20,7 @@ npx -y prism-mcp-server
20
20
 
21
21
  Works with **Claude Desktop · Claude Code · Cursor · Windsurf · Cline · Gemini · Antigravity** — **any MCP client.**
22
22
 
23
- ## 📖 Table of Contents
23
+ ## Table of Contents
24
24
 
25
25
  - [Why Prism?](#why-prism)
26
26
  - [Quick Start](#quick-start)
@@ -28,8 +28,8 @@ Works with **Claude Desktop · Claude Code · Cursor · Windsurf · Cline · Gem
28
28
  - [Setup Guides](#setup-guides)
29
29
  - [Universal Import: Bring Your History](#universal-import-bring-your-history)
30
30
  - [What Makes Prism Different](#what-makes-prism-different)
31
- - [Synapse Engine (v8.0)](#-synapse-engine-v80)
32
- - [Cognitive Architecture (v7.8)](#-cognitive-architecture-v78)
31
+ - [Synapse Engine (v8.0)](#synapse-engine-v80)
32
+ - [Cognitive Architecture (v7.8)](#cognitive-architecture-v78)
33
33
  - [Data Privacy & Egress](#data-privacy--egress)
34
34
  - [Use Cases](#use-cases)
35
35
  - [What's New](#whats-new)
@@ -53,15 +53,15 @@ Every time you start a new conversation with an AI coding assistant, it starts f
53
53
 
54
54
  Prism has three pillars:
55
55
 
56
- 1. **🧠 Cognitive Memory** — Memories are ranked like a human brain: recently and frequently accessed context surfaces first, while stale context fades naturally via ACT-R activation decay. Raw experience consolidates into semantic principles through Hebbian learning. The result is retrieval quality that no flat vector search can match. *(See [Cognitive Architecture](#-cognitive-architecture-v78) and [Scientific Foundation](#-scientific-foundation).)*
56
+ 1. **🧠 Cognitive Memory** — Memories are ranked like a human brain: recently and frequently accessed context surfaces first, while stale context fades naturally via ACT-R activation decay. Raw experience consolidates into semantic principles through Hebbian learning. The result is retrieval quality that no flat vector search can match. *(See [Cognitive Architecture](#cognitive-architecture-v78) and [Scientific Foundation](#scientific-foundation).)*
57
57
 
58
- 2. **⚡ Synapse Engine (GraphRAG)** — When your agent searches for "Error X", the Synapse Engine doesn't just find logs mentioning "Error X". Multi-hop energy propagation traverses the causal graph — dampened by fan effect, bounded by lateral inhibition — and surfaces "Workaround Y" connected to "Architecture Decision Z". Nodes discovered exclusively via graph traversal are tagged `[🌐 Synapse]` so you can *see* the engine working. *(See [Synapse Engine](#-synapse-engine-v80).)*
58
+ 2. **⚡ Synapse Engine (GraphRAG)** — When your agent searches for "Error X", the Synapse Engine doesn't just find logs mentioning "Error X". Multi-hop energy propagation traverses the causal graph — dampened by fan effect, bounded by lateral inhibition — and surfaces "Workaround Y" connected to "Architecture Decision Z". Nodes discovered exclusively via graph traversal are tagged `[🌐 Synapse]` so you can *see* the engine working. *(See [Synapse Engine](#synapse-engine-v80).)*
59
59
 
60
- 3. **🏭 Autonomous Execution (Dark Factory)** — When you're ready, Prism can run coding tasks end-to-end with a fail-closed pipeline where an adversarial evaluator catches bugs the generator missed — before you ever see the PR. *(See [Dark Factory](#-dark-factory--adversarial-autonomous-pipelines).)*
60
+ 3. **🏭 Autonomous Execution (Dark Factory)** — When you're ready, Prism can run coding tasks end-to-end with a fail-closed pipeline where an adversarial evaluator catches bugs the generator missed — before you ever see the PR. *(See [Dark Factory](#dark-factory--adversarial-autonomous-pipelines).)*
61
61
 
62
62
  ---
63
63
 
64
- ## 🚀 Quick Start
64
+ ## Quick Start
65
65
 
66
66
  ### Prerequisites
67
67
 
@@ -135,7 +135,7 @@ Then open `http://localhost:3001` instead.
135
135
 
136
136
  ---
137
137
 
138
- ## The Magic Moment
138
+ ## The Magic Moment
139
139
 
140
140
  > **Session 1** (Monday evening):
141
141
  > ```
@@ -156,7 +156,7 @@ Then open `http://localhost:3001` instead.
156
156
 
157
157
  ---
158
158
 
159
- ## 📖 Setup Guides
159
+ ## Setup Guides
160
160
 
161
161
  <details>
162
162
  <summary><strong>Claude Desktop</strong></summary>
@@ -379,7 +379,7 @@ Prism can be deployed natively to cloud platforms like [Render](https://render.c
379
379
 
380
380
  ---
381
381
 
382
- ## 📥 Universal Import: Bring Your History
382
+ ## Universal Import: Bring Your History
383
383
 
384
384
  Switching to Prism? Don't leave months of AI session history behind. Prism can **ingest historical sessions from Claude Code, Gemini, and OpenAI** and give your Mind Palace an instant head start — no manual re-entry required.
385
385
 
@@ -412,7 +412,7 @@ npx -y prism-mcp-server universal-import --format gemini --path ./gemini_history
412
412
 
413
413
  ---
414
414
 
415
- ## What Makes Prism Different
415
+ ## What Makes Prism Different
416
416
 
417
417
 
418
418
  ### 🧠 Your Agent Learns From Mistakes
@@ -455,12 +455,12 @@ OpenTelemetry spans for every MCP tool call, LLM hop, and background worker. Rou
455
455
  ### 🌐 Autonomous Web Scholar
456
456
  Prism researches while you sleep. A background pipeline searches the web, scrapes articles, synthesizes findings via LLM, and injects results directly into your semantic memory — fully searchable on your next session. Brave Search → Firecrawl scrape → LLM synthesis → Prism ledger. Task-aware, Hivemind-integrated, and zero-config when API keys are missing (falls back to Yahoo + Readability).
457
457
 
458
- ### 🏭 Dark Factory — Adversarial Autonomous Pipelines
458
+ ### Dark Factory — Adversarial Autonomous Pipelines
459
459
  When you trigger a Dark Factory pipeline, Prism doesn't just run your task — it fights itself to produce high-quality output. A `PLAN_CONTRACT` step locks a machine-parseable rubric before any code is written. After execution, an **Adversarial Evaluator** (in a fully isolated context) scores the output against the rubric. It cannot pass the Generator without providing exact file and line evidence for every failing criterion. Failed evaluations inject the critique directly into the Generator's retry prompt so it's never flying blind. The result: security issues, regressions, and lazy debug logs caught autonomously — before you ever see the PR. → [See it in action](examples/adversarial-eval-demo/README.md)
460
460
 
461
461
  ---
462
462
 
463
- ## Synapse Engine (v8.0)
463
+ ## Synapse Engine (v8.0)
464
464
 
465
465
  > *Standard RAG retrieves documents. GraphRAG traverses relationships. The Synapse Engine does both — a pure, storage-agnostic multi-hop propagation engine that turns your agent's memory into an associative reasoning network.*
466
466
 
@@ -507,7 +507,7 @@ The Synapse Engine (v8.0) replaces the legacy SQL-coupled spreading activation w
507
507
 
508
508
  ---
509
509
 
510
- ## 🧠 Cognitive Architecture (v7.8)
510
+ ## Cognitive Architecture (v7.8)
511
511
 
512
512
  > *Prism v7.8 is our biggest leap forward yet. We have moved beyond flat vector search and implemented a true Cognitive Architecture inspired by human brain mechanics. With the new ACT-R Spreading Activation Engine, Episodic-to-Semantic memory consolidation, and Uncertainty-Aware Rejection Gates, Prism doesn't just store logs anymore — it forms principles, follows causal trains of thought, and possesses the self-awareness to know when it lacks information.*
513
513
 
@@ -572,7 +572,7 @@ Standard RAG (Retrieval-Augmented Generation) is now a commodity. Everyone has v
572
572
 
573
573
  ---
574
574
 
575
- ## 🔒 Data Privacy & Egress
575
+ ## Data Privacy & Egress
576
576
 
577
577
  **Where is my data stored?**
578
578
 
@@ -603,7 +603,7 @@ Prism will recreate the directory with empty databases on next startup.
603
603
 
604
604
  ---
605
605
 
606
- ## 🎯 Use Cases
606
+ ## Use Cases
607
607
 
608
608
  - **Long-running feature work** — Save state at end of day, restore full context next morning. No re-explaining.
609
609
  - **Multi-agent collaboration** — Dev, QA, and PM agents share real-time context without stepping on each other's memory.
@@ -632,7 +632,7 @@ Then continue a specific thread with a follow-up message to the selected agent,
632
632
 
633
633
  ---
634
634
 
635
- ## ⚔️ Adversarial Evaluation in Action
635
+ ## Adversarial Evaluation in Action
636
636
 
637
637
  > **Split-Brain Anti-Sycophancy** — the signature feature of v7.4.0.
638
638
 
@@ -737,12 +737,12 @@ The Generator strips the `console.log`, resubmits, and the next `EVALUATE` retur
737
737
 
738
738
  ---
739
739
 
740
- ## 🆕 What's New
740
+ ## What's New
741
741
 
742
742
  > **Current release: v8.0.0 — Synapse Engine**
743
743
 
744
744
  - ⚡ **v8.0.0 — Synapse Engine:** Pure, storage-agnostic multi-hop graph propagation engine replaces the legacy SQL-coupled spreading activation. O(T × M) bounded ACT-R energy propagation with dampened fan effect, asymmetric bidirectional flow, cyclic loop prevention, and sigmoid normalization. Full integration into both SQLite and Supabase backends. 5 new config knobs. Battle-hardened with NaN guards, config clamping, non-fatal enrichment, and 16 passing tests. **Memory search now follows the causal graph, not just keywords.** → [Synapse Engine](#synapse-engine-v80)
745
- - 🧠 **v7.8.x — Cognitive Architecture:** Episodic-to-Semantic consolidation (Hebbian learning), ACT-R Spreading Activation with multi-hop causal reasoning, Uncertainty-Aware Rejection Gate, and Dynamic Fast Weight Decay. Validated by **LoCoMo-Plus benchmark**. → [Cognitive Architecture](#-cognitive-architecture-v78)
745
+ - 🧠 **v7.8.x — Cognitive Architecture:** Episodic-to-Semantic consolidation (Hebbian learning), ACT-R Spreading Activation with multi-hop causal reasoning, Uncertainty-Aware Rejection Gate, and Dynamic Fast Weight Decay. Validated by **LoCoMo-Plus benchmark**. → [Cognitive Architecture](#cognitive-architecture-v78)
746
746
  - 🌐 **v7.7.0 — Cloud-Native SSE Transport:** Full unauthenticated and authenticated Server-Sent Events MCP support for seamless network deployments.
747
747
  - 🩺 **v7.5.0 — Intent Health Dashboard + Security Hardening:** Real-time 0–100 project health scoring (staleness × TODO load × decisions). 10 XSS injection vectors patched. Algorithm hardened with NaN guards and score ceiling.
748
748
  - ⚔️ **v7.4.0 — Adversarial Evaluation:** Split-brain anti-sycophancy pipeline. Generator and evaluator in isolated roles with evidence-bound findings.
@@ -752,7 +752,7 @@ The Generator strips the `console.log`, resubmits, and the next `EVALUATE` retur
752
752
 
753
753
  ---
754
754
 
755
- ## ⚔️ How Prism Compares
755
+ ## How Prism Compares
756
756
 
757
757
  Standard memory servers (like Mem0, Zep, or the baseline Anthropic MCP) act as passive filing cabinets — they wait for the LLM to search them. **Prism is an active cognitive architecture.** Designed specifically for the **Model Context Protocol (MCP)**, Prism doesn't just store vectors — it consolidates experience into principles, traverses causal graphs for multi-hop reasoning, and rejects queries it can't confidently answer.
758
758
 
@@ -804,7 +804,7 @@ Every other AI coding pipeline has a fatal flaw: it asks the same model that wro
804
804
 
805
805
  ---
806
806
 
807
- ## 🔧 Tool Reference
807
+ ## Tool Reference
808
808
 
809
809
  Prism ships 30+ tools, but **90% of your workflow uses just three:**
810
810
 
@@ -1066,7 +1066,7 @@ Prism is a **stdio-based MCP server** that manages persistent agent memory. Here
1066
1066
 
1067
1067
  ### Auto-Load Architecture
1068
1068
 
1069
- Each MCP client has its own mechanism for ensuring Prism context loads on session start. See the platform-specific [Setup Guides](#-setup-guides) above for detailed instructions:
1069
+ Each MCP client has its own mechanism for ensuring Prism context loads on session start. See the platform-specific [Setup Guides](#setup-guides) above for detailed instructions:
1070
1070
 
1071
1071
  - **Claude Code** — Lifecycle hooks (`SessionStart` / `Stop`)
1072
1072
  - **Gemini / Antigravity** — Three-layer architecture (User Rules + AGENTS.md + Startup Skill)
@@ -1077,7 +1077,7 @@ All platforms benefit from the **server-side fallback** (v5.2.1): if `session_lo
1077
1077
 
1078
1078
  ---
1079
1079
 
1080
- ## 🧬 Scientific Foundation
1080
+ ## Scientific Foundation
1081
1081
 
1082
1082
  Prism has evolved from smart session logging into a **cognitive memory architecture** — grounded in real research, not marketing. Every retrieval decision is backed by peer-reviewed models from cognitive psychology, neuroscience, and distributed computing.
1083
1083
 
@@ -1120,7 +1120,7 @@ Prism has evolved from smart session logging into a **cognitive memory architect
1120
1120
 
1121
1121
  ---
1122
1122
 
1123
- ## 📦 Milestones & Roadmap
1123
+ ## Milestones & Roadmap
1124
1124
 
1125
1125
  > **Current: v8.0.0** — Synapse Engine ([CHANGELOG](CHANGELOG.md))
1126
1126
 
@@ -1145,7 +1145,7 @@ Prism has evolved from smart session logging into a **cognitive memory architect
1145
1145
  👉 **[Full ROADMAP.md →](ROADMAP.md)**
1146
1146
 
1147
1147
 
1148
- ## Troubleshooting FAQ
1148
+ ## Troubleshooting FAQ
1149
1149
 
1150
1150
  **Q: Why is the dashboard project selector stuck on "Loading projects..."?**
1151
1151
  A: Fixed in v7.3.3. The root cause was a multi-layer quote-escaping trap in the `abortPipeline` onclick handler that generated a `SyntaxError` in the browser, silently killing the entire dashboard IIFE. Update to v7.3.3+ (`npx -y prism-mcp-server`). If still stuck, check that Supabase env values are properly set (unresolved placeholders like `${SUPABASE_URL}` cause `/api/projects` to return empty). Prism auto-falls back to local SQLite when Supabase is misconfigured.
@@ -599,6 +599,13 @@ async function runnerTick() {
599
599
  const harnessPath = path.join(path.resolve(spec.workingDirectory), 'verification_harness.json');
600
600
  if (fs.existsSync(harnessPath)) {
601
601
  try {
602
+ // MED-5 FIX: Guard against LLM-generated files that are malformed or
603
+ // excessively large. Cap at 1MB to prevent heap exhaustion.
604
+ const MAX_HARNESS_SIZE = 1_000_000;
605
+ const stat = fs.statSync(harnessPath);
606
+ if (stat.size > MAX_HARNESS_SIZE) {
607
+ throw new Error(`Verification harness too large (${stat.size} bytes, max ${MAX_HARNESS_SIZE}). Possible corrupt LLM output.`);
608
+ }
602
609
  const rawHarness = fs.readFileSync(harnessPath, 'utf8');
603
610
  const harnessData = JSON.parse(rawHarness);
604
611
  // GAP-5 fix: Persist the harness so CLI drift detection works for DarkFactory runs
@@ -45,8 +45,16 @@ export class SafetyController {
45
45
  if (!spec.workingDirectory)
46
46
  return true;
47
47
  // Resolve symlinks and protect against ../ escapes.
48
- const resolvedTarget = path.resolve(targetPath);
49
- const resolvedWorkspace = path.resolve(spec.workingDirectory);
48
+ let resolvedTarget = path.resolve(targetPath);
49
+ let resolvedWorkspace = path.resolve(spec.workingDirectory);
50
+ // EDGE-2 FIX: macOS (HFS+/APFS) and Windows (NTFS) use case-insensitive
51
+ // filesystems by default. Without case normalization, "/App/Workspace"
52
+ // and "/app/workspace" are treated as different paths — allowing a scope
53
+ // escape via case mismatch.
54
+ if (process.platform === 'darwin' || process.platform === 'win32') {
55
+ resolvedTarget = resolvedTarget.toLowerCase();
56
+ resolvedWorkspace = resolvedWorkspace.toLowerCase();
57
+ }
50
58
  // Path Traversal Guard: A naive startsWith() check is vulnerable to
51
59
  // prefix collisions — e.g. /app/workspace-hacked passes startsWith('/app/workspace').
52
60
  // We require EITHER exact match OR the target starts with workspace + path separator.
@@ -5,6 +5,12 @@ const SDM_M = 10000;
5
5
  export const D_ADDR_UINT32 = PRISM_DEFAULT_CONFIG.d / 32;
6
6
  // Bump this whenever the PRNG algorithm changes, to invalidate stale persisted state.
7
7
  export const SDM_ADDRESS_VERSION = 2;
8
+ // EDGE-5 FIX: Separate version constant for the HDC concept dictionary.
9
+ // SDM_ADDRESS_VERSION tracks the PRNG algorithm for hard-location addresses.
10
+ // HDC_DICTIONARY_VERSION tracks the binary encoding format for concept vectors.
11
+ // Using the same constant creates false coupling — a PRNG change doesn't
12
+ // invalidate the concept dictionary, and vice versa.
13
+ export const HDC_DICTIONARY_VERSION = 1;
8
14
  // The hard threshold boundary applied to counters during HDC writes
9
15
  // to retain memory plasticity over long periods.
10
16
  const COUNTER_CLIP = 20;
@@ -271,17 +277,35 @@ export class SparseDistributedMemory {
271
277
  }
272
278
  return state;
273
279
  }
280
+ /** Returns the current mode lock of this engine instance. */
281
+ getMode() {
282
+ return this._mode;
283
+ }
274
284
  /**
275
285
  * Import a previously serialized 1D Float32Array matrix back into
276
286
  * the 2D counters array.
287
+ *
288
+ * IMPORTANT: Uses slice() (not subarray()) to create independent copies
289
+ * of each counter row. subarray() creates aliased views over the same
290
+ * ArrayBuffer — if the source buffer is GC'd or detached, all counters
291
+ * would silently point to invalid memory.
292
+ *
293
+ * @param state - 1D Float32Array of length SDM_M * D
294
+ * @param mode - The mode this state was exported from. If provided,
295
+ * locks the engine to this mode to prevent HDC/semantic
296
+ * cross-talk on deserialization. (Default: preserve current)
277
297
  */
278
- importState(state) {
298
+ importState(state, mode) {
279
299
  if (state.length !== SDM_M * PRISM_DEFAULT_CONFIG.d) {
280
300
  throw new Error(`Invalid SDM state size: expected ${SDM_M * PRISM_DEFAULT_CONFIG.d}, got ${state.length}`);
281
301
  }
282
302
  for (let i = 0; i < SDM_M; i++) {
283
- // Subarray creates a fast view over the underlying buffer
284
- this.counters[i] = state.subarray(i * PRISM_DEFAULT_CONFIG.d, (i + 1) * PRISM_DEFAULT_CONFIG.d);
303
+ // slice() creates an independent copy safe against source buffer detachment
304
+ this.counters[i] = state.slice(i * PRISM_DEFAULT_CONFIG.d, (i + 1) * PRISM_DEFAULT_CONFIG.d);
305
+ }
306
+ // Restore the mode lock if persisted alongside the counter state
307
+ if (mode) {
308
+ this._mode = mode;
285
309
  }
286
310
  }
287
311
  }
@@ -1411,19 +1411,22 @@ export class SqliteStorage {
1411
1411
  conditions.push("role = ?");
1412
1412
  args.push(params.role);
1413
1413
  }
1414
+ // Escape LIKE wildcards (% and _) in user input to prevent pattern injection.
1415
+ // Without this, a search for "100%_done" would match unintended rows.
1416
+ const escapeLike = (s) => s.replace(/[%_]/g, '\\$&');
1414
1417
  // Add LIKE conditions for each keyword
1415
1418
  for (const kw of params.keywords) {
1416
1419
  if (kw.length > 2) {
1417
- conditions.push("(summary LIKE ? OR keywords LIKE ? OR decisions LIKE ?)");
1418
- const pattern = `%${kw}%`;
1420
+ conditions.push("(summary LIKE ? ESCAPE '\\' OR keywords LIKE ? ESCAPE '\\' OR decisions LIKE ? ESCAPE '\\')");
1421
+ const pattern = `%${escapeLike(kw)}%`;
1419
1422
  args.push(pattern, pattern, pattern);
1420
1423
  }
1421
1424
  }
1422
1425
  // BUG FIX: queryText was previously ignored — if keywords were empty,
1423
1426
  // zero search filters were added, returning unfiltered top-N results.
1424
1427
  if (params.queryText) {
1425
- conditions.push("(summary LIKE ? OR keywords LIKE ? OR decisions LIKE ?)");
1426
- const pattern = `%${params.queryText}%`;
1428
+ conditions.push("(summary LIKE ? ESCAPE '\\' OR keywords LIKE ? ESCAPE '\\' OR decisions LIKE ? ESCAPE '\\')");
1429
+ const pattern = `%${escapeLike(params.queryText)}%`;
1427
1430
  args.push(pattern, pattern, pattern);
1428
1431
  }
1429
1432
  args.push(params.limit);
@@ -1564,12 +1567,16 @@ export class SqliteStorage {
1564
1567
  fallbackConditions.push("role = ?");
1565
1568
  fallbackArgs.push(params.role);
1566
1569
  }
1570
+ // PERF: LIMIT 5000 prevents unbounded heap usage on large datasets.
1571
+ // Tier-2 scores all rows JS-side, so we cap to a safe ceiling.
1572
+ const TIER2_MAX_CANDIDATES = 5000;
1567
1573
  const fallbackSql = `
1568
1574
  SELECT id, project, summary, decisions, files_changed,
1569
1575
  session_date, created_at, is_rollup, importance, last_accessed_at,
1570
1576
  embedding_compressed, embedding_turbo_radius
1571
1577
  FROM session_ledger
1572
1578
  WHERE ${fallbackConditions.join(" AND ")}
1579
+ LIMIT ${TIER2_MAX_CANDIDATES}
1573
1580
  `;
1574
1581
  const fallbackResult = await this.db.execute({ sql: fallbackSql, args: fallbackArgs });
1575
1582
  // Score each entry using asymmetric cosine similarity
@@ -1642,7 +1649,7 @@ export class SqliteStorage {
1642
1649
  if (missingIds.length > 0) {
1643
1650
  const placeholders = missingIds.map(() => '?').join(',');
1644
1651
  const missingQuery = `
1645
- SELECT id, project, summary, session_date, decisions, files_changed
1652
+ SELECT id, project, summary, session_date, decisions, files_changed, keywords, is_rollup, importance, last_accessed_at
1646
1653
  FROM session_ledger
1647
1654
  WHERE id IN (${placeholders}) AND deleted_at IS NULL AND user_id = ?
1648
1655
  `;
@@ -1655,6 +1662,9 @@ export class SqliteStorage {
1655
1662
  session_date: row.session_date,
1656
1663
  decisions: this.parseJsonColumn(row.decisions),
1657
1664
  files_changed: this.parseJsonColumn(row.files_changed),
1665
+ is_rollup: Boolean(row.is_rollup),
1666
+ importance: Number(row.importance) || 0,
1667
+ last_accessed_at: row.last_accessed_at || null,
1658
1668
  similarity: 0.0,
1659
1669
  });
1660
1670
  }
@@ -1666,6 +1676,7 @@ export class SqliteStorage {
1666
1676
  const normEnergy = normalizeActivationEnergy(r.activationEnergy);
1667
1677
  node.activationScore = normEnergy;
1668
1678
  node.rawActivationEnergy = r.activationEnergy;
1679
+ node.isDiscovered = r.isDiscovered;
1669
1680
  // Hybrid blend: 70% original match relevance, 30% structural energy
1670
1681
  node.hybridScore = (node.similarity * 0.7) + (normEnergy * 0.3);
1671
1682
  finalResults.push(node);
@@ -2085,13 +2096,16 @@ export class SqliteStorage {
2085
2096
  const setClauses = ["status = ?"];
2086
2097
  const args = [status];
2087
2098
  // Allow watchdog to set arbitrary safe fields (e.g., loop_count reset)
2099
+ // SECURITY: Hardcoded allowlist + regex validation prevents SQL injection
2100
+ // even if new keys are added without review.
2088
2101
  const ALLOWED_FIELDS = new Set([
2089
2102
  "loop_count", "task_start_time", "expected_duration_minutes",
2090
2103
  "task_hash", "current_task",
2091
2104
  ]);
2105
+ const SAFE_COLUMN_RE = /^[a-z_]+$/;
2092
2106
  if (additionalFields) {
2093
2107
  for (const [key, val] of Object.entries(additionalFields)) {
2094
- if (ALLOWED_FIELDS.has(key)) {
2108
+ if (ALLOWED_FIELDS.has(key) && SAFE_COLUMN_RE.test(key)) {
2095
2109
  setClauses.push(`${key} = ?`);
2096
2110
  args.push(val);
2097
2111
  }
@@ -2505,16 +2519,16 @@ export class SqliteStorage {
2505
2519
  // The vector is a Uint32Array. We need its underlying buffer for SQLite.
2506
2520
  // Wrap in Uint8Array to satisfy @libsql/client InValue typing which rejects SharedArrayBuffer
2507
2521
  const buffer = new Uint8Array(vector.buffer, vector.byteOffset, vector.byteLength);
2508
- const { SDM_ADDRESS_VERSION } = await import('../sdm/sdmEngine.js');
2522
+ const { HDC_DICTIONARY_VERSION } = await import('../sdm/sdmEngine.js');
2509
2523
  await this.db.execute({
2510
2524
  sql: `INSERT INTO hdc_dictionary (concept_name, vector, prng_version)
2511
2525
  VALUES (?, ?, ?)
2512
2526
  ON CONFLICT(concept_name) DO UPDATE SET
2513
2527
  vector = excluded.vector,
2514
2528
  prng_version = excluded.prng_version`,
2515
- args: [concept, buffer, SDM_ADDRESS_VERSION],
2529
+ args: [concept, buffer, HDC_DICTIONARY_VERSION],
2516
2530
  });
2517
- debugLog(`[SqliteStorage] Persisted HDC orthogonal concept v${SDM_ADDRESS_VERSION} to dictionary: ${concept}`);
2531
+ debugLog(`[SqliteStorage] Persisted HDC concept v${HDC_DICTIONARY_VERSION} to dictionary: ${concept}`);
2518
2532
  }
2519
2533
  // ─── v6.1: Storage Hygiene ────────────────────────────────────────────
2520
2534
  /**
@@ -2743,11 +2757,29 @@ export class SqliteStorage {
2743
2757
  args: [sourceId, targetId, linkType],
2744
2758
  });
2745
2759
  }
2746
- async decayLinks(olderThanDays) {
2760
+ async decayLinks(olderThanDays, userId) {
2747
2761
  // Reduce strength by -0.05 for non-structural associative links not traversed in N days.
2748
2762
  // Floor at 0.0 (enforced by CHECK constraint) — links at 0.0 are
2749
2763
  // effectively dead but preserved for provenance audit.
2750
2764
  // We only decay related_to heuristical links, not factual structural links.
2765
+ //
2766
+ // TENANT ISOLATION: When userId is provided, scope decay to links
2767
+ // owned by this user's ledger entries (prevents cross-tenant decay
2768
+ // in shared-database deployments).
2769
+ if (userId) {
2770
+ const result = await this.db.execute({
2771
+ sql: `UPDATE memory_links
2772
+ SET strength = MAX(strength - 0.05, 0.0)
2773
+ WHERE last_traversed_at < datetime('now', ?)
2774
+ AND link_type IN ('related_to')
2775
+ AND source_id IN (
2776
+ SELECT id FROM session_ledger WHERE user_id = ?
2777
+ )`,
2778
+ args: [`-${olderThanDays} days`, userId],
2779
+ });
2780
+ return result.rowsAffected;
2781
+ }
2782
+ // Fallback: unscoped decay (backward-compatible for single-tenant)
2751
2783
  const result = await this.db.execute({
2752
2784
  sql: `UPDATE memory_links
2753
2785
  SET strength = MAX(strength - 0.05, 0.0)
@@ -374,7 +374,7 @@ export class SupabaseStorage {
374
374
  id: `in.(${missingIds.join(",")})`,
375
375
  user_id: `eq.${userId}`,
376
376
  deleted_at: "is.null",
377
- select: "id,project,summary,session_date,decisions,files_changed",
377
+ select: "id,project,summary,session_date,decisions,files_changed,is_rollup,importance,last_accessed_at",
378
378
  });
379
379
  for (const row of (Array.isArray(rows) ? rows : [])) {
380
380
  fullNodeMap.set(row.id, {
@@ -384,6 +384,9 @@ export class SupabaseStorage {
384
384
  session_date: row.session_date,
385
385
  decisions: Array.isArray(row.decisions) ? row.decisions : [],
386
386
  files_changed: Array.isArray(row.files_changed) ? row.files_changed : [],
387
+ is_rollup: Boolean(row.is_rollup),
388
+ importance: Number(row.importance) || 0,
389
+ last_accessed_at: row.last_accessed_at || null,
387
390
  similarity: 0.0,
388
391
  });
389
392
  }
@@ -400,6 +403,7 @@ export class SupabaseStorage {
400
403
  const normEnergy = normalizeActivationEnergy(r.activationEnergy);
401
404
  node.activationScore = normEnergy;
402
405
  node.rawActivationEnergy = r.activationEnergy;
406
+ node.isDiscovered = r.isDiscovered;
403
407
  // Hybrid blend: 70% original match relevance, 30% structural energy
404
408
  node.hybridScore = (node.similarity * 0.7) + (normEnergy * 0.3);
405
409
  finalResults.push(node);
@@ -968,13 +972,13 @@ export class SupabaseStorage {
968
972
  }
969
973
  async saveHdcConcept(concept, vector) {
970
974
  const base64Vector = this.uint32ToBase64(vector);
971
- const { SDM_ADDRESS_VERSION } = await import('../sdm/sdmEngine.js');
975
+ const { HDC_DICTIONARY_VERSION } = await import('../sdm/sdmEngine.js');
972
976
  await supabasePost("hdc_dictionary", {
973
977
  concept_name: concept,
974
978
  vector: base64Vector,
975
- prng_version: SDM_ADDRESS_VERSION,
979
+ prng_version: HDC_DICTIONARY_VERSION,
976
980
  }, { on_conflict: "concept_name" }, { Prefer: "return=minimal,resolution=merge-duplicates" });
977
- debugLog(`[SupabaseStorage] Persisted HDC concept v${SDM_ADDRESS_VERSION} to dictionary: ${concept}`);
981
+ debugLog(`[SupabaseStorage] Persisted HDC concept v${HDC_DICTIONARY_VERSION} to dictionary: ${concept}`);
978
982
  }
979
983
  // ─── v6.1: Storage Hygiene ────────────────────────────────────
980
984
  async vacuumDatabase(_opts) {
@@ -1152,7 +1156,7 @@ export class SupabaseStorage {
1152
1156
  return;
1153
1157
  }
1154
1158
  }
1155
- async decayLinks(olderThanDays) {
1159
+ async decayLinks(olderThanDays, _userId) {
1156
1160
  try {
1157
1161
  const affected = await supabaseRpc("prism_decay_links", {
1158
1162
  p_older_than_days: olderThanDays
@@ -146,67 +146,9 @@ export async function knowledgeSearchHandler(args) {
146
146
  });
147
147
  contentBlocks.push(traceToContentBlock(trace));
148
148
  }
149
- // ── v6.0 Phase 3: 1-Hop Graph Expansion ──────────────────
150
- // Same pattern as sessionSearchMemoryHandler:
151
- // Traverse outbound links from direct hits to find associated memories.
152
- // Graph-expanded results are BONUS — don't consume limit slots.
153
- try {
154
- // Extract IDs from the knowledge search results
155
- const directIds = new Set();
156
- if (data.results && Array.isArray(data.results)) {
157
- for (const entry of data.results) {
158
- if (entry?.id)
159
- directIds.add(entry.id);
160
- }
161
- }
162
- if (directIds.size > 0) {
163
- const enrichedIds = new Set();
164
- const maxGraphResults = Math.min(limit, 10);
165
- for (const directId of directIds) {
166
- if (enrichedIds.size >= maxGraphResults)
167
- break;
168
- const links = await storage.getLinksFrom(directId, PRISM_USER_ID, 0.3, 5);
169
- for (const link of links) {
170
- if (!directIds.has(link.target_id) && !enrichedIds.has(link.target_id)) {
171
- enrichedIds.add(link.target_id);
172
- if (enrichedIds.size >= maxGraphResults)
173
- break;
174
- }
175
- }
176
- }
177
- if (enrichedIds.size > 0) {
178
- const enrichedEntries = await storage.getLedgerEntries({
179
- user_id: `eq.${PRISM_USER_ID}`,
180
- ids: [...enrichedIds],
181
- select: "id,summary,project,created_at",
182
- });
183
- if (enrichedEntries.length > 0) {
184
- const graphFormatted = enrichedEntries.map((e) => `[🔗] ${e.created_at?.split("T")[0] || "unknown"} — ${e.project || "unknown"}\n` +
185
- ` Summary: ${e.summary}`).join("\n");
186
- contentBlocks[0] = {
187
- type: "text",
188
- text: contentBlocks[0].text +
189
- `\n\n🔗 Graph-connected memories (${enrichedEntries.length} via 1-hop expansion):\n\n${graphFormatted}`,
190
- };
191
- // Fire-and-forget: reinforce traversed links
192
- for (const directId of directIds) {
193
- storage.getLinksFrom(directId, PRISM_USER_ID, 0.3, 5)
194
- .then(links => {
195
- for (const link of links) {
196
- if (enrichedIds.has(link.target_id)) {
197
- storage.reinforceLink(directId, link.target_id, link.link_type).catch(() => { });
198
- }
199
- }
200
- })
201
- .catch(() => { });
202
- }
203
- }
204
- }
205
- }
206
- }
207
- catch (graphErr) {
208
- debugLog(`[knowledge_search] Graph expansion failed (non-fatal): ${graphErr instanceof Error ? graphErr.message : String(graphErr)}`);
209
- }
149
+ // v8.0: Legacy v6.0 1-Hop Graph Expansion has been removed.
150
+ // The Synapse Engine now handles multi-hop traversal at the storage layer,
151
+ // integrating discovered nodes into the main ranked result array.
210
152
  return { content: contentBlocks, isError: false };
211
153
  }
212
154
  export async function knowledgeForgetHandler(args) {
@@ -437,9 +379,12 @@ export async function sessionSearchMemoryHandler(args) {
437
379
  // They should decay 50% slower than raw episodic chatter to retain long-term context.
438
380
  const decayRate = r.is_rollup ? PRISM_ACTR_DECAY * 0.5 : PRISM_ACTR_DECAY;
439
381
  const Bi = baseLevelActivation(timestamps, now, decayRate);
440
- // S_i: Structural activation energy from Synapse logic
382
+ // S_i: Normalized structural activation energy from Synapse logic
441
383
  // (computed during applySynapse in storage layer)
442
- const Si = (typeof r.rawActivationEnergy === 'number') ? r.rawActivationEnergy : 0;
384
+ // v8.0: Use normalized 0-1 activationScore, NOT unbounded rawActivationEnergy.
385
+ // Raw energy can reach 15+ for hub nodes, which saturates the sigmoid
386
+ // and erases Bi (recency/frequency) from the composite score.
387
+ const Si = (typeof r.activationScore === 'number') ? r.activationScore : 0;
443
388
  // Composite retrieval score
444
389
  const composite = compositeRetrievalScore(typeof r.similarity === "number" ? r.similarity : 0, Bi + Si, PRISM_ACTR_WEIGHT_SIMILARITY, PRISM_ACTR_WEIGHT_ACTIVATION, PRISM_ACTR_SIGMOID_MIDPOINT, PRISM_ACTR_SIGMOID_STEEPNESS);
445
390
  // Attach to result for re-sorting and display
@@ -510,7 +455,9 @@ export async function sessionSearchMemoryHandler(args) {
510
455
  const actrStr = r._actr_composite !== undefined
511
456
  ? ` ACT-R: composite=${r._actr_composite.toFixed(3)} (B=${r._actr_Bi?.toFixed(2)}, S=${r._actr_Si?.toFixed(3)})\n`
512
457
  : "";
513
- return `[${i + 1}] ${simScore} similar ${r.session_date || "unknown date"}\n` +
458
+ // v8.0: Tag nodes discovered via Synapse multi-hop traversal
459
+ const synapseTag = r.isDiscovered ? " [🌐 Synapse]" : "";
460
+ return `[${i + 1}] ${simScore} similar${synapseTag} — ${r.session_date || "unknown date"}\n` +
514
461
  ` Project: ${r.project}\n` +
515
462
  ` Summary: ${r.summary}\n` +
516
463
  importanceStr +
@@ -551,62 +498,9 @@ export async function sessionSearchMemoryHandler(args) {
551
498
  });
552
499
  contentBlocks.push(traceToContentBlock(trace));
553
500
  }
554
- // ── v6.0 Phase 3: 1-Hop Graph Expansion ──────────────────
555
- // After direct hits, traverse outbound links from each result to
556
- // find associated memories. Graph-expanded results are BONUS they
557
- // don't consume limit slots. Hard-capped at `limit` additional results
558
- // to protect LLM context windows.
559
- //
560
- // Fire-and-forget: errors degrade gracefully to just direct hits.
561
- try {
562
- const directIds = new Set(results.map((r) => r.id).filter(Boolean));
563
- const enrichedIds = new Set();
564
- const maxGraphResults = Math.min(limit, 10); // Hard cap
565
- for (const directId of directIds) {
566
- if (enrichedIds.size >= maxGraphResults)
567
- break;
568
- const links = await storage.getLinksFrom(directId, PRISM_USER_ID, 0.3, 5);
569
- for (const link of links) {
570
- if (!directIds.has(link.target_id) && !enrichedIds.has(link.target_id)) {
571
- enrichedIds.add(link.target_id);
572
- if (enrichedIds.size >= maxGraphResults)
573
- break;
574
- }
575
- }
576
- }
577
- if (enrichedIds.size > 0) {
578
- // Fetch the actual entries for enriched IDs
579
- const enrichedEntries = await storage.getLedgerEntries({
580
- user_id: `eq.${PRISM_USER_ID}`,
581
- ids: [...enrichedIds],
582
- select: "id,summary,project,created_at",
583
- });
584
- if (enrichedEntries.length > 0) {
585
- const graphFormatted = enrichedEntries.map((e) => `[🔗] ${e.created_at?.split("T")[0] || "unknown"} — ${e.project || "unknown"}\n` +
586
- ` Summary: ${e.summary}`).join("\n");
587
- contentBlocks[0] = {
588
- type: "text",
589
- text: contentBlocks[0].text +
590
- `\n\n🔗 Graph-connected memories (${enrichedEntries.length} via 1-hop expansion):\n\n${graphFormatted}`,
591
- };
592
- // Fire-and-forget: reinforce traversed links
593
- for (const directId of directIds) {
594
- storage.getLinksFrom(directId, PRISM_USER_ID, 0.3, 5)
595
- .then(links => {
596
- for (const link of links) {
597
- if (enrichedIds.has(link.target_id)) {
598
- storage.reinforceLink(directId, link.target_id, link.link_type).catch(() => { });
599
- }
600
- }
601
- })
602
- .catch(() => { });
603
- }
604
- }
605
- }
606
- }
607
- catch (graphErr) {
608
- debugLog(`[session_search_memory] Graph expansion failed (non-fatal): ${graphErr instanceof Error ? graphErr.message : String(graphErr)}`);
609
- }
501
+ // v8.0: Legacy v6.0 1-Hop Graph Expansion has been removed.
502
+ // The Synapse Engine now handles multi-hop traversal at the storage layer,
503
+ // integrating discovered nodes into the main ranked result array.
610
504
  return { content: contentBlocks, isError: false };
611
505
  }
612
506
  catch (err) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prism-mcp-server",
3
- "version": "8.0.0",
3
+ "version": "8.0.2",
4
4
  "mcpName": "io.github.dcostenco/prism-mcp",
5
5
  "description": "The Mind Palace for AI Agents — a true Cognitive Architecture with Hebbian learning (episodic→semantic consolidation), ACT-R spreading activation (multi-hop causal reasoning), uncertainty-aware rejection gates (agents that know when they don't know), adversarial evaluation (anti-sycophancy), fail-closed Dark Factory pipelines, persistent memory (SQLite/Supabase), multi-agent Hivemind, time travel & visual dashboard. Zero-config local mode.",
6
6
  "module": "index.ts",
@@ -1,107 +0,0 @@
1
- /**
2
- * Apply ACT-R inspired Spreading Activation, Lateral Inhibition, and Fan Effect.
3
- * It traverses the `memory_links` table over T iterations starting from the given anchors.
4
- */
5
- export async function applySpreadingActivation(db, anchors, options, userId) {
6
- if (!options.enabled || anchors.length === 0)
7
- return anchors;
8
- const T = options.iterations ?? 3;
9
- const S = options.spreadFactor ?? 0.8;
10
- const softM = 20; // Soft lateral inhibition during propagation
11
- const finalM = options.lateralInhibition ?? 7; // Final hard lateral inhibition
12
- // State: current activation score for nodes.
13
- let activeNodes = new Map();
14
- for (const anchor of anchors) {
15
- activeNodes.set(anchor.id, anchor.similarity || 1.0);
16
- }
17
- for (let t = 0; t < T; t++) {
18
- const nextNodes = new Map();
19
- // Preserve existing activation: a_i^(t+1) = a_i^(t) + incoming
20
- for (const [id, score] of activeNodes.entries()) {
21
- nextNodes.set(id, score);
22
- }
23
- const currentIds = Array.from(activeNodes.keys());
24
- if (currentIds.length === 0)
25
- break;
26
- const placeholders = currentIds.map(() => '?').join(',');
27
- // Fetch edges connected to active nodes with LIMIT to prevent explosion on hub nodes.
28
- const edgeQuery = `
29
- SELECT source_id, target_id, strength
30
- FROM memory_links
31
- WHERE source_id IN (${placeholders}) OR target_id IN (${placeholders})
32
- LIMIT 200
33
- `;
34
- const edgeArgs = [...currentIds, ...currentIds];
35
- const edgeRes = await db.execute({ sql: edgeQuery, args: edgeArgs });
36
- // Compute out-degree (Fan Effect) directly from fetched edge rows
37
- // instead of a separate SQL round-trip — halves query count per iteration.
38
- const fanMap = new Map();
39
- for (const row of edgeRes.rows) {
40
- const src = row.source_id;
41
- if (activeNodes.has(src)) {
42
- fanMap.set(src, (fanMap.get(src) || 0) + 1);
43
- }
44
- }
45
- for (const row of edgeRes.rows) {
46
- const source = row.source_id;
47
- const target = row.target_id;
48
- const strength = Number(row.strength);
49
- // Forward flow: Source is active, flows to Target
50
- if (activeNodes.has(source)) {
51
- const fan = fanMap.get(source) || 1;
52
- // Dampened fan effect: instead of strict 1/fan, we use 1 / ln(fan + e)
53
- const dampedFan = Math.log(fan + Math.E);
54
- const flow = S * (strength * activeNodes.get(source) / dampedFan);
55
- nextNodes.set(target, (nextNodes.get(target) || 0) + flow);
56
- }
57
- // Backward flow: Target is active, flows backward to Source with a heavier penalty
58
- if (activeNodes.has(target)) {
59
- const flow = (S * 0.5) * (strength * activeNodes.get(target));
60
- nextNodes.set(source, (nextNodes.get(source) || 0) + flow);
61
- }
62
- }
63
- // Soft lateral inhibition: Keep only top softM candidates to prevent explosion
64
- const sorted = Array.from(nextNodes.entries()).sort((a, b) => b[1] - a[1]);
65
- activeNodes = new Map(sorted.slice(0, softM));
66
- }
67
- // Final evaluation
68
- const finalIds = Array.from(activeNodes.keys()).slice(0, finalM);
69
- const anchorMap = new Map();
70
- for (const a of anchors)
71
- anchorMap.set(a.id, a);
72
- const finalResults = [];
73
- const missingIds = finalIds.filter(id => !anchorMap.has(id));
74
- if (missingIds.length > 0) {
75
- const placeholders = missingIds.map(() => '?').join(',');
76
- const missingQuery = `
77
- SELECT id, project, summary, session_date, decisions, files_changed
78
- FROM session_ledger
79
- WHERE id IN (${placeholders}) AND deleted_at IS NULL AND user_id = ?
80
- `;
81
- const missingRes = await db.execute({ sql: missingQuery, args: [...missingIds, userId] });
82
- for (const row of missingRes.rows) {
83
- anchorMap.set(row.id, {
84
- id: row.id,
85
- project: row.project,
86
- summary: row.summary,
87
- session_date: row.session_date,
88
- decisions: row.decisions && typeof row.decisions === 'string' ? JSON.parse(row.decisions) : undefined,
89
- files_changed: row.files_changed && typeof row.files_changed === 'string' ? JSON.parse(row.files_changed) : undefined,
90
- similarity: 0.0 // Base similarity is 0 since it wasn't matched originally
91
- });
92
- }
93
- }
94
- // Compute Hybrid Score and return M nodes
95
- for (const id of finalIds) {
96
- if (anchorMap.has(id)) {
97
- const node = anchorMap.get(id);
98
- const activationScore = activeNodes.get(id) || 0;
99
- node.activationScore = activationScore;
100
- // Hybrid blend: 70% original match relevance, 30% activation structural energy
101
- node.hybridScore = (node.similarity * 0.7) + (activationScore * 0.3);
102
- finalResults.push(node);
103
- }
104
- }
105
- // Sort descending by Hybrid Score
106
- return finalResults.sort((a, b) => (b.hybridScore || 0) - (a.hybridScore || 0));
107
- }