import { restartBrowser } from "./browser-launcher.js";
import { logger } from "../logger.js";
import { evaluateToolAlarm } from "./tool-alarm-evaluator.js";
/**
 * Maximum time (ms) to wait for the evaluator API call before giving up.
 * Prevents a hung evaluator from permanently blocking the alarm for a tool.
 */
const EVALUATOR_TIMEOUT_MS = 15_000;
/** Maximum time (ms) a single browser tool call may run before the query is aborted. */
const BROWSER_TOOL_TIMEOUT_MS = 120_000;
/**
 * How long (ms) a non-browser tool call runs before the first smart alarm fires.
 * After each alarm the evaluator decides whether to continue or abort.
 */
const GENERAL_ALARM_INTERVAL_MS = 30_000;
/**
 * Absolute ceiling (ms) for any non-browser tool call.  If the evaluator keeps
 * saying "continue" past this point the call is force-aborted as a safety net.
 * This is intentionally very generous (30 min) because legitimate operations
 * like shell builds, large downloads, or batch processing can run for a long
 * time.
 */
const ABSOLUTE_TIMEOUT_MS = 30 * 60 * 1000;
/** How often (ms) the backup watchdog checks for stale tool calls. */
const WATCHDOG_POLL_INTERVAL_MS = 15_000;
/**
 * Monitors in-flight tool calls and aborts the query when a call appears stuck.
 *
 * **Browser tools** (Playwright / computer_use): hard timeout at 120 s, then
 * restart Chrome and abort.
 *
 * **All other tools**: after 30 s a "smart alarm" fires — a fast model decides
 * whether to keep waiting or abort.  Alarms repeat every 30 s for as long as
 * the evaluator says "continue".  A generous absolute ceiling
 * ({@link ABSOLUTE_TIMEOUT_MS}, default 30 min) force-aborts as a last-resort
 * safety net.
 */
export class ToolCallWatchdog {
    onTimeout;
    config;
    activityTracker;
    browserTimeoutMs;
    activeToolCalls = new Map();
    watchdogTimer;
    result = null;
    triggerInProgress = false;
    constructor(onTimeout, config, activityTracker, browserTimeoutMs = BROWSER_TOOL_TIMEOUT_MS) {
        this.onTimeout = onTimeout;
        this.config = config;
        this.activityTracker = activityTracker;
        this.browserTimeoutMs = browserTimeoutMs;
        this.watchdogTimer = setInterval(() => {
            this.checkForTimeouts();
        }, WATCHDOG_POLL_INTERVAL_MS);
    }
    // ---------------------------------------------------------------------------
    // Classification
    // ---------------------------------------------------------------------------
    /** Whether a tool name is a GUI-related tool (Playwright, computer_use, or android_use). */
    isGUITool(name) {
        return (name.startsWith("mcp__playwright__") ||
            name.startsWith("mcp__visionclaw__computer_use_") ||
            name.startsWith("mcp__visionclaw__android_use_"));
    }
    /** Whether a tool name is a browser-related tool (Playwright or computer_use). */
    isBrowserTool(name) {
        return (name.startsWith("mcp__playwright__") ||
            name.startsWith("mcp__visionclaw__computer_use_"));
    }
    // ---------------------------------------------------------------------------
    // Tracking
    // ---------------------------------------------------------------------------
    /** Record that a tool call has started. */
    trackStart(toolUseId, toolName, toolInput) {
        this.activeToolCalls.set(toolUseId, {
            name: toolName,
            startMs: Date.now(),
            input: toolInput ?? "",
            alarmCount: 0,
            alarmInFlight: false,
        });
    }
    /** Record that a tool call has completed (success or error). */
    trackEnd(toolUseId) {
        this.activeToolCalls.delete(toolUseId);
    }
    // ---------------------------------------------------------------------------
    // Progress events (SDK `tool_progress`)
    // ---------------------------------------------------------------------------
    /**
     * Handle a `tool_progress` SDK event. If the elapsed time exceeds
     * the threshold for a browser tool, triggers the timeout.
     */
    handleProgress(toolUseId, toolName, elapsedSeconds) {
        if (this.result)
            return;
        const name = toolName ?? "unknown";
        if (this.isGUITool(name) && elapsedSeconds >= this.browserTimeoutMs / 1000) {
            void this.triggerBrowserTimeout(name, toolUseId, elapsedSeconds * 1000);
        }
    }
    // ---------------------------------------------------------------------------
    // Results
    // ---------------------------------------------------------------------------
    /** Return the timeout info if a timeout was triggered, or `null`. */
    getResult() {
        return this.result;
    }
    /** Whether a timeout has been triggered (query should be considered aborted). */
    get triggered() {
        return this.result !== null;
    }
    /** Stop the watchdog timer. Must be called when the stream ends. */
    dispose() {
        clearInterval(this.watchdogTimer);
    }
    // ---------------------------------------------------------------------------
    // Periodic check
    // ---------------------------------------------------------------------------
    /**
     * Backup watchdog check: scans all active tool calls.
     * - Browser tools: hard timeout.
     * - Other tools: smart alarm evaluation.
     */
    checkForTimeouts() {
        if (this.result || this.triggerInProgress)
            return;
        const now = Date.now();
        for (const [id, info] of this.activeToolCalls) {
            const elapsed = now - info.startMs;
            if (this.isBrowserTool(info.name)) {
                // Browser tools: hard timeout
                if (elapsed >= this.browserTimeoutMs) {
                    void this.triggerBrowserTimeout(info.name, id, elapsed);
                    break;
                }
            }
            else {
                // Non-browser tools: smart alarm
                void this.maybeFireSmartAlarm(id, info, elapsed);
            }
        }
    }
    // ---------------------------------------------------------------------------
    // Browser hard timeout (existing behavior)
    // ---------------------------------------------------------------------------
    async triggerBrowserTimeout(toolName, toolUseId, elapsedMs) {
        if (this.result || this.triggerInProgress)
            return;
        this.triggerInProgress = true;
        logger.err(`Tool call timeout: ${toolName} (${toolUseId}) after ${Math.round(elapsedMs / 1000)}s — aborting query`);
        let browserRestarted = false;
        // Only restart Chrome via CDP when using the playwright backend.
        // With the playwriter extension, the relay reconnects automatically.
        if (this.config?.browserBackend !== "playwriter") {
            try {
                await restartBrowser();
                browserRestarted = true;
            }
            catch (err) {
                logger.warn(`Browser restart after timeout failed: ${err instanceof Error ? err.message : String(err)}`);
            }
        }
        this.result = { toolName, toolUseId, elapsedMs, browserRestarted };
        this.onTimeout();
    }
    // ---------------------------------------------------------------------------
    // Smart alarm for non-browser tools
    // ---------------------------------------------------------------------------
    /**
     * Fire a smart alarm if the non-browser tool call has exceeded the alarm
     * interval.  The alarm threshold is `GENERAL_ALARM_INTERVAL_MS * (alarmCount + 1)`
     * so successive alarms are spaced evenly.
     */
    async maybeFireSmartAlarm(toolUseId, info, elapsedMs) {
        // Absolute safety net FIRST: force-abort after ABSOLUTE_TIMEOUT_MS
        // regardless of evaluator decisions or in-flight evaluations.  This must
        // be checked before `alarmInFlight` so a hung evaluator cannot prevent
        // the safety ceiling from firing.
        if (elapsedMs >= ABSOLUTE_TIMEOUT_MS) {
            const elapsedMin = Math.round(elapsedMs / 60_000);
            logger.err(`Smart alarm force-abort: ${info.name} (${toolUseId}) after ${elapsedMin}min (absolute ceiling) — aborting query`);
            this.triggerInProgress = true;
            this.result = {
                toolName: info.name,
                toolUseId,
                toolInput: info.input,
                elapsedMs,
                browserRestarted: false,
                smartAlarm: true,
                smartAlarmReason: `Force-aborted after ${elapsedMin} minutes (absolute safety ceiling).`,
            };
            this.onTimeout();
            return;
        }
        // Not yet time for the next alarm?
        const nextAlarmAt = GENERAL_ALARM_INTERVAL_MS * (info.alarmCount + 1);
        if (elapsedMs < nextAlarmAt)
            return;
        // Already evaluating or already triggered?
        if (info.alarmInFlight || this.result || this.triggerInProgress)
            return;
        // If we don't have config we can't call the evaluator -- just log and let
        // the absolute timeout ceiling handle it.
        if (!this.config) {
            info.alarmCount++;
            logger.warn(`Smart alarm #${info.alarmCount} for ${info.name} (${toolUseId}) at ${Math.round(elapsedMs / 1000)}s — no config, skipping evaluator (absolute ceiling: ${Math.round(ABSOLUTE_TIMEOUT_MS / 60_000)}min)`);
            return;
        }
        info.alarmInFlight = true;
        info.alarmCount++;
        const alarmNumber = info.alarmCount;
        const elapsedSec = Math.round(elapsedMs / 1000);
        logger.system(`Smart alarm #${alarmNumber} for ${info.name} (${toolUseId}) at ${elapsedSec}s — evaluating…`);
        const context = {
            toolName: info.name,
            toolInput: info.input,
            elapsedSeconds: elapsedSec,
            alarmCount: alarmNumber,
            absoluteTimeoutSeconds: Math.round(ABSOLUTE_TIMEOUT_MS / 1000),
            activitySummary: this.activityTracker?.getSummary() ?? "(not available)",
        };
        let fallbackTimer;
        try {
            const decision = await Promise.race([
                evaluateToolAlarm(context, this.config),
                new Promise((resolve) => {
                    fallbackTimer = setTimeout(() => {
                        logger.warn(`Smart alarm #${alarmNumber} for ${info.name}: evaluator timed out after ${EVALUATOR_TIMEOUT_MS / 1000}s`);
                        resolve(null);
                    }, EVALUATOR_TIMEOUT_MS);
                }),
            ]);
            clearTimeout(fallbackTimer);
            // Check if the tool already completed while we were evaluating
            if (!this.activeToolCalls.has(toolUseId)) {
                logger.system(`Smart alarm #${alarmNumber} for ${info.name}: tool completed during evaluation — ignoring decision`);
                return;
            }
            // Re-check after async gap: another alarm or the browser watchdog may
            // have triggered while we were awaiting the evaluator.
            if (this.result || this.triggerInProgress)
                return;
            if (decision === null) {
                // Evaluator failed — default to continue
                logger.warn(`Smart alarm #${alarmNumber} for ${info.name}: evaluator returned null — defaulting to continue`);
            }
            else if (decision.decision === "abort") {
                logger.err(`Smart alarm #${alarmNumber} for ${info.name}: ABORT — ${decision.reason}`);
                this.triggerInProgress = true;
                this.result = {
                    toolName: info.name,
                    toolUseId,
                    toolInput: info.input,
                    elapsedMs: Date.now() - info.startMs,
                    browserRestarted: false,
                    smartAlarm: true,
                    smartAlarmReason: decision.reason,
                };
                this.onTimeout();
            }
            else {
                logger.system(`Smart alarm #${alarmNumber} for ${info.name}: CONTINUE — ${decision.reason}`);
            }
        }
        catch (err) {
            clearTimeout(fallbackTimer);
            logger.warn(`Smart alarm #${alarmNumber} evaluation error: ${err instanceof Error ? err.message : String(err)} — defaulting to continue`);
        }
        finally {
            // Only clear the flag if the tracked call still exists (it may have been
            // deleted by trackEnd while the evaluation was in flight).
            const current = this.activeToolCalls.get(toolUseId);
            if (current) {
                current.alarmInFlight = false;
            }
        }
    }
}
//# sourceMappingURL=tool-call-watchdog.js.map