can1357/oh-my-pi · error · ToolError

${label} cannot run: this handle was invalidated after ${sta

Error message

${label} cannot run: this handle was invalidated after ${state.invalidatedBy} timed out; run tab.observe() or tab.ariaSnapshot() to resolve a fresh handle

What it means

Element handles tracked by the tab worker can be invalidated when an operation on them times out. runGuardedHandleAction checks `state.invalidatedBy` before running any action; if a previous operation timed out and invalidated the handle, every subsequent action on that stale handle throws this error, telling you to re-observe.

Source

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

	type: ElementHandle["type"];
	invalidatedBy?: string;
}

/** Symbol-keyed original methods travel with each cached handle without enumerating or colliding. */
const RAW_HANDLE_METHODS = Symbol("browser.rawHandleMethods");

type HandleWithRawMethods = ActionableHandle & { [RAW_HANDLE_METHODS]?: RawHandleMethods };

async function runGuardedHandleAction<T>(
	handle: ElementHandle,
	state: RawHandleMethods,
	label: string,
	signal: AbortSignal,
	action: () => Promise<T>,
	invalidate?: () => Promise<void>,
): Promise<T> {
	if (state.invalidatedBy) {
		throw new ToolError(
			`${label} cannot run: this handle was invalidated after ${state.invalidatedBy} timed out; ` +
				"run tab.observe() or tab.ariaSnapshot() to resolve a fresh handle",
		);
	}
	throwIfAborted(signal);
	const pending = action();
	try {
		return await untilAborted(signal, () => pending);
	} catch (error) {
		if (!signal.aborted) throw error;
		state.invalidatedBy = label;
		void pending.catch(() => undefined);
		await withTimeout(
			Promise.all([handle.dispose().catch(() => undefined), invalidate?.().catch(() => undefined)]),
			HANDLE_ACTION_INVALIDATION_TIMEOUT_MS,
			`Timed out invalidating ${label}`,
		).catch(() => undefined);
		throw error;

View on GitHub (pinned to 9690622007)

Solutions

  1. Run `tab.observe()` or `tab.ariaSnapshot()` to resolve a fresh handle, then retry the action with the new id.
  2. Retry promptly after observing — handles go stale when the DOM changes again.
  3. Avoid reusing handle ids across multiple steps; resolve, act, discard.
  4. If timeouts are the root cause, make the selector more specific or wait for the element to be actionable before acting.

Example fix

// before
tab.click(handleId); // handleId invalidated by earlier timeout
// after
const obs = await tab.observe();
const fresh = obs.find(el => el.role === "button" && el.name === "Submit");
await tab.click(fresh.id);
Defensive patterns

Strategy: try-catch

Try / catch

try {
  await tab.click(handleId);
} catch (err) {
  if (err instanceof ToolError && err.message.includes("was invalidated")) {
    const fresh = (await tab.observe()).find(el => el.name === targetName);
    await tab.click(fresh.id);
  } else throw err;
}

Prevention

When it happens

Trigger: Holding a handle from tab.observe()/tab.id() across a slow page update, letting an action on it time out (setting `invalidatedBy`), then calling another action (click/fill) on the same handle id.

Common situations: Pages that re-render or navigate between observe and act, making handles stale; long hangs on a click that poison the handle for later calls; agent loops reusing old handle ids after a timeout.

Understand the failure class

Related errors


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