Skip to content

How It Works

Maxim Mironenko edited this page Jun 11, 2026 · 16 revisions

How It Works

Architecture Overview

┌─────────────────────────────────────────────────┐
│            Claude Code Sessions                  │
│  Session A        Session B        Session C     │
└──────┬───────────────┬────────────────┬──────────┘
       │               │                │
       │  Stateless HTTP (MCP protocol) │
       └───────┬───────┘                │
               ▼                        │
    ┌──────────────────────┐            │
    │  MCP Streamable HTTP │            │
    │  Server (port 8765)  │◄───────────┘
    │                      │
    │  - MCP tools (/)     │
    │  - REST API (/api/*) │
    │  - WebSocket (/ws)   │
    └──────────┬───────────┘
               │
               ▼
    ┌──────────────────────┐
    │ MultiProjectGraphStore│
    │  - In-memory graphs  │
    │  - Write-through     │
    │  - Auto-compact      │
    └──────────┬───────────┘
               │
        ┌──────┴──────┐
        ▼             ▼
~/.knowledge-graph/
├── user.json          (global)
├── sessions.json
└── projects/
    ├── my-app/graph.json
    └── other/graph.json

Two-Level Model

Knowledge is split into two levels, all stored centrally:

Level What Goes Here Storage Location
user Cross-project wisdom: preferences, meta-learnings, architectural principles, personal patterns ~/.knowledge-graph/user.json
project Codebase-specific: architecture decisions, dependencies, debugging discoveries, conventions ~/.knowledge-graph/projects/<slug>/graph.json

The user graph is a singleton — one file, always loaded. Project graphs are loaded on demand when a session registers with a project_path.

Nodes and Edges

The graph has two entry types:

Nodes — Named concepts, patterns, or insights:

{
  "id": "auth-token-refresh",
  "gist": "JWT refresh uses sliding window; silent failure if session expired",
  "notes": ["discovered during auth debugging 2025-01"],
  "touches": ["src/auth/refresh.py"]
}

Edges — Relationships between nodes, files, or concepts:

{
  "from": "auth-module",
  "to": "session-handler",
  "rel": "requires-init",
  "notes": ["token validation assumes active session"]
}

Edges can reference node IDs or file paths directly — you don't need a node for every file.

Data Flow

  1. Session starts → Claude calls kg_read(cwd) which initializes the session and returns the full active graph (both levels) plus a session_id
  2. During work → Claude calls kg_put_node/kg_put_edge to capture insights
  3. Write-through → Every mutation saves to disk immediately (atomic write: temp file + rename)
  4. Background maintenance → Periodic thread (30s) runs compaction and orphan pruning
  5. Multi-session → Other sessions call kg_sync(session_id) to get changes since their start time

All operations go through the shared in-memory store. Write-through ensures disk is always up-to-date.

Auto-Compaction

Maintenance runs after every write and on the periodic background tick. At most one of archive/refill acts per tick:

Pass 1 — Archive (when active graph exceeds the token limit):

  1. Score each eligible node using two percentile-ranked signals:
    • Recencymax(last_write_ts, last_read_ts). Reading a node via kg_read(id) refreshes its recency.
    • Connectedness — Weighted in/out edges: in_degree × 0.66 + out_degree × 0.33. An edge to an active neighbour counts at full weight; an edge to an archived neighbour counts at 0.2 (so a cluster that archived together isn't scored as fully disconnected); edges to orphaned nodes count for nothing.
  2. Final score = 0.33 × recency + 0.66 × connectedness (weighted sum of percentiles)
  3. Archive lowest-scoring nodes until graph is under COMPACTION_TARGET_RATIO (0.8) of the token limit
  4. Grace period — Nodes created recently are never archived (see KG_GRACE_PERIOD_DAYS). Grace is based on creation time only — updates and reads do not reset it.

Archived nodes get _archived: true. Their edges remain visible as memory traces while the other endpoint is active. Use kg_read(cwd, id) to promote them back to active.

Pass 1b — Resurrection (runs after archiving): Re-scores archived + active nodes together. If any pre-existing archived node outscores a just-archived node by ≥0.05, they swap — the better-scoring archived node is restored to active (bounded so a swap never pushes the graph back over the limit). This ensures archiving history doesn't permanently strand nodes that became well-connected after the fact.

Pass 1r — Refill (when the active graph sits below the fill ceiling): The reverse of archiving. Whenever active tokens are below COMPACTION_TARGET_RATIO (0.8) of the limit, the highest-scored archived nodes are promoted back to active until the ceiling is reached. Promotion is iterative — after each promotion the remaining candidates are re-scored, because the promoted node's edges just became live and raise its neighbours' connectedness. This lets a dense archived cluster lead itself back: pull the hub, its satellites re-rank to the top, pull them next. A candidate too large for the remaining headroom is skipped (it doesn't block smaller candidates behind it). Refill never runs on a tick that just archived, and the 0.8 ceiling sits safely below the 1.0 archive threshold — the two passes cannot thrash.

Pass 2 — Orphan (when archived section exceeds 30% of token budget):

  1. Count archived nodes — each costs ~5 tokens as an ID line in kg_read output
  2. When archived tokens exceed ARCHIVED_BUDGET_RATIO (30%) of max_tokens, demote lowest-connectivity archived nodes to orphaned (_orphaned_ts = now)
  3. Orphaned nodes are invisible in kg_read and kg_sync — they no longer consume context

Three-Tier Node States

State In kg_read In kg_search Recovery
active gist visible
archived ID visible; edges visible while the other endpoint is active kg_read(cwd, id) → promotes to active (refill also promotes automatically when budget allows)
orphaned invisible ✓ flagged search → kg_read(cwd, id)

Chain rescue: reading an archived node promotes it to active AND rescues any of its orphaned neighbors back to archived — their IDs and edges reappear as crumbs to follow.

Permanent deletion: orphaned nodes with no recall after KG_ORPHAN_GRACE_DAYS (365 days) are permanently deleted from disk.

Token Estimation

The system estimates token cost to decide when to compact. The estimator charges exactly what kg_read renders (a single predicate, edge_is_live, drives both):

Component Estimate
Active node 20 tokens base + 1 token per 4 chars of gist (notes are fetched on demand, not charged)
Archived node 5 tokens (its collapsed ID line)
Orphaned node 0 (invisible)
Live edge (≥1 active or file/artifact endpoint) 15 tokens
Edge between two archived nodes 0 (suppressed from output, not charged)

These are rough estimates — the goal is keeping the active graph small enough to fit in Claude's context window without being wasteful.

Transport: Stateless HTTP

The server uses MCP's Streamable HTTP transport in stateless mode. Each request is independent — no session IDs preserved between HTTP requests by the MCP layer.

This is because Claude Code's MCP client doesn't preserve session IDs between requests. The server was tested with stateful mode and it failed — Claude Code simply doesn't send the mcp-session-id header back.

Session tracking for sync (kg_sync) is handled at the application level, not the transport level.

Concurrency

  • Thread-safe via threading.RLock on the graph store
  • Multiple Claude Code sessions can read/write simultaneously
  • Last write wins — no conflict resolution beyond that
  • The kg_sync tool lets sessions pull changes made by other sessions

Clone this wiki locally