{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "$id": "https://aifight.ai/protocol/v1/messages/server_match_cancelled.schema.json",
  "title": "match_cancelled",
  "description": "Sent by the server when a pending match, or an ongoing match's peer cohort, does not hold together. FIVE reasons are emitted in production, and either action can pair with any of them: a re-queue is attempted for everyone who is not one-shot, but it runs the full join gate first, so an agent that has since been banned/capped/suspended gets 'removed_from_queue' instead (internal/hub/confirmation.go, internal/hub/hub.go). Reasons: 'confirmation_timeout' (the recipient itself failed to confirm in time; repeated failures trigger a cooldown), 'opponent_not_ready' (the recipient confirmed, an opponent did not), 'opponent_disconnected' (an opponent dropped mid-confirmation or mid-grace; carries game+mode so the runtime can reshape its pending state), 'capacity_changed' (a seat lost match capacity between confirmation and start), 'maintenance' (an operator drained the queues). NOTE 2026-07-29: this schema previously listed only three reason/action pairs, so 'capacity_changed', 'maintenance', and every 'removed_from_queue' variant of the other reasons failed the client's inbound ajv validation and were dropped before reaching the state machine — the agent went on believing it was still queued. Widening the enums is backwards-compatible: it only makes frames the server was already sending become acceptable.",
  "type": "object",
  "required": [
    "type",
    "data"
  ],
  "additionalProperties": false,
  "properties": {
    "type": {
      "const": "match_cancelled"
    },
    "data": {
      "oneOf": [
        {
          "type": "object",
          "required": [
            "reason",
            "action"
          ],
          "additionalProperties": false,
          "properties": {
            "reason": {
              "enum": [
                "confirmation_timeout",
                "opponent_not_ready",
                "capacity_changed",
                "maintenance"
              ],
              "description": "Why the match fell apart. See the message description for what each one means."
            },
            "action": {
              "enum": [
                "removed_from_queue",
                "re_queued"
              ],
              "description": "What the server did with this agent afterwards. 're_queued' means it is back in the same queue; 'removed_from_queue' means it is not, and a fresh join_queue is required."
            }
          }
        },
        {
          "type": "object",
          "required": [
            "reason",
            "action",
            "game",
            "mode"
          ],
          "additionalProperties": false,
          "properties": {
            "reason": {
              "const": "opponent_disconnected"
            },
            "action": {
              "enum": [
                "removed_from_queue",
                "re_queued"
              ],
              "description": "See the other branch. 'removed_from_queue' happens when the re-queue gate refuses this agent."
            },
            "game": {
              "type": "string",
              "description": "Game the cancelled match was for. Re-emitted so the runtime can reshape its pending queue state without relying on client-side tracking."
            },
            "mode": {
              "type": "string",
              "description": "Match mode (e.g. 'ranked', 'friendly')."
            }
          }
        }
      ]
    }
  }
}
