diff --git a/CHANGELOG.md b/CHANGELOG.md index 999ba959c..0949ab940 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/src/browser/server-context.remote-tab-ops.test.ts b/src/browser/server-context.remote-tab-ops.test.ts index a4ae8b539..9847b20cf 100644 --- a/src/browser/server-context.remote-tab-ops.test.ts +++ b/src/browser/server-context.remote-tab-ops.test.ts @@ -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>); + + 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>); + + 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" }, diff --git a/src/browser/server-context.ts b/src/browser/server-context.ts index be1a80ae7..fa6f5ac3a 100644 --- a/src/browser/server-context.ts +++ b/src/browser/server-context.ts @@ -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; }