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 +25 -25
- package/dist/darkfactory/runner.js +7 -0
- package/dist/darkfactory/safetyController.js +10 -2
- package/dist/sdm/sdmEngine.js +27 -3
- package/dist/storage/sqlite.js +42 -10
- package/dist/storage/supabase.js +9 -5
- package/dist/tools/graphHandlers.js +14 -120
- package/package.json +1 -1
- package/dist/memory/spreadingActivation.js +0 -107
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
|
-
##
|
|
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)](
|
|
32
|
-
- [Cognitive Architecture (v7.8)](
|
|
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](
|
|
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](
|
|
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](
|
|
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
|
-
##
|
|
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
|
-
##
|
|
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
|
-
##
|
|
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
|
-
##
|
|
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
|
-
##
|
|
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
|
-
###
|
|
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
|
-
##
|
|
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
|
-
##
|
|
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
|
-
##
|
|
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
|
-
##
|
|
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
|
-
##
|
|
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
|
-
##
|
|
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](
|
|
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
|
-
##
|
|
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
|
-
##
|
|
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](
|
|
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
|
-
##
|
|
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
|
-
##
|
|
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
|
-
##
|
|
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
|
-
|
|
49
|
-
|
|
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.
|
package/dist/sdm/sdmEngine.js
CHANGED
|
@@ -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
|
-
//
|
|
284
|
-
this.counters[i] = state.
|
|
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
|
}
|
package/dist/storage/sqlite.js
CHANGED
|
@@ -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 {
|
|
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,
|
|
2529
|
+
args: [concept, buffer, HDC_DICTIONARY_VERSION],
|
|
2516
2530
|
});
|
|
2517
|
-
debugLog(`[SqliteStorage] Persisted HDC
|
|
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)
|
package/dist/storage/supabase.js
CHANGED
|
@@ -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 {
|
|
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:
|
|
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${
|
|
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
|
-
//
|
|
150
|
-
//
|
|
151
|
-
//
|
|
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:
|
|
382
|
+
// S_i: Normalized structural activation energy from Synapse logic
|
|
441
383
|
// (computed during applySynapse in storage layer)
|
|
442
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
555
|
-
//
|
|
556
|
-
//
|
|
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.
|
|
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
|
-
}
|