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