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 +114 -31
- package/dist/browserCli.js +56 -0
- package/dist/cli.js +69 -22
- package/dist/storage/synalux.js +42 -23
- package/dist/tools/ledgerHandlers.js +78 -26
- package/dist/utils/memoryQuality.js +51 -0
- package/dist/utils/startupRecovery.js +13 -0
- package/docs/prism-browser.md +89 -0
- package/package.json +4 -2
- package/scripts/dev/browse.py +1345 -0
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
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
|
532
|
-
|
|
533
|
-
|
|
|
534
|
-
|
|
|
535
|
-
|
|
|
536
|
-
|
|
|
537
|
-
|
|
|
538
|
-
|
|
|
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
|
|
547
|
-
| Knowledge
|
|
548
|
-
|
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
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(
|
|
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(
|
|
25
|
-
?
|
|
26
|
-
: Array.isArray(
|
|
27
|
-
?
|
|
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 ||
|
|
36
|
-
last_summary:
|
|
37
|
-
pending_todo:
|
|
38
|
-
active_decisions:
|
|
39
|
-
keywords:
|
|
40
|
-
key_context:
|
|
41
|
-
active_branch:
|
|
42
|
-
version:
|
|
43
|
-
updated_at:
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
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
|
+
}
|
package/dist/storage/synalux.js
CHANGED
|
@@ -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
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
return
|
|
219
|
-
|
|
220
|
-
|
|
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
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
|
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 (!
|
|
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
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
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 -
|
|
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
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
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:
|
|
1572
|
+
projects: renderedProjects,
|
|
1522
1573
|
depth,
|
|
1574
|
+
context_source: usedLocalFallback ? "local-last-good" : activeStorageBackend,
|
|
1523
1575
|
},
|
|
1524
1576
|
};
|
|
1525
1577
|
}
|