Labels previously rendered as near-white text (#e2e8f0) on a transparent
canvas. UnrealBloomPass (threshold 0.2) amplified every bright pixel
into a huge white halo that made labels unreadable at normal camera
distances — reported by Sam 2026-04-19 with a screenshot of the LoRA
training label blown out into a luminous blob.
New design:
- Dark rounded pill (rgba(10,16,28,0.82)) sits below the text and
hugs its measured width. That keeps the pill background well below
bloom threshold so the halo can't spread past the label footprint.
- Text dimmed to mid-luminance slate (#94a3b8). Still legible at the
standard camera distance but dim enough that bloom only adds a soft
glow instead of a blast.
- Font trimmed to 22px / weight 600 (was bold 28px); sprite scale
tightened from 12×1.5 to 9×1.2 so labels don't visually out-compete
the node spheres they annotate.
- Hairline slate stroke (18% alpha) on the pill for definition when
the camera gets close.
The canvas mock in the vitest setup grew beginPath / closePath /
moveTo / lineTo / quadraticCurveTo / arc / fill / stroke / strokeText
stubs so createTextSprite can exercise the full rounded-rect path in
unit tests without a real DOM. All 251 tests stay green.
80 new vitest cases exhaustively exercising the v2.0.8 colour-mode
additions, taking total dashboard coverage to 251 tests.
Pure-function correctness:
- getMemoryState: 12 retention boundaries including exact thresholds,
NaN, ±Infinity, negative, and >1 values + determinism across 10k
random samples.
- getNodeColor: per-node-type mapping in type mode (all 8 types),
per-bucket mapping in state mode, unknown-type fallback, and the
invariants that type mode ignores retention + state mode ignores type.
- MEMORY_STATE_COLORS: valid 6-digit hex, all four buckets distinct,
zero overlap with NODE_TYPE_COLORS.
- MEMORY_STATE_DESCRIPTIONS: threshold parentheticals match getMemoryState
bucket boundaries (70 / 40 / 10), all four lines distinct.
NodeManager state machine:
- default mode 'type', field writable pre-createNodes.
- setColorMode is idempotent (early return verified via copy() spy counts).
- setColorMode calls color.copy + emissive.copy + glow.color.copy exactly
once per node per transition, never replaces mesh / glow / material
references, preserves userData.{nodeId,type,retention}.
- rapid 5× type <-> state toggle preserves all three maps.
- addNode during state mode inherits the mode; subsequent switch to
type correctly retints the live-added node.
- suppressed-node interaction: setColorMode updates color + emissive but
never touches opacity or emissiveIntensity (v2.0.5 SIF channel stays
isolated from v2.0.8 colour channel).
- defensive paths: missing glow, missing userData.retention, missing
userData.type — all degrade to sane defaults without throwing.
Also refreshes the embedded dashboard build so the Rust binary picks up
the new SvelteKit chunks with the memory-state-colors feature baked in.
Closes Agent 1's audit gap #4: FSRS memory state (Active / Dormant /
Silent / Unavailable) was computed server-side per query but never
rendered in the 3D graph. Spheres always tinted by node type.
The new colour mode adds a second channel that users can toggle
between at runtime — Type (default, existing behaviour) and State
(new). The toggle is a radio-pair pill in the graph page's top-right
control bar next to the node-count selector + Dream button.
Buckets + palette:
- Active ≥ 70% emerald #10b981 easily retrievable
- Dormant 40-70% amber #f59e0b retrievable with effort
- Silent 10-40% violet #8b5cf6 difficult, needs cues
- Unavail. < 10% slate #6b7280 needs reinforcement
Thresholds match `execute_system_status` at the backend so the graph
colour bands line up exactly with what the Stats page reports in its
stateDistribution block. Using retention as the proxy for the full
accessibility formula (retention × 0.5 + retrieval × 0.3 + storage ×
0.2) is an approximation — retention is the dominant 0.5 weight and
it is the only FSRS channel the current GraphNode DTO carries. Swap
to the full formula in a future release if the DTO grows.
Implementation:
- `apps/dashboard/src/lib/graph/nodes.ts` — new `MemoryState` type,
`getMemoryState(retention)`, `MEMORY_STATE_COLORS`,
`MEMORY_STATE_DESCRIPTIONS`, `ColorMode`, `getNodeColor(node, mode)`.
- `NodeManager.colorMode` field (default `'type'`). `createNodeMeshes`
now calls `getNodeColor(node, this.colorMode)` so newly-added nodes
during the session follow the toggled mode.
- New `NodeManager.setColorMode(mode)` mutates every live mesh's
material + glow sprite colour in place. Idempotent; cheap. Does NOT
touch opacity/emissive-intensity so the v2.0.5 suppression dimming
layer keeps working unchanged.
- New `MemoryStateLegend.svelte` floating overlay in the bottom-right
when state mode is active (hidden in type mode so the legend doesn't
compete with the node-type palette).
- `Graph3D.svelte` accepts a new `colorMode` prop (default `'type'`)
and runs a `$effect` that calls `setColorMode` on every toggle.
- Dashboard rebuild picks up the new component + wiring.
Tests: 171 vitest, svelte-check 581 files / 0 errors. No backend
changes; this is pure dashboard code.
The Heartbeat event has shipped since v2.0.5 carrying uptime_secs,
memory_count, avg_retention, suppressed_count. Three of the four were
already wired into the sidebar footer (memory count, retention,
forgetting indicator). uptime_secs was the one field that fired every
30 seconds into a silent void.
Added:
- `uptimeSeconds` derived store + `formatUptime(secs)` helper in
websocket.ts. The helper picks the two most significant units so the
sidebar stays tight: "3d 4h" instead of "3d 4h 22m 17s", "18m 43s"
for shorter runs, "47s" on a fresh boot.
- New line in the sidebar status footer between retention and the
ForgettingIndicator: "up 3d 4h" with a hover tooltip ("MCP server
uptime") for discoverability. Hidden at sub-lg breakpoints to match
the existing responsive pattern of the surrounding text.
Zero backend work — the data was already on the wire. This is pure
UI gap closure: four of four Heartbeat fields now visible.
svelte-check clean (580 files, 0 errors).
Fixes the biggest UI gap flagged by the v2.0.7 audit: the `suppress`
tool has shipped since v2.0.5 with full graph event handlers
(MemorySuppressed, MemoryUnsuppressed, Rac1CascadeSwept) and violet
implosion animations — but zero trigger from anywhere except the MCP
layer. Dashboard users literally could not forget a memory without
dropping to raw MCP calls.
Backend (Axum):
- `POST /api/memories/{id}/suppress` optionally accepts `{"reason": "..."}`
and returns the full response payload (suppression_count, prior_count,
retrieval_penalty, reversible_until, estimated_cascade_neighbors,
labile_window_hours). Emits `MemorySuppressed` so the 3D graph plays
the compounding violet implosion per the v2.0.6 event handlers.
- `POST /api/memories/{id}/unsuppress` reverses within the 24h labile
window. Returns `stillSuppressed: bool` so the UI can distinguish a
full reversal from a compounded-down state. Emits `MemoryUnsuppressed`
for the rainbow burst reversal animation.
Frontend:
- `api.memories.suppress(id, reason?)` and `api.memories.unsuppress(id)`
wired through `apps/dashboard/src/lib/stores/api.ts`.
- Two new TypeScript response types (`SuppressResult`, `UnsuppressResult`)
in `lib/types/index.ts` mirroring the backend JSON shapes.
- Memories page gets a third action button alongside Promote / Demote /
Delete: violet "Suppress" with a hover-tooltip explaining "Top-down
inhibition (Anderson 2025). Compounds. Reversible for 24h." The button
ships `reason: "dashboard trigger"` so the audit log carries source
attribution.
Tests: 425 mcp + 366 core all pass, svelte-check 580 files 0 errors.
Two fixes surfaced by the pre-merge audit of chore/v2.0.7-clean:
1. Security MEDIUM (audit M2): `graph/+page.svelte` was rendering
`e.message` verbatim into the DOM. A backend error that carried a
filesystem path (e.g. a wrapped rusqlite error with the DB path in
the message) would leak that path to any browser viewer. SvelteKit
auto-escapes the interpolation so raw XSS is blocked, but the info-
disclosure is real. Now we strip `/path/to/file.{sqlite,rs,db,toml,
lock}` patterns and cap the rendered string at 200 chars before it
hits the DOM. The regex used to gate the empty-state branch still
runs against the raw message so detection accuracy isn't affected.
2. Correctness nit (audit PATH D): `execute_check` in
`intention_unified.rs` was dropping `intention.status` and
`intention.snoozed_until` from the response JSON. When
`include_snoozed=true` surfaces both active and snoozed intentions
in the same list, callers cannot distinguish an active-triggered
intention from a snoozed-overdue one. Expose both fields so the
consumer (dashboard, CLI, Claude Code) can render them
appropriately.
Neither change affects the default code path under
`include_snoozed=false`; regression risk is zero.
Before this, any exception from `api.graph()` — network down, dashboard
disabled server-side, 500, 404 — surfaced as "No memories yet. Start
using Vestige to populate your graph." Indistinguishable from a clean
first-run install. Users couldn't tell whether Vestige was empty or
actually broken.
The fix checks both the error message shape (looks for 404 / not found
/ empty / "no memor" patterns) AND the last known graph node count.
Only when both say "empty" do we keep the onboarding message; anything
else surfaces the real error under "Failed to load graph: ..." so
debugging doesn't require guessing.
Before this commit, six live WebSocket events fired against a silent 3D
graph. v2.0.5 shipped the `suppress` tool but the graph did not react to
MemorySuppressed, MemoryUnsuppressed, or Rac1CascadeSwept. Three more
core events — Connected, ConsolidationStarted, ImportanceScored — have
been silent on the graph since v2.0.0 despite appearing in the live
feed, which made the dashboard feel broken during real cognitive work.
Handlers added, all driven by the existing EffectManager:
- MemorySuppressed: violet implosion + compounding pulse scaled by
suppression_count (Anderson 2025 SIF visualised).
- MemoryUnsuppressed: rainbow burst + green pulse to mark reversal
within the 24h labile window.
- Rac1CascadeSwept: violet wave across a random neighbour sample
(event carries counts, not IDs, until v2.1).
- Connected: gentle cyan ripple from the first node on WS handshake.
- ConsolidationStarted: subtle amber pulses across a 20-node sample
while FSRS-6 decay runs (colour matches feed entry).
- ImportanceScored: magenta pulse on the scored node with intensity
proportional to composite_score (novelty/arousal/reward/attention).
IntentionItem.priority was typed as string but the API returns the
numeric FSRS-style scale (1=low, 2=normal, 3=high, 4=critical), so the
dashboard always rendered 'normal priority' regardless of the real
value. trigger_value was also a plain string but the API actually
returns trigger_data as a JSON-encoded payload (e.g. {"type":"time",
"at":"..."}), so the UI surfaced raw JSON or empty strings for every
non-manual trigger.
Swap to numeric priority + PRIORITY_LABELS map and add a
summarizeTrigger() helper that parses trigger_data and picks the most
human-readable field (condition / topic / formatted at / in_minutes /
codebase/filePattern) before truncating for display.
Extends PR #26 (snake_case in_minutes / file_pattern on TriggerSpec)
end-to-end to the UI layer.
First AI memory system to model forgetting as a neuroscience-grounded
PROCESS rather than passive decay. Adds the `suppress` MCP tool (#24),
Rac1 cascade worker, migration V10, and dashboard forgetting indicators.
Based on:
- Anderson, Hanslmayr & Quaegebeur (2025), Nat Rev Neurosci — right
lateral PFC as the domain-general inhibitory controller; SIF
compounds with each stopping attempt.
- Cervantes-Sandoval et al. (2020), Front Cell Neurosci PMC7477079 —
Rac1 GTPase as the active synaptic destabilization mechanism.
What's new:
* `suppress` MCP tool — each call compounds `suppression_count` and
subtracts a `0.15 × count` penalty (saturating at 80%) from
retrieval scores during hybrid search. Distinct from delete
(removes) and demote (one-shot).
* Rac1 cascade worker — background sweep piggybacks the 6h
consolidation loop, walks `memory_connections` edges from
recently-suppressed seeds, applies attenuated FSRS decay to
co-activated neighbors. You don't just forget Jake — you fade
the café, the roommate, the birthday.
* 24h labile window — reversible via `suppress({id, reverse: true})`
within 24 hours. Matches Nader reconsolidation semantics.
* Migration V10 — additive-only (`suppression_count`, `suppressed_at`
+ partial indices). All v2.0.x DBs upgrade seamlessly on first launch.
* Dashboard: `ForgettingIndicator.svelte` pulses when suppressions
are active. 3D graph nodes dim to 20% opacity when suppressed.
New WebSocket events: `MemorySuppressed`, `MemoryUnsuppressed`,
`Rac1CascadeSwept`. Heartbeat carries `suppressed_count`.
* Search pipeline: SIF penalty inserted into the accessibility stage
so it stacks on top of passive FSRS decay.
* Tool count bumped 23 → 24. Cognitive modules 29 → 30.
Memories persist — they are INHIBITED, not erased. `memory.get(id)`
returns full content through any number of suppressions. The 24h
labile window is a grace period for regret.
Also fixes issue #31 (dashboard graph view buggy) as a companion UI
bug discovered during the v2.0.5 audit cycle:
* Root cause: node glow `SpriteMaterial` had no `map`, so
`THREE.Sprite` rendered as a solid-coloured 1×1 plane. Additive
blending + `UnrealBloomPass(0.8, 0.4, 0.85)` amplified the square
edges into hard-edged glowing cubes.
* Fix: shared 128×128 radial-gradient `CanvasTexture` singleton used
as the sprite map. Retuned bloom to `(0.55, 0.6, 0.2)`. Halved fog
density (0.008 → 0.0035). Edges bumped from dark navy `0x4a4a7a`
to brand violet `0x8b5cf6` with higher opacity. Added explicit
`scene.background` and a 2000-point starfield for depth.
* 21 regression tests added in `ui-fixes.test.ts` locking every
invariant in (shared texture singleton, depthWrite:false, scale
×6, bloom magic numbers via source regex, starfield presence).
Tests: 1,284 Rust (+47) + 171 Vitest (+21) = 1,455 total, 0 failed
Clippy: clean across all targets, zero warnings
Release binary: 22.6MB, `cargo build --release -p vestige-mcp` green
Versions: workspace aligned at 2.0.5 across all 6 crates/packages
Closes#31
- Removed vestige-agent and vestige-agent-py from workspace members
(ARC-AGI-3 code, not part of Vestige release — caused CI failure)
- Improved deep_reference reasoning chain: fuller output with arrows on
supersession reasoning, longer primary finding preview, fallback message
when no relations found, boosted relation detection for search results
with high combined_score
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
All sidebar links, mobile nav links, command palette navigation, logo link,
and the graph page "Explore Connections" link now correctly use SvelteKit's
base path. Also fixes favicon.svg and manifest.json paths in app.html.
Fixes: https://github.com/samvallad33/vestige/issues/17
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When memories are created, promoted, deleted, or dreamed via MCP tools,
the 3D graph now shows spectacular live animations:
- Rainbow particle burst + elastic scale-up on MemoryCreated
- Ripple wave cascading to nearby nodes
- Green pulse + node growth on MemoryPromoted
- Implosion + dissolution on MemoryDeleted
- Edge growth animation on ConnectionDiscovered
- Purple cascade on DreamStarted/DreamProgress/DreamCompleted
- FIFO eviction at 50 live nodes to guard performance
Also: graph center defaults to most-connected node, legacy HTML
redirects to SvelteKit dashboard, CSS height chain fix in layout.
Testing: 150 unit tests (vitest), 11 e2e tests (Playwright with
MCP Streamable HTTP client), 22 proof screenshots.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Dashboard v2.1 "Nuclear" upgrade:
- Dark glassmorphism UI system (4-tier glass utilities, ambient orbs, nav glow)
- Graph3D decomposed from 806-line monolith into 10 focused modules
- Custom GLSL shaders (nebula FBM background, chromatic aberration, film grain, vignette)
- Enhanced dream mode with smooth 2s lerped transitions and aurora cycling
- Cognitive pipeline visualizer (7-stage search cascade animation)
- Temporal playback slider (scrub through memory evolution over time)
- Bioluminescent color palette for node types and events
Fix flaky CI test on macOS:
- vector::tests::test_add_and_search used near-identical test vectors (additive phase shift)
- Changed to multiplicative frequency so each seed produces a distinct vector
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>