fix(browser): recover stale remote target ids
Co-authored-by: Ilya Strelov <10761735+strelov1@users.noreply.github.com>
This commit is contained in:
@@ -54,6 +54,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Slack/Upload: resolve bare user IDs (U-prefix) to DM channel IDs via `conversations.open` before calling `files.uploadV2`, which rejects non-channel IDs. `chat.postMessage` tolerates user IDs directly, but `files.uploadV2` → `completeUploadExternal` validates `channel_id` against `^[CGDZ][A-Z0-9]{8,}$`, causing `invalid_arguments` when agents reply with media to DM conversations.
|
||||
- Browser/Relay: treat extension websocket as connected only when `OPEN`, allow reconnect when a stale `CLOSING/CLOSED` extension socket lingers, and guard stale socket message/close handlers so late events cannot clear active relay state; includes regression coverage for live-duplicate `409` rejection and immediate reconnect-after-close races. (#15099, #18698, #20688)
|
||||
- Browser/Extension Relay: refactor the MV3 worker to preserve debugger attachments across relay drops, auto-reconnect with bounded backoff+jitter, persist and rehydrate attached tab state via `chrome.storage.session`, recover from `target_closed` navigation detaches, guard stale socket handlers, enforce per-tab operation locks and per-request timeouts, and add lifecycle keepalive/badge refresh hooks (`alarms`, `webNavigation`). (#15099, #6175, #8468, #9807)
|
||||
- Browser/Remote CDP: extend stale-target recovery so `ensureTabAvailable()` now reuses the sole available tab for remote CDP profiles (same behavior as extension profiles) while preserving strict `tab not found` errors when multiple tabs exist; includes remote-profile regression tests. (#15989)
|
||||
- Signal/RPC: guard malformed Signal RPC JSON responses with a clear status-scoped error and add regression coverage for invalid JSON responses. (#22995) Thanks @adhitShet.
|
||||
- Gateway/Subagents: guard gateway and subagent session-key/message trim paths against undefined inputs to prevent early `Cannot read properties of undefined (reading 'trim')` crashes during subagent spawn and wait flows.
|
||||
- Agents/Workspace: guard `resolveUserPath` against undefined/null input to prevent `Cannot read properties of undefined (reading 'trim')` crashes when workspace paths are missing in embedded runner flows.
|
||||
|
||||
@@ -153,6 +153,55 @@ describe("browser server-context remote profile tab operations", () => {
|
||||
expect(second.targetId).toBe("A");
|
||||
});
|
||||
|
||||
it("falls back to the only tab for remote profiles when targetId is stale", async () => {
|
||||
const responses = [
|
||||
[{ targetId: "T1", title: "Tab 1", url: "https://example.com", type: "page" }],
|
||||
[{ targetId: "T1", title: "Tab 1", url: "https://example.com", type: "page" }],
|
||||
];
|
||||
const listPagesViaPlaywright = vi.fn(async () => {
|
||||
const next = responses.shift();
|
||||
if (!next) {
|
||||
throw new Error("no more responses");
|
||||
}
|
||||
return next;
|
||||
});
|
||||
|
||||
vi.spyOn(pwAiModule, "getPwAiModule").mockResolvedValue({
|
||||
listPagesViaPlaywright,
|
||||
} as unknown as Awaited<ReturnType<typeof pwAiModule.getPwAiModule>>);
|
||||
|
||||
const { remote } = createRemoteRouteHarness();
|
||||
const chosen = await remote.ensureTabAvailable("STALE_TARGET");
|
||||
expect(chosen.targetId).toBe("T1");
|
||||
});
|
||||
|
||||
it("keeps rejecting stale targetId for remote profiles when multiple tabs exist", async () => {
|
||||
const responses = [
|
||||
[
|
||||
{ targetId: "A", title: "A", url: "https://a.example", type: "page" },
|
||||
{ targetId: "B", title: "B", url: "https://b.example", type: "page" },
|
||||
],
|
||||
[
|
||||
{ targetId: "A", title: "A", url: "https://a.example", type: "page" },
|
||||
{ targetId: "B", title: "B", url: "https://b.example", type: "page" },
|
||||
],
|
||||
];
|
||||
const listPagesViaPlaywright = vi.fn(async () => {
|
||||
const next = responses.shift();
|
||||
if (!next) {
|
||||
throw new Error("no more responses");
|
||||
}
|
||||
return next;
|
||||
});
|
||||
|
||||
vi.spyOn(pwAiModule, "getPwAiModule").mockResolvedValue({
|
||||
listPagesViaPlaywright,
|
||||
} as unknown as Awaited<ReturnType<typeof pwAiModule.getPwAiModule>>);
|
||||
|
||||
const { remote } = createRemoteRouteHarness();
|
||||
await expect(remote.ensureTabAvailable("STALE_TARGET")).rejects.toThrow(/tab not found/i);
|
||||
});
|
||||
|
||||
it("uses Playwright focus for remote profiles when available", async () => {
|
||||
const listPagesViaPlaywright = vi.fn(async () => [
|
||||
{ targetId: "T1", title: "Tab 1", url: "https://example.com", type: "page" },
|
||||
|
||||
@@ -410,8 +410,12 @@ function createProfileContext(
|
||||
};
|
||||
|
||||
let chosen = targetId ? resolveById(targetId) : pickDefault();
|
||||
if (!chosen && profile.driver === "extension" && candidates.length === 1) {
|
||||
// If an agent passes a stale/foreign targetId but we only have a single attached tab,
|
||||
if (
|
||||
!chosen &&
|
||||
(profile.driver === "extension" || !profile.cdpIsLoopback) &&
|
||||
candidates.length === 1
|
||||
) {
|
||||
// If an agent passes a stale/foreign targetId but only one candidate remains,
|
||||
// recover by using that tab instead of failing hard.
|
||||
chosen = candidates[0] ?? null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user