> Complete content index: https://sirannon.sondelali.com/llms.txt

# Memory and forgetting

> Give every fact a source and an expiry date, mark the old row as replaced when a new value supersedes it, read back the price that was current on a date you choose, and delete the rows no query returns.

URL: https://sirannon.sondelali.com/docs/agent-memory
Section: AI agents
Version: 0.3 (@delali/sirannon-db@0.3.0, latest)

<AgentChat
  caption="The tool returns two prices for one subject, because no column in the table separates the current price from the old one."
  messages={[
    { role: 'user', name: 'Amelia Novák', text: 'What do we charge for a Tier 2 renewal?' },
    {
      role: 'assistant',
      name: 'brief-writer',
      tool: {
        name: 'recall',
        input: { subject: 'Tier 2 renewal' },
        columns: ['value', 'learned on', 'replaced by'],
        rows: [
          ['$4,200', '12 Jan 2026', null],
          ['$3,600', '3 Mar 2026', null],
        ],
        emphasisRow: 0,
        summary: 'The replaced by column is empty on both rows, so the tool returns both of them.',
      },
      text: 'Tier 2 renewal is $4,200.',
    },
    { role: 'user', name: 'Amelia Novák', text: 'It has been $3,600 since March.' },
    {
      role: 'assistant',
      name: 'brief-writer',
      text: 'You are right, and I cannot tell which of the two is current. Both prices are in the brain, and neither row is marked as replaced.',
    },
  ]}
/>

A company brain is a single store of facts that every agent in the company reads before it answers. The brain in that exchange is a SQLite database, and `recall` is the tool that `brief-writer` calls to read it.

That call returns two rows for one price, because someone in pricing wrote $4,200 when the company set that price and wrote $3,600 when the price changed in March. The `superseded_by` column is empty on both rows, so the agent answers from whichever of the two `recall` returns first. Amelia therefore cannot tell from that answer whether the agent misread the brain or the database holds the wrong price.

You close that gap in four steps, adding a timestamp for the date pricing wrote each value, along with the id of the row that replaced it, so that `recall` can take a date as an argument and return the price that was current on it. Each code block below holds one whole file.

## Where the wrong answer comes from

The database stores every fact in one table, where each row in `fact` holds a subject, a predicate, and a value. Three further columns are on the table from the start, and you bring each of them into the `recall` query as this page goes on: `learned_at` holds the date on which pricing wrote the value, `expires_at` holds the date after which the query drops the row, and `superseded_by` holds the id of the row that replaced it.

<DataTable
  columns={['id', 'subject', 'predicate', 'value', 'source', 'written_by', 'learned_at', 'expires_at', 'superseded_by']}
  rows={[
    [1, 'Tier 2 renewal', 'list_price', '$4,200', 'pricing-sheet', 'pricing-team', 1768176000000, null, null],
    [2, 'Tier 2 renewal', 'list_price', '$3,600', 'pricing-sheet', 'pricing-team', 1772496000000, null, null],
  ]}
  caption="Both writes leave a row in the fact table. learned_at and expires_at hold epoch milliseconds, so 1768176000000 is 12 January 2026 and 1772496000000 is 3 March 2026."
/>

`superseded_by` is null on both of the rows, so nothing in the table separates the two prices.

`remember` and `recall` are plain functions over the database, while `brainTools` wraps them as tools that the agent can call, which leaves the same two functions available to another framework or an MCP server.

`openBrain` creates the `data` directory and rebuilds the table on every run, so that the panels below print the same figures whenever you run them. `mock-model.ts` is there for the same reason, because a real model would word its answer differently on every call. The mock calls `recall` once and then repeats the value from the first row the tool returned, so its answer is whatever the database holds.

<CodeGroup defaultTab="brain.ts">

```ts title="brain.ts"

const JANUARY = Date.UTC(2026, 0, 12)
const MARCH = Date.UTC(2026, 2, 3)

const db = await openBrain()

await remember(db, {
  subject: 'Tier 2 renewal',
  predicate: 'list_price',
  value: '$4,200',
  source: 'pricing-sheet',
  writtenBy: 'pricing-team',
  learnedAt: JANUARY,
})
await remember(db, {
  subject: 'Tier 2 renewal',
  predicate: 'list_price',
  value: '$3,600',
  source: 'pricing-sheet',
  writtenBy: 'pricing-team',
  learnedAt: MARCH,
})

const briefWriter = new ToolLoopAgent({
  model: mockModel('Tier 2 renewal'),
  instructions: 'Answer from the company brain, using the recall tool.',
  tools: brainTools(db),
})

const reply = await briefWriter.generate({ prompt: 'What do we charge for a Tier 2 renewal?' })

const stored = await db.query<{ value: string; learned_at: number }>(
  'SELECT value, learned_at FROM fact WHERE subject = ? ORDER BY id',
  ['Tier 2 renewal']
)

console.log(
  JSON.stringify(
    {
      answer: reply.text,
      rows: stored.map((row) => ({ value: row.value, learnedOn: new Date(row.learned_at).toISOString().slice(0, 10) })),
    },
    null,
    2
  )
)

await sirannon.shutdown()
```

```json result open
{
  "answer": "Tier 2 renewal is $4,200.",
  "rows": [
    {
      "value": "$4,200",
      "learnedOn": "2026-01-12"
    },
    {
      "value": "$3,600",
      "learnedOn": "2026-03-03"
    }
  ]
}
```

```ts title="memory-tools.ts"

export interface Belief {
  predicate: string
  value: string
}

export interface LearnedFact {
  subject: string
  predicate: string
  value: string
  source: string
  writtenBy: string
  learnedAt: number
  expiresAt?: number
}

export const remember = async (db: Database, fact: LearnedFact): Promise<void> => {
  await db.execute(
    `INSERT INTO fact (subject, predicate, value, source, written_by, learned_at, expires_at)
     VALUES (?, ?, ?, ?, ?, ?, ?)`,
    [fact.subject, fact.predicate, fact.value, fact.source, fact.writtenBy, fact.learnedAt, fact.expiresAt ?? null]
  )
}

export const recall = async (db: Database, subject: string): Promise<Belief[]> =>
  db.query<Belief>('SELECT predicate, value FROM fact WHERE subject = ? ORDER BY id', [subject])

export const brainTools = (db: Database) => ({
  recall: tool({
    description: 'Look up the current value the company brain holds for a subject.',
    inputSchema: z.object({ subject: z.string() }),
    execute: async ({ subject }: { subject: string }): Promise<Belief[]> => recall(db, subject),
  }),
})
```

```ts title="schema.ts"

export const sirannon = new Sirannon({ driver: betterSqlite3() })

export const openBrain = async (): Promise<Database> => {
  mkdirSync('./data', { recursive: true })

  const db = await sirannon.open('brain', './data/brain.db')

  await db.execute('DROP TABLE IF EXISTS fact')
  await db.execute(`CREATE TABLE fact (
    id INTEGER PRIMARY KEY,
    subject TEXT NOT NULL,
    predicate TEXT NOT NULL,
    value TEXT NOT NULL,
    source TEXT NOT NULL,
    written_by TEXT NOT NULL,
    learned_at INTEGER NOT NULL,
    expires_at INTEGER,
    superseded_by INTEGER REFERENCES fact(id)
  )`)

  return db
}
```

```ts title="mock-model.ts"

const NO_USAGE = {
  inputTokens: { total: 0, noCache: 0, cacheRead: undefined, cacheWrite: undefined },
  outputTokens: { total: 0, text: 0, reasoning: undefined },
}

interface ReturnedRow {
  value: string
}

interface ToolMessage {
  role: string
  content: { output?: { value?: unknown } }[]
}

const rowsReturnedSoFar = (prompt: unknown): ReturnedRow[] | undefined => {
  for (const message of prompt as ToolMessage[]) {
    if (message.role !== 'tool') continue
    for (const part of message.content) {
      if (Array.isArray(part.output?.value)) return part.output.value as ReturnedRow[]
    }
  }
  return undefined
}

export const mockModel = (subject: string) =>
  new MockLanguageModelV4({
    doGenerate: async ({ prompt }) => {
      const rows = rowsReturnedSoFar(prompt)

      if (rows === undefined) {
        return {
          content: [
            {
              type: 'tool-call' as const,
              toolCallId: 'recall-1',
              toolName: 'recall',
              input: JSON.stringify({ subject }),
            },
          ],
          finishReason: { unified: 'tool-calls' as const, raw: undefined },
          usage: NO_USAGE,
          warnings: [],
        }
      }

      const first = rows[0]
      const text = first === undefined ? `I have no record of ${subject}.` : `${subject} is ${first.value}.`

      return {
        content: [{ type: 'text' as const, text }],
        finishReason: { unified: 'stop' as const, raw: undefined },
        usage: NO_USAGE,
        warnings: [],
      }
    },
  })
```

</CodeGroup>

The agent answers with the older price, because `recall` orders by `id` and row 1 has the lower one. Even so, every statement in the run succeeds and the run reports no error.

## Retire the old row when a new value supersedes it

A new price takes two writes, one that inserts the new value and one that marks the old value as replaced. Run both of them in the same transaction, so that no query ever returns the two rows together as current.

The insert goes first, and the update then writes the new row's id into `superseded_by` on every earlier row for that subject and predicate. `recall` returns only the rows whose `superseded_by` is null, while the old row stays in the table, because you would need it later to explain why the agent answered as it did.

The authors of a 2026 survey of agent memory call this failure stale commitment, and they write that "to mark a value superseded rather than deleted, the store must represent both current and historical truth with authority and timestamps, which many flat-text memories do not" ([arXiv 2606.30306](https://arxiv.org/abs/2606.30306)).

<CodeGroup defaultTab="brain.ts">

```ts title="brain.ts"

const JANUARY = Date.UTC(2026, 0, 12)
const MARCH = Date.UTC(2026, 2, 3)

const db = await openBrain()

await remember(db, {
  subject: 'Tier 2 renewal',
  predicate: 'list_price',
  value: '$4,200',
  source: 'pricing-sheet',
  writtenBy: 'pricing-team',
  learnedAt: JANUARY,
})
await remember(db, {
  subject: 'Tier 2 renewal',
  predicate: 'list_price',
  value: '$3,600',
  source: 'pricing-sheet',
  writtenBy: 'pricing-team',
  learnedAt: MARCH,
})

const briefWriter = new ToolLoopAgent({
  model: mockModel('Tier 2 renewal'),
  instructions: 'Answer from the company brain, using the recall tool.',
  tools: brainTools(db),
})

const reply = await briefWriter.generate({ prompt: 'What do we charge for a Tier 2 renewal?' })

// [!code --:2]
const stored = await db.query<{ value: string; learned_at: number }>(
  'SELECT value, learned_at FROM fact WHERE subject = ? ORDER BY id',
// [!code ++:2]
const stored = await db.query<{ id: number; value: string; learned_at: number; superseded_by: number | null }>(
  'SELECT id, value, learned_at, superseded_by FROM fact WHERE subject = ? ORDER BY id',
  ['Tier 2 renewal']
)

console.log(
  JSON.stringify(
    {
      answer: reply.text,
      rows: stored.map((row) => ({ value: row.value, learnedOn: new Date(row.learned_at).toISOString().slice(0, 10) })), // [!code --]
// [!code ++:6]
      rows: stored.map((row) => ({
        id: row.id,
        value: row.value,
        learnedOn: new Date(row.learned_at).toISOString().slice(0, 10),
        supersededBy: row.superseded_by,
      })),
    },
    null,
    2
  )
)

await sirannon.shutdown()
```

```json result open
{
  "answer": "Tier 2 renewal is $3,600.",
  "rows": [
    {
      "id": 1,
      "value": "$4,200",
      "learnedOn": "2026-01-12",
      "supersededBy": 2
    },
    {
      "id": 2,
      "value": "$3,600",
      "learnedOn": "2026-03-03",
      "supersededBy": null
    }
  ]
}
```

```ts title="memory-tools.ts"

export interface Belief {
  predicate: string
  value: string
}

export interface LearnedFact {
  subject: string
  predicate: string
  value: string
  source: string
  writtenBy: string
  learnedAt: number
  expiresAt?: number
}

export const remember = async (db: Database, fact: LearnedFact): Promise<void> => {
// [!code --:5]
  await db.execute(
    `INSERT INTO fact (subject, predicate, value, source, written_by, learned_at, expires_at)
     VALUES (?, ?, ?, ?, ?, ?, ?)`,
    [fact.subject, fact.predicate, fact.value, fact.source, fact.writtenBy, fact.learnedAt, fact.expiresAt ?? null]
  )
// [!code ++:12]
  await db.transaction(async (tx) => {
    const inserted = await tx.execute(
      `INSERT INTO fact (subject, predicate, value, source, written_by, learned_at, expires_at)
       VALUES (?, ?, ?, ?, ?, ?, ?)`,
      [fact.subject, fact.predicate, fact.value, fact.source, fact.writtenBy, fact.learnedAt, fact.expiresAt ?? null]
    )
    await tx.execute(
      `UPDATE fact SET superseded_by = ?
       WHERE subject = ? AND predicate = ? AND superseded_by IS NULL AND id <> ?`,
      [inserted.lastInsertRowId, fact.subject, fact.predicate, inserted.lastInsertRowId]
    )
  })
}

export const recall = async (db: Database, subject: string): Promise<Belief[]> =>
  db.query<Belief>('SELECT predicate, value FROM fact WHERE subject = ? ORDER BY id', [subject]) // [!code --]
// [!code ++:6]
  db.query<Belief>(
    `SELECT predicate, value FROM fact
     WHERE subject = ? AND superseded_by IS NULL
     ORDER BY predicate`,
    [subject]
  )

export const brainTools = (db: Database) => ({
  recall: tool({
    description: 'Look up the current value the company brain holds for a subject.',
    inputSchema: z.object({ subject: z.string() }),
    execute: async ({ subject }: { subject: string }): Promise<Belief[]> => recall(db, subject),
  }),
})
```

</CodeGroup>

Row 1 now holds 2 in `superseded_by`, while row 2 holds null, which is why `recall` returns $3,600 today. Nothing in `mock-model.ts` changed between the two runs, so the rows in the database are what changed the answer.

## Ask for the price that was current on a date you name

Amelia's second question is about the past, and a query that returns only the current row cannot answer it. `recall` therefore takes the date as an argument, and its WHERE clause keeps a row whose `learned_at` falls on or before that date, whose `expires_at` falls after that date, and whose `superseded_by` is either null or holds the id of a row written later.

The caller passes that date to `brainTools`, so the agent chooses the subject while the caller fixes the date. An agent that chose its own date would be choosing which version of the price it answers from.

<CodeGroup defaultTab="brain.ts">

```ts title="brain.ts"

const JANUARY = Date.UTC(2026, 0, 12)
const FEBRUARY = Date.UTC(2026, 1, 9) // [!code ++]
const MARCH = Date.UTC(2026, 2, 3)
const TODAY = Date.UTC(2026, 7, 27) // [!code ++]

const db = await openBrain()

await remember(db, {
  subject: 'Tier 2 renewal',
  predicate: 'list_price',
  value: '$4,200',
  source: 'pricing-sheet',
  writtenBy: 'pricing-team',
  learnedAt: JANUARY,
})
await remember(db, {
  subject: 'Tier 2 renewal',
  predicate: 'list_price',
  value: '$3,600',
  source: 'pricing-sheet',
  writtenBy: 'pricing-team',
  learnedAt: MARCH,
})

// [!code --:7]
const briefWriter = new ToolLoopAgent({
  model: mockModel('Tier 2 renewal'),
  instructions: 'Answer from the company brain, using the recall tool.',
  tools: brainTools(db),
})

const reply = await briefWriter.generate({ prompt: 'What do we charge for a Tier 2 renewal?' })
// [!code ++:9]
const askBriefWriter = async (asOf: number): Promise<string> => {
  const briefWriter = new ToolLoopAgent({
    model: mockModel('Tier 2 renewal'),
    instructions: 'Answer from the company brain, using the recall tool.',
    tools: brainTools(db, asOf),
  })
  const reply = await briefWriter.generate({ prompt: 'What do we charge for a Tier 2 renewal?' })
  return reply.text
}

const stored = await db.query<{ id: number; value: string; learned_at: number; superseded_by: number | null }>(
  'SELECT id, value, learned_at, superseded_by FROM fact WHERE subject = ? ORDER BY id',
  ['Tier 2 renewal']
)

console.log(
  JSON.stringify(
    {
      answer: reply.text, // [!code --]
// [!code ++:2]
      askedToday: await askBriefWriter(TODAY),
      askedAsOfFebruary: await askBriefWriter(FEBRUARY),
      rows: stored.map((row) => ({
        id: row.id,
        value: row.value,
        learnedOn: new Date(row.learned_at).toISOString().slice(0, 10),
        supersededBy: row.superseded_by,
      })),
    },
    null,
    2
  )
)

await sirannon.shutdown()
```

```json result open
{
  "askedToday": "Tier 2 renewal is $3,600.",
  "askedAsOfFebruary": "Tier 2 renewal is $4,200.",
  "rows": [
    {
      "id": 1,
      "value": "$4,200",
      "learnedOn": "2026-01-12",
      "supersededBy": 2
    },
    {
      "id": 2,
      "value": "$3,600",
      "learnedOn": "2026-03-03",
      "supersededBy": null
    }
  ]
}
```

```ts title="memory-tools.ts"

export interface Belief {
  predicate: string
  value: string
}

export interface LearnedFact {
  subject: string
  predicate: string
  value: string
  source: string
  writtenBy: string
  learnedAt: number
  expiresAt?: number
}

export const remember = async (db: Database, fact: LearnedFact): Promise<void> => {
  await db.transaction(async (tx) => {
    const inserted = await tx.execute(
      `INSERT INTO fact (subject, predicate, value, source, written_by, learned_at, expires_at)
       VALUES (?, ?, ?, ?, ?, ?, ?)`,
      [fact.subject, fact.predicate, fact.value, fact.source, fact.writtenBy, fact.learnedAt, fact.expiresAt ?? null]
    )
    await tx.execute(
      `UPDATE fact SET superseded_by = ?
       WHERE subject = ? AND predicate = ? AND superseded_by IS NULL AND id <> ?`,
      [inserted.lastInsertRowId, fact.subject, fact.predicate, inserted.lastInsertRowId]
    )
  })
}

export const recall = async (db: Database, subject: string): Promise<Belief[]> => // [!code --]
export const recall = async (db: Database, subject: string, asOf: number): Promise<Belief[]> => // [!code ++]
  db.query<Belief>(
// [!code --:2]
    `SELECT predicate, value FROM fact
     WHERE subject = ? AND superseded_by IS NULL
// [!code ++:8]
    `SELECT predicate, value FROM fact AS f
     WHERE subject = ?
       AND learned_at <= ?
       AND (expires_at IS NULL OR expires_at > ?)
       AND (
         superseded_by IS NULL
         OR (SELECT learned_at FROM fact AS newer WHERE newer.id = f.superseded_by) > ?
       )
     ORDER BY predicate`,
    [subject] // [!code --]
    [subject, asOf, asOf, asOf] // [!code ++]
  )

export const brainTools = (db: Database) => ({ // [!code --]
export const brainTools = (db: Database, asOf: number) => ({ // [!code ++]
  recall: tool({
    description: 'Look up the current value the company brain holds for a subject.', // [!code --]
    description: 'Look up the value the company brain held for a subject on the date this tool is fixed to.', // [!code ++]
    inputSchema: z.object({ subject: z.string() }),
    execute: async ({ subject }: { subject: string }): Promise<Belief[]> => recall(db, subject), // [!code --]
    execute: async ({ subject }: { subject: string }): Promise<Belief[]> => recall(db, subject, asOf), // [!code ++]
  }),
})
```

</CodeGroup>

One agent and one tool now produce two answers, and each of them is correct for the date in the query. A quote of $4,200 sent in February would have matched the table as it stood then, while the same quote sent today would be wrong.

## Delete the superseded rows, and lose the history with them

A superseded row stays on the disk, because `recall` drops it from the results and deletes nothing. Only a delete bounds how large the table grows. `forget` deletes the superseded rows that pricing wrote before a cutoff you choose, and it returns how many of them it deleted, which is the number you would send to your metrics system.

The panel below shows both answers and the row count, before the delete and after it. Once the older row is gone, the only price left in the table is the current one, so take the cutoff from what your own auditors require.

<CodeGroup defaultTab="brain.ts">

```ts title="brain.ts"

import { brainTools, remember } from './memory-tools' // [!code --]
import { brainTools, forget, remember } from './memory-tools' // [!code ++]

const JANUARY = Date.UTC(2026, 0, 12)
const FEBRUARY = Date.UTC(2026, 1, 9)
const MARCH = Date.UTC(2026, 2, 3)
const TODAY = Date.UTC(2026, 7, 27)
const SUPERSEDED_RETENTION = 90 * 24 * 60 * 60 * 1000 // [!code ++]

const db = await openBrain()

await remember(db, {
  subject: 'Tier 2 renewal',
  predicate: 'list_price',
  value: '$4,200',
  source: 'pricing-sheet',
  writtenBy: 'pricing-team',
  learnedAt: JANUARY,
})
await remember(db, {
  subject: 'Tier 2 renewal',
  predicate: 'list_price',
  value: '$3,600',
  source: 'pricing-sheet',
  writtenBy: 'pricing-team',
  learnedAt: MARCH,
})

const askBriefWriter = async (asOf: number): Promise<string> => {
  const briefWriter = new ToolLoopAgent({
    model: mockModel('Tier 2 renewal'),
    instructions: 'Answer from the company brain, using the recall tool.',
    tools: brainTools(db, asOf),
  })
  const reply = await briefWriter.generate({ prompt: 'What do we charge for a Tier 2 renewal?' })
  return reply.text
}

// [!code --:4]
const stored = await db.query<{ id: number; value: string; learned_at: number; superseded_by: number | null }>(
  'SELECT id, value, learned_at, superseded_by FROM fact WHERE subject = ? ORDER BY id',
  ['Tier 2 renewal']
)
// [!code ++:7]
const beforeForget = {
  askedToday: await askBriefWriter(TODAY),
  askedAsOfFebruary: await askBriefWriter(FEBRUARY),
  factRows: (await db.query<{ n: number }>('SELECT COUNT(*) AS n FROM fact'))[0]?.n,
}

const removed = await forget(db, TODAY - SUPERSEDED_RETENTION)

console.log(
  JSON.stringify(
    {
// [!code --:8]
      askedToday: await askBriefWriter(TODAY),
      askedAsOfFebruary: await askBriefWriter(FEBRUARY),
      rows: stored.map((row) => ({
        id: row.id,
        value: row.value,
        learnedOn: new Date(row.learned_at).toISOString().slice(0, 10),
        supersededBy: row.superseded_by,
      })),
// [!code ++:7]
      beforeForget,
      removed,
      afterForget: {
        askedToday: await askBriefWriter(TODAY),
        askedAsOfFebruary: await askBriefWriter(FEBRUARY),
        factRows: (await db.query<{ n: number }>('SELECT COUNT(*) AS n FROM fact'))[0]?.n,
      },
    },
    null,
    2
  )
)

await sirannon.shutdown()
```

```json result open
{
  "beforeForget": {
    "askedToday": "Tier 2 renewal is $3,600.",
    "askedAsOfFebruary": "Tier 2 renewal is $4,200.",
    "factRows": 2
  },
  "removed": 1,
  "afterForget": {
    "askedToday": "Tier 2 renewal is $3,600.",
    "askedAsOfFebruary": "I have no record of Tier 2 renewal.",
    "factRows": 1
  }
}
```

```ts title="memory-tools.ts"

export interface Belief {
  predicate: string
  value: string
}

export interface LearnedFact {
  subject: string
  predicate: string
  value: string
  source: string
  writtenBy: string
  learnedAt: number
  expiresAt?: number
}

export const remember = async (db: Database, fact: LearnedFact): Promise<void> => {
  await db.transaction(async (tx) => {
    const inserted = await tx.execute(
      `INSERT INTO fact (subject, predicate, value, source, written_by, learned_at, expires_at)
       VALUES (?, ?, ?, ?, ?, ?, ?)`,
      [fact.subject, fact.predicate, fact.value, fact.source, fact.writtenBy, fact.learnedAt, fact.expiresAt ?? null]
    )
    await tx.execute(
      `UPDATE fact SET superseded_by = ?
       WHERE subject = ? AND predicate = ? AND superseded_by IS NULL AND id <> ?`,
      [inserted.lastInsertRowId, fact.subject, fact.predicate, inserted.lastInsertRowId]
    )
  })
}

export const recall = async (db: Database, subject: string, asOf: number): Promise<Belief[]> =>
  db.query<Belief>(
    `SELECT predicate, value FROM fact AS f
     WHERE subject = ?
       AND learned_at <= ?
       AND (expires_at IS NULL OR expires_at > ?)
       AND (
         superseded_by IS NULL
         OR (SELECT learned_at FROM fact AS newer WHERE newer.id = f.superseded_by) > ?
       )
     ORDER BY predicate`,
    [subject, asOf, asOf, asOf]
  )

export const brainTools = (db: Database, asOf: number) => ({
  recall: tool({
    description: 'Look up the value the company brain held for a subject on the date this tool is fixed to.',
    inputSchema: z.object({ subject: z.string() }),
    execute: async ({ subject }: { subject: string }): Promise<Belief[]> => recall(db, subject, asOf),
  }),
})
// [!code ++:7]

export const forget = async (db: Database, supersededBefore: number): Promise<number> => {
  const removed = await db.execute('DELETE FROM fact WHERE superseded_by IS NOT NULL AND learned_at < ?', [
    supersededBefore,
  ])
  return removed.changes
}
```

</CodeGroup>

The agent still answers today's question, because `forget` keeps every row whose `superseded_by` is null. It cannot answer the February one any more, and it says so in place of guessing. Once you delete the history, `recall` still returns today's price, while every question about February returns nothing.

## Run it against a real model

Swap the mock for a model string, and leave the rest of the file unchanged. The agent would then decide for itself when to call `recall`, and its wording would change from one run to the next, which is why no panel follows this step.

```ts title="brain.ts"

import { mockModel } from './mock-model' // [!code --]

const JANUARY = Date.UTC(2026, 0, 12)
const FEBRUARY = Date.UTC(2026, 1, 9)
const MARCH = Date.UTC(2026, 2, 3)
const TODAY = Date.UTC(2026, 7, 27)
const SUPERSEDED_RETENTION = 90 * 24 * 60 * 60 * 1000

const db = await openBrain()

await remember(db, {
  subject: 'Tier 2 renewal',
  predicate: 'list_price',
  value: '$4,200',
  source: 'pricing-sheet',
  writtenBy: 'pricing-team',
  learnedAt: JANUARY,
})
await remember(db, {
  subject: 'Tier 2 renewal',
  predicate: 'list_price',
  value: '$3,600',
  source: 'pricing-sheet',
  writtenBy: 'pricing-team',
  learnedAt: MARCH,
})

const askBriefWriter = async (asOf: number): Promise<string> => {
  const briefWriter = new ToolLoopAgent({
    model: mockModel('Tier 2 renewal'), // [!code --]
    model: 'openai/gpt-5.6-sol', // [!code ++]
    instructions: 'Answer from the company brain, using the recall tool.',
    tools: brainTools(db, asOf),
  })
  const reply = await briefWriter.generate({ prompt: 'What do we charge for a Tier 2 renewal?' })
  return reply.text
}

const beforeForget = {
  askedToday: await askBriefWriter(TODAY),
  askedAsOfFebruary: await askBriefWriter(FEBRUARY),
  factRows: (await db.query<{ n: number }>('SELECT COUNT(*) AS n FROM fact'))[0]?.n,
}

const removed = await forget(db, TODAY - SUPERSEDED_RETENTION)

console.log(
  JSON.stringify(
    {
      beforeForget,
      removed,
      afterForget: {
        askedToday: await askBriefWriter(TODAY),
        askedAsOfFebruary: await askBriefWriter(FEBRUARY),
        factRows: (await db.query<{ n: number }>('SELECT COUNT(*) AS n FROM fact'))[0]?.n,
      },
    },
    null,
    2
  )
)

await sirannon.shutdown()
```

## Where to go next

A value someone wrote by mistake needs more than a delete, so [rewinding memory](/docs/agent-memory-rewind) restores the whole database to the state it was in before that write. Where a user asks in their own words for something you stored under another subject, [retrieval with Narsil](/docs/agent-retrieval) searches these same rows by meaning. For a record of which agent wrote each row, read [auditing a fleet](/docs/agent-audit).
