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
- Upgrade/verify the cmux daemon supports browser.open_split returning surface_id.
- Check CMUX_WORKSPACE_ID / CMUX_SURFACE_ID env vars point at the correct workspace.
- Attach to an existing surface by passing the surface UUID explicitly instead of requesting a new split.
- 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
- Keep the cmux daemon updated to a version returning surface_id
- Attach to an existing CMUX_SURFACE_ID instead of requesting new splits when possible
- Set CMUX_WORKSPACE_ID correctly so the split request lands in a valid workspace
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
- Replacement text is not valid UTF-8: {err}
- invalid glob `{pattern}`: {error}
- RPC chunk received before protocol negotiation
- RPC protocol v2 negotiation failed
- RPC chunk exceeded the transport limit
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/4251be1f6d892242.
Report an issue: GitHub.