How ai-memory Versions Pages and Maintains Their Links: A Technical Deep Dive

ai-memory implements append-only versioning where each page edit creates a new row with a supersedes pointer to its predecessor, automatically rewiring incoming links to the latest version.

Every wiki page in ai-memory is stored as a versioned record in SQLite. Rather than updating rows in place, the system creates immutable versions and uses boolean flags and foreign key relationships to track the current head of each page. This design preserves full history while keeping links consistent across edits.

The Pages Table and Versioning Model

The pages table stores each version as a distinct row. Two columns control visibility and lineage:

Column Purpose
is_latest Boolean flag; exactly one version per page path has 1
supersedes Nullable foreign key pointing to the previous version's ID

When content changes, upsert_page_in_tx in crates/ai-memory-store/src/ops.rs executes a two-step supersession:

// Mark the old version as historical
tx.execute(
    "UPDATE pages SET is_latest = 0 WHERE id = ?1",
    params![&existing.id],
)?;

// Insert the new version, linking back to its predecessor
tx.execute(
    "INSERT INTO pages (…, is_latest, supersedes, …) VALUES (…, 1, ?, …)",
    params![
        …,
        &existing.id,  // ← supersedes pointer

    ],
)?;

The Page struct in crates/ai-memory-core/src/page.rs (lines 71-93) reflects this schema:

pub struct Page {
    pub id: PageId,
    pub is_latest: bool,
    pub supersedes: Option<PageId>,   // ← chain to previous version

}

Older versions remain reachable by traversing the supersedes chain—they are never deleted, only demoted from is_latest.

ai-memory distinguishes between outgoing links (from a page's markdown) and incoming links (other pages pointing to it). Both require coordination during supersession.

Each version stores its own set of outgoing links discovered in its markdown body. The replace_links_in_tx function (called during upsert) deletes old outgoing links for that path and inserts fresh ones for the new version.

When a page is superseded, incoming links must point to the new version. The refresh_incoming_links_for_path function (lines 1121-1182 in ops.rs) handles this:

tx.execute(
    "UPDATE links SET to_page_id = ?1 WHERE to_path = ?2 …",
    params![latest_page_id.as_bytes(), page.path.as_str(), …],
)?;

This single update affects:

  • Same-project links (to_workspace_id IS NULL)
  • Cross-project links (explicit workspace references)

Both bare path references and fully qualified cross-project links are remapped atomically in the same transaction that creates the new page version.

Beyond hyperlinks, pages connect to entities (named concepts extracted from content). These relationships are also versioned in the entity_page_links table:

// attach_entities_in_tx - lines 1000-1009 in ops.rs
tx.execute(
    "INSERT INTO entity_page_links (entity_id, page_id, valid_from) …",
    params![entity_id, page_id.as_bytes(), version_created_at],
)?;

When superseded, entity links are "closed" by setting superseded_at, mirroring the same temporal semantics as page-level links. This ensures entity relationships reflect the knowledge state at each point in time.

Querying Version History

The supersedes column enables full chain reconstruction. Use this recursive CTE to walk backward from the current version:

WITH RECURSIVE chain(id, supersedes, created_at) AS (
    SELECT id, supersedes, created_at
    FROM pages
    WHERE workspace_id = ? AND project_id = ? AND path = ? AND is_latest = 1
    
    UNION ALL
    
    SELECT p.id, p.supersedes, p.created_at
    FROM pages p
    JOIN chain c ON p.id = c.supersedes
)
SELECT id, created_at FROM chain ORDER BY created_at DESC;

Or query the supersession relationship directly:

SELECT child.id AS version_id,
       parent.id AS replaced_version,
       child.created_at
FROM pages child
JOIN pages parent ON child.supersedes = parent.id
WHERE child.is_latest = 0;

Creating and Updating Pages

Rust API Example

use ai_memory_core::{NewPage, PagePath, Tier};
use ai_memory_store::ops::upsert_page;

let page = NewPage {
    workspace_id,
    project_id,
    path: PagePath::new("notes/knowledge.md"),
    title: "Knowledge".into(),
    body: "# Knowledge\n\nImportant ideas.".into(),

    tier: Tier::Semantic,
    frontmatter_json: serde_json::json!({ "tags": ["knowledge"] }),
    pinned: false,
    links: vec![],          // outgoing links auto-extracted from body
    author_id: None,
    expires_at: None,
    entities: vec![],       // entities auto-attached during processing
};

let page_id = upsert_page(&mut conn, &page)?;
// If content changed: creates new version, marks old is_latest=0, migrates links

CLI Example


# Initial creation

ai-memory write-page \
  --workspace main \
  --project notes \
  --path "ideas.md" \
  --title "Ideas" \
  --body "# Ideas\n\nFirst draft."

# Subsequent edit creates new version automatically

ai-memory write-page \
  --workspace main \
  --project notes \
  --path "ideas.md" \
  --title "Ideas" \
  --body "# Ideas\n\nRevised with additional research."

The CLI triggers the same supersession logic: old version demoted, new version inserted with supersedes reference, incoming links remapped.

Key Implementation Files

File Role
crates/ai-memory-core/src/page.rs Domain type with is_latest and supersedes fields
crates/ai-memory-store/src/ops.rs Version creation, link replacement, incoming link refresh
crates/ai-memory-cli/src/commands/write_page.rs CLI wrapper for store operations
crates/ai-memory-wiki/src/wiki.rs File watcher and atomic markdown writer

Summary

  • Append-only versioning: Every edit inserts a new pages row; no updates in place
  • Supersession chain: supersedes column links each version to its predecessor
  • Automatic link migration: Incoming links update atomically to point at the new is_latest version
  • Entity temporal validity: entity_page_links tracks when relationships were active
  • Full history preservation: All versions remain queryable via the supersedes chain

Frequently Asked Questions

What happens to old page versions when I edit a page?

Old versions remain in the pages table with is_latest = 0. They are demoted but not deleted, preserving complete edit history. The new version's supersedes column points directly to the previous version, creating a traversable chain.

All incoming links update atomically during the same transaction that creates the new version. The refresh_incoming_links_for_path function remaps to_page_id for every link pointing to that path, covering both same-project and cross-project references.

Can I query which version of a page existed at a specific time?

Yes. Join pages with entity_page_links or use the created_at timestamp. Since versions are immutable and timestamped, you can find the version active at any point by selecting MAX(created_at) where created_at <= target_time for a given path.

Are entity relationships also versioned?

Yes. entity_page_links includes valid_from and superseded_at timestamps. When a page version is superseded, its entity links are closed and new links are created for the replacement version, maintaining accurate temporal scope for concept associations.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →