fix(agent): flatten multi-part user_message in codex intermediate-ack detector

Vision requests routed through the OpenAI-compat API server forward the
raw multi-part content list ([{type:"text"}, {type:"image_url"}, ...])
straight through as user_message. The codex intermediate-ack detector
flattened it with (user_message or "").strip(), so a truthy list survived
and .strip() raised AttributeError — killing any Codex-routed vision turn
that took the require_workspace path.

Route through the existing _summarize_user_message_for_log helper (which
already backs the logging/banner previews on main), and widen the param
type hint from str to Any to match how the function is actually called.

The two logging-preview sites the original PR also touched were fixed
independently on main by the conversation-loop refactor.

Co-authored-by: Hermes Agent <agent@nousresearch.com>
This commit is contained in:
Tao Yan 2026-06-30 01:22:10 -07:00 committed by Teknium
parent cd9f5cc671
commit b8ebe32866
3 changed files with 33 additions and 2 deletions

View file

@ -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

View file

@ -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)

View file

@ -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")