# How Feedback Signals Are Attached to Page Versions in ai-memory

> Learn how ai-memory attaches feedback signals like helpful or stale to specific page versions using immutable rows in the page_feedback table. Understand versioned feedback.

- Repository: [Fabio Akita/ai-memory](https://github.com/akitaonrails/ai-memory)
- Tags: internals
- Published: 2026-08-22

---

**Feedback signals are attached to specific page versions by inserting immutable rows into an append-only `page_feedback` table that references the current `page_id`, binding signals like `helpful` or `stale` to exact versions rather than the page path.**

The akitaonrails/ai-memory project implements a versioned memory system where feedback signals play a critical role in content retention and quality assessment. When users mark content as helpful, stale, or wrong, these signals must be tied to specific revisions to maintain accurate historical records. The system achieves this through a strict append-only schema design that immutably links feedback to the exact page version it references.

## The Append-Only Feedback Schema

The storage mechanism relies on the `page_feedback` table, defined in migration **V37** at [[`crates/ai-memory-store/migrations/V37__page_feedback.sql`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-store/migrations/V37__page_feedback.sql)](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-store/migrations/V37__page_feedback.sql). This table implements an **append-only** architecture where each feedback signal is recorded as a new, immutable row.

The schema includes:

- **`page_id`**: References the specific version in the `pages` table
- **`kind`**: Enum values including `helpful`, `not_helpful`, `stale`, and `wrong`
- **`reason`**: Optional text explaining the feedback
- **`salience_after`**: Optional numeric value affecting page importance
- **`author_id`**: Identifier of the user providing feedback

Because the table never updates existing rows, every signal remains permanently attached to the `page_id` of the version current at the time of submission.

## Recording Feedback via the Writer API

When feedback is submitted through the MCP tool `memory_feedback`, the system routes the request to `store::Writer::record_page_feedback` in [[`crates/ai-memory-store/src/ops.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-store/src/ops.rs)](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-store/src/ops.rs#L1659). This function executes an INSERT statement that specifically targets the current version by filtering for `is_latest = 1` in the `pages` table.

The binding process works as follows:

1. The MCP tool validates the signal against the `PageFeedbackKind` enum defined in [[`crates/ai-memory-core/src/page.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-core/src/page.rs)](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-core/src/page.rs#L269)
2. The writer resolves the page path to the current `page_id` (where `is_latest = 1`)
3. A new row is inserted into `page_feedback` with that specific `page_id`

This ensures that even if the page is rewritten later (receiving a new `page_id`), the feedback remains attached to the specific version the user actually reviewed.

## Querying Feedback with the Reader

To retrieve feedback for display or linting, the `Reader` implementation in [[`crates/ai-memory-store/src/reader.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-store/src/reader.rs)](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-store/src/reader.rs#L3227) joins the `pages` table with `page_feedback` rows matching the specific `page_id`. The query returns all feedback records associated with that version, making the signals visible alongside the page content.

This join operation enables:

- Display of historical feedback when viewing specific versions
- Calculation of derived metrics like `pages.salience`
- Lint findings that highlight problematic or outdated content

## Impact on Page Salience and Retention

Feedback signals directly influence the **salience** score stored in the `pages` table. As documented in [[`docs/ARCHITECTURE.md`](https://github.com/akitaonrails/ai-memory/blob/main/docs/ARCHITECTURE.md)](https://github.com/akitaonrails/ai-memory/blob/main/docs/ARCHITECTURE.md#L70), the `salience` column (added in V37) is calculated from the most recent `salience_after` values in the feedback table.

The retention sweep uses this derived score to determine whether to keep or decay episodic pages. When a page receives `stale` or `wrong` feedback with low salience values, the system prioritizes it for cleanup, while `helpful` signals with high salience extend retention periods.

## Practical Code Examples

**Command-Line Interface**

Mark the current version of a document as helpful using the CLI:

```bash
ai-memory memory_feedback \
    --path docs/usage.md \
    --signal helpful \
    --reason "Answered my question" \
    --author "$(whoami)"

```

**MCP Tool Payload**

Call the `memory_feedback` tool directly via JSON:

```json
{
  "tool": "memory_feedback",
  "args": {
    "path": "docs/ARCHITECTURE.md",
    "signal": "stale",
    "reason": "Content out‑of‑date",
    "author_id": "user-42"
  }
}

```

**Rust API Implementation**

Record feedback programmatically using the `Writer` handle:

```rust
use ai_memory_store::Writer;

let result = writer
    .record_page_feedback(
        "docs/architecture.md".into(),
        ai_memory_core::page::PageFeedbackKind::Stale,
        Some("Content no longer reflects the design".into()),
        None,
        author_id,
    )
    .await?;

```

**Reading Feedback Records**

Retrieve feedback for analysis:

```rust
use ai_memory_store::Reader;

let feedback = reader
    .page_feedback("docs/architecture.md")
    .await?;

for fb in feedback {
    println!("{} – {}", fb.kind, fb.reason.unwrap_or_default());
}

```

## Summary

- Feedback signals are stored in the append-only `page_feedback` table created in migration V37
- Each signal binds to a specific version via the `page_id` of the current row (`is_latest = 1`)
- The `PageFeedbackKind` enum in `ai-memory-core` validates signals like `helpful`, `stale`, `not_helpful`, and `wrong`
- The writer API in [`crates/ai-memory-store/src/ops.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-store/src/ops.rs) handles immutable INSERT operations
- Reader queries join feedback to pages for display and linting purposes
- Derived `salience` scores drive the retention sweep algorithm

## Frequently Asked Questions

### How does ai-memory prevent feedback from being lost when a page is rewritten?

When a page is rewritten, it receives a new `page_id` in the `pages` table. Because `page_feedback` rows reference specific `page_id` values rather than paths, historical feedback remains attached to the old version. The new version starts with a clean feedback slate, ensuring signals always describe the content they were attached to.

### Can feedback signals be updated or deleted after submission?

No. The `page_feedback` table is designed as append-only with immutable rows. If a user changes their opinion, they must submit a new feedback record with a different `kind` value. The system treats all feedback records as permanent audit history.

### What is the difference between `stale` and `wrong` feedback types?

Both signal content problems, but `stale` indicates the information is outdated or no longer relevant, while `wrong` indicates factual errors or incorrect content. These distinctions help the retention algorithm prioritize which pages to decay first, with `wrong` typically triggering more aggressive cleanup than `stale`.

### How does the `salience_after` parameter affect page storage?

The optional `salience_after` field in a feedback record directly updates the derived `pages.salience` column. Higher values extend how long the system retains the page version, while lower values or negative signals accelerate decay during the retention sweep process.