can1357/oh-my-pi · error · ToolError
${label} returned a Promise, but this surface evaluates sync
Error message
${label} returned a Promise, but this surface evaluates synchronously and cannot await it — return a plain value (poll with waitForFunction for async state instead) What it means
The cmux browser eval wrapper runs page scripts through a synchronous envelope (serializeEvalWithEnvelope). When the evaluated function returns a Promise, the daemon cannot serialize/await it, so the envelope flags it with __ompPromise and unwrapEvalEnvelope converts that flag into this ToolError explaining the surface is synchronous. It exists to turn an opaque 'unsupported type' daemon failure into an actionable message.
Source
Thrown at packages/coding-agent/src/tools/browser/cmux/rpc.ts:163
} catch (e) {
return { __ompErr: (e && (e.stack || e.message)) || String(e) };
}
})()`;
}
/**
* Decode a {@link serializeEvalWithEnvelope} result: rethrow page-side
* exceptions as rich {@link ToolError}s, reject unserializable Promise
* returns with an actionable message, and pass through values from daemons
* that did not run the wrapper.
*/
export function unwrapEvalEnvelope<TResult>(value: unknown, label: string): TResult {
if (value && typeof value === "object") {
if ("__ompErr" in value && typeof value.__ompErr === "string") {
throw new ToolError(`${label} threw a JavaScript exception:\n${value.__ompErr}`);
}
if ("__ompPromise" in value && value.__ompPromise === true) {
throw new ToolError(
`${label} returned a Promise, but this surface evaluates synchronously and cannot await it — return a plain value (poll with waitForFunction for async state instead)`,
);
}
if ("__ompOk" in value) {
return value.__ompOk as TResult;
}
}
return value as TResult;
}
export function mapWaitUntil(waitUntil: string | undefined): "interactive" | "complete" {
return waitUntil === "domcontentloaded" ? "interactive" : "complete";
}
export interface ResolveCmuxKindOptions {
surface?: string;
settingEnabled?: boolean;
}View on GitHub (pinned to 9690622007)
Solutions
- Remove async/await and return a plain synchronous value from the eval function (DOM reads like textContent/getBoundingClientRect are synchronous)
- If you need data from an async operation (fetch, storage), poll for it from the host with waitForFunction instead of awaiting inside eval
- Call .then-free synchronous accessors inside eval and compose async steps as multiple separate evaluate calls
Example fix
// before
const title = await browser.evaluate(async () => {
const res = await fetch('/api/title');
return (await res.json()).title;
});
// after
await browser.waitForFunction(() => document.querySelector('#title') !== null);
const title = await browser.evaluate(() => document.querySelector('#title').textContent); Defensive patterns
Strategy: validation
Validate before calling
function returnsPromise(fn) {
try {
return fn() && typeof fn().then === 'function';
} catch { return false; }
}
// before calling evaluate: if (returnsPromise(myFn)) rewrite to a sync function Type guard
function isEvalEnvelope(v: unknown): v is { __ompOk?: unknown; __ompErr?: string; __ompPromise?: boolean } {
return typeof v === 'object' && v !== null &&
('__ompOk' in v || '__ompErr' in v || '__ompPromise' in v);
} Try / catch
try {
const value = unwrapEvalEnvelope(result, 'evaluate');
} catch (err) {
if (err instanceof ToolError && err.message.includes('returned a Promise')) {
// rewrite script to sync form or switch to waitForFunction polling
}
throw err;
} Prevention
- Never write async eval functions for cmux surfaces — DOM state reads are synchronous
- Use waitForFunction on the host side for anything requiring eventual consistency
- Avoid fetch()/Promise-based APIs inside evaluate; do them outside the browser surface
- Review eval snippets for arrow functions marked async before submitting
When it happens
Trigger: Calling evaluate() or evaluateOnSelector() with a function/expression whose return value is a Promise — e.g. `async () => { ... }`, `() => fetch('/api')`, or `() => document.querySelector('x').asyncMethod()`. The wrapper detects `typeof __v.then === 'function'` and flags it before any serialization is attempted.
Common situations: Writing eval scripts as async functions out of habit from Puppeteer/Playwright where evaluate awaits promises; calling browser fetch() or storage APIs inside eval; wrapping an async helper to read DOM state that is actually available synchronously.
Related errors
- operation canceled
- agent() received invalid arguments: ${result.summary}
- agent() blocked: turn token budget exhausted (${turnBudget.s
- ${failureMessage}${recoveryHint} (subagent failure: ${policy
- agent() isolated apply failed for ${result.id}${summary ? `:
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/9d44a14f164d2bfd.
Report an issue: GitHub.