npx -y @nehloo/graphnosis@latest demoOpens a 3D graph of a sample corpus in your browser. Ask it a question and watch the retrieval walk light up. No install, no API key, no network.
Point it at your own files instead:
npx -y @nehloo/graphnosis@latest demo ./my-vault-notesArrowheads mark directed edges — typed logic that points somewhere
(causes, precedes, supersedes, contains). Plain lines are undirected
association (similar-to, shares-entity, co-occurs). Both layers sit on the
same nodes and are walked together. That is the whole idea, and it is easier to
see than to read about.
Cloned the repository instead? A clone has no dist/ until you build it — see
Build from source.
npx -y @nehloo/graphnosis@latest serve ./my-vault-notescurl localhost:7777/query -d '{"q":"why did the deploy fail?"}'Plain HTTP, so Python, Go, Ruby and curl all work. The same binary is also an MCP
server (npx -y @nehloo/graphnosis@latest mcp), which Claude Desktop, Cursor and Zed
connect to with one config block.
On other languages. There is one implementation of the engine, on purpose. Retrieval from a fixed graph is deterministic — the same
.gaiand question give the same subgraph — and independent ports are the fastest way to lose that, because tie-breaking, hash iteration order and Unicode handling all differ subtly between runtimes. Cold ingest that enables optional LLM summaries is not deterministic unless those generated summaries are pinned. So other languages get a process boundary, not a rewrite.
npm install @nehloo/graphnosisimport { Graphnosis } from '@nehloo/graphnosis';
const g = new Graphnosis({ name: 'incident' });
g.addText('The checkout deploy failed on Tuesday because the orders migration timed out.', 'deploy-log.md');
g.addText('The orders migration timed out because the index on customer_id was missing.', 'postmortem.md');
g.addText('We added the missing index on customer_id the following morning.', 'followup.md');
g.build();
console.log(g.query('why did the checkout deploy fail?').subgraph.serialized);=== KNOWLEDGE SUBGRAPH (4 nodes, 3 edges) ===
--- NODES ---
[n1|fact|0.49|src:deploy-log.md] The checkout deploy failed on Tuesday because the orders migration timed out.
[n3|fact|0.11|src:postmortem.md] The orders migration timed out because the index on customer_id was missing.
[n4|fact|0.04|src:followup.md] We added the missing index on customer_id the following morning.
--- DIRECTED ---
n2 -[contains:1.0]-> n1
--- UNDIRECTED ---
n1 ~[similar-to:0.4]~ n3
n3 ~[similar-to:0.5]~ n4
A vector store would return three passages ranked by similarity and leave the model to guess how they relate. Here the failure, the root cause and the remedy are linked, and the model is told so.
const prompt = g.prompt('why did the checkout deploy fail?');
// a system prompt containing that subgraph — send it to Claude, GPT, Ollama,
// Bedrock. Graphnosis never makes the call itself.Give each source its own
sourceFile. Repeating one across severaladdTextcalls collides chunk keys and retrieval returns nothing.
One node set, two edge classes, walked together. What that picture holds, and
what a folder of .md, a vector index, or a conventional graph database each
cannot give you:
Two co-equal edge layers, not one. Directed edges carry typed logic —
causes, precedes, supersedes, contains. Undirected edges carry
association — similar-to, shares-entity, co-occurs. Both are independently
queryable classes over the same nodes, and one BFS walks them together. A
property graph makes you commit to a schema first; RAG has no edges at all.
Retrieval returns a subgraph, not a ranked list. The model receives the facts and the relations between them, serialized. A vector store returns passages by cosine distance and leaves the model to guess how they connect.
The same question gives the same subgraph. Retrieval from a fixed graph is
deterministic — one .gai, one question, one answer shape, every time. No
temperature, no index drift, no re-embedding between runs.
Contradictions surface instead of merging. Conflicting facts are reported as
a conflict for the owner to adjudicate. Appending to a .md file keeps both
silently; a vector store silently returns whichever ranked higher.
Memory ages. Nodes carry createdAt, lastAccessedAt, accessCount,
validUntil and a confidence that decays with disuse. Superseded facts retire
without being destroyed. Flat files and vector chunks have no notion of stale.
It is one portable file you own. A .gai is bytes on your disk — copy it,
commit it, hand it to someone, diff it. No server, no vector database, no
account.
No API key, no network. Lexical retrieval is the default and runs entirely on-device. Embeddings and LLM summaries are optional paths, not the price of entry.
None of these are implementation preferences. They are the properties the substrate paper sets out and this library implements — the co-equal dual graph, the deterministic owner-adjudicated contradiction contract, and the indelibility and order-independent merge guarantees.
What that means for agentic graph engineering: memory stops being a probabilistic index you hope returns the right chunk, and becomes a substrate you can reason about before you run it. That reduces to five requirements a memory layer either meets or does not — independent of control flow, so a loop and a graph are judged by the same five.
§1 Provenance. Every fact traces back to where it came from. Nodes carry
their src: through retrieval, so any claim in the returned subgraph walks back
to the file that produced it. An agent that cannot cite cannot be audited.
§2 Indelibility. Nothing is silently overwritten — contradictions are kept and owner-adjudicated. Supersession is an edge rather than a delete, so you can still ask what the agent knew at the moment it acted, after it has learned otherwise.
§3 Determinism. The same question gets the same answer, twice. A bad retrieval is reproducible, which is the difference between debugging an agent and re-rolling it. Order-independent merge extends this across writers: two agents recording the same facts in different orders converge on the same graph, so concurrent and resumed sessions do not drift apart.
§4 Capped authority. What proposes an action never approves its own limits. The engine reports a conflict; it never picks the winner. Retraction is a soft delete the owner can reverse, and the optional LLM passes enrich — they are never the writer of record.
§5 Co-location. Memory, skills, and agents live in one place, as one
structure. This library is the substrate half: one .gai holding the facts and
the relations between them, with no second index to keep in sync. The procedural
half — skills held as subgraphs in that same store — is the subject of the
second preprint and ships in the
desktop app.
To measure that, we wrote a preprint paper: The Un-Brain, CC BY 4.0 on Zenodo — 10.5281/zenodo.20843387
const g = new Graphnosis({ name: 'policy' });
g.addText('The Acme Corporation service agreement sets the API rate limit for the Reporting endpoint at 1000 requests per minute.', 'contract-2024.md');
g.build();
const res = g.appendText(
'The Acme Corporation service agreement was corrected: the Reporting endpoint rate limit is no longer 1000 requests per minute.',
'contract-2025.md',
);
res.contradictions.length; // 1
res.contradictions[0].sharedEntities; // ['The Acme Corporation', 'Acme', 'Corporation', 'Reporting']Nothing is resolved for you. The new passage is ingested like any other, and the old node is left exactly as it was — not retired, not rewritten, not merged away. You are told there is a conflict and you decide: supersede, keep both, or reject. A vector store has no place to put that decision, so it silently keeps both and lets ranking pick a winner per query.
Detection is deliberately conservative: two or more shared strong entities, both passages over 60 characters, and an explicit conflict marker (
corrected,no longer,superseded,disputed, …). A bare numeric flip with no marker is missed. A false contradiction is worse than a missed one.
const buf = g.toBuffer(); // .gai — msgpack, magic 'AIKG', checksummed
const restored = new Graphnosis({ name: 'notes' });
restored.fromBuffer(buf); // instance method, not staticNodes, typed edges, provenance and timestamps. Optional HMAC-SHA256 if you want it tamper-evident. Write it to disk, put it in S3, ship it in a Lambda, hand it to a colleague. Format spec →
Retrieval writes nothing, and ranking depends only on the serialized graph state
plus the query — so two queries against the same .gai can be diffed and any
difference is a real change. The one clock it reads is retirement: a node is
excluded once its retirement instant has passed, which is why a graph holding
retired or time-bounded content answers differently tomorrow. Pin it by passing
retiredAt to get a fixed as-of view. Optional LLM enrichment during a fresh
ingest is a separate, generative step.
Load real files
await g.appendFile('./docs/architecture.md'); // auto-detects format
await g.appendFolder('./docs');
await g.appendPdf(readFileSync('spec.pdf')); // pdf, csv, json, html tooadd* builds a graph; append* adds to one that already exists. Calling
append* before build() fails.
Semantic retrieval (optional, needs an API key)
npm install ai @ai-sdk/openaiimport { openaiEmbedAdapter } from '@nehloo/graphnosis/adapters/openai';
await g.buildEmbeddings({ adapter: openaiEmbedAdapter() }); // reads OPENAI_API_KEY
const r = await g.queryHybrid('what broke last week?');ai and @ai-sdk/openai are peer dependencies, and the adapter is required —
without one buildEmbeddings() throws before it reaches the network. TF-IDF
works with zero network access; embeddings are opt-in and additive.
Persistence
g.saveGai('memory.gai'); g.loadGai('memory.gai');
const buf = g.toBuffer(); // serverless-friendly, no filesystem
// SQLite is an optional backend — `better-sqlite3` is an optionalDependency,
// so install it if your platform skipped the native build. Loading needs the
// graph id as well as the file, because one database can hold several graphs.
g.saveSqlite('memory.db');
g.loadSqlite('memory.db', 'incident');Non-English corpora
import { unicodeAnalyzer } from '@nehloo/graphnosis';
const g = new Graphnosis({ name: 'ro', analyzer: unicodeAnalyzer });The default folds diacritics and strips English stopwords. unicodeAnalyzer
preserves diacritics where they are phonemic.
Corrections and forgetting
g.supersede(oldNodeId, 'The rate limit is now 500 rpm.', 'contract renegotiated');
g.forgetTopic('deprecated-api');
g.forgetBefore(Date.parse('2024-01-01')); // milliseconds, not a Date
const audit = g.reflect(); // contradictions + discovery in one pass
g.retired(); // everything you have forgotten, still readableForgetting is not erasure. A retired node keeps its content, its id, its
edges and its provenance — it is excluded from every prompt and from nothing
else, and retired() is the audit surface that makes that checkable. An empty
result there means nothing has been retired, never that something was
destroyed. Retiring by supersede also blocks the source from re-introducing
the corrected-away text on the next sync; retiring by delete does not, so
re-adding a file restores it.
Yes — if you are building an agent that needs memory across sessions, you want relationships between facts rather than nearest neighbours, you need the answer to be reproducible, or you want the memory to be a portable file.
Probably not — if you want a hosted memory service, your corpus is one document, or plain semantic search already answers your questions. Use a vector store; it is simpler and it fits.
Known limits. Contradiction detection is pattern-based and misses conflicts with no explicit marker. Retrieval is lexical by default, so a question sharing no vocabulary with its source needs the embedding path. Identity extraction still treats two capitalised words in content as a possible person, so organization names can be false positives; structural headings are excluded. The current benchmark figure is being re-measured on this release line, so the table below reports only what is published and reproducible — good, not solved.
A current headline figure is deliberately not quoted here. This release line changed retrieval — retirement, expiry and structural ranking all moved — so the last measured score no longer describes the code you would install. It is being re-measured on LongMemEval's 500-question set with official judge prompts, GPT-4o answering and judging, at a pinned temperature of 0. The number returns when the run does.
What is published and reproducible today, from earlier release lines:
| arm | score |
|---|---|
| historical single-run best: dual-graph + router + summaries + preferences | 78.00% |
| same, embeddings removed (TF-IDF only) | 64.60% |
| flat top-k, identical seed pool | 49.00% |
| fully on-device (Llama 3.2 3B, zero cloud) | 41.60% |
Those rows ship as raw run artifacts in
benchmarks/evidence/, and
node benchmarks/evidence/verify.mjs recomputes each one and exits non-zero if
any disagree. They are a single run each on an older line — read them as
evidence that the ablations separate, not as a current score.
The published package ships a prebuilt dist/. A clone does not — dist/ is
generated, never committed — so build it once before the CLI will run:
git clone https://github.com/nehloo/Graphnosis.git
cd Graphnosis
npm installnpm install runs the prepare script, which runs npm run build:lib and
writes dist/. There is no separate build step to remember. Then run the CLI
from your clone:
node dist/cli/index.js demoRebuild after editing anything under src/ — or if you installed with
--ignore-scripts, which skips prepare:
npm run build:libTo put the graphnosis command on your PATH, pointed at your clone:
npm link
npxwill not run your clone. Inside the repository,npx -y @nehloo/graphnosis@latest demostill downloads the published release and runs that — a clone is not an installed dependency of itself, sonpxfinds nothing local and falls through to the registry. Usenode dist/cli/index.jswhen you mean your own build.Why every command above pins
@latest. Without it,npxwill happily reuse an older copy it already has — one cached from an earlier run, or one sitting in a project'snode_modules. If that copy predates the CLI, which arrived in 0.9.0, you getcould not determine executable to run, orgraphnosis: command not found. Pinning@latestforces a fresh resolve against the registry.
Documents are chunked into nodes. Two edge layers are built over the same node set — directed for typed logic, undirected for association. A query seeds into the graph lexically, walks both layers in one BFS with score decay, and returns the subgraph.
Run npx -y @nehloo/graphnosis@latest demo and look at it; the picture explains the
rest faster than this section can.
A preprint, CC BY 4.0 on Zenodo.
The Un-Brain — the substrate this library implements: the co-equal dual graph (directed and undirected edges as two independently queryable classes over one node set), the deterministic owner-adjudicated contradiction contract, and the indelibility and order-independent merge properties. It reports the retrieval ablation the benchmark table above draws on. 10.5281/zenodo.20843387
Both papers, this one and the skills paper below, describe the wider Graphnosis system, of which this package is the engine. Parts they discuss that sit above the library — the MCP surface, encrypted sync — are separate repositories.
Graphnosis is a desktop application built on this library. Source at nehloo-interactive/graphnosis-app, under the Functional Source License 1.1 (Apache-2.0 future) — a different licence from this package.
A second preprint, also CC BY 4.0 on Zenodo, describes the mechanism the app adds on top of the engine.
Borrowable Skills as Lean Un-Ganglia Subgraphs — procedures held as subgraphs in the same store as the facts they operate on: trainable, composable, bounded-loop SOPs co-located with memory, and borrowable across that store. Continues the first paper. 10.5281/zenodo.21205599
That is not a design you have to build yourself. It ships: train an SOP, walk it
a step at a time, watch it improve run over run, export it as a signed .gsk
pack. The skills reference documents
the behaviour the paper measures.


