From e53d05a872436259a7dfa15b4ddef6e972ae4e5f Mon Sep 17 00:00:00 2001 From: root Date: Tue, 29 Sep 2026 03:13:40 +0800 Subject: [PATCH] fix(browser): reload session state when another process switched the tab api and worker processes each keep adapter state in memory. After the upstream reaped an idle tab and the api process reopened one, the agent worker kept pointing at the lost tab and every screenshot / snapshot in solve_challenge failed with not_found (REAL-2 task 4). loaded() now restores from the database when its current tab differs. Needs image rebuild. Co-Authored-By: Claude Opus 5.5 --- apps/server/src/modules/browser/service.ts | 5 ++++- apps/server/test/e2e/e14-vision.test.ts | 18 +++++++++++++++++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/apps/server/src/modules/browser/service.ts b/apps/server/src/modules/browser/service.ts index 93b5122f..f8921f7d 100644 --- a/apps/server/src/modules/browser/service.ts +++ b/apps/server/src/modules/browser/service.ts @@ -529,7 +529,10 @@ export class BrowserService { throw new BrowserServiceError("precondition", "站点已停用,浏览器会话已关闭"); } const provider = this.providerFor(prow); - if (!provider.sessionInfo(row.id)) { + // api / worker 各有一份适配器内存状态:另一个进程换了当前标签页(回收后重开)时,按数据库重新载入 + const info = provider.sessionInfo(row.id); + const dbTab = (obj(row.recovery_state) as RecoveryState).currentTabId ?? null; + if (!info || (dbTab !== null && info.currentTabId !== dbTab)) { const tabs = await this.tabRows(row.id); provider.restoreSession({ session: toSession(row), diff --git a/apps/server/test/e2e/e14-vision.test.ts b/apps/server/test/e2e/e14-vision.test.ts index b159298e..9c1f84dd 100644 --- a/apps/server/test/e2e/e14-vision.test.ts +++ b/apps/server/test/e2e/e14-vision.test.ts @@ -6,7 +6,7 @@ import type { FakeScript, FakeToolResult, InvokeContext } from "@umt/agent"; import { createMockCamofox, type MockCamofox } from "@umt/testkit"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; import { browserServiceFor } from "../../src/modules/browser/index.ts"; -import type { BrowserService } from "../../src/modules/browser/service.ts"; +import { BrowserService } from "../../src/modules/browser/service.ts"; import { createE2E, type E2E, pgAvailable, runEvents, runRow, waitFor } from "./harness.ts"; // E14 视觉自动化闭环(VIS-1):fake 视觉模型(脚本返回固定坐标)+ testkit mock camofox(视口 1600×1000、DPR 1, @@ -353,4 +353,20 @@ describe.skipIf(!hasPg)("E14 视觉自动化闭环", () => { expect(ledger.length).toBe(2); for (const l of ledger) expect((l.output as Any).modelImage).toBeNull(); }, 90_000); + + it("另一个进程(api)在标签页被回收后重开,本进程(worker)按数据库换到新标签页", async () => { + await open(ids.site, ids.provider, `${ORIGIN}/torrents.php`); + const worker = new BrowserService(e.booted.ctx, svc.opts); + const auto = { kind: "automation", holder: "agent:e14" } as const; + const snap = (s: BrowserService) => s.run(sid, auto, (p, ref) => p.snapshot(ref), { write: false, persist: false }); + await snap(worker); + const old = (await svc.view(sid)).currentTabId!; + mock.dropTab(old); + await expect(snap(svc)).rejects.toThrow(/回收/); + await svc.run(sid, auto, (p, _ref, x) => + p.openTab(sid, { url: `${ORIGIN}/torrents.php`, budget: x.budget, as: "automation" }), + ); + expect((await svc.view(sid)).currentTabId).not.toBe(old); + await expect(snap(worker)).resolves.toMatchObject({ url: expect.stringContaining("/torrents.php") }); + }, 60_000); });