Skip to content

Instantly share code, notes, and snippets.

@karpathy
Created April 4, 2026 16:25
Show Gist options
  • Select an option

  • Save karpathy/442a6bf555914893e9891c11519de94f to your computer and use it in GitHub Desktop.

Select an option

Save karpathy/442a6bf555914893e9891c11519de94f to your computer and use it in GitHub Desktop.
llm-wiki

LLM Wiki

A pattern for building personal knowledge bases using LLMs.

This is an idea file, it is designed to be copy pasted to your own LLM Agent (e.g. OpenAI Codex, Claude Code, OpenCode / Pi, or etc.). Its goal is to communicate the high level idea, but your agent will build out the specifics in collaboration with you.

The core idea

Most people's experience with LLMs and documents looks like RAG: you upload a collection of files, the LLM retrieves relevant chunks at query time, and generates an answer. This works, but the LLM is rediscovering knowledge from scratch on every question. There's no accumulation. Ask a subtle question that requires synthesizing five documents, and the LLM has to find and piece together the relevant fragments every time. Nothing is built up. NotebookLM, ChatGPT file uploads, and most RAG systems work this way.

The idea here is different. Instead of just retrieving from raw documents at query time, the LLM incrementally builds and maintains a persistent wiki — a structured, interlinked collection of markdown files that sits between you and the raw sources. When you add a new source, the LLM doesn't just index it for later retrieval. It reads it, extracts the key information, and integrates it into the existing wiki — updating entity pages, revising topic summaries, noting where new data contradicts old claims, strengthening or challenging the evolving synthesis. The knowledge is compiled once and then kept current, not re-derived on every query.

This is the key difference: the wiki is a persistent, compounding artifact. The cross-references are already there. The contradictions have already been flagged. The synthesis already reflects everything you've read. The wiki keeps getting richer with every source you add and every question you ask.

You never (or rarely) write the wiki yourself — the LLM writes and maintains all of it. You're in charge of sourcing, exploration, and asking the right questions. The LLM does all the grunt work — the summarizing, cross-referencing, filing, and bookkeeping that makes a knowledge base actually useful over time. In practice, I have the LLM agent open on one side and Obsidian open on the other. The LLM makes edits based on our conversation, and I browse the results in real time — following links, checking the graph view, reading the updated pages. Obsidian is the IDE; the LLM is the programmer; the wiki is the codebase.

This can apply to a lot of different contexts. A few examples:

  • Personal: tracking your own goals, health, psychology, self-improvement — filing journal entries, articles, podcast notes, and building up a structured picture of yourself over time.
  • Research: going deep on a topic over weeks or months — reading papers, articles, reports, and incrementally building a comprehensive wiki with an evolving thesis.
  • Reading a book: filing each chapter as you go, building out pages for characters, themes, plot threads, and how they connect. By the end you have a rich companion wiki. Think of fan wikis like Tolkien Gateway — thousands of interlinked pages covering characters, places, events, languages, built by a community of volunteers over years. You could build something like that personally as you read, with the LLM doing all the cross-referencing and maintenance.
  • Business/team: an internal wiki maintained by LLMs, fed by Slack threads, meeting transcripts, project documents, customer calls. Possibly with humans in the loop reviewing updates. The wiki stays current because the LLM does the maintenance that no one on the team wants to do.
  • Competitive analysis, due diligence, trip planning, course notes, hobby deep-dives — anything where you're accumulating knowledge over time and want it organized rather than scattered.

Architecture

There are three layers:

Raw sources — your curated collection of source documents. Articles, papers, images, data files. These are immutable — the LLM reads from them but never modifies them. This is your source of truth.

The wiki — a directory of LLM-generated markdown files. Summaries, entity pages, concept pages, comparisons, an overview, a synthesis. The LLM owns this layer entirely. It creates pages, updates them when new sources arrive, maintains cross-references, and keeps everything consistent. You read it; the LLM writes it.

The schema — a document (e.g. CLAUDE.md for Claude Code or AGENTS.md for Codex) that tells the LLM how the wiki is structured, what the conventions are, and what workflows to follow when ingesting sources, answering questions, or maintaining the wiki. This is the key configuration file — it's what makes the LLM a disciplined wiki maintainer rather than a generic chatbot. You and the LLM co-evolve this over time as you figure out what works for your domain.

Operations

Ingest. You drop a new source into the raw collection and tell the LLM to process it. An example flow: the LLM reads the source, discusses key takeaways with you, writes a summary page in the wiki, updates the index, updates relevant entity and concept pages across the wiki, and appends an entry to the log. A single source might touch 10-15 wiki pages. Personally I prefer to ingest sources one at a time and stay involved — I read the summaries, check the updates, and guide the LLM on what to emphasize. But you could also batch-ingest many sources at once with less supervision. It's up to you to develop the workflow that fits your style and document it in the schema for future sessions.

Query. You ask questions against the wiki. The LLM searches for relevant pages, reads them, and synthesizes an answer with citations. Answers can take different forms depending on the question — a markdown page, a comparison table, a slide deck (Marp), a chart (matplotlib), a canvas. The important insight: good answers can be filed back into the wiki as new pages. A comparison you asked for, an analysis, a connection you discovered — these are valuable and shouldn't disappear into chat history. This way your explorations compound in the knowledge base just like ingested sources do.

Lint. Periodically, ask the LLM to health-check the wiki. Look for: contradictions between pages, stale claims that newer sources have superseded, orphan pages with no inbound links, important concepts mentioned but lacking their own page, missing cross-references, data gaps that could be filled with a web search. The LLM is good at suggesting new questions to investigate and new sources to look for. This keeps the wiki healthy as it grows.

Indexing and logging

Two special files help the LLM (and you) navigate the wiki as it grows. They serve different purposes:

index.md is content-oriented. It's a catalog of everything in the wiki — each page listed with a link, a one-line summary, and optionally metadata like date or source count. Organized by category (entities, concepts, sources, etc.). The LLM updates it on every ingest. When answering a query, the LLM reads the index first to find relevant pages, then drills into them. This works surprisingly well at moderate scale (~100 sources, ~hundreds of pages) and avoids the need for embedding-based RAG infrastructure.

log.md is chronological. It's an append-only record of what happened and when — ingests, queries, lint passes. A useful tip: if each entry starts with a consistent prefix (e.g. ## [2026-04-02] ingest | Article Title), the log becomes parseable with simple unix tools — grep "^## \[" log.md | tail -5 gives you the last 5 entries. The log gives you a timeline of the wiki's evolution and helps the LLM understand what's been done recently.

Optional: CLI tools

At some point you may want to build small tools that help the LLM operate on the wiki more efficiently. A search engine over the wiki pages is the most obvious one — at small scale the index file is enough, but as the wiki grows you want proper search. qmd is a good option: it's a local search engine for markdown files with hybrid BM25/vector search and LLM re-ranking, all on-device. It has both a CLI (so the LLM can shell out to it) and an MCP server (so the LLM can use it as a native tool). You could also build something simpler yourself — the LLM can help you vibe-code a naive search script as the need arises.

Tips and tricks

  • Obsidian Web Clipper is a browser extension that converts web articles to markdown. Very useful for quickly getting sources into your raw collection.
  • Download images locally. In Obsidian Settings → Files and links, set "Attachment folder path" to a fixed directory (e.g. raw/assets/). Then in Settings → Hotkeys, search for "Download" to find "Download attachments for current file" and bind it to a hotkey (e.g. Ctrl+Shift+D). After clipping an article, hit the hotkey and all images get downloaded to local disk. This is optional but useful — it lets the LLM view and reference images directly instead of relying on URLs that may break. Note that LLMs can't natively read markdown with inline images in one pass — the workaround is to have the LLM read the text first, then view some or all of the referenced images separately to gain additional context. It's a bit clunky but works well enough.
  • Obsidian's graph view is the best way to see the shape of your wiki — what's connected to what, which pages are hubs, which are orphans.
  • Marp is a markdown-based slide deck format. Obsidian has a plugin for it. Useful for generating presentations directly from wiki content.
  • Dataview is an Obsidian plugin that runs queries over page frontmatter. If your LLM adds YAML frontmatter to wiki pages (tags, dates, source counts), Dataview can generate dynamic tables and lists.
  • The wiki is just a git repo of markdown files. You get version history, branching, and collaboration for free.

Why this works

The tedious part of maintaining a knowledge base is not the reading or the thinking — it's the bookkeeping. Updating cross-references, keeping summaries current, noting when new data contradicts old claims, maintaining consistency across dozens of pages. Humans abandon wikis because the maintenance burden grows faster than the value. LLMs don't get bored, don't forget to update a cross-reference, and can touch 15 files in one pass. The wiki stays maintained because the cost of maintenance is near zero.

The human's job is to curate sources, direct the analysis, ask good questions, and think about what it all means. The LLM's job is everything else.

The idea is related in spirit to Vannevar Bush's Memex (1945) — a personal, curated knowledge store with associative trails between documents. Bush's vision was closer to this than to what the web became: private, actively curated, with the connections between documents as valuable as the documents themselves. The part he couldn't solve was who does the maintenance. The LLM handles that.

Note

This document is intentionally abstract. It describes the idea, not a specific implementation. The exact directory structure, the schema conventions, the page formats, the tooling — all of that will depend on your domain, your preferences, and your LLM of choice. Everything mentioned above is optional and modular — pick what's useful, ignore what isn't. For example: your sources might be text-only, so you don't need image handling at all. Your wiki might be small enough that the index file is all you need, no search engine required. You might not care about slide decks and just want markdown pages. You might want a completely different set of output formats. The right way to use this is to share it with your LLM agent and work together to instantiate a version that fits your needs. The document's only job is to communicate the pattern. Your LLM can figure out the rest.

@paulmchen

Copy link
Copy Markdown

Synthadoc Community Edition v0.7.0 is released.

Three new features focused on interactivity, speed, and visibility into the wiki's health:

  1. Local Web Chat UI: "synthadoc web" CLI starts a local chat interface in your browser, local-only, nothing leaves your machine. Use it the same way you use the CLI: ask questions about your wiki's domain and get streamed, cited answers.

Beyond content queries, the UI also auto-detects the health of your wiki (Explorer, Health Check, or Power User mode) and surfaces context-appropriate prompts: if your wiki has orphan pages or contradictions, those show up as suggested actions rather than generic hints. You can also give operational commands directly in the chat: "run lint", "show wiki status", "schedule ingest every night at 9 PM", the Action Agent parses those and executes them live against your wiki, with results shown inline.

→ Quick-start demo: Step 22 (https://github.com/axoviq-ai/synthadoc/blob/main/docs/user-quick-start-guide.md#step-22--use-the-web-chat-ui)

  1. Streaming Query: responses now stream token-by-token instead of waiting for the full answer. Works in the CLI, in Obsidian, and in the new web UI. The first token appears almost immediately after you ask; citations and confidence scores appear at the end of the stream once the full answer is assembled. If you need a cached static result, --no-stream still gives you the old behaviour.

→ Quick-start demo: Step 5 (https://github.com/axoviq-ai/synthadoc/blob/main/docs/user-quick-start-guide.md#step-5--query-the-pre-built-wiki-cli--obsidian)

  1. Query Result Caching: repeated queries return instantly from a local cache instead of hitting the LLM again. The cache is shared across the CLI, the web UI, and the Obsidian plugin, keyed on the question text, the current wiki epoch, and the model. The wiki epoch increments automatically on every ingest and every lifecycle state change, so the moment your wiki changes, all prior cached answers are bypassed immediately.

    → Quick-start demo: Step 23 (https://github.com/axoviq-ai/synthadoc/blob/main/docs/user-quick-start-guide.md#step-23--query-caching)

  2. Also in v0.7.0: expanded built-in hints covering orphan pages, adversarial warnings, and lifecycle state queries; live lint-report and wiki-status summaries available directly from the chat UI.

If any of this is useful or you have thoughts on the direction, feedback is very welcome, and a ⭐ always helps the project reach more people.

👉 https://github.com/axoviq-ai/synthadoc

@skyllwt

skyllwt commented Jun 6, 2026

Copy link
Copy Markdown

Love this LLM-Wiki idea? We built a full open-source system on it → AutoSci

Come and enjoy: https://github.com/skyllwt/AutoSci

autosci_fig1_v1-第 15 页 drawio png

Karpathy's pattern (immutable raw/ → an LLM-compiled wiki/ → a CLAUDE.md schema) is the exact foundation of AutoSci — an agent that turns the wiki into a research memory and then does autonomous science on top of it. All on Claude Code:

  • 📚 Ingest papers into a cross-linked wiki of concept/method/idea pages ([[wikilinks]], contradiction edges — the LLM-Wiki, fully realized)
  • 💡 Ideate → experiment → write: it reads its own memory to generate ideas, design + run experiments, draft the paper, and handle rebuttals
  • 🧬 Self-evolving memory: between projects it consolidates, re-weights, and re-links the wiki (a "sleep" phase)
  • 🕸️ Multi-agent DAGs for the hard reasoning steps

We've already used it to write 3 papers end-to-end. Fully open (MIT).

⭐ If this is where you want LLM-Wikis to go, a star genuinely helps us! and we welcome issues/PRs/Contributors
👉 https://github.com/skyllwt/AutoSci
📄 Paper: https://arxiv.org/abs/2605.31468

Demo: 【北大做了一个会自我进化的科研 Agent:AutoSci】 https://www.bilibili.com/video/BV19gVg6pEk6/?share_source=copy_web&vd_source=338de971cb27f42aaaf5d8bfdeed04b3

截图 2026-06-02 09-03-07

RED: 北大做了一个“越做科研越聪明”的AI科学家 北大团队... http://xhslink.com/o/2clEkgugEPw
复制后打开【小红书】查看笔记!

@vvvvvivekkk

Copy link
Copy Markdown

Interesting to see so many LLM-Wiki implementations emerge from this gist.

I took a slightly different direction with LLM-Wiki-v3.

Most systems stop at "LLM-maintained markdown + retrieval." V3 tries to make the wiki a reliable long-term knowledge substrate rather than just a generated documentation layer.

Key ideas:

• Markdown + Git are the only source of truth. Every vector index, BM25 index, graph, and retrieval artifact is disposable and fully rebuildable.

• Provenance is non-negotiable. Instead of confidence scores, every claim traces back to sources, spans, extractors, and timestamps. Trust is reconstructed, not guessed.

• Knowledge doesn't decay. Facts are superseded, not deleted. The system preserves what was believed, when it was believed, and what replaced it.

• Retrieval is hybrid by design (BM25 + dense + graph + RRF + reranking), but retrieval is treated as infrastructure—not memory itself.

• Autonomous writes are gated. Agents cannot directly mutate knowledge. Changes flow through evaluation, attribution checks, and audit trails before becoming part of the wiki.

• Every operation is auditable and reversible. The goal is knowledge evolution with accountability.

• The wiki is domain-agnostic. The real product is the schema. Change the schema and the same engine becomes a research wiki, company brain, scientific memory system, or personal knowledge substrate.

My belief is that the next generation of AI memory systems won't be vector databases or chat histories. They'll be versioned knowledge systems with provenance, evaluation gates, and explicit evolution paths.

LLM-Wiki-v3 is my attempt at exploring that direction.

https://github.com/vvvvvivekkk/LLM-Wiki-v3

ChatGPT Image Jun 6, 2026, 07_03_30 PM

@Sistema2D

Copy link
Copy Markdown

@witwaycorp

Copy link
Copy Markdown

Don't know guys... Sounds to me that once we have the data indexed and ready to go in a db-like system, I don't see much value for AI at that point. Sounds like a search engine would do the same job of retrieving the data given it's been distilled into easy to find info. I get the use of AI to generate the indexes but for frontend user-facing stuff, sounds like it's overkill. Reminds me of the blockchain conversation. The more you press people about their use case, more often than not, you end up with the same answer: well, sir, we already have something for that, it's called a database. :)

@lastforkbender

Copy link
Copy Markdown
# B-Spline KAN-CMS / Self-aware thru discrete combinator, SVD compression rating, spawned meta-controllers and weight budgeting
import json
import struct
import math
from functools import partial
from pathlib import Path
from typing import List, Tuple, Dict, Optional
import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F

USE_SPARSEMAX = False
GUMBEL_TEMP_INIT = 1.0
GUMBEL_TEMP_MIN = 0.1
GUMBEL_ANNEAL = 0.995
COMBINATOR_BRANCHING = 4
COMBINATOR_DEPTH = 2
SVD_COMPRESS_FORWARD = True
SVD_COMPRESS_GRADS = False
SVD_RANK_BUDGET = 4
SVD_EPS = 1e-6

def make_uniform_knot_vector(a: float, b: float, n_ctrl: int, degree: int, clamped: bool = True) -> np.ndarray:
    if clamped:
        interior = np.linspace(a, b, n_ctrl - degree + 1) if n_ctrl > degree else np.linspace(a, b, 2)
        start = np.full(degree, interior[0])
        end = np.full(degree, interior[-1])
        knots = np.concatenate([start, interior, end])
    else:
        knots = np.linspace(a, b, n_ctrl + degree + 1)
    return knots.astype(np.float32)

def bspline_basis_1d(x: torch.Tensor, knots: torch.Tensor, degree: int) -> torch.Tensor:
    K = knots.shape[0]
    n_basis = K - degree - 1
    device = x.device
    knots_t = knots.to(device)
    basis = []
    for i in range(n_basis):
        left = knots_t[i]
        right = knots_t[i+1]
        mask = ((x >= left) & (x < right)) | ((i == n_basis - 1) & (x == knots_t[-1]))
        basis.append(mask.to(torch.float32))
    B = torch.stack(basis, dim=-1)
    for d in range(1, degree + 1):
        B_d = torch.zeros_like(B)
        for i in range(n_basis):
            denom1 = knots_t[i + d] - knots_t[i]
            denom2 = knots_t[i + d + 1] - knots_t[i + 1]
            term1 = ((x - knots_t[i]).unsqueeze(-1) / denom1) * B[..., i:i + 1] if denom1 != 0 else 0.0
            term2 = ((knots_t[i + d + 1] - x).unsqueeze(-1) / denom2) * B[..., i + 1:i + 2] if denom2 != 0 else 0.0
            B_d[..., i:i+1] = term1 + term2
        B = B_d
    return B

def reflect_coords(x: torch.Tensor, a: torch.Tensor, b: torch.Tensor, reflect_flags: List[bool], soft_eps: float = 1e-4) -> torch.Tensor:
    x_out = x.clone()
    for j, rf in enumerate(reflect_flags):
        if not rf:
            continue
        aj = a[..., j] if a.ndim > 1 else a[j]
        bj = b[..., j] if b.ndim > 1 else b[j]
        span = torch.clamp(bj - aj, min=1e-6)
        t = (x[..., j] - aj) / span
        twopi = t - (2.0 * torch.floor(t / 2.0))
        diff = twopi - 1.0
        mirrored = torch.sqrt(diff * diff + soft_eps)
        mirrored = torch.clamp(mirrored, 0.0, 1.0)
        x_out[..., j] = aj + mirrored * span
    return x_out

class BSplineField(nn.Module):
    def __init__(self, dims: int, knots_per_dim: List[np.ndarray], degree: int, ctrl_point_dim: int, factorized_rank: Optional[int] = None, dtype: torch.dtype = torch.float32):
        super().__init__()
        self.m = dims
        self.degree = degree
        self.register_buffer("knots", torch.stack([torch.tensor(k.astype(np.float32)) for k in knots_per_dim]))
        self.grid_sizes = [len(k) - degree - 1 for k in knots_per_dim]
        self.ctrl_point_dim = ctrl_point_dim
        grid_count = int(np.prod(self.grid_sizes))
        if factorized_rank is None:
            self.ctrl_points = nn.Parameter(torch.randn(grid_count, ctrl_point_dim, dtype=dtype) * 0.01)
        else:
            self.grid_coeffs = nn.Parameter(torch.randn(grid_count, factorized_rank, dtype=dtype) * 0.01)
            self.U = nn.Parameter(torch.randn(factorized_rank, ctrl_point_dim, dtype=dtype) * 0.01)

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        batch_size = x.shape[0]
        basis_list = [bspline_basis_1d(x[..., j], self.knots[j], self.degree) for j in range(self.m)]
        letters = "ijklmnopqrstuvwxyz"[:self.m]
        input_shapes = ",".join([f"b{l}" for l in letters])
        output_shape = f"b{letters}"
        einsum_eq = f"{input_shapes}->{output_shape}"
        B = torch.einsum(einsum_eq, *basis_list)
        Bflat = B.reshape(batch_size, -1)
        if hasattr(self, "ctrl_points"):
            return torch.matmul(Bflat, self.ctrl_points)
        return torch.matmul(torch.matmul(Bflat, self.grid_coeffs), self.U)

    def grid_shape(self) -> Tuple[int, ...]:
        return tuple(self.grid_sizes)

class MetaController(nn.Module):
    def __init__(self, context_dim: int, ctrl_point_count: int, ctrl_point_dim: int, interval_dims: int, hidden: int = 256):
        super().__init__()
        self.ctrl_point_count = ctrl_point_count
        self.ctrl_point_dim = ctrl_point_dim
        self.interval_dims = interval_dims
        self.shared = nn.Sequential(
            nn.Linear(context_dim, hidden),
            nn.ReLU(),
            nn.Linear(hidden, hidden),
            nn.ReLU())
        self.delta_head = nn.Linear(hidden, ctrl_point_count * ctrl_point_dim)
        self.interval_head = nn.Linear(hidden, interval_dims * 3)
        self.gate_head = nn.Linear(hidden, ctrl_point_dim)

    def forward(self, z: torch.Tensor) -> Dict[str, torch.Tensor]:
        h = self.shared(z)
        intervals_raw = self.interval_head(h).reshape(*h.shape[:-1], self.interval_dims, 3)
        return {
            "deltas": self.delta_head(h),
            "a_delta": intervals_raw[..., 0],
            "b_delta": intervals_raw[..., 1],
            "refl_prob": torch.sigmoid(intervals_raw[..., 2]),
            "gate": torch.sigmoid(self.gate_head(h))}

def sparsemax(input_t: torch.Tensor, dim: int = -1) -> torch.Tensor:
    input_shifted = input_t - input_t.max(dim=dim, keepdim=True)[0]
    z_sorted, _ = torch.sort(input_shifted, descending=True, dim=dim)
    zs_cumsum = z_sorted.cumsum(dim)
    k_array = torch.arange(1, input_t.size(dim) + 1, device=input_t.device, dtype=input_t.dtype)
    k_cond = 1 + k_array * z_sorted > zs_cumsum
    k = k_cond.sum(dim=dim, keepdim=True)
    zs_threshold = (zs_cumsum.gather(dim, k - 1) - 1) / k.type(input_t.dtype)
    return torch.clamp(input_shifted - zs_threshold, min=0.0)

class Selector(nn.Module):
    def __init__(self, in_dim: int, out_dim: int, hidden: int = 256, use_sparsemax: bool = False, temp_init: float = GUMBEL_TEMP_INIT, temp_min: float = GUMBEL_TEMP_MIN, anneal: float = GUMBEL_ANNEAL):
        super().__init__()
        self.net = nn.Sequential(nn.Linear(in_dim, hidden), nn.ReLU(), nn.Linear(hidden, out_dim))
        self.use_sparsemax = use_sparsemax
        self.register_buffer("temp", torch.tensor(temp_init))
        self.temp_min = temp_min
        self.anneal = anneal

    def forward(self, z: torch.Tensor, hard: bool = False) -> torch.Tensor:
        logits = self.net(z)
        if self.use_sparsemax:
            return sparsemax(logits, dim=-1)
        u = torch.rand_like(logits)
        g = -torch.log(-torch.log(u + 1e-20) + 1e-20)
        y = torch.softmax((logits + g) / self.temp, dim=-1)
        if hard:
            y_hard = torch.zeros_like(y).scatter_(-1, y.argmax(dim=-1, keepdim=True), 1.0)
            y = (y_hard - y).detach() + y
        return y

    def step_anneal(self):
        self.temp = torch.clamp(self.temp * self.anneal, min=self.temp_min)

class Combinator(nn.Module):
    def __init__(self, leaf_count: int, branching: int = COMBINATOR_BRANCHING, depth: int = COMBINATOR_DEPTH):
        super().__init__()
        self.leaf_count = leaf_count
        levels = []
        current = leaf_count
        for d in range(depth):
            parents = math.ceil(current / branching)
            P = torch.zeros(parents, current, dtype=torch.float32)
            for p in range(parents):
                start = p * branching
                end = min(start + branching, current)
                if end > start:
                    P[p, start:end] = 1.0 / (end - start)
            levels.append(P); current = parents
        self.levels = nn.ParameterList([nn.Parameter(l, requires_grad=False) for l in levels])
        self.level_gates = nn.ParameterList([nn.Parameter(torch.ones(1, l.shape[0])) for l in levels])

    def forward(self, leaf_probs: torch.Tensor) -> Tuple[torch.Tensor, List[torch.Tensor]]:
        cur = leaf_probs
        trans_mats = []
        for d, P in enumerate(self.levels):
            parent_probs = torch.matmul(cur, P.t())
            parent_probs = parent_probs * torch.sigmoid(self.level_gates[d])
            trans_mats.append(P)
            cur = parent_probs
        return cur, trans_mats

def batched_svd_lowrank(mat: torch.Tensor, rank: int) -> torch.Tensor:
    if mat.ndim == 2:
        U, S, Vt = torch.linalg.svd(mat, full_matrices=False)
        k = min(rank, S.shape[-1])
        return (U[:, :k] * S[:k]) @ Vt[:k, :]
    elif mat.ndim == 3:
        U, S, Vt = torch.linalg.svd(mat, full_matrices=False)
        k = min(rank, S.shape[-1])
        return (U[..., :k] * S[..., None, :k]) @ Vt[..., :k, :]
    else:
        raise ValueError("Unsupported tensor dimensional properties for SVD.")

class KANWeightGenerator(nn.Module):
    def __init__(self, spline: BSplineField, base_intervals: List[Tuple[float, float]], reflect_flags: List[bool],
                 selector: Optional[Selector] = None, combinator: Optional[Combinator] = None, top_count: Optional[int] = None,
                 svd_rank: int = SVD_RANK_BUDGET, compress_forward: bool = SVD_COMPRESS_FORWARD, compress_grads: bool = SVD_COMPRESS_GRADS):
        super().__init__()
        self.spline = spline
        self.register_buffer("base_a", torch.tensor([iv[0] for iv in base_intervals], dtype=torch.float32))
        self.register_buffer("base_b", torch.tensor([iv[1] for iv in base_intervals], dtype=torch.float32))
        self.reflect_flags = reflect_flags
        self.selector = selector
        self.combinator = combinator
        self.svd_rank = svd_rank
        self.compress_forward = compress_forward
        self.compress_grads = compress_grads
        grid_count = int(np.prod(spline.grid_shape()))
        if self.selector is not None and self.combinator is not None and top_count is not None:
            self.comb_to_leaf = nn.Linear(top_count, grid_count)
        self.proj = nn.Linear(grid_count * spline.ctrl_point_dim, spline.ctrl_point_dim)
        self._grad_hooks_installed = False

    def install_grad_hooks(self):
        if self._grad_hooks_installed or not self.compress_grads:
            return
        for p in self.spline.parameters():
            if p.requires_grad:
                p.register_hook(partial(self._grad_compress_hook, rank=self.svd_rank))
        self._grad_hooks_installed = True

    @staticmethod
    def _grad_compress_hook(grad: torch.Tensor, rank: int):
        if grad.ndim == 2:
            return batched_svd_lowrank(grad, rank)
        return grad

    def forward(self, coords: torch.Tensor, meta_out: Dict, context_z: Optional[torch.Tensor] = None) -> torch.Tensor:
        a = self.base_a + meta_out["a_delta"]
        b = self.base_b + meta_out["b_delta"]
        avg_rp = torch.mean(meta_out["refl_prob"], dim=0) if meta_out["refl_prob"].ndim > 1 else meta_out["refl_prob"]
        resolved_flags = [(prob > 0.5).item() for prob in avg_rp]
        coords_ref = reflect_coords(coords, a, b, resolved_flags)
        base_w = self.spline(coords_ref)
        deltas = meta_out["deltas"].reshape(base_w.shape[0], -1)
        if self.selector is not None and context_z is not None and hasattr(self, 'comb_to_leaf'):
            sel_probs = self.selector(context_z)
            top_probs, _ = self.combinator(sel_probs)
            leaf_mod = torch.sigmoid(self.comb_to_leaf(top_probs))
            leaf_mod_exp = leaf_mod.unsqueeze(-1).expand(-1, -1, self.spline.ctrl_point_dim).reshape(deltas.shape)
            deltas = deltas * leaf_mod_exp
        if self.compress_forward:
            try:
                deltas = batched_svd_lowrank(deltas, self.svd_rank)
            except Exception:
                pass
        delta_w = self.proj(deltas)
        w = base_w + delta_w
        gate = meta_out["gate"]
        w = w * (gate if gate.ndim == 2 else gate.mean(dim=0, keepdim=True))
        self.install_grad_hooks()
        return w

def save_kanbs(path: str, spline: BSplineField, json_sidecar: Optional[Dict] = None):
    p = Path(path)
    json_path = p.with_suffix(p.suffix + ".json")
    with open(p, "wb") as f:
        f.write(b'KANB')
        f.write(struct.pack("<IIIIII", 1, 1, spline.m, spline.degree, spline.ctrl_point_dim, 1 if hasattr(spline, "grid_coeffs") else 0))
        grid_shape = spline.grid_shape()
        f.write(struct.pack("<I", int(np.prod(grid_shape))))
        for g in grid_shape:
            f.write(struct.pack("<I", g))
        for k in spline.knots:
            arr = k.cpu().numpy().astype(np.float32)
            f.write(struct.pack("<I", len(arr)) + arr.tobytes())
        if hasattr(spline, "ctrl_points"):
            f.write(spline.ctrl_points.detach().cpu().numpy().tobytes())
        else:
            f.write(spline.grid_coeffs.detach().cpu().numpy().tobytes())
            f.write(spline.U.detach().cpu().numpy().tobytes())
    js = json_sidecar or {}
    js.update({"grid_shape": grid_shape, "degree": spline.degree, "ctrl_point_dim": spline.ctrl_point_dim})
    with open(json_path, "w", encoding="utf-8") as jf:
        json.dump(js, jf, indent=2)

def demo_train():
    torch.manual_seed(0)
    device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
    m, degree, ctrl_dim, ctx_dim = 2, 3, 16, 10
    grid_sizes = [6, 5]
    knots = [make_uniform_knot_vector(0.0, 1.0, gs, degree) for gs in grid_sizes]
    spline = BSplineField(m, knots, degree, ctrl_dim).to(device)
    grid_count = int(np.prod(spline.grid_shape()))
    meta = MetaController(context_dim=ctx_dim, ctrl_point_count=grid_count, ctrl_point_dim=ctrl_dim, interval_dims=m).to(device)
    selector = Selector(in_dim=ctx_dim, out_dim=grid_count, use_sparsemax=USE_SPARSEMAX).to(device)
    combinator = Combinator(leaf_count=grid_count, branching=COMBINATOR_BRANCHING, depth=COMBINATOR_DEPTH).to(device)
    dummy_leaves = torch.zeros(1, grid_count).to(device)
    dummy_top, _ = combinator(dummy_leaves)
    top_count = dummy_top.shape[-1]
    kg = KANWeightGenerator(spline, base_intervals=[(0.0, 1.0)] * m, reflect_flags=[True] * m,
                           selector=selector, combinator=combinator, top_count=top_count,
                           svd_rank=SVD_RANK_BUDGET, compress_forward=SVD_COMPRESS_FORWARD, 
                           compress_grads=SVD_COMPRESS_GRADS).to(device)

    opt = torch.optim.Adam(list(spline.parameters()) + list(meta.parameters()) + list(selector.parameters()) + list(kg.parameters()), lr=1e-2)
    for step in range(201):
        coords = torch.rand(8, m, device=device)
        z = torch.randn(8, ctx_dim, device=device)
        w = kg(coords, meta(z), context_z=z)
        target = torch.sin(coords.sum(dim=-1, keepdim=True)).expand(-1, ctrl_dim)
        loss = F.mse_loss(w, target)
        opt.zero_grad()
        loss.backward()
        opt.step()
        selector.step_anneal()
        if step % 50 == 0: print(f"Step {step:03d} | Loss: {loss.item():.6f} | Temp: {float(selector.temp):.4f}")
    save_kanbs("example_extended.kanbs", spline, json_sidecar={"demo": True, "extended": True})
    print("Execution complete.")

if __name__ == "__main__":
    demo_train()

@hereisSwapnil

Copy link
Copy Markdown

Inspired by this gist, I built memwiki but took it in a more focused direction: persistent memory specifically for AI coding agents (Claude Code, Cursor, Copilot, etc.).
The problem I was solving: every time you start a new coding session, your agent forgets why an architectural decision was made, what the established patterns are, which bugs are known, and what was left in-progress. I call it Agent Amnesia. It's the single most annoying thing about daily AI-assisted development.

The fix is one command:
npx memwiki init

This scaffolds a .memory/ folder and places hook files (.cursorrules, CLAUDE.md, AGENTS.md, .github/copilot-instructions.md) in your project root. Those hooks act as the automatic "front door" - every agent that opens the repo gets told: read .memory/wiki/hot.md before doing anything else.

The agent is instructed to update hot.md before ending every session, append to log.md, and create new domain-specific pages (e.g. .memory/wiki/auth-system.md) as the project grows - linked back to index.md so context windows don't overflow.

Where it diverges from Karpathy's general wiki pattern:
Karpathy's pattern is document-oriented you feed it articles, papers, notes, and it builds a knowledge base. memwiki is project-oriented the "sources" are your codebase, your decisions, your in-progress work. The wiki is the project's brain, not a research library. The agents write to it as a side effect of working, not as a separate ingestion step.
It also ships as an open standard rather than an opinionated app the .memory/ folder is just markdown + git. Works with any agent, any editor, any team member.

Zero lock-in. Zero dependencies. MIT.
👉 https://www.npmjs.com/package/memwiki
👉 https://github.com/hereisSwapnil/memwiki
👉 npx memwiki init

@Clod

Clod commented Jun 10, 2026

Copy link
Copy Markdown

Built an open-source take on this as a pair of marimo notebook apps,
partly derived from @lucasastorian's earlier llmwiki:
https://github.com/Clod/llmwiki-marimo (Apache-2.0)

Design choices that might interest folks here:

  • Local-first, no vector DB — sources, chunks, and wiki pages live in one SQLite file;
    retrieval is FTS5 plus reading whole wiki pages, which holds up fine at personal-wiki scale.
  • Citations as a contract — the chat agent can only answer through tools that return wiki
    pages or source chunks, so every fact in an answer cites the page it came from.
  • Human-in-the-loop growth — a good chat answer can be promoted to a permanent wiki page
    with one form, so the wiki grows by curation rather than bulk generation.
  • Lint & repair — broken links, orphan pages, and stale summaries are detected and fixable
    from the UI, treating the schema rules as checkable invariants.

Ships with a sample wiki so you can try the read/chat side without ingesting anything.

@wiltodelta

Copy link
Copy Markdown

Andrej, thank you for this. I spent the next year building my own version: 55 data sources, 7,405 contacts deduped across platforms, health biomarkers going back to 2015. The main extension is the relationship graph. Same pipeline idea, but the target query is "who do I know and how well" rather than "what do I know". Wrote up what I learned here: https://gist.github.com/wiltodelta/970cf102ea05cc89d5b85653a773cb7f

@paulmchen

Copy link
Copy Markdown

Synthadoc Community Edition v0.8.0 is released.

Three new features focused on conversational depth, provider choice, and operational visibility:

  1. Multi-turn Conversation: the Query Web UI now maintains full conversation history across turns within a session. Follow-up questions like "Which company did they work for?" or "What other inventions came out of that same lab?" resolve correctly because the server injects prior turns into every request and rewrites context-dependent phrases into standalone queries before retrieval. When a session grows long, the oldest turns are automatically compressed into a [Session summary] - context is preserved, not discarded. Sessions are persisted on the backend and survive page refreshes and restarts; the left sidebar shows your history as a collapsible tree with turn-count badges, and clicking any item fully restores that conversation so you can continue where you left off.

→ Quick-start demo: Step 22 (https://github.com/axoviq-ai/synthadoc/blob/main/docs/user-quick-start-guide.md#step-22--use-the-web-chat-ui)

  1. Qwen DashScope + Claude Opus 4.8 support: Qwen cloud models (qwen-plus, qwen-max, qwq-32b) now work via DashScope's OpenAI-compatible endpoint, new DashScope accounts get 1 million free tokens valid for 90 days, which makes it a practical zero-cost alternative to Gemini for users who have exhausted their free quota. Claude Opus 4.8 is also now an officially supported option for users who want maximum answer quality.

→ Provider setup: Appendix C (https://github.com/axoviq-ai/synthadoc/blob/main/docs/user-quick-start-guide.md#appendix-c--switching-llm-providers)

  1. Conversational job operations: the Action Agent now handles live job queries directly in the chat. Ask "show me job status" and you get a real-time table of all background jobs; candidate job IDs appear as chip buttons for one-click drill-down into a specific job's detail. Ask "show me failed and skipped jobs" and the table filters immediately, the conversation carries context across those operational turns, so a second chip click on the same list works without re-asking. This extends the chat-as-control-panel pattern from v0.7.0 (lint, scaffold, lifecycle) into job monitoring.

→ Quick-start demo: Step 22 multi-turn section (https://github.com/axoviq-ai/synthadoc/blob/main/docs/user-quick-start-guide.md#synthadoc-operation-multi-turn)

  1. Also in v0.8.0: settings gear in the web UI for per-request query timeout (no config file edit needed); query timeout propagated through the SSE stream so slow reasoning models fail fast with a clear error instead of hanging silently.

If any of this is useful or you have thoughts on the direction, feedback is very welcome, and a ⭐ always helps the project reach more people.

👉 https://github.com/axoviq-ai/synthadoc

@gowtham0992

Copy link
Copy Markdown

Link v1.4.0 is live.

Local, source-backed memory for AI agents.

What changed:

  • Installed CLI is now lnk to avoid the Unix/macOS link command collision.
  • Official CLI skills are included for agents that prefer lazy-loaded CLI workflows over MCP setup.
  • MCP remains supported for structured tool calls.
  • Windows PowerShell installers are included.
  • Homebrew, PyPI, and MCP Registry are updated to 1.4.0.
image

Install:

brew install gowtham0992/link/link
lnk demo
lnk serve link-demo

MCP package:

python3 -m pip install --upgrade link-mcp

Links:
GitHub: https://github.com/gowtham0992/link
Docs: https://gowtham0992.github.io/link/
MCP Registry: https://registry.modelcontextprotocol.io/?q=io.github.gowtham0992%2Flink
PyPI: https://pypi.org/project/link-mcp/

@studebaker8

Copy link
Copy Markdown

Is it common practice outside the US to jump on other people's bandwagons in attempt to ride their coattails and advertise their "products" in the process?

This is an insult to Karpathy's work.

@theafh

theafh commented Jun 14, 2026

Copy link
Copy Markdown

This idea was really inspiring! The “LLM-maintained wiki as a compiled knowledge layer” framing helped me make something I had been circling around more concrete.

I built a repo-local knowledge base that generalizes the pattern for projects: a wiki skill family for durable, focused knowledge, and a task skill family for repo-native work tracking.

The wiki side is meant to feel like a lightweight Confluence that lives with the code: raw sources stay separate, the agent maintains structured markdown pages, index.md and log.md make it discoverable, and schema rules keep future agents from treating it as just another notes folder.

The task side applies the same idea to execution state: markdown task files live alongside the repo, with status, provenance, implementation notes, audit state, and archive behavior. Roughly: Jira/Trello-style project memory, but plain text, git-backed, and directly usable by agents.

It is still intentionally simple: Make, shell, markdown, git. No database, no hosted service, no third-party software, no fluff! Just focused skills for every part of the workflow life cycle. The main goal is to make project knowledge and project work durable enough that a new agent session can rediscover the context without having to replay it to the human.

Repo: https://github.com/theafh/ai-modules

@kytmanov

Copy link
Copy Markdown

Synto v0.6.0

Synto v0.6.0 is out.
https://github.com/kytmanov/synto

Main addition: Real concept identity + curation tools

Concepts now have a stable entity_id under the hood, separate from the display name. Homonyms (same word, different domains) stop colliding or getting mangled. Ingest order no longer decides whether something becomes its own page - a later note that promotes a weak alias just surfaces a merge candidate in synto doctor / synto concept inspect instead of silently creating duplicates.

New commands to steer the graph the LLM is building

  • synto concept merge LOSER WINNER - moves sources + edges, retires the loser, absorbs its label as a blessed alias
  • synto concept split NAME --sense ... - carves it into multiple senses + leaves a disambiguation stub
  • synto concept unmerge, rename, inspect, keep

Also new

synto serve --transport streamable-http so remote machines or agents on your LAN can hit the MCP server (no built-in auth - run on a trusted network or behind a proxy; DNS rebinding protection is on; use --allowed-host for anything a reverse proxy forwards).

Same shape as before

  • works with local LLMs
  • great with Ollama and LM Studio
  • plain Markdown
  • Obsidian-friendly
  • no vector DB
  • no cloud required
  • multi-language

Star it if you want to support local-first AI tools.
Fork it if you want to build on it.

@SinghAbhinav04

Copy link
Copy Markdown

Love this LLM-Wiki idea? We built a full open-source system on it → AutoSci

Come and enjoy: https://github.com/skyllwt/AutoSci

autosci_fig1_v1-第 15 页 drawio png Karpathy's pattern (immutable raw/ → an LLM-compiled wiki/ → a CLAUDE.md schema) is the exact foundation of AutoSci — an agent that turns the wiki into a research memory and then does autonomous science on top of it. All on Claude Code:
  • 📚 Ingest papers into a cross-linked wiki of concept/method/idea pages ([[wikilinks]], contradiction edges — the LLM-Wiki, fully realized)
  • 💡 Ideate → experiment → write: it reads its own memory to generate ideas, design + run experiments, draft the paper, and handle rebuttals
  • 🧬 Self-evolving memory: between projects it consolidates, re-weights, and re-links the wiki (a "sleep" phase)
  • 🕸️ Multi-agent DAGs for the hard reasoning steps

We've already used it to write 3 papers end-to-end. Fully open (MIT).

⭐ If this is where you want LLM-Wikis to go, a star genuinely helps us! and we welcome issues/PRs/Contributors 👉 https://github.com/skyllwt/AutoSci 📄 Paper: https://arxiv.org/abs/2605.31468

Demo: 【北大做了一个会自我进化的科研 Agent:AutoSci】 https://www.bilibili.com/video/BV19gVg6pEk6/?share_source=copy_web&vd_source=338de971cb27f42aaaf5d8bfdeed04b3

截图 2026-06-02 09-03-07 RED: 北大做了一个“越做科研越聪明”的AI科学家 北大团队... http://xhslink.com/o/2clEkgugEPw 复制后打开【小红书】查看笔记!

I dont think we need a whole app for something which is done via model itself by a single prompt this is simply over engineering and wastage

@psinetron

Copy link
Copy Markdown

Hi Andrej, thank you for the brilliant write-up! This exact concept of flat-file agentic memory completely changes how we interact with coding assistants.

Inspired heavily by your thoughts here, I recently built and open-sourced EchoesVault — a persistent memory plugin for OpenCode that implements this pattern autonomously.

We took the "memory-as-markdown" idea and added a few strict engineering guardrails:

  • Event-Driven Logging: Instead of just dumping context at the end of the day, the AI uses background skills to autonomously log mid-session context switches and architectural decisions.
  • Google OKF Compliance: The memory structure enforces Google's Open Knowledge Format, ensuring the AI's memory is interoperable with standard parsers.
  • Obsidian Native: The vault renders perfectly in Obsidian, allowing developers to visually explore the AI's architectural graph.
  • Surgical Index Updates: To prevent token inflation, the agent uses array-based incremental updates for the index.md rather than rewriting the whole file.

Thank you for sparking the idea! If anyone in the thread is building similar agentic workflows or using OpenCode, I'd love your feedback on the implementation: https://github.com/psinetron/echoes-vault-opencode

@deepak-bhardwaj-ps

Copy link
Copy Markdown

One pattern I've noticed across many of the projects in this thread is that they focus heavily on what the agent remembers.

Smriti-MCP started from a slightly different question:

How does the agent remember consistently across sessions, tools, models, and runtimes?

Instead of treating memory as a feature inside a specific agent framework, Smriti exposes memory as an MCP-native service that any compatible agent can use.

A few design principles that shaped it:

  • Memory is externalised from the agent lifecycle.
  • Context should survive model changes.
  • Knowledge should be reusable across multiple agents.
  • Memory must be queryable, governed, and inspectable.
  • The memory layer should remain useful even when the orchestration layer changes.

What interests me most is that we're gradually moving from "prompt engineering" toward memory architecture.

The next generation of agent systems will not be differentiated by model choice alone.

They will be differentiated by:

  • what they know,
  • how they remember,
  • how they evolve knowledge over time,
  • and how reliably they can retrieve the right context at the right moment.

Smriti-MCP is my attempt to explore that direction as a portable memory layer for agent ecosystems.

Project:

Smriti MCP

@Motya-cobol

Copy link
Copy Markdown

Hello Andrei and the community.
I’m building a Markdown/Obsidian-style knowledge wiki for teaching a health informatics course, using Codex to help ingest sources, maintain links, and later query the wiki for presentations and curriculum design.

I ran into a token-efficiency problem that may be interesting: the token burn was not mainly from reasoning or output, but from repeated context loading. Codex kept re-reading files like AGENTS.md, wiki/index.md, manifests, handoffs, raw sources, and tool scripts across short sessions. Even small source-ingest tasks became expensive.

I’ve started moving toward a split workflow: deterministic Python scripts handle source intake, route indexes, validation, and compact handoffs; Codex is reserved for higher-value graph curation, synthesis, and judgment. I’m also using compact routing files instead of asking the model to scan the whole wiki.

I would be grateful for any advice on the right architecture for this kind of “wiki as working memory” setup. Specifically, how would you structure retrieval, summaries, graph edges, and agent instructions so Codex can reason over a rich knowledge base without repeatedly consuming the entire context?

Even a few pointers or design principles would be very helpful.

@yazanabuashour

Copy link
Copy Markdown

One thing I think the LLM-wiki pattern needs as it grows past toy scale is a
hard boundary between:

  1. canonical human-readable Markdown
  2. derived recall/projection state
  3. agent write authority

The failure mode I kept running into was not "can the agent write a wiki page?"
It was:

  • stale synthesis pages that still looked authoritative
  • duplicate pages for the same concept
  • retrieval results with no stable citation contract
  • agents silently turning generated summaries into truth

I have been building OpenClerk around that boundary:

  • Markdown remains canonical
  • SQLite/FTS/graph state is derived and auditable
  • every retrieval result carries doc_id, chunk_id, and citation path
  • duplicate/stale/freshness checks are explicit reports
  • writes go through planned/approved runner actions
  • semantic search/OCR are optional modules, not hidden defaults

Repo: https://github.com/yazanabuashour/openclerk

I would especially love criticism on the runner boundary: should freshness and
duplicate signals be advisory reports, hard write gates, or retrieval-time
ranking inputs?

@deepak-bhardwaj-ps

Copy link
Copy Markdown

Hello Andrei and the community. I’m building a Markdown/Obsidian-style knowledge wiki for teaching a health informatics course, using Codex to help ingest sources, maintain links, and later query the wiki for presentations and curriculum design.

I ran into a token-efficiency problem that may be interesting: the token burn was not mainly from reasoning or output, but from repeated context loading. Codex kept re-reading files like AGENTS.md, wiki/index.md, manifests, handoffs, raw sources, and tool scripts across short sessions. Even small source-ingest tasks became expensive.

I’ve started moving toward a split workflow: deterministic Python scripts handle source intake, route indexes, validation, and compact handoffs; Codex is reserved for higher-value graph curation, synthesis, and judgment. I’m also using compact routing files instead of asking the model to scan the whole wiki.

I would be grateful for any advice on the right architecture for this kind of “wiki as working memory” setup. Specifically, how would you structure retrieval, summaries, graph edges, and agent instructions so Codex can reason over a rich knowledge base without repeatedly consuming the entire context?

Even a few pointers or design principles would be very helpful.

You can try Smriti MCP I dhared above on the thread. Out of the box agents struggle to use wiki efficiently. I have built it for that purpose and you can configure same MCP with multiple agents to collaborate and share wiki.

@LARIkoz

LARIkoz commented Jun 18, 2026

Copy link
Copy Markdown

Implemented this end-to-end as Eidetic (memory for Claude Code): https://github.com/LARIkoz/eidetic — compounding pages (a good answer files back; a re-touch appends to a History/Update section instead of duplicating), an explicit schema as the maintenance contract, typed pages, and an op-log.

Two additions from running it on an ~1.8k-file / 16k-chunk store: (1) auto-extraction at session end so the wiki grows without manual ingest, and (2) drift detection — your "note where new data contradicts old", automated as stale-age / broken-link / confidence-escalation penalties; past ~day 60 the real failure mode is confident-but-stale memory making the agent worse, not better. The "LLMs don't get bored" framing nails the thesis — thanks for writing it up.

@pumblus

pumblus commented Jun 18, 2026

Copy link
Copy Markdown

Thank you Andrej for this write-up. I ended up exploring one specific piece of the pattern: how to make an LLM Wiki portable and agent-maintainable without turning it into a new app.

I built OKF Harness around that idea: https://github.com/pumblus/okf-harness

It uses local folders, immutable raw sources, OKF-compatible markdown, an okfh --json CLI for Claude Code or Codex, bounded reads, validation, citations, and a local graph report. The goal is to keep the wiki as ordinary files while giving agents a stable contract for maintaining it.

Would be interested to hear how others are handling portability, provenance, and validation across agent clients.

@mm-weber

Copy link
Copy Markdown

Thank you Andrej for the write-up and making this pattern so popular!
Also everybody else in this comment section is coming up with fantastic idea.
That is truly inspiring in the truest sense of the word.


So, I've been collecting well-formated data in an Obsidian Vault for roughly 3 years (a tabletop RPG campaign), and I asked myself: How can you automate the re-occuring tasks that come with TTRPG campaign management?

How to effectively cross-reference several wikilinked markdown files and several (image-only) PDF documents to generate the next session without contradicting the established campaign canon?

Here's my solution to that.
I just shipped your new favorite TTRPG Worldbuilding companion Loremaester:

TL;DR

Loremaester is a close cousin to Andrej's pattern that solves the problem of making the content of (image-only) PDF documents available to Claude-Code as a second source to reason over, at scale. Ingest your PDFs into your locally hosted pgvector + PostgreSQL DB.

Cross-reference hundreds of linked markdown files ("the canon") with dozens of PDF documents ("the corpus") to generate the next session without contradicting the established campaign lore.

It is fllipping the usual RAG architecture:
Instead of retrieval at the center with reasoning attached to it, Claude Code sits at the center as planner, reasoner, and writer.

Loremaester

A worldbuilding assistant for Claude Code

header
loremaester_hero.mp4

This project turns:

  • scanned PDFs,
  • Obsidian vaults,
  • wikilink graphs,
  • hybrid retrieval,
  • and agent workflows

into a persistent, self-compounding knowledge system.

Key ideas:

  • Claude Code sits above the retrieval layer as planner, reasoner, and writer.
  • Navigating Obsidian's structured wikilink graph surface the most important contextual connections between notes.
  • Human-reviewed synthesis compounds the knowledge base over time.

Under the hood:

  • Claude-Code as the agentic orchestrator
    • 3 MCP servers, 14 skills
    • hosted inside a hardened devcontainer with restricted egress
  • R2R (pgvector + PostgreSQL) for hybrid semantic + keyword retrieval
  • Ollama + mxbai-embed-large for embedding
  • OCR pipeline (CUDA, CPU) for image-only PDF
  • Obsidian for viewing and manual editing

Architecture:

Request flow

loremaester_request_flow

Would especially love feedback from people working on:

  • agents
  • memory systems
  • retrieval infra
  • knowledge graphs
  • other shapes of the LLM-wiki pattern

@demartinogiuseppe

Copy link
Copy Markdown

@pursultani

Strongest framing of this in the thread, I think. The policy/representation split is the right cut, and I agree the policy is the load-bearing half. Let me push on the other half then, the representation.
The thing I keep coming back to: is a contradiction an edge or a node? Typed edges (A contradicts B) make it a property of a relation between two pages, which is fine as far as it goes. But I'd make the tension itself a first-class object: its own page, with frontmatter for status, the poles it's holding, resolution attempts, provenance. Edges work while a contradiction is binary and stable. The moment it isn't, they run thin. Think of a tension among three or more claims, or one that's already real before either side is a proper page yet, or the cases where the content of the contradiction (why it won't resolve) is the whole point. There's nowhere on an edge to hang that. A node gives you somewhere.
It's also, I think, where the node form quietly handles the vocabulary-governance worry you raised at the end. You're not policing extends vs supersedes vs was a response to on every ingest, because the edges stay dumb (claims just participate in a tension) and the semantics all live on the node: its status (open / held / resolved-with-proof / dissolved) and the provenance of whatever closed it. The ontology work stays in one place instead of leaking into every link.
I've built a version of this as an Obsidian plugin, Antinomia — contradiction-as-node, a local model doing the inference, resolutions that carry their proof so a "resolved" tension has to cite what closed it instead of getting quietly overwritten. Code's here: https://github.com/demartinogiuseppe/antinomia. If you feel like digging into the why, the fuller argument's written up here: https://doi.org/10.5281/zenodo.20369124. You've framed this more sharply than anyone else in the thread, so I'm curious what sent you toward edges — Dataview making them cheap to query, or did the node version come up and just not convince you?

1000094694

@theluk

theluk commented Jun 19, 2026

Copy link
Copy Markdown

I really love the Linting. I start using it more and more as an agent.

btw I poured your logic into a skill of ours http://knolo.io/skills/knowledge-wiki

it built it out pretty nicely, I really love the index and the linter agent!

CleanShot 2026-06-19 at 22 31 09

@arrrr110

arrrr110 commented Jun 20, 2026

Copy link
Copy Markdown

谢谢你分享的智慧,我在AI的帮助下构建了一个简单的LLM-Wiki项目。

欢迎伙伴们点评。

https://github.com/arrrr110/Nemsy
奈姆希(Nemsy):一个自我构建的私人知识助理

名字取自炉石传说中的 Nemsy Necrofizzle —— 聪明、好奇、充满能量。

本项目基于 Karpathy 的 LLM Wiki 理念,由 DeepSeek 超长上下文模型驱动,以 Obsidian Vault 为知识源,为唯一用户提供持续积累、自主归纳的个人知识服务。

@lidorshimoni

Copy link
Copy Markdown

I really love this pattern. Seeing some of the pushback comparing this to a standard database (re: @witwaycorp) kind of misses the core value proposition. A DB or search engine just retrieves raw data—you still have to mentally parse everything and connect the dots yourself. This LLM-Wiki shifts that cognitive load. The LLM does the heavy lifting of resolving contradictions and building out the graph during the ingest phase. You're building a compounding asset, not just a search index.

That said, taking this from a concept to a stable architecture brings up some real scaling bottlenecks. For those of you already hacking on implementations here (@vvvvvivekkk, @skyllwt, @axoviq-ai), I’d love to hear how you’re handling a couple of things:

  1. Synthesis Decay & Knowledge Drift
    When the LLM constantly rewrites entity pages as new data flows in, how do you prevent the original nuance from getting compressed out? Or worse, hallucinations slowly becoming the "ground truth" over multiple update cycles? Are you sticking strictly to append-only, or maybe forcing the LLM to anchor its claims with hard quotes from the immutable raw/ files?

  2. Scaling the "Lint" Step
    Triggering a full health-check across the entire wiki for contradictions is going to nuke the context window and get expensive fast. Have any of you tried a "local subgraph" approach to linting? For example, if a new source updates a specific concept, you only trigger a lint on that node and its 1st or 2nd-degree connections rather than checking the whole repo?

Curious to hear how you guys are optimizing the backend logic and maintenance loops in your forks!

@hmbseaotter

Copy link
Copy Markdown

Contradiction checking does not need to be a monolithic full-repo LLM pass.

It can be implemented as several smaller steps:

  1. Per-source contradiction detection (at ingest time)
    This is the high-frequency activity: it runs on every source ingest. The schema says to compare the incoming claim against what the touched pages already say. A source touches ~8–15 pages, so the model only loads those pages, not the whole repo. My schema classifies a detected contradiction into one of three severities — soft, scope-mismatch, or hard (and "none" when there is no conflict). Soft and scope-mismatch are non-blocking: they get flagged, referenced, compared and explained, and I permit them since they can be useful in setting the subject matter's peripheral context. I also have a mechanism to keep an eye on soft/scope contradictions so they do not quietly accumulate over time without any review. Hard contradictions are not acceptable — they stop the ingestion run, hold the commit, send me a notification, and block continuation of ingestion until I manually resolve them (with an explanation of the resolution) inside the MD files. Each flagged contradiction carries a machine-readable severity token plus a status line, e.g.:
Contradiction severity: hard
Status: Unresolved — flagged for user review
  1. The commit gate - it is deterministic and carries zero context cost.
    This is what holds a commit for hard contradictions on every source. The commit gate is not an LLM pass at all — it is a Python os.walk over the "wiki/" folder that greps each page for "Status: Unresolved" and reads the severity token. Yes, it touches every file, but only via cheap disk I/O + regex; it never reaches into the context window. So this "scan the whole repo on every commit" costs ~nothing and scales to any size.

  2. The periodic lint backstop — the only genuinely broad pass, and the one to watch.
    This is where the concern about nuking the context window has real merit. In my lint workflow, the deterministic checks still sweep the whole wiki, but most of that is shell, not LLM: orphan detection, missing-page detection, and unreferenced-image checks are comm/grep passes (comm to diff sorted page-lists against sorted wikilink targets). Only the reasoning-heavy checks — the contradiction backstop, causal-chain gaps, thin-page judgment, and missing cross-references — need the model. This way I avoid a naive "load every page, cross-check all pairs" approach with O(n²) that would get painful fast as the wiki scales.

@lidorshimoni proposed a subgraph approach, which I then formalized into my schema. (A partial delta-scoping already existed in my compile step; reading the post pushed me to make scoped linting the explicit rule.) The fit is natural: a contradiction can only exist between claims about the same entity or relationship, so two pages sharing no concept and no [[wikilink]] essentially can't contradict each other. (The edge case is two pages that independently mention the same external entity without linking to a shared page for it — dense cross-referencing reduces this but does not eliminate it.) My wiki already encodes relatedness as explicit graph edges — wikilinks plus the directional "What causes this" / "What this causes" links — so the contradiction surface is bounded to each source's touched pages plus their 1st/2nd-degree link neighbors. The graph hands us the neighborhood for free.

The reasoning-heavy backstop therefore runs over nodes changed since the last lint plus their graph neighbors only, not the full repo. This keeps cost bounded as the wiki grows and still catches newly-introduced cross-page conflicts.

One gap this leaves intentionally: a contradiction between two old, unchanged pages that have never landed in the same lint neighborhood. That is real, and I am not pretending it isn't. The mitigation is periodic full sweeps — currently after large ingestion rounds and on explicit request, not on an automated schedule. This trades recall for cost, and I think that is the right tradeoff: the per-source check and the commit gate already filter the cases that would actually corrupt the wiki. The full sweep is a cleanup pass, not a primary defense.

@distorx

distorx commented Jun 21, 2026

Copy link
Copy Markdown

OKF — a production instance of this pattern, with the index + graph internals

Running this for a self-hosted infra fleet (a mail server, ~30 Tailscale hosts, Proxmox, CI runners).
~7,500 type-tagged notes; the vault is the operational source of truth, not just reading notes.
A few internals that made it hold up past the "index.md is enough" scale:

flowchart LR
  S[Raw sources<br/>repos · live configs · API specs · kanban · fleet] -->|exporters on timers| V[Vault<br/>~7,500 md + YAML frontmatter]
  V -->|okf sync /10min| F[(notes<br/>FTS5 trigram)]
  V -->|okf sync /10min| E[(emb<br/>1024-d vectors)]
  V -->|okf-crosslink| M[per-area MOC Map hubs]
  F --> Q[okf search / semantic / record / run]
  E --> Q
  Q --> A[agent &amp; human]
Loading

Ingest is scripted, not one-source-at-a-time. Exporters walk the live systems on timers — network
inventory, Google/OpenAPI discovery (→ ~1,880 api-method notes), kanban→notes, commits, model &
provider registries. The wiki re-derives itself as the infra changes under you.

The index is a derived dual index (okf-index.db, ~53 MB, throwaway/rebuildable):

  • notes = FTS5 trigram (so MATCH behaves like substring); emb = 1024-d float32 unit vectors
    (mxbai-embed-large via local Ollama, 4 KB/note); meta/config for bookkeeping.
  • Latency: keyword ~0.07 s (vs 0.5–1.2 s full scan); semantic ~1.1 s cold — dominated by
    process + numpy import + the query-embed, not the matrix (loading ~19 MB is ~50 ms).
  • Integrity guards (why it never drifts): content-hash authority (git/exporters churn mtime; a
    sha1 decides real change, so no spurious ~5-min re-embeds); embedder-identity guard (model swap →
    full re-embed, never mixes vector spaces); independent commit (FTS advances even if Ollama is
    down mid-sync — the embedding is deferred + retried, never marked done with a stale vector);
    deletion reconciliation + zero-candidate full-walk fallback (true mirror, never misses an add).
    Vault = truth; index = throwaway.

It's plain SQLite — okf shells out, no ORM:

-- derived schema (rebuildable; the vault is the source of truth)
CREATE VIRTUAL TABLE notes USING fts5(rel UNINDEXED, hay, tokenize='trigram');
CREATE TABLE meta  (rel TEXT PRIMARY KEY, mtime REAL, rid INTEGER, hash TEXT);
CREATE TABLE emb   (rel TEXT PRIMARY KEY, mtime REAL, vec BLOB, hash TEXT);   -- 4096-byte float32 blob
CREATE TABLE config(k TEXT PRIMARY KEY, v TEXT);                              -- emb_model, emb_dim, schema_version

-- keyword: trigram tokenizer makes MATCH behave like substring; narrows ~7,500 -> a few
SELECT rel FROM notes WHERE notes MATCH '"gitea"';        -- then exact `ql in hay` re-check per candidate

-- filtered (frontmatter is folded into `hay`, lowercased): type + a field
SELECT rel FROM notes WHERE notes MATCH '"type:bill"' AND notes MATCH '"billable:true"';

-- incremental upsert: skip unchanged, delete+reinsert changed by FTS rowid
SELECT rel, mtime, rid, hash FROM meta WHERE rel = :rel;       -- mtime fast-path, hash authority
DELETE FROM notes WHERE rowid = :rid;                          -- then INSERT the new hay
INSERT INTO notes(rowid, rel, hay) VALUES (:rid, :rel, :hay);

-- semantic: pull unit vectors, cosine = dot product in numpy (no SQL vector op)
SELECT rel, vec FROM emb;

Indexing notes: rel is the PK everywhere (O(1) upsert / delete-reinsert); meta.rid maps a
note to its FTS rowid so a change is DELETE+INSERT by rowid (no full reindex); the trigram
tokenizer is the index — substring MATCH with no LIKE table-scan — and FTS5 maintains its own
shadow tables (notes_data/_idx/_content/_docsize). There's deliberately no B-tree on hay; FTS5
owns that, and the exact re-check per candidate keeps results identical to a full scan.

The graph is maintained and linted. okf lint reports: orphans (no inbound [[links]]), stubs
(broken links = not-yet-written knowledge, informational), near-duplicate / contradiction candidates
by embedding cosine, and conformance (frontmatter + non-empty type). A cross-link pass maintains
per-area MOC "Map" hubs — it writes only the hubs, never the member notes, so there's no churn —
and took orphans from ~31% → ~19% in one run.

Convergence: we'd independently named it "OKF," then found Google's Open Knowledge Format spec
describing almost exactly the design (md + YAML frontmatter, type required, links = untyped edges,
broken links tolerated). Aligning to v0.1 means external OKF tooling could consume the vault unchanged.

The "maintenance ≈ 0" claim is the crux: timers + the agent keep the ingest, the dual index, and the
graph all current, so unlike every wiki I've abandoned, this one stays connected and in-sync without
me touching it.

@MarcoPorcellato

Copy link
Copy Markdown

What about "Reinforcement Learning (RLHF) for PKMs"?

Right now, our graphs are "flat". Every block or bullet has a static weight of 1.0. Search relies on text matching, but human memory doesn't work like that. Some ideas are core pillars, others are just fleeting notes.

I’m releasing an architectural RFC to the open-source community: Applying Reinforcement Learning from Human Feedback (RLHF) to Personal Knowledge Graphs.

Instead of forcing users to manually rate notes (flashcards/spaced repetition), the database should passively learn from our UI interactions:

  • Rewards (+ weight): When you transclude a block ((uuid)) or zoom into it (Focus Mode), the database learns this is a foundational node.

  • Penalties (- weight): When a block is ignored in search results (scroll-past) or hasn't been touched in months (temporal decay), its semantic weight drops.

I wrote a detailed Gist outlining the architecture, the pseudo-code for the dynamic weights, and how it alters global search ranking. I've released the concept under the Apache 2.0 license so anyone can experiment with it or build plugins.

You can take a look and contribute here:
https://gist.github.com/MarcoPorcellato/9e5226408c56048b16957771f9056e28

I'm building this into the core of Matryca Brain (next step from Matryca Plumber), but I’d love to hear the thoughts of the Logseq community. Is anyone else exploring dynamic node weighting based on implicit UI feedback? Let's discuss the architecture!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment