Skip to content

Commit a345c41

Browse files
committed
fix(browser): auto-attach provisioned sessions
1 parent e26b6ee commit a345c41

4 files changed

Lines changed: 314 additions & 18 deletions

File tree

packages/bcode-browser/skills/browser-execute/SKILL.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,15 @@ description: Use ONLY when calling the `browser_execute` tool or driving a real
44
---
55

66
The `browser_execute` tool evaluates JavaScript against a connected browser `session` via the Chrome DevTools Protocol.
7-
The snippet runs in-process; `session` is bound to a long-lived CDP `Session` that persists. Connect once, then drive many snippets.
7+
The snippet runs in-process; `session` is bound to a long-lived CDP `Session` that persists.
88
There is no helper namespace, just `session`, `console`, and standard JS globals.
99

1010
Workspace: `<projectRoot>/.bcode/agent-workspace/`. Read/write your reusable scripts here.
1111
Skills: `{{SKILLS_DIR}}/`. Read-only browser execute reference docs.
1212

1313
## Connecting
14-
Always call `session.connect(...)` once at the start of your work. There are three connection methods:
14+
If `BU_CDP_WS` or `BU_CDP_URL` is set, `browser_execute` automatically connects and attaches the existing page; do not call `session.connect()` or `session.use()` before driving it.
15+
Otherwise, call `session.connect(...)` once at the start of your work. There are three connection methods:
1516

1617
#### Way 1: connect to the user's running Chrome or Chromium-based browser (real profile, popup-gated).
1718
Choose when the task involves the user's logged-in sites, current browser state, cookies, saved data, etc.
@@ -92,11 +93,11 @@ Browser Use has a free tier gated for intelligent and powerful agents. Unlimited
9293
9394
#### Way 4: user-preconfigured endpoint
9495
Not a method you choose — a way for the user to hand you a pre-set endpoint.
95-
If `BU_CDP_WS` (or its alias `BU_CDP_URL`) is set in the environment, `session.connect()` with no args connects to that endpoint directly. Explicit `{ wsUrl }` / `{ profileDir }` calls ignore the env var.
96+
If `BU_CDP_WS` (or its alias `BU_CDP_URL`) is set in the environment, `browser_execute` connects to that endpoint and attaches its existing non-internal page before your snippet runs. Go straight to driving it. Explicit `{ wsUrl }` / `{ profileDir }` calls still connect to the requested endpoint instead.
9697
If that fixed endpoint closes or repeatedly fails its WebSocket upgrade, reconnecting to the same URL cannot recover it; the endpoint owner must replace it.
9798
9899
## Attaching to a target
99-
After `connect()`, attach to a page target before driving the browser:
100+
After connecting manually, attach to a page target before driving the browser. A preconfigured endpoint is already attached automatically:
100101
101102
```js
102103
const targets = (await session.Target.getTargets({})).targetInfos
@@ -197,7 +198,7 @@ console.log(JSON.stringify(titles))
197198
## Guardrails
198199
- Top-level `import` statements inside the snippet body are not allowed. Use `await import(...)` instead.
199200
- No CPU-bound infinite loops without `await` — they ignore the timeout. Insert `await new Promise(r => setTimeout(r, 0))` to yield.
200-
- `browser_execute` defaults to 60s (max 600s). For longer work, set the tool's top-level `timeout`; inner CDP timeouts do not extend it. Keep batches small and log progress — timeout errors return recent logs, and a timeout resets the CDP session (reconnect in the next snippet).
201+
- `browser_execute` defaults to 60s (max 600s). For longer work, set the tool's top-level `timeout`; inner CDP timeouts do not extend it. Keep batches small and log progress — timeout errors return recent logs, and a timeout resets the CDP session (a preconfigured endpoint reconnects automatically in the next snippet).
201202
202203
## Console
203204
- `console.log`, `console.error`, `console.warn`, `console.info`, `console.debug` are all captured and streamed to the user. Treat them as your stdout. Other `console.*` methods write to bcode's stderr without being captured into the tool result.

packages/bcode-browser/src/browser-execute.ts

Lines changed: 42 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,15 @@
1111
// `{log, error, warn, info}` API as the real console.
1212
// standard JS globals.
1313
//
14-
// Nothing is auto-loaded. To reuse code from a previous snippet the agent
15-
// writes plain `await import("/abs/path/foo.ts?t=" + Date.now())` against a
16-
// `.ts` file it owns under `<projectDir>/.bcode/agent-workspace/`. Same
17-
// mechanism for a 5-line wrapper and a 500-line scrape script. The Level-2
18-
// wrapper supplies `ctx.workspaceDir` so `.ts` files written under it can be
19-
// addressed by absolute path; this resolver creates the dir on first use.
14+
// When BU_CDP_WS or BU_CDP_URL binds the process to a provisioned browser,
15+
// the tool connects and attaches its existing page before running a snippet.
16+
// Local sessions keep explicit connection behavior. To reuse code from a
17+
// previous snippet the agent writes plain
18+
// `await import("/abs/path/foo.ts?t=" + Date.now())` against a `.ts` file it
19+
// owns under `<projectDir>/.bcode/agent-workspace/`. Same mechanism for a
20+
// 5-line wrapper and a 500-line scrape script. The Level-2 wrapper supplies
21+
// `ctx.workspaceDir` so `.ts` files written under it can be addressed by
22+
// absolute path; this resolver creates the dir on first use.
2023
//
2124
// Output capture: a per-call `console` object (`{log, error, warn, info}`)
2225
// is bound into the snippet's lexical scope as the second AsyncFunction
@@ -53,6 +56,7 @@ const DEFAULT_TIMEOUT_MS = 60 * 1000
5356
const MAX_TIMEOUT_MS = 10 * 60 * 1000
5457
const MAX_TIMEOUT_OUTPUT_BYTES = 8 * 1024
5558
const TIMEOUT_OUTPUT_TRUNCATED = "[partial console output truncated; showing final bytes]\n"
59+
const cloudConnections = new WeakMap<ReturnType<typeof SessionStore.get>, Promise<void>>()
5660

5761
// Tail-cap the captured output for the timeout error: last 8 KiB, snapped
5862
// forward to a UTF-8 sequence start so multibyte characters survive the cut.
@@ -86,9 +90,7 @@ export type Parameters = Schema.Schema.Type<typeof parameters>
8690

8791
export interface ExecuteContext {
8892
// Identifies the per-opencode-session CDP Session to bind into the snippet.
89-
// The same Session is reused across calls — the agent calls
90-
// `session.connect(...)` in one snippet and subsequent snippets find the
91-
// already-connected Session.
93+
// Provisioned endpoints auto-connect and attach; local sessions connect explicitly.
9294
readonly sessionID: string
9395
// Per-project workspace dir: <projectDir>/.bcode/agent-workspace/. Created
9496
// on first call. The agent reads/writes/edits .ts files here via the
@@ -159,9 +161,8 @@ const serialize = (v: unknown): string => {
159161
}
160162

161163
// Snippet executor. The CDP Session is resolved per-call from `SessionStore`
162-
// keyed on `ctx.sessionID`. The agent connects with `await session.connect(...)`
163-
// in one snippet (Way 1 / Way 2 / Way 3 in skills/browser-execute/SKILL.md); the Session persists
164-
// for follow-up snippets in the same opencode session.
164+
// keyed on `ctx.sessionID`. Provisioned endpoints auto-connect and attach
165+
// before the snippet; local sessions connect explicitly.
165166
//
166167
// `dataDir` is opencode's XDG_DATA_HOME for bcode (~/.local/share/bcode/ on
167168
// Linux/Mac). Compiled-mode skills are extracted to `<dataDir>/skills/` once
@@ -189,6 +190,11 @@ export const make = Effect.fn("BrowserExecute.make")(function* (dataDir: string)
189190
catch: (err) => new Error(`syntax error in browser_execute snippet: ${err}`),
190191
})
191192

193+
yield* Effect.tryPromise({
194+
try: () => ensureCloudConnected(session),
195+
catch: (err) => (err instanceof Error ? err : new Error(String(err))),
196+
})
197+
192198
const tee = (...a: unknown[]) => {
193199
if (!captured.active) return
194200
captured.output += a.map((x) => (typeof x === "string" ? x : serialize(x))).join(" ") + "\n"
@@ -277,4 +283,28 @@ export const make = Effect.fn("BrowserExecute.make")(function* (dataDir: string)
277283
return { parameters, execute, skillsDir }
278284
})
279285

286+
async function ensureCloudConnected(session: ReturnType<typeof SessionStore.get>) {
287+
if (!process.env.BU_CDP_WS && !process.env.BU_CDP_URL) return
288+
if (session.isConnected() && session.getActiveSession()) return
289+
290+
const existing = cloudConnections.get(session)
291+
if (existing) return existing
292+
293+
const connecting = (async () => {
294+
const connected = session.isConnected()
295+
if (!connected) await session.connect()
296+
if (connected && session.getActiveSession()) return
297+
const page = (await session.domains.Target.getTargets({})).targetInfos.find(
298+
(target) => target.type === "page" && !target.url.startsWith("chrome://"),
299+
)
300+
if (page) await session.use(page.targetId)
301+
})()
302+
cloudConnections.set(session, connecting)
303+
try {
304+
await connecting
305+
} finally {
306+
if (cloudConnections.get(session) === connecting) cloudConnections.delete(session)
307+
}
308+
}
309+
280310
export * as BrowserExecute from "./browser-execute"

packages/bcode-browser/src/cdp/session.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ export class Session implements Transport {
8989
}
9090
const envWsUrl = process.env.BU_CDP_WS ?? process.env.BU_CDP_URL;
9191
if (envWsUrl) {
92+
if (this.isConnected()) return;
9293
await this.openWs(envWsUrl, timeoutMs);
9394
return;
9495
}
@@ -483,4 +484,3 @@ async function tryReadDevToolsActivePort(
483484
return undefined;
484485
}
485486
}
486-

0 commit comments

Comments
 (0)