Michel wrote:but imagine the same protocol needing to have a way to say, "take back 2 moves to get to the same side's turn"
This is the "remove" command in the winboard protocol.
I don't like the undo/remove commands either. I think their effect on the protocol state is complicated to specify.
The 'undo' and 'remove' commands are a legacy of GNU Chess, and understandable from the fact that for GNU Chess the user interface was the raw protocol. So verbosity was an extremely undesirable trade, as the user would be expected to type each and every command by hand.
So having to type 'go' after every move to get a reply was (justly) deemed highly undesirable. (Although I think typing an empty line, as I do in native 'micro-Max protocol', would be bearable.) Hence the fact that WB protocol always uses 'auto-go' (= non-force) mode. And that again makes simply giving two 'undo' commands in a row to retract the last move impossible, because after the first 'undo' it wuld again be the engine's turn, and it would spontaneously redo the move. So you would have to type something like "force; undo; undo; usermove XXXX; go", and I can imagine why the designers thought that "remove" was more user friendly...
For use as a communication protocol between automated entities such arguments of course no longer play a role. I think it would indeed be highly preferable if GUIs would implement a user takeback by simply resending the game, starting with 'new' (or 'setboard'). Problem is that for many existing engines 'new' could cause hash-table clearing, or even require a complete relaunching of the engine process (for reuse=0 engines). Which I think would still be acceptable after takeback in a human-engine game, but would be very undesirable for stepping through a game / variation tree during analysis.
How about adding an "undo=0" feature to the protocol? Which engines could use to indicate they don't support undo / remove (which we would then label 'deprecated'), and let the GUI simply resend the game (new; force; usermove MOVE1; ....; usermove MOVEN; playother;) in case the user requests an 'undo'? It would then be the responsibility of the engine to make sure it does not erase its hash table during such reloading.
As I already wrote above, this is exactly the way how I implement 'undo' now in my engines. Just keep a record of all moves played or entered, and stuff that list (appropriately clipped) back into the input buffer when the engine receives 'undo'/'remove'. We might as well have the GUI do it for us...