22.3 KB · updated 2026-07-06 · md

v0.2.0.md

docs/release-notes/v0.2.0.md

Tesserae v0.2.0 — Typed retrieval, code graph, ontology evolution

<!-- translations:start -->

한국어 · 中文 · 日本語 · Русский · Español · Français · Deutsch

<!-- translations:end -->

Released 2026-05-21 · PyPI · GitHub release · pip install --upgrade tesserae

Tesserae 0.2.0 is the first feature release on top of the v0.1.0 line and ships six additive features with no breaking changes. The release pushes Tesserae's retrieval surface from "BM25 over a typed graph" to a HippoRAG-2-style hybrid retriever with Personalized PageRank seed expansion, lays a code-as-graph foundation that links prose insights to source symbols, and adds two ontology-evolution passes (community summaries and memory decay) plus a schema-drift report. The features map to three points in the user's workflow: better recall at query time (graph_ppr, hybrid search_nodes), richer nodes at compile time (ingest-code, community summaries, decay/supersedes), and a faster iteration loop on the ontology itself (schema-drift).

Table of contents:

  1. MD0 — Personalized PageRank over the typed graph
  2. Hybrid MD0 — BM25 + lexical + embedding via RRF
  3. MD0 — Python AST → typed code graph
  4. Community summaries — Louvain + LLM (opt-in)
  5. Decay scoring + MD0 + MD1
  6. MD0 — EDC enum proposals
  7. Upgrading from v0.1.0
  8. Strategic context

1. graph_ppr — Personalized PageRank over the typed graph

What it is

A new MCP tool that runs Personalized PageRank (PPR) seeded at one or more node IDs and returns the top-k most relevant nodes by stationary-distribution mass. The walk is type-aware: edges are weighted by their relation kind, with the default profile upweighting the four edge types that carry the most "real" signal in a Tesserae graph — derived_from_session, discussed_in, references, and supersedes. This is the same multi-hop seed-expansion primitive used by HippoRAG 2, but adapted to Tesserae's typed edges rather than a passage-level co-occurrence graph.

Concretely, graph_ppr answers the question "what is this node's neighborhood, weighted by the relations I actually care about?" in a way plain BFS or BM25 cannot. A Decision seed will pull in the sessions that produced it, the questions it answered, the documents it superseded, and the concepts those documents talked about — even if none of those nodes mention the same keywords.

How to use

The tool is registered automatically when you start the Tesserae MCP server. From any MCP client:

// Seed at a single node, default α=0.15, top-10 result
{
  "tool": "graph_ppr",
  "arguments": {
    "seed_node_id": "decision-2026-05-12-switch-to-hybrid-retrieval",
    "top_k": 10
  }
}

// Seed at multiple nodes (a "concept cluster"), custom α + edge weights
{
  "tool": "graph_ppr",
  "arguments": {
    "seed_node_ids": ["concept-rrf", "concept-bm25", "concept-embedding-fusion"],
    "top_k": 25,
    "alpha": 0.2,
    "edge_type_weights": {
      "derived_from_session": 3.0,
      "discussed_in": 2.0,
      "references": 1.5,
      "supersedes": 1.5
    }
  }
}

The tool returns an array of {node_id, score, kind, title} records ordered by stationary mass. From a Claude Code session, just ask the agent "use graph_ppr seeded at <node> with top-k 15" and the using-tesserae skill will route the call.

When to use it

Reach for graph_ppr when:

  • You have one well-identified node (a decision, a paper, a session finding) and need its neighborhood in graph-distance terms, not its keyword-matched siblings.
  • You want multi-hop recall — e.g. "everything two-or-three relations away from this concept, biased by session-derived findings".
  • Plain search_nodes returns the right node but its references-linked context is too narrow.

For a free-text query starting from a phrase, prefer hybrid search_nodes (feature 2); for retrieving the textual context of a known node, prefer node_context.

Where it lives

Core implementation: MD0. Tool registration: tesserae/mcp_server.py.

Caveats

  • The default α=0.15 (restart probability) and 50-iteration cap are tuned for graphs in the 5k–50k node range typical of a project memory. Very large graphs (>250k nodes) may need a lower α and a node-degree-aware initialization; this is a follow-up.
  • Edge weights are applied symmetrically; an asymmetric profile (e.g. weighting references differently from its inverse) is not yet exposed.
  • The implementation is pure-Python and ships no new runtime dependencies; if you have a hot loop calling graph_ppr thousands of times in a single session, consider caching the column-stochastic matrix between calls.

2. Hybrid search_nodes — BM25 + lexical + embedding via RRF

What it is

The existing search_nodes MCP tool grew a mode parameter and a fused retriever underneath. The new default, mode="hybrid", runs three retrievers in parallel — BM25 (the v0.1.0 baseline), lexical (token-overlap / substring), and embedding (semantic similarity) — and fuses them via Reciprocal Rank Fusion (RRF(d) = Σ 1/(k + rank_i(d)), k=60, the Cormack et al. 2009 constant). This is the same fusion recipe used by Microsoft GraphRAG's local search and by LightRAG's hybrid mode.

The mode parameter accepts:

ModeRetrievers used
hybrid (default)BM25 + lexical + embedding, fused via RRF
lexicalLexical only
bm25BM25 only
embeddingEmbedding only
legacyThe exact v0.1.0 ranking — preserved bit-for-bit

The embedding backend autodetects in this order: if sentence-transformers is importable, it loads sentence-transformers/all-MiniLM-L6-v2 (384-dim, ~80MB, MIT-licensed, no network call after first download). If not, it falls back to a deterministic hash-bucket projection — usable, dependency-free, and reproducible, but with weaker semantic recall.

A new companion MCP tool, embedding_status, reports which backend is active so you can confirm whether your hybrid scores benefit from real embeddings or are running on the hash-bucket fallback.

How to use

// New default — fused, returns top 10
{
  "tool": "search_nodes",
  "arguments": { "query": "RRF fusion constant choice", "top_k": 10 }
}

// Force a single retriever for ablation
{ "tool": "search_nodes", "arguments": { "query": "RRF fusion", "mode": "bm25" } }

// Roll back to v0.1.0 behaviour bit-for-bit
{ "tool": "search_nodes", "arguments": { "query": "RRF fusion", "mode": "legacy" } }

// Check which embedding backend is active
{ "tool": "embedding_status", "arguments": {} }

To get the real sentence-transformers backend instead of the hash-bucket fallback:

pip install 'sentence-transformers>=2.7'
# First call downloads the MiniLM weights (~80MB) to the HF cache; subsequent calls are offline.

When to use which mode

  • hybrid for everything by default. RRF is robust to one retriever returning garbage because the rank-reciprocal damping bounds any single retriever's contribution.
  • bm25 when the query is essentially a tag or a structural slug (concept-rrf, decision-2026-05-12-…) — exact-match dominates and embeddings add noise.
  • embedding when the query is a paraphrase or a question and the matching nodes use very different vocabulary.
  • legacy for ablations against a v0.1.0 baseline, or as an escape hatch if a single regression shows up.

Where it lives

Core implementation: MD0.

Caveats

  • The hash-bucket fallback is reproducible but has limited semantic recall on paraphrases — embedding_status makes the choice visible.
  • The current RRF constant is fixed at 60; making it configurable per-call is a small follow-up if you want to test other values.
  • Embedding caching is per-process; a persisted embedding store (Lance/SQLite-vec) is a future v0.3 candidate.

3. tesserae project ingest-code — Python AST → typed code graph

What it is

A new CLI subcommand that walks a Python repository using the standard library's ast module and mints five typed node kinds and five edge kinds:

NodesEdges
CODE_FILE, CODE_MODULE, CODE_CLASS, CODE_FUNCTION, CODE_METHODcontains, calls, imports, inherits_from, declared_in

The result is persisted to .tesserae/code-graph.json and ingested by the next tesserae project compile, so code symbols become first-class graph nodes you can link to from prose, search via search_nodes, expand via graph_ppr, and visualize alongside concepts and papers. This lays the foundation for linking decisions to the symbols that implemented them — a wedge no other PKM-AI tool currently fills.

How to use

# From an activated Tesserae project
tesserae project ingest-code

# Or against an arbitrary root
tesserae project ingest-code --root ./my-package

# Then a normal compile picks up the new code-graph.json
tesserae project compile

After the next compile, code nodes become searchable:

tesserae ask "Where is the RRF fusion implemented?"

…and the agent can walk from a prose Decision node to the CODE_FUNCTION that implements it via the references / declared_in edges.

When to use it

  • You want decisions and findings to link to the actual symbols they discuss, not just the README that mentions those symbols.
  • You want code-aware retrieval without setting up a separate code-search index — the same search_nodes + graph_ppr tools now cover both prose and code.
  • You're using the Understand-Anything integration but want a lighter, dependency-free code graph that lives in the main Tesserae graph rather than a sidecar JSON.

Where it lives

Core implementation: MD0.

Caveats

  • Python only in v0.2.0. The extractor is structured to add per-language walkers later; TypeScript / Rust / Go are on the v0.3 roadmap.
  • calls edges resolve by name lookup within the same module's scope; cross-module call resolution uses import aliases but does not do full type inference. Expect a moderate false-positive rate on heavily-overloaded names — fine for retrieval, not for refactoring.
  • Decorators are recorded on the decorated node's attributes but do not (yet) mint their own edge kind.

4. Community summaries — Louvain + LLM (opt-in)

What it is

An opt-in post-compile pass that runs Louvain community detection over the typed graph, then asks the LLM to produce a one-paragraph summary per community of size ≥ 4. Each summary is materialized as a COMMUNITY_SUMMARY node, linked to its members by summarizes edges, and cached by the SHA of the member-id list so a re-compile only re-summarizes communities whose membership actually changed. This is the same pattern Microsoft GraphRAG uses to surface "what is this cluster about?" at query time.

A new MCP tool, list_communities(min_size, limit), returns the community-summary nodes ranked by member count so you can browse the graph's macro-structure without loading it.

How to use

Opt in via an environment variable, then compile:

export TESSERAE_COMMUNITY_SUMMARIES=true
tesserae project compile

Then query the resulting summaries:

{ "tool": "list_communities", "arguments": { "min_size": 5, "limit": 20 } }

Or follow a community-summary node's summarizes edges back to its members via node_context.

When to enable it

  • You have a medium-to-large graph (>500 nodes is the rule of thumb) and want a table of contents at the cluster level.
  • You want LLM-written cluster labels to attach to graph-view colour groups in the static site.
  • You're producing periodic digests (weekly / monthly) and want a "macro-structure" section that updates only when membership actually shifted.

If your graph is small (a single-project README + a handful of docs), the structural Synthesis nodes already cover this — community summaries add noise, not signal.

Where it lives

Core implementation: MD0.

Caveats

  • Off by default — Louvain + LLM summarization adds wall time, and the LLM cost is non-trivial for graphs with many communities. The cache amortizes this across re-compiles.
  • Community membership is non-deterministic across Louvain seeds; the pass pins the seed for reproducibility within a project, but cross-project comparisons of community IDs are not meaningful.
  • The summary prompt is generic; per-domain prompt overrides are a planned follow-up.

5. Decay scoring + supersedes + fresh_insights

What it is

Two complementary primitives, both inspired by A-MEM's Ebbinghaus-decay memory model:

Decay scoring assigns every session-finding node (Insight / Decision / Question / TODO / Hypothesis / Takeaway) a continuous decay_score ∈ (0, 1] using the classical forgetting curve:

decay_score = exp(-ln(2) · age_days / half_life)

The default half-life is 14 days, and an access_count bump (each time the node is surfaced by ask or fresh_insights) slows decay further. Structural-decision timestamps are derived from the parent session so backfilled findings get a sensible age.

supersedes pass is an opt-in post-compile step that finds near-duplicate session findings via Jaccard similarity on title + content shingles, then asks the LLM to judge whether the newer one truly supersedes the older one. A confirmed pair gets a supersedes edge and the older node's effective decay_score is suppressed.

A new MCP tool, fresh_insights(limit, kind), returns the top findings by current decay_score, excluding superseded nodes. This is the canonical "what should the agent prioritize from past sessions?" query.

How to use

Decay scoring runs on every compile automatically. To enable the supersedes pass:

export TESSERAE_SUPERSEDE_PASS=true
tesserae project compile

Then query the freshest non-superseded findings:

{
  "tool": "fresh_insights",
  "arguments": { "limit": 15, "kind": "Decision" }
}

Or omit kind to get a mixed feed across all session-finding kinds.

When to enable the supersedes pass

  • You're accumulating sessions weekly and your Decision / Hypothesis queries are starting to return contradictory stale entries.
  • You want the agent's using-tesserae recall flow to bias toward "this is the current thinking on X" rather than "everything we ever thought about X".
  • Cost is acceptable: each compile pays one Jaccard pass (cheap) plus N LLM judge calls for candidate pairs over the Jaccard threshold (cached by content hash).

If you only have a few weeks of sessions, decay alone is usually enough — the supersedes edge is an optimization for older, denser graphs.

Where it lives

  • Decay scoring: MD0
  • Supersedes pass: MD0

Caveats

  • The 14-day half-life is a sensible default for active research projects; long-horizon archival graphs may want 60-90 days. A per-kind half-life table is a planned v0.3 feature.
  • Jaccard candidate generation uses a fixed shingle size of 5; very short findings (one-sentence Takeaways) may under-cluster.
  • The supersedes LLM judge is conservative by design — false-negatives (missed duplicates) outweigh false-positives (incorrect supersedes edges).

6. tesserae project schema-drift — EDC enum proposals

What it is

A new CLI subcommand that helps the ontology evolve along with the corpus. It implements the EDC pattern (Extract-Define-Canonicalize, EMNLP'24): for each high-volume node kind, it Jaccard-clusters the members by title+content shingles, then asks the LLM to propose PascalCase sub-types with a short rationale and three representative member previews per cluster. The output is written to .tesserae/schema-drift.md as a checklist of copy-pasteable enum additions for the ontology files in ontology/.

LLM proposals are cached atomically (write-temp-then-rename) and keyed by the SHA of the cluster's member IDs, so re-running the pass is cheap and converges to the same proposals when the corpus is unchanged.

How to use

tesserae project schema-drift
# → writes .tesserae/schema-drift.md

Then open the report, review the proposed enums (each block is a copy-pasteable Python literal), and paste the accepted ones into the relevant file under ontology/. The next tesserae project compile will validate against the updated schema.

When to use it

  • After a large batch of new docs / sessions, before promoting a new node-kind value to a first-class enum.
  • Periodically (monthly) on long-running projects to surface ontology drift — the report is fast and read-only.
  • As a hand-off artifact: the markdown is self-contained, so you can hand it to a reviewer (or another agent) without setting up the full Tesserae CLI.

Where it lives

Core implementation: MD0.

Caveats

  • Proposals are suggestions — the human (or a downstream agent) still decides which clusters become real enum values. The pass deliberately does not mutate ontology/ on its own.
  • Jaccard clustering shares the same shingle-size assumption as the supersedes pass; very short members may cluster less crisply.
  • The pass focuses on high-volume kinds; rare kinds (≤ 3 members) are skipped to avoid speculative enums.

Upgrading from v0.1.0

pip install --upgrade tesserae

That's the whole upgrade — v0.2.0 is additive with no breaking changes. A handful of optional knobs are worth knowing about:

Environment variables for the opt-in passes:

# Enable the post-compile community-summary pass (feature 4)
export TESSERAE_COMMUNITY_SUMMARIES=true

# Enable the supersedes near-duplicate pass (feature 5)
export TESSERAE_SUPERSEDE_PASS=true

The one MCP surface change you should know about: search_nodes now defaults to mode="hybrid". If you have automation that depended on the exact v0.1.0 ranking, pass mode="legacy" — the legacy path is preserved bit-for-bit.

New, optional Python dep for the real embedding backend:

pip install 'sentence-transformers>=2.7'

If you skip this, search_nodes mode="hybrid" still works (hash-bucket fallback); embedding_status will report hash-bucket instead of minilm-l6-v2 so the choice is observable.

New CLI subcommands to add to your muscle memory:

tesserae project ingest-code      # mint code nodes (feature 3)
tesserae project schema-drift     # propose ontology enums (feature 6)

New MCP tools the agent now has available:

  • graph_ppr (feature 1)
  • embedding_status (feature 2)
  • list_communities (feature 4)
  • fresh_insights (feature 5)

Everything else — slash commands, hooks, ask, the wiki / Obsidian projection — is unchanged.


Strategic context

v0.2.0 brings Tesserae structurally in line with the 2026 GraphRAG / agent-memory state of the art:

  • Hybrid retrieval (BM25 + lexical + embedding via RRF) is the same fusion recipe used by Microsoft GraphRAG local-search and LightRAG's hybrid mode.
  • Personalized PageRank seed expansion is the multi-hop primitive at the heart of HippoRAG 2, adapted here to typed edges rather than passage co-occurrence.
  • Community summaries mirror Microsoft GraphRAG's cluster-summary level — the layer that gives "what is this corpus broadly about?" queries a fast, structural answer.
  • Decay scoring + supersedes is the A-MEM Ebbinghaus model applied to project memory; the supersedes pass is closer in spirit to Mem0's "ADD / UPDATE / DELETE" memory operations but expressed as a pure edge rather than an in-place rewrite.
  • Schema-drift via EDC is the same Extract-Define-Canonicalize idea from EMNLP'24, applied to ontology evolution rather than relation extraction.

What remains Tesserae-specific and a defensible wedge: typed edges everywhere, session findings as first-class graph nodes, and (now) code symbols inside that same graph. Most competitors in the 2026 landscape lack typed edges entirely; among those that have them, none link prose decisions to code symbols — the feat/code-graph foundation laid in v0.2.0 is where that wedge starts to compound.

See also: