Shared cancel action — a fuz_app protocol action validating the
spec+handler tuple pattern on a notification-kind action.
Semantics: the client sends `{jsonrpc, method: 'cancel', params:
{request_id}}` to abort an in-flight request on the same socket.
register_action_ws intercepts this notification and aborts the
matching pending request's ctx.signal. Unknown ids are no-ops by design —
races between response arrival and cancel delivery are safe without extra
coordination.
The handler field is an empty stub: cancel semantics are dispatcher-owned
(the dispatcher has the {request_id → AbortController} map, not the
handler). The handler exists for symmetry with other protocol actions
like heartbeat_action; the dispatcher never calls it. Consumers
spread cancel_action (or the protocol_actions bundle from
actions/protocol.ts) into their server's actions array so spec_by_method
knows about it (enabling input validation on incoming cancels) and so
create_rpc_client codegen produces app.api.cancel() when desired —
though FrontendWebsocketClient.request({signal}) sends the cancel on
abort without needing the typed API.
Wire format is snake_case cancel with {request_id}, not MCP's
$/cancelRequest with {requestId} — fuz_app's WS transport isn't MCP,
and adopting MCP's convention would leak protocol-specific framing into
the base transport. When MCP elicitation (Phase 5) lands, a translation
layer at the MCP adapter is the right seam.