How to handle canvas conflicts
Goal: when a canvas write comes back as conflict, recover without overwriting whatever a peer changed in the meantime.
1. Always send expectedRevision
Every write to a canvas carries the revision the caller believes is current. Read the canvas first if you don't already have it:
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "canvas_get_state",
"arguments": { "workspaceId": "<workspace>", "canvasId": "<canvas>" }
}
}Note the revision in the result — this is the expectedRevision your next write should carry.
2. Send the write
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "canvas_move_elements",
"arguments": {
"workspaceId": "<workspace>",
"canvasId": "<canvas>",
"expectedRevision": 41,
"reason": "Align the three notes into a row"
}
}
}3. Check the outcome explicitly
Every canvas write returns one of applied, proposed, conflict, or rejected — never a bare success/failure boolean. Do not treat "no thrown error" as "applied." If the outcome is conflict, the response carries both revisions:
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"resultType": "tools/call",
"content": [{
"type": "text",
"text": "{\"outcome\":\"conflict\",\"expectedRevision\":41,\"currentRevision\":43}"
}],
"isError": false
}
}4. Re-read the canvas
Call canvas_get_state again to get the current scene and revision — 43 in this example, not 41. Do not simply resubmit the old write with the revision number bumped; the scene itself may have changed underneath your intended edit.
5. Recompute your change against the current scene
Reapply your intended change on top of the scene you just re-read, not on top of the stale one you started from.
6. Resubmit with the new revision
{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "canvas_move_elements",
"arguments": {
"workspaceId": "<workspace>",
"canvasId": "<canvas>",
"expectedRevision": 43,
"reason": "Align the three notes into a row"
}
}
}7. Treat repeated conflicts as contention, not a bug
If a caller keeps losing the race, that's evidence of concurrent activity on the same canvas, not evidence of a broken client. Back off and retry rather than looping tightly.
8. Don't confuse conflict with rejected
A rejected outcome — an insufficient role, or a reason containing a newline or control character — is not fixed by re-reading and resubmitting. Fix the actual cause (request a role grant, or send a clean single-line reason) before retrying. See Canvas Roles.