diff --git a/agent/agent_runtime_helpers.py b/agent/agent_runtime_helpers.py index 0b04eb38c83..72db4477e4b 100644 --- a/agent/agent_runtime_helpers.py +++ b/agent/agent_runtime_helpers.py @@ -2206,7 +2206,7 @@ def sanitize_api_messages(messages: List[Dict[str, Any]]) -> List[Dict[str, Any] def looks_like_codex_intermediate_ack( agent, - user_message: str, + user_message: Any, assistant_content: str, messages: List[Dict[str, Any]], require_workspace: bool = True, @@ -2286,7 +2286,14 @@ def looks_like_codex_intermediate_ack( if not require_workspace: return True - user_text = (user_message or "").strip().lower() + # ``user_message`` is typed ``str`` but can arrive as an OpenAI-style + # multi-part content list (``[{type:"text",...}, {type:"image_url",...}]``) + # for vision requests routed through the OpenAI-compat API server. A + # truthy list survives ``(user_message or "")`` and then ``.strip()`` + # raises ``AttributeError`` — flatten to text first. + from agent.codex_responses_adapter import _summarize_user_message_for_log + + user_text = _summarize_user_message_for_log(user_message).strip().lower() user_targets_workspace = ( any(marker in user_text for marker in workspace_markers) or "~/" in user_text diff --git a/scripts/release.py b/scripts/release.py index 084e960f495..9e7a46be36c 100755 --- a/scripts/release.py +++ b/scripts/release.py @@ -50,6 +50,7 @@ AUTHOR_MAP = { "tenoryang@outlook.com": "MarioYounger", # PR #9028 salvage (bash/sh heredoc approval, NFKC homograph fold, execute_code CREDS/BEARER/APIKEY env filter) "peet.wannasarnmetha@gmail.com": "peetwan", # PR #51841 salvage (loopback ws-ping tuning + token-frame coalescing + loop heartbeat; #48445/#50005) "297292863+Zyxxx-xxxyZ@users.noreply.github.com": "Zyxxx-xxxyZ", # PR #54287 salvage (route frontend-polled inline RPCs to _LONG_HANDLERS; #48445/#50005) + "kevenyanisme@gmail.com": "DataAdvisory", # PR #9562 salvage (flatten multi-part user_message in codex intermediate-ack detector so vision turns don't crash) "telos@apex-z.com": "telos-oc", # PR #14353 salvage (propagate custom_providers key_env into ProviderDef.api_key_env_vars; named + bare-custom self-heal paths) "256073454+Kolektori@users.noreply.github.com": "Kolektori", # PR #6436 salvage (require approval for host-bound Docker commands; container guard fast-path) "41764686+LIC99@users.noreply.github.com": "LIC99", # PR #4682 salvage (warn + default to manual on unknown approvals.mode; #4261) diff --git a/tests/agent/test_intent_ack_continuation.py b/tests/agent/test_intent_ack_continuation.py index a529020aa7c..3903ee7a8d3 100644 --- a/tests/agent/test_intent_ack_continuation.py +++ b/tests/agent/test_intent_ack_continuation.py @@ -112,6 +112,29 @@ def test_codex_only_path_requires_workspace(): ) +def test_multipart_user_message_does_not_crash_on_workspace_path(): + """#9562: vision requests forward ``user_message`` as a multi-part list. + + The OpenAI-compat API server passes the raw ``content`` field straight + through for vision turns, so ``user_message`` reaches the detector as + ``[{type:"text",...}, {type:"image_url",...}]``. The ``require_workspace`` + path flattened it with ``(user_message or "").strip()`` — a truthy list + survived and ``.strip()`` raised ``AttributeError``, killing the turn. + The text part still has to drive workspace detection. + """ + a = _agent("auto", "codex_responses") + multipart = [ + {"type": "text", "text": CODE_USER}, + {"type": "image_url", "image_url": {"url": "data:image/png;base64,AAAA"}}, + ] + msgs = [{"role": "user", "content": multipart}] + # No crash, and the text part ("review the codebase in /app") still + # satisfies the workspace requirement so the ack fires. + assert looks_like_codex_intermediate_ack( + a, multipart, CODE_ACK, msgs, require_workspace=True + ) + + def test_all_path_drops_workspace_requirement(): """The #27881 fix: opted-in turns catch non-codebase intent acks.""" a = _agent(True, "chat_completions")