Repository navigation
v2.2.0
Several agent PROCESSES can now drive tabs of ONE user-launched Firefox at the same time. Firefox serves exactly one WebDriver BiDi session per browser (measured on 153.0.3: a second session.new gets "Maximum number of active sessions", a second WebSocket to /session/<id> is refused at the HTTP upgrade). Previously a second cdp-toolkit process attaching to the same Firefox was refused after the session-slot wait; now the process that wins the slot fronts the one real session with a loopback BiDi multiplexer, and every other process joins it instead of waiting. Measured (bun run firefox:multi:smoke, headless Firefox 153.0.3, 4 agents × 6 rounds): before, 4 server processes attached to one Firefox — 1 of 4 completed, the other 3 refused after the session wait; after, all 4 completed concurrently (40 calls, wall 437ms vs serial 1375ms, 4 distinct processes in flight), a killed server process left 3 survivors finishing with 0 retries, and a killed daemon was replaced within the recovery path with every survivor finishing on its original tab via the stale-id hint (firefox:coord:smoke 11/11, bun test 929 pass). See docs/design/2026-09-04-firefox-multi-agent-session-mux.md.
Added
src/bidi/mux.ts— a zero-dependency loopback WebSocket server that speaks BiDi over one real upstream session: answerssession.new/end/subscribe/unsubscribe/statuslocally, forwards every other command on the single upstream connection, fans events out per client.src/bidi/mux-daemon.ts(built todist/bidi/mux-daemon.js, a fourthscripts/build.tsentrypoint) — a detached daemon that owns the real BiDi session and hosts the mux, so the session outlives every client process. The first process to attach an endpoint spawns it; every process, including the spawner, joins it. It exits on its own afterCDP_FIREFOX_MUX_IDLE_MS(default 15000ms) with no clients, when Firefox goes away, or onSIGTERM/SIGINT/SIGHUP. Diagnostics:<CDP_ARTIFACT_DIR or /tmp/cdp-toolkit>/ff-mux-<endpoint>.log.CDP_FIREFOX_MUX(off|host|daemon, defaultdaemonwhen attaching andhostwhen launching),CDP_FIREFOX_MUX_IDLE_MS(default15000),CDP_FIREFOX_MUX_DAEMON(absolute path override of the daemon script; tests only).- A stale-target-id hint. Firefox assigns new browsing-context ids whenever its BiDi session is re-created (measured: true even after a clean
session.end), so if the daemon dies and a new one takes over, ids issued before the handover are stale. The driver'sno-such-targeterror now names this and tells the agent to re-resolve the tab withlist_pages(url:/title:selectors) and claim it again. src/bidi/session-lease.ts:SessionSlotRecordgainsmuxEndpoint;acquireSessionSlotreturnsjoinMuximmediately when a live holder advertises one, instead of polling out toSessionSlotBusyError; newadvertiseMux()andmuxDaemonLogFile().- New live harness
test/firefox-multi-agent-smoke.ts(bun run firefox:multi:smoke): N agent PROCESSES each claim their own tab of one shared headless Firefox, navigate, write a marker, and run several rounds of isolated reads, so a call landing in another tab fails; every call is timestamped so concurrency is measured, not assumed. Also covers a SIGKILL of one server process (survivors finish with 0 retries) and a SIGKILL of the daemon itself (survivors hit the stale-target hint, re-resolve, and finish on the same tab). - New unit tests
test/bidi-mux.test.ts,test/mux-daemon.test.ts, plus additions totest/session-lease.test.tsandtest/session-coord.test.ts.
Changed
test/firefox-session-coord-smoke.ts: scenarios 1–4 are now pinned toCDP_FIREFOX_MUX=off(they test the no-mux refusal/orphan-recovery semantics specifically); a new scenario 5 covers the mux default, asserting a live holder is a detached daemon, a second process joins it instead of waiting, the daemon survives its spawner exiting, and a killed daemon's slot is recovered by the existing orphan-steal + Marionette force-clear path.
Fixed
evaluate_script.argsnow declaresitems: {}. It was the only array property intools/listwithout anitemsschema, and Gemini function declarations reject exactly that shape: a host that forwarded the full tool list to a Gemini model (opencode through an OpenRouter Gemini lane, 135 tools) got no response headers at all until the gateway timed the request out. Every other array in the listing already carrieditems; the livebun src/mcp.tstools/listnow shows none missing.