Back to blog
EngineeringDate unavailable· min read

Ghosts in the Machine: Fixing Memory's Soft Delete Bug

We shipped a fix today that closed a hole in our memory system: deleted entities were haunting our queries.

What Broke

Memory entities in Sabine use soft delete. When you remove an entity, we set deleted_at and flip is_active to false instead of destroying the row. This preserves audit trails and lets us restore accidentally deleted data.

Except our queries weren't checking those flags. Deleted entities were showing up in push_back context retrieval, memory graph queries, and the EntityGrid UI. Agents were seeing ghosts—records that should have been invisible.

This surfaced during a TDD audit as deferred findings D1 and D2. We marked them for follow-up instead of blocking the original PR. Today we closed the loop.

What Changed

We added is_active = true filters across five locations:

backend/inference/push_back.py — Entity retrieval for agent context now skips soft-deleted records.

backend/magma/query.py — Memory graph queries respect active status.

lib/agent/graph_builder.py — Graph construction excludes deleted entities.

lib/agent/routers/memory.py — Memory router endpoints return only active entities.

src/components/memory/EntityGrid.audit.te — Frontend audit tests enforce the active filter.

The fix is surgical: every query that touches memory entities now includes the active check. Deleted records stay in the database for audit purposes but disappear from all operational queries and UI surfaces.

Why It Matters

Memory pollution degrades agent performance. When deleted entities leak into context retrieval, agents waste tokens processing irrelevant data. Worse, if a deleted entity contradicts current state, the agent might make decisions based on stale information.

This fix also proved our deferred findings workflow works. TDD audits catch issues we can't address immediately. We document them, create Linear tickets, and ship fixes in separate PRs. The original feature doesn't get blocked, and the debt doesn't get forgotten.

What's Next

We're building a linter rule to enforce active filters on all entity queries. Every new query should include is_active = true by default, with explicit opt-out for audit queries that need to see deleted records.

We're also adding integration tests that verify soft delete behavior end-to-end: create an entity, delete it, confirm it disappears from all query surfaces but remains accessible via direct database inspection.

Finally, we're auditing other tables with soft delete to ensure they have consistent filter patterns. Memory entities were the most visible gap, but we want to close any others before they surface in production.