reportsUndoDelete.test.tsxrefactor(chat): an approval that carries its own instructions (design: reversible-writes-and-approval phases 2-3) Approve was addressed by a ticket in a KV namespace with a one-hour TTL, written by only one of the two chat engines. Cards older than an hour could not be applied on either; cards from the legacy engine could not be applied at all. Every proposedDocumentDelete row ever persisted came from that engine, and not one was applicable. The ticket is gone. A proposal now carries a server-minted `id` and an `_apply` record on the card itself, which lands on the chat message we already persist. Approve sends {threadId, messageId}; the server reads the tool and args off its OWN row, so the client says which proposal and never what it does. No second store, no expiry, one path for both engines. `_apply` is stripped in two places, not one. The obvious place is on read; the one that matters is `recordedStreamWriter`, because the card reaches the browser during the turn, long before anyone reads it back — and args can carry credentials. Any future `_`-prefixed field gets the same treatment. Untangles the two questions that were one boolean. `surfaceRequiresApproval` now keys on the POLICY KEY, not on the presence of a persist hook — "should this ask" and "how would it be applied" are different things, and conflating them is why the legacy engine could not be given the ability to defer a write without silently flipping ten card types to auto-apply. A call that cannot be classified still asks, which is what granular skill tools do today, unchanged. Both engines' family paths now classify: the legacy one gets withApprovalContext, which CLONES the request context per call. Mutating the shared one would make a later approval replay this call's arguments. The double-click guard improved on the way: claiming a proposal is a conditional UPDATE ... RETURNING, atomic in Postgres, where the KV version had a real get-then-delete race. A failed apply hands the claim back so the user can retry. Phase 2 — undo now sits where the delete happens. The Files tab already had one, wired to files.restoreNode, which authorized through a scope check while its delete authorized through deleteDocumentGuarded; repointed at documents.restore so undo asks the identical question. Reports gains an undo and, for the first time, any delete error handling at all. Co-Authored-By: Claude Opus 5 <<email>>Sep 7, 2026, 11:21 PM