@enricai/barnacle 1.12.70 → 1.12.72
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/recon/capture-filters.d.ts +8 -1
- package/dist/recon/capture-filters.d.ts.map +1 -1
- package/dist/recon/capture-filters.js +97 -26
- package/dist/recon/capture-filters.js.map +1 -1
- package/dist/scraper/browser-click-expr.d.ts +3 -4
- package/dist/scraper/browser-click-expr.d.ts.map +1 -1
- package/dist/scraper/browser-click-expr.js +3 -4
- package/dist/scraper/browser-click-expr.js.map +1 -1
- package/dist/scraper/flow-runner.d.ts +27 -6
- package/dist/scraper/flow-runner.d.ts.map +1 -1
- package/dist/scraper/flow-runner.js +328 -70
- package/dist/scraper/flow-runner.js.map +1 -1
- package/dist/scraper/frame-target.d.ts +12 -0
- package/dist/scraper/frame-target.d.ts.map +1 -1
- package/dist/scraper/frame-target.js +1 -0
- package/dist/scraper/frame-target.js.map +1 -1
- package/dist/scraper/submit-control.d.ts +52 -2
- package/dist/scraper/submit-control.d.ts.map +1 -1
- package/dist/scraper/submit-control.js +117 -21
- package/dist/scraper/submit-control.js.map +1 -1
- package/dist/scraper/target-resolution-diagnostic.d.ts +53 -0
- package/dist/scraper/target-resolution-diagnostic.d.ts.map +1 -0
- package/dist/scraper/target-resolution-diagnostic.js +153 -0
- package/dist/scraper/target-resolution-diagnostic.js.map +1 -0
- package/dist/scripts/recon-browser.d.ts +32 -2
- package/dist/scripts/recon-browser.d.ts.map +1 -1
- package/dist/scripts/recon-browser.js +2 -0
- package/dist/scripts/recon-browser.js.map +1 -1
- package/dist/scripts/recon-generate.d.ts +148 -6
- package/dist/scripts/recon-generate.d.ts.map +1 -1
- package/dist/scripts/recon-generate.js +575 -93
- package/dist/scripts/recon-generate.js.map +1 -1
- package/package.json +1 -1
- package/dist/scraper/deep-query.d.ts +0 -44
- package/dist/scraper/deep-query.d.ts.map +0 -1
- package/dist/scraper/deep-query.js +0 -93
- package/dist/scraper/deep-query.js.map +0 -1
|
@@ -25,6 +25,7 @@ exports.findRecentBackendError = findRecentBackendError;
|
|
|
25
25
|
exports.findRecentPageTransition = findRecentPageTransition;
|
|
26
26
|
exports.isWizardExitAction = isWizardExitAction;
|
|
27
27
|
exports.isAdvanceStep = isAdvanceStep;
|
|
28
|
+
exports.isSubmitIntentStep = isSubmitIntentStep;
|
|
28
29
|
exports.isCheckboxOrRadioIntentStep = isCheckboxOrRadioIntentStep;
|
|
29
30
|
exports.shouldCaptureSelectionState = shouldCaptureSelectionState;
|
|
30
31
|
exports.flowHasSubmitSemantics = flowHasSubmitSemantics;
|
|
@@ -128,6 +129,7 @@ const session_teardown_1 = require("./session-teardown");
|
|
|
128
129
|
const spa_readiness_1 = require("./spa-readiness");
|
|
129
130
|
const stagehand_guard_1 = require("./stagehand-guard");
|
|
130
131
|
const submit_control_1 = require("./submit-control");
|
|
132
|
+
const target_resolution_diagnostic_1 = require("./target-resolution-diagnostic");
|
|
131
133
|
const watchdog_1 = require("./watchdog");
|
|
132
134
|
const recon_shared_1 = require("../scripts/recon-shared");
|
|
133
135
|
const client_1 = require("../testmail/client");
|
|
@@ -1195,6 +1197,36 @@ function isAdvanceStep(instruction) {
|
|
|
1195
1197
|
const haystack = instruction.toLowerCase();
|
|
1196
1198
|
return ADVANCE_STEP_PHRASES.some((p) => haystack.includes(p));
|
|
1197
1199
|
}
|
|
1200
|
+
/**
|
|
1201
|
+
* Phrases (in the ORIGINAL flow instruction) that mark a step whose intent is to
|
|
1202
|
+
* SUBMIT a form — a final "Submit" click — as opposed to a field-answer
|
|
1203
|
+
* or advance step. Matched against the step text, not a resolved element
|
|
1204
|
+
* description. Deliberately narrow (a click-on-submit phrase, not the bare
|
|
1205
|
+
* word "submit") so a field named e.g. "Submit Date" or a verification step
|
|
1206
|
+
* that merely mentions the submit button is never misclassified.
|
|
1207
|
+
*/
|
|
1208
|
+
const SUBMIT_INTENT_STEP_PHRASES = [
|
|
1209
|
+
"to submit the",
|
|
1210
|
+
"click 'submit'",
|
|
1211
|
+
'click "submit"',
|
|
1212
|
+
"click the submit",
|
|
1213
|
+
"click submit",
|
|
1214
|
+
];
|
|
1215
|
+
/**
|
|
1216
|
+
* Is this flow step a submit click (the final "Submit"/"Apply" action), rather
|
|
1217
|
+
* than a field-answer or advance step? Mirrors `isAdvanceStep` so a step that
|
|
1218
|
+
* unambiguously names a submit *click* in its own instruction is recognized as
|
|
1219
|
+
* submit-shaped even when the flow file has no explicit `submitStep` flag and
|
|
1220
|
+
* the step isn't the flow's last one. Keyed on the ORIGINAL step instruction,
|
|
1221
|
+
* same discipline as `isAdvanceStep`/`isCheckboxOrRadioIntentStep`. Pure;
|
|
1222
|
+
* unit-testable seam paralleling both.
|
|
1223
|
+
*/
|
|
1224
|
+
function isSubmitIntentStep(instruction) {
|
|
1225
|
+
if (!instruction)
|
|
1226
|
+
return false;
|
|
1227
|
+
const haystack = instruction.toLowerCase();
|
|
1228
|
+
return SUBMIT_INTENT_STEP_PHRASES.some((p) => haystack.includes(p));
|
|
1229
|
+
}
|
|
1198
1230
|
/**
|
|
1199
1231
|
* Is this flow step's intent to check a checkbox or select a radio option?
|
|
1200
1232
|
* Used to veto a weak page-wide DOM signal (htmlDelta/textChanged/formValueChanged)
|
|
@@ -1556,9 +1588,10 @@ function isClickViewSwapVerified(params) {
|
|
|
1556
1588
|
// a real network/URL transition, leaving it structurally unverifiable.
|
|
1557
1589
|
// `resolvedElementIsSubmitShaped` closes the same gap from the OTHER
|
|
1558
1590
|
// direction: a step the flow never flagged `submitStep` can still resolve
|
|
1559
|
-
// onto an objectively submit-shaped control
|
|
1560
|
-
//
|
|
1561
|
-
//
|
|
1591
|
+
// onto an objectively submit-shaped control per submit-control.ts's
|
|
1592
|
+
// SUBMIT_SHAPE_EXPR (an explicit `type="submit"` affordance, or a
|
|
1593
|
+
// non-native control that clears the same name/negative-text
|
|
1594
|
+
// corroboration bar) — that control needs the same real-transition proof
|
|
1562
1595
|
// regardless of flow authoring, so a byte-positive form reset can't be
|
|
1563
1596
|
// mistaken for a byte-positive real submit.
|
|
1564
1597
|
if (submitStep || resolvedElementIsSubmitShaped)
|
|
@@ -1698,7 +1731,7 @@ function describeAttemptEffectSignals(pre, post, recentCaptureMeta, preMetaLengt
|
|
|
1698
1731
|
* keeps healing opportunistically.
|
|
1699
1732
|
*/
|
|
1700
1733
|
function shouldSkipTechnique(params) {
|
|
1701
|
-
const { technique, priorAttempts, advanceUnmovedAfterAttempt1,
|
|
1734
|
+
const { technique, priorAttempts, advanceUnmovedAfterAttempt1, attempt1UnreachableViaLightDom, submitShapedStep, } = params;
|
|
1702
1735
|
// Unmoved-advance short-circuit (measured: attempts 2-4 recovered a stuck
|
|
1703
1736
|
// advance 0 times in 289 steps). When attempt-1's act-string clicked the Next
|
|
1704
1737
|
// and the wizard did NOT move forward (non-advancing POST, or no effect),
|
|
@@ -1719,14 +1752,16 @@ function shouldSkipTechnique(params) {
|
|
|
1719
1752
|
reason: "advance step did not move the wizard on attempt 1; re-observe/re-click cannot advance it — skipping to rephrase/replan",
|
|
1720
1753
|
};
|
|
1721
1754
|
}
|
|
1722
|
-
//
|
|
1723
|
-
//
|
|
1724
|
-
// no real DOM growth
|
|
1725
|
-
// recon-submit-phantom-click bug report's
|
|
1726
|
-
// into a shadow root / web component
|
|
1727
|
-
//
|
|
1728
|
-
//
|
|
1729
|
-
//
|
|
1755
|
+
// Unreachable-via-light-DOM short-circuit: attempt 1 either phantom-clicked
|
|
1756
|
+
// (Stagehand reported success but pre/post shows zero network, zero URL
|
|
1757
|
+
// change, and no real DOM growth) or came back "unresolved" (act() found
|
|
1758
|
+
// zero candidates at all) — the recon-submit-phantom-click bug report's
|
|
1759
|
+
// light-DOM resolver can't see into a shadow root / web component in
|
|
1760
|
+
// either case, and "unresolved" is if anything stronger evidence of that
|
|
1761
|
+
// than "phantom" (there was no candidate to even click). Repeating
|
|
1762
|
+
// observe-act / structured-click / observe-act-exclude re-resolves the
|
|
1763
|
+
// SAME light-DOM-only view of the page and would no-op identically, so
|
|
1764
|
+
// skip straight to deep-submit-locator (attempt 2) instead. llm-rephrase
|
|
1730
1765
|
// (attempt 4) is never skipped — a differently-worded instruction is still
|
|
1731
1766
|
// a distinct attempt worth trying if the deep locator also fails. Gated on
|
|
1732
1767
|
// submitShapedStep: the deep submit-control locator ranks submit-shaped
|
|
@@ -1734,14 +1769,14 @@ function shouldSkipTechnique(params) {
|
|
|
1734
1769
|
// is a guaranteed no-op — leave the normal ladder intact so
|
|
1735
1770
|
// structured-click / observe-act-exclude, the techniques that can
|
|
1736
1771
|
// actually click it, still run.
|
|
1737
|
-
if (
|
|
1772
|
+
if (attempt1UnreachableViaLightDom === true &&
|
|
1738
1773
|
submitShapedStep === true &&
|
|
1739
1774
|
(technique === "observe-act" ||
|
|
1740
1775
|
technique === "structured-click" ||
|
|
1741
1776
|
technique === "observe-act-exclude")) {
|
|
1742
1777
|
return {
|
|
1743
1778
|
skip: true,
|
|
1744
|
-
reason: "attempt 1
|
|
1779
|
+
reason: "attempt 1 could not reach the target via the light-DOM resolver (phantom click or zero candidates resolved) on a submit-shaped step; re-observe/re-click cannot reach a target the light-DOM resolver can't see — escalating to the deep submit-control locator",
|
|
1745
1780
|
};
|
|
1746
1781
|
}
|
|
1747
1782
|
if (technique === "structured-click") {
|
|
@@ -3237,6 +3272,37 @@ function xpathTailForRetarget(xpath) {
|
|
|
3237
3272
|
const tail = steps.slice(-2).filter(Boolean);
|
|
3238
3273
|
return tail.length > 0 ? tail.join("/") : null;
|
|
3239
3274
|
}
|
|
3275
|
+
/**
|
|
3276
|
+
* In-page source for `(tail) => Element | null`: resolves
|
|
3277
|
+
* {@link xpathTailForRetarget}'s loose `//`+tail re-anchor against the live
|
|
3278
|
+
* DOM, disambiguating when the loose pattern matches more than one element.
|
|
3279
|
+
* The tail deliberately drops every attribute above the leaf+parent tag
|
|
3280
|
+
* pair, so on a page with several same-tag-shaped siblings (e.g. a repeated
|
|
3281
|
+
* "sign in" control sitting where a "create account" submit button used to
|
|
3282
|
+
* resolve) `FIRST_ORDERED_NODE_TYPE` silently accepts whichever one happens
|
|
3283
|
+
* to come first in document order — not necessarily the element the primary
|
|
3284
|
+
* xpath was tracking. Snapshotting every match and preferring one that is
|
|
3285
|
+
* objectively submit-shaped — reusing submit-control.ts's exported
|
|
3286
|
+
* {@link SUBMIT_SHAPE_EXPR}, the single site-agnostic predicate every submit-
|
|
3287
|
+
* shape consumer now shares — and not negative-text (reuses submit-
|
|
3288
|
+
* control.ts's own {@link NEGATIVE_TEXT_EXPR} vocabulary rather than a
|
|
3289
|
+
* second copy) recovers the plausible candidate instead of an arbitrary one.
|
|
3290
|
+
* Falls back to the first document-order match when no candidate clears that
|
|
3291
|
+
* bar, preserving the previous behavior for every single-match (the common)
|
|
3292
|
+
* case.
|
|
3293
|
+
*/
|
|
3294
|
+
const XPATH_TAIL_RETARGET_RESOLVE_FN_SRC = `((tail) => {
|
|
3295
|
+
const isNegative = ${submit_control_1.NEGATIVE_TEXT_EXPR};
|
|
3296
|
+
const accessibleName = (el) =>
|
|
3297
|
+
(el.getAttribute("aria-label") || el.textContent || "").replace(/\\s+/g, " ").trim().toLowerCase();
|
|
3298
|
+
const isSubmitShaped = ${submit_control_1.SUBMIT_SHAPE_EXPR};
|
|
3299
|
+
const r = document.evaluate("//" + tail, document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
|
|
3300
|
+
const nodes = [];
|
|
3301
|
+
for (let i = 0; i < r.snapshotLength; i++) nodes.push(r.snapshotItem(i));
|
|
3302
|
+
if (nodes.length <= 1) return nodes[0] || null;
|
|
3303
|
+
const preferred = nodes.find((el) => isSubmitShaped(el) && !isNegative(accessibleName(el)));
|
|
3304
|
+
return preferred || nodes[0];
|
|
3305
|
+
})`;
|
|
3240
3306
|
/**
|
|
3241
3307
|
* Resolve any selector a caller might hold into a bare XPath body for
|
|
3242
3308
|
* `document.evaluate`. `verifyFillReadback` is shared across call sites that
|
|
@@ -3487,17 +3553,26 @@ async function resolvedClickTargetStillPresent(target, selector) {
|
|
|
3487
3553
|
}
|
|
3488
3554
|
}
|
|
3489
3555
|
/**
|
|
3490
|
-
* Site-agnostic
|
|
3491
|
-
*
|
|
3492
|
-
*
|
|
3493
|
-
*
|
|
3494
|
-
*
|
|
3495
|
-
*
|
|
3496
|
-
*
|
|
3497
|
-
*
|
|
3498
|
-
*
|
|
3499
|
-
*
|
|
3500
|
-
*
|
|
3556
|
+
* Site-agnostic submit-shape signal read directly from the resolved click
|
|
3557
|
+
* target — the counterpart to the flow-authored `submitStep` flag. A step
|
|
3558
|
+
* the flow never flagged can still resolve onto an objectively submit-shaped
|
|
3559
|
+
* control, per submit-control.ts's exported {@link SUBMIT_SHAPE_EXPR}: an
|
|
3560
|
+
* `<input type="submit">` / `<input type="image">`, a `<button>`/`<input>`
|
|
3561
|
+
* carrying an EXPLICIT `type="submit"` attribute, or — the same
|
|
3562
|
+
* corroboration-requiring path a non-native control must clear — a
|
|
3563
|
+
* tag/role-agnostic control (e.g. a `<div>`/`<a>`) whose accessible name
|
|
3564
|
+
* reads a generic action verb, or is the sole actionable control in its
|
|
3565
|
+
* form. A default-type `<button>` merely owned by a `<form>` no longer
|
|
3566
|
+
* earns unconditional credit from that structural fact alone: the HTML
|
|
3567
|
+
* spec makes a button's default type submit, but structure alone is not
|
|
3568
|
+
* proof of submit intent (e.g. a "Show contact form" reveal toggle a page
|
|
3569
|
+
* happens to nest inside a `<form>`), so it must clear the same
|
|
3570
|
+
* corroboration bar as a non-native control. Reuses submit-control.ts's
|
|
3571
|
+
* own single predicate instead of a hand-duplicated tag/type-only check,
|
|
3572
|
+
* so a legitimately-clicked non-native submit control is not
|
|
3573
|
+
* mis-classified as "not submit-shaped" here. Returns `false` — never
|
|
3574
|
+
* manufactures a submit-shape veto — on a non-xpath selector, a miss, or an
|
|
3575
|
+
* evaluate failure.
|
|
3501
3576
|
*
|
|
3502
3577
|
* `xpathTail` (optional): the n+16 fallback's own `xpathTailForRetarget`
|
|
3503
3578
|
* re-anchor. Its `clickExpr` resolves primary-xpath-then-tail before
|
|
@@ -3516,15 +3591,10 @@ async function resolvedClickTargetIsSubmitShaped(target, selector, xpathTail = n
|
|
|
3516
3591
|
const r = document.evaluate(${JSON.stringify(xpath)}, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
|
|
3517
3592
|
let el = r.singleNodeValue;
|
|
3518
3593
|
if (!el && ${JSON.stringify(xpathTail)}) {
|
|
3519
|
-
|
|
3520
|
-
|
|
3521
|
-
}
|
|
3522
|
-
|
|
3523
|
-
const tag = el.tagName.toUpperCase();
|
|
3524
|
-
const type = (el.getAttribute("type") || "").toLowerCase();
|
|
3525
|
-
if (tag === "INPUT" && (type === "submit" || type === "image")) return true;
|
|
3526
|
-
if (tag === "BUTTON" && (type === "submit" || type === "") && el.closest("form")) return true;
|
|
3527
|
-
return false;
|
|
3594
|
+
el = ${XPATH_TAIL_RETARGET_RESOLVE_FN_SRC}(${JSON.stringify(xpathTail)});
|
|
3595
|
+
}
|
|
3596
|
+
const isSubmitShaped = ${submit_control_1.SUBMIT_SHAPE_EXPR};
|
|
3597
|
+
return isSubmitShaped(el);
|
|
3528
3598
|
})()`;
|
|
3529
3599
|
try {
|
|
3530
3600
|
return (await target.evaluate(expr)) === true;
|
|
@@ -7741,6 +7811,118 @@ function spliceEmailStepFillValue(step, code) {
|
|
|
7741
7811
|
function redactIfSensitive(text, sensitiveValue) {
|
|
7742
7812
|
return sensitiveValue && text === sensitiveValue ? "[redacted]" : text;
|
|
7743
7813
|
}
|
|
7814
|
+
/**
|
|
7815
|
+
* Last-resort escalation for the cascade-exhaustion path: when every
|
|
7816
|
+
* resolution technique has already failed against the resolved (declared-or-
|
|
7817
|
+
* main) frame and no `frameSelector` was declared for this step, probe every
|
|
7818
|
+
* same-origin CHILD frame Stagehand's CDP layer has attached to
|
|
7819
|
+
* (`page.frames()`) for a submit-shaped candidate, using the same
|
|
7820
|
+
* tag/role-agnostic ranking (`buildRankSubmitCandidatesExpr`) and click
|
|
7821
|
+
* primitive (`buildClickByDeepIndexExpr`) the declared-frame cascade already
|
|
7822
|
+
* trusts. This closes the "target lives inside an undeclared iframe" gap for
|
|
7823
|
+
* the ONE case that's safe to probe blindly: a same-origin child, where
|
|
7824
|
+
* `location.href` is readable and its origin can be compared directly against
|
|
7825
|
+
* the top document's. A cross-origin child (e.g. a checkout iframe payment
|
|
7826
|
+
* widget) is excluded on purpose — CDP's `Runtime.evaluate` can still reach a
|
|
7827
|
+
* cross-origin OOPIF's document, so an origin check (not a thrown evaluate)
|
|
7828
|
+
* is the only thing that would stop this function from guessing which
|
|
7829
|
+
* cross-origin iframe hosts the target with no declared selector to anchor
|
|
7830
|
+
* on. That guess is exactly what `frameSelector` exists so a flow author
|
|
7831
|
+
* never has to make; this fallback stays within the same-origin case the
|
|
7832
|
+
* gap report calls out and leaves cross-origin as the flow author's declared
|
|
7833
|
+
* `frameSelector` responsibility, unchanged.
|
|
7834
|
+
*
|
|
7835
|
+
* Every candidate frame is ranked and, on a hit, clicked and verified through
|
|
7836
|
+
* the SAME `snapshotPage`/`classifyPhantomClick` pre/post pair the cascade's
|
|
7837
|
+
* own `deep-submit-locator` escalation uses (including the single runner-up
|
|
7838
|
+
* retry on a phantom top pick) — there is no new unverified success path,
|
|
7839
|
+
* only a new place to look for the candidate before giving up.
|
|
7840
|
+
*
|
|
7841
|
+
* Returns the clicked `FrameTarget` on a verified (non-phantom) click, or
|
|
7842
|
+
* `null` if no same-origin child frame yielded a submit-shaped candidate, no
|
|
7843
|
+
* candidate was actually clickable, or every click phantomed.
|
|
7844
|
+
*/
|
|
7845
|
+
async function probeChildFrameSubmitFallback(params) {
|
|
7846
|
+
const { page, signalCounter } = params;
|
|
7847
|
+
// `page.mainFrameId`/`page.frames` are read defensively: some call sites
|
|
7848
|
+
// (and their test fakes) model a `Page` that never attaches child frames at
|
|
7849
|
+
// all and doesn't implement this pair, which is indistinguishable here from
|
|
7850
|
+
// "no child frames exist" — either way there is nothing for this fallback
|
|
7851
|
+
// to probe, so it degrades to its normal "nothing found" return rather than
|
|
7852
|
+
// throwing out of an otherwise-best-effort escalation.
|
|
7853
|
+
let candidateFrames;
|
|
7854
|
+
try {
|
|
7855
|
+
const mainFrameId = page.mainFrameId();
|
|
7856
|
+
candidateFrames = page.frames().filter((frame) => frame.frameId !== mainFrameId);
|
|
7857
|
+
}
|
|
7858
|
+
catch {
|
|
7859
|
+
return null;
|
|
7860
|
+
}
|
|
7861
|
+
if (candidateFrames.length === 0)
|
|
7862
|
+
return null;
|
|
7863
|
+
const pageOrigin = (() => {
|
|
7864
|
+
try {
|
|
7865
|
+
return new URL(page.url()).origin;
|
|
7866
|
+
}
|
|
7867
|
+
catch {
|
|
7868
|
+
return null;
|
|
7869
|
+
}
|
|
7870
|
+
})();
|
|
7871
|
+
if (!pageOrigin)
|
|
7872
|
+
return null;
|
|
7873
|
+
for (const frame of candidateFrames) {
|
|
7874
|
+
const frameUrl = await Promise.resolve(frame.evaluate("location.href")).catch(() => null);
|
|
7875
|
+
if (!frameUrl)
|
|
7876
|
+
continue;
|
|
7877
|
+
const frameOrigin = (() => {
|
|
7878
|
+
try {
|
|
7879
|
+
return new URL(frameUrl).origin;
|
|
7880
|
+
}
|
|
7881
|
+
catch {
|
|
7882
|
+
return null;
|
|
7883
|
+
}
|
|
7884
|
+
})();
|
|
7885
|
+
// Cross-origin: documented rule-out, not a code defect. See docblock.
|
|
7886
|
+
if (frameOrigin !== pageOrigin)
|
|
7887
|
+
continue;
|
|
7888
|
+
const target = (0, frame_target_1.childFrameTarget)(page, frame, "(auto-discovered same-origin iframe)");
|
|
7889
|
+
const rankResult = await Promise.resolve(target.evaluate((0, submit_control_1.buildRankSubmitCandidatesExpr)())).catch(() => null);
|
|
7890
|
+
const ranked = Array.isArray(rankResult) ? rankResult : [];
|
|
7891
|
+
if (ranked.length === 0)
|
|
7892
|
+
continue;
|
|
7893
|
+
// biome-ignore lint/style/noNonNullAssertion: guarded by the length check above
|
|
7894
|
+
const top = ranked[0];
|
|
7895
|
+
const pre = await snapshotPage(target, signalCounter, page);
|
|
7896
|
+
const clickResult = (await Promise.resolve(target.evaluate((0, submit_control_1.buildClickByDeepIndexExpr)(top.deepIndex))).catch(() => ({ clicked: false })));
|
|
7897
|
+
if (!clickResult.clicked)
|
|
7898
|
+
continue;
|
|
7899
|
+
const post = await snapshotPage(target, signalCounter, page);
|
|
7900
|
+
const verdict = (0, phantom_click_1.classifyPhantomClick)({
|
|
7901
|
+
actResultSuccess: true,
|
|
7902
|
+
pre,
|
|
7903
|
+
post,
|
|
7904
|
+
isSubmitShapedStep: true,
|
|
7905
|
+
});
|
|
7906
|
+
if (verdict !== "phantom")
|
|
7907
|
+
return target;
|
|
7908
|
+
const runnerUp = ranked[1];
|
|
7909
|
+
if (!runnerUp)
|
|
7910
|
+
continue;
|
|
7911
|
+
const runnerUpClickResult = (await Promise.resolve(target.evaluate((0, submit_control_1.buildClickByDeepIndexExpr)(runnerUp.deepIndex))).catch(() => ({ clicked: false })));
|
|
7912
|
+
if (!runnerUpClickResult.clicked)
|
|
7913
|
+
continue;
|
|
7914
|
+
const runnerUpPost = await snapshotPage(target, signalCounter, page);
|
|
7915
|
+
const runnerUpVerdict = (0, phantom_click_1.classifyPhantomClick)({
|
|
7916
|
+
actResultSuccess: true,
|
|
7917
|
+
pre,
|
|
7918
|
+
post: runnerUpPost,
|
|
7919
|
+
isSubmitShapedStep: true,
|
|
7920
|
+
});
|
|
7921
|
+
if (runnerUpVerdict !== "phantom")
|
|
7922
|
+
return target;
|
|
7923
|
+
}
|
|
7924
|
+
return null;
|
|
7925
|
+
}
|
|
7744
7926
|
async function executeStepWithHealing(params) {
|
|
7745
7927
|
const { stagehand, page, optional, upload, submitStep, captchaGated = false, emailStep = false, emailStepConfig, allocatedInbox = null, sessionProxy = null, flowHasSubmitSemantics: flowHasSubmitSemanticsFlag, stepIndex, totalSteps, phase, signalCounter, recentCaptures, recentCaptureMeta, anthropic, rephraseModel, logger, captureFn, uploadFixture, isFinalStep, submitEndpointPattern, submittedStateSelectors, requireSubmitEndpointMatch, advanceTransitionBodyPattern, successUrlFragments, successPageTitleHints, ownBackendHostnames, knownErrorClassPrefixes, wizardExitButtonLabels, getSuppressedAisdkElementIdErrorCount, trajectory, onStepFailure, onStepHeal, } = params;
|
|
7746
7928
|
// Mutable: the `emailStep` code-extract path splices the extracted code
|
|
@@ -8068,7 +8250,7 @@ async function executeStepWithHealing(params) {
|
|
|
8068
8250
|
// when no solver key is configured or the solve otherwise fails — that
|
|
8069
8251
|
// propagates out of this function unchanged, so the step fails cleanly
|
|
8070
8252
|
// instead of silently falling through as if unverified-but-passing.
|
|
8071
|
-
const isSubmitOrFinalStep = submitStep || (isFinalStep && flowHasSubmitSemanticsFlag);
|
|
8253
|
+
const isSubmitOrFinalStep = submitStep || (isFinalStep && flowHasSubmitSemanticsFlag) || isSubmitIntentStep(step);
|
|
8072
8254
|
if (captchaGated && isSubmitOrFinalStep) {
|
|
8073
8255
|
let captchaTarget = frameTarget ?? (0, frame_target_1.mainFrameTarget)(page);
|
|
8074
8256
|
const sitekeyProbeExpr = `(() => {
|
|
@@ -8410,6 +8592,25 @@ async function executeStepWithHealing(params) {
|
|
|
8410
8592
|
logger.error(`${formatStepPrefix(stepIndex, totalSteps)} backend error detected (submit endpoint returned 5xx: ${backendErrorUrl}); aborting cascade`);
|
|
8411
8593
|
throw new errors_2.StepVerificationError(`${formatStepPrefix(stepIndex, totalSteps)} (${step.slice(0, 60)}) backend 5xx at ${backendErrorUrl} — unrecoverable`, "backend-error-unrecoverable");
|
|
8412
8594
|
}
|
|
8595
|
+
// Bounded same-origin child-iframe fallback: the probe above only ever
|
|
8596
|
+
// looked at the resolved (declared-or-main) frame's own light+shadow DOM,
|
|
8597
|
+
// so a target genuinely inside an undeclared same-origin iframe is
|
|
8598
|
+
// invisible to it. Only tried when this step never declared its own
|
|
8599
|
+
// frameSelector — a declared-and-used frame means the flow author already
|
|
8600
|
+
// told us where to look, and guessing elsewhere would second-guess that —
|
|
8601
|
+
// and only when this step is itself submit-shaped, since the probe ranks
|
|
8602
|
+
// for a submit-shaped candidate and firing it on a non-submit step (e.g.
|
|
8603
|
+
// a radio/select answer) could complete the step by clicking an unrelated
|
|
8604
|
+
// submit control in an unrelated same-origin child iframe.
|
|
8605
|
+
if (!frameTarget?.declaredFrameSelector &&
|
|
8606
|
+
(submitStep || (isFinalStep && flowHasSubmitSemanticsFlag) || isSubmitIntentStep(step))) {
|
|
8607
|
+
const fallbackTarget = await probeChildFrameSubmitFallback({ page, signalCounter });
|
|
8608
|
+
if (fallbackTarget) {
|
|
8609
|
+
logger.info(`${formatStepPrefix(stepIndex, totalSteps)} probe-absent: no candidate in the resolved frame, but a same-origin child iframe surfaced a submit-shaped candidate — clicked and verified there instead of failing`);
|
|
8610
|
+
trajectory?.push({ stepIndex, verifiedBy: "dom" });
|
|
8611
|
+
return "completed";
|
|
8612
|
+
}
|
|
8613
|
+
}
|
|
8413
8614
|
// Capture diagnostics + write a failure dump BEFORE throwing so the
|
|
8414
8615
|
// global replan path's `readFailureDumpEvidence` can populate the
|
|
8415
8616
|
// prompt's CURRENTLY VISIBLE / UNFOCUSED OBSERVE / PAGE BODY HTML
|
|
@@ -8433,6 +8634,9 @@ async function executeStepWithHealing(params) {
|
|
|
8433
8634
|
const unfocusedObserve = probeAbsentObservedUnfocused.length === 0 && frameTarget?.frame
|
|
8434
8635
|
? await deepLocatorCandidatesAsActions(page, frameTarget)
|
|
8435
8636
|
: probeAbsentObservedUnfocused;
|
|
8637
|
+
const targetResolutionDiagnostic = submitStep || (isFinalStep && flowHasSubmitSemanticsFlag) || isSubmitIntentStep(step)
|
|
8638
|
+
? await (0, target_resolution_diagnostic_1.captureTargetResolutionDiagnosticSnapshot)(frameTarget, page)
|
|
8639
|
+
: null;
|
|
8436
8640
|
const dumpPath = onStepFailure?.({
|
|
8437
8641
|
stepIndex,
|
|
8438
8642
|
phase,
|
|
@@ -8444,6 +8648,7 @@ async function executeStepWithHealing(params) {
|
|
|
8444
8648
|
recentCaptures,
|
|
8445
8649
|
bodyOuterHtml,
|
|
8446
8650
|
unfocusedObserve,
|
|
8651
|
+
targetResolutionDiagnostic,
|
|
8447
8652
|
}) ?? null;
|
|
8448
8653
|
throw new errors_2.StepVerificationError(`${formatStepPrefix(stepIndex, totalSteps)} (${step.slice(0, 60)}) probe found no candidates on page${dumpPath ? `; see ${dumpPath}` : ""}`, "probe-absent");
|
|
8449
8654
|
}
|
|
@@ -8705,6 +8910,13 @@ async function executeStepWithHealing(params) {
|
|
|
8705
8910
|
}
|
|
8706
8911
|
};
|
|
8707
8912
|
let phantomClickAfterAttempt1 = false;
|
|
8913
|
+
let attempt1UnreachableViaLightDom = false;
|
|
8914
|
+
// Last candidate list the deep-submit-locator itself ranked this step, if
|
|
8915
|
+
// any — reused by the cascade-exhausted diagnostic snapshot below so it
|
|
8916
|
+
// doesn't re-run buildRankSubmitCandidatesExpr (and inflate the rank-call
|
|
8917
|
+
// count the phantom-click-escalation tests assert on) when the cascade
|
|
8918
|
+
// already has an up-to-date ranking to report from.
|
|
8919
|
+
let lastRankedSubmitCandidates = null;
|
|
8708
8920
|
for (let attempt = 1; attempt <= MAX_STEP_ATTEMPTS; attempt++) {
|
|
8709
8921
|
// Telemetry-driven technique-skip: when a cascade technique's
|
|
8710
8922
|
// preconditions cannot be met by the prior attempts' state, running
|
|
@@ -8713,7 +8925,8 @@ async function executeStepWithHealing(params) {
|
|
|
8713
8925
|
// techniques faster.
|
|
8714
8926
|
if (attempt > 1) {
|
|
8715
8927
|
const wouldBeTechnique = attempt === 2
|
|
8716
|
-
?
|
|
8928
|
+
? attempt1UnreachableViaLightDom &&
|
|
8929
|
+
(submitStep || (isFinalStep && flowHasSubmitSemanticsFlag) || isSubmitIntentStep(step))
|
|
8717
8930
|
? "deep-submit-locator"
|
|
8718
8931
|
: phantomClickAfterAttempt1
|
|
8719
8932
|
? "trusted-click-retry"
|
|
@@ -8731,8 +8944,8 @@ async function executeStepWithHealing(params) {
|
|
|
8731
8944
|
errorMessage: a.errorMessage,
|
|
8732
8945
|
})),
|
|
8733
8946
|
advanceUnmovedAfterAttempt1,
|
|
8734
|
-
|
|
8735
|
-
submitShapedStep: submitStep || (isFinalStep && flowHasSubmitSemanticsFlag),
|
|
8947
|
+
attempt1UnreachableViaLightDom,
|
|
8948
|
+
submitShapedStep: submitStep || (isFinalStep && flowHasSubmitSemanticsFlag) || isSubmitIntentStep(step),
|
|
8736
8949
|
});
|
|
8737
8950
|
if (decision.skip) {
|
|
8738
8951
|
logger.info(`${formatStepPrefix(stepIndex, totalSteps)} attempt ${attempt} (${wouldBeTechnique}) skipped: ${decision.reason}`);
|
|
@@ -8869,13 +9082,14 @@ async function executeStepWithHealing(params) {
|
|
|
8869
9082
|
}
|
|
8870
9083
|
}
|
|
8871
9084
|
else if (attempt === 2 &&
|
|
8872
|
-
|
|
8873
|
-
(submitStep || (isFinalStep && flowHasSubmitSemanticsFlag))) {
|
|
8874
|
-
// Deep submit-control locator: attempt 1 phantom-clicked
|
|
8875
|
-
// reported success but pre/post showed zero effect)
|
|
8876
|
-
//
|
|
8877
|
-
//
|
|
8878
|
-
//
|
|
9085
|
+
attempt1UnreachableViaLightDom &&
|
|
9086
|
+
(submitStep || (isFinalStep && flowHasSubmitSemanticsFlag) || isSubmitIntentStep(step))) {
|
|
9087
|
+
// Deep submit-control locator: attempt 1 either phantom-clicked
|
|
9088
|
+
// (Stagehand reported success but pre/post showed zero effect) or
|
|
9089
|
+
// came back "unresolved" (act() found zero candidates at all), so the
|
|
9090
|
+
// target is almost certainly unreachable via document.querySelectorAll
|
|
9091
|
+
// — most likely rendered inside an open shadow root by a web-component
|
|
9092
|
+
// / framework-native submit control (see recon-submit-phantom-click bug
|
|
8879
9093
|
// report). Rank every submit-shaped candidate the deep traversal can
|
|
8880
9094
|
// reach and click the top-ranked one; the ranking already excludes
|
|
8881
9095
|
// Back/Cancel/Save-draft-shaped controls, so a false-positive submit
|
|
@@ -8902,6 +9116,7 @@ async function executeStepWithHealing(params) {
|
|
|
8902
9116
|
record.errorMessage = `deep-submit-locator: rank evaluate threw ${(0, errors_1.toErrorMessage)(err)}`;
|
|
8903
9117
|
break;
|
|
8904
9118
|
}
|
|
9119
|
+
lastRankedSubmitCandidates = ranked;
|
|
8905
9120
|
if (ranked.length === 0) {
|
|
8906
9121
|
record.errorMessage = "deep-submit-locator: no submit-shaped candidate found";
|
|
8907
9122
|
break;
|
|
@@ -8994,7 +9209,7 @@ async function executeStepWithHealing(params) {
|
|
|
8994
9209
|
}
|
|
8995
9210
|
else if (attempt === 2 &&
|
|
8996
9211
|
phantomClickAfterAttempt1 &&
|
|
8997
|
-
!(submitStep || (isFinalStep && flowHasSubmitSemanticsFlag))) {
|
|
9212
|
+
!(submitStep || (isFinalStep && flowHasSubmitSemanticsFlag) || isSubmitIntentStep(step))) {
|
|
8998
9213
|
// Trusted-click retry: attempt 1 phantom-clicked a NON-submit control —
|
|
8999
9214
|
// Stagehand reported success but pre/post showed zero effect. On a
|
|
9000
9215
|
// design-system widget (React synthetic-event delegation, custom
|
|
@@ -9674,7 +9889,7 @@ async function executeStepWithHealing(params) {
|
|
|
9674
9889
|
// `advanceTransitionBodyPattern` are unaffected.
|
|
9675
9890
|
const domVerifiedForStep = isDomOnlyAdvanceVerified({
|
|
9676
9891
|
hasPattern: advanceTransitionBodyPattern !== null,
|
|
9677
|
-
isFinalOrSubmit: submitStep || (isFinalStep && flowHasSubmitSemanticsFlag),
|
|
9892
|
+
isFinalOrSubmit: submitStep || (isFinalStep && flowHasSubmitSemanticsFlag) || isSubmitIntentStep(step),
|
|
9678
9893
|
isAdvance: isAdvanceStep(step),
|
|
9679
9894
|
domVerified,
|
|
9680
9895
|
networkIsRealAdvance,
|
|
@@ -9762,7 +9977,8 @@ async function executeStepWithHealing(params) {
|
|
|
9762
9977
|
// gate (requireSubmitEndpoint) will re-litigate the credit below.
|
|
9763
9978
|
const formValueWeakSignalAllowed = !(submitStep ||
|
|
9764
9979
|
(isFinalStep && flowHasSubmitSemanticsFlag) ||
|
|
9765
|
-
resolvedElementIsSubmitShaped
|
|
9980
|
+
resolvedElementIsSubmitShaped ||
|
|
9981
|
+
isSubmitIntentStep(step)) || requireSubmitEndpoint;
|
|
9766
9982
|
const formValueVerified = isStateClass &&
|
|
9767
9983
|
formValueWeakSignalAllowed &&
|
|
9768
9984
|
post.formValueSignature !== pre.formValueSignature;
|
|
@@ -9868,7 +10084,7 @@ async function executeStepWithHealing(params) {
|
|
|
9868
10084
|
pre,
|
|
9869
10085
|
post,
|
|
9870
10086
|
elementStateChanged: domVerified,
|
|
9871
|
-
isSubmitShapedStep: submitStep || (isFinalStep && flowHasSubmitSemanticsFlag),
|
|
10087
|
+
isSubmitShapedStep: submitStep || (isFinalStep && flowHasSubmitSemanticsFlag) || isSubmitIntentStep(step),
|
|
9872
10088
|
});
|
|
9873
10089
|
// An `"effective"` verdict driven purely by the page-wide byte-delta
|
|
9874
10090
|
// floor (`TRIVIAL_DOM_DELTA_BYTES`, 500B) is intentionally NOT trusted
|
|
@@ -9889,7 +10105,8 @@ async function executeStepWithHealing(params) {
|
|
|
9889
10105
|
// respects the SAME DOM-only-advance veto as the block above: a wizard
|
|
9890
10106
|
// "Next" step on a pattern-configured site whose only signal is a field
|
|
9891
10107
|
// toggle must stay unverified, not get waved through by the verdict.
|
|
9892
|
-
const domEffectiveVerdict = !(submitStep || (isFinalStep && flowHasSubmitSemanticsFlag)) &&
|
|
10108
|
+
const domEffectiveVerdict = !(submitStep || (isFinalStep && flowHasSubmitSemanticsFlag) || isSubmitIntentStep(step)) &&
|
|
10109
|
+
domVerifiedForStep;
|
|
9893
10110
|
let verified = networkIsRealAdvance ||
|
|
9894
10111
|
urlChanged ||
|
|
9895
10112
|
domVerifiedForStep ||
|
|
@@ -10140,7 +10357,7 @@ async function executeStepWithHealing(params) {
|
|
|
10140
10357
|
const n16TrustedClickFailure = !n16TrustedDelivered && "reason" in n16TrustedClickResult
|
|
10141
10358
|
? n16TrustedClickResult
|
|
10142
10359
|
: null;
|
|
10143
|
-
const clickExpr = `(() => { const r = document.evaluate(${JSON.stringify(xpath)}, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null); let el = r.singleNodeValue; if (!el && ${JSON.stringify(xpathTail)}) {
|
|
10360
|
+
const clickExpr = `(() => { const r = document.evaluate(${JSON.stringify(xpath)}, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null); let el = r.singleNodeValue; if (!el && ${JSON.stringify(xpathTail)}) { el = ${XPATH_TAIL_RETARGET_RESOLVE_FN_SRC}(${JSON.stringify(xpathTail)}); } if (!el || typeof el.click !== "function") return { fired: false }; if (el.tagName === "LABEL") { const wrapped = el.querySelector("input[type=checkbox], input[type=radio]"); if (wrapped) el = wrapped; } if (el.type === "checkbox" || el.type === "radio") { el.checked = true; el.dispatchEvent(new Event("click", { bubbles: true })); el.dispatchEvent(new Event("change", { bubbles: true })); return { fired: true, kind: "checkbox", checked: el.checked }; } let __n16SmMatched = false; ${(0, browser_click_expr_1.retargetToSelectionMarkerExpr)("el", "__n16SmMatched")} ${n16TrustedDelivered ? "" : (0, browser_click_expr_1.clickActivationExpr)("el")} if (__n16SmMatched) { el.dispatchEvent(new Event("change", { bubbles: true })); } return { fired: true, kind: "click" }; })()`;
|
|
10144
10361
|
const n16FallbackTarget = frameTarget ?? (0, frame_target_1.mainFrameTarget)(page);
|
|
10145
10362
|
const probeResult = (await n16FallbackTarget.evaluate(clickExpr));
|
|
10146
10363
|
const fired = probeResult.fired;
|
|
@@ -10159,8 +10376,7 @@ async function executeStepWithHealing(params) {
|
|
|
10159
10376
|
const r = document.evaluate(${JSON.stringify(xpath)}, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
|
|
10160
10377
|
let node = r.singleNodeValue;
|
|
10161
10378
|
if (!node && ${JSON.stringify(xpathTail)}) {
|
|
10162
|
-
|
|
10163
|
-
node = r2.singleNodeValue;
|
|
10379
|
+
node = ${XPATH_TAIL_RETARGET_RESOLVE_FN_SRC}(${JSON.stringify(xpathTail)});
|
|
10164
10380
|
}
|
|
10165
10381
|
if (!node) return false;
|
|
10166
10382
|
for (let depth = 0; depth < 6 && node; depth++) {
|
|
@@ -10243,8 +10459,7 @@ async function executeStepWithHealing(params) {
|
|
|
10243
10459
|
const r = document.evaluate(${JSON.stringify(xpath)}, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
|
|
10244
10460
|
let el = r.singleNodeValue;
|
|
10245
10461
|
if (!el && ${JSON.stringify(xpathTail)}) {
|
|
10246
|
-
|
|
10247
|
-
el = r2.singleNodeValue;
|
|
10462
|
+
el = ${XPATH_TAIL_RETARGET_RESOLVE_FN_SRC}(${JSON.stringify(xpathTail)});
|
|
10248
10463
|
}
|
|
10249
10464
|
return el ? isDisabled(el) : false;
|
|
10250
10465
|
})()`;
|
|
@@ -10284,7 +10499,7 @@ async function executeStepWithHealing(params) {
|
|
|
10284
10499
|
}));
|
|
10285
10500
|
const fallbackDomOnlyAdvance = shouldVetoFallbackAdvance({
|
|
10286
10501
|
hasPattern: advanceTransitionBodyPattern !== null,
|
|
10287
|
-
isFinalOrSubmit: submitStep || (isFinalStep && flowHasSubmitSemanticsFlag),
|
|
10502
|
+
isFinalOrSubmit: submitStep || (isFinalStep && flowHasSubmitSemanticsFlag) || isSubmitIntentStep(step),
|
|
10288
10503
|
isAdvance: isAdvanceStep(step),
|
|
10289
10504
|
retryUrlChanged,
|
|
10290
10505
|
retryNetworkIsRealAdvance,
|
|
@@ -10345,7 +10560,8 @@ async function executeStepWithHealing(params) {
|
|
|
10345
10560
|
// value is read below.
|
|
10346
10561
|
const retrySubmitShaped = submitStep ||
|
|
10347
10562
|
(isFinalStep && flowHasSubmitSemanticsFlag) ||
|
|
10348
|
-
retryResolvedElementIsSubmitShaped
|
|
10563
|
+
retryResolvedElementIsSubmitShaped ||
|
|
10564
|
+
isSubmitIntentStep(step);
|
|
10349
10565
|
const weakDomSignalsAllowed = ((!isFinalStep && !submitStep && !retryResolvedElementIsSubmitShaped) ||
|
|
10350
10566
|
requireSubmitEndpoint) &&
|
|
10351
10567
|
!isCheckboxOrRadioIntentStep(step) &&
|
|
@@ -10568,7 +10784,7 @@ async function executeStepWithHealing(params) {
|
|
|
10568
10784
|
// dumps in a 2026-06-10 survey had the paired touched+dirty + visible
|
|
10569
10785
|
// error text pattern with 3 distinct rejection messages.
|
|
10570
10786
|
if (record.resolvedMethod === "click" &&
|
|
10571
|
-
(submitStep || (isFinalStep && flowHasSubmitSemanticsFlag))) {
|
|
10787
|
+
(submitStep || (isFinalStep && flowHasSubmitSemanticsFlag) || isSubmitIntentStep(step))) {
|
|
10572
10788
|
const live = await extractLivePageFormEvidence(page, frameTarget ?? (0, frame_target_1.mainFrameTarget)(page), {
|
|
10573
10789
|
client: anthropic,
|
|
10574
10790
|
knownErrorClassPrefixes,
|
|
@@ -10604,26 +10820,44 @@ async function executeStepWithHealing(params) {
|
|
|
10604
10820
|
isAdvanceStep(step) &&
|
|
10605
10821
|
!urlChanged &&
|
|
10606
10822
|
!networkIsRealAdvance;
|
|
10607
|
-
// Attempt 1
|
|
10608
|
-
//
|
|
10609
|
-
//
|
|
10610
|
-
//
|
|
10611
|
-
//
|
|
10612
|
-
//
|
|
10823
|
+
// Attempt 1's classifyPhantomClick verdict: "phantom" means Stagehand
|
|
10824
|
+
// reported success but pre/post shows zero observable effect; the
|
|
10825
|
+
// zero-effect delta is the primary signal there (classifyPhantomClick
|
|
10826
|
+
// above). "unresolved" means act() found zero candidates at all — a
|
|
10827
|
+
// strictly stronger signal that the target is unreachable via the
|
|
10828
|
+
// light-DOM resolver, since there wasn't even a candidate to click. The
|
|
10829
|
+
// live AISDK elementId suppression counter is corroborating evidence
|
|
10830
|
+
// only — logged, never gating, since a nonzero count alone is too weak
|
|
10831
|
+
// a signal on a run that sees dozens of suppressions across hundreds of
|
|
10832
|
+
// unrelated steps.
|
|
10613
10833
|
phantomClickAfterAttempt1 = record.phantomClickVerdict === "phantom";
|
|
10614
|
-
|
|
10834
|
+
const attempt1Unresolved = record.phantomClickVerdict === "unresolved";
|
|
10835
|
+
attempt1UnreachableViaLightDom = phantomClickAfterAttempt1 || attempt1Unresolved;
|
|
10836
|
+
if (attempt1UnreachableViaLightDom) {
|
|
10615
10837
|
const suppressedCount = getSuppressedAisdkElementIdErrorCount?.();
|
|
10616
|
-
const
|
|
10838
|
+
const verdictDescription = phantomClickAfterAttempt1
|
|
10839
|
+
? "phantom click detected"
|
|
10840
|
+
: "no candidates resolved (unresolved verdict)";
|
|
10841
|
+
const outcomeDescription = phantomClickAfterAttempt1
|
|
10842
|
+
? "reported success with no network/url/dom change"
|
|
10843
|
+
: "act() found zero candidates";
|
|
10844
|
+
// trusted-click-retry needs attempt 1's resolved selector
|
|
10845
|
+
// (triedSelectors), which is empty on an "unresolved" verdict, so a
|
|
10846
|
+
// non-submit step with that verdict falls through to the normal
|
|
10847
|
+
// ladder instead of escalating.
|
|
10848
|
+
const escalationTarget = submitStep || (isFinalStep && flowHasSubmitSemanticsFlag) || isSubmitIntentStep(step)
|
|
10617
10849
|
? "escalating attempt 2 to deep-submit-locator"
|
|
10618
|
-
:
|
|
10619
|
-
|
|
10850
|
+
: phantomClickAfterAttempt1
|
|
10851
|
+
? "non-submit step — escalating attempt 2 to trusted-click-retry (trusted CDP click on the resolved target)"
|
|
10852
|
+
: "non-submit step — no resolved target for trusted-click-retry; attempt 2 continues the normal ladder";
|
|
10853
|
+
logger.warn(`${formatStepPrefix(stepIndex, totalSteps)} ${verdictDescription} on attempt 1 (${record.technique}): ${outcomeDescription}${suppressedCount !== undefined ? `; ${suppressedCount} AISDK elementId errors suppressed this session (corroborating, not causal)` : ""} — ${escalationTarget}`);
|
|
10620
10854
|
}
|
|
10621
10855
|
const postAttemptInvalidCount = await countNgInvalidContainers(frameTarget ?? (0, frame_target_1.mainFrameTarget)(page));
|
|
10622
10856
|
const earlyExit = isSubmitRevealedInvalid({
|
|
10623
10857
|
// Treat the canonical submit click as "final" for this predicate
|
|
10624
10858
|
// even when it lives mid-flow. See requireSubmitEndpoint derivation
|
|
10625
10859
|
// above for the same gate-widening rationale.
|
|
10626
|
-
isFinalStep: submitStep || (isFinalStep && flowHasSubmitSemanticsFlag),
|
|
10860
|
+
isFinalStep: submitStep || (isFinalStep && flowHasSubmitSemanticsFlag) || isSubmitIntentStep(step),
|
|
10627
10861
|
requireSubmitEndpoint,
|
|
10628
10862
|
resolvedMethod: record.resolvedMethod,
|
|
10629
10863
|
effectSignals,
|
|
@@ -10643,7 +10877,7 @@ async function executeStepWithHealing(params) {
|
|
|
10643
10877
|
// can reorder a later step forward — instead of burning the cascade.
|
|
10644
10878
|
const advanceStalled = isAdvanceStalled({
|
|
10645
10879
|
isAdvance: isAdvanceStep(step),
|
|
10646
|
-
isFinalOrSubmit: submitStep || (isFinalStep && flowHasSubmitSemanticsFlag),
|
|
10880
|
+
isFinalOrSubmit: submitStep || (isFinalStep && flowHasSubmitSemanticsFlag) || isSubmitIntentStep(step),
|
|
10647
10881
|
hasPattern: advanceTransitionBodyPattern !== null,
|
|
10648
10882
|
clickFired: record.resolvedMethod === "click" && record.actResultSuccess === true,
|
|
10649
10883
|
networkFired,
|
|
@@ -10658,6 +10892,26 @@ async function executeStepWithHealing(params) {
|
|
|
10658
10892
|
}
|
|
10659
10893
|
}
|
|
10660
10894
|
}
|
|
10895
|
+
// Bounded same-origin child-iframe fallback: every attempt above only ever
|
|
10896
|
+
// operated against the resolved (declared-or-main) frame, so a target
|
|
10897
|
+
// genuinely inside an undeclared same-origin iframe was invisible to all of
|
|
10898
|
+
// them. Only tried when this step never declared its own frameSelector — a
|
|
10899
|
+
// declared-and-used frame means the flow author already told us where to
|
|
10900
|
+
// look, so this can only ever fire on a case that today always fails
|
|
10901
|
+
// (no regression risk to any currently-passing path) — and only when this
|
|
10902
|
+
// step is itself submit-shaped, matching every other
|
|
10903
|
+
// buildRankSubmitCandidatesExpr call site's guard in this file, since
|
|
10904
|
+
// firing on a non-submit step could complete it by clicking an unrelated
|
|
10905
|
+
// submit control in an unrelated same-origin child iframe.
|
|
10906
|
+
if (!frameTarget?.declaredFrameSelector &&
|
|
10907
|
+
(submitStep || (isFinalStep && flowHasSubmitSemanticsFlag) || isSubmitIntentStep(step))) {
|
|
10908
|
+
const fallbackTarget = await probeChildFrameSubmitFallback({ page, signalCounter });
|
|
10909
|
+
if (fallbackTarget) {
|
|
10910
|
+
logger.info(`${formatStepPrefix(stepIndex, totalSteps)} cascade-exhausted: no candidate resolvable in the declared/main frame, but a same-origin child iframe surfaced a submit-shaped candidate — clicked and verified there instead of failing`);
|
|
10911
|
+
trajectory?.push({ stepIndex, verifiedBy: "dom" });
|
|
10912
|
+
return "completed";
|
|
10913
|
+
}
|
|
10914
|
+
}
|
|
10661
10915
|
const cascadeExhaustObservedFinal = await (0, stagehand_guard_1.guardedObserve)(stagehand, step, { timeout: exports.STEP_WATCHDOG_MS }, captureFn, frameTarget).catch(() => []);
|
|
10662
10916
|
// observe() is blind to a cross-origin OOPIF, so a frame-scoped empty
|
|
10663
10917
|
// result degrades to the deep-locator resolver for dump evidence — this
|
|
@@ -10680,6 +10934,9 @@ async function executeStepWithHealing(params) {
|
|
|
10680
10934
|
const unfocusedObserve = cascadeExhaustObservedUnfocused.length === 0 && frameTarget?.frame
|
|
10681
10935
|
? await deepLocatorCandidatesAsActions(page, frameTarget)
|
|
10682
10936
|
: cascadeExhaustObservedUnfocused;
|
|
10937
|
+
const targetResolutionDiagnostic = submitStep || (isFinalStep && flowHasSubmitSemanticsFlag) || isSubmitIntentStep(step)
|
|
10938
|
+
? await (0, target_resolution_diagnostic_1.captureTargetResolutionDiagnosticSnapshot)(frameTarget, page, lastRankedSubmitCandidates)
|
|
10939
|
+
: null;
|
|
10683
10940
|
const dumpPath = onStepFailure?.({
|
|
10684
10941
|
stepIndex,
|
|
10685
10942
|
phase,
|
|
@@ -10691,6 +10948,7 @@ async function executeStepWithHealing(params) {
|
|
|
10691
10948
|
recentCaptures,
|
|
10692
10949
|
bodyOuterHtml,
|
|
10693
10950
|
unfocusedObserve,
|
|
10951
|
+
targetResolutionDiagnostic,
|
|
10694
10952
|
}) ?? null;
|
|
10695
10953
|
if (dumpPath !== null) {
|
|
10696
10954
|
logger.error(`${formatStepPrefix(stepIndex, totalSteps)} failed after ${MAX_STEP_ATTEMPTS} attempts; diagnostic bundle: ${dumpPath}`);
|