Docs menu
GDPR / PII purge
Permanently deletes a run/thread's journal trace (purgeRun/purgeThread) — the journal's append-only philosophy's only exception, made for legal deletion. purgeRun recursively cascade-deletes sub-agent/network children as well.
What it's for / when to use it#
GNL's journal is normally append-only (see deterministic replay, exactly-once tools) — records are never deleted or overwritten. But when a user makes a GDPR "forget me" request, or PII (personal data) accidentally ends up in a run, that trace needs to be permanently deleted. purgeRun and purgeThread exist exactly for this scenario: as a deliberate, single exception to the journal's append-only rule, the entire key prefix of the relevant run or thread (model/tool/input/wf/proc/cfg/memory entries) is permanently deleted — irreversibly.
Setup / import#
import { purgeRun, purgeThread } from '@gnldev/durable';There's no separate subpackage path — purgeRun, purgeThread, and sweepRuns all come directly from @gnldev/durable's root export. The journal you're using (SqliteStorage, PostgresStorage, InMemoryStorage) must support the deletePrefix port — all four built-in adapters (InMemory/SQLite/Postgres/Redis) provide it; a journal that doesn't support it throws a clear error.
Step-by-step usage#
1. Permanently delete a run's entire trace (all runId:* keys belonging to that run, plus its memory marker):
import { purgeRun, purgeThread } from '@gnldev/durable';
const removed = await purgeRun(journal, 'run-123');
// removed: the number of journal keys deleted (parent + EVERY sub-agent/network child)Before deletion, if the run has already been added to a budget counter (see budget/quota), that cost is automatically deducted from the counter — otherwise __usage__ would go stale after the purge and the deleted run's cost would keep being counted as a ghost. This deduction only happens if the run was actually counted; it doesn't accidentally push an uncounted run (e.g. one still suspended awaiting approval) into the negative.
purgeRun is RECURSIVE: a run may have branched into other runs — children of dynamic multi-agent routing (runNetwork) (net:<runId>:<i>) or sub-agent runs opened by static agent_<name> tool calls (agent:<toolCallId>). BEFORE deletion, the parent's journal is scanned (from its tool entries and net:-prefixed keys); every child found is cascade-deleted along with its own children, at any depth — with cycle/repeat protection included. Result: a user's GDPR "forget me" request leaves no orphaned trace at any level, even if that user's PII leaked into sub-agent outputs.
2. Delete a thread's (BasicMemory-based) journal trace:
await purgeThread(journal, 'thread-abc');
// deletes the mem:thread-abc:* keyspurgeThread deletes the journal-based BasicMemory trace (mem:<threadId>:*). If you're using a separate rich memory store (e.g. vector-based), you also need to call that store's own deletion API (AgentMemory.deleteThread) separately.3. Delete a single run from Studio (requires a writable journal + operator authorization):
DELETE /runs/:id
→ { ok: true, deleted: <number of keys removed> }API reference#
purgeRun(journal, runId, seen?) → Promise<number>. Permanently deletes a run's ENTIRE trace (<runId>:* + memory marker); it is RECURSIVE — it cascade-deletes sub-agent (agent:<toolCallId>) and network (net:<runId>:<i>) children at any depth (cycle-safe). Deducts the run's cost from the budget counter before deleting (best-effort).
purgeThread(journal, threadId) → Promise<number>. Permanently deletes a thread's BasicMemory trace (mem:<threadId>:*); returns the number of deleted keys.
sweepRuns(journal, { olderThanMs, keepSuspended?, now? }) → Promise<SweepResult>. Bulk-deletes old runs via purgeRun according to a retention policy (see retention/TTL sweep).
DELETE /runs/:id is a root-level (organization-unscoped) admin endpoint — it operates on the raw journal. That's why an identity bound to an organization cannot call this endpoint (returns 403); only an operator unbound from any organization can perform a run purge — otherwise a bound identity could delete another organization's run too. The operation is irreversible and is written to the audit log as run.purge, along with the actor and the number of deleted records.