import { getAllTools } from "../llm/tools.js";
import { BrowserActions } from "../browser/actions.js";
import { runBrowserTool, capToolResultText } from "./tool-runner.js";
import { ScriptRunner } from "../browser/script-runner.js";
import { ExecutionLogger } from "../logging/logger.js";
import { executeStoreAction } from "./executors/store-executor.js";
import { SkillGenerator, } from "./skill-generator.js";
import { ImageGenerator } from "./image-generator.js";
import { ToolLoopDetector, detectorOptionsForProtocol, } from "./tool-loop-detection.js";
import { executeWebFetchAction } from "./executors/web-fetch-executor.js";
import { normalizeStoragePolicies, renderStoragePolicySection, StoragePolicyRuntimeTracker, } from "./storage-policy.js";
import { buildLlmContext, compactMessagesWithState, CONTEXT_BUDGETS, ContextTranscript, createEmptyContextState, estimateMessagesChars, truncateContextSection, } from "./context.js";
import { executeReadOnlySkillAction } from "./executors/skill-executor.js";
import path from "node:path";
import { randomUUID } from "node:crypto";
function estimateCharCount(messages) {
    return estimateMessagesChars(messages);
}
export class TaskRunner {
    config;
    llmClient;
    browser;
    browserActions;
    memory;
    store;
    skillGenerator;
    imageGenerator = null;
    scheduler = null;
    constructor(config, llmClient, browser, memory, store) {
        this.config = config;
        this.llmClient = llmClient;
        this.browser = browser;
        this.browserActions = new BrowserActions(browser, config.browser.actionDelay, path.join(config.workspace, "attachments"), { allowEvaluateFallbacks: config.browser.allowEvaluateFallbacks });
        this.memory = memory;
        this.store = store;
        this.skillGenerator = new SkillGenerator(config.workspace, llmClient);
        if (config.llm.imageGeneration) {
            this.imageGenerator = new ImageGenerator(config.llm.imageGeneration, config.llm.baseUrl, config.workspace);
        }
    }
    /** Set scheduler reference for auto-linking skills to tasks */
    setScheduler(scheduler) {
        this.scheduler = scheduler;
    }
    /** Run a scheduled task with learning loop */
    async runTask(task, signal) {
        throwIfAborted(signal);
        const logsDir = path.join(this.config.workspace, "logs");
        const logger = new ExecutionLogger(logsDir, task.name);
        this.browserActions.setLogger(logger);
        const startedAt = Date.now();
        const storagePolicies = task.storagePolicyMode === "off"
            ? []
            : normalizeStoragePolicies(task.storagePolicies, task.id);
        const canUseSkill = !!task.skillRef && storagePolicies.length === 0;
        const mode = canUseSkill ? "skill" : "llm";
        logger.log("task_start", {
            taskId: task.id,
            taskName: task.name,
            mode,
            prompt: task.prompt,
            skillRef: task.skillRef,
            storagePolicies,
            cron: task.cron,
            timezone: task.timezone,
        });
        try {
            // Path A: Skill-based execution — run skill, then try to improve if needed
            if (canUseSkill && task.skillRef) {
                throwIfAborted(signal);
                const skill = this.skillGenerator.loadSkill(task.skillRef);
                if (skill) {
                    console.log(`  Using skill: ${skill.name}`);
                    const fallbackInstructions = this.skillGenerator.renderLoadedInstructionSkillSection(this.skillGenerator.getAutoLoadedInstructionSkills(`${task.name}\n${task.prompt}\n${skill.name}\n${skill.description}`));
                    const runner = new ScriptRunner(this.browserActions, this.llmClient, logger, fallbackInstructions, this.store);
                    const report = await runner.run(skill);
                    throwIfAborted(signal);
                    // Update skill usage stats
                    this.skillGenerator.updateSkillMeta(task.skillRef, report.completed);
                    // Learning: if some steps used fallback, improve the skill
                    // TODO: temporarily disabled — re-enable when ready
                    // if (report.fallbackCount > 0 && report.completed) {
                    //   console.log(`🧠 Learning: ${report.fallbackCount} step(s) used fallback, improving skill...`);
                    //   await this.skillGenerator.improveSkill(skill, report);
                    // }
                    const reportText = this.formatReport(report);
                    // Propagate failure so callers (scheduler, /run) record correct status
                    if (!report.completed) {
                        logger.log("task_end", {
                            durationMs: Date.now() - startedAt,
                            mode,
                            completionStatus: "failed",
                            stopReason: "skill_failed",
                            successCount: report.successCount,
                            totalSteps: report.totalSteps,
                            fallbackCount: report.fallbackCount,
                            resultPreview: reportText.slice(0, 500),
                        }, false);
                        throw new Error(reportText);
                    }
                    logger.log("task_end", {
                        durationMs: Date.now() - startedAt,
                        mode,
                        completionStatus: "complete",
                        stopReason: "completed",
                        successCount: report.successCount,
                        totalSteps: report.totalSteps,
                        fallbackCount: report.fallbackCount,
                        resultPreview: reportText.slice(0, 500),
                    });
                    return reportText;
                }
            }
            // Path B: LLM-driven execution — then auto-generate skill if worthwhile
            const run = await this.runLlmDriven(task.prompt, logger, signal, `${task.name}\n${task.prompt}`, storagePolicies);
            const result = run.content;
            // Learning: auto-generate skill from successful LLM-driven execution
            // TODO: temporarily disabled — re-enable when ready
            // await this.tryAutoGenerateSkill(task, logger);
            logger.log("task_end", {
                durationMs: Date.now() - startedAt,
                mode,
                completionStatus: run.completionStatus,
                stopReason: run.stopReason,
                resultPreview: result.slice(0, 500),
            });
            return result;
        }
        catch (err) {
            logger.log("task_end", {
                durationMs: Date.now() - startedAt,
                mode,
                completionStatus: "failed",
                stopReason: "error",
                error: err instanceof Error ? err.message : String(err),
            }, false);
            throw err;
        }
        finally {
            this.browserActions.clearLogger(logger);
            console.log(`  Log saved: ${logger.getLogFilePath()}`);
        }
    }
    /** Try to auto-generate a skill from a successful LLM-driven task and link it */
    async tryAutoGenerateSkill(task, logger) {
        const logPath = logger.getLogFilePath();
        if (!this.skillGenerator.shouldAutoGenerate(logPath)) {
            return;
        }
        console.log(`🧠 Learning: auto-generating skill from task "${task.name}"...`);
        try {
            const saveResult = await this.skillGenerator.generate(logPath);
            console.log(`  ${saveResult}`);
            // Auto-link the new skill to this scheduled task
            if (this.scheduler && saveResult.startsWith("Skill saved:")) {
                // Extract skill name from the saved file path
                const match = saveResult.match(/\/([^/]+)\.md$/);
                if (match) {
                    const skillRef = match[1];
                    this.scheduler.linkSkill(task.id, skillRef);
                    console.log(`  Skill "${skillRef}" auto-linked to task "${task.name}"`);
                }
            }
        }
        catch (err) {
            console.error("  Auto skill generation failed:", err);
        }
    }
    /** Format an execution report as readable text */
    formatReport(report) {
        const lines = [`Skill "${report.skillName}": ${report.completed ? "completed" : "failed"}`];
        lines.push(`  ${report.successCount}/${report.totalSteps} steps ok, ${report.fallbackCount} fallback(s)`);
        for (const step of report.steps) {
            const status = step.success
                ? step.usedFallback ? "ok (fallback)" : "ok"
                : `FAILED: ${step.error}`;
            lines.push(`  Step ${step.index + 1} [${step.action}]: ${status}`);
        }
        return lines.join("\n");
    }
    /** Run an ad-hoc prompt in LLM-driven mode */
    async runPrompt(prompt, taskName = "adhoc") {
        const logsDir = path.join(this.config.workspace, "logs");
        const logger = new ExecutionLogger(logsDir, taskName);
        this.browserActions.setLogger(logger);
        const startedAt = Date.now();
        logger.log("task_start", { taskName, mode: "adhoc", prompt });
        try {
            const run = await this.runLlmDriven(prompt, logger, undefined, `${taskName}\n${prompt}`);
            const result = run.content;
            logger.log("task_end", {
                durationMs: Date.now() - startedAt,
                mode: "adhoc",
                completionStatus: run.completionStatus,
                stopReason: run.stopReason,
                resultPreview: result.slice(0, 500),
            });
            return result;
        }
        catch (err) {
            logger.log("task_end", {
                durationMs: Date.now() - startedAt,
                mode: "adhoc",
                completionStatus: "failed",
                stopReason: "error",
                error: err instanceof Error ? err.message : String(err),
            }, false);
            throw err;
        }
        finally {
            this.browserActions.clearLogger(logger);
        }
    }
    async runLlmDriven(prompt, logger, signal, skillTriggerText = prompt, storagePolicies = []) {
        throwIfAborted(signal);
        // Fresh session id per task run; pins every iteration of this task to
        // the same upstream account via the gateway's sticky-session router.
        const sessionId = randomUUID();
        // Build system prompt with memory context
        const memoryIndex = this.memory.loadIndexCapped({
            maxEntries: CONTEXT_BUDGETS.task.memoryIndexMaxEntries,
            maxBytes: CONTEXT_BUDGETS.task.memoryIndexMaxBytes,
        });
        const relevantMemory = this.memory.loadRelevantMemories(prompt.split(/\s+/).filter((w) => w.length > 3), {
            maxFiles: CONTEXT_BUDGETS.task.relevantMemoryMaxFiles,
            maxTotalBytes: CONTEXT_BUDGETS.task.relevantMemoryMaxTotalBytes,
            perFileBytes: CONTEXT_BUDGETS.task.relevantMemoryPerFileBytes,
        });
        const loadedInstructionSkills = this.skillGenerator.getAutoLoadedInstructionSkills(skillTriggerText);
        let systemPrompt = this.buildSystemPrompt(memoryIndex, relevantMemory, loadedInstructionSkills, storagePolicies);
        // Scheduled tasks run without a chat context, so attachment.send has no
        // recipient and attachment handlers are only implemented in chat mode.
        // Keep skill in task mode as read-only (list/read) so tasks can load
        // installed instruction skills without being able to modify them.
        const tools = getAllTools({
            imageGeneration: !!this.imageGenerator,
            taskMode: true,
        }).filter((t) => t.name !== "attachment");
        const loopDetector = new ToolLoopDetector(detectorOptionsForProtocol(this.config.llm.protocol));
        const storageTracker = new StoragePolicyRuntimeTracker(storagePolicies);
        let messages = [
            { role: "user", content: prompt },
        ];
        const contextTranscript = new ContextTranscript(this.config.workspace, "task");
        contextTranscript.append(messages[0], { source: "task_prompt" });
        let contextState = createEmptyContextState();
        const pushMessage = (message, meta) => {
            messages.push(message);
            contextTranscript.append(message, meta);
        };
        let iterations = 0;
        let finalContent = "";
        const maxIter = this.config.maxIterations;
        let forceStop = false;
        let completed = false;
        let contextCompactionCooldown = 0;
        const maybeCompactContext = async (purpose) => {
            if (estimateMessagesChars(messages) <= CONTEXT_BUDGETS.task.compactionCharBudget) {
                contextCompactionCooldown = 0;
                return;
            }
            if (contextCompactionCooldown > 0) {
                contextCompactionCooldown--;
                logger.log("context_compaction", {
                    iteration: iterations,
                    ...(purpose ? { purpose } : {}),
                    reason: "cooldown",
                    retainedCount: messages.length,
                }, false);
                return;
            }
            const compacted = await compactMessagesWithState({
                messages,
                state: contextState,
                llmClient: this.llmClient,
                charBudget: CONTEXT_BUDGETS.task.compactionCharBudget,
                keepRatio: CONTEXT_BUDGETS.task.keepRatio,
                preserveFirst: true,
                transcript: contextTranscript,
                sessionId,
            });
            throwIfAborted(signal);
            if (compacted.changed) {
                logger.log("context_compaction", {
                    iteration: iterations,
                    ...(purpose ? { purpose } : {}),
                    summarizedCount: compacted.summarizedCount,
                    retainedCount: compacted.retainedCount,
                    stateChars: JSON.stringify(compacted.state).length,
                });
            }
            else if (compacted.reason === "summarization_failed") {
                contextCompactionCooldown = 3;
                logger.log("context_compaction", {
                    iteration: iterations,
                    ...(purpose ? { purpose } : {}),
                    reason: compacted.reason,
                    retainedCount: compacted.retainedCount,
                }, false);
            }
            messages = compacted.messages;
            contextState = compacted.state;
        };
        while (iterations < maxIter && !forceStop) {
            throwIfAborted(signal);
            iterations++;
            // Summarize old messages when context grows large, instead of hard truncation.
            // Write back to `messages` so we don't re-summarize the same content each round.
            await maybeCompactContext();
            const llmContext = buildLlmContext({
                baseSystemPrompt: systemPrompt,
                state: contextState,
                messages,
                optimizeOptions: { preserveFirst: true },
                contextTitle: "Earlier Actions State",
                transcript: contextTranscript,
            });
            const llmStart = Date.now();
            logger.log("llm_call", {
                iteration: iterations,
                purpose: "main_loop",
                messageCount: llmContext.messages.length,
                charCount: estimateCharCount(llmContext.messages),
                toolCount: tools.length,
                systemPromptChars: llmContext.systemPrompt.length,
                contextManifest: llmContext.manifest,
            });
            const response = await this.llmClient.chat(llmContext.messages, tools, llmContext.systemPrompt, {
                systemStaticChars: llmContext.systemPromptStatic.length,
                cacheTools: tools.length > 0,
                cacheMessages: llmContext.messages.length > 0,
                sessionId,
            }, signal);
            throwIfAborted(signal);
            logger.log("llm_response", {
                iteration: iterations,
                purpose: "main_loop",
                durationMs: Date.now() - llmStart,
                stopReason: response.stopReason,
                contentLen: response.content?.length ?? 0,
                toolCallCount: response.toolCalls.length,
                toolCalls: response.toolCalls.map((tc) => ({
                    name: tc.name,
                    action: tc.arguments?.action ?? null,
                })),
                usage: response.usage,
            });
            if (response.content) {
                finalContent = response.content;
                // Notify loop detector that LLM produced text
                if (response.content.length > 100) {
                    loopDetector.notifySubstantiveResponse();
                }
            }
            // If no tool calls, we're done
            if (response.toolCalls.length === 0 || response.stopReason === "stop") {
                const storageReminder = storageTracker.completionReminder();
                if (storageReminder) {
                    pushMessage({ role: "user", content: storageReminder }, { source: "storage_policy" });
                    continue;
                }
                completed = true;
                break;
            }
            // Add assistant message with tool calls
            pushMessage({
                role: "assistant",
                content: response.content,
                toolCalls: response.toolCalls,
            }, { source: "llm" });
            // Execute each tool call, checking for loops after each one
            for (let ti = 0; ti < response.toolCalls.length; ti++) {
                throwIfAborted(signal);
                const tc = response.toolCalls[ti];
                // Check for loops before executing (skip remaining calls if critical)
                const preCheck = loopDetector.check();
                if (preCheck.status === "critical") {
                    // Return a skip message for this and all remaining tool calls
                    for (let ri = ti; ri < response.toolCalls.length; ri++) {
                        pushMessage({
                            role: "tool",
                            content: `[Skipped: loop detected — ${preCheck.pattern}]`,
                            toolCallId: response.toolCalls[ri].id,
                        }, { source: "loop_detector", toolName: response.toolCalls[ri].name });
                    }
                    console.log(`  Loop detected (${preCheck.pattern}): skipping ${response.toolCalls.length - ti} remaining tool call(s)`);
                    pushMessage({ role: "user", content: preCheck.message }, { source: "loop_detector" });
                    forceStop = true;
                    break;
                }
                const rawResult = await this.executeTool(tc, logger);
                if (tc.name === "store") {
                    storageTracker.recordStoreCall(tc.arguments.action || "", tc.arguments.collection);
                }
                throwIfAborted(signal);
                const result = capToolResultText(tc.name, rawResult, logger);
                pushMessage({
                    role: "tool",
                    content: result,
                    toolCallId: tc.id,
                }, { source: "tool", toolName: tc.name });
                const action = tc.arguments.action || tc.name;
                loopDetector.record(tc.name, action, tc.arguments, result);
            }
            // Post-batch loop check (handles warning + critical for the last call)
            if (!forceStop) {
                const loopCheck = loopDetector.check();
                if (loopCheck.status === "critical") {
                    console.log(`  Loop detected (${loopCheck.pattern}): forcing stop`);
                    pushMessage({ role: "user", content: loopCheck.message }, { source: "loop_detector" });
                    forceStop = true;
                }
                else if (loopCheck.status === "warning") {
                    console.log(`  Loop warning (${loopCheck.pattern}): injecting hint`);
                    pushMessage({ role: "user", content: loopCheck.message }, { source: "loop_detector" });
                }
            }
        }
        // Reached iteration limit or forced stop — ask LLM for a final summary
        if (!completed && (iterations >= maxIter || forceStop)) {
            throwIfAborted(signal);
            const stopReason = forceStop
                ? "A repetitive action loop was detected and stopped."
                : "You've reached the maximum number of tool-calling iterations allowed.";
            pushMessage({
                role: "user",
                content: `${stopReason} Please provide a final response summarizing what you've found and accomplished so far, without calling any more tools.`,
            }, { source: "iteration_limit" });
            await maybeCompactContext("final_summary");
            const llmContext = buildLlmContext({
                baseSystemPrompt: systemPrompt,
                state: contextState,
                messages,
                optimizeOptions: { preserveFirst: true },
                contextTitle: "Earlier Actions State",
                transcript: contextTranscript,
            });
            const llmStart = Date.now();
            logger.log("llm_call", {
                iteration: iterations,
                purpose: "final_summary",
                messageCount: llmContext.messages.length,
                charCount: estimateCharCount(llmContext.messages),
                toolCount: 0,
                systemPromptChars: llmContext.systemPrompt.length,
                forceStop,
                contextManifest: llmContext.manifest,
            });
            const summary = await this.llmClient.chat(llmContext.messages, undefined, llmContext.systemPrompt, {
                systemStaticChars: llmContext.systemPromptStatic.length,
                cacheMessages: llmContext.messages.length > 0,
                sessionId,
            }, signal);
            throwIfAborted(signal);
            logger.log("llm_response", {
                iteration: iterations,
                purpose: "final_summary",
                durationMs: Date.now() - llmStart,
                stopReason: summary.stopReason,
                contentLen: summary.content?.length ?? 0,
                toolCallCount: 0,
                usage: summary.usage,
            });
            finalContent = summary.content;
        }
        const stopReason = completed ? "completed" : forceStop ? "loop_detected" : "max_iterations";
        return {
            content: finalContent,
            completionStatus: stopReason === "completed" ? "complete" : "partial",
            stopReason,
        };
    }
    async executeTool(tc, logger) {
        const args = tc.arguments;
        const action = args.action;
        // Browser tools log themselves inside BrowserActions; the shared runner
        // adds the agent-level tool_call entry (ok bit + duration).
        switch (tc.name) {
            case "browser": {
                const r = await runBrowserTool(this.browserActions, args, logger);
                return r.text;
            }
            case "memory": {
                const start = Date.now();
                const result = this.executeMemoryAction(action, args);
                logger?.log("memory", {
                    action,
                    key: args.key,
                    durationMs: Date.now() - start,
                    resultLen: result.length,
                });
                return result;
            }
            case "store": {
                const start = Date.now();
                const result = executeStoreAction(this.store, action, args);
                logger?.log("store", {
                    action,
                    collection: args.collection,
                    id: args.id,
                    durationMs: Date.now() - start,
                    resultLen: result.length,
                });
                return result;
            }
            case "skill": {
                const start = Date.now();
                const result = executeReadOnlySkillAction(this.skillGenerator, action, args);
                logger?.log("skill", {
                    action,
                    name: args.name,
                    durationMs: Date.now() - start,
                    resultLen: result.length,
                    readOnly: true,
                }, !result.startsWith("Error:"));
                return result;
            }
            case "web_fetch": {
                const start = Date.now();
                const result = await executeWebFetchAction(args);
                logger?.log("web_fetch", {
                    url: args.url,
                    durationMs: Date.now() - start,
                    resultLen: result.length,
                    success: !result.startsWith("Error:"),
                }, !result.startsWith("Error:"));
                return result;
            }
            case "scheduler":
                logger?.log("scheduler", { action, note: "managed via commands" });
                return `(scheduler managed via commands)`;
            case "image_generation": {
                if (!this.imageGenerator) {
                    return "Error: image_generation is not configured (set config.imageGeneration in your config.json).";
                }
                if (action !== "generate" && action !== "edit") {
                    return `Unknown image_generation action: ${action}`;
                }
                const start = Date.now();
                const result = action === "generate"
                    ? await this.imageGenerator.generate(args, null)
                    : await this.imageGenerator.edit(args, null);
                logger?.log("image_generation", {
                    action,
                    durationMs: Date.now() - start,
                    resultLen: result.length,
                });
                return result;
            }
            default:
                logger?.log("tool_unknown", { name: tc.name }, false);
                return `Unknown tool: ${tc.name}`;
        }
    }
    executeMemoryAction(action, args) {
        switch (action) {
            case "save":
                return (`memory.save is disabled during scheduled tasks. ` +
                    `For structured cross-run state (processed ids, visited URLs, ` +
                    `tickets, observations, counters, cursors) use the store tool. ` +
                    `For freeform notes, wait for the user to request a save via chat.`);
            case "read":
                return this.memory.read(args.key);
            case "list":
                return this.memory.list();
            default:
                return `Unknown memory action: ${action}`;
        }
    }
    loadInstalledSkillsIndex() {
        try {
            const skills = this.skillGenerator.listSkills();
            return truncateContextSection(skills, CONTEXT_BUDGETS.task.installedSkillsMaxChars, "installed skills list truncated");
        }
        catch (err) {
            const msg = err instanceof Error ? err.message : String(err);
            return `(could not load installed skills: ${msg})`;
        }
    }
    buildSystemPrompt(memoryIndex, relevantMemory, loadedInstructionSkills, storagePolicies = []) {
        const installedSkills = this.loadInstalledSkillsIndex();
        const autoLoadedSkills = this.skillGenerator.renderLoadedInstructionSkillSection(loadedInstructionSkills);
        let prompt = `You are a browser automation assistant named "${this.config.name}". You help users accomplish tasks by controlling a web browser.

## Available Tools
- Use web_fetch for read-only HTTP(S) JSON/text/markdown endpoints and public REST APIs.
  It is the safe replacement for instruction-skill examples that mention WebFetch or curl.
- Use skill.list / skill.read for read-only access to installed skills during
  scheduled tasks. Skills are procedures or replay scripts stored in
  workspace/skills/ plus packaged built-ins. Scheduled tasks cannot create,
  overwrite, install, or delete skills.

## Installed Skills Routing
The installed skills list below is authoritative routing context. Before
guessing a website, endpoint, or source, check whether an installed skill
already matches the task. If it matches, read that skill with skill.read and
follow its instructions.

Installed skills:
${installedSkills}

You have a browser tool with two levels of actions:
- **Top-level actions**: navigate, snapshot, screenshot, tabs, open, focus, close, console, dialog, upload
- **Interactions via act**: action="act" with kind="click", "type", "press", "hover", "drag", "select", "fill", "resize", "wait", "evaluate" (disabled by default), "close"

## How to use browser tool
1. Use browser action "navigate" to go to a URL
2. Use browser action "snapshot" to see the page; full/interactive snapshots give element refs like [e1], [e2], while outline snapshots give selector hints
3. Use browser action "act" with kind "click", "type", etc. using element refs or CSS selectors
4. After interactions, snapshot again to verify results

Example: to click element e5, use: { action: "act", kind: "click", ref: "e5" }
Example: to type text, use: { action: "act", kind: "type", ref: "e3", text: "hello" }

## Snapshot
- full/interactive snapshots capture the **full page accessibility tree regardless of scroll position**.
- For unknown or large pages, start with snapshot mode "outline" maxChars 6000. Treat it as a page index.
- For action discovery on large pages, use snapshot mode "efficient"; it is interactive + compact + depth-limited and capped at 8000 chars.
- For content, use selector-scoped mode "full" maxChars 10000 after choosing a selector from outline/efficient, or use extractVisibleText/extractLinks/extractTweetCards when applicable.
- Do not request maxChars above 16000. The worker clamps it, and wider snapshots waste context. Narrow the selector instead.
- Avoid selector "main", "body", "html", or no selector with mode "full" on large SPAs; use outline or efficient first.
- Use compact, depth, maxChars for further control.

## Efficiency Guidelines
- Minimize tool calls. Plan multiple steps ahead before acting.
- Do NOT repeat the same action if it produced no new results.
- When collecting data, decide what "enough" means before starting. 10 quality records > 100 duplicates.
- If you receive a loop warning, immediately change approach: scroll farther, refresh the snapshot, switch selectors, skip stale candidates, or use a different extraction path. A loop warning is not permission to finish an open-ended goal; for "find X", "keep scrolling until Y", or "until found" tasks, continue until the target is found, the source is genuinely exhausted, or a real blocker appears.
- **Random delays between actions are injected automatically. Do NOT add artificial delays.**
- **click, hover, and type auto-scroll the element into view.** No need to scroll first.
- For infinite-scroll pages, use act kind "scroll" with wheel input to load more content, then snapshot to check.

## Hover
- Use act kind "hover" with "ref" to trigger tooltips, popups, or hover cards.
- Do NOT use evaluate to dispatch mouse/click events. Use act kind "hover" or "click". DOM dispatch fallbacks are disabled by default; recover with a fresh snapshot, a better selector, scroll, or visible UI controls.

## New Actions
- **console**: Read browser console messages. Use level="error" to filter. Useful for debugging.
- **dialog**: Arm a handler for the next alert/confirm/prompt dialog. Call BEFORE the action that triggers it.
- **upload**: Upload files. With ref: sets files on input element. Without ref: arms next file chooser.
- **act kind "drag"**: Drag from startRef to endRef.
- **act kind "fill"**: Batch fill form fields. Each field: {ref, value, type?}. Use type "checkbox"/"radio" for toggles.
- **act kind "resize"**: Resize viewport (width, height).

## Evaluate (JavaScript) Guidelines
- Evaluate is disabled by default. Prefer high-level helpers such as
  extractVisibleText, queryByText, findClickableNearText, extractLinks, and
  extractTweetCards.
- NEVER use evaluate for setTimeout, delays, sleep, waiting, DOM-state writes,
  or mouse/click interactions.

## Verifying actions actually worked
A successful click/type/submit is NOT proof the user-visible task happened —
many sites accept the click without sending the post, or accept text without
inserting it into the editor. After any submit/post/send/search/upload, you
MUST verify the resulting page state, ideally by passing an \`expect\` block
on the same act call. Examples:

- After clicking "Post"/"Send"/"Publish": expect a durable state change such
  as urlMatches, selectorGone for the compose/dialog, a visible success
  toast/new item, or inputValue.equals: "" on the submitted editor when a
  cleared editor is the reliable success signal.
- After search: expect urlContains/urlMatches plus a result-list selector or result text.
- After upload: expect an attachment/file-list selector or text containing the filename.

If you forget expect, take a fresh snapshot or call action="verify" before
declaring the task done. Scheduled tasks should fail loudly when verification
fails — do not return success.

${autoLoadedSkills}

## Memory
Use memory.read / memory.list to recall freeform user-saved notes (site
quirks, login flows, authoritative rules the user has dictated). You CANNOT
save memories during scheduled tasks — memory.save will be rejected. memory
is for human-curated freeform content only.

## Store
For structured cross-run state — processed ids, visited URLs, tickets,
observations, counters, cursors, compact histories — use the store tool.
store.has(collection, id) is an O(1) existence check; store.get reads one
record; store.put(collection, id, value?, meta?) records or updates one
entry. Never use memory.read + grep for structured lookups — it bloats every
transcript iteration and makes state handling less reliable.`;
        const storagePolicySection = renderStoragePolicySection(storagePolicies);
        if (storagePolicySection) {
            prompt += `\n\n${storagePolicySection}`;
        }
        if (memoryIndex.trim().length > 20) {
            prompt += `\n\n## Saved Memories\n${memoryIndex}`;
        }
        if (relevantMemory.trim()) {
            prompt += `\n\n## Relevant Memory Details\n${relevantMemory}`;
        }
        return prompt;
    }
}
function throwIfAborted(signal) {
    if (!signal?.aborted)
        return;
    const reason = signal.reason;
    if (reason instanceof Error)
        throw reason;
    throw new Error(reason ? String(reason) : "Task aborted");
}
//# sourceMappingURL=task-runner.js.map