Address review: the generate event resolves the agent target it serves

A winning overlay could reload after handleGo() minted a session but
before its result post landed. The close released its lease, the
server replayed the still-pending target, and another tab (or the
reloaded page, once it abandoned the unknown session) could claim it
and fire a second Go for a request that already had a session.

The overlay now names the target on the generate event it fires for it
(`agentTarget: {targetId, result}`, the same result it posts), and the
helper resolves the pending request the moment that event is accepted,
stripping the envelope before journaling. Whichever of the event and
the result post lands first answers; a page that dies between Go and
its result cannot leave the request pending, and a request whose event
never reached the helper is served exactly once by the rescuer.

Tests: a Rust integration case and a Node protocol case (claim, Go
event without a result post, verdict carries the session, a late claim
finds nothing pending, the journal carries no envelope), contract pins
for the handoff, and the contract doc.

Written with AI assistance (Claude).

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 335945525d
commit b5210471fb
6 changed files with 128 additions and 2 deletions
+29 -1
View File
@@ -7211,6 +7211,10 @@
// it, and only the tab that holds the lease can renew it.
const AGENT_TARGET_CLIENT_ID = id8();
// The agent target an agent-initiated Go is serving: set by
// actOnAgentTarget around its handleGo call, read once by handleGo.
let agentTargetForGo = null;
function claimAgentTarget(targetId, report) {
return fetch('http://localhost:' + PORT + '/agent-target-claim?token=' + TOKEN, {
method: 'POST',
@@ -7446,7 +7450,14 @@
updateBarContent('configure');
const input = uiGetById(PREFIX + '-input');
if (input) input.value = msg.prompt || '';
// The target rides on the generate event too: the helper resolves
// the request from whichever lands first, so a page that dies
// between Go and its result cannot leave the request pending for a
// second Go elsewhere.
const candidate = describeAgentTargetCandidate(el);
agentTargetForGo = { targetId: msg.targetId, matchCount: resolved.matchCount, action: msg.action, count: msg.count, element: candidate };
handleGo();
agentTargetForGo = null;
if (state === 'GENERATING' && currentSessionId) {
reply({
ok: true,
@@ -7454,7 +7465,7 @@
sessionId: currentSessionId,
action: msg.action,
count: msg.count,
element: describeAgentTargetCandidate(el),
element: candidate,
});
} else {
reply({ ok: false, error: 'go_failed', state });
@@ -8175,6 +8186,23 @@
};
if (snapshot.comments.length > 0) basePayload.comments = snapshot.comments;
if (snapshot.strokes.length > 0) basePayload.strokes = snapshot.strokes;
if (agentTargetForGo) {
// An agent-initiated Go names the target it serves (see
// actOnAgentTarget): the helper resolves that request from this event
// as well as from the overlay's own result post.
basePayload.agentTarget = {
targetId: agentTargetForGo.targetId,
result: {
ok: true,
matchCount: agentTargetForGo.matchCount,
sessionId: currentSessionId,
action: agentTargetForGo.action,
count: agentTargetForGo.count,
element: agentTargetForGo.element,
},
};
agentTargetForGo = null;
}
// Hide the interactive overlay so it doesn't linger during generation.
hideAnnotOverlay();