feat(api): undo an agent write by proposing its inverse (ADR-0031) #354

Closed
opened 2026-06-29 12:30:13 +00:00 by james · 0 comments
Owner

Second half of epic #47's "audit log + undo": the backend to reverse an audited agent write. Design is ADR-0031 (this PR adds it). The activity-screen undo button is a follow-up.

Design (ADR-0031, in this PR)

An undo is not a new mutator path — it loads an audit event, computes the inverse tool + input, and runs that tool's execute, which persists a normal agent_proposals row. The client confirms it through the existing proposal read (#342) + commit/reject (#51) flow. So an undo is itself confirmed, audited, and undoable, and gets conflict re-validation for free at commit time.

Inverse keyed by entity_type + the original action class (the tool name isn't a clean verb_nounadd_note_to_person → entityType person_note):

  • safe_createdelete_<entityType>({ id })
  • safe_update / destructive_updateupdate_<entityType>({ id, ...changed fields restored to before })
  • destructive_deletenot undoable (the snapshot doesn't capture cascading children, and the original id can't be restored through create) → 409 not_undoable

Scope

  • docs/adr/0031-agent-write-undo.md.
  • POST /api/agent/audit/{id}/undo → builds + persists the inverse proposal, returns it as ProposalDto (the shape the confirmation UI already consumes). Per-user 404; 409 not_undoable for deletes / missing inverse tool.
  • lib/agent/undo.ts (buildUndoProposal), AuditEventsRepository.findById.
  • zod/openapi + api-client types regenerated.

Out of scope (follow-ups)

  • The activity-screen undo button + confirmation UI.
  • Undoing deletes (needs full-subtree audit capture + restore-by-id).

Part of epic #47.

Second half of epic #47's "audit log + undo": the backend to reverse an audited agent write. Design is **ADR-0031** (this PR adds it). The activity-screen undo button is a follow-up. ## Design (ADR-0031, in this PR) An undo is **not a new mutator path** — it loads an audit event, computes the inverse tool + input, and runs that tool's `execute`, which persists a normal `agent_proposals` row. The client confirms it through the existing proposal read (#342) + commit/reject (#51) flow. So an undo is itself **confirmed, audited, and undoable**, and gets conflict re-validation for free at commit time. Inverse keyed by `entity_type` + the original `action` class (the tool name isn't a clean `verb_noun` — `add_note_to_person` → entityType `person_note`): - `safe_create` → `delete_<entityType>({ id })` - `safe_update` / `destructive_update` → `update_<entityType>({ id, ...changed fields restored to before })` - `destructive_delete` → **not undoable** (the snapshot doesn't capture cascading children, and the original id can't be restored through create) → 409 `not_undoable` ## Scope - `docs/adr/0031-agent-write-undo.md`. - `POST /api/agent/audit/{id}/undo` → builds + persists the inverse proposal, returns it as `ProposalDto` (the shape the confirmation UI already consumes). Per-user 404; 409 `not_undoable` for deletes / missing inverse tool. - `lib/agent/undo.ts` (`buildUndoProposal`), `AuditEventsRepository.findById`. - zod/openapi + api-client types regenerated. ## Out of scope (follow-ups) - The activity-screen **undo button** + confirmation UI. - Undoing deletes (needs full-subtree audit capture + restore-by-id). Part of epic #47.
james closed this issue 2026-06-29 12:46:19 +00:00
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#354
No description provided.