Skip to content

Commit b81ffe5

Browse files
authored
fix the session close default session creation behavior (#82)
1 parent 49bc547 commit b81ffe5

File tree

6 files changed

+126
-38
lines changed

6 files changed

+126
-38
lines changed

browserbase/src/context.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { BrowserSession } from "./sessionManager.js";
33
import {
44
getSession,
55
defaultSessionId,
6+
getSessionReadOnly,
67
} from "./sessionManager.js";
78
import type { Tool, ToolResult } from "./tools/tool.js";
89
import type { Config } from "../config.js";
@@ -195,6 +196,34 @@ export class Context {
195196
return session.browser;
196197
}
197198

199+
/**
200+
* Get the active browser without triggering session creation.
201+
* This is a read-only operation used when we need to check for an existing browser
202+
* without side effects (e.g., during close operations).
203+
* @returns The browser if it exists and is connected, null otherwise
204+
*/
205+
public getActiveBrowserReadOnly(): BrowserSession["browser"] | null {
206+
const session = getSessionReadOnly(this.currentSessionId);
207+
if (!session || !session.browser || !session.browser.isConnected()) {
208+
return null;
209+
}
210+
return session.browser;
211+
}
212+
213+
/**
214+
* Get the active page without triggering session creation.
215+
* This is a read-only operation used when we need to check for an existing page
216+
* without side effects.
217+
* @returns The page if it exists and is not closed, null otherwise
218+
*/
219+
public getActivePageReadOnly(): BrowserSession["page"] | null {
220+
const session = getSessionReadOnly(this.currentSessionId);
221+
if (!session || !session.page || session.page.isClosed()) {
222+
return null;
223+
}
224+
return session.page;
225+
}
226+
198227
public async waitForTimeout(timeoutMillis: number): Promise<void> {
199228
return new Promise((resolve) => setTimeout(resolve, timeoutMillis));
200229
}
@@ -507,7 +536,8 @@ export class Context {
507536

508537
// 2. Prepare and add additional textual information (URL, Title, Snapshot)
509538
const additionalInfoParts: string[] = [];
510-
const currentPage = await this.getActivePage();
539+
// Use read-only version to avoid creating sessions after close
540+
const currentPage = this.getActivePageReadOnly();
511541

512542
if (currentPage) {
513543
try {

browserbase/src/sessionManager.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,64 @@ export async function getSession(
324324
return sessionObj;
325325
}
326326

327+
/**
328+
* Get a session by ID without creating new sessions.
329+
* This is a read-only operation that never triggers session creation.
330+
* Used for operations like closing sessions where we don't want side effects.
331+
* @param sessionId The session ID to retrieve
332+
* @returns The session if it exists and is valid, null otherwise
333+
*/
334+
export function getSessionReadOnly(sessionId: string): BrowserSession | null {
335+
// Check if it's the default session
336+
if (sessionId === defaultSessionId && defaultBrowserSession) {
337+
// Only return if it's actually connected and valid
338+
if (defaultBrowserSession.browser.isConnected() && !defaultBrowserSession.page.isClosed()) {
339+
return defaultBrowserSession;
340+
}
341+
return null;
342+
}
343+
344+
// For non-default sessions, check the browsers map
345+
const sessionObj = browsers.get(sessionId);
346+
if (!sessionObj) {
347+
return null;
348+
}
349+
350+
// Validate the session is still active
351+
if (!sessionObj.browser.isConnected() || sessionObj.page.isClosed()) {
352+
return null;
353+
}
354+
355+
return sessionObj;
356+
}
357+
358+
/**
359+
* Clean up a session by removing it from tracking.
360+
* This is called after a browser is closed to ensure proper cleanup.
361+
* @param sessionId The session ID to clean up
362+
*/
363+
export function cleanupSession(sessionId: string): void {
364+
process.stderr.write(
365+
`[SessionManager] Cleaning up session: ${sessionId}\n`
366+
);
367+
368+
// Remove from browsers map
369+
browsers.delete(sessionId);
370+
371+
// Clear default session reference if this was the default
372+
if (sessionId === defaultSessionId && defaultBrowserSession) {
373+
defaultBrowserSession = null;
374+
}
375+
376+
// Reset active session to default if this was the active one
377+
if (activeSessionId === sessionId) {
378+
process.stderr.write(
379+
`[SessionManager] Cleaned up active session ${sessionId}, resetting to default.\n`
380+
);
381+
setActiveSessionId(defaultSessionId);
382+
}
383+
}
384+
327385
// Function to close all managed browser sessions gracefully
328386
export async function closeAllSessions(): Promise<void> {
329387
process.stderr.write(`[SessionManager] Closing all sessions...\n`);

browserbase/src/tools/common.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,15 @@ async function handleWait(context: Context, params: WaitInput): Promise
2828

2929
// Define tool using handle
3030
const waitTool: Tool<typeof WaitInputSchema> = {
31-
capability: 'core', // Add capability
31+
capability: 'core',
3232
schema: waitSchema,
3333
handle: handleWait,
3434
};
3535

3636

3737
// --- Tool: Close ---
3838
const CloseInputSchema = z.object({
39-
random_string: z.string().optional().describe("Dummy parameter") // Keep schema if needed
39+
random_string: z.string().optional().describe("Dummy parameter")
4040
});
4141
type CloseInput = z.infer<typeof CloseInputSchema>;
4242

browserbase/src/tools/getText.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import type { ToolActionResult } from '../context.js';
66
// --- Tool: Get Text ---
77
const GetTextInputSchema = z.object({
88
selector: z.string().optional().describe("Optional CSS selector to get text from. If omitted, gets text from the whole body."),
9-
sessionId: z.string().optional(), // Keep for schema consistency if needed elsewhere
109
});
1110
type GetTextInput = z.infer<typeof GetTextInputSchema>;
1211

browserbase/src/tools/session.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@ import {
88
createNewBrowserSession,
99
defaultSessionId,
1010
ensureDefaultSessionInternal,
11+
cleanupSession,
1112
type BrowserSession,
1213
} from "../sessionManager.js";
1314

14-
1515
// --- Tool: Create Session ---
1616
const CreateSessionInputSchema = z.object({
1717
// Keep sessionId optional, but clarify its role
@@ -43,7 +43,8 @@ async function handleCreateSession(
4343
let targetSessionId: string;
4444

4545
if (params.sessionId) {
46-
targetSessionId = params.sessionId;
46+
const projectId = config.browserbaseProjectId || '';
47+
targetSessionId = `${params.sessionId}_${projectId}`;
4748
process.stderr.write(
4849
`[tool.createSession] Attempting to create/assign session with specified ID: ${targetSessionId}`
4950
);
@@ -140,10 +141,10 @@ async function handleCloseSession(
140141
let browserClosedSuccessfully = false;
141142
let browserCloseErrorMessage = "";
142143

143-
// Step 1: Attempt to get the active browser instance
144+
// Step 1: Attempt to get the active browser instance WITHOUT creating a new one
144145
try {
145-
// This call might be associated with 'previousSessionId' (which could be default or specific)
146-
browser = await context.getActiveBrowser();
146+
// Use read-only version to avoid creating new sessions
147+
browser = context.getActiveBrowserReadOnly();
147148
} catch (error: any) {
148149
process.stderr.write(
149150
`[tool.closeSession] Error retrieving active browser (session ID was ${previousSessionId || 'default/unknown'}): ${error.message || String(error)}`
@@ -164,6 +165,9 @@ async function handleCloseSession(
164165
`[tool.closeSession] Browser connection for session (was ${previousSessionId}) closed.`
165166
);
166167

168+
// Clean up the session from tracking
169+
cleanupSession(previousSessionId);
170+
167171
process.stderr.write(
168172
`[tool.closeSession] View session replay at https://www.browserbase.com/sessions/${previousSessionId}`
169173
);
@@ -204,7 +208,7 @@ async function handleCloseSession(
204208
if (browserClosedSuccessfully) { // Browser was present and closed
205209
let successMessage = `Browserbase session (associated with context ID ${previousSessionId || 'default'}) closed successfully. Context reset to default.`;
206210
if (previousSessionId && previousSessionId !== defaultSessionId) {
207-
successMessage += ` If this was a uniquely named session (${previousSessionId}), view replay (if available) at https://browserbase.com/sessions/${previousSessionId}`;
211+
successMessage += ` If this was a uniquely named session (${previousSessionId}), view replay (if available) at https://browserbase.com/sessions`;
208212
}
209213
return { content: [{ type: "text", text: successMessage }] };
210214
}

browserbase/src/tools/snapshot.ts

Lines changed: 25 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@ import type {
33
TextContent,
44
ImageContent,
55
} from "@modelcontextprotocol/sdk/types.js";
6+
import type { Locator, PageScreenshotOptions } from "playwright-core";
67

78
import { defineTool, type ToolResult, } from "./tool.js";
89
import type { Context, ToolActionResult } from "../context.js";
9-
import type { Page, Locator } from "playwright-core";
1010
import { PageSnapshot } from "../pageSnapshot.js";
1111
import { outputFile } from "../config.js";
1212

@@ -358,27 +358,23 @@ const selectOption = defineTool({
358358
});
359359

360360
// --- Tool: Screenshot (Adapted Handle, Example Action) ---
361-
const screenshotSchema = z
362-
.object({
363-
raw: z
364-
.boolean()
365-
.optional()
366-
.describe(
367-
"Whether to return without compression (PNG). Default is false (JPEG)."
368-
),
369-
element: z
370-
.string()
371-
.optional()
372-
.describe("Human-readable element description."),
373-
ref: z
374-
.string()
375-
.optional()
376-
.describe("Exact target element reference from the page snapshot."),
377-
})
378-
.refine((data) => !!data.element === !!data.ref, {
379-
message: "Both element and ref must be provided or neither.",
380-
path: ["ref", "element"],
381-
});
361+
const screenshotSchema = z.object({
362+
raw: z
363+
.boolean()
364+
.optional()
365+
.describe(
366+
"Whether to return without compression (PNG). Default is false (JPEG)."
367+
),
368+
element: z
369+
.string()
370+
.optional()
371+
.describe("Human-readable element description."),
372+
ref: z
373+
.string()
374+
.optional()
375+
.describe("Exact target element reference from the page snapshot.")
376+
});
377+
382378
type ScreenshotInput = z.infer<typeof screenshotSchema>;
383379

384380
const screenshot = defineTool<typeof screenshotSchema>({
@@ -392,6 +388,10 @@ const screenshot = defineTool({
392388
context: Context,
393389
params: ScreenshotInput
394390
): Promise<ToolResult> => {
391+
if (!!params.element !== !!params.ref) {
392+
throw new Error("Both element and ref must be provided or neither.");
393+
}
394+
395395
const page = await context.getActivePage();
396396
if (!page) {
397397
throw new Error("No active page found for screenshot");
@@ -407,15 +407,12 @@ const screenshot = defineTool({
407407
`screenshot-${Date.now()}.${fileType}`
408408
);
409409

410-
const baseOptions: Omit<
411-
Parameters<Page["screenshot"]>[0],
412-
"type" | "quality" | "path"
413-
> = {
410+
const baseOptions: PageScreenshotOptions = {
414411
scale: "css",
415412
timeout: 15000, // Kept existing timeout
416413
};
417414

418-
let options: Parameters<Page["screenshot"]>[0];
415+
let options: PageScreenshotOptions;
419416

420417
if (fileType === "jpeg") {
421418
options = {
@@ -499,4 +496,4 @@ export async function generateLocator(locator: Locator): Promise {
499496
return (locator as any)._generateLocatorString();
500497
}
501498

502-
export default [snapshot, click, drag, hover, type, selectOption, screenshot];
499+
export default [snapshot, click, drag, hover, type, selectOption, screenshot];

0 commit comments

Comments
 (0)