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 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 12-skill foundation. Paid accounts receive
116
- the current subscribed routing set. Upgrades install newly entitled packages;
117
- downgrades remove only Prism-owned packages while preserving local skills and
118
- locally modified conflicts.
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 Claude or the Synalux portal. 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`.
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 + cloud fallback |
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 + file paths + search queries, sent to the portal over TLS (PHI-redacted before transit) |
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.** All cloud writes pass through automatic redaction (SSNs, dates of birth, medical record numbers, phone numbers, emails, and clinical identifiers are stripped before transit). For regulated workloads, run the **local tier** for full air-gap, or use **Enterprise** which includes a HIPAA Business Associate Agreement.
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 Claude via the Synalux portal.
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 | Tier |
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
- Every tool-grounded answer on paid tiers passes through deterministic L3 routing rules and an NLI grounding verifier before reaching the user. Free-tier users get the deterministic gates (L1, L3-Tool, L3-Tier0) without the model-based NLI check.
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-Tool** | Tool name remap + false-positive rejection | None (deterministic) | 0 ms |
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 (Standard tier and up), timeout, ambiguity, or missing evidence yields a refusal, not pass-through. Free-tier users get the deterministic L1/L3-Tool gates but not the NLI verifier.
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
- **Reproduce every number yourself.** All evals are open-source and self-contained:
470
-
471
- ```bash
472
- git clone https://github.com/dcostenco/prism-coder && cd prism-coder
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 eval (115 cases, 12 categories, 3-seed mean).** Routing accuracy includes the deterministic L3 correction layer — the same rules that run in production. On this narrow tool-routing task all fleet models achieve near-perfect accuracy. Be honest with yourself about what that means: the eval is **near-saturated** for this taxonomy — it measures whether the right one of a small set of MCP tools is selected, not general capability. The useful takeaway is **offline routing reliability at zero cost**, not that a 2.3 GB model rivals a frontier model in general.
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
- ### Code Generation Quality (27B vs Claude Opus)
488
-
489
- Three progressively harder Python tasks run through `prism_infer(mode:"code", think:true)` on the local 27B and compared with Claude Opus. Both produce correct, production-quality code. The 27B is slightly more verbose (docstrings, examples); Opus is slightly tighter (`__slots__`, early-exit DFS). On routine coding the 27B at $0 replaces cloud calls entirely.
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
- | Task | used_cloud | Quality Gate | Latency | What happened |
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
- The quality gate detected repeated sentences (≥3 of the same sentence in ≥6 total) in the 27B's Trie output and escalated automatically. The cloud fallback returned clean code. On a second run of the same prompt, the 27B produced clean output without escalation — the loop is stochastic, not systematic.
569
-
570
- **Takeaway:** for ~80–90% of coding tasks, the 27B handles everything locally at $0. The quality gate + cloud escalation exists as a safety net for the remaining cases where the local model loops, truncates, or produces empty output. Paid tiers get automatic escalation; free tier gets the local result with a warning.
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 model ceilings, and increased limits. Local model ceilings are advisory — on-device models run on your Ollama regardless of plan; the ceiling gates cloud inference and `prism_infer` routing.
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
- | Local model ceiling | up to 4b | up to 9b | up to 27b | up to 27b |
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 | -- | Claude Opus 4.7 | Claude Opus 4.7 | Priority + Opus 4.7 |
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` | Natural-language Q&A over the memory store |
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 Claude
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 your host model's (Claude's) own token spend; use Claude Code's `/cost` command for that.
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
- Routing is automatic: `9b → 4b → cloud fallback` on desktop/server, `2b → cloud fallback` on mobile/iPhone. For iOS or another machine on the same network, run `OLLAMA_HOST=0.0.0.0 ollama serve` and point `LOCAL_LLM_URL` at the host's IP.
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 via `prism_infer`, capped at the 4B model tier |
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 | Up to 27B locally + cloud cascade (9B 27B → Claude) when local is unavailable |
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
@@ -262,4 +262,7 @@ export function registerShutdownHandlers() {
262
262
  process.stdin.on("close", () => {
263
263
  shutdown("CLIENT_DISCONNECTED_STDIN_CLOSED");
264
264
  });
265
+ return (reason) => {
266
+ void shutdown(reason);
267
+ };
265
268
  }
@@ -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 — connection-scoped, in-process.
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 (connection-scoped, cannot be portal-side):
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