prism-mcp-server 20.2.9 → 20.3.1
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 +50 -0
- package/dist/storage/portalContracts.js +4 -0
- package/dist/storage/synalux.js +102 -6
- package/dist/tools/graphHandlers.js +56 -6
- package/dist/tools/hygieneHandlers.js +28 -14
- package/dist/tools/ledgerHandlers.js +9 -1
- package/dist/utils/healthCheck.js +15 -1
- package/docs/prism-browser.md +86 -2
- package/package.json +1 -1
- package/scripts/dev/browse.py +1729 -622
package/README.md
CHANGED
|
@@ -61,6 +61,41 @@ features.
|
|
|
61
61
|
<details>
|
|
62
62
|
<summary>Release history (optional)</summary>
|
|
63
63
|
|
|
64
|
+
## What's New in v20.3.1
|
|
65
|
+
|
|
66
|
+
### Prism Browser Reports Real Failures
|
|
67
|
+
|
|
68
|
+
`prism browser` could not fail a test. `eval 1 === 2` returned `status: ok`
|
|
69
|
+
with exit code 0, a page serving HTTP 500 reported `status: ok`, and console
|
|
70
|
+
errors and uncaught page exceptions were discarded entirely. This release adds
|
|
71
|
+
assertions — `assert-text`, `assert-visible`, `assert-count`, `assert-url`,
|
|
72
|
+
`assert-title`, `assert-eval`, `assert-no-page-errors` — that return
|
|
73
|
+
`status: failed` and a non-zero exit. `open` now reports `http_status` and
|
|
74
|
+
fails on 400 or higher, screenshots are validated rather than assumed, and
|
|
75
|
+
`eval` returns native JSON with its type instead of a Python `repr`.
|
|
76
|
+
|
|
77
|
+
The fingerprint layer had never been applied: a wrong keyword argument made
|
|
78
|
+
the stealth library throw on every launch — 1,139 failures and 0 successes
|
|
79
|
+
since April — while the runner reported it as active. It is fixed, and a layer
|
|
80
|
+
that cannot be applied now fails loudly. The headless build no longer
|
|
81
|
+
advertises itself through `navigator.userAgentData` or the `Sec-CH-UA` header,
|
|
82
|
+
and a patch that corrupted `Object.getOwnPropertyDescriptor` on every page
|
|
83
|
+
under test has been removed. These remain best-effort test aids, not a
|
|
84
|
+
guarantee against bot detection.
|
|
85
|
+
|
|
86
|
+
`--local-only` now actually isolates: WebSocket, EventSource, WebRTC and
|
|
87
|
+
`sendBeacon` egress bypass request routing and were never blocked, and service
|
|
88
|
+
workers were allowed through. `--cleanup` was a no-op in the two modes agents
|
|
89
|
+
use. Site isolation, phishing detection and popup blocking are no longer
|
|
90
|
+
disabled by default, since these profiles hold live authenticated cookies.
|
|
91
|
+
|
|
92
|
+
New for test runs: `--ephemeral-profile` and `--storage-state` for hermetic
|
|
93
|
+
authenticated flows, `pages`/`switch-page` so OAuth popups are reachable,
|
|
94
|
+
`--fail-fast`, `--fast`, `--trace`/`--video`/`--har`, and
|
|
95
|
+
`profiles --prune-older-than` for profile maintenance.
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
64
99
|
## What's New in v20.2.7
|
|
65
100
|
|
|
66
101
|
### Session Saves Survive Agent Restarts
|
|
@@ -77,6 +112,21 @@ to the patched 8.5.23 release.
|
|
|
77
112
|
|
|
78
113
|
---
|
|
79
114
|
|
|
115
|
+
## What's New in v20.3.0
|
|
116
|
+
|
|
117
|
+
### Hybrid Memory Search (Portal Tier)
|
|
118
|
+
|
|
119
|
+
`session_search_memory` on the portal tier (Synalux-backed installs) now
|
|
120
|
+
fuses semantic similarity with exact-term lexical matching via weighted
|
|
121
|
+
reciprocal-rank fusion. Measured on blind probes against a real
|
|
122
|
+
8.5k-entry corpus: fused retrieval was **never worse** than semantic
|
|
123
|
+
alone at top-5, and exact identifiers — TPNs, function names, error
|
|
124
|
+
strings — now rescue queries that embedding similarity blurs. Results say how they were found — `hybrid retrieval`
|
|
125
|
+
headers, per-hit `sem#/lex#` arms — and a lexical-only rescue is labelled
|
|
126
|
+
`exact-term match` instead of pretending to a similarity score. Local
|
|
127
|
+
SQLite installs keep pure vector search; hybrid needs the portal's
|
|
128
|
+
lexical index.
|
|
129
|
+
|
|
80
130
|
## What's New in v20.2.6
|
|
81
131
|
|
|
82
132
|
### Safer Configuration Updates Across Every Agent
|
|
@@ -39,4 +39,8 @@ export const KnowledgeSearchResponseSchema = z.object({
|
|
|
39
39
|
action: z.literal("knowledge_search"),
|
|
40
40
|
count: z.number(),
|
|
41
41
|
results: z.array(z.record(z.string(), z.unknown())),
|
|
42
|
+
/** How the portal matched. Optional so an older portal deployment that
|
|
43
|
+
* predates the ranked search RPC still validates. When present and
|
|
44
|
+
* 'relaxed', callers MUST NOT present the rows as exact hits. */
|
|
45
|
+
match_mode: z.enum(["strict", "relaxed", "unfiltered", "none"]).optional(),
|
|
42
46
|
});
|
package/dist/storage/synalux.js
CHANGED
|
@@ -25,17 +25,30 @@
|
|
|
25
25
|
* - searchKnowledge → POST /api/v1/prism/memory action=search
|
|
26
26
|
* - softDeleteLedger → POST /api/v1/prism/memory action=forget_memory (Phase 3 Tier A)
|
|
27
27
|
* - hardDeleteLedger → POST /api/v1/prism/memory action=forget_memory (Phase 3 Tier A)
|
|
28
|
+
* - searchMemory → POST /api/v1/prism/memory action=search_memory
|
|
29
|
+
* - getHistory → POST /api/v1/prism/memory action=memory_history
|
|
30
|
+
* - patchLedger → POST /api/v1/prism/memory action=save_embedding
|
|
31
|
+
* - getEntriesMissingEmbeddings → POST /api/v1/prism/memory action=list_missing_embeddings
|
|
28
32
|
*
|
|
29
33
|
* Methods still falling through to SupabaseStorage (Phase 3 Tier B+):
|
|
30
|
-
*
|
|
31
|
-
*
|
|
34
|
+
* save_experience direct entrypoint, compactLedger, image ops,
|
|
35
|
+
* hivemind, etc. Anything in this group requires a direct SUPABASE_URL
|
|
36
|
+
* and therefore does NOT work on paid-tier installs — that is precisely
|
|
37
|
+
* how embedding writes failed silently: patchLedger was inherited, threw
|
|
38
|
+
* against a URL that is not configured, and the caller swallowed it.
|
|
39
|
+
* Before relying on an inherited method, check it is actually reachable.
|
|
40
|
+
*
|
|
41
|
+
* NOTE: this list was previously wrong — it named searchMemory and
|
|
42
|
+
* history as falling through when both had already been overridden.
|
|
43
|
+
* A stale routing map here sends the next reader down the wrong path,
|
|
44
|
+
* so amend it in the same commit that moves a method.
|
|
32
45
|
* See portal/docs/PHASE_3_PORTAL_ENDPOINTS.md for the full catalog.
|
|
33
46
|
* ═══════════════════════════════════════════════════════════════════
|
|
34
47
|
*/
|
|
35
48
|
import { SupabaseStorage } from "./supabase.js";
|
|
36
49
|
import { debugLog } from "../utils/logger.js";
|
|
37
50
|
import { PRISM_SYNALUX_BASE_URL, PRISM_SYNALUX_API_KEY } from "../config.js";
|
|
38
|
-
import { KnowledgeSearchRequestSchema } from "./portalContracts.js";
|
|
51
|
+
import { KnowledgeSearchRequestSchema, KnowledgeSearchResponseSchema } from "./portalContracts.js";
|
|
39
52
|
function resolveKnowledgeScope(callerScope) {
|
|
40
53
|
if (callerScope === "user" || callerScope === "workspace") {
|
|
41
54
|
return callerScope;
|
|
@@ -330,6 +343,8 @@ export class SynaluxStorage extends SupabaseStorage {
|
|
|
330
343
|
const result = await this.portalPost("/api/v1/prism/memory", {
|
|
331
344
|
action: "search_memory",
|
|
332
345
|
project: params.project ?? undefined,
|
|
346
|
+
// Enables the portal's hybrid lexical+semantic fusion (see interface).
|
|
347
|
+
query: params.queryText || undefined,
|
|
333
348
|
query_embedding: parsed,
|
|
334
349
|
similarity_threshold: params.similarityThreshold,
|
|
335
350
|
limit: params.limit,
|
|
@@ -358,9 +373,81 @@ export class SynaluxStorage extends SupabaseStorage {
|
|
|
358
373
|
scope: resolveKnowledgeScope(params.scope),
|
|
359
374
|
});
|
|
360
375
|
const result = await this.portalPost("/api/v1/prism/memory", wireBody);
|
|
376
|
+
// Validate the RESPONSE against the shared contract, not just the request.
|
|
377
|
+
// Before this, only the outgoing shape was checked — so a portal-side
|
|
378
|
+
// field rename would have gone unnoticed on both sides, which is exactly
|
|
379
|
+
// the 2026-05-24 class of incident this file exists to prevent.
|
|
380
|
+
// safeParse (not parse) on purpose: drift must be loud, but it must not
|
|
381
|
+
// take knowledge_search offline. The build-time contract test is what
|
|
382
|
+
// fails hard; at runtime we log and degrade to lenient extraction.
|
|
383
|
+
const validated = KnowledgeSearchResponseSchema.safeParse(result);
|
|
384
|
+
if (!validated.success) {
|
|
385
|
+
console.error("[synalux] knowledge_search response failed contract validation — " +
|
|
386
|
+
"portal and client may have drifted: " +
|
|
387
|
+
JSON.stringify(validated.error.issues.map(i => ({ path: i.path, code: i.code }))));
|
|
388
|
+
}
|
|
361
389
|
const count = typeof result.count === "number" ? result.count : 0;
|
|
362
390
|
const results = Array.isArray(result.results) ? result.results : [];
|
|
363
|
-
|
|
391
|
+
const matchMode = validated.success ? validated.data.match_mode : undefined;
|
|
392
|
+
return { count, results, match_mode: matchMode };
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Persist embedding data for an already-saved entry.
|
|
396
|
+
*
|
|
397
|
+
* MUST be overridden here. SupabaseStorage.patchLedger writes straight to
|
|
398
|
+
* Supabase via supabasePatch, which needs a direct SUPABASE_URL — not
|
|
399
|
+
* configured for paid-tier installs. Inheriting it meant every embedding
|
|
400
|
+
* write threw, and session_save_ledger's fire-and-forget catch swallowed
|
|
401
|
+
* the error while still reporting "Embedding generation queued". The
|
|
402
|
+
* result was 0 of 8,560 rows carrying an embedding and semantic search
|
|
403
|
+
* silently returning nothing.
|
|
404
|
+
*
|
|
405
|
+
* Only the vector is sent. embedding_compressed / embedding_format /
|
|
406
|
+
* embedding_turbo_radius are local-SQLite columns that do not exist on the
|
|
407
|
+
* portal schema; forwarding them would fail the whole write for fields the
|
|
408
|
+
* server has nowhere to put.
|
|
409
|
+
*/
|
|
410
|
+
async patchLedger(id, data) {
|
|
411
|
+
const raw = data.embedding;
|
|
412
|
+
if (raw === undefined || raw === null)
|
|
413
|
+
return;
|
|
414
|
+
// ledgerHandlers JSON-stringifies the vector before patching; accept both.
|
|
415
|
+
let vector = raw;
|
|
416
|
+
if (typeof raw === "string") {
|
|
417
|
+
try {
|
|
418
|
+
vector = JSON.parse(raw);
|
|
419
|
+
}
|
|
420
|
+
catch {
|
|
421
|
+
throw new Error("patchLedger: embedding string is not valid JSON");
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
if (!Array.isArray(vector)) {
|
|
425
|
+
throw new Error("patchLedger: embedding must be an array");
|
|
426
|
+
}
|
|
427
|
+
await this.portalPost("/api/v1/prism/memory", {
|
|
428
|
+
action: "save_embedding",
|
|
429
|
+
memory_id: id,
|
|
430
|
+
embedding: vector,
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* Rows semantic search cannot find, read through the portal.
|
|
435
|
+
*
|
|
436
|
+
* The backfill tool used to reach these via inherited getLedgerEntries —
|
|
437
|
+
* a direct Supabase read paid-tier installs cannot make (NXDOMAIN), so the
|
|
438
|
+
* repair tool could never see what needed repairing. The portal endpoint
|
|
439
|
+
* ignores cursorId (it always returns the oldest missing rows, and rows
|
|
440
|
+
* gain embeddings as the backfill proceeds, so the frontier advances by
|
|
441
|
+
* itself); it is accepted here to satisfy the shared signature.
|
|
442
|
+
*/
|
|
443
|
+
async getEntriesMissingEmbeddings(params) {
|
|
444
|
+
const result = await this.portalPost("/api/v1/prism/memory", {
|
|
445
|
+
action: "list_missing_embeddings",
|
|
446
|
+
limit: params.limit,
|
|
447
|
+
...(params.project ? { project: params.project } : {}),
|
|
448
|
+
});
|
|
449
|
+
const entries = Array.isArray(result.entries) ? result.entries : [];
|
|
450
|
+
return entries;
|
|
364
451
|
}
|
|
365
452
|
// ─── Time Travel ─────────────────────────────────────────────
|
|
366
453
|
// Phase 3 Tier B: route memory_history through portal instead of
|
|
@@ -397,8 +484,15 @@ export class SynaluxStorage extends SupabaseStorage {
|
|
|
397
484
|
const inventory = result.inventory;
|
|
398
485
|
const totalActiveEntries = typeof inventory?.ledger_entries === "number" ? inventory.ledger_entries : 0;
|
|
399
486
|
const totalHandoffs = typeof inventory?.active_projects === "number" ? inventory.active_projects : 0;
|
|
487
|
+
// Hardcoding 0 here certified a 100%-missing-embeddings outage as
|
|
488
|
+
// "HEALTHY — all clean". Use the portal's real count; if the portal
|
|
489
|
+
// predates the field, report -1 so healthCheck can say "unknown"
|
|
490
|
+
// instead of lying in either direction.
|
|
491
|
+
const missingEmbeddings = typeof inventory?.ledger_missing_embeddings === "number"
|
|
492
|
+
? inventory.ledger_missing_embeddings
|
|
493
|
+
: -1;
|
|
400
494
|
return {
|
|
401
|
-
missingEmbeddings
|
|
495
|
+
missingEmbeddings,
|
|
402
496
|
activeLedgerSummaries: [],
|
|
403
497
|
orphanedHandoffs: [],
|
|
404
498
|
staleRollups: 0,
|
|
@@ -411,7 +505,9 @@ export class SynaluxStorage extends SupabaseStorage {
|
|
|
411
505
|
catch (e) {
|
|
412
506
|
debugLog("[SynaluxStorage] getHealthStats failed: " + (e instanceof Error ? e.message : String(e)));
|
|
413
507
|
return {
|
|
414
|
-
|
|
508
|
+
// Portal unreachable: coverage is UNKNOWN, not zero. -1 makes the
|
|
509
|
+
// health check say so rather than certify blind.
|
|
510
|
+
missingEmbeddings: -1,
|
|
415
511
|
activeLedgerSummaries: [],
|
|
416
512
|
orphanedHandoffs: [],
|
|
417
513
|
staleRollups: 0,
|
|
@@ -57,6 +57,56 @@ import { ConceptDictionary } from "../sdm/conceptDictionary.js";
|
|
|
57
57
|
import { PolicyGateway } from "../sdm/policyGateway.js";
|
|
58
58
|
import { getSdmEngine } from "../sdm/sdmEngine.js";
|
|
59
59
|
import { PRISM_HDC_ENABLED, PRISM_HDC_EXPLAINABILITY_ENABLED, PRISM_HDC_POLICY_FALLBACK_THRESHOLD, PRISM_HDC_POLICY_CLARIFY_THRESHOLD, } from "../config.js";
|
|
60
|
+
/**
|
|
61
|
+
* Header line for knowledge_search results.
|
|
62
|
+
*
|
|
63
|
+
* A 'relaxed' match_mode means no entry matched every query term — the portal
|
|
64
|
+
* widened the search and returned the closest entries. Wording those the same
|
|
65
|
+
* as an exact hit is how a best-effort guess gets read as a confirmed answer,
|
|
66
|
+
* so the distinction is stated in the text the agent actually sees.
|
|
67
|
+
*
|
|
68
|
+
* Exported so the wording is covered by a test that exercises this function
|
|
69
|
+
* rather than a copy of it.
|
|
70
|
+
*/
|
|
71
|
+
export function formatKnowledgeHeader(resultCount, matchMode) {
|
|
72
|
+
if (matchMode === "relaxed") {
|
|
73
|
+
return `🧠 No exact match. ${resultCount} closest ${resultCount === 1 ? "entry" : "entries"} ` +
|
|
74
|
+
`(widened search — treat as leads, not confirmed answers):`;
|
|
75
|
+
}
|
|
76
|
+
return `🧠 Found ${resultCount} knowledge entries:`;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Header + per-hit scoring for session_search_memory results.
|
|
80
|
+
*
|
|
81
|
+
* Exported so wording is covered by tests that call the shipped functions —
|
|
82
|
+
* a lesson from the knowledge_search match_mode fix, whose first tests
|
|
83
|
+
* mirrored the logic and would have passed while the handler regressed.
|
|
84
|
+
*
|
|
85
|
+
* Hybrid detection is per-row: portal fusion (weighted RRF, measured 59%
|
|
86
|
+
* blind hit@1 vs 45% semantic-only) annotates each row with semantic_rank /
|
|
87
|
+
* lexical_rank. Any row carrying lexical_rank means the lexical arm ran, so
|
|
88
|
+
* calling the results "semantically similar" would misstate how they were
|
|
89
|
+
* found — and a lexical-only rescue has NO similarity score at all, which
|
|
90
|
+
* previously rendered as "N/A similar".
|
|
91
|
+
*/
|
|
92
|
+
export function isHybridSearchResults(results) {
|
|
93
|
+
return results.some((r) => r?.lexical_rank !== undefined && r?.lexical_rank !== null);
|
|
94
|
+
}
|
|
95
|
+
export function searchResultsHeader(count, hybrid) {
|
|
96
|
+
return hybrid
|
|
97
|
+
? `🧠 Found ${count} matching sessions (hybrid retrieval — semantic meaning + exact terms):`
|
|
98
|
+
: `🧠 Found ${count} semantically similar sessions:`;
|
|
99
|
+
}
|
|
100
|
+
export function formatHitScore(r) {
|
|
101
|
+
const sim = typeof r.similarity === "number" ? `${(r.similarity * 100).toFixed(1)}% similar` : null;
|
|
102
|
+
const sem = r.semantic_rank !== undefined && r.semantic_rank !== null ? `sem#${r.semantic_rank + 1}` : null;
|
|
103
|
+
const lex = r.lexical_rank !== undefined && r.lexical_rank !== null ? `lex#${r.lexical_rank + 1}` : null;
|
|
104
|
+
if (sem || lex) {
|
|
105
|
+
const arms = [sem, lex].filter(Boolean).join(" + ");
|
|
106
|
+
return sim ? `${sim} (${arms})` : `exact-term match (${arms})`;
|
|
107
|
+
}
|
|
108
|
+
return sim ?? "N/A similar";
|
|
109
|
+
}
|
|
60
110
|
export async function knowledgeSearchHandler(args) {
|
|
61
111
|
if (!isKnowledgeSearchArgs(args)) {
|
|
62
112
|
throw new Error("Invalid arguments for knowledge_search");
|
|
@@ -136,7 +186,7 @@ export async function knowledgeSearchHandler(args) {
|
|
|
136
186
|
// Phase 1: Wrap in contentBlocks array for optional trace attachment
|
|
137
187
|
const contentBlocks = [{
|
|
138
188
|
type: "text",
|
|
139
|
-
text:
|
|
189
|
+
text: `${formatKnowledgeHeader(resultCount, data.match_mode)}\n\n${JSON.stringify(data.results, null, 2)}`,
|
|
140
190
|
}];
|
|
141
191
|
// Phase 1: Attach MemoryTrace with strategy="keyword" and timing data
|
|
142
192
|
if (enable_trace) {
|
|
@@ -377,6 +427,8 @@ export async function sessionSearchMemoryHandler(args) {
|
|
|
377
427
|
: Math.min(limit, 20);
|
|
378
428
|
const results = await storage.searchMemory({
|
|
379
429
|
queryEmbedding: JSON.stringify(queryEmbedding),
|
|
430
|
+
// Portal-backed installs fuse this with lexical search (weighted RRF).
|
|
431
|
+
queryText: query,
|
|
380
432
|
project: project || null,
|
|
381
433
|
limit: candidateLimit,
|
|
382
434
|
similarityThreshold: similarity_threshold,
|
|
@@ -516,9 +568,7 @@ export async function sessionSearchMemoryHandler(args) {
|
|
|
516
568
|
}
|
|
517
569
|
// Format results with similarity scores + effective importance + ACT-R
|
|
518
570
|
const formatted = results.map((r, i) => {
|
|
519
|
-
const simScore =
|
|
520
|
-
? `${(r.similarity * 100).toFixed(1)}%`
|
|
521
|
-
: "N/A";
|
|
571
|
+
const simScore = formatHitScore(r);
|
|
522
572
|
// Dynamic importance decay (uses ACT-R internally when enabled)
|
|
523
573
|
const baseImportance = r.importance ?? 0;
|
|
524
574
|
const effectiveImportance = computeEffectiveImportance(baseImportance, r.last_accessed_at, r.created_at, Boolean(r.is_rollup));
|
|
@@ -529,7 +579,7 @@ export async function sessionSearchMemoryHandler(args) {
|
|
|
529
579
|
const actrStr = r._actr_composite !== undefined
|
|
530
580
|
? ` ACT-R: composite=${r._actr_composite.toFixed(3)} (B=${r._actr_Bi?.toFixed(2)}, S=${r._actr_Si?.toFixed(3)})\n`
|
|
531
581
|
: "";
|
|
532
|
-
return `[${i + 1}] ${simScore}
|
|
582
|
+
return `[${i + 1}] ${simScore} — ${r.session_date || "unknown date"}\n` +
|
|
533
583
|
` Project: ${r.project}\n` +
|
|
534
584
|
` Summary: ${r.summary}\n` +
|
|
535
585
|
importanceStr +
|
|
@@ -540,7 +590,7 @@ export async function sessionSearchMemoryHandler(args) {
|
|
|
540
590
|
// Phase 1: content[0] = human-readable results (unchanged from pre-Phase 1)
|
|
541
591
|
const contentBlocks = [{
|
|
542
592
|
type: "text",
|
|
543
|
-
text:
|
|
593
|
+
text: `${searchResultsHeader(results.length, isHybridSearchResults(results))}\n\n${formatted}`,
|
|
544
594
|
}];
|
|
545
595
|
// Phase 1: content[1] = machine-readable MemoryTrace (only when enable_trace=true)
|
|
546
596
|
// topScore is read from results[0].similarity — this is the cosine distance
|
|
@@ -60,22 +60,36 @@ export async function backfillEmbeddingsHandler(args) {
|
|
|
60
60
|
debugLog(`[backfill_embeddings] ${dry_run ? "DRY RUN: " : ""}` +
|
|
61
61
|
`project=${project || "all"}, limit=${safeLimit}`);
|
|
62
62
|
const storage = await getStorage();
|
|
63
|
-
// Find entries missing embeddings
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
63
|
+
// Find entries missing embeddings. Prefer the dedicated method: the old
|
|
64
|
+
// PostgREST-param path below goes through getLedgerEntries, which
|
|
65
|
+
// SynaluxStorage inherits as a DIRECT Supabase read — paid-tier installs
|
|
66
|
+
// have no SUPABASE_URL, so that read dies with NXDOMAIN and this tool
|
|
67
|
+
// could never see what it was supposed to repair.
|
|
68
|
+
let entries;
|
|
69
|
+
if (typeof storage.getEntriesMissingEmbeddings === "function") {
|
|
70
|
+
entries = await storage.getEntriesMissingEmbeddings({
|
|
71
|
+
limit: safeLimit,
|
|
72
|
+
...(project ? { project } : {}),
|
|
73
|
+
...(args._cursor_id ? { cursorId: args._cursor_id } : {}),
|
|
74
|
+
});
|
|
74
75
|
}
|
|
75
|
-
|
|
76
|
-
params
|
|
76
|
+
else {
|
|
77
|
+
const params = {
|
|
78
|
+
"embedding": "is.null",
|
|
79
|
+
"archived_at": "is.null",
|
|
80
|
+
user_id: `eq.${PRISM_USER_ID}`,
|
|
81
|
+
order: "id.asc",
|
|
82
|
+
limit: String(safeLimit),
|
|
83
|
+
select: "id,summary,decisions,project",
|
|
84
|
+
};
|
|
85
|
+
if (args._cursor_id) {
|
|
86
|
+
params.id = `gt.${args._cursor_id}`;
|
|
87
|
+
}
|
|
88
|
+
if (project) {
|
|
89
|
+
params.project = `eq.${project}`;
|
|
90
|
+
}
|
|
91
|
+
entries = await storage.getLedgerEntries(params);
|
|
77
92
|
}
|
|
78
|
-
const entries = await storage.getLedgerEntries(params);
|
|
79
93
|
if (entries.length === 0) {
|
|
80
94
|
return {
|
|
81
95
|
content: [{
|
|
@@ -627,6 +627,7 @@ export async function sessionSaveHandoffHandler(args, server) {
|
|
|
627
627
|
// Await the attempt so short-lived CLI/MCP transports cannot exit before the
|
|
628
628
|
// snapshot reaches storage. Failure stays non-fatal because the primary
|
|
629
629
|
// handoff has already been durably saved.
|
|
630
|
+
let historySnapshotSaved = false;
|
|
630
631
|
if (data.status === "created" || data.status === "updated") {
|
|
631
632
|
const snapshotEntry = {
|
|
632
633
|
project,
|
|
@@ -637,10 +638,12 @@ export async function sessionSaveHandoffHandler(args, server) {
|
|
|
637
638
|
keywords: keywords ?? null,
|
|
638
639
|
key_context: key_context ?? null,
|
|
639
640
|
active_branch: active_branch ?? null,
|
|
641
|
+
role: effectiveRole,
|
|
640
642
|
version: newVersion,
|
|
641
643
|
};
|
|
642
644
|
try {
|
|
643
|
-
await storage.saveHistorySnapshot(snapshotEntry);
|
|
645
|
+
await storage.saveHistorySnapshot(snapshotEntry, active_branch ?? "main");
|
|
646
|
+
historySnapshotSaved = true;
|
|
644
647
|
}
|
|
645
648
|
catch (err) {
|
|
646
649
|
console.error(`[session_save_handoff] History snapshot failed (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -800,12 +803,16 @@ export async function sessionSaveHandoffHandler(args, server) {
|
|
|
800
803
|
console.error("[FactMerger] Module load failed (non-fatal): " + err));
|
|
801
804
|
}
|
|
802
805
|
const metricsBlock = formatInferenceMetrics();
|
|
806
|
+
const historySnapshotLine = historySnapshotSaved
|
|
807
|
+
? `🕘 Versioned history snapshot saved.\n`
|
|
808
|
+
: `⚠️ Primary handoff saved, but its versioned history snapshot was not saved. Check memory_history before relying on time travel.\n`;
|
|
803
809
|
// Build response text based on whether a CRDT merge occurred
|
|
804
810
|
const responseText = (_saveHandoffGateWarning ? `⚠️ ${_saveHandoffGateWarning}\n\n` : "") +
|
|
805
811
|
(isMerged
|
|
806
812
|
? `🔄 Auto-merged conflict for "${project}" (v${expected_version} → v${newVersion})\n` +
|
|
807
813
|
`Strategy: ${JSON.stringify(mergeStrategy)}\n` +
|
|
808
814
|
(last_summary ? `Summary: ${last_summary}\n` : "") +
|
|
815
|
+
historySnapshotLine +
|
|
809
816
|
metricsBlock +
|
|
810
817
|
`\n🔑 Remember: pass expected_version: ${newVersion} on your next save ` +
|
|
811
818
|
`to maintain concurrency control.`
|
|
@@ -814,6 +821,7 @@ export async function sessionSaveHandoffHandler(args, server) {
|
|
|
814
821
|
(last_summary ? `Last summary: ${last_summary}\n` : "") +
|
|
815
822
|
(open_todos?.length ? `Open TODOs: ${open_todos.length} items\n` : "") +
|
|
816
823
|
(active_branch ? `Active branch: ${active_branch}\n` : "") +
|
|
824
|
+
historySnapshotLine +
|
|
817
825
|
(embeddingQueued
|
|
818
826
|
? `📊 Embedding generation queued for semantic search.\n`
|
|
819
827
|
: `📊 Primary history saved; optional semantic indexing was not queued.\n`) +
|
|
@@ -217,7 +217,21 @@ export function runHealthCheck(stats) {
|
|
|
217
217
|
severity: stats.missingEmbeddings > 10 ? "error" : "warning", // >10 = critical
|
|
218
218
|
message: `${stats.missingEmbeddings} ledger entries have no embedding vector`,
|
|
219
219
|
count: stats.missingEmbeddings, // how many are affected
|
|
220
|
-
suggestion: "Run
|
|
220
|
+
suggestion: "Run session_backfill_embeddings to generate the missing vectors",
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
else if (stats.missingEmbeddings < 0) {
|
|
224
|
+
// -1 = coverage is UNKNOWN (portal unreachable, or a portal that predates
|
|
225
|
+
// the ledger_missing_embeddings field). Saying nothing here is how a
|
|
226
|
+
// 100%-missing outage was certified "HEALTHY — all clean": the storage
|
|
227
|
+
// layer hardcoded 0 and this check had no way to distinguish "verified
|
|
228
|
+
// zero" from "never looked".
|
|
229
|
+
issues.push({
|
|
230
|
+
check: "missing_embeddings",
|
|
231
|
+
severity: "warning",
|
|
232
|
+
message: "Embedding coverage could not be verified (portal did not report a count)",
|
|
233
|
+
count: 0,
|
|
234
|
+
suggestion: "Update the Synalux portal, or check connectivity, then re-run session_health_check",
|
|
221
235
|
});
|
|
222
236
|
}
|
|
223
237
|
// ── Check 2: Duplicate Entries ─────────────────────────────
|
package/docs/prism-browser.md
CHANGED
|
@@ -18,16 +18,100 @@ so a separate app or DMG is not required.
|
|
|
18
18
|
scripts, allowing deterministic feature flags, fixtures, capability shims,
|
|
19
19
|
or instrumentation for localhost tests.
|
|
20
20
|
- **A constrained injection boundary.** Injection requires `--local-only`,
|
|
21
|
-
which rejects public navigation
|
|
21
|
+
which rejects public navigation, non-loopback subrequests, service workers,
|
|
22
|
+
and WebSocket/EventSource/WebRTC egress.
|
|
22
23
|
- **Private audit records.** The runner stores a local audit trail with private
|
|
23
24
|
filesystem permissions and removes URL credentials, query strings,
|
|
24
|
-
fragments, common email/phone patterns,
|
|
25
|
+
fragments, common email/phone patterns, record identifiers in URL paths, and
|
|
26
|
+
injected source text.
|
|
27
|
+
- **Failure signals instead of silence.** Console errors, uncaught page
|
|
28
|
+
exceptions, and failed requests are captured and attached to command output.
|
|
29
|
+
An HTTP status of 400 or higher, an empty screenshot frame, and a failed
|
|
30
|
+
assertion each produce a non-zero exit.
|
|
25
31
|
|
|
26
32
|
These are orchestration and safety benefits. Prism Browser does not replace
|
|
27
33
|
Playwright Test: use raw Playwright when you need its complete fixture,
|
|
28
34
|
assertion, trace, project, or parallel-worker APIs. Compatibility patches are
|
|
29
35
|
best effort and are not a CAPTCHA-bypass guarantee.
|
|
30
36
|
|
|
37
|
+
## Assertions and exit codes
|
|
38
|
+
|
|
39
|
+
A command reports `status: "ok"` only when it actually succeeded. Anything else
|
|
40
|
+
(`failed`, `error`, `timeout`) marks the run, and `pipe` exits non-zero.
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
printf '%s\n' \
|
|
44
|
+
'open http://127.0.0.1:3000' \
|
|
45
|
+
'assert-title Dashboard' \
|
|
46
|
+
'assert-text #status Ready' \
|
|
47
|
+
'assert-count [data-row] 12' \
|
|
48
|
+
'assert-no-page-errors' \
|
|
49
|
+
| prism browser --headless --fast --fail-fast pipe
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
`eval` returns the native JSON value with its type, so results are machine
|
|
53
|
+
readable:
|
|
54
|
+
|
|
55
|
+
```json
|
|
56
|
+
{"status": "ok", "result": {"a": 1}, "type": "dict", "serializable": true}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
`assert-eval` is the assertion form — `eval` alone never fails a run, because a
|
|
60
|
+
falsy expression is a legitimate result. Use `--fail-fast` for test runs so a
|
|
61
|
+
failed navigation cannot be followed by commands that silently target the
|
|
62
|
+
previous page.
|
|
63
|
+
|
|
64
|
+
A trailing backslash continues a command onto the next line, which makes
|
|
65
|
+
multi-line JavaScript and multi-line input text expressible. Quoted arguments
|
|
66
|
+
are parsed with shell-style quoting, so `type "div > .cell" "two words"` works.
|
|
67
|
+
|
|
68
|
+
## Hermetic runs
|
|
69
|
+
|
|
70
|
+
Persistent profiles are convenient for interactive work and wrong for tests:
|
|
71
|
+
state carries between runs and makes them order dependent. For test runs use
|
|
72
|
+
|
|
73
|
+
- `--ephemeral-profile` — a throwaway profile directory, removed on exit.
|
|
74
|
+
- `--storage-state PATH` — seed cookies and localStorage from a Playwright
|
|
75
|
+
storage-state file, so authenticated flows skip the login UI.
|
|
76
|
+
- `save-storage PATH` — write the current state back out.
|
|
77
|
+
- `--fast` — skip the human-latency emulation (per-character typing and
|
|
78
|
+
inter-action pauses), which otherwise dominates a test's wall clock.
|
|
79
|
+
|
|
80
|
+
`--trace PATH`, `--video DIR`, and `--har PATH` record Playwright artifacts for
|
|
81
|
+
a failing run.
|
|
82
|
+
|
|
83
|
+
## Multiple pages
|
|
84
|
+
|
|
85
|
+
A popup (OAuth, payment, print preview) opens a new page. `pages` lists them,
|
|
86
|
+
`switch-page N` makes one active, and `close-page` disposes of it. Without
|
|
87
|
+
switching, commands continue to target the original page.
|
|
88
|
+
|
|
89
|
+
## Fingerprint patches
|
|
90
|
+
|
|
91
|
+
`--stealth full` applies playwright-stealth, a CDP
|
|
92
|
+
`Emulation.setUserAgentOverride` carrying full `userAgentMetadata`, and a
|
|
93
|
+
supplementary init script. The CDP override is what keeps `navigator.userAgent`,
|
|
94
|
+
`navigator.userAgentData`, and the `Sec-CH-UA` request headers consistent;
|
|
95
|
+
rewriting headers through request interception does not work, because Chromium
|
|
96
|
+
re-adds client hints after interception.
|
|
97
|
+
|
|
98
|
+
Applied layers are verified rather than assumed. The runner probes the page for
|
|
99
|
+
UA/brand/platform/webdriver agreement at startup and again after the first real
|
|
100
|
+
navigation, and `--stealth full` fails with an actionable error when a requested
|
|
101
|
+
layer cannot be applied. Pass `--allow-degraded-stealth` to proceed anyway, or
|
|
102
|
+
`--stealth light` to skip the library layer deliberately. The `fingerprint`
|
|
103
|
+
command reports the current state.
|
|
104
|
+
|
|
105
|
+
These patches do not disable site isolation, client-side phishing detection, or
|
|
106
|
+
popup blocking. Profiles hold live authenticated cookies, and trading away those
|
|
107
|
+
mitigations for a fingerprint delta is the wrong exchange.
|
|
108
|
+
|
|
109
|
+
## Profile maintenance
|
|
110
|
+
|
|
111
|
+
Persistent profiles accumulate. `prism browser profiles` lists them by size and
|
|
112
|
+
age; `--prune-older-than DAYS` reports what is stale and deletes it only when
|
|
113
|
+
`--yes` is also passed.
|
|
114
|
+
|
|
31
115
|
## Install the local runtime
|
|
32
116
|
|
|
33
117
|
```bash
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "prism-mcp-server",
|
|
3
|
-
"version": "20.
|
|
3
|
+
"version": "20.3.1",
|
|
4
4
|
"mcpName": "io.github.dcostenco/prism-coder",
|
|
5
5
|
"description": "Prism Coder — Cognitive memory + tool-calling intelligence for AI agents. Mind Palace persistent memory (BFCL Gold Certified, 100% Tool-Call Accuracy, 114 Agent Skills, PHI Guard, Tier Enforcement, Prompt-Based Skill Routing, Zero-Search HDC/HRR retrieval, HRR Semantic Drift Detection across BCBA/Coding/AAC domains, HIPAA-hardened local or subscription-gated Synalux storage, SLERP-optimized GRPO alignment) plus the prism-coder 1.7B–32B open-weights LLM fleet.",
|
|
6
6
|
"module": "index.ts",
|