Reference-counted true erasure of events. Preview → Execute → Status → Cancel.
Erasures
The only path to true event deletion. Reference-counted: events with cross-scope references are redacted (payload blanked, ID preserved); events without references are deleted from the WAL. The full surface is preview + manifest + execute + status + cancel — long-running jobs deserve a proper lifecycle.
For non-destructive forget across derived layers, use POST /v1/forget.
Stability: experimental until the refcount algorithm is hardened and the cross-workspace propagation rules complete legal review. Returns X-Cortex-Stability: experimental.
POST /v1/erasures/preview — Dry run
Capability: forget.gdpr (preview is gated by the same cap as execute — callers can't enumerate without authorization).
{
"scope": "org:acme/user:alice",
"audit_note": "DSR #1234 — preview"
}
Response
{
"preview_id": "ervw_01HX...",
"scope": "org:acme/user:alice",
"estimated_affected": {
"events": 12480, "episodes": 318, "facts": 4120, "beliefs": 318, "understanding": 12
},
"refcount_breakdown": {
"events_to_delete": 9420,
"events_to_redact": 3060,
"events_under_legal_hold": 0
},
"cross_scope_propagation": {
"affected_workspaces": [
{ "scope": "ws:acme-q3-launch", "events_referenced": 412, "co_owners": ["user:priya@acme"] }
],
"requires_capability": "forget.gdpr.cross_workspace"
},
"legal_holds": [],
"estimated_duration_ms": 240000,
"manifest_url": "/v1/erasures/preview/ervw_01HX.../manifest"
}
GET /v1/erasures/preview//manifest — Full plan
Line-by-line: every event ID to delete vs. redact, every cross-scope reference, every belief to demote. For legal review and customer approval. Manifests expire after 24 h.
POST /v1/erasures — Execute
Capability: forget.gdpr. Cross-workspace propagation additionally requires forget.gdpr.cross_workspace.
{
"scope": "org:acme/user:alice",
"from_preview_id": "ervw_01HX...",
"audit_note": "DSR #1234 — Alice exercised right to erasure",
"idempotency_key": "erasure-dsr-1234"
}
If from_preview_id is set and the manifest is no longer current (new events captured in scope since the preview), the request 409s and a fresh preview is required. If from_preview_id is omitted, the server runs a preview inline before executing.
Response — always 202
{
"erasure_id": "erasure_01HX...",
"status": "running",
"manifest_url": "/v1/erasures/erasure_01HX.../manifest",
"lifecycle_stream": "/v1/lifecycle/stream?erasure_id=erasure_01HX..."
}
GET /v1/erasures/ — Status
{
"erasure_id": "erasure_01HX...",
"status": "running",
"phase": "delete",
"fraction_complete": 0.78,
"progress": {
"deleted_events": 9420,
"redacted_events": 3060,
"demoted_beliefs": 18,
"elapsed_ms": 180000
},
"audit_id": null
}
audit_id populates when status: "completed".
POST /v1/erasures//cancel — Best-effort cancel
Stops at the next phase boundary. Already-deleted events cannot be restored (their WAL slots are gone). Response indicates what completed before cancellation took effect.
{
"erasure_id": "erasure_01HX...",
"cancellation_accepted": true,
"deleted_events_before_cancel": 4280,
"audit_id": "audit_01HX..."
}
Lifecycle events emitted
event: erasure_progress
data: { "erasure_id": "...", "phase": "enumerate", "fraction_complete": 0.12 }
event: erasure_progress
data: { "erasure_id": "...", "phase": "refcount", "fraction_complete": 0.45 }
event: erasure_progress
data: { "erasure_id": "...", "phase": "delete", "fraction_complete": 0.78,
"deleted_events": 9420, "redacted_events": 3060 }
event: erasure_complete
data: { "erasure_id": "...", "summary": {...}, "audit_id": "audit_01HX..." }
Phases in order: enumerate → refcount → delete → cleanup.