From fd35e98aaf9df17d47c5a4e13077a9885b901b82 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 8 May 2026 01:20:47 +0000 Subject: [PATCH 01/12] =?UTF-8?q?Add=20Layer=20101=20=E2=80=94=20Episodic?= =?UTF-8?q?=20Cortex=20(vault=20that=20talks=20back)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cannibalized from breferrari/obsidian-mind (taxonomy + lifecycle hooks) and @cyrilxbt's "second brain" architecture (Daily Brief / Weekly Synthesis prompts), but BrainSNN-native: every capture flows through the Layer 4 firewall, Layer 29 affect decoder, and Layer 87 genre classifier before landing — and the merged region heatmap drives the 3D cortex instead of just filing into a folder. What ships: - 8-category episodic taxonomy with regex triggers + per-category brain-region affinity (decision/insight/question/artifact/win/ project/person/incident). - routeCapture() pipeline + persistent episodic memory store with on-demand MiniLM embeddings (Layer 24), cosine findSimilar, and union-find cluster mining. - Synthesis engine: dailyBrief() returns connections / pattern / question; weeklySynthesis() returns thesis / contradictions / knowledge gaps / one action. Local deterministic path uses the pre-computed BrainSNN signals; Gemma path layers a system prompt that forces the LLM to reference those signals; falls back on any error. - EpisodicCortexPanel: capture form, 8-category filter pills, search, recent timeline, brief + synthesis cards, warm-embeddings button, export/import bundle, wipe. - episodicDream.js subscribes to Layer 26's dream phase and every 4 cycles runs consolidationPass over the last 30d of captures — STDP-reinforces every connectome edge touching the cluster's dominant region. Captures gain a ◐N consolidation counter. - 4 new MCP tools (episodic_capture / episodic_brief / episodic_synthesis / episodic_list) so Claude Code / Codex agents on the WS relay can drive the layer programmatically. Why deeper than Obsidian: connections aren't a graph plugin — they are the physics of the cortex. Embedded out of the box, manipulation-pressure-scored before they pollute the vault, affect-tagged, region-mapped, and STDP-rehearsed during idle Dream Mode. https://claude.ai/code/session_01V5jAHiMiD5biJm3MJKNPfr --- .ai-memory/MEMORY.md | 43 ++ brainsnn-r3f-app/src/App.jsx | 19 +- .../src/components/EpisodicCortexPanel.jsx | 544 ++++++++++++++++++ brainsnn-r3f-app/src/data/episodicTaxonomy.js | 169 ++++++ brainsnn-r3f-app/src/utils/episodicDream.js | 117 ++++ brainsnn-r3f-app/src/utils/episodicMemory.js | 447 ++++++++++++++ brainsnn-r3f-app/src/utils/episodicRouter.js | 298 ++++++++++ .../src/utils/episodicSynthesis.js | 517 +++++++++++++++++ brainsnn-r3f-app/src/utils/layerCatalog.js | 1 + brainsnn-r3f-app/src/utils/mcpBridge.js | 102 ++++ 10 files changed, 2256 insertions(+), 1 deletion(-) create mode 100644 brainsnn-r3f-app/src/components/EpisodicCortexPanel.jsx create mode 100644 brainsnn-r3f-app/src/data/episodicTaxonomy.js create mode 100644 brainsnn-r3f-app/src/utils/episodicDream.js create mode 100644 brainsnn-r3f-app/src/utils/episodicMemory.js create mode 100644 brainsnn-r3f-app/src/utils/episodicRouter.js create mode 100644 brainsnn-r3f-app/src/utils/episodicSynthesis.js diff --git a/.ai-memory/MEMORY.md b/.ai-memory/MEMORY.md index cfe0846..2734c9c 100644 --- a/.ai-memory/MEMORY.md +++ b/.ai-memory/MEMORY.md @@ -697,3 +697,46 @@ Club Penguin-style AI debate arena live at https://penguinwalk.co per-user immunity / streak / scan-count / receipt stats, and lists the four ways to navigate (⌘K / Shift-? / Role Tour / Layer Explorer). The "you made it here" panel. +101. Episodic Cortex — vault that talks back (cannibalized from + breferrari/obsidian-mind + @cyrilxbt's "second brain" guide) + - 8-category episodic taxonomy: decision / insight / question / + artifact / win / project / person / incident, each with regex + triggers + brain-region affinity vector. data/episodicTaxonomy.js + - routeCapture(text) orchestrates the full pipeline per note: + Layer 4 firewall + Layer 29 affect decoder + Layer 87 genre + classifier + episodic taxonomy → merged region heatmap that + drives the 3D brain. utils/episodicRouter.js + - episodicMemory.js: persistent log (brainsnn_episodic_v1, cap + 800), MiniLM embeddings (separate brainsnn_episodic_emb_v1 + slot), findSimilar / mineClusters via cosine + union-find, + addCapture / addInsight / deleteCapture / togglePinned / + export+import bundle. Synthesis insights are saved back as + first-class captures (kind: 'insight') — the vault literally + talks to itself. + - episodicSynthesis.js: dailyBrief() returns connections / + pattern / question; weeklySynthesis() returns emerging + thesis / contradictions / knowledge gaps / one action. Local + path uses cluster mining + valence pairs + open-question + detection; Gemma path adds a system prompt that forces the + LLM to reference the pre-computed BrainSNN signals. Falls + back to local on Gemma error. + - episodicDream.js subscribes to Layer 26 dream phase and every + 4 cycles runs consolidationPass over captures from the last + 30d. Dominant region of each cluster STDP-reinforces every + connectome edge that touches it. markConsolidated() decorates + participating captures so the timeline shows ◐N counters. + - EpisodicCortexPanel: capture form + 4 example seeds + 8 + category filter pills + search + recent timeline + Daily + Brief / Weekly Synthesis cards + warm-embeddings + export / + import / wipe. Dropping any capture into the panel pushes a + blended region overlay into the live brain via setState. + - MCP tools (Layer 19): episodic_capture, episodic_brief, + episodic_synthesis, episodic_list — agents on the WebSocket + relay can now feed captures into and synthesize from the + Episodic Cortex. + - Why this is deeper than Obsidian: every capture is + semantically embedded out of the box, manipulation-pressure- + scored before it pollutes the vault, affect-tagged, region- + mapped onto a 3D cortex, and STDP-rehearsed during idle Dream + Mode. Connections aren't a graph plugin — they are the + physics of the brain. diff --git a/brainsnn-r3f-app/src/App.jsx b/brainsnn-r3f-app/src/App.jsx index afb3b3f..c05ddcd 100644 --- a/brainsnn-r3f-app/src/App.jsx +++ b/brainsnn-r3f-app/src/App.jsx @@ -81,6 +81,8 @@ import HotkeyMap from './components/HotkeyMap'; import ThemePanel from './components/ThemePanel'; import CommunityPackPanel from './components/CommunityPackPanel'; import MilestonePanel from './components/MilestonePanel'; +import EpisodicCortexPanel from './components/EpisodicCortexPanel'; +import { captureToBrainState } from './utils/episodicRouter'; import { registerServiceWorker } from './utils/pwa'; import { registerTheme } from './utils/theme'; import DreamModePanel from './components/DreamModePanel'; @@ -106,6 +108,7 @@ import { applyMockEEG, connectMuseEEG, connectSerialEEG, mapEEGToRegions, parseM import { startCanvasRecording } from './utils/recording'; import { listSnapshots, loadSnapshot, saveSnapshot } from './utils/snapshots'; import { registerDreamProviders, startDreamMonitor, stopDreamMonitor, markActivity } from './utils/dreamMode'; +import { mountEpisodicDream, unmountEpisodicDream } from './utils/episodicDream'; import { mapRagToRegions } from './utils/neuroRag'; import { mapMultimodalToRegions } from './utils/multimodalRag'; import { mapFusedToRegions } from './utils/vectorGraphFusion'; @@ -231,7 +234,11 @@ export default function App() { narrate: (text) => toastInfo(text) }); startDreamMonitor(); - return () => stopDreamMonitor(); + mountEpisodicDream(setState); + return () => { + stopDreamMonitor(); + unmountEpisodicDream(); + }; }, []); const trends = useMemo(() => { @@ -779,6 +786,16 @@ export default function App() { + + { + markActivity(); + setState((prev) => captureToBrainState(prev, capture, { additive: true })); + toastSuccess(`Episodic · ${capture.title.slice(0, 36)}${capture.title.length > 36 ? '…' : ''}`); + }} + /> + + diff --git a/brainsnn-r3f-app/src/components/EpisodicCortexPanel.jsx b/brainsnn-r3f-app/src/components/EpisodicCortexPanel.jsx new file mode 100644 index 0000000..7a0de25 --- /dev/null +++ b/brainsnn-r3f-app/src/components/EpisodicCortexPanel.jsx @@ -0,0 +1,544 @@ +import React, { useEffect, useMemo, useState } from 'react'; +import { + addCapture, + addInsight, + getCaptures, + deleteCapture, + togglePinned, + clearAllCaptures, + ensureAllEmbeddings, + subscribeEpisodic, + captureStats, + exportEpisodicBundle, + importEpisodicBundle +} from '../utils/episodicMemory'; +import { captureToBrainState } from '../utils/episodicRouter'; +import { dailyBrief, weeklySynthesis } from '../utils/episodicSynthesis'; +import { + EPISODIC_CATEGORIES, + EPISODIC_IDS, + categoryColor +} from '../data/episodicTaxonomy'; +import { initEmbeddings, isReady as embeddingsReady } from '../utils/embeddings'; +import { isGemmaConfigured } from '../utils/gemmaEngine'; + +const EXAMPLE_CAPTURES = [ + { + title: 'Picked Postgres over DynamoDB', + text: 'Decided to go with Postgres for the new analytics service. Trade-off: we lose horizontal scale-out for free, but query flexibility is more valuable for this team. Locked in for 2026 H1.' + }, + { + title: 'Production outage Wednesday', + text: 'Auth service crashed at 14:02. Root cause was a missing migration on a staging-only column. Customer-facing for 8 minutes. Regret not catching this in the deploy review — should have been obvious.' + }, + { + title: 'Ferrari paper on second brain', + text: 'Read https://github.com/breferrari/obsidian-mind — the lifecycle hooks pattern (SessionStart / Stop / PostToolUse) is interesting. Same shape as STDP consolidation in Dream Mode. Quote: "procedural code owns the environment, the agent owns content."' + }, + { + title: 'Why does pressure spike on weekends?', + text: 'Question I keep returning to: the firewall logs show mean pressure climbs 30% on Saturdays. Is it me reading more outrage content, or is the corpus actually different on weekends?' + } +]; + +function fmtTime(ts) { + const d = new Date(ts); + const now = Date.now(); + const diff = now - ts; + const min = Math.floor(diff / 60000); + if (min < 1) return 'just now'; + if (min < 60) return `${min}m`; + const hr = Math.floor(min / 60); + if (hr < 24) return `${hr}h`; + const dd = Math.floor(hr / 24); + if (dd < 7) return `${dd}d`; + return d.toISOString().slice(0, 10); +} + +function CategoryChip({ id, score, isPrimary }) { + const cat = EPISODIC_CATEGORIES[id]; + if (!cat) return null; + const pct = Math.round((score || 0) * 100); + return ( + + {cat.icon} {cat.label} {pct ? `${pct}%` : ''} + + ); +} + +function CaptureCard({ capture, onApply, onDelete, onPin }) { + const cat = EPISODIC_CATEGORIES[capture.primary]; + const fwPct = Math.round((capture.firewall?.pressure || 0) * 100); + const affChip = capture.affects?.dominant?.[0]; + + return ( +
+
+ + {capture.kind === 'insight' && ✦ insight} + {capture.title} + + + {fmtTime(capture.ts)} + {capture.consolidatedCount > 0 && ( + + · ◐{capture.consolidatedCount} + + )} + +
+
+ + {capture.secondary && } + {affChip && ( + + ♥ {affChip.label} + + )} + {fwPct > 25 && ( + 50 ? '#ff4066' : '#fdab43', color: fwPct > 50 ? '#ff4066' : '#fdab43' }} title="Firewall manipulation pressure"> + ⚠ {fwPct}% + + )} + {capture.urls?.[0] && ( + + ↗ link + + )} +
+

+ {capture.text.slice(0, 220)}{capture.text.length > 220 ? '…' : ''} +

+
+ + + +
+
+ ); +} + +function BriefCard({ brief, onUseInsight }) { + if (!brief) return null; + return ( +
+
+ Daily Brief + + {brief.count} captures · {brief.source}{brief.gemmaError ? ` (gemma: ${brief.gemmaError})` : ''} + +
+ +
+ Connections + {brief.connections?.length ? brief.connections.map((c, i) => ( +
+
{i + 1}. {c.headline}
+
{c.detail}
+
+ )) :

No connections yet — feed it more captures.

} +
+ +
+ Pattern +

{brief.pattern}

+
+ +
+ Question +

"{brief.question}"

+
+ +
+ +
+
+ ); +} + +function SynthesisCard({ synth, onUseInsight }) { + if (!synth) return null; + return ( +
+
+ Weekly Synthesis + {synth.count} captures · {synth.source} +
+ +
+ Emerging thesis +

{synth.emergingThesis}

+
+ +
+ Contradictions + {synth.contradictions?.length ? synth.contradictions.map((c, i) => ( +
+
{c.headline}
+
{c.detail}
+
+ )) :

None detected — your beliefs are aligned this week.

} +
+ +
+ Knowledge gaps + {synth.knowledgeGaps?.length ? synth.knowledgeGaps.map((g, i) => ( +
+
{g.headline}
+
{g.detail}
+
+ )) :

No gaps — every open thread has a follow-up.

} +
+ +
+ One action +

{synth.oneAction}

+
+ +
+ +
+
+ ); +} + +/** + * Layer 101 — Episodic Cortex. + * + * Capture every article, tweet, voice-note transcript, decision, or + * idea. Each capture flows through Layer 4 (firewall) + Layer 29 + * (affect) + Layer 87 (genre) + the Layer 101 episodic taxonomy and + * lights up the 3D brain accordingly. Push the same captures through + * Daily Brief / Weekly Synthesis to make the vault talk back. + */ +export default function EpisodicCortexPanel({ onApplyEpisodic }) { + const [draft, setDraft] = useState(''); + const [draftTitle, setDraftTitle] = useState(''); + const [filter, setFilter] = useState('all'); + const [search, setSearch] = useState(''); + const [, setTick] = useState(0); // re-render trigger + const [brief, setBrief] = useState(null); + const [synth, setSynth] = useState(null); + const [busy, setBusy] = useState(null); // 'embed' | 'brief' | 'synth' | null + const [embedReady, setEmbedReady] = useState(embeddingsReady()); + + useEffect(() => { + const unsub = subscribeEpisodic(() => setTick((x) => x + 1)); + return () => unsub(); + }, []); + + // Re-render when embeddings warm up + useEffect(() => { + if (embedReady) return; + const t = setInterval(() => { + if (embeddingsReady()) { + setEmbedReady(true); + clearInterval(t); + } + }, 1500); + return () => clearInterval(t); + }, [embedReady]); + + const captures = useMemo(() => { + const opts = {}; + if (filter !== 'all') opts.category = filter; + if (search.trim()) opts.search = search.trim(); + return getCaptures(opts).slice(0, 30); + }, [filter, search]); + + const stats = useMemo(() => captureStats(), [captures]); + + function handleCapture() { + const text = draft.trim(); + if (!text) return; + const cap = addCapture(text, { title: draftTitle.trim() || undefined }); + if (cap) { + onApplyEpisodic?.(cap); + setDraft(''); + setDraftTitle(''); + } + } + + function handleApply(capture) { + onApplyEpisodic?.(capture); + } + + function handleDelete(capture) { + if (!confirm(`Delete "${capture.title}"?`)) return; + deleteCapture(capture.id); + } + + function handlePin(capture) { + togglePinned(capture.id); + } + + function loadExample(i) { + const ex = EXAMPLE_CAPTURES[i]; + setDraftTitle(ex.title); + setDraft(ex.text); + } + + async function handleWarmEmbed() { + setBusy('embed'); + try { + if (!embeddingsReady()) await initEmbeddings(); + setEmbedReady(true); + const res = await ensureAllEmbeddings(); + alert(res.ok ? `Embedded ${res.done}/${res.total} captures.` : `Embedding paused: ${res.reason}`); + } catch (err) { + alert(`Embedding error: ${err.message || err}`); + } finally { + setBusy(null); + } + } + + async function handleBrief() { + setBusy('brief'); + try { + const all = getCaptures(); + const result = await dailyBrief(all); + setBrief(result); + } finally { + setBusy(null); + } + } + + async function handleSynthesis() { + setBusy('synth'); + try { + const all = getCaptures(); + const result = await weeklySynthesis(all); + setSynth(result); + } finally { + setBusy(null); + } + } + + function handleSaveBriefAsInsight(b) { + if (!b) return; + const lines = []; + lines.push(`# Daily Brief — ${new Date().toISOString().slice(0, 10)}`); + lines.push(`Pattern: ${b.pattern}`); + lines.push(`Question: ${b.question}`); + if (b.connections?.length) { + lines.push('Connections:'); + b.connections.forEach((c, i) => lines.push(`${i + 1}. ${c.headline} — ${c.detail}`)); + } + addInsight(lines.join('\n'), { title: `Brief · ${new Date().toISOString().slice(0, 10)}` }); + } + + function handleSaveSynthAsInsight(s) { + if (!s) return; + const lines = []; + lines.push(`# Weekly Synthesis — ${new Date().toISOString().slice(0, 10)}`); + lines.push(`Thesis: ${s.emergingThesis}`); + lines.push(`Action: ${s.oneAction}`); + if (s.contradictions?.length) { + lines.push('Contradictions:'); + s.contradictions.forEach((c, i) => lines.push(`${i + 1}. ${c.headline} — ${c.detail}`)); + } + if (s.knowledgeGaps?.length) { + lines.push('Gaps:'); + s.knowledgeGaps.forEach((g, i) => lines.push(`${i + 1}. ${g.headline} — ${g.detail}`)); + } + addInsight(lines.join('\n'), { title: `Synthesis · ${new Date().toISOString().slice(0, 10)}` }); + } + + function handleExport() { + const bundle = exportEpisodicBundle(); + const blob = new Blob([JSON.stringify(bundle, null, 2)], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `brainsnn-episodic-${new Date().toISOString().slice(0, 10)}.json`; + a.click(); + URL.revokeObjectURL(url); + } + + function handleImport(e) { + const file = e.target.files?.[0]; + if (!file) return; + const reader = new FileReader(); + reader.onload = () => { + try { + const bundle = JSON.parse(String(reader.result)); + const res = importEpisodicBundle(bundle, { merge: true }); + if (res.ok) alert(`Imported. Total captures: ${res.count}`); + else alert(`Import failed: ${res.reason}`); + } catch (err) { + alert(`Bad bundle: ${err.message}`); + } + }; + reader.readAsText(file); + } + + function handleWipe() { + if (!confirm('Wipe ALL episodic captures? This cannot be undone.')) return; + clearAllCaptures(); + setBrief(null); + setSynth(null); + } + + const gemmaOn = isGemmaConfigured(); + + return ( +
+
Layer 101 · episodic cortex
+

The vault that talks back

+

+ Drop articles, tweets, voice-note transcripts, decisions, or shower-thought ideas. + Each capture flows through the firewall, the affect decoder, and an 8-way + episodic router — then lights up the 3D brain. Two outputs make the vault + push insights back: Daily Brief (connections / pattern / + question) and Weekly Synthesis (thesis / contradictions / + gaps / one action). Local-only by default; Gemma upgrades the synthesis + prompts when configured{gemmaOn ? ' — and it is configured.' : '.'} +

+ +
+ Try: + {EXAMPLE_CAPTURES.map((ex, i) => ( + + ))} +
+ + setDraftTitle(e.target.value.slice(0, 120))} + maxLength={120} + style={{ width: '100%', marginBottom: 8 }} + /> +