diff --git a/agent/display.py b/agent/display.py index 97be479fe7..0627480457 100644 --- a/agent/display.py +++ b/agent/display.py @@ -210,18 +210,13 @@ def _scan_quoted(text: str) -> Iterator[tuple[int, str, bool]]: def _split_shell_words(segment: str) -> list[str]: - words: list[str] = [] - buf: list[str] = [] + parts: list[list[str]] = [[]] for _, ch, quoted in _scan_quoted(segment): if not quoted and ch.isspace(): - if buf: - words.append("".join(buf)) - buf = [] + parts.append([]) else: - buf.append(ch) - if buf: - words.append("".join(buf)) - return words + parts[-1].append(ch) + return ["".join(p) for p in parts if p] def _strip_shell_pipe_tail(segment: str) -> str: @@ -236,30 +231,20 @@ def _strip_shell_pipe_tail(segment: str) -> str: def _split_shell_compound(command: str) -> list[str]: """Split on unquoted ``&&`` / ``||`` / ``;`` / newline, dropping pipe tails per segment.""" - segments: list[str] = [] - buf: list[str] = [] + raw: list[list[str]] = [[]] skip = False - - def _flush() -> None: - segment = _strip_shell_pipe_tail("".join(buf).strip()) - if segment: - segments.append(segment) - buf.clear() - for i, ch, quoted in _scan_quoted(command): if skip: skip = False - continue - if not quoted and (command.startswith("&&", i) or command.startswith("||", i)): - _flush() + elif not quoted and (command.startswith("&&", i) or command.startswith("||", i)): + raw.append([]) skip = True - continue - if not quoted and ch in {";", "\n"}: - _flush() - continue - buf.append(ch) - _flush() - return segments + elif not quoted and ch in {";", "\n"}: + raw.append([]) + else: + raw[-1].append(ch) + segments = (_strip_shell_pipe_tail("".join(buf).strip()) for buf in raw) + return [s for s in segments if s] def _shell_head_word(segment: str) -> str: @@ -278,13 +263,12 @@ def _clean_shell_segment(segment: str) -> str: while i < len(words): word = words[i] if re.match(r"^\d*(?:>>?|<)$", word): - i += 2 - continue - if re.match(r"^\d*(?:>&|<&)\d+$", word) or re.match(r"^\d*>&\d+$", word): + i += 2 # operator + target + elif re.match(r"^\d*(?:>&|<&)\d+$", word): + i += 1 + else: + out.append(word) i += 1 - continue - out.append(word) - i += 1 return " ".join(out).strip() diff --git a/agent/error_classifier.py b/agent/error_classifier.py index abea68b4e3..45c310f6b8 100644 --- a/agent/error_classifier.py +++ b/agent/error_classifier.py @@ -323,26 +323,23 @@ def _v(reason: FailoverReason, **hints: Any) -> Verdict: _ROTATE_FALLBACK = {"should_rotate_credential": True, "should_fallback": True} +_ABORT_FALLBACK = {"retryable": False, "should_fallback": True} +_R = FailoverReason -_V_BILLING = _v(FailoverReason.billing, retryable=False, **_ROTATE_FALLBACK) -_V_RATE_LIMIT = _v(FailoverReason.rate_limit, **_ROTATE_FALLBACK) -_V_OVERLOADED = _v(FailoverReason.overloaded) -_V_SERVER_ERROR = _v(FailoverReason.server_error) -_V_CONTEXT_OVERFLOW = _v(FailoverReason.context_overflow, should_compress=True) -_V_PAYLOAD_TOO_LARGE = _v(FailoverReason.payload_too_large, should_compress=True) -_V_MODEL_NOT_FOUND = _v(FailoverReason.model_not_found, retryable=False, should_fallback=True) -_V_POLICY_BLOCKED = _v(FailoverReason.provider_policy_blocked, retryable=False) -_V_CONTENT_BLOCKED = _v(FailoverReason.content_policy_blocked, retryable=False, should_fallback=True) -_V_FORMAT_ERROR = _v(FailoverReason.format_error, retryable=False, should_fallback=True) -_V_AUTH_ROTATE = _v(FailoverReason.auth, retryable=False, **_ROTATE_FALLBACK) -_V_AUTH_FALLBACK = _v(FailoverReason.auth, retryable=False, should_fallback=True) -_V_TIMEOUT = _v(FailoverReason.timeout) -_V_SSL_CERT = _v(FailoverReason.ssl_cert_verification, retryable=False) -_V_IMAGE_TOO_LARGE = _v(FailoverReason.image_too_large) -_V_IMAGE_CORRUPT = _v(FailoverReason.image_corrupt) -_V_MULTIMODAL = _v(FailoverReason.multimodal_tool_content_unsupported) -_V_INVALID_ENCRYPTED = _v(FailoverReason.invalid_encrypted_content) -_V_UNKNOWN = _v(FailoverReason.unknown) +_V_BILLING = _v(_R.billing, retryable=False, **_ROTATE_FALLBACK) +_V_RATE_LIMIT = _v(_R.rate_limit, **_ROTATE_FALLBACK) +_V_AUTH_ROTATE = _v(_R.auth, retryable=False, **_ROTATE_FALLBACK) +_V_AUTH_FALLBACK = _v(_R.auth, **_ABORT_FALLBACK) +_V_MODEL_NOT_FOUND = _v(_R.model_not_found, **_ABORT_FALLBACK) +_V_CONTENT_BLOCKED = _v(_R.content_policy_blocked, **_ABORT_FALLBACK) +_V_FORMAT_ERROR = _v(_R.format_error, **_ABORT_FALLBACK) +_V_POLICY_BLOCKED = _v(_R.provider_policy_blocked, retryable=False) +_V_SSL_CERT = _v(_R.ssl_cert_verification, retryable=False) +_V_CONTEXT_OVERFLOW = _v(_R.context_overflow, should_compress=True) +_V_PAYLOAD_TOO_LARGE = _v(_R.payload_too_large, should_compress=True) +_V_OVERLOADED, _V_SERVER_ERROR, _V_TIMEOUT, _V_UNKNOWN = map(_v, (_R.overloaded, _R.server_error, _R.timeout, _R.unknown)) +_V_IMAGE_TOO_LARGE, _V_IMAGE_CORRUPT = _v(_R.image_too_large), _v(_R.image_corrupt) +_V_MULTIMODAL, _V_INVALID_ENCRYPTED = _v(_R.multimodal_tool_content_unsupported), _v(_R.invalid_encrypted_content) def _billing_hints(error_msg: str) -> Verdict: