can1357/oh-my-pi · error · ToolError

Unknown element id ${id}. Run tab.observe() to refresh the e

Error message

Unknown element id ${id}. Run tab.observe() to refresh the element list.

What it means

Interactions can reference cached elements by numeric id from the last observe(). #resolveCachedHandle looks the id up in #elementCache; an unknown id (never observed, out of range, or cache cleared by a new observe) throws this ToolError telling the user to re-observe. The cache is not auto-refreshed because element lists can be large and refreshes are expensive.

Source

Thrown at packages/coding-agent/src/tools/browser/tab-worker.ts:2123

	async #waitForResponse(
		pattern: string | RegExp | ((response: HTTPResponse) => boolean | Promise<boolean>),
		timeout: number,
		signal: AbortSignal,
	): 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) {

View on GitHub (pinned to 9690622007)

Solutions

  1. Run tab.observe() to get the current element list, then use ids from that fresh output.
  2. Retry the action with the corrected id — check the id is in the latest observe result.
  3. If observing frequently invalidates ids, snapshot the needed selector/aria-ref strings instead of ids.
  4. Avoid interleaving observes from multiple consumers on the same tab.

Example fix

// before
await tab.click(42); // id 42 from an older observe
// after
const { elements } = await tab.observe();
const target = elements.find(e => e.name === "Submit");
await tab.click(target.id);
Defensive patterns

Strategy: try-catch

Validate before calling

const known = latestObservation.elements.some(e => e.id === id);
if (!known) throw new Error(`id ${id} not in latest observe() output; re-observe first`);

Try / catch

try {
  await tab.click(id);
} catch (err) {
  if (err.message.startsWith("Unknown element id")) {
    const fresh = await tab.observe();
    const el = fresh.elements.find(e => e.name === expectedName);
    if (el) return tab.click(el.id);
  }
  throw err;
}

Prevention

When it happens

Trigger: Calling an action with an element id that was never returned by tab.observe(), an id beyond the current list, or an id invalidated because another observe() cleared and rebuilt #elementCache.

Common situations: LLM/agent hallucinating an id or reusing ids from a previous observe after the list shifted; two clients sharing one tab where each observe() invalidates the other's ids; ids from a different tab's observation.

Related errors


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/ab483b15688f6fc3. Report an issue: GitHub.