@enricai/barnacle 1.12.70 → 1.12.71
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/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 +332 -61
- 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 +30 -2
- package/dist/scraper/submit-control.d.ts.map +1 -1
- package/dist/scraper/submit-control.js +79 -20
- 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.map +1 -1
- package/dist/scripts/recon-generate.js +84 -27
- 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)
|
|
@@ -1698,7 +1730,7 @@ function describeAttemptEffectSignals(pre, post, recentCaptureMeta, preMetaLengt
|
|
|
1698
1730
|
* keeps healing opportunistically.
|
|
1699
1731
|
*/
|
|
1700
1732
|
function shouldSkipTechnique(params) {
|
|
1701
|
-
const { technique, priorAttempts, advanceUnmovedAfterAttempt1,
|
|
1733
|
+
const { technique, priorAttempts, advanceUnmovedAfterAttempt1, attempt1UnreachableViaLightDom, submitShapedStep, } = params;
|
|
1702
1734
|
// Unmoved-advance short-circuit (measured: attempts 2-4 recovered a stuck
|
|
1703
1735
|
// advance 0 times in 289 steps). When attempt-1's act-string clicked the Next
|
|
1704
1736
|
// and the wizard did NOT move forward (non-advancing POST, or no effect),
|
|
@@ -1719,14 +1751,16 @@ function shouldSkipTechnique(params) {
|
|
|
1719
1751
|
reason: "advance step did not move the wizard on attempt 1; re-observe/re-click cannot advance it — skipping to rephrase/replan",
|
|
1720
1752
|
};
|
|
1721
1753
|
}
|
|
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
|
-
//
|
|
1754
|
+
// Unreachable-via-light-DOM short-circuit: attempt 1 either phantom-clicked
|
|
1755
|
+
// (Stagehand reported success but pre/post shows zero network, zero URL
|
|
1756
|
+
// change, and no real DOM growth) or came back "unresolved" (act() found
|
|
1757
|
+
// zero candidates at all) — the recon-submit-phantom-click bug report's
|
|
1758
|
+
// light-DOM resolver can't see into a shadow root / web component in
|
|
1759
|
+
// either case, and "unresolved" is if anything stronger evidence of that
|
|
1760
|
+
// than "phantom" (there was no candidate to even click). Repeating
|
|
1761
|
+
// observe-act / structured-click / observe-act-exclude re-resolves the
|
|
1762
|
+
// SAME light-DOM-only view of the page and would no-op identically, so
|
|
1763
|
+
// skip straight to deep-submit-locator (attempt 2) instead. llm-rephrase
|
|
1730
1764
|
// (attempt 4) is never skipped — a differently-worded instruction is still
|
|
1731
1765
|
// a distinct attempt worth trying if the deep locator also fails. Gated on
|
|
1732
1766
|
// submitShapedStep: the deep submit-control locator ranks submit-shaped
|
|
@@ -1734,14 +1768,14 @@ function shouldSkipTechnique(params) {
|
|
|
1734
1768
|
// is a guaranteed no-op — leave the normal ladder intact so
|
|
1735
1769
|
// structured-click / observe-act-exclude, the techniques that can
|
|
1736
1770
|
// actually click it, still run.
|
|
1737
|
-
if (
|
|
1771
|
+
if (attempt1UnreachableViaLightDom === true &&
|
|
1738
1772
|
submitShapedStep === true &&
|
|
1739
1773
|
(technique === "observe-act" ||
|
|
1740
1774
|
technique === "structured-click" ||
|
|
1741
1775
|
technique === "observe-act-exclude")) {
|
|
1742
1776
|
return {
|
|
1743
1777
|
skip: true,
|
|
1744
|
-
reason: "attempt 1
|
|
1778
|
+
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
1779
|
};
|
|
1746
1780
|
}
|
|
1747
1781
|
if (technique === "structured-click") {
|
|
@@ -3237,6 +3271,46 @@ function xpathTailForRetarget(xpath) {
|
|
|
3237
3271
|
const tail = steps.slice(-2).filter(Boolean);
|
|
3238
3272
|
return tail.length > 0 ? tail.join("/") : null;
|
|
3239
3273
|
}
|
|
3274
|
+
/**
|
|
3275
|
+
* In-page source for `(tail) => Element | null`: resolves
|
|
3276
|
+
* {@link xpathTailForRetarget}'s loose `//`+tail re-anchor against the live
|
|
3277
|
+
* DOM, disambiguating when the loose pattern matches more than one element.
|
|
3278
|
+
* The tail deliberately drops every attribute above the leaf+parent tag
|
|
3279
|
+
* pair, so on a page with several same-tag-shaped siblings (e.g. a repeated
|
|
3280
|
+
* "sign in" control sitting where a "create account" submit button used to
|
|
3281
|
+
* resolve) `FIRST_ORDERED_NODE_TYPE` silently accepts whichever one happens
|
|
3282
|
+
* to come first in document order — not necessarily the element the primary
|
|
3283
|
+
* xpath was tracking. Snapshotting every match and preferring one that is
|
|
3284
|
+
* objectively submit-shaped — native `type="submit"`/`type="image"`/
|
|
3285
|
+
* form-owned button, or (reusing submit-control.ts's exported
|
|
3286
|
+
* {@link SUBMIT_SHAPE_FALLBACK_EXPR}) a tag/role-agnostic generic-action
|
|
3287
|
+
* control — and not negative-text (reuses submit-control.ts's own
|
|
3288
|
+
* {@link NEGATIVE_TEXT_EXPR} vocabulary rather than a second copy) recovers
|
|
3289
|
+
* 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 isFallbackShaped = ${submit_control_1.SUBMIT_SHAPE_FALLBACK_EXPR};
|
|
3299
|
+
const isSubmitShaped = (el) => {
|
|
3300
|
+
if (!el || !el.tagName) return false;
|
|
3301
|
+
const tag = el.tagName.toUpperCase();
|
|
3302
|
+
const type = (el.getAttribute("type") || "").toLowerCase();
|
|
3303
|
+
if (tag === "INPUT" && (type === "submit" || type === "image")) return true;
|
|
3304
|
+
if (tag === "BUTTON" && (type === "submit" || type === "") && el.closest("form")) return true;
|
|
3305
|
+
return isFallbackShaped(el, accessibleName(el), isNegative);
|
|
3306
|
+
};
|
|
3307
|
+
const r = document.evaluate("//" + tail, document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
|
|
3308
|
+
const nodes = [];
|
|
3309
|
+
for (let i = 0; i < r.snapshotLength; i++) nodes.push(r.snapshotItem(i));
|
|
3310
|
+
if (nodes.length <= 1) return nodes[0] || null;
|
|
3311
|
+
const preferred = nodes.find((el) => isSubmitShaped(el) && !isNegative(accessibleName(el)));
|
|
3312
|
+
return preferred || nodes[0];
|
|
3313
|
+
})`;
|
|
3240
3314
|
/**
|
|
3241
3315
|
* Resolve any selector a caller might hold into a bare XPath body for
|
|
3242
3316
|
* `document.evaluate`. `verifyFillReadback` is shared across call sites that
|
|
@@ -3487,17 +3561,21 @@ async function resolvedClickTargetStillPresent(target, selector) {
|
|
|
3487
3561
|
}
|
|
3488
3562
|
}
|
|
3489
3563
|
/**
|
|
3490
|
-
* Site-agnostic
|
|
3491
|
-
*
|
|
3492
|
-
*
|
|
3493
|
-
*
|
|
3494
|
-
*
|
|
3495
|
-
*
|
|
3496
|
-
*
|
|
3497
|
-
*
|
|
3498
|
-
*
|
|
3499
|
-
*
|
|
3500
|
-
*
|
|
3564
|
+
* Site-agnostic submit-shape signal read directly from the resolved click
|
|
3565
|
+
* target — the counterpart to the flow-authored `submitStep` flag. A step
|
|
3566
|
+
* the flow never flagged can still resolve onto an objectively submit-shaped
|
|
3567
|
+
* control: `<input type="submit">` / `<input type="image">`, a `<button>`
|
|
3568
|
+
* with no `type` (or `type="submit"`) owned by a `<form>` — per the HTML
|
|
3569
|
+
* spec a button's default type IS submit, so an unmarked in-form button
|
|
3570
|
+
* commits the form exactly like an explicit `type="submit"` one — or, via
|
|
3571
|
+
* {@link SUBMIT_SHAPE_FALLBACK_EXPR}, a tag/role-agnostic control (e.g. a
|
|
3572
|
+
* `<div>`/`<a>`) whose accessible name reads a generic action verb, or is the
|
|
3573
|
+
* sole actionable control in its form. Reuses submit-control.ts's own
|
|
3574
|
+
* widened predicate instead of a narrower, hand-duplicated tag/type-only
|
|
3575
|
+
* check, so a legitimately-clicked non-native submit control is not
|
|
3576
|
+
* mis-classified as "not submit-shaped" here. Returns `false` — never
|
|
3577
|
+
* manufactures a submit-shape veto — on a non-xpath selector, a miss, or an
|
|
3578
|
+
* evaluate failure.
|
|
3501
3579
|
*
|
|
3502
3580
|
* `xpathTail` (optional): the n+16 fallback's own `xpathTailForRetarget`
|
|
3503
3581
|
* re-anchor. Its `clickExpr` resolves primary-xpath-then-tail before
|
|
@@ -3516,15 +3594,20 @@ async function resolvedClickTargetIsSubmitShaped(target, selector, xpathTail = n
|
|
|
3516
3594
|
const r = document.evaluate(${JSON.stringify(xpath)}, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
|
|
3517
3595
|
let el = r.singleNodeValue;
|
|
3518
3596
|
if (!el && ${JSON.stringify(xpathTail)}) {
|
|
3519
|
-
|
|
3520
|
-
el = r2.singleNodeValue;
|
|
3597
|
+
el = ${XPATH_TAIL_RETARGET_RESOLVE_FN_SRC}(${JSON.stringify(xpathTail)});
|
|
3521
3598
|
}
|
|
3522
3599
|
if (!el || !el.tagName) return false;
|
|
3523
3600
|
const tag = el.tagName.toUpperCase();
|
|
3524
3601
|
const type = (el.getAttribute("type") || "").toLowerCase();
|
|
3525
3602
|
if (tag === "INPUT" && (type === "submit" || type === "image")) return true;
|
|
3526
3603
|
if (tag === "BUTTON" && (type === "submit" || type === "") && el.closest("form")) return true;
|
|
3527
|
-
|
|
3604
|
+
const isNegative = ${submit_control_1.NEGATIVE_TEXT_EXPR};
|
|
3605
|
+
const accessibleName = (node) =>
|
|
3606
|
+
(node.getAttribute("aria-label") || node.textContent || "").replace(/\\s+/g, " ").trim().toLowerCase();
|
|
3607
|
+
const isFallbackShaped = ${submit_control_1.SUBMIT_SHAPE_FALLBACK_EXPR};
|
|
3608
|
+
const name = accessibleName(el);
|
|
3609
|
+
if (isNegative(name)) return false;
|
|
3610
|
+
return isFallbackShaped(el, name, isNegative);
|
|
3528
3611
|
})()`;
|
|
3529
3612
|
try {
|
|
3530
3613
|
return (await target.evaluate(expr)) === true;
|
|
@@ -7741,6 +7824,118 @@ function spliceEmailStepFillValue(step, code) {
|
|
|
7741
7824
|
function redactIfSensitive(text, sensitiveValue) {
|
|
7742
7825
|
return sensitiveValue && text === sensitiveValue ? "[redacted]" : text;
|
|
7743
7826
|
}
|
|
7827
|
+
/**
|
|
7828
|
+
* Last-resort escalation for the cascade-exhaustion path: when every
|
|
7829
|
+
* resolution technique has already failed against the resolved (declared-or-
|
|
7830
|
+
* main) frame and no `frameSelector` was declared for this step, probe every
|
|
7831
|
+
* same-origin CHILD frame Stagehand's CDP layer has attached to
|
|
7832
|
+
* (`page.frames()`) for a submit-shaped candidate, using the same
|
|
7833
|
+
* tag/role-agnostic ranking (`buildRankSubmitCandidatesExpr`) and click
|
|
7834
|
+
* primitive (`buildClickByDeepIndexExpr`) the declared-frame cascade already
|
|
7835
|
+
* trusts. This closes the "target lives inside an undeclared iframe" gap for
|
|
7836
|
+
* the ONE case that's safe to probe blindly: a same-origin child, where
|
|
7837
|
+
* `location.href` is readable and its origin can be compared directly against
|
|
7838
|
+
* the top document's. A cross-origin child (e.g. a checkout iframe payment
|
|
7839
|
+
* widget) is excluded on purpose — CDP's `Runtime.evaluate` can still reach a
|
|
7840
|
+
* cross-origin OOPIF's document, so an origin check (not a thrown evaluate)
|
|
7841
|
+
* is the only thing that would stop this function from guessing which
|
|
7842
|
+
* cross-origin iframe hosts the target with no declared selector to anchor
|
|
7843
|
+
* on. That guess is exactly what `frameSelector` exists so a flow author
|
|
7844
|
+
* never has to make; this fallback stays within the same-origin case the
|
|
7845
|
+
* gap report calls out and leaves cross-origin as the flow author's declared
|
|
7846
|
+
* `frameSelector` responsibility, unchanged.
|
|
7847
|
+
*
|
|
7848
|
+
* Every candidate frame is ranked and, on a hit, clicked and verified through
|
|
7849
|
+
* the SAME `snapshotPage`/`classifyPhantomClick` pre/post pair the cascade's
|
|
7850
|
+
* own `deep-submit-locator` escalation uses (including the single runner-up
|
|
7851
|
+
* retry on a phantom top pick) — there is no new unverified success path,
|
|
7852
|
+
* only a new place to look for the candidate before giving up.
|
|
7853
|
+
*
|
|
7854
|
+
* Returns the clicked `FrameTarget` on a verified (non-phantom) click, or
|
|
7855
|
+
* `null` if no same-origin child frame yielded a submit-shaped candidate, no
|
|
7856
|
+
* candidate was actually clickable, or every click phantomed.
|
|
7857
|
+
*/
|
|
7858
|
+
async function probeChildFrameSubmitFallback(params) {
|
|
7859
|
+
const { page, signalCounter } = params;
|
|
7860
|
+
// `page.mainFrameId`/`page.frames` are read defensively: some call sites
|
|
7861
|
+
// (and their test fakes) model a `Page` that never attaches child frames at
|
|
7862
|
+
// all and doesn't implement this pair, which is indistinguishable here from
|
|
7863
|
+
// "no child frames exist" — either way there is nothing for this fallback
|
|
7864
|
+
// to probe, so it degrades to its normal "nothing found" return rather than
|
|
7865
|
+
// throwing out of an otherwise-best-effort escalation.
|
|
7866
|
+
let candidateFrames;
|
|
7867
|
+
try {
|
|
7868
|
+
const mainFrameId = page.mainFrameId();
|
|
7869
|
+
candidateFrames = page.frames().filter((frame) => frame.frameId !== mainFrameId);
|
|
7870
|
+
}
|
|
7871
|
+
catch {
|
|
7872
|
+
return null;
|
|
7873
|
+
}
|
|
7874
|
+
if (candidateFrames.length === 0)
|
|
7875
|
+
return null;
|
|
7876
|
+
const pageOrigin = (() => {
|
|
7877
|
+
try {
|
|
7878
|
+
return new URL(page.url()).origin;
|
|
7879
|
+
}
|
|
7880
|
+
catch {
|
|
7881
|
+
return null;
|
|
7882
|
+
}
|
|
7883
|
+
})();
|
|
7884
|
+
if (!pageOrigin)
|
|
7885
|
+
return null;
|
|
7886
|
+
for (const frame of candidateFrames) {
|
|
7887
|
+
const frameUrl = await Promise.resolve(frame.evaluate("location.href")).catch(() => null);
|
|
7888
|
+
if (!frameUrl)
|
|
7889
|
+
continue;
|
|
7890
|
+
const frameOrigin = (() => {
|
|
7891
|
+
try {
|
|
7892
|
+
return new URL(frameUrl).origin;
|
|
7893
|
+
}
|
|
7894
|
+
catch {
|
|
7895
|
+
return null;
|
|
7896
|
+
}
|
|
7897
|
+
})();
|
|
7898
|
+
// Cross-origin: documented rule-out, not a code defect. See docblock.
|
|
7899
|
+
if (frameOrigin !== pageOrigin)
|
|
7900
|
+
continue;
|
|
7901
|
+
const target = (0, frame_target_1.childFrameTarget)(page, frame, "(auto-discovered same-origin iframe)");
|
|
7902
|
+
const rankResult = await Promise.resolve(target.evaluate((0, submit_control_1.buildRankSubmitCandidatesExpr)())).catch(() => null);
|
|
7903
|
+
const ranked = Array.isArray(rankResult) ? rankResult : [];
|
|
7904
|
+
if (ranked.length === 0)
|
|
7905
|
+
continue;
|
|
7906
|
+
// biome-ignore lint/style/noNonNullAssertion: guarded by the length check above
|
|
7907
|
+
const top = ranked[0];
|
|
7908
|
+
const pre = await snapshotPage(target, signalCounter, page);
|
|
7909
|
+
const clickResult = (await Promise.resolve(target.evaluate((0, submit_control_1.buildClickByDeepIndexExpr)(top.deepIndex))).catch(() => ({ clicked: false })));
|
|
7910
|
+
if (!clickResult.clicked)
|
|
7911
|
+
continue;
|
|
7912
|
+
const post = await snapshotPage(target, signalCounter, page);
|
|
7913
|
+
const verdict = (0, phantom_click_1.classifyPhantomClick)({
|
|
7914
|
+
actResultSuccess: true,
|
|
7915
|
+
pre,
|
|
7916
|
+
post,
|
|
7917
|
+
isSubmitShapedStep: true,
|
|
7918
|
+
});
|
|
7919
|
+
if (verdict !== "phantom")
|
|
7920
|
+
return target;
|
|
7921
|
+
const runnerUp = ranked[1];
|
|
7922
|
+
if (!runnerUp)
|
|
7923
|
+
continue;
|
|
7924
|
+
const runnerUpClickResult = (await Promise.resolve(target.evaluate((0, submit_control_1.buildClickByDeepIndexExpr)(runnerUp.deepIndex))).catch(() => ({ clicked: false })));
|
|
7925
|
+
if (!runnerUpClickResult.clicked)
|
|
7926
|
+
continue;
|
|
7927
|
+
const runnerUpPost = await snapshotPage(target, signalCounter, page);
|
|
7928
|
+
const runnerUpVerdict = (0, phantom_click_1.classifyPhantomClick)({
|
|
7929
|
+
actResultSuccess: true,
|
|
7930
|
+
pre,
|
|
7931
|
+
post: runnerUpPost,
|
|
7932
|
+
isSubmitShapedStep: true,
|
|
7933
|
+
});
|
|
7934
|
+
if (runnerUpVerdict !== "phantom")
|
|
7935
|
+
return target;
|
|
7936
|
+
}
|
|
7937
|
+
return null;
|
|
7938
|
+
}
|
|
7744
7939
|
async function executeStepWithHealing(params) {
|
|
7745
7940
|
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
7941
|
// Mutable: the `emailStep` code-extract path splices the extracted code
|
|
@@ -8068,7 +8263,7 @@ async function executeStepWithHealing(params) {
|
|
|
8068
8263
|
// when no solver key is configured or the solve otherwise fails — that
|
|
8069
8264
|
// propagates out of this function unchanged, so the step fails cleanly
|
|
8070
8265
|
// instead of silently falling through as if unverified-but-passing.
|
|
8071
|
-
const isSubmitOrFinalStep = submitStep || (isFinalStep && flowHasSubmitSemanticsFlag);
|
|
8266
|
+
const isSubmitOrFinalStep = submitStep || (isFinalStep && flowHasSubmitSemanticsFlag) || isSubmitIntentStep(step);
|
|
8072
8267
|
if (captchaGated && isSubmitOrFinalStep) {
|
|
8073
8268
|
let captchaTarget = frameTarget ?? (0, frame_target_1.mainFrameTarget)(page);
|
|
8074
8269
|
const sitekeyProbeExpr = `(() => {
|
|
@@ -8410,6 +8605,25 @@ async function executeStepWithHealing(params) {
|
|
|
8410
8605
|
logger.error(`${formatStepPrefix(stepIndex, totalSteps)} backend error detected (submit endpoint returned 5xx: ${backendErrorUrl}); aborting cascade`);
|
|
8411
8606
|
throw new errors_2.StepVerificationError(`${formatStepPrefix(stepIndex, totalSteps)} (${step.slice(0, 60)}) backend 5xx at ${backendErrorUrl} — unrecoverable`, "backend-error-unrecoverable");
|
|
8412
8607
|
}
|
|
8608
|
+
// Bounded same-origin child-iframe fallback: the probe above only ever
|
|
8609
|
+
// looked at the resolved (declared-or-main) frame's own light+shadow DOM,
|
|
8610
|
+
// so a target genuinely inside an undeclared same-origin iframe is
|
|
8611
|
+
// invisible to it. Only tried when this step never declared its own
|
|
8612
|
+
// frameSelector — a declared-and-used frame means the flow author already
|
|
8613
|
+
// told us where to look, and guessing elsewhere would second-guess that —
|
|
8614
|
+
// and only when this step is itself submit-shaped, since the probe ranks
|
|
8615
|
+
// for a submit-shaped candidate and firing it on a non-submit step (e.g.
|
|
8616
|
+
// a radio/select answer) could complete the step by clicking an unrelated
|
|
8617
|
+
// submit control in an unrelated same-origin child iframe.
|
|
8618
|
+
if (!frameTarget?.declaredFrameSelector &&
|
|
8619
|
+
(submitStep || (isFinalStep && flowHasSubmitSemanticsFlag) || isSubmitIntentStep(step))) {
|
|
8620
|
+
const fallbackTarget = await probeChildFrameSubmitFallback({ page, signalCounter });
|
|
8621
|
+
if (fallbackTarget) {
|
|
8622
|
+
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`);
|
|
8623
|
+
trajectory?.push({ stepIndex, verifiedBy: "dom" });
|
|
8624
|
+
return "completed";
|
|
8625
|
+
}
|
|
8626
|
+
}
|
|
8413
8627
|
// Capture diagnostics + write a failure dump BEFORE throwing so the
|
|
8414
8628
|
// global replan path's `readFailureDumpEvidence` can populate the
|
|
8415
8629
|
// prompt's CURRENTLY VISIBLE / UNFOCUSED OBSERVE / PAGE BODY HTML
|
|
@@ -8433,6 +8647,9 @@ async function executeStepWithHealing(params) {
|
|
|
8433
8647
|
const unfocusedObserve = probeAbsentObservedUnfocused.length === 0 && frameTarget?.frame
|
|
8434
8648
|
? await deepLocatorCandidatesAsActions(page, frameTarget)
|
|
8435
8649
|
: probeAbsentObservedUnfocused;
|
|
8650
|
+
const targetResolutionDiagnostic = submitStep || (isFinalStep && flowHasSubmitSemanticsFlag) || isSubmitIntentStep(step)
|
|
8651
|
+
? await (0, target_resolution_diagnostic_1.captureTargetResolutionDiagnosticSnapshot)(frameTarget, page)
|
|
8652
|
+
: null;
|
|
8436
8653
|
const dumpPath = onStepFailure?.({
|
|
8437
8654
|
stepIndex,
|
|
8438
8655
|
phase,
|
|
@@ -8444,6 +8661,7 @@ async function executeStepWithHealing(params) {
|
|
|
8444
8661
|
recentCaptures,
|
|
8445
8662
|
bodyOuterHtml,
|
|
8446
8663
|
unfocusedObserve,
|
|
8664
|
+
targetResolutionDiagnostic,
|
|
8447
8665
|
}) ?? null;
|
|
8448
8666
|
throw new errors_2.StepVerificationError(`${formatStepPrefix(stepIndex, totalSteps)} (${step.slice(0, 60)}) probe found no candidates on page${dumpPath ? `; see ${dumpPath}` : ""}`, "probe-absent");
|
|
8449
8667
|
}
|
|
@@ -8705,6 +8923,13 @@ async function executeStepWithHealing(params) {
|
|
|
8705
8923
|
}
|
|
8706
8924
|
};
|
|
8707
8925
|
let phantomClickAfterAttempt1 = false;
|
|
8926
|
+
let attempt1UnreachableViaLightDom = false;
|
|
8927
|
+
// Last candidate list the deep-submit-locator itself ranked this step, if
|
|
8928
|
+
// any — reused by the cascade-exhausted diagnostic snapshot below so it
|
|
8929
|
+
// doesn't re-run buildRankSubmitCandidatesExpr (and inflate the rank-call
|
|
8930
|
+
// count the phantom-click-escalation tests assert on) when the cascade
|
|
8931
|
+
// already has an up-to-date ranking to report from.
|
|
8932
|
+
let lastRankedSubmitCandidates = null;
|
|
8708
8933
|
for (let attempt = 1; attempt <= MAX_STEP_ATTEMPTS; attempt++) {
|
|
8709
8934
|
// Telemetry-driven technique-skip: when a cascade technique's
|
|
8710
8935
|
// preconditions cannot be met by the prior attempts' state, running
|
|
@@ -8713,7 +8938,8 @@ async function executeStepWithHealing(params) {
|
|
|
8713
8938
|
// techniques faster.
|
|
8714
8939
|
if (attempt > 1) {
|
|
8715
8940
|
const wouldBeTechnique = attempt === 2
|
|
8716
|
-
?
|
|
8941
|
+
? attempt1UnreachableViaLightDom &&
|
|
8942
|
+
(submitStep || (isFinalStep && flowHasSubmitSemanticsFlag) || isSubmitIntentStep(step))
|
|
8717
8943
|
? "deep-submit-locator"
|
|
8718
8944
|
: phantomClickAfterAttempt1
|
|
8719
8945
|
? "trusted-click-retry"
|
|
@@ -8731,8 +8957,8 @@ async function executeStepWithHealing(params) {
|
|
|
8731
8957
|
errorMessage: a.errorMessage,
|
|
8732
8958
|
})),
|
|
8733
8959
|
advanceUnmovedAfterAttempt1,
|
|
8734
|
-
|
|
8735
|
-
submitShapedStep: submitStep || (isFinalStep && flowHasSubmitSemanticsFlag),
|
|
8960
|
+
attempt1UnreachableViaLightDom,
|
|
8961
|
+
submitShapedStep: submitStep || (isFinalStep && flowHasSubmitSemanticsFlag) || isSubmitIntentStep(step),
|
|
8736
8962
|
});
|
|
8737
8963
|
if (decision.skip) {
|
|
8738
8964
|
logger.info(`${formatStepPrefix(stepIndex, totalSteps)} attempt ${attempt} (${wouldBeTechnique}) skipped: ${decision.reason}`);
|
|
@@ -8869,13 +9095,14 @@ async function executeStepWithHealing(params) {
|
|
|
8869
9095
|
}
|
|
8870
9096
|
}
|
|
8871
9097
|
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
|
-
//
|
|
9098
|
+
attempt1UnreachableViaLightDom &&
|
|
9099
|
+
(submitStep || (isFinalStep && flowHasSubmitSemanticsFlag) || isSubmitIntentStep(step))) {
|
|
9100
|
+
// Deep submit-control locator: attempt 1 either phantom-clicked
|
|
9101
|
+
// (Stagehand reported success but pre/post showed zero effect) or
|
|
9102
|
+
// came back "unresolved" (act() found zero candidates at all), so the
|
|
9103
|
+
// target is almost certainly unreachable via document.querySelectorAll
|
|
9104
|
+
// — most likely rendered inside an open shadow root by a web-component
|
|
9105
|
+
// / framework-native submit control (see recon-submit-phantom-click bug
|
|
8879
9106
|
// report). Rank every submit-shaped candidate the deep traversal can
|
|
8880
9107
|
// reach and click the top-ranked one; the ranking already excludes
|
|
8881
9108
|
// Back/Cancel/Save-draft-shaped controls, so a false-positive submit
|
|
@@ -8902,6 +9129,7 @@ async function executeStepWithHealing(params) {
|
|
|
8902
9129
|
record.errorMessage = `deep-submit-locator: rank evaluate threw ${(0, errors_1.toErrorMessage)(err)}`;
|
|
8903
9130
|
break;
|
|
8904
9131
|
}
|
|
9132
|
+
lastRankedSubmitCandidates = ranked;
|
|
8905
9133
|
if (ranked.length === 0) {
|
|
8906
9134
|
record.errorMessage = "deep-submit-locator: no submit-shaped candidate found";
|
|
8907
9135
|
break;
|
|
@@ -8994,7 +9222,7 @@ async function executeStepWithHealing(params) {
|
|
|
8994
9222
|
}
|
|
8995
9223
|
else if (attempt === 2 &&
|
|
8996
9224
|
phantomClickAfterAttempt1 &&
|
|
8997
|
-
!(submitStep || (isFinalStep && flowHasSubmitSemanticsFlag))) {
|
|
9225
|
+
!(submitStep || (isFinalStep && flowHasSubmitSemanticsFlag) || isSubmitIntentStep(step))) {
|
|
8998
9226
|
// Trusted-click retry: attempt 1 phantom-clicked a NON-submit control —
|
|
8999
9227
|
// Stagehand reported success but pre/post showed zero effect. On a
|
|
9000
9228
|
// design-system widget (React synthetic-event delegation, custom
|
|
@@ -9674,7 +9902,7 @@ async function executeStepWithHealing(params) {
|
|
|
9674
9902
|
// `advanceTransitionBodyPattern` are unaffected.
|
|
9675
9903
|
const domVerifiedForStep = isDomOnlyAdvanceVerified({
|
|
9676
9904
|
hasPattern: advanceTransitionBodyPattern !== null,
|
|
9677
|
-
isFinalOrSubmit: submitStep || (isFinalStep && flowHasSubmitSemanticsFlag),
|
|
9905
|
+
isFinalOrSubmit: submitStep || (isFinalStep && flowHasSubmitSemanticsFlag) || isSubmitIntentStep(step),
|
|
9678
9906
|
isAdvance: isAdvanceStep(step),
|
|
9679
9907
|
domVerified,
|
|
9680
9908
|
networkIsRealAdvance,
|
|
@@ -9762,7 +9990,8 @@ async function executeStepWithHealing(params) {
|
|
|
9762
9990
|
// gate (requireSubmitEndpoint) will re-litigate the credit below.
|
|
9763
9991
|
const formValueWeakSignalAllowed = !(submitStep ||
|
|
9764
9992
|
(isFinalStep && flowHasSubmitSemanticsFlag) ||
|
|
9765
|
-
resolvedElementIsSubmitShaped
|
|
9993
|
+
resolvedElementIsSubmitShaped ||
|
|
9994
|
+
isSubmitIntentStep(step)) || requireSubmitEndpoint;
|
|
9766
9995
|
const formValueVerified = isStateClass &&
|
|
9767
9996
|
formValueWeakSignalAllowed &&
|
|
9768
9997
|
post.formValueSignature !== pre.formValueSignature;
|
|
@@ -9868,7 +10097,7 @@ async function executeStepWithHealing(params) {
|
|
|
9868
10097
|
pre,
|
|
9869
10098
|
post,
|
|
9870
10099
|
elementStateChanged: domVerified,
|
|
9871
|
-
isSubmitShapedStep: submitStep || (isFinalStep && flowHasSubmitSemanticsFlag),
|
|
10100
|
+
isSubmitShapedStep: submitStep || (isFinalStep && flowHasSubmitSemanticsFlag) || isSubmitIntentStep(step),
|
|
9872
10101
|
});
|
|
9873
10102
|
// An `"effective"` verdict driven purely by the page-wide byte-delta
|
|
9874
10103
|
// floor (`TRIVIAL_DOM_DELTA_BYTES`, 500B) is intentionally NOT trusted
|
|
@@ -9889,7 +10118,8 @@ async function executeStepWithHealing(params) {
|
|
|
9889
10118
|
// respects the SAME DOM-only-advance veto as the block above: a wizard
|
|
9890
10119
|
// "Next" step on a pattern-configured site whose only signal is a field
|
|
9891
10120
|
// toggle must stay unverified, not get waved through by the verdict.
|
|
9892
|
-
const domEffectiveVerdict = !(submitStep || (isFinalStep && flowHasSubmitSemanticsFlag)) &&
|
|
10121
|
+
const domEffectiveVerdict = !(submitStep || (isFinalStep && flowHasSubmitSemanticsFlag) || isSubmitIntentStep(step)) &&
|
|
10122
|
+
domVerifiedForStep;
|
|
9893
10123
|
let verified = networkIsRealAdvance ||
|
|
9894
10124
|
urlChanged ||
|
|
9895
10125
|
domVerifiedForStep ||
|
|
@@ -10140,7 +10370,7 @@ async function executeStepWithHealing(params) {
|
|
|
10140
10370
|
const n16TrustedClickFailure = !n16TrustedDelivered && "reason" in n16TrustedClickResult
|
|
10141
10371
|
? n16TrustedClickResult
|
|
10142
10372
|
: 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)}) {
|
|
10373
|
+
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
10374
|
const n16FallbackTarget = frameTarget ?? (0, frame_target_1.mainFrameTarget)(page);
|
|
10145
10375
|
const probeResult = (await n16FallbackTarget.evaluate(clickExpr));
|
|
10146
10376
|
const fired = probeResult.fired;
|
|
@@ -10159,8 +10389,7 @@ async function executeStepWithHealing(params) {
|
|
|
10159
10389
|
const r = document.evaluate(${JSON.stringify(xpath)}, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
|
|
10160
10390
|
let node = r.singleNodeValue;
|
|
10161
10391
|
if (!node && ${JSON.stringify(xpathTail)}) {
|
|
10162
|
-
|
|
10163
|
-
node = r2.singleNodeValue;
|
|
10392
|
+
node = ${XPATH_TAIL_RETARGET_RESOLVE_FN_SRC}(${JSON.stringify(xpathTail)});
|
|
10164
10393
|
}
|
|
10165
10394
|
if (!node) return false;
|
|
10166
10395
|
for (let depth = 0; depth < 6 && node; depth++) {
|
|
@@ -10243,8 +10472,7 @@ async function executeStepWithHealing(params) {
|
|
|
10243
10472
|
const r = document.evaluate(${JSON.stringify(xpath)}, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
|
|
10244
10473
|
let el = r.singleNodeValue;
|
|
10245
10474
|
if (!el && ${JSON.stringify(xpathTail)}) {
|
|
10246
|
-
|
|
10247
|
-
el = r2.singleNodeValue;
|
|
10475
|
+
el = ${XPATH_TAIL_RETARGET_RESOLVE_FN_SRC}(${JSON.stringify(xpathTail)});
|
|
10248
10476
|
}
|
|
10249
10477
|
return el ? isDisabled(el) : false;
|
|
10250
10478
|
})()`;
|
|
@@ -10284,7 +10512,7 @@ async function executeStepWithHealing(params) {
|
|
|
10284
10512
|
}));
|
|
10285
10513
|
const fallbackDomOnlyAdvance = shouldVetoFallbackAdvance({
|
|
10286
10514
|
hasPattern: advanceTransitionBodyPattern !== null,
|
|
10287
|
-
isFinalOrSubmit: submitStep || (isFinalStep && flowHasSubmitSemanticsFlag),
|
|
10515
|
+
isFinalOrSubmit: submitStep || (isFinalStep && flowHasSubmitSemanticsFlag) || isSubmitIntentStep(step),
|
|
10288
10516
|
isAdvance: isAdvanceStep(step),
|
|
10289
10517
|
retryUrlChanged,
|
|
10290
10518
|
retryNetworkIsRealAdvance,
|
|
@@ -10345,7 +10573,8 @@ async function executeStepWithHealing(params) {
|
|
|
10345
10573
|
// value is read below.
|
|
10346
10574
|
const retrySubmitShaped = submitStep ||
|
|
10347
10575
|
(isFinalStep && flowHasSubmitSemanticsFlag) ||
|
|
10348
|
-
retryResolvedElementIsSubmitShaped
|
|
10576
|
+
retryResolvedElementIsSubmitShaped ||
|
|
10577
|
+
isSubmitIntentStep(step);
|
|
10349
10578
|
const weakDomSignalsAllowed = ((!isFinalStep && !submitStep && !retryResolvedElementIsSubmitShaped) ||
|
|
10350
10579
|
requireSubmitEndpoint) &&
|
|
10351
10580
|
!isCheckboxOrRadioIntentStep(step) &&
|
|
@@ -10568,7 +10797,7 @@ async function executeStepWithHealing(params) {
|
|
|
10568
10797
|
// dumps in a 2026-06-10 survey had the paired touched+dirty + visible
|
|
10569
10798
|
// error text pattern with 3 distinct rejection messages.
|
|
10570
10799
|
if (record.resolvedMethod === "click" &&
|
|
10571
|
-
(submitStep || (isFinalStep && flowHasSubmitSemanticsFlag))) {
|
|
10800
|
+
(submitStep || (isFinalStep && flowHasSubmitSemanticsFlag) || isSubmitIntentStep(step))) {
|
|
10572
10801
|
const live = await extractLivePageFormEvidence(page, frameTarget ?? (0, frame_target_1.mainFrameTarget)(page), {
|
|
10573
10802
|
client: anthropic,
|
|
10574
10803
|
knownErrorClassPrefixes,
|
|
@@ -10604,26 +10833,44 @@ async function executeStepWithHealing(params) {
|
|
|
10604
10833
|
isAdvanceStep(step) &&
|
|
10605
10834
|
!urlChanged &&
|
|
10606
10835
|
!networkIsRealAdvance;
|
|
10607
|
-
// Attempt 1
|
|
10608
|
-
//
|
|
10609
|
-
//
|
|
10610
|
-
//
|
|
10611
|
-
//
|
|
10612
|
-
//
|
|
10836
|
+
// Attempt 1's classifyPhantomClick verdict: "phantom" means Stagehand
|
|
10837
|
+
// reported success but pre/post shows zero observable effect; the
|
|
10838
|
+
// zero-effect delta is the primary signal there (classifyPhantomClick
|
|
10839
|
+
// above). "unresolved" means act() found zero candidates at all — a
|
|
10840
|
+
// strictly stronger signal that the target is unreachable via the
|
|
10841
|
+
// light-DOM resolver, since there wasn't even a candidate to click. The
|
|
10842
|
+
// live AISDK elementId suppression counter is corroborating evidence
|
|
10843
|
+
// only — logged, never gating, since a nonzero count alone is too weak
|
|
10844
|
+
// a signal on a run that sees dozens of suppressions across hundreds of
|
|
10845
|
+
// unrelated steps.
|
|
10613
10846
|
phantomClickAfterAttempt1 = record.phantomClickVerdict === "phantom";
|
|
10614
|
-
|
|
10847
|
+
const attempt1Unresolved = record.phantomClickVerdict === "unresolved";
|
|
10848
|
+
attempt1UnreachableViaLightDom = phantomClickAfterAttempt1 || attempt1Unresolved;
|
|
10849
|
+
if (attempt1UnreachableViaLightDom) {
|
|
10615
10850
|
const suppressedCount = getSuppressedAisdkElementIdErrorCount?.();
|
|
10616
|
-
const
|
|
10851
|
+
const verdictDescription = phantomClickAfterAttempt1
|
|
10852
|
+
? "phantom click detected"
|
|
10853
|
+
: "no candidates resolved (unresolved verdict)";
|
|
10854
|
+
const outcomeDescription = phantomClickAfterAttempt1
|
|
10855
|
+
? "reported success with no network/url/dom change"
|
|
10856
|
+
: "act() found zero candidates";
|
|
10857
|
+
// trusted-click-retry needs attempt 1's resolved selector
|
|
10858
|
+
// (triedSelectors), which is empty on an "unresolved" verdict, so a
|
|
10859
|
+
// non-submit step with that verdict falls through to the normal
|
|
10860
|
+
// ladder instead of escalating.
|
|
10861
|
+
const escalationTarget = submitStep || (isFinalStep && flowHasSubmitSemanticsFlag) || isSubmitIntentStep(step)
|
|
10617
10862
|
? "escalating attempt 2 to deep-submit-locator"
|
|
10618
|
-
:
|
|
10619
|
-
|
|
10863
|
+
: phantomClickAfterAttempt1
|
|
10864
|
+
? "non-submit step — escalating attempt 2 to trusted-click-retry (trusted CDP click on the resolved target)"
|
|
10865
|
+
: "non-submit step — no resolved target for trusted-click-retry; attempt 2 continues the normal ladder";
|
|
10866
|
+
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
10867
|
}
|
|
10621
10868
|
const postAttemptInvalidCount = await countNgInvalidContainers(frameTarget ?? (0, frame_target_1.mainFrameTarget)(page));
|
|
10622
10869
|
const earlyExit = isSubmitRevealedInvalid({
|
|
10623
10870
|
// Treat the canonical submit click as "final" for this predicate
|
|
10624
10871
|
// even when it lives mid-flow. See requireSubmitEndpoint derivation
|
|
10625
10872
|
// above for the same gate-widening rationale.
|
|
10626
|
-
isFinalStep: submitStep || (isFinalStep && flowHasSubmitSemanticsFlag),
|
|
10873
|
+
isFinalStep: submitStep || (isFinalStep && flowHasSubmitSemanticsFlag) || isSubmitIntentStep(step),
|
|
10627
10874
|
requireSubmitEndpoint,
|
|
10628
10875
|
resolvedMethod: record.resolvedMethod,
|
|
10629
10876
|
effectSignals,
|
|
@@ -10643,7 +10890,7 @@ async function executeStepWithHealing(params) {
|
|
|
10643
10890
|
// can reorder a later step forward — instead of burning the cascade.
|
|
10644
10891
|
const advanceStalled = isAdvanceStalled({
|
|
10645
10892
|
isAdvance: isAdvanceStep(step),
|
|
10646
|
-
isFinalOrSubmit: submitStep || (isFinalStep && flowHasSubmitSemanticsFlag),
|
|
10893
|
+
isFinalOrSubmit: submitStep || (isFinalStep && flowHasSubmitSemanticsFlag) || isSubmitIntentStep(step),
|
|
10647
10894
|
hasPattern: advanceTransitionBodyPattern !== null,
|
|
10648
10895
|
clickFired: record.resolvedMethod === "click" && record.actResultSuccess === true,
|
|
10649
10896
|
networkFired,
|
|
@@ -10658,6 +10905,26 @@ async function executeStepWithHealing(params) {
|
|
|
10658
10905
|
}
|
|
10659
10906
|
}
|
|
10660
10907
|
}
|
|
10908
|
+
// Bounded same-origin child-iframe fallback: every attempt above only ever
|
|
10909
|
+
// operated against the resolved (declared-or-main) frame, so a target
|
|
10910
|
+
// genuinely inside an undeclared same-origin iframe was invisible to all of
|
|
10911
|
+
// them. Only tried when this step never declared its own frameSelector — a
|
|
10912
|
+
// declared-and-used frame means the flow author already told us where to
|
|
10913
|
+
// look, so this can only ever fire on a case that today always fails
|
|
10914
|
+
// (no regression risk to any currently-passing path) — and only when this
|
|
10915
|
+
// step is itself submit-shaped, matching every other
|
|
10916
|
+
// buildRankSubmitCandidatesExpr call site's guard in this file, since
|
|
10917
|
+
// firing on a non-submit step could complete it by clicking an unrelated
|
|
10918
|
+
// submit control in an unrelated same-origin child iframe.
|
|
10919
|
+
if (!frameTarget?.declaredFrameSelector &&
|
|
10920
|
+
(submitStep || (isFinalStep && flowHasSubmitSemanticsFlag) || isSubmitIntentStep(step))) {
|
|
10921
|
+
const fallbackTarget = await probeChildFrameSubmitFallback({ page, signalCounter });
|
|
10922
|
+
if (fallbackTarget) {
|
|
10923
|
+
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`);
|
|
10924
|
+
trajectory?.push({ stepIndex, verifiedBy: "dom" });
|
|
10925
|
+
return "completed";
|
|
10926
|
+
}
|
|
10927
|
+
}
|
|
10661
10928
|
const cascadeExhaustObservedFinal = await (0, stagehand_guard_1.guardedObserve)(stagehand, step, { timeout: exports.STEP_WATCHDOG_MS }, captureFn, frameTarget).catch(() => []);
|
|
10662
10929
|
// observe() is blind to a cross-origin OOPIF, so a frame-scoped empty
|
|
10663
10930
|
// result degrades to the deep-locator resolver for dump evidence — this
|
|
@@ -10680,6 +10947,9 @@ async function executeStepWithHealing(params) {
|
|
|
10680
10947
|
const unfocusedObserve = cascadeExhaustObservedUnfocused.length === 0 && frameTarget?.frame
|
|
10681
10948
|
? await deepLocatorCandidatesAsActions(page, frameTarget)
|
|
10682
10949
|
: cascadeExhaustObservedUnfocused;
|
|
10950
|
+
const targetResolutionDiagnostic = submitStep || (isFinalStep && flowHasSubmitSemanticsFlag) || isSubmitIntentStep(step)
|
|
10951
|
+
? await (0, target_resolution_diagnostic_1.captureTargetResolutionDiagnosticSnapshot)(frameTarget, page, lastRankedSubmitCandidates)
|
|
10952
|
+
: null;
|
|
10683
10953
|
const dumpPath = onStepFailure?.({
|
|
10684
10954
|
stepIndex,
|
|
10685
10955
|
phase,
|
|
@@ -10691,6 +10961,7 @@ async function executeStepWithHealing(params) {
|
|
|
10691
10961
|
recentCaptures,
|
|
10692
10962
|
bodyOuterHtml,
|
|
10693
10963
|
unfocusedObserve,
|
|
10964
|
+
targetResolutionDiagnostic,
|
|
10694
10965
|
}) ?? null;
|
|
10695
10966
|
if (dumpPath !== null) {
|
|
10696
10967
|
logger.error(`${formatStepPrefix(stepIndex, totalSteps)} failed after ${MAX_STEP_ATTEMPTS} attempts; diagnostic bundle: ${dumpPath}`);
|