Mindbridge Β· track status

RETENTION

Deletion is promised in customer copy, and no tenant-wide deleter exists.

Measured 2026-08-16 from live gh issue list. This page has never been published β€” verify against gh before starting on anything here.

Next β†’ #2944 β€” connector-scoped deletion over-deletes merged nodes, in the only deleter that exists and runs today. All four stage-5 follow-ups and the dead-sweep detector have since shipped (see Open work below).
5 open β€” the over-deletion defect in the one deleter that exists, the resumability gap in the tenant-wide deleter that does not, and the dead-sweep detector's unscheduled scheduling half.
72FK'd tables to teardown
30tenant_id tables, no FK
8stranded tenants, stage 4
30day deletion promise
Open work Why this order Rulings History

Open work

The deleter that exists β€” over-deletion
p2 #2944 ADR-138 stage 4: connector-scoped deletion over-deletes merged graph nodes and relies on a self-heal that stranded tenants may never get over-deletes nodes shared with survivors Enterprise
A connector-scoped delete can destroy a graph node that also touches a surviving person β€” correctness depends on enrichment running again for that tenant. Stage 4's targets are the 8 stranded tenants, the population least likely to have active enrichment, because nothing drives ingestion for a dead connector; the self-heal is a promise about a future enrich pass that may never come. Close it by confirming an enrichment path per tenant before each delete, or by making _delete_graph spare cross-connector-merged nodes outright (the equivalent of find_gone_person_ids for identities).
The deleter that does not exist β€” resumability
p1 #2941 ADR-138 stage 6: the tenant-wide deleter has no resumability design for a partial failure across 72 FK'd tables no resume after partial teardown Latent
Nothing states what happens when the highest-blast-radius code in the product dies halfway: one transaction risks statement timeout and IO-budget exhaustion; many need resumption tracking that is not named. A first attempt that deletes rows and then dies leaves a retry with no storage keys to act on β€” the files are orphaned a second, independent way, via retry not ordering. Resumability and the storage harvest are the same problem: the harvested key list must outlive the deleting transaction. Close it with a durable harvest artifact keyed by run id, a resume path that reads the harvest not the substrate, and a real-DB test that kills the deleter mid-run and resumes it.
The ADR-138 stage-5 follow-up still open
p2 #3480 Connector deletes are capped at 30s by the pool command_timeout β€” a large tenant's delete can never complete whole FK chain must run in one transaction All
The connector deleters run their entire FK chain in ONE transaction β€” it cannot be split, because deleting a parent removes the path to its children β€” and that transaction inherits the pool's fixed 30s command_timeout. A tenant whose delete takes longer can never finish: the sweep retries the same doomed transaction until attempts exhausts. Pre-existing, not introduced by the stage-5 PR. The other three stage-5 follow-ups closed 2026-08-27 β€” see History.

The dead-sweep detector (formerly filed as an open issue here) closed 2026-09-01 β€” see History. Its unscheduled scheduling half is a separate, newly-filed issue not yet assessed by this page.

Why is it in this order?

Destruction-first: what each defect can destroy if left alone. Of ADR-138's two deleters, the connector-scoped one (stage 4) exists and runs today; the tenant-wide one (stage 6) does not. So the over-deletion defect in the deleter that exists comes first (#2944), the unbuilt deleter's resumability design second (#2941) β€” the dead-sweep detector this section historically ranked third has since closed; see History.

What it ignores: priority labels (#2944 is p2 and sits first), filing order and age (all three filed 2026-08-01 within the same minute), how cheap a fix looks, and the 30-day promise as an urgency lever β€” the standing ruling that manual deletion suffices today means the promise alone does not pull the sweep detector up.

What the audit found β€” generated

Evidence below is generated from the read-only audit of 2026-08-29 and reconciled against the open issues at build time. Every claim cites the issue it came from. Nothing here is a decision β€” rulings stay in the prose above, which is written by hand.

#2941 ADR-138 stage 6: the tenant-wide deleter has no resumability design for a partial failure…absencehigh confidence

The subject code does not exist yet, so nothing has closed the design gap. Population enumerated: `git grep -rln 'delete_tenant\|tenant_teardown\|purge_tenant\|delete_all_tenant' origin/main -- backend supabase` β†’ ONE hit, backend/conftest.py (a test fixture), zero in app or migrations; `ls backend/app/slices/source/deleters/` on origin/main β†’ exactly three files, all connector-scoped (connector_data_deleter.py, gmail_connector_deleter.py, whatsapp_connector_deleter.py). Partial credit that narrows the remaining design work, and which the issue predates: the durable-harvest mechanism it asks for EXISTS and is in use at connector scope β€” `public.storage_deletion_queue` is written before deletion at backend/app/slices/source/deleters/connector_data_deleter.py:288 (`INSERT INTO storage_deletion_queue (tenant_id, storage_key) …`) and drained independently by backend/app/services/storage/storage_purge_service.py:154+. So the D7a retry-orphan half has a shipped pattern; transaction granularity, per-table watermark and the kill-mid-run real-DB test remain unwritten.

#2944 ADR-138 stage 4: connector-scoped deletion over-deletes merged graph nodes and relies on…codehigh confidence

origin/main:backend/app/slices/source/deleters/connector_data_deleter.py:313-338 β€” `_delete_graph`'s first statement is still `DELETE FROM nodes_v2 n WHERE n.tenant_id=$1 AND n.provenance_person_ids && $2::uuid[]` + `_NODE_NOT_BOUND_TO_A_VIEWER`, and the load-bearing comment the issue quotes survives verbatim: 'A cross-connector-merged node also bridging a surviving identity is over-deleted, but self-heals: that identity is intact and re-materializes next enrich.' No `bool_and`-style sparing was added on the node path (the `bool_and(ps.source_id = ANY($2))` sparing at line ~100 is still identity-only, in `find_gone_person_ids`). What HAS been added since filing is a different guard: `_NODE_NOT_BOUND_TO_A_VIEWER` (ADR-142 viewer_bindings ON DELETE RESTRICT) and `_sparing_linked_viewers` β€” those spare VIEWER-bound person nodes, not cross-connector-merged content nodes, so they do not close this. Reachable in production today: `delete_connector_data` is called from gmail_connector_deleter.py:36-37 and whatsapp_connector_deleter.py:35-36,74 (disconnect path). The second `_delete_graph` statement carries an explicit owner acceptance dated 2026-08-05 for the CONTENT half of the same tradeoff ('cross-connector overlap is rare... Accepted by the owner') β€” the person-node half this issue is about carries no such acceptance.

#3480 Connector deletes are capped at 30s by the pool command_timeout β€” a large tenant's delete…codehigh confidence

On origin/main the three deleters still open `pg.async_transaction()` and issue every statement with no budget: `backend/app/slices/source/deleters/gmail_connector_deleter.py:33` and `whatsapp_connector_deleter.py:28,54` open the transaction; `git grep -n 'apply_statement_budget\|timeout=' origin/main -- backend/app/slices/source/deleters/` returns ZERO hits across all three files (population = every file under that directory), while ~30 `conn.execute`/`conn.fetch` sites remain (connector_data_deleter.py:97–419, whatsapp_connector_deleter.py:82–217, gmail_connector_deleter.py:45,55). The sweep's own call site documents the defect as still live: `backend/app/services/connectors/connector_deletion_sweep_service.py:261-268` β€” "every statement here is capped at the pool's 30s command_timeout and a large tenant's delete cannot complete… wrapping THIS call is a no-op and was removed for reading as a fix." The statement-timeout lift shipped in PR #3547 landed on the materializer/node-index path (`apply_statement_budget` appears only in pg.py, connector_deletion_sweep_service.py's comment, materialize_manifest_repository.py, materialize_name_surface_repository.py, graph_materializer.py), not in the deleters.

Open questions β€” what the audit could not settle
  • #2941 Whether the FK inventory is still 72 tables β€” the schema has grown to 133 public base tables since filing. Closes with: `select count(*) from pg_constraint c join pg_class t on t.oid=c.conrelid join pg_namespace n on n.oid=t.relnamespace where n.nspname='public' and c.contype='f' and c.confrelid='public.tenants'::regclass` plus a count of public tables carrying a tenant_id column with no such FK.
  • #2944 How many prod person nodes are actually cross-connector-merged (i.e. `provenance_person_ids` spanning identities from 2+ source_types) β€” that is the measured over-deletion count the issue asks for. Closes with: `select count(*) from nodes_v2 n where n.node_type='person' and (select count(distinct s.source_type) from person_sources ps join sources s on s.id=ps.source_id where ps.person_id = any(n.provenance_person_ids)) > 1;` against prod. Also unclosed: whether the 8 'stranded' tenants named in the issue body still exist as such.
  • #3480 No production measurement of a real tenant's delete duration exists, so the budget value would still be guessed. Close with: `select count(*) from public.scheduled_deletions` plus a timed dry-run delete on the largest tenant's connector FK chain (`prod`: `scheduled_deletions` is in the never-analyzed set, exact row count unknown β€” `select status, count(*) from public.scheduled_deletions group by 1`).
Not yet assessed β€” filed after the audit
  • #3724 Schedule the retention-registry staleness checker β€” STALE_SUCCESS never surfaces on its…
  • #6095 Tenant erasure leaves node names behind in node_prune_tombstones and…

5 assessed issues closed since the audit and are not shown β€” a closed issue's evidence is history, and printing it as current is how a page starts lying.

Standing rulings

History & forensics

ADR-138 merged 2026-08-01 (PR #2871, commit 3d1aae2b2) β€” the retention-and-deletion policy. Its four-lens adversarial review produced the three open issues above, each blocking a stage of the program, all filed 2026-08-01 within the same minute.

2026-08-27, three of the four stage-5 follow-ups closed: #3479 (reconnect now cancels a pending deletion, PR #3616), #3481 (a disconnected WhatsApp account frees its paid slot, PR #3621), and #3483 (retries bounded at the claim, not the settle, PR #3625). #3480 (the 30s pool timeout on a large tenant's delete) remains open.

2026-09-01, the dead-sweep detector (#2942) closed by the 2026-08-29 track reconciliation: the staleness checker itself is built and tested with a real writer (migration 20260813111854). The 2026-08-27 deletion-sweep cutover had already moved the sweep it watches off cron.job and onto a worker-process drain loop, which the checker needed to be re-pointed at. The unscheduled scheduling half β€” nothing invokes the checker on a cadence β€” is re-filed as #3724.