CortexDB
Memory framework
First committed by liliang-cn
Published in liliang-cn/cortexdb. Publisher page
478 GitHub stars
What it is
Local-first AI agent memory with a knowledge graph, ontology constraints, and traceable reasoning—all in one SQLite file.
AutoGineer overview, from the brain’s own files
CortexDB is a memory framework designed for AI agents that provides persistent, structured, and traceable storage. It integrates a knowledge graph with vector and full-text search capabilities, allowing agents to store and retrieve information with semantic context. The system supports complex data types, including media, and maintains a history of changes to the graph and ontology. It is built to be portable, operating as a pure Go library or a service, and supports multi-agent environments through shared gRPC connections. With features like SHACL-based ontology governance and traceable reasoning, it enables agents to manage project context, session history, and durable facts effectively.
How it works
Memory model: Hybrid
AutoGineer overview, from the brain’s own files
CortexDB stores all memory and knowledge graph data in a single SQLite file. It uses a hybrid retrieval approach that combines vector search, full-text search, and graph traversal. Agents interact with the database through a set of tools that allow them to remember facts, recall information, and manage an ontology. The system tracks the history of every change, including additions, replacements, and retractions, and provides a live 3D view for visualizing the graph and monitoring agent activity.
| Folder | Role | Files |
|---|---|---|
| .github | agent-config | 6 |
| docs | docs | 54 |
| examples | examples | 41 |
| pkg | source-code | 1671 |
| scripts | scripts | 2 |
| skills | agent-instructions | 6 |
| .agents | folder | 3 |
| .claude-plugin | folder | 1 |
| clients | folder | 72 |
| cmd | folder | 85 |
| deploy | folder | 10 |
| internal | folder | 8 |
| plugins | folder | 79 |
| proto | folder | 9 |
The agent reads
- search_media evidence: README.md
Requirements
Runtimes: Go
Databases: PostgreSQL, SQLite
API keys (by name): CORTEXDB_EMBED_API_KEY, CORTEXDB_GRPC_TOKEN, CORTEXDB_MEDIA_EMBED_API_KEY, CORTEXDB_MEDIA_TRANSCRIBE_API_KEY, OPENAI_API_KEY
Works with: Claude Code
Set it up
This brain runs code automatically: 4 hooks (codex). Review plugins/cortexdb/hooks/hooks.json before opening the folder in Claude Code. No step below turns a hook on for you.
plugins/cortexdb/hooks/hooks.json:${CLAUDE_PLUGIN_ROOT}/bin/cortexdb-session-startplugins/cortexdb/hooks/hooks.json:${CLAUDE_PLUGIN_ROOT}/bin/cortexdb-recallplugins/cortexdb/hooks/hooks.json:${CLAUDE_PLUGIN_ROOT}/bin/cortexdb-file-contextplugins/cortexdb/hooks/hooks.json:${CLAUDE_PLUGIN_ROOT}/bin/cortexdb-session-end
- Not scanned by AutoGineerThe author’s README also describes installing it as a Claude Code plugin marketplace. Read its install section: AutoGineer did not scan that path. https://github.com/liliang-cn/cortexdb/blob/d39714c86784edd23d42ce152a0243464ad2b15a/README.md#install
- Not scanned by AutoGineerThe author’s README also describes installing it with go install. Read its install section: AutoGineer did not scan that path. https://github.com/liliang-cn/cortexdb/blob/d39714c86784edd23d42ce152a0243464ad2b15a/README.md#install
First committed by liliang-cn. Published in liliang-cn/cortexdb. Licence: MIT License.
Usage
AutoGineer overview, from the brain’s own files
- Integrating persistent memory into AI agents via Go, Python, Rust, or Node.js clients.
- Sharing a centralized knowledge base across multiple agents or machines using the gRPC server.
- Managing project-specific context and session history for coding agents like Claude Code.
- Visualizing and auditing agent memory and reasoning through a live 3D graph interface.
- “Remember that Alice prefers tabs.”
- The agent saves the fact 'Alice prefers tabs' into the user-scoped knowledge memory.
- “What does Alice prefer?”
- The agent retrieves the stored preference from the knowledge memory and provides the answer.
Documentation by liliang-cn
CortexDB
Image: Go Reference Image: CI Image: codecov Image: License: MIT
AI memory and a knowledge graph in one SQLite file. Pure Go, no service to run, works without an embedding model.
Install
| Go library | go get github.com/liliang-cn/cortexdb/v2 |
| Claude Code | /plugin marketplace add liliang-cn/cortexdb, then /plugin install cortexdb@cortexdb |
| Codex | codex plugin marketplace add liliang-cn/cortexdb && codex plugin add cortexdb@cortexdb |
| Claude Code mod (optional) | /plugin install cortexdb-live@cortexdb |
| DeepSeek Harness | dsh plugin --profile web add "github:liliang-cn/cortexdb#semver:^2&path:/plugins/dsh-cortexdb-memory" (guide) |
| Obsidian | unzip a release's cortexdb-obsidian.zip into <vault>/.obsidian/plugins/ (guide) |
| Shared-brain server | go install github.com/liliang-cn/cortexdb/v2/cmd/cortexdb-grpc@latest |
| Clients | cargo add cortexdb-client · pip install cortexdb-client · npm install cortexdb-client |
Use
db, _ := cortexdb.Open(cortexdb.DefaultConfig("brain.db"))
defer db.Close()
brain := db.KnowledgeMemory()
_, _ = brain.Remember(ctx, cortexdb.KnowledgeMemoryRememberRequest{MemoryID: "alice-tabs", UserID: "alice", Scope: "user", Content: "Alice prefers tabs."})
rec, _ := brain.Recall(ctx, cortexdb.KnowledgeMemoryRecallRequest{Query: "what does Alice prefer?", UserID: "alice"})
fmt.Println(rec.ContextPack.Text)The plugin gives Claude Code and Codex one global brain at ~/.cortexdb/cortexdb.db, /remember, /recall and an auto-recall hook. A session starts with an index of what the brain knows about its project, an agent about to open a file is told what happened to it before, and a session's durable facts are captured with <private> text and credentials kept out. Point several agents or machines at one cortexdb-grpc and they share the same memory and graph.
In Codex, review and trust the plugin hooks in /hooks to activate automatic
recall. See the plugin guide for setup and
cortexdb-mcp --doctor --self-test diagnostics (v2.119.0+).
Live view
A web UI over the brain, local or shared: run cortexdb-mcp-stdio --graph-3d, or ask the agent to call serve_graph_3d.
Graph: the knowledge graph in 3D. A recall from any agent or machine lights the records it returned, and the Live feed says who asked what.
Recalls: every recall with its plan, its hits and why each ranked (
CORTEXDB_RECALL_LOG=1).Changes and Activity: every fact added, replaced or retracted, and the tool calls the brain handled.
Ontology: the declared types held against what the records are actually typed as.
Library: the same memories as a reading room, shelved by project.
Read-only, light by default, works on a phone, in English or Chinese (the toggle in the top bar). See the guide.
Claude Code mod
cortexdb-live is an optional mod that runs inside Claude Code on top of the cortexdb plugin. Install it after the plugin:
/plugin install cortexdb-live@cortexdbThe mod calls three of the plugin's tools itself, and a hook gets no permission prompt, so allow them once in ~/.claude/settings.json (not needed in bypass mode):
"permissions": {
"allow": [
"mcp__plugin_cortexdb_cortexdb__knowledge_memory_recall",
"mcp__plugin_cortexdb_cortexdb__graph_statistics",
"mcp__plugin_cortexdb_cortexdb__memory_save"
]
}Without them the status line names the missing rules.
Planned recall. Before the brain is searched, haiku turns each prompt into keywords in Chinese and English, aliases, entity names and a retrieval mode. A bare "ok" or "go ahead" searches nothing. The band above the prompt shows what was recalled; expand it to see the plan and every hit. Hide lasts until the next recall;
/cortexdb-showbrings the hidden one back.Status line. Which brain the session uses, its node count and how long the last recall took, or why the brain can't be reached.
Capture. 90 seconds after a session goes idle, haiku distils the new part of the conversation into durable memories (
auto:<session>:<slug>). Each later pass replaces a memory under the same slug rather than adding a copy. Older memories a new one makes untrue (a host moved, a decision reversed) are marked superseded: kept, but no longer recalled as current. The project's Claude Code file memory (~/.claude/projects/<project>/memory/) is shown to haiku as already recorded, so a capture does not restate a note in other words. A prompt with an image attached is not recalled for.
Haiku runs on Claude Code's own model access, so no API key is needed. Language: /config → cortexdb-live → language (auto, zh, en). The mod honours the plugin's own switches: cortexdb-recall --disable and cortexdb-session-end --disable turn recall and capture off for both. While the mod runs, the plugin's shell recall and capture hooks stand down, so nothing is done twice. Codex keeps those hooks. Mods are an early-access Claude Code feature.
What's inside
Vectors (HNSW, IVF, flat, binary codes), FTS5 full-text search, hybrid and graph retrieval
EmbeddedConfigfor small devices: an SQ8 index, bounded SQLite caches, snapshots that survive a power cut — 100k 768-d vectors open in 0.28 s with a 193 MB heapRAG knowledge, scoped agent memory, context packs with sources — memories say whether they were verified or are a guess, and recall keeps the two apart
RDF 1.2 knowledge graph: SPARQL 1.1/1.2, RDFS + OWL 2 RL inference, SHACL Core + SHACL-SPARQL, RDF/XML import — passing the W3C SPARQL 1.1/1.2, RDF 1.2 (Turtle, TriG, N-Triples, N-Quads, RDF/XML) and SHACL test suites; RDFS and OWL 2 RL are covered by the repo's own tests — and read-only Cypher
Palantir-style ontology with governed actions
100+ tools, in-process or over MCP
serve_graph_3d: a live 3D view to find, ask, query and expand, on desktop or phone, with a Changes page that says in words what was added, replaced or retracted as it happens, and an Entity page with each relation, why its value is in effect and what it replaced; or the same brain as a library, each memory a book on its project's shelfimport_agent_memory: bring in what Claude Code, Codex and DeepSeek Harness already remember — memory notes,CLAUDE.md/AGENTS.md, Codex's memories, and optionally past sessions distilled into memories — into a local or shared brainChange feed: every committed write, in order, exactly once
Observability: a request id and a W3C trace (
traceparent) from MCP through gRPC to the trace and the recall log, recalls that explain themselves and say when they were degraded, embedder health in/metricsand/healthz, and an opt-in recall log with a/recallspageImages, recordings and videos (
ingest_media): found by a text question through their caption, visible text and transcript, a hit saying where in the file — with or without an embedder; with a CLIP-style media embedder also by what they show, and by an image (search_media)History of the graph and the ontology: for an entity, relation, triple or schema, every version, when and why it changed, and which one is in effect and why (
graph_history,knowledge_graph_history,ontology_history);as_ofon SPARQL, find, expand and Cypher; an entity timeline, an As of pin and an ontology version list in the live viewSQLite by default, PostgreSQL + pgvector with a
postgres://DSN
Environment
| Variable | Meaning |
|---|---|
CORTEXDB_PATH | Database file (default ~/.cortexdb/cortexdb.db) |
CORTEXDB_REMOTE | Use a shared cortexdb-grpc at host:port (plaintext) or https://host (TLS, behind a proxy) instead of a local file |
CORTEXDB_GRPC_TOKEN | Bearer token for that server |
CORTEXDB_GRPC_ADDR | Server listen address (default 127.0.0.1:47821) |
CORTEXDB_EMBED_BASE_URL / CORTEXDB_EMBED_API_KEY | cortexdb-mcp-stdio: OpenAI-compatible embeddings endpoint and its key (fall back to OPENAI_BASE_URL / OPENAI_API_KEY); unset = lexical mode |
OPENAI_BASE_URL / OPENAI_API_KEY | Server: the embeddings endpoint and key (cortexdb-grpc reads only these, not CORTEXDB_EMBED_BASE_URL); unset = lexical mode |
CORTEXDB_EMBED_MODEL / CORTEXDB_EMBED_DIM | Embedding model and dimension, for both (default text-embedding-3-small / 1536) |
CORTEXDB_VECTOR_INDEX / CORTEXDB_BINARY_OVERSAMPLE | Server: hnsw (default) or, on a SQLite store, binary (1-bit codes in memory, 1/32 of the vectors, each search rescored from the stored vectors, no snapshot); the oversample is the binary shortlist multiplier, from 1 to 1000 (default 8) |
CORTEXDB_REVIEW_GATE | Server and cortexdb-mcp-stdio: 1 leaves proposed and rejected documents and memories out of search and recall (default off; a curation filter, not access control) |
CORTEXDB_SLOW_MS | Server: log calls slower than this, with the time each stage took (default 1000, 0 = none) |
CORTEXDB_TRACE / CORTEXDB_TRACE_FILE | Server: log every call; also append the records to a JSON-lines file |
CORTEXDB_LOG_FORMAT / CORTEXDB_LOG_LEVEL | Server: text (logfmt, default) or json; debug, info (default), warn, error |
CORTEXDB_TRACE_MAX_MB / CORTEXDB_TRACE_KEEP / CORTEXDB_TRACE_MAX_AGE_DAYS | Server: rotate the trace file at this size (100), keep this many old files (5) for at most this many days (30) |
CORTEXDB_EMBED_PROBE_SECONDS | Server: probe the embedder this often, for /healthz and /metrics (default 60, 0 = off) |
CORTEXDB_RECALL_LOG | Server and cortexdb-mcp-stdio: 1 keeps a log of recalls — queries included — for the /recalls page and the recall_log_* tools (default off) |
CORTEXDB_RECALL_LOG_DAYS / CORTEXDB_RECALL_LOG_MAX_ROWS | How long (30 days) and how many (100000) recalls the log keeps |
CORTEXDB_MEDIA_VISION_URL / CORTEXDB_MEDIA_VISION_MODEL | Server and cortexdb-mcp-stdio: an Ollama vision model that captions images and video frames for ingest_media (e.g. http://localhost:11434 / qwen3.5:4b) |
CORTEXDB_MEDIA_TRANSCRIBE_URL / CORTEXDB_MEDIA_TRANSCRIBE_MODEL / CORTEXDB_MEDIA_TRANSCRIBE_API_KEY | An OpenAI-compatible …/v1 transcription endpoint for audio and the sound of videos (OpenAI, or whisper.cpp's whisper-server --inference-path /v1/audio/transcriptions --convert) |
CORTEXDB_MEDIA_EMBED_URL / CORTEXDB_MEDIA_EMBED_MODEL / CORTEXDB_MEDIA_EMBED_API_KEY | A CLIP-style embedding endpoint (infinity's /embeddings with modality) that puts images, video frames and text in one space, so an image is found by what it shows and search_media takes an image as the query |
CORTEXDB_MEDIA_EMBED_AUDIO / CORTEXDB_MEDIA_EMBED_MIN_SCORE | 1 when that model embeds sound too (CLAP); the cosine a media vector must reach to be a hit (0.2) |
CORTEXDB_MEDIA_FRAME_INTERVAL / CORTEXDB_MEDIA_MAX_BYTES | Seconds between the video frames captioned (10, 0 = none; needs ffmpeg), and the size cap of one file (32 MiB) |
Docs
Website · Guide (full feature reference) · Examples · Changelog · 中文
License
MIT
CHANGELOG.md
Changelog
All notable changes to this project will be documented in this file.
[Unreleased]
[2.150.0] - 2026-11-10
Added
The live view can be served past this machine:
--graph-3d --listen host:port(liveview.StartAt) behind an access token (CORTEXDB_VIEW_TOKEN,--token-file,liveview.WithAccessToken). A browser signs in with a form that trades the token for an HttpOnly cookie; a script sends a bearer header. A non-loopback address without a token is refused.
[2.149.1] - 2026-11-10
Fixed
The live view's Ontology page counts a node stored under one of a type's aliases (
pr,work item,repository) as that type, instead of listing the alias outside the model.
[2.149.0] - 2026-11-10
Fixed
Activating a vocabulary ontology no longer moves entities to new nodes (#152). A declared type whose primary key is filled from the name (
name, asontology_draftdrafts it) gave every entity written without a stated key a typed ID,entity:<type>:<name>, beside theentity:<name>node it already had. Nothing merged the two, and recall, which seeds by name, never reached the typed one. Under vocabulary enforcement such an entity now keeps its name-keyed node; an entity that states its key still gets the typed one, and strict enforcement is unchanged.The live view speaks one language on every page (#158). Only the Recalls and Library pages had Chinese, so a Chinese browser showed them under an English top bar, beside English Graph, Changes, Activity, Ontology and Versions pages. Every page, the top bar, the command palette and relative times now follow one choice:
?lang=when the link has it, then a language toggle in the top bar (remembered in this browser), then the browser's language.The live view's Activity page no longer opens empty after the view starts (#158). Its first read of the recall log only marked where the log stood, so until someone recalled again the page said there were no calls while the Recalls page listed them. The latest recalls are now in the list a page opens with; they light nothing on the graph.
Added
The cortexdb-live mod's capture is told the active ontology's object types too (#156), as the Go capture is: each entity's type is a declared api name (an alias or a case variant is saved under the type's own name),
file, or empty when no declared type fits. A brain with no active ontology gets the prompt it had.The live view shows every recall as it happens, whoever made it (#148). Its activity feed used to show only the tool calls of the one process serving it, so on a shared brain the auto-recalls of every other session never appeared, and the recalls timeline caught them only every 15 seconds. While a page is open the view now reads the recall log's head on every poll, and each new recall reaches every page as activity: the graph lights the records it returned, the feed says who asked what, and the timeline shows it at once.
An ontology object type's
aliasesnow resolve (#150). A write naming an alias (pr,pull request) is stored under the type's own name (merge_request), on the node it already had. A query by the type, or by an interface it implements, also finds nodes stored under its aliases. The field was stored before but nothing read it, so a vocabulary schema folded case only. A schema whose alias names another type or interface, or is claimed by two types, is refused.Extractors are given the active ontology's types (#153). The session capture and graphflow's LLM extractor asked the model for a free-form entity type, so every session invented its own spelling and the shared brain held 268 node types, most with one or two nodes. With an active schema they now read it once per run (a shared brain through its
ontology_listtool), list each declared type's api name, description and aliases in the prompt, and ask for one of those api names or an empty type. An alias is saved as its api name and an undeclared type as the generic untyped entity. The list is capped (60 types, 160-character descriptions, 5 aliases) so a large schema cannot crowd out the transcript. With no active schema, or when it cannot be read, the prompts are byte for byte what they were. NewDB.ActiveExtractorVocabularyandExtractorVocabularyFromSchemasbuild the same list for other extractors.
[2.148.2] - 2026-11-10
Fixed
Auto-recall no longer recalls for a background task's notification (#146). Claude Code delivers a finished background task as a turn of its own, a
<task-notification>with nothing the person typed; on the live brain one recall in seven was one of these. The hook and the cortexdb-live mod now ask with what the person typed, without the machinery Claude Code injects, and skip a turn that is all machinery, as capture already does.
[2.148.1] - 2026-11-09
Fixed
Auto-recall asks with the start of the last reply again (#144). 2.148.0 left it out of the query, and a follow-up that names nothing ("?", "做吧", "测试通过了吗?") was recalled for its own few words: on the conversations replayed against a snapshot of the brain it lost the memory the reply was about, which 2.147.1 found. The hook asks as 2.147.1 did,
CORTEXDB_RECALL_TOPKof each kind, and the cortexdb-live mod's fallback query carries the reply again.A prompt sent with an image from the Claude desktop app is not recalled for either (#144). The app does not write
[Image #1]into the prompt; the hook reads the image block from the prompt's own transcript entry, and the mod fromprompt.submit's attachments.
[2.148.0] - 2026-11-09
Changed
Auto-recall asks with the prompt and the project, and no longer with the start of the last reply (#142). On a short prompt those 300 runes outweighed the prompt, and a new subject was answered with the previous one. The last reply now only decides which of the hits the prompt found come first: the hook asks for twice
CORTEXDB_RECALL_TOPKof each kind and keeps those sharing the most of its words. A prompt with an image attached ([Image #1]) is not recalled for, since the hook cannot see the picture. The cortexdb-live mod does the same.A Claude Code capture is shown the project's file memory (
~/.claude/projects/<slug>/memory/*.md) as already recorded, and extracts only what those notes lack (#142). They are in context every session and say more, so a capture that restated one in other words took a recall slot and added nothing. A project without a file memory is captured as before. The SessionEnd capture,import_agent_memory's session import and the cortexdb-live mod all do this.
[2.147.1] - 2026-11-09
Fixed
Rewriting a chunk on SQLite (
Upsert,UpsertBatch,UpsertBatchTx,UpsertMultiVector) no longer leaves the old entry inchunks_ftsandchunks_fts_cjk(#141).INSERT OR REPLACEremoved the old row without firing the delete triggers, so every rewrite left an index entry whose row was gone: reads dropped it, but it skewed BM25 and grew the indexes without bound. The writers now upsert withON CONFLICT(id) DO UPDATE, which fires the update triggers; every column is still replaced, and a rewritten row keeps its rowid. A store written before this keeps the entries it already has untilINSERT INTO chunks_fts(chunks_fts) VALUES('rebuild')(and the same forchunks_fts_cjk) runs. Lauro Foletti.
[2.147.0] - 2026-11-07
Added
liveview.WithoutPages: a host that mounts the live view leaves pages out of it (PageChanges,PageActivity,PageRecalls,PageLibrary,PageOntology,PageVersions), as an option toNewandStart(#137). A page left out is gone from the navigation, the command palette and the router, no page links to it, and its path and the API routes only it reads answer 404; leaving out Recalls also takes the graph's colour-by-recalls mode and recall replay./api/ontologygoes only once both Ontology and Versions are out, and/api/changesstays, since the graph's inspector and the entity page read it. The graph and the entity page cannot be left out, andNewrefuses an unknown name.Server.HiddenPagesreports the set.
Fixed
The Ontology page no longer shows a STRICT badge for a report whose
Enforcementis empty (#139). CortexDB's own sources always fill it; an empty one comes from a host's ownSource.Ontologythat declared no enforcement, and now shows no badge.
[2.146.0] - 2026-11-06
Added
ingest_media(GraphRAGToolbox.IngestMedia): images, recordings and videos as documents whose text is derived from the file, a caption and the visible text of an image, the timed transcript of audio, a video's transcript and frame captions on one timeline. The text is searched like any document, with or without an embedder, its entities extracted asknowledge_saveextracts them; a hit carriesmediawith themedia_typeand thestart_ms/end_msof each matching chunk. The derivers are interfaces (WithImageDescriber,WithTranscriber,WithFrameSampler);pkg/mediahas an Ollama describer, an OpenAI-compatible transcriber (whisper.cpp's server too) and an ffmpeg frame sampler, configured byCORTEXDB_MEDIA_*incortexdb-grpcandcortexdb-mcp-stdio. A caller may send text it derived itself with the file's sha256. The file's hash is the content hash: an unchanged file is not derived again, another model replaces only its chunks. The bytes are not stored.examples/20_media(#103).History of the knowledge graph and the ontology (#125).
graph_history(db.GraphHistory) lists the versions of one entity and its relations, oldest first, grouped into one line per subject, predicate and direction, in bounded pages (limit,max_versions,cursor). Each version carries its valid interval, status, reason,superseded_by, producer, provenance and the schema id and version that decided its closure; each version in effect, now or atas_of, carriesin_effect(only_version,replaced,revised,merge_survivor,several_in_effectwith the schema's opinion). When the active ontology gives a link aONEside,upsert_relationsand graphflow now close the old value in the transaction that writes the new one; relations extracted from documents are not superseded and read asseveral_in_effect.RDF triples keep their history. A delete, a SPARQL Update or an inference refresh moves the triple to
kg_triples_history;knowledge_graph_historylists the versions of a subject's triples, andknowledge_graph_query,knowledge_graph_find,expand_graphandgraph_cypher_querytake an optionalas_of. A write underas_ofand a malformed instant are refused.vacuum_graphalso purges triple history (PurgeReport.Triples).Ontology schemas keep every version and every activation interval (
ontology_schema_versions,ontology_activations). New toolontology_history;ontology_gettakesversionoras_of,ontology_difftakesfrom_version/to_version,ontology_action_audittakesschema_id/schema_version. A rollback is a save of the old definition. A deleted schema keeps its history.Live view: History on a selected node opens a timeline with one rail per relation and direction and the
in_effectreason in words; find and expand follow the As of pin (as_ofon/api/findand/api/expand); the new page/ontology-historylists a schema's versions and diffs any two (/api/history,/api/ontology/history,/api/ontology/diff). A key confined to a collection or user is refused the three history tools; a read-only key may call them throughCallTool.The live view is a new app (#127): Vite, React, TypeScript, Tailwind and shadcn, built into
pkg/liveview/distand embedded, sogo installstill needs no Node. Light by default, a dark theme on the toggle, laid out for a phone as well as a desktop. It updates over one Server-Sent Events stream,/api/stream: asnapshotwith the cursor it was taken at, thendelta,activityandchangeevents, eachchangecarrying its seq as the event id. A page that reconnects sendsLast-Event-ID(or?after=) and gets the changes it missed exactly once. A page the feed cannot catch up that way gets aresetwith the reason and a fresh snapshot:pruned(its cursor is older than retention),behind(too much happened for one replay),ahead(its cursor is from a brain further along) orunavailable(the feed could not be read). New pages: Changes, every fact added, replaced, retracted or merged, newest first, in words ("Leo lives_in Chengdu, replaced Beijing"), filtered by kind, op, producer and entity, live as they happen (/api/changes:before,after,limit,kind,op,producer,entity); Entity, one entity's relations grouped by predicate and direction, each current value with why it is in effect and the versions it replaced, and its recent changes (/api/entity?id=); and Activity, the tool calls the brain handles, when the view is served by the MCP server (serve_graph_3d). On the graph, relations are directed and coloured by predicate,co_occursis grey and hidden by default, node types get distinct hues by how common they are, and a page opened on a node frames it in the part of the screen the panels leave uncovered. The view can be mounted under a prefix of another mux (http.StripPrefix); the server tells the app its base. The graph's three colour presets (?theme=space|ember|mono) are gone: the app has one palette, light or dark (?mode=).The live view's other four pages are in the app too (#131), at the same paths and with the same query options, so
serve_graph_3d'slibrary_urland/recalls?trace=links still land; Go now writes no HTML of its own. Ontology draws the schema as lanes, with its findings, the draft review and its threshold, and reads itself again when a schema is saved; Versions (/ontology-history) keeps the compared pair in the link (?schema=&from=&to=) and shows a new version as it is saved; Recalls picks up a new recall as it is logged; Library is the same reading room, lit by its windows in the light theme and by lamps in the dark. Recalls and Library speak Chinese and English (?lang=, otherwise the browser's language). The four load when first visited, so the first page is no heavier.liveview.Event.Nodes: node ids an observed event lights exactly, for a host that reads its store through the Go API rather than tools and builds its own events forServer.Observe(#135). UnlikeTerms, which are matched loosely against names, they are joined on identity:machine:1does not lightmachine:10, and a two-character name still lights its node. The graph and the library both read them.changes_sincereads backwards and explains (#127).beforereturns the newest events below a seq, newest first (negative: from the head), withnext_cursorthebeforeof the next older page.explainadds to node and edge events what the history tables say:superseded_by,replaces,schema_idandschema_version, andlabelsfor the nodes they name, in a fixed number of queries per page (ChangesSinceRequest.Before,.Explain;graph.ChangesQuery).Media vectors (#103): with
WithMediaEmbedder(a CLIP, SigLIP or CLAP model;media.NewOpenAIMediaEmbedderfor infinity's/embeddingswith amodality,CORTEXDB_MEDIA_EMBED_*) images, video frames and, for a model that takes sound, recordings are embedded into the model's own space, in amedia_vectorstable apart from the text index, written with the chunks and deleted with the document.knowledge_searchand recall add what that space finds for the question without reordering the text answer (media_scoreon a hit), past a cosine floor (WithMediaScoreFloor, 0.2) and 85% of the best media hit. New toolsearch_mediasearches it by text or by an image. An unchanged file is not embedded again.
Fixed
The live view answers a request's own not-found with 404, not 502, from a local file and from a shared brain (
codes.NotFound): an unknown schema on/api/ontology/historyis a 404 that says it has no history. A brain without the recall log returnscortexdb.ErrRecallLogOff, whichcortexdb-grpcsends asFailedPreconditionand the view as 409, and the page reads the status rather than the message. A shared brain older than this still answersInternal, and the Recalls page shows its sentence.The graph page reads
?replay=,?focus=and?color=again when a link is followed inside the app, not only on a full load.The live view's graph no longer flickers while it is handled (#133). A hover centres the view once the pointer has rested on a node for about 0.1 s and lets go once it has been off for about 0.2 s, never while the camera is being dragged, and lets go when the pointer leaves the canvas; moving from one node to the next passes straight between them. What a focus dims eases in and out over about 0.2 s (at once under reduced motion). A name already on the canvas keeps its place against one that drifts into it, names fade in and out, and a node near the size at which it is named no longer gains and loses its name as the camera moves. The labels of parallel relations between two nodes each get their own line; every one after the first used to be taken for an overlap and was never drawn.
[2.145.0] - 2026-11-03
v2.144.0 was tagged on a feature branch by mistake and is retracted; it holds the same two changes on top of unreleased work. Use v2.145.0.
Added
cortexdb.NewOpenAIEmbedderandcortexdb.EmbedderFromEnv: the OpenAI-compatible embedder (OpenAI, Ollama, vLLM …) that cortexdb-mcp and cortexdb-grpc used privately, for any program that embeds CortexDB.EmbedderFromEnvreads the sameCORTEXDB_EMBED_*variables as cortexdb-mcp and is nil when none is set.
Fixed
Keyword recall counts an English word and its inflections as one word: "what does Alice own?" finds "Alice owns Apollo", and "shipped" covers "ships", "box" "boxes". The relevance floors compared short words whole, so such a row was dropped as sharing one word of the question. Coverage now compares words by a light stem (CJK unchanged), and the prefix keyword query searches the stem (
own*).
[2.143.0] - 2026-11-03
Known issue: on PostgreSQL, lexical knowledge search ranks its candidates in storage order, so the keyword floor can leave a lexical search empty where SQLite answers (#119, since v2.138.0).
Added
ingest_documenttakesif_absent: true(write only a new document id) andif_sha256(write only over the stored_content_sha256the caller read). A refused condition writes nothing and fails withErrPreconditionFailed: gRPCFailedPrecondition, HTTP 412. Both together are refused (ErrConflictingConditions, 400). On PostgreSQL every knowledge write and delete now takes a transaction advisory lock on the document id, so the check and the write are one step. Lauro Foletti (#120).knowledge_searchandaggregate_metadatatakeinclude_proposed: under the review gate, proposed documents are served and counted beside accepted ones, and rejected ones stay hidden; areview_statusfilter still decides. Without the gate it changes nothing. A hit'smetadatacarries_review_status, as before. Lauro Foletti (#121).
Fixed
knowledge_delete, andknowledge_save/knowledge_updateover a stored document, take the document out of thesource_document_idsof every entity that lists it, asdelete_document_graphdoes. An entity left with no source and held by no edge is removed, mentioned or not; before, it kept naming a deleted document and outlived its last source. Lauro Foletti (#122).knowledge_saveandknowledge_updaterecord the document among the sources of the entities they write, asupsert_entitiesdoes, so an entity a document alone declared goes with that document or with the version that drops it. Entities another document still sources, and nodes written by hand, stay. Entities written before this release carry no source and are kept. Lauro Foletti (#123).
[2.142.0] - 2026-10-31
Known issue: on PostgreSQL, lexical knowledge search ranks its candidates in storage order, so the keyword floor can leave a lexical search empty where SQLite answers (#119, since v2.138.0).
Added
find_nodestakesskip_chunks: nodes whose id beginschunk:, the chunks ingest writes, are left out of the match and their text is not decoded. Document nodes are still matched, and an untitled document holds its text, so the saving is largest on a store whose documents have titles. Off by default; an older server ignores the field. Lauro Foletti.cortexdb-grpcreadsCORTEXDB_VECTOR_INDEX(hnsw, the default, orbinaryon a SQLite store) andCORTEXDB_BINARY_OVERSAMPLE, and logs the index it opens with. The binary index holds 1-bit codes in memory and keeps no snapshot: a store of 40,000 384-d vectors opens in 42 MB instead of 300. Lauro Foletti.
Fixed
A store whose HNSW snapshot was written quantized is rebuilt from its table when it is reopened without quantization or with another kind or number of bits, instead of loading codes nothing can read. A store closed under
EmbeddedConfig(SQ8) panicked when reopened withDefaultConfig, as every binary incmd/does, and so did an IVF store with a quantizer left by an earlier HNSW index; one moved between SQ8, SQ4 and binary quantization, or from binary to none, opened and then found almost nothing. The quantizer is now written or dropped in the same transaction as the snapshot, so the rebuild is paid once. A store already closed under the wrong configuration by an earlier version holds codes beside a quantizer that matches them and is not repaired: delete its HNSW snapshot rows (DELETE FROM index_snapshots WHERE type LIKE 'HNSW%' OR type = 'QUANTIZER') and it is rebuilt on the next open.ImportIndexstill loads a graph as it is given. Reported by Lauro Foletti.A vector search on PostgreSQL no longer stops at the first 40 rows of the HNSW index. pgvector's scan returns at most
hnsw.ef_searchrows (40 unless set), before anyWHERE: a search for more than 40 came back with 40, and one restricted to a collection, a metadata value, an ACL, a facet or a radius came back with the few of the first 40 that passed (3 of 5 for a collection holding 5 % of the store), all without an error. An unfiltered search of up to 1000 rows now widens the scan to its limit; a filtered, unbounded or longer one is ranked exactly, without the index. The graph's nearest nodes, asked for at least 50, got 40 and now get their limit; the binary index's shortlist past 1000 rows (fromtop_k126 at the default oversample) failed with anhnsw.ef_searchrange error and is now ranked exactly. The scan still counts row versions a vacuum has not removed yet, so an unfiltered search on a table with many updated or deleted rows can still come back short until the next vacuum.On PostgreSQL, graph nodes written in a batch (
UpsertNodesBatch,ExecuteBatch, and so ingest, memory, graph import and knowledge transactions) and the RDF term nodes written with triples are found by their vector. OnlyUpsertNodecopied a vector intograph_node_vectors, the table the graph's vector search reads: once a single node had been written alone, no node written in a batch was found by similarity, with no error. These writes now put the mirror row in their own transaction, and opening a store mirrors the stored vectors that an earlier version left out; a stored vector it cannot take is skipped, and read again at the next open. Each node of a batch now pays the HNSW index insert that eachUpsertNodealready paid: a batch of 500 nodes of 384 dimensions went from 33 ms to 3.9 s here; a bulk load can dropidx_graph_node_vectors_cosand let the next open rebuild it.On PostgreSQL, the graph's vector table holds, and is asked about, only vectors of its column's width, read from the column (or the configured one, for a column without a width), and finite. A store opened with the default config makes that table 64 wide; with an embedder of another width, every graph vector search failed (
expected 64 dimensions, not 8), andUpsertNodereturned that error after writing the node. Such a search is now scored in Go and such a node is written without an error, left out of the table, as is a vector holding NaN or an infinity.
[2.141.0] - 2026-10-29
Security
find_nodessizes its matches by the nodes it can match, not by the caller'slimit: a limit past what a slice can hold (1 << 62) panicked, and since gRPC handlers do not recover,cortexdb-grpcexited. Any key allowed to callfind_nodescould stop the server.A node follows its document's scope rules only when its id is the one ingest derives from its document (
doc:<id>,chunk:<id>:NNN), not when its type saysdocumentorchunk: an entity another collection typeddocumentwas served whole to a key confined to a collection that knows it, with that collection's description and collections list.A gRPC handler that panics is answered with
Internaland logged with its stack instead of taking the server down.
Both reported privately by Lauro Foletti.
Added
A key confined to a collection or user may call
find_nodes, held by the facade's hidden-node filter;node_typesmatches the type the caller is served (#111). Lauro Foletti.Flow traces (W3C Trace Context): gRPC, HTTP and MCP calls join the caller's
traceparent(MCP:params._meta.traceparent) as a child span or start a trace, return their own span (traceparent, MCP_metacortexdb/traceparent), and forward it to the brain, the embedder, LLM, reranker and SPARQLSERVICE. Log lines, trace records and the recall log carrytrace_idandspan_id;recall_log_listand/recalls?trace=filter by it;observability.WithTraceparentjoins library calls to a flow.
[2.140.0] - 2026-10-28
Added
Request ids from MCP through gRPC to the trace record and back (
x-request-id, MCP_metacortexdb/request_id); the Node and Python clients send one per call and put it on errors (#104).Plugins and the
cortexdb-livemod report a reply that is not JSON by its size, first bytes and request id, not by the parser's message (#104).Recalls explain themselves (
explain: true, and always in the trace): plan, candidates per retriever with raw scores, fused list, what each floor dropped, final hits. Adegradedfield says when the answer was keyword-only because the embedder was unreachable (#104).Metrics: embedder reachability, latency and a periodic probe; recalls per tool and key; empty and degraded recalls; floor drops; reply sizes.
/healthzincludes the embedder (#104).Structured logs (
log/slog,CORTEXDB_LOG_FORMAT,CORTEXDB_LOG_LEVEL) and a rotating trace file (#104).Memories say how they are known:
grade(verified,asserted, …) andgrade_whyon save and update,min_gradeon memory search and recall, guesses in their ownunverified_memoriessection of the context pack and markedUNVERIFIEDin auto-recall, graded capture, and--export-memory --min-grade verifiedfor sharing lessons without the guesses (#107).An opt-in recall log (
CORTEXDB_RECALL_LOG=1) withrecall_log_*tools, feedback that exports as recall-eval golden cases, a/recallspage in the live view, and graph shading and replay by recall (#104).
Changed
The server's log lines are
log/slogrecords (logfmt by default): the request log writesreq_id,rpc,dur_s,stages_ms.<stage>andslow=truewhere it wrotedur=…s,<stage>=…sand(slow). Every value is quoted when it needs to be, so a caller's tool name cannot write fields of its own. A parser of the old text line needs updating; the JSON-lines trace file keeps its fields and gainsreq_id,resp_bytes,result_bytes, the client's host and project, and a recall's explain record (#104).A knowledge search whose vector half failed now says it answered by keyword (
effective_mode: lexical, anddegraded), as a memory search already did (#104).The Python client's generated code is regenerated from the current protos, so
AdminService.Backup,ContractServiceandDecisionServiceare callable from Python (#104).find_nodesfolds each node's name once a call rather than once per name asked about: a batch of eight names takes 156 ms on a 30,000-node store, where it took 798 ms (#110). Lauro Foletti.
[2.139.1] - 2026-10-27
Security
POST /v1/graph/sparqlis a write unless the executor's parser says the query does not mutate, as the tool route and gRPCQuerySparqlalready were. It was classified as a read for every query, so a read-only key could store or delete triples withINSERT DATA/DELETE DATA. Reported privately by Lauro Foletti.
Changed
upsert_entitiesrecords the collections of the stored chunks it names on each entity, in union with the stored list, so a caller confined to one of them is served the entity (#105). Lauro Foletti.
[2.139.0] - 2026-10-24
Security
A request body that names one field twice (the same key, or two spellings encoding/json reads into one field, such as
queryandQUERYoruser_idanduſer_id), at any depth, is refused with 400 on REST and InvalidArgument on gRPC when it comes from a confined or read-only key, before any policy reads it. The policy read the last copy of a key, by its exact spelling, and the handler a different one, so such a body could read, overwrite or delete another collection's or user's rows, and a read-only key could run a SPARQL update. Reported privately by Lauro Foletti.Under a caller scope, the graph tools (expand_graph, get_nodes, knowledge_memory_neighbors, knowledge_memory_shortest_path, knowledge_memory_expand_entity_context, search_paths, search_chunks_by_entities) walk only the nodes and edges the caller is served. They walked the whole graph and served the caller's own nodes they reached, which told a key confined to one collection what another collection states, through a relation or a chunk it could not read.
The request log quotes a tool name that is not a plain word, so a caller cannot write fields or lines of its own into it.
Added
Confined keys may call the graph tools, confined by the facade (#97, #95, #96, #94); the graph lane of a query with a collection walks only that collection's graph (#98). Lauro Foletti.
Servers read the key file again when it changes (#87); every typed write signs
written_byfrom the calling key (#88); extract_conversation, memory_list_all and promote_to_knowledge honour the caller's memory scope (#89); the request log names the rows a tool call names (#90). Lauro Foletti.knowledge_gettakes a list of ids, at most 100, each served once, asmemory_getnow does too (#91). Lauro Foletti.check_against_sources: the facts of a sentence (numbers, names) that its cited sources do not contain, with no model involved; a name is found as a word of a source, not inside another word (#92). Lauro Foletti.glama.jsonnames the maintainer for the Glama listing.
Changed
The duplicate graph indexes idx_edges_from and idx_edges_to are dropped; the composite and walk indexes serve the same queries (#93). Lauro Foletti.
Keyword retrieval folds accents as the word index does (#100), the words of alternate queries join the keyword query (#101), and with alternate queries or keywords every phrasing runs, fused by rank (#102); a Swiss lexical retrieval set joins the eval (#99). Lauro Foletti.
In that fusion the question's own lists count in full and those of alternate queries and keywords half; an alternate whose words are all in the question is left out, ties go to the question's own rank, the keyword floor is applied before the cut to top_k, and the alternates and synonyms a search measures are capped. On a snapshot of a live brain, with planned queries (lexical): knowledge hit@3 8/18 → 14/18 and memory off-topic rows 6 → 3; on the golden sets as written, unchanged.
Fixed
Two documents that state the same relation keep an edge each, with its own provenance; the second overwrote the first's.
cortexdb-live: a recall whose reply passes the host's size limit is asked again as an index, and a reply that is not JSON is reported by what it says instead of as a JSON parse error.
[2.138.0] - 2026-10-23
Changed
Keyword hits are scored by the rarity of the words they match: relevance is the IDF-weighted share of the question's words a row covers, so a row that shares only a common word ("是什么", "CortexDB") with the question no longer scores high, and a hit must match more than one word of the question. Fusion ranks a hit found by both retrievers first and breaks ties in favour of the semantic one.
Knowledge search has a relevance floor: a chunk must reach a cosine of 0.40 or a keyword coverage of 0.25, and keyword memory hits a coverage of 0.25; below it fewer or no hits come back instead of noise.
CORTEXDB_KNOWLEDGE_VECTOR_FLOOR,CORTEXDB_KNOWLEDGE_KEYWORD_FLOORandCORTEXDB_MEMORY_KEYWORD_FLOORtune them. On a snapshot of a live brain: memory hit@5 0.604 → 0.750 (lexical) and 0.750 → 0.833 (hybrid), no labelled answer lost; hits returned for off-topic questions 43 → 1 (knowledge, hybrid). In lexical mode a memory that shares one word with a longer question is no longer returned.
Fixed
The HNSW index loaded from its snapshot could stay far behind the table and nobody noticed. On a copy of a live brain, 316 of 529 rows were missing from the loaded index, 66 nodes still held 64-d lexical vectors their rows had been re-embedded away from, and 1,296 of 1,509 nodes were tombstones. Vector search matched brute force on 1 of 18 golden queries, and knowledge hit@3 was 4/18. Opening a store now checks a loaded HNSW or IVF snapshot against the table: missing rows are inserted, ids that are gone are removed, and rows written since the snapshot are written again. A snapshot that cannot be patched (mixed or foreign dimensions, more tombstones than live nodes, or over 30% of rows off) is rebuilt. The outcome is logged, to the standard logger when the store has no logger. On the same copy: 18/18 and 16/18.
Writing a row the index already held kept the old vector. Re-embedding,
UpsertBatchand transactional upserts all go throughHNSW.Upsertnow, which replaces the node, and treats an identical vector as no change.A reloaded HNSW index lost its dimension, so the first insert after a restart set it. One 64-d lexical vector written first, and every 768-d write was refused until the next restart.
Loadnow takes the dimension from the nodes. A rebuild uses the dimension most rows have, not the first row's.DeleteByDocID,ClearByDocID,ClearandDeleteCollectionnow take the deleted ids out of the in-memory indexes as well.
[2.137.0] - 2026-10-23
Added
ingest_documenthashes its content. An identical re-ingest (same text, chunking, collection, metadata and vectors) answersunchangedand writes nothing; a re-ingest of changed text keeps the chunks and the graph the caller attached where they still apply.dedupe: truerefuses content already stored in the same collection and names the holder (#75).Documents can expire:
expires_atoningest_document,knowledge_saveandknowledge_updateleaves an expired document out of every search, chunk tool, graph walk and aggregate.sweep_expired,DB.SweepExpiredandcortexdb-mcp-stdio --sweep-expired [--dry-run] [--json]delete expired documents and memories (#76).Documents and memories carry a review decision (
review_status,reviewed_by,reviewed_at); withCORTEXDB_REVIEW_GATE=1orWithReviewGate()proposed and rejected rows are left out of search and recall. A curation filter, not access control (#77).
Fixed
With an embedder,
ingest_documentwrote lexical vectors of another width, which the vector index refused, so its chunks never reached semantic search. It now embeds asknowledge_savedoes (#84).vector_dimension_repairwrote each repaired row back without its document, metadata and ACL. It now changes the vector only, and gives back to chunk rows an earlier repair stripped their document and metadata, from the chunk id and the document record; their ACL cannot be recovered (#84).
Upgrade notes
The engine keeps its own marks under keys that start with
_(_expires_at,_review_status,_content_sha256,_vectors, …). A database that already used_expires_ator_review_statusas ordinary metadata will see those rows hidden; run--sweep-expired --dry-runbefore the first real sweep.Documents stored before this release carry no
_vectorsmark, so their nextingest_documentrewrites them once, with the current embedder's vectors, instead of answeringunchanged.
[2.136.0] - 2026-10-22
Added
A scope-confined key can call the toolbox:
CallToolover gRPC andPOST /v1/tools/<name>over REST, which were refused to it outright. An explicit allowlist inpkg/authz(one line per tool, closed by default, held complete against the tool definitions by a test) says which tools a confined key may call and classifies every argument: the confined field is forced to the key's value, two-store tools are gated per store, and every argument that names a row by id, nested ones included, is checked against the row, a foreign id answered as missing. Declared entities and relations, engine metadata keys and path retrieval are refused to a confined key (#72, #73, #74).
Changed
memory_savechecks itssupersedestargets before it writes: a missing or foreign target now leaves no new memory, where it used to store the memory and then report the target missing (#72).
[2.135.1] - 2026-10-22
Fixed
Over REST, a confined key could overwrite a row of another scope by naming its id on
POST /v1/knowledgeorPOST /v1/memory, and so move it into its own scope, and could retire another user's memory throughsupersedes. The REST policy now reads the row an upsert names, under every spelling of the id field, and answers a row outside the key's scope as not found, as gRPC does; a supersede target outside the scope, or missing, is refused before anything is written. An id of blanks is now guarded like any other on both ports (#82).InsertGraphDocumentlet a collection-confined key overwrite a document of another collection by id; it now goes through the same guard asSaveKnowledge(#83).SQLite's
JSONArrayContainsfailed the whole statement on a node whose properties held no JSON, so one such node broke every document deletion (#79).The geo index shared a point's metadata map with callers on the way in and out; every path now copies it (#80).
Redeclaring a histogram with different buckets returned the first declaration silently; it now panics like any other conflicting redeclaration (#81).
[2.135.0] - 2026-10-21
Changed
cortexdb.WithMemoryOwners(v2.134.1) is replaced bycortexdb.WithCallerScope, which carries a confined caller's memory and knowledge rules and its identity.authz.Key.CallerContextbuilds it; both ports already call that, so a server needs no change.
Fixed
A key confined to some user_ids could create a bucket nobody had used yet, the global bucket of a new namespace or a session id another user had not yet opened, and own what unconfined writers saved there later. The global bucket now belongs to nobody whoever writes to it first, a confined key cannot create it, and a save that names a user_id is refused (invalid) in a session another user owns.
Identical memory searches that arrive together share one run; a run is no longer shared between callers with different scopes, which handed a narrower key the answer of a wider one.
POST /v1/recallfor a confined key reached the store its scope does not cover: a user-confined key recalled every collection's knowledge, a collection-confined key any user's memories. Each half of a recall now answers only what the key reaches, the rules the typed RPCs apply to rows read by id, and a confined caller gets no graph facts, whose shared edges carry no scope.The graph lane of cortex_query stays in the request's collection (#78).
[2.134.1] - 2026-10-20
Fixed
A key confined to some user_ids could read and write another user's session memories by sending its own
user_idbeside the other user'ssession_id: the policy checked the fields and the session then picked the bucket. A confined key now reaches only memory buckets its user owns, over gRPC, REST and recall alike. A bucket it may not read answers like an empty one, and a write into it is refused as not found. The global bucket, which no user owns, is out of reach for a user-confined key too, as unowned rows already were for reads by id. Unconfined keys and the legacy token are unchanged.REST saves are signed with
written_byandupdated_byfrom the authenticated key, as gRPC saves have been since v2.134.0.
[2.134.0] - 2026-10-19
Added
Saves are signed. Behind a key policy, memory_save, memory_update, knowledge_save and knowledge_update record the authenticated key as
written_by(a save) orupdated_by(an update, which keeps the originalwritten_by), replacing whatever the caller wrote under those names.cortexdb.WithWriterdoes the same for an embedded server.Memory search hits carry
via, the retrievers that found them (semantic,keyword,graph), andsimilarity, the raw vector similarity before the importance and recency boosts that makescore.Auto-recall explains each hit, in the injected block and in the cortexdb-live band: the retriever and its own score, the query terms the hit contains, and where it came from (date, agent, source, model, key, host, project, session, URL). The shared
legacy-tokenis not shown as a writer.Captured memories record
agent,hostandprojectbesidesession.
Changed
Auto-recall asks with the conversation. The cortexdb-live planner gets the project and the last four turns and resolves what the message points at ("这个修复", "按建议", "the fix") before it writes the query. Without a plan, and in the shell hook, the query carries the project and the start of the last reply, so a short follow-up no longer recalls another project's notes.
[2.133.0] - 2026-10-19
Added
A key's scope may list several values per field:
user_ids,scopes,namespacesandcollectionsbeside the singularuser_id,scope,namespaceandcollection, which keep working; the two forms add up. A request still names one value; the list widens what the key may name. A list holding an empty string is refused when the key file loads (#71).The gRPC request log names the key behind each call (
key=<id>, never the secret) and the tool a CallTool ran (tool=<name>), on served and denied calls alike. CORTEXDB_TRACE_FILE records carry the same two fields (#68).ingest_document takes
source_urlandauthorand keeps them, with the caller's metadata, on the document record and the document node, as knowledge_save does, so a search hit reports where the document came from. A re-ingest replaces the record's provenance (#69).
Fixed
A scope-confined key could reach another user's rows over REST by sending a second spelling of the field:
{"user_id":"hermes","USER_ID":"zeus"}passed the check, which read only the exact key, while the handler's JSON decoder took zeus. Every case variant, top-level or nested, is now held to the confinement (#64).A graph search confined to one collection reached another collection's chunks through an entity both mention. Entity nodes now record which collections mention them, the walk keeps to entities the searched collection knows, and every chunk it reaches is checked against the collection. Unconfined searches are unchanged (#70).
The SessionEnd capture hook declines before writing anything when no LLM is configured, and removes its payload file however the capture ends, including a signal; payloads older than a day are swept (#67).
IVF training rejects vectors of the wrong dimension instead of panicking (#65). OptimizedBinaryQuantizer.Project returns nil for a vector longer than its input dimension instead of panicking (#63).
[2.132.1] - 2026-10-18
Fixed
changes_since over MCP failed output validation for every event that carried a before or after row summary: the inferred schema read those fields as byte arrays and rejected the JSON objects the change feed writes. They now get an open schema and stay embedded JSON (#61).
MultiIndex rerank keeps the primary index's distance as the first vote. A candidate no secondary index returned used to score zero and rank first, and with no secondary indices every result came back with distance zero in map order (#57). MultiIndex and HybridIndex return nothing for a non-positive k instead of panicking (#58).
HNSW and FlatIndex batch inserts validate every vector's dimension before writing, so a batch with one bad vector leaves the index unchanged (#55, #56).
A k-means cluster that attracts no vector keeps its centroid instead of being reset to the origin, where it won encodes for any vector near zero (#59). SearchBinary sorts in O(n log n) and keeps database order on equal distances (#60).
Geo search between antipodal points returned NaN distances, so a point on the far side of the globe was missing from KNN and whole-sphere radius searches (#54).
[2.132.0] - 2026-10-17
Added
In the live graph view, clicking a memory node opens the whole memory: its text, rendered with the headings, bullets, bold and code it was written in, when it was saved, its scope and importance, and its metadata (type, project, topic, recall count and the rest). The node itself is labelled with a clipped summary; until now the panel showed only that. Works on a local database and on a shared brain, where it reads through memory_get, which is added to the view's read-only tool list. The panel widens for a memory.
[2.131.0] - 2026-10-17
Changed
The live graph view re-reads the graph only when the change feed says a node or edge changed, and once a minute regardless. It used to pull the whole graph every two seconds over a fresh connection: on the shared brain (2,000 nodes, 5,600 edges) that is about a second of server time a read, and one open page held the brain at 35% of a core on a two-core host. With the page open it now sits at 7%, against 6% with no page. A brain without changes_since is read on every tick, as before.
Every call the view makes to a shared brain goes over one kept connection instead of dialing per call.
The view loads 3d-force-graph, three and the bloom pass as one bundle embedded in the binary, gzipped and cached for good. The page used to load two copies of three from unpkg, and drew nothing offline.
Pages and JSON are gzipped: the graph goes from 755 KB to 112 KB, the page from 141 KB to 47 KB. The event stream is left uncompressed.
A flash animates the node's mesh instead of re-setting every accessor on every node and link every 90 ms. With twenty nodes flashing the page held 30 fps and spent 1.75 s of every 3 s in long tasks; it now holds its full frame rate with none.
The view and the GraphFlow 3D export open light. Dark and follow-the-system are one click away and remembered.
Added
Nodes are named as the camera comes close: a sphere drawn at least 7 px across gets its label, largest first, without overlaps. Zoomed into a neighbourhood of the shared brain that is 30 names where there were 3.
The type filter has "All but memories" (
?type=!memory): memories are 906 of the shared brain's 2,000 nodes, and hiding them shows the entities and projects between them.
[2.130.1] - 2026-10-16
Fixed
A one-word query was padded with the shortest memories in the store. After the memories that contain the word, the semantic hits filled the rest of topK, and a single word embeds next to whatever is shortest: on the shared brain the user's city (seven characters) and name came back for 48 of 60 one-word queries. Semantic hits for a one-word query now have to clear the memory floor plus 0.16 (0.47 on embeddinggemma). They reach 3 of the 60, hit@5 stays 0.917, and a memory that means the word without containing it (在持美股 for the VOO and OKLO holdings) still fills. Sentence queries are unchanged: golden-set hit@5 0.792 and MRR 0.627 before and after.
Index and quantizer fixes contributed in #43–#53: IVF inverted lists stay aligned after Delete and an ID added again replaces its entry; LSH and HNSW replace or re-insert an ID that was there before; HNSW and IVF snapshots load atomically and inconsistent ones are rejected, so the index is rebuilt instead of panicking; a non-positive k returns nothing; scalar, binary and product quantizers validate their state, sizes, training vectors, queries and codes; the IVF size is read under the index lock.
TestProductQuantizerSearch failed about 8 times in 2,000 runs: it required the query's own code to rank first, which a lossy product quantizer does not promise. It now requires it in the top 5.
[2.130.0] - 2026-10-16
Changed
Memory recall with an embedder fuses the semantic and keyword answers by reciprocal rank, as knowledge search already did. The semantic list used to fill topK first and the keyword list only what was left, so five vaguely similar memories always beat an exact match. On a snapshot of the shared brain: hit@5 0.771 → 0.792 on the golden set (MRR unchanged), and on 24 one-word queries the share of top-5 memories that contain the word went from 0.30 to 0.85.
A query that is a single term (an acronym, a name, up to four Chinese characters) is answered by the memories and knowledge chunks that contain it first, with semantic hits filling the rest. Asking "HA" returned the user's name and city; it now returns the memories about HA.
Fixed
Text with few spaces, such as Chinese, was cut into fixed-width windows that started and ended mid-word and could leave a last chunk of a single ")". Windows now end at a space or punctuation, and a short remainder joins the last window. Documents saved before keep their chunks until they are updated.
Knowledge hits with equal scores were ordered by id, so a document sorting early in the alphabet won every tie. They keep the order retrieval gave them.
[2.129.3] - 2026-10-15
Fixed
geo.GeoIndex.Insertleft an earlier point with the same ID in its grid cell, so a moved point was still found at its old location and a point inserted twice in place was listed twice (#39).geo.GeoIndex.SearchRadiussized its longitude window as at the equator, so it missed points inside the radius away from the equator and never looked across the antimeridian (#40).ProductQuantizer.DeserializeCodebookspanicked on truncated data, allocated from an untrusted header, and left a half-loaded quantizer on failure; it now checks the header against the body first (#41).BinaryQuantizer.SearchBinaryranked codes of the wrong length first and panicked on a non-positivetopK(#42).
[2.129.2] - 2026-10-14
Fixed
The cortexdb-live mod did not load on Claude Code 2.1.299, which refuses an optional call on
$($.session.root?.()) as reading the event as a value, so recall and capture were off in every session. It calls$.session.root()directly and keeps absolute paths where the call fails.
[2.129.1] - 2026-10-14
Fixed
A client of a shared brain no longer fails a call with
connection reset by peerwhen the connection it held was lost while idle — the brain failed over, or the connection was dropped on the way. The remote MCP server and liveview ping an idle connection every 30 s, so a dead one is replaced before a call needs it, and retry a call that fails as UNAVAILABLE once on a fresh connection; the server never returns that code itself. The gRPC server accepts those pings (one per 20 s, with or without a call in flight); an older server hangs up on them, so upgrade the server first.
[2.129.0] - 2026-10-14
Changed
A memory recall is several times faster, and stays fast under a burst. Measured on a two-core node against a copy of a 4,300-memory shared brain with its embedding model on another machine: one recall went from 1.0 s (p50) / 1.6 s (p95) to 0.30 s / 0.45 s, and sixteen at once from 6.2 s / 7.8 s to 0.82 s / 0.84 s.
The vector scan keeps each session's decoded vectors in memory, checked against a version SQLite triggers bump on every insert, delete or update of a vector or its session — by any writer — and reads only the rows that make the cut in full. Vectors are encoded and decoded directly instead of one value at a time through
binary.Read.The full-text update triggers on
messagesfire on a change ofcontentonly. They fired on any column, so every recall counter written to a memory's metadata rewrote its text in both full-text indexes.Query embeddings are cached (2,048 texts), and concurrent requests for the same text share one call to the model.
The keyword search runs beside the embedding and the vector search instead of after them, in memory recall and in hybrid knowledge search. A query embedding gives up after 800 ms (
CORTEXDB_QUERY_EMBED_TIMEOUT_MS) and the search answers by keyword; hybrid knowledge search no longer waits on the model without a bound.Identical memory searches that arrive together share one run, and each is still counted as a recall.
[2.128.0] - 2026-10-14
Added
A shared brain can be reached over TLS:
CORTEXDB_REMOTE=https://host[:port](port 443 by default) dials with TLS checked against the system roots, for acortexdb-grpcpublished behind a TLS-terminating proxy such as Caddy with anh2c://upstream.host:portandhttp://host:portstay plaintext.cortexdb-mcp,cortexdb-grpc -healthand the live view all go through the samepkg/rpc.Endpoint.
[2.127.2] - 2026-10-14
Fixed
The context pack that recall returns clipped each memory and knowledge section at a byte count, which split a CJK character and left U+FFFD (
/dev/lab/data �...). The cut keeps the same byte budget and steps back to the start of the character it would have split.
[2.127.1] - 2026-10-14
Fixed
Knowledge snippets, graphflow summaries and rule-syntax errors were cut at a byte count, which in Chinese, Japanese or Korean text lands inside a character and leaves U+FFFD (
高可用�) at the end. They are cut at a character count now; a snippet is 180 characters.The Obsidian panel no longer repeats a knowledge document's title at the start of its snippet, and expanding a card shows its text as it is, rather than rendering a flattened snippet that begins with
#as one large heading.
Added
The Obsidian panel reads a result in full: its title, or the expand button, opens the whole memory or the whole knowledge document rendered as Markdown. Copy and insert take the full text.
[2.127.0] - 2026-10-14
Added
An Obsidian plugin (
plugins/obsidian-cortexdb, released ascortexdb-obsidian.zip). A side panel shows what the brain knows about the open note — memories, knowledge, and the entities they touch — and answers typed questions. Commands remember a selection as a memory, and send a note or the vault as knowledge documents (obsidian:<vault>/<path>, only changed notes, deleted ones taken out, optionally as you write). Export the brain writes every memory and entity as linked notes, so the graph view and backlinks show the brain; an edited memory note goes back throughmemory_update. The server token lives in Obsidian's keychain, not the vault.
Fixed
cortexdb-grpc -hprinted$CORTEXDB_GRPC_TOKEN: the flag took the token as its default, and the help screen shows defaults. The token is now read from the environment after the flags are parsed.
Changed
The DeepSeek Harness plugin installs from this repository's release tags (
dsh plugin --profile web add "github:liliang-cn/cortexdb#semver:^2&path:/plugins/dsh-cortexdb-memory") instead of npm, and CI no longer publishes it there. A release needs no npm token, and the plugin is never behind the binaries it downloads.
[2.126.0] - 2026-10-14
Added
A session starts with an index of what the brain knows about its project. The plugin's SessionStart hook (
cortexdb-mcp --session-context) finds the graph node of typeprojectnamed like the working directory or its git repository (case and punctuation ignored) and lists the memories that mention it, newest first, one line each — id, date, first line — leaving out superseded ones;CORTEXDB_SESSION_CONTEXT_MAX(default 30) caps it. The first prompt of a session usually says too little for the per-prompt recall to find anything.Memories about files. A capture lists the files the session read and modified, named from their repository root, and the model links each memory to the files it is about as entities of type
file. A new PreToolUse hook on Read, Edit and Write (--file-context) hands the agent the current memories about a file before it opens it, once per file per session.knowledge_memory_recalltakesdetail: "index": the same hits in the same order, each cut to its first line, for a caller that picks what to read before reading it.memory_gettakesmemory_idsto fetch several at once, in order, listing unknown ids inmissing.Captures are scrubbed before the model sees them and before anything is saved: text between
<private>and</private>is removed, and credentials in the shapes they are issued in (API keys, GitHub, AWS, Slack, Google and npm tokens,Bearertokens, PEM private keys,password=-style settings) become[REDACTED]. This covers Claude Code, Codex and DeepSeek Harness sessions,import_agent_memory's session import and the cortexdb-live mod.A captured fact that repeats a current memory word for word (case and punctuation aside) is not saved again; the import reports such facts as
duplicates.
Fixed
A hook's context block could exceed what Claude Code passes on: past about ten thousand characters it replaces
additionalContextwith a short preview, so the model received none of the recall. Every hook block now stays under 9,000 characters, dropping whole entries from the end and saying how many it left out; the cortexdb-live mod's recall does the same.Two tests failed for reasons outside the code: the hook tests inside a session running the cortexdb-live mod, whose variables tell the hooks to stand down, and the reembed backlog test whenever the background healer filled the backlog first.
[2.125.1] - 2026-10-14
Thirty of these fixes came as pull requests from @rudycelekli (#9–#38), each with a regression test; three follow-ups adjust how two of them fail.
Fixed
Graph: PageRank dropped the score of nodes with no outgoing edges, so scores no longer summed to 1; it is redistributed now. GraphML and GEXF imports gave vectorless nodes a fake 3-d zero vector and replaced an unreadable vector with zeros; vectorless nodes stay vectorless and a malformed vector fails the import. RDF/XML accepted mismatched and unclosed elements. SPARQL
DISTINCTandGROUP BYkeys were joined with separators that values can contain, so different solutions could collide. SHACL did not checkxsd:negativeIntegerorxsd:nonPositiveInteger.Cypher:
range()with a step near the integer limits wrapped around instead of stopping, andsubstring(),left()andright()accepted positions past what a string slice can hold.Semantic router: a route whose best score was zero or negative (negative Euclidean distance, negative dot product) could never be chosen; functions were registered under the function name while routing returns the route name; per-function extractors and a schema's
requiredlist were ignored; keyword patterns were not escaped and extracted values were lowercased; a failed utterance embedding was cached for good; the cached embedder and the sparse encoders' refit were not safe under concurrent use; one batch could add two routes of the same name, and a nil route panicked.agentmem:
AppendContextread, concatenated and wrote back, so concurrent appends lost sections (20 concurrent appends kept 15 on SQLite and 5 on PostgreSQL); it is one upsert now. A reflection could mark an observation of another scope stale; such a target is now ignored and reported inIgnoredTargets, and the observation is still saved.Importers and connectors: duplicate CSV header names silently shadowed a column (now an error; an empty header becomes
column_<n>); a negative sample size was used as given instead of the default; email masking cut multi-byte local parts and printed a value with no@unmasked; columns that were NULL in every row were dropped.quickand text APIs wrapped embedding errors with%v, soerrors.Iscould not see the cause; an empty vector batch with non-empty contents returned success. CSV export did not report a failed final flush. Key files with trailing data after the JSON document were accepted. Exported memory frontmatter mis-escaped backslashes, and now keeps<,>and&readable.graphflow lost extraction metadata in JSON exports and aliased the caller's metadata map while merging nodes.
The DeepSeek Harness plugin accepted a truncated gRPC frame or protobuf field as a successful, shorter answer.
[2.125.0] - 2026-10-12
Added
DeepSeek Harness support.
plugins/dsh-cortexdb-memory, published to npm ascortexdb-dsh-memoryand installed withdsh plugin --profile <profile> add cortexdb-dsh-memory, is a cordis bundle that gives dsh what the Claude Code and Codex plugins give those agents: recall before each prompt a person types (the same question, top-3, snippets and wording as their recall hook, across the whole brain unlessscope/namespace/userIdnarrow it), capture of a session's durable facts when it goes quiet or ends (auto:dsh-<session>:<slug>, retiring the memories they make untrue), andcortexdb_memory_search/_store/_forgettools. It connects to a sidecar atendpointor starts the releasecortexdb-grpcof its own version against the local database. It has no runtime dependencies — gRPC is spoken overnode:http2— because dsh installs plugins with pnpm, which refuses unapproved build scripts, and every gRPC library for Node brings protobufjs and its postinstall.cortexdb-mcp --capture-sessionreads a dsh session log as well as a Claude Code transcript, andimport_agent_memoryhas adsh_sessionssource (included by--sessions) that distils past sessions from$DSH_HOME(default~/.dsh). dsh's logs are Zstandard-compressed JSON lines; a session still being written is read up to its last complete frame.The gRPC server logs every call slower than
CORTEXDB_SLOW_MS(default 1000,0for none) with the time each stage took — embedding, vector, lexical, graph — so a slow recall says which of the embedder, the index or the graph it waited on.CORTEXDB_TRACE=1logs every call;CORTEXDB_TRACE_FILEalso appends the records as JSON lines.
Fixed
A recall waited as long as its embedder took to embed the query — 24 seconds when the embedder was a model on another machine that had to load first. Embedding the query is bounded at 3 seconds; past it the recall answers by keyword, as it did anyway once the caller gave up.
The live view's library had no project shelves for a local brain: loading it dropped the
mentionsedges memories make, which are what shelve them.TestHealerBacksOffWhileTheEmbedderIsDownfailed on a loaded machine: it allowed eight embedder calls for about five healing passes, but each pass makes two (21 memories, in batches of 16).
[2.124.0] - 2026-10-08
Added
cortexdb.EmbeddedConfig(path), a configuration for small devices — a Raspberry Pi, an industrial gateway, a router-class ARM board. The HNSW index is held as SQ8 codes; SQLite keeps a 1 MiB page cache on each of at most 4 connections, maps up to 64 MiB of the file and puts temporary b-trees in files; the index snapshot is saved every 10 minutes when it has changed. On 100,000 768-d vectors a store opened from its snapshot in 0.28 s with a 193 MB heap and a 297 MB peak RSS, against 1.7 s, 485 MB and 1,982 MB for the default configuration before this release.Config.Quantization,Config.Binary,Config.ResourcesandConfig.SnapshotIntervalon the facade, which offered none of them: the store could quantize its index, but nothing opened throughcortexdb.Opencould ask it to.core.ResourceConfigsets SQLite's per-connection page cache, memory mapping, temporary storage and pool size.
Fixed
Rebuilding the HNSW index from stored vectors split them among four goroutines, built a graph from each and merged them — four graphs that hardly linked to each other. Recall@10 was 0.24 on 100,000 clustered 768-d vectors; it is 0.65–0.72 now that the rebuild builds one graph. Every open without a usable snapshot took this path.
index.HNSW.InsertBatchParallelnow inserts on one goroutine for the same reason.The rebuild decoded every stored vector into one slice before inserting any, doubling its peak; it streams now. Peak RSS for that open fell from 2,359 MB to 786 MB at 100,000 vectors (435 MB with
EmbeddedConfig).Index snapshots were one blob, built whole in memory to save and read whole to load. They are written and read in 4 MiB chunks; a snapshot written before this release still loads.
A snapshot was rewritten on every close and every auto-save tick, changed or not (
AutoSave.MinChangeswas never consulted). It is written only when the index has changed.Closing allowed five seconds to save the snapshot, which a large index overran; the write was abandoned and the next open rebuilt everything.
A quantized HNSW or binary index reopened with its dimension left to auto-detection was built without its codes — float32 vectors, or no binary index at all and a table scan per search — and a float32 snapshot was loaded as it was under a quantized configuration. Both now learn the dimension from the stored vectors, and a float32 snapshot is rebuilt.
The binary index decoded every vector into memory to learn its center; it streams the table twice instead and keeps only the codes.
A search with no vector index held the whole table in memory to sort it; it keeps the best TopK as it scans.
SQ8 distances decoded each code bit by bit into a fresh vector. 8-bit codes are measured in one pass with no allocation, and building a graph no longer re-measures candidates it has just ranked or prunes a full neighbour list on every new link: a 20,000-vector quantized rebuild went from 1 m 45 s to 15 s.
[2.123.0] - 2026-10-07
Fixed
Memories saved while the embedder is down get their vectors once it answers. A save that could not embed used to fall back to keyword search silently and keep its row without a vector for good; now a background pass embeds every memory that lacks a vector (or has one of the wrong size) when the store opens and again after any save that had to go without one, backing off from 30s to 30m while the embedder stays unreachable. Tune or disable it with
WithVectorHealing.An embedder outage is visible: recall's decision says it answered by keyword search only and how many memories still lack vectors,
graph_healthraises anembedderalert, and gRPCInfo, HTTP/infoandcortexdb-grpc -healthreportembedder_healthy,embedder_errorandmemories_without_vector(the health probe still exits 0).db.EmbedderStatusreturns the same.Semantic memory recall drops the 0.28–0.31 noise band: the floor is now 0.31 (
DefaultMemorySemanticFloor), configurable withWithMemorySemanticFloororCORTEXDB_MEMORY_SEMANTIC_FLOOR. Keyword matches are never floored.
[2.122.1] - 2026-10-06
Added
import_agent_memory, an MCP tool that brings in the memory a person already has: Claude Code's memory notes,CLAUDE.md/AGENTS.md, and Codex's own memories (~/.codex/memories_1.sqlite,~/.codex/memories) by default; withclaude_sessions/codex_sessions, past transcripts distilled into memories the way a session is captured when it ends, a few per call (max_sessions,since), remembered in~/.cortexdb/imported-sessions.jsonso none is read twice. It reads files where the MCP server runs and writes to the brain that server uses, local or shared.dry_runcounts first.Capture retires what it made untrue. The SessionEnd capture, the cortexdb-live mod and the session import look each new fact up and ask the model which existing memories it contradicts; those are saved as superseded by it — kept and exported, no longer recalled as current. The later date wins: an old session's fact never retires a newer memory, and is dropped when a newer one already contradicts it. At most five per pass.
A superseded memory records when (
superseded_at), so a mistaken pass can be found and undone.
Fixed
--import-agent-memorywrote to the local file even whenCORTEXDB_REMOTEnamed a shared brain, and reported success. It now runs the same import as the tool, into the configured brain, and takes--sessions,--since,--maxand--dry-run.Live view library: walls, cornices, window mullions and bookcases no longer flicker where two faces shared a plane.
[2.122.0] - 2026-10-06
Added
Live view: a library. The same brain, drawn as a reading room at
/libraryand linked from the graph and ontology pages. Each memory is a book on the shelf of the project it names — the most specific one, when it names several — and shelves stand in wings cut from the projects' own relations with the module's Leiden. Memories that name no project fill the general stacks, documents the reference room. An entity is an index card: opening it threads every book that mentions it back to the catalogue and lays its relations out on a table. A book opens on the lectern with its text paged, its index and the books sharing most of it, each a link. Recalls reported over the activity stream light books where they stand. Day and night follow the graph page's setting; Chinese or English follows the browser (?lang=).serve_graph_3dreturnslibrary_urland opens it withview: "library".
[2.121.2] - 2026-10-06
Fixed
cortexdb-live: every session started with "unreachable: no connected MCP tool graph_statistics" and no node count for five minutes. The mod asked for the count before the session's MCP servers had finished connecting. A server still connecting is now asked again a few seconds later, and a successful recall fills in a missing count.
[2.121.1] - 2026-10-06
Fixed
cortexdb-live: outside bypass mode every recall, node count and capture failed with a clipped "recall failed: … ref". A mod's own MCP calls pass the session's permission rules and a hook has no prompt to ask with, so they were denied. The status line now names thepermissions.allowrules to add, and the README gives them.cortexdb-live: the band and status line came out in English on a Chinese system, because Claude Code's ownlanguagerow reads English until set. Only a Chinese value there decides now; otherwise the locale and macOS's language do.
[2.121.0] - 2026-10-06
Added
cortexdb-live, an optional Claude Code mod in the same marketplace (/plugin install cortexdb-live@cortexdb). Haiku plans each prompt's recall (Chinese and English keywords, aliases, entity names, retrieval mode, or no search at all for a bare go-ahead); what was recalled shows above the prompt (/cortexdb-showbrings back one that was hidden); the status line shows the brain, its size and the last recall's time; and an idle session is distilled into memories with haiku. No API key: it runs on Claude Code's own model access.languageisauto,zhoren.
Changed
The plugin's shell recall and capture hooks stand down in a session the mod runs (
CORTEXDB_RECALL_BY_MOD,CORTEXDB_CAPTURE_BY_MOD), so nothing is recalled or captured twice. Codex is unaffected.
[2.120.2] - 2026-10-05
Fixed
Live view: a node with no relations could be found but not opened — clicking it said "No node …", because expanding it returned nothing, not even the node itself. Expanding such a node now returns the node.
Live view: no more lavender. The hashed colour of a type whose hue fell in indigo (
fileamong them) read violet once lit and shaded; hues 225°–250° are now pulled to a plain blue. On light grounds, dimmed nodes faded to a pastel of their hue, which made blue ones lavender; they now fade to their own grey.Live view, light themes: answer source titles and ids, Cypher's table (its header was a near-black bar), facts, the contract's figures and an inspector field's value still used the dark theme's literal greys and were close to invisible; they now use the theme's inks.
[2.120.1] - 2026-10-04
Fixed
SPARQL: a trailing
VALUESclause afterGROUP BY,HAVINGorORDER BYfailed to parse (unexpected token "VALUES"): those modifiers read it as one more group or sort key. SPARQL 1.1 puts the ValuesClause after every solution modifier; it now parses after any of them.
[2.120.0] - 2026-10-04
Added
The execution graph API: an agent's record of its own run, as graph records.
StartRun,BeginStep/EndStep(the two-phase write: a step is on the record as running, withTRIGGEREDedges from the steps it consumed, before its work runs),RecordStep,FinishRun; and to read it backGetRun,ListRuns,GetStep,RunSteps,SummarizeRun(tokens, cost, open and failed steps, critical path, least confident step),StepLineage(upstream or downstream overTRIGGERED) andReplayRun(the run as of any instant, from history). Step kinds are node types and Cypher labels; sub-agent runs hang off the step that started them (SPAWNED). The nine are also MCP tools,execution_*, five of them writes.examples/19_execution_graphnow uses the API.SPARQL 1.1 in full, measured against the W3C suites on SQLite and PostgreSQL: query 329/331 (the two are a deliberate leniency,
"1" + "2"is 3), update 157/157, results formats 10/10, federated query 10/10; SPARQL 1.2 269/269. New: every property path (sequence, negated property sets,?, with SPARQL's set/bag semantics); XSD casts and calls by IRI; trailingVALUES;GRAPH ?gover patterns that do not name the graph, subqueries andMINUSinside it; bottom-up scope forFILTER/BINDand the grouped-SELECT,BINDand subquery syntax rules;ADD,COPY,MOVE,CLEAR,DROP,CREATEandLOAD(refused: the store never fetches);USING/USING NAMEDas the update's dataset;SELECT *in order of appearance.SPARQLResult.WriteResults: SPARQL results as JSON, XML, CSV or TSV, triple terms and base directions included;ReadSPARQLResultsJSON.SERVICEthroughGraphStore.SetSPARQLServiceHandler, off by default so a query never reaches the network on its own;HTTPSPARQLServiceis a SPARQL 1.1 Protocol client.RDF/XML import (
RDFFormatRDFXML), including the RDF 1.2 additions (its:dir,rdf:parseType="Triple", annotations); the W3C RDF 1.1 and RDF 1.2 RDF/XML suites pass in full.SHACL Core in full — every property path,
sh:qualifiedValueShapewith its counts and disjointness,sh:lessThan,sh:lessThanOrEquals, implicit class targets,sh:deactivated— passing the W3C Core suite 98/98; and SHACL-SPARQL:sh:sparqlconstraints and SPARQL-based constraint components with pre-binding, 21/22 ($shapesGraph, optional in the spec, is not supported).SHACLValidationResult.SourceConstraintnames the constraint.The rest of the OWL 2 RL rule tables:
someValuesFrom,allValuesFrom,hasValue,intersectionOf,unionOf,oneOf, max (qualified) cardinality andowl:hasKey, the schema rules over restrictions and lists, and as reported contradictions irreflexive and asymmetric properties, negative property assertions,complementOf,owl:Nothing,AllDisjointClasses,AllDisjointPropertiesandAllDifferent. The rules are gated on their schema, so a graph that states no OWL pays about 5% on a refresh.Node property indexes:
GraphStore.IndexNodeProperty(ctx, key)puts an expression index on one JSON property (NodePropertyIndexes,DropNodePropertyIndex; opt-in per key, as each is paid for on every write). Equality andINfilters on an indexed key become index lookups inGraphFilter.Propertieswith no change of its own, and in Cypher, which reads the index catalog and also starts its joins from such a filter as it does fromid(n) = …; on SQLite numeric ranges on an indexed key use it as well. Results are identical with and without an index. Keys are identifier characters only, and a key is written into SQL only from the catalog.
Changed
A graph node no longer needs a vector.
UpsertNode,UpsertNodesBatchandExecuteBatchaccept a node with none, for structural records found by id, type, properties and edges rather than by similarity (an agent's execution steps, runs, bookkeeping), which previously had to invent one. It is stored as an empty vector, so existing brains need no migration, and it is never a vector-search candidate: hybrid search,GraphVectorSearch,SimilarityInGraphandPredictEdgesskip it or give it no vector score, instead of scoring it 0 (cosine) or -Inf (Euclidean). A node rewritten without a vector is removed from the HNSW index and from pgvector's table.
Fixed
SPARQL
FILTERandBINDno longer see variables bound outside their group (a nested group'sFILTER(?v = ?z)read an outer?z; aBINDin aUNIONbranch computed from an outer variable), and aFILTERapplies to its whole group wherever it is written.IRI()resolves againstBASE,BNODE(str)is per solution, decimal results print exactly (the W3C test'sSUMis11.1, not11.100000000000001), andSELECTaliases already in scope are a syntax error.SHACL:
sh:datatypechecks the range of bounded integer types ("300"^^xsd:byteis ill-formed), a date with a timezone and one without are incomparable, and onlysh:uniqueLang trueactivates the constraint.Concurrent writers in one process no longer fail with
database is lockedon SQLite. SQLite's busy handler polls with sleeps growing to 100ms, so under steady contention the writer that had waited longest kept waking after the lock had been taken again and could starve pastbusy_timeout: eight goroutines batch-upserting an execution graph lost 3–9 writes in 15,000, after waits of 5–8.5s, and with the change feed off the worst wait still reached 4.6s. The store now wraps its SQLite connections so that a transaction that may write, or a statement outside one that writes, first takes its turn on a first-come-first-served queue shared by every pool on the same file in the process; reads and read-only transactions never queue. Throughput is unchanged, there are no failures, and the slowest write in the same run takes 48–78ms. Other processes still meet at SQLite's lock as before.Cypher queries anchored on an id (
WHERE id(t) = $id, or a node bound by an earlier clause) now walk out from that node on SQLite instead of growing with the graph. The anchor was already the traversal's seed, but SQLite's planner — with no statistics on a brain nobody hasANALYZEd — drove typed hops against the direction ofidx_edges_compositefromidx_edges_type(every edge of the type, on every hop) and scanned whole node tables to probe a variable-length traversal's few rows. On a 20,000-step execution graph a lineage walk back from one step took 67–89ms, one hop back 11ms and two hops 40ms; each now takes under 2ms at any size. An anchored part of a pattern is joined in a fixed order out from its anchor with its relationship types kept off the type index; clauses without an anchor, and PostgreSQL, plan as before. The openCypher TCK result is unchanged (2256 pass, 0 wrong).A backdated graph write — a
GraphNodeorGraphEdgewhoseValidFromis earlier than the moment it is written — no longer erases the version it replaces from as-of reads. The old version was closed at the statedValidFrom, so between that instant and the write no version was visible; writing back a node read withGetNode, which returns the version's ownValidFrom, made the replaced version invisible at every instant. The replaced version is now also kept as a history row covering the corrected stretch, retracted at the moment of the write, on every write path (UpsertNode,UpsertEdge, the batch upserts andExecuteBatch) and both backends. Ordinary writes archive exactly as before.
[2.119.1] - 2026-10-03
Fixed
--doctorreported healthy local brains as failed in two cases. The integrity check shared the 15-second network timeout, and at the ~90 MB/s measured on a cluster node any brain over about 1.3 GB timed out; it now has its own 10-minute limit and says so if it runs out. A WAL brain in a directory that cannot be written, with no-shmfile, failed with "attempt to write a readonly database"; it is now checked as immutable and the report notes it.The
hook_trustfield is gone from the--doctorreport: it was the same sentence on every run and checked nothing.
[2.119.0] - 2026-10-03
Added
MCP binary
--doctor: JSON diagnostics for the active local or remote brain, binary version and auto-recall setting. Checks SQLite read-only or discovers remote tools with authentication;--self-testverifies memory persistence and lexical recall in a temporary database.
Fixed
Auto-recall on a remote-only machine no longer requires a local database.
Session directives also run on resume and use host-independent tool names; they distinguish semantic relations from mere name co-occurrence.
Codex documentation explains plugin hook discovery and required trust review.
[2.118.0] - 2026-10-03
Added
liveview.RemoteSource(addr, token)— a live-view source over a shared brain whose address and token a host keeps in its own settings. Assembling one fromLoadRemotealone gave a view that drew the graph and nothing else: no find, ask or Cypher across the store, no node-by-node expansion, no inspector, no contract.OpenSourcenow builds its remote branch with it, so there is one implementation. (SourceForis the same for a local brain a host already holds open.)
Changed
No violet or magenta in the live view.
conceptwas a violet andpersona magenta, the ontology page's lane colours had both, and the hashed colour for any other node type could land in that band. Concept is now an ochre, person a rose, and hashed hues skip 250°–330°. Products embedding the view had been re-painting it through page globals to get there.
[2.117.1] - 2026-10-03
Fixed
Live view: nothing covers the title card any more. The ☀/☾ switch sat on the end of the title's "ontology →" link — the card kept room for the collapse button only — and now has its own gutter, and goes away when the card is folded. On narrow screens and phones the explore bar was placed 88px down while the card ends near 97px, so it hid the card's last line; it now follows the card's real bottom edge (wrapped, folded, or moved down by the past banner), and the card is bounded by where the controls chip actually starts, which is wider on a touch screen.
Live view: the + and – on folded chips are centred. They were typed glyphs pinned 5px from the top of chips whose height came from each panel's own font (29px for one, 32px for the rest), so they sat low. They are now drawn bars, centred by construction, in chips of one height; on a touch screen the chip grows to hold the 34px button. Checked in a browser from 320px to 1344px wide, with and without touch.
[2.117.0] - 2026-10-02
Changed
The live view, redesigned. Glass panels on a deep field with one accent; the explore bar is the page's main control, its results fold away once you open a node so the node is not under them; node types are chips with counts, each one a filter; the inspector leads with the record's name and a coloured type chip. Names are drawn on the scene — the hubs on a first look, then whatever is selected, hovered or lit — placed every frame from the graph's own projection and kept from overlapping. Focus no longer turns the rest of the brain into grey beads: dimmed nodes keep their hue at a fifth of its brightness and shrink. Bloom is calmer and the layout airier, so the core is no longer one white-cyan blur. Flying to a node stops at a fixed distance along the line of sight instead of inside the cluster, re-aims once the layout settles, and on a phone lands above the inspector sheet. Controls, activity and the contract start folded; the remembered fold layout is reset once (new storage key) so everyone gets the new defaults.
Themes, light and dark. Three families — Space (the default), Ember and Mono — each with a light and a dark set for the panels and the scene, and a mode that is Light, Dark or Auto (follows the system, and follows it when it changes). The title card's ☀/☾ switches light and dark in one click; the controls choose family and mode; both are remembered per browser, are URL parameters (
?theme=ember&mode=light), and can be sent by an embedder ({type:"cortexdb:theme", theme, mode}). Light modes turn bloom off — glow on a light ground only greys it — and use darker inks of the same type hues.Phones. The light/dark switch has its own thumb-sized place on the title card; the selected node's label stays a solid accent chip in light mode; and a link that opens on a node keeps that node in frame while a 2,000-node layout settles — the camera follows it until the simulation rests or the reader touches the screen — instead of showing it drift off and leaving an empty view.
Fixed
CI:
pkg/cortexdbtook 583s ofgo test's 10-minute default under-raceand coverage, and the v2.116.0 tag build crossed it. The race step now has a 25-minute budget; the 33,000-entity placeholder test, single-goroutine and 266s under the race detector against 4s without, skips under-raceand runs in its own step without it; the as-of query-plan test seeds 60 rows instead of 2,000, since the planner has no statistics and the plan does not depend on the count. 2.116.0 has no release binaries — its tag build timed out — but it is on the module proxy, so 2.117.0 ships its changes.
[2.116.0] - 2026-10-02
Added
The live view can be asked, not only looked at.
serve_graph_3ddrew the store's most-connected core — 2,000 nodes of a shared brain — and nothing past it was reachable from the page. Its new Explore bar finds a node by name anywhere in the store, asks the brain a question (recall's facts, memories and passages, with every thing the answer names lit up and one click to bring the rest into view), and runs read-only Cypher as a table whose nodes are drawn. Any node can be expanded to its neighbours, including ones the core left out; hovering a node lights its neighbourhood; relations can be filtered by type.A shared brain gets the inspector. Clicking a node on a shared brain used to say the source could not look a record up. The inspector now reads it through the brain's own tools (
get_nodes,get_chunks,fact_provenance): source document, chunk, producer, grade, when it became true, and the text it was drawn from.Every view is a link.
?focus=ID|name&hops=N,?find=,?ask=,?cypher=,?q=,?type=,?edge=,?explore=0, and a Share button that copies the current one (the system share sheet on a phone). Embedders can do the same withpostMessage(cortexdb:focus,cortexdb:find,cortexdb:ask,cortexdb:cypher).Phones. One-line title, the Explore bar across the top, the inspector as a bottom sheet, corner panels folded to chips, thumb-sized controls, 16px inputs so iOS does not zoom, glow and particles off by default on a small touch screen.
Fixed
Expanding a hub drew dots with no relations:
expand_graphspent its limit on the memories that mention the hub, joined by bookkeeping edges the scene does not draw. The view now asks for the hub's relations with parameterised Cypher and falls back toexpand_graphon a store without Cypher.Merging nodes into the scene mutated the arrays the running simulation held, so a tick between the merge and the library applying it met links still holding bare ids (
Cannot create property 'vx' on string). Structural deltas had the same latent fault.
Security
The view stays read-only by construction: it can run only the tools on
liveview.ExploreTools, a test holds every one against the catalogue'sMutates, and the callers refuse any other name before a request leaves the process. SPARQL is not on the list, becauseknowledge_graph_queryalso runs updates.
[2.115.0] - 2026-10-02
Added
Retrieval that walks the graph.
retrieval_mode: "ppr"runs Personalized PageRank (HippoRAG 2) from the entities a question names and fuses the walk with the lexical or hybrid first stage by rank. Without an embedderautotakes the walk when the question names an entity specific enough to start from, and stays lexical otherwise. Recall@5 throughSaveKnowledge/SearchKnowledge, lexical →auto, no embedder (cortexdb-bench, full sets): 2WikiMultiHopQA 0.657 → 0.807, MuSiQue 0.457 → 0.542, LoCoMo 0.493 → 0.495, LongMemEval 0.861 → 0.862; p95 under 30 ms.A change feed. Every committed write to nodes, edges, triples, memories, knowledge and ontology schemas lands in
change_login the same transaction: commit order, exactly once, nothing from a rolled-back transaction, on both backends. Read by cursor (changes_since,db.Changes) or subscribe in-process; seven days or 500,000 events kept by default.Inferences maintained automatically. Delete-and-rederive over the change feed keeps materialized RDFS / OWL inferences current as facts change, asynchronously;
WaitForInferencereads your own writes. Agreement with a full recomputation was checked over thousands of random edit sequences.OWL 2 RL keys, property chains and contradictions.
FunctionalPropertyandInverseFunctionalPropertyderivesameAs;propertyChainAxiomderives compositions.disjointWith,propertyDisjointWith, two values of a functional property anddifferentFromagainst a derivedsameAsare reported with the conflicting triples — on the tool, the Go API and the typed RPC — never resolved by guessing.SHACL-AF triple rules (
sh:TripleRulewith conditions, order and node expressions), run to a fixpoint, every result explainable (knowledge_graph_shacl_rules).RDF 1.2 and SPARQL 1.2: triple terms
<<( s p o )>>, reified triples,{| |}annotations, base-directed literals, triple-term patterns and functions. The in-scope W3C RDF 1.2 and SPARQL 1.2 suites pass on both backends. N-Triples, N-Quads, Turtle and TriG are parsed in-house.Read-only Cypher / GQL over the property graph (
graph_cypher_query): MATCH, OPTIONAL MATCH, WITH, variable-length paths up to six hops, aggregation. Zero wrong answers across 3,897 openCypher TCK scenarios; what it cannot do, it refuses. Every user value is bound, never spliced.Leiden communities.
build_community_hierarchyrefines Louvain's moves so no community is ever disconnected.Binary quantization (
IndexTypeBinary): 1-bit codes, a Hamming shortlist rescored exactly. Recall@10 0.979–0.996 on real 768-d embeddings at 8× oversample, 1/32 of the memory;binary_quantizeon PostgreSQL.cortexdb-bench: LongMemEval, LoCoMo, MuSiQue and 2WikiMultiHopQA through the public facade with one command, datasets pinned by sha256, every retrieval mode includingauto. Baselines for v2.114.1 are inpkg/eval/testdata.
Changed
SaveKnowledgebuilds the entity graph without an embedder. It used to skip extraction when no embedder was set — the plugin's default — so a knowledge base had chunks and no entities, and every graph mode silently scored what lexical did. A document title that reads as a name is now an entity every chunk of the document mentions; a fragment of a name the caller declared ("Bridge" of "Bridge 01") is not extracted as an entity of its own.The walk does not start from hubs. An entity more than 10% of passages mention — a conversation's speaker — is not a seed; walking from one cost LoCoMo ten points of recall@1.
graphmode fuses by rank, so an exact lexical match is never buried under graph neighbours.Writes are cheaper on SQLite. Write statements stay prepared across transactions, statement journals and temp b-trees live in memory, and the allocator keeps a reserve of slabs instead of mapping and unmapping one per statement. What the change feed adds to a write through the facade, p50: memory 22–25% → 11–18%, knowledge 32–34% → 26–30%, entities 44–45% → 35–38%.
MMR reranking tokenizes each candidate once and updates redundancy incrementally: 200 candidates 25.1 s → 5.4 ms, identical output.
Fixed
The change feed's
graph_nodestriggers built an ephemeral index on every write: anINlist in theirWHENclause. Equalities now; trigger version 4.Tests no longer leave databases in the source tree (10,819 files had built up); every test database lives under
t.TempDir().
[2.114.1] - 2026-09-29
Fixed
HNSW built ten layers where it should build two or three.
selectLevelpromoted a node with probability 1/2 per level whateverMwas — mL = 1/ln 2 instead of the paper's 1/ln M — and theMLfield held the same wrong constant and was never read. At M = 16 a thousand nodes reached level 8–13; the upper layers were a handful of nodes wired to one another, greedy descent entered the bottom layer far from the query, and a vector searched for by its own coordinates was occasionally not found:TestHNSWLargeScalefailed the v2.114.0 tag build withvec_0answered byvec_378at distance 8.0. Levels now follow floor(-ln U · 1/ln M), taken fromMrather than the storedMLso a saved index builds its new nodes correctly; the same test passed 400 of 400 runs afterwards, and a new test holds the level distribution to 1/M. 2.114.0 is on the module proxy but its release build failed, so it has no binaries; use this instead. Everything 2.114.0 added is here.
[2.114.0] - 2026-09-29
Added
A Chinese question finds its answer without an embedder. The word index saw
微信机器人为什么从来不主动给我发提醒as one seventeen-character token and asked the trigram index for it as a phrase — and a trigram index cannot match the two-character words most Chinese is made of anyway. On a snapshot of the shared brain, five of six Chinese sentence queries in the golden set returned an empty list while the memory answering them was there. Lexical search now also cuts a CJK question at function words (的, 了, 为什么, 那个, …), breaks the rest into character bigrams, finds candidates by substring and ranks them with BM25, merged beside the word-index results. It covers memory recall,SearchKnowledge,SearchTextOnlyand thesearch_texttool, on SQLite and PostgreSQL alike, and runs inside the authorization gate. A row is returned only when it matches more than one word of the question — two separate stretches, one stretch of four characters, or all of a one-word question — and at least 30% of its content characters. Measured on the real golden set (48 labelled queries, 7 negatives, lexical, no LLM): hit@5 0.604 → 0.875, MRR 0.503 → 0.734, Chinese natural questions 0.50 → 0.86, and every Chinese negative still returns nothing. 29–46 ms per query over 20,000 chunks.pkg/evalgainsBuiltinChinese(), an invented Chinese ops notebook with sentence queries and negatives, andDataset.Negativesfor queries that must come back empty. CI holds it at recall@5 ≥ 0.90 and MRR ≥ 0.85.
Fixed
A keyword search ranked the best match last.
SearchTextOnlywith keywords or alternate queries — and the FTS fallback behind hybrid search — mapped FTS5'sbm25(), which is negative and lower for a better match, to a score with1/(1+(-bm25)): highest for the weakest match. SQL returned rows best first and the merge re-sorted them worst first, on both backends. This is the path an agent reaches by expanding keywords, which is what every tool description tells it to do when there is no embedder. The memory path had the same inversion and was fixed earlier; this one was missed.
[2.113.0] - 2026-09-28
Added
History says why a row ended.
graph_node_historyandgraph_edge_historygainreason,superseded_byandproducer, migrated in place on SQLite and PostgreSQL (rows archived before this read with an empty reason — the store does not know, and does not invent one). Every archive path records one: an upsert's replaced version issuperseded(by its own id), a delete isretracted, and the facade's own paths saydocument_deleted,orphaned,prunedormerged. Callers state their own withgraph.WithInvalidation. Read back throughNodeHistory/EdgeHistory(Go, both onGraphStoreand the facade),HistoryReasons, and on every retracted or changed row ofgraph_diffasinvalidation. Exercising every invalidation path the facade and graphflow expose: 15 of 15 history rows carry a reason, against 0 of 13 before.Entity resolution links first, merges later.
graphflow.ResolveEntitiesscores each candidate pair — name similarity (normalized key, or Jaro-Winkler capped at 0.92), shared neighbours, type agreement, and whether the model grouped it — and merges only at 0.95 or above. From 0.85 it writes apossiblySameedge with the evidence, gradedheldwith a_why, socontract_needs_attentionlists it for a person; grading that edgeverifiedmerges the pair on the next pass,refusedkeeps it apart. On a 24-pair labeled fixture (12 aliases, 12 near-miss homonyms and siblings): false merges 5/12 → 0/12 without a model and 6/12 → 0/12 with one; aliases found (merged or linked) 8/12 → 10/12 and 10/12 → 11/12, of which 6 are merged outright.ResolveOptions.Mode = ResolveMergeAll(CLI--merge-all) restores the unscored behaviour.verify_claims(DB.VerifyClaims): checks (subject, relation, object) triples against the graph and answers supported / contradicted / absent per claim, with the evidence edges and their provenance and contract fields. Contradicted comes from the store's own notions — ontology cardinality or a caller-declared single-valued relation, an endedvalid_to,_contradicts. Deterministic. On a seeded 337-claim set: supported and contradicted precision 1.00; contradicted recall 0.80 with the ontology alone (the misses are a relation the ontology does not describe) and 1.00 once declared.graph_health(DB.GraphHealth): growth per producer, degree tail, supersessions per day and temporal invariant violations, each reporting whether it fired.search_paths— path retrieval for multi-hop questions. Bounded breadth-first search between the entities a question names (or every path out of one), scoreddecay^(hops-1)× relation weights, each path returned as edges that cite their source chunks, plus the deduplicated evidence set in path order. MCP tool,DB.SearchPaths,graph.GraphStore.SearchPaths, andReturnPathson GraphRAG queries (return_pathsonsearch_graphrag_lexical). Measured on a 10-question fixture (2–3 hops, look-alike distractors, lexical mode, k=5): full-evidence hit rate 0.50 forsearch_textandsearch_graphrag_lexical, 0.80 forsearch_paths; distractor share 0.43 / 0.36 / 0.20. On the six questions naming both ends of the chain, 1.00 against 0.67 at best. On the four naming one end, paths tie lexical search at 0.50 — with one seed the walk does not know which way the question goes. A homonym merged into one node (a chef sharing a founder's name) yields a one-hop path that outranks the true chain; only entity resolution fixes that.Per-relation-type weight and depth.
graph.RelationPolicies({type: {weight, max_depth}},"*"for the rest) onSearchPaths,expand_graph(relation_policies, which also returns per-node scores and makeslimitkeep the best),Neighbors/ScoredNeighborsandHybridSearch(GraphFilter.Relations). Unset, all of them behave as before. Around five entities whose real facts sit among co-occurrence edges, down-weightingco_occurs_withto 0.2 and capping it at one hop took precision@5 from 0.40 to 0.80 and nDCG@10 from 0.88 to 1.00 — a weighting chosen knowing the fixture, so it shows the mechanism, not a forecast.global_searchandbuild_community_hierarchytools.graphflow.GlobalSearchwas reachable only from Go and the--global-searchCLI mode; it is now an MCP tool oncortexdb-mcp-stdio(local mode) andgraphflow.NewMCPServer. The caller chooses it; nothing routes a query to it.Hierarchical communities.
graph.GraphStore.HierarchicalCommunities/graph.LouvainHierarchyrun Louvain with modularity gain and keep every level;graphflow.BuildCommunityHierarchywrites reports bottom-up (parents from children's reports) with or without a model, andGlobalSearchtakes aLevel. On a 2000-entity planted graph: 2 levels, 67 then 17 communities.PageRank cache.
rank_graph_nodesserves scores from a side table with their computation time, recomputing when the graph changed (refresh,allow_stale,max_age_seconds; Go:GraphPageRankOptions.Cache,RefreshPageRankCache,InvalidatePageRankCache,StartPageRankRefresher). 2000 nodes / 8770 edges: 16.4ms p50 uncached, 0.11ms p50 cached.
Changed
Merges, fact supersession and graph-edit deletes leave history.
MergeEntitiesarchives the alias's edges before repointing them, and the alias itself, with reasonmergedand the survivor assuperseded_by; it no longer drops unrelated self-loops elsewhere in the graph.graphflow.SupersedeFactrewrites the closed fact through the graph store (so its open version reaches history) instead of an in-place UPDATE, and runs on PostgreSQL;SupersedeFactWithnames what replaced it, andTemporalFact.SupersededByreads it back.ApplyGraphEditsrelation deletes go throughRetractEdgeAtrather than a bare DELETE, which left no trace.
Fixed
Ingesting after an as-of query failed on SQLite with "malformed JSON": the as-of expression index was an unguarded
json_extract, which SQLite evaluates on every insert, and chunk edges carry empty properties. The index is now guarded (and renamed; the old one is dropped on first use).HybridSearchbreaks score ties by node id; equal scores used to come out in map order, differently from run to run.
[2.112.0] - 2026-09-25
Added
The property graph answers SPARQL. The shared brain had 3,812 nodes and 5,809 edges in its property graph and zero rows in
kg_triples, so SPARQL, RDFS inference and SHACL — real features — had nothing to act on:SELECT ?p (COUNT(*) AS ?n) WHERE { ?s ?p ?o } GROUP BY ?preturned nothing.FindTriples, the one read path all three share, now also returns the triples the property graph implies, read-only, in graph<urn:cortexdb:graph:property>:cxn:nodes,cxt:types,cxr:relations,cxp:properties, andrdfs:labelfromnameortitle. The pattern is pushed down into SQL on both backends, nothing is copied, and deleting or inserting a projected triple is refused. On a snapshot of the brain, "who depends on CortexDB" isSELECT ?who WHERE { ?x cxr:depends_on ?y . ?y rdfs:label "CortexDB" . ?x rdfs:label ?who }and returns eleven distinct projects in 14 ms (some twice, because the brain stores them under two nodes — which is whatowl:sameAsis for).An OWL-RL subset:
owl:inverseOf,owl:SymmetricProperty,owl:TransitiveProperty,owl:equivalentClass,owl:equivalentPropertyandowl:sameAs, each inferred triple explainable back to explicit ones. Aimed at what extraction gets wrong. On the brain snapshot,cxt:host owl:equivalentClass cxt:Hostunifies two spellings of one type, andowl:sameAsbetweennode-eandsds-e— one machine stored twice, each copy holding half its facts — gives either name all nine relations. A sameAs class abovemax_same_as_class_size(default 32) is reported inoversized_same_as_classesrather than materialized, on the tool, the Go API and the typed RPC alike.SHACL constraints:
sh:class(followingrdfs:subClassOf*),sh:minExclusive/sh:maxExclusive,sh:hasValue,sh:minLength/sh:maxLength,sh:languageIn,sh:uniqueLang,sh:node,sh:not,sh:and,sh:or,sh:xone,sh:closedwithsh:ignoredProperties,sh:targetSubjectsOf/sh:targetObjectsOf,sh:inversePath,sh:flags, and constraints on node shapes, not only property shapes. Results name the constraint component. Recursive shapes and malformed lists are errors, not loops or silent passes.SPARQL:
FROM/FROM NAMED, GRAPH blocks in CONSTRUCT templates, and the SPARQL 1.1 function library (term tests, strings counted in characters, numerics, dates, hashes). A per-row type error drops the row in FILTER and leaves the variable unbound in BIND, as the spec says, instead of failing the query.JSON-LD 1.1 import and export (
jsonld, alsojson-ld), throughpiprate/json-goldbehind a loader that performs no I/O: schema.org's context is answered from memory, any other remote@contextis refused with an error naming it. json-gold panics on some trivial inputs; those are caught and returned as errors, and kept as fuzz seeds.
Changed
Inference is semi-naive. Each round joins only what the previous round derived, instead of every pair of triples. 2,044 explicit triples: 11.87 s → 60 ms. 20,035: the old engine had not finished after 28 CPU-minutes; the new one takes 1.84 s. Output is identical to the old engine on the golden fixtures, and rule credit no longer depends on map order.
SHACL treats a plain literal as
xsd:string(RDF 1.1), checks the lexical form of common XSD datatypes, and reports malformed shape parameters as errors instead of skipping them.
Fixed
ORDER BYon an aggregate alias sorted wrongly.GROUP BY ?t ORDER BY DESC(?n)over the brain returned counts in the order 403, 107, 2015, 2, 1: ORDER BY could not see SELECT aliases in grouped queries, and compared values as text.A literal joined into a subject position failed the whole query.
?x ?rel ?y . ?y rdfs:label ?obinds?yto a name whenever?relis a label, and the query died withrdf subject must be iri or blank node. That row now simply has no solutions.Deletes counted requests, not removals. SPARQL
DELETE WHERE/DELETE … INSERTandDeleteKnowledgeGraphadded one per triple asked for, including triples that were never stored.Inferred triples carried incomplete support ids, so traces stopped at the first inferred step.
The plugin manifests were at 2.109.0 while
version.gosaid 2.111.1, so CI failed for 2.110.0 and 2.111.x and no release binaries were built; the plugin's MCP launcher kept downloading the 2.109.0 server. All four now read 2.112.0.
[2.111.1] - 2026-09-15
Fixed
The checkpointer added in 2.111.0 raced; use this instead.
Initis called more than once on the same store —hindsight.Newopens the database, which inits it, and then inits the vector store again itself — and the second call assigned fresh channels over the ones the running checkpoint goroutine was selecting on. A write under the store lock against a read holding no lock at all, plus a goroutine left running on channels nobody could reach to stop it. Seventeen tests inpkg/hindsight, none of them about checkpointing, failed under-racewith that report. The goroutine now closes over its channels as locals so there is no shared read to race, a second start returns early, andstopWALCheckpointerreleases the lock before waiting. 2.111.0 is on the module proxy and should be skipped.
[2.111.0] - 2026-09-15
Fixed
The write-ahead log no longer grows until the disk is full. An Athanor instance ingesting 248 documents produced an 11 GB
brain.db-walagainst a 636 MB database, filled the 57 GB disk it shared with other services, and took an unrelated application's PostgreSQL down with it — every connection answeringFATAL: could not write init file: No space left on devicefor ninety seconds. Nothing in this package had ever calledwal_checkpoint. SQLite's own automatic checkpoint could not cover it: it can only copy out frames older than the oldest reader still holding a snapshot, and the store keeps a pool of 25 connections with 10 idle for two hours, so during any sustained ingest there is always a reader in flight and every automatic checkpoint gives up silently. The store now checkpoints on a clock of its own, every 30 seconds, withTRUNCATErather thanPASSIVE— the problem is the file's size, andPASSIVEleaves the log as long as it ever got. A checkpoint that cannot get its locks reports busy rather than failing, and waits for the next tick. In-memory stores start no goroutine.
[2.109.0] - 2026-09-14
Added
UpdateDocumentis on theStoreinterface. Both backends had it and the interface did not, so a caller holding the store as an interface — whichcortexdb.DBdoes — could create a document and delete one but never write over one. Replacing a record meant delete then create, with a window where it is neither. Same gapparity_aggregate_test.gowas written about; the test is next to it and runs on both backends.
[2.108.0] - 2026-09-14
Fixed
Deleting a document's graph is one transaction. The removal is four write phases — the relation edges, an UPDATE per entity that survives with one fewer source, the nodes, and the vector index sync — and they ran as four independent statements. A failure between any two left a graph half removed: edges gone with their endpoints still standing, or some entities detached and the rest still naming a document that is no longer there. Nothing downstream can tell that state from a real one, and running the delete again does not restore what the first pass took. The reads now happen on the same transaction as the writes, so the plan and what it acts on cannot drift; the index sync stays outside and after the commit, because syncing ids whose rows a rollback restored would be worse than not syncing at all.
[2.107.0] - 2026-09-14
The provenance a machine-written fact could not carry.
Added
RDFTriple.Provenance, merged onto the edge a triple becomes. An import wrote triples into the brain carrying nothing about their origin, so a fact read out of a live database could not say which plan was signed to read the table, which run read it, or who authorized it — all of which were in the ledger and on none of the facts. The map lands where the knowledge contract is already read from: edge properties. It cannot overwrite the keys that say what the triple is, because a fact able to relabel itself as inferred through the field meant to hold it to account is worse than an anonymous one. It is not part of the triple's identity, so re-importing a row refreshes its provenance rather than forking the fact.importflow.WithProvenance, stamped by the one sink every triple reaches — column mapping, relation mapping and AI extraction all pass through it, and a path that forgot would produce exactly the anonymous facts this ends. The importer invents nothing: what a row out of somebody's production database is worth is the calling product's decision, not this code's.
Changed
uncited_factscounts a named source as an accounting. A live-database row and a rule-derived edge have no chunk anybody can quote and are not anonymous; reporting them would bury the facts that genuinely came from nowhere under one line per imported row.The two copies of the RDF edge-property construction are one function. They were identical, and a provenance legible through one path and not the other would have been a hole nobody could see.
[2.106.0] - 2026-09-14
Two defects on the provenance surface, both found by calling it over a port rather than from a test, and the flag that makes an upgrade checkable.
Added
-versionon all three binaries.cortexdb-grpc -versionprints the version of the file on disk and exits, opening no database, reading no.envand touching no network — it answers on a machine where nothing is configured yet, which is the machine you are on right after copying a binary into place.-healthalready reported the version of a server that is running; there was no way to ask the artifact, so telling two builds apart meant comparing checksums against a build you still had lying around. Both spellings work, and only as the first argument: in-db --versionthe second word is a database path, and a typo that printed a version and exited 0 would look, under systemd, like a unit that came up.cortexdb-mcp-stdioandcortexdb-connector-mcpanswer the same way, since an MCP client cannot be asked what it launched.
Fixed
fact_provenance,uncited_factsandvector_dimension_repairwere listed on every door and reachable through none of them. All three were in the catalogue/v1/toolspublishes, classified for authorization, and registered with the MCP server — and absent fromGraphRAGToolbox.Call, which the REST port and the gRPCToolCallboth dispatch through. A caller could read the catalogue, build a valid request from the published schema, and get backunknown tool: fact_provenance. Asking a record where it came from is what the store is for, and over both ports it could not be asked. Registering a handler with the MCP server consumes the definition, so it looked like registration and nothing downstream noticed the missing case. A test now walks the catalogue and calls every name with malformed input: a known name fails at decode, an unknown one says so, and no tool executes.The sweep for uncited facts failed on any brain with a document in it.
ingest_documentwrites onehas_chunkedge per chunk, and an edge written with no properties gets the empty string in that column. The empty string is not JSON, sojson_extracton it is an error rather than a null, and one such row failed the whole query — withmalformed JSON, naming neither the column nor the row. Every brain in use has ingested a document, so the tool that reports which facts cannot say where they came from was itself the one tool that could not be run. The dialect already carriedJSONTextGuarded, written for exactly this and used by five other queries. Structural edges (has_chunk,mentions) are excluded while the query is open: a document owning its chunks is not a claim anybody can cite a source for, and one per chunk of every document would bury the facts that are actually uncited.
[2.105.0] - 2026-09-14
Four capabilities built from parts the engine already had and nothing could reach, and the three defects that turned up in them once real data was pointed at them.
Added
graph_schemaandgraph_property_valuesreport the graph's observed schema: which node and edge types exist, which node-type pairs each edge type really connects, which property keys each type carries, and what values one key actually takes.ontology_getanswers the declared schema, which is opt-in and absent on exactly the graphs built by extraction whose shape nobody knows. The answer carries a renderedtextform, because it is read in a prompt rather than parsed. Without the value distribution, a filter written from a key's name —color == "black"against rows that all sayBLK— returns nothing and reads as a fact.rank_graph_nodes,predict_graph_edges,graph_statisticsopen the rest ofpkg/graph/graph_algorithms.go, which had no callers at all.predict_graph_edgesreportstied_at_top: several unrelated nodes tied at exactly 1.0 means the graph's vectors cannot tell them apart, which is what short lexical hashes do to short entity names, and a maximal score is the worst possible way to say "no signal".disambiguate_mentionsresolves an ambiguous name against the other names given with it — shortest paths between their candidates, each rendered as a sentence with its edge ids, ranked by that support. The deterministic four steps are the library's and the choice is the caller's, so it works with no model at all and can always explain itself. A mention nothing connects to is unresolved rather than the nearest string match, and a bound that stopped the search is reported apart from an absence.ChunkWindow/chunk_windowwidens a hit to its neighbours in the same document. Off by default; neighbours are context and never hits — not scored, not reranked, never displacing a match, tagged+contextin the assembled text. Applied on every retrieval path, not only the one it was written against: an option honoured on one path and ignored on another is worse than no option.
Fixed
PredictEdgeslet its two signals cancel instead of agreeing. It averaged vector similarity with a structural score that is strictly below 1, so purely structural evidence sat permanently under the 0.5 a prediction must clear — a pair sharing every neighbour and nothing else could never be returned — and averaging lowered a strong similarity, so sharing neighbours demoted a pair. The branch only ran when there was structure, which made structure strictly harmful. Replaced with a noisy-OR: the structural term closes part of the gap the similarity leaves to certainty, so it can only raise a score, and structure alone still carries a pair over the line.EdgeShapesandEdgeEndpointPairscould not be filtered to the untyped bucket. Their SELECTs present a NULL type as"", which is the only name a caller is given for it, and their WHERE compared the raw column, where NULL equals nothing — so asking for rows the previous call had just counted returned an empty answer.NodePropertyKeysalready filtered on the COALESCE'd expression; the other two now do too.PredictEdgessilently dropped edges it could not scan, leaving holes in the topology every prediction is computed from.
[2.104.0] - 2026-09-14
Four capabilities existed in the engine and could not be used, and looking at why turned up three defects in them.
Fixed
RangeSearchreturned opposite things on the two backends. SQLite put the distance inScoreand sorted ascending; PostgreSQL delegated toSearch, soScorewas a similarity sorted descending — and it silently capped at a thousand rows and accepted a non-positive radius. The same call, a different answer, no error.Scoreis the similarity on both now, descending;TopK0 means every match;radius <= 0is refused.Its distance conversion guessed the metric from the sign of a score. Cosine runs −1..1 and embedding vectors routinely have negative components, so cosine −0.5 came out as distance 0.5 instead of 1.5: the whole negative half of the range scored as nearer than it was, and a radius of 0.5 quietly admitted vectors anti-correlated with the query. Orthogonal — exactly 0 — came out as distance 0, the nearest possible, and passed every radius. The conversion now subtracts from the metric's own fixed point,
similarityFn(query, query), which needs nothing to know which metric it has.SearchWithFacetshad never worked with a facet on SQLite. Its query LEFT JOINscollectionsontoembeddings, both tables have ametadatacolumn, and the facet conditions named it unqualified — so every facet filter and everyopts.Filterfailed with "ambiguous column name: metadata" before a row was read. The method worked only when asked to filter nothing.Aggregation names went into SQL unchecked.
order_bywas pasted raw afterORDER BY, so an arbitrary expression ran; aFilterskey could close its ownjson_extractand reopen the next one, turning the predicate into a tautology — a count scoped to a value nothing had returned the whole table. Names are now checked fail-closed before they reach the statement, by two rules: one for names that stay inside a JSON path (dots and hyphens stay legal, becauseuser.idandcontent-typeare real keys) and a stricter one for names that become bare SQL.A Postgres test leaked a database per run. A test function's defers all run before any
t.Cleanup, sodefer admin.Close()shut the connection the cleanup needed to drop its database, and the drop discarded its error.
Added
BatchRangeSearch,SearchWithFacets,AggregateandVectorAggregateare implemented on PostgreSQL and promoted into theStoreinterface. They were*SQLiteStore-only and outside the interface, so nothing holding aStore—pkg/cortexdb.DBholds one — could reach them, and anything that did would break under PostgreSQL.VectorAggregatereduces the vectors themselves: centroid, geometric median (Weiszfeld with the Vardi–Zhang step for a coincident iterate), and the medoid — the stored record closest to all the others in its group, returned with its text. It answers "which of these near-duplicates is the canonical one" and "what is this cluster about" arithmetically, with no model in the loop.Facade and tools.
RangeSearchVector/RangeSearchText,Aggregate,SearchWithFacets,VectorAggregate; toolssearch_vector_range,aggregate_metadata,representative_records. The range tool reportstruncated, because a trimmed range answer is otherwise indistinguishable from a complete one and decays into an unlabelled top-K.
Changed
SKILL.mdhad two unresolved merge-conflict markers committed into it, swallowing the decision-ledger and declared-inference sections into each other. Resolved as the union; both features shipped.gofmthad never been run on thirty files, sogofmt -l .could not come back clean and was useless as a gate.
[2.103.0] - 2026-09-12
Fixed
--export-memorywritessourceandexpires_atinto the frontmatter. It wrote scope, namespace, importance and created_at and droppedsource, so from the export a verbatim transcript turn and a distilled experience were indistinguishable — both carry the sameharness-<timestamp>-<n>id. A deletion plan drawn from such an export would have taken 387 experience memories along with the noise; it was caught only because the plan was read before it was applied. Absent fields stay absent.
[2.102.0] - 2026-09-12
A bulk delete of 3390 memories, done through memory_delete, left 1097
memory:<id> graph nodes pointing at rows that no longer existed. Nothing could
remove them: delete_entities only knows entity: nodes, and the junk prune
only looks at entities.
Fixed
DeleteMemoryretracts the memory's graph node. A memory saved with entities has amemory:<id>node and mention edges; deleting the row now retracts them in the same call (archived, so still readable as of before the delete). The response says whether a node went (graph_node_retracted). A memory that never had a node costs one no-op statement.
Added
PruneDanglingMemoryNodes, wired into--graph-cleanupas its own stage and selectable alone with--dangling-only, retractsmemory:nodes whose row is gone. Separate from the junk-entity prune so a brain can be repaired after a bulk delete without also deciding what to do about its entities.--dry-runnames every node it would take.
[2.101.0] - 2026-09-12
A shared brain grew past what its own listing tool could return. memory_list_all
answered with truncated: true and no way to continue, so --export-memory
wrote zero files and the brain had no backup path at all. Both bulk listings now
page.
Added
memory_list_allandgraph_list_alltake acursorand returnnext_cursoralongsidetruncated. The cursor is opaque and keyset-based, never an offset: a shared brain is written while it is read, and an offset silently skips records inserted behind it. It is also tagged with the listing that produced it, so feeding one listing's cursor to the other fails loudly instead of returning a plausible empty page.graph_list_allgainedorder. The default still returns the most-connected core, which is what makes a large graph renderable, and setstruncatedalone — degree ranking has no stable page boundary to resume from.order: "id"walks the whole graph in a stable, resumable order. In that mode a page's edges may reference nodes on a later page; the subgraph is complete once the walk finishes. Any otherordervalue is now rejected rather than silently falling back to the ranked mode.
Changed
defaultMemoryListLimitdrops from 5000 to 500. The old default was a promise the transport could not keep: 5000 records exceed the 4 MiB gRPC message limit, which gives out near 1100. The cursor, not a large limit, is how a caller gets everything.--export-memory(and--memory-html,--memory-usage,--sync-memory) walk the cursor through one shared remote-fetch path. Against a server too old to page they now fail loudly naming the cause, rather than writing a partial backup that looks complete.
Fixed
The keyset comparison was wrong on PostgreSQL and silently dropped rows.
created_atis SQLite TEXT at one-second granularity but a microsecondTIMESTAMPon PostgreSQL, and the cutoff was formatted with a layout carrying no fractional seconds. A row inside the cutoff second was then neither<nor=it, so it was skipped on every page, forever, with no error. The binding is now per-dialect, and a parity test seeds sub-second instants to hold it.
[2.100.0] - 2026-09-09
An example that put vectors, the knowledge graph and the ontology over one estate found five defects at the seams between them. All five close here.
Fixed
Naming an object no longer erases what is known about it. An entity upsert replaced the property map, so a caller that only named an object — a document declaring the entities it mentions, which carries identity and nothing else — wiped every domain property an earlier, fuller write had established. Silently, with the ontology's required-property check still passing because the primary key was the one thing a mention carried. An upsert now updates: a property the request names wins, one it leaves out survives, and there is no way to remove a property by omitting it (use
delete_entities). Three write paths had the bug — the toolbox upsert, declared entities inSaveKnowledge, and the extractor's mentions on both knowledge paths — and they share one merger. The prior read is outside the write transaction, so the rule holds against committed state, not against a concurrent writer; a property an ontology revision no longer declares survives on rows written before the change.A strict ontology no longer blocks embedder-backed
SaveKnowledge. The built-in extractor types everything it findsentity; that literal was validated as an ordinary object type against a schema with no reason to declare it, so a governed ontology and embedder-backed knowledge were mutually exclusive and the error blamed the user's schema for a name the library had chosen. The bookkeeping type is exempt unless a schema declares it and means something by it. Theupsert_entitiesandSaveKnowledgeentity doors still validate a caller-typedentity; an extractor supplied by the caller can write a bare name-keyed node under strict enforcement, and the guide now says so.Search hits name the typed objects their chunks mention. The chunk-to-entity lookup matched
node_type = 'entity'— what an untyped entity is stored as — so under an ontology, where an entity's node type is its object type, every hit came back with an empty entity list for exactly the users who had adopted the schema. It follows the mention edge now. And it runs in auto mode: naming what a chunk mentions had been gated behind the decision to run graph expansion, which a lowercase question never triggers. An explicit lexical mode ordisable_graphstill leaves the graph untouched.vectorizedis embedded on write. The flag was stored, validated and inherited, and nothing ever embedded the property, so thenearest_neighborsobject-set predicate compared a real embedding of the query against lexical hash vectors and matched nothing. With an embedder, an object whose type declares a vectorized property gets its node vector from the embedding of that text (name first), in one batch per write. Without one the node keeps its lexical vector and the write goes through; a dimension that does not match the store's is refused.GetNodesBatchchunks its ids (999 per query). It bound one placeholder per id and never chunked, so an ingest of more than 32766 entities failed with "too many SQL variables" on every path that read prior state.
Added
Hybrid results keep what each retriever thought. Reciprocal rank fusion at k=60 scores the top three results 1/61, 1/62 and 1/63 whatever the corpus — right to order by, wrong to read, and until now the only thing kept. Every chunk carries
VectorScore/VectorRankandLexicalScore/LexicalRank(rank 0 means that retriever never returned it); aKnowledgeSearchHitcarries its 1-basedRankand the best of each score. The fused order is unchanged.Example
18_vector_graph_ontology— the three layers over one replicated storage estate, and the seams between them in both directions: a passage's chunks walk to the objects they mention, an exact typed set walks back to the prose about it, and retrieved passages become astaticobject set intersected with a graph traversal. Runs lexically with no model; with an embedder,nearest_neighborsanswers over a vectorized property. Its README records the five defects above.
[2.99.0] - 2026-09-06
Added
Point-in-time reads.
graph_nodesandgraph_edgescarryvalid_from,valid_to,recorded_atandretracted_at, added byALTER TABLEunder the existing duplicate-column guard with defaults that make every existing row current — a brain that predates this opens and reads exactly as it did. Default reads are unchanged; as-of is opt-in.Delete becomes retraction.
DeleteNode/DeleteEdgeand the delete tools keep their names and their observable current behaviour, but the row stays withretracted_atset, so an as-of read from before that instant still sees it.vacuum_graphis the single operation that removes anything permanently: it refuses without a cutoff and offers a dry run.graph_snapshot,graph_diff(reads) andvacuum_graph(a write), plusGraphDiff(t1, t2)reporting what was added, retracted and changed between two instants, and an Allen interval helper over(valid_from, valid_to)— how two claims about one subject state that the runbook's validity ended where the incident's began.
[2.98.0] - 2026-09-05
The brain could say how it knew a fact. It could not say why anything was done, and it could derive only the one rule shape we shipped. Both close here.
Added
Decisions as first-class records (
RecordDecision,DecisionChain,Precedents,DecisionsBy; toolsdecision_record/decision_chain/decision_precedents; gRPCDecisionService). A decision is a graph record —decision:<id>withbased_on,aboutandsupersedesedges — so a chain is a graph walk and every existing tool sees it. It carries the contract: a named actor signed it, so it isverified,_producer=human,_by=<actor>;contract_tallycounts it. A premise that is an edge is anchored on that edge's subject node and named exactly inpremise_edge_id, becausegraph_edges.to_node_idis a foreign key both backends enforce; the chain reports the edge's grade. Unknown premise, empty actor, or a supersedes target that does not exist: refused before the first write.Rules a user declares (
pkg/graph/rules*.go;RuleDefinitionwith textIF p(?x,?y) AND q(?y,?z) THEN r(?x,?z)or structured when/then; toolsrules_save/rules_list/rules_delete/rules_apply/rules_explain). Semi-naive forward chaining to a fixpoint; derived edges carryrule_id,rule_text,support_edge_idsand a confidence; re-running derives nothing new;inference_explainshows the chain. Hard caps (16 iterations, 50 000 derived edges, overridable) that write nothing when hit and say so. Literal terms match by node id, thenType:Name, then bare name; a term matching nothing is reported inunresolved_termsrather than matching nothing quietly. Rules persist in akg_rulestable, not as graph nodes: a rule is configuration for the engine, not a fact about the world.
Changed
apply_inferenceruns through the rule engine — its two-hop request is one rule, and the old join is gone, so there is one derivation path. Tool name, arguments and provenance unchanged; the response gainedunchanged_edge_ids.
[2.97.0] - 2026-09-05
Changed
REST tool calls are classified per tool, from
ToolDefinition.Mutates, the same way the gRPC side already was. Until nowPOST /v1/tools/{name}was a write for every name, so a read-only key could not callcontract_tally— a read-only auditor unable to read the audit, found the first time the doors were run with one. An unknown tool name is refused by the policy, naming it; a confined key is refused every tool, because a tool's arguments are opaque JSON no scope can be checked against (gRPC's rule too).knowledge_graph_queryis narrowed to a read when the executor's own parser says the query does not mutate, so a read-only key keepsSELECTon both ports and the two ports cannot disagree.
[2.96.0] - 2026-09-05
Added
liveview.SourceFor(db, describe)— a live view over a brain the caller already holds open.OpenSourceopens its own database from the environment; a process writing through a*cortexdb.DBshould not open the same file a second time to draw it. The Source does not close what it did not open.
[2.95.0] - 2026-09-05
Added
rpcserver.AuthInterceptor— the policyNewWithPolicyinstalls, as a value a process can put in agrpc.Serverof its own.Registerattaches the services; this attaches the authorization; together they areNewWithPolicywithout the server it constructs. For the process that mountscortexdb.v1beside other services on one listener, where the alternatives were to rewrite the policy or to drop it.
[2.94.0] - 2026-09-05
Added
liveview.Newand(*Server).Handler— the live graph view as routes a process mounts on a mux it already owns, instead of a loopback listener of its own.Startis nowNewplus a listener and behaves exactly as before. The pages fetch by relative path, so the handler works under a prefix (/graph/, stripped, trailing slash). Needed by a process that serves the brain as one page of a larger service and should not run a second HTTP server on a second port for one route.
[2.93.0] - 2026-09-05
The knowledge contract: what a record must carry to be trusted and explained, and the three ways to read it back.
Added
The contract itself (
pkg/cortexdb/contract.go, and the design indocs/superpowers/specs/2026-09-05-knowledge-contract-design.md). Every producer that writes into this store — alchemy, argus, DataIntelligence, the memory tools — answers the same two questions about each record: how do I know this, and how sure am I. Each answered in its own words and the vocabularies shared none. This is the one vocabulary, as reserved_-prefixed metadata keys on the maps the store already carries. No RPC changed to add it._gradeis closed and five-valued —verified,self_consistent,asserted,held,refused— and answers only "by what kind of thing is this established". The producer's own finer word rides in_stateuntouched, which is what keeps_gradefrom flattening distinctions each producer documents at length._whyis required whenever a record is held or refused: a refusal without a reason is noise the reader will delete.ValidateContractis a library call a producer makes before it writes; the server stores what it is given.ContractTallyandNeedsAttention(pkg/cortexdb/contract_query.go) read it back. The tally's two uncomfortable fields are why the type exists:Untaggedis every record nobody graded, which on a real shelf is the largest number and whose omission turns a five-bar chart over 3% of the data into one that looks like all of it;Unknownis every grade the contract does not define, which means a producer is writing something wrong and must not be folded in with producers writing nothing.PropertyCountsandRecordsWithProperties(pkg/graph), the domain-neutral half: group and list nodes and edges by any top-level property.GraphFilter.Propertiescould already narrow a search to one value; nothing could count them and nothing saw edges at all, so callers wanting the breakdown wrote SQL over both tables themselves.contract_tallyandcontract_needs_attentionover MCP, both reads. A tool that could write a grade would let something mark a record verified with nothing having checked it.ContractServiceover gRPC (proto/cortexdb/v1/contract.proto), for the consumers that are not in this process: a Rust one and a separate Go one that must not open the same database file.The live view says how much of what it shows stands on what. A panel with the tally and the records waiting for a person, on its own fifteen-second timer through its own endpoint, and not fetched at all while folded — the structure poll runs every two seconds and this is two aggregate scans.
?contract=0turns it off.
Fixed
scripts/gen-proto.shvendors the protos into the Node client too. They were copied by hand, which is how that copy came to be missing an RPCproto/had had for a while: a vendored copy nothing syncs is a stale copy.
[2.92.1] - 2026-09-03
Changed
pkg/liveview?spin=Nis a pace, not a switch:spin=1is the Orbit button's own pace (a turn every forty-six seconds, which on a dense graph reads as standing still),spin=4a turn every twelve. Any positive value starts the orbit.
[2.92.0] - 2026-09-03
The live view could be embedded but not configured: an application putting the
graph inside a dial got the graph and the five control panels drawn over it,
with no way to ask for the graph alone. Every switch on those panels is now
also a query parameter — ?panels=0, ?spin=1, ?glow=0, ?flow=0,
?bg=RRGGBB, ?fit=0 — read once on load and applied where the switch lives.
They set starting positions, not locks: a drag still stops the orbit and a
button still flips its switch.
Added
pkg/liveviewURL options.panels=0hides the control panels and the boot splash;spin=1presses Orbit once the layout has settled;glow=0andflow=0start with bloom and link particles off;bg=RRGGBBsets the scene and page background;fit=0stops the camera re-framing after the first fit, for an embedder whose frame must not jump.
[2.91.0] - 2026-09-03
CortexDB could be run as a service, but not operated as one, and its own headline — many agents on one shared brain — was enforced by a single token that gave every agent full read/write over every other agent's memory. This release is that gap.
Added
A REST API (
pkg/httpapi, served bycortexdb-grpc -http-addr). Nineteen endpoints over the same facade calls the gRPC handlers make, standard library only.POST /v1/tools/{name}reaches the whole toolbox, so every current and future tool is curlable. Existing gRPC users are unaffected; the point is that evaluating CortexDB no longer starts with installing Go or generating a client.Scoped API keys (
pkg/authz). A key carries a clearance (read-only / read-write) and a scope confininguser_id,scope,namespace,collection. Both ports read the same key file and a denial reads identically on either. The singleCORTEXDB_GRPC_TOKENdeployment keeps working unchanged, as one implicit full-access key.Two design choices are load-bearing. Method classification is an explicit table with a test that walks the registered gRPC service descriptors and fails on any method missing from it — a
HasPrefix(method, "Delete")heuristic would silently misclassify the next RPC somebody adds, and a write passing as a read is the failure this exists to prevent. It earned itself immediately: it caughtAdminService/Backup, added in the same release by different work. Row confinement rejects rather than narrows — a caller that asked for something it may not have is told, not handed a quietly different answer.This is not transport security. The gRPC transport is plaintext by design; scoped keys reduce the blast radius between cooperating agents on a trusted network, and change nothing for someone reading the wire.
Observability (
pkg/observability,rpcserver.MetricsInterceptor). Prometheus text format, expvar, and a tracing interface this repo owns with a no-op default — all standard library, noclient_golangand no OTel ingo.mod, for the same reason no LLM SDK is inpkg/. Labels are bounded to gRPC method and status code, asserted by a test that walks the exposition and fails on any other label name. Metrics wrap authorization rather than sitting inside it, so denials are counted: on a server whose new feature is scoped keys, an emptyerrors_totalunder a brute-forced token was the wrong answer.AdminService.Backupanddb.Backup, confined to a configured backup directory (absolute paths,..and re-entering traversals refused).SQLiteStore.Backuphad existed since long before and been reachable from nowhere. Backends declare it through an optionalcore.Backupperrather thanBrainStore: a brain on PostgreSQL is complete without a self-backup, because its backups arepg_dumpand a retention policy.QuerySource(pkg/cortexdb): an external search engine — Meilisearch, Weaviate — as one fused retrieval lane without becoming the storage. A source returns ids and scores; content is hydrated from the brain, so a source cannot inject text CortexDB never stored and a stale id is dropped rather than fabricated into a result.examples/17_query_sourceimplements it against a real Meilisearch in plainnet/http.deploy/: a hardened systemd unit, a container image whoseHEALTHCHECKis the server binary itself (cortexdb-grpc -health), a compose file, and the backup, upgrade and port-override notes. Verified by installing it on Ubuntu 24.04 with systemd 255.graph.SPARQLMutatesreports whether a query would change the graph, by asking the executor's own parser. It exists so authorization can tell a SPARQL read from a SPARQL write without maintaining a second parser that has to agree withExecuteSPARQLforever.ToolDefinition.Mutates, set on all 61 tools, soCallToolis authorized by the named tool rather than by a blanket classification. Without it a read-only key could call no tool at all, including search — which made read-only useless for the MCP client path, since it proxies everything throughCallTool.
Fixed
A confined key could overwrite and steal another user's rows by naming their id.
SaveMemory,SaveKnowledgeandUpdateKnowledgeare upserts: the request carries the caller's ownuser_id, so the scope check passed, while the id named somebody else's row. For memory theON CONFLICT DO UPDATEbehind it also re-homed the row into the caller's bucket, so the theft erased the owner field that would have caught it. Found by demonstrating it against a running server. Present only in unreleased commits; no released version enforced scopes at all.SQLiteStore.Backupinterpolated its destination intoVACUUM INTO '%s'. The driver executes multiple statements from one string, so a filename ofx'; DROP TABLE embeddings; --dropped the table and returned a nil error. Latent while nothing exposed the path; the new backup RPC would have made it remotely reachable. Now a bound parameter..dockerignore's*.db-waldid not cross a/, sopkg/'s test databases were in every build context: 3.34GB and 47.6s of it, now 214.82kB and 0.3s.
Changed
QuerySparqlandknowledge_graph_queryare writes by default, narrowed to reads per call when the parser says the query does not mutate. CortexDB's SPARQL subset executesINSERT DATAand theDELETEforms, so a tool named "query" can rewrite the graph. A read-only key keepsSELECT,ASK,CONSTRUCTandDESCRIBE.A confined key can now reach its own rows by id.
GetMemory,UpdateMemory,DeleteMemory,GetKnowledgeandDeleteKnowledgecheck ownership against the stored row instead of refusing outright. A forbidden row and a missing one answer identically, so ids cannot be enumerated to map the brain.PostgreSQL + pgvector as a storage backend is documented, which it was not. The capability shipped earlier; a
postgres://DSN moves the whole brain, and 104 opt-in parity tests cover it.
[2.90.0] - 2026-09-02
Added
GraphFilter.Containsmatches a property's text case-insensitively, ANDed with everything else on the filter.Propertiesis how a caller names a batch it already knows; this is how a search enters the graph at all. The fold is spelledLOWER()on both sides rather than left toLIKE— SQLite folds ASCII case and PostgreSQL does not — and the needle is escaped, because_and%are ordinary characters in a name and unescaped they are wildcards: searching a schema foruser_idalso matcheduserxid, and searching for a literal%returned every node in the store.ListNodesisGetAllNodeswithout the vector column. A vector is the largest thing on a node, and enumerating the types in a 400k-node import throughGetAllNodesmoves a gigabyte of embeddings across the driver and decodes every one of them in order to count names. Nodes come back with a nilVectorrather than an empty one.ListNodes,GetAllNodesandCountNodesshare one WHERE builder, so a list and the count printed beside it cannot come to disagree about which rows matched.
[2.89.0] - 2026-09-02
Added
A read can now name the batch it wants.
GraphFiltergainsPropertiesandLimit, andGetAllNodeshonours both.graph_nodesis shared by everything on the machine, and the only filter a read had wasnode_type— but a type name is not a batch. An importer that wrote a thousandPersonnodes on Tuesday and a thousand more on Friday could ask for all two thousand or for none, even though it had stamped which run each row belonged to on the way in.Propertiesmatches top-level string fields in a node'spropertiesJSON, ANDed, read through the guarded dialect helper:propertiesis a TEXT column, a node written without any carries the empty string, and an unguarded read of that raises "malformed JSON" on SQLite and "invalid input syntax" on PostgreSQL — failing the whole statement over one unrelated row, so a caller asking about their own batch would be told their batch was empty.CountNodesreports how many rows match a filter, deliberately ignoring itsLimit. It is the other half of a cap: a caller handed 100 rows has learned nothing about whether there were 100 or 4000, and a count that stopped at the cap would always equal the cap.Both fields are additive. A filter whose new fields are zero produces the query it produced before, and a capped read is ordered by id while an uncapped one keeps the cost it had.
[2.88.0] - 2026-08-30
Added
The live view's panels fold. Four panels sit in the four corners of the scene they describe. On a wide window they have room; in a narrow one — a phone, or the view framed inside another application — they meet in the middle and there is more chrome than graph. Embedded in a 440px panel the title card ran under the controls and the legend and the activity feed took the bottom third between them.
Each panel now folds to a chip in its corner naming what it is hiding, so the view can be cleared without losing the way back. What is folded is remembered per browser; with nothing remembered yet the choice comes from the window, and below 760px the two panels carrying no controls start folded — a window narrow enough for them to overlap the graph is one where they were covering it.
Fixed
The title card no longer runs under the controls on a narrow window. Below 760px it is bounded to the room left beside them and wraps instead of sliding underneath.
[2.87.0] - 2026-08-29
Fixed
CREATE EXTENSION IF NOT EXISTSis not protection against a concurrent creation. It checks and inserts in two steps, so two connections running it at the same moment both pass the guard and one loses in the catalogue withduplicate key value violates unique constraint pg_extension_name_index— an error naming a system index and nothing the caller did. Opening two brains on one PostgreSQL database at once is ordinary and each initialises its extensions on the way up, so this is reachable in production, not only under a parallel test run.PostgresStore.Initreturned "pgvector is required by the PostgreSQL store" and refused to start;GraphStore.initPgVectorrecorded "pgvector unavailable" and did exact scans for the life of that store, having taken a momentary collision for a deployment fact; and the lexical DDL, run as one list whose loop broke on first error, lost both indexes and left CJK search linear without saying so.sqldialect.EnsureExtensionreplaces all three. It does not try to recognise the race by SQLSTATE or error text — on any failure it asks the catalogue whether the extension is present, which is the only question whose answer does not depend on a server's wording. A managed instance that refusesCREATE EXTENSIONstill gets an error, because there the extension really is absent.The race only exists while the extension is absent, which is why it presented as unreproducible flakiness: a database set up once by hand never opens the window. Dropping the extension first makes it fire on every run.
Changed
PostgresLexicalDDLno longer returns theCREATE EXTENSIONstatement; the extension is named by the newPostgresLexicalExtensionand should be created withsqldialect.EnsureExtensionbefore running the index DDL. Creating an extension is not the same kind of operation as creating an index and cannot be one statement in a list.pg_trgmis now created by the test suite rather than assumed. Nothing in this module ever created it: the trigram index worked only because some earlier run had installed it by hand, and a database without it failed withoperator class "gin_trgm_ops" does not exist, which names the index and not the missing extension.
Added
internal/pgtest, one place for PostgreSQL test isolation. Every PostgreSQL test read one DSN and most shared one schema, so a package'sDROP TABLE ... CASCADEreached into whatever elsego test ./...had running. It hands out a schema per test (Open/DSN) or a database per test (Database, for the database-wide operations a schema cannot isolate), creates the extensions under an advisory lock, and drops what it made. Two packages had already worked around this by hand; both now use the helper.
[2.86.0] - 2026-08-29
Added
The graph can be asked what is unreachable, and what things are called. 2.85.0 gave the graph a census by type and stopped one caller writing SQL for it; the same caller had four more queries against
graph_nodes, and two of them are questions this library should answer.Connectivitycounts the nodes no edge touches alongside the total, from one statement so the two describe one graph — a large share of orphans means writes landed and their edges did not, which is the state a store reaches when an ingest's edges are rejected or when entities accumulate across re-ingests with nothing joining them, and it is invisible in every count that only grows. It is distinct fromGraphStatistics.ConnectedComponents, which asks how the reachable part is divided rather than how much is reachable at all.NodeLabelslists nodes with their type and label, narrowed by id prefix and minimum label length: which spellings a vocabulary produced, whether one concept arrived under two names, which labels are long enough to match text against.EntityNodeIDPrefix, exported besideEntityNodeID. The caller listing entity nodes was matchingid LIKE 'entity:%', and reaching into this library's naming is worse than reaching into its tables — nothing would have failed if the namespacing changed. The five places insidepkg/cortexdbthat spelled the prefix themselves now use the constant.
Notes
NodeLabelQuery.IDPrefixis matched literally:%and_in a prefix are escaped rather than treated as wildcards, since a caller passing an id convention is passing a literal. Without the escaping the test for it fails on both backends.The two
pkg/corePostgreSQL lexical tests fail undergo test ./...and pass under-p 1. The packages share one database andpkg/graph's harness truncates it per run, so adding parity tests anywhere makes the collision likelier. Pre-existing.
[2.85.0] - 2026-08-29
Added
The graph can be asked what is in it, by type.
GetGraphStatisticsreports how many nodes there are and never what kind, so a caller checking its graph against a declared vocabulary had nowhere to go but SQL of its own againstgraph_nodesandgraph_edges. That is the worst place to leave a caller: its query is pinned to this schema and to one database's dialect, neither of which is promised to it, and when the graph layer learned to run on PostgreSQL as well as SQLite nothing in such a query would have failed loudly — it still parses, still returns rows, and simply stops describing the graph. Four methods onGraph()cover what those callers were writing:NodeTypeCountsandEdgeTypeCountsfor the breakdown by type,EdgeShapesfor the type shapes the edges actually have — how a declared relation is wrong — andEdgeEndpointPairsfor the nodes those edges run through, which is what is wrong. Untyped nodes are counted under""rather than dropped, so the counts still sum to the node count. Both edge queries take optional edge types and push the filter into SQL. All four go through the dialect-aware path and are covered by the parity suite, which asks both databases the same question and compares the answers field by field.
[2.84.4] - 2026-08-29
Added
The page takes a highlight from whoever embedded it. An application mounting this view has its own search box, and searching it asks about the same things these nodes are. It cannot reach into the page — different origin, behind its own proxy — so it posts
{type: "cortexdb:highlight", query}and the page puts that in the Find box a person would have typed into. Unknown messages are ignored, so an embedder speaking a later dialect gets a page that works rather than one that throws.
[2.84.3] - 2026-08-29
Fixed
The scene follows its container. The HUD is positioned in CSS and reflowed on its own; the WebGL canvas is a fixed pixel buffer and did not, so maximising the window grew the frame around a picture still the size the window had been at load. It is observed with a ResizeObserver on the element rather than a window resize listener, because an embedded page shares a window with panels that can be collapsed — that changes the container's width with no window resize to hear.
The glow resizes too, and is sized to the container in the first place. The bloom pass keeps its own render targets and was built once from
window.innerWidth. Embedded, that is the wrong size from the start, and it stayed wrong afterwards.The reconnect path closes the stream with a method that exists.
es.Close()should bees.close(); a rename applied across the package's Go files reached into the JavaScript held in one of them, and nothing failed to compile to say so. The error handler threw instead of closing, on the path that runs when the connection drops.
[2.84.2] - 2026-08-29
Fixed
The live view can be mounted behind a reverse proxy. Its page opened the event stream at an absolute
/api/stream, which leaves whatever prefix the page was served under and lands on whatever the host serves at/api. The stream is now asked for relative to the page, so a view mounted at/graph/finds it at/graph/api/stream.This is what the package doc already told embedders to do — the view binds loopback and has no authentication, so an application that needs it reachable further should put its own authenticated proxy in front. It just could not actually be done until now.
[2.84.1] - 2026-08-29
Fixed
Closing a live view no longer waits on the streams it is closing.
Shutdownstops the listener at once and then waits for handlers to finish, and every open page is holding an SSE stream that ends only when its request context does — so the wait always ran to its deadline. Two seconds of nothing on every close, with the caller's lock held, and the timeout swallowed so it looked like a clean shutdown.Measured: 2.00s before, 0.25s after. It surfaced downstream as an application's view of one graph still answering while it was switching to another.
[2.84.0] - 2026-08-29
Added
The live 3D view is a package now, not just a tool.
pkg/liveview— moved out ofinternal/, where it had been for exactly one release. Nothing about it changed except that other modules can import it: it was internal because the only consumer was the MCP server, and that stopped being true the moment an application wanted to put the graph on one of its own screens.A caller supplies a
Source— anything that reads nodes and edges — so an embedder that already knows which brain it talks to can say so directly withLoadRemote, rather than setting environment variables forOpenSourceto read back.The listener still binds
127.0.0.1and still has no option to widen it. The page is the whole graph with nothing in front of it; an embedder that needs it reachable further should put its own authenticated proxy there.
[2.83.0] - 2026-08-29
Added
The brain as something you watch, not something you re-render.
render_graph_htmlwrites a file and is finished; the picture is true for the instant it was taken. The newserve_graph_3dtool holds a page open instead — a rotatable WebGL scene on127.0.0.1that keeps up with the brain on its own.Two things move on it, and they arrive by different routes:
Structure — nodes and relations appearing or disappearing. There is no change feed to subscribe to, and the graph can be written by any machine sharing it, so the server re-reads it every two seconds and streams the difference. Only the difference: the layout keeps every node that did not change exactly where it had settled, and reheats only when something new actually arrives.
Activity — a query running, something being saved, a relation being drawn. A query changes nothing in the graph, so no amount of polling can ever show one. These are observed as they happen, from inside the MCP server handling the call, and the nodes they named light up on the page while the answer is still being written.
That second half is why the view is served from the MCP process rather than a side-car. Both modes get it from one hook —
AddReceivingMiddlewareon the*mcp.Server— which is the only point local mode (tools registered from the library) and shared-brain mode (tools proxied over gRPC) have in common. Neither the library's tool surface nor the gRPC proxy had to learn that a view exists.On the page: glow is a real bloom pass, so brightness is earned — a hub with forty edges is drawn bigger and blooms wider, and a node the brain just touched blooms because it went white. Links carry moving light, and Trace path takes two nodes and lights the chain between them while everything else falls away. The camera orbits on a toggle and yields the moment you drag it.
Also
--graph-3don the command line, for a view without an agent attached. It polls, so it sees structure and not queries — and says so on the page, because a ticker that never moves is otherwise indistinguishable from a broken one.The listener binds
127.0.0.1and there is deliberately no flag to widen it: the page is every entity anyone stored, with no authentication in front of it.A place to put a graph that is not the brain. Four tools —
side_graph_write,side_graph_read,side_graph_list,side_graph_drop— write nodes and edges into a named graph kept in its own local database, andserve_graph_3dtakes agraphname so each one can be watched in 3D on its own port.The brain is what is known. A run's steps, the plan behind them and what each was expected to do are none of that: they happened once, most are wrong by tomorrow, and there are thousands of them. A few thousand step nodes on top of a few thousand real ones and the graph stops being readable.
This is the mechanism and not the policy. Nothing in the implementation mentions runs, steps or expectations — CortexDB does not know what those are. It gives a caller more than one graph and gets out of the way; what belongs in which is the caller's decision.
Two things follow from how the store actually works, rather than from preference:
A side graph is a separate database file, because
graph_nodesandgraph_edgeshave no tenancy column. There is no namespace to scope by, so "another graph" can only honestly mean another store. It also makes a side graph disposable by deleting one file, which is most of the point.Side graphs are always local, even when the brain is shared. These tools are registered beside the renderers rather than proxied over gRPC, so scratch work never travels to the machine everyone else reads, and no server needs upgrading to use them. The brain is shared; your working notes are yours.
A graph name becomes a filename, so it is checked against an allowlist (
^[a-z0-9][a-z0-9_-]{0,63}$) rather than a blocklist — that list is never finished...,a/b,.hidden,a.dband a name with a NUL in it are all refused.
Fixed
Node labels no longer carry line breaks into the views that print them. A label is usually the first line of the text it came from, and that text has newlines in it.
clipLabelcollapses whitespace before clipping, which also fixes the same ragged output in the static renderer.nextedges are no longer dropped by name. Both graph readers skipped the type outright, alongsidehas_chunk, as structural. Butnextis also the obvious name for one thing following another — a plan's steps, a trace — and a caller who modelled a sequence got its nodes back with every link between them silently missing. What makes an edge structural is that it wires chunks, so the endpoints decide now, not the label. Chunk wiring stops inflating degree too, which is what decides who survives a truncated listing.Both read paths changed: the local SQL the views use, and
ListGraphAll, which is what a shared brain answers with — so a shared deployment needs this version on the server before its own view shows those edges.The shared-brain truncation note stopped repeating forever.
fetchGraphRemoteprints "showing the 2000 most-connected of 4015 nodes" — useful once from a one-shot render, and nothing but noise from a poller calling it every two seconds.
Performance
The live view runs at 59fps on a 2000-node, 5892-edge brain, up from 17. Measured on the page, not estimated. Almost all of it was one thing: an arrowhead on every link is a separate cone mesh, and 5892 of them cost three quarters of the frame budget. Arrowheads and flow particles are now spent only where they say something — the traced path, and links the brain just wrote — on any graph large enough for the difference to matter.
Two layout fixes came with it. Repulsion had no maximum range, so the handful of nodes with no edges drifted until the interesting part was a speck in the middle of empty space; capping the range keeps the graph together and is cheaper besides. And framing now ignores the far tail, because fitting everything means mostly framing the gap between the outliers and the rest.
[2.82.2] - 2026-08-28
Fixed
Tests no longer name their databases after a clock that cannot count that fast. Nothing in the shipped library changed; this is the test suite, and it had been failing about one run in six for a reason that looked like a fault in the store:
open lexical evaluation db: failed to initialize store: vectorstore: init: failed to enable foreign keys: database is locked (5) (SQLITE_BUSY)Two
t.Parallel()tests both built their path asfmt.Sprintf("test_evaluation_lexical_%d.db", time.Now().UnixNano()). The name says nanoseconds; the clock underneath does not have to tick that fast, and on darwin/arm64 it ticks once per microsecond — two goroutines reading it together get the same number about three times in four. That is not two databases but one file opened by two connection pools, and the loser reports SQLITE_BUSY at Open. When the two missed each other at Open they collided over the data instead, one test's rows landing in the other's assertions.internal/testname.Nanoreplaces the call at all 150 sites: still an int64, still ordered, still roughly the wall clock, but never handed out twice in a process, and seeded with the pid becausego test ./...runs packages in parallel. The evaluation fixture also moves intot.TempDir()— it had been writing its database next to the source and removing only half of it, leaving the-waland-shmbehind run after run.Measured both ways: with the old naming, 4 of 25 runs fail; with this, 0 of 25, five clean runs of the full suite on each backend, and a clean
-race.
[2.82.1] - 2026-08-28
Fixed
A search survives the
vectortype being replaced under a live connection.vectoris not a built-in: its OID is assigned when the extension is created and a new one is assigned if it is ever dropped and created again — a restore, an extension reinstall, a test suite sharing the database. pgx caches prepared statements per connection and a statement's descriptor names its parameter and result type OIDs, so a connection that prepared a vector query beforehand then asks the server about an OID that is gone:ERROR: cache lookup failed for type 91164 (SQLSTATE XX000)It reaches only the statements carrying the vector type in their descriptor — a bound parameter or an operator operand — which is the vector search path and nothing else. The error is raised while the server resolves the types, before the statement runs, and pgx drops the offending cache entry on the way out, so the reads now retry once and succeed. Once rather than in a loop, because a second failure means something other than a replaced type. Matched on SQLSTATE and message text: XX000 is PostgreSQL's catch-all internal error and 0A000 covers every unsupported feature, so either code alone would swallow failures that have to reach the caller.
Worth knowing alongside it, because the two arrive together:
DROP EXTENSION vector CASCADEalso drops every vector column. The tables survive andembeddings.vectordoes not, so an extension reinstall needs the columns put back whatever this retry does.
[2.82.0] - 2026-08-27
Added
A brain can live on PostgreSQL. The DSN picks the backend: a path is the SQLite file it has always been, a
postgres://URL is PostgreSQL with pgvector. Everything follows — documents, collections, sessions and messages, the graph, RDF triples, temporal facts, agent memory — and vector search happens in the database rather than in Go.pkg/sqldialectholds the handful of things that genuinely cannot be written once; the rest of the SQL is shared. SQLite is untouched and remains the default.A fact can say where it came from.
fact_provenanceanduncited_factsreport which chunk asserted an edge and which edges nothing supports.The ontology decides what contradicts what. A link type declared ONE on a side makes a second value for that side a contradiction rather than a second fact, so supersession is deterministic instead of heuristic.
Fixed
Ten defects found by sending the same JSON to the same tool on both backends and comparing the answers — sixty-four calls, seventeen of which failed on PostgreSQL while every unit test stayed green.
All graph-mode retrieval on PostgreSQL.
json_valid/json_extractwere written out in four files; the one reading a chunk'sdocument_idsits under every graph-mode query and raisedfunction json_valid(text) does not exist. Now on the dialect, withJSONFlagkept separate fromJSONTextGuardedbecause a JSONtruereads back as the integer1on SQLite and the text'true'on PostgreSQL — an inference rule was re-deriving its own output.Document deletion on PostgreSQL, which used
json_eachand answered with "syntax error at end of input", and ran its queries on the raw handle so its placeholders were never rebound.Lexical search was handed FTS5 syntax as if it were prose. The retrieval layer quotes every token and emits
owner OR name;plainto_tsqueryread that as an AND over the English word "or", which no document contains.ParseFTS5reads the expression now, and the rank is built from the same parse.Every memory search on PostgreSQL, which asked for
messages_fts MATCH ?— an FTS5 virtual table and an operator that database has neither of.Object sets and ontology links, which compared with
COLLATE NOCASE:collation "nocase" for encoding "UTF8" does not exist.Saving a memory on PostgreSQL. The vector went in as SQLite's blob encoding, so
memory_save,knowledge_memory_rememberand every consolidation failed.Recall accounting, patched with
json_setand discarding its errors by design, so it wrote nothing on PostgreSQL and said nothing about it:recall_countstayed at zero forever.Ontology actions on PostgreSQL. The audit table's DDL used
INTEGER PRIMARY KEY AUTOINCREMENT, so it could not be created and no action could be applied.SELECT EXISTSscanned into anintwas next in line.Saving new knowledge on PostgreSQL.
GetDocumentspelled "not found" as a bare error, andSaveKnowledgereads that answer witherrors.Isto decide create-or-update.Scored rows carry their collection name again from the vector arm, which had drifted from the other two copies of the projection.
Two more that are not portability at all, found because the other backend disagreed:
SQLite's vector search post-filtered. The index was asked for the globally nearest rows and the collection filter applied afterwards, so a search scoped to a collection whose rows missed the global top-k came back short — or empty. It failed worst on a large store with several collections, which is the case it exists for.
Graph edges were read with no
ORDER BY. SQLite happened to return insertion order; PostgreSQL returned whatever the plan produced, and could return something else next time. Neighbors feeds graph-mode retrieval, so the same question could retrieve a different set of chunks on two runs of one database.
[2.81.0] - 2026-08-27
Fixed
Entity resolution no longer merges across node types. Grouping was by spelling alone, so "Primary" the state and "primary" the node — one canonical key apart — merged, putting a role where a machine belongs and ending every
has_stateedge at a Node. The deterministic pass now keys groups by type as well, and the LLM pass takes the first member's type as the group's. Untyped nodes share the empty type and still meet each other; same-type spellings merge exactly as before.
[2.80.0] - 2026-08-26
Fixed
A relation endpoint resolves to the entity the schema is about. An endpoint names an entity, and a name can belong to more than one node; resolution took the first by id. In a store holding a prose vocabulary and a code graph — a "Snapshot" entity and a Go type of that name — the code node's id sorted first, so edges extracted from runbooks attached to structs. An endpoint now prefers a node whose type the ontology declares; among equally declared or undeclared candidates the id still decides, so a store with no ontology is unaffected.
[2.79.0] - 2026-08-26
Fixed
A flag is no longer merged with an identifier.
canonicalKeydropped punctuation, so--read-onlyandReadOnlyshared a key and entity resolution collapsed them under whichever won — losing the difference between what you paste into a shell and the field of the same idea, which is the one thing a CLI's help text is ingested for. Five of eighteen proposed merges on a live base were this shape, and node type did not separate them. A leading dash now survives into the key; two spellings of one flag still merge.
[2.78.0] - 2026-08-26
Added
Entity resolution can be scoped to node types.
ResolveOptions.NodeTypes(and--resolve-entities --types A,B) restricts the merge to the types named. Resolution reads a node's content as its name, so a store that also holds a code graph — where each symbol keeps its bare name as content and its path in the id — had every package'smain.gosharing one canonical key, and a resolve pass merged distinct files into one with their edges repointed. Empty keeps every entity, which is what callers had. The CLI prints the scope so a merge over everything cannot be mistaken for a scoped one.
[2.77.0] - 2026-08-26
Added
Aliases resolve at query time.
--resolve-entitiesmerged "K8s" into "Kubernetes" and recorded the alias — and a query naming the alias then derived the dead node's id and found nothing. Lookup now falls through to alias records when the direct node is gone. The resolve prompt also covers plural forms and cross-language names (VM/VMs, 李员外/Liang Li).Recall usage accounting and
--memory-usage. Every memory a search returns getsrecall_countandlast_recalled_atstamped server-side, best-effort. Deliberately observability, not policy — recall begets recall, so counts never feed ranking. The report shows most-recalled memories and never-recalled ones older than a cutoff as prune candidates for--export-memory/--sync-memory.
[2.76.0] - 2026-08-26
Added
Automatic session capture — the write-side loop. A SessionEnd hook hands the finished transcript to
--capture-session, which distils it into fact-scoped memories viaCORTEXDB_LLM_*(declining quietly when unset) and saves each with its entities, so auto-captured facts are reachable through semantic, lexical and graph recall alike. One fact per memory by design: long mixed notes embed to mush. The hook detaches immediately — a session's exit never waits on a model. Ids are stable per session and slug, so re-capture overwrites rather than stacks. Disable withcortexdb-session-end --disable. Measured: probes phrased in Chinese hit English auto-captured facts at rank 1.
[2.75.1] - 2026-08-26
Added
--graph-cleanup [--dry-run] [--prune-only|--reindex-only]. Prunes generic entity nodes whose names the current extraction rules would never produce (typed nodes are exempt: a declared type means a person said what the thing is), naming every victim, and backfills graph presence for stored memories — entities extracted with the current rules, a memory node, mention edges — so the pre-2.74 backlog is reachable through entity_names like a new save. Capped at twelve entities per memory. Idempotent.
Fixed
Graph memory recall cut on an alphabet when mention counts tied. The candidate list was truncated to topK in SQL order before boosts ran; on a well-indexed graph a popular entity has hundreds of one-mention neighbours, so ties went to whichever memory id sorted first. Candidates are now boosted and sorted before the cut.
[2.75.0] - 2026-08-26
Added
Semantic memory recall with a calibrated noise floor. The memory path now uses the cosine scores
SearchChatHistoryalways computed and threw away, filters them at a floor calibrated on a live store (unrelated probes peaked at 0.263, the weakest genuine hit scored 0.311; the floor sits at 0.28), and merges above-floor semantic hits ahead of lexical ones instead of replacing them. Golden-set effect against a copy of the live brain: total recall@5 0.654 → 0.846, paraphrase 0.000 → 0.667 with both cross-lingual cases recovered. Saving no longer fails when the embedder does — the memory is stored without a vector and--reembed-memories(new) fills the backlog; the existing reembed pass covered knowledge chunks only.supersedeson memory_save. A correction names the memories it replaces; they stay stored and exported withsuperseded_byin their metadata, but recall stops presenting them as current. Superseding a missing id fails loudly.Importance and age rank memories, as a bounded tie-breaker. The multiplier lives in [0.85, 1.0]: equal matches order by importance and recency, a genuinely better match cannot be dethroned by being old. Raw multiplicative decay was measured first and rejected — it pushed correct months-old answers out of the top ranks.
--recall-eval <golden.jsonl>measures recall quality (recall@K, MRR, forbidden ids, score spread, per category) against either brain, so retrieval changes are judged by a fixed yardstick.
[2.74.1] - 2026-08-23
Fixed
--export-memorydropped memories, and--sync-memory --prunewould have finished the job. Measured against a store of 2043: the export reported all of them and wrote 2041 files, of which one held the index. Three memories were gone, and because the directory is what a sync treats as the truth, pruning from it would have deleted those three for real. Three defects, all in the export.slugifyreturned"memory"when nothing survived — every CJK-only title did — and on macOS and Windowsmemory.mdisMEMORY.md, so writing the index overwrote a memory; the id, which is ASCII and always present, is the fallback now, and the index name is reserved before the first file is written.uniqueSlugassumed its suffixed form was free:"situation"seen nine times produced"situation-9", which is also what a memory titled"situation 9"slugs to, and the second write replaced the first — each candidate is checked now. And nothing compared the file count to the memory count, so both losses were silent; the export verifies before reporting success and fails loudly otherwise. The frontmatternameis the slug the file actually got, so it can no longer be empty. The same store now exports 2043 files and syncs back with nothing to create, update or delete.
[2.74.0] - 2026-08-23
Fixed
Memory recall ranked its worst matches first. FTS5
bm25()is negative and a better match is more negative. Both memory paths turned it into a score with1/(1+|rank|), which is decreasing in match quality, and then sorted descending — so theORDER BYhanded back the best match first and the scoring put it last. A long memory that mentioned a term once outranked a short one about nothing else, and the top-K cut discarded the real hits. Measured on a five-document corpus: the strong match scored 0.589 against the weak match's 0.807. In a populated store it showed up as recalled memories whose scores sat inside a 0.006-wide band — no discrimination left to rank on, so recall read as noise.pkg/core/chat.gowas never affected: it orders bybm25()in SQL and never converts, which is why knowledge retrieval kept working while memory recall did not.
Added
Memories can be corrected and forgotten by editing files.
--export-memorywrote a directory of Markdown files but nothing read one back, so removing a wrong memory meant callingmemory_deletewith an id nobody had written down. (--import-agent-memoryis not the reverse: it scans~/.claudeand~/.codex, writes into a knowledge collection rather than the memory store, and only ever adds.)--sync-memory [dir] [--prune] [--dry-run]closes the loop, using the frontmattermetadata.idthat the export already emits to match a file to its record; a file without one gets an id from its name, so a memory can also be written by hand. Planning is separate from applying, because withCORTEXDB_REMOTEset the same plan is replayed throughmemory_saveandmemory_delete.--pruneis opt-in and refuses to run against a directory holding no memory files — an empty directory is far more likely to be the wrong path than an instruction to delete everything — and deletions are listed by id before they happen.Entity hints can reach a memory, because memories are now in the graph.
saveMemoryGraphwrote the entities a memory was saved with but nothing joining them to it: mention edges hang offChunkIDsand it passed none. The graph grew entity nodes no memory pointed at, so memory search answeredentity_nameswith "this API does not use graph expansion" — which was true; there was no edge leading to a memory to walk. A memory now gets a node of its own, typedmemoryrather than left to the stub filler that would call it a chunk, and each entity mentions it, putting memories on the same footing as knowledge chunks. Graph hits are scored by how many of the named entities a memory mentions and merged ahead of lexical ones, which still run to fill top-K, so a graph miss degrades to the old answer instead of no answer. Auto mode will not infer graph from entity-looking words in the query on this path: memory graph presence is opt-in, so a guess usually finds nothing and only mislabels the decision on the way to the same lexical answer.memory_searchtakesentity_namesdirectly.
[2.73.0] - 2026-08-23
Added
A link side may name an interface, so a polymorphic relation can be traversed. An interface was a first-class thing on one half of the schema and did not exist on the other: a type filter on
FindNodes, search or brain expanded one into its implementors, while a link side refused to name one at all. The gap was not inert. Foundry models a relation whose source is polymorphic — anything Protector protects a Volume — by pointing a side at the interface, and with that unavailable the only encoding left was an open supertype standing in for "any of these". CortexDB decides a traversal's direction by keeping only nodes of the far side's object type, and nothing is ever stored under a supertype's name, so every such traversal returned the empty set — which is also what the graph says about a subject it knows nothing about, on a schema that read as complete. Validation,orientLinkand the search-around filter now all ask the type closure instead of comparing the name.
Fixed
Two ambiguities interfaces make reachable are now rejected at save time. A side name has to identify one hop from the object type a traversal starts at, and an interface makes a collision indirect:
protectson Protector andprotectson Snapshot do not collide by name, but a traversal starting at a Snapshot matches both and declaration order decides which. And a link whose two ends overlap without being identical has no unambiguous direction — with one side over {Snapshot, Backup} and the other over {Backup, Volume}, an edge between two Backups reads both ways. Complete overlap stays legal: that is a self-link, where either orientation is the same statement. A foreign key on an interface side is also refused, since a key is a column on one concrete row.
[2.72.2] - 2026-08-21
Fixed
Auto-recall injects memories, not just knowledge. The
--recallmode behind the Claude CodeUserPromptSubmithook askedknowledge_search, which reads durable knowledge only — so anything written withmemory_savenever reached a prompt automatically, however well it matched. The failure looked like working software: memories were injected, they were simply always knowledge documents. Both paths now ask for the fused view (knowledge_memory_recallon a shared brain,KnowledgeMemory().Recallon a local one), memories first, with light graph expansion because this runs before every message. Memory bodies are flattened to one line and cut to 220 runes — knowledge arrives pre-snippetted, memories arrive whole. A brain on an older server still answers in the knowledge-only shape, and that shape is still parsed.
[2.72.1] - 2026-08-20
Fixed
Graph read paths no longer fail with "no such table" on a fresh database. The
graph_nodes/graph_edgesschema was only created lazily by write paths viaInitGraphSchema, so read-only tools hitting a database that had never taken a graph write —find_nodes,object_set_resolve,expand_graph, graph queries, the graph branch ofknowledge_memory_recall— errored withSQL logic error: no such table: graph_nodes(orgraph_edges) instead of returning an empty result.Opennow initializes the graph schema eagerly (idempotent, cached), so every read path sees the tables from the first query and an empty database answers with empty sets. Downstream callers that special-cased these errors as "empty graph" no longer need to.
[2.72.0] - 2026-08-19
Changed
README overhaul (EN + CN) to match the code. Documented the
KnowledgeMemorybrain facade (Recall/Remember/Reflect/Consolidate/BuildContextPack/PromoteToKnowledgeand graph exploration), composable retrieval viadb.Query/cortex_query(prefetch lanes, RRF/DBSF fusion, theAuthorizeretrieval gate and pluggable reranking), deterministicextract_conversation, property-graph inference (apply_inference) next to the RDFSknowledge_graph_infer_*surface, entity provenance +delete_document_graph, and the supporting packages (eval,rpcserver,agentmem/hindsight,semantic-router,quantization,geo). The Chinese README also picks up the ontologyenforcement: "vocabulary"mode and the interface/object-type name-collision rule it was missing.Version manifests re-synced with releases.
version.goand the plugin manifests sat at 2.67.0 while 2.68–2.71 shipped, so plugin installs kept downloading the 2.67.0 MCP binary — this release moves the pinned binary forward past the 2.68–2.71 retrieval fixes (FTS5 query sanitization, theAuthorizewidening, the HNSWef >= kfix).
[2.71.0] - 2026-08-15
Fixed
HNSW search was capped at ~50 results regardless of TopK.
efis the size of the candidate list a graph walk keeps, so it must be at leastk— the search was calling the index withk = TopK*2and the configuredEfSearch(default 50), which returnsefneighbours and says nothing about the rest. Every vector search therefore truncated at ~50: a request for 2000 came back with 50, and any row ranked past that was unreachable through any API — deep retrieval, pagination-style over-fetch, and theAuthorizewidening added in 2.70.0 all silently hit the same invisible ceiling.efis now raised tokwhen the configuration sets it lower.
[2.70.1] - 2026-08-15
Fixed
Authorizewidening stopped one round too early. A recall that returned fewer rows than it asked for was read as "the corpus is spent", but hybrid recall fuses vector and BM25 results and dedupes, so it is routinely short while a wider fetch would still reach more. Only a round that adds no new rows ends the search now. Without this the widening still missed the authorized rows in the case it was written for.
[2.70.0] - 2026-08-15
Fixed
Authorizenow delivers the TopK it documents. The retrieval gate promised that "the search over-fetches internally so the caller still receives up to TopK authorized results", but the over-fetch was a single fixed round ofmax(TopK*5, 50): a predicate selective enough to matter saw every one of its rows ranked below that cut and the caller got a near-empty result that reads as "nothing matched". This is worst for the feature's main use case — an RBAC subject who may read a small share of the corpus could search and find almost nothing, with no error to say why. The gate now widens its recall (doubling, bounded at 4096) until TopK candidates pass or the corpus is exhausted; a generous predicate still costs exactly one pass. Found in a store where one imported code graph outnumbered the prose ten to one and scored identically, so no prose row reached rank 50.
[2.69.0] - 2026-08-15
Added
Entity provenance, and deletion shaped like ingest.
upsert_entitiesnow records the union of everydocument_idthat asserted an entity in the node'ssource_document_ids, and creates stub chunk nodes for mention edges whose chunks were embedded outside the graph — previously those edges died on the foreign key, for a long time silently, so a store could hold hundreds of entities with no record of what mentioned them and no way to ask "where did this come from". On that provenance sitsdelete_document_graph(alsoGraphRAGToolbox.DeleteDocumentGraph): it removes a document's chunk and document nodes, its relation edges, and the entities it alone asserted; entities other documents also assert are detached, not deleted.dry_runreports without removing. Stores audited after a few rebuild cycles carried ~20% orphaned entity nodes; this is the missing half of the ingest lifecycle that produced them.enforcement: "vocabulary"on ontology schemas. The strict default forced extraction pipelines to choose: activate the schema and lose every extracted entity to primary-key validation (LLM extraction from prose has no storage identity to offer), or leave it inactive and lose canonical type spellings and interface retrieval with it. A vocabulary schema canonicalizes without gating: declared types with a supplied primary key still get typed node IDs, keyless and undeclared ones fall back to name-derived IDs, undeclared link types pass through.strict_actionsand vocabulary enforcement are rejected as contradictory at save time.
Fixed
Batch write failures are no longer silent. The graph batch writers report per-row rejections in their result rather than their error return, and every ingest-path caller discarded the result: an ingest whose every row was rejected still answered "ok". Callers now fold the result back into an error (
BatchResult.Err), and the batch writers create the graph schema themselves instead of failing every row with "no such table" when a caller reached them before initialization.
Notes
The v1
graph_ontology_schemastable is legacy: v2 stores schemas inontology_schemas_v2and neither reads, writes nor drops the old table. An emptygraph_ontology_schemasin an upgraded store is expected and ignorable.
[2.68.0] - 2026-08-15
Fixed
A user's query is no longer read as FTS5 syntax. Hyphenated and punctuated terms (
on-drbd-demote-failure,al-extents) made FTS5 raise a syntax error instead of matching; queries are now sanitized into plain terms before they reach the index.
[2.67.0] - 2026-08-10
Added
A Palantir-style ontology, replacing ontology-lite. Object types now carry typed properties and a mandatory primary key, so two spellings of one airport stop becoming two nodes — identity is
objectType + primaryKeyrather than a normalised name. Link types are bidirectional with a cardinality per side, the way Foundry models them: a one-to-many link is oneONEside and oneMANYside, and only theONEside may name a foreign key. 14 property data types, includingvector,geopoint,arrayandstruct.Interfaces give polymorphic retrieval. An object set or
find_nodesquery againstFacilityreturns every implementing object type, and a new implementor becomes visible without touching the query. Interfaces may extend several parents; cycles and unsatisfied required properties are rejected at save time rather than surfacing later as a confusing write failure.object_set_resolve— a composable object set algebra.base,interface_base,static,reference,filter,search_around,union,intersect,subtract, with predicateseq/lt/lte/gt/gte/in/is_null/contains/starts_with/contains_all_terms/contains_any_term/nearest_neighborsandand/or/not. This is the part worth having: vector KNN, full-text terms and link traversal become peer operators inside one expression instead of three retrieval APIs that cannot be combined. Chainedsearch_aroundis capped at three hops, matching Foundry's own limit.ontology_action_apply/ontology_action_list— governed writes. An action declares typed parameters, declarative rules and submission criteria, and every application is recorded in an audit trail.validate_onlychecks parameters and criteria without writing;return_editsreports what changed (the two are mutually exclusive, as in OSDK 2.0). Settingstrict_actionson a schema closes the generic upsert tools so actions become the only write path — the point being that an agent gets a reviewed, named surface rather than free-form graph access.ontology_diff— reports what changed between two schema versions and flags the changes that would invalidate data already written: removed types, changed property types, newly required properties, a changed primary key, tightened cardinality.Typed tool generation from a schema (
GenerateOntologyTools) — turns action and object types into tool definitions whose parameter names, types and required-ness the model can see, instead of prose inside a generic upsert. Bounded at 32 by default and deliberately not auto-registered: the cost of a large generated surface lands on the agent's context window.gRPC
schema_jsononSaveOntologySchemaRequestandOntologySchema. The v1 repeated fields cannot express a primary key or a typed property, so they are deprecated (numbers retained) and a request setting only them is refused with a message saying so, rather than silently ignored.
Fixed
Relations whose endpoints were created in the same batch failed validation with "could not resolve" — the resolver only looked in the graph, never at the entities being written alongside.
Endpoint resolution by name could return a chunk.
graph_nodes.contentalso holds chunk text and document titles, andchunk:sorts beforeentity:, so an unqualified lookup preferred them.Node type casing was whatever the last writer used. Node IDs already folded case, so
type: "airport"andtype: "Airport"were one node with a flappingnode_typecolumn that leaked into subgraphs, exports and model context. Ontology-validated writes now store the declared casing.
Known limitations
OntologyProperty.Vectorizedis declarative only — no write path embeds those properties yet, and entity vectors are lexical hashes, sonearest_neighborsis meaningful only with an explicit query vector.An active ontology constrains
SaveKnowledge: its built-in extractor emits untyped entities, so the schema needs a catch-allentityobject type and arelated_tolink type.modify_objectdoes not updategraph_nodes.content, so name-based endpoint resolution still finds the pre-rename title.Foundry's function runtime, branches/proposals, dynamic row-level security and backing datasources are deliberately not modelled.
[2.63.2] - 2026-08-04
Added
memory_list_all— bulk listing for the views that need every record rather than a search result. It reportstruncatedwhen a limit cut the listing short, because an export that silently dropped records would look complete and not be.memoryflow_apply_memory_edits— apply memory edits an agent has already decided on: add, update, or supersede. The apply path is exposed rather than the propose path on purpose: whoever calls the tool is a model that has just read the conversation, so having it decide the edits is better informed and cheaper than a second LLM round-trip inside the server. Superseding is opt-in, capped, and reversible — the old memory is kept and linked forward.LLM-driven memory maintenance in
pkg/memoryflow(ProposeMemoryEdits,ApplyMemoryEdits,UpdateMemoryFromText) — the memory-side equivalent ofUpdateGraphFromText, deliberately without its delete. A graph fact removed in error is re-derivable; a memory is often the only record that something was said.
Fixed
Three memoryflow tools were defined but never reachable over MCP —
memoryflow_resolve_taxonomy,memoryflow_list_episodesandmemoryflow_get_transcriptwere inDefinitions()and absent fromNewMCPServer, so they worked through the Go API and were invisible to every MCP host. Same gapfind_nodesshipped with in 2.62.0;pkg/cortexdbgrew a coverage test then, andmemoryflow— which has the identical two-list shape — never did. It does now, and the test found all three on its first run.--memory-htmland--export-memoryignored the shared brain, rendering a local file that nothing writes to on a machine using a remote. They now fetch every record over gRPC.
[2.63.1] - 2026-08-04
Fixed
Auto-recall ignored the shared brain.
--recallreturned frommainbefore theCORTEXDB_REMOTEbranch was ever reached, so a machine configured for a shared brain still answered theUserPromptSubmithook from its own local database file. The failure is silent and reads as working software: memories are injected on every prompt, they are simply the wrong ones — frozen at whenever that machine switched over. Found on a host whose local file had been read-only for hours while every write went to the shared brain; the hook kept surfacing month-old entries and never once surfaced anything written that afternoon. The hook now callsknowledge_searchover gRPC whenCORTEXDB_REMOTEis set, and both paths render hits through one formatter so the text an agent sees never depends on where the brain lives. Silent on every failure as before: a bad token, an unreachable server, or an unexpected response shape all inject nothing rather than blocking the prompt.The other one-shot modes (
--graph-html,--export-memory,--learn-path) still act on a local database; the docs now say which is which.
[2.62.1] - 2026-07-31
Fixed
find_nodeswas defined but not reachable over MCP. It shipped in 2.62.0 with a schema, a dispatcher entry, tests and a changelog entry — and was absent from every host that speaks MCP rather than the Go API, becauseNewMCPServerregisters each tool by hand and that list is separate fromDefinitions(). Nothing about the symptom points at the cause: a host asks for a tool and is told it does not exist, by a server whose release notes say it does. Found within the hour by the application it was built for.The two lists now have to agree. A test connects a client to the server over an in-memory transport, asks it what it exposes the way a host does, and fails naming any defined tool that is missing.
LearningPath,NextConcepts,MissingPrerequisitesandUpdateGraphFromTextare not caught by it — they are not inDefinitions()at all, and remain CLI-only.
[2.62.0] - 2026-07-31
Added
find_nodes— enter the property graph by name.expand_graphandget_nodesboth take node IDs and nothing else, and nothing turned a name into one. A caller holding a name had to derive the ID its writer would have produced: re-implement someone else's hashing, guess the entity type, and hope. A wrong derivation returned an empty subgraph — the same answer the graph gives for something it has genuinely never heard of — so the failure was silent and read as missing data.Found from outside: a tutoring application built a study-path planner on these edges and got nothing on every query against a real database, because 957 of its 1017 prerequisite edges hung off IDs whose scheme it had guessed wrong. Deriving an ID also needs every input the writer used — for that application, which textbook a concept came from — and 953 of its 1030 sessions do not have one, so 92% of the time there was nothing to derive from and the walk had no seeds at all. A name needs none of that.
It matches strings, not meanings. Measured against that database:
two phase commitreachesTwo-Phase Commit, 泰勒级数 reaches 泰勒级数 — but 左极限 does not reach "Left-Hand Limit", and is not meant to. Translation is a different problem and this is not a step towards it. An earlier draft of this entry used that example as though it worked; it does not.Three passes, weakest last — exact, case-and-punctuation folded, containment — and every match says which it was, because a caller acting on a containment hit should be able to decline. Shorter names win among equals: extra words narrow a concept, so a bare "Limit" wants
Limitand notInfinite Limit.node_typeskeeps places in a book ("Chapter 4") out of an answer about things to learn.The fold keeps letters and digits by Unicode class rather than by ASCII. Without that every CJK name folds to
""and they all match each other — proven by breaking it deliberately, which resolved 量子色动力学, absent from the graph, confidently onto an unrelated Chinese concept.
Fixed
LearningPathno longer reports a concept as cyclic for being stuck behind a cycle.Cycleswas read off the topological sort: whatever Kahn's algorithm was still holding when it stalled got named. That set is "has an unmet prerequisite" — the cycle plus everything queued behind it — and the two coincide only when the whole graph is one cycle, which is what the test built. On two disjoint cycles under a target requiring both, seven concepts produced eleven entries: the second cycle twice, and the target itself, which nothing requires and so cannot lie on any cycle. Now computed with Tarjan before the sort runs, so each concept appears once and only if it is genuinely on a cycle. The stall still breaks the deadlock the same deterministic way; it just no longer draws conclusions from being stuck.
[2.61.0] - 2026-07-31
Not logged when released. Recorded here from the release commit.
Added
Learning prerequisite graphs (
pkg/graphflow/learning.go). Study material as concepts linked by prerequisite edges.LearningPathreturns the prerequisite closure of a target, topologically sorted, minus what is mastered — the question retrieval cannot answer: what must I learn, and in what order, before X.NextConceptsgives the learnable frontier,MissingPrerequisitesexplains being stuck,MarkMasteredpersists mastery on the graph.LLM-driven graph CRUD (
pkg/graphflow/graph_edit.go). Ingestion only ever added, so wrong facts accumulated with no way to retract them.UpdateGraphFromTextshows an LLM the relevant existing subgraph alongside new text and applies the resulting add/update/delete edits. Deliberately embedder-free — relevant entities are found by lexical mention, writes use lexical vectors — so the whole surface works with only a chat model. Deletes are opt-in (AllowDelete), capped per run, and previewable withDryRun.Reachable from
cortexdb-mcp --graph-updateonly; it is not registered as an MCP tool, so a host that speaks MCP rather than the CLI cannot use it.
[2.60.3] - 2026-07-28
Fixed
A lexical-only search whose one arm cannot run now fails instead of returning nothing.
HybridSearchbuilt the keyword query, handed it to SQLite and ignored the error (if err == nil). With no vector supplied that arm is the only arm, so a broken or half-migrated FTS index produced an empty result set and a nil error — indistinguishable from a corpus that genuinely contains nothing, and unfalsifiable from outside because every caller sees success. It now returns the error when it is the only arm, and degrades with a log line when a vector arm is still answering (vector-only results look exactly like ordinary ones, so the difference has to be said out loud).Rows that will not scan are logged instead of silently dropped. Three
rows.Scansites — two inHybridSearch, one inftsSearch— didcontinueon error. One column that will not scan (a NULL metadata, a NULL collection_id) silently cost every row it affected, which is how a search that matched plenty answers "nothing found".HNSWHybridSearchno longer truncates an uncapped query to nothing.TopKis optional, and its siblingHybridSearchtreats zero as "no cap" — but the HNSW path truncated unconditionally, so the same query returned every match without an index and an empty slice with one. An uncapped query now goes down the exhaustive path, because an HNSW search takes a candidate count and cannot answer "everything".
Known issue
SimpleHNSWrecall is not deterministic and can be poor.Addlinks each node to the candidates from a single-candidate search of the entry point, and withrandomLevel()deciding the layout, 2 runs in 15 of a four-node fixture returned 1 of 4 matches. Real HNSW selectsMneighbours from anefConstructioncandidate set; fixing this is a rewrite of the index's construction rather than a guard, so it is recorded rather than patched. Searches that need reliable recall should leave the HNSW index off, where the exhaustive path is exact.
[2.60.2] - 2026-07-28
Fixed
search_textreturned nothing whentop_kwas omitted.top_kis optional in the tool schema, so a model calling the tool routinely leaves it out — and the zero was carried all the way into the finalordered[:TopK]truncation, cutting the entire result set away. The tool answeredchunks: nullwith a plan and a decision that both looked correct, so it read as "this corpus does not contain the term" while the FTS index held dozens of matches; the same query withtop_k: 5returned five.SearchTextOnly's existing default did not help because it normalises its own copy of the options, not the value the caller's merge truncates by. Normalised once at the top of the candidate merge, against a new package-leveldefaultSearchTopK. Every existing search test passedTopKexplicitly, which is why none of them caught it.
[2.60.1] - 2026-07-27
Fixed
plan.collectionis accepted as shorthand forplan.filters.collection.collectionis a top-level parameter of the same call and also lives insideplan.filters, so a model reaches forplan.collection— and withadditionalProperties: falsethat was a hard schema rejection (unexpected additional properties ["collection"]), costing a whole model round trip on every scoped search before it guessed the longer spelling. It is now folded into the filters before anything downstream sees the plan. Precedence is unchanged and explicit: the request's owncollectionoutranksplan.filters.collection, which outranks the shorthand.
[2.60.0] - 2026-07-26
Fixed
A named collection no longer leaks other collections' chunks. Hybrid search runs a vector arm and a keyword arm; only the vector arm applied
opts.Collection. The keyword arm queried the shared FTS index unrestricted, so asking one collection returned rows from every other one — and with no query vector supplied (SearchTextOnly, and thereforesearch_textand every lexical-modeknowledge_search) the keyword arm is the only arm, so the collection filter had no effect at all. A tutor scoped to one textbook could be handed another book's text, or another user's, as if it came from the book asked for.An unscoped search no longer means "only the default collection". Query defaults substituted
graphrag_chunkswhenever a caller named no collection, silently narrowing a search that asked for no narrowing; anything ingested elsewhere — imported agent memory, a per-book collection — was unreachable unless the caller already knew where to look. Empty now means unrestricted, as it does on the vector arm. Ingest still defaults, since content has to land somewhere. This was masked by the leak above: the unrestricted keyword arm happened to return the rows the filter should have excluded.Two-character Chinese terms are findable again. The trigram tokenizer cannot produce a token shorter than three characters, so
MATCHreturned nothing for 乘法, 分数, 除法, 周长 and most other Chinese words a lesson is actually about, however many chunks contained them. Such queries now fall back to a substring scan (collection filter and all) instead of reporting an empty corpus.
[2.59.4] - 2026-07-25
Fixed
knowledge_saveno longer turns romanisation into graph entities. 2.59.3 guarded thegraphflowheuristic extractor, butknowledge_saveuses the GraphRAG Title Case extractor, which was untouched — so a Chinese textbook still produced entities likeentity:wngandentity:gu_drand no concepts at all. Worse, a scanned book's text layer mangles pinyin tone marks into stray capitals (hSn dAi,pT Wng,Gu Dr), which are valid Title Case, so tone marks cannot be tested for. When the text contains CJK, Latin matches must now be at least three letters per word and contain a vowel: syllable debris is dropped while genuine bilingual entities (Transformer,Chain Rule) and Latin-only text are untouched.
[2.59.3] - 2026-07-25
Fixed
The heuristic extractor no longer fills CJK graphs with romanisation. Entities were found with a capitalised-Latin-word pattern, which on Chinese, Japanese or Korean text can only match romanisation — a primary-school textbook prints pinyin above every new character, so its graph ended up holding syllable fragments (
entity:wng,entity:qn) and not one real concept. The Latin pass is now skipped for predominantly CJK documents (leaving extraction to an LLM extractor) and tone-marked syllables are rejected everywhere. Explicit backtick spans and Latin-text extraction are unchanged.
[2.59.2] - 2026-07-25
Fixed
vector_dimension_repairreconciles metadata even when there is nothing to re-embed. An early return skipped the reconciliation whenever the candidate list was empty, which is exactly the state a previous repair leaves behind: correct vectors, stale declared dimension. The drift report then kept flagging rows that were already fixed.
[2.59.1] - 2026-07-25
Fixed
Re-embedding now reconciles the collection's declared dimension. A collection records the size it was created with, so after
vector_dimension_repairrewrote the vectors the drift report kept flagging every repaired row — the numbers were right and the metadata was stale.ReconcileCollectionDimensionsupdates a collection once all of its vectors agree on one size, and the repair reports how many it reconciled.
[2.59.0] - 2026-07-25
Added
Vector-dimension drift is now visible and repairable. A store outlives the embedding model that filled it: change models and the old rows keep their old dimensionality. Those vectors cannot enter the vector index — one graph holds one dimensionality — so they quietly stop being retrievable by similarity while lexical search still finds them, which hides the loss. Nothing reported it.
SQLiteStore.DimensionReportgroups stored vectors by collection and size and counts the rows that disagree with their collection.DB.ReembedMismatchedVectorsrepairs them by embedding the stored text again with the configured embedder, in batches, with a dry run. The numbers cannot be salvaged by truncating or padding — vectors from two models occupy unrelated spaces — so re-embedding is the only honest repair.Both are exposed as the
vector_dimension_repairMCP tool, which defaults todry_run: trueso an operator can look before writing.
[2.58.1] - 2026-07-25
Fixed
A store holding more than one vector dimensionality no longer crashes the process.
CosineDistanceiterated one vector while indexing the other, so comparing a 4096-dim vector with a 768-dim one panicked withindex out of range [768] with length 768. The panic surfaced insideHNSW.Insertduringknowledge_save, killing the server mid-write — a database that had outlived an embedding-model change could not be written to at all. All three distance functions now treat mismatched or empty vectors as incomparable (maximum distance) instead of indexing out of bounds, andHNSW.Insertrejects a vector whose dimensionality differs from the graph's with a clear error. Callers already log and skip, so mixed-dimension stores stay writable. (IVF and LSH already checked dimensions; HNSW was the outlier.)
[2.58.0] - 2026-07-25
Fixed
Lexical search now works for Chinese, Japanese and Korean text. FTS5's default
unicode61tokenizer does not segment CJK: a whole run of Han characters becomes a single token, so no realistic Chinese query could ever match. Lexical search silently returned zero rows for CJK corpora while English worked fine, which also made hybrid search fall back to vectors alone and leftmemory_searchunable to find Chinese memories. Each FTS index now has a_cjkcompanion built with thetrigramtokenizer, andknowledge_search,search_text, hybrid search, message keyword search and memory recall route to it when the query contains CJK.The word indexes keep the
unicode61tokenizer. Replacing it outright was the obvious one-line fix but it degrades English relevance — trigram is a substring index, not a word index — and it reorderedTestHybridSearch. Routing by script fixes CJK without touching existing behaviour for space-separated languages.Existing databases are upgraded on open: the companion indexes are created and built once over the current corpus, recorded via
PRAGMA user_version.Known limit: the trigram tokenizer matches substrings of three characters or more, so a query of only one or two CJK characters (e.g.
函数) still matches nothing and has to rely on vector search.
Added
CORTEXDB_LEXICAL_DIMto set the lexical placeholder vector dimension when no collection dimension is configured.Adaptive embedding batches in
cortexdb-mcp-stdio:CORTEXDB_EMBED_BATCH_SIZEandCORTEXDB_EMBED_TIMEOUT_SECONDSare honoured, oversized batches are split, and chunk character limits are enforced before embedding.
[2.51.0] - 2026-07-09
🚀 Features
(graphflow) LLM-backed graph distillation for organize (v2.51.0)
[2.50.0] - 2026-07-09
🚀 Features
(mcp-stdio) Env-driven embedder for semantic retrieval (v2.50.0)
[client-v0.1.1] - 2026-07-09
⚙️ Miscellaneous Tasks
Fix crates.io version check (User-Agent) + tolerate already-published in publish-clients
(clients) Bump Rust/Python/Node clients to 0.1.1
[client-v0.1.0] - 2026-07-09
⚙️ Miscellaneous Tasks
Publish-clients workflow — auto-publish Rust/Python/Node on client-v* tags
[2.49.0] - 2026-07-09
🚀 Features
(tools) Extract_conversation tool — summary/entities/relations from a chat (v2.49.0)
[2.48.0] - 2026-07-09
🚀 Features
(graph) Degree-ranked node selection for the graph view (v2.48.0)
[2.47.0] - 2026-07-09
🚀 Features
(import) Import Claude Code / Codex memory into the brain + build graph (v2.47.0)
[2.46.0] - 2026-07-09
🚀 Features
(plugin) Sharpen SessionStart memory directive — recall/graph/save (v2.46.0)
[2.45.0] - 2026-07-05
🚀 Features
(retrieval) Hybrid RRF knowledge search, sentence-aware chunking, harder eval (v2.45.0)
[2.44.0] - 2026-07-04
🐛 Bug Fixes
(retrieval) Auto mode uses the embedder for semantic knowledge search (v2.44.0)
[2.43.0] - 2026-07-04
🚀 Features
(quality) Retrieval-eval suite + fuzz/property tests; fix FTS NUL crash (v2.43.0)
[2.42.0] - 2026-06-30
🐛 Bug Fixes
(graph) Entity-quality filter + render the real graph (v2.42.0)
[2.41.0] - 2026-06-30
🚀 Features
(graph) Organize memories into the graph; /cortexdb-graph extracts then shows (v2.41.0)
[2.40.0] - 2026-06-30
🐛 Bug Fixes
(plugin) Install is pure prebuilt-binary download, no Go ever (v2.40.0)
[2.39.0] - 2026-06-30
🚀 Features
(recall) Surface graph facts in knowledge_memory_recall (v2.39.0)
📚 Documentation
Clarify Codex plugin install
Sharpen CortexDB positioning
[2.38.0] - 2026-06-28
🚀 Features
(plugin) Proactive memory directive, /remember /recall /cortexdb-graph, graph viewer (v2.38.0)
[2.37.0] - 2026-06-28
🚀 Features
Public generic Rerank API (cross-encoder + MMR)
🐛 Bug Fixes
(ci) Sync plugin manifests to 2.37.0 (version gate)
📚 Documentation
Add a Claude Code plugin install + usage section to the READMEs
[2.36.0] - 2026-06-25
🚀 Features
(plugin) Default to a single global ~/.cortexdb/cortexdb.db (v2.36.0)
[2.35.0] - 2026-06-25
🐛 Bug Fixes
(plugin) Version-pin the MCP binary cache so upgrades auto-refresh (v2.35.0)
[2.33.1] - 2026-06-25
🐛 Bug Fixes
(search) Escape FTS5 syntax in lexical query text (v2.33.1)
[2.33.0] - 2026-06-25
🚀 Features
(search) Retrieval-layer Authorize gate, Reranker + MinScore, options-based hybrid search
📚 Documentation
Split Claude Code plugin install into separate slash commands
[2.32.0] - 2026-06-24
🚀 Features
Default DB path to .cortexdb/, auto-create parent dirs (v2.32.0)
[2.31.0] - 2026-06-24
🚀 Features
Auto-recall UserPromptSubmit hook for the plugin (v2.31.0)
[2.30.1] - 2026-06-24
🧪 Testing
(knowledge) Regression for no-embedder lexical save+search (v2.30.1)
[2.30.0] - 2026-06-24
🚀 Features
(plugin) Ship prebuilt MCP binaries, drop Go-toolchain requirement (v2.30.0)
[2.29.0] - 2026-06-24
🚀 Features
(plugin) Package CortexDB as a Claude Code & Codex plugin (v2.29.0)
[2.28.0] - 2026-06-23
🚀 Features
(graphflow) Add 3d html export view
⚙️ Miscellaneous Tasks
(release) V2.28.0
[2.27.0] - 2026-06-23
🚀 Features
Add composable cortex query api
📚 Documentation
(spec) Profileflow — LLM-maintained narrative user profile
⚙️ Miscellaneous Tasks
(release) V2.27.0
[2.26.0] - 2026-06-20
🚀 Features
Export EntityNodeID, add GraphStore.MergeEntities, ALTER TABLE FKs
[2.25.0] - 2026-06-17
🚀 Features
(importflow) ParseDDL — parse CREATE TABLE columns, primary keys, foreign keys
(importflow) MappingFromDDL — deterministic DDL -> MappingPlan (PK->id, FK->relations)
(importflow) Importflow_ddl_plan tool — DDL -> reviewable MappingPlan over MCP
(importflow) MappingFromDDLWithLLM — LLM refines deterministic DDL→KG baseline with graceful fallback
(importflow) Importflow_ddl_plan_ai MCP tool — LLM-refined DDL→KG plan with baseline alongside
🐛 Bug Fixes
(importflow) Guarantee LLM-refined DDL plans are executable
(importflow) Text-extraction triples use IRI subjects, not literals
📚 Documentation
(spec) DDL -> knowledge-graph MappingPlan (deterministic) + importflow_ddl_plan MCP tool
(plan) DDL -> knowledge-graph MappingPlan implementation plan
Importflow DDL -> knowledge-graph mapping (MappingFromDDL + importflow_ddl_plan)
(spec) LLM-enhanced DDL→KG mapping (refine-the-baseline + importflow_ddl_plan_ai)
(plan) LLM-enhanced DDL→KG mapping implementation plan
(importflow) Clarify executability guard scope and text-IRI case-folding
🎨 Styling
(importflow) Gofmt toolbox.go schema helpers
🧪 Testing
(importflow) Cover CONSTRAINT-form PRIMARY KEY / FOREIGN KEY in ParseDDL
(importflow) E2e DDL -> MappingPlan -> knowledge graph (FK becomes a relation edge)
(importflow) Skipped live DDL→KG LLM-refine test against OPENAI_*/.env model
⚙️ Miscellaneous Tasks
(release) V2.25.0 — LLM-enhanced DDL→KG mapping (importflow_ddl_plan_ai)
[2.24.1] - 2026-06-16
🚀 Features
(examples) Semantic RAG — pluggable embedding model (DashScope text-embedding-v4) for vector search by meaning + LLM answer
🐛 Bug Fixes
(examples) 05_graphflow falls back to heuristic extractor if the LLM endpoint is unreachable; README marks per-example prereqs (LLM/DB)
(examples) 08 + 12 load .env and use OPENAI_* config (08 no longer requires local Ollama; 12 honors OPENAI_MODEL)
📚 Documentation
Surface data connector + CDC and examples 09-13 in README/README_CN and landing page
⚙️ Miscellaneous Tasks
(release) V2.24.1 — examples + docs (semantic-RAG example, .env/OPENAI_* config, connector/CDC in README + landing)
[2.24.0] - 2026-06-16
🚀 Features
(connector) ChangeEvent.Position + position-based checkpoint wiring (Route B)
(connector) Postgres logical-replication change source (pgoutput, Route B)
(connector) MySQL binlog change source (ROW, Route B)
(examples) Support-agent brain — end-to-end live Postgres/MySQL → desensitize → RAG+KG → masked Q&A → un-mask → CDC sync
(examples) Optional LLM answer step for support-brain (answers over masked context only)
(examples) Unified brain — dual-source PG+MySQL, concurrent streaming CDC, free-text PII, RDFS+SPARQL+SHACL reasoning; quiet binlog logger
(examples) Incident-analysis agent (LLM-required) — LLM KG extraction + agentic tool use over CortexDB
(examples) Scale + analytics — bulk dual-source ingest with throughput, SPARQL/RDFS/SHACL analytics, CDC under load
🐛 Bug Fixes
(connector) ValueToString renders time.Time as RFC3339 so polling watermark round-trips (Postgres timestamptz)
(core) Apply SQLite pragmas via modernc _pragma() DSN syntax
📚 Documentation
(plan) Connector CDC phase 2 (Postgres logical replication) implementation plan
(connector) Document Postgres logical-replication CDC (Route B-PG)
(plan) Connector CDC phase 3 (MySQL binlog) implementation plan
(connector) MySQL binlog CDC (Route B-MySQL); CDC complete for PG + MySQL
🧪 Testing
(connector) Live PG logical-replication CDC e2e (insert/update/delete, no PII)
(connector) Live MySQL binlog CDC e2e (insert/update/delete, no PII)
⚙️ Miscellaneous Tasks
Gitignore SQLite WAL/SHM sidecars (test DBs not ending in .db)
(release) V2.24.0 — CDC complete (PG logical replication + MySQL binlog), 4 end-to-end examples, SQLite concurrency fix
[2.23.0] - 2026-06-16
🚀 Features
(connector) MCP server + tools (NewMCPServer/RunMCPStdio/RegisterMCPTools), cortexdb-connector-mcp cmd; document connector in skills/README
(connector) CDC change model (ChangeOp/ChangeEvent/ChangeSource)
(connector) Knowledge-DB-backed CDC checkpoint store
(connector) CDC apply helpers (one-row source, delete-key resolver)
(connector) CDC watcher apply loop (idempotent upsert + hard delete, key precondition)
(connector) Polling change source (Route A) + incremental sqlSource reader
(connector) CDC checkpoint seed/save for polling watcher (resume)
(connector) Compile-time assert polling source satisfies cursorCheckpointer
🚜 Refactor
(connector) Watcher uses maps.Copy + deterministic column order (review nits)
📚 Documentation
(spec) Connector CDC / near-real-time sync (polling + PG logical replication + MySQL binlog)
(plan) Connector CDC phase 1 (polling) implementation plan
(connector) Near-real-time CDC sync (polling) in README/SKILL
🧪 Testing
(connector) E2e desensitized import into knowledge graph (tokenized entity IRIs, edges preserved, no PII)
(connector) Live polling CDC e2e (upsert, no PII, resume) for Postgres + MySQL
⚙️ Miscellaneous Tasks
(deploy) Docker demo for OpenClaw + Hermes agents using CortexDB; add .dockerignore
(release) V2.23.0 — connector near-real-time CDC sync (polling) + MCP server/tools
[2.22.0] - 2026-06-16
🚀 Features
(clients) Python (cortexdb-client) + Node (@cortexdb/client) gRPC clients
(skills) Agentskills.io skills for Hermes (Python) + OpenClaw (Node) memory
(connector) Core privacy types (PiiKind/Sensitivity/MaskAction/MaskingPlan)
(connector) Masking primitives (mask/generalize/redact)
(connector) Rule-based PII classifier (name hints + value regex)
(connector) AES-GCM token vault with per-tenant key + deterministic tokens
(connector) Env + file key providers
(connector) Regex free-text PII scanner (in-place redaction)
(connector) Desensitizer + Source decorator (mask/drop/pseudonymize/text-scan)
(connector) BuildMaskingPlan (default-deny) + Unmask
(connector) LLM classifier + rule->LLM chain
(connector) Live Postgres source via information_schema
(connector) Live MySQL source via information_schema
(connector) Agent toolbox (introspect/plan/run/unmask) for importflow MCP
🐛 Bug Fixes
(connector) Vault enforces 32-byte AES-256 key (no silent downgrade)
(connector) Desensitizer fails closed on unlisted columns (default-deny gate)
🚜 Refactor
(connector) Escape identifier quotes; drop unused readRows cols param
📚 Documentation
(landing) Add self-hosted GoatCounter analytics (host-prefixed paths)
Document Python (PyPI) + Node (npm) clients — all three published as cortexdb-client
(landing+readme) Agent Skills section — install CortexDB memory for Hermes/OpenClaw from ClawHub/skills.sh/git
(landing) Add Go/Rust/Python/Node client section; remove misplaced Rust 'example' card
(spec) Data connector with privacy/desensitization — pkg/connector design
(spec) Connector rides importflow MCP; add v1 token-vault key custody for un-mask
(plan) Data connector with privacy/desensitization implementation plan
(connector) Example 09 + README/SKILL data-connector section
🧪 Testing
(connector) End-to-end desensitized import into ImportFlow RAG (no PII leak)
⚙️ Miscellaneous Tasks
(release) V2.22.0 — data connector (privacy/desensitization), Python/Node clients, agent skills
[2.21.0] - 2026-06-07
🚀 Features
(rpc) Proto contract cortexdb.v1 + generated Go stubs
(rpcserver) Scaffolding, error mapping, bearer-token auth interceptor
(rpcserver) Bufconn harness, conversion helpers, AdminService
(rpcserver) KnowledgeService
(rpcserver) MemoryService
(rpcserver) GraphRagService
(rpcserver) KnowledgeGraphService (RDF/SPARQL/SHACL/inference/ontology)
(rpcserver) ToolsService generic dispatch
(cmd) Cortexdb-grpc sidecar binary with token auth and optional OpenAI-compatible embedder
(rust) Cortexdb-client crate with typed sub-clients and bearer-token interceptor
(rust) Managed-server feature — sidecar resolve/download/spawn with auto token
(rust) E2e_ollama example — qwen3.5 extraction + embeddinggemma vector search + graph expand
(examples) 08_self_knowledge_graph — build a KG of CortexDB itself via graphflow (ollama or OpenAI-compatible extractor)
🐛 Bug Fixes
(graphflow) Decode base64 graph payload as UTF-8 in HTML viz (atob mojibake for non-ASCII labels)
📚 Documentation
(spec) GRPC sidecar server + Rust client design
(spec) Add bearer-token auth to gRPC sidecar design
(spec) Note single-node v1 and multi-node evolution path
(spec) Document multi-tenancy model — single DB per process in v1, in-file scoping
(plan) GRPC sidecar + Rust client implementation plan
GRPC sidecar + Rust client usage (README/README_CN/SKILL)
(landing) Self knowledge-graph demo section — screenshots + graph-backed Q&A
(landing) Refresh for v2.21 — gRPC/Rust feature cards, honest stats, FAQ + JSON-LD/OG/llms.txt SEO-GEO, fix ScrollToPlugin
(landing) Tighten hero subtitle
(landing) Fix footer dead links — enable Discussions, drop missing CONTRIBUTING
(pages) Add .nojekyll — Go braces in plan docs break Liquid parsing
Sync README/README_CN/SKILL/examples — example 08, crates.io install, importflow entries
🧪 Testing
(rust) Sidecar integration test + roundtrip example (verified against ollama embeddinggemma)
(examples) Knowledge-graph QA test over self_kg.db (skips when graph not generated)
⚙️ Miscellaneous Tasks
Rust client job + multi-platform sidecar release assets
(release) V2.21.0 — gRPC sidecar + Rust client (cortexdb-client on crates.io)
[2.20.3] - 2026-05-31
🐛 Bug Fixes
(graphrag) Sliding word-window chunker (merge short lines) instead of per-paragraph splitting
[2.20.2] - 2026-05-31
⚙️ Miscellaneous Tasks
Bump version to 2.20.2
[2.20.1] - 2026-05-30
🚀 Features
(importflow) Add MCP server (importflow_plan/run) and document in SKILL
📚 Documentation
(site) Add ImportFlow to landing page (feature, architecture, example)
⚙️ Miscellaneous Tasks
(release) V2.20.1 — importflow MCP server + skill docs
[2.20.0] - 2026-05-30
🚀 Features
(importflow) Add package skeleton and core types
(importflow) Add Source interface and in-memory CSVSource
(importflow) Parse INSERT statements from SQL dumps
(importflow) Parse PG COPY blocks and report unparsed statements
(importflow) Add MappingPlan types and template renderer
(importflow) Add deterministic Mapper for RAG chunks and KG triples
(importflow) Add batching RAGSink over InsertTextBatch
(importflow) Add batching KGSink over UpsertKnowledgeGraph
(importflow) Add MappingInferer and TextRefiner with JSONGenerator defaults
(importflow) Add Importer orchestration (Plan/Run/AutoImport)
(importflow) Add agent-callable Toolbox (importflow_plan/run)
(importflow) Add e2e test, runnable example, and docs
🐛 Bug Fixes
(importflow) Parse CREATE TABLE before COPY blocks and make dump escaping dialect-aware
📚 Documentation
Add importflow external-data import design spec
Add importflow implementation plan
⚙️ Miscellaneous Tasks
(release) V2.20.0 — add importflow external-data import layer
[2.19.1] - 2026-05-30
🐛 Bug Fixes
Omit empty "required" from tool JSON schemas; add KG e2e coverage
[2.19.0] - 2026-04-28
🚀 Features
Add pkg/agentmem SQL-backed agent memory layer
[2.18.0] - 2026-04-12
🚀 Features
Expand knowledge graph workflows
Align knowledge memory architecture
📚 Documentation
Align examples with architecture
[2.17.0] - 2026-04-08
🚀 Features
Add memoryflow workflow layer
[2.16.0] - 2026-04-01
🚀 Features
Add embedded RDF knowledge graph and RDFS-lite inference
[2.15.1] - 2026-03-29
🚀 Features
(brain) Add unified knowledge memory and MCP tools
[2.14.0] - 2026-03-29
📚 Documentation
Add SKILL.md for Claude Code integration
[2.13.0] - 2026-03-20
🚀 Features
Fallback to FTS5/BM25 when embedder not configured
LLM-assisted retrieval and external vector support
📚 Documentation
Unify CortexDB branding and update API references
[2.9.1] - 2026-03-06
🚀 Features
Add Content field to Document for full document storage
[2.9.0] - 2026-03-06
🐛 Bug Fixes
(hindsight) Only apply RRF fusion when multiple strategies enabled
[2.8.0] - 2026-03-05
⚡ Performance
Optimize GraphRAG startup speed with batch insert, parallel build, quantization, and auto-save
[2.7.0] - 2026-03-05
🚀 Features
Add Embedder interface for dual-mode text/vector operations
Add Info() interface to get database configuration
🐛 Bug Fixes
Complete HybridSearch implementation and update CI for v2.7.0
📚 Documentation
Improve Go code style in READMEs
(examples) Add example for new high-level text APIs
(examples) Add structured data examples (Textification, Memory Entities, GraphRAG)
Add new text and structured data APIs to READMEs
Rewrite README for CortexDB with new positioning and features
⚙️ Miscellaneous Tasks
Rename project from sqvect to cortexdb
[2.6.1] - 2026-02-20
🚀 Features
Add UpdateDocument method for metadata updates
🐛 Bug Fixes
Eliminate data race in auto-retain goroutine vs Close()
[2.6.0] - 2026-02-19
🚀 Features
Auto-retain via sys.AddMessage with sliding-window async extraction
📚 Documentation
Fix README.md duplicate content and update both READMEs for v2.5.0
🎨 Styling
Format alignment fixes in hindsight.go, sqvect.go and README table
[2.5.0] - 2026-02-19
🚜 Refactor
Merge pkg/memory into pkg/hindsight, remove redundant package
[2.4.0] - 2026-02-19
🚀 Features
Add pkg/memory with Hindsight-style retain/recall/reflect and extensibility hooks
[2.3.2] - 2026-02-05
🐛 Bug Fixes
Properly persist and load banks in CreateBank
[2.3.1] - 2026-02-04
📚 Documentation
Update documentation for v2.3.1
[2.3.0] - 2026-02-04
🚀 Features
Add semantic-router module with hybrid and function calling support
[2.2.0] - 2026-02-03
🐛 Bug Fixes
Correct module path in CI workflow
Update all paths to use /v2 module path
Remove /v2 from CI/CD badge URL
Remove /v2 from GitHub release badge URL
🚜 Refactor
Optimize graph algorithms and clarify performance claims
⚙️ Miscellaneous Tasks
Add automatic release creation on tag push
Update codecov to v5 with CODECOV_TOKEN
Simplify workflows and fix release generation
[2.1.0] - 2026-02-03
🚀 Features
Add Hindsight AI agent memory system (v2.1.0)
📚 Documentation
Add Chinese README (README_CN.md)
[2.0.1] - 2026-01-15
🚀 Features
Upgrade to v2 module path
[2.0.0] - 2026-01-14
🚜 Refactor
Complete project overhaul for v2.0.0 (file splitting, UUID, observability, and passing tests)
📚 Documentation
Comprehensive update of Go documentation comments for v2.0.0
[1.4.0] - 2026-01-14
📚 Documentation
Update examples and README with RAG features
[1.3.0] - 2026-01-14
🚀 Features
Add RAG capabilities (documents, chat, hybrid search, ACL) and update docs/examples
[1.2.0] - 2026-01-13
🚀 Features
Add advanced vector search features and fix range search
Add advanced vector search features and LLM integration
Add IVF index support, benchmarking tools, and updated docs
Integrate SQ8 quantization and optimize WAL/dimension policies
🐛 Bug Fixes
Remove docs deployment from release workflow
Resolve all golangci-lint issues
[1.0.0] - 2025-09-09
🚀 Features
Enhance document retrieval and listing capabilities
Implement HNSW (Hierarchical Navigable Small World) index
Implement core functionality improvements
Replace CGO SQLite with pure Go implementation
Implement core functionality improvements
Add similarity scoring improvements
Implement core functionality improvements
🐛 Bug Fixes
Resolve search filtering errors with metadata queries
Resolve all golangci-lint issues and add GitHub Pages deployment
🚜 Refactor
Restructure codebase with modular architecture and advanced features
⚡ Performance
Optimize pinyin processing performance
Optimize pinyin processing performance
🧪 Testing
Add comprehensive test coverage for document listing
[0.2.0] - 2025-08-07
🚀 Features
List all docs
[0.1.0] - 2025-08-07
💼 Other
V0.1.0
<!-- generated by git-cliff -->
512 files · 501 not served here
- .agents/plugins/marketplace.json (not served here)
- .agents/product-marketing.md (not served here)
- .agents/skills/cortexdb/SKILL.md (not served here)
- .claude-plugin/marketplace.json (not served here)
- .dockerignore (not served here)
- .github/ISSUE_TEMPLATE/bug_report.md (not served here)
- .github/ISSUE_TEMPLATE/feature_request.md (not served here)
- .github/pull_request_template.md (not served here)
- .github/workflows/ci.yml (not served here)
- .github/workflows/pages.yml (not served here)
- .github/workflows/publish-clients.yml (not served here)
- .gitignore (not served here)
- ATTRIBUTION.md
- AUTOGINEER-RUNS.md
- CHANGELOG.md
- CONTRIBUTING.md
- LICENSE
- README.md
- README_CN.md
- SKILL.md (not served here)
- clients/node/.gitignore (not served here)
- clients/node/README.md (not served here)
- clients/node/examples/agent_memory.js (not served here)
- clients/node/package-lock.json (not served here)
- clients/node/package.json (not served here)
- clients/node/proto/cortexdb/v1/admin.proto (not served here)
- clients/node/proto/cortexdb/v1/common.proto (not served here)
- clients/node/proto/cortexdb/v1/contract.proto (not served here)
- clients/node/proto/cortexdb/v1/decision.proto (not served here)
- clients/node/proto/cortexdb/v1/graph.proto (not served here)
- clients/node/proto/cortexdb/v1/graphrag.proto (not served here)
- clients/node/proto/cortexdb/v1/knowledge.proto (not served here)
- clients/node/proto/cortexdb/v1/memory.proto (not served here)
- clients/node/proto/cortexdb/v1/tools.proto (not served here)
- clients/node/src/index.d.ts (not served here)
- clients/node/src/index.js (not served here)
- clients/node/test/integration.js (not served here)
- clients/python/.gitignore (not served here)
- clients/python/MANIFEST.in (not served here)
- clients/python/README.md (not served here)
- clients/python/cortexdb_client/__init__.py (not served here)
- clients/python/cortexdb_client/_pb/__init__.py (not served here)
- clients/python/cortexdb_client/_pb/cortexdb/__init__.py (not served here)
- clients/python/cortexdb_client/_pb/cortexdb/v1/__init__.py (not served here)
- clients/python/cortexdb_client/_pb/cortexdb/v1/admin_pb2.py (not served here)
- clients/python/cortexdb_client/_pb/cortexdb/v1/admin_pb2_grpc.py (not served here)
- clients/python/cortexdb_client/_pb/cortexdb/v1/common_pb2.py (not served here)
- clients/python/cortexdb_client/_pb/cortexdb/v1/common_pb2_grpc.py (not served here)
- clients/python/cortexdb_client/_pb/cortexdb/v1/contract_pb2.py (not served here)
- clients/python/cortexdb_client/_pb/cortexdb/v1/contract_pb2_grpc.py (not served here)
- clients/python/cortexdb_client/_pb/cortexdb/v1/decision_pb2.py (not served here)
- clients/python/cortexdb_client/_pb/cortexdb/v1/decision_pb2_grpc.py (not served here)
- clients/python/cortexdb_client/_pb/cortexdb/v1/graph_pb2.py (not served here)
- clients/python/cortexdb_client/_pb/cortexdb/v1/graph_pb2_grpc.py (not served here)
- clients/python/cortexdb_client/_pb/cortexdb/v1/graphrag_pb2.py (not served here)
- clients/python/cortexdb_client/_pb/cortexdb/v1/graphrag_pb2_grpc.py (not served here)
- clients/python/cortexdb_client/_pb/cortexdb/v1/knowledge_pb2.py (not served here)
- clients/python/cortexdb_client/_pb/cortexdb/v1/knowledge_pb2_grpc.py (not served here)
- clients/python/cortexdb_client/_pb/cortexdb/v1/memory_pb2.py (not served here)
- clients/python/cortexdb_client/_pb/cortexdb/v1/memory_pb2_grpc.py (not served here)
- clients/python/cortexdb_client/_pb/cortexdb/v1/tools_pb2.py (not served here)
- clients/python/cortexdb_client/_pb/cortexdb/v1/tools_pb2_grpc.py (not served here)
- clients/python/cortexdb_client/client.py (not served here)
- clients/python/cortexdb_client/proto.py (not served here)
- clients/python/examples/agent_memory.py (not served here)
- clients/python/gen.sh (not served here)
- clients/python/pyproject.toml (not served here)
- clients/python/tests/test_integration.py (not served here)
- clients/python/uv.lock (not served here)
- clients/rust/.gitignore (not served here)
- clients/rust/Cargo.lock (not served here)
- clients/rust/Cargo.toml (not served here)
- clients/rust/cortexdb-client/Cargo.toml (not served here)
- clients/rust/cortexdb-client/README.md (not served here)
- clients/rust/cortexdb-client/examples/e2e_ollama.rs (not served here)
- clients/rust/cortexdb-client/examples/roundtrip.rs (not served here)
- clients/rust/cortexdb-client/proto/cortexdb/v1/admin.proto (not served here)
- clients/rust/cortexdb-client/proto/cortexdb/v1/common.proto (not served here)
- clients/rust/cortexdb-client/proto/cortexdb/v1/contract.proto (not served here)
- clients/rust/cortexdb-client/proto/cortexdb/v1/decision.proto (not served here)
- clients/rust/cortexdb-client/proto/cortexdb/v1/graph.proto (not served here)
- clients/rust/cortexdb-client/proto/cortexdb/v1/graphrag.proto (not served here)
- clients/rust/cortexdb-client/proto/cortexdb/v1/knowledge.proto (not served here)
- clients/rust/cortexdb-client/proto/cortexdb/v1/memory.proto (not served here)
- clients/rust/cortexdb-client/proto/cortexdb/v1/tools.proto (not served here)
- clients/rust/cortexdb-client/src/client.rs (not served here)
- clients/rust/cortexdb-client/src/gen/cortexdb.v1.rs (not served here)
- clients/rust/cortexdb-client/src/lib.rs (not served here)
- clients/rust/cortexdb-client/src/sidecar.rs (not served here)
- clients/rust/cortexdb-client/tests/integration.rs (not served here)
- clients/rust/gen/Cargo.toml (not served here)
- clients/rust/gen/src/main.rs (not served here)
- cmd/cortexdb-bench/embedder.go (not served here)
- cmd/cortexdb-bench/fetch.go (not served here)
- cmd/cortexdb-bench/main.go (not served here)
- cmd/cortexdb-bench/main_test.go (not served here)
- cmd/cortexdb-bench/retrieve.go (not served here)
- cmd/cortexdb-connector-mcp/main.go (not served here)
- cmd/cortexdb-grpc/embedder.go (not served here)
- cmd/cortexdb-grpc/embedder_test.go (not served here)
- cmd/cortexdb-grpc/embedder_watch.go (not served here)
- cmd/cortexdb-grpc/embedder_watch_test.go (not served here)
- cmd/cortexdb-grpc/health.go (not served here)
- cmd/cortexdb-grpc/health_test.go (not served here)
- cmd/cortexdb-grpc/main.go (not served here)
- cmd/cortexdb-grpc/review_gate.go (not served here)
- cmd/cortexdb-grpc/review_gate_test.go (not served here)
- cmd/cortexdb-grpc/vector_index.go (not served here)
- cmd/cortexdb-grpc/vector_index_test.go (not served here)
- cmd/cortexdb-grpc/version.go (not served here)
- cmd/cortexdb-grpc/version_test.go (not served here)
- cmd/cortexdb-mcp-stdio/capture_grade_test.go (not served here)
- cmd/cortexdb-mcp-stdio/capture_ontology_test.go (not served here)
- cmd/cortexdb-mcp-stdio/capture_session.go (not served here)
- cmd/cortexdb-mcp-stdio/capture_session_test.go (not served here)
- cmd/cortexdb-mcp-stdio/capture_supersede.go (not served here)
- cmd/cortexdb-mcp-stdio/dedup.go (not served here)
- cmd/cortexdb-mcp-stdio/doctor.go (not served here)
- cmd/cortexdb-mcp-stdio/doctor_test.go (not served here)
- cmd/cortexdb-mcp-stdio/dsh_sessions.go (not served here)
- cmd/cortexdb-mcp-stdio/dsh_sessions_test.go (not served here)
- cmd/cortexdb-mcp-stdio/embedder.go (not served here)
- cmd/cortexdb-mcp-stdio/export_memory.go (not served here)
- cmd/cortexdb-mcp-stdio/export_memory_test.go (not served here)
- cmd/cortexdb-mcp-stdio/export_yaml_test.go (not served here)
- cmd/cortexdb-mcp-stdio/facts_asof.go (not served here)
- cmd/cortexdb-mcp-stdio/file_context.go (not served here)
- cmd/cortexdb-mcp-stdio/file_memory.go (not served here)
- cmd/cortexdb-mcp-stdio/file_memory_test.go (not served here)
- cmd/cortexdb-mcp-stdio/files.go (not served here)
- cmd/cortexdb-mcp-stdio/files_test.go (not served here)
- cmd/cortexdb-mcp-stdio/global_search.go (not served here)
- cmd/cortexdb-mcp-stdio/graph.go (not served here)
- cmd/cortexdb-mcp-stdio/graph_cleanup.go (not served here)
- cmd/cortexdb-mcp-stdio/graph_html_tool.go (not served here)
- cmd/cortexdb-mcp-stdio/graph_live_activity.go (not served here)
- cmd/cortexdb-mcp-stdio/graph_live_cli.go (not served here)
- cmd/cortexdb-mcp-stdio/graph_live_tool.go (not served here)
- cmd/cortexdb-mcp-stdio/graph_update.go (not served here)
- cmd/cortexdb-mcp-stdio/hook_budget.go (not served here)
- cmd/cortexdb-mcp-stdio/import.go (not served here)
- cmd/cortexdb-mcp-stdio/import_agent.go (not served here)
- cmd/cortexdb-mcp-stdio/import_agent_test.go (not served here)
- cmd/cortexdb-mcp-stdio/import_code.go (not served here)
- cmd/cortexdb-mcp-stdio/learning.go (not served here)
- cmd/cortexdb-mcp-stdio/llm.go (not served here)
- cmd/cortexdb-mcp-stdio/main.go (not served here)
- cmd/cortexdb-mcp-stdio/memory_html.go (not served here)
- cmd/cortexdb-mcp-stdio/memory_usage.go (not served here)
- cmd/cortexdb-mcp-stdio/multi_hop.go (not served here)
- cmd/cortexdb-mcp-stdio/query_transform.go (not served here)
- cmd/cortexdb-mcp-stdio/recall.go (not served here)
- cmd/cortexdb-mcp-stdio/recall_eval.go (not served here)
- cmd/cortexdb-mcp-stdio/recall_remote.go (not served here)
- cmd/cortexdb-mcp-stdio/recall_remote_test.go (not served here)
- cmd/cortexdb-mcp-stdio/recall_situation.go (not served here)
- cmd/cortexdb-mcp-stdio/recall_situation_test.go (not served here)
- cmd/cortexdb-mcp-stdio/reembed_memories.go (not served here)
- cmd/cortexdb-mcp-stdio/remote.go (not served here)
- cmd/cortexdb-mcp-stdio/remote_bulk.go (not served here)
- cmd/cortexdb-mcp-stdio/remote_bulk_test.go (not served here)
- cmd/cortexdb-mcp-stdio/remote_test.go (not served here)
- cmd/cortexdb-mcp-stdio/remote_trace_test.go (not served here)
- cmd/cortexdb-mcp-stdio/reranker.go (not served here)
- cmd/cortexdb-mcp-stdio/reranker_test.go (not served here)
- cmd/cortexdb-mcp-stdio/resolve_entities.go (not served here)
- cmd/cortexdb-mcp-stdio/review_gate_test.go (not served here)
- cmd/cortexdb-mcp-stdio/scrub.go (not served here)
- cmd/cortexdb-mcp-stdio/scrub_test.go (not served here)
- cmd/cortexdb-mcp-stdio/session_context.go (not served here)
- cmd/cortexdb-mcp-stdio/session_context_test.go (not served here)
- cmd/cortexdb-mcp-stdio/side_graph.go (not served here)
- cmd/cortexdb-mcp-stdio/side_graph_test.go (not served here)
- cmd/cortexdb-mcp-stdio/side_graph_tool.go (not served here)
- cmd/cortexdb-mcp-stdio/sweep_expired.go (not served here)
- cmd/cortexdb-mcp-stdio/sweep_expired_test.go (not served here)
- cmd/cortexdb-mcp-stdio/sync_memory.go (not served here)
- deploy/README.md (not served here)
- deploy/agents-demo/Dockerfile.hermes (not served here)
- deploy/agents-demo/USAGE_RECORD.md (not served here)
- deploy/agents-demo/docker-compose.yml (not served here)
- deploy/agents-demo/entrypoint.hermes.sh (not served here)
- deploy/docker/.env.example (not served here)
- deploy/docker/Dockerfile (not served here)
- deploy/docker/docker-compose.yml (not served here)
- deploy/systemd/cortexdb-grpc.env.example (not served here)
- deploy/systemd/cortexdb-grpc.service (not served here)
- doc.go (not served here)
- docs/.nojekyll (not served here)
- docs/404.html (not served here)
- docs/GUIDE.md (not served here)
- docs/GUIDE_CN.md (not served here)
- docs/LAUNCH_KIT.md
- docs/PROJECT_OVERVIEW.md
- docs/assets/cortexdb-mobile-poster-qr-redesign.png (not served here)
- docs/assets/cortexdb-mobile-poster-with-qr.png (not served here)
- docs/assets/cortexdb-mobile-poster.png (not served here)
- docs/assets/fonts/instrument-sans-latin.woff2 (not served here)
- docs/assets/fonts/jetbrains-mono-latin.woff2 (not served here)
- docs/assets/img/apple-touch-icon.png (not served here)
- docs/assets/img/icon-512.png (not served here)
- docs/assets/img/live-view-dark-1600.webp (not served here)
- docs/assets/img/live-view-dark-800.webp (not served here)
- docs/assets/img/live-view-en.gif (not served here)
- docs/assets/img/live-view-light-1600.webp (not served here)
- docs/assets/img/live-view-light-800.webp (not served here)
- docs/assets/img/live-view-phone-390.webp (not served here)
- docs/assets/img/live-view-phone-780.webp (not served here)
- docs/assets/img/live-view-zh.gif (not served here)
- docs/assets/img/og-zh.png (not served here)
- docs/assets/img/og.png (not served here)
- docs/assets/selfkg-overview.png (not served here)
- docs/assets/selfkg-zoom.png (not served here)
- docs/assets/site.css (not served here)
- docs/assets/site.js (not served here)
- docs/favicon.svg (not served here)
- docs/index.html (not served here)
- docs/llms-full.txt (not served here)
- docs/llms.txt (not served here)
- docs/robots.txt (not served here)
- docs/site.webmanifest (not served here)
- docs/sitemap.xml (not served here)
- docs/superpowers/plans/2026-05-30-importflow.md (not served here)
- docs/superpowers/plans/2026-06-07-grpc-sidecar-rust-client.md (not served here)
- docs/superpowers/plans/2026-06-12-data-connector-privacy.md (not served here)
- docs/superpowers/plans/2026-06-16-connector-cdc-phase1-polling.md (not served here)
- docs/superpowers/plans/2026-06-16-connector-cdc-phase2-pg-logical.md (not served here)
- docs/superpowers/plans/2026-06-16-connector-cdc-phase3-mysql-binlog.md (not served here)
- docs/superpowers/plans/2026-06-17-ddl-to-kg-llm.md (not served here)
- docs/superpowers/plans/2026-06-17-ddl-to-kg-mapping.md (not served here)
- docs/superpowers/plans/2026-08-10-palantir-ontology.md (not served here)
- docs/superpowers/plans/2026-09-12-listing-cursors.md (not served here)
- docs/superpowers/specs/2026-05-30-importflow-design.md (not served here)
- docs/superpowers/specs/2026-06-07-grpc-sidecar-rust-client-design.md (not served here)
- docs/superpowers/specs/2026-06-12-data-connector-privacy-design.md (not served here)
- docs/superpowers/specs/2026-06-16-connector-cdc-design.md (not served here)
- docs/superpowers/specs/2026-06-17-ddl-to-kg-llm-design.md (not served here)
- docs/superpowers/specs/2026-06-17-ddl-to-kg-mapping-design.md (not served here)
- docs/superpowers/specs/2026-06-23-profileflow-design.md (not served here)
- docs/superpowers/specs/2026-09-05-knowledge-contract-design.md (not served here)
- docs/superpowers/specs/2026-09-12-cortexdb-cli-design.md (not served here)
- docs/zh/index.html (not served here)
- examples/01_core/main.go (not served here)
- examples/02_rag/main.go (not served here)
- examples/03_memoryflow/main.go (not served here)
- examples/04_knowledge_graph/main.go (not served here)
- examples/05_graphflow/main.go (not served here)
- examples/05_graphflow/openai.go (not served here)
- examples/06_tools_mcp/main.go (not served here)
- examples/07_importflow/main.go (not served here)
- examples/08_self_knowledge_graph/.gitignore (not served here)
- examples/08_self_knowledge_graph/main.go (not served here)
- examples/08_self_knowledge_graph/qa_test.go (not served here)
- examples/09_connector/main.go (not served here)
- examples/10_support_brain/README.md (not served here)
- examples/10_support_brain/llm.go (not served here)
- examples/10_support_brain/main.go (not served here)
- examples/11_unified_brain/README.md (not served here)
- examples/11_unified_brain/main.go (not served here)
- examples/12_incident_agent/README.md (not served here)
- examples/12_incident_agent/main.go (not served here)
- examples/12_incident_agent/openai.go (not served here)
- examples/13_scale_analytics/README.md (not served here)
- examples/13_scale_analytics/main.go (not served here)
- examples/14_semantic_rag/README.md (not served here)
- examples/14_semantic_rag/embedder.go (not served here)
- examples/14_semantic_rag/main.go (not served here)
- examples/15_cortex_query/main.go (not served here)
- examples/16_ontology/main.go (not served here)
- examples/17_query_source/main.go (not served here)
- examples/17_query_source/meilisearch.go (not served here)
- examples/18_vector_graph_ontology/README.md (not served here)
- examples/18_vector_graph_ontology/embedder.go (not served here)
- examples/18_vector_graph_ontology/estate.go (not served here)
- examples/18_vector_graph_ontology/main.go (not served here)
- examples/18_vector_graph_ontology/schema.go (not served here)
- examples/19_execution_graph/bench.go (not served here)
- examples/19_execution_graph/main.go (not served here)
- examples/19_execution_graph/main_test.go (not served here)
- examples/20_media/main.go (not served here)
- examples/21_history/main.go (not served here)
- examples/README.md (not served here)
- examples/kg_e2e/main.go (not served here)
- glama.json (not served here)
- go.mod (not served here)
- go.sum (not served here)
- internal/encoding/utils.go (not served here)
- internal/encoding/vector_test.go (not served here)
- internal/pgscan/pgscan.go (not served here)
- internal/pgscan/pgscan_test.go (not served here)
- internal/pgtest/pgtest.go (not served here)
- internal/pgtest/pgtest_test.go (not served here)
- internal/testname/testname.go (not served here)
- internal/testname/testname_test.go (not served here)
- pkg/agentmem/agentmem_test.go (not served here)
- pkg/agentmem/backend_parity_test.go (not served here)
- pkg/agentmem/bank.go (not served here)
- pkg/agentmem/context.go (not served here)
- pkg/agentmem/context_append_test.go (not served here)
- pkg/agentmem/crud.go (not served here)
- pkg/agentmem/doc.go (not served here)
- pkg/agentmem/hindsight.go (not served here)
- pkg/agentmem/ranking_test.go (not served here)
- pkg/agentmem/reflect.go (not served here)
- pkg/agentmem/reflect_scope_test.go (not served here)
- pkg/agentmem/schema.go (not served here)
- pkg/agentmem/search.go (not served here)
- pkg/agentmem/sql_exec.go (not served here)
- pkg/agentmem/store.go (not served here)
- pkg/agentmem/types.go (not served here)
- pkg/authz/allowlist.go (not served here)
- pkg/authz/allowlist_graph_test.go (not served here)
- pkg/authz/allowlist_test.go (not served here)
- pkg/authz/ambiguous.go (not served here)
- pkg/authz/ambiguous_test.go (not served here)
- pkg/authz/authz.go (not served here)
- pkg/authz/authz_test.go (not served here)
- pkg/authz/caller.go (not served here)
- pkg/authz/check_sources_confined_test.go (not served here)
- pkg/authz/http.go (not served here)
- pkg/authz/key_file_trailing_test.go (not served here)
- pkg/authz/key_source.go (not served here)
- pkg/authz/key_source_test.go (not served here)
- pkg/authz/knowledge_get_list_test.go (not served here)
- pkg/authz/methods.go (not served here)
- pkg/authz/row_ids_test.go (not served here)
- pkg/authz/rows.go (not served here)
- pkg/authz/rows_test.go (not served here)
- pkg/authz/tools.go (not served here)
- pkg/authz/tools_test.go (not served here)
- pkg/connector/apply.go (not served here)
- pkg/connector/apply_test.go (not served here)
- pkg/connector/cdc.go (not served here)
- pkg/connector/cdc_e2e_test.go (not served here)
- pkg/connector/cdc_mysql.go (not served here)
- pkg/connector/cdc_mysql_e2e_test.go (not served here)
- pkg/connector/cdc_mysql_test.go (not served here)
- pkg/connector/cdc_pg.go (not served here)
- pkg/connector/cdc_pg_e2e_test.go (not served here)
- pkg/connector/cdc_pg_test.go (not served here)
- pkg/connector/cdc_polling.go (not served here)
- pkg/connector/cdc_polling_test.go (not served here)
- pkg/connector/cdc_test.go (not served here)
- pkg/connector/checkpoint.go (not served here)
- pkg/connector/checkpoint_test.go (not served here)
- pkg/connector/classify.go (not served here)
- pkg/connector/classify_llm.go (not served here)
- pkg/connector/classify_llm_test.go (not served here)
- pkg/connector/classify_test.go (not served here)
- pkg/connector/desensitize.go (not served here)
- pkg/connector/desensitize_nulls_test.go (not served here)
- pkg/connector/desensitize_test.go (not served here)
- pkg/connector/e2e_test.go (not served here)
- pkg/connector/keyprovider.go (not served here)
- pkg/connector/keyprovider_test.go (not served here)
- pkg/connector/mask.go (not served here)
- pkg/connector/mask_email_test.go (not served here)
- pkg/connector/mask_test.go (not served here)
- pkg/connector/mcp.go (not served here)
- pkg/connector/mcp_test.go (not served here)
- pkg/connector/plan.go (not served here)
- pkg/connector/plan_test.go (not served here)
- pkg/connector/source_mysql.go (not served here)
- pkg/connector/source_mysql_test.go (not served here)
- pkg/connector/source_postgres.go (not served here)
- pkg/connector/source_postgres_test.go (not served here)
- pkg/connector/source_value_test.go (not served here)
- pkg/connector/textscan.go (not served here)
- pkg/connector/textscan_test.go (not served here)
- pkg/connector/toolbox.go (not served here)
- pkg/connector/toolbox_test.go (not served here)
- pkg/connector/types.go (not served here)
- pkg/connector/types_test.go (not served here)
- pkg/connector/vault.go (not served here)
- pkg/connector/vault_test.go (not served here)
- pkg/connector/watcher.go (not served here)
- pkg/connector/watcher_position_test.go (not served here)
- pkg/connector/watcher_resume_test.go (not served here)
- pkg/connector/watcher_test.go (not served here)
- pkg/core/advanced_filter.go (not served here)
- pkg/core/advanced_search.go (not served here)
- pkg/core/advanced_search_collection_test.go (not served here)
- pkg/core/aggregate_exclude_parity_test.go (not served here)
- pkg/core/aggregate_only_parity_test.go (not served here)
- pkg/core/aggregation_identifier_test.go (not served here)
- pkg/core/aggregations.go (not served here)
- pkg/core/aggregations_test.go (not served here)
- pkg/core/batch_delete_test.go (not served here)
- pkg/core/brain_store.go (not served here)
- pkg/core/chat.go (not served here)
- pkg/core/chat_vector_cache.go (not served here)
- pkg/core/chat_vector_cache_test.go (not served here)
- pkg/core/collections.go (not served here)
- pkg/core/collections_test.go (not served here)
- pkg/core/csv_flush_error_test.go (not served here)
- pkg/core/dimension.go (not served here)
- pkg/core/dimension_report.go (not served here)
- pkg/core/dimension_test.go (not served here)
- pkg/core/doc.go (not served here)
- pkg/core/document.go (not served here)
- pkg/core/embedding.go (not served here)
- pkg/core/embedding_upsert_fts_test.go (not served here)
- pkg/core/errors.go (not served here)
- pkg/core/faceted_search.go (not served here)
- pkg/core/fts_cjk.go (not served here)
- pkg/core/fts_cjk_test.go (not served here)
- pkg/core/fts_match_test.go (not served here)
- pkg/core/fts_postgres.go (not served here)
- pkg/core/fts_postgres_test.go (not served here)
- pkg/core/fts_translation_test.go (not served here)
- pkg/core/hnsw_simple_test.go (not served here)
- pkg/core/hnsw_test.go (not served here)
- pkg/core/hnsw_topk_test.go (not served here)
- pkg/core/hybrid_failure_test.go (not served here)
- pkg/core/index_sync.go (not served here)
- pkg/core/io.go (not served here)
- pkg/core/io_test.go (not served here)
- pkg/core/logger.go (not served here)
- pkg/core/multi_vector.go (not served here)
- pkg/core/parity_aggregate_test.go (not served here)
- pkg/core/parity_range_test.go (not served here)
- pkg/core/pg_stale_type.go (not served here)
- pkg/core/pg_stale_type_test.go (not served here)
- pkg/core/quantization_test.go (not served here)
- pkg/core/rag_test.go (not served here)
- pkg/core/range_search_test.go (not served here)
- pkg/core/registry.go (not served here)
- pkg/core/registry_test.go (not served here)
- pkg/core/reranker.go (not served here)
- pkg/core/reranker_test.go (not served here)
- pkg/core/resources_test.go (not served here)
- pkg/core/search_filter_test.go (not served here)
- pkg/core/similarity.go (not served here)
- pkg/core/snapshot_chunks.go (not served here)
- pkg/core/snapshot_quantizer_test.go (not served here)
- pkg/core/snapshot_reconcile.go (not served here)
- pkg/core/snapshot_reconcile_test.go (not served here)
- pkg/core/sqlite_allocator.go (not served here)
- pkg/core/sqlite_writelock.go (not served here)
- pkg/core/sqlite_writelock_test.go (not served here)
- pkg/core/store.go (not served here)
- pkg/core/store_binary.go (not served here)
- pkg/core/store_binary_test.go (not served here)
- pkg/core/store_checkpoint.go (not served here)
- pkg/core/store_checkpoint_test.go (not served here)
- pkg/core/store_close.go (not served here)
- pkg/core/store_crud.go (not served here)
- pkg/core/store_index.go (not served here)
- pkg/core/store_init.go (not served here)
- pkg/core/store_parity_reads_test.go (not served here)
- pkg/core/store_parity_test.go (not served here)
- pkg/core/store_postgres.go (not served here)
- pkg/core/store_postgres_aggregate.go (not served here)
- pkg/core/store_postgres_aggregate_where_test.go (not served here)
- pkg/core/store_postgres_assert.go (not served here)
- pkg/core/store_postgres_binary.go (not served here)
- pkg/core/store_postgres_brain_assert.go (not served here)
- pkg/core/store_postgres_chat.go (not served here)
- pkg/core/store_postgres_chat_test.go (not served here)
- pkg/core/store_postgres_collections.go (not served here)
- pkg/core/store_postgres_dims.go (not served here)
- pkg/core/store_postgres_dims_test.go (not served here)
- pkg/core/store_postgres_faceted.go (not served here)
- pkg/core/store_postgres_nearest.go (not served here)
- pkg/core/store_postgres_nearest_test.go (not served here)
- pkg/core/store_postgres_read.go (not served here)
- pkg/core/store_postgres_read_test.go (not served here)
- pkg/core/store_postgres_search.go (not served here)
- pkg/core/store_postgres_search_test.go (not served here)
- pkg/core/store_query.go (not served here)
- pkg/core/store_search.go (not served here)
- pkg/core/store_test.go (not served here)
- pkg/core/store_tx.go (not served here)
- pkg/core/streaming.go (not served here)
- pkg/core/streaming_test.go (not served here)
- pkg/core/text_similarity.go (not served here)
- pkg/core/vector_aggregate.go (not served here)
- pkg/core/vector_aggregate_postgres.go (not served here)
- pkg/core/vector_aggregate_test.go (not served here)
- pkg/cortexdb/aggregate.go (not served here)
- pkg/cortexdb/aggregate_test.go (not served here)
- pkg/cortexdb/backup.go (not served here)
- pkg/cortexdb/backup_test.go (not served here)
- pkg/cortexdb/brain.go (not served here)
- pkg/cortexdb/brain_graph_facts.go (not served here)
- pkg/cortexdb/brain_graph_facts_test.go (not served here)
- pkg/cortexdb/brain_index.go (not served here)
- pkg/cortexdb/brain_index_test.go (not served here)
- pkg/cortexdb/brain_on_postgres_test.go (not served here)
- pkg/cortexdb/brain_test.go (not served here)
- pkg/cortexdb/brain_tool_api.go (not served here)
- pkg/cortexdb/brain_tooldefs.go (not served here)
- pkg/cortexdb/brain_types.go (not served here)
- pkg/cortexdb/caller_scope_memory_tools_test.go (not served here)
- pkg/cortexdb/changes.go (not served here)
- pkg/cortexdb/changes_explain_test.go (not served here)
- pkg/cortexdb/changes_mcp_schema_test.go (not served here)
- pkg/cortexdb/changes_overhead_test.go (not served here)
- pkg/cortexdb/changes_test.go (not served here)
- pkg/cortexdb/check_sources.go (not served here)
- pkg/cortexdb/check_sources_test.go (not served here)
- pkg/cortexdb/chunk_window.go (not served here)
- pkg/cortexdb/chunk_window_test.go (not served here)
- pkg/cortexdb/column_rename_test.go (not served here)
- pkg/cortexdb/contract.go (not served here)
- pkg/cortexdb/contract_query.go (not served here)
- pkg/cortexdb/contract_query_test.go (not served here)
- pkg/cortexdb/contract_test.go (not served here)
- pkg/cortexdb/contract_tooldefs.go (not served here)
- pkg/cortexdb/cortexdb.go (not served here)
- pkg/graph/testdata/shacl/LICENSE.md
- pkg/graph/testdata/w3c/LICENSE.md
Advantages
- Pure Go implementation requires no external service to run. evidence: README.md
- Works without an embedding model. evidence: README.md
Limitations
- Requires specific permissions for automatic recall hooks in Claude Code. evidence: README.md
Security
Risk level: Medium risk
This brain runs
Agent instructions that run or fetch something (86)
.agents/skills/cortexdb/SKILL.md:Run with `GOMEMLIMIT` set below the device's memory so the garbage collector works before the kernel does..agents/skills/cortexdb/SKILL.md:The live view (`serve_graph_3d`) can be asked, not only looked at: find any node in the store, ask a question and see what the answer names, run read-only Cypher, expand a node's neighbours past the drawn core..agents/skills/cortexdb/SKILL.md:From a shell: `cortexdb-mcp --import-agent-memory [--sessions] [--since 30d] [--max N] [--dry-run]`..agents/skills/cortexdb/SKILL.md:re-run the rules after a manual full refresh, which clears them..agents/skills/cortexdb/SKILL.md:Results are explainable inferred triples named `shacl_triple_rule:*`, and the shapes passed are the whole rule set: a rule left out is retracted on the next run..agents/skills/cortexdb/SKILL.md:An agent's record of its own run, as graph records: `StartRun`, `BeginStep` / `EndStep` (a step is written as running, with `TRIGGERED` edges from the steps it consumed, before its work runs), `RecordStep`, `FinishRun`;.agents/skills/cortexdb/SKILL.md:read back with `GetRun`, `ListRuns`, `RunSteps`, `SummarizeRun`, `StepLineage` and `ReplayRun` (the run as of any instant)..agents/skills/cortexdb/SKILL.md:go run ./examples/01_core.agents/skills/cortexdb/SKILL.md:go run ./examples/02_rag.agents/skills/cortexdb/SKILL.md:go run ./examples/03_memoryflow.agents/skills/cortexdb/SKILL.md:go run ./examples/04_knowledge_graph.agents/skills/cortexdb/SKILL.md:go run ./examples/05_graphflow.agents/skills/cortexdb/SKILL.md:go run ./examples/06_tools_mcp.agents/skills/cortexdb/SKILL.md:When changing CortexDB, run:SKILL.md:| Scenario | Start with | What it really does | | --- | --- | --- | | Store vectors, FTS5, or run simple TopK retrieval | `pkg/cortexdb` `Quick()`;SKILL.md:| | Import external CSV or MySQL/PG SQL dumps into RAG + knowledge graph | `pkg/importflow` `New().Run()` / `AutoImport()` | Parses a source into records, routes columns via a `MappingPlan` to RAG content and KG entities/relations, with optional AI mapping inference and triple extraction.SKILL.md:Codebase knowledge graph: `graphflow.Pipeline.Run`SKILL.md:Run with `GOMEMLIMIT` set below the device's memory so the garbage collector works before the kernel does.SKILL.md:`PostgresStore` satisfies the same `BrainStore` contract as `SQLiteStore`: vectors, hybrid search, memory, RDF graph, SPARQL, ontology and the tool surface all run on either.SKILL.md:without it the run skips them, naming what is uncovered (`pkg/connector`'s source/CDC tests take their own `CONNECTOR_PG_DSN` / `CONNECTOR_PG_REPL_DSN` / `CONNECTOR_MYSQL_*`).SKILL.md:past it the document is left out of every search, chunk tool, graph walk and aggregate, and `sweep_expired` / `cortexdb-mcp-stdio --sweep-expired [--dry-run]` deletes expired documents and memories.SKILL.md:re-run the rules after a manual full refresh, which clears them.SKILL.md:Results are explainable inferred triples named `shacl_triple_rule:*`, and the shapes passed are the whole rule set: a rule left out is retracted on the next run.SKILL.md:// Which nodes those edges run through — what is wrong.SKILL.md:An agent's own record of a task: one `AgentRun` node per run and one node per step — `LLMCall`, `ToolCall`, `Retrieval`, `DecisionPoint`, `Validation` or a kind of your own, which is also the node type and Cypher label — joined by `HAS_STEP` (run → step), `TRIGGERED` (a step → a step th …[+67 chars]SKILL.md:run, _ := db.StartRun(ctx, cortexdb.RunStart{Task: "checkout p99 alert", Agent: "devops-agent"})SKILL.md:plan, _ := db.RecordStep(ctx, cortexdb.StepStart{RunID: run.ID, Kind: cortexdb.StepKindLLMCall, Name: "plan"},SKILL.md:st, _ := db.BeginStep(ctx, cortexdb.StepStart{RunID: run.ID, Kind: cortexdb.StepKindToolCall, Name: "restart_pool", Parents: []string{plan.ID}})SKILL.md:_, _ = db.FinishRun(ctx, run.ID, cortexdb.RunEnd{Outcome: "resolved"})SKILL.md:sum, _ := db.SummarizeRun(ctx, run.ID) // tokens, cost, open/failed steps, critical path, least confident stepSKILL.md:then, _ := db.ReplayRun(ctx, run.ID, someInstant) // the run as it stood then, from historySKILL.md:Every write fails before writing anything when the run is not running, a parent does not exist, the kind cannot be a label, or an attribute reuses a name the graph writes itself.SKILL.md:a run finishes once.SKILL.md:// Dry run first; with neither rule_ids nor rules, every enabled rule runs.SKILL.md:rep, _ := im.Run(ctx, src, plan) // rep.RowsRead/ChunksIndexed/TriplesCreated/UnparsedStatementsSKILL.md:With an inferer, `im.AutoImport(ctx, src, importflow.Goal{BuildRAG: true, BuildKG: true})` does plan + run in one step.SKILL.md:Review the plan, then feed it to `im.Run` / the connector to build the graph.SKILL.md:plan.Sign("you", time.Now()) // Run refuses an unsigned planSKILL.md:rep, _ := importflow.New(db).Run(ctx, connector.Desensitized(src, d), mapping)SKILL.md:**Library API only** — `w.Run(ctx)` does one pass and the caller schedules it (loop / cron).SKILL.md:_ = w.Run(ctx) // schedule on an interval; resumes from the checkpointSKILL.md:**Route B-PG (Postgres logical replication, `NewPostgresCDCSource`):** a true CDC source over `pgoutput` that **captures hard deletes** and **streams continuously** — `w.Run(ctx)` blocks until ctx is cancelled and resumes by LSN from the checkpoint (`Checkpoint.Position`).SKILL.md:go w.Run(ctx) // streams insert/update/delete continuously; resumes by LSNSKILL.md:Like Route B-PG it **captures hard deletes** and **streams continuously** — `w.Run(ctx)` blocks until ctx is cancelled and resumes by binlog position (`Checkpoint.Position`).SKILL.md:go w.Run(ctx) // streams insert/update/delete; resumes by binlog positionSKILL.md:Wire via `connector.NewMCPServer(tb, opts)` / `connector.RunMCPStdio(ctx, tb, opts)`, `connector.RegisterMCPTools(server, tb)` to ride another server, or run `cmd/cortexdb-connector-mcp`.SKILL.md:Transport is plaintext by design — run it over loopback, a trusted LAN, or Tailscale, or publish it as `https://host` behind a TLS-terminating proxy (`CORTEXDB_REMOTE=https://host`;SKILL.md:the latter two need direct database access, so they run where the database lives.SKILL.md:From a shell: `cortexdb-mcp --import-agent-memory [--sessions] [--since 30d] [--max N] [--dry-run]`.SKILL.md:With an active ontology, a capture is told its object types: the active schema is read once per run (on a shared brain through `ontology_list`), each declared type's `api_name`, a one-line description and its aliases are listed in the prompt — at most 60 types, 160-character descriptio …[+152 chars]SKILL.md:graphflow's `LLMExtractor` does the same under `Pipeline.Run` (undeclared → `graphflow_node`).SKILL.md:Python — `clients/python/` (PyPI): `pip install cortexdb-client`;SKILL.md:Node — `clients/node/` (npm): `npm install cortexdb-client`;SKILL.md:Published to ClawHub (`clawhub skill install cortexdb-agent-memory`) and installable from git / skills.sh (`hermes skills install git:liliang-cn/cortexdb@main`, `npx skills add liliang-cn/cortexdb`).SKILL.md:go run ./examples/01_coreSKILL.md:go run ./examples/02_ragSKILL.md:go run ./examples/03_memoryflowSKILL.md:go run ./examples/04_knowledge_graphSKILL.md:go run ./examples/05_graphflowSKILL.md:go run ./examples/06_tools_mcpSKILL.md:go run ./examples/07_importflowSKILL.md:go run ./examples/08_self_knowledge_graph # docs -> graphflow -> KG of this project; qa_test.go answers from graph edgesSKILL.md:go run ./examples/16_ontology # typed object types, actions, object sets, typed tools, schema diffSKILL.md:When changing CortexDB, run:plugins/cortexdb/skills/cortexdb/SKILL.md:Run with `GOMEMLIMIT` set below the device's memory so the garbage collector works before the kernel does.plugins/cortexdb/skills/cortexdb/SKILL.md:The live view (`serve_graph_3d`) can be asked, not only looked at: find any node in the store, ask a question and see what the answer names, run read-only Cypher, expand a node's neighbours past the drawn core.plugins/cortexdb/skills/cortexdb/SKILL.md:From a shell: `cortexdb-mcp --import-agent-memory [--sessions] [--since 30d] [--max N] [--dry-run]`.plugins/cortexdb/skills/cortexdb/SKILL.md:re-run the rules after a manual full refresh, which clears them.plugins/cortexdb/skills/cortexdb/SKILL.md:Results are explainable inferred triples named `shacl_triple_rule:*`, and the shapes passed are the whole rule set: a rule left out is retracted on the next run.plugins/cortexdb/skills/cortexdb/SKILL.md:An agent's record of its own run, as graph records: `StartRun`, `BeginStep` / `EndStep` (a step is written as running, with `TRIGGERED` edges from the steps it consumed, before its work runs), `RecordStep`, `FinishRun`;plugins/cortexdb/skills/cortexdb/SKILL.md:read back with `GetRun`, `ListRuns`, `RunSteps`, `SummarizeRun`, `StepLineage` and `ReplayRun` (the run as of any instant).plugins/cortexdb/skills/cortexdb/SKILL.md:Wire via `connector.NewToolbox(db, opts)` + `connector.NewMCPServer(tb, opts)` / `connector.RunMCPStdio(ctx, tb, opts)`, `connector.RegisterMCPTools(server, tb)` to ride another server, or run `cmd/cortexdb-connector-mcp`.plugins/cortexdb/skills/cortexdb/SKILL.md:go run ./examples/01_coreplugins/cortexdb/skills/cortexdb/SKILL.md:go run ./examples/02_ragplugins/cortexdb/skills/cortexdb/SKILL.md:go run ./examples/03_memoryflowplugins/cortexdb/skills/cortexdb/SKILL.md:go run ./examples/04_knowledge_graphplugins/cortexdb/skills/cortexdb/SKILL.md:go run ./examples/05_graphflowplugins/cortexdb/skills/cortexdb/SKILL.md:go run ./examples/06_tools_mcpplugins/cortexdb/skills/cortexdb/SKILL.md:When changing CortexDB, run:skills/cortexdb-memory-hermes/SKILL.md:# install the binary (or download a prebuilt release)skills/cortexdb-memory-hermes/SKILL.md:go install github.com/liliang-cn/cortexdb/v2/cmd/cortexdb-grpc@latestskills/cortexdb-memory-hermes/SKILL.md:For automatic pre-turn recall and completed-turn synchronization, install the native Hermes `MemoryProvider` at `plugins/hermes-cortexdb-memory` and set `memory.provider: cortexdb`.skills/cortexdb-memory-hermes/SKILL.md:Hermes also speaks MCP — if you prefer, run `cortexdb-mcp-stdio` and connect it as an MCP server instead of (or alongside) this skill.skills/cortexdb-memory-openclaw/SKILL.md:# install the binary (or download a prebuilt release)skills/cortexdb-memory-openclaw/SKILL.md:go install github.com/liliang-cn/cortexdb/v2/cmd/cortexdb-grpc@latestskills/cortexdb-memory-openclaw/SKILL.md:For automatic OpenClaw memory lifecycle integration, install the native plugin at `plugins/openclaw-cortexdb-memory` and select `cortexdb-memory` in `plugins.slots.memory`.
MCP servers (1)
plugins/cortexdb/.mcp.json(cortexdb):${CLAUDE_PLUGIN_ROOT}/bin/cortexdb-mcp
Hooks (4)
plugins/cortexdb/hooks/hooks.json(SessionStart):${CLAUDE_PLUGIN_ROOT}/bin/cortexdb-session-startplugins/cortexdb/hooks/hooks.json(UserPromptSubmit):${CLAUDE_PLUGIN_ROOT}/bin/cortexdb-recallplugins/cortexdb/hooks/hooks.json(PreToolUse):${CLAUDE_PLUGIN_ROOT}/bin/cortexdb-file-contextplugins/cortexdb/hooks/hooks.json(SessionEnd):${CLAUDE_PLUGIN_ROOT}/bin/cortexdb-session-end
- Medium risk: exfil-endpoint (.agents/skills/cortexdb/SKILL.md:301)
- Medium risk: exfil-endpoint (plugins/cortexdb/skills/cortexdb/SKILL.md:308)
- Low risk: network-instructions (SKILL.md:1379)
- Low risk: network-instructions (skills/cortexdb-memory-hermes/SKILL.md:49)
Attribution and licence
- First committed by
liliang-cn
- Publisher
- Liang Li (Community)
- Source
- liliang-cn/cortexdb · commit d39714c
- Licence
- MIT License (files served here)
- ATTRIBUTION.md (written by AutoGineer)
- Synced
- Contributors
- liliang-cn, lfoletti
AutoGineer is not affiliated with or endorsed by liliang-cn.
Install from source
501 of its files are not served here, so there is no ZIP. liliang-cn/cortexdb at d39714c