hard_delete_engagement() discovers every table with a foreign key referencing engagements.id via information_schema, then runs DELETE FROM {table} WHERE {fk_column} = :eid for each. This is correct for rows whose entire existence is scoped to the engagement (memberships, conversation_turns, assertions, etc.) — but host_account.current_engagement_id and host_account.personal_engagement_id are pointer columns on a principal's own permanent account row, not rows scoped to the engagement. When a hard-deleted engagement is referenced by either pointer, the function deletes the entire host_account row rather than clearing the pointer — deleting a real account as collateral damage from deleting one of the engagements it happened to reference.
This is exactly what happened during CR-2026-141's live verification: a synthetic engagement was created, navigated into (setting host_account.current_engagement_id via the app's normal "where am I" tracking), and then hard-deleted for cleanup. The hard-delete wholesale-removed the principal's host_account row, breaking GET /me (401 "Account record missing") and forcing that principal's browser to bounce to sign-in on every hard refresh.
hard_delete_engagement() needs to distinguish pointer columns from ownership columns. For host_account.current_engagement_id and host_account.personal_engagement_id specifically (and any future column of the same shape — a reference to an engagement on a row that is not otherwise scoped by that engagement), the correct action on the referenced engagement's deletion is UPDATE ... SET {column} = NULL WHERE {column} = :eid, not DELETE FROM {table} WHERE {column} = :eid.
The information_schema-driven discovery approach (deliberately chosen over a hand-curated table list, per the function's own docstring, to avoid drifting out of sync with new migrations) should stay — the fix is in how discovered columns are handled, not in reverting to a hand-list. One shape: classify each discovered (table, column) pair by whether the table's primary key is the same as (or derived from) the referencing column (ownership — delete) versus a genuinely independent identity that merely points at the engagement (pointer — null out). host_account is keyed on id (the principal's own id), completely independent of current_engagement_id/personal_engagement_id — a clear pointer case.
No conflict. Data-integrity fix — a principal's own account record must never be an unintended casualty of deleting an engagement it happens to reference.
Must reproduce the exact failure mode first: create a synthetic engagement, set it as a principal's current_engagement_id (via the app's own current-engagement flow, not a raw UPDATE), hard-delete the engagement, and confirm today's behavior deletes the host_account row. Then confirm the fix nulls the pointer instead, leaving the row (and every other field) intact. Verification must use a synthetic engagement and a synthetic/disposable principal path — never a real principal's account, given this CR exists precisely because that combination caused real damage.
No change to the information_schema-discovery strategy itself, or to how genuinely engagement-scoped tables (memberships, conversation_turns, assertions, etc.) are handled — those should continue to be deleted outright. No change to host_account's schema. No general audit of every FK-referencing table for the same pointer-vs-ownership question — this CR fixes the two known pointer columns on host_account; a broader audit, if warranted, is separate follow-on work.