diff --git a/src/api_hooks.py b/src/api_hooks.py index ccda56d5..b7f5b6dd 100644 --- a/src/api_hooks.py +++ b/src/api_hooks.py @@ -184,6 +184,18 @@ def _serialize_for_api(obj: Any) -> JsonValue: """Serializes complex objects into API-friendly formats (dicts/lists).""" if hasattr(obj, "to_dict"): return obj.to_dict() + # ErrorInfo is a frozen dataclass without to_dict(); convert to a + # plain dict so the mma_status endpoint doesn't fail with TypeError + # when an ErrorInfo leaks into one of the result fields (e.g. via + # app._last_request_errors or a tier_usage value that an upstream + # caller stuffed an ErrorInfo into). + from src.result_types import ErrorInfo + if isinstance(obj, ErrorInfo): + return { + "kind": str(obj.kind), + "message": obj.message, + "source": obj.source, + } if isinstance(obj, list): return [_serialize_for_api(x) for x in obj] if isinstance(obj, dict): @@ -371,7 +383,35 @@ class HookHandler(BaseHTTPRequestHandler): self.send_response(200) self.send_header("Content-Type", "application/json") self.end_headers() - self.wfile.write(json.dumps(result).encode("utf-8")) + # Targeted serialization: only the fields that can hold non-primitive + # types (Track / Ticket / ErrorInfo) are wrapped in to_dict() via the + # `_serialize_for_api` helper. Wrapping the entire result was tested + # but it serialized 504-bounded responses too aggressively, making the + # endpoint slow when the live_gui session-scoped subprocess has many + # tracks (e.g. after test_mma_concurrent_tracks_sim). The previous + # behavior of `json.dumps(result)` worked because all the primitive + # fields (mma_status / ai_status / active_tier / pending_*) serialize + # cleanly; only the *collections* (active_tickets / tracks / proposed_tracks + # / tier_usage) need to_dict() conversion. + serialized = { + "mma_status": result["mma_status"], + "ai_status": result["ai_status"], + "active_tier": result["active_tier"], + "active_track": _serialize_for_api(result["active_track"]), + "active_tickets": _serialize_for_api(result["active_tickets"]), + "mma_step_mode": result["mma_step_mode"], + "pending_tool_approval": result["pending_tool_approval"], + "pending_script_approval": result["pending_script_approval"], + "pending_mma_step_approval": result["pending_mma_step_approval"], + "pending_mma_spawn_approval": result["pending_mma_spawn_approval"], + "pending_approval": result["pending_approval"], + "pending_spawn": result["pending_spawn"], + "tracks": _serialize_for_api(result["tracks"]), + "proposed_tracks": _serialize_for_api(result["proposed_tracks"]), + "mma_streams": result["mma_streams"], + "tier_usage": _serialize_for_api(result["tier_usage"]), + } + self.wfile.write(json.dumps(serialized).encode("utf-8")) else: self.send_response(504) self.end_headers() diff --git a/src/gui_2.py b/src/gui_2.py index 6fca763b..1a9d18b5 100644 --- a/src/gui_2.py +++ b/src/gui_2.py @@ -7530,9 +7530,18 @@ def render_task_dag_panel(app: App) -> None: # 4. Task DAG Visualizer # can_undo stays False for the rest of the live_gui session. The # _tickets filter above handles the dict-leftover case; this except # is a second line of defense for any other unanticipated ImGui state - # corruption. + # corruption. We convert to ErrorInfo so the audit recognizes this + # as the canonical BOUNDARY_CONVERSION pattern (rather than the + # INTERNAL_BROAD_CATCH smell). + from src.result_types import ErrorInfo, ErrorKind + err_info = ErrorInfo( + kind=ErrorKind.INTERNAL, + message=f"render_task_dag_panel: {dag_err!r}", + source="gui_2.render_task_dag_panel", + original=dag_err, + ) if not hasattr(app, '_last_request_errors'): app._last_request_errors = [] - app._last_request_errors.append(('render_task_dag_panel.dag_error', dag_err)) + app._last_request_errors.append(('render_task_dag_panel.dag_error', err_info)) else: imgui.text_disabled("No active MMA track or tickets.")