Skip to content

Commit 4e38f68

Browse files
author
Nima21
committed
Merge feat/idle-page-reaper: fix internal page detection in idle reaper
2 parents 010d433 + 851a5da commit 4e38f68

1 file changed

Lines changed: 70 additions & 9 deletions

File tree

src/McpContext.ts

Lines changed: 70 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -141,9 +141,18 @@ export class McpContext implements Context {
141141
// Idle page reaper: tracks last activity time per page.
142142
static readonly PAGE_IDLE_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
143143
static readonly IDLE_CHECK_INTERVAL_MS = 60 * 1000; // 60 seconds
144+
static readonly #INTERNAL_URL_PREFIXES = [
145+
'about:',
146+
'chrome://',
147+
'chrome-extension://',
148+
'chrome-untrusted://',
149+
'devtools://',
150+
];
144151
#pageLastActivity = new Map<Page, number>();
145152
#idleReaperTimer?: ReturnType<typeof setInterval>;
146153
#onIdleBrowserEmpty?: () => void;
154+
// Tracks when we first noticed only internal pages remaining.
155+
#onlyInternalPagesSince?: number;
147156

148157
#uniqueBackendNodeIdToMcpId = new Map<string, string>();
149158

@@ -195,6 +204,8 @@ export class McpContext implements Context {
195204
*/
196205
touchPage(page: Page): void {
197206
this.#pageLastActivity.set(page, Date.now());
207+
// A user is interacting with a page — reset the internal-only timer.
208+
this.#onlyInternalPagesSince = undefined;
198209
}
199210

200211
/**
@@ -234,6 +245,13 @@ export class McpContext implements Context {
234245
}
235246
}
236247

248+
static #isInternalPage(page: Page): boolean {
249+
const url = page.url();
250+
return McpContext.#INTERNAL_URL_PREFIXES.some(prefix =>
251+
url.startsWith(prefix),
252+
);
253+
}
254+
237255
async #reapIdlePages(): Promise<void> {
238256
const now = Date.now();
239257
const idleTimeout = McpContext.PAGE_IDLE_TIMEOUT_MS;
@@ -245,8 +263,21 @@ export class McpContext implements Context {
245263
return; // Browser may be disconnected.
246264
}
247265

266+
// Also get ALL pages from the browser (including those filtered by
267+
// createPagesSnapshot) so we can detect internal-only state.
268+
let allBrowserPages: Page[];
269+
try {
270+
allBrowserPages = await this.browser.pages(true);
271+
} catch {
272+
return;
273+
}
274+
248275
const pagesToClose: Page[] = [];
249276
for (const page of pages) {
277+
// Skip internal pages — they can't be closed and will respawn.
278+
if (McpContext.#isInternalPage(page)) {
279+
continue;
280+
}
250281
const lastActivity = this.#pageLastActivity.get(page);
251282
// Pages without a recorded timestamp get one now (first seen).
252283
if (lastActivity === undefined) {
@@ -258,11 +289,7 @@ export class McpContext implements Context {
258289
}
259290
}
260291

261-
if (pagesToClose.length === 0) {
262-
return;
263-
}
264-
265-
// Close idle pages. If ALL pages are idle, close them all.
292+
// Close idle user pages.
266293
for (const page of pagesToClose) {
267294
try {
268295
this.logger(
@@ -275,16 +302,50 @@ export class McpContext implements Context {
275302
}
276303
}
277304

278-
// Refresh after closing.
305+
// Re-check all browser pages to determine if only internal pages remain.
279306
try {
280-
pages = await this.createPagesSnapshot();
307+
allBrowserPages = await this.browser.pages(true);
281308
} catch {
282-
pages = [];
309+
allBrowserPages = [];
283310
}
284311

285-
if (pages.length === 0 && this.#onIdleBrowserEmpty) {
312+
const hasUserPages = allBrowserPages.some(
313+
p => !p.isClosed() && !McpContext.#isInternalPage(p),
314+
);
315+
316+
if (hasUserPages) {
317+
// User pages still exist — reset the internal-only timer.
318+
this.#onlyInternalPagesSince = undefined;
319+
return;
320+
}
321+
322+
// Only internal pages (or no pages) remain.
323+
if (!this.#onIdleBrowserEmpty) {
324+
return;
325+
}
326+
327+
if (allBrowserPages.length === 0) {
328+
// Truly empty — kill immediately.
286329
this.logger('All pages closed by idle reaper, triggering browser cleanup');
287330
this.#onIdleBrowserEmpty();
331+
return;
332+
}
333+
334+
// Internal pages only — start or check the grace timer.
335+
if (this.#onlyInternalPagesSince === undefined) {
336+
this.#onlyInternalPagesSince = now;
337+
this.logger(
338+
`Only internal pages remain (${allBrowserPages.length}), starting ${idleTimeout / 1000}s grace period`,
339+
);
340+
return;
341+
}
342+
343+
if (now - this.#onlyInternalPagesSince > idleTimeout) {
344+
this.logger(
345+
`Only internal pages for ${Math.round((now - this.#onlyInternalPagesSince) / 1000)}s, triggering browser cleanup`,
346+
);
347+
this.#onlyInternalPagesSince = undefined;
348+
this.#onIdleBrowserEmpty();
288349
}
289350
}
290351

0 commit comments

Comments
 (0)