diff --git a/apps/dashboard/src/lib/components/DuplicateCluster.svelte b/apps/dashboard/src/lib/components/DuplicateCluster.svelte
index f414ec5..98802f2 100644
--- a/apps/dashboard/src/lib/components/DuplicateCluster.svelte
+++ b/apps/dashboard/src/lib/components/DuplicateCluster.svelte
@@ -2,7 +2,9 @@
DuplicateCluster — renders a single cosine-similarity cluster from the
`find_duplicates` MCP tool. Shows similarity bar (color-coded by severity),
stacked memory cards with type/retention/tags/date, and action controls
- (Merge all → highest-retention winner, Review → expand, Dismiss → hide).
+ (Review → expand, Dismiss → hide). The "Merge all → winner" button renders
+ ONLY when the host wires `onMerge` to a real merge action — today the merge
+ itself lives in the MCP `dedup` tool, so the dashboard page omits it.
Pure helpers live in `./duplicates-helpers.ts` and are unit-tested there.
Keep this file focused on rendering + glue.
@@ -160,17 +162,20 @@
{/each}
-
+
-
+ {#if onMerge}
+
+ {/if}
{/if}
diff --git a/apps/dashboard/src/lib/observatory/ObservatoryStage.svelte b/apps/dashboard/src/lib/observatory/ObservatoryStage.svelte
index 0c1fda4..bbe556c 100644
--- a/apps/dashboard/src/lib/observatory/ObservatoryStage.svelte
+++ b/apps/dashboard/src/lib/observatory/ObservatoryStage.svelte
@@ -20,7 +20,7 @@
import RescueVerdict from '$lib/observatory/overlays/RescueVerdict.svelte';
import ObservatoryCanvas from '$lib/components/ObservatoryCanvas.svelte';
import { DEMO_MODES, type DemoMode } from '$lib/observatory/types';
- import type { ObservatoryEngine } from '$lib/observatory/engine';
+ import type { EngineStatus, ObservatoryEngine } from '$lib/observatory/engine';
import { NodeRenderer } from '$lib/observatory/node-renderer';
import { BirthRenderer } from '$lib/observatory/birth-renderer';
import { RescueRenderer } from '$lib/observatory/rescue-renderer';
@@ -62,6 +62,14 @@
* back with its memory id (the host opens its inspector panel).
*/
onpick?: (memoryId: string) => void;
+ /**
+ * Fired when the WebGPU engine cannot run — `'gpu' in navigator` was
+ * true but requestAdapter() returned null, requestDevice() threw, or
+ * the device was lost (Linux/VM/blocklisted GPU). Hosts with a
+ * non-WebGPU renderer switch to it instead of stranding the user on
+ * the offline panel.
+ */
+ onfallback?: (reason: string) => void;
}
let {
@@ -74,7 +82,8 @@
onexit,
embedded = false,
chrome = 'full',
- onpick
+ onpick,
+ onfallback
}: Props = $props();
// GPU picking — screen px → NDC → NodeRenderer.pickAt (one readback/click).
@@ -169,6 +178,13 @@
renderer = new NodeRenderer(e);
}
+ // Adapter/device boot failure or a lost device — let the host swap to a
+ // renderer that works here (the graph page flips to the classic Three.js
+ // inspector) instead of leaving a dead field on screen.
+ function handleStatus(s: EngineStatus) {
+ if (s.state === 'unsupported' || s.state === 'error') onfallback?.(s.reason);
+ }
+
// Upload the memory field once the engine AND the graph are both ready.
$effect(() => {
if (engine && renderer && graphData && !uploaded) {
@@ -299,6 +315,7 @@
{freezeFrame}
onframe={handleFrame}
onready={handleReady}
+ onstatus={handleStatus}
/>
diff --git a/apps/dashboard/src/routes/(app)/contradictions/+page.svelte b/apps/dashboard/src/routes/(app)/contradictions/+page.svelte
index 5373045..4569c47 100644
--- a/apps/dashboard/src/routes/(app)/contradictions/+page.svelte
+++ b/apps/dashboard/src/routes/(app)/contradictions/+page.svelte
@@ -11,7 +11,6 @@
severityColor,
severityLabel,
truncate,
- uniqueMemoryCount,
avgTrustDelta as avgTrustDeltaFn,
} from '$components/contradiction-helpers';
@@ -22,6 +21,10 @@
// System-wide count from the backend, vs. the derived stats below which
// reflect only the pairs the page holds.
let totalDetected = $state(0);
+ // How many memories the backend actually scanned — it only analyzes the
+ // most recent `limit` memories, so the stat copy must say so instead of
+ // implying a whole-corpus sweep.
+ let memoriesAnalyzed = $state(0);
let loading = $state(true);
let error = $state(null);
@@ -32,10 +35,12 @@
const res = await api.contradictions();
contradictions = res.contradictions;
totalDetected = res.total;
+ memoriesAnalyzed = res.memoriesAnalyzed;
} catch (e) {
error = e instanceof Error ? e.message : 'Failed to load contradictions';
contradictions = [];
totalDetected = 0;
+ memoriesAnalyzed = 0;
} finally {
loading = false;
}
@@ -114,10 +119,10 @@
focusedPairIndex = i;
}
- // --- Stats. `totalDetected` is the backend's system-wide count; everything
- // else is derived from the pairs the page actually holds so the numbers are
+ // --- Stats. `totalDetected` + `memoriesAnalyzed` come from the backend
+ // (which scans only the most recent `limit` memories); everything else is
+ // derived from the pairs the page actually holds so the numbers are
// self-consistent with what the user sees. ---
- const totalMemoriesInvolved = $derived(uniqueMemoryCount(contradictions));
const avgTrustDelta = $derived(avgTrustDeltaFn(contradictions));
// Map filtered index -> original index in `contradictions` so the
@@ -194,7 +199,7 @@
- contradictions across {totalMemoriesInvolved.toLocaleString()} memories
+ contradictions among the {memoriesAnalyzed.toLocaleString()} most recent memories
diff --git a/apps/dashboard/src/routes/(app)/duplicates/+page.svelte b/apps/dashboard/src/routes/(app)/duplicates/+page.svelte
index 05f6d98..b1eb331 100644
--- a/apps/dashboard/src/routes/(app)/duplicates/+page.svelte
+++ b/apps/dashboard/src/routes/(app)/duplicates/+page.svelte
@@ -2,8 +2,10 @@
Memory Hygiene — Duplicate Detection
Dashboard exposure of the `find_duplicates` MCP tool. Threshold slider
(0.70-0.95) reruns cosine-similarity clustering. Each cluster renders as a
- DuplicateCluster with similarity bar, stacked memory cards, and merge /
- review / dismiss actions.
+ DuplicateCluster with similarity bar, stacked memory cards, and review /
+ dismiss actions. This page is inspection-only: the actual merge lives in
+ the MCP `dedup` tool, so no onMerge is wired here (the merge button only
+ renders when a host provides a real merge action).
-->