Repository: browserbase/stagehand
File: packages/core/lib/v3/understudy/context.ts (TypeScript)

Code before:
    url.startsWith("filesystem:")
  )
    return true;
  return false;
}

function isNonWebTarget(info: Protocol.Target.TargetInfo): boolean {
  return (
    (info.type !== "page" && info.type !== "iframe") ||
    !hasInjectableDOM(info.url)
  );
}

function isTopLevelPage(info: Protocol.Target.TargetInfo): boolean {
  const ti = info as unknown as { subtype?: string };
  return info.type === "page" && ti.subtype !== "iframe";
}

const DEFAULT_FIRST_TOP_LEVEL_PAGE_TIMEOUT_MS = 5000;
const CI_FIRST_TOP_LEVEL_PAGE_TIMEOUT_MS = 30000;
const FIRST_TOP_LEVEL_PAGE_TIMEOUT_ENV =
  "STAGEHAND_FIRST_TOP_LEVEL_PAGE_TIMEOUT_MS";
const WAIT_FOR_FIRST_TOP_LEVEL_PAGE_OPERATION =
  "waitForFirstTopLevelPage (no top-level Page)";

class WaitForFirstTopLevelPageTimeoutError extends TimeoutError {
  constructor(timeoutMs: number) {
    super(WAIT_FOR_FIRST_TOP_LEVEL_PAGE_OPERATION, timeoutMs);
  }
}

function getFirstTopLevelPageTimeoutMs(): number {
  return (
    getEnvTimeoutMs(FIRST_TOP_LEVEL_PAGE_TIMEOUT_ENV) ??
    (process.env.CI
      ? CI_FIRST_TOP_LEVEL_PAGE_TIMEOUT_MS
      : DEFAULT_FIRST_TOP_LEVEL_PAGE_TIMEOUT_MS)
  );
}

/**
 * V3Context
 *
 * Owns the root CDP connection and wires Target/Page events into Page.
 * Maintains one Page per top-level target, adopts OOPIF child sessions into the owner Page,
 * and tracks target→page and (root) frame→target mappings for lookups.
 *
 * IMPORTANT: FrameId → session ownership is managed inside Page (via its FrameRegistry).
 * Context never “guesses” owners; it simply forwards events (with the emitting session)
 * so Page can record the correct owner at event time.
 */

Review comment (suggestion):
i dont think we need another error class for this unless you feel strongly. either way, it should go inside the `packages/core/types/sdkErrors.ts` & be exported with the other error types 

Code after:
    url.startsWith("filesystem:")
  )
    return true;
  return false;
}

function isNonWebTarget(info: Protocol.Target.TargetInfo): boolean {
  return (
    (info.type !== "page" && info.type !== "iframe") ||
    !hasInjectableDOM(info.url)
  );
}

function isTopLevelPage(info: Protocol.Target.TargetInfo): boolean {
  const ti = info as unknown as { subtype?: string };
  return info.type === "page" && ti.subtype !== "iframe";
}

const DEFAULT_FIRST_TOP_LEVEL_PAGE_TIMEOUT_MS = 5000;
const CI_FIRST_TOP_LEVEL_PAGE_TIMEOUT_MS = 30000;
const FIRST_TOP_LEVEL_PAGE_TIMEOUT_ENV =
  "STAGEHAND_FIRST_TOP_LEVEL_PAGE_TIMEOUT_MS";
const WAIT_FOR_FIRST_TOP_LEVEL_PAGE_OPERATION =
  "waitForFirstTopLevelPage (no top-level Page)";

function getFirstTopLevelPageTimeoutMs(): number {
  return (
    getEnvTimeoutMs(FIRST_TOP_LEVEL_PAGE_TIMEOUT_ENV) ??
    (process.env.CI
      ? CI_FIRST_TOP_LEVEL_PAGE_TIMEOUT_MS
      : DEFAULT_FIRST_TOP_LEVEL_PAGE_TIMEOUT_MS)
  );
}

/**
 * V3Context
 *
 * Owns the root CDP connection and wires Target/Page events into Page.
 * Maintains one Page per top-level target, adopts OOPIF child sessions into the owner Page,
 * and tracks target→page and (root) frame→target mappings for lookups.
 *
 * IMPORTANT: FrameId → session ownership is managed inside Page (via its FrameRegistry).
 * Context never “guesses” owners; it simply forwards events (with the emitting session)
 * so Page can record the correct owner at event time.
 */
export class V3Context {
  private constructor(
    readonly conn: CdpConnection,
    private readonly env: "LOCAL" | "BROWSERBASE" = "LOCAL",
    private readonly apiClient: StagehandAPIClient | null = null,
    private readonly localBrowserLaunchOptions: LocalBrowserLaunchOptions | null = null,