use cases

An AI Assistant's Memory That Forgets Properly: People, Facts and Embeddings in One Transaction

Say you’re building an assistant for account managers. It listens to what they tell it and keeps the useful parts, like Dana preferring calls after ten or Acme renewing in March. Before it answers a message, it pulls up the memories that matter and puts them in front of the language model.

Two kinds of lookup hide in that. One is about meaning: which memories are closest to what was just said. The other is about people: which memories are about Dana. Then comes the part most memory setups handle badly. When Dana asks to be forgotten, every memory about her has to go, vectors included, and a copy left behind in a search index means the job isn’t done.

Memories linked to person:dana and company:acme by about links. Recall ranks the memories about Dana by closeness to the message, and forgetting Dana deletes her memories, her record and every link in one transaction.

Remembering

Each memory is a record with its text and its vector, linked to everyone it mentions, and it’s all written in one transaction:

err := db.Update(func(tx *hypercrux.Tx) error {
	key := "memory:" + newID()
	if err := tx.Put(key, hypercrux.Fields{"text": fact, "said": time.Now(), "vec": embed(fact)}); err != nil {
		return err
	}
	for _, who := range mentioned { // such as "person:dana" or "company:acme"
		if err := tx.Link(key, "about", who); err != nil {
			return err
		}
	}
	return nil
})

newID stands for however you make IDs, and embed for your embedding model. The file refuses a link to a record that doesn’t exist, and that refusal rolls back the whole memory, so create a person’s record before the first memory about them.

Recalling

Before answering a message about Dana, the assistant asks for the eight memories about her that are closest to the message:

rows, err := db.Query(`
	SELECT m.text, m.said
	FROM json_each(walk(?, 1, 'about', 'in')) w
	JOIN memory m ON m.key = w.value
	WHERE m.vec IS NOT NULL
	ORDER BY distance(m.vec, ?)
	LIMIT 8`, "person:dana", embed(message))

The walk goes one step backwards along about links, from Dana to the memories that point at her. The rest is ordinary SQL, so keeping only the last three months is one more condition: AND m.said > date('now', '-90 days').

Forgetting

When Dana asks to be forgotten, one transaction deletes every memory about her and then her own record:

err := db.Update(func(tx *hypercrux.Tx) error {
	if _, err := tx.Exec(`DELETE FROM memory WHERE key IN
		(SELECT value FROM json_each(walk(?, 1, 'about', 'in')))`, "person:dana"); err != nil {
		return err
	}
	return tx.Delete("person:dana")
})

Each deleted memory takes its vector with it, since the vector is a column in the same row, and the triggers in the file remove its links in the same transaction. A memory about both of them, such as Dana signing off Acme’s budget, goes too, because it’s about her. Acme’s own memories stay. If you’d rather keep the Acme half, rewrite that memory without her before you delete.

There’s no second index to purge afterwards. db.Check() confirms that keys, rows, links and vectors still agree.

The Bytes on Disk

Deleting rows is the database’s half of forgetting. SQLite leaves deleted bytes sitting in free space inside the file until it needs that space again, so after a deletion request, rebuild the file without them:

_, err := db.Exec("VACUUM")
if err == nil {
	_, err = db.Exec("PRAGMA wal_checkpoint(TRUNCATE)")
}

Both lines matter. HyperCrux files keep a write-ahead log, and VACUUM writes the rebuilt pages there first. The old pages, deleted text and all, stay in the main file until the checkpoint copies the new ones over them and empties the log.

Backups made before the request still hold the old data and need a retention rule of their own. So does anything the assistant already sent to a language model provider, which is out of the file’s reach.

What It Costs

Writing a memory is a small transaction. In the recorded benchmarks, a put with a 384-value vector, committed and flushed to disk on its own, took about 0.37 milliseconds. Recall only compares the memories linked to one person, usually a few dozen, so it stays quick when the file holds many thousands of memories about other people.

The assistant can run in a single process or several on the same machine, and they can share one file. That’s the boundary: one machine, one file. An assistant serving a whole company from many servers needs a database server.

Forgetting as One Transaction

An assistant’s memory has to be searchable by meaning and by person, and it has to be deletable without leftovers. With the vector stored in the memory’s own row and the links tied to it by triggers, the delete is a single transaction that either happens completely or not at all. The reference has the full Go API.