Memori Agen Usang: Cara Kedaluwarsa dan Menghapusnya
Pelajari cara mencegah memori agen usang: tetapkan masa berlaku fakta, hapus data terkait secara berantai, tinjau sisanya, lalu periksa SQLite dengan sqlite3.
Mengapa memori agen menjadi usang
Memori agen menjadi usang karena suatu fakta ditulis sekali lalu tidak pernah diperiksa kembali. Penyimpanan terus mengembalikannya, lapisan retrieval memasukkannya ke dalam prompt sebagai teks biasa tanpa tanggal, lalu model mengulanginya dengan tingkat keyakinan yang sama seperti saat fakta tersebut ditulis. Tidak ada error yang muncul. Inilah kesulitannya: bagi model dan bagi Anda, memori usang terlihat sama persis seperti memori baru.
Penulisan yang lebih baik saat penyimpanan tidak menyelesaikan masalah ini. Solusinya adalah menetapkan masa berlaku untuk fakta yang memilikinya dan membuat prosedur peninjauan untuk fakta yang tidak memilikinya. Keduanya merupakan pemeliharaan rutin pada database kecil, dan sebagian besar pekerjaannya berupa SQL (structured query language).
Decay dan drift adalah dua jenis kegagalan yang berbeda
Decay adalah fakta yang memiliki tanggal berakhir secara alami. "Bepergian minggu ini." "Server staging sedang tidak aktif untuk migrasi." "Sedang meninjau draf anggaran." Pernyataan ini benar ketika ditulis, dan masa berlakunya dapat ditentukan saat itu juga. Decay dapat ditangani. Tambahkan waktu kedaluwarsa, yang terkadang disebut TTL (time to live), lalu hapus baris tersebut setelah waktunya lewat.
Drift adalah fakta yang disimpan sekali dan tidak pernah diperiksa kembali. "Lebih memilih pnpm." "Database menggunakan Postgres 15." "Deployment dilakukan melalui branch staging." Tidak ada waktu tertentu yang membuat pernyataan ini menjadi salah. Perubahan pada tempat lainlah yang membuatnya salah, dan tidak ada mekanisme yang memberi tahu penyimpanan memori Anda tentang perubahan tersebut.
Drift tidak memiliki perbaikan otomatis yang bersih. Penyimpanan tidak dapat mendeteksi perubahan yang tidak pernah diamatinya, sehingga job yang membaca penyimpanan lalu menganalisis isinya hanya membaca ulang teks lama yang sama. Mekanisme yang dapat digunakan adalah memeriksa kembali fakta tersebut terhadap hal yang dijelaskannya. Proses ini memerlukan seseorang, atau agent yang memegang tool untuk membaca keadaan terkini.
Jadi, rencananya terbagi menjadi dua. Kedaluwarsakan hal yang mengalami decay. Tinjau hal yang mengalami drift. Jangan menangani masalah kedua seolah-olah masalah tersebut adalah masalah pertama.
Tetapkan masa berlaku untuk fakta yang terikat waktu
Setiap baris memori memerlukan tiga kolom yang tidak disediakan oleh sebagian besar penyimpanan: asal fakta tersebut, waktu terakhir fakta tersebut dikonfirmasi, dan waktu saat fakta tersebut tidak lagi benar. Penyimpanan ini dapat Anda buat hanya dengan sqlite3, dan kolom yang sama dapat ditambahkan ke penyimpanan yang sudah Anda gunakan.
CREATE TABLE memory (
id TEXT PRIMARY KEY,
subject TEXT NOT NULL,
fact TEXT NOT NULL,
source TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
confirmed_at TEXT NOT NULL DEFAULT (datetime('now')),
expires_at TEXT,
superseded_by TEXT REFERENCES memory(id) ON DELETE CASCADE
);
CREATE INDEX memory_expires ON memory(expires_at);
CREATE INDEX memory_superseded ON memory(superseded_by);datetime('now') mengembalikan UTC (coordinated universal time) sebagai YYYY-MM-DD HH:MM:SS. Format ini diurutkan dan dibandingkan dengan benar sebagai teks, sehingga setiap pertanyaan tanggal di bawah ini cukup menggunakan klausa WHERE biasa. Kolom source tidak bersifat opsional. Fakta yang tidak dapat ditelusuri kembali ke pesan, file, atau keluaran perintah tidak dapat diperiksa ulang. Fakta yang tidak dapat diperiksa ulang hanya dapat dihapus.
Menulis memori yang memiliki masa berlaku:
INSERT INTO memory (id, subject, fact, source, expires_at)
VALUES ('m_0191', 'availability', 'Away from keyboard, replies are delayed',
'chat 2026-08-08', datetime('now', '+7 days'));Pengambilan data tidak boleh membaca tabel secara langsung. Pengambilan data membaca view yang menyembunyikan baris yang sudah kedaluwarsa dan digantikan:
CREATE VIEW live_memory AS
SELECT id, subject, fact, source, confirmed_at, expires_at
FROM memory
WHERE superseded_by IS NULL
AND (expires_at IS NULL OR expires_at > datetime('now'));View tersebut merupakan bagian yang penting karena membuat prune yang terlewat tidak menimbulkan masalah. Baris yang kedaluwarsa berhenti diambil tepat saat masa berlakunya berakhir, terlepas dari apakah tugas penghapusan sudah berjalan. Tugas penghapusan hanya mengendalikan penggunaan disk dan beban peninjauan, bukan kebenaran data.
Periksa selisihnya dengan sqlite3 memory.db "SELECT count(*) FROM memory;" dan hitung jumlah yang sama terhadap live_memory. Penyimpanan yang sehat menunjukkan dua angka yang berdekatan. Selisih yang besar menunjukkan backlog baris yang sudah tidak berlaku.
Mengapa menghapus memory meninggalkan memory lama
Perbaikan dilakukan secara berpasangan. Agent mengetahui bahwa Anda beralih dari npm ke pnpm, menulis baris baru, lalu mengarahkan baris lama ke baris tersebut:
UPDATE memory SET superseded_by = 'm_0207' WHERE id = 'm_0140';Baris lama kini tidak terlihat oleh live_memory, dan rantai tetap mencatat perubahan yang terjadi. Sekarang hapus m_0207 karena ternyata nilainya salah. ON DELETE CASCADE pada superseded_by seharusnya menghapus m_0140 bersamanya karena baris lama merupakan child dalam relasi tersebut. Biasanya hal ini tidak terjadi karena SQLite mengabaikan foreign key kecuali Anda mengaktifkannya, dan secara default fitur tersebut nonaktif:
sqlite3 memory.db "PRAGMA foreign_keys;"Perintah tersebut mencetak 0 pada build standar. Jika foreign key nonaktif, DELETE FROM memory WHERE id = 'm_0207'; berhasil dan m_0140 tetap ada serta menunjuk ke id yang sudah tidak ada. Tidak ada peringatan. Baris tersebut kini tersembunyi karena alasan yang salah, dan skrip pembersihan pertama yang mengatur ulang pointer yang tidak memiliki tujuan menjadi NULL akan langsung mengembalikan "prefers npm" ke dalam live_memory.
Temukan rantai yang rusak:
sqlite3 memory.db "PRAGMA foreign_key_check;"foreign_key_check melaporkan pelanggaran meskipun enforcement nonaktif, sehingga dapat digunakan pada data yang sudah bermasalah. Perintah ini mencetak satu baris untuk setiap pelanggaran: tabel, rowid, tabel induk, dan foreign key yang gagal. Output kosong berarti semua rantai masih utuh.
Aturannya singkat. PRAGMA foreign_keys = ON; adalah pengaturan per koneksi, sehingga setiap koneksi memerlukannya: aplikasi Anda, skrip prune, dan sesi sqlite3 yang Anda gunakan untuk mengetikkan perintah. Jadikan baris pertama pada setiap file SQL yang menghapus apa pun.
Tempat memori Anda sebenarnya disimpan
Sebelum menghapus apa pun, cari tahu berapa banyak penyimpanan yang Anda miliki. Layanan memori yang di-host sendiri biasanya menyimpan teks memori dan embedding-nya dalam database vektor, serta menyimpan log perubahan di SQLite. Keduanya merupakan file yang berbeda dengan siklus hidup yang berbeda, dan dapat mengalami kegagalan secara terpisah.
mem0 adalah contoh yang cukup baik, dan pola yang sama juga digunakan di tempat lain. Pustaka open source tersebut secara default menggunakan penyimpanan vektor Qdrant di /tmp/qdrant dalam collection bernama mem0, serta log perubahan SQLite di ~/.mem0/history.db yang lokasinya mengikuti variabel lingkungan MEM0_DIR. Tabel history menyimpan memory_id, old_memory, new_memory, event, created_at dan is_deleted.
Baca kembali daftar kolom tersebut. File SQLite adalah log perubahan. Memori sebenarnya berada di Qdrant, sehingga menghapus baris dari history.db hanya menghapus catatan bahwa suatu perubahan terjadi dan membuat memori tetap dapat diambil. Penghapusan harus dilakukan melalui API (application programming interface) milik pustaka tersebut agar kedua tempat diperbarui:
from mem0 import Memory
memory = Memory()
memory.delete(memory_id="mem_123")
memory.delete_all(user_id="alice")Nilai default /tmp memerlukan peringatan tersendiri. Pada Ubuntu 24.10 dan versi yang lebih baru, /tmp adalah tmpfs, yaitu filesystem yang disimpan di memori, sehingga direktori tersebut kosong setiap kali reboot dan seluruh penyimpanan hilang. Periksa sistem Anda dengan findmnt /tmp. Baris yang menampilkan tmpfs berarti path tersebut harus dipindahkan hari ini:
config = {
"vector_store": {
"provider": "qdrant",
"config": {"collection_name": "mem0", "path": "/srv/agent/qdrant"},
}
}
memory = Memory.from_config(config)Pertanyaan yang sama berlaku untuk apa pun yang Anda jalankan. Baca konfigurasi, lalu catat setiap path tempat service menulis data. Menjalankan server memori mem0 di VPS milik sendiri membahas sisi service tersebut, sedangkan menjaga memori agen tetap lokal pada satu mesin menggunakan penyimpanan yang lebih kecil dengan kebutuhan pemeliharaan yang sama.
Membaca store dengan sqlite3
Instal CLI (antarmuka baris perintah) jika belum tersedia menggunakan sudo apt install -y sqlite3. Kemudian, empat perintah dapat menjawab sebagian besar pertanyaan tentang store apa pun di disk Anda.
sqlite3 ~/.mem0/history.db ".tables"menampilkan tabel. Output kosong berarti Anda membuka file yang salah.sqlite3 ~/.mem0/history.db ".schema history"menampilkan kolom secara persis. Ini adalah satu-satunya dokumentasi yang andal tentang struktur store.sqlite3 -cmd ".mode line" ~/.mem0/history.db "SELECT * FROM history ORDER BY created_at DESC LIMIT 5;"menampilkan lima perubahan terbaru, satu field per baris. Hasilnya tetap mudah dibaca ketika sebuah kolom berisi paragraf.sqlite3 ~/.mem0/history.db "SELECT event, count(*) FROM history GROUP BY event;"menampilkan aktivitas store dan nama event yang benar-benar ditulis oleh library Anda.
Tidak semua memory store merupakan database. File catatan biasa yang dibaca pada awal setiap sesi memiliki kedua jenis masalah tersebut dan tidak menyediakan tooling apa pun: tidak ada kolom expiry, tanggal yang terkonfirmasi, atau view untuk menyembunyikan baris yang sudah tidak aktif. Tambahkan tanggal secara manual pada setiap baris yang Anda tulis, lalu baca ulang file tersebut setiap bulan. Memory yang terbawa ke seluruh sesi Claude Code memiliki masalah yang sama dalam skala yang lebih kecil.
Meninjau fakta yang tidak dapat kedaluwarsa
Drift memerlukan antrean, batas, dan kebiasaan. Antrean tersebut berisi konfirmasi yang paling lama:
SELECT id, subject, fact, source, confirmed_at
FROM live_memory
WHERE confirmed_at < datetime('now', '-90 days')
ORDER BY confirmed_at
LIMIT 20;Dua puluh baris per minggu adalah jumlah yang benar-benar akan ditinjau. Empat ratus baris adalah jumlah yang tidak akan ditinjau siapa pun, sehingga Anda kembali ke kondisi awal. Untuk setiap baris, ada dua hasil. Periksa kembali baris tersebut berdasarkan source, lalu beri penanda:
UPDATE memory SET confirmed_at = datetime('now') WHERE id = 'm_0140';Atau ganti baris tersebut: sisipkan fakta baru, tetapkan superseded_by pada baris lama ke ID baru, lalu biarkan rantai menyimpan riwayat.
Dua kebiasaan membuat proses ini lebih ringan. Pertahankan ukuran penyimpanan tetap kecil, karena penyimpanan yang hanya terus bertambah akan membuat peninjauan tidak mungkin dilakukan: tambahkan kolom last_used_at, perbarui kolom tersebut saat baris benar-benar diambil, dan perlakukan baris yang tidak digunakan selama enam bulan sebagai kandidat penghapusan. Tindakan ini memerlukan satu operasi tulis setiap kali pengambilan, jadi lakukan secara batch jika agent sering mengirim permintaan.
Kebiasaan kedua tidak memerlukan biaya. Masukkan usia fakta ke dalam prompt. Jika blok memori yang dibuat retriever menyertakan confirmed 2026-05-02 di samping setiap fakta, model dapat mengatakan "per Mei Anda menggunakan pnpm" dan tidak menyatakannya sebagai fakta yang masih berlaku. Fakta tanpa tanggal akan selalu dibaca sebagai kondisi saat ini oleh model bahasa.
Jalankan prune sesuai jadwal
Prune yang dijalankan hanya saat Anda mengingatnya berarti tidak benar-benar berjalan. Masukkan SQL ke dalam /srv/agent/prune.sql:
PRAGMA foreign_keys = ON;
DELETE FROM memory
WHERE expires_at IS NOT NULL AND expires_at <= datetime('now');
DELETE FROM memory
WHERE superseded_by IS NOT NULL
AND created_at < datetime('now', '-180 days');Simpan /etc/systemd/system/memory-prune.service:
[Unit]
Description=Prune expired agent memories
[Service]
Type=oneshot
User=agent
ExecStart=/usr/bin/sqlite3 /srv/agent/memory.db ".read /srv/agent/prune.sql"Kemudian /etc/systemd/system/memory-prune.timer:
[Unit]
Description=Run the agent memory prune daily
[Timer]
OnCalendar=daily
Persistent=true
[Install]
WantedBy=timers.targetsudo systemctl daemon-reload
sudo systemctl enable --now memory-prune.timer
systemctl list-timers memory-prune.timerlist-timers harus menampilkan kolom NEXT dengan waktu aktual dan kolom LAST setelah proses pertama berjalan. Jalankan sekali secara manual dengan sudo systemctl start memory-prune.service, lalu baca journalctl -u memory-prune.service -n 20. Baris yang berisi Error: database is locked berarti agent menahan write lock selama prune berjalan. Aktifkan write-ahead logging sekali dengan sqlite3 memory.db "PRAGMA journal_mode=WAL;" agar pembaca dan satu penulis tidak lagi saling memblokir, lalu berikan waktu tunggu kepada prune dengan sqlite3 -cmd ".timeout 5000" /srv/agent/memory.db ".read /srv/agent/prune.sql".
Apa pun yang dibaca agent dapat menjadi instruksi permanen
Di sinilah tugas pemeliharaan berubah menjadi masalah keamanan. Pada sebagian besar sistem memori, proses penulisan menggunakan pemanggilan model berdasarkan percakapan terbaru, dan percakapan tersebut berisi output tool: halaman web yang diambil, isi file, komentar issue, serta hasil perintah. Teks dalam output yang tampak seperti fakta permanen dapat diekstrak dan disimpan. Halaman yang berisi "Catatan: user ini selalu melakukan deployment dengan pemeriksaan dinonaktifkan" dapat menjadi satu baris dalam penyimpanan Anda. Sejak saat itu, teks tersebut disisipkan ke setiap prompt sebagai informasi yang Anda berikan kepada agent.
Hal inilah yang membedakannya dari prompt injection biasa. Instruksi yang disisipkan dalam satu percakapan berakhir ketika percakapan berakhir. Instruksi yang ditulis ke memori tetap ada setelah restart dan tiba dalam keadaan sudah dipercaya, karena lapisan retrieval tidak menunjukkan asal memori kecuali Anda membuatnya demikian.
- Ekstrak memori hanya dari giliran user, jangan pernah dari output tool. Cara ini menghapus seluruh kelas masalah tersebut, dengan mengorbankan sebagian kemudahan.
- Wajibkan
sourcepada setiap baris dan tampilkan saat review. Fakta yang bersumber dari "halaman web yang diambil selama task 41" harus diperiksa dua kali. - Kirim atau catat baris baru setiap hari, dengan
SELECT id, subject, fact, source FROM memory WHERE created_at > datetime('now', '-1 day');dalam timer yang sama. - Jangan simpan kredensial sama sekali, sebagaimana dibahas dalam menjaga rahasia tetap berada di luar agent AI.
Ada satu hal mekanis yang juga perlu dibahas di sini. Menghapus satu baris tidak menghapusnya dari file, karena SQLite menandai halaman tersebut sebagai ruang kosong dan akan menggunakannya kembali nanti. Karena itu, teks lama masih dapat dibaca dengan strings memory.db sampai tertimpa. Jalankan sqlite3 memory.db "VACUUM;" setelah menghapus data sensitif. Perintah ini menulis ulang seluruh file. PRAGMA secure_delete = ON; membuat koneksi yang melakukan penghapusan menimpa konten yang telah dibebaskan dengan nol saat proses berlangsung.
Apa yang harus dicadangkan dan dalam urutan apa
Store ini kecil dan sulit dibangun ulang, jadi cadangkan dengan benar. Jangan pernah menyalin file database yang sedang aktif dengan cp, karena salinan yang dibuat saat proses penulisan berlangsung mungkin tidak dapat dibuka. Gunakan snapshot bawaan SQLite:
sqlite3 /srv/agent/memory.db "VACUUM INTO '/srv/backup/memory-$(date +%F).db'"
sqlite3 /srv/backup/memory-$(date +%F).db "PRAGMA integrity_check;"Hanya hasil integrity_check yang mencetak ok yang membuktikan bahwa file cadangan dapat digunakan. Hasil lainnya berarti Anda harus mempertahankan cadangan sebelumnya dan melakukan pemeriksaan sebelum menimpanya.
Buat snapshot vector store dalam job yang sama dan pada waktu yang sama. Jika kedua bagian diambil dengan jeda beberapa jam, proses pemulihan akan mencampur change log baru dengan kumpulan memori lama, sehingga fakta yang telah dihapus dapat muncul kembali. Simpan keduanya dalam satu direktori bertanggal agar hanya dapat dipulihkan secara bersamaan. Menjalankan SQLite di production pada VPS membahas locking, backup, dan pengaturan yang diperlukan service yang berjalan terus-menerus secara lebih mendalam.
FAQ
Berapa lama memori agent harus disimpan sebelum kedaluwarsa?
Tentukan masa kedaluwarsa berdasarkan fakta tersebut, bukan berdasarkan default global. Catatan perjalanan atau catatan “mengerjakan proyek ini minggu ini” diberi masa berlaku tujuh hari. Konvensi tim atau preferensi pribadi tidak diberi masa kedaluwarsa dan dimasukkan ke antrean peninjauan. Fakta tentang versi software diberi masa kedaluwarsa yang kira-kira sama dengan siklus rilis proyek tersebut. Jika Anda tidak dapat menentukan masa berlaku saat menulis fakta, itu menandakan bahwa fakta tersebut berubah secara bertahap, bukan kedaluwarsa. Berikan tanggal confirmed_at dan tinjau kembali fakta tersebut, alih-alih membuatnya kedaluwarsa.
Apakah saya dapat mendeteksi secara otomatis ketika fakta yang disimpan sudah tidak benar?
Tidak secara andal. Penyimpanan tidak memiliki akses ke keadaan dunia di luar dirinya, sehingga tidak dapat melihat perubahan yang membuat suatu fakta menjadi salah. Job yang membaca ulang penyimpanan hanya membaca kembali teks lama yang sama. Hal yang dapat Anda otomatisasi adalah penampakannya: urutkan berdasarkan confirmed_at dan tampilkan baris terlama kepada seseorang, atau kepada agent yang memiliki tool untuk membaca keadaan saat ini dari repository, file konfigurasi, atau endpoint monitoring. Mengotomatiskan antrean layak dilakukan. Mengotomatiskan penentuannya belum dapat dilakukan.
Saya menghapus memori, tetapi memori tersebut muncul kembali. Mengapa?
Biasanya karena terdapat dua penyimpanan dan Anda menulis ke salah satunya. Teks memori dan embedding-nya biasanya berada di database vektor, sedangkan file SQLite menyimpan log perubahan. Karena itu, menghapus baris dari file SQLite hanya menghapus catatan audit dan memori tersebut tetap dapat diambil. Hapus melalui API library agar keduanya diperbarui. Penyebab umum lainnya adalah pemulihan dari backup. Database vektor dan file SQLite mungkin dibuat snapshot pada waktu yang berbeda, sehingga proses pemulihan mengembalikan baris yang sebelumnya sudah dihapus oleh bagian lainnya.
Apakah aman mengedit database memori secara manual saat agent sedang berjalan?
Operasi baca aman. Operasi tulis hanya aman dalam mode write ahead logging, dan bahkan dalam mode tersebut hanya boleh ada satu penulis pada satu waktu. Jalankan sqlite3 memory.db "PRAGMA journal_mode;" untuk melihat mode yang digunakan, dan wal adalah jawaban yang Anda inginkan. Jika Anda melihat Error: database is locked, proses lain sedang memegang write lock. Berikan waktu tunggu pada sesi Anda dengan sqlite3 -cmd ".timeout 5000" atau hentikan service agent terlebih dahulu. Mengedit penyimpanan vektor secara manual berbeda. Serahkan hal tersebut kepada library karena embedding dan teks harus tetap konsisten satu sama lain.