can1357/oh-my-pi · error · ToolError

cmux browser.open_split did not return a surface_id

Error message

cmux browser.open_split did not return a surface_id

What it means

For cmux tabs with no pre-attached surface, the library calls the cmux 'browser.open_split' JSON-RPC method and expects the response to carry a non-empty surface_id string. If the response lacks it, the library throws ToolError because it cannot address or track the created split without an identifier.

Source

Thrown at packages/coding-agent/src/tools/browser/tab-supervisor.ts:440

): Promise<AcquireTabResult> {
	const attachedSurface = opts.cmuxSurface ?? browser.surface;
	if (attachedSurface?.startsWith("surface:")) {
		throw new ToolError(
			"app.surface must be a surface UUID (e.g. CMUX_SURFACE_ID), not a 'surface:N' ref; omit it to open a new split",
		);
	}

	let surfaceId = attachedSurface;
	let initialUrl = opts.url;
	let ownsSurface = false;
	try {
		if (!surfaceId) {
			const params: Record<string, unknown> = { url: opts.url ?? "about:blank", focus: false };
			if (process.env.CMUX_WORKSPACE_ID) params.workspace_id = process.env.CMUX_WORKSPACE_ID;
			if (process.env.CMUX_SURFACE_ID) params.surface_id = process.env.CMUX_SURFACE_ID;
			const result = await browser.client.request("browser.open_split", params, { timeoutMs: opts.timeoutMs });
			if (typeof result.surface_id !== "string" || result.surface_id.length === 0) {
				throw new ToolError("cmux browser.open_split did not return a surface_id");
			}
			surfaceId = result.surface_id;
			ownsSurface = true;
			if (typeof result.url === "string" && result.url.length > 0) initialUrl = result.url;
			if (opts.url) {
				await browser.client.request(
					"browser.wait",
					{
						surface_id: surfaceId,
						load_state: mapWaitUntil(opts.waitUntil ?? "load"),
						timeout_ms: opts.timeoutMs,
					},
					{ timeoutMs: opts.timeoutMs },
				);
			}
		}

		const cmuxTab = new CmuxTab({ client: browser.client, surfaceId, url: initialUrl });

View on GitHub (pinned to 9690622007)

Solutions

  1. Upgrade/verify the cmux daemon supports browser.open_split returning surface_id.
  2. Check CMUX_WORKSPACE_ID / CMUX_SURFACE_ID env vars point at the correct workspace.
  3. Attach to an existing surface by passing the surface UUID explicitly instead of requesting a new split.
  4. Retry the call — a transient daemon error may have produced a truncated response.

Example fix

// before
const tab = await browserTool({ action: "open" }); // daemon returns {} -> error
// after
await browserTool({ action: "open", cmuxSurface: process.env.CMUX_SURFACE_ID }); // attach to known UUID
Defensive patterns

Strategy: retry

Validate before calling

// verify cmux daemon is reachable and current
const pong = await browser.client.request("ping", {}, { timeoutMs: 5000 });

Type guard

function hasSurfaceId(r: unknown): r is { surface_id: string } {
  return typeof (r as any)?.surface_id === "string" && (r as any).surface_id.length > 0;
}

Try / catch

try {
  await openCmuxSplit();
} catch (e) {
  if (/did not return a surface_id/.test(e.message)) {
    await restartCmuxDaemon();
    await openCmuxSplit(); // retry once
  } else throw e;
}

Prevention

When it happens

Trigger: The cmux host returns a malformed/empty result from browser.open_split — e.g. an outdated cmux server, wrong RPC surface, or the workspace/surface parameters being rejected silently.

Common situations: Running against a cmux daemon version that predates surface_id in open_split responses; connecting to a non-cmux endpoint that answers the method with a bare ack; network proxy stripping response fields.

Related errors


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/4251be1f6d892242. Report an issue: GitHub.