From 41bc66b5e5143cdc316761b17366257725023a4f Mon Sep 17 00:00:00 2001 From: Daniel Sticker Date: Mon, 9 Nov 2026 11:05:31 +0100 Subject: [PATCH 1/2] fix(runtime): Hint at extension reconnect delays --- CHANGELOG.md | 10 +++++++- compatibility/manifest.json | 10 +++++++- docs/troubleshooting.md | 2 ++ package-lock.json | 4 ++-- package.json | 2 +- plugin/.claude-plugin/plugin.json | 2 +- plugin/skills/setup/SKILL.md | 2 +- src/runtime/server.ts | 8 +++++-- src/runtime/tool-errors.ts | 33 +++++++++++++++++++++------ test/latest.test.mjs | 38 ++++++++++++++++++++++--------- 10 files changed, 84 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c74d39..857b047 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), ## [Unreleased] +## [0.4.1] - 2026-11-09 + +### Added + +- Compatibility manifest entry for ChatGPT.app 26.1103.60933, verified end to end against Helium +- `run` appends a retry hint to `Browser is not available` and `No browser is available` errors. The browser extension needs up to 30 seconds to reconnect after a browser start, an app restart or a wake from sleep. + ## [0.4.0] - 2026-11-04 ### Added @@ -91,7 +98,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), - Disabled-by-default Claude Code plugin with setup, doctor, and browser skills - stderr secret redaction and strict stdout protocol purity -[Unreleased]: https://github.com/stickerdaniel/browserjack/compare/v0.4.0...HEAD +[Unreleased]: https://github.com/stickerdaniel/browserjack/compare/v0.4.1...HEAD +[0.4.1]: https://github.com/stickerdaniel/browserjack/compare/v0.4.0...v0.4.1 [0.4.0]: https://github.com/stickerdaniel/browserjack/compare/v0.3.3...v0.4.0 [0.3.3]: https://github.com/stickerdaniel/browserjack/compare/v0.3.2...v0.3.3 [0.3.2]: https://github.com/stickerdaniel/browserjack/compare/v0.3.1...v0.3.2 diff --git a/compatibility/manifest.json b/compatibility/manifest.json index da2a464..c5e9d8a 100644 --- a/compatibility/manifest.json +++ b/compatibility/manifest.json @@ -1,6 +1,6 @@ { "schemaVersion": 1, - "generatedAt": "2026-11-04", + "generatedAt": "2026-11-09", "entries": [ { "bundleId": "com.openai.codex", @@ -33,6 +33,14 @@ "teamId": "2DC432GLL2", "architectures": ["arm64"], "browserClientSha256": "6704e0f3b0c94d3e041a22488550d4c29e6252a934163d0aa539a7d947d5d17b" + }, + { + "bundleId": "com.openai.codex", + "appVersion": "26.1103.60933", + "pluginVersion": "26.1103.60933", + "teamId": "2DC432GLL2", + "architectures": ["arm64"], + "browserClientSha256": "becbcc75697f519cad57e002267f14581dd759d235021411ed61637b961ce469" } ] } diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 88b48c4..d34f269 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -28,6 +28,8 @@ The installed shim pins the exact Node.js executable used at install time. A Hom `doctor --live` verifies that the runtime starts and that `agent.browsers.list()` works; zero connected backends is not a runtime failure. Make sure the ChatGPT/Codex extension is installed and enabled in the browser profile you intend to use, and that ChatGPT.app is running. +A tool call then fails with `Browser is not available: chrome` or `No browser is available`. The extension reconnects to ChatGPT.app on its own: it retries every 5 seconds and, while its service worker sleeps, every 30 seconds. Right after the browser starts, ChatGPT.app restarts, or the Mac wakes, the first call can fail before that reconnect. `run` appends a hint to these errors that tells the agent to retry for up to 30 seconds. + ## The browser asks for site or automation permission OpenAI's extension has its own permission layer, independent of Claude Code's permission mode. When a site prompts, choose `Allow for this site` to persist an origin-specific entry. Avoid `Allow for all sites` or any global never-ask setting; Browserjack does not enable those and you should not either. diff --git a/package-lock.json b/package-lock.json index 0419d81..2bfdcfc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "browserjack", - "version": "0.4.0", + "version": "0.4.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "browserjack", - "version": "0.4.0", + "version": "0.4.1", "cpu": [ "arm64" ], diff --git a/package.json b/package.json index 92d7e6b..3c40e66 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "browserjack", - "version": "0.4.0", + "version": "0.4.1", "description": "A local macOS MCP bridge that lets Claude Code and other MCP clients reuse OpenAI's installed Codex browser runtime.", "keywords": [ "browser-automation", diff --git a/plugin/.claude-plugin/plugin.json b/plugin/.claude-plugin/plugin.json index 6a6a5fc..e7b60ea 100644 --- a/plugin/.claude-plugin/plugin.json +++ b/plugin/.claude-plugin/plugin.json @@ -2,7 +2,7 @@ "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", "name": "browserjack", "displayName": "Browserjack", - "version": "0.4.0", + "version": "0.4.1", "description": "Use OpenAI's locally installed Codex browser runtime from Claude Code (macOS)", "author": { "name": "Daniel Sticker" diff --git a/plugin/skills/setup/SKILL.md b/plugin/skills/setup/SKILL.md index 84206d7..f7c4239 100644 --- a/plugin/skills/setup/SKILL.md +++ b/plugin/skills/setup/SKILL.md @@ -8,7 +8,7 @@ Explain the trust boundary before installation: the bridge can control authentic For a published release, install an exact reviewed version: ```bash -npx --yes browserjack@0.4.0 setup --client plugin --scope user +npx --yes browserjack@0.4.1 setup --client plugin --scope user ``` For a source checkout, run: diff --git a/src/runtime/server.ts b/src/runtime/server.ts index f220273..f40be91 100644 --- a/src/runtime/server.ts +++ b/src/runtime/server.ts @@ -12,7 +12,7 @@ import { createRuntimeLaunch } from "./launch.js"; import { readLines } from "./lines.js"; import { createTurnState, injectTurnMetadata } from "./metadata.js"; import { RedactingTransform } from "./redact.js"; -import { annotateToolError } from "./tool-errors.js"; +import { annotateToolError, noConnectedBrowserNotice } from "./tool-errors.js"; const MAX_MCP_LINE_BYTES = 16 * 1024 * 1024; @@ -70,10 +70,14 @@ async function forwardChildOutput( browserClientUrl: string, outdatedAppNotice: () => Promise, ): Promise { + const notices = async (errorText: string): Promise => + [noConnectedBrowserNotice(errorText), await outdatedAppNotice()].filter( + (notice) => notice !== null, + ); for await (const line of readLines(output, MAX_MCP_LINE_BYTES)) { await writeWithBackpressure( process.stdout, - (await annotateToolError(line, outdatedAppNotice)) ?? + (await annotateToolError(line, notices)) ?? addBrowserRuntimeInstructions(line, browserClientUrl), ); } diff --git a/src/runtime/tool-errors.ts b/src/runtime/tool-errors.ts index fb6fd41..1aaed6f 100644 --- a/src/runtime/tool-errors.ts +++ b/src/runtime/tool-errors.ts @@ -1,11 +1,24 @@ import { isJsonObject } from "../lib/json.js"; -// Appends a Browserjack notice to a failed tools/call result, so the agent sees -// the likely cause next to OpenAI's error text. Resolves to null for every other -// line and when there is no notice, and only then looks the notice up. +// OpenAI's browser service throws these while no browser extension is connected +// to ChatGPT.app, for example `Browser is not available: chrome`. +const NO_CONNECTED_BROWSER = /Browser is not available: |No browser is available/; + +// The extension retries its native-host connection every 5 seconds and, while +// its service worker sleeps, on a 30-second alarm. +export function noConnectedBrowserNotice(errorText: string): string | null { + return NO_CONNECTED_BROWSER.test(errorText) + ? "The requested browser has no extension connected to ChatGPT.app yet. The extension reconnects within 30 seconds after the browser starts, ChatGPT.app restarts or the Mac wakes. Retry the same call every 5 seconds for up to 30 seconds. If it still fails, confirm that the browser is running and the ChatGPT extension is enabled in it." + : null; +} + +// Appends Browserjack notices to a failed tools/call result, so the agent sees +// the likely cause next to OpenAI's error text. `notices` receives the result's +// joined text and runs only for failed results. Resolves to null for every +// other line and when there are no notices. export async function annotateToolError( line: string, - notice: () => Promise, + notices: (errorText: string) => Promise, ): Promise { let message: unknown; try { @@ -21,10 +34,16 @@ export async function annotateToolError( ) { return null; } - const text = await notice(); - if (text === null) { + const errorText = message.result.content + .map((item) => (isJsonObject(item) && typeof item.text === "string" ? item.text : "")) + .join("\n"); + const texts = await notices(errorText); + if (texts.length === 0) { return null; } - const content = [...message.result.content, { type: "text", text: `browserjack: ${text}` }]; + const content = [ + ...message.result.content, + ...texts.map((text) => ({ type: "text", text: `browserjack: ${text}` })), + ]; return `${JSON.stringify({ ...message, result: { ...message.result, content } })}\n`; } diff --git a/test/latest.test.mjs b/test/latest.test.mjs index ccf4141..476148b 100644 --- a/test/latest.test.mjs +++ b/test/latest.test.mjs @@ -2,7 +2,7 @@ import assert from "node:assert/strict"; import test from "node:test"; import { createOutdatedAppCheck, isOlderVersion, parseNewestBuild } from "../dist/compat/latest.js"; -import { annotateToolError } from "../dist/runtime/tool-errors.js"; +import { annotateToolError, noConnectedBrowserNotice } from "../dist/runtime/tool-errors.js"; // Trimmed from OpenAI's appcast on 2026-11-04; the first item is cut down to // its delta enclosure to exercise the full-zip requirement. @@ -21,15 +21,16 @@ const appcast = ` `; +function failedResult(text) { + return JSON.stringify({ + jsonrpc: "2.0", + id: 3, + result: { content: [{ type: "text", text }], isError: true }, + }); +} + // The js tool result a goto produced on ChatGPT.app 26.1021 with extension 1.26.1102. -const failedToolLine = JSON.stringify({ - jsonrpc: "2.0", - id: 3, - result: { - content: [{ type: "text", text: "Cannot read properties of undefined (reading 'timeoutMs')" }], - isError: true, - }, -}); +const failedToolLine = failedResult("Cannot read properties of undefined (reading 'timeoutMs')"); test("takes the newest feed item that has a full arm64 zip", () => { assert.deepEqual(parseNewestBuild(appcast), { @@ -87,7 +88,7 @@ test("reads the feed once per day and retries a failure after ten minutes", asyn }); test("appends the notice to a failed tool result", async () => { - const line = await annotateToolError(failedToolLine, async () => "Update ChatGPT.app."); + const line = await annotateToolError(failedToolLine, async () => ["Update ChatGPT.app."]); const { result } = JSON.parse(line ?? "null"); assert.equal(result.isError, true); assert.deepEqual(result.content.at(-1), { @@ -107,5 +108,20 @@ test("leaves successful results alone without looking up a notice", async () => }); assert.equal(await annotateToolError(success, unexpectedNotice), null); assert.equal(await annotateToolError("not json", unexpectedNotice), null); - assert.equal(await annotateToolError(failedToolLine, async () => null), null); + assert.equal(await annotateToolError(failedToolLine, async () => []), null); +}); + +test("tells the agent to retry while no browser extension is connected", async () => { + // The js tool result on ChatGPT.app 26.1103 while Helium's extension had not reconnected. + const unavailable = failedResult("Browser is not available: chrome"); + const line = await annotateToolError(unavailable, async (errorText) => + [noConnectedBrowserNotice(errorText)].filter((notice) => notice !== null), + ); + const { result } = JSON.parse(line ?? "null"); + assert.match( + result.content.at(-1).text, + /^browserjack: The requested browser has no extension connected/, + ); + assert.notEqual(noConnectedBrowserNotice("No browser is available"), null); + assert.equal(noConnectedBrowserNotice("Cannot read properties of undefined"), null); }); From b9b22f88af1ae5e8271447dd2dcd3977eecab7ba Mon Sep 17 00:00:00 2001 From: Daniel Sticker Date: Mon, 9 Nov 2026 11:19:22 +0100 Subject: [PATCH 2/2] fix(runtime): Clarify browser discovery errors --- CHANGELOG.md | 2 +- docs/troubleshooting.md | 2 +- src/runtime/server.ts | 4 +-- src/runtime/tool-errors.ts | 19 +++++----- test/latest.test.mjs | 73 +++++++++++++++++++++++++++++++------- 5 files changed, 74 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 857b047..d956641 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), ### Added - Compatibility manifest entry for ChatGPT.app 26.1103.60933, verified end to end against Helium -- `run` appends a retry hint to `Browser is not available` and `No browser is available` errors. The browser extension needs up to 30 seconds to reconnect after a browser start, an app restart or a wake from sleep. +- `run` explains unavailable-browser errors without treating them as proof of an incompatible ChatGPT.app release or replaying failed tool calls. ## [0.4.0] - 2026-11-04 diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index d34f269..7851248 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -28,7 +28,7 @@ The installed shim pins the exact Node.js executable used at install time. A Hom `doctor --live` verifies that the runtime starts and that `agent.browsers.list()` works; zero connected backends is not a runtime failure. Make sure the ChatGPT/Codex extension is installed and enabled in the browser profile you intend to use, and that ChatGPT.app is running. -A tool call then fails with `Browser is not available: chrome` or `No browser is available`. The extension reconnects to ChatGPT.app on its own: it retries every 5 seconds and, while its service worker sleeps, every 30 seconds. Right after the browser starts, ChatGPT.app restarts, or the Mac wakes, the first call can fail before that reconnect. `run` appends a hint to these errors that tells the agent to retry for up to 30 seconds. +`Browser is not available: chrome` means the runtime could not find the requested browser. `No browser is available` means discovery found no usable backend. A disconnected extension or an unavailable browser ID can cause these errors; they do not by themselves establish that ChatGPT.app is incompatible. `run` appends this distinction to failed tool results. It does not replay tool calls, which may already have performed actions before failing. ## The browser asks for site or automation permission diff --git a/src/runtime/server.ts b/src/runtime/server.ts index f40be91..1f610b5 100644 --- a/src/runtime/server.ts +++ b/src/runtime/server.ts @@ -12,7 +12,7 @@ import { createRuntimeLaunch } from "./launch.js"; import { readLines } from "./lines.js"; import { createTurnState, injectTurnMetadata } from "./metadata.js"; import { RedactingTransform } from "./redact.js"; -import { annotateToolError, noConnectedBrowserNotice } from "./tool-errors.js"; +import { annotateToolError, browserUnavailableNotice } from "./tool-errors.js"; const MAX_MCP_LINE_BYTES = 16 * 1024 * 1024; @@ -71,7 +71,7 @@ async function forwardChildOutput( outdatedAppNotice: () => Promise, ): Promise { const notices = async (errorText: string): Promise => - [noConnectedBrowserNotice(errorText), await outdatedAppNotice()].filter( + [browserUnavailableNotice(errorText), await outdatedAppNotice()].filter( (notice) => notice !== null, ); for await (const line of readLines(output, MAX_MCP_LINE_BYTES)) { diff --git a/src/runtime/tool-errors.ts b/src/runtime/tool-errors.ts index 1aaed6f..f623552 100644 --- a/src/runtime/tool-errors.ts +++ b/src/runtime/tool-errors.ts @@ -1,14 +1,13 @@ import { isJsonObject } from "../lib/json.js"; -// OpenAI's browser service throws these while no browser extension is connected -// to ChatGPT.app, for example `Browser is not available: chrome`. -const NO_CONNECTED_BROWSER = /Browser is not available: |No browser is available/; +// OpenAI reports an unavailable browser when discovery cannot match its ID or +// family. The error alone says nothing about app/runtime compatibility. +const NO_AVAILABLE_BROWSER = + /^(?:Error: )?(?:Browser is not available: [^\r\n]+|No browser is available)$/m; -// The extension retries its native-host connection every 5 seconds and, while -// its service worker sleeps, on a 30-second alarm. -export function noConnectedBrowserNotice(errorText: string): string | null { - return NO_CONNECTED_BROWSER.test(errorText) - ? "The requested browser has no extension connected to ChatGPT.app yet. The extension reconnects within 30 seconds after the browser starts, ChatGPT.app restarts or the Mac wakes. Retry the same call every 5 seconds for up to 30 seconds. If it still fails, confirm that the browser is running and the ChatGPT extension is enabled in it." +export function browserUnavailableNotice(errorText: string): string | null { + return NO_AVAILABLE_BROWSER.test(errorText) + ? "The runtime could not find the requested browser. A disconnected extension or an unavailable browser ID can cause this error. This error alone does not establish that ChatGPT.app is incompatible." : null; } @@ -35,7 +34,9 @@ export async function annotateToolError( return null; } const errorText = message.result.content - .map((item) => (isJsonObject(item) && typeof item.text === "string" ? item.text : "")) + .map((item) => + isJsonObject(item) && item.type === "text" && typeof item.text === "string" ? item.text : "", + ) .join("\n"); const texts = await notices(errorText); if (texts.length === 0) { diff --git a/test/latest.test.mjs b/test/latest.test.mjs index 476148b..c5bc530 100644 --- a/test/latest.test.mjs +++ b/test/latest.test.mjs @@ -2,7 +2,7 @@ import assert from "node:assert/strict"; import test from "node:test"; import { createOutdatedAppCheck, isOlderVersion, parseNewestBuild } from "../dist/compat/latest.js"; -import { annotateToolError, noConnectedBrowserNotice } from "../dist/runtime/tool-errors.js"; +import { annotateToolError, browserUnavailableNotice } from "../dist/runtime/tool-errors.js"; // Trimmed from OpenAI's appcast on 2026-11-04; the first item is cut down to // its delta enclosure to exercise the full-zip requirement. @@ -111,17 +111,64 @@ test("leaves successful results alone without looking up a notice", async () => assert.equal(await annotateToolError(failedToolLine, async () => []), null); }); -test("tells the agent to retry while no browser extension is connected", async () => { - // The js tool result on ChatGPT.app 26.1103 while Helium's extension had not reconnected. - const unavailable = failedResult("Browser is not available: chrome"); - const line = await annotateToolError(unavailable, async (errorText) => - [noConnectedBrowserNotice(errorText)].filter((notice) => notice !== null), - ); - const { result } = JSON.parse(line ?? "null"); - assert.match( - result.content.at(-1).text, - /^browserjack: The requested browser has no extension connected/, +test("annotates unavailable browsers without assuming an extension connection", async () => { + // The first result was captured from ChatGPT.app 26.1103 with an unavailable Edge backend. + for (const error of [ + "Browser is not available: edge", + "Browser is not available: chrome", + "No browser is available", + "Browser is not available: dot", + "Browser is not available: 42", + ]) { + const line = await annotateToolError(failedResult(error), async (errorText) => + [browserUnavailableNotice(errorText)].filter((notice) => notice !== null), + ); + const { result } = JSON.parse(line ?? "null"); + assert.equal(result.isError, true); + assert.deepEqual(result.content[0], { type: "text", text: error }); + assert.match( + result.content.at(-1).text, + /^browserjack: The runtime could not find the requested browser/, + ); + } + assert.equal(browserUnavailableNotice("Cannot read properties of undefined"), null); + assert.equal(browserUnavailableNotice("A page says Browser is not available: chrome"), null); +}); + +test("reads only text content and preserves mixed content and metadata", async () => { + const message = { + jsonrpc: "2.0", + id: 3, + result: { + isError: true, + _meta: { "codex/telemetry": { error_code: "browser_error" } }, + content: [ + { type: "text", text: "Browser is not available: edge" }, + { type: "text", text: 'Reported browsers: [{"family":"chrome","id":"1"}]' }, + { type: "image", mimeType: "image/png", data: "AA==" }, + { type: "resource", resource: { uri: "test:log", text: "resource text" } }, + null, + ], + }, + }; + const notices = ["Diagnostic", "Update notice"]; + const output = JSON.parse( + await annotateToolError(JSON.stringify(message), async (text) => { + assert.equal( + text, + 'Browser is not available: edge\nReported browsers: [{"family":"chrome","id":"1"}]\n\n\n', + ); + return notices; + }), ); - assert.notEqual(noConnectedBrowserNotice("No browser is available"), null); - assert.equal(noConnectedBrowserNotice("Cannot read properties of undefined"), null); + assert.deepEqual(output, { + ...message, + result: { + ...message.result, + content: [ + ...message.result.content, + ...notices.map((text) => ({ type: "text", text: `browserjack: ${text}` })), + ], + }, + }); });