feat(webapp): undo a whole agent run from the run card (#156)

* feat(webapp): undo a whole agent run from the run card

The run card was grouped for this and stopped one button short: every row
inside it carried an action, the header carried none, so reverting a bad run
meant clicking file by file and hoping you got them all.

POST /api/p/<id>/undo-run works out, for every path the run touched, the op
that puts it back — a put at the pre-run blob, or a delete for a file the run
created — and writes them all in ONE journal append. That is the atomicity
argument, not an optimization: one Put of one object either lands or it does
not, so there is no half-undone run to report. appendOps is the batch write
every path in the package now goes through; appendOp is its single-op call.

Selection is by the journal an op was READ FROM, never op.Device — that field
is arbitrary JSON any member with write access can put in their own journal,
and the card attributes rows the same way. The note form additionally requires
an empty Session, because runs.ts can never file a session-carrying op under a
note-keyed card.

Append-only throughout: the run's own ops are never edited or removed, so
one-writer-per-journal and deterministic replay both survive. The undo's ops
carry a note naming the run, so the undo is itself a run card you can undo.

The confirm asks the server for the file list rather than deriving it from the
loaded feed (paged and filterable, so a client-computed list is wrong exactly
when the run is old), lists every path with its action, and names the one thing
that can burn someone: a file a teammate changed after the run is reverted too.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(webapp): the undo confirm names the paths it will not write

planUndo already refuses a path the hub's own upload door would refuse — a
peer can push one under .bdrive/ or with a control character in it — but the
dialog listed only what the undo WOULD do, which reads as "all of it".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Snow Lee (Sungwon)
2026-08-18 14:04:35 -07:00
committed by GitHub
co-authored by Claude Opus 5
parent 6f0f474903
commit eb01953729
20 changed files with 1392 additions and 148 deletions
@@ -66,6 +66,26 @@ Two things worth knowing:
the note is free text anyone can set with `bdrive sync --note`, so joining on
it would let one person's changes attach to another person's card.
### Undoing a whole run
The card's header carries **Undo this run**: one click puts back every file
that run touched. A file it edited returns to the content it had just before
the run; a file it created is removed. The confirm lists every path with what
will happen to it before anything is written, so you can read the whole thing
and cancel.
Two things it says out loud, because they are the ones that can surprise you:
- **A file someone changed after the run is reverted too**, and the confirm
counts them. That is the same last-writer-wins rule the rest of BearDrive
follows, but it is worth seeing before you click.
- **A file already holding its pre-run content is skipped**, not written —
reported as skipped rather than as a failure.
Nothing is erased. The undo is new changes appended to history like any other,
written in a single batch, so it is itself a run card you can undo. Undoing
needs write access on the project; a read-only member sees no button.
Per-session detail is kept for 30 days by default
(`reads.session_retention_days`, see [Hub config](/reference/hub-config/));
after that the run card shows changes only. Read *counts* are unaffected — they