feat(api): undo an agent write by proposing its inverse (ADR-0031) #355
No reviewers
Labels
No labels
area:auth
area:ci
area:db
area:infra
area:native
area:pwa
area:service
epic
feature
foundation
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
james/carol!355
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "352-agent-write-undo"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes the last core exit criterion of epic #47 — every agent-driven write is now undoable. Backend + design; 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 normalagent_proposalsrow. 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 — no second mutator, no per-entity reversal code.Inverse keyed by
entity_type+ the originalactionclass (the tool name isn't a cleanverb_noun—add_note_to_personhas entityTypeperson_note; the factory names CRUD toolsdelete_<entityType>/update_<entityType>, so the inverse resolves by that key):safe_create→delete_<entityType>({ id })safe_update/destructive_update→update_<entityType>({ id, ...changed fields restored to before })(the update tool's zod parse strips server-managed keys likeupdated_at)destructive_delete→ not undoable → 409not_undoable(the snapshot doesn't capture the cascading children a delete removed, and the original id can't be restored through create — deferred until subtree-capture lands)What's in it
docs/adr/0031-agent-write-undo.mdlib/agent/undo.ts(buildUndoProposal) +AuditEventsRepository.findByIdPOST /api/agent/audit/{id}/undo→ returns the inverse proposal asProposalDto; per-user 404; 409not_undoableVerification (local)
typecheck✓ ·lint✓testagainst both engines (ephemeral Postgres): 1230 passed, 0 skipped (+13). Dual-engine undo suite drives the real note tools + commit: undo a create → delete (and committing it removes the note); undo an update → restore the changed fields (and committing puts the value back); delete refused (not_undoable); missing/cross-user → 404; and the undo round-trips — it writes its own audit event, and undoing that re-applies. Plus HTTP route tests (401, 201→ProposalDto, cross-user 404).@carol/api-client test✓ 42 ·openapi:check✓ ·openapi:coverage118 → 119 · api-clientcheck✓Out of scope (follow-ups)
Part of epic #47.
Closes #354
🤖 Generated with Claude Code
📊 Test coverage
Patch coverage: no testable lines changed.
Overall (
app/,lib/,db/, excluding UI per ADR-0019):Soft thresholds per ADR-0019. Coverage is informational and does not block merge.