{"record":{"id":"f6c9451b6cf66e25","repo":"can1357/oh-my-pi","slug":"element-id-id-is-stale-run-tab-observe-again","errorCode":null,"errorMessage":"Element id ${id} is stale. Run tab.observe() again.","messagePattern":"Element id (.+?) is stale\\. Run tab\\.observe\\(\\) again\\.","errorType":"exception","errorClass":"ToolError","httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/tools/browser/tab-worker.ts","lineNumber":2128,"sourceCode":"\t): Promise<HTTPResponse> {\n\t\tconst page = this.#requirePage();\n\t\tconst predicate: (response: HTTPResponse) => boolean | Promise<boolean> =\n\t\t\ttypeof pattern === \"function\"\n\t\t\t\t? pattern\n\t\t\t\t: pattern instanceof RegExp\n\t\t\t\t\t? response => pattern.test(response.url())\n\t\t\t\t\t: response => response.url().includes(pattern);\n\t\treturn (await untilAborted(signal, () => page.waitForResponse(predicate, { timeout, signal }))) as HTTPResponse;\n\t}\n\n\tasync #resolveCachedHandle(id: number): Promise<ElementHandle> {\n\t\tconst handle = this.#elementCache.get(id);\n\t\tif (!handle) throw new ToolError(`Unknown element id ${id}. Run tab.observe() to refresh the element list.`);\n\t\ttry {\n\t\t\tconst isConnected = (await handle.evaluate(el => el.isConnected)) as boolean;\n\t\t\tif (!isConnected) {\n\t\t\t\tthis.#clearElementCache();\n\t\t\t\tthrow new ToolError(`Element id ${id} is stale. Run tab.observe() again.`);\n\t\t\t}\n\t\t} catch (err) {\n\t\t\tif (err instanceof ToolError) throw err;\n\t\t\tthis.#clearElementCache();\n\t\t\tthrow new ToolError(`Element id ${id} is stale. Run tab.observe() again.`);\n\t\t}\n\t\treturn handle;\n\t}\n\n\tasync #resolveAriaRef(id: string): Promise<ElementHandle> {\n\t\tconst ref = parseAriaRefSelector(id) ?? id.trim();\n\t\tconst handle = await resolveAriaRefHandle(this.#requirePage(), ref);\n\t\tif (!handle) {\n\t\t\tthrow new ToolError(\n\t\t\t\t`Unknown ARIA ref ${JSON.stringify(ref)}. Run tab.ariaSnapshot() to refresh refs (they renumber each snapshot).`,\n\t\t\t);\n\t\t}\n\t\treturn handle;","sourceCodeStart":2110,"sourceCodeEnd":2146,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/tools/browser/tab-worker.ts#L2110-L2146","documentation":"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).","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nconst id = (await tab.observe()).elements[3].id;\nawait clickApplyButton(); // triggers re-render\nawait tab.click(id); // stale\n// after\nconst id = (await tab.observe()).elements[3].id;\nawait clickApplyButton();\nawait tab.waitForLoad?.();\nconst fresh = (await tab.observe()).elements.find(e => e.name === \"the element\");\nawait tab.click(fresh.id);","handlingStrategy":"retry","validationCode":"const connected = await tab.evaluate((el) => el.isConnected, id);\nif (!connected) await tab.observe();","typeGuard":null,"tryCatchPattern":"try {\n  await tab.click(id);\n} catch (err) {\n  if (err.message.startsWith(\"Element id\") && err.message.includes(\"stale\")) {\n    const fresh = await tab.observe();\n    const el = fresh.elements.find(e => e.name === expectedName);\n    if (el) return tab.click(el.id);\n  }\n  throw err;\n}","preventionTips":["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"],"tags":["browser","stale-element","dom","cache"],"backgroundTag":"stale-element-reference","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}