惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

罗磊的独立博客
爱范儿
爱范儿
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
博客园_首页
博客园 - 叶小钗
酷 壳 – CoolShell
酷 壳 – CoolShell
Apple Machine Learning Research
Apple Machine Learning Research
云风的 BLOG
云风的 BLOG
量子位
博客园 - 三生石上(FineUI控件)
Stack Overflow Blog
Stack Overflow Blog
小众软件
小众软件
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
V
V2EX
人人都是产品经理
人人都是产品经理
V
Visual Studio Blog
Jina AI
Jina AI
L
LangChain Blog
M
MIT News - Artificial intelligence
MongoDB | Blog
MongoDB | Blog
Last Week in AI
Last Week in AI
Martin Fowler
Martin Fowler
WordPress大学
WordPress大学

Recent Commits to openclaw:main

test: merge chat side-result checks · openclaw/openclaw@ddd2c2a test: merge cron history checks · openclaw/openclaw@f7eb746 test: merge responsive navigation shell checks · openclaw/openclaw@c2e4b47 docs(changelog): add codex oauth fixes · openclaw/openclaw@628e6cd test: merge navigation routing cases · openclaw/openclaw@5d8cecb Tests: mock channel registry bundled fallback · openclaw/openclaw@2b08233 Secrets: avoid broad web search discovery for single plugin config · openclaw/openclaw@a464f59 test: merge config view browser checks · openclaw/openclaw@20cf511 fix(status): align oauth health with runtime · openclaw/openclaw@eed7116 feat: add macOS screen snapshots for monitor preview (#67954) thanks … · openclaw/openclaw@f377db1 fix: report shared auth scopes in hello-ok (#67810) thanks @BunsDev · openclaw/openclaw@0b6c39b Auto-reply: avoid eager bundled route fallback · openclaw/openclaw@3ea1bf4 Tests: narrow session binding contract setup · openclaw/openclaw@54e4e16 fix(macOS): enable undo/redo in webchat composer text input (#34962) · openclaw/openclaw@00951dc Tests: speed up channel setup promotion · openclaw/openclaw@82b529a Docs: refresh agent instructions · openclaw/openclaw@5775fe2 fix(auth): serialize OAuth refresh across agents to fix #26322 (#67876) · openclaw/openclaw@8e79080 test: allow ollama public surface boundary test · openclaw/openclaw@7d4f1a6 Docs: add test performance guardrails · openclaw/openclaw@89706d3 Tests: restore context-engine usage proof · openclaw/openclaw@e4c4f95 Tests: slim context engine runtime coverage · openclaw/openclaw@74c198f ci: retry failed custom checkouts · openclaw/openclaw@0ee5baf test: trim duplicate provider auth onboarding cases · openclaw/openclaw@1ffc02e matrix: fix sessions_spawn --thread subagent session spawning (#67643) · openclaw/openclaw@1ce2596 test: reduce auth choice fixture churn · openclaw/openclaw@857b9cd test: mock health status config boundaries · openclaw/openclaw@9d5ab4a test: mock onboard config io boundary · openclaw/openclaw@299694d test: mock legacy state plugin boundaries · openclaw/openclaw@2713089 test: mock channel install boundaries · openclaw/openclaw@b945248 test: mock doctor preview channel boundaries · openclaw/openclaw@b1a3ad4
Fix sticky Chrome MCP status probes · openclaw/openclaw@3...
steipete · 2026-04-25 · via Recent Commits to openclaw:main

@@ -31,6 +31,18 @@ type ChromeMcpSession = {

3131

ready: Promise<void>;

3232

};

333334+

type ChromeMcpCallOptions = {

35+

ephemeral?: boolean;

36+

timeoutMs?: number;

37+

signal?: AbortSignal;

38+

};

39+40+

type ChromeMcpSessionLease = {

41+

session: ChromeMcpSession;

42+

cacheKey: string;

43+

temporary: boolean;

44+

};

45+3446

type ChromeMcpSessionFactory = (

3547

profileName: string,

3648

userDataDir?: string,

@@ -332,7 +344,42 @@ async function createRealSession(

332344

};

333345

}

334346335-

async function getSession(profileName: string, userDataDir?: string): Promise<ChromeMcpSession> {

347+

async function waitForChromeMcpReady(

348+

session: ChromeMcpSession,

349+

profileName: string,

350+

timeoutMs?: number,

351+

): Promise<void> {

352+

if (!timeoutMs || timeoutMs <= 0) {

353+

await session.ready;

354+

return;

355+

}

356+357+

let timer: ReturnType<typeof setTimeout> | undefined;

358+

try {

359+

await Promise.race([

360+

session.ready,

361+

new Promise<never>((_, reject) => {

362+

timer = setTimeout(() => {

363+

reject(

364+

new BrowserProfileUnavailableError(

365+

`Chrome MCP existing-session attach for profile "${profileName}" timed out after ${timeoutMs}ms.`,

366+

),

367+

);

368+

}, timeoutMs);

369+

}),

370+

]);

371+

} finally {

372+

if (timer) {

373+

clearTimeout(timer);

374+

}

375+

}

376+

}

377+378+

async function getSession(

379+

profileName: string,

380+

userDataDir?: string,

381+

timeoutMs?: number,

382+

): Promise<ChromeMcpSession> {

336383

const cacheKey = buildChromeMcpSessionCacheKey(profileName, userDataDir);

337384

await closeChromeMcpSessionsForProfile(profileName, cacheKey);

338385

@@ -364,7 +411,7 @@ async function getSession(profileName: string, userDataDir?: string): Promise<Ch

364411

}

365412

}

366413

try {

367-

await session.ready;

414+

await waitForChromeMcpReady(session, profileName, timeoutMs);

368415

return session;

369416

} catch (err) {

370417

const current = sessions.get(cacheKey);

@@ -375,23 +422,110 @@ async function getSession(profileName: string, userDataDir?: string): Promise<Ch

375422

}

376423

}

377424425+

async function getExistingSession(

426+

cacheKey: string,

427+

profileName: string,

428+

timeoutMs?: number,

429+

): Promise<ChromeMcpSession | null> {

430+

let session = sessions.get(cacheKey);

431+

if (session && session.transport.pid === null) {

432+

sessions.delete(cacheKey);

433+

session = undefined;

434+

}

435+

if (session) {

436+

try {

437+

await waitForChromeMcpReady(session, profileName, timeoutMs);

438+

return session;

439+

} catch (err) {

440+

const current = sessions.get(cacheKey);

441+

if (current?.transport === session.transport) {

442+

sessions.delete(cacheKey);

443+

}

444+

throw err;

445+

}

446+

}

447+448+

const pending = pendingSessions.get(cacheKey);

449+

if (!pending) {

450+

return null;

451+

}

452+453+

session = await pending;

454+

try {

455+

await waitForChromeMcpReady(session, profileName, timeoutMs);

456+

return session;

457+

} catch (err) {

458+

const current = sessions.get(cacheKey);

459+

if (current?.transport === session.transport) {

460+

sessions.delete(cacheKey);

461+

}

462+

throw err;

463+

}

464+

}

465+466+

async function createEphemeralSession(

467+

profileName: string,

468+

userDataDir?: string,

469+

timeoutMs?: number,

470+

): Promise<ChromeMcpSession> {

471+

const session = await (sessionFactory ?? createRealSession)(profileName, userDataDir);

472+

try {

473+

await waitForChromeMcpReady(session, profileName, timeoutMs);

474+

return session;

475+

} catch (err) {

476+

await session.client.close().catch(() => {});

477+

throw err;

478+

}

479+

}

480+481+

async function leaseSession(

482+

profileName: string,

483+

userDataDir?: string,

484+

options: ChromeMcpCallOptions = {},

485+

): Promise<ChromeMcpSessionLease> {

486+

const cacheKey = buildChromeMcpSessionCacheKey(profileName, userDataDir);

487+

if (!options.ephemeral) {

488+

return {

489+

session: await getSession(profileName, userDataDir, options.timeoutMs),

490+

cacheKey,

491+

temporary: false,

492+

};

493+

}

494+495+

// Status probes should avoid seeding the shared attach session cache, but they can safely

496+

// reuse a real cached session if one already exists.

497+

const existingSession = await getExistingSession(cacheKey, profileName, options.timeoutMs);

498+

if (existingSession) {

499+

return {

500+

session: existingSession,

501+

cacheKey,

502+

temporary: false,

503+

};

504+

}

505+506+

return {

507+

session: await createEphemeralSession(profileName, userDataDir, options.timeoutMs),

508+

cacheKey,

509+

temporary: true,

510+

};

511+

}

512+378513

async function callTool(

379514

profileName: string,

380515

userDataDir: string | undefined,

381516

name: string,

382517

args: Record<string, unknown> = {},

383-

opts?: { timeoutMs?: number; signal?: AbortSignal },

518+

options: ChromeMcpCallOptions = {},

384519

): Promise<ChromeMcpToolResult> {

385-

const cacheKey = buildChromeMcpSessionCacheKey(profileName, userDataDir);

386-

const timeoutMs = opts?.timeoutMs;

387-

const signal = opts?.signal;

520+

const timeoutMs = options.timeoutMs;

521+

const signal = options.signal;

388522

if (signal?.aborted) {

389523

throw signal.reason ?? new Error("aborted");

390524

}

391525392526

for (let attempt = 0; attempt < 2; attempt += 1) {

393-

const session = await getSession(profileName, userDataDir);

394-

const rawCall = session.client.callTool({

527+

const lease = await leaseSession(profileName, userDataDir, options);

528+

const rawCall = lease.session.client.callTool({

395529

name,

396530

arguments: args,

397531

}) as Promise<ChromeMcpToolResult>;

@@ -430,10 +564,12 @@ async function callTool(

430564

void rawCall.catch(() => {});

431565

// Transport/connection error, timeout, or abort: tear down session so it reconnects.

432566

// Transport-identity check prevents clobbering a replacement session created concurrently.

433-

const cur = sessions.get(cacheKey);

434-

if (cur?.transport === session.transport) {

435-

sessions.delete(cacheKey);

436-

await session.client.close().catch(() => {});

567+

if (!lease.temporary) {

568+

const cur = sessions.get(lease.cacheKey);

569+

if (cur?.transport === lease.session.transport) {

570+

sessions.delete(lease.cacheKey);

571+

await lease.session.client.close().catch(() => {});

572+

}

437573

}

438574

throw err;

439575

} finally {

@@ -443,17 +579,22 @@ async function callTool(

443579

if (signal && abortListener) {

444580

signal.removeEventListener("abort", abortListener);

445581

}

582+

if (lease.temporary) {

583+

await lease.session.client.close().catch(() => {});

584+

}

446585

}

447586

// Tool-level errors (element not found, script error, etc.) don't indicate a

448587

// broken connection. A stale selected-page error does poison the Chrome MCP

449588

// session, so reconnect and retry that one once.

450589

if (result.isError) {

451590

const message = extractToolErrorMessage(result, name);

452591

if (shouldReconnectForToolError(name, message)) {

453-

const cur = sessions.get(cacheKey);

454-

if (cur?.transport === session.transport) {

455-

sessions.delete(cacheKey);

456-

await session.client.close().catch(() => {});

592+

if (!lease.temporary) {

593+

const cur = sessions.get(lease.cacheKey);

594+

if (cur?.transport === lease.session.transport) {

595+

sessions.delete(lease.cacheKey);

596+

await lease.session.client.close().catch(() => {});

597+

}

457598

}

458599

if (attempt === 0) {

459600

continue;

@@ -492,8 +633,12 @@ async function findPageById(

492633

export async function ensureChromeMcpAvailable(

493634

profileName: string,

494635

userDataDir?: string,

636+

options: ChromeMcpCallOptions = {},

495637

): Promise<void> {

496-

await getSession(profileName, userDataDir);

638+

const lease = await leaseSession(profileName, userDataDir, options);

639+

if (lease.temporary) {

640+

await lease.session.client.close().catch(() => {});

641+

}

497642

}

498643499644

export function getChromeMcpPid(profileName: string): number | null {

@@ -519,16 +664,18 @@ export async function stopAllChromeMcpSessions(): Promise<void> {

519664

export async function listChromeMcpPages(

520665

profileName: string,

521666

userDataDir?: string,

667+

options: ChromeMcpCallOptions = {},

522668

): Promise<ChromeMcpStructuredPage[]> {

523-

const result = await callTool(profileName, userDataDir, "list_pages");

669+

const result = await callTool(profileName, userDataDir, "list_pages", {}, options);

524670

return extractStructuredPages(result);

525671

}

526672527673

export async function listChromeMcpTabs(

528674

profileName: string,

529675

userDataDir?: string,

676+

options: ChromeMcpCallOptions = {},

530677

): Promise<BrowserTab[]> {

531-

return toBrowserTabs(await listChromeMcpPages(profileName, userDataDir));

678+

return toBrowserTabs(await listChromeMcpPages(profileName, userDataDir, options));

532679

}

533680534681

export async function openChromeMcpTab(