from __future__ import annotations

import argparse
import ast
import difflib
import hashlib
import os
import platform
import py_compile
import queue
import re
import shlex
import shutil
import subprocess
import sys
import tempfile
import threading
import time
import uuid
from collections.abc import Callable, Iterator, Sequence
from concurrent.futures import ThreadPoolExecutor, as_completed
from dataclasses import dataclass, field, replace
from datetime import datetime, timezone
from enum import Enum
from pathlib import Path, PurePosixPath
from typing import TYPE_CHECKING, Any, NamedTuple, cast

from . import _json as json

try:
    import tomllib as _toml_loader
except ModuleNotFoundError:
    _toml_loader = None

from evals.adaptive_online_policy import (
    ACTION_INVOKE_HELPER,
    ACTION_SKIP_HELPER,
    ONLINE_POLICY_ALGOS,
    ONLINE_POLICY_MODES,
    AdaptiveOnlinePolicyController,
    OnlinePolicyConfig,
    PolicyDecision,
)

if TYPE_CHECKING:
    from evals.patchers.payload_types import ContextRequestPayload
else:
    ContextRequestPayload = dict[str, Any]  # type: ignore[misc,assignment]

_REPO_ROOT = Path(__file__).resolve().parents[1]
_SRC_ROOT = _REPO_ROOT / "src"
if str(_SRC_ROOT) not in sys.path:
    sys.path.insert(0, str(_SRC_ROOT))


class Condition(str, Enum):
    TRACEBACK_ONLY = "traceback_only"
    WITH_SNAPSHOT = "with_snapshot"
    WITH_SNAPSHOT_STRUCTURED = "with_snapshot_structured"
    ADAPTIVE_GATED = "adaptive_gated"
    ADAPTIVE_LLM_DISCRETION = "adaptive_llm_discretion"


VALID_CATEGORIES = {
    "hidden_state",
    "mutability_aliasing",
    "async_temporal",
    "cross_file_contract",
    "data_shape_runtime",
    "parse_value",
    "path_import_env",
    "traceback_controls",
    # Extended categories for imported datasets.
    "syntax_error",
    "reference_error",
    "logic_error",
    "type_coercion",
}
VALID_DIFFICULTIES = {"easy", "medium", "hard"}
VALID_SNAPSHOT_DEPENDENCIES = {"required", "helpful", "none"}
PATCH_APPLY_TIMEOUT_SEC = 10
PYTEST_TIMEOUT_RETURNCODE = 124
EVAL_RECORD_SCHEMA_VERSION = 3
RECORD_MODES = {"full", "hash", "none"}
CONTEXT_PROTOCOLS = {"monolithic", "staged"}
RETRY_PROMPT_MODES = {"full", "delta", "auto"}
RETRY_CONTEXT_MODES = {"fresh", "history"}
CONTEXT_COMPACTION_MODES = {"off", "auto_overflow", "always"}
CONTEXT_COMPACTION_SCOPE_TOKENS = {"snapshot", "pytest", "files"}
ADAPTIVE_GATE_POLICIES = {"aggressive", "balanced", "conservative"}
ADAPTIVE_HELPER_BUDGET_PROFILES = {"default", "compact"}
ADAPTIVE_OPENAI_HELPER_RESPONSE_MODES = {"inherit", "edits", "diff"}
OPENAI_RESPONSE_MODE_AUTOSWITCH_MODES = {"off", "retry_to_diff"}
ADAPTIVE_ONLINE_POLICY_MODES = set(ONLINE_POLICY_MODES)
ADAPTIVE_ONLINE_POLICY_ALGOS = set(ONLINE_POLICY_ALGOS)
OPENAI_CONTEXT_WINDOW_CHARS_PER_TOKEN = 4
OPENAI_CONTEXT_WINDOW_RESERVE_TOKENS = 1024
ADAPTIVE_REWARD_LAMBDA_TOKENS_DEFAULT = 0.08
ADAPTIVE_REWARD_MU_LATENCY_DEFAULT = 0.05
ADAPTIVE_REWARD_TOKEN_SCALE_DEFAULT = 1000.0
ADAPTIVE_REWARD_LATENCY_SCALE_DEFAULT = 10.0
PATCH_VERIFIER_SCOPE_DISABLED = "disabled"
PATCH_VERIFIER_SCOPE_ADAPTIVE_ONLY = "adaptive_only"
PATCH_VERIFIER_MIN_SCORE = 2
DEFAULT_SYNTAX_PREFLIGHT_MAX_CYCLES = 2
DEFAULT_ADAPTIVE_BUDGET_TOPUP_MAX_CALLS = 1
DEFAULT_ADAPTIVE_NO_PROGRESS_TOKEN_CAP = 150_000
DEFAULT_TRANSPORT_FAIL_FAST_STREAK = 5
DEFAULT_PROGRESS_HEARTBEAT_SEC = 30.0
DEFAULT_PYTEST_OUTPUT_TAIL_LINES = 4_000
PERSISTED_PYTEST_OUTPUT_HARD_CHAR_CAP = 250_000
DEFAULT_SNAPSHOT_ARTIFACT_MAX_STRING_CHARS = 16_000
PROGRESS_VERBOSITIES = {"basic", "detailed", "trace"}
PATCHER_CONTEXT_LIMIT_ERROR_MARKERS = (
    "exceeds the available context size",
    "context size",
    "greater than the context length",
    "tokens to keep from the initial prompt is greater than the context length",
    "maximum context length",
    "prompt is too long",
)
CONTEXT_RECOVERY_FAILURE_KINDS = {"context_limit", "transport"}
RETRYABLE_HTTP_STATUS_CODES = {408, 409, 425, 429}
CONTEXT_RECOVERY_PROMPT_BUDGET = {
    "total_chars": 24_000,
    "retry_delta_chars": 4_000,
    "stdout_chars": 4_000,
    "snapshot_chars": 6_000,
    "evidence_chars": 10_000,
    "retry_history_chars": 2_000,
}
OPENAI_API_KEY_ENV_FALLBACKS = ("Z_API", "OPENAI_API_KEY", "LLMDEBUG_EVAL_OPENAI_API_KEY")
VALID_EXECUTORS = {"local", "testcontainers"}
CONTAINER_NETWORK_MODES = {"none", "bridge"}
DEFAULT_EXECUTOR = "testcontainers"
DEFAULT_CONTAINER_IMAGE = os.getenv("LLMDEBUG_EVAL_CONTAINER_IMAGE", "llmdebug-eval-runner:latest")
DEFAULT_CONTAINER_NETWORK = "none"
DEFAULT_CONTAINER_CPUS = 1.0
DEFAULT_CONTAINER_MEMORY = "1g"
DEFAULT_CONTAINER_PIDS_LIMIT = 256
DEFAULT_CONTAINER_TMPFS_SIZE_MB = 128
DEFAULT_CONTAINER_CLEANUP_STALE_GRACE_SEC = 300
EVAL_CONTAINER_LABEL_KEY = "io.llmdebug.eval.managed"
EVAL_CONTAINER_LABEL_VALUE = "1"
_CONTAINER_ACTIVE_STATES = {"running", "restarting", "paused"}
RUN_EVAL_CONFIG_SECTION = "run_eval"
RUN_EVAL_CONFIG_LOCK_SCHEMA_VERSION = 1
RUN_EVAL_LOCKFILE_REDACTED = "***REDACTED***"
RUN_EVAL_SECRET_CONFIG_LITERAL_KEYS = {"openai_api_key"}
RUN_EVAL_SECRET_CONFIG_ENV_KEYS = {"openai_api_key_env"}
RUN_EVAL_LOCKFILE_REDACTED_KEYS = {"openai_api_key"}


def _default_container_user() -> str:
    try:
        uid = getattr(os, "getuid", None)
        gid = getattr(os, "getgid", None)
        if callable(uid) and callable(gid):
            return f"{int(uid())}:{int(gid())}"
    except Exception:
        pass
    return "0:0"


DEFAULT_CONTAINER_USER = _default_container_user()


@dataclass(frozen=True)
class StageAContextData:
    blocks: list[dict[str, Any]]
    text: str
    used_fallback: bool
    requests_count: int
    chars_returned: int
    request_success: bool | None
    request_payload: ContextRequestPayload | None
    request_raw: str | None
    reason: str
    evidence_items: list[dict[str, Any]] = field(default_factory=list)


def _empty_stage_a_context(*, reason: str = "not_used") -> StageAContextData:
    return StageAContextData(
        blocks=[],
        text="",
        used_fallback=False,
        requests_count=0,
        chars_returned=0,
        request_success=None,
        request_payload=None,
        request_raw=None,
        reason=reason,
        evidence_items=[],
    )


@dataclass(frozen=True)
class ContextCompactionBundle:
    text: str
    source_sections: list[str]
    source_chars: dict[str, int]
    replace_snapshot: bool
    replace_pytest_output: bool
    replace_stage_a_context: bool
    replace_project_files: bool


@dataclass
class _ActiveRunSettings:
    deterministic: bool = False
    seed: int | None = None


_ACTIVE_RUN_SETTINGS = _ActiveRunSettings()
_ACTIVE_ONLINE_POLICY_CONTROLLER: AdaptiveOnlinePolicyController | None = None


def _first_nonempty_env(*keys: str) -> str:
    for key in keys:
        value = os.getenv(str(key), "")
        if isinstance(value, str) and value.strip():
            return value
    return ""


def _default_openai_api_key() -> str:
    return _first_nonempty_env(*OPENAI_API_KEY_ENV_FALLBACKS)


@dataclass(frozen=True)
class ConfigResolutionState:
    config_paths: list[str] = field(default_factory=list)
    value_sources: dict[str, str] = field(default_factory=dict)


def _load_toml_document(path: Path) -> dict[str, Any]:
    try:
        with path.open("rb") as handle:
            if _toml_loader is not None:
                raw = _toml_loader.load(handle)
            else:
                try:
                    import tomli  # type: ignore[import-not-found]
                except ModuleNotFoundError as exc:
                    raise ValueError(
                        "TOML parsing requires Python 3.11+ or an installed 'tomli' package."
                    ) from exc
                raw = tomli.load(handle)
    except OSError as exc:
        raise ValueError(f"cannot read config file {path}: {exc}") from exc
    except Exception as exc:
        raise ValueError(f"invalid TOML in {path}: {type(exc).__name__}: {exc}") from exc

    if not isinstance(raw, dict):
        raise ValueError(f"{path}: config root must be a TOML table/object")
    return dict(raw)


def _extract_run_eval_config_payload(path: Path, root: dict[str, Any]) -> dict[str, Any]:
    if RUN_EVAL_CONFIG_SECTION not in root:
        return root
    section = root.get(RUN_EVAL_CONFIG_SECTION)
    if not isinstance(section, dict):
        raise ValueError(f"{path}: [{RUN_EVAL_CONFIG_SECTION}] must be a TOML table/object")
    return dict(section)


def _flatten_config_mapping(data: dict[str, Any], *, prefix: str = "") -> dict[str, Any]:
    flat: dict[str, Any] = {}
    for raw_key, raw_value in data.items():
        token = str(raw_key).strip().replace("-", "_").lower()
        if not token:
            continue
        full_key = f"{prefix}{token}" if not prefix else f"{prefix}_{token}"
        if isinstance(raw_value, dict):
            flat.update(_flatten_config_mapping(dict(raw_value), prefix=full_key))
            continue
        flat[full_key] = raw_value
    return flat


def _collect_parser_actions_by_dest(
    parser: argparse.ArgumentParser,
) -> dict[str, list[argparse.Action]]:
    actions: dict[str, list[argparse.Action]] = {}
    for action in parser._actions:
        if not action.option_strings:
            continue
        if action.dest == argparse.SUPPRESS:
            continue
        actions.setdefault(action.dest, []).append(action)
    return actions


def _collect_cli_specified_dests(
    parser: argparse.ArgumentParser, argv_tokens: list[str]
) -> set[str]:
    option_to_action = parser._option_string_actions
    out: set[str] = set()
    for token in argv_tokens:
        if token == "--":
            break
        if not token.startswith("-"):
            continue
        option = token.split("=", 1)[0]
        action = option_to_action.get(option)
        if action is None:
            continue
        if action.dest == argparse.SUPPRESS:
            continue
        out.add(action.dest)
    return out


def _coerce_config_bool(raw: Any, *, key: str) -> bool:
    if isinstance(raw, bool):
        return raw
    if isinstance(raw, str):
        normalized = raw.strip().lower()
        if normalized in {"1", "true", "yes", "on"}:
            return True
        if normalized in {"0", "false", "no", "off"}:
            return False
    raise ValueError(f"config key '{key}' must be a boolean")


def _coerce_config_scalar_for_action(raw: Any, *, action: argparse.Action, key: str) -> Any:
    if isinstance(raw, (list, dict)):
        raise ValueError(f"config key '{key}' must be a scalar value")
    value = raw
    converter = getattr(action, "type", None)
    if converter is not None:
        try:
            value = converter(raw)
        except Exception as exc:
            raise ValueError(
                f"config key '{key}' has invalid value {raw!r}: {type(exc).__name__}: {exc}"
            ) from exc
    choices = getattr(action, "choices", None)
    if choices is not None and value not in choices:
        allowed = ", ".join(str(item) for item in choices)
        raise ValueError(f"config key '{key}' must be one of: {allowed}")
    return value


def _coerce_config_value_for_dest(raw: Any, *, key: str, actions: list[argparse.Action]) -> Any:
    append_action = next((a for a in actions if isinstance(a, argparse._AppendAction)), None)
    if append_action is not None:
        raw_items = raw if isinstance(raw, list) else [raw]
        return [
            _coerce_config_scalar_for_action(item, action=append_action, key=key)
            for item in raw_items
        ]

    if any(
        isinstance(action, (argparse._StoreTrueAction, argparse._StoreFalseAction))
        for action in actions
    ):
        return _coerce_config_bool(raw, key=key)

    store_action = next((a for a in actions if isinstance(a, argparse._StoreAction)), None)
    if store_action is None:
        raise ValueError(f"config key '{key}' is mapped to unsupported parser action")
    return _coerce_config_scalar_for_action(raw, action=store_action, key=key)


def _read_run_eval_config(path: Path) -> dict[str, Any]:
    root = _load_toml_document(path)
    payload = _extract_run_eval_config_payload(path, root)
    if not isinstance(payload, dict):
        raise ValueError(f"{path}: run-eval config payload must be a TOML table/object")
    return _flatten_config_mapping(payload)


def _apply_toml_configs_to_args(
    *,
    parser: argparse.ArgumentParser,
    args: argparse.Namespace,
    argv_tokens: list[str],
) -> ConfigResolutionState:
    config_paths_raw = getattr(args, "config", [])
    config_paths = [str(item).strip() for item in config_paths_raw if str(item).strip()]
    if not config_paths:
        return ConfigResolutionState(
            config_paths=[],
            value_sources=dict.fromkeys(vars(args), "default"),
        )

    actions_by_dest = _collect_parser_actions_by_dest(parser)
    cli_specified = _collect_cli_specified_dests(parser, argv_tokens)
    value_sources = dict.fromkeys(vars(args), "default")
    for key in cli_specified:
        if key in value_sources:
            value_sources[key] = "cli"

    resolved_paths: list[str] = []
    for config_path_raw in config_paths:
        path = Path(config_path_raw).expanduser()
        if not path.exists():
            raise ValueError(f"config file not found: {path}")
        if not path.is_file():
            raise ValueError(f"config path is not a file: {path}")
        path = path.resolve()
        resolved_paths.append(str(path))
        payload = _read_run_eval_config(path)
        for key, raw_value in payload.items():
            if key == "config":
                raise ValueError(f"{path}: nested 'config' keys are not supported")
            if key in RUN_EVAL_SECRET_CONFIG_LITERAL_KEYS:
                if raw_value is None or (isinstance(raw_value, str) and not raw_value.strip()):
                    continue
                raise ValueError(
                    f"{path}: '{key}' is not allowed in config files. "
                    "Use 'openai_api_key_env' to reference an environment variable."
                )
            if key in RUN_EVAL_SECRET_CONFIG_ENV_KEYS:
                env_key = str(raw_value).strip()
                if not env_key:
                    raise ValueError(
                        f"{path}: '{key}' must be a non-empty environment variable name"
                    )
                if "openai_api_key" in cli_specified:
                    continue
                env_value = os.getenv(env_key, "")
                if not env_value:
                    raise ValueError(
                        f"{path}: environment variable {env_key!r} is required by '{key}' but is empty or unset"
                    )
                args.openai_api_key = env_value
                value_sources["openai_api_key"] = f"config:{path} env:{env_key}"
                continue

            actions = actions_by_dest.get(key)
            if actions is None:
                raise ValueError(f"{path}: unknown config key '{key}'")
            if key in cli_specified:
                continue
            coerced = _coerce_config_value_for_dest(raw_value, key=key, actions=actions)
            setattr(args, key, coerced)
            value_sources[key] = f"config:{path}"

    return ConfigResolutionState(config_paths=resolved_paths, value_sources=value_sources)


def _json_compatible(value: Any) -> Any:
    if value is None or isinstance(value, (str, int, float, bool)):
        return value
    if isinstance(value, Path):
        return str(value)
    if isinstance(value, dict):
        return {str(key): _json_compatible(raw) for key, raw in value.items()}
    if isinstance(value, (list, tuple, set)):
        return [_json_compatible(raw) for raw in value]
    return str(value)


def _redact_cli_argv(argv_tokens: list[str]) -> list[str]:
    out: list[str] = []
    redact_next = False
    for token in argv_tokens:
        if redact_next:
            out.append(RUN_EVAL_LOCKFILE_REDACTED)
            redact_next = False
            continue
        if token == "--openai-api-key":
            out.append(token)
            redact_next = True
            continue
        if token.startswith("--openai-api-key="):
            out.append(f"--openai-api-key={RUN_EVAL_LOCKFILE_REDACTED}")
            continue
        out.append(token)
    return out


def _resolved_args_for_lockfile(args: argparse.Namespace) -> dict[str, Any]:
    payload: dict[str, Any] = {}
    for key, raw in vars(args).items():
        if key in RUN_EVAL_LOCKFILE_REDACTED_KEYS:
            text = str(raw).strip() if raw is not None else ""
            payload[key] = RUN_EVAL_LOCKFILE_REDACTED if text else ""
            continue
        payload[key] = _json_compatible(raw)
    return dict(sorted(payload.items()))


def _write_resolved_run_config_lockfile(
    *,
    run_id: str,
    args: argparse.Namespace,
    argv_tokens: list[str],
    run_settings_fingerprint: str,
    config_state: ConfigResolutionState,
) -> Path:
    path = _REPO_ROOT / "evals" / "artifacts" / "configs" / f"{run_id}.resolved.json"
    payload = {
        "schema_version": RUN_EVAL_CONFIG_LOCK_SCHEMA_VERSION,
        "run_id": run_id,
        "generated_at_utc": datetime.now(timezone.utc)
        .replace(microsecond=0)
        .isoformat()
        .replace("+00:00", "Z"),
        "run_settings_fingerprint": run_settings_fingerprint,
        "config_files": list(config_state.config_paths),
        "value_sources": dict(sorted(config_state.value_sources.items())),
        "cli_argv": _redact_cli_argv(argv_tokens),
        "resolved_args": _resolved_args_for_lockfile(args),
    }
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(
        json.dumps(payload, indent=2, sort_keys=True, ensure_ascii=False), encoding="utf-8"
    )
    return path


def add_execution_arguments(
    parser: argparse.ArgumentParser,
    *,
    default_executor: str = DEFAULT_EXECUTOR,
    include_defaults: bool = True,
) -> None:
    def _default(value: Any) -> Any:
        return value if include_defaults else argparse.SUPPRESS

    parser.add_argument(
        "--executor",
        choices=["local", "testcontainers"],
        default=_default(default_executor),
        help="Test execution backend.",
    )
    parser.add_argument(
        "--container-image",
        default=_default(DEFAULT_CONTAINER_IMAGE),
        help="Container image for --executor testcontainers.",
    )
    parser.add_argument(
        "--container-network",
        choices=["none", "bridge"],
        default=_default(DEFAULT_CONTAINER_NETWORK),
        help="Container network mode for --executor testcontainers.",
    )
    parser.add_argument(
        "--container-cpus",
        type=float,
        default=_default(DEFAULT_CONTAINER_CPUS),
        help="CPU limit for containerized pytest execution.",
    )
    parser.add_argument(
        "--container-memory",
        default=_default(DEFAULT_CONTAINER_MEMORY),
        help="Memory limit for containerized pytest execution (e.g. 1g).",
    )
    parser.add_argument(
        "--container-pids-limit",
        type=int,
        default=_default(DEFAULT_CONTAINER_PIDS_LIMIT),
        help="PID limit for containerized pytest execution.",
    )
    parser.add_argument(
        "--container-user",
        default=_default(DEFAULT_CONTAINER_USER),
        help="Container user for pytest execution (uid:gid).",
    )
    parser.add_argument(
        "--container-tmpfs-size-mb",
        type=int,
        default=_default(DEFAULT_CONTAINER_TMPFS_SIZE_MB),
        help="Size in MB for /tmp tmpfs in the container.",
    )
    parser.add_argument(
        "--container-mount-repo-src",
        dest="container_mount_repo_src",
        action="store_true",
        default=_default(True),
        help="Mount local src/ into container as read-only for llmdebug plugin imports.",
    )
    parser.add_argument(
        "--container-no-mount-repo-src",
        dest="container_mount_repo_src",
        action="store_false",
        default=_default(True),
        help="Do not mount local src/ into container.",
    )
    parser.add_argument(
        "--container-env",
        action="append",
        default=_default([]),
        help="Environment variable to inject in container (KEY=VALUE, repeatable).",
    )


def _parse_container_env(entries: list[str]) -> dict[str, str]:
    out: dict[str, str] = {}
    for raw in entries:
        item = str(raw).strip()
        if not item:
            continue
        if "=" not in item:
            raise ValueError(f"invalid --container-env entry {raw!r}: expected KEY=VALUE")
        key, value = item.split("=", 1)
        key = key.strip()
        if not key:
            raise ValueError(f"invalid --container-env entry {raw!r}: empty key")
        out[key] = value
    return out


def build_execution_config_from_args(args: argparse.Namespace) -> ExecutionConfig:
    executor = str(getattr(args, "executor", DEFAULT_EXECUTOR)).strip().lower()
    if executor not in VALID_EXECUTORS:
        raise ValueError(f"invalid --executor: {executor!r}")
    container_network = (
        str(getattr(args, "container_network", DEFAULT_CONTAINER_NETWORK)).strip().lower()
    )
    if container_network not in CONTAINER_NETWORK_MODES:
        raise ValueError(f"invalid --container-network: {container_network!r}")
    container_image = str(getattr(args, "container_image", DEFAULT_CONTAINER_IMAGE)).strip()
    if not container_image:
        raise ValueError("--container-image must be non-empty")
    container_cpus = float(getattr(args, "container_cpus", DEFAULT_CONTAINER_CPUS))
    if container_cpus <= 0:
        raise ValueError("--container-cpus must be > 0")
    container_pids_limit = int(getattr(args, "container_pids_limit", DEFAULT_CONTAINER_PIDS_LIMIT))
    if container_pids_limit <= 0:
        raise ValueError("--container-pids-limit must be > 0")
    container_tmpfs_size_mb = int(
        getattr(args, "container_tmpfs_size_mb", DEFAULT_CONTAINER_TMPFS_SIZE_MB)
    )
    if container_tmpfs_size_mb <= 0:
        raise ValueError("--container-tmpfs-size-mb must be > 0")
    container_user = str(getattr(args, "container_user", DEFAULT_CONTAINER_USER)).strip()
    if not container_user:
        raise ValueError("--container-user must be non-empty")
    container_memory = str(getattr(args, "container_memory", DEFAULT_CONTAINER_MEMORY)).strip()
    if not container_memory:
        raise ValueError("--container-memory must be non-empty")
    env_raw = getattr(args, "container_env", [])
    container_env = _parse_container_env(env_raw if isinstance(env_raw, list) else [])
    return ExecutionConfig(
        executor=executor,
        container_image=container_image,
        container_network=container_network,
        container_cpus=container_cpus,
        container_memory=container_memory,
        container_pids_limit=container_pids_limit,
        container_user=container_user,
        container_tmpfs_size_mb=container_tmpfs_size_mb,
        container_mount_repo_src=bool(getattr(args, "container_mount_repo_src", True)),
        container_env=container_env,
    )


def get_active_execution_config() -> ExecutionConfig:
    return _ACTIVE_EXECUTION_CONFIG


def set_active_execution_config(config: ExecutionConfig) -> ExecutionConfig:
    global _ACTIVE_EXECUTION_CONFIG
    previous = _ACTIVE_EXECUTION_CONFIG
    _ACTIVE_EXECUTION_CONFIG = config
    return previous


def _execution_config_payload(config: ExecutionConfig) -> dict[str, Any]:
    return {
        "executor": config.executor,
        "container_image": config.container_image,
        "container_network": config.container_network,
        "container_cpus": config.container_cpus,
        "container_memory": config.container_memory,
        "container_pids_limit": config.container_pids_limit,
        "container_user": config.container_user,
        "container_tmpfs_size_mb": config.container_tmpfs_size_mb,
        "container_mount_repo_src": config.container_mount_repo_src,
        "container_env_keys": sorted(config.container_env),
    }


def check_testcontainers_runtime_available() -> tuple[bool, str]:
    try:
        import docker  # type: ignore[import-not-found]
        from testcontainers.core.container import DockerContainer  # noqa: F401
    except Exception as exc:
        return (
            False,
            "testcontainers runtime is not available "
            f"({type(exc).__name__}: {exc}). Install llmdebug[evals].",
        )
    try:
        client = docker.from_env()
        client.ping()
        client.close()
    except Exception as exc:
        return False, f"docker daemon is not reachable ({type(exc).__name__}: {exc})."
    return True, "ok"


def _docker_client_from_env() -> Any:
    import docker  # type: ignore[import-not-found]

    return docker.from_env()


def _parse_docker_created_timestamp_utc(raw: Any) -> datetime | None:
    text = str(raw).strip()
    if not text:
        return None
    if text.endswith("Z"):
        text = text[:-1] + "+00:00"
    if "." in text:
        head, tail = text.split(".", 1)
        frac = tail
        suffix = ""
        tz_pos = max(frac.find("+"), frac.find("-"))
        if tz_pos > 0:
            suffix = frac[tz_pos:]
            frac = frac[:tz_pos]
        if frac:
            frac = frac[:6]
            text = f"{head}.{frac}{suffix}"
        else:
            text = f"{head}{suffix}"
    try:
        parsed = datetime.fromisoformat(text)
    except ValueError:
        return None
    if parsed.tzinfo is None:
        parsed = parsed.replace(tzinfo=timezone.utc)
    return parsed.astimezone(timezone.utc)


def _container_created_at_utc(container: Any) -> datetime | None:
    attrs = getattr(container, "attrs", None)
    if not isinstance(attrs, dict):
        return None
    return _parse_docker_created_timestamp_utc(attrs.get("Created"))


def cleanup_stale_eval_containers(
    *,
    grace_sec: int = DEFAULT_CONTAINER_CLEANUP_STALE_GRACE_SEC,
    now_utc: datetime | None = None,
) -> tuple[int, int, str | None]:
    grace = max(0, int(grace_sec))
    try:
        client = _docker_client_from_env()
    except Exception as exc:
        return 0, 0, f"docker client unavailable ({type(exc).__name__}: {exc})"

    removed = 0
    failed = 0
    now_value = (
        now_utc.astimezone(timezone.utc) if now_utc is not None else datetime.now(timezone.utc)
    )
    label_filter = f"{EVAL_CONTAINER_LABEL_KEY}={EVAL_CONTAINER_LABEL_VALUE}"
    try:
        containers = client.containers.list(
            all=True,
            filters={"label": [label_filter]},
        )
        for container in containers:
            status = str(getattr(container, "status", "")).strip().lower()
            if status in _CONTAINER_ACTIVE_STATES:
                continue
            created_at = _container_created_at_utc(container)
            if created_at is not None and grace > 0:
                age_sec = (now_value - created_at).total_seconds()
                if age_sec < float(grace):
                    continue
            try:
                container.remove(force=True, v=True)
                removed += 1
            except Exception:
                failed += 1
    except Exception as exc:
        return 0, 0, f"unable to inspect containers ({type(exc).__name__}: {exc})"
    finally:
        try:
            client.close()
        except Exception:
            pass
    return removed, failed, None


class _EvalContainer:
    """Wraps a running docker-py container for reuse via ``exec_run``."""

    def __init__(self, container: Any, workdirs_host: Path) -> None:
        self._container = container
        self._workdirs_host = workdirs_host

    def exec_pytest(
        self,
        *,
        host_cwd: Path,
        command: list[str],
        env: dict[str, str],
        timeout_sec: int,
    ) -> RunResult:
        """Run *command* inside the container via ``exec_run``."""
        try:
            guest_rel = host_cwd.relative_to(self._workdirs_host)
        except ValueError as exc:
            raise RuntimeError(
                f"host_cwd {host_cwd} is not under workdirs_host {self._workdirs_host}"
            ) from exc
        guest_workdir = str(PurePosixPath("/workdirs") / guest_rel.as_posix())

        full_cmd = ["timeout", "-k", "5", str(timeout_sec), *command]

        start = time.perf_counter()
        exit_code, output = self._container.exec_run(
            cmd=full_cmd,
            environment=env,
            workdir=guest_workdir,
            demux=True,
        )
        end = time.perf_counter()

        if isinstance(output, tuple):
            stdout_raw, stderr_raw = output
        else:
            stdout_raw, stderr_raw = output, b""
        stdout = _coerce_subprocess_output(stdout_raw)
        stderr = _coerce_subprocess_output(stderr_raw)

        timed_out = exit_code == PYTEST_TIMEOUT_RETURNCODE
        if timed_out:
            timeout_message = f"pytest timed out after {timeout_sec}s"
            stderr = timeout_message if not stderr else f"{stderr}\n{timeout_message}"

        return RunResult(
            returncode=int(exit_code),
            duration_sec=float(end - start),
            stdout=stdout,
            stderr=stderr,
            timed_out=timed_out,
            timeout_sec=timeout_sec if timed_out else None,
        )

    def stop(self) -> None:
        """Stop and remove the container."""
        try:
            self._container.stop(timeout=5)
        except Exception:
            pass
        try:
            self._container.remove(force=True, v=True)
        except Exception:
            pass


class _EvalContainerPool:
    """Thread-safe pool of reusable Docker containers for eval pytest runs."""

    def __init__(
        self,
        *,
        size: int,
        execution: ExecutionConfig,
        workdirs_host: Path,
    ) -> None:
        import docker  # type: ignore[import-not-found]

        self._workdirs_host = workdirs_host
        workdirs_host.mkdir(parents=True, exist_ok=True)

        client: Any = docker.from_env()
        tmpfs_size = max(1, int(execution.container_tmpfs_size_mb))

        volumes: dict[str, dict[str, str]] = {
            str(workdirs_host): {"bind": "/workdirs", "mode": "rw"},
        }
        if execution.container_mount_repo_src:
            volumes[str(_SRC_ROOT)] = {"bind": "/repo_src", "mode": "ro"}

        self._containers: list[_EvalContainer] = []
        self._queue: queue.Queue[_EvalContainer] = queue.Queue()

        try:
            for _ in range(size):
                raw = client.containers.run(
                    execution.container_image,
                    command=["sleep", "infinity"],
                    detach=True,
                    read_only=True,
                    network_mode=execution.container_network,
                    tmpfs={"/tmp": f"rw,noexec,nosuid,nodev,size={tmpfs_size}m"},
                    cap_drop=["ALL"],
                    security_opt=["no-new-privileges:true"],
                    pids_limit=int(execution.container_pids_limit),
                    mem_limit=execution.container_memory,
                    nano_cpus=int(float(execution.container_cpus) * 1_000_000_000),
                    user=execution.container_user,
                    labels={EVAL_CONTAINER_LABEL_KEY: EVAL_CONTAINER_LABEL_VALUE},
                    volumes=volumes,
                )
                wrapper = _EvalContainer(raw, workdirs_host)
                self._containers.append(wrapper)
                self._queue.put(wrapper)

            # Health check: verify GNU timeout is available in the image.
            first = self._containers[0]
            exit_code, _ = first._container.exec_run(["timeout", "--version"])
            if exit_code != 0:
                raise RuntimeError(
                    f"Container image {execution.container_image!r} does not have GNU "
                    "'timeout' command. Install coreutils in the image."
                )
        except Exception:
            self.shutdown()
            raise
        finally:
            client.close()

    def acquire(self, timeout: float = 300) -> _EvalContainer:
        """Get a container from the pool (blocks until one is available)."""
        try:
            return self._queue.get(timeout=timeout)
        except queue.Empty:
            raise RuntimeError(
                f"Timed out waiting for a container from pool after {timeout}s"
            ) from None

    def release(self, container: _EvalContainer) -> None:
        """Return a container to the pool."""
        self._queue.put(container)

    def shutdown(self) -> None:
        """Stop and remove all containers in the pool."""
        for c in self._containers:
            c.stop()
        self._containers.clear()


@dataclass(frozen=True)
class CaseSpec:
    case_id: str
    path: Path
    category: str
    difficulty: str
    snapshot_dependency: str
    expected_exception: str
    tags: list[str]
    python_args: list[str]
    timeout_sec: int
    description: str | None = None
    # Extended fields for imported cases (all optional, backward-compatible).
    bug_source: str | None = None
    contamination_risk: str | None = None
    expected_fix_lines: int | None = None
    source_id: str | None = None
    difficulty_rationale: str | None = None


@dataclass(frozen=True)
class RunResult:
    returncode: int
    duration_sec: float
    stdout: str
    stderr: str
    timed_out: bool = False
    timeout_sec: int | None = None


@dataclass(frozen=True)
class UnitExecutionOutcome:
    case: CaseSpec
    condition: Condition
    case_records: list[dict[str, Any]] | None
    unit_duration_sec: float
    runtime_error: str | None = None
    records_streamed: bool = False


@dataclass(frozen=True)
class _TrailingResultsTailIssue:
    line_no: int
    truncate_offset_bytes: int
    raw_line_bytes: bytes
    parse_error: str


@dataclass(frozen=True)
class ExecutionConfig:
    executor: str = DEFAULT_EXECUTOR
    container_image: str = DEFAULT_CONTAINER_IMAGE
    container_network: str = DEFAULT_CONTAINER_NETWORK
    container_cpus: float = DEFAULT_CONTAINER_CPUS
    container_memory: str = DEFAULT_CONTAINER_MEMORY
    container_pids_limit: int = DEFAULT_CONTAINER_PIDS_LIMIT
    container_user: str = DEFAULT_CONTAINER_USER
    container_tmpfs_size_mb: int = DEFAULT_CONTAINER_TMPFS_SIZE_MB
    container_mount_repo_src: bool = True
    container_env: dict[str, str] = field(default_factory=dict)


_ACTIVE_EXECUTION_CONFIG = ExecutionConfig()
_ACTIVE_CONTAINER_POOL: _EvalContainerPool | None = None
_PROGRESS_LEVEL_ORDER = {"basic": 1, "detailed": 2, "trace": 3}
_PROGRESS_PRINT_LOCK = threading.Lock()


def _normalized_progress_verbosity(raw: Any) -> str:
    text = str(raw or "").strip().lower()
    return text if text in PROGRESS_VERBOSITIES else "basic"


def _progress_is_enabled(verbosity: str, level: str) -> bool:
    normalized_verbosity = _normalized_progress_verbosity(verbosity)
    normalized_level = _normalized_progress_verbosity(level)
    return _PROGRESS_LEVEL_ORDER[normalized_verbosity] >= _PROGRESS_LEVEL_ORDER[normalized_level]


def _progress_print(
    *,
    verbosity: str,
    level: str,
    message: str,
    flush: bool = True,
) -> None:
    if not _progress_is_enabled(verbosity, level):
        return
    with _PROGRESS_PRINT_LOCK:
        print(message, flush=flush)


def _format_duration_precise(seconds: float | None) -> str:
    if seconds is None:
        return "unknown"
    normalized = max(0.0, float(seconds))
    if normalized < 60.0:
        return f"{normalized:.2f}s"
    return _format_duration(normalized)


class _RunResultsLock:
    def __init__(self, *, path: Path, handle: Any) -> None:
        self.path = path
        self._handle = handle
        self._released = False

    @classmethod
    def acquire(
        cls,
        *,
        results_path: Path,
        run_id: str,
    ) -> tuple[_RunResultsLock | None, str | None, str | None]:
        lock_path = results_path.with_suffix(results_path.suffix + ".lock")
        lock_path.parent.mkdir(parents=True, exist_ok=True)
        handle = lock_path.open("a+", encoding="utf-8")
        try:
            try:
                import fcntl
            except ModuleNotFoundError:
                handle.close()
                return (
                    None,
                    None,
                    (
                        "run lock not available on this platform "
                        "(missing fcntl); continuing without cross-process lock."
                    ),
                )
            try:
                fcntl.flock(handle.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
            except OSError as exc:
                handle.close()
                errno_value = getattr(exc, "errno", None)
                if errno_value in {11, 35}:
                    return (
                        None,
                        f"Run lock already held for run_id={run_id!r} ({lock_path}).",
                        None,
                    )
                return None, f"failed to acquire run lock: {type(exc).__name__}: {exc}", None
            handle.seek(0)
            handle.truncate()
            payload = {
                "run_id": run_id,
                "pid": os.getpid(),
                "acquired_at_utc": datetime.now(timezone.utc)
                .replace(microsecond=0)
                .isoformat()
                .replace("+00:00", "Z"),
                "results_path": str(results_path),
            }
            handle.write(json.dumps(payload, ensure_ascii=False) + "\n")
            handle.flush()
            return cls(path=lock_path, handle=handle), None, None
        except Exception:
            try:
                handle.close()
            except Exception:
                pass
            raise

    def release(self) -> None:
        if self._released:
            return
        self._released = True
        try:
            try:
                import fcntl

                fcntl.flock(self._handle.fileno(), fcntl.LOCK_UN)
            except Exception:
                pass
            self._handle.close()
        finally:
            try:
                self.path.unlink()
            except OSError:
                pass


class _SynchronizedJsonlWriter:
    def __init__(self, path: Path) -> None:
        self._path = path
        self._lock = threading.Lock()
        self._handle: Any = None

    def append_one(self, record: dict[str, Any]) -> None:
        line = json.dumps(record, ensure_ascii=False) + "\n"
        with self._lock:
            if self._handle is None:
                self._path.parent.mkdir(parents=True, exist_ok=True)
                self._handle = self._path.open("a", encoding="utf-8")
            self._handle.write(line)
            self._handle.flush()

    def close(self) -> None:
        with self._lock:
            if self._handle is None:
                return
            try:
                self._handle.close()
            finally:
                self._handle = None


def main(argv: list[str] | None = None) -> int:
    global _ACTIVE_RUN_SETTINGS, _ACTIVE_CONTAINER_POOL, _ACTIVE_ONLINE_POLICY_CONTROLLER
    argv_tokens = list(sys.argv[1:] if argv is None else argv)
    parser = argparse.ArgumentParser(description="Run llmdebug A/B eval harness.")
    parser.add_argument("--cases-dir", default="evals/cases", help="Cases directory.")
    parser.add_argument("--case", action="append", default=[], help="Case id(s) to run.")
    parser.add_argument(
        "--case-list-file",
        action="append",
        default=[],
        help=(
            "Path to newline-delimited case ids to include (repeatable). "
            "Blank lines and '#' comments are ignored."
        ),
    )
    parser.add_argument(
        "--category", action="append", default=[], help="Category filter (repeatable)."
    )
    parser.add_argument(
        "--difficulty", action="append", default=[], help="Difficulty filter (repeatable)."
    )
    parser.add_argument(
        "--snapshot-dependency",
        action="append",
        default=[],
        help="Snapshot dependency filter (repeatable): required|helpful|none.",
    )
    parser.add_argument(
        "--bug-source", action="append", default=[], help="Bug source filter (repeatable)."
    )
    parser.add_argument(
        "--max-cases", type=int, default=0, help="Max number of cases to run (0 = all)."
    )
    parser.add_argument(
        "--jobs",
        type=int,
        default=1,
        help="Number of case/condition units to execute concurrently.",
    )
    parser.add_argument(
        "--progress-verbosity",
        default="basic",
        help="Progress verbosity level for runtime status output (basic|detailed|trace).",
    )
    parser.add_argument(
        "--progress-heartbeat-sec",
        type=float,
        default=DEFAULT_PROGRESS_HEARTBEAT_SEC,
        help="Heartbeat interval in seconds for in-flight unit progress (0 disables).",
    )
    parser.add_argument(
        "--transport-fail-fast-streak",
        type=int,
        default=DEFAULT_TRANSPORT_FAIL_FAST_STREAK,
        help=(
            "Abort early after this many consecutive terminal transport request failures "
            "(0 disables; default: 5)."
        ),
    )
    parser.add_argument(
        "--conditions",
        default="traceback_only,with_snapshot",
        help=(
            "Comma-separated list: "
            "traceback_only,with_snapshot,with_snapshot_structured,"
            "adaptive_gated,adaptive_llm_discretion"
        ),
    )
    parser.add_argument(
        "--run-id",
        default="",
        help="Optional run identifier. If the matching results file exists, unfinished units are resumed.",
    )
    parser.add_argument(
        "--config",
        action="append",
        default=[],
        help=(
            "Path to TOML config file with run settings. "
            "Top-level keys or [run_eval] keys map to CLI destinations."
        ),
    )
    parser.add_argument("--k", type=int, default=3, help="Max patch attempts per condition.")
    parser.add_argument(
        "--patcher",
        default="command",
        choices=["command", "heuristic", "null", "openai"],
        help="Patch strategy.",
    )
    parser.add_argument(
        "--patch-cmd",
        default="",
        help="Command to run for command patcher (overrides LLMDEBUG_EVAL_PATCH_CMD).",
    )
    parser.add_argument(
        "--patch-cmd-timeout-sec",
        type=float,
        default=30.0,
        help="Timeout in seconds for command patcher subprocess execution.",
    )
    parser.add_argument(
        "--patch-cmd-shell",
        action="store_true",
        help="Run command patcher with shell=True (default: disabled).",
    )
    parser.add_argument(
        "--output-format", default="json_compact", choices=["json", "json_compact", "toon"]
    )
    parser.add_argument("--keep-workdirs", action="store_true", help="Keep copied case workdirs.")
    parser.add_argument(
        "--openai-base-url", default=os.getenv("OPENAI_BASE_URL", "http://127.0.0.1:1234")
    )
    parser.add_argument("--openai-model", default=os.getenv("OPENAI_MODEL", ""))
    parser.add_argument("--openai-api-key", default=_default_openai_api_key())
    parser.add_argument(
        "--openai-http2",
        dest="openai_http2",
        action="store_true",
        default=True,
        help="Enable HTTP/2 for OpenAI-compatible transport (default: enabled).",
    )
    parser.add_argument(
        "--no-openai-http2",
        dest="openai_http2",
        action="store_false",
        help="Disable HTTP/2 and force HTTP/1.1 for OpenAI-compatible transport.",
    )
    parser.add_argument("--openai-timeout-sec", type=float, default=120.0)
    parser.add_argument(
        "--openai-transport-max-retries",
        type=int,
        default=6,
        help=(
            "Max immediate retries for retryable OpenAI transport failures inside one request "
            "(default: 6)."
        ),
    )
    parser.add_argument(
        "--openai-transport-retry-backoff-base-sec",
        type=float,
        default=1.0,
        help="Base exponential backoff seconds for OpenAI transport retries (default: 1.0).",
    )
    parser.add_argument(
        "--openai-transport-retry-backoff-max-sec",
        type=float,
        default=8.0,
        help="Max backoff seconds for OpenAI transport retries (default: 8.0).",
    )
    parser.add_argument(
        "--openai-max-propose-sec",
        type=float,
        default=180.0,
        help=(
            "Maximum wall-clock budget per patch attempt for OpenAI propose calls "
            "(initial + recovery proposals), including per-call retries/fallback calls "
            "(0 disables cap)."
        ),
    )
    parser.add_argument("--openai-max-tokens", type=int, default=2048)
    parser.add_argument(
        "--openai-context-window-tokens",
        type=int,
        default=0,
        help=(
            "Optional OpenAI-compatible context window for Stage-B prompt preflight capping "
            "(0 disables guard)."
        ),
    )
    parser.add_argument(
        "--openai-auto-context-window",
        action="store_true",
        default=False,
        help=(
            "Probe OpenAI-compatible model metadata for context window tokens before run start "
            "(manual --openai-context-window-tokens overrides)."
        ),
    )
    parser.add_argument("--openai-temperature", type=float, default=0.0)
    parser.add_argument("--openai-response-mode", choices=["edits", "diff"], default="edits")
    parser.add_argument(
        "--openai-response-mode-autoswitch",
        choices=["off", "retry_to_diff"],
        default="off",
        help=(
            "OpenAI retry policy for internal syntax/sanitize retries: off|retry_to_diff "
            "(default: off)."
        ),
    )
    parser.add_argument(
        "--openai-force-patch-attempt",
        action="store_true",
        help="Force a minimal patch attempt when model output cannot be parsed into a diff.",
    )
    parser.add_argument(
        "--force-patch-attempt",
        dest="force_patch_attempt",
        action="store_true",
        default=True,
        help="Force a deterministic runner-side patch attempt when patcher returns no diff (default: enabled).",
    )
    parser.add_argument(
        "--no-force-patch-attempt",
        dest="force_patch_attempt",
        action="store_false",
        help="Disable runner-side forced patch attempts when no diff is proposed.",
    )
    parser.add_argument(
        "--no-files-context", action="store_true", help="Do not include full file text in prompt."
    )
    parser.add_argument(
        "--max-context-chars",
        type=int,
        default=200_000,
        help="Max characters for project file context in prompt (default: 200000).",
    )
    parser.add_argument(
        "--context-protocol",
        choices=["monolithic", "staged"],
        default="monolithic",
        help="Context protocol for prompting: monolithic|staged (default: monolithic).",
    )
    parser.add_argument(
        "--context-stage-a-max-requests",
        type=int,
        default=6,
        help="Max Stage-A context requests when --context-protocol=staged (default: 6).",
    )
    parser.add_argument(
        "--context-stage-a-max-chars",
        type=int,
        default=120_000,
        help="Max characters returned from Stage-A context retrieval (default: 120000).",
    )
    parser.add_argument(
        "--retry-prompt-mode",
        choices=["full", "delta", "auto"],
        default="auto",
        help="Retry prompt protocol: full|delta|auto (default: auto).",
    )
    parser.add_argument(
        "--retry-context-mode",
        choices=["fresh", "history"],
        default="history",
        help="Retry context mode: fresh|history (default: history).",
    )
    parser.add_argument(
        "--retry-history-max-chars",
        type=int,
        default=48_000,
        help="Max characters of retry history transcript to include when --retry-context-mode=history (default: 48000).",
    )
    parser.add_argument(
        "--pytest-output-tail-lines",
        type=int,
        default=DEFAULT_PYTEST_OUTPUT_TAIL_LINES,
        help=(
            "Persist at most the latest N lines in pytest stdout/stderr artifact files "
            f"(default: {DEFAULT_PYTEST_OUTPUT_TAIL_LINES}, 0 disables line truncation)."
        ),
    )
    parser.add_argument(
        "--snapshot-artifact-max-string-chars",
        type=int,
        default=DEFAULT_SNAPSHOT_ARTIFACT_MAX_STRING_CHARS,
        help=(
            "Per-string cap for persisted snapshot.json artifacts "
            f"(default: {DEFAULT_SNAPSHOT_ARTIFACT_MAX_STRING_CHARS}, 0 disables)."
        ),
    )
    parser.add_argument(
        "--prompt-budget-total-chars",
        type=int,
        default=180_000,
        help="Stage-B prompt total character budget (0 disables total budget cap).",
    )
    parser.add_argument(
        "--prompt-budget-retry-delta-chars",
        type=int,
        default=16_000,
        help="Per-section character cap for retry delta packet in Stage-B prompts.",
    )
    parser.add_argument(
        "--prompt-budget-stdout-chars",
        type=int,
        default=32_000,
        help="Per-section character cap for pytest stdout/stderr in Stage-B prompts.",
    )
    parser.add_argument(
        "--prompt-budget-snapshot-chars",
        type=int,
        default=32_000,
        help="Per-section character cap for snapshot summary in Stage-B prompts.",
    )
    parser.add_argument(
        "--prompt-budget-evidence-chars",
        type=int,
        default=64_000,
        help="Per-section character cap for Stage-A evidence blocks in Stage-B prompts.",
    )
    parser.add_argument(
        "--prompt-budget-retry-history-chars",
        type=int,
        default=12_000,
        help="Per-section character cap for retry-history transcript in Stage-B prompts.",
    )
    parser.add_argument(
        "--context-compaction-mode",
        choices=["off", "auto_overflow", "always"],
        default="auto_overflow",
        help=(
            "Delegated context compaction policy for Stage-B prompts: "
            "off|auto_overflow|always (default: auto_overflow)."
        ),
    )
    parser.add_argument(
        "--context-compaction-scope",
        default="snapshot,pytest,files",
        help=(
            "Comma-separated raw context sources eligible for delegated compaction "
            "(snapshot,pytest,files)."
        ),
    )
    parser.add_argument(
        "--context-compaction-max-input-chars",
        type=int,
        default=120_000,
        help="Max characters sent to delegated context compaction calls (default: 120000).",
    )
    parser.add_argument(
        "--context-compaction-target-chars",
        type=int,
        default=18_000,
        help="Target compacted context size in characters (default: 18000).",
    )
    parser.add_argument(
        "--context-compaction-max-calls",
        type=int,
        default=1,
        help="Max delegated context compaction calls per case/condition (default: 1, 0 disables).",
    )
    parser.add_argument(
        "--adaptive-gate-policy",
        choices=["aggressive", "balanced", "conservative"],
        default="balanced",
        help="Gate policy for adaptive_gated helper invocation.",
    )
    parser.add_argument(
        "--adaptive-max-helper-calls",
        type=int,
        default=1,
        help="Max helper invocations per case/condition for adaptive modes (default: 1, 0 = unlimited).",
    )
    parser.add_argument(
        "--adaptive-budget-topup-max-calls",
        type=int,
        default=DEFAULT_ADAPTIVE_BUDGET_TOPUP_MAX_CALLS,
        help=(
            "Extra one-time helper budget grants per case/condition when adaptive policies are "
            "stagnating (default: 1, 0 disables)."
        ),
    )
    parser.add_argument(
        "--adaptive-stagnation-window",
        type=int,
        default=1,
        help="Consecutive repeated-failure window for adaptive gate decisions.",
    )
    parser.add_argument(
        "--adaptive-saturation-threshold",
        type=float,
        default=0.75,
        help="Disable further helper calls when low-yield helper ratio reaches this threshold.",
    )
    parser.add_argument(
        "--adaptive-no-progress-token-cap",
        type=int,
        default=DEFAULT_ADAPTIVE_NO_PROGRESS_TOKEN_CAP,
        help=(
            "Stop adaptive loops when repeated no-progress attempts accumulate at least this many "
            "tokens (default: 150000, 0 disables)."
        ),
    )
    parser.add_argument(
        "--adaptive-helper-budget-profile",
        choices=["default", "compact"],
        default="default",
        help="Prompt-budget profile applied when adaptive helper is invoked.",
    )
    parser.add_argument(
        "--adaptive-openai-helper-response-mode",
        choices=["inherit", "edits", "diff"],
        default="inherit",
        help="OpenAI response-mode override for adaptive helper-invoked attempts (default: inherit).",
    )
    parser.add_argument(
        "--adaptive-informativeness-pregate",
        action="store_true",
        default=False,
        help="Enable rule-based traceback informativeness pre-gate for adaptive conditions.",
    )
    parser.add_argument(
        "--adaptive-informativeness-pregate-threshold",
        type=float,
        default=0.7,
        help="Score threshold to skip Stage-A (0.0-1.0, default: 0.7).",
    )
    parser.add_argument(
        "--adaptive-online-policy-mode",
        choices=["off", "shadow", "on"],
        default="off",
        help="Online adaptive policy mode for adaptive_llm_discretion (default: off).",
    )
    parser.add_argument(
        "--adaptive-online-policy-algo",
        choices=["linucb"],
        default="linucb",
        help="Online adaptive policy algorithm (default: linucb).",
    )
    parser.add_argument(
        "--adaptive-online-alpha",
        type=float,
        default=0.8,
        help="LinUCB exploration coefficient alpha (default: 0.8).",
    )
    parser.add_argument(
        "--adaptive-online-epsilon",
        type=float,
        default=0.05,
        help="Epsilon-greedy exploration probability (default: 0.05).",
    )
    parser.add_argument(
        "--adaptive-online-l2",
        type=float,
        default=1.0,
        help="L2 regularization for LinUCB matrices (default: 1.0).",
    )
    parser.add_argument(
        "--adaptive-online-warmup-decisions",
        type=int,
        default=200,
        help="Uniform warmup decisions before LinUCB greedy/explore policy (default: 200).",
    )
    parser.add_argument(
        "--adaptive-online-save-every",
        type=int,
        default=50,
        help="Persist online policy state every N updates (default: 50).",
    )
    parser.add_argument(
        "--adaptive-online-state-path",
        default="",
        help="Optional path for adaptive online policy state JSON.",
    )
    parser.add_argument(
        "--adaptive-online-events-path",
        default="",
        help="Optional path for adaptive online policy JSONL event log.",
    )
    parser.add_argument(
        "--adaptive-reward-lambda-tokens",
        type=float,
        default=ADAPTIVE_REWARD_LAMBDA_TOKENS_DEFAULT,
        help=(
            "Reward penalty coefficient for token usage: "
            "reward = success - lambda*(tokens/token_scale) - mu*(latency/latency_scale)."
        ),
    )
    parser.add_argument(
        "--adaptive-reward-mu-latency",
        type=float,
        default=ADAPTIVE_REWARD_MU_LATENCY_DEFAULT,
        help="Reward penalty coefficient for latency term (default: 0.05).",
    )
    parser.add_argument(
        "--adaptive-reward-token-scale",
        type=float,
        default=ADAPTIVE_REWARD_TOKEN_SCALE_DEFAULT,
        help="Token normalization scale in adaptive reward (default: 1000).",
    )
    parser.add_argument(
        "--adaptive-reward-latency-scale",
        type=float,
        default=ADAPTIVE_REWARD_LATENCY_SCALE_DEFAULT,
        help="Latency normalization scale in adaptive reward seconds (default: 10).",
    )
    parser.add_argument(
        "--syntax-preflight-enabled",
        dest="syntax_preflight_enabled",
        action="store_true",
        default=True,
        help=(
            "Validate patched Python files with py_compile before test execution "
            "(default: enabled)."
        ),
    )
    parser.add_argument(
        "--no-syntax-preflight",
        dest="syntax_preflight_enabled",
        action="store_false",
        help="Disable py_compile syntax preflight before test execution.",
    )
    parser.add_argument(
        "--syntax-preflight-max-cycles",
        type=int,
        default=DEFAULT_SYNTAX_PREFLIGHT_MAX_CYCLES,
        help=(
            "Maximum internal syntax-repair cycles per logical attempt "
            "(default: 2, does not consume additional retry attempts)."
        ),
    )
    parser.add_argument(
        "--patch-sanitize-enabled",
        dest="patch_sanitize_enabled",
        action="store_true",
        default=True,
        help=(
            "Reject malformed patch text patterns before patch apply/syntax preflight "
            "(default: enabled)."
        ),
    )
    parser.add_argument(
        "--no-patch-sanitize",
        dest="patch_sanitize_enabled",
        action="store_false",
        help="Disable patch text sanitation guard before syntax preflight.",
    )
    parser.add_argument(
        "--patch-verifier-infer-test-source-scope",
        dest="patch_verifier_infer_test_source_scope",
        action="store_true",
        default=True,
        help=(
            "Allow patch verifier crash-scope matching via source imports inferred from "
            "failing/crash test files (default: enabled)."
        ),
    )
    parser.add_argument(
        "--no-patch-verifier-infer-test-source-scope",
        dest="patch_verifier_infer_test_source_scope",
        action="store_false",
        help="Disable patch verifier test-to-source crash-scope inference.",
    )
    parser.add_argument(
        "--deterministic",
        action="store_true",
        help="Enable deterministic mode (requires --seed; fails fast if guarantees are missing).",
    )
    parser.add_argument(
        "--seed",
        type=int,
        default=None,
        help="Deterministic seed used for patch generation and subprocess hash randomization.",
    )
    parser.add_argument(
        "--record-prompt",
        choices=["full", "hash", "none"],
        default="hash",
        help="How to persist prompt material into attempt metadata.",
    )
    parser.add_argument(
        "--record-evidence",
        choices=["full", "hash", "none"],
        default="hash",
        help="How to persist evidence material into attempt metadata.",
    )
    parser.add_argument(
        "--record-env",
        action="append",
        default=[],
        help="Environment variable key to include in run_metadata (repeatable).",
    )
    parser.add_argument(
        "--record-deps",
        choices=["none", "minimal", "runtime"],
        default="minimal",
        help="Dependency version capture mode for run_metadata.",
    )
    add_execution_arguments(parser, default_executor=DEFAULT_EXECUTOR)
    args = parser.parse_args(argv_tokens)
    try:
        config_state = _apply_toml_configs_to_args(
            parser=parser, args=args, argv_tokens=argv_tokens
        )
    except ValueError as exc:
        print(f"Invalid --config: {exc}")
        return 2

    if args.deterministic and args.seed is None:
        print("Invalid arguments: --seed is required when --deterministic is enabled.")
        return 2
    if args.record_prompt not in RECORD_MODES:
        print(f"Invalid --record-prompt mode: {args.record_prompt}")
        return 2
    if args.record_evidence not in RECORD_MODES:
        print(f"Invalid --record-evidence mode: {args.record_evidence}")
        return 2
    if args.context_protocol not in CONTEXT_PROTOCOLS:
        print(f"Invalid --context-protocol: {args.context_protocol}")
        return 2
    if int(args.context_stage_a_max_requests) < 1:
        print("Invalid --context-stage-a-max-requests: must be >= 1")
        return 2
    if int(args.context_stage_a_max_chars) < 1:
        print("Invalid --context-stage-a-max-chars: must be >= 1")
        return 2
    if args.retry_prompt_mode not in RETRY_PROMPT_MODES:
        print(f"Invalid --retry-prompt-mode: {args.retry_prompt_mode}")
        return 2
    if args.retry_context_mode not in RETRY_CONTEXT_MODES:
        print(f"Invalid --retry-context-mode: {args.retry_context_mode}")
        return 2
    if int(args.retry_history_max_chars) < 0:
        print("Invalid --retry-history-max-chars: must be >= 0")
        return 2
    if int(args.pytest_output_tail_lines) < 0:
        print("Invalid --pytest-output-tail-lines: must be >= 0")
        return 2
    if int(args.snapshot_artifact_max_string_chars) < 0:
        print("Invalid --snapshot-artifact-max-string-chars: must be >= 0")
        return 2
    if int(args.prompt_budget_total_chars) < 0:
        print("Invalid --prompt-budget-total-chars: must be >= 0")
        return 2
    if int(args.prompt_budget_retry_delta_chars) < 0:
        print("Invalid --prompt-budget-retry-delta-chars: must be >= 0")
        return 2
    if int(args.prompt_budget_stdout_chars) < 0:
        print("Invalid --prompt-budget-stdout-chars: must be >= 0")
        return 2
    if int(args.prompt_budget_snapshot_chars) < 0:
        print("Invalid --prompt-budget-snapshot-chars: must be >= 0")
        return 2
    if int(args.prompt_budget_evidence_chars) < 0:
        print("Invalid --prompt-budget-evidence-chars: must be >= 0")
        return 2
    if int(args.prompt_budget_retry_history_chars) < 0:
        print("Invalid --prompt-budget-retry-history-chars: must be >= 0")
        return 2
    if str(args.context_compaction_mode) not in CONTEXT_COMPACTION_MODES:
        print(f"Invalid --context-compaction-mode: {args.context_compaction_mode}")
        return 2
    context_compaction_scope_tokens = {
        token.strip().lower()
        for token in str(args.context_compaction_scope or "").split(",")
        if token.strip()
    }
    invalid_context_compaction_scope_tokens = sorted(
        token
        for token in context_compaction_scope_tokens
        if token not in CONTEXT_COMPACTION_SCOPE_TOKENS
    )
    if invalid_context_compaction_scope_tokens:
        print(
            "Invalid --context-compaction-scope: unknown token(s): "
            + ", ".join(invalid_context_compaction_scope_tokens)
            + " (allowed: snapshot,pytest,files)"
        )
        return 2
    if int(args.context_compaction_max_input_chars) < 0:
        print("Invalid --context-compaction-max-input-chars: must be >= 0")
        return 2
    if int(args.context_compaction_target_chars) < 1:
        print("Invalid --context-compaction-target-chars: must be >= 1")
        return 2
    if int(args.context_compaction_max_calls) < 0:
        print("Invalid --context-compaction-max-calls: must be >= 0")
        return 2
    if int(args.openai_context_window_tokens) < 0:
        print("Invalid --openai-context-window-tokens: must be >= 0")
        return 2
    if int(args.openai_transport_max_retries) < 0:
        print("Invalid --openai-transport-max-retries: must be >= 0")
        return 2
    if float(args.openai_transport_retry_backoff_base_sec) < 0.0:
        print("Invalid --openai-transport-retry-backoff-base-sec: must be >= 0")
        return 2
    if float(args.openai_transport_retry_backoff_max_sec) < 0.0:
        print("Invalid --openai-transport-retry-backoff-max-sec: must be >= 0")
        return 2
    if float(args.openai_transport_retry_backoff_max_sec) < float(
        args.openai_transport_retry_backoff_base_sec
    ):
        print(
            "Invalid --openai-transport-retry-backoff-max-sec: must be >= "
            "--openai-transport-retry-backoff-base-sec"
        )
        return 2
    if str(args.openai_response_mode_autoswitch) not in OPENAI_RESPONSE_MODE_AUTOSWITCH_MODES:
        print(f"Invalid --openai-response-mode-autoswitch: {args.openai_response_mode_autoswitch}")
        return 2
    if args.adaptive_gate_policy not in ADAPTIVE_GATE_POLICIES:
        print(f"Invalid --adaptive-gate-policy: {args.adaptive_gate_policy}")
        return 2
    if int(args.adaptive_max_helper_calls) < 0:
        print("Invalid --adaptive-max-helper-calls: must be >= 0")
        return 2
    if int(args.adaptive_budget_topup_max_calls) < 0:
        print("Invalid --adaptive-budget-topup-max-calls: must be >= 0")
        return 2
    if int(args.adaptive_stagnation_window) < 1:
        print("Invalid --adaptive-stagnation-window: must be >= 1")
        return 2
    if not (0.0 <= float(args.adaptive_saturation_threshold) <= 1.0):
        print("Invalid --adaptive-saturation-threshold: must be between 0.0 and 1.0")
        return 2
    if int(args.adaptive_no_progress_token_cap) < 0:
        print("Invalid --adaptive-no-progress-token-cap: must be >= 0")
        return 2
    if args.adaptive_helper_budget_profile not in ADAPTIVE_HELPER_BUDGET_PROFILES:
        print(f"Invalid --adaptive-helper-budget-profile: {args.adaptive_helper_budget_profile}")
        return 2
    if args.adaptive_openai_helper_response_mode not in ADAPTIVE_OPENAI_HELPER_RESPONSE_MODES:
        print(
            "Invalid --adaptive-openai-helper-response-mode: "
            f"{args.adaptive_openai_helper_response_mode}"
        )
        return 2
    if not (0.0 <= float(args.adaptive_informativeness_pregate_threshold) <= 1.0):
        print("Invalid --adaptive-informativeness-pregate-threshold: must be between 0.0 and 1.0")
        return 2
    if str(args.adaptive_online_policy_mode) not in ADAPTIVE_ONLINE_POLICY_MODES:
        print(f"Invalid --adaptive-online-policy-mode: {args.adaptive_online_policy_mode}")
        return 2
    if str(args.adaptive_online_policy_algo) not in ADAPTIVE_ONLINE_POLICY_ALGOS:
        print(f"Invalid --adaptive-online-policy-algo: {args.adaptive_online_policy_algo}")
        return 2
    if float(args.adaptive_online_alpha) < 0.0:
        print("Invalid --adaptive-online-alpha: must be >= 0")
        return 2
    if not (0.0 <= float(args.adaptive_online_epsilon) <= 1.0):
        print("Invalid --adaptive-online-epsilon: must be between 0.0 and 1.0")
        return 2
    if float(args.adaptive_online_l2) <= 0.0:
        print("Invalid --adaptive-online-l2: must be > 0")
        return 2
    if int(args.adaptive_online_warmup_decisions) < 0:
        print("Invalid --adaptive-online-warmup-decisions: must be >= 0")
        return 2
    if int(args.adaptive_online_save_every) < 1:
        print("Invalid --adaptive-online-save-every: must be >= 1")
        return 2
    if float(args.adaptive_reward_lambda_tokens) < 0.0:
        print("Invalid --adaptive-reward-lambda-tokens: must be >= 0")
        return 2
    if float(args.adaptive_reward_mu_latency) < 0.0:
        print("Invalid --adaptive-reward-mu-latency: must be >= 0")
        return 2
    if float(args.adaptive_reward_token_scale) <= 0.0:
        print("Invalid --adaptive-reward-token-scale: must be > 0")
        return 2
    if float(args.adaptive_reward_latency_scale) <= 0.0:
        print("Invalid --adaptive-reward-latency-scale: must be > 0")
        return 2
    if int(args.syntax_preflight_max_cycles) < 1:
        print("Invalid --syntax-preflight-max-cycles: must be >= 1")
        return 2
    if str(args.progress_verbosity) not in PROGRESS_VERBOSITIES:
        print(f"Invalid --progress-verbosity: {args.progress_verbosity}")
        return 2
    if float(args.progress_heartbeat_sec) < 0.0:
        print("Invalid --progress-heartbeat-sec: must be >= 0")
        return 2
    if int(args.jobs) < 1:
        print("Invalid --jobs: must be >= 1")
        return 2
    if int(args.transport_fail_fast_streak) < 0:
        print("Invalid --transport-fail-fast-streak: must be >= 0")
        return 2
    try:
        execution_config = build_execution_config_from_args(args)
    except ValueError as exc:
        print(f"Invalid arguments: {exc}")
        return 2
    if args.deterministic and args.patcher == "openai" and float(args.openai_temperature) != 0.0:
        print("Deterministic mode enabled: forcing --openai-temperature=0.0")
        args.openai_temperature = 0.0
    if (
        str(args.openai_response_mode_autoswitch) == "retry_to_diff"
        and int(args.syntax_preflight_max_cycles) < 2
    ):
        print(
            "Warning: --openai-response-mode-autoswitch=retry_to_diff is ineffective when "
            "--syntax-preflight-max-cycles < 2."
        )

    cases_dir = _REPO_ROOT / args.cases_dir
    try:
        cases = load_cases(cases_dir)
    except ValueError as e:
        print(f"Invalid case metadata: {e}")
        return 2

    wanted_case_ids = set(args.case)
    case_list_ids: list[str] = []
    for case_list_file in args.case_list_file:
        path = Path(str(case_list_file)).expanduser()
        if not path.exists():
            print(f"Case-list file not found: {path}")
            return 2
        if not path.is_file():
            print(f"Case-list path is not a file: {path}")
            return 2
        try:
            content = path.read_text(encoding="utf-8")
        except (OSError, UnicodeError) as exc:
            print(f"Failed to read case-list file {path}: {exc}")
            return 2
        for line in content.splitlines():
            normalized = line.strip()
            if not normalized or normalized.startswith("#"):
                continue
            case_list_ids.append(normalized)
    wanted_case_ids.update(case_list_ids)
    if wanted_case_ids:
        available_case_ids = {case.case_id for case in cases}
        unknown_case_ids = sorted(wanted_case_ids.difference(available_case_ids))
        if unknown_case_ids:
            print(
                "Unknown case id(s) requested via --case/--case-list-file: "
                + ", ".join(unknown_case_ids)
            )
            return 2
        cases = [c for c in cases if c.case_id in wanted_case_ids]
    if args.category:
        wanted_categories = {x.strip() for x in args.category if x.strip()}
        cases = [c for c in cases if c.category in wanted_categories]
    if args.difficulty:
        wanted_difficulties = {x.strip() for x in args.difficulty if x.strip()}
        cases = [c for c in cases if c.difficulty in wanted_difficulties]
    if args.snapshot_dependency:
        wanted_snapshot_dependency = {x.strip() for x in args.snapshot_dependency if x.strip()}
        cases = [c for c in cases if c.snapshot_dependency in wanted_snapshot_dependency]
    if args.bug_source:
        wanted_bug_sources = {x.strip() for x in args.bug_source if x.strip()}
        cases = [c for c in cases if (c.bug_source or "hand_crafted") in wanted_bug_sources]
    if args.max_cases > 0:
        cases = cases[: args.max_cases]

    if not cases:
        print("No cases found.")
        return 2

    try:
        conditions = [Condition(c.strip()) for c in args.conditions.split(",") if c.strip()]
    except ValueError as exc:
        print(f"Invalid --conditions value: {exc}")
        return 2
    run_id = str(args.run_id).strip() or (
        time.strftime("%Y%m%dT%H%M%S") + "-" + uuid.uuid4().hex[:8]
    )

    if not str(args.adaptive_online_state_path).strip():
        args.adaptive_online_state_path = str(
            _REPO_ROOT / "evals" / "artifacts" / "policies" / f"{run_id}.json"
        )
        if config_state.value_sources.get("adaptive_online_state_path") == "default":
            config_state.value_sources["adaptive_online_state_path"] = "derived_from_run_id"
    if not str(args.adaptive_online_events_path).strip():
        args.adaptive_online_events_path = str(
            _REPO_ROOT / "evals" / "results" / f"{run_id}.adaptive_policy.jsonl"
        )
        if config_state.value_sources.get("adaptive_online_events_path") == "default":
            config_state.value_sources["adaptive_online_events_path"] = "derived_from_run_id"

    artifacts_root = _REPO_ROOT / "evals" / "artifacts" / run_id
    results_root = _REPO_ROOT / "evals" / "results"
    results_root.mkdir(parents=True, exist_ok=True)
    results_path = results_root / f"{run_id}.jsonl"
    run_results_lock: _RunResultsLock | None = None
    results_writer: _SynchronizedJsonlWriter | None = None

    def _close_run_results_io() -> None:
        nonlocal run_results_lock, results_writer
        if results_writer is not None:
            results_writer.close()
            results_writer = None
        if run_results_lock is not None:
            run_results_lock.release()
            run_results_lock = None

    execution_units = [(case, condition) for case in cases for condition in conditions]
    total_units = len(execution_units)
    run_settings_fingerprint = _build_run_settings_fingerprint(
        args=args, cases=cases, conditions=conditions
    )
    try:
        resolved_config_lock_path = _write_resolved_run_config_lockfile(
            run_id=run_id,
            args=args,
            argv_tokens=argv_tokens,
            run_settings_fingerprint=run_settings_fingerprint,
            config_state=config_state,
        )
    except OSError as exc:
        resolved_config_lock_path = None
        print(f"Warning: failed to write resolved config lock file: {exc}")

    run_results_lock, run_lock_error, run_lock_warning = _RunResultsLock.acquire(
        results_path=results_path,
        run_id=run_id,
    )
    if run_lock_error is not None:
        print(f"Run aborted: {run_lock_error}")
        return 2
    if run_lock_warning is not None:
        print(f"Run lock warning: {run_lock_warning}")

    try:
        (
            existing_completed_units,
            existing_run_metadata,
            existing_run_settings_fingerprint,
            existing_records_count,
            trailing_tail_issue,
        ) = _load_existing_run_state(
            results_path=results_path,
            run_id=run_id,
            max_attempts_per_unit=int(args.k),
        )
    except ValueError as exc:
        _close_run_results_io()
        print(f"Resume aborted: {exc}")
        return 2
    if trailing_tail_issue is not None:
        try:
            sidecar_path, removed_bytes = _repair_trailing_results_tail(
                results_path=results_path,
                run_id=run_id,
                issue=trailing_tail_issue,
            )
        except OSError as exc:
            _close_run_results_io()
            print(
                "Resume aborted: failed to repair malformed trailing JSON record: "
                f"{type(exc).__name__}: {exc}"
            )
            return 2
        print(
            "Resume repair: "
            f"truncated malformed trailing JSON record at line {trailing_tail_issue.line_no} "
            f"(removed_bytes={removed_bytes}, backup={sidecar_path})"
        )
    if existing_records_count > 0:
        if not existing_run_settings_fingerprint:
            _close_run_results_io()
            print(
                "Resume aborted: existing results are missing run_settings_fingerprint; "
                "cannot safely resume."
            )
            return 2
        if existing_run_settings_fingerprint != run_settings_fingerprint:
            print(
                "Resume warning: run settings fingerprint mismatch for existing run_id; "
                "continuing with existing completed-unit state."
            )

    planned_unit_keys = {(case.case_id, condition.value) for case, condition in execution_units}
    resumed_completed_units = {key for key in existing_completed_units if key in planned_unit_keys}
    skipped_units = len(resumed_completed_units)
    remaining_units = total_units - skipped_units

    if remaining_units > 0 and execution_config.executor == "testcontainers":
        ok, message = check_testcontainers_runtime_available()
        if not ok:
            _close_run_results_io()
            print(f"Environment check failed: {message}")
            return 2
        removed, failed, cleanup_error = cleanup_stale_eval_containers(
            grace_sec=DEFAULT_CONTAINER_CLEANUP_STALE_GRACE_SEC
        )
        if cleanup_error is not None:
            print(f"Container cleanup skipped: {cleanup_error}")
        elif removed > 0 or failed > 0:
            print(f"Container cleanup: removed={removed}, failed={failed}")

    print(f"Run: {run_id}")
    if config_state.config_paths:
        print("Config: " + ", ".join(config_state.config_paths))
    if resolved_config_lock_path is not None:
        print(f"Resolved config lock: {resolved_config_lock_path}")
    print(f"Patcher: {args.patcher}")
    print(f"Executor: {execution_config.executor}")
    print(f"Jobs: {int(args.jobs)}")
    print(f"Progress verbosity: {args.progress_verbosity}")
    print(f"Progress heartbeat sec: {float(args.progress_heartbeat_sec):.1f}")
    print(f"Transport fail-fast streak: {int(args.transport_fail_fast_streak)}")
    print(f"Runner force patch attempt: {'on' if args.force_patch_attempt else 'off'}")
    print(
        "Syntax preflight: "
        f"{'on' if args.syntax_preflight_enabled else 'off'} "
        f"(max_cycles={int(args.syntax_preflight_max_cycles)})"
    )
    print(f"Patch sanitize guard: {'on' if args.patch_sanitize_enabled else 'off'}")
    print(
        "Adaptive online policy: "
        f"mode={args.adaptive_online_policy_mode!s}, "
        f"algo={args.adaptive_online_policy_algo!s}, "
        f"alpha={float(args.adaptive_online_alpha):.3f}, "
        f"epsilon={float(args.adaptive_online_epsilon):.3f}"
    )
    if str(args.adaptive_online_policy_mode) != "off":
        print(f"Adaptive online policy state: {args.adaptive_online_state_path}")
        print(f"Adaptive online policy events: {args.adaptive_online_events_path}")
    print(f"Results: {results_path}")
    print(f"Progress: total={total_units}, completed={skipped_units}, remaining={remaining_units}")
    if remaining_units <= 0:
        _close_run_results_io()
        print("Nothing to do: all planned case/condition units are already complete.")
        return 0

    results_writer = _SynchronizedJsonlWriter(results_path)
    jobs = int(args.jobs)
    try:
        patcher = make_patcher(args)
    except BaseException:
        _close_run_results_io()
        raise
    patcher_name = str(getattr(patcher, "name", str(args.patcher)))
    if patcher_name != str(args.patcher):
        print(f"Patcher resolved: {patcher_name}")
    try:
        (
            openai_context_window_tokens_effective,
            openai_context_window_info,
        ) = _resolve_openai_context_window_for_run(
            args=args,
            patcher=patcher,
        )
    except BaseException:
        _close_run_results_io()
        raise
    openai_context_window_source = str(
        openai_context_window_info.get("openai_context_window_source") or "disabled"
    )
    openai_context_window_probe_provider = str(
        openai_context_window_info.get("openai_context_window_probe_provider") or ""
    )
    openai_context_window_probe_error = str(
        openai_context_window_info.get("openai_context_window_probe_error") or ""
    )
    if patcher_name == "openai":
        print(
            "OpenAI context window guard: "
            f"effective={openai_context_window_tokens_effective} "
            f"source={openai_context_window_source}"
        )
        if openai_context_window_probe_provider:
            print(f"OpenAI context probe provider: {openai_context_window_probe_provider}")
        if openai_context_window_probe_error:
            print(f"OpenAI context probe warning: {openai_context_window_probe_error}")
        print(f"OpenAI response autoswitch: {args.openai_response_mode_autoswitch}")

    if isinstance(existing_run_metadata, dict):
        run_metadata = dict(existing_run_metadata)
        run_metadata.setdefault("run_id", run_id)
        run_metadata.setdefault("run_settings_fingerprint", run_settings_fingerprint)
    else:
        started_at_utc = (
            datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
        )
        run_metadata = _build_run_metadata(
            args=args,
            patcher_name=patcher_name,
            run_id=run_id,
            started_at_utc=started_at_utc,
            run_settings_fingerprint=run_settings_fingerprint,
        )
    if config_state.config_paths:
        run_metadata.setdefault("config_files", list(config_state.config_paths))
    if resolved_config_lock_path is not None:
        run_metadata.setdefault("resolved_config_lock_path", str(resolved_config_lock_path))
    patcher_config = run_metadata.get("patcher_config")
    if isinstance(patcher_config, dict):
        patcher_config["openai_auto_context_window"] = bool(
            getattr(args, "openai_auto_context_window", False)
        )
        patcher_config["openai_response_mode_autoswitch"] = str(
            getattr(args, "openai_response_mode_autoswitch", "off")
        )
        patcher_config["openai_context_window_tokens_effective"] = int(
            openai_context_window_tokens_effective
        )
        patcher_config["openai_context_window_source"] = openai_context_window_source
        patcher_config["openai_context_window_probe_provider"] = (
            openai_context_window_probe_provider
        )
        patcher_config["openai_context_window_probe_error"] = openai_context_window_probe_error

    try:
        online_policy_config = OnlinePolicyConfig.normalize(
            mode=str(args.adaptive_online_policy_mode),
            algorithm=str(args.adaptive_online_policy_algo),
            alpha=float(args.adaptive_online_alpha),
            epsilon=float(args.adaptive_online_epsilon),
            l2=float(args.adaptive_online_l2),
            warmup_decisions=int(args.adaptive_online_warmup_decisions),
            save_every=int(args.adaptive_online_save_every),
            state_path=Path(str(args.adaptive_online_state_path)).expanduser().resolve(),
            events_path=Path(str(args.adaptive_online_events_path)).expanduser().resolve(),
            random_seed=(int(args.seed) if args.seed is not None else None),
        )
    except BaseException:
        _close_run_results_io()
        raise
    if online_policy_config.mode == "off":
        online_policy_controller: AdaptiveOnlinePolicyController | None = None
    else:
        try:
            online_policy_controller = AdaptiveOnlinePolicyController(online_policy_config)
        except BaseException:
            _close_run_results_io()
            raise
        if online_policy_controller.load_error:
            print(
                "Online adaptive policy state load warning: "
                f"{online_policy_controller.load_error}; starting with a fresh state."
            )

    _ACTIVE_ONLINE_POLICY_CONTROLLER = online_policy_controller

    previous_run_settings = _ACTIVE_RUN_SETTINGS
    previous_execution_config = set_active_execution_config(execution_config)
    _ACTIVE_RUN_SETTINGS = _ActiveRunSettings(
        deterministic=bool(args.deterministic),
        seed=args.seed if args.deterministic else None,
    )
    run_started = time.monotonic()
    completed_units = 0
    executed_units = 0
    runtime_errors: list[tuple[str, str, str]] = []
    transport_fail_fast_streak = max(0, int(args.transport_fail_fast_streak))
    transport_failure_streak = 0
    transport_fail_fast_triggered = False
    transport_fail_fast_reason = ""
    transport_fail_fast_skipped_units = 0
    transport_fail_fast_skip_reported = False

    def _estimate_remaining_eta_seconds(*, elapsed_sec: float) -> float | None:
        """Estimate wall-clock ETA using observed throughput.

        This is parallel-aware by design: ``executed_units / elapsed`` already
        reflects the effective throughput of the configured worker count, retry
        mix, and runtime overheads.
        """
        remaining_units = total_units - completed_units
        if remaining_units <= 0:
            return 0.0
        if executed_units <= 0 or elapsed_sec <= 0:
            return None
        units_per_sec = executed_units / elapsed_sec
        if units_per_sec <= 0:
            return None
        return remaining_units / units_per_sec

    def _extract_progress_metrics(
        case_records: list[dict[str, Any]],
    ) -> tuple[int, int, float, float]:
        """Return (attempts_used, max_attempts, total_llm_sec, total_test_sec).

        For test time: count the initial ``before_duration_sec`` once, then
        only ``after_duration_sec`` for each attempt (the next attempt's
        "before" is the previous attempt's "after", so this avoids
        double-counting).
        """
        attempts_used = len(case_records)
        max_attempts = 0
        if case_records:
            meta = case_records[0].get("attempt_metadata")
            if isinstance(meta, dict):
                max_attempts = int(meta.get("max_attempts", 0) or 0)

        total_llm_sec = 0.0
        total_test_sec = (
            float(case_records[0].get("before_duration_sec", 0) or 0) if case_records else 0.0
        )
        for r in case_records:
            pm = r.get("patch_meta")
            if isinstance(pm, dict):
                total_llm_sec += float(pm.get("propose_duration_sec", 0) or 0)
            total_test_sec += float(r.get("after_duration_sec", 0) or 0)

        return attempts_used, max_attempts, total_llm_sec, total_test_sec

    def _append_records_safely(case_records: list[dict[str, Any]]) -> None:
        if not case_records:
            return
        if results_writer is None:
            raise RuntimeError("results writer is not initialized")
        for record in case_records:
            results_writer.append_one(record)

    def _handle_outcome(outcome: UnitExecutionOutcome) -> bool:
        nonlocal completed_units
        nonlocal executed_units
        nonlocal transport_failure_streak
        nonlocal transport_fail_fast_triggered
        nonlocal transport_fail_fast_reason
        unit_duration = max(0.0, float(outcome.unit_duration_sec))
        completed_units += 1
        executed_units += 1
        elapsed = time.monotonic() - run_started
        eta = _estimate_remaining_eta_seconds(elapsed_sec=elapsed)
        progress_pct = 100.0 * (completed_units / total_units) if total_units else 100.0
        case_id = outcome.case.case_id
        condition_value = outcome.condition.value
        if outcome.runtime_error is not None:
            reason = _single_line_error(outcome.runtime_error)
            runtime_errors.append((case_id, condition_value, reason))
            print(
                f"[{completed_units}/{total_units} {progress_pct:.1f}%] "
                f"ERROR {case_id}/{condition_value} reason={reason} "
                f"unit={_format_duration(unit_duration)} "
                f"elapsed={_format_duration(elapsed)} "
                f"eta={_format_duration(eta)}"
            )
            if transport_fail_fast_streak > 0:
                if _is_transport_runtime_error(reason):
                    transport_failure_streak += 1
                else:
                    transport_failure_streak = 0
                if (
                    transport_failure_streak >= transport_fail_fast_streak
                    and not transport_fail_fast_triggered
                ):
                    transport_fail_fast_triggered = True
                    transport_fail_fast_reason = (
                        f"{transport_failure_streak} consecutive transport/crash runtime failures"
                    )
                    print(
                        "Fail-fast triggered: "
                        f"{transport_fail_fast_reason} "
                        f"(threshold={transport_fail_fast_streak})."
                    )
                    return True
            return False
        case_records = outcome.case_records or []
        if not outcome.records_streamed:
            _append_records_safely(case_records)
        unit_success = any(bool(rec.get("success")) for rec in case_records)
        status = "success" if unit_success else "failed"
        attempts_used, max_attempts, total_llm_sec, total_test_sec = _extract_progress_metrics(
            case_records
        )
        first_note = str(case_records[0].get("note") or "") if case_records else ""
        k_str = "k=0" if first_note == "no_patch_needed" else f"k={attempts_used}/{max_attempts}"
        print(
            f"[{completed_units}/{total_units} {progress_pct:.1f}%] "
            f"DONE {case_id}/{condition_value} status={status} "
            f"{k_str} llm={_format_duration(total_llm_sec)} test={_format_duration(total_test_sec)} "
            f"unit={_format_duration(unit_duration)} "
            f"elapsed={_format_duration(elapsed)} "
            f"eta={_format_duration(eta)}",
            flush=True,
        )
        if transport_fail_fast_streak <= 0:
            return False
        terminal_record = case_records[-1] if case_records else None
        terminal_transport_failure = bool(
            isinstance(terminal_record, dict)
            and _record_is_transport_request_failure(terminal_record)
        )
        if terminal_transport_failure:
            transport_failure_streak += 1
        else:
            transport_failure_streak = 0
        if transport_failure_streak < transport_fail_fast_streak:
            return False
        if not transport_fail_fast_triggered:
            transport_fail_fast_triggered = True
            transport_fail_fast_reason = (
                f"{transport_failure_streak} consecutive transport patcher request failures"
            )
            print(
                "Fail-fast triggered: "
                f"{transport_fail_fast_reason} "
                f"(threshold={transport_fail_fast_streak})."
            )
        return True

    pending_units: list[tuple[CaseSpec, Condition]] = []
    for case, condition in execution_units:
        unit_key = (case.case_id, condition.value)
        if unit_key in resumed_completed_units:
            completed_units += 1
            progress_pct = 100.0 * (completed_units / total_units) if total_units else 100.0
            print(
                f"[{completed_units}/{total_units} {progress_pct:.1f}%] "
                f"SKIP (resumed) {case.case_id}/{condition.value}",
                flush=True,
            )
            continue
        pending_units.append((case, condition))

    def _stream_result_record(record: dict[str, Any]) -> None:
        if results_writer is None:
            raise RuntimeError("results writer is not initialized")
        results_writer.append_one(record)

    shared_unit_kwargs: dict[str, Any] = {
        "artifacts_root": artifacts_root,
        "patcher_args": args,
        "k": int(args.k),
        "output_format": str(args.output_format),
        "run_id": run_id,
        "include_files_context": not bool(args.no_files_context),
        "max_context_chars": int(args.max_context_chars),
        "force_patch_attempt": bool(args.force_patch_attempt),
        "context_protocol": str(args.context_protocol),
        "context_stage_a_max_requests": int(args.context_stage_a_max_requests),
        "context_stage_a_max_chars": int(args.context_stage_a_max_chars),
        "retry_prompt_mode": str(args.retry_prompt_mode),
        "retry_context_mode": str(args.retry_context_mode),
        "retry_history_max_chars": int(args.retry_history_max_chars),
        "pytest_output_tail_lines": int(
            getattr(args, "pytest_output_tail_lines", DEFAULT_PYTEST_OUTPUT_TAIL_LINES)
        ),
        "snapshot_artifact_max_string_chars": int(
            getattr(
                args,
                "snapshot_artifact_max_string_chars",
                DEFAULT_SNAPSHOT_ARTIFACT_MAX_STRING_CHARS,
            )
        ),
        "prompt_budget_total_chars": int(args.prompt_budget_total_chars),
        "prompt_budget_retry_delta_chars": int(args.prompt_budget_retry_delta_chars),
        "prompt_budget_stdout_chars": int(args.prompt_budget_stdout_chars),
        "prompt_budget_snapshot_chars": int(args.prompt_budget_snapshot_chars),
        "prompt_budget_evidence_chars": int(args.prompt_budget_evidence_chars),
        "prompt_budget_retry_history_chars": int(args.prompt_budget_retry_history_chars),
        "context_compaction_mode": str(args.context_compaction_mode),
        "context_compaction_scope": str(args.context_compaction_scope),
        "context_compaction_max_input_chars": int(args.context_compaction_max_input_chars),
        "context_compaction_target_chars": int(args.context_compaction_target_chars),
        "context_compaction_max_calls": int(args.context_compaction_max_calls),
        "openai_context_window_tokens": int(openai_context_window_tokens_effective),
        "openai_context_window_source": openai_context_window_source,
        "openai_context_window_probe_provider": openai_context_window_probe_provider,
        "openai_context_window_probe_error": openai_context_window_probe_error,
        "openai_response_mode_autoswitch": str(args.openai_response_mode_autoswitch),
        "adaptive_gate_policy": str(args.adaptive_gate_policy),
        "adaptive_max_helper_calls": int(args.adaptive_max_helper_calls),
        "adaptive_budget_topup_max_calls": int(args.adaptive_budget_topup_max_calls),
        "adaptive_stagnation_window": int(args.adaptive_stagnation_window),
        "adaptive_saturation_threshold": float(args.adaptive_saturation_threshold),
        "adaptive_no_progress_token_cap": int(args.adaptive_no_progress_token_cap),
        "adaptive_helper_budget_profile": str(args.adaptive_helper_budget_profile),
        "adaptive_openai_helper_response_mode": str(args.adaptive_openai_helper_response_mode),
        "adaptive_informativeness_pregate": bool(args.adaptive_informativeness_pregate),
        "adaptive_informativeness_pregate_threshold": float(
            args.adaptive_informativeness_pregate_threshold
        ),
        "syntax_preflight_enabled": bool(args.syntax_preflight_enabled),
        "syntax_preflight_max_cycles": int(args.syntax_preflight_max_cycles),
        "patch_sanitize_enabled": bool(args.patch_sanitize_enabled),
        "patch_verifier_infer_test_source_scope": bool(args.patch_verifier_infer_test_source_scope),
        "adaptive_online_policy_controller": online_policy_controller,
        "adaptive_online_policy_mode": str(args.adaptive_online_policy_mode),
        "adaptive_online_policy_algo": str(args.adaptive_online_policy_algo),
        "adaptive_reward_lambda_tokens": float(args.adaptive_reward_lambda_tokens),
        "adaptive_reward_mu_latency": float(args.adaptive_reward_mu_latency),
        "adaptive_reward_token_scale": float(args.adaptive_reward_token_scale),
        "adaptive_reward_latency_scale": float(args.adaptive_reward_latency_scale),
        "run_metadata": run_metadata,
        "record_prompt": str(args.record_prompt),
        "record_evidence": str(args.record_evidence),
        "deterministic": bool(args.deterministic),
        "seed": args.seed if args.deterministic else None,
        "keep_workdirs": bool(args.keep_workdirs),
        "progress_verbosity": str(args.progress_verbosity),
        "progress_heartbeat_sec": float(args.progress_heartbeat_sec),
        "record_sink": _stream_result_record,
    }

    if execution_config.executor == "testcontainers" and pending_units:
        pool_size = max(1, jobs)
        print(f"Container pool: starting {pool_size} container(s)...", flush=True)
        try:
            _ACTIVE_CONTAINER_POOL = _EvalContainerPool(
                size=pool_size,
                execution=execution_config,
                workdirs_host=artifacts_root / "workdirs",
            )
            print(f"Container pool: {pool_size} container(s) ready", flush=True)
        except Exception as pool_exc:
            print(
                f"Container pool: failed to start ({type(pool_exc).__name__}: {pool_exc}), "
                "falling back to per-call containers",
                flush=True,
            )

    try:
        if jobs == 1:
            for unit_index, (case, condition) in enumerate(pending_units):
                progress_pct = 100.0 * (completed_units / total_units) if total_units else 100.0
                print(
                    f"[{completed_units + 1}/{total_units} {progress_pct:.1f}%] "
                    f"START {case.case_id}/{condition.value}",
                    flush=True,
                )
                outcome = _execute_eval_unit(
                    case=case,
                    condition=condition,
                    patcher_override=patcher,
                    **shared_unit_kwargs,
                )
                should_abort = _handle_outcome(outcome)
                if should_abort:
                    remaining_units = max(0, len(pending_units) - (unit_index + 1))
                    if remaining_units > 0:
                        transport_fail_fast_skipped_units += remaining_units
                        print(f"Fail-fast: skipping {remaining_units} not-yet-started unit(s).")
                    break
        else:
            future_map: dict[Any, tuple[CaseSpec, Condition]] = {}
            pending_iter = iter(pending_units)
            submitted_units = 0

            def _submit_next(executor: ThreadPoolExecutor) -> bool:
                nonlocal submitted_units
                try:
                    case, condition = next(pending_iter)
                except StopIteration:
                    return False
                progress_pct = 100.0 * (completed_units / total_units) if total_units else 100.0
                ordinal = completed_units + len(future_map) + 1
                print(
                    f"[{ordinal}/{total_units} {progress_pct:.1f}%] "
                    f"START {case.case_id}/{condition.value}",
                    flush=True,
                )
                future = executor.submit(
                    _execute_eval_unit,
                    case=case,
                    condition=condition,
                    patcher_override=None,
                    **shared_unit_kwargs,
                )
                future_map[future] = (case, condition)
                submitted_units += 1
                return True

            with ThreadPoolExecutor(max_workers=jobs) as executor:
                while (
                    len(future_map) < jobs
                    and (not transport_fail_fast_triggered)
                    and _submit_next(executor)
                ):
                    pass
                while future_map:
                    future = next(as_completed(tuple(future_map.keys())))
                    case, condition = future_map[future]
                    del future_map[future]
                    try:
                        outcome = future.result()
                    except Exception as exc:
                        outcome = UnitExecutionOutcome(
                            case=case,
                            condition=condition,
                            case_records=None,
                            unit_duration_sec=0.0,
                            runtime_error=f"{type(exc).__name__}: {exc}",
                        )
                    should_abort = _handle_outcome(outcome)
                    if should_abort and (not transport_fail_fast_skip_reported):
                        cancelled = 0
                        for pending_future in list(future_map):
                            if pending_future.cancel():
                                del future_map[pending_future]
                                cancelled += 1
                        unscheduled = max(0, len(pending_units) - submitted_units)
                        skipped_now = cancelled + unscheduled
                        if skipped_now > 0:
                            transport_fail_fast_skipped_units += skipped_now
                            print(f"Fail-fast: skipping {skipped_now} pending/unscheduled unit(s).")
                        transport_fail_fast_skip_reported = True
                    while (
                        len(future_map) < jobs
                        and (not transport_fail_fast_triggered)
                        and _submit_next(executor)
                    ):
                        pass
    finally:
        if _ACTIVE_CONTAINER_POOL is not None:
            _ACTIVE_CONTAINER_POOL.shutdown()
            _ACTIVE_CONTAINER_POOL = None
        if _ACTIVE_ONLINE_POLICY_CONTROLLER is not None:
            try:
                _ACTIVE_ONLINE_POLICY_CONTROLLER.close()
            except Exception as exc:
                print(f"Online adaptive policy close warning: {type(exc).__name__}: {exc}")
            _ACTIVE_ONLINE_POLICY_CONTROLLER = None
        _close_run_results_io()
        _ACTIVE_RUN_SETTINGS = previous_run_settings
        set_active_execution_config(previous_execution_config)

    total_elapsed = time.monotonic() - run_started
    errors_suffix = f", errors={len(runtime_errors)}" if runtime_errors else ""
    print(
        "Progress: finished "
        f"executed={executed_units}, skipped={skipped_units}, total={total_units}, "
        f"elapsed={_format_duration(total_elapsed)}{errors_suffix}"
    )
    if runtime_errors:
        print(
            "Eval finished with "
            f"{len(runtime_errors)} runtime error(s). "
            "Rerun with the same --run-id to retry unfinished units."
        )
    if transport_fail_fast_triggered:
        print(
            "Eval aborted early by transport fail-fast: "
            f"{transport_fail_fast_reason} "
            f"(threshold={transport_fail_fast_streak}, "
            f"skipped_units={transport_fail_fast_skipped_units}). "
            "Rerun with --transport-fail-fast-streak=0 to disable."
        )
        return 2
    if runtime_errors:
        return 2
    return 0


def _single_line_error(raw: str, *, max_chars: int = 240) -> str:
    text = " | ".join(segment.strip() for segment in str(raw).splitlines() if segment.strip())
    if not text:
        text = str(raw).strip() or "unknown runtime error"
    if len(text) <= max_chars:
        return text
    return text[: max_chars - 3] + "..."


def _is_transport_runtime_error(message: str) -> bool:
    text = str(message or "").strip().lower()
    if not text:
        return False
    markers = (
        "segmentation fault",
        "connection refused",
        "connection reset",
        "brokenpipeerror",
        "remotedisconnected",
        "temporarily unavailable",
        "server disconnected",
        "failed to establish a new connection",
    )
    return any(marker in text for marker in markers)


def _record_is_transport_request_failure(record: dict[str, Any]) -> bool:
    note = str(record.get("note") or "")
    if note != "patcher_request_failed":
        return False
    patch_meta = record.get("patch_meta")
    if not isinstance(patch_meta, dict):
        return False
    if bool(patch_meta.get("patcher_request_failed")):
        return str(patch_meta.get("patcher_request_failure_kind") or "") == "transport"
    failed, kind = _classify_patcher_request_failure(patch_meta)
    return bool(failed and kind == "transport")


def _execute_eval_unit(
    *,
    case: CaseSpec,
    condition: Condition,
    artifacts_root: Path,
    patcher_args: argparse.Namespace,
    patcher_override: Any | None,
    k: int,
    output_format: str,
    run_id: str,
    include_files_context: bool,
    max_context_chars: int,
    force_patch_attempt: bool,
    context_protocol: str,
    context_stage_a_max_requests: int,
    context_stage_a_max_chars: int,
    retry_prompt_mode: str,
    retry_context_mode: str,
    retry_history_max_chars: int,
    pytest_output_tail_lines: int,
    snapshot_artifact_max_string_chars: int,
    prompt_budget_total_chars: int,
    prompt_budget_retry_delta_chars: int,
    prompt_budget_stdout_chars: int,
    prompt_budget_snapshot_chars: int,
    prompt_budget_evidence_chars: int,
    prompt_budget_retry_history_chars: int,
    context_compaction_mode: str,
    context_compaction_scope: str,
    context_compaction_max_input_chars: int,
    context_compaction_target_chars: int,
    context_compaction_max_calls: int,
    openai_context_window_tokens: int,
    openai_context_window_source: str,
    openai_context_window_probe_provider: str,
    openai_context_window_probe_error: str,
    openai_response_mode_autoswitch: str,
    adaptive_gate_policy: str,
    adaptive_max_helper_calls: int,
    adaptive_budget_topup_max_calls: int,
    adaptive_stagnation_window: int,
    adaptive_saturation_threshold: float,
    adaptive_no_progress_token_cap: int,
    adaptive_helper_budget_profile: str,
    adaptive_openai_helper_response_mode: str,
    adaptive_informativeness_pregate: bool,
    adaptive_informativeness_pregate_threshold: float,
    syntax_preflight_enabled: bool,
    syntax_preflight_max_cycles: int,
    patch_sanitize_enabled: bool,
    patch_verifier_infer_test_source_scope: bool,
    adaptive_online_policy_controller: AdaptiveOnlinePolicyController | None,
    adaptive_online_policy_mode: str,
    adaptive_online_policy_algo: str,
    adaptive_reward_lambda_tokens: float,
    adaptive_reward_mu_latency: float,
    adaptive_reward_token_scale: float,
    adaptive_reward_latency_scale: float,
    run_metadata: dict[str, Any],
    record_prompt: str,
    record_evidence: str,
    deterministic: bool,
    seed: int | None,
    keep_workdirs: bool,
    progress_verbosity: str,
    progress_heartbeat_sec: float,
    record_sink: Callable[[dict[str, Any]], None] | None,
) -> UnitExecutionOutcome:
    unit_started = time.monotonic()
    normalized_progress_verbosity = _normalized_progress_verbosity(progress_verbosity)
    normalized_progress_heartbeat_sec = max(0.0, float(progress_heartbeat_sec))
    workdir = artifacts_root / "workdirs" / case.case_id / condition.value
    context_dir_base = artifacts_root / "contexts" / case.case_id / condition.value

    runtime_error: str | None = None
    case_records: list[dict[str, Any]] | None = None
    streamed_records_count = 0
    progress_state_lock = threading.Lock()
    progress_state: dict[str, Any] = {
        "phase": "initializing",
        "phase_started_monotonic": unit_started,
        "attempt": 0,
        "max_attempts": int(k),
        "syntax_cycle": 0,
        "syntax_max_cycles": 0,
        "last_note": "",
    }
    heartbeat_stop_event = threading.Event()
    heartbeat_thread: threading.Thread | None = None

    def _coerce_nonnegative_float(raw: Any) -> float:
        try:
            return max(0.0, float(raw))
        except (TypeError, ValueError):
            return 0.0

    def _snapshot_progress_state() -> dict[str, Any]:
        with progress_state_lock:
            return dict(progress_state)

    def _update_progress_state(event: str, payload: dict[str, Any] | None = None) -> None:
        now = time.monotonic()
        with progress_state_lock:
            progress_state["phase"] = str(event or "running")
            progress_state["phase_started_monotonic"] = now
            if isinstance(payload, dict):
                for key in ("attempt", "max_attempts", "syntax_cycle", "syntax_max_cycles"):
                    value = payload.get(key)
                    if isinstance(value, int):
                        progress_state[key] = int(value)
                note_value = payload.get("note")
                if note_value is not None:
                    progress_state["last_note"] = str(note_value)

        if _progress_is_enabled(normalized_progress_verbosity, "trace"):
            snapshot = _snapshot_progress_state()
            attempt_value = int(snapshot.get("attempt") or 0)
            max_attempts_value = int(snapshot.get("max_attempts") or 0)
            attempt_fragment = (
                f" attempt={attempt_value}/{max_attempts_value}"
                if attempt_value > 0 and max_attempts_value > 0
                else (f" attempt={attempt_value}" if attempt_value > 0 else "")
            )
            syntax_cycle = int(snapshot.get("syntax_cycle") or 0)
            syntax_max_cycles = int(snapshot.get("syntax_max_cycles") or 0)
            syntax_fragment = (
                f" syntax_cycle={syntax_cycle}/{syntax_max_cycles}"
                if syntax_cycle > 0 and syntax_max_cycles > 0
                else ""
            )
            _progress_print(
                verbosity=normalized_progress_verbosity,
                level="trace",
                message=(
                    f"[TRACE] {case.case_id}/{condition.value} "
                    f"phase={snapshot.get('phase', 'running')}{attempt_fragment}{syntax_fragment}"
                ),
            )

    def _emit_progress_heartbeat() -> None:
        snapshot = _snapshot_progress_state()
        now = time.monotonic()
        unit_elapsed = _format_duration_precise(now - unit_started)
        phase_started = _coerce_nonnegative_float(
            snapshot.get("phase_started_monotonic", unit_started)
        )
        if phase_started <= 0.0:
            phase_started = unit_started
        phase_elapsed = _format_duration_precise(now - phase_started)
        attempt_value = int(snapshot.get("attempt") or 0)
        max_attempts_value = int(snapshot.get("max_attempts") or 0)
        attempt_fragment = (
            f" attempt={attempt_value}/{max_attempts_value}"
            if attempt_value > 0 and max_attempts_value > 0
            else (f" attempt={attempt_value}" if attempt_value > 0 else "")
        )
        note_text = str(snapshot.get("last_note") or "").strip()
        note_fragment = f" note={note_text}" if note_text else ""
        _progress_print(
            verbosity=normalized_progress_verbosity,
            level="detailed",
            message=(
                f"[HEARTBEAT] {case.case_id}/{condition.value}{attempt_fragment} "
                f"phase={snapshot.get('phase', 'running')} "
                f"records={streamed_records_count} "
                f"unit={unit_elapsed} phase_elapsed={phase_elapsed}{note_fragment}"
            ),
        )

    def _on_progress_update(event: str, payload: dict[str, Any] | None = None) -> None:
        _update_progress_state(event, payload)

    def _on_record(record: dict[str, Any]) -> None:
        nonlocal streamed_records_count
        if record_sink is not None:
            record_sink(record)
            streamed_records_count += 1
        note = str(record.get("note") or "")
        attempt = int(record.get("attempt") or 0)
        _update_progress_state(
            "record_emitted",
            {
                "attempt": attempt,
                "max_attempts": int(k),
                "note": note,
            },
        )
        if _progress_is_enabled(normalized_progress_verbosity, "detailed"):
            patch_meta = record.get("patch_meta")
            propose_sec = (
                _coerce_nonnegative_float(patch_meta.get("propose_duration_sec"))
                if isinstance(patch_meta, dict)
                else 0.0
            )
            test_after_sec = _coerce_nonnegative_float(record.get("after_duration_sec"))
            attempt_wall_sec = _coerce_nonnegative_float(record.get("attempt_wall_clock_sec"))
            status_label = "success" if bool(record.get("success")) else "failed"
            _progress_print(
                verbosity=normalized_progress_verbosity,
                level="detailed",
                message=(
                    f"[ATTEMPT] {case.case_id}/{condition.value} "
                    f"attempt={attempt}/{int(k)} status={status_label} note={note or 'none'} "
                    f"wall={_format_duration_precise(attempt_wall_sec)} "
                    f"llm={_format_duration_precise(propose_sec)} "
                    f"test_after={_format_duration_precise(test_after_sec)}"
                ),
            )

    if normalized_progress_heartbeat_sec > 0.0 and _progress_is_enabled(
        normalized_progress_verbosity, "detailed"
    ):

        def _heartbeat_loop() -> None:
            while not heartbeat_stop_event.wait(normalized_progress_heartbeat_sec):
                _emit_progress_heartbeat()

        heartbeat_thread = threading.Thread(
            target=_heartbeat_loop,
            name=f"eval-heartbeat-{case.case_id}-{condition.value}",
            daemon=True,
        )
        heartbeat_thread.start()

    try:
        _update_progress_state("setup_workdir")
        workdir.parent.mkdir(parents=True, exist_ok=True)
        context_dir_base.mkdir(parents=True, exist_ok=True)

        if workdir.exists():
            shutil.rmtree(workdir)
        shutil.copytree(
            case.path,
            workdir,
            dirs_exist_ok=True,
            ignore=shutil.ignore_patterns("__pycache__", "*.pyc", "*.pyo"),
        )

        patcher = patcher_override if patcher_override is not None else make_patcher(patcher_args)
        _update_progress_state("run_one_condition")
        case_records = run_one_condition(
            case=case,
            condition=condition,
            workdir=workdir,
            context_dir_base=context_dir_base,
            patcher=patcher,
            k=k,
            output_format=output_format,
            run_id=run_id,
            include_files_context=include_files_context,
            max_context_chars=max_context_chars,
            force_patch_attempt=force_patch_attempt,
            context_protocol=context_protocol,
            context_stage_a_max_requests=context_stage_a_max_requests,
            context_stage_a_max_chars=context_stage_a_max_chars,
            retry_prompt_mode=retry_prompt_mode,
            retry_context_mode=retry_context_mode,
            retry_history_max_chars=retry_history_max_chars,
            pytest_output_tail_lines=pytest_output_tail_lines,
            snapshot_artifact_max_string_chars=snapshot_artifact_max_string_chars,
            prompt_budget_total_chars=prompt_budget_total_chars,
            prompt_budget_retry_delta_chars=prompt_budget_retry_delta_chars,
            prompt_budget_stdout_chars=prompt_budget_stdout_chars,
            prompt_budget_snapshot_chars=prompt_budget_snapshot_chars,
            prompt_budget_evidence_chars=prompt_budget_evidence_chars,
            prompt_budget_retry_history_chars=prompt_budget_retry_history_chars,
            context_compaction_mode=context_compaction_mode,
            context_compaction_scope=context_compaction_scope,
            context_compaction_max_input_chars=context_compaction_max_input_chars,
            context_compaction_target_chars=context_compaction_target_chars,
            context_compaction_max_calls=context_compaction_max_calls,
            openai_context_window_tokens=openai_context_window_tokens,
            openai_context_window_source=openai_context_window_source,
            openai_context_window_probe_provider=openai_context_window_probe_provider,
            openai_context_window_probe_error=openai_context_window_probe_error,
            openai_response_mode_autoswitch=openai_response_mode_autoswitch,
            adaptive_gate_policy=adaptive_gate_policy,
            adaptive_max_helper_calls=adaptive_max_helper_calls,
            adaptive_budget_topup_max_calls=adaptive_budget_topup_max_calls,
            adaptive_stagnation_window=adaptive_stagnation_window,
            adaptive_saturation_threshold=adaptive_saturation_threshold,
            adaptive_no_progress_token_cap=adaptive_no_progress_token_cap,
            adaptive_helper_budget_profile=adaptive_helper_budget_profile,
            adaptive_openai_helper_response_mode=adaptive_openai_helper_response_mode,
            adaptive_informativeness_pregate=adaptive_informativeness_pregate,
            adaptive_informativeness_pregate_threshold=adaptive_informativeness_pregate_threshold,
            syntax_preflight_enabled=syntax_preflight_enabled,
            syntax_preflight_max_cycles=syntax_preflight_max_cycles,
            patch_sanitize_enabled=patch_sanitize_enabled,
            patch_verifier_infer_test_source_scope=patch_verifier_infer_test_source_scope,
            adaptive_online_policy_controller=adaptive_online_policy_controller,
            adaptive_online_policy_mode=adaptive_online_policy_mode,
            adaptive_online_policy_algo=adaptive_online_policy_algo,
            adaptive_reward_lambda_tokens=adaptive_reward_lambda_tokens,
            adaptive_reward_mu_latency=adaptive_reward_mu_latency,
            adaptive_reward_token_scale=adaptive_reward_token_scale,
            adaptive_reward_latency_scale=adaptive_reward_latency_scale,
            run_metadata=run_metadata,
            record_prompt=record_prompt,
            record_evidence=record_evidence,
            deterministic=deterministic,
            seed=seed,
            record_sink=_on_record,
            progress_update=_on_progress_update,
        )
        _update_progress_state(
            "unit_completed",
            {
                "note": "completed",
            },
        )
    except RuntimeError as exc:
        runtime_error = str(exc)
    except Exception as exc:
        runtime_error = f"{type(exc).__name__}: {exc}"
    finally:
        heartbeat_stop_event.set()
        if heartbeat_thread is not None:
            heartbeat_thread.join(timeout=0.25)
        if not keep_workdirs:
            shutil.rmtree(workdir, ignore_errors=True)

    return UnitExecutionOutcome(
        case=case,
        condition=condition,
        case_records=case_records,
        unit_duration_sec=time.monotonic() - unit_started,
        runtime_error=runtime_error,
        records_streamed=streamed_records_count > 0,
    )


def _format_duration(seconds: float | None) -> str:
    if seconds is None:
        return "unknown"
    whole_seconds = max(0, round(seconds))
    hours, rem = divmod(whole_seconds, 3600)
    minutes, secs = divmod(rem, 60)
    if hours:
        return f"{hours}h{minutes:02d}m{secs:02d}s"
    if minutes:
        return f"{minutes}m{secs:02d}s"
    return f"{secs}s"


def _build_run_settings_fingerprint(
    *,
    args: argparse.Namespace,
    cases: list[CaseSpec],
    conditions: list[Condition],
) -> str:
    payload = {
        "cases_dir": str(args.cases_dir),
        "selected_case_ids": [case.case_id for case in cases],
        "conditions": [condition.value for condition in conditions],
        "patcher_config": _build_patcher_config(args=args, patcher_name=str(args.patcher)),
        "record_prompt": str(args.record_prompt),
        "record_evidence": str(args.record_evidence),
        "record_env": sorted(key.strip() for key in args.record_env if str(key).strip()),
        "record_deps": str(args.record_deps),
        "retry_context_mode": str(args.retry_context_mode),
        "retry_history_max_chars": int(args.retry_history_max_chars),
        "prompt_budget_retry_history_chars": int(args.prompt_budget_retry_history_chars),
    }
    return _sha256_jsonlike(payload)


def _load_existing_run_state(
    *,
    results_path: Path,
    run_id: str,
    max_attempts_per_unit: int,
) -> tuple[
    set[tuple[str, str]],
    dict[str, Any] | None,
    str | None,
    int,
    _TrailingResultsTailIssue | None,
]:
    if not results_path.exists():
        return set(), None, None, 0, None

    per_unit_stats: dict[tuple[str, str], dict[str, Any]] = {}
    first_run_metadata: dict[str, Any] | None = None
    settings_fingerprint: str | None = None
    records_count = 0
    trailing_tail_issue: _TrailingResultsTailIssue | None = None

    def _consume_record(*, record: dict[str, Any], line_no: int) -> None:
        nonlocal first_run_metadata, settings_fingerprint, records_count

        record_run_id = record.get("run_id")
        if not isinstance(record_run_id, str) or not record_run_id:
            raise ValueError(f"{results_path}:{line_no}: run_id must be a non-empty string")
        if record_run_id != run_id:
            raise ValueError(
                f"{results_path}:{line_no}: run_id mismatch ({record_run_id!r} != {run_id!r})"
            )

        case_id = record.get("case_id")
        if not isinstance(case_id, str) or not case_id:
            raise ValueError(f"{results_path}:{line_no}: case_id must be a non-empty string")
        condition = record.get("condition")
        if not isinstance(condition, str) or not condition:
            raise ValueError(f"{results_path}:{line_no}: condition must be a non-empty string")
        attempt = record.get("attempt")
        if not isinstance(attempt, int) or attempt < 0:
            raise ValueError(f"{results_path}:{line_no}: attempt must be a non-negative integer")
        success = record.get("success")
        if not isinstance(success, bool):
            raise ValueError(f"{results_path}:{line_no}: success must be a boolean")

        run_metadata = record.get("run_metadata")
        if not isinstance(run_metadata, dict):
            raise ValueError(f"{results_path}:{line_no}: run_metadata must be an object")
        if run_metadata.get("run_id") != run_id:
            raise ValueError(f"{results_path}:{line_no}: run_metadata.run_id must match run_id")

        current_fingerprint = run_metadata.get("run_settings_fingerprint")
        if current_fingerprint is not None and (
            not isinstance(current_fingerprint, str) or not current_fingerprint
        ):
            raise ValueError(
                f"{results_path}:{line_no}: run_settings_fingerprint must be a non-empty string"
            )
        if isinstance(current_fingerprint, str):
            if settings_fingerprint is None:
                settings_fingerprint = current_fingerprint
            elif settings_fingerprint != current_fingerprint:
                raise ValueError(
                    f"{results_path}:{line_no}: inconsistent run_settings_fingerprint values"
                )

        if first_run_metadata is None:
            first_run_metadata = dict(run_metadata)

        unit_key = (case_id, condition)
        unit_stats = per_unit_stats.setdefault(
            unit_key,
            {"max_attempt": -1, "any_success": False},
        )
        unit_stats["max_attempt"] = max(int(unit_stats["max_attempt"]), attempt)
        if success:
            unit_stats["any_success"] = True
        records_count += 1

    def _parse_record_from_line_bytes(*, line_bytes: bytes, line_no: int) -> dict[str, Any]:
        try:
            line_text = line_bytes.decode("utf-8")
        except UnicodeDecodeError as exc:
            raise ValueError(
                f"{results_path}:{line_no}: invalid UTF-8 ({type(exc).__name__}: {exc})"
            ) from exc
        try:
            record = json.loads(line_text)
        except json.JSONDecodeError as exc:
            raise ValueError(
                f"{results_path}:{line_no}: invalid JSON ({type(exc).__name__}: {exc})"
            ) from exc
        if not isinstance(record, dict):
            raise ValueError(f"{results_path}:{line_no}: record must be a JSON object")
        return record

    pending_line_no: int | None = None
    pending_line_start_offset: int = 0
    pending_line_bytes: bytes | None = None
    offset_bytes = 0
    with results_path.open("rb") as handle:
        for line_no, line_bytes in enumerate(handle, start=1):
            line_start_offset = offset_bytes
            offset_bytes += len(line_bytes)
            if not line_bytes.strip():
                continue
            if pending_line_no is not None and pending_line_bytes is not None:
                record = _parse_record_from_line_bytes(
                    line_bytes=pending_line_bytes,
                    line_no=pending_line_no,
                )
                _consume_record(record=record, line_no=pending_line_no)
            pending_line_no = line_no
            pending_line_start_offset = line_start_offset
            pending_line_bytes = line_bytes

    if pending_line_no is not None and pending_line_bytes is not None:
        try:
            trailing_record = _parse_record_from_line_bytes(
                line_bytes=pending_line_bytes,
                line_no=pending_line_no,
            )
        except ValueError as exc:
            error_text = str(exc)
            is_parse_error = "invalid JSON" in error_text or "invalid UTF-8" in error_text
            if not is_parse_error:
                raise
            trailing_tail_issue = _TrailingResultsTailIssue(
                line_no=pending_line_no,
                truncate_offset_bytes=pending_line_start_offset,
                raw_line_bytes=pending_line_bytes,
                parse_error=error_text,
            )
        else:
            _consume_record(record=trailing_record, line_no=pending_line_no)

    required_attempts = max(1, int(max_attempts_per_unit))
    completed_units = {
        unit_key
        for unit_key, stats in per_unit_stats.items()
        if bool(stats.get("any_success")) or int(stats.get("max_attempt", -1)) >= required_attempts
    }
    return (
        completed_units,
        first_run_metadata,
        settings_fingerprint,
        records_count,
        trailing_tail_issue,
    )


def _repair_trailing_results_tail(
    *,
    results_path: Path,
    run_id: str,
    issue: _TrailingResultsTailIssue,
) -> tuple[Path, int]:
    original_size = int(results_path.stat().st_size)
    truncate_offset = int(issue.truncate_offset_bytes)
    if truncate_offset < 0 or truncate_offset > original_size:
        raise OSError(
            f"invalid truncate offset {truncate_offset} for file size {original_size}"
        )
    with results_path.open("rb") as source:
        source.seek(truncate_offset)
        removed_tail_bytes = source.read()

    timestamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
    sidecar_path = results_path.with_name(f"{results_path.name}.corrupt-tail-{timestamp}.log")
    suffix_index = 1
    while sidecar_path.exists():
        sidecar_path = results_path.with_name(
            f"{results_path.name}.corrupt-tail-{timestamp}.{suffix_index}.log"
        )
        suffix_index += 1

    header = (
        "# llmdebug eval trailing-tail repair backup\n"
        f"run_id={run_id}\n"
        f"results_path={results_path}\n"
        f"line_no={issue.line_no}\n"
        f"truncate_offset_bytes={truncate_offset}\n"
        f"original_size_bytes={original_size}\n"
        f"parse_error={issue.parse_error}\n\n"
    ).encode("utf-8", errors="strict")
    with sidecar_path.open("wb") as sidecar:
        sidecar.write(header)
        sidecar.write(removed_tail_bytes)

    with results_path.open("r+b") as handle:
        handle.truncate(truncate_offset)
        handle.flush()

    removed_bytes = original_size - truncate_offset
    return sidecar_path, removed_bytes


def load_cases(cases_dir: Path) -> list[CaseSpec]:
    if not cases_dir.exists():
        return []

    out: list[CaseSpec] = []
    for child in sorted(cases_dir.iterdir()):
        if not child.is_dir():
            continue
        case_id = child.name
        meta_path = child / "case.json"
        meta: dict[str, Any] = {}
        if meta_path.exists():
            try:
                meta = json.loads(meta_path.read_text(encoding="utf-8"))
            except json.JSONDecodeError as e:
                raise ValueError(f"{case_id}: invalid JSON in case.json: {e}") from e
            except OSError as e:
                raise ValueError(f"{case_id}: cannot read case.json: {e}") from e

        category = meta.get("category")
        if not isinstance(category, str) or category not in VALID_CATEGORIES:
            raise ValueError(f"{case_id}: category must be one of {sorted(VALID_CATEGORIES)}")

        difficulty = meta.get("difficulty")
        if not isinstance(difficulty, str) or difficulty not in VALID_DIFFICULTIES:
            raise ValueError(f"{case_id}: difficulty must be one of {sorted(VALID_DIFFICULTIES)}")

        snapshot_dependency = meta.get("snapshot_dependency")
        if (
            not isinstance(snapshot_dependency, str)
            or snapshot_dependency not in VALID_SNAPSHOT_DEPENDENCIES
        ):
            raise ValueError(
                f"{case_id}: snapshot_dependency must be one of {sorted(VALID_SNAPSHOT_DEPENDENCIES)}"
            )

        expected_exception = meta.get("expected_exception")
        if not isinstance(expected_exception, str) or not expected_exception:
            raise ValueError(f"{case_id}: expected_exception must be a non-empty string")

        tags_raw = meta.get("tags")
        if tags_raw is None:
            tags = []
        elif isinstance(tags_raw, list) and all(isinstance(x, str) and x for x in tags_raw):
            tags = list(tags_raw)
        else:
            raise ValueError(f"{case_id}: tags must be an array of non-empty strings")

        python_args_raw = meta.get("python_args")
        if (
            not isinstance(python_args_raw, list)
            or not python_args_raw
            or not all(isinstance(x, str) and x for x in python_args_raw)
        ):
            python_args = ["-m", "pytest", "-q"]
        else:
            python_args = list(python_args_raw)

        timeout_sec_raw = meta.get("timeout_sec")
        if not isinstance(timeout_sec_raw, int) or timeout_sec_raw <= 0:
            timeout_sec = 120
        else:
            timeout_sec = timeout_sec_raw

        description = meta.get("description") if isinstance(meta.get("description"), str) else None

        # Extended optional fields for imported cases.
        bug_source = meta.get("bug_source") if isinstance(meta.get("bug_source"), str) else None
        contamination_risk = (
            meta.get("contamination_risk")
            if isinstance(meta.get("contamination_risk"), str)
            else None
        )
        expected_fix_lines_raw = meta.get("expected_fix_lines")
        expected_fix_lines = (
            expected_fix_lines_raw
            if isinstance(expected_fix_lines_raw, int) and expected_fix_lines_raw > 0
            else None
        )
        source_id = meta.get("source_id") if isinstance(meta.get("source_id"), str) else None
        difficulty_rationale = (
            meta.get("difficulty_rationale")
            if isinstance(meta.get("difficulty_rationale"), str)
            else None
        )

        out.append(
            CaseSpec(
                case_id=case_id,
                path=child,
                category=category,
                difficulty=difficulty,
                snapshot_dependency=snapshot_dependency,
                expected_exception=expected_exception,
                tags=tags,
                python_args=python_args,
                timeout_sec=timeout_sec,
                description=description,
                bug_source=bug_source,
                contamination_risk=contamination_risk,
                expected_fix_lines=expected_fix_lines,
                source_id=source_id,
                difficulty_rationale=difficulty_rationale,
            )
        )
    return out


def make_patcher(args: argparse.Namespace):
    from evals.patchers import CommandPatcher, HeuristicSnapshotPatcher, NullPatcher
    from evals.patchers.command_patcher import patcher_from_env

    if args.patcher == "openai":
        from evals.patchers import OpenAICompatPatcher

        model = (args.openai_model or "").strip()
        if not model:
            raise SystemExit(
                "--openai-model is required for --patcher openai (or set OPENAI_MODEL)."
            )
        api_key = (args.openai_api_key or "").strip() or None
        return OpenAICompatPatcher(
            base_url=str(args.openai_base_url),
            model=model,
            api_key=api_key,
            timeout_sec=float(args.openai_timeout_sec),
            http2=bool(getattr(args, "openai_http2", True)),
            transport_max_retries=int(args.openai_transport_max_retries),
            transport_retry_backoff_base_sec=float(args.openai_transport_retry_backoff_base_sec),
            transport_retry_backoff_max_sec=float(args.openai_transport_retry_backoff_max_sec),
            max_propose_sec=float(args.openai_max_propose_sec),
            max_tokens=int(args.openai_max_tokens),
            temperature=float(args.openai_temperature),
            response_mode=str(args.openai_response_mode),
            force_patch_attempt=bool(args.openai_force_patch_attempt),
            seed=args.seed if bool(args.deterministic) else None,
        )

    if args.patcher == "heuristic":
        return HeuristicSnapshotPatcher()
    if args.patcher == "null":
        return NullPatcher()

    if args.patch_cmd:
        return CommandPatcher(
            cmd=str(args.patch_cmd),
            shell=bool(args.patch_cmd_shell),
            timeout_sec=float(args.patch_cmd_timeout_sec),
        )
    env_patcher = patcher_from_env()
    if env_patcher is None:
        raise SystemExit(
            "Command patcher selected but no command configured. "
            "Set --patch-cmd or LLMDEBUG_EVAL_PATCH_CMD."
        )
    return env_patcher


def _resolve_openai_context_window_for_run(
    *,
    args: argparse.Namespace,
    patcher: Any,
) -> tuple[int, dict[str, Any]]:
    manual_tokens = max(0, int(getattr(args, "openai_context_window_tokens", 0)))
    info: dict[str, Any] = {
        "openai_context_window_tokens_effective": manual_tokens,
        "openai_context_window_source": "manual_flag" if manual_tokens > 0 else "disabled",
        "openai_context_window_probe_provider": "",
        "openai_context_window_probe_error": "",
    }
    if manual_tokens > 0:
        return manual_tokens, info

    if str(getattr(args, "patcher", "")) != "openai":
        return 0, info
    if str(getattr(patcher, "name", "")) != "openai":
        return 0, info
    if not bool(getattr(args, "openai_auto_context_window", False)):
        return 0, info

    probe_context_window = getattr(patcher, "probe_context_window_tokens", None)
    if not callable(probe_context_window):
        info["openai_context_window_source"] = "auto_probe_unavailable"
        info["openai_context_window_probe_error"] = "patcher_missing_probe_context_window_tokens"
        return 0, info

    try:
        probe_tokens_raw, probe_meta_raw = probe_context_window()
    except Exception as exc:
        info["openai_context_window_source"] = "auto_probe_failed"
        info["openai_context_window_probe_error"] = f"{type(exc).__name__}: {exc}"
        return 0, info

    probe_meta = dict(probe_meta_raw) if isinstance(probe_meta_raw, dict) else {}
    provider = str(
        probe_meta.get("context_window_probe_provider") or probe_meta.get("provider") or ""
    )
    source = str(probe_meta.get("context_window_source") or "auto_probe")
    probe_error = str(probe_meta.get("context_window_probe_error") or probe_meta.get("error") or "")
    info["openai_context_window_probe_provider"] = provider

    try:
        probe_tokens = int(probe_tokens_raw)
    except (TypeError, ValueError):
        probe_tokens = 0
    if probe_tokens > 0:
        info["openai_context_window_tokens_effective"] = probe_tokens
        info["openai_context_window_source"] = source
        info["openai_context_window_probe_error"] = ""
        return probe_tokens, info

    info["openai_context_window_source"] = "auto_probe_failed"
    info["openai_context_window_probe_error"] = probe_error or "probe_returned_no_context_window"
    return 0, info


def _build_run_metadata(
    *,
    args: argparse.Namespace,
    patcher_name: str,
    run_id: str,
    started_at_utc: str,
    run_settings_fingerprint: str,
) -> dict[str, Any]:
    patcher_config = _build_patcher_config(args=args, patcher_name=patcher_name)
    metadata: dict[str, Any] = {
        "run_id": run_id,
        "started_at_utc": started_at_utc,
        "deterministic": bool(args.deterministic),
        "seed": args.seed if bool(args.deterministic) else None,
        "python_version": platform.python_version(),
        "platform": platform.platform(),
        "llmdebug_version": _resolve_llmdebug_version(),
        "patcher_name": patcher_name,
        "patcher_config": patcher_config,
        "patcher_config_fingerprint": _sha256_jsonlike(patcher_config),
        "run_settings_fingerprint": run_settings_fingerprint,
        "record_policy": {
            "prompt": str(args.record_prompt),
            "evidence": str(args.record_evidence),
        },
    }
    env_capture = _collect_allowed_env(args.record_env)
    if env_capture:
        metadata["env"] = env_capture
    dependency_versions = _collect_dependency_versions(mode=str(args.record_deps))
    if dependency_versions:
        metadata["dependency_versions"] = dependency_versions
    return metadata


def _build_patcher_config(*, args: argparse.Namespace, patcher_name: str) -> dict[str, Any]:
    config: dict[str, Any] = {
        "patcher": str(args.patcher),
        "patcher_name": patcher_name,
        "k": int(args.k),
        "transport_fail_fast_streak": int(
            getattr(args, "transport_fail_fast_streak", DEFAULT_TRANSPORT_FAIL_FAST_STREAK)
        ),
        "output_format": str(args.output_format),
        "force_patch_attempt": bool(args.force_patch_attempt),
        "deterministic": bool(args.deterministic),
        "seed": args.seed if bool(args.deterministic) else None,
        "no_files_context": bool(args.no_files_context),
        "max_context_chars": int(args.max_context_chars),
        "context_protocol": str(args.context_protocol),
        "context_stage_a_max_requests": int(args.context_stage_a_max_requests),
        "context_stage_a_max_chars": int(args.context_stage_a_max_chars),
        "retry_prompt_mode": str(args.retry_prompt_mode),
        "retry_context_mode": str(args.retry_context_mode),
        "retry_history_max_chars": int(args.retry_history_max_chars),
        "pytest_output_tail_lines": int(
            getattr(args, "pytest_output_tail_lines", DEFAULT_PYTEST_OUTPUT_TAIL_LINES)
        ),
        "snapshot_artifact_max_string_chars": int(
            getattr(
                args,
                "snapshot_artifact_max_string_chars",
                DEFAULT_SNAPSHOT_ARTIFACT_MAX_STRING_CHARS,
            )
        ),
        "prompt_budget_total_chars": int(args.prompt_budget_total_chars),
        "prompt_budget_retry_delta_chars": int(args.prompt_budget_retry_delta_chars),
        "prompt_budget_stdout_chars": int(args.prompt_budget_stdout_chars),
        "prompt_budget_snapshot_chars": int(args.prompt_budget_snapshot_chars),
        "prompt_budget_evidence_chars": int(args.prompt_budget_evidence_chars),
        "prompt_budget_retry_history_chars": int(args.prompt_budget_retry_history_chars),
        "context_compaction_mode": str(args.context_compaction_mode),
        "context_compaction_scope": str(args.context_compaction_scope),
        "context_compaction_max_input_chars": int(args.context_compaction_max_input_chars),
        "context_compaction_target_chars": int(args.context_compaction_target_chars),
        "context_compaction_max_calls": int(args.context_compaction_max_calls),
        "openai_auto_context_window": bool(getattr(args, "openai_auto_context_window", False)),
        "openai_response_mode_autoswitch": str(
            getattr(args, "openai_response_mode_autoswitch", "off")
        ),
        "adaptive_gate_policy": str(getattr(args, "adaptive_gate_policy", "balanced")),
        "adaptive_max_helper_calls": int(getattr(args, "adaptive_max_helper_calls", 1)),
        "adaptive_stagnation_window": int(getattr(args, "adaptive_stagnation_window", 1)),
        "adaptive_saturation_threshold": float(
            getattr(args, "adaptive_saturation_threshold", 0.75)
        ),
        "adaptive_no_progress_token_cap": int(
            getattr(args, "adaptive_no_progress_token_cap", DEFAULT_ADAPTIVE_NO_PROGRESS_TOKEN_CAP)
        ),
        "adaptive_helper_budget_profile": str(
            getattr(args, "adaptive_helper_budget_profile", "default")
        ),
        "adaptive_budget_topup_max_calls": int(
            getattr(
                args, "adaptive_budget_topup_max_calls", DEFAULT_ADAPTIVE_BUDGET_TOPUP_MAX_CALLS
            )
        ),
        "adaptive_openai_helper_response_mode": str(
            getattr(args, "adaptive_openai_helper_response_mode", "inherit")
        ),
        "adaptive_informativeness_pregate": bool(
            getattr(args, "adaptive_informativeness_pregate", False)
        ),
        "adaptive_informativeness_pregate_threshold": float(
            getattr(args, "adaptive_informativeness_pregate_threshold", 0.7)
        ),
        "adaptive_online_policy_mode": str(getattr(args, "adaptive_online_policy_mode", "off")),
        "adaptive_online_policy_algo": str(getattr(args, "adaptive_online_policy_algo", "linucb")),
        "adaptive_online_alpha": float(getattr(args, "adaptive_online_alpha", 0.8)),
        "adaptive_online_epsilon": float(getattr(args, "adaptive_online_epsilon", 0.05)),
        "adaptive_online_l2": float(getattr(args, "adaptive_online_l2", 1.0)),
        "adaptive_online_warmup_decisions": int(
            getattr(args, "adaptive_online_warmup_decisions", 200)
        ),
        "adaptive_online_save_every": int(getattr(args, "adaptive_online_save_every", 50)),
        "adaptive_online_state_path": str(getattr(args, "adaptive_online_state_path", "")),
        "adaptive_online_events_path": str(getattr(args, "adaptive_online_events_path", "")),
        "adaptive_reward_lambda_tokens": float(
            getattr(args, "adaptive_reward_lambda_tokens", ADAPTIVE_REWARD_LAMBDA_TOKENS_DEFAULT)
        ),
        "adaptive_reward_mu_latency": float(
            getattr(args, "adaptive_reward_mu_latency", ADAPTIVE_REWARD_MU_LATENCY_DEFAULT)
        ),
        "adaptive_reward_token_scale": float(
            getattr(args, "adaptive_reward_token_scale", ADAPTIVE_REWARD_TOKEN_SCALE_DEFAULT)
        ),
        "adaptive_reward_latency_scale": float(
            getattr(args, "adaptive_reward_latency_scale", ADAPTIVE_REWARD_LATENCY_SCALE_DEFAULT)
        ),
        "syntax_preflight_enabled": bool(getattr(args, "syntax_preflight_enabled", True)),
        "syntax_preflight_max_cycles": int(
            getattr(args, "syntax_preflight_max_cycles", DEFAULT_SYNTAX_PREFLIGHT_MAX_CYCLES)
        ),
        "patch_sanitize_enabled": bool(getattr(args, "patch_sanitize_enabled", True)),
        "patch_verifier_infer_test_source_scope": bool(
            getattr(args, "patch_verifier_infer_test_source_scope", True)
        ),
    }
    try:
        config["execution"] = _execution_config_payload(build_execution_config_from_args(args))
    except ValueError:
        pass
    if str(args.patcher) == "openai":
        config.update(
            {
                "openai_base_url": str(args.openai_base_url),
                "openai_model": str(args.openai_model),
                "openai_http2": bool(getattr(args, "openai_http2", True)),
                "openai_timeout_sec": float(args.openai_timeout_sec),
                "openai_transport_max_retries": int(args.openai_transport_max_retries),
                "openai_transport_retry_backoff_base_sec": float(
                    args.openai_transport_retry_backoff_base_sec
                ),
                "openai_transport_retry_backoff_max_sec": float(
                    args.openai_transport_retry_backoff_max_sec
                ),
                "openai_max_propose_sec": float(args.openai_max_propose_sec),
                "openai_max_tokens": int(args.openai_max_tokens),
                "openai_context_window_tokens": int(args.openai_context_window_tokens),
                "openai_auto_context_window": bool(args.openai_auto_context_window),
                "openai_temperature": float(args.openai_temperature),
                "openai_response_mode": str(args.openai_response_mode),
                "openai_response_mode_autoswitch": str(args.openai_response_mode_autoswitch),
                "openai_force_patch_attempt": bool(args.openai_force_patch_attempt),
            }
        )
    elif str(args.patcher) == "command":
        config["patch_cmd"] = str(args.patch_cmd or os.getenv("LLMDEBUG_EVAL_PATCH_CMD", ""))
        config["patch_cmd_shell"] = bool(
            args.patch_cmd_shell
            or os.getenv("LLMDEBUG_EVAL_PATCH_CMD_SHELL", "").strip().lower()
            in {"1", "true", "yes", "on"}
        )
        try:
            env_timeout = float(os.getenv("LLMDEBUG_EVAL_PATCH_CMD_TIMEOUT_SEC", "30.0"))
        except ValueError:
            env_timeout = 30.0
        config["patch_cmd_timeout_sec"] = float(
            args.patch_cmd_timeout_sec if args.patch_cmd else env_timeout
        )
    return config


def _resolve_llmdebug_version() -> str:
    try:
        import llmdebug  # type: ignore

        value = getattr(llmdebug, "__version__", None)
        if isinstance(value, str) and value:
            return value
    except Exception:
        pass
    return "unknown"


def _collect_allowed_env(keys: list[str]) -> dict[str, str]:
    out: dict[str, str] = {}
    for raw in keys:
        key = raw.strip()
        if not key:
            continue
        value = os.environ.get(key)
        if value is not None:
            out[key] = value
    return out


def _collect_dependency_versions(*, mode: str) -> dict[str, str]:
    if mode == "none":
        return {}

    try:
        from importlib import metadata as importlib_metadata
    except ImportError:
        return {}

    if mode == "runtime":
        out: dict[str, str] = {}
        for dist in importlib_metadata.distributions():
            try:
                name = dist.metadata.get("Name")
                version = dist.version
            except Exception:
                continue
            if not name or not version:
                continue
            out[str(name)] = str(version)
        return dict(sorted(out.items()))

    names = (
        "llmdebug",
        "pytest",
        "mcp",
        "openai",
        "numpy",
        "torch",
        "jax",
        "tensorflow",
    )
    out: dict[str, str] = {}
    for name in names:
        try:
            out[name] = importlib_metadata.version(name)
        except importlib_metadata.PackageNotFoundError:
            continue
        except Exception:
            continue
    return out


def _sha256_text(text: str) -> str:
    return hashlib.sha256(text.encode("utf-8")).hexdigest()


def _sha256_jsonlike(data: Any) -> str:
    payload = json.dumps(
        data, ensure_ascii=False, sort_keys=True, separators=(",", ":"), default=str
    )
    return _sha256_text(payload)


def _normalize_openai_base_url(base_url: str) -> str:
    base = base_url.rstrip("/")
    if base.endswith(("/v1", "/v2", "/v3", "/v4")):
        return base
    return base + "/v1"


def _default_run_metadata(*, run_id: str, patcher_name: str) -> dict[str, Any]:
    return {
        "run_id": run_id,
        "started_at_utc": datetime.now(timezone.utc)
        .replace(microsecond=0)
        .isoformat()
        .replace("+00:00", "Z"),
        "deterministic": False,
        "seed": None,
        "python_version": platform.python_version(),
        "platform": platform.platform(),
        "llmdebug_version": _resolve_llmdebug_version(),
        "patcher_name": patcher_name,
        "patcher_config": {"patcher_name": patcher_name},
        "patcher_config_fingerprint": _sha256_text("default"),
        "run_settings_fingerprint": _sha256_text("default-run-settings"),
        "record_policy": {"prompt": "hash", "evidence": "hash"},
    }


def _build_base_record(
    *,
    case: CaseSpec,
    condition: Condition,
    run_id: str,
    attempt: int,
    patcher_name: str,
    run_metadata: dict[str, Any],
    attempt_metadata: dict[str, Any],
    model_metadata: dict[str, Any],
    context_protocol: str = "monolithic",
) -> dict[str, Any]:
    return {
        "schema_version": EVAL_RECORD_SCHEMA_VERSION,
        "run_id": run_id,
        "case_id": case.case_id,
        "category": case.category,
        "difficulty": case.difficulty,
        "snapshot_dependency": case.snapshot_dependency,
        "expected_exception": case.expected_exception,
        "tags": case.tags,
        "bug_source": case.bug_source,
        "contamination_risk": case.contamination_risk,
        "condition": condition.value,
        "attempt": attempt,
        "patcher": patcher_name,
        "context_protocol": context_protocol,
        "run_metadata": dict(run_metadata),
        "attempt_metadata": dict(attempt_metadata),
        "model_metadata": dict(model_metadata),
    }


def _empty_attempt_metadata(*, attempt: int) -> dict[str, Any]:
    return {
        "attempt": attempt,
        "max_attempts": 0,
        "attempt_ordinal_label": "0/0",
        "prompt_sha256": None,
        "prompt_path": None,
        "evidence_sha256": None,
        "evidence_path": None,
        "stage_a_prompt_sha256": None,
        "stage_a_response_sha256": None,
        "stage_a_requests_count": 0,
        "stage_a_chars_returned": 0,
        "stage_a_request_success": None,
        "stage_b_prompt_chars": None,
        "llm_call_count": 0,
        "tokens_prompt": 0,
        "tokens_completion": 0,
        "tokens_total": 0,
        "context_protocol": "monolithic",
        "retry_prompt_mode": "full",
        "retry_context_mode": "history",
        "retry_history_chars": 0,
        "retry_history_truncated": False,
        "retry_history_turns_included": 0,
        "retry_history_budget_chars": 0,
        "retry_history_budget_applied": False,
        "retry_delta_prompt": False,
        "retry_delta_chars": 0,
        "adaptive_mode": "none",
        "adaptive_policy": "disabled",
        "adaptive_signature_repeat": False,
        "adaptive_stagnation_streak": 0,
        "adaptive_helper_invoked": False,
        "adaptive_helper_decision_reason": "",
        "adaptive_stagnation_override_applied": False,
        "adaptive_stagnation_override_reason": "",
        "adaptive_helper_need_more_context": None,
        "adaptive_helper_calls_used_before_attempt": 0,
        "adaptive_helper_calls_used_after_attempt": 0,
        "adaptive_helper_budget_max_calls": 0,
        "adaptive_helper_budget_unlimited": True,
        "adaptive_helper_budget_exhausted": False,
        "adaptive_helper_saturation_guarded": False,
        "adaptive_helper_budget_profile": "default",
        "adaptive_budget_topup_granted": False,
        "adaptive_budget_topup_reason": "",
        "adaptive_context_requested": False,
        "adaptive_snapshot_included": False,
        "adaptive_snapshot_attempt1_denied": False,
        "adaptive_snapshot_attempt1_denied_reason": "",
        "adaptive_progress_made": False,
        "adaptive_progress_reason": "",
        "adaptive_no_progress_streak": 0,
        "adaptive_progress_stop_candidate": False,
        "adaptive_progress_stop_triggered": False,
        "adaptive_progress_stop_reason": "",
        "evidence_ids_new_count": 0,
        "evidence_ids_reused_count": 0,
        "evidence_ids_sent_count": 0,
        "stage_a_index_included": False,
        "stage_a_index_reason": "",
        "openai_preflight_prompt_cap_chars": None,
        "openai_preflight_trimmed": False,
        "openai_context_window_source": "",
        "openai_context_window_probe_provider": "",
        "openai_context_window_probe_error": "",
        "openai_response_mode_autoswitch_policy": "off",
        "openai_response_mode_autoswitched": False,
        "openai_response_mode_autoswitch_from": "",
        "openai_response_mode_autoswitch_to": "",
        "openai_response_mode_autoswitch_cycle": 0,
        "openai_response_mode_autoswitch_reason": "",
        "prompt_budget_used_chars": None,
        "prompt_budget_overflow_chars": 0,
        "prompt_budget_dropped_sections": [],
        "context_compaction_mode": "off",
        "context_compaction_scope": [],
        "context_compaction_supported": False,
        "context_compaction_requested": False,
        "context_compaction_request_reason": "",
        "context_compaction_trigger": "",
        "context_compaction_auto_overflow_triggered": False,
        "context_compaction_invoked": False,
        "context_compaction_success": False,
        "context_compaction_error": "",
        "context_compaction_calls_used": 0,
        "context_compaction_max_calls": 0,
        "context_compaction_source_sections": [],
        "context_compaction_source_chars": {},
        "context_compaction_replaced_sections": [],
        "context_compaction_input_chars": 0,
        "context_compaction_target_chars": 0,
        "context_compaction_output_chars": 0,
        "context_compaction_prompt_chars": 0,
        "patch_verifier_enabled": False,
        "patch_verifier_scope": PATCH_VERIFIER_SCOPE_DISABLED,
        "patch_verifier_passed": None,
        "patch_verifier_score": None,
        "patch_verifier_reasons": [],
        "patcher_request_failed": False,
        "patcher_request_failure_kind": "",
        "patcher_request_failure_error": "",
        "patcher_request_failure_status": None,
        "patcher_request_failure_exc": "",
        "runner_forced_patch_skipped": False,
        "runner_forced_patch_skip_reason": "",
        "patch_sanitize_enabled": False,
        "patch_sanitize_passed": False,
        "patch_sanitize_rejected": False,
        "patch_sanitize_reasons": [],
        "patch_sanitize_error_msg": "",
        "adaptive_informativeness_pregate_fired": False,
        "adaptive_informativeness_pregate_score": 0.0,
        "adaptive_informativeness_pregate_exception_type": "",
        "adaptive_informativeness_pregate_reason": "",
        "adaptive_informativeness_pregate_features": {},
        "adaptive_online_policy_mode": "off",
        "adaptive_online_policy_algo": "",
        "adaptive_online_decision_id": "",
        "adaptive_online_decision_strategy": "",
        "adaptive_online_action_selected": "none",
        "adaptive_online_action_applied": "none",
        "adaptive_online_propensity": 1.0,
        "adaptive_online_override_reason": "",
        "adaptive_online_feature_vector": {},
        "adaptive_online_eligible": False,
        "adaptive_online_reward_lambda_tokens": ADAPTIVE_REWARD_LAMBDA_TOKENS_DEFAULT,
        "adaptive_online_reward_mu_latency": ADAPTIVE_REWARD_MU_LATENCY_DEFAULT,
        "adaptive_online_reward_token_scale": ADAPTIVE_REWARD_TOKEN_SCALE_DEFAULT,
        "adaptive_online_reward_latency_scale": ADAPTIVE_REWARD_LATENCY_SCALE_DEFAULT,
        "adaptive_online_reward": None,
        "adaptive_online_reward_success": 0.0,
        "adaptive_online_reward_token_penalty": 0.0,
        "adaptive_online_reward_latency_penalty": 0.0,
        "adaptive_online_updated": False,
        "adaptive_online_update_reason": "",
        "adaptive_online_event_logged": False,
        "syntax_preflight_enabled": False,
        "syntax_preflight_cycles_used": 0,
        "syntax_preflight_failures": 0,
        "syntax_preflight_last_error_type": "",
        "syntax_preflight_last_error_msg": "",
    }


def _materialize_attempt_metadata(
    *,
    attempt_dir: Path,
    attempt: int,
    max_attempts: int,
    evidence: dict[str, Any],
    prompt: str,
    record_prompt: str,
    record_evidence: str,
    context_protocol: str,
    stage_a_prompt: str | None = None,
    stage_a_raw_response: str | None = None,
    stage_a_request_payload: ContextRequestPayload | None = None,
    stage_a_requests_count: int = 0,
    stage_a_chars_returned: int = 0,
    stage_a_request_success: bool | None = None,
    retry_prompt_mode: str = "full",
    retry_context_mode: str = "history",
    retry_history_chars: int = 0,
    retry_history_truncated: bool = False,
    retry_history_turns_included: int = 0,
    retry_history_budget_chars: int = 0,
    retry_history_budget_applied: bool = False,
    retry_delta_prompt: bool = False,
    retry_delta_chars: int = 0,
    adaptive_mode: str = "none",
    adaptive_policy: str = "disabled",
    adaptive_signature_repeat: bool = False,
    adaptive_stagnation_streak: int = 0,
    adaptive_helper_invoked: bool = False,
    adaptive_helper_decision_reason: str = "",
    adaptive_stagnation_override_applied: bool = False,
    adaptive_stagnation_override_reason: str = "",
    adaptive_helper_need_more_context: bool | None = None,
    adaptive_helper_calls_used_before_attempt: int = 0,
    adaptive_helper_calls_used_after_attempt: int = 0,
    adaptive_helper_budget_max_calls: int = 0,
    adaptive_helper_budget_unlimited: bool = True,
    adaptive_helper_budget_exhausted: bool = False,
    adaptive_helper_saturation_guarded: bool = False,
    adaptive_helper_budget_profile: str = "default",
    adaptive_budget_topup_granted: bool = False,
    adaptive_budget_topup_reason: str = "",
    adaptive_context_requested: bool = False,
    adaptive_snapshot_included: bool = False,
    adaptive_snapshot_attempt1_denied: bool = False,
    adaptive_snapshot_attempt1_denied_reason: str = "",
    adaptive_progress_made: bool = False,
    adaptive_progress_reason: str = "",
    adaptive_no_progress_streak: int = 0,
    adaptive_progress_stop_candidate: bool = False,
    adaptive_progress_stop_triggered: bool = False,
    adaptive_progress_stop_reason: str = "",
    llm_call_count: int = 0,
    tokens_prompt: int = 0,
    tokens_completion: int = 0,
    tokens_total: int = 0,
    evidence_ids_new_count: int = 0,
    evidence_ids_reused_count: int = 0,
    evidence_ids_sent_count: int = 0,
    stage_a_index_included: bool = False,
    stage_a_index_reason: str = "",
    openai_preflight_prompt_cap_chars: int | None = None,
    openai_preflight_trimmed: bool = False,
    openai_context_window_source: str = "",
    openai_context_window_probe_provider: str = "",
    openai_context_window_probe_error: str = "",
    openai_response_mode_autoswitch_policy: str = "off",
    openai_response_mode_autoswitched: bool = False,
    openai_response_mode_autoswitch_from: str = "",
    openai_response_mode_autoswitch_to: str = "",
    openai_response_mode_autoswitch_cycle: int = 0,
    openai_response_mode_autoswitch_reason: str = "",
    prompt_budget_used_chars: int | None = None,
    prompt_budget_overflow_chars: int = 0,
    prompt_budget_dropped_sections: list[str] | None = None,
    context_compaction_mode: str = "off",
    context_compaction_scope: list[str] | None = None,
    context_compaction_supported: bool = False,
    context_compaction_requested: bool = False,
    context_compaction_request_reason: str = "",
    context_compaction_trigger: str = "",
    context_compaction_auto_overflow_triggered: bool = False,
    context_compaction_invoked: bool = False,
    context_compaction_success: bool = False,
    context_compaction_error: str = "",
    context_compaction_calls_used: int = 0,
    context_compaction_max_calls: int = 0,
    context_compaction_source_sections: list[str] | None = None,
    context_compaction_source_chars: dict[str, int] | None = None,
    context_compaction_replaced_sections: list[str] | None = None,
    context_compaction_input_chars: int = 0,
    context_compaction_target_chars: int = 0,
    context_compaction_output_chars: int = 0,
    context_compaction_prompt_chars: int = 0,
    adaptive_informativeness_pregate_fired: bool = False,
    adaptive_informativeness_pregate_score: float = 0.0,
    adaptive_informativeness_pregate_exception_type: str = "",
    adaptive_informativeness_pregate_reason: str = "",
    adaptive_informativeness_pregate_features: dict[str, float] | None = None,
    syntax_preflight_enabled: bool = False,
    syntax_preflight_cycles_used: int = 0,
    syntax_preflight_failures: int = 0,
    syntax_preflight_last_error_type: str = "",
    syntax_preflight_last_error_msg: str = "",
) -> dict[str, Any]:
    metadata = _empty_attempt_metadata(attempt=attempt)
    metadata["max_attempts"] = max(0, int(max_attempts))
    metadata["attempt_ordinal_label"] = f"{attempt}/{max(0, int(max_attempts))}"
    metadata["context_protocol"] = context_protocol
    metadata["stage_a_requests_count"] = int(stage_a_requests_count)
    metadata["stage_a_chars_returned"] = int(stage_a_chars_returned)
    metadata["stage_a_request_success"] = stage_a_request_success
    metadata["stage_b_prompt_chars"] = len(prompt)
    metadata["llm_call_count"] = max(0, int(llm_call_count))
    metadata["tokens_prompt"] = max(0, int(tokens_prompt))
    metadata["tokens_completion"] = max(0, int(tokens_completion))
    metadata["tokens_total"] = max(0, int(tokens_total))
    metadata["retry_prompt_mode"] = retry_prompt_mode
    metadata["retry_context_mode"] = retry_context_mode
    metadata["retry_history_chars"] = int(retry_history_chars)
    metadata["retry_history_truncated"] = bool(retry_history_truncated)
    metadata["retry_history_turns_included"] = int(retry_history_turns_included)
    metadata["retry_history_budget_chars"] = int(retry_history_budget_chars)
    metadata["retry_history_budget_applied"] = bool(retry_history_budget_applied)
    metadata["retry_delta_prompt"] = bool(retry_delta_prompt)
    metadata["retry_delta_chars"] = int(retry_delta_chars)
    metadata["adaptive_mode"] = str(adaptive_mode or "none")
    metadata["adaptive_policy"] = str(adaptive_policy or "disabled")
    metadata["adaptive_signature_repeat"] = bool(adaptive_signature_repeat)
    metadata["adaptive_stagnation_streak"] = int(adaptive_stagnation_streak)
    metadata["adaptive_helper_invoked"] = bool(adaptive_helper_invoked)
    metadata["adaptive_helper_decision_reason"] = str(adaptive_helper_decision_reason or "")
    metadata["adaptive_stagnation_override_applied"] = bool(adaptive_stagnation_override_applied)
    metadata["adaptive_stagnation_override_reason"] = str(adaptive_stagnation_override_reason or "")
    metadata["adaptive_helper_need_more_context"] = adaptive_helper_need_more_context
    metadata["adaptive_helper_calls_used_before_attempt"] = int(
        adaptive_helper_calls_used_before_attempt
    )
    metadata["adaptive_helper_calls_used_after_attempt"] = int(
        adaptive_helper_calls_used_after_attempt
    )
    metadata["adaptive_helper_budget_max_calls"] = int(adaptive_helper_budget_max_calls)
    metadata["adaptive_helper_budget_unlimited"] = bool(adaptive_helper_budget_unlimited)
    metadata["adaptive_helper_budget_exhausted"] = bool(adaptive_helper_budget_exhausted)
    metadata["adaptive_helper_saturation_guarded"] = bool(adaptive_helper_saturation_guarded)
    metadata["adaptive_helper_budget_profile"] = str(adaptive_helper_budget_profile or "default")
    metadata["adaptive_budget_topup_granted"] = bool(adaptive_budget_topup_granted)
    metadata["adaptive_budget_topup_reason"] = str(adaptive_budget_topup_reason or "")
    metadata["adaptive_context_requested"] = bool(adaptive_context_requested)
    metadata["adaptive_snapshot_included"] = bool(adaptive_snapshot_included)
    metadata["adaptive_snapshot_attempt1_denied"] = bool(adaptive_snapshot_attempt1_denied)
    metadata["adaptive_snapshot_attempt1_denied_reason"] = str(
        adaptive_snapshot_attempt1_denied_reason or ""
    )
    metadata["adaptive_progress_made"] = bool(adaptive_progress_made)
    metadata["adaptive_progress_reason"] = str(adaptive_progress_reason or "")
    metadata["adaptive_no_progress_streak"] = int(adaptive_no_progress_streak)
    metadata["adaptive_progress_stop_candidate"] = bool(adaptive_progress_stop_candidate)
    metadata["adaptive_progress_stop_triggered"] = bool(adaptive_progress_stop_triggered)
    metadata["adaptive_progress_stop_reason"] = str(adaptive_progress_stop_reason or "")
    metadata["evidence_ids_new_count"] = int(evidence_ids_new_count)
    metadata["evidence_ids_reused_count"] = int(evidence_ids_reused_count)
    metadata["evidence_ids_sent_count"] = int(evidence_ids_sent_count)
    metadata["stage_a_index_included"] = bool(stage_a_index_included)
    metadata["stage_a_index_reason"] = str(stage_a_index_reason or "")
    metadata["openai_preflight_prompt_cap_chars"] = openai_preflight_prompt_cap_chars
    metadata["openai_preflight_trimmed"] = bool(openai_preflight_trimmed)
    metadata["openai_context_window_source"] = str(openai_context_window_source or "")
    metadata["openai_context_window_probe_provider"] = str(
        openai_context_window_probe_provider or ""
    )
    metadata["openai_context_window_probe_error"] = str(openai_context_window_probe_error or "")
    metadata["openai_response_mode_autoswitch_policy"] = str(
        openai_response_mode_autoswitch_policy or "off"
    )
    metadata["openai_response_mode_autoswitched"] = bool(openai_response_mode_autoswitched)
    metadata["openai_response_mode_autoswitch_from"] = str(
        openai_response_mode_autoswitch_from or ""
    )
    metadata["openai_response_mode_autoswitch_to"] = str(openai_response_mode_autoswitch_to or "")
    metadata["openai_response_mode_autoswitch_cycle"] = int(openai_response_mode_autoswitch_cycle)
    metadata["openai_response_mode_autoswitch_reason"] = str(
        openai_response_mode_autoswitch_reason or ""
    )
    metadata["prompt_budget_used_chars"] = prompt_budget_used_chars
    metadata["prompt_budget_overflow_chars"] = int(prompt_budget_overflow_chars)
    metadata["prompt_budget_dropped_sections"] = list(prompt_budget_dropped_sections or [])
    metadata["context_compaction_mode"] = str(context_compaction_mode or "off")
    metadata["context_compaction_scope"] = list(context_compaction_scope or [])
    metadata["context_compaction_supported"] = bool(context_compaction_supported)
    metadata["context_compaction_requested"] = bool(context_compaction_requested)
    metadata["context_compaction_request_reason"] = str(context_compaction_request_reason or "")
    metadata["context_compaction_trigger"] = str(context_compaction_trigger or "")
    metadata["context_compaction_auto_overflow_triggered"] = bool(
        context_compaction_auto_overflow_triggered
    )
    metadata["context_compaction_invoked"] = bool(context_compaction_invoked)
    metadata["context_compaction_success"] = bool(context_compaction_success)
    metadata["context_compaction_error"] = str(context_compaction_error or "")
    metadata["context_compaction_calls_used"] = int(context_compaction_calls_used)
    metadata["context_compaction_max_calls"] = int(context_compaction_max_calls)
    metadata["context_compaction_source_sections"] = list(context_compaction_source_sections or [])
    metadata["context_compaction_source_chars"] = dict(context_compaction_source_chars or {})
    metadata["context_compaction_replaced_sections"] = list(
        context_compaction_replaced_sections or []
    )
    metadata["context_compaction_input_chars"] = int(context_compaction_input_chars)
    metadata["context_compaction_target_chars"] = int(context_compaction_target_chars)
    metadata["context_compaction_output_chars"] = int(context_compaction_output_chars)
    metadata["context_compaction_prompt_chars"] = int(context_compaction_prompt_chars)
    metadata["adaptive_informativeness_pregate_fired"] = bool(
        adaptive_informativeness_pregate_fired
    )
    metadata["adaptive_informativeness_pregate_score"] = float(
        adaptive_informativeness_pregate_score
    )
    metadata["adaptive_informativeness_pregate_exception_type"] = str(
        adaptive_informativeness_pregate_exception_type or ""
    )
    metadata["adaptive_informativeness_pregate_reason"] = str(
        adaptive_informativeness_pregate_reason or ""
    )
    metadata["adaptive_informativeness_pregate_features"] = dict(
        adaptive_informativeness_pregate_features or {}
    )
    metadata["syntax_preflight_enabled"] = bool(syntax_preflight_enabled)
    metadata["syntax_preflight_cycles_used"] = int(syntax_preflight_cycles_used)
    metadata["syntax_preflight_failures"] = int(syntax_preflight_failures)
    metadata["syntax_preflight_last_error_type"] = str(syntax_preflight_last_error_type or "")
    metadata["syntax_preflight_last_error_msg"] = str(syntax_preflight_last_error_msg or "")

    if record_evidence != "none":
        metadata["evidence_sha256"] = _sha256_jsonlike(evidence)
    if record_evidence == "full":
        evidence_path = attempt_dir / "evidence.json"
        evidence_path.write_text(
            json.dumps(evidence, indent=2, ensure_ascii=False), encoding="utf-8"
        )
        metadata["evidence_path"] = str(evidence_path)

    if record_prompt != "none":
        metadata["prompt_sha256"] = _sha256_text(prompt)
    if record_prompt == "full":
        prompt_path = attempt_dir / "prompt.txt"
        prompt_path.write_text(prompt, encoding="utf-8")
        metadata["prompt_path"] = str(prompt_path)

    if stage_a_prompt:
        metadata["stage_a_prompt_sha256"] = _sha256_text(stage_a_prompt)
    stage_a_response_text = ""
    if stage_a_raw_response:
        stage_a_response_text = stage_a_raw_response
    elif stage_a_request_payload is not None:
        stage_a_response_text = json.dumps(stage_a_request_payload, ensure_ascii=False, default=str)
    if stage_a_response_text:
        metadata["stage_a_response_sha256"] = _sha256_text(stage_a_response_text)

    return metadata


def _nonnegative_int(value: Any, *, default: int = 0) -> int:
    try:
        coerced = int(value)
    except (TypeError, ValueError):
        return int(default)
    return max(0, coerced)


def _usage_token_counts_from_usage_payload(usage: Any) -> tuple[int, int, int]:
    if not isinstance(usage, dict):
        return 0, 0, 0

    prompt_tokens = _nonnegative_int(usage.get("prompt_tokens"), default=0)
    completion_tokens = _nonnegative_int(usage.get("completion_tokens"), default=0)
    # Anthropic-compatible payloads often use input/output token fields.
    if prompt_tokens == 0 and completion_tokens == 0:
        prompt_tokens = _nonnegative_int(usage.get("input_tokens"), default=0)
        completion_tokens = _nonnegative_int(usage.get("output_tokens"), default=0)

    total_tokens_raw = usage.get("total_tokens")
    if total_tokens_raw is None:
        total_tokens = prompt_tokens + completion_tokens
    else:
        total_tokens = _nonnegative_int(total_tokens_raw, default=prompt_tokens + completion_tokens)
    return prompt_tokens, completion_tokens, total_tokens


def _usage_token_counts_from_patch_meta(patch_meta: dict[str, Any] | None) -> tuple[int, int, int]:
    if not isinstance(patch_meta, dict):
        return 0, 0, 0

    usage_prompt, usage_completion, usage_total = _usage_token_counts_from_usage_payload(
        patch_meta.get("usage")
    )
    prompt_tokens = _nonnegative_int(patch_meta.get("tokens_prompt"), default=usage_prompt)
    completion_tokens = _nonnegative_int(
        patch_meta.get("tokens_completion"),
        default=usage_completion,
    )
    total_tokens = _nonnegative_int(
        patch_meta.get("tokens_total"),
        default=usage_total if usage_total > 0 else (prompt_tokens + completion_tokens),
    )
    return prompt_tokens, completion_tokens, total_tokens


def _llm_call_count_from_meta(meta: dict[str, Any] | None, *, default: int = 0) -> int:
    if not isinstance(meta, dict):
        return max(0, int(default))
    value = meta.get("llm_call_count")
    if isinstance(value, int) and value >= 0:
        return value
    return max(0, int(default))


def _aggregate_attempt_llm_usage(
    *,
    stage_a_meta: dict[str, Any] | None,
    patch_meta: dict[str, Any] | None,
    stage_a_default_llm_calls: int,
    patch_default_llm_calls: int,
) -> tuple[int, int, int, int]:
    stage_calls = _llm_call_count_from_meta(stage_a_meta, default=stage_a_default_llm_calls)
    patch_calls = _llm_call_count_from_meta(patch_meta, default=patch_default_llm_calls)
    stage_prompt, stage_completion, stage_total = _usage_token_counts_from_patch_meta(stage_a_meta)
    patch_prompt, patch_completion, patch_total = _usage_token_counts_from_patch_meta(patch_meta)
    return (
        stage_calls + patch_calls,
        stage_prompt + patch_prompt,
        stage_completion + patch_completion,
        stage_total + patch_total,
    )


def _apply_usage_totals_to_patch_meta(
    *,
    patch_meta: dict[str, Any],
    llm_call_count: int,
    tokens_prompt: int,
    tokens_completion: int,
    tokens_total: int,
) -> None:
    normalized_calls = _nonnegative_int(llm_call_count, default=0)
    normalized_prompt = _nonnegative_int(tokens_prompt, default=0)
    normalized_completion = _nonnegative_int(tokens_completion, default=0)
    normalized_total = _nonnegative_int(
        tokens_total,
        default=normalized_prompt + normalized_completion,
    )
    patch_meta["llm_call_count"] = normalized_calls
    patch_meta["tokens_prompt"] = normalized_prompt
    patch_meta["tokens_completion"] = normalized_completion
    patch_meta["tokens_total"] = normalized_total
    patch_meta["usage"] = {
        "prompt_tokens": normalized_prompt,
        "completion_tokens": normalized_completion,
        "total_tokens": normalized_total,
    }


def _sync_attempt_usage_metadata(
    *,
    attempt_metadata: dict[str, Any],
    patch_meta: dict[str, Any],
    llm_call_count: int,
) -> None:
    prompt_tokens, completion_tokens, total_tokens = _usage_token_counts_from_patch_meta(patch_meta)
    normalized_calls = _nonnegative_int(llm_call_count, default=0)
    attempt_metadata["llm_call_count"] = normalized_calls
    attempt_metadata["tokens_prompt"] = prompt_tokens
    attempt_metadata["tokens_completion"] = completion_tokens
    attempt_metadata["tokens_total"] = total_tokens
    patch_meta["llm_call_count"] = normalized_calls
    patch_meta["tokens_prompt"] = prompt_tokens
    patch_meta["tokens_completion"] = completion_tokens
    patch_meta["tokens_total"] = total_tokens


def _summarize_context_compaction_meta(meta: dict[str, Any]) -> dict[str, Any]:
    def _clip(value: Any, *, max_chars: int = 320) -> str:
        text = str(value or "").strip()
        if len(text) <= max_chars:
            return text
        return text[: max_chars - 3].rstrip() + "..."

    out: dict[str, Any] = {}
    for key in (
        "llm_call_count",
        "compact_context_duration_sec",
        "compact_context_target_chars",
        "compact_context_input_chars",
        "compact_context_output_chars",
        "compact_context_output_truncated",
        "api_compat_seed_requested",
        "api_compat_seed_acknowledged",
        "api_compat_seed_response",
    ):
        if key in meta:
            out[key] = meta[key]
    status_value = meta.get("status")
    if isinstance(status_value, int):
        out["status"] = status_value
    usage = meta.get("usage")
    if isinstance(usage, dict):
        prompt_tokens, completion_tokens, total_tokens = _usage_token_counts_from_usage_payload(
            usage
        )
        out["usage"] = {
            "prompt_tokens": prompt_tokens,
            "completion_tokens": completion_tokens,
            "total_tokens": total_tokens,
        }
    for key in ("error", "exc", "reason", "fallback_reason"):
        text = _clip(meta.get(key), max_chars=240)
        if text:
            out[key] = text
    body_text = _clip(meta.get("body"), max_chars=320)
    if body_text:
        out["body"] = body_text
    return out


def _sync_syntax_preflight_metadata(
    *,
    attempt_metadata: dict[str, Any],
    patch_meta: dict[str, Any],
    enabled: bool,
    cycles_used: int,
    failures: int,
    last_error_type: str,
    last_error_msg: str,
    events: list[dict[str, Any]],
) -> None:
    patch_meta["syntax_preflight_enabled"] = bool(enabled)
    patch_meta["syntax_preflight_cycles_used"] = int(cycles_used)
    patch_meta["syntax_preflight_failures"] = int(failures)
    patch_meta["syntax_preflight_last_error_type"] = str(last_error_type or "")
    patch_meta["syntax_preflight_last_error_msg"] = str(last_error_msg or "")
    patch_meta["syntax_preflight_events"] = list(events)
    attempt_metadata["syntax_preflight_enabled"] = bool(enabled)
    attempt_metadata["syntax_preflight_cycles_used"] = int(cycles_used)
    attempt_metadata["syntax_preflight_failures"] = int(failures)
    attempt_metadata["syntax_preflight_last_error_type"] = str(last_error_type or "")
    attempt_metadata["syntax_preflight_last_error_msg"] = str(last_error_msg or "")


def _is_context_limit_http_error_message(body: Any) -> bool:
    text = str(body or "").strip().lower()
    if not text:
        return False
    return any(marker in text for marker in PATCHER_CONTEXT_LIMIT_ERROR_MARKERS)


def _is_retryable_transport_http_status(status: Any) -> bool:
    if not isinstance(status, int):
        return False
    return status in RETRYABLE_HTTP_STATUS_CODES or (500 <= status < 600)


def _classify_patcher_request_failure(meta: dict[str, Any]) -> tuple[bool, str]:
    error = str(meta.get("error") or "").strip()
    if not error:
        return False, ""
    if error in {"request_failed", "request_budget_exceeded", "bad_request_url"}:
        return True, "transport"
    if error == "http_error":
        if _is_context_limit_http_error_message(meta.get("body")):
            return True, "context_limit"
        if _is_retryable_transport_http_status(meta.get("status")):
            return True, "transport"
    return False, ""


def _sync_patcher_request_failure_metadata(
    *,
    attempt_metadata: dict[str, Any],
    patch_meta: dict[str, Any],
    failed: bool,
    kind: str = "",
    error: str = "",
    status: int | None = None,
    exc: str = "",
    runner_forced_patch_skipped: bool = False,
    runner_forced_patch_skip_reason: str = "",
) -> None:
    values: dict[str, Any] = {
        "patcher_request_failed": bool(failed),
        "patcher_request_failure_kind": str(kind or ""),
        "patcher_request_failure_error": str(error or ""),
        "patcher_request_failure_status": int(status) if isinstance(status, int) else None,
        "patcher_request_failure_exc": str(exc or ""),
        "runner_forced_patch_skipped": bool(runner_forced_patch_skipped),
        "runner_forced_patch_skip_reason": str(runner_forced_patch_skip_reason or ""),
    }
    patch_meta.update(values)
    attempt_metadata.update(values)


def _sync_patch_sanitize_metadata(
    *,
    attempt_metadata: dict[str, Any],
    patch_meta: dict[str, Any],
    enabled: bool,
    passed: bool,
    rejected: bool,
    reasons: list[str],
    error_msg: str,
) -> None:
    values: dict[str, Any] = {
        "patch_sanitize_enabled": bool(enabled),
        "patch_sanitize_passed": bool(passed),
        "patch_sanitize_rejected": bool(rejected),
        "patch_sanitize_reasons": list(reasons),
        "patch_sanitize_error_msg": str(error_msg or ""),
    }
    patch_meta.update(values)
    attempt_metadata.update(values)


def _sync_openai_response_mode_autoswitch_metadata(
    *,
    attempt_metadata: dict[str, Any],
    patch_meta: dict[str, Any],
    policy: str,
    switched: bool,
    from_mode: str = "",
    to_mode: str = "",
    cycle: int = 0,
    reason: str = "",
) -> None:
    values: dict[str, Any] = {
        "openai_response_mode_autoswitch_policy": str(policy or "off"),
        "openai_response_mode_autoswitched": bool(switched),
        "openai_response_mode_autoswitch_from": str(from_mode or ""),
        "openai_response_mode_autoswitch_to": str(to_mode or ""),
        "openai_response_mode_autoswitch_cycle": int(cycle),
        "openai_response_mode_autoswitch_reason": str(reason or ""),
    }
    patch_meta.update(values)
    attempt_metadata.update(values)


def _maybe_autoswitch_openai_response_mode(
    *,
    patcher_for_attempt: Any,
    policy: str,
    reason: str,
    syntax_cycle: int,
    patch_meta: dict[str, Any],
    attempt_metadata: dict[str, Any],
) -> tuple[Any, bool]:
    if policy != "retry_to_diff":
        return patcher_for_attempt, False
    current_mode = str(getattr(patcher_for_attempt, "response_mode", "")).strip()
    if current_mode != "edits":
        return patcher_for_attempt, False
    try:
        next_patcher = replace(patcher_for_attempt, response_mode="diff")
    except Exception as exc:
        patch_meta["openai_response_mode_autoswitch_error"] = f"{type(exc).__name__}: {exc}"
        return patcher_for_attempt, False
    _sync_openai_response_mode_autoswitch_metadata(
        attempt_metadata=attempt_metadata,
        patch_meta=patch_meta,
        policy=policy,
        switched=True,
        from_mode="edits",
        to_mode="diff",
        cycle=syntax_cycle,
        reason=reason,
    )
    return next_patcher, True


def _append_retry_history_turn_if_enabled(
    *,
    retry_context_mode: str,
    retry_history_turns: list[str],
    attempt: int,
    note: str,
    failure_signature: str,
    patch_meta: dict[str, Any],
) -> None:
    if retry_context_mode != "history":
        return
    retry_history_turns.append(
        _render_retry_history_turn(
            attempt=attempt,
            note=note,
            failure_signature=failure_signature,
            patch_meta=patch_meta,
        )
    )


def _build_attempt_outcome_record(
    *,
    case: CaseSpec,
    condition: Condition,
    run_id: str,
    attempt: int,
    patcher_name: str,
    run_metadata: dict[str, Any],
    attempt_metadata: dict[str, Any],
    patcher: Any,
    patch_meta: dict[str, Any] | None,
    context_protocol: str,
    success: bool,
    note: str,
    before: RunResult,
    cumulative_runtime_sec: float,
    after: RunResult | None = None,
    attempt_wall_clock_sec: float | None = None,
) -> dict[str, Any]:
    record = _build_base_record(
        case=case,
        condition=condition,
        run_id=run_id,
        attempt=attempt,
        patcher_name=patcher_name,
        run_metadata=run_metadata,
        attempt_metadata=attempt_metadata,
        model_metadata=_build_model_metadata(patcher=patcher, patch_meta=patch_meta),
        context_protocol=context_protocol,
    )
    default_attempt_wall_clock_sec = float(before.duration_sec)
    if after is not None:
        default_attempt_wall_clock_sec += float(after.duration_sec)
    measured_attempt_wall_clock_sec = (
        float(attempt_wall_clock_sec)
        if isinstance(attempt_wall_clock_sec, (int, float))
        else default_attempt_wall_clock_sec
    )
    measured_attempt_wall_clock_sec = max(0.0, measured_attempt_wall_clock_sec)
    payload: dict[str, Any] = {
        "success": success,
        "before_returncode": before.returncode,
        "before_duration_sec": before.duration_sec,
        "cumulative_runtime_sec": cumulative_runtime_sec,
        "attempt_wall_clock_sec": measured_attempt_wall_clock_sec,
        "note": note,
    }
    if after is not None:
        payload["after_returncode"] = after.returncode
        payload["after_duration_sec"] = after.duration_sec
    if isinstance(patch_meta, dict):
        payload["patch_meta"] = patch_meta
        payload["patch_touches_tests"] = bool(patch_meta.get("patch_touches_tests"))
        payload["patch_test_file_count"] = _nonnegative_int(patch_meta.get("patch_test_file_count"))
        payload["patch_file_count"] = _nonnegative_int(patch_meta.get("patch_file_count"))
    record.update(payload)
    record.update(_timeout_fields(after if after is not None else before))
    return record


def _measure_attempt_wall_clock_sec(attempt_start_monotonic: float) -> float:
    return max(0.0, float(time.perf_counter() - attempt_start_monotonic))


def _build_model_metadata(*, patcher: Any, patch_meta: dict[str, Any] | None) -> dict[str, Any]:
    patcher_name = str(getattr(patcher, "name", "unknown"))
    metadata: dict[str, Any] = {"backend": patcher_name, "name": patcher_name}
    if patcher_name != "openai":
        return metadata

    patch_meta_dict = patch_meta if isinstance(patch_meta, dict) else {}
    requested_seed = patch_meta_dict.get("api_compat_seed_requested")
    if requested_seed is None:
        requested_seed = getattr(patcher, "seed", None)
    acknowledged = patch_meta_dict.get("api_compat_seed_acknowledged")
    metadata.update(
        {
            "backend": "openai",
            "base_url": str(
                patch_meta_dict.get("base_url")
                or _normalize_openai_base_url(str(getattr(patcher, "base_url", "")))
            ),
            "model": str(patch_meta_dict.get("model") or getattr(patcher, "model", "")),
            "temperature": patch_meta_dict.get(
                "temperature", getattr(patcher, "temperature", None)
            ),
            "max_tokens": patch_meta_dict.get("max_tokens", getattr(patcher, "max_tokens", None)),
            "response_mode": str(
                patch_meta_dict.get("response_mode") or getattr(patcher, "response_mode", "")
            ),
            "api_compat_seed_requested": requested_seed,
            "api_compat_seed_acknowledged": bool(acknowledged)
            if acknowledged is not None
            else False,
            "api_compat_seed_response": patch_meta_dict.get("api_compat_seed_response"),
        }
    )
    return metadata


def _ensure_deterministic_seed_ack(
    *,
    deterministic: bool,
    seed: int | None,
    patcher_name: str,
    patch_meta: dict[str, Any],
    case_id: str,
    condition: str,
    attempt: int,
) -> None:
    if not deterministic or patcher_name != "openai":
        return

    requested = patch_meta.get("api_compat_seed_requested")
    acknowledged = patch_meta.get("api_compat_seed_acknowledged")
    if requested != seed:
        raise RuntimeError(
            "deterministic seed mismatch for "
            f"{case_id}/{condition}/attempt={attempt}: requested={requested!r}, expected={seed!r}"
        )
    if acknowledged is not True:
        error = patch_meta.get("error")
        body = patch_meta.get("body")
        exc = patch_meta.get("exc")
        details = error or body or exc or "backend did not acknowledge seed support"
        raise RuntimeError(
            f"deterministic mode failed for {case_id}/{condition}/attempt={attempt}: {details}"
        )


def _adaptive_gate_should_invoke_helper(
    *,
    policy: str,
    attempt: int,
    timed_out: bool,
    signature_repeated: bool,
    stagnation_streak: int,
    stagnation_window: int,
) -> tuple[bool, str]:
    if timed_out:
        return True, "gate_timeout"
    if policy == "aggressive":
        return True, "gate_aggressive"

    normalized_window = max(1, int(stagnation_window))
    if policy == "conservative":
        required_repeats = max(normalized_window + 1, 2)
        if signature_repeated and stagnation_streak >= required_repeats:
            return True, "gate_conservative_repeated_signature"
        return False, "gate_conservative_waiting"

    # balanced (default)
    if attempt >= 2 and signature_repeated and stagnation_streak >= normalized_window:
        return True, "gate_balanced_repeated_signature"
    return False, "gate_balanced_waiting"


def _adaptive_progress_signal(
    *,
    signature_repeated: bool,
    new_evidence_count: int,
) -> tuple[bool, str]:
    if not signature_repeated:
        return True, "failure_signature_changed"
    if int(new_evidence_count) > 0:
        return True, "new_evidence_added"
    return False, "repeated_signature_no_new_evidence"


def _adaptive_should_stop_for_no_progress(
    *,
    no_progress_streak: int,
    stop_window: int,
    helper_budget_exhausted: bool,
    helper_saturation_guarded: bool,
    helper_decision_reason: str,
) -> tuple[bool, str]:
    if int(no_progress_streak) < max(1, int(stop_window)):
        return False, "streak_below_window"
    if helper_budget_exhausted:
        return True, "helper_budget_exhausted"
    if helper_saturation_guarded:
        return True, "helper_saturation_guard"
    if helper_decision_reason in {
        "llm_declined_helper",
        "llm_invalid_response_no_helper",
        "llm_snapshot_request_denied_attempt1",
        "request_context_unavailable",
    }:
        return True, helper_decision_reason
    return False, "waiting_for_more_signal"


def _mark_adaptive_progress_stop(
    *,
    attempt_metadata: dict[str, Any],
    patch_meta: dict[str, Any],
    is_adaptive_condition: bool,
    adaptive_progress_stop_candidate: bool,
    adaptive_progress_stop_reason: str,
    note: str,
) -> bool:
    triggered = bool(
        is_adaptive_condition
        and adaptive_progress_stop_candidate
        and note
        in {
            "hard_stop_no_progress",
            "no_patch_proposed",
            "patcher_request_failed",
            "forced_noop_patch",
            "patch_verifier_rejected",
            "syntax_preflight_failed",
            "patch_apply_failed",
            "tests_still_failing",
        }
    )
    attempt_metadata["adaptive_progress_stop_triggered"] = triggered
    patch_meta["adaptive_progress_stop_triggered"] = triggered
    patch_meta["adaptive_progress_stop_reason"] = adaptive_progress_stop_reason
    if triggered:
        patch_meta["adaptive_progress_stop_note"] = note
    return triggered


def _apply_no_progress_token_cap_candidate(
    *,
    is_adaptive_condition: bool,
    adaptive_progress_made: bool,
    adaptive_no_progress_token_sum: int,
    attempt_tokens_total: int,
    adaptive_no_progress_token_cap: int,
    adaptive_progress_stop_candidate: bool,
    adaptive_progress_stop_reason: str,
) -> tuple[bool, str, int, bool]:
    next_sum = max(0, int(adaptive_no_progress_token_sum))
    if not is_adaptive_condition:
        return adaptive_progress_stop_candidate, adaptive_progress_stop_reason, next_sum, False
    if adaptive_progress_made:
        next_sum = 0
    else:
        next_sum += max(0, int(attempt_tokens_total))

    cap = max(0, int(adaptive_no_progress_token_cap))
    cap_hit = cap > 0 and next_sum >= cap
    if cap_hit:
        return True, "no_progress_token_cap", next_sum, True
    return adaptive_progress_stop_candidate, adaptive_progress_stop_reason, next_sum, False


def _adaptive_online_feature_vector(
    *,
    attempt: int,
    max_attempts: int,
    timed_out: bool,
    failure_signature_repeat: bool,
    stagnation_streak: int,
    helper_calls_used: int,
    helper_calls_without_context: int,
    adaptive_no_progress_streak: int,
    helper_budget_exhausted: bool,
    helper_saturation_guarded: bool,
    supports_context_request: bool,
    snapshot_available: bool,
    pregate_result: PreGateResult,
    effective_helper_budget_limit: int,
    adaptive_budget_unlimited: bool,
    adaptive_budget_topup_calls_used: int,
    adaptive_budget_topup_max_calls: int,
    last_helper_need_more_context: bool | None,
    last_helper_decision_reason: str,
    context_compaction_enabled: bool,
    supports_context_compaction: bool,
    context_compaction_calls_used: int,
    context_compaction_max_calls: int,
    last_context_compaction_requested: bool,
    last_context_compaction_invoked: bool,
    last_context_compaction_success: bool,
    last_context_compaction_ratio: float,
    last_context_compaction_trigger: str,
    case_difficulty: str,
    case_snapshot_dependency: str,
    case_bug_source: str | None,
) -> dict[str, float]:
    difficulty_ordinal = {
        "easy": 0.0,
        "medium": 0.5,
        "hard": 1.0,
    }.get(str(case_difficulty).strip().lower(), 0.5)
    snapshot_dependency_ordinal = {
        "none": 0.0,
        "helpful": 0.5,
        "required": 1.0,
    }.get(str(case_snapshot_dependency).strip().lower(), 0.5)
    bug_source_normalized = str(case_bug_source or "hand_crafted").strip().lower()
    pregate_exception_type_normalized = str(pregate_result.exception_type).strip().lower()
    max_attempts_safe = max(1, int(max_attempts))
    attempt_safe = max(1, int(attempt))
    helper_calls_used_safe = max(0, int(helper_calls_used))
    helper_budget_limit_safe = max(0, int(effective_helper_budget_limit))
    helper_budget_remaining = max(0, helper_budget_limit_safe - helper_calls_used_safe)
    helper_budget_remaining_frac = (
        1.0
        if adaptive_budget_unlimited
        else (
            helper_budget_remaining / float(helper_budget_limit_safe)
            if helper_budget_limit_safe > 0
            else 0.0
        )
    )
    topup_max_safe = max(0, int(adaptive_budget_topup_max_calls))
    topup_used_safe = max(0, int(adaptive_budget_topup_calls_used))
    helper_topup_used_frac = (
        min(1.0, topup_used_safe / float(topup_max_safe)) if topup_max_safe > 0 else 0.0
    )
    helper_topup_remaining_frac = (
        max(0.0, min(1.0, (topup_max_safe - topup_used_safe) / float(topup_max_safe)))
        if topup_max_safe > 0
        else 0.0
    )
    last_decision_reason = str(last_helper_decision_reason or "").strip()
    context_compaction_calls_used_safe = max(0, int(context_compaction_calls_used))
    context_compaction_max_calls_safe = max(0, int(context_compaction_max_calls))
    context_compaction_calls_used_frac = (
        min(1.0, context_compaction_calls_used_safe / float(context_compaction_max_calls_safe))
        if context_compaction_max_calls_safe > 0
        else 0.0
    )
    context_compaction_calls_remaining_frac = (
        max(
            0.0,
            min(
                1.0,
                (context_compaction_max_calls_safe - context_compaction_calls_used_safe)
                / float(context_compaction_max_calls_safe),
            ),
        )
        if context_compaction_max_calls_safe > 0
        else 0.0
    )
    last_compaction_trigger = str(last_context_compaction_trigger or "")
    low_yield_ratio = (
        (max(0, int(helper_calls_without_context)) / float(helper_calls_used_safe))
        if helper_calls_used_safe > 0
        else 0.0
    )
    return {
        "bias": 1.0,
        "attempt_frac": float(attempt_safe / float(max_attempts_safe)),
        "attempts_remaining_frac": float(
            max(0.0, (max_attempts_safe - attempt_safe) / float(max_attempts_safe))
        ),
        "timed_out": 1.0 if timed_out else 0.0,
        "failure_signature_repeat": 1.0 if failure_signature_repeat else 0.0,
        "stagnation_streak": float(max(0, int(stagnation_streak))),
        "helper_calls_used": float(helper_calls_used_safe),
        "helper_low_yield_ratio": float(low_yield_ratio),
        "helper_budget_remaining_frac": float(helper_budget_remaining_frac),
        "helper_topup_used_frac": float(helper_topup_used_frac),
        "helper_topup_remaining_frac": float(helper_topup_remaining_frac),
        "adaptive_no_progress_streak": float(max(0, int(adaptive_no_progress_streak))),
        "helper_budget_exhausted": 1.0 if helper_budget_exhausted else 0.0,
        "helper_saturation_guarded": 1.0 if helper_saturation_guarded else 0.0,
        "supports_context_request": 1.0 if supports_context_request else 0.0,
        "context_compaction_enabled": 1.0 if context_compaction_enabled else 0.0,
        "supports_context_compaction": 1.0 if supports_context_compaction else 0.0,
        "context_compaction_calls_used_frac": float(context_compaction_calls_used_frac),
        "context_compaction_calls_remaining_frac": float(context_compaction_calls_remaining_frac),
        "last_context_compaction_requested": 1.0 if last_context_compaction_requested else 0.0,
        "last_context_compaction_invoked": 1.0 if last_context_compaction_invoked else 0.0,
        "last_context_compaction_success": 1.0 if last_context_compaction_success else 0.0,
        "last_context_compaction_ratio": float(max(0.0, min(1.0, last_context_compaction_ratio))),
        "last_context_compaction_trigger_stage_a": 1.0
        if last_compaction_trigger == "stage_a_requested"
        else 0.0,
        "last_context_compaction_trigger_overflow": 1.0
        if last_compaction_trigger == "prompt_overflow"
        else 0.0,
        "last_context_compaction_trigger_always": 1.0
        if last_compaction_trigger == "mode_always"
        else 0.0,
        "snapshot_available": 1.0 if snapshot_available else 0.0,
        "last_helper_need_more_context": 1.0 if last_helper_need_more_context is True else 0.0,
        "last_helper_invalid_response": 1.0
        if last_decision_reason == "llm_invalid_response_no_helper"
        else 0.0,
        "last_helper_declined": 1.0 if last_decision_reason == "llm_declined_helper" else 0.0,
        "case_difficulty_ord": float(difficulty_ordinal),
        "case_snapshot_dependency_ord": float(snapshot_dependency_ordinal),
        "case_bug_source_imported": 0.0 if bug_source_normalized == "hand_crafted" else 1.0,
        "pregate_score": float(max(0.0, min(1.0, pregate_result.score))),
        "pregate_fired": 1.0 if pregate_result.should_skip else 0.0,
        "pregate_type_specificity": float(max(0.0, min(1.0, pregate_result.f_type_specificity))),
        "pregate_message_diagnostic": float(
            max(0.0, min(1.0, pregate_result.f_message_diagnostic))
        ),
        "pregate_crash_in_user_code": float(
            max(0.0, min(1.0, pregate_result.f_crash_in_user_code))
        ),
        "pregate_assertion_quality": float(max(0.0, min(1.0, pregate_result.f_assertion_quality))),
        "pregate_frame_depth": float(max(0.0, min(1.0, pregate_result.f_frame_depth))),
        "pregate_exception_is_assertion": 1.0
        if pregate_exception_type_normalized == "assertionerror"
        else 0.0,
        "pregate_exception_is_timeout": 1.0
        if pregate_exception_type_normalized == "timeouterror"
        else 0.0,
        "pregate_exception_is_syntax": 1.0
        if pregate_exception_type_normalized in {"syntaxerror", "indentationerror", "taberror"}
        else 0.0,
    }


def _adaptive_online_reward_components(
    *,
    success: bool,
    tokens_total: int,
    attempt_wall_sec: float,
    lambda_tokens: float,
    mu_latency: float,
    token_scale: float,
    latency_scale: float,
) -> tuple[float, float, float, float]:
    success_term = 1.0 if bool(success) else 0.0
    token_penalty = max(0.0, float(lambda_tokens)) * (
        max(0.0, float(tokens_total)) / max(1.0, float(token_scale))
    )
    latency_penalty = max(0.0, float(mu_latency)) * (
        max(0.0, float(attempt_wall_sec)) / max(1.0, float(latency_scale))
    )
    reward = success_term - token_penalty - latency_penalty
    return reward, success_term, token_penalty, latency_penalty


def _sync_adaptive_online_metadata(
    *,
    attempt_metadata: dict[str, Any],
    patch_meta: dict[str, Any],
    mode: str,
    algo: str,
    decision: PolicyDecision | None,
    eligible: bool,
    override_reason: str,
    feature_values: dict[str, float],
    reward_lambda_tokens: float,
    reward_mu_latency: float,
    reward_token_scale: float,
    reward_latency_scale: float,
    reward: float | None = None,
    reward_success: float = 0.0,
    reward_token_penalty: float = 0.0,
    reward_latency_penalty: float = 0.0,
    updated: bool = False,
    update_reason: str = "",
    event_logged: bool = False,
) -> None:
    selected_action = decision.selected_action if decision is not None else "none"
    applied_action = decision.applied_action if decision is not None else "none"
    decision_id = decision.decision_id if decision is not None else ""
    decision_strategy = decision.decision_strategy if decision is not None else ""
    propensity = float(decision.propensity) if decision is not None else 1.0
    fields = {
        "adaptive_online_policy_mode": str(mode),
        "adaptive_online_policy_algo": str(algo),
        "adaptive_online_decision_id": str(decision_id),
        "adaptive_online_decision_strategy": str(decision_strategy),
        "adaptive_online_action_selected": str(selected_action),
        "adaptive_online_action_applied": str(applied_action),
        "adaptive_online_propensity": float(propensity),
        "adaptive_online_override_reason": str(override_reason),
        "adaptive_online_feature_vector": dict(feature_values),
        "adaptive_online_eligible": bool(eligible),
        "adaptive_online_reward_lambda_tokens": float(reward_lambda_tokens),
        "adaptive_online_reward_mu_latency": float(reward_mu_latency),
        "adaptive_online_reward_token_scale": float(reward_token_scale),
        "adaptive_online_reward_latency_scale": float(reward_latency_scale),
        "adaptive_online_reward": reward,
        "adaptive_online_reward_success": float(reward_success),
        "adaptive_online_reward_token_penalty": float(reward_token_penalty),
        "adaptive_online_reward_latency_penalty": float(reward_latency_penalty),
        "adaptive_online_updated": bool(updated),
        "adaptive_online_update_reason": str(update_reason),
        "adaptive_online_event_logged": bool(event_logged),
    }
    attempt_metadata.update(fields)
    patch_meta.update(fields)


def _finalize_adaptive_online_attempt(
    *,
    note: str,
    success: bool,
    attempt_wall_sec: float,
    is_adaptive_llm_discretion: bool,
    online_policy_decision: PolicyDecision | None,
    adaptive_online_policy_controller: AdaptiveOnlinePolicyController | None,
    run_id: str,
    case_id: str,
    condition_value: str,
    attempt: int,
    attempt_metadata: dict[str, Any],
    patch_meta: dict[str, Any],
    online_policy_mode_for_attempt: str,
    online_policy_algo_for_attempt: str,
    online_policy_eligible: bool,
    online_policy_override_reason: str,
    online_policy_features: dict[str, float],
    helper_decision_reason: str,
    normalized_adaptive_online_policy_mode: str,
    reward_lambda_tokens: float,
    reward_mu_latency: float,
    reward_token_scale: float,
    reward_latency_scale: float,
    override_reason: str = "",
) -> None:
    effective_override_reason = (
        online_policy_override_reason
        if online_policy_override_reason
        else str(override_reason or "")
    )
    tokens_total_for_reward = _nonnegative_int(
        attempt_metadata.get("tokens_total", patch_meta.get("tokens_total"))
    )
    reward, reward_success_term, reward_token_penalty, reward_latency_penalty = (
        _adaptive_online_reward_components(
            success=success,
            tokens_total=tokens_total_for_reward,
            attempt_wall_sec=attempt_wall_sec,
            lambda_tokens=reward_lambda_tokens,
            mu_latency=reward_mu_latency,
            token_scale=reward_token_scale,
            latency_scale=reward_latency_scale,
        )
    )
    updated = False
    event_logged = False
    update_reason = "policy_off"
    if (
        is_adaptive_llm_discretion
        and online_policy_decision is not None
        and adaptive_online_policy_controller is not None
    ):
        try:
            policy_event = adaptive_online_policy_controller.record_outcome(
                decision=online_policy_decision,
                run_id=run_id,
                case_id=case_id,
                condition=condition_value,
                attempt=attempt,
                note=note,
                reward=reward,
                reward_success=reward_success_term,
                reward_token_penalty=reward_token_penalty,
                reward_latency_penalty=reward_latency_penalty,
                success=success,
                tokens_total=tokens_total_for_reward,
                attempt_wall_sec=attempt_wall_sec,
                override_reason=effective_override_reason,
            )
            updated = bool(policy_event.model_updated)
            event_logged = True
            update_reason = str(policy_event.update_reason)
            if not effective_override_reason:
                effective_override_reason = str(policy_event.override_reason)
        except Exception as exc:
            update_reason = f"event_logging_error:{type(exc).__name__}"
    elif is_adaptive_llm_discretion and online_policy_decision is None:
        if not online_policy_eligible:
            update_reason = f"not_eligible:{helper_decision_reason}"
        elif normalized_adaptive_online_policy_mode == "off":
            update_reason = "policy_off"
        else:
            update_reason = "no_decision_emitted"
    elif is_adaptive_llm_discretion:
        update_reason = "policy_controller_unavailable"
    else:
        update_reason = "non_llm_discretion_condition"

    _sync_adaptive_online_metadata(
        attempt_metadata=attempt_metadata,
        patch_meta=patch_meta,
        mode=online_policy_mode_for_attempt,
        algo=online_policy_algo_for_attempt,
        decision=online_policy_decision,
        eligible=online_policy_eligible,
        override_reason=effective_override_reason,
        feature_values=online_policy_features,
        reward_lambda_tokens=reward_lambda_tokens,
        reward_mu_latency=reward_mu_latency,
        reward_token_scale=reward_token_scale,
        reward_latency_scale=reward_latency_scale,
        reward=reward,
        reward_success=reward_success_term,
        reward_token_penalty=reward_token_penalty,
        reward_latency_penalty=reward_latency_penalty,
        updated=updated,
        update_reason=update_reason,
        event_logged=event_logged,
    )


# ---------------------------------------------------------------------------
# P1.1 — Rule-based traceback informativeness pre-gate
# ---------------------------------------------------------------------------

_EXCEPTION_TYPE_SPECIFICITY: dict[str, float] = {
    # Tier A: Always diagnostic (score >= 0.8)
    "SyntaxError": 1.0,
    "IndentationError": 1.0,
    "TabError": 1.0,
    "NameError": 0.9,
    "UnboundLocalError": 0.9,
    "ModuleNotFoundError": 0.9,
    "FileNotFoundError": 0.9,
    "ImportError": 0.85,
    "ZeroDivisionError": 0.8,
    # Tier B: Usually diagnostic (score 0.6-0.8)
    "KeyError": 0.7,
    "IndexError": 0.7,
    "AttributeError": 0.7,
    "StopIteration": 0.6,
    "OverflowError": 0.5,
    # Tier C: Context-dependent (score 0.3-0.5)
    "TypeError": 0.4,
    "RecursionError": 0.35,  # Need to see what's recursing
    "ValueError": 0.3,
    # Tier D: Low informativeness (score <= 0.1)
    "AssertionError": 0.1,
    "TimeoutError": 0.05,
    "RuntimeError": 0.05,
    "CancelledError": 0.05,
    "InvalidStateError": 0.05,
}
_EXCEPTION_TYPE_SPECIFICITY_DEFAULT = 0.2

_DIAGNOSTIC_MESSAGE_PATTERNS: list[re.Pattern[str]] = [
    re.compile(r"name\s+'[^']+'\s+is\s+not\s+defined", re.IGNORECASE),
    re.compile(r"No\s+module\s+named", re.IGNORECASE),
    re.compile(r"invalid\s+syntax", re.IGNORECASE),
    re.compile(r"unexpected\s+indent", re.IGNORECASE),
    re.compile(r"unexpected\s+EOF", re.IGNORECASE),
    re.compile(r"list\s+index\s+out\s+of\s+range", re.IGNORECASE),
    re.compile(r"has\s+no\s+attribute\s+'[^']+'", re.IGNORECASE),
    re.compile(r"division\s+by\s+zero", re.IGNORECASE),
    re.compile(r"not\s+subscriptable", re.IGNORECASE),
    re.compile(r"object\s+is\s+not\s+callable", re.IGNORECASE),
    re.compile(r"cannot\s+import\s+name", re.IGNORECASE),
    re.compile(r"No\s+such\s+file\s+or\s+directory", re.IGNORECASE),
]

_ASSERTION_VALUE_PATTERN = re.compile(
    r"(?:==|!=|>=|<=|>|<|assert|expected|got|actual)\s*\S", re.IGNORECASE
)

_STDOUT_PYTEST_ERROR_RE = re.compile(r"^E\s+(\w+(?:\.\w+)*):\s*(.*)", re.MULTILINE)
_STDOUT_TRACEBACK_ERROR_RE = re.compile(r"^(\w+(?:Error|Exception|Warning)):\s*(.*)", re.MULTILINE)


class PreGateResult(NamedTuple):
    should_skip: bool
    score: float
    exception_type: str
    reason: str
    f_type_specificity: float
    f_message_diagnostic: float
    f_crash_in_user_code: float
    f_assertion_quality: float
    f_frame_depth: float


def _extract_exception_info(
    *,
    snapshot: dict[str, Any] | None,
    stdout: str,
    stderr: str,
) -> tuple[str, str, int, bool]:
    """Extract (exception_type, message, frame_count, crash_in_user_code)."""
    if snapshot and isinstance(snapshot.get("exception"), dict):
        exc_info = snapshot["exception"]
        exc_type = str(exc_info.get("type", "Unknown"))
        exc_msg = str(exc_info.get("message", ""))
        frames = snapshot.get("frames")
        frame_count = len(frames) if isinstance(frames, list) else 0
        crash_in_user_code = False
        if isinstance(frames, list) and len(frames) > 0:
            crash_frame = frames[0]
            if isinstance(crash_frame, dict):
                crash_in_user_code = crash_frame.get("file_rel") is not None
        # Strip module prefix (e.g., "builtins.ValueError" -> "ValueError")
        if "." in exc_type:
            exc_type = exc_type.rsplit(".", 1)[-1]
        return exc_type, exc_msg, frame_count, crash_in_user_code

    # Fallback: parse from stdout/stderr
    for text in (stdout, stderr):
        if not text:
            continue
        m = _STDOUT_PYTEST_ERROR_RE.search(text)
        if m:
            exc_type = m.group(1).rsplit(".", 1)[-1]
            return exc_type, m.group(2).strip(), 0, False
        m = _STDOUT_TRACEBACK_ERROR_RE.search(text)
        if m:
            exc_type = m.group(1).rsplit(".", 1)[-1]
            return exc_type, m.group(2).strip(), 0, False

    return "Unknown", "", 0, False


def _traceback_informativeness_score(
    *,
    exception_type: str,
    exception_message: str,
    frame_count: int,
    crash_in_user_code: bool,
) -> PreGateResult:
    """Score traceback informativeness and return a PreGateResult with all features."""
    # Signal 1: Exception type specificity (weight 0.30)
    f_type_specificity = _EXCEPTION_TYPE_SPECIFICITY.get(
        exception_type, _EXCEPTION_TYPE_SPECIFICITY_DEFAULT
    )

    # Signal 2: Message contains diagnostic values (weight 0.25)
    f_message_diagnostic = 0.0
    if exception_message:
        for pattern in _DIAGNOSTIC_MESSAGE_PATTERNS:
            if pattern.search(exception_message):
                f_message_diagnostic = 1.0
                break
        if f_message_diagnostic == 0.0:
            f_message_diagnostic = 0.3  # Non-empty but generic
    # else: empty message → 0.0

    # Signal 3: Crash frame in user code (weight 0.15)
    f_crash_in_user_code = 1.0 if crash_in_user_code else 0.0

    # Signal 4: Assertion message quality (weight 0.15)
    if exception_type == "AssertionError":
        if exception_message and _ASSERTION_VALUE_PATTERN.search(exception_message):
            f_assertion_quality = 1.0
        elif exception_message:
            f_assertion_quality = 0.5
        else:
            f_assertion_quality = 0.0
    else:
        f_assertion_quality = 0.5  # Neutral for non-assertions

    # Signal 5: Frame count / depth (weight 0.15)
    # frame_count=0 means no structured data (stdout fallback) — treat as neutral.
    if frame_count == 0:
        f_frame_depth = 0.5
    elif frame_count <= 3:
        f_frame_depth = 1.0
    elif frame_count <= 6:
        f_frame_depth = 0.7
    elif frame_count <= 10:
        f_frame_depth = 0.4
    else:
        f_frame_depth = 0.2

    score = (
        0.30 * f_type_specificity
        + 0.25 * f_message_diagnostic
        + 0.15 * f_crash_in_user_code
        + 0.15 * f_assertion_quality
        + 0.15 * f_frame_depth
    )
    score = min(1.0, max(0.0, score))

    tier = (
        "A"
        if f_type_specificity >= 0.8
        else ("B" if f_type_specificity >= 0.5 else ("C" if f_type_specificity >= 0.3 else "D"))
    )
    reason = f"{exception_type}_tier{tier}"

    return PreGateResult(
        should_skip=False,  # Caller decides based on threshold
        score=score,
        exception_type=exception_type,
        reason=reason,
        f_type_specificity=f_type_specificity,
        f_message_diagnostic=f_message_diagnostic,
        f_crash_in_user_code=f_crash_in_user_code,
        f_assertion_quality=f_assertion_quality,
        f_frame_depth=f_frame_depth,
    )


def _traceback_informativeness_pre_gate(
    *,
    snapshot: dict[str, Any] | None,
    stdout: str,
    stderr: str,
    threshold: float,
) -> PreGateResult:
    """Return a PreGateResult with should_skip set based on threshold."""
    exc_type, exc_msg, frame_count, crash_in_user = _extract_exception_info(
        snapshot=snapshot,
        stdout=stdout,
        stderr=stderr,
    )
    result = _traceback_informativeness_score(
        exception_type=exc_type,
        exception_message=exc_msg,
        frame_count=frame_count,
        crash_in_user_code=crash_in_user,
    )
    return result._replace(should_skip=result.score >= threshold)


def _adaptive_prompt_budget_for_attempt(
    *,
    base_budget: dict[str, int],
    helper_invoked: bool,
    profile: str,
) -> dict[str, int]:
    out = dict(base_budget)
    if not helper_invoked or profile != "compact":
        return out

    def _cap(value: int, limit: int) -> int:
        if value <= 0:
            return value
        return min(value, limit)

    out["total_chars"] = _cap(int(out.get("total_chars", 0)), 120_000)
    out["retry_delta_chars"] = _cap(int(out.get("retry_delta_chars", 0)), 12_000)
    out["stdout_chars"] = _cap(int(out.get("stdout_chars", 0)), 16_000)
    out["snapshot_chars"] = _cap(int(out.get("snapshot_chars", 0)), 20_000)
    out["evidence_chars"] = _cap(int(out.get("evidence_chars", 0)), 32_000)
    out["retry_history_chars"] = _cap(int(out.get("retry_history_chars", 0)), 8_000)
    return out


def _openai_preflight_total_char_cap(
    *,
    context_window_tokens: int,
    max_tokens: int,
    reserve_tokens: int = OPENAI_CONTEXT_WINDOW_RESERVE_TOKENS,
) -> int:
    normalized_window = max(0, int(context_window_tokens))
    if normalized_window <= 0:
        return 0
    usable_tokens = normalized_window - max(0, int(max_tokens)) - max(0, int(reserve_tokens))
    # Guard enabled: never return 0, otherwise budget logic treats it as "uncapped".
    if usable_tokens <= 0:
        return 1
    return max(1, usable_tokens * OPENAI_CONTEXT_WINDOW_CHARS_PER_TOKEN)


def _is_snapshot_condition(condition: Condition) -> bool:
    return condition in {
        Condition.WITH_SNAPSHOT,
        Condition.WITH_SNAPSHOT_STRUCTURED,
        Condition.ADAPTIVE_GATED,
        Condition.ADAPTIVE_LLM_DISCRETION,
    }


def run_one_condition(
    *,
    case: CaseSpec,
    condition: Condition,
    workdir: Path,
    context_dir_base: Path,
    patcher,
    k: int,
    output_format: str,
    run_id: str,
    include_files_context: bool,
    max_context_chars: int = 200_000,
    force_patch_attempt: bool,
    context_protocol: str = "monolithic",
    context_stage_a_max_requests: int = 6,
    context_stage_a_max_chars: int = 120_000,
    retry_prompt_mode: str = "auto",
    retry_context_mode: str = "history",
    retry_history_max_chars: int = 48_000,
    pytest_output_tail_lines: int = DEFAULT_PYTEST_OUTPUT_TAIL_LINES,
    snapshot_artifact_max_string_chars: int = DEFAULT_SNAPSHOT_ARTIFACT_MAX_STRING_CHARS,
    prompt_budget_total_chars: int = 180_000,
    prompt_budget_retry_delta_chars: int = 16_000,
    prompt_budget_stdout_chars: int = 32_000,
    prompt_budget_snapshot_chars: int = 32_000,
    prompt_budget_evidence_chars: int = 64_000,
    prompt_budget_retry_history_chars: int = 12_000,
    context_compaction_mode: str = "auto_overflow",
    context_compaction_scope: str = "snapshot,pytest,files",
    context_compaction_max_input_chars: int = 120_000,
    context_compaction_target_chars: int = 18_000,
    context_compaction_max_calls: int = 1,
    openai_context_window_tokens: int = 0,
    openai_context_window_source: str = "",
    openai_context_window_probe_provider: str = "",
    openai_context_window_probe_error: str = "",
    openai_response_mode_autoswitch: str = "off",
    adaptive_gate_policy: str = "balanced",
    adaptive_max_helper_calls: int = 1,
    adaptive_budget_topup_max_calls: int = DEFAULT_ADAPTIVE_BUDGET_TOPUP_MAX_CALLS,
    adaptive_stagnation_window: int = 1,
    adaptive_saturation_threshold: float = 0.75,
    adaptive_no_progress_token_cap: int = DEFAULT_ADAPTIVE_NO_PROGRESS_TOKEN_CAP,
    adaptive_helper_budget_profile: str = "default",
    adaptive_openai_helper_response_mode: str = "inherit",
    adaptive_informativeness_pregate: bool = False,
    adaptive_informativeness_pregate_threshold: float = 0.7,
    syntax_preflight_enabled: bool = True,
    syntax_preflight_max_cycles: int = DEFAULT_SYNTAX_PREFLIGHT_MAX_CYCLES,
    patch_sanitize_enabled: bool = True,
    patch_verifier_infer_test_source_scope: bool = True,
    adaptive_online_policy_controller: AdaptiveOnlinePolicyController | None = None,
    adaptive_online_policy_mode: str = "off",
    adaptive_online_policy_algo: str = "linucb",
    adaptive_reward_lambda_tokens: float = ADAPTIVE_REWARD_LAMBDA_TOKENS_DEFAULT,
    adaptive_reward_mu_latency: float = ADAPTIVE_REWARD_MU_LATENCY_DEFAULT,
    adaptive_reward_token_scale: float = ADAPTIVE_REWARD_TOKEN_SCALE_DEFAULT,
    adaptive_reward_latency_scale: float = ADAPTIVE_REWARD_LATENCY_SCALE_DEFAULT,
    run_metadata: dict[str, Any] | None = None,
    record_prompt: str = "hash",
    record_evidence: str = "hash",
    deterministic: bool = False,
    seed: int | None = None,
    record_sink: Callable[[dict[str, Any]], None] | None = None,
    progress_update: Callable[[str, dict[str, Any] | None], None] | None = None,
) -> list[dict[str, Any]]:
    records: list[dict[str, Any]] = []

    def _emit_progress(event: str, payload: dict[str, Any] | None = None) -> None:
        if progress_update is None:
            return
        try:
            progress_update(event, payload)
        except Exception:
            # Progress callbacks are best-effort and must not break eval execution.
            return

    def _append_record(record: dict[str, Any]) -> None:
        records.append(record)
        _emit_progress(
            "record_emitted",
            {
                "attempt": int(record.get("attempt") or 0),
                "note": str(record.get("note") or ""),
                "success": bool(record.get("success")),
            },
        )
        if record_sink is not None:
            record_sink(record)

    normalized_context_protocol = (
        context_protocol if context_protocol in CONTEXT_PROTOCOLS else "monolithic"
    )
    is_structured_condition = condition == Condition.WITH_SNAPSHOT_STRUCTURED
    is_adaptive_gated = condition == Condition.ADAPTIVE_GATED
    is_adaptive_llm_discretion = condition == Condition.ADAPTIVE_LLM_DISCRETION
    is_adaptive_condition = is_adaptive_gated or is_adaptive_llm_discretion
    default_context_protocol = (
        "staged"
        if is_structured_condition
        else ("monolithic" if is_adaptive_condition else normalized_context_protocol)
    )
    normalized_retry_prompt_mode = (
        retry_prompt_mode if retry_prompt_mode in RETRY_PROMPT_MODES else "auto"
    )
    if normalized_retry_prompt_mode == "auto":
        base_retry_prompt_mode = "delta" if is_structured_condition else "full"
    else:
        base_retry_prompt_mode = normalized_retry_prompt_mode
    normalized_retry_context_mode = (
        retry_context_mode if retry_context_mode in RETRY_CONTEXT_MODES else "history"
    )
    effective_retry_context_mode = normalized_retry_context_mode
    effective_retry_history_max_chars = max(0, int(retry_history_max_chars))
    normalized_pytest_output_tail_lines = max(0, int(pytest_output_tail_lines))
    normalized_snapshot_artifact_max_string_chars = max(0, int(snapshot_artifact_max_string_chars))
    stage_a_max_requests = max(1, int(context_stage_a_max_requests))
    stage_a_max_chars = max(1, int(context_stage_a_max_chars))
    prompt_budget = {
        "total_chars": max(0, int(prompt_budget_total_chars)),
        "retry_delta_chars": max(0, int(prompt_budget_retry_delta_chars)),
        "stdout_chars": max(0, int(prompt_budget_stdout_chars)),
        "snapshot_chars": max(0, int(prompt_budget_snapshot_chars)),
        "evidence_chars": max(0, int(prompt_budget_evidence_chars)),
        "retry_history_chars": max(0, int(prompt_budget_retry_history_chars)),
    }
    normalized_context_compaction_mode = (
        context_compaction_mode
        if context_compaction_mode in CONTEXT_COMPACTION_MODES
        else "auto_overflow"
    )
    normalized_context_compaction_scope = {
        token.strip().lower()
        for token in str(context_compaction_scope or "").split(",")
        if token.strip() and token.strip().lower() in CONTEXT_COMPACTION_SCOPE_TOKENS
    }
    if not normalized_context_compaction_scope:
        normalized_context_compaction_scope = {"snapshot", "pytest", "files"}
    normalized_context_compaction_max_input_chars = max(0, int(context_compaction_max_input_chars))
    normalized_context_compaction_target_chars = max(1, int(context_compaction_target_chars))
    normalized_context_compaction_max_calls = max(0, int(context_compaction_max_calls))
    normalized_openai_context_window_tokens = max(0, int(openai_context_window_tokens))
    normalized_openai_context_window_source = str(openai_context_window_source or "")
    normalized_openai_context_window_probe_provider = str(
        openai_context_window_probe_provider or ""
    )
    normalized_openai_context_window_probe_error = str(openai_context_window_probe_error or "")
    normalized_openai_response_mode_autoswitch = (
        openai_response_mode_autoswitch
        if openai_response_mode_autoswitch in OPENAI_RESPONSE_MODE_AUTOSWITCH_MODES
        else "off"
    )
    normalized_adaptive_gate_policy = (
        adaptive_gate_policy if adaptive_gate_policy in ADAPTIVE_GATE_POLICIES else "balanced"
    )
    normalized_adaptive_max_helper_calls = max(0, int(adaptive_max_helper_calls))
    adaptive_budget_unlimited = normalized_adaptive_max_helper_calls == 0
    normalized_adaptive_budget_topup_max_calls = max(0, int(adaptive_budget_topup_max_calls))
    normalized_adaptive_stagnation_window = max(1, int(adaptive_stagnation_window))
    adaptive_no_progress_stop_window = max(2, normalized_adaptive_stagnation_window + 1)
    normalized_adaptive_saturation_threshold = min(
        1.0, max(0.0, float(adaptive_saturation_threshold))
    )
    normalized_adaptive_no_progress_token_cap = max(0, int(adaptive_no_progress_token_cap))
    normalized_adaptive_helper_budget_profile = (
        adaptive_helper_budget_profile
        if adaptive_helper_budget_profile in ADAPTIVE_HELPER_BUDGET_PROFILES
        else "default"
    )
    normalized_adaptive_openai_helper_response_mode = (
        adaptive_openai_helper_response_mode
        if adaptive_openai_helper_response_mode in ADAPTIVE_OPENAI_HELPER_RESPONSE_MODES
        else "inherit"
    )
    adaptive_mode_label = (
        "gated"
        if is_adaptive_gated
        else ("llm_discretion" if is_adaptive_llm_discretion else "none")
    )
    adaptive_policy_label = normalized_adaptive_gate_policy if is_adaptive_condition else "disabled"
    normalized_adaptive_online_policy_mode = (
        adaptive_online_policy_mode
        if adaptive_online_policy_mode in ADAPTIVE_ONLINE_POLICY_MODES
        else "off"
    )
    normalized_adaptive_online_policy_algo = (
        adaptive_online_policy_algo
        if adaptive_online_policy_algo in ADAPTIVE_ONLINE_POLICY_ALGOS
        else "linucb"
    )
    normalized_syntax_preflight_enabled = bool(syntax_preflight_enabled)
    normalized_syntax_preflight_max_cycles = max(1, int(syntax_preflight_max_cycles))
    normalized_patch_sanitize_enabled = bool(patch_sanitize_enabled)
    normalized_patch_verifier_infer_test_source_scope = bool(patch_verifier_infer_test_source_scope)
    if adaptive_online_policy_controller is not None:
        normalized_adaptive_online_policy_mode = adaptive_online_policy_controller.mode
        normalized_adaptive_online_policy_algo = adaptive_online_policy_controller.algorithm
    normalized_adaptive_reward_lambda_tokens = max(0.0, float(adaptive_reward_lambda_tokens))
    normalized_adaptive_reward_mu_latency = max(0.0, float(adaptive_reward_mu_latency))
    normalized_adaptive_reward_token_scale = max(1.0, float(adaptive_reward_token_scale))
    normalized_adaptive_reward_latency_scale = max(1.0, float(adaptive_reward_latency_scale))
    effective_run_metadata = (
        dict(run_metadata)
        if isinstance(run_metadata, dict)
        else _default_run_metadata(
            run_id=run_id,
            patcher_name=str(getattr(patcher, "name", "unknown")),
        )
    )
    patcher_name = str(getattr(patcher, "name", "unknown"))
    is_openai_patcher = patcher_name == "openai"
    context_compaction_fn = getattr(patcher, "compact_context", None)
    supports_context_compaction = callable(context_compaction_fn)
    context_compaction_calls_used = 0
    last_context_compaction_requested = False
    last_context_compaction_invoked = False
    last_context_compaction_success = False
    last_context_compaction_ratio = 0.0
    last_context_compaction_trigger = ""

    llmdebug_dir = workdir / ".llmdebug"
    previous_failure_context: dict[str, Any] | None = None
    seen_evidence_ids: set[str] = set()
    retry_history_turns: list[str] = []
    runner_force_patch_attempt_enabled = bool(force_patch_attempt)
    helper_calls_used = 0
    helper_calls_without_context = 0
    adaptive_budget_topup_calls_used = 0
    last_helper_need_more_context: bool | None = None
    last_helper_decision_reason = ""
    stagnation_streak = 0
    adaptive_no_progress_streak = 0
    adaptive_no_progress_token_sum = 0
    file_index_cache: str | None = None
    file_index_lines_cache: tuple[str, ...] | None = None
    monolithic_context_cache: str | None = None

    def _file_index_lines_for_attempt() -> tuple[str, ...]:
        nonlocal file_index_lines_cache, file_index_cache
        if file_index_lines_cache is None:
            file_index_lines_cache = tuple(_collect_file_index_lines(workdir))
            file_index_cache = "\n".join(file_index_lines_cache)
        return file_index_lines_cache

    def _file_index_text_for_attempt() -> str:
        nonlocal file_index_cache
        if file_index_cache is None:
            file_index_cache = "\n".join(_file_index_lines_for_attempt())
        return file_index_cache

    def _monolithic_context_for_attempt() -> str:
        nonlocal monolithic_context_cache
        if monolithic_context_cache is None:
            monolithic_context_cache = collect_text_context(
                workdir,
                max_total_chars=max_context_chars,
                file_index_lines=_file_index_lines_for_attempt(),
            )
        return monolithic_context_cache

    def _invalidate_file_catalog_cache() -> None:
        nonlocal file_index_cache, file_index_lines_cache, monolithic_context_cache
        file_index_cache = None
        file_index_lines_cache = None
        monolithic_context_cache = None

    # Initial run (attempt 0, no patch yet).
    shutil.rmtree(llmdebug_dir, ignore_errors=True)
    _emit_progress(
        "pytest_before_initial",
        {
            "attempt": 0,
            "max_attempts": int(k),
        },
    )
    current = run_pytest(workdir, case.python_args, condition, case.timeout_sec, output_format)
    cumulative_runtime_sec = current.duration_sec
    if current.returncode == 0:
        attempt_dir = context_dir_base / "attempt_00"
        attempt_dir.mkdir(parents=True, exist_ok=True)
        _write_run_outputs(
            attempt_dir,
            current,
            prefix="pytest_before",
            tail_lines=normalized_pytest_output_tail_lines,
        )
        record = _build_base_record(
            case=case,
            condition=condition,
            run_id=run_id,
            attempt=0,
            patcher_name=patcher_name,
            run_metadata=effective_run_metadata,
            attempt_metadata={
                **_empty_attempt_metadata(attempt=0),
                "max_attempts": int(k),
                "attempt_ordinal_label": f"0/{int(k)}",
                "context_protocol": default_context_protocol,
                "retry_prompt_mode": base_retry_prompt_mode,
                "retry_context_mode": effective_retry_context_mode,
                "retry_history_budget_chars": int(prompt_budget.get("retry_history_chars", 0)),
                "openai_context_window_source": normalized_openai_context_window_source,
                "openai_context_window_probe_provider": normalized_openai_context_window_probe_provider,
                "openai_context_window_probe_error": normalized_openai_context_window_probe_error,
                "openai_response_mode_autoswitch_policy": normalized_openai_response_mode_autoswitch,
                "adaptive_mode": adaptive_mode_label,
                "adaptive_policy": adaptive_policy_label,
                "adaptive_helper_budget_max_calls": normalized_adaptive_max_helper_calls,
                "adaptive_helper_budget_unlimited": adaptive_budget_unlimited,
                "adaptive_helper_budget_profile": normalized_adaptive_helper_budget_profile,
                "adaptive_budget_topup_granted": False,
                "adaptive_budget_topup_reason": "",
                "adaptive_online_policy_mode": normalized_adaptive_online_policy_mode,
                "adaptive_online_policy_algo": normalized_adaptive_online_policy_algo,
                "adaptive_online_reward_lambda_tokens": normalized_adaptive_reward_lambda_tokens,
                "adaptive_online_reward_mu_latency": normalized_adaptive_reward_mu_latency,
                "adaptive_online_reward_token_scale": normalized_adaptive_reward_token_scale,
                "adaptive_online_reward_latency_scale": normalized_adaptive_reward_latency_scale,
                "syntax_preflight_enabled": normalized_syntax_preflight_enabled,
                "syntax_preflight_cycles_used": 0,
                "syntax_preflight_failures": 0,
                "syntax_preflight_last_error_type": "",
                "syntax_preflight_last_error_msg": "",
            },
            model_metadata=_build_model_metadata(patcher=patcher, patch_meta=None),
            context_protocol=default_context_protocol,
        )
        record.update(
            {
                "success": True,
                "before_returncode": current.returncode,
                "before_duration_sec": current.duration_sec,
                "cumulative_runtime_sec": cumulative_runtime_sec,
                "attempt_wall_clock_sec": float(current.duration_sec),
                "note": "no_patch_needed",
            }
        )
        record.update(_timeout_fields(current))
        _append_record(record)
        return records

    for attempt in range(1, k + 1):
        attempt_start = time.perf_counter()
        _emit_progress(
            "attempt_start",
            {
                "attempt": int(attempt),
                "max_attempts": int(k),
            },
        )
        attempt_dir = context_dir_base / f"attempt_{attempt:02d}"
        attempt_dir.mkdir(parents=True, exist_ok=True)

        _write_run_outputs(
            attempt_dir,
            current,
            prefix="pytest_before",
            tail_lines=normalized_pytest_output_tail_lines,
        )

        snapshot = None
        snapshot_error = None
        snapshot_full_text: str | None = None
        snapshot_artifact_text: str | None = None
        if _is_snapshot_condition(condition):
            snapshot, snapshot_error = read_latest_snapshot(llmdebug_dir)
            if snapshot is not None:
                snapshot_full_text = json.dumps(snapshot, indent=2, ensure_ascii=False)
                snapshot_artifact = _truncate_snapshot_for_artifact(
                    snapshot,
                    max_string_chars=normalized_snapshot_artifact_max_string_chars,
                )
                snapshot_artifact_text = json.dumps(snapshot_artifact, indent=2, ensure_ascii=False)
                (attempt_dir / "snapshot.json").write_text(snapshot_artifact_text, encoding="utf-8")

        failure_signature = _build_failure_signature_for_attempt(run=current, snapshot=snapshot)
        previous_failure_signature = (
            previous_failure_context.get("failure_signature")
            if isinstance(previous_failure_context, dict)
            else None
        )
        failure_signature_repeat = bool(
            previous_failure_signature and previous_failure_signature == failure_signature
        )
        stagnation_streak = (stagnation_streak + 1) if failure_signature_repeat else 0
        effective_helper_budget_limit = (
            normalized_adaptive_max_helper_calls + adaptive_budget_topup_calls_used
        )
        helper_budget_exhausted = bool(
            is_adaptive_condition
            and (not adaptive_budget_unlimited)
            and helper_calls_used >= effective_helper_budget_limit
        )
        helper_saturation_guarded = False
        if (
            is_adaptive_condition
            and helper_calls_used >= max(2, normalized_adaptive_stagnation_window)
            and helper_calls_used > 0
        ):
            low_yield_ratio = helper_calls_without_context / float(helper_calls_used)
            helper_saturation_guarded = low_yield_ratio >= normalized_adaptive_saturation_threshold

        helper_decision_mode = adaptive_mode_label
        helper_invoked = False
        helper_need_more_context: bool | None = None
        helper_decision_reason = "not_adaptive_condition"
        adaptive_context_requested = False
        adaptive_snapshot_included = False
        adaptive_snapshot_attempt1_denied = False
        adaptive_snapshot_attempt1_denied_reason = ""
        adaptive_budget_topup_granted = False
        adaptive_budget_topup_reason = ""
        request_context_fn = getattr(patcher, "request_context", None)
        supports_context_request = callable(request_context_fn)
        online_policy_decision: PolicyDecision | None = None
        online_policy_features: dict[str, float] = {}
        online_policy_eligible = False
        online_policy_override_reason = ""
        online_policy_mode_for_attempt = (
            normalized_adaptive_online_policy_mode if is_adaptive_llm_discretion else "off"
        )
        online_policy_algo_for_attempt = (
            normalized_adaptive_online_policy_algo if is_adaptive_llm_discretion else ""
        )
        adaptive_stagnation_override_applied = False
        adaptive_stagnation_override_reason = ""
        adaptive_force_helper_stage_a = False

        # --- Pre-gate: traceback informativeness ---
        pregate_result = PreGateResult(
            should_skip=False,
            score=0.0,
            exception_type="",
            reason="",
            f_type_specificity=0.0,
            f_message_diagnostic=0.0,
            f_crash_in_user_code=0.0,
            f_assertion_quality=0.0,
            f_frame_depth=0.0,
        )
        if is_adaptive_condition and adaptive_informativeness_pregate:
            pregate_result = _traceback_informativeness_pre_gate(
                snapshot=snapshot,
                stdout=current.stdout,
                stderr=current.stderr,
                threshold=adaptive_informativeness_pregate_threshold,
            )
        if (
            is_adaptive_condition
            and (not adaptive_budget_unlimited)
            and helper_budget_exhausted
            and normalized_adaptive_budget_topup_max_calls > 0
            and adaptive_budget_topup_calls_used < normalized_adaptive_budget_topup_max_calls
            and last_helper_need_more_context is True
            and failure_signature_repeat
            and adaptive_no_progress_streak > 0
            and (not helper_saturation_guarded)
            and (not pregate_result.should_skip)
        ):
            adaptive_budget_topup_calls_used += 1
            adaptive_budget_topup_granted = True
            adaptive_budget_topup_reason = "need_more_context_recovery_once"
            helper_budget_exhausted = False

        if is_adaptive_llm_discretion:
            online_policy_features = _adaptive_online_feature_vector(
                attempt=attempt,
                max_attempts=k,
                timed_out=bool(current.timed_out),
                failure_signature_repeat=failure_signature_repeat,
                stagnation_streak=stagnation_streak,
                helper_calls_used=helper_calls_used,
                helper_calls_without_context=helper_calls_without_context,
                adaptive_no_progress_streak=adaptive_no_progress_streak,
                helper_budget_exhausted=helper_budget_exhausted,
                helper_saturation_guarded=helper_saturation_guarded,
                supports_context_request=supports_context_request,
                snapshot_available=snapshot is not None,
                pregate_result=pregate_result,
                effective_helper_budget_limit=effective_helper_budget_limit,
                adaptive_budget_unlimited=adaptive_budget_unlimited,
                adaptive_budget_topup_calls_used=adaptive_budget_topup_calls_used,
                adaptive_budget_topup_max_calls=normalized_adaptive_budget_topup_max_calls,
                last_helper_need_more_context=last_helper_need_more_context,
                last_helper_decision_reason=last_helper_decision_reason,
                context_compaction_enabled=normalized_context_compaction_mode != "off",
                supports_context_compaction=supports_context_compaction,
                context_compaction_calls_used=context_compaction_calls_used,
                context_compaction_max_calls=normalized_context_compaction_max_calls,
                last_context_compaction_requested=last_context_compaction_requested,
                last_context_compaction_invoked=last_context_compaction_invoked,
                last_context_compaction_success=last_context_compaction_success,
                last_context_compaction_ratio=last_context_compaction_ratio,
                last_context_compaction_trigger=last_context_compaction_trigger,
                case_difficulty=case.difficulty,
                case_snapshot_dependency=case.snapshot_dependency,
                case_bug_source=case.bug_source,
            )

        if is_adaptive_condition:
            if pregate_result.should_skip:
                helper_decision_reason = "informativeness_pregate_skip"
                online_policy_override_reason = "constraint_informativeness_pregate_skip"
            elif helper_budget_exhausted:
                helper_decision_reason = "helper_budget_exhausted"
                online_policy_override_reason = "constraint_helper_budget_exhausted"
            elif helper_saturation_guarded:
                helper_decision_reason = "helper_saturation_guard"
                online_policy_override_reason = "constraint_helper_saturation_guard"
            elif is_adaptive_gated:
                helper_invoked, helper_decision_reason = _adaptive_gate_should_invoke_helper(
                    policy=normalized_adaptive_gate_policy,
                    attempt=attempt,
                    timed_out=bool(current.timed_out),
                    signature_repeated=failure_signature_repeat,
                    stagnation_streak=stagnation_streak,
                    stagnation_window=normalized_adaptive_stagnation_window,
                )
            else:
                helper_decision_reason = "llm_discretion_pending"
                if not supports_context_request:
                    helper_decision_reason = "request_context_unavailable"
                    online_policy_override_reason = "constraint_request_context_unavailable"
                else:
                    online_policy_eligible = True
                    if (
                        adaptive_online_policy_controller is not None
                        and normalized_adaptive_online_policy_mode in {"shadow", "on"}
                    ):
                        online_policy_decision = adaptive_online_policy_controller.decide(
                            feature_values=online_policy_features,
                            default_action=ACTION_INVOKE_HELPER,
                        )
                        online_policy_mode_for_attempt = online_policy_decision.mode
                        online_policy_algo_for_attempt = online_policy_decision.algorithm
                        if (
                            online_policy_decision.applied_action
                            != online_policy_decision.selected_action
                        ):
                            online_policy_override_reason = "shadow_mode_baseline"
                    elif normalized_adaptive_online_policy_mode == "off":
                        online_policy_override_reason = "policy_off"
                    else:
                        online_policy_override_reason = "policy_controller_unavailable"
            if adaptive_budget_topup_granted and not online_policy_override_reason:
                online_policy_override_reason = "constraint_helper_budget_topup"

            previous_note_for_override = (
                str(previous_failure_context.get("note") or "")
                if isinstance(previous_failure_context, dict)
                else ""
            )
            previous_stagnation_signal = previous_note_for_override in {
                "no_patch_proposed",
                "patcher_request_failed",
                "forced_noop_patch",
                "patch_verifier_rejected",
                "syntax_preflight_failed",
                "patch_apply_failed",
                "tests_still_failing",
                "retry_gate_no_new_evidence",
                "hard_stop_no_progress",
            }
            policy_skip_signal = bool(
                online_policy_decision is not None
                and online_policy_decision.applied_action == ACTION_SKIP_HELPER
            )
            historical_skip_signal = last_helper_decision_reason in {
                "online_policy_skip_helper",
                "llm_declined_helper",
                "llm_invalid_response_no_helper",
                "llm_snapshot_request_denied_attempt1",
            }
            if (
                is_adaptive_llm_discretion
                and (not helper_invoked)
                and failure_signature_repeat
                and stagnation_streak > 0
                and previous_stagnation_signal
                and (policy_skip_signal or historical_skip_signal)
                and (not pregate_result.should_skip)
                and supports_context_request
            ):
                adaptive_stagnation_override_reason = "repeated_no_progress_after_stagnation_note"
                if (
                    helper_budget_exhausted
                    and (not adaptive_budget_unlimited)
                    and normalized_adaptive_budget_topup_max_calls > 0
                    and adaptive_budget_topup_calls_used
                    < normalized_adaptive_budget_topup_max_calls
                ):
                    adaptive_budget_topup_calls_used += 1
                    adaptive_budget_topup_granted = True
                    adaptive_budget_topup_reason = "stagnation_override_recovery_once"
                    helper_budget_exhausted = False
                if not helper_budget_exhausted and not helper_saturation_guarded:
                    adaptive_stagnation_override_applied = True
                    adaptive_force_helper_stage_a = True
                    helper_invoked = True
                    helper_decision_reason = "stagnation_override_force_helper"
                    online_policy_override_reason = "constraint_stagnation_override_force_helper"

        current_failure_context: dict[str, Any] = {
            "attempt": attempt,
            "failure_signature": failure_signature,
            "stdout": current.stdout,
            "stderr": current.stderr,
            "snapshot": snapshot,
        }
        previous_attempt_diff = _build_structured_previous_attempt_diff(
            previous_failure_context,
            current_failure_context=current_failure_context,
        )
        previous_note = (
            str(previous_failure_context.get("note") or "")
            if isinstance(previous_failure_context, dict)
            else ""
        )
        previous_patch_verifier_rejected = bool(
            isinstance(previous_failure_context, dict)
            and previous_failure_context.get("patch_verifier_rejected")
        )
        previous_patch_verifier_reasons_raw = (
            previous_failure_context.get("patch_verifier_reasons")
            if isinstance(previous_failure_context, dict)
            else []
        )
        previous_patch_verifier_reasons = (
            [str(item) for item in previous_patch_verifier_reasons_raw]
            if isinstance(previous_patch_verifier_reasons_raw, list)
            else []
        )
        previous_patch_verifier_scope = (
            str(previous_failure_context.get("patch_verifier_scope") or "")
            if isinstance(previous_failure_context, dict)
            else ""
        )
        previous_timed_out = bool(
            isinstance(previous_failure_context, dict) and previous_failure_context.get("timed_out")
        )
        previous_timeout_sec_raw = (
            previous_failure_context.get("timeout_sec")
            if isinstance(previous_failure_context, dict)
            else None
        )
        previous_timeout_sec = (
            int(previous_timeout_sec_raw) if isinstance(previous_timeout_sec_raw, int) else None
        )

        file_index_lines = _file_index_lines_for_attempt()
        file_index = _file_index_text_for_attempt()
        attempt_context_protocol = default_context_protocol
        evidence = {
            "case_id": case.case_id,
            "condition": condition.value,
            "attempt": attempt,
            "repro": [sys.executable, *case.python_args],
            "stdout": current.stdout,
            "stderr": current.stderr,
            "failure_signature": failure_signature,
            "previous_failure_signature": previous_failure_signature,
            "failure_signature_repeat": failure_signature_repeat,
            "previous_attempt_diff": previous_attempt_diff,
            "timed_out": bool(current.timed_out),
            "timeout_sec": int(current.timeout_sec)
            if isinstance(current.timeout_sec, int)
            else None,
            "previous_note": previous_note,
            "previous_patch_verifier_rejected": previous_patch_verifier_rejected,
            "previous_patch_verifier_reasons": previous_patch_verifier_reasons,
            "previous_patch_verifier_scope": previous_patch_verifier_scope,
            "previous_timed_out": previous_timed_out,
            "previous_timeout_sec": previous_timeout_sec,
            "file_index": file_index,
            "files": "",
            "snapshot_available": snapshot is not None,
            "snapshot_error": snapshot_error,
        }
        snapshot_for_prompt = snapshot if not is_adaptive_condition else None
        stage_a_prompt: str | None = None
        stage_a_data = _empty_stage_a_context(reason="not_used")
        stage_a_meta: dict[str, Any] = {}
        stage_a_request_invoked = False
        stage_a_request_payload: ContextRequestPayload | None = None
        stage_a_raw_response: str | None = None
        stage_a_need_snapshot = False
        stage_a_snapshot_justification = ""
        stage_a_need_compact_context = False
        stage_a_compact_context_reason = ""
        run_stage_a = False
        if is_structured_condition:
            run_stage_a = True
        elif is_adaptive_gated:
            run_stage_a = helper_invoked
        elif is_adaptive_llm_discretion:
            can_run_stage_a = (
                not pregate_result.should_skip
                and not helper_budget_exhausted
                and not helper_saturation_guarded
                and supports_context_request
            )
            if adaptive_force_helper_stage_a:
                run_stage_a = can_run_stage_a
                if not run_stage_a and not adaptive_stagnation_override_reason:
                    adaptive_stagnation_override_reason = "stagnation_override_constraints_blocked"
            elif not can_run_stage_a:
                run_stage_a = False
            elif online_policy_decision is None:
                run_stage_a = True
            else:
                run_stage_a = online_policy_decision.applied_action == ACTION_INVOKE_HELPER
                if not run_stage_a:
                    helper_decision_reason = "online_policy_skip_helper"
        elif default_context_protocol == "staged":
            run_stage_a = True

        if run_stage_a:
            stage_a_prompt = render_stage_a_request_prompt(
                evidence=evidence,
                snapshot=snapshot,
                helper_budget_total=(
                    effective_helper_budget_limit
                    if is_adaptive_condition and not adaptive_budget_unlimited
                    else 0
                ),
                helper_budget_remaining=(
                    max(0, effective_helper_budget_limit - helper_calls_used)
                    if is_adaptive_condition and not adaptive_budget_unlimited
                    else 0
                ),
            )
            if supports_context_request:
                stage_a_request_invoked = True
                context_result = request_context_fn(
                    prompt=stage_a_prompt,
                    context_dir=attempt_dir,
                    cwd=workdir,
                )
            else:
                context_result = None
            if context_result is not None:
                if hasattr(context_result, "meta") and isinstance(context_result.meta, dict):
                    stage_a_meta.update(context_result.meta)
                request_candidate = (
                    context_result.request_json if hasattr(context_result, "request_json") else None
                )
                request_payload = _coerce_context_request_payload(request_candidate)
                if request_payload is not None:
                    stage_a_request_payload = request_payload
                if hasattr(context_result, "raw_response"):
                    raw_candidate = context_result.raw_response
                    if isinstance(raw_candidate, str):
                        stage_a_raw_response = raw_candidate

            if stage_a_request_payload is not None:
                stage_a_need_snapshot = bool(stage_a_request_payload.get("need_snapshot"))
                stage_a_snapshot_justification = str(
                    stage_a_request_payload.get("snapshot_justification") or ""
                )
                stage_a_need_compact_context = bool(
                    stage_a_request_payload.get("need_compact_context")
                )
                stage_a_compact_context_reason = str(
                    stage_a_request_payload.get("compact_context_reason") or ""
                )
                stage_a_data = execute_context_requests(
                    request_payload=stage_a_request_payload,
                    root=workdir,
                    snapshot=snapshot,
                    file_index=file_index_lines,
                    stdout=current.stdout,
                    stderr=current.stderr,
                    max_requests=stage_a_max_requests,
                    max_total_chars=stage_a_max_chars,
                )
                if is_adaptive_condition:
                    helper_need_more_context = bool(
                        stage_a_request_payload.get("need_more_context")
                    )
                    if is_adaptive_llm_discretion:
                        helper_invoked = helper_need_more_context or stage_a_need_snapshot
                        helper_decision_reason = (
                            "llm_requested_helper" if helper_invoked else "llm_declined_helper"
                        )
                    elif is_adaptive_gated and not helper_invoked and helper_need_more_context:
                        helper_invoked = True
                        helper_decision_reason = "gate_requested_helper"
            else:
                if is_adaptive_llm_discretion:
                    helper_invoked = False
                    helper_decision_reason = "llm_invalid_response_no_helper"
                    stage_a_data = StageAContextData(
                        blocks=[],
                        text="",
                        used_fallback=False,
                        requests_count=0,
                        chars_returned=0,
                        request_success=False,
                        request_payload=None,
                        request_raw=stage_a_raw_response,
                        reason="invalid_or_missing_stage_a_response",
                        evidence_items=[],
                    )
                else:
                    stage_a_data = build_fallback_context_bundle(
                        root=workdir,
                        snapshot=snapshot,
                        stdout=current.stdout,
                        stderr=current.stderr,
                        max_total_chars=stage_a_max_chars,
                        reason="invalid_or_missing_stage_a_response",
                        file_index_lines=file_index_lines,
                        request_raw=stage_a_raw_response,
                    )

            if stage_a_data.request_payload is None:
                stage_a_data = StageAContextData(
                    blocks=stage_a_data.blocks,
                    text=stage_a_data.text,
                    used_fallback=stage_a_data.used_fallback,
                    requests_count=stage_a_data.requests_count,
                    chars_returned=stage_a_data.chars_returned,
                    request_success=stage_a_data.request_success,
                    request_payload=stage_a_request_payload,
                    request_raw=stage_a_raw_response,
                    reason=stage_a_data.reason,
                    evidence_items=stage_a_data.evidence_items,
                )
        else:
            if is_adaptive_llm_discretion:
                if pregate_result.should_skip:
                    pass  # Keep "informativeness_pregate_skip" from earlier
                elif helper_budget_exhausted:
                    helper_decision_reason = "helper_budget_exhausted"
                elif helper_saturation_guarded:
                    helper_decision_reason = "helper_saturation_guard"
                elif not supports_context_request:
                    helper_decision_reason = "request_context_unavailable"
                stage_a_data = _empty_stage_a_context(reason=helper_decision_reason)
            elif is_adaptive_gated:
                stage_a_data = _empty_stage_a_context(reason=helper_decision_reason)

        if is_adaptive_condition:
            adaptive_context_requested = stage_a_data.requests_count > 0

            # Attempt-1 snapshot restriction: require justification to include snapshot in Stage-B.
            if (
                attempt == 1
                and stage_a_need_snapshot
                and _is_generic_snapshot_justification(stage_a_snapshot_justification)
            ):
                adaptive_snapshot_attempt1_denied = True
                adaptive_snapshot_attempt1_denied_reason = "missing_or_generic_justification"
                stage_a_need_snapshot = False

            adaptive_snapshot_included = bool(stage_a_need_snapshot and snapshot is not None)
            attempt_context_protocol = (
                "staged"
                if (adaptive_context_requested or adaptive_snapshot_included)
                else "monolithic"
            )
            snapshot_for_prompt = snapshot if adaptive_snapshot_included else None
            helper_invoked = bool(adaptive_context_requested or adaptive_snapshot_included)
            if (
                is_adaptive_llm_discretion
                and not helper_invoked
                and helper_decision_reason == "llm_requested_helper"
            ):
                helper_decision_reason = (
                    "llm_snapshot_request_denied_attempt1"
                    if adaptive_snapshot_attempt1_denied
                    else "llm_declined_helper"
                )
            if helper_invoked:
                helper_calls_used += 1
                if adaptive_context_requested and stage_a_data.chars_returned <= 0:
                    helper_calls_without_context += 1
            if isinstance(helper_need_more_context, bool):
                last_helper_need_more_context = helper_need_more_context
        elif default_context_protocol == "staged":
            attempt_context_protocol = "staged"
            snapshot_for_prompt = snapshot
        else:
            attempt_context_protocol = "monolithic"
            snapshot_for_prompt = snapshot
        if is_adaptive_condition:
            last_helper_decision_reason = str(helper_decision_reason)

        project_files_context = (
            _monolithic_context_for_attempt()
            if include_files_context and attempt_context_protocol == "monolithic"
            else ""
        )
        evidence["files"] = project_files_context
        evidence["snapshot_available"] = snapshot_for_prompt is not None

        stage_a_default_llm_calls = 1 if (is_openai_patcher and stage_a_request_invoked) else 0

        structured_seed_data = _empty_stage_a_context()
        structured_seed_active = is_structured_condition or (
            is_adaptive_condition and helper_invoked
        )
        if structured_seed_active:
            structured_seed_data = build_fallback_context_bundle(
                root=workdir,
                snapshot=snapshot_for_prompt,
                stdout=current.stdout,
                stderr=current.stderr,
                max_total_chars=stage_a_max_chars,
                reason="structured_seed_bundle"
                if is_structured_condition
                else "adaptive_helper_seed_bundle",
                file_index_lines=file_index_lines,
            )

        combined_evidence_items = _merge_evidence_items(
            structured_seed_data.evidence_items if structured_seed_active else [],
            stage_a_data.evidence_items,
        )
        combined_evidence_ids = _evidence_ids_from_items(combined_evidence_items)
        new_evidence_items = [
            item
            for item in combined_evidence_items
            if str(item.get("id") or "") not in seen_evidence_ids
        ]
        new_evidence_ids = _evidence_ids_from_items(new_evidence_items)
        reused_evidence_ids = [
            evidence_id for evidence_id in combined_evidence_ids if evidence_id in seen_evidence_ids
        ]
        seen_evidence_ids.update(combined_evidence_ids)

        attempt_retry_prompt_mode = base_retry_prompt_mode
        is_delta_retry = attempt_retry_prompt_mode == "delta" and attempt > 1
        evidence_items_for_prompt = (
            new_evidence_items if is_delta_retry else combined_evidence_items
        )
        stage_a_context_full = _render_evidence_items(combined_evidence_items)
        stage_a_context_text = (
            _render_evidence_items(new_evidence_items) if is_delta_retry else stage_a_context_full
        )
        stage_a_evidence_index_text = _render_evidence_index(evidence_items_for_prompt)
        retry_delta_chars = len(stage_a_context_text) if is_delta_retry else 0
        adaptive_progress_made = False
        adaptive_progress_reason = "not_adaptive_condition"
        adaptive_no_progress_streak_current = 0
        adaptive_progress_stop_candidate = False
        adaptive_progress_stop_reason = "not_adaptive_condition"
        if is_adaptive_condition:
            adaptive_progress_made, adaptive_progress_reason = _adaptive_progress_signal(
                signature_repeated=failure_signature_repeat,
                new_evidence_count=len(new_evidence_ids),
            )
            if adaptive_progress_made:
                adaptive_no_progress_streak = 0
            else:
                adaptive_no_progress_streak += 1
            adaptive_no_progress_streak_current = adaptive_no_progress_streak
            (
                adaptive_progress_stop_candidate,
                adaptive_progress_stop_reason,
            ) = _adaptive_should_stop_for_no_progress(
                no_progress_streak=adaptive_no_progress_streak_current,
                stop_window=adaptive_no_progress_stop_window,
                helper_budget_exhausted=helper_budget_exhausted,
                helper_saturation_guarded=helper_saturation_guarded,
                helper_decision_reason=helper_decision_reason,
            )

        retry_packet = ""
        if structured_seed_active or is_delta_retry:
            retry_packet = _render_retry_packet(
                failure_signature=failure_signature,
                previous_failure_signature=evidence.get("previous_failure_signature"),
                failure_signature_repeat=bool(evidence.get("failure_signature_repeat")),
                previous_attempt_diff=previous_attempt_diff,
                timed_out=bool(evidence.get("timed_out")),
                timeout_sec=(
                    int(evidence.get("timeout_sec"))
                    if isinstance(evidence.get("timeout_sec"), int)
                    else None
                ),
                previous_note=str(evidence.get("previous_note") or ""),
                previous_patch_verifier_rejected=bool(
                    evidence.get("previous_patch_verifier_rejected")
                ),
                previous_patch_verifier_reasons=(
                    list(evidence.get("previous_patch_verifier_reasons"))
                    if isinstance(evidence.get("previous_patch_verifier_reasons"), list)
                    else []
                ),
                previous_patch_verifier_scope=str(
                    evidence.get("previous_patch_verifier_scope") or ""
                ),
            )
        evidence_fingerprint = _compute_evidence_fingerprint(
            failure_signature=failure_signature,
            stage_a_blocks=stage_a_data.blocks,
            stage_a_context=stage_a_context_full,
            structured_seed_blocks=structured_seed_data.blocks if structured_seed_active else [],
        )
        current_failure_context["evidence_fingerprint"] = evidence_fingerprint

        (
            retry_history_text,
            retry_history_chars,
            retry_history_turns_included,
            retry_history_truncated,
        ) = (
            _build_retry_history_section(
                prior_turns=retry_history_turns,
                max_chars=effective_retry_history_max_chars,
            )
            if effective_retry_context_mode == "history" and attempt > 1
            else ("", 0, 0, False)
        )
        attempt_prompt_budget = _adaptive_prompt_budget_for_attempt(
            base_budget=prompt_budget,
            helper_invoked=(is_adaptive_condition and helper_invoked),
            profile=normalized_adaptive_helper_budget_profile,
        )
        openai_preflight_prompt_cap_chars: int | None = None
        openai_preflight_trimmed = False
        if is_openai_patcher and normalized_openai_context_window_tokens > 0:
            openai_max_tokens_for_attempt = int(getattr(patcher, "max_tokens", 0) or 0)
            openai_preflight_prompt_cap_chars = _openai_preflight_total_char_cap(
                context_window_tokens=normalized_openai_context_window_tokens,
                max_tokens=openai_max_tokens_for_attempt,
            )
            previous_total_chars = int(attempt_prompt_budget.get("total_chars", 0))
            next_total_chars = (
                openai_preflight_prompt_cap_chars
                if previous_total_chars <= 0
                else min(previous_total_chars, openai_preflight_prompt_cap_chars)
            )
            openai_preflight_trimmed = (previous_total_chars <= 0 and next_total_chars > 0) or (
                previous_total_chars > 0 and next_total_chars < previous_total_chars
            )
            attempt_prompt_budget["total_chars"] = max(0, int(next_total_chars))
        include_stage_a_evidence_index = bool(
            stage_a_evidence_index_text and (is_delta_retry or not stage_a_context_text.strip())
        )
        stage_a_index_reason = (
            "delta_id_map"
            if (include_stage_a_evidence_index and is_delta_retry)
            else ("context_missing" if include_stage_a_evidence_index else "redundant_skipped")
        )
        include_snapshot_in_prompt = True
        include_pytest_output_in_prompt = True
        include_stage_a_context_in_prompt = True
        include_project_files_in_prompt = attempt_context_protocol == "monolithic"
        compacted_context_for_prompt = ""
        context_compaction_requested = bool(stage_a_need_compact_context)
        context_compaction_request_reason = str(stage_a_compact_context_reason or "")
        context_compaction_invoked = False
        context_compaction_success = False
        context_compaction_trigger = "not_needed"
        context_compaction_error = ""
        context_compaction_auto_overflow_triggered = False
        context_compaction_replaced_sections: list[str] = []
        context_compaction_source_sections: list[str] = []
        context_compaction_source_chars: dict[str, int] = {}
        context_compaction_input_chars = 0
        context_compaction_output_chars = 0
        context_compaction_prompt_chars = 0
        context_compaction_meta: dict[str, Any] = {}

        prompt_sections = _build_stage_b_prompt_sections(
            evidence=evidence,
            snapshot=snapshot_for_prompt,
            stage_a_context=stage_a_context_text,
            stage_a_evidence_index=stage_a_evidence_index_text,
            include_stage_a_evidence_index=include_stage_a_evidence_index,
            compacted_context=compacted_context_for_prompt,
            include_snapshot=include_snapshot_in_prompt,
            include_pytest_output=include_pytest_output_in_prompt,
            include_stage_a_context=include_stage_a_context_in_prompt,
            include_project_files=include_project_files_in_prompt,
            retry_packet=retry_packet,
            retry_prompt_mode=attempt_retry_prompt_mode,
            retry_history=retry_history_text,
            attempt=attempt,
            known_evidence_refs=reused_evidence_ids,
            new_evidence_ids=new_evidence_ids,
        )
        prompt_budget_result = _apply_prompt_budget(
            sections=prompt_sections,
            budget=attempt_prompt_budget,
            is_delta_retry=is_delta_retry,
        )
        # If Stage-A context had to be truncated/dropped, re-introduce compact ID index.
        if (
            stage_a_evidence_index_text
            and not include_stage_a_evidence_index
            and prompt_budget_result.section_status.get("stage_a_context")
            in {"truncated", "dropped"}
        ):
            prompt_sections = _build_stage_b_prompt_sections(
                evidence=evidence,
                snapshot=snapshot_for_prompt,
                stage_a_context=stage_a_context_text,
                stage_a_evidence_index=stage_a_evidence_index_text,
                include_stage_a_evidence_index=True,
                compacted_context=compacted_context_for_prompt,
                include_snapshot=include_snapshot_in_prompt,
                include_pytest_output=include_pytest_output_in_prompt,
                include_stage_a_context=include_stage_a_context_in_prompt,
                include_project_files=include_project_files_in_prompt,
                retry_packet=retry_packet,
                retry_prompt_mode=attempt_retry_prompt_mode,
                retry_history=retry_history_text,
                attempt=attempt,
                known_evidence_refs=reused_evidence_ids,
                new_evidence_ids=new_evidence_ids,
            )
            prompt_budget_result = _apply_prompt_budget(
                sections=prompt_sections,
                budget=attempt_prompt_budget,
                is_delta_retry=is_delta_retry,
            )
            include_stage_a_evidence_index = True
            stage_a_index_reason = "context_truncated_fallback"

        context_compaction_auto_overflow_triggered = bool(
            prompt_budget_result.overflow_chars > 0
            or prompt_budget_result.section_status.get("snapshot") in {"truncated", "dropped"}
            or prompt_budget_result.section_status.get("pytest_output") in {"truncated", "dropped"}
            or prompt_budget_result.section_status.get("project_files") in {"truncated", "dropped"}
            or prompt_budget_result.section_status.get("stage_a_context")
            in {"truncated", "dropped"}
        )
        context_compaction_bundle = _build_context_compaction_bundle(
            scope=normalized_context_compaction_scope,
            snapshot=snapshot_for_prompt,
            stdout=current.stdout,
            stderr=current.stderr,
            stage_a_context=stage_a_context_text,
            project_files_context=project_files_context if include_project_files_in_prompt else "",
            max_input_chars=normalized_context_compaction_max_input_chars,
        )
        context_compaction_source_sections = list(context_compaction_bundle.source_sections)
        context_compaction_source_chars = dict(context_compaction_bundle.source_chars)
        context_compaction_input_chars = len(context_compaction_bundle.text)
        should_compact_context, context_compaction_trigger = _should_invoke_context_compaction(
            mode=normalized_context_compaction_mode,
            supports_context_compaction=supports_context_compaction,
            stage_a_requested=context_compaction_requested,
            auto_overflow_triggered=context_compaction_auto_overflow_triggered,
            calls_used=context_compaction_calls_used,
            max_calls=normalized_context_compaction_max_calls,
            bundle_input_chars=context_compaction_input_chars,
        )
        if should_compact_context:
            context_compaction_invoked = True
            context_compaction_calls_used += 1
            context_compaction_prompt = render_context_compaction_prompt(
                evidence=evidence,
                attempt=attempt,
                reason=context_compaction_request_reason,
                source_sections=context_compaction_source_sections,
                target_chars=normalized_context_compaction_target_chars,
            )
            context_compaction_prompt_chars = len(context_compaction_prompt)
            try:
                compaction_result = context_compaction_fn(
                    prompt=context_compaction_prompt,
                    context_text=context_compaction_bundle.text,
                    context_dir=attempt_dir,
                    cwd=workdir,
                    target_chars=normalized_context_compaction_target_chars,
                )
            except Exception as exc:
                compaction_result = None
                context_compaction_error = f"{type(exc).__name__}: {exc}"
            if compaction_result is not None:
                if hasattr(compaction_result, "meta") and isinstance(compaction_result.meta, dict):
                    context_compaction_meta = dict(compaction_result.meta)
                compacted_text_candidate = (
                    compaction_result.compacted_text
                    if hasattr(compaction_result, "compacted_text")
                    else None
                )
                if isinstance(compacted_text_candidate, str) and compacted_text_candidate.strip():
                    compacted_context_for_prompt = compacted_text_candidate.strip()
                    context_compaction_output_chars = len(compacted_context_for_prompt)
                    context_compaction_success = True
                elif not context_compaction_error:
                    context_compaction_error = str(context_compaction_meta.get("error") or "")
            elif not context_compaction_error:
                context_compaction_error = "compact_context_unavailable"

            if context_compaction_success:
                include_snapshot_in_prompt = not context_compaction_bundle.replace_snapshot
                include_pytest_output_in_prompt = (
                    not context_compaction_bundle.replace_pytest_output
                )
                include_stage_a_context_in_prompt = (
                    not context_compaction_bundle.replace_stage_a_context
                )
                include_project_files_in_prompt = include_project_files_in_prompt and (
                    not context_compaction_bundle.replace_project_files
                )
                if context_compaction_bundle.replace_snapshot:
                    context_compaction_replaced_sections.append("snapshot")
                if context_compaction_bundle.replace_pytest_output:
                    context_compaction_replaced_sections.append("pytest_output")
                if context_compaction_bundle.replace_stage_a_context:
                    context_compaction_replaced_sections.append("stage_a_context")
                if context_compaction_bundle.replace_project_files:
                    context_compaction_replaced_sections.append("project_files")
                prompt_sections = _build_stage_b_prompt_sections(
                    evidence=evidence,
                    snapshot=snapshot_for_prompt,
                    stage_a_context=stage_a_context_text,
                    stage_a_evidence_index=stage_a_evidence_index_text,
                    include_stage_a_evidence_index=include_stage_a_evidence_index,
                    compacted_context=compacted_context_for_prompt,
                    include_snapshot=include_snapshot_in_prompt,
                    include_pytest_output=include_pytest_output_in_prompt,
                    include_stage_a_context=include_stage_a_context_in_prompt,
                    include_project_files=include_project_files_in_prompt,
                    retry_packet=retry_packet,
                    retry_prompt_mode=attempt_retry_prompt_mode,
                    retry_history=retry_history_text,
                    attempt=attempt,
                    known_evidence_refs=reused_evidence_ids,
                    new_evidence_ids=new_evidence_ids,
                )
                prompt_budget_result = _apply_prompt_budget(
                    sections=prompt_sections,
                    budget=attempt_prompt_budget,
                    is_delta_retry=is_delta_retry,
                )
                if (
                    stage_a_evidence_index_text
                    and not include_stage_a_evidence_index
                    and (
                        prompt_budget_result.section_status.get("stage_a_context")
                        in {"truncated", "dropped"}
                        or prompt_budget_result.section_status.get("compacted_context")
                        in {"truncated", "dropped"}
                    )
                ):
                    prompt_sections = _build_stage_b_prompt_sections(
                        evidence=evidence,
                        snapshot=snapshot_for_prompt,
                        stage_a_context=stage_a_context_text,
                        stage_a_evidence_index=stage_a_evidence_index_text,
                        include_stage_a_evidence_index=True,
                        compacted_context=compacted_context_for_prompt,
                        include_snapshot=include_snapshot_in_prompt,
                        include_pytest_output=include_pytest_output_in_prompt,
                        include_stage_a_context=include_stage_a_context_in_prompt,
                        include_project_files=include_project_files_in_prompt,
                        retry_packet=retry_packet,
                        retry_prompt_mode=attempt_retry_prompt_mode,
                        retry_history=retry_history_text,
                        attempt=attempt,
                        known_evidence_refs=reused_evidence_ids,
                        new_evidence_ids=new_evidence_ids,
                    )
                    prompt_budget_result = _apply_prompt_budget(
                        sections=prompt_sections,
                        budget=attempt_prompt_budget,
                        is_delta_retry=is_delta_retry,
                    )
                    include_stage_a_evidence_index = True
                    stage_a_index_reason = "context_truncated_fallback"

        last_context_compaction_requested = context_compaction_requested
        last_context_compaction_invoked = context_compaction_invoked
        if context_compaction_invoked:
            last_context_compaction_success = context_compaction_success
            last_context_compaction_trigger = context_compaction_trigger
            if context_compaction_input_chars > 0 and context_compaction_output_chars > 0:
                last_context_compaction_ratio = min(
                    1.0,
                    context_compaction_output_chars / float(max(1, context_compaction_input_chars)),
                )
            else:
                last_context_compaction_ratio = 0.0
        elif context_compaction_requested:
            last_context_compaction_success = False
            last_context_compaction_trigger = context_compaction_trigger

        prompt = prompt_budget_result.prompt
        retry_history_budget_chars = int(attempt_prompt_budget.get("retry_history_chars", 0))
        retry_history_budget_applied = bool(
            retry_history_text
            and (
                retry_history_text not in prompt
                or (
                    retry_history_budget_chars > 0
                    and retry_history_chars > retry_history_budget_chars
                )
            )
        )
        attempt_metadata = _materialize_attempt_metadata(
            attempt_dir=attempt_dir,
            attempt=attempt,
            max_attempts=k,
            evidence=evidence,
            prompt=prompt,
            record_prompt=record_prompt,
            record_evidence=record_evidence,
            context_protocol=attempt_context_protocol,
            stage_a_prompt=stage_a_prompt,
            stage_a_raw_response=stage_a_data.request_raw,
            stage_a_request_payload=stage_a_data.request_payload,
            stage_a_requests_count=stage_a_data.requests_count,
            stage_a_chars_returned=stage_a_data.chars_returned,
            stage_a_request_success=stage_a_data.request_success,
            retry_prompt_mode=attempt_retry_prompt_mode,
            retry_context_mode=effective_retry_context_mode,
            retry_history_chars=retry_history_chars,
            retry_history_truncated=retry_history_truncated,
            retry_history_turns_included=retry_history_turns_included,
            retry_history_budget_chars=retry_history_budget_chars,
            retry_history_budget_applied=retry_history_budget_applied,
            retry_delta_prompt=is_delta_retry,
            retry_delta_chars=retry_delta_chars,
            adaptive_mode=helper_decision_mode,
            adaptive_policy=adaptive_policy_label,
            adaptive_signature_repeat=failure_signature_repeat,
            adaptive_stagnation_streak=stagnation_streak,
            adaptive_helper_invoked=helper_invoked,
            adaptive_helper_decision_reason=helper_decision_reason,
            adaptive_stagnation_override_applied=adaptive_stagnation_override_applied,
            adaptive_stagnation_override_reason=adaptive_stagnation_override_reason,
            adaptive_helper_need_more_context=helper_need_more_context,
            adaptive_helper_calls_used_before_attempt=max(
                0,
                helper_calls_used - (1 if (is_adaptive_condition and helper_invoked) else 0),
            ),
            adaptive_helper_calls_used_after_attempt=helper_calls_used,
            adaptive_helper_budget_max_calls=normalized_adaptive_max_helper_calls,
            adaptive_helper_budget_unlimited=adaptive_budget_unlimited,
            adaptive_helper_budget_exhausted=helper_budget_exhausted,
            adaptive_helper_saturation_guarded=helper_saturation_guarded,
            adaptive_helper_budget_profile=normalized_adaptive_helper_budget_profile,
            adaptive_budget_topup_granted=adaptive_budget_topup_granted,
            adaptive_budget_topup_reason=adaptive_budget_topup_reason,
            adaptive_context_requested=adaptive_context_requested,
            adaptive_snapshot_included=adaptive_snapshot_included,
            adaptive_snapshot_attempt1_denied=adaptive_snapshot_attempt1_denied,
            adaptive_snapshot_attempt1_denied_reason=adaptive_snapshot_attempt1_denied_reason,
            adaptive_progress_made=adaptive_progress_made,
            adaptive_progress_reason=adaptive_progress_reason,
            adaptive_no_progress_streak=adaptive_no_progress_streak_current,
            adaptive_progress_stop_candidate=adaptive_progress_stop_candidate,
            adaptive_progress_stop_triggered=False,
            adaptive_progress_stop_reason=adaptive_progress_stop_reason,
            llm_call_count=0,
            tokens_prompt=0,
            tokens_completion=0,
            tokens_total=0,
            evidence_ids_new_count=len(new_evidence_ids),
            evidence_ids_reused_count=len(reused_evidence_ids),
            evidence_ids_sent_count=len(_evidence_ids_from_items(evidence_items_for_prompt)),
            prompt_budget_used_chars=prompt_budget_result.used_chars,
            prompt_budget_overflow_chars=prompt_budget_result.overflow_chars,
            prompt_budget_dropped_sections=prompt_budget_result.dropped_sections,
            stage_a_index_included=bool(include_stage_a_evidence_index),
            stage_a_index_reason=stage_a_index_reason,
            context_compaction_mode=normalized_context_compaction_mode,
            context_compaction_scope=sorted(normalized_context_compaction_scope),
            context_compaction_supported=supports_context_compaction,
            context_compaction_requested=context_compaction_requested,
            context_compaction_request_reason=context_compaction_request_reason,
            context_compaction_trigger=context_compaction_trigger,
            context_compaction_auto_overflow_triggered=context_compaction_auto_overflow_triggered,
            context_compaction_invoked=context_compaction_invoked,
            context_compaction_success=context_compaction_success,
            context_compaction_error=context_compaction_error,
            context_compaction_calls_used=context_compaction_calls_used,
            context_compaction_max_calls=normalized_context_compaction_max_calls,
            context_compaction_source_sections=context_compaction_source_sections,
            context_compaction_source_chars=context_compaction_source_chars,
            context_compaction_replaced_sections=context_compaction_replaced_sections,
            context_compaction_input_chars=context_compaction_input_chars,
            context_compaction_target_chars=normalized_context_compaction_target_chars,
            context_compaction_output_chars=context_compaction_output_chars,
            context_compaction_prompt_chars=context_compaction_prompt_chars,
            openai_preflight_prompt_cap_chars=openai_preflight_prompt_cap_chars,
            openai_preflight_trimmed=openai_preflight_trimmed,
            openai_context_window_source=normalized_openai_context_window_source,
            openai_context_window_probe_provider=normalized_openai_context_window_probe_provider,
            openai_context_window_probe_error=normalized_openai_context_window_probe_error,
            openai_response_mode_autoswitch_policy=normalized_openai_response_mode_autoswitch,
            openai_response_mode_autoswitched=False,
            openai_response_mode_autoswitch_from="",
            openai_response_mode_autoswitch_to="",
            openai_response_mode_autoswitch_cycle=0,
            openai_response_mode_autoswitch_reason="",
            adaptive_informativeness_pregate_fired=pregate_result.should_skip,
            adaptive_informativeness_pregate_score=pregate_result.score,
            adaptive_informativeness_pregate_exception_type=pregate_result.exception_type,
            adaptive_informativeness_pregate_reason=pregate_result.reason,
            adaptive_informativeness_pregate_features={
                "type_specificity": pregate_result.f_type_specificity,
                "message_diagnostic": pregate_result.f_message_diagnostic,
                "crash_in_user_code": pregate_result.f_crash_in_user_code,
                "assertion_quality": pregate_result.f_assertion_quality,
                "frame_depth": pregate_result.f_frame_depth,
            },
            syntax_preflight_enabled=normalized_syntax_preflight_enabled,
            syntax_preflight_cycles_used=0,
            syntax_preflight_failures=0,
            syntax_preflight_last_error_type="",
            syntax_preflight_last_error_msg="",
        )

        patch_meta: dict[str, Any] = {}
        patch_meta["context_protocol"] = attempt_context_protocol
        patch_meta["evidence_fingerprint"] = evidence_fingerprint
        patch_meta["retry_prompt_mode"] = attempt_retry_prompt_mode
        patch_meta["retry_context_mode"] = effective_retry_context_mode
        patch_meta["retry_history_chars"] = retry_history_chars
        patch_meta["retry_history_truncated"] = retry_history_truncated
        patch_meta["retry_history_turns_included"] = retry_history_turns_included
        patch_meta["retry_history_budget_chars"] = retry_history_budget_chars
        patch_meta["retry_history_budget_applied"] = retry_history_budget_applied
        patch_meta["retry_delta_prompt"] = is_delta_retry
        patch_meta["retry_delta_chars"] = retry_delta_chars
        patch_meta["evidence_ids_new_count"] = len(new_evidence_ids)
        patch_meta["evidence_ids_reused_count"] = len(reused_evidence_ids)
        patch_meta["evidence_ids_sent_count"] = len(
            _evidence_ids_from_items(evidence_items_for_prompt)
        )
        patch_meta["evidence_ids_new"] = new_evidence_ids[:20]
        patch_meta["evidence_ids_reused"] = reused_evidence_ids[:20]
        patch_meta["prompt_budget_used_chars"] = prompt_budget_result.used_chars
        patch_meta["prompt_budget_overflow_chars"] = prompt_budget_result.overflow_chars
        patch_meta["prompt_budget_dropped_sections"] = list(prompt_budget_result.dropped_sections)
        patch_meta["stage_a_index_included"] = bool(include_stage_a_evidence_index)
        patch_meta["stage_a_index_reason"] = stage_a_index_reason
        patch_meta["context_compaction_mode"] = normalized_context_compaction_mode
        patch_meta["context_compaction_scope"] = sorted(normalized_context_compaction_scope)
        patch_meta["context_compaction_supported"] = supports_context_compaction
        patch_meta["context_compaction_requested"] = context_compaction_requested
        patch_meta["context_compaction_request_reason"] = context_compaction_request_reason
        patch_meta["context_compaction_trigger"] = context_compaction_trigger
        patch_meta["context_compaction_auto_overflow_triggered"] = (
            context_compaction_auto_overflow_triggered
        )
        patch_meta["context_compaction_invoked"] = context_compaction_invoked
        patch_meta["context_compaction_success"] = context_compaction_success
        patch_meta["context_compaction_error"] = context_compaction_error
        patch_meta["context_compaction_calls_used"] = context_compaction_calls_used
        patch_meta["context_compaction_max_calls"] = normalized_context_compaction_max_calls
        patch_meta["context_compaction_source_sections"] = list(context_compaction_source_sections)
        patch_meta["context_compaction_source_chars"] = dict(context_compaction_source_chars)
        patch_meta["context_compaction_replaced_sections"] = list(
            context_compaction_replaced_sections
        )
        patch_meta["context_compaction_input_chars"] = context_compaction_input_chars
        patch_meta["context_compaction_target_chars"] = normalized_context_compaction_target_chars
        patch_meta["context_compaction_output_chars"] = context_compaction_output_chars
        patch_meta["context_compaction_prompt_chars"] = context_compaction_prompt_chars
        if context_compaction_meta:
            patch_meta["context_compaction_meta"] = _summarize_context_compaction_meta(
                context_compaction_meta
            )
        patch_meta["openai_preflight_prompt_cap_chars"] = openai_preflight_prompt_cap_chars
        patch_meta["openai_preflight_trimmed"] = openai_preflight_trimmed
        patch_meta["openai_context_window_source"] = normalized_openai_context_window_source
        patch_meta["openai_context_window_probe_provider"] = (
            normalized_openai_context_window_probe_provider
        )
        patch_meta["openai_context_window_probe_error"] = (
            normalized_openai_context_window_probe_error
        )
        patch_meta["adaptive_mode"] = helper_decision_mode
        patch_meta["adaptive_policy"] = adaptive_policy_label
        patch_meta["adaptive_signature_repeat"] = failure_signature_repeat
        patch_meta["adaptive_stagnation_streak"] = stagnation_streak
        patch_meta["adaptive_helper_invoked"] = helper_invoked
        patch_meta["adaptive_helper_decision_reason"] = helper_decision_reason
        patch_meta["adaptive_stagnation_override_applied"] = adaptive_stagnation_override_applied
        patch_meta["adaptive_stagnation_override_reason"] = adaptive_stagnation_override_reason
        patch_meta["adaptive_helper_need_more_context"] = helper_need_more_context
        patch_meta["adaptive_helper_calls_used_before_attempt"] = max(
            0,
            helper_calls_used - (1 if (is_adaptive_condition and helper_invoked) else 0),
        )
        patch_meta["adaptive_helper_calls_used_after_attempt"] = helper_calls_used
        patch_meta["adaptive_helper_budget_max_calls"] = normalized_adaptive_max_helper_calls
        patch_meta["adaptive_helper_budget_unlimited"] = adaptive_budget_unlimited
        patch_meta["adaptive_helper_budget_exhausted"] = helper_budget_exhausted
        patch_meta["adaptive_helper_saturation_guarded"] = helper_saturation_guarded
        patch_meta["adaptive_saturation_threshold"] = normalized_adaptive_saturation_threshold
        patch_meta["adaptive_helper_budget_profile"] = normalized_adaptive_helper_budget_profile
        patch_meta["adaptive_budget_topup_granted"] = adaptive_budget_topup_granted
        patch_meta["adaptive_budget_topup_reason"] = adaptive_budget_topup_reason
        patch_meta["adaptive_informativeness_pregate_fired"] = pregate_result.should_skip
        patch_meta["adaptive_informativeness_pregate_score"] = pregate_result.score
        patch_meta["adaptive_informativeness_pregate_exception_type"] = (
            pregate_result.exception_type
        )
        patch_meta["adaptive_informativeness_pregate_reason"] = pregate_result.reason
        patch_meta["adaptive_informativeness_pregate_features"] = {
            "type_specificity": pregate_result.f_type_specificity,
            "message_diagnostic": pregate_result.f_message_diagnostic,
            "crash_in_user_code": pregate_result.f_crash_in_user_code,
            "assertion_quality": pregate_result.f_assertion_quality,
            "frame_depth": pregate_result.f_frame_depth,
        }
        patch_meta["adaptive_context_requested"] = adaptive_context_requested
        patch_meta["adaptive_snapshot_included"] = adaptive_snapshot_included
        patch_meta["adaptive_snapshot_attempt1_denied"] = adaptive_snapshot_attempt1_denied
        patch_meta["adaptive_snapshot_attempt1_denied_reason"] = (
            adaptive_snapshot_attempt1_denied_reason
        )
        patch_meta["adaptive_progress_made"] = adaptive_progress_made
        patch_meta["adaptive_progress_reason"] = adaptive_progress_reason
        patch_meta["adaptive_no_progress_streak"] = adaptive_no_progress_streak_current
        patch_meta["adaptive_progress_stop_candidate"] = adaptive_progress_stop_candidate
        patch_meta["adaptive_progress_stop_triggered"] = False
        patch_meta["adaptive_progress_stop_reason"] = adaptive_progress_stop_reason
        patch_meta["patch_file_count"] = 0
        patch_meta["patch_test_file_count"] = 0
        patch_meta["patch_non_test_file_count"] = 0
        patch_meta["patch_touches_tests"] = False
        patch_meta["patch_file_paths"] = []
        patch_meta["patch_test_file_paths"] = []
        patch_meta["patch_verifier_enabled"] = False
        patch_meta["patch_verifier_scope"] = PATCH_VERIFIER_SCOPE_DISABLED
        patch_meta["patch_verifier_passed"] = None
        patch_meta["patch_verifier_score"] = None
        patch_meta["patch_verifier_reasons"] = []
        patch_meta["patch_verifier_inferred_source_scope"] = []
        patch_meta["patch_verifier_source_scope_matched"] = False
        patch_meta["syntax_preflight_enabled"] = normalized_syntax_preflight_enabled
        patch_meta["syntax_preflight_cycles_used"] = 0
        patch_meta["syntax_preflight_failures"] = 0
        patch_meta["syntax_preflight_last_error_type"] = ""
        patch_meta["syntax_preflight_last_error_msg"] = ""
        patch_meta["syntax_preflight_events"] = []
        _sync_openai_response_mode_autoswitch_metadata(
            attempt_metadata=attempt_metadata,
            patch_meta=patch_meta,
            policy=normalized_openai_response_mode_autoswitch,
            switched=False,
            from_mode="",
            to_mode="",
            cycle=0,
            reason="",
        )
        _sync_patcher_request_failure_metadata(
            attempt_metadata=attempt_metadata,
            patch_meta=patch_meta,
            failed=False,
        )
        _sync_patch_sanitize_metadata(
            attempt_metadata=attempt_metadata,
            patch_meta=patch_meta,
            enabled=normalized_patch_sanitize_enabled,
            passed=False,
            rejected=False,
            reasons=[],
            error_msg="",
        )
        if attempt_context_protocol == "staged" or stage_a_request_invoked:
            patch_meta["stage_a_request_success"] = stage_a_data.request_success
            patch_meta["stage_a_used_fallback"] = stage_a_data.used_fallback
            patch_meta["stage_a_reason"] = stage_a_data.reason
            patch_meta["stage_a_requests_count"] = stage_a_data.requests_count
            patch_meta["stage_a_chars_returned"] = stage_a_data.chars_returned
            if stage_a_request_payload:
                patch_meta["stage_a_diagnosis"] = str(stage_a_request_payload.get("diagnosis", ""))
                patch_meta["stage_a_missing_fact"] = stage_a_request_payload.get("missing_fact")
                patch_meta["stage_a_need_compact_context"] = bool(
                    stage_a_request_payload.get("need_compact_context")
                )
                patch_meta["stage_a_compact_context_reason"] = str(
                    stage_a_request_payload.get("compact_context_reason") or ""
                )
            if stage_a_meta:
                patch_meta["stage_a_meta"] = stage_a_meta
        (
            attempt_llm_calls,
            attempt_tokens_prompt,
            attempt_tokens_completion,
            attempt_tokens_total,
        ) = _aggregate_attempt_llm_usage(
            stage_a_meta=stage_a_meta,
            patch_meta=None,
            stage_a_default_llm_calls=stage_a_default_llm_calls,
            patch_default_llm_calls=0,
        )
        if context_compaction_invoked:
            (
                compaction_llm_calls,
                compaction_tokens_prompt,
                compaction_tokens_completion,
                compaction_tokens_total,
            ) = _aggregate_attempt_llm_usage(
                stage_a_meta=None,
                patch_meta=context_compaction_meta if context_compaction_meta else None,
                stage_a_default_llm_calls=0,
                patch_default_llm_calls=1 if is_openai_patcher else 0,
            )
            attempt_llm_calls += compaction_llm_calls
            attempt_tokens_prompt += compaction_tokens_prompt
            attempt_tokens_completion += compaction_tokens_completion
            attempt_tokens_total += compaction_tokens_total
        _apply_usage_totals_to_patch_meta(
            patch_meta=patch_meta,
            llm_call_count=attempt_llm_calls,
            tokens_prompt=attempt_tokens_prompt,
            tokens_completion=attempt_tokens_completion,
            tokens_total=attempt_tokens_total,
        )
        _sync_attempt_usage_metadata(
            attempt_metadata=attempt_metadata,
            patch_meta=patch_meta,
            llm_call_count=attempt_llm_calls,
        )
        _sync_adaptive_online_metadata(
            attempt_metadata=attempt_metadata,
            patch_meta=patch_meta,
            mode=online_policy_mode_for_attempt,
            algo=online_policy_algo_for_attempt,
            decision=online_policy_decision,
            eligible=online_policy_eligible,
            override_reason=online_policy_override_reason,
            feature_values=online_policy_features,
            reward_lambda_tokens=normalized_adaptive_reward_lambda_tokens,
            reward_mu_latency=normalized_adaptive_reward_mu_latency,
            reward_token_scale=normalized_adaptive_reward_token_scale,
            reward_latency_scale=normalized_adaptive_reward_latency_scale,
        )
        online_finalize_kwargs = {
            "is_adaptive_llm_discretion": is_adaptive_llm_discretion,
            "online_policy_decision": online_policy_decision,
            "adaptive_online_policy_controller": adaptive_online_policy_controller,
            "run_id": run_id,
            "case_id": case.case_id,
            "condition_value": condition.value,
            "attempt": attempt,
            "attempt_metadata": attempt_metadata,
            "patch_meta": patch_meta,
            "online_policy_mode_for_attempt": online_policy_mode_for_attempt,
            "online_policy_algo_for_attempt": online_policy_algo_for_attempt,
            "online_policy_eligible": online_policy_eligible,
            "online_policy_override_reason": online_policy_override_reason,
            "online_policy_features": online_policy_features,
            "helper_decision_reason": helper_decision_reason,
            "normalized_adaptive_online_policy_mode": normalized_adaptive_online_policy_mode,
            "reward_lambda_tokens": normalized_adaptive_reward_lambda_tokens,
            "reward_mu_latency": normalized_adaptive_reward_mu_latency,
            "reward_token_scale": normalized_adaptive_reward_token_scale,
            "reward_latency_scale": normalized_adaptive_reward_latency_scale,
        }

        patcher_for_attempt = patcher
        if (
            is_openai_patcher
            and is_adaptive_condition
            and helper_invoked
            and normalized_adaptive_openai_helper_response_mode != "inherit"
        ):
            target_mode = normalized_adaptive_openai_helper_response_mode
            current_mode = str(getattr(patcher, "response_mode", "")).strip()
            if target_mode and target_mode != current_mode:
                try:
                    patcher_for_attempt = replace(patcher, response_mode=target_mode)
                    patch_meta["adaptive_openai_response_mode_override"] = target_mode
                except Exception as exc:
                    patch_meta["adaptive_openai_response_mode_override_error"] = (
                        f"{type(exc).__name__}: {exc}"
                    )

        if is_structured_condition and bool(evidence.get("failure_signature_repeat")):
            patch_meta["retry_gate_evaluated"] = True
            patch_meta["retry_gate_blocked"] = False
            patch_meta["retry_gate_reason"] = "new_evidence_present"
            prev_evidence_fingerprint = ""
            if isinstance(previous_failure_context, dict):
                prev_evidence_fingerprint = str(
                    previous_failure_context.get("evidence_fingerprint") or ""
                )
            if prev_evidence_fingerprint and prev_evidence_fingerprint == evidence_fingerprint:
                patch_meta["retry_gate_blocked"] = True
                patch_meta["retry_gate_reason"] = "same_signature_without_new_evidence"
                patch_meta["retry_gate_previous_evidence_fingerprint"] = prev_evidence_fingerprint
                attempt_wall_sec = _measure_attempt_wall_clock_sec(attempt_start)
                cumulative_runtime_sec += attempt_wall_sec
                _finalize_adaptive_online_attempt(
                    note="retry_gate_no_new_evidence",
                    success=False,
                    attempt_wall_sec=attempt_wall_sec,
                    **online_finalize_kwargs,
                )
                record = _build_attempt_outcome_record(
                    case=case,
                    condition=condition,
                    run_id=run_id,
                    attempt=attempt,
                    patcher_name=patcher_name,
                    run_metadata=effective_run_metadata,
                    attempt_metadata=attempt_metadata,
                    patcher=patcher_for_attempt,
                    patch_meta=patch_meta,
                    context_protocol=attempt_context_protocol,
                    success=False,
                    note="retry_gate_no_new_evidence",
                    before=current,
                    cumulative_runtime_sec=cumulative_runtime_sec,
                    attempt_wall_clock_sec=attempt_wall_sec,
                )
                _append_record(record)
                _append_retry_history_turn_if_enabled(
                    retry_context_mode=effective_retry_context_mode,
                    retry_history_turns=retry_history_turns,
                    attempt=attempt,
                    note="retry_gate_no_new_evidence",
                    failure_signature=failure_signature,
                    patch_meta=patch_meta,
                )
                previous_failure_context = _enrich_failure_context(
                    base=current_failure_context,
                    note="retry_gate_no_new_evidence",
                    patch_meta=patch_meta,
                    run=current,
                )
                continue

        syntax_preflight_cycles_used = 0
        syntax_preflight_failures = 0
        syntax_preflight_last_error_type = ""
        syntax_preflight_last_error_msg = ""
        syntax_preflight_events: list[dict[str, Any]] = []
        syntax_preflight_max_cycles_for_attempt = (
            normalized_syntax_preflight_max_cycles if normalized_syntax_preflight_enabled else 1
        )
        syntax_retry_feedback = ""
        attempt_shared_propose_budget_sec = 0.0
        attempt_shared_propose_deadline_monotonic: float | None = None
        if is_openai_patcher:
            raw_attempt_budget_sec = getattr(patcher_for_attempt, "max_propose_sec", 0.0)
            try:
                attempt_shared_propose_budget_sec = max(0.0, float(raw_attempt_budget_sec))
            except (TypeError, ValueError):
                attempt_shared_propose_budget_sec = 0.0
            if attempt_shared_propose_budget_sec > 0:
                attempt_shared_propose_deadline_monotonic = (
                    time.monotonic() + attempt_shared_propose_budget_sec
                )
        attempt_metadata["attempt_shared_propose_budget_sec"] = attempt_shared_propose_budget_sec
        patch_meta["attempt_shared_propose_budget_sec"] = attempt_shared_propose_budget_sec
        attempt_llm_calls_cumulative = _nonnegative_int(
            attempt_metadata.get("llm_call_count"), default=0
        )
        attempt_tokens_prompt_cumulative = _nonnegative_int(
            attempt_metadata.get("tokens_prompt"), default=0
        )
        attempt_tokens_completion_cumulative = _nonnegative_int(
            attempt_metadata.get("tokens_completion"), default=0
        )
        attempt_tokens_total_cumulative = _nonnegative_int(
            attempt_metadata.get("tokens_total"), default=0
        )
        diff_to_apply = ""
        context_recovery_invoked = False
        attempt_terminated_early = False
        should_break_after_attempt = False
        patch_applied_and_ready = False

        _sync_syntax_preflight_metadata(
            attempt_metadata=attempt_metadata,
            patch_meta=patch_meta,
            enabled=normalized_syntax_preflight_enabled,
            cycles_used=syntax_preflight_cycles_used,
            failures=syntax_preflight_failures,
            last_error_type=syntax_preflight_last_error_type,
            last_error_msg=syntax_preflight_last_error_msg,
            events=syntax_preflight_events,
        )
        for syntax_cycle in range(1, syntax_preflight_max_cycles_for_attempt + 1):
            syntax_preflight_cycles_used = syntax_cycle
            _emit_progress(
                "attempt_propose_patch",
                {
                    "attempt": int(attempt),
                    "max_attempts": int(k),
                    "syntax_cycle": int(syntax_cycle),
                    "syntax_max_cycles": int(syntax_preflight_max_cycles_for_attempt),
                },
            )
            cycle_prompt = prompt + syntax_retry_feedback
            patch_start = time.perf_counter()
            patch = _propose_patch_with_snapshot_context(
                patcher=patcher_for_attempt,
                prompt=cycle_prompt,
                context_dir=attempt_dir,
                cwd=workdir,
                snapshot_full_text=snapshot_full_text,
                snapshot_artifact_text=snapshot_artifact_text,
                attempt_deadline_monotonic=attempt_shared_propose_deadline_monotonic,
            )
            patch_duration_sec = time.perf_counter() - patch_start
            patch_usage_meta: dict[str, Any] | None = None
            for key in (
                "llm_call_count",
                "tokens_prompt",
                "tokens_completion",
                "tokens_total",
                "usage",
            ):
                patch_meta.pop(key, None)
            for key in (
                "patch_diff_sha256",
                "patch_diff_chars",
                "patch_file_count",
                "patch_test_file_count",
                "patch_non_test_file_count",
                "patch_touches_tests",
                "patch_file_paths",
                "patch_test_file_paths",
                "patch_verifier_passed",
                "patch_verifier_score",
                "patch_verifier_reasons",
                "patch_verifier_patch_files",
                "patch_verifier_crash_file",
                "patch_verifier_crash_file_matched",
                "patch_verifier_crash_neighbor_matched",
                "patch_verifier_crash_neighbors",
                "patch_verifier_inferred_source_scope",
                "patch_verifier_source_scope_matched",
                "patch_verifier_failing_test_file",
                "patch_verifier_touches_test_file",
                "patch_verifier_touches_failing_test_file",
                "patch_verifier_substantive_change",
                "apply_duration_sec",
                "initial_apply_error",
                "runner_forced_patch_attempt",
                "runner_forced_patch_target",
                "runner_forced_patch_reason",
                "runner_forced_patch_build_sec",
                "runner_forced_patch_recovery",
                "runner_forced_patch_recovery_build_sec",
                "runner_forced_patch_recovery_apply_sec",
                "runner_forced_patch_recovery_target",
                "runner_forced_patch_recovery_reason",
                "runner_forced_patch_recovery_touches_tests",
                "runner_forced_patch_recovery_file_count",
                "runner_forced_patch_recovery_test_file_count",
                "runner_forced_patch_recovery_file_paths",
                "runner_forced_patch_recovery_error",
                "runner_forced_patch_skipped",
                "runner_forced_patch_skip_reason",
                "runner_forced_patch_marker_only",
                "runner_forced_patch_disabled_after_noop",
                "context_recovery_invoked",
                "context_recovery_kind",
                "context_recovery_prompt_chars_before",
                "context_recovery_prompt_chars_after",
                "context_recovery_duration_sec",
                "context_recovery_retry_success",
                "context_recovery_raw_meta",
                "no_diff_recovery_invoked",
                "no_diff_recovery_prompt_chars_before",
                "no_diff_recovery_prompt_chars_after",
                "no_diff_recovery_duration_sec",
                "no_diff_recovery_retry_success",
                "no_diff_recovery_raw_meta",
                "patcher_request_failed",
                "patcher_request_failure_kind",
                "patcher_request_failure_error",
                "patcher_request_failure_status",
                "patcher_request_failure_exc",
                "patch_sanitize_enabled",
                "patch_sanitize_passed",
                "patch_sanitize_rejected",
                "patch_sanitize_reasons",
                "patch_sanitize_error_msg",
                "adaptive_no_progress_token_sum",
                "adaptive_no_progress_token_cap",
                "adaptive_no_progress_token_cap_hit",
            ):
                patch_meta.pop(key, None)
            if isinstance(patch.meta, dict):
                patch_usage_meta = dict(patch.meta)
                patch_meta.update(patch_usage_meta)
            elif patch.meta is not None:
                patch_meta["raw_meta"] = patch.meta
            patch_meta["propose_duration_sec"] = patch_duration_sec
            patch_meta["syntax_preflight_cycle"] = syntax_cycle
            patch_meta["syntax_preflight_prompt_chars"] = len(cycle_prompt)
            _ensure_deterministic_seed_ack(
                deterministic=deterministic,
                seed=seed,
                patcher_name=patcher_name,
                patch_meta=patch_meta,
                case_id=case.case_id,
                condition=condition.value,
                attempt=attempt,
            )
            (
                cycle_llm_calls,
                cycle_tokens_prompt,
                cycle_tokens_completion,
                cycle_tokens_total,
            ) = _aggregate_attempt_llm_usage(
                stage_a_meta=None,
                patch_meta=patch_usage_meta,
                stage_a_default_llm_calls=0,
                patch_default_llm_calls=1 if is_openai_patcher else 0,
            )
            attempt_llm_calls_cumulative += cycle_llm_calls
            attempt_tokens_prompt_cumulative += cycle_tokens_prompt
            attempt_tokens_completion_cumulative += cycle_tokens_completion
            attempt_tokens_total_cumulative += cycle_tokens_total
            _apply_usage_totals_to_patch_meta(
                patch_meta=patch_meta,
                llm_call_count=attempt_llm_calls_cumulative,
                tokens_prompt=attempt_tokens_prompt_cumulative,
                tokens_completion=attempt_tokens_completion_cumulative,
                tokens_total=attempt_tokens_total_cumulative,
            )
            _sync_attempt_usage_metadata(
                attempt_metadata=attempt_metadata,
                patch_meta=patch_meta,
                llm_call_count=attempt_llm_calls_cumulative,
            )
            _sync_syntax_preflight_metadata(
                attempt_metadata=attempt_metadata,
                patch_meta=patch_meta,
                enabled=normalized_syntax_preflight_enabled,
                cycles_used=syntax_preflight_cycles_used,
                failures=syntax_preflight_failures,
                last_error_type=syntax_preflight_last_error_type,
                last_error_msg=syntax_preflight_last_error_msg,
                events=syntax_preflight_events,
            )
            _sync_patcher_request_failure_metadata(
                attempt_metadata=attempt_metadata,
                patch_meta=patch_meta,
                failed=False,
            )
            _sync_patch_sanitize_metadata(
                attempt_metadata=attempt_metadata,
                patch_meta=patch_meta,
                enabled=normalized_patch_sanitize_enabled,
                passed=False,
                rejected=False,
                reasons=[],
                error_msg="",
            )

            diff_to_apply = patch.diff
            request_failed, request_failure_kind = _classify_patcher_request_failure(patch_meta)
            request_failure_error = str(patch_meta.get("error") or "")
            request_failure_status = patch_meta.get("status")
            request_failure_status_int = (
                int(request_failure_status) if isinstance(request_failure_status, int) else None
            )
            request_failure_exc = str(patch_meta.get("exc") or "")
            if request_failed:
                _sync_patcher_request_failure_metadata(
                    attempt_metadata=attempt_metadata,
                    patch_meta=patch_meta,
                    failed=True,
                    kind=request_failure_kind,
                    error=request_failure_error,
                    status=request_failure_status_int,
                    exc=request_failure_exc,
                )
            if (
                diff_to_apply is None
                and request_failed
                and not context_recovery_invoked
                and request_failure_kind in CONTEXT_RECOVERY_FAILURE_KINDS
            ):
                context_recovery_invoked = True
                recovery_sections = _build_stage_b_prompt_sections(
                    evidence=evidence,
                    snapshot=snapshot_for_prompt,
                    stage_a_context=stage_a_context_text,
                    stage_a_evidence_index=stage_a_evidence_index_text,
                    include_stage_a_evidence_index=True,
                    compacted_context=compacted_context_for_prompt,
                    include_snapshot=include_snapshot_in_prompt,
                    include_pytest_output=include_pytest_output_in_prompt,
                    include_stage_a_context=include_stage_a_context_in_prompt,
                    include_project_files=False,
                    retry_packet=retry_packet,
                    retry_prompt_mode=attempt_retry_prompt_mode,
                    retry_history=retry_history_text,
                    attempt=attempt,
                    known_evidence_refs=reused_evidence_ids,
                    new_evidence_ids=new_evidence_ids,
                )
                recovery_budget_result = _apply_prompt_budget(
                    sections=recovery_sections,
                    budget=CONTEXT_RECOVERY_PROMPT_BUDGET,
                    is_delta_retry=is_delta_retry,
                )
                recovery_prompt = recovery_budget_result.prompt + syntax_retry_feedback
                recovery_patch_start = time.perf_counter()
                recovery_patch = _propose_patch_with_snapshot_context(
                    patcher=patcher_for_attempt,
                    prompt=recovery_prompt,
                    context_dir=attempt_dir,
                    cwd=workdir,
                    snapshot_full_text=snapshot_full_text,
                    snapshot_artifact_text=snapshot_artifact_text,
                    attempt_deadline_monotonic=attempt_shared_propose_deadline_monotonic,
                )
                recovery_patch_duration_sec = time.perf_counter() - recovery_patch_start

                recovery_usage_meta: dict[str, Any] | None = None
                for key in ("error", "status", "body", "exc"):
                    patch_meta.pop(key, None)
                if isinstance(recovery_patch.meta, dict):
                    recovery_usage_meta = dict(recovery_patch.meta)
                    patch_meta.update(recovery_usage_meta)
                elif recovery_patch.meta is not None:
                    patch_meta["context_recovery_raw_meta"] = recovery_patch.meta
                patch_meta["propose_duration_sec"] = (
                    patch_duration_sec + recovery_patch_duration_sec
                )
                patch_meta["context_recovery_invoked"] = True
                patch_meta["context_recovery_kind"] = request_failure_kind
                patch_meta["context_recovery_prompt_chars_before"] = len(cycle_prompt)
                patch_meta["context_recovery_prompt_chars_after"] = len(recovery_prompt)
                patch_meta["context_recovery_duration_sec"] = recovery_patch_duration_sec
                (
                    recovery_llm_calls,
                    recovery_tokens_prompt,
                    recovery_tokens_completion,
                    recovery_tokens_total,
                ) = _aggregate_attempt_llm_usage(
                    stage_a_meta=None,
                    patch_meta=recovery_usage_meta,
                    stage_a_default_llm_calls=0,
                    patch_default_llm_calls=1 if is_openai_patcher else 0,
                )
                attempt_llm_calls_cumulative += recovery_llm_calls
                attempt_tokens_prompt_cumulative += recovery_tokens_prompt
                attempt_tokens_completion_cumulative += recovery_tokens_completion
                attempt_tokens_total_cumulative += recovery_tokens_total
                _apply_usage_totals_to_patch_meta(
                    patch_meta=patch_meta,
                    llm_call_count=attempt_llm_calls_cumulative,
                    tokens_prompt=attempt_tokens_prompt_cumulative,
                    tokens_completion=attempt_tokens_completion_cumulative,
                    tokens_total=attempt_tokens_total_cumulative,
                )
                _sync_attempt_usage_metadata(
                    attempt_metadata=attempt_metadata,
                    patch_meta=patch_meta,
                    llm_call_count=attempt_llm_calls_cumulative,
                )
                _ensure_deterministic_seed_ack(
                    deterministic=deterministic,
                    seed=seed,
                    patcher_name=patcher_name,
                    patch_meta=patch_meta,
                    case_id=case.case_id,
                    condition=condition.value,
                    attempt=attempt,
                )
                diff_to_apply = recovery_patch.diff
                patch_meta["context_recovery_retry_success"] = diff_to_apply is not None
                request_failed, request_failure_kind = _classify_patcher_request_failure(patch_meta)
                request_failure_error = str(patch_meta.get("error") or "")
                request_failure_status = patch_meta.get("status")
                request_failure_status_int = (
                    int(request_failure_status) if isinstance(request_failure_status, int) else None
                )
                request_failure_exc = str(patch_meta.get("exc") or "")
                _sync_patcher_request_failure_metadata(
                    attempt_metadata=attempt_metadata,
                    patch_meta=patch_meta,
                    failed=request_failed,
                    kind=request_failure_kind if request_failed else "",
                    error=request_failure_error if request_failed else "",
                    status=request_failure_status_int if request_failed else None,
                    exc=request_failure_exc if request_failed else "",
                )
            if (
                diff_to_apply is None
                and is_openai_patcher
                and (not request_failed)
                and str(patch_meta.get("error") or "").strip()
            ):
                no_diff_recovery_sections = _build_stage_b_prompt_sections(
                    evidence=evidence,
                    snapshot=snapshot_for_prompt,
                    stage_a_context=stage_a_context_text,
                    stage_a_evidence_index=stage_a_evidence_index_text,
                    include_stage_a_evidence_index=True,
                    compacted_context=compacted_context_for_prompt,
                    include_snapshot=include_snapshot_in_prompt,
                    include_pytest_output=include_pytest_output_in_prompt,
                    include_stage_a_context=include_stage_a_context_in_prompt,
                    include_project_files=False,
                    retry_packet=retry_packet,
                    retry_prompt_mode=attempt_retry_prompt_mode,
                    retry_history=retry_history_text,
                    attempt=attempt,
                    known_evidence_refs=reused_evidence_ids,
                    new_evidence_ids=new_evidence_ids,
                )
                no_diff_recovery_sections.append(
                    {
                        "key": "no_diff_recovery",
                        "priority": 3,
                        "text": (
                            "Retry directive: the previous response did not produce an applicable "
                            "patch. Return one concrete minimal diff that changes source behavior "
                            "in traceback-linked non-test files whenever possible.\n"
                        ),
                    }
                )
                no_diff_recovery_budget_result = _apply_prompt_budget(
                    sections=no_diff_recovery_sections,
                    budget=CONTEXT_RECOVERY_PROMPT_BUDGET,
                    is_delta_retry=is_delta_retry,
                )
                no_diff_recovery_prompt = (
                    no_diff_recovery_budget_result.prompt + syntax_retry_feedback
                )
                no_diff_recovery_start = time.perf_counter()
                no_diff_recovery_patch = _propose_patch_with_snapshot_context(
                    patcher=patcher_for_attempt,
                    prompt=no_diff_recovery_prompt,
                    context_dir=attempt_dir,
                    cwd=workdir,
                    snapshot_full_text=snapshot_full_text,
                    snapshot_artifact_text=snapshot_artifact_text,
                    attempt_deadline_monotonic=attempt_shared_propose_deadline_monotonic,
                )
                no_diff_recovery_duration_sec = time.perf_counter() - no_diff_recovery_start

                no_diff_recovery_usage_meta: dict[str, Any] | None = None
                for key in ("error", "status", "body", "exc"):
                    patch_meta.pop(key, None)
                if isinstance(no_diff_recovery_patch.meta, dict):
                    no_diff_recovery_usage_meta = dict(no_diff_recovery_patch.meta)
                    patch_meta.update(no_diff_recovery_usage_meta)
                elif no_diff_recovery_patch.meta is not None:
                    patch_meta["no_diff_recovery_raw_meta"] = no_diff_recovery_patch.meta
                existing_propose_duration = float(
                    patch_meta.get("propose_duration_sec", patch_duration_sec) or patch_duration_sec
                )
                patch_meta["propose_duration_sec"] = (
                    existing_propose_duration + no_diff_recovery_duration_sec
                )
                patch_meta["no_diff_recovery_invoked"] = True
                patch_meta["no_diff_recovery_prompt_chars_before"] = len(cycle_prompt)
                patch_meta["no_diff_recovery_prompt_chars_after"] = len(no_diff_recovery_prompt)
                patch_meta["no_diff_recovery_duration_sec"] = no_diff_recovery_duration_sec
                (
                    no_diff_recovery_llm_calls,
                    no_diff_recovery_tokens_prompt,
                    no_diff_recovery_tokens_completion,
                    no_diff_recovery_tokens_total,
                ) = _aggregate_attempt_llm_usage(
                    stage_a_meta=None,
                    patch_meta=no_diff_recovery_usage_meta,
                    stage_a_default_llm_calls=0,
                    patch_default_llm_calls=1 if is_openai_patcher else 0,
                )
                attempt_llm_calls_cumulative += no_diff_recovery_llm_calls
                attempt_tokens_prompt_cumulative += no_diff_recovery_tokens_prompt
                attempt_tokens_completion_cumulative += no_diff_recovery_tokens_completion
                attempt_tokens_total_cumulative += no_diff_recovery_tokens_total
                _apply_usage_totals_to_patch_meta(
                    patch_meta=patch_meta,
                    llm_call_count=attempt_llm_calls_cumulative,
                    tokens_prompt=attempt_tokens_prompt_cumulative,
                    tokens_completion=attempt_tokens_completion_cumulative,
                    tokens_total=attempt_tokens_total_cumulative,
                )
                _sync_attempt_usage_metadata(
                    attempt_metadata=attempt_metadata,
                    patch_meta=patch_meta,
                    llm_call_count=attempt_llm_calls_cumulative,
                )
                _ensure_deterministic_seed_ack(
                    deterministic=deterministic,
                    seed=seed,
                    patcher_name=patcher_name,
                    patch_meta=patch_meta,
                    case_id=case.case_id,
                    condition=condition.value,
                    attempt=attempt,
                )
                diff_to_apply = no_diff_recovery_patch.diff
                patch_meta["no_diff_recovery_retry_success"] = diff_to_apply is not None
                request_failed, request_failure_kind = _classify_patcher_request_failure(patch_meta)
                request_failure_error = str(patch_meta.get("error") or "")
                request_failure_status = patch_meta.get("status")
                request_failure_status_int = (
                    int(request_failure_status) if isinstance(request_failure_status, int) else None
                )
                request_failure_exc = str(patch_meta.get("exc") or "")
                _sync_patcher_request_failure_metadata(
                    attempt_metadata=attempt_metadata,
                    patch_meta=patch_meta,
                    failed=request_failed,
                    kind=request_failure_kind if request_failed else "",
                    error=request_failure_error if request_failed else "",
                    status=request_failure_status_int if request_failed else None,
                    exc=request_failure_exc if request_failed else "",
                )
            if diff_to_apply is None and runner_force_patch_attempt_enabled:
                if request_failed:
                    _sync_patcher_request_failure_metadata(
                        attempt_metadata=attempt_metadata,
                        patch_meta=patch_meta,
                        failed=True,
                        kind=request_failure_kind,
                        error=request_failure_error,
                        status=request_failure_status_int,
                        exc=request_failure_exc,
                        runner_forced_patch_skipped=True,
                        runner_forced_patch_skip_reason="transport_or_context_failure",
                    )
                else:
                    force_start = time.perf_counter()
                    forced = _build_runner_forced_attempt_diff(
                        cwd=workdir, file_index=file_index, attempt=attempt
                    )
                    patch_meta["runner_forced_patch_build_sec"] = time.perf_counter() - force_start
                    if forced is not None:
                        forced_target, diff_to_apply = forced
                        patch_meta["runner_forced_patch_attempt"] = True
                        patch_meta["runner_forced_patch_target"] = forced_target
                        patch_meta["runner_forced_patch_reason"] = "no_patch_proposed"
            no_diff_note_override = ""
            if diff_to_apply is not None and _is_forced_noop_marker_diff(diff_to_apply):
                patch_meta["runner_forced_patch_marker_only"] = True
                patch_meta["runner_forced_patch_disabled_after_noop"] = True
                attempt_metadata["runner_forced_patch_marker_only"] = True
                attempt_metadata["runner_forced_patch_disabled_after_noop"] = True
                runner_force_patch_attempt_enabled = False
                diff_to_apply = None
                no_diff_note_override = "forced_noop_patch"
            if diff_to_apply is None:
                no_diff_note = (
                    no_diff_note_override
                    if no_diff_note_override
                    else ("patcher_request_failed" if request_failed else "no_patch_proposed")
                )
                (
                    adaptive_progress_stop_candidate_for_note,
                    adaptive_progress_stop_reason_for_note,
                    adaptive_no_progress_token_sum,
                    adaptive_no_progress_token_cap_hit,
                ) = _apply_no_progress_token_cap_candidate(
                    is_adaptive_condition=is_adaptive_condition,
                    adaptive_progress_made=adaptive_progress_made,
                    adaptive_no_progress_token_sum=adaptive_no_progress_token_sum,
                    attempt_tokens_total=_nonnegative_int(
                        attempt_metadata.get("tokens_total"), default=0
                    ),
                    adaptive_no_progress_token_cap=normalized_adaptive_no_progress_token_cap,
                    adaptive_progress_stop_candidate=adaptive_progress_stop_candidate,
                    adaptive_progress_stop_reason=adaptive_progress_stop_reason,
                )
                patch_meta["adaptive_no_progress_token_sum"] = adaptive_no_progress_token_sum
                patch_meta["adaptive_no_progress_token_cap"] = (
                    normalized_adaptive_no_progress_token_cap
                )
                patch_meta["adaptive_no_progress_token_cap_hit"] = (
                    adaptive_no_progress_token_cap_hit
                )
                adaptive_stop_triggered = _mark_adaptive_progress_stop(
                    attempt_metadata=attempt_metadata,
                    patch_meta=patch_meta,
                    is_adaptive_condition=is_adaptive_condition,
                    adaptive_progress_stop_candidate=adaptive_progress_stop_candidate_for_note,
                    adaptive_progress_stop_reason=adaptive_progress_stop_reason_for_note,
                    note=no_diff_note,
                )
                _sync_syntax_preflight_metadata(
                    attempt_metadata=attempt_metadata,
                    patch_meta=patch_meta,
                    enabled=normalized_syntax_preflight_enabled,
                    cycles_used=syntax_preflight_cycles_used,
                    failures=syntax_preflight_failures,
                    last_error_type=syntax_preflight_last_error_type,
                    last_error_msg=syntax_preflight_last_error_msg,
                    events=syntax_preflight_events,
                )
                attempt_wall_sec = _measure_attempt_wall_clock_sec(attempt_start)
                cumulative_runtime_sec += attempt_wall_sec
                _finalize_adaptive_online_attempt(
                    note=no_diff_note,
                    success=False,
                    attempt_wall_sec=attempt_wall_sec,
                    **online_finalize_kwargs,
                )
                record = _build_attempt_outcome_record(
                    case=case,
                    condition=condition,
                    run_id=run_id,
                    attempt=attempt,
                    patcher_name=patcher_name,
                    run_metadata=effective_run_metadata,
                    attempt_metadata=attempt_metadata,
                    patcher=patcher_for_attempt,
                    patch_meta=patch_meta,
                    context_protocol=attempt_context_protocol,
                    success=False,
                    note=no_diff_note,
                    before=current,
                    cumulative_runtime_sec=cumulative_runtime_sec,
                    attempt_wall_clock_sec=attempt_wall_sec,
                )
                _append_record(record)
                _append_retry_history_turn_if_enabled(
                    retry_context_mode=effective_retry_context_mode,
                    retry_history_turns=retry_history_turns,
                    attempt=attempt,
                    note=no_diff_note,
                    failure_signature=failure_signature,
                    patch_meta=patch_meta,
                )
                previous_failure_context = _enrich_failure_context(
                    base=current_failure_context,
                    note=no_diff_note,
                    patch_meta=patch_meta,
                    run=current,
                )
                attempt_terminated_early = True
                should_break_after_attempt = adaptive_stop_triggered
                break

            patch_meta["patch_diff_sha256"] = _sha256_text(diff_to_apply)
            patch_meta["patch_diff_chars"] = len(diff_to_apply)
            patch_meta.update(_compute_patch_path_metrics(diff_to_apply))
            patch_path = attempt_dir / "patch.diff"
            if syntax_cycle > 1:
                patch_path = attempt_dir / f"patch_cycle_{syntax_cycle:02d}.diff"
            patch_path.write_text(diff_to_apply, encoding="utf-8")

            patch_verifier_scope = (
                PATCH_VERIFIER_SCOPE_ADAPTIVE_ONLY
                if is_adaptive_condition
                else PATCH_VERIFIER_SCOPE_DISABLED
            )
            patch_verifier_enabled = patch_verifier_scope == PATCH_VERIFIER_SCOPE_ADAPTIVE_ONLY
            patch_verifier_passed: bool | None = None
            patch_verifier_score: int | None = None
            patch_verifier_reasons: list[str] = []
            if patch_verifier_enabled:
                patch_verifier_passed, patch_verifier_meta = _score_patch_relevance(
                    diff=diff_to_apply,
                    cwd=workdir,
                    snapshot=snapshot,
                    stdout=current.stdout,
                    stderr=current.stderr,
                    infer_test_source_scope=normalized_patch_verifier_infer_test_source_scope,
                    file_index_lines=file_index_lines,
                )
                patch_meta.update(patch_verifier_meta)
                patch_verifier_score = int(patch_verifier_meta.get("patch_verifier_score", 0))
                raw_reasons = patch_verifier_meta.get("patch_verifier_reasons")
                if isinstance(raw_reasons, list):
                    patch_verifier_reasons = [str(item) for item in raw_reasons]
            patch_meta["patch_verifier_enabled"] = patch_verifier_enabled
            patch_meta["patch_verifier_scope"] = patch_verifier_scope
            patch_meta["patch_verifier_passed"] = patch_verifier_passed
            patch_meta["patch_verifier_score"] = patch_verifier_score
            patch_meta["patch_verifier_reasons"] = patch_verifier_reasons
            attempt_metadata["patch_verifier_enabled"] = patch_verifier_enabled
            attempt_metadata["patch_verifier_scope"] = patch_verifier_scope
            attempt_metadata["patch_verifier_passed"] = patch_verifier_passed
            attempt_metadata["patch_verifier_score"] = patch_verifier_score
            attempt_metadata["patch_verifier_reasons"] = list(patch_verifier_reasons)
            _sync_syntax_preflight_metadata(
                attempt_metadata=attempt_metadata,
                patch_meta=patch_meta,
                enabled=normalized_syntax_preflight_enabled,
                cycles_used=syntax_preflight_cycles_used,
                failures=syntax_preflight_failures,
                last_error_type=syntax_preflight_last_error_type,
                last_error_msg=syntax_preflight_last_error_msg,
                events=syntax_preflight_events,
            )
            if patch_verifier_enabled and patch_verifier_passed is False:
                (
                    adaptive_progress_stop_candidate_for_note,
                    adaptive_progress_stop_reason_for_note,
                    adaptive_no_progress_token_sum,
                    adaptive_no_progress_token_cap_hit,
                ) = _apply_no_progress_token_cap_candidate(
                    is_adaptive_condition=is_adaptive_condition,
                    adaptive_progress_made=adaptive_progress_made,
                    adaptive_no_progress_token_sum=adaptive_no_progress_token_sum,
                    attempt_tokens_total=_nonnegative_int(
                        attempt_metadata.get("tokens_total"), default=0
                    ),
                    adaptive_no_progress_token_cap=normalized_adaptive_no_progress_token_cap,
                    adaptive_progress_stop_candidate=adaptive_progress_stop_candidate,
                    adaptive_progress_stop_reason=adaptive_progress_stop_reason,
                )
                patch_meta["adaptive_no_progress_token_sum"] = adaptive_no_progress_token_sum
                patch_meta["adaptive_no_progress_token_cap"] = (
                    normalized_adaptive_no_progress_token_cap
                )
                patch_meta["adaptive_no_progress_token_cap_hit"] = (
                    adaptive_no_progress_token_cap_hit
                )
                adaptive_stop_triggered = _mark_adaptive_progress_stop(
                    attempt_metadata=attempt_metadata,
                    patch_meta=patch_meta,
                    is_adaptive_condition=is_adaptive_condition,
                    adaptive_progress_stop_candidate=adaptive_progress_stop_candidate_for_note,
                    adaptive_progress_stop_reason=adaptive_progress_stop_reason_for_note,
                    note="patch_verifier_rejected",
                )
                attempt_wall_sec = _measure_attempt_wall_clock_sec(attempt_start)
                cumulative_runtime_sec += attempt_wall_sec
                _finalize_adaptive_online_attempt(
                    note="patch_verifier_rejected",
                    success=False,
                    attempt_wall_sec=attempt_wall_sec,
                    **online_finalize_kwargs,
                )
                record = _build_attempt_outcome_record(
                    case=case,
                    condition=condition,
                    run_id=run_id,
                    attempt=attempt,
                    patcher_name=patcher_name,
                    run_metadata=effective_run_metadata,
                    attempt_metadata=attempt_metadata,
                    patcher=patcher_for_attempt,
                    patch_meta=patch_meta,
                    context_protocol=attempt_context_protocol,
                    success=False,
                    note="patch_verifier_rejected",
                    before=current,
                    cumulative_runtime_sec=cumulative_runtime_sec,
                    attempt_wall_clock_sec=attempt_wall_sec,
                )
                _append_record(record)
                _append_retry_history_turn_if_enabled(
                    retry_context_mode=effective_retry_context_mode,
                    retry_history_turns=retry_history_turns,
                    attempt=attempt,
                    note="patch_verifier_rejected",
                    failure_signature=failure_signature,
                    patch_meta=patch_meta,
                )
                previous_failure_context = _enrich_failure_context(
                    base=current_failure_context,
                    note="patch_verifier_rejected",
                    patch_meta=patch_meta,
                    run=current,
                )
                attempt_terminated_early = True
                should_break_after_attempt = adaptive_stop_triggered
                break

            if normalized_patch_sanitize_enabled:
                sanitize_ok, sanitize_meta = _run_patch_sanitization(diff=diff_to_apply)
                sanitize_reasons_raw = sanitize_meta.get("patch_sanitize_reasons")
                sanitize_reasons = (
                    [str(item) for item in sanitize_reasons_raw]
                    if isinstance(sanitize_reasons_raw, list)
                    else []
                )
                sanitize_error_msg = str(sanitize_meta.get("patch_sanitize_error_msg") or "")
                _sync_patch_sanitize_metadata(
                    attempt_metadata=attempt_metadata,
                    patch_meta=patch_meta,
                    enabled=True,
                    passed=bool(sanitize_ok),
                    rejected=not bool(sanitize_ok),
                    reasons=sanitize_reasons,
                    error_msg=sanitize_error_msg,
                )
                if not sanitize_ok:
                    sanitize_checked_files_raw = sanitize_meta.get("patch_sanitize_checked_files")
                    sanitize_checked_files = (
                        [str(item) for item in sanitize_checked_files_raw]
                        if isinstance(sanitize_checked_files_raw, list)
                        else []
                    )
                    sanitize_error_file = (
                        sanitize_checked_files[0] if sanitize_checked_files else ""
                    )
                    syntax_event = {
                        "cycle": syntax_cycle,
                        "passed": False,
                        "syntax_preflight_checked_files": sanitize_checked_files,
                        "syntax_preflight_checked_count": int(
                            sanitize_meta.get("patch_sanitize_checked_count") or 0
                        ),
                        "syntax_preflight_error_file": sanitize_error_file,
                        "syntax_preflight_error_type": "PatchSanitizationError",
                        "syntax_preflight_error_msg": sanitize_error_msg,
                        "patch_sanitize_rejected": True,
                        "patch_sanitize_reasons": sanitize_reasons,
                    }
                    syntax_preflight_events.append(syntax_event)
                    syntax_preflight_failures += 1
                    syntax_preflight_last_error_type = "PatchSanitizationError"
                    syntax_preflight_last_error_msg = sanitize_error_msg
                    _sync_syntax_preflight_metadata(
                        attempt_metadata=attempt_metadata,
                        patch_meta=patch_meta,
                        enabled=normalized_syntax_preflight_enabled,
                        cycles_used=syntax_preflight_cycles_used,
                        failures=syntax_preflight_failures,
                        last_error_type=syntax_preflight_last_error_type,
                        last_error_msg=syntax_preflight_last_error_msg,
                        events=syntax_preflight_events,
                    )
                    if syntax_cycle < syntax_preflight_max_cycles_for_attempt:
                        if is_openai_patcher:
                            patcher_for_attempt, _ = _maybe_autoswitch_openai_response_mode(
                                patcher_for_attempt=patcher_for_attempt,
                                policy=normalized_openai_response_mode_autoswitch,
                                reason="patch_sanitize_rejected",
                                syntax_cycle=syntax_cycle,
                                patch_meta=patch_meta,
                                attempt_metadata=attempt_metadata,
                            )
                        syntax_retry_feedback = _render_patch_sanitize_retry_feedback(
                            cycle=syntax_cycle,
                            max_cycles=syntax_preflight_max_cycles_for_attempt,
                            sanitize_error=sanitize_error_msg,
                        )
                        continue

                    (
                        adaptive_progress_stop_candidate_for_note,
                        adaptive_progress_stop_reason_for_note,
                        adaptive_no_progress_token_sum,
                        adaptive_no_progress_token_cap_hit,
                    ) = _apply_no_progress_token_cap_candidate(
                        is_adaptive_condition=is_adaptive_condition,
                        adaptive_progress_made=adaptive_progress_made,
                        adaptive_no_progress_token_sum=adaptive_no_progress_token_sum,
                        attempt_tokens_total=_nonnegative_int(
                            attempt_metadata.get("tokens_total"), default=0
                        ),
                        adaptive_no_progress_token_cap=normalized_adaptive_no_progress_token_cap,
                        adaptive_progress_stop_candidate=adaptive_progress_stop_candidate,
                        adaptive_progress_stop_reason=adaptive_progress_stop_reason,
                    )
                    patch_meta["adaptive_no_progress_token_sum"] = adaptive_no_progress_token_sum
                    patch_meta["adaptive_no_progress_token_cap"] = (
                        normalized_adaptive_no_progress_token_cap
                    )
                    patch_meta["adaptive_no_progress_token_cap_hit"] = (
                        adaptive_no_progress_token_cap_hit
                    )
                    adaptive_stop_triggered = _mark_adaptive_progress_stop(
                        attempt_metadata=attempt_metadata,
                        patch_meta=patch_meta,
                        is_adaptive_condition=is_adaptive_condition,
                        adaptive_progress_stop_candidate=adaptive_progress_stop_candidate_for_note,
                        adaptive_progress_stop_reason=adaptive_progress_stop_reason_for_note,
                        note="syntax_preflight_failed",
                    )
                    attempt_wall_sec = _measure_attempt_wall_clock_sec(attempt_start)
                    cumulative_runtime_sec += attempt_wall_sec
                    _finalize_adaptive_online_attempt(
                        note="syntax_preflight_failed",
                        success=False,
                        attempt_wall_sec=attempt_wall_sec,
                        **online_finalize_kwargs,
                    )
                    record = _build_attempt_outcome_record(
                        case=case,
                        condition=condition,
                        run_id=run_id,
                        attempt=attempt,
                        patcher_name=patcher_name,
                        run_metadata=effective_run_metadata,
                        attempt_metadata=attempt_metadata,
                        patcher=patcher_for_attempt,
                        patch_meta=patch_meta,
                        context_protocol=attempt_context_protocol,
                        success=False,
                        note="syntax_preflight_failed",
                        before=current,
                        cumulative_runtime_sec=cumulative_runtime_sec,
                        attempt_wall_clock_sec=attempt_wall_sec,
                    )
                    _append_record(record)
                    _append_retry_history_turn_if_enabled(
                        retry_context_mode=effective_retry_context_mode,
                        retry_history_turns=retry_history_turns,
                        attempt=attempt,
                        note="syntax_preflight_failed",
                        failure_signature=failure_signature,
                        patch_meta=patch_meta,
                    )
                    previous_failure_context = _enrich_failure_context(
                        base=current_failure_context,
                        note="syntax_preflight_failed",
                        patch_meta=patch_meta,
                        run=current,
                    )
                    attempt_terminated_early = True
                    should_break_after_attempt = adaptive_stop_triggered
                    break

            file_backups = _backup_diff_target_files(cwd=workdir, diff=diff_to_apply)
            applied_diff_for_preflight = diff_to_apply
            apply_start = time.perf_counter()
            apply_ok, apply_msg = apply_diff(workdir, diff_to_apply)
            patch_meta["apply_duration_sec"] = time.perf_counter() - apply_start
            if not apply_ok and runner_force_patch_attempt_enabled:
                patch_meta["initial_apply_error"] = apply_msg
                recovery_build_start = time.perf_counter()
                recovery_patch = _build_runner_forced_attempt_diff(
                    cwd=workdir,
                    file_index=file_index,
                    attempt=attempt,
                )
                patch_meta["runner_forced_patch_recovery_build_sec"] = (
                    time.perf_counter() - recovery_build_start
                )
                if recovery_patch is not None:
                    recovery_target, recovery_diff = recovery_patch
                    (attempt_dir / "patch_recovery.diff").write_text(
                        recovery_diff, encoding="utf-8"
                    )
                    recovery_metrics = _compute_patch_path_metrics(recovery_diff)
                    recovery_backups = _backup_diff_target_files(cwd=workdir, diff=recovery_diff)
                    for rel, payload in recovery_backups.items():
                        file_backups.setdefault(rel, payload)
                    recovery_apply_start = time.perf_counter()
                    recovery_ok, recovery_msg = apply_diff(workdir, recovery_diff)
                    patch_meta["runner_forced_patch_recovery_apply_sec"] = (
                        time.perf_counter() - recovery_apply_start
                    )
                    patch_meta["runner_forced_patch_recovery"] = True
                    patch_meta["runner_forced_patch_recovery_target"] = recovery_target
                    patch_meta["runner_forced_patch_recovery_reason"] = "patch_apply_failed"
                    patch_meta["runner_forced_patch_recovery_touches_tests"] = bool(
                        recovery_metrics["patch_touches_tests"]
                    )
                    patch_meta["runner_forced_patch_recovery_file_count"] = int(
                        recovery_metrics["patch_file_count"]
                    )
                    patch_meta["runner_forced_patch_recovery_test_file_count"] = int(
                        recovery_metrics["patch_test_file_count"]
                    )
                    patch_meta["runner_forced_patch_recovery_file_paths"] = list(
                        recovery_metrics["patch_file_paths"]
                    )
                    if recovery_ok:
                        apply_ok = True
                        apply_msg = recovery_msg
                        applied_diff_for_preflight = recovery_diff
                    else:
                        patch_meta["runner_forced_patch_recovery_error"] = recovery_msg
            if not apply_ok:
                (
                    adaptive_progress_stop_candidate_for_note,
                    adaptive_progress_stop_reason_for_note,
                    adaptive_no_progress_token_sum,
                    adaptive_no_progress_token_cap_hit,
                ) = _apply_no_progress_token_cap_candidate(
                    is_adaptive_condition=is_adaptive_condition,
                    adaptive_progress_made=adaptive_progress_made,
                    adaptive_no_progress_token_sum=adaptive_no_progress_token_sum,
                    attempt_tokens_total=_nonnegative_int(
                        attempt_metadata.get("tokens_total"), default=0
                    ),
                    adaptive_no_progress_token_cap=normalized_adaptive_no_progress_token_cap,
                    adaptive_progress_stop_candidate=adaptive_progress_stop_candidate,
                    adaptive_progress_stop_reason=adaptive_progress_stop_reason,
                )
                patch_meta["adaptive_no_progress_token_sum"] = adaptive_no_progress_token_sum
                patch_meta["adaptive_no_progress_token_cap"] = (
                    normalized_adaptive_no_progress_token_cap
                )
                patch_meta["adaptive_no_progress_token_cap_hit"] = (
                    adaptive_no_progress_token_cap_hit
                )
                adaptive_stop_triggered = _mark_adaptive_progress_stop(
                    attempt_metadata=attempt_metadata,
                    patch_meta=patch_meta,
                    is_adaptive_condition=is_adaptive_condition,
                    adaptive_progress_stop_candidate=adaptive_progress_stop_candidate_for_note,
                    adaptive_progress_stop_reason=adaptive_progress_stop_reason_for_note,
                    note="patch_apply_failed",
                )
                _sync_syntax_preflight_metadata(
                    attempt_metadata=attempt_metadata,
                    patch_meta=patch_meta,
                    enabled=normalized_syntax_preflight_enabled,
                    cycles_used=syntax_preflight_cycles_used,
                    failures=syntax_preflight_failures,
                    last_error_type=syntax_preflight_last_error_type,
                    last_error_msg=syntax_preflight_last_error_msg,
                    events=syntax_preflight_events,
                )
                (attempt_dir / "apply_error.txt").write_text(apply_msg, encoding="utf-8")
                attempt_wall_sec = _measure_attempt_wall_clock_sec(attempt_start)
                cumulative_runtime_sec += attempt_wall_sec
                _finalize_adaptive_online_attempt(
                    note="patch_apply_failed",
                    success=False,
                    attempt_wall_sec=attempt_wall_sec,
                    **online_finalize_kwargs,
                )
                record = _build_attempt_outcome_record(
                    case=case,
                    condition=condition,
                    run_id=run_id,
                    attempt=attempt,
                    patcher_name=patcher_name,
                    run_metadata=effective_run_metadata,
                    attempt_metadata=attempt_metadata,
                    patcher=patcher_for_attempt,
                    patch_meta=patch_meta,
                    context_protocol=attempt_context_protocol,
                    success=False,
                    note="patch_apply_failed",
                    before=current,
                    cumulative_runtime_sec=cumulative_runtime_sec,
                    attempt_wall_clock_sec=attempt_wall_sec,
                )
                _append_record(record)
                _append_retry_history_turn_if_enabled(
                    retry_context_mode=effective_retry_context_mode,
                    retry_history_turns=retry_history_turns,
                    attempt=attempt,
                    note="patch_apply_failed",
                    failure_signature=failure_signature,
                    patch_meta=patch_meta,
                )
                previous_failure_context = _enrich_failure_context(
                    base=current_failure_context,
                    note="patch_apply_failed",
                    patch_meta=patch_meta,
                    run=current,
                )
                attempt_terminated_early = True
                should_break_after_attempt = adaptive_stop_triggered
                break

            _invalidate_file_catalog_cache()

            if normalized_syntax_preflight_enabled:
                syntax_preflight_passed, syntax_preflight_meta = _run_syntax_preflight(
                    cwd=workdir,
                    diff=applied_diff_for_preflight,
                )
                syntax_event = {
                    "cycle": syntax_cycle,
                    "passed": bool(syntax_preflight_passed),
                    **syntax_preflight_meta,
                }
                syntax_preflight_events.append(syntax_event)
                if syntax_preflight_passed:
                    _sync_syntax_preflight_metadata(
                        attempt_metadata=attempt_metadata,
                        patch_meta=patch_meta,
                        enabled=normalized_syntax_preflight_enabled,
                        cycles_used=syntax_preflight_cycles_used,
                        failures=syntax_preflight_failures,
                        last_error_type=syntax_preflight_last_error_type,
                        last_error_msg=syntax_preflight_last_error_msg,
                        events=syntax_preflight_events,
                    )
                    patch_applied_and_ready = True
                    break

                syntax_preflight_failures += 1
                syntax_preflight_last_error_type = str(
                    syntax_preflight_meta.get("syntax_preflight_error_type") or ""
                )
                syntax_preflight_last_error_msg = str(
                    syntax_preflight_meta.get("syntax_preflight_error_msg") or ""
                )
                _restore_diff_target_files(cwd=workdir, backups=file_backups)
                _sync_syntax_preflight_metadata(
                    attempt_metadata=attempt_metadata,
                    patch_meta=patch_meta,
                    enabled=normalized_syntax_preflight_enabled,
                    cycles_used=syntax_preflight_cycles_used,
                    failures=syntax_preflight_failures,
                    last_error_type=syntax_preflight_last_error_type,
                    last_error_msg=syntax_preflight_last_error_msg,
                    events=syntax_preflight_events,
                )
                if syntax_cycle < syntax_preflight_max_cycles_for_attempt:
                    if is_openai_patcher:
                        patcher_for_attempt, _ = _maybe_autoswitch_openai_response_mode(
                            patcher_for_attempt=patcher_for_attempt,
                            policy=normalized_openai_response_mode_autoswitch,
                            reason="syntax_preflight_failed",
                            syntax_cycle=syntax_cycle,
                            patch_meta=patch_meta,
                            attempt_metadata=attempt_metadata,
                        )
                    syntax_retry_feedback = _render_syntax_preflight_retry_feedback(
                        cycle=syntax_cycle,
                        max_cycles=syntax_preflight_max_cycles_for_attempt,
                        error_file=str(
                            syntax_preflight_meta.get("syntax_preflight_error_file") or ""
                        ),
                        error_type=syntax_preflight_last_error_type,
                        error_msg=syntax_preflight_last_error_msg,
                    )
                    continue

                (
                    adaptive_progress_stop_candidate_for_note,
                    adaptive_progress_stop_reason_for_note,
                    adaptive_no_progress_token_sum,
                    adaptive_no_progress_token_cap_hit,
                ) = _apply_no_progress_token_cap_candidate(
                    is_adaptive_condition=is_adaptive_condition,
                    adaptive_progress_made=adaptive_progress_made,
                    adaptive_no_progress_token_sum=adaptive_no_progress_token_sum,
                    attempt_tokens_total=_nonnegative_int(
                        attempt_metadata.get("tokens_total"), default=0
                    ),
                    adaptive_no_progress_token_cap=normalized_adaptive_no_progress_token_cap,
                    adaptive_progress_stop_candidate=adaptive_progress_stop_candidate,
                    adaptive_progress_stop_reason=adaptive_progress_stop_reason,
                )
                patch_meta["adaptive_no_progress_token_sum"] = adaptive_no_progress_token_sum
                patch_meta["adaptive_no_progress_token_cap"] = (
                    normalized_adaptive_no_progress_token_cap
                )
                patch_meta["adaptive_no_progress_token_cap_hit"] = (
                    adaptive_no_progress_token_cap_hit
                )
                adaptive_stop_triggered = _mark_adaptive_progress_stop(
                    attempt_metadata=attempt_metadata,
                    patch_meta=patch_meta,
                    is_adaptive_condition=is_adaptive_condition,
                    adaptive_progress_stop_candidate=adaptive_progress_stop_candidate_for_note,
                    adaptive_progress_stop_reason=adaptive_progress_stop_reason_for_note,
                    note="syntax_preflight_failed",
                )
                attempt_wall_sec = _measure_attempt_wall_clock_sec(attempt_start)
                cumulative_runtime_sec += attempt_wall_sec
                _finalize_adaptive_online_attempt(
                    note="syntax_preflight_failed",
                    success=False,
                    attempt_wall_sec=attempt_wall_sec,
                    **online_finalize_kwargs,
                )
                record = _build_attempt_outcome_record(
                    case=case,
                    condition=condition,
                    run_id=run_id,
                    attempt=attempt,
                    patcher_name=patcher_name,
                    run_metadata=effective_run_metadata,
                    attempt_metadata=attempt_metadata,
                    patcher=patcher_for_attempt,
                    patch_meta=patch_meta,
                    context_protocol=attempt_context_protocol,
                    success=False,
                    note="syntax_preflight_failed",
                    before=current,
                    cumulative_runtime_sec=cumulative_runtime_sec,
                    attempt_wall_clock_sec=attempt_wall_sec,
                )
                _append_record(record)
                _append_retry_history_turn_if_enabled(
                    retry_context_mode=effective_retry_context_mode,
                    retry_history_turns=retry_history_turns,
                    attempt=attempt,
                    note="syntax_preflight_failed",
                    failure_signature=failure_signature,
                    patch_meta=patch_meta,
                )
                previous_failure_context = _enrich_failure_context(
                    base=current_failure_context,
                    note="syntax_preflight_failed",
                    patch_meta=patch_meta,
                    run=current,
                )
                attempt_terminated_early = True
                should_break_after_attempt = adaptive_stop_triggered
                break

            syntax_preflight_events.append(
                {
                    "cycle": syntax_cycle,
                    "passed": True,
                    "syntax_preflight_checked_files": [],
                    "syntax_preflight_checked_count": 0,
                    "syntax_preflight_error_file": "",
                    "syntax_preflight_error_type": "",
                    "syntax_preflight_error_msg": "",
                }
            )
            _sync_syntax_preflight_metadata(
                attempt_metadata=attempt_metadata,
                patch_meta=patch_meta,
                enabled=normalized_syntax_preflight_enabled,
                cycles_used=syntax_preflight_cycles_used,
                failures=syntax_preflight_failures,
                last_error_type=syntax_preflight_last_error_type,
                last_error_msg=syntax_preflight_last_error_msg,
                events=syntax_preflight_events,
            )
            patch_applied_and_ready = True
            break

        _sync_syntax_preflight_metadata(
            attempt_metadata=attempt_metadata,
            patch_meta=patch_meta,
            enabled=normalized_syntax_preflight_enabled,
            cycles_used=syntax_preflight_cycles_used,
            failures=syntax_preflight_failures,
            last_error_type=syntax_preflight_last_error_type,
            last_error_msg=syntax_preflight_last_error_msg,
            events=syntax_preflight_events,
        )
        if attempt_terminated_early:
            if should_break_after_attempt:
                break
            continue
        if not patch_applied_and_ready:
            continue

        # Re-run after patch.
        shutil.rmtree(llmdebug_dir, ignore_errors=True)
        _emit_progress(
            "attempt_pytest_after",
            {
                "attempt": int(attempt),
                "max_attempts": int(k),
            },
        )
        after = run_pytest(workdir, case.python_args, condition, case.timeout_sec, output_format)
        _write_run_outputs(
            attempt_dir,
            after,
            prefix="pytest_after",
            tail_lines=normalized_pytest_output_tail_lines,
        )
        success = after.returncode == 0
        note = "success" if success else "tests_still_failing"
        adaptive_progress_stop_candidate_for_note = adaptive_progress_stop_candidate
        adaptive_progress_stop_reason_for_note = adaptive_progress_stop_reason
        if is_adaptive_condition and not success:
            after_snapshot, _ = read_latest_snapshot(llmdebug_dir)
            after_failure_signature = _build_failure_signature_for_attempt(
                run=after,
                snapshot=after_snapshot,
            )
            after_signature_repeated = bool(after_failure_signature == failure_signature)
            post_progress_made, post_progress_reason = _adaptive_progress_signal(
                signature_repeated=after_signature_repeated,
                new_evidence_count=0,
            )
            post_no_progress_streak = (
                0 if post_progress_made else adaptive_no_progress_streak_current
            )
            (
                adaptive_progress_stop_candidate_for_note,
                adaptive_progress_stop_reason_for_note,
            ) = _adaptive_should_stop_for_no_progress(
                no_progress_streak=post_no_progress_streak,
                stop_window=adaptive_no_progress_stop_window,
                helper_budget_exhausted=helper_budget_exhausted,
                helper_saturation_guarded=helper_saturation_guarded,
                helper_decision_reason=helper_decision_reason,
            )
            attempt_metadata["adaptive_progress_made"] = bool(post_progress_made)
            attempt_metadata["adaptive_progress_reason"] = str(post_progress_reason)
            attempt_metadata["adaptive_no_progress_streak"] = int(post_no_progress_streak)
            attempt_metadata["adaptive_progress_stop_candidate"] = bool(
                adaptive_progress_stop_candidate_for_note
            )
            attempt_metadata["adaptive_progress_stop_reason"] = str(
                adaptive_progress_stop_reason_for_note
            )
            patch_meta["adaptive_progress_recomputed_after_patch"] = True
            patch_meta["adaptive_postpatch_failure_signature"] = after_failure_signature
            patch_meta["adaptive_postpatch_signature_repeat"] = after_signature_repeated
            patch_meta["adaptive_progress_made"] = bool(post_progress_made)
            patch_meta["adaptive_progress_reason"] = str(post_progress_reason)
            patch_meta["adaptive_no_progress_streak"] = int(post_no_progress_streak)
            patch_meta["adaptive_progress_stop_candidate"] = bool(
                adaptive_progress_stop_candidate_for_note
            )
            patch_meta["adaptive_progress_stop_reason"] = str(
                adaptive_progress_stop_reason_for_note
            )
        (
            adaptive_progress_stop_candidate_for_note,
            adaptive_progress_stop_reason_for_note,
            adaptive_no_progress_token_sum,
            adaptive_no_progress_token_cap_hit,
        ) = _apply_no_progress_token_cap_candidate(
            is_adaptive_condition=is_adaptive_condition,
            adaptive_progress_made=bool(attempt_metadata.get("adaptive_progress_made")),
            adaptive_no_progress_token_sum=adaptive_no_progress_token_sum,
            attempt_tokens_total=_nonnegative_int(attempt_metadata.get("tokens_total"), default=0),
            adaptive_no_progress_token_cap=normalized_adaptive_no_progress_token_cap,
            adaptive_progress_stop_candidate=adaptive_progress_stop_candidate_for_note,
            adaptive_progress_stop_reason=adaptive_progress_stop_reason_for_note,
        )
        patch_meta["adaptive_no_progress_token_sum"] = adaptive_no_progress_token_sum
        patch_meta["adaptive_no_progress_token_cap"] = normalized_adaptive_no_progress_token_cap
        patch_meta["adaptive_no_progress_token_cap_hit"] = adaptive_no_progress_token_cap_hit
        adaptive_stop_triggered = _mark_adaptive_progress_stop(
            attempt_metadata=attempt_metadata,
            patch_meta=patch_meta,
            is_adaptive_condition=is_adaptive_condition,
            adaptive_progress_stop_candidate=adaptive_progress_stop_candidate_for_note,
            adaptive_progress_stop_reason=adaptive_progress_stop_reason_for_note,
            note=note,
        )
        attempt_wall_sec = _measure_attempt_wall_clock_sec(attempt_start)
        cumulative_runtime_sec += attempt_wall_sec
        _finalize_adaptive_online_attempt(
            note=note,
            success=success,
            attempt_wall_sec=attempt_wall_sec,
            **online_finalize_kwargs,
        )
        record = _build_attempt_outcome_record(
            case=case,
            condition=condition,
            run_id=run_id,
            attempt=attempt,
            patcher_name=patcher_name,
            run_metadata=effective_run_metadata,
            attempt_metadata=attempt_metadata,
            patcher=patcher_for_attempt,
            patch_meta=patch_meta,
            context_protocol=attempt_context_protocol,
            success=success,
            note=note,
            before=current,
            cumulative_runtime_sec=cumulative_runtime_sec,
            after=after,
            attempt_wall_clock_sec=attempt_wall_sec,
        )
        _append_record(record)
        _append_retry_history_turn_if_enabled(
            retry_context_mode=effective_retry_context_mode,
            retry_history_turns=retry_history_turns,
            attempt=attempt,
            note=note,
            failure_signature=failure_signature,
            patch_meta=patch_meta,
        )

        if success:
            break
        if adaptive_stop_triggered:
            break

        # Continue: next attempt uses this failure as evidence.
        previous_failure_context = _enrich_failure_context(
            base=current_failure_context,
            note=note,
            patch_meta=patch_meta,
            run=after,
        )
        current = after

    return records


def _run_pytest_pooled(
    *,
    cwd: Path,
    python_args: list[str],
    condition: Condition,
    timeout_sec: int,
    output_format: str,
    execution: ExecutionConfig,
    pool: _EvalContainerPool,
) -> RunResult:
    """Run pytest in a pooled container via ``exec_run``."""
    env = _base_pytest_env(
        output_format=output_format,
        snapshot_condition=_is_snapshot_condition(condition),
    )
    if execution.container_mount_repo_src:
        env["PYTHONPATH"] = "/repo_src"
    env.update(execution.container_env)

    command = _build_pytest_args(
        python_cmd="python",
        python_args=python_args,
        condition=condition,
    )

    container = pool.acquire()
    try:
        return container.exec_pytest(
            host_cwd=cwd,
            command=command,
            env=env,
            timeout_sec=timeout_sec,
        )
    finally:
        pool.release(container)


def run_pytest(
    cwd: Path,
    python_args: list[str],
    condition: Condition,
    timeout_sec: int,
    output_format: str,
    *,
    execution: ExecutionConfig | None = None,
) -> RunResult:
    active_execution = execution if execution is not None else get_active_execution_config()
    if active_execution.executor == "local":
        return _run_pytest_local(
            cwd=cwd,
            python_args=python_args,
            condition=condition,
            timeout_sec=timeout_sec,
            output_format=output_format,
        )
    if active_execution.executor == "testcontainers":
        pool = _ACTIVE_CONTAINER_POOL
        if pool is not None:
            return _run_pytest_pooled(
                cwd=cwd,
                python_args=python_args,
                condition=condition,
                timeout_sec=timeout_sec,
                output_format=output_format,
                execution=active_execution,
                pool=pool,
            )
        return _run_pytest_testcontainers(
            cwd=cwd,
            python_args=python_args,
            condition=condition,
            timeout_sec=timeout_sec,
            output_format=output_format,
            execution=active_execution,
        )
    raise RuntimeError(f"unsupported executor: {active_execution.executor!r}")


def _build_pytest_args(
    *, python_cmd: str, python_args: list[str], condition: Condition
) -> list[str]:
    args = [python_cmd, *python_args]
    if _is_snapshot_condition(condition):
        if "-m" in python_args and "pytest" in python_args:
            idx = python_args.index("pytest")
            args = [
                python_cmd,
                *python_args[: idx + 1],
                "-p",
                "llmdebug.pytest_plugin",
                *python_args[idx + 1 :],
            ]
        else:
            args = [python_cmd, *python_args, "-p", "llmdebug.pytest_plugin"]
    return args


def _base_pytest_env(*, output_format: str, snapshot_condition: bool) -> dict[str, str]:
    env = {
        "PYTEST_DISABLE_PLUGIN_AUTOLOAD": "1",
        "PYTHONDONTWRITEBYTECODE": "1",
        "LLMDEBUG_OUTPUT_FORMAT": output_format,
    }
    if _ACTIVE_RUN_SETTINGS.deterministic and _ACTIVE_RUN_SETTINGS.seed is not None:
        env["PYTHONHASHSEED"] = str(_ACTIVE_RUN_SETTINGS.seed)
        env.setdefault("TZ", "UTC")
    if snapshot_condition:
        env.setdefault("LLMDEBUG_DEBUG", "0")
        env.setdefault("LLMDEBUG_INCLUDE_GIT", "false")
        # Disable built-in hypothesis/category hints: the model should infer diagnosis itself.
        env["LLMDEBUG_CATEGORIZE_ERRORS"] = "0"
    return env


def _run_pytest_local(
    *,
    cwd: Path,
    python_args: list[str],
    condition: Condition,
    timeout_sec: int,
    output_format: str,
) -> RunResult:
    env = os.environ.copy()
    env["PYTHONPATH"] = os.pathsep.join([str(_SRC_ROOT), env.get("PYTHONPATH", "")]).strip(
        os.pathsep
    )
    env.update(
        _base_pytest_env(
            output_format=output_format,
            snapshot_condition=_is_snapshot_condition(condition),
        )
    )
    args = _build_pytest_args(
        python_cmd=sys.executable,
        python_args=python_args,
        condition=condition,
    )

    start = time.perf_counter()
    try:
        proc = subprocess.run(
            args,
            cwd=cwd,
            env=env,
            capture_output=True,
            text=True,
            timeout=timeout_sec,
        )
    except subprocess.TimeoutExpired as exc:
        end = time.perf_counter()
        partial_stdout = _coerce_subprocess_output(exc.stdout)
        partial_stderr = _coerce_subprocess_output(exc.stderr)
        timeout_message = f"pytest timed out after {timeout_sec}s"
        stderr = timeout_message if not partial_stderr else f"{partial_stderr}\n{timeout_message}"
        return RunResult(
            returncode=PYTEST_TIMEOUT_RETURNCODE,
            duration_sec=float(end - start),
            stdout=partial_stdout,
            stderr=stderr,
            timed_out=True,
            timeout_sec=timeout_sec,
        )
    end = time.perf_counter()
    return RunResult(
        returncode=int(proc.returncode),
        duration_sec=float(end - start),
        stdout=proc.stdout,
        stderr=proc.stderr,
        timed_out=False,
        timeout_sec=None,
    )


def _status_code_from_wait_result(result: Any) -> int:
    if isinstance(result, int):
        return result
    if isinstance(result, dict):
        raw = result.get("StatusCode")
        if isinstance(raw, int):
            return raw
        try:
            return int(raw)
        except Exception:
            return 1
    return 1


def _is_container_wait_timeout(exc: Exception) -> bool:
    name = type(exc).__name__
    if "ReadTimeout" in name:
        return True
    text = str(exc).lower()
    return "read timed out" in text or "timed out" in text


def _run_pytest_testcontainers(
    *,
    cwd: Path,
    python_args: list[str],
    condition: Condition,
    timeout_sec: int,
    output_format: str,
    execution: ExecutionConfig,
) -> RunResult:
    try:
        from testcontainers.core.container import DockerContainer
    except Exception as exc:
        raise RuntimeError(
            "testcontainers execution requested but dependency is missing "
            f"({type(exc).__name__}: {exc}). Install llmdebug[evals]."
        ) from exc

    container_env = _base_pytest_env(
        output_format=output_format,
        snapshot_condition=_is_snapshot_condition(condition),
    )
    if execution.container_mount_repo_src:
        container_env["PYTHONPATH"] = "/repo_src"
    container_env.update(execution.container_env)

    command = _build_pytest_args(
        python_cmd="python",
        python_args=python_args,
        condition=condition,
    )
    tmpfs_size = max(1, int(execution.container_tmpfs_size_mb))
    kwargs: dict[str, Any] = {
        "working_dir": "/workspace",
        "network_mode": execution.container_network,
        "read_only": True,
        "tmpfs": {"/tmp": f"rw,noexec,nosuid,nodev,size={tmpfs_size}m"},
        "cap_drop": ["ALL"],
        "security_opt": ["no-new-privileges:true"],
        "pids_limit": int(execution.container_pids_limit),
        "mem_limit": execution.container_memory,
        "nano_cpus": int(float(execution.container_cpus) * 1_000_000_000),
        "user": execution.container_user,
        "labels": {
            EVAL_CONTAINER_LABEL_KEY: EVAL_CONTAINER_LABEL_VALUE,
        },
    }

    container = (
        DockerContainer(execution.container_image)
        .with_volume_mapping(str(cwd), "/workspace", mode="rw")
        .with_command(command)
        .with_kwargs(**kwargs)
    )
    if execution.container_mount_repo_src:
        container = container.with_volume_mapping(str(_SRC_ROOT), "/repo_src", mode="ro")
    for key, value in sorted(container_env.items()):
        container = container.with_env(key, value)

    start = time.perf_counter()
    stdout = ""
    stderr = ""
    timed_out = False
    returncode = 1
    try:
        container.start()
        wrapped = container.get_wrapped_container()
        if wrapped is None:
            raise RuntimeError("testcontainer started without wrapped container handle")
        try:
            wait_result = wrapped.wait(timeout=timeout_sec)
            returncode = _status_code_from_wait_result(wait_result)
        except Exception as exc:
            if _is_container_wait_timeout(exc):
                timed_out = True
                returncode = PYTEST_TIMEOUT_RETURNCODE
                try:
                    wrapped.kill()
                except Exception:
                    pass
            else:
                raise RuntimeError(f"container wait failed: {type(exc).__name__}: {exc}") from exc

        out_raw, err_raw = container.get_logs()
        stdout = _coerce_subprocess_output(out_raw)
        stderr = _coerce_subprocess_output(err_raw)
    except RuntimeError:
        raise
    except Exception as exc:
        raise RuntimeError(
            "testcontainers execution failed: "
            f"{type(exc).__name__}: {exc} "
            f"(image={execution.container_image!r}, network={execution.container_network!r})"
        ) from exc
    finally:
        try:
            container.stop()
        except Exception:
            pass

    end = time.perf_counter()
    if timed_out:
        timeout_message = f"pytest timed out after {timeout_sec}s"
        stderr = timeout_message if not stderr else f"{stderr}\n{timeout_message}"
        return RunResult(
            returncode=PYTEST_TIMEOUT_RETURNCODE,
            duration_sec=float(end - start),
            stdout=stdout,
            stderr=stderr,
            timed_out=True,
            timeout_sec=timeout_sec,
        )
    return RunResult(
        returncode=int(returncode),
        duration_sec=float(end - start),
        stdout=stdout,
        stderr=stderr,
        timed_out=False,
        timeout_sec=None,
    )


def _truncate_persisted_pytest_output(
    text: str,
    *,
    tail_lines: int,
    hard_char_cap: int | None = None,
) -> str:
    normalized = text or ""
    normalized_tail_lines = max(0, int(tail_lines))
    if normalized_tail_lines > 0 and normalized:
        had_trailing_newline = normalized.endswith("\n")
        lines = normalized.splitlines()
        total_lines = len(lines)
        if total_lines > normalized_tail_lines:
            dropped_lines = total_lines - normalized_tail_lines
            normalized = "\n".join(lines[-normalized_tail_lines:])
            if had_trailing_newline:
                normalized += "\n"
            normalized = (
                f"...[llmdebug: truncated {dropped_lines} earlier line(s)]...\n{normalized}"
            )

    resolved_hard_char_cap = (
        PERSISTED_PYTEST_OUTPUT_HARD_CHAR_CAP if hard_char_cap is None else hard_char_cap
    )
    normalized_hard_char_cap = max(0, int(resolved_hard_char_cap))
    if normalized_hard_char_cap > 0 and len(normalized) > normalized_hard_char_cap:
        dropped_chars = len(normalized) - normalized_hard_char_cap
        normalized = (
            f"...[llmdebug: truncated {dropped_chars} earlier character(s)]...\n"
            f"{normalized[-normalized_hard_char_cap:]}"
        )
    return normalized


def _truncate_snapshot_string_for_artifact(value: str, *, max_chars: int) -> str:
    normalized_max_chars = max(0, int(max_chars))
    if normalized_max_chars <= 0 or len(value) <= normalized_max_chars:
        return value
    dropped_chars = len(value) - normalized_max_chars
    marker = f"...[llmdebug: snapshot string truncated {dropped_chars} chars]..."
    if normalized_max_chars <= len(marker):
        return marker[:normalized_max_chars]
    keep_chars = normalized_max_chars - len(marker)
    head_chars = keep_chars // 2
    tail_chars = keep_chars - head_chars
    return value[:head_chars] + marker + value[-tail_chars:]


def _truncate_snapshot_for_artifact(
    snapshot: dict[str, Any], *, max_string_chars: int
) -> dict[str, Any]:
    normalized_max_string_chars = max(0, int(max_string_chars))

    def _rewrite(value: Any) -> Any:
        if isinstance(value, str):
            return _truncate_snapshot_string_for_artifact(
                value, max_chars=normalized_max_string_chars
            )
        if isinstance(value, dict):
            return {key: _rewrite(item) for key, item in value.items()}
        if isinstance(value, list):
            return [_rewrite(item) for item in value]
        return value

    rewritten = _rewrite(snapshot)
    return rewritten if isinstance(rewritten, dict) else dict(snapshot)


def _write_run_outputs(
    out_dir: Path,
    run: RunResult,
    *,
    prefix: str,
    tail_lines: int = DEFAULT_PYTEST_OUTPUT_TAIL_LINES,
) -> None:
    stdout = _truncate_persisted_pytest_output(run.stdout or "", tail_lines=tail_lines)
    stderr = _truncate_persisted_pytest_output(run.stderr or "", tail_lines=tail_lines)
    (out_dir / f"{prefix}_stdout.txt").write_text(stdout, encoding="utf-8")
    (out_dir / f"{prefix}_stderr.txt").write_text(stderr, encoding="utf-8")
    combined = stdout + ("\n" if stdout and stderr else "") + stderr
    combined = _truncate_persisted_pytest_output(combined, tail_lines=0)
    (out_dir / f"{prefix}_combined.txt").write_text(combined, encoding="utf-8")


def _remaining_attempt_propose_budget_sec(deadline_monotonic: float | None) -> float | None:
    if deadline_monotonic is None:
        return None
    return max(0.0, float(deadline_monotonic) - time.monotonic())


def _build_attempt_budget_exhausted_patch_result(
    *,
    patcher: Any,
    remaining_budget_sec: float,
) -> Any:
    from evals.patchers.base import PatchResult

    meta: dict[str, Any] = {
        "error": "request_budget_exceeded",
        "effective_timeout_sec": 0.0,
        "remaining_budget_sec": max(0.0, float(remaining_budget_sec)),
        "llm_call_count": 0,
        "tokens_prompt": 0,
        "tokens_completion": 0,
        "tokens_total": 0,
        "usage": {
            "prompt_tokens": 0,
            "completion_tokens": 0,
            "total_tokens": 0,
        },
        "transport_retry_count": 0,
        "attempt_shared_propose_deadline_enabled": True,
        "attempt_shared_propose_budget_exhausted": True,
        "attempt_shared_propose_budget_remaining_sec": max(0.0, float(remaining_budget_sec)),
    }
    patcher_name = str(getattr(patcher, "name", "") or "")
    if patcher_name:
        meta["patcher"] = patcher_name
    model_name = str(getattr(patcher, "model", "") or "")
    if model_name:
        meta["model"] = model_name
    base_url = str(getattr(patcher, "base_url", "") or "")
    if base_url:
        meta["base_url"] = base_url
    response_mode = str(getattr(patcher, "response_mode", "") or "")
    if response_mode:
        meta["response_mode"] = response_mode
    return PatchResult(diff=None, meta=meta)


def _patcher_with_shared_attempt_budget(
    *,
    patcher: Any,
    attempt_deadline_monotonic: float | None,
) -> tuple[Any, float | None, float | None, bool]:
    remaining_budget_sec = _remaining_attempt_propose_budget_sec(attempt_deadline_monotonic)
    if remaining_budget_sec is None:
        return patcher, None, None, False

    if str(getattr(patcher, "name", "")).strip() != "openai":
        return patcher, remaining_budget_sec, None, False

    raw_max_propose_sec = getattr(patcher, "max_propose_sec", 0.0)
    try:
        configured_max_propose_sec = float(raw_max_propose_sec)
    except (TypeError, ValueError):
        configured_max_propose_sec = 0.0
    if configured_max_propose_sec <= 0:
        return patcher, remaining_budget_sec, None, False

    clamped_max_propose_sec = max(0.0, min(configured_max_propose_sec, remaining_budget_sec))
    if abs(clamped_max_propose_sec - configured_max_propose_sec) < 1e-6:
        return patcher, remaining_budget_sec, clamped_max_propose_sec, False

    try:
        clamped_patcher = replace(patcher, max_propose_sec=clamped_max_propose_sec)
    except Exception:
        return patcher, remaining_budget_sec, configured_max_propose_sec, False
    return clamped_patcher, remaining_budget_sec, clamped_max_propose_sec, True


def _propose_patch_with_snapshot_context(
    *,
    patcher: Any,
    prompt: str,
    context_dir: Path,
    cwd: Path,
    snapshot_full_text: str | None,
    snapshot_artifact_text: str | None,
    attempt_deadline_monotonic: float | None = None,
) -> Any:
    """Provide full snapshot context during propose, then restore artifact-capped file."""
    from evals.patchers.base import PatchResult

    (
        effective_patcher,
        remaining_budget_sec,
        call_budget_sec,
        budget_clamped,
    ) = _patcher_with_shared_attempt_budget(
        patcher=patcher,
        attempt_deadline_monotonic=attempt_deadline_monotonic,
    )
    if remaining_budget_sec is not None and remaining_budget_sec <= 0:
        return _build_attempt_budget_exhausted_patch_result(
            patcher=patcher,
            remaining_budget_sec=remaining_budget_sec,
        )

    def _call_propose_patch() -> Any:
        return effective_patcher.propose_patch(prompt=prompt, context_dir=context_dir, cwd=cwd)

    if (
        snapshot_full_text is None
        or snapshot_artifact_text is None
        or snapshot_full_text == snapshot_artifact_text
    ):
        result = _call_propose_patch()
    else:
        snapshot_path = context_dir / "snapshot.json"
        snapshot_path.write_text(snapshot_full_text, encoding="utf-8")
        try:
            result = _call_propose_patch()
        finally:
            snapshot_path.write_text(snapshot_artifact_text, encoding="utf-8")

    if not hasattr(result, "diff") or not hasattr(result, "meta"):
        return result

    result_meta = result.meta
    if not isinstance(result_meta, dict):
        return result
    enriched_meta = dict(result_meta)
    if remaining_budget_sec is not None:
        enriched_meta["attempt_shared_propose_deadline_enabled"] = True
        enriched_meta["attempt_shared_propose_budget_remaining_sec"] = remaining_budget_sec
        if call_budget_sec is not None:
            enriched_meta["attempt_shared_propose_call_budget_sec"] = call_budget_sec
        if budget_clamped:
            enriched_meta["attempt_shared_propose_budget_clamped"] = True
    return PatchResult(diff=result.diff, meta=enriched_meta)


def _coerce_subprocess_output(value: str | bytes | None) -> str:
    if value is None:
        return ""
    if isinstance(value, bytes):
        return value.decode("utf-8", errors="replace")
    return value


def _timeout_fields(run: RunResult) -> dict[str, Any]:
    if run.timed_out:
        return {
            "timed_out": True,
            "timeout_sec": run.timeout_sec,
        }
    return {}


def _enrich_failure_context(
    *,
    base: dict[str, Any],
    note: str,
    patch_meta: dict[str, Any] | None,
    run: RunResult,
) -> dict[str, Any]:
    enriched = dict(base)
    enriched["note"] = str(note or "")
    enriched["timed_out"] = bool(run.timed_out)
    enriched["timeout_sec"] = int(run.timeout_sec) if isinstance(run.timeout_sec, int) else None
    meta = patch_meta if isinstance(patch_meta, dict) else {}
    verifier_reasons_raw = meta.get("patch_verifier_reasons")
    verifier_reasons = (
        [str(item) for item in verifier_reasons_raw]
        if isinstance(verifier_reasons_raw, list)
        else []
    )
    enriched["patch_verifier_rejected"] = bool(meta.get("patch_verifier_enabled")) and (
        meta.get("patch_verifier_passed") is False
    )
    enriched["patch_verifier_reasons"] = verifier_reasons
    enriched["patch_verifier_scope"] = str(meta.get("patch_verifier_scope") or "")
    return enriched


def read_latest_snapshot(out_dir: Path) -> tuple[dict[str, Any] | None, str | None]:
    try:
        from llmdebug.output import get_latest_snapshot

        snap = get_latest_snapshot(str(out_dir))
        return snap, None
    except Exception as e:
        return None, f"{type(e).__name__}: {e}"


def _build_failure_signature_for_attempt(*, run: RunResult, snapshot: dict[str, Any] | None) -> str:
    try:
        from llmdebug.rca_state import build_failure_signature

        return build_failure_signature(snapshot, stdout=run.stdout, stderr=run.stderr)
    except Exception as e:
        sys.stderr.write(
            f"llmdebug eval: build_failure_signature failed, using text fallback: "
            f"{type(e).__name__}: {e}\n"
        )
        text = (run.stderr or "").strip() or (run.stdout or "").strip() or "unknown_failure"
        head = "\n".join([line for line in text.splitlines() if line.strip()][-3:])
        digest = hashlib.sha256(head.encode("utf-8")).hexdigest()[:16]
        return f"TextFailure:{digest}"


def _build_structured_previous_attempt_diff(
    previous_failure_context: dict[str, Any] | None,
    *,
    current_failure_context: dict[str, Any],
) -> dict[str, Any] | None:
    if not previous_failure_context:
        return None

    prev_attempt = int(previous_failure_context.get("attempt", 0))
    curr_attempt = int(current_failure_context.get("attempt", 0))
    prev_signature = str(previous_failure_context.get("failure_signature") or "")
    curr_signature = str(current_failure_context.get("failure_signature") or "")

    prev_snapshot = previous_failure_context.get("snapshot")
    curr_snapshot = current_failure_context.get("snapshot")
    if isinstance(prev_snapshot, dict) and isinstance(curr_snapshot, dict):
        try:
            from llmdebug.snapshot_diff import diff_snapshots, structured_diff_summary

            diff = diff_snapshots(prev_snapshot, curr_snapshot)
            structured = structured_diff_summary(diff)
            structured["attempt_prev"] = f"attempt_{prev_attempt:02d}"
            structured["attempt_curr"] = f"attempt_{curr_attempt:02d}"
            return structured
        except Exception as e:
            sys.stderr.write(
                f"llmdebug eval: structured diff failed, using text fallback: "
                f"{type(e).__name__}: {e}\n"
            )
            pass

    prev_output = (
        str(previous_failure_context.get("stderr") or "")
        + "\n"
        + str(previous_failure_context.get("stdout") or "")
    ).strip()
    curr_output = (
        str(current_failure_context.get("stderr") or "")
        + "\n"
        + str(current_failure_context.get("stdout") or "")
    ).strip()

    return {
        "attempt_prev": f"attempt_{prev_attempt:02d}",
        "attempt_curr": f"attempt_{curr_attempt:02d}",
        "failure_signature_same": prev_signature == curr_signature,
        "evidence_delta": {
            "new_frames": [],
            "dropped_frames": [],
            "message_changed": prev_output != curr_output,
        },
        "change_delta": {
            "touched_files": [],
            "touched_functions": [],
        },
        "hypothesis_delta": "unknown",
        "verification_delta": {
            "old_status": "unknown",
            "new_status": "unknown",
            "status_changed": False,
        },
    }


def _format_local_value(name: str, value: Any, meta: dict[str, Any] | None) -> str:
    """Format a single local variable with type/shape annotation."""
    meta = meta or {}
    type_str = str(meta.get("type", ""))
    # Strip 'builtins.' prefix for readability.
    type_str = type_str.removeprefix("builtins.")

    # Array-like: show shape and dtype prominently.
    if isinstance(value, dict) and "__array__" in value:
        shape = value.get("shape")
        dtype = value.get("dtype", "")
        arr_type = value.get("__array__", "ndarray")
        shape_str = f"({', '.join(str(s) for s in shape)})" if isinstance(shape, list) else "?"
        return f"    {name} = <{arr_type}, shape={shape_str}, dtype={dtype}>"

    # Scalar or small repr.
    try:
        repr_str = json.dumps(value, ensure_ascii=False, default=str)
    except Exception:
        repr_str = repr(value)
    # Truncate long values.
    if len(repr_str) > 120:
        repr_str = repr_str[:117] + "..."

    # Add type annotation.
    annotation = ""
    length = meta.get("len")
    shape = meta.get("shape")
    if shape is not None and isinstance(shape, list):
        annotation = f"  ({type_str}, shape=({', '.join(str(s) for s in shape)}))"
    elif length is not None:
        annotation = f"  ({type_str}, len={length})"
    elif type_str:
        annotation = f"  ({type_str})"

    return f"    {name} = {repr_str}{annotation}"


def _format_snapshot_section(snapshot: dict[str, Any], *, max_frames: int = 3) -> str:
    """Format a snapshot into readable text for the LLM prompt."""
    lines: list[str] = []
    lines.append("=== llmdebug Debug Snapshot ===")
    lines.append("")

    # Exception header.
    exc = snapshot.get("exception", {})
    exc_type = exc.get("type", "Unknown")
    exc_msg = exc.get("message", "")
    lines.append(f"Exception: {exc_type} — {exc_msg}")

    lines.append("")

    # Frames — only user code (file_rel is not None), up to max_frames.
    frames = snapshot.get("frames", [])
    shown = 0
    for i, frame in enumerate(frames):
        if shown >= max_frames:
            break
        # Skip framework internals (file_rel is None for site-packages).
        file_rel = frame.get("file_rel")
        if file_rel is None:
            continue

        label = "crash site" if i == 0 else "caller"
        func = frame.get("function", "?")
        line_no = frame.get("line", "?")
        code = frame.get("code", "")
        lines.append(f"Frame {shown} ({label}): {file_rel}:{line_no} in {func}()")
        if code:
            lines.append(f"  Code: {code}")

        # Locals with metadata.
        frame_locals = frame.get("locals", {})
        locals_meta = frame.get("locals_meta", {})
        if frame_locals:
            lines.append("  Locals:")
            for var_name, var_value in frame_locals.items():
                # Skip pytest-internal variables.
                if var_name.startswith("@"):
                    continue
                var_meta = locals_meta.get(var_name)
                lines.append(_format_local_value(var_name, var_value, var_meta))
        lines.append("")
        shown += 1

    lines.append("===")
    return "\n".join(lines)


def _format_snapshot_index_section(snapshot: dict[str, Any], *, max_frames: int = 6) -> str:
    """Format a snapshot index for helper decision prompts.

    This intentionally excludes locals/values to avoid biasing helper decisions and to keep the
    payload compact. The full snapshot is available in Stage-B when explicitly requested.
    """
    lines: list[str] = []
    lines.append("=== llmdebug Snapshot Index ===")

    exc = snapshot.get("exception", {})
    exc_type = exc.get("type", "Unknown")
    exc_msg = exc.get("message", "")
    lines.append(f"Exception: {exc_type} — {exc_msg}")
    lines.append("")

    frames_obj = snapshot.get("frames")
    frames = frames_obj if isinstance(frames_obj, list) else []
    shown = 0
    for i, frame in enumerate(frames):
        if shown >= max(0, int(max_frames)):
            break
        if not isinstance(frame, dict):
            continue
        file_rel = frame.get("file_rel")
        if file_rel is None:
            continue
        label = "crash site" if i == 0 else "caller"
        func = frame.get("function", "?")
        line_no = frame.get("line", "?")
        lines.append(f"Frame {shown} ({label}): {file_rel}:{line_no} in {func}()")
        shown += 1

    lines.append("===")
    return "\n".join(lines)


_GENERIC_SNAPSHOT_JUSTIFICATIONS = {
    "need more context",
    "insufficient context",
    "not enough info",
    "not enough information",
    "need snapshot",
    "need the snapshot",
    "need full snapshot",
    "need the full snapshot",
    "require snapshot",
    "require the snapshot",
    "require full snapshot",
    "require the full snapshot",
    "requires snapshot",
    "requires the snapshot",
    "requires full snapshot",
    "requires the full snapshot",
    "more context",
}


def _is_generic_snapshot_justification(text: str) -> bool:
    normalized = re.sub(r"\s+", " ", re.sub(r"[^a-z0-9]+", " ", text.lower())).strip()
    if not normalized:
        return True
    if normalized in _GENERIC_SNAPSHOT_JUSTIFICATIONS:
        return True
    if re.fullmatch(
        r"(need|require|requires)\s+(more\s+)?(context|info|information)(\s+please)?", normalized
    ):
        return True
    if re.fullmatch(
        r"(need|require|requires)\s+(the\s+)?(full\s+)?snapshot(\s+please)?",
        normalized,
    ):
        return True
    return bool(
        re.fullmatch(
            r"(insufficient|not\s+enough)\s+(context|info|information)",
            normalized,
        )
    )


_STAGE_A_CALIBRATION_EXAMPLES = (
    "Examples of correct decisions:\n"
    "\n"
    "Example 1 — traceback IS sufficient:\n"
    "  Exception: IndexError: list index out of range\n"
    "  Crash: utils.py:42 in get_last() → return items[length]\n"
    '  Diagnosis: "Off-by-one: items[length] should be items[length - 1]"\n'
    "  Missing fact: null → need_more_context=false\n"
    "\n"
    "Example 2 — traceback IS sufficient:\n"
    "  Exception: KeyError: 'user_id'\n"
    '  Crash: handlers.py:15 in process() → uid = data["user_id"]\n'
    "  Diagnosis: \"Dict key mismatch: key is likely 'userId' or 'id'\"\n"
    "  Missing fact: null → need_more_context=false\n"
    "\n"
    "Example 3 — traceback NOT sufficient:\n"
    "  Exception: AssertionError (no message)\n"
    "  Crash: test_parser.py:30 in test_parse() → assert result == expected\n"
    '  Diagnosis: "Cannot determine root cause: assertion has no message and crash is in test"\n'
    '  Missing fact: "parser.py source to see parse() implementation" → need_more_context=true\n'
)


def render_stage_a_request_prompt(
    *,
    evidence: dict[str, Any],
    snapshot: dict[str, Any] | None,
    helper_budget_total: int = 0,
    helper_budget_remaining: int = 0,
) -> str:
    repro = evidence.get("repro")
    stdout = evidence.get("stdout", "")
    stderr = evidence.get("stderr", "")
    failure_signature = evidence.get("failure_signature", "")
    previous_failure_signature = evidence.get("previous_failure_signature")
    failure_signature_repeat = bool(evidence.get("failure_signature_repeat"))
    previous_attempt_diff = evidence.get("previous_attempt_diff")
    file_index = evidence.get("file_index", "")

    parts: list[str] = []
    parts.append(
        "You are selecting additional debugging evidence before proposing a patch.\n"
        "\n"
        "STEP 1 — DIAGNOSE from traceback alone:\n"
        "Read the exception type, message, crash file/line, and test output below.\n"
        "Write a one-sentence root-cause hypothesis.\n"
        "Then name a specific fact that is MISSING from the traceback and would change your diagnosis,\n"
        "or write null if no fact is missing.\n"
        "\n"
        "STEP 2 — DECIDE:\n"
        "If missing_fact is null → set need_more_context=false, requests=[].\n"
        "If you named a missing fact → request ONLY that item (1 request, max 2).\n"
        "Only set need_snapshot=true if you require the full llmdebug snapshot (locals/state); "
        "prefer targeted frame requests.\n"
        "Set need_compact_context=true only when the current evidence is too large/noisy and "
        "a compacted evidence bundle is needed before patching.\n"
        "\n" + _STAGE_A_CALIBRATION_EXAMPLES + "\n"
        "Return STRICT JSON only with this schema:\n"
        "{\n"
        '  "diagnosis": "one-sentence root-cause hypothesis",\n'
        '  "missing_fact": "specific fact needed, or null",\n'
        '  "need_more_context": true,\n'
        '  "need_snapshot": false,\n'
        '  "need_compact_context": false,\n'
        '  "requests": [\n'
        '    {"type":"file","path":"relative/path.py"},\n'
        '    {"type":"search","path":"relative/path.py","pattern":"literal_text"},\n'
        '    {"type":"frame","index":1}\n'
        "  ],\n"
        '  "snapshot_justification":"one short sentence (only when need_snapshot=true)",\n'
        '  "compact_context_reason":"one short sentence (only when need_compact_context=true)",\n'
        '  "sufficiency_rationale":"one short sentence"\n'
        "}\n"
        "Allowed request types: file, search, frame."
    )
    if helper_budget_total > 0:
        parts.append(
            f"Budget: You have {helper_budget_remaining} helper call(s) remaining "
            f"(of {helper_budget_total} total). "
            "If you use it on low-value context, no further calls are available."
        )
    parts.append(f"Repro command:\n{repro}\n")
    parts.append("Pytest output (stdout):\n" + (stdout or "").rstrip() + "\n")
    if stderr:
        parts.append("Pytest output (stderr):\n" + stderr.rstrip() + "\n")
    if snapshot is not None:
        condition = str(evidence.get("condition") or "")
        is_adaptive = condition in {
            Condition.ADAPTIVE_GATED.value,
            Condition.ADAPTIVE_LLM_DISCRETION.value,
        }
        parts.append(
            _format_snapshot_index_section(snapshot)
            if is_adaptive
            else _format_snapshot_section(snapshot)
        )
    if failure_signature:
        parts.append(f"Current failure signature:\n{failure_signature}\n")
    if previous_failure_signature is not None:
        parts.append(f"Previous failure signature:\n{previous_failure_signature}\n")
        parts.append(f"Failure signature repeated:\n{failure_signature_repeat}\n")
    if previous_attempt_diff is not None:
        prompt_safe_diff = _prompt_safe_previous_attempt_diff(previous_attempt_diff)
        parts.append(
            "Structured diff vs previous failed attempt:\n"
            + json.dumps(prompt_safe_diff, indent=2, ensure_ascii=False)
            + "\n"
        )
    if file_index:
        parts.append(
            "Available files (use exact relative paths):\n" + str(file_index).rstrip() + "\n"
        )
    parts.append(
        "Do NOT propose code edits in this step. "
        "Return JSON only. Use need_more_context=false with requests=[] when evidence is sufficient."
    )
    return "\n".join(parts).rstrip() + "\n"


def render_stage_b_patch_prompt(
    *,
    evidence: dict[str, Any],
    snapshot: dict[str, Any] | None,
    stage_a_context: str = "",
    include_project_files: bool = True,
    retry_packet: str = "",
    retry_history: str = "",
    retry_prompt_mode: str = "full",
    attempt: int = 1,
    known_evidence_refs: list[str] | None = None,
    new_evidence_ids: list[str] | None = None,
    stage_a_evidence_index: str = "",
    include_stage_a_evidence_index: bool = True,
    compacted_context: str = "",
    include_snapshot: bool = True,
    include_pytest_output: bool = True,
    include_stage_a_context: bool = True,
) -> str:
    sections = _build_stage_b_prompt_sections(
        evidence=evidence,
        snapshot=snapshot,
        stage_a_context=stage_a_context,
        stage_a_evidence_index=stage_a_evidence_index,
        include_stage_a_evidence_index=include_stage_a_evidence_index,
        compacted_context=compacted_context,
        include_snapshot=include_snapshot,
        include_pytest_output=include_pytest_output,
        include_stage_a_context=include_stage_a_context,
        include_project_files=include_project_files,
        retry_packet=retry_packet,
        retry_history=retry_history,
        retry_prompt_mode=retry_prompt_mode,
        attempt=attempt,
        known_evidence_refs=known_evidence_refs,
        new_evidence_ids=new_evidence_ids,
    )
    return _join_prompt_sections(sections)


def _build_stage_b_prompt_sections(
    *,
    evidence: dict[str, Any],
    snapshot: dict[str, Any] | None,
    stage_a_context: str,
    stage_a_evidence_index: str,
    include_stage_a_evidence_index: bool,
    compacted_context: str,
    include_snapshot: bool,
    include_pytest_output: bool,
    include_stage_a_context: bool,
    include_project_files: bool,
    retry_packet: str,
    retry_history: str,
    retry_prompt_mode: str,
    attempt: int,
    known_evidence_refs: list[str] | None,
    new_evidence_ids: list[str] | None,
) -> list[dict[str, Any]]:
    repro = evidence.get("repro")
    stdout = evidence.get("stdout", "")
    stderr = evidence.get("stderr", "")
    failure_signature = evidence.get("failure_signature", "")
    previous_failure_signature = evidence.get("previous_failure_signature")
    failure_signature_repeat = bool(evidence.get("failure_signature_repeat"))
    previous_attempt_diff = evidence.get("previous_attempt_diff")
    timed_out = bool(evidence.get("timed_out"))
    timeout_sec_raw = evidence.get("timeout_sec")
    timeout_sec = int(timeout_sec_raw) if isinstance(timeout_sec_raw, int) else None
    previous_note = str(evidence.get("previous_note") or "")
    previous_patch_verifier_rejected = bool(evidence.get("previous_patch_verifier_rejected"))
    previous_patch_verifier_reasons_raw = evidence.get("previous_patch_verifier_reasons")
    previous_patch_verifier_reasons = (
        [str(item) for item in previous_patch_verifier_reasons_raw]
        if isinstance(previous_patch_verifier_reasons_raw, list)
        else []
    )
    previous_patch_verifier_scope = str(evidence.get("previous_patch_verifier_scope") or "")
    file_index = evidence.get("file_index", "")
    files = evidence.get("files") or ""
    has_previous = previous_failure_signature is not None
    is_delta_retry = retry_prompt_mode == "delta" and attempt > 1

    sections: list[dict[str, Any]] = []
    if is_delta_retry:
        sections.append(
            {
                "key": "instructions",
                "priority": 3,
                "text": (
                    "You are a coding assistant. This is a retry with delta-first protocol.\n"
                    "Use the retry packet and new evidence blocks first.\n"
                    "Prioritize non-test source files; edit tests only when evidence shows a test is wrong.\n"
                    "Ignore cosmetic CLI/TUI wording or formatting tweaks unless evidence requires them.\n"
                    "Avoid equivalent edits unless evidence changed.\n"
                    "Goal: produce the minimal edit that resolves the failing behavior."
                ),
            }
        )
    else:
        sections.append(
            {
                "key": "instructions",
                "priority": 3,
                "text": (
                    "You are a coding assistant. Fix the failing behavior shown by the tests.\n"
                    "Prioritize non-test source files; edit tests only when evidence shows a test is wrong.\n"
                    "Ignore cosmetic CLI/TUI wording or formatting tweaks unless evidence requires them.\n"
                    "Make the smallest behavior-changing edit (avoid extra refactors or added features).\n"
                    "Do not submit observability-only patches (logging/comments/assertions) unless required "
                    "for the fix.\n"
                    "Do not repeat an equivalent patch when the failure signature is unchanged."
                ),
            }
        )

    rca_lines = [
        "RCA checklist (internal reasoning only, do not print these sections):",
        "1) Observed failure with concrete evidence",
        "2) Falsifiable root-cause diagnosis",
        "3) Why evidence supports that diagnosis",
        "4) Minimal fix plan",
        "5) Verification plan",
    ]
    if has_previous:
        rca_lines.append("6) Diff vs previous failed attempt")
    sections.append({"key": "rca_checklist", "priority": 3, "text": "\n".join(rca_lines)})

    if retry_packet:
        sections.append(
            {"key": "retry_packet", "priority": 3, "text": retry_packet.rstrip() + "\n"}
        )
    if retry_history:
        sections.append(
            {
                "key": "retry_history",
                "priority": 2,
                "text": "Retry history from previous attempts:\n" + retry_history.rstrip() + "\n",
            }
        )

    sections.append({"key": "repro", "priority": 3, "text": f"Repro command:\n{repro}\n"})

    if not is_delta_retry and include_pytest_output:
        pytest_block = "Pytest output (stdout):\n" + (stdout or "").rstrip() + "\n"
        if stderr:
            pytest_block += "\nPytest output (stderr):\n" + stderr.rstrip() + "\n"
        sections.append({"key": "pytest_output", "priority": 2, "text": pytest_block})

    if not is_delta_retry and include_snapshot and snapshot is not None:
        sections.append(
            {"key": "snapshot", "priority": 2, "text": _format_snapshot_section(snapshot)}
        )
        sections.append(
            {
                "key": "distractor_mitigation",
                "priority": 2,
                "text": (
                    "The snapshot above contains many locals. Focus ONLY on variables mentioned "
                    "in the exception message, the crash line, or the test assertion. Other locals "
                    "are likely irrelevant distractors — ignore them."
                ),
            }
        )

    if failure_signature:
        sections.append(
            {
                "key": "failure_signatures",
                "priority": 3,
                "text": f"Current failure signature:\n{failure_signature}\n",
            }
        )
    if has_previous:
        sections.append(
            {
                "key": "failure_signatures",
                "priority": 3,
                "text": (
                    f"Previous failure signature:\n{previous_failure_signature}\n"
                    f"Failure signature repeated:\n{failure_signature_repeat}\n"
                ),
            }
        )

    if (not is_delta_retry) and previous_attempt_diff is not None:
        prompt_safe_diff = _prompt_safe_previous_attempt_diff(previous_attempt_diff)
        sections.append(
            {
                "key": "previous_attempt_diff",
                "priority": 2,
                "text": (
                    "Structured diff vs previous failed attempt:\n"
                    + json.dumps(prompt_safe_diff, indent=2, ensure_ascii=False)
                    + "\n"
                ),
            }
        )
    if failure_signature_repeat:
        sections.append(
            {
                "key": "loop_breaker",
                "priority": 3,
                "text": (
                    "Loop breaker: The signature repeated. "
                    "Change either diagnosis or evidence basis before retrying.\n"
                ),
            }
        )
    if timed_out:
        timeout_label = (
            f"{timeout_sec}s" if isinstance(timeout_sec, int) and timeout_sec > 0 else "timeout"
        )
        sections.append(
            {
                "key": "timeout_risk",
                "priority": 3,
                "text": (
                    "Timeout risk signal: the latest failing run timed out "
                    f"(budget={timeout_label}).\n"
                    "Prefer low-risk fixes that reduce pathological loops/retries, avoid added "
                    "blocking I/O, and preserve bounded behavior.\n"
                ),
            }
        )
    if previous_note == "patch_verifier_rejected" or previous_patch_verifier_rejected:
        reasons_text = (
            ", ".join(previous_patch_verifier_reasons[:6])
            if previous_patch_verifier_reasons
            else "insufficient source relevance"
        )
        scope_text = previous_patch_verifier_scope or "adaptive_only"
        sections.append(
            {
                "key": "verifier_retry_guidance",
                "priority": 3,
                "text": (
                    "Verifier-aware retry guidance: the previous candidate was rejected by patch "
                    f"verifier (scope={scope_text}; reasons={reasons_text}).\n"
                    "Prioritize traceback-linked non-test source edits first; treat test edits as "
                    "last resort unless evidence proves the test is wrong.\n"
                ),
            }
        )

    if not is_delta_retry and file_index:
        sections.append(
            {
                "key": "file_index",
                "priority": 1,
                "text": "Available files (use exact relative paths):\n"
                + str(file_index).rstrip()
                + "\n",
            }
        )
    if compacted_context:
        sections.append(
            {
                "key": "compacted_context",
                "priority": 2,
                "text": "Compacted context summary:\n" + compacted_context.rstrip() + "\n",
            }
        )
    if include_stage_a_context and stage_a_context:
        sections.append(
            {
                "key": "stage_a_context",
                "priority": 2,
                "text": "Stage-A requested context:\n" + stage_a_context.rstrip() + "\n",
            }
        )
    if include_stage_a_evidence_index and stage_a_evidence_index:
        sections.append(
            {
                "key": "stage_a_evidence_index",
                "priority": 1,
                "text": "Stage-A evidence index:\n" + stage_a_evidence_index.rstrip() + "\n",
            }
        )

    known_refs = [item for item in (known_evidence_refs or []) if item]
    if is_delta_retry and known_refs:
        sections.append(
            {
                "key": "known_evidence_refs",
                "priority": 1,
                "text": "Known evidence references:\n"
                + "\n".join(f"- {item}" for item in known_refs),
            }
        )
    if is_delta_retry and new_evidence_ids:
        sections.append(
            {
                "key": "new_evidence_ids",
                "priority": 2,
                "text": "New evidence IDs in this retry:\n"
                + "\n".join(f"- {item}" for item in new_evidence_ids),
            }
        )
    if include_project_files and files and not is_delta_retry:
        sections.append(
            {
                "key": "project_files",
                "priority": 0,
                "text": "Project files:\n" + str(files).rstrip() + "\n",
            }
        )

    sections.append(
        {
            "key": "closing",
            "priority": 2,
            "text": (
                "CRITICAL — YOUR PATCH MUST:\n"
                "- Target the ROOT CAUSE visible in the traceback, not the symptom.\n"
                "- If the traceback alone identifies the bug, ignore supplementary context.\n"
                "- Be the smallest correct edit (no refactoring, no added features).\n"
                "- Not repeat a previously failed equivalent patch."
            ),
        }
    )
    return sections


@dataclass(frozen=True)
class PromptBudgetResult:
    prompt: str
    used_chars: int
    overflow_chars: int
    dropped_sections: list[str]
    section_status: dict[str, str] = field(default_factory=dict)


def _join_prompt_sections(sections: list[dict[str, Any]]) -> str:
    parts = [
        str(section.get("text") or "").strip("\n") for section in sections if section.get("text")
    ]
    if not parts:
        return ""
    return "\n\n".join(parts).rstrip() + "\n"


def _truncate_text_for_section(*, key: str, text: str, max_chars: int) -> str:
    if max_chars <= 0:
        return ""
    if len(text) <= max_chars:
        return text
    if max_chars <= 64:
        return text[:max_chars]

    marker = "\n...[truncated]...\n"
    if len(marker) >= max_chars:
        return text[:max_chars]

    if key in {"pytest_output", "retry_packet"}:
        tail_chars = max_chars // 2
        head_chars = max_chars - tail_chars - len(marker)
        if head_chars < 0:
            return text[-max_chars:]
        return text[:head_chars] + marker + text[-tail_chars:]

    head_chars = max_chars - len(marker)
    return text[:head_chars] + marker


def _section_text_by_key(sections: list[dict[str, Any]]) -> dict[str, str]:
    by_key: dict[str, str] = {}
    for section in sections:
        key = str(section.get("key") or "")
        if not key:
            continue
        text = str(section.get("text") or "")
        if key in by_key:
            by_key[key] = by_key[key] + text
        else:
            by_key[key] = text
    return by_key


def _apply_prompt_budget(
    *,
    sections: list[dict[str, Any]],
    budget: dict[str, int],
    is_delta_retry: bool,
) -> PromptBudgetResult:
    capped_sections = [dict(section) for section in sections]
    original_by_key = _section_text_by_key(sections)
    per_section_limits = {
        "retry_packet": int(budget.get("retry_delta_chars", 0)),
        "pytest_output": int(budget.get("stdout_chars", 0)),
        "snapshot": int(budget.get("snapshot_chars", 0)),
        "compacted_context": int(budget.get("evidence_chars", 0)),
        "stage_a_context": int(budget.get("evidence_chars", 0)),
        "stage_a_evidence_index": int(budget.get("evidence_chars", 0)),
        "retry_history": int(budget.get("retry_history_chars", 0)),
    }

    for section in capped_sections:
        key = str(section.get("key") or "")
        text = str(section.get("text") or "")
        limit = per_section_limits.get(key, 0)
        if limit > 0:
            section["text"] = _truncate_text_for_section(key=key, text=text, max_chars=limit)

    prompt = _join_prompt_sections(capped_sections)
    total_budget = int(budget.get("total_chars", 0))
    if total_budget <= 0:
        final_by_key = _section_text_by_key(capped_sections)
        return PromptBudgetResult(
            prompt=prompt,
            used_chars=len(prompt),
            overflow_chars=0,
            dropped_sections=[],
            section_status={
                key: ("full" if final_by_key.get(key, "") == original else "truncated")
                for key, original in original_by_key.items()
                if original
            },
        )
    if len(prompt) <= total_budget:
        final_by_key = _section_text_by_key(capped_sections)
        return PromptBudgetResult(
            prompt=prompt,
            used_chars=len(prompt),
            overflow_chars=0,
            dropped_sections=[],
            section_status={
                key: ("full" if final_by_key.get(key, "") == original else "truncated")
                for key, original in original_by_key.items()
                if original
            },
        )

    overflow = len(prompt) - total_budget
    dropped_sections: list[str] = []
    drop_order = [
        "project_files",
        "file_index",
        "stage_a_evidence_index",
        "known_evidence_refs",
        "retry_history",
        "snapshot",
        "pytest_output",
        "compacted_context",
        "stage_a_context",
        "previous_attempt_diff",
    ]
    min_keep_by_key = {
        "snapshot": 240,
        "pytest_output": 240,
        "previous_attempt_diff": 240,
        "retry_history": 160,
        "compacted_context": 512 if is_delta_retry else 240,
        "stage_a_context": 512 if is_delta_retry else 240,
    }

    for key in drop_order:
        if overflow <= 0:
            break
        for section in capped_sections:
            if str(section.get("key") or "") != key:
                continue
            text = str(section.get("text") or "")
            if not text:
                continue
            min_keep = min(min_keep_by_key.get(key, 0), len(text))
            target = max(min_keep, len(text) - overflow)
            next_text = _truncate_text_for_section(key=key, text=text, max_chars=target)
            overflow -= max(0, len(text) - len(next_text))
            section["text"] = next_text
            if not next_text.strip():
                dropped_sections.append(key)
            break

    prompt = _join_prompt_sections(capped_sections)
    if len(prompt) > total_budget:
        prompt = _truncate_text_for_section(key="final", text=prompt, max_chars=total_budget)
    overflow_chars = max(0, len(prompt) - total_budget)
    final_by_key = _section_text_by_key(capped_sections)
    section_status: dict[str, str] = {}
    for key, original_text in original_by_key.items():
        if not original_text:
            continue
        final_text = final_by_key.get(key, "")
        if not final_text.strip():
            section_status[key] = "dropped"
        elif final_text == original_text:
            section_status[key] = "full"
        else:
            section_status[key] = "truncated"
    return PromptBudgetResult(
        prompt=prompt,
        used_chars=len(prompt),
        overflow_chars=overflow_chars,
        dropped_sections=sorted(set(dropped_sections)),
        section_status=section_status,
    )


def _build_context_compaction_bundle(
    *,
    scope: set[str],
    snapshot: dict[str, Any] | None,
    stdout: str,
    stderr: str,
    stage_a_context: str,
    project_files_context: str,
    max_input_chars: int,
) -> ContextCompactionBundle:
    parts: list[str] = []
    source_sections: list[str] = []
    source_chars: dict[str, int] = {}
    replace_snapshot = False
    replace_pytest_output = False
    replace_stage_a_context = False
    replace_project_files = False

    if "pytest" in scope:
        pytest_parts: list[str] = []
        if stdout:
            pytest_parts.append("Pytest output (stdout):\n" + stdout.rstrip())
        if stderr:
            pytest_parts.append("Pytest output (stderr):\n" + stderr.rstrip())
        if pytest_parts:
            pytest_text = "\n\n".join(pytest_parts).rstrip() + "\n"
            parts.append("=== pytest_output ===\n" + pytest_text)
            source_sections.append("pytest_output")
            source_chars["pytest_output"] = len(pytest_text)
            replace_pytest_output = True

    if "snapshot" in scope and snapshot is not None:
        snapshot_text = _format_snapshot_section(snapshot).rstrip() + "\n"
        parts.append("=== snapshot ===\n" + snapshot_text)
        source_sections.append("snapshot")
        source_chars["snapshot"] = len(snapshot_text)
        replace_snapshot = True

    if "files" in scope:
        if stage_a_context:
            stage_a_text = stage_a_context.rstrip() + "\n"
            parts.append("=== stage_a_context ===\n" + stage_a_text)
            source_sections.append("stage_a_context")
            source_chars["stage_a_context"] = len(stage_a_text)
            replace_stage_a_context = True
        if project_files_context:
            project_text = project_files_context.rstrip() + "\n"
            parts.append("=== project_files ===\n" + project_text)
            source_sections.append("project_files")
            source_chars["project_files"] = len(project_text)
            replace_project_files = True

    bundle_text = "\n\n".join(parts).rstrip()
    if bundle_text:
        bundle_text += "\n"
    if max_input_chars > 0 and len(bundle_text) > max_input_chars:
        bundle_text = _truncate_text_for_section(
            key="compaction_input",
            text=bundle_text,
            max_chars=max_input_chars,
        )
    return ContextCompactionBundle(
        text=bundle_text,
        source_sections=source_sections,
        source_chars=source_chars,
        replace_snapshot=replace_snapshot,
        replace_pytest_output=replace_pytest_output,
        replace_stage_a_context=replace_stage_a_context,
        replace_project_files=replace_project_files,
    )


def _should_invoke_context_compaction(
    *,
    mode: str,
    supports_context_compaction: bool,
    stage_a_requested: bool,
    auto_overflow_triggered: bool,
    calls_used: int,
    max_calls: int,
    bundle_input_chars: int,
) -> tuple[bool, str]:
    if mode == "off":
        return False, "mode_off"
    if not supports_context_compaction:
        return False, "patcher_unsupported"
    max_calls_safe = max(0, int(max_calls))
    if max_calls_safe <= 0:
        return False, "max_calls_disabled"
    if max(0, int(calls_used)) >= max_calls_safe:
        return False, "max_calls_reached"
    if max(0, int(bundle_input_chars)) <= 0:
        return False, "empty_input"
    if stage_a_requested:
        return True, "stage_a_requested"
    if mode == "always":
        return True, "mode_always"
    if mode == "auto_overflow" and auto_overflow_triggered:
        return True, "prompt_overflow"
    return False, "auto_no_overflow"


def render_context_compaction_prompt(
    *,
    evidence: dict[str, Any],
    attempt: int,
    reason: str,
    source_sections: list[str],
    target_chars: int,
) -> str:
    case_id = str(evidence.get("case_id") or "")
    condition = str(evidence.get("condition") or "")
    failure_signature = str(evidence.get("failure_signature") or "")
    repro = evidence.get("repro")
    source_list = ", ".join(source_sections) if source_sections else "none"
    reason_line = str(reason or "").strip()
    if not reason_line:
        reason_line = "prompt overflow or noisy evidence"
    return (
        "You compact debugging evidence for a downstream patching model.\n"
        "Return a compact, loss-aware summary with exact literals for key facts.\n"
        "Preserve crash file/line, assertion text, failing values, and critical paths.\n"
        "Do not suggest edits.\n\n"
        f"Case: {case_id}\n"
        f"Condition: {condition}\n"
        f"Attempt: {attempt}\n"
        f"Failure signature: {failure_signature}\n"
        f"Repro: {repro}\n"
        f"Reason: {reason_line}\n"
        f"Sources: {source_list}\n"
        f"Target chars: {max(1, int(target_chars))}\n"
    )


def render_prompt(*, evidence: dict[str, Any], snapshot: dict[str, Any] | None) -> str:
    """Backward-compatible prompt renderer (monolithic context protocol)."""
    return render_stage_b_patch_prompt(
        evidence=evidence,
        snapshot=snapshot,
        stage_a_context="",
        include_project_files=True,
    )


def _build_retry_history_section(
    *,
    prior_turns: list[str],
    max_chars: int,
) -> tuple[str, int, int, bool]:
    if not prior_turns:
        return "", 0, 0, False
    if max_chars <= 0:
        return "", 0, 0, True

    cleaned_turns = [str(turn).strip() for turn in prior_turns if str(turn).strip()]
    if not cleaned_turns:
        return "", 0, 0, False

    selected_turns: list[str] = []
    used_chars = 0
    truncated = False
    for turn in reversed(cleaned_turns):
        candidate = turn if not selected_turns else "\n\n" + turn
        if used_chars + len(candidate) <= max_chars:
            selected_turns.append(turn)
            used_chars += len(candidate)
            continue

        truncated = True
        if not selected_turns:
            clipped = turn[:max_chars].rstrip()
            if clipped:
                selected_turns.append(clipped)
                used_chars = len(clipped)
        break

    if not selected_turns:
        return "", 0, 0, truncated

    rendered = "\n\n".join(selected_turns).rstrip()
    return rendered, len(rendered), len(selected_turns), truncated


def _render_retry_history_turn(
    *,
    attempt: int,
    note: str,
    failure_signature: str,
    patch_meta: dict[str, Any] | None,
) -> str:
    def _compact(value: Any, *, max_chars: int = 220) -> str:
        text = re.sub(r"\s+", " ", str(value)).strip()
        if len(text) <= max_chars:
            return text
        suffix = "...[truncated]"
        keep = max(0, max_chars - len(suffix))
        return text[:keep].rstrip() + suffix

    def _safe_int(value: Any) -> int:
        try:
            return int(value)
        except (TypeError, ValueError):
            return 0

    meta = patch_meta if isinstance(patch_meta, dict) else {}
    lines = [
        f"Attempt {attempt}",
        f"Outcome: {note}",
    ]
    if failure_signature:
        lines.append(f"Failure signature: {_compact(failure_signature)}")

    retry_gate_reason = str(meta.get("retry_gate_reason") or "").strip()
    if retry_gate_reason:
        if meta.get("retry_gate_blocked") is True:
            lines.append(f"Retry gate: blocked ({_compact(retry_gate_reason)})")
        else:
            lines.append(f"Retry gate: {_compact(retry_gate_reason)}")

    patcher_reason = str(meta.get("reason") or "").strip()
    if patcher_reason:
        lines.append(f"Patcher reason: {_compact(patcher_reason)}")
    if bool(meta.get("patcher_request_failed")):
        kind = str(meta.get("patcher_request_failure_kind") or "transport")
        err = str(meta.get("patcher_request_failure_error") or "")
        lines.append(f"Patcher request failed: {kind} ({_compact(err)})")
    if bool(meta.get("runner_forced_patch_skipped")):
        reason = str(meta.get("runner_forced_patch_skip_reason") or "")
        lines.append(f"Runner forced patch skipped: {_compact(reason)}")

    patch_digest = str(meta.get("patch_diff_sha256") or "").strip()
    if patch_digest:
        patch_chars = _safe_int(meta.get("patch_diff_chars"))
        lines.append(f"Patch digest: {patch_digest} (chars={patch_chars})")
    patch_file_count = _safe_int(meta.get("patch_file_count"))
    patch_test_file_count = _safe_int(meta.get("patch_test_file_count"))
    if patch_file_count > 0:
        lines.append(
            "Patch touches tests: "
            f"{bool(meta.get('patch_touches_tests'))} "
            f"(test_files={patch_test_file_count}, total_files={patch_file_count})"
        )
    if bool(meta.get("patch_verifier_enabled")):
        verifier_verdict = meta.get("patch_verifier_passed")
        verifier_score = _safe_int(meta.get("patch_verifier_score"))
        if verifier_verdict is True:
            lines.append(f"Patch verifier: pass (score={verifier_score})")
        elif verifier_verdict is False:
            lines.append(f"Patch verifier: reject (score={verifier_score})")
        verifier_reasons = meta.get("patch_verifier_reasons")
        if isinstance(verifier_reasons, list) and verifier_reasons:
            rendered = ", ".join(_compact(item, max_chars=80) for item in verifier_reasons[:8])
            lines.append(f"Patch verifier reasons: {rendered}")

    apply_error = str(meta.get("initial_apply_error") or "").strip()
    if apply_error:
        lines.append(f"Initial apply error: {_compact(apply_error)}")

    if meta.get("runner_forced_patch_attempt"):
        lines.append("Runner forced patch attempt: true")
    if meta.get("runner_forced_patch_recovery"):
        lines.append("Runner forced patch recovery: true")
    if bool(meta.get("patch_sanitize_rejected")):
        sanitize_error = str(meta.get("patch_sanitize_error_msg") or "")
        lines.append(f"Patch sanitize: rejected ({_compact(sanitize_error)})")
    if meta.get("retry_delta_prompt"):
        lines.append("Retry delta prompt: true")

    new_count = _safe_int(meta.get("evidence_ids_new_count"))
    reused_count = _safe_int(meta.get("evidence_ids_reused_count"))
    sent_count = _safe_int(meta.get("evidence_ids_sent_count"))
    if new_count or reused_count or sent_count:
        lines.append(f"Evidence IDs: new={new_count}, reused={reused_count}, sent={sent_count}")

    propose_sec = meta.get("propose_duration_sec")
    if isinstance(propose_sec, (int, float)):
        lines.append(f"Propose duration sec: {float(propose_sec):.3f}")
    apply_sec = meta.get("apply_duration_sec")
    if isinstance(apply_sec, (int, float)):
        lines.append(f"Apply duration sec: {float(apply_sec):.3f}")

    return "\n".join(lines).strip()


def _render_retry_packet(
    *,
    failure_signature: str,
    previous_failure_signature: Any,
    failure_signature_repeat: bool,
    previous_attempt_diff: dict[str, Any] | None,
    timed_out: bool = False,
    timeout_sec: int | None = None,
    previous_note: str = "",
    previous_patch_verifier_rejected: bool = False,
    previous_patch_verifier_reasons: list[str] | None = None,
    previous_patch_verifier_scope: str = "",
) -> str:
    prompt_safe_diff = _prompt_safe_previous_attempt_diff(previous_attempt_diff)
    packet: dict[str, Any] = {
        "failure_signature": failure_signature,
        "previous_failure_signature": previous_failure_signature,
        "failure_signature_repeat": failure_signature_repeat,
        "previous_attempt_diff": prompt_safe_diff,
        "timed_out": bool(timed_out),
        "timeout_sec": int(timeout_sec) if isinstance(timeout_sec, int) else None,
        "previous_note": str(previous_note or ""),
        "previous_patch_verifier_rejected": bool(previous_patch_verifier_rejected),
        "previous_patch_verifier_reasons": list(previous_patch_verifier_reasons or []),
        "previous_patch_verifier_scope": str(previous_patch_verifier_scope or ""),
    }
    return (
        "Retry packet (delta-first, structured):\n"
        + json.dumps(packet, indent=2, ensure_ascii=False, default=str)
        + "\n"
    )


def _prompt_safe_previous_attempt_diff(
    previous_attempt_diff: dict[str, Any] | None,
) -> dict[str, Any] | None:
    if not isinstance(previous_attempt_diff, dict):
        return None

    def _rewrite(value: Any) -> Any:
        if isinstance(value, dict):
            out: dict[str, Any] = {}
            for key, child in value.items():
                mapped_key = "diagnosis_delta" if str(key) == "hypothesis_delta" else str(key)
                out[mapped_key] = _rewrite(child)
            return out
        if isinstance(value, list):
            return [_rewrite(item) for item in value]
        return value

    rewritten = _rewrite(previous_attempt_diff)
    if isinstance(rewritten, dict):
        return rewritten
    return None


def _compute_evidence_fingerprint(
    *,
    failure_signature: str,
    stage_a_blocks: list[dict[str, Any]],
    stage_a_context: str,
    structured_seed_blocks: list[dict[str, Any]],
) -> str:
    payload = {
        "failure_signature": failure_signature,
        "stage_a_blocks": stage_a_blocks,
        "stage_a_block_ids": [
            str(block.get("id") or "")
            for block in stage_a_blocks
            if isinstance(block, dict) and block.get("id")
        ],
        "structured_seed_blocks": structured_seed_blocks,
        "stage_a_context_sha256": _sha256_text(stage_a_context or ""),
    }
    return _sha256_jsonlike(payload)


def _build_evidence_item_id(*, kind: str, locator: str, text: str) -> str:
    payload = f"{kind}|{locator}|{_sha256_text(text)}"
    return "e_" + _sha256_text(payload)[:10]


def _annotate_evidence_text(*, evidence_id: str, kind: str, locator: str, text: str) -> str:
    header = f"[{evidence_id}] {kind}:{locator}".strip()
    body = str(text or "").rstrip()
    if not body:
        return header + "\n"
    return header + "\n" + body + "\n"


def _build_evidence_item(
    *,
    kind: str,
    locator: str,
    text: str,
    meta: dict[str, Any],
) -> dict[str, Any]:
    evidence_id = _build_evidence_item_id(kind=kind, locator=locator, text=text)
    annotated_text = _annotate_evidence_text(
        evidence_id=evidence_id,
        kind=kind,
        locator=locator,
        text=text,
    )
    return {
        "id": evidence_id,
        "type": kind,
        "locator": locator,
        "text": annotated_text,
        "chars": len(annotated_text),
        **meta,
    }


def _render_evidence_items(items: list[dict[str, Any]]) -> str:
    parts: list[str] = []
    for item in items:
        text = str(item.get("text") or "")
        if text:
            parts.append(text.rstrip())
    if not parts:
        return ""
    return "\n\n".join(parts).rstrip() + "\n"


def _render_evidence_index(items: list[dict[str, Any]]) -> str:
    lines: list[str] = []
    for item in items:
        if not isinstance(item, dict):
            continue
        evidence_id = str(item.get("id") or "").strip()
        if not evidence_id:
            continue
        kind = str(item.get("type") or "evidence").strip() or "evidence"
        locator = str(item.get("locator") or item.get("path") or "").strip()
        chars_value = item.get("chars")
        chars_suffix = ""
        if isinstance(chars_value, int) and chars_value > 0:
            chars_suffix = f" ({chars_value} chars)"
        if locator:
            lines.append(f"- {evidence_id}: {kind}:{locator}{chars_suffix}")
        else:
            lines.append(f"- {evidence_id}: {kind}{chars_suffix}")
    if not lines:
        return ""
    return "\n".join(lines).rstrip() + "\n"


def _merge_evidence_items(*groups: list[dict[str, Any]]) -> list[dict[str, Any]]:
    out: list[dict[str, Any]] = []
    seen: set[str] = set()
    for group in groups:
        for item in group:
            if not isinstance(item, dict):
                continue
            evidence_id = str(item.get("id") or "")
            if not evidence_id or evidence_id in seen:
                continue
            seen.add(evidence_id)
            out.append(item)
    return out


def _evidence_ids_from_items(items: list[dict[str, Any]]) -> list[str]:
    return [
        str(item.get("id") or "") for item in items if isinstance(item, dict) and item.get("id")
    ]


def _coerce_payload_bool(value: Any) -> bool:
    if isinstance(value, bool):
        return value
    if value is None:
        return False
    if isinstance(value, (int, float)):
        return bool(value)
    if isinstance(value, str):
        normalized = value.strip().lower()
        if normalized in {"true", "1", "yes", "on"}:
            return True
        return normalized not in {"false", "0", "no", "off", ""}
    return bool(value)


def _coerce_context_request_payload(value: Any) -> ContextRequestPayload | None:
    if not isinstance(value, dict):
        return None
    requests_raw = value.get("requests")
    if requests_raw is not None and not isinstance(requests_raw, list):
        return None
    request_entries: list[dict[str, Any]] = []
    if isinstance(requests_raw, list):
        for entry in requests_raw:
            if not isinstance(entry, dict):
                continue
            request_type_raw = entry.get("type")
            if not isinstance(request_type_raw, str) or not request_type_raw.strip():
                continue
            request_type = request_type_raw.strip()
            normalized: dict[str, Any] = {"type": request_type}
            for key in ("path", "pattern"):
                raw = entry.get(key)
                if isinstance(raw, str) and raw:
                    normalized[key] = raw
            if request_type == "frame":
                index = entry.get("index")
                if not isinstance(index, int) or isinstance(index, bool):
                    continue
                normalized["index"] = index
            if normalized:
                request_entries.append(normalized)
    payload: ContextRequestPayload = {
        "need_more_context": _coerce_payload_bool(value.get("need_more_context")),
        "need_snapshot": _coerce_payload_bool(value.get("need_snapshot")),
        "need_compact_context": _coerce_payload_bool(value.get("need_compact_context")),
        "requests": cast(Any, request_entries),
    }
    snapshot_justification = value.get("snapshot_justification")
    if isinstance(snapshot_justification, str) and snapshot_justification:
        payload["snapshot_justification"] = snapshot_justification
    compact_context_reason = value.get("compact_context_reason")
    if isinstance(compact_context_reason, str) and compact_context_reason:
        payload["compact_context_reason"] = compact_context_reason
    rationale = value.get("sufficiency_rationale")
    if isinstance(rationale, str) and rationale:
        payload["sufficiency_rationale"] = rationale
    diagnosis = value.get("diagnosis")
    if isinstance(diagnosis, str) and diagnosis.strip():
        payload["diagnosis"] = diagnosis.strip()
    missing_fact = value.get("missing_fact")
    if missing_fact is None and "missing_fact" in value:
        payload["missing_fact"] = None  # Explicitly null = "none needed"
    elif isinstance(missing_fact, str) and missing_fact.strip():
        payload["missing_fact"] = missing_fact.strip()
    return payload


def _normalize_context_rel_path(raw_path: str) -> str | None:
    candidate = raw_path.strip()
    if not candidate:
        return None
    candidate = candidate.replace("\\", "/")
    if candidate.startswith("/") or re.match(r"^[A-Za-z]:($|/)", candidate):
        return None

    normalized_parts: list[str] = []
    for part in candidate.split("/"):
        if part in {"", "."}:
            continue
        if part == "..":
            return None
        normalized_parts.append(part)
    if not normalized_parts:
        return None
    return "/".join(normalized_parts)


def _iter_file_index_lines(file_index: str | Sequence[str]) -> Iterator[str]:
    if isinstance(file_index, str):
        yield from file_index.splitlines()
        return
    for line in file_index:
        yield str(line)


def _read_text_limited(path: Path, *, max_chars: int) -> str | None:
    """Read at most ``max_chars`` chars from a text file (+truncation marker)."""
    cap = max(1, int(max_chars))
    try:
        with path.open(encoding="utf-8", errors="replace") as handle:
            text = handle.read(cap + 1)
    except Exception:
        return None

    if len(text) <= cap:
        return text
    return text[:cap] + "\n...[TRUNC]\n"


def _literal_search_matches_in_file(
    path: Path,
    pattern: str,
    *,
    max_matches: int,
    max_scan_chars: int,
) -> tuple[list[str], bool]:
    """Stream literal search over file content with bounded scan size."""
    if not pattern:
        return [], False

    out: list[str] = []
    scanned_chars = 0
    scan_cap = max(1, int(max_scan_chars))
    try:
        with path.open(encoding="utf-8", errors="replace") as handle:
            for idx, line in enumerate(handle, start=1):
                scanned_chars += len(line)
                if pattern in line:
                    rendered = line.rstrip()
                    if len(rendered) > 220:
                        rendered = rendered[:217] + "..."
                    out.append(f"{idx}: {rendered}")
                    if len(out) >= max_matches:
                        return out, scanned_chars >= scan_cap
                if scanned_chars >= scan_cap:
                    return out, True
    except Exception:
        return [], False
    return out, False


def _collect_file_index_lines(root: Path) -> list[str]:
    include_suffixes = {".py", ".txt", ".json", ".md"}
    exclude_dirs = {".llmdebug", "__pycache__", ".venv", ".git"}
    collected: list[str] = []

    for dirpath, dirnames, filenames in os.walk(root):
        dirnames[:] = sorted(name for name in dirnames if name not in exclude_dirs)
        dirpath_path = Path(dirpath)
        for filename in sorted(filenames):
            path = dirpath_path / filename
            if path.suffix not in include_suffixes:
                continue
            try:
                rel = str(path.relative_to(root))
            except Exception:
                rel = str(path)
            collected.append(rel)
    collected.sort()
    return collected


def execute_context_requests(
    *,
    request_payload: ContextRequestPayload,
    root: Path,
    snapshot: dict[str, Any] | None,
    file_index: str | Sequence[str],
    stdout: str,
    stderr: str,
    max_requests: int,
    max_total_chars: int,
) -> StageAContextData:
    file_index_lines = tuple(_iter_file_index_lines(file_index))
    requests_raw = request_payload.get("requests")
    if not isinstance(requests_raw, list):
        return build_fallback_context_bundle(
            root=root,
            snapshot=snapshot,
            stdout=stdout,
            stderr=stderr,
            max_total_chars=max_total_chars,
            reason="malformed_stage_a_requests",
            file_index_lines=file_index_lines,
        )

    need_more_context = _coerce_payload_bool(request_payload.get("need_more_context"))
    if not need_more_context:
        return StageAContextData(
            blocks=[],
            text="",
            used_fallback=False,
            requests_count=0,
            chars_returned=0,
            request_success=True,
            request_payload=request_payload,
            request_raw=None,
            reason="sufficient_context",
        )

    allowed_paths: dict[str, str] = {}
    for line in file_index_lines:
        normalized = _normalize_context_rel_path(line)
        if normalized and normalized not in allowed_paths:
            allowed_paths[normalized] = normalized
    capped = requests_raw[: max(1, max_requests)]
    blocks: list[dict[str, Any]] = []
    evidence_items: list[dict[str, Any]] = []
    seen_evidence_ids: set[str] = set()
    total_chars = 0
    accepted = 0
    max_per_block = 40_000

    for req in capped:
        if not isinstance(req, dict):
            continue
        kind = str(req.get("type") or "").strip()
        if kind == "file":
            normalized_rel_path = _normalize_context_rel_path(str(req.get("path") or ""))
            if normalized_rel_path is None:
                continue
            rel_path = allowed_paths.get(normalized_rel_path)
            if rel_path is None:
                continue
            path = root / rel_path
            if not path.exists() or not path.is_file():
                continue
            text = _read_text_limited(path, max_chars=max_per_block)
            if text is None:
                continue
            block_text = f"--- {rel_path} ---\n{text.rstrip()}\n"
            if total_chars + len(block_text) > max_total_chars:
                break
            evidence_item = _build_evidence_item(
                kind="file",
                locator=rel_path,
                text=block_text,
                meta={"path": rel_path},
            )
            evidence_id = str(evidence_item.get("id") or "")
            if evidence_id in seen_evidence_ids:
                continue
            seen_evidence_ids.add(evidence_id)
            blocks.append(
                {
                    "id": evidence_id,
                    "type": "file",
                    "path": rel_path,
                    "chars": len(block_text),
                }
            )
            evidence_items.append(evidence_item)
            total_chars += len(block_text)
            accepted += 1
            continue

        if kind == "search":
            normalized_rel_path = _normalize_context_rel_path(str(req.get("path") or ""))
            pattern = str(req.get("pattern") or "")
            if normalized_rel_path is None or not pattern:
                continue
            rel_path = allowed_paths.get(normalized_rel_path)
            if rel_path is None:
                continue
            path = root / rel_path
            if not path.exists() or not path.is_file():
                continue
            matches, search_truncated = _literal_search_matches_in_file(
                path,
                pattern,
                max_matches=20,
                max_scan_chars=max_per_block * 6,
            )
            lines = [f"--- search {rel_path!s} pattern={pattern!r} ---"]
            if matches:
                lines.extend(matches)
            else:
                lines.append("(no literal matches)")
            if search_truncated:
                lines.append("(search truncated)")
            block_text = "\n".join(lines).rstrip() + "\n"
            block_text = block_text[:max_per_block]
            if total_chars + len(block_text) > max_total_chars:
                break
            evidence_item = _build_evidence_item(
                kind="search",
                locator=f"{rel_path}:{pattern}",
                text=block_text,
                meta={"path": rel_path, "pattern": pattern, "matches": len(matches)},
            )
            evidence_id = str(evidence_item.get("id") or "")
            if evidence_id in seen_evidence_ids:
                continue
            seen_evidence_ids.add(evidence_id)
            blocks.append(
                {
                    "id": evidence_id,
                    "type": "search",
                    "path": rel_path,
                    "pattern": pattern,
                    "matches": len(matches),
                    "chars": len(block_text),
                }
            )
            evidence_items.append(evidence_item)
            total_chars += len(block_text)
            accepted += 1
            continue

        if kind == "frame":
            if not isinstance(snapshot, dict):
                continue
            idx = req.get("index")
            if not isinstance(idx, int):
                continue
            frames_obj = snapshot.get("frames")
            frames = frames_obj if isinstance(frames_obj, list) else []
            if idx < 0 or idx >= len(frames):
                continue
            frame = frames[idx]
            if not isinstance(frame, dict):
                continue
            file_rel = frame.get("file_rel", frame.get("file", "?"))
            line_no = frame.get("line", "?")
            func = frame.get("function", "?")
            code = frame.get("code", "")
            block_lines = [f"--- frame[{idx}] {file_rel}:{line_no} in {func}() ---"]
            if code:
                block_lines.append(f"code: {code}")
            locals_dict = frame.get("locals")
            if isinstance(locals_dict, dict) and locals_dict:
                block_lines.append("locals:")
                for key, value in list(locals_dict.items())[:20]:
                    if str(key).startswith("@"):
                        continue
                    try:
                        rendered_value = json.dumps(value, ensure_ascii=False, default=str)
                    except Exception:
                        rendered_value = repr(value)
                    if len(rendered_value) > 160:
                        rendered_value = rendered_value[:157] + "..."
                    block_lines.append(f"  {key} = {rendered_value}")
            block_text = "\n".join(block_lines).rstrip() + "\n"
            block_text = block_text[:max_per_block]
            if total_chars + len(block_text) > max_total_chars:
                break
            evidence_item = _build_evidence_item(
                kind="frame",
                locator=f"frame:{idx}",
                text=block_text,
                meta={"index": idx},
            )
            evidence_id = str(evidence_item.get("id") or "")
            if evidence_id in seen_evidence_ids:
                continue
            seen_evidence_ids.add(evidence_id)
            blocks.append(
                {"id": evidence_id, "type": "frame", "index": idx, "chars": len(block_text)}
            )
            evidence_items.append(evidence_item)
            total_chars += len(block_text)
            accepted += 1
            continue

    if accepted == 0:
        return build_fallback_context_bundle(
            root=root,
            snapshot=snapshot,
            stdout=stdout,
            stderr=stderr,
            max_total_chars=max_total_chars,
            reason="no_valid_stage_a_requests",
            file_index_lines=file_index_lines,
            request_payload=request_payload,
        )

    return StageAContextData(
        blocks=blocks,
        text=_render_evidence_items(evidence_items),
        used_fallback=False,
        requests_count=accepted,
        chars_returned=total_chars,
        request_success=True,
        request_payload=request_payload,
        request_raw=None,
        reason="ok",
        evidence_items=evidence_items,
    )


def build_fallback_context_bundle(
    *,
    root: Path,
    snapshot: dict[str, Any] | None,
    stdout: str,
    stderr: str,
    max_total_chars: int,
    reason: str,
    file_index_lines: Sequence[str] | None = None,
    request_payload: ContextRequestPayload | None = None,
    request_raw: str | None = None,
) -> StageAContextData:
    candidate_paths: list[str] = []
    crash_file = _crash_file_from_snapshot(snapshot)
    if crash_file is not None:
        candidate_paths.append(crash_file)

    failing_test = _detect_failing_test_file(
        stdout=stdout,
        stderr=stderr,
        root=root,
        file_index_lines=file_index_lines,
    )
    if failing_test is not None and failing_test not in candidate_paths:
        candidate_paths.append(failing_test)

    neighbors = _import_neighbors(root=root, rel_path=crash_file, limit=8)
    for neighbor in neighbors:
        if neighbor not in candidate_paths:
            candidate_paths.append(neighbor)

    if not candidate_paths:
        lines = (
            file_index_lines if file_index_lines is not None else _collect_file_index_lines(root)
        )
        candidate_paths = [str(line).strip() for line in lines if str(line).strip()][:3]

    blocks: list[dict[str, Any]] = []
    evidence_items: list[dict[str, Any]] = []
    seen_evidence_ids: set[str] = set()
    total_chars = 0
    for rel_path in candidate_paths:
        rel = rel_path.strip()
        if not rel:
            continue
        target = root / rel
        if not target.exists() or not target.is_file():
            continue
        remaining_chars = max_total_chars - total_chars
        if remaining_chars <= 0:
            break
        text = _read_text_limited(target, max_chars=min(40_000, remaining_chars))
        if text is None:
            continue
        block = f"--- {rel} ---\n{text.rstrip()}\n"
        if total_chars + len(block) > max_total_chars:
            break
        evidence_item = _build_evidence_item(
            kind="file",
            locator=rel,
            text=block,
            meta={"path": rel},
        )
        evidence_id = str(evidence_item.get("id") or "")
        if evidence_id in seen_evidence_ids:
            continue
        seen_evidence_ids.add(evidence_id)
        evidence_items.append(evidence_item)
        blocks.append({"id": evidence_id, "type": "file", "path": rel, "chars": len(block)})
        total_chars += len(block)

    return StageAContextData(
        blocks=blocks,
        text=_render_evidence_items(evidence_items),
        used_fallback=True,
        requests_count=len(blocks),
        chars_returned=total_chars,
        request_success=False,
        request_payload=request_payload,
        request_raw=request_raw,
        reason=reason,
        evidence_items=evidence_items,
    )


def _crash_file_from_snapshot(snapshot: dict[str, Any] | None) -> str | None:
    if not isinstance(snapshot, dict):
        return None
    frames_obj = snapshot.get("frames")
    frames = frames_obj if isinstance(frames_obj, list) else []
    for frame in frames:
        if not isinstance(frame, dict):
            continue
        rel = frame.get("file_rel")
        if isinstance(rel, str) and rel.strip():
            return rel.strip()
    return None


def _detect_failing_test_file(
    *,
    stdout: str,
    stderr: str,
    root: Path,
    file_index_lines: Sequence[str] | None = None,
) -> str | None:
    combined = f"{stdout}\n{stderr}"
    for match in re.finditer(r"([A-Za-z0-9_./\\-]*test[A-Za-z0-9_./\\-]*\.py)", combined):
        candidate = match.group(1).replace("\\", "/")
        if not candidate:
            continue
        candidate_path = root / candidate
        if candidate_path.exists() and candidate_path.is_file():
            return candidate
    # Fallback: first local test_*.py file.
    lines = file_index_lines if file_index_lines is not None else _collect_file_index_lines(root)
    for rel in lines:
        name = Path(rel).name
        if name.startswith("test_") and rel.endswith(".py"):
            return rel
    return None


def _import_neighbors(*, root: Path, rel_path: str | None, limit: int) -> list[str]:
    if not rel_path:
        return []
    path = root / rel_path
    if not path.exists() or not path.is_file():
        return []
    text = _read_text_limited(path, max_chars=120_000)
    if text is None:
        return []

    modules: list[str] = []
    for line in text.splitlines():
        m_from = re.match(r"\s*from\s+([A-Za-z_][A-Za-z0-9_\.]*)\s+import\s+", line)
        if m_from:
            modules.append(m_from.group(1))
            continue
        m_import = re.match(r"\s*import\s+([A-Za-z_][A-Za-z0-9_\.]*)", line)
        if m_import:
            modules.append(m_import.group(1))

    out: list[str] = []
    for module in modules:
        candidate = module.replace(".", "/") + ".py"
        candidate_path = root / candidate
        if candidate_path.exists() and candidate_path.is_file() and candidate not in out:
            out.append(candidate)
        if len(out) >= limit:
            break
    return out


def _resolve_subprocess_script_path(
    *,
    root: Path,
    raw_path: str,
    file_index_lines: Sequence[str] | None = None,
) -> str | None:
    token = str(raw_path or "").strip().strip("'\"")
    if not token:
        return None
    normalized = token.replace("\\", "/")
    while normalized.startswith("./"):
        normalized = normalized[2:]
    if not normalized.endswith(".py"):
        return None
    if normalized.startswith("-"):
        return None

    root_resolved = root.resolve()
    candidate_paths: list[Path] = []
    if Path(normalized).is_absolute():
        candidate_paths.append(Path(normalized))
    elif _is_safe_relative_path(normalized):
        candidate_paths.append(root / normalized)
    else:
        return None

    for candidate in candidate_paths:
        try:
            resolved = candidate.resolve()
            resolved.relative_to(root_resolved)
        except Exception:
            continue
        if resolved.exists() and resolved.is_file():
            return str(PurePosixPath(resolved.relative_to(root_resolved).as_posix()))

    # Basename-only fallback for subprocess calls like ["python", "buggy.py"].
    if "/" in normalized:
        return None
    lines = file_index_lines if file_index_lines is not None else _collect_file_index_lines(root)
    matches = [
        rel for rel in lines if rel.endswith(".py") and Path(rel).name == Path(normalized).name
    ]
    if len(matches) == 1:
        return str(PurePosixPath(matches[0].replace("\\", "/")))
    return None


def _extract_subprocess_script_neighbors(
    *,
    root: Path,
    rel_path: str | None,
    limit: int,
    file_index_lines: Sequence[str] | None = None,
) -> list[str]:
    if not rel_path:
        return []
    path = root / rel_path
    if not path.exists() or not path.is_file():
        return []
    text = _read_text_limited(path, max_chars=240_000)
    if text is None:
        return []
    try:
        tree = ast.parse(text)
    except SyntaxError:
        return []

    call_names = {"run", "popen", "call", "check_call", "check_output"}
    subprocess_module_aliases: set[str] = {"subprocess"}
    subprocess_member_aliases: set[str] = set()
    for node in ast.walk(tree):
        if isinstance(node, ast.Import):
            for alias in node.names:
                if alias.name == "subprocess":
                    subprocess_module_aliases.add(alias.asname or alias.name)
            continue
        if isinstance(node, ast.ImportFrom) and node.module == "subprocess":
            for alias in node.names:
                imported = alias.name.lower()
                if imported in call_names:
                    subprocess_member_aliases.add(alias.asname or alias.name)

    def _candidate_script_tokens(raw: str) -> list[str]:
        text_value = str(raw or "").strip()
        if not text_value:
            return []
        out: list[str] = []
        try:
            split_tokens = shlex.split(text_value)
        except ValueError:
            split_tokens = []
        tokens = split_tokens if split_tokens else [text_value]
        for token in tokens:
            for match in re.findall(r"[A-Za-z0-9_./\\-]+\.py", token):
                if match not in out:
                    out.append(match)
        return out

    def _string_values(value: ast.AST | None) -> list[str]:
        if value is None:
            return []
        if isinstance(value, ast.Constant) and isinstance(value.value, str):
            return [value.value]
        if isinstance(value, (ast.List, ast.Tuple, ast.Set)):
            out_values: list[str] = []
            for entry in value.elts:
                out_values.extend(_string_values(entry))
            return out_values
        if isinstance(value, ast.JoinedStr):
            literal_parts = [
                entry.value
                for entry in value.values
                if isinstance(entry, ast.Constant) and isinstance(entry.value, str)
            ]
            rendered = "".join(literal_parts).strip()
            return [rendered] if rendered else []
        return []

    resolved_neighbors: list[str] = []
    seen_neighbors: set[str] = set()
    for node in ast.walk(tree):
        if not isinstance(node, ast.Call):
            continue
        is_subprocess_call = False
        if isinstance(node.func, ast.Attribute):
            if (
                node.func.attr.lower() in call_names
                and isinstance(node.func.value, ast.Name)
                and node.func.value.id in subprocess_module_aliases
            ):
                is_subprocess_call = True
        elif isinstance(node.func, ast.Name) and node.func.id in subprocess_member_aliases:
            is_subprocess_call = True
        if not is_subprocess_call:
            continue

        candidate_values: list[str] = []
        if node.args:
            candidate_values.extend(_string_values(node.args[0]))
        for keyword in node.keywords:
            if keyword.arg == "args":
                candidate_values.extend(_string_values(keyword.value))
        for candidate in candidate_values:
            for token in _candidate_script_tokens(candidate):
                resolved = _resolve_subprocess_script_path(
                    root=root,
                    raw_path=token,
                    file_index_lines=file_index_lines,
                )
                if not resolved:
                    continue
                if resolved in seen_neighbors:
                    continue
                seen_neighbors.add(resolved)
                resolved_neighbors.append(resolved)
                if len(resolved_neighbors) >= limit:
                    return resolved_neighbors
    return resolved_neighbors


def apply_diff(cwd: Path, diff: str) -> tuple[bool, str]:
    path_ok, path_error = _validate_diff_paths_within_cwd(cwd, diff)
    if not path_ok:
        return False, path_error

    # Prefer POSIX patch so paths are resolved relative to `cwd` (not git toplevel).
    patch = shutil.which("patch")
    if patch:
        try:
            proc = subprocess.run(
                [patch, "-p1", "--batch", "--forward"],
                input=diff,
                text=True,
                cwd=cwd,
                capture_output=True,
                timeout=PATCH_APPLY_TIMEOUT_SEC,
            )
        except subprocess.TimeoutExpired:
            return False, f"patch timed out after {PATCH_APPLY_TIMEOUT_SEC}s"
        if proc.returncode == 0:
            return True, ""
        msg = (proc.stderr or proc.stdout or "").strip()
        relaxed_ok, relaxed_msg = _apply_relaxed_replacements(cwd, diff)
        if relaxed_ok:
            return True, "applied via relaxed replacement fallback"
        return False, f"patch failed: {msg}; relaxed fallback failed: {relaxed_msg}"

    # Fallback to git apply when patch utility is unavailable.
    git = shutil.which("git")
    if git:
        try:
            proc = subprocess.run(
                [git, "apply", "--whitespace=nowarn", "-"],
                input=diff,
                text=True,
                cwd=cwd,
                capture_output=True,
                timeout=PATCH_APPLY_TIMEOUT_SEC,
            )
        except subprocess.TimeoutExpired:
            return False, f"git apply timed out after {PATCH_APPLY_TIMEOUT_SEC}s"
        if proc.returncode == 0:
            return True, ""
        msg = (proc.stderr or proc.stdout or "").strip()
        return False, f"git apply failed: {msg}"

    return False, "No patch tool found (git or patch required)."


def _extract_diff_paths(diff: str) -> list[str]:
    paths: list[str] = []
    for raw_line in diff.splitlines():
        line = raw_line.strip()
        if line.startswith("diff --git "):
            tokens = line.split()
            if len(tokens) >= 4:
                for token in (tokens[2], tokens[3]):
                    normalized = _normalize_diff_path_token(token)
                    if normalized is not None:
                        paths.append(normalized)
            continue
        if line.startswith("--- ") or line.startswith("+++ "):
            token = line[4:].strip().split()[0] if line[4:].strip() else ""
            normalized = _normalize_diff_path_token(token)
            if normalized is not None:
                paths.append(normalized)
    seen: set[str] = set()
    unique: list[str] = []
    for path in paths:
        if path in seen:
            continue
        seen.add(path)
        unique.append(path)
    return unique


def _normalize_diff_path_token(token: str) -> str | None:
    if not token:
        return None
    if token in {"a/", "b/"}:
        return None
    if token.startswith("a/") or token.startswith("b/"):
        token = token[2:]
    return token


def _is_test_file_path(path: str) -> bool:
    normalized = path.replace("\\", "/")
    pure = PurePosixPath(normalized)
    parts = [part.lower() for part in pure.parts]
    if not parts:
        return False
    file_name = parts[-1]
    if any(part in {"tests", "test"} for part in parts):
        return True
    if file_name == "conftest.py":
        return True
    if file_name.startswith("test_"):
        return True
    return bool(file_name.endswith("_test.py"))


def _compute_patch_path_metrics(diff: str) -> dict[str, Any]:
    normalized_paths = [
        str(PurePosixPath(path.replace("\\", "/"))) for path in _extract_diff_paths(diff)
    ]
    test_paths = [path for path in normalized_paths if _is_test_file_path(path)]
    non_test_paths = [path for path in normalized_paths if not _is_test_file_path(path)]
    return {
        "patch_file_count": len(normalized_paths),
        "patch_test_file_count": len(test_paths),
        "patch_non_test_file_count": len(non_test_paths),
        "patch_touches_tests": bool(test_paths),
        "patch_file_paths": normalized_paths,
        "patch_test_file_paths": test_paths,
    }


def _backup_diff_target_files(*, cwd: Path, diff: str) -> dict[str, bytes]:
    backups: dict[str, bytes] = {}
    for rel in _extract_diff_paths(diff):
        if rel == "/dev/null":
            continue
        normalized = str(PurePosixPath(rel.replace("\\", "/")))
        target = cwd / normalized
        if not target.exists() or not target.is_file():
            continue
        try:
            backups[normalized] = target.read_bytes()
        except Exception:
            continue
    return backups


def _restore_diff_target_files(*, cwd: Path, backups: dict[str, bytes]) -> None:
    for rel, payload in backups.items():
        target = cwd / rel
        try:
            target.parent.mkdir(parents=True, exist_ok=True)
            target.write_bytes(payload)
        except Exception:
            continue


def _python_files_touched_by_diff(*, cwd: Path, diff: str) -> list[str]:
    out: list[str] = []
    for rel in _extract_diff_paths(diff):
        normalized = str(PurePosixPath(rel.replace("\\", "/")))
        if not normalized.endswith(".py"):
            continue
        target = cwd / normalized
        if not target.exists() or not target.is_file():
            continue
        if normalized not in out:
            out.append(normalized)
    return out


def _patch_sanitize_line_reasons(line: str) -> list[str]:
    reasons: list[str] = []
    if "\\Exception" in line:
        reasons.append("escaped_backslash_before_exception_token")
    if "\\n" in line or "\\t" in line:
        contains_quoted_escape = bool(re.search(r"""['"][^'"]*(\\n|\\t)[^'"]*['"]""", line))
        if not contains_quoted_escape:
            reasons.append("literal_escaped_newline_or_tab_in_code")
    if "await " in line and line.rstrip().endswith('",'):
        reasons.append("suspicious_trailing_quote_comma_after_await")
    return reasons


def _run_patch_sanitization(*, diff: str) -> tuple[bool, dict[str, Any]]:
    current_file = ""
    current_new_lineno = 0
    in_hunk = False
    checked_files: list[str] = []
    findings: list[str] = []

    for raw_line in diff.splitlines():
        if raw_line.startswith("+++ "):
            token = raw_line[4:].strip().split()[0] if raw_line[4:].strip() else ""
            normalized = _normalize_diff_path_token(token)
            current_file = str(PurePosixPath((normalized or "").replace("\\", "/")))
            in_hunk = False
            if current_file and current_file.endswith(".py") and current_file not in checked_files:
                checked_files.append(current_file)
            continue
        if raw_line.startswith("@@"):
            match = re.search(r"\+(\d+)", raw_line)
            current_new_lineno = int(match.group(1)) - 1 if match else 0
            in_hunk = True
            continue
        if not in_hunk:
            continue
        if not current_file or not current_file.endswith(".py"):
            continue

        if raw_line.startswith("+") and not raw_line.startswith("+++ "):
            current_new_lineno += 1
            line = raw_line[1:]
            line_reasons = _patch_sanitize_line_reasons(line)
            findings.extend(
                f"{current_file}:{current_new_lineno}:{reason}" for reason in line_reasons
            )
            continue
        if raw_line.startswith(" "):
            current_new_lineno += 1
            continue
        if raw_line.startswith("-"):
            continue

    if not findings:
        return True, {
            "patch_sanitize_checked_files": checked_files,
            "patch_sanitize_checked_count": len(checked_files),
            "patch_sanitize_reasons": [],
            "patch_sanitize_error_msg": "",
        }

    preview = ", ".join(findings[:6])
    if len(findings) > 6:
        preview = f"{preview}, ... (+{len(findings) - 6} more)"
    return False, {
        "patch_sanitize_checked_files": checked_files,
        "patch_sanitize_checked_count": len(checked_files),
        "patch_sanitize_reasons": findings,
        "patch_sanitize_error_msg": f"Patch sanitation rejected malformed patch text: {preview}",
    }


def _run_syntax_preflight(*, cwd: Path, diff: str) -> tuple[bool, dict[str, Any]]:
    python_files = _python_files_touched_by_diff(cwd=cwd, diff=diff)
    if not python_files:
        return True, {
            "syntax_preflight_checked_files": [],
            "syntax_preflight_checked_count": 0,
            "syntax_preflight_error_file": "",
            "syntax_preflight_error_type": "",
            "syntax_preflight_error_msg": "",
        }

    with tempfile.TemporaryDirectory(prefix="llmdebug-syntax-preflight-") as tmpdir:
        tmp_root = Path(tmpdir)
        for idx, rel in enumerate(python_files):
            source_path = cwd / rel
            pyc_path = tmp_root / f"{idx:04d}.pyc"
            try:
                py_compile.compile(
                    str(source_path),
                    cfile=str(pyc_path),
                    dfile=rel,
                    doraise=True,
                )
            except py_compile.PyCompileError as exc:
                return False, {
                    "syntax_preflight_checked_files": list(python_files),
                    "syntax_preflight_checked_count": len(python_files),
                    "syntax_preflight_error_file": rel,
                    "syntax_preflight_error_type": type(exc).__name__,
                    "syntax_preflight_error_msg": str(exc).strip(),
                }
            except Exception as exc:
                return False, {
                    "syntax_preflight_checked_files": list(python_files),
                    "syntax_preflight_checked_count": len(python_files),
                    "syntax_preflight_error_file": rel,
                    "syntax_preflight_error_type": type(exc).__name__,
                    "syntax_preflight_error_msg": f"{type(exc).__name__}: {exc}",
                }

    return True, {
        "syntax_preflight_checked_files": list(python_files),
        "syntax_preflight_checked_count": len(python_files),
        "syntax_preflight_error_file": "",
        "syntax_preflight_error_type": "",
        "syntax_preflight_error_msg": "",
    }


def _render_syntax_preflight_retry_feedback(
    *,
    cycle: int,
    max_cycles: int,
    error_file: str,
    error_type: str,
    error_msg: str,
) -> str:
    trimmed_error = (error_msg or "").strip()
    if len(trimmed_error) > 800:
        trimmed_error = trimmed_error[:797] + "..."
    return (
        "\n\n"
        "Retry request: prior patch introduced a Python syntax/compile error.\n"
        f"- cycle: {cycle}/{max_cycles}\n"
        f"- file: {error_file or 'unknown'}\n"
        f"- error_type: {error_type or 'SyntaxError'}\n"
        f"- error: {trimmed_error or 'unknown syntax error'}\n"
        "Please return a corrected unified diff that fixes this syntax issue first.\n"
    )


def _render_patch_sanitize_retry_feedback(
    *,
    cycle: int,
    max_cycles: int,
    sanitize_error: str,
) -> str:
    trimmed_error = (sanitize_error or "").strip()
    if len(trimmed_error) > 800:
        trimmed_error = trimmed_error[:797] + "..."
    return (
        "\n\n"
        "Retry request: prior patch text was malformed and rejected by sanitation checks.\n"
        f"- cycle: {cycle}/{max_cycles}\n"
        f"- issue: {trimmed_error or 'unknown patch sanitation issue'}\n"
        "Please return a corrected unified diff without escaped newline/tab artifacts.\n"
    )


def _extract_candidate_patch_files(diff: str) -> list[str]:
    return [str(PurePosixPath(path.replace("\\", "/"))) for path in _extract_diff_paths(diff)]


def _patch_mentions_crash_file(*, patch_files: list[str], crash_file: str | None) -> bool:
    if not crash_file:
        return False
    crash_norm = str(PurePosixPath(crash_file.replace("\\", "/")))
    crash_name = PurePosixPath(crash_norm).name
    for path in patch_files:
        path_norm = str(PurePosixPath(path.replace("\\", "/")))
        if path_norm == crash_norm or PurePosixPath(path_norm).name == crash_name:
            return True
    return False


def _patch_mentions_failing_test_context(
    *, patch_files: list[str], failing_test_file: str | None
) -> bool:
    if not failing_test_file:
        return False
    test_norm = str(PurePosixPath(failing_test_file.replace("\\", "/")))
    test_name = PurePosixPath(test_norm).name
    for path in patch_files:
        path_norm = str(PurePosixPath(path.replace("\\", "/")))
        if path_norm == test_norm or PurePosixPath(path_norm).name == test_name:
            return True
    return False


def _patch_mentions_scope_files(*, patch_files: list[str], scope_files: set[str]) -> bool:
    if not scope_files:
        return False
    scope_by_name: dict[str, set[str]] = {}
    for scope_path in scope_files:
        scope_name = PurePosixPath(scope_path).name
        scope_by_name.setdefault(scope_name, set()).add(scope_path)
    for path in patch_files:
        path_norm = str(PurePosixPath(path.replace("\\", "/")))
        if path_norm in scope_files:
            return True
        # Compatibility fallback: allow basename-only matches only when the patch path
        # itself has no directory segments and maps unambiguously to exactly one scope file.
        if "/" not in path_norm:
            candidates = scope_by_name.get(PurePosixPath(path_norm).name, set())
            if len(candidates) == 1:
                return True
    return False


def _infer_test_source_scope(
    *,
    cwd: Path,
    crash_file: str | None,
    failing_test_file: str | None,
    enabled: bool,
    file_index_lines: Sequence[str] | None = None,
) -> list[str]:
    if not enabled:
        return []

    candidates: list[str] = []
    if isinstance(crash_file, str) and crash_file and _is_test_file_path(crash_file):
        candidates.append(crash_file)
    if (
        isinstance(failing_test_file, str)
        and failing_test_file
        and _is_test_file_path(failing_test_file)
        and failing_test_file not in candidates
    ):
        candidates.append(failing_test_file)

    inferred: list[str] = []
    for rel in candidates:
        for neighbor in _import_neighbors(root=cwd, rel_path=rel, limit=12):
            normalized = str(PurePosixPath(neighbor.replace("\\", "/")))
            if normalized not in inferred:
                inferred.append(normalized)
        for neighbor in _extract_subprocess_script_neighbors(
            root=cwd,
            rel_path=rel,
            limit=12,
            file_index_lines=file_index_lines,
        ):
            normalized = str(PurePosixPath(neighbor.replace("\\", "/")))
            if normalized not in inferred:
                inferred.append(normalized)
    return inferred


def _patch_contains_substantive_change(diff: str) -> bool:
    for line in diff.splitlines():
        if not line:
            continue
        if line.startswith(("diff --git ", "index ", "@@", "--- ", "+++ ")):
            continue
        if line.startswith(("+", "-")):
            return True
    return False


def _score_patch_relevance(
    *,
    diff: str,
    cwd: Path,
    snapshot: dict[str, Any] | None,
    stdout: str,
    stderr: str,
    infer_test_source_scope: bool = True,
    file_index_lines: Sequence[str] | None = None,
) -> tuple[bool, dict[str, Any]]:
    patch_files = _extract_candidate_patch_files(diff)
    crash_file = _crash_file_from_snapshot(snapshot)
    crash_neighbors = (
        _import_neighbors(root=cwd, rel_path=crash_file, limit=12) if crash_file else []
    )
    crash_neighbor_set = {str(PurePosixPath(path.replace("\\", "/"))) for path in crash_neighbors}
    failing_test_file = _detect_failing_test_file(
        stdout=stdout,
        stderr=stderr,
        root=cwd,
        file_index_lines=file_index_lines,
    )
    inferred_source_scope = _infer_test_source_scope(
        cwd=cwd,
        crash_file=crash_file,
        failing_test_file=failing_test_file,
        enabled=infer_test_source_scope,
        file_index_lines=file_index_lines,
    )
    inferred_source_scope_set = {
        str(PurePosixPath(path.replace("\\", "/"))) for path in inferred_source_scope
    }

    substantive_change = _patch_contains_substantive_change(diff)
    matches_crash_file = _patch_mentions_crash_file(patch_files=patch_files, crash_file=crash_file)
    matches_crash_neighbor = any(path in crash_neighbor_set for path in patch_files)
    matches_inferred_source_scope = _patch_mentions_scope_files(
        patch_files=patch_files,
        scope_files=inferred_source_scope_set,
    )
    touches_test_file = any(_is_test_file_path(path) for path in patch_files)
    touches_failing_test_file = _patch_mentions_failing_test_context(
        patch_files=patch_files,
        failing_test_file=failing_test_file,
    )

    score = 0
    reasons: list[str] = []
    if substantive_change:
        score += 1
        reasons.append("substantive_change")
    else:
        score -= 2
        reasons.append("no_substantive_change")

    if crash_file:
        if matches_crash_file:
            score += 2
            reasons.append("matches_crash_file")
        elif matches_crash_neighbor:
            score += 1
            reasons.append("matches_crash_neighbor")
        elif matches_inferred_source_scope:
            score += 1
            reasons.append("matches_inferred_source_scope")
        else:
            score -= 1
            reasons.append("misses_crash_scope")
    else:
        if matches_inferred_source_scope:
            score += 1
            reasons.append("matches_inferred_source_scope")
        else:
            reasons.append("no_crash_file_available")

    if touches_failing_test_file:
        score -= 2
        reasons.append("touches_failing_test_file")
    elif touches_test_file:
        score -= 1
        reasons.append("touches_test_file")
    else:
        score += 1
        reasons.append("non_test_edit")

    passed = bool(score >= PATCH_VERIFIER_MIN_SCORE)

    return passed, {
        "patch_verifier_passed": passed,
        "patch_verifier_score": int(score),
        "patch_verifier_min_score": int(PATCH_VERIFIER_MIN_SCORE),
        "patch_verifier_reasons": reasons,
        "patch_verifier_patch_files": patch_files,
        "patch_verifier_crash_file": crash_file,
        "patch_verifier_crash_file_matched": matches_crash_file,
        "patch_verifier_crash_neighbor_matched": matches_crash_neighbor,
        "patch_verifier_crash_neighbors": sorted(crash_neighbor_set),
        "patch_verifier_inferred_source_scope": sorted(inferred_source_scope_set),
        "patch_verifier_source_scope_matched": matches_inferred_source_scope,
        "patch_verifier_failing_test_file": failing_test_file,
        "patch_verifier_touches_test_file": touches_test_file,
        "patch_verifier_touches_failing_test_file": touches_failing_test_file,
        "patch_verifier_substantive_change": substantive_change,
    }


def _is_safe_relative_path(path: str) -> bool:
    if not path or path == "/dev/null":
        return False
    normalized = path.replace("\\", "/")
    pure = PurePosixPath(normalized)
    if pure.is_absolute():
        return False
    parts = pure.parts
    if not parts:
        return False
    return not any(part in {"", ".", ".."} for part in parts)


def _validate_diff_paths_within_cwd(cwd: Path, diff: str) -> tuple[bool, str]:
    paths = _extract_diff_paths(diff)
    if not paths:
        return False, "patch rejected: no file paths found in diff"
    root = cwd.resolve()
    for raw_path in paths:
        if raw_path == "/dev/null":
            return False, "patch rejected: /dev/null paths are not supported by eval harness"
        if not _is_safe_relative_path(raw_path):
            return False, f"patch rejected: unsafe path {raw_path!r}"
        resolved = (root / raw_path).resolve()
        try:
            resolved.relative_to(root)
        except Exception:
            return False, f"patch rejected: path escapes workdir {raw_path!r}"
    return True, ""


def _apply_relaxed_replacements(cwd: Path, diff: str) -> tuple[bool, str]:
    """Best-effort fallback for near-correct model diffs with bad hunk context.

    Supports only line-for-line replacements extracted from unified hunks.
    """
    replacements: dict[str, list[tuple[str, str, list[str], list[str]]]] = {}
    current_path: str | None = None
    lines = diff.splitlines()
    i = 0
    while i < len(lines):
        raw_line = lines[i]
        if raw_line.startswith("+++ b/"):
            current_path = raw_line[6:].strip()
            i += 1
            continue
        if raw_line.startswith("@@"):
            if current_path is None:
                i += 1
                continue
            hunk_lines: list[str] = []
            i += 1
            while i < len(lines):
                nxt = lines[i]
                if (
                    nxt.startswith("@@")
                    or nxt.startswith("diff --git ")
                    or nxt.startswith("--- a/")
                    or nxt.startswith("+++ b/")
                ):
                    break
                hunk_lines.append(nxt)
                i += 1
            replacements.setdefault(current_path, []).extend(
                _extract_relaxed_pairs_from_hunk(hunk_lines)
            )
            continue
        i += 1

    if not replacements:
        return False, "no replaceable line pairs found"

    root = cwd.resolve()
    pending_updates: list[tuple[Path, str]] = []
    for rel_path, pairs in replacements.items():
        file_path = (cwd / rel_path).resolve()
        try:
            file_path.relative_to(root)
        except Exception:
            return False, f"path outside working directory: {rel_path}"
        if not file_path.exists():
            return False, f"file not found: {rel_path}"
        text = file_path.read_text(encoding="utf-8")
        had_newline = text.endswith("\n")
        line_values = text.splitlines()

        changed = False
        for old_line, new_line, before_ctx, after_ctx in pairs:
            matching_indices: list[int] = []
            for idx, existing_line in enumerate(line_values):
                if existing_line != old_line:
                    continue
                if before_ctx:
                    start = idx - len(before_ctx)
                    if start < 0 or line_values[start:idx] != before_ctx:
                        continue
                if after_ctx:
                    end = idx + 1 + len(after_ctx)
                    if end > len(line_values) or line_values[idx + 1 : end] != after_ctx:
                        continue
                matching_indices.append(idx)

            if len(matching_indices) == 0:
                return (
                    False,
                    f"could not find context-anchored target in {rel_path!r}: {old_line!r}",
                )
            if len(matching_indices) > 1:
                return False, f"ambiguous relaxed replacement target in {rel_path!r}: {old_line!r}"
            line_values[matching_indices[0]] = new_line
            changed = True

        if changed:
            updated = "\n".join(line_values)
            if had_newline:
                updated += "\n"
            pending_updates.append((file_path, updated))

    for file_path, updated in pending_updates:
        file_path.write_text(updated, encoding="utf-8")

    return True, ""


def _extract_relaxed_pairs_from_hunk(
    hunk_lines: list[str],
) -> list[tuple[str, str, list[str], list[str]]]:
    """Extract replaceable `-`/`+` pairs with nearby context anchors."""
    out: list[tuple[str, str, list[str], list[str]]] = []
    recent_context: list[str] = []
    i = 0
    while i < len(hunk_lines):
        line = hunk_lines[i]
        if line.startswith(" "):
            recent_context.append(line[1:])
            if len(recent_context) > 3:
                recent_context.pop(0)
            i += 1
            continue
        if line.startswith("-") and i + 1 < len(hunk_lines) and hunk_lines[i + 1].startswith("+"):
            old_line = line[1:]
            new_line = hunk_lines[i + 1][1:]
            after_ctx: list[str] = []
            j = i + 2
            while j < len(hunk_lines) and hunk_lines[j].startswith(" ") and len(after_ctx) < 2:
                after_ctx.append(hunk_lines[j][1:])
                j += 1
            before_ctx = recent_context[-2:] if recent_context else []
            out.append((old_line, new_line, before_ctx, after_ctx))
            i += 2
            continue
        i += 1
    return out


def _is_forced_noop_marker_diff(diff: str) -> bool:
    changed_lines_seen = False
    for line in str(diff or "").splitlines():
        if not line:
            continue
        if line.startswith(
            (
                "diff --git ",
                "index ",
                "new file mode ",
                "deleted file mode ",
                "old mode ",
                "new mode ",
                "similarity index ",
                "rename from ",
                "rename to ",
                "copy from ",
                "copy to ",
                "--- ",
                "+++ ",
                "@@",
                "\\",
            )
        ):
            continue
        if line.startswith(" "):
            continue
        if line.startswith("+") or line.startswith("-"):
            payload = line[1:].strip()
            if not payload:
                continue
            changed_lines_seen = True
            if not payload.startswith("# llmdebug-force-attempt-"):
                return False
            continue
        return False
    return changed_lines_seen


def _build_runner_forced_attempt_diff(
    *, cwd: Path, file_index: str, attempt: int
) -> tuple[str, str] | None:
    """Build a deterministic minimal patch when a patcher returns no diff."""
    candidates = [line.strip() for line in file_index.splitlines() if line.strip()]
    if not candidates:
        return None

    target_rel = _select_forced_patch_target(candidates)
    if target_rel is None:
        return None

    target_path = (cwd / target_rel).resolve()
    try:
        target_path.relative_to(cwd.resolve())
    except Exception:
        return None
    if not target_path.exists():
        return None

    before = target_path.read_text(encoding="utf-8")
    marker_base = f"# llmdebug-force-attempt-{attempt}"
    marker = marker_base
    suffix = 2
    while marker in before:
        marker = f"{marker_base}-{suffix}"
        suffix += 1
    sep = "" if before.endswith("\n") else "\n"
    after = before + f"{sep}{marker}\n"
    if after == before:
        return None

    diff = _build_diff_from_changes([(target_rel, before, after)])
    if not diff.strip():
        return None
    return target_rel, diff


def _select_forced_patch_target(candidates: list[str]) -> str | None:
    target_rel: str | None = None
    for rel in candidates:
        if not rel.endswith(".py"):
            continue
        lower = rel.lower()
        name = Path(rel).name.lower()
        if name.startswith("test_") or "/test" in lower or "\\test" in lower:
            continue
        target_rel = rel
        break
    if target_rel is not None:
        return target_rel
    for rel in candidates:
        if rel.endswith(".py"):
            return rel
    return None


def _build_diff_from_changes(changes: list[tuple[str, str, str]]) -> str:
    parts: list[str] = []
    for path, before, after in changes:
        udiff = difflib.unified_diff(
            before.splitlines(),
            after.splitlines(),
            fromfile=f"a/{path}",
            tofile=f"b/{path}",
            lineterm="",
        )
        file_lines = list(udiff)
        if not file_lines:
            continue
        parts.append(f"diff --git a/{path} b/{path}")
        parts.extend(file_lines)
        if parts and parts[-1] != "":
            parts.append("")
    return "\n".join(parts).rstrip("\n") + "\n"


def collect_text_context(
    root: Path,
    *,
    max_total_chars: int = 200_000,
    file_index_lines: Sequence[str] | None = None,
) -> str:
    """Collect small text files from a case directory for LLM context."""
    max_total_bytes = max_total_chars
    max_file_bytes = 50_000

    parts: list[str] = []
    total = 0

    lines = file_index_lines if file_index_lines is not None else _collect_file_index_lines(root)
    for rel_text in lines:
        rel = Path(str(rel_text))
        path = root / rel
        if not path.exists() or not path.is_file():
            continue
        try:
            rel_display = path.relative_to(root)
        except Exception:
            rel_display = path
        text = _read_text_limited(path, max_chars=max_file_bytes)
        if text is None:
            continue

        block = f"--- {rel_display} ---\n{text.rstrip()}\n"
        if total + len(block) > max_total_bytes:
            break
        parts.append(block)
        total += len(block)

    return "\n".join(parts).rstrip() + ("\n" if parts else "")


def collect_file_index(root: Path) -> str:
    return "\n".join(_collect_file_index_lines(root))


if __name__ == "__main__":
    raise SystemExit(main())
