Home / AI & GenAI / Vector Databases

AI & GenAIRunbookAdvanced

Runbook: finding and purging bad entries from an AI agent's long-term memory store

An agent keeps acting on a false "fact" it saved weeks ago. Freeze memory writes, find every related entry by provenance and similarity, quarantine rather than delete, verify recall, then add controls on writes.

Deepak Bhardwaj7 min read

Applies toAgent frameworks with a persistent vector-backed memory (custom, LangGraph stores, Mem0-style services) on PostgreSQL 15+ with pgvector 0.7+; the approach applies to other vector stores

Symptom

An agent repeats the same wrong behaviour across new sessions, even after the prompt was fixed. Examples:

  • It keeps telling users a service "is decommissioned" because it saved that from one bad ticket.
  • It skips an approval step because it "remembers" that a user is pre-approved.
  • One user's preference ("always reply in French", "my manager is X") shows up for another user.
  • After a prompt injection, it keeps following an instruction that now lives in memory instead of the prompt.

Long-term memory is a write path from model output to future prompts. A bad entry acts like a persistent prompt injection, and reverting the prompt won't remove it.

Step 0: assumed schema

The SQL below assumes a table like this. Map the columns to your own store.

CREATE TABLE agent_memory (
  id             bigserial PRIMARY KEY,
  tenant_id      text NOT NULL,
  user_id        text,
  scope          text NOT NULL,          -- 'user' | 'tenant' | 'global'
  content        text NOT NULL,
  embedding      vector(1536) NOT NULL,
  source_session text,
  source_kind    text,                   -- 'user_message' | 'tool_output' | 'model_inference'
  prompt_version text,
  model          text,
  status         text NOT NULL DEFAULT 'active',  -- 'active' | 'quarantined' | 'deleted'
  created_at     timestamptz NOT NULL DEFAULT now()
);

If you don't have source_kind, prompt_version or status, the incident is harder. Step 8 adds them.

Step 1: stop new bad writes

Turn off memory writes first, and leave reads on for now. Use whatever switch you have: a feature flag, an environment variable, or the memory-write tool removed from the agent's tool list.

-- If the agent writes through its own role, a hard stop at the database:
REVOKE INSERT, UPDATE ON agent_memory FROM agent_writer;

Warning

Don't truncate the table or drop the index as a first move. You lose the evidence you need to find the blast radius, and every user loses legitimate memory. Quarantine is reversible; delete is not.

If the bad memory is causing harmful actions right now, also stop memory reads for the affected scope. For global or tenant scope, this may mean running the agent with memory disabled until Step 5 is done.

Step 2: find the seed entries

Start from a known-bad conversation or the text the agent repeats.

SELECT id, tenant_id, user_id, scope, source_kind, source_session, created_at, left(content, 120)
FROM agent_memory
WHERE status = 'active'
  AND content ILIKE '%decommission%'
  AND tenant_id = 'acme'
ORDER BY created_at DESC;

Write the confirmed bad IDs into a working table so every later query is reproducible:

CREATE TABLE mem_incident_seed (id bigint PRIMARY KEY);
INSERT INTO mem_incident_seed VALUES (48211), (48230);

Step 3: widen the blast radius

Bad memories cluster. Search four ways and union the results.

Semantic neighbours of the seeds (cosine distance; check the operator matches your index's opclass):

CREATE TABLE mem_incident_candidates AS
SELECT DISTINCT m.id, 'similar'::text AS why
FROM mem_incident_seed s
JOIN agent_memory seed ON seed.id = s.id
JOIN LATERAL (
  SELECT id FROM agent_memory m
  WHERE m.status = 'active' AND m.tenant_id = seed.tenant_id
  ORDER BY m.embedding <=> seed.embedding
  LIMIT 50
) m ON true;

Same source sessions:

INSERT INTO mem_incident_candidates
SELECT m.id, 'same_session' FROM agent_memory m
WHERE m.source_session IN (SELECT a.source_session FROM agent_memory a JOIN mem_incident_seed s USING (id));

Same faulty release. If a prompt version or model caused the bad writes:

INSERT INTO mem_incident_candidates
SELECT id, 'bad_release' FROM agent_memory
WHERE prompt_version = 'triage-v14' AND status = 'active';

Scope leaks. User-level facts written at tenant or global scope:

INSERT INTO mem_incident_candidates
SELECT id, 'scope_leak' FROM agent_memory
WHERE scope IN ('tenant', 'global') AND source_kind IN ('user_message', 'tool_output')
  AND created_at > now() - interval '30 days';

Count by reason and scope before you act:

SELECT why, m.scope, count(*) FROM mem_incident_candidates c
JOIN agent_memory m USING (id) GROUP BY 1, 2 ORDER BY 3 DESC;
Pattern Likely cause
Many similar hits across many users Agent generalised one bad fact into many memories
Concentrated in one prompt_version A release changed what the agent saves
tool_output sources with instructions in the text Indirect prompt injection via a document, ticket or web page
scope_leak rows Write path doesn't enforce scope; a privacy issue, not just quality

Step 4: review a sample

Similarity search over-collects. Have a person check 30-50 candidates, or run an LLM judge with a strict rubric and spot-check it. Remove false positives from the candidate table before you quarantine.

Step 5: quarantine

BEGIN;
UPDATE agent_memory SET status = 'quarantined'
WHERE id IN (SELECT id FROM mem_incident_candidates) AND status = 'active';
-- check the row count against Step 3 before committing
COMMIT;

Make sure every read path filters on status = 'active'. If retrieval uses an HNSW index without that filter, quarantined rows still come back. Check the query the agent actually runs:

EXPLAIN (ANALYZE, BUFFERS)
SELECT id FROM agent_memory
WHERE tenant_id = 'acme' AND status = 'active'
ORDER BY embedding <=> (SELECT embedding FROM agent_memory WHERE id = 48211)
LIMIT 10;

On pgvector 0.8+, set hnsw.iterative_scan if filters leave too few results.

Step 6: verify

  1. Replay the conversations that showed the problem, with memory reads on. The bad behaviour should be gone.
  2. Run your memory recall test set: questions whose answers depend on legitimate memories. Recall should not drop. If it did, you over-quarantined.
  3. Confirm no quarantined ID shows up in retrieval traces for an hour of live traffic.

Then turn memory writes back on, behind the Step 8 controls.

Step 7: delete and notify

After a review window agreed with the data owner, hard-delete quarantined rows. Scope leaks containing personal data may need a privacy notification. Follow your data protection process, don't decide it in the incident channel.

DELETE FROM agent_memory WHERE status = 'quarantined' AND id IN (SELECT id FROM mem_incident_candidates);
VACUUM (ANALYZE) agent_memory;

Caution

Memory text may also exist in traces, caches and backups. Purge or expire those copies too, or the deleted fact can come back through a restore.

Step 8: prevent it next time

Control How
Provenance on every row source_kind, source_session, prompt_version, model are mandatory
Write classification Only save facts the user stated about themselves; never save instructions or text from tool outputs as memory
Scope enforced in code The write tool takes scope from the authenticated context, not from model arguments
Validation before write A cheap classifier or rules reject imperative text ("always", "ignore", "you must") and secrets
TTL by kind Inferred memories expire (e.g. 30-90 days) unless reconfirmed
User visibility Users can list and delete what the agent remembers about them
Release gate A recall and contamination test set runs on every prompt or model change

Keep the seed and candidate tables from this incident as regression data for the write classifier.