fix(canvas): claim the writer before a mutation's first read
Every canvas mutation opened its transaction with a read — a liveness check, or the row it was about to rewrite. SeaORM's SQLite backend can only issue a plain deferred `BEGIN`, so those transactions took a read snapshot and only tried to become a writer later; when any other pooled connection committed in between, SQLite could not promote the now-stale snapshot and failed the WHOLE transaction with `SQLITE_BUSY_SNAPSHOT` (517). That reaches the user as "database is locked" with nothing actually deadlocked, and `busy_timeout` does not cover it — it retries ordinary lock contention only. The runtime pool holds five connections, so the losing side of that race was ordinary use: dragging a card while an agent streams, the ACP transcript write-behind holding a connection for the whole turn. All eight mutations now open with `claim_writer`, which takes the write lock by touching the revision row's timestamp. The value is deliberately left ALONE — `bump_revision` still owns the counter at the end of the transaction, so a mutation that turns out to be a no-op, or that rolls back, consumes no revision and leaves no gap in the dense sequence clients use to tell "applied" from "refetch the whole snapshot". A fresh database has no row to touch, so the claim inserts one at "0", which is what a missing row already reads as. The read-only `snapshot` stays out of it. `delete_node` takes the claim as well, even though it happens to open with a `DELETE`: the invariant is that canvas transactions claim first, and one that holds only by accident of statement order breaks the next time a check is added above the write. `acp::manager::persist_fork_outcome` documents this same hazard and `folder_group_service` sidesteps it; this is that rule applied to the canvas.
X
xintaofei committed
e6e56e35b88992d70620bc1b685589c6fd9d680e
Parent: dde7e21