Skip to content

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:

json
{
  "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

json
{
  "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:

json
{
  "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

json
{
  "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.

Built with purpose.