can1357/oh-my-pi · error · ToolError

Unknown ARIA ref ${JSON.stringify(ref)}. Run tab.ariaSnapsho

Error message

Unknown ARIA ref ${JSON.stringify(ref)}. Run tab.ariaSnapshot() to refresh refs (they renumber each snapshot).

What it means

ARIA refs produced by tab.ariaSnapshot() are resolved against the live page; the ref numbering is only valid for that snapshot. When resolveAriaRefHandle cannot find an element matching the given ref (the snapshot is outdated or the ref was never issued), this ToolError is thrown and points the caller at ariaSnapshot() to regenerate refs.

Source

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

		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;
	}

	/**
	 * Resolve a selector to an ElementHandle for handle-based actions. An
	 * `aria-ref=eN` selector resolves against the latest ariaSnapshot's refs
	 * (main world); anything else goes through the normal locator wait.
	 */
	async #resolveActionHandle(selector: string, timeoutMs: number, sig: AbortSignal): Promise<ElementHandle> {
		if (parseAriaRefSelector(selector) !== null) return this.#resolveAriaRef(selector);
		return (await untilAborted(sig, () =>
			this.#requirePage().locator(normalizeSelector(selector)).setTimeout(timeoutMs).waitHandle({ signal: sig }),
		)) as ElementHandle;
	}
	#clearElementCache(): void {

View on GitHub (pinned to 9690622007)

Solutions

  1. Run tab.ariaSnapshot() to refresh refs, then use the newly issued ref
  2. Re-snapshot after any action that mutates the page before referencing elements again
  3. Verify the ref comes from the same tab/snapshot you are operating on
  4. If refs churn rapidly, interact via tab.observe() element ids or selectors instead

Example fix

// before
await tab.click({ ref: 'e5' }); // from an old snapshot
// after
const snap = await tab.ariaSnapshot();
await tab.click({ ref: snap.findRef('Submit') ?? 'e1' });
Defensive patterns

Strategy: validation

Validate before calling

const ref = parseAriaRefSelector(id);
if (!ref || !/^[A-Za-z0-9_-]+$/.test(String(ref))) throw new Error('invalid aria ref');

Type guard

function isAriaRef(v: unknown): v is string {
	return typeof v === 'string' && v.trim().length > 0;
}

Try / catch

try {
	await act({ ref });
} catch (err) {
	if (err instanceof ToolError && /Unknown ARIA ref/.test(err.message)) {
		const snap = await tab.ariaSnapshot();
		ref = remapRef(snap, ref); // refresh and remap
		await act({ ref });
	} else throw err;
}

Prevention

When it happens

Trigger: Calling an action with a ref from an older ariaSnapshot() after the page changed (refs renumber each snapshot); passing a malformed or arbitrary string as the ref; snapshot expired after DOM mutations.

Common situations: Agent snapshots once and reuses refs across multiple steps; page content is dynamic so refs shift between snapshot and action; ref string typo or copied from a different tab.

Related errors


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