diff --git a/agent/hooks/task-state.ts b/agent/hooks/task-state.ts index 0bd6c1e..b17dbfa 100644 --- a/agent/hooks/task-state.ts +++ b/agent/hooks/task-state.ts @@ -7,9 +7,10 @@ const update = (event: HookEvent, ctx: { session: { id: string } }) => taskState.update((current) => applyTaskEvent(current, event, ctx.session.id)); const cancel = async (event: HookEvent<"turn.cancelled">, ctx: { session: { id: string } }) => { - update(event, ctx); await closeBrowserRuntime(ctx.session.id); + update(event, ctx); }; + export default defineHook({ events: { "session.started": update, diff --git a/server/browser/privacy-lock.ts b/server/browser/privacy-lock.ts index 1216426..f53ca7d 100644 --- a/server/browser/privacy-lock.ts +++ b/server/browser/privacy-lock.ts @@ -14,6 +14,7 @@ export async function quarantineBrowser(): Promise { export async function withBrowserPrivacyLock(sessionId: string, operation: () => Promise, database?: Pool, existingBrowser = false): Promise { if (!database && isLinkSandbox()) { + if (!process.env.LINK_SANDBOX_ACCESS_TOKEN?.trim()) return operation(); let state = sandboxBrowsers.get(sessionId); if (!state && existingBrowser) throw new Error("The sandbox cannot recover a browser from another process. Use human handoff to inspect the existing purchase; do not retry it."); if (!state) { state = { tail: Promise.resolve(), quarantined: false }; sandboxBrowsers.set(sessionId, state); } diff --git a/tests/browser/browser-close.test.ts b/tests/browser/browser-close.test.ts index 9c5fc93..447b880 100644 --- a/tests/browser/browser-close.test.ts +++ b/tests/browser/browser-close.test.ts @@ -17,6 +17,7 @@ import Browserbase from "@browserbasehq/sdk"; import { BrowserSession } from "../../server/browser/browser-session"; import type { ConversationState } from "../../server/browser/types"; import { browserChannelEvents, closeBrowserRuntime, hasBrowserRuntime, runBrowserOperation } from "../../agent/lib/browser-runtime"; +import taskHooks from "../../agent/hooks/task-state"; import { taskState } from "../../agent/lib/task-state"; import { ContextContainer, contextStorage } from "../../node_modules/eve/dist/src/context/container.js"; @@ -105,4 +106,22 @@ describe("terminal cleanup in a fresh Eve worker", () => { await closeBrowserRuntime(id); } })); + + it("turn.cancelled releases the persisted browser before clearing its ID", () => + contextStorage.run(new ContextContainer(), async () => { + const id = "fresh-worker-cancel"; + taskState.update((current) => ({ + ...current, + browser: { lifecycle: "active", sessionId: "persisted-browser" }, + })); + release.mockResolvedValue({ status: "COMPLETED" }); + await taskHooks.events!["turn.cancelled"]!( + { type: "turn.cancelled", meta: { id: "evt_cancel" } } as never, + { session: { id } } as never, + ); + expect(release).toHaveBeenCalledExactlyOnceWith("persisted-browser", { status: "REQUEST_RELEASE" }); + expect(taskState.get()).toMatchObject({ status: "canceled", browser: { lifecycle: "closed" } }); + expect(taskState.get().browser.sessionId).toBeUndefined(); + expect(hasBrowserRuntime(id)).toBe(false); + })); });