Post-ship audit surfaced 6 FATALs and 4 upgrades. Shipping 5 of the 6 +
all 4 upgrades. FATAL 4 (VRAM hemorrhage from un-pooled label canvases
in createTextSprite) is pre-existing, not from this session, and scoped
separately for a proper texture-pool refactor.
**FATAL 1 — Toast Silent Lobotomy** (stores/toast.ts)
Subscriber tracked events[0] only. When Svelte batched multiple events
in one update tick (swarm firing DreamCompleted + ConnectionDiscovered
within the same millisecond), every event but the newest got silently
dropped. Fixed to walk from index 0 until hitting lastSeen — same
pattern as Graph3D.processEvents. Processes oldest-first to preserve
narrative order.
**FATAL 2 — Premature Birth** (graph/nodes.ts + graph/events.ts)
Orb flight is 138 frames; materialization was 30 frames. Node popped
fully grown ~100 frames before orb arrived — cheap UI glitch instead
of a biological birth. Added `addNode(..., { isBirthRitual: true })`
option that reserves the physics slot but hides mesh/glow/label and
skips the materializing queue. New `igniteNode(id)` flips visibility
and enqueues materialization. events.ts onArrive now calls igniteNode
at the exact docking moment, so the elastic spring-up peaks on impact.
**FATAL 3 — 120Hz ProMotion Time-Bomb** (components/Graph3D.svelte)
All physics + effect counters are frame-based. On a 120Hz display every
ritual ran at 2x speed. Added a `lastTime`-based governor in animate()
that early-returns if dt < 16ms, clamping effective rate to ~60fps.
`- (dt % 16)` carry avoids long-term drift. Zero API changes; tonight's
fast fix until physics is rewritten to use dt.
**FATAL 5 — Bezier GC Panic** (graph/effects.ts birth-orb update)
Flight phase allocated a new Vector3 (control point) and a new
QuadraticBezierCurve3 every frame per orb. With 3 orbs in flight that's
360 objects/sec for the GC to collect. Rewrote as inline algebraic
evaluation — zero allocations per frame, identical curve.
**FATAL 6 — Phantom Shockwave** (graph/events.ts)
A 166ms setTimeout fired the 2nd shockwave. If the user navigated
away during that window the scene was disposed, the timer still
fired, and .add() on a dead scene threw unhandled rejection. Dropped
the setTimeout entirely; both shockwaves fire immediately in onArrive
with different scales/colors for the same layered-crash feel.
**UPGRADE 1 — Sanhedrin Shatter** (graph/effects.ts birth-orb update)
If getTargetPos() returns undefined AFTER gestation (target node was
deleted mid-ritual — Stop hook sniping a hallucination), the orb
turns blood-red, triggers a violent implosion in place, and skips
the arrival cascade. Cognitive immune system made visible.
**UPGRADE 2 — Newton's Cradle** (graph/events.ts onArrive)
On docking the target mesh's scale gets bumped 1.8×, so the elastic
materialization + force-sim springs physically recoil instead of the
orb landing silently. The graph flinches when an idea is born into it.
**UPGRADE 3 — Hover Panic** (stores/toast.ts + InsightToast.svelte)
Paused dwell timer on mouseenter/focus, resume on mouseleave/blur.
Stored remaining ms at pause so resume schedules a correctly-sized
timer. CSS pairs via `animation-play-state: paused` on the progress
bar. A toast the user is reading no longer dismisses mid-sentence.
**UPGRADE 4 — Event Horizon Guard** (components/Graph3D.svelte)
If >MAX_EVENTS (200) events arrive in one tick, lastProcessedEvent
falls off the end of the array and the walk consumes ALL 200 entries
as "fresh" — GPU meltdown from 200 simultaneous births. Detect the
overflow and drop the batch with a console.warn, advancing the
high-water mark so next frame is normal.
Build + test:
- npm run check: 0 errors, 0 warnings
- npm test: 251/251 pass
- npm run build: clean static build
v2.3 "Terrarium" headline feature. When a MemoryCreated event arrives, a
glowing orb materialises in the cosmic center (camera-relative z=-40),
gestates for ~800ms growing from a tiny spark into a full orb, then arcs
along a dynamic quadratic Bezier curve to the live position of the real
node, and on arrival hands off to the existing RainbowBurst + Shockwave +
RippleWave cascade. The target position is re-resolved every frame so
the force simulation can move the destination during flight without the
orb losing its mark.
**New primitive — EffectManager.createBirthOrb()** (effects.ts):
Accepts a camera, a color, a live target-position getter, and an
arrival callback. Owns a sprite pair (outer halo + inner bright core),
both depthTest:false with renderOrder 999/1000 so the orb is always
visible through the starfield and the graph.
- Gestation phase: easeOutCubic growth + sinusoidal pulse, halo tints
from neutral to event color as the ritual charges.
- Flight phase: QuadraticBezierCurve3 with control point at midpoint
raised on Y by 30 + 15% of orb-to-target distance (shooting-star
arc). Sampled with easeInOutQuad. Orb shrinks ~35% approaching target.
- Arrival: fires onArrive callback once, then fades out over 8 frames
while expanding slightly (energy dispersal).
- Caller's onArrive triggers the burst cascade at arrivePos (NOT the
original spawnPos — the force sim may have moved the target during
the ritual, so we re-read nodeManager.positions on arrival).
- Dispose path integrated with existing EffectManager.dispose().
**Event pipeline fix — Graph3D.processEvents()**:
Previously tracked `processedEventCount` assuming APPEND order, but
websocket.ts PREPENDS new events (index 0) and caps the array at
MAX_EVENTS. Result: only the first MemoryCreated event after page
load fired correctly; subsequent ones reprocessed the oldest entry.
Fixed to walk from index 0 until hitting the last-processed event
by reference identity — correct regardless of array direction or
eviction pressure. Events are then processed oldest-first so causes
precede effects. Found while wiring the v2.3 demo button; would have
manifested as "first orb only" in production.
**Demo trigger** (Settings -> Birth Ritual Preview):
Button that calls websocket.injectEvent() with a synthetic
MemoryCreated event, cycling through node types (fact / concept /
pattern / decision / person / place) to showcase the type-color
mapping. Downstream consumers can't distinguish synthetic from real,
so this drives the full ritual end-to-end. Intended for demo clip
recording for the Wednesday launch.
**Test coverage:**
- events.test.ts now tests the v2.3 birth ritual path: spawns 2+
sprites in the scene immediately, and fires the full arrival
cascade after driving the effects.update() loop past the ritual
duration.
- three-mock.ts extended with Vector3.addVectors, Vector3.applyQuaternion,
Color.multiplyScalar, Quaternion, QuadraticBezierCurve3, Texture,
and Object3D.quaternion/renderOrder so production code runs unaltered
in tests.
Build + typecheck:
- npm run check: 0 errors, 0 warnings across 583 files
- npm test: 251/251 pass (net +0 from v2.2)
- npm run build: clean adapter-static output
The Sanhedrin Shatter (anti-birth ritual for hallucination veto) needs
server-side event plumbing and is deferred. Ship this as the Wednesday
visual mic-drop.
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.
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>