prism-mcp-server 20.2.6 → 20.2.8
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 +119 -113
- package/dist/lifecycle.js +3 -0
- package/dist/mcpTransportHealth.js +62 -0
- package/dist/server.js +10 -1
- package/dist/session/sessionContext.js +113 -15
- package/dist/storage/configStorage.js +100 -0
- package/dist/tools/graphHandlers.js +5 -4
- package/dist/tools/handlers.js +2 -2
- package/dist/tools/ledgerHandlers.js +12 -16
- package/dist/tools/prismInferHandler.js +274 -14
- package/dist/tools/queryMemoryNaturalHandler.js +514 -0
- package/dist/tools/sessionMemoryDefinitions.js +23 -5
- package/dist/tools/skillRouting.js +1 -0
- package/dist/tools/v12Handlers.js +2 -46
- package/dist/utils/braveApi.js +31 -48
- package/dist/utils/codingQualityPolicy.js +400 -0
- package/dist/utils/entitlements.js +1 -0
- package/dist/utils/layer1.js +63 -4
- package/dist/utils/nlQuery.js +6 -33
- package/dist/utils/qualityGate.js +32 -15
- package/dist/utils/routeContract.js +319 -0
- package/dist/utils/synaluxSearch.js +12 -8
- package/package.json +3 -2
- package/dist/boundaries/__tests__/boundaries.test.js +0 -46
- package/dist/session/__tests__/sessionContext.test.js +0 -134
- package/dist/tools/__tests__/ingestHandler.test.js +0 -323
- package/dist/tools/__tests__/layer1Integration.test.js +0 -590
- package/dist/tools/__tests__/ledgerHandlers.test.js +0 -1281
package/README.md
CHANGED
|
@@ -25,6 +25,10 @@ A paid subscription adds cloud sync, higher model tiers, and team features throu
|
|
|
25
25
|
- **Local-first inference** — bounded work is routed through local Ollama models
|
|
26
26
|
first, with automatic 2B/4B/9B/27B selection based on installed models,
|
|
27
27
|
available RAM, context fit, and subscription entitlements.
|
|
28
|
+
- **Route-output enforcement** — route mode returns only well-formed calls to
|
|
29
|
+
tools the host actually advertised. Standard and higher plans can add
|
|
30
|
+
authenticated deterministic correction; `route_guard: "local"` keeps the
|
|
31
|
+
prompt and draft entirely on-device.
|
|
28
32
|
- **One setup for every agent** — `prism connect` configures Claude Code,
|
|
29
33
|
Claude Desktop, Cursor, Gemini CLI, and Codex while preserving unrelated
|
|
30
34
|
settings.
|
|
@@ -57,6 +61,38 @@ features.
|
|
|
57
61
|
<details>
|
|
58
62
|
<summary>Release history (optional)</summary>
|
|
59
63
|
|
|
64
|
+
## What's New in v20.2.7
|
|
65
|
+
|
|
66
|
+
### Session Saves Survive Agent Restarts
|
|
67
|
+
Prism now remembers that a conversation successfully loaded its project context
|
|
68
|
+
when the MCP server restarts or another Prism process handles the next request.
|
|
69
|
+
`session_save_ledger` and `session_save_handoff` no longer fail with a false
|
|
70
|
+
`context_not_loaded` error in that flow.
|
|
71
|
+
|
|
72
|
+
The recovery remains fail-closed: authorization is limited to the exact project
|
|
73
|
+
and conversation, expires with the existing context window, and stores no
|
|
74
|
+
plaintext conversation identifier. Cross-project, forged, malformed, expired,
|
|
75
|
+
or future-dated receipts are still rejected. The release also updates PostCSS
|
|
76
|
+
to the patched 8.5.23 release.
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## What's New in v20.2.6
|
|
81
|
+
|
|
82
|
+
### Safer Configuration Updates Across Every Agent
|
|
83
|
+
`prism connect` now reads Claude, Cursor, Gemini, and Codex configuration
|
|
84
|
+
through a single verified file snapshot, preventing another process from
|
|
85
|
+
swapping a file between Prism's safety check and its read. Supported symlinked
|
|
86
|
+
dotfiles still work, while dangling or planted symlinks fail loudly instead of
|
|
87
|
+
being followed or overwritten. This release also carries the patched
|
|
88
|
+
dependencies and cross-platform release checks introduced in v20.2.5.
|
|
89
|
+
|
|
90
|
+
Cloud fallback is now documented consistently as Gemini 3.6 Flash. Plan
|
|
91
|
+
ceilings govern automatic `prism_infer` routing; direct use of any downloaded
|
|
92
|
+
model through local Ollama remains free on every tier.
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
60
96
|
## What's New in v20.2.4
|
|
61
97
|
|
|
62
98
|
### Reliable Session Memory That Shows Work, Not Greetings
|
|
@@ -112,10 +148,13 @@ that would require a host lifecycle hook, launcher, extension, or Prism-owned
|
|
|
112
148
|
panel. Context loading itself remains complete even when a host shortens the
|
|
113
149
|
visible reply.
|
|
114
150
|
|
|
115
|
-
Free accounts receive the protected
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
151
|
+
Free accounts receive the protected 13-skill foundation. It includes
|
|
152
|
+
`current-staging-acceptance`, the strict completion extension of
|
|
153
|
+
`evidence-first-protocol`: agents must use the exact current staging artifact
|
|
154
|
+
and inspect every case and screenshot before reporting acceptance. Paid
|
|
155
|
+
accounts receive the current subscribed routing set. Upgrades install newly
|
|
156
|
+
entitled packages; downgrades remove only Prism-owned packages while preserving
|
|
157
|
+
local skills and locally modified conflicts.
|
|
119
158
|
|
|
120
159
|
When upgrading an older Claude Code installation, `prism connect` removes only
|
|
121
160
|
the exact Prism-owned startup, skill-sync, handoff, and drift hook actions from
|
|
@@ -354,7 +393,7 @@ Every `prism_infer` call tracks which model handled it (local Ollama vs cloud) a
|
|
|
354
393
|
synalux-27b: 2 calls, 1,500 tokens, avg 1,100ms
|
|
355
394
|
```
|
|
356
395
|
|
|
357
|
-
**Cloud tokens saved** is the honest routing metric — it accrues only when local Ollama handles a call that would otherwise have gone to
|
|
396
|
+
**Cloud tokens saved** is the honest routing metric — it accrues only when local Ollama handles a call that would otherwise have gone to Synalux cloud inference. A compact version appears inline after every 5th `prism_infer` call: `📊 local 10 (83%) · cloud 2 (17%) · ~11,570 tok · avg 1,240ms · 11,570 cloud tok saved`.
|
|
358
397
|
|
|
359
398
|
Local calls use actual Ollama token counts (`prompt_eval_count` / `eval_count` from Ollama); cloud calls use char/4 estimates. Metrics are tracked locally — no portal dependency, no env vars, works offline. Per-call data is also forwarded to the Synalux portal as best-effort analytics (independent of the display).
|
|
360
399
|
|
|
@@ -413,13 +452,18 @@ The free tier runs entirely on your machine. Paid tiers add cloud sync through t
|
|
|
413
452
|
| | Local tier (free) | Cloud tier (paid) |
|
|
414
453
|
|---|---|---|
|
|
415
454
|
| Memory storage | Local SQLite | Synalux portal (Supabase-backed) |
|
|
416
|
-
| Inference | Local Ollama models | Local models +
|
|
455
|
+
| Inference | Local Ollama models | Local models + Gemini 3.6 Flash fallback |
|
|
417
456
|
| API keys required | None | Synalux subscription key |
|
|
418
457
|
| Web search / scrape | Not included | Via Synalux portal (provider keys server-side) |
|
|
419
|
-
| What leaves your machine | Nothing | Memory text
|
|
458
|
+
| What leaves your machine | Nothing | Memory text, file paths, search queries, and inference prompts/drafts when their cloud feature is used, sent to the portal over TLS. Cloud memory writes are PHI-redacted; inference and route requests are transient. |
|
|
420
459
|
| Works offline | ✅ | Local features yes; sync/cloud no |
|
|
421
460
|
|
|
422
|
-
**Handling sensitive data.**
|
|
461
|
+
**Handling sensitive data.** Cloud memory writes pass through automatic
|
|
462
|
+
redaction (SSNs, dates of birth, medical record numbers, phone numbers, emails,
|
|
463
|
+
and clinical identifiers are stripped before storage). Cloud inference and
|
|
464
|
+
route correction send the request over TLS for processing and do not store it
|
|
465
|
+
as Prism memory; use `route_guard: "local"` or the **local tier** for a full
|
|
466
|
+
air-gap. **Enterprise** includes a HIPAA Business Associate Agreement.
|
|
423
467
|
|
|
424
468
|
---
|
|
425
469
|
|
|
@@ -427,15 +471,27 @@ The free tier runs entirely on your machine. Paid tiers add cloud sync through t
|
|
|
427
471
|
|
|
428
472
|
The `prism-coder` fleet uses Qwen3.5 for MCP tool-routing AND general inference. The 9B and 27B are fine-tuned with LoRA (r=128, all 64 layers including DeltaNet); the 2B and 4B use stock Qwen3.5-4B at different quantization levels. The 27B scored 100% on BFCL function-calling and 100% on an internal 15-problem coding eval at $0 inference cost.
|
|
429
473
|
|
|
430
|
-
`prism_infer` supports three modes: `route` (tool routing, fast, nothink), `chat` (conversation with thinking), and `code` (code generation with thinking). In chat/code modes, the model uses `<think>` blocks for chain-of-thought reasoning, which are stripped before the response is served. If the local model fails a quality gate (empty, think-only, or truncated), paid tiers automatically escalate to
|
|
474
|
+
`prism_infer` supports three modes: `route` (tool routing, fast, nothink), `chat` (conversation with thinking), and `code` (code generation with thinking). In chat/code modes, the model uses `<think>` blocks for chain-of-thought reasoning, which are stripped before the response is served. If the local model fails a quality gate (empty, think-only, or truncated), paid tiers automatically escalate to Gemini 3.6 Flash via the Synalux portal.
|
|
475
|
+
|
|
476
|
+
Every route-mode result is parsed locally and checked against `allowed_tools`
|
|
477
|
+
before it reaches the host. Malformed or unadvertised calls become `NO_TOOL`.
|
|
478
|
+
With `route_guard: "auto"` (the default), Standard and higher plans also send
|
|
479
|
+
a well-formed draft for one of Prism's seven trained tools—or an unadvertised
|
|
480
|
+
draft that may need correction—to Synalux for authenticated deterministic
|
|
481
|
+
correction. Advertised custom host tools remain local. Set
|
|
482
|
+
`route_guard: "local"` for a fully on-device route path.
|
|
431
483
|
|
|
432
|
-
| Model | Ollama tag | Size | [BFCL](https://gorilla.cs.berkeley.edu/blogs/12_bfcl_v3_multi_turn.html) Accuracy | Role |
|
|
484
|
+
| Model | Ollama tag | Size | [BFCL](https://gorilla.cs.berkeley.edu/blogs/12_bfcl_v3_multi_turn.html) Accuracy | Role | Automatic routing tier |
|
|
433
485
|
|---|---|---|---|---|---|
|
|
434
486
|
| Qwen3.5-4B Q3_K_M | `prism-coder:2b` | 2.3 GB | 99.1% × 3 seeds | iPhone / mobile first gate | Free |
|
|
435
487
|
| Qwen3.5-4B Q4_K_M | `prism-coder:4b` | 3.4 GB | 100% × 3 seeds | Verifier | Free |
|
|
436
488
|
| Qwen3.5-9B (LoRA) | `prism-coder:9b` | 5.8 GB | 100% × 3 seeds | Default router | Standard+ |
|
|
437
489
|
| Qwen3.5-27B (LoRA) | `prism-coder:27b` | 16 GB | 100% × 3 seeds | Quality tier (DeltaNet, 28.5 tok/s) | Advanced+ |
|
|
438
490
|
|
|
491
|
+
These tiers control automatic `prism_infer` selection, not Ollama itself. Any
|
|
492
|
+
user can run any downloaded on-device model directly through Ollama on every
|
|
493
|
+
plan.
|
|
494
|
+
|
|
439
495
|
Weights: [huggingface.co/dcostenco](https://huggingface.co/dcostenco) (public GGUF). Latency depends on model size and hardware — see [Benchmarks](#benchmarks) to measure it on your own machine rather than trusting a printed number.
|
|
440
496
|
|
|
441
497
|
### Cascade
|
|
@@ -445,36 +501,43 @@ query → prism-coder:9b (local router, default)
|
|
|
445
501
|
→ prism-coder:4b (grounding verifier)
|
|
446
502
|
→ prism-coder:2b (iPhone / mobile, auto-selected by RAM)
|
|
447
503
|
→ prism-coder:27b (complex tasks, on demand)
|
|
448
|
-
→ cloud fallback (paid tiers, for max quality)
|
|
504
|
+
→ Gemini 3.6 Flash cloud fallback (paid tiers, for max quality)
|
|
449
505
|
```
|
|
450
506
|
|
|
451
507
|
### Multi-Layer Verification
|
|
452
508
|
|
|
453
|
-
|
|
509
|
+
Route output and evidence-grounded answers use separate gates. Every tier gets
|
|
510
|
+
the local route parser and advertised-tool registry; Standard and higher plans
|
|
511
|
+
can add the private deterministic route correction. Evidence verification is
|
|
512
|
+
opt-in (or automatic when evidence is supplied) and remains separate from route
|
|
513
|
+
selection.
|
|
454
514
|
|
|
455
515
|
| Layer | What | Model | Cost |
|
|
456
516
|
|---|---|---|---|
|
|
457
517
|
| **L1** | Crisis/medical safety gate | None (regex) | 0 ms |
|
|
458
|
-
| **L3-
|
|
518
|
+
| **L3-Registry** | Envelope validation + advertised-tool enforcement (all tiers) | None | 0 ms |
|
|
519
|
+
| **L3-Route** | Authenticated deterministic route correction (Standard+) | None | Network latency |
|
|
459
520
|
| **L3-Tier0** | Integer grounding (set membership) | None (deterministic) | 0 ms |
|
|
460
521
|
| **L3-Tier2** | NLI verifier (claim → ENTAILED/NEUTRAL/CONTRADICTED) | prism-coder:2b | ~200 ms |
|
|
461
522
|
| **L4** | Hallucination judge (opt-out for clinical) | prism-coder:4b | ~500 ms |
|
|
462
523
|
|
|
463
|
-
Fail-closed on the verified path: when the grounding verifier runs
|
|
524
|
+
Fail-closed on the verified path: when the grounding verifier runs, timeout,
|
|
525
|
+
ambiguity, or missing evidence yields a refusal, not pass-through. If the paid
|
|
526
|
+
route correction is unavailable, the local registry still blocks malformed
|
|
527
|
+
and unadvertised calls and reports an allowed preserved route as degraded.
|
|
464
528
|
|
|
465
529
|
---
|
|
466
530
|
|
|
467
531
|
## Benchmarks
|
|
468
532
|
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
pip install anthropic requests
|
|
474
|
-
python3 tests/benchmarks/prism-routing-100/benchmark.py --models 2b 4b 9b 27b
|
|
475
|
-
```
|
|
533
|
+
Published benchmark numbers are concise summaries of internal deterministic
|
|
534
|
+
evaluation. Evaluators, exhaustive cases, exact tier-routing matrices, and raw
|
|
535
|
+
model outputs stay in the private engineering repository and are not included
|
|
536
|
+
in the npm package or public source tree.
|
|
476
537
|
|
|
477
|
-
**Routing
|
|
538
|
+
**Routing evaluation.** On a narrow tool-selection suite, the fleet achieved
|
|
539
|
+
near-saturated results across three seeds. This measures offline MCP routing
|
|
540
|
+
reliability, not general model capability.
|
|
478
541
|
|
|
479
542
|
| Model | Routing accuracy | Notes |
|
|
480
543
|
|---|---|---|
|
|
@@ -484,90 +547,17 @@ python3 tests/benchmarks/prism-routing-100/benchmark.py --models 2b 4b 9b 27b
|
|
|
484
547
|
|
|
485
548
|
**Memory uplift (LoCoMo-Plus, self-published).** A separate long-context dialogue benchmark ([dcostenco/Locomo-Plus](https://github.com/dcostenco/Locomo-Plus)) measures how much structured memory helps a base model retain multi-day context. Results show large gains when a model is paired with Prism memory versus running raw. Note this benchmark is authored, run, and LLM-judged by this project — treat it as a reproducible demonstration, not an independent third-party result, and run it yourself with the commands in that repo.
|
|
486
549
|
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
| Task | Local 27B | Claude Opus | Verdict |
|
|
492
|
-
|------|-----------|-------------|---------|
|
|
493
|
-
| Fibonacci with memoization | `@lru_cache`, ValueError on negative, docstring | Nested `_fib` to keep cache private | Both correct, equivalent |
|
|
494
|
-
| LRU Cache (OrderedDict, O(1)) | `Any` keys, isinstance capacity check, `__repr__` | `Hashable` key type (more precise), same ops | Both correct, Opus marginally tighter |
|
|
495
|
-
| Trie with autocomplete | `.lower()` normalization, collect+sort+slice | `__slots__` on TrieNode, early-exit DFS at limit | Both correct, Opus slightly more optimized |
|
|
496
|
-
|
|
497
|
-
<details>
|
|
498
|
-
<summary>Local 27B output — Trie with autocomplete (hardest task)</summary>
|
|
499
|
-
|
|
500
|
-
```python
|
|
501
|
-
class TrieNode:
|
|
502
|
-
def __init__(self):
|
|
503
|
-
self.children: dict[str, 'TrieNode'] = {}
|
|
504
|
-
self.is_end_of_word: bool = False
|
|
505
|
-
|
|
506
|
-
class Trie:
|
|
507
|
-
def __init__(self):
|
|
508
|
-
self.root: TrieNode = TrieNode()
|
|
509
|
-
|
|
510
|
-
def insert(self, word: str) -> None:
|
|
511
|
-
node = self.root
|
|
512
|
-
for char in word.lower():
|
|
513
|
-
if char not in node.children:
|
|
514
|
-
node.children[char] = TrieNode()
|
|
515
|
-
node = node.children[char]
|
|
516
|
-
node.is_end_of_word = True
|
|
517
|
-
|
|
518
|
-
def search(self, word: str) -> bool:
|
|
519
|
-
node = self._get_node(word.lower())
|
|
520
|
-
return node is not None and node.is_end_of_word
|
|
521
|
-
|
|
522
|
-
def starts_with(self, prefix: str) -> bool:
|
|
523
|
-
return self._get_node(prefix.lower()) is not None
|
|
524
|
-
|
|
525
|
-
def autocomplete(self, prefix: str, limit: int = 5) -> list[str]:
|
|
526
|
-
node = self._get_node(prefix.lower())
|
|
527
|
-
if node is None:
|
|
528
|
-
return []
|
|
529
|
-
results: list[str] = []
|
|
530
|
-
self._collect_words(node, prefix.lower(), results)
|
|
531
|
-
results.sort()
|
|
532
|
-
return results[:limit]
|
|
533
|
-
|
|
534
|
-
def _get_node(self, key: str) -> 'TrieNode | None':
|
|
535
|
-
node = self.root
|
|
536
|
-
for char in key:
|
|
537
|
-
if char not in node.children:
|
|
538
|
-
return None
|
|
539
|
-
node = node.children[char]
|
|
540
|
-
return node
|
|
541
|
-
|
|
542
|
-
def _collect_words(self, node: TrieNode, prefix: str, results: list[str]) -> None:
|
|
543
|
-
if node.is_end_of_word:
|
|
544
|
-
results.append(prefix)
|
|
545
|
-
for char, child in sorted(node.children.items()):
|
|
546
|
-
self._collect_words(child, prefix + char, results)
|
|
547
|
-
```
|
|
548
|
-
|
|
549
|
-
</details>
|
|
550
|
-
|
|
551
|
-
| Metric | Local 27B | Cloud (Opus) |
|
|
552
|
-
|--------|-----------|-------------|
|
|
553
|
-
| Latency (Trie task) | ~30s | ~8s |
|
|
554
|
-
| Cost | $0 | ~$0.05 |
|
|
555
|
-
| Think mode | Enabled (stripped before serving) | N/A |
|
|
556
|
-
| Quality gate | Passed (no escalation needed) | N/A |
|
|
557
|
-
|
|
558
|
-
### Cloud Escalation in Practice (`cloud_fallback: true`)
|
|
559
|
-
|
|
560
|
-
The same three tasks with `cloud_fallback: true` — the quality gate decides whether local output is good enough or needs cloud escalation.
|
|
550
|
+
**Code generation evaluation.** In a small July 2026 deterministic execution
|
|
551
|
+
check, the local 9B passed 2/3 tasks; the local 27B and Gemini 3.6 Flash each
|
|
552
|
+
passed 3/3. This is a self-published regression signal, not an independent
|
|
553
|
+
leaderboard or a claim of broad model equivalence.
|
|
561
554
|
|
|
562
|
-
|
|
563
|
-
|------|:----------:|-------------|---------|---------------|
|
|
564
|
-
| Fibonacci (simple) | **no** | Passed | 11s | 27B served directly, $0 |
|
|
565
|
-
| LRU Cache (medium) | **no** | Passed | 21s | 27B served directly, $0 |
|
|
566
|
-
| Trie (hard) | **yes** | `loop_detected` | 55s | 27B looped → gate caught it → escalated to cloud 27B |
|
|
555
|
+
### Cloud Escalation (`cloud_fallback: true`)
|
|
567
556
|
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
557
|
+
Prism always tries an eligible local model first. If the quality gate detects
|
|
558
|
+
an empty, truncated, think-only, or looping response, paid tiers can retry the
|
|
559
|
+
request through Gemini 3.6 Flash. Free-tier routing stays local and reports the
|
|
560
|
+
quality-gate outcome without making a cloud call.
|
|
571
561
|
|
|
572
562
|
---
|
|
573
563
|
|
|
@@ -618,17 +608,17 @@ and [Amazon Q Developer](https://aws.amazon.com/q/developer/pricing/).
|
|
|
618
608
|
|
|
619
609
|
## Plans
|
|
620
610
|
|
|
621
|
-
All on-device models are free to run locally via Ollama on every tier. A subscription gates **cloud** features, higher
|
|
611
|
+
All on-device models are free to run locally via Ollama on every tier. A subscription gates **cloud** features, higher automatic-routing ceilings, and increased limits. On-device models run through your Ollama regardless of plan; the ceiling applies only to cloud inference and automatic `prism_infer` routing.
|
|
622
612
|
|
|
623
613
|
| | **Free** | **Standard** $19/mo | **Advanced** $49/mo | **Enterprise** $99/mo |
|
|
624
614
|
|---|---|---|---|---|
|
|
625
615
|
| Seats | 1 | 1 | up to 5 | up to 25 |
|
|
626
|
-
|
|
|
616
|
+
| Automatic `prism_infer` ceiling | up to 4b | up to 9b | up to 27b | up to 27b |
|
|
627
617
|
| Cloud inference | -- | ✅ | ✅ | ✅ (priority) |
|
|
628
618
|
| Cloud Coder (Web IDE) | -- | ✅ | ✅ | ✅ (priority) |
|
|
629
619
|
| Cloud search | -- | ✅ | ✅ | ✅ |
|
|
630
620
|
| Max output tokens | 512 | 1,024 | 2,048 | 4,096 |
|
|
631
|
-
| Cloud fallback | -- |
|
|
621
|
+
| Cloud fallback | -- | Gemini 3.6 Flash | Gemini 3.6 Flash | Gemini 3.6 Flash (priority) |
|
|
632
622
|
| Grounding verifier (fact-check AI output) | -- | ✅ | ✅ | ✅ |
|
|
633
623
|
| Memory sync (cloud) | -- | ✅ | ✅ | ✅ |
|
|
634
624
|
| Knowledge / session memory | limited | unlimited | unlimited | unlimited |
|
|
@@ -650,13 +640,25 @@ Prism exposes 40+ MCP tools. The core memory loop:
|
|
|
650
640
|
| `session_save_ledger` | Append an immutable session log entry |
|
|
651
641
|
| `session_save_handoff` | Save live state for the next session |
|
|
652
642
|
| `knowledge_search` | Semantic + keyword search over all memories |
|
|
653
|
-
| `query_memory_natural` |
|
|
643
|
+
| `query_memory_natural` | Memory-first Q&A with a grounded live-source fallback on paid tiers |
|
|
654
644
|
| `session_detect_drift` | Detect when a session has drifted from its goal |
|
|
655
645
|
| `verify_behavior` | Pre-edit scenario challenge — catch bad changes before they happen |
|
|
656
646
|
| `knowledge_ingest` | Teach Prism a codebase or document |
|
|
657
647
|
| `prism_infer` | Local-first inference (route/chat/code modes, thinking, cloud escalation) |
|
|
658
648
|
| `inference_metrics` | Session delegation or persisted MCP + VS Code panel local/cloud stats |
|
|
659
649
|
|
|
650
|
+
### `query_memory_natural` — memory first, current sources when needed
|
|
651
|
+
|
|
652
|
+
Ask one natural-language question instead of choosing separate memory, search,
|
|
653
|
+
scrape, and inference tools. Prism searches its accumulated project memory
|
|
654
|
+
first. If no useful evidence exists, paid tiers run one bounded Synalux search
|
|
655
|
+
(Firecrawl, Gemini 3.6 Google Search grounding, then legacy Brave fallback),
|
|
656
|
+
resolve and preserve the source URLs, scrape the leading page, and ask a
|
|
657
|
+
RAM-safe local Prism Coder model to answer from that evidence. The paid-tier
|
|
658
|
+
Gemini 3.6 verifier checks the draft before it is served. Reserved or uncertain
|
|
659
|
+
clinical content never enters the web-grounded local path; it follows Prism's
|
|
660
|
+
cloud-or-refuse safety boundary.
|
|
661
|
+
|
|
660
662
|
### `prism_infer` — local-first inference with cloud escalation
|
|
661
663
|
|
|
662
664
|
```typescript
|
|
@@ -667,7 +669,7 @@ prism_infer({
|
|
|
667
669
|
model_ceiling: "27b", // use the quality tier
|
|
668
670
|
})
|
|
669
671
|
// → 27B generates code locally ($0), with thinking for quality
|
|
670
|
-
// → If quality gate fails + paid tier → auto-escalate to
|
|
672
|
+
// → If quality gate fails + paid tier → auto-escalate to Gemini 3.6 Flash
|
|
671
673
|
```
|
|
672
674
|
|
|
673
675
|
| Mode | Think | Model | Use case |
|
|
@@ -694,7 +696,7 @@ Call `inference_metrics` anytime mid-session to see how many `prism_infer` calls
|
|
|
694
696
|
|
|
695
697
|
The same block also appears automatically in `session_save_ledger` and `session_save_handoff` responses at session end.
|
|
696
698
|
|
|
697
|
-
**Note:** The default session view tracks this MCP process's `prism_infer` delegation. The all-time view combines persisted MCP calls with Synalux VS Code panel inference. Neither view includes
|
|
699
|
+
**Note:** The default session view tracks this MCP process's `prism_infer` delegation. The all-time view combines persisted MCP calls with Synalux VS Code panel inference. Neither view includes the host agent's own token spend; use that host's native usage reporting when available.
|
|
698
700
|
|
|
699
701
|
### Local-model delegation (default)
|
|
700
702
|
|
|
@@ -883,7 +885,11 @@ ollama pull dcostenco/prism-coder:9b # default router
|
|
|
883
885
|
export LOCAL_LLM_URL=http://localhost:11434
|
|
884
886
|
```
|
|
885
887
|
|
|
886
|
-
|
|
888
|
+
Self-hosted routing stays local: `9b → 4b` on desktop/server and `2b` on
|
|
889
|
+
mobile/iPhone, with 27B available when installed and RAM-safe. Synalux-hosted
|
|
890
|
+
paid tiers can use Gemini 3.6 Flash as the cloud fallback. For iOS or another
|
|
891
|
+
machine on the same network, run `OLLAMA_HOST=0.0.0.0 ollama serve` and point
|
|
892
|
+
`LOCAL_LLM_URL` at the host's IP.
|
|
887
893
|
|
|
888
894
|
---
|
|
889
895
|
|
|
@@ -934,7 +940,7 @@ It reads `~/.prism-mcp/data.db` and POSTs entries to the portal. Ledger entries
|
|
|
934
940
|
|
|
935
941
|
| Feature | Details |
|
|
936
942
|
|---------|---------|
|
|
937
|
-
| Local inference | Ollama
|
|
943
|
+
| Local inference | Direct Ollama use is unrestricted; automatic `prism_infer` routing selects up to 4B |
|
|
938
944
|
| Session memory | Persistent sessions, handoffs, ledger — all local SQLite |
|
|
939
945
|
| Knowledge search | Semantic search across session history |
|
|
940
946
|
| Skills | All skills available locally (run `sync-skills.sh` to populate) |
|
|
@@ -946,7 +952,7 @@ Everything in Free, plus:
|
|
|
946
952
|
|
|
947
953
|
| Feature | Details |
|
|
948
954
|
|---------|---------|
|
|
949
|
-
| Model ceiling |
|
|
955
|
+
| Model ceiling | Automatic `prism_infer` routing up to 27B + Gemini 3.6 Flash fallback when local is unavailable |
|
|
950
956
|
| Skill routing | Portal resolves which skills to load based on your project and prompt |
|
|
951
957
|
| Cross-device memory | Supabase cloud sync — sessions survive across machines |
|
|
952
958
|
| Grounding verifier | L3 NLI verification on model outputs |
|
package/dist/lifecycle.js
CHANGED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
const DEFAULT_KEEPALIVE_INTERVAL_MS = 60_000;
|
|
2
|
+
const DEFAULT_PING_TIMEOUT_MS = 15_000;
|
|
3
|
+
/**
|
|
4
|
+
* Keeps long-idle stdio MCP sessions active and converts a silently dead
|
|
5
|
+
* client transport into a clean server shutdown. Some hosts retain the child
|
|
6
|
+
* pipes after their protocol worker closes, so stdin "close" alone cannot
|
|
7
|
+
* detect the failure.
|
|
8
|
+
*/
|
|
9
|
+
export function monitorMcpTransport(server, options) {
|
|
10
|
+
const intervalMs = options.intervalMs ?? DEFAULT_KEEPALIVE_INTERVAL_MS;
|
|
11
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_PING_TIMEOUT_MS;
|
|
12
|
+
const previousOnClose = server.onclose;
|
|
13
|
+
let stopped = false;
|
|
14
|
+
let pingInFlight = false;
|
|
15
|
+
const stop = () => {
|
|
16
|
+
if (stopped)
|
|
17
|
+
return;
|
|
18
|
+
stopped = true;
|
|
19
|
+
clearInterval(timer);
|
|
20
|
+
if (server.onclose === handleClose) {
|
|
21
|
+
server.onclose = previousOnClose;
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
const fail = (reason, error) => {
|
|
25
|
+
if (stopped)
|
|
26
|
+
return;
|
|
27
|
+
stop();
|
|
28
|
+
options.onFailure(reason, error);
|
|
29
|
+
};
|
|
30
|
+
const handleClose = () => {
|
|
31
|
+
previousOnClose?.();
|
|
32
|
+
fail("MCP_TRANSPORT_CLOSED");
|
|
33
|
+
};
|
|
34
|
+
server.onclose = handleClose;
|
|
35
|
+
const check = async () => {
|
|
36
|
+
if (stopped || pingInFlight)
|
|
37
|
+
return;
|
|
38
|
+
pingInFlight = true;
|
|
39
|
+
let timeout;
|
|
40
|
+
try {
|
|
41
|
+
await Promise.race([
|
|
42
|
+
server.ping(),
|
|
43
|
+
new Promise((_, reject) => {
|
|
44
|
+
timeout = setTimeout(() => reject(new Error(`MCP keepalive timed out after ${timeoutMs}ms`)), timeoutMs);
|
|
45
|
+
}),
|
|
46
|
+
]);
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
fail("MCP_KEEPALIVE_FAILED", error instanceof Error ? error : new Error(String(error)));
|
|
50
|
+
}
|
|
51
|
+
finally {
|
|
52
|
+
if (timeout)
|
|
53
|
+
clearTimeout(timeout);
|
|
54
|
+
pingInFlight = false;
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
const timer = setInterval(() => {
|
|
58
|
+
void check();
|
|
59
|
+
}, intervalMs);
|
|
60
|
+
timer.unref?.();
|
|
61
|
+
return stop;
|
|
62
|
+
}
|
package/dist/server.js
CHANGED
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
*/
|
|
40
40
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
41
41
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
42
|
+
import { monitorMcpTransport } from "./mcpTransportHealth.js";
|
|
42
43
|
import { buildVSCodePrompt } from "./aba-protocol.js";
|
|
43
44
|
import { CallToolRequestSchema, ListToolsRequestSchema,
|
|
44
45
|
// ─── v0.4.0: MCP Prompts support (Enhancement #1) ───
|
|
@@ -1252,7 +1253,15 @@ export async function startServer() {
|
|
|
1252
1253
|
// Register graceful shutdown handlers (SIGTERM, SIGINT, SIGHUP, stdin close).
|
|
1253
1254
|
// The stdin close handler is critical — when MCP clients disconnect, they
|
|
1254
1255
|
// often just close the pipe without sending a signal, leaving zombie processes.
|
|
1255
|
-
registerShutdownHandlers();
|
|
1256
|
+
const requestShutdown = registerShutdownHandlers();
|
|
1257
|
+
monitorMcpTransport(server, {
|
|
1258
|
+
onFailure: (reason, error) => {
|
|
1259
|
+
if (error) {
|
|
1260
|
+
console.error(`[Prism] MCP transport health check failed: ${error.message}`);
|
|
1261
|
+
}
|
|
1262
|
+
requestShutdown(reason);
|
|
1263
|
+
},
|
|
1264
|
+
});
|
|
1256
1265
|
// Pre-warm storage AFTER connecting — fired async so we never block the
|
|
1257
1266
|
// stdio handshake. Supabase REST initialization can take 500ms–5s; blocking
|
|
1258
1267
|
// on it before server.connect() was the root cause of the 1m 56s CLI delay.
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Session state tracking —
|
|
2
|
+
* Session state tracking — in-process hot path with a durable local receipt.
|
|
3
3
|
*
|
|
4
4
|
* This is NOT business logic — it's MCP connection lifecycle state.
|
|
5
5
|
* Business logic (skill routing, budget tranching, content resolution)
|
|
6
6
|
* lives in the synalux portal at /api/v1/prism/skills.
|
|
7
7
|
*
|
|
8
|
-
* What stays here (
|
|
8
|
+
* What stays here (host lifecycle state, cannot be portal-side):
|
|
9
9
|
* markContextLoaded / requireContextLoaded — write-gate for session tools
|
|
10
10
|
* noteInferenceForSession — telemetry counter
|
|
11
11
|
* drift timer — connection-scoped GATE 5 enforcement
|
|
@@ -14,9 +14,12 @@
|
|
|
14
14
|
* Skill routing, budget tranching, content loading, phantom detection,
|
|
15
15
|
* prompt-keyword matching, user-local skill loading, context-discovery.
|
|
16
16
|
*/
|
|
17
|
+
import { createHash } from "node:crypto";
|
|
17
18
|
import { BOUNDARIES_VERSION as CURRENT_BOUNDARIES_VERSION } from "../boundaries/boundaries.js";
|
|
19
|
+
import * as configStorage from "../storage/configStorage.js";
|
|
18
20
|
const SESSION_TTL_MS = 6 * 60 * 60 * 1000; // 6 h — conversation-scoped
|
|
19
21
|
const MAX_SESSIONS = 10_000;
|
|
22
|
+
const RECEIPT_CLOCK_SKEW_MS = 60_000;
|
|
20
23
|
/**
|
|
21
24
|
* Connection-scoped fallback: remember the last conversation_id seen via
|
|
22
25
|
* markContextLoaded so that tools which don't carry conversation_id
|
|
@@ -81,6 +84,62 @@ export function markContextLoaded(conversationId, project, boundariesVersion) {
|
|
|
81
84
|
s.boundariesVersion = boundariesVersion;
|
|
82
85
|
lastSeenConversationId = conversationId;
|
|
83
86
|
}
|
|
87
|
+
function contextNotLoadedError(project) {
|
|
88
|
+
const projectNote = project
|
|
89
|
+
? " the requested project was not loaded for this conversation."
|
|
90
|
+
: "";
|
|
91
|
+
return {
|
|
92
|
+
blocked: true,
|
|
93
|
+
error: "context_not_loaded:" + projectNote + " Call session_bootstrap(conversation_id) or " +
|
|
94
|
+
"session_load_context(project, conversation_id) " +
|
|
95
|
+
"before this action. This project-scoped tool needs confirmed working context " +
|
|
96
|
+
"to act correctly. (Enforced server-side — applies to every host.)",
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
function contextExpiredError() {
|
|
100
|
+
return {
|
|
101
|
+
blocked: true,
|
|
102
|
+
error: "context_not_loaded: session expired (6 h TTL). Call " +
|
|
103
|
+
"session_bootstrap(conversation_id) or session_load_context(project, conversation_id) again. " +
|
|
104
|
+
"(Enforced server-side — applies to every host.)",
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
function hashReceiptScope(kind, value) {
|
|
108
|
+
return createHash("sha256").update(`prism-session-${kind}\0${value}`).digest("hex");
|
|
109
|
+
}
|
|
110
|
+
async function persistContextReceipt(conversationId, project, boundariesVersion, loadedAt) {
|
|
111
|
+
const now = Date.now();
|
|
112
|
+
const receipt = {
|
|
113
|
+
conversationHash: hashReceiptScope("conversation", conversationId),
|
|
114
|
+
projectHash: hashReceiptScope("project", project),
|
|
115
|
+
project,
|
|
116
|
+
boundariesVersion,
|
|
117
|
+
loadedAt,
|
|
118
|
+
lastSeen: now,
|
|
119
|
+
};
|
|
120
|
+
await configStorage.saveSessionContextReceipt(receipt, now - SESSION_TTL_MS);
|
|
121
|
+
}
|
|
122
|
+
async function persistContextReceiptBestEffort(conversationId, project, boundariesVersion, loadedAt) {
|
|
123
|
+
try {
|
|
124
|
+
await persistContextReceipt(conversationId, project, boundariesVersion, loadedAt);
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
// Keep the established same-process path available on read-only or damaged
|
|
128
|
+
// config stores, but make degraded restart recovery visible to operators.
|
|
129
|
+
console.error(`[sessionContext] Durable context receipt unavailable for project "${project}": ` +
|
|
130
|
+
`${error instanceof Error ? error.message : String(error)}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Registers successful context materialization and persists an opaque receipt.
|
|
135
|
+
* The conversation_id never leaves this process unhashed.
|
|
136
|
+
*/
|
|
137
|
+
export async function registerContextLoaded(conversationId, project, boundariesVersion) {
|
|
138
|
+
markContextLoaded(conversationId, project, boundariesVersion);
|
|
139
|
+
noteDriftSessionStart(conversationId);
|
|
140
|
+
const s = sessions.get(conversationId);
|
|
141
|
+
await persistContextReceiptBestEffort(conversationId, project, boundariesVersion, s?.driftSessionStart ?? Date.now());
|
|
142
|
+
}
|
|
84
143
|
/**
|
|
85
144
|
* Soft gate for handlers that need project context to be CORRECT (not safe).
|
|
86
145
|
*
|
|
@@ -108,21 +167,10 @@ export function requireContextLoaded(conversationId) {
|
|
|
108
167
|
// even if no write has triggered eviction yet. Evict immediately on detection.
|
|
109
168
|
if (s && (Date.now() - s.lastSeen) > SESSION_TTL_MS) {
|
|
110
169
|
sessions.delete(conversationId);
|
|
111
|
-
return
|
|
112
|
-
blocked: true,
|
|
113
|
-
error: "context_not_loaded: session expired (6 h TTL). Call " +
|
|
114
|
-
"session_bootstrap(conversation_id) or session_load_context(project, conversation_id) again. " +
|
|
115
|
-
"(Enforced server-side — applies to every host.)",
|
|
116
|
-
};
|
|
170
|
+
return contextExpiredError();
|
|
117
171
|
}
|
|
118
172
|
if (!s || !s.contextLoaded) {
|
|
119
|
-
return
|
|
120
|
-
blocked: true,
|
|
121
|
-
error: "context_not_loaded: call session_bootstrap(conversation_id) or " +
|
|
122
|
-
"session_load_context(project, conversation_id) " +
|
|
123
|
-
"before this action. This project-scoped tool needs confirmed working context " +
|
|
124
|
-
"to act correctly. (Enforced server-side — applies to every host.)",
|
|
125
|
-
};
|
|
173
|
+
return contextNotLoadedError();
|
|
126
174
|
}
|
|
127
175
|
// Touch on valid read — maintains LRU order.
|
|
128
176
|
touch(conversationId, s);
|
|
@@ -139,6 +187,56 @@ export function requireContextLoaded(conversationId) {
|
|
|
139
187
|
}
|
|
140
188
|
return null;
|
|
141
189
|
}
|
|
190
|
+
/**
|
|
191
|
+
* Project-scoped durable gate used by ledger and handoff writes.
|
|
192
|
+
*
|
|
193
|
+
* The in-memory state remains the hot path. If another MCP process loaded the
|
|
194
|
+
* requested project, or this process restarted, an unexpired hashed receipt
|
|
195
|
+
* restores only that exact project. Unknown, malformed, expired, and
|
|
196
|
+
* cross-project lookups remain fail-closed.
|
|
197
|
+
*/
|
|
198
|
+
export async function requireContextLoadedForProject(conversationId, project) {
|
|
199
|
+
if (conversationId === undefined)
|
|
200
|
+
return null;
|
|
201
|
+
if (!conversationId || !project.trim())
|
|
202
|
+
return contextNotLoadedError(project || undefined);
|
|
203
|
+
const memoryGate = requireContextLoaded(conversationId);
|
|
204
|
+
const memoryState = sessions.get(conversationId);
|
|
205
|
+
if (memoryState?.contextLoaded && memoryState.project === project &&
|
|
206
|
+
!(memoryGate && memoryGate.blocked)) {
|
|
207
|
+
await persistContextReceiptBestEffort(conversationId, project, memoryState.boundariesVersion ?? CURRENT_BOUNDARIES_VERSION, memoryState.driftSessionStart ?? memoryState.lastSeen);
|
|
208
|
+
return memoryGate;
|
|
209
|
+
}
|
|
210
|
+
let receipt = null;
|
|
211
|
+
try {
|
|
212
|
+
receipt = await configStorage.getSessionContextReceipt(hashReceiptScope("conversation", conversationId), hashReceiptScope("project", project));
|
|
213
|
+
}
|
|
214
|
+
catch (error) {
|
|
215
|
+
console.error(`[sessionContext] Durable context receipt lookup failed for project "${project}": ` +
|
|
216
|
+
`${error instanceof Error ? error.message : String(error)}`);
|
|
217
|
+
return memoryGate && memoryGate.blocked ? memoryGate : contextNotLoadedError(project);
|
|
218
|
+
}
|
|
219
|
+
if (!receipt)
|
|
220
|
+
return memoryGate?.blocked ? memoryGate : contextNotLoadedError(project);
|
|
221
|
+
const now = Date.now();
|
|
222
|
+
const invalidReceipt = receipt.project !== project ||
|
|
223
|
+
!Number.isFinite(receipt.loadedAt) ||
|
|
224
|
+
!Number.isFinite(receipt.lastSeen) ||
|
|
225
|
+
receipt.loadedAt <= 0 ||
|
|
226
|
+
receipt.loadedAt > receipt.lastSeen ||
|
|
227
|
+
receipt.lastSeen > now + RECEIPT_CLOCK_SKEW_MS;
|
|
228
|
+
if (invalidReceipt)
|
|
229
|
+
return contextNotLoadedError(project);
|
|
230
|
+
if ((now - receipt.lastSeen) > SESSION_TTL_MS)
|
|
231
|
+
return contextExpiredError();
|
|
232
|
+
markContextLoaded(conversationId, project, receipt.boundariesVersion);
|
|
233
|
+
const restored = sessions.get(conversationId);
|
|
234
|
+
if (restored)
|
|
235
|
+
restored.driftSessionStart = receipt.loadedAt;
|
|
236
|
+
const restoredGate = requireContextLoaded(conversationId);
|
|
237
|
+
await persistContextReceiptBestEffort(conversationId, project, receipt.boundariesVersion, receipt.loadedAt);
|
|
238
|
+
return restoredGate;
|
|
239
|
+
}
|
|
142
240
|
/** Best-effort telemetry from prism_infer. Never affects a safety decision. */
|
|
143
241
|
export function noteInferenceForSession(conversationId, info) {
|
|
144
242
|
// Only update sessions that already exist — don't create ghost stubs for
|