can1357/oh-my-pi · error · Error
Element handle selector no longer resolves
Error message
Element handle selector no longer resolves
What it means
CmuxElementHandle invokes actions by re-resolving its stored selector inside the page (findElement(spec)); when the selector no longer matches any element in the live DOM, the page-side script throws 'Element handle selector no longer resolves', which surfaces as this ToolError. It means the element the handle was created from has been removed, re-rendered, or the selector text no longer matches.
Source
Thrown at packages/coding-agent/src/tools/browser/cmux/cmux-tab.ts:806
}
async elementExists(selector: string): Promise<boolean> {
return await this.#selectorExists(this.#selectorSpec(selector));
}
async elementBox(selector: string): Promise<BoundingBox | null> {
return await this.#selectorBox(this.#selectorSpec(selector));
}
async evaluateOnSelector<TResult>(selector: string, source: string, args: unknown[]): Promise<TResult> {
const spec = this.#selectorSpec(selector);
const script = `(() => {
const spec = ${JSON.stringify(spec)};
const source = ${JSON.stringify(source)};
const args = ${JSON.stringify(args)};
${PAGE_SELECTOR_HELPERS}
const element = findElement(spec);
if (!element) throw new Error("Element handle selector no longer resolves");
const callable = (0, eval)("(" + source + ")");
return callable(element, ...args);
})()`;
// Envelope so a stale selector or a throwing callback reports its actual
// error instead of the daemon's generic js_error (see tab.evaluate()).
const result = (await this.#request("browser.eval", {
script: serializeEvalWithEnvelope(script, []),
})) as CmuxEvalResult;
return unwrapEvalEnvelope<TResult>(result.value, "elementHandle.evaluate()");
}
async pageContent(): Promise<string> {
return await this.#evalScript<string>("document.documentElement.outerHTML");
}
async pageScreenshot(opts: ScreenshotOptions = {}): Promise<Buffer | string> {
if (opts.selector) await this.scrollIntoView(opts.selector);
const result = await this.#captureScreenshotPng(this.#runContext?.timeoutMs ?? 30_000);View on GitHub (pinned to 9690622007)
Solutions
- Re-acquire the handle right before use: call tab.waitForSelector(sel) again instead of reusing the old CmuxElementHandle.
- Minimize the gap between obtaining the handle and acting on it — keep actions on the same element adjacent in code.
- Use a more stable selector (data-testid, id, semantic role) so re-resolution survives re-renders.
- If the page navigates or mutates heavily, perform the action inside tab.evaluate() against a freshly queried element.
Example fix
// before: stale handle reused after re-render
const btn = await tab.waitForSelector("button.submit");
await someAsyncWork();
await btn.click(); // selector no longer resolves
// after: re-resolve just before acting
await someAsyncWork();
const btn = await tab.waitForSelector("button.submit");
await btn.click(); Defensive patterns
Strategy: retry
Validate before calling
// re-verify the selector still resolves before using the handle const stillThere = await tab.evaluate( (sel) => !!document.querySelector(sel), selector, ); if (!stillThere) await tab.waitForSelector(selector);
Try / catch
try {
await elementHandle.click();
} catch (err) {
if (err instanceof ToolError && err.message.includes("no longer resolves")) {
const fresh = await tab.waitForSelector(selector, { timeout: 10_000 });
await fresh.click();
} else throw err;
} Prevention
- Re-acquire handles immediately before acting instead of caching them across awaits
- Prefer stable selectors (data-testid, id) over generated classes and nth-position
- Refresh handles after any navigation or known re-render boundary
- Keep the handle→action gap free of awaits that can trigger framework updates
When it happens
Trigger: Using a CmuxElementHandle obtained via tab.waitFor()/tab.waitForSelector() after a framework re-render replaced the node; the DOM subtree was torn down (route change, modal close); the handle's selector relied on nth-position text/structure that changed; the page navigated away.
Common situations: React/Vue/Svelte keyed re-renders that recreate elements between waitFor and the action; handles held across await points while an animation swaps nodes; stale handles reused after waitForNavigation; auto-generated classes changing between renders when the selector was built from them.
Related errors
- tab.ariaSnapshot: selector ${__sel} matched no element
- No element matched ${spec.raw}
- Element id ${id} is stale. Run tab.observe() again.
- Browser selector must be a string; got ${kind}. tab.click/ty
- No page target matched ${JSON.stringify(options.matcher)}. A
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/3beda3ddc85bd8c1.
Report an issue: GitHub.