cd /news/artificial-intelligence/okf-graph-wiki-agent-maintained-know… · home topics artificial-intelligence article
[ARTICLE · art-97724] src=gist.github.com ↗ pub= topic=artificial-intelligence verified=true sentiment=· neutral

OKF Graph Wiki — Agent-Maintained Knowledge Graph for Context Retrieval

A developer has built an agent-maintained knowledge graph wiki using the Open Knowledge Format (OKF), extending Andrej Karpathy's LLM Wiki pattern. The system compiles typed subject-predicate-object triples into a SQLite index for fast, deterministic retrieval, prioritizing context assembly for LLM queries over human browsing. The project includes a TypeScript/SQLite reference implementation and an implementation spec.

read37 min views1 publishedAug 14, 2026

I've been running Andrej Karpathy's LLM Wiki pattern for several months — reading sources, compiling them into a compounding, git-diffable wiki instead of re-deriving everything from scratch every session — and I'm a genuine convert. But two things kept nagging at me: query salience (finding the right page reliably, not just something plausible) and token economy (not re-reading half the wiki to answer one question).

The missing piece turned out to be the Open Knowledge Format (OKF): structured YAML frontmatter an agent can use to record the graph it's already implicitly building in prose, as typed subject–predicate–object triples rather than untyped markdown links. To keep retrieval fast and deterministic — not another LLM call on every question — that frontmatter compiles into a disposable SQLite index. What follows specs that redesign for someone to actually build — an implementation spec, not a blog post, where every section states what to build rather than just the idea. It ships with a working TypeScript/SQLite reference implementation (Appendix) and pitfalls validated by building and running it, not guessed at (§10).

The original gist frames the wiki as a human-browsable, compounding artifact: an agent reads sources, updates markdown pages, a person reads the results in Obsidian. That's still true here, but it is not the primary purpose of this version. The primary purpose is: given a question posed to an LLM, retrieve and assemble the right slice of the graph into that LLM's context window. Human browsing is a secondary, free side benefit of keeping everything as linked markdown. Every design decision below is made in service of the retrieval goal first.

Concretely, that reframing changed three things versus the original gist: the file format is now OKF (structured provenance/trust/lifecycle fields, not just prose), relationships are typed subject-predicate-object triples in frontmatter (not just untyped markdown links), and there is a retrieval pipeline spec (section 6) that did not exist in the original idea at all.

  • Maintain a persistent, compounding knowledge base as a directory of plain markdown files, agent-written and agent-maintained.
  • Represent facts as typed, sourced, subject-predicate-object triples that a query engine can traverse, not just prose an LLM re-reads from scratch each time.
  • At question time, retrieve a bounded, relevance-ranked, trust-ranked set of facts and background prose that fits a context budget — this is the deliverable, not an afterthought.
  • Stay diffable, portable, and toolable: git-diffable markdown as the only source of truth; every other artifact (SQLite index, timeline.md

,index.md

entries) is a regenerable build product, never hand-edited.

Not a general agent-memory system. This is not session memory, preference tracking, or conversational recall — see §9.3 for why an existing tool in that space (mnemon-dev/mnemon

) was evaluated and rejected as the storage engine.Not a fixed ontology. No upfront RDF/OWL schema. Both OKF'stype

and this spec's predicate vocabulary are open and grow by convention, documented as they're coined (§8).Not a graph database. SQLite with JSON1 functions is sufficient at the scale this pattern is meant for (hundreds to low thousands of concepts); this spec does not call for Neo4j or similar.

Considered a fourth layer, router.md

(a hand-curated dispatch table from question domain to subdirectory), and dropped it: context_for()

(§6) queries the whole bundle in one hybrid-retrieval pass and doesn't benefit from a coarse pre-filter, and the one audience that would have needed file-based dispatch — a human browsing the vault in Obsidian — already has direct SQL access to wiki.db

, either via Datasette or a Obsidian SQLite-query plugin (SQLite Explorer, SQLite DB, and similar all support embedding live query results in a note). The only scenario where a router file would still earn its keep is an agent given pure filesystem read access with no shell or SQL tool at all; not designed for by default.

bundle/
  CLAUDE.md / AGENTS.md   # schema doc: conventions, predicate vocabulary, ingest rules
  wiki.db                 # SQLite index, regenerated from frontmatter, gitignored or snapshotted
  log.md                  # OKF-native edit history (when the wiki changed)
  timeline.md             # world-event chronology (when things happened), regenerated
  wiki-trigger-keywords.txt  # concept titles + tags, regenerated — optional harness-hook pre-filter (§12)
  entities/
    index.md              # OKF-native per-directory catalog
    <concept>.md           # OKF concept files, one per entity/topic
  computations/
    ...                    # optional: OKF Attested Computation concepts, if the bundle needs sanctioned metrics
  references/
    ...                    # OKF convention: mirrored external material, executor/attester code
File Role Maintained how
index.md
Per-directory catalog (OKF §8): concept list with one-line descriptions, for progressive disclosure. Regenerable from concept frontmatter; may also be hand-edited.
Concept files The atomic units of knowledge: frontmatter + prose body. Source of truth.
Agent-written during ingest (§7.1).
timeline.md
Chronological ledger of world events (as opposed to log.md , which is edit history), built from every date-qualified relations entry across the bundle.
Fully regenerated, never hand-edited.
wiki.db
SQLite index over all concept frontmatter, the substrate the retrieval pipeline (§6) queries. Fully regenerated on every ingest.
wiki-trigger-keywords.txt
Flat, deduped list of every concept's title and tags — a deterministic pre-filter an optional harness hook can grep against, to nudge querying the wiki without paying context_for() 's cost on every turn. Not wired to anything by default; see §12.
Fully regenerated on every Reindex.

Every concept is one markdown file: YAML frontmatter + prose body, per OKF v0.2. Concept identity is its bundle-relative path with .md

removed (OKF §2) — no separate ID scheme needed.

The original gist names the content a wiki should hold: "Summaries, entity pages, concept pages, comparisons, an overview, a synthesis." This spec keeps that taxonomy — it does not collapse into one generic file kind. It maps onto OKF's open type

field (§4.2) plus the derived timeline (§3):

| Page kind | type value | Carries relations ? | What it's for | |---|---|---|---| | Entity page | Person , Organization , Product , Place , Country , or Entity as a fallback | Yes — the primary source of typed triples | A specific named person, organization, product, place, country (§4.5 worked example). Use the most specific value that fits — a person's file gets type: Person , not generic Entity ; fall back to Entity only when none of the starter set applies. | | Concept page | Concept | Optional — abstract topics rarely assert their own triples, but link and synthesize several entities' relations in prose | An idea, a trend, a comparison, a synthesis, an overview — the original gist's "comparisons, an overview, a synthesis." | | World event chronology | (not a page type) | n/a — derived view, not authored content | timeline.md (§3), built from every date-qualified relations entry across Entity and Concept pages. Not its own type — deliberately: it stays a regenerated aggregate, never hand-authored, per the source-of-truth rule in §1. |

Concept pages are where a synthesized answer worth keeping gets filed back into the wiki (§7.2) — this is the mechanism that keeps the bundle compounding beyond raw source ingest, not just entity extraction.

Field OKF section Required? Purpose
type
§4.1 Required
e.g. Person , Entity , Playbook , Metric . Open vocabulary — see §8.
title , description , tags
§4.1 Recommended Display, seed-matching, and index generation.
sources
§5.1 Optional Provenance: what this concept was derived from. Each relations entry cites into this by id .
generated
§5.2 Recommended { by, at } — who/what wrote the current content, actor convention (§7 of OKF).
verified
§5.2–5.3 Optional Drives the trust tier used in retrieval ranking (§6.4): no verified → unverified; non-human: actor → machine-confirmed; any human: actor → human-reviewed.
status , stale_after
§5.4–5.5 Optional Lifecycle. stale_after is checked at retrieval time (§6.4), not just for display.
superseded_by , supersedes
extension, not in OKF core
Optional Concept-file-level supersession — a whole concept file explicitly replaced by another. See §7.7. Distinct from stale_after (time-based expiry, no replacement recorded) and from a per-fact contradiction (§7.3's contradiction check, which flags but does not resolve).
relations
extension, not in OKF core
Optional Typed triples. See §4.3. Valid per OKF §4.1 ("producers MAY include any additional keys... consumers MUST NOT reject documents with unrecognized fields") — this bundle stays OKF v0.2 conformant.

Provenance convention for locally-mirrored sources. When a sources[]

entry's material was saved locally into references/

during ingest (§7.1), record the bundle-relative path there as resource

— e.g. resource: references/tc-profile-2026.html

— rather than just the original external URL. A sources[]

entry whose resource

is not a references/...

path (a bare external URL, or the field absent entirely) marks a concept as not traceable to a locally mirrored document — the signal §7.6's Rebuilding from references operation uses to distinguish an ingest-derived concept file from a hand-assembled one (a filed-back Concept page built from discussion and general web search, for instance) without needing a separate flag.

relations:
  - { predicate: invested_in, object: /entities/openai.md, date: "2019-03-01", source: tc-profile-2026 }
  - { predicate: married_to, object: /entities/laura-arrillaga-andreessen.md }
  - { predicate: born_on, object: "1971-07-16" }
  • The concept file itself is the implicit subject— no separate subject field, identity is the file path (OKF §2). predicate

: free-text string. Open vocabulary, governed by §8.object

: either a bundle-relative path beginning with/

(an entity-to-entity edge — the target should resolve to another concept, but per OKF §6.1 consumers MUST tolerate a broken target, since it may represent not-yet-written knowledge) or a literal value (an entity-to-value fact, e.g. a birth date).date

(optional): an ISO 8601 date,the field this spec uses for temporal qualification of a fact — do not name this field See §10.1 for why.on

.source

(optional): joins to asources[].id

on the same concept, reusing OKF's existing per-claim attribution mechanism (§5.1) rather than inventing a second citation scheme.

Prose links in the body ([OpenAI](/entities/openai.md)

) still work as OKF intends — untyped, human-narrative traversal. relations

is the machine layer for typed graph queries; it does not replace prose links, it sits alongside them.

---
type: Person
title: Antony Blinken
description: United States Secretary of State (2021-2025).
tags: [person, united-states, foreign-policy]
relations:
  - { predicate: secretary_of, object: /entities/us-department-of-state.md, date: "2021-01-26", source: state-dept-bio }
  - { predicate: represents, object: /entities/united-states.md, source: state-dept-bio }
generated: { by: "reference_agent/claude-sonnet-5", at: "2026-08-09T10:00:00Z" }
verified: { by: "human:reviewer", at: "2026-08-09T11:00:00Z" }
sources:
  - { id: state-dept-bio, resource: "https://2021-2025.state.gov/biographies/antony-j-blinken/", title: "U.S. Department of State — Antony J. Blinken" }
---


Antony Blinken served as [the United States](/entities/united-states.md)'s
Secretary of State from January 2021, heading the
[Department of State](/entities/us-department-of-state.md).[^state-dept-bio]

[^state-dept-bio]: U.S. Department of State — Antony J. Blinken

No relations

block required; it links to and synthesizes existing Entity pages instead of asserting its own triples. This is what a filed- back query answer (§7.2) typically looks like.

---
type: Concept
title: The U.S. Department of State under Antony Blinken
description: Synthesis of continuity in U.S. foreign-policy leadership during Antony Blinken's tenure as Secretary of State.
tags: [synthesis, united-states, foreign-policy]
generated: { by: "reference_agent/claude-sonnet-5", at: "2026-08-09T12:00:00Z" }
---


From January 2021, [Antony Blinken](/entities/antony-blinken.md)
represented [the United States](/entities/united-states.md) as Secretary
of State — see [the timeline](/timeline.md#2021-01-26) for when the
appointment began relative to other tracked events.

Do not build a bespoke database engine. Load every concept file's frontmatter into a concepts

table with a small — the reference implementation's build-index.ts

does this in well under a hundred lines using the yaml

npm package for parsing and bun:sqlite

for the database; any TypeScript/JS equivalent works the same way.

List-valued fields (relations

, sources

, tags

) land as JSON text columns automatically — query them with SQLite's built-in json_each

/ json_extract

, no separate relations

table or ETL step required. The auto-generated _path

column doubles as concept identity, matching OKF's own file-path-is-identity rule for free.

Add on top of the base concepts

table:

: an FTS5 virtual table overconcepts_fts

title

,description

,tags

, and body text, for BM25 keyword search (§6.1).:embeddings

(path TEXT PRIMARY KEY, vector TEXT)

— one row per concept, vector as a JSON float array, produced by whichever embedding backend is configured (§6.1, §9.2).

Rebuild wiki.db

(and concepts_fts

/embeddings

) as the last step of every ingest (§7.1). It is a cache, never a second source of truth.

An existing SQLite-backed, four-graph (temporal/entity/causal/semantic) agent memory tool was evaluated and rejected as the storage layer for this bundle. Recorded here so the implementing agent doesn't re-derive or re-litigate this:

  • Its SQLite DB is the source of truth — no per-node markdown files exist to keep git-diffable, which inverts this spec's core principle (§1, §3). - Its insights

schema is a flat memory-snippet model with no room for OKF'stype

/sources

/verified

/status

/stale_after

fields. - Its edges are inferred heuristically (embedding similarity thresholds, keyword regex, temporal proximity) from a fixed four-type enum, not deliberately asserted, sourced, open-vocabulary predicates.

  • It has importance-decay and auto-pruning by design, which works against the "nothing gets silently dropped" compounding-wiki goal.

This is the actual deliverable. Given a question, produce a context block for the LLM answering it.

Three signals, fused with Reciprocal Rank Fusion (score = Σ 1/(k + rank + 1)

, k ≈ 60

), exact-title match weighted 2x by being included in the fusion twice:

Exact title match— the concept's fulltitle

appears in the question at a whole-word/phrase boundary, not as a bare substring. Boundary-checked is load-bearing: a concept titled "AI" must not seed on the word "explain", nor "Go" on "ago". Cheap, high precision, andnecessary: on a cross-linked wiki, a concept is routinely mentioned on other concepts' pages, so term-frequency methods alone cannot distinguish "the question is about X" from "X is merely mentioned here" (see §10.3 — this is a validated finding, not a hypothetical).FTS5 BM25 overconcepts_fts

— keyword recall for partial or non-exact mentions. Used forrankingcandidates as-is; used forconfidenceonly after a word-coverage check, not on raw presence alone — validated, not hypothetical (§10.4).Vector cosine similarity overembeddings

— semantic recall for paraphrased questions with no lexical overlap.Embedding backend:(nomic-embed-text

via a local Ollama endpointlocalhost:11434

), withsearch_document:

/search_query:

prefixing per its convention — decided, not a placeholder. See §9.2 for why a TF-IDF stand-in was used during prototyping and is not carried forward.

Lightweight keyword classification into WHY | WHEN | ENTITY | GENERAL

(extend as needed). Drives predicate weighting in §6.3 — a pattern worth keeping from Mnemon's intent-adaptive traversal even though its storage engine was rejected (§5.1): a WHY question should weight causally-loaded predicates higher, a WHEN question should weight the date-qualified subset higher, and so on.

BFS from the seed concept(s), hop limit 2 by default. Score every traversed relation:

score = predicate_weight(intent, predicate) * hop_decay^hop * trust_multiplier(subject_trust_tier)

predicate_weight

: a small hand-maintained table per intent (grows with the predicate vocabulary, §8).hop_decay

: constant < 1 (e.g. 0.6) — relevance decays with distance from the seed.trust_multiplier

:{human-reviewed: 1.0, machine-confirmed: 0.8, unverified: 0.6}

, derived fromverified

per OKF §5.3.

Do not return the whole neighborhood — this is what makes the retrieval bounded rather than a graph dump.

Sort scored triples descending, take the top N by context budget. Exclude or down-rank facts where today >= stale_after

(OKF §5.5). Trust tier is a first-class ranking input here, not just display metadata — when more candidates exist than the budget allows, prefer human-reviewed, non-stale facts over unverified ones at equal predicate weight. A concept file with status: superseded

gets its own filtering pass, distinct from stale_after

's — see §7.7.

Render for the LLM prompt as compact fact lines plus prose background from the seed concepts' bodies, not raw JSON:

- Andreessen Horowitz invested_in OpenAI, as of 2021-04-01 (trust: human-reviewed, source: a16z-portfolio)

Triples answer that a fact holds; the seed concepts' prose bodies are included for facts that need more than a triple to convey correctly (why, not just that).

  • Agent reads a new source, extracts facts, discusses with the user (or proceeds unsupervised, per the bundle's own policy in CLAUDE.md

/AGENTS.md

). - Creates or updates the relevant concept file(s): body prose, relations

,sources

,generated

. A single source may touch several concepts. Hard rule — assert both directions of a relation in the same pass. When Antony Blinken's concept file gets{ predicate: secretary_of, object: /entities/us-department-of-state.md }

, the Department's concept file gets the matching{ predicate: secretary, object: /entities/antony-blinken.md }

in the same ingest step. Chosen over computing inverses at query time from a predicate-inverse lookup table: it keeps the query layer (§6) simple, at the cost of ingest needing to touch both concepts. §7.3's asymmetric-relations lint check exists specifically to catch a violation of this rule.- Regenerates wiki.db

(§5) andtimeline.md

(§3) unconditionally — every ingest, no exceptions, regardless of how small the change — and anyindex.md

entries affected. - Appends an entry to log.md

(OKF §9). Never hand-editstimeline.md

,wiki.db

, or auto-generatedindex.md

entries directly — they are step-4 outputs, not inputs.

The context_for(question)

pipeline, §6. This is the hot path and should be fast — it's a handful of indexed SQLite queries, not a full-bundle scan.

A synthesized answer worth keeping — a comparison, an analysis, a connection the retrieval surfaced — should be filed back as a new Concept page (§4.1) rather than left in chat history, the same compounding behavior the original gist specified ("good answers can be filed back into the wiki as new pages... your explorations compound in the knowledge base just like ingested sources do"). Filing back runs the same steps as §7.1: write the page, regenerate wiki.db

/timeline.md

, update index.md

, log it.

Periodic health-check pass over wiki.db

, expressible as SQL:

  • Orphan concepts: no inbound relations

and no inbound prose links. - Broken relation targets: object

paths that don't resolve to an existing concept (tolerate per OKF §6.1, but surface for review). - Stale concepts: stale_after

in the past. - Predicate drift: near-duplicate predicates that should be merged ( invested_in

vsinvested-in

vsbacks

) — prune during lint, don't let the vocabulary fork. - Contradictions: conflicting relations

asserted about the same subject/predicate pair from different sources. Exempts a small, hand-maintained set of predicates that are inherently multi-valued (has_part

— a subject can genuinely have several parts at once), so the check only fires on predicates expected to hold one object. - Asymmetric relations: a relations

entry with no corresponding reverse entry on the object concept — see the hard rule in §7.1; this is the check that catches a violation of it.

Lint detects; it does not itself rewrite files. A predicate-synonym finding, for example, gets fixed by a §7.4 Reindex-triggering bulk edit or a normal ingest-style edit, not by the lint pass itself.

A full rebuild of wiki.db

(§5), timeline.md

(§3), and wiki-trigger-keywords.txt

(§3, §12 — every concept's title

and tags

, deduped) from every concept file, independent of any single content change. Idempotent — running it twice with no intervening edits produces the same output. Needed for:

Embedding backend changes(§9.2): switching the embedding model (e.g. upgradingnomic-embed-text

to a newer version) requires re-embedding every concept, not just recently-touched ones.Recovery:wiki.db

is a disposable cache (§5); if it's lost or corrupted, Reindex is how it comes back, with no dependency on ingest history.Bulk migrations: executing a predicate rename or OKF-version upgrade that lint (§7.3) flagged, across every affected file, then rebuilding the index in one pass rather than file-by-file ingest edits.

Ingest, Query, and Lint have no feedback loop on retrieval quality — only on data integrity. This matters specifically because context_for()

feeds an LLM automatically rather than a human reading the wiki directly, so a ranking regression can go unnoticed. A commenter on the original Karpathy gist measured this failure mode directly: a compiled wiki answered confidently even for questions from a domain the wiki had never covered, with no signal anywhere that the answer wasn't grounded — every query returned a full set of "relevant" hits regardless.

Maintain a fixed regression set of questions with expected seed concepts and/or expected facts, split between in-bundle and deliberately out-of-bundle questions. Periodically (or on every Reindex) run context_for()

against the set and check: does the right concept still seed first (catches the exact-title/BM25/vector-ranking regressions from §10.3), does an out-of-bundle question return a clear "no confident match" rather than a plausible-looking wrong answer, and has trust-tier ranking (§6.4) held. This is the retrieval-quality analog of §7.3's data-integrity checks — both are lint in spirit, but this one needs a labeled question set to run against, not just a schema/frontmatter scan.

A recovery/refresh operation, distinct from Ingest (§7.1) and Reindex (§7.4): discards concept files that were produced by ingesting a source now sitting in references/

, and reconstructs them by re-running Ingest against those same references — e.g. after fixing a bad extraction, adopting a new type

convention (§8), or wanting a later ingest process's quality applied to older pages. Unlike Reindex, this does touch concept files, deliberately — the goal is regenerating them, not just the derived wiki.db

/timeline.md

cache.

Scope, using the §4.2 provenance convention. A concept file is ingest-derived — and therefore in scope for replacement — only if its sources

frontmatter cites at least one references/...

-rooted resource

. A concept file with no sources

block, or whose sources are all external URLs never mirrored locally, is out of scope by construction: that is the shape of a filed-back Concept page (§4.1, §7.2) built from discussion and general web search rather than a raw ingest of a saved document, and this operation must never touch it.

Procedure:

  • Identify candidates by scanning every concept's sources

for areferences/...

-rootedresource

— deterministic, run through the CLI (rebuild-candidates

, reads fromwiki.db

) rather than a raw shell scan, same rationale as the rest of §7's operations. - Confirm scope with the user before deleting anything — an ingest-derived file may since have accumulated a verified: {by: "human:..."}

stamp or hand edits worth preserving; a passage worth keeping that isn't grounded in a reference should be moved into its own Concept page first, since Concept pages stay out of scope here by design. - Delete the confirmed concept files. references/

itself,log.md

, and every out-of-scope concept file are untouched. - Re-run Ingest (§7.1) over the same references/

material, one source at a time — this is what actually reconstructs the concept files, now under the bundle's current conventions (§8). - Reindex (§7.4) once every deleted concept file has been recreated.

  • Run Lint (§7.3), then manually confirm prose links inside preserved Concept pages still resolve — Lint's broken-target check covers typed relations

only, not markdown prose links, so a concept recreated under a slightly different path won't necessarily surface there. - Append one log.md

entry for the whole rebuild, not one per re-ingested reference — this is a single deliberate operation, not N separate ingests.

Lint (extends §7.3). One check, using the same provenance scan as step 1 above, run in the opposite direction: unreviewed-reference — a file under references/

that no concept's sources[].resource

cites anywhere in the bundle. This is a deterministic set difference (references/

directory listing minus every cited resource

path), not a freshness or mtime check — there is no ingestion-tracking state anywhere in this design (§9.3: regenerate, never persist-and-drift), so this is the closest thing to "what's new in references/

" the pipeline offers. It is a prompt to look, not a verdict: a reference can legitimately have zero citing concepts because it was reviewed and deliberately never promoted to a concept page — out of the bundle's domain, or mirrored for context only. Don't treat every finding as missed work; check the reference before assuming it needs ingesting.

A concept-file-level mechanism for recording that one whole concept file has been replaced by another — not a per-fact confidence score with continuous decay (that's out of scope; see the note at the end of this section). Distinct from two things already in this spec that look similar but do less:

stale_after

(§4.2, §6.4) marks a fact as expired bydate, with no record of what, if anything, replaced it.- Lint's contradiction

check (§7.3)detectsthat two relations conflict; it does not resolve which one is current or link them.

Supersession closes that gap for the concept-file granularity: an explicit, dated, bidirectional pointer from an outdated concept file to the one that replaced it, with the old file preserved (not deleted) and excluded from default retrieval rather than silently left ambiguous.

When to use it. Only when the old concept file remains independently meaningful as a historical record — a former officeholder, a superseded org structure, a deprecated architecture decision. If the old content was simply wrong (a bad extraction, a typo, a source that turned out to be inaccurate), fix it in place via a normal Ingest edit (§7.1) — no supersession chain. Rule of thumb: if "what did we believe before <date>

?" is a legitimate question about this concept, use supersession; if it isn't, just correct the file.

Frontmatter fields (extension, like relations

— not OKF core):

status: superseded
superseded_by: /entities/bill-clinton-presidency.md
superseded_at: "1993-01-20"   # optional; defaults to the successor's generated.at if absent
supersedes: [/entities/george-hw-bush-presidency.md]

superseded_by

: bundle-relative path (with.md

, like arelations

object

— §4.3) to the one concept file that replaces this one. A concept has at most onesuperseded_by

at a time.supersedes

: array — a successor may replace more than one prior concept file (e.g. a merge).- Setting superseded_by

without also settingstatus: superseded

is invalid —status

is what §6.4's default filtering and §7.3's lint actually key off;superseded_by

supplies the destination.

Hard rule — assert both directions in the same edit, exactly the discipline §7.1 requires for relations

: when A gets superseded_by: B

, B gets supersedes: [..., A]

in the same pass. §7.7's asymmetric-supersession

lint check below exists specifically to catch a violation of this rule, mirroring §7.3's existing asymmetric-relation

check.

Chains. Supersession may chain (A → B → C — e.g. George H. W. Bush → Bill Clinton → George W. Bush) — no automatic mid-chain resolution is attempted; a query-time chain walk (below) always resolves to the end of the chain. Bound the walk (e.g. 5 hops) to survive an accidentally-introduced cycle without hanging — the supersession-cycle

lint check below is what's supposed to catch that case before it ever reaches query time.

Retrieval integration (extends §6.4 and §6.3):

  • A concept file with status: superseded

is excluded from seed matching by default (§6.1) — exact-title, BM25, and vector search must not surface it as a top-level seed. Instead, resolve transparently to the end of itssuperseded_by

chain, the concept-file analogue of howstale_after

already excludes a fact rather than presenting it as current. - This is exclude-from-seeding, not delete-from-graph: a superseded concept remains a valid relations

object

on other, still-active concepts (e.g.{ predicate: preceded, object: /entities/george-hw-bush-presidency.md }

) and must resolve normally, not be flagged asbroken-target

. - During bounded graph expansion (§6.3), a hop that lands on a superseded concept is down-weighted, not excluded outright — a SUPERSEDED_MULTIPLIER

(e.g.0.15

) alongside the existingtrust_multiplier

in the scoring formula, preserving "deprioritized, not deleted" rather than adding a second binary include/exclude gate next tostale_after

's. - An explicitly retrospective question ("what was true before <date>

?", "who was President before Bill Clinton?") is exactly what a superseded concept still needs to answer — treat this as a new case for §6.2's intent detection (aHISTORICAL

bucket, or a modifier onWHEN

) that lifts the default seeding exclusion for that query only, rather than ever deleting the content. - Serialization (§6.5): a fact drawn from a superseded concept renders with an explicit marker so the answering LLM can never present it as current without the reader being able to tell:

- George H. W. Bush inaugurated President, as of 1989-01-20 (superseded by /entities/bill-clinton-presidency.md as of 1993-01-20)

Lint (extends §7.3) — two new checks, same "detects, does not rewrite" discipline as the rest of §7.3:

dangling-supersession:superseded_by

doesn't resolve to an existing concept file. Parallel tobroken-target

, scoped to this field.asymmetric-supersession:superseded_by

set on A with no matchingsupersedes

entry on the target, or vice versa. Parallel toasymmetric-relation

; catches a violation of the hard rule above.supersession-cycle: walkingsuperseded_by

from any concept revisits a concept already in the chain. Catches an accidental loop before it breaks the bounded query-time walk above.

** entities/index.md.** A superseded concept's entry is not removed — it's still real prior content, worth being able to find by browsing — but gets annotated with the successor path, generated the same non-hand-edited way as every other index entry (§3, §7.1 step 5).

Interaction with §7.6. Orthogonal: being status: superseded

does not exempt a concept file from Rebuilding from references — scope there is decided purely by the provenance convention (§4.2), independent of lifecycle status.

Non-goal, explicitly. This is a discrete, binary predecessor→successor pointer at the concept-file granularity — not the broader "continuous confidence score with time-decay and reinforcement" some wiki-pattern writeups propose at the individual-fact level. That's a materially different, heavier mechanism (per-fact provenance counting, a decay function, a reinforcement event); nothing here should be read as a first step toward it without a separate design pass.

Both type

(OKF-native) and predicate

(this spec's extension) are open vocabularies, not fixed enums — consistent with OKF's own "producers pick descriptive values, consumers tolerate unknown ones" stance (§4.2). Concept

, plus the Entity-page starter set — Person

, Organization

, Product

, Place

, Country

— are the type

values every bundle should expect to use from day one (§4.1); use the specific value whenever a page clearly fits one — a person's file gets type: Person

, not generic Entity

— and reach for plain Entity

only as a fallback when none of the starter set applies. Everything else (Playbook

, Metric

, Attested Computation

, or a bundle-specific type) is genuinely open and grows as needed. Document predicates as they're coined during ingest in CLAUDE.md

/AGENTS.md

, prune synonyms during lint (§7.3). Do not pre-design an ontology before there's a bundle to observe.

Surfaced as open questions during design, since closed. Recorded with rationale rather than deleted, so the implementing agent knows why, not just what:

founded

/ founded_by

, invested_in

/ backed_by

: hard ingest rule, both directions asserted in the same pass (§7.1 step 3), enforced by the asymmetric-relations lint check (§7.3). Rejected alternative: computing inverses automatically from a predicate-inverse lookup table at query time, which would need only one direction hand-asserted. Chosen against, because it pushes complexity into the query layer (§6) for every read, in exchange for saving one extra frontmatter edit at ingest — the wrong trade given queries vastly outnumber ingests over the bundle's life.

Decided (§6.1): nomic-embed-text

via a local Ollama endpoint (localhost:11434

) — the same convention Mnemon uses, evaluated and kept even though Mnemon's storage engine was not (§5.1). During early prototyping, a TF-IDF vector stood in for the mechanism (RRF fusion, ranking) because the sandbox used to build the first prototype couldn't reach Hugging Face Hub to pull model weights — an environment constraint, not a design choice. TF-IDF was never carried forward: it's corpus-relative (needs refitting as the bundle grows, unlike static per-document embeddings) and, being pure term-frequency, won't generalize to paraphrased questions the way dense embeddings should. The finished reference implementation calls Ollama directly for every embedding (ollama_embed

, wired at both index-build and query time) — there is no TF-IDF fallback in the production path.

wiki.db

and timeline.md

are regenerated on every ingest, unconditionally (§7.1 step 4) — never hand-edited, never treated as a second source of truth. This is the property the rest of the design leans on (§1, §3): concept frontmatter is authoritative, everything else is a rebuildable cache. Dropping router.md

(§3) removed the one file in the bundle that didn't follow this rule.

These were hit while prototyping this exact design and cost real debugging time — treat as hard rules, not suggestions.

YAML 1.1 parses bare on

/ off

/ yes

/ no

as booleans (the "Norway problem"). on: 2009-07-06

silently becomes key true

. Use date

(§4.2).

An unquoted 2026-08-09

-style scalar risks being auto-parsed into a native date object under some YAML parsers' default schema, and naive downstream serialization can then produce a broken, non-ISO string instead of clean ISO text — silently breaking OKF's own ISO-8601 requirement for generated.at

(§5.2). The reference implementation's frontmatter reader (lib/frontmatter.ts

) uses the yaml

npm package's YAML 1.2 core schema, which does not have this failure mode — verified directly, not assumed. Quote every date/datetime frontmatter value as a string regardless of which parser is in use anyway — it's the portable convention for any other tooling (a YAML 1.1 parser, an editor plugin) that might read this bundle, and it costs nothing.

Validated directly: on a 4-concept test bundle, both FTS5 BM25 and a TF-IDF vector both ranked the wrong entity first for "Who is Marc Andreessen?" — because a related concept's page happened to mention "Marc Andreessen" in its own body text. Term-frequency and even embedding similarity alone cannot reliably distinguish "this page is about X" from "this page merely references X" on a cross-linked wiki, which is the normal case, not an edge case. §6.1's exact-title signal exists specifically to fix this and must not be dropped as a "redundant" simplification later.

Also validated directly: a naive substring check for this signal is wrong, not just imprecise — a concept titled "AI" seeded on any question containing "explain" (expl-AI-n

), and "Go" seeded on "ago". Match at a whole-word/phrase boundary (Unicode letter/number boundaries, not ASCII \b

, so this also holds for non-English titles), never plain .includes()

.

Validated directly against a 66-concept real-world bundle (Northlight, Aug 2026), not a synthetic one: "How do I fix a flat bicycle tire?" was gated as confident because FTS5 MATCH

returned a nonempty result set — the original rule — and its top-ranked bm25() result outscored a genuinely relevant question's top-ranked result by raw score magnitude. The match was a coincidence: "flat" hit the target concept's "apartment" sense inside an unrelated housing-policy essay, "fix" is a generic word, and neither "bicycle" nor "tire" appeared anywhere in that document at all — 2 of 4 query words genuinely present, covering half the question, yet it out-scored a real match by bm25()'s raw number.

The mechanism: bm25()'s IDF term rewards rarity. A document containing one coincidentally rare query word scores higher than a document covering several common ones, regardless of how much of the question it actually accounts for — this is not a tunable edge case, it is what IDF does by design, so no threshold on the raw score fixes it.

Measured across this same bundle: every genuinely relevant question tested had 100% of its extracted content words present in its correct top-ranked match; every deliberately off-domain natural-English question tested had ≤50%. Gate BM25 confidence on word coverage of the top match — the fraction of the question's content words that actually appear in it, checked independently of bm25()'s score — not on score magnitude and not on mere nonemptiness. >0.5

(strictly more than half) separates the two clusters with margin on the cases tested; retune per-bundle via evaluate.json

(§7.5) the same way the vector threshold is retuned. This does not change which candidates get ranked by BM25 — only whether a BM25-only match is trusted enough to answer at all.

Everything in §1–§9 targets the scale this pattern is meant for — hundreds to low thousands of concepts (§2's non-goal: this is deliberately not a graph database). Nothing in this section should be built now. It's recorded so the trigger conditions are explicit if the bundle ever outgrows that range, rather than discovered as a production incident.

Document count alone is not the right scaling variable to watch — it's a proxy that tracks some components and not others.

Component Degrades along Not along Rough threshold Fix
FTS5 keyword search doc count Scales to millions; no revisit needed.
relations JSON column
Edge count (concepts × avg relations/concept) — JSON1 has no index, so json_each is scanned per query.
Doc count in isolation Tens of thousands of edges, once §6.3's multi-hop expansion starts self-joining over an unindexed view. §11.2
embeddings table
Vector row count — the reference implementation's cosine-similarity scorer is a brute-force in-process loop over every row today. Doc count in the abstract, though it tracks 1:1 (one vector per concept) Low thousands of vectors, where per-query linear scan starts adding real latency. §11.3
Graph traversal Edge density and required traversal depth. Doc count Only if requirements shift to unbounded-depth or whole-graph algorithms — not reached by §6.3's bounded 1–2 hop retrieval. §11.4

When: multi-hop queries (§6.3) measurably slow down doing self-joins over json_each

— in practice, tens of thousands of edges (a few thousand concepts at typical relation density).

What: a build step folded into Reindex (§7.4) that flattens every concept's relations

array into rows of a normalized, indexed table:

create table edges (
  subject_path   text not null,
  predicate      text not null,
  object_path    text,   -- null if the object is a literal, not a concept
  object_literal text,   -- populated when the object is not a concept path
  date           text,
  source_id      text
);
create index idx_edges_subject on edges(subject_path);
create index idx_edges_object  on edges(object_path);

Concept frontmatter's relations

field (§4.3) is unchanged — this is a query-layer materialization, not a file-format change. context_for()

's expansion query swaps json_each

/json_extract

calls for plain indexed joins against edges

; same logic, faster execution.

When: cosine-similarity retrieval (§6.1) adds noticeable per-query latency — roughly low thousands of concepts, since the current scorer is an unindexed linear scan over every embedding row, run in-process on each query.

What: replace the plain embeddings(path, vector)

table with a sqlite-vec

vec0

virtual table and query via SQL KNN instead of an in-process loop. It's real and actively maintained — a single dependency-free C extension — but still pre-v1 (0.1.10-alpha at time of writing): pin a version and expect this migration to need revisiting before it hits 1.0. Embedding generation itself (Ollama + nomic-embed-text

, §9.2) is unaffected; only storage and query of the resulting vectors change.

Not triggered by concept count. Triggered by either:

  • Edge density growing far past a sparse wiki-style graph, dense enough that indexed-table joins (§11.2) stop being fast even bounded to 1–2 hops.
  • A requirement change: if context_for()

ever needs unbounded-depth traversal or whole-graph algorithms (shortest path across the entire bundle, centrality, community detection) rather than the bounded, budget-constrained expansion specified in §6.3 — a different query shape that SQL joins express poorly regardless of indexing, where Neo4j/Cypher-class engines earn their keep.

If neither condition holds, §11.2's indexed edge table keeps SQLite adequate indefinitely. §6's retrieval design was built to never need more than a bounded hop count in the first place, which is exactly the case a relational engine handles well.

§7.2 assumes the calling agent judges when a question is in-bundle and invokes context_for()

deliberately. That judgment can silently fail to fire in practice — a full stretch of clearly in-domain conversation can pass with zero automatic queries, because nothing forces the check. This section specs an optional mitigation, external to this bundle's own CLI: a harness-level hook (Claude Code's settings.json

hooks — UserPromptSubmit

) that nudges the agent to query when the incoming message plausibly touches the bundle. Not installed by /add-llm-okf-graph-wiki-vb

by default — a per-group opt-in, since it adds a script and a settings.json

entry the operator should choose to add, not something every bundle needs.

The hook's only job is a cheap, deterministic yes/no on whether the incoming message is in-bundle. It must not run full context_for()

(§6) itself — that pipeline (FTS + vector embedding + bounded graph expansion) is comparatively expensive, and would be paid on every turn regardless of topic if the hook triggered it directly, including the majority of turns that have nothing to do with the bundle's domain. On a match, the hook should only emit a short nudge string reminding the agent to consider running query

— the agent still pays for context_for()

only if and when it actually acts on the nudge.

wiki-trigger-keywords.txt

(§3, §7.4) is the input: every concept's title

and tags

, deduped and regenerated on every Reindex — same "cache, never hand-edited" treatment as wiki.db

/timeline.md

(§9.3). Deliberately narrower than a full free-text scan: proper nouns and specific tags have a much lower false-positive rate than generic topic words matched against arbitrary prose (validated directly — see §12.4).

A plain shell script registered as an additional entry under the harness's hooks.UserPromptSubmit

array (alongside any hook already registered there, not replacing it):

  • Read the harness's stdin JSON payload, extract the prompt text ( .prompt

, falling back to other field names in case the harness's schema differs — fail silent-open, i.e. no nudge, on a schema mismatch rather than erroring). - Loop grep -qiFw

(fixed-string, whole-word, case-insensitive) over each non-comment, non-blank line ofwiki-trigger-keywords.txt

. - On any match, print one line naming the matched concept(s) and suggesting the query

op. Exit 0 either way — this hook must never block the turn.

The keywords file lives inside the bundle (§3); the hook script itself lives in the group's own workspace (/workspace/agent/hooks/

by convention), not under the shared, read-only skill directories (/app/skills/...

) — those are mounted read-only to the agent and can't host a script that needs per-group editing.

Concretely, the registration:

{
  "hooks": {
    "UserPromptSubmit": [
      { "hooks": [{ "type": "command", "command": "<existing-hook-if-any>" }] },
      {
        "hooks": [
          {
            "type": "command",
            "command": "/workspace/agent/hooks/wiki-trigger.sh",
            "timeout": 5
          }
        ]
      }
    ]
  }
}

A short timeout

matters: this hook must never meaningfully delay a turn — if it hangs, the harness should cut it off well before it becomes noticeable.

Three were hit building this pattern, worth carrying forward as hard rules rather than rediscovering:

Whole-word matching, not substring. A trigger list sourced fromtags

(§7.4) routinely includes short slugs (ai

,ft

, and similar). Plain substringgrep -F

matches those inside unrelated words (ai

inside "again,"ft

inside "often"), producing near-constant false positives. Use-w

(grep -qiFw

).Harness-injected wrapper text can collide with keywords. Some harnesses prepend boilerplate to every prompt (e.g. a context/metadata tag carrying the install timezone) before the hook ever sees it. A keyword that happens to match a token inside that boilerplate (a country or city name embedded in a timezone string, for instance) fires on every single turn regardless of what the user actually typed. Strip known wrapper markup from the extracted prompt text before matching.The reference container doesn't install it by default — a script that assumes it's onjq

is not guaranteed present in the agent image.PATH

fails silently (empty prompt extracted, hook effectively always a no-op) rather than erroring loudly. Either addjq

via the bundle's owninstall_packages

self-mod path (§ container skill's self-mod docs) before relying on it, or parse the stdin JSON with something already guaranteed available (e.g. a one-linenode -e

fallback) so the hook degrades to "no nudge" only on a genuine schema mismatch, not on a missing binary.

The gate itself is free relative to context_for()

— a loop of grep

calls over a short keyword list, a few milliseconds, no LLM or embedding call. Token cost only appears on an actual match, and even then is just the fixed reminder string (no retrieved facts injected).

A finished TypeScript implementation (runs on Bun) demonstrates every mechanism above end-to-end, one file per concern:

build-index.ts

— reindex: walksentities/

/computations/

, rebuildsconcepts

,concepts_fts

(FTS5), andembeddings

from scratch (§5, §7.1 step 4).context-for.ts

context_for(question)

: hybrid seed matching (exact title + FTS5 + vector cosine, fused), bounded graph expansion, trust- and budget-aware filtering (§6).lib/ollama-embed.ts

— realnomic-embed-text

embeddings via a local Ollama endpoint, plus the cosine-similarity scorer (§9.2). No TF-IDF or other placeholder in this path.lib/rrf.ts

— reciprocal rank fusion across the three seed signals.lib/graph-score.ts

— bounded multi-hop expansion and predicate-aware scoring (§6.2–§6.3).lib/frontmatter.ts

— the YAML 1.2 core-schema frontmatter reader/writer (see §10.2's note on the YAML 1.1 date-coercion pitfall it sidesteps).lint.ts

,evaluate.ts

,timeline.ts

,keywords.ts

— the remaining CLI operations (§7.3–§7.4);keywords.ts

regenerateswiki-trigger-keywords.txt

(§12's optional hook input) from every concept'stitle

andtags

, skipping non-string tag values (frontmatter documentstags

as strings, but a bare unquoted number in YAML — e.g. a year written as2009

instead of"2009"

— parses as a JSON number; caught in practice, not hypothetical, and also the right call independent of the parse-safety angle, since a bare number is a high-false-positive trigger keyword regardless of type).

Each lib/

module has unit tests alongside it. Treat this as a direct, runnable reference for shape and query patterns in any language/runtime — nothing in it is a placeholder standing in for a not-yet-built mechanism.

── more in #artificial-intelligence 4 stories · sorted by recency
── more on @andrej karpathy 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/okf-graph-wiki-agent…] indexed:0 read:37min 2026-08-14 ·