Roll call: a page that cannot resolve the target declines instead of claiming

Field-testing with two pages open showed the first-wins claim letting the
wrong page answer: a tab whose page lacks the element won the claim,
resolved the selector locally, and replied no_match while another page had
the element. The overlay now resolves the selector before any claim and,
when its page cannot resolve it, declines with reason no_match and the
resolution verdict; the same check runs on the busy-to-idle re-claim. The
server records that verdict on the report and, once every connected
overlay has declined, prefers a report that could serve later (a tab
mid-session or with an apply in flight, which answers busy so the agent
retries) over no_match, and returns the resolution verdict only when no
page can serve; the timeout uses the same precedence.

Also from the same field tests: a tab on another page of the app was
resuming this page's session from the per-origin localStorage cache after
a dev-server reload re-initialised it, then sat in GENERATING for a wrapper
it never renders and declined every later target. restoreSessionWithoutWrapper
now resumes a cached session only on the page that saved it, the check the
server-adoption branch beside it already applied.

Covered by two Rust integration cases, two protocol cases, and contract
assertions for the resolve-before-claim path and the page gate; the
cross-page scenario of the field harness passes on a two-page site.

AI-assisted: found by field tests and fixed with Claude Code under
maintainer direction.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Abdul Wahab
2026-09-15 05:45:49 +05:00
committed by Abdul Wahab
co-authored by Claude Fable 5
parent 8fb7f7fec5
commit 25263adc51
7 changed files with 177 additions and 7 deletions
+21 -1
View File
@@ -7263,6 +7263,7 @@
if (agentTargetOverlayGone()) return;
const busy = agentTargetBusyReason();
if (busy) { declineAgentTargetBusy(msg, busy); return; }
if (declineAgentTargetUnresolvable(msg)) return;
claimAgentTarget(msg.targetId, { eligible: true }).then((claim) => {
if (claim.granted) { actOnAgentTarget(msg); return; }
if (!claim.pending) return;
@@ -7284,6 +7285,19 @@
// target, so a replay must not start a second claim or a second Go.
const agentTargetsSeen = [];
// Only a page that can resolve the target claims it. A tab whose page
// lacks the element declines with its resolution verdict instead, so a
// first-wins claim never lets the wrong page answer for a target that
// another page has. The server prefers a busy report (a tab that could
// serve later) over these, and returns the resolution verdict only when
// no connected page can serve.
function declineAgentTargetUnresolvable(msg) {
const probe = resolveAgentTargetElement(msg);
if (!probe.error) return false;
claimAgentTarget(msg.targetId, { eligible: false, state, reason: 'no_match', result: probe.error });
return true;
}
function handleAgentTarget(msg) {
if (!msg || typeof msg.targetId !== 'string') return;
if (agentTargetsSeen.includes(msg.targetId)) return;
@@ -7297,6 +7311,7 @@
declineAgentTargetBusy(msg, busy);
return;
}
if (declineAgentTargetUnresolvable(msg)) return;
// Eligible tabs race for the server's lease and only the holder acts. A
// hidden tab yields a short head start so a visible one wins when both
// exist, and still serves the request on its own: the user finds the
@@ -9376,7 +9391,12 @@ void main() {
}
function restoreSessionWithoutWrapper(reason, activeSessions) {
const cached = loadSession();
// The session cache is per origin, so a tab on another page of the same
// app sees this page's session too. Only the page that saved it may
// resume it: the server-adoption branch below already applies the same
// check, and a tab on another page has nothing to render for it.
const cachedRaw = loadSession();
const cached = cachedRaw?.id && !pageMatchesCurrent(cachedRaw.pageUrl) ? null : cachedRaw;
// localStorage is a cache, not a gate. A cleared tab, a second browser
// profile, or a teardown that dropped local state all leave the durable
// server session as the only record of work in progress; adopt it instead