Skip to content

Commit 753257d

Browse files
steipetejung0han
andcommitted
fix(browser): clear stale cookies before navigation
Co-authored-by: Dongwoo Jeong <dongwoo@duck.com>
1 parent f7c28d5 commit 753257d

6 files changed

Lines changed: 152 additions & 27 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020

2121
- Browser: retain runtime, model-selection, and redacted prompt-commit diagnostics in failed session metadata when ChatGPT submission verification times out. Fixes #286. Thanks @LeoLin990405!
2222
- API: forward configured reasoning effort through custom OpenAI-compatible chat-completions gateways.
23-
- Browser: clear stale ChatGPT temporary-conversation cookies before runs so accumulated `conv_key_*` entries do not bloat request headers and trigger header-size failures.
23+
- Browser: clear stale ChatGPT temporary-conversation cookies before navigation while preserving keys for open or resumed conversations, preventing accumulated `conv_key_*` entries from triggering header-size failures. Thanks @jung0han!
2424
- Browser: accept a stable, exact file-input name match when ChatGPT marks the composer ready but exposes no attachment chip or count, while still waiting through active uploads and rejecting missing or extra files. Fixes #275. Thanks @wangwllu!
2525
- Browser: avoid returning truncated Pro answers when completion controls appear during the thinking-to-answer transition. Thanks @xuan-wei!
2626
- Browser/Bridge: improve ChatGPT ZIP artifact capture before bridge transfer by broadening sandbox/file-card/download-control discovery, adding sanitized direct-download diagnostics, and falling back to scoped browser downloads when sandbox fetches fail. Thanks @DK625!

src/browser/cookies.ts

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,39 @@ import { getCookies, type Cookie } from "@steipete/sweet-cookie";
55

66
export class ChromeCookieSyncError extends Error {}
77

8-
export async function clearChatGptConversationCookies(
8+
export async function clearStaleChatGptConversationCookies(
99
Network: ChromeClient["Network"],
10+
Target: ChromeClient["Target"],
1011
logger: BrowserLogger,
12+
options: { preserveConversationIds?: readonly (string | null | undefined)[] } = {},
1113
): Promise<number> {
1214
try {
15+
const preservedNames = new Set(
16+
(options.preserveConversationIds ?? [])
17+
.filter((id): id is string => Boolean(id))
18+
.map((id) => `conv_key_${id}`),
19+
);
20+
try {
21+
const { targetInfos = [] } = await Target.getTargets();
22+
for (const target of targetInfos) {
23+
const conversationId = extractChatGptConversationId(target.url ?? "");
24+
if (conversationId) {
25+
preservedNames.add(`conv_key_${conversationId}`);
26+
}
27+
}
28+
} catch (error) {
29+
const message = error instanceof Error ? error.message : String(error);
30+
logger(
31+
`[cookies] Failed to inspect active ChatGPT conversations; skipping stale cookie cleanup: ${message}`,
32+
);
33+
return 0;
34+
}
35+
1336
const { cookies = [] } = await Network.getAllCookies();
14-
const targets = cookies.filter((cookie) => isChatGptConversationCookie(cookie));
37+
const targets = cookies.filter(
38+
(cookie) =>
39+
isChatGptConversationCookie(cookie) && !preservedNames.has(String(cookie.name ?? "")),
40+
);
1541
if (targets.length === 0) {
1642
return 0;
1743
}
@@ -27,9 +53,7 @@ export async function clearChatGptConversationCookies(
2753
deleted += 1;
2854
} catch (error) {
2955
const message = error instanceof Error ? error.message : String(error);
30-
logger(
31-
`[cookies] Failed to clear stale ChatGPT conversation cookie ${cookie.name}: ${message}`,
32-
);
56+
logger(`[cookies] Failed to clear a stale ChatGPT conversation cookie: ${message}`);
3357
}
3458
}
3559

@@ -174,7 +198,7 @@ async function readChromeCookies(
174198
}
175199

176200
function isChatGptConversationCookie(cookie: { name?: string; domain?: string }): boolean {
177-
if (!cookie.name?.startsWith("conv")) {
201+
if (!cookie.name?.startsWith("conv_key_")) {
178202
return false;
179203
}
180204
const domain = String(cookie.domain ?? "")
@@ -183,6 +207,19 @@ function isChatGptConversationCookie(cookie: { name?: string; domain?: string })
183207
return domain === "chatgpt.com" || domain === "chat.openai.com";
184208
}
185209

210+
function extractChatGptConversationId(url: string): string | undefined {
211+
try {
212+
const parsed = new URL(url);
213+
const domain = parsed.hostname.toLowerCase();
214+
if (domain !== "chatgpt.com" && domain !== "chat.openai.com") {
215+
return undefined;
216+
}
217+
return parsed.pathname.match(/\/c\/([a-zA-Z0-9-]+)/)?.[1];
218+
} catch {
219+
return undefined;
220+
}
221+
}
222+
186223
function normalizeInlineCookies(rawCookies: CookieParam[], fallbackHost: string): CookieParam[] {
187224
const merged = new Map<string, CookieParam>();
188225
for (const cookie of rawCookies) {

src/browser/index.ts

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import {
2323
closeRemoteChromeTarget,
2424
closeBlankChromeTabs,
2525
} from "./chromeLifecycle.js";
26-
import { clearChatGptConversationCookies, syncCookies } from "./cookies.js";
26+
import { clearStaleChatGptConversationCookies, syncCookies } from "./cookies.js";
2727
import {
2828
navigateToChatGPT,
2929
navigateToPromptReadyWithFallback,
@@ -1106,7 +1106,7 @@ export async function runBrowserMode(options: BrowserRunOptions): Promise<Browse
11061106
} else {
11071107
const strictTabIsolation = Boolean(manualLogin && reusedChrome);
11081108
const devtoolsRetries = manualLogin ? 6 : 0;
1109-
const connection = await connectWithNewTab(chrome.port, logger, config.url, chromeHost, {
1109+
const connection = await connectWithNewTab(chrome.port, logger, "about:blank", chromeHost, {
11101110
fallbackToDefault: !strictTabIsolation,
11111111
retries: devtoolsRetries,
11121112
retryDelayMs: 500,
@@ -1142,7 +1142,7 @@ export async function runBrowserMode(options: BrowserRunOptions): Promise<Browse
11421142
});
11431143
const raceWithDisconnect = <T>(promise: Promise<T>): Promise<T> =>
11441144
Promise.race([promise, disconnectPromise]);
1145-
const { Network, Page, Runtime, Input, DOM } = client;
1145+
const { Network, Page, Runtime, Input, DOM, Target } = client;
11461146

11471147
if (!config.headless && config.hideWindow) {
11481148
await hideChromeWindow(chrome, logger);
@@ -1201,7 +1201,12 @@ export async function runBrowserMode(options: BrowserRunOptions): Promise<Browse
12011201
: "Skipping Chrome cookie sync (--browser-no-cookie-sync)",
12021202
);
12031203
}
1204-
await clearChatGptConversationCookies(Network, logger);
1204+
await clearStaleChatGptConversationCookies(Network, Target, logger, {
1205+
preserveConversationIds: [
1206+
extractConversationIdFromUrl(config.resumeConversationUrl ?? ""),
1207+
extractConversationIdFromUrl(lastUrl ?? ""),
1208+
],
1209+
});
12051210

12061211
if (cookieSyncEnabled && !manualLogin && (appliedCookies ?? 0) === 0 && !config.inlineCookies) {
12071212
// Learned: if the profile has no ChatGPT cookies, browser mode will just bounce to login.
@@ -2797,9 +2802,16 @@ async function runRemoteBrowserMode(
27972802
`Attached to existing remote ChatGPT tab ${attached.targetId}${attached.tab.url ? ` (${attached.tab.url})` : ""}`,
27982803
);
27992804
} else {
2800-
connection = await connectToRemoteChrome(host, port, logger, config.url, browserWSEndpoint, {
2801-
approvalWaitMs: config.attachRunning && browserWSEndpoint ? 20_000 : undefined,
2802-
});
2805+
connection = await connectToRemoteChrome(
2806+
host,
2807+
port,
2808+
logger,
2809+
"about:blank",
2810+
browserWSEndpoint,
2811+
{
2812+
approvalWaitMs: config.attachRunning && browserWSEndpoint ? 20_000 : undefined,
2813+
},
2814+
);
28032815
client = connection.client;
28042816
remoteTargetId = connection.targetId ?? null;
28052817
ownsTarget = true;
@@ -2816,7 +2828,7 @@ async function runRemoteBrowserMode(
28162828
connectionClosedUnexpectedly = true;
28172829
};
28182830
client.on("disconnect", markConnectionLost);
2819-
const { Network, Page, Runtime, Input, DOM } = client;
2831+
const { Network, Page, Runtime, Input, DOM, Target } = client;
28202832

28212833
const domainEnablers = [Network.enable({}), Page.enable(), Runtime.enable()];
28222834
if (DOM && typeof DOM.enable === "function") {
@@ -2850,7 +2862,12 @@ async function runRemoteBrowserMode(
28502862

28512863
// Skip cookie sync for remote Chrome - it already has cookies
28522864
logger("Skipping cookie sync for remote Chrome (using existing session)");
2853-
await clearChatGptConversationCookies(Network, logger);
2865+
await clearStaleChatGptConversationCookies(Network, Target, logger, {
2866+
preserveConversationIds: [
2867+
extractConversationIdFromUrl(config.resumeConversationUrl ?? ""),
2868+
extractConversationIdFromUrl(lastUrl ?? ""),
2869+
],
2870+
});
28542871

28552872
if (config.resumeConversationUrl) {
28562873
await navigateToChatGPT(Page, Runtime, config.resumeConversationUrl, logger);

src/browser/projectSourcesRunner.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
registerTerminationHooks,
1111
} from "./chromeLifecycle.js";
1212
import { resolveBrowserConfig } from "./config.js";
13-
import { syncCookies } from "./cookies.js";
13+
import { clearStaleChatGptConversationCookies, syncCookies } from "./cookies.js";
1414
import {
1515
installJavaScriptDialogAutoDismissal,
1616
navigateToChatGPT,
@@ -180,7 +180,7 @@ export async function runBrowserProjectSources(
180180
const raceWithDisconnect = <T>(promise: Promise<T>): Promise<T> =>
181181
Promise.race([promise, disconnectPromise]);
182182

183-
const { Network, Page, Runtime, Input, DOM } = client;
183+
const { Network, Page, Runtime, Input, DOM, Target } = client;
184184
if (!config.headless && config.hideWindow) {
185185
await hideChromeWindow(chrome, logger);
186186
}
@@ -200,6 +200,7 @@ export async function runBrowserProjectSources(
200200
manualLogin,
201201
logger,
202202
});
203+
await clearStaleChatGptConversationCookies(Network, Target, logger);
203204

204205
await raceWithDisconnect(navigateToChatGPT(Page, Runtime, CHATGPT_URL, logger));
205206
await raceWithDisconnect(

src/browser/reattach.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import {
2020
listRemoteChromeTargets,
2121
} from "./chromeLifecycle.js";
2222
import { resolveBrowserConfig } from "./config.js";
23-
import { syncCookies } from "./cookies.js";
23+
import { clearStaleChatGptConversationCookies, syncCookies } from "./cookies.js";
2424
import { CHATGPT_URL } from "./constants.js";
2525
import { buildConversationTurnListExpression } from "./conversationTurns.js";
2626
import { cleanupStaleProfileState } from "./profileState.js";
@@ -271,7 +271,7 @@ async function resumeBrowserSessionViaNewChrome(
271271
const chrome = await launchChrome(resolved, userDataDir, logger);
272272
const chromeHost = (chrome as unknown as { host?: string }).host ?? "127.0.0.1";
273273
const client = await connectToChrome(chrome.port, logger, chromeHost);
274-
const { Network, Page, Runtime, DOM } = client;
274+
const { Network, Page, Runtime, DOM, Target } = client;
275275

276276
if (Runtime?.enable) {
277277
await Runtime.enable();
@@ -294,6 +294,14 @@ async function resumeBrowserSessionViaNewChrome(
294294
});
295295
}
296296

297+
await clearStaleChatGptConversationCookies(Network, Target, logger, {
298+
preserveConversationIds: [
299+
runtime.conversationId,
300+
extractConversationIdFromUrl(runtime.tabUrl ?? ""),
301+
extractConversationIdFromUrl(resolved.url),
302+
],
303+
});
304+
297305
await navigateToChatGPT(Page, Runtime, CHATGPT_URL, logger);
298306
await ensureNotBlocked(Runtime, resolved.headless, logger);
299307
await ensureLoggedIn(Runtime, logger, { appliedCookies });

tests/browser/cookies.test.ts

Lines changed: 69 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { beforeEach, describe, expect, test, vi } from "vitest";
22
import {
3-
clearChatGptConversationCookies,
3+
clearStaleChatGptConversationCookies,
44
syncCookies,
55
ChromeCookieSyncError,
66
} from "../../src/browser/cookies.js";
@@ -16,23 +16,33 @@ beforeEach(() => {
1616
logger.mockReset();
1717
});
1818

19-
describe("clearChatGptConversationCookies", () => {
20-
test("deletes only ChatGPT conv cookies", async () => {
19+
describe("clearStaleChatGptConversationCookies", () => {
20+
test("deletes only stale ChatGPT conversation keys", async () => {
2121
const deleteCookies = vi.fn().mockResolvedValue(undefined);
2222
const Network = {
2323
getAllCookies: vi.fn().mockResolvedValue({
2424
cookies: [
2525
{ name: "conv_key_123", domain: "chatgpt.com", path: "/" },
2626
{ name: "conv_key_456", domain: ".chat.openai.com", path: "/" },
27+
{ name: "conv_key_active-open", domain: ".chatgpt.com", path: "/" },
2728
{ name: "__Secure-next-auth.session-token", domain: "chatgpt.com", path: "/" },
29+
{ name: "conv_tracking", domain: "chatgpt.com", path: "/" },
2830
{ name: "Conversion", domain: "www.googleadservices.com", path: "/" },
2931
{ name: "conv_tracking", domain: "example.com", path: "/" },
3032
],
3133
}),
3234
deleteCookies,
3335
} as unknown as ChromeClient["Network"];
36+
const Target = {
37+
getTargets: vi.fn().mockResolvedValue({
38+
targetInfos: [
39+
{ type: "page", url: "https://chatgpt.com/c/active-open" },
40+
{ type: "page", url: "https://example.com/c/not-chatgpt" },
41+
],
42+
}),
43+
} as unknown as ChromeClient["Target"];
3444

35-
const deleted = await clearChatGptConversationCookies(Network, logger);
45+
const deleted = await clearStaleChatGptConversationCookies(Network, Target, logger);
3646

3747
expect(deleted).toBe(2);
3848
expect(deleteCookies).toHaveBeenCalledTimes(2);
@@ -49,6 +59,34 @@ describe("clearChatGptConversationCookies", () => {
4959
expect(logger).toHaveBeenCalledWith("[cookies] Cleared 2 stale ChatGPT conversation cookies.");
5060
});
5161

62+
test("preserves keys for conversations that may be resumed", async () => {
63+
const deleteCookies = vi.fn().mockResolvedValue(undefined);
64+
const Network = {
65+
getAllCookies: vi.fn().mockResolvedValue({
66+
cookies: [
67+
{ name: "conv_key_active-id", domain: ".chatgpt.com", path: "/" },
68+
{ name: "conv_key_stale-id", domain: ".chatgpt.com", path: "/" },
69+
],
70+
}),
71+
deleteCookies,
72+
} as unknown as ChromeClient["Network"];
73+
const Target = {
74+
getTargets: vi.fn().mockResolvedValue({ targetInfos: [] }),
75+
} as unknown as ChromeClient["Target"];
76+
77+
const deleted = await clearStaleChatGptConversationCookies(Network, Target, logger, {
78+
preserveConversationIds: [undefined, "active-id"],
79+
});
80+
81+
expect(deleted).toBe(1);
82+
expect(deleteCookies).toHaveBeenCalledOnce();
83+
expect(deleteCookies).toHaveBeenCalledWith({
84+
name: "conv_key_stale-id",
85+
domain: ".chatgpt.com",
86+
path: "/",
87+
});
88+
});
89+
5290
test("continues when individual stale cookie deletion fails", async () => {
5391
const deleteCookies = vi
5492
.fn()
@@ -63,12 +101,15 @@ describe("clearChatGptConversationCookies", () => {
63101
}),
64102
deleteCookies,
65103
} as unknown as ChromeClient["Network"];
104+
const Target = {
105+
getTargets: vi.fn().mockResolvedValue({ targetInfos: [] }),
106+
} as unknown as ChromeClient["Target"];
66107

67-
const deleted = await clearChatGptConversationCookies(Network, logger);
108+
const deleted = await clearStaleChatGptConversationCookies(Network, Target, logger);
68109

69110
expect(deleted).toBe(1);
70111
expect(logger).toHaveBeenCalledWith(
71-
expect.stringContaining("Failed to clear stale ChatGPT conversation cookie conv_key_failed"),
112+
"[cookies] Failed to clear a stale ChatGPT conversation cookie: locked",
72113
);
73114
expect(logger).toHaveBeenCalledWith("[cookies] Cleared 1 stale ChatGPT conversation cookie.");
74115
});
@@ -78,14 +119,35 @@ describe("clearChatGptConversationCookies", () => {
78119
getAllCookies: vi.fn().mockRejectedValue(new Error("devtools unavailable")),
79120
deleteCookies: vi.fn(),
80121
} as unknown as ChromeClient["Network"];
122+
const Target = {
123+
getTargets: vi.fn().mockResolvedValue({ targetInfos: [] }),
124+
} as unknown as ChromeClient["Target"];
81125

82-
const deleted = await clearChatGptConversationCookies(Network, logger);
126+
const deleted = await clearStaleChatGptConversationCookies(Network, Target, logger);
83127

84128
expect(deleted).toBe(0);
85129
expect(logger).toHaveBeenCalledWith(
86130
"[cookies] Failed to inspect ChatGPT conversation cookies: devtools unavailable",
87131
);
88132
});
133+
134+
test("skips cleanup when active conversation discovery fails", async () => {
135+
const Network = {
136+
getAllCookies: vi.fn(),
137+
deleteCookies: vi.fn(),
138+
} as unknown as ChromeClient["Network"];
139+
const Target = {
140+
getTargets: vi.fn().mockRejectedValue(new Error("target discovery unavailable")),
141+
} as unknown as ChromeClient["Target"];
142+
143+
const deleted = await clearStaleChatGptConversationCookies(Network, Target, logger);
144+
145+
expect(deleted).toBe(0);
146+
expect(Network.getAllCookies).not.toHaveBeenCalled();
147+
expect(logger).toHaveBeenCalledWith(
148+
"[cookies] Failed to inspect active ChatGPT conversations; skipping stale cookie cleanup: target discovery unavailable",
149+
);
150+
});
89151
});
90152

91153
describe("syncCookies", () => {

0 commit comments

Comments
 (0)