import type { Browser, BrowserContext, Page } from 'playwright-core'; import { type BrowserHostOptions } from './browser-host-client.js'; import type { BrowserLaunchResult, BrowserPageInfo, BrowserProvisionIo, BrowserProvisionReport, BrowserSessionInfo } from './browser-types.js'; export interface BrowserLaunchOptions { readonly profileName?: string | undefined; readonly headless?: boolean | undefined; readonly viewport?: { readonly width: number; readonly height: number; } | undefined; } export interface BrowserAttachOptions { /** A CDP endpoint of a browser the user already has running. */ readonly cdpEndpoint: string; } export declare class BrowserSessionError extends Error { readonly fix: string | null; constructor(message: string, fix?: string | null); } /** * Where saved browser profiles live under a home directory the caller owns. * * The surface root is the product's own segment under `~/.goodvibes/`, passed * in rather than hardcoded, so two surfaces sharing a machine never share a * profile directory. */ export declare function browserProfileRoot(homeDirectory: string, surfaceRoot: string): string; /** * Where screenshots go: the platform's surface-scoped storage root, alongside * profiles. Not a visible folder in someone's project, a surface's own files * belong in that surface's own place, and session write provenance is what lets * it read them back. */ export declare function browserScreenshotRoot(homeDirectory: string, surfaceRoot: string): string; interface DriverApi { readonly chromium: { readonly launchPersistentContext: (userDataDir: string, options: Record) => Promise; readonly connectOverCDP: (endpoint: string, options?: Record) => Promise; }; } export interface BrowserSessionManagerDeps { /** Directory that holds saved browser profiles. Owned by the composition root. */ readonly profileRoot: string; /** * The product's storage root segment under `~/.goodvibes/`, used for the * managed driver directory. Required rather than defaulted: a driver this * process installs has to land in a directory a named surface owns. */ readonly surfaceRoot: string; readonly io?: BrowserProvisionIo; /** Loads the Playwright driver. Injected so ownership rules are testable offline. */ readonly loadDriver?: () => DriverApi | null; /** Finds which candidate endpoint has a browser behind it. */ readonly probeEndpoint?: (candidates: readonly string[]) => Promise; /** Home directory owning the managed browser cache. Defaults to the profile root's owner. */ readonly homeDirectory?: string; /** Where the node-hosted browser host lives, for a build that stages it itself. */ readonly host?: BrowserHostOptions; } /** * Owns every live browser connection. * * The safety rule this class exists to enforce structurally: a browser the * agent did not start has no code path that ends it. `closeSession` refuses * attached sessions outright, and shutdown only closes what this manager * launched. Ownership is recorded at connect time, not decided later. */ export declare class BrowserSessionManager { private readonly sessions; /** Node-hosted driver processes, one per session that needed one. */ private readonly hosts; private sessionCounter; private lastProvision; /** * A launch already in progress, so a second `launch()` call made while the * first is still opening a browser waits on the SAME attempt instead of * starting a second browser process against the same profile directory. * This is what turned "the model called launch three times while the * first was still opening" into three windows. */ private launchInFlight; /** * Consecutive launch failures, reset to 0 on any success. Capped at * `MAX_CONSECUTIVE_LAUNCH_FAILURES`, after that many failures in a row, * `launch()` refuses to try again on its own rather than opening another * window that will fail the same way. One value 2 means one real retry * after the first failure, not open-ended retrying. */ private consecutiveLaunchFailures; private static readonly MAX_CONSECUTIVE_LAUNCH_FAILURES; private readonly io; private readonly profileRoot; private readonly homeDirectory; private readonly loadDriver; private readonly probeEndpoint; private readonly hostOptions; constructor(deps: BrowserSessionManagerDeps); /** Install-kind-aware remediation, from the same injected IO the policy uses. */ private driverFix; provisionReport(): BrowserProvisionReport | null; provision(options?: { readonly repair?: boolean | undefined; readonly allowDownload?: boolean | undefined; }): Promise; private driver; /** * Whether this manager's own record still reflects a live browser. * * `isClosed` is read defensively rather than assumed: a driver double that * does not implement it (every existing test fixture) is treated as alive * rather than dead, because the failure mode of wrongly calling a live * session dead is a second browser process fighting the first one over the * same profile directory, the exact bug this method exists to prevent. * Only an explicit `true` counts as dead. */ private isSessionAlive; /** * The live launched session to reuse, if any. A dead one found along the * way is dropped from the registry first, so a stale entry never blocks a * fresh launch from going ahead. */ private liveLaunchedSession; /** * Opens a managed browser, or hands back the one already open. * * Three rules live here, in order: * 1. One live launched session at a time, a second call reuses it * instead of opening another window on the same profile directory. * 2. A launch already in flight is awaited rather than duplicated, so * concurrent calls cannot start two browser processes at once. * 3. Failures are counted; after MAX_CONSECUTIVE_LAUNCH_FAILURES in a row * launch refuses to try again on its own, and says so in plain words, * instead of opening one more window that will fail the same way. */ launch(options?: BrowserLaunchOptions): Promise; private performLaunch; /** * Connects to a browser the user already has running. The returned session is * permanently marked attached, which is what makes closing it impossible. */ attach(options: BrowserAttachOptions): Promise; /** * Attaches by way of the Node-hosted driver. * * Used when the in-process client cannot complete the handshake. The session * it produces is an ordinary attached session: origin 'attached', so nothing * here can close the user's browser. */ private attachThroughHost; private register; private trackPage; private describe; list(): readonly BrowserSessionInfo[]; info(sessionId: string): BrowserSessionInfo; hasSessions(): boolean; /** The session a call should act on when none is named. */ defaultSessionId(): string | null; private require; requireContext(sessionId: string): BrowserContext; page(sessionId: string, pageId?: string): Promise<{ readonly pageId: string; readonly page: Page; }>; newPage(sessionId: string): Promise<{ readonly pageId: string; readonly page: Page; }>; setActivePage(sessionId: string, pageId: string): void; pageList(sessionId: string): Promise; /** * Ends a browser this agent started. Attached browsers are refused: the agent * has no path to end a session a person is using. Closing a TAB inside an * attached browser is a separate, page-scoped action. */ closeSession(sessionId: string): Promise; /** * Disconnects from a session without ending the browser. For attached * browsers this is the ONLY exit: the CDP transport is dropped and the * user's browser keeps running with its tabs and login intact. */ release(sessionId: string): BrowserSessionInfo; /** Closes only what this agent launched. Attached browsers are left running. */ shutdown(): Promise; } /** * Endpoints to try for an attach, in order. * * A browser started with --remote-debugging-port may end up listening on IPv6 * loopback only, in which case connecting to 127.0.0.1 hangs until it times * out. Accepting a bare port number and trying both loopback families turns a * confusing 30-second stall into a connection that just works. */ export declare function cdpEndpointCandidates(endpoint: string): readonly string[]; export interface ReachableCdpEndpoint { readonly endpoint: string; readonly webSocketDebuggerUrl: string | null; } export declare function hasDisplay(): boolean; export {}; //# sourceMappingURL=browser-sessions.d.ts.map