from __future__ import annotations

import asyncio
import json
import time
import re
import zipfile
from collections import defaultdict, Counter, deque
from datetime import datetime, timedelta
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple

from werkzeug.utils import secure_filename

from config import (
    OUTPUT_FORMATS,
    AUTO_FIX_TOOL_CALL,
    AUTO_FIX_MAX_ATTEMPTS,
    MAX_ITERATIONS_PER_TASK,
    MAX_CONSECUTIVE_SAME_TOOL,
    MAX_TOTAL_TOOL_CALLS,
    TOOL_CALL_COOLDOWN,
    MAX_UPLOAD_SIZE,
    DEFAULT_CONVERSATIONS_LIMIT,
    MAX_CONVERSATIONS_LIMIT,
    CONVERSATIONS_DIR,
    DEFAULT_RESPONSE_MAX_TOKENS,
    DEFAULT_PROJECT_PATH,
    LOGS_DIR,
    AGENT_VERSION,
    THINKING_FAST_INTERVAL,
    PROJECT_MAX_STORAGE_MB,
    PROJECT_MAX_STORAGE_BYTES,
    UPLOAD_SCAN_LOG_SUBDIR,
)
from modules.personalization_manager import (
    load_personalization_config,
    save_personalization_config,
    THINKING_INTERVAL_MIN,
    THINKING_INTERVAL_MAX,
    resolve_context_compression_settings,
)
from modules.skill_hint_manager import SkillHintManager
from modules.upload_security import UploadSecurityError
from modules.user_manager import UserWorkspace
from modules.usage_tracker import QUOTA_DEFAULTS
from modules.sub_agent_manager import TERMINAL_STATUSES
from modules.versioning_manager import ConversationVersioningManager, VersioningError
from core.web_terminal import WebTerminal
from utils.tool_result_formatter import format_tool_result_for_context
from utils.conversation_manager import ConversationManager
from config.model_profiles import get_model_context_window, get_model_profile

from .auth_helpers import api_login_required, resolve_admin_policy, get_current_user_record, get_current_username
from .context import with_terminal, get_gui_manager, get_upload_guard, build_upload_error_response, ensure_conversation_loaded, reset_system_state, get_user_resources, get_or_create_usage_tracker
from .utils_common import (
    build_review_lines,
    debug_log,
    log_backend_chunk,
    log_frontend_chunk,
    log_streaming_debug_entry,
    brief_log,
    DEBUG_LOG_FILE,
    CHUNK_BACKEND_LOG_FILE,
    CHUNK_FRONTEND_LOG_FILE,
    STREAMING_DEBUG_LOG_FILE,
)
from .security import rate_limited, compact_web_search_result, consume_socket_token, prune_socket_tokens, validate_csrf_request, requires_csrf_protection, get_csrf_token
from .monitor import cache_monitor_snapshot, get_cached_monitor_snapshot
from .extensions import socketio
from .state import (
    MONITOR_FILE_TOOLS,
    MONITOR_MEMORY_TOOLS,
    MONITOR_SNAPSHOT_CHAR_LIMIT,
    MONITOR_MEMORY_ENTRY_LIMIT,
    RATE_LIMIT_BUCKETS,
    FAILURE_TRACKERS,
    pending_socket_tokens,
    usage_trackers,
    MONITOR_SNAPSHOT_CACHE,
    MONITOR_SNAPSHOT_CACHE_LIMIT,
    PROJECT_STORAGE_CACHE,
    PROJECT_STORAGE_CACHE_TTL_SECONDS,
    RECENT_UPLOAD_EVENT_LIMIT,
    RECENT_UPLOAD_FEED_LIMIT,
    THINKING_FAILURE_KEYWORDS,
    TITLE_PROMPT_PATH,
    get_last_active_ts,
    user_manager,
    container_manager,
    custom_tool_registry,
    user_terminals,
    terminal_rooms,
    connection_users,
    stop_flags,
    active_polling_tasks,
    get_stop_flag,
    set_stop_flag,
    clear_stop_flag,
)
from .chat_flow_helpers import (
    detect_malformed_tool_call as _detect_malformed_tool_call,
    detect_tool_failure,
    get_thinking_state,
    mark_force_thinking as _mark_force_thinking,
    mark_suppress_thinking,
    apply_thinking_schedule as _apply_thinking_schedule,
    update_thinking_after_call as _update_thinking_after_call,
    maybe_mark_failure_from_message as _maybe_mark_failure_from_message,
    generate_conversation_title_background as _generate_conversation_title_background,
)


from .chat_flow_runner_helpers import (
    extract_intent_from_partial,
    resolve_monitor_path,
    resolve_monitor_memory,
    capture_monitor_snapshot,
)


from .chat_flow_runtime import (
    generate_conversation_title_background,
    mark_force_thinking,
    apply_thinking_schedule,
    update_thinking_after_call,
    maybe_mark_failure_from_message,
    detect_malformed_tool_call,
)

from .chat_flow_task_support import process_sub_agent_updates, process_background_command_updates
from .chat_flow_tool_loop import execute_tool_calls
from .chat_flow_stream_loop import run_streaming_attempts
from .deep_compression import run_deep_compression


def _should_skip_versioning_for_message(
    *,
    message: str,
    auto_user_message_event: bool,
) -> bool:
    if auto_user_message_event:
        return True
    text = (message or "").strip()
    if not text:
        return True
    if text.startswith("这是一句系统自动发送的user消息"):
        return True
    return False


def _prepare_hidden_versioning_baseline_for_first_input(
    *,
    web_terminal,
    workspace,
    conversation_id: str,
    message: str,
    auto_user_message_event: bool,
) -> None:
    """Ensure first-run baseline is committed (hidden, no checkpoint row)."""
    try:
        if _should_skip_versioning_for_message(
            message=message,
            auto_user_message_event=auto_user_message_event,
        ):
            return
        cm = getattr(getattr(web_terminal, "context_manager", None), "conversation_manager", None)
        if not cm:
            return
        conv_data = cm.load_conversation(conversation_id) or {}
        versioning_meta = ((conv_data.get("metadata") or {}).get("versioning") or {})
        if not bool(versioning_meta.get("enabled", False)):
            return
        tracking_mode = ConversationVersioningManager.normalize_tracking_mode(
            versioning_meta.get("tracking_mode")
        )
        if (
            tracking_mode == ConversationVersioningManager.TRACKING_MODE_WORKSPACE_AND_CONVERSATION
            and not bool(getattr(web_terminal, "username", None) == "host")
        ):
            return
        manager = ConversationVersioningManager(
            project_path=workspace.project_path,
            data_dir=workspace.data_dir,
            conversation_id=conversation_id,
        )
        baseline = manager.ensure_baseline_for_first_input(tracking_mode=tracking_mode)
        debug_log(
            f"[Versioning][Baseline] conv={conversation_id} "
            f"tracking_mode={tracking_mode} "
            f"created={baseline.get('created')} skipped={baseline.get('skipped')} "
            f"reason={baseline.get('reason')} head={baseline.get('head')}"
        )
    except VersioningError as exc:
        debug_log(f"[Versioning] 创建首轮基线失败: {exc}")
    except Exception as exc:
        debug_log(f"[Versioning] 创建首轮基线异常: {exc}")


def _record_hidden_versioning_checkpoint_after_run(
    *,
    web_terminal,
    workspace,
    conversation_id: str,
    message: str,
    message_index: int,
    auto_user_message_event: bool,
    run_status: str = "completed",
) -> None:
    """Hidden snapshot after current manual user input run finished."""
    try:
        if _should_skip_versioning_for_message(
            message=message,
            auto_user_message_event=auto_user_message_event,
        ):
            return
        cm = getattr(getattr(web_terminal, "context_manager", None), "conversation_manager", None)
        if not cm:
            return
        conv_data = cm.load_conversation(conversation_id) or {}
        versioning_meta = ((conv_data.get("metadata") or {}).get("versioning") or {})
        if not bool(versioning_meta.get("enabled", False)):
            return
        tracking_mode = ConversationVersioningManager.normalize_tracking_mode(
            versioning_meta.get("tracking_mode")
        )
        if (
            tracking_mode == ConversationVersioningManager.TRACKING_MODE_WORKSPACE_AND_CONVERSATION
            and not bool(getattr(web_terminal, "username", None) == "host")
        ):
            return
        snapshot_messages = conv_data.get("messages") or []
        snapshot_payload = {
            "conversation_id": conversation_id,
            "title": conv_data.get("title"),
            "metadata": conv_data.get("metadata") or {},
            "messages": snapshot_messages,
            "message_index": int(message_index),
            "run_status": str(run_status or "completed"),
        }

        manager = ConversationVersioningManager(
            project_path=workspace.project_path,
            data_dir=workspace.data_dir,
            conversation_id=conversation_id,
        )
        row = manager.create_checkpoint(
            message=message,
            message_index=message_index,
            workspace_path=str(workspace.project_path),
            conversation_snapshot=snapshot_payload,
            run_status=str(run_status or "completed"),
            tracking_mode=tracking_mode,
        )
        debug_log(
            f"[Versioning][Checkpoint] conv={conversation_id} seq={row.get('seq')} "
            f"tracking_mode={tracking_mode} "
            f"msg_index={message_index} snapshot_messages={len(snapshot_messages)} "
            f"commit={row.get('commit')} changed={row.get('changed')} status={row.get('run_status')}"
        )
        cm.update_conversation_metadata(
            conversation_id,
            {
                "versioning": {
                    "enabled": True,
                    "mode": "overwrite",
                    "tracking_mode": tracking_mode,
                    "last_commit": row.get("commit"),
                    "last_input_seq": int(row.get("seq") or 0),
                    "updated_at": datetime.now().isoformat(),
                }
            },
        )
    except VersioningError as exc:
        debug_log(f"[Versioning] 记录快照失败: {exc}")
    except Exception as exc:
        debug_log(f"[Versioning] 记录快照异常: {exc}")


async def _dispatch_completion_user_notice(
    *,
    web_terminal,
    workspace,
    sender,
    client_sid: str,
    username: str,
    conversation_id: str,
    user_message: str,
    extra_payload: Optional[Dict[str, Any]] = None,
):
    """复用子智能体完成后的 user 代发机制。"""
    extra_payload = extra_payload or {}
    message_source = str(extra_payload.get("message_source") or "").strip().lower()
    if not message_source:
        if extra_payload.get("runtime_mode_notice"):
            message_source = "notify"
        elif extra_payload.get("runtime_guidance"):
            message_source = "guidance"
        elif extra_payload.get("background_command_notice"):
            message_source = "background_command"
        elif extra_payload.get("sub_agent_notice"):
            message_source = "sub_agent"
    if message_source not in {"user", "guidance", "notify", "presend", "sub_agent", "background_command"}:
        message_source = "user"
    try:
        from .tasks import task_manager
        workspace_id = getattr(workspace, "workspace_id", None) or "default"
        host_mode = bool(getattr(workspace, "username", None) == "host")
        session_data = {
            "username": username,
            "role": getattr(web_terminal, "user_role", "user"),
            "is_api_user": getattr(web_terminal, "user_role", "") == "api",
            "host_mode": host_mode,
            "host_workspace_id": workspace_id if host_mode else None,
            "workspace_id": workspace_id,
            "run_mode": getattr(web_terminal, "run_mode", None),
            "thinking_mode": getattr(web_terminal, "thinking_mode", None),
            "model_key": getattr(web_terminal, "model_key", None),
            "message_source": message_source,
        }
        # 关键：通知类后台任务需要把 user_message 写入任务事件流，
        # 否则前端轮询只会看到 AI/tool 事件，看不到 user_message。
        session_data["auto_user_message_event"] = True
        session_data["auto_user_message_payload"] = dict(extra_payload or {})
        rec = task_manager.create_chat_task(
            username,
            workspace_id,
            user_message,
            [],
            conversation_id,
            model_key=session_data.get("model_key"),
            thinking_mode=session_data.get("thinking_mode"),
            run_mode=session_data.get("run_mode"),
            session_data=session_data,
        )
        payload = {
            'message': user_message,
            'conversation_id': conversation_id,
            'task_id': rec.task_id,
            'message_source': message_source,
        }
        payload.update(extra_payload)
        sender('user_message', payload)
        return
    except Exception as e:
        debug_log(f"[CompletionNotice] 创建后台消息任务失败，回退直接执行: {e}")

    payload = {
        'message': user_message,
        'conversation_id': conversation_id,
        'message_source': message_source,
    }
    payload.update(extra_payload)
    sender('user_message', payload)
    try:
        task_handle = asyncio.create_task(handle_task_with_sender(
            terminal=web_terminal,
            workspace=workspace,
            message=user_message,
            images=[],
            sender=sender,
            client_sid=client_sid,
            username=username,
            videos=[],
            auto_user_message_event=True,
        ))
        await task_handle
    except Exception as inner_exc:
        debug_log(f"[CompletionNotice] 回退处理 user_message 失败: {inner_exc}")


def _build_shared_waiting_payload(items: List[Dict[str, Any]]) -> Dict[str, Any]:
    """构建与子智能体一致的 waiting 事件载荷结构。"""
    normalized = []
    for item in items or []:
        summary = item.get('summary') or item.get('command') or '后台任务'
        normalized.append({
            'agent_id': item.get('agent_id') or item.get('command_id') or item.get('task_id'),
            'summary': summary,
        })
    return {
        'count': len(normalized),
        'tasks': normalized,
    }

async def poll_sub_agent_completion(*, web_terminal, workspace, conversation_id, client_sid, username):
    """后台轮询子智能体完成状态，完成后触发新一轮对话"""
    from .extensions import socketio

    manager = getattr(web_terminal, "sub_agent_manager", None)
    if not manager:
        debug_log("[SubAgent] poll_sub_agent_completion: manager 不存在")
        return
    if not hasattr(web_terminal, "_announced_sub_agent_tasks"):
        web_terminal._announced_sub_agent_tasks = set()

    max_wait_time = 3600  # 最多等待1小时
    start_wait = time.time()

    debug_log(f"[SubAgent] 开始后台轮询，conversation_id={conversation_id}, username={username}")

    # 创建 sender 函数，用于发送 socket 事件
    def sender(event_type, data):
        try:
            socketio.emit(event_type, data, room=f"user_{username}")
            debug_log(f"[SubAgent] 发送事件: {event_type}")
        except Exception as e:
            debug_log(f"[SubAgent] 发送事件失败: {event_type}, 错误: {e}")

    while (time.time() - start_wait) < max_wait_time:
        debug_log(f"[SubAgent] 轮询检查...")

        # 检查停止标志
        client_stop_info = get_stop_flag(client_sid, username)
        if client_stop_info:
            stop_requested = client_stop_info.get('stop', False) if isinstance(client_stop_info, dict) else client_stop_info
            if stop_requested:
                debug_log("[SubAgent] 用户请求停止，终止轮询")
                break

        # 若主对话仍在工具循环中，暂不消费完成事件，避免抢占 system 消息插入
        if getattr(web_terminal, "_tool_loop_active", False):
            debug_log("[SubAgent] 主对话工具循环中，延迟后台轮询发送 user 消息")
            await asyncio.sleep(1)
            continue

        updates = manager.poll_updates()
        debug_log(f"[SubAgent] poll_updates 返回 {len(updates)} 个更新")

        for update in updates:
            agent_id = update.get("agent_id")
            summary = update.get("summary")
            result_summary = update.get("result_summary") or update.get("message", "")
            deliverables_dir = update.get("deliverables_dir", "")
            status = update.get("status")
            task_id = update.get("task_id")
            task_info = manager.tasks.get(task_id) if task_id else None
            task_conv_id = task_info.get("conversation_id") if isinstance(task_info, dict) else None
            if task_conv_id and task_conv_id != conversation_id:
                debug_log(f"[SubAgent] 跳过非当前对话任务: task={task_id} conv={task_conv_id} current={conversation_id}")
                continue
            if task_id and task_info is None:
                debug_log(f"[SubAgent] 找不到任务详情，跳过: task={task_id}")
                continue
            if status == "terminated" or (isinstance(task_info, dict) and task_info.get("notified")):
                debug_log(f"[SubAgent] 跳过已终止/已通知任务: task={task_id} status={status}")
                continue

            debug_log(f"[SubAgent] 子智能体{agent_id}完成，状态: {status}")

            # 构建 user 消息（后台完成时才发送）
            prefix = "这是一句系统自动发送的user消息，用于通知你子智能体已经运行完成"
            runtime_line = ""
            elapsed_seconds = update.get("runtime_seconds")
            if elapsed_seconds is None:
                elapsed_seconds = update.get("elapsed_seconds")
            if status == "completed" and isinstance(elapsed_seconds, (int, float)):
                runtime_line = f"\n\n运行了{int(round(elapsed_seconds))}秒"
            user_message = f"""{prefix}

子智能体{agent_id} ({summary}) 已完成任务。

{result_summary}
{runtime_line}

交付目录：{deliverables_dir}"""

            debug_log(f"[SubAgent] 准备发送 user_message: {user_message[:100]}...")

            has_remaining = False
            remaining_count = 0
            try:
                if task_id:
                    web_terminal._announced_sub_agent_tasks.add(task_id)
                    if isinstance(task_info, dict):
                        task_info["notified"] = True
                        task_info["updated_at"] = time.time()
                        try:
                            manager._save_state()
                        except Exception as exc:
                            debug_log(f"[SubAgent] 保存通知状态失败: {exc}")

                # 计算剩余子智能体状态（用于前端清理等待标记）
                if not hasattr(web_terminal, "_announced_sub_agent_tasks"):
                    web_terminal._announced_sub_agent_tasks = set()
                announced = web_terminal._announced_sub_agent_tasks
                running_tasks = [
                    task for task in manager.tasks.values()
                    if isinstance(task, dict)
                    and task.get("status") not in TERMINAL_STATUSES.union({"terminated"})
                    and task.get("run_in_background")
                    and task.get("conversation_id") == conversation_id
                ]
                pending_notice_tasks = [
                    task for task in manager.tasks.values()
                    if isinstance(task, dict)
                    and task.get("status") in TERMINAL_STATUSES.union({"terminated"})
                    and task.get("run_in_background")
                    and task.get("conversation_id") == conversation_id
                    and task.get("task_id") not in announced
                    and not task.get("notified")
                ]
                remaining_count = len(running_tasks) + len(pending_notice_tasks)
                has_remaining = remaining_count > 0
                await _dispatch_completion_user_notice(
                    web_terminal=web_terminal,
                    workspace=workspace,
                    sender=sender,
                    client_sid=client_sid,
                    username=username,
                    conversation_id=conversation_id,
                    user_message=user_message,
                    extra_payload={
                        'sub_agent_notice': True,
                        'message_source': 'sub_agent',
                        'has_running_sub_agents': has_remaining,
                        'remaining_count': remaining_count,
                    },
                )
            except Exception as e:
                debug_log(f"[SubAgent] 创建后台任务失败，回退直接执行: {e}")
                await _dispatch_completion_user_notice(
                    web_terminal=web_terminal,
                    workspace=workspace,
                    sender=sender,
                    client_sid=client_sid,
                    username=username,
                    conversation_id=conversation_id,
                    user_message=user_message,
                    extra_payload={
                        'sub_agent_notice': True,
                        'message_source': 'sub_agent',
                        'has_running_sub_agents': has_remaining,
                        'remaining_count': remaining_count,
                    },
                )

            return  # 只处理第一个完成的子智能体

        # 检查是否还有运行中的任务
        running_tasks = [
            task for task in manager.tasks.values()
            if task.get("status") not in {"completed", "failed", "timeout", "terminated"}
            and task.get("run_in_background")
            and task.get("conversation_id") == conversation_id
        ]

        debug_log(f"[SubAgent] 当前还有 {len(running_tasks)} 个运行中的任务")

        if not running_tasks:
            debug_log("[SubAgent] 所有子智能体已完成")
            # 若状态已提前被更新为终态（poll_updates 返回空），补发完成提示
            completed_tasks = [
                task for task in manager.tasks.values()
                if task.get("status") in {"completed", "failed", "timeout"}
                and task.get("run_in_background")
                and task.get("conversation_id") == conversation_id
                and not task.get("notified")
            ]
            if completed_tasks:
                completed_tasks.sort(
                    key=lambda item: item.get("updated_at") or item.get("created_at") or 0,
                    reverse=True
                )
                task = completed_tasks[0]
                agent_id = task.get("agent_id")
                summary = task.get("summary") or ""
                final_result = task.get("final_result") or {}
                result_summary = (
                    final_result.get("message")
                    or final_result.get("result_summary")
                    or final_result.get("system_message")
                    or ""
                )
                deliverables_dir = final_result.get("deliverables_dir") or task.get("deliverables_dir") or ""
                status = final_result.get("status") or task.get("status")
                debug_log(f"[SubAgent] 补发完成提示: task={task.get('task_id')} status={status}")

                user_message = f"""子智能体{agent_id} ({summary}) 已完成任务。

{result_summary}

交付目录：{deliverables_dir}"""

                try:
                    task_id = task.get("task_id")
                    if task_id:
                        web_terminal._announced_sub_agent_tasks.add(task_id)
                        if isinstance(task, dict):
                            task["notified"] = True
                            task["updated_at"] = time.time()
                            try:
                                manager._save_state()
                            except Exception as exc:
                                debug_log(f"[SubAgent] 保存通知状态失败: {exc}")
                    sender('user_message', {
                        'message': user_message,
                        'conversation_id': conversation_id
                    })
                    from .tasks import task_manager
                    workspace_id = getattr(workspace, "workspace_id", None) or "default"
                    host_mode = bool(getattr(workspace, "username", None) == "host")
                    session_data = {
                        "username": username,
                        "role": getattr(web_terminal, "user_role", "user"),
                        "is_api_user": getattr(web_terminal, "user_role", "") == "api",
                        "host_mode": host_mode,
                        "host_workspace_id": workspace_id if host_mode else None,
                        "workspace_id": workspace_id,
                        "run_mode": getattr(web_terminal, "run_mode", None),
                        "thinking_mode": getattr(web_terminal, "thinking_mode", None),
                        "model_key": getattr(web_terminal, "model_key", None),
                    }
                    # 标记为自动发送的user消息（子智能体完成通知）
                    session_data["auto_user_message_event"] = True
                    session_data["auto_user_message_payload"] = {
                        "sub_agent_notice": True,
                        "message_source": "sub_agent",
                    }
                    rec = task_manager.create_chat_task(
                        username,
                        workspace_id,
                        user_message,
                        [],
                        conversation_id,
                        model_key=session_data.get("model_key"),
                        thinking_mode=session_data.get("thinking_mode"),
                        run_mode=session_data.get("run_mode"),
                        session_data=session_data,
                    )
                    debug_log(f"[SubAgent] 补发通知创建后台任务: task_id={rec.task_id}")
                except Exception as e:
                    debug_log(f"[SubAgent] 补发通知创建后台任务失败，回退直接执行: {e}")
                    try:
                        task_handle = asyncio.create_task(handle_task_with_sender(
                            terminal=web_terminal,
                            workspace=workspace,
                            message=user_message,
                            images=[],
                            sender=sender,
                            client_sid=client_sid,
                            username=username,
                            videos=[]
                        ))
                        await task_handle
                    except Exception as inner_exc:
                        debug_log(f"[SubAgent] 补发完成提示失败: {inner_exc}")
                        import traceback
                        debug_log(f"[SubAgent] 错误堆栈: {traceback.format_exc()}")
            break

        await asyncio.sleep(5)

    debug_log("[SubAgent] 后台轮询结束")


async def poll_background_command_completion(*, web_terminal, workspace, conversation_id: str, client_sid: str, username: str):
    """后台轮询 run_command 后台任务并在主流程结束后代发 user 通知。"""
    from .extensions import socketio

    manager = getattr(web_terminal, "background_command_manager", None)
    if not manager:
        debug_log("[BgCommand] poll_background_command_completion: manager 不存在")
        return

    max_wait_time = 3600
    start_wait = time.time()
    debug_log(f"[BgCommand] 开始后台轮询，conversation_id={conversation_id}, username={username}")

    def sender(event_type, data):
        try:
            socketio.emit(event_type, data, room=f"user_{username}")
        except Exception as e:
            debug_log(f"[BgCommand] 发送事件失败: {event_type}, 错误: {e}")

    while (time.time() - start_wait) < max_wait_time:
        client_stop_info = get_stop_flag(client_sid, username)
        if client_stop_info:
            stop_requested = client_stop_info.get('stop', False) if isinstance(client_stop_info, dict) else client_stop_info
            if stop_requested:
                debug_log("[BgCommand] 用户请求停止，终止轮询")
                break

        if getattr(web_terminal, "_tool_loop_active", False):
            debug_log("[BgCmdDebug] tool_loop_active=True, 延迟 user 代发轮询")
            await asyncio.sleep(1)
            continue

        updates = manager.poll_updates(conversation_id=conversation_id)
        debug_log(f"[BgCmdDebug] background polling updates={len(updates)} conv={conversation_id}")
        for update in updates:
            command_id = update.get("command_id")
            output = update.get("output") or ""
            content = "[后台 run_command 完成]\n" + (output if output else "[no_output]")
            prefix = "这是一句系统自动发送的user消息，用于通知你后台run_command已经运行完成"
            user_message = f"{prefix}\n\n{content}"
            debug_log(f"[BgCmdDebug] preparing user notice command_id={command_id} output_len={len(output)}")
            manager.mark_notified(str(command_id))
            has_remaining = manager.has_pending_for_conversation(conversation_id)
            await _dispatch_completion_user_notice(
                web_terminal=web_terminal,
                workspace=workspace,
                sender=sender,
                client_sid=client_sid,
                username=username,
                conversation_id=conversation_id,
                user_message=user_message,
                extra_payload={
                    # 与子智能体完成通知完全复用同一前端通道/处理逻辑
                    'sub_agent_notice': True,
                    'message_source': 'background_command',
                    'remaining_count': 1 if has_remaining else 0,
                    'has_running_sub_agents': has_remaining,
                    'background_command_notice': True,
                    'has_running_background_commands': has_remaining,
                },
            )
            debug_log(f"[BgCmdDebug] user notice dispatched command_id={command_id} has_remaining={has_remaining}")
            return

        if not manager.has_pending_for_conversation(conversation_id):
            debug_log("[BgCmdDebug] no pending background commands, stop polling")
            break
        await asyncio.sleep(5)

    debug_log("[BgCommand] 后台轮询结束")

async def handle_task_with_sender(
    terminal: WebTerminal,
    workspace: UserWorkspace,
    message,
    images,
    sender,
    client_sid,
    username: str,
    videos=None,
    auto_user_message_event: bool = False,
):
    """处理任务并发送消息 - 集成token统计版本"""
    from .extensions import socketio

    web_terminal = terminal
    conversation_id = getattr(web_terminal.context_manager, "current_conversation_id", None)
    videos = videos or []
    raw_sender = sender

    def sender(event_type, data):
        """为关键事件补充会话标识，便于前端定位报错归属。"""
        if not isinstance(data, dict):
            raw_sender(event_type, data)
            return
        payload = dict(data)
        current_conv = conversation_id or getattr(web_terminal.context_manager, "current_conversation_id", None)

        # 为所有事件添加 conversation_id，确保前端能正确匹配
        if current_conv and event_type not in {"connect", "disconnect", "system_ready"}:
            payload.setdefault("conversation_id", current_conv)

        # 调试信息：记录关键事件
        if event_type in {"user_message", "ai_message_start", "text_start", "text_chunk", "tool_preparing"}:
            debug_log(f"[SENDER] 发送事件: {event_type}, conversation_id={current_conv}, data_keys={list(payload.keys())}")

        # 为关键事件添加额外的标识信息
        if event_type in {"error", "quota_exceeded", "task_stopped", "task_complete"}:
            task_id = getattr(web_terminal, "task_id", None) or client_sid
            if task_id:
                payload.setdefault("task_id", task_id)
            if client_sid:
                payload.setdefault("client_sid", client_sid)

        raw_sender(event_type, payload)
    
    # 如果是思考模式，重置状态
    if web_terminal.thinking_mode:
        web_terminal.api_client.start_new_task(force_deep=web_terminal.deep_thinking_mode)
        state = get_thinking_state(web_terminal)
        state["fast_streak"] = 0
        state["force_next"] = False
        state["suppress_next"] = False
    
    # 添加到对话历史
    user_work_started_at = datetime.now().isoformat()
    user_message_index = -1
    user_work_finalized = False
    history_len_before = len(getattr(web_terminal.context_manager, "conversation_history", []) or [])
    is_first_user_message = history_len_before == 0
    # 构建 user 消息来源与 metadata
    source_from_terminal = str(getattr(web_terminal, "_current_user_message_source", "user") or "user").strip().lower()
    user_message_source = source_from_terminal or "user"
    if auto_user_message_event:
        auto_payload = getattr(web_terminal, "_auto_user_message_payload", None)
        if isinstance(auto_payload, dict):
            src = str(auto_payload.get("message_source") or "").strip().lower()
            if src:
                user_message_source = src
            elif auto_payload.get("runtime_mode_notice"):
                user_message_source = "notify"
            elif auto_payload.get("runtime_guidance"):
                user_message_source = "guidance"
            elif auto_payload.get("background_command_notice"):
                user_message_source = "background_command"
            elif auto_payload.get("sub_agent_notice"):
                user_message_source = "sub_agent"
    if user_message_source not in {"user", "guidance", "notify", "presend", "sub_agent", "background_command"}:
        user_message_source = "user"

    # 构建user消息metadata
    user_message_metadata = {
        "message_source": user_message_source,
    }
    if user_message_source in {"user", "presend", "sub_agent", "background_command"}:
        user_message_metadata["work_timer"] = {
            "status": "working",
            "started_at": user_work_started_at
        }
    # 如果是自动发送的user消息（子智能体/后台命令完成通知），添加标记
    if auto_user_message_event:
        user_message_metadata["is_auto_generated"] = True
        user_message_metadata["auto_message_type"] = "completion_notice"
    saved_user_message = web_terminal.context_manager.add_conversation(
        "user",
        message,
        images=images,
        videos=videos,
        metadata=user_message_metadata
    )
    if not auto_user_message_event:
        try:
            sender(
                'user_message',
                {
                    "message": message,
                    "images": (saved_user_message or {}).get("images") or images or [],
                    "videos": (saved_user_message or {}).get("videos") or videos or [],
                    "media_refs": (saved_user_message or {}).get("media_refs") or [],
                    "message_source": user_message_source,
                    "conversation_id": conversation_id,
                },
            )
        except Exception as exc:
            debug_log(f"[TaskFlow] 发送 user_message 回显失败: {exc}")
    try:
        user_message_index = len(getattr(web_terminal.context_manager, "conversation_history", []) or []) - 1
    except Exception:
        user_message_index = -1
    _prepare_hidden_versioning_baseline_for_first_input(
        web_terminal=web_terminal,
        workspace=workspace,
        conversation_id=conversation_id,
        message=message,
        auto_user_message_event=bool(auto_user_message_event),
    )

    def finalize_user_work_timer():
        nonlocal user_work_finalized
        if user_work_finalized:
            return
        history = getattr(web_terminal.context_manager, "conversation_history", None) or []
        if user_message_index < 0 or user_message_index >= len(history):
            return
        target_msg = history[user_message_index] or {}
        if target_msg.get("role") != "user":
            return
        metadata = target_msg.get("metadata") or {}
        timer = metadata.get("work_timer")
        if not isinstance(timer, dict):
            return
        started_at = timer.get("started_at") or target_msg.get("timestamp") or user_work_started_at
        start_ts = None
        try:
            start_ts = datetime.fromisoformat(str(started_at).replace("Z", "+00:00")).timestamp()
        except Exception:
            start_ts = None
        now_ts = time.time()
        duration_ms = int(max(0.0, (now_ts - start_ts) * 1000.0)) if start_ts is not None else 0
        timer.update({
            "status": "completed",
            "started_at": started_at,
            "finished_at": datetime.now().isoformat(),
            "duration_ms": duration_ms
        })
        metadata["work_timer"] = timer
        target_msg["metadata"] = metadata
        history[user_message_index] = target_msg
        web_terminal.context_manager.auto_save_conversation(force=True)
        user_work_finalized = True

    versioning_checkpoint_recorded = False

    def finalize_run_versioning_checkpoint(run_status: str = "completed"):
        nonlocal versioning_checkpoint_recorded
        if versioning_checkpoint_recorded:
            return
        _record_hidden_versioning_checkpoint_after_run(
            web_terminal=web_terminal,
            workspace=workspace,
            conversation_id=conversation_id,
            message=message,
            message_index=user_message_index,
            auto_user_message_event=bool(auto_user_message_event),
            run_status=run_status,
        )
        versioning_checkpoint_recorded = True

    # Skill 提示系统：检测关键词并在用户消息之后插入 system 消息
    try:
        personal_config = load_personalization_config(workspace.data_dir)
        skill_hints_enabled = personal_config.get("skill_hints_enabled", False)

        if skill_hints_enabled and message:
            hint_manager = SkillHintManager()
            hint_manager.set_enabled(True)
            hint_messages = hint_manager.build_hint_messages(message)

            # 将提示消息插入到对话历史中（在用户消息之后）
            for hint_msg in hint_messages:
                debug_log(f"[Skill Hints] 插入提示消息: {hint_msg['content'][:100]}")
                web_terminal.context_manager.add_conversation(
                    "system",
                    hint_msg["content"]
                )
                # 验证插入后的消息
                last_msg = web_terminal.context_manager.conversation_history[-1]
                debug_log(f"[Skill Hints] 插入后验证 - role: {last_msg.get('role')}, content: {last_msg.get('content')[:100]}")
    except Exception as exc:
        debug_log(f"Skill hints 处理失败: {exc}")

    if is_first_user_message and getattr(web_terminal, "context_manager", None):
        try:
            personal_config = load_personalization_config(workspace.data_dir)
        except Exception:
            personal_config = {}
        auto_title_enabled = personal_config.get("auto_generate_title", True)
        skip_auto_title_generation = bool(
            (web_terminal.context_manager.conversation_metadata or {}).get("skip_auto_title_generation", False)
        )
        if auto_title_enabled and not skip_auto_title_generation:
            conv_id = getattr(web_terminal.context_manager, "current_conversation_id", None)
            socketio.start_background_task(
                generate_conversation_title_background,
                web_terminal,
                conv_id,
                message,
                username
            )

    # 自动深层压缩（用户输入后触发）
    try:
        personal_config = load_personalization_config(workspace.data_dir)
    except Exception:
        personal_config = {}
    compression_settings = resolve_context_compression_settings(personal_config)
    auto_deep_enabled = bool(personal_config.get("auto_deep_compress_enabled", False))
    current_tokens_for_deep = web_terminal.context_manager.get_current_context_tokens(conversation_id)
    if (
        auto_deep_enabled
        and current_tokens_for_deep > compression_settings["deep_trigger_tokens"]
        and not web_terminal.context_manager.is_compression_in_progress()
    ):
        web_terminal.context_manager._set_meta_flag("is_ultra_long_conversation", True)
        sender('compression_state', {
            "conversation_id": conversation_id,
            "in_progress": True,
            "mode": "auto",
            "stage": "queued"
        })
        deep_result = await run_deep_compression(
            web_terminal=web_terminal,
            workspace=workspace,
            conversation_id=conversation_id,
            mode="auto",
            sender=sender,
        )
        if not deep_result.get("success"):
            sender('error', {
                "message": deep_result.get("error") or "自动深层压缩失败",
                "conversation_id": conversation_id,
            })
        else:
            guide_message = (deep_result.get("guide_message") or "").strip()
            if guide_message:
                finalize_user_work_timer()
                finalize_run_versioning_checkpoint("auto_deep_compress_handoff")
                await handle_task_with_sender(
                    web_terminal,
                    workspace,
                    guide_message,
                    [],
                    sender,
                    client_sid,
                    username,
                    []
                )
                return
        finalize_user_work_timer()
        finalize_run_versioning_checkpoint("auto_deep_compress_end")
        return
    
    # === 移除：不在这里计算输入token，改为在每次API调用前计算 ===
    
    # 构建上下文和消息（用于API调用）
    context = web_terminal.build_context()
    messages = web_terminal.build_messages(context, message)
    tools = web_terminal.define_tools()
    try:
        profile = get_model_profile(getattr(web_terminal, "model_key", None) or "kimi-k2.5")
        web_terminal.apply_model_profile(profile)
    except Exception as exc:
        debug_log(f"更新模型配置失败: {exc}")

    # === 上下文预算与安全校验（避免超出模型上下文） ===
    max_context_tokens = get_model_context_window(getattr(web_terminal, "model_key", None) or "kimi-k2.5")
    current_tokens = web_terminal.context_manager.get_current_context_tokens(conversation_id)
    # 提前同步给底层客户端，动态收缩 max_tokens
    web_terminal.api_client.update_context_budget(current_tokens, max_context_tokens)
    if max_context_tokens:
        if current_tokens >= max_context_tokens:
            err_msg = (
                f"当前对话上下文已达 {current_tokens} tokens，超过模型上限 "
                f"{max_context_tokens}，请先使用压缩功能或清理对话后再试。"
            )
            debug_log(err_msg)
            web_terminal.context_manager.add_conversation("system", err_msg)
            sender('error', {
                'message': err_msg,
                'status_code': 400,
                'error_type': 'context_overflow'
            })
            finalize_user_work_timer()
            finalize_run_versioning_checkpoint("context_overflow")
            return
        usage_percent = (current_tokens / max_context_tokens) * 100
        warned = web_terminal.context_manager.conversation_metadata.get("context_warning_sent", False)
        if usage_percent >= 70 and not warned:
            warn_msg = (
                f"当前对话上下文约占 {usage_percent:.1f}%（{current_tokens}/{max_context_tokens}），"
                "建议使用压缩功能。"
            )
            web_terminal.context_manager.conversation_metadata["context_warning_sent"] = True
            web_terminal.context_manager.auto_save_conversation(force=True)
            sender('context_warning', {
                'title': '上下文过长',
                'message': warn_msg,
                'type': 'warning',
                'conversation_id': conversation_id
            })
    
    # 开始新的AI消息
    sender('ai_message_start', {})
    
    # 增量保存相关变量
    accumulated_response = ""   # 累积的响应内容
    is_first_iteration = True   # 是否是第一次迭代
    
    # 统计和限制变量
    total_iterations = 0
    total_tool_calls = 0
    consecutive_same_tool = defaultdict(int)
    last_tool_name = ""
    auto_fix_attempts = 0
    last_tool_call_time = 0
    detected_tool_intent: Dict[str, str] = {}
    
    # 设置最大迭代次数（API 可覆盖）；None 表示不限制
    max_iterations_override = getattr(web_terminal, "max_iterations_override", None)
    max_iterations = max_iterations_override if max_iterations_override is not None else MAX_ITERATIONS_PER_TASK
    max_api_retries = 4
    retry_delay_seconds = 10
    

    iteration = 0
    while max_iterations is None or iteration < max_iterations:
        # 检查停止标志
        stop_entry = get_stop_flag(client_sid, username)
        if stop_entry and stop_entry.get('stop'):
            debug_log(f"[Task] 检测到停止标志，退出循环")
            sender('task_stopped', {
                'message': '任务已停止',
                'reason': 'user_requested'
            })
            break

        current_iteration = iteration + 1
        iteration += 1
        total_iterations += 1
        iteration_limit_label = max_iterations if max_iterations is not None else "∞"
        debug_log(f"\n--- 迭代 {current_iteration}/{iteration_limit_label} 开始 ---")
        
        # 检查是否超过总工具调用限制
        if MAX_TOTAL_TOOL_CALLS is not None and total_tool_calls >= MAX_TOTAL_TOOL_CALLS:
            debug_log(f"已达到最大工具调用次数限制 ({MAX_TOTAL_TOOL_CALLS})")
            sender('system_message', {
                'content': f'⚠️ 已达到最大工具调用次数限制 ({MAX_TOTAL_TOOL_CALLS})，任务结束。'
            })
            mark_force_thinking(web_terminal, reason="tool_limit")
            break
        
        apply_thinking_schedule(web_terminal)

        full_response = ""
        tool_calls = []
        current_thinking = ""
        detected_tools = {}
        last_usage_payload = None
        
        # 状态标志
        in_thinking = False
        thinking_started = False
        thinking_ended = False
        text_started = False
        text_has_content = False
        text_streaming = False
        text_chunk_index = 0
        last_text_chunk_time: Optional[float] = None
        
        # 计数器
        chunk_count = 0
        reasoning_chunks = 0
        content_chunks = 0
        tool_chunks = 0
        last_finish_reason = None

        thinking_expected = web_terminal.api_client.get_current_thinking_mode()
        debug_log(f"思考模式: {thinking_expected}")
        quota_allowed = True
        quota_info = {}
        if hasattr(web_terminal, "record_model_call"):
            quota_allowed, quota_info = web_terminal.record_model_call(bool(thinking_expected))
        if not quota_allowed:
            quota_type = 'thinking' if thinking_expected else 'fast'
            socketio.emit('quota_notice', {
                'type': quota_type,
                'reset_at': quota_info.get('reset_at'),
                'limit': quota_info.get('limit'),
                'count': quota_info.get('count')
            }, room=f"user_{getattr(web_terminal, 'username', '')}")
            sender('quota_exceeded', {
                'type': quota_type,
                'reset_at': quota_info.get('reset_at')
            })
            sender('error', {
                'message': "配额已达到上限，暂时无法继续调用模型。",
                'quota': quota_info
            })
            finalize_user_work_timer()
            finalize_run_versioning_checkpoint("quota_exceeded")
            return
        
        tool_call_limit_label = MAX_TOTAL_TOOL_CALLS if MAX_TOTAL_TOOL_CALLS is not None else "∞"
        print(f"[API] 第{current_iteration}次调用 (总工具调用: {total_tool_calls}/{tool_call_limit_label})")
        
        stream_result = await run_streaming_attempts(
            web_terminal=web_terminal,
            messages=messages,
            tools=tools,
            sender=sender,
            client_sid=client_sid,
            username=username,
            conversation_id=conversation_id,
            current_iteration=current_iteration,
            max_api_retries=max_api_retries,
            retry_delay_seconds=retry_delay_seconds,
            detected_tool_intent=detected_tool_intent,
            full_response=full_response,
            tool_calls=tool_calls,
            current_thinking=current_thinking,
            detected_tools=detected_tools,
            last_usage_payload=last_usage_payload,
            in_thinking=in_thinking,
            thinking_started=thinking_started,
            thinking_ended=thinking_ended,
            text_started=text_started,
            text_has_content=text_has_content,
            text_streaming=text_streaming,
            text_chunk_index=text_chunk_index,
            last_text_chunk_time=last_text_chunk_time,
            chunk_count=chunk_count,
            reasoning_chunks=reasoning_chunks,
            content_chunks=content_chunks,
            tool_chunks=tool_chunks,
            last_finish_reason=last_finish_reason,
            accumulated_response=accumulated_response,
        )
        if stream_result.get("stopped"):
            finalize_user_work_timer()
            finalize_run_versioning_checkpoint("stopped")
            return

        full_response = stream_result["full_response"]
        tool_calls = stream_result["tool_calls"]
        current_thinking = stream_result["current_thinking"]
        detected_tools = stream_result["detected_tools"]
        last_usage_payload = stream_result["last_usage_payload"]
        in_thinking = stream_result["in_thinking"]
        thinking_started = stream_result["thinking_started"]
        thinking_ended = stream_result["thinking_ended"]
        text_started = stream_result["text_started"]
        text_has_content = stream_result["text_has_content"]
        text_streaming = stream_result["text_streaming"]
        text_chunk_index = stream_result["text_chunk_index"]
        last_text_chunk_time = stream_result["last_text_chunk_time"]
        chunk_count = stream_result["chunk_count"]
        reasoning_chunks = stream_result["reasoning_chunks"]
        content_chunks = stream_result["content_chunks"]
        tool_chunks = stream_result["tool_chunks"]
        last_finish_reason = stream_result["last_finish_reason"]
        accumulated_response = stream_result["accumulated_response"]

        # 流结束后的处理
        debug_log(f"\n流结束统计:")
        debug_log(f"  总chunks: {chunk_count}")
        debug_log(f"  思考chunks: {reasoning_chunks}")
        debug_log(f"  内容chunks: {content_chunks}")
        debug_log(f"  工具chunks: {tool_chunks}")
        debug_log(f"  收集到的思考: {len(current_thinking)} 字符")
        debug_log(f"  收集到的正文: {len(full_response)} 字符")
        debug_log(f"  收集到的工具: {len(tool_calls)} 个")
        
        # 结束未完成的流
        if in_thinking and not thinking_ended:
            sender('thinking_end', {'full_content': current_thinking})
            await asyncio.sleep(0.1)
            
        
        # 确保text_end事件被发送
        if text_started and text_has_content:
            debug_log(f"发送text_end事件，完整内容长度: {len(full_response)}")
            sender('text_end', {'full_content': full_response})
            await asyncio.sleep(0.1)
            text_streaming = False
            
            if full_response.strip():
                debug_log(f"流式文本内容长度: {len(full_response)} 字符")
        
        if web_terminal.api_client.last_call_used_thinking and current_thinking:
            web_terminal.api_client.current_task_thinking = current_thinking or ""
        if web_terminal.api_client.current_task_first_call:
            web_terminal.api_client.current_task_first_call = False
        update_thinking_after_call(web_terminal)
        
        # 检测是否有格式错误的工具调用
        if not tool_calls and full_response and AUTO_FIX_TOOL_CALL:
            if detect_malformed_tool_call(full_response):
                auto_fix_attempts += 1
                
                if auto_fix_attempts <= AUTO_FIX_MAX_ATTEMPTS:
                    debug_log(f"检测到格式错误的工具调用，尝试自动修复 (尝试 {auto_fix_attempts}/{AUTO_FIX_MAX_ATTEMPTS})")
                    
                    fix_message = "你使用了错误的格式输出工具调用。请使用正确的工具调用格式而不是直接输出JSON。根据当前进度继续执行任务。"
                    
                    sender('system_message', {
                        'content': f'⚠️ 自动修复: {fix_message}'
                    })
                    maybe_mark_failure_from_message(web_terminal, f'⚠️ 自动修复: {fix_message}')
                    
                    messages.append({
                        "role": "user",
                        "content": fix_message
                    })
                    
                    await asyncio.sleep(1)
                    continue
                else:
                    debug_log(f"自动修复尝试已达上限 ({AUTO_FIX_MAX_ATTEMPTS})")
                    sender('system_message', {
                        'content': f'⌘ 工具调用格式错误，自动修复失败。请手动检查并重试。'
                    })
                    maybe_mark_failure_from_message(web_terminal, '⌘ 工具调用格式错误，自动修复失败。请手动检查并重试。')
                    break
        
        # 构建助手消息（用于API继续对话）
        assistant_content_parts = []
        
        if full_response:
            assistant_content_parts.append(full_response)
        
        assistant_content = "\n".join(assistant_content_parts) if assistant_content_parts else ""
        
        # 添加到消息历史（用于API继续对话，不保存到文件）
        assistant_message = {
            "role": "assistant",
            "content": assistant_content,
            "tool_calls": tool_calls,
            # thinking 模式下，reasoning_content 需要原样回传；即使该轮为空字符串也保留字段
            "reasoning_content": current_thinking or "",
        }
        
        messages.append(assistant_message)
        if assistant_content or current_thinking or tool_calls:
            web_terminal.context_manager.add_conversation(
                "assistant",
                assistant_content,
                tool_calls=tool_calls if tool_calls else None,
                reasoning_content=current_thinking or ""
            )
        
        # 为下一轮迭代重置流状态标志，但保留 full_response 供上面保存使用
        text_streaming = False
        text_started = False
        text_has_content = False
        full_response = ""

        if not tool_calls:
            debug_log("没有工具调用，结束迭代")
            break
        
        # 检查连续相同工具调用
        for tc in tool_calls:
            tool_name = tc["function"]["name"]
            
            if tool_name == last_tool_name:
                consecutive_same_tool[tool_name] += 1
                
                if (
                    MAX_CONSECUTIVE_SAME_TOOL is not None
                    and consecutive_same_tool[tool_name] >= MAX_CONSECUTIVE_SAME_TOOL
                ):
                    debug_log(f"警告: 连续调用相同工具 {tool_name} 已达 {MAX_CONSECUTIVE_SAME_TOOL} 次")
                    sender('system_message', {
                        'content': f'⚠️ 检测到重复调用 {tool_name} 工具 {MAX_CONSECUTIVE_SAME_TOOL} 次，可能存在循环。'
                    })
                    maybe_mark_failure_from_message(web_terminal, f'⚠️ 检测到重复调用 {tool_name} 工具 {MAX_CONSECUTIVE_SAME_TOOL} 次，可能存在循环。')
                    
                    if consecutive_same_tool[tool_name] >= MAX_CONSECUTIVE_SAME_TOOL + 2:
                        debug_log(f"终止: 工具 {tool_name} 调用次数过多")
                        sender('system_message', {
                            'content': f'⌘ 工具 {tool_name} 重复调用过多，任务终止。'
                        })
                        maybe_mark_failure_from_message(web_terminal, f'⌘ 工具 {tool_name} 重复调用过多，任务终止。')
                        break
            else:
                consecutive_same_tool.clear()
                consecutive_same_tool[tool_name] = 1
            
            last_tool_name = tool_name

        # 更新统计
        total_tool_calls += len(tool_calls)

        # 执行每个工具
        tool_loop_result = await execute_tool_calls(
            web_terminal=web_terminal,
            tool_calls=tool_calls,
            sender=sender,
            messages=messages,
            client_sid=client_sid,
            username=username,
            iteration=iteration,
            conversation_id=conversation_id,
            last_tool_call_time=last_tool_call_time,
            process_sub_agent_updates=process_sub_agent_updates,
            process_background_command_updates=process_background_command_updates,
            maybe_mark_failure_from_message=maybe_mark_failure_from_message,
            mark_force_thinking=mark_force_thinking,
            get_stop_flag=get_stop_flag,
            clear_stop_flag=clear_stop_flag,
            workspace=workspace,
        )
        last_tool_call_time = tool_loop_result.get("last_tool_call_time", last_tool_call_time)
        if tool_loop_result.get("stopped"):
            finalize_user_work_timer()
            finalize_run_versioning_checkpoint("stopped")
            return
        if tool_loop_result.get("approval_rejected"):
            sender('task_stopped', {
                'message': tool_loop_result.get("approval_message") or '操作被用户拒绝',
                'reason': 'approval_rejected',
                'conversation_id': conversation_id
            })
            finalize_user_work_timer()
            finalize_run_versioning_checkpoint("approval_rejected")
            return
        if tool_loop_result.get("deep_compressed"):
            deep_result = tool_loop_result.get("deep_result") or {}
            guide_message = (deep_result.get("guide_message") or "").strip()
            if deep_result.get("success") and guide_message:
                finalize_user_work_timer()
                finalize_run_versioning_checkpoint("deep_compress_handoff")
                await handle_task_with_sender(
                    web_terminal,
                    workspace,
                    guide_message,
                    [],
                    sender,
                    client_sid,
                    username,
                    []
                )
                return
            finalize_user_work_timer()
            finalize_run_versioning_checkpoint("deep_compress_end")
            return
        
        # 标记不再是第一次迭代
        is_first_iteration = False

    
    # 最终统计
    debug_log(f"\n{'='*40}")
    debug_log(f"任务完成统计:")
    debug_log(f"  总迭代次数: {total_iterations}")
    debug_log(f"  总工具调用: {total_tool_calls}")
    debug_log(f"  自动修复尝试: {auto_fix_attempts}")
    debug_log(f"  累积响应: {len(accumulated_response)} 字符")
    debug_log(f"{'='*40}\n")
    
    # 检查是否有后台运行的子智能体或待通知的完成任务
    manager = getattr(web_terminal, "sub_agent_manager", None)
    has_running_sub_agents = False
    bg_manager = getattr(web_terminal, "background_command_manager", None)
    has_running_background_commands = False
    if manager:
        try:
            manager.reconcile_task_states(conversation_id=conversation_id)
        except Exception as exc:
            debug_log(f"[SubAgent] reconcile_task_states failed: {exc}")
        if not hasattr(web_terminal, "_announced_sub_agent_tasks"):
            web_terminal._announced_sub_agent_tasks = set()
        running_tasks = [
            task for task in manager.tasks.values()
            if task.get("status") not in TERMINAL_STATUSES.union({"terminated"})
            and task.get("run_in_background")
            and task.get("conversation_id") == conversation_id
        ]
        pending_notice_tasks = [
            task for task in manager.tasks.values()
            if task.get("status") in TERMINAL_STATUSES.union({"terminated"})
            and task.get("run_in_background")
            and task.get("conversation_id") == conversation_id
            and task.get("task_id") not in web_terminal._announced_sub_agent_tasks
        ]

        if running_tasks or pending_notice_tasks:
            has_running_sub_agents = True
            notify_tasks = running_tasks + pending_notice_tasks
            debug_log(f"[SubAgent] 后台子智能体等待: running={len(running_tasks)} pending_notice={len(pending_notice_tasks)}")
            # 先通知前端：有子智能体在运行/待通知，保持等待状态
            sender('sub_agent_waiting', {
                'count': len(notify_tasks),
                'tasks': [{'agent_id': t.get('agent_id'), 'summary': t.get('summary')} for t in notify_tasks]
            })

            # 启动后台任务来轮询/补发子智能体完成
            def run_poll():
                import asyncio
                loop = asyncio.new_event_loop()
                asyncio.set_event_loop(loop)
                try:
                    loop.run_until_complete(poll_sub_agent_completion(
                        web_terminal=web_terminal,
                        workspace=workspace,
                        conversation_id=conversation_id,
                        client_sid=client_sid,
                        username=username
                    ))
                finally:
                    loop.close()

            socketio.start_background_task(run_poll)

    # 检查是否有后台 run_command 或待通知任务
    if bg_manager and conversation_id:
        try:
            bg_manager.reconcile_stale_records(conversation_id=conversation_id)
        except Exception as exc:
            debug_log(f"[BgCmdDebug] reconcile_stale_records failed: {exc}")
        waiting_items = bg_manager.list_waiting_items(conversation_id)
        if waiting_items:
            has_running_background_commands = True
            # 与子智能体完全复用同一 waiting 事件（前端已有稳定处理链路）
            sender('sub_agent_waiting', _build_shared_waiting_payload(waiting_items))

            def run_bg_poll():
                import asyncio
                loop = asyncio.new_event_loop()
                asyncio.set_event_loop(loop)
                try:
                    loop.run_until_complete(poll_background_command_completion(
                        web_terminal=web_terminal,
                        workspace=workspace,
                        conversation_id=conversation_id,
                        client_sid=client_sid,
                        username=username
                    ))
                finally:
                    loop.close()

            socketio.start_background_task(run_bg_poll)

    has_running_completion_jobs = has_running_sub_agents or has_running_background_commands
    pending_runtime_guidance_messages: List[str] = []
    try:
        from .tasks import task_manager

        pending_runtime_guidance_messages = task_manager.consume_runtime_guidance_messages(
            username=username,
            task_id=client_sid,
        )
    except Exception as exc:
        debug_log(f"[RuntimeGuidance] 读取剩余引导消息失败: {exc}")
        pending_runtime_guidance_messages = []

    # 发送完成事件（如果有后台完成任务在运行，前端会保持等待状态）
    if not has_running_completion_jobs:
        finalize_user_work_timer()
        finalize_run_versioning_checkpoint("completed")
    else:
        finalize_run_versioning_checkpoint("waiting_background")
    sender('task_complete', {
        'total_iterations': total_iterations,
        'total_tool_calls': total_tool_calls,
        'auto_fix_attempts': auto_fix_attempts,
        # 沿用子智能体字段，确保前端直接走已验证通路
        'has_running_sub_agents': has_running_completion_jobs,
        'has_running_background_commands': has_running_background_commands,
        'pending_runtime_guidance_messages': pending_runtime_guidance_messages,
    })
