From 1ade8e03893c81b7df2c3eaa3d9c4089bdc14229 Mon Sep 17 00:00:00 2001 From: jepson2k <55201008+Jepson2k@users.noreply.github.com> Date: Thu, 3 Sep 2026 22:26:11 +0000 Subject: [PATCH 1/2] RobotError is waldoctl's; waldoctl pin -> v0.11.1 A frontend represents a refused command the same way whichever backend raised it, so the refusal type is the contract's. Same six fields, same wire list in both directions, and an exception rather than a dataclass, so a client can raise it as-is. make_error, the catalog and extract_robot_error are unchanged. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_014Bo12kumRx9PHnY9bL8qgn --- parol6/server/controller.py | 53 ++++++++++++++++++----------------- parol6/utils/error_catalog.py | 43 ++++------------------------ pyproject.toml | 2 +- 3 files changed, 35 insertions(+), 63 deletions(-) diff --git a/parol6/server/controller.py b/parol6/server/controller.py index ab250d4..738b085 100644 --- a/parol6/server/controller.py +++ b/parol6/server/controller.py @@ -10,9 +10,11 @@ import sys import threading import time -from dataclasses import dataclass, replace +from dataclasses import dataclass from typing import Any +import psutil +from waldoctl import ActionState from parol6.ack_policy import AckPolicy from parol6.commands.base import ( @@ -29,9 +31,18 @@ StopCommand, ) from parol6.commands.utility_commands import ResetStateCommand -from parol6.server.command_executor import CommandExecutor, QueueFullError -from parol6.server.motion_planner import MotionPlanner, PlanCommand -from parol6.server.segment_player import SegmentPlayer +from parol6.config import ( + INTERVAL_S, + MAX_POLL_COUNT, + MCAST_GROUP, + MCAST_IF, + MCAST_PORT, + MCAST_TTL, + STATUS_BROADCAST_INTERVAL, + STATUS_RATE_HZ, + STATUS_STALE_S, + TRACE, +) from parol6.protocol.wire import ( CommandCode, ToolActionCmd, @@ -40,18 +51,14 @@ pack_ok_index, unpack_rx_frame_into, ) -from parol6.utils.error_catalog import RobotError, extract_robot_error, make_error -from parol6.utils.error_codes import ErrorCode +from parol6.server.async_logging import AsyncLogHandler +from parol6.server.command_executor import CommandExecutor, QueueFullError from parol6.server.command_registry import ( CommandCategory, create_command, create_command_from_struct, discover_commands, ) -from parol6.server.state import ControllerState, StateManager -from waldoctl import ActionState -from parol6.server.status_broadcast import StatusBroadcaster -from parol6.server.async_logging import AsyncLogHandler from parol6.server.loop_timer import ( EventRateMetrics, GCTracker, @@ -59,24 +66,16 @@ PhaseTimer, format_hz_summary, ) +from parol6.server.motion_planner import MotionPlanner, PlanCommand +from parol6.server.segment_player import SegmentPlayer +from parol6.server.state import ControllerState, StateManager +from parol6.server.status_broadcast import StatusBroadcaster from parol6.server.status_cache import close_cache, get_cache from parol6.server.transport_manager import TransportManager from parol6.server.transports.mock_serial_transport import MockSerialTransport from parol6.server.transports.udp_transport import UDPTransport -from parol6.config import ( - TRACE, - INTERVAL_S, - MAX_POLL_COUNT, - MCAST_GROUP, - MCAST_PORT, - MCAST_IF, - MCAST_TTL, - STATUS_RATE_HZ, - STATUS_STALE_S, - STATUS_BROADCAST_INTERVAL, -) - -import psutil +from parol6.utils.error_catalog import RobotError, extract_robot_error, make_error +from parol6.utils.error_codes import ErrorCode logger = logging.getLogger("parol6.server.controller") @@ -415,7 +414,11 @@ def _tick_tool_cmd(self, state: ControllerState) -> None: raw_error = self._tool_cmd.robot_error or make_error( ErrorCode.MOTN_TICK_FAILED, detail=type(self._tool_cmd).__name__ ) - state.error = replace(raw_error, command_index=self._tool_cmd_index) + # The refusal type is an exception now, not a dataclass, so + # re-attributing it is a rebuild from its own wire fields. + attributed = raw_error.to_wire() + attributed[0] = self._tool_cmd_index + state.error = RobotError.from_wire(attributed) state.action_state = ActionState.ERROR state.completed_command_index = max( state.completed_command_index, self._tool_cmd_index diff --git a/parol6/utils/error_catalog.py b/parol6/utils/error_catalog.py index b102a6a..3301824 100644 --- a/parol6/utils/error_catalog.py +++ b/parol6/utils/error_catalog.py @@ -8,45 +8,14 @@ from dataclasses import dataclass -from .error_codes import ErrorCode - - -@dataclass(frozen=True) -class RobotError: - """Structured error with code, title, cause, effect, and remedy.""" - - command_index: int - code: int - title: str - cause: str - effect: str - remedy: str +from waldoctl.errors import RobotError as _RobotError - def to_wire(self) -> list: - """Serialize to a list for ormsgpack packing.""" - return [ - self.command_index, - self.code, - self.title, - self.cause, - self.effect, - self.remedy, - ] - - @staticmethod - def from_wire(data: list) -> RobotError: - """Reconstruct from a wire-format list.""" - return RobotError( - command_index=data[0], - code=data[1], - title=data[2], - cause=data[3], - effect=data[4], - remedy=data[5], - ) +from .error_codes import ErrorCode - def __str__(self) -> str: - return f"[{self.code}] {self.title}: {self.cause}" +# The refusal type is waldoctl's: a frontend represents a refused command +# the same way whichever backend raised it. Same six fields, same wire +# list in both directions, and an exception a client can raise as-is. +RobotError = _RobotError @dataclass(frozen=True) diff --git a/pyproject.toml b/pyproject.toml index 4a08877..37bf2c3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,7 +46,7 @@ dependencies = [ "psutil>=5.9", "msgspec>=0.18", "ormsgpack>=1.4.0", - "waldoctl @ git+https://github.com/Jepson2k/waldoctl.git@v0.7.0", + "waldoctl @ git+https://github.com/Jepson2k/waldoctl.git@v0.11.1", ] [tool.setuptools.packages.find] From e1a43123d3f0a7ef42e9756b6659da474228f5d9 Mon Sep 17 00:00:00 2001 From: jepson2k <55201008+Jepson2k@users.noreply.github.com> Date: Thu, 3 Sep 2026 22:47:03 +0000 Subject: [PATCH 2/2] waldoctl pin -> v0.11.2 RobotError serialises back to the wire and survives copy/pickle, which the controller's state snapshot needs. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_014Bo12kumRx9PHnY9bL8qgn --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 37bf2c3..7111869 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,7 +46,7 @@ dependencies = [ "psutil>=5.9", "msgspec>=0.18", "ormsgpack>=1.4.0", - "waldoctl @ git+https://github.com/Jepson2k/waldoctl.git@v0.11.1", + "waldoctl @ git+https://github.com/Jepson2k/waldoctl.git@v0.11.2", ] [tool.setuptools.packages.find]