Projects · visualization

It renders, it never remembers: Hypermnesia MCP Viz, traced through its source

Cortex writes what a session learned into PostgreSQL. Claude Code leaves a separate record on disk, one JSONL file per session. Neither is readable without a query. This tool turns both — plus your local git and a codebase graph — into six reading angles in a browser tab, served by a process that binds to loopback and shuts itself down after ten idle minutes. It renders; it does not remember. That sentence is exactly true of Cortex’s memory tables and false of the database as a whole, and the second exhibit says where the line falls instead of waiting for you to find it.

Runsone stdio MCP server, one detached HTTP server bound to 127.0.0.1:3458, and one graph-build process
ReadsCortex's PostgreSQL tables, ~/.claude/projects/*.jsonl and your local git
Writesfive tables of its own in that same database, including one row per tool call in session_activity
Viewssix tabs; Trace is the default and needs no database at all
Measured tree166 Python files, 35 211 lines; 96 JavaScript files, 22 274 lines outside vendor/
Access controlthe loopback bind, a Host check and an Origin check. No token, cookie or API key
Exhibit 01 · architecture

One MCP server, one detached web server, three sources it does not own

The MCP host starts a launcher, which installs the tool’s own dependencies into a private directory and then runs the server in the same process.3 That server speaks stdio and publishes exactly two tools.2 Opening a visualization renders nothing in-process: it spawns a separate, detached operating-system process that binds 127.0.0.1 and serves the browser page.4 A third process does the graph build, because a CPU-bound build starves the GIL when it runs as a thread.6 Everything below describes the hypermnesia-mcp-viz distribution at version 3.2.0.1

MCP host Claude Code · Codex CLI Cursor · Windsurf · VS Code BOOTSTRAP scripts/launcher.py pip install --target deps/ MCP SERVER · STDIO cortex_viz/__main__.py open_visualization() get_methodology_graph() Popen(start_new_session=True) DETACHED OS PROCESS http_standalone.py bind 127.0.0.1:3458 ThreadingMixIn · HTTP/1.1 idle shutdown after 600 s build_process.py spawn · Queue(maxsize=4096) Browser page ui/ — 96 .js files 22 274 lines no bundler, no framework CORTEX POSTGRESQL · READ memories · entities memory_entities · wiki.* psycopg 3 · pools 2/8 and 1/2 SELECT only ITS OWN TABLES · WRITTEN workflow_graph_layout(_lod) workflow_graph_snapshot* session_activity CREATE · INSERT · DELETE ~/.claude/projects/*.jsonl + local git read only — no store needed POST /api/activity Session hooks 0.5 s timeout · always exit 0 launches runpy.run_module stdio SSE · NDJSON · Arrow IPC reads writes reads
writes, or startsreads only
Nothing here is a cloud service. The listening address is 127.0.0.1 at every bind site, and the browser is opened through an allow-list that silently drops any URL not matching ^https?://127\.0\.0\.1:\d{1,5}.4 Port 3458 is a preference, not a guarantee: the server falls back to an OS-assigned port and records the real one in ~/.cache/cortex/viz-server.json.5 The tree at this revision measures 166 Python files, 35 211 lines and 96 JavaScript files, 22 274 lines outside vendor/ — recounted here because the project’s own documents give three figures that disagree with each other.8
Exhibit 02 · the boundary

“Read-only”, said exactly

The claim on the box is that this tool never writes a memory. It is true, and it is narrower than it sounds. Nothing writes Cortex’s memory tables: the reader’s entire method surface is SELECT, the schema preflight is one catalogue query, the health probe is SELECT 1.16 But the connection is not opened read-only — no SET TRANSACTION READ ONLY, no restricted role, and autocommit=True, which commits every statement immediately, DDL included.9 The tool creates and writes five tables of its own in the same database111213. The project states that correction itself rather than leaving the older blanket claim standing.10

ONE DATABASE · ONE CONNECTION POOL · NO READ-ONLY TRANSACTION, NO RESTRICTED ROLE ConnectionPool(kwargs={"row_factory": dict_row, "autocommit": True}) CORTEX’S TABLES · READ ONLY memories entities memory_entities consolidation_log prospective_memories procedural_skills schemas wiki.pages wiki.links wiki.citations wiki.page_sources wiki.memos wiki.claim_events THE ONLY READERS MemoryReader.query() — SELECT only schema_preflight — one catalogue query db_probe — SELECT 1 wiki_page_actions_pg — two SELECT ITS OWN TABLES · CREATED AND WRITTEN workflow_graph_layout DELETE with no WHERE, then INSERT by executemany workflow_graph_layout_lod CREATE TABLE IF NOT EXISTS + CREATE INDEX + DELETE workflow_graph_snapshot · _snapshot_scoped CREATE TABLE — reached from the READ path too session_activity CREATE TABLE + INSERT … RETURNING id one row per tool call, in every session The guarantee is structural, not a database privilege: no import mcp_server.* anywhere in cortex_viz/ PostToolUse (.*) · UserPromptSubmit POST /api/activity, exempt from the same-origin check
Why this matters before you install it. The five tables live inside Cortex’s own database, and session_activity receives one row for every tool call of every session — that stream is what makes the live view live.14 Two guards stand between a web page and this server: every request must carry a loopback Host (otherwise 421), and every POST a loopback Origin or Referer (otherwise 403), with one documented exemption for the hook, which is a CLI and not a browser.15 There is no token, cookie or API key anywhere: the access control is the loopback bind.32 The hook is held to a hard contract of its own: never block, never raise, always exit zero.7
Exhibit 03 · pipeline

From the row to the pixel

A memory row has no position. Everything between the SELECT and the dot on your screen is computed here, and each step carries constants chosen against a measurement rather than by taste. The edges come from three additive channels: shared entities weighted by inverse document frequency, nearest neighbours in the embedding space, and temporal co-access.17 The colour comes from Leiden community detection at a resolution picked as the knee of a measured sweep.18 The position comes from igraph’s DrL, and only above two hundred nodes.19

1 · READ AND PLACE memory_read.py SELECT * FROM memories effective_heat(m, NOW()) entities · memory_entities memory_associations.py three additive channels TF-IDF top_k 8 · kNN 0.6 temporal window 2.0 h community_detection.py Leiden · CPM objective gamma 0.0005 · seed 42 absent → colour by kind layout_engine.py igraph DrL if N ≥ 200 else FR, niter=200 → [−1, 1] · SHA-256[:16] 2 · PERSIST layout_pg_store.py workflow_graph_layout keyset pages of 50 000 lod_aggregator.py 2^L × 2^L · max_level 7 sum(count) == len(rows) snapshot_pg_store.py ndjson.v1, gzipped 1 000 nodes · 5 000 edges/frame graph_wire.py [id, kind, x, y] no edges on the wire at all 3 · SERVE AND RENDER /api/graph/full/stream 1 MiB read chunks 512 KiB minimum line /api/quadtree Arrow IPC, float32 gzip level 6 /api/tile/{z}/{x}/{y}.png Datashader 512 × 512 eq_hist · spread(px=1) graph_stream_loader.js FRAME_BUDGET_MS 25 complete, or flagged truncated Why it is framed at every boundary: the single-document form crossed ~1.17 GB decompressed at 278 557 nodes and 5 526 064 edges. measured 2026-07-02 · snapshot_pg_store.py:57-59
The honesty is in the accounting. The browser loader declares a load complete only when the received counts equal the announced totals; otherwise the result carries truncated: true rather than a quietly thinner picture.23 The frame budget is 25 ms — half the 50 ms long-task threshold, cited in the file as the RAIL model. Above 25 000 nodes the renderer drops calls edges, a threshold taken from measured main-thread freeze points rather than a round number.24 Two limits are worth knowing before you point this at a large store: the DrL bake was observed to run for hours on a 278k-node graph, and the layout’s seed parameter is declared but never read, so the layout is not reproducible.19 Two more accountings sit behind the figure: the level-of-detail pyramid asserts that the counts at every level sum to the rows it was handed,20 and the AST level is withheld until you ask for it, in batches of two hundred symbols with a three-minute ceiling per project.25
Exhibit 04 · the views

Six reading angles, and what each one needs

The MCP tool accepts only two view values, galaxy and brain; the six angles are tabs inside the page, not tool arguments.2 One of them works with no database at all: Trace is the default landing view and reads only your session logs and your git, which makes the tool useful before Cortex is installed.27 The other five need the store, and say so natively instead of failing at the first click.29

ui/unified-viz.html — SERVED ON / Graph data-view=graph Trace data-view=trace DEFAULT LANDING Knowledge data-view=knowledge Wiki data-view=wiki Board data-view=timeline LABEL ≠ ID Graph · /api/graph/full/stream, then phases gated on /api/graph/progress Trace · /api/trace/{domains, sessions, chain, file, impact} — served live, never snapshotted Knowledge and Board · /api/memories, paged, with server-side facets Wiki · /api/wiki/{list, projects, page, memos, graph} COVERAGE INDICATOR: GRAPH AND TRACE ONLY — THE FOUR OTHERS RENDER WITHOUT ONE Brain — served on /brain a separate page: the button navigates three.js r137, CDN with SRI brain.glb 13 161 040 bytes, CC-BY-4.0 GET /api/capabilities no PostgreSQL → trace-only: five tabs natively disabled, DB-backed routes answer 503 Announced in the tool description, absent from the page: Pipeline (removed 2026-07-05 with its stylesheet and script), Atlas, Emotion. Reachable by URL but listed in no view bar: /atom. Present in the tree, routed by nothing: methodology-viz.html. open_visualization.py:40, :80 · unified-viz.html:14, :454
What a degraded mode looks like when it is designed. Without a reachable store the tabs are natively disabled and carry the reason in their title attribute; the database-backed routes answer 503 with an explicit db_unavailable body naming the missing dependency, rather than 404.2829 The coverage indicator — the feature that promises a view will say when it cannot show everything — is registered for two views out of six, a gap the project records against itself.30

What is measured, and what is only claimed

The project separates those two registers in its own files, and this dossier keeps the separation. The middle column is reproducible and dated. The right column holds design intentions with no attached protocol, quoted as such rather than repeated as performance.

SubjectMeasured, with a dateClaimed, no protocol found
Corpus size278 557 nodes / 5 526 064 edges, 2026-07-02 — the measurement that forced the framed format21“the galaxy builds end-to-end at 75k+ nodes”
Community resolutionsweep over 9 889 memories / 27 346 edges, 2026-07-07; gamma 0.0005 taken at the knee18
Browser thresholdsfreeze points measured on the N ≈ 17k → 27k jump24“first paint of the skeleton lands in ~1 s”
Tiles64 658 rows read in 35 ms, extrapolated to 250 000 rows inside a 200 ms budget22“<5 ms even for 10M-row tables”
Tests and audits988 passed / 10 skipped, 81 % statement coverage; OpenSSF Scorecard 7.4, Best Practices Silver, both 2026-08-0331
Maturityclassifier Development Status :: 3 - Alpha, single maintainer33

Three limits are stated by the project rather than discovered against it: it will not write Cortex’s memory tables, ever; it will not be deployed remotely, because the loopback bind and the absence of authentication are the design;32 and Windows is not declared supported, only partly addressed.33 One documentation claim does not survive a reading of the tree: the offline-operation sentence in PRIVACY.md is contradicted by KaTeX, d3, mermaid and CodeMirror loading from CDNs on the unified page, several without an integrity hash.26

1cortex_viz/identity.py:8:8-10 — distribution name hypermnesia-mcp-viz, registry id, version 3.2.0
2cortex_viz/__main__.py:22:22-31 (FastMCP), :119 (mcp.run(transport="stdio")) — two tools registered; the MCP view argument accepts only galaxy and brain
3scripts/launcher.py:99:99-117 (atomic --target install), :156, :174-175 (DATABASE_URL defaulted to postgresql://127.0.0.1:5432/cortex)
4cortex_viz/server/http_launcher.py:25:25 (PORTS = {"unified": 3458}), :336 (start_new_session=True), :355-365 (browser allow-list)
5cortex_viz/server/http_standalone.py:48:48-51 (threaded, daemon threads), :137 (HTTP/1.1, required by SSE), :197-199 (bind 127.0.0.1, then an OS-assigned port) · server/viz_instance.py writes the port registry
6cortex_viz/server/build_process.py:85:85-90 — spawn context, Queue(maxsize=4096), process cortex-graph-build-proc; the child opens its own reader and returns the graph through a temporary pickle
7cortex_viz/hooks/activity_capture.py:10:10-12 (never block, never raise, always exit 0), :24 (_TIMEOUT_S = 0.5)
8README.md:266:266 — the extraction invariant. Volumes recounted on the tree at 2482563: docs/ARCHITECTURE.md announces 98 files / 26k lines, SECURITY.md 91 JS files / 25k lines
9cortex_viz/infrastructure/memory_read.py:188:188-195 — pool opened with autocommit: True; the query() docstring still claims DML “would be rolled back”, which that line contradicts
10docs/ASSURANCE_CASE.md:27:27-37 — “A precision the older docs got wrong… It is not a read-only database client”; SECURITY.md and PRIVACY.md were corrected in the same change
11cortex_viz/infrastructure/layout_pg_store.py:69:69-78 — clear, then bulk insert, committed explicitly
12cortex_viz/infrastructure/lod_pg_store.py:20:20-33 — DDL self-ensured on first use
13cortex_viz/infrastructure/snapshot_pg_store.py:88:88, :113 (the two snapshot tables), :169 — _ensure_table, also reached when reading the latest snapshot
14cortex_viz/infrastructure/activity_store.py:18:18-39 (table and three indexes), :73 (insert), :100 (the read path ensures the table as well)
15cortex_viz/server/http_security.py:22:22 (loopback set), :35 (Host), :61 (the exact Origin reflected, never *), :82 (same-origin write) · http_standalone.py:139-144 (421), :164-172 (403 and the hook exemption)
16cortex_viz/infrastructure/schema_preflight.py:12:12-13 (“ONE read-only catalog query — no DDL, no writes”), :74, :159
17cortex_viz/infrastructure/memory_associations.py:97:97 (top_k = 8), :103 (stop-word ceiling 0.10), :110 (min_sim = 0.6; corpus p25 0.628, median 0.679, p75 0.744, 2026-07-07), :117 (2.0 h window)
18cortex_viz/core/community_detection.py:49:49-57 — sweep over 9 889 memories / 27 346 co-entity edges, 2026-07-07; :63 (gamma 0.0005), :70 (seed 42). The label propagation it replaced collapsed 87–93 % of memories into one community
19cortex_viz/core/layout_engine.py:85:85-88 (DrL above 200 nodes, Fruchterman-Reingold below), :26-44 (topology fingerprint, SHA-256 truncated to 16 hex); the seed argument at :52 is never read · server/graphbuildrun.py:348 (hours at 278k nodes, 2026-07-02)
20cortex_viz/core/lod_aggregator.py:57:57 (max_level = 7), :71-73 (the conservation invariant and its complexity)
21cortex_viz/infrastructure/snapshot_pg_store.py:57:57-59 (the 1.17 GB measurement), :151-152 (frame sizes)
22cortex_viz/core/tile_renderer.py:62:62 (512 px tiles), :99-102 (Datashader count_cat, eq_hist, spread) · handlers/tile_handler.py — raw-render ceiling of 250 000 rows, and the 64 658-row / 35 ms basis for it
23ui/unified/js/graph_stream_loader.js:33:33-35 (high and low water, 25 ms budget), :137-145 (completeness gate, truncated flag)
24ui/unified/js/workflow_graph_lod.js:24:24-26 — heavy above 8 000, snap above 15 000, extreme above 25 000 · workflow_graph.js:30 (always canvas), :216-220 (alpha decay 0.018/0.022, velocity decay 0.78)
25cortex_viz/server/graph_build_l6.py:28:28 (180 s per project), :102 (batches of 200 symbols) — the AST level is deferred until you ask for it
26ui/unified-viz.html:21:21-23 (KaTeX 0.16.11, no integrity) · ui/unified/js/workflow_graph.js:22 (d3 7.8.5) · ui/unified/js/wiki.js (mermaid 10.9.0 and CodeMirror 6 from esm.sh) — against PRIVACY.md’s “work fully offline”
27ui/unified/js/state.js:20:20 (activeView: 'trace') · README.md:87 — Trace needs no Cortex, no PostgreSQL and no setup
28cortex_viz/server/http_standalone_nodb.py:37:37 (the DB-backed route set), :66-90 (503 chosen over 404 or 410, with the reason written down), :106-115 (the capability map)
29ui/unified/js/capabilities.js:24:24-27 — native disabled, title “Requires the Cortex memory engine (PostgreSQL) — not connected”
30ui/unified/js/coverage_indicator.js:22:22-36 — only graph and trace are registered; docs/ROADMAP.md records the gap
31docs/ASSURANCE_CASE.md:1 § 7 — test totals, statement coverage, zero open CodeQL alerts, zero surviving mutants on the path-containment primitive · README.md:288-289 (OpenSSF badges)
32docs/ROADMAP.md:76:76-78 — “The server binds 127.0.0.1 and has no authentication by design. Exposing it would require a different threat model and a different product.”
33docs/ROADMAP.md:44:44-46 (Windows “partly addressed”, needing a verification pass), :5-7 (single maintainer) · pyproject.toml classifiers — Alpha