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

> Discover how ai-memory versions pages using append-only edits and supersedes pointers. Learn how it automatically maintains links to the latest page versions.

- Repository: [Fabio Akita/ai-memory](https://github.com/akitaonrails/ai-memory)
- Tags: deep-dive
- Published: 2026-09-05

---

**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`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-store/src/ops.rs) executes a two-step supersession:

```rust
// 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`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-core/src/page.rs) (lines 71-93) reflects this schema:

```rust
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`.

## How Links Stay Current Across Versions

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

### Outgoing Links: Rewritten Per Version

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.

### Incoming Links: Migrated to the New Head

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`](https://github.com/akitaonrails/ai-memory/blob/main/ops.rs)) handles this:

```rust
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.

## Entity Links and Temporal Scoping

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

```rust
// 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:

```sql
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:

```sql
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

```rust
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

```bash

# 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`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-core/src/page.rs) | Domain type with `is_latest` and `supersedes` fields |
| [`crates/ai-memory-store/src/ops.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-store/src/ops.rs) | Version creation, link replacement, incoming link refresh |
| [`crates/ai-memory-cli/src/commands/write_page.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-cli/src/commands/write_page.rs) | CLI wrapper for store operations |
| [`crates/ai-memory-wiki/src/wiki.rs`](https://github.com/akitaonrails/ai-memory/blob/main/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.

### How does ai-memory prevent broken links when pages change?

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.