SIGN IN SIGN UP

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