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 <[email protected]>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-09-29 03:13:40 +08:00
1 parent be216fbe65
commit e53d05a872
2 files changed
+21 -2

No files matched your search

+4 -1
View File
@@ -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),
+17 -1
View File
@@ -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);
});