const { v4: uuidv4 } = require('uuid');
const fs = require('fs');
const db = require('../../db/database');
const { compact } = require('./compaction');
const { compactPayloadForModel } = require('./preModelCompaction');
const {
  getConversationContext,
  buildSummaryCarrier,
  refreshConversationSummary,
  sanitizeConversationMessages
} = require('./history');
const { ensureDefaultAiSettings, getAiSettings } = require('./settings');
const {
  activateTools,
  buildToolCatalog,
  selectInitialTools,
  selectToolsForTask,
} = require('./toolSelector');
const { compactToolResult } = require('./toolResult');
const { salvageTextToolCalls } = require('./toolCallSalvage');
const { sanitizeModelOutput } = require('./outputSanitizer');
const {
  buildAnalysisPrompt,
  buildExecutionGuidance,
  buildPlanPrompt,
  buildVerifierPrompt,
  isDirectAnswerEligibleAnalysis,
  normalizeExecutionPlan,
  normalizeTaskAnalysis,
  normalizeVerificationResult,
  parseJsonObject,
  promoteAnalysisMode,
  shouldRunVerifier,
} = require('./taskAnalysis');
const { getCapabilityHealth, summarizeCapabilityHealth } = require('./capabilityHealth');
const {
  buildPlatformFormattingGuide,
  splitOutgoingMessageForPlatform,
} = require('../messaging/formatting_guides');
const {
  buildInterimMetadata,
  buildInterimSignature,
  normalizeInterimKind,
} = require('./interim');
const { recordRunEvent } = require('./runEvents');
const {
  buildDeliverableWorkflowGuidance,
  DeliverableValidationError,
  extractArtifactsFromResult,
  getDeliverableWorkflow,
  selectDeliverableWorkflow,
  validateDeliverableExecution,
} = require('./deliverables');
const { buildLoopPolicy, resolveToolResultLimits } = require('./loopPolicy');
const { globalHooks } = require('./hooks');
const { withProviderRetry, isTransientError } = require('./providerRetry');
const { normalizeCompletionConfidence, shouldAcceptTaskComplete } = require('./completion');
const { normalizeUsage, recordModelUsage } = require('./usage');
const { ToolRepetitionGuard } = require('./repetitionGuard');
const { shortenRunId, summarizeForLog, parseMaybeJson } = require('./logFormat');
const {
  normalizeOutgoingMessage,
  clampRunContext,
  joinSentMessages,
  normalizeInterimText,
  buildBlankMessagingReplyPrompt,
  buildDeterministicMessagingFallback,
  buildMessagingFailureScenario,
  buildDeterministicMessagingErrorReply,
  buildModelFailureLoopPrompt,
} = require('./messagingFallback');
const {
  classifyToolExecution,
  summarizeToolExecutions,
  summarizeAvailableTools,
  inferToolFailureMessage,
  buildAutonomousRecoveryContext,
} = require('./toolEvidence');

function generateTitle(task) {
  if (!task || typeof task !== 'string') return 'Untitled';
  const msgMatch = task.match(/received a (?:message|media|image|video|file|audio)[^:]*:\s*(.+)/is);
  if (msgMatch) {
    const body = msgMatch[1].replace(/\n[\s\S]*/s, '').trim();
    return body.slice(0, 90) || 'Incoming message';
  }
  const cleaned = task.replace(/^\[.*?\]\s*/i, '').replace(/^(system|task|prompt)[:\s]+/i, '').trim();
  return cleaned.slice(0, 90);
}

function buildInitialRunMetadata(options = {}) {
  const metadata = {};
  if (options.taskId != null && String(options.taskId).trim()) {
    metadata.taskId = options.taskId;
  }
  if (options.widgetId != null && String(options.widgetId).trim()) {
    metadata.widgetId = options.widgetId;
  }
  return metadata;
}

function planningDepthForForceMode(forceMode) {
  return forceMode === 'plan_execute' ? 'deep' : 'light';
}

function buildSkipTaskAnalysisResult(forceMode) {
  return {
    mode: forceMode === 'plan_execute' ? 'plan_execute' : 'execute',
    reply_mode: 'task',
    freshness_risk: 'none',
    verification_need: 'none',
    planning_depth: planningDepthForForceMode(forceMode),
    confidence: 0.5,
    suggested_tools: [],
    needs_subagents: false,
    draft_reply: '',
    goal: 'Complete the user request accurately.',
    success_criteria: [],
    complexity: forceMode === 'plan_execute' ? 'complex' : 'standard',
    autonomy_level: forceMode === 'plan_execute' ? 'high' : 'normal',
    progress_update_policy: 'optional',
    parallel_work: false,
    completion_confidence_required: forceMode === 'plan_execute' ? 'high' : 'medium',
  };
}

function buildAnalyzeTaskFallback(forceMode, userMessage = '') {
  return {
    mode: forceMode || 'execute',
    verification_need: 'light',
    planning_depth: planningDepthForForceMode(forceMode),
    goal: userMessage ? String(userMessage).trim().slice(0, 300) : '',
    complexity: forceMode === 'plan_execute' ? 'complex' : 'standard',
    autonomy_level: forceMode === 'plan_execute' ? 'high' : 'normal',
    progress_update_policy: 'optional',
    parallel_work: false,
    completion_confidence_required: forceMode === 'plan_execute' ? 'high' : 'medium',
  };
}

function applyForcedAnalysisMode(analysis, forceMode) {
  if (!analysis || typeof analysis !== 'object') return analysis;
  if (forceMode !== 'plan_execute') return analysis;
  return {
    ...analysis,
    mode: 'plan_execute',
    planning_depth: 'deep',
    complexity: 'complex',
    autonomy_level: 'high',
    completion_confidence_required: analysis.completion_confidence_required || 'high',
  };
}

function buildAutonomyPolicyFromAnalysis(analysis = {}) {
  return {
    complexity: analysis.complexity || 'standard',
    autonomy_level: analysis.autonomy_level || 'normal',
    progress_update_policy: analysis.progress_update_policy || 'optional',
    parallel_work: analysis.parallel_work === true,
    completion_confidence_required: analysis.completion_confidence_required || 'medium',
  };
}

async function getProviderForUser(userId, task = '', isSubagent = false, modelOverride = null, providerConfig = {}) {
  const { getSupportedModels, createProviderInstance } = require('./models');
  const agentId = providerConfig.agentId || null;
  const aiSettings = getAiSettings(userId, agentId);
  const models = await getSupportedModels(userId, agentId);

  let enabledIds = Array.isArray(aiSettings.enabled_models) ? aiSettings.enabled_models : [];
  const defaultChatModel = aiSettings.default_chat_model || 'auto';
  const defaultSubagentModel = aiSettings.default_subagent_model || 'auto';
  const smarterSelection = aiSettings.smarter_model_selector !== false && aiSettings.smarter_model_selector !== 'false';

  const knownModelIds = new Set(models.map((m) => m.id));
  const selectableModels = models.filter((m) => m.available !== false);

  enabledIds = Array.isArray(enabledIds)
    ? enabledIds
      .map((id) => String(id))
      .filter((id) => knownModelIds.has(id))
    : [];

  let availableModels = selectableModels.filter((m) => enabledIds.includes(m.id));
  if (availableModels.length === 0) {
    enabledIds = selectableModels.map((m) => m.id);
    availableModels = [...selectableModels];
  }

  const fallbackModel = availableModels.length > 0 ? availableModels[0] : selectableModels[0];

  if (!fallbackModel) {
    throw new Error('No AI providers are currently available. Open Settings and configure at least one provider.');
  }

  let selectedModelDef = fallbackModel;
  const userSelectedDefault = isSubagent ? defaultSubagentModel : defaultChatModel;

  if (modelOverride && typeof modelOverride === 'string') {
    const requested = models.find((m) => m.id === modelOverride.trim());
    if (requested && requested.available !== false && enabledIds.includes(requested.id)) {
      selectedModelDef = requested;
      return {
        provider: createProviderInstance(selectedModelDef.provider, userId, providerConfig),
        model: selectedModelDef.id,
        providerName: selectedModelDef.provider
      };
    }
  }

  if (userSelectedDefault && userSelectedDefault !== 'auto') {
    selectedModelDef = models.find((m) => m.id === userSelectedDefault) || fallbackModel;
  } else {
    const selectionHint = providerConfig.selectionHint && typeof providerConfig.selectionHint === 'object'
      ? providerConfig.selectionHint
      : {};
    const preferredPurpose = String(selectionHint.purpose || '').trim().toLowerCase();
    const highAutonomy = selectionHint.autonomyLevel === 'high' || selectionHint.complexity === 'complex';
    const requiredConfidence = String(selectionHint.requiredConfidence || '').trim().toLowerCase();
    const costMode = String(selectionHint.costMode || aiSettings.cost_mode || 'balanced_auto').trim().toLowerCase();
    const requestedPurpose = ['planning', 'coding', 'general', 'fast'].includes(preferredPurpose)
      ? preferredPurpose
      : '';
    const priceRank = { free: 0, cheap: 1, medium: 2, expensive: 3 };
    const chooseForPurpose = (purpose) => {
      const candidates = availableModels.filter((model) => model.purpose === purpose);
      if (candidates.length === 0) return null;
      if (['economy', 'cost_saver', 'lowest_cost'].includes(costMode)) {
        return [...candidates].sort((left, right) => (
          (priceRank[left.priceTier] ?? 99) - (priceRank[right.priceTier] ?? 99)
        ))[0];
      }
      if (['quality', 'highest_quality'].includes(costMode) || requiredConfidence === 'high') {
        return candidates.find((model) => model.priceTier !== 'free' && model.priceTier !== 'cheap') || candidates[0];
      }
      return candidates[0];
    };

    if (smarterSelection && requestedPurpose) {
      selectedModelDef = chooseForPurpose(requestedPurpose) || fallbackModel;
    } else if (smarterSelection && highAutonomy) {
      selectedModelDef = chooseForPurpose('planning') || chooseForPurpose('general') || fallbackModel;
    } else if (isSubagent) {
      selectedModelDef = chooseForPurpose('fast') || fallbackModel;
    } else {
      selectedModelDef = chooseForPurpose('general') || fallbackModel;
    }
  }

  return {
    provider: createProviderInstance(selectedModelDef.provider, userId, providerConfig),
    model: selectedModelDef.id,
    providerName: selectedModelDef.provider
  };
}

async function getFailureFallbackModelId(userId, agentId, currentModelId, preferredFallbackId = null, failureError = null) {
  const { getSupportedModels } = require('./models');
  const aiSettings = getAiSettings(userId, agentId);
  const models = await getSupportedModels(userId, agentId);
  const availableModels = models.filter((model) => model.available !== false);
  const knownIds = new Set(availableModels.map((model) => model.id));
  const enabledIds = Array.isArray(aiSettings.enabled_models)
    ? aiSettings.enabled_models.map((id) => String(id)).filter((id) => knownIds.has(id))
    : [];
  const pool = enabledIds.length > 0
    ? availableModels.filter((model) => enabledIds.includes(model.id))
    : availableModels;
  const currentModel = pool.find((model) => model.id === currentModelId)
    || availableModels.find((model) => model.id === currentModelId)
    || null;

  // When the failure is a provider-level rate limit, the preferred fallback is
  // likely on the same provider and will hit the same limit. Skip it and prefer
  // a fallback from a different provider instead.
  const isProviderRateLimit = /429|rate.?limit|free-models-per/i.test(String(failureError?.message || ''));

  if (preferredFallbackId && preferredFallbackId !== currentModelId && !isProviderRateLimit) {
    const preferred = pool.find((model) => model.id === preferredFallbackId)
      || availableModels.find((model) => model.id === preferredFallbackId);
    if (preferred) return preferred.id;
  }

  if (currentModel?.provider) {
    const differentProvider = pool.find((model) => model.id !== currentModelId && model.provider !== currentModel.provider)
      || availableModels.find((model) => model.id !== currentModelId && model.provider !== currentModel.provider);
    if (differentProvider) return differentProvider.id;
  }

  // If no different-provider model exists, still try the preferred fallback
  // even on rate limits (it's better than nothing).
  if (preferredFallbackId && preferredFallbackId !== currentModelId) {
    const preferred = pool.find((model) => model.id === preferredFallbackId)
      || availableModels.find((model) => model.id === preferredFallbackId);
    if (preferred) return preferred.id;
  }

  const differentModel = pool.find((model) => model.id !== currentModelId)
    || availableModels.find((model) => model.id !== currentModelId);
  return differentModel?.id || null;
}

function estimateTokenValue(value) {
  if (!value) return 0;
  if (typeof value === 'string') return Math.ceil(value.length / 4);
  return Math.ceil(JSON.stringify(value).length / 4);
}

class AgentEngine {
  constructor(io, services = {}) {
    this.io = io;
    this.activeRuns = new Map();
    this.subagents = new Map();
    this.app = services.app || null;
    this.browserController = services.browserController || null;
    this.androidController = services.androidController || null;
    this.runtimeManager = services.runtimeManager || null;
    this.workspaceManager = services.workspaceManager || null;
    this.messagingManager = services.messagingManager || null;
    this.mcpManager = services.mcpManager || services.mcpClient || null;
    this.skillRunner = services.skillRunner || null;
    this.taskRuntime = services.taskRuntime || null;
    this.memoryManager = services.memoryManager || null;
    this.voiceRuntimeManager = services.voiceRuntimeManager || null;
  }

  async buildSystemPrompt(userId, context = {}) {
    const { buildSystemPromptSections } = require('./systemPrompt');
    const { MemoryManager } = require('../memory/manager');
    const memoryManager = this.memoryManager || new MemoryManager();
    const promptSections = await buildSystemPromptSections(userId, context, memoryManager);
    const skillRunner = context.skillRunner || this.skillRunner || null;
    const skillsPrompt = skillRunner?.getSkillsForPrompt?.({
      maxTotalChars: 9000,
      maxDescriptionChars: 180,
      maxTriggerChars: 100,
    }) || '';
    return {
      stable: [promptSections.stable, skillsPrompt].filter(Boolean).join('\n\n'),
      dynamic: promptSections.dynamic,
    };
  }

  persistRunMetadata(runId, patch = {}) {
    if (!runId || !patch || typeof patch !== 'object') return;
    const existing = db.prepare('SELECT metadata_json FROM agent_runs WHERE id = ?').get(runId);
    const current = parseMaybeJson(existing?.metadata_json, {}) || {};
    const next = { ...current, ...patch };
    db.prepare('UPDATE agent_runs SET metadata_json = ? WHERE id = ?')
      .run(JSON.stringify(next), runId);
  }

  recordRunEvent(userId, runId, eventType, payload = {}, options = {}) {
    try {
      return recordRunEvent({
        runId,
        userId,
        agentId: options.agentId || null,
        eventType,
        requestId: options.requestId || null,
        stepId: options.stepId || null,
        payload,
      });
    } catch {
      return null;
    }
  }

  async persistDeliverableMemory(userId, runId, agentId, deliverableResult) {
    if (!this.memoryManager?.saveMemory || !deliverableResult?.summary) return;
    try {
      await this.memoryManager.saveMemory(
        userId,
        deliverableResult.summary,
        'tasks',
        deliverableResult.validation?.status === 'passed' ? 7 : 5,
        {
          agentId,
          sourceRef: {
            sourceType: 'deliverable_run',
            sourceId: runId,
            sourceLabel: deliverableResult.type || 'deliverable',
          },
          metadata: {
            deliverableType: deliverableResult.type,
            status: deliverableResult.status,
            artifactCount: Array.isArray(deliverableResult.artifacts)
              ? deliverableResult.artifacts.length
              : 0,
            artifacts: Array.isArray(deliverableResult.artifacts)
              ? deliverableResult.artifacts.slice(0, 6)
              : [],
          },
        },
      );
    } catch (error) {
      console.error('[Engine] Failed to persist deliverable memory:', error?.message || error);
    }
  }

  async publishInterimUpdate({
    userId,
    runId,
    agentId = null,
    triggerSource = 'web',
    conversationId = null,
    platform = null,
    chatId = null,
    content,
    kind,
    expectsReply = false,
    deferFollowUp = false,
  } = {}) {
    const runMeta = this.getRunMeta(runId);
    if (!runMeta || runMeta.aborted) {
      return { sent: false, skipped: true, reason: 'Run is no longer active.' };
    }

    const normalizedKind = normalizeInterimKind(kind);
    const normalizedContent = normalizeInterimText(
      content,
      triggerSource === 'messaging' ? platform : null
    );
    if (!normalizedContent || normalizedContent.toUpperCase() === '[NO RESPONSE]') {
      return { sent: false, skipped: true, reason: 'Interim content must be non-empty.' };
    }

    const signature = buildInterimSignature({
      content: normalizedContent,
      kind: normalizedKind,
      expectsReply,
      platform: triggerSource === 'messaging' ? platform : 'web',
    });
    if (runMeta.interimSignatures?.has(signature)) {
      return { sent: false, skipped: true, duplicate: true };
    }

    const metadata = buildInterimMetadata({
      kind: normalizedKind,
      expectsReply,
    });
    if (deferFollowUp === true) {
      metadata.defer_follow_up = true;
    }
    const createdAt = new Date().toISOString();

    if (triggerSource === 'messaging') {
      if (!platform || !chatId || !this.messagingManager) {
        return { sent: false, skipped: true, reason: 'Messaging context is not available.' };
      }
      await this.messagingManager.sendMessage(userId, platform, chatId, normalizedContent, {
        agentId,
        runId,
        persistConversation: true,
        metadata,
        deliveryKind: 'interim',
      });
    } else if (triggerSource === 'voice_live') {
      const voiceSessionId = runMeta.voiceSessionId || null;
      const manager = this.voiceRuntimeManager || this.app?.locals?.voiceRuntimeManager || null;
      if (!voiceSessionId || !manager || typeof manager.publishInterimUpdate !== 'function') {
        return { sent: false, skipped: true, reason: 'Voice session context is not available.' };
      }
      await manager.publishInterimUpdate({
        sessionId: voiceSessionId,
        content: normalizedContent,
        kind: normalizedKind,
        expectsReply,
        deferFollowUp,
      });
    } else {
      db.prepare(
        'INSERT INTO conversation_history (user_id, agent_id, agent_run_id, role, content, metadata) VALUES (?, ?, ?, ?, ?, ?)'
      ).run(userId, agentId, runId, 'assistant', normalizedContent, JSON.stringify(metadata));

      if (conversationId) {
        db.prepare('INSERT INTO conversation_messages (conversation_id, role, content) VALUES (?, ?, ?)')
          .run(conversationId, 'assistant', normalizedContent);
      }
    }

    if (!runMeta.interimSignatures) runMeta.interimSignatures = new Set();
    if (!Array.isArray(runMeta.interimMessages)) runMeta.interimMessages = [];
    runMeta.interimSignatures.add(signature);
    runMeta.interimMessages.push({
      content: normalizedContent,
      kind: normalizedKind,
      expectsReply: expectsReply === true,
      deferFollowUp: deferFollowUp === true,
      createdAt,
    });
    runMeta.lastInterimMessage = normalizedContent;

    this.emit(userId, 'run:assistant_interim', {
      runId,
      content: normalizedContent,
      kind: normalizedKind,
      expectsReply: expectsReply === true,
      deferFollowUp: deferFollowUp === true,
      triggerSource,
      platform: triggerSource === 'messaging' ? platform : 'web',
    });

    const terminalInterim = expectsReply === true;
    if (terminalInterim) {
      runMeta.terminalInterim = {
        kind: normalizedKind,
        content: normalizedContent,
        createdAt,
      };
    }
    this.persistRunMetadata(runId, {
      latestInterim: {
        kind: normalizedKind,
        expectsReply: expectsReply === true,
        deferFollowUp: deferFollowUp === true,
        content: normalizedContent,
        createdAt,
      },
      terminalInterim: terminalInterim
        ? { kind: normalizedKind, content: normalizedContent, createdAt }
        : null,
    });

    return {
      sent: true,
      kind: normalizedKind,
      expectsReply: expectsReply === true,
      deferFollowUp: deferFollowUp === true,
      content: normalizedContent,
      terminal: terminalInterim,
    };
  }

  async requestStructuredJson({
    provider,
    providerName,
    model,
    messages,
    prompt,
    maxTokens = 1400,
    normalize,
    fallback = {},
    reasoningEffort,
    telemetry = null,
    phase = 'structured',
  }) {
    const startedAt = Date.now();
    const response = await withProviderRetry(
      () => provider.chat(
        sanitizeConversationMessages([
          ...messages,
          { role: 'system', content: prompt },
        ]),
        [],
        {
          model,
          maxTokens,
          reasoningEffort: reasoningEffort || this.getReasoningEffort(providerName, {}),
        }
      ),
      { label: `Engine ${model} (structured)` }
    );
    if (telemetry?.runId && telemetry?.userId) {
      recordModelUsage({
        runId: telemetry.runId,
        stepId: telemetry.stepId || null,
        userId: telemetry.userId,
        agentId: telemetry.agentId || null,
        provider: providerName,
        model,
        phase,
        usage: response.usage,
        latencyMs: Date.now() - startedAt,
      });
    }

    const parsed = parseJsonObject(response.content || '');
    const normalizedUsage = normalizeUsage(response.usage);
    return {
      value: normalize(parsed || {}, fallback),
      raw: response.content || '',
      usage: normalizedUsage?.totalTokens || 0,
    };
  }

  async requestModelResponse({
    provider,
    providerName,
    model,
    messages,
    tools,
    options,
    runId,
    iteration,
  }) {
    const startedAt = Date.now();
    const requestMessages = sanitizeConversationMessages(messages);
    const callOptions = {
      model,
      reasoningEffort: this.getReasoningEffort(providerName, options),
    };

    const attemptModelCall = async () => {
      let response = null;
      let streamContent = '';

      if (options.stream !== false) {
        let emittedContent = false;
        const stream = provider.stream(requestMessages, tools, callOptions);
        try {
          for await (const chunk of stream) {
            if (chunk.type === 'content') {
              emittedContent = true;
              streamContent += chunk.content;
              this.emit(options.userId, 'run:stream', {
                runId,
                content: sanitizeModelOutput(streamContent, { model }),
                iteration,
              });
            }
            if (chunk.type === 'done') {
              response = chunk;
            }
            if (chunk.type === 'tool_calls') {
              response = {
                content: chunk.content || streamContent,
                toolCalls: chunk.toolCalls,
                providerContentBlocks: chunk.providerContentBlocks || null,
                finishReason: 'tool_calls',
                usage: chunk.usage || null,
              };
            }
          }
        } catch (err) {
          // Once tokens have streamed to the client a retry would duplicate
          // output, so only the pre-stream window is safe to replay.
          if (emittedContent) err.__providerRetryUnsafe = true;
          throw err;
        }
      } else {
        response = await provider.chat(requestMessages, tools, callOptions);
      }

      return { response, streamContent };
    };

    const { response, streamContent } = await withProviderRetry(attemptModelCall, {
      ...(options.retry || {}),
      label: `Engine ${model}`,
      isRetryable: (err) => !err?.__providerRetryUnsafe && isTransientError(err),
      onRetry: ({ attempt, delayMs }) => {
        this.emit(options.userId, 'run:interim', {
          runId,
          message: `Model service busy; retrying (attempt ${attempt}) in ${Math.max(1, Math.round(delayMs / 1000))}s.`,
          phase: 'recovering',
        });
      },
    });

    const resolvedResponse = response || {
      content: streamContent,
      toolCalls: [],
      finishReason: 'stop',
      usage: null,
    };
    const hasContent = Boolean(String(resolvedResponse.content || streamContent || '').trim());
    const hasToolCalls = Array.isArray(resolvedResponse.toolCalls) && resolvedResponse.toolCalls.length > 0;
    if (!hasContent && !hasToolCalls) {
      const error = new Error(`Model ${model} returned an empty response.`);
      error.code = 'MODEL_EMPTY_RESPONSE';
      throw error;
    }
    if (options.runId && options.userId) {
      recordModelUsage({
        runId: options.runId,
        stepId: options.stepId || null,
        userId: options.userId,
        agentId: options.agentId || null,
        provider: providerName,
        model,
        phase: options.phase || 'model_turn',
        usage: resolvedResponse.usage,
        latencyMs: Date.now() - startedAt,
        metadata: { iteration },
      });
    }

    return {
      response: resolvedResponse,
      responseModel: model,
      streamContent,
    };
  }

  async analyzeTask({
    provider,
    providerName,
    model,
    messages,
    tools,
    capabilityHealth,
    forceMode,
    userMessage,
    options,
  }) {
    const summary = summarizeCapabilityHealth(capabilityHealth);
    const response = await this.requestStructuredJson({
      provider,
      providerName,
      model,
      messages,
      prompt: buildAnalysisPrompt({
        capabilityHealth: summary,
        tools,
        forceMode,
      }),
      maxTokens: 1100,
      normalize: normalizeTaskAnalysis,
      fallback: buildAnalyzeTaskFallback(forceMode, userMessage),
      reasoningEffort: this.getReasoningEffort(providerName, options),
      telemetry: options,
      phase: 'task_analysis',
    });

    return {
      analysis: response.value,
      raw: response.raw,
      usage: response.usage,
      capabilitySummary: summary,
    };
  }

  async createExecutionPlan({
    provider,
    providerName,
    model,
    messages,
    analysis,
    capabilitySummary,
    options,
  }) {
    const response = await this.requestStructuredJson({
      provider,
      providerName,
      model,
      messages,
      prompt: buildPlanPrompt(analysis, capabilitySummary),
      maxTokens: 1400,
      normalize: normalizeExecutionPlan,
      fallback: {
        success_criteria: analysis.success_criteria,
      },
      reasoningEffort: this.getReasoningEffort(providerName, options),
      telemetry: options,
      phase: 'execution_plan',
    });

    return {
      plan: response.value,
      raw: response.raw,
      usage: response.usage,
    };
  }

  async decideLoopState({
    provider,
    providerName,
    model,
    messages,
    tools,
    analysis,
    plan,
    toolExecutions,
    lastReply,
    triggerSource,
    messagingSent,
    iteration,
    maxIterations,
    options,
    fallbackStatus,
  }) {
    const successCriteria = Array.isArray(plan?.success_criteria)
      ? plan.success_criteria
        .map((item) => String(item || '').trim())
        .filter(Boolean)
        .slice(0, 6)
      : [];

    const response = await this.requestStructuredJson({
      provider,
      providerName,
      model,
      messages,
      prompt: [
        'Return JSON only.',
        'Decide whether this run should continue autonomously or stop now.',
        'Schema: {"status":"continue|complete|blocked","reason":"short concrete reason"}',
        'Rules:',
        '- Use "continue" whenever any safe next step remains in this same run.',
        '- Use "complete" only when the requested outcome is actually achieved or a truthful final user reply is already ready now.',
        '- Use "blocked" only when a specific external dependency outside this run is required.',
        '- If the latest draft asks the user for a missing required value, confirmation, or choice needed to proceed, use "blocked" so the run waits instead of repeating the same ask.',
        '- A progress update is not complete.',
        '- A single failed tool attempt is not blocked if another safe retry, verification step, or alternative path remains.',
        '- A tool-specific API error, timeout, rate limit, or missing result inside this run is usually "continue", not "blocked", if any other available tool could still make progress.',
        '- If completion_confidence_required is high and the latest draft depends on unverified assumptions, use "continue" so the run can gather evidence, inspect state, or narrow the reply.',
        triggerSource === 'messaging' && messagingSent
          ? '- A reply was already delivered to the user via send_message. Use "complete" unless there is concrete remaining work (e.g., a tool call you still need to make) before the task is truly done. Do not send follow-up elaborations or re-introductions.'
          : triggerSource === 'messaging'
            ? '- For messaging, do not stop on a partial status message. Continue unless the task is actually complete or externally blocked. If you already asked for missing user input, choose "blocked" and wait.'
            : '- Do not stop just because you wrote a status update. Continue unless the task is actually complete or externally blocked.',
        analysis?.goal ? `Goal: ${analysis.goal}` : '',
        `Autonomy contract: complexity=${analysis?.complexity || 'standard'}; autonomy_level=${analysis?.autonomy_level || 'normal'}; progress_update_policy=${analysis?.progress_update_policy || 'optional'}; parallel_work=${analysis?.parallel_work === true}; completion_confidence_required=${analysis?.completion_confidence_required || 'medium'}.`,
        successCriteria.length > 0 ? `Success criteria:\n${successCriteria.map((item, index) => `${index + 1}. ${item}`).join('\n')}` : '',
        `Current iteration: ${iteration} of ${maxIterations}.`,
        `Available tools in this run: ${summarizeAvailableTools(tools) || 'none'}`,
        `Recent tool evidence:\n${summarizeToolExecutions(toolExecutions, 8) || 'none'}`,
        `Latest draft reply:\n${normalizeOutgoingMessage(lastReply) || '(empty)'}`,
      ].filter(Boolean).join('\n'),
      maxTokens: 320,
      normalize: (raw) => {
        const allowed = new Set(['continue', 'complete', 'blocked']);
        const requestedStatus = String(raw.status || '').trim().toLowerCase();
        return {
          status: allowed.has(requestedStatus) ? requestedStatus : fallbackStatus,
          reason: String(raw.reason || '').trim().slice(0, 400),
        };
      },
      fallback: { status: fallbackStatus },
      reasoningEffort: this.getReasoningEffort(providerName, options),
      telemetry: options,
      phase: 'loop_decision',
    });

    return {
      decision: response.value,
      usage: response.usage,
    };
  }

  async verifyFinalResponse({
    provider,
    providerName,
    model,
    messages,
    analysis,
    tools,
    toolExecutions,
    finalReply,
    options,
  }) {
    const evidenceSources = [...new Set(
      toolExecutions
        .map((item) => item.evidenceSource)
        .filter(Boolean)
    )];
    const response = await this.requestStructuredJson({
      provider,
      providerName,
      model,
      messages,
      prompt: buildVerifierPrompt({
        analysis,
        tools,
        toolExecutionSummary: summarizeToolExecutions(toolExecutions),
        evidenceSources,
        finalReply,
      }),
      maxTokens: 1200,
      normalize: (raw) => normalizeVerificationResult(raw, finalReply),
      fallback: {
        status: analysis.freshness_risk === 'none' ? 'verified' : 'insufficient_evidence',
        final_reply: finalReply,
      },
      reasoningEffort: this.getReasoningEffort(providerName, options),
      telemetry: options,
      phase: 'verification',
    });

    return {
      verification: response.value,
      raw: response.raw,
      usage: response.usage,
      evidenceSources,
    };
  }

  async refreshConversationState({
    conversationId,
    provider,
    providerName,
    model,
    finalReply,
    analysis,
    verification,
    historyWindow,
    options,
  }) {
    if (!conversationId) return null;
    const { MemoryManager } = require('../memory/manager');
    const memoryManager = this.memoryManager || new MemoryManager();
    const context = getConversationContext(conversationId, Math.max(historyWindow, 8));
    const existingState = memoryManager.getConversationState(conversationId);
    const promptMessages = [
      {
        role: 'system',
        content: 'Return JSON only. Distill the current thread working state. Keep it concise and concrete. Track summary, open_commitments, unresolved_questions, referenced_entities, and last_verified_facts. Do not invent facts.'
      },
      {
        role: 'user',
        content: [
          existingState?.summary ? `Existing state:\n${JSON.stringify(existingState, null, 2)}` : 'Existing state: none',
          context.summary ? `Conversation summary:\n${context.summary}` : 'Conversation summary: none',
          `Recent thread messages:\n${JSON.stringify(context.recentMessages.slice(-8), null, 2)}`,
          `Latest final reply:\n${finalReply || '(empty)'}`,
          verification?.status ? `Verification status: ${verification.status}` : '',
          verification?.final_reply && verification.final_reply !== finalReply ? `Verified reply:\n${verification.final_reply}` : '',
          analysis?.goal ? `Thread goal: ${analysis.goal}` : '',
        ].filter(Boolean).join('\n\n')
      }
    ];

    const response = await provider.chat(promptMessages, [], {
      model,
      maxTokens: 800,
      reasoningEffort: this.getReasoningEffort(providerName, options),
    });
    const parsed = parseJsonObject(response.content || '') || {};
    const nextState = {
      summary: String(parsed.summary || existingState?.summary || '').trim(),
      open_commitments: Array.isArray(parsed.open_commitments) ? parsed.open_commitments.slice(0, 8).map((item) => String(item || '').trim()).filter(Boolean) : [],
      unresolved_questions: Array.isArray(parsed.unresolved_questions) ? parsed.unresolved_questions.slice(0, 8).map((item) => String(item || '').trim()).filter(Boolean) : [],
      referenced_entities: Array.isArray(parsed.referenced_entities) ? parsed.referenced_entities.slice(0, 12).map((item) => String(item || '').trim()).filter(Boolean) : [],
      last_verified_facts: Array.isArray(parsed.last_verified_facts) ? parsed.last_verified_facts.slice(0, 10).map((item) => String(item || '').trim()).filter(Boolean) : [],
    };

    if (verification?.status === 'verified' && String(finalReply || '').trim()) {
      nextState.last_verified_facts = [...new Set([
        ...nextState.last_verified_facts,
        clampRunContext(verification.final_reply || finalReply, 280),
      ])].slice(-10);
    }

    memoryManager.updateConversationState(conversationId, nextState);
    return nextState;
  }

  async recoverBlankMessagingReply({
    userId,
    runId,
    messages,
    provider,
    model,
    providerName,
    options,
    stepIndex,
    failedStepCount,
    toolExecutions = [],
    tools = []
  }) {
    const attempts = 3;
    let recoveredContent = '';
    let totalTokens = 0;

    for (let attempt = 1; attempt <= attempts; attempt++) {
      console.warn(
        `[Run ${shortenRunId(runId)}] blank_reply_recovery attempt=${attempt} model=${model}`
      );
      try {
        const response = await provider.chat(
          sanitizeConversationMessages([
            ...messages,
            {
              role: 'system',
              content: buildBlankMessagingReplyPrompt(attempt, options?.source || null)
            }
          ]),
          [],
          {
            model,
            reasoningEffort: this.getReasoningEffort(providerName, options)
          }
        );
        totalTokens += response.usage?.totalTokens || 0;
        recoveredContent = sanitizeModelOutput(response.content || '', { model });
        if (normalizeOutgoingMessage(recoveredContent)) {
          console.info(
            `[Run ${shortenRunId(runId)}] blank_reply_recovery succeeded attempt=${attempt}`
          );
          return { content: recoveredContent, tokens: totalTokens, recovered: true };
        }
      } catch (recoverErr) {
        console.warn(
          `[Run ${shortenRunId(runId)}] blank_reply_recovery attempt=${attempt} failed: ${summarizeForLog(recoverErr?.message || recoverErr, 180)}`
        );
      }
    }

    const error = new Error(
      buildDeterministicMessagingFallback({
        failedStepCount,
        stepIndex,
        toolExecutions,
      })
    );
    error.code = 'BLANK_MESSAGING_REPLY';
    error.recoveryTokens = totalTokens;
    throw error;
  }

  getAvailableTools(app, options = {}) {
    const { getAvailableTools } = require('./tools');
    return getAvailableTools(app, options);
  }

  async executeTool(toolName, args, context) {
    const { executeTool } = require('./tools');
    return executeTool(toolName, args, context, this);
  }

  isReadOnlyToolCall(toolCall) {
    const name = String(toolCall?.function?.name || '');
    const readOnly = new Set([
      'read_file',
      'list_directory',
      'search_files',
      'code_navigate',
      'query_structured_data',
      'memory_recall',
      'memory_read',
      'session_search',
      'web_search',
      'list_tasks',
      'list_skills',
      'list_subagents',
      'recordings_list',
      'recordings_get',
      'recordings_search',
      'read_health_data',
    ]);
    if (name === 'http_request') {
      try {
        const args = JSON.parse(toolCall.function.arguments || '{}');
        return String(args.method || 'GET').toUpperCase() === 'GET';
      } catch {
        return false;
      }
    }
    return readOnly.has(name);
  }

  async executeReadOnlyBatch(toolCalls, context = {}) {
    const {
      userId,
      runId,
      agentId,
      app,
      triggerType,
      triggerSource,
      conversationId,
      startingStepIndex,
      options = {},
    } = context;
    const prepared = [];
    let nextStepIndex = startingStepIndex;
    for (const toolCall of toolCalls) {
      nextStepIndex += 1;
      let toolArgs = {};
      try { toolArgs = JSON.parse(toolCall.function.arguments || '{}'); } catch {}
      const toolName = toolCall.function.name;
      const repetitionGuard = this.getRunMeta(runId)?.repetitionGuard;
      if (repetitionGuard?.shouldBlock(toolName, toolArgs)) {
        const result = {
          status: 'blocked',
          reason: 'The same read-only call already returned an unchanged result twice.',
        };
        prepared.push({
          toolCall,
          toolName,
          toolArgs,
          stepIndex: nextStepIndex,
          blocked: true,
          result,
          error: result.reason,
        });
        this.recordRunEvent(userId, runId, 'repetition_blocked', {
          toolName,
          toolArgs,
          parallel: true,
        }, { agentId });
        continue;
      }
      if (globalHooks.has('before_tool_call')) {
        const hookResult = await globalHooks.run('before_tool_call', {
          toolName,
          toolArgs,
          runId,
          userId,
          agentId,
          iteration: context.iteration,
        });
        if (hookResult.block) {
          prepared.push({
            toolCall,
            toolName,
            toolArgs,
            stepIndex: nextStepIndex,
            blocked: true,
            result: { status: 'skipped', reason: 'Blocked by policy.' },
          });
          continue;
        }
        if (hookResult.toolArgs) toolArgs = hookResult.toolArgs;
      }
      const stepId = uuidv4();
      db.prepare(
        `INSERT INTO agent_steps (
          id, run_id, step_index, type, description, status, tool_name, tool_input, started_at
        ) VALUES (?, ?, ?, ?, ?, 'running', ?, ?, datetime('now'))`
      ).run(
        stepId,
        runId,
        nextStepIndex,
        this.getStepType(toolName),
        `${toolName}: ${JSON.stringify(toolArgs).slice(0, 200)}`,
        toolName,
        JSON.stringify(toolArgs),
      );
      this.emit(userId, 'run:tool_start', {
        runId,
        stepId,
        stepIndex: nextStepIndex,
        toolName,
        toolArgs,
        type: this.getStepType(toolName),
      });
      this.recordRunEvent(userId, runId, 'tool_started', {
        stepIndex: nextStepIndex,
        toolName,
        toolArgs,
        type: this.getStepType(toolName),
        parallel: true,
      }, { agentId, stepId });
      prepared.push({ toolCall, toolName, toolArgs, stepId, stepIndex: nextStepIndex });
    }
    this.recordRunEvent(userId, runId, 'parallel_batch_started', {
      toolNames: prepared.map((item) => item.toolName),
      count: prepared.length,
    }, { agentId });
    const results = await Promise.all(prepared.map(async (item) => {
      if (item.blocked) return item;
      const startedAt = Date.now();
      try {
        const result = await this.executeTool(item.toolName, item.toolArgs, {
          userId,
          runId,
          agentId,
          app,
          triggerType,
          triggerSource,
          conversationId,
          source: options.source || null,
          chatId: options.chatId || null,
          taskId: options.taskId || null,
          widgetId: options.widgetId || null,
          deliveryState: options.deliveryState || null,
          allowMultipleProactiveMessages: options.allowMultipleProactiveMessages === true,
          allowExternalSideEffects: false,
        });
        const error = inferToolFailureMessage(item.toolName, result);
        const status = error ? 'failed' : 'completed';
        db.prepare(
          `UPDATE agent_steps
           SET status = ?, result = ?, error = ?, screenshot_path = ?, completed_at = datetime('now')
           WHERE id = ?`
        ).run(
          status,
          JSON.stringify(result).slice(0, 20000),
          error || null,
          result?.screenshotPath || null,
          item.stepId,
        );
        this.emit(userId, 'run:tool_end', {
          runId,
          stepId: item.stepId,
          toolName: item.toolName,
          result,
          error: error || undefined,
          status,
        });
        this.recordRunEvent(userId, runId, error ? 'tool_failed' : 'tool_completed', {
          toolName: item.toolName,
          status,
          durationMs: Date.now() - startedAt,
          resultPreview: summarizeForLog(result),
          parallel: true,
        }, { agentId, stepId: item.stepId });
        return { ...item, result, error };
      } catch (err) {
        db.prepare(
          `UPDATE agent_steps SET status = 'failed', error = ?, completed_at = datetime('now') WHERE id = ?`
        ).run(err.message, item.stepId);
        this.emit(userId, 'run:tool_end', {
          runId,
          stepId: item.stepId,
          toolName: item.toolName,
          error: err.message,
          status: 'failed',
        });
        this.recordRunEvent(userId, runId, 'tool_failed', {
          toolName: item.toolName,
          status: 'failed',
          error: err.message,
          durationMs: Date.now() - startedAt,
          parallel: true,
        }, { agentId, stepId: item.stepId });
        return { ...item, result: { error: err.message }, error: err.message };
      }
    }));
    this.recordRunEvent(userId, runId, 'parallel_batch_completed', {
      toolNames: results.map((item) => item.toolName),
      failedCount: results.filter((item) => item.error).length,
    }, { agentId });
    return { results, endingStepIndex: nextStepIndex };
  }

  async persistRunContext(userId, {
    triggerSource,
    runTitle,
    userMessage,
    lastContent,
    stepIndex,
    skipPersistence = false
  }) {
    if (skipPersistence) {
      return;
    }
    void userId;
    void triggerSource;
    void runTitle;
    void userMessage;
    void lastContent;
    void stepIndex;
    // Run receipts belong in agent_runs/session history, not long-term memory.
    // Long-term memory should only contain durable facts or explicitly saved context.
    return;
  }

  getRunMeta(runId) {
    return this.activeRuns.get(runId) || null;
  }

  initializeToolRuntime(runId, allTools, initialTools, options = {}) {
    const runMeta = this.getRunMeta(runId);
    if (!runMeta) return;
    runMeta.toolCatalog = Array.isArray(allTools) ? allTools : [];
    runMeta.activeTools = Array.isArray(initialTools) ? initialTools : [];
    runMeta.toolSelectionOptions = {
      widgetId: options.widgetId || null,
    };
  }

  getActiveTools(runId) {
    return this.getRunMeta(runId)?.activeTools || [];
  }

  activateToolsForRun(runId, names = []) {
    const runMeta = this.getRunMeta(runId);
    if (!runMeta) throw new Error('Run is not active.');
    const result = activateTools(
      runMeta.activeTools,
      runMeta.toolCatalog,
      names,
      runMeta.toolSelectionOptions,
    );
    runMeta.activeTools = result.tools;
    this.recordRunEvent(runMeta.userId, runId, 'tools_activated', {
      activated: result.activated,
      evicted: result.evicted,
      unknown: result.unknown,
      notActivated: result.notActivated,
      activeToolNames: result.tools.map((tool) => tool.name),
    }, { agentId: runMeta.agentId });
    return {
      success: result.unknown.length === 0 && result.notActivated.length === 0,
      activated: result.activated,
      evicted: result.evicted,
      unknown: result.unknown,
      not_activated: result.notActivated,
      active_tools: result.tools.map((tool) => tool.name),
    };
  }

  findActiveRunForUser(userId, predicate = null) {
    let candidate = null;
    for (const [runId, runMeta] of this.activeRuns.entries()) {
      if (runMeta.userId !== userId || runMeta.aborted) continue;
      if (typeof predicate === 'function' && !predicate(runMeta, runId)) continue;
      if (!candidate || (runMeta.startedAt || 0) >= (candidate.startedAt || 0)) {
        candidate = { runId, ...runMeta };
      }
    }
    return candidate;
  }

  findSteerableRunForUser(userId, triggerSource = 'web') {
    return this.findActiveRunForUser(
      userId,
      (runMeta) => runMeta.triggerSource === triggerSource && runMeta.triggerType === 'user'
    );
  }

  enqueueSteering(runId, content, metadata = {}) {
    const runMeta = this.getRunMeta(runId);
    const trimmed = typeof content === 'string' ? content.trim() : '';
    if (!runMeta || runMeta.aborted || !trimmed) return null;

    const item = {
      id: uuidv4(),
      content: trimmed,
      metadata,
      createdAt: new Date().toISOString()
    };

    runMeta.steeringQueue.push(item);
    this.emit(runMeta.userId, 'run:steer_queued', {
      runId,
      content: item.content,
      pendingCount: runMeta.steeringQueue.length
    });

    return {
      runId,
      pendingCount: runMeta.steeringQueue.length,
      item
    };
  }

  applyQueuedSteering(runId, messages, { userId, conversationId }) {
    const runMeta = this.getRunMeta(runId);
    if (!runMeta?.steeringQueue?.length) {
      return { messages, appliedCount: 0 };
    }

    const queued = runMeta.steeringQueue.splice(0, runMeta.steeringQueue.length);
    messages.push({
      role: 'system',
      content: [
        'The user sent follow-up messages while you were already working.',
        'Treat them as steering or next-up context for the same conversation.',
        'If a message materially changes the active task, incorporate it now.',
        'If it is unrelated or better handled after the current task, finish the current work first and then address it.'
      ].join(' ')
    });

    for (const entry of queued) {
      messages.push({ role: 'user', content: entry.content });
      if (conversationId) {
        db.prepare('INSERT INTO conversation_messages (conversation_id, role, content) VALUES (?, ?, ?)')
          .run(conversationId, 'user', entry.content);
      }
    }

    this.emit(userId, 'run:steer_applied', {
      runId,
      count: queued.length,
      pendingCount: runMeta.steeringQueue.length,
      latestContent: queued[queued.length - 1]?.content || ''
    });

    return { messages, appliedCount: queued.length };
  }

  isRunStopped(runId) {
    return this.getRunMeta(runId)?.aborted === true;
  }

  attachProcessToRun(runId, pid) {
    const runMeta = this.getRunMeta(runId);
    if (!runMeta || !pid) return;
    runMeta.toolPids.add(pid);
    if (runMeta.aborted) {
      if (this.runtimeManager && typeof this.runtimeManager.killCommand === 'function') {
        void this.runtimeManager.killCommand(runMeta.userId, pid, 'aborted');
      }
    }
  }

  detachProcessFromRun(runId, pid) {
    const runMeta = this.getRunMeta(runId);
    if (!runMeta || !pid) return;
    runMeta.toolPids.delete(pid);
  }

  // getIterationLimit() removed — use buildLoopPolicy() directly.
  // maxIterations is derived in runWithModel from loopPolicy.maxIterations.

  getReasoningEffort(providerName, options = {}) {
    if (providerName === 'google') return undefined;
    if (options.latencyProfile === 'voice') {
      return 'low';
    }
    return options.reasoningEffort || process.env.REASONING_EFFORT || 'low';
  }

  shouldFastCompleteVoiceReply({
    options = {},
    toolExecutions = [],
    failedStepCount = 0,
    messagingSent = false,
    lastReply = '',
  }) {
    return options.latencyProfile === 'voice'
      && toolExecutions.length === 0
      && failedStepCount === 0
      && !messagingSent
      && Boolean(String(lastReply || '').trim());
  }

  getMessagingRetryLimit(maxIterations) {
    // Cap at 3: more than 3 autonomous messaging retries indicates a structural
    // problem (model unavailable, bad config) that more retries won't solve.
    return Math.min(3, Math.max(1, maxIterations));
  }

  buildContextMessages(systemPrompt, summaryMessage, historyMessages, recallMsg) {
    const messages = [];
    if (systemPrompt && typeof systemPrompt === 'object') {
      if (systemPrompt.stable) {
        messages.push({
          role: 'system',
          content: systemPrompt.stable,
        });
      }
      if (systemPrompt.dynamic) {
        messages.push({ role: 'system', content: systemPrompt.dynamic });
      }
    } else {
      messages.push({ role: 'system', content: systemPrompt });
    }
    if (summaryMessage) messages.push(summaryMessage);
    if (Array.isArray(historyMessages)) messages.push(...historyMessages);
    if (recallMsg) messages.push({ role: 'system', content: recallMsg });
    return messages;
  }

  buildUserMessage(userMessage, options = {}) {
    if (!options.mediaAttachments || options.mediaAttachments.length === 0) {
      return { role: 'user', content: userMessage };
    }

    const contentArr = [{ type: 'text', text: userMessage }];
    for (const att of options.mediaAttachments) {
      if ((att.type === 'image' || att.type === 'video') && att.path) {
        try {
          if (fs.existsSync(att.path)) {
            const b64 = fs.readFileSync(att.path).toString('base64');
            const mime = att.path.endsWith('.png') ? 'image/png' : att.path.endsWith('.gif') ? 'image/gif' : 'image/jpeg';
            contentArr.push({ type: 'image_url', image_url: { url: `data:${mime};base64,${b64}` } });
          }
        } catch (err) {
          console.warn(`[AgentEngine] Failed to read attachment at ${att.path}:`, err?.message);
        }
      }
    }

    return { role: 'user', content: contentArr.length > 1 ? contentArr : userMessage };
  }

  estimatePromptMetrics(messages, tools) {
    const metrics = {
      systemPromptTokens: 0,
      toolSchemaTokens: estimateTokenValue(tools),
      historyTokens: 0,
      recalledMemoryTokens: 0,
      toolReplayTokens: 0,
      totalEstimatedTokens: 0
    };

    messages.forEach((msg, index) => {
      const contentTokens = estimateTokenValue(msg.content);
      const callTokens = estimateTokenValue(msg.tool_calls);
      const total = contentTokens + callTokens;

      if (msg.role === 'tool') {
        metrics.toolReplayTokens += total;
      } else if (msg.role === 'system' && index === 0) {
        metrics.systemPromptTokens += total;
      } else if (msg.role === 'system' && /^\[Recalled memory/.test(msg.content || '')) {
        metrics.recalledMemoryTokens += total;
      } else {
        metrics.historyTokens += total;
      }
    });

    metrics.totalEstimatedTokens = metrics.systemPromptTokens
      + metrics.toolSchemaTokens
      + metrics.historyTokens
      + metrics.recalledMemoryTokens
      + metrics.toolReplayTokens;

    return metrics;
  }

  mergePromptMetrics(summary, metrics, iteration, toolCount) {
    return {
      iterationsObserved: Math.max(summary.iterationsObserved || 0, iteration),
      toolCount,
      maxEstimatedTokens: Math.max(summary.maxEstimatedTokens || 0, metrics.totalEstimatedTokens),
      maxSystemPromptTokens: Math.max(summary.maxSystemPromptTokens || 0, metrics.systemPromptTokens),
      maxToolSchemaTokens: Math.max(summary.maxToolSchemaTokens || 0, metrics.toolSchemaTokens),
      maxHistoryTokens: Math.max(summary.maxHistoryTokens || 0, metrics.historyTokens),
      maxRecalledMemoryTokens: Math.max(summary.maxRecalledMemoryTokens || 0, metrics.recalledMemoryTokens),
      maxToolReplayTokens: Math.max(summary.maxToolReplayTokens || 0, metrics.toolReplayTokens),
      lastEstimate: metrics
    };
  }

  async persistPromptMetrics(runId, metrics) {
    db.prepare('UPDATE agent_runs SET prompt_metrics = ? WHERE id = ?')
      .run(JSON.stringify(metrics), runId);
  }

  async run(userId, userMessage, options = {}) {
    return this.runWithModel(
      userId,
      userMessage,
      options,
      typeof options.model === 'string' && options.model.trim()
        ? options.model.trim()
        : null,
    );
  }

  async runWithModel(userId, userMessage, options = {}, _modelOverride = null) {
    const triggerType = options.triggerType || 'user';
    const { resolveAgentId } = require('../agents/manager');
    const agentId = resolveAgentId(userId, options.agentId || options.agent_id || null);
    ensureDefaultAiSettings(userId, agentId);
    const aiSettings = getAiSettings(userId, agentId);

    const userLimits = db.prepare('SELECT rate_limit_4h, rate_limit_weekly FROM users WHERE id = ?').get(userId);
    const globalLimit4h = process.env.NEOAGENT_RATE_LIMIT_4H ? parseInt(process.env.NEOAGENT_RATE_LIMIT_4H, 10) : null;
    const globalLimitWeekly = process.env.NEOAGENT_RATE_LIMIT_WEEKLY ? parseInt(process.env.NEOAGENT_RATE_LIMIT_WEEKLY, 10) : null;
    const effective4h = userLimits?.rate_limit_4h ?? globalLimit4h;
    const effectiveWeekly = userLimits?.rate_limit_weekly ?? globalLimitWeekly;
    if (effective4h) {
      const h4Tokens = db.prepare("SELECT COALESCE(SUM(total_tokens), 0) as t FROM agent_runs WHERE user_id = ? AND created_at > datetime('now', '-4 hours')").get(userId).t;
      if (h4Tokens >= effective4h) {
        throw new Error(`Rate limit exceeded: You have used ${h4Tokens} tokens in the last 4 hours (limit: ${effective4h}).`);
      }
    }
    if (effectiveWeekly) {
      const weeklyTokens = db.prepare("SELECT COALESCE(SUM(total_tokens), 0) as t FROM agent_runs WHERE user_id = ? AND created_at > datetime('now', '-7 days')").get(userId).t;
      if (weeklyTokens >= effectiveWeekly) {
        throw new Error(`Rate limit exceeded: You have used ${weeklyTokens} tokens in the last 7 days (limit: ${effectiveWeekly}).`);
      }
    }

    const runId = options.runId || uuidv4();
    const conversationId = options.conversationId;
    const app = options.app || this.app;
    const triggerSource = options.triggerSource || 'web';
    const historyWindow = Math.max(
      1,
      Number(options.historyWindow || aiSettings.chat_history_window) || aiSettings.chat_history_window,
    );
    // loopPolicy is built after task analysis so analysisMode can be passed in;
    // we build a provisional policy now (with default mode) and rebuild after
    // analysis when the mode is known. See the post-analysis policy rebuild below.
    let loopPolicy = buildLoopPolicy(aiSettings, triggerType, 'execute', options);
    let maxIterations = loopPolicy.maxIterations;
    const providerStatusConfig = {
      agentId,
      onStatus: (status) => {
        if (!status?.message) return;
        this.emit(userId, 'run:interim', {
          runId,
          message: status.message,
          phase: status.phase
        });
      }
    };
    const selectedProvider = await getProviderForUser(
      userId,
      userMessage,
      triggerType === 'subagent',
      _modelOverride,
      providerStatusConfig
    );
    let provider = selectedProvider.provider;
    let model = selectedProvider.model;
    let providerName = selectedProvider.providerName;
    const switchToFallbackModel = async (failedModel, error, phase) => {
      const fallbackModelId = await getFailureFallbackModelId(userId, agentId, failedModel, aiSettings.fallback_model_id, error);
      if (!fallbackModelId || fallbackModelId === failedModel) return false;
      console.log(`[Engine] ${phase} failed on ${failedModel}; attempting fallback to: ${fallbackModelId}`);
      this.emit(userId, 'run:interim', {
        runId,
        message: `Model service failed on ${failedModel}; retrying with ${fallbackModelId}.`,
        phase: 'model_fallback'
      });
      const fallback = await getProviderForUser(
        userId,
        userMessage,
        triggerType === 'subagent',
        fallbackModelId,
        providerStatusConfig
      );
      provider = fallback.provider;
      model = fallback.model;
      providerName = fallback.providerName;
      return true;
    };
    const runWithModelFallback = async (phase, fn) => {
      try {
        return await fn();
      } catch (err) {
        const failedModel = model;
        const switched = await switchToFallbackModel(failedModel, err, phase);
        if (!switched) throw err;
        return await fn();
      }
    };

    const runTitle = generateTitle(userMessage);
    const initialRunMetadata = buildInitialRunMetadata(options);
    db.prepare(`INSERT OR REPLACE INTO agent_runs(
      id, user_id, agent_id, title, status, trigger_type, trigger_source, model, metadata_json
    ) VALUES(?, ?, ?, ?, 'running', ?, ?, ?, ?)`).run(
      runId,
      userId,
      agentId,
      runTitle,
      triggerType,
      triggerSource,
      model,
      Object.keys(initialRunMetadata).length ? JSON.stringify(initialRunMetadata) : null,
    );

    const retryMessagingState = options.messagingRetryState || {};
    const carriedVisibleMessage = String(retryMessagingState.lastVisibleMessage || '').trim();
    const carriedExplicitMessageSent = retryMessagingState.explicitMessageSent === true;

    this.activeRuns.set(runId, {
      userId,
      agentId,
      status: 'running',
      aborted: false,
      messagingSent: false,
      noResponse: false,
      explicitMessageSent: carriedExplicitMessageSent,
      lastSentMessage: carriedExplicitMessageSent ? carriedVisibleMessage : '',
      sentMessages: [],
      widgetSnapshotSaved: false,
      triggerType,
      triggerSource,
      startedAt: Date.now(),
      lastToolName: null,
      lastToolTarget: null,
      lastInterimMessage: carriedExplicitMessageSent ? '' : carriedVisibleMessage,
      interimMessages: [],
      interimSignatures: new Set(),
      terminalInterim: null,
      voiceSessionId: options.voiceSessionId || null,
      steeringQueue: [],
      toolPids: new Set(),
      repetitionGuard: new ToolRepetitionGuard(),
    });
    this.emit(userId, 'run:start', { runId, agentId, title: runTitle, model, triggerType, triggerSource });
    this.recordRunEvent(userId, runId, 'run_started', {
      title: runTitle,
      model,
      triggerType,
      triggerSource,
    }, { agentId });
    console.info(
      `[Run ${shortenRunId(runId)}] started trigger=${triggerSource} type=${triggerType} model=${model} title=${summarizeForLog(runTitle, 120)}`
    );

    const systemPrompt = await this.buildSystemPrompt(userId, {
      ...(options.context || {}),
      userMessage,
      agentId,
      triggerSource,
    });
    // Pass short descriptions so the model always knows every available tool.
    // compactToolDefinition caps tool desc at 120 chars, param desc at 70 chars.
    const builtInTools = this.getAvailableTools(app, {
      includeDescriptions: true,
      userId,
      agentId,
      triggerType,
      triggerSource,
      widgetId: options.widgetId || null,
    });
    const mcpManager = app?.locals?.mcpManager || app?.locals?.mcpClient || this.mcpManager;
    const integrationManager = app?.locals?.integrationManager || null;
    const mcpTools = mcpManager ? mcpManager.getAllTools(userId, { agentId }) : [];
    const allTools = selectToolsForTask(userMessage, builtInTools, mcpTools, options);
    let tools = allTools;
    const toolNames = allTools.map((tool) => tool.name).filter(Boolean);
    const coreToolStatus = {
      send_message: toolNames.includes('send_message'),
      create_task: toolNames.includes('create_task'),
      list_tasks: toolNames.includes('list_tasks'),
      update_task: toolNames.includes('update_task'),
      delete_task: toolNames.includes('delete_task'),
    };
    this.recordRunEvent(userId, runId, 'tool_inventory', {
      total: toolNames.length,
      builtInTotal: builtInTools.length,
      mcpTotal: mcpTools.length,
      core: coreToolStatus,
    }, { agentId });
    console.info(
      `[Run ${shortenRunId(runId)}] tools total=${toolNames.length} builtIns=${builtInTools.length} mcp=${mcpTools.length} core=${JSON.stringify(coreToolStatus)}`
    );
    const capabilityHealth = await getCapabilityHealth({ userId, agentId, app, engine: this });
    const capabilitySummary = summarizeCapabilityHealth(capabilityHealth);
    const integrationSummary = integrationManager?.summarizeConnectedProviders?.(userId, agentId) || '';

    const { MemoryManager } = require('../memory/manager');
    const memoryManager = this.memoryManager || new MemoryManager();
    const recallQuery = options.context?.rawUserMessage || userMessage;
    const recallMsg = options.skipGlobalRecall === true
      ? null
      : await memoryManager.buildRecallMessage(userId, recallQuery, { agentId });

    let summaryMessage = null;
    let historyMessages = [];

    if (conversationId && options.skipConversationHistory !== true) {
      const conversationContext = getConversationContext(conversationId, historyWindow);
      summaryMessage = buildSummaryCarrier(conversationContext.summary || options.priorSummary || '');
      historyMessages = conversationContext.recentMessages.length > 0
        ? conversationContext.recentMessages
        : (options.priorMessages || []).slice(-historyWindow).filter((pm) => pm.role && pm.content);
    } else {
      summaryMessage = buildSummaryCarrier(options.priorSummary || '');
      historyMessages = (options.priorMessages || []).slice(-historyWindow).filter((pm) => pm.role && pm.content);
    }

    let messages = this.buildContextMessages(systemPrompt, summaryMessage, historyMessages, recallMsg);
    if (capabilitySummary) {
      messages.push({ role: 'system', content: `[Capability health]\n${capabilitySummary}` });
    }
    if (integrationSummary) {
      messages.push({ role: 'system', content: `[Official integrations]\n${integrationSummary}` });
    }
    const threadStateMessage = conversationId ? memoryManager.buildConversationStateMessage(conversationId) : null;
    if (threadStateMessage) {
      messages.push({ role: 'system', content: threadStateMessage });
    }
    this.recordRunEvent(userId, runId, 'memory_injected', {
      hasRecallContext: Boolean(recallMsg),
      hasThreadState: Boolean(threadStateMessage),
      recallPreview: recallMsg ? String(recallMsg).slice(0, 240) : '',
    }, { agentId });
    messages.push(this.buildUserMessage(userMessage, options));
    messages = sanitizeConversationMessages(messages);

    if (conversationId) {
      db.prepare('INSERT INTO conversation_messages (conversation_id, role, content) VALUES (?, ?, ?)')
        .run(conversationId, 'user', userMessage);
    }

    let iteration = 0;
    let totalTokens = 0;
    let lastContent = '';
    let stepIndex = 0;
    let failedStepCount = 0;
    let modelFailureRecoveries = 0;
    let promptMetrics = {};
    let toolExecutions = [];
    let compactionMetrics = [];
    let analysis = null;
    let plan = null;
    let verification = null;
    let deliverableWorkflow = null;
    let deliverablePlan = null;
    let deliverableArtifacts = [];
    let deliverableValidation = null;
    let directAnswerEligible = false;
    let analysisUsage = 0;

    try {
      if (options.skipTaskAnalysis === true) {
        analysis = buildSkipTaskAnalysisResult(options.forceMode);
      } else {
        const analysisResult = await runWithModelFallback('task analysis', () => this.analyzeTask({
          provider,
          providerName,
          model,
          messages,
          tools,
          capabilityHealth,
          forceMode: options.forceMode || null,
          userMessage,
          options: { ...options, triggerSource, runId, userId, agentId },
        }));
        analysisUsage = analysisResult.usage || 0;
        totalTokens += analysisUsage;
        analysis = applyForcedAnalysisMode({ ...analysisResult.analysis }, options.forceMode);
        if (!analysis.goal && userMessage) {
          analysis.goal = String(userMessage).trim().slice(0, 300);
        }
        analysis.mode = promoteAnalysisMode(analysis.mode, {
          verificationNeed: analysis.verification_need,
          freshnessRisk: analysis.freshness_risk,
          draftReply: analysis.draft_reply,
          planningDepth: analysis.planning_depth,
        });

        stepIndex += 1;
        const analysisStepId = uuidv4();
        db.prepare(`INSERT INTO agent_steps
          (id, run_id, step_index, type, description, status, result, started_at, completed_at)
          VALUES (?, ?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'))`)
          .run(
            analysisStepId,
            runId,
            stepIndex,
            'analysis',
            'Task analysis contract',
            'completed',
            JSON.stringify(analysis).slice(0, 20000)
          );
        this.persistRunMetadata(runId, {
          taskAnalysis: analysis,
          capabilityHealth,
        });
        this.emit(userId, 'run:analysis', {
          runId,
          ...analysis,
          capabilitySummary,
        });

      }

      tools = selectInitialTools(allTools, analysis.suggested_tools, {
        widgetId: options.widgetId || null,
      });
      this.initializeToolRuntime(runId, allTools, tools, options);
      messages.push({
        role: 'system',
        content: [
          '[Available tool catalog]',
          buildToolCatalog(allTools),
          '',
          `Active tools: ${tools.map((tool) => tool.name).join(', ')}`,
          'Use activate_tools with exact catalog names when another schema is required.',
        ].join('\n'),
      });
      this.recordRunEvent(userId, runId, 'tool_selection_applied', {
        activeToolNames: tools.map((tool) => tool.name),
        catalogSize: allTools.length,
      }, { agentId });

      const activeDefaultModelSetting = triggerType === 'subagent'
        ? aiSettings.default_subagent_model
        : aiSettings.default_chat_model;
      if (!_modelOverride && activeDefaultModelSetting === 'auto' && aiSettings.smarter_model_selector !== false) {
        const requestedPurpose = analysis?.mode === 'plan_execute' || analysis?.complexity === 'complex' || analysis?.autonomy_level === 'high'
          ? 'planning'
          : triggerType === 'subagent'
            ? 'fast'
            : '';
        if (requestedPurpose) {
          const selectedAfterAnalysis = await getProviderForUser(
            userId,
            userMessage,
            triggerType === 'subagent',
            null,
            {
              ...providerStatusConfig,
              selectionHint: {
                purpose: requestedPurpose,
                complexity: analysis?.complexity,
                autonomyLevel: analysis?.autonomy_level,
                requiredConfidence: analysis?.completion_confidence_required,
                costMode: aiSettings.cost_mode,
              },
            }
          );
          if (selectedAfterAnalysis.model !== model) {
            provider = selectedAfterAnalysis.provider;
            model = selectedAfterAnalysis.model;
            providerName = selectedAfterAnalysis.providerName;
            db.prepare('UPDATE agent_runs SET model = ?, updated_at = datetime(\'now\') WHERE id = ?')
              .run(model, runId);
            this.emit(userId, 'run:interim', {
              runId,
              message: `Switched to ${model} for this run after task analysis.`,
              phase: 'model_selection'
            });
          }
        }
      }

      // Rebuild loop policy with the resolved analysis mode. Runs in both the
      // normal path and the skipTaskAnalysis path so that forceMode='plan_execute'
      // (or any mode set by buildSkipTaskAnalysisResult) raises the iteration
      // ceiling correctly.
      loopPolicy = buildLoopPolicy(aiSettings, triggerType, analysis.mode || 'execute', {
        ...options,
        autonomyPolicy: buildAutonomyPolicyFromAnalysis(analysis),
      });
      maxIterations = loopPolicy.maxIterations;

      if (options.skipDeliverableWorkflow !== true) {
        const deliverableSelectionResult = await selectDeliverableWorkflow({
          engine: this,
          provider,
          providerName,
          model,
          messages,
          tools,
          options: { ...options, runId, userId, agentId },
        });
        totalTokens += deliverableSelectionResult.usage || 0;
        const selectedWorkflow = getDeliverableWorkflow(deliverableSelectionResult.selection.type);
        if (selectedWorkflow?.canHandle(deliverableSelectionResult.selection)) {
          deliverableWorkflow = {
            workflow: selectedWorkflow,
            selection: deliverableSelectionResult.selection,
            request: selectedWorkflow.normalizeRequest({
              ...deliverableSelectionResult.selection,
              userMessage,
            }),
          };
          deliverablePlan = selectedWorkflow.buildExecutionPlan(deliverableWorkflow.request, {
            analysis,
            tools,
            options,
          });
          await selectedWorkflow.run(deliverablePlan, {
            engine: this,
            userId,
            agentId,
            runId,
            agentId,
            app,
          });
          this.persistRunMetadata(runId, {
            deliverableWorkflow: {
              ...deliverableWorkflow.selection,
              plan: deliverablePlan,
            },
          });
          this.recordRunEvent(userId, runId, 'deliverable_workflow_selected', {
            type: deliverableWorkflow.selection.type,
            confidence: deliverableWorkflow.selection.confidence,
            goal: deliverableWorkflow.selection.goal,
            requestedOutputs: deliverableWorkflow.selection.requestedOutputs,
          }, { agentId });
        }
      }

      if (analysis.mode === 'plan_execute') {
        const planResult = await runWithModelFallback('execution planning', () => this.createExecutionPlan({
          provider,
          providerName,
          model,
          messages,
          analysis,
          capabilitySummary,
          options: { ...options, runId, userId, agentId },
        }));
        totalTokens += planResult.usage || 0;
        plan = planResult.plan;
        stepIndex += 1;
        const planStepId = uuidv4();
        db.prepare(`INSERT INTO agent_steps
          (id, run_id, step_index, type, description, status, result, started_at, completed_at)
          VALUES (?, ?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'))`)
          .run(
            planStepId,
            runId,
            stepIndex,
            'planning',
            'Execution plan',
            'completed',
            JSON.stringify(plan).slice(0, 20000)
          );
        this.persistRunMetadata(runId, { executionPlan: plan });
        this.emit(userId, 'run:plan', {
          runId,
          steps: plan.steps,
          successCriteria: plan.success_criteria,
          verificationFocus: plan.verification_focus,
        });
      }

      messages.push({
        role: 'system',
        content: buildExecutionGuidance({
          analysis,
          plan,
          capabilityHealth: capabilitySummary,
        }),
      });
      if (deliverablePlan) {
        messages.push({
          role: 'system',
          content: buildDeliverableWorkflowGuidance(deliverablePlan),
        });
        this.recordRunEvent(userId, runId, 'deliverable_execution_started', {
          type: deliverableWorkflow?.selection?.type,
          preferredTools: deliverablePlan.preferredTools || [],
          expectedOutputs: deliverablePlan.expectedOutputs || [],
        }, { agentId });
      }
      messages = sanitizeConversationMessages(messages);

      directAnswerEligible = isDirectAnswerEligibleAnalysis(analysis)
        && Boolean(normalizeOutgoingMessage(analysis.draft_reply));

      if (directAnswerEligible) {
        iteration = 1;
        lastContent = analysis.draft_reply.trim();
        messages.push({ role: 'assistant', content: lastContent });
        if (conversationId) {
          db.prepare('INSERT INTO conversation_messages (conversation_id, role, content, tokens) VALUES (?, ?, ?, ?)')
            .run(conversationId, 'assistant', lastContent, analysisUsage);
        }
      }

      // BUG FIX: consecutiveToolFailures was previously declared INSIDE the
      // while loop (resetting each iteration). It is now tracked across the
      // full run so the failure guard fires correctly after 5 consecutive failures
      // regardless of which iteration they fall in.
      let consecutiveToolFailures = 0;

      while (!directAnswerEligible && iteration < maxIterations) {
        if (this.isRunStopped(runId)) break;
        iteration++;

        const steeringAtLoopStart = this.applyQueuedSteering(runId, messages, {
          userId,
          conversationId
        });
        messages = steeringAtLoopStart.messages;
        messages = sanitizeConversationMessages(messages);

        let metrics = this.estimatePromptMetrics(messages, tools);
        const contextWindow = provider.getContextWindow(model);
        if (metrics.totalEstimatedTokens > contextWindow * loopPolicy.compactionThreshold) {
          messages = await compact(messages, provider, model, contextWindow);
          messages = sanitizeConversationMessages(messages);
          this.emit(userId, 'run:compaction', { runId, iteration });
          metrics = this.estimatePromptMetrics(messages, tools);
        }

        promptMetrics = this.mergePromptMetrics(promptMetrics, metrics, iteration, tools.length);
        this.persistPromptMetrics(runId, promptMetrics).catch(() => { });
        this.emit(userId, 'run:thinking', { runId, iteration });
        this.recordRunEvent(userId, runId, 'model_turn_started', {
          iteration,
          toolCount: tools.length,
        }, { agentId });

        let response;
        let responseModel = model;
        let streamContent = '';

        const tryModelCall = async (retryForFallback = true) => {
          try {
            const modelCall = await this.requestModelResponse({
              provider,
              providerName,
              model,
              messages,
              tools,
              options: { ...options, userId, agentId, runId, phase: 'model_turn' },
              runId,
              iteration,
            });
            response = modelCall.response;
            responseModel = modelCall.responseModel;
            streamContent = modelCall.streamContent;
          } catch (err) {
            console.error(`[Engine] Model call failed (${model}):`, err.message);
            const fallbackModelId = retryForFallback
              ? await getFailureFallbackModelId(userId, agentId, model, aiSettings.fallback_model_id, err)
              : null;
            if (fallbackModelId) {
              const failedModel = model;
              console.log(`[Engine] Attempting fallback to: ${fallbackModelId}`);
              const fallback = await getProviderForUser(
                userId,
                userMessage,
                triggerType === 'subagent',
                fallbackModelId,
                providerStatusConfig
              );
              provider = fallback.provider;
              model = fallback.model;
              providerName = fallback.providerName;

              const retryMessages = sanitizeConversationMessages([
                ...messages,
                {
                  role: 'system',
                  content: buildModelFailureLoopPrompt({
                    failedModel,
                    nextModel: model,
                    errorMessage: err.message
                  })
                }
              ]);

              const fallbackCall = await this.requestModelResponse({
                provider,
                providerName,
                model,
                messages: retryMessages,
                tools,
                options: { ...options, userId },
                runId,
                iteration,
              });
              response = fallbackCall.response;
              responseModel = fallbackCall.responseModel;
              streamContent = fallbackCall.streamContent;
            } else {
              throw err;
            }
          }
        };

        try {
          await tryModelCall();
        } catch (err) {
          const modelError = String(err?.message || 'Model call failed');

          if (modelFailureRecoveries < loopPolicy.maxModelFailureRecoveries) {
            const failedModel = model;
            const switched = await switchToFallbackModel(failedModel, err, 'model turn');
            if (!switched) throw err;
            modelFailureRecoveries += 1;
            failedStepCount += 1;
            messages.push({
              role: 'system',
              content: buildModelFailureLoopPrompt({
                failedModel,
                nextModel: model,
                errorMessage: modelError
              })
            });
            this.emit(userId, 'run:interim', {
              runId,
              message: 'Model call failed; adapting and retrying autonomously.',
              phase: 'recovering'
            });
            continue;
          }

          throw err;
        }

        if (!response) {
          response = { content: streamContent, toolCalls: [], finishReason: 'stop', usage: null };
        }

        if (response.usage) {
          totalTokens += response.usage.totalTokens || 0;
        }

        lastContent = sanitizeModelOutput(response.content || streamContent || '', { model: responseModel });

        if ((!response.toolCalls || response.toolCalls.length === 0) && lastContent) {
          const salvaged = salvageTextToolCalls(lastContent, tools);
          if (salvaged.toolCalls.length > 0) {
            response.toolCalls = salvaged.toolCalls;
            response.finishReason = 'tool_calls';
            response.content = salvaged.content;
            lastContent = salvaged.content;
          }
        }

        this.recordRunEvent(userId, runId, 'model_turn_completed', {
          iteration,
          toolCallCount: response.toolCalls?.length || 0,
          contentPreview: String(lastContent || streamContent || '').slice(0, 240),
        }, { agentId });

        const assistantMessage = { role: 'assistant', content: lastContent };
        if (response.toolCalls?.length) assistantMessage.tool_calls = response.toolCalls;
        if (response.providerContentBlocks?.length) assistantMessage.providerContentBlocks = response.providerContentBlocks;
        messages.push(assistantMessage);

        if (conversationId) {
          db.prepare('INSERT INTO conversation_messages (conversation_id, role, content, tool_calls, tokens) VALUES (?, ?, ?, ?, ?)')
            .run(
              conversationId,
              'assistant',
              lastContent,
              response.toolCalls?.length ? JSON.stringify(response.toolCalls) : null,
              response.usage?.totalTokens || 0
            );
        }

        if (!response.toolCalls || response.toolCalls.length === 0) {
          const steeringAfterResponse = this.applyQueuedSteering(runId, messages, {
            userId,
            conversationId
          });
          messages = steeringAfterResponse.messages;
          if (steeringAfterResponse.appliedCount > 0) {
            iteration = Math.max(0, iteration - 1);
            lastContent = '';
            continue;
          }
          const messagingSent = this.activeRuns.get(runId)?.messagingSent || false;
          if (this.shouldFastCompleteVoiceReply({
            options,
            toolExecutions,
            failedStepCount,
            messagingSent,
            lastReply: lastContent,
          })) {
            break;
          }
          if (iteration < maxIterations) {
            const proactiveRunNeedsDecision = (
              (triggerSource === 'schedule' || triggerSource === 'tasks')
              && this.activeRuns.get(runId)?.noResponse !== true
              && options.deliveryState?.noResponse !== true
            );
            const fallbackStatus = (
              proactiveRunNeedsDecision
              || toolExecutions.length > 0
              || failedStepCount > 0
              || messagingSent
            ) ? 'continue' : 'complete';
            const loopState = await runWithModelFallback('loop decision', () => this.decideLoopState({
              provider,
              providerName,
              model,
              messages,
              tools,
              analysis,
              plan,
              toolExecutions,
              lastReply: lastContent,
              triggerSource,
              messagingSent,
              iteration,
              maxIterations,
              options: { ...options, runId, userId, agentId },
              fallbackStatus,
            }));
            totalTokens += loopState.usage || 0;
            if (loopState.decision.status === 'continue') {
              messages.push({
                role: 'system',
                content: [
                  loopState.decision.reason ? `Continue working: ${loopState.decision.reason}.` : 'Continue working autonomously.',
                  messagingSent
                    ? 'You already sent a user-facing message in this run. Keep working silently unless you have a materially new finished result or a real external blocker.'
                    : 'Use send_interim_update sparingly if a short real update or question would help. Otherwise keep working until you have the result or a real blocker.',
                ].join(' ')
              });
              lastContent = '';
              continue;
            }
          }
          break;
        }

        const canRunParallelBatch = (
          response.toolCalls.length > 1
          && response.toolCalls.every((toolCall) => this.isReadOnlyToolCall(toolCall))
        );
        if (canRunParallelBatch) {
          const batch = await this.executeReadOnlyBatch(response.toolCalls, {
            userId,
            runId,
            agentId,
            app,
            triggerType,
            triggerSource,
            conversationId,
            startingStepIndex: stepIndex,
            iteration,
            options,
          });
          stepIndex = batch.endingStepIndex;
          for (const item of batch.results) {
            const execution = classifyToolExecution(
              item.toolName,
              item.toolArgs,
              item.result,
              item.error || '',
            );
            execution.input = item.toolArgs;
            execution.artifacts = await extractArtifactsFromResult(item.toolName, item.result);
            toolExecutions.push(execution);
            this.getRunMeta(runId)?.repetitionGuard?.observe(item.toolName, item.toolArgs, item.result);
            if (item.error) failedStepCount += 1;
            const modelPayload = compactPayloadForModel(item.toolName, item.result);
            const toolResultLimits = resolveToolResultLimits(item.toolName, loopPolicy);
            const toolMessage = {
              role: 'tool',
              name: item.toolName,
              tool_call_id: item.toolCall.id,
              content: compactToolResult(item.toolName, item.toolArgs, modelPayload.result, {
                softLimit: toolResultLimits.softLimit,
                hardLimit: toolResultLimits.hardLimit,
              }),
            };
            messages.push(toolMessage);
            if (conversationId) {
              db.prepare(
                `INSERT INTO conversation_messages (
                  conversation_id, role, content, tool_call_id, name
                ) VALUES (?, 'tool', ?, ?, ?)`
              ).run(conversationId, toolMessage.content, item.toolCall.id, item.toolName);
            }
          }
          this.persistRunMetadata(runId, {
            evidenceSources: [...new Set(toolExecutions.map((item) => item.evidenceSource).filter(Boolean))],
            subagentState: this.listSubagents(runId),
            deliverableArtifacts,
            compactionMetrics: compactionMetrics.slice(-20),
          });
          continue;
        }

        for (const toolCall of response.toolCalls) {
          if (this.isRunStopped(runId)) break;
          stepIndex++;
          const stepId = uuidv4();
          const toolName = toolCall.function.name;
          const stepStartedAt = Date.now();
          let toolArgs;
          try {
            toolArgs = JSON.parse(toolCall.function.arguments || '{}');
          } catch {
            toolArgs = {};
          }

          // ── task_complete: AI explicitly signals the task is fully done ──
          // Handle before DB insert / before_tool_call hook — this is not a
          // regular tool execution, it is a loop-exit signal.
          if (toolName === 'task_complete') {
            const finalMessage = String(toolArgs.message || '').trim();
            const confidence = normalizeCompletionConfidence(toolArgs.confidence || 'medium');
            const completionDecision = shouldAcceptTaskComplete({
              confidence,
              requiredConfidence: analysis?.completion_confidence_required || 'medium',
              iteration,
              maxIterations,
            });
            this.recordRunEvent(userId, runId, 'task_complete_signaled', {
              confidence,
              requiredConfidence: analysis?.completion_confidence_required || 'medium',
              accepted: completionDecision.accept,
              iteration,
              messageLength: finalMessage.length,
            }, { agentId });
            console.info(
              `[Run ${shortenRunId(runId)}] task_complete signaled at iteration=${iteration} confidence=${confidence} accepted=${completionDecision.accept}`
            );
            if (!completionDecision.accept) {
              messages.push({
                role: 'tool',
                name: toolName,
                tool_call_id: toolCall.id,
                content: JSON.stringify({
                  status: 'continue',
                  reason: completionDecision.reason,
                  required_confidence: analysis?.completion_confidence_required || 'medium',
                }),
              });
              messages.push({
                role: 'system',
                content: `${completionDecision.reason} Do not ask the user to decide the next step unless external input is truly required.`
              });
              continue;
            }
            if (completionDecision.reason) {
              messages.push({
                role: 'system',
                content: completionDecision.reason,
              });
            }
            lastContent = finalMessage; // empty string is valid; downstream handles it
            directAnswerEligible = true;
            break; // exit the for-loop; the while condition will also exit
          }

          const repetitionGuard = this.getRunMeta(runId)?.repetitionGuard;
          if (repetitionGuard?.shouldBlock(toolName, toolArgs)) {
            const blockedResult = {
              tool: toolName,
              status: 'blocked',
              reason: 'The same tool call already returned an unchanged result twice. Change the approach or complete with the available evidence.',
            };
            messages.push({
              role: 'tool',
              name: toolName,
              tool_call_id: toolCall.id,
              content: JSON.stringify(blockedResult),
            });
            this.recordRunEvent(userId, runId, 'repetition_blocked', {
              toolName,
              toolArgs,
            }, { agentId });
            this.emit(userId, 'run:tool_end', {
              runId,
              toolName,
              status: 'blocked',
              result: blockedResult,
            });
            messages.push({
              role: 'system',
              content: 'The repeated call was blocked because it made no progress. Use a different tool, change the arguments, or finish with a truthful result.',
            });
            continue;
          }

          // ── before_tool_call hook ──
          // Plugins can block a tool call (e.g. security policy) or mutate args.
          if (globalHooks.has('before_tool_call')) {
            const hookCtx = { toolName, toolArgs, runId, userId, agentId, iteration };
            const hookResult = await globalHooks.run('before_tool_call', hookCtx);
            if (hookResult.block) {
              console.warn(`[Run ${shortenRunId(runId)}] before_tool_call hook blocked tool=${toolName}`);
              // Treat as a soft skip — add a skipped tool message so the model knows
              messages.push({
                role: 'tool',
                name: toolName,
                tool_call_id: toolCall.id,
                content: JSON.stringify({ tool: toolName, status: 'skipped', reason: 'Blocked by policy.' }),
              });
              continue;
            }
            if (hookResult.toolArgs) toolArgs = hookResult.toolArgs;
          }

          db.prepare('INSERT INTO agent_steps (id, run_id, step_index, type, description, status, tool_name, tool_input, started_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, datetime(\'now\'))')
            .run(stepId, runId, stepIndex, this.getStepType(toolName), `${toolName}: ${JSON.stringify(toolArgs).slice(0, 200)} `, 'running', toolName, JSON.stringify(toolArgs));

          this.emit(userId, 'run:tool_start', {
            runId, stepId, stepIndex, toolName, toolArgs,
            type: this.getStepType(toolName)
          });
          this.recordRunEvent(userId, runId, 'tool_started', {
            stepIndex,
            toolName,
            toolArgs,
            type: this.getStepType(toolName),
          }, { agentId, stepId });
          console.info(
            `[Run ${shortenRunId(runId)}] step=${stepIndex} start tool=${toolName} args=${summarizeForLog(toolArgs)}`
          );

          let toolResult;
          let toolErrorMessage = '';
          try {
            toolResult = await this.executeTool(toolName, toolArgs, {
              userId,
              runId,
              agentId,
              app,
              triggerType,
              triggerSource,
              conversationId,
              source: options.source || null,
              chatId: options.chatId || null,
              taskId: options.taskId || null,
              widgetId: options.widgetId || null,
              deliveryState: options.deliveryState || null,
              allowMultipleProactiveMessages: options.allowMultipleProactiveMessages === true,
              allowExternalSideEffects: options.allowExternalSideEffects === true,
            });
            this.detachProcessFromRun(runId, toolResult?.pid);
            toolErrorMessage = inferToolFailureMessage(toolName, toolResult);
            if (toolErrorMessage) {
              failedStepCount++;
            }
            const screenshotPath = toolResult?.screenshotPath || null;
            const stepStatus = this.isRunStopped(runId) ? 'stopped' : (toolErrorMessage ? 'failed' : 'completed');
            db.prepare('UPDATE agent_steps SET status = ?, result = ?, error = ?, screenshot_path = ?, completed_at = datetime(\'now\') WHERE id = ?')
              .run(stepStatus, JSON.stringify(toolResult).slice(0, 20000), toolErrorMessage || null, screenshotPath, stepId);
            if (toolErrorMessage) {
              this.emit(userId, 'run:tool_end', { runId, stepId, toolName, error: toolErrorMessage, result: toolResult, screenshotPath, status: stepStatus });
              this.recordRunEvent(userId, runId, 'tool_failed', {
                toolName,
                status: stepStatus,
                error: toolErrorMessage,
                durationMs: Date.now() - stepStartedAt,
                resultPreview: summarizeForLog(toolResult),
              }, { agentId, stepId });
              console.warn(
                `[Run ${shortenRunId(runId)}] step=${stepIndex} failed tool=${toolName} durationMs=${Date.now() - stepStartedAt} error=${summarizeForLog(toolErrorMessage, 160)}`
              );
            } else {
              this.emit(userId, 'run:tool_end', { runId, stepId, toolName, result: toolResult, screenshotPath, status: stepStatus });
              this.recordRunEvent(userId, runId, 'tool_completed', {
                toolName,
                status: stepStatus,
                durationMs: Date.now() - stepStartedAt,
                resultPreview: summarizeForLog(toolResult),
              }, { agentId, stepId });
              console.info(
                `[Run ${shortenRunId(runId)}] step=${stepIndex} done tool=${toolName} status=${stepStatus} durationMs=${Date.now() - stepStartedAt} result=${summarizeForLog(toolResult)}`
              );
            }
          } catch (err) {
            toolResult = { error: err.message };
            toolErrorMessage = String(err.message || 'Tool execution failed');
            failedStepCount++;
            this.detachProcessFromRun(runId, toolResult?.pid);
            db.prepare('UPDATE agent_steps SET status = ?, error = ?, completed_at = datetime(\'now\') WHERE id = ?')
              .run('failed', err.message, stepId);
            this.emit(userId, 'run:tool_end', { runId, stepId, toolName, error: err.message, status: 'failed' });
            this.recordRunEvent(userId, runId, 'tool_failed', {
              toolName,
              status: 'failed',
              error: err.message,
              durationMs: Date.now() - stepStartedAt,
            }, { agentId, stepId });
            console.warn(
              `[Run ${shortenRunId(runId)}] step=${stepIndex} failed tool=${toolName} durationMs=${Date.now() - stepStartedAt} error=${summarizeForLog(err.message, 160)}`
            );
          }

          const execution = classifyToolExecution(toolName, toolArgs, toolResult, toolErrorMessage);
          execution.input = toolArgs;
          repetitionGuard?.observe(toolName, toolArgs, toolResult);
          execution.artifacts = await extractArtifactsFromResult(toolName, toolResult);
          toolExecutions.push(execution);
          if (deliverableWorkflow && Array.isArray(execution.artifacts) && execution.artifacts.length > 0) {
            for (const artifact of execution.artifacts) {
              if (!deliverableArtifacts.some((existing) => (
                (existing.path && artifact.path && existing.path === artifact.path)
                || (existing.uri && artifact.uri && existing.uri === artifact.uri)
              ))) {
                deliverableArtifacts.push(artifact);
                this.recordRunEvent(userId, runId, 'deliverable_artifact_produced', {
                  type: deliverableWorkflow.selection.type,
                  toolName,
                  artifact,
                }, { agentId, stepId });
              }
            }
          }
          this.persistRunMetadata(runId, {
            evidenceSources: [...new Set(toolExecutions.map((item) => item.evidenceSource).filter(Boolean))],
            subagentState: this.listSubagents(runId),
            deliverableArtifacts,
            compactionMetrics: compactionMetrics.slice(-20),
          });
          const modelPayload = compactPayloadForModel(toolName, toolResult);
          if (modelPayload.metrics?.applied) {
            const metric = {
              toolName,
              stepId,
              ...modelPayload.metrics,
              createdAt: new Date().toISOString(),
            };
            compactionMetrics.push(metric);
            this.persistRunMetadata(runId, {
              compactionMetrics: compactionMetrics.slice(-20),
            });
            this.recordRunEvent(userId, runId, 'pre_model_compaction_applied', {
              toolName,
              metrics: modelPayload.metrics,
            }, { agentId, stepId });
          }

          const toolResultLimits = resolveToolResultLimits(toolName, loopPolicy);
          const toolMessage = {
            role: 'tool',
            name: toolName,
            tool_call_id: toolCall.id,
            content: compactToolResult(toolName, toolArgs, modelPayload.result, {
              softLimit: toolResultLimits.softLimit,
              hardLimit: toolResultLimits.hardLimit,
            })
          };
          messages.push(toolMessage);
          if (toolName === 'activate_tools' && !toolErrorMessage) {
            tools = this.getActiveTools(runId);
          }

          if (toolErrorMessage) {
            consecutiveToolFailures += 1;
            const alternativeTools = summarizeAvailableTools(tools, { exclude: toolName });
            messages.push({
              role: 'system',
              content: [
                `Tool "${toolName}" failed with error: ${summarizeForLog(toolErrorMessage, 240)}.`,
                'This tool failure is not, by itself, a user-facing blocker.',
                'Continue autonomously: retry with corrected arguments, try an alternative tool/path, or verify the outcome using other available tools.',
                alternativeTools ? `Other available tools in this run: ${alternativeTools}.` : '',
                'Only stop and tell the user you are blocked if the remaining issue truly requires an external dependency or user action outside this run.'
              ].filter(Boolean).join(' ')
            });

            if (consecutiveToolFailures >= loopPolicy.maxConsecutiveToolFailures) {
              messages.push({
                role: 'system',
                content: `There were ${consecutiveToolFailures} consecutive tool failures. Stop calling tools now and return a clear blocker response that summarizes attempted actions and concrete errors.`
              });
              break;
            }
          } else {
            consecutiveToolFailures = 0;
          }

          if (toolName === 'send_interim_update') {
            messages.push({
              role: 'system',
              content: 'An interim user-visible update was already sent. Do not later output meta commentary about having already replied. When you have the final answer, give the answer itself. If you need to deliver that final answer to the user in messaging, use send_message.'
            });
          }

          if (toolName === 'execute_command' && (toolResult?.timedOut || toolResult?.killed)) {
            messages.push({
              role: 'system',
              content: 'The previous shell command did not finish cleanly. Keep working until you rerun it with enough time or verify the requested outcome with follow-up commands.'
            });
          }

          if (
            toolName === 'execute_command'
            && toolResult?.exitCode !== undefined
            && toolResult.exitCode !== 0
          ) {
            messages.push({
              role: 'system',
              content: 'The previous shell command exited non-zero. Treat its output as partial evidence only. If it chained multiple shell segments, later segments may not have run. Do not summarize missing sections as observed facts; rerun or verify them separately first.'
            });
          }

          if (conversationId) {
            db.prepare('INSERT INTO conversation_messages (conversation_id, role, content, tool_call_id, name) VALUES (?, ?, ?, ?, ?)')
              .run(conversationId, 'tool', toolMessage.content, toolCall.id, toolName);
          }

          const runMeta = this.activeRuns.get(runId);
          if (runMeta) {
            runMeta.lastToolName = toolName;
            runMeta.lastToolTarget = toolName === 'send_message' ? toolArgs.to : null;
            if (toolName === 'save_widget_snapshot' && !toolErrorMessage) {
              runMeta.widgetSnapshotSaved = true;
            }
          }

          if (toolName === 'save_widget_snapshot' && !toolErrorMessage) {
            lastContent = 'Widget snapshot updated.';
            break;
          }

          if (runMeta?.terminalInterim) {
            break;
          }
        }

        if (this.isRunStopped(runId)) break;
        if (this.getRunMeta(runId)?.terminalInterim) break;
        if (this.getRunMeta(runId)?.widgetSnapshotSaved) break;
        if (!this.activeRuns.has(runId)) break;
      }

      if (this.isRunStopped(runId)) {
        db.prepare('UPDATE agent_runs SET status = ?, updated_at = datetime(\'now\'), completed_at = datetime(\'now\') WHERE id = ?')
          .run('stopped', runId);
        console.warn(
          `[Run ${shortenRunId(runId)}] stopped trigger=${triggerSource} steps=${stepIndex} tokens=${totalTokens}`
        );
        this.activeRuns.delete(runId);
        this.emit(userId, 'run:stopped', { runId, triggerSource });
        this.recordRunEvent(userId, runId, 'run_stopped', {
          triggerSource,
          totalTokens,
          iterations: iteration,
        }, { agentId });
        return { runId, content: '', totalTokens, iterations: iteration, status: 'stopped' };
      }

      const runMeta = this.activeRuns.get(runId);
      if (runMeta?.terminalInterim) {
        lastContent = '';
      }
      if (runMeta?.widgetSnapshotSaved && !lastContent) {
        lastContent = 'Widget snapshot updated.';
      }
      const messagingSent = runMeta?.messagingSent || false;
      const lastToolWasMessaging = runMeta?.lastToolName === 'send_message' || runMeta?.lastToolName === 'make_call';

      if (triggerSource === 'messaging' && !normalizeOutgoingMessage(lastContent, options?.source || null) && !messagingSent) {
        const recovered = await this.recoverBlankMessagingReply({
          userId,
          runId,
          messages,
          provider,
          model,
          providerName,
          options: { ...options, runId, userId, agentId },
          stepIndex,
          failedStepCount,
          toolExecutions,
          tools
        });
        lastContent = recovered.content;
        totalTokens += recovered.tokens || 0;
        if (normalizeOutgoingMessage(lastContent, options?.source || null)) {
          messages.push({ role: 'assistant', content: lastContent });
          if (conversationId) {
            db.prepare('INSERT INTO conversation_messages (conversation_id, role, content, tokens) VALUES (?, ?, ?, ?)')
              .run(conversationId, 'assistant', lastContent, recovered.tokens || 0);
          }
        }
      }

      if (
        !normalizeOutgoingMessage(lastContent, options?.source || null)
        && !messagingSent
        && runMeta?.widgetSnapshotSaved !== true
      ) {
        const explicitNoResponse = (
          runMeta?.noResponse === true
          || options.deliveryState?.noResponse === true
        );
        if (
          (triggerSource === 'schedule' || triggerSource === 'tasks')
          && !explicitNoResponse
        ) {
          throw new Error(
            'Background run ended without producing a result or an explicit no-response decision.',
          );
        }
        if (iteration >= maxIterations) {
          throw new Error(`Iteration limit reached before explicit completion after ${maxIterations} iterations.`);
        }
        if (stepIndex > 0 && !lastToolWasMessaging) {
          throw new Error('Run ended without an explicit completion or blocker reply.');
        }
      }

      const sentMessageText = joinSentMessages(runMeta?.sentMessages);
      const normalizedLastContent = normalizeOutgoingMessage(lastContent, options?.source || null);
      let finalResponseText = messagingSent
        ? (sentMessageText || (normalizedLastContent ? lastContent.trim() : ''))
        : (normalizedLastContent ? lastContent.trim() : sentMessageText);
      const lastVisibleMessage = normalizeOutgoingMessage(
        runMeta?.lastSentMessage
        || runMeta?.lastInterimMessage
        || (Array.isArray(runMeta?.sentMessages) ? runMeta.sentMessages[runMeta.sentMessages.length - 1] : '')
        || '',
        options?.source || null
      );

      if (
        options.skipVerifier !== true
        && shouldRunVerifier({
        analysis,
        toolExecutions,
        finalReply: finalResponseText,
      })) {
        const verificationResult = await runWithModelFallback('final verification', () => this.verifyFinalResponse({
          provider,
          providerName,
          model,
          messages,
          analysis,
          tools,
          toolExecutions,
          finalReply: finalResponseText,
          options: { ...options, runId, userId, agentId },
        }));
        totalTokens += verificationResult.usage || 0;
        verification = verificationResult.verification;
        if (verification.final_reply) {
          finalResponseText = verification.final_reply;
          lastContent = verification.final_reply;
        }

        stepIndex += 1;
        const verificationStepId = uuidv4();
        db.prepare(`INSERT INTO agent_steps
          (id, run_id, step_index, type, description, status, result, started_at, completed_at)
          VALUES (?, ?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'))`)
          .run(
            verificationStepId,
            runId,
            stepIndex,
            'verification',
            'Evidence verification',
            verification.status === 'verified' ? 'completed' : 'failed',
            JSON.stringify(verification).slice(0, 20000)
          );
        this.persistRunMetadata(runId, {
          verification,
          evidenceSources: verificationResult.evidenceSources,
        });
        this.emit(userId, 'run:verification', {
          runId,
          ...verification,
          evidenceSources: verificationResult.evidenceSources,
        });
      }

      if (deliverableWorkflow && deliverablePlan) {
        this.recordRunEvent(userId, runId, 'deliverable_validation_started', {
          type: deliverableWorkflow.selection.type,
          artifactCount: deliverableArtifacts.length,
        }, { agentId });
        const validationResult = await validateDeliverableExecution({
          workflow: deliverableWorkflow.workflow,
          request: deliverableWorkflow.request,
          plan: deliverablePlan,
          finalReply: finalResponseText,
          artifacts: deliverableArtifacts,
          toolExecutions,
          runId,
        });
        deliverableValidation = validationResult.validation;
        this.persistRunMetadata(runId, {
          deliverable: validationResult.result,
        });
        if (deliverableValidation.status !== 'passed') {
          this.recordRunEvent(userId, runId, 'deliverable_validation_failed', {
            type: deliverableWorkflow.selection.type,
            errors: deliverableValidation.errors,
            warnings: deliverableValidation.warnings,
          }, { agentId });
          throw new DeliverableValidationError(
            deliverableValidation.summary || `Deliverable validation failed for ${deliverableWorkflow.selection.type}.`,
            {
              validation: deliverableValidation,
              result: validationResult.result,
            },
          );
        }
        await this.persistDeliverableMemory(userId, runId, agentId, validationResult.result);
        this.recordRunEvent(userId, runId, 'deliverable_completed', {
          type: deliverableWorkflow.selection.type,
          artifactCount: validationResult.result.artifacts.length,
          summary: validationResult.result.summary,
        }, { agentId });
      }

      db.prepare('UPDATE agent_runs SET status = ?, total_tokens = ?, final_response = ?, updated_at = datetime(\'now\'), completed_at = datetime(\'now\') WHERE id = ?')
        .run('completed', totalTokens, finalResponseText || null, runId);

      if (conversationId) {
        db.prepare('UPDATE conversations SET total_tokens = total_tokens + ?, updated_at = datetime(\'now\') WHERE id = ?')
          .run(totalTokens, conversationId);
        if (options.skipConversationMaintenance !== true) {
          refreshConversationSummary(conversationId, provider, model, historyWindow).catch((err) => {
            console.error('[AI] Conversation summary refresh failed:', err.message);
          });
          this.refreshConversationState({
            conversationId,
            provider,
            providerName,
            model,
            finalReply: finalResponseText,
            analysis,
            verification,
            historyWindow,
            options,
          }).catch((err) => {
            console.error('[AI] Conversation working state refresh failed:', err.message);
          });
        }
      }

      await this.persistPromptMetrics(runId, {
        ...promptMetrics,
        finalTotalTokens: totalTokens
      });

      await this.persistRunContext(userId, {
        triggerSource,
        runTitle,
        userMessage,
        lastContent: finalResponseText,
        stepIndex,
        skipPersistence: options.skipRunContextPersistence === true
      });

      // Fallback: if this was a messaging-triggered run and no user-visible
      // message was already sent in this run, auto-send the final assistant text.
      // After any visible reply already went out, later user-facing messages
      // must be sent explicitly via send_message.
      if (triggerSource === 'messaging' && options.source && options.chatId) {
        // Strip [NO RESPONSE] markers the AI may have embedded anywhere in the text,
        // then only send if real content remains.
        const cleanedContent = normalizeOutgoingMessage(lastContent || '', options.source, {
          collapseWhitespace: false
        });
        const shouldSendFallback = (
          cleanedContent
          && runMeta?.explicitMessageSent !== true
          && !lastVisibleMessage
        );
        if (shouldSendFallback) {
          const manager = this.messagingManager;
          if (manager) {
            const chunks = splitOutgoingMessageForPlatform(options.source, cleanedContent);
            console.info(
              `[Run ${shortenRunId(runId)}] messaging_fallback chunks=${chunks.length} to=${summarizeForLog(options.chatId, 80)}`
            );
            for (let i = 0; i < chunks.length; i++) {
              if (i > 0) {
                const delay = Math.max(1000, Math.min(chunks[i].length * 30, 4000));
                await manager.sendTyping(userId, options.source, options.chatId, true, { agentId }).catch(() => { });
                await new Promise((resolve) => setTimeout(resolve, delay));
              }
              await manager.sendMessage(userId, options.source, options.chatId, chunks[i], { runId, agentId }).catch((err) =>
                console.error('[Engine] Auto-reply fallback failed:', err.message)
              );
            }
          }
        }
      }

      console.info(
        `[Run ${shortenRunId(runId)}] completed trigger=${triggerSource} steps=${stepIndex} tokens=${totalTokens} durationMs=${runMeta?.startedAt ? Date.now() - runMeta.startedAt : 0} finalResponse=${finalResponseText ? 'yes' : 'no'} sentMessages=${runMeta?.sentMessages?.length || 0}`
      );
      this.cleanupSubagentsForRun(runId, { cancelRunning: true });
      this.activeRuns.delete(runId);
      this.emit(userId, 'run:complete', {
        runId,
        content: lastContent,
        totalTokens,
        iterations: iteration,
        triggerSource,
        executionMode: analysis?.mode || 'execute',
        verificationStatus: verification?.status || 'skipped',
      });
      this.recordRunEvent(userId, runId, 'run_completed', {
        contentPreview: String(finalResponseText || lastContent || '').slice(0, 240),
        totalTokens,
        iterations: iteration,
        triggerSource,
        executionMode: analysis?.mode || 'execute',
        verificationStatus: verification?.status || 'skipped',
      }, { agentId });
      // ── on_loop_end hook ──
      // Fire-and-forget: plugins can use this for self-improvement, memory
      // consolidation, analytics, or other post-run housekeeping.
      if (globalHooks.has('on_loop_end')) {
        globalHooks.run('on_loop_end', {
          userId, runId, agentId, status: 'completed',
          iterations: iteration, totalTokens,
          taskAnalysis: analysis,
          finalContent: finalResponseText,
        }).catch(() => {});
      }
      if (this.learningManager) {
        try {
          const learningSteps = db.prepare(
            `SELECT tool_name, tool_input, result, status
             FROM agent_steps WHERE run_id = ? ORDER BY step_index ASC`
          ).all(runId);
          this.learningManager.maybeCaptureDraft({
            userId,
            agentId,
            runId,
            triggerSource,
            triggerType,
            task: userMessage,
            title: runTitle,
            finalContent: finalResponseText || lastContent,
            steps: learningSteps,
          });
        } catch (learningError) {
          console.warn('[Engine] Skill reflection failed:', learningError.message);
        }
      }

      return { runId, content: lastContent, totalTokens, iterations: iteration, status: 'completed' };
    } catch (err) {
      if (this.isRunStopped(runId)) {
        db.prepare('UPDATE agent_runs SET status = ?, updated_at = datetime(\'now\'), completed_at = datetime(\'now\') WHERE id = ?')
          .run('stopped', runId);
        console.warn(
          `[Run ${shortenRunId(runId)}] stopped trigger=${triggerSource} steps=${stepIndex} tokens=${totalTokens}`
        );
        this.cleanupSubagentsForRun(runId, { cancelRunning: true });
        this.activeRuns.delete(runId);
        this.emit(userId, 'run:stopped', { runId, triggerSource });
        this.recordRunEvent(userId, runId, 'run_stopped', {
          triggerSource,
          totalTokens,
          iterations: iteration,
        }, { agentId });
        return { runId, content: '', totalTokens, iterations: iteration, status: 'stopped' };
      }

      const runMeta = this.activeRuns.get(runId);
      const retryCount = Number(options.messagingAutonomousRetryCount || 0);
      // Rate-limit errors (429) must not trigger messaging retries: the model
      // won't be available in the milliseconds between retries, so spawning new
      // runs just compounds the rate-limit pressure with no benefit.
      const isRateLimitError = /429|rate.?limit|free-models-per/i.test(String(err?.message || ''));
      const canRetryMessagingRun = (
        triggerSource === 'messaging'
        && options.source
        && options.chatId
        && err?.disableAutonomousRetry !== true
        && !isRateLimitError
        && retryCount < this.getMessagingRetryLimit(maxIterations)
      );

      if (canRetryMessagingRun) {
        const recoveryContext = buildAutonomousRecoveryContext({
          err,
          toolExecutions,
          tools,
          userMessage,
          visibleMessageSent: Boolean(
            runMeta?.lastSentMessage
            || runMeta?.lastInterimMessage
            || runMeta?.messagingSent === true
          ),
        });
        const lastVisibleMessage = normalizeOutgoingMessage(
          runMeta?.lastSentMessage
          || runMeta?.lastInterimMessage
          || '',
          options?.source || null
        );
        db.prepare('UPDATE agent_runs SET status = ?, error = ?, updated_at = datetime(\'now\') WHERE id = ?')
          .run('retrying', err.message, runId);
        console.warn(
          `[Run ${shortenRunId(runId)}] retrying_messaging_attempt=${retryCount + 1} reason=${summarizeForLog(err.message, 140)}`
        );
        this.cleanupSubagentsForRun(runId, { cancelRunning: true });
        this.activeRuns.delete(runId);
        this.emit(userId, 'run:interim', {
          runId,
          message: 'Retrying internally after a transient failure.',
          phase: 'retrying'
        });

        const retryOptions = {
          ...options,
          messagingAutonomousRetryCount: retryCount + 1,
          messagingRetryState: {
            lastVisibleMessage: lastVisibleMessage || String(options?.messagingRetryState?.lastVisibleMessage || '').trim(),
            explicitMessageSent: runMeta?.explicitMessageSent === true || options?.messagingRetryState?.explicitMessageSent === true,
          },
          context: {
            ...(options.context || {}),
            additionalContext: [
              options.context?.additionalContext || '',
              recoveryContext,
            ].filter(Boolean).join('\n\n')
          }
        };
        delete retryOptions.runId;

        return this.runWithModel(userId, userMessage, retryOptions, _modelOverride);
      }

      const deliverableFailureResponse = err?.deliverableResult?.summary
        || err?.deliverableValidation?.summary
        || '';
      let messagingFailureContent = '';
      let sendSucceeded = false;
      if (triggerSource === 'messaging' && options.source && options.chatId) {
        if (!runMeta?.messagingSent) {
          const manager = this.messagingManager;
          if (manager) {
            const failureScenario = buildMessagingFailureScenario({
              err,
              failedStepCount,
              stepIndex,
              toolExecutions,
            });
            try {
              const failedMessage = sanitizeConversationMessages([
                ...messages,
                {
                  role: 'system',
                  content: `The run encountered a runtime error and cannot continue reliably. Use the actual run scenario below to explain the blocker naturally.\n\nScenario:\n${failureScenario || 'No additional scenario details were captured.'}\n\nDo not call tools. Write exactly one short user message. Do not ask the user to resend or restate the same task. Only ask the user for something if a specific external input, permission, or configuration change is actually required. Do not promise future work unless it will happen automatically before this reply is sent.\n\n${buildPlatformFormattingGuide(options?.source || null)}`
                }
              ]);
              const modelReply = await provider.chat(failedMessage, [], {
                model,
                reasoningEffort: this.getReasoningEffort(providerName, options)
              });
              const drafted = sanitizeModelOutput(modelReply.content || '', { model });
              if (normalizeOutgoingMessage(drafted, options?.source || null)) {
                messagingFailureContent = drafted.trim();
              }
            } catch {
              // Fall back to deterministic text below.
            }

            if (!messagingFailureContent) {
              messagingFailureContent = buildDeterministicMessagingErrorReply({
                err,
                failedStepCount,
                stepIndex,
                toolExecutions,
              });
            }

            try {
              await manager.sendMessage(userId, options.source, options.chatId, messagingFailureContent, { runId, agentId });
              sendSucceeded = true;
              if (runMeta) {
                runMeta.lastSentMessage = messagingFailureContent;
                if (!Array.isArray(runMeta.sentMessages)) runMeta.sentMessages = [];
                runMeta.sentMessages.push(messagingFailureContent);
              }
            } catch (sendErr) {
              console.error('[Engine] Messaging error fallback failed:', sendErr.message);
              messagingFailureContent = '';
            }
          }
        }
      }

      db.prepare('UPDATE agent_runs SET status = ?, error = ?, final_response = ?, updated_at = datetime(\'now\') WHERE id = ?')
        .run(
          'failed',
          err.message,
          sendSucceeded
            ? (messagingFailureContent || null)
            : (deliverableFailureResponse || null),
          runId,
        );
      console.error(
        `[Run ${shortenRunId(runId)}] failed trigger=${triggerSource} steps=${stepIndex} tokens=${totalTokens} error=${summarizeForLog(err.message, 180)}`
      );

      this.cleanupSubagentsForRun(runId, { cancelRunning: true });
      this.activeRuns.delete(runId);
      this.emit(userId, 'run:error', { runId, error: err.message });
      this.recordRunEvent(userId, runId, 'run_failed', {
        error: err.message,
        totalTokens,
        iterations: iteration,
        deliverableType: deliverableWorkflow?.selection?.type || null,
      }, { agentId });

      if (messagingFailureContent) {
        return {
          runId,
          content: messagingFailureContent,
          totalTokens,
          iterations: iteration,
          status: 'failed'
        };
      }

      throw err;
    }
  }

  async spawnSubagent(userId, parentRunId, task, options = {}) {
    const handle = uuidv4();
    const childRunId = uuidv4();
    let relevantMemories = [];
    try {
      relevantMemories = this.memoryManager
        ? await this.memoryManager.recallMemory(userId, task, {
          agentId: options.agentId || null,
          limit: 4,
        })
        : [];
    } catch {}
    const subEngine = new AgentEngine(this.io, {
      app: options.app || this.app,
      browserController: this.browserController,
      androidController: this.androidController,
      runtimeManager: this.runtimeManager,
      workspaceManager: this.workspaceManager,
      messagingManager: this.messagingManager,
      mcpManager: this.mcpManager,
      skillRunner: this.skillRunner,
      taskRuntime: this.taskRuntime,
      memoryManager: this.memoryManager,
    });

    const subagentContract = [
      `Goal: ${String(task || '').trim()}`,
      options.context ? `Constraints and relevant context:\n${String(options.context).trim()}` : '',
      relevantMemories.length > 0
        ? `Top relevant memories: ${JSON.stringify(relevantMemories.map((memory) => ({
          content: memory.content,
          confidence: memory.confidence,
        })))}`
        : '',
      Array.isArray(options.requiredArtifacts) && options.requiredArtifacts.length > 0
        ? `Required artifacts: ${JSON.stringify(options.requiredArtifacts)}`
        : '',
      Array.isArray(options.selectedTools) && options.selectedTools.length > 0
        ? `Selected tools: ${JSON.stringify(options.selectedTools.slice(0, 20))}`
        : '',
      'Return a single JSON object with exactly these top-level fields: status, findings, evidence, artifacts, confidence, remaining_blockers.',
      'status must be completed, partial, or blocked. findings, evidence, artifacts, and remaining_blockers must be arrays. confidence must be low, medium, or high.',
    ].filter(Boolean).join('\n\n');
    const record = {
      handle,
      parentRunId,
      childRunId,
      userId,
      agentId: options.agentId || null,
      task: subagentContract,
      model: options.model || null,
      status: 'running',
      createdAt: new Date().toISOString(),
      result: null,
      error: null,
      engine: subEngine,
      promise: null,
    };
    this.subagents.set(handle, record);
    this.emit(userId, 'run:subagent', {
      runId: parentRunId,
      handle,
      childRunId,
      status: 'running',
      task: clampRunContext(task, 180),
    });

    record.promise = (async () => {
      try {
        const result = await subEngine.runWithModel(
          userId,
          subagentContract,
          {
            app: options.app || this.app,
            triggerType: 'subagent',
            triggerSource: 'agent',
            runId: childRunId,
            agentId: options.agentId || null,
          },
          options.model || null
        );
        record.status = result.status || 'completed';
        let structured = null;
        try {
          const raw = String(result.content || '').trim().replace(/^```(?:json)?\s*/i, '').replace(/\s*```$/, '');
          const parsed = JSON.parse(raw);
          if (parsed && typeof parsed === 'object') structured = parsed;
        } catch {}
        record.result = {
          runId: result.runId,
          status: structured?.status || result.status || 'completed',
          findings: Array.isArray(structured?.findings) ? structured.findings : [String(result.content || '').trim()].filter(Boolean),
          evidence: Array.isArray(structured?.evidence) ? structured.evidence : [],
          artifacts: Array.isArray(structured?.artifacts) ? structured.artifacts : [],
          confidence: ['low', 'medium', 'high'].includes(structured?.confidence) ? structured.confidence : 'medium',
          remainingBlockers: Array.isArray(structured?.remaining_blockers) ? structured.remaining_blockers : [],
          totalTokens: result.totalTokens,
          iterations: result.iterations,
        };
        this.emit(userId, 'run:subagent', {
          runId: parentRunId,
          handle,
          childRunId,
          status: record.status,
          result: record.result,
        });
        return record;
      } catch (err) {
        record.status = 'failed';
        record.error = err.message;
        this.emit(userId, 'run:subagent', {
          runId: parentRunId,
          handle,
          childRunId,
          status: 'failed',
          error: err.message,
        });
        throw err;
      }
    })();

    return {
      handle,
      status: 'running',
      childRunId,
      task: clampRunContext(task, 180),
    };
  }

  async delegateToAgent({
    userId,
    parentAgentId,
    parentRunId,
    target,
    task,
    context = '',
    app = null,
    allowExternalSideEffects = false,
  } = {}) {
    const { agentCanDelegateTo, getAgentById, getAgentBySlug, resolveAgentId } = require('../agents/manager');
    const targetText = String(target || '').trim();
    const taskText = String(task || '').trim();
    if (!targetText || !taskText) {
      throw new Error('Target agent and task are required.');
    }

    let targetAgent = getAgentById(userId, targetText) || getAgentBySlug(userId, targetText);
    if (!targetAgent) {
      targetAgent = db.prepare(
        "SELECT * FROM agents WHERE user_id = ? AND status = 'active' AND lower(display_name) = lower(?)"
      ).get(userId, targetText);
    }
    if (!targetAgent || targetAgent.status !== 'active') {
      throw new Error(`No active specialist agent matches "${targetText}".`);
    }

    const scopedParentAgentId = resolveAgentId(userId, parentAgentId);
    const parentAgent = getAgentById(userId, scopedParentAgentId);
    if (targetAgent.id === scopedParentAgentId) {
      throw new Error('An agent cannot delegate to itself.');
    }
    if (!agentCanDelegateTo(parentAgent, targetAgent)) {
      throw new Error(`${parentAgent?.display_name || 'This agent'} is not allowed to delegate tasks to ${targetAgent.display_name}.`);
    }

    const delegationId = uuidv4();
    const childRunId = uuidv4();
    db.prepare(
      `INSERT INTO agent_delegations (
        id, user_id, parent_agent_id, target_agent_id, parent_run_id, child_run_id, task, context, status
      ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'running')`
    ).run(
      delegationId,
      userId,
      scopedParentAgentId,
      targetAgent.id,
      parentRunId || null,
      childRunId,
      taskText,
      context || null,
    );

    const delegatedPrompt = [
      '[SYSTEM: Delegated specialist-agent task]',
      `You are running as ${targetAgent.display_name} (${targetAgent.slug}).`,
      'Complete this delegated task using only your own agent memory, settings, credentials, and available tools.',
      allowExternalSideEffects
        ? 'External side effects are allowed only when they directly satisfy the delegated task.'
        : 'Do not send external messages, make calls, or change external shared systems. Return findings and recommendations to the parent agent instead.',
      '',
      `Task:\n${taskText}`,
      context ? `\nContext from parent agent:\n${context}` : '',
    ].filter(Boolean).join('\n');

    try {
      const result = await this.runWithModel(
        userId,
        delegatedPrompt,
        {
          app: app || this.app,
          runId: childRunId,
          agentId: targetAgent.id,
          triggerType: 'agent_delegation',
          triggerSource: 'agent_delegation',
          skipConversationHistory: true,
          skipConversationMaintenance: true,
          context: { additionalContext: `Parent run: ${parentRunId || 'unknown'}` },
          allowExternalSideEffects,
        },
        null,
      );
      const summary = String(result?.content || '').trim();
      db.prepare(
        `UPDATE agent_delegations
         SET status = ?, result_summary = ?, updated_at = datetime('now'), completed_at = datetime('now')
         WHERE id = ?`
      ).run(result?.status || 'completed', summary.slice(0, 20000), delegationId);
      return {
        delegationId,
        targetAgent: {
          id: targetAgent.id,
          slug: targetAgent.slug,
          name: targetAgent.display_name,
        },
        childRunId: result?.runId || childRunId,
        status: result?.status || 'completed',
        summary,
        totalTokens: result?.totalTokens || 0,
      };
    } catch (err) {
      db.prepare(
        `UPDATE agent_delegations
         SET status = 'failed', error = ?, updated_at = datetime('now'), completed_at = datetime('now')
         WHERE id = ?`
      ).run(String(err?.message || err).slice(0, 20000), delegationId);
      throw err;
    }
  }

  listSubagents(parentRunId = null) {
    return Array.from(this.subagents.values())
      .filter((record) => !parentRunId || record.parentRunId === parentRunId)
      .map((record) => ({
        handle: record.handle,
        parentRunId: record.parentRunId,
        childRunId: record.childRunId,
        status: record.status,
        task: clampRunContext(record.task, 180),
        result: record.result,
        error: record.error,
        createdAt: record.createdAt,
      }));
  }

  cleanupSubagentsForRun(parentRunId, options = {}) {
    if (!parentRunId) return;
    const cancelRunning = options.cancelRunning !== false;
    for (const [handle, record] of this.subagents.entries()) {
      if (record.parentRunId !== parentRunId) continue;
      if (cancelRunning && record.status === 'running') {
        try {
          record.engine?.abort(record.childRunId);
          record.status = 'cancelled';
        } catch (err) {
          console.warn(`[AgentEngine] Failed to abort subagent ${handle}:`, err?.message);
        }
      }
      this.subagents.delete(handle);
    }
  }

  async waitForSubagent(handle, options = {}) {
    const record = this.subagents.get(handle);
    if (!record) {
      return { error: `Unknown sub-agent handle: ${handle}` };
    }
    if (options.parentRunId && record.parentRunId !== options.parentRunId) {
      return { error: 'That sub-agent does not belong to the current parent run.' };
    }

    if (record.status !== 'running' || !record.promise) {
      return {
        handle,
        status: record.status,
        result: record.result,
        error: record.error,
      };
    }

    const timeoutMs = Math.max(1000, Number(options.timeoutMs) || 30000);
    const timeout = new Promise((resolve) => {
      setTimeout(() => resolve(null), timeoutMs);
    });
    const settled = await Promise.race([
      record.promise.then(() => record).catch(() => record),
      timeout,
    ]);

    if (!settled) {
      return {
        handle,
        status: 'running',
        timedOut: true,
      };
    }

    return {
      handle,
      status: record.status,
      result: record.result,
      error: record.error,
    };
  }

  async cancelSubagent(handle, options = {}) {
    const record = this.subagents.get(handle);
    if (!record) {
      return { error: `Unknown sub-agent handle: ${handle}` };
    }
    if (options.parentRunId && record.parentRunId !== options.parentRunId) {
      return { error: 'That sub-agent does not belong to the current parent run.' };
    }
    if (record.status !== 'running') {
      return {
        handle,
        status: record.status,
        result: record.result,
        error: record.error,
      };
    }

    record.engine?.abort(record.childRunId);
    record.status = 'cancelled';
    this.emit(record.userId, 'run:subagent', {
      runId: record.parentRunId,
      handle,
      childRunId: record.childRunId,
      status: 'cancelled',
    });

    return { handle, status: 'cancelled' };
  }

  stopRun(runId) {
    const runMeta = this.activeRuns.get(runId);
    const delegatedChildren = db.prepare(
      "SELECT child_run_id FROM agent_delegations WHERE parent_run_id = ? AND status = 'running'"
    ).all(runId);
    if (runMeta) {
      runMeta.status = 'stopped';
      runMeta.aborted = true;
      this.emit(runMeta.userId, 'run:stopping', { runId });
      for (const pid of runMeta.toolPids) {
        if (this.runtimeManager && typeof this.runtimeManager.killCommand === 'function') {
          void this.runtimeManager.killCommand(runMeta.userId, pid, 'aborted');
        }
      }
      runMeta.toolPids.clear();
    }
    for (const child of delegatedChildren) {
      if (child.child_run_id && child.child_run_id !== runId) {
        this.stopRun(child.child_run_id);
      }
    }
    db.prepare(
      "UPDATE agent_delegations SET status = 'stopped', updated_at = datetime('now'), completed_at = datetime('now') WHERE parent_run_id = ? AND status = 'running'"
    ).run(runId);
    db.prepare("UPDATE agent_runs SET status = 'stopped', updated_at = datetime('now') WHERE id = ?").run(runId);
  }

  abort(runId, { userId } = {}) {
    if (!runId) return false;
    if (userId != null) {
      // Ownership gate: never let one user abort another user's active run.
      const runMeta = this.activeRuns.get(runId);
      if (runMeta && Number(runMeta.userId) !== Number(userId)) return false;
    }
    this.stopRun(runId);
    return true;
  }

  abortAll(userId) {
    for (const [runId, run] of this.activeRuns) {
      if (run.userId === userId) this.stopRun(runId);
    }
  }

  getStepType(toolName) {
    if (toolName.startsWith('browser_')) return 'browser';
    if (toolName.startsWith('android_')) return 'android';
    if (toolName === 'execute_command') return 'cli';
    if (toolName.startsWith('memory_')) return 'memory';
    if (toolName === 'send_interim_update') return 'note';
    if (toolName === 'send_message') return 'messaging';
    if (toolName === 'make_call') return 'messaging';
    if (toolName.startsWith('mcp_') || toolName.includes('mcp')) return 'mcp';
    if (toolName === 'create_task' || toolName === 'update_task' || toolName === 'delete_task' || toolName === 'list_tasks' || toolName.includes('widget')) return 'tasks';
    if (toolName.includes('subagent')) return 'subagent';
    if (toolName === 'think') return 'thinking';
    return 'tool';
  }

  emit(userId, event, data) {
    if (this.io) {
      this.io.to(`user:${userId}`).emit(event, data);
    }
  }
}

module.exports = { AgentEngine, buildInitialRunMetadata, getProviderForUser };
