fix(browser): recover stale remote target ids

Co-authored-by: Ilya Strelov <10761735+strelov1@users.noreply.github.com>
This commit is contained in:
Peter Steinberger
2026-02-22 18:19:51 +01:00
parent 1fe2043742
commit 9ea5228f42
3 changed files with 56 additions and 2 deletions

View File

@@ -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.

View File

@@ -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" },

View File

@@ -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;
}