Follow-up to the salvaged fix from #114734 (@liuhao1024), widening it to the
whole class behind #114727:
- DashboardOAuthFlow.mark_error: first reason wins. Waking the callback
waiter makes the worker fail with a derived error and its own mark_error
used to overwrite flow.error, so the dashboard/Desktop poll showed the
generic "did not include an authorization code" instead of the cause
(e.g. "OAuth cancelled by user", a pre-redirect DCR failure).
- wait_for_callback fall-through names the server, status and recorded
error instead of the fixed no-code sentence.
- exception_message(): str(exc) or the type name; used at every
mark_error call site (tui_gateway/mcp_oauth_sessions.py worker and
start_flow, hermes_cli/web_server_mcp.py, hermes_cli/web_routers/mcp.py)
so a bare TimeoutError()/RuntimeError() no longer records "".
- start_flow no longer stamps "Timed out waiting for MCP authorization URL"
over a worker failure that happened before the URL was published.
Tests trimmed to two invariants (cause reaches the waiter and is never
clobbered; blank exception text stays diagnosable).