prism-mcp-server 20.2.2 → 20.2.4

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
@@ -18,6 +18,58 @@ A paid subscription adds cloud sync, higher model tiers, and team features throu
18
18
 
19
19
  ---
20
20
 
21
+ ## What Prism gives you
22
+
23
+ - **Session memory that survives restarts** — resume projects with handoff notes,
24
+ recent work, open TODOs, and configurable quick, standard, or deep context.
25
+ - **Local-first inference** — bounded work is routed through local Ollama models
26
+ first, with automatic 2B/4B/9B/27B selection based on installed models,
27
+ available RAM, context fit, and subscription entitlements.
28
+ - **One setup for every agent** — `prism connect` configures Claude Code,
29
+ Claude Desktop, Cursor, Gemini CLI, and Codex while preserving unrelated
30
+ settings.
31
+ - **Subscription-aware skills** — entitled skills are synchronized before the
32
+ host launches, with safe upgrades, downgrades, conflict preservation, and
33
+ offline last-good recovery.
34
+ - **Hook-free startup** — MCP metadata and native instructions request Prism's
35
+ startup context without requiring lifecycle hooks or a Prism-owned launcher.
36
+ - **Safe escalation and observability** — inference outcomes are explicit,
37
+ reserved content remains fail-closed, and local/cloud usage is recorded for
38
+ review.
39
+
40
+ ## Get started
41
+
42
+ ```bash
43
+ npm install -g prism-mcp-server
44
+ prism connect
45
+ ```
46
+
47
+ Use `prism connect --dry-run` to preview changes, `prism connect --all` to
48
+ configure every detected host, or `prism connect --refresh` to reconcile
49
+ Prism-managed entries after an upgrade. Restart the host after connecting.
50
+
51
+ Prism works locally without an account, API key, or cloud subscription. Add a
52
+ Synalux subscription when you want cloud memory, paid-tier skills, or team
53
+ features.
54
+
55
+ ---
56
+
57
+ <details>
58
+ <summary>Release history (optional)</summary>
59
+
60
+ ## What's New in v20.2.4
61
+
62
+ ### Reliable Session Memory That Shows Work, Not Greetings
63
+ Greeting-only assistant replies are skipped before ledger writes. Existing
64
+ greeting rows are filtered at read time across native startup, MCP context, and
65
+ `prism load --json`, while entries containing decisions, TODOs, changed files,
66
+ or non-session events remain visible. Historical rows are not destructively
67
+ deleted. If Synalux has a transient startup failure, Prism displays one bounded
68
+ local last-good snapshot and clearly labels it; permanent authorization or
69
+ validation failures still fail loud, and later writes remain cloud-routed.
70
+
71
+ ---
72
+
21
73
  ## What's New in v20.2.2
22
74
 
23
75
  ### One Local-First Workflow Across Every Agent
@@ -186,6 +238,8 @@ External contributions now require signing the [Individual CLA](./CLA.md). The C
186
238
 
187
239
  ---
188
240
 
241
+ </details>
242
+
189
243
  ## Quickstart
190
244
 
191
245
  The free tier needs no account, no API key, and no cloud. Install Prism, then
@@ -521,42 +575,44 @@ The quality gate detected repeated sentences (≥3 of the same sentence in ≥6
521
575
 
522
576
  ### vs AI coding assistants
523
577
 
524
- These tables are the maintainer's assessment as of June 2026. Verify claims that matter to you — products change fast.
525
-
526
- | Feature | Prism Coder | GitHub Copilot | Cursor | Windsurf | Amazon Q | Devin |
527
- |---|:---:|:---:|:---:|:---:|:---:|:---:|
528
- | Local inference (open-weight) | | | | | | ❌ |
529
- | Works fully offline | ✅ (free tier) | ❌ | ❌ | ❌ | ❌ | ❌ |
530
- | Persistent cross-session memory | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ |
531
- | Session drift detection | | | | | | |
532
- | L3 grounding verifier | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
533
- | Behavioral verification (pre-edit) | ✅ | | | | ❌ | ❌ |
534
- | MCP server (tools + memory) | ✅ | | | | ❌ | ❌ |
535
- | Web IDE | ✅ | | | | | ✅ |
536
- | VS Code extension | ✅ | ✅ | | | | |
537
- | Flat-rate team pricing | ✅ | (per-seat) | (per-seat) | | ❌ | ❌ |
538
- | HIPAA BAA available | (Enterprise) | | | | | ❌ |
578
+ Product capabilities and plans change frequently. The comparison below is
579
+ intentionally limited to publicly documented differences; it is not a claim
580
+ that another product lacks an unlisted feature.
581
+
582
+ Legend:documented, conditional or plan-dependent, not compared, ? verify
583
+ with the provider.
584
+
585
+ | Capability | Prism Coder | GitHub Copilot | Cursor | Amazon Q Developer |
586
+ |---|:---:|:---:|:---:|:---:|
587
+ | Local/open-weight inference | ✅ | | | |
588
+ | Offline workflow | ✅ | | ? | ? |
589
+ | Cross-session memory | ✅ | ([docs](https://docs.github.com/en/copilot/reference/copilot-cli-reference/cli-command-reference)) | | |
590
+ | MCP integration | ✅ | ✅ ([docs](https://docs.github.com/en/copilot/reference/copilot-cli-reference/cli-command-reference)) | ([pricing](https://cursor.com/en-US/pricing)) | |
591
+ | Local-first model routing | ✅ | | | |
592
+ | Session drift and grounding checks | | | | |
593
+ | Setup surface | ✅ five hosts | ✅ CLI/IDE | ✅ editor/agents | ✅ IDE/CLI ([overview](https://aws.amazon.com/q/developer/build/)) |
594
+ | Pricing model | ✅ Synalux tiers | ◐ | ◐ ([pricing](https://cursor.com/en-US/pricing)) | ✅ free + $19 Pro ([pricing](https://aws.amazon.com/q/developer/pricing/)) |
595
+
596
+ Prism-specific compliance, contractual, and pricing terms are documented in
597
+ the Synalux service agreement. Do not infer a competitor's HIPAA, BAA, or data
598
+ handling status from this table.
539
599
 
540
600
  ### vs local AI / memory tools
541
601
 
542
602
  | Feature | Prism Coder | Ollama | LM Studio | Mem0 | Zep |
543
603
  |---|:---:|:---:|:---:|:---:|:---:|
544
- | Local inference cascade | ✅ | ✅ | ✅ | | |
545
- | Cloud fallback | ✅ | | | | |
546
- | Persistent cross-session memory | ✅ | | | ✅ | ✅ |
547
- | Knowledge ingestion (MCP + webhook) | | | | | |
548
- | Cognitive routing (3-store) | ✅ | | | | |
549
- | Session drift detection | ✅ | ❌ | ❌ | ❌ | ❌ |
550
- | Native MCP server | ✅ | ❌ | ❌ | ❌ | ❌ |
551
- | Web IDE + VS Code extension | ✅ | ❌ | ❌ | ❌ | ❌ |
552
-
553
- ### Pricing — flat-rate, not per-seat
554
-
555
- | | **Prism Coder** | GitHub Copilot | Cursor | Amazon Q |
556
- |---|:---:|:---:|:---:|:---:|
557
- | **Individual** | **$19/mo** | $10/mo | $20/mo | $19/mo |
558
- | **Team (5 devs)** | **$49/mo flat** | $95/mo | $200/mo | $95/mo |
559
- | **Enterprise (25 devs)** | **$99/mo flat** | $195/mo | $1,000/mo | Custom |
604
+ | Local inference cascade | ✅ | ✅ runtime | ✅ app | | |
605
+ | Cloud fallback | ✅ optional | | provider-dependent | | |
606
+ | Persistent memory | ✅ | | project context | ✅ | ✅ |
607
+ | Knowledge/tool integration | ✅ MCP + ingestion | APIs | integrations | SDK/API | SDK/API |
608
+ | MCP server | ✅ native | | client integration | client integration | client integration |
609
+
610
+ ### Pricing
611
+
612
+ Prism's current published tiers are listed below. Competitor pricing is
613
+ usage- and plan-dependent, so consult the provider directly: [GitHub
614
+ Copilot](https://github.com/features/copilot/plans), [Cursor](https://cursor.com/en-US/pricing),
615
+ and [Amazon Q Developer](https://aws.amazon.com/q/developer/pricing/).
560
616
 
561
617
  ---
562
618
 
@@ -686,10 +742,37 @@ prism save # save ledger + handoff
686
742
  prism search <query> # search code across repos (exact / regex / symbol / semantic)
687
743
  prism review <files...> # AI code review — security, performance, style
688
744
  prism scan <files...> # security scan — secrets, licenses, Dockerfile
745
+ prism browser ... # persistent local browser testing and structured automation
689
746
  prism push # push local SQLite to the cloud backend
690
747
  prism register-models # alias dcostenco/prism-coder:* -> prism-coder:*
691
748
  ```
692
749
 
750
+ ### `prism browser` — local browser testing
751
+
752
+ The npm package includes Prism's Python/Playwright browser runner; no separate
753
+ Prism Browser app or DMG is required. It adds a stable agent-facing CLI around
754
+ Playwright with reusable named profiles, multi-action pipe/REPL sessions,
755
+ redacted local audit logs, and guarded preload scripts for local apps. Use pipe
756
+ or REPL mode when several actions must share one page session:
757
+
758
+ ```bash
759
+ printf 'open http://127.0.0.1:3000\nwait-for #app\nread-dom #app\n' | \
760
+ prism browser --headless --local-only pipe
761
+ ```
762
+
763
+ Local apps can load repeatable pre-navigation test helpers with
764
+ `--inject ./tests/browser-init.js`. Custom injection requires `--local-only`;
765
+ public navigation and non-loopback requests are rejected in that mode. Install
766
+ the local runtime once with `pip3 install playwright playwright-stealth` and
767
+ `python3 -m playwright install chromium`.
768
+
769
+ Use raw Playwright for authored suites that need its full assertion, tracing,
770
+ fixture, and parallel-worker APIs. Use `prism browser` when an AI agent needs a
771
+ small, persistent, auditable local browser session through one consistent CLI.
772
+ The compatibility patches are best effort; they are not a CAPTCHA-bypass
773
+ guarantee. See [Prism Browser local testing](docs/prism-browser.md) for the
774
+ command surface, safety model, and verified acceptance cases.
775
+
693
776
  ### `prism search` — semantic code search
694
777
 
695
778
  <p align="center">
@@ -0,0 +1,56 @@
1
+ import { spawn, spawnSync } from 'node:child_process';
2
+ import { existsSync } from 'node:fs';
3
+ import { fileURLToPath } from 'node:url';
4
+ export function resolveBundledBrowserScript() {
5
+ return fileURLToPath(new URL('../scripts/dev/browse.py', import.meta.url));
6
+ }
7
+ export function pythonCandidates(env = process.env, platform = process.platform) {
8
+ if (env.PRISM_PYTHON) {
9
+ return [{ executable: env.PRISM_PYTHON, prefixArgs: [] }];
10
+ }
11
+ return platform === 'win32'
12
+ ? [
13
+ { executable: 'py', prefixArgs: ['-3'] },
14
+ { executable: 'python', prefixArgs: [] },
15
+ ]
16
+ : [
17
+ { executable: 'python3', prefixArgs: [] },
18
+ { executable: 'python', prefixArgs: [] },
19
+ ];
20
+ }
21
+ export function resolvePythonCommand(env = process.env, platform = process.platform) {
22
+ for (const candidate of pythonCandidates(env, platform)) {
23
+ const result = spawnSync(candidate.executable, [...candidate.prefixArgs, '--version'], { env, encoding: 'utf8', shell: false });
24
+ if (!result.error && result.status === 0)
25
+ return candidate;
26
+ }
27
+ return null;
28
+ }
29
+ export function hasPlaywrightRuntime(python, env = process.env) {
30
+ const result = spawnSync(python.executable, [...python.prefixArgs, '-c', 'import playwright.sync_api'], { env, encoding: 'utf8', shell: false });
31
+ return !result.error && result.status === 0;
32
+ }
33
+ export async function runBrowserCli(args, env = process.env) {
34
+ const scriptPath = resolveBundledBrowserScript();
35
+ if (!existsSync(scriptPath)) {
36
+ throw new Error(`Bundled Prism Browser script is missing: ${scriptPath}`);
37
+ }
38
+ const python = resolvePythonCommand(env);
39
+ if (!python) {
40
+ throw new Error('Python 3 is required for Prism Browser. Set PRISM_PYTHON to a Python 3 executable.');
41
+ }
42
+ if (!args.includes('--help') && !args.includes('-h') && !hasPlaywrightRuntime(python, env)) {
43
+ throw new Error('Python Playwright is required. Run: pip3 install playwright playwright-stealth && python3 -m playwright install chromium');
44
+ }
45
+ return await new Promise((resolve, reject) => {
46
+ const child = spawn(python.executable, [...python.prefixArgs, scriptPath, ...args], { env, shell: false, stdio: 'inherit' });
47
+ child.once('error', reject);
48
+ child.once('exit', (code, signal) => {
49
+ if (signal) {
50
+ reject(new Error(`Prism Browser terminated by signal ${signal}`));
51
+ return;
52
+ }
53
+ resolve(code ?? 1);
54
+ });
55
+ });
56
+ }
package/dist/cli.js CHANGED
@@ -9,22 +9,26 @@ import { PRISM_USER_ID, SERVER_CONFIG } from './config.js';
9
9
  import { getCurrentGitState } from './utils/git.js';
10
10
  import { sessionBootstrapHandler, sessionLoadContextHandler, sessionSaveLedgerHandler, sessionSaveHandoffHandler, } from './tools/ledgerHandlers.js';
11
11
  import { configureClaudeAgentPolicy, configureClaudeNativeStartup, configureCodexAgentPolicy, configureCodexNativeStartup, configureGeminiAgentPolicy, configureGeminiNativeStartup, connectHosts, migrateLegacyClaudeHooks, migrateLegacyClaudeInstructions, migrateLegacyClaudeManagedStartup, migrateLegacyClaudeProjectMcp, normalizeHostName, } from './connect.js';
12
+ import { runBrowserCli } from './browserCli.js';
13
+ import { filterPrismMemoryContext } from './utils/memoryQuality.js';
14
+ import { isRecoverableStartupStorageError } from './utils/startupRecovery.js';
12
15
  const program = new Command();
13
16
  /** Build the stable `prism load --json` envelope from depth-specific context. */
14
17
  export function buildLoadJsonOutput(project, data, level, metadata) {
18
+ const filteredData = filterPrismMemoryContext(data);
15
19
  let history = [];
16
20
  let historyLimit = 0;
17
21
  if (level === 'standard') {
18
- history = Array.isArray(data.recent_sessions) ? data.recent_sessions : [];
22
+ history = Array.isArray(filteredData.recent_sessions) ? filteredData.recent_sessions : [];
19
23
  historyLimit = 5;
20
24
  }
21
25
  else if (level === 'deep') {
22
26
  // Canonical deep context uses session_history. Fall back only when that
23
27
  // field is absent so older portal responses remain consumable.
24
- history = Array.isArray(data.session_history)
25
- ? data.session_history
26
- : Array.isArray(data.recent_sessions)
27
- ? data.recent_sessions
28
+ history = Array.isArray(filteredData.session_history)
29
+ ? filteredData.session_history
30
+ : Array.isArray(filteredData.recent_sessions)
31
+ ? filteredData.recent_sessions
28
32
  : [];
29
33
  historyLimit = 50;
30
34
  }
@@ -32,15 +36,15 @@ export function buildLoadJsonOutput(project, data, level, metadata) {
32
36
  agent_name: metadata.agentName || null,
33
37
  handoff: [{
34
38
  project,
35
- role: metadata.effectiveRole || data.role || 'global',
36
- last_summary: data.last_summary || null,
37
- pending_todo: data.pending_todo || null,
38
- active_decisions: data.active_decisions || null,
39
- keywords: data.keywords || null,
40
- key_context: data.key_context || null,
41
- active_branch: data.active_branch || null,
42
- version: data.version ?? null,
43
- updated_at: data.updated_at || null,
39
+ role: metadata.effectiveRole || filteredData.role || 'global',
40
+ last_summary: filteredData.last_summary || null,
41
+ pending_todo: filteredData.pending_todo || null,
42
+ active_decisions: filteredData.active_decisions || null,
43
+ keywords: filteredData.keywords || null,
44
+ key_context: filteredData.key_context || null,
45
+ active_branch: filteredData.active_branch || null,
46
+ version: filteredData.version ?? null,
47
+ updated_at: filteredData.updated_at || null,
44
48
  }],
45
49
  // Keep the established key for callers while sourcing the canonical field
46
50
  // for the requested depth.
@@ -60,21 +64,46 @@ program
60
64
  .description('Prism — The Mind Palace for AI Agents')
61
65
  .version(SERVER_CONFIG.version);
62
66
  /** Print the canonical hook-free first-turn display used by the MCP tool. */
67
+ export const isRecoverableBootstrapStorageError = isRecoverableStartupStorageError;
68
+ async function printBootstrapDisplay() {
69
+ const result = await sessionBootstrapHandler({});
70
+ const output = result.content?.map((part) => part?.text).filter(Boolean).join('\n') || '';
71
+ if (!output)
72
+ throw new Error('session_bootstrap returned no display content');
73
+ console.log(output);
74
+ if (result.isError)
75
+ process.exitCode = 1;
76
+ }
63
77
  export async function runBootstrapCommand() {
78
+ const previousStorage = process.env.PRISM_STORAGE;
64
79
  try {
65
- const result = await sessionBootstrapHandler({});
66
- const output = result.content?.map((part) => part?.text).filter(Boolean).join('\n') || '';
67
- if (!output)
68
- throw new Error('session_bootstrap returned no display content');
69
- console.log(output);
70
- if (result.isError)
71
- process.exitCode = 1;
80
+ try {
81
+ await printBootstrapDisplay();
82
+ }
83
+ catch (err) {
84
+ if (!isRecoverableBootstrapStorageError(err))
85
+ throw err;
86
+ // Startup is a read-only display. If paid cloud memory is temporarily
87
+ // unavailable, retry this one read against the local last-good database
88
+ // instead of blocking the host's entire first turn. Do not persist the
89
+ // override: normal saves still use the configured subscription backend.
90
+ await closeStorage().catch(() => { });
91
+ process.env.PRISM_STORAGE = 'local';
92
+ console.error('Prism cloud startup unavailable; using local last-good context for this startup.');
93
+ await printBootstrapDisplay();
94
+ }
72
95
  }
73
96
  catch (err) {
74
97
  console.error(`Bootstrap failed: ${err instanceof Error ? err.message : String(err)}`);
75
98
  process.exitCode = 1;
76
99
  }
77
100
  finally {
101
+ if (previousStorage === undefined) {
102
+ delete process.env.PRISM_STORAGE;
103
+ }
104
+ else {
105
+ process.env.PRISM_STORAGE = previousStorage;
106
+ }
78
107
  await closeStorage().catch(() => { });
79
108
  }
80
109
  }
@@ -82,6 +111,11 @@ program
82
111
  .command('bootstrap')
83
112
  .description('Print the canonical dashboard-configured first-turn Prism greeting')
84
113
  .action(runBootstrapCommand);
114
+ // Parsed by the direct dispatch at the bottom so all Python CLI flags pass
115
+ // through unchanged. Registering it here keeps the command visible in help.
116
+ program
117
+ .command('browser')
118
+ .description('Run the packaged local Prism Browser automation CLI');
85
119
  // ─── prism connect ────────────────────────────────────────────
86
120
  // Registers this installed package with supported MCP hosts. The
87
121
  // merge is additive: an existing `prism` or `prism-mcp` entry is
@@ -834,4 +868,17 @@ program
834
868
  if (fail > 0)
835
869
  process.exit(1);
836
870
  });
837
- await program.parseAsync(process.argv);
871
+ if (process.argv[2] === 'browser') {
872
+ try {
873
+ const exitCode = await runBrowserCli(process.argv.slice(3));
874
+ if (exitCode !== 0)
875
+ process.exitCode = exitCode;
876
+ }
877
+ catch (err) {
878
+ console.error(`Prism Browser failed: ${err instanceof Error ? err.message : String(err)}`);
879
+ process.exitCode = 1;
880
+ }
881
+ }
882
+ else {
883
+ await program.parseAsync(process.argv);
884
+ }
@@ -47,12 +47,16 @@ function resolveKnowledgeScope(callerScope) {
47
47
  }
48
48
  /** Refresh JWT this many ms before expiry to avoid edge-case 401s. */
49
49
  const JWT_REFRESH_LEEWAY_MS = 60_000;
50
+ function buildContextLoadKey(project, level, userId, role) {
51
+ return JSON.stringify({ project, level, userId, role: role ?? "" });
52
+ }
50
53
  export class SynaluxStorage extends SupabaseStorage {
51
54
  baseUrl;
52
55
  refreshToken;
53
56
  cachedJwt = null;
54
57
  cachedJwtExpiresAt = 0;
55
58
  inflightExchange = null;
59
+ inflightContextLoads = new Map();
56
60
  constructor() {
57
61
  super();
58
62
  const url = process.env.PRISM_SYNALUX_BASE_URL || PRISM_SYNALUX_BASE_URL;
@@ -203,31 +207,46 @@ export class SynaluxStorage extends SupabaseStorage {
203
207
  }
204
208
  // ─── Context ─────────────────────────────────────────────────
205
209
  async loadContext(project, level, userId, role) {
206
- const result = await this.portalPost("/api/v1/prism/memory", {
207
- action: "load_context",
208
- project,
209
- level,
210
- user_id: userId,
211
- role,
212
- });
213
- // Current portals return the canonical flat `context`. Older deployed
214
- // portals returned `{ handoff, recent_sessions }`; normalize that envelope
215
- // here so the formatter can still see last_summary, TODOs, version, and
216
- // session history during a rolling portal/client upgrade.
217
- if (result.context === null)
218
- return null;
219
- if (result.context && typeof result.context === "object" && !Array.isArray(result.context)) {
220
- return result.context;
210
+ const key = buildContextLoadKey(project, level, userId, role);
211
+ const existing = this.inflightContextLoads.get(key);
212
+ if (existing)
213
+ return existing;
214
+ const request = (async () => {
215
+ const result = await this.portalPost("/api/v1/prism/memory", {
216
+ action: "load_context",
217
+ project,
218
+ level,
219
+ user_id: userId,
220
+ role,
221
+ });
222
+ // Current portals return the canonical flat `context`. Older deployed
223
+ // portals returned `{ handoff, recent_sessions }`; normalize that envelope
224
+ // here so the formatter can still see last_summary, TODOs, version, and
225
+ // session history during a rolling portal/client upgrade.
226
+ if (result.context === null)
227
+ return null;
228
+ if (result.context && typeof result.context === "object" && !Array.isArray(result.context)) {
229
+ return result.context;
230
+ }
231
+ if (result.handoff === null)
232
+ return null;
233
+ if (result.handoff && typeof result.handoff === "object" && !Array.isArray(result.handoff)) {
234
+ return {
235
+ ...result.handoff,
236
+ recent_sessions: Array.isArray(result.recent_sessions) ? result.recent_sessions : [],
237
+ };
238
+ }
239
+ return result;
240
+ })();
241
+ this.inflightContextLoads.set(key, request);
242
+ try {
243
+ return await request;
221
244
  }
222
- if (result.handoff === null)
223
- return null;
224
- if (result.handoff && typeof result.handoff === "object" && !Array.isArray(result.handoff)) {
225
- return {
226
- ...result.handoff,
227
- recent_sessions: Array.isArray(result.recent_sessions) ? result.recent_sessions : [],
228
- };
245
+ finally {
246
+ if (this.inflightContextLoads.get(key) === request) {
247
+ this.inflightContextLoads.delete(key);
248
+ }
229
249
  }
230
- return result;
231
250
  }
232
251
  // ─── Forget memory (GDPR surgical deletion) ──────────────────
233
252
  // Phase 3 Tier A: route both soft and hard delete through the
@@ -31,6 +31,7 @@ import { getCurrentGitState, getGitDrift } from "../utils/git.js";
31
31
  import { getSetting, getAllSettings, refreshConfigStorageCache } from "../storage/configStorage.js";
32
32
  import { mergeHandoff, dbToHandoffSchema, sanitizeForMerge } from "../utils/crdtMerge.js";
33
33
  import { resolveProject } from "../utils/projectResolver.js";
34
+ import { isRecoverableStartupStorageError, LOCAL_STARTUP_FALLBACK_NOTICE, } from "../utils/startupRecovery.js";
34
35
  import { PRISM_USER_ID, PRISM_AUTO_CAPTURE, PRISM_CAPTURE_PORTS } from "../config.js";
35
36
  import { captureLocalEnvironment } from "../utils/autoCapture.js";
36
37
  import { fireCaptionAsync } from "../utils/imageCaptioner.js";
@@ -264,6 +265,7 @@ function compactWithOmissionCount(value, maxChars) {
264
265
  */
265
266
  import { computeEffectiveImportance, recordMemoryAccess } from "../utils/cognitiveMemory.js";
266
267
  import { formatInferenceMetrics, resetInferenceMetrics, getInferenceSnapshot } from "../utils/inferenceMetrics.js";
268
+ import { filterPrismMemoryContext, isGreetingOnlyMemoryEntry } from "../utils/memoryQuality.js";
267
269
  export async function sessionSaveLedgerHandler(args) {
268
270
  if (!isSessionSaveLedgerArgs(args)) {
269
271
  throw new Error("Invalid arguments for session_save_ledger");
@@ -289,6 +291,16 @@ export async function sessionSaveLedgerHandler(args) {
289
291
  const files_changed = args.files_changed ? sanitizeArray(args.files_changed) : undefined;
290
292
  const decisions = args.decisions ? sanitizeArray(args.decisions) : undefined;
291
293
  const role = args.role;
294
+ if (isGreetingOnlyMemoryEntry({ summary, todos, files_changed, decisions })) {
295
+ return {
296
+ content: [{
297
+ type: "text",
298
+ text: (_saveLedgerGateWarning ? `⚠️ ${_saveLedgerGateWarning}\n\n` : "") +
299
+ "ℹ️ Greeting-only turn skipped; no ledger entry was written.",
300
+ }],
301
+ isError: false,
302
+ };
303
+ }
292
304
  const storage = await getStorage();
293
305
  // ─── Project mismatch validation (v13 — hard-rejects on mismatch) ───
294
306
  // Replaces the old soft-warning behavior that allowed cross-project
@@ -817,7 +829,7 @@ export async function sessionLoadContextHandler(args, options = {}) {
817
829
  const protectedFallbackNames = new Set(protectedFallbackEntries.map((entry) => entry.name));
818
830
  const manifestSnapshot = await resolveNativeSkillManifestSnapshot(skillSyncResult);
819
831
  const entitledSkillNames = new Set(manifestSnapshot.names);
820
- const storage = await getStorage();
832
+ const storage = options.storageOverride ?? await getStorage();
821
833
  const effectiveRole = role || await getSetting("default_role", "") || undefined;
822
834
  const loadEntitledRoleSkill = async () => {
823
835
  if (!effectiveRole)
@@ -830,11 +842,11 @@ export async function sessionLoadContextHandler(args, options = {}) {
830
842
  }
831
843
  return await getSetting(`user_skill:${effectiveRole}`, "");
832
844
  };
833
- const data = await storage.loadContext(project, level, PRISM_USER_ID, effectiveRole); // v3.0: role with dashboard fallback
845
+ const loadedData = await storage.loadContext(project, level, PRISM_USER_ID, effectiveRole); // v3.0: role with dashboard fallback
834
846
  // F4 fix: inject protected skills even for fresh projects.
835
847
  // Previously this returned before skill injection, leaving new projects with zero
836
848
  // behavioral guardrails for the entire session. Now protected skills always load.
837
- if (!data) {
849
+ if (!loadedData) {
838
850
  let freshSkillBlock = "";
839
851
  const freshLoadedSkills = new Set();
840
852
  if (includeSkillContent) {
@@ -896,6 +908,7 @@ export async function sessionLoadContextHandler(args, options = {}) {
896
908
  isError: false,
897
909
  };
898
910
  }
911
+ const data = filterPrismMemoryContext(loadedData);
899
912
  const version = data?.version;
900
913
  const versionNote = version
901
914
  ? `\n\n🔑 Session version: ${version}. Pass expected_version: ${version} when saving handoff.`
@@ -1416,11 +1429,13 @@ export async function sessionLoadContextHandler(args, options = {}) {
1416
1429
  isError: false,
1417
1430
  };
1418
1431
  }
1419
- /**
1420
- * Hook-free first-turn entrypoint used by the native prism-startup skill.
1421
- * Configuration, rather than the host model, owns project and depth selection.
1422
- */
1423
- export async function sessionBootstrapHandler(args = {}) {
1432
+ async function createLocalStartupStorage() {
1433
+ const { SqliteStorage } = await import("../storage/sqlite.js");
1434
+ const storage = new SqliteStorage();
1435
+ await storage.initialize(true);
1436
+ return storage;
1437
+ }
1438
+ export async function sessionBootstrapHandler(args = {}, options = {}) {
1424
1439
  if (typeof args !== "object" || args === null || Array.isArray(args)) {
1425
1440
  throw new Error("Invalid arguments for session_bootstrap");
1426
1441
  }
@@ -1487,39 +1502,76 @@ export async function sessionBootstrapHandler(args = {}) {
1487
1502
  : "";
1488
1503
  const omissionLength = omittedProjectsText ? omittedProjectsText.length + 2 : 0;
1489
1504
  const separatorsLength = Math.max(0, renderedProjectCount - 1) * 2;
1490
- perProjectMaxChars = Math.floor((startupMaxChars - startupHeader.length - systemReadyBlock.length - 4 - omissionLength - separatorsLength) /
1505
+ perProjectMaxChars = Math.floor((startupMaxChars - startupHeader.length - systemReadyBlock.length - LOCAL_STARTUP_FALLBACK_NOTICE.length -
1506
+ 6 - omissionLength - separatorsLength) /
1491
1507
  renderedProjectCount);
1492
1508
  if (perProjectMaxChars >= 512 || renderedProjectCount === 1)
1493
1509
  break;
1494
1510
  renderedProjectCount -= 1;
1495
1511
  }
1496
- const loaded = [];
1497
- let hadError = false;
1498
- for (const project of projects.slice(0, renderedProjectCount)) {
1499
- const result = await sessionLoadContextHandler({
1500
- project,
1501
- level: depth,
1502
- role: defaultRole || undefined,
1503
- conversation_id: conversationId,
1504
- prompt: input.prompt,
1505
- }, { includeSkillContent: false, nativeMaxChars: perProjectMaxChars, skillSyncResult });
1506
- const text = result.content?.map((part) => part?.text).filter(Boolean).join("\n") ||
1507
- `No session context found for project "${project}".`;
1508
- loaded.push(text);
1509
- hadError ||= result.isError === true;
1512
+ const renderedProjects = projects.slice(0, renderedProjectCount);
1513
+ const loadProjects = async (storageOverride) => {
1514
+ const loaded = [];
1515
+ let hadError = false;
1516
+ for (const project of renderedProjects) {
1517
+ const result = await sessionLoadContextHandler({
1518
+ project,
1519
+ level: depth,
1520
+ role: defaultRole || undefined,
1521
+ conversation_id: conversationId,
1522
+ prompt: input.prompt,
1523
+ }, {
1524
+ includeSkillContent: false,
1525
+ nativeMaxChars: perProjectMaxChars,
1526
+ skillSyncResult,
1527
+ storageOverride,
1528
+ });
1529
+ const text = result.content?.map((part) => part?.text).filter(Boolean).join("\n") ||
1530
+ `No session context found for project "${project}".`;
1531
+ loaded.push(text);
1532
+ hadError ||= result.isError === true;
1533
+ }
1534
+ return { loaded, hadError };
1535
+ };
1536
+ let localStorage = null;
1537
+ let usedLocalFallback = false;
1538
+ let startupContext;
1539
+ try {
1540
+ try {
1541
+ startupContext = await loadProjects();
1542
+ }
1543
+ catch (error) {
1544
+ if (!isRecoverableStartupStorageError(error))
1545
+ throw error;
1546
+ localStorage = await (options.localStorageFactory ?? createLocalStartupStorage)();
1547
+ usedLocalFallback = true;
1548
+ startupContext = await loadProjects(localStorage);
1549
+ }
1550
+ }
1551
+ finally {
1552
+ if (localStorage) {
1553
+ try {
1554
+ await localStorage.close();
1555
+ }
1556
+ catch (error) {
1557
+ debugLog(`[session_bootstrap] Local fallback close failed: ${error instanceof Error ? error.message : String(error)}`);
1558
+ }
1559
+ }
1510
1560
  }
1561
+ const fallbackNotice = usedLocalFallback ? `${LOCAL_STARTUP_FALLBACK_NOTICE}\n\n` : "";
1511
1562
  return {
1512
1563
  content: [{
1513
1564
  type: "text",
1514
- text: `${startupHeader}\n\n${loaded.join("\n\n")}` +
1565
+ text: `${startupHeader}\n\n${fallbackNotice}${startupContext.loaded.join("\n\n")}` +
1515
1566
  (omittedProjectsText ? `\n\n${omittedProjectsText}` : "") +
1516
1567
  `\n\n${systemReadyBlock}`,
1517
1568
  }],
1518
- isError: hadError,
1569
+ isError: startupContext.hadError,
1519
1570
  structuredContent: {
1520
1571
  conversation_id: conversationId,
1521
- projects: projects.slice(0, renderedProjectCount),
1572
+ projects: renderedProjects,
1522
1573
  depth,
1574
+ context_source: usedLocalFallback ? "local-last-good" : activeStorageBackend,
1523
1575
  },
1524
1576
  };
1525
1577
  }