[Bug]: preview_click still gives each step a full timeout after #17693, so a stalled click's reason never reaches the agent#25557

opened15d agobyPpnupu

Before submitting

  • I searched existing issues. Closest is #12898 (closed by #17693); this is the residual it left.
  • I included enough detail to reproduce or investigate the problem.

Area: apps/server

Steps to reproduce

Not yet run; this follows from the code paths below.

  1. Load a page where the target button is inserted after about 2 s and is disabled:
    html
    1<script>setTimeout(() => document.body.insertAdjacentHTML("beforeend", '<button id="save" disabled>Save</button>'), 2000)</script>
  2. Call preview_click immediately after loading, while #save is absent and more than one second remains before insertion (otherwise reload before retrying; this repro is timing-sensitive). Call it with selector: "#save" and the default timeoutMs (15000).

Expected behavior

Within the request's timeoutMs, the click fails with Playwright's reason (here, "element is not enabled"), and the agent can see that reason.

Actual behavior

ServerBrowserPage.click hands the full timeoutMs to each step separately rather than sharing one deadline:

ts
1const timeout = input.timeoutMs ?? DEFAULT_TIMEOUT_MS; // 15_000
2const point = await targetPoint(page, locator, input, timeout); // scrollIntoViewIfNeeded(timeout) + boundingBox(timeout)
3await pointer(point, "click");
4locator.click({ ...options, timeout }) // a fresh 15 s

The broker starts its response timer right after enqueueing the request (timeoutMs + HOST_RESPONSE_GRACE_MS = 16 s), without waiting for the page operation to start. Nothing gives the page side the time that remains. SessionControl checks the request deadline only before queued work starts. So when queueing, waiting for the target, and preparing the click take longer than the 1 s grace, Playwright's click timeout fires after the broker has already expired. The broker then evicts the host and returns a generic Preview automation click timed out after 15000ms. with no cause. Broker expiry does not cancel a click that has already started, so it can still apply later.

Even when Playwright's timeout does arrive in time, the reason is lost twice:

  • toOperationError keeps only message.split("\n")[0], which drops Playwright's call log, where the actionability reason lives.
  • PreviewAutomationTimeoutError.message prints only the operation and duration, not the host's cause.

Observed behavior: an agent clicking the Namecheap Advanced DNS "Save" link (table.advanced-dns a.save) got this generic timeout twice. Both server spans ended at about 16,002 ms with no cause, and the target resolved about 2 s into each request. The server trace files have rotated since, so that span data is no longer available. The desktop PreviewManager.emitAgentPointer spans (16:44:30 and 16:45:16 local time) are still present.

Relationship to existing reports

  • #12898 already reported this per-step click budget (in this comment). Its closing fix, #17693, added 1 s of response grace but left the per-step budgets unchanged. This report covers what remains: no shared remaining-time budget, and the lost actionability reason.
  • PR #23564 bounds the whole server handler by request.timeoutMs. That prevents this stalled handler from reaching broker expiry when delivery and response overhead fit within the grace period. With that change, a stalled click returns the handler's timeout instead, so Playwright's reason is still never seen.
  • PR #23107 makes timeout messages distinguish browser-side timeouts from broker timeouts. It does not keep Playwright's call log or give the click its remaining time.
  • #3714 / PR #23489 add a post-timeout target-count diagnostic and expose the bounded server diagnostic in the public timeout message. They don't preserve Playwright's actionability log or share one remaining-time budget, and replies arriving after broker expiry are still discarded.
  • #16921 / PR #17871 cover host eviction after timeouts.

Suggested fix

Establish one request deadline, calculate the remaining budget after queueing and before each step (targetPoint, locator.click), and reserve time for diagnostics and response delivery. Carry the relevant actionability line from the Playwright call log into the caller-facing timeout message.

Impact

Major degradation or frequent failure

Version or commit

T3 Code (Beta) 0.1.2. The same click timeout behavior is present in v0.1.3-nightly.20261026.3717 and origin/main.

Environment

macOS 27.0, desktop app. The shared-browser tab was driven by an agent through the T3 MCP preview tools: in-process server-browser host, desktop CDP bridge.

Workaround

On that site, filling the fields and calling element.click() from preview_evaluate worked. Before retrying, check whether a timed-out click already applied, so the action isn't duplicated.