can1357/oh-my-pi · error · ToolError
app.surface must be a surface UUID (e.g. CMUX_SURFACE_ID), n
Error message
app.surface must be a surface UUID (e.g. CMUX_SURFACE_ID), not a 'surface:N' ref; omit it to open a new split
What it means
When opening a cmux-attached tab, the resolved surface identifier must be a raw surface UUID. If it starts with the legacy 'surface:' ref prefix, the library refuses it because the cmux API expects a UUID in app.surface (e.g. CMUX_SURFACE_ID). Omitting app.surface opens a new split instead.
Source
Thrown at packages/coding-agent/src/tools/browser/tab-supervisor.ts:425
ownerSessionId: opts.ownerSessionId,
};
worker.onMessage(msg => handleTabMessage(tab, msg));
tabs.set(name, tab);
// Durably record ownership so another live omp process can reap this page if
// this process dies abnormally before its own teardown closes the tab.
const scope = sharedScopeOf(browser);
if (scope) void recordSharedTarget(scope, info.targetId);
return { tab, created: true };
}
async function acquireCmuxTab(
name: string,
browser: CmuxBrowserHandle,
opts: AcquireTabOptions,
): 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;View on GitHub (pinned to 9690622007)
Solutions
- Pass the raw UUID (value of CMUX_SURFACE_ID) as the surface instead of a 'surface:N' ref.
- Omit the surface option entirely so the call opens a new split.
- Convert at the boundary: strip/resolve the 'surface:' prefix to its UUID before calling the tool.
- Update stored configuration/scripts that still record surface refs in the old format.
Example fix
// before
await browserTool({ action: "open", cmuxSurface: "surface:3" });
// after
await browserTool({ action: "open", cmuxSurface: process.env.CMUX_SURFACE_ID }); // raw UUID Defensive patterns
Strategy: validation
Validate before calling
const surface = opts.cmuxSurface ?? process.env.CMUX_SURFACE_ID;
if (surface && surface.startsWith("surface:")) {
throw new Error(`Pass the surface UUID, not '${surface}'; omit to open a new split`);
} Type guard
function isSurfaceUuid(v: string): boolean {
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(v);
} Prevention
- Always source surfaces from CMUX_SURFACE_ID (UUID), never from UI 'surface:N' refs
- Validate format before passing: reject values containing 'surface:'
- Prefer omitting the surface when a new split is acceptable
When it happens
Trigger: Calling browser open with cmuxSurface set (or a browser handle whose .surface carries) a value like 'surface:3' instead of the UUID that CMUX_SURFACE_ID exposes.
Common situations: Copying a surface ref from cmux UI/logs instead of reading the CMUX_SURFACE_ID env var; stale code passing the old 'surface:N' format after a cmux API change; hardcoding a ref captured from a previous session.
Related errors
- oldText must not be empty.
- At least one output ID is required
- Unrecognized source format. Did you mean './${source}' (loca
- launch program resolves to a directory: ${displayPath}. Pass
- invalid mask ${mask}
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/f56a6a34a99fa190.
Report an issue: GitHub.