feat(api): make agent deletes undoable (subtree capture + restore-by-id) #362

Open
opened 2026-06-29 13:19:04 +00:00 by james · 0 comments
Owner

ADR-0031 / #354 deliberately left destructive_delete un-undoable: POST /api/agent/audit/{id}/undo returns 409 not_undoable for deletes, and the activity UI (#356) hides the undo affordance for the delete-family tools. This ticket removes that limitation.

Why it's hard today (from ADR-0031)

  1. The audit_events before snapshot captures only the single deleted row, not the cascading children a delete removes (deleting a Person cascades to its notes, contacts, organization links). Re-creating the row alone silently loses the subtree.
  2. Restoring the exact original id (so existing references hold) isn't expressible through the create tools, which generate fresh ids.

Scope

  • Capture the full deleted subtree in the audit record at delete time (a structured snapshot of the row + its cascading children), so an undo has everything it needs.
  • Restore-by-id repository methods (insert with a specified id, including children) so the inverse re-creates the exact rows.
  • Extend buildUndoProposal so destructive_delete produces a working inverse (a restore), going through the same propose-then-confirm + audit machinery.
  • Conflict handling: refuse/withhold if an id now collides or a parent no longer exists; surface a clear message.
  • Dual-engine tests (delete → undo → row + children restored with original ids); update the isUndoable client heuristic + the activity copy.

Design first (extends ADR-0031). Part of epic #47.

ADR-0031 / #354 deliberately left `destructive_delete` un-undoable: `POST /api/agent/audit/{id}/undo` returns 409 `not_undoable` for deletes, and the activity UI (#356) hides the undo affordance for the delete-family tools. This ticket removes that limitation. ## Why it's hard today (from ADR-0031) 1. The `audit_events` `before` snapshot captures only the **single deleted row**, not the cascading children a delete removes (deleting a Person cascades to its notes, contacts, organization links). Re-creating the row alone silently loses the subtree. 2. Restoring the **exact original id** (so existing references hold) isn't expressible through the create tools, which generate fresh ids. ## Scope - **Capture the full deleted subtree** in the audit record at delete time (a structured snapshot of the row + its cascading children), so an undo has everything it needs. - **Restore-by-id** repository methods (insert with a specified id, including children) so the inverse re-creates the exact rows. - Extend `buildUndoProposal` so `destructive_delete` produces a working inverse (a restore), going through the same propose-then-confirm + audit machinery. - Conflict handling: refuse/withhold if an id now collides or a parent no longer exists; surface a clear message. - Dual-engine tests (delete → undo → row + children restored with original ids); update the `isUndoable` client heuristic + the activity copy. Design first (extends ADR-0031). Part of epic #47.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
james/carol#362
No description provided.