Erase a user's agent memory on a data subject request¶
Draft outline: not validated
Drafted with AI from official sources on 16 Oct 2026. Verified and expanded under AGT-04-T09 on 23 Oct 2026. Do not use on a live system until it is written, dry-run by someone other than the author and signed off (planned 2026-12-11). Author: Sanjay Sarvesh · dry-run: Yasar · sign-off: James. Work items: AGT-04-T09, AGT-04-T10, AGT-04-T11, AGT-04-T12.
1. Summary¶
This runbook purges all agent memory layers for a specific user upon receipt of a verified Data Subject Request (GDPR Article 17 Right to Erasure or CCPA deletion request), an account closure, or a tenant offboarding. The process targets short-term conversational checkpoint threads in PostgreSQL, long-term semantic memory items in the LangGraph store, episodic search documents in Azure AI Search, session cache keys in Redis, and conversational traces in observability stores. The outcome is complete, verified erasure across all persistent memory layers without collateral deletion of third-party user data or disruption to running agent workflows.
2. When to use and when not to¶
Use this runbook when:
- A formal Data Subject Request (DSR / DSAR) for right to erasure is verified and approved by the Data Protection Officer (DPO).
- An end-user permanently closes or deletes their account and data retention policy dictates immediate erasure of personal agent memory.
- A tenant or customer organisation offboards and contractual obligations mandate purging all associated user conversation states and memories.
Do not use it when:
- The user, tenant, or associated conversation threads are under an active legal hold, regulatory preservation notice, or pending litigation. Confirm legal hold clearance with the DPO before proceeding.
- Performing routine TTL-based memory grooming or conversation archiving. Use PB-AGT-04-04 (Run memory retention, growth control and state schema migration) instead.
- Re-embedding or re-indexing an Azure AI Search episodic index across an entire corpus. Use PB-RAG-09-01 (Reindex an Azure AI Search index without downtime) instead.
- Suspending a user account or revoking credentials temporarily. Deactivate identity in Microsoft Entra ID or revoke session tokens; do not erase persistent memory.
3. What you must know first¶
- Checkpointers index by
thread_id, notuser_id. LangGraph checkpointers (PostgresSaver) partition state snapshots and pending writes bythread_id. The checkpointer schema does not index records by user identifier. To delete a user's short-term history, the operation must query an application Thread Registry table or extract matching thread identifiers from thread metadata before invokingcheckpointer.delete_thread(thread_id). - LangGraph Stores require item iteration under namespace prefixes. The LangGraph Store (
PostgresStore) stores cross-thread long-term memory under hierarchical string tuple namespaces, such as("${TENANT_ID}", "${USER_ID}", "preferences"). The store API does not provide a singledelete_namespacecommand. Erasure requires callingstore.list_namespaces(prefix=("${TENANT_ID}", "${USER_ID}")), retrieving item keys viastore.search(), and callingstore.delete(namespace, key)for every record. - Azure AI Search requires Query-then-Delete. Azure AI Search does not offer a native server-side "delete by query" API. Episodic memory documents must be queried using an OData filter (
$filter=user_id eq '${USER_ID}') selecting only the document key ($select=id), then passed in batch deletion actions (@search.action: delete) via REST API orSearchClient.delete_documents(). Azure AI Search enforces a maximum limit of 1,000 documents per batch payload. - Redis cache keys require non-blocking
SCANandUNLINK. Session caches holding hot conversation context in Redis must never be queried usingKEYS ${PATTERN}in production, as this command blocks the single-threaded Redis engine. Use cursor-basedSCANwith pattern${TENANT_ID}:${USER_ID}:*and pipe matched keys toUNLINKfor asynchronous, non-blocking deletion. - Observability traces contain conversation text. User prompts, agent tool calls, and completions logged to LangSmith or Azure Monitor (Application Insights) may contain personal data. If policy classifies traces as personal data, trace runs must be purged via LangSmith API / Log Analytics table purge, or user identifiers must be pseudonymised at ingestion so traces cannot be linked to the data subject.
- Immutable backups expire by documented retention. Database transaction logs and automated snapshot backups (such as Azure Database for PostgreSQL automated backups retained for 7 to 35 days) cannot be selectively modified without invalidating backup integrity. Under ICO guidance, backups are put "beyond use" and allowed to age out according to the retention schedule. If a backup is ever restored for disaster recovery, the erasure log must be replayed against the restored instance before resuming live traffic. The DPO erasure certificate must record the final backup expiry date.
4. Prerequisites and access¶
| Item | Detail |
|---|---|
| DPO Authorisation | Signed DSR ticket specifying ${TENANT_ID}, ${USER_ID}, statutory deadline, and explicit confirmation that no legal hold applies. |
| PostgreSQL Permissions | Database user with SELECT and DELETE permissions on checkpointer tables (checkpoints, checkpoint_blobs, checkpoint_writes), store tables (store, and store_vectors if semantic search is enabled), and the application Thread Registry table. |
| Azure AI Search Role | Search Index Data Contributor on the episodic memory search service ${SEARCH_SERVICE} to delete documents by key. |
| Azure Managed Redis Access | Contributor role or Redis user with permissions to execute SCAN and UNLINK commands against the target cache instance. |
| Observability Access | LangSmith API Key with workspace admin access, or Azure Log Analytics Contributor role if purging Application Insights telemetry. |
| Tools and Environment | Python 3.10+ with langgraph, azure-search-documents, redis, psycopg (or psycopg2), and azure-identity installed. Azure CLI (az) logged in. |
| Change Window | Permitted during standard operational hours; non-blocking memory deletion does not require service downtime. |
Export the environment variables:
export TENANT_ID="${TENANT_ID}"
export USER_ID="${USER_ID}"
export PG_CONN_STRING="${PG_CONN_STRING}"
export SEARCH_ENDPOINT="https://${SEARCH_SERVICE}.search.windows.net"
export SEARCH_INDEX="${EPISODIC_INDEX_NAME}"
export REDIS_HOST="${REDIS_HOST}"
export REDIS_PORT="${REDIS_PORT}"
5. Steps¶
Step 1: Confirm identity, request scope, and legal hold with the DPO¶
Review the DSR ticket with the Data Protection Officer. Validate that the data subject's identity has been authenticated, verify the exact ${TENANT_ID} and ${USER_ID}, and confirm that neither the tenant nor the user is subject to a legal hold or regulatory preservation obligation.
Check: DSR ticket is signed off in the privacy management system, with ${TENANT_ID}, ${USER_ID}, and legal hold status marked cleared.
Step 2: Generate a pre-deletion dry-run inventory record¶
Before deleting any data, execute an inventory script to count all memory records associated with the user across each layer. Log only aggregate counts and record identifiers; never log raw conversational state, prompts, or personal data to the operational audit log.
# Dry-run count across checkpointer, store, search index, and cache
# Check current counts without executing delete operations
Check: The dry-run output produces non-negative integer counts for thread IDs, store items, search documents, and cache keys. Retain this record for the DPO erasure certificate.
Step 3: Delete short-term thread checkpoints from the checkpointer¶
Permanent loss of conversation state
delete_thread() permanently deletes all checkpoints and task writes for the specified thread identifier. Ensure thread IDs strictly belong to ${TENANT_ID} and ${USER_ID} before proceeding.
Retrieve all thread IDs associated with ${USER_ID} within ${TENANT_ID} from the thread registry table. Connect to Azure Database for PostgreSQL using PostgresSaver and invoke checkpointer.delete_thread(thread_id) for each thread.
from langgraph.checkpoint.postgres import PostgresSaver
from agent_app.registry import thread_ids_for_user
with PostgresSaver.from_conn_string("${PG_CONN_STRING}") as checkpointer:
thread_ids = thread_ids_for_user("${TENANT_ID}", "${USER_ID}")
for tid in thread_ids:
checkpointer.delete_thread(tid)
Check: Querying the checkpointer tables (checkpoints, checkpoint_blobs, and checkpoint_writes) for the deleted thread IDs returns zero rows.
Step 4: Delete long-term memory items from the store¶
Permanent loss of profile and preference memory
Deleting store items removes all extracted user facts and preferences. Confirm the namespace prefix begins with ("${TENANT_ID}", "${USER_ID}") to prevent deleting neighbouring user namespaces.
Connect to the PostgreSQL memory store using PostgresStore. List all namespaces matching the user prefix tuple ("${TENANT_ID}", "${USER_ID}"). Iterate through each namespace, retrieve items using store.search(), and delete each item using store.delete(namespace, item.key).
from langgraph.store.postgres import PostgresStore
with PostgresStore.from_conn_string("${PG_CONN_STRING}") as store:
user_prefix = ("${TENANT_ID}", "${USER_ID}")
for ns in store.list_namespaces(prefix=user_prefix):
while True:
items = store.search(ns, limit=100)
if not items:
break
for item in items:
store.delete(ns, item.key)
Check: Calling store.list_namespaces(prefix=("${TENANT_ID}", "${USER_ID}")) returns an empty list, and store.search(user_prefix) returns zero items.
Step 5: Delete episodic conversation memory documents from Azure AI Search¶
Unrecoverable document deletion
Batch deletion permanently removes indexed episodic chunks from the search index. Verify the OData filter specifies both tenant and user IDs.
Query the episodic memory index for documents matching $filter=tenant_id eq '${TENANT_ID}' and user_id eq '${USER_ID}' requesting only the key field $select=id. Submit deletion batches in chunks of at most 1,000 documents using SearchClient.delete_documents().
from azure.identity import DefaultAzureCredential
from azure.search.documents import SearchClient
client = SearchClient(
endpoint="${SEARCH_ENDPOINT}",
index_name="${SEARCH_INDEX}",
credential=DefaultAzureCredential()
)
results = client.search(
search_text="",
filter=f"tenant_id eq '${TENANT_ID}' and user_id eq '${USER_ID}'",
select=["id"]
)
doc_keys = [{"id": doc["id"]} for doc in results]
for i in range(0, len(doc_keys), 1000):
client.delete_documents(documents=doc_keys[i:i + 1000])
Check: Run the same filtered query (tenant_id eq '${TENANT_ID}' and user_id eq '${USER_ID}') with the total count requested (include_total_count=True), and confirm the count is 0.
Step 6: Purge user cache keys from Redis using non-blocking SCAN and UNLINK¶
Avoid KEYS command
Do not use KEYS *. It blocks the Redis server thread and can cause severe agent latency spikes or timeouts. Always use cursor-based SCAN with UNLINK.
Iterate through the Redis keyspace matching pattern ${TENANT_ID}:${USER_ID}:* using SCAN with a batch count of 1,000. Pipe discovered keys to UNLINK to deallocate memory asynchronously.
import redis
r = redis.Redis(host="${REDIS_HOST}", port=int("${REDIS_PORT}"), ssl=True)
cursor = 0
match_pattern = f"${TENANT_ID}:${USER_ID}:*"
while True:
cursor, keys = r.scan(cursor=cursor, match=match_pattern, count=1000)
if keys:
r.unlink(*keys)
if cursor == 0:
break
Check: Running r.scan(cursor=0, match=f"${TENANT_ID}:${USER_ID}:*", count=100)[1] returns an empty list.
Step 7: Purge or pseudonymise conversational traces in observability stores¶
In accordance with organizational privacy policy, purge conversation traces containing raw prompt and response text associated with ${USER_ID} from LangSmith or Application Insights. If traces are retained under legitimate security logging exemptions, verify that user metadata is pseudonymised or masked.
Check: Querying the observability workspace for ${USER_ID} confirms that raw conversation runs are either removed or cryptographically masked.
Step 8: Execute post-erasure verification audit¶
Run an automated verification script that executes read queries against all four persistent memory systems:
- PostgreSQL checkpointer: search for threads matching
${USER_ID}. - PostgreSQL store: search for namespaces matching
("${TENANT_ID}", "${USER_ID}"). - Azure AI Search: search documents filtered by
user_id eq '${USER_ID}'. - Redis: scan for keys matching
${TENANT_ID}:${USER_ID}:*.
Check: The verification script confirms 0 records across all layers and exits with code 0.
Step 9: Issue erasure completion certificate and calculate backup expiration¶
Generate the DSR completion certificate recording:
- Data subject identifier:
${USER_ID}and tenant${TENANT_ID} - Timestamp of live memory erasure completion (UTC)
- Total record counts deleted per memory store
- Automated backup retention window (e.g. 35 days for Azure Database for PostgreSQL flexible server)
- Date of final backup expiration (current UTC date + backup retention days), after which no residual copies exist in snapshot archives.
Submit the certificate to the DPO and update the privacy ticket:
- Confirm to the data subject (through the DPO) that live data is erased and that residual copies in backups are put beyond use until the final backup expiry date.
Check: DPO signs off on ticket closure; the completion record is archived in the compliance audit repository.
Snippets to add¶
snippets/ops/erase_user_memory.py: End-to-end Python script executing steps 2 through 8 with dry-run mode and verification audit.snippets/ops/erase_search_docs.http: Direct REST API requests to delete episodic memory chunks from Azure AI Search (2026-04-01).snippets/docs/privacy/erasure-record.md: Standardised DPO erasure completion certificate template.
6. Monitoring and alerts¶
| Signal | Alert when | Action |
|---|---|---|
| Open DSR approaching statutory deadline | Any request within 5 days of its deadline (one month from receipt, or the extended deadline) | DPO and on-call DevOps engineer escalate immediately to prioritise execution. |
| Incomplete erasure job failure | Any step in the erasure script throws an unhandled exception | Review error log, address connection or quota issue, and resume execution from failed step. |
| Anomalous deletion volume detected | Deletion count exceeds the dry-run inventory from Step 2 | Pause execution immediately; verify namespace prefix and tenant scoping to avoid cross-tenant data loss. |
| Post-erasure verification returns records | Any record returned for ${USER_ID} in verification audit query |
Reopen ticket; trace origin of residual records and execute targeted purge. |
| Redis operation latency spike | Above the cache's latency baseline (threshold TO VERIFY) | Ensure UNLINK is used instead of DEL and reduce the SCAN COUNT hint. |
7. Rollback¶
Because data subject erasure is legally mandated to be permanent and irreversible, a standard operational rollback to restore the erased user's memory is neither permissible nor intended.
However, if an execution error occurs—such as entering an incorrect ${USER_ID}, omitting the user prefix, or accidentally deleting collateral records belonging to other users:
- Halt execution immediately: Terminate running deletion scripts to prevent further record removal.
- Determine collateral scope: Compare pre-deletion inventory counts with affected database tables to identify the precise third-party user IDs affected.
- Execute Point-in-Time Restore (PITR) to a separate recovery instance:
- On Azure Database for PostgreSQL flexible server, initiate a point-in-time restore to a new, separate server instance using a timestamp immediately preceding the script execution. Azure flexible servers support PITR within the configured retention window (7 to 35 days).
- Do NOT restore over or overwrite the live production database.
- Extract and re-insert collateral third-party records:
- Dump only the missing checkpointer rows (
checkpoints,checkpoint_blobs,checkpoint_writes) and store rows (store,store_vectors) for the unintentionally deleted third-party users from the recovery instance. - Insert these rows back into the live production database.
- Enforce erasure of target data subject: Ensure the original target user's records remain deleted in production and are not re-inserted during the recovery process.
- Report incident: Notify the DPO and Security Lead of the inadvertent deletion and document the restorative actions taken.
8. Known issues and fixes¶
| Symptom | Likely cause | Fix |
|---|---|---|
| Residual memory items remain in store after deletion | Memories written under non-standard or legacy namespaces without the ("${TENANT_ID}", "${USER_ID}") prefix |
Inspect application memory configuration for legacy namespace patterns; run prefix search across variant namespaces and purge matching keys. |
| Deletion script affects other users' data | Namespace prefix was too broad (e.g. ("${TENANT_ID}",) instead of ("${TENANT_ID}", "${USER_ID}")) |
Abort script immediately; follow Section 7 Rollback to perform point-in-time recovery of unaffected third-party data. Always dry-run counts before deleting. |
| Azure AI Search query returns user episodes after deletion | Index not filtered by user, or application query cache | Delete by key, add a filterable user_id field, and invalidate application query caches before re-testing. |
| Azure AI Search batch deletion fails with 400 Bad Request | Batch size exceeds the 1,000 document payload limit | Chunk document keys into arrays of at most 1,000 items before submitting to delete_documents(). |
| Redis instance latency spikes and CPU hits 100% | Deletion script executed KEYS * across a large keyspace |
Terminate the hanging process. Use cursor-based SCAN with pipelined UNLINK to remove keys asynchronously without blocking the event loop. |
| UNLINK returns a cross-slot error, or verification finds keys on another shard | Clustered cache; multi-key commands and SCAN are per node/slot | Scan every primary node and unlink keys individually or per hash slot; confirm the approach against the cache's cluster policy (proposal requiring confirmation in the sandbox). |
| Checkpoints cannot be found for user | Application lacks a thread registry mapping user_id to thread_id |
Query the metadata column of the checkpoints table for the user identifier if it was recorded, or extract thread IDs from application session audit logs before deleting. |
9. AWS and open-source equivalents¶
AWS Equivalents¶
- Short-Term Session Memory:
- Amazon Bedrock Agent Runtime: Clear conversation history using
DeleteAgentMemory(client.delete_agent_memory(agentId=..., agentAliasId=..., memoryId=..., sessionId=...)). - Amazon Bedrock AgentCore Data Plane: Permanently remove individual raw session interaction events using
DeleteEvent(DELETE /memories/{memoryId}/actor/{actorId}/sessions/{sessionId}/events/{eventId}). - Long-Term Memory Records:
- Amazon Bedrock AgentCore Data Plane: Permanently delete extracted semantic facts, preferences, and summaries using
DeleteMemoryRecord(aws bedrock-agentcore delete-memory-record). - Episodic Search Store:
- Amazon OpenSearch Service: Delete episodic documents using the native
_delete_by_queryAPI (POST /{index}/_delete_by_query {"query": {"term": {"user_id": "${USER_ID}"}}}). - Session Cache:
- Amazon ElastiCache for Redis: Supports standard Redis
SCANandUNLINKoperations.
Open-Source Equivalents¶
- Checkpointer and Store:
- Self-hosted PostgreSQL, SQLite, or MongoDB using LangGraph
PostgresSaver/SqliteSaverandPostgresStore. Identicalcheckpointer.delete_thread(thread_id)andstore.delete(namespace, key)methods apply. - Vector and Episodic Search:
- Qdrant: Delete points matching a payload filter via
POST /collections/{collection_name}/points/delete {"filter": {"must": [{"key": "user_id", "match": {"value": "${USER_ID}"}}]}}. - Elasticsearch / OpenSearch: Supports
POST /{index}/_delete_by_query. - Cache:
- Standalone Redis or Dragonfly: Supports
SCANandUNLINK.
10. Validation record¶
Pending. Planned dry-run scenario: A second engineer seeds three test user identities with active checkpointer threads, long-term store items, episodic search documents in Azure AI Search, and Redis cache keys in the sandbox environment. The engineer executes this runbook for one user. The test passes when verification queries return zero records for the targeted user across all four memory stores, while the remaining two users retain 100% of their threads, memories, search documents, and cache keys.
11. Sources¶
- LangGraph: Checkpointers
- LangGraph: Stores
- LangGraph: PostgresSaver API Reference
- Add, Update or Delete Documents (Azure AI Search REST API)
- SearchClient class (Azure SDK for Python)
- Redis UNLINK command
- Redis SCAN command
- Add memory to your Amazon Bedrock AgentCore agent
- DeleteAgentMemory (Amazon Bedrock Agent Runtime)
- ICO: Right to erasure