import { isProcessorWorkflow } from '../chunk-53NCBZKI.js';
export { BrowserCliHandler, browserCliHandler } from '../chunk-F4PWGVHW.js';
import { MastraBase } from '../chunk-WENZPAHS.js';
import { RegisteredLogger } from '../chunk-DBBWTK24.js';
import { existsSync, readdirSync, lstatSync, unlinkSync } from 'fs';
import { join } from 'path';
import { EventEmitter } from 'events';

// src/browser/errors.ts
var RETRYABLE_CODES = /* @__PURE__ */ new Set(["timeout", "element_blocked"]);
function createError(code, message, hint) {
  return {
    success: false,
    code,
    message,
    recoveryHint: hint,
    canRetry: RETRYABLE_CODES.has(code)
  };
}

// src/browser/processor.ts
var REMINDER_TYPE = "browser-context";
var BrowserContextProcessor = class {
  id = "browser-context";
  processInput(args) {
    const ctx = args.requestContext?.get("browser");
    if (!ctx) return args.messageList;
    const lines = [`You have access to a browser (${ctx.provider}).`];
    if (ctx.headless === false) {
      lines.push("The browser is running in visible mode (not headless).");
    }
    if (ctx.sessionId) {
      lines.push(`Session ID: ${ctx.sessionId}`);
    }
    if (ctx.providerType === "cli" && ctx.cdpUrl) {
      lines.push(`CDP WebSocket URL: ${ctx.cdpUrl}`);
    }
    const systemMessages = [...args.systemMessages, { role: "system", content: lines.join(" ") }];
    return { messages: args.messages, systemMessages };
  }
  processInputStep(args) {
    if (args.stepNumber !== 0) return;
    const ctx = args.requestContext?.get("browser");
    if (!ctx) return;
    const parts = [];
    if (ctx.currentUrl) {
      parts.push(`Current URL: ${escapeXml(ctx.currentUrl)}`);
    }
    if (ctx.pageTitle) {
      parts.push(`Page title: ${escapeXml(ctx.pageTitle)}`);
    }
    if (parts.length === 0) return;
    const reminderText = parts.join(" | ");
    const reminderMarkup = `<system-reminder type="${REMINDER_TYPE}">${reminderText}</system-reminder>`;
    const existingMessages = args.messageList.get.all.db();
    if (hasTrailingBrowserReminder(existingMessages, ctx.currentUrl, ctx.pageTitle)) {
      return;
    }
    const reminderMessage = createBrowserReminderMessage(reminderMarkup, ctx.currentUrl, ctx.pageTitle);
    args.messageList.add(reminderMessage, "user");
    args.rotateResponseMessageId?.();
    return args.messageList;
  }
};
function createBrowserReminderMessage(reminderMarkup, url, title) {
  const reminderMeta = {
    type: REMINDER_TYPE,
    url,
    title
  };
  const content = {
    format: 2,
    parts: [{ type: "text", text: reminderMarkup }],
    metadata: {
      systemReminder: reminderMeta
    }
  };
  return {
    id: crypto.randomUUID(),
    role: "user",
    content,
    createdAt: /* @__PURE__ */ new Date()
  };
}
function escapeXml(value) {
  return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
}
function hasTrailingBrowserReminder(messages, url, title) {
  const msg = messages[messages.length - 1];
  if (!msg || msg.role !== "user") return false;
  const metadata = msg.content.metadata;
  if (typeof metadata !== "object" || metadata === null || !("systemReminder" in metadata)) {
    return false;
  }
  const reminder = metadata.systemReminder;
  return reminder?.type === REMINDER_TYPE && reminder.url === url && reminder.title === title;
}

// src/browser/thread-manager.ts
var DEFAULT_THREAD_ID = "__default__";
var ThreadManager = class {
  scope;
  logger;
  sessions = /* @__PURE__ */ new Map();
  activeThreadId = DEFAULT_THREAD_ID;
  /** Preserved browser state that survives session clears (for browser restore) */
  savedBrowserStates = /* @__PURE__ */ new Map();
  /** Shared manager instance (used for 'shared' scope) */
  sharedManager = null;
  /** Map of thread ID to dedicated manager instance (for 'thread' scope) */
  threadManagers = /* @__PURE__ */ new Map();
  onSessionCreated;
  onSessionDestroyed;
  constructor(config) {
    this.scope = config.scope;
    this.logger = config.logger;
    this.onSessionCreated = config.onSessionCreated;
    this.onSessionDestroyed = config.onSessionDestroyed;
  }
  /**
   * Get the current browser scope mode.
   */
  getScope() {
    return this.scope;
  }
  /**
   * Get the currently active thread ID.
   */
  getActiveThreadId() {
    return this.activeThreadId;
  }
  /**
   * Set the shared manager instance (called after browser launch).
   */
  setSharedManager(manager) {
    this.sharedManager = manager;
  }
  /**
   * Clear the shared manager instance (called when browser disconnects).
   */
  clearSharedManager() {
    this.sharedManager = null;
  }
  /**
   * Get the manager for an existing thread session without creating a new one.
   *
   * For 'thread' scope: Returns the thread-specific manager, or null if no session exists.
   * For 'shared' scope: Returns the shared manager (all threads use the same instance).
   *
   * @param threadId - Thread identifier (defaults to DEFAULT_THREAD_ID)
   * @returns The manager for the thread, or null if not found (thread scope only)
   */
  getExistingManagerForThread(threadId) {
    const effectiveThreadId = threadId ?? DEFAULT_THREAD_ID;
    if (this.scope === "thread") {
      return this.threadManagers.get(effectiveThreadId) ?? null;
    }
    return this.sharedManager;
  }
  /**
   * Check if any thread managers are still running (for 'thread' scope).
   */
  hasActiveThreadManagers() {
    return this.threadManagers.size > 0;
  }
  /**
   * Clear all session tracking without closing managers.
   * Used when browsers have been externally closed and we just need to reset state.
   */
  clearAllSessions() {
    this.threadManagers.clear();
    this.sessions.clear();
    this.activeThreadId = DEFAULT_THREAD_ID;
  }
  /**
   * Get a session by thread ID.
   */
  getSession(threadId) {
    return this.sessions.get(threadId);
  }
  /**
   * Check if a session exists for a thread.
   */
  hasSession(threadId) {
    return this.sessions.has(threadId);
  }
  /**
   * List all active sessions.
   */
  listSessions() {
    return Array.from(this.sessions.values());
  }
  /**
   * Get the number of active sessions.
   */
  getSessionCount() {
    return this.sessions.size;
  }
  /**
   * Get or create a session for a thread, and return the browser manager for that thread.
   *
   * For 'shared' scope, returns the shared manager.
   * For 'thread' scope, creates/returns a dedicated manager for the thread.
   *
   * @param threadId - Thread identifier (uses DEFAULT_THREAD_ID if not provided)
   * @returns The browser manager for the thread
   */
  async getManagerForThread(threadId) {
    const effectiveThreadId = threadId ?? DEFAULT_THREAD_ID;
    if (this.scope === "shared") {
      return this.getSharedManager();
    }
    let session = this.sessions.get(effectiveThreadId);
    if (!session) {
      session = await this.createSession(effectiveThreadId);
      this.sessions.set(effectiveThreadId, session);
      this.logger?.debug?.(`Created thread session: ${effectiveThreadId}`);
      this.onSessionCreated?.(session);
    }
    this.activeThreadId = effectiveThreadId;
    return this.getManagerForSession(session);
  }
  /**
   * Destroy a specific thread's session.
   *
   * @param threadId - Thread identifier
   */
  async destroySession(threadId) {
    const session = this.sessions.get(threadId);
    if (!session) {
      return;
    }
    await this.doDestroySession(session);
    this.threadManagers.delete(threadId);
    this.sessions.delete(threadId);
    this.logger?.debug?.(`Destroyed thread session: ${threadId}`);
    this.onSessionDestroyed?.(threadId);
    if (this.activeThreadId === threadId) {
      this.activeThreadId = DEFAULT_THREAD_ID;
    }
  }
  /**
   * Destroy all thread sessions.
   */
  async destroyAllSessions() {
    const threadIds = Array.from(this.sessions.keys());
    for (const threadId of threadIds) {
      await this.destroySession(threadId);
    }
    this.activeThreadId = DEFAULT_THREAD_ID;
  }
  /**
   * Update the browser state for a thread session.
   * Also saves to persistent storage so state survives session clears.
   */
  updateBrowserState(threadId, state) {
    const filteredTabs = state.tabs.filter((tab) => tab.url && tab.url !== "about:blank");
    if (filteredTabs.length === 0) {
      return;
    }
    const filteredState = {
      tabs: filteredTabs,
      activeTabIndex: Math.max(0, Math.min(state.activeTabIndex, filteredTabs.length - 1))
    };
    const session = this.sessions.get(threadId);
    if (session) {
      session.browserState = filteredState;
    }
    this.savedBrowserStates.set(threadId, filteredState);
  }
  /**
   * Get the saved browser state for a thread (survives session clears).
   */
  getSavedBrowserState(threadId) {
    const session = this.sessions.get(threadId);
    if (session?.browserState) {
      return session.browserState;
    }
    return this.savedBrowserStates.get(threadId);
  }
  /**
   * Clear a specific thread's session without closing the browser.
   * Used when a thread's browser has been externally closed.
   * Preserves the browser state for potential restoration.
   *
   * @param threadId - The thread ID to clear
   */
  clearSession(threadId) {
    const session = this.sessions.get(threadId);
    if (session?.browserState) {
      this.savedBrowserStates.set(threadId, session.browserState);
    }
    this.threadManagers.delete(threadId);
    this.sessions.delete(threadId);
    if (this.activeThreadId === threadId) {
      this.activeThreadId = DEFAULT_THREAD_ID;
    }
  }
  // ---------------------------------------------------------------------------
  // Abstract methods to be implemented by subclasses
  // ---------------------------------------------------------------------------
  /**
   * Get the shared browser manager (used for 'shared' scope and default thread).
   * @throws Error if shared manager is not initialized
   */
  getSharedManager() {
    if (!this.sharedManager) {
      throw new Error("Browser not launched");
    }
    return this.sharedManager;
  }
};

// src/browser/browser.ts
var CHROME_LOCK_FILES = ["SingletonLock", "SingletonSocket", "SingletonCookie", "chrome.pid", "RunningChromeVersion"];
function cleanupProfileLockFiles(profilePath, logger) {
  if (!profilePath || !existsSync(profilePath)) {
    return;
  }
  try {
    const entries = readdirSync(profilePath);
    for (const entry of entries) {
      if (CHROME_LOCK_FILES.includes(entry)) {
        const fullPath = join(profilePath, entry);
        try {
          const stat = lstatSync(fullPath);
          if (stat.isFile() || stat.isSymbolicLink()) {
            unlinkSync(fullPath);
            logger?.debug?.(`Removed stale lock file: ${fullPath}`);
          }
        } catch (err) {
          logger?.warn?.(`Failed to remove lock file ${fullPath}: ${err}`);
        }
      }
    }
  } catch (err) {
    logger?.warn?.(`Failed to clean up profile lock files in ${profilePath}: ${err}`);
  }
}
function killProcessGroup(pid, logger) {
  if (pid == null) return;
  try {
    process.kill(-pid, "SIGKILL");
    logger?.debug?.(`Killed process group for PID ${pid}`);
  } catch (err) {
    const code = err.code;
    if (code !== "ESRCH") {
      logger?.warn?.(`Failed to kill process group ${pid}: ${code ?? err}`);
    }
  }
}
var MastraBrowser = class _MastraBrowser extends MastraBase {
  /**
   * Provider type for runtime enforcement.
   * - 'sdk': SDK providers (AgentBrowser, StagehandBrowser) — use with Agent.browser
   * - 'cli': CLI providers (BrowserViewer) — use with Workspace.browser
   * Defaults to 'sdk' for backward compatibility with existing providers.
   */
  providerType = "sdk";
  // ---------------------------------------------------------------------------
  // State
  // ---------------------------------------------------------------------------
  /** Current lifecycle status */
  status = "pending";
  /** Error message when status is 'error' */
  error;
  /**
   * Whether the browser is running in headless mode.
   * Returns true by default if not explicitly configured.
   */
  get headless() {
    return this.config.headless ?? true;
  }
  /** Last known browser state before browser was closed (for restore on relaunch) */
  lastBrowserState;
  /**
   * Shared manager instance for 'shared' scope mode.
   * Type varies by provider (e.g., BrowserManager for agent-browser, Stagehand for stagehand).
   * Providers should cast this to their specific type when accessing.
   */
  sharedManager = null;
  /** Configuration */
  config;
  /**
   * Thread manager for handling thread-scoped browser sessions.
   * Set by subclasses that support thread isolation.
   */
  threadManager;
  /**
   * Current thread ID for browser operations.
   * Used by thread isolation to route operations to the correct session.
   */
  currentThreadId = DEFAULT_THREAD_ID;
  // ---------------------------------------------------------------------------
  // Screencast State
  // ---------------------------------------------------------------------------
  /** Default key for shared scope screencast streams */
  static SHARED_STREAM_KEY = "__shared__";
  /** Active screencast streams per thread (for triggering reconnects on tab changes) */
  activeScreencastStreams = /* @__PURE__ */ new Map();
  // ---------------------------------------------------------------------------
  // Process ID Tracking (for orphaned process cleanup)
  // ---------------------------------------------------------------------------
  /**
   * PID of the shared browser process.
   * Set by providers after launch so the base class can kill the process group
   * (GPU, renderer, crashpad, etc.) when the browser disconnects or closes.
   */
  sharedBrowserPid;
  /**
   * PIDs of per-thread browser processes.
   * Set by providers after creating a thread session.
   */
  threadBrowserPids = /* @__PURE__ */ new Map();
  /**
   * Get the stream key for a thread (or shared key for shared scope).
   * @param threadId - Optional thread ID
   * @returns The stream key to use for the screencast streams map
   */
  getStreamKey(threadId) {
    return threadId || _MastraBrowser.SHARED_STREAM_KEY;
  }
  /**
   * Reconnect the active screencast for a specific thread.
   * Called internally when tabs are switched or closed.
   */
  async reconnectScreencastForThread(threadId, reason) {
    const streamKey = this.getStreamKey(threadId);
    const stream = this.activeScreencastStreams.get(streamKey);
    if (!stream || !stream.isActive()) {
      return;
    }
    if (!this.isBrowserRunning()) {
      this.logger.debug?.("Skipping screencast reconnect - browser not running");
      return;
    }
    const scope = this.getScope();
    if (scope === "thread" && threadId && !this.threadManager?.getExistingManagerForThread(threadId)) {
      this.logger.debug?.(`Skipping screencast reconnect - no session for thread ${threadId}`);
      return;
    }
    this.logger.debug?.(`Reconnecting screencast: ${reason}`);
    try {
      await new Promise((resolve) => setTimeout(resolve, 150));
      await stream.reconnect();
      const activePage = await this.getActivePage(threadId);
      if (activePage) {
        const url = activePage.url();
        if (url) {
          stream.emitUrl(url);
        }
      }
    } catch (error) {
      this.logger.debug?.("Screencast reconnect failed", error);
    }
  }
  /**
   * Update the browser state in the thread session.
   * Called on navigation, tab open/close to keep state fresh.
   */
  updateSessionBrowserState(threadId) {
    try {
      const effectiveThreadId = threadId ?? this.getCurrentThread() ?? DEFAULT_THREAD_ID;
      const state = this.getBrowserStateForThread(effectiveThreadId);
      if (state) {
        this.threadManager?.updateBrowserState(effectiveThreadId, state);
      }
    } catch {
    }
  }
  // ---------------------------------------------------------------------------
  // Lifecycle Promise Tracking (prevents race conditions)
  // ---------------------------------------------------------------------------
  _launchPromise;
  _closePromise;
  // ---------------------------------------------------------------------------
  // Constructor
  // ---------------------------------------------------------------------------
  constructor(config = {}) {
    super({ name: "MastraBrowser", component: RegisteredLogger.BROWSER });
    this.config = config;
    const scope = config.scope;
    if (config.cdpUrl && scope === "thread") {
      throw new Error(
        `Invalid browser configuration: "cdpUrl" and "scope: 'thread'" cannot be used together.

\u2022 cdpUrl connects to a single existing browser instance (all threads share it)
\u2022 scope: "thread" requires spawning separate browser instances per thread

To fix this, either:
1. Remove cdpUrl to let the provider spawn separate browser instances (supports thread isolation)
2. Use scope: "shared" when connecting via cdpUrl (all threads share one browser)`
      );
    }
    if (config.cdpUrl && (config.profile || config.executablePath)) {
      const conflicting = [config.profile && "profile", config.executablePath && "executablePath"].filter(Boolean).join(" and ");
      throw new Error(
        `Invalid browser configuration: "cdpUrl" cannot be used with ${conflicting}.

\u2022 cdpUrl connects to an existing browser (which has its own profile and executable)
\u2022 profile and executablePath are launch-time options for spawning a new browser

To fix this, either:
1. Remove cdpUrl to launch a new browser with your profile/executable
2. Remove profile/executablePath to connect to the existing browser via CDP`
      );
    }
  }
  /**
   * Get the CDP WebSocket URL for connecting to this browser.
   * CLI providers (BrowserViewer) implement this to expose the URL for CLI tools.
   * SDK providers typically return null as they manage their own CDP connections.
   *
   * @param _threadId - Thread identifier (for thread-scoped browsers)
   * @returns The CDP WebSocket URL (e.g., ws://127.0.0.1:9222/devtools/browser/...)
   */
  getCdpUrl(_threadId) {
    return null;
  }
  /**
   * Launch the browser.
   * Race-condition-safe - handles concurrent calls, status management, and lifecycle hooks.
   * @param _threadId - Thread identifier (for thread-scoped browsers, launches a browser for that thread)
   */
  async launch(threadId) {
    if (threadId !== void 0) {
      this.setCurrentThread(threadId);
    }
    if (this.status === "ready") {
      return;
    }
    if (this.status === "launching" && this._launchPromise) {
      return this._launchPromise;
    }
    if (this.status === "closing" || this.status === "closed") {
      throw new Error(`Cannot launch browser in '${this.status}' state`);
    }
    this.status = "launching";
    this.error = void 0;
    this._launchPromise = (async () => {
      try {
        await this.doLaunch();
        this.status = "ready";
        if (this.config.onLaunch) {
          await this.config.onLaunch({ browser: this });
        }
        this.notifyBrowserReady();
      } catch (err) {
        this.status = "error";
        this.error = err instanceof Error ? err.message : String(err);
        throw err;
      } finally {
        this._launchPromise = void 0;
      }
    })();
    return this._launchPromise;
  }
  /**
   * Close the browser.
   * Race-condition-safe - handles concurrent calls, status management, and lifecycle hooks.
   */
  async close() {
    if (this.status === "closed") {
      return;
    }
    if (this.status === "closing" && this._closePromise) {
      return this._closePromise;
    }
    if (this.status === "launching" && this._launchPromise) {
      try {
        await this._launchPromise;
      } catch {
        this.status = "closed";
        return;
      }
    }
    if (this.config.onClose && this.status === "ready") {
      await this.config.onClose({ browser: this });
    }
    const currentState = await this.getBrowserState();
    if (currentState && currentState.tabs.length > 0) {
      this.lastBrowserState = currentState;
    }
    this.status = "closing";
    this._closePromise = (async () => {
      try {
        await this.doClose();
        this.status = "closed";
        this.notifyBrowserClosed();
        if (this.config.profile) {
          cleanupProfileLockFiles(this.config.profile, this.logger);
        }
      } catch (err) {
        this.status = "error";
        this.error = err instanceof Error ? err.message : String(err);
        throw err;
      } finally {
        this._closePromise = void 0;
        killProcessGroup(this.sharedBrowserPid, this.logger);
        this.sharedBrowserPid = void 0;
        for (const [, pid] of this.threadBrowserPids) {
          killProcessGroup(pid, this.logger);
        }
        this.threadBrowserPids.clear();
      }
    })();
    return this._closePromise;
  }
  /**
   * Connect to an external browser via CDP URL for screencast.
   *
   * Use this when an agent is using their own external CDP (e.g., browser-use cloud).
   * Connects Playwright to the external browser to enable screencast without launching
   * our own browser.
   *
   * Override this in subclasses that support external CDP connections.
   * The base implementation throws an error.
   *
   * @param cdpUrl - The external CDP WebSocket URL (wss://... or ws://...)
   * @param threadId - Thread ID to associate the session with
   */
  async connectToExternalCdp(_cdpUrl, _threadId) {
    throw new Error(`${this.provider} does not support connecting to external CDP`);
  }
  /**
   * Ensure the browser is ready, launching if needed.
   * If browser was previously closed, it will be re-launched.
   */
  async ensureReady() {
    if (this.status === "ready") {
      const stillAlive = await this.checkBrowserAlive();
      if (stillAlive) {
        return;
      }
      this.status = "closed";
    }
    if (this.status === "pending" || this.status === "error" || this.status === "closed") {
      if (this.status === "closed") {
        this.status = "pending";
      }
      await this.launch();
      return;
    }
    if (this.status === "launching") {
      await this._launchPromise;
      return;
    }
    if (this.status === "closing") {
      await this._closePromise;
      this.status = "pending";
      await this.launch();
      return;
    }
    throw new Error(`Browser is ${this.status} and cannot be used`);
  }
  /**
   * Check if the browser is still alive.
   * Override in subclass to detect externally closed browsers.
   * @returns true if browser is alive, false if it was externally closed
   */
  async checkBrowserAlive() {
    return true;
  }
  /**
   * Check if the browser is currently running.
   * @param _threadId - Thread identifier (for thread-scoped browsers)
   */
  isBrowserRunning(_threadId) {
    return this.status === "ready";
  }
  // ---------------------------------------------------------------------------
  // CDP URL Resolution
  // ---------------------------------------------------------------------------
  /**
   * Resolve a CDP URL from a static string or async provider function.
   * @param cdpUrl - Static string or async function returning the CDP URL
   * @returns Resolved CDP URL string
   */
  async resolveCdpUrl(cdpUrl) {
    return typeof cdpUrl === "function" ? await cdpUrl() : cdpUrl;
  }
  /**
   * Resolve an HTTP CDP endpoint to a WebSocket URL by fetching /json/version.
   *
   * Cloud browser providers (Browser-Use, Browserless, etc.) often expose HTTP
   * endpoints that need to be resolved to WebSocket URLs for direct CDP connections.
   *
   * - If the URL starts with `ws://` or `wss://`, returns it as-is
   * - If the URL starts with `http://` or `https://`, fetches /json/version to get webSocketDebuggerUrl
   *
   * @param url - CDP URL (HTTP or WebSocket)
   * @returns WebSocket URL for CDP connection
   */
  async resolveWebSocketUrl(url) {
    if (url.startsWith("ws://") || url.startsWith("wss://")) {
      return url;
    }
    if (url.startsWith("http://") || url.startsWith("https://")) {
      const baseUrl = url.replace(/\/$/, "");
      const versionUrl = `${baseUrl}/json/version`;
      this.logger.debug?.(`Resolving WebSocket URL from ${versionUrl}`);
      const controller = new AbortController();
      const timeoutId = setTimeout(() => controller.abort(), 1e4);
      try {
        const response = await fetch(versionUrl, { signal: controller.signal });
        clearTimeout(timeoutId);
        if (!response.ok) {
          throw new Error(
            `Failed to fetch CDP version info from ${versionUrl}: ${response.status} ${response.statusText}`
          );
        }
        const data = await response.json();
        if (!data.webSocketDebuggerUrl) {
          throw new Error(`No webSocketDebuggerUrl found in CDP version response from ${versionUrl}`);
        }
        this.logger.debug?.(`Resolved WebSocket URL: ${data.webSocketDebuggerUrl}`);
        return data.webSocketDebuggerUrl;
      } catch (error) {
        clearTimeout(timeoutId);
        if (error instanceof Error && error.name === "AbortError") {
          throw new Error(`Timeout resolving WebSocket URL from ${versionUrl} (10s)`);
        }
        throw error;
      }
    }
    return url;
  }
  // ---------------------------------------------------------------------------
  // Disconnection Detection & Error Handling
  // ---------------------------------------------------------------------------
  /**
   * Error patterns that indicate browser disconnection.
   * Used by isDisconnectionError() to detect external browser closure.
   */
  static DISCONNECTION_PATTERNS = [
    "Target closed",
    "Target page, context or browser has been closed",
    "Browser has been closed",
    "Connection closed",
    "Protocol error",
    "Session closed",
    "browser has disconnected",
    "closed externally"
  ];
  /**
   * Check if an error message indicates browser disconnection.
   * @param message - Error message to check
   * @returns true if the message indicates disconnection
   */
  isDisconnectionError(message) {
    const lowerMessage = message.toLowerCase();
    return _MastraBrowser.DISCONNECTION_PATTERNS.some((pattern) => lowerMessage.includes(pattern.toLowerCase()));
  }
  /**
   * Handle browser disconnection by updating status and notifying listeners.
   * Called when browser is detected as externally closed.
   *
   * For 'thread' scope: clears only the specific thread's session (other threads unaffected)
   * For 'shared' scope: clears the shared manager and updates global status
   */
  handleBrowserDisconnected() {
    const scope = this.threadManager?.getScope();
    const threadId = this.getCurrentThread();
    if (scope === "thread" && threadId !== DEFAULT_THREAD_ID) {
      const pid = this.threadBrowserPids.get(threadId);
      killProcessGroup(pid, this.logger);
      this.threadBrowserPids.delete(threadId);
      this.threadManager.clearSession(threadId);
      this.logger.debug?.(`Cleared browser session for thread: ${threadId}`);
      this.notifyBrowserClosed(threadId);
    } else {
      killProcessGroup(this.sharedBrowserPid, this.logger);
      this.sharedBrowserPid = void 0;
      this.sharedManager = null;
      this.threadManager?.clearSharedManager();
      if (this.status !== "closed") {
        this.status = "closed";
        this.logger.debug?.("Browser was externally closed, status set to closed");
        this.notifyBrowserClosed();
      }
    }
    if (this.config.profile) {
      cleanupProfileLockFiles(this.config.profile, this.logger);
    }
  }
  /**
   * Create a BrowserToolError from an exception.
   * Handles common error patterns including disconnection detection.
   * Subclasses can override to add provider-specific error handling.
   *
   * @param error - The caught error
   * @param context - Description of what operation failed (e.g., "Click operation")
   * @returns Structured BrowserToolError
   */
  createErrorFromException(error, context) {
    const msg = error instanceof Error ? error.message : String(error);
    if (this.isDisconnectionError(msg)) {
      this.handleBrowserDisconnected();
      return createError(
        "browser_closed",
        "Browser was closed externally.",
        "The browser window was closed. Please retry to re-launch."
      );
    }
    if (msg.includes("timeout") || msg.includes("Timeout") || msg.includes("aborted")) {
      return createError("timeout", `${context} timed out.`, "Try again or increase timeout.");
    }
    if (msg.includes("not launched") || msg.includes("Browser is not launched")) {
      return createError(
        "browser_error",
        "Browser was not initialized.",
        "This is an internal error - please try again."
      );
    }
    return createError("browser_error", `${context} failed: ${msg}`, "Check the browser state and try again.");
  }
  /**
   * Create a specific error type.
   * Convenience method for providers to create typed errors.
   */
  createError(code, message, hint) {
    return createError(code, message, hint);
  }
  // ---------------------------------------------------------------------------
  // Browser Ready/Closed Callbacks
  // ---------------------------------------------------------------------------
  _onReadyCallbacks = /* @__PURE__ */ new Set();
  _onClosedCallbacks = /* @__PURE__ */ new Set();
  /** Thread-specific ready callbacks. Key is threadId. */
  _onThreadReadyCallbacks = /* @__PURE__ */ new Map();
  /** Thread-specific closed callbacks. Key is threadId. */
  _onThreadClosedCallbacks = /* @__PURE__ */ new Map();
  /**
   * Register a callback to be invoked when the browser becomes ready.
   * If browser is already running, callback is invoked immediately.
   * The callback is ALWAYS registered (even if invoked immediately) so it will
   * also fire on future "ready" events (e.g., session creation for thread isolation).
   * @param callback - Function to call when browser is ready
   * @param threadId - Optional thread ID to scope the callback to a specific thread
   * @returns Cleanup function to unregister the callback
   */
  onBrowserReady(callback, threadId) {
    if (threadId) {
      let threadCallbacks = this._onThreadReadyCallbacks.get(threadId);
      if (!threadCallbacks) {
        threadCallbacks = /* @__PURE__ */ new Set();
        this._onThreadReadyCallbacks.set(threadId, threadCallbacks);
      }
      threadCallbacks.add(callback);
      if (this.hasThreadSession(threadId)) {
        callback();
      }
      return () => {
        threadCallbacks.delete(callback);
        if (threadCallbacks.size === 0) {
          this._onThreadReadyCallbacks.delete(threadId);
        }
      };
    }
    this._onReadyCallbacks.add(callback);
    if (this.isBrowserRunning()) {
      callback();
    }
    return () => {
      this._onReadyCallbacks.delete(callback);
    };
  }
  /**
   * Register a callback to be invoked when the browser closes.
   * Useful for screencast to broadcast browser_closed status.
   * @param callback - Function to call when browser closes
   * @param threadId - Optional thread ID to scope the callback to a specific thread
   * @returns Cleanup function to unregister the callback
   */
  onBrowserClosed(callback, threadId) {
    if (threadId) {
      let threadCallbacks = this._onThreadClosedCallbacks.get(threadId);
      if (!threadCallbacks) {
        threadCallbacks = /* @__PURE__ */ new Set();
        this._onThreadClosedCallbacks.set(threadId, threadCallbacks);
      }
      threadCallbacks.add(callback);
      return () => {
        threadCallbacks.delete(callback);
        if (threadCallbacks.size === 0) {
          this._onThreadClosedCallbacks.delete(threadId);
        }
      };
    }
    this._onClosedCallbacks.add(callback);
    return () => {
      this._onClosedCallbacks.delete(callback);
    };
  }
  /**
   * Notify registered callbacks that browser is ready.
   * @param threadId - If provided, only notify callbacks for that thread (for thread scope)
   */
  notifyBrowserReady(threadId) {
    if (threadId) {
      const threadCallbacks = this._onThreadReadyCallbacks.get(threadId);
      if (threadCallbacks) {
        for (const callback of threadCallbacks) {
          try {
            callback();
          } catch {
          }
        }
      }
    } else {
      for (const callback of this._onReadyCallbacks) {
        try {
          callback();
        } catch {
        }
      }
      for (const [, threadCallbacks] of this._onThreadReadyCallbacks) {
        for (const callback of threadCallbacks) {
          try {
            callback();
          } catch {
          }
        }
      }
    }
  }
  /**
   * Notify registered callbacks that browser has closed.
   * @param threadId - If provided, only notify callbacks for that thread (for thread scope)
   */
  notifyBrowserClosed(threadId) {
    if (threadId) {
      const threadCallbacks = this._onThreadClosedCallbacks.get(threadId);
      if (threadCallbacks) {
        for (const callback of threadCallbacks) {
          try {
            callback();
          } catch {
          }
        }
      }
    } else {
      for (const callback of this._onClosedCallbacks) {
        try {
          callback();
        } catch {
        }
      }
      for (const [, threadCallbacks] of this._onThreadClosedCallbacks) {
        for (const callback of threadCallbacks) {
          try {
            callback();
          } catch {
          }
        }
      }
    }
  }
  // ---------------------------------------------------------------------------
  // URL Access (optional - providers that support it should override)
  // ---------------------------------------------------------------------------
  /**
   * Get the current page URL without launching the browser.
   * @param threadId - Optional thread ID for thread-isolated browsers
   * @returns The current URL string, or null if browser is not running or not supported
   */
  async getCurrentUrl(_threadId) {
    return null;
  }
  /**
   * Get the current browser state (all tabs and active tab index).
   * Override in subclass to provide actual tab state.
   * @param _threadId - Optional thread ID for thread-isolated sessions
   * @returns The browser state, or null if not available
   */
  async getBrowserState(_threadId) {
    return null;
  }
  /**
   * Get the last known browser state before the browser was closed.
   * Useful for restoring state on relaunch.
   * @param threadId - Optional thread ID for thread-isolated sessions
   * @returns The last browser state, or undefined if not available
   */
  getLastBrowserState(threadId) {
    if (threadId && this.threadManager) {
      const savedState = this.threadManager.getSavedBrowserState(threadId);
      if (savedState) {
        return savedState;
      }
    }
    return this.lastBrowserState;
  }
  /**
   * Get all open tabs with their URLs and titles.
   * Override in subclass to provide actual tab info.
   * @param _threadId - Optional thread ID for thread-isolated sessions
   * @returns Array of tab states
   */
  async getTabState(_threadId) {
    return [];
  }
  /**
   * Get the active tab index.
   * Override in subclass to provide actual active tab index.
   * @param _threadId - Optional thread ID for thread-isolated sessions
   * @returns The active tab index (0-based), or 0 if not available
   */
  async getActiveTabIndex(_threadId) {
    return 0;
  }
  /**
   * Navigate to a URL (simple form). Override in subclass if supported.
   * Used internally for restoring state on relaunch.
   * Named `navigateTo` to avoid conflicts with tool methods that have richer signatures.
   */
  async navigateTo(_url) {
  }
  // ---------------------------------------------------------------------------
  // Thread Management
  // ---------------------------------------------------------------------------
  /**
   * Set the current thread ID for subsequent browser operations.
   * Called by tools before executing browser actions to ensure
   * operations are routed to the correct thread session.
   *
   * @param threadId - The thread ID, or undefined to use the default thread
   */
  setCurrentThread(threadId) {
    this.currentThreadId = threadId ?? DEFAULT_THREAD_ID;
  }
  /**
   * Get the current thread ID.
   * @returns The current thread ID being used for operations
   */
  getCurrentThread() {
    return this.currentThreadId;
  }
  /**
   * Get the browser scope mode.
   * @returns The scope from threadManager or config, defaults to 'shared'
   */
  getScope() {
    return this.threadManager?.getScope() ?? this.config.scope ?? "shared";
  }
  // ---------------------------------------------------------------------------
  // Screencast (optional - for Studio live view)
  // ---------------------------------------------------------------------------
  /**
   * Start screencast streaming. Override in subclass if supported.
   */
  async startScreencast(_options) {
    throw new Error("Screencast not supported by this provider");
  }
  /**
   * Check if a thread has an existing browser session.
   * Used by startScreencastIfBrowserActive to prevent showing another thread's page.
   *
   * If threadManager is set, delegates to it. Otherwise returns true (no isolation).
   * Subclasses can override for custom behavior.
   *
   * @returns true if session exists or thread isolation is not used
   */
  hasThreadSession(threadId) {
    if (!this.threadManager) {
      return true;
    }
    const scope = this.threadManager.getScope();
    if (scope === "shared") {
      return true;
    }
    return this.threadManager.hasSession(threadId);
  }
  /**
   * Close a specific thread's browser session.
   * Delegates to ThreadManager and notifies registered callbacks.
   *
   * For 'thread' scope, this closes only that thread's browser instance.
   * For 'shared' scope, this is a no-op (use close() to close the shared browser).
   *
   * @param threadId - The thread ID whose session should be closed
   */
  async closeThreadSession(threadId) {
    if (!this.threadManager) {
      return;
    }
    await this.threadManager.destroySession(threadId);
    const pid = this.threadBrowserPids.get(threadId);
    killProcessGroup(pid, this.logger);
    this.threadBrowserPids.delete(threadId);
    this.notifyBrowserClosed(threadId);
    if (this.config.profile) {
      cleanupProfileLockFiles(this.config.profile, this.logger);
    }
  }
  /**
   * Handle browser disconnection for a specific thread.
   * Called when a thread's browser is closed externally (e.g., user closes browser window).
   * Clears the thread session and notifies registered callbacks.
   *
   * @param threadId - The thread ID whose session was disconnected
   */
  handleThreadBrowserDisconnected(threadId) {
    if (!this.threadManager) {
      return;
    }
    const pid = this.threadBrowserPids.get(threadId);
    killProcessGroup(pid, this.logger);
    this.threadBrowserPids.delete(threadId);
    this.threadManager.clearSession(threadId);
    this.logger.debug?.(`Cleared browser session for thread: ${threadId}`);
    this.notifyBrowserClosed(threadId);
    if (this.config.profile) {
      cleanupProfileLockFiles(this.config.profile, this.logger);
    }
  }
  /**
   * Get a session identifier for a specific thread.
   * In thread scope, returns a composite ID (browser:threadId).
   * In shared scope or without thread manager, returns the browser instance ID.
   */
  getSessionId(threadId) {
    if (!threadId || !this.threadManager) {
      return this.id;
    }
    const scope = this.threadManager.getScope();
    if (scope === "shared") {
      return this.id;
    }
    return `${this.id}:${threadId}`;
  }
  /**
   * Start screencast only if browser is already running.
   * Does NOT launch the browser.
   * Uses config.screencast options as defaults if no options provided.
   *
   * For thread-isolated browsers ('browser' mode):
   * - Returns null if the thread doesn't have an existing browser session
   */
  async startScreencastIfBrowserActive(options) {
    const mergedOptions = this.config.screencast || options ? { ...this.config.screencast, ...options } : void 0;
    const threadId = mergedOptions?.threadId;
    const scope = this.threadManager?.getScope() ?? this.config.scope ?? "shared";
    if (!this.isBrowserRunning(threadId)) {
      return null;
    }
    if (scope === "shared") {
      return this.startScreencast(mergedOptions);
    }
    if (threadId && !this.hasThreadSession(threadId)) {
      console.log(
        `[MastraBrowser] startScreencastIfBrowserActive: hasThreadSession(${threadId})=false, scope=${scope}`
      );
      return null;
    }
    return this.startScreencast(mergedOptions);
  }
  // ---------------------------------------------------------------------------
  // Event Injection (optional - for Studio live view)
  // ---------------------------------------------------------------------------
  /**
   * Inject a mouse event. Override in subclass if supported.
   * @param event - Mouse event parameters
   * @param threadId - Optional thread ID for thread-isolated sessions
   */
  async injectMouseEvent(_event, _threadId) {
    throw new Error("Mouse event injection not supported by this provider");
  }
  /**
   * Inject a keyboard event. Override in subclass if supported.
   * @param event - Keyboard event parameters
   * @param threadId - Optional thread ID for thread-isolated sessions
   */
  async injectKeyboardEvent(_event, _threadId) {
    throw new Error("Keyboard event injection not supported by this provider");
  }
  // ---------------------------------------------------------------------------
  // Input Processors
  // ---------------------------------------------------------------------------
  /**
   * Returns browser input processors (e.g., BrowserContextProcessor for context injection).
   * Skips if the user already added a processor with the same id.
   *
   * This method is similar to AgentChannels.getInputProcessors() and allows
   * browser implementations to provide their own processors.
   *
   * @param configuredProcessors - Processors already configured by the user (for deduplication)
   * @returns Array of input processors for this browser instance
   */
  getInputProcessors(configuredProcessors = []) {
    const hasProcessor = configuredProcessors.some(
      (p) => !isProcessorWorkflow(p) && "id" in p && p.id === "browser-context"
    );
    if (hasProcessor) return [];
    return [new BrowserContextProcessor()];
  }
};

// src/browser/screencast/types.ts
var SCREENCAST_DEFAULTS = {
  format: "jpeg",
  quality: 80,
  maxWidth: 1280,
  maxHeight: 720,
  everyNthFrame: 1
};
var ScreencastStream = class extends EventEmitter {
  /** Whether screencast is currently active */
  active = false;
  /** Resolved options with defaults applied (excludes threadId which is only used for page selection) */
  options;
  /** CDP session provider */
  provider;
  /** Current CDP session */
  cdpSession = null;
  /** Frame handler reference (for cleanup) */
  frameHandler = null;
  /**
   * Creates a new ScreencastStream.
   *
   * @param provider - CDP session provider (browser instance)
   * @param options - Screencast configuration options
   */
  constructor(provider, options) {
    super();
    this.provider = provider;
    const { threadId: _, ...cdpOptions } = options ?? {};
    this.options = { ...SCREENCAST_DEFAULTS, ...cdpOptions };
  }
  /**
   * Start the screencast.
   * If already active, returns immediately.
   */
  async start() {
    if (this.active) {
      return;
    }
    if (!this.provider.isBrowserRunning()) {
      throw new Error("Browser is not running");
    }
    try {
      this.cdpSession = await this.provider.getCdpSession();
      this.frameHandler = (params) => {
        const frameData = {
          data: params.data,
          // CDP provides timestamp in seconds, convert to milliseconds for consistency
          timestamp: params.metadata?.timestamp ? params.metadata.timestamp * 1e3 : Date.now(),
          viewport: {
            width: params.metadata?.deviceWidth ?? 0,
            height: params.metadata?.deviceHeight ?? 0,
            offsetTop: params.metadata?.offsetTop,
            scrollOffsetX: params.metadata?.scrollOffsetX,
            scrollOffsetY: params.metadata?.scrollOffsetY,
            pageScaleFactor: params.metadata?.pageScaleFactor
          },
          sessionId: params.sessionId
        };
        this.emit("frame", frameData);
        this.acknowledgeFrame(params.sessionId);
      };
      this.cdpSession.on("Page.screencastFrame", this.frameHandler);
      try {
        await this.cdpSession.send("Page.startScreencast", {
          format: this.options.format,
          quality: this.options.quality,
          maxWidth: this.options.maxWidth,
          maxHeight: this.options.maxHeight,
          everyNthFrame: this.options.everyNthFrame
        });
      } catch (startError) {
        if (this.cdpSession?.off) {
          try {
            this.cdpSession.off("Page.screencastFrame", this.frameHandler);
          } catch {
          }
        }
        this.frameHandler = null;
        this.cdpSession = null;
        throw startError;
      }
      this.active = true;
    } catch (error) {
      const err = error instanceof Error ? error : new Error(String(error));
      this.emit("error", err);
      throw err;
    }
  }
  /**
   * Acknowledge a frame to CDP (required to continue receiving frames).
   */
  acknowledgeFrame(sessionId) {
    if (!this.cdpSession) return;
    this.cdpSession.send("Page.screencastFrameAck", { sessionId }).catch(() => {
    });
  }
  /**
   * Stop the screencast and release resources.
   * Safe to call even if browser/CDP session is already closed.
   */
  async stop() {
    if (!this.active) {
      return;
    }
    this.active = false;
    let hadError = false;
    if (this.cdpSession && this.frameHandler && this.cdpSession.off) {
      try {
        this.cdpSession.off("Page.screencastFrame", this.frameHandler);
      } catch {
      }
    }
    this.frameHandler = null;
    if (this.cdpSession) {
      try {
        await this.cdpSession.send("Page.stopScreencast");
      } catch {
        hadError = true;
      }
      this.cdpSession = null;
    }
    this.emit("stop", hadError ? "error" : "manual");
  }
  /**
   * Check if screencast is currently active.
   */
  isActive() {
    return this.active;
  }
  /**
   * Emit a URL update event.
   * Browser providers call this when navigation is detected.
   */
  emitUrl(url) {
    this.emit("url", url);
  }
  /**
   * Reconnect the screencast by stopping and restarting.
   * Use this when the active page/tab changes.
   *
   * @returns Promise that resolves when reconnection is complete
   * @throws Error if reconnection fails (also emits 'error' event)
   */
  async reconnect() {
    if (this.cdpSession && this.frameHandler && this.cdpSession.off) {
      try {
        this.cdpSession.off("Page.screencastFrame", this.frameHandler);
      } catch {
      }
    }
    this.frameHandler = null;
    if (this.cdpSession) {
      try {
        await this.cdpSession.send("Page.stopScreencast");
      } catch {
      }
      this.cdpSession = null;
    }
    this.active = false;
    try {
      await this.start();
    } catch (error) {
      const err = error instanceof Error ? error : new Error(String(error));
      console.error("[ScreencastStream.reconnect] Failed to reconnect:", err);
      throw err;
    }
  }
};

export { BrowserContextProcessor, DEFAULT_THREAD_ID, MastraBrowser, SCREENCAST_DEFAULTS, ScreencastStream as ScreencastStreamImpl, ThreadManager, createError };
//# sourceMappingURL=index.js.map
//# sourceMappingURL=index.js.map