can1357/oh-my-pi · error · ToolError
Element id ${id} is stale. Run tab.observe() again.
Error message
Element id ${id} is stale. Run tab.observe() again. What it means
After finding the cached handle, the worker verifies el.isConnected inside the page. A disconnected element means the DOM node was removed/replaced since observe() (navigation, re-render, hydration), so the cache is cleared and this ToolError is thrown. The same message is also thrown if checking isConnected itself fails (e.g. the execution context was destroyed by a navigation).
Source
Thrown at packages/coding-agent/src/tools/browser/tab-worker.ts:2128
): Promise<HTTPResponse> {
const page = this.#requirePage();
const predicate: (response: HTTPResponse) => boolean | Promise<boolean> =
typeof pattern === "function"
? pattern
: pattern instanceof RegExp
? response => pattern.test(response.url())
: response => response.url().includes(pattern);
return (await untilAborted(signal, () => page.waitForResponse(predicate, { timeout, signal }))) as HTTPResponse;
}
async #resolveCachedHandle(id: number): Promise<ElementHandle> {
const handle = this.#elementCache.get(id);
if (!handle) throw new ToolError(`Unknown element id ${id}. Run tab.observe() to refresh the element list.`);
try {
const isConnected = (await handle.evaluate(el => el.isConnected)) as boolean;
if (!isConnected) {
this.#clearElementCache();
throw new ToolError(`Element id ${id} is stale. Run tab.observe() again.`);
}
} catch (err) {
if (err instanceof ToolError) throw err;
this.#clearElementCache();
throw new ToolError(`Element id ${id} is stale. Run tab.observe() again.`);
}
return handle;
}
async #resolveAriaRef(id: string): Promise<ElementHandle> {
const ref = parseAriaRefSelector(id) ?? id.trim();
const handle = await resolveAriaRefHandle(this.#requirePage(), ref);
if (!handle) {
throw new ToolError(
`Unknown ARIA ref ${JSON.stringify(ref)}. Run tab.ariaSnapshot() to refresh refs (they renumber each snapshot).`,
);
}
return handle;View on GitHub (pinned to 9690622007)
Solutions
- Run tab.observe() again immediately before the action to rebuild ids, then retry with the fresh id.
- Minimize the delay between observe and action; perform actions right after observing.
- If navigations are expected, wait for load after navigation and re-observe before interacting.
- For flaky re-rendering pages, target stable selectors/aria-refs rather than cached ids.
Example fix
// before const id = (await tab.observe()).elements[3].id; await clickApplyButton(); // triggers re-render await tab.click(id); // stale // after const id = (await tab.observe()).elements[3].id; await clickApplyButton(); await tab.waitForLoad?.(); const fresh = (await tab.observe()).elements.find(e => e.name === "the element"); await tab.click(fresh.id);
Defensive patterns
Strategy: retry
Validate before calling
const connected = await tab.evaluate((el) => el.isConnected, id); if (!connected) await tab.observe();
Try / catch
try {
await tab.click(id);
} catch (err) {
if (err.message.startsWith("Element id") && err.message.includes("stale")) {
const fresh = await tab.observe();
const el = fresh.elements.find(e => e.name === expectedName);
if (el) return tab.click(el.id);
}
throw err;
} Prevention
- Re-observe immediately after any navigation or SPA route change
- Keep the gap between observe and action short
- Wait for load/network idle before acting after navigations
- Prefer stable selectors/aria-refs over cached ids on frequently re-rendering pages
When it happens
Trigger: Acting on an element id after the page navigated or the framework re-rendered and detached the node; evaluation context destroyed mid-check (SPA client-side navigation); element removed by an async update between observe and action.
Common situations: React/Vue re-render replacing list items; SPA route change wiping old nodes; login/session redirect between observe and click; long delay between observe and action letting the page update itself.
Related errors
- Element handle selector no longer resolves
- tab.ariaSnapshot: selector ${__sel} matched no element
- No element matched ${spec.raw}
- tab.select() requires a <select> element
- tab.uploadFile() requires an <input type="file"> element (go
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/f6c9451b6cf66e25.
Report an issue: GitHub.